Всё началось с принтера. Точнее — с 1700 рублей, типографии на Театральной и фотографии моря в Абхазии. Кадр был невероятный: бирюзовая вода, низкое солнце, плачущие эвкалипты, и такой цвет, что хотелось окунуться в дисплей. Я выбрал баритовую бумагу, хотел потом вставить в рамку. Прождал сорок минут в очереди и... На выходе грязно-голубая лужа.
Нормальный человек сказал бы «плохая типография» и пошёл дальше. Но у меня профдеформация, я полез дебажить цвет. И через пару вечеров кроличьей норы и экспериментов на коленке я знал о мониторах столько, что мне стало физически некомфортно на них смотреть.

Прежде чем мы дойдём до кода (куда же без него) — короткая вводная.
Монитор показывает три цвета. Красный, зелёный, синий.
Возьмите лупу, а лучше камеру телефона, и поднесите к экрану. Там решётка из крошечных фонариков. Когда монитор хочет «жёлтый» — зажигает красный и зелёный рядом. Никакого жёлтого фотона не рождается. Ваш мозг видит два огонька и галлюцинирует жёлтый. «Белый» — все три на максимум. Три разноцветных точки, а мозг рапортует: белый, чистый, красивый.

Это работает из-за штуки под названием метамерия — у нас три типа колбочек в сетчатке, и разные комбинации длин волн могут вызывать одинаковый отклик. Монитор не воспроизводит спектр объекта. Он подбирает такой коктейль из трёх огоньков, чтобы ваши колбочки не заметили подмену. И это не «приблизительный», а математически идентичный отклик. Мозг физически не может отличить.
(Раки-богомолы (который изображён на превью статьи), имеют 16 типов фоторецепторов. Им бы наши мониторы казались мерцающим мусором. Но у них нет Хабра.)
Окей, это всё прикольно, но вот в чём засада. Три фонарика покрывают не все цвета, которые видит глаз. sRGB — стандартное пространство большинства мониторов — это ~35% видимых цветов. Остальные 65% ваш экран показать не может. Он показывает ближайшее, что умеет.
Мой абхазский бирюзовый был из тех 65%.
Вот на этом месте я сказал «ладно, может я хотя бы узнаю, насколько сильно монитор врёт?». И пошёл к ChatGPT.
Акт первый, в котором ChatGPT уверенно решает не ту задачу
Промпт был простой: «Напиши на Python программу, которая принимает спектральные данные цвета и показывает, как он выглядит на мониторе с sRGB-гаммой, и как выглядел бы в реальности. Визуализируй разницу.»
Тридцать секунд — и передо мной идеальный код. colour-science, PIL.ImageCms, конвертация XYZ → Lab → sRGB, matplotlib. Чистый, с docstring'ами, даже вежливые коменты добавил. Я прям умилился.
Запускаю. Два квадрата: «истинный цвет» и «цвет на мониторе». Оба одинаковые.

Я секунд двадцать смотрел на экран, пытаясь понять. Потом полез в код. GPT использовал sRGB как исходное и целевое пространство. Сконвертировал sRGB в sRGB и гордо доложил, что они совпадают.
Это как если бы я спросил «сколько я плачу за квартиру», а мне ответили «ваша квартира стоит столько, сколько вы за неё платите». Спасибо, очень информативно.
Акт второй, в котором GPT выдумывает спектр «по вайбу»
Ладно, сам виноват — нечётко сформулировал. Пишу уточнение: нужна спектральная конвертация, из спектрального распределения мощности через функции цветового соответствия CIE 1931, в XYZ, и уже потом в sRGB. И «истинный цвет» — это XYZ-координаты, а «цвет на мониторе» — после gamut clipping'а.
Второй ответ лучше. Код реально использует colour.sd_to_XYZ(). Прогресс. Но спектр GPT захардкодил как плоскую линию от 380 до 780 нм. Это идеальный белый свет. Я, значит, спрашиваю «почему мой бирюзовый не печатается», а мне моделируют белый.
Прошу подставить реальный спектр бирюзовой воды.
GPT генерирует гауссиан с пиком на 490 нм. С комментарием: «Typical turquoise water reflectance spectrum.» Типичный. Он его выдумал. Просто нарисовал колокольчик «по вайбу» и подписал «типичный». И это бы даже прокатило — для первого приближения форма не безумная — но я-то хотел мой конкретный бирюзовый, а не среднестатистическую бирюзу из вакуума.

