
简介这份资源是面向高校计算机、软件工程等专业学生的毕业设计完整项目包主题为基于OpenCV与Tesseract的中文扫描票据OCR识别适合正在准备毕设、需要OCR实战案例或想学习图像预处理与文字识别流程的开发者参考。压缩包共143个文件约105.26MB以83个Python源码为核心辅以XML配置、PNG示例图、PDF说明文档及C/C、Cython加速文件并包含VGGnet_fast_rcnn模型检查点与训练数据覆盖从票据图像预处理、版面分析到文字识别的完整链路。项目已获导师认可并高分通过代码经测试可正常运行功能完整。目前已有540人学习下载。读者可据此掌握OpenCV图像增强、Tesseract中文识别配置、深度学习检测模型调用等关键环节并借助现成目录结构与数据快速复现实验、撰写论文或二次开发省去从零搭建环境的时间成本。1. 票据 OCR 这件事为什么 OpenCV 加 Tesseract 依然是毕业设计里最稳的起手式手里拿到一张增值税发票或者出租车票的扫描件想把它变成结构化数据第一反应往往是找个现成的云 API 调一下。但如果你正在做毕业设计或者需要在离线环境里跑一套自己能完全掌控的流程OpenCV 加 Tesseract 这套组合依然是绕不开的起点。它不依赖网络不需要申请密钥所有中间结果都能可视化调试这对需要写论文、画流程图、分析算法步骤的场景来说比调一个黑盒接口有价值得多。这篇笔记拆解的就是这条链路从一张扫描票据图片输入到 OpenCV 做预处理再到 Tesseract 做中文识别最后拿到可用的文本行。适合正在做 OCR 相关毕业设计、需要自己搭一套可解释流程的读者也适合想理解传统 OCR 管线到底在干什么的工程师。2. 先把管线拆开OpenCV 预处理到底在票据上做了什么2.1 票据扫描件的三个典型退化问题扫描票据和自然场景文字识别是两回事。自然场景里你要处理透视、光照、任意角度而票据扫描件的问题更集中第一是背景噪声扫描仪玻璃上的灰尘、纸张本身的纹理、复印产生的底灰都会让二值化后的图像出现大量孤立噪点第二是倾斜进纸时哪怕偏了两三度Tesseract 的行分割就会开始丢字或者把两行粘在一起第三是分辨率不匹配很多扫描件默认 200 DPI中文字符笔画密集这个分辨率下“口”和“囗”在像素层面几乎没区别。我一般会先写一个诊断脚本把图片的尺寸、DPI 估计值、灰度直方图一次性打出来再决定后面用哪套参数。不要上来就套一套固定流程票据类型不同预处理策略差很多。import cv2 import numpy as np def diagnose(image_path): img cv2.imread(image_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) print(f尺寸: {img.shape[1]}x{img.shape[0]}) print(f灰度均值: {gray.mean():.1f}, 标准差: {gray.std():.1f}) # 用拉普拉斯方差估计模糊程度 lap_var cv2.Laplacian(gray, cv2.CV_64F).var() print(f拉普拉斯方差: {lap_var:.1f} (低于100通常偏模糊)) # 估计倾斜角用最小外接矩形 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU) coords np.column_stack(np.where(binary 0)) if len(coords) 100: angle cv2.minAreaRect(coords)[-1] if angle -45: angle 90 angle print(f估计倾斜角: {angle:.2f} 度) return img, gray diagnose(invoice_001.jpg)这段代码做三件事输出尺寸和灰度统计、用拉普拉斯方差判断模糊程度、用最小外接矩形估计倾斜角。拉普拉斯方差低于 100 的时候说明图像高频信息不足要么重新扫描要么先做锐化再进后续流程。倾斜角超过 1.5 度就值得做纠偏否则 Tesseract 的版面分析会开始出错。2.2 灰度化、二值化和去噪的先后顺序很多人习惯先二值化再去噪这在票据场景下容易翻车。原因是票据上的印章、手写签名、表格线在二值化后会和文字粘连去噪算法分不清哪些是噪声哪些是笔画。我一般会走这条路径灰度化 → 自适应直方图均衡CLAHE→ 轻度高斯模糊 → 自适应二值化 → 形态学开运算去孤立点。def preprocess(gray): # CLAHE 限制对比度自适应直方图均衡clipLimit 控制过增强 clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) enhanced clahe.apply(gray) # 高斯模糊核用 3x3票据文字笔画细核太大会糊 blurred cv2.GaussianBlur(enhanced, (3, 3), 0) # 自适应二值化blockSize 取 31C 取 10 binary cv2.adaptiveThreshold( blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, blockSize31, C10 ) # 开运算去掉小于 2x2 的孤立噪点 kernel np.ones((2, 2), np.uint8) cleaned cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) return cleanedCLAHE 的 clipLimit 设 2.0 是经验值设到 4.0 以上会把纸张纹理也增强出来。blockSize 必须取奇数31 对应大约 31 像素的局部窗口票据文字高度一般在 20 到 40 像素之间这个窗口能覆盖一个字符的局部背景。C 值是从局部均值里减去的常数设 10 是为了在背景偏灰的扫描件上仍然能把文字分出来。如果二值化后文字断裂严重先把 C 降到 5 试试如果背景噪点太多把 blockSize 提到 51。2.3 倾斜校正和 ROI 裁剪的实操参数倾斜校正用霍夫变换或者最小外接矩形都行但票据场景下我更倾向用霍夫变换找表格线因为票据通常有水平边框用边框角度做校正比用文字块更稳。def deskew(binary): # 霍夫变换找直线阈值设 100最小线长设图像宽度的 1/4 lines cv2.HoughLinesP(binary, 1, np.pi / 180, threshold100, minLineLengthbinary.shape[1] // 4, maxLineGap10) angles [] if lines is not None: for line in lines: x1, y1, x2, y2 line[0] if x2 - x1 0: continue angle np.degrees(np.arctan2(y2 - y1, x2 - x1)) # 只保留接近水平的线 if abs(angle) 10: angles.append(angle) if not angles: return binary, 0.0 median_angle np.median(angles) h, w binary.shape M cv2.getRotationMatrix2D((w // 2, h // 2), median_angle, 1.0) rotated cv2.warpAffine(binary, M, (w, h), flagscv2.INTER_CUBIC, borderModecv2.BORDER_REPLICATE) return rotated, median_angleminLineLength 设成图像宽度的四分之一是为了过滤掉文字笔画形成的短线。maxLineGap 设 10 是允许表格线有轻微断裂。旋转时用 INTER_CUBIC 插值比默认的线性插值在文字边缘上更干净。borderMode 用 REPLICATE 避免旋转后出现黑边被 Tesseract 当成内容。ROI 裁剪这块如果票据版式固定直接写死坐标区域最省事如果版式有变化用轮廓检测找最大矩形区域再裁。我一般会先跑一遍全图识别看 Tesseract 在哪些区域置信度低再针对性地裁。3. Tesseract 中文识别的配置、训练数据和调用方式3.1 中文识别必须换掉的三个默认配置Tesseract 默认是英文识别直接拿来做中文票据结果基本没法看。必须改的三个地方语言包换成 chi_sim、页面分割模式改成 6 或 4、保留空格的方式调整。语言包方面chi_sim 是简体中文的标准训练数据下载后放到 TESSDATA_PREFIX 指向的目录里。页面分割模式--psm对票据特别关键默认的 3 是全自动分割在票据这种多栏、有表格线的版面上会乱切6 是假设整块文本适合票据主体区域4 是假设单列可变大小文本适合裁剪后的单行字段。import pytesseract from PIL import Image # 指定 tesseract 可执行文件路径Windows 下需要 pytesseract.pytesseract.tesseract_cmd rC:\Program Files\Tesseract-OCR\tesseract.exe def ocr_chinese(image, psm6): config f--psm {psm} --oem 1 -l chi_sim # oem 1 表示只使用 LSTM 引擎比默认的混合引擎在中文上更稳 text pytesseract.image_to_string(image, configconfig) return text # 对预处理后的二值图做识别 result ocr_chinese(cleaned, psm6) print(result)--oem 1 这个参数容易被忽略。Tesseract 4 以后默认用 LSTM 加传统引擎的混合模式但在中文票据上传统引擎的贡献基本是负的强制用 LSTM 反而更干净。--psm 6 适合整张票据的主体识别如果你已经裁出了单行金额或者日期换成 --psm 7 会更准。3.2 用 image_to_data 拿到每个词的置信度和坐标只拿纯文本是不够的票据识别需要知道哪个词在哪个位置、置信度多少才能做后续的字段抽取和校验。image_to_data 返回的是 TSV 格式包含每个词的边界框和置信度。def ocr_with_boxes(image): data pytesseract.image_to_data( image, config--psm 6 --oem 1 -l chi_sim, output_typepytesseract.Output.DICT ) results [] n len(data[text]) for i in range(n): text data[text][i].strip() conf int(data[conf][i]) if text and conf 30: # 置信度低于 30 的直接丢弃 results.append({ text: text, conf: conf, left: data[left][i], top: data[top][i], width: data[width][i], height: data[height][i] }) return results boxes ocr_with_boxes(cleaned) for b in boxes[:10]: print(f{b[text]} (置信度 {b[conf]}) ({b[left]},{b[top]}))置信度阈值设 30 是个保守值。票据上的关键字段比如金额、发票号如果识别置信度低于 60基本可以判定这一帧有问题需要回退到原图重新裁剪或者调整预处理参数。坐标信息可以用来做版面还原把识别结果按 top 坐标排序同一行的词合并就能还原出票据的文本行结构。3.3 中文训练数据的微调路径Tesseract 自带的 chi_sim 是在通用语料上训练的对票据里的特殊字体、针式打印字体、手写体识别率会明显下降。如果毕业设计需要体现“优化”环节微调训练数据是一个可写的点。流程是用 jTessBoxEditor 或者脚本生成 box 文件标注字符坐标和内容然后用 tesstrain 脚本重新训练。# 生成训练数据的基本流程Linux 环境 # 1. 准备字体图片和对应的 ground truth 文本 # 2. 生成 box 文件 tesseract font_image.tif font_name -l chi_sim batch.nochop makebox # 3. 人工校正 box 文件后执行训练 tesstrain.sh --fonts_dir /usr/share/fonts \ --lang chi_sim \ --linedata_only \ --noextract_font_properties \ --training_text ./training_text.txt \ --output_dir ./output这条路径的坑在于训练数据量不够的时候微调后的模型在通用场景下会退化。我一般建议至少准备 500 到 1000 行标注数据而且要和测试集来自同一批扫描设备。如果只是毕业设计演示用自带的 chi_sim 加上针对性的预处理效果已经能覆盖大部分票据类型。4. 从识别结果到结构化字段后处理才是真正拉开差距的地方4.1 用正则和关键词锚点抽取发票号、金额、日期Tesseract 输出的文本是带噪声的直接拿去做字段匹配命中率很低。我一般会先做一轮清洗去掉连续空格、把全角数字转半角、修正常见混淆字符比如“O”和“0”、“l”和“1”。然后用关键词锚点定位字段。import re def normalize_text(text): # 全角转半角 text text.translate(str.maketrans( , 0123456789.,: )) # 去掉多余空格 text re.sub(r\s, , text) # 修正常见 OCR 混淆 text text.replace(O, 0).replace(l, 1) return text def extract_fields(text): text normalize_text(text) fields {} # 发票号通常 8 到 20 位数字 m re.search(r发票代码[:]?\s*(\d{10,12}), text) if m: fields[invoice_code] m.group(1) # 金额匹配“价税合计”后面的数字 m re.search(r价税合计.*?[¥]?\s*([\d,]\.\d{2}), text) if m: fields[total] m.group(1).replace(,, ) # 日期 m re.search(r(\d{4})\s*年\s*(\d{1,2})\s*月\s*(\d{1,2})\s*日, text) if m: fields[date] f{m.group(1)}-{m.group(2).zfill(2)}-{m.group(3).zfill(2)} return fields sample 发票代码011002000311 价税合计 ¥1,234.56 2023年5月12日 print(extract_fields(sample))正则里的.*?是非贪婪匹配防止跨字段吞掉内容。金额匹配里同时考虑了 ¥ 和 两种符号。日期做了补零处理方便后续入库。这套规则不是万能的票据版式一变就要调但作为毕业设计里的“后处理模块”逻辑清晰、可解释性强写进论文里比调 API 有内容得多。4.2 置信度过滤和二次识别策略不是所有识别结果都值得信任。我的做法是对置信度低于阈值的字段回到原图对应坐标区域用更小的 ROI 和更严格的预处理参数重新识别一次。二次识别时把 --psm 改成 7单行往往能把第一次因为版面分割错误导致的低置信度结果救回来。def reocr_field(image, box, original_conf, threshold60): if original_conf threshold: return None # 不需要二次识别 x, y, w, h box[left], box[top], box[width], box[height] # 向外扩 5 像素避免切掉笔画边缘 pad 5 roi image[max(0, y-pad):yhpad, max(0, x-pad):xwpad] if roi.size 0: return None # 二次识别用单行模式 text pytesseract.image_to_string( roi, config--psm 7 --oem 1 -l chi_sim ).strip() return text if text else None扩边 5 像素是为了防止第一次的边界框切掉了字符边缘。二次识别只对低置信度字段触发不会拖慢整体速度。如果二次识别结果和第一次差异很大说明这个区域本身质量有问题应该在结果里标记为“需人工复核”而不是强行选一个。5. 避坑票据 OCR 从跑通到能用之间隔着的五个坑5.1 二值化后文字断裂Tesseract 把“口”认成“囗”现象是识别结果里出现大量形近字错误尤其是笔画密集的字符。原因通常是自适应二值化的 blockSize 太小局部窗口只覆盖了笔画的一部分导致笔画中间被判定为背景。解决方法是把 blockSize 从 31 提到 51 甚至 71同时把 C 值从 10 降到 5让二值化更保守。如果还是断裂在二值化前加一步 3x3 的高斯模糊让笔画边缘更平滑。5.2 表格线被当成文字识别结果里混入大量竖线票据的表格线在二值化后是连续的长直线Tesseract 会把它们当成字符或者字符的一部分。解决方法是做形态学操作提取长直线然后从二值图里减掉。用 1x50 的水平核和 50x1 的垂直核分别做开运算得到表格线掩膜再用 bitwise_not 和原图做与运算。def remove_table_lines(binary): # 提取水平线 h_kernel cv2.getStructuringElement(cv2.MORPH_RECT, (50, 1)) h_lines cv2.morphologyEx(binary, cv2.MORPH_OPEN, h_kernel) # 提取垂直线 v_kernel cv2.getStructuringElement(cv2.MORPH_RECT, (1, 50)) v_lines cv2.morphologyEx(binary, cv2.MORPH_OPEN, v_kernel) # 合并线条掩膜 lines cv2.add(h_lines, v_lines) # 从原图中减去线条 result cv2.subtract(binary, lines) return result核的长度 50 是根据票据表格线的典型长度定的如果表格线更短降到 30 也能用。减掉线条后文字笔画如果有和线条重叠的部分会留下缺口但 Tesseract 对缺口的容忍度比对着线条要高得多。5.3 中文语言包路径不对报错 “Failed loading language chi_sim”这个报错在 Windows 上特别常见。Tesseract 找语言包的路径由 TESSDATA_PREFIX 环境变量决定如果没设或者设错了就找不到 chi_sim.traineddata。解决方法是先确认 chi_sim.traineddata 文件的实际位置然后设置环境变量或者在代码里通过 --tessdata-dir 参数显式指定。config --psm 6 --oem 1 -l chi_sim --tessdata-dir C:/Tesseract-OCR/tessdata注意路径里用正斜杠或者双反斜杠单反斜杠在 Python 字符串里会被当成转义字符。如果还是报错检查 traineddata 文件的版本和 Tesseract 主程序版本是否匹配4.x 的语言包不能用在 5.x 上。5.4 识别速度慢到无法接受单张票据超过 10 秒Tesseract 在 CPU 上跑中文识别确实不快但单张票据超过 10 秒通常是有问题的。最常见的原因是图像分辨率太高比如 600 DPI 扫描的 A4 票据像素量是 200 DPI 的 9 倍。解决方法是在预处理阶段就把图像缩放到宽度 1500 到 2000 像素之间这个范围对中文识别足够再大就是浪费。另一个原因是 --psm 设成了 3全自动Tesseract 会尝试多种版面分割策略耗时成倍增加。票据场景直接用 --psm 6 或 4。5.5 训练数据微调后通用场景识别率反而下降这是过拟合的典型表现。微调时用的票据样本字体单一、版式固定模型把训练集的特征当成了通用特征。解决方法是混合训练在微调数据里掺入 20% 到 30% 的通用中文文本行让模型保持对通用字形的记忆。另外微调时的学习率不要设太高Tesseract 的 tesstrain 脚本里可以通过 --learning_rate 控制默认值 0.0001 在票据微调场景下通常够用调到 0.001 就容易过拟合。6. 把识别结果用起来批量处理、结果校验和一套可复现的评估方法单张票据跑通之后下一步是批量处理。我一般会写一个带进度输出的批处理脚本把预处理、识别、后处理串起来每张图输出一个 JSON 结果文件同时记录耗时和平均置信度。这样跑完一批之后可以直接按置信度排序优先人工复核低置信度的那些。import os import json import time def batch_process(input_dir, output_dir): os.makedirs(output_dir, exist_okTrue) files [f for f in os.listdir(input_dir) if f.lower().endswith((.jpg, .png, .tif))] summary [] for fname in files: t0 time.time() img cv2.imread(os.path.join(input_dir, fname)) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) cleaned preprocess(gray) cleaned, angle deskew(cleaned) cleaned remove_table_lines(cleaned) boxes ocr_with_boxes(cleaned) text .join(b[text] for b in boxes) fields extract_fields(text) avg_conf sum(b[conf] for b in boxes) / max(len(boxes), 1) result { file: fname, fields: fields, avg_conf: round(avg_conf, 1), skew_angle: round(angle, 2), elapsed: round(time.time() - t0, 2) } summary.append(result) with open(os.path.join(output_dir, fname .json), w, encodingutf-8) as fp: json.dump(result, fp, ensure_asciiFalse, indent2) # 输出汇总按置信度升序排列方便优先复核 summary.sort(keylambda x: x[avg_conf]) with open(os.path.join(output_dir, _summary.json), w, encodingutf-8) as fp: json.dump(summary, fp, ensure_asciiFalse, indent2) return summary results batch_process(./invoices, ./output) for r in results[:5]: print(f{r[file]}: 置信度 {r[avg_conf]}, 耗时 {r[elapsed]}s)这个脚本的关键在于汇总文件按平均置信度升序排列跑完一批之后你打开 _summary.json排在最前面的就是最可能需要人工干预的。评估方法上如果手里有标注好的 ground truth可以算字符级准确率CAR和字段级准确率FAR。字符级准确率用编辑距离算字段级准确率就是抽出来的金额、日期、发票号完全正确的比例。我一般会跑一个 50 张票据的小测试集如果字段级准确率低于 70%先别急着调 Tesseract 参数回头检查预处理阶段的二值化效果大部分问题出在那里。还有一个习惯每次调整预处理参数或者 Tesseract 配置都把参数和对应的评估结果记在一个表格里。票据 OCR 的参数之间是耦合的改了 blockSize 可能影响倾斜校正的效果没有记录的话调着调着就忘了哪组参数最好。这个习惯帮我省了很多后悔药也希望帮到你。本文还有配套的精品资源点击获取