Skip to content

About

GLM-OCR 文档解析流水线的 C++ 移植 (MNN): PP-DocLayoutV3 版面检测 + GLM-OCR VLM 识别, 面向 Android 端侧部署

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

19 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GLM-OCR C++ 文档识别模块(PP-DocLayoutV3 + GLM-OCR-MNN)

用 MNN 推理两个模型,构建完整的文档 OCR Pipeline:

  1. PP-DocLayoutV3 — 版面检测:检测文本段落、标题、公式、插图、表格、印章等区域,并给出阅读顺序
  2. GLM-OCR-MNN — 文字识别:对版面检测裁剪出的区域进行 OCR 识别,输出文字内容

所有输入输出规格均经过实测验证(验证工具见 tools/ 目录 Python 脚本)。

目录结构

glmocr_cpp/
├── CMakeLists.txt            构建脚本
├── src/                      # C++ 源码
│   ├── layout.hpp / .cpp     版面检测: LayoutDetector (预处理+推理+后处理)
│   ├── loader.hpp / .cpp     OCR 大模型接口 (GlmOcr, MNN LLM 进程内封装)
│   ├── formatter.hpp / .cpp  结果格式化 (去重/标题/公式/markdown/json)
│   ├── crop_scaler.hpp / .cpp 裁剪缩放 (按 label 的宽高 scale, tiers 分档)
│   ├── label_router.hpp      25 类标签唯一路由表 (task/标准名)
│   ├── log.hpp               日志模块 (分级日志 + 分层耗时树)
│   ├── pipeline.cpp          完整流水线入口 (编排)
│   └── main.cpp              命令行 demo (layout_demo)
├── tools/                    # Python 验证/标定工具
│   ├── verify_postprocess.py 后处理一致性验证 (C++ vs Python)
│   ├── inspect_mnn.py        读取 .mnn 元信息 (输入输出名/shape)
│   └── probe_mnn_run.py      造合成图验证输出格式
├── config/
│   └── scale_config.example.json  缩放配置示例 (JSON)
├── doc/pipeline.md           处理逻辑/使用说明/关键模块/扩展路线
├── models/                   模型 (gitignored, 从 ModelScope 下载)
│   ├── PP-DocLayoutV3.mnn    版面模型 (PP-DocLayoutV3_onnx 经 MNNConvert)
│   └── GLM-OCR-MNN -> ...    GLM-OCR 4bit 模型 (符号链接)
├── img/test.png              测试图 (数学试卷, 实为 WebP 格式)
├── mnn_lib/                  MNN 库 (gitignored, 从 MNN 源码编译)
└── build/                    编译产物 (gitignored)

模型规格(实测确认)

输入(3 个,全部 float32)

名称 shape 含义
image [1, 3, 800, 800] 预处理后的图:RGB、拉伸resize 到 800×800(bicubic,不保宽高比)、÷255、HWC→CHW
im_shape [1, 2] 原图尺寸 (h, w)
scale_factor [1, 2] 缩放比 (h/800, w/800)

输出(3 个)

名称 shape 含义
fetch_name_0 [300, 7] float32 每行 (label_id, score, x1, y1, x2, y2, order);坐标在 800×800 输入坐标系;图内不做阈值和 NMS,300 行全量输出
fetch_name_1 [1] 常量 300(top-k),无实际用途
fetch_name_2 [300, 200, 200] int32 每个 query 的 1/4 分辨率 mask(提取 polygon 用),bbox 方案下忽略

类别表(label_id → 类别名,来自源模型 inference.yml)

id 类别 id 类别 id 类别
0 abstract 9 footer_image 18 reference
1 algorithm 10 footnote 19 reference_content
2 aside_text 11 formula_number 20 seal
3 chart 12 header 21 table
4 content 13 header_image 22 text
5 display_formula 14 image 23 vertical_text
6 doc_title 15 inline_formula 24 vision_footnote
7 figure_title 16 number
8 footer 17 paragraph_title

C++ 中用 layout_label_name(id) 查询(layout.hpp)。

GLM-OCR-MNN 模型规格(实测确认)

GLM-OCR 是一个视觉语言模型(VLM),用于文档图像的文字识别。它通过 MNN 的 LLM 引擎 (llm_demo) 运行,不是直接喂 tensor,而是通过 prompt 格式驱动。

模型文件

文件 大小 说明
config.json 342B 运行时配置(后端类型、线程数、采样器等)
llm_config.json 884B 模型架构参数(hidden_size、image_mean/norm、chat template)
llm.mnn 486KB LLM 主干网络图
llm.mnn.weight 365MB LLM 权重(4bit 量化)
visual.mnn 516KB 视觉编码器图
visual.mnn.weight 462MB 视觉编码器权重
embeddings_bf16.bin 182MB 词嵌入权重(bf16)
tokenizer.txt 592KB 分词器词表

