OpenCV轮廓几何特征全解析:外接矩形、最小外接矩形、多边形拟合与外接圆实战指南

📅 2026/8/3 3:15:08 👁️ 阅读次数 📝 编程学习
OpenCV轮廓几何特征全解析:外接矩形、最小外接矩形、多边形拟合与外接圆实战指南

1. 项目概述:从轮廓到几何特征的精准捕捉

在计算机视觉和图像处理的实际项目中,我们常常需要从一张图片中识别出目标物体,并对其进行量化和分析。比如,在工业质检中,我们需要知道一个零件的长宽尺寸;在自动驾驶中,需要框出车辆或行人的位置;甚至在文档扫描应用中,需要自动矫正倾斜的纸张。这些需求的核心,都离不开一个基础操作:从目标轮廓中提取出能够描述其位置、大小和形状的几何特征

opencv-python库为我们提供了强大的轮廓处理工具链,其中,外接矩形、最小外接矩形、多边形拟合和外接圆是四种最常用、也最核心的几何特征描述方法。它们就像是给一个形状模糊的物体套上不同“模具”,从而得到标准化的、可度量的几何参数。很多新手可能会混淆这些概念,或者只知道调用函数,却不清楚背后的原理和适用场景。比如,一个倾斜的条形码,用普通外接矩形框出来的面积会很大且包含大量背景,而最小外接矩形则能紧密贴合,更准确地反映其真实朝向和尺寸。

这篇文章,我将结合自己多年在图像算法开发中踩过的坑,为你彻底拆解这四种几何特征。我不会只给你罗列函数API,而是会深入讲解它们各自的数学原理、计算过程、在OpenCV中的具体实现,以及最重要的——在什么场景下该用哪一个,以及如何避免常见的陷阱。无论你是正在做课程设计的学生,还是需要解决实际工程问题的开发者,相信这些从实战中总结出的经验都能让你少走弯路。

2. 核心概念辨析:四种几何特征的本质差异

在深入代码之前,我们必须从几何意义上厘清这四者的区别。这是正确选型的基础,否则很容易“张冠李戴”,得出错误的分析结果。

2.1 外接矩形 (Bounding Rectangle)

这是最直观、计算最快的一种。外接矩形,顾名思义,就是一个能完全包围住目标轮廓的、边与图像坐标轴平行(即水平/垂直)的矩形。它只关心轮廓在X轴和Y轴方向上的最大、最小范围。

  • 计算原理:遍历轮廓上的所有像素点,分别找出X坐标的最大值(x_max)、最小值(x_min)和Y坐标的最大值(y_max)、最小值(y_min)。那么,矩形的左上角顶点就是(x_min, y_min),宽度为(x_max - x_min),高度为(y_max - y_min)
  • 特点:由于它的边平行于坐标轴,所以这个矩形不会随着物体的旋转而旋转。当物体倾斜时,外接矩形会变得很大,包含很多非物体区域的背景。
  • OpenCV函数cv2.boundingRect(contour)
  • 生活类比:就像给一个任意形状的玩具找一个刚好能装下它的、正放的方形纸盒。玩具如果斜着放,纸盒就必须做得更大。

2.2 最小外接矩形 (Minimum Area Rectangle / Rotated Rectangle)

这是外接矩形的“升级版”。它寻找的是能够包围轮廓的所有可能的矩形中,面积最小的那一个。这个矩形的边不一定平行于坐标轴,它可以旋转任意角度来紧密贴合目标。

  • 计算原理:OpenCV通常采用“旋转卡壳”算法。简单理解,算法会尝试用一对平行的“卡尺”夹住轮廓,并旋转这对卡尺,寻找在某个角度下,平行线之间的垂直距离(即矩形的一边)和另一对垂直平行线之间的距离(矩形的另一边)乘积(即面积)最小的状态。
  • 特点:能更精确地反映倾斜物体的实际朝向和最小包围尺寸。返回的结果通常包含矩形的中心点(cx, cy)、宽度(width)、高度(height)和旋转角度(angle)这里有个巨坑:OpenCV中,这个角度angle的范围是[-90, 0)度,表示矩形从水平位置(宽度边为水平)逆时针旋转的角度。当矩形“站立”时(高度大于宽度),宽度和高度可能会互换,需要小心处理。
  • OpenCV函数cv2.minAreaRect(contour)返回一个RotatedRect对象。用cv2.boxPoints(rotated_rect)可以获取它的四个顶点坐标。
  • 生活类比:给同一个倾斜的玩具,找一个可以定制角度的、刚好紧密包裹它的最小长方形礼品盒。这个盒子是贴着玩具边缘的。

