基于OpenCV的实时文档扫描与矫正技术

文章目录

    • 引言
    • 一、系统概述
    • 二、核心代码解析
      • 1. 导入必要库
      • 2. 辅助函数定义
      • 3. 坐标点排序函数
      • 4. 透视变换函数
      • 5. 主程序流程
    • 三、完整代码
    • 四、结语

引言

在日常工作和学习中,我们经常需要将纸质文档数字化。手动拍摄文档照片常常会出现角度倾斜、透视变形等问题,影响后续使用。本文将介绍如何使用Python和OpenCV构建一个实时文档扫描与矫正系统,能够通过摄像头自动检测文档边缘并进行透视变换矫正。

一、系统概述

该系统主要实现以下功能:

  1. 实时摄像头捕获图像
  2. 边缘检测和轮廓查找
  3. 文档轮廓识别
  4. 透视变换矫正文档
  5. 二值化处理增强可读性

二、核心代码解析

1. 导入必要库

import numpy as np
import cv2

我们主要使用NumPy进行数值计算,OpenCV进行图像处理。

2. 辅助函数定义

首先定义了一个简单的图像显示函数,方便调试:

def cv_show(name,img):cv2.imshow(name,img)cv2.waitKey(10)

3. 坐标点排序函数

order_points函数用于将检测到的文档四个角点按顺序排列(左上、右上、右下、左下):

def order_points(pts):rect = np.zeros((4,2),dtype="float32")s = pts.sum(axis=1)rect[0] = pts[np.argmin(s)]  # 左上点(x+y最小)rect[2] = pts[np.argmax(s)]  # 右下点(x+y最大)diff = np.diff(pts,axis=1)rect[1] = pts[np.argmin(diff)]  # 右上点(y-x最小)rect[3] = pts[np.argmax(diff)]  # 左下点(y-x最大)return rect

这个函数的作用是对给定的4个二维坐标点进行排序,使其按照左上、右上、右下、左下的顺序排列。这在文档扫描、图像矫正等应用中非常重要,因为我们需要知道每个角点的确切位置才能正确地进行透视变换。

函数详细解析

(1)排序逻辑说明

  1. 左上点(rect[0]):选择x+y值最小的点

    • 因为左上角在坐标系中 x 和 y 值都较小,相加结果最小
  2. 右下点(rect[2]):选择x+y值最大的点

    • 因为右下角在坐标系中 x 和 y 值都较大,相加结果最大
  3. 右上点(rect[1]):选择y-x值最小的点

    • 右上角的特点是 y 相对较小而 x 相对较大,所以 y-x 值最小
  4. 左下点(rect[3]):选择y-x值最大的点

    • 左下角的特点是 y 相对较大而 x 相对较小,所以 y-x 值最大

(2)示例

假设有4个点:

	A(10, 20)  # 假设是左上B(50, 20)  # 右上C(50, 60)  # 右下D(10, 60)  # 左下

计算过程:

  1. x+y值:[30, 70, 110, 70]

    • 最小30 → A(左上)
    • 最大110 → C(右下)
  2. y-x值:[10, -30, 10, 50]

    • 最小-30 → B(右上)
    • 最大50 → D(左下)

最终排序结果:[A, B, C, D] 即 [左上, 右上, 右下, 左下]

(3)为什么这种方法有效

这种方法利用了二维坐标点的几何特性:

  • 在标准坐标系中,左上角的x和y值都较小
  • 右下角的x和y值都较大
  • 右上角的x较大而y较小
  • 左下角的x较小而y较大

通过简单的加减运算就能可靠地区分出各个角点,不需要复杂的几何计算。

4. 透视变换函数

four_point_transform函数实现了文档矫正的核心功能:

def four_point_transform(image,pts):rect = order_points(pts)(tl,tr,br,bl) = rect# 计算变换后的宽度和高度widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))maxWidth = max(int(widthA),int(widthB))heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))maxHeight = max(int(heightA),int(heightB))# 定义目标图像坐标dst = np.array([[0,0],[maxWidth - 1,0],[maxWidth - 1,maxHeight - 1],[0,maxHeight - 1]],dtype="float32")# 计算透视变换矩阵并应用M = cv2.getPerspectiveTransform(rect,dst)warped = cv2.warpPerspective(image,M,(maxWidth,maxHeight))return warped

这个函数实现了透视变换(Perspective Transformation),用于将图像中的任意四边形区域矫正为一个矩形(即"去透视"效果)。

函数详细解析

  1. 输入参数
def four_point_transform(image, pts):
  • image: 原始图像
  • pts: 包含4个点的数组,表示要转换的四边形区域
  1. 坐标点排序
rect = order_points(pts)
(tl, tr, br, bl) = rect  # 分解为左上(top-left)、右上(top-right)、右下(bottom-right)、左下(bottom-left)

