通道混合器,一个 3×3 矩阵:它保留什么、在哪里截断、在哪里运行
作者: Anastasiia Butova, ComfyUI 与扩散模型工程师,贝尔格莱德 · 发布于 2026-09-23
译自英文版: English
在 第 4 部分 讲完感知范围工具之后,通道混合器是我调色工作室里最直接的工具:三个输出通道,每个都是三个输入通道的加权和,总共九个数字。代码只有几行。真正有意思的问题都在它周围:它混合的是哪种 RGB,"保留"究竟是什么意思,超出 0..1 范围时会发生什么,以及它在整条链路中的哪个位置运行。我在 9 月 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 字段。解析器会把它读作单位矩阵,一项专门的检查确认,这种旧的调色在 JavaScript 实现和 C++ 引擎中渲染出来的差值都正好是 0。
它混合的是哪种 RGB
矩阵作用于编码后的 sRGB,也就是 PNG 或 JPEG 存储的那些 0..1 数值,而不是线性光。这一点写进了工具的约定中,写作"编码 sRGB → 编码 sRGB",它带来一个可见的后果:混合编码后的数值并不等于混合光。
以纯红色为例,第一行 R′ = 0.5 R + 0.5 G。在编码后的 sRGB 中,红色输出是 0.5。若在线性光中做同样的混合,再编码回来,结果是 0.735。编码域的混合明显更暗,而且差距会随着被混合通道之间差异的增大而扩大。目前的引擎没有线性光模式,因此为线性混合器写的配方,不能照搬成数值。
行和保留了什么,又没有保留什么
如果每一行的和都是 1,那么灰色输入 [x, x, x] 会保持为 [x, x, x],因为 M·1 = 1。界面会把三行的和显示在矩阵旁边,并用文字说明中性色是否得以保留。数学检查在灰度 0.18、0.54 和 1 上确认了这一点,误差为 1.1 × 10⁻¹⁶,所用矩阵的行分别为 (0.70, 0.20, 0.10)、(0.15, 0.70, 0.15) 和 (0.20, 0.20, 0.60)。
这是中性色保留,很容易把它误读为亮度保留。其实不是。亮度是 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 后,仍会把一部分绿色混入红色通道,但灰色恰好保持在原位。

没有偏移列
Photoshop 的通道混合器有一个 Constant(常量)滑块,范围从 −200% 到 +200%,可以让输出通道变暗或变亮;源通道的滑块限制在 ±200%(Adobe:混合颜色通道)。我的混合器没有偏移量。它是纯粹的 3×3 映射,而不是 3×4 的仿射映射。系数范围到 ±4,也就是 ±400%。引擎的照片阶段确实带有逐通道的 Levels,R、G、B 各有输出黑场和白场,但目前界面中没有任何控件会写入它们(第 2 部分 中有说明),而且它们运行在混合器之前,而不是之后。因此,今天无法在混合之后提升或压低某个通道。我不声称与 Photoshop 或其他封闭编辑器在数值上一致;这个矩阵是我自己声明的约定。
没有单色复选框的单色化
工作室里没有单色开关。三行完全相同,就得到单色图像,而各行的数值决定了最终外观。若每行都使用 (0.2126, 0.7152, 0.0722),输出就是对编码值计算的 Rec. 709 亮度,即 Y′,而不是相对亮度:权重是标准权重,但作用域是伽马编码后的数值。各行的和为 1,所以灰色保持不变,色卡变成干净的灰阶。
Photoshop 的通道混合器在文档中也是以同样的方式描述它的黑白预设的,即权重三元组(不要与单独的"黑白"调整混为一谈,两者的部分预设名称相同):黄色滤镜预设为红 34%、绿 66%、蓝 0%,红外预设为红 −70%、绿 200%、蓝 −30%。把这样的三元组填入三行,在这里能得到相近的混合效果。我还没有把像素与 Photoshop 的输出逐一比对,所以不保证它们一致。
截断与色域
经过 M·RGB 之后,每个输出通道会被钳制到 [0, 1]。引擎中的注释将其表述为已声明的 SDR 行为:各行可能有意超出显示范围,因此会截断,而不是改变早先 Oklab 色域压缩的含义。负系数是通常的达到这种情况的途径。R′ = 1.6 R − 0.6 G 保持灰色(该行的和为 1),但仍把图表上 72 个通道值中的 8 个推到了范围之外。它们被截断,截断处的细节就丢失了。对此的检查输入 [0.8, 0.3, 0.4],经过红色行 (2, 0, 0),断言输出为 [1, 0.3, 0.4]:钳制是显式的,而不是输出编码器偶然造成的。
混合器在 Oklab 阶段的软色域压缩之后运行,因此得不到它的帮助。Oklab 阶段温和地压进 sRGB 之内的颜色,可能会被矩阵再次推出去,从而被硬截断。对比度控件也有同样的特性;在对比度 1.5 时,它的钳制会把 256 个输入码值中的 86 个压平,这是一个尚未解决的遗留问题,而不是已经解决的问题。
它在流水线中的位置
color.cpp 中 Transform::apply 内部的顺序:
- 输入阶段:可选的色卡 Levels,然后是导入的 LUT,按其自身的强度混合。
- 线性光:白平衡增益、色温与色调、曝光。
- Oklab 创意阶段:参考匹配、色相与饱和度、色彩锚点、色相带;然后可选的软色域压缩,并回到编码 sRGB。
- 对比度,围绕编码 sRGB 中的 0.5。
- 主曲线,然后是 R、G、B 曲线。
- 照片阶段:Levels、亮度曲线、影调控件、自然饱和度、色彩平衡轮、选择性颜色。
- 通道混合器,然后钳制。
- 保护色和色相锁定,将结果向原始像素混合回去。
- 全局强度,将整个结果与原图混合。
这个顺序带来两点结果。保护位于混合器之后,所以即使矩阵在它周围交换了通道,被锁定的肤色仍会保持其精确的源 RGB。另外,直到并包括混合器在内的每一步,都只依赖于像素自身的 RGB,而不依赖于它的位置或邻域,因此混合器可以和其余全局链路一起烘焙进 CUBE 和 Hald 导出中。
如何检验
- 一组数学检查针对其自身定义测试矩阵,检查中性行、显式截断和旧调色,并以命名错误拒绝系数 5。
- 原生引擎渲染旧调色,差值为 0。
- 对 600 个随机的有效调色(每个有 30% 概率带有扰动的混合器)进行带种子的 JavaScript 与 C++ 一致性运行,在 196,200 个通道值上的一致程度达到 8.7 × 10⁻¹⁴。
这些只证明了算术和约定。它们不能说明某个矩阵在某张照片上好不好看,我也不会用它们来宣称这一点。
限制
- 只支持编码 sRGB;没有线性光模式。
- 没有偏移列,没有单色开关,没有保持亮度的选项。显示的是行和,而不是亮度。
- 矩阵之后是硬钳制到 [0, 1],它自己没有色域压缩。
- 主窗口中是 SDR、8 位 Canvas 解码。16 位路径只存在于 Grid Lab 中。
下一部分也是最后一部分是 3D LUT:整个全局链路(包括混合器)如何被烘焙进 .cube 或 Hald 图像,以及其中有多少能在格点之间保留下来。