2.3 多边形拟合 (Polygon Approximation)

前面两种方法得到的都是矩形,但很多物体并不是矩形的。多边形拟合的目标是用一个顶点更少的、更简单的多边形来近似表示复杂的轮廓,保留其主要形状特征,同时过滤掉噪声和细节。

  • 计算原理:最常用的算法是Douglas-Peucker算法(OpenCV中cv2.approxPolyDP的实现)。它的思想很巧妙:轮廓是一条由许多点组成的曲线。算法首先连接曲线的首尾两点得到一条线段。然后找出原始曲线上离这条线段最远的点,如果这个距离大于设定的阈值epsilon,就把这个点保留为关键顶点。接着,用这个关键点将曲线分成两段,对每一段递归地重复这个过程。最终,所有保留下的点就构成了拟合后的多边形。
  • 特点epsilon参数是关键,它是精度阈值。值越大,拟合出的多边形越简单(顶点越少),但与原始轮廓差异越大;值越小,拟合越精确,但顶点越多,越接近原始轮廓。常用于判断轮廓形状(如通过顶点数判断是三角形、矩形还是多边形)、轮廓简化或压缩存储。
  • OpenCV函数cv2.approxPolyDP(curve, epsilon, closed)
  • 生活类比:用有限的几根直线段(比如6根或8根)去拼出一个复杂地图(如中国地图)的轮廓。线段越少,轮廓越抽象;线段越多,越接近真实形状。

2.4 外接圆 (Minimum Enclosing Circle)

寻找一个能完全包围轮廓的最小的圆。它用一个中心点和半径来描述轮廓。

  • 计算原理:OpenCV内部通常采用一种迭代算法,或者更高效的Welzl算法(随机增量法)。算法目标是找到一个圆,使得所有轮廓点都在圆内或圆上,并且圆的半径尽可能小。
  • 特点:对于长宽比接近1的、近似圆形的物体(如细胞、硬币、瓶盖),外接圆是非常好的描述符。它的表示非常简洁(一个中心点+半径),并且具有旋转不变性。但对于细长的物体(如一支笔),外接圆会非常大,效率低下且不精确。
  • OpenCV函数cv2.minEnclosingCircle(contour)返回圆心(x, y)和半径radius
  • 生活类比:给一个物体套上一个最小尺寸的“泡泡”或“光环”。

注意:这四种方法都是基于已检测到的轮廓(contour)进行计算的。轮廓检测(如使用cv2.findContours)的质量直接决定了这些几何特征提取的准确性。如果轮廓本身有断裂、噪声或粘连,那么提取出的几何特征也是不可靠的。

3. OpenCV实战:函数详解与代码实现

理论清楚了,我们直接上代码,看看在opencv-python中如何具体使用这些函数,并可视化结果。我会用一个同时包含矩形、三角形和圆形物体的图片作为示例。

3.1 环境准备与图像预处理

首先,确保你已经安装了OpenCV。如果没有,通过pip安装:pip install opencv-python。我们将创建一个模拟图像,并对其进行预处理以得到清晰的轮廓。

