首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >告别 HSV 红色死区:用 Lab 色空间重新设计工业级颜色检测

告别 HSV 红色死区:用 Lab 色空间重新设计工业级颜色检测

作者头像
javpower
发布2026-04-28 13:18:13
发布2026-04-28 13:18:13
990
举报

告别 HSV 红色死区:用 Lab 色空间重新设计工业级颜色检测

一套基于 OpenCV + Lab 色空间的完整颜色检测方案,覆盖像素分类、色差计算、形状度量、颜色比对,直接可用。


01 | 为什么不用 HSV?

做视觉检测的同学一定写过类似代码:

代码语言:javascript
复制
Core.inRange(hsvImage, new Scalar(0, 100, 100), new Scalar(10, 255, 255), mask1);
Core.inRange(hsvImage, new Scalar(170, 100, 100), new Scalar(180, 255, 255), mask2);
Core.add(mask1, mask2, redMask);

HSV 检测颜色有三个经典痛点

痛点一:红色跨越 0°/180° 边界

OpenCV 的 HSV 色相范围是 0-180,红色正好卡在两端——H=0 和 H=170~180 都是红。每次检测红色都要写两次 inRange 再合并,代码丑且容易忘。

HSV 色轮:红色卡在 0° 和 360° 两端,形成天然"断带"
HSV 色轮:红色卡在 0° 和 360° 两端,形成天然"断带"

HSV 色轮:红色卡在 0° 和 360° 两端,形成天然"断带"

痛点二:S/V 死区

低饱和度或低明度时,HSV 的色相 H 值不稳定、无意义。你必须额外判断 S 和 V 的范围来排除"伪颜色",阈值怎么定全靠经验。

痛点三:感知不均匀

HSV 中 H=10(橙)和 H=20(还是橙)的视觉差异,跟 H=60(黄绿)和 H=70(绿)的视觉差异完全不一样。色差计算在 HSV 空间里毫无意义。

Lab 色空间天然解决这三个问题。


02 | Lab 色空间的三个优势

Lab 色空间用 a*b* 两个轴表示颜色:

Lab 色空间 a*b* 平面:横轴绿-红,纵轴蓝-黄,角度即色相,半径即色度
Lab 色空间 a*b* 平面:横轴绿-红,纵轴蓝-黄,角度即色相,半径即色度

Lab 色空间 a*b* 平面:横轴绿-红,纵轴蓝-黄,角度即色相,半径即色度

代码语言:javascript
复制
a*: 负值=绿  ←  0(中性)  →  正值=红
b*: 负值=蓝  ←  0(中性)  →  正值=黄
L*:  0(黑)  →  100(白)

在 (a*, b*) 平面上,每个像素就是一个点。点到原点的距离是色度 (chroma)——色度低就是黑白灰,色度高就是彩色。点的角度就是色相——一圈连续 360°,红色不再断裂。

3D Lab 色空间示意:L* 为亮度轴,a*b* 平面展开全部色相
3D Lab 色空间示意:L* 为亮度轴,a*b* 平面展开全部色相

3D Lab 色空间示意:L* 为亮度轴,a*b* 平面展开全部色相


03 | 分类算法:从像素到 10 种颜色

整个分类过程分两步走:

第一步:色度分流

OpenCV 的 Lab 是 CV_8UC3,L/a/b 各 0-255,中性点在 128。

第二步:色相角度分类

有彩色用 atan2(b-128, a-128) 算色相角度,对照色相表:

颜色

色相角度范围

[0°, 40°) ∪ [310°, 360°)

[40°, 70°)

[70°, 100°)

绿

[100°, 220°)

[220°, 260°)

[260°, 310°)

[310°, 360°) ∪ ...

实际实现中转回 HSV 的 H 通道做分类(因为 OpenCV 已有高效的 HSV 转换),但 Lab 的优势体现在色差计算和像素级过滤上。

10 种标准颜色,全覆盖,无死区,无重叠


04 | 性能设计:一次预计算,多次查询

工业场景经常需要对同一张图片检测多个区域。如果每个区域都做一次 BGR→Lab 转换,性能浪费严重。

设计思路:构造时一次性完成预计算,查询时只做掩码 + 连通域分析。

使用方式:

代码语言:javascript
复制
// 预计算一次
try (ColorImageAnalyzer analyzer = ColorDetector.prepare(image)) {

    // 查询多次,每次只做轻量操作
    List<ColorResult> r1 = analyzer.detect(polygon1, 3, 0.05, SortOrder.BY_PERCENT);
    List<ColorResult> r2 = analyzer.detect(polygon2, 5, 0.02, SortOrder.LEFT_TO_RIGHT);
    List<ColorResult> r3 = analyzer.detect(polygon3, 10, 0.01, SortOrder.TOP_TO_BOTTOM);
}

