HarmonyOS趣味相机实战第15篇:人体贴纸坐标映射、旋转镜像与Cover裁剪对齐

📅 2026/7/18 17:26:34
HarmonyOS趣味相机实战第15篇:人体贴纸坐标映射、旋转镜像与Cover裁剪对齐
HarmonyOS趣味相机实战第15篇人体贴纸坐标映射、旋转镜像与Cover裁剪对齐摘要人体识别返回了正确关键点贴纸仍可能偏移、左右颠倒或在横竖屏切换后飞出画布。原因通常不在识别模型而在坐标链路相机源图尺寸、传感器旋转、前置镜像、预览 Surface 的 Cover 裁剪和 ArkUI 图层左上角坐标属于不同空间。本文基于D:/APP/1quweixiangji趣味相机工程复盘CoreVisionHumanService.ets与HumanFittingService.ets。我们把坐标处理拆成“源图校正、显示归一化、Cover 映射、语义锚点、尺寸估算、边缘夹取”六步并补充可自动执行的几何测试。该方法适用于眼镜、猫耳、翅膀、手持道具等需要跟随人体的装饰层。工程背景与源码定位文件作用本文关注点entry/src/main/ets/service/CoreVisionHumanService.ets骨骼检测、人体分割、目标标准化源图旋转、镜像与预览映射entry/src/main/ets/service/HumanFittingService.ets锚点解析、位置和缩放计算关键点优先与多级降级entry/src/main/ets/service/CameraPreviewService.ets相机会话、预览方向、目标融合提供旋转、镜头位置与目标数据entry/src/main/ets/model/DecorationModels.ets关键点、目标、锚点和素材类型坐标协议entry/src/main/ets/service/DecorationLibrary.ets声明素材锚点和默认缩放拟合算法输入entry/src/main/ets/pages/Index.ets316 x 390预览工作台最终 ArkUI 图层空间环境与坐标边界项目当前值说明应用版本1.0.4来自AppScope/app.json5target SDK6.0.2(22)工程构建配置应用模型Stage 模型UIAbility ArkUI预览逻辑宽度316拟合服务常量预览逻辑高度390拟合服务常量相机方向0/90/180/270camera.ImageRotation前置镜头水平镜像必须在旋转后按显示宽度处理预览填充Cover可能裁掉上下或左右区域一、先明确六个坐标空间完整链路中至少存在六套坐标相机传感器坐标 - PixelMap 源图坐标 - 旋转后的显示坐标 - 前置镜像坐标 - Cover 裁剪后的预览坐标 - 贴纸图层左上角坐标识别 API 返回的点通常以输入 PixelMap 为基准而 ArkUI 的Position使用页面逻辑像素。直接执行下面的代码必然存在设备差异layer.xkeypoint.x;layer.ykeypoint.y;除此之外关键点代表贴纸中心锚点图层定位通常代表左上角所以还要减去可视尺寸的一半。二、旋转必须先于镜像项目把源点转换到显示方向privatestaticsourcePointToDisplay(sourceX:number,sourceY:number,sourceSize:SourceSize):DisplayPoint{letdisplayX:numbersourceX;letdisplayY:numbersourceY;if(sourceSize.rotationcamera.ImageRotation.ROTATION_90){displayXsourceSize.height-sourceY;displayYsourceX;}elseif(sourceSize.rotationcamera.ImageRotation.ROTATION_180){displayXsourceSize.width-sourceX;displayYsourceSize.height-sourceY;}elseif(sourceSize.rotationcamera.ImageRotation.ROTATION_270){displayXsourceY;displayYsourceSize.width-sourceX;}constdisplaySize:DisplaySizethis.displaySize(sourceSize);if(sourceSize.mirrorX){displayXdisplaySize.width-displayX;}return{x:this.clamp(displayX,0,displaySize.width),y:this.clamp(displayY,0,displaySize.height)};}为什么顺序不能反旋转 90 度后显示宽度等于源图高度。如果先用源图宽度做水平镜像再旋转镜像轴会落在错误维度上。对应显示尺寸privatestaticdisplaySize(sourceSize:SourceSize):DisplaySize{if(sourceSize.rotationcamera.ImageRotation.ROTATION_90||sourceSize.rotationcamera.ImageRotation.ROTATION_270){return{width:sourceSize.height,height:sourceSize.width};}return{width:sourceSize.width,height:sourceSize.height};}三、Cover 不是等比缩放那么简单预览框是316 x 390。如果显示图像宽高比与预览不一致Cover 会放大并裁掉一部分内容。constsourceAspect:numberdisplayWidth/displayHeight;consttargetAspect:number316/390;当目标更宽时图像按宽度填充垂直方向被裁剪if(targetAspectsourceAspect){constscaleY:numbertargetAspect/sourceAspect;normalizedYnormalizedY*scaleY-(scaleY-1)/2;}当源图更宽时图像按高度填充水平方向被裁剪elseif(targetAspectsourceAspect){constscaleX:numbersourceAspect/targetAspect;normalizedXnormalizedX*scaleX-(scaleX-1)/2;}最后映射到预览return{x:clamp(normalizedX*316,0,316),y:clamp(normalizedY*390,0,390),confidence};这里的减法(scale - 1) / 2表示居中裁剪。如果 UI 使用顶部对齐或自定义偏移公式必须同步调整。四、矩形映射也要转换两个角人体框不能只缩放宽高旋转后宽高会交换。稳定做法是转换左上角和右下角privatestaticrectToPreview(left:number,top:number,width:number,height:number,sourceSize:SourceSize):NormalizedRect{consttopLeftpointToPreview(left,top,sourceSize);constbottomRightpointToPreview(leftwidth,topheight,sourceSize);return{left:Math.min(topLeft.x,bottomRight.x),top:Math.min(topLeft.y,bottomRight.y),width:Math.abs(bottomRight.x-topLeft.x),height:Math.abs(bottomRight.y-topLeft.y)};}对于 90 度旋转严格的矩形变换还应转换四个角再取包围盒因为原左上角不一定对应显示后的左上角。当前输入为轴对齐人体框时可以进一步增强functionboundsOf(points:BodyContourPoint[]):NormalizedRect{constxspoints.map(pointpoint.x);constyspoints.map(pointpoint.y);constleftMath.min(...xs);consttopMath.min(...ys);return{left,top,width:Math.max(...xs)-left,height:Math.max(...ys)-top};}五、锚点解析使用“可靠数据优先”素材只声明AnchorType拟合服务决定使用哪一份几何数据privatestaticanchorCenter(anchor:AnchorType,target:DetectedTarget):BodyKeypoint|null{switch(anchor){caseheadTop:returnthis.headTopPoint(target);caseeyes:returnthis.eyesCenter(target);casecheeks:returnthis.cheekCenter(target);casenoseMouth:returnthis.noseMouthCenter(target);caseneck:returnthis.neckPoint(target);caseshoulder:returnthis.averageKeypointPair(target,leftShoulder,rightShoulder,leftShoulder)??this.outlineCenterAtRatio(target,0.4,leftShoulder);casebodyCenter:returnthis.bodyCenter(target);casehand:returnthis.handPoint(target)??this.outlineSidePointAtRatio(target,0.64,rightHand);default:returnnull;}}这条链路体现了稳定性优先级真实关键点 - 面部元数据 - 人体分割轮廓 - 人体矩形框比例估算 - 默认位置某一路数据暂时缺失时贴纸不会瞬间消失而是降低拟合精度。六、双眼中心比单眼更稳定眼镜的中心点应取双眼平均值并用较低置信度作为结果置信度privatestaticaverageKeypointPair(target:DetectedTarget,leftName:BodyKeypointName,rightName:BodyKeypointName,resultName:BodyKeypointName):BodyKeypoint|null{constleftthis.findKeypoint(target,[leftName]);constrightthis.findKeypoint(target,[rightName]);if(leftnull||rightnull){returnleft??right;}return{name:resultName,x:(left.xright.x)/2,y:(left.yright.y)/2,confidence:Math.min(left.confidence,right.confidence)};}取最小置信度比平均值更保守。如果一只眼被遮挡结果不应该表现得和双眼清晰时一样可靠。七、关键点查找要设置置信度门槛项目的拟合精度分为四类exporttypeHumanFitPrecisionsegmentation|pose|estimatedPose|box;判断逻辑staticprecision(target:DetectedTarget|null):HumanFitPrecision{if(targetnull){returnbox;}if(target.fitSourceposeKeypointsthis.hasReliableKeypoints(target)){returnpose;}if(this.hasReliableKeypoints(target)){returnestimatedPose;}if(target.fitSourcesegmentationMasktarget.segmentationtarget.segmentation.outline.length0){returnsegmentation;}returnbox;}页面可以根据精度决定是否显示“已精准贴合”但不要把内部算法术语直接展示给普通用户。更重要的是算法能根据精度调整平滑力度高精度关键点响应更快估算框变化需要更强滤波。八、贴纸缩放应该绑定身体特征不同锚点不能只用统一的人体框宽度。项目按语义选择基准staticscaleFor(item:DecorationItem,target:DetectedTarget,textLike:boolean):number{constfaceWidththis.faceWidth(target);constbodyWidththis.bodyWidth(target);constshoulderWidththis.shoulderWidth(target,bodyWidth);letdesiredSizeMath.max(34,faceWidth*1.35);switch(item.anchor){caseheadTop:desiredSizefaceWidth*1.46;break;caseeyes:casenoseMouth:casecheeks:desiredSizefaceWidth*1.2;break;caseneck:desiredSizeMath.max(faceWidth*0.72,shoulderWidth*0.5);break;caseshoulder:desiredSizeMath.max(target.width*0.58,shoulderWidth*1.16);break;casebodyCenter:desiredSizebodyWidth*0.38;break;casehand:desiredSizeMath.max(faceWidth*0.64,bodyWidth*0.28);break;}constbaseSizetextLike?82:64;returnclamp(desiredSize/baseSize,0.42,1.65);}0.42和1.65是安全上下界防止识别框抖动导致贴纸瞬间极小或铺满全屏。这些参数应通过多距离真机样本回归不应只在单张图片上调试。九、中心点要转换为左上角图层横坐标staticlayerX(anchor:AnchorType,target:DetectedTarget,visualSize:number,defaultX:number):number{if(anchoraround||anchorfullFrame){returndefaultX;}constcenterthis.anchorCenter(anchor,target);if(center!null){returnthis.clampLayerX(center.x-visualSize/2,visualSize);}returnthis.fallbackLayerX(anchor,target,visualSize);}头顶贴纸的纵向中心需要额外上移constyanchorheadTop?center.y-visualSize*0.58:center.y-visualSize/2;这里使用0.58而不是0.5是为了让帽子和耳朵更多位于头部上方。眼镜、腮红等仍使用中心对齐。十、边缘夹取要考虑贴纸尺寸只把x限制在[0, 316]并不够因为贴纸有宽度privatestaticclampLayerX(value:number,visualSize:number):number{returnclamp(Math.round(value),8,PREVIEW_FRAME_WIDTH-visualSize-8);}privatestaticclampLayerY(value:number,visualSize:number):number{returnclamp(Math.round(value),12,PREVIEW_FRAME_HEIGHT-visualSize-12);}左右保留 8、上下保留 12 的安全边距既避免内容被裁掉也给选中框和手势留出空间。需要防守一个特殊情况当visualSize大于画布尺寸时上界小于下界。生产实现可先限制尺寸constsafeSizeMath.min(visualSize,PREVIEW_FRAME_WIDTH-16,PREVIEW_FRAME_HEIGHT-24);十一、识别框降级仍要保持语义只有人体框时项目按比例估算consttoptarget.centerY-target.height/2;switch(anchor){caseneck:anchorCenterYtoptarget.height*0.38;break;caseshoulder:anchorCenterYtoptarget.height*0.45;break;casebodyCenter:anchorCenterYtoptarget.height*0.62;break;casehand:anchorCenterYtoptarget.height*0.68;break;}降级不意味着把所有素材放到人体框中心。即使精度下降也要保留“头、肩、手、身体”的相对语义用户感知会稳定很多。十二、多人场景要给目标分配稳定 ID如果每帧都按数组下标生成person_0两个人交叉时贴纸会交换。更稳定的关联策略使用矩形交并比。中心点距离。人体框面积变化。面部中心是否落在人体框内。上一帧目标 ID。可定义匹配分数constdistanceScore1-Math.sqrt(dx*dxdy*dy);constscoreoverlapAreadistanceScore*20000(containsFace?30000:0);匹配后应设置最大距离阈值防止把画面另一侧的新人物误接到旧 ID。十三、为抖动增加时间滤波识别坐标逐帧变化直接更新图层会轻微振动。可以对同一目标和锚点做指数平滑interfaceSmoothedPoint{x:number;y:number;}functionsmoothPoint(previous:SmoothedPoint,current:SmoothedPoint,alpha:number):SmoothedPoint{return{x:previous.x(current.x-previous.x)*alpha,y:previous.y(current.y-previous.y)*alpha};}推荐按精度选择alpha精度建议 alpha表现pose0.55响应快estimatedPose0.38适度平滑segmentation0.28抑制轮廓抖动box0.22以稳定为主目标消失时不要无限保留旧坐标可设置 300 至 600 毫秒短暂保持超过时隐藏跟随型贴纸。十四、纯函数测试覆盖四个方向坐标算法最适合自动测试。以1000 x 750源图为例至少覆盖describe(sourcePointToDisplay,(){it(maps rotation 90,(){constpsourcePointToDisplay(100,200,{width:1000,height:750,rotation:camera.ImageRotation.ROTATION_90,mirrorX:false});expect(p.x).assertEqual(550);expect(p.y).assertEqual(100);});it(mirrors after rotation,(){constpsourcePointToDisplay(100,200,{width:1000,height:750,rotation:camera.ImageRotation.ROTATION_90,mirrorX:true});expect(p.x).assertEqual(200);expect(p.y).assertEqual(100);});});还要覆盖四种旋转角度的四个角点。前置与后置镜头的左右关系。4:3、16:9、正方形源图的 Cover 裁剪。人体位于四个画布边缘时贴纸不越界。只有单眼、只有鼻子和只有人体框时的降级。超大贴纸尺寸不会产生负上界。十五、真机验收矩阵场景操作预期后置竖屏人物站画面中心眼镜与双眼重合前置竖屏左右挥手手持贴纸与镜像方向一致人物靠左缓慢走到边缘贴纸不超出预览近距离面部占画面大部分缩放不超过上限远距离全身进入画面贴纸不缩成不可见半遮挡遮住一只眼眼镜降级但不跳出画布多人交叉两人交换位置目标 ID 尽量保持稳定横竖切换重建预览会话旧旋转结果不污染新会话十六、常见问题排查现象原因修复方向前置贴纸左右反了镜像未处理或处理顺序错误旋转后按显示宽度镜像90 度时位置偏移宽高未交换使用旋转后的显示尺寸中心正确但贴纸偏半个宽度锚点中心直接当左上角减去visualSize / 2人物靠边贴纸消失只限制中心点上界减去贴纸尺寸16:9 预览上下偏忽略 Cover 裁剪加入居中裁剪补偿识别偶发缺点时贴纸闪烁没有降级链路关键点、轮廓、框逐级回退多人时贴纸互换使用数组下标关联IoU 与距离做跨帧匹配贴纸持续抖动原始坐标直接渲染按精度执行时间平滑十七、上线前验收清单源图旋转在 0、90、180、270 度均正确。前置镜像在旋转之后执行。显示宽高随 90/270 度交换。Preview 的 Cover 裁剪公式与 ArkUI 布局一致。点、矩形和轮廓使用同一映射函数。每种素材锚点都有关键点与降级实现。中心锚点已转换为图层左上角。贴纸缩放绑定面宽、肩宽或体宽语义。缩放和位置都有安全上下界。多人目标具有稳定关联策略。坐标更新带时间平滑和失效时间。几何纯函数测试覆盖旋转、镜像和裁剪。真机测试覆盖前后镜头、远近距离和画面边缘。总结人体贴纸对齐不是一个x * scale公式而是一条完整坐标管线。源图先处理旋转再处理前置镜像随后按 Preview 的 Cover 规则映射到316 x 390画布语义锚点再从关键点、面部元数据、分割轮廓和人体框中逐级解析最后换算为贴纸左上角并执行边缘夹取。将这些步骤拆成纯函数后坐标问题可以通过固定样本自动回归而不再依赖反复拖动真机调参。识别能力升级时只需更换目标数据来源装饰素材和 ArkUI 图层仍可复用同一套锚点协议。