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

news2024/11/24 16:39:47

前言

在本文中,我们将探讨如何使用 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,易于实现且效果显著。了解相关函数,通过适当调整参数,该技术可以适应不同的应用需求和条件。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1797647.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot登录认证--衔接SpringBoot案例通关版

文章目录 登录认证登录校验-概述登录校验 会话技术什么是会话呢?cookie Session令牌技术登录认证-登录校验-JWT令牌-介绍JWT登录校验过滤器流程 SpringBoot案例通关版,上接这篇 登录认证 先讲解基本的登录功能 登录功能本质就是查询操作 那么查询完毕后返回一个Emp对象 如…

SpringSecurity6从入门到实战之登录表单的提交(源码级讲解,耐心看完)

SpringSecurity6从入门到实战之登录表单的提交(源码级讲解,耐心看完) 文接上回,当SpringSecurity帮我们生成了一个默认对象.本文继续对登录流程进行探索,我们如何通过账号密码进行表单的提交,SpringSecurity在这过程中又帮助我们做了什么 登录表单的提交的源码分析 在之前了解…

SITNE24V2BNQ-3/TR一种瞬态电压抑制器,对标PESD1CAN

SITNE24V2BNQ是一种瞬态电压抑制器&#xff0c;设计用于保护两个汽车控制器区域 网络(CAN)母线不受ESD等瞬变造成的损坏。 SITNE24V2BNQ采用SOT-23封装。标准产品不含铅和卤素。 产品参数 方向&#xff1a;双向通道数&#xff1a;2VRWM(V)(Max)&#xff1a;24IPP8/20μS(A)(M…

cad转换为空间数据库方案

autodesk cad 通过另存为dxf格式 如 dxf2010 或者dxf2012。 再通过supermap desktop 软件 可以转换为arcgis esri shape arcgis esri shape 可以用arcgis打开做建库操作。 可以通过第二个个人工具&#xff0c;读取cad设置的颜色&#xff0c;达到数据颜色gis中和cad中一致。 …

金鸣识别:助您快速识别竖排图片文字

大家有没有遇到过这种情况&#xff1f; 当你手捧一本古籍&#xff0c;或是浏览某些特殊的书籍时&#xff0c;文字却是从右到左竖向排版的。这种排版方式&#xff0c;仿佛让我们穿越到了古代&#xff0c;感受到了那种独特的韵味。但问题是&#xff0c;一般的OCR软件根本不支持这…

为何瑞士银行成了富人的“保险箱”?

​瑞士银行&#xff0c;这个名字大家耳熟能详&#xff0c;为啥呢&#xff1f;因为它被誉为“全球最安全银行”。那么&#xff0c;这“最安全”的名头是怎么来的呢&#xff1f;它的金库又藏在哪儿呢&#xff1f; 话说在1930年代&#xff0c;德国纳粹迫害犹太人&#xff0c;导致…

RBAC 模型梳理

1. RBAC 模型是什么 RBAC&#xff08;Role-Based Access Control&#xff09;即&#xff1a;基于角色的权限控制。通过角色关联用户&#xff0c;角色关联权限的方式间接赋予用户权限。 RBAC 模型由 4 个基础模型组成&#xff1a; 基本模型 RBAC0&#xff08;Core RBAC&#x…

Qwen2本地部署的实战教程

大家好,我是herosunly。985院校硕士毕业,现担任算法研究员一职,热衷于机器学习算法研究与应用。曾获得阿里云天池比赛第一名,CCF比赛第二名,科大讯飞比赛第三名。拥有多项发明专利。对机器学习和深度学习拥有自己独到的见解。曾经辅导过若干个非计算机专业的学生进入到算法…

归并排序(分治)

归并排序 概念介绍原理解释&#xff1a;案例步骤&#xff1a;稳定性&#xff1a;画图理解如下 代码实现 概念介绍 归并排序&#xff08;Merge Sort&#xff09;是一种经典的排序算法&#xff0c;基于分治的思想&#xff0c;它将待排序数组分成两个子数组&#xff0c;分别排序&…

12. ESP32-JSON(Arduino)

