OCR 没有适用于所有场景的最佳引擎。正确选择取决于文档复杂度、语言、隐私边界、流量、延迟、基础设施,以及团队愿意维护多少识别流程。
本文不从代码开始,而是先建立选型逻辑:对比常见 OCR 方案,说明它们分别适合什么环境,再记录 ToolGarden 如何使用 PaddleOCR JS、PP-OCRv5 和 ONNX Runtime 完成一次生产落地。浏览器只是本文的具体案例,不是结论的边界;同一套判断方法也适用于服务端、桌面应用、移动端、内部系统和公开网站。
先定义需求,不要先决定模型
OCR 准确率不是一个脱离场景的数字。同一个模型可能擅长清晰英文扫描件,却不适合中文店铺招牌、旋转小票、密集表格、手写内容或弱光手机照片。选型前应准备有代表性的测试集,并明确什么才算可用结果:纯文本、带坐标文字块、表格、键值字段、可搜索 PDF,还是完整文档重建。
运行约束同样重要。需要确认图片能否离开设备、是否必须离线、首次允许下载多大模型、支持哪些浏览器或操作系统、是否有 GPU、并发量有多大,以及由谁负责更新模型和运行时二进制文件。这些问题通常比一次准确率演示更能决定技术路线。
- 识别质量:语言、字体、旋转、透视、手写、表格与小字。
- 输出要求:纯文本、坐标、阅读顺序、结构化字段或版面恢复。
- 运行成本:延迟、吞吐、冷启动、内存、包体和离线能力。
- 数据治理:上传策略、数据区域、保留周期、审计与供应商依赖。
- 维护责任:集成、模型升级、预处理、后处理和测试由谁承担。
对比几条常见 OCR 技术路线
有价值的比较不能只分成本地和云端。云文档 API、PaddleOCR Python 服务、Tesseract、PaddleOCR JS 和自研 ONNX 流水线提供了不同抽象层,也分别把成本放在基础设施、网络传输、客户端资源、研发维护和供应商依赖上。
下表是选型参考,不是绝对排名。最终准确率必须在同一批图片、语言、预处理和输出要求下测量。拿云端表格解析器与本地纯文本模型直接比较,会因为任务不一致而得到没有意义的结论。
| 技术路线 | 更适合的场景 | 主要优势 | 成本与限制 |
|---|---|---|---|
| 云 OCR 或文档 API | 复杂表单、表格、证件、小票和快速上线 | 托管扩缩容、结构化能力成熟、客户端负担小 | 文件上传、持续费用、网络延迟、留存审查、供应商锁定 |
| PaddleOCR Python 或原生服务 | 服务端、桌面后端、私有基础设施 | 开源完整产线、可调能力强、更容易使用 GPU | 原生依赖、需要运维、部署体积较大 |
| PaddleOCR JS 与 PP-OCRv5 | 网页、Electron、离线优先和不上传产品 | 本地推理、复用 Paddle 产线、可静态交付 | WASM 性能、首次模型下载、浏览器内存与兼容性 |
| Tesseract 或 Tesseract.js | 清晰扫描件、简单版式、已有语言包场景 | 生态成熟、经典 OCR 流程稳定 | 场景文字与复杂版式通常较弱,依赖预处理 |
| 自研 ONNX 流水线 | 特殊模型、特殊硬件或强控制要求 | 模型、张量、批处理与输出完全可控 | 预处理、后处理和兼容性维护成本最高 |
根据运行环境和产品边界做选择
如果允许上传文件,并且必须快速获得表格结构、表单字段或文档语义,托管式文档 OCR API 往往是最短路径。如果数据必须留在受控基础设施内,而且需要 GPU 吞吐,PaddleOCR Python 或其他原生产线通常比强行把浏览器运行时搬到服务端更合适。
Electron 可以复用 JavaScript 与 WASM,桌面 sidecar 也可以运行 Python/C++。移动端应评估原生推理和设备加速。对于清晰、规则扫描件,只要实测达到要求,Tesseract 仍然合理。只有额外控制能解决明确问题时,才值得承担自研 ONNX 流水线的维护成本。
ToolGarden 选择 PaddleOCR JS 与 PP-OCRv5,是因为产品边界明确:静态网站、图片不上传、没有 OCR 后端、需要识别多语言印刷体,并且能接受首次模型下载。约束改变时,推荐方案也应改变。
实现路径一:先建立基准,再开始集成
编写适配代码前,先准备一组小而稳定的基准图片。至少包含清晰截图、手机拍照、简体中文、繁体中文、英文、日文、小字号、旋转、低对比度,以及一种产品明确不承诺还原的复杂版式。应记录期望文本和关键字段,不能只凭肉眼判断看起来不错。
同时测量首次初始化、缓存后识别、峰值内存、模型传输量、检测框数量,以及字符或关键字段准确率。所有候选方案必须使用相同原图比较。模型更大不保证更好,因为预处理、检测、裁剪、字典、解码和阅读顺序都会影响结果。
| 指标 | 意义 | 测试方式 |
|---|---|---|
| 冷启动 | 包含运行时、模型下载、解包和 session 创建 | 空缓存与慢速网络 |
| 热启动耗时 | 代表重复使用体验 | 第二张及后续图片 |
| 识别质量 | 发现错字、漏字与顺序错误 | 按语言和图片类型统计 |
| 资源使用 | 发现移动端不稳定 | 大图与大量文本框 |
| 错误恢复 | 确认失败后能够重试 | 断网、损坏资源、终止 Worker |
实现路径二:隔离重计算并定义通信协议
页面负责 UI 状态、文件校验、进度展示和本地化错误;module Worker 负责图片解码、OpenCV 预处理、模型初始化、推理和结果转换。每条请求和响应都携带 ID,旧请求的延迟进度不能结束新请求。
文件转换成 ArrayBuffer 并加入 transfer list,所有权移动到 Worker,避免大图片再次克隆。任务结束后移除监听器。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]);实现路径三:按运行环境适配图片解码
Web Worker 有 Blob、createImageBitmap、ImageData 和 OffscreenCanvas,却没有 document、HTMLCanvasElement、HTMLImageElement 或 HTMLVideoElement。部分图片依赖仍会检查这些对象,这就是 document is not defined 和 HTMLImageElement is not defined 的来源。
适配层只补充依赖实际读取的对象:canvas 映射到 OffscreenCanvas,sourceToMat 解码 Blob、绘制离屏画布、读取 ImageData 并创建 OpenCV Mat,dispose 删除 Mat 并关闭 bitmap。Node、Electron 主进程或原生应用应替换为各自的解码器,而不是扩大 DOM shim。
function installWorkerCanvasDomShim() {
Object.defineProperty(globalThis, 'HTMLCanvasElement', {
configurable: true,
value: OffscreenCanvas,
});
Object.defineProperty(globalThis, 'document', {
configurable: true,
value: {
createElement(tagName: string) {
if (tagName.toLowerCase() !== 'canvas') throw new Error('Unsupported element');
return new OffscreenCanvas(1, 1);
},
},
});
}实现路径四:把 PP-OCRv5 作为完整版本单元加载
应用显式传入检测和识别模型,不依赖远程默认地址。语言映射成 Paddle 标识,OCR 实例按语言缓存,检测与识别批量大小可以分别调节。
ONNX Runtime 使用 WASM、SIMD、单线程并关闭 proxy。单线程不要求跨源隔离。示例阈值是针对当前图片场景的调优结果,并非通用常量;小字、场景文字和移动端限制都需要重新基准测试。
const ocr = await PaddleOCR.create({
lang: toPaddleLanguage(language),
ocrVersion: 'PP-OCRv5',
textDetectionModelName: 'PP-OCRv5_mobile_det',
textDetectionModelAsset: {
url: '/models/paddleocr/ppocr-v5/PP-OCRv5_mobile_det_onnx_infer.tar',
},
textRecognitionModelName: 'PP-OCRv5_mobile_rec',
textRecognitionModelAsset: {
url: '/models/paddleocr/ppocr-v5/PP-OCRv5_mobile_rec_onnx_infer.tar',
},
sourceToMat: sourceToMatInWorker,
ortOptions: {
backend: 'wasm',
wasmPaths: {
mjs: '/models/paddleocr/onnxruntime-web/ort-wasm-simd-threaded.mjs',
wasm: '/models/paddleocr/onnxruntime-web/ort-wasm-simd-threaded.wasm',
},
numThreads: 1,
simd: true,
proxy: false,
},
});
const [result] = await ocr.predict(image, {
textDetLimitSideLen: 1280,
textDetThresh: 0.24,
textDetBoxThresh: 0.34,
textDetUnclipRatio: 1.8,
textRecScoreThresh: 0.28,
});实现路径五:在应用边界统一结果
依赖输出被转换成应用自己的判别联合类型。成功包含文本、文字块、置信度、坐标、图片尺寸和耗时;失败使用 model_load_failed、worker_timeout、recognition_failed、no_text_detected 等稳定代码,UI 不需要解析异常字符串。
多边形转换成展示矩形,过滤空内容和低置信度项,再按垂直中心和平均行高归并。行内从左到右,各行从上到下,最后用换行连接。结果适合纯文本复制,但不承诺重建表格、分栏和原文档样式。
需要注意:存活、交付、缓存和内存
固定总超时会混淆缓慢进展与真正失败。Worker 在初始化和识别期间每十秒发送心跳;页面收到消息后重置无响应计时器,并对模型和处理阶段设置独立硬上限。每条失败路径都丢弃 Worker,确保可以干净重试。
同源模型消除了第三方 CORS 和可用性风险,但 URL、MIME、缓存更新、文件限制和首次下载仍要治理。ONNX Runtime JavaScript 与 WASM 必须同版本。OpenCV Mat、ImageBitmap、URL、监听器、定时器、失败 Promise 和 Service Worker 缓存都要管理生命周期。
async function withProgressHeartbeat(progress, operation) {
postProgress(progress);
const heartbeat = setInterval(() => postProgress(progress), 10_000);
try {
return await operation();
} finally {
clearInterval(heartbeat);
}
}
const IDLE_TIMEOUT = 180_000;
const MODEL_PHASE_TIMEOUT = 12 * 60_000;
const PROCESSING_PHASE_TIMEOUT = 6 * 60_000;
function failTimeout() {
worker.terminate();
cachedWorker = null;
resolve({ ok: false, code: 'worker_timeout' });
}- 把 npm 依赖与二进制资源作为一组锁定,并检查最终 bundle。
- 用稳定 URL 和正确 Content-Type 提供模型、MJS 与 WASM。
- 缓存成功初始化,失败 Promise 必须移除。
- 分配 Canvas 前限制像素,默认不要并发识别。
- 根据构建内容生成 Service Worker 缓存版本。
遇到的问题,以及报错真正说明什么
故障分别来自安装、打包、资源交付、Worker 兼容、运行时 ABI 和缓存。把所有消息当成孤立 npm 问题会持续返工。有效方法是先判断症状属于哪一层,再决定是否改依赖。
最后的超时最容易误导。延长时间无法修复不兼容运行时。真正定位依靠一张带固定文字的最小图片,通过生产 Worker 和公开路径完成全链路识别,最终暴露隐藏的 _OrtGetInputName 错误。
| 表面报错 | 根因 | 长期解决方式 |
|---|---|---|
| npm edgesOut 错误 | 安装器解析依赖树失败 | 使用确切兼容依赖和可复现安装模式 |
| 找不到 ort.bundle.min.mjs | 运行时入口不存在 | 映射到所选版本真实入口 |
| 找不到 ORT 指定版本 | 请求了未发布版本 | 确认 registry 后锁定版本 |
| WASM 超过文件限制 | 错误运行时变体进入产物 | 只交付所需文件并检查体积 |
| 动态 MJS 加载失败 | URL 或模块部署错误 | 使用同源 URL 并检查生产响应 |
| document 或 HTMLImageElement 缺失 | DOM 代码进入 Worker | 使用受限 OffscreenCanvas 适配 |
| _OrtGetInputName 缺失 | JS 与 WASM ABI 不同 | 内置并校验匹配运行时 |
| Worker 超时 | 固定计时器掩盖异常 | 分层错误与心跳存活判断 |
验证完整路径,而不是只验证构建
类型检查不能证明模型能下载、MJS 能找到 WASM、Worker 能解码图片。验证必须经过生产公开路径、缓存层、Worker 入口和 OCR API。先用带固定短语的生成图片做确定性冒烟,再用基准集评估质量。
测试空缓存、热缓存、慢网络、缓存后离线、旋转、语言、大图、强制终止和再次识别。部署验证使用新 origin 或清除 Service Worker。构建期 SHA-256 校验可以阻止资源缺失或混合版本进入生产。
for (const asset of pinnedOcrAssets) {
const actual = createHash('sha256')
.update(readFileSync(asset.path))
.digest('hex');
if (actual !== asset.sha256) {
throw new Error(`OCR asset checksum mismatch: ${asset.path}`);
}
}总结
OCR 应按任务和运行边界选型,而不是按模型大小或单张演示图决定。需要托管式结构化文档能力时可选云文档 API;受控服务端和 GPU 场景优先评估原生 PaddleOCR;规则扫描件仍可评估 Tesseract;本地处理与 Web 交付是硬要求时,PaddleOCR JS 与 PP-OCRv5 是合理选择。无论哪条路线,预处理、运行时兼容、结果契约、存活判断、缓存、内存和端到端验证都是 OCR 系统的一部分。完整记录这条决策链,比复制某段代码更有长期价值。