import cv2 import numpy as np import matplotlib.pyplot as plt # 创建一个黑色背景的画布 canvas = np.zeros((400, 600, 3), dtype=np.uint8) # 在画布上绘制几个形状作为我们的“目标物体” # 1. 一个红色的倾斜矩形 (模拟一个倾斜的卡片) rect_points = np.array([[100, 50], [250, 80], [230, 180], [80, 150]], np.int32) cv2.fillPoly(canvas, [rect_points], (0, 0, 255)) # 红色 # 2. 一个绿色的三角形 triangle_points = np.array([[400, 100], [500, 100], [450, 200]], np.int32) cv2.fillPoly(canvas, [triangle_points], (0, 255, 0)) # 绿色 # 3. 一个蓝色的圆形 (模拟一个硬币或瓶盖) center = (300, 300) radius = 40 cv2.circle(canvas, center, radius, (255, 0, 0), -1) # 蓝色,填充 # 为了轮廓检测,需要将彩色图像转为灰度,并二值化 gray = cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB) # 注意:matplotlib用RGB,OpenCV用BGR gray = cv2.cvtColor(gray, cv2.COLOR_RGB2GRAY) _, binary = cv2.threshold(gray, 1, 255, cv2.THRESH_BINARY) # 查找轮廓 # 使用cv2.RETR_EXTERNAL只检测最外层轮廓,cv2.CHAIN_APPROX_SIMPLE压缩轮廓点 contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 创建一个用于绘制结果的图像副本 result_img = canvas.copy()

这段代码创建了一个包含三种形状的图像,并将其二值化,然后使用cv2.findContours找到了三个独立的轮廓。cv2.CHAIN_APPROX_SIMPLE参数很重要,它会压缩水平、垂直和对角线方向的冗余点,只保留端点,从而节省内存。对于我们的几何计算,这足够了。

3.2 外接矩形与最小外接矩形的绘制与对比

接下来,我们分别对每个轮廓计算并绘制外接矩形和最小外接矩形,直观感受它们的区别。

# 为了清晰,我们逐个轮廓处理 for i, cnt in enumerate(contours): # 1. 计算并绘制外接矩形 (绿色) x, y, w, h = cv2.boundingRect(cnt) cv2.rectangle(result_img, (x, y), (x+w, y+h), (0, 255, 0), 2) # 绿色矩形框 # 2. 计算并绘制最小外接矩形 (青色) rotated_rect = cv2.minAreaRect(cnt) box_points = cv2.boxPoints(rotated_rect) # 获取旋转矩形的四个顶点 box_points = np.int0(box_points) # 转换为整数 cv2.drawContours(result_img, [box_points], 0, (255, 255, 0), 2) # 青色多边形框 # 打印信息进行对比 print(f"轮廓 {i}:") print(f" 外接矩形: 左上角({x}, {y}), 宽{w}, 高{h}, 面积{w*h}") print(f" 最小外接矩形: 中心{rotated_rect[0]}, 宽高{rotated_rect[1]}, 角度{rotated_rect[2]:.2f}度") # 使用matplotlib显示结果 plt.figure(figsize=(12, 5)) plt.subplot(121), plt.imshow(cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB)), plt.title('原始形状') plt.subplot(122), plt.imshow(cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB)), plt.title('外接矩形(绿) vs 最小外接矩形(青)') plt.show()

运行这段代码,你会清晰地看到:

  • 对于倾斜的红色矩形,绿色的外接矩形(平行于坐标轴)很大,而青色的最小外接矩形则紧密地贴合了红色矩形的边缘,并且是倾斜的。打印出的面积也会显示,最小外接矩形的面积远小于外接矩形。
  • 对于绿色的正三角形,两种矩形框出的区域可能比较接近,但最小外接矩形依然会找到一个更贴合的朝向。
  • 对于蓝色的圆形,两种矩形都会是一个正方形(因为圆在各个方向上的投影范围相同),且面积相近。

