简介:这是一套可直接运行的Python OCR图片识别项目源码,核心基于rapidOCR改造而来,面向需要快速集成图片转文字能力的开发者与学习者。项目内置/engine/ocr接口,可接收http链接、本地文件、文件流等多种形式的图片输入,识别后返回带标记的HTML结构,既能直接嵌入其他页面,也支持复制复用,适合做文档数字化、票据识别或网页端文字提取等场景。压缩包共46个文件,约14.26MB,包含23个py源码、17个pyc编译文件、3个onnx模型文件,以及1个html页面、1个yaml配置和1个txt说明,涵盖检测、识别、方向分类等完整模块,目录结构清晰,便于二次开发与调试。目前已有1739人学习下载,读者可借此掌握OCR接口封装、模型调用与HTML结果渲染的完整实现思路,快速搭建属于自己的图片文字识别服务。
1. 从一张发票截图说起:这套 OCR 源码到底能干什么
上周帮朋友处理一批报销单据,三百多张发票截图,财务要求全部转成可检索的 HTML 表格。我第一反应是找在线 OCR 工具,结果要么限制张数,要么返回纯文本丢了版面结构,表格线一塌糊涂。后来翻到一套 Python 实现的 OCR 源码,核心是从 RapidOCR 改造而来,不仅能识别文字,还能把识别结果按位置关系还原成 HTML 结构——标题归标题、表格归表格、段落归段落。这正好戳中了「图片转文字」场景里最容易被忽略的痛点:光有文字没用,版面信息丢了,后期还得人工重新排版。
这套源码适合谁?如果你手头有批量图片需要结构化提取——合同扫描件、票据、报表截图、书籍页面——又不想依赖外部 API,它值得拆开看看。纯 Python 实现,依赖清晰,改造空间大。下面我从环境搭建到参数调优,把踩过的坑和能直接抄的代码都摆出来。
2. 拆解 RapidOCR 改造版:识别引擎与 HTML 还原逻辑
2.1 三阶段流水线:检测、方向分类、识别
这套源码沿用了 RapidOCR 的核心架构,本质上是三个模型串联干活。第一阶段是文本检测,用 DBNet 系列模型在图片上画出所有可能包含文字的矩形框;第二阶段是方向分类,判断每个框里的文字是 0 度还是 180 度,防止倒置图片识别出乱码;第三阶段是文本识别,用 CRNN 或 SVTR 结构把框里的图像转成字符序列。
为什么选这个组合?DBNet 对任意形状文本的检测鲁棒性比传统 MSER 好太多,尤其是票据上那种密集小字。方向分类模型虽然增加了一点推理时间,但省掉了「图片转文字」时最烦人的手动旋转步骤。识别模型用的是轻量级骨干网络,在 CPU 上单张 A4 扫描件大约 1.5 到 3 秒,GPU 上能压到 200 毫秒以内。
源码里把这三个阶段封装成了OcrEngine类,初始化时加载三个 ONNX 模型文件。我注意到它没有用 PaddleOCR 的预测库,而是直接走 ONNX Runtime,这样部署时不用装 PaddlePaddle 那一大堆依赖,对纯 Python 环境友好很多。
2.2 HTML 结构还原:从文本框到 DOM 树
识别出来的只是一堆带坐标的文本行,怎么变成 HTML?源码里有个HtmlBuilder模块,逻辑分三步走。
第一步,按 Y 轴坐标对文本框聚类。相邻且垂直重叠度高的框归为同一行,行与行之间根据间距判断是段落内换行还是新段落。第二步,识别表格结构。如果某组文本框的 X 轴对齐关系呈现明显的列特征,且行数超过阈值,就标记为表格区域,用<table>包裹。第三步,根据字体大小和加粗程度推断标题层级,映射到<h1>到<h6>。
这里有个设计取舍值得说:它没有用复杂的版面分析模型,而是基于规则做启发式判断。好处是快、可解释、容易改;坏处是对复杂杂志版面会翻车。我实测下来,票据和合同这类规整文档准确率很高,但遇到多栏排版加图片环绕的页面,表格识别会误判。源码里留了table_detection_threshold参数,后面会讲怎么调。
2.3 环境搭建与模型文件放置
先看依赖。源码根目录有个requirements.txt,核心就四个包:
# 创建虚拟环境,避免和系统 Python 冲突 python -m venv ocr_env source ocr_env/bin/activate # Windows 用 ocr_env\Scripts\activate # 安装核心依赖 pip install onnxruntime opencv-python numpy pillow pip install shapely pyclipper # DBNet 后处理需要逻辑说明:onnxruntime是推理引擎,选 CPU 版就行,除非你有 CUDA 环境可以换onnxruntime-gpu。opencv-python负责图像预处理和画框调试。shapely和pyclipper是 DBNet 做多边形框后处理用的,缺一个都会在检测阶段报ModuleNotFoundError。
模型文件需要单独下载,源码包里通常不附带。常见做法是去 RapidOCR 的模型仓库拿三个 ONNX 文件:检测模型约 4.7MB,方向分类模型约 1.4MB,识别模型约 10MB。放到models/目录下,命名保持和源码里config.yaml一致。我一般会检查文件 MD5,防止下载中断导致模型加载时报Invalid ONNX model。
提示:如果
onnxruntime安装后 import 报错,大概率是 numpy 版本冲突。先pip install numpy==1.24.3锁住版本,再装 onnxruntime。
3. 跑通第一个 Demo:从图片到 HTML 的完整调用链
3.1 初始化引擎与单图识别
源码的入口很直接,我把它简化成一个最小调用示例:
from ocr_engine import OcrEngine from html_builder import HtmlBuilder # 初始化引擎,指定模型目录和设备 engine = OcrEngine( det_model_path="models/det_model.onnx", cls_model_path="models/cls_model.onnx", rec_model_path="models/rec_model.onnx", use_gpu=False, # 有 CUDA 改 True det_limit_side_len=960 # 检测输入边长,影响小字召回 ) # 读取图片并执行识别 result = engine.predict("invoice_001.png") # result 结构:list of dict,每个 dict 含 box, text, score for item in result[:3]: print(item["text"], item["score"], item["box"])逻辑说明:predict方法内部依次调用检测、方向分类、识别三个模型,返回的box是四个角点坐标,score是识别置信度。det_limit_side_len这个参数很关键——设太小,密集小字会漏检;设太大,CPU 推理时间线性增长。我一般从 960 起步,票据类可以加到 1280。
参数说明:use_gpu为 False 时走 CPUExecutionProvider,为 True 时走 CUDAExecutionProvider,需要提前装好对应版本的 onnxruntime-gpu 和 CUDA 库。如果设了 True 但环境没配好,会静默回退到 CPU,不会报错,这点要注意。
3.2 生成 HTML 并处理表格
拿到识别结果后,交给HtmlBuilder生成结构:
builder = HtmlBuilder( table_detection_threshold=0.6, # 表格判定阈值 paragraph_gap_ratio=1.5, # 段落间距系数 heading_font_ratio=1.3 # 标题字号比例阈值 ) html_str = builder.build(result, image_width=1240, image_height=1754) # 写入文件 with open("output.html", "w", encoding="utf-8") as f: f.write(html_str)逻辑说明:build方法接收识别结果和原图尺寸,内部先做行聚类,再判断表格区域,最后按字号和位置生成标签。table_detection_threshold控制表格判定的严格程度——值越高,越不容易把普通文本误判成表格,但可能漏掉真正的表格。paragraph_gap_ratio决定行间距超过行高多少倍时另起段落,默认 1.5 适合大多数文档。
参数说明:heading_font_ratio是标题判定的核心。源码会统计所有文本框的高度中位数,超过中位数 1.3 倍的框标记为标题候选,再按高度排序映射到 h1-h6。如果文档标题和正文字号差异不大,这个值要调低到 1.15 左右。
3.3 批量处理与并发控制
单张跑通后,批量场景需要控制内存和并发:
import os from concurrent.futures import ThreadPoolExecutor image_dir = "invoices/" output_dir = "html_output/" os.makedirs(output_dir, exist_ok=True) def process_one(img_name): result = engine.predict(os.path.join(image_dir, img_name)) html = builder.build(result, 1240, 1754) out_path = os.path.join(output_dir, img_name.replace(".png", ".html")) with open(out_path, "w", encoding="utf-8") as f: f.write(html) return img_name # 线程数建议等于 CPU 核心数,不要超过 8 with ThreadPoolExecutor(max_workers=4) as pool: for name in pool.map(process_one, os.listdir(image_dir)): print(f"done: {name}")逻辑说明:ONNX Runtime 在 CPU 上推理时会释放 GIL,所以用线程池能吃到多核收益。但max_workers不要设太高,超过物理核心数后上下文切换开销会抵消收益。我实测 4 核机器设 4 最稳,8 核设 6 到 8。
参数说明:如果图片尺寸差异大,建议先统一缩放到长边 1600 像素再送入引擎,避免单张大图拖慢整批。源码里没有内置缩放逻辑,需要自己在predict前加cv2.resize。
4. 避坑指南:识别率骤降与 HTML 错乱的排查手册
4.1 现象:识别结果全是乱码或空字符串
原因:方向分类模型没生效,或者图片本身是 90 度旋转的。RapidOCR 的方向分类只处理 0 度和 180 度,不处理 90 度和 270 度旋转。如果手机拍的图片横着存,检测框会画得乱七八糟,识别自然出不来。
解决:在送入引擎前加一步 EXIF 方向校正。用 PIL 读取时调ImageOps.exif_transpose,或者用 OpenCV 根据 EXIF 标签旋转。我一般会在预处理阶段统一做一次,代码就三行:
from PIL import Image, ImageOps img = Image.open(path) img = ImageOps.exif_transpose(img) # 自动按 EXIF 旋转 img.save(path) # 覆盖原图或另存4.2 现象:表格被识别成普通段落,<table>标签没生成
原因:table_detection_threshold设太高,或者表格线不明显导致文本框对齐特征弱。源码判断表格依赖两个条件——列对齐度和行数。如果表格没有边框线,只有文字,列对齐度会下降。
解决:先把阈值从 0.6 降到 0.4 试一次。如果还不行,检查识别结果里同一行的文本框 X 坐标是否对齐。常见做法是在预处理阶段做一次形态学操作,把浅色表格线增强,再送入检测模型。另外,如果表格列数少于 3 列,源码默认不判定为表格,这个逻辑在HtmlBuilder里可以改。
4.3 现象:CPU 推理一张图超过 10 秒
原因:det_limit_side_len设太大,或者图片本身分辨率过高。DBNet 的计算量和输入边长平方成正比,边长从 960 加到 1920,耗时翻四倍。
解决:先确认图片长边是否超过 2000 像素。如果是,先缩放到 1600 再送入。然后检查det_limit_side_len是否被误设为 1920 以上。我一般把上限锁在 1280,超过这个值对小字召回提升有限,但耗时增加明显。如果必须处理高清图,考虑换onnxruntime-gpu,或者用intra_op_num_threads限制线程数避免争抢。
4.4 现象:HTML 里标题层级全乱,正文被标成 h3
原因:heading_font_ratio设太低,把稍大一点的正文误判为标题。或者文档里存在多种字号,中位数被拉偏。
解决:先打印所有文本框的高度分布,看中位数落在哪个区间。如果正文和标题字号差异小于 1.2 倍,把heading_font_ratio调到 1.4 以上,同时加一个最小高度阈值,低于该值的框不参与标题判定。源码里heading_font_ratio是全局参数,更精细的做法是按区域动态计算,但这需要改HtmlBuilder的内部逻辑。
4.5 现象:识别置信度普遍低于 0.5,但文字肉眼可辨
原因:图片对比度太低、有噪点,或者识别模型对某种字体不适应。RapidOCR 的识别模型训练集以印刷体为主,手写体或艺术字识别率会明显下降。
解决:预处理阶段加自适应二值化。用 OpenCV 的adaptiveThreshold处理后再送入识别,对低对比度图片提升明显。如果字体特殊,考虑用 PaddleOCR 的识别模型替换,但要注意输出格式对齐。我遇到过一次发票上用了特殊数字字体,识别率只有 30%,换了模型后到 85%,但推理时间增加了 40%。
5. 进阶调优:用置信度过滤和区域裁剪把准确率再拉一档
跑通基础流程后,真正影响落地效果的是后处理策略。源码默认返回所有识别结果,但实际业务里,低置信度的结果往往需要人工复核,不如直接过滤掉减少干扰。我一般会加一层置信度过滤,同时结合区域裁剪做二次识别。
先看置信度过滤的实现:
# 按置信度过滤,低于阈值的框丢弃 CONF_THRESHOLD = 0.6 filtered = [item for item in result if item["score"] >= CONF_THRESHOLD] # 对低置信度区域做裁剪放大后二次识别 low_conf_items = [item for item in result if item["score"] < CONF_THRESHOLD] for item in low_conf_items: x_coords = [p[0] for p in item["box"]] y_coords = [p[1] for p in item["box"]] x1, x2 = int(min(x_coords)), int(max(x_coords)) y1, y2 = int(min(y_coords)), int(max(y_coords)) # 向外扩 5 像素,避免切掉边缘 crop = img[y1-5:y2+5, x1-5:x2+5] # 放大 2 倍再识别 crop = cv2.resize(crop, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC) retry_result = engine.predict(crop) if retry_result and retry_result[0]["score"] > item["score"]: item["text"] = retry_result[0]["text"] item["score"] = retry_result[0]["score"]逻辑说明:第一层过滤把明显不可信的结果剔掉,减少 HTML 里的噪声。第二层对低置信度区域裁剪放大后重新识别——小字放大后识别模型的特征提取更充分,实测能把部分 0.4 左右的置信度拉到 0.7 以上。注意裁剪时向外扩几个像素,否则容易把文字边缘切掉,反而更差。
参数说明:CONF_THRESHOLD设 0.6 是保守值,票据类可以降到 0.5,书籍类可以升到 0.7。放大倍数fx=2是经验值,再大收益递减且耗时增加。如果原图本身分辨率很低,放大反而会引入插值噪声,这时候应该先做超分辨率或者直接放弃。
另一个技巧是分区域调参。同一张图里,表格区域和正文区域的最优det_limit_side_len可能不同。我一般先用大图跑一遍检测,拿到所有文本框后,按区域密度动态决定是否对密集区域单独裁剪重跑。这个逻辑源码里没有,需要自己加,但代码量不大,核心就是根据框的分布算一个密度图,超过阈值就裁剪。
最后说一个验证方法:拿一批已知正确结果的图片,跑完 OCR 后计算字符级准确率。我习惯用difflib.SequenceMatcher快速算相似度,低于 0.9 的挑出来人工看,定位是检测漏框还是识别错字。从那以后我每次换模型或调参数,都强制走一遍这个验证流程,不然改了一个参数,这边好了那边崩了,根本不知道哪里出的问题。希望帮到你。
本文还有配套的精品资源,点击获取