使用之前介绍的order_points函数将4个点按顺序排列

  1. 计算输出图像的宽度
widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))  # 底边长度
widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))  # 顶边长度
maxWidth = max(int(widthA), int(widthB))  # 取最大值作为输出图像宽度

计算四边形底部和顶部的边长,选择较长的作为输出宽度

  1. 计算输出图像的高度
heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))  # 右边高度
heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))  # 左边高度
maxHeight = max(int(heightA), int(heightB))  # 取最大值作为输出图像高度

计算四边形右侧和左侧的边长,选择较长的作为输出高度

  1. 定义目标矩形坐标
dst = np.array([[0, 0],  # 左上[maxWidth - 1, 0],  # 右上[maxWidth - 1, maxHeight - 1],  # 右下[0, maxHeight - 1]  # 左下
], dtype="float32")

定义变换后的矩形角点坐标(从(0,0)开始的正矩形)

  1. 计算透视变换矩阵并应用
M = cv2.getPerspectiveTransform(rect, dst)  # 计算变换矩阵
warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))  # 应用变换
  • getPerspectiveTransform: 计算从原始四边形到目标矩形的3x3变换矩阵
  • warpPerspective: 应用这个变换矩阵到原始图像
  1. 返回结果
return warped

返回矫正后的矩形图像

  1. 透视变换原理图示
原始图像中的四边形               变换后的矩形tl--------tr                    0--------maxWidth\        /                      |        |\      /                       |        |bl----br                       maxHeight
  1. 为什么需要这样计算宽度和高度?

取最大值的原因

  • 原始四边形可能有透视变形,两条对边长度可能不等
  • 选择较大的值可以确保所有内容都能包含在输出图像中

减1的原因

  • 图像坐标从0开始,所以宽度为maxWidth的图像,最大x坐标是maxWidth-1

5. 主程序流程

主程序实现了实时文档检测和矫正的完整流程:

  1. 初始化摄像头
cap = cv2.VideoCapture(0)
if not cap.isOpened():print("Cannot open camera")exit()
  1. 实时处理循环
while True:flag = 0ret,image = cap.read()orig = image.copy()if not ret:print("不能读取摄像头")break
  1. 图像预处理
gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray,(5,5),0)  # 高斯滤波降噪
edged = cv2.Canny(gray,75,200)  # Canny边缘检测
  1. 轮廓检测与筛选
