news 2026/10/2 13:21:02

PaddleOCR多进程GPU推理实战:200万张图片批量OCR提速方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PaddleOCR多进程GPU推理实战:200万张图片批量OCR提速方案

上个月有个朋友找我,说手里攒了200万张图片要做OCR,按他之前的单进程脚本,估摸着要跑一周多,问我有没有更快的方案。这个需求不复杂,但特别容易做砸:有人第一反应是上多线程,结果在Python的GIL面前撞墙;有人直接开十个进程裸跑PaddleOCR,显卡立刻OOM。PaddleOCR本身对单张图的推理已经很快了,难的是怎么把“单张快”变成“批量快”。

这篇文章把我验证过的方案完整拆开讲:PaddleOCR多进程GPU推理到底怎么设计、进程数怎么定、显存怎么分、任务怎么分发、断点续跑怎么做,以及一套可以直接拿去用的生产级代码。适合刚接触PaddleOCR、需要批量处理几十万到几百万张图片的读者参考。

1. 200万张图的吞吐瓶颈:慢的不只是显卡

1.1 先算一笔账:单进程到底要跑多久

不要急着写代码,先估算需求量级。假设一张普通截图或文档照片,PaddleOCR用GPU推理,检测加方向分类加识别全流程大约耗时50到100毫秒。取个中间值80毫秒,200万张图单进程就是160000秒,约44小时,几乎两天两夜不关机。

如果图片更复杂,比如自然场景、密集的小字票据、超长图,单张耗时很轻松翻到200毫秒以上,那单进程就是要跑一周的量级。这种任务用单进程硬扛,时间和机器稳定性都是巨大风险,必须上并行。

1.2 拆解PaddleOCR的单张链路:GPU只占一小截

很多人以为OCR的瓶颈全在GPU,实际拆开PaddleOCR处理一张图的完整流程,会看到一串串行操作:

  • 图片解码,cv2.imread或者imdecode,在CPU上执行
  • 缩放、归一化等预处理,CPU执行
  • 文本检测模型前向推理,GPU执行
  • 根据检测框裁剪文本区域,CPU执行
  • 方向分类模型推理(如果启用),GPU执行
  • 文本识别模型推理,GPU执行
  • CTC解码、置信度计算等后处理,CPU执行

一张图下来,GPU真正参与的可能只占一半时间。剩下的CPU解码、预处理、后处理、Python调用开销全是隐藏成本。所以当你想提速,不能只盯着显卡,还得把CPU核数、磁盘IO、进程调度一起算进去。这也是后面多进程方案能明显生效的根本原因:它同时吃满了GPU和多个CPU核。

1.3 为什么多线程和多卡batch都没想象中好用

先泼一盆冷水:Python多线程在这个场景里基本没有收益。PaddleOCR的Python层有大量预处理、后处理和对象封装逻辑,这些代码受GIL限制,多个线程并不能真正并行执行。虽然paddle的C++算子内部会释放GIL,但整个pipeline里Python侧占比太高,多线程实测往往连30%的加速都拿不到。

那直接把很多张图塞给PaddleOCR做batch呢?PaddleOCR 3.x的predict确实支持传入图片列表,识别模型内部会组batch。但检测阶段对变长输入很不友好,padding会浪费大量算力,而且batch大了显存直线上升。对200万张尺寸参差不齐的图片来说,batch方案要处理的分支太多,不如多进程方案通用、稳定。

多进程的本质是:每个worker进程自己初始化一份PaddleOCR实例,独占自己的CPU线程和显存上下文,互不干扰。进程间用队列传图片路径,几乎不传大对象,通信开销小,扩展起来也容易。

2. GPU版PaddleOCR环境准备:装对版本就避开一半的坑

2.1 安装:paddlepaddle-gpu和paddleocr版本要配套

PaddleOCR本身只是上层调用库,真正的推理引擎是PaddlePaddle。装GPU版时最容易犯的错是装了CPU版paddle,代码不报错,但全程跑CPU。安装前先确认CUDA版本,对应关系在Paddle官网有明确说明。大致命令是:

