O mixer de canais, uma matriz 3×3: o que preserva, onde corta e onde roda
Autora: Anastasiia Butova, engenheira de ComfyUI e modelos de difusão, Belgrado · publicado em 2026-09-23
Tradução da versão em inglês: English
Depois das ferramentas de faixa perceptual da parte 4, o mixer de canais é o instrumento mais bruto do meu estúdio de cor: três canais de saída, cada um uma soma ponderada dos três canais de entrada, nove números no total. O código tem poucas linhas. As perguntas interessantes estão em volta dele: qual RGB ele mistura, o que "preservar" significa, o que acontece fora de 0..1 e onde ele roda na cadeia. Eu o adicionei ao estúdio em 20 de setembro. Abaixo está como ele funciona e, com a mesma clareza, o que ele não faz.
A operação inteira
[ R′ ] [ m00 m01 m02 ] [ R ]
[ G′ ] = [ m10 m11 m12 ] [ G ]
[ B′ ] [ m20 m21 m22 ] [ B ]
Todo coeficiente precisa ser finito e estar dentro de [−4, 4]; o padrão é a matriz identidade. No motor em C++ (color.cpp), a multiplicação e a sua etapa ficam assim:
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 é falso para uma identidade exata, então um mixer sem mexer não custa nada e não muda nada. Projetos salvos antes de o mixer existir não têm nenhum campo channelMatrix. O parser lê isso como identidade, e uma verificação específica confirma que um grade antigo assim é renderizado com diferença exatamente 0, tanto na implementação em JavaScript quanto no motor em C++.
Qual RGB ele mistura
A matriz trabalha em sRGB codificado, os mesmos valores 0..1 que um PNG ou JPEG armazena, e não em luz linear. Isso está escrito no contrato da ferramenta como "sRGB codificado → sRGB codificado", e tem uma consequência visível: misturar valores codificados não é misturar luz.
Pegue o vermelho puro e a linha R′ = 0,5 R + 0,5 G. Em sRGB codificado, a saída vermelha é 0,5. A mesma mistura feita em luz linear e codificada de volta dá 0,735. A mistura codificada fica visivelmente mais escura, e a diferença cresce com a distância entre os canais misturados. O motor atual não tem um modo de luz linear, então uma receita escrita para um mixer linear não se transfere como números.
O que a soma de uma linha preserva, e o que não preserva
Se toda linha somar 1, uma entrada cinza [x, x, x] continua [x, x, x], porque M·1 = 1. A interface mostra as três somas das linhas ao lado da matriz e diz em palavras se os neutros sobrevivem. A verificação matemática confirma isso com erro de 1,1 × 10⁻¹⁶ em cinzas de 0,18, 0,54 e 1, com uma matriz cujas linhas são (0,70; 0,20; 0,10), (0,15; 0,70; 0,15) e (0,20; 0,20; 0,60).
Isso é preservação de neutros, e é fácil lê-la como preservação de brilho. Não é. Luma é w·RGB com w = (0,2126; 0,7152; 0,0722), e manter o luma de todas as cores exige wᵀM = wᵀ: uma condição sobre as colunas ponderadas por w, não sobre as linhas. Trocar vermelho e azul satisfaz perfeitamente a regra das linhas e mantém todos os cinzas, mas o vermelho puro cai de luma 0,2126 para 0,0722. O mixer não tem modo de preservação de luma; as somas das linhas são o único invariante que ele informa.
O erro oposto é igualmente fácil. R′ = R + 0,20 G faz a soma da linha vermelha dar 1,2, e no gráfico sintético abaixo ela tinge a rampa de cinzas em até 38 códigos em direção ao vermelho. Reescalar a linha para R′ = 0,80 R + 0,20 G ainda move parte do verde para o canal vermelho, mas os cinzas ficam exatamente onde estavam.