关键参数(llm_config.json)

参数 值 说明
model_type glm_ocr 模型类型标识
hidden_size 1536 隐层维度
image_size 420 视觉编码器输入尺寸(正方形,420×420)
is_mrope true 使用 M-RoPE(多维旋转位置编码)
is_visual true 包含视觉编码器
image_mean [122.77, 116.75, 104.09] 图像归一化均值(R/G/B)
image_norm [0.01460, 0.01501, 0.01422] 图像归一化标准差倒数
vision_start 59256 图像起始 token id
vision_end 59257 图像结束 token id
image_pad 59280 图像填充 token id

Prompt 格式

<img>图片路径</img>类别标签 Recognition:
  • <img>...</img>: MNN Omni 引擎的图像标签,引擎自动解析路径并调用视觉编码器
  • 类别标签: 由上游 layout 检测提供(如 Text、Table、Formula、Image)
  • Recognition:: 固定后缀,触发模型进行文字识别

示例:

<img>/path/to/cropped_region.jpg</img>Text Recognition:

三种 task prompt 实测结论(2026-09-03,WSL CPU 4线程)

prompt 输出格式 实测样例
Text Recognition: 纯文本(含题目选项) 试卷第1题完整文字 ✓
Table Recognition: HTML <table>...</table> examples/source/table.png → 完整表格 HTML ✓
Formula Recognition: LaTeX,$$...$$ 包裹 $$x^{2}+10x=25$$ ✓
  • 多 <img> 标签:引擎支持多图编码(tokens 增长),但模型无法区分多图归属, 输出只覆盖其中一张 ⇒ 逐区域单图调用,与 Python SDK 行为一致。
  • ⚠️ 静默失败模式:MNN_BUILD_LLM_OMNI=OFF 编译出的 llm_demo 遇 <img> 不报错、 静默当纯文本(prompt tokens 仅 ~10、vision time 0.00)。重新编译后恢复正常。 每次换 MNN 版本/重编后,必须复跑上面的 prompt 验收。

编译要求

必须开启 OMNI 支持(视觉编码器):

cmake .. -DMNN_LOW_MEMORY=ON -DMNN_BUILD_LLM=ON -DMNN_BUILD_LLM_OMNI=ON -DMNN_SUPPORT_TRANSFORMER_FUSE=ON
make -j

如果 MNN_BUILD_LLM_OMNI=OFF,vision time 将为 0.00s,图像不会被处理。

性能参考(WSL CPU, 4线程)

指标 值 说明
视觉编码 ~11s / 0.55MP 对 1789×300 裁剪区域
Prefill 速度 ~369 tok/s prompt token 预填充
Decode 速度 ~55 tok/s 文本生成
Prompt tokens ~716 包含图像 token + 文本 token
首次加载 ~6.5s 模型加载+tuning

使用方式

# 通过 llm_demo 运行
cd /path/to/mnn/build
./llm_demo /path/to/GLM-OCR-MNN/config.json prompt.txt

# prompt.txt 内容:
# <img>/path/to/image.jpg</img>Text Recognition:

编译

依赖:MNN ≥ 2.0(C++ 库)、OpenCV(core/imgproc/imgcodecs)、CMake ≥ 3.16、C++17。

本目录已自带 MNN 库:mnn_lib/(lib/libMNN.so + include/,来自 WSL /home/dr/mnn_build/MNN 的编译产物,Linux x86_64)。直接用它编译:

cd glmocr_cpp
cmake -B build -DMNN_ROOT=$PWD/mnn_lib -DCMAKE_BUILD_TYPE=Release
cmake --build build -j 8
# 运行时自动从 mnn_lib/lib 找 libMNN.so (rpath 已设置), 无需 LD_LIBRARY_PATH

换平台(如 Windows/MSVC)时,把 mnn_lib/ 换成对应平台的 MNN 库即可:

# Linux 全局安装的 MNN
cmake -B build -DMNN_ROOT=/usr/local/mnn -DOpenCV_DIR=<opencv构建目录>
# Windows (MSVC)
cmake -B build -DMNN_ROOT=C:/libs/mnn
cmake --build build --config Release

MNN 源码编译(需要自己编时):

git clone https://github.com/alibaba/MNN.git && cd MNN
mkdir build && cd build
cmake .. -DMNN_BUILD_CONVERTER=OFF -DMNN_BUILD_DEMO=OFF
make -j8 && cmake --install . --prefix /usr/local/mnn

