最近在小程序里做了一套人脸检测拍照引导,目标很明确:用户进入相机后,必须满足"正脸、距离合适、居中、无遮挡"之后,才能拍照进入后续测肤流程。
一开始以为只是调个相机组件,后来发现真正麻烦的是:微信小程序给你的不是完整原生相机能力,而是一套沙盒里的视觉能力。很多判断都要自己用规则补齐。
技术方案
这套方案主要用了:
wx.createVKSession():获取人脸 anchor。- WebGL canvas:渲染相机帧。
- 自定义引导层:绘制取景框、状态边框、提示文案。
- 人脸规则判断:距离、居中、姿态、遮挡、置信度。
核心流程大概是:
js
const session = wx.createVKSession({
track: { face: { mode: 1 } },
version: 'v1',
gl,
cameraPosition: 1
})
session.on('updateAnchors', (anchors) => {
const result = evaluateFaceGuidance(anchors[0])
applyGuidance(result)
})
updateAnchors 会返回人脸框、关键点、角度、置信度等信息。真正的判断逻辑都放在 evaluateFaceGuidance() 里。
状态设计
我把人脸状态拆成几个阶段:
js
NO_FACE // 未检测到人脸
TOO_FAR // 太远
TOO_NEAR // 太近
OFF_CENTER // 没对准取景框
BAD_POSE // 姿态或遮挡不合格
READY // 可以拍照
整体判断顺序是:
- 有没有人脸。
- 人脸大小是否合适。
- 人脸中心是否在取景框里。
- 是否正脸。
- 是否有遮挡。
- 是否允许拍照。
距离判断用的是"人脸宽度 / 取景框宽度":
js
export const FACE_SCALE_RANGE = {
min: 1.12,
max: 1.3
}
这个值不是固定标准,需要真机反复调。不同机型、人脸距离、预览比例都会影响效果。
坑一:坐标是镜像的
前置摄像头下,用户看到的是镜像画面,但 VKSession 返回的人脸坐标不一定和视觉预览一致。
所以我做了镜像处理:
js
if (options.mirrorX !== false) {
x = 1 - x - w
}
否则会出现很诡异的问题:用户明明往左移,算法却觉得他往右偏。
坑二:左转头能识别,右转头很差
最开始我只用 yaw 判断左右转头:
js
if (Math.abs(angles.yaw) > opt.maxYaw) {
issues.push('请正对镜头,不要左右转头')
}
但真机测试发现,左转头提示比较准,右转头经常漏判。
原因大概率是 VKSession 返回的 yaw 在左右方向上不完全对称。后来我加了关键点分布兜底:如果关键点明显偏向脸框某一侧,就认为不是正脸。
js
const sideBalance = minSide / maxSide
const meanOffset = Math.abs((meanX - centerX) / rect.w)
if (sideBalance < 0.58 || meanOffset > 0.12) {
return { ok: false, issue: '请正对镜头,不要左右转头' }
}
这个兜底比单看 yaw 稳很多。
坑三:遮挡判断一开始"不生效"
遮挡是最容易误判的地方。
一开始我的逻辑是:关键点数量太少就跳过,关键点落入脸框比例太低也跳过。结果手挡住一部分脸时,关键点刚好变少或异常,反而绕过了遮挡判断。
后来改成:
- 有一定关键点就参与判断。
- 关键点覆盖范围太窄,认为脸部不完整。
- 上下左右关键点分布不均,认为有遮挡。
- 置信度低时,也提示脸部不完整。
文案最后统一成:
脸部不完整,请移开遮挡物
这个文案比"脸部不全"更自然,也能覆盖手挡脸、额头没露、眼睛被遮、嘴巴被挡等情况。
坑四:置信度不是遮挡概率
VKSession 返回的 confidence 可以理解成"当前人脸识别结果有多可靠"。
但它不是"遮挡概率"。
低置信度可能是遮挡,也可能是光线差、侧脸、模糊、距离不合适。所以不能只靠它判断遮挡,只能作为辅助条件。
js
if (confidence != null && confidence < 0.55) {
issues.push('脸部不完整,请移开遮挡物')
}
最终还是要结合关键点、脸框形态、姿态一起判断。
坑五:不要每帧都刷新 UI
updateAnchors 触发频率很高,如果每帧都 setData,页面会很卡。
我做了节流:
js
const now = Date.now()
if (now - this.lastUiUpdate < 80) return
this.lastUiUpdate = now
80ms 左右对引导提示已经够用了,用户感知不到明显延迟,但性能会稳定很多。
坑六:WebGL 上面普通 view 盖不上去
因为相机帧是 WebGL canvas,普通 view 层级有时会出问题。提示文案这类 UI 最好用 cover-view:
xml
<cover-view class="hint-overlay">
<cover-view class="main-tip">{{mainTip}}</cover-view>
</cover-view>
否则容易出现真机和开发者工具表现不一致。
最后总结
小程序里做人脸识别引导,难点不在"能不能识别人脸",而在于把识别结果变成稳定、可解释、体验好的拍照规则。
我的经验是:
- 不要只依赖单个字段,比如
yaw或confidence。 - 正脸判断要结合角度、脸框形态、关键点分布。
- 遮挡判断要避免过早跳过。
- 真机调参比开发者工具重要得多。
- 提示文案要产品化,不要暴露算法术语。
如果只是简单拍照,可以用普通 <camera>。但如果要做"正脸、无遮挡、距离合适"的强引导,wx.createVKSession + WebGL + 自定义规则 是一条可行路线,只是需要大量真机测试和阈值调整。