
视觉 AI 能力在移动端的落地这两年最大的变化就是从云端往端侧迁移。以前做一个人脸检测或者 OCR 识别第一反应是调云端接口传图、等返回、解析 JSON链路长、有网络依赖、还涉及隐私合规问题。HarmonyOS 从 5.0 开始把 Core Vision Kit 这套端侧视觉能力逐步补齐到 HarmonyOS 7 这一代人脸检测和通用文字识别OCR已经能做到几行代码接入、完全本地推理的程度。我最近在一个实际项目里把这两个能力都跑通了从环境配置到参数调优踩了不少坑这篇文章就把整个接入过程拆开讲清楚。如果你正在做 HarmonyOS 应用需要做人脸相关功能比如打卡、活体前置检测、相册人像归类或者文字识别功能比如证件录入、票据扫描、文档数字化那 Core Vision Kit 基本是目前最省事的选择。它不需要你自带模型、不需要配推理框架、不需要处理图像预处理系统级 API 直接给你结果。下面我按先搞清楚它是什么、再动手接、最后调优避坑的顺序展开每一步都附上我实际验证过的代码和参数。1. 先搞清楚 Core Vision Kit 到底给了你什么很多人一上来就急着写代码结果卡在这个 API 到底返回什么结构坐标系是哪个原点这种问题上。我建议先把能力边界摸清楚后面接入会顺很多。1.1 人脸检测和 OCR 在端侧是怎么跑起来的Core Vision Kit 是 HarmonyOS 系统内置的视觉能力集合它把模型推理、图像预处理、后处理这些脏活都封装在系统层。你调用的时候传进去的是一张图片的 PixelMap 或者图片路径拿回来的是结构化结果——人脸就是一堆关键点坐标和置信度文字就是一行行的文本内容加包围框。这里有个关键认知它不是上传到某个服务再返回而是完全在设备本地完成的。模型文件随系统镜像预置你的应用只是调用方。这意味着三件事第一没有网络也能用第二图片不出设备隐私合规压力小很多第三首次调用会有一次模型加载开销大概几十到一百多毫秒之后就走缓存了。我实测下来一张 1080P 的图片做人脸检测端侧耗时在 30~80ms 区间取决于设备芯片OCR 因为要处理文字行分割和识别耗时会高一些一张 A4 文档大概 200~500ms。这个性能对于绝大多数交互场景是够用的但如果要做实时视频流逐帧检测就得考虑降采样或者跳帧策略了。1.2 两个能力的输入输出对照在动手之前先把两个能力的输入输出对齐一下避免接的时候来回翻文档。能力输入输出核心字段典型耗时人脸检测PixelMap / 图片 URI人脸框、106 个关键点、置信度、角度30~80ms通用文字识别PixelMap / 图片 URI文本块列表、每块的四点包围框、置信度200~500ms人脸检测返回的关键点数量是 106 点这个信息量其实挺大的——除了常规的五官轮廓还包括眉毛、眼睛细节、嘴唇内外轮廓。如果你只是要判断有没有人脸人脸在哪个位置用包围框就够了如果要做表情分析、活体检测的前置对齐那 106 点就派上用场了。OCR 这边要注意它返回的是文本块而不是单个字符。每个文本块是一行或者一段连续文字带一个四边形的包围框四个顶点坐标。这个设计对后续做版面分析很友好但如果你需要精确到字符级别的位置就得自己根据包围框和文本长度做估算。1.3 什么场景该用它什么场景别硬上不是所有视觉需求都适合 Core Vision Kit。我总结了几条判断标准适合人脸位置检测、人脸关键点、证件/票据/文档的文字提取、截图文字识别、相册图片文字检索。不适合人脸识别1:1 或 1:N 比对这需要额外的特征提取和比对库、手写体识别通用 OCR 对印刷体友好手写体准确率会掉、复杂版面还原表格结构、多栏排版需要自己后处理。提示Core Vision Kit 的人脸检测只做检测不做识别。也就是说它能告诉你图里有几张脸、每张脸在哪但没法告诉你这是谁。身份比对需要你自己接人脸特征提取能力这是两回事别混淆。2. 接入前的环境准备与依赖配置环境这块看着简单但 HarmonyOS 的 API 版本和 SDK 对应关系如果搞错编译期就会报一堆找不到符号的错。我踩过一次坑折腾了半小时才发现是 API 版本没对齐。2.1 API 版本与 SDK 的对应关系Core Vision Kit 的视觉能力在 API 12 及以上版本才比较完整。如果你用的是 HarmonyOS NEXT 的 SDK对应的是 5.0.0(12) 这个版本线。这里要特别注意API 版本号12和 SDK 版本号5.0.0是两套编号别看到 5.0.0 就以为是 API 5。在build-profile.json5里compileSdkVersion和compatibleSdkVersion都要设到 12 或以上。我建议compatibleSdkVersion不要设太低否则你调用的新 API 在低版本设备上会直接崩而且这种崩溃在开发机上测不出来只有真机低版本才会暴露。{ app: { products: [ { name: default, compileSdkVersion: 12, compatibleSdkVersion: 12, targetSdkVersion: 12 } ] } }2.2 权限声明哪些是必须的哪些容易漏视觉能力本身处理的是你传进去的图片如果图片来自相册或者相机那权限是相册/相机那边的不是 Core Vision Kit 要的。但有一个容易漏的点如果你从文件路径读取图片需要文件读取权限。{ module: { requestPermissions: [ { name: ohos.permission.READ_IMAGEVIDEO, reason: $string:read_image_reason, usedScene: { abilities: [EntryAbility], when: inuse } } ] } }reason字段是必填的而且必须是字符串资源引用不能直接写中文。我第一次直接写了个中文字符串编译过了但安装时被拒报的是权限声明格式错误。这个坑很隐蔽因为编译期不报错。2.3 图片输入的三种形态与选择Core Vision Kit 接受三种图片输入形态选哪种直接影响你的代码复杂度PixelMap最灵活适合你已经拿到解码后的位图或者需要对图片做预处理旋转、裁剪、缩放后再送检。图片 URI最省事直接传file://开头的路径系统内部帮你解码。ArrayBuffer适合图片数据来自网络流或者内存缓冲的场景。我个人的选择习惯是如果图片需要预处理一律先转 PixelMap如果只是原图直接识别用 URI 最省心。因为 URI 方式系统内部会做一次解码省了你手动createImageSource的代码而且解码参数由系统优化过性能反而更稳。3. 人脸检测接入从拿到 PixelMap 到解析 106 个关键点人脸检测这块代码量不大但坐标系和关键点索引是最容易出错的地方。我把完整流程拆成四步。3.1 初始化检测器与配置参数先拿到检测器实例。Core Vision Kit 的 API 设计是配置 检测分离的配置项通过一个 Config 对象传入。import { faceDetector } from kit.CoreVisionKit; import { image } from kit.ImageKit; async function initFaceDetector(): PromisefaceDetector.FaceDetector { // 创建检测器指定检测模式 const detector await faceDetector.createFaceDetector(); return detector; }配置里最关键的几个参数检测模式单脸模式 vs 多脸模式。单脸模式速度快适合打卡、自拍这类场景多脸模式会遍历全图适合合影、人群统计。关键点开关如果你只要人脸框把关键点关掉能省一点耗时。最小人脸尺寸这个参数决定了多小的脸会被忽略。设太小会引入大量误检设太大会漏掉远处的脸。我实测的经验值是最小人脸尺寸设成图片短边的 5%~10% 比较合理。比如 1080P 图片短边 1080最小脸设 54~108 像素。低于这个值误检率会明显上升。3.2 图片预处理旋转和缩放为什么不能省这里是我踩过最大的坑。手机拍的照片EXIF 里带旋转信息如果你直接解码成 PixelMap 送检检测器看到的是未旋转的原始像素结果就是人脸框位置全错或者干脆检测不到。正确做法是解码时就把旋转应用上import { image } from kit.ImageKit; async function loadAndFixOrientation(uri: string): Promiseimage.PixelMap { const imageSource image.createImageSource(uri); // 读取 EXIF 方向信息 const imageInfo await imageSource.getImageInfo(); const decodingOptions: image.DecodingOptions { desiredPixelFormat: image.PixelMapFormat.RGBA_8888, // 关键让解码器自动应用 EXIF 旋转 rotate: imageInfo.orientation }; const pixelMap await imageSource.createPixelMap(decodingOptions); return pixelMap; }rotate这个参数一定要传不传的话后面所有坐标都是错的。我一开始没传测试图是横着拍的结果检测框画出来是歪的排查了半天才定位到 EXIF。另一个预处理是缩放。如果原图是 4000x3000 这种大图直接送检会很慢。我的做法是如果图片长边超过 2000 像素先等比缩放到长边 2000 再送检。检测精度损失很小但速度能快一倍以上。缩放用pixelMap.scale()就行。3.3 调用检测与结果结构解析配置好之后调用检测async function detectFaces(pixelMap: image.PixelMap) { const detector await faceDetector.createFaceDetector(); const visionInfo: faceDetector.VisionInfo { pixelMap: pixelMap }; const faces await detector.detect(visionInfo); return faces; }返回的faces是一个数组每个元素包含boundingBox人脸包围框{ left, top, right, bottom }坐标原点是图片左上角。landmarks106 个关键点数组每个点是{ x, y }。confidence置信度0~1 之间。rotationAngle人脸在图片中的旋转角度。这里有个细节包围框的坐标是相对于你传入的 PixelMap 的。如果你在送检前做了缩放那拿到的坐标是缩放后的坐标系要映射回原图得乘上缩放比例。我建议在代码里维护一个scaleRatio变量检测完统一做一次坐标映射别在多个地方零散地乘。3.4 106 个关键点的索引含义与常用点位106 点这个数量官方文档给了一张索引图但实际用的时候经常要查。我把最常用的几个点位索引列出来方便你直接抄部位索引范围说明左眼中心66~71左眼轮廓点右眼中心75~80右眼轮廓点鼻尖85单点左嘴角90单点右嘴角96单点下巴16单点如果你要做人脸对齐比如把人脸旋转到正脸用左右眼中心连线算角度就够了。具体做法是取左眼中心点和右眼中心点算两点连线和水平线的夹角然后反向旋转图片。这个角度和rotationAngle字段基本一致但自己算更可控。注意关键点索引在不同版本 SDK 里可能有微调接入前务必对照当前版本的官方文档确认一遍。我遇到过升级 SDK 后索引偏移的情况虽然不常见但一旦发生就是全盘错位。4. 通用文字识别接入文本块、包围框与置信度过滤OCR 这块比人脸检测复杂一些因为返回的是文本块列表后续怎么用取决于你的业务。我按识别—过滤—后处理三步来讲。4.1 初始化 OCR 引擎与识别模式选择OCR 引擎的创建和人脸检测类似import { textRecognition } from kit.CoreVisionKit; async function initTextRecognizer(): PromisetextRecognition.TextRecognizer { const recognizer await textRecognition.createTextRecognizer(); return recognizer; }OCR 有一个重要的模式选择通用模式 vs 文档模式。通用模式适合自然场景文字路牌、菜单、商品包装文档模式适合印刷文档、票据、证件。两者的模型不同识别策略也不同。我实测的结论是文档类图片一定要用文档模式通用模式在文档上会把行间距识别错导致文本块合并或断裂。反过来自然场景用文档模式准确率也会掉。选对模式比调任何参数都管用。4.2 识别调用与文本块结构async function recognizeText(pixelMap: image.PixelMap) { const recognizer await textRecognition.createTextRecognizer(); const visionInfo: textRecognition.VisionInfo { pixelMap: pixelMap }; const result await recognizer.recognizeText(visionInfo); return result; }返回的result里核心是textBlocks数组。每个文本块包含value识别出的文本字符串。boundingBox四点包围框{ points: [{x,y}, {x,y}, {x,y}, {x,y}] }顺序是左上、右上、右下、左下。confidence置信度。这里要注意文本块的顺序不一定是阅读顺序。系统返回的顺序可能是按检测到的先后不保证从上到下、从左到右。如果你要做版面还原得自己按包围框的 y 坐标排序同一行的再按 x 排序。4.3 置信度过滤与低质量结果处理OCR 一定会返回一些低置信度的垃圾结果尤其是图片有噪点、反光、模糊的时候。我的做法是设一个置信度阈值低于阈值的直接丢弃。阈值设多少我实测下来0.5 是个比较稳的起点。低于 0.5 的结果基本是噪声高于 0.8 的基本可信。0.5~0.8 之间的需要结合业务判断——如果是证件号、金额这种关键字段宁可漏也别错阈值可以提到 0.7如果是全文检索阈值可以降到 0.4多召回一些。const CONFIDENCE_THRESHOLD 0.5; const validBlocks result.textBlocks.filter( block block.confidence CONFIDENCE_THRESHOLD );除了置信度还有一个过滤维度是文本块面积。太小的文本块比如几个像素的噪点被识别成字符直接丢掉。我一般设一个最小面积阈值比如包围框面积小于图片面积的 0.01% 就丢弃。4.4 后处理按行合并与阅读顺序还原原始返回的文本块是散乱的要变成可读的文本需要做行合并。思路是按包围框的 y 中心坐标排序。y 中心接近的差值小于行高的一半归为同一行。同一行内按 x 坐标从左到右排序。行与行之间按 y 从上到下拼接。function sortBlocksByReadingOrder(blocks: textRecognition.TextBlock[]): textRecognition.TextBlock[] { // 先按 y 中心排序 const sorted [...blocks].sort((a, b) { const ay (a.boundingBox.points[0].y a.boundingBox.points[2].y) / 2; const by (b.boundingBox.points[0].y b.boundingBox.points[2].y) / 2; return ay - by; }); // 再对同一行的按 x 排序 // ... 行分组逻辑 return sorted; }这段逻辑看着简单但实际做的时候行高估算是个麻烦事。我的经验是用所有文本块高度的中位数作为行高参考比用平均值稳因为偶尔会有特别高或特别矮的块拉偏平均值。5. 两个能力协同使用的实战场景单独用人脸检测或单独用 OCR 都不难真正有价值的是两者结合。我举两个我实际做过的场景。5.1 证件录入人脸 文字的组合校验做证件录入的时候一个常见需求是既要提取证件上的文字信息又要确认证件上的人脸照片区域。这时候两个能力可以并行调用async function processIdCard(pixelMap: image.PixelMap) { const [faces, textResult] await Promise.all([ detectFaces(pixelMap), recognizeText(pixelMap) ]); // faces 里应该有一张人脸证件照 // textResult 里提取姓名、证件号等字段 return { faces, textResult }; }并行调用能省时间因为两个能力互不依赖。但要注意并行调用会同时占用推理资源在低端设备上可能触发资源竞争导致其中一个变慢。如果设备性能一般建议串行调用先做人脸再做 OCR。组合校验的价值在于如果 OCR 提取到了证件号但人脸检测没检测到人脸那这张图很可能是翻拍或者伪造的可以作为一个风控信号。5.2 文档扫描先检测文字区域再裁剪增强做文档扫描的时候一个痛点是图片里有大量背景直接 OCR 会引入噪声。我的做法是先用 OCR 拿到所有文本块的包围框算出所有框的并集也就是文字区域的外接矩形然后裁剪出这个区域再做一次精细 OCR。这个两遍 OCR的策略第一遍用低分辨率快速定位文字区域第二遍用高分辨率精细识别。实测下来比直接对全图做高分辨率 OCR 又快又准。6. 性能调优与踩坑实录这部分是我最想分享的因为文档里不会写这些。6.1 首次调用慢模型加载的预热策略前面提过首次调用有模型加载开销。如果你的应用是用户点一下才触发识别那第一次体验会很差——用户等了一百多毫秒才出结果。我的做法是在页面 onPageShow 的时候做一次预热拿一张极小的空白图比如 10x10 像素跑一次检测把模型加载到内存。这样用户真正操作的时候模型已经在内存里了响应就是纯推理时间。// 页面显示时预热 onPageShow() { const warmupPixelMap createTinyPixelMap(); // 10x10 空白图 detectFaces(warmupPixelMap).catch(() {}); }预热用的图要足够小否则预热本身就慢。10x10 足够了检测器不在乎图里有没有脸它只是要完成一次完整的加载流程。6.2 内存管理PixelMap 用完必须释放PixelMap 是占内存的大户一张 1080P 的 RGBA_8888 位图就是 8MB 左右。如果你连续处理多张图不释放内存会迅速涨上去在低端设备上直接 OOM。每处理完一张图一定要调用pixelMap.release()。我建议用 try-finally 包起来确保异常路径也能释放async function safeProcess(uri: string) { let pixelMap: image.PixelMap | null null; try { pixelMap await loadAndFixOrientation(uri); return await detectFaces(pixelMap); } finally { if (pixelMap) { await pixelMap.release(); } } }这个坑我踩得很惨一个批量处理相册的功能处理到第 20 张图就崩了排查发现是 PixelMap 没释放。6.3 常见问题排查对照表我把接入过程中遇到的问题整理成表方便你对照排查现象可能原因排查方向检测不到人脸图片未应用 EXIF 旋转检查解码时是否传了 rotate人脸框位置偏移送检前做了缩放但坐标未映射检查 scaleRatio 是否应用OCR 结果乱序未做阅读顺序排序按 y 中心 x 坐标排序识别准确率低模式选错通用 vs 文档根据图片类型切换模式首次调用卡顿模型未预热页面加载时做预热内存持续增长PixelMap 未释放检查 release 调用低版本设备崩溃compatibleSdkVersion 设太低提高最低兼容版本6.4 关于识别准确率的几个现实预期最后说点实在的。Core Vision Kit 的 OCR 在印刷体、清晰图片上的准确率很高我实测中文文档能到 95% 以上。但有几个场景准确率会明显下降手写体通用 OCR 对手写体支持有限尤其是连笔字准确率可能掉到 60% 以下。艺术字体花体、变形字体识别率低。低光照/反光图片质量差识别率断崖式下跌。竖排文字部分场景下竖排识别会错乱。如果你的业务涉及这些场景别指望一个通用 OCR 搞定要么做图像增强预处理要么考虑专门的模型。Core Vision Kit 的定位是通用能力不是万能能力认清边界能省很多无用功。人脸检测这边正脸、清晰、光照正常的图片检测率接近 100%。侧脸超过 45 度、遮挡严重、极小的人脸检测率会下降。如果你的场景是侧脸或者大角度建议在业务层做多帧检测取最优而不是指望单帧搞定。整体接完这两个能力我的感受是 HarmonyOS 这套端侧视觉 API 的完成度已经相当高了接入成本比自建推理管线低一个数量级。真正花时间的不是写代码而是搞清楚坐标系、模式选择、内存管理这些文档里一笔带过但实际很要命的细节。把上面这些坑避开基本能一次跑通。