运行命令

# 基本用法: layout_demo <model.mnn> <图片路径> [可视化输出路径]
./layout_demo ../models/PP-DocLayoutV3.mnn ../img/test.png

# 指定可视化保存路径(不指定则自动存为 <图片路径>_vis.jpg)
./layout_demo ../models/PP-DocLayoutV3.mnn ../img/test.png vis.jpg

完整 OCR Pipeline(layout → 裁剪 → GLM-OCR → 按 order 输出)

./build/pipeline models/PP-DocLayoutV3.mnn /mnt/d/mnn/models/GLM-OCR-MNN/config.json <image> [output.txt]

流程与任务路由(对齐 glmocr SDK label_task_mapping):

任务 类别 行为
Text abstract/algorithm/content/doc_title/figure_title/paragraph_title/reference_content/text/vertical_text/vision_footnote/seal/formula_number "Text Recognition:" → 纯文本
Table table "Table Recognition:" → HTML
Formula display_formula/inline_formula "Formula Recognition:" → LaTeX
Skip chart/image 只保存裁剪图 imgs/page0_idx*.jpg,markdown 中以 ![Image](imgs/...) 引用
Abandon header/footer/header_image/footer_image/footnote/number/aside_text/reference 直接丢弃

实现位置:src/loader.cpp(ocr_task_type 路由 + GlmOcr 进程内封装)、src/pipeline.cpp(串线)、src/formatter.cpp(结果格式化)、src/label_router.hpp(唯一路由表)。

指定输出文件时生成三个产物:result.txt(调试用逐区域明细)、result.md(最终 markdown)、result.json(结构化结果)。

结果格式化(src/formatter.cpp,对齐 result_formatter.py + 增强)

  • 标题:doc_title → # ,paragraph_title → ## (先剥掉 OCR 输出里自带的 #/- /* )
  • 公式:重排为 $$\n...\n$$ 独立块
  • 文本:·/•/* → - 列表;(A)xxx→(A) xxx、1.xxx/3)xxx→1. xxx/3) xxx;单换行 → 双换行(markdown 段落)
  • 重复标点折叠:..... → ...(对齐 _clean_content)
  • 跨区域内容去重(C++ 增强,Python 版没有):重叠框把同一段文字识别两次时只保留一份。 规则 A:同为 text 且空间重叠(交/较小框面积)≥0.4 且归一化前缀 LCS 相似度 ≥0.65 → 丢低分者; 规则 B:归一化后短文本(≥24 码点)是长文本子串 → 丢短者。 实测 test.png 中"第4题"被两个部分重叠的 text 框(order=56/57)各识别一次,去重后只保留高分框。
  • JSON:[{index, label(text/table/formula/image), content, bbox_2d(原图像素), order, image_path?}]

后处理验证

# C++ 跑检测并 dump 原始 [300,7] 输出 (main 已默认 dump 到 /tmp/fetch_name_0.txt)
./build/layout_demo models/PP-DocLayoutV3.mnn img/test.png
# Python 端用同一份原始输出复现后处理链, diff 应完全一致
python3 tools/verify_postprocess.py --raw /tmp/fetch_name_0.txt 2381x3367

已验证 4 张图(test/paper/table/seal)C++ 与 Python 后处理输出逐框完全一致。 后处理链(对齐 layout_postprocess_utils.apply_layout_postprocess): score 过滤 → NMS(0.6/0.98) → 整页 image 过滤(0.82/0.93, 全滤光时不清空) → 嵌套框合并(全类 large, reference=small, preserve=image/seal/chart, is_contained 交面积/自身面积≥0.8 不带+1) → stable 按 order 排序 → unclip(默认 1.0,1.0 可调) → clamp + 无效剔除。unclip+clamp 后的框即 OCR 裁剪框。

注意:C++(OpenCV INTER_CUBIC) 与 Python(PIL BICUBIC) 预处理 resize 有细微插值差异, 会导致检测坐标/order 有 1-2px 级漂移——这是推理输入差异,非后处理逻辑差异; 严格对比请用 --raw 模式喂同一份原始输出。

# 基本用法: layout_demo <model.mnn> <图片路径> [可视化输出路径]
./layout_demo ../models/PP-DocLayoutV3.mnn ../img/test.png

# 指定可视化保存路径(不指定则自动存为 <图片路径>_vis.jpg)
./layout_demo ../models/PP-DocLayoutV3.mnn ../img/test.png vis.jpg

输出示例:

原图: 2381 x 3367
推理耗时: 743 ms (CPU)
检测到 56 个区域 (按阅读顺序):
  doc_title          score=0.925 [   454.2,   110.4,  1916.9,  181.8] order=1
  paragraph_title    score=0.871 [   130.2,   212.0,   872.7,  267.0] order=2
  text               score=0.952 [   132.9,   277.8,  1923.8,  577.6] order=3
  ...
可视化已保存: ../img/test.png_vis.jpg

框颜色含义(可视化):蓝=text、红=image、绿=table、紫=seal、橙=其余类别。

在自己代码中使用(API)

#include "layout.hpp"

cv::Mat bgr = cv::imread("doc.png");          // 支持 jpg/png/webp(视 OpenCV 编译选项)

LayoutDetector det;
det.loadModel("models/PP-DocLayoutV3.mnn", /*num_threads=*/4);
det.setScoreThreshold(0.3f);                  // 可选
det.setDebug(false);                          // true 时 dump 模型输出

