简介:这是一套基于 rapidOCR 改造的完整 OCR 图片识别源码项目,面向需要将图片文字识别能力集成到自有系统中的开发者与 Python 学习者。项目已实现 OCR 识别并返回带标记的 HTML 结构,内置 /engine/ocr 接口,可接收 HTTP 链接、本地文件、文件流等多种形式的图片输入,返回的 HTML 可直接嵌入其他页面或复制使用,适合文档数字化、票据识别、网页内容提取等场景。压缩包共 46 个文件,以 23 个 py 源码与 17 个 pyc 编译文件为主体,另含 3 个 onnx 模型文件、1 个 yaml 配置、1 个 html 页面及 1 个 txt 说明,整体约 14.26MB,目录涵盖识别引擎、检测与方向分类模块、Web 服务及工具封装,结构清晰便于二次开发。目前已有 1739 人学习下载,读者可据此快速理解 rapidOCR 的改造思路、接口封装方式与 HTML 结果生成逻辑,并在此基础上扩展自己的识别服务。1. 从一张发票截图说起:这个 OCR 项目到底能干什么上周帮朋友处理一批报销单据,三百多张发票截图,需要把上面的发票号、金额、开票日期全部提取出来录入表格。手动敲了两小时才搞定四十张,眼睛都快瞎了。当时就想,要是有个能直接调用的 OCR 工具就好了——不是那种需要注册账号、上传图片到别人服务器的在线服务,而是能跑在自己机器上、拿到源码就能改的本地方案。这份资源正好解决这个问题。它是一套完整的 OCR 图片识别源码,用 Python 实现,核心基于 RapidOCR 改造而来。跟原版最大的区别在于:识别结果不是返回一堆坐标和纯文本,而是直接输出结构化的 HTML。这意味着你拿到结果后可以直接塞进网页渲染,或者用解析库提取字段,省掉了大量后处理工作。适合谁用?需要批量处理图片转文字的后端开发者、想给内部系统加 OCR 能力但不想调外部 API 的团队,以及正在找一个能跑通、能改的 OCR 入门项目的 Python 学习者。2. 技术选型与架构拆解:为什么是 RapidOCR 加 HTML 输出2.1 为什么选 RapidOCR 而不是 Tesseract 或 PaddleOCROCR 这个领域,开源方案不少,但真正能在普通开发机上跑起来、不折腾环境的没几个。Tesseract 是老牌选手,但对中文的识别效果一直不太理想,尤其是发票、表格这类版式复杂的图片,经常把数字和文字混在一起。PaddleOCR 效果好,但依赖 PaddlePaddle 框架,装完之后动辄几个 G,对只想跑个 OCR 的开发者来说太重了。RapidOCR 的定位很聪明:它把 PaddleOCR 的模型拿过来,用 ONNX Runtime 做推理后端。ONNX Runtime 的安装包小得多,CPU 推理速度也够用,不需要 CUDA 也能跑。这个项目在 RapidOCR 的基础上做了三件事:封装了更简洁的调用接口、增加了 HTML 结构化输出、补充了批量处理的入口。常见做法是直接用 RapidOCR 的 Python 包,但这个项目把模型文件、配置、后处理逻辑都打包在一起,拿到源码就能跑,不用再去翻文档找模型下载地址。2.2 整体架构:从图片输入到 HTML 输出的四层结构项目的目录结构大致是这样的:ocr_project/ ├── models/ # ONNX 模型文件 │ ├── det_model.onnx # 文本检测模型 │ ├── rec_model.onnx # 文本识别模型 │ └── cls_model.onnx # 方向分类模型 ├── config/ │ └── config.yaml # 模型路径、阈值等配置 ├── core/ │ ├── detector.py # 文本检测封装 │ ├── recognizer.py # 文本识别封装 │ └── pipeline.py # 串联检测+识别+后处理 ├── output/ │ └── html_builder.py # HTML 结构生成 ├── utils/ │ ├── image_utils.py # 图片预处理 │ └── postprocess.py # 识别结果后处理 ├── main.py # 命令行入口 └── requirements.txt四层结构各司其职:模型层放 ONNX 文件,不依赖网络下载;核心层封装检测和识别逻辑,每个模块可以单独替换;输出层负责把识别结果转成 HTML;工具层处理图片读取、缩放、结果过滤这些杂活。这种分层的好处是,如果你想换检测模型,只改detector.py和配置文件就行,识别和输出部分不用动。2.3 核心流程:一张图片进来,HTML 出去整个处理链路是这样的:图片先经过预处理(缩放、归一化),然后送入检测模型找出所有文本框的位置,接着对每个文本框做方向分类(判断是否旋转了 180 度),再送入识别模型得到文字内容,最后把文字和位置信息组装成 HTML 结构。这里有个关键设计:检测和识别是分开的两个模型。检测模型只负责找“哪里有字”,识别模型只负责“是什么字”。这样做的好处是灵活——如果某类图片的文本排列特别密集,可以单独调检测模型的阈值;如果识别准确率不够,可以换更大的识别模型而不影响检测部分。3. 跑起来:环境配置与最小可运行示例3.1 环境准备:Python 版本与依赖安装这个项目对 Python 版本的要求是 3.8 到 3.11,推荐 3.10。太老的版本不支持 ONNX Runtime 的新特性,太新的版本(3.12+)有些依赖包还没跟上。依赖清单里核心的就三个:onnxruntime、opencv-python、numpy。requirements.txt里还列了pyyaml用于读配置,pillow用于图片格式兼容。安装命令很直接:# 创建虚拟环境(推荐,避免污染全局包) python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装依赖 pip install -r requirements.txt如果 pip 安装 onnxruntime 时卡住,常见原因是网络问题。可以换用国内镜像源:pip install onnxruntime -i https://pypi.tuna.tsinghua.edu.cn/simple注意:不需要单独安装 PyTorch 或 PaddlePaddle,这个项目只用 ONNX Runtime 做推理。这是它比 PaddleOCR 轻量的关键原因。3.2 模型文件放哪里:路径配置与校验模型文件在models/目录下,三个 ONNX 文件加起来大概 15MB 左右。如果下载的源码包里没有模型文件,需要单独获取。配置文件config/config.yaml里指定了模型路径:det_model_path: "models/det_model.onnx" rec_model_path: "models/rec_model.onnx" cls_model_path: "models/cls_model.onnx" det_threshold: 0.3 # 检测阈值,越低检测到的框越多 rec_batch_size: 6 # 识别时的批处理大小 use_gpu: false # 是否使用 GPU 推理det_threshold这个参数值得说一下。默认 0.3 适合大多数场景,但如果图片里的文字特别小或者对比度低,可以降到 0.2 试试;如果检测出太多无关的框(比如把噪点当成文字),就调到 0.5。rec_batch_size影响内存占用和速度,CPU 推理时设 6 到 8 比较平衡,设太大反而会因为内存交换变慢。3.3 第一个可运行示例:识别一张本地图片项目提供了一个命令行入口main.py,最简单的用法是:python main.py --image test.jpg --output result.html这条命令会读取test.jpg,识别其中的文字,然后把结果写入result.html。如果想在代码里调用,可以这样写:from core.pipeline import OCRPipeline # 初始化 pipeline,会自动加载模型 pipeline = OCRPipeline(config_path="config/config.yaml") # 对单张图片做识别 result = pipeline.process("test.jpg") # result 是一个字典,包含 html 和 raw_text 两个字段 print(result["raw_text"]) # 纯文本结果 print(result["html"]) # HTML 结构结果OCRPipeline的process方法接收图片路径,返回一个字典。raw_text是按阅读顺序拼接的纯文本,html是带<div>和<span>标签的结构化输出。如果你只需要文字内容,用raw_text就够了;如果要在网页上展示识别结果并保留位置信息,用html。3.4 批量处理:一次跑完一个文件夹实际工作中很少只处理一张图。项目里有一个batch_process.py脚本,可以指定输入文件夹和输出文件夹:python batch_process.py --input_dir ./images --output_dir ./results --format html--format参数支持html、txt、json三种输出格式。批量处理时,每个图片会生成一个同名的输出文件。如果图片很多,建议先用--limit 10跑一小批看看效果,确认没问题再全量跑。提示:批量处理时如果遇到某张图片报错,脚本默认会跳过并记录到error.log,不会中断整个任务。这个行为可以通过--stop_on_error参数改成遇到错误就停止。4. 核心模块拆解:检测、识别与 HTML 生成的关键代码4.1 文本检测:从图片中找到所有文字区域检测模块的职责是把图片里所有可能包含文字的区域框出来。核心代码在core/detector.py:import cv2 import numpy as np import onnxruntime as ort class TextDetector: def __init__(self, model_path, threshold=0.3): # 创建 ONNX Runtime 会话,CPU 推理 self.session = ort.InferenceSession( model_path, providers=['CPUExecutionProvider'] ) self.threshold = threshold # 获取输入名称,通常是 'input' self.input_name = self.session.get_inputs()[0].name def detect(self, image): # 图片归一化:缩放、减均值、除标准差 img = self._preprocess(image) # 推理:输入是 1x3xHxW 的 tensor outputs = self.session.run(None, {self.input_name: img}) # 后处理:从概率图还原出文本框坐标 boxes = self._postprocess(outputs[0]) return boxes def _preprocess(self, image): # 将图片缩放到 32 的倍数,保持长宽比 h, w = image.shape[:2] scale = 1.0 if max(h, w) > 960: scale = 960.0 / max(h, w) new_h, new_w = int(h * scale), int(w * scale) # 确保是 32 的倍数 new_h = max(32, (new_h // 32) * 32) new_w = max(32, (new_w // 32) * 32) resized = cv2.resize(image, (new_w, new_h)) # 归一化到 [0,1] 并调整通道顺序 img = resized.astype(np.float32) / 255.0 img = img.transpose(2, 0, 1) # HWC -> CHW img = np.expand_dims(img, axis=0) # 加 batch 维度 return img_preprocess里有两个细节值得注意。第一,图片尺寸要缩放到 32 的倍数,这是因为检测模型的下采样倍率是 32,不满足这个条件会导致特征图尺寸对不上。第二,归一化时只做了除以 255,没有减均值除标准差,这是因为 RapidOCR 用的模型在训练时就是这么处理的,保持一致才能保证效果。_postprocess的逻辑稍微复杂一些,大致是:模型输出一张概率图,每个像素值表示“这个位置是文字的概率”。先按阈值二值化,然后找连通区域,对每个连通区域求最小外接矩形,最后用非极大值抑制去掉重叠的框。这部分代码在utils/postprocess.py里,大概 80 行左右。4.2 文本识别:把文字区域转成字符串识别模块接收检测到的每个文本框,输出对应的文字。代码在core/recognizer.py:class TextRecognizer: def __init__(self, model_path, batch_size=6): self.session = ort.InferenceSession( model_path, providers=['CPUExecutionProvider'] ) self.batch_size = batch_size # 字符集:模型输出的索引到实际字符的映射 self.charset = self._load_charset() def recognize(self, image, boxes): # 从原图中裁剪出每个文本框对应的区域 crops = [self._crop(image, box) for box in boxes] results = [] # 分批推理,避免一次性占用太多内存 for i in range(0, len(crops), self.batch_size): batch = crops[i:i + self.batch_size] # 将 batch 中的图片统一缩放到相同高度 batch_input = self._prepare_batch(batch) outputs = self.session.run(None, {self.input_name: batch_input}) # 解码:将模型输出的索引序列转成字符串 texts = self._decode(outputs[0]) results.extend(texts) return results def _crop(self, image, box): # box 是四个点的坐标,用透视变换把文字区域摆正 # 如果文字有倾斜,这一步能显著提升识别率 pts = np.array(box, dtype=np.float32) # 计算目标矩形的宽高 width = int(max(np.linalg.norm(pts[0] - pts[1]), np.linalg.norm(pts[2] - pts[3]))) height = int(max(np.linalg.norm(pts[0] - pts[3]), np.linalg.norm(pts[1] - pts[2]))) dst = np.array([[0, 0], [width, 0], [width, height], [0, height]], dtype=np.float32) M = cv2.getPerspectiveTransform(pts, dst) return cv2.warpPerspective(image, M, (width, height))_crop里的透视变换是个关键步骤。检测模型给出的框通常是倾斜的四边形,如果直接按外接矩形裁剪,文字会有旋转,识别率会下降。用透视变换把四个角点映射到一个正矩形上,相当于把文字“摆正”了再识别。这个操作对发票、身份证这类有固定版式的图片效果提升很明显。_decode的逻辑是 CTC 解码的简化版:模型输出一个序列,每个位置对应字符集里的一个索引,连续的相同索引只保留一个,空白符(索引 0)直接跳过。这部分代码大概 30 行,核心就是一个循环加去重。4.3 HTML 生成:把识别结果变成结构化标签输出模块在output/html_builder.py,负责把文字和位置信息组装成 HTML:class HTMLBuilder: def __init__(self, image_width, image_height): self.image_width = image_width self.image_height = image_height def build(self, boxes, texts): # 按文字框的纵坐标排序,模拟从上到下的阅读顺序 items = list(zip(boxes, texts)) items.sort(key=lambda x: (x[0][0][1], x[0][0][0])) html_parts = ['<div class="ocr-result">'] for box, text in items: # 计算文字框的左上角坐标和宽高 x = int(min(p[0] for p in box)) y = int(min(p[1] for p in box)) w = int(max(p[0] for p in box)) - x h = int(max(p[1] for p in box)) - y # 用绝对定位把文字放在对应位置 style = f"left:{x}px;top:{y}px;width:{w}px;height:{h}px;" html_parts.append( f'<span class="ocr-text" style="{style}">{text}</span>' ) html_parts.append('</div>') return '\n'.join(html_parts)生成的 HTML 结构很简单:一个外层div包裹所有文字,每个文字块是一个绝对定位的span。这样做的好处是,如果你把原图作为背景,文字会精确地覆盖在原来的位置上。如果不需要位置信息,只想要纯文本,可以用raw_text字段,它是按阅读顺序拼接的字符串。注意:HTML 里的文字没有做转义。如果识别结果里包含<或>这类字符,直接插入会导致 HTML 结构错乱。实际使用时建议在build方法里加一个html.escape(text)。5. 避坑与排查:跑这个项目时容易翻车的几个地方5.1 模型加载报错:Invalid ONNX model现象:运行main.py时抛出onnxruntime.capi.onnxruntime_pybind11_state.InvalidArgument,提示模型文件无效。原因:最常见的情况是模型文件下载不完整,或者下载的是 Git LFS 的指针文件而不是实际模型。有些代码托管平台对大文件用 LFS 管理,直接点下载按钮拿到的是一个几十字节的文本文件,里面写着version https://git-lfs...。解决:检查models/目录下每个文件的大小。检测模型大概 4-5MB,识别模型 8-10MB,分类模型 1-2MB。如果文件只有几百字节,说明是 LFS 指针,需要用git lfs pull拉取,或者从其他渠道获取完整的 ONNX 文件。5.2 识别结果全是乱码或空字符串现象:图片能正常读取,检测框也画出来了,但识别出来的文字是乱码,或者直接是空字符串。原因:字符集不匹配。识别模型的输出是索引序列,需要用一个字符集文件把索引映射成实际字符。如果字符集文件缺失或者和模型不对应,解码出来的就是乱码。解决:检查core/recognizer.py里_load_charset方法读取的文件路径。字符集通常是一个keys.txt或charset.txt,每行一个字符。确保这个文件和识别模型是配套的——不同版本的模型可能用了不同的字符集。5.3 图片太大导致内存溢出现象:处理高分辨率图片(比如 4000x3000 以上)时,程序被系统杀掉,或者报MemoryError。原因:检测模型在预处理时会把图片缩放到 960 以内,但识别模型裁剪出的文字区域如果很大,batch 推理时占用的内存会急剧上升。尤其是rec_batch_size设得比较大时。解决:在config.yaml里把rec_batch_size降到 2 或 4。另外可以在image_utils.py里加一个预处理步骤,如果图片宽度超过 2000 像素,先等比缩放到 2000 再送入检测。5.4 检测框太多导致识别速度慢现象:一张 A4 大小的文档图片,检测出 200 多个文本框,识别阶段跑了十几秒。原因:det_threshold设得太低,把一些噪点或背景纹理也当成了文字。另外,如果图片里有大量细小的文字(比如表格里的数字),检测模型会为每个小文字单独出一个框。解决:把det_threshold从 0.3 调到 0.5,减少误检。如果确实需要识别小文字,可以在后处理阶段加一个过滤:面积小于 10x10 像素的框直接丢弃。这个逻辑可以加在postprocess.py的filter_boxes函数里。5.5 HTML 输出中文乱码现象:生成的result.html用浏览器打开后,中文显示为乱码。原因:HTML 文件没有声明字符编码,浏览器默认用 ISO-8859-1 解析。解决:在HTMLBuilder.build返回的字符串前面加上<meta charset="utf-8">,或者写文件时用encoding='utf-8'参数。如果是在 Web 框架里直接返回 HTML 字符串,确保响应头里的Content-Type包含charset=utf-8。6. 二次开发:换模型、加字段、接接口的改造思路拿到源码跑通之后,下一步就是按自己的需求改。这个项目的结构对二次开发比较友好,几个常见的改造方向我按难度从低到高排一下。最简单的改造是调整输出格式。比如你不需要 HTML,而是想把结果直接写入数据库。可以在output/目录下新建一个json_builder.py,模仿html_builder.py的结构,把build方法的返回值改成 JSON 字符串。然后在pipeline.py里加一个output_format参数,根据参数选择用哪个 builder。这个改动大概 20 行代码,不需要动核心的检测和识别逻辑。稍微复杂一点的是替换识别模型。RapidOCR 用的是 PP-OCRv4 的识别模型,如果你有自己训练的模型(比如专门识别手写体或特定字体),可以导出成 ONNX 格式,然后替换models/rec_model.onnx。需要注意的是,自定义模型的输入尺寸和字符集可能和原模型不一样,要同步修改config.yaml里的rec_image_shape和charset_path。我一般会先用onnxruntime的session.get_inputs()打印出新模型的输入形状,确认和代码里的预处理逻辑匹配。再进一步是加一个 Web 接口。项目本身是命令行工具,但实际使用时往往需要让其他系统调用。可以用 FastAPI 包一层:from fastapi import FastAPI, UploadFile from core.pipeline import OCRPipeline app = FastAPI() pipeline = OCRPipeline(config_path="config/config.yaml") @app.post("/ocr") async def ocr_endpoint(file: UploadFile): # 保存上传的图片到临时文件 contents = await file.read() with open("/tmp/upload.jpg", "wb") as f: f.write(contents) # 调用 pipeline 识别 result = pipeline.process("/tmp/upload.jpg") return {"html": result["html"], "text": result["raw_text"]}这个接口接收图片上传,返回识别结果。部署时用uvicorn main:app --host 0.0.0.0 --port 8000启动。如果并发量不大,单 worker 就够了;如果有多人同时用,可以加--workers 4,但要注意每个 worker 都会加载一份模型,内存占用会翻倍。还有一个容易被忽略的改造点是错误处理。原项目的pipeline.process在遇到无法读取的图片时会直接抛异常。在生产环境里,更好的做法是捕获异常并返回一个结构化的错误信息。我一般会在pipeline.py里加一个try/except,把异常类型和图片路径记录到日志,然后返回一个包含error字段的字典。这样上层调用方可以根据error字段判断是重试还是跳过。最后说一个我踩过的坑:替换模型后一定要用同一批测试图片对比新旧模型的效果。有一次我换了一个号称“更高精度”的识别模型,结果在发票数据集上准确率反而下降了,原因是新模型对数字和字母的区分度不如原模型。从那以后我每次换模型都强制走一遍回归测试,至少跑 50 张覆盖不同版式的图片,对比字段级的准确率。希望这些经验能帮到你,少走点弯路。本文还有配套的精品资源,点击获取
简介:这是一套基于 rapidOCR 改造的完整 OCR 图片识别源码项目,面向需要将图片文字识别能力集成到自有系统中的开发者与 Python 学习者。项目已实现 OCR 识别并返回带标记的 HTML 结构,内置 /engine/ocr 接口,可接收 HTTP 链接、本地文件、文件流等多种形式的图片输入,返回的 HTML 可直接嵌入其他页面或复制使用,适合文档数字化、票据识别、网页内容提取等场景。压缩包共 46 个文件,以 23 个 py 源码与 17 个 pyc 编译文件为主体,另含 3 个 onnx 模型文件、1 个 yaml 配置、1 个 html 页面及 1 个 txt 说明,整体约 14.26MB,目录涵盖识别引擎、检测与方向分类模块、Web 服务及工具封装,结构清晰便于二次开发。目前已有 1739 人学习下载,读者可据此快速理解 rapidOCR 的改造思路、接口封装方式与 HTML 结果生成逻辑,并在此基础上扩展自己的识别服务。1. 从一张发票截图说起:这个 OCR 项目到底能干什么上周帮朋友处理一批报销单据,三百多张发票截图,需要把上面的发票号、金额、开票日期全部提取出来录入表格。手动敲了两小时才搞定四十张,眼睛都快瞎了。当时就想,要是有个能直接调用的 OCR 工具就好了——不是那种需要注册账号、上传图片到别人服务器的在线服务,而是能跑在自己机器上、拿到源码就能改的本地方案。这份资源正好解决这个问题。它是一套完整的 OCR 图片识别源码,用 Python 实现,核心基于 RapidOCR 改造而来。跟原版最大的区别在于:识别结果不是返回一堆坐标和纯文本,而是直接输出结构化的 HTML。这意味着你拿到结果后可以直接塞进网页渲染,或者用解析库提取字段,省掉了大量后处理工作。适合谁用?需要批量处理图片转文字的后端开发者、想给内部系统加 OCR 能力但不想调外部 API 的团队,以及正在找一个能跑通、能改的 OCR 入门项目的 Python 学习者。2. 技术选型与架构拆解:为什么是 RapidOCR 加 HTML 输出2.1 为什么选 RapidOCR 而不是 Tesseract 或 PaddleOCROCR 这个领域,开源方案不少,但真正能在普通开发机上跑起来、不折腾环境的没几个。Tesseract 是老牌选手,但对中文的识别效果一直不太理想,尤其是发票、表格这类版式复杂的图片,经常把数字和文字混在一起。PaddleOCR 效果好,但依赖 PaddlePaddle 框架,装完之后动辄几个 G,对只想跑个 OCR 的开发者来说太重了。RapidOCR 的定位很聪明:它把 PaddleOCR 的模型拿过来,用 ONNX Runtime 做推理后端。ONNX Runtime 的安装包小得多,CPU 推理速度也够用,不需要 CUDA 也能跑。这个项目在 RapidOCR 的基础上做了三件事:封装了更简洁的调用接口、增加了 HTML 结构化输出、补充了批量处理的入口。常见做法是直接用 RapidOCR 的 Python 包,但这个项目把模型文件、配置、后处理逻辑都打包在一起,拿到源码就能跑,不用再去翻文档找模型下载地址。2.2 整体架构:从图片输入到 HTML 输出的四层结构项目的目录结构大致是这样的:ocr_project/ ├── models/ # ONNX 模型文件 │ ├── det_model.onnx # 文本检测模型 │ ├── rec_model.onnx # 文本识别模型 │ └── cls_model.onnx # 方向分类模型 ├── config/ │ └── config.yaml # 模型路径、阈值等配置 ├── core/ │ ├── detector.py # 文本检测封装 │ ├── recognizer.py # 文本识别封装 │ └── pipeline.py # 串联检测+识别+后处理 ├── output/ │ └── html_builder.py # HTML 结构生成 ├── utils/ │ ├── image_utils.py # 图片预处理 │ └── postprocess.py # 识别结果后处理 ├── main.py # 命令行入口 └── requirements.txt四层结构各司其职:模型层放 ONNX 文件,不依赖网络下载;核心层封装检测和识别逻辑,每个模块可以单独替换;输出层负责把识别结果转成 HTML;工具层处理图片读取、缩放、结果过滤这些杂活。这种分层的好处是,如果你想换检测模型,只改detector.py和配置文件就行,识别和输出部分不用动。2.3 核心流程:一张图片进来,HTML 出去整个处理链路是这样的:图片先经过预处理(缩放、归一化),然后送入检测模型找出所有文本框的位置,接着对每个文本框做方向分类(判断是否旋转了 180 度),再送入识别模型得到文字内容,最后把文字和位置信息组装成 HTML 结构。这里有个关键设计:检测和识别是分开的两个模型。检测模型只负责找“哪里有字”,识别模型只负责“是什么字”。这样做的好处是灵活——如果某类图片的文本排列特别密集,可以单独调检测模型的阈值;如果识别准确率不够,可以换更大的识别模型而不影响检测部分。3. 跑起来:环境配置与最小可运行示例3.1 环境准备:Python 版本与依赖安装这个项目对 Python 版本的要求是 3.8 到 3.11,推荐 3.10。太老的版本不支持 ONNX Runtime 的新特性,太新的版本(3.12+)有些依赖包还没跟上。依赖清单里核心的就三个:onnxruntime、opencv-python、numpy。requirements.txt里还列了pyyaml用于读配置,pillow用于图片格式兼容。安装命令很直接:# 创建虚拟环境(推荐,避免污染全局包) python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装依赖 pip install -r requirements.txt如果 pip 安装 onnxruntime 时卡住,常见原因是网络问题。可以换用国内镜像源:pip install onnxruntime -i https://pypi.tuna.tsinghua.edu.cn/simple注意:不需要单独安装 PyTorch 或 PaddlePaddle,这个项目只用 ONNX Runtime 做推理。这是它比 PaddleOCR 轻量的关键原因。3.2 模型文件放哪里:路径配置与校验模型文件在models/目录下,三个 ONNX 文件加起来大概 15MB 左右。如果下载的源码包里没有模型文件,需要单独获取。配置文件config/config.yaml里指定了模型路径:det_model_path: "models/det_model.onnx" rec_model_path: "models/rec_model.onnx" cls_model_path: "models/cls_model.onnx" det_threshold: 0.3 # 检测阈值,越低检测到的框越多 rec_batch_size: 6 # 识别时的批处理大小 use_gpu: false # 是否使用 GPU 推理det_threshold这个参数值得说一下。默认 0.3 适合大多数场景,但如果图片里的文字特别小或者对比度低,可以降到 0.2 试试;如果检测出太多无关的框(比如把噪点当成文字),就调到 0.5。rec_batch_size影响内存占用和速度,CPU 推理时设 6 到 8 比较平衡,设太大反而会因为内存交换变慢。3.3 第一个可运行示例:识别一张本地图片项目提供了一个命令行入口main.py,最简单的用法是:python main.py --image test.jpg --output result.html这条命令会读取test.jpg,识别其中的文字,然后把结果写入result.html。如果想在代码里调用,可以这样写:from core.pipeline import OCRPipeline # 初始化 pipeline,会自动加载模型 pipeline = OCRPipeline(config_path="config/config.yaml") # 对单张图片做识别 result = pipeline.process("test.jpg") # result 是一个字典,包含 html 和 raw_text 两个字段 print(result["raw_text"]) # 纯文本结果 print(result["html"]) # HTML 结构结果OCRPipeline的process方法接收图片路径,返回一个字典。raw_text是按阅读顺序拼接的纯文本,html是带<div>和<span>标签的结构化输出。如果你只需要文字内容,用raw_text就够了;如果要在网页上展示识别结果并保留位置信息,用html。3.4 批量处理:一次跑完一个文件夹实际工作中很少只处理一张图。项目里有一个batch_process.py脚本,可以指定输入文件夹和输出文件夹:python batch_process.py --input_dir ./images --output_dir ./results --format html--format参数支持html、txt、json三种输出格式。批量处理时,每个图片会生成一个同名的输出文件。如果图片很多,建议先用--limit 10跑一小批看看效果,确认没问题再全量跑。提示:批量处理时如果遇到某张图片报错,脚本默认会跳过并记录到error.log,不会中断整个任务。这个行为可以通过--stop_on_error参数改成遇到错误就停止。4. 核心模块拆解:检测、识别与 HTML 生成的关键代码4.1 文本检测:从图片中找到所有文字区域检测模块的职责是把图片里所有可能包含文字的区域框出来。核心代码在core/detector.py:import cv2 import numpy as np import onnxruntime as ort class TextDetector: def __init__(self, model_path, threshold=0.3): # 创建 ONNX Runtime 会话,CPU 推理 self.session = ort.InferenceSession( model_path, providers=['CPUExecutionProvider'] ) self.threshold = threshold # 获取输入名称,通常是 'input' self.input_name = self.session.get_inputs()[0].name def detect(self, image): # 图片归一化:缩放、减均值、除标准差 img = self._preprocess(image) # 推理:输入是 1x3xHxW 的 tensor outputs = self.session.run(None, {self.input_name: img}) # 后处理:从概率图还原出文本框坐标 boxes = self._postprocess(outputs[0]) return boxes def _preprocess(self, image): # 将图片缩放到 32 的倍数,保持长宽比 h, w = image.shape[:2] scale = 1.0 if max(h, w) > 960: scale = 960.0 / max(h, w) new_h, new_w = int(h * scale), int(w * scale) # 确保是 32 的倍数 new_h = max(32, (new_h // 32) * 32) new_w = max(32, (new_w // 32) * 32) resized = cv2.resize(image, (new_w, new_h)) # 归一化到 [0,1] 并调整通道顺序 img = resized.astype(np.float32) / 255.0 img = img.transpose(2, 0, 1) # HWC -> CHW img = np.expand_dims(img, axis=0) # 加 batch 维度 return img_preprocess里有两个细节值得注意。第一,图片尺寸要缩放到 32 的倍数,这是因为检测模型的下采样倍率是 32,不满足这个条件会导致特征图尺寸对不上。第二,归一化时只做了除以 255,没有减均值除标准差,这是因为 RapidOCR 用的模型在训练时就是这么处理的,保持一致才能保证效果。_postprocess的逻辑稍微复杂一些,大致是:模型输出一张概率图,每个像素值表示“这个位置是文字的概率”。先按阈值二值化,然后找连通区域,对每个连通区域求最小外接矩形,最后用非极大值抑制去掉重叠的框。这部分代码在utils/postprocess.py里,大概 80 行左右。4.2 文本识别:把文字区域转成字符串识别模块接收检测到的每个文本框,输出对应的文字。代码在core/recognizer.py:class TextRecognizer: def __init__(self, model_path, batch_size=6): self.session = ort.InferenceSession( model_path, providers=['CPUExecutionProvider'] ) self.batch_size = batch_size # 字符集:模型输出的索引到实际字符的映射 self.charset = self._load_charset() def recognize(self, image, boxes): # 从原图中裁剪出每个文本框对应的区域 crops = [self._crop(image, box) for box in boxes] results = [] # 分批推理,避免一次性占用太多内存 for i in range(0, len(crops), self.batch_size): batch = crops[i:i + self.batch_size] # 将 batch 中的图片统一缩放到相同高度 batch_input = self._prepare_batch(batch) outputs = self.session.run(None, {self.input_name: batch_input}) # 解码:将模型输出的索引序列转成字符串 texts = self._decode(outputs[0]) results.extend(texts) return results def _crop(self, image, box): # box 是四个点的坐标,用透视变换把文字区域摆正 # 如果文字有倾斜,这一步能显著提升识别率 pts = np.array(box, dtype=np.float32) # 计算目标矩形的宽高 width = int(max(np.linalg.norm(pts[0] - pts[1]), np.linalg.norm(pts[2] - pts[3]))) height = int(max(np.linalg.norm(pts[0] - pts[3]), np.linalg.norm(pts[1] - pts[2]))) dst = np.array([[0, 0], [width, 0], [width, height], [0, height]], dtype=np.float32) M = cv2.getPerspectiveTransform(pts, dst) return cv2.warpPerspective(image, M, (width, height))_crop里的透视变换是个关键步骤。检测模型给出的框通常是倾斜的四边形,如果直接按外接矩形裁剪,文字会有旋转,识别率会下降。用透视变换把四个角点映射到一个正矩形上,相当于把文字“摆正”了再识别。这个操作对发票、身份证这类有固定版式的图片效果提升很明显。_decode的逻辑是 CTC 解码的简化版:模型输出一个序列,每个位置对应字符集里的一个索引,连续的相同索引只保留一个,空白符(索引 0)直接跳过。这部分代码大概 30 行,核心就是一个循环加去重。4.3 HTML 生成:把识别结果变成结构化标签输出模块在output/html_builder.py,负责把文字和位置信息组装成 HTML:class HTMLBuilder: def __init__(self, image_width, image_height): self.image_width = image_width self.image_height = image_height def build(self, boxes, texts): # 按文字框的纵坐标排序,模拟从上到下的阅读顺序 items = list(zip(boxes, texts)) items.sort(key=lambda x: (x[0][0][1], x[0][0][0])) html_parts = ['<div class="ocr-result">'] for box, text in items: # 计算文字框的左上角坐标和宽高 x = int(min(p[0] for p in box)) y = int(min(p[1] for p in box)) w = int(max(p[0] for p in box)) - x h = int(max(p[1] for p in box)) - y # 用绝对定位把文字放在对应位置 style = f"left:{x}px;top:{y}px;width:{w}px;height:{h}px;" html_parts.append( f'<span class="ocr-text" style="{style}">{text}</span>' ) html_parts.append('</div>') return '\n'.join(html_parts)生成的 HTML 结构很简单:一个外层div包裹所有文字,每个文字块是一个绝对定位的span。这样做的好处是,如果你把原图作为背景,文字会精确地覆盖在原来的位置上。如果不需要位置信息,只想要纯文本,可以用raw_text字段,它是按阅读顺序拼接的字符串。注意:HTML 里的文字没有做转义。如果识别结果里包含<或>这类字符,直接插入会导致 HTML 结构错乱。实际使用时建议在build方法里加一个html.escape(text)。5. 避坑与排查:跑这个项目时容易翻车的几个地方5.1 模型加载报错:Invalid ONNX model现象:运行main.py时抛出onnxruntime.capi.onnxruntime_pybind11_state.InvalidArgument,提示模型文件无效。原因:最常见的情况是模型文件下载不完整,或者下载的是 Git LFS 的指针文件而不是实际模型。有些代码托管平台对大文件用 LFS 管理,直接点下载按钮拿到的是一个几十字节的文本文件,里面写着version https://git-lfs...。解决:检查models/目录下每个文件的大小。检测模型大概 4-5MB,识别模型 8-10MB,分类模型 1-2MB。如果文件只有几百字节,说明是 LFS 指针,需要用git lfs pull拉取,或者从其他渠道获取完整的 ONNX 文件。5.2 识别结果全是乱码或空字符串现象:图片能正常读取,检测框也画出来了,但识别出来的文字是乱码,或者直接是空字符串。原因:字符集不匹配。识别模型的输出是索引序列,需要用一个字符集文件把索引映射成实际字符。如果字符集文件缺失或者和模型不对应,解码出来的就是乱码。解决:检查core/recognizer.py里_load_charset方法读取的文件路径。字符集通常是一个keys.txt或charset.txt,每行一个字符。确保这个文件和识别模型是配套的——不同版本的模型可能用了不同的字符集。5.3 图片太大导致内存溢出现象:处理高分辨率图片(比如 4000x3000 以上)时,程序被系统杀掉,或者报MemoryError。原因:检测模型在预处理时会把图片缩放到 960 以内,但识别模型裁剪出的文字区域如果很大,batch 推理时占用的内存会急剧上升。尤其是rec_batch_size设得比较大时。解决:在config.yaml里把rec_batch_size降到 2 或 4。另外可以在image_utils.py里加一个预处理步骤,如果图片宽度超过 2000 像素,先等比缩放到 2000 再送入检测。5.4 检测框太多导致识别速度慢现象:一张 A4 大小的文档图片,检测出 200 多个文本框,识别阶段跑了十几秒。原因:det_threshold设得太低,把一些噪点或背景纹理也当成了文字。另外,如果图片里有大量细小的文字(比如表格里的数字),检测模型会为每个小文字单独出一个框。解决:把det_threshold从 0.3 调到 0.5,减少误检。如果确实需要识别小文字,可以在后处理阶段加一个过滤:面积小于 10x10 像素的框直接丢弃。这个逻辑可以加在postprocess.py的filter_boxes函数里。5.5 HTML 输出中文乱码现象:生成的result.html用浏览器打开后,中文显示为乱码。原因:HTML 文件没有声明字符编码,浏览器默认用 ISO-8859-1 解析。解决:在HTMLBuilder.build返回的字符串前面加上<meta charset="utf-8">,或者写文件时用encoding='utf-8'参数。如果是在 Web 框架里直接返回 HTML 字符串,确保响应头里的Content-Type包含charset=utf-8。6. 二次开发:换模型、加字段、接接口的改造思路拿到源码跑通之后,下一步就是按自己的需求改。这个项目的结构对二次开发比较友好,几个常见的改造方向我按难度从低到高排一下。最简单的改造是调整输出格式。比如你不需要 HTML,而是想把结果直接写入数据库。可以在output/目录下新建一个json_builder.py,模仿html_builder.py的结构,把build方法的返回值改成 JSON 字符串。然后在pipeline.py里加一个output_format参数,根据参数选择用哪个 builder。这个改动大概 20 行代码,不需要动核心的检测和识别逻辑。稍微复杂一点的是替换识别模型。RapidOCR 用的是 PP-OCRv4 的识别模型,如果你有自己训练的模型(比如专门识别手写体或特定字体),可以导出成 ONNX 格式,然后替换models/rec_model.onnx。需要注意的是,自定义模型的输入尺寸和字符集可能和原模型不一样,要同步修改config.yaml里的rec_image_shape和charset_path。我一般会先用onnxruntime的session.get_inputs()打印出新模型的输入形状,确认和代码里的预处理逻辑匹配。再进一步是加一个 Web 接口。项目本身是命令行工具,但实际使用时往往需要让其他系统调用。可以用 FastAPI 包一层:from fastapi import FastAPI, UploadFile from core.pipeline import OCRPipeline app = FastAPI() pipeline = OCRPipeline(config_path="config/config.yaml") @app.post("/ocr") async def ocr_endpoint(file: UploadFile): # 保存上传的图片到临时文件 contents = await file.read() with open("/tmp/upload.jpg", "wb") as f: f.write(contents) # 调用 pipeline 识别 result = pipeline.process("/tmp/upload.jpg") return {"html": result["html"], "text": result["raw_text"]}这个接口接收图片上传,返回识别结果。部署时用uvicorn main:app --host 0.0.0.0 --port 8000启动。如果并发量不大,单 worker 就够了;如果有多人同时用,可以加--workers 4,但要注意每个 worker 都会加载一份模型,内存占用会翻倍。还有一个容易被忽略的改造点是错误处理。原项目的pipeline.process在遇到无法读取的图片时会直接抛异常。在生产环境里,更好的做法是捕获异常并返回一个结构化的错误信息。我一般会在pipeline.py里加一个try/except,把异常类型和图片路径记录到日志,然后返回一个包含error字段的字典。这样上层调用方可以根据error字段判断是重试还是跳过。最后说一个我踩过的坑:替换模型后一定要用同一批测试图片对比新旧模型的效果。有一次我换了一个号称“更高精度”的识别模型,结果在发票数据集上准确率反而下降了,原因是新模型对数字和字母的区分度不如原模型。从那以后我每次换模型都强制走一遍回归测试,至少跑 50 张覆盖不同版式的图片,对比字段级的准确率。希望这些经验能帮到你,少走点弯路。本文还有配套的精品资源,点击获取