简介:一份基于 Python、OpenCV 与 tesseract 的中文扫描票据 OCR 识别项目,面向计算机相关专业学生、教师及企业开发者,可直接用于毕业设计、课程设计、项目初期演示或作为学习 OCR 与图像处理的进阶案例。项目为个人高分毕业设计,已获导师指导认可并通过答辩,代码经过实际运行测试,配套使用文档、说明文档及完整资料,能够帮助读者快速掌握从票据图像预处理、文字区域检测到识别输出的完整流程。压缩包共 144 个文件,以 Python 源文件为主,辅以 XML 配置、PDF/Markdown 文档、预训练模型检查点与依赖资源;整体约 105.23MB,目录结构覆盖模型、脚本、数据与文档模块,便于对照阅读和二次开发。目前已有 205 人学习下载,代码结构清晰,关键模块便于根据实际需求替换模型或调整识别流程,可直接作为课设、毕设交付内容使用。
1. 中文扫描票据OCR:为什么用Python+OpenCV+tesseract这套组合就能落地
去年帮财务做报销票据自动录入,办公桌上一线排开几百张扫描件:发票、银行回单、行程单,歪的歪、糊的糊,红章还压在金额上。试过云接口,数据要出网、按张付费,财务当场摇头;最后落地方案就是Python+OpenCV+tesseract,在本地跑中文扫描票据OCR识别。OpenCV负责把图洗干净,tesseract负责把中文认出来,Python把整套流程串成自动脚本。这个组合对固定版式票据、单机处理、成本敏感的场景非常对路,尤其适合票据种类不多、量在几万张以内的内部系统。这篇按我当时从零到能用的顺序讲:先理清预处理和识别链路,再落地代码,最后说清参数和坑。
2. 从扫描图到文本:拆解中文票据识别的图像预处理链路
2.1 扫描票据的黑匣子:为什么tesseract吃预处理这套
tesseract本身能读图,但直接把扫描票据丢进去,十有八九翻车。扫描件的特点很集中:纸张底色偏黄或偏灰、文字边缘有噪点、红章和发票监制章叠在字符上、原稿放置角度略歪。tesseract做字符分割时,是按相邻像素和灰度变化来找字符边界的,一旦背景有花纹、印章边缘、阴影,分割线就会画歪,中文经常被认成“口口口”。
做OCR识别固定模板票据,预处理甚至可以先写死:灰度、去噪、二值化、倾斜校正。这四步解决的问题完全不同。灰度是去掉彩色通道干扰,尤其让红章不再以高饱和红色块参与二值化;去噪是为了消掉扫描件的颗粒感;二值化把文字变成白底黑字,给tesseract一个几乎只有字符的输入;倾斜校正则避免文字行基线偏移导致识别成乱序。少了其中任何一步,准确率都会肉眼可见地下降。所以预处理不是锦上添花,而是识别结果的“黑匣子”入口,入口脏了,后面全白搭。
2.2 灰度、去噪、二值化与倾斜校正:OpenCV关键步骤与参数
我一般把预处理拆成四个函数,每一步都能独立调参和验证。先用一张真实扫描件跑通流程,再固化参数。核心代码如下:
import cv2 import numpy as np def preprocess_for_ocr(image_path: str): # 读入原始扫描件,BGR 格式 img = cv2.imread(image_path) if img is None: raise FileNotFoundError(f"无法读取图片: {image_path}") # 1. 灰度:去掉彩色通道,避免红章、蓝色字干扰 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 2. 高斯去噪:核大小 3x3,去颗粒同时保留字符边缘 gray = cv2.GaussianBlur(gray, (3, 3), 0) # 3. 自适应二值化:解决扫描件光照不均、中间亮四周暗 # blockSize 必须为奇数,31 表示按 31x31 邻域计算阈值 # C=10 表示从局部均值中减去 10,值越大二值化后保留的细节越少 thresh = cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, blockSize=31, C=10 ) # 4. 形态学开运算:先腐蚀后膨胀,去掉孤立的黑色噪点 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) cleaned = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel) return cleaned说明一下几个容易踩的细节。GaussianBlur的核不是越大越好,票据字号通常在 10~14 磅,3x3 足够,用 5x5 会让小字黏连。adaptiveThreshold我优先选ADAPTIVE_THRESH_GAUSSIAN_C,因为扫描件受灯光影响常有渐变底色,全局阈值cv2.threshold在边角经常把背景也二值化成黑色。blockSize=31是经验值,票据上每行文字高度大约在 30~40 像素,这能让文字行被当作局部背景,而不是被吞掉。
二值化之后,还要处理扫描件最常见的倾斜问题。倾斜超过 2~3 度,tesseract 的行切分就开始乱,常见表现是“每行前半句正常、后半句跑到上一行”。我用最小外接矩形做快速校正:
def deskew(image, gray): # 输入为 0/255 二值图,文字为黑色 # 找到所有非零像素坐标,求最小外接矩形 coords = cv2.findNonZero(cv2.bitwise_not(gray)) if coords is None: return image angle = cv2.minAreaRect(coords)[-1] # 角度范围是 [-90, 0),超过 -45 就转成锐角 if angle < -45: angle = 90 + angle h, w = image.shape[:2] center = (w // 2, h // 2) matrix = cv2.getRotationMatrix2D(center, angle, 1.0) # borderValue=255 补白,避免旋转后边角出现黑边 return cv2.warpAffine(image, matrix, (w, h), flags=cv2.INTER_CUBIC, borderValue=255)这段的算法逻辑是:扫描件上的文字和表格线共同构成一个矩形点阵,minAreaRect会算出这个点阵的倾斜角。角度在 -45 度到 0 度之间,如果是 -60 度,实际是向右倾斜了 30 度,所以要加 90 度转成锐角。INTER_CUBIC插值对文字边缘的保留比默认的线性插值好,对票据这种高密度字符尤其明显。倾斜校正是整个预处理里“性价比”最高的一步,尤其当扫描仪自动进纸时,角度经常在 1~5 度之间抖动,不校正识别准确率直接掉三成。
2.3 tesseract参数怎么给:chi_sim、psm、oem的组合
图像洗好之后,识别参数决定了结果上限。pytesseract 只是壳,真正执行的是 tesseract 引擎和中文语言包chi_sim。调用时最关键的三个参数是lang、psm和oem:
import pytesseract from PIL import Image text = pytesseract.image_to_string( Image.open("cleaned.png"), lang="chi_sim", config="--psm 6 --oem 1" ) print(text)lang="chi_sim"表示使用简体中文语言包,必须提前安装,否则报错Failed loading language 'chi_sim'。--psm是页面分割模式,票据识别最常用的几个值如下:
| psm 值 | 含义 | 适用场景 |
|---|---|---|
| 3 | 自动分页,自动检测文本块 | 版式未知、整页票据 |
| 4 | 单一文本块(可变长度) | 有固定表头、字段分散的票据 |
| 6 | 单一均匀文本块 | 发票内容区、正文区域 |
| 11 | 稀疏文本,尽量找任意文字 | 印章文字、随机备注 |
| 5 | 纵向排列的文本块 | 竖排公告、老式表格右侧说明 |
针对中文扫描票据,我默认用--psm 6。因为票据主体内容(项目、金额、编号)通常是一个连续的文本块,PSM 6 会跳过版式分析,直接按行切分,速度快、误切少。--psm 3也不是不行,但遇到骑缝章、二维码、红章,容易把干扰内容当成独立文本块,导致输出顺序混乱。--psm 11适合识别散落在凭证角落的手写备注或印章日期,但准确率比固定版式低不少,只做补充。
--oem是识别引擎模式:0 老式引擎、1 LSTM 引擎、2 两者结合、3 默认自动。现在主流中文语言包都是为 LSTM 训练,--oem 1是首选。如果发现专业名词识别率低,可以试试--oem 3,在老版和新版引擎之间自动切,某些票据字体在老引擎上反而能对上。这些参数不是一次定死,我在实际项目中会为每类票据做一组配置,放在配置文件里,后面到第六章再说怎么组织。
3. 跑通第一行中文识别:环境搭建、最小脚本与源码包的文件组织
3.1 环境和语言包安装
整套环境依赖 Python、OpenCV、tesseract 本体和中文语言包。最容易翻车的地方是:只pip install pytesseract,没装 tesseract 引擎,运行时报找不到 tesseract。另外,OpenCV 装的是opencv-python,不是opencv。安装命令如下:
# Python 依赖 pip install opencv-python pytesseract pillow # Linux (Debian/Ubuntu) 下安装 tesseract 和中文语言包 sudo apt update sudo apt install tesseract-ocr tesseract-ocr-chi-sim # macOS brew install tesseract tesseract-langWindows 下需要到 tesseract 的官方仓库安装完整的 exe,安装时勾选中文语言包。装完在 Python 里验证:
import pytesseract # 如果 tesseract 不在 PATH 中,Windows 需要显式指定路径 # pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe" print(pytesseract.get_languages(config=""))如果输出的语言列表里有chi_sim,说明引擎和语言包都就位了。tesseract-ocr-chi-sim这个包只在 Linux 的 apt 源比较全,Windows 安装器一般会包含简体中文,但有些精简版没有,需要单独补语言包。补完记住刷新环境变量,不然tesseract_cmd还是找不到新的 tessdata 路径。
3.2 最小识别脚本
装完环境,用下面这段代码验证整条链路。这个脚本不做复杂处理,目的是确认“从图像输入到文本输出”通没通:
import cv2 import pytesseract # 读取原图 img = cv2.imread("sample_bill.jpg") if img is None: raise FileNotFoundError("请检查图片路径") # 灰度 + 高斯模糊 + 自适应二值化 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray = cv2.GaussianBlur(gray, (3, 3), 0) thresh = cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 31, 10 ) # 识别中文,psm=6 适合票据正文 text = pytesseract.image_to_string( thresh, lang="chi_sim", config="--psm 6 --oem 1" ) print(text)逻辑很清楚:先用 OpenCV 读图并二值化,把预处理结果直接传给 pytesseract。这里有个容易被忽略的地方:pytesseract 对 numpy 数组和二值图很敏感,如果输入是三维彩色数组,tesseract 会自动转灰度导致预处理白做。所以这里提前转了灰度再传。psm 6是给一整块连续文本用的,如果扫描件上有多个独立区域,先把这块跑通,再进入第四章的 ROI 方案。
参数怎么调?blockSize=31, C=10是默认起手式。如果票据底色偏白、字迹清晰,可以改成blockSize=41, C=15,减少背景纹理;如果票据是复写纸,底色反灰、字迹浅,把 C 降到 5,保留更多暗色细节。二值化之后建议顺手cv2.imwrite("preview.png", thresh)存一份预览,肉眼看不清的字,tesseract 大概率也认不出来。
3.3 源码包的常见文件组织
项目里拿了别人的源码包或自己重构时,我一般按以下方式组织文件,方便调试也方便交接:
project/ ├── ocr/ │ ├── __init__.py │ ├── preprocess.py # 图像预处理 │ └── recognizer.py # tesseract 封装 ├── config/ │ └── ocr.yaml # 每类票据的 psm/oem/ROI 参数 ├── docs/ │ └── 使用说明.md ├── samples/ │ ├── normal_bill.jpg │ └── rotated_bill.jpg ├── requirements.txt └── main.py拿到这类源码包,先看requirements.txt和使用说明,确认作者用了哪个版本的 OpenCV 和 tesseract。我看到很多人直接pip install -r requirements.txt跑不起来,原因大都是 tesseract 语言包缺失,这不怪源码,是该装到系统层面的依赖没装。preprocess.py和recognizer.py分开写的好处是,单张图识别效果不对时,可以先跑预处理存图,再手工用 tesseract 命令行试参数,不用每次跑整套 Python 流程。
4. 把“能识别”做成“能录入”:ROI定位、字段提取与后处理
4.1 ROI裁切与透视变换
整页识别只是验证链路,真正做到能录入,必须把识别范围锁到关键字段区域。电子发票版式固定,但扫描票据受放纸位置影响,同一个“金额合计”字段在整页中的像素坐标会漂移 10~50 像素。直接按绝对坐标裁切不靠谱,要先找到票据外边框或表格线的位置。
我的做法是先用模板比对标定四个角点,然后做透视变换拉正。四角可以来自人工标注,也可以由最大外轮廓自动得到:
import cv2 import numpy as np def find_bill_corners(binary): # 找出面积最大的四边形轮廓,当作票据外框 contours, _ = cv2.findContours( binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) if not contours: raise ValueError("没有找到票据外框") contour = max(contours, key=cv2.contourArea) epsilon = 0.02 * cv2.arcLength(contour, True) approx = cv2.approxPolyDP(contour, epsilon, True) if len(approx) != 4: # 不是规则四边形,退回最小外接矩形 rect = cv2.minAreaRect(contour) return cv2.boxPoints(rect) return approx.reshape(4, 2).astype(np.float32) def warp_roi(image, corners): # 按角点排序:左上、右上、右下、左下 pts = np.array(corners, dtype=np.float32) rect = np.zeros((4, 2), dtype=np.float32) s = pts.sum(axis=1) rect[0] = pts[np.argmin(s)] # 左上 rect[2] = pts[np.argmax(s)] # 右下 diff = np.diff(pts, axis=1).reshape(-1) rect[1] = pts[np.argmin(diff)] # 右上 rect[3] = pts[np.argmax(diff)] # 左下 width = int(max(np.linalg.norm(rect[1] - rect[0]), np.linalg.norm(rect[2] - rect[3]))) height = int(max(np.linalg.norm(rect[3] - rect[0]), np.linalg.norm(rect[2] - rect[1]))) dst = np.array([[0, 0], [width - 1, 0], [width - 1, height - 1], [0, height - 1]], dtype=np.float32) matrix = cv2.getPerspectiveTransform(rect, dst) return cv2.warpPerspective(image, matrix, (width, height))透视变换的价值不止是“摆正”,更是把拍摄角度造成的梯形失真去掉。普通扫描仪很少出现这种失真,但用手机拍的高拍仪图片,俯仰角稍微偏一点,票据上下宽度就不同,文字笔画也会粗细不均。做过透视变换后,后续ROI坐标才能在多次扫描之间通用。
拿到拉正后的图像,再按模板坐标裁切字段区域。比如“发票号码”通常在外框右上角,“小写金额”在表格右下角,我可以把坐标写进配置。ROI裁切时注意多留 5~10 像素边距,否则字符贴边会被裁掉,切出来的图再交给 tesseract 时,记得把分辨率放大到至少 300 DPI 对应的像素尺寸,cv2.resize用插值放大 2 倍后再识别更稳。
4.2 竖排文本和表格怎么切
有相当一部分老式票据的备注栏是竖排的。tesseract 的--psm 5专门处理纵向排列文本,但前提是输入图像里只有这一列竖排文字。我的做法是把竖排区域裁出来,旋转 90 度变成横排,再用--psm 6识别,这样对中文语言包更友好。竖排区域如果混在横排表格里,旋转反而会被周围文字干扰,宁可先裁切再旋转,也不要整页旋转。
表格是票据识别的另一大难点。多栏明细的抬头、数量和单价各占各列,tesseract 默认按行输出,结果往往是把同一行的三个列值合并成一串。我一般用水平投影切行,再做列切分:
def split_table_rows(binary): # 对二值图做水平投影,统计每行黑色像素数量 h, w = binary.shape row_hist = cv2.reduce(binary, 1, cv2.REDUCE_AVG).flatten() rows = [] in_row = False start = 0 for y in range(h): # 黑色像素多的行属于表格内容 if row_hist[y] < 0.2: if not in_row: in_row = True start = y else: if in_row: rows.append((start, y)) in_row = False return rows这段代码把二值图中黑色像素密度高的连续区段当作一个文本行。阈值0.2可以根据表格线和文字粗细调整,我建议先用cv2.imshow把行分割结果可视化再批量应用。分出来的每一行其实还能继续做竖直投影,按列分隔线切成单元格。表格处理是不是必须?看业务需求,如果只需要票面总金额,不录明细,那就不切表格,直接识别 ROI 区域反而省事。
4.3 正则与格式化的后处理
tesseract 返回的文本永远是“原始字符串”,哪怕识别完全准确,也可能带着全角空格、半角括号混用、金额多了小数点。直接存库前必须做后处理。固定字段的提取用正则就够,比如发票号码、日期、金额:
import re def extract_values(text): result = {} # 发票号码:一般 8~20 位数字,可能带 - 或 后 8 位 m = re.search(r'发票号码[::\s]*([0-9]{8,20})', text) if m: result['invoice_no'] = m.group(1) # 日期:2024-01-05 / 2024年1月5日 m = re.search(r'(\d{4})[年\-/](\d{1,2})[月\-/](\d{1,2})日?', text) if m: result['date'] = f"{m.group(1)}-{int(m.group(2)):02d}-{int(m.group(3)):02d}" # 金额:保留两位小数,兼容千分位 m = re.search(r'金额[::\s]*([0-9,]+\.?\d{0,2})', text) if m: result['amount'] = float(m.group(1).replace(',', '')) return result这里有几个使用习惯要说清楚。正则里的[::\s]*是为了吸收识别出的冒号全角/半角不一致,以及可能多出来的空格。金额关键词不能写死为合计金额,因为扫描票据上印的可能是“金额合计(小写)”,要自己在测试集上看实际 OCR 输出哪种变体,再修改正则。日期里的年和-都要覆盖,因为老年票据用中文年月日,新票据用横线。
后处理还有一个容易被遗忘的步骤:全角转半角。如果下游对接是 SAP 或者 ERP,全角数字会被当成异常字符。干净的做法是统一转半角:
def full_to_half(s): result = [] for ch in s: code = ord(ch) if code == 0x3000: code = 32 elif 0xFF01 <= code <= 0xFF5E: code -= 0xFEE0 result.append(chr(code)) return ''.join(result)这段不是识别逻辑,但能避免字符编码层面的奇怪错误。票据里的“FP”全角字母在 OCR 结果里经常出现,转成“FP”后数据库字段才认。
5. 中文扫描票据识别常见问题排查:五条血泪踩坑记录
5.1 识别结果全是乱码或方块
现象:tesseract 跑完,输出里没有中文,全是????、方框或不认识的符号。
原因九成是中文语言包没装或者路径不对。pytesseract 默认去找系统 tessdata 目录,Windows 下如果语言包没被放到 tessdata 里,chi_sim根本加载不了。顺便检查一下图片本身,如果票据扫描时已经是二值化的低质量阈值图,背景和文字混在一起,也会输出一堆噪声。
解决分两步:先验证语言包环境,再验证图像质量。在 Python 里打印pytesseract.get_languages(),没有chi_sim就去补装;第二步把预处理后的图像保存成图片直接肉眼看,如果人眼都认不出内容,就别怪 tesseract。还有一个小概率原因:传给 pytesseract 的图像是彩色 BGR 数组,而灰度转换差错导致两点化不正常。统一在预处理里用cv2.cvtColor转灰度,不要依赖 tesseract 内置转换。
5.2 中文能认但数字字母老错
现象:整段文字识别得七七八八,但发票号码和金额数字频繁出错,把 8 认成 3,把 0 认成 9。
原因:数字和字母在票据上往往是等宽打印体,笔画间距小,二值化后字符边缘粘连,tesseract 的分割不确定。另一个原因是 psm 模式把整页当成文本块,数字区域的前后文被表格线干扰,切到了错误的字符边界。
解决方式是“白名单 + 放大”。“纯数字字段”用字符白名单限定识别范围:
amount_text = pytesseract.image_to_string( amount_roi, lang="chi_sim", config="--psm 7 -c tessedit_char_whitelist=0123456789.," )--psm 7表示单行文本,适合这种金额字段;-c tessedit_char_whitelist让 tesseract 只输出白名单内的字符。注意白名单对chi_sim语言包同样生效,中文不会出现在输出里。如果字符仍偏小,用cv2.resize将 ROI 放大 2~3 倍,插值用INTER_CUBIC,字符高度至少到 30 像素。千万别放大完不做二值化,直接传原图,tesseract 内部处理大彩色图反而更慢。
5.3 扫描件歪斜导致漏行
现象:识别输出的行顺序错乱,比如第一行文字跑到第三行末尾,有时整行漏掉。
原因:票据放置角度偏移,或者扫描软件开启自动裁边后把部分边缘文字切掉。tesseract 在分行时按文本框水平线积分,歪斜角度超过 3 度时,行与行之间会有粘连,导致错行。
解决:保留第二章的deskew函数,并且在一进 OCR 前先做倾斜校正。有一点要注意,minAreaRect对没有表格线的纯文本票据可能估算不准,因为字符之间没有成片的连通域。这种情况可以先做膨胀,把文字行连成块再算角度:
def robust_deskew_angle(binary): kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (20, 5)) dilated = cv2.dilate(binary, kernel, iterations=3) coords = cv2.findNonZero(cv2.bitwise_not(dilated)) angle = cv2.minAreaRect(coords)[-1] if angle < -45: angle = 90 + angle return angle膨胀核的水平方向取 20、垂直方向取 5,能把一行行文字横向连接成一个长条矩形,这样最小外接矩形的角度才稳定。这是个很实用的技巧,我第一次在扫描票据上跑通就是靠它解决了 80% 的歪斜漏行问题。
5.4 tesseract报错 Failed loading language chi_sim
现象:运行时报错,明确提到Failed loading language 'chi_sim',或Tesseract couldn't load any languages。
原因:语言包文件没安装,或者TESSDATA_PREFIX环境变量指向错误目录。Linux 下tesseract-ocr-chi-sim包安装后语言包在/usr/share/tesseract-ocr/4.00/tessdata这类路径;Windows 下安装器会装到 Tesseract 安装目录下的tessdata。但 Python 进程的环境变量可能没有继承这个路径。
解决:不要只靠默认路径,在代码里显式指定 tessdata 目录:
import pytesseract pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe" pytesseract.pytesseract.tessdata_dir = r"C:\Program Files\Tesseract-OCR\tessdata"这样能绕过操作系统 PATH 和系统环境变量带来的“玄学”问题。注意tessdata_dir属性在 pytesseract 新版本里叫tessdata_dir,旧版本可能不支持,就改为在调用时传--tessdata-dir参数。改完重启 Python 进程再验证。
5.5 处理大图内存飙升、耗时过长
现象:一张扫描件 2000 万像素,预处理二值化没问题,但 tesseract 识别要十几秒甚至几十秒,或者内存直接涨到几个 G。
原因:扫描仪设置的分辨率太高,默认 600 DPI 对 OCR 来说过度。tesseract 的 LSTM 引擎需要在 CPU 上跑 CNN,图片像素越多,计算量按面积增长。而且 OpenCV 的adaptiveThreshold对几千万像素大图会创建多个中间矩阵,内存压力很大。
解决:统一在预处理前做一次降采样。对票据,长边压到 2000~3000 像素已经足够;分辨率不够时优先放大 ROI 而不是整页放大。代码很短:
def resize_long_edge(img, max_len=2400): h, w = img.shape[:2] scale = min(max_len / h, max_len / w) if scale >= 1.0: return img new_size = (int(w * scale), int(h * scale)) return cv2.resize(img, new_size, interpolation=cv2.INTER_AREA)INTER_AREA在缩小图像时能保留更多结构信息,不会像INTER_LINEAR那样出现锯齿。这条经验适用所有扫描 OCR 场景:先缩图再二值化,耗时能从十几秒降到两秒以内。内存压力主要来自自适应二值化,如果缩到 2400px 还报内存,可以改为先全局阈值做粗过滤,再对 ROI 区域单独自适应二值化。
6. 进阶用法:批处理识别与人工校对闭环
6.1 批量遍历目录并记录置信度
单张票据跑通后,最终要面对整批文件。我习惯用pytesseract.image_to_data拿到每个字符的置信度,并把平均置信度和原始文本一起落盘。这样哪个文件识别得不好,一眼就能挑出来:
import os import csv import time import pytesseract from PIL import Image import cv2 def batch_ocr_folder(folder, out_csv): with open(out_csv, "w", encoding="utf-8", newline="") as f: writer = csv.writer(f) writer.writerow(["file", "text", "avg_conf", "time_sec"]) for name in sorted(os.listdir(folder)): if not name.lower().endswith((".jpg", ".png", ".jpeg")): continue path = os.path.join(folder, name) img = cv2.imread(path) if img is None: continue # 降采样再预处理,避免大图卡死 img = cv2.resize(img, (2400, int(2400 * img.shape[0] / img.shape[1]))) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray = cv2.GaussianBlur(gray, (3, 3), 0) thresh = cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 31, 10 ) t0 = time.time() data = pytesseract.image_to_data( thresh, lang="chi_sim", config="--psm 6 --oem 1", output_type=pytesseract.Output.DICT ) confs = [int(c) for c in data["conf"] if c != "-1"] text = "".join([t for t in data["text"] if t.strip()]) avg_conf = sum(confs) / len(confs) if confs else 0 writer.writerow([name, text, round(avg_conf, 1), round(time.time() - t0, 2)])这段代码把预处理和识别串起来了,同时拿到每个词的置信度。image_to_data返回的 dict 里,conf和text按行/词对齐,过滤掉-1的无效值再算平均。CSV 里avg_conf低于 60 的文件就是重点关注对象。批量跑之前,先拿 10 张“好图”测一遍,确认平均置信度能在 75 以上,再大规模执行。
6.2 用可配置参数文件管住识别变量
票据识别最大的维护成本不是代码,而是不同票据类型对应的参数。我最终把所有可调项都放进一个 YAML 文件,避免改参数要翻代码:
bill_type: restaurant_invoice lang: chi_sim psm: 6 oem: 1 preprocess: blockSize: 31 C: 10 max_long_edge: 2400 fields: invoice_no: roi: [120, 80, 400, 120] # x, y, w, h whitelist: "0123456789-" amount: roi: [300, 600, 500, 650] whitelist: "0123456789."代码里不再写死参数,而是启动时加载配置:
import yaml with open("config/ocr.yaml", encoding="utf-8") as fp: cfg = yaml.safe_load(fp) config_str = ( f"--psm {cfg['psm']} --oem {cfg['oem']}" f" -c tessedit_char_whitelist={cfg['fields']['amount']['whitelist']}" )这有点像给识别流程留了一颗“后悔药”:新来一版票据,先复制配置改成新类型,跑测试集对比正确率,确定可行后再整体切换,不会动到生产代码。字段级校准也能用配置做,比如发票号码识别差一位,就去核对 ROI 坐标是不是偏了。这个习惯帮我省了很多次“今天能跑、明天不能跑”的尴尬。
最后说一个我自己的教训:批量识别结果直接入库前,永远先抽样人工复核,别迷信置信度。置信度是 tesseract 内部概率映射出来的,对某个特定字体可能系统性偏高。我会把低置信度和高置信度的结果各抽 10 条打印出来对一眼,确认没有隐性错字后才跑全量。这套 Python+OpenCV+tesseract 的方案说到底没有魔法,它把脏活累活自动化了,但关键字段的最终校验,还得靠业务规则和人工把关。希望这篇能帮你少踩几个我踩过的坑。
本文还有配套的精品资源,点击获取