cnts = cv2.findContours(edged,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[-2]
cnts = sorted(cnts,key=cv2.contourArea,reverse=True)[:3]  # 取面积最大的3个轮廓for c in cnts:peri = cv2.arcLength(c,True)  # 计算轮廓周长approx = cv2.approxPolyDP(c,0.05 * peri,True)  # 多边形近似area = cv2.contourArea(approx)# 筛选四边形且面积足够大的轮廓if area > 20000 and len(approx) == 4:screenCnt = approxflag = 1break
  1. 文档矫正与显示
if flag == 1:# 绘制轮廓image_contours = cv2.drawContours(image,[screenCnt],0,(0,255,0),2)# 透视变换warped = four_point_transform(orig,screenCnt.reshape(4,2))# 二值化处理warped = cv2.cvtColor(warped,cv2.COLOR_BGR2GRAY)ref = cv2.threshold(warped,0,255,cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]

三、完整代码

# 导入工具包
import numpy as np
import cv2def cv_show(name,img):cv2.imshow(name,img)cv2.waitKey(10)
def order_points(pts):# 一共4个坐标点rect = np.zeros((4,2),dtype="float32") # 用来存储排序之后的坐标位置# 按顺序找到对应坐标0123分别是 左上、右上、右下、左下s = pts.sum(axis=1) #对pts矩阵的每一行进行求和操作,(x+y)rect[0] = pts[np.argmin(s)]rect[2] = pts[np.argmax(s)]diff = np.diff(pts,axis=1) #对pts矩阵的每一行进行求差操作,(y-x)rect[1] = pts[np.argmin(diff)]rect[3] = pts[np.argmax(diff)]return rectdef four_point_transform(image,pts):# 获取输入坐标点rect = order_points(pts)(tl,tr,br,bl) = rect# 计算输入的w和h值widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))maxWidth = max(int(widthA),int(widthB))heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))maxHeight = max(int(heightA),int(heightB))# 变换后对应坐标位置dst = np.array([[0,0],[maxWidth - 1,0],[maxWidth - 1,maxHeight - 1],[0,maxHeight - 1]],dtype="float32")M = cv2.getPerspectiveTransform(rect,dst)warped = cv2.warpPerspective(image,M,(maxWidth,maxHeight))# 返回变换后的结果return warped# 读取输入
import cv2
cap = cv2.VideoCapture(0)  # 确保摄像头是可以启动的状态
if not cap.isOpened():   #打开失败print("Cannot open camera")exit()while True:flag = 0 # 用于标时 当前是否检测到文档ret,image = cap.read()  # 如果正确读取帧,ret为Trueorig = image.copy()if not ret: #读取失败,则退出循环print("不能读取摄像头")breakcv_show("image",image)gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)# 预处理gray = cv2.GaussianBlur(gray,(5,5),0) # 高斯滤波edged = cv2.Canny(gray,75,200)cv_show('1',edged)# 轮廓检测cnts = cv2.findContours(edged,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[-2]cnts = sorted(cnts,key=cv2.contourArea,reverse=True)[:3]image_contours = cv2.drawContours(image,cnts,-1,(0,255,0),2)cv_show("image_contours",image_contours)# 遍历轮廓for c in cnts:# 计算轮廓近似peri = cv2.arcLength(c,True) # 计算轮廓的周长# C 表示输入的点集# epsilon表示从原始轮廓到近似轮廓的最大距离,它是一个准确度参数# True表示封闭的approx = cv2.approxPolyDP(c,0.05 * peri,True) # 轮廓近似area = cv2.contourArea(approx)# 4个点的时候就拿出来if area > 20000 and len(approx) == 4:screenCnt = approxflag = 1print(peri,area)print("检测到文档")breakif flag == 1:# 展示结果# print("STEP 2: 获取轮廓")image_contours = cv2.drawContours(image,[screenCnt],0,(0,255,0),2)cv_show("image",image_contours)# 透视变换warped = four_point_transform(orig,screenCnt.reshape(4,2))cv_show("warped",warped)# 二值处理warped = cv2.cvtColor(warped,cv2.COLOR_BGR2GRAY)# ref = cv2.threshold(warped,220,255,cv2.THRESH_BINARY)[1]ref = cv2.threshold(warped,0,255,cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]cv_show("ref",ref)
cap.release() # 释放捕捉器
cv2.destroyAllWindows() #关闭图像窗口

四、结语

本文介绍了一个基于OpenCV的实时文档扫描与矫正系统,通过边缘检测、轮廓分析和透视变换等技术,实现了文档的自动检测和矫正。该系统可以方便地应用于日常文档数字化工作,提高工作效率。

完整代码已在上文中给出,读者可以根据自己的需求进行修改和扩展。OpenCV提供了强大的图像处理能力,结合Python的简洁语法,使得开发这样的实用系统变得简单高效。

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

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

相关文章

jenkins pipeline实现CI/CD

在企业级的架构中,CI/CD是必不可少的一个环节,它可以让开发人员只关注于开发,而不必去关注项目的构建和部署,从而提高开发人员的效率。 本文我们来介绍一下使用jenkins 的pipeline来进行java项目的自动构建以及部署。我们通过脚本…

InfluxDB 3 Core + Java 11 + Spring Boot:打造高效物联网数据平台

一、 引言:为什么选择InfluxDB 3? 项目背景: 在我们的隧道风机监控系统中,实时数据的采集、存储和高效查询是至关重要的核心需求。风机运行产生的振动、倾角、电流、温度等参数是典型的时序数据,具有高并发写入、数据…

泰国SAP ERP实施如何应对挑战?工博科技赋能中企出海EEC战略

泰国正依托"东部经济走廊(EEC)"与RCEP协定叠加优势,为中国企业提供面向亚太市场的战略机遇。作为2022年泰国主要外资来源国之一,中国企业通过电子制造、智能家电、数据中心及新能源车等领域的投资深度参与泰国"4.0…

【设计模式】- 创建者模式

单例模型 饿汉式 静态方法创建对象 public class Singleton {// 私有构造方法private Singleton(){}private static Singleton instance new Singleton();// 提供一个外界获取的方法public static Singleton getInstance(){return instance;} }静态代码块创建对象 public …

逻辑与非逻辑的弥聚

非逻辑弥聚与逻辑弥聚是复杂系统中两种不同的信息整合方式。逻辑弥聚侧重于通过明确的规则、规律和结构化方法,将分散的信息或功能进行有序的组织和集中处理,强调理性和确定性。而非逻辑弥聚则更多地涉及情感、直觉、经验等非线性、非结构化的因素&#…

Linux进程信号(三)之信号产生2

文章目录 4. 由软件条件产生信号5. 硬件异常产生信号模拟一下除0错误和野指针异常除0错误野指针错误 总结思考一下 4. 由软件条件产生信号 SIGPIPE是一种由软件条件产生的信号,在“管道”中已经介绍过了。 软件条件不就绪,很明显这个软件条件没有直接报错&#xff…

读取18B20的问题,时钟太慢了

使用MSP430,1M时钟,在读取18B20数据时,一直存在问题,使用逻辑分析仪读取的数据也是莫名其妙,查看电路图和器件也没有发现问题,就这样断断续续的卡了一周多。 今天忽然想把时钟升一下试试,原来1…

第12章 Java多线程机制

12.1 进程与线程 4种状态:新建、运行、中断和死亡。 (新建、运行、中断和死亡) 建立线程的两种方法:用Thread类或其子类。 线程新建后,必须调用 start () 方法使其进入就绪队列,才有机会获得 CPU 资源&a…

利用 Amazon Bedrock Data Automation(BDA)对视频数据进行自动化处理与检索

当前点播视频平台搜索功能主要是基于视频标题的关键字检索。对于点播平台而言,我们希望可以通过优化视频搜索体验满足用户通过模糊描述查找视频的需求,从而提高用户的搜索体验。借助 Amazon Bedrock Data Automation(BDA)技术&…

React 19版本refs也支持清理函数了。

文章目录 前言一、refs 支持清理函数二、案例演示1.useEffect写法2.React 19改进 的ref写法 总结 前言 React 19版本发布了ref支持清理函数了,这样就可以达到useEffect一样的效果了。为啥需要清理函数呢,这是因为节约内存。 清理事件监听(避…

城市内涝监测预警系统守护城市安全

一、系统背景 城市内涝是指由于强降水或连续性降水超过城市排水能力,导致城市内产生积水灾害的现象。随着气候变化和城市化进程的加快,城市内涝现象愈发频繁和严重。传统的城市排水系统已难以满足当前的城市排水需求,特别是在暴雨等极端天气条…

Flink 作业提交流程

Apache Flink 的 作业提交流程(Job Submission Process) 是指从用户编写完 Flink 应用程序,到最终在 Flink 集群上运行并执行任务的整个过程。它涉及多个组件之间的交互,包括客户端、JobManager、TaskManager 和 ResourceManager。…

ctr查看镜像

# 拉取镜像到 k8s.io 命名空间 sudo nerdctl --namespace k8s.io pull nginx:1.23.4 # 验证镜像是否已下载 sudo nerdctl --namespace k8s.io images 下载镜像到k8s.io名称空间下 nerdctl --namespace k8s.io pull zookeeper:3.6.2 sudo ctr image pull --namespace k8s.io …

中科院自动化研究所通用空中任务无人机!基于大模型的通用任务执行与自主飞行

作者: Ji Zhao and Xiao Lin 单位:中科院自动化研究所 论文标题:General-Purpose Aerial Intelligent Agents Empowered by Large Language Models 论文链接:https://arxiv.org/pdf/2503.08302 主要贡献 硬件-软件协同设计框…

数据结构 -- 树形查找(三)红黑树

红黑树 为什么要发明红黑树 平衡二叉树AVL:插入/删除很容易破坏平衡性,需要频繁调整树的形态。如:插入操作导致不平衡,则需要先计算平衡因子,找到最小不平衡子树(时间开销大),在进行…

容器化-k8s-使用和部署

一、K8s 使用 1、基本概念 集群: 由 master 节点和多个 slaver 节点组成,是 K8s 的运行基础。节点: 可以是物理机或虚拟机,是 K8s 集群的工作单元,运行容器化应用。Pod: K8s 中最小的部署单元,一个 Pod 可以包含一个或多个紧密相关的容器,这些容器共享网络和存储资源。…

力扣-283-移动零

1.题目描述 2.题目链接 283. 移动零 - 力扣&#xff08;LeetCode&#xff09; 3.题目代码 class Solution {public void moveZeroes(int[] nums) {int dest-1;int cur0;while(cur<nums.length){if(nums[cur]0){cur;}else if(nums[cur]!0){swap(nums,cur,dest1);cur;dest…

前端开发笔记与实践

一、Vue 开发规范与响应式机制 1. 组件命名规范 自定义组件使用大驼峰命名法&#xff08;如 MyComponent&#xff09;&#xff0c;符合 Vue 官方推荐&#xff0c;便于与原生 HTML 元素区分。 2. Proxy vs defineProperty 特性Proxy&#xff08;Vue3&#xff09;Object.defi…

如何给PSCAD添加库文件

1、点击Options 2、选择蓝色的选项 3、查看Intel(R) Visual Fortran Compiler XE 的版本 4、打开原文件的Library 5、打开 6、点击这个文件的右键 7、然后选择第一项project setting 9、先把第8步中link里面原有的路径删除&#xff0c;再点browes[A1] &#xff0c;然后选择 [A…

milvus+flask山寨《从零构建向量数据库》第7章case2

继续流水账完这本书&#xff0c;这个案例是打造文字形式的个人知识库雏形。 create_context_db: # Milvus Setup Arguments COLLECTION_NAME text_content_search DIMENSION 2048 MILVUS_HOST "localhost" MILVUS_PORT "19530"# Inference Arguments…