实操心得:处理cv2.minAreaRect返回的角度时务必小心。它的角度定义是:将水平线(与矩形宽度边平行)逆时针旋转,第一次遇到矩形任意一边时所转过的角度,范围在[-90, 0)度。这意味着,当矩形“站立”(高度>宽度)时,OpenCV可能会交换宽和高,以保证宽度是较大的那个尺寸?不,实际上OpenCV的size(宽高)是未交换的,角度总是关于“宽度”边。一个更稳妥的做法是:永远根据角度将矩形归一化到[0, 90)度区间,并相应地处理宽高,或者直接使用四个顶点boxPoints进行计算,这比解析角度和宽高更不容易出错。

3.3 多边形拟合的精度控制实战

多边形拟合的核心在于参数epsilon的选择。我们以倾斜的矩形轮廓为例,看看不同epsilon值的效果。

# 我们取第一个轮廓(倾斜矩形)来做多边形拟合演示 cnt_example = contours[0] # 计算轮廓的周长,epsilon通常取周长的某个百分比 peri = cv2.arcLength(cnt_example, True) # 尝试不同的epsilon值 epsilon_list = [0.01 * peri, 0.03 * peri, 0.05 * peri, 0.1 * peri] titles = ['1% Perimeter', '3% Perimeter', '5% Perimeter', '10% Perimeter'] plt.figure(figsize=(15, 10)) for i, (eps, title) in enumerate(zip(epsilon_list, titles)): # 进行多边形拟合 approx = cv2.approxPolyDP(cnt_example, epsilon=eps, closed=True) # 创建新图像用于绘制 img_approx = canvas.copy() # 绘制原始轮廓(红色细线) cv2.drawContours(img_approx, [cnt_example], -1, (0, 0, 255), 1) # 绘制拟合多边形(黄色粗线) cv2.drawContours(img_approx, [approx], -1, (0, 255, 255), 3) # 标记拟合多边形的顶点(蓝色圆点) for point in approx: cv2.circle(img_approx, tuple(point[0]), 5, (255, 0, 0), -1) plt.subplot(2, 2, i+1) plt.imshow(cv2.cvtColor(img_approx, cv2.COLOR_BGR2RGB)) plt.title(f'{title} (Epsilon={eps:.1f}), Vertices={len(approx)}') plt.axis('off') plt.tight_layout() plt.show()

观察结果:

  • epsilon=0.01*周长:拟合非常精确,生成的多边形顶点很多(可能超过4个),几乎和原始轮廓重合。这适用于需要高精度还原形状的场景。
  • epsilon=0.03*周长:开始简化,但可能仍能识别出这是一个四边形(顶点数接近4)。这是常用的平衡点。
  • epsilon=0.05周长 或 0.1周长:过度简化。对于一个矩形轮廓,拟合结果可能变成一个三角形甚至更简单的形状,丢失了关键特征。

注意事项cv2.approxPolyDPclosed参数应设为True,因为我们处理的是闭合轮廓。通过顶点数len(approx),我们可以做简单的形状识别:顶点数≈3是三角形,≈4是矩形/四边形,>4是多边形,顶点数很多且拟合误差很小时,可能是圆形。但要注意,一个正方形用大epsilon拟合也可能变成四边形或三角形,所以阈值选择需要根据实际物体和图像分辨率反复调试。

3.4 外接圆的计算与应用场景

最后,我们计算并绘制每个轮廓的外接圆。

result_img_circle = canvas.copy() for i, cnt in enumerate(contours): # 计算外接圆 (x, y), radius = cv2.minEnclosingCircle(cnt) center = (int(x), int(y)) radius = int(radius) # 绘制外接圆 cv2.circle(result_img_circle, center, radius, (255, 0, 255), 2) # 洋红色圆 # 绘制圆心 cv2.circle(result_img_circle, center, 3, (255, 255, 0), -1) # 青色点 print(f"轮廓 {i} 外接圆: 圆心{center}, 半径{radius}") plt.figure(figsize=(10, 4)) plt.subplot(121), plt.imshow(cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB)), plt.title('原始形状') plt.subplot(122), plt.imshow(cv2.cvtColor(result_img_circle, cv2.COLOR_BGR2RGB)), plt.title('外接圆(洋红)与圆心(青)') plt.show()