Акт третий, в котором я делаю сам
Окей. Закрыл чат с GPT. Нашел сайт с базами спектров отражения для морской воды при разных условиях. Нашёл данные по Чёрному морю. CSV, 401 точка, 380–780 нм, шаг 1 нм. Настоящие данные, снятые спектрофотометром.
Дальше руками:
import colour import numpy as np # Загружаем РЕАЛЬНЫЙ спектр, а не галлюцинацию data = np.loadtxt('black_sea_turquoise.csv', delimiter=',') sd = colour.SpectralDistribution( dict(zip(data[:, 0], data[:, 1])), name='Black Sea Turquoise' ) # Спектр → XYZ → sRGB XYZ = colour.sd_to_XYZ(sd, illuminant=colour.SDS_ILLUMINANTS['D65']) srgb_raw = colour.XYZ_to_sRGB(XYZ / 100) # момент истины srgb_clipped = np.clip(srgb_raw, 0, 1) # Считаем Delta E — насколько заметна разница для глаза Lab_true = colour.XYZ_to_Lab(XYZ / 100) Lab_clipped = colour.XYZ_to_Lab(colour.sRGB_to_XYZ(srgb_clipped)) delta_e = colour.delta_E(Lab_true, Lab_clipped, method='CIE 2000') print(f'sRGB raw: {srgb_raw}') print(f'sRGB clipped: {srgb_clipped}') print(f'Delta E: {delta_e:.1f}') print(f'Вне гаммы: {any(c < 0 or c > 1 for c in srgb_raw)}')
Запускаю.
sRGB raw: [0.2847, 1.0731, 0.7892] sRGB clipped: [0.2847, 1.0000, 0.7892] Delta E: 4.3 Вне гаммы: True
Зелёная компонента — 1.07. Монитору нужно выдать 107% яркости зелёного субпикселя. Физически невозможно. Delta E 4.3 — перцептивно заметная разница. Не «два одинаковых квадрата», как мне GPT нарисовал.
А потом я ради интереса посчитал Delta E до CMYK (то, что делает принтер). 11.2. Одиннадцать. Это уже «совсем другой цвет». Это моя грязно-голубая лужа на баритовой бумаге.
Типография ни при чём. Виновата была физика.
Я не остановился
Следующий шаг был очевиден, а давайте проверим все пиксели фотографии? Берём RAW-файл (не JPEG — в нём уже всё обрезано до sRGB, информация потеряна), конвертируем через rawpy в XYZ, и пиксель за пикселем смотрим, кто вне гаммы.
import rawpy raw = rawpy.imread('abkhazia_sunset.CR3') rgb_linear = raw.postprocess( output_color=rawpy.ColorSpace.XYZ, no_auto_bright=True, gamma=(1, 1) ) srgb = colour.XYZ_to_sRGB(rgb_linear / rgb_linear.max()) out_of_gamut = np.any((srgb < 0) | (srgb > 1), axis=-1) print(f'Пикселей вне гаммы: {100 * out_of_gamut.sum() / out_of_gamut.size:.1f}%')
Двадцать три процента пикселей вранье. Практически каждый четвёртый. И максимальная концентрация именно в зоне воды, в том самом бирюзовом, из-за которого всё началось.
Я сделал heatmap: красное — монитор сильно врёт, зелёное — почти честен. Фотография выглядела как температурная карта пациента с пятнистой лихорадкой. Море — практически красное. Небо — частично жёлтое. Песок и камни — почти всё зелёное. Справедливости ради в передаче бежевого sRGB вполне честен.

