1. 为什么先把cvtColor和基本图形练扎实
接触OpenCV 4.x的人,十有八九第一段代码就是读取图片、转灰度、画个框。这两块单独看确实不难,但往后做目标检测、颜色追踪、图像标注、界面交互,全都要踩在它们上面。我见过不少新人一上来就冲深度学习模型,结果连调个HSV颜色范围都要折腾半天,更别提在画面上做可视化标注了。实际上cvtColor和基本图形绘制就是整个OpenCV工具箱里的“地基”,地基稳了,后面盖楼才不慌。
先说一个常见的误区:很多人以为cvtColor就是把彩色图变黑白,画图形就是圈圈框框,没什么好学的。但真到了项目中你会发现问题一堆——比如摄像头出来的图经过处理之后颜色偏得离谱、在屏幕上看蓝红对调,或者你在画检测框的时候明明坐标算对了,画出来却跑到画面外。这些问题追到底,都要回到颜色空间和绘图API的细节上。
这篇文章就把这两块掰开揉碎讲清楚。我会从OpenCV 4.x的实际使用角度出发,先拆解cvtColor里各种颜色空间的含义和适用场景,再深入图形绘制函数的每个参数和隐藏细节,最后用一个“颜色分割标注”的小案例把两者串起来。整个过程配有可直接运行的Python示例,也会穿插一些我实际调试中踩过的坑。不论你刚下载OpenCV,还是已经写过一阵子,这都值得通读一遍。
2. cvtColor是颜色空间的翻译官
2.1 为什么OpenCV默认是BGR而不是RGB
OpenCV里彩色图像默认的通道排列是BGR,也就是前两个字节是蓝色分量和绿色分量,第三个字节才是红色分量。“顺序反了”这件事,是新手最常见的困惑来源。
你如果直接用cv2.imread()读图,得到的是一个形状为(H, W, 3)的NumPy数组,其中最后一个维度是三个通道。这三个通道既不是RGB也不是LAB,而是BGR。很多人在用matplotlib的imshow()直接显示这张图时发现颜色不对劲——天空变橙色,绿叶变紫色——原因就是matplotlib默认认为数据是RGB顺序,而OpenCV给的是BGR。
解决的办法不多,但都很直接。一种是在显示前做一次通道转换:
import cv2 import matplotlib.pyplot as plt image = cv2.imread("sample.jpg") rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) plt.imshow(rgb_image) plt.axis("off") plt.show()也可以用cv2.imshow()直接显示,不经过matplotlib,这样就完全不用关心BGR和RGB的转换问题,因为OpenCV内部就是以BGR为约定的。
提示:
cvtColor的第一个参数是输入图像,第二个参数是目标图像,第三个参数是转换代码。它本身不会修改原图,而是返回一张新图。很多人一开始以为它会原地转换,结果发现原图没变,是因为理解错了。
2.2 灰度图为什么是很多算法的前提
灰度图只有一个通道,每个像素用一个0到255的数值表示亮度。之所以大量算法要求先转灰度,是因为很多特征算子只需要亮度信息,不需要颜色信息。比如边缘检测、角点检测、轮廓提取,它们的目标是找到形状和结构上的变化,颜色反而不重要。
转换代码是cv2.COLOR_BGR2GRAY:
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)这个转换不是简简单单取三个通道平均值,而是按照人眼对RGB三色的敏感度做了加权。OpenCV用的是如下近似公式:
Y = 0.299 * R + 0.587 * G + 0.114 * B人眼对绿色最敏感、红色次之、蓝色最不敏感,所以绿色通道的权重最高。这个公式在YUV色彩空间里其实也是同一个来源。就是这种加权方式,让转出来的灰度图比直接取平均更符合人的视觉感受。
让我举一个实际的例子。一张有蓝天和绿叶的风景照,如果直接用(B + G + R) / 3取平均,蓝色和绿色对亮度的影响会被放得差不多高,叶子灰蒙蒙的;而用加权公式,绿叶和蓝天的层次感会明显更自然。这就是为什么OpenCV和很多图像处理库里都用这套系数,而不是简单的算术平均。
2.3 HSV才是颜色分割的地图
HSV代表色调(Hue)、饱和度(Saturation)、明度(Value)。这是做颜色识别最常用的颜色空间,因为它在设计上非常贴近人理解颜色的方式。
可以这样类比:你站在涂料店货架前想买“一种鲜艳的纯绿色”,你会先确定“绿”这个色相,再看这瓶涂料“够不够浓”就是饱和度,最后看它“亮不亮”就是明度。BGR三个通道很难直观地表达“这是不是绿色”,但HSV能直接把“色相”单独抽出来。
OpenCV里从BGR转HSV的代码非常简单:
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)真正要注意的是通道范围。OpenCV为了把数据塞进uint8类型,对HSV做了一个特殊压缩:
- H通道范围是0到179,而大多数教材上写的是0到360度。
- S通道范围是0到255。
- V通道范围是0到255。
换句话说,你在网上找来的HSV阈值表,如果人家写的是H(0-360), S(0-100), V(0-100),搬到OpenCV里直接用必然出错,必须按比例换算。换算公式很粗暴:OpenCV的H值等于角度除以2,S和V是把百分比乘以2.55。
还有一个坑,红色在色相环上同时横跨0和180两端,也就是说纯红色的H值既有接近0的区间,又有接近179的区间。做红色检测时,用一个区间是罩不全的。这事放到后面的例子细讲,先记住结论:红实检测尽量用两个HSV区间合并处理。
2.4 YUV、Lab、其他常用转换
HSV之外,还有几个颜色空间会在特定场景出现,虽然不常用,但懂比不懂好。
YUV主要用于视频压缩和电视广播标准。Y代表亮度,U和V代表色度。人眼对亮度变化远比对颜色变化敏感,因此在视频编码里可以对UV通道多做压缩,画面观感不会明显下降。如果你接触过视频流、硬件编码之类的项目,cv2.COLOR_BGR2YUV大概率会遇到。
Lab颜色空间的设计目标是接近人的视觉感知,它的L通道代表亮度,a通道大致对应绿到红的范围,b通道对应蓝到黄的范围。Lab的好处是颜色数值之间的欧氏距离和人眼感受到的色差比较一致,所以一些图像分割和质量评价任务会优先用它。用cv2.COLOR_BGR2Lab即可转换。
至于什么时候选哪个,我的经验判断标准就一句话:如果需要“挑出某种颜色”,优先HSV;如果要做视频硬件相关的处理,可能碰到YUV;如果涉及感知一致性的算法实验,再考虑Lab。其他绝大部分灰度化、显示转换,BGR转RGB和转GRAY已经覆盖了大部分需求。
2.5 多通道图像处理中容易被忽略的细节
cvtColor的完整函数签名里还有一个可选参数dstCn,它用来指定目标图像的通道数。大多数情况下你不传也完全没问题,函数会自动根据转换代码决定输出通道数。比如COLOR_BGR2GRAY自动输出单通道,COLOR_BGR2HSV自动输出三通道。
但有一种情况值得提一下:你手里有一张带透明通道的PNG图,读进来是四通道的BGRA,直接调用cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)会报错,因为输入图像是四通道,函数不知道你要不要保留透明度。合理做法是先分通道或者用cv2.COLOR_BGRA2GRAY这样的专用代码。类似地,如果有图像数据是线性的或者HDR,需要先用适当方式变换再转,否则转出来的颜色会很怪。
还有一点,在高动态范围区间上做颜色转换,16位图或32位浮点图需要指定合理的转换方式,否则精度丢失会非常明显。比如从16位灰度图转8位,不预先做缩放直接转,可能得到一片黑或者一片白。这个问题的根源不是转换函数,而是数据范围变了,映射却还在原来的0到255范围上。
3. 基本图形绘制:不只是画线画框
3.1 绘制函数都有一个共同的开局:就地修改
OpenCV的图形绘制函数,比如cv2.line、cv2.rectangle、cv2.circle、cv2.putText,都是直接在原图上修改的,不会返回一张新图。
# 注意:image 会被直接修改,不需要再接收返回值 cv2.rectangle(image, (50, 50), (200, 200), (0, 0, 255), 2)很多初学者会写成image = cv2.rectangle(image, ...),其实两个写法效果一样,因为矩形已经画进了原图,赋值只是把同一个对象给了左边变量。真正要小心的是:如果你在检测流程里把原图当“背景画布”,后面还要读原始像素做其他判断,那一定先复制一份。
annotated = original.copy() cv2.rectangle(annotated, (50, 50), (200, 200), (0, 0, 255), 2)我习惯的做法是一开始就把“待绘制的图”和“原始数据图”明确分开。尤其做目标追踪时,每一帧都要在原图上画检测框,但后续算法可能还要用没有画框的干净图,这时候不复制就会把结果污染掉。
3.2 线的粗细、线型与抗锯齿
以画直线为例,函数签名是:
cv2.line(image, point1, point2, color, thickness=1, lineType=cv2.LINE_8)point1和point2是两个像素坐标元组,(x, y)形式。坐标原点在图像左上角,x轴向右,y轴向下。这一点和数学课上习惯的坐标方向不一样,初学容易偏掉,尤其是你从直角坐标系迁移过来时,经常把图形画得上下颠倒。
thickness表示线条粗细,单位是像素。如果这个值是-1,表示填充整个图形内部,而不是只画边界。这一条在做实心圆、实心矩形时非常常用。
lineType是很多人忽略的参数。cv2.LINE_8是常规8连通绘制,速度快,适合大多数场景。cv2.LINE_AA是抗锯齿模式,画出来的线条边缘更平滑,视觉效果更好,但速度稍慢。如果只是画在视频实时画面上,LINE_8就够了;如果是导出报表图、做静态结果的展示图,LINE_AA看起来会舒服很多。
举个实际例子。画一个标记点位置的小十字,用不同线型对比是很直观的:
cv2.line(image, (100, 80), (100, 120), (0, 255, 0), 2, cv2.LINE_AA) cv2.line(image, (80, 100), (120, 100), (0, 255, 0), 2, cv2.LINE_AA)3.3 圆形、椭圆、多边形和填充
圆形:
cv2.circle(image, center, radius, color, thickness=1, lineType=cv2.LINE_8)center是圆心坐标,radius是半径。画实心圆就是thickness=-1。检测到目标之后,很多场景用圆心标记比用矩形框更直观,比如球体、瞳孔、粒子追踪。
椭圆稍微复杂一点:
cv2.ellipse(image, center, axes, angle, startAngle, endAngle, color, thickness)axes是一个元组(长轴的一半长度, 短轴的一半长度);angle是整个椭圆相对于水平方向旋转的角度;startAngle和endAngle是画弧时的起止角度。0度指向12点钟方向?这里要明确一下,OpenCV的椭圆角度是从水平方向逆时针算的。0度从右侧水平位置开始,90度在下方——因为y轴向下。这和数学坐标里逆时针为正也不完全一致,多画两次就记住了。
如果你用startAngle=0, endAngle=360,画出来就是完整圆。用0, 180画的是上半圆还是下半圆,和角度方向有关,建议自己跑一跑验证,画图相关的坑用肉眼验证最快。
多边形:
cv2.polylines(image, [points], isClosed, color, thickness) cv2.fillPoly(image, [points], color)points是一个形状为(N, 1, 2)的NumPy数组,表示多个顶点坐标。isClosed=True表示首尾相连,形成封闭多边形。cv2.fillPoly直接填充内部,适合做区域涂色。
需要特别注意的是,cv2.polylines的顶点数组必须写成[points]这种带中括号的形式,因为函数接受的是多边形列表,即使你只画一个多边形也要套一层列表。漏了这一层,代码会报类型错误,或者画出来完全不对。
3.4 文本标注:putText的基本用法和中文难题
cv2.putText用来在图像上写字:
cv2.putText(image, text, org, fontFace, fontScale, color, thickness=1, lineType=cv2.LINE_8)org是文字左下角的坐标;fontFace是字体类型,常用的是cv2.FONT_HERSHEY_SIMPLEX和cv2.FONT_HERSHEY_COMPLEX;fontScale控制字号大小,注意它不是像素尺寸,而是字体缩放系数。同一个fontScale在不同分辨率图像上的实际显示大小可能不一样,这点在录视频做标注时容易翻车。想精确控制文字宽度,可以用cv2.getTextSize()先测量:
(text_width, text_height), baseline = cv2.getTextSize( "detected", cv2.FONT_HERSHEY_SIMPLEX, 0.8, 2 )然后把文字放在框上方居中,用这个小技巧可以把标注做得更整齐。
中文是老大难。OpenCV内置字体不支持中文,直接cv2.putText画“检测到目标”四个字,出来的全是问号。解决方案常见有两个方向。一是用PIL(Pillow)的ImageDraw来画中文,再将PIL图像转换回OpenCV的BGR格式;二是用freetype这种封装库直接加载支持中文的字体文件。我在实际项目里通常选用PIL方案,因为Pillow普及率高,代码也好写。
3.5 画布坐标、越界与绘制顺序的经验
绘制顺序很关键。轮廓线、填充色、文字标注这三者的顺序决定了最终视觉效果。通常先画填充区域,再画边界线,最后写字,否则文字会被后面画的粗线条盖住。
坐标越界也有讲究。OpenCV画矩形、画圆时,即使图形部分超出图像边界,一般不会报错,只绘制画布内的可见部分。这在动态目标检测中很友好,因为目标走到画面边缘时,框总是“切”着画面边沿,不会被异常中断。但如果你后续要拿这些坐标做数组索引,就必须自己裁剪,不能指望OpenCV帮你处理。
另外,绘制时的BGR颜色顺序务必要记牢。比如你想画一个红色框,应该写(0, 0, 255)而不是(255, 0, 0)。很多人在实际项目里调试半天颜色不对,最后发现就是通道顺序问题。
4. 实操案例:把两个知识点串成完整的颜色追踪标注
4.1 项目目标与整体流程
下面来看一个能直接跑起来的例子。我们从摄像头或静态图片里找出“蓝色物体”,圈出它,并标注位置信息。这个例子既有cvtColor的使用,又有inRange阈值分割、轮廓查找和图形绘制的完整步骤。
整体流程分四步:
- 读入图像,把BGR转成HSV。
- 用
cv2.inRange根据H/S/V范围生成掩码。 - 对掩码做简单的形态学清理,找轮廓。
- 在原图上绘制检测框、中心点和文本信息。
先看静态图版本,逻辑最清晰:
import cv2 import numpy as np img = cv2.imread("scene.jpg") hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) lower_blue = np.array([100, 80, 80]) upper_blue = np.array([130, 255, 255]) mask = cv2.inRange(hsv, lower_blue, upper_blue) contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: area = cv2.contourArea(cnt) if area < 500: continue x, y, w, h = cv2.boundingRect(cnt) cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 2) cv2.circle(img, (x + w // 2, y + h // 2), 4, (0, 255, 0), -1) cv2.putText(img, "blue target", (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2) cv2.imshow("result", img) cv2.waitKey(0) cv2.destroyAllWindows()这里用cv2.RETR_EXTERNAL只取最外层轮廓,适合找物体外边界;cv2.CHAIN_APPROX_SIMPLE压缩轮廓点,省内存。
4.2 HSV阈值为什么比较难调
HSV阈值是这类任务里最费时间的一环。很多设备之间的颜色差异很大,同一个蓝色在室内灯光、户外阳光下差别明显。数值写得太窄,反光区域被漏掉;写得太宽,周围很多相近颜色都会被框进来。
我之前在某项目里识别一块蓝色的仪表盘,白天正常,傍晚灯光一黄,HSV数值怎么调都过检。后来发现问题是V通道受亮度影响太大,要适当抬高S和V的低阈值,同时把H的区间稍微拓宽。这个调整没有黄金公式,我给你一个调试工具:用cv2.createTrackbar把H/S/V上下限做成滑动条,实时观察掩码效果,比一遍遍改代码重启快得多。
cv2.namedWindow("mask") cv2.createTrackbar("H_min", "mask", 100, 179, nothing) cv2.createTrackbar("H_max", "mask", 130, 179, nothing) cv2.createTrackbar("S_min", "mask", 80, 255, nothing) cv2.createTrackbar("V_min", "mask", 80, 255, nothing)现场调整时,把掩码窗口和原图窗口并排放开,你很快就能找到稳定范围。记住这个方法之后,你几乎可以应对任何颜色检测场景。
4.3 红色检测的特殊处理
刚才提到红色的H通道横跨0和180两端,这个坑实打实会出现。假设你要检测一个红色杯子,HSV转出来之后,它的H值可能落在0到10之间,同时也可能落在170到179之间。你如果只写一个lower_red = [0, 100, 100]和upper_red = [10, 255, 255],漏掉下半段非常常见。
标准处理方式是生成两个mask再合并:
lower_red1 = np.array([0, 80, 80]) upper_red1 = np.array([10, 255, 255]) mask1 = cv2.inRange(hsv, lower_red1, upper_red1) lower_red2 = np.array([170, 80, 80]) upper_red2 = np.array([179, 255, 255]) mask2 = cv2.inRange(hsv, lower_red2, upper_red2) mask = cv2.bitwise_or(mask1, mask2)对于其他颜色,比如绿色、蓝色、黄色,因为色相环上相对集中,一般用一个区间就够了,但红色必须特殊对待。这算是HSV颜色空间里最典型的“反直觉”点。
4.4 掩码可视化:怎么把mask叠加回原图
调阈值的时候,如果你只盯着黑白的mask看,很多时候看不出哪些区域是“漏了的边界”。最直观的方式是把mask叠加在原图上,半透明地显示出来。
做法不复杂,先生成一个与原图同尺寸的彩色掩码图,然后和原图做加权融合:
overlay = np.zeros_like(img) overlay[mask > 0] = (0, 0, 255) # 把mask区域标记为红色 merged = cv2.addWeighted(img, 0.6, overlay, 0.4, 0) cv2.imshow("merge", merged)这个可视化方式比干看mask黑白图像高效得多。你能立刻看到哪些白色噪点是误检,哪些目标边缘没罩全,调参时心里特别有数。
4.5 接上摄像头视频流时的调整
接摄像头时,核心思路不变,只是加一层循环,每帧都做同样处理:
cap = cv2.VideoCapture(0) while True: ok, frame = cap.read() if not ok: break hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask = cv2.inRange(hsv, lower_blue, upper_blue) result = frame.copy() contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: area = cv2.contourArea(cnt) if area < 800: continue x, y, w, h = cv2.boundingRect(cnt) cv2.rectangle(result, (x, y), (x + w, y + h), (0, 0, 255), 2) cv2.imshow("live", result) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()视频场景里尤其要注意两点。第一,每一帧都做全分辨率形态学操作会有性能压力,可以先把图像缩小处理,找到框之后按比例映射回原尺寸。第二,waitKey(1)里的延时决定了帧率,大部分默认值1毫秒够了,不用额外加sleep。
从工程角度,我通常还会加一个“防抖”逻辑:只保留面积最大或者离上一帧最近的目标,避免画面里有多个相近颜色的区域时,检测框在它们之间来回跳。
5. 常见问题与排查技巧
5.1 颜色显示错乱:蓝红对调
这个问题几乎人人都会遇到。症状是图像或绘制图形的颜色和预期相反,绿色画成了紫色,红色画成了青色。
排查顺序三条:
- 检查是不是在显示时走了
matplotlib,如果是,BGR先转RGB。 - 检查自己写颜色值的时候是不是写成了RGB习惯,比如红色应该
(0, 0, 255),写成了(255, 0, 0)就是蓝色。 - 检查有没有经过某些第三方图像库(像
PIL、imageio)中转,这些库通常用RGB顺序,交接时要做通道转换。
5.2 绘制时图片格式不匹配
有些绘制函数对图像类型很敏感。比如在单通道灰度图上画彩色矩形,OpenCV通常会报错或者画出来的颜色不符合预期,因为灰度图一个像素只有一个数值,根本装不下三通道颜色。解决办法是先转成三通道的BGR格式再绘制:
color_img = cv2.cvtColor(gray_img, cv2.COLOR_GRAY2BGR)还有一个更隐晦的坑:图像类型是uint8还是float32。很多绘图函数的颜色值默认按0到255的uint8解析,如果你图像是0到1的浮点数,画出来的颜色会过曝或者看不见。两条路,要么把图转回uint8,要么绘制时把100、255这类值改成0到1范围中的对应小数。
5.3 中文字符变成问号
前面已经说过,OpenCV内置字体不支持中文。你写了中文文本在图像上,大概率是一堆问号“???”。这是字体不支持,不是编码错误。
最省事的解决方案是用Pillow:
from PIL import Image, ImageDraw, ImageFont # 先有了OpenCV的image_bgr image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB) pil_img = Image.fromarray(image_rgb) draw = ImageDraw.Draw(pil_img) font = ImageFont.truetype("simhei.ttf", 28) draw.text((x, y), "检测到目标", font=font, fill=(255, 0, 0)) image_bgr = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)注意字体文件路径,如果你系统里没有simhei.ttf,就用其他支持中文的字体文件,比如msyh.ttc或NotoSansCJK。这个方法要多花一点内存和时间,但在有中文标注的项目里非常可靠。
5.4 轮廓查找不稳定:噪点太多或漏检
findContours在掩码有大量噪点时,检测出来的轮廓会很碎,或者大物体被分割成了好几块。常规做法是先用形态学操作把掩码收拾干净:
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)MORPH_OPEN先腐蚀后膨胀,主要去白色小噪点;MORPH_CLOSE先膨胀后腐蚀,主要补内部空洞。这个组合在颜色检测任务里几乎是标配。
如果目标面积本来就小,形态学开运算核不能开得太大,否则会直接把小目标消掉。一般先从小核比如(3,3)开始试,效果不明显再往上加。
5.5 为什么我的圆变成了椭圆
正常情况下,如果一个像素是正方形(大多数显示器都是),画出来的圆就是圆,不会变成椭圆。但如果你的图像本身做了resize,宽高比例发生变化,画上去的圆自然会被拉伸成椭圆。更常见的情况是,你用了某种相机标定或矫正流程,输出图像的宽高不再对应原始比例。
检查方法很简单:看一下你当前图像的长宽和原始图像长宽是否一致。如果经过了非等比缩放,圆会妥妥地变成椭圆。解决办法是绘制之前等比缩放,或者根据缩放比例调整半径。这个问题在实际项目中经常出现,尤其是在把相机图像和检测层输出叠加时。
5.6 视频里绘制卡顿或掉帧
绘制本身很轻量,除非每帧画了大量复杂多边形和文字。如果你发现视频实时检测时掉帧严重,先排查是否在每一帧里做了大矩阵运算,比如全图cvtColor不可避免,但如果每帧还做全图的形态学大核操作,就会变慢。
优化手段优先级:缩小处理帧 -> 减少形态学核尺寸 -> 减少绘制元素 -> 用LINE_8替代LINE_AA。别一上来就换机器,先把管线里这些冗余去掉,很多掉帧问题是算法浪费造成的。
6. 经验收尾:这两个基础模块值得多花时间
我实际用过一阵子OpenCV之后,最大的感受是:这些“基础API”才是项目里花费时间最多的地方。跑模型、调参那些环节反而一旦流程跑通就很稳定,真正让你反复改代码的,多半是通道顺序、颜色范围、坐标计算、字体显示这些底层细节。
cvtColor和基本图形绘制,看起来都是几分钟能上手的简单调用,但它们背后连接着图像的数据格式、坐标系约定、颜色空间数值范围这些整个OpenCV最核心的设计。把这两块吃透,之后就很容易举一反三,比如看到findContours输出的轮廓点集不会慌,看到cvtColor的转换代码表也不至于一头雾水。
最后分享一个小技巧:把本文里调HSV阈值那套滑杆工具保存成一个脚本模板,下次换一个项目就能直接改参数用,不用重新造轮子。这种小工具积累多了,会在后面省下非常多的调试时间。