Der Kanalmixer, eine 3×3-Matrix: was sie erhält, wo sie clippt, wo sie läuft
Autorin: Anastasiia Butova, Ingenieurin für ComfyUI und Diffusionsmodelle, Belgrad · veröffentlicht 2026-09-23
Übersetzung der englischen Fassung: English
Nach den wahrnehmungsbasierten Farbbereichswerkzeugen aus Teil 4 ist der Kanalmixer das stumpfste Instrument in meinem Farbstudio: drei Ausgabekanäle, jeder eine gewichtete Summe der drei Eingabekanäle, insgesamt neun Zahlen. Der Code umfasst ein paar Zeilen. Interessant sind die Fragen drumherum: welches RGB gemischt wird, was „erhalten“ bedeutet, was außerhalb von 0..1 passiert und wo in der Kette er läuft. Ich habe ihn am 20. September ins Studio aufgenommen. Hier steht, wie er funktioniert und, ebenso deutlich, was er nicht tut.
Die ganze Operation
[ R′ ] [ m00 m01 m02 ] [ R ]
[ G′ ] = [ m10 m11 m12 ] [ G ]
[ B′ ] [ m20 m21 m22 ] [ B ]
Jeder Koeffizient muss endlich sein und im Bereich [−4, 4] liegen; der Standardwert ist die Einheitsmatrix. In der C++-Engine (color.cpp) sehen die Multiplikation und ihre Stufe so aus:
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 ist bei einer exakten Einheitsmatrix falsch, ein unberührter Mixer kostet also nichts und ändert nichts. Projekte, die vor dem Mixer gespeichert wurden, haben gar kein Feld channelMatrix. Der Parser liest das als Einheitsmatrix, und eine eigene Prüfung bestätigt, dass ein solcher alter Grade sowohl in der JavaScript-Implementierung als auch in der C++-Engine mit einer Differenz von genau 0 gerendert wird.
Welches RGB gemischt wird
Die Matrix arbeitet auf kodiertem sRGB, also denselben Werten zwischen 0 und 1, die eine PNG- oder JPEG-Datei speichert, nicht auf linearem Licht. Das steht im Vertrag des Werkzeugs als „kodiertes sRGB → kodiertes sRGB“, und es hat eine sichtbare Folge: Kodierte Werte zu mischen ist nicht dasselbe wie Licht zu mischen.
Nimm reines Rot und die Zeile R′ = 0,5 R + 0,5 G. In kodiertem sRGB ist der rote Ausgang 0,5. Dieselbe Mischung im linearen Licht und zurück kodiert ergibt 0,735. Die kodierte Mischung ist deutlich dunkler, und die Lücke wächst mit dem Unterschied zwischen den gemischten Kanälen. Die aktuelle Engine hat keinen Modus für lineares Licht, deshalb lässt sich ein Rezept, das für einen linearen Mixer geschrieben wurde, nicht als Zahlen übertragen.
Was eine Zeilensumme erhält und was nicht
Wenn jede Zeile zu 1 summiert, bleibt ein graues Eingangssignal [x, x, x] als [x, x, x] erhalten, denn M·1 = 1. Die Oberfläche zeigt die drei Zeilensummen neben der Matrix und sagt in Worten, ob Neutralgrau erhalten bleibt. Die Mathe-Prüfung bestätigt es auf 1,1 × 10⁻¹⁶ für Graustufen von 0,18, 0,54 und 1, mit einer Matrix, deren Zeilen (0,70, 0,20, 0,10), (0,15, 0,70, 0,15) und (0,20, 0,20, 0,60) sind.
Das ist die Erhaltung von Neutralgrau, und es ist leicht, es als Erhaltung der Helligkeit zu lesen. Das ist es nicht. Luma ist w·RGB mit w = (0,2126, 0,7152, 0,0722), und die Luma jeder Farbe zu erhalten verlangt wᵀM = wᵀ: eine Bedingung an die mit w gewichteten Spalten, nicht an die Zeilen. Vertauscht man Rot und Blau, ist die Zeilenregel perfekt erfüllt und jedes Grau bleibt erhalten, doch reines Rot fällt von Luma 0,2126 auf 0,0722. Der Mixer hat keinen Modus, der Luma erhält; die Zeilensummen sind die einzige Invariante, die er ausweist.
Der umgekehrte Fehler ist genauso leicht zu machen. R′ = R + 0,20 G macht die Zeilensumme für Rot 1,2, und auf der unten gezeigten synthetischen Tafel färbt sie die Grauskala um bis zu 38 Codes Richtung Rot. Skaliert man die Zeile zu R′ = 0,80 R + 0,20 G, wandert trotzdem etwas Grün in den roten Kanal, aber die Grautöne bleiben genau dort, wo sie waren.

