ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

倾斜边的角度计算

2026/8/13 20:13:11 拓冰建站 浏览量
倾斜边的角度计算

1. 如图

  如下图所示:白色背景,绿色目标,有一定倾斜角度。

img

2. 代码

  直接给出代码

import cv2
import numpy as np
import osdef extract_side_edges(closed):"""从二值图中提取左右边界点。假设上边和下边始终水平,逐行扫描,取每一行最左/最右的白色像素点,分别作为左边界和右边界的采样点。"""h, w = closed.shapeleft_pts, right_pts = [], []for y in range(h):xs = np.where(closed[y] > 128)[0]if len(xs) == 0:continueleft_pts.append((xs.min(), y))right_pts.append((xs.max(), y))return np.array(left_pts, dtype=np.float32), np.array(right_pts, dtype=np.float32)def ransac_line_angle(pts, n_iter=2000, inlier_thresh=1.5, min_inlier_ratio=0.3, seed=42):"""用RANSAC拟合边界点的主要直线趋势,返回相对竖直方向的夹角(度)。直线以 x = m*y + b 的形式表示(以y为自变量),因为边界整体沿竖直方向延伸。RANSAC能自动剔除局部毛刺/离群点,只信任多数点公认的直线趋势,保证稳定性。"""rng = np.random.default_rng(seed)n = len(pts)if n < 2:return 0.0, ptsxs, ys = pts[:, 0], pts[:, 1]best_inlier_mask = Nonebest_count = -1for _ in range(n_iter):i1, i2 = rng.choice(n, size=2, replace=False)y1, y2 = ys[i1], ys[i2]if abs(y2 - y1) < 1e-6:continuem = (xs[i2] - xs[i1]) / (y2 - y1)b = xs[i1] - m * y1pred_x = m * ys + bresidual = np.abs(xs - pred_x)inlier_mask = residual < inlier_threshcount = int(np.sum(inlier_mask))if count > best_count:best_count = countbest_inlier_mask = inlier_maskif best_inlier_mask is None or best_count < n * min_inlier_ratio:# 没找到足够一致的直线段(点集噪声太大),退化为用全部点拟合,保证稳定输出best_inlier_mask = np.ones(n, dtype=bool)inlier_pts = pts[best_inlier_mask]# 用内点做最终最小二乘精修,得到直线方向向量vx, vy, x0, y0 = cv2.fitLine(inlier_pts, cv2.DIST_L2, 0, 0.01, 0.01).flatten()# 方向向量转角度,并归一化到 (-90, 90],避免180度歧义angle = np.degrees(np.arctan2(vx, vy))angle = angle % 180if angle > 90:angle -= 180return angle, inlier_ptsdef rectify_fiber_orientation(image_path, output_dir='.'):"""完整流程:1. 读取原始彩色图2. Otsu二值化 + 闭运算,得到前景掩码(中间结果保存)3. 提取左右边界点,RANSAC拟合角度(左右各自打印,并取平均)4. 用该平均角度把原始彩色图旋转摆正(自动处理旋转方向,不改变图像宽高)5. 保存所有中间结果和最终结果"""os.makedirs(output_dir, exist_ok=True)# ---------- 1. 读取原图 ----------image = cv2.imread(image_path, 1)if image is None:raise FileNotFoundError(f'无法读取图片: {image_path}')h, w = image.shape[:2]# ---------- 2. 二值化 + 闭运算 ----------gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)_, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)cv2.imwrite(os.path.join(output_dir, 'threshold.png'), thresh)kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 15))closed = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)cv2.imwrite(os.path.join(output_dir, 'closed.png'), closed)# ---------- 3. 提取左右边界 + RANSAC拟合角度 ----------left_pts, right_pts = extract_side_edges(closed)if len(left_pts) < 2 or len(right_pts) < 2:print('警告: 前景像素点太少,无法拟合边界直线,返回原图')return imageleft_angle, left_inliers = ransac_line_angle(left_pts)right_angle, right_inliers = ransac_line_angle(right_pts)avg_angle = (left_angle + right_angle) / 2.0print(f'左边界倾斜角度: {left_angle:.3f} 度  (拟合点数: {len(left_inliers)}/{len(left_pts)})')print(f'右边界倾斜角度: {right_angle:.3f} 度  (拟合点数: {len(right_inliers)}/{len(right_pts)})')print(f'平均倾斜角度(用于旋转): {avg_angle:.3f} 度')# 边界拟合可视化(中间结果)vis = cv2.cvtColor(closed, cv2.COLOR_GRAY2BGR)for x, y in left_pts:cv2.circle(vis, (int(x), int(y)), 1, (255, 128, 0), -1)   # 左边界全部点: 蓝色for x, y in right_pts:cv2.circle(vis, (int(x), int(y)), 1, (0, 200, 255), -1)   # 右边界全部点: 黄色for x, y in left_inliers:cv2.circle(vis, (int(x), int(y)), 1, (0, 0, 255), -1)     # 左边界拟合内点: 红色for x, y in right_inliers:cv2.circle(vis, (int(x), int(y)), 1, (0, 0, 255), -1)     # 右边界拟合内点: 红色cv2.imwrite(os.path.join(output_dir, 'edge_fit_visualization.png'), vis)# ---------- 4. 旋转摆正原图 ----------# 关键点1: cv2.getRotationMatrix2D 的角度是"逆时针为正",#          而我们的 avg_angle 定义是"边界相对竖直方向的偏角"(atan2(vx,vy)得到),#          两者符号约定刚好相反,所以旋转时要取负号,才能把偏斜"转回竖直"。#          (已用数学坐标点旋转的方式做过交叉验证,确认这个符号正确)# 关键点2: 角度范围被 ransac_line_angle 限定在 (-90, 90] 之内,#          且实际场景角度通常是小角度(远小于45度),不会出现"长边被转成短边"#          导致宽高互换90度的情况;为保险起见,仍做一次显式assert防护。rotate_angle = -avg_angleassert -45 <= rotate_angle <= 45, f'旋转角度异常: {rotate_angle}, 请检查角度计算是否正确'M = cv2.getRotationMatrix2D((w / 2, h / 2), rotate_angle, 1.0)rectified = cv2.warpAffine(image, M, (w, h),  # 保持原图宽高不变,不做扩边borderMode=cv2.BORDER_CONSTANT,borderValue=(255, 255, 255))# ---------- 5. 保存最终结果 ----------out_path = os.path.join(output_dir, 'rectified.png')cv2.imwrite(out_path, rectified)print(f'摆正后的图片已保存到: {out_path}')return rectifiedif __name__ == '__main__':rectify_fiber_orientation(image_path='./img.png',output_dir='./output3')

closed

edge_fit_visualization

rectified