PP-LCNet DocOri —— 文档整图方向分类器 ONNX 版
模型简介
本模型源自 PaddleX / PaddleOCR 生态的 PP-LCNet_x1_0_doc_ori 文档方向分类器,已转换为 ONNX 格式,专为离线本地推理场景优化。
在 OCR 流水线中,扫描件、手机拍照、传真件等文档图像可能存在 90°、180° 甚至 270° 的旋转。如果直接将歪斜图像送入 OCR 引擎,文本识别准确率会大幅下降。本模型作为 OCR 前置步骤,自动判断文档图像的朝向并矫正为正向。
核心价值:在纯 CPU 环境下,以毫秒级延迟完成文档方向四分类(0°/90°/180°/270°),提升下游 OCR 识别质量,无需依赖 GPU 或 PaddlePaddle 运行时。
模型文件
| 文件 | 大小 | 说明 |
|---|---|---|
model.onnx |
~6.5 MB | ONNX 模型(包含图结构与权重) |
单一文件,无外部数据依赖,直接加载即可推理。
模型架构
- 基础模型:PP-LCNet_x1_0_doc_ori(百度 PaddleClas)
- 网络结构:PP-LCNet(轻量级 CPU 友好 CNN)
- 输入规格:
- 尺寸:
224 × 224RGB 图像 - 预处理:短边等比缩放至 256 → 中心裁剪 224 → ImageNet 归一化
- 归一化参数:
mean=[0.485, 0.456, 0.406],std=[0.229, 0.224, 0.225]
- 尺寸:
- 输出:
float32[1, 4]四类 softmax 分数 - 类别映射:
| class index | 角度 | 含义 |
|---|---|---|
| 0 | 0° | 正向(无需旋转) |
| 1 | 90° | 顺时针旋转 90° |
| 2 | 180° | 倒置 |
| 3 | 270° | 逆时针旋转 90°(或顺时针 270°) |
工作流程:
输入图像(任意朝向、任意分辨率)
│
▼
┌─────────────────────────────────┐
│ 预处理 │
│ resize(短边=256) │
│ → centerCrop(224×224) │
│ → normalize(ImageNet mean/std) │
│ → NCHW float32[1,3,224,224] │
└─────────────┬───────────────────┘
▼
┌─────────────────────────────────┐
│ ONNX 推理 (PP-LCNet) │
│ 输出: float32[1, 4] │
└─────────────┬───────────────────┘
▼
┌─────────────────────────────────┐
│ argmax → 角度 │
│ → 反向旋转矫正 │
│ → 输出正向图像 │
└─────────────────────────────────┘
快速开始
环境要求
onnxruntime>= 1.24.3- 图像预处理:
numpy+PIL/opencv-python(Python),或@napi-rs/canvas+sharp(Node.js)
Python 示例
import onnxruntime as ort
import numpy as np
from PIL import Image
# 1. 加载模型
model_path = "./pp-lcnet-doc-ori/model.onnx"
session = ort.InferenceSession(model_path)
# 2. 图像预处理
def preprocess(image_path: str) -> np.ndarray:
"""短边缩放 256 → 中心裁剪 224 → ImageNet 归一化 → NCHW"""
img = Image.open(image_path).convert("RGB")
w, h = img.size
# 短边等比缩放
scale = 256 / min(w, h)
new_w, new_h = int(round(w * scale)), int(round(h * scale))
img = img.resize((new_w, new_h), Image.BILINEAR)
# 中心裁剪 224
left = (new_w - 224) // 2
top = (new_h - 224) // 2
img = img.crop((left, top, left + 224, top + 224))
# 归一化 (ImageNet)
mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)
std = np.array([0.229, 0.224, 0.225], dtype=np.float32)
arr = np.array(img, dtype=np.float32) / 255.0
arr = (arr - mean) / std
# HWC → NCHW
return np.expand_dims(arr.transpose(2, 0, 1), axis=0)
# 3. 推理
input_tensor = preprocess("document.jpg")
input_name = session.get_inputs()[0].name
outputs = session.run(None, {input_name: input_tensor})
scores = outputs[0][0] # [4]
# 4. argmax → 角度
ANGLE_MAP = {0: 0, 1: 90, 2: 180, 3: 270}
pred_class = int(np.argmax(scores))
angle = ANGLE_MAP[pred_class]
print(f"预测角度: {angle}°")
print(f"各类置信度: 0°={scores[0]:.3f}, 90°={scores[1]:.3f}, 180°={scores[2]:.3f}, 270°={scores[3]:.3f}")
TypeScript / Node.js 示例
import * as ort from "onnxruntime-node";
import { createCanvas, loadImage } from "@napi-rs/canvas";
// 归一化参数
const MEAN = [0.485, 0.456, 0.406];
const STD = [0.229, 0.224, 0.225];
const INPUT_SIZE = 224;
const RESIZE_SHORT = 256;
async function preprocessImage(buffer: ArrayBuffer): Promise<Float32Array> {
const img = await loadImage(Buffer.from(buffer));
const w = img.width, h = img.height;
// 短边缩放
const scale = RESIZE_SHORT / Math.min(w, h);
const newW = Math.round(w * scale), newH = Math.round(h * scale);
const resizeCanvas = createCanvas(newW, newH);
resizeCanvas.getContext("2d").drawImage(img, 0, 0, newW, newH);
// 中心裁剪
const sx = Math.max(0, Math.floor((newW - INPUT_SIZE) / 2));
const sy = Math.max(0, Math.floor((newH - INPUT_SIZE) / 2));
const cropCanvas = createCanvas(INPUT_SIZE, INPUT_SIZE);
cropCanvas.getContext("2d").drawImage(resizeCanvas, sx, sy, INPUT_SIZE, INPUT_SIZE, 0, 0, INPUT_SIZE, INPUT_SIZE);
const pixels = cropCanvas.getContext("2d").getImageData(0, 0, INPUT_SIZE, INPUT_SIZE).data;
// NCHW + 归一化
const plane = INPUT_SIZE * INPUT_SIZE;
const out = new Float32Array(3 * plane);
for (let i = 0; i < plane; i++) {
out[i] = (pixels[i * 4] / 255 - MEAN[0]) / STD[0];
out[plane + i] = (pixels[i * 4 + 1] / 255 - MEAN[1]) / STD[1];
out[plane * 2 + i] = (pixels[i * 4 + 2] / 255 - MEAN[2]) / STD[2];
}
return out;
}
// 使用
const session = await ort.InferenceSession.create("./pp-lcnet-doc-ori/model.onnx");
const tensorData = await preprocessImage(imageBuffer);
const tensor = new ort.Tensor("float32", tensorData, [1, 3, 224, 224]);
const inputName = session.inputNames[0];
const outputName = session.outputNames[0];
const outputs = await session.run({ [inputName]: tensor });
const scores = outputs[outputName].data as Float32Array;
const ANGLE_MAP = { 0: 0, 1: 90, 2: 180, 3: 270 };
const predClass = scores.indexOf(Math.max(...scores));
console.log(`预测角度: ${ANGLE_MAP[predClass]}°`);
应用场景
OCR 前置方向矫正
- 扫描件处理:律所扫描的合同、判决书、证据材料可能存在各种朝向
- 手机拍照:律师用手机拍摄的案卷材料常常歪斜
- 传真件数字化:传真机输出的文档角度不可控
- 批量文档自动化:大批量文档入库前自动统一朝向,无需人工逐页检查
降级策略
当模型文件未下载或推理失败时,系统自动跳过方向矫正,按 0°(正向)处理,不阻断下游 OCR 流程:
模型可用 → classify() → rotateToUpright() → 矫正图像 → OCR
模型不可用 → 跳过矫正 → 原图直接送 OCR
技术细节
网络结构
PP-LCNet(PP-Lightweight CPU Network)是百度提出的轻量级图像分类网络,设计目标为 CPU 友好:
- 使用深度可分离卷积 + SE 注意力模块
- 比 MobileNetV3 更快的推理速度
- 专为 ARM / x86 CPU 优化
- 在本项目中,单次推理耗时 < 50ms(桌面 CPU)
训练数据
原模型由 PaddleX 团队使用文档图像方向分类数据集训练,覆盖中英文文档、表格、票据、证件等多种文档类型。
与官方 Paddle 模型的关系
| 方面 | 官方 Paddle 模型 | 本模型 (ONNX) |
|---|---|---|
| 运行时 | PaddlePaddle | ONNX Runtime |
| 格式 | inference.pdmodel | model.onnx |
| 依赖 | paddlepaddle (~500MB) | onnxruntime (~30MB) |
| GPU | 支持 | CPU only |
| 推理速度 (CPU) | ~80ms | ~50ms |
| 适合桌面应用 | ❌ 运行时过大 | ✅ 轻量集成 |
ONNX 转换说明
本模型从 PaddlePaddle 原生格式通过 paddle2onnx 工具转换而来:
- paddle2onnx:2.1.0
- paddlepaddle:2.6.2 / 3.0.0 nightly
- 转换命令:
paddle2onnx --model_dir ./ --model_filename inference.pdmodel --save_file model.onnx
已知问题:PaddlePaddle 3.0 部分模型包使用
inference.json替代inference.pdmodel,Windows 环境下paddle2onnx 2.1.0要求 paddlepaddle ≥ 3.0.0.dev,且可能因 MSVC 运行时缺失导致转换中断。
许可证
本模型遵循 Apache License 2.0,与 PaddleOCR / PaddleX 官方保持一致。
引用
如果你在研究中使用了本模型,请引用 PaddleOCR:
@misc{paddleocr,
title={PaddleOCR: Awesome Multilingual OCR Toolkits Based on PaddlePaddle},
author={PaddlePaddle Authors},
howpublished={\url{https://github.com/PaddlePaddle/PaddleOCR}},
year={2024}
}
@article{pp-lcnet,
title={PP-LCNet: A Lightweight CPU Convolutional Neural Network},
author={Cui, Cheng and Gao, Tingquan and Wei, Shengyu and Du, Yuning and Guo, Ruoyu and Dong, Shuilong and Lu, Bin and Zhou, Ying and Lv, Xueying and Liu, Qiwen and others},
journal={arXiv preprint arXiv:2109.15099},
year={2021}
}
致谢
- 原始模型:PaddleOCR / PaddleX(百度)
- ONNX 转换:基于 twmht/PP-LCNet_x1_0_doc_ori_infer_onnx 二次整理
- 转换工具:paddle2onnx
- 运行时:ONNX Runtime