尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于OpenCV与Faster R-CNN的票据识别系统构建

基于OpenCV与Faster R-CNN的票据识别系统构建 简介一套面向毕业设计与课程实训的票据OCR识别项目源码基于OpenCV图形库与TensorFlow深度学习框架开发适合计算机相关专业学生、教师及企业开发者用于票据文字识别场景的快速上手与二次开发。资源整体约105.23MB共143个文件主体为83个Python脚本另含12个XML配置、7张PNG图片、7个PDF文档及若干依赖包与模型文件目录下设OCR_pic图片库、ocrFile图片预处理与测试脚本、lib识别引擎等模块定位清晰便于按需查阅。项目代码均通过运行测试提供依赖安装文件与OCR识别测试脚本可结合OpenCV、Tesseract、TensorFlow完成从图像预处理、文字定位到识别输出的完整流程包内还包含模型权重及目标检测相关组件便于进阶学习。目前已有402人学习使用兼具工程项目与教学演示价值既适合作为毕业设计、课程设计、大作业的参考实现也适合入门者作为练手项目逐步进阶。1. 票据识别为什么需要一条自己的OCR管线直接调用现成的OCR服务返回的是一整张图的文本块拿不到“发票号码”“开票日期”“金额”这种字段级坐标。这个基于OpenCV Python的毕业设计项目本质是一条可本地运行的票据识别管线先用OpenCV做图像矫正和预处理再用目标检测网络VGG16 Fast R-CNN定位票据上的关键字段区域最后交给Tesseract做文字识别。整套流程在离线环境下能跑适合课程设计、毕设演示也适合想理解OCR全链路的入门者。和PaddleOCR、DeepSeek OCR这类端到端方案比它的优势在于每个环节都透明可改字段坐标、置信度、预处理参数全在自己手里。下面按管线顺序拆每一段的实现方式和坑。2. 票据图像预处理与透视矫正OpenCV参数怎么调才有效2.1 倾斜检测与旋转矫正minAreaRect 的实际用法真实票据进扫描仪或者手机拍照几乎都有几度倾斜直接丢给OCR引擎识别率会掉得很明显。这个项目里/photoPretreatLib目录承担的就是这部分工作。先做倾斜矫正用最小外接矩形算倾斜角然后旋转回来。import cv2 import numpy as np def deskew(image): gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 反色票据文字为黑背景为白 gray_inv cv2.bitwise_not(gray) # 所有非零像素点构成点集 coords np.column_stack(np.where(gray_inv 0)) # 用最小外接矩形求整体的倾斜角度 angle cv2.minAreaRect(coords)[-1] if angle -45: angle -(90 angle) else: angle -angle h, w image.shape[:2] M cv2.getRotationMatrix2D((w // 2, h // 2), angle, 1.0) rotated cv2.warpAffine(image, M, (w, h), flagscv2.INTER_CUBIC, borderModecv2.BORDER_REPLICATE) return rotated, anglecv2.minAreaRect返回的第三个值是矩形相对于水平轴的旋转角范围是[-90, 0)。直接拿这个角度去旋转会有方向问题所以代码里做了一次角度修正当角度小于-45时补到-90到0之间。np.column_stack(np.where(...))是把所有前景像素的坐标收集成点集比单纯用边缘检测更稳票据的表格线和文字都能参与角度计算。需要注意的是borderModecv2.BORDER_REPLICATE复边缘像素避免旋转后图像四周出现黑边干扰后续的轮廓查找。2.2 透视矫正从四角定位到变换矩阵倾斜矫正解决的是二维旋转问题但拍照角度带来的透视形变必须靠透视变换。项目里的思路是先找票据外轮廓再取四个角点做单应变换。这里最容易被忽略的是轮廓筛选条件光靠findContours会把发票章、二维码区域也框进来。def four_point_transform(image, 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)] (tl, tr, br, bl) rect widthA np.linalg.norm(br - bl) widthB np.linalg.norm(tr - tl) maxWidth max(int(widthA), int(widthB)) heightA np.linalg.norm(tr - br) heightB np.linalg.norm(tl - bl) maxHeight max(int(heightA), int(heightB)) dst np.array([ [0, 0], [maxWidth - 1, 0], [maxWidth - 1, maxHeight - 1], [0, maxHeight - 1]], dtypefloat32) M cv2.getPerspectiveTransform(rect, dst) warped cv2.warpPerspective(image, M, (maxWidth, maxHeight)) return warpedgetPerspectiveTransform需要四个点一一对应顺序错误会导致输出被镜像或者撕裂。np.diff(pts, axis1)的计算逻辑是右上角的 y 减 x 值最小左下角的 y 减 x 值最大用这个性质区分剩下两个点。透视变换的目标尺寸用两对平行边的欧氏距离取最大值保证票据内容不被横向或纵向压缩变形。这个函数在后续识别字段区域时会用到检测网络给出的坐标是相对票据原图的矫正后需要同步映射。2.3 二值化与去噪为什么票据不适合全局阈值票据背景通常是浅色但金额区域的底纹、发票章、税控码这些元素灰度值分布不均匀用cv2.threshold全局阈值处理大概率会把印章部分和二值文字混在一起。项目里/photoPretreatLib应该用的是自适应阈值这是我的处理经验也是这类场景下的常见做法。def preprocess_for_ocr(gray): # 适度降噪核大小不要超过3否则会把细笔画抹掉 blurred cv2.GaussianBlur(gray, (3, 3), 0) # 自适应阈值blockSize取奇数C是常量修正项 thresh cv2.adaptiveThreshold( blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 31, 15 ) # 去除孤立噪点使用开运算 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2)) opened cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel) return openedblockSize31意味着每个像素的阈值由周边 31×31 邻域的加权均值决定C15是修正值越大结果越暗。这两个参数对票据识别的影响很大块太小会把油墨不均匀的区域切碎块太大又逼近全局阈值效果。MORPH_OPEN开运算在去掉孤立噪声点的同时能保持文字笔画的连续。相比直接二值化这套组合对增值税发票上浅蓝色底纹的抑制效果更可控。2.4 印章遮挡处理HSV 色彩通道的隔离技巧增值税发票上红章经常压在金额数字上直接二值化会把文字和印章融在一起。项目中比较巧妙的做法是利用HSV色彩空间把红色通道单独提取或抑制。红色的色调H值在0~10和170~180两个区间恰好绕开其它颜色。def remove_seal(image): hsv cv2.cvtColor(image, cv2.COLOR_BGR2HSV) lower_red1 np.array([0, 40, 40]) upper_red1 np.array([10, 255, 255]) lower_red2 np.array([170, 40, 40]) upper_red2 np.array([180, 255, 255]) mask1 cv2.inRange(hsv, lower_red1, upper_red1) mask2 cv2.inRange(hsv, lower_red2, upper_red2) mask cv2.bitwise_or(mask1, mask2) # 将红色区域用周围像素填充 result cv2.inpaint(image, mask, 3, cv2.INPAINT_TELEA) return resultcv2.inpaint会根据mask区域的边界像素进行插值填充radius3控制填充范围。印章笔画细半径3够用如果金额区域同时有红色字体这个方法会把字也抹掉所以在实际项目中红色印章抑制要放在字段区域确定之后做或者只对金额字段位置生效。3. 基于Faster R-CNN的票据字段区域检测从VGG16权重加载说起3.1 为什么检测层要单独用深度学习Tesseract 这类OCR引擎只能识别文字不能回答“哪个区域是发票代码”。票据版面通常有多个字段直接整图识别出来的文本是离散无序的后续字段映射成本很高。这个项目的/lib目录里放着VGGnet_fast_rcnn_iter_150000.ckpt.data-00000-of-00001这是 VGG16 作为骨干网络、迭代训练了 15 万次的 Fast R-CNN 模型。它的作用不是识别文字而是回归出每个字段区域的边界框输出形式是(类别, 置信度, x1, y1, x2, y2)。热词里提到opencv 4.5.2 原生支持 code128这在票据场景中对应的是条形码区域的定位本质上也是先检测到条码区域再做解码和这里的区域检测思路一致。3.2 模型推理流程与输出解析模型推理部分的代码逻辑是固定的读图、缩放、前向传播、NMS去重。因为权重是TensorFlow的ckpt格式对应的是 py-faster-rcnn 那套实现工程里还带了cython_nms和gpu_nms两个模块。推理流程拆解如下。import cv2 import numpy as np import tensorflow as tf def detect_rois(sess, img, scales(600,)): h, w img.shape[:2] blob np.zeros((1, h, w, 3), dtypenp.float32) blob[0, :, :, :] img # 归一化和训练时保持一致的像素均值 pixel_means np.array([[[102.9801, 115.9465, 122.7717]]]) blob - pixel_means # 前向传播得到 rois 和 scores rois, scores sess.run( [RPN/rpn_rois:0, cls_prob:0], feed_dict{input_image:0: blob} ) # NMS 抑制重叠框 keep nms(np.hstack((rois[:, 1:5], scores[:, 1:])), 0.3) rois rois[keep] scores scores[keep] return rois, scoressess.run的两个输出节点名是py-faster-rcnn的默认命名RPN/rpn_rois是区域建议网络的输出cls_prob是类别概率。NMS 的阈值0.3在这个项目里比较合理票据字段区域相互独立、重叠较少阈值太低会漏检长条形的“发票号码”区域太高则会在表格线上产生大量冗余框。pixel_means用的是 VGG16 预训练模型的 BGR 均值顺序和cv2.imread的读取顺序一致这一步做错会导致检测框整体偏移。gpu_nms目录里的nms_kernel.cu是CUDA实现的NMS加速模块。如果运行环境没有GPU需要用python setup.py build_ext --inplace编译 CPU 版本的cython_nms否则推理时会报ImportError。我在实际跑这类项目时遇到过几次问题多出在 CUDA 版本和gpu_nms.cpp编译时的路径不对可以先检查lib/utils下有没有生成.so文件。3.3 检测结果到 OCR 输入的坐标映射检测网络输出的坐标是相对缩放后图像的OCR 阶段需要映射回预处理后的原图坐标。项目里常见的做法是记录缩放比例scale然后直接做逆变换。这里有一个容易忽略的点透视矫正发生在检测之前还是之后直接影响坐标映射公式。def map_roi_to_orig(roi, scale, M_inv): x1, y1, x2, y2 roi pts np.array([ [x1 / scale, y1 / scale], [x2 / scale, y2 / scale] ], dtypefloat32).reshape(-1, 1, 2) # 用逆透视变换矩阵把检测框角点映射回矫正前图像 mapped cv2.perspectiveTransform(pts, M_inv) return mapped.reshape(-1, 2)M_inv是cv2.getPerspectiveTransform计算出的变换矩阵的逆矩阵用np.linalg.inv(M)得到。cv2.perspectiveTransform支持批量点变换这里传入的是两个角点返回它们在原图坐标系下的位置。做这一步是为了方便后续判断识别结果有没有串行比如把“小写金额”的识别结果错误归属到“价税合计”字段。4. Tesseract识别参数与中文票据后处理psm、白名单与置信度过滤4.1 Tesseract版本选择与语言包安装识别层用的是 Tesseract这个项目里依赖的是tesseract引擎。安装时建议直接用 Tesseract 5.3.0 版本2022年末的稳定版相比老版本它的 LSTM 引擎对数字串识别率提升明显尤其是发票号码这种等宽字体。字段类型推荐 psm白名单说明发票号码 / 发票代码70123456789单行文本纯数字开票日期70123456789-年月日:日期格式固定金额小写60123456789.允许小数点收款人 / 复核人7不设置中英文混合人名备注 / 项目名称3不设置多行自由排版语言包方面中文票据需要chi_sim或chi_sim_vert安装到 tessdata 目录。注意 Tesseract 5.x 的 tessdata 路径和 4.x 不同Windows 下默认在安装目录的tessdata文件夹Linux 下在/usr/share/tesseract-ocr/5/tessdata/如果加载语言包时报Failed loading language chi_sim首先检查环境变量TESSDATA_PREFIX是否指向正确。4.2 image_to_data拿坐标和置信度不做整图字符串识别Tesseract 提供的image_to_string只能拿到纯字符串拿不到每个字段的坐标和置信度。项目里识别字段区域时应该用image_to_data配合image_to_boxes把每个词的位置和置信度一起拿回来这样可以对低置信度的结果做二次处理。import pytesseract from pytesseract import Output import cv2 def ocr_field(image, langchi_simeng, psm7, whitelistNone): config f--psm {psm} if whitelist: config f -c tessedit_char_whitelist{whitelist} # 放大两倍识别小字号数字对发票号码这类字段很有效 if image.shape[1] 200: image cv2.resize(image, None, fx2.0, fy2.0, interpolationcv2.INTER_CUBIC) data pytesseract.image_to_data(image, langlang, configconfig, output_typeOutput.DICT) result [] n len(data[text]) for i in range(n): text data[text][i].strip() conf int(data[conf][i]) if text and conf 60: x, y, w, h data[left][i], data[top][i], data[width][i], data[height][i] result.append({ text: text, conf: conf, bbox: (x, y, x w, y h) }) return resultimage_to_data返回的conf值范围是0~100低于 60 的结果在票据这种印刷体场景里通常有问题直接过滤掉可以避免把印章误识别出来的乱码写入结果。psm7对单行文本最高效长表格线或分段内容时改成psm6表示统一的文本块。whitelist的作用不只是提高准确率还能让日期字段不会识别出O和0混在一起的情况。fx2.0放大两倍的逻辑是Tesseract 对低于 10 像素高度的字符很容易漏识别放大后特征更明显识别速度会慢一点但对字段级识别无所谓。4.3 正则清洗与字段映射OCR 输出不能直接入库。中文发票里发票号码后面经常带着换行或者空格金额可能识别出千分位逗号日期可能把:认成:。项目里第二步要做规则清洗。import re def normalize_ticket_field(key, raw_text): text raw_text.replace( , ).replace(\n, ) if key invoice_no: # 只保留数字和字母发票号码通常是20位 text re.sub(r[^0-9A-Z], , text.upper()) elif key amount: # 去掉千分位逗号只保留数字和小数点 text re.sub(r[^0-9.], , text) if text.count(.) 1: text text.split(.)[0] . text.split(.)[1] elif key date: text re.sub(r[年月日], -, text) text re.sub(r[^0-9-], , text) return text清洗要配合image_to_data的坐标信息使用。比如金额字段区域识别出1234.56正则把全角逗号去掉后变成1234.56但如果区域本身混入了发票章的残留文本就需要回到坐标层面去判断文本块的中心点是否落在bbox范围内而不是简单地按顺序拼接。常见做法是检测网络输出的roi是一个字段区域用iou判断 Tesseract 返回的词块是否属于当前区域这样就不怕版面错乱导致的前后字段文本粘连。4.4 与 PaddleOCR / CRNN 的选型对比这个项目用 Tesseract主要因素是离线可运行和依赖简单。Tesseract 的识别率在清晰印刷体场景下和 PaddleOCR 的差距没有想象中那么大主要弱势在复杂版面和低分辨率图片。PaddleOCR 的 PP-OCRv4 模型对模糊、倾斜、光照不均的鲁棒性更好但模型体积大部署时还得转 infer 模型。CRNN 这类序列识别模型需要自己标注数据训练在票据字段这种定制场景反而更适合做特定字段识别但工程量至少多两倍。如果是毕设演示Tesseract 足够如果是真实生产系统PaddleOCR 替代 Tesseract 的成本主要在预处理环节后面识别部分改成paddleocr.ocr(img)即可。5. 优化与验证从 demo 到可交付的票据识别系统5.1 光照和清晰度问题的最后一道防线预处理做得再多拍糊的照片也不可能识别出正确结果。在实际项目中我会在/OCR_pic测试目录里放三组样张正午强光、阴影遮挡、昏暗环境分别统计字段识别正确率。发现某个字段识别率低于 80%先看conf分布多数低于 30 说明是图像质量问题这时候靠调 Tesseract 参数没用要回到预处理加一步cv2.createCLAHE(clipLimit2.0, tileGridSize(8,8))做对比度受限的自适应直方图均衡化。def clahe_preprocess(gray): clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) enhanced clahe.apply(gray) return enhancedclipLimit是对比度限制阈值太高会导致票据底纹噪点放大。对发票这种浅底色、深字体的图像2.0起步就够了。5.2 字段识别准确率的量化评估项目里没有现成的评估脚本建议搭一个简单的字段级准确率统计器。用公开数据集的思路自己标注 20 张图把期望结果和 OCR 结果逐一对比。可以借鉴热词里提到的 IIIT5K 这类公开基准集的做法只是不必那么大关键在评估维度字段数、正确字段数、精确率和召回率。统计脚本的伪代码如下。def evaluate(results, ground_truth): total_fields 0 correct_fields 0 for tick_id, fields in results.items(): for field_key, value in fields.items(): total_fields 1 gt ground_truth[tick_id].get(field_key) if gt and normalize(value) normalize(gt): correct_fields 1 accuracy correct_fields / total_fields return accuracy评估结果出来后按字段拆开看准确率通常问题集中在“金额”和“备注”两个字段。金额是数字密集区域Tesseract 偶尔把8识别成1备注是自由文字混淆率高。对金额字段把白名单设置成0123456789.能解决大部分问题。5.3 识别结果的落库策略最后一个建议工程细节识别结果写入数据库时不要直接覆盖原记录。票据录入的业务场景里同票号的票据可能被反复扫描两次一次清晰一次模糊模糊那次识别结果可能把原来正确的金额覆盖掉。写入时先做字段级比对只有conf更高的结果才允许覆盖图片竖拍和横拍导致的坐标偏移要提前归一化。def upsert_ticket_record(conn, ticket): # 查旧记录的每个字段置信度只有确信度更高才更新 old conn.execute( SELECT field_key, conf FROM ticket_fields WHERE ticket_id?, (ticket[ticket_id],) ).fetchall() for field in ticket[fields]: old_conf dict(old).get(field[key], 0) if field[conf] old_conf: conn.execute( UPDATE ticket_fields SET value?, conf? WHERE ticket_id? AND field_key?, (field[value], field[conf], ticket[ticket_id], field[key]) ) conn.commit()这样做还有一个好处项目答辩时可以展示“系统对重复扫描图片做了智能纠偏”而不仅仅是“识别出来”。本文还有配套的精品资源点击获取
返回列表