简介:Umi-OCR for linux 是一套面向 Linux 平台的光学字符识别工具,基于深度学习算法实现多语言文字识别,适合需要在服务器或嵌入式环境中完成图像转文本任务的开发者与运维人员。资源包共 56 个文件,以 sample 示例、head 头文件、md 说明文档、xz 压缩包、json 配置、py 脚本及 Dockerfile 等为主,整体约 308.04MB,涵盖嵌入式版本、PaddleOCR 模型参数、运行时依赖组件与容器化部署文件,目录结构清晰,便于按模块取用。已有 308 人学习下载。借助其中的示例、配置与脚本,读者可快速搭建命令行 OCR 识别流程,将工具嵌入应用作为后台服务处理上传图片,并参考 Dockerfile 与依赖说明完成环境部署;同时可结合 JSON 结构化输出与多线程处理思路,将其应用于书籍数字化、票据表格信息提取、商品信息录入等场景,或基于开源代码进行模型调优与功能扩展。
1. Umi-OCR for Linux:把离线批量识别塞进无桌面环境的那点事
很多人第一次在 Linux 上跑 Umi-OCR,是冲着「离线、批量、不联网」这三个词去的。它本质是一个基于 PaddleOCR 的本地 OCR 工具,Windows 上有现成界面,但到了 Linux,尤其是无桌面、只有 SSH 的服务器上,事情就变得微妙了:没有 GUI,怎么调?依赖怎么装?模型放哪?我最初接手的需求很朴素——一批扫描件要转成可检索文本,数据不能出内网,机器是一台只有命令行的 Ubuntu。折腾一圈后我发现,Umi-OCR 在 Linux 上的价值不在「界面」,而在它把 PaddleOCR 的推理流程、模型管理和批量调度封装成了可脚本化的形态。这篇笔记就按「概念立住 → 环境搭起来 → 批量跑通 → 参数调优 → 避坑」的顺序,把我在无桌面 Linux 上复现这套流程的细节讲清楚,适合需要离线 OCR 的后端、运维和数据工程同学照着做。
2. 先搞清楚 Umi-OCR 在 Linux 上到底跑的是什么
2.1 它不是「一个软件」,而是三层结构
很多人把 Umi-OCR 当成一个绿色软件,下载解压就能用。这个认知在 Windows 上没问题,在 Linux 上会直接导致你找不到入口。拆开看,它其实是三层:
第一层是推理内核,也就是 PaddleOCR 的检测模型(DBNet)加识别模型(CRNN/SVTR)。这一层决定了识别精度和速度,也是真正吃 CPU/GPU 的部分。第二层是调度层,Umi-OCR 自己写的任务队列、图片预处理、结果聚合逻辑,负责把一堆图片喂给内核再把文本收回来。第三层才是界面层,Windows 上是 Qt 写的 GUI,Linux 上如果你只要命令行,这一层可以直接不要。
理解这三层的意义在于:Linux 上你要复现的是前两层,界面层缺失不影响核心功能。我见过有人卡在「没有 GUI 怎么启动」上,其实只要找到调度层的入口脚本或命令行参数,就能绕开界面直接跑批。
2.2 为什么 Linux 部署比 Windows 麻烦:依赖链的差异
Windows 版 Umi-OCR 把 PaddlePaddle、OpenCV、模型文件全打包进了一个目录,双击即用。Linux 上没有这种「绿色包」的惯例,你得自己把依赖链补齐。核心依赖大致是这几类:
| 依赖类别 | 具体组件 | 作用 | 常见坑 |
|---|---|---|---|
| 深度学习框架 | PaddlePaddle(CPU 或 GPU 版) | 模型推理 | CPU 版和 GPU 版不能混装,CUDA 版本要对齐 |
| 图像处理 | OpenCV、Pillow | 图片解码、预处理 | 无桌面环境缺 libGL,import cv2 直接报错 |
| 数值计算 | NumPy | 张量运算 | 版本过高可能和 Paddle 冲突 |
| 字体/编码 | 中文字体、locale | 结果输出 | locale 没设 UTF-8,中文变问号 |
| 模型文件 | 检测模型 + 识别模型 | 实际权重 | 路径写错不会报错,只会返回空结果 |
这张表里最容易被忽略的是 OpenCV 的 libGL 问题。服务器上没装图形库,import cv2会抛libGL.so.1: cannot open shared object file。解决办法不是装整个桌面,而是补libgl1和libglib2.0-0这两个运行库。这个坑我在三台不同机器上踩过,每次都是同一个报错。
2.3 选 CPU 版还是 GPU 版:先看你的批量规模
选型逻辑很简单:如果你一次要处理几千张图,GPU 版值得折腾 CUDA;如果只是几百张、偶尔跑一次,CPU 版省心得多。我一般这样判断:
- 图片数量 < 500 张,且对时间不敏感:CPU 版,装完就能跑,不用碰显卡驱动。
- 图片数量 > 2000 张,或需要反复跑:GPU 版,单张推理能从几百毫秒降到几十毫秒。
- 机器是云服务器且没有独显:别想了,CPU 版,或者考虑多进程并行。
这里有个反直觉的点:CPU 版在多核机器上开多进程,吞吐量不一定比单进程 GPU 差。因为 OCR 推理本身不是特别吃并行度,瓶颈常在图片解码和 IO。我实测过一台 16 核 CPU 机器开 8 进程,处理 3000 张 A4 扫描件,总耗时和一张中端显卡差不多。所以别盲目追 GPU,先算清楚你的瓶颈在哪。
3. 在无桌面 Linux 上把环境搭起来
3.1 用 conda 隔离环境,别污染系统 Python
Linux 服务器上最忌讳直接pip install到系统 Python,因为 PaddlePaddle 对 NumPy 和 protobuf 的版本有要求,很容易和系统里已有的包打架。我习惯用 conda 建独立环境:
# 创建独立环境,指定 Python 3.9(Paddle 对 3.10+ 支持较晚) conda create -n umiocr python=3.9 -y conda activate umiocr # 安装 CPU 版 PaddlePaddle,指定官方源 python -m pip install paddlepaddle==2.5.2 -i https://pypi.tuna.tsinghua.edu.cn/simple # 安装图像和推理辅助库 pip install opencv-python-headless pillow numpy这里有几个参数要解释。opencv-python-headless是关键,它是 OpenCV 的无 GUI 版本,不依赖 libGL,专门给服务器用。如果你装了普通opencv-python,在无桌面环境会报图形库缺失。PaddlePaddle 版本我锁在 2.5.2,是因为再高的版本对 Python 3.9 的兼容性开始变差,而 2.5.x 是经过大量项目验证的稳定档。-i指定国内源是为了下载速度,Paddle 的包有几百 MB,走默认源容易超时。
装完后验证:
python -c "import paddle; print(paddle.__version__); paddle.utils.run_check()"run_check()会输出环境是否正常。如果它提示 CUDA 相关警告但你用的是 CPU 版,可以忽略,只要最后打印出PaddlePaddle is installed successfully就行。
3.2 模型文件放哪:路径约定与手动下载
Umi-OCR 的模型不是 pip 包自带的,需要单独放。常见做法是在项目目录下建一个models文件夹,里面分det(检测)和rec(识别)两个子目录。检测模型一般用轻量版 DBNet,识别模型用中文通用版。文件结构大致是:
umiocr/ ├── models/ │ ├── det/ │ │ └── ch_PP-OCRv4_det_infer/ │ └── rec/ │ └── ch_PP-OCRv4_rec_infer/ ├── images/ └── run_ocr.py模型文件通常包含.pdmodel(网络结构)和.pdiparams(权重)两个文件,缺一不可。我踩过的坑是:只放了.pdmodel没放.pdiparams,程序不报错,但识别结果全是空字符串。因为 Paddle 加载模型时如果权重文件缺失,有时会静默失败。所以放完模型后,一定先用单张图测一次,确认有输出再跑批量。
3.3 写一个最小可跑的识别脚本
环境好了,模型放了,接下来用一个脚本验证链路通不通:
import os from paddleocr import PaddleOCR # 初始化 OCR,指定模型路径和语言 ocr = PaddleOCR( det_model_dir='models/det/ch_PP-OCRv4_det_infer', rec_model_dir='models/rec/ch_PP-OCRv4_rec_infer', use_angle_cls=False, # 无桌面场景一般不需要方向分类,省时间 lang='ch', show_log=False # 关掉冗余日志,批量时输出更干净 ) # 单张图识别 img_path = 'images/sample.png' result = ocr.ocr(img_path, cls=False) # result 是嵌套列表,逐行提取文本 for line in result[0]: text = line[1][0] confidence = line[1][1] print(f'{confidence:.3f}\t{text}')这段代码的逻辑是:初始化时把检测和识别模型路径写死,避免它去联网下载。use_angle_cls=False是因为扫描件通常方向正确,方向分类会额外跑一个模型,批量时拖慢速度。show_log=False让输出只剩结果,方便重定向到文件。result[0]对应第一张图,里面每个元素是[坐标框, (文本, 置信度)]。置信度低于 0.5 的行我一般会过滤掉,后面批量脚本里会加这个逻辑。
跑通这张图,说明环境、模型、推理链路都没问题,可以进入批量阶段。
4. 批量识别:从单张到几千张的工程化改造
4.1 批量脚本的骨架:遍历、过滤、写结果
单张能跑之后,批量就是加一层循环和结果落盘。但直接 for 循环会有一个问题:OCR 初始化很慢,每次循环都重新建对象会浪费大量时间。正确做法是初始化一次,循环里只调ocr.ocr():
import os import csv from paddleocr import PaddleOCR ocr = PaddleOCR( det_model_dir='models/det/ch_PP-OCRv4_det_infer', rec_model_dir='models/rec/ch_PP-OCRv4_rec_infer', use_angle_cls=False, lang='ch', show_log=False ) img_dir = 'images' out_csv = 'result.csv' min_confidence = 0.5 # 低于此置信度的行丢弃 with open(out_csv, 'w', newline='', encoding='utf-8') as f: writer = csv.writer(f) writer.writerow(['filename', 'text', 'confidence']) for name in sorted(os.listdir(img_dir)): if not name.lower().endswith(('.png', '.jpg', '.jpeg')): continue path = os.path.join(img_dir, name) try: result = ocr.ocr(path, cls=False) except Exception as e: print(f'[FAIL] {name}: {e}') continue if not result or not result[0]: print(f'[EMPTY] {name}') continue for line in result[0]: text, conf = line[1][0], line[1][1] if conf >= min_confidence: writer.writerow([name, text, f'{conf:.3f}']) print(f'[OK] {name}')这段脚本的关键点有三个。第一,ocr对象在循环外初始化,只建一次。第二,用try/except包住单张识别,某张图损坏不会中断整个批次。第三,结果写 CSV 而不是直接 print,方便后续用 Excel 或数据库处理。min_confidence设 0.5 是经验值,低于这个值的行多半是噪点或误检,留着反而干扰检索。
4.2 多进程加速:怎么切分任务才不打架
单进程跑几千张图,CPU 利用率往往只有一两个核。要提速,最直接的办法是多进程。但 PaddleOCR 对象不能跨进程共享,每个进程得自己初始化一份。我一般用multiprocessing.Pool,按图片数量切分:
import os from multiprocessing import Pool from paddleocr import PaddleOCR def process_batch(args): img_list, worker_id = args # 每个进程独立初始化,避免共享冲突 ocr = PaddleOCR( det_model_dir='models/det/ch_PP-OCRv4_det_infer', rec_model_dir='models/rec/ch_PP-OCRv4_rec_infer', use_angle_cls=False, lang='ch', show_log=False ) results = [] for path in img_list: try: res = ocr.ocr(path, cls=False) if res and res[0]: for line in res[0]: results.append((os.path.basename(path), line[1][0], line[1][1])) except Exception as e: print(f'[FAIL] {path}: {e}') return results if __name__ == '__main__': img_dir = 'images' all_imgs = [os.path.join(img_dir, n) for n in sorted(os.listdir(img_dir)) if n.lower().endswith(('.png', '.jpg', '.jpeg'))] n_workers = 4 # 按 CPU 核数调整,一般设为核心数的一半到全部 chunk_size = max(1, len(all_imgs) // n_workers) chunks = [(all_imgs[i:i+chunk_size], i) for i in range(0, len(all_imgs), chunk_size)] with Pool(n_workers) as pool: all_results = pool.map(process_batch, chunks) # 合并结果写文件 with open('result_mp.csv', 'w', newline='', encoding='utf-8') as f: import csv writer = csv.writer(f) writer.writerow(['filename', 'text', 'confidence']) for batch in all_results: for row in batch: writer.writerow(row)参数上,n_workers不是越大越好。每个进程都要加载一份模型,内存占用会翻倍。我一般设成 CPU 核心数的一半,比如 8 核设 4 进程。如果内存紧张,就设 2。chunk_size用总数除以进程数,保证每个进程任务量均衡。注意if __name__ == '__main__':这行不能省,否则多进程在 Linux 上会递归创建子进程,直接把机器拖死。
4.3 结果后处理:把散乱文本拼成可检索内容
OCR 出来的是一行行文本,但实际使用往往需要按图片聚合,甚至按段落合并。常见做法是:同一张图的行按纵坐标排序,再拼接。PaddleOCR 返回的坐标框里,左上角 y 值就是排序依据:
def merge_lines(result): """把一张图的识别结果按从上到下排序后拼接""" lines = [] for item in result[0]: box = item[0] # 四点坐标 text = item[1][0] y_top = min(p[1] for p in box) # 取上边界 lines.append((y_top, text)) lines.sort(key=lambda x: x[0]) return '\n'.join(t for _, t in lines)这个函数解决的是「识别顺序乱」的问题。PaddleOCR 默认按检测框的某种顺序返回,不保证从上到下。如果不排序,拼出来的段落会前后颠倒。按 y 坐标排序后,基本能还原阅读顺序。对于双栏排版,这个简单排序会失效,需要先按 x 坐标分栏,那是更复杂的场景,一般扫描件用不上。
5. 避坑与排查:那些让我返工三次的问题
5.1 识别结果全空,但程序不报错
现象:脚本正常跑完,CSV 里只有表头,没有任何文本行。
原因:模型路径不对,或者模型文件不完整。PaddleOCR 在模型加载失败时,某些版本不会抛异常,而是返回空结果。
解决:先确认det_model_dir和rec_model_dir下的文件是否齐全,至少要有.pdmodel和.pdiparams。然后用单张图跑一次,把show_log=True打开,看日志里有没有load model成功的提示。如果日志显示加载了但结果为空,换一张清晰的图再试,排除图片本身问题。
5.2 中文输出变成问号或乱码
现象:CSV 用 Excel 打开,中文全是???。
原因:两个层面。一是 Python 写文件时没指定encoding='utf-8';二是系统 locale 不是 UTF-8,导致终端输出乱码。
解决:写文件时显式加encoding='utf-8'。系统层面执行locale查看,如果不是en_US.UTF-8或zh_CN.UTF-8,在~/.bashrc里加export LANG=en_US.UTF-8和export LC_ALL=en_US.UTF-8,重新登录。Excel 打开 CSV 乱码是 Excel 的问题,用「数据 → 从文本导入」并选 UTF-8 编码即可。
5.3 多进程跑一半卡死或内存爆掉
现象:开了 8 个进程,跑到中途机器失去响应,或者进程被 OOM Killer 杀掉。
原因:每个进程加载一份 Paddle 模型,内存占用是单进程的数倍。如果图片尺寸大,解码时还会额外吃内存。
解决:把n_workers降到 2 或 4,观察内存曲线。另外在PaddleOCR初始化时加enable_mkldnn=False,MKLDNN 加速在某些 CPU 上会引入内存泄漏。如果还是不够,分批处理,每批 500 张,跑完释放进程再起下一批。
5.4 置信度阈值设太高,漏掉正常文本
现象:明明图片很清晰,但结果里少了很多行。
原因:min_confidence设得过高,比如 0.9,把一些字体特殊或背景复杂的正常文本也过滤了。
解决:先把阈值降到 0.3,跑一批看结果,再根据实际误检情况往上调。我一般用 0.5 作为起点,如果发现漏检多就降到 0.4,误检多就升到 0.6。这个值没有万能解,跟你的图片质量强相关。
5.5 图片格式不支持导致静默跳过
现象:目录里明明有图,但结果里没有对应的文件名。
原因:脚本只过滤了.png/.jpg/.jpeg,如果图片是.bmp、.tif或.webp,会被直接跳过,而且不报错。
解决:在过滤条件里补上你实际用到的格式,或者干脆用 Pillow 尝试打开来判断是否为图片。更稳妥的做法是维护一个支持格式列表,遇到不认识的格式打印警告,而不是静默忽略。
6. 进阶:把识别精度和吞吐再压一压的几个手段
6.1 图片预处理:二值化和去噪的取舍
扫描件常见问题是背景发灰、有噪点。直接送进 OCR,检测框会变多,识别置信度下降。我一般加一步预处理:
import cv2 import numpy as np def preprocess(img_path): img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) # 自适应二值化,对光照不均的扫描件效果好 binary = cv2.adaptiveThreshold( img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, blockSize=31, # 邻域大小,奇数,越大越平滑 C=10 # 阈值偏移,越大越偏向背景 ) # 轻度去噪,去掉孤立噪点 denoised = cv2.medianBlur(binary, 3) return denoisedblockSize和C是两个关键参数。blockSize太小,文字笔画会被当成背景;太大,细小文字会糊。31 是我在 A4 扫描件上试出来的平衡点。C控制二值化的松紧,10 偏保守,能保留更多笔画。预处理后把图片存成临时文件再喂给 OCR,比直接传原图精度高一截。但注意:如果原图本身质量很好,预处理反而可能引入伪影,所以这一步要按批次抽样验证。
6.2 按图片尺寸动态调整检测参数
PaddleOCR 的检测模型对输入尺寸敏感。默认会把图片缩放到一个固定尺寸,如果原图特别大或特别小,检测效果会打折。可以在初始化时指定det_limit_side_len:
ocr = PaddleOCR( det_model_dir='models/det/ch_PP-OCRv4_det_infer', rec_model_dir='models/rec/ch_PP-OCRv4_rec_infer', det_limit_side_len=1280, # 长边限制,默认 960 use_angle_cls=False, lang='ch', show_log=False )det_limit_side_len设大,小文字检测更准,但速度变慢。我处理合同扫描件时设 1280,处理普通文档设 960。这个值要根据你的图片里最小文字的高度来调,没有固定公式,试两三批就能找到合适的。
6.3 用置信度分布反推参数是否合理
跑完一批后,别只看结果,把置信度分布画出来(或者简单统计一下):
import csv from collections import Counter with open('result.csv', encoding='utf-8') as f: reader = csv.DictReader(f) confs = [float(row['confidence']) for row in reader] # 按 0.1 分桶统计 buckets = Counter(int(c * 10) / 10 for c in confs) for k in sorted(buckets): print(f'{k:.1f}: {buckets[k]}')如果大量结果集中在 0.5 到 0.6 之间,说明你的阈值卡在边缘,可能漏掉不少。如果集中在 0.9 以上,说明图片质量好,可以适当提高阈值减少误检。这个分布比单看几张图更能反映整体情况,是我每次调参后的固定动作。
6.4 一个我常犯的错误:忘了清理临时文件
预处理会生成临时图片,多进程跑完可能留下几千个临时文件,把磁盘塞满。我现在的习惯是在脚本里用tempfile模块,或者跑完统一清理。这个坑不涉及技术难点,但一旦发生就是磁盘告警,半夜被叫起来处理。所以批量脚本里加一行清理逻辑,比事后补救省心得多。
最后说个习惯:每次换新类型的图片,先抽 20 张跑一遍,人工核对结果,确认精度和参数合适了再全量跑。OCR 没有万能参数,不同来源的扫描件差异很大,抽样验证是唯一靠谱的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取