# 以CUDA 11.8为例 python3 -m pip install paddlepaddle-gpu==2.6.1 -i https://mirror.baidu.com/pypi/simple python3 -m pip install paddleocr==2.7.3 # 新版PaddleOCR 3.x则直接装最新paddlepaddle-gpu python3 -m pip install paddlepaddle-gpu paddleocr

我习惯装完后立刻跑一次官方自检:

python3 -c "import paddle; paddle.utils.run_check()"

看到PaddlePaddle is installed successfully! Let's start deep learning with PaddlePaddle now.说明paddle本身没问题。然后确认CUDA编译状态:

python3 -c "import paddle; print('cuda:', paddle.is_compiled_with_cuda())"

输出True再继续,否则说明装的是CPU版本,要用-i https://mirror.baidu.com/pypi/simple重装。

2.2 确认GPU真的被用上:两步验证

很多人在这一步栽跟头。paddle编译带CUDA不代表PaddleOCR推理就用了GPU,还要看实际运行时的显存占用。最直接的方法:跑一张图的同时,另开终端执行nvidia-smi,观察有没有python进程占显存。

如果发现显存一直是0,检查两处:一是PaddleOCR初始化时是否传了device="gpu:0"(3.x)或use_gpu=True(2.x);二是环境变量CUDA_VISIBLE_DEVICES是否把GPU屏蔽了。

提示:如果用的是PaddleOCR 2.x,初始化时gpu_mem默认是8000MB,这只是初始显存池大小,不代表固定占用。实际峰值要跑一批图后看nvidia-smi。

2.3 PaddleOCR 2.x与3.x的API差异:初始化、调用和返回值

PaddleOCR在3.0版本前后API变化很大,网上教程大量混着2.x和3.x的写法,初学者很容易抄岔。我列个简表:

项目PaddleOCR 2.xPaddleOCR 3.x
初始化设备参数use_gpu=True, gpu_id=0device="gpu:0"
方向分类开关use_angle_cls=Truetextline_orientation=True
推理入口ocr.ocr(img, cls=True)ocr.predict(img)
返回结构嵌套list,内层是[框坐标, (文本, 置信度)]List[Dict],key包括rec_texts/rec_scores/rec_polys

因为版本差异大,我建议在项目代码里写一个小适配函数,先读paddleocr.__version__再决定初始化参数和调用方式。后面核心代码部分会给出完整实现。这个适配成本很低,但能让你在不同环境、不同教程之间无缝切换。

3. 多进程方案设计:任务怎么分、进程数怎么定、显存怎么省

3.1 架构选型:主调度+worker进程,而不是裸Pool

很多教程会用multiprocessing.Pool直接开进程池,代码短,但200万张图面前有几个问题:进度不好统计、异常不好兜底、断点续跑要额外做。我推荐用“主调度进程 + N个worker进程 + 两个队列”的结构:

  • 主进程:扫描图片目录,构建待处理列表,向任务队列分发热门路径,同时从结果队列收集进度和失败信息
  • 任务队列:存放待处理图片路径,注意要限制大小,防止一次性把200万个字符串塞进内存和管道缓冲
  • 结果队列:worker把每张图的处理结果状态回传,主进程负责打印进度、累计失败、写日志
  • N个worker进程:各自初始化PaddleOCR实例,循环从任务队列取图片,处理单张,结果直接落盘

这个结构下,主进程和worker之间只传字符串路径和轻量状态消息,不传图片数据,通信开销很低。而且worker独立崩溃不影响其他进程,主进程能感知到。

3.2 为什么每个worker必须自己初始化PaddleOCR

这是多进程方案里最重要的一个设计决策。不要在主进程初始化PaddleOCR然后传给子进程用。原因有两个:

第一,CUDA context和进程强相关。fork出来的子进程虽然能继承父进程内存,但CUDA context在fork后使用会出现各种诡异问题,轻则警告,重则直接崩。安全做法是每个worker进程启动后,自己在内部初始化PaddleOCR。

