目录
快速指南 -- 仅供参考,请根据项目需求来定=====( ̄▽ ̄*)b
为什么要用 Lab 插值?????
如果你想让颜色渐变“看起来对人眼更自然、更线性”,用 Lab 插值通常比在 RGB 空间线性插值好很多。
原因很直白:
-
RGB(尤其 sRGB)是为了显示器/设备设计的,颜色数值对人的视觉并不是线性的。两个 RGB 值中点不一定看起来是“视觉上的中点”。
-
CIE Lab(L*a*b*)是经过色觉建模得到的接近“感知均匀”的色彩空间:同样的数值差在视觉上更接近同等的感知差异。
所以在 Lab 空间里做线性插值,视觉上往往更平滑、不会中间出现不自然的“泛灰”或亮度骤变。
Lab 的背景渊源
CIE Lab 出自国际照明委员会(CIE),目的是尽量把“颜色差(ΔE)”变成对人眼更一致的量度。它基于中间的 XYZ 空间,再做非线性变换得到 L, a, b 三个分量:
-
L 表示亮度(0 黑 到 100 亮)
-
a 表示绿—红轴
-
b 表示蓝—黄轴
数学流程(RGB → XYZ → Lab)
必须要懂的关键步骤(sRGB 情况,白点 D65)
实际工程里你常常碰到 sRGB(网页/显示器色彩)。完整流程:
-
先把 0..255 的 sRGB 规范化到 0..1(浮点)
-
做反伽玛(linearize):sRGB 是非线性的(伽玛曲线)
[ C_{lin} = \begin{cases} \dfrac{C_{srgb}}{12.92}, & C_{srgb} \le 0.04045 \ \left(\dfrac{C_{srgb}+0.055}{1.055}\right)^{2.4}, & C_{srgb} > 0.04045 \end{cases} ]对 R,G,B 三通道分别做。
-
线性 RGB → XYZ(矩阵变换)
[ \begin{bmatrix} X\Y\Z \end{bmatrix} \begin{bmatrix} 0.4124564 & 0.3575761 & 0.1804375\ 0.2126729 & 0.7151522 & 0.0721750\ 0.0193339 & 0.1191920 & 0.9503041 \end{bmatrix} \begin{bmatrix} R_{lin}\G_{lin}\B_{lin} \end{bmatrix} ](假定白点 D65)用 sRGB 到 XYZ 的 3×3 矩阵(数值是固定的)
-
XYZ → Lab(使用参考白点 (X_n,Y_n,Z_n),sRGB 通常用 D65:(X_n=95.047, Y_n=100.000, Z_n=108.883)(也可用归一化 1.0))
先做归一化 (x = X/X_n, y = Y/Y_n, z = Z/Z_n),然后对每个分量做 f 函数:[ f(t) = \begin{cases} t^{1/3}, & t > (\frac{6}{29})^3 \ \frac{1}{3}(\frac{29}{6})^2 t + \frac{4}{29}, & t \le (\frac{6}{29})^3 \end{cases} ]最后
[ L^* = 116 f(y) - 16,\quad a^* = 500\big(f(x)-f(y)\big),\quad b^* = 200\big(f(y)-f(z)\big) ]
读到这里你可能觉得“哇,好复杂,我想放弃XDD”。没错手写转换会比较繁琐,但只要理解步骤就行,工程里常用库(OpenCV、LittleCMS、colour-science)会帮你做这些转换。
插值本体:在 Lab 空间做线性插值
把起点 RGB 转成 Lab:(L_1,a_1,b_1);终点 RGB 转成 Lab:(L_2,a_2,b_2)。
然后对三通道做线性插值:
[
L(t)=L_1 + t (L_2-L_1),\quad a(t)=a_1 + t (a_2-a_1),\quad b(t)=b_1 + t (b_2-b_1)
]
再把 (L(t),a(t),b(t)) 转回 RGB(反向过程)。这就是 Lab 插值的全部精髓。
实践:C++ + OpenCV 示例
在 C++ 工程中,最简单的办法是借助 OpenCV(它支持 RGB↔Lab 的转换,并且跨平台)。下面给出完整的示例,用来生成 N 步渐变颜色(Lab 插值)并打印 RGB 值或保存为一条小图片。
注意 OpenCV 的颜色顺序是 BGR(而不是 RGB),而且函数
cvtColor期望的值域取决于你用的类型(CV_8UC3 或 CV_32FC3)。下面用浮点(0..1)更直观。
代码(C++ + OpenCV)
// build: requires OpenCV
// g++ lab_gradient.cpp -o lab_gradient `pkg-config --cflags --libs opencv4`
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
#include <iomanip>
// helper: convert 0..255 RGB -> cv::Vec3f BGR float 0..1
cv::Vec3f rgbToBgrFloat(int r, int g, int b) {
return cv::Vec3f(b/255.0f, g/255.0f, r/255.0f);
}
int main() {
// start and end colors (R,G,B)
cv::Vec3f start_rgb(158, 222, 134);
cv::Vec3f end_rgb(0, 128, 0);
// convert to BGR float (0..1) for OpenCV
cv::Mat start_rgb_mat(1,1,CV_32FC3);
start_rgb_mat.at<cv::Vec3f>(0,0) = rgbToBgrFloat((int)start_rgb[0], (int)start_rgb[1], (int)start_rgb[2]);
cv::Mat end_rgb_mat(1,1,CV_32FC3);
end_rgb_mat.at<cv::Vec3f>(0,0) = rgbToBgrFloat((int)end_rgb[0], (int)end_rgb[1], (int)end_rgb[2]);
// convert to Lab (OpenCV uses D65 for CV_32F with COLOR_BGR2Lab)
cv::Mat start_lab, end_lab;
cv::cvtColor(start_rgb_mat, start_lab, cv::COLOR_BGR2Lab);
cv::cvtColor(end_rgb_mat, end_lab, cv::COLOR_BGR2Lab);
cv::Vec3f sLab = start_lab.at<cv::Vec3f>(0,0);
cv::Vec3f eLab = end_lab.at<cv::Vec3f>(0,0);
int steps = 10;
std::vector<cv::Vec3b> resultColors; // will store BGR 0..255
for (int i = 0; i < steps; ++i) {
float t = steps==1 ? 0.f : (float)i / (steps - 1);
cv::Vec3f interpLab = sLab + t*(eLab - sLab);
// convert back to BGR
cv::Mat labMat(1,1,CV_32FC3);
labMat.at<cv::Vec3f>(0,0) = interpLab;
cv::Mat bgrMat;
cv::cvtColor(labMat, bgrMat, cv::COLOR_Lab2BGR);
cv::Vec3f bgrFloat = bgrMat.at<cv::Vec3f>(0,0);
// clamp and convert to 0..255 uchar
cv::Vec3b bgrU8(
(uchar)std::round(std::min(std::max(bgrFloat[0]*255.0f, 0.0f), 255.0f)),
(uchar)std::round(std::min(std::max(bgrFloat[1]*255.0f, 0.0f), 255.0f)),
(uchar)std::round(std::min(std::max(bgrFloat[2]*255.0f, 0.0f), 255.0f))
);
resultColors.push_back(bgrU8);
}
// print out as RGB for inspection
std::cout << "Lab interpolation result (RGB):\n";
for (auto &c : resultColors) {
// OpenCV store BGR
std::cout << "RGB(" << (int)c[2] << ", " << (int)c[1] << ", " << (int)c[0] << ")\n";
}
// optional: create a small image to visualize the gradient
int width = 600, height = 50;
cv::Mat vis(height, width, CV_8UC3);
for (int x = 0; x < width; ++x) {
float t = float(x) / float(width - 1);
int idx = int(t * (steps - 1) + 0.5f);
vis.col(x) = resultColors[idx];
}
cv::cvtColor(vis, vis, cv::COLOR_BGR2RGB); // if you want to save as RGB PNG visually consistent
cv::imwrite("lab_gradient.png", vis);
return 0;
}
说明
-
我把起始/结束 RGB 先转成 OpenCV 的浮点 BGR,再
cvtColor(..., COLOR_BGR2Lab)得到 Lab 空间表示(OpenCV 的 Lab 通常 L:[0,100],a/b 约在 [-127,127] 范围内,但当使用 CV_32F 时具体尺度需参考文档;上面代码直接用 OpenCV API,避免自己写转换) -
在 Lab 空间做线性插值,最后再
cvtColor(..., COLOR_Lab2BGR)转回 BGR,最后 clamp 到 [0,255]。 -
可能会出现 out-of-gamut(Lab → RGB 得到负值或超过 255),所以需要 clamp 或做 gamut 映射。
常见坑
1. Out-of-gamut(超出显示范围)
Lab 空间的很多值映射回 RGB 可能不是合法的显示颜色(会出现负值或大于 1)。解决办法:
-
最简单:clamp(0..255)——实用但可能失真(颜色被剪切)。
-
更好的做法:用色彩管理库(LittleCMS)做色彩空间转换并指定渲染意图(如相对色度、绝对色度、感知等)以做更合理的 gamut 映射。
2. 白点/参考白的统一
确保转换使用的白点(通常 sRGB 使用 D65)一致。OpenCV 的默认转换假定 sRGB/D65,使用库时注意不要混白点。
3. 精度/类型
使用浮点(CV_32F)进行中间计算,精度更好。用 CV_8U 可能导致量化误差。
4. 颜色抖动(banding)
如果生成很细的渐变但出现条带(banding),可能是量化问题(8-bit 不够)或显示设备本身问题。解决方式:在渲染时使用更高位深(16-bit 或 float)或做抖动处理。
5. 性能
Lab 转换比直接 RGB lerp 稍重(需要矩阵乘法 + gamma 等),但对通常 UI/图像级任务,OpenCV 的实现很快。只有在超高性能场景才需要手工优化或用 GPU。
如何确认 Lab 插值更好
-
做一个视觉对比:在同一图上渲染 RGB lerp 与 Lab lerp 的两条渐变带,分别用相同的起终颜色(比如你给的绿色系)。真人观看或用色差度量(ΔE)比较中间色与起终颜色的感知距离是否“线性”。
-
如果你在做数据可视化(需要严格的 perceptual uniformity),建议使用 Lab 或更现代的色彩空间(CAM16-UCS / Oklab)。
进阶(真的好难XDD):还有哪些更好的色彩空间?
-
Oklab / Oklch:最近比较火的替代方案,设计目标是更简单、更接近人眼感知且避免某些 Lab 的问题。若工程可控,Oklab 插值效果也非常好。
-
CAM16-UCS:色觉模型,能更好地预测视觉差异,但实现复杂。
总结:如果你要追求“更自然”的渐变而不想复杂到色彩管理专家级别,Lab 插值 + 简单 gamut clamp 是非常实际、收益高的方案;更进阶可考虑 Oklab 或完整色彩管理流程(ICC profile + LittleCMS)。
快速指南 -- 仅供参考,请根据项目需求来定=====( ̄▽ ̄*)b
-
首选:使用 OpenCV 做 Lab 插值(见上面代码),能快速上线且视觉效果好。
-
若要求严格:在转换时使用 LittleCMS 做 RGB↔Lab(或使用 ICC profile)、选择合适的渲染意图做 gamut 映射。
-
若为了速度:使用 RGB lerp(简单且速度快),但在浅-深对比大的情况下视觉上可能不如 Lab。
-
调试技巧:把每一步(RGB→Lab→插值→RGB)保存为图像,对比并查看是否有颜色剪切/条带,必要时增加步数或使用 16-bit。
暂时就只有这些,后续学到哪再总结吧。

2071

被折叠的 条评论
为什么被折叠?



