首页 / 资讯中心 / 文章详情

OpenCV轮廓检测实战:预处理、findContours与形状识别

OpenCV轮廓检测实战:预处理、findContours与形状识别 ★ FEATURED ARTICLE
1. 轮廓检测的底层认知它和边缘提取根本不是一回事这几年前前后后做过不少图像处理的项目从零件尺寸测量到文档自动矫正再到简单的物体分类几乎每个任务都绕不开轮廓检测。很多刚开始接触OpenCV的朋友容易陷入一个误区把轮廓检测等同于边缘提取觉得Canny边缘检测出来了轮廓也就到手了。这个概念必须要纠正因为这两者在本质上是完全不同的东西搞混了后面所有步骤都会跟着出错。边缘检测比如Canny、Sobel输出的是像素级的结果它找的是图像中亮度剧烈变化的位置出来的是一堆分散的、可能是断断续续的白色像素点。而轮廓检测输出的是对象级的结果它找的是连通区域的边界是一条完整的、闭合的曲线能直接对应到图像里某个具体的物体。你可以把边缘想象成墙上画的一条条线而轮廓是沿着每个物体边界走一圈画出来的封闭路径。边缘不一定是闭合的但轮廓描述的是一个完整的形状。OpenCV里的findContours函数干的事情本质上是把二值图像里的白色连通区域提取出来然后沿着这个区域的边界追踪出一条有序的点序列。实际项目中为什么更偏爱轮廓而不是边缘我个人的体会是轮廓自带几何结构信息。基于轮廓你可以直接计算面积、周长、外接矩形、凸包、质心可以做形状匹配、透视变换、物体计数。边缘只是一堆点要还原成对象还得再做连通性分析平白多绕一圈。如果你要做的是对象级别的分析直接上轮廓检测省时省力还稳定。2. 预处理链路为什么二值化之前的每一步都在决定成败2.1 灰度化和噪声处理的必要性用OpenCV做轮廓检测标准操作是先把图像转成灰度图再做阈值或边缘处理得到二值图最后调用findContours。但很多新手在这里会漏掉一个关键点直接从彩色图转二值图是禁忌。彩色图有三个通道直接做阈值处理时每个通道的响应不一样出来的二值图噪声大得离谱后续提取出来的轮廓也是破碎的、不规则的。我的建议是先转灰度然后用高斯模糊去噪再做二值化。高斯模糊的核大小一般取3x3或5x5就行不要开太大否则会把细小轮廓的边缘洗平导致轮廓失真。实测下来5x5的高斯模糊对抑制椒盐噪声非常有效处理工业相机拍的零件图基本够用。这里有个容易被忽视的细节如果你的图像里前景和背景光照不均匀直接全局阈值会翻车。这种情况下先试试cv2.adaptiveThreshold它把图像划分成小区域每个区域单独计算阈值对光照渐变的图像适应性要好得多。实际做文档拍照矫正时我几乎都用自适应阈值效果比固定阈值稳定一个量级。2.2 阈值选择OTSU还是固定值二值化这一步我建议优先考虑OTSU大津法。它的原理是根据图像的灰度直方图自动计算一个能把前景和背景类间方差最大化的阈值不需要人工干预。用代码写就是cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)第二个参数随便填0就行因为OTSU会自己算。那什么时候用固定阈值如果你的图像是受控环境下拍的比如固定光源下的流水线拍摄前景和背景的灰度值分布非常稳定这时候固定阈值更快也更稳定。我做过一个零件分拣的小项目光源固定、角度固定用固定阈值200二值化几百张图没有一次失手。反过来如果是自然光下拍的户外图片老老实实交给OTSU或者自适应阈值。预处理做完轮廓检测的基础就扎实了。最后提醒一点二值化之后尽量做一次形态学操作。我常用cv2.morphologyEx配合椭圆核做一次闭运算把物体内部的细缝和小孔填上让同一个物体的轮廓尽量保持完整不至于一个零件被拆成三四个轮廓。3. findContours深入解析模式选择与层级关系3.1 两个关键参数的模式对比cv2.findContours在OpenCV中的函数签名是这样的以OpenCV 3之后为准contours, hierarchy cv2.findContours(image, mode, method[, contours[, hierarchy[, offset]]])第一个参数是二值图第二个参数是轮廓检索模式第三个参数是轮廓逼近方法。轮廓检索模式最常用的有这几种cv2.RETR_EXTERNAL只提取最外层轮廓内部嵌套的不管。cv2.RETR_LIST提取所有轮廓但不建立父子关系所有轮廓平级。cv2.RETR_TREE提取所有轮廓并且建立完整的层级关系树能反映轮廓之间的嵌套和包含关系。这三个模式怎么选如果你只是统计图里有多少个物体用RETR_EXTERNAL就够了速度最快。如果你要识别图像里嵌套的结构比如检测到外轮廓后还想知道它的内孔有几个就必须用RETR_TREE。3.2 轮廓逼近方法的差异轮廓逼近方法这个参数同样关键。cv2.CHAIN_APPROX_SIMPLE会压缩掉轮廓上的冗余点只保留能描述轮廓几何形状的关键点——比如一条直线段只保留两个端点所以四边形轮廓压缩完只剩4个点。cv2.CHAIN_APPROX_NONE则会把轮廓边界上所有像素点都保留下来不压缩。对大部分应用我都用SIMPLE。原因很简单点少意味着后续计算量小而且在做多边形逼近和形状匹配时点太多反而容易引入噪声。但如果你的目标是计算轮廓的精确周长或者需要逐像素精细描述那就需要NONE模式了。3.3 hierarchy层级信息的实际用法hierarchy是一个1 x N x 4的数组每个轮廓对应一组四个值[next, previous, first_child, parent]。我用一个实际例子说明怎么读懂它假设图像里有一个大圆大圆里面有一个小方框。运行findContours加RETR_TREE之后大圆的轮廓和小方框的轮廓都会被检测出来。大圆轮廓的first_child指向小方框轮廓的索引小方框轮廓的parent指向大圆轮廓的索引。如果你只想处理最内层或特定层级的轮廓就必须靠这个层级关系做筛选。我做PCB板检测时就用到了这个特性——板子外形是一个大轮廓上面显著焊盘是大轮廓里的第一层子轮廓焊盘里的过孔又是下一层子轮廓。要做的是只保留焊盘位置的轮廓信息那就遍历轮廓判断每个轮廓的parent是否指向大轮廓是就保留否则丢弃。这一个判断条件能过滤掉绝大部分噪声轮廓非常高效。代码示意如下import cv2 image cv2.imread(pcb.png) gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) blurred cv2.GaussianBlur(gray, (5, 5), 0) _, thresh cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU) contours, hierarchy cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) # 假设board_contour是外层板子的轮廓索引 board_contour 0 for i, cont in enumerate(contours): # hierarchy[0][i][3]是第i个轮廓的父轮廓索引 if hierarchy[0][i][3] board_contour: cv2.drawContours(image, contours, i, (0, 255, 0), 2)4. 轮廓特征计算从几何参数到形状描述子4.1 面积、周长与轮廓逼近提取到轮廓之后真正有价值的是对每个轮廓做特征计算。cv2.contourArea计算的是轮廓包围的面积cv2.arcLength计算的是轮廓的周长。这两个参数我一贯把它们当作第一道筛选关卡面积过小的一律视为噪声过滤掉这样后续处理的数据量能减少一大半。cv2.approxPolyDP轮廓逼近在形状识别中则是核心操作。它用一条更少顶点的折线去逼近原始轮廓参数epsilon控制逼近精度一般取轮廓周长的百分比。常用的取法是0.02 * arc_length效果比较均衡。逼近之后根据顶点数就能方便地判断轮廓对应的是三角形、四边形还是更复杂的多边形approx cv2.approxPolyDP(contour, 0.02 * cv2.arcLength(contour, True), True) if len(approx) 3: shape triangle elif len(approx) 4: shape rectangle else: shape circle4.2 外接矩形、最小外接矩形与凸包cv2.boundingRect返回的是与坐标轴对齐的外接矩形四个值分别是左上角x、y坐标和宽高。它计算快适合做粗略定位。cv2.minAreaRect则是寻找能刚好框住轮廓的面积最小的旋转矩形返回的是一个RotatedRect对象包含中心点、宽高和旋转角度。做OCR字符矫正、车牌识别这类场景时最小外接矩形几乎是标配因为物体的方向通常是任意的轴对齐矩形框不住真实形态。cv2.convexHull凸包是另一个很有用的函数。它相当于用一根橡皮筋紧紧包住轮廓所有点得到的外包围形状。凸包和原始轮廓之间的凹陷区域凸性缺陷在很多场景里非常有价值最典型的就是手势识别——手指之间的凹陷就是凸性缺陷。通过检测凸性缺陷的数量和深度可以估算伸出了几根手指。4.3 Hu矩形状描述的定海神针说到形状匹配就绕不开Hu矩。cv2.HuMoments计算的是轮廓的7个不变矩这些矩具有平移、旋转和缩放不变性——同一个物体不管怎么移动、旋转、放大缩小它的Hu矩在理论上保持不变。这一点太实用了意味着你不需要让待识别物体端正地摆在图像里也能识别出来。实际项目里我是这么用的对每个目标轮廓计算Hu矩再计算它与参考轮廓的Hu矩之间的欧几里得距离距离越小说明形状越接近。为了不同形状的矩数值范围统一通常需要对Hu矩取对数-sign(h) * log10(abs(h))否则高阶矩的数值可能小到接近零反而失去区分度。5. 实战从图像输入到形状识别的完整流程5.1 搭建一个形状识别Demo这里用一个实战例子把前面讲的所有环节串起来。目标是从一张包含若干形状的图中自动识别出三角形、矩形、圆形并标出轮廓。代码我直接给出一份可以跑通的完整流程import cv2 import numpy as np # 读取图像并预处理 img cv2.imread(shapes_sample.png) if img is None: print(图片读取失败请检查路径) exit() ratio img.shape[0] / 500 # 缩放比例用于把结果映射回原图尺寸 resized cv2.resize(img, (int(img.shape[1] * ratio), 500)) gray cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY) blurred cv2.GaussianBlur(gray, (5, 5), 0) # OTSU自动阈值注意用INV模式让前景为白色 _, thresh cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU) # 提取轮廓 contours, _ cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: area cv2.contourArea(cnt) if area 500: # 过滤掉小噪声 continue peri cv2.arcLength(cnt, True) approx cv2.approxPolyDP(cnt, 0.02 * peri, True) vertices len(approx) x, y, w, h cv2.boundingRect(approx) if vertices 3: shape_name Triangle color (0, 0, 255) elif vertices 4: # 用宽高比判断是正方形还是矩形 aspect_ratio w / float(h) if 0.95 aspect_ratio 1.05: shape_name Square color (0, 255, 0) else: shape_name Rectangle color (0, 255, 255) elif vertices 7: shape_name Circle color (255, 0, 0) else: shape_name Polygon color (200, 100, 200) cv2.drawContours(resized, [approx], -1, color, 2) cv2.putText(resized, shape_name, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) cv2.imshow(Shapes Recognition, resized) cv2.imwrite(shapes_result.png, resized) cv2.waitKey(0) cv2.destroyAllWindows()5.2 判断逻辑的细节补充识别圆形这里我写的是顶点数大于等于7就判定为圆。其实更严谨的判法是计算轮廓面积与最小外接圆面积的比例_, radius cv2.minEnclosingCircle(cnt) circle_area np.pi * radius * radius ratio area / circle_area # 比值接近1说明轮廓非常接近圆形 if ratio 0.95: shape_name Circle原理是圆的轮廓面积和它的最小外接圆面积之比理论上等于1轮廓越接近圆越趋近1。矩形判断同理也可以用轮廓面积除以最小外接矩形面积数值越接近1说明越接近矩形。这套组合拳比单独看顶点数可靠得多尤其在轮廓受压变形的情况下。5.3 几个我在实操中反复踩过的坑第一个坑是缩放后坐标映射。如果对图像先做了缩放后面画轮廓和框的时候要记得把坐标乘回去或者统一在缩放后的尺寸上操作否则框和轮廓对不上。上面的代码我统一在缩放后的图像上操作就避免了映射问题。第二个坑是**findContours会修改输入图像**。OpenCV 3之后findContours会修改传入的二值图如果你后面还需要原二值图做别的操作记得传thresh.copy()进去。这个坑我当年踩过排查了好久才发现图像被原地改了。第三个坑是阈值方向搞反。有个很常见的问题是物体是白色的背景是黑色的但你用了THRESH_BINARY之后发现所有轮廓都提取不出来。原因是你把白色的物体变成了黑色黑色背景变成了白色这时提取到的轮廓是背景的轮廓。看代码里我用了THRESH_BINARY_INV就是把前景物体变成白色、背景变成黑色findContours只找白色区域这样才能提取到物体本身。6. 高阶玩法轮廓分析在真实项目中的拓展6.1 文档扫描矫正中的四角定位做过文档扫描类App的都知道手机拍出来的文档都是透视变形的要矫正成规整的矩形第一步就是要找到文档的四个角点。这个任务本质上就是轮廓分析# 拿到轮廓后找到面积最大的轮廓它就是文档区域 doc_contour max(contours, keycv2.contourArea) # 做多边形逼近得到四边形四个顶点 peri cv2.arcLength(doc_contour, True) approx cv2.approxPolyDP(doc_contour, 0.02 * peri, True) # 如果逼近结果正好是4个点就可以做透视变换了 if len(approx) 4: # 把四个点按左上、右上、右下、左下排序后传给getPerspectiveTransform pts approx.reshape(4, 2) ...这套流程的精髓在于先找到最大轮廓再把轮廓简化为四边形最后用四个角点做透视变换。如果这个文档在图片里没有完整拍全最大轮廓可能是墙面或其他杂物这时候需要结合面积阈值和形状判断做多重筛选不能一上来就认定最大轮廓是文档。6.2 运动检测中的轮廓统计视频流处理里先用背景减除或帧差法得到前景的掩码图再对这个掩码图做轮廓检测统计轮廓数量和面积就能得到画面中有几个运动的物体以及它们的大小。这是很多安防摄像头移动侦测功能的基础实现方式。实际做的时候有个优化技巧不必对每一帧都做完整轮廓检测可以降低检测频率比如每秒检测5帧其余帧只做简单的帧差判断出现大范围变化再触发完整轮廓分析。这样CPU占用大幅下降性能提升非常明显。6.3 目标计数与尺寸估计工业场景里经常需要统计传送带上的零件数量。做法是对相机拍到的图像做轮廓检测筛选出面积在零件合理范围内的轮廓然后统计轮廓个数。更进一步的结合标定板的物理尺寸和像素尺寸换算比例可以用轮廓的外接矩形或最小外接矩形推算零件的实际物理尺寸自动挑出尺寸不合格的次品。这套方案的关键是标定正确。拍一张已知尺寸的标定物比如一个直径10mm的圆用轮廓检测得到它的像素直径就能算出每毫米对应的像素数。之后所有轮廓的像素尺寸除以这个比例就是实际物理尺寸。具体场景中建议多拍几个位置取平均值因为镜头畸变会导致画面边缘的像素比例和中心不一致。6.4 性能优化降低轮廓检测计算量轮廓检测本身不算慢但处理视频流或高分辨率图像时计算量会陡增。我的习惯是先降采样再检测把图像宽度缩到800像素以内再二值化和提取轮廓速度能提升好几倍。只对感兴趣区域ROI做轮廓检测不做全图扫描。用RETR_EXTERNAL代替RETR_TREE少算层级信息速度更快。二值化前先做高斯模糊可以减少许多零碎小轮廓从而减少后续遍历和筛选的开销。7. 常见问题速查轮廓检测的疑难杂症7.1 轮廓断成了好几段怎么办这通常是二值图里物体内部有断裂或噪声导致的。优先尝试闭运算MORPH_CLOSE把断裂处连接起来核大小根据断裂宽度调整一般从5x5开始试。如果断裂特别严重加大核的尺寸再配合膨胀操作处理。7.2 检测到了很多小杂点轮廓杂点来源一般是噪声或细小纹理。两种方案一是在findContours之前用中值滤波或高斯滤波去噪二是在检测完之后按面积阈值过滤cv2.contourArea(cnt) threshold的直接跳过。我倾向于两者都做预处理和后处理配合稳定性最好。7.3 轮廓坐标和原图对不上检查你是否对原图像做了缩放或裁剪而轮廓是从处理后的图上取的。如果操作了缩放一定要做坐标还原。或者更省事的办法所有处理都在同一份缩放后的图像上完成最后再根据缩放比例把关键坐标映射回原图。7.4 同一物体检测出内外两层轮廓原因是物体内部有孔洞或者颜色不连续二值化后变成了独立的白色区域。用RETR_EXTERNAL可以减少这种情况或者用形态学闭运算先填洞。如果确实需要保留孔洞信息就用RETR_TREE然后根据层级关系筛选要哪一层。7.5 OpenCV版本不同导致findContours报错这个非常常见。OpenCV 2的findContours返回的是两个值OpenCV 3及之后返回的是两个值还是三个值取决于具体版本和API。我建议在代码开头做一次版本判断或者统一用新版本的接收方式version cv2.__version__.split(.)[0] if version 4: result cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) contours result[0] else: result cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) _, contours, _ result以上问题你只要在实际开发里走一遍大概率都会碰到。排查顺序我建议永远是先看二值图和预处理结果再用可视化把二值图、轮廓、最终标注一层层画出来对比。轮廓检测这类任务视觉反馈比日志调试直观得多出问题先别猜画出中间结果一眼就能定位。这几年玩下来我的体会是轮廓分析看着简单但真正用好的关键不在findContours本身而在于把预处理、模式选择、后处理过滤和特征计算串成一条完备的链路。你把这条链路想透彻了无论是做文档矫正、物体计数还是形状识别都只是在这条链路上换几个参数的事。先沉下心把基础链路玩熟练再在实战里逐步加需求这条路走下来是最稳的。
阅读完成 · 觉得有帮助?
咨询建站