El mezclador de canales, una matriz 3×3: qué conserva, dónde recorta y dónde se ejecuta
Autora: Anastasiia Butova, ingeniera de ComfyUI y modelos de difusión, Belgrado · publicado 2026-09-23
Traducción de la versión en inglés: English
Tras las herramientas de rango perceptual de la parte 4, el mezclador de canales es el instrumento más directo de mi estudio de color: tres canales de salida, cada uno una suma ponderada de los tres canales de entrada, nueve números en total. El código son unas pocas líneas. Las preguntas interesantes están alrededor: qué RGB mezcla, qué significa "conservar", qué pasa fuera de 0..1 y dónde se sitúa en la cadena. Lo añadí al estudio el 20 de septiembre. Abajo explico cómo funciona y, con la misma claridad, lo que no hace.
La operación completa
[ R′ ] [ m00 m01 m02 ] [ R ]
[ G′ ] = [ m10 m11 m12 ] [ G ]
[ B′ ] [ m20 m21 m22 ] [ B ]
Cada coeficiente debe ser finito y estar dentro de [−4, 4]; el valor por defecto es la matriz identidad. En el motor C++ (color.cpp) la multiplicación y su etapa tienen este aspecto:
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 es falso para una identidad exacta, así que un mezclador sin tocar no cuesta nada ni cambia nada. Los proyectos guardados antes de que existiera el mezclador no tienen ningún campo channelMatrix. El parser lo lee como identidad, y una comprobación específica confirma que un gradado antiguo así se renderiza con una diferencia exacta de 0, tanto en la implementación JavaScript como en el motor C++.
Qué RGB mezcla
La matriz trabaja sobre sRGB codificado, los mismos valores 0..1 que guarda un PNG o un JPEG, no sobre luz lineal. Eso está escrito en el contrato de la herramienta como "sRGB codificado → sRGB codificado", y tiene una consecuencia visible: mezclar valores codificados no es mezclar luz.
Tomemos el rojo puro y la fila R′ = 0,5 R + 0,5 G. En sRGB codificado, la salida roja es 0,5. La misma mezcla hecha en luz lineal y codificada de nuevo da 0,735. La mezcla codificada es notablemente más oscura, y la diferencia crece con la distancia entre los canales que se mezclan. El motor actual no tiene modo de luz lineal, así que una receta escrita para un mezclador lineal no se traslada como números.
Qué conserva la suma de una fila, y qué no
Si cada fila suma 1, un gris de entrada [x, x, x] se queda en [x, x, x], porque M·1 = 1. La interfaz muestra las tres sumas de fila junto a la matriz y dice con palabras si los neutros sobreviven. La comprobación matemática lo confirma con un error de 1,1 × 10⁻¹⁶ en grises de 0,18, 0,54 y 1, con una matriz cuyas filas son (0,70, 0,20, 0,10), (0,15, 0,70, 0,15) y (0,20, 0,20, 0,60).
Eso es conservación de neutros, y es fácil leerlo como conservación del brillo. No lo es. La luma es w·RGB con w = (0,2126, 0,7152, 0,0722), y mantener la luma de cada color exige wᵀM = wᵀ: una condición sobre las columnas ponderadas por w, no sobre las filas. Intercambiar rojo y azul cumple la regla de filas a la perfección y conserva todos los grises, y sin embargo el rojo puro baja de luma 0,2126 a 0,0722. El mezclador no tiene un modo que conserve la luma; las sumas de fila son el único invariante que informa.
El error contrario es igual de fácil. R′ = R + 0,20 G hace que la suma de la fila roja sea 1,2, y en la carta sintética de abajo tiñe la rampa de grises hacia el rojo hasta 38 códigos. Reescalar la fila a R′ = 0,80 R + 0,20 G sigue pasando algo de verde al canal rojo, pero los grises se quedan exactamente donde estaban.