从图像和打印信息可以看出:

  • 对于圆形,外接圆完美匹配,圆心和半径就是其真实几何参数。
  • 对于倾斜矩形和三角形,外接圆为了完全包围它们,半径会比较大,圆心也基本位于物体的几何中心。这直观地展示了外接圆对非圆形物体的“不经济性”。

4. 综合应用与场景选型指南

掌握了单个技术后,如何在实际项目中组合使用并做出正确选择?下面我通过几个典型场景来分析。

4.1 场景一:文档扫描与透视矫正

需求:用手机拍一张倾斜的文档(如发票、书页),希望自动矫正成正面视角的矩形图像。方案

  1. 轮廓检测:通过边缘检测或二值化,找到文档这个最大的四边形轮廓。
  2. 多边形拟合:使用cv2.approxPolyDP,设置一个合适的epsilon(如周长的0.02倍),将轮廓拟合为一个四边形(4个顶点)。如果得到的顶点数不是4,可能需要调整阈值或预处理步骤。
  3. 顶点排序:得到的4个顶点顺序是随机的。我们需要将其排序为[左上, 右上, 右下, 左下]的顺序,这是进行透视变换所必需的。可以通过计算顶点x+y的和(最小为左上,最大为右下)以及x-y的差等方法进行排序。
  4. 透视变换:使用cv2.getPerspectiveTransform计算变换矩阵,然后用cv2.warpPerspective将倾斜文档“拉直”。

为什么不用最小外接矩形?最小外接矩形虽然能给出倾斜角度,但它返回的是一个矩形,而拍摄变形的文档是一个梯形或任意四边形(透视效果)。最小外接矩形会丢失这种透视信息,矫正效果不自然。多边形拟合得到了四个角点,才能进行真正的透视矫正。

4.2 场景二:工业零件尺寸测量

需求:在传送带上检测螺栓,并测量其头部对边宽度(即扳手尺寸)。方案

  1. 轮廓检测:定位出每个螺栓的轮廓。
  2. 最小外接矩形:对每个螺栓轮廓计算cv2.minAreaRect。螺栓头部通常是正六边形,其最小外接矩形恰好能反映对边距离。
  3. 尺寸计算:从RotatedRect中取出宽度(width)和高度(height)。对于规则的正六边形螺栓头,这个矩形的短边长度就近似等于对边宽度(S值)。你需要根据已知的像素-实际尺寸比例(比如先拍一个标定块)进行换算。
  4. 角度判断:旋转角度angle可以判断零件在传送带上的朝向,用于后续的机械臂抓取定位。

为什么不用外接圆或多边形拟合?外接圆无法给出对边距离。多边形拟合可以得到六边形的顶点,通过计算对边顶点距离也能得到尺寸,但计算比直接取最小外接矩形的宽/高更复杂,且对轮廓完整性要求更高。最小外接矩形在这里是效率与精度兼顾的最佳选择。

4.3 场景三:细胞计数与大小分析

需求:在显微镜图像中统计圆形细胞的数量并分析其直径分布。方案

  1. 图像分割与轮廓检测:通过阈值分割或更高级的分水岭算法,将粘连细胞分开,得到每个细胞的独立轮廓。
  2. 外接圆计算:对每个轮廓计算cv2.minEnclosingCircle。因为细胞近似圆形,外接圆能很好地逼近其实际大小。
  3. 参数获取:直接读取圆的半径(radius),直径即为2 * radius。根据标定的微米/像素比例,换算成实际尺寸。圆心(center)可用于细胞的空间分布分析。
  4. 过滤与统计:可以根据圆的半径(或面积)过滤掉过小(可能是噪声)或过大(可能是细胞团块)的检测结果,然后进行计数和直径分布统计。

为什么不用矩形?细胞是圆形的,用矩形(无论是哪种)来描述都会引入系统误差。外接圆的表示最自然,计算也方便。

4.4 选型决策流程图

