文章目录
Photoshop 中有色彩平衡(Color Balance,在中文语境下容易与白平衡 White Balance 相混淆,这里仅指 Photoshop 的 Color Balance 调整图层)功能,可以对图片的颜色倾向进行调整。
而作为图像算法工程师,自然对其背后的原理和实现比较感兴趣。这篇文章就来说说 Photoshop 中的色彩平衡功能背后的原理,文末附有代码实现。
Photoshop 中的色彩平衡功能面板如下:

最上面是影调(Tone),用来限定调整的是图像的哪部分影调下的色彩,有高光(Highlights)、中间调(Midtones)和阴影(Shadows)。
中间是三个调节颜色的滑杆,详细原理会涉及到 RGB 色彩系统的基本运算,这里不再展开。
最下面是一个保持明度(Preserve Luminosity)的选框,选中了表示采用的新版算法(其实也不新了,Photoshop CS6 时代就已经有了),在颜色调整的同时尽量保持明度不变(当然,看了后面的原理分析就知道,实际上并没有严格保持明度不变,只是切换新老算法的一个开关而已);而老算法现在使用得很少了,这里不展开分析。
1. 基本设定
1.1 sRGB vs. 线性 RGB
首先一个问题,Photoshop 中,进行色彩平衡处理的时候,是在图像 RGB 空间(通常是 sRGB)处理,还是在线性 RGB 空间处理?
这个问题是重要的,如果没搞对,会导致结果的颜色变的奇怪。即使在一些成熟的商业方案上仍然有这种低级错误(参见 YouTube 上一个比较精彩的科普 Computer Color is Broken 需要科学上网,有一个中译版可以看这里:计算机的颜色不是连续的 – Computer Color is Broken – 译学馆 – 知识无疆界,但我个人觉得翻译得不太好)
为确定这个问题,这里我做了一个简单的实验。在色彩平衡面板中随便调整一些参数(比如把中间调红色 +30),把处理前后的图中每个像素的值进行比对,画在图上:图中横坐标是原始的像素值,纵坐标是处理前后像素值的变化量。这里的像素值的含义,是 sRGB 空间下的像素值。

处理前后像素值的变化,横坐标是原始像素值,纵坐标是像素值变化量
图中画出了红色和蓝色两个通道,绿色通道和蓝色通道表现一样,在同一张图上就不画出来了。这里可以很明显的看到由于量化(Quantization)造成的台阶。这预示着 Photoshop 中色彩平衡功能在计算的时候是在 sRGB 空间中进行的,否则图中出现的应当是弯曲的台阶,而不是现在这样平直的台阶。
看得出来这是严谨性和性能之间的一个权衡,从原理来说,在线性 RGB 空间中做计算更合理,也更容易设计真正的保持明度的算法,但从 sRGB 转到 线性 RGB 需要一定的计算量,而实际中是不是严格保持明度这一点并不是那么重要,通常摄影师会单独用其他的调整图层进行明度的调整。
1.2 参数规约化
我们知道,等量的 RGB 三色混合,得到的是不同明度的灰色。如果将三个调整滑杆都拖动相同的数值,那么理论上将不会对原图做任何变化(加减灰色,颜色上当然不应当有变化,而明度上我们需要保持明度,所以也不应该有变化)。举个例子,三个滑杆值 (+20, +35, +15) 的效果等价于 (+5, +20, 0) 的效果,也等价于 (0, +15, -5) 的效果。
这一点可以在 Photoshop 里得到验证。
这个特性虽然很好理解,但是给算法的实现上带来了麻烦:有许多组看起来不同的参数,得到的效果要保持一致。要得到这个效果,一般从两个方向来考虑,要么设计一个很复杂的周期函数,自动把这种参数的等量变化内化到函数周期中去;要么只考虑某种特殊情况的参数,其他情况都通过参数等量变化来规约到这种特殊情况下来。这里我选择第二种思路,我把这个思路称为参数的规约化,有点类似于函数的主值区间的意思。
那么怎么规约参数呢?一个很直观的思路就是,在等量变化的前提下,选用绝对值之和最小的那组参数。因为绝对值之和最小意味着对原图的改变也最小。这个目标用公式来表达就是
其中,R, G, B 分别是三个调整参数,d 就是待定的等量变化量。经过简单分类讨论可以知道,对 R, G, B 三者进行排序,令 d 等于排序第二的那个量的相反数,则整个目标函数能取得最小值。
用前面举过的例子来说,三个参数为 (+20, +35, +15),排序之后第二的是 +20,于是令 d = -20,得到规约之后的正则参数为 (0, +15, -5)
本文之后的讨论,未单独说明的,都指规约化之后的参数。
2. 中间色调(Midtones)处理算法
经过一些简单的实验,我总结出这样几个结论:
单独改动任何一个参数,都会引起本通道朝向一个方向变化,同时另外两个通道朝向另一个方向变化。举例来说,对 R 参数 +20,则 R 通道像素值会变大,而 G, B 通道像素值会(等量地)变小——两边的变化方向完全相反;
改动不同的参数,效果是单独改动的结果的叠加。
那么,关键的问题就是,参数的值是怎么影响像素值的,换句话说,是个什么函数形式。