Keine Offset-Spalte
Der Kanalmixer von Photoshop hat einen Regler „Konstante“ von −200 % bis +200 %, der den Ausgangskanal dunkler oder heller macht, und Quellregler, die auf ±200 % begrenzt sind (Adobe: Farbkanäle mischen). Meiner hat keinen Offset. Er ist eine reine 3×3-Abbildung, keine 3×4-affine. Die Koeffizienten gehen bis ±4, also ±400 %. Die Fotostufe der Engine enthält zwar kanalweises Levels mit einem Ausgangsschwarz und -weiß für R, G und B, aber noch kein Bedienelement der Oberfläche schreibt sie (Teil 2 sagt das), und sie laufen vor dem Mixer, nicht danach. Einen Kanal nach der Mischung anzuheben oder zu drücken, ist heute schlicht nicht vorgesehen. Ich behaupte keine numerische Übereinstimmung mit Photoshop oder einem anderen geschlossenen Editor; die Matrix ist mein eigener, erklärter Vertrag.
Monochrom ohne Kontrollkästchen
Es gibt keinen Schalter „Monochrom“. Drei identische Zeilen ergeben ein monochromes Bild, und die Zeilenwerte bestimmen die Anmutung. Mit (0,2126, 0,7152, 0,0722) in jeder Zeile ist die Ausgabe Rec.-709-Luma, berechnet auf kodierten Werten, Y′, nicht relative Leuchtdichte: Die Gewichte sind die Standardwerte, der Bereich ist gammakodiert. Die Zeilensummen sind 1, Grautöne bleiben also stehen, und die Tafel wird zu einer sauberen Graustufenskala.
Photoshops Kanalmixer dokumentiert seine Schwarzweiß-Voreinstellungen genauso, als Gewichtstripel (nicht zu verwechseln mit der eigenständigen Korrektur Schwarzweiß, deren Voreinstellungen teilweise gleiche Namen tragen): Die Voreinstellung für den Gelbfilter ist Rot 34 %, Grün 66 %, Blau 0 %, und die für Infrarot ist Rot −70 %, Grün 200 %, Blau −30 %. In alle drei Zeilen eingetragen, ergibt ein solches Tripel hier eine vergleichbare Mischung. Ich habe die Pixel nicht mit der Ausgabe von Photoshop verglichen, deshalb würde ich nicht versprechen, dass sie übereinstimmen.
Clipping und Farbumfang
Nach M·RGB wird jeder Ausgangskanal auf [0, 1] begrenzt. Der Kommentar in der Engine formuliert es als deklariertes SDR-Verhalten: Zeilen dürfen den Anzeigebereich absichtlich überschreiten und clippen daher, statt die Bedeutung der früheren Oklab-Farbumfangskompression zu verändern. Negative Koeffizienten sind der übliche Weg dorthin. R′ = 1,6 R − 0,6 G erhält Grautöne (die Zeile summiert sich zu 1) und drückt trotzdem 8 der 72 Kanalwerte auf der Tafel außerhalb des Bereichs. Sie werden geclippt, und Details dort sind verloren. Die Prüfung dafür speist [0,8, 0,3, 0,4] durch eine rote Zeile (2, 0, 0) und erwartet [1, 0,3, 0,4]: Die Begrenzung ist ausdrücklich, kein Zufall des Ausgabe-Encoders.
Der Mixer läuft nach der weichen Farbumfangskompression der Oklab-Stufe, bekommt also keine Hilfe von ihr. Eine Farbe, die die Oklab-Stufe sanft in sRGB hineingebracht hat, kann durch die Matrix wieder hinausgedrückt und hart geclippt werden. Der Kontrastregler teilt diese Eigenschaft: Bei Kontrast 1,5 lässt seine Begrenzung 86 von 256 Eingangscodes auf denselben Wert zusammenfallen, und das ist ein offenes Ticket, kein gelöstes Problem.
Wo er in der Pipeline sitzt
Die Reihenfolge innerhalb von Transform::apply in color.cpp:
- Eingangsstufe: optionales Levels der Prüftafel, dann die importierte LUT, mit eigener Stärke eingemischt.
- Lineares Licht: Weißabgleich-Verstärkungen, Temperatur und Tönung, Belichtung.
- Kreative Oklab-Stufe: Referenzabgleich, Farbton und Sättigung, Pins, Farbtonbänder; dann optional weiche Farbumfangskompression und die Rückkehr zu kodiertem sRGB.
- Kontrast um 0,5 in kodiertem sRGB.
- Hauptkurve, dann die Kurven für R, G und B.
- Fotostufe: Levels, Luma-Kurve, Tonwertregler, Vibrance, Farbräder, Selektive Farbe.
- Der Kanalmixer, dann das Clamping.
- Geschützte Farben und Farbtonsperren mischen zurück zum Originalpixel.
- Die globale Stärke mischt das gesamte Ergebnis mit dem Original.
Zwei Dinge folgen aus dieser Reihenfolge. Schutzmaßnahmen kommen nach dem Mixer, sodass ein gesperrter Hautton seine exakten Quell-RGB-Werte behält, selbst wenn die Matrix die Kanäle um ihn herum vertauscht. Und jeder Schritt bis einschließlich des Mixers hängt nur vom eigenen RGB des Pixels ab, nie von Position oder Nachbarn, daher lässt sich der Mixer mit dem Rest der globalen Kette in CUBE- und Hald-Exporte einbacken.
Wie es geprüft wird
- Eine Mathe-Familie führt die Matrix gegen ihre eigene Definition aus, prüft die neutralen Zeilen, das explizite Clipping und den alten Grade und weist einen Koeffizienten von 5 mit einem benannten Fehler zurück.
- Die native Engine rendert den alten Grade mit einer Differenz von 0.
- Ein seeded Paritätslauf in JavaScript und C++ über 600 zufällige gültige Grades, jeder mit einer Wahrscheinlichkeit von 30 % für einen gestörten Mixer, stimmt auf 8,7 × 10⁻¹⁴ über 196.200 Kanalwerte überein.
Das belegt die Arithmetik und den Vertrag. Es sagt nichts darüber, ob eine bestimmte Matrix auf einem bestimmten Foto gut aussieht, und ich verwende es nicht, um das zu behaupten.
Grenzen
- Nur kodiertes sRGB; kein Modus für lineares Licht.
- Keine Offset-Spalte, kein Schalter „Monochrom“, keine Option zur Erhaltung der Luma. Zeilensummen werden angezeigt; Luma nicht.
- Ein hartes Clamping auf [0, 1] nach der Matrix, ohne eigene Farbumfangskompression.
- SDR, 8-Bit-Canvas-Dekodierung im Hauptfenster. Der 16-Bit-Pfad existiert nur im Grid Lab.
Der nächste und letzte Teil handelt von 3D-LUTs: wie diese ganze globale Kette, Mixer eingeschlossen, in eine .cube- oder Hald-Datei eingebacken wird und wie viel davon zwischen den Knoten erhalten bleibt.