• 用 OpenCV 实现图像中水平线检测与校正


    前言

    在本文中,我们将探讨如何使用 Python 和 OpenCV 库来检测图像中的水平线,并对图像进行旋转校正以使这些线条水平。这种技术可广泛应用于文档扫描、建筑摄影校正以及机器视觉中的各种场景。

    环境准备

    首先,确保您的环境中安装了 OpenCV 库。如果还没有安装,可以通过以下命令安装,要注意尽管代码里我们都是使用的cv2,但是安装包要选opencv-python:

    pip install opencv-python
    

    试验效果

    原始图像

    在这里插入图片描述

    找出水平线

    在这里插入图片描述

    基于统计角度旋转

    在这里插入图片描述

    步骤概述

    1. 图像加载与预处理:加载图像,转换为灰度图,然后使用 Canny 算法检测边缘。
    2. 线条检测:应用霍夫变换来识别图像中的线条。
    3. 水平线条筛选:过滤出接近水平的线条。
    4. 线条可视化:在图像上绘制检测到的水平线。
    5. 计算需要的旋转角度:计算线条的加权平均角度,以确定图像应旋转的角度。
    6. 图像旋转校正:根据计算出的角度旋转图像,以校正线条至水平。

    详细实现

    1. 图像加载与预处理
      加载图像并将其转换为灰度图,这是大多数图像处理任务的常见做法,因为它简化了接下来的处理步骤。
    image = cv2.imread('test.png') # 读取图片
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 转换成灰度图像
    
    1. 边缘检测
      使用 Canny 算法进行边缘检测,这是一种广泛使用的边缘检测算法,因为它有效地识别图像中的线条和边缘。
    edges = cv2.Canny(gray, 50, 150, apertureSize=3)
    

    参数详解
    gray:这是输入图像,Canny 边缘检测通常在灰度图像上进行,因为边缘检测是基于图像亮度变化的。
    50:第一个阈值用于边缘检测的低阈值。这是用于Canny算法中的双阈值过程的较低边界。低于此阈值的像素点不会被视为边缘。
    150:第二个阈值用于边缘检测的高阈值。这是用于Canny算法中的双阈值过程的较高边界。高于此阈值的像素点将被视为边缘的强候选者。
    apertureSize=3:这是用于内部边缘检测的Sobel算子的大小。apertureSize定义了计算图像梯度所用的Sobel核的大小。常用的尺寸是3,但也可以使用更大的尺寸如5或7,这在处理较大的边缘时可以提供更平滑的结果。

    1. 线条检测与筛选
      通过霍夫变换检测线条,然后筛选出接近水平的线条。我们定义了一个函数 filter_horizontal_lines,它计算每条线的角度,并筛选出角度小于设定阈值的线条。
    	def filter_horizontal_lines(lines, angle_threshold=10):
        horizontal_lines = []
        if lines is not None:
            for line in lines:
                x1, y1, x2, y2 = line[0]
                angle = np.degrees(np.arctan2(y2 - y1, x2 - x1))
                if abs(angle) < angle_threshold:
                    horizontal_lines.append(line)
        return horizontal_lines
    
    	lines = cv2.HoughLinesP(edges, 1, np.pi / 180, 100, minLineLength=100, maxLineGap=10)
    	horizontal_lines = filter_horizontal_lines(lines)
    

    HoughLinesP参数说明

    # HoughLinesP使用概率霍夫变换检测图像中的线段 (注意HoughLines和HoughLinesP是两个函数方法)
    lines = cv2.HoughLinesP(
        edges,             # 边缘图像,通常是Canny边缘检测的输出
        1,                 # rho - 累加器的距离精度,以像素为单位
        np.pi / 180,       # theta - 累加器的角度精度,以弧度为单位
        100,               # threshold - 累加器的阈值,仅返回大于此阈值的线段
        minLineLength=100, # minLineLength - 线段的最小长度
        maxLineGap=10      # maxLineGap - 同一线条上允许的最大间隙
    )
    
    1. 计算旋转角度
      我们定义了一个函数 calculate_average_angle,它计算所有检测到的水平线条的加权平均角度。这个角度将用于图像旋转校正。注意这里的np.average(angles, weights=lengths)使用了加权,也就是这个函数会基于找到的线段长度,进行角度的加权平均,如果你只是单纯的关注线段的所有角度,可以删掉weights这个参数。
    def calculate_average_angle(lines):
        """
        计算线条的加权平均角度。
    
        参数:
            lines (list): 包含线条的列表,每条线条由两个点的坐标表示,格式为 [x1, y1, x2, y2]。
    
        返回:
            float: 线条的加权平均角度,以度为单位。如果没有符合条件的线条,则返回 0。
        """
        angles = []
        lengths = []
        if lines:
            for line in lines:
                x1, y1, x2, y2 = line[0]
                # 计算线条的长度
                length = np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
                # 计算线条的角度,以度为单位
                angle = np.degrees(np.arctan2(y2 - y1, x2 - x1))
    
                # 角度校正,确保处理的角度是接近水平的
                if abs(angle) > 90:
                    angle -= 180
                # 只考虑接近水平的线条
                if abs(angle) < 20:  # 可调整此阈值以更好地适应具体情况
                    angles.append(angle)
                    lengths.append(length)
    
        # 计算加权平均角度
        if lengths:
            average_angle = np.average(angles, weights=lengths)
        else:
            average_angle = 0
    
        return average_angle
        
    average_angle = calculate_average_angle(horizontal_lines) # 调用函数完成平均角度计算
    
    1. 图像旋转校正
      最后,我们基于返回的角度,旋转图像,使线条尽可能水平。我们使用 OpenCV 提供的仿射变换函数 cv2.warpAffine 来完成这个任务。
    def rotate_image(image, angle):
        (h, w) = image.shape[:2]
        center = (w // 2, h // 2)
        M = cv2.getRotationMatrix2D(center, angle, 1.0)
        rotated = cv2.warpAffine(image, M, (w, h))
        return rotated
    
    rotated_image = rotate_image(image, average_angle)
    cv2.imwrite('rotated_image.jpg', rotated_image)
    

    代码纯享版

    import cv2
    import numpy as np
    
    def filter_horizontal_lines(lines, angle_threshold=10):
        horizontal_lines = []
        if lines is not None:
            for line in lines:
                x1, y1, x2, y2 = line[0]
                angle = np.degrees(np.arctan2(y2 - y1, x2 - x1))
                if abs(angle) < angle_threshold:
                    horizontal_lines.append(line)
        return horizontal_lines
    #
    #
    def calculate_average_angle(lines):
        angles = []
        lengths = []
        if lines:
            for line in lines:
                x1, y1, x2, y2 = line[0]
                length = np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
                angle = np.degrees(np.arctan2(y2 - y1, x2 - x1))
    
                # 角度校正,确保处理的角度是接近水平的
                if abs(angle) > 90:
                    angle -= 180
                # 只考虑接近水平的线条
                if abs(angle) < 20:  # 可调整此阈值以更好地适应具体情况
                    angles.append(angle)
                    lengths.append(length)
    
        # 计算加权平均角度
        if lengths:
            average_angle = np.average(angles, weights=lengths)
        else:
            average_angle = 0
    
        return average_angle
    
    
    
    # def calculate_average_angle(lines):
    #     angles = []
    #     lengths = []
    #     filtered_lines = []
    # 
    #     if lines:
    #         # 计算每条线的长度和角度
    #         for line in lines:
    #             x1, y1, x2, y2 = line[0]
    #             length = np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
    #             angle = np.degrees(np.arctan2(y2 - y1, x2 - x1))
    # 
    #             # 角度校正,确保处理的角度是接近水平的
    #             if abs(angle) > 90:
    #                 angle -= 180
    #             if abs(angle) < 20:  # 只考虑接近水平的线条
    #                 filtered_lines.append((angle, length))
    # 
    #         # 按长度排序,并取最长的前9条线
    #         filtered_lines.sort(key=lambda x: x[1], reverse=True)
    #         top_lines = filtered_lines[:9]
    # 
    #         # 分割角度和长度
    #         angles, lengths = zip(*top_lines) if top_lines else ([], [])
    # 
    #     # 计算加权平均角度
    #     if lengths:
    #         average_angle = np.average(angles, weights=lengths)
    #     else:
    #         average_angle = 0
    # 
    #     return average_angle
    
    # 使用此函数时,确保传入的lines是过滤后只包含接近水平的线条
    
    def draw_lines(image, lines):
        for line in lines:
            x1, y1, x2, y2 = line[0]
            cv2.line(image, (x1, y1), (x2, y2), (0, 255, 0), 3)
        return image
    
    def rotate_image(image, angle):
        (h, w) = image.shape[:2]
        center = (w // 2, h // 2)
        M = cv2.getRotationMatrix2D(center, angle, 1.0)
        rotated = cv2.warpAffine(image, M, (w, h))
        return rotated
    
    # 加载图像
    image = cv2.imread('test.png')
    
    # 转换为灰度图
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    
    # 边缘检测
    edges = cv2.Canny(gray, 50, 150, apertureSize=3)
    
    # 使用霍夫变换检测线条
    lines = cv2.HoughLinesP(edges, 1, np.pi / 180, threshold=100, minLineLength=100, maxLineGap=10)
    
    # 过滤出横向线条
    horizontal_lines = filter_horizontal_lines(lines)
    
    # 在原图上绘制检测到的横向线条
    image_with_lines = draw_lines(np.copy(image), horizontal_lines)
    
    # 保存带线条的图像
    cv2.imwrite('image_with_horizontal_lines.jpg', image_with_lines)
    
    # 计算线条的加权平均角度
    average_angle = calculate_average_angle(horizontal_lines)
    print("计算得到的加权平均角度为:", average_angle)
    
    # 旋转整个图像使线条水平
    rotated_image = rotate_image(image, average_angle)  # 根据角度旋转 正角度表示逆时针旋转,而负角度表示顺时针旋转
    
    # 保存旋转后的图像
    cv2.imwrite('rotated_image.jpg', rotated_image)
    
    print("旋转后的图像已保存为 'rotated_image.jpg'")
    

    结论

    通过上述步骤,我们能够自动检测并校正图像中的水平线,这对于许多自动化处理任务来说是非常有用的。本文介绍的方法仅依赖于 OpenCV,易于实现且效果显著。了解相关函数,通过适当调整参数,该技术可以适应不同的应用需求和条件。

  • 相关阅读:
    《Python神经网络编程》学习笔记
    FAQ docker运行tomcat提示找不到文件
    【优化组合】基于人工蜂群算法求解投资优化组合问题附matlab代码
    [Spring Cloud] Nacos 实战 + Aws云服务器
    ModStartBlog 现代化个人博客系统 v5.2.0 主题开发增强,新增联系方式
    RocketMq简介及安装、docker安装rocketmq、安装rocketmq可视化管理端
    FMT自抗扰控制算法(ADRC)现已开源
    python从入门到实践 第17章:使用API自己感悟和部分代码
    git使用小结
    springBoot框架简介入门教程(快速学习版)
  • 原文地址:https://blog.csdn.net/crazyjinks/article/details/139423283