中间色调红色+70之后,原始像素值和处理后像素值的变化情况。横坐标是原始像素值,纵坐标是处理后像素值。图中画出的是红色和蓝色通道的情况。
从处理前后像素值变化的图上可以猜测,这个函数可能是个指数函数:,其中 alpha 是个随滑块值变化的参数。尝试不同的滑块参数,对函数进行拟合,发现指数函数对不同参数下的变化都能有很好的拟合,因此我可以大胆猜测,中间色调的处理算法就是指数函数。
接下来的问题就是,指数 alpha 是如何随着滑块参数变化的。
很显然,alpha 处于指数位置,那么 alpha 随着滑块参数 v 的变化,应当满足:,也就是在对数坐标下,alpha~v 函数图像是关于原点对称的。经过实验,拟合了不同滑块参数 v 对应的不同 alpha 值,画在对数坐标下,近似为一条过原点的直线,拟合直线方程为 log(alpha) = -0.0033944 v

中间色调参数 alpha 随着滑块参数 v 的变化情况
所以中间调的处理算法可以总结为
3. 高光(Highlights)和阴影(Shadows)处理算法
高光和阴影的调整算法,与中间调调整算法不一样,从调整前后的像素值变化曲线上就能看出来

高光红色+50,处理后像素与原始像素的对比情况。图中画出了红色和蓝色通道。

阴影红色+50,处理后像素值与原始像素的对比情况。图中画出了红色和蓝色通道。
高光阴影的处理过程有几个特点:
– 对于正向的调整,比如同样只调整红色 +50,高光的处理算法是只增加红色通道的值,其他两个通道不变;而阴影的处理算法是不变红色通道值,把其他两个通道值减少;
– 对于负向的调整,比如同样只调整红色 -50,高光的处理算法是不变红色通道值,其他两个通道值增加;而阴影的处理算法是只减少红色通道值,其他两个通道值不变;
– 相同的一点是,两边的算法看起来都是一个线性调整。
既然是线性调整,对高光算法而言,一定过原点,所以函数形式为
其中 c 是「横截距」,就是右端点移动的多少,很显然,调整强度越大,右端点移动越多,c 也越大;
而对于阴影调整算法,类似的可以写成
这里 c 的含义类似,只是用来衡量左端点移动的距离,调整的强度越大,左端点移动越多,c 也越大。
剩下的问题就是,这个 c 值与滑块参数之间有什么关系。经过实验,发现 c 值和滑块值之间也是一个线性关系

高光阴影参数 c 随滑块参数 v 的变化情况
所以,高光和阴影的算法可以总结如下:
高光部分:
阴影部分:
4. 代码(matlab 版本)
代码中有一些变量名和文章中不一样(比如 alpha / gamma 什么的)…懒得改了,就这样吧
clear; close all; clc;
% Coefficients may be not normalized
shadows_coef = [0, -20, 30];
midtones_coef = [-20, 20, 0];
highlights_coef = [15, 0, -20];
% Normalize these coefficients
tmp = sort(shadows_coef);
shadows_coef = shadows_coef - tmp(2);
tmp = sort(midtones_coef);
midtones_coef = midtones_coef - tmp(2);
tmp = sort(highlights_coef);
highlights_coef = highlights_coef - tmp(2);
% Read an image
img1 = im2double(imread('lut0.png'));
img2 = img1;
% Do highlights
highlights_alpha = 0.0039230 * highlights_coef;
highlights_alpha = diag(highlights_alpha .* (highlights_alpha > 0)) * ...
[1, 0, 0; 0, 1, 0; 0, 0, 1] + ...
diag(-highlights_alpha .* (highlights_alpha < 0)) * ...
[0, 1, 1; 1, 0, 1; 1, 1, 0];
highlights_alpha = sum(highlights_alpha);
img2 = bsxfun(@(a, b)a ./ (1 - b), ...
img2, reshape(highlights_alpha, [1, 1, 3]));
img2 = min(max(img2, 0), 1); % Clamp to 0 and 1
% Do shadows
shadows_alpha = 0.0039230 * shadows_coef;
shadows_alpha = diag(-shadows_alpha .* (shadows_alpha < 0)) * ...
[1, 0, 0; 0, 1, 0; 0, 0, 1] + ...
diag(shadows_alpha .* (shadows_alpha > 0)) * ...
[0, 1, 1; 1, 0, 1; 1, 1, 0];
shadows_alpha = sum(shadows_alpha);
img2 = bsxfun(@(a, b)(a - b) ./ (1 - b), ...
img2, reshape(shadows_alpha, [1, 1, 3]));
img2 = min(max(img2, 0), 1); % Clamp to 0 and 1
% Do midtones
midtones_gamma = exp(sum(diag(-0.0033944 * midtones_coef) * ...
[1, -1, -1; -1, 1, -1; -1, -1, 1]));
img2 = bsxfun(@power, img2, ...
reshape(midtones_gamma, [1, 1, 3]));
img2 = min(max(img2, 0), 1); % Clamp to 0 and 1
figure(1); clf;
imshow(img2);
现在,你也可以用 matlab 完美实现色彩平衡功能了~
