Lab(CIE L*a*b*)插值学习

目录

为什么要用 Lab 插值?????

Lab 的背景渊源

数学流程(RGB → XYZ → Lab)

线性 RGB → XYZ(矩阵变换)

插值本体:在 Lab 空间做线性插值

实践:C++ + OpenCV 示例

常见坑

1. Out-of-gamut(超出显示范围)

2. 白点/参考白的统一

3. 精度/类型

4. 颜色抖动(banding)

5. 性能

如何确认 Lab 插值更好

进阶(真的好难XDD):还有哪些更好的色彩空间?

快速指南 -- 仅供参考,请根据项目需求来定=====( ̄▽ ̄*)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(网页/显示器色彩)。完整流程:

  1. 先把 0..255 的 sRGB 规范化到 0..1(浮点)

  2. 做反伽玛(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 三通道分别做。

  3. 线性 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 矩阵(数值是固定的)

  4. 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 lerpLab lerp 的两条渐变带,分别用相同的起终颜色(比如你给的绿色系)。真人观看或用色差度量(ΔE)比较中间色与起终颜色的感知距离是否“线性”。

  • 如果你在做数据可视化(需要严格的 perceptual uniformity),建议使用 Lab 或更现代的色彩空间(CAM16-UCS / Oklab)。


进阶(真的好难XDD):还有哪些更好的色彩空间?

  • Oklab / Oklch:最近比较火的替代方案,设计目标是更简单、更接近人眼感知且避免某些 Lab 的问题。若工程可控,Oklab 插值效果也非常好。

  • CAM16-UCS:色觉模型,能更好地预测视觉差异,但实现复杂。

总结:如果你要追求“更自然”的渐变而不想复杂到色彩管理专家级别,Lab 插值 + 简单 gamut clamp 是非常实际、收益高的方案;更进阶可考虑 Oklab 或完整色彩管理流程(ICC profile + LittleCMS)。


快速指南 -- 仅供参考,请根据项目需求来定=====( ̄▽ ̄*)b

  1. 首选:使用 OpenCV 做 Lab 插值(见上面代码),能快速上线且视觉效果好。

  2. 若要求严格:在转换时使用 LittleCMS 做 RGB↔Lab(或使用 ICC profile)、选择合适的渲染意图做 gamut 映射。

  3. 若为了速度:使用 RGB lerp(简单且速度快),但在浅-深对比大的情况下视觉上可能不如 Lab。

  4. 调试技巧:把每一步(RGB→Lab→插值→RGB)保存为图像,对比并查看是否有颜色剪切/条带,必要时增加步数或使用 16-bit。


暂时就只有这些,后续学到哪再总结吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沉夢志昂丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值