OpenCV Color Correction 色彩校正系列:线性化变换(Linearization Transformation)原理、公式与源码实现解析
【免费下载链接】opencvOpen Source Computer Vision Library项目地址: https://gitcode.com/GitHub_Trending/opencv31/opencv
色彩校正(Color Correction)的第一步,往往就是把检测到的颜色"线性化"。本文以 OpenCVphoto模块中 Color Correction Model(CCM)的官方教程文档 linearization_transformation.markdown 为主体,系统讲解**恒等变换(Identical Transformation)、Gamma 校正、多项式拟合(含逐通道拟合、灰度多项式拟合、对数多项式拟合)**三类线性化方法的数学原理与适用场景,并结合本仓库 modules/photo/src/ccm 的linearize源码实现,说明每一类方法在cv::ccm中究竟如何被建模、拟合与调用。读完本文,你将能够正确理解 CCM 流水线中线性化环节的作用,掌握各线性化方法的公式与参数含义,并能在自己的 C++/Python 项目中准确配置LinearizationType完成图像颜色校正。
本篇是色彩校正系列中的"线性化"篇,完整的端到端模型使用请参考姊妹文档 Color Correction Model 教程。
概览:为什么要先做线性化?
在颜色校正流程里,线性化(Linearization)是第一个步骤:先把相机检测到的颜色转换为与亮度成线性关系的值。之所以需要这一步,是因为输入色彩空间(input color space)通常未经过标定,CCD/CMOS 传感器采集到的原始响应与真实光强之间往往存在非线性失真;只有在线性化的 RGB 空间里,才能用一个简单的 $3\times 3$ 颜色校正矩阵(CCM)把设备颜色映射到参考颜色。
由于输入色彩空间未知,无法直接套用标准色彩空间的"解码函数",因此实际工程中普遍采用**经验方法(empirical methods)**完成线性化,最常见的有三种:
- 恒等变换(Identical Transformation)—— 不做任何改动,适用于输入本身已线性(如 RAW);
- Gamma 校正(Gamma Correction)—— 用幂函数做非线性补偿,$gamma$ 通常取 2.2;
- 多项式拟合(Polynomial Fitting)—— 用检测色拟合参考色,得到逐元素映射多项式,派生出资深变体:逐通道拟合、灰度拟合、对数拟合。
线性化本质上是一个逐元素(element-wise)函数,即对每个颜色通道独立做标量映射。原文档给出了如下统一符号约定:
| 符号 | 含义 |
|---|---|
| $C$ | 任意颜色通道($R$、$G$ 或 $B$) |
| $R, G, B$ | 分别表示红、绿、蓝三个颜色通道 |
| $G$ | 灰度(Grayscale)值 |
| $s,\ sl$ | 检测数据(detected data)及其线性化值;前者是输入,后者是输出 |
| $d,\ dl$ | 参考数据(reference data)及其线性化值 |
即:下标 $s$ 表示相机实测数据,$dl$ 表示来自色卡/参考光源的标准线性值;线性化的目标就是寻找映射 $f$,使得 $C_{sl}=f(C_s)$ 尽量逼近 $C_{dl}$。
恒等变换(Identical Transformation)
公式:
$$ C_{sl}=C_s $$
即输入什么、输出什么,不做任何改变。其使用前提是:输入 RGB 图像的三刺激值(tristimulus values)已经与亮度成正比,无需再线性化。
最典型的场景是RAW 格式的测量数据:RAW 数据在读出阶段只做模拟增益与量化,尚未经过相机内部的 Tone Mapping、Gamma 编码等非线性处理,因此本身就处于线性光域,可直接进入后续矩阵拟合环节。
对应到源码,这一策略被建模为LinearIdentity类(linearize.hpp),其linearize()直接返回输入(linearize.cpp):
Mat Linear::linearize(Mat inp) { return inp; };Gamma 校正(Gamma Correction)
Gamma 校正是对 RGB 空间施加非线性的一种常用手段。关于 Gamma 的更完整背景(编码 Gamma 与解码 Gamma 等)可参见 Color Space 相关文档;在线性化环节里,Gamma 校正是"用幂函数反推线性值"的经验近似。
在线性化应用中,$gamma$ 通常取2.2,同时允许用户自定义数值(例如某些相机的 OETF 曲线接近 1/1.8 或 1/2.0 时可按需调整)。
公式(分段定义,保证负值也有定义):
$$ \begin{aligned} C_{sl}=C_s^{\gamma},&\qquad C_s\ge 0\ C_{sl}=-(-C_s)^{\gamma},&\qquad C_s< 0 \end{aligned} $$
即对非负输入直接做幂运算,对负输入先取反、做幂、再取负,从而保证整个实数域上映射连续且有符号一致。
源码实现:cv::ccm::gammaCorrection
OpenCV 在 modules/photo/include/opencv2/photo/ccm.hpp 中将 Gamma 校正作为公开 API 导出:
CV_EXPORTS_W void gammaCorrection(InputArray src, OutputArray dst, double gamma);其实现位于 modules/photo/src/ccm/utils.cpp,内部实现要点非常值得注意:
- 使用
CV_Assert(gamma > 0)强制 gamma 必须大于 0; - 依据图像深度选择归一化最大值
maxVal:CV_8U为 255.0、CV_16U为 65535.0、CV_16S为 32767.0、CV_32F/CV_64F为 1.0,其他深度直接抛出StsUnsupportedFormat; - 对
CV_8U图像做了查找表(LUT)优化:预先构造 256 项lut[i]=pow(i/255.0, gamma)*255.0,再调用cv::LUT完成整幅图像映射,速度远快于逐像素pow; - 其他深度则先把像素归一化到 $[0,1]$,
cv::pow后按maxVal还原为原深度。
在 CCM 线性化框架中,Gamma 方法由LinearGamma类封装(linearize.cpp),其linearize()就是直接调用上面这个gammaCorrection:
Mat LinearGamma::linearize(Mat inp) { Mat out; gammaCorrection(inp, out, gamma); return out; };多项式拟合(Polynomial Fitting)
当设备响应曲线比单一幂函数更"扭曲"时,可以用多项式逼近任意单调曲线。多项式拟合类线性化直接把线性化函数建模为一个多项式:
$$ f(x)=a_nx^n+a_{n-1}x^{n-1}+\dots+a_0 $$
则线性化输出为:
$$ C_{sl}=f(C_s) $$
使用约束:
- 次数一般取 $n \le 3$,以避免过拟合(overfitting);
- 拟合需要"线性化后的参考色"与对应的"检测色"成对参与;
- 并非所有色块都能参与计算——检测结果中发生**饱和(saturation)**的颜色必须被剔除,详见算法介绍文档(在 ColorCorrectionModel 中对应
setSaturatedThreshold与coloredPatchesMask)。
多项式拟合在原文档基础上按拟合方式细分为三种子方法,下面逐一展开并对照源码。
逐通道多项式拟合(Fitting Channels Respectively)
对 RGB 三通道分别使用三个独立多项式 $r(x), g(x), b(x)$:
$$ \begin{aligned} R_{sl}&=r(R_s)\ G_{sl}&=g(G_s)\ B_{sl}&=b(B_s) \end{aligned} $$
每个多项式的生成准则是:最小化检测数据与线性化参考数据之间的残差平方和(RSS)。以 R 通道为例:
$$ R=\arg\min_{f}\left(\sum\left(R_{dl}-f(R_s)\right)^2\right) $$
这等价于对下面这组方程做最小二乘回归:
$$ \begin{aligned} f(R_{s1})&=R_{dl1}\ f(R_{s2})&=R_{dl2}\ \dots \end{aligned} $$
将多项式形式代入后,方程可写成 Vandermonde 矩阵形式:
$$ \begin{bmatrix} R_{s1}^{n} & R_{s1}^{n-1} & \dots & 1\ R_{s2}^{n} & R_{s2}^{n-1} & \dots & 1\ \vdots & \vdots & \ddots & \vdots \end{bmatrix} \begin{bmatrix} a_{n}\ a_{n-1}\ \vdots \ a_0 \end{bmatrix}
\begin{bmatrix} R_{dl1}\ R_{dl2}\ \vdots \end{bmatrix} $$
即矩阵方程:
$$ AX=B $$
系数通过正规方程求解:
$$ X=(A^TA)^{-1}A^TB $$
在 Python/numpy 中可直接用numpy.polyfit得到系数:
R = polyfit(R_s, R_dl)关于单调性(monotonicity)的重要说明:理论上我们希望得到的多项式在区间 $[0,1]$ 上严格单调递增(这样才不会出现"颜色越亮、输出越暗"的荒谬反转),但约束单调性需要用非线性优化方法生成多项式,这会大幅增加程序复杂度。由于单调性并不影响颜色校正程序的正确运行(拟合多项式只用于把检测色映射到近似线性域,后续还有 CCM 矩阵与距离度量把关),所以工程实现仍采用普通polyfit。G、B 通道参数可用同样的方式分别导出。
源码实现:Polyfit 类与 LinearColor 模板
Polyfit类(linearize.hpp、linearize.cpp)忠实复现了上述矩阵求解过程:
Polyfit::Polyfit(Mat x, Mat y, int deg_) : deg(deg_) { int n = x.cols * x.rows * x.channels(); x = x.reshape(1, n); y = y.reshape(1, n); Mat_<double> A = Mat_<double>::ones(n, deg + 1); // Vandermonde 矩阵 for (int i = 0; i < n; ++i) for (int j = 1; j < A.cols; ++j) A.at<double>(i, j) = x.at<double>(i) * A.at<double>(i, j - 1); Mat y_(y); cv::solve(A, y_, p, DECOMP_SVD); // 最小二乘求解 AX=B }与文档中 $X=(A^TA)^{-1}A^TB$ 的闭式解不同,源码实际调用cv::solve(..., DECOMP_SVD)用SVD 分解做数值稳定的最小二乘求解,结果等价但更稳健。系数存于p,推理时对每个标量执行 $p_0+p_1 x+p_2 x^2+\dots$ 的霍纳式累加(fromEW)。
逐通道拟合本身由模板类LinearColor<T>承担(linearize.hpp):构造阶段把检测色src与参考色按mask取出后split成三个单通道,对 R/G/B 分别T(schannels[i], dchannels[i], deg)拟合出pr/pg/pb;推理阶段同样split输入、分别套用pr/pg/pb后再merge回三通道图像。这里T既可以是上面的Polyfit(对应LINEARIZATION_COLORPOLYFIT),也可以是下面的LogPolyfit(对应LINEARIZATION_COLORLOGPOLYFIT)。
灰度多项式拟合(Grayscale Polynomial Fitting)
该方法只用一个多项式作用于所有通道,仍然是把检测色向线性化参考色做polyfit,但只有参考色中的灰色块(gray patches)可以参与计算——因为彩色块的通道值差异会使单变量多项式无法同时拟合。
由于与参考色灰色块对应的检测色未必是灰色(相机白平衡/通道增益可能偏色),需要先把检测色"灰度化(grayed)"。原文档指出:灰度本应指 XYZ 色彩空间的Y 通道(亮度),但检测数据的色彩空间未定、无法精确转换到 XYZ,因此用sRGB 亮度公式近似:
$$ G_{s}=0.2126R_{s}+0.7152G_{s}+0.0722B_{s} $$
得到灰度后即可拟合:
$$ f=\mathrm{polyfit}(G_s,\ G_{dl}) $$
得到 $f$ 后,对所有像素/通道统一执行 $C_{sl}=f(C_s)$ 完成线性化。
源码实现:LinearGray 模板
LinearGray<T>(linearize.hpp)的实现与文档一一对应:
LinearGray(int deg_, Mat src, Color dst, Mat mask, RGBBase_ cs) : deg(deg_) { dst.getGray(); // 计算参考色的灰度掩码 grays Mat lear_gray_mask = mask & dst.grays; // 只保留"非饱和 ∧ 灰色"的色块 Mat gray; cvtColor(src, gray, COLOR_RGB2GRAY); // 检测色用 OpenCV 灰度转换近似 gray.copyTo(src); Mat dst_ = maskCopyTo(dst.toGray(cs.illumobserver), lear_gray_mask); calc(src, dst_); // polyfit(G_s, G_dl) }几个值得注意的实现细节:
- 参考色的灰色掩码由
Color::getGray()(color.hpp)在构造Color时预先算好; - 检测色的灰度化直接使用
cv::cvtColor(src, gray, COLOR_RGB2GRAY),其内置加权系数正是文档给出的 $0.2126/0.7152/0.0722$(sRGB/Rec.709 亮度系数),是文档公式在 OpenCV 中的直接落地; - 而参考色则通过
dst.toGray(cs.illumobserver)转换到 XYZ 的 Y 通道(真正的标准灰度),可见"检测色用 sRGB 近似、参考色用标准 XYZ Y"这一不对称处理与文档描述完全一致; - 最终只有一个多项式 $p$,
linearize()对任意输入直接return p(inp);。
对数多项式拟合(Logarithmic Polynomial Fitting)
Gamma 校正取对数后是一个线性关系:
$$ \ln(C_{sl})=\gamma\ln(C_s),\qquad C_s\ge0 $$
可见 $\ln(C_s)$ 与 $\ln(C_{sl})$ 之间存在线性关系。因此可以认为对数域公式是多项式关系的近似:存在多项式 $f$,使得:
$$ \begin{aligned} \ln(C_{sl})&=f(\ln(C_s)),\qquad C_s>0\ C_{sl}&=0,\qquad C_s=0 \end{aligned} $$
为什么要单独把 0 映射为 0?因为 $\exp(\ln(0))\to\infty$,对数值无法处理 0;所以公式规定通道分量为 0 时直接映射为 0。
拟合过程:对数值做polyfit:
$$ \begin{aligned} r&=\mathrm{polyfit}(\ln(R_s),\ \ln(R_{dl}))\ g&=\mathrm{polyfit}(\ln(G_s),\ \ln(G_{dl}))\ b&=\mathrm{polyfit}(\ln(B_s),\ \ln(B_{dl})) \end{aligned} $$
注意事项:ln的参数不能为 0。因此在拟合前必须把 $R_s$ 与 $R_{dl}$、$G_s$ 与 $G_{dl}$、$B_s$ 与 $B_{dl}$ 中所有值为 0 的样本成对删除。最终的拟合方程(推理形式)为:
$$ \begin{aligned} \ln(R_{sl})&=r(\ln(R_s)),\quad R_s>0;\qquad R_{sl}=0,\quad R_s=0\ \ln(G_{sl})&=g(\ln(G_s)),\quad G_s>0;\qquad G_{sl}=0,\quad G_s=0\ \ln(B_{sl})&=b(\ln(B_s)),\quad B_s>0;\qquad B_{sl}=0,\quad B_s=0 \end{aligned} $$
对灰度多项式(对数版)同样有:
$$ f=\mathrm{polyfit}\left(\ln(G_{sl}),\ \ln(G_{dl})\right) $$
以及推理规则 $\ln(C_{sl})=f(\ln(C_s))\ (C_s>0)$,$C_{sl}=0\ (C_s=0)$。
源码实现:LogPolyfit 类
LogPolyfit(linearize.cpp)将文档中的两个约定落实为两处关键代码:
LogPolyfit::LogPolyfit(Mat x, Mat y, int deg_) : deg(deg_) { Mat mask_ = (x > 0) & (y > 0); // ① 剔除 R_s 或 R_dl 中为 0 的样本 Mat src_, dst_, s_, d_; src_ = maskCopyTo(x, mask_); dst_ = maskCopyTo(y, mask_); log(src_, s_); log(dst_, d_); p = Polyfit(s_, d_, deg); // ② 在对数域做普通多项式拟合 }推理端(operator())则构造inp >= 0的掩码,对每个像素执行 $\log \to p(\cdot) \to \exp$,并把原来 $\le 0$ 的位置用掩码直接置零,与文档中"零分量直接映射为零"的公式完全吻合。
枚举定义与工厂函数:如何在代码中选择线性化方法
原文档给出的枚举名为LINEAR_TYPE:
enum LINEAR_TYPE { LINEARIZATION_IDENTITY, // No change LINEARIZATION_GAMMA, // Gamma correction; requires gamma value LINEARIZATION_COLORPOLYFIT, // Polynomial fitting for each channel; requires degree LINEARIZATION_COLORLOGPOLYFIT, // Logarithmic polynomial fitting; requires degree LINEARIZATION_GRAYPOLYFIT, // Grayscale polynomial fitting; requires degree and dst_whites LINEARIZATION_GRAYLOGPOLYFIT // Grayscale logarithmic polynomial fitting; requires degree and dst_whites };需要留意的是:在当前仓库的最新头文件中,该枚举的正式名字是cv::ccm::LinearizationType(见 ccm.hpp),枚举值名称与文档一致,均为LINEARIZATION_*前缀。如果阅读到使用LINEAR_TYPE名称的旧资料,应视为同一概念的早期命名。
六种类型与类实现的对应关系
这些枚举值在运行时由工厂函数getLinear()(linearize.cpp)映射到具体类:
枚举值(LinearizationType) | 方法 | 依赖参数 | 实例化类 | 拟合/处理对象 |
|---|---|---|---|---|
LINEARIZATION_IDENTITY | 恒等变换 | — | LinearIdentity | 原样输出 |
LINEARIZATION_GAMMA | Gamma 校正 | gamma | LinearGamma | 逐元素幂函数 |
LINEARIZATION_COLORPOLYFIT | 逐通道多项式拟合 | deg | LinearColor<Polyfit> | R/G/B 三通道各自拟合 |
LINEARIZATION_COLORLOGPOLYFIT | 逐通道对数多项式拟合 | deg | LinearColor<LogPolyfit> | 三通道在对数域各自拟合 |
LINEARIZATION_GRAYPOLYFIT | 灰度多项式拟合 | deg+ 参考色灰度 | LinearGray<Polyfit> | 单一多项式 |
LINEARIZATION_GRAYLOGPOLYFIT | 灰度对数多项式拟合 | deg+ 参考色灰度 | LinearGray<LogPolyfit> | 单一多项式(对数域) |
getLinear()内部就是一个switch,把linearizationType逐一映射到对应类的make_shared构造,并对非法值抛出CV_Error(Error::StsBadArg, "Wrong linearizationType!")。通过模板参数T在Polyfit/LogPolyfit间切换,是 C++ 层面复用"逐通道/灰度"两种拟合策略的精巧设计。
ColorCorrectionModel 中的配套 API 与默认值
线性化环节被集成在ColorCorrectionModel中,其配套 setter 与默认值均定义于 ccm.hpp:
| 方法 | 作用 | 默认值 | 生效条件 |
|---|---|---|---|
setLinearization(LinearizationType) | 选择线性化方法 | LINEARIZATION_GAMMA | 始终生效 |
setLinearizationGamma(double) | 设置 Gamma 值 | 2.2 | 仅当使用 Gamma 线性化 |
setLinearizationDegree(int) | 设置多项式次数 | 3 | 仅当使用四类 polyfit 方法 |
setSaturatedThreshold(lower, upper) | 设置饱和判定阈值,闭区间[lower, upper]内的颜色参与损失与初值计算 | 0, 0 | 饱和色剔除 |
线性化在 compute() 流水线中的位置
在 ccm.cpp 的ColorCorrectionModel::compute()中可以看到线性化的完整调用链:
Mat saturateMask = saturate(p->src, p->saturatedThreshold[0], p->saturatedThreshold[1]); p->linear = getLinear(p->gamma, p->deg, p->src, p->ref, saturateMask, (p->cs), p->linearizationType); p->calWeightsMasks(p->weightsList, p->weightsCoeff, saturateMask); p->srcRgbl = p->linear->linearize(maskCopyTo(p->src, p->mask)); // ← 对检测色执行线性化 p->ref.colors = maskCopyTo(p->ref.colors, p->mask); p->dstRgbl = p->ref.to(*(p->cs.l)).colors; // 参考色转到线性 RGB流程可归纳为四步:
saturate()依据饱和阈值生成saturateMask(对应文档"饱和检测色需要被移除");getLinear(...)按用户选择的linearizationType构造线性化对象(含用非饱和样本完成Polyfit/LogPolyfit的系数拟合);calWeightsMasks(...)计算权重与参与拟合的有效掩码;- 对掩码后的检测色调用
linear->linearize(...)得到线性检测色srcRgbl,与转换到同一线性 RGB 空间的参考色dstRgbl对齐,供后续 CCM 矩阵拟合与损失计算使用。
可见线性化质量直接影响后续 $3\times3$(CCM_LINEAR)或 $4\times3$(CCM_AFFINE)校正矩阵的拟合精度,是整条色彩校正链路的基石。
工程使用建议与验证线索
代码清单:所有线性化相关头文件与实现集中在 modules/photo/src/ccm/linearize.hpp、modules/photo/src/ccm/linearize.cpp 与 modules/photo/src/ccm/color.hpp,公共 API 见 ccm.hpp。引入方式即文档给出的:
#include <opencv2/photo/ccm.hpp>实践路径:需要真实运行的端到端示例时,可参考配套教程 Color Correction Model 及其在 samples/cpp 下通过
@snippet引用的示例源码(检测色卡 + 拟合 +correctImage应用)。该示例会演示如何在"检测出色卡色块颜色"之后,调用ColorCorrectionModel并选择合适的线性化参数。测试与回归:仓库内置 modules/photo/test/test_ccm.cpp 测试 CCM 模块整体行为,另有 modules/photo/perf/perf_ccm.cpp 做性能基准;修改或选用不同线性化方法后可通过这些测试做正确性回归验证。
选型小结:输入是 RAW 或已知线性的设备数据选
LINEARIZATION_IDENTITY;多数消费级相机的 sRGB/JPEG 输出选默认LINEARIZATION_GAMMA($\gamma=2.2$);非线性较强或需要更精细标定时选LINEARIZATION_COLORPOLYFIT,次数从 3 起、配合饱和色剔除使用;灰度版(GRAYPOLYFIT与GRAYLOGPOLYFIT)在参考色卡缺少足够彩色块、只有灰阶可用时更为稳健;对数版则更适合暗部信息丰富、需放大低亮度拟合权重的场景。
参考资料
- 本文主体内容整理自 OpenCV 官方教程 linearization_transformation.markdown。
- 原文档引用的 [1-3] 为多项式拟合方法与经验研究,[4] 描述非线性多项式生成方法(单调性约束优化),[5] 为灰度计算的 sRGB 近似公式。
- 源码依据:modules/photo/src/ccm/linearize.cpp、modules/photo/src/ccm/linearize.hpp、modules/photo/src/ccm/utils.cpp、modules/photo/src/ccm/ccm.cpp 与 ccm.hpp。
【免费下载链接】opencvOpen Source Computer Vision Library项目地址: https://gitcode.com/GitHub_Trending/opencv31/opencv
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考