
简介本资源是一个基于PaddlePaddle实现的轻量级文档扫描与边界检测开源项目面向机器视觉初学者、AI实践者及PaddlePaddle平台学习者解决日常文档图像中背景干扰大、边缘不清晰、内容定位不准等核心问题。压缩包共19个文件含4个核心Python脚本main.py、predict.py、transform.py、demo.py、8张JPG/JPEG格式测试图像含收据、美元钞票、手机拍摄文档等真实场景样本、1个README.md说明文档、1个requirements.txt依赖清单及PNG/MD/TXT等辅助文件整体大小为14.12MB结构简洁、开箱即用。已有220人下载学习适合快速上手目标检测实战。用户可直接运行预测脚本完成文档四边定位与透视矫正深入理解PaddlePaddle目标检测模型部署流程模块化代码设计便于二次开发完整注释与示例图像显著降低学习门槛配套README与典型输入输出样例为视觉识别教学与工程化迁移提供可靠参考。1. 这不是“一键扫描”而是用 PaddlePaddle 精准框出文档四边的端到端视觉 pipeline你拍一张歪斜、带阴影、背景杂乱的纸质收据手机 App 能瞬间裁切、拉直、增强——背后不是魔法而是一套基于目标检测的文档边界定位系统。这个Document-Scanner.zip项目正是用 PaddlePaddle 实现了这一过程的核心能力它不依赖 OCR 提取文字而是先用目标检测模型如 PP-YOLOE 或自定义轻量 Head在原始图像中精准回归出文档区域的四顶点坐标再通过透视变换完成几何校正。这意味着即使文档被手遮挡一半、压在书页边缘、或反光严重只要视觉上存在可辨识的轮廓模型仍能给出鲁棒的边界框。它适合正在学习 PaddlePaddle 视觉开发的工程师、需要快速集成文档预处理模块的产品团队以及想深入理解“检测→几何变换→输出”闭环的算法实践者。项目结构干净main.py是入口predict.py封装推理逻辑transform.py实现核心透视校正——所有环节都可调试、可替换、可量化评估。2. 文档边界检测为什么选目标检测而非传统图像处理2.1 传统方法的失效场景与检测方案的必要性早期文档扫描常依赖 Canny 边缘检测 Hough 变换找直线或基于 HSV 颜色空间分割白纸区域。这类方法在理想光照、纯白背景、无遮挡条件下尚可工作但一旦遇到以下真实场景即崩溃手机拍摄时文档倾斜角度 30°Hough 检测出多条干扰线背景为木纹桌面或格子笔记本Canny 输出大量伪边缘文档局部反光或阴影导致二值化阈值失效多张纸堆叠拍摄模型需区分最上层纸张而非整摞。目标检测方案本项目采用 PaddleDetection 中的 PP-YOLOE-tiny将问题转化为“定位一个四边形实例”直接回归其最小外接矩形RotatedBox或四个顶点坐标PointSet。相比语义分割需逐像素分类检测模型参数量更小、推理更快相比关键点检测需额外设计后处理PP-YOLOE 原生支持 RotatedBox 输出天然适配文档四边建模。提示项目未提供训练好的.pdparams模型文件需自行训练或下载 PaddleDetection 官方预训练权重。predict.py中model paddle.jit.load(inference_model)行暗示模型已导出为静态图格式这是 PaddlePaddle 部署的关键步骤。2.2 模型选型与 PaddlePaddle 生态适配逻辑PaddlePaddle 在文档类任务中具备三重优势原生 RotatedBox 支持PaddleDetection 的PPYOLOEHead可配置rotatedTrue输出(cx, cy, w, h, angle)五元组避免 OpenMMLab 等框架需手动改写 Head 的麻烦轻量部署友好PP-YOLOE-tiny在 320×320 输入下FP32 推理速度达 42 FPSRTX 3060满足移动端实时需求数据增强链路完整PaddleDetection 内置RandomRotate,RandomPerspective,RandomLighting等针对文档畸变的增强算子比 OpenCV 手写增强更鲁棒。项目requirements.txt明确指定paddledet2.5.0该版本已稳定支持 RotatedBox 训练。若升级至 3.x 版本需注意RotatedAnchorGenerator已移除需改用RBoxAssigner配合RotatedBBoxCoder。2.3 从 predict.py 看检测推理全流程# predict.py 关键片段已补全注释 import paddle from paddledet.engine import Trainer from paddledet.data.transforms import Resize, NormalizeImage, Permute from paddledet.models import create def load_model(model_path): # 加载静态图模型非动态图 model paddle.jit.load(model_path) # model_path 指向 inference_model/ model.eval() return model def preprocess_image(img_path): # 步骤1读取并归一化 img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 步骤2Resize 到模型输入尺寸如 640x640 transform Resize(target_size[640, 640]) img transform({image: img})[image] # 步骤3Normalize PermuteCHW 格式 img NormalizeImage(mean[123.675, 116.28, 103.53], std[58.395, 57.12, 57.375])({image: img})[image] img Permute()(img)[image] # [H,W,C] - [C,H,W] return paddle.to_tensor(img).unsqueeze(0) # 添加 batch 维度 def run_inference(model, img_tensor): # 步骤4执行前向推理 outs model(img_tensor) # 输出为 dict含 bbox 和 bbox_num # 步骤5解析 RotatedBox 结果关键 boxes outs[bbox].numpy() # shape: [N, 6] → [x1,y1,x2,y2,score,class_id] # 注意PP-YOLOE-tiny 默认输出水平框若启用 rotated此处应为 [N, 7] → [cx,cy,w,h,angle,score,class_id] return boxes # 实际调用 model load_model(./inference_model) img_tensor preprocess_image(receipt.jpeg) boxes run_inference(model, img_tensor) print(f检测到 {len(boxes)} 个文档区域)这段代码揭示了三个易错点输入尺寸必须匹配Resize的target_size必须与训练时一致否则 bbox 坐标会偏移归一化参数硬编码mean/std来自 ImageNet若训练时用了自定义均值如文档图像均值此处需同步修改RotatedBox 解析逻辑缺失当前代码按水平框解析若模型实际输出 RotatedBox需用paddledet.utils.visualizer.rbox2poly()将(cx,cy,w,h,angle)转为四顶点坐标。3. 透视变换校正从检测框到平整文档图像的数学实现3.1 四顶点坐标的提取与排序逻辑检测模型输出的 RotatedBox 坐标是(cx, cy, w, h, angle)但透视变换需要明确的顺时针四顶点顺序左上→右上→右下→左下。transform.py中的get_document_vertices()函数承担此任务# transform.py 片段 import numpy as np import cv2 def rbox_to_vertices(cx, cy, w, h, angle): 将旋转矩形参数转为四顶点坐标顺时针 :param cx, cy: 中心点 :param w, h: 宽高注意w 对应 x 轴方向h 对应 y 轴方向 :param angle: 弧度制旋转角逆时针为正 # 1. 生成未旋转的四个顶点以中心为原点 pts np.array([[-w/2, -h/2], [w/2, -h/2], [w/2, h/2], [-w/2, h/2]]) # 2. 构造旋转矩阵 R np.array([[np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]]) # 3. 旋转并平移 rotated_pts pts R.T np.array([cx, cy]) return rotated_pts.astype(np.float32) def sort_vertices(vertices): 对四顶点按顺时针排序确保左上为起点 使用极角排序以重心为原点计算各点相对于重心的角度 centroid np.mean(vertices, axis0) angles np.arctan2(vertices[:, 1] - centroid[1], vertices[:, 0] - centroid[0]) # 将角度映射到 [0, 2π)并排序 angles (angles 2 * np.pi) % (2 * np.pi) idx np.argsort(angles) return vertices[idx].astype(np.float32)这段代码的关键在于rbox_to_vertices中R.T的使用因 OpenCV 的cv2.warpPerspective要求目标顶点为顺时针而标准旋转矩阵作用于列向量需转置。若忽略.T会导致顶点顺序错乱校正后文档镜像翻转。3.2 透视变换矩阵的构建与 OpenCV 实现获得排序后的四顶点src_pts后需定义目标矩形dst_pts通常为(0,0), (width,0), (width,height), (0,height)。cv2.getPerspectiveTransform自动计算单应性矩阵Mdef warp_document(image, src_pts, dst_width800, dst_height1200): 执行透视变换 :param image: 原始 BGR 图像 :param src_pts: 排序后的四顶点float32, shape(4,2) :param dst_width, dst_height: 输出文档宽高单位像素 # 定义目标矩形顶点左上→右上→右下→左下 dst_pts np.array([ [0, 0], [dst_width, 0], [dst_width, dst_height], [0, dst_height] ], dtypenp.float32) # 计算透视变换矩阵 M cv2.getPerspectiveTransform(src_pts, dst_pts) # 执行变换注意dsize 参数为 (width, height)非 (height, width) warped cv2.warpPerspective(image, M, (dst_width, dst_height)) return warped # 示例调用 original_img cv2.imread(receipt.jpeg) vertices rbox_to_vertices(cx320, cy240, w400, h600, angle0.26) # 15度弧度 sorted_v sort_vertices(vertices) result warp_document(original_img, sorted_v, dst_width800, dst_height1200) cv2.imwrite(output.jpg, result)注意cv2.warpPerspective的dsize参数顺序是(width, height)与 NumPy 数组的(height, width)相反。若传入(1200, 800)输出图像将被错误拉伸。3.3 边界裁剪与对比度增强的后处理策略透视变换后的图像常存在黑边因M矩阵计算时部分区域映射到负坐标需裁剪有效区域def crop_black_borders(img): 基于阈值的自动裁剪 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 找到非黑区域的边界 _, binary cv2.threshold(gray, 10, 255, cv2.THRESH_BINARY) coords cv2.findNonZero(binary) if coords is not None: x, y, w, h cv2.boundingRect(coords) return img[y:yh, x:xw] return img def enhance_contrast(img): CLAHE 增强避免全局直方图均衡化过曝 clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8,8)) lab cv2.cvtColor(img, cv2.COLOR_BGR2LAB) l, a, b cv2.split(lab) l clahe.apply(l) enhanced cv2.merge((l, a, b)) return cv2.cvtColor(enhanced, cv2.COLOR_LAB2BGR)crop_black_borders使用cv2.findNonZero比np.any(img, axis2)更高效CLAHE的clipLimit2.0是经验值过高会导致噪点放大过低则增强不足。4. requirements.txt 依赖管理与环境复现避坑指南4.1 依赖声明缺失导致的典型故障现象项目提供的requirements.txt是环境复现的唯一权威依据但实际运行中常因以下原因失败PaddlePaddle 版本冲突paddledet2.5.0依赖paddlepaddle-gpu2.4.0若系统已装paddlepaddle-cpu2.5.1import paddledet会报ModuleNotFoundErrorOpenCV 版本不兼容cv2.getPerspectiveTransform在 4.5.0 支持 float64 输入但旧版仅支持 float32若src_pts为float64会静默返回None缺少 CUDA 工具链GPU 版本需nvidia-cuda-toolkit但pip install paddlepaddle-gpu不自动安装需提前apt install nvidia-cuda-toolkitUbuntu。4.2 完整可复现的依赖安装命令# 创建隔离环境推荐 conda避免污染系统 Python conda create -n docscan python3.8 conda activate docscan # 安装 PaddlePaddle GPU 版根据 CUDA 版本选择此处以 CUDA 11.2 为例 pip install paddlepaddle-gpu2.4.3.post112 -f https://www.paddlepaddle.org.cn/whl/linux/gpu/develop.html # 安装 PaddleDetection 2.5.0必须指定版本3.x 不兼容 pip install paddledet2.5.0 # 安装其他依赖按 requirements.txt 顺序避免版本冲突 pip install opencv-python4.5.5.64 pip install numpy1.21.6 pip install matplotlib3.5.2 pip install tqdm4.64.0 pip install pyyaml6.0 pip install shapely1.8.2 # paddledet 2.5.0 依赖提示paddledet2.5.0的setup.py中声明shapely1.7.0但shapely2.0.0会因 API 变更导致rbox2poly()报错必须锁定shapely1.8.2。4.3 验证环境是否正确的三步检查法基础模块导入python -c import paddle; print(paddle.__version__) python -c import paddledet; print(paddledet.__version__)输出应为2.4.3和2.5.0。模型加载测试# 尝试加载 demo 模型需先下载 inference_model/ import paddle model paddle.jit.load(./inference_model) print(模型加载成功输入形状:, model.forward.__code__.co_varnames)OpenCV 功能验证import cv2 import numpy as np pts np.array([[0,0],[100,0],[100,100],[0,100]], dtypenp.float32) M cv2.getPerspectiveTransform(pts, pts) # 应返回单位矩阵 assert M.shape (3,3), OpenCV 透视变换异常若任一检查失败需回溯pip list查看实际安装版本并用pip install --force-reinstall强制覆盖。5. 模型精度优化从 detect 到 transform 的端到端调优技巧5.1 检测阶段的针对性数据增强策略文档图像的常见畸变包括透视畸变相机俯角导致的梯形变形光照不均台灯直射造成的明暗分区纹理干扰木质桌面、格子纸背景。在configs/det/rbox_ppyoloe_tiny.yml中应强化以下增强TrainDataset: dataset: PPYOLOEDataset anno_file: dataset/annotations/train.json dataset_dir: dataset image_dir: images transforms: - type: RandomDistort brightness_range: [0.8, 1.2] # 光照扰动范围扩大 contrast_range: [0.7, 1.3] - type: RandomRotate degree: 15 # 旋转角度上限提高至15度模拟手持抖动 keep_shape: false # 允许图像缩放避免黑边 - type: RandomPerspective scale: [0.8, 1.2] # 透视缩放因子模拟不同拍摄距离 p: 0.5 # 应用概率提升至0.5keep_shape: false是关键——它允许RandomRotate在旋转后自动裁剪有效区域避免训练时出现大面积黑边使模型更关注文档内容而非背景。5.2 透视变换的亚像素级精度控制默认cv2.warpPerspective使用双线性插值但在文档边缘易产生锯齿。启用cv2.INTER_CUBIC并添加borderMode可提升质量# 替代原 warp_document 函数 def warp_document_high_quality(image, src_pts, dst_width800, dst_height1200): dst_pts np.array([[0,0],[dst_width,0],[dst_width,dst_height],[0,dst_height]], dtypenp.float32) M cv2.getPerspectiveTransform(src_pts, dst_pts) # 使用三次插值 复制边缘模式 warped cv2.warpPerspective( image, M, (dst_width, dst_height), flagscv2.INTER_CUBIC, # 替代默认的 INTER_LINEAR borderModecv2.BORDER_REPLICATE # 避免边缘黑线 ) # 可选添加轻微高斯模糊抑制插值噪声 warped cv2.GaussianBlur(warped, (3,3), 0) return warpedcv2.INTER_CUBIC比INTER_LINEAR多计算 4 倍像素但文档清晰度提升显著BORDER_REPLICATE将边缘像素向外复制消除BORDER_CONSTANT默认黑边导致的校正后文档边框发虚。5.3 端到端评估指标的设计与落地仅看检测 mAP 不足以反映扫描效果需定义下游任务指标几何校正误差GCE对已知标准文档如 A4 白纸测量校正后长宽比与 1.414 的绝对误差文本可读性得分TRS用 PaddleOCR 对校正前后图像做 OCR统计字符识别准确率提升百分比。简易 TRS 计算脚本from paddleocr import PaddleOCR ocr PaddleOCR(use_angle_clsTrue, langch) def calculate_trs(original_path, warped_path, gt_text发票金额¥123.45): # 获取原始图 OCR 结果 result_orig ocr.ocr(original_path, clsTrue) text_orig .join([line[1][0] for line in result_orig[0]]) if result_orig[0] else # 获取校正图 OCR 结果 result_warp ocr.ocr(warped_path, clsTrue) text_warp .join([line[1][0] for line in result_warp[0]]) if result_warp[0] else # 计算字符级准确率简化版 acc_orig sum(c in gt_text for c in text_orig) / len(gt_text) if gt_text else 0 acc_warp sum(c in gt_text for c in text_warp) / len(gt_text) if gt_text else 0 return f原始图 TRS: {acc_orig:.2f}, 校正图 TRS: {acc_warp:.2f}, 提升: {acc_warp-acc_orig:.2f} # 示例 print(calculate_trs(receipt.jpeg, output.jpg))该脚本直接调用 PaddleOCR无需训练即可量化校正对下游 OCR 的增益——这才是文档扫描系统的终极 KPI。本文还有配套的精品资源点击获取