颜色空间与目标分割:从像素到区域的实战之路
各位同学,今天我们来聊聊目标跟踪里最基础、也最核心的一步——颜色空间与目标分割。
说实话,我刚开始做视觉算法时,觉得颜色空间就是个数学变换,没什么大不了的。直到有一次在工厂做零件分拣项目,光线一变,整个分割就崩了。嗯,从那以后,我再也不敢小看颜色空间的选择了。
1. RGB、HSV、HSL:三个颜色空间的恩怨情仇
先说说最常见的RGB。红绿蓝三通道,显示器、摄像头都用它。但RGB有个致命问题——它对光照变化太敏感了。
举个例子:一个红色苹果,在强光下RGB值可能是(200, 50, 30),在阴影里就变成了(100, 25, 15)。你看,三个通道一起变,你很难用一个固定阈值去分割。
这时候,HSV就派上用场了。
HSV把颜色拆成色调(Hue)、饱和度(Saturation)、明度(Value)。色调是颜色的本质,饱和度是鲜艳程度,明度是亮度。你想想看,不管光照怎么变,一个红苹果的色调始终在0-10度左右。这就好办了。
我个人习惯在目标跟踪项目里优先用HSV。比如跟踪一个红色小球,我只需要设定H通道在[0, 10]和[160, 180](红色在色环两端),S通道大于某个值,V通道大于某个值。光照变化?只要不是全黑,基本不影响。
至于HSL,它和HSV很像,区别在于L通道(亮度)的计算方式。HSL的亮度更符合人眼感知,但实际工程中我用得不多。为什么?因为OpenCV默认的转换函数是BGR2HSV,用HSL还得自己写转换,麻烦。
核心结论:目标跟踪场景下,HSV > HSL > RGB。RGB只适合在受控光照环境下使用。
2. 颜色阈值分割:把像素变成区域
有了颜色空间,下一步就是分割。说白了,就是告诉程序:「把颜色在某个范围内的像素都标记出来」。
在OpenCV里,我们用cv2.inRange()函数。我贴一段我项目里常用的代码:
import cv2
import numpy as np
# 读取图像并转换到HSV
img = cv2.imread('red_ball.jpg')
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# 定义红色的HSV范围
lower_red1 = np.array([0, 50, 50])
upper_red1 = np.array([10, 255, 255])
lower_red2 = np.array([160, 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)
# 显示结果
cv2.imshow('Mask', mask)
cv2.waitKey(0)
这里有个坑,我曾经踩过:红色在HSV色环上是首尾相连的。所以你需要两个范围,一个从0到10,一个从160到180。我第一次做的时候只设了一个范围,结果红色小球只分割出来一半,另一半死活找不到。排查了半天才发现是这个问题。
小技巧:调试阈值时,别靠猜。用cv2.createTrackbar()做个滑动条,实时调整H、S、V的上下限。我每个项目都会写一个阈值调试工具,省时省力。
3. 形态学操作:给分割结果「美容」
分割出来的mask往往有噪点、有空洞、边缘毛刺。这时候就需要形态学操作来「修图」了。
形态学操作的核心就四个字:腐蚀、膨胀。其他操作都是它们的组合。
3.1 腐蚀与膨胀
腐蚀:让白色区域「瘦一圈」。能去掉小的噪点,但也会让目标变小。
膨胀:让白色区域「胖一圈」。能填补小空洞,但也会让目标变大。
我一般这样用:先腐蚀去掉噪点,再膨胀恢复大小。或者反过来,先膨胀填补空洞,再腐蚀恢复轮廓。
kernel = np.ones((5,5), np.uint8)
# 腐蚀
eroded = cv2.erode(mask, kernel, iterations=1)
# 膨胀
dilated = cv2.dilate(mask, kernel, iterations=1)
kernel的大小和迭代次数是关键参数。kernel越大,效果越猛。我建议从3x3开始试,不够再加。
3.2 开运算与闭运算
开运算 = 先腐蚀后膨胀。用来去除小噪点,同时尽量保持目标大小不变。
闭运算 = 先膨胀后腐蚀。用来填补小空洞,同时尽量保持目标大小不变。
OpenCV提供了现成的函数:
# 开运算
opening = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
# 闭运算
closing = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
注意:形态学操作不是万能的。如果噪点比目标还大,或者空洞比目标还多,那说明你的阈值分割本身就有问题。先回去调阈值,别在形态学上死磕。
4. 知识体系总览
下面这张图是我自己画的,把今天的内容串起来了:
5. 实战中的避坑指南
最后,分享几个我这些年踩过的坑:
- 别用RGB做阈值分割——除非你的光照完全可控。我在一个户外无人机跟踪项目里吃过这个亏,RGB阈值调了一整天,第二天阴天全废了。
- HSV的H通道范围是0-179——不是0-255。OpenCV里HSV的H范围是0-179,S和V是0-255。我第一次用的时候设了H=255,结果全黑,查了半天文档才发现。
- 形态学操作的kernel别太大——5x5通常够用,7x7算大的了。我曾经为了去噪用了15x15的kernel,结果目标形状完全变了,跟踪框直接飞了。
- 先调阈值,再调形态学——这个顺序别搞反。阈值调好了,形态学只是锦上添花;阈值没调好,形态学就是雪上加霜。
总结一下:颜色空间选HSV,阈值分割用inRange,形态学操作按需组合。这套组合拳,能解决80%的简单目标跟踪场景。剩下的20%,我们后面章节再聊。