1. 项目定位与整体思路:为什么"两步接入"是最务实的姿势
如果你最近在 HarmonyOS 7 上做视觉 AI 相关功能,八成绕不开两个再常见不过的诉求:让 App 认出画面里的人,再让 App 读出画面里的字。人脸检测和通用文字识别(OCR)可以说是端侧视觉 AI 落地最常用的两个入口,一个解决"谁在哪儿"的问题,一个解决"写了什么"的问题。我这次把两项能力合并在同一个工程里,走了一遍完整接入流程,期间踩了不少真机上的坑。做完才发现,乍一看是两套独立能力,真正梳理清楚之后,底层接入逻辑几乎同构——图像输入、引擎推理、结果解析,三步走完,这也是标题里"两步接入"的由来:人脸检测走一遍,OCR 再走一遍,每一遍都是同样的节奏。
这篇文章我会从需求拆解讲到环境准备,再到人脸检测和 OCR 的具体接入代码、参数调优、组合场景编排,最后把真机调试中遇到的典型问题整理成速查表。适合刚拿到 HarmonyOS NEXT SDK(API 12+,对应 5.0.0(12))准备做视觉功能的开发者,也适合已经在做但被人脸框、文字块、PixelMap 格式整得头大的朋友对照参考。
1.1 需求拆解:检测和识别其实是同一个处理链路
先说说我最初的需求:在一个 App 里同时做两件事——对摄像头预览画面里的人脸进行实时框选,并且对相册导入的图片进行 OCR 文字抽取。表面上看,这是两个完全不同的功能模块,分开找 SDK、分开接权限、分开写回调,似乎都很合理。但实际梳理之后发现,它们的处理链路高度一致:先准备好一张位图,把它转成系统图像框架支持的格式,然后交给对应的检测引擎,等引擎回调结构化结果,最后在业务层消费这些结果。
人脸检测输出的结果是检测框、关键点、姿态角,OCR 输出的结果是文本块、行、词以及各自的置信度。数据结构不同,但流程骨架完全一样。这个认识很重要,一旦你意识到这一点,工程结构就可以统一设计:图像获取层单独抽出来,检测调度层做成通用模板,只在引擎实例化和结果解析两个环节区分人脸与 OCR。这样写出来的代码不会出现两套风格割裂的实现,后续维护和扩展新检测能力时也能少改很多代码。
我见过不少项目,人脸检测用一套回调风格,OCR 又用另一套回调风格,一个页面里堆了三种异步处理范式,最后排查问题的时候简直是灾难。所以这篇文章里我会按"统一流程 + 差异化配置"的方式来组织代码,这也是我沉淀下来的一个经验。
1.2 方案选型:系统级视觉 AI 相比自研和三方平台的优势
在做技术选型时,其实有几条路可以走:完全自研模型、接入三方云服务、使用系统级视觉 AI 能力。先说自研,人脸检测和通用 OCR 都不是那种一两个轻量模型能搞定的场景,需要数据、训练、端侧模型转换、推理框架集成,还要处理不同设备的算子兼容性。对一个以业务功能为主的项目来说,这个投入产出比通常不太划算。除非你的场景非常垂直,比如只识别人脸特定部位、只识别特定版式的票据,才值得考虑自研轻量模型。
三方云服务是很多团队的选择,成熟稳定、精度高,像百度 OCR、腾讯 OCR,以及一些一体化 AI 视觉平台,确实在复杂场景下表现不错。但云服务有几个绕不开的问题:图片要出设备,涉及隐私合规和网络传输;按量付费,规模化之后成本不可控;弱网或离线环境下服务直接不可用。对需要端侧实时响应的功能,或者对人脸这类敏感生物信息,云方案并不是最优解。
我自己最终选了 HarmonyOS 7 的系统级视觉 AI 能力来做。它在 API 12+ 的 SDK 里已经以 Kit 的形式开放,端侧推理、数据不出设备,调用方式和系统相机、图像编解码组件配合天然顺畅,没有额外引入体积和授权成本。当然它也不是万能的,中文场景下对复杂版式、手写文字的识别效果和云服务还有差距。但以我实测的效果来看,印刷体、标准证件、常规环境下的中文文本识别已经够用,在速度和隐私安全上有明显优势。这里需要说明一下:每个版本对 Kit 的封装方式和接口名可能有细节差异,下文代码以我当前使用的 5.0.0(12) 版本为基准,你接入时请以官方最新接口文档为准,但整体流程是一致的。
1.3 环境准备与工程基线
动笔写代码之前,先把环境基线定清楚,可以少走很多弯路。DevEco Studio 我使用的是支持 HarmonyOS NEXT 的 5.x 版本,SDK 版本选择了 API 12 及以上,也就是 5.0.0(12)。目标设备是真机,因为人脸检测和 OCR 涉及相机权限和 NPU 调用,模拟器在很多情况下跑不出真实性能,影像输入输出链路也可能有差异。
工程里需要配置两处:一是module.json5里的权限声明,相机权限和读取媒体库权限都要提前配上,权限申请时机放到业务触发前,尽量用动态申请而不是一进 App 就弹窗;二是图像输入源的处理,从相机拿到的预览流、从相册选出的图片,最终都要统一转成 PixelMap 对象再喂给检测引擎。我建议在最开始就封装一个通用的"图像源转换工具",把 Bitmap、相机帧、文件路径统一转换成引擎能够消费的格式,后续接人脸和 OCR 时只需要调用同一个入口。
环境这块还有一个容易忽略的点:国产手机厂商的系统版本碎片化很常见,同一套接口在不同版本上的表现不完全一样,比如某些版本对人脸关键点的数量定义有差异。我的建议是,在代码里对返回结果做一层"字段防御",不要假设关键点一定存在,不要假设置信度一定有值,这样换机调试时不会因为空指针直接崩掉。
2. 第一步实战:人脸检测的接入、结果解读与调优
2.1 权限申请与图像源生产
人脸检测的第一步不是调用 API,而是拿到合法的图像源。如果走相机实时检测,需要在module.json5中声明ohos.permission.CAMERA,并在首次启动相机前完成动态授权。如果走相册选图,则涉及读取媒体库的权限,一般通过 PhotoAccessHelper 拉起系统相册选择器,选择结果返回一个图片 URI,再通过image.createImageSource解码成 PixelMap。
这里有第一个关键细节:检测引擎对 PixelMap 的内部格式有一定要求。我实测下来,最稳妥的方式是主动核对 PixelMap 的 pixel format,如果不是 RGBA_8888,可以先做一次格式转换再交给检测器。这个转换不是可选项,而是避坑项——有些场景下检测结果为空或者偶发崩溃,原因就是图像格式不匹配。你可以在拿到 PixelMap 后打印它的 pixelFormat,如果发现是 BGRA 或者其他格式,用ImageConverter或者重新绘制的方式转换。
import { image } from '@kit.ImageKit'; import { faceDetection } from '@kit.CoreVisionKit'; async function detectFaces(src: image.PixelMap) { // 创建人脸检测器,部分 API 版本支持直接静态调用检测方法 const detector = await faceDetection.getFaceDetector(); // 执行单帧检测 const result = await detector.detect(src); console.info(`检出人脸数量: ${result.faces.length}`); result.faces.forEach((face, index) => { const box = face.boundingBox; console.info(`第${index}张脸 -> x:${box.x} y:${box.y} w:${box.width} h:${box.height}`); }); // 用完记得释放,避免占用过多内存 detector.release(); return result; }上面是单帧检测的最简实现,适合先把流程跑通。第一次调用时,引擎会加载模型,耗时通常明显偏高,我建议在 App 启动后台默默做一次预热检测,或者至少把引擎初始化提前到空闲时机,不要让用户在点击"开始检测"之后干等。
2.2 从单帧到摄像头流:连续检测的节奏控制
单帧检测跑通之后,真正的难点在连续视频流检测。直接对每一帧都调用检测器,十个设备九个扛不住,你会发现设备发热、帧率下降、NPU 负载被打满,而且人脸检测算法对相邻帧的重复检测没有意义。正确的做法是控制检测频率,比如每 200ms 检测一次,或者每采集 5 帧检测一次。检测结果可以常驻给 UI 绘制层使用,这样画面上的框不会抖动太剧烈。
我选用的方案是:相机预览回调拿到 PixelMap 后,先判断时间戳距离上一次检测是否超过阈值,超过才进入检测逻辑;检测代码放在子线程,主线程只负责把结果转成 UI 层的坐标。需要注意预览流的像素格式转换,这个过程尽量复用同一个 PixelMap 对象和 Buffer,避免每帧都重新分配内存。实测下来,加入频率控制后,设备温度恢复正常水平,框选流畅度也明显提升。对实时性要求极高的场景,还可以结合视频帧的帧率做降采样,把图片缩小到 720P 再检测,速度和精度权衡下来性价比最高。
let lastDetectTime = 0; const DETECT_INTERVAL_MS = 200; async function onFrameArrived(pixelMap: image.PixelMap) { const now = Date.now(); if (now - lastDetectTime < DETECT_INTERVAL_MS) { return; } lastDetectTime = now; try { const result = await faceDetection.detect(pixelMap); // 将检测结果投递到 UI 线程进行绘制 emitFaceBoxes(result.faces); } catch (err) { console.error(`检测失败: ${JSON.stringify(err)}`); } }这里的核心思想很简单:吞吐量不够的时候,就用采样频率换稳定性。视觉 AI 在端侧的瓶颈从来不是算法,而是内存带宽和 NPU 调度,按需检测是绕不开的工程手段。
2.3 检测结果的字段解读与应用差异
拿到检测结果之后,很多人直接拿来画框,然后就完了。实际上人脸检测器返回的信息远不止一个矩形框。我常用的是四类信息:检测框 boundingBox、关键点 landmarks、姿态角 pose、置信度 confidence。它们各自有不同的应用价值。
| 结果字段 | 含义 | 典型使用场景 |
|---|---|---|
| boundingBox | 人脸在图像中的位置和大小 | 绘制检测框、裁剪头像、区域裁剪 |
| landmarks | 眼睛、鼻子、嘴巴等关键点坐标 | 贴纸、美颜锚点、活体检测辅助 |
| pose | 俯仰角、偏航角、翻滚角 | 判断正脸/侧脸、人像质量评估 |
| confidence | 检测结果的置信度 | 过滤误检、多结果排序 |
举个例子,如果你要做"人脸质量检测",只看 boundingBox 是不够的,还需要结合姿态角判断用户是否正对屏幕,结合置信度判断画面是否模糊。如果要做"头像裁剪",可以基于关键点计算眼睛位置,把头部区域居中后再裁。这些字段的存在,让人脸检测不只是一个画框工具,而是更上层业务的感知底座。
2.4 参数调优与人脸检测避坑心得
参数调优这块,我踩过的坑值得单独说几个。第一个是关于检测框抖动。单帧检测时人脸边缘的框在连续帧中会有毫米级的浮动,直接绘制出来会有呼吸感。解决办法是对检测框做平滑处理,比如使用简单的指数加权移动平均,或者规定一个最小移动阈值,小于阈值的位移直接忽略。第二个坑是多张人脸时的性能问题。默认参数下引擎会检测画面中的所有人脸,如果业务只需要最大的那张人脸,可以在拿到结果后按面积排序取最大值,同时通过参数关闭关键点输出或者其他不需要的特性,能省下不少耗时。
还有一个经验:不要在检测回调里做耗时操作。比如往数据库写日志、云同步、复杂的 JSON 序列化,这些都会阻塞线程池,导致后续帧排队,最终表现为卡顿。正确的做法是检测回调里只做轻量级的数据整理,重活全部丢到异步队列。
// 选面积最大的人脸,常用于单人人像场景 const largestFace = result.faces.sort((a, b) => { const areaA = a.boundingBox.width * a.boundingBox.height; const areaB = b.boundingBox.width * b.boundingBox.height; return areaB - areaA; })[0];3. 第二步入场:通用文字识别(OCR)的接入与工程化
3.1 系统 OCR 跟云 OCR 怎么选
OCR 这个领域,第三方方案已经非常成熟,从百度的通用文字识别到腾讯的 OCR,再到开源界的 Tesseract 和 PaddleOCR 系列,各有拥趸。在 HarmonyOS 上选型,我认为要分场景看。对需要联网、复杂版式、票据印章混合的识别场景,云 OCR 的泛化能力确实更强,很多接口直接帮你把字段结构化好了,比如身份证上的姓名、地址、身份证号,合同里的金额、日期、单位等,这对业务开发来说省事很多。但代价是图片必须上传到服务器,延迟受网络影响,费用会随调用量水涨船高。近年来 PaddleOCR 在端侧部署的呼声很高,对于有算法团队、愿意维护模型文件的项目来说也是不错的选择,但要自己解决模型体积和算子兼容问题。
代码层面我没有选择云端,而是继续用系统级 OCR。它的优势有两个:一是端侧推理,敏感图片完全不出设备;二是接入成本极低,不需要引入第三方 SDK,也不需要网络状态判断。劣势在复杂版式和手写体上比较明显,对严重倾斜、艺术字体、复杂背景的文本,识别率会下降。所以我的判断是:如果你做的是通用相册文字提取、文档扫描、界面截图识别这类常规场景,系统 OCR 完全够用;如果你做的是合同审查、身份证信息入库这种强调字段正确率的业务,可以优先评估云方案,或者用系统 OCR 做初筛、云 OCR 做复核的混合路线。
3.2 系统 OCR 的模型加载与识别流程
系统 OCR 的接入流程,和人脸检测几乎如出一辙:创建引擎、传入图像、解析结果。只是引擎的参数和数据结构不同。实际写代码时,我抽象了一个通用的识别入口,把引擎的创建参数放到了配置里,这样后续切换语言模型或者调整识别策略,不需要改动调用方。
import { textRecognition } from '@kit.CoreVisionKit'; import { image } from '@kit.ImageKit'; export interface OCRLineInfo { text: string; confidence: number; } async function recognizeTextFromImage(src: image.PixelMap): Promise<OCRLineInfo[]> { const engine = await textRecognition.createTextRecognizer({ // 使用中文简体模型,开启多语言辅助识别 language: 'zh-Hans', enableMultiLanguage: true, }); try { const result = await engine.recognizeText(src); const lines: OCRLineInfo[] = []; // 结果结构:文本块 -> 文本行 -> 词 for (const block of result.textBlocks) { for (const line of block.lines) { lines.push({ text: line.text, confidence: line.confidence }); } } return lines; } finally { engine.release(); } }这里我想强调引擎释放的问题。很多人在首次接入时没有养成"用完即释放"的习惯,在测试页面反复进入退出,结果内存持续增长。系统视觉引擎在端侧会常驻模型和中间缓存,不释放的话,最终会把应用内存顶到危险水位。所以我会在finally里保证释放,业务层也尽量做到页面销毁时主动回收。
3.3 中文、多语言与识别结果的后处理策略
说一个容易被忽视的点:语言参数zh-Hans不只是"中文开关",它直接影响模型加载和识别策略。如果不显式设置,默认语言可能以英文为主,中文内容识别出来的邪门程度会让你怀疑人生。我测试过一份中文菜单截图,默认参数下把"宫保鸡丁"识别成了一堆字母和数字的混合体,显式指定简体中文后才恢复正常的文本输出。
多语言场景也类似。如果你要识别日文、韩文内容,建议在支持的语言列表里明确选择对应语言模型。之前有人问我为什么一段韩文用普通 OCR 识别不了,多半就是语言模型没对上——这跟 PaddleX 里配置韩文识别是同一个道理,语言不是一个通用黑盒特征,而是需要针对性加载模型资源的。工程上的做法是:根据业务区域动态配置语言参数,不要用一套配置跑遍全球。
const languageMap: Record<string, string> = { 'zh': 'zh-Hans', 'zh-TW': 'zh-Hant', 'en': 'en', 'ja': 'ja', 'ko': 'ko', }; const language = languageMap[userLocale] ?? 'en';拿到文本行之后,后处理是决定业务体验的关键。直接把文本逐行拼接成一个大字符串,在很多场景下是不够的。我的习惯是保留坐标信息,把文本行按照从上到下、从左到右的顺序排序,然后再拼接,对版式还原至关重要。如果识别的是票据或卡片,还可以根据坐标做字段归类,比如把"姓名:"后面的文本行作为姓名值提取出来。这一步虽然属于业务逻辑,但直接影响 OCR 功能的实用度,值得专门抽个模块来做。
3.4 OCR 实战踩坑:清晰度、倾斜与复杂背景
OCR 最大的敌人不是算法,而是图像质量。我总结出三类最常见的问题和对应的预处理手段。第一类是低清晰度,解决方法是上传前做一次分辨率检查,宽度低于 1440px 的图片建议重新放大或用超分模型增强,至少在识别前把图片缩放到合理尺寸。第二类是倾斜,文本区域如果歪斜超过一定角度,识别率会明显下降。这时可以先用图像检测里的文本块坐标计算倾斜角度,再做仿射校正,然后再进入识别流程。第三类是复杂背景——深色背景上的浅色文字、背景有花纹的文字,我都实测过,预处理阶段先做对比度增强和二值化,效果通常有明显改观。
// 简单的对比度增强逻辑,适用浅色文字深背景的场景 async function improveContrast(src: image.PixelMap): Promise<image.PixelMap> { // 这里可以调用图像编辑接口调整对比度和亮度 // 也可以在 Canvas 上重新绘制,配合全局渲染参数实现 return src; }当然,预处理不是越多越好,过度处理反而会损伤文字边缘。我的建议是建立一个小型样本库,用 20-50 张真实业务图片反复测试不同预处理组合,选定一到两套参数固化下来,别每次上线都凭感觉调。
4. 两步组合实战:人脸与 OCR 合作能做什么
4.1 组合场景一:证件识别时的人脸预检
人脸检测和 OCR 放在一起能做的事很多,最常见的是证件识别。想象这样一个流程:用户上传身份证照片,系统先用人脸检测确认照片中确实包含人脸,并检测人脸姿态和清晰度是否符合要求,如果人脸角度过偏、置信度过低,直接让用户重新拍摄;人脸预检通过之后,再把图片交给 OCR,从识别结果里提取姓名、性别、身份证号等关键字段。
这个组合的价值在于把"文字识别"和"人像校验"两个动作编排成了一个完整的用户交互流程。既避免了用户传一张模糊或非人像图片进来浪费 OCR 调用,又能在识别字段之外做一次真实性粗校验。实际编码时,你只需要把两个步骤串成一个异步管道,前一步失败就中断后续操作,并给出针对性的提示文案。
async function verifyIdCard(pixelMap: image.PixelMap) { // 第一步:做人脸预检 const faceResult = await detectFaces(pixelMap); const face = faceResult.faces[0]; if (!face || face.confidence < 0.6) { throw new Error('未检测到清晰人脸,请重新拍摄'); } if (Math.abs(face.pose.yaw) > 15 || Math.abs(face.pose.pitch) > 15) { throw new Error('请正对镜头拍摄'); } // 第二步:进入 OCR 字段抽取 const lines = await recognizeTextFromImage(pixelMap); const idCardInfo = extractIdCardFields(lines); return idCardInfo; }4.2 组合场景二:相册场景里的人与文字双索引
另一个有意思的场景是智能相册。App 可以对用户相册里的照片做双重标注:人像照片打上"人物 A、人物 B"的标签,街拍或者截图照片提取文字内容建索引。这样在搜索时就既可以按"谁"来找照片,也可以按"拍了什么字"来找照片。对人脸聚类结果再做一次 OCR,照片的语义信息会丰富很多。
这个场景的工程难点是批量处理时的任务调度。相册里几百张照片不可能一次性全跑完,需要做一个队列,按时间切片处理,并且让用户感知进度。我的做法是第一批只处理用户最近 100 张照片,后续通过空闲时间窗口继续处理,避免一上来就占用大量系统资源导致 App 卡顿。任务状态记录到本地数据库,中断后可以断点续跑。
从性能角度看,批量预处理非常关键。原图直接检测既慢又费内存,我会先缩略到 1080P 宽度再进入检测管线,人脸检测和 OCR 各跑一遍,单张图片全流程能压到 400ms 以内。
4.3 组合编排与错误处理的最佳实践
两个能力组合之后,错误处理要比单能力更仔细。我的原则是做到"失败可归因":人脸检测失败和 OCR 识别失败要输出不同的错误码,在日志里能直接区分是哪一步出了问题。不要一个笼统的 catch 把所有异常吞掉,否则线上排查时你只能对着一个"识别失败"的提示发愁。
建议的错误处理结构是这样的:人脸检测阶段返回三个状态——通过、低置信度、无人脸;OCR 阶段返回两个状态——成功、低置信度。每种状态对应不同的用户提示文案,比如低置信度时可以引导用户调整光线,而不是简单说"请重试"。把这些状态定义成一个联合类型,放在公共模块,两个人脸和 OCR 的调用方都能引用,维护起来不会乱。
5. 性能与内存治理:视觉 AI 最容易翻车的地方
5.1 图像压缩与格式转换的策略
图像预处理直接影响检测的速度和稳定性。原图动辄 4000x3000 的分辨率,交给端侧模型推理,耗时和内存都会爆炸。我实测 1080P 宽度是精度和性能的黄金平衡点,低于这个分辨率,小字和远距离人脸开始丢失;高太多,推理时间翻倍但精度提升有限。对 OCR 场景,文字区域如果很小,1080P 仍可能不够,这时可以先用目标检测思路定位文字区域,再对该区域做局部放大识别,效果比整图放大要好得多。
格式转换这一层我上面已经提过,这里再补充一个细节:PixelMap 的格式转换和缩放最好在图像解码阶段一次性完成,不要在检测前临时转换。每多一次图像内存复制,对视频流场景来说都是可以感知的延迟。从相册选图时,可以用工具方法设置期望的输出尺寸和格式,直接让解码器输出目标格式。
5.2 异步并发与内存释放的生命周期管理
端侧 AI 的线程模型需要认真设计。我的建议是建立一个专门的"视觉任务线程池",人脸和 OCR 都提交到这个线程池执行,主线程只接收结果回调。这样可以避免 UI 线程卡顿,也便于统一控制并发数量。并发开的太大并不会有帮助,因为 NPU 和 CPU 的算力是有限的,同时跑三四个任务只会让他们互相抢占资源,最后整体变慢。我通常把并发数限制在 2 以内。
内存生命周期则要遵守"谁创建谁释放"的原则。引擎实例在页面创建时初始化,在页面销毁时释放;PixelMap 在提交给引擎后,如果业务不再需要展示原图,可以立刻释放;识别结果中的大对象,比如原始检测数据,在转换成业务模型后也应清理。用 LeakCanary 或 DevEco 自带的内存分析工具定期检查,能发现不少隐性泄漏。
5.3 真机实测:一组可以当参考线的数据
这里给一组我拿真机测出来的参考数据。设备是普通中端机型,屏幕 1080P,拍照后处理一张 1200 万像素照片:缩放到 1080P 后,人脸检测单帧耗时大约 50-80ms,占用内存上涨约 30-50MB;OCR 识别一张密集文字截图耗时大约 200-350ms,内存上涨约 50-80MB。首帧调用因为要加载模型,耗时是日常的 3-4 倍,之后会稳定下来。
这组数据不具普适性,但可以帮你建立心理预期。如果你的真机数据差距过大,先检查是不是忘了做图像缩放,再检查是不是同时跑了两个引擎,大概率问题出在这两处。
6. 常见问题排查与调试技巧实录
6.1 高频问题速查表
把这两步接入做完,我把过程中遇到频次最高的问题整理成了一个速查表,很多问题看起来是玄学,其实根因都很具体。
| 现象 | 根因 | 处理办法 |
|---|---|---|
| 人脸检测一直返回 0 张 | PixelMap 格式不兼容 | 转成 RGBA_8888 后重试 |
| OCR 中文输出乱码 | 未指定语言或语言参数错误 | 显式配置zh-Hans |
| 第一次调用卡顿明显 | 引擎模型冷加载 | 启动时预热或提前创建引擎 |
| 视频流检测发热严重 | 每帧都调用检测器 | 加检测间隔,限制到 3-5 帧一次 |
| 识别结果张冠李戴 | 文本行乱序 | 按坐标排序后再拼接 |
| 页面退出后内存不降 | 引擎和 PixelMap 未释放 | 在生命周期回调中统一释放 |
| OCR 识别不了韩文/日文 | 未加载对应语言模型 | 设置匹配语言的语言参数 |
| 相同图片两次结果不同 | 端侧引擎存在随机性 | 多次取众数或提高输入清晰度 |
6.2 调试三板斧:静态图、中间结果、日志埋点
最后分享我的调试方法。第一板斧,永远先用静态图调试。从系统相册选一张目标图片,走完"解码-转格式-检测-解析"全过程,确认每个环节的输出都符合预期,再去接相机流。相机流环节变量太多,光影、帧率、预览尺寸都会干扰判断,出问题很难定位。第二板斧,把小结果可视化出来。我习惯在调试页面把检测框、OCR 文本块直接绘制在图片上,这样一眼就能看出检测位置是否准确、漏检区域在哪里,比纯看日志高效得多。第三板斧,给每个重要节点打上时间戳日志。从图像进入检测器,到引擎返回原始结果,再到业务层拿到最终数据,每段耗时分开统计。性能问题出现时,你能立即判断瓶颈是在图像处理、模型推理还是业务逻辑。
人脸检测和 OCR 的组合,是整个端侧视觉 AI 能力里上手门槛最低、实用价值最高的两个入口。我个人做完这个项目的体会是:系统能力的边界比想象中大,但决定体验上限的往往不在 API 本身,而在图像预处理、频率控制、内存管理和错误归因这些工程细节。如果你准备做类似功能,务必先把静态图流程跑通,再去碰相机流;先保证结果稳定,再去追帧率。最后提醒一句,人脸信息属于敏感个人信息,功能上线前一定要做好用户告知和授权流程,界面展示时注意脱敏,这是从业者必须守住的底线。