为了帮助你快速决策,我总结了一个简单的决策流程:

开始 │ ├─ 目标物体是否近似圆形? (如细胞、硬币、孔洞) │ ├─ 是 → 使用【外接圆】获取圆心和半径。 │ └─ 否 → │ ├─ 是否需要紧密包围盒,且物体有明确朝向? (如零件、车牌、文本行) │ │ ├─ 是 → 使用【最小外接矩形】获取中心、长宽和角度。 │ │ └─ 否 → │ │ ├─ 是否只需一个简单的、轴对齐的包围框? (如目标检测的初始框、区域统计) │ │ │ ├─ 是 → 使用【外接矩形】,计算最快。 │ │ │ └─ 否 → │ │ └─ 物体的形状复杂,需要简化表示或识别多边形? (如文档角点、自定义形状) │ │ └─ 是 → 使用【多边形拟合】,通过控制epsilon平衡精度与简化程度。 │ └─ └─ 结束

5. 常见问题排查与性能优化技巧

在实际编码中,你肯定会遇到各种奇怪的问题。下面是我踩过的一些坑和解决方案。

5.1 轮廓查找失败或结果异常

  • 问题cv2.findContours返回的轮廓列表为空,或者轮廓非常破碎。
  • 排查
    1. 检查二值图像:在调用findContours前,先用cv2.imshow或保存图像的方式查看你的二值图(binary)。确保目标物体是白色(255),背景是黑色(0),并且物体区域是连通的。
    2. 调整阈值:如果使用固定阈值cv2.threshold,光照变化会导致二值化效果差。尝试使用自适应阈值cv2.adaptiveThreshold或大津法cv2.THRESH_OTSU
    3. 预处理:在二值化前,加入高斯模糊cv2.GaussianBlur可以平滑噪声,帮助获得更干净的轮廓。使用形态学操作(如开运算cv2.morphologyEx)可以去除小噪点,闭合细小裂缝。
    4. 轮廓检索模式cv2.RETR_EXTERNAL只找最外层轮廓。如果你的目标物体内部有空洞,并且你需要内外轮廓,应使用cv2.RETR_TREEcv2.RETR_LIST

5.2 最小外接矩形的角度与宽高困惑

  • 问题:从cv2.minAreaRect得到的角度angle和宽高(width, height),有时和视觉直觉不符,宽高可能会互换。
  • 解决方案:不要依赖angle的原始值来做绝对判断。一个健壮的处理方法是:
    def get_ordered_rect(rotated_rect): """获取一个规范化表示的最小外接矩形:确保宽度 >= 高度,角度在[0, 90)度""" center, size, angle = rotated_rect width, height = size # 确保宽度总是大于等于高度 if width < height: width, height = height, width angle += 90 # 角度调整 # 将角度规范到 [0, 90) 度区间 angle = angle % 90 return (center, (width, height), angle)
    或者,更简单直接地使用四个顶点boxPoints。你可以通过计算顶点之间的距离来确定长边和短边,通过计算长边与水平线的夹角来得到你需要的角度。

5.3 多边形拟合顶点数不稳定

  • 问题:同一个物体,在不同图片或不同光照下,cv2.approxPolyDP返回的顶点数飘忽不定。
  • 解决方案
    1. 固定epsilon策略:不要用绝对像素值作为epsilon,而是使用轮廓周长的百分比(如0.02 * peri)。这样算法对物体大小不敏感。
    2. 轮廓平滑:在拟合前,可以对轮廓点进行轻微的平滑处理,比如使用cv2.convexHull先求取凸包,再对凸包进行多边形拟合,可以消除轮廓上的凹坑干扰。
    3. 后处理判断:不要期望顶点数绝对等于3、4、5。可以设置一个范围,例如,如果顶点数在[3,5]之间,我们可以认为它是三角形或四边形。结合轮廓面积、长宽比等其他特征进行综合判断会更可靠。

5.4 性能优化建议

