ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

用OpenCV实现文档扫描:从边缘检测到透视校正完整指南

用OpenCV实现文档扫描:从边缘检测到透视校正完整指南 1. 项目概述与最终效果预览作为一个经常和图像处理打交道的人我电脑里一直躺着几十张拍歪了的合同、书页和名片照片。以前处理这些资料要么用手机上的扫描类App要么用Photoshop手工拉透视线条效率低不说还经常把画面裁坏。后来我干脆用OpenCV自己写了一个文档扫描工具从边缘检测定位文档区域到透视校正输出规整的扫描件全套逻辑不过200多行Python代码效果却相当能打。这个项目其实解决的是一个很刺痛的需求你用手机随便拍一张纸它不可能是绝对端正的——桌面有景深、纸张有弯曲、光线有阴影直接拿去打印或者做OCR识别准确率和观感都会大打折扣。而OpenCV提供的Canny边缘检测、轮廓搜索、多边形逼近、透视变换这四条技术链路恰好能形成一个完整的自动化流程。最早我是看了网上零散的教程照着做了几张测试图效果还行但很不稳定后来自己动手调参数、加保护逻辑、处理各种翻车情况才慢慢打磨成现在这个版本。这篇文章会把整个实现过程掰开揉碎来讲适合已经装好Python、想找一个完整OpenCV实战项目练手的读者也适合做完初步接触后想理解“文档扫描类App背后的原理是什么”的人。无需任何机器学习基础只要会基本的Python语法就行。2. 环境准备OpenCV安装与开发环境搭建2.1 快速安装OpenCV的两种方式做这个项目首先得把OpenCV跑起来。我用的是Python 3.9版本OpenCV用的是目前稳定性比较好的4.x系列。安装方式很简单直接用pip安装即可pip install opencv-python如果你后面还需要处理视频或者用一些高级特征检测算法可以再加一个pip install opencv-contrib-python很多初学者在这一步会遇到一个非常尴尬的报错ModuleNotFoundError: No module named cv2。这个报错90%的情况不是OpenCV没装上而是Python环境搞混了——比如你直接在系统自带的Python上安装了包但IDE里配置的解释器是另一个虚拟环境。我吃过这个亏当时的解决方法是先在命令行里输入python --version确认版本再输入python -m pip list | findstr opencvWindows环境Mac/Linux把findstr换成grep查看OpenCV是否真的装到了当前环境。还有一个常见的坑是只用了某个在线编辑器或者Jupyter Notebook在服务器端跑得好好的换到本地就找不到包了这种就要检查是不是每次启动都激活了同一个虚拟环境。一般来说用Anaconda的话可以创建独立环境conda create -n scanner python3.9 conda activate scanner pip install opencv-python这样环境干净后面装其它依赖也不会互相污染。2.2 用一张测试图快速验证安装安装完之后先别急着写完整的扫描流程立刻跑一段极简代码验证环境是否正常import cv2 # 读取图片 img cv2.imread(test.jpg) print(Image shape:, img.shape) # 转为灰度图并做边缘检测 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) edges cv2.Canny(gray, 50, 150) # 保存结果 cv2.imwrite(edges.jpg, edges) print(OK)这段代码的核心作用是打通“读图-处理-写图”这整条链路。如果输出能看到图片尺寸并且生成了edges.jpg说明OpenCV的基本功能正常可以开始后文的工作了。如果图像显示是空的或者直接报错先检查图片路径——相对路径要看你当前工作目录在哪。我写代码的习惯是先把图片和脚本放在同一个目录下用os.chdir()切过去或者干脆用绝对路径省得被路径问题折腾。3. 文档扫描的核心思路为什么要用边缘检测和透视校正3.1 边缘检测选型Canny为什么是这个任务的主力一提到边缘检测很多入门读者第一反应是Sobel或者Prewitt这类算子。确实这类算子在教科书里占据了大量篇幅它的原理是通过计算图像灰度在水平和垂直方向的梯度得到边缘的强度和方向信息。但放在“文档扫描”这个真实场景里Sobel有一个致命弱点——它对噪声敏感检测结果里会残留大量琐碎的纹理边缘比如纸张上的细小文字、纤维纹理都会形成干扰。Prewitt算子原理类似区别只在卷积核系数不同两者的本质逻辑非常相似。相比之下Canny边缘检测在工程实践里几乎是检测纸张边缘的默认选择。它的完整流程是先用高斯滤波平滑图像降低噪声对梯度计算的干扰然后用Sobel算子计算梯度幅值和方向接着做非极大值抑制把梯度方向上不是局部最大值的点剔除让边缘变得细而连续最后利用双阈值机制设定一个高阈值和一个低阈值高阈值确定强边缘低阈值则帮助弱边缘与强边缘相连通。实际参数选择上我经过很多轮实验在普通室内光照场景下Canny的低阈值取50左右、高阈值取150左右效果最稳定。这个组合能把纸张边缘这种强梯度区域完整保留下来同时过滤掉大部分杂乱纹理。如果光照特别不均匀可以把高斯模糊的卷积核从默认的3x3调大到5x5虽然边缘定位精度会损失一点点但换来的是整体连续性明显提升。3.2 为什么需要透视校正而不是仿射变换很多人以为把歪了的文档拉正只是把图像旋转一个角度就行了这个想法对俯拍但纸张水平的情况勉强适用。但现实中手机拍摄文档几乎都是斜着拍的——纸张平面和镜头平面存在一个夹角表现出来就会呈现“近大远小”的透视变形上下边不再平行矩形变成了梯形或者不规则的四边形。这种情况下单纯旋转图像根本无济于事必须做透视校正。透视校正的核心是计算一个单应性矩阵Homography Matrix它描述了图像平面上任意点和校正平面之间的映射关系。只要能找到文档的四个角点坐标就能构造出这个3x3矩阵然后把原图上任意像素通过矩阵运算映射到目标位置。OpenCV里一行cv2.getPerspectiveTransform就能算出来再用cv2.warpPerspective执行映射。这里面的关键不是API有多深奥而是“四个角点怎么定位准确”这恰恰是困扰绝大多数初学者的地方。后面我会详细说说如何通过边缘检测、轮廓搜索、多边形逼近这一整套流程来稳定拿到四个角点而不是靠手动点选。4. 实现第一站图像读取、预处理和边缘检测实战4.1 读取图像与基础预处理流程先打开一张测试图片做预处理。这里我故意选了一张带桌面纹理和阴影的照片因为太干净的测试图根本说明不了问题。import cv2 import numpy as np def read_and_preprocess(image_path): # 读入图像 img cv2.imread(image_path) # 记录原始尺寸后面有用 orig_height, orig_width img.shape[:2] # 缩放到合适的处理尺寸提高处理速度和稳定性 scale 800.0 / max(orig_height, orig_width) new_width int(orig_width * scale) new_height int(orig_height * scale) resized cv2.resize(img, (new_width, new_height)) # 转灰度 轻微高斯模糊 gray cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY) blurred cv2.GaussianBlur(gray, (5, 5), 0) # Canny边缘检测 edges cv2.Canny(blurred, 50, 150) return resized, edges为什么要先缩放因为如果原图是4000x3000这种尺寸直接做边缘检测和轮廓查找计算量会大很多而且边缘检测结果很容易包含过多细节。缩放至长边800像素左右既保留了足够的轮廓信息又让后续的轮廓搜索快得飞起。高斯模糊这一层的参数我特意用了5x5卷积核因为文档照片里纸张边缘区域往往伴随着阴影阴影会产生一条较宽的灰度过渡带如果模糊得不够Canny可能会把阴影和纸张边缘的重叠区域误检成两三条平行边缘造成轮廓断裂。5x5是一个兼顾细节和平滑的经验值。4.2 Canny关键参数调试心得Canny的两个阈值很多教程直接写死50和150但是不同光照条件下这个参数需要微调。我记得自己第一次用手机在晚上开灯拍的A4纸整个图片的对比度极大白色的纸和深色的桌面几乎是两极分化这时候阈值50/150会检出一堆文字边缘反而把纸张最外围的边缘淹没了。后来我采取了一个笨但有效的方法先用cv2.createTrackbar做了一个简单的滑动条程序实时查看两个阈值对边缘图的影响然后靠肉眼挑出最干净的一组参数。具体手段是在窗口里实时更新边缘图cv2.namedWindow(Canny Demo) cv2.createTrackbar(Low, Canny Demo, 50, 255, lambda x: None) cv2.createTrackbar(High, Canny Demo, 150, 255, lambda x: None) while True: low cv2.getTrackbarPos(Low, Canny Demo) high cv2.getTrackbarPos(High, Canny Demo) edges cv2.Canny(blurred, low, high) cv2.imshow(Canny Demo, edges) if cv2.waitKey(1) 0xFF ord(q): break cv2.destroyAllWindows()调参的结果是在强对比度场景下低阈值可以提到70到80高阈值提到170到200这样能屏蔽掉大部分纸张内部的文字边缘反之在光线柔和但纸张和背景颜色接近的时候低阈值降到30左右才能保证边缘不中断。要注意Canny的阈值还有一个经验法则高阈值约等于低阈值的2到3倍。如果两个阈值过于接近边缘会被切得很碎若差值太大则容易丢失弱边缘。这些细节不实际跑一遍光看书是体会不到的。4.3 边缘检测之后的轮廓发现与初筛边缘检测只得到了一个二值图还需要把边缘连接起来形成轮廓。这里用到的是cv2.findContours但有个细节很多人会忽略OpenCV的findContours在版本4.x返回两个值(contours, hierarchy)而在OpenCV 3.x早期版本返回三个值多了个image。如果你的代码报“not enough values to unpack”基本就是版本差异导致的直接用contours, _ cv2.findContours(...)就能规避。代码接着写contours, _ cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)模式参数cv2.RETR_EXTERNAL表示只提取最外层的轮廓不关心内层嵌套。这对于文档检测非常重要——文档区域内肯定有大量文字和表格线如果使用RETR_LIST或者RETR_TREE内部轮廓也会被提取出来后续排序筛选的工作量会成倍增加而且很容易误判。存储格式参数cv2.CHAIN_APPROX_SIMPLE也很关键它会把一条直线上的多个点压缩成首尾两个点大大减少轮廓点数方便后面做多边形拟合。拿到轮廓后还要做一次初筛。一个合格文档的外轮廓应该具备三个特征面积足够大、轮廓较长、形状接近矩形。最暴力的筛选方式是按轮廓面积从大到小排序取最大的一个。但这里有个实际问题——当背景里有窗户、椅子、桌沿等大轮廓时最大轮廓不一定就是文档。所以我会多管齐下先对轮廓做多边形逼近再检查逼近结果是否有四个顶点并且面积占比合理。5. 实现第二站轮廓筛选与文档角点定位5.1 多边形逼近从一堆轮廓到四个角点上面提到初筛后得到了候选轮廓接下来要做的就是最核心的一步对轮廓做多边形逼近找到最接近矩形的那个多边形。def get_corner_points(contour): # 计算轮廓周长用于设置逼近精度 peri cv2.arcLength(contour, True) # 多边形逼近epsilon是当前轮廓周长的一定比例 approx cv2.approxPolyDP(contour, 0.02 * peri, True) return approx这里面的关键参数是arcLength计算出的轮廓周长以及approxPolyDP接收的0.02 * peri这个比例。这个比例决定了逼近的难度——比例越小逼近后的多边形越接近原始轮廓顶点数可能很多比例越大顶点越少但也可能削掉真实的角点。经过大量实测我发现0.02这个比例对于文档边缘是经验值中的甜点。如果纸张边缘非常平直可以降低到0.01得到更精确的角点如果纸张边缘本身有弯曲或褶皱则要提高到0.03左右否则逼近出来的多边形会在弯曲处多出一两个顶点。多边形的顶点数可以通过len(approx)拿到。在理想情况下文档外轮廓应该逼近为4个顶点。但实际中经常得到5个或者6个顶点这通常是因为纸张某条边被桌面阴影分割了或者边缘检测时把纸边和桌面上的条纹误连到了一起。5.2 候选排序和矩形度判断针对顶点数不是4的情况我的策略是排序所有轮廓筛选出面积最大的若干个再逐一检查。# 按照轮廓面积从大到小排序 contours sorted(contours, keycv2.contourArea, reverseTrue) for contour in contours: peri cv2.arcLength(contour, True) approx cv2.approxPolyDP(contour, 0.02 * peri, True) # 只有逗留4个顶点的多边形才进入下一轮 if len(approx) 4: # 进一步检查这个四边形是否足够大 area cv2.contourArea(approx) if area (edges.shape[0] * edges.shape[1]) * 0.1: return approx面积大于整张图10%这个条件有些保守但能有效排除那些背景里的细小四边形物体。如果换一个光照更均匀的场景可以放宽到5%甚至更低。如果你在处理需要支持名片、身份证这类较小文档的场景这个10%的比例就得下调。简单做法是把最小面积变为可配置项根据你的主要拍摄对象灵活调整。我在项目里做成了参数默认0.1用户用了好几张不同距离拍的照片后可以手动微调。5.3 定位四个角点的先后顺序这一步是最容易忽视但也是最重要的。透视变换需要明确知道四个点分别对应原始图像中的左上、右上、右下、左下。如果顺序搞错了输出的结果可能是旋转了90度的甚至是对角翻折的那就是纯粹的灾难了。一开始我犯过这个错误后来才总结出一个通用的排序方法先计算四个点的中心坐标然后以中心为参考系把四个点按照角度排序。def order_points(pts): # 初始化坐标点列表 rect np.zeros((4, 2), dtypefloat32) # 计算每个点的坐标和 s pts.sum(axis1) # 左上角的坐标和最小 rect[0] pts[np.argmin(s)] # 右下角的坐标和最大 rect[2] pts[np.argmax(s)] # 计算每个点的坐标差 diff np.diff(pts, axis1) # 右上角的坐标差最小 rect[1] pts[np.argmin(diff)] # 左下角的坐标差最大 rect[3] pts[np.argmax(diff)] return rect这个排序方法的底层逻辑是在一张正常的图像坐标系中原点在左上角x轴向右y轴向下。因此左上角的xy值最小右下角的xy值最大而右上角的x-y值最小x大且y小左下角的x-y值最大。使用坐标和以及坐标差就能稳定地把四个点归位。但要注意这个顺序仅适用于文档主体在图片中大致呈凸四边形并且没有严重翻转的情况。如果文档是倒着放的排出来的“左上”会对应文档的右下此时输出的扫描图会是倒的。要解决这个问题我通常会在检测阶段额外加一个粗略的方向判断或者干脆在输出界面提供一个旋转按钮手一抖就转回来了省心省力。6. 实现第三站透视变换与扫描件生成6.1 计算目标尺寸有了四个角点之后下一步要确定校正后的目标尺寸。这里不能用原始的图像宽高直接映射因为原始图像中文档区域只占一部分而且像素密度不均匀近处的边明显比远处的边更长。所以我们按照文档的实际边长来计算输出尺寸。最常见的做法是取上边和下边的平均值作为目标宽度取左边和右边的平均值作为目标高度def compute_dimensions(rect): (tl, tr, br, bl) rect # 计算上边和下边长度 width_top np.linalg.norm(tr - tl) width_bottom np.linalg.norm(br - bl) # 取平均作为最终宽度 max_width max(int(width_top), int(width_bottom)) # 计算左边和右边长度 height_left np.linalg.norm(bl - tl) height_right np.linalg.norm(br - tr) # 取平均作为最终高度 max_height max(int(height_left), int(height_right)) return max_width, max_height这里我倾向于取最大值而不是平均值。原因在于文档边缘检测本身就存在微小误差如果取平均值可能让输出的长边被压缩出现边线往里收的视觉瑕疵取最大值能保证纸张的四个角都被完全包含在输出画面内相比之下更保险。6.2 关键变换getPerspectiveTransform warpPerspective目标尺寸算出来后剩下的就非常简单了def four_point_transform(image, pts): # 获取排好序的四个点 rect order_points(pts) (tl, tr, br, bl) rect # 计算目标尺寸 max_width, max_height compute_dimensions(rect) # 构造目标图像的四个角点 dst np.array([ [0, 0], [max_width - 1, 0], [max_width - 1, max_height - 1], [0, max_height - 1] ], dtypefloat32) # 计算单应性矩阵并应用透视变换 M cv2.getPerspectiveTransform(rect, dst) warped cv2.warpPerspective(image, M, (max_width, max_height)) return warped细节在这个-1上目标图像的坐标最大值是width-1和height-1而不是width和height。如果直接使用max_width作为坐标值生成的图像会有整整一个像素的偏移在实际操作中虽然看不出来太大问题但对于追求精确的扫描输出来说这个细节是值得注意的。cv2.warpPerspective默认的插值方式是双线性插值对文档扫描来说足够了。但如果你处理的是带有很多细线、小字的单据建议显式指定cv2.INTER_CUBIC或者cv2.INTER_LANCZOS4这两种插值方式对边缘的保持更细腻输出文字不容易出现锯齿。6.3 预处理原图还是缩放图一个影响精度的关键点上面我的代码处理流程是先把原始图像缩放到长边800像素然后在缩放图上做边缘检测和角点定位但是最后使用four_point_transform时要把检测到的四个角点映射回原始图像的坐标空间。这是新手最容易踩的坑之一——直接用缩放图上的坐标去校正原始高分辨率图像结果输出画面严重偏移角落被裁掉一大块。解决办法有两种。第一种是在缩放之前记录下缩放比例然后对检测到的四个角点坐标做反向缩放# 缩放比例 scale 800.0 / max(orig_height, orig_width) # 检测到的角点映射回原始坐标 orig_pts approx.reshape(4, 2) / scale warped four_point_transform(orig_img, orig_pts)第二种方法是把整条流程全部放到原始分辨率上执行但这样做速度会慢很多边缘检测也会引入更多无关细节。所以我在实际项目中会选择第一种方法——先用缩放图找到角点映射到原始坐标后再做透视变换。这样既保证了精确度又没有牺牲计算性能。7. 输出优化与文件保存7.1 灰度化与二值化提升扫描质感透视校正之后得到的扫描件还带着当前光照的颜色和阴影如果要追求那种扫描App常见的白底黑字效果还需要额外处理一步。针对白底文档最简单有效的方式是灰度化后做自适应二值化gray_warped cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY) # 自适应阈值二值化适合光照不均的情况 binary cv2.adaptiveThreshold( gray_warped, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 31, # 块大小最好是奇数 10 # 常量值越大越容易变白 )这里需要解释一下为什么不用全局阈值cv2.threshold。因为文档在扫描过程中虽然做了透视校正但光照在纸面上的分布依然不均匀有可能左边偏暗右边偏亮。全局阈值只能取一个固定的值必然导致一部分区域变成大片黑色。而自适应阈值会以每个像素为中心计算它周围一个局部区域的加权平均再以此为参考做阈值判断因此能够有效应对光照渐变的问题。块大小参数31的意思是每个像素会参考它周围31x31范围内的灰度分布。这个值设得太小二值化结果会出现很多噪点设得太大又可能让细小文字被合并成黑色块。根据不同文字大小可以做调整普通A4文档用31是比较稳的。常量值10是一个微调项越大判定为白色的条件越苛刻输出也会更白更干净。7.2 保存为高质量图像最后一步就是保存。这里有个小技巧使用cv2.imwrite保存为不同格式时可以通过额外参数控制图片质量。# 保存为PNG无损压缩 cv2.imwrite(scan.png, binary) # 保存为高质量JPEG质量参数95 cv2.imwrite(scan.jpg, binary, [cv2.IMWRITE_JPEG_QUALITY, 95])PNG格式适合后续做OCR的情况因为它是无损压缩文字边缘不会产生压缩伪影。JPEG格式的文件小得多方便发邮件或者存手机相册但质量参数如果低于80文字边缘会出现明显的模糊和块状痕迹。如果想要极高还原度的扫描输出PNG是更优选。8. 常见问题与排查技巧实录8.1 找不到轮廓或者轮廓顶点数不是4这是出现频率最高的问题。综合看下来大多数原因是Canny生成的边缘图不连续纸张边缘断成了好几段。排查时先把边缘图保存出来肉眼观察一下是不是边缘内部包含大量文字轮廓或者边缘外部包含一堆背景干扰。解决方案按优先级排序调整Canny阈值让外部边缘更突出加大高斯模糊的卷积核比如从5x5改成7x7做一个形态学闭运算把断裂的边缘连接起来kernel np.ones((5, 5), np.uint8) closed cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel)闭运算的原理是先膨胀后腐蚀膨胀能让边缘向外扩展从而连接断裂处腐蚀再恢复原来的粗细。这一步对修复断裂边缘非常有效用了之后我的检测成功率有了肉眼可见的提升。8.2 透视校正后图像是斜的或者翻转的有一个很典型的现象检测出来的四边形本身是正确的但校正之后文字是倒的。这种情况几乎都是四个点的排序逻辑出了问题。原文档如果旋转角度过大比如文档本身是倒着放的那么按照固定排序规则得到的“左上角”其实是视觉上的“左上角”而不是文档真正的左上角。做项目时要考虑这个边界情况的话我一般在界面上加一个“旋转180度”的按钮手动补救兼容所有角度。如果你想让代码全自动可以额外引入OCR软件包来检测文字方向但这就超出本文范围了而且会大大增加依赖复杂度。8.3 输出图像边缘发虚、文字模糊原因通常是透视变换时目标尺寸算小了导致原本高分辨率的信息被压缩到一个较小的画布上。解决方法是把输出尺寸再放大一些比如乘一个经验系数。scale_factor 1.5 max_width int(max_width * scale_factor) max_height int(max_height * scale_factor)更高的输出分辨率能让边缘检测的误差被进一步稀释同时也为后续的OCR提供更好的输入质量。8.4 常见问题速查表现象可能原因优先排查顺序找不到轮廓边缘断裂严重1.降低Canny低阈值 2.加大模糊核 3.增加形态学闭运算轮廓点过多边缘过于复杂1.调高approxPolyDP的epsilon比例 2.检查是否有背景干扰输出图翻转角点排序错误1.检查order_points逻辑 2.手动旋转测试输出图模糊目标尺寸过小1.增大输出分辨率 2.使用更高质量的插值方法处理速度慢原始图像过大1.先缩放再检测 2.边缘坐标映射回原图后再校正9. 进阶思考如何把代码整合成完整的可执行程序到这里整个核心链路就讲完了。下面把这些代码片段整合成一个完整的可运行版本整体结构清晰方便查阅。import cv2 import numpy as np def order_points(pts): rect np.zeros((4, 2), dtypefloat32) s pts.sum(axis1) rect[0] pts[np.argmin(s)] rect[2] pts[np.argmax(s)] diff np.diff(pts, axis1) rect[1] pts[np.argmin(diff)] rect[3] pts[np.argmax(diff)] return rect def compute_dimensions(rect): (tl, tr, br, bl) rect width_top np.linalg.norm(tr - tl) width_bottom np.linalg.norm(br - bl) height_left np.linalg.norm(bl - tl) height_right np.linalg.norm(br - tr) max_width max(int(width_top), int(width_bottom)) max_height max(int(height_left), int(height_right)) return max_width, max_height def four_point_transform(image, pts): rect order_points(pts) max_width, max_height compute_dimensions(rect) dst np.array([ [0, 0], [max_width - 1, 0], [max_width - 1, max_height - 1], [0, max_height - 1] ], dtypefloat32) M cv2.getPerspectiveTransform(rect, dst) return cv2.warpPerspective(image, M, (max_width, max_height)) def scan_document(image_path): orig_img cv2.imread(image_path) orig_h, orig_w orig_img.shape[:2] scale 800.0 / max(orig_h, orig_w) new_w, new_h int(orig_w * scale), int(orig_h * scale) resized cv2.resize(orig_img, (new_w, new_h)) gray cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY) blurred cv2.GaussianBlur(gray, (5, 5), 0) edges cv2.Canny(blurred, 50, 150) kernel np.ones((5, 5), np.uint8) edges cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel) contours, _ cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) contours sorted(contours, keycv2.contourArea, reverseTrue) for contour in contours: peri cv2.arcLength(contour, True) approx cv2.approxPolyDP(contour, 0.02 * peri, True) if len(approx) 4: area cv2.contourArea(approx) if area (new_h * new_w) * 0.1: pts approx.reshape(4, 2).astype(float32) # 映射回原图坐标 pts * 1.0 / scale warped four_point_transform(orig_img, pts) return warped return None if __name__ __main__: result scan_document(test_contract.jpg) if result is not None: gray cv2.cvtColor(result, cv2.COLOR_BGR2GRAY) binary cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 31, 10 ) cv2.imwrite(scan_result.png, binary) print(扫描完成输出 scan_result.png) else: print(未找到文档区域请调整拍摄角度或光照条件)运行这个程序时把你的测试图片路径填进去如果一切正常就能在目录下找到输出的扫描件。我拿手机随手拍的一张带有复杂背景的合同照实测整个过程大约耗时0.3秒输出文字清晰白底干净效果已经完全可以替代普通扫描仪了。这个项目做到这个程度已经足够日常使用后续你还可以把相机调用接进来做成实时扫描也可以加入OCR自动识别文字核心思路都不会变依然是边缘检测、角点定位、透视校正这三板斧。代码本身不复杂真正有价值的是理解每一步为什么这样做以及在各种翻车场景下如何对症下药这才是做图像处理项目最磨人的地方也是收获最大的地方。
返回列表