ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

HarmonyOS 7 视觉 AI 两步接入:人脸检测与 OCR 实战

HarmonyOS 7 视觉 AI 两步接入:人脸检测与 OCR 实战 1. 为什么要在 HarmonyOS 7 上做视觉 AI 两步接入1.1 从“能跑”到“好用”的视觉能力分水岭HarmonyOS 7 把视觉 AI 能力收拢到 Core Vision Kit 之后很多做端侧应用的朋友第一反应是“又多了一套要学的东西”。但实际接进去跑一遍就会发现它解决的是过去端侧视觉最头疼的两个问题一是模型体积和推理速度的平衡二是系统级能力与业务代码的耦合度。人脸检测和通用文字识别OCR这两个能力恰好是绝大多数视觉类应用的第一步和第二步——先知道画面里有没有人、人在哪再知道画面里写了什么字。这两步串起来就能撑起考勤打卡、证件识别、票据录入、课堂笔记扫描、无障碍阅读等一大票场景。我这次拿到的需求很典型一个内部使用的巡检记录应用需要现场拍照后自动框出人脸位置做身份核验同时把设备铭牌上的文字提取出来归档。过去这类需求要么依赖云端接口要么自己塞一个 TFLite 模型进去前者有网络延迟和隐私顾虑后者光是模型转换和算子适配就能耗掉一周。Core Vision Kit 把这两件事都做成了系统级 API调用方式统一返回结构清晰对于不想在模型层面反复折腾的团队来说省下来的时间足够把业务逻辑打磨两轮。1.2 人脸检测与 OCR 的能力边界先摸清楚在动手写代码之前有必要把这两个能力的边界说清楚否则很容易在验收阶段被业务方问住。人脸检测在 Core Vision Kit 里提供的是检测框、关键点眼睛、鼻子、嘴角等以及置信度它不做人脸比对也不做人脸特征提取。也就是说它能告诉你“这里有一张脸”但不能告诉你“这是谁”。如果你需要身份核验得在检测到人脸之后自己接一个特征比对模块或者把裁剪后的人脸图交给后端做比对。通用文字识别这边能力覆盖的是印刷体为主的文字提取支持中英文混排、数字、常见符号返回的是文本块列表每个块带边界框和置信度。它对手写体的支持有限对极端倾斜、强反光、低对比度的场景也需要配合预处理。我实测下来正常光照下的设备铭牌、A4 打印文档、快递面单识别率相当可观但如果是手写签到表或者被油污覆盖的金属标牌就得在拍照环节做引导或者加一道图像增强。提示Core Vision Kit 的能力是端侧推理不依赖网络这一点在弱网环境或者对数据隐私敏感的场景里是决定性优势。但端侧模型体积有限别指望它能处理特别复杂的版面分析复杂版面建议先做区域裁剪再送识别。1.3 两步接入的整体思路所谓“两步接入”指的是把视觉能力拆成两个独立的调用链路第一步做人脸检测拿到人脸区域和关键点第二步做文字识别拿到文本内容。这两步可以串行也可以并行取决于业务是否需要根据人脸位置来限定 OCR 区域。比如巡检场景里如果人脸和铭牌在同一张图里可以先检测人脸排除干扰区域再对剩余区域做 OCR如果人脸和铭牌是两张图那就各走各的链路互不干扰。这种拆分的好处是职责清晰调试的时候能快速定位是哪一步出了问题。人脸检测不准就去调检测参数OCR 识别率低就去查图像质量和预处理。比起把两个能力揉在一个大函数里拆开之后维护成本低得多。下面我会按照“环境准备—人脸检测—文字识别—联调优化”的顺序把每一步的细节和踩过的坑都摊开讲。2. 环境准备与工程配置的实操细节2.1 SDK 版本与设备要求Core Vision Kit 的能力在 HarmonyOS Next SDKAPI 12上才完整开放如果你手上的工程还是 API 11 或者更早需要先升级编译基线。我这次用的是 5.0.0(12) 的 SDKDevEco Studio 版本对应到 5.0 以上。设备方面真机调试是必须的模拟器对相机和视觉能力的支持不完整很多 API 在模拟器上直接返回空结果这一点在官方文档里写得比较含蓄但实际开发中几乎人人踩过。在build-profile.json5里需要确认compileSdkVersion和compatibleSdkVersion都指向 12 或更高。如果工程里有多个模块注意每个模块的配置都要对齐否则会出现主模块能编译、feature 模块找不到类的尴尬情况。我遇到过因为一个 feature 模块的 SDK 版本没改导致kit.CoreVisionKit导入报红排查了半天才发现是模块级配置不一致。{ app: { compileSdkVersion: 12, compatibleSdkVersion: 12 } }2.2 权限声明与动态申请相机权限和读取图片的权限是基础但容易被忽略的是部分视觉能力在首次调用时会触发系统级的能力下载或初始化这个过程需要网络权限。虽然推理本身是端侧但能力包的首次准备可能依赖网络。所以module.json5里至少要声明ohos.permission.CAMERA、ohos.permission.READ_IMAGEVIDEO以及ohos.permission.INTERNET作为兜底。动态申请这块相机权限属于 user_grant 类型必须在运行时弹窗申请。我的做法是在页面aboutToAppear里先检查权限状态没有就发起申请申请结果用回调处理不要用同步等待否则会阻塞 UI 线程。实测下来如果用户在弹窗里点了拒绝再次申请时系统可能不再弹窗这时候需要引导用户去设置页手动开启代码里要做好这个分支。import { abilityAccessCtrl, common } from kit.AbilityKit; async function requestCameraPermission(context: common.UIAbilityContext): Promiseboolean { const atManager abilityAccessCtrl.createAtManager(); const result await atManager.requestPermissionsFromUser(context, [ohos.permission.CAMERA]); return result.authResults[0] 0; }2.3 依赖导入与初始化时机Core Vision Kit 的导入路径是kit.CoreVisionKit里面包含faceDetector和textRecognition等模块。初始化时机很关键不要放在aboutToAppear里同步做因为初始化可能涉及 native 层加载耗时在几十到几百毫秒不等。我的做法是在页面加载完成后用一个异步任务去预热等用户真正触发拍照或选图时能力已经就绪体验上几乎没有等待感。import { faceDetector, textRecognition } from kit.CoreVisionKit; async function warmUp() { await faceDetector.init(); await textRecognition.init(); }注意初始化失败不要直接崩溃要做好降级处理。我见过因为设备不支持某个能力导致 init 抛异常结果整个页面白屏的情况。用 try-catch 包住失败时给用户一个“当前设备不支持该功能”的提示比直接闪退体面得多。3. 人脸检测接入从拍照到拿到关键点3.1 图像输入格式的坑人脸检测的输入是PixelMap或者图像文件路径但实际用下来PixelMap的兼容性最好。如果你从相机拿到的是 JPEG 数据需要先解码成PixelMap再送检测。这里有个细节解码时的采样率会影响检测速度和精度。我试过用原始分辨率送检测一张 4000x3000 的图检测耗时明显上升而人脸区域在整图中的占比并不大。后来改成先按长边缩放到 1280 再送检测速度提升明显精度几乎没有损失因为人脸检测模型本身对输入尺寸有归一化处理。import { image } from kit.ImageKit; async function decodeToPixelMap(data: ArrayBuffer): Promiseimage.PixelMap { const source image.createImageSource(data); const opts: image.DecodingOptions { desiredSize: { width: 1280, height: 960 } }; return await source.createPixelMap(opts); }3.2 检测参数怎么调faceDetector.detect()的入参里比较关键的是检测模式和人脸数量上限。检测模式分为快速模式和精确模式快速模式适合实时预览流精确模式适合单张拍照后的静态检测。人脸数量上限默认是 5如果你的场景是单人核验设成 1 能减少后处理负担如果是合影场景按实际人数上限设置设太大反而会增加误检概率。返回结果里每个人脸对象包含边界框boundingBox、关键点数组landmarks和置信度confidence。置信度阈值我一般设在 0.7 左右低于这个值的框直接丢弃。实测发现侧脸和遮挡情况下置信度会明显下降如果业务对漏检敏感可以降到 0.5但误检会增多需要根据场景权衡。参数推荐值说明检测模式静态图用精确模式实时流用快速模式人脸上限单人场景设 1合影按实际人数置信度阈值0.7漏检敏感可降至 0.5输入长边1280兼顾速度与精度3.3 关键点数据的业务用法拿到关键点之后能做的事情比想象中多。比如判断人脸是否正对镜头可以用左右眼关键点的水平距离和鼻子关键点的位置关系来估算偏转角度。偏转超过一定阈值就提示用户“请正对镜头”这在考勤打卡场景里非常实用。再比如可以用关键点做简单的人脸对齐把倾斜的人脸旋转到正方向再送给后续的比对模块能提升比对通过率。function isFrontFacing(landmarks: faceDetector.Landmark[]): boolean { const leftEye landmarks.find(l l.type faceDetector.LandmarkType.LEFT_EYE); const rightEye landmarks.find(l l.type faceDetector.LandmarkType.RIGHT_EYE); const nose landmarks.find(l l.type faceDetector.LandmarkType.NOSE); if (!leftEye || !rightEye || !nose) return false; const eyeCenterX (leftEye.position.x rightEye.position.x) / 2; const offset Math.abs(nose.position.x - eyeCenterX); const eyeDistance Math.abs(rightEye.position.x - leftEye.position.x); return offset / eyeDistance 0.15; }实操心得关键点的坐标系是相对于原图的如果你在送检测之前做了缩放拿到的关键点坐标需要按缩放比例还原否则画框和实际人脸会对不上。这个坑我在第一版里踩过预览框偏了半个脸排查了一下午才发现是坐标没换算。4. 通用文字识别接入从文本块到结构化字段4.1 OCR 的输入预处理文字识别对人脸检测的输入要求更挑剔。人脸检测对光照和对比度有一定容忍度但 OCR 对图像质量非常敏感。我总结下来送 OCR 之前最好做三件事转灰度、提升对比度、适度锐化。转灰度能减少颜色通道的干扰提升对比度能让文字边缘更清晰锐化则能补偿拍照时的轻微模糊。Core Vision Kit 本身没有提供预处理接口这部分需要自己用 ImageKit 的像素操作或者引入轻量的图像处理库来做。function enhanceForOcr(pixelMap: image.PixelMap): image.PixelMap { // 伪代码示意实际需用 pixelMap 的读写接口逐像素处理 // 1. 转灰度gray 0.299*R 0.587*G 0.114*B // 2. 对比度拉伸newVal (val - 128) * factor 128 // 3. 锐化卷积核 [[0,-1,0],[-1,5,-1],[0,-1,0]] return pixelMap; }4.2 识别结果的结构与解析textRecognition.recognize()返回的是一个文本块数组每个块包含text、boundingBox和confidence。文本块是按行组织的但行与行之间的顺序不一定符合阅读习惯尤其是多栏排版或者有表格的情况下。我的做法是先按boundingBox的纵坐标排序再按横坐标排序这样能还原出大致的阅读顺序。对于表格类内容还需要根据横坐标的分布做列对齐这部分逻辑比较复杂如果业务对表格结构要求高建议先做区域裁剪把表格单独切出来再识别。function sortTextBlocks(blocks: textRecognition.TextBlock[]): textRecognition.TextBlock[] { return blocks.sort((a, b) { const yDiff a.boundingBox.top - b.boundingBox.top; if (Math.abs(yDiff) 10) return yDiff; return a.boundingBox.left - b.boundingBox.left; }); }4.3 从文本到结构化字段的映射识别出文字只是第一步业务真正需要的是结构化字段。比如设备铭牌上要提取“设备编号”“型号”“生产日期”这些字段的位置相对固定可以用关键词匹配加位置约束的方式来做。我的做法是先用正则匹配关键词再根据关键词所在文本块的边界框去邻近区域找对应的值。这种方法比纯正则更稳因为铭牌上的值可能包含特殊字符纯正则容易漏。字段匹配策略容错处理设备编号关键词“编号”右侧同行的文本块允许编号含字母数字混排型号关键词“型号”右侧同行的文本块去除空格和特殊符号生产日期日期正则 关键词邻近支持多种日期格式提示OCR 返回的置信度可以用来做质量门禁。如果某个关键字段的置信度低于 0.6不要直接入库而是标记为“待人工确认”让用户手动修正。这个策略在票据录入场景里能显著降低错误率。5. 两步联调与性能优化的实战记录5.1 串行还是并行人脸检测和 OCR 是串行还是并行取决于业务逻辑。如果 OCR 区域需要根据人脸位置来排除那就必须串行先检测人脸再把人脸区域从 OCR 输入里裁掉。如果两者互不干扰并行能省时间。我实测下来一张 1280 长边的图人脸检测耗时在 80 到 150 毫秒OCR 耗时在 200 到 400 毫秒串行总耗时在 300 到 550 毫秒之间对于拍照后等待结果的场景完全可以接受。如果做实时流那就得把检测频率降下来比如每三帧检测一次中间帧复用上一次的结果。async function processImage(pixelMap: image.PixelMap) { const faces await faceDetector.detect(pixelMap); const ocrInput faces.length 0 ? cropOutFaces(pixelMap, faces) : pixelMap; const texts await textRecognition.recognize(ocrInput); return { faces, texts }; }5.2 内存与线程管理视觉能力调用会占用 native 内存如果频繁调用不及时释放很容易触发 OOM。我的做法是每次处理完一张图手动调用pixelMap.release()释放图像资源检测和识别返回的对象虽然由 GC 管理但底层 native 内存的释放有时滞后所以在页面销毁或者批量处理结束后主动触发一次能力模块的release()。另外这些调用不要放在主线程用 TaskPool 或者 Worker 放到后台线程避免阻塞 UI。import { taskpool } from kit.ArkTS; Concurrent async function detectInBackground(data: ArrayBuffer): PromisefaceDetector.Face[] { const pixelMap await decodeToPixelMap(data); const faces await faceDetector.detect(pixelMap); pixelMap.release(); return faces; }5.3 实测性能数据与调优方向我在一台中端设备上做了几组对比测试数据如下场景输入长边人脸检测耗时OCR 耗时总耗时单人证件照128085ms210ms295ms多人合影1280140ms380ms520ms设备铭牌128090ms320ms410ms原始分辨率4000420ms1200ms1620ms从数据能看出来缩放对性能的影响是决定性的。原始分辨率下总耗时超过 1.5 秒用户体验明显下降缩放到 1280 之后总耗时控制在 500 毫秒以内体感上几乎是“拍完就出结果”。所以我的建议是除非业务对极小文字有识别需求否则一律先缩放再送检测和识别。6. 常见问题排查与避坑清单6.1 检测不到人脸或识别不出文字这是最高频的问题排查顺序建议从图像质量开始。先确认送进去的PixelMap不是空图或者全黑图再确认图像方向是否正确。相机拍出来的图可能带旋转信息如果解码时没处理送进去的就是旋转 90 度的图人脸检测和 OCR 都会失效。处理方法是读取 EXIF 里的方向信息在解码时做对应的旋转。function getRotationFromExif(data: ArrayBuffer): number { // 解析 EXIF 方向标签返回需要旋转的角度 // 常见值10度690度8270度 return 0; }6.2 置信度忽高忽低置信度波动大通常和光照条件、拍摄距离有关。如果业务对稳定性要求高可以在拍照环节加引导比如画一个取景框提示用户把目标放在框内保持适当距离。另外连续多帧检测取最高置信度的结果比单帧检测更稳。我在巡检应用里就是连续采三帧取置信度最高的那一帧的结果误检率明显下降。6.3 初始化失败与能力不可用初始化失败的原因主要有三类设备不支持、SDK 版本不匹配、权限未授予。排查时先看日志里有没有明确的错误码再逐项确认。如果是设备不支持那没办法只能降级如果是版本问题升级 SDK如果是权限问题检查动态申请的逻辑是否走到了。我遇到过一种情况是权限申请的回调里没有处理“永久拒绝”的分支导致用户以为授权了实际没有调用时直接失败。问题现象可能原因解决方向init 抛异常设备不支持降级或提示用户detect 返回空图像方向错误处理 EXIF 旋转OCR 结果乱序未排序按坐标排序置信度低光照差或距离远加拍照引导内存增长未释放 PixelMap手动 release6.4 多模块同时调用的资源竞争如果页面里同时有人脸检测和 OCR 在跑而且都放在后台线程可能会出现 native 层资源竞争表现为其中一个调用偶发失败。我的做法是给这两个能力加一个简单的互斥锁同一时间只允许一个能力在跑虽然牺牲了一点并行度但稳定性提升明显。对于大多数拍照后处理的场景这点并行度损失可以接受。let visionLock false; async function withVisionLockT(fn: () PromiseT): PromiseT { while (visionLock) { await new Promise(resolve setTimeout(resolve, 10)); } visionLock true; try { return await fn(); } finally { visionLock false; } }实操心得调试阶段建议把每一步的中间图像保存到应用沙箱里方便回看是哪一步出了问题。人脸检测的框可以画在图上OCR 的文本块也可以叠加显示这样一眼就能看出是检测偏了还是识别错了。这个习惯帮我省了大量猜测的时间。7. 能力扩展与后续演进方向7.1 从检测到识别的链路延伸人脸检测和 OCR 只是视觉能力的入口往后延伸还有很多可做的。比如人脸检测之后接活体检测判断是真人还是照片翻拍OCR 之后接自然语言处理把提取的文本做语义理解和字段归一化。Core Vision Kit 目前提供的是基础能力复杂的业务逻辑需要自己组合。我的建议是先把基础链路跑通跑稳再逐步叠加不要一上来就追求大而全。7.2 端云协同的取舍虽然 Core Vision Kit 是端侧能力但并不意味着所有场景都要纯端侧。对于识别精度要求极高、端侧模型搞不定的场景可以把端侧检测和识别作为初筛把不确定的结果送到云端做二次确认。这样既保留了端侧的隐私和速度优势又借助云端补足了精度。取舍的关键在于业务对延迟和隐私的容忍度没有标准答案得根据具体场景来定。7.3 版本升级的注意事项HarmonyOS 的视觉能力还在快速迭代API 签名和返回结构在不同版本之间可能有调整。升级 SDK 之前建议先把当前版本的调用逻辑和返回结构记录下来升级后做对比测试。我遇到过升级后boundingBox的字段名从left改成x的情况虽然改动不大但如果不注意编译能过、运行报错排查起来很费时间。锁定版本、做好回归测试是避免这类问题的有效手段。最后分享一个我在实际项目里养成的习惯每接入一个新能力先写一个最小的验证 Demo只做一件事把输入输出打日志。等这个 Demo 跑通了再往业务工程里集成。这样能把能力本身的问题和业务代码的问题隔离开排查效率高很多。视觉 AI 这类涉及图像和 native 调用的能力变量多、链路长隔离验证是最省时间的做法。
返回列表