在HarmonyOS的文档扫描增强管线中,将增强后的图片送入系统OCR并输出文本,并非简单的API调用。系统OCR对输入格式要求苛刻,且存在冷启动延迟等问题。本文分享一套封装方案,总代码不足300行,但每个函数都来自实际踩坑。
- 用户选图
- ↓
- decodeImageForOcr() — 解码+降分辨率+HDR容错
- ↓
- cropPixelMapToMaxAspectRatioIfNeeded() — 超长图居中裁剪
- ↓
- pixelMapToFlatRgba8() — 像素格式扁平化
- ↓
- recognizeTextFromPixelMapWithRetry() — 带重试的OCR调用
- ↓
- sanitizeRecognizedText() — 控制字符清洗
- ↓
- 返回纯文本
复制代码
一、解码容错:三级回退链
OCR推荐RGBA_8888格式,动态范围SDR或AUTO。但实际图片可能是HEIF、HDR、非标准JPEG等。使用三级回退:先尝试AUTO动态范围,失败后尝试SDR,最后不限格式。注意每次try失败后必须重新createImageSource,因为HarmonyOS的ImageSource在解码失败后状态不确定,复用会抛异常。
- async function decodeImageForOcr(filePath: string): Promise<image.PixelMap> {
- const src = image.createImageSource(filePath);
- const sizeOpt = { width: 1600, height: 1600 };
- // 第一优先:RGBA+AUTO
- try { return await src.createPixelMap({ desiredSize: sizeOpt, editable: true, desiredPixelFormat: image.PixelMapFormat.RGBA_8888, desiredDynamicRange: image.DecodingDynamicRange.AUTO }); } catch (_e) {}
- // 第二优先:RGBA+SDR
- try { src = image.createImageSource(filePath); return await src.createPixelMap({ desiredSize: sizeOpt, editable: true, desiredPixelFormat: image.PixelMapFormat.RGBA_8888, desiredDynamicRange: image.DecodingDynamicRange.SDR }); } catch (_e2) {}
- // 第三优先:不限格式
- return await src.createPixelMap({ desiredSize: sizeOpt, editable: true });
- }
复制代码 AUTO优先是因为部分HDR图片只有用AUTO才能正确解码;SDR优先于不限格式,因为OCR期望RGBA_8888,不限格式可能返回BGRA或YUV导致异常。
二、超长图裁剪
系统建议OCR输入宽高比不超过2:1。超长截图如果不裁剪,OCR可能只识别前半段。采用居中裁剪,丢弃边角内容。
- async function cropPixelMapToMaxAspectRatioIfNeeded(pm: PixelMap): Promise<PixelMap> {
- const info = await pm.getImageInfo();
- const w = info.size.width, h = info.size.height;
- const ratio = w > h ? w / h : h / w;
- if (ratio <= 2 + 1e-6) return pm;
- let region: image.Region;
- if (w / h > 2) {
- const nw = Math.floor(h * 2);
- const nx = Math.floor((w - nw) / 2);
- region = { x: nx, y: 0, size: { width: nw, height: h } };
- } else {
- const nh = Math.floor(w * 2);
- const ny = Math.floor((h - nh) / 2);
- region = { x: 0, y: ny, size: { width: w, height: nh } };
- }
- await pm.crop(region);
- return pm;
- }
复制代码 裁剪失败不中断,catch后用原图。
三、像素扁平化:为什么需要重建PixelMap
HarmonyOS的PixelMap内部可能使用多种格式(RGBA、BGRA、YUV等)。即使解码指定RGBA_8888,某些设备底层可能偷偷用BGRA。OCR引擎直接读像素数据会出错。通过readPixelsToBuffer读出原始字节,再用RGBA_8888格式重新创建PixelMap,强制统一格式。
- async function pixelMapToFlatRgba8(pm: PixelMap): Promise<PixelMap> {
- const info = await pm.getImageInfo();
- const w = info.size.width, h = info.size.height;
- const buf = new ArrayBuffer(pm.getPixelBytesNumber());
- await pm.readPixelsToBuffer(buf);
- const init = { editable: true, pixelFormat: image.PixelMapFormat.RGBA_8888, size: { width: w, height: h } };
- return await image.createPixelMap(buf, init);
- }
复制代码 “读出再重建”消除了手动检测BGRA的复杂逻辑。
四、重试策略:处理OCR冷启动
系统OCR服务(CoreVisionKit)首次初始化需要500ms~2s,可能超时。重试2次,每次间隔400ms。同时预初始化:进入OCR页面时调用init()。
- async function recognizeTextFromPixelMapWithRetry(pm: PixelMap): Promise<string> {
- const MAX_RETRIES = 2, RETRY_DELAY = 400;
- let lastErr = new Error('OCR failed');
- for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
- try {
- if (attempt > 0) await sleepMs(RETRY_DELAY);
- return await recognizeTextFromPixelMap(pm);
- } catch (e) { lastErr = e instanceof Error ? e : new Error('OCR failed'); }
- }
- throw lastErr;
- }
复制代码 预初始化把冷启动延迟提前到页面进入时,用户感知延迟从1~2s降至0ms。
五、调用链与回退
完整流程中,扁平化路径失败后自动回退到非扁平化路径。因为某些设备上readPixelsToBuffer可能抛异常(如内存不足),此时用原始PixelMap调OCR反而成功。
六、OCR结果解析与清洗
OCR返回的TextRecognitionResult结构可能版本不同。采用三级回退:value → textWords → blocks→lines。textWords字段在TypeScript类型定义中不存在,但运行时可能存在,使用Record<string, object>动态访问绕过类型检查。
文本清洗时保留换行、回车、制表符,丢弃其他控制字符。逐charCode比较比正则表达式快3~5倍。
七、错误码映射与踩坑记录
错误码如1001400001直接展示用户看不懂,映射为中文提示并截断到96字符。
常见坑:
1. OCR返回空字符串而非报错——需判断空结果并提示“未检测到文字”。
2. HDR图片像素值可能超255——SDR优先于AUTO强制转8-bit。
3. 大图片内存峰值——扁平化后立即release原始PixelMap。
4. 并发OCR调用导致死锁——用Promise链串行化。
八、性能与总结
冷启动延迟主要由预初始化消除;热启动250~730ms。优势是零包体、系统级维护、原生中文支持;代价是不能自定义模型、离线训练。
核心设计思想:用确定性工程手段应对不确定系统行为——格式不确定就强制重建,服务不确定就重试+预初始化,SDK不确定就动态类型兜底。 |