Sin columna de desplazamiento
El Channel Mixer de Photoshop tiene un control Constante de −200 % a +200 % que hace más oscuro o más claro el canal de salida, y controles de origen limitados a ±200 % (Adobe: mezclar canales de color). El mío no tiene desplazamiento. Es un mapa 3×3 puro, no un afín 3×4. Los coeficientes llegan a ±4, es decir, ±400 %. La etapa fotográfica del motor sí lleva Niveles por canal con negro y blanco de salida para R, G y B, pero ningún control de la interfaz los escribe todavía (la parte 2 lo indica), y se ejecutan antes del mezclador, no después. Un canal aclarado o aplastado después de la mezcla simplemente no está disponible hoy. No afirmo paridad numérica con Photoshop ni con ningún otro editor cerrado; la matriz es mi propio contrato declarado.
Monocromo sin casilla
No hay un interruptor de Monocromo. Tres filas idénticas dan una imagen monocroma, y los valores de las filas deciden el aspecto. Con (0,2126, 0,7152, 0,0722) en cada fila, la salida es la luma Rec. 709 calculada sobre valores codificados, Y′, no la luminancia relativa: los pesos son los estándar, el dominio está codificado con gamma. Las sumas de fila son 1, así que los grises se mantienen y la carta se convierte en una escala de grises limpia.
El Channel Mixer de Photoshop documenta sus preajustes de blanco y negro de la misma forma, como ternas de pesos (no hay que confundirlos con el ajuste independiente de Blanco y negro, cuyos preajustes comparten algunos nombres): el preajuste de filtro amarillo es Rojo 34 %, Verde 66 %, Azul 0 %, y el de infrarrojo es Rojo −70 %, Verde 200 %, Azul −30 %. Escrita en las tres filas, una terna así da una mezcla comparable aquí. No he comparado los píxeles con la salida de Photoshop, así que no prometería que coincidan.
Recorte y gama
Tras M·RGB, cada canal de salida se recorta a [0, 1]. El comentario del motor lo expone como el comportamiento SDR declarado: las filas pueden superar intencionadamente el rango de la pantalla y por tanto recortar, en lugar de cambiar el significado de la compresión de gama Oklab anterior. Los coeficientes negativos son la forma habitual de llegar ahí. R′ = 1,6 R − 0,6 G conserva los grises (la fila suma 1) y aun así empuja 8 de los 72 valores de canal de la carta fuera del rango. Se recortan, y el detalle allí se pierde. La comprobación de esto alimenta [0,8, 0,3, 0,4] a través de una fila roja de (2, 0, 0) y afirma [1, 0,3, 0,4]: el recorte es explícito, no un accidente del codificador de salida.
El mezclador se ejecuta después de la compresión suave de gama de la etapa Oklab, así que no recibe ayuda de ella. Un color que la etapa Oklab llevó con suavidad dentro de sRGB puede volver a salir por la matriz y recortarse duro. El control de contraste comparte esta propiedad; con contraste 1,5, su recorte aplana 86 de los 256 códigos de entrada, y eso es un ticket abierto, no un problema resuelto.
Dónde se sitúa en el pipeline
El orden dentro de Transform::apply en color.cpp:
- Etapa de entrada: Niveles opcionales de checker, luego el LUT importado, mezclado según su propia intensidad.
- Luz lineal: ganancias de balance de blancos, temperatura y tinte, exposición.
- Etapa creativa Oklab: ajuste de referencia, tono y saturación, pines, bandas de tono; después compresión suave de gama opcional y vuelta a sRGB codificado.
- Contraste alrededor de 0,5 en sRGB codificado.
- Curva maestra, luego las curvas R, G y B.
- Etapa fotográfica: Niveles, curva de luma, controles de tono, vibrance, ruedas de gradado, Selective Color.
- El mezclador de canales, y luego el recorte.
- Colores protegidos y bloqueos de tono mezclan de nuevo hacia el píxel original.
- La intensidad global mezcla el resultado completo con el original.
De este orden se deducen dos cosas. Las protecciones van después del mezclador, así que un tono de piel bloqueado conserva su RGB de origen exacto incluso cuando la matriz intercambia canales a su alrededor. Y cada paso hasta el mezclador incluido depende solo del RGB del propio píxel, nunca de su posición ni de sus vecinos, así que el mezclador se hornea en las exportaciones CUBE y Hald junto con el resto de la cadena global.
Cómo se comprueba
- Una familia de pruebas matemáticas ejecuta la matriz contra su propia definición, comprueba las filas neutras, el recorte explícito y el gradado heredado, y rechaza un coeficiente de 5 con un error con nombre.
- El motor nativo renderiza el gradado heredado con una diferencia de 0.
- Una comparación de paridad JavaScript y C++ con semilla, sobre 600 gradados válidos aleatorios, cada uno con un 30 % de probabilidad de tener un mezclador perturbado, concuerda hasta 8,7 × 10⁻¹⁴ en 196.200 valores de canal.
Esto prueba la aritmética y el contrato. No dice si una matriz dada se ve bien en una fotografía dada, y no las uso para afirmar eso.
Límites
- Solo sRGB codificado; no hay modo de luz lineal.
- Sin columna de desplazamiento, sin interruptor de Monocromo, sin opción que conserve la luma. Se muestran las sumas de fila; la luma no.
- Un recorte duro a [0, 1] tras la matriz, sin compresión de gama propia.
- SDR, decodificación de Canvas de 8 bits en la ventana principal. La ruta de 16 bits existe solo en el Grid Lab.
La siguiente y última parte trata de los LUT 3D: cómo toda esta cadena global, con el mezclador incluido, se hornea en un .cube o en una imagen Hald, y cuánto de ella sobrevive entre los nodos.