多区域批量检测也有静态方法,内部自动共享同一个 analyzer:

代码语言:javascript
复制
Map<String, List<Point2D>> regions = Map.of("zone1", poly1, "zone2", poly2);
Map<String, List<ColorResult>> batch = ColorDetector.detectColors(image, regions, 3, 0.05, BY_PERCENT);

05 | 检测结果:远不止颜色名称

每个 ColorResult 包含的信息量远超"这是什么颜色":

这些字段为下游任务提供了丰富的输入——无论是色差比对、形状匹配,还是灯带位置判断。


06 | 四层过滤:从粗到细,精准定位

ColorFilter 支持四层 AND 组合过滤:

关键设计:所有过滤在 buildLabelMap 阶段逐像素执行,不满足的像素直接标记为背景(label=0),后续连通域分析完全忽略它们,零额外开销。

OpenCV connectedComponentsWithStats 标签图示意:每个连通域被赋予独立标签编号
OpenCV connectedComponentsWithStats 标签图示意:每个连通域被赋予独立标签编号

OpenCV connectedComponentsWithStats 标签图示意:每个连通域被赋予独立标签编号

代码语言:javascript
复制
ColorFilter filter = ColorFilter.builder()
    .includeColors(Set.of("blue"))
    .hsvRanges(List.of(HSVColorRange.of(100, 130, 80, 255, 50, 255)))
    .hexMatch(HexColorMatch.of("#0066FF", 15.0))
    .build();

List<ColorResult> results = ColorDetector.detectColors(image, polygon,
    3, 0.05, SortOrder.BY_PERCENT, filter);

07 | 色差计算:ΔE76

两个颜色"差多少"不是拍脑袋说的,Lab 空间有标准的色差公式 ΔE76:

ΔE 色差示意:相同 Lab 值差异在人眼中的可感知程度
ΔE 色差示意:相同 Lab 值差异在人眼中的可感知程度

ΔE 色差示意:相同 Lab 值差异在人眼中的可感知程度

一行代码:

代码语言:javascript
复制
double delta = ColorDetector.colorDifference(resultA, resultB);

实际应用场景:检测灯带颜色是否偏移、产品配色是否一致、印刷色差是否在公差内。


08 | 形状度量:不只是"有多大"

工业检测经常需要描述色块的形状特征。ShapeMetrics 基于 OpenCV 轮廓计算,提供 9 项指标:

Feret 直径测量示意:maxFeret 为最大卡尺直径,minFeret 为最小卡尺直径
Feret 直径测量示意:maxFeret 为最大卡尺直径,minFeret 为最小卡尺直径

Feret 直径测量示意:maxFeret 为最大卡尺直径,minFeret 为最小卡尺直径

还有形状相似度——用 Hu 不变矩比较两个轮廓:

Hu 不变矩形状匹配:平移/缩放/旋转不变,不同形态的同一字母被识别为相似
Hu 不变矩形状匹配:平移/缩放/旋转不变,不同形态的同一字母被识别为相似

Hu 不变矩形状匹配:平移/缩放/旋转不变,不同形态的同一字母被识别为相似

代码语言:javascript
复制
double sim = ColorDetector.shapeSimilarity(contourA, contourB);
// 返回值越小越相似:<0.1 几乎一致,>1.0 完全不同
// 平移、缩放、旋转不变——10×5 和 100×50 的矩形判定为相同形状

09 | 颜色比对:ColorComparator

检测出颜色后,实际业务经常需要回答一个问题:**"这两个区域的颜色分布是否一致?"**

ColorComparator 封装了多维度比对逻辑:

内置 5 种预设模式:

使用示例:

代码语言:javascript
复制
ColorComparator cmp = ColorComparator.builder()
    .proportionTolerance(0.15)
    .maxDeltaE(15.0)
    .missingColorPenalty()
    .scoreThreshold(0.7)
    .build();

ColorMatchResult result = cmp.compare(templateColors, targetColors);

result.isPassed();           // true/false
result.getScore();           // 0.0 ~ 1.0 综合评分
result.getDetails();         // 每种颜色的匹配细节
result.getRuleScores();      // 每条规则的得分
result.getTemplateCoverage();// 模板覆盖率

10 | 完整使用示例

场景:检测 PCB 板上 3 个指示灯的颜色和状态

工业 AOI 视觉检测:PCB 板 LED 指示灯颜色识别是典型应用场景
工业 AOI 视觉检测:PCB 板 LED 指示灯颜色识别是典型应用场景

工业 AOI 视觉检测:PCB 板 LED 指示灯颜色识别是典型应用场景

代码语言:javascript
复制
// 1. 读取图片
Mat image = imread("pcb_board.jpg");

// 2. 定义 3 个指示灯的多边形区域
Map<String, List<Point2D>> leds = Map.of(
    "LED1", List.of(p(100,50), p(120,50), p(120,70), p(100,70)),
    "LED2", List.of(p(200,50), p(220,50), p(220,70), p(200,70)),
    "LED3", List.of(p(300,50), p(320,50), p(320,70), p(300,70))
);

// 3. 过滤条件:只看红/绿/蓝,忽略黑白灰,且 ΔE ≤ 20
ColorFilter filter = ColorFilter.builder()
    .includeColors(Set.of("red", "green", "blue"))
    .hexMatch(HexColorMatch.of("#FF0000", 20.0))
    .hexMatch(HexColorMatch.of("#00FF00", 20.0))
    .hexMatch(HexColorMatch.of("#0000FF", 20.0))
    .build();

// 4. 批量检测
Map<String, List<ColorResult>> results = ColorDetector.detectColors(
    image, leds, 1, 0.10, SortOrder.BY_PERCENT, filter);

// 5. 判断每个灯的状态
for (var entry : results.entrySet()) {
    String led = entry.getKey();
    List<ColorResult> colors = entry.getValue();
    if (colors.isEmpty()) {
        System.out.println(led + ": 灭");
    } else {
        ColorResult c = colors.get(0);
        System.out.printf("%s: %s %s (%.0f%%)%n",
            led, c.getColorName(), c.getHexColor(), c.getPercent() * 100);
    }
}

输出:

代码语言:javascript
复制
LED1: red #D42A2A (92%)
LED2: green #22CC44 (88%)
LED3: 灭

场景:比对模板和产品的颜色是否一致

代码语言:javascript
复制
List<ColorResult> template = ColorDetector.detectColors(tplImage, tplPoly, 3, 0.05, BY_PERCENT);
List<ColorResult> product  = ColorDetector.detectImages(prodImage, prodPoly, 3, 0.05, BY_PERCENT);

ColorComparator cmp = ColorComparator.builder()
    .proportionTolerance(0.10)
    .maxDeltaE(10.0)
    .failOnMissingColor()
    .scoreThreshold(0.75)
    .build();

ColorMatchResult result = cmp.compare(template, product);
if (result.isPassed()) {
    System.out.println("颜色合格,评分: " + result.getScore());
} else {
    System.out.println("颜色不合格,查看差异:");
    for (ColorMatchDetail d : result.getDetails()) {
        System.out.printf("  %s: ΔE=%.1f, 占比差=%.1f%%, 状态=%s%n",
            d.getColorName(), d.getDeltaE(), d.getProportionDiff() * 100, d.getStatus());
    }
}

11 | 技术选型总结

设计决策

选择

原因

色空间

Lab (CV_8UC3)

感知均匀、亮度独立、色相连续

像素分类

HSV H 通道 + S/V 阈值

OpenCV HSV 转换高效,结合 Lab 做色差

连通域

connectedComponentsWithStats

一次调用拿齐像素数/质心/bbox

形状度量

minAreaRect + convexHull + arcLength

工业 Feret 直径标准

形状相似度

Hu 不变矩 (matchShapes)

平移/缩放/旋转不变

色差

ΔE76 (CIE76)

简单、实用、与 Lab 天然匹配

过滤架构

标签图阶段过滤,label=0 排除

零额外开销,不影响后续分析

预计算

ColorImageAnalyzer (AutoCloseable)

一次转换,多次查询


12 | 写在最后

颜色检测看似简单,做到工业级需要处理很多细节:

  • 色空间选择决定了分类的准确性和色差计算的意义
  • 预计算架构决定了多区域场景下的性能
  • 多层过滤决定了从"粗筛"到"精准"的灵活性
  • 形状度量让颜色检测从"有没有"升级到"长什么样"

这套方案已经覆盖了灯带检测、PCB 指示灯判定、产品配色比对等多个场景。

如果你也在做类似的视觉检测工作,希望这篇文章对你有帮助。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-04-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Coder建设 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 告别 HSV 红色死区:用 Lab 色空间重新设计工业级颜色检测
    • 01 | 为什么不用 HSV?
    • 02 | Lab 色空间的三个优势
    • 03 | 分类算法:从像素到 10 种颜色
      • 第一步:色度分流
      • 第二步:色相角度分类
    • 04 | 性能设计:一次预计算,多次查询
    • 05 | 检测结果:远不止颜色名称
    • 06 | 四层过滤:从粗到细,精准定位
    • 07 | 色差计算:ΔE76
    • 08 | 形状度量:不只是"有多大"
    • 09 | 颜色比对:ColorComparator
    • 10 | 完整使用示例
      • 场景:检测 PCB 板上 3 个指示灯的颜色和状态
      • 场景:比对模板和产品的颜色是否一致
    • 11 | 技术选型总结
    • 12 | 写在最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档