std::vector<LayoutBox> boxes;
det.detect(bgr, &boxes);                      // 可循环调用, 模型只加载一次

for (const auto& b : boxes) {                 // 已按阅读顺序排序
    // b.label_id / layout_label_name(b.label_id)
    // b.score, b.x1, b.y1, b.x2, b.y2  (原图像素坐标, 已裁剪到图边界)
    // b.order                           (阅读顺序)
}

内部数据流(与 Python 版 glmocr 对齐):

BGR→RGB → 拉伸800×800(bicubic) → ÷255 → CHW
  + im_shape=(h,w), scale_factor=(h/800, w/800)
  → MNN 推理 → fetch_name_0 [300,7]
  → score≥阈值过滤 → 坐标×(w/800, h/800)回原图 → 裁剪到边界
  → NMS(同类0.6/异类0.98, 与 glmocr layout_postprocess_utils 一致)
  → 按 order 升序(阅读顺序)输出

参数设置

参数 默认值 位置 说明
置信度阈值 0.3 det.setScoreThreshold() 调高→框更少更准;调低→召回更多。Python 版 glmocr 用 0.3,PaddleX 可视化默认 0.5
NMS IoU(同类) 0.6 layout.cpp nms() 内 0.6f 同类框 IoU≥此值被抑制,与 Python 版一致
NMS IoU(异类) 0.98 同上 0.98f 异类几乎不互相抑制(允许同区域多标签竞争,靠 score 取胜)
输入尺寸 800 layout.cpp kInputSize 模型固定 800×800,勿改(模型是固定 shape 导出的)
线程数 4 det.loadModel(path, n) CPU 推理线程数,按机器核心数调
推理后端 CPU layout.cpp MNN_FORWARD_CPU GPU 改 MNN_FORWARD_OPENCL(移动GPU)/ MNN_FORWARD_CUDA(N卡),需对应编译 MNN 后端
Debug dump 关 det.setDebug(true) 打印 fetch_name_0 的 shape 和前 12 个值,排查模型/换模型时用

精度参考(img/test.png,2381×3367 数学试卷)

  • 56 个区域:1 doc_title / 2 paragraph_title / 10 text / 27 inline_formula / 11 image / 2 vision_footnote 等
  • 全部框准确贴合;阅读顺序 order 1→292 严格单调
  • 耗时 743 ms(WSL CPU);同一逻辑 Python 端验证脚本 test_image.py 可随时复跑

注意事项

  • 图内无阈值/NMS:模型输出 300 行全量结果(同一区域可能以不同 label 重复出现), 过滤和 NMS 全在 C++ 侧完成,改阈值不会触发重新编译模型。
  • 坐标换算:模型输出坐标在 800×800 坐标系,detect() 内已完成回原图换算, 拿到的 LayoutBox 直接是原图像素坐标。
  • PDF 不支持:本模块只收图片。PDF 需先自行渲染成图(如 pdfium/MuPDF)。
  • WebP:测试图实际是 WebP。OpenCV 需带 WebP 支持(pip/官方预编译版一般都有), 否则 cv::imread 返回空。
  • fetch_name_2(mask):需要 polygon 级精细轮廓时才用(mask → findContours → 多边形), 当前实现忽略它,区域裁剪用矩形框已足够。
  • 换模型时:先用 inspect_mnn.py 确认输入输出名和 shape, 再用 probe_mnn_run.py / test_image.py 验证输出格式是否仍是 [300,7] 布局。

About

GLM-OCR 文档解析流水线的 C++ 移植 (MNN): PP-DocLayoutV3 版面检测 + GLM-OCR VLM 识别, 面向 Android 端侧部署

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages