Channel mixer, матрица 3×3: что сохраняет, где клипует, где работает
Автор: Anastasiia Butova, инженер по ComfyUI и диффузионным моделям, Белград · опубликовано 2026-09-23
Перевод английской версии: English
После перцептивных инструментов диапазонов из части 4 channel mixer — самый грубый инструмент в моей колор-студии: три выходных канала, каждый из которых — взвешенная сумма трёх входных, всего девять чисел. Код занимает несколько строк. Интересные вопросы — вокруг него: какой RGB он смешивает, что значит «сохранить», что происходит за пределами 0..1 и где он стоит в цепочке. Я добавила его в студию 20 сентября. Ниже — как он работает и, так же прямо, чего он не делает.
Вся операция
[ R′ ] [ m00 m01 m02 ] [ R ]
[ G′ ] = [ m10 m11 m12 ] [ G ]
[ B′ ] [ m20 m21 m22 ] [ B ]
Каждый коэффициент должен быть конечным и лежать в [−4, 4]; по умолчанию — единичная матрица. В C++-движке (color.cpp) умножение и его стадия выглядят так:
Vec3 applyChannelMatrix(const std::array<double, 9>& matrix, const Vec3& encodedRgb) {
return {
matrix[0] * encodedRgb[0] + matrix[1] * encodedRgb[1] + matrix[2] * encodedRgb[2],
matrix[3] * encodedRgb[0] + matrix[4] * encodedRgb[1] + matrix[5] * encodedRgb[2],
matrix[6] * encodedRgb[0] + matrix[7] * encodedRgb[1] + matrix[8] * encodedRgb[2],
};
}
// ... inside Transform::apply, after curves and the photo stage:
if (hasChannelMatrix) {
out = applyChannelMatrix(grade.channelMatrix, out);
for (double& value : out) value = clamp(value);
}
hasChannelMatrix равен false для точной единичной матрицы, так что нетронутый микшер не стоит ничего и ничего не меняет. Проекты, сохранённые до появления микшера, не имеют поля channelMatrix вовсе. Парсер читает это как единичную матрицу, а отдельная проверка подтверждает, что такой старый грейд рендерится с разницей ровно 0 и в JavaScript-реализации, и в C++-движке.
Какой RGB он смешивает
Матрица работает с кодированным sRGB — теми же значениями 0..1, которые хранит PNG или JPEG, а не с линейным светом. Это записано в контракт инструмента как «кодированный sRGB → кодированный sRGB», и у этого есть заметное следствие: смешивать закодированные значения — не то же самое, что смешивать свет.
Возьмём чистый красный и строку R′ = 0.5 R + 0.5 G. В кодированном sRGB красный выход равен 0.5. То же смешение, выполненное в линейном свете и закодированное обратно, даёт 0.735. Смешение в кодированных значениях заметно темнее, и разрыв растёт с разницей между смешиваемыми каналами. Линейного режима в текущем движке нет, поэтому рецепт, написанный для линейного микшера, не переносится сюда теми же числами.
Что сохраняет сумма строки, а что нет
Если сумма каждой строки равна 1, серый вход [x, x, x] остаётся [x, x, x], потому что M·1 = 1. Интерфейс показывает три суммы строк рядом с матрицей и словами говорит, сохраняются ли нейтральные тона. Проверка по математике подтверждает это с точностью 1.1 × 10⁻¹⁶ на серых 0.18, 0.54 и 1 с матрицей, строки которой равны (0.70, 0.20, 0.10), (0.15, 0.70, 0.15) и (0.20, 0.20, 0.60).
Это сохранение нейтральных тонов, и его легко принять за сохранение яркости. Это не так. Яркость (luma) — это w·RGB, где w = (0.2126, 0.7152, 0.0722), и чтобы сохранить яркость каждого цвета, нужно wᵀM = wᵀ: условие на столбцы, взвешенные по w, а не на строки. Поменяйте местами красный и синий: правило строк выполнено идеально, все серые сохранились, но чистый красный падает по яркости с 0.2126 до 0.0722. У микшера нет режима сохранения яркости; суммы строк — единственный инвариант, который он показывает.
Обратная ошибка так же легка. R′ = R + 0.20 G делает сумму красной строки равной 1.2, и на синтетической цветовой мишени ниже она окрашивает серую шкалу в красный — до 38 кодов. Масштабирование строки до R′ = 0.80 R + 0.20 G всё равно переносит часть зелёного в красный канал, а серые остаются ровно на месте.