当需要处理视频流或大量图片时,性能至关重要。

  • 减少轮廓数量:在cv2.findContours后,根据轮廓面积cv2.contourArea或外接矩形大小过滤掉过小的轮廓(可能是噪声)。
  • 降低分辨率:如果精度允许,可以先将图像缩放(cv2.resize)到更小的尺寸进行处理,轮廓点数量会大幅减少,所有几何计算都会变快。
  • 避免重复计算:如果你需要外接矩形、面积、周长等多个属性,尽量在一次循环中计算完,避免对同一个轮廓多次遍历。
  • 使用Numpy向量化操作:在遍历轮廓点寻找极值等操作时,利用Numpy的数组操作(如cnt[:,:,0].min())比纯Python循环快得多。

5.5 一个综合示例:识别并测量图片中的卡片

让我们用一个完整的例子结束,假设我们要识别图片中所有的矩形卡片,并输出它们的长、宽和面积(以像素为单位)。

def detect_and_measure_cards(image_path): # 1. 读取并预处理图像 img = cv2.imread(image_path) if img is None: print("图像读取失败") return gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 使用高斯模糊去噪 blurred = cv2.GaussianBlur(gray, (5, 5), 0) # 使用自适应阈值应对光照不均 binary = cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 2) # 2. 形态学操作,闭合卡片内部文字造成的空洞,连接边缘 kernel = np.ones((3,3), np.uint8) closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel, iterations=2) # 3. 查找轮廓 contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) result_img = img.copy() card_info = [] for cnt in contours: # 4. 面积过滤,去掉太小的轮廓 area = cv2.contourArea(cnt) if area < 500: # 根据图片大小调整阈值 continue # 5. 多边形拟合,判断是否为四边形 peri = cv2.arcLength(cnt, True) approx = cv2.approxPolyDP(cnt, epsilon=0.02*peri, closed=True) if len(approx) == 4: # 认为是四边形 # 6. 计算最小外接矩形获取精确尺寸 rect = cv2.minAreaRect(cnt) (cx, cy), (w, h), angle = rect # 使用规范化函数 (cx, cy), (w_norm, h_norm), angle_norm = get_ordered_rect(rect) # 7. 根据长宽比过滤,避免将细长条误判为卡片 aspect_ratio = max(w_norm, h_norm) / (min(w_norm, h_norm) + 1e-5) if aspect_ratio > 5: # 长宽比过大,可能是线条 continue # 8. 绘制并记录信息 box = cv2.boxPoints(rect) box = np.int0(box) cv2.drawContours(result_img, [box], 0, (0, 255, 0), 2) cv2.putText(result_img, f'W:{w_norm:.0f},H:{h_norm:.0f}', (int(cx)-50, int(cy)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255,0,0), 2) card_info.append({ 'center': (cx, cy), 'width': w_norm, 'height': h_norm, 'area': w_norm * h_norm, 'angle': angle_norm }) # 9. 显示结果 plt.figure(figsize=(15, 6)) plt.subplot(131), plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)), plt.title('原图') plt.subplot(132), plt.imshow(closed, 'gray'), plt.title('预处理后二值图') plt.subplot(133), plt.imshow(cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB)), plt.title('检测与测量结果') plt.show() for i, info in enumerate(card_info): print(f"卡片 {i+1}: 中心{info['center']}, 宽{info['width']:.1f}px, 高{info['height']:.1f}px, " f"面积{info['area']:.1f}px^2, 角度{info['angle']:.1f}°") return card_info # 假设你有一张名为 'cards.jpg' 的图片 # card_measurements = detect_and_measure_cards('cards.jpg')

这个例子融合了轮廓过滤、多边形形状判断、最小外接矩形测量等多个技巧,是一个接近实际应用的完整流程。记住,没有一套参数能通吃所有场景,area阈值、epsilon系数、长宽比阈值等都需要根据你的具体图片进行调整。最好的办法是写一个简单的GUI或者用Jupyter Notebook,实时调整这些参数并观察效果,找到最适合你当前任务的“黄金参数”。