简介:OCR(光学字符识别)是图像文字数字化的重要技术,广泛用于发票归档、证件识别等场景。其工作原理通常包含文本检测、方向分类和字符识别三个环节,深度学习模型如PP-OCRv5通过轻量级设计在精度与速度间取得平衡。在实际工程中,OCR服务化部署不仅依赖模型推理,还涉及操作系统环境、依赖库配置、进程守护与参数调优。例如在Ubuntu20.04上结合PaddleOCR搭建服务,需处理CUDA/Python环境、systemd托管、阈值和batch调整等细节。本文从部署包结构出发,逐步说明环境准备、服务启动、关键参数调优与典型踩坑点,为生产环境中的OCR落地提供参考。
1. PP-OCRv5 在 ubuntu20.04 上跑 OCR 服务:先想清楚它替你省了什么
一张带红章、手写体备注和倾斜表头的发票照片丢进接口,三秒内返回结构化文字——这是 PP-OCRv5 在 ubuntu20.04 上做 OCR 识别服务最常见的落地场景。但真正做过一次部署的人都知道,最耗时间的不是下载模型,而是把环境、依赖、模型路径和服务启动脚本一件件抠明白。lw.PP-OCRService.tar.gz这类部署包的价值,本质上就是把黑匣子打开:模型放哪、参数怎么调、服务怎么起、挂了怎么拉起来。这篇笔记写给要在生产环境里把 OCR 服务真正跑起来的人,不聊算法论文,只聊解包、启动、调参和踩坑。
2. 为什么是 PP-OCRv5 + ubuntu20.04:选型和环境准备
2.1 PP-OCRv5 相比旧版的三个实用升级点
如果你用过 PP-OCRv3 或 v4,v5 上手的感觉应该是“门槛又低了一截”。我一般在选型时最看重三点。第一是检测端对非工整排版的容忍度明显更好,比如发票里歪斜的表头、印章压住的文字、手写数字混排的表格,v5 的检测框不再像老版本那样容易把半个字符切出去;第二是识别端的模型更轻,CPU 上跑 batch 1 的单张推理延迟比 v4 低,显存占用也友好,这点对只有一张 T4 甚至纯 CPU 的服务器很重要;第三是部署方式更省事,模型文件可以直接从 PaddleOCR 官方 release 里下载,不需要自己重新导出推理模型。
选 ubuntu20.04 不是因为它新,而是因为它在服务器存量里足够大。paddlepaddle 和 PaddleOCR 的依赖在 20.04 上踩坑最少,opencv、libgl、python3.8 这套组合的兼容性问题基本都被社区磨平了。如果你是 production 环境,没必要追 22.04 或 24.04,20.04 的 apt 源、CUDA 驱动版本和 Python 轮子档位是最稳的。
2.2 ubuntu20.04 基础环境:换源、驱动、Python 依赖
装系统这一步跳过,重点说装完系统之后的三个动作。第一是换 apt 源,默认源在国内慢到怀疑人生;第二是关闭休眠,服务器上的 OCR 服务跑着跑着睡过去是生产事故;第三是 GPU 驱动和 CUDA 的选型,PP-OCRv5 的推理不需要你装完整版 CUDA toolkit,装好 NVIDIA 驱动,然后用 pip 装对应版本的 paddlepaddle-gpu 即可,驱动版本对齐 NVDIA 官方列表就行。
换源和关闭休眠直接给命令:
# 备份原有源 sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak # 写入清华源(ubuntu20.04 即 focal) cat <<EOF | sudo tee /etc/apt/sources.list deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal main restricted universe multiverse deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-updates main restricted universe multiverse deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-security main restricted universe multiverse EOF sudo apt update && sudo apt upgrade -y # 关闭休眠 sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target换源后apt update如果报错,先看/etc/apt/sources.list里有没有写错发行版代号,focal 是 20.04 的代号,写成 jammy 会直接 404。GPU 驱动装完用nvidia-smi验证,能打印出驱动版本和显存就算过。接下来建 Python 环境,我一般不用系统 Python,避免把系统搞乱:
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh bash Miniconda3-latest-Linux-x86_64.sh conda create -n ocr python=3.8 -y conda activate ocr pip install paddlepaddle-gpu pip install paddleocrpaddlepaddle-gpu 会自动匹配你机器上的 CUDA 版本,装完建议跑一句验证:
python -c "import paddle; paddle.utils.run_check()"如果输出PaddlePaddle is installed successfully,环境就通了。这一步出错最常见的原因是 conda 把numpy版本升到了 2.x,和 paddle 的预编译二进制冲突,遇到就把 numpy 降回 1.24。
3. lw.PP-OCRService.tar.gz 部署包:解开看结构,照着跑起来
3.1 解包后的目录长什么样
拿到lw.PP-OCRService.tar.gz先别急着tar -xzf就完事。这个包名叫“Service”,说明它不只是模型权重,而是带服务容器的完整部署单元。我建议固定在一个专门目录里解,比如/opt/ocrservice,避免散落在 home 目录下以后找不着。先看一眼结构:
mkdir -p /opt/ocrservice && tar -xzf lw.PP-OCRService.tar.gz -C /opt/ocrservice cd /opt/ocrservice && find . -maxdepth 2 -type d | sort一般这类部署包会包含四个部分:一是模型的三个子目录,对应文本检测、方向分类、文本识别;二是 Python 服务代码或可执行入口,大概率是一个 Flask 或 FastAPI 应用;三是配置文件,里面是模型路径、监听端口、推理参数;四是启动/停止脚本和依赖清单。用ls -lh看一眼模型目录大小,如果识别模型只有几 MB 到几十 MB,说明是优化后的移动端模型,CPU 推理也能跑;如果几百 MB,那就是服务端大模型,最好确认有 GPU。
3.2 用 systemd 托管服务:从手动启动到开机自启
解包后先别急着跑,看requirements.txt或README里的依赖列表,把缺口补齐。启动方式常见有两种,一种是包内给了start.sh,一种是要自己调 Python 入口。不管哪种,我强烈建议用 systemd 托管的模式跑,别挂在终端里,一旦 SSH 断开服务就没了。先手动启动一次验证路径:
cd /opt/ocrservice # 如果有虚拟环境就用它,没有先创一个 python -m venv venv source venv/bin/activate pip install -r requirements.txt python server.py --port 8866看到日志出现Uvicorn running on http://0.0.0.0:8866或 Flask 的Running on字样,说明服务进程起来了。此时用 curl 打一发请求做冒烟测试:
curl -X POST http://127.0.0.1:8866/ocr \ -H "Content-Type: multipart/form-data" \ -F "file=@./test.jpg" | head -200返回里能看到"text": "发票号码..."之类的字段就说明通了。手动验证没问题后,杀掉进程,配置成 systemd 服务:
sudo tee /etc/systemd/system/ocrservice.service <<EOF [Unit] Description=PP-OCRv5 OCR Service After=network-online.target [Service] Type=simple WorkingDirectory=/opt/ocrservice ExecStart=/opt/ocrservice/venv/bin/python /opt/ocrservice/server.py --port 8866 Restart=always RestartSec=3 Environment=PYTHONUNBUFFERED=1 [Install] WantedBy=multi-user.target EOF sudo systemctl daemon-reload sudo systemctl enable --now ocrservice这里的Restart=always是关键参数,OCR 服务跑久了容易因为显存碎片或线程堆积挂掉,systemd 会在三秒后拉起来,这比写死循环脚本优雅得多。PYTHONUNBUFFERED=1保证日志实时写进 journal,排查问题时不用等缓冲区刷盘。
4. 三个必须调的参数:检测阈值、方向分类、batch 大小
4.1 det_db_thresh 检测阈值:漏检与误检的平衡
PP-OCRv5 的文本检测默认参数适合扫描文档,但真实业务图里往往有印章、背景纹理、水印。检测模块的det_db_thresh控制预测图二值化的阈值,默认 0.3。这个值调低到 0.2,能捞回对比度低的浅色文字,但代价是印章、背景干扰也会被当成文本区域;调高到 0.4 会减少误检,可浅色字和小字可能整块丢掉。我的经验是先从默认值出发,拿 50 张你最典型的业务图跑一遍,统计漏检率和误检率再决定方向。配置入口一般在部署包的 config 文件里,用 Python 调用时则长这样:
from paddleocr import PaddleOCR ocr = PaddleOCR( det_model_dir="/opt/ocrservice/models/det", rec_model_dir="/opt/ocrservice/models/rec", cls_model_dir="/opt/ocrservice/models/cls", det_db_thresh=0.3, # 检测二值化阈值,建议 0.2~0.4 之间试 det_db_box_thresh=0.5, # 检测框置信度阈值,过滤低质量框 use_angle_cls=True, # 是否启用方向分类器 rec_batch_num=6, # 识别批大小 use_gpu=False, # CPU 环境跑,GPU 环境改 True lang="ch", )det_db_box_thresh和det_db_thresh是两个维度,前者过滤“这个框里有没有字”,后者过滤“像素算不算文字”。如果图上有大量半透明的底纹,优先调det_db_box_thresh而不是动det_db_thresh,底纹会被框住但置信度低,用框阈值滤掉不伤正文。
4.2 use_angle_cls:方向分类器开不开是个成本问题
use_angle_cls=True意味着每张图先过一个 90/180/270 度的分类模型,再把图摆正送给识别模型。好处是拍照上传的图不会被旋转搞乱,坏处是推理链路上多了一道耗时。我在实际项目里见过最冤的翻车:业务图全是手机拍照,90% 的方向正确,但为了那 10% 开了方向分类器,CPU 上单张延迟直接翻倍,吞吐被打骨折。如果你的图片源已经明确是扫描仪输出或系统截图,方向不会乱,直接关掉;如果图源是 C 端用户上传,开,别犹豫。方向分类模型本身不重,但在 CPU 上跑的并发场景下,它消耗的算力占比能让 QPS 掉 30% 以上。
4.3 rec_batch_num:吞吐与显存的折中
rec_batch_num控制识别阶段一次处理的文本行数,默认值按 GPU 环境给到 6,CPU 环境建议降到 2~4。我在这上面吃过亏:第一次部署时图省事没改参数,GPU 服务跑了一周后开始偶发超时,排查半天发现是并发请求一多,每批 6 行文本行的显存叠加把 8G 卡塞满了,触发显存碎片后推理时间指数上涨。调成 4 之后稳定多了,吞吐反而上去。这个参数的本质是“算力换并发”,batch 越大单张利用率越高,但峰值内存也越高,服务是单线程推理还是多进程并发,直接决定你该往哪个方向调。用多 worker 跑并发时,每个 worker 都会加载一份模型,显存占用要按 worker 数乘算。
5. 避坑:部署 PP-OCRv5 服务常见的 5 个翻车点
5.1 libGL.so.1 找不到:opencv 的经典玄学
现象:启动服务时直接报ImportError: libGL.so.1: cannot open shared object file,或者更隐蔽的ImportError: libgthread-2.0.so.0一闪而过。
原因:PaddleOCR 依赖 opencv-python,而 opencv 的预编译轮子要加载系统的 libGL 库,conda 的干净环境里往往没有这玩意。这不是模型问题,是系统库缺失。玄学在于:有些人的 conda 环境自带,有些人的就没有,跟装 conda 时的 base 环境有关。
解决:不要自己编译 opencv,直接 apt 装两个库就行:
sudo apt update sudo apt install -y libgl1 libglib2.0-0装完重新source venv/bin/activate && python -c "import cv2",不报错就过了。
5.2 第一次启动卡在 “Downloading” 不动
现象:本地明明有模型文件,启动时日志却卡在类似Downloading det model from ...,等几分钟一动不动。
原因:部署包里的模型路径配置指向了空目录,或者paddleocr检测到模型目录里没有完整的inference.pdmodel和inference.pdiparams文件,自动走起了联网下载逻辑。服务器如果访问外网受限,就卡死在这里。
解决:确认模型目录里文件齐全,用ls检查一下是否有inference.pdmodel和inference.pdiparams。如果下载了一半,删掉重下或从别的机器拷贝。然后显式指定模型目录,别依赖自动下载。还有一种隐藏情况:路径写对了,但代码里PaddleOCR(det_model_dir="det")用的是相对路径,而 systemd 的WorkingDirectory指错了地方,启动时工作目录不在包目录下,相对路径就解析错了。
5.3 rec_batch_num 按默认值跑,CPU 上延迟爆炸
现象:CPU 机器上单张图片识别要 5 秒起步,并发两个请求直接排队到 10 秒以上。
原因:CPU 推理本身就是短板,rec_batch_num默认 6 让 CPU 一次计算 6 行文字的特征,计算量线性上升,但 CPU 不像 GPU 有并行优势,反而因为内存带宽瓶颈拖慢单行速度。
解决:把rec_batch_num调到 2,检测和识别的use_tensorrt关掉(CPU 上没意义),同时确认推理线程数。paddle 的paddle.set_num_threads(4)写在服务入口最前面,别让 paddle 默认吃满所有核心,否则和 web 框架的并发模型打架。
5.4 多 worker 并发启动:显存原地翻倍
现象:用uvicorn --workers 4启动服务,启动时报CUDA out of memory,或者启动成功后一压测就 OOM。
原因:PaddleOCR 不是在进程间共享模型的。每个 worker 都是独立 Python 进程,各自加载一套检测、方向分类、识别模型到显存。3 个模型都吃显存,4 个 worker 就是 4 份。
解决:要么把--workers改回 1,用进程内并发;要么保证显存足够再开多 worker。8G 显存跑 PP-OCRv5 的 server 模型,建议最多 2 个 worker。还有个土办法是预热:服务启动后主动送一张空白图跑一遍推理,让显存分配稳定后再接受流量,能减少启动瞬间的 OOM 概率。
5.5 服务跑一周后越来越慢,重启就好
现象:刚启动时单张 300ms,第三天变 800ms,第五天直接 2 秒,重启进程后又恢复正常。
原因:这是 OCR 服务最常见的慢性病。要么是显存碎片导致后续分配变慢,要么是某个调用方在持续传异常图片导致检测模块缓存膨胀,要么是日志文件疯狂增长把磁盘 IO 拖垮。用nvidia-smi看显存占用,再用systemctl status ocrservice看有没有反复重启过。
解决:先加日志观察哪个环节慢,最省事的是用 systemd 定时重启兜底:
sudo tee /etc/systemd/system/ocrservice-restart.timer <<EOF [Unit] Description=Restart OCR Service daily at 4am [Timer] OnCalendar=*-*-* 04:00:00 Persistent=true [Install] WantedBy=timers.target EOF sudo systemctl enable --now ocrservice-restart.timer这是治标。治本要找到拖慢根因,多数时候是有人在调用时传了超长文本行的图,识别模型处理时间变长,请求堆积,导致后续请求排队。限流和超时熔断得加上。
6. 把服务做得更牢靠:模型预热、超时熔断、日志审计
模型预热是上线前必做的一步。paddleocr 的模型加载是惰性的,第一次请求才真正把权重加载到显存并完成算子初始化,所以第一个请求往往特别慢,还会触发 CUDA context 创建。常见做法是服务启动入口加一段预热代码:
# server.py 里, 在监听端口前执行一次推理 import numpy as np from PIL import Image from paddleocr import PaddleOCR ocr = PaddleOCR( det_model_dir="/opt/ocrservice/models/det", rec_model_dir="/opt/ocrservice/models/rec", cls_model_dir="/opt/ocrservice/models/cls", use_angle_cls=True, rec_batch_num=4, ) def warmup(): blank = Image.new("RGB", (64, 64), (255, 255, 255)) blank.save("/tmp/warmup.jpg") ocr.ocr("/tmp/warmup.jpg") warmup()API 封装上建议把超时和限流放在反向代理层解决。我之前用裸 uvicorn 对外直接暴露,结果一个调用方传了张超大图进来,检测模块处理了 30 秒才返回,其余请求全部排队超时。后来在 Nginx 层加了client_max_body_size 10m和proxy_read_timeout 30s,接口层加了请求耗时统计,生产稳定多了。调用方传图前也压缩一下,长边超过 2000px 先等比缩放,OCR 识别率不会掉多少,但速度能翻好几倍。
最后说日志。部署包默认日志打到 journald 里,建议把识别结果命中率、耗时分布、失败原因单独输出到一个文件。服务出问题时别只盯着模型调参数,先看日志里失败请求的特征分布——是某类图片集体超时,还是某个调用方的图片格式一直不对,这些信息比调一个百分点阈值有用得多。排查时要能还原调用链,OCR 服务前后往往有文件存储和业务系统,没有完整日志审计链路,出了问题只能靠猜。我现在的习惯是每台 OCR 节点上常备一条命令用来做健康检查:
curl -s -w "time_total: %{time_total}s\n" \ -X POST http://127.0.0.1:8866/ocr \ -F "file=@/opt/ocrservice/test.jpg" \ -o /dev/null这一条能在服务被 systemd 拉起后快速确认推理链路是否真的可用,而不是只看进程还活着。OCR 服务在 ubuntu20.04 上落地这件事,最怕的就是把模型跑通当成服务跑通,真正生产环境里的稳定性功夫,都在模型之外。希望帮到你。
本文还有配套的精品资源,点击获取