资讯动态

Python OpenCV实现几何形状检测与尺寸测量完整指南

发布时间:2026/9/8 22:23:44 来源:尧图企业网站定制
简介这套方案基于OpenCV实现图像中几何形状的检测与尺寸测量面向图像处理初学者及OpenCV开发者。核心思路采用pixels per metric ratio每度量比的像素比例尺通过已知物体的实际长度与像素数量换算像素与物理单位的关系进而测量边长、直径、内角等关键参数。压缩包内含25个文件主要包括6个Python脚本、9张测试图片、5个XML配置以及依赖说明文本覆盖形状检测、矩形标注、交互式多边形标定等模块并附带requirements依赖清单便于直接运行与二次开发。包体约13.22MB轻量紧凑适合学习与实践。目前已有1370人学习下载。通过完整源码与示例图片读者可掌握基于OpenCV的物体尺寸测量流程理解图像坐标系与实际物理尺度的转换逻辑并能够将方法迁移到工业测量、文档扫描等场景。 做过机器视觉测量的人应该都有体会给一张零件照片让你测出它的边长、内角、直径看似简单真正跑起来却到处是坑。这篇文章我想把自己用Python和OpenCV实现几何形状检测与尺寸测量的完整思路整理出来包括图像预处理、轮廓发现、形状判定、边长直径和内角的计算逻辑以及像素值怎么换算成毫米这种“文档里不会细讲”的关键问题。内容主要面向有Python基础、想用OpenCV做物体测量或形状识别的开发者。看完你不仅能跑通代码还能理解每个阶段为什么这么做遇到光照不均、轮廓断边、测量精度差这类实际问题时也能有明确的排查方向。1. 拿到一张图测量任务到底在测什么很多初学者上来就写代码找轮廓结果发现识别不准、数值乱跳。问题的根源往往不是代码写错了而是没想清楚图像测量的本质逻辑。1.1 图像测量不是“量像素”而是“量物理世界”图像中的一切测量最终都要落到像素坐标上。相机把三维世界投影成二维像素数组我们测量的是“某个轮廓在图像里占了几个像素”。但用户要的边长、直径通常是毫米、厘米这类物理单位所以图像测量本质上得做一次坐标变换。这里有一个基本换算关系实际尺寸 像素距离 × 像素/毫米比例。也就是说想测出真实物理尺寸必须先知道每个像素代表多少毫米这个比例通常靠图像中的已知尺寸参考物标定。如果被测物体和相机距离固定、焦距固定像素比例就是一个稳定值。一旦物体离相机近了或远了比例就变了测量值也会跟着飘。这是所有图像测量的基础约束后面所有精度问题都绕不开这一点。1.2 这条技术链路要打通哪几个环节从原始图像到测量数值中间要经历四个环节图像预处理把彩色图转成灰度图、去除噪声、增强目标与背景的对比度为后续提取做好铺垫。轮廓发现找到图像中所有对象的边界像素集合。OpenCV里最常用的是findContours函数。形状分类根据轮廓的几何特征区分三角形、矩形、多边形、圆形。核心是approxPolyDP多边形逼近加上顶点数量或圆形度判定。几何测量对每个识别出的形状计算边长、内角、直径并用标定比例换算成物理尺寸。这四个环节不是孤立的前面一步的质量直接决定后面一步的准确性。比如预处理没做好轮廓就会断或者多出毛刺。我在实际项目中见过最多的问题就是approxPolyDP参数调不对矩形被识别成五边形或者三角形后面测出来的边长、内角自然全是错的。所以下面从预处理开始逐个拆解。2. 让图像里只有形状预处理与轮廓发现的细节轮廓提取的前提是目标与背景分离。如果图像里物体颜色和背景接近或者光照不均匀直接做轮廓提取会得到一堆碎边这是图像测量最常遇到的第一道坎。2.1 灰度、滤波、二值化为什么顺序不能乱常规流程是这样的cvtColor转灰度把三通道的颜色信息降成单通道灰度。很多教程直接就做这一步但要注意如果目标颜色与背景颜色差异明显也可以先做颜色通道分离或HSV色彩空间提取效果比单纯灰度化更好。GaussianBlur高斯滤波去除传感器噪声和细小纹理干扰。这个操作非常关键不滤波直接做边缘检测会检测出大量由噪点产生的假边缘。threshold或Canny得到二值图让目标区域变成白色、背景变成黑色方便轮廓算法找到对象的边界。顺序上滤波必须在阈值化之前。因为高斯模糊的作用是把噪声平滑掉如果先做阈值噪声会被放大成黑白噪块再滤波也救不回来了。阈值选择也有讲究。如果目标与背景灰度差异明显用固定阈值cv2.THRESH_BINARY就行。如果光照不均固定阈值会出错——亮的区域全被当成目标暗的区域目标消失。这时候推荐cv2.adaptiveThreshold自适应阈值或者用Otsu自动计算最优阈值。Otsu适合灰度直方图呈现双峰分布的图像实测稳定简单场景够用。2.2 轮廓提取findContours的正确打开方式二值图准备好后直接用cv2.findContours提取轮廓contours, hierarchy cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)有两个选择需要解释清楚。第一个是mode参数。RETR_EXTERNAL只取最外层轮廓适合测量规则物体因为内层轮廓通常是文字、孔洞、纹理之类的干扰。RETR_LIST取所有轮廓适合需要检测内部孔洞或多目标叠放的场景。我做测量时习惯用RETR_EXTERNAL省去后续大量过滤工作。第二个是method参数。CHAIN_APPROX_SIMPLE会压缩轮廓点只保留端点比如矩形只保留四个顶点。CHAIN_APPROX_NONE保留所有边界点。测量场景下用SIMPLE就够了点少了计算还快。提取完后通常有一堆小轮廓来源于噪点和微小杂块。我会先按轮廓面积从大到小排序再过滤掉面积小于某个阈值的轮廓。这里我一个常用的经验面积阈值可以先按图像总面积的0.1%~0.5%来设置再根据实际效果微调。2.3 多边形逼近approxPolyDP的epsilon参数是怎么决定形状识别结果的拿到轮廓之后下一步是把轮廓拟合成多边形。approxPolyDP做的是“用更少的点去逼近原轮廓”逼近的精度由epsilon控制epsilon 0.02 * cv2.arcLength(contour, True) approx cv2.approxPolyDP(contour, epsilon, True)epsilon的含义是“原轮廓上的点到逼近多边形对应边的最大允许距离”。它越大逼近越粗糙得到的顶点越少它越小逼近越精细顶点越多。通常取轮廓周长的1%~5%。这个参数直接决定形状分类的成败。给个具体例子一个矩形轮廓如果边缘有轻微锯齿epsilon取太小可能拟合出5、6个顶点被误判成五边形取太大三角形可能被拟合成一条近似直线或四边形。所以epsilon要根据轮廓的实际平滑度做调整。固定的0.02倍周长能覆盖多数情况但对噪声敏感的图像不够。我实测中遇到过的情况是矩形在epsilon0.01时识别出6个点调到0.03后才稳定成4个点。反过来一个梯形用0.05就可能被逼成三角形。所以遇到识别不稳定时优先检查epsilon。3. 判断“这是什么形状”识别逻辑与鲁棒性取舍形状分类是测量前的关键一步。识别错了形状后续算边长、内角就失去了意义。3.1 顶点数量判定三角形、四边形……多边形多边形逼近完成后统计顶点的数量就能判断大致形状3个顶点三角形4个顶点四边形矩形、梯形、平行四边形都有可能5个及以上多边形但实际图像往往没那么干净。由于透视、噪声、边缘毛刺的存在一个矩形可能被拟合成5个点其中一个点几乎在一条边上角度接近180度。我遇到这种情况时会做一个“伪点合并”处理遍历相邻三个顶点形成的角度如果某个角度接近180度比如大于170度就认为中间那个点是冗余点把它去掉重新构建轮廓。这个方法很朴素但实测非常有用。它相当于在approxPolyDP的结果上再做一次“精炼”能显著提高四边形识别率。3.2 圆的检测HoughCircles还是轮廓拟合圆形没有顶点需要单独处理。两种主流方案第一种是cv2.HoughCircles基于霍夫变换在参数空间找圆。它对残缺圆和噪声有较好的容忍度但参数非常敏感尤其是minDist、param1、param2这三个值换一张图可能就得重新调一遍。第二种是轮廓拟合。先找到轮廓用cv2.minEnclosingCircle(contour)得到最小外接圆和半径再算圆形度area cv2.contourArea(contour) circle_area math.pi * radius * radius circularity area / circle_area如果circularity大于0.85具体阈值看应用场景就判定为圆。这种方法的好处是不用调霍夫参数而且直接得到了半径方便后续测直径。我个人更偏向轮廓拟合法因为它与前面的轮廓提取流程浑然一体代码统一逻辑也更直观。但要注意圆形度对轮廓质量的要求比较高如果图像中圆形边缘有缺口circularity会明显下降。此时可以先做一次cv2.morphologyEx闭运算把断裂的边缘连起来。3.3 小面积噪点过滤与形态学清理实际图像中轮廓提取结果往往混有各种噪声轮廓——灰尘、反光点、文字笔划等。对此我有个三步处理习惯面积过滤丢掉面积小于阈值的轮廓。形态学处理用cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)做闭运算消除目标内部的黑色空洞再用MORPH_OPEN开运算去掉孤立的小白点。操作顺序不要颠倒先闭后开对颗粒噪声更友好。轮廓平滑对二值图做一次高斯模糊再二值化能让轮廓边缘更平滑减少后续多边形逼近时的顶点抖动。这一步做完后面的形状分类和测量就轻松很多。测量精度的提升往往不是靠高深算法而是这些预处理细节的累积。4. 从轮廓到尺寸边长、直径、内角的数学实现形状认出来了接下来就是纯数学计算。这一步虽然简单但有个最容易翻车的地方——顶点顺序。4.1 边长欧氏距离与顶点对对于多边形边长就是相邻两个顶点的欧氏距离def point_distance(p1, p2): return math.sqrt((p1[0] - p2[0]) ** 2 (p1[1] - p2[1]) ** 2)如果轮廓有n个顶点依次计算(v0,v1),(v1,v2),...,(v_{n-2},v_{n-1})的距离最后再补上(v_{n-1},v0)就得到全部n条边长。这里要注意的是approxPolyDP返回的顶点顺序虽然不是严格有序但它基本是沿着轮廓走的所以相邻顶点之间确实对应着一条实际边。如果顺序乱了算出来的“边长”可能就是对角线了。为了稳妥起见我会先把顶点按角度排序下面内角计算部分会具体讲。4.2 内角向量点积与余弦定理以及顶点顺序的重要性内角用三个相邻顶点计算。设要计算角B顶点是p1(A), p2(B), p3(C)角B由向量BA和BC构成def angle_between_three_points(p1, p2, p3): v1 (p1[0] - p2[0], p1[1] - p2[1]) v2 (p3[0] - p2[0], p3[1] - p2[1]) dot v1[0] * v2[0] v1[1] * v2[1] len1 math.sqrt(v1[0] ** 2 v1[1] ** 2) len2 math.sqrt(v2[0] ** 2 v2[1] ** 2) cos_angle max(-1.0, min(1.0, dot / (len1 * len2))) return math.degrees(math.acos(cos_angle))dot / (len1 * len2)的结果用max(-1.0, min(1.0, ...))夹一下是防止浮点误差导致acos参数超出[-1,1]范围。顶点顺序的重要性在这里体现得很明显。如果顶点是无序的相邻的“三个点”未必是真正的相邻顶点算出来的角度就成了乱码。所以我在计算前会做一个处理先把轮廓所有顶点按绕质心的极角排序也就是按atan2(y - cy, x - cx)排成顺时针或逆时针序列。排序后p[i-1]、p[i]、p[i1]就是真实的相邻关系计算的每个内角才可靠。4.3 直径最小外接圆与Hough检测的对比计算圆形物体的直径处理起来最简单。用minEnclosingCircle得到半径直径就是2 * radius(x, y), radius cv2.minEnclosingCircle(contour) diameter 2 * radius如果你用的是HoughCircles它直接返回圆的圆心和半径计算后同样乘2即可。不过这里有个细节要注意minEnclosingCircle算的是“轮廓的外接最小圆”如果轮廓边缘有缺口或凸起圆的半径会偏大。这种情况下可以先对轮廓做cv2.convexHull凸包操作把缺口填补成凸多边形再计算外接圆这样得到的半径更接近真实值。这个操作在处理带有凹槽或毛边的圆形零件时效果明显。5. 像素值换算成毫米透视、标定与精度坑计算做完得到的是像素值。想输出“边长25.4mm”这种结果必须经历标定这一步。很多教程不讲这部分导致读者跑通代码后测一组数发现和真实尺寸对不上就开始怀疑算法本身有问题。其实不是算法的问题是少了标定。5.1 像素比例测距法的前提与局限最常用的标定方法是像素比例测距法在图像中放置一个已知物理尺寸的参考物比如一枚直径精确为20mm的硬币测出它所占的像素数比例就是scale 实际尺寸 / 像素尺寸 # 单位mm/pixel之后测量任意目标时像素距离乘上这个比例就得到真实尺寸。这个方法的成立前提是参考物与被测物体处于同一平面、同一高度。如果被测物体比参考物更靠近相机它在图像中会显得更大用参考物算出的比例去测它结果会偏大。这也是用单目相机做测量最根本的局限性。所以在实际项目中我通常会把参考物和被测量零件放在同一个平整平面上并且保证相机光轴尽量垂直于这个平面。这样得到的比例才有意义。5.2 透视变形怎么影响测量结果假设相机没有正对着被测平面而是倾斜了一定角度。此时矩形在图像里会变成梯形圆形会变成椭圆。虽然多边形逼近依然能找到4个顶点但算出来的边长比例、内角数值全是扭曲的——矩形的四个直角可能变成70度和110度。这种情况下需要做透视校正。方法是对参考物常见用棋盘格或已知尺寸的矩形卡片进行cv2.getPerspectiveTransform和cv2.warpPerspective先把图像校正成俯视视角再做测量。这一步能大幅提升测量精度代价是需要额外的标定步骤代码也多了一些复杂度。严格来说这属于相机标定的范畴和本项目的核心代码是两层东西。但在要求较高精度时误差小于3%透视校正几乎是必经之路。5.3 标定物摆放的实操准则结合自己做过的项目标定物摆放有这几个实操建议参考物必须清晰可见边缘不能被遮挡否则像素尺寸会测不准。参考物尽量大一些占据图像面积的20%~40%为佳。参考物太小像素尺寸误差会被放大比例计算的偏差也更大。参考物和被测量物要同时出现在同一张图像里避免移动相机或物体后比例发生改变。如果条件允许用高对比度参考物——比如黑色背景上的白色圆形片——这样轮廓提取精度更高标定也更稳定。实现时我习惯把参考物单独检测并计算scale然后用同一个拍摄条件下的比例去测其他所有目标。这样写代码也简单先识别参考物算比例再识别目标物算尺寸整个流程串起来就是一条流水线。6. 完整可运行的代码示例直接抄作业前五章把原理讲透了这章给出一个完整的Python实现。它能够检测图像中的三角形、四边形、多边形和圆形输出每个形状的边长、内角、直径并结合参考物比例输出毫米值。6.1 主流程代码import cv2 import numpy as np import math def order_points_clockwise(pts): 按绕质心的角度排序轮廓顶点保证计算内角时顶点相邻 cx sum(p[0] for p in pts) / len(pts) cy sum(p[1] for p in pts) / len(pts) return sorted(pts, keylambda p: math.atan2(p[1] - cy, p[0] - cx)) def point_distance(p1, p2): return math.sqrt((p1[0] - p2[0]) ** 2 (p1[1] - p2[1]) ** 2) def angle_between_three_points(p1, p2, p3): 计算 p2 为顶点的内角单位为度 v1 (p1[0] - p2[0], p1[1] - p2[1]) v2 (p3[0] - p2[0], p3[1] - p2[1]) dot v1[0] * v2[0] v1[1] * v2[1] len1 math.sqrt(v1[0] ** 2 v1[1] ** 2) len2 math.sqrt(v2[0] ** 2 v2[1] ** 2) if len1 0 or len2 0: return 0.0 cos_angle max(-1.0, min(1.0, dot / (len1 * len2))) return math.degrees(math.acos(cos_angle)) def merge_nearly_collinear_points(approx, angle_threshold170.0): 合并接近180度的冗余点提升形状识别稳定性 pts [tuple(p[0]) for p in approx] n len(pts) merged True while merged and n 3: merged False for i in range(n): prev pts[(i - 1) % n] cur pts[i] nxt pts[(i 1) % n] angle angle_between_three_points(prev, cur, nxt) if angle angle_threshold: pts.pop(i) merged True break n len(pts) return pts def detect_and_measure(image_path, scale_mm_per_pixelNone): img cv2.imread(image_path) if img is None: print(图片读取失败) return height, width img.shape[:2] # 1. 预处理 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blurred cv2.GaussianBlur(gray, (5, 5), 0) _, binary cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 形态学闭运算 开运算清理边缘和噪点 kernel np.ones((5, 5), np.uint8) binary cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) binary cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) # 2. 轮廓提取 contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 3. 按面积过滤 min_area (height * width) * 0.001 valid_contours [c for c in contours if cv2.contourArea(c) min_area] valid_contours.sort(keycv2.contourArea, reverseTrue) results [] for contour in valid_contours: area cv2.contourArea(contour) perimeter cv2.arcLength(contour, True) if perimeter 0: continue # 多边形逼近 epsilon 0.02 * perimeter approx cv2.approxPolyDP(contour, epsilon, True) pts merge_nearly_collinear_points(approx) if len(pts) 3: continue # 4. 形状分类 测量 if len(pts) 3: shape_name Triangle side_lengths [ point_distance(pts[i], pts[(i 1) % 3]) for i in range(3) ] angles [ angle_between_three_points(pts[(i - 1) % 3], pts[i], pts[(i 1) % 3]) for i in range(3) ] results.append({ type: shape_name, vertices: pts, sides: side_lengths, angles: angles, diameter: None }) elif len(pts) 4: shape_name Quadrilateral ordered order_points_clockwise(pts) side_lengths [ point_distance(ordered[i], ordered[(i 1) % 4]) for i in range(4) ] angles [ angle_between_three_points(ordered[(i - 1) % 4], ordered[i], ordered[(i 1) % 4]) for i in range(4) ] results.append({ type: shape_name, vertices: ordered, sides: side_lengths, angles: angles, diameter: None }) else: # 5个点以上先检查圆形度更像圆就按圆处理 (cx, cy), radius cv2.minEnclosingCircle(contour) circle_area math.pi * radius * radius circularity area / circle_area if circle_area 0 else 0 if circularity 0.85: shape_name Circle results.append({ type: shape_name, vertices: None, sides: None, angles: None, diameter: 2 * radius }) else: shape_name fPolygon({len(pts)}) ordered order_points_clockwise(pts) side_lengths [ point_distance(ordered[i], ordered[(i 1) % len(ordered)]) for i in range(len(ordered)) ] angles [ angle_between_three_points( ordered[(i - 1) % len(ordered)], ordered[i], ordered[(i 1) % len(ordered)] ) for i in range(len(ordered)) ] results.append({ type: shape_name, vertices: ordered, sides: side_lengths, angles: angles, diameter: None }) # 5. 输出结果 for idx, r in enumerate(results): print(f形状 {idx 1}: {r[type]}) if r[diameter] is not None: if scale_mm_per_pixel: print(f 直径 {r[diameter]:.2f} px ({r[diameter] * scale_mm_per_pixel:.2f} mm)) else: print(f 直径 {r[diameter]:.2f} px) if r[sides]: print(f 边长(px): , .join([f{s:.2f} for s in r[sides]])) if scale_mm_per_pixel: print(f 边长(mm): , .join([f{s * scale_mm_per_pixel:.2f} for s in r[sides]])) if r[angles]: print(f 内角(deg): , .join([f{a:.2f} for a in r[angles]])) # 6. 可视化结果 output img.copy() for r in results: if r[vertices]: pts_arr np.array(r[vertices], dtypenp.int32).reshape(-1, 1, 2) cv2.polylines(output, [pts_arr], True, (0, 255, 0), 2) label r[type] if r[diameter] is not None: label r[type] cx int(sum(p[0] for p in r[vertices]) / len(r[vertices])) cy int(sum(p[1] for p in r[vertices]) / len(r[vertices])) cv2.putText(output, label, (cx - 20, cy), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imshow(Result, output) cv2.waitKey(0) cv2.destroyAllWindows() if __name__ __main__: # scale_mm_per_pixel 由参考物标定得到例如参考物直径20mm像素直径100px则填0.2 detect_and_measure(shapes.png, scale_mm_per_pixelNone)这段代码把前面几章讲的所有环节串在了一起。实际使用时要注意这几个地方第一merge_nearly_collinear_points里的角度阈值要根据轮廓质量调整轮廓越毛糙阈值可以设得越宽松第二min_enclosing_circle的圆形度阈值也不是固定值有的圆形表面反光严重时阈值降到0.75更合理第三scale_mm_per_pixel建议通过检测参考物轮廓后自动计算不要硬编码。6.2 一个典型的输出示例假设图像里有一个边长约50像素的三角形、一个边长约80×40像素的矩形和一个直径约100像素的圆输出效果类似这样形状 1: Triangle 边长(px): 49.85, 50.12, 50.03 内角(deg): 60.10, 59.95, 59.95 形状 2: Quadrilateral 边长(px): 80.02, 39.98, 80.05, 39.90 内角(deg): 90.05, 89.95, 89.98, 90.02 形状 3: Circle 直径 100.24 px如果scale_mm_per_pixel0.2边长和直径就会再输出一行毫米值三角形的三条边都接近10mm矩形是16mm×8mm圆形直径约20.05mm。从输出可以看出数据处理得越干净测量结果就越接近理论值。如果内角出现明显偏离比如矩形出现88度和92度大半原因是轮廓提取时边缘有毛边或者透视变形。这类问题不是算法能完全弥补的要从图像采集阶段就想办法解决。6.3 几个实测下来最有用的经验最后分享三条实测经验。第一cv2.putText不支持直接输出中文会在图像上显示乱码。如果要在结果图上注释中文需要借助PIL把中文绘制到图像上或者干脆用英文标签代替。生产项目中我一般用英文或拼音省事且稳定。第二参考物自动标定的代码并不复杂用同样的轮廓检测流程找到参考物测出它的像素直径或边长再用已知物理尺寸除以像素尺寸得到比例。这样整个程序可以做到“一键出结果”不需要人工填参数也减少出错机会。第三任何测量系统都要做重复性验证。同一张图片应该每次跑出相同结果这是确定性的但换一个角度重新拍照结果可能会变。所以判断测量算法好坏的标准不是“它测出的数是不是恰好等于理论值”而是“在同一环境下多次测量的方差够不够小”。如果方差大先回到预处理环节找原因不要盲目调测量代码。图像测量的边界不在算法层面而在图像质量本身。预处理每多做一步数据越干净识别和测量就越稳。这条链路跑通之后稍微扩展一下思路——把视频流的每帧都丢进这个检测函数就是一个小型的实时尺寸监测系统再加上一个固定的参考物和工厂里的固定工位就接近一条基础的视觉检测雏形了。做图像处理项目的乐趣就在这里核心逻辑一旦打通很多场景都能直接复用。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价