网页端OCR, 加载6mb大小模型, 又快又准, 百度这次真香

0 阅读7分钟

40岁了, 还在做开发, 再多卷一会, 哎

我想给内部系统加个 OCR 功能,用户上传截图自动识别文字。然后发现了百度的PaddleOCR

图片

毕竟专用OCR模型, 识别率比大模型优秀

图片

传统做法:后端起个 Python 服务,装 PaddleOCR,部署 Flask。麻烦不说,还得考虑并发和资源占用。

我就想:能不能让浏览器自己干这事儿?

图片

这是我的做demo, 完整代码最后

1. onnxruntime-web 是什么

图片

ONNX Runtime 是微软开源的推理引擎,onnxruntime-web 是它的浏览器版本。

底层用 WebGL 或 WebGPU,你的显卡在浏览器里给 AI 打工了。

接入一行:

如果项目用 npm:

npm install onnxruntime-web

用 bun 也一样:

bun add onnxruntime-web

安装后创建 src/index.ts 作为入口:

import * as ort from 'onnxruntime-web';
ort.env.wasm.wasmPaths = '/ort/';
(window as any).ort = ort;

用构建工具打包到静态目录:

# bun
bun build ./src/index.ts --outfile=./static/ort.js --target=browser

# 或用 webpack / vite 都行

HTML 里引用打包产物:

<script src="ort.js"></script>

加载模型、准备输入、推理、拿结果,四步完成,没有服务端,全在浏览器。

2. 下载模型

PP-OCRv6 的 ONNX 模型用魔塔 ModelScope 下载,国内速度快:

在魔搭上搜索PP-OCRv6_tiny

图片

然后下载对应的onxx文件

图片

解释下这2个文件干啥的:

PP-OCRv6_tiny_det_onnx:文本检测模型核心功能:在图像中精确定位并标记所有文本区域的位置,通常用边界框(Bounding Boxes) 圈出。

主要特点:该模型属于tiny(微型)系列,专为端侧和IoT场景设计,追求极致轻量和高速度。模型参数量仅43万(0.43M),大小约1.9 MB。

应用场景:擅长处理手写、印刷、旋转、弯曲和艺术字体等多种复杂场景的文字。

PP-OCRv6_tiny_rec_onnx:文本识别模型核心功能:接收检测模型给出的文本区域图像,将其中的视觉信息转化为计算机可以编辑和搜索的电子文本。

主要特点:同样作为tiny系列,它是PP-OCRv6中最轻量的识别模型。模型参数量为110万(1.1M),大小约4.4 MB,并支持49种语言的识别。

应用场景:能高效识别简体中文、繁体中文、英文、日文等,并能处理手写、竖排、拼音、生僻字等复杂文本。

static/
  models/
    PP-OCRv6_det_tiny.onnx
    PP-OCRv6_rec_tiny.onnx

3. OCR 流水线

OCR 不是单一模型,是文本检测加文本识别两个模型串联。

我选的是 PP-OCRv6,百度最新的开源 OCR 模型,有 tiny、small、medium 三个尺寸。

上传图片
  → 缩放至 960 以内(32 倍数)
  → 检测模型推理,得到概率图
  → 后处理:二值化 → 连通域 → 文本框
  → 对每个文本框裁剪
  → 缩放至 48xW
  → 识别模型推理
  → CTC 解码 → 得到文字

tiny 版本检测模型 1.74 MB,识别模型 4.28 MB,加起来 6 MB 多,浏览器加载很快。

4. 图像预处理

图片

检测模型需要特定尺寸的输入:长边不超过 960,且必须是 32 的倍数。

为什么 32?因为模型内部有步长为 32 的下采样层,输入尺寸不是 32 的倍数会导致输出截断。

预处理三件事:

参数来自官方模型配置,不是随便定的。

5. 文本检测:DBNet 后处理

图片

检测模型输出一张概率图,每个像素表示"这里是文字"的概率。

后处理分三步:

二值化:像素值大于 0.2 标为文字,否则标为背景。

连通域标记:用 BFS 把相邻的白点连成一片。每个连通域就是一个文字区域。

unclip 外扩:模型输出的边界通常比真实文字略小,按 面积 × 系数 / 周长 的比例向外扩张,系数取 1.4。

