Python+OpenCV实现文档扫描与旋转校正
Python+OpenCV实现文档扫描与旋转校正
你是不是也遇到过这样的烦恼?用手机拍了一张文档照片,结果拍歪了,或者因为角度问题,文字看起来是倾斜的。这时候如果直接打印或者上传,效果肯定不好。手动调整吧,又麻烦又费时间。
其实,这种问题完全可以用代码来解决。今天我就来分享一个用Python和OpenCV实现的文档扫描与旋转校正方案。这个方案能自动检测文档边缘,进行透视变换,还能校正旋转角度,最后还能增强图像质量。整个过程就像给文档拍了个“证件照”,让它变得规规矩矩。
1. 环境准备与快速部署
1.1 安装必要的库
首先,确保你的Python环境已经安装好。我建议使用Python 3.7或更高版本。然后,通过pip安装我们需要的库:
pip install opencv-python
pip install numpy
pip install scikit-image
如果你用的是Anaconda,也可以用conda安装:
conda install -c conda-forge opencv
conda install numpy scikit-image
1.2 验证安装
安装完成后,可以写个简单的脚本来验证一下:
import cv2
import numpy as np
from skimage import io
print(f"OpenCV版本: {cv2.__version__}")
print(f"NumPy版本: {np.__version__}")
如果运行没有报错,说明环境已经准备好了。
2. 核心功能实现
2.1 边缘检测与轮廓查找
文档扫描的第一步是找到文档的边界。我们通常用边缘检测算法来找出文档的轮廓。
def find_document_contour(image_path):
# 读取图像
image = cv2.imread(image_path)
if image is None:
print(f"无法读取图像: {image_path}")
return None
# 保存原始图像副本
original = image.copy()
# 转换为灰度图
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 高斯模糊,减少噪声
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
# Canny边缘检测
edged = cv2.Canny(blurred, 50, 150)
# 查找轮廓
contours, _ = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 按面积排序,取最大的几个轮廓
contours = sorted(contours, key=cv2.contourArea, reverse=True)[:5]
# 寻找近似矩形的轮廓
document_contour = None
for contour in contours:
# 计算轮廓周长
perimeter = cv2.arcLength(contour, True)
# 多边形近似
approx = cv2.approxPolyDP(contour, 0.02 * perimeter, True)
# 如果是四边形(文档通常是矩形)
if len(approx) == 4:
document_contour = approx
break
return original, document_contour
2.2 透视变换
找到文档轮廓后,我们需要进行透视变换,把倾斜的文档"拉正"。
def perspective_transform(image, contour):
# 获取轮廓的四个顶点
points = contour.reshape(4, 2)
# 计算四个顶点的中心点
center = np.mean(points, axis=0)
# 根据中心点将顶点分为左上、右上、右下、左下
diff = points - center
angles = np.arctan2(diff[:, 1], diff[:, 0])
# 按角度排序
sorted_indices = np.argsort(angles)
points = points[sorted_indices]
# 确保顺序是:左上、右上、右下、左下
# 调整顺序,使第一个点是左上角
if points[0][0] > points[1][0]:
points = np.roll(points, -1, axis=0)
# 定义目标矩形的尺寸
# 计算原始文档的宽度和高度
width_top = np.linalg.norm(points[1] - points[0])
width_bottom = np.linalg.norm(points[2] - points[3])
max_width = max(int(width_top), int(width_bottom))
height_left = np.linalg.norm(points[3] - points[0])
height_right = np.linalg.norm(points[2] - points[1])
max_height = max(int(height_left), int(height_right))
# 目标矩形的四个顶点
dst_points = np.array([
[0, 0],
[max_width - 1, 0],
[max_width - 1, max_height - 1],
[0, max_height - 1]
], dtype="float32")
# 计算透视变换矩阵
matrix = cv2.getPerspectiveTransform(points.astype("float32"), dst_points)
# 应用透视变换
warped = cv2.warpPerspective(image, matrix, (max_width, max_height))
return warped
2.3 旋转角度检测与校正
有时候文档本身是正的,但是拍照时旋转了。这时候我们需要检测旋转角度并进行校正。
def detect_rotation_angle(image):
"""
检测图像的旋转角度
返回:旋转角度(度)
"""
# 转换为灰度图
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 二值化
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
# 查找轮廓
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not contours:
return 0
# 找到最大的轮廓(假设是文档主体)
largest_contour = max(contours, key=cv2.contourArea)
# 计算最小外接矩形
rect = cv2.minAreaRect(largest_contour)
angle = rect[2]
# 调整角度范围到[-45, 45]
if angle < -45:
angle = 90 + angle
elif angle > 45:
angle = angle - 90
return angle
def rotate_image(image, angle):
"""
旋转图像
"""
if abs(angle) < 1: # 角度太小,不旋转
return image
# 获取图像尺寸
(h, w) = image.shape[:2]
center = (w // 2, h // 2)
# 计算旋转矩阵
M = cv2.getRotationMatrix2D(center, angle, 1.0)
# 计算旋转后的图像尺寸
cos = np.abs(M[0, 0])
sin = np.abs(M[0, 1])
new_w = int((h * sin) + (w * cos))
new_h = int((h * cos) + (w * sin))
# 调整旋转矩阵的平移分量
M[0, 2] += (new_w / 2) - center[0]
M[1, 2] += (new_h / 2) - center[1]
# 执行旋转
rotated = cv2.warpAffine(image, M, (new_w, new_h),
flags=cv2.INTER_CUBIC,
borderMode=cv2.BORDER_REPLICATE)
return rotated
2.4 图像增强
校正后的文档可能对比度不够或者有噪点,我们可以进行一些增强处理。
def enhance_document(image):
"""
增强文档图像质量
"""
# 转换为灰度图
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 自适应阈值二值化
binary = cv2.adaptiveThreshold(gray, 255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 11, 2)
# 去噪
denoised = cv2.medianBlur(binary, 3)
# 形态学操作,去除小噪点
kernel = np.ones((2, 2), np.uint8)
cleaned = cv2.morphologyEx(denoised, cv2.MORPH_CLOSE, kernel)
# 转换为BGR格式返回
enhanced = cv2.cvtColor(cleaned, cv2.COLOR_GRAY2BGR)
return enhanced
3. 完整流程示例
现在我们把所有功能组合起来,实现一个完整的文档扫描流程。
def scan_document(image_path, output_path=None):
"""
完整的文档扫描流程
"""
print(f"处理图像: {image_path}")
# 1. 查找文档轮廓
result = find_document_contour(image_path)
if result is None:
print("未找到文档轮廓")
return None
original, contour = result
if contour is None:
print("未找到合适的文档轮廓,尝试直接旋转校正...")
# 如果没有找到轮廓,直接处理原图
processed = original.copy()
else:
# 2. 透视变换
print("进行透视变换...")
warped = perspective_transform(original, contour)
processed = warped
# 3. 检测旋转角度
print("检测旋转角度...")
angle = detect_rotation_angle(processed)
print(f"检测到旋转角度: {angle:.2f}度")
# 4. 旋转校正
if abs(angle) > 1:
print("进行旋转校正...")
processed = rotate_image(processed, angle)
# 5. 图像增强
print("进行图像增强...")
enhanced = enhance_document(processed)
# 6. 保存结果
if output_path:
cv2.imwrite(output_path, enhanced)
print(f"结果已保存到: {output_path}")
return enhanced
# 使用示例
if __name__ == "__main__":
# 输入图像路径
input_image = "document_photo.jpg"
# 输出图像路径
output_image = "scanned_document.jpg"
# 执行文档扫描
result = scan_document(input_image, output_image)
if result is not None:
# 显示结果
cv2.imshow("原始图像", cv2.imread(input_image))
cv2.imshow("扫描结果", result)
cv2.waitKey(0)
cv2.destroyAllWindows()
4. 实用技巧与进阶功能
4.1 批量处理多个文档
如果你有很多文档需要处理,可以写一个批量处理的函数:
import os
from glob import glob
def batch_scan_documents(input_folder, output_folder):
"""
批量处理文件夹中的所有文档图像
"""
# 创建输出文件夹
os.makedirs(output_folder, exist_ok=True)
# 查找所有图像文件
image_extensions = ['*.jpg', '*.jpeg', '*.png', '*.bmp']
image_files = []
for ext in image_extensions:
image_files.extend(glob(os.path.join(input_folder, ext)))
print(f"找到 {len(image_files)} 个图像文件")
# 批量处理
for i, image_path in enumerate(image_files, 1):
print(f"\n处理第 {i}/{len(image_files)} 个文件: {os.path.basename(image_path)}")
# 生成输出路径
filename = os.path.basename(image_path)
name, ext = os.path.splitext(filename)
output_path = os.path.join(output_folder, f"{name}_scanned{ext}")
try:
# 执行文档扫描
result = scan_document(image_path, output_path)
if result is not None:
print(f"✓ 处理成功")
else:
print(f"✗ 处理失败")
except Exception as e:
print(f"✗ 处理出错: {str(e)}")
print(f"\n批量处理完成!结果保存在: {output_folder}")
# 使用示例
batch_scan_documents("input_documents", "output_documents")
4.2 调整参数优化效果
不同的文档可能需要不同的参数。你可以根据实际情况调整这些参数:
def scan_document_with_params(image_path, params=None):
"""
带参数调整的文档扫描
"""
# 默认参数
default_params = {
'canny_low': 50, # Canny边缘检测低阈值
'canny_high': 150, # Canny边缘检测高阈值
'blur_size': 5, # 高斯模糊核大小
'approx_epsilon': 0.02, # 多边形近似精度
'min_angle': 1, # 最小旋转角度(小于此值不旋转)
'adaptive_block': 11, # 自适应阈值块大小
'adaptive_c': 2 # 自适应阈值常数
}
# 合并用户参数
if params:
default_params.update(params)
params = default_params
# 这里可以修改之前的函数,使用这些参数
# 例如修改find_document_contour函数:
# edged = cv2.Canny(blurred, params['canny_low'], params['canny_high'])
# ...
# 具体实现略,原理相同
4.3 添加GUI界面
如果你想让非技术人员也能使用这个工具,可以添加一个简单的GUI界面:
import tkinter as tk
from tkinter import filedialog, messagebox
from PIL import Image, ImageTk
import cv2
class DocumentScannerApp:
def __init__(self, root):
self.root = root
self.root.title("文档扫描工具")
self.root.geometry("800x600")
# 创建界面元素
self.create_widgets()
# 当前图像
self.current_image = None
self.processed_image = None
def create_widgets(self):
# 菜单栏
menubar = tk.Menu(self.root)
self.root.config(menu=menubar)
file_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="文件", menu=file_menu)
file_menu.add_command(label="打开图像", command=self.open_image)
file_menu.add_command(label="保存结果", command=self.save_image)
file_menu.add_separator()
file_menu.add_command(label="退出", command=self.root.quit)
# 处理菜单
process_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="处理", menu=process_menu)
process_menu.add_command(label="扫描文档", command=self.scan_document)
process_menu.add_command(label="旋转校正", command=self.rotate_document)
# 图像显示区域
self.image_label = tk.Label(self.root, text="请打开一个图像文件", bg="lightgray")
self.image_label.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 状态栏
self.status_var = tk.StringVar()
self.status_var.set("就绪")
status_bar = tk.Label(self.root, textvariable=self.status_var,
bd=1, relief=tk.SUNKEN, anchor=tk.W)
status_bar.pack(side=tk.BOTTOM, fill=tk.X)
def open_image(self):
file_path = filedialog.askopenfilename(
title="选择图像文件",
filetypes=[("图像文件", "*.jpg *.jpeg *.png *.bmp")]
)
if file_path:
try:
# 读取图像
self.current_image = cv2.imread(file_path)
if self.current_image is not None:
self.display_image(self.current_image)
self.status_var.set(f"已打开: {file_path}")
else:
messagebox.showerror("错误", "无法读取图像文件")
except Exception as e:
messagebox.showerror("错误", f"打开图像失败: {str(e)}")
def scan_document(self):
if self.current_image is None:
messagebox.showwarning("警告", "请先打开一个图像文件")
return
self.status_var.set("正在处理...")
self.root.update()
try:
# 这里调用之前实现的文档扫描函数
# 为了简化示例,这里只是演示流程
# 实际使用时应该调用完整的scan_document函数
# 模拟处理过程
import time
time.sleep(1) # 模拟处理时间
# 显示处理结果
self.display_image(self.current_image) # 这里应该显示处理后的图像
self.status_var.set("处理完成")
except Exception as e:
messagebox.showerror("错误", f"处理失败: {str(e)}")
self.status_var.set("处理失败")
def display_image(self, image):
# 调整图像大小以适应窗口
h, w = image.shape[:2]
max_size = 600
if h > max_size or w > max_size:
scale = max_size / max(h, w)
new_h, new_w = int(h * scale), int(w * scale)
image = cv2.resize(image, (new_w, new_h))
# 转换颜色空间
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# 转换为PIL图像
pil_image = Image.fromarray(image_rgb)
# 转换为Tkinter图像
tk_image = ImageTk.PhotoImage(pil_image)
# 更新显示
self.image_label.config(image=tk_image, text="")
self.image_label.image = tk_image # 保持引用
def save_image(self):
if self.processed_image is None:
messagebox.showwarning("警告", "没有可保存的图像")
return
file_path = filedialog.asksaveasfilename(
title="保存图像",
defaultextension=".jpg",
filetypes=[("JPEG图像", "*.jpg"), ("PNG图像", "*.png")]
)
if file_path:
try:
cv2.imwrite(file_path, self.processed_image)
messagebox.showinfo("成功", f"图像已保存到: {file_path}")
self.status_var.set(f"已保存: {file_path}")
except Exception as e:
messagebox.showerror("错误", f"保存失败: {str(e)}")
# 启动应用
if __name__ == "__main__":
root = tk.Tk()
app = DocumentScannerApp(root)
root.mainloop()
5. 常见问题与解决方案
5.1 找不到文档轮廓怎么办?
如果文档背景复杂或者光照不均匀,可能找不到正确的轮廓。可以尝试:
- 调整Canny边缘检测参数:降低阈值可以检测到更多边缘,但可能会有更多噪声。
- 增加高斯模糊:使用更大的模糊核可以减少噪声。
- 预处理图像:先进行直方图均衡化或对比度拉伸。
def preprocess_image(image):
"""图像预处理"""
# 转换为灰度图
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 直方图均衡化
equalized = cv2.equalizeHist(gray)
# 对比度拉伸
min_val = np.min(equalized)
max_val = np.max(equalized)
stretched = ((equalized - min_val) / (max_val - min_val) * 255).astype(np.uint8)
return stretched
5.2 透视变换后图像变形怎么办?
如果透视变换后的图像看起来变形,可能是顶点顺序错了。可以添加额外的验证:
def validate_perspective_points(points):
"""
验证透视变换的四个点是否合理
"""
# 计算四边形的面积
area = cv2.contourArea(points)
# 计算长宽比
width_top = np.linalg.norm(points[1] - points[0])
width_bottom = np.linalg.norm(points[2] - points[3])
height_left = np.linalg.norm(points[3] - points[0])
height_right = np.linalg.norm(points[2] - points[1])
# 检查长宽比是否合理(文档通常不是特别长或特别宽)
aspect_ratio = max(width_top, width_bottom) / max(height_left, height_right)
# 合理的文档长宽比通常在0.5到2之间
if aspect_ratio < 0.5 or aspect_ratio > 2:
return False
# 检查面积是否足够大
if area < 10000: # 假设最小面积
return False
return True
5.3 旋转角度检测不准确怎么办?
对于某些特殊文档,旋转角度检测可能不准确。可以尝试:
- 使用霍夫变换检测直线:通过检测文档中的文本行方向来确定旋转角度。
- 结合多种方法:使用最小外接矩形和霍夫变换的结果进行加权平均。
- 人工校正:在GUI中添加手动旋转功能。
def detect_angle_with_hough(image):
"""
使用霍夫变换检测旋转角度
"""
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 边缘检测
edges = cv2.Canny(gray, 50, 150, apertureSize=3)
# 霍夫变换检测直线
lines = cv2.HoughLines(edges, 1, np.pi/180, 100)
if lines is None:
return 0
angles = []
for line in lines[:20]: # 取前20条直线
rho, theta = line[0]
# 只考虑接近水平或垂直的直线
if theta < np.pi/4 or theta > 3*np.pi/4:
angle = np.degrees(theta - np.pi/2)
else:
angle = np.degrees(theta)
# 调整角度到[-45, 45]
if angle > 45:
angle = angle - 90
elif angle < -45:
angle = 90 + angle
angles.append(angle)
# 计算平均角度
if angles:
return np.median(angles)
else:
return 0
6. 总结
通过这篇文章,我们实现了一个完整的文档扫描与旋转校正系统。从边缘检测、透视变换到旋转校正和图像增强,每个步骤都有详细的代码实现和解释。
实际使用中,你可能需要根据具体的文档类型和拍摄条件调整参数。比如,对于白底黑字的文档,二值化效果会很好;但对于彩色背景或者有图片的文档,可能需要更复杂的处理方法。
这个方案的优势在于完全开源、可定制,而且处理速度很快。你可以把它集成到自己的项目中,或者根据需求进行扩展,比如添加OCR文字识别功能,实现从图像到可编辑文本的完整流程。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
更多推荐



所有评论(0)