HarmonyOS 相机 + 文字识别实现拍照识字 39 识别性能优化与内存管理
39 识别性能优化与内存管理
引言
"拍照识别文字"的体验好不好,一半看识别准不准,一半看快不快、稳不稳——尤其是连续拍照时,卡顿和内存暴涨会直接毁掉体验。OCR 是典型的计算密集 + 内存密集任务:一张高像素照片要解码成 RGBA 像素图、送入识别引擎,结果动辄几十毫秒到几百毫秒,解码出的 PixelMap 又是内存大户。本文结合工程 Camera.ets 中 recognizeImage 的真实代码,分析性能瓶颈来源,给出缩小输入、手动释放、避免重复创建、异步化四条优化主线,并梳理"拍照 → 识别"的时序,附一份可直接落地的优化清单。
正文知识点
1. 性能瓶颈在哪
一次识别的时间大致由四段组成:
- 图片解码:
image.createImageSource(buffer)+createPixelMap()把 JPEG 解成像素图,图越大越慢、内存越高; - 朝向检测:
isDirectionDetectionSupported: true时先判断文字方向再旋转,是纯额外开销(详见本系列第 36 篇); - 引擎识别:NPU/CPU 推理,与图像尺寸强相关;
- 结果序列化:生成
value、blocks等结构。
输入图的尺寸是最大变量:官方建议 720p 以上即可保证识别质量,过高分辨率(如 4000×3000 原图)对精度提升有限,却线性放大解码、推理、内存三笔开销。所以第一条优化就是控制输入尺寸。
2. 缩小图片:createPixelMap 时指定尺寸
image.ImageSource.createPixelMap 支持传入 InitializationOptions,其中 desiredSize 可以直接在解码阶段把像素图缩到目标分辨率,比"先解大图再缩放"省一次内存峰值。注意 VisionInfo.pixelMap 要求颜色格式为 RGBA_8888,缩放时不要顺手改成 RGB_565,否则会参数校验失败。
3. 手动释放:release() 是硬性要求
工程中的释放代码是教科书式示范(见下方代码示例)。imageResource(ImageSource)和 pixelMapInstance(PixelMap)都是必须手动释放的 Native 资源,ArkTS 侧垃圾回收不会及时回收底层 C++ 内存。不释放的后果:连续拍十几张后应用内存只涨不跌,最终被系统按内存水位回收甚至闪退。此外相机侧 releaseCamera() 里对 cameraInput、previewOutput、photoOutput、captureSession 的逐一 release() 同样属于这一条——凡是拿到"对象资源"的地方,都要问一句:它释放了吗?
4. 避免重复创建与并发识别
recognizeImage每次调用都新建ImageSource和PixelMap,用完即释放,这是正确的;要避免的是把"释放"漏掉,或把ImageSource存成成员变量长期占用。- 连续快速点快门时,
photoAvailable回调可能连续触发。如果上一次识别还没结束,this.result会被后一次覆盖,属于"可接受"的最终值语义;但若要更严谨,可以在takePicture()里加"识别中"标志,识别期间忽略新的拍照请求。
5. 异步与 UI 卡顿
recognizeImage 是 async 方法,recognizeText 返回 Promise,识别本身不阻塞 UI 线程;真正需要警惕的是大图解码那一步——它跑在异步上下文里,但如果输入是超大图,解码耗时同样可观,此时 UI 虽然没有卡死,弹窗打开会明显滞后。配合第 2 条的"解码期缩小",能同时缓解耗时与内存。
6. 拍照 → 识别时序
工程时序为:takePicture()(先置空 this.result)→ photoOutput.capture() → 会话回调 photoAvailable → photo.main.getComponent(JPEG) 取字节 → recognizeImage(buffer) 识别 → 写回 this.result → @Watch 触发弹窗。时序上的两个注意点:takePicture 置空 result 是为了防止旧结果重复弹出(详见第 37 篇);getComponent 的回调里 errCode 非 0 或 component 为 undefined 时直接 return,避免把脏数据送进识别。
代码示例
工程释放段逐行讲解(源码参考:entry/src/main/ets/common/utils/Camera.ets):
async recognizeImage(buffer: ArrayBuffer): Promise<string> {
let imageResource = image.createImageSource(buffer); // ① 解码源
let pixelMapInstance = await imageResource.createPixelMap(); // ② 像素图(内存大户)
let visionInfo: textRecognition.VisionInfo = {
pixelMap: pixelMapInstance
};
let textConfiguration: textRecognition.TextRecognitionConfiguration = {
isDirectionDetectionSupported: true
};
let recognitionString: string = '';
const context: common.UIAbilityContext = AppStorage.get('context') as common.UIAbilityContext;
try {
if (canIUse('SystemCapability.AI.OCR.TextRecognition')) {
await textRecognition.recognizeText(visionInfo, textConfiguration).then((TextRecognitionResult) => {
if (TextRecognitionResult.value === '') {
recognitionString = context.resourceManager.getStringSync($r('app.string.unrecognizable').id);
} else {
recognitionString = TextRecognitionResult.value;
}
})
pixelMapInstance.release(); // ③ 识别完成立即释放像素图
imageResource.release(); // ④ 释放解码源
} else {
recognitionString = context.resourceManager.getStringSync($r('app.string.Device_not_support').id);
Logger.error(TAG, `device not support`);
}
} catch (error) {
let err = error as BusinessError;
hilog.error(0x0000, 'Camera', `recognizeImage failed. code=${err.code}, message=${err.message}`);
}
return recognitionString;
}
两点改进建议:③④ 放在 finally 里更稳妥(异常路径也能释放);② 处可用 desiredSize 提前缩小。优化版如下:
async recognizeImageOptimized(buffer: ArrayBuffer): Promise<string> {
let imageResource = image.createImageSource(buffer);
// 解码期直接缩到 1080 宽以内(建议 720p 以上,1080 是安全上限)
let pixelMapInstance = await imageResource.createPixelMap({
desiredSize: { width: 1080, height: 1080 },
editable: false,
pixelFormat: image.PixelMapFormat.RGBA_8888 // VisionInfo 要求 RGBA_8888
});
let visionInfo: textRecognition.VisionInfo = { pixelMap: pixelMapInstance };
let textConfiguration: textRecognition.TextRecognitionConfiguration = {
isDirectionDetectionSupported: false // 已知正向时关闭朝向检测
};
let recognitionString: string = '';
try {
if (canIUse('SystemCapability.AI.OCR.TextRecognition')) {
const result = await textRecognition.recognizeText(visionInfo, textConfiguration);
recognitionString = result.value === '' ?
AppStorage.get<common.UIAbilityContext>('context')!.resourceManager.getStringSync($r('app.string.unrecognizable').id) :
result.value;
} else {
recognitionString = AppStorage.get<common.UIAbilityContext>('context')!.resourceManager.getStringSync($r('app.string.Device_not_support').id);
}
} catch (error) {
let err = error as BusinessError;
hilog.error(0x0000, 'Camera', `recognizeImage failed. code=${err.code}, message=${err.message}`);
} finally {
// 无论成功失败都释放,防止内存泄漏
pixelMapInstance.release();
imageResource.release();
}
return recognitionString;
}
运行效果与注意事项
- 内存观测:用
hdc shell查看应用内存,或在 DevEco Studio 的 Profiler 里连续拍照识别,观察 native 内存曲线是否持续走高——若只涨不跌,基本可以断定有release()漏网之鱼。 - 优化前后对比:把输入从 4000×3000 缩到 1080 内 + 关闭朝向检测,单次识别耗时通常能缩短一半以上,弹窗出现明显更快。
- finally 的重要性:
recognizeText抛异常(超时 200、服务异常 1001400002 等)时,原工程③④两行不会执行;用finally统一释放是更稳妥的写法。 - 相机资源:页面
onPageHide/aboutToDisappear时调用releaseCamera()逐一释放相机输出与会话,否则切后台再回来可能黑屏或无法重新取景。
优化清单
- [ ] 输入图用
desiredSize缩到 1080 以内,保持RGBA_8888; - [ ] 已知正向图片关闭
isDirectionDetectionSupported; - [ ]
pixelMapInstance.release()与imageResource.release()放进finally; - [ ] 页面隐藏/销毁时释放相机会话与输出;
- [ ] 连续拍照加"识别中"互斥标志,避免并发覆盖结果;
- [ ] 识别前后打
hilog时间戳,量化每段耗时,持续调优。
总结
OCR 优化的本质是"掐两头":输入端控制图片尺寸、关掉不必要的朝向检测,输出端保证资源必释放。release() 放在 finally、createPixelMap 时指定 desiredSize,是成本最低、收益最大的两处改动;再配合拍照时序上的互斥与页面生命周期里的相机释放,一个"又快又稳"的拍照识别应用就成型了。
*本系列文章基于 HarmonyOS 5.0.5 SDK(API 12)与示例工程 aicharacter-recognition 编写,文中接口以对应版本 API 参考为准。*
更多推荐



所有评论(0)