Окей, а можно починить?
Оказывается, инженеры пытаются решить проблему «три фонарика — это мало» с 1953 года, когда NTSC выбрал RGB-фосфоры для цветного ТВ. Три — это минимум, при котором метамерия ещё работает. С тех пор каждое десятилетие кто-нибудь пытается сделать лучше.
Sharp в 2010-м добавил жёлтый субпиксель (Quattron). Логика железная: жёлто-зелёная область — самое слабое место RGB-треугольника, четвёртый субпиксель расширит охват. Джордж Такей (Сулу из Star Trek) снялся в рекламе. Проблема: весь контент в мире закодирован в три канала. Телевизор должен был выдумывать жёлтую компоненту. Алгоритм работал... ну, на тестовых градиентах впечатляюще, а на реальном видео — неотличимо от хорошей RGB-панели. Через несколько лет свернули.
Но в медицинских мониторах идея живёт. NEC и Barco делают панели с 5-6 субпикселями — RGB + голубой + жёлтый + фиолетовый. Стоят от 3 до 15 миллионов рублей. Там вопрос не «красиво», а «хирург должен отличить живую ткань от некроза», и один оттенок розового — буквально жизнь или смерть.
Canon и Toshiba в начале 2000-х потратили миллиард долларов на SED — дисплей, где каждый субпиксель имеет собственный микроскопический электронный эмиттер. По сути — плоский ЭЛТ. Контрастность как у кинескопа, время отклика в микросекундах, цветопередача определяется фосфорами — бери любые. Прототип 36" показали на CEATEC 2006, обзоры были восторженные. Потом патентная война, производственный ад (обеспечить однородную эмиссию на миллионах точек — задачка на миллиард), и LCD тем временем подешевели в десять раз. Проект закрыли. Миллиард. В корзину.
Лазерные дисплеи — вот где теория идеальна. Ширина спектральной линии лазера — меньше 1 нм, против 30-50 нм у обычного LED. Три лазера дают три абсолютно чистых цвета, треугольник на CIE-диаграмме — максимально широкий из возможных. Christie CP4325-RGB покрывает 96% Rec. 2020. Почему не в каждом мониторе? Спекл-шум — лазерный свет когерентный и создаёт зернистую интерференцию на однородных поверхностях. Борются вибрирующими диффузорами и MEMS-зеркалами. Некоторые стартапы вообще развернулись и стали использовать спекл для 3D-сканирования и баг стал фичей.
Мониторы за миллионы, которые выглядят хуже вашего
Мой любимый парадокс из всей этой истории.
Sony BVM-HX3110. 6-8 миллионов рублей. Меньше разрешение, чем у вашего игрового за 35 тысяч. Тусклее подсветка. Частота обновления — ниже. И за ними очередь из киностудий, типографий и музеев. Потому что они очень точные.
Когда колорист на киностудии грейдит очередной сезон сериала, он сидит за таким монитором. Этот монитор пытается показать правду. Если на нём тень чуть зеленоватая, то она будет чуть зеленоватой на 94% телевизоров в мире. Потому что BVM — точка отсчёта, эталон, от которого пляшут все остальные.
А ваш MSI с «vivid mode» нагло перенасыщает всё подряд, потому что в магазине рядом с конкурентами надо казаться ярче. Думаю это осознанное бизнес-решение.
Я, кстати, зашёл на Авито посмотреть б/у эталонные мониторы. EIZO CG277 2014 года — 180 тысяч. За 2014 год. Десятилетний монитор, дороже нового игрового. Потому что точность не обесценивается.
Монитор ещё и деградирует
Монитор врёт, но он хотя бы врёт стабильно? Нет. Со временем враньё плывёт.
OLED: синий субпиксель деградирует в 3-4 раза быстрее красного — 15 000 часов полураспада против 50 000. Если вы по 10 часов в день пишете код на белом фоне — через два года синий в зоне редактора будет тусклее, чем в таскбаре. Burn-in. И ваш ICC-профиль, который калибровался под свежую панель, теперь описывает монитор, которого больше нет. Gamut mapping едет в произвольную сторону.
LG воткнула белый субпиксель в WOLED, чтобы разгрузить синий. Samsung в QD-OLED пустила все субпиксели синими, а красный и зелёный получает через квантовые точки (сульфид индия — кадмий убрали из-за RoHS). Pixel shifting, компенсационные циклы ночью... Но физику деградации органики не обманешь.
IPS: засветы по углам — подсветка просачивается мимо кристаллов там, где рамка давит неравномерно. IPS glow — не дефект вашего экземпляра, а свойство технологии: под углом поляризация нарушается. Лечится одним способом — не смотреть под углом. Ну или купить VA и терпеть смазы.
Битые пиксели: ISO 9241-307 допускает до 2 ярких и 2 тёмных дефектных субпикселя для мониторов второго класса. Вам могут продать экран с четырьмя битыми точками и (если я правильно все понял) это не брак.
Gamut mapping, или софтверное вранье поверх хардверного
np.clip(srgb_raw, 0, 1) это самый тривиальный способ обработки цветов вне гаммы — просто обрезать. Компонента больше единицы? Ставим единицу. Всё. Бирюзовый превращается в просто «ярко-голубой». Десять разных оттенков схлопываются в один.
Это называется clipping, и именно это делает большинство программ по умолчанию.
Есть способ поумнее — perceptual mapping: все цвета пропорционально «сжимаются» к центру, чтобы самые насыщенные влезли. Отношения между цветами сохраняются, но всё становится чуть тусклее — даже то, что и так в гамме. Ваш монитор прямо сейчас, скорее всего, работает в этом режиме.
И есть saturation mapping — приоритет яркости. Ваш коллега с кислотно-зелёными графиками в PowerPoint невольный фанат saturation mapping.
Вместо заключения
Я не перепечатал ту фотографию. Повесил на стену, с грязно-голубой водой. Грустно конечно, что 23% пикселей вранье. Delta E 4.3 на бирюзовом. 11.2 в CMYK.
Какой из этих цветов «настоящий»? На мониторе три фонарика обманывают мои колбочки. На бумаге четыре краски отражают свет, который тоже обманывает колбочки. «Настоящий» бирюзовый остался в Абхазии, в июле, в нейронной сети моего зрительного нерва. Которая тоже, строго говоря, врала — три типа колбочек и двести миллионов лет эволюционных компромиссов.
