目录
OpenCV实现文档自动矫正
1. OpenCV文档矫正的方法
(1)基于霍夫变换的文档矫正方法
(2)基于透视变换的文档矫正方法
2. OpenCV文档自动矫正实现
(0)项目说明
(1)基于霍夫变换的文档矫正方法(效果较差)
(2)基于透视变换的文档矫正方法(效果较好)
(3)文档矫正Android实现
3.项目源码下载
本篇,我们将基于OpenCV实现一个简易的文档自动矫正算法,支持通过用户交互实现文档矫正,也支持通过算法实现完全自动文档矫正,即文档一键矫正;使用用户交互时,需要用户使用鼠标标记图像中文档的四个角点的位置;该方法,不受背景图案影响,矫正精度取决于人工标记文档角点的精度;当然,也可以完全通过算法自动计算,实现真正的文档一键矫正
整套项目源码下载:OpenCV实现文档自动矫正
来,先看一下Demo矫正效果,小黑子又来啦~
通过用户交互实现文档矫正 | 完全自动文档矫正(文档一键矫正) |
【尊重原创,转载请注明出处】https://blog.csdn.net/guyuealian/article/details/128021906
对于普通的文档来说,文字排版一般是是一行一行,因此,如果能求得文档的倾斜角度,通过仿射变换就可实现文档旋正了;文档的倾斜角度可以通过霍夫变换计算。
优缺点:
- 优点:算法实现比较简单,可完全自动化,无需人工调整位置;
- 缺点:矫正效果比较差,容易受背景图案的影响,特别是当背景比较复杂时,矫正效果一塌糊涂。倾斜角度大于45°时,矫正方向容易反向;并且无法进行立体矫正;
透视变换在图像还原的上的应用很广泛,他是将成像投影到一个新的视平面。比如两个摄像头在不同的角度对统一物体进行拍照,物体上的同一个点在两张照片上的坐标是不一样的,为了实现两张图片同一个点的对应关系映射,透视变换就实现了此功能。
假设原图上某点P0,经过某种变换后(变换矩阵H)得到图像P1,那么P0和P1的对应关系可表示为:P0 = H * P1
如果,我们可以获得文档的四个角点位置P0(可以通过用户交互标记,也可以算法自动获得),并且已知并希望其矫正后的角点位置P1(文档是矩形的,矫正后的角点位置可以大致估计),利用P1和P0四个角点的对应关系,我们可以估计其变换矩阵H,再进行透视变换,其矫正效果会比直接使用霍夫变换的文档矫正方法要好很多。
优缺点:
- 优点:算法实现也比较简单,不受倾斜角度大小影响,矫正效果贼好,应用场景比较广
- 缺点:需要获得文档的四个角度位置,因而需要用户交互进行标记;文档的四个角点也可以通过图像处理或者深度学习去预测位置,可实现文档一键矫正。
```
.
├── data # 测试数据
│ ├── image1
│ └── image2
├── utils # 项目相关算法工具
├── demo_correction_v1.py # 基于霍夫变换的文档矫正方法(效果较差)
├── demo_correction_v2.py # 基于透视变换的文档矫正方法(效果较好)
├── requirements.txt # 项目依赖pythonb包,pip安装即可
├── LICENCE
└── README.md
```
项目代码需要用到pybaseutils工具,请使用pip安装即可;其他python包,请参考本人requirements.txt文件版本说明
pip install pybaseutils
下图给出的是基于霍夫变换的文档矫正方法,其算法过程是:
- 对图像进行滤波平滑等处理,减少虚假边缘和噪声的影响;
- 先对图像进行Canny边缘检测
- 使用霍夫变换计算所有符合条件的线段
- 计算所有线段的倾斜角度,
- 将线段分为两类,大于45°作为纵向线段,用蓝色线段表示;小于45°作为横向线段,用红色线段表示,如下图所示
- 由于只考虑倾斜角度小于45°时的文档矫正,因此只需要计算倾斜角度小于45°的线段的平均角度,即是文档的倾斜角度
- 对图像进行反向旋转,即可得到矫正后的文档图片
实现代码如下:
# -*-coding: utf-8 -*-
"""
@Author : PKing
@E-mail : [email protected]
@Date : 2022-11-24 22:13:25
@Brief :
"""
import cv2
import numpy as np
from pybaseutils import geometry_tools, image_utils, file_utils
class ImageCorrection(object):
"""图像矫正程序"""
@staticmethod
def get_hough_lines(img: np.ndarray, rho=1, theta=np.pi / 180, threshold=100, max_angle=35, max_lines=50,
thickness=2, vis=False):
"""
参考:https://blog.csdn.net/on2way/article/details/47028969
:param img: 输入图像
:param rho: 线段以像素为单位的距离精度,double类型的,推荐用1.0
:param theta: 线段以弧度为单位的角度精度,推荐用numpy.pi/180
:param threshold: : 累加平面的阈值参数,int类型,超过设定阈值才被检测出线段,
值越大,意味着检出的线段越长,检出的线段个数越少。根据情况推荐先用100试试
:return:
"""
if len(img.shape) == 3:
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 灰度图像
else:
gray = img.copy()
gray = image_utils.get_image_mask(gray, inv=True)
edge = cv2.Canny(gray, threshold1=0, threshold2=255, apertureSize=3)
# lines is (num_lines,1,2)==>(r,θ)==>(距离rho,角度theta)
lines = cv2.HoughLines(edge, rho=rho, theta=theta, threshold=threshold)
lines = [] if lines is None else lines[:, 0, :]
lines = lines[0:min(len(lines), max_lines)]
angles = []
for i in range(len(lines)):
rho, theta = lines[i] # 其中theta是与Y轴的夹角
angle = 90 - theta * (180 / np.pi)
# print(rho, theta, angle)
if abs(angle) < max_angle: # 水平直线
angles.append(angle)
if vis:
# 该直线与第一列的交点
pt1 = (0, int(rho / np.sin(theta)))
# 该直线与最后一列的交点
pt2 = (edge.shape[1], int((rho - edge.shape[1] * np.cos(theta)) / np.sin(theta)))
# 绘制一条直线
cv2.line(img, pt1, pt2, (0, 0, 255), thickness=thickness)
else: # 垂直直线
if vis:
# (theta < (np.pi / 4.)) or (theta > (3. * np.pi / 4.0)) 垂直直线(<45°,>135°)
# 该直线与第一行的交点
pt1 = (int(rho / np.cos(theta)), 0)
# 该直线与最后一行的焦点
pt2 = (int((rho - edge.shape[0] * np.sin(theta)) / np.cos(theta)), edge.shape[0])
# 绘制一条白线
cv2.line(img, pt1, pt2, (255, 0, 0), thickness=thickness)
angle = 0 if len(angles) < 1 else ImageCorrection.get_lines_mean_angle(angles)
return angle, img
@staticmethod
def get_hough_lines_p(img: np.ndarray, rho=1, theta=np.pi / 180, threshold=100, max_angle=45,
max_lines=200, minLineLength=100, maxLineGap=10, thickness=2, vis=False):
"""
https://blog.csdn.net/on2way/article/details/47028969
:param img: 输入图像
:param rho: 线段以像素为单位的距离精度,double类型的,推荐用1.0
:param theta: 线段以弧度为单位的角度精度,推荐用numpy.pi/180
:param threshold: : 累加平面的阈值参数,int类型,超过设定阈值才被检测出线段,
值越大,意味着检出的线段越长,检出的线段个数越少。根据情况推荐先用100试试
:param minLineLength 用来控制「接受直线的最小长度」的值,默认值为 0。
:param maxLineGap 用来控制接受共线线段之间的最小间隔,即在一条线中两点的最大间隔。
:return:
"""
if len(img.shape) == 3:
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 灰度图像
else:
gray = img.copy()
gray = image_utils.get_image_mask(gray, inv=True)
edge = cv2.Canny(gray, threshold1=0, threshold2=255, apertureSize=3)
lines = cv2.HoughLinesP(edge, rho=rho, theta=theta,
threshold=threshold,
minLineLength=minLineLength,
maxLineGap=maxLineGap)
lines = [] if lines is None else lines[:, 0, :]
lines = lines[0:min(len(lines), max_lines)]
angles = []
for x1, y1, x2, y2 in lines[:]:
pt1, pt2 = (x1, y1), (x2, y2) # P12 = point2-point1
angle = geometry_tools.compute_horizontal_angle(pt1, pt2, minangle=False)
# print(pt1, pt2, angle)
if abs(angle) < max_angle: # 水平直线
angles.append(angle)
if vis: cv2.line(img, pt1, pt2, color=(0, 0, 255), thickness=thickness)
else: # 垂直直线
if vis: cv2.line(img, pt1, pt2, color=(255, 0, 0), thickness=thickness)
angle = 0 if len(angles) < 1 else ImageCorrection.get_lines_mean_angle(angles)
return angle, img
@staticmethod
def get_lines_mean_angle(angles):
"""求直线簇的平均角度"""
angles = sorted(angles)
r = len(angles) // 2
ar = (r - r // 2, r + r // 2 + 1)
angles = angles[ar[0]:ar[1]]
angle = np.mean(angles)
return angle
@staticmethod
def rotation(image, angle):
"""实现图像旋转"""
h, w = image.shape[:2]
center = (w / 2., h / 2.)
mat = cv2.getRotationMatrix2D(center, angle, 1.0)
image = cv2.warpAffine(image, mat, dsize=(w, h), borderMode=cv2.BORDER_CONSTANT, borderValue=(127, 127, 127))
return image
@staticmethod
def correct(image, max_angle=35, vis=False):
"""
图像矫正
:param image: 输入RGB或BGR图像
:param max_angle: 图像最大的倾斜角度,超过该角度的无法矫正,默认不超过35°
:param vis: 是否可视化图像矫正结果
:return: image返回矫正后的图像
:return: angle返回原始图像倾斜的角度
"""
# angle, image_line = ImageCorrection.get_hough_lines(image, max_angle=max_angle,vis=vis)
angle, image_line = ImageCorrection.get_hough_lines_p(image, max_angle=max_angle, vis=vis)
image = ImageCorrection.rotation(image, angle=-angle) # 9ms
if vis:
print(angle)
image_line = image_utils.resize_image(image_line, size=(None, image.shape[0]))
image_line = np.hstack((image_line, image))
image_utils.cv_show_image("Origin-Alignment", image_line, delay=0, use_rgb=False)
return image, angle
def image_correction_demo(image_dir):
"""
:param image_dir:
:return:
"""
image_list = file_utils.get_files_lists(image_dir)
alignment = ImageCorrection()
for image_file in image_list:
print(image_file)
image = cv2.imread(image_file)
image, angle = alignment.correct(image, vis=True)
print("倾斜角度:{}".format(angle))
print("--" * 10)
if __name__ == "__main__":
image_dir = "data/image1" # 测试图片
image_correction_demo(image_dir)
该方法,矫正效果比较差,容易受背景图案的影响,特别是当背景比较复杂时,矫正效果一塌糊涂。倾斜角度大于45°时,矫正方向容易反向;并且无法进行立体矫正:
如果,我们可以获得文档的四个角点位置P0(可以通过用户交互标记,也可以算法自动获得,即一键文档矫正),并且已知并希望其矫正后的角点位置P1(文档是矩形的,矫正后的角点位置可以大致估计),利用P1和P0四个角点的对应关系,我们可以估计其变换矩阵H,再进行透视变换,其矫正效果会比直接使用霍夫变换的文档矫正方法要好很多。
基于透视变换的文档矫正方法的实现过程如下:
① 如果选择用户交互获得,则use_mouse=True,这时需要使用鼠标标记图像中文档的四个角点的位置;该方法,不受背景图案影响,矫正精度取决于人工标记文档角点的精度
② 如果选择算法自动计算(文档一键矫正),则use_mouse=False,这时算法会通过图像处理自动获得文档角点的位置;该方法需要通过图像处理自动获得文档的四个角点,受背景图案影响较大。
由于实际文档是矩形,其长宽比是固定的;基于这一先验信息,我们可以利用原始图像的四个角点位置P0,大致估计其矫正后四个角点的位置P1。
基于透视变换的文档矫正方法的关键代码如下:
def document_correct_by_mouse(image, winname="document_correct_by_mouse"):
"""
通过鼠标操作获得文档的四个角点
:param image: 输入图像
:param winname: 窗口名称
:return:
"""
corners = np.zeros(shape=(0, 2), dtype=np.int32)
mouse = mouse_utils.DrawImageMouse(max_point=4, thickness=5)
image_utils.cv_show_image("correct-result", np.zeros_like(image) + 128, use_rgb=False, delay=1)
while len(corners) < 4:
corners = mouse.draw_image_polygon_on_mouse(image, winname=winname)
corners = np.asarray(corners)
if len(corners) < 4:
mouse.clear()
print("已经标记了文档的{}个角点,需要标记4个角点".format(len(corners)))
cv2.waitKey(0)
print("标记文档的4个角点={}".format(corners.tolist()))
return corners
def document_correct_by_auto(image, winname="document_correct_by_auto", vis=False):
"""
通过算法自动获得文档的四个角点
:param image: 输入图像
:param winname: 窗口名称
:param vis: 是否可视化
:return:
"""
corners = corner_utils.get_document_corners(image)
if vis:
image = image_utils.draw_image_points_lines(image, corners, fontScale=2.0, thickness=5)
image_utils.cv_show_image(winname, image, use_rgb=False)
return corners
def document_correct_image_example(image, use_mouse=False, winname="document", vis=True):
"""
通过算法自动获得文档的四个角点
:param image: 输入图像
:param use_mouse: True:通过鼠标操作获得文档的四个角点
False:通过算法自动获得文档的四个角点
:param winname: 窗口名称
:param vis: 可视化效果
:return:
"""
# 获得文档的四个角点
if use_mouse:
corners = document_correct_by_mouse(image, winname=winname) # 通过鼠标操作获得文档的四个角点;
else:
corners = document_correct_by_auto(image) # 通过算法自动获得文档的四个角点
# 在原图显示角点
image = image_utils.draw_image_points_lines(image, corners, circle_color=(0, 255, 0), fontScale=2.0, thickness=5)
image_utils.cv_show_image(winname, image, use_rgb=False, delay=10)
# 实现文档矫正
document_image_correct(image, corners, vis=vis)
if __name__ == '__main__':
image_dir = "data/image1" # 测试图片
use_mouse = True # 是否通过鼠标操作获得文档的四个角点
image_list = file_utils.get_files_lists(image_dir)
for image_file in image_list:
print(image_file)
image = cv2.imread(image_file)
document_correct_image_example(image, use_mouse=use_mouse)
cv2.waitKey(0)
整体而言,基于透视变换的文档矫正方法会比基于霍夫变换的文档矫正方法的矫正效果要好很多
下面是Demo矫正效果
① 如果选择用户交互获得,则use_mouse=True,这时需要使用鼠标标记图像中文档的四个角点的位置;该方法,不受背景图案影响,矫正精度取决于人工标记文档角点的精度
② 如果选择算法自动计算(文档一键矫正),则use_mouse=False,这时算法会通过图像处理自动获得文档角点的位置;该方法需要通过图像处理自动获得文档的四个角点,受背景图案影响较大。
通过用户交互实现文档矫正 | 完全自动文档矫正(文档一键矫正) |
目前,正计划实现Android版本的文档矫正Demo,如果你有这方面的技术需求,可以微信公众号联系哦
整套项目源码下载:OpenCV实现文档自动矫正
- 基于霍夫变换的文档矫正方法:demo_correction_v1.py
- 基于透视变换的文档矫正方法: demo_correction_v2.py,设置use_mouse=True通过用户交互进行文档矫正;设置use_mouse=False,通过算法自动计算,实现文档一键矫正
- 相关测试数据: 数据放在项目data目录下