OCR 并不是调用一次神经网络。真正可用的浏览器流水线要先找到文字区域、纠正倒置裁剪、识别不同宽度的文本行、解码字符概率、恢复阅读顺序,并让这些重计算远离 UI 主线程。
这套实现把检测、方向分类和识别三个 ONNX session 放进 module Web Worker。图片通过可转移字节进入 worker,OffscreenCanvas 完成预处理,模型 session 只加载一次,进度与结果则通过 request ID 对应。
把 OCR 拆成三个模型阶段
检测模型输出文本像素概率图。后处理对概率图做阈值化和膨胀,寻找连通区域,过滤弱区域,扩展边界框,合并同一行碎片,并按阅读顺序排序。
每个检测裁剪先经过 0/180 度分类,再送入识别模型。识别结果按 CTC 方式去掉 blank 和连续重复类。最终合并会保留换行,并根据英文或 CJK 文本决定是否插入空格。
| 阶段 | 输入 | 输出 |
|---|---|---|
| 检测 | 缩放后的整图张量 | 排序后的文本框 |
| 方向分类 | 固定尺寸文本裁剪 | 0° 或 180° |
| 识别 | 高度 48px 的变宽裁剪 | 文字与置信度 |
| 合并 | 识别块与坐标框 | 可读多行文本 |
把图片字节转移到独立 Worker
页面复用一个 worker,并为每个请求分配唯一 ID。把 ArrayBuffer 放进 transfer list,会转移所有权而不是克隆一份大图片。进度和结果按 ID 过滤,避免无关响应错误地结束另一个 Promise。
每次请求注册临时 message 与 error listener,结束后移除,并设置超时。worker 崩溃时要 terminate 并清空缓存实例,下一次不能继续复用损坏 worker。
const worker = new Worker(
new URL('../workers/ocr-accurate.worker.ts', import.meta.url),
{ type: 'module' },
);
const data = await file.arrayBuffer();
worker.postMessage({
id: requestId,
type: 'recognize',
file: { data, type: file.type, name: file.name, size: file.size },
language,
}, [data]);只加载一次 ONNX Session
ONNX Runtime Web 使用同源 WASM 路径、顺序执行、图优化和 WASM provider。三个模型与字符字典并行加载,再缓存共享 Promise。第一张图片承担模型成本,后续请求复用 session。
单线程配置不需要跨源隔离,内存行为也更可预测,代价是峰值吞吐较低。对一次处理一张图片的工具来说,这通常是合理取舍。
const [det, cls, rec, dictionary] = await Promise.all([
ort.InferenceSession.create('/models/ocr/det-mobile.onnx', options),
ort.InferenceSession.create('/models/ocr/cls.onnx', options),
ort.InferenceSession.create('/models/ocr/rec-unified-mobile.onnx', options),
fetch('/models/ocr/rec-unified-dict.txt').then(response => response.text()),
]);
const boxes = await detectTextBoxes(det, sourceImage);
for (const box of boxes) {
const crop = cropCanvas(sourceImage, box);
const oriented = await classifyTextOrientation(cls, crop);
blocks.push(await recognizeTextCrop(rec, oriented.canvas, dictionary, language));
}
return mergeRecognizedBlocks(blocks, language);每个模型都要匹配自己的预处理
createImageBitmap 解码 Blob,OffscreenCanvas 让像素操作留在 worker。分配更多画布前先检查像素上限。检测阶段缩放最长边并把尺寸对齐到 32;分类阶段拉伸到固定输入;识别阶段保留比例并在右侧补白。
检测使用 ImageNet normalization,分类和识别使用 Paddle 风格 normalization。百分位对比度拉伸能改善浅色文字,但只应在真实对比范围足够时启用。
- 绘制进 OffscreenCanvas 后立即关闭 ImageBitmap。
- 生成 CHW Float32 张量并明确拆分 RGB 通道。
- 识别高度固定,文本行最大宽度受限。
- 补白区域填充白色,避免透明像素变成黑色。
后处理和模型推理同样重要
原始概率图并不是文本行列表。阈值、膨胀、连通区域、得分过滤、边界扩展、行碎片合并与阅读排序,决定识别器看到的是完整文本还是破碎片段。
识别输出也要正确解码:忽略 blank、折叠连续相同类别、累计置信度,并在候选分数接近时优先选择目标语言字符。这样既能减少跨文字体系噪声,又不会硬性屏蔽标点与拉丁字符。
诚实展示进度与能力边界
模型、准备、检测、分类、识别和合并应成为独立进度阶段。逐框处理时还要显示已处理数量与总数,因为文本区域很多的页面,大部分时间可能花在检测之后。
轴对齐文本框和 0/180 分类无法彻底解决透视、曲线文字、竖排、手写或 90 度旋转。要求文档级准确率时,需要几何矫正、更完整的方向处理或专业服务。
总结
浏览器 OCR 的质量来自模型推理与传统后处理共同设计。把工作移入 Worker,缓存三个 session,分别匹配预处理,转移大缓冲,重建文本框与阅读顺序,并清楚说明剩余限制,才能形成完整工具。