Нет столбца смещения
У Channel Mixer в Photoshop есть ползунок Constant от −200 % до +200 %, который делает выходной канал темнее или светлее, и ползунки источников в пределах ±200 % (Adobe: mix colour channels). У моего смещения нет. Это чистое отображение 3×3, а не аффинное 3×4. Коэффициенты доходят до ±4, то есть до ±400 %. Фотостадия движка содержит поканальный Levels с чёрной и белой точкой выхода для R, G и B, но ни один элемент интерфейса их пока не записывает (часть 2 об этом говорит), и они работают до микшера, а не после. Поднять или прижать канал после микширования сегодня просто нельзя. Я не утверждаю численного совпадения с Photoshop или каким-либо другим закрытым редактором; матрица — моя собственная, заявленная контрактом.
Монохром без галочки
Переключателя Monochrome нет. Три одинаковые строки дают монохромное изображение, а значения строк задают внешний вид. С (0.2126, 0.7152, 0.0722) в каждой строке выход — это яркость Rec. 709, посчитанная по кодированным значениям, Y′, а не относительная яркость: веса стандартные, а область — гамма-кодированная. Суммы строк равны 1, так что серые остаются на месте, и таблица превращается в чистую серую шкалу.
Пресеты чёрно-белого в Channel Mixer Photoshop описаны так же — как тройки весов (не путать с отдельной корректировкой Black & White, у которой часть названий совпадает): пресет жёлтого фильтра — Red 34 %, Green 66 %, Blue 0 %, а инфракрасный — Red −70 %, Green 200 %, Blue −30 %. Если вписать такую тройку во все три строки, здесь получится сопоставимое смешение. Я не сравнивала пиксели с выходом Photoshop, поэтому не стала бы обещать их совпадение.
Клиппинг и гамут
После M·RGB каждый выходной канал обрезается до [0, 1]. Комментарий в движке формулирует это как объявленное поведение SDR: строки могут намеренно выходить за диапазон дисплея и поэтому клиппить, а не менять смысл более раннего сжатия гамута в Oklab. Отрицательные коэффициенты — обычный способ туда попасть. R′ = 1.6 R − 0.6 G сохраняет серые (строка даёт 1) и всё же выталкивает 8 из 72 значений каналов на таблице за пределы диапазона. Они обрезаются, и детали там теряются. Проверка для этого подаёт [0.8, 0.3, 0.4] через красную строку (2, 0, 0) и утверждает [1, 0.3, 0.4]: обрезка явная, а не случайность выходного кодировщика.
Микшер работает после мягкого сжатия гамута стадии Oklab, так что помощи от него нет. Цвет, который стадия Oklab мягко загнала внутрь sRGB, матрица может снова вытолкнуть наружу и жёстко обрезать. Контраст обладает тем же свойством; при контрасте 1.5 его обрезка «съедает» 86 из 256 входных кодов, и это открытая задача, а не решённая проблема.
Где он стоит в конвейере
Порядок внутри Transform::apply в color.cpp:
- Входная стадия: необязательный Levels для чекера, затем импортированный LUT с собственной силой смешения.
- Линейный свет: коэффициенты баланса белого, температура и тинт, экспозиция.
- Творческая стадия Oklab: референсное совпадение, оттенок и насыщенность, пины, полосы оттенков; затем, при необходимости, мягкое сжатие гаммута и возврат к кодированному sRGB.
- Контраст вокруг 0.5 в кодированном sRGB.
- Мастер-кривая, затем кривые R, G и B.
- Фотостадия: Levels, кривая яркости, тоновые ползунки, vibrance, колёса цветокоррекции (grading wheels), Selective Color.
- Channel mixer, затем обрезка.
- Защищённые цвета и блокировки оттенков подмешивают обратно исходный пиксель.
- Глобальная сила смешивает весь результат с исходным изображением.
Из этого порядка следуют два вывода. Защиты стоят после микшера, поэтому заблокированный оттенок кожи сохраняет точный исходный RGB, даже если матрица переставляет каналы вокруг него. И каждый шаг до микшера включительно зависит только от собственного RGB пикселя, а не от его позиции или соседей, так что микшер запекается в экспорт CUBE и Hald вместе с остальной глобальной цепочкой.
Как это проверено
- Семейство проверок по математике прогоняет матрицу через её собственное определение, проверяет нейтральные строки, явную обрезку и legacy-грейд и отклоняет коэффициент 5 с именованной ошибкой.
- Нативный движок рендерит legacy-грейд с разницей 0.
- Прогон паритета с фиксированным seed JavaScript и C++ по 600 случайным допустимым грейдам, из которых у каждого с вероятностью 30 % микшер возмущён, сходится до 8.7 × 10⁻¹⁴ на 196 200 значениях каналов.
Это доказывает арифметику и контракт. Это ничего не говорит о том, выглядит ли данная матрица хорошо на данной фотографии, и я не использую эти проверки, чтобы это утверждать.
Ограничения
- Только кодированный sRGB; линейного режима нет.
- Нет столбца смещения, нет переключателя Monochrome, нет варианта с сохранением яркости. Суммы строк показываются; яркость — нет.
- Жёсткая обрезка до [0, 1] после матрицы, без собственного сжатия гамута.
- SDR, 8-битное декодирование Canvas в главном окне. 16-битный путь существует только в Grid Lab.
Следующая и последняя часть — о 3D LUT: как вся эта глобальная цепочка, микшер включительно, запекается в .cube или Hald-изображение и сколько от неё сохраняется между узлами.