
1. 从一张图片开始聊颜色识别很多人第一次接触计算机视觉都是从“让程序认出图片里那个红色的球”这种需求开始的。听起来简单但真动手做的时候你会发现光照一变、背景一杂程序就开始胡言乱语。我当初也是这么过来的踩了不少坑后来才慢慢摸清楚基于颜色的目标识别核心不在于代码有多复杂而在于你对颜色空间的理解和对场景的预处理是否到位。这篇文章要聊的就是怎么用 OpenCV 做最基础的颜色目标识别——输入是一张静态图片输出是把你想要的颜色区域框出来或者单独提取出来。适合谁看刚学 Python 和 OpenCV 的入门者、需要快速验证颜色方案的产品原型开发者、以及想给非技术同事演示“机器怎么看见颜色”的工程师。不需要深度学习不需要GPU一台普通笔记本就能跑。我见过太多教程一上来就甩一段cv2.inRange的代码然后告诉你“调一下阈值就行”。但阈值到底怎么调为什么用 HSV 不用 RGB掩膜做完之后还能干什么这些才是真正决定你能不能把 demo 变成可用工具的关键。下面我就按自己实际做项目的思路把整个流程拆开讲一遍。2. 整体设计思路与颜色空间选型2.1 为什么不用 RGB 而用 HSV刚入门的人最容易犯的错就是直接拿 RGB 三个通道去卡阈值。比如你想找红色可能会写R 200G 50B 50。在实验室灯光下跑通了换一张图就废了。原因很简单RGB 三个通道是强耦合的光照强度一变三个值同时变你根本没法用一个固定范围去覆盖所有情况。HSV 就不一样了。H色相表示“是什么颜色”S饱和度表示“颜色有多纯”V明度表示“有多亮”。把颜色和亮度拆开之后你只需要卡住 H 的范围S 和 V 给一个宽松的区间就能适应不同光照。举个例子红色的 H 大概在 0 到 10 和 170 到 180 两个区间OpenCV 里 H 的范围是 0-179S 和 V 只要不太低就行。这样即使图片暗一点或亮一点红色还是能被抓住。注意OpenCV 读进来的图片默认是 BGR 顺序不是 RGB。转 HSV 之前不用手动换通道cv2.cvtColor(img, cv2.COLOR_BGR2HSV)会自动处理。2.2 整体流程拆解整个识别流程可以分成五步我习惯在写代码之前先在纸上画一遍读图与预处理把图片读进来做一点模糊去噪避免噪点被当成颜色目标。颜色空间转换BGR 转 HSV为后面的阈值分割做准备。生成掩膜用cv2.inRange把目标颜色范围变成二值掩膜白色是目标黑色是背景。形态学处理对掩膜做开运算或闭运算去掉小噪点、填补空洞。结果提取与展示用掩膜和原图做按位与得到只保留目标颜色的图像或者找轮廓画框。这五步里第三步的阈值范围是核心第四步的形态学操作是提升效果的关键。很多人做完第三步就结束了结果掩膜上全是小白点框出来的东西乱七八糟。后面我会详细讲怎么调。2.3 方案选型的取舍有人会问为什么不用cv2.cvtColor直接转灰度然后卡阈值因为灰度丢失了颜色信息你没法区分红色和蓝色。也有人问为什么不用深度学习对于“只认颜色”这种简单任务深度学习是杀鸡用牛刀而且需要标注数据、训练时间完全没必要。OpenCV 的颜色阈值法在受控环境下足够快、足够准代码量不到 30 行。当然它也有局限如果背景里有和目标颜色相近的物体或者光照极端变化效果会下降。这时候你可以加一些后处理比如限制目标区域的面积范围、长宽比或者结合边缘检测。但作为入门级方案先把基础流程跑通再逐步加约束才是正确的学习路径。3. 核心细节解析与实操要点3.1 HSV 阈值到底怎么定这是最多人卡住的地方。我刚开始的时候对着一个红色物体用取色器读了一个 HSV 值然后上下各加减 10结果发现要么漏掉一部分要么把背景也框进来了。后来我总结了一个比较稳的方法第一步采样。用画图工具或者写个小脚本在目标区域上多点几个像素记录 H、S、V 的值。不要只取一个点因为物体表面颜色不均匀。第二步统计范围。把采样到的 H 值排序取最小和最大然后左右各留 5 到 10 的余量。S 和 V 可以放宽一些比如 S 从 50 到 255V 从 50 到 255。第三步验证与微调。用cv2.inRange生成掩膜显示出来看。如果目标区域有空洞说明 S 或 V 的下限太高如果背景有白点说明 H 的范围太宽或者 S 的下限太低。这里有个经验红色在 HSV 里是跨界的。H 从 0 到 10 是红色170 到 179 也是红色。所以你需要做两次inRange然后把两个掩膜相加。很多人不知道这一点只卡了 0 到 10结果发现暗红色识别不到。import cv2 import numpy as np img cv2.imread(test.jpg) hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 红色两个区间 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.add(mask1, mask2)3.2 形态学操作开运算与闭运算掩膜生成之后你会发现上面有很多小噪点或者目标区域内部有空洞。这时候就需要形态学操作。开运算是先腐蚀后膨胀用来去掉小噪点闭运算是先膨胀后腐蚀用来填补空洞。我一般会先做一次开运算再做一次闭运算。核的大小很关键。太小了去噪不干净太大了会把目标也吃掉。对于一张 640x480 的图片我通常用 5x5 的核。如果图片更大比如 1920x1080可以用 7x7 或 9x9。你可以这样写kernel np.ones((5, 5), np.uint8) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)提示如果目标本身很小比如只有几十个像素那核要更小3x3 就够了。否则目标会被腐蚀掉。3.3 掩膜之后能做什么掩膜是一张二值图白色区域就是你要的目标。基于它你可以做很多事情提取颜色区域result cv2.bitwise_and(img, img, maskmask)这样得到一张只保留目标颜色的图背景全黑。找轮廓并画框contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)然后对每个轮廓算外接矩形画在原始图上。计算目标面积和位置用cv2.contourArea和cv2.boundingRect可以进一步做筛选比如只保留面积大于 500 像素的轮廓。我一般会先做提取再做轮廓筛选。因为提取出来的图更直观方便你判断阈值调得对不对。轮廓筛选则是为了去掉那些面积很小的噪点区域。3.4 常见坑点与注意事项图片路径不要有中文OpenCV 的imread在某些版本下对中文路径支持不好会返回 None。建议用英文路径或者用cv2.imdecode配合np.fromfile。HSV 的 H 范围是 0-179不是 0-360。如果你从其他工具拿到 0-360 的值记得除以 2。S 和 V 的下限不要设太高否则暗部或浅色区域会被漏掉。一般从 50 开始试。形态学核不要用矩形虽然np.ones是矩形但有时候用椭圆核cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5,5))效果更自然尤其是圆形目标。显示图片时注意窗口大小如果图片太大cv2.imshow会超出屏幕。可以用cv2.namedWindow设置WINDOW_NORMAL然后cv2.resizeWindow。4. 完整实操流程与代码实现4.1 环境准备与依赖安装我假设你已经装了 Python。如果没装去官网下个 3.8 以上的版本。然后打开终端装两个库pip install opencv-python numpyopencv-python是主库numpy用来做数组运算。如果你需要显示图片还需要matplotlib但 OpenCV 自带的imshow就够了不用额外装。注意有些教程会让你装opencv-contrib-python那个包含更多扩展模块。对于颜色识别普通版足够。4.2 分步代码与参数说明下面是我实际项目里常用的一个完整脚本我把它拆成几块来讲。第一块读图与预处理import cv2 import numpy as np # 读图 img cv2.imread(color_target.jpg) if img is None: print(图片读取失败检查路径) exit() # 高斯模糊去噪 blurred cv2.GaussianBlur(img, (5, 5), 0)高斯模糊的核大小用 5x5标准差设 0 让 OpenCV 自动算。这一步不是必须的但如果图片噪点多加上会好很多。第二块转 HSV 并生成掩膜hsv cv2.cvtColor(blurred, cv2.COLOR_BGR2HSV) # 以红色为例 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.add(mask1, mask2)如果你要识别绿色H 大概在 40 到 80 之间蓝色在 100 到 140 之间。具体值可以用取色器采样后微调。第三块形态学处理kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)这里我用了椭圆核比矩形核更柔和。开运算去噪闭运算填洞。第四块提取结果与画框# 提取颜色区域 result cv2.bitwise_and(img, img, maskmask) # 找轮廓 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 在原图上画框 output img.copy() for cnt in contours: area cv2.contourArea(cnt) if area 500: # 过滤小噪点 x, y, w, h cv2.boundingRect(cnt) cv2.rectangle(output, (x, y), (xw, yh), (0, 255, 0), 2) cv2.putText(output, fArea: {int(area)}, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)面积阈值 500 是我根据 640x480 图片估的。如果你的图片更大这个值要相应调大。画框用绿色线宽 2文字显示面积方便调试。第五块显示与保存cv2.imshow(Original, img) cv2.imshow(Mask, mask) cv2.imshow(Result, result) cv2.imshow(Output, output) cv2.waitKey(0) cv2.destroyAllWindows() cv2.imwrite(mask.jpg, mask) cv2.imwrite(result.jpg, result) cv2.imwrite(output.jpg, output)waitKey(0)会一直等按键按任意键关闭窗口。保存图片方便你对比不同参数的效果。4.3 参数调试的实操记录我第一次跑这个脚本时用的是网上找的一张红色苹果图片。阈值设的 H 从 0 到 10S 从 100 到 255V 从 100 到 255。结果掩膜上只有苹果最亮的部分是白的暗部全黑。后来我把 S 和 V 的下限降到 50暗部就出来了。但背景里有个棕色的桌子H 大概在 15 左右也被框进来了。我把 H 的上限从 10 降到 8桌子就没了。第二次跑换了一张红色衣服的人像。衣服上有褶皱阴影部分 H 偏移到了 170 左右。我加了第二个区间 170 到 180衣服就完整了。但头发是黑色的S 很低没被框进来这正好符合预期。第三次跑图片里有多个红色物体大小不一。面积阈值设 500 把一个小红点过滤掉了但那个小红点其实是目标的一部分。我把阈值降到 100结果噪点也进来了。最后我用了轮廓的宽高比来筛选只保留宽高比在 0.5 到 2 之间的轮廓效果就好多了。这些经验告诉我没有一套参数能通吃所有图片。你必须根据具体场景去调而且最好写一个带滑动条的调试工具边拖边看。4.4 做一个带滑动条的调试工具OpenCV 的createTrackbar可以帮你快速调阈值。下面是一个简化版def nothing(x): pass cv2.namedWindow(Trackbars) cv2.createTrackbar(H_min, Trackbars, 0, 179, nothing) cv2.createTrackbar(H_max, Trackbars, 10, 179, nothing) cv2.createTrackbar(S_min, Trackbars, 50, 255, nothing) cv2.createTrackbar(S_max, Trackbars, 255, 255, nothing) cv2.createTrackbar(V_min, Trackbars, 50, 255, nothing) cv2.createTrackbar(V_max, Trackbars, 255, 255, nothing) while True: h_min cv2.getTrackbarPos(H_min, Trackbars) h_max cv2.getTrackbarPos(H_max, Trackbars) s_min cv2.getTrackbarPos(S_min, Trackbars) s_max cv2.getTrackbarPos(S_max, Trackbars) v_min cv2.getTrackbarPos(V_min, Trackbars) v_max cv2.getTrackbarPos(V_max, Trackbars) lower np.array([h_min, s_min, v_min]) upper np.array([h_max, s_max, v_max]) mask cv2.inRange(hsv, lower, upper) result cv2.bitwise_and(img, img, maskmask) cv2.imshow(Mask, mask) cv2.imshow(Result, result) if cv2.waitKey(1) 0xFF 27: break cv2.destroyAllWindows()按 ESC 退出。这个工具我每次换新场景都会先跑一遍把阈值调好再写进正式脚本。效率比盲猜高十倍。5. 常见问题与排查技巧实录5.1 掩膜全黑或全白怎么办全黑说明你的阈值范围太窄或者颜色空间转错了。先检查cv2.cvtColor的第二个参数是不是cv2.COLOR_BGR2HSV。然后打印一下图片中心点的 HSV 值看看 H 是不是在 0 到 179 之间。如果 H 是 0 到 360说明你用了别的库转的需要除以 2。全白说明阈值范围太宽把背景也包进来了。把 S 和 V 的下限调高或者缩小 H 的范围。如果还是全白检查图片是不是灰度图灰度图转 HSV 后 H 全是 0。5.2 目标区域有空洞怎么补空洞通常是因为目标表面有高光或阴影导致部分像素的 S 或 V 掉出了范围。解决办法有两个一是降低 S 和 V 的下限让更多像素被包含进来二是用闭运算先膨胀再腐蚀把空洞填上。如果空洞很大可以把闭运算的核加大到 7x7 或 9x9。提示闭运算填洞的同时也会让目标边缘往外扩所以核不要太大够用就行。5.3 背景里有相似颜色怎么排除这是最头疼的问题。如果背景颜色和目标颜色在 HSV 空间里很接近单靠阈值很难分开。这时候可以加一些几何约束面积筛选只保留面积在某个范围内的轮廓。位置筛选只保留在图片某个区域的轮廓比如下半部分。形状筛选用cv2.approxPolyDP逼近轮廓看顶点数是否符合预期。长宽比筛选只保留宽高比在某个范围的轮廓。如果这些都不行那就需要考虑用深度学习方法或者换一个颜色空间比如 Lab。但对于入门级项目先把几何约束用上大部分场景都能应付。5.4 常见问题速查表问题现象可能原因解决方法掩膜全黑阈值范围太窄颜色空间转错放宽阈值检查cvtColor参数掩膜全白阈值范围太宽图片是灰度图收紧阈值确认图片是彩色目标有空洞高光或阴影导致 S/V 掉出范围降低 S/V 下限闭运算填洞背景有噪点相似颜色干扰图片噪点多开运算去噪加面积筛选红色识别不全红色跨 H 区间用两个区间相加轮廓太多小噪点被当成目标提高面积阈值形态学开运算图片读取失败路径有中文文件不存在用英文路径检查文件显示窗口太大图片分辨率高用WINDOW_NORMAL和resizeWindow5.5 独家避坑技巧先看 HSV 直方图在调阈值之前用cv2.calcHist算一下 H 通道的直方图看看目标颜色的峰值在哪里。这比盲调快得多。保存中间结果每做一步都imwrite保存下来出问题时可以回溯是哪一步错了。用cv2.split分离通道有时候单独看 H、S、V 三个通道的灰度图能更直观地理解为什么某些像素被漏掉。不要迷信网上的阈值不同相机、不同光照下同一个颜色的 HSV 值可能差很多。一定要自己采样。批量测试如果你要处理多张图片写个循环把每张图的掩膜保存下来统一看效果比一张张调快。6. 从静态图片到实际应用的扩展思路静态图片跑通之后你可以很自然地扩展到视频流。把cv2.imread换成cv2.VideoCapture(0)然后在一个while循环里逐帧处理。逻辑完全一样只是每帧都要重新算掩膜和轮廓。这时候要注意性能高斯模糊和形态学操作比较耗时如果帧率不够可以降低分辨率或者跳过模糊步骤。另一个扩展方向是结合轮廓的几何特征做简单分类。比如你识别红色和绿色两个目标可以分别算它们的质心然后判断哪个在左边、哪个在右边。这在做简单的分拣演示或者互动装置时很有用。如果你想让识别更稳可以引入背景减除。先用几张没有目标的背景图算一个平均背景然后用当前帧减去背景再在差值图上做颜色阈值。这样能有效排除静态背景的干扰。OpenCV 提供了cv2.createBackgroundSubtractorMOG2但那个更适合运动目标静态图片用不上。最后如果你发现颜色阈值法在某个场景下实在调不好不要死磕。换一个思路先用边缘检测把物体轮廓找出来再在轮廓内部统计颜色直方图判断主色调。这样比全局阈值更鲁棒但代码量会大一些。作为入门先把今天这套流程吃透后面再慢慢升级。我个人在实际操作中的体会是颜色识别看起来简单但真正决定效果的往往不是代码而是你对场景的理解和对参数的耐心。同一段代码别人跑出来是垃圾你跑出来是精品差别就在那几十次的微调里。别怕麻烦把滑动条工具用起来把中间结果存下来多试几次你就能找到那个“刚刚好”的阈值。