倾斜边的角度计算

1. 如图

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

img

2. 代码

  直接给出代码

import cv2
import numpy as np
import os


def extract_side_edges(closed):
    """
    从二值图中提取左右边界点。
    假设上边和下边始终水平,逐行扫描,取每一行最左/最右的白色像素点,
    分别作为左边界和右边界的采样点。
    """
    h, w = closed.shape
    left_pts, right_pts = [], []
    for y in range(h):
        xs = np.where(closed[y] > 128)[0]
        if len(xs) == 0:
            continue
        left_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, pts

    xs, ys = pts[:, 0], pts[:, 1]
    best_inlier_mask = None
    best_count = -1

    for _ 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:
            continue
        m = (xs[i2] - xs[i1]) / (y2 - y1)
        b = xs[i1] - m * y1

        pred_x = m * ys + b
        residual = np.abs(xs - pred_x)
        inlier_mask = residual < inlier_thresh
        count = int(np.sum(inlier_mask))

        if count > best_count:
            best_count = count
            best_inlier_mask = inlier_mask

    if 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 % 180
    if angle > 90:
        angle -= 180

    return angle, inlier_pts


def 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 image

    left_angle, left_inliers = ransac_line_angle(left_pts)
    right_angle, right_inliers = ransac_line_angle(right_pts)
    avg_angle = (left_angle + right_angle) / 2.0

    print(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_angle
    assert -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 rectified


if __name__ == '__main__':
    rectify_fiber_orientation(
        image_path='./img.png',
        output_dir='./output3'
    )

closed

edge_fit_visualization

rectified

 

posted @ 2026-08-13 20:01  wancy  阅读(3)  评论(0)    收藏  举报