基于Python的重叠图形检测计数问题求助(含OpenCV代码)
问题概述
需要开发工具检测并计数图片中的正方形、三角形与圆形,图形存在重叠情况(示例图为几何图形组成的火车)。当前用OpenCV编写的代码存在两个问题:
- 轮子实际是10个圆(5个大圆各含1个小圆),但代码检测出19个圆
- 被轮子遮挡的四边形未被正确识别
当前代码:
import cv2 img = cv2.imread('trem.png') gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) _, threshold = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY) contours, _ = cv2.findContours( threshold, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) i = 0 shapes = dict() triangle = quadrilateral = circle = 0 for contour in contours: if i == 0: i = 1 continue approx = cv2.approxPolyDP( contour, 0.01 * cv2.arcLength(contour, True), True) cv2.drawContours(img, [contour], 0, (0, 0, 255), 5) M = cv2.moments(contour) if M['m00'] != 0.0: x = int(M['m10'] / M['m00']) y = int(M['m01'] / M['m00']) if len(approx) == 3: triangle += 1 shapes.update({'Triangle': triangle}) elif len(approx) == 4: quadrilateral += 1 shapes.update({'Quadrilateral': quadrilateral}) else: circle += 1 shapes.update({'Circle': circle}) cv2.imshow('shapes', img) print(shapes) cv2.waitKey(0) cv2.destroyAllWindows()
当前输出:{'Circle': 19, 'Quadrilateral': 5, 'Triangle': 2}
正确结果:{'Circle': 10, 'Quadrilateral': 6, 'Triangle': 2}
OpenCV代码修正方案
1. 解决圆计数错误问题
原代码靠“非3/4边的轮廓都算圆”的逻辑太粗糙,加上RETR_TREE会提取所有层级的小轮廓(比如圆的内部毛刺、噪点),导致计数虚高。修正方法:
- 过滤极小面积的噪点轮廓(比如面积小于50像素的直接跳过)
- 用圆形度判断圆:公式为
4π×面积÷(周长²),值越接近1越接近标准圆形,一般取阈值>0.8 - 用
RETR_CCOMP提取内外层轮廓,确保能抓到大圆内部的小圆,同时避免冗余轮廓
2. 解决被遮挡四边形未识别问题
原代码的简单阈值会把被遮挡的边缘断开,加上approxPolyDP的epsilon参数(0.01)太严格,导致被遮挡的四边形轮廓被拆成多个小轮廓。修正方法:
- 用Canny边缘检测替代简单阈值,保留更多边缘信息
- 加膨胀操作,把被遮挡断开的边缘重新连接
- 调整
approxPolyDP的epsilon到0.03,让被遮挡的四边形轮廓能被近似成4条边
修改后的代码
import cv2 import numpy as np img = cv2.imread('trem.png') gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 用Canny边缘检测+膨胀替代简单阈值,修复被遮挡的边缘 edges = cv2.Canny(gray, 50, 150) kernel = np.ones((3, 3), np.uint8) edges = cv2.dilate(edges, kernel, iterations=1) # 提取所有内外轮廓,层级结构用CCOMP更清晰 contours, hierarchy = cv2.findContours(edges, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) shapes = {'Triangle': 0, 'Quadrilateral': 0, 'Circle': 0} for idx, contour in enumerate(contours): # 过滤极小噪点 area = cv2.contourArea(contour) if area < 50: continue perimeter = cv2.arcLength(contour, True) # 调整近似多边形的精度,适配被遮挡的轮廓 approx = cv2.approxPolyDP(contour, 0.03 * perimeter, True) # 计算圆形度,排除非圆的不规则轮廓 circularity = 4 * np.pi * (area / (perimeter ** 2)) if perimeter != 0 else 0 # 判断图形类型 if len(approx) == 3: shapes['Triangle'] += 1 elif len(approx) == 4: shapes['Quadrilateral'] += 1 elif circularity > 0.8: shapes['Circle'] += 1 print(shapes) cv2.imshow('processed edges', edges) cv2.waitKey(0) cv2.destroyAllWindows()
不使用OpenCV的实现思路
假设已经用PIL+Numpy提取了轮廓(比如通过二值化、连通区域分析得到边界点),判断图形类型的核心逻辑如下:
- 三角形:实现Douglas-Peucker近似多边形算法,得到的顶点数为3;或者计算轮廓的凸包,凸包顶点数为3
- 四边形:近似多边形顶点数为4;若需区分正方形,可额外判断四条边长度接近、内角接近90度
- 圆形:计算圆形度(公式同前),值大于0.8则判定为圆
核心步骤示例(伪代码)
from PIL import Image import numpy as np from scipy.ndimage import label # 1. 加载图片并二值化 img = Image.open('trem.png').convert('L') img_np = np.array(img) # 手动实现Otsu阈值二值化(替代OpenCV的自动阈值) hist, bins = np.histogram(img_np, bins=256) bin_centers = (bins[:-1] + bins[1:]) / 2 weight1 = np.cumsum(hist) weight2 = np.cumsum(hist[::-1])[::-1] mean1 = np.cumsum(hist * bin_centers) / weight1 mean2 = (np.cumsum((hist * bin_centers)[::-1]) / weight2[::-1])[::-1] variance12 = weight1[:-1] * weight2[1:] * (mean1[:-1] - mean2[1:]) ** 2 idx = np.argmax(variance12) threshold = bin_centers[idx] binary = (img_np < threshold).astype(np.uint8) * 255 # 2. 找连通区域 labeled, num_features = label(binary == 0) # 假设黑色是图形区域 # 3. 对每个连通区域提取轮廓并判断类型 for i in range(1, num_features+1): # 提取区域边界点 coords = np.argwhere(labeled == i) # 计算面积(像素数) area = len(coords) # 提取边缘点(简化版:取区域的外围像素) # 计算周长(边缘点的欧氏距离求和) # 实现Douglas-Peucker算法得到近似多边形顶点数 # 计算圆形度 # 根据顶点数和圆形度判断图形类型
内容的提问来源于stack exchange,提问作者SolaDeCoro
相关产品推荐
相关产品推荐

