HarmonyOS趣味相机实战第20篇:MetadataOutput人脸框归一化、旋转镜像与目标估算 HarmonyOS趣味相机实战第20篇MetadataOutput人脸框归一化、旋转镜像与目标估算摘要CameraKit 的 MetadataOutput 可以低成本提供人脸矩形适合驱动眼镜、猫耳和腮红等面部贴纸。但原始camera.Rect不能直接作为 ArkUI 坐标它可能以归一化值或 Profile 像素表示还要经过预览旋转、前置镜像和 Cover 裁剪。若任一环节错误人脸框看似有效贴纸却会左右颠倒或整体偏移。本文基于D:/APP/1quweixiangji趣味相机工程复盘CameraPreviewService.ets中 MetadataOutput 的能力探测、回调绑定、矩形标准化和目标估算。重点是如何把只有人脸框的高频元数据转换为统一CameraDetectedTarget再与低频骨骼结果融合同时保证不支持该能力的设备仍可正常预览和拍照。工程背景与源码定位文件作用本文关注点entry/src/main/ets/service/CameraPreviewService.etsMetadataOutput 创建、回调和映射主要实现entry/src/main/ets/service/CoreVisionHumanService.ets骨骼和分割识别低频补充来源entry/src/main/ets/service/HumanFittingService.ets面部和身体锚点解析消费统一目标entry/src/main/ets/model/DecorationModels.ets目标、关键点和来源类型数据协议entry/src/main/ets/pages/Index.ets316 x 390预览画布最终坐标空间entry/src/main/ets/service/CameraDeviceService.ets前后镜头选择镜像策略来源环境与数据边界项目当前值说明应用版本1.0.4当前工程target SDK6.0.2(22)构建配置场景模式NORMAL_PHOTOPhotoSessionMetadata 类型FACE_DETECTION设备支持时启用预览逻辑尺寸316 x 390统一输出坐标人脸新鲜期1800ms避免无限复用旧结果目标来源标记faceMetadata与骨骼来源区分一、MetadataOutput必须先做能力探测项目不会直接创建人脸输出而是检查能力列表privatestaticcreateFaceMetadataOutput(manager:camera.CameraManager,capability:camera.CameraOutputCapability):camera.MetadataOutput|null{constsupported:booleancapability.supportedMetadataObjectTypes.some((objectType:camera.MetadataObjectType){returnobjectTypecamera.MetadataObjectType.FACE_DETECTION;});if(!supported){returnnull;}try{returnmanager.createMetadataOutput([camera.MetadataObjectType.FACE_DETECTION]);}catch(error){hilog.warn(DOMAIN,TAG,create metadata output failed);returnnull;}}这里将 Metadata 定位为增强能力。返回 null 时基础 PreviewOutput 和 PhotoOutput 仍继续配置。二、只有业务需要识别时才创建startPreview()通过回调是否存在决定constshouldDetectTargets:booleantargetCallback!undefined;constmetadataOutput:camera.MetadataOutput|nullshouldDetectTargets?createFaceMetadataOutput(manager,capability):null;如果页面没有跟随贴纸或识别 UI就不增加 MetadataOutput也不启动 Surface 采样。按需创建能减少会话复杂度和设备压力。三、回调必须保存函数引用项目绑定privatestaticbindMetadataAvailable(metadataOutput:camera.MetadataOutput):void{constcallback:AsyncCallbackArraycamera.MetadataObject(error,metadataObjects):void{if(error){hilog.warn(DOMAIN,TAG,metadata available failed);CameraPreviewService.notifyTargets([]);return;}CameraPreviewService.notifyTargets(CameraPreviewService.toDetectedTargets(metadataObjects));};CameraPreviewService.metadataAvailableCallbackcallback;metadataOutput.on(metadataObjectsAvailable,callback);}停止时使用同一引用metadataOutput.off(metadataObjectsAvailable,metadataAvailableCallback);如果绑定和解绑各写一个匿名函数off()无法移除旧监听镜头多次重建后会收到重复回调。四、回调错误不应摧毁会话Metadata 回调失败时清空目标但不关闭相机if(error){notifyTargets([]);return;}这是正确的故障域。人脸识别暂时不可用预览、拍照、闪光灯和相册仍可工作。只有 Session、Input 或基础 Output 失败时才需要重建整个会话。五、camera.Rect可能存在两种量纲矩形字段可能表现为归一化坐标left/top/right/bottom 位于 0..1 Profile像素坐标字段基于预览宽高工程的标准化函数结合 Profile 尺寸判断。可以把一个坐标轴抽象为interfaceCameraNormalizedAxis{start:number;length:number;}输入为startValue与endValuefunctionnormalizeMetadataAxis(startValue:number,endValue:number,profileSize:number):CameraNormalizedAxis|null{if(!Number.isFinite(startValue)||!Number.isFinite(endValue)||endValuestartValue){returnnull;}constlooksNormalizedstartValue0endValue1.0001;if(looksNormalized){return{start:clamp(startValue,0,1),length:clamp(endValue-startValue,0,1)};}if(profileSize0){returnnull;}return{start:clamp(startValue/profileSize,0,1),length:clamp((endValue-startValue)/profileSize,0,1)};}不要只凭数值小于 1 就断言量纲。Profile 尺寸和 API 版本边界也应进入日志与测试。六、拒绝非法矩形比勉强渲染更安全标准化前至少检查所有字段为有限数值。right 大于 left。bottom 大于 top。宽高大于最小阈值。标准化后仍与[0,1]有交集。Profile 宽高在像素模式下有效。interfaceCameraNormalizedRect{left:number;top:number;width:number;height:number;}functionisUsableRect(rect:CameraNormalizedRect):boolean{returnrect.width0.002rect.height0.002rect.left1rect.top1rect.leftrect.width0rect.toprect.height0;}坏矩形应被过滤不要生成位于画布角落的假人脸。七、旋转归一化矩形预览可能为 90、180、270 度。矩形在[0,1]空间内旋转functionrotateNormalizedRectForPortrait(rect:CameraNormalizedRect,rotation:camera.ImageRotation):CameraNormalizedRect{if(rotationcamera.ImageRotation.ROTATION_90){return{left:1-rect.top-rect.height,top:rect.left,width:rect.height,height:rect.width};}if(rotationcamera.ImageRotation.ROTATION_180){return{left:1-rect.left-rect.width,top:1-rect.top-rect.height,width:rect.width,height:rect.height};}if(rotationcamera.ImageRotation.ROTATION_270){return{left:rect.top,top:1-rect.left-rect.width,width:rect.height,height:rect.width};}returnrect;}90/270 度时宽高交换这是最容易漏掉的细节。八、前置镜像在旋转之后处理if(activeCameraPositionfront){mapped{left:clamp(1-mapped.left-mapped.width,0,1),top:mapped.top,width:mapped.width,height:mapped.height};}镜像公式必须减去矩形宽度。只写left 1 - left会把矩形右边缘当作新左边缘整体向右偏一个框宽。九、Cover裁剪需要二次归一化相机 Profile 比例与316 x 390不一致时Preview 使用 Cover。工程根据旋转后的预览比例计算constsourceAspectrotatedPreviewAspect();consttargetAspectPREVIEW_FRAME_WIDTH/PREVIEW_FRAME_HEIGHT;当目标更宽constscaleYtargetAspect/sourceAspect;return{left:rect.left,top:clamp(rect.top*scaleY-(scaleY-1)/2,0,1),width:rect.width,height:clamp(rect.height*scaleY,0,1)};当源图更宽constscaleXsourceAspect/targetAspect;return{left:clamp(rect.left*scaleX-(scaleX-1)/2,0,1),top:rect.top,width:clamp(rect.width*scaleX,0,1),height:rect.height};这一步必须与 ArkUI 的实际ImageFit.Cover或 XComponent 容器策略一致。十、映射到预览逻辑坐标constfaceCenterX(rect.leftrect.width/2)*PREVIEW_FRAME_WIDTH;constfaceCenterY(rect.toprect.height/2)*PREVIEW_FRAME_HEIGHT;constfaceWidthrect.width*PREVIEW_FRAME_WIDTH;constfaceHeightrect.height*PREVIEW_FRAME_HEIGHT;输出统一使用316 x 390后续 HumanFittingService 不需要知道原始 Profile 尺寸。十一、仅有人脸框时估算人体目标人脸 Metadata 缺少肩、手和身体框。项目按比例扩展constportraitWidthclamp(faceWidth*2.35,86,218);constportraitHeightclamp(faceHeight*4.35,178,352);人体中心通常位于人脸中心下方constportraitCenterYclamp(faceCenterYportraitHeight*0.32,portraitHeight/2,PREVIEW_FRAME_HEIGHT-portraitHeight/2);这些系数是降级估算不是骨骼检测结果。目标必须标记fitSource:faceMetadata页面和算法可以据此采用更强平滑或限制身体贴纸精度。十二、生成估算关键点要降低置信度项目基于面部和人体框估算constkeypoints:BodyKeypoint[][keypoint(head,faceCenterX,Math.max(top,faceCenterY-faceHeight*0.72),0.62),keypoint(leftEye,faceCenterX-faceWidth*0.19,faceCenterY-faceHeight*0.1,0.78),keypoint(rightEye,faceCenterXfaceWidth*0.19,faceCenterY-faceHeight*0.1,0.78),keypoint(nose,faceCenterX,faceCenterYfaceHeight*0.12,0.72),keypoint(mouth,faceCenterX,faceCenterYfaceHeight*0.32,0.68),keypoint(leftShoulder,centerX-width*0.29,shoulderY,0.5),keypoint(rightShoulder,centerXwidth*0.29,shoulderY,0.5)];面部点比手、膝、踝的估算置信度高。调用方不能把这些点与真实骨骼结果等价处理。十三、轮廓是视觉降级而不是分割结果估算轮廓由一组比例点组成可以支持肩部和身体中心定位但不能用于精细抠图。应明确区分faceMetadata估算轮廓 - 几何占位 segmentationMask轮廓 - 像素级分割边界 poseKeypoints - 骨骼关键点不要把估算轮廓写入segmentation字段否则后续渲染可能误以为可以做前后景遮挡。十四、人脸结果按新鲜期缓存回调到来时记录lastFaceMetadataTargetstargets;lastFaceMetadataAtDate.now();使用前判断constfaceMetadataFreshlastFaceMetadataTargets.length0Date.now()-lastFaceMetadataAt1800;新鲜期可以桥接一次回调抖动但人物离开后不会无限保留。时间判断应使用单调时间源时更稳健当前Date.now()足以作为项目基线。十五、与骨骼结果融合而不是覆盖人脸 Metadata 提供更稳定的面部框CoreVision 骨骼提供肩、肘、腕等点。融合规则用人脸目标的ID、面部中心和面部关键点 用骨骼目标的身体关键点 置信度取更可靠来源 fitSource升级为poseKeypoints匹配前验证人脸中心是否落入人体框并结合矩形重叠和中心距离。多人场景宁愿不融合也不能把 A 的脸拼到 B 的身体上。十六、停止时严格解绑和释放if(metadataOutput!null){if(metadataAvailableCallback!null){metadataOutput.off(metadataObjectsAvailable,metadataAvailableCallback);}awaitmetadataOutput.stop();awaitmetadataOutput.release();}同时清空lastFaceMetadataTargets[];lastFaceMetadataAt0;metadataAvailableCallbacknull;否则新镜头启动后可能误用旧镜头的人脸框尤其前后镜像状态不同。十七、纯函数测试矩阵输入断言[0,1]矩形保持归一化Profile 像素矩形正确除以宽高right left返回 nullNaN/Infinity返回 nullrotation 90宽高交换且位置正确rotation 180两轴翻转rotation 270宽高交换且反向front 镜像left 1-left-width4:3 到竖屏 Cover裁剪补偿正确极小人脸框被最小阈值过滤或夹取示例it(mirrors normalized rect by its right edge,0,(){constrect{left:0.1,top:0.2,width:0.3,height:0.4};constmirroredmirrorRect(rect);expect(mirrored.left).assertEqual(0.6);expect(mirrored.width).assertEqual(0.3);});十八、真机验收后置镜头进入预览 - 人脸从中心移动到四角 - 检查框与贴纸同向 切换前置镜头 - 左右转头和挥手 - 检查镜像一致 双人进入画面并交叉 - 检查目标不重复、不串人 遮挡半张脸 - Metadata短暂丢失时平滑降级 退出页面再进入 - 不收到旧回调还要覆盖不支持FACE_DETECTION的设备确认基础预览和拍照不受影响。十九、常见问题排查现象可能原因排查方式前置人脸框左右偏镜像未减矩形宽度检查1-left-width90度预览框宽高反了旋转未交换 width/height测试四种旋转某设备框缩在左上角像素坐标被当归一化结合 Profile 尺寸判断人脸正确但身体贴纸偏估算人体比例不适配按距离和设备样本调参两人贴纸互换人脸骨骼匹配过宽增加包含和距离拒绝阈值切镜头后短暂反向旧 Metadata 缓存未清stop 时清空时间和目标不支持Metadata时黑屏把输出当强制能力null 时继续基础会话多次进入收到重复回调off 使用了不同函数引用保存原始 callback二十、上线前验收清单创建 MetadataOutput 前检查 FACE_DETECTION 能力。不支持时基础预览和拍照仍可用。回调引用被保存并用于off()。回调错误只降级识别不销毁会话。camera.Rect 同时兼容归一化与像素量纲。非法矩形在进入 UI 前被过滤。0/90/180/270 度映射通过测试。前置镜像在旋转后执行。Cover 裁剪与实际预览布局一致。输出统一到316 x 390坐标。人体估算结果明确标记faceMetadata。估算关键点使用分级置信度。人脸缓存有 1800ms 新鲜期。多人融合具有错误匹配拒绝条件。停止时解绑、停止、释放并清空缓存。总结MetadataOutput 的价值在于以较低成本提供高频人脸定位但它不是可以直接渲染的坐标。可靠链路需要先探测设备能力再把camera.Rect标准化依次处理旋转、前置镜像和 Cover 裁剪最后映射到统一预览坐标。只有人脸框时可以估算人体几何但必须保留来源和较低置信度有骨骼结果时再按空间关系融合。这样人脸元数据既能提升面部贴纸稳定性又不会成为不支持设备、多人场景和镜头切换中的单点故障。