Nenhuma coluna de offset
O Channel Mixer do Photoshop tem um controle Constant que vai de −200% a +200% e torna o canal de saída mais escuro ou mais claro, além de controles de origem limitados a ±200% (Adobe: mix colour channels). O meu não tem offset. É um mapa 3×3 puro, não um afim 3×4. Os coeficientes vão até ±4, ou seja, ±400%. A etapa de foto do motor até carrega Levels por canal com um preto e um branco de saída para R, G e B, mas nenhum controle da interface os escreve ainda (a parte 2 diz isso), e eles rodam antes do mixer, não depois. Um canal clareado ou escurecido após a mistura simplesmente não está disponível hoje. Não afirmo paridade numérica com o Photoshop nem com nenhum outro editor fechado; a matriz é um contrato próprio e declarado.
Monocromático sem uma caixa de seleção
Não existe um interruptor de Monocromático. Três linhas idênticas dão uma imagem monocromática, e os valores das linhas decidem a aparência. Com (0,2126; 0,7152; 0,0722) em todas as linhas, a saída é o luma Rec. 709 calculado sobre valores codificados, Y′, e não a luminância relativa: os pesos são os padrão, e o domínio é codificado com gama. As somas das linhas são 1, então os cinzas permanecem onde estão, e o gráfico vira uma escala de cinza limpa.
O Channel Mixer do Photoshop documenta suas predefinições de preto e branco do mesmo jeito, como trios de pesos (sem confundir com o ajuste separado Preto e Branco, cujas predefinições compartilham alguns nomes): a predefinição de filtro amarelo é Vermelho 34%, Verde 66%, Azul 0%, e a de infravermelho é Vermelho −70%, Verde 200%, Azul −30%. Digitado nas três linhas, um trio desses dá uma mistura comparável aqui. Não comparei os pixels com a saída do Photoshop, então não prometeria que coincidem.
Clipagem e gamute
Depois de M·RGB, cada canal de saída é limitado a [0, 1]. O comentário no motor declara isso como o comportamento SDR declarado: as linhas podem intencionalmente ultrapassar a faixa do display e, portanto, cortar, em vez de alterar o significado da compressão de gamute anterior no Oklab. Coeficientes negativos são a forma usual de chegar lá. R′ = 1,6 R − 0,6 G mantém os cinzas (a linha soma 1) e ainda empurra 8 dos 72 valores de canal do gráfico para fora da faixa. Eles são cortados, e o detalhe ali se perde. A verificação para isso alimenta [0,8; 0,3; 0,4] por uma linha vermelha (2, 0, 0) e afirma [1; 0,3; 0,4]: o clamp é explícito, não um acidente do codificador de saída.
O mixer roda depois da compressão suave de gamute da etapa Oklab, então não recebe ajuda dela. Uma cor que a etapa Oklab trouxe suavemente para dentro do sRGB pode ser empurrada para fora de novo pela matriz e cortada de forma rígida. O controle de contraste compartilha essa propriedade; com contraste 1,5, seu clamp achata 86 de 256 códigos de entrada, e isso é um ticket aberto, não um problema resolvido.
Onde ele fica no pipeline
A ordem dentro de Transform::apply em color.cpp:
- Etapa de entrada: Levels opcional do checker, depois o LUT importado, misturado pela sua própria intensidade.
- Luz linear: ganhos de balanço de brancos, temperatura e tonalidade, exposição.
- Etapa criativa em Oklab: match de referência, matiz e saturação, pins, faixas de matiz; depois compressão suave de gamute opcional e retorno ao sRGB codificado.
- Contraste em torno de 0,5 no sRGB codificado.
- Curva mestra, depois as curvas de R, G e B.
- Etapa de foto: Levels, curva de luma, controles de tom, vibrance, rodas de gradação, Selective Color.
- O mixer de canais, depois o clamp.
- Cores protegidas e travas de matiz misturam de volta em direção ao pixel original.
- A intensidade global mistura o resultado inteiro com o original.
Duas coisas decorrem dessa ordem. As proteções vêm depois do mixer, então um matiz de pele travado mantém seu RGB de origem exato mesmo quando a matriz troca canais ao redor dele. E toda etapa até o mixer, inclusive, depende apenas do próprio RGB do pixel, nunca da sua posição ou dos vizinhos, então o mixer é gravado em exportações CUBE e Hald junto com o resto da cadeia global.
Como é verificado
- Uma família de verificações matemáticas roda a matriz contra a própria definição, verifica as linhas neutras, o corte explícito e o grade legado, e rejeita um coeficiente de 5 com um erro nomeado.
- O motor nativo renderiza o grade legado com diferença de 0.
- Uma comparação de paridade semeada entre JavaScript e C++ sobre 600 grades válidos aleatórios, cada um com 30% de chance de um mixer perturbado, concorda até 8,7 × 10⁻¹⁴ em 196.200 valores de canal.
Isso prova a aritmética e o contrato. Não diz se uma determinada matriz fica boa numa determinada fotografia, e não as uso para afirmar isso.
Limites
- Apenas sRGB codificado; sem modo de luz linear.
- Sem coluna de offset, sem interruptor de Monocromático, sem opção de preservar luma. As somas das linhas são mostradas; o luma, não.
- Um clamp rígido em [0, 1] depois da matriz, sem compressão de gamute própria.
- Decodificação SDR de 8 bits no Canvas da janela principal. O caminho de 16 bits existe apenas no Grid Lab.
A próxima e última parte trata dos LUTs 3D: como toda essa cadeia global, incluindo o mixer, é gravada num .cube ou numa imagem Hald, e quanto dela sobrevive entre os nós.