Python OCR图片识别源码:rapidOCR实现图片转HTML结构化输出
简介这是一份基于 rapidOCR 改造的完整 OCR 图片识别源码项目面向需要快速集成图片转文字能力的 Python 开发者与后端工程师。项目已实现 OCR 识别并直接返回带标记的 HTML 结构可嵌入其他页面或复制使用内置 /engine/ocr 接口支持 HTTP 图片、本地文件、文件流等多种输入形式适合文档数字化、票据识别、网页内容提取等场景。压缩包共 46 个文件约 14.26MB以 23 个 py 源码与 17 个 pyc 编译文件为主体另含 3 个 onnx 模型文件、1 个 yaml 配置、1 个 html 页面及说明文本目录涵盖 rapid_ocr 核心模块、web_service 服务入口与 common 工具封装结构清晰便于二次开发。目前已有 1739 人学习下载读者可借此掌握 OCR 引擎调用、模型加载与 HTML 结果渲染的完整链路并参考接口封装与配置方式快速落地到自身项目。1. 从一张发票截图说起这套 OCR 源码到底能干什么上周有个做财务系统的朋友甩过来一张发票截图问我能不能把上面的抬头、税号、金额自动抠出来。我第一反应是调云服务 API但他那边是内网环境数据不能出本地。于是我翻出了这套基于 Python 实现的 OCR 图片识别源码——它最直接的价值就是你给它一张图它还你一段带结构的 HTML文字位置、层级关系都标好了不是干巴巴的一串字符。这套源码的核心是从 rapidOCR 改造过来的但作者做了一件很关键的事把识别结果重新组织成了 HTML 结构。这意味着你拿到的不是纯文本而是带坐标和标签的富文本前端可以直接渲染后端可以按区域提取字段。适合谁用做票据识别、证件录入、文档数字化的开发者尤其是那些需要在本地跑、不想依赖外部接口的场景。图片转文字这件事本身不新鲜但能把版面结构一起返回的轻量级方案确实省了不少后处理功夫。2. 技术选型拆解为什么是 rapidOCR 而不是 Tesseract2.1 识别引擎的取舍逻辑这套源码没有用 Tesseract而是选了 rapidOCR 作为底座。原因很实际Tesseract 对中文混排的支持一直是个玄学尤其是发票、合同这类有表格线的场景识别率波动很大。rapidOCR 底层用的是 PaddleOCR 的轻量模型但做了推理优化不需要你装 PaddlePaddle 那一整套重依赖。源码里把 rapidOCR 的检测、识别、方向分类三个模块串成了流水线。检测模块负责找出文字区域识别模块负责把区域转成字符方向分类模块处理旋转文本。这三个模块的权重文件是独立加载的意味着你可以按需替换——比如你的场景全是横排文字方向分类那部分可以直接跳过省一点推理时间。# 核心初始化逻辑简化示意 from rapidocr_onnxruntime import RapidOCR # 初始化引擎指定检测和识别模型路径 engine RapidOCR( det_model_pathmodels/det.onnx, # 文字检测模型 rec_model_pathmodels/rec.onnx, # 文字识别模型 cls_model_pathmodels/cls.onnx, # 方向分类模型 use_angle_clsTrue, # 是否启用方向分类 det_limit_side_len960 # 检测时图像长边限制 )这里有几个参数值得注意。use_angle_cls控制是否启用方向分类如果你的图片都是正向拍摄的设为 False 能省掉一次推理。det_limit_side_len决定了输入图像的长边缩放尺寸设太大显存吃紧设太小小字识别不出来960 是个比较平衡的值。我一般会先跑一遍默认参数看识别结果里有没有漏字再针对性调整。2.2 HTML 结构输出的实现思路识别完文字只是第一步这套源码真正花心思的地方在于把结果组织成 HTML。它没有用复杂的版面分析模型而是基于检测框的坐标做了一套简单的聚类规则同一行内的框按 x 坐标排序行与行之间按 y 坐标排序然后根据框的高度和间距判断段落归属。def build_html_structure(ocr_results): ocr_results: [(box, text, score), ...] box: [[x1,y1],[x2,y2],[x3,y3],[x4,y4]] # 按 y 坐标排序划分行 sorted_results sorted(ocr_results, keylambda r: min(p[1] for p in r[0])) lines [] current_line [] last_y None for box, text, score in sorted_results: y_center sum(p[1] for p in box) / 4 if last_y is None or abs(y_center - last_y) 15: # 15px 行高阈值 current_line.append((box, text, score)) else: lines.append(sorted(current_line, keylambda x: min(p[0] for p in x[0]))) current_line [(box, text, score)] last_y y_center if current_line: lines.append(sorted(current_line, keylambda x: min(p[0] for p in x[0]))) # 生成 HTML html_parts [] for line in lines: line_text .join(item[1] for item in line) html_parts.append(fp{line_text}/p) return \n.join(html_parts)这段代码的逻辑很直白先按 y 坐标把文字框分行行内再按 x 坐标排序最后每行包一个p标签。行高阈值 15px 是个经验值图片分辨率高的时候要相应调大。如果你需要更精细的结构比如识别表格可以在这个基础上加列对齐判断——同一列的文字框 x 坐标应该接近。注意这个聚类逻辑对倾斜拍摄的图片效果会打折扣因为 y 坐标排序的前提是文本行基本水平。如果图片倾斜超过 10 度建议先做一次透视校正。3. 跑起来再说环境配置与最小可运行示例3.1 依赖安装与模型准备这套源码的依赖不算多核心就是 onnxruntime 和 opencv。onnxruntime 负责模型推理opencv 负责图像预处理。我建议用 conda 建一个独立环境避免和系统里的其他包打架。# 创建虚拟环境 conda create -n ocr_demo python3.9 conda activate ocr_demo # 安装核心依赖 pip install onnxruntime1.16.0 pip install opencv-python4.8.0 pip install numpy1.24.0 pip install pillow10.0.0 # 如果要用 rapidocr 的封装 pip install rapidocr-onnxruntime1.3.0模型文件需要单独下载一般放在项目根目录的models/文件夹下。检测模型、识别模型、方向分类模型各一个 onnx 文件加起来大概 15MB 左右。源码的 README 里通常会给出下载链接如果没有可以去 rapidOCR 的官方仓库找对应版本的模型。3.2 最小可运行脚本下面这个脚本是我从源码里抽出来的最小闭环去掉业务逻辑后大概 40 行能直接跑通图片到 HTML 的完整流程。import cv2 import numpy as np from rapidocr_onnxruntime import RapidOCR def image_to_html(image_path, output_pathoutput.html): # 1. 读取图片 img cv2.imread(image_path) if img is None: raise FileNotFoundError(f图片读取失败: {image_path}) # 2. 初始化 OCR 引擎 engine RapidOCR( det_model_pathmodels/det.onnx, rec_model_pathmodels/rec.onnx, cls_model_pathmodels/cls.onnx, use_angle_clsTrue ) # 3. 执行识别 result, elapse engine(img) # result 格式: [[box, text, score], ...] if not result: print(未识别到文字) return # 4. 按行聚类并生成 HTML sorted_result sorted(result, keylambda r: min(p[1] for p in r[0])) lines [] current_line [] last_y None for box, text, score in sorted_result: y_center sum(p[1] for p in box) / 4 if last_y is None or abs(y_center - last_y) 15: current_line.append((box, text, score)) else: lines.append(sorted(current_line, keylambda x: min(p[0] for p in x[0]))) current_line [(box, text, score)] last_y y_center if current_line: lines.append(sorted(current_line, keylambda x: min(p[0] for p in x[0]))) # 5. 写入 HTML 文件 html [!DOCTYPE htmlhtmlheadmeta charsetutf-8/headbody] for line in lines: line_text .join(item[1] for item in line) html.append(fp{line_text}/p) html.append(/body/html) with open(output_path, w, encodingutf-8) as f: f.write(\n.join(html)) print(f识别完成共 {len(lines)} 行结果已写入 {output_path}) if __name__ __main__: image_to_html(test_invoice.jpg)跑这个脚本之前确认models/目录下有那三个 onnx 文件。engine(img)返回的result是一个列表每个元素是[box, text, score]box 是四个点的坐标text 是识别出的文字score 是置信度。elapse是各阶段的耗时调试性能的时候可以打印出来看。3.3 识别效果验证与参数微调第一次跑建议拿一张文字清晰的截图比如网页截图或者扫描件。如果识别结果里出现大量乱码先检查图片是不是 RGB 三通道——opencv 读进来默认是 BGR但 rapidOCR 内部会处理一般不用手动转。如果漏字严重把det_limit_side_len调大到 1280 试试代价是推理时间增加。置信度低于 0.5 的结果我一般会过滤掉但发票上的小字金额有时候置信度就是上不去这时候宁可留着人工复核也别一刀切。源码里默认没有做置信度过滤你可以自己在生成 HTML 之前加一层判断。4. 核心模块拆解从图像预处理到 HTML 渲染的完整链路4.1 图像预处理模块源码里的预处理比想象中简单没有做复杂的去噪或二值化主要就是尺寸调整和通道转换。这其实是个明智的选择——过度预处理反而会丢失文字边缘信息让识别模型犯迷糊。def preprocess_image(img, limit_side_len960): 将图像长边缩放到指定尺寸保持宽高比 h, w img.shape[:2] max_side max(h, w) if max_side limit_side_len: scale limit_side_len / max_side new_w int(w * scale) new_h int(h * scale) img cv2.resize(img, (new_w, new_h), interpolationcv2.INTER_LINEAR) # 确保是 3 通道 if len(img.shape) 2: img cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) return imglimit_side_len这个参数控制的是长边缩放上限。为什么是长边而不是固定宽高因为文字识别对分辨率敏感但不同图片的宽高比差异很大固定尺寸会拉伸变形。按长边等比缩放能保证文字不变形同时控制计算量。插值方式用INTER_LINEAR就够了INTER_CUBIC虽然更平滑但速度慢一倍对识别率提升微乎其微。4.2 检测与识别流水线检测模块输出的是文字区域的四边形框识别模块再把每个框裁出来转成字符。这两个模块之间有一个 ROI 裁剪的步骤源码里用 opencv 的透视变换实现。def crop_roi(img, box): 从原图中裁剪出四边形区域并矫正为矩形 box np.array(box, dtypenp.float32) # 计算目标矩形宽高 width int(max(np.linalg.norm(box[0] - box[1]), np.linalg.norm(box[2] - box[3]))) height int(max(np.linalg.norm(box[0] - box[3]), np.linalg.norm(box[1] - box[2]))) # 目标矩形四个角点 dst np.array([[0, 0], [width, 0], [width, height], [0, height]], dtypenp.float32) # 透视变换 M cv2.getPerspectiveTransform(box, dst) roi cv2.warpPerspective(img, M, (width, height)) return roi透视变换的好处是能把倾斜的文字框矫正成正矩形识别模块对正矩形输入的准确率明显更高。width和height取的是四边形对边的最大长度这样能保证裁剪区域完整覆盖文字。如果框本身是正矩形这个变换就退化成普通的裁剪没有额外开销。4.3 HTML 结构生成与样式注入生成 HTML 的时候源码默认只输出纯文本段落但你可以很容易地加上样式和坐标信息。我一般会在p标签上加>def generate_html_with_bbox(lines): 生成带坐标信息的 HTML html [!DOCTYPE htmlhtmlheadmeta charsetutf-8, stylep{margin:4px 0;font-size:14px;}/style/headbody] for line in lines: # 计算整行的包围盒 all_points [p for item in line for p in item[0]] x_min min(p[0] for p in all_points) y_min min(p[1] for p in all_points) x_max max(p[0] for p in all_points) y_max max(p[1] for p in all_points) line_text .join(item[1] for item in line) bbox_str f{x_min:.0f},{y_min:.0f},{x_max:.0f},{y_max:.0f} html.append(fp>def remove_table_lines(img): 去除表格线减少误检 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 二值化 binary cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 15, -2) # 横向长线 h_kernel cv2.getStructuringElement(cv2.MORPH_RECT, (img.shape[1]//20, 1)) h_lines cv2.morphologyEx(binary, cv2.MORPH_OPEN, h_kernel) # 纵向长线 v_kernel cv2.getStructuringElement(cv2.MORPH_RECT, (1, img.shape[0]//20)) v_lines cv2.morphologyEx(binary, cv2.MORPH_OPEN, v_kernel) # 合并并膨胀 lines cv2.add(h_lines, v_lines) lines cv2.dilate(lines, cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))) # 填充白色 img[lines 0] (255, 255, 255) return img这个函数放在preprocess_image之后调用。注意adaptiveThreshold的 block size 和 C 值需要根据图片对比度微调对比度低的扫描件把 C 值调到 -5 左右。6. 二次开发把识别结果接到业务系统里的几个改造点6.1 按区域提取字段而不是全文返回实际业务里很少需要整页 HTML更多是“把发票号码给我”。这时候可以在 HTML 生成之前加一层字段映射。思路是先定义每个字段的关键词和相对位置然后在识别结果里找匹配。def extract_field(ocr_results, keyword, directionright, max_distance200): 根据关键词提取右侧或下方的文字 direction: right 或 below max_distance: 最大搜索距离像素 keyword_box None for box, text, score in ocr_results: if keyword in text: keyword_box box break if keyword_box is None: return None # 关键词框的中心点 kx sum(p[0] for p in keyword_box) / 4 ky sum(p[1] for p in keyword_box) / 4 candidates [] for box, text, score in ocr_results: if box is keyword_box: continue cx sum(p[0] for p in box) / 4 cy sum(p[1] for p in box) / 4 if direction right and abs(cy - ky) 20 and 0 cx - kx max_distance: candidates.append((cx - kx, text)) elif direction below and abs(cx - kx) 50 and 0 cy - ky max_distance: candidates.append((cy - ky, text)) if candidates: candidates.sort(keylambda x: x[0]) return candidates[0][1] return None这个函数能处理大部分“标签在左、值在右”的场景。max_distance控制搜索范围发票号码一般离“号码”两个字不会超过 200px。如果字段值是多行文字把candidates全部返回再拼接。6.2 批量处理与异步化改造单张识别跑通了下一步就是批量。源码里是同步阻塞的处理 100 张图要等很久。改造方向有两个多进程池和异步队列。from concurrent.futures import ProcessPoolExecutor import glob def batch_process(image_dir, output_dir, workers4): 多进程批量处理 image_paths glob.glob(f{image_dir}/*.jpg) glob.glob(f{image_dir}/*.png) with ProcessPoolExecutor(max_workersworkers) as executor: futures [] for img_path in image_paths: output_path f{output_dir}/{img_path.split(/)[-1].rsplit(., 1)[0]}.html futures.append(executor.submit(image_to_html, img_path, output_path)) for future in futures: try: future.result() except Exception as e: print(f处理失败: {e})workers的数量建议设为 CPU 核心数因为 onnxruntime 默认用满所有核心开太多进程反而会互相抢资源。如果图片数量超过 1000 张建议改成消息队列模式用 Redis 或者 RabbitMQ 做任务分发避免一次性把所有路径加载到内存。6.3 模型替换与自定义训练rapidOCR 的模型是可以替换的。如果你有特定场景的数据比如手写体或者特殊字体可以用 PaddleOCR 的训练工具 fine-tune 一个识别模型然后导出成 onnx 替换掉默认的rec.onnx。替换的时候注意字典文件要同步换否则识别结果会错位。导出 onnx 的命令大概是这样paddle2onnx --model_dir ./inference_model \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file ./models/custom_rec.onnx \ --opset_version 11 \ --enable_onnx_checker Trueopset_version建议用 11兼容性最好。导出之后用onnxruntime加载测试一下确认输入输出 shape 和原模型一致。输入一般是[1, 3, 32, 320]输出是[1, seq_len, num_classes]。6.4 一个我踩过的坑去年做票据识别的时候我图省事直接把det_limit_side_len设成了 1920想着分辨率越高识别越准。结果测试集上准确率确实涨了两个点但线上环境一跑就崩——并发上来之后内存直接爆了。后来把参数降到 960准确率只掉了 0.5 个点内存占用降了 60%。从那以后我每次调参都强制走一遍压力测试用locust或者wrk模拟 50 并发盯着内存和 CPU 曲线看。OCR 这类推理服务参数调优的收益往往抵不过资源消耗的代价平衡点比极值重要得多。希望这套源码能帮你把图片转文字这件事跑通少走点我当年走过的弯路。本文还有配套的精品资源点击获取