过滤掉太小的区域(短边小于 3 像素)和置信度太低的区域。

按 y 坐标排序,保证阅读顺序。

6. 文本识别:CRNN + CTC 解码

每个文本框裁剪出来,缩放到 48 像素高,宽度按比例保持。

识别模型输出 [1, T, C],T 是时间步数,C 是字符类别数(6906)。

用 CTC 贪心解码把概率变成文字:

每个时间步取概率最大的索引,去掉空白符(索引 0),合并连续重复的索引。映射到字符集就是文字。

举个例子:输出 [15, 15, 15, 0, 0, 23, 23, 0, 5] → 合并去重 → [15, 23, 5] → 字符集映射 → "你好"。

图片

7. 踩坑:识别出乱码

一开始跑测试,结果是这样:

#1 沼桷轻哔茸藩舅爪锵喇
#2 ,辜唤
#3 沼桷轻哔暖敬茸藩舅爪锵喇梓备字蹿心航瞠郊

全乱码。

问题在字符集。ONNX 模型输出 6906 维,但我之前下载的字典只有 6622 字符。

argmax 算出来的索引如果大于 6622,就映射不到字符,全变成 

解决方案:直接从 ONNX 模型元数据里提取字符集。

PP-OCRv6 的 ONNX 模型在内部嵌入了字符列表。我写了个 Python 脚本解析 protobuf 元数据,用 varint 解码读取字段长度,提取出 6904 个字符。

加上索引 0(blank)和最后一个(space),正好对应模型的 6906 维输出。

保存为 JSON 文件,前端加载时拼接成字符数组。

8. 踩坑:softmax 算出 NaN

加上置信度计算后,又出新问题:

#1  conf NaN%  undefined字

原因是模型输出含 NaN 值,Math.exp(NaN) 在整行 softmax 里传播扩散。

加了防护:argmax 时跳过 !isFinite(v) 的值;softmax 时跳过 diff < -50 和 !isFinite(diff) 的值。整行都不可用时跳过该时间步。置信度用 Math.max(0.001, Math.min(p, 0.999)) 兜底。

9. 踩坑:softmax 阈值过滤掉所有结果

加上置信度过滤后,结果又全没了:

⚠ 未检测到文本

原因是 softmax 在 6906 维时,单字符概率很小,最高也就 0.05 左右。我设的 0.3 阈值把全部结果都过滤了。

改成 0 不过滤,等看到实际分布再调。

10. 构建完整应用

把检测和识别串起来,再加个 UI 就是完整应用。

页面布局:左侧拖拽上传区加 Canvas 画布,右侧显示模型状态、进度条、识别结果。

识别结果按行显示,每行带序号、置信度百分比、识别文字。置信度高的绿色,中等的黄色,低的不显示。

画布上用彩色矩形框标注每个文本框的位置,上方显示文字和置信度。

图片

11. 踩坑:small 模型架构完全不同

我想换更大的 small 模型试试。结果发现:

| 对比项 | tiny | small | | :-- | :-- | :-- | | Det 输出 | [1,3,960,960] DBNet 三通道 | [1,1,640,640] 单通道二值图 | | Rec 词表 | 6906(6904 字符) | 18710(18708 字符) | | 检测后处理 | DBNet 阈值 + unclip | 轮廓检测 | | 输入尺寸 | [1,3,960,960] | [1,3,640,640] |

small 模型换了检测头架构,不再是 DBNet,是输出二值图后用轮廓法找文字区域。前端后处理代码需要重写。

tiny 已经够用,small 改天再研究。

11. 完整项目结构

demo/2/
├── package.json
├── server.ts              # Bun 静态服务器
└── static/
    ├── index.html         # OCR 应用
    ├── ppocr_keys_v6_tiny.json  # 6904 字符集
    ├── ppocr_keys_v6_tiny.txt   # 字符集(文本格式)
    └── models/
        ├── PP-OCRv6_det_tiny.onnx   # 检测模型 1.74MB
        └── PP-OCRv6_rec_tiny.onnx   # 识别模型 4.28MB

启动:

cd demo/2
bun install
bun run dev

打开 http://localhost:3001 就能用。

图片

13. 完整代码

公众号: 半刻纬度, 发送关键词"ocr"即可得到完整代码下载链接

图片