一套基于 OpenCV + Lab 色空间的完整颜色检测方案,覆盖像素分类、色差计算、形状度量、颜色比对,直接可用。
做视觉检测的同学一定写过类似代码:
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° 两端,形成天然"断带"
痛点二:S/V 死区
低饱和度或低明度时,HSV 的色相 H 值不稳定、无意义。你必须额外判断 S 和 V 的范围来排除"伪颜色",阈值怎么定全靠经验。
痛点三:感知不均匀
HSV 中 H=10(橙)和 H=20(还是橙)的视觉差异,跟 H=60(黄绿)和 H=70(绿)的视觉差异完全不一样。色差计算在 HSV 空间里毫无意义。
Lab 色空间天然解决这三个问题。

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

Lab 色空间 a*b* 平面:横轴绿-红,纵轴蓝-黄,角度即色相,半径即色度
a*: 负值=绿 ← 0(中性) → 正值=红
b*: 负值=蓝 ← 0(中性) → 正值=黄
L*: 0(黑) → 100(白)
在 (a*, b*) 平面上,每个像素就是一个点。点到原点的距离是色度 (chroma)——色度低就是黑白灰,色度高就是彩色。点的角度就是色相——一圈连续 360°,红色不再断裂。

3D Lab 色空间示意:L* 为亮度轴,a*b* 平面展开全部色相
整个分类过程分两步走:

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 种标准颜色,全覆盖,无死区,无重叠。
工业场景经常需要对同一张图片检测多个区域。如果每个区域都做一次 BGR→Lab 转换,性能浪费严重。
设计思路:构造时一次性完成预计算,查询时只做掩码 + 连通域分析。

使用方式:
// 预计算一次
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:
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);
每个 ColorResult 包含的信息量远超"这是什么颜色":

这些字段为下游任务提供了丰富的输入——无论是色差比对、形状匹配,还是灯带位置判断。
ColorFilter 支持四层 AND 组合过滤:

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

OpenCV connectedComponentsWithStats 标签图示意:每个连通域被赋予独立标签编号
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);
两个颜色"差多少"不是拍脑袋说的,Lab 空间有标准的色差公式 ΔE76:

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

一行代码:
double delta = ColorDetector.colorDifference(resultA, resultB);
实际应用场景:检测灯带颜色是否偏移、产品配色是否一致、印刷色差是否在公差内。
工业检测经常需要描述色块的形状特征。ShapeMetrics 基于 OpenCV 轮廓计算,提供 9 项指标:


Feret 直径测量示意:maxFeret 为最大卡尺直径,minFeret 为最小卡尺直径
还有形状相似度——用 Hu 不变矩比较两个轮廓:

Hu 不变矩形状匹配:平移/缩放/旋转不变,不同形态的同一字母被识别为相似
double sim = ColorDetector.shapeSimilarity(contourA, contourB);
// 返回值越小越相似:<0.1 几乎一致,>1.0 完全不同
// 平移、缩放、旋转不变——10×5 和 100×50 的矩形判定为相同形状
检测出颜色后,实际业务经常需要回答一个问题:**"这两个区域的颜色分布是否一致?"**
ColorComparator 封装了多维度比对逻辑:

内置 5 种预设模式:

使用示例:
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();// 模板覆盖率

工业 AOI 视觉检测:PCB 板 LED 指示灯颜色识别是典型应用场景
// 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);
}
}
输出:
LED1: red #D42A2A (92%)
LED2: green #22CC44 (88%)
LED3: 灭
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());
}
}
设计决策 | 选择 | 原因 |
|---|---|---|
色空间 | 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) | 一次转换,多次查询 |
颜色检测看似简单,做到工业级需要处理很多细节:
这套方案已经覆盖了灯带检测、PCB 指示灯判定、产品配色比对等多个场景。
如果你也在做类似的视觉检测工作,希望这篇文章对你有帮助。