免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

颜色分割实战:从HSV阈值到GrabCut的工程化指南

颜色分割实战:从HSV阈值到GrabCut的工程化指南 简介基于颜色的图像分割资源包面向 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色彩空间把颜色描述拆成了三个独立维度HHue色相表示这是什么颜色在OpenCV中范围是0到179SSaturation饱和度表示颜色的鲜艳程度0到255VValue明度表示颜色有多亮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, connectivity8) 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_mask0-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还是深度学习都值得你把它吃到透。本文还有配套的精品资源点击获取
返回列表