使用ESP32和Arduino框架处理JSON数据 在物联网&#xff08;IoT&#xff09;开发中&#xff0c;ESP32是一款功能强大的微控制器&#xff0c;它结合了Wi-Fi和蓝牙功能&#xff0c;适用于各种智能设备和传感器项目。JSON&#xff08;JavaScript Object Notation&#xff09;是一种…

C++三大特性之继承,详细介绍

阿尼亚全程陪伴大家学习~ 前言 每个程序员在开发新系统时&#xff0c;都希望能够利用已有的软件资源&#xff0c;以缩短开发周期&#xff0c;提高开发效率。 为了提高软件的可重用性(reusability)&#xff0c;C提供了类的继承机制。 1.继承的概念 继承&#xff1a; 指在现有…

储能逆变器测试负载箱是如何实现的

储能逆变器测试负载箱是专门用于模拟各种负载条件的设备&#xff0c;主要用于对储能逆变器进行性能测试和评估。它可以根据实际需求&#xff0c;模拟不同的负载类型、负载大小和负载变化率&#xff0c;从而为储能逆变器的设计和优化提供准确的数据支持。那么&#xff0c;储能逆…

关于gitee上传成功没有绿点

今天上传完代码以后&#xff0c;打开gitee看了一下&#xff0c;但是看到昨天和今天都没有小绿点&#xff0c;仔细思考了一番&#xff0c;是仓库满了&#xff1f;不对啊&#xff0c;如果满了的话&#xff0c;上传就会失败啊&#xff0c;那这是什么问题呢&#xff1f; 原来是因为…

20240607在Toybrick的TB-RK3588开发板的Android12下适配IMX415摄像头和ov50c40

20240607在Toybrick的TB-RK3588开发板的Android12下适配IMX415摄像头和ov50c40 2024/6/7 11:42 【4K/8K摄像头发热量巨大&#xff0c;请做好散热措施&#xff0c;最好使用散热片鼓风机模式&#xff01;】 结论&#xff1a;欢迎您入坑。 Toybrick的TB-RK3588开发板的技术支持不…

AlaSQL.js:用SQL解锁JavaScript数据操作的魔法

简介 先附上 github 地址 https://github.com/AlaSQL/alasql AlaSQL.js - JavaScript SQL database for browser and Node.js. Handles both traditional relational tables and nested JSON data (NoSQL). Export, store, and import data from localStorage, IndexedDB, or …

从零开始学JAVA

一、编写Hello world程序 public class JavaMain1 {//主程序执行入口&#xff0c;main方法public static void main(String[] args){System.out.println("Hello world!");} } 运行结果 Hello world! java编写主程序常见错误&#xff1a; 1、System ---首字母没有…

vue2+elementui,动态生成的表单校验

话不多,先上一段视频,看看是不是你们需要的效果 elementui动态生成表单校验 附上代码 <template><div class"home"><div class"home-box"><!-- <menuHtml></menuHtml> --><div class"home-div"><…

使用2个手机文件恢复工具,轻松找回文件

在这个智能手机横行的时代&#xff0c;手机已经成为我们生活中不可或缺的一部分。然而&#xff0c;就像生活中的一切事物一样&#xff0c;手机也有可能出现意外&#xff0c;比如文件丢失。这就像是你在超市购物&#xff0c;结果发现钱包不见了&#xff0c;那种感觉真是让人抓狂…

CTFHUB-技能树-web-web前置技能-HTTP协议全

目录 1.请求方式 2.302跳转 3.Cookie 4.基础认证 5.响应包源码 1.请求方式 curl -v -X http://challenge-3022c877a8dcedeb.sandbox.ctfhub.com:10800/index.php 2.302跳转 参考链接&#xff1a;http://t.csdnimg.cn/aqdNG 301——永久性重定向。该状态码表示请求的资源已…

攻防世界---misc---津门杯2021-m1

1、题目描述&#xff0c;下载附件是一张bmp格式的图片 2、直觉告诉我这和图片的颜色通道有关 3、于是我就尝试用stegslove打开图片 4、将颜色通道都改为0&#xff0c;点击preview 5、然后发现一串base64编码 6、解码得flag flag{l5DGqF1pPzOb2LU919LMaBYS5B1G01FD}