第二,PaddleOCR实例内部有线程池和显存池,多个进程各自维护一套,反而隔离干净。每个worker各用各的模型副本,不会出现并发读写的竞争问题。

代价是每个worker加载模型需要几秒到几十秒,但对200万张图来说,启动开销可以忽略。如果是小批量测试,会觉得这个设计很笨重,但规模上来后这是最稳的。

3.3 进程数估算:显存和CPU核数两个约束

进程数不是越大越好。主要有两个约束:

显存约束:每个worker初始化PaddleOCR后,显存初始池可以设小,但推理峰值会根据图片分辨率浮动。以PP-OCRv4移动版模型为例,单worker峰值显存约1.5到2.5GB。用12GB显卡,最多也就开4到5个worker,再多直接OOM。

CPU约束:每个worker的图片解码、预处理、后处理都要吃CPU。单worker建议分配1到1.5个CPU核。8核机器开4个worker基本就到头了,再开多CPU上下文切换反而拖慢吞吐。

我实测下来,一份可参考的配置表如下:

显卡显存单worker显存池设定建议worker数
6GB256MB2
8GB512MB2到3
12GB512MB到1GB3到4
24GB1GB6到8
48GB1到2GB8到12

注意:这个表针对通用中文识别移动版模型,如果换server版模型,显存占用要翻倍。

3.4 多卡和Windows下的特殊注意

如果机器有多张显卡,最简单的分配方式是按worker编号取模:gpu_id = worker_id % n_gpu。比如2张卡4个worker,那么worker0和worker2共用卡0,worker1和worker3共用卡1。这样轮询分配能让多卡负载基本均衡。

Windows下有个坑:multiprocessing默认用spawn方式启动子进程,子进程会重新import主模块,所以代码必须用if __name__ == "__main__":保护。另外spawn模式下日志、全局变量都不会自动继承,worker里需要重新配置。Linux和macOS默认fork模式,则没有这些问题。

4. 生产级代码:任务队列、原子写盘、进度统计一体的实现

4.1 任务清单和断点标记:靠结果文件判断已完成

200万张图的任务,最怕跑了一半断电、报错、被人Ctrl+C。所以断点续跑不是可选项,是必选项。

我的做法:每张源图对应一个结果文件,结果文件名由源图路径的MD5哈希决定,存放在输出目录下的两级分桶目录里。比如:

ocr_output/results/ ab/ abc123....json abd456....json cd/ cde789....json

启动时扫描所有图片,对每张图计算目标结果文件路径,如果结果文件已存在,就跳过。这样断点续跑的判断逻辑简单到极致:结果文件存在等于处理完成。不需要单独维护任务清单数据库,也不用担心状态不同步。

分桶目录的原因也很实际:200万个json直接放一个目录,ls都要卡半天,文件系统性能会急剧下降。按哈希前两位分256个桶,每个桶平均不到1万个文件,IO压力小很多。

4.2 worker主体:兼容2.x/3.x的结果解析与异常兜底

下面这段是worker的核心逻辑,完整实现了模型初始化、单图处理、兼容解析、失败重试。先看依赖和初始化:

import argparse import hashlib import json import logging import os import time from pathlib import Path from queue import Empty from threading import Thread from multiprocessing import Process, Queue import cv2 import numpy as np logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", ) logger = logging.getLogger("main") def imread_unicode(path): """解决Windows下OpenCV读取中文路径失败的问题""" try: return cv2.imread(path) except Exception: data = np.fromfile(path, dtype=np.uint8) return cv2.imdecode(data, cv2.IMREAD_COLOR) def build_ocr(args, worker_id=0): """根据PaddleOCR版本自动适配初始化参数""" from paddleocr import PaddleOCR import paddleocr major = int(paddleocr.__version__.split(".")[0]) if major >= 3: kwargs = dict( lang=args.lang, device=f"gpu:{args.gpu_id}", ) if args.use_angle_cls: kwargs["textline_orientation"] = True else: kwargs = dict( lang=args.lang, use_gpu=True, gpu_id=args.gpu_id, gpu_mem=args.gpu_mem, use_angle_cls=args.use_angle_cls, ) ocr = PaddleOCR(**kwargs) return ocr, major

构建OCR时有两个细节值得提:一是gpu_mem建议设成512到1024,别用默认的8000,多进程下会爆显存;二是新版3.x如果提示textline_orientation参数不存在,可以直接去掉这个参数,或者改用inspect.signature(PaddleOCR.__init__)看一眼当前版本支持哪些参数。

接着是单图处理函数,这里做了两轮重试,并实现了原子写盘:

def parse_result(result, major): """解析PaddleOCR返回值,兼容2.x和3.x结构""" texts = [] if major >= 3: for r in result: if not isinstance(r, dict): continue rec_texts = r.get("rec_texts") or r.get("texts") or [] rec_scores = r.get("rec_scores") or r.get("scores") or [] rec_polys = r.get("rec_polys") or r.get("dt_polys") or [] for i, text in enumerate(rec_texts): score = rec_scores[i] if i < len(rec_scores) else None poly = rec_polys[i] if i < len(rec_polys) else None if hasattr(poly, "tolist"): poly = poly.tolist() texts.append({ "text": str(text), "confidence": float(score) if score is not None else None, "box": poly, }) else: # 兼容2.x不同版本的嵌套结构 if result and isinstance(result[0], list) and result[0] and isinstance(result[0][0], list): result = result[0] for item in result: try: box, text_score = item[0], item[1] text, score = text_score[0], text_score[1] texts.append({ "box": box, "text": str(text), "confidence": float(score), }) except Exception: continue return texts def process_one(ocr, major, img_path, out_path, args): st = time.time() last_err = None for attempt in range(2): try: if attempt == 0: img = imread_unicode(img_path) if img is None: raise ValueError("图片无法解码") if major >= 3: result = ocr.predict(input=img) else: result = ocr.ocr(img, cls=args.use_angle_cls) else: # 第一次失败后尝试直接传路径,让PaddleOCR内部自己解码 if major >= 3: result = ocr.predict(input=img_path) else: result = ocr.ocr(img_path, cls=args.use_angle_cls) texts = parse_result(result, major) payload = { "image_path": img_path, "texts": texts, "count": len(texts), "elapsed_ms": round((time.time() - st) * 1000, 2), "ts": time.time(), } out_path.parent.mkdir(parents=True, exist_ok=True) tmp_path = out_path.with_suffix(".json.tmp") with open(tmp_path, "w", encoding="utf-8") as f: json.dump(payload, f, ensure_ascii=False, indent=1) os.replace(tmp_path, out_path) return True, None except Exception as e: last_err = e time.sleep(0.2) return False, str(last_err)

原子写盘是最容易被忽略的细节。先写到.json.tmp,再os.replace换成正式文件名。这样即使进程在写文件瞬间被杀,也只会留下一个.tmp垃圾文件,不会出现半截json。断点续跑时,不完整的.tmp不会影响结果文件存在性判断,下次重跑会重新处理源图。

然后定义worker循环:

def worker_loop(worker_id, task_queue, progress_queue, args): try: ocr, major = build_ocr(args, worker_id) except Exception as e: logger.error("worker %d 模型初始化失败: %s", worker_id, e) return logger.info("worker %d 模型加载完成", worker_id) idle_times = 0 while True: try: img_path = task_queue.get(timeout=3) idle_times = 0 except Empty: idle_times += 1 if idle_times > 20: logger.warning("worker %d 连续空闲,退出", worker_id) break continue except (EOFError, OSError): break if img_path is None: break out_path = result_path_for(img_path, args.output) if out_path.exists(): progress_queue.put({"type": "skip", "image": img_path, "ts": time.time()}) continue ok, err = process_one(ocr, major, img_path, out_path, args) progress_queue.put({ "type": "ok" if ok else "fail", "image": img_path, "error": err, "ts": time.time(), })

idle_times是防止毒丸丢失导致worker永远空转的兜底机制。正常情况下每个worker会收到一个None毒丸后退出;万一队列状态异常,连续空闲60秒后也会安全退出。

4.3 主调度循环:进度、ETA、失败收集、Ctrl+C优雅退出

主进程负责扫描图片、分发任务、收集结果、打印进度。完整代码如下:

def scan_images(root, exts): files = [] for ext in exts.split(","): ext = ext.strip().lower() files.extend([str(p) for p in Path(root).rglob("*" + ext)]) files.extend([str(p) for p in Path(root).rglob("*" + ext.upper())]) return sorted(set(files), key=lambda x: x.lower()) def result_path_for(img_path, output_dir): h = hashlib.md5(img_path.encode("utf-8")).hexdigest() return Path(output_dir) / "results" / h[:2] / f"{h}.json" def main(): args = parse_args() Path(args.output).mkdir(parents=True, exist_ok=True) images = scan_images(args.input, args.exts) todo = [p for p in images if not result_path_for(p, args.output).exists()] done_before = len(images) - len(todo) logger.info("扫描到 %d 张图片,历史已完成 %d 张,待处理 %d 张", len(images), done_before, len(todo)) if not todo: logger.info("没有待处理任务,直接退出") return task_queue = Queue(maxsize=args.queue_size) progress_queue = Queue() def feeder(): for img in todo: task_queue.put(img) for _ in range(args.workers): task_queue.put(None) feeder_thread = Thread(target=feeder, daemon=True) feeder_thread.start() workers = [] for i in range(args.workers): p = Process(target=worker_loop, args=(i, task_queue, progress_queue, args)) p.start() workers.append(p) fail_fp = open(Path(args.output) / "failed.txt", "a", encoding="utf-8") done_count = 0 fail_count = 0 start_ts = time.time() latest_print = 0.0 def handle_message(msg): nonlocal done_count, fail_count if msg["type"] == "fail": fail_count += 1 fail_fp.write(json.dumps(msg, ensure_ascii=False) + "\n") fail_fp.flush() else: done_count += 1 try: while any(p.is_alive() for p in workers): try: msg = progress_queue.get(timeout=0.5) except Empty: continue handle_message(msg) now = time.time() finished = done_count + fail_count if finished > 0 and now - latest_print >= 5: elapsed = now - start_ts rate = finished / elapsed remain = len(todo) - finished eta = remain / rate if rate > 0 else float("inf") latest_print = now logger.info( "已完成 %d / %d | 成功 %d 失败 %d | 速率 %.2f 张/s | ETA %.2f 小时", finished, len(todo), done_count, fail_count, rate, eta / 3600, ) except KeyboardInterrupt: logger.warning("收到Ctrl+C,正在终止worker...已处理结果保留,重新运行可断点续跑") for p in workers: p.terminate() for p in workers: p.join() fail_fp.close() return while True: try: msg = progress_queue.get_nowait() except Empty: break handle_message(msg) for p in workers: p.join() fail_fp.close() logger.info( "全部结束 | 成功 %d 张 失败 %d 张 | 总耗时 %.1f 分钟", done_count, fail_count, (time.time() - start_ts) / 60, )

feeder线程负责把待处理图片批量放入任务队列,因为队列设置了maxsize,放满会自动阻塞,正好实现了“生产者-消费者”的背压机制,不会把200万个路径一次性推进管道缓冲区导致内存暴涨。

进度打印每5秒一次,包含完成数、成功率、实时速率和剩余时间估算。这个信息在实际跑批时非常关键,你能直观看到方案是否达到预期,而不是盲跑。

入口处加上参数解析和if __name__ == "__main__"保护:

def parse_args(): p = argparse.ArgumentParser(description="PaddleOCR多进程GPU推理") p.add_argument("--input", required=True, help="图片根目录") p.add_argument("--output", default="ocr_output", help="结果输出目录") p.add_argument("--workers", type=int, default=4, help="worker进程数") p.add_argument("--gpu-id", type=int, default=0, help="使用的GPU编号") p.add_argument("--gpu-id-per-worker", action="store_true", help="多个GPU轮询分配") p.add_argument("--gpu-mem", type=int, default=1024, help="单worker显存池大小(MB)") p.add_argument("--use-angle-cls", action="store_true", help="启用方向分类器") p.add_argument("--lang", default="ch", help="识别语言") p.add_argument("--queue-size", type=int, default=2000, help="任务队列上限") p.add_argument("--exts", default=".jpg,.jpeg,.png,.bmp,.tif,.tiff,.webp", help="图片扩展名") return p.parse_args() if __name__ == "__main__": main()

运行命令示例:

python3 ocr_parallel.py \ --input /data/images \ --output /data/ocr_output \ --workers 4 \ --gpu-id 0 \ --gpu-mem 1024 \ --use-angle-cls

5. 实测性能数据与调优路径

5.1 不同worker数下的吞吐对照

我在一台NVIDIA RTX 3080 10G、8核CPU、NVMe SSD的机器上,用普通中文发票、截图、文档照片混合数据集做了压测。图片平均单张耗时约70到100毫秒。不同worker数的吞吐如下:

worker数吞吐(张/秒)加速比说明
1121.0基准
2231.92接近线性
3302.5显存和CPU都还有余量
4322.67开始触及CPU解码瓶颈
5292.42不升反降,上下文切换变多

200万张图在4个worker下大约是32张/秒,一小时11.5万张,总耗时约17个小时。对比单进程的44小时,提速2.7倍左右。如果你的显卡更强、CPU核心更多,提速比例还会更高。

这里必须强调:具体数字受显卡型号、CPU性能、图片复杂度、存储介质影响极大。尤其是图片内容,同样一张截图可能40毫秒完成,一张密集的A4文档可能180毫秒。不要拿别人的数字当自己的指标,跑起来后看日志里的实时速率才最准。

5.2 影响吞吐的隐形因素:cpu_threads、gpu_mem、图片尺寸

很多人调了半天发现性能上不去,问题往往出在几个不起眼的参数上。

cpu_threads:PaddleOCR初始化时默认会给每个实例分配一定数量的CPU线程。多进程下如果每个worker都默认开10个线程,4个worker就是40个线程,8核机器直接爆炸,全部时间耗在线程切换上。建议在初始化时手动传cpu_threads=2,或者用paddle.set_num_threads(2)控制。

gpu_mem:这个是显存池初始大小,不是上限。设太大会让多进程瞬间把显存占满,设太小会导致频繁重新分配,影响速度。我在代码里用1024MB,实际可以根据nvidia-smi观察结果微调。

图片尺寸:PaddleOCR检测阶段默认会限制最长边,通常960像素。如果原图分辨率远高于这个值,检测前会先缩小,小字可能识别不清;如果调高这个限制,速度会明显下降。200万张图的话,我建议先抽样50张看文字大小分布,再决定是否需要改这个参数。

还有一个容易忽略的点:传numpy数组给predict比传图片路径少一次内部路径解析和文件检查,实测大约能省几毫秒。代码里第一轮就是先imread再传数组,就是这个原因。

5.3 一张调优检查清单

按优先级排下来,批量跑之前过一遍:

  • 确认装的是GPU版paddle,is_compiled_with_cuda()为True
  • 每个worker单独初始化PaddleOCR,不要复用主进程实例
  • 单worker的cpu_threads调到2到4,避免线程爆炸
  • 单worker的gpu_mem设512到1024,避免显存池爆掉
  • 图片方向统一的话,关闭方向分类器,能省一次前向推理
  • 先用1000张图小批量跑通,确认结果格式正确后再放开全量
  • 观察nvidia-smi的显存占用和日志里的吞吐速率,据此微调worker数
  • 确认输出目录是SSD,机械盘在200万小文件写入场景会成为新瓶颈

6. 断点续跑与故障兜底:200万级任务不能少的几件事

6.1 中断之后重启:零损失的恢复依赖原子写盘

跑200万张图,几乎不可能一次跑完不出意外。断电、有人误操作、显卡驱动崩溃、killed都会发生。这套方案跑了一半被中断,大部分已完成的结果已经通过原子写盘落到了磁盘,重新执行一条同样的命令,会自动跳过所有已完成的图片,继续处理剩下的。

需要注意一个小细节:如果某张图正在被worker处理时进程被kill -9,它可能留下一半的.tmp文件。下次启动扫描时,结果文件不存在,这张图会被重新处理。.tmp文件本身虽然残留,但不会影响判断,定期手动清理一下即可。

6.2 OOM、损坏图片、超长图的应对

显存OOM是最常见的故障。如果一个worker在处理某张超高分辨率大图时显存尖峰超过显存总量,操作系统可能会杀掉进程甚至整个程序。我建议:一是在worker里做图片尺寸上限保护,超过比如4096x4096的图片先等比缩小再推理;二是把nvidia-smi的监控脚本挂上,一旦显存占用超过90%就告警。

损坏图片、截断的jpg、伪造后缀的文件在200万张里一定会出现。处理逻辑里cv2.imread返回None直接记录失败,不崩溃。重试一次后仍失败,会把错误信息发送给主进程,统一追加到failed.txt。

失败文件列表非常重要。跑完之后,对这几条失败记录单独处理,可能只是几十张图,人工或换一种解码方式就能补上。

6.3 结果抽检与二次清洗

批量跑完不等于工作结束。我建议从结果里随机抽取500到1000张,人工看一遍识别质量。重点看低置信度的结果,把置信度低于某个阈值(比如0.6)的文本单独导出来,分析是图片质量问题还是模型能力问题。

对200万级别的结果,最后脑子里一定要有清洗意识:全角半角统一、中文标点粘连修正、常见OCR错字替换、空文本过滤。我自己的做法是把所有结果灌进SQLite,按图片编号建索引,后续做检索、去重、二次标注都会方便很多。

这套方案跑通后,如果你想进一步提速,可以往两个方向走:一是换PaddleOCR的server版模型,精度和速度都有提升空间,但显存占用会更高;二是引入TensorRT推理后端,把三个模型都换成TensorRT引擎,吞吐还能往上走一截。不过在此之前,先把多进程这套基础架构稳定跑起来,数据量上来之后,再谈单卡极致优化也不迟。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 13:20:58

Android 7.0+ Whistle证书安装全路径指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 13:19:23

论文双检冲刺全攻略:一篇讲透查重与AIGC一起压降

很多同学在盲审前的最后两周都会遇到同一个窘境&#xff1a;查重率好不容易压到合格线&#xff0c;AIGC 检测却悄悄超标&#xff1b;反过来猛降 AIGC&#xff0c;重复率又反弹。查重和 AIGC 像两道先后收紧的闸&#xff0c;单独应对都不难&#xff0c;难的是在同一篇稿子里把两…

作者头像 李华
网站建设 2026/10/2 13:16:23

凸优化入门:凸函数定义、判定条件与主流求解方法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 13:16:18

回归评价指标详解:MSE、RMSE、MAE与R²的选择与实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 13:15:46

信息技术服务实战:从SLA保障到业务价值交付

1. 这不是教科书里的“信息技术服务”&#xff0c;而是每天在客户会议室里反复推演的真实战场“第3章 信息技术服务&#xff08;一&#xff09;”——光看这个标题&#xff0c;很多人第一反应是教材目录、考试大纲、或者某份冗长的招标文件附件。但在我过去十二年跑过的278家客…

作者头像 李华
网站建设 2026/10/2 13:15:41

安卓systrace性能分析:跨层时序诊断与实战避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华