简介:基于颜色的图像分割资源包面向 OpenCV 初学者与计算机视觉入门者,围绕像素颜色特征解决目标区域分离问题。压缩包体积仅 235KB,整体非常小巧,便于快速下载并对照源码进行学习。已有 732 人学习下载,适合希望从零上手颜色分割实践的读者。资源围绕色彩空间转换、阈值分割、K-means 聚类、混合高斯模型、边缘检测与区域生长等经典方法展开,既解释了肤色检测、植被识别等场景下为何常选用 HSV 或 YCrCb 空间,也通过 cvtColor()、threshold()、inRange() 等 OpenCV 常用函数的实际调用,展示从像素特征到二值掩码的完整实现路径,并帮助读者理解关键参数和算法选择。对希望将算法落地为可运行代码的读者,这份资源提供了简洁的示例工程,能帮助理解不同颜色分割策略的适用边界,并为后续学习 GrabCut、水平集等高级方法打下基础。 上个月接到一个小需求:在传送带上统计红、蓝两种工件的数量,视频流实时处理。我心里想这不就是送分题吗——遍历像素,判断RGB分量谁大谁小就完事。结果第一次现场测试就被打脸:同一个红色工件,光源直射和阴影边缘处的RGB值差了一倍多,固定阈值怎么调都顾此失彼。那天晚上我开始系统地补"基于颜色的图像分割"这一课,才明白这个看似基础的方法里藏着一整套颜色描述、阈值标定和抗干扰的工程学问。
基于颜色的图像分割,说白了就是把图像里颜色相近的像素归成一类,把目标区域从背景里分离出来。它不需要训练样本、不需要GPU,几条OpenCV函数就能跑起来,所以在工业分拣、农业视觉、医疗图像预筛、广告牌定位这些场景里,它至今仍是性价比极高的入场方案。这篇文章我就从那次翻车经历出发,把颜色分割的原理、颜色空间选择、阈值标定、抗干扰处理,以及如何和GrabCut、深度学习分割组合使用,完整地串一遍。不管你是刚接触OpenCV的新手,还是要在项目里快速落地分割模块,这套思路应该都能直接用上。
1. 先搞清楚颜色分割在解决什么问题
1.1 从"分拣工件"这个需求说起
颜色分割本质上是做"像素级别的分类":算法根据每个像素的颜色特征,判断它属于目标还是背景。这里的关键词是"颜色特征"——你用什么方式描述一个像素的颜色,直接决定了后续所有处理的质量。
我当初犯的错误,就是直接用RGB三通道做判断。这个思路在数学上完全成立,但在工程上非常脆弱。原因在于RGB三个分量高度耦合,任何一个通道发生变化(比如光照变暗导致所有通道值成比例下降),都会被误判成"变成了另一个颜色"。像素的RGB数值和人类视觉感知的"颜色"之间,隔着一道非常不稳定的映射关系。
所以做颜色分割,第一步不是写代码,而是回答三个问题:目标颜色的分布范围是什么?背景中是否存在相同或相近的颜色?光照条件是否可控?这三个问题想清楚了,基本能判断出颜色分割在这个项目里是主角、辅助角色,还是压根不该用。
1.2 哪些场景适合用,哪些场景会翻车
我整理了一个常用判断表,做方案选型时非常实用:
| 场景 | 颜色分割是否够用 | 原因 |
|---|---|---|
| 红蓝工件计数 | 够用 | 目标颜色差异大,光照可控 |
| 广告牌区域定位 | 够用,但需后处理 | 颜色鲜艳,但有反光和透视变形 |
| 口腔疾病图像分割 | 不够用 | 牙齿、牙龈、病变区域颜色差异细微 |
| 番茄成熟度识别 | 大部分够用 | 红绿对比明显,但果实受遮挡时需补充几何约束 |
| 无人车道路分割 | 不够用 | 路面颜色随光照和材质剧烈变化,需要语义理解 |
判断原则很简单:目标与背景是否有明确的颜色区分度,以及这种区分度在整条产线或整个使用周期内是否稳定。颜色分割的天然优势是速度快、可解释性强、调试直观;天然短板是对光照敏感、对纹理和形状不敏感。如果两个物体颜色接近但形状不同,单靠颜色必然翻车。
2. 颜色空间选择:为什么RGB阈值方案一开始就输了
2.1 RGB的三个通道和"感知距离"的错位
RGB是面向显示设备设计的颜色模型,红绿蓝三种色光叠加成不同的颜色。它的好处是硬件友好,屏幕直接拿这三个分量驱动像素;坏处是"数值距离"和"视觉距离"严重不一致。
举个直观例子:亮红色是(230, 40, 40),暗红色是(120, 20, 20),这两个像素在RGB空间里的欧氏距离超过110,但人眼一看都是"红色"。反过来,(200, 100, 50)和(200, 90, 45)数值很近,视觉上一个是橙色一个是棕色,差异却很明显。RGB的数值变化里,三个通道同时缩放代表亮度变化,而人眼更关心的是色相变化。你拿RGB做颜色分割,等于把亮度和颜色揉在一起判断,光照稍微一变,结果就跟着崩。
2.2 HSV:把"是什么色"和"有多亮"拆开来看
HSV色彩空间把颜色描述拆成了三个独立维度:
- H(Hue):色相,表示这是什么颜色,在OpenCV中范围是0到179
- S(Saturation):饱和度,表示颜色的鲜艳程度,0到255
- V(Value):明度,表示颜色有多亮,0到255
特别提醒,OpenCV里H的范围是0到179,不是常见的0到360,因为OpenCV用uint8存H分量,360放不下,就压缩了一半。很多新手用别的教程里的HSV范围直接套,结果什么都分割不出来。
HSV最大的价值在于:我们做颜色分割时,主要盯住H通道,S和V用来做辅助约束。比如红色工件的H值大约在0到10或170到179附近,S通常在50以上,V则由光照决定。设定阈值时把H范围放宽一点、把S下限抬高一点,就能容忍一部分光照变化。
下面是一段基于OpenCV的HSV转换代码,这个操作几乎是所有颜色分割的起点:
import cv2 import numpy as np img = cv2.imread("sample.jpg") hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 之后所有阈值操作都在hsv图像上进行2.3 Lab空间:一个更接近人眼感知的备选
如果说HSV是工程上最顺手的颜色空间,那Lab就是理论上更科学的颜色空间。
Lab空间有三个通道:L表示亮度,a通道从绿色渐变到红色,b通道从蓝色渐变到黄色。它设计的目标就是让"色差"和"欧氏距离"尽量一致,也就是说,在Lab空间里两个颜色的欧氏距离越大,人眼感知的差异也越大。这个性质在做肤色分割、食品颜色分级时特别有价值。
不过Lab在OpenCV里是float类型,处理起来比HSV麻烦一些,而且a、b通道的数值范围不像HSV那么直觉。我的建议是:常规分割用HSV,色差评价需求优先的项目(比如果实成熟度分档)再用Lab。作为一个备选方案,它值得你替换试一下,但没必要当作默认选项。
3. 阈值设定全流程:从inRange到自动标定
3.1 善用鼠标取色,先把目标颜色的分布摸清楚
我见过太多人一上来就拍脑袋写cv2.inRange(hsv, (0, 100, 100), (10, 255, 255)),然后对着分割结果瞎试参数。正确做法是先采集真实数据,看目标像素的HSV分布到底长什么样。
给你一个实用的小工具:用鼠标在图像上点击,打印当前像素的BGR和HSV值。多采集几十个点,覆盖同一个目标的不同位置、不同角度、不同光照,你就能大概知道该把阈值范围设在哪里。
import cv2 img = cv2.imread("sample.jpg") hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) def on_mouse(event, x, y, flags, param): if event == cv2.EVENT_LBUTTONDOWN: print("BGR:", img[y, x], "HSV:", hsv[y, x]) cv2.imshow("img", img) cv2.setMouseCallback("img", on_mouse) cv2.waitKey(0) cv2.destroyAllWindows()这套操作下来,你会发现同一个红色工件在不同位置的H值可能分布在0到10和170到179两端,S值波动也可能很大。这时候你才算真正掌握了当前画面的颜色分布,而不是靠蒙。
3.2 inRange与"红色要分两段"的经典坑
cv2.inRange()是颜色分割的核心函数,它输入一张图像和阈值上下界,输出一张二值掩膜:落在阈值范围内的像素设为255,其余设为0。
lower_red1 = np.array([0, 50, 50]) upper_red1 = np.array([10, 255, 255]) lower_red2 = np.array([170, 50, 50]) upper_red2 = np.array([180, 255, 255]) mask1 = cv2.inRange(hsv, lower_red1, upper_red1) mask2 = cv2.inRange(hsv, lower_red2, upper_red2) mask = cv2.bitwise_or(mask1, mask2)注意红色在HSV色相环里的一个特殊问题:H值在0到179之间首尾相接,0和179实际上是相邻的颜色,都是红。所以红色天然被切成了两段,必须分别设置区间,再用按位或合并。这个坑网上一搜一大片,当初我也在这里卡了好久。
另外,S和V的下限不建议设成0。S下限用于排除灰白区域和反光高光,V下限用于排除过暗的阴影区域。如果你把S只能设到0,那一切灰色、白色像素都会被误判成目标色。
3.3 Otsu和自适应阈值:什么时候才轮得到它们
如果是目标颜色明确且稳定,inRange永远是最直接的选择。但有些场景你根本说不清"目标颜色到底是多少",比如板材表面的划痕检测、纸箱上的印刷缺陷分割,这类问题只有"前景和背景亮度不同"这一条线索。
这时候Otsu算法就派上用场了。它自动搜索一个灰度阈值,使得前景和背景两类像素的类间方差最大,相当于帮你自动找分割点。
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) _, mask = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)一句话说明原理:Otsu把一幅灰度图的直方图当成两类像素的混合分布,遍历所有可能的阈值,选择让两类像素"间距最大、内部最紧凑"的那个值。它的前提是直方图呈现明显的双峰,如果光照不均导致灰度峰被拉平,Otsu效果会明显退化。此时可以考虑自适应阈值(adaptiveThreshold),它会按局部邻域计算阈值,对缓慢变化的光照有一定的抵抗能力。
4. 真实画面里的干扰:光照、阴影、相似色与反光
4.1 光照变化和相机自动白平衡是两大隐性杀手
在实验室里调好的阈值,一到现场就失效,这是颜色分割项目最常见的翻车方式。最大元凶有两个:光源变化和相机自动白平衡。
即使采用HSV空间,极端光照下H值也会漂移——当物体表面亮度太低时,色彩信息丢失,所有颜色都往灰黑色靠拢,这时候H通道变成噪声,单纯放宽阈值毫无意义。应对方法:工业场景优先加装稳定的光源和遮光罩,把环境光对画面的影响降到最低;相机要关闭自动白平衡,固定曝光时间。很多颜色分割项目不稳定,不是算法不行,而是相机参数在自动漂移,每帧画面的颜色基准都不一样,你再怎么调阈值都白搭。
这些都属于"拍摄端约束",是我在实际项目里学到的最重要一课:颜色分割的上限,有一半在硬件选型和相机配置上就决定了。
4.2 开闭运算:分割掩膜的必修课
即便阈值和拍摄端都做好了,初版的掩膜通常还是会有噪点和孔洞。目标表面可能会有反光点,被阈值排除后变成掩膜上的黑点;背景里偶尔出现一些孤立噪点,只占很少像素。这时需要形态学操作来处理。
开运算是先腐蚀后膨胀,能去掉小噪点、断开细小的误检连接;闭运算是先膨胀后腐蚀,能填充目标内部的细小孔洞、连接相邻区域。两者配合使用,掩膜质量会有质的提升。
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)内核大小选择有个经验值:噪声颗粒大就加大内核,目标细节多就缩小内核,从3x3开始试,效果不明显再逐步往上加。不要一上来就用15x15,否则目标边缘会被磨得不成形。
4.3 相似色与高光反光:颜色特征也有扛不住的时候
颜色分割最怕两类情况:目标颜色跟背景太接近,或者目标表面出现高光。
对于相似色问题,可以在HSV之外补充其他判断维度。例如把Lab空间的a、b通道加入约束条件,或者降低H阈值区间同时增加S、V条件,也可以用形状特性做二次筛选——面积、长宽比、圆形度等几何特征能帮你排除颜色相近但形状不符的区域。在口腔疾病图像分割这类任务里,牙龈和病变区域颜色差异很细微,纯颜色分割很难直接作为最终方案,它更适合做ROI的快速定位,缩小后续精分割的处理范围。
对于高光反光,主要原因是高光区域饱和度S极低,表现为类似白色的像素,颜色信息已经损失殆尽。工业相机加持偏光镜、调整光源入射角度是更根本的办法;算法层面,可以同时检测高光区域,在掩膜中对这些区域做插值或标记待处理,而不是硬靠颜色判断。
5. 从"像素分类"到"目标分割":后处理与算法组合
5.1 连通域分析:把掩膜变成可统计的目标列表
掩膜只是第一步,项目中通常还需要知道"有几个目标、每个目标在哪、有多大"。连通域分析就是干这个的。
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(mask, connectivity=8) for i in range(1, num_labels): area = stats[i, cv2.CC_STAT_AREA] if area < 500: continue x = stats[i, cv2.CC_STAT_LEFT] y = stats[i, cv2.CC_STAT_TOP] w = stats[i, cv2.CC_STAT_WIDTH] h = stats[i, cv2.CC_STAT_HEIGHT] cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)这段代码遍历所有连通区域,跳过面积小于500的噪声块,然后画框。实际项目里,面积过滤条件通常根据目标在画面中的实际像素尺寸来定,可以在调试时先跑一遍,看真实目标的面积分布再设定阈值。除了面积,连通域的宽高比、外接矩形占比也都是很有用的过滤特征,例如识别长条形的广告牌区域时,宽高比就是关键特征。
5.2 用颜色做种子,用GrabCut细化边缘
纯颜色分割得到的边缘通常比较粗糙,尤其在目标颜色和背景颜色交界处,阈值判断容易把交界像素分错。想要更精细的目标轮廓,可以和GrabCut组合使用。
GrabCut是经典的交互式分割算法,它把图像建模成图结构,用高斯混合模型估计前景和背景的颜色分布,通过最小化能量函数来求解每个像素属于前景还是背景。它需要初始化信息,而颜色分割恰好可以充当这个初始化——用颜色掩膜告诉GrabCut哪些区域几乎肯定是前景,再由它对边缘精细调整。
# grabcut_mask:0-3的标志位 gc_mask = np.zeros(img.shape[:2], np.uint8) gc_mask[mask == 255] = cv2.GC_FGD gc_mask[mask == 0] = cv2.GC_PR_BGD bgd_model = np.zeros((1, 65), np.float64) fgd_model = np.zeros((1, 65), np.float64) cv2.grabCut(img, gc_mask, None, bgd_model, fgd_model, 5, cv2.GC_INIT_WITH_MASK) final = np.where((gc_mask == cv2.GC_FGD) | (gc_mask == cv2.GC_PR_FGD), 255, 0).astype("uint8")这种"先颜色定位,再图割精修"的组合拳,在广告牌分割、产品抠图这类场景里表现相当稳健。颜色分割负责找到大致位置,GrabCut负责把边界修出干净的轮廓,比单独使用任何一种都可靠。
5.3 深度学习时代,颜色分割怎么继续发光
现在聊图像分割,大家脑子里蹦出来的是UNet、SegNet、DeepLab这些深度学习模型。在口腔疾病图像分割这类复杂语义任务上,深度学习确实把传统方法甩开很远,这个没有争议。
但颜色分割并没有过时,反而在很多项目里换了个位置继续发挥作用。一是做预标注工具,深度学习模型需要大量标注数据,用颜色分割快速生成初版掩膜,再人工修正,能省下大量标注时间。二是做ROI定位,用颜色先在整幅图里把可能的区域找出来,缩小深度学习模型的输入范围,减少计算量。三是在工业规则场景中,一个HSV+形态学+连通域的传统方案能稳定跑几十帧,完全没必要为了"潮流"去上GPU和模型训练。
另外还有一个思路值得尝试:把HSV、Lab这样的颜色特征作为额外通道,叠加到深度学习模型的输入里。很多分割场景下,颜色信息是深度模型最容易利用的线索之一,这么做往往能小幅提升精度,尤其当训练数据有限的时候。
回到开头那个分拣需求。我最后没上深度学习,就用HSV色彩空间加inRange阈值、形态学清理、连通域统计,在CPU上跑到了30帧以上,误报率低到了可接受范围。那次经历给我的最大启发是:接到一个分割任务,先别急着堆模型,花十分钟观察目标颜色是否稳定、背景能否约束。如果颜色特征足够强,传统方法依然是最短路径,而颜色分割作为基本功,无论后面接的是GrabCut还是深度学习,都值得你把它吃到透。
本文还有配套的精品资源,点击获取