如何将分割模型生成的二值掩码轮廓近似为四个角点
提取二值掩码轮廓的最小面积四点包围方案
核心思路
原方案的问题在于minAreaRect强制生成矩形,无法适配非矩形的目标轮廓;approxPolyDP的近似结果受epsilon参数影响极大,难以稳定得到四个点且保证最小面积。我们可以通过以下步骤解决:
- 预处理优化:用开运算替代单纯腐蚀,在去除噪声的同时减少轮廓过度收缩
- 凸包简化:提取轮廓的凸包,过滤凹点和局部噪声,保留核心外围边界
- 最小面积四边形拟合:基于凸包点集,找到能覆盖大部分轮廓点的最小面积四边形
实现代码
替换原代码中轮廓近似及之后的逻辑,完整修改后的代码如下:
#!/usr/bin/env python3 import os from os import path as osp import cv2 import numpy as np path = "seg_masks" im_list = os.listdir(path) def lcc(binary_image:np.ndarray)->np.ndarray: # 获取最大连通分量 num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(binary_image, connectivity=8) largest_component_label = np.argmax(stats[1:, cv2.CC_STAT_AREA]) + 1 # 跳过背景标签0 largest_component_mask = (labels == largest_component_label).astype(np.uint8) return largest_component_mask def point_in_quadrilateral(point, quad): # 判断点是否在四边形内部(含边界) def cross(o, a, b): return (a[0]-o[0])*(b[1]-o[1]) - (a[1]-o[1])*(b[0]-o[0]) a, b, c, d = quad d1 = cross(a, b, point) d2 = cross(b, c, point) d3 = cross(c, d, point) d4 = cross(d, a, point) has_neg = (d1 < 0) or (d2 < 0) or (d3 < 0) or (d4 < 0) has_pos = (d1 > 0) or (d2 > 0) or (d3 > 0) or (d4 > 0) return not (has_neg and has_pos) def find_min_area_quadrilateral(contour, coverage_threshold=0.95): # 计算轮廓凸包 hull = cv2.convexHull(contour) hull_points = np.squeeze(hull, axis=1) n = len(hull_points) if n < 4: return None # 凸包点不足4个,无法生成四边形 min_area = float('inf') best_quad = None # 遍历凸包中所有四点组合,筛选覆盖达标且面积最小的四边形 for i in range(n): for j in range(i+1, n): for k in range(j+1, n): for l in range(k+1, n): quad = [hull_points[i], hull_points[j], hull_points[k], hull_points[l]] area = cv2.contourArea(np.array([quad], dtype=np.int32)) if area >= min_area: continue # 检查轮廓点覆盖比例 covered = 0 for p in contour: if point_in_quadrilateral(p[0], quad): covered +=1 coverage = covered / len(contour) if coverage >= coverage_threshold and area < min_area: min_area = area best_quad = quad # 若遍历无结果,用凸包极值点生成初始四边形 if best_quad is None: x_sorted = sorted(hull_points, key=lambda p: p[0]) y_sorted = sorted(hull_points, key=lambda p: p[1]) leftmost = x_sorted[0] rightmost = x_sorted[-1] topmost = y_sorted[0] bottommost = y_sorted[-1] best_quad = [leftmost, topmost, rightmost, bottommost] return np.array(best_quad, dtype=np.int32) for img_name in im_list: bgr_img_mask = cv2.imread(osp.join(path, img_name), 0) cv2.imwrite(osp.join(path, "white", img_name), bgr_img_mask) lcc_mask = lcc(bgr_img_mask) # 开运算替代单纯腐蚀,保留轮廓形状的同时去噪 cl_ker = 5 kernel = np.ones((cl_ker, cl_ker), np.uint8) opening = cv2.morphologyEx(lcc_mask, cv2.MORPH_OPEN, kernel, iterations=2) contours, _ = cv2.findContours( opening, mode=cv2.RETR_EXTERNAL, # 仅提取最外层轮廓,减少干扰 method=cv2.CHAIN_APPROX_SIMPLE # 压缩轮廓点,降低计算量 ) if(len(contours)): max_cnt = max(contours, key=cv2.contourArea) quad = find_min_area_quadrilateral(max_cnt) if quad is not None: # 绘制目标四边形 bgr_img_mask = cv2.drawContours(bgr_img_mask, [quad], 0, 200, 2) # 绘制凸包作为参考 hull = cv2.convexHull(max_cnt) cv2.drawContours(bgr_img_mask, [hull], -1, 128, 1) win_name = "img" cv2.namedWindow(win_name, cv2.WINDOW_NORMAL) cv2.imshow(win_name, bgr_img_mask) cv2.waitKey(0) cv2.destroyAllWindows()
关键优化点说明
- 预处理:
MORPH_OPEN(开运算)先腐蚀去噪,再膨胀恢复轮廓,避免单纯腐蚀导致的轮廓过度收缩 - 轮廓提取:
RETR_EXTERNAL只保留最外层轮廓,CHAIN_APPROX_SIMPLE压缩冗余点,大幅降低后续计算量 - 凸包简化:过滤轮廓的凹点和局部噪声,保留最外围边界,是最小包围计算的核心基础
- 四边形筛选:通过遍历凸包四点组合,确保生成的四边形覆盖指定比例(默认95%)的原始轮廓点,同时面积最小
- 效率兼容:若凸包点数量过大,可替换为旋转卡尺算法(O(n)时间复杂度)来计算最小面积外接四边形
内容的提问来源于stack exchange,提问作者bhomaidan90
相关产品推荐
相关产品推荐

