前言
需求很朴素:做个证件照 App,用户拍完立刻告诉他"这张能不能过"。
我调 faceDetector.detect(),拿返回值定阈值。第一版上线前自测,四个地方对不上:
probability返回 1.000——正面照是 1.000,侧头 40° 的废片也是 1.000。拿它当质量分,等于没有质量分。- 用户明明侧头约 40°,
pose.yaw报 -25.97。我按"±10° 算合格"写的阈值,判出来的结果和肉眼对不上。 - 一张糊掉的六人老合影,
detect()返回空数组。不是"检测到人但质量差",是根本没看见脸。 points数组长度是 5。d.ts 里只写了Array<FacePoint>,没说几个点、什么顺序。
这一篇就是这四条的排查记录。
这个能力本来该解决什么
证件照、门禁抓拍、视频会议入会前检查、直播开播前校验——共同点是在提交前 1 秒给出可/不可,而不是事后被打回。
faceDetector(@kit.CoreVisionKit)在端侧完成检测,不联网、不要权限,返回人脸框、姿态角、特征点、置信度。做"拍摄即时反馈"是合适的。
但要注意它的定位是检测(有没有脸、脸在哪、姿态如何),不是质量评估(曝光、虚焦、背景是否合规)。后者要自己补,见最后一节。
接口
import { faceDetector } from '@kit.CoreVisionKit';
faceDetector.init(): Promise<boolean>
faceDetector.init(cfg: FaceRecognitionConfiguration): Promise<boolean> // cfg = { faceBlock: boolean }
faceDetector.detect(vi: VisionInfo): Promise<Array<Face>> // vi = { pixelMap }
faceDetector.release(): Promise<void>
interface Face {
readonly probability: number;
readonly block?: FaceBlock; // 需 init 时 faceBlock:true 才有值
readonly pose: FacePose; // { yaw, pitch, roll }
readonly rect: FaceRectangle; // { left, top, width, height }
readonly points: Array<FacePoint>; // { x, y }[]
}
- yaw / pitch / roll:偏航(左右转)、俯仰(抬头低头)、侧倾(歪头)三个旋转维度。
- FaceBlock:枚举
{ UNINITIALIZED = -1, UNBLOCKED = 0, BLOCKED = 1 }。- 端侧检测:识别过程在设备本地完成,人脸图像不外传,这对证件照类应用是硬性要求。
版本:phone / tablet 自 5.0.0(12),2in1 自 5.0.1(13)。不是 API 26 新增,鸿蒙 7 上只是继续可用。
还有一点容易漏:block 字段要显式 init({ faceBlock: true }) 才会填充,默认 init() 时它是 undefined。
开发步骤
第一步:生命周期
和 OCR 一样,init() 全局一次,别每次检测都调:
private inited: boolean = false;
async init(): Promise<void> {
if (this.inited) {
return;
}
try {
this.inited = await faceDetector.init();
} catch (err) {
const e = err as BusinessError;
throw new Error(`人脸检测初始化失败(${e.code}):${e.message}`);
}
}第二步:检测并挑最大的一张脸
合影场景下 detect() 会返回多张,证件照要取最大那张:
const faces: faceDetector.Face[] = await faceDetector.detect({ pixelMap: pm });
let best: faceDetector.Face = faces[0];
for (let i = 1; i < faces.length; i++) {
if (faces[i].rect.width > best.rect.width) {
best = faces[i];
}
}第三步:体检规则全部用相对量
绝对像素没有意义,同一张脸在不同分辨率图上框大小不同。所有阈值都换算成比例:
const ratio: number = rect.width / imgW; // 人脸占比
const off: number = Math.abs(rect.left + rect.width / 2 - imgW / 2) / imgW; // 水平偏离
const topGap: number = rect.top / imgH; // 头顶留白第四步:打印日志时避开一个坑
hilog 不支持 %{public}f。我第一版这么写:
hilog.info(0x0000, TAG, 'yaw=%{public}f pitch=%{public}f pts=%{public}d',
face.pose.yaw, face.pose.pitch, face.points.length);真机输出:
yaw=}f pitch=}f pts=3.702503浮点参数被吞掉,后面的整数参数串位到了错误的占位符上。我一度以为 points.length 是 370 万。改成先放大再打印才对:
// hilog 不支持 %{public}f,浮点必须先放大成整数再打印
hilog.info(0x0000, TAG, 'yaw100=%{public}d pitch100=%{public}d pts=%{public}d',
Math.round(face.pose.yaw * 100), Math.round(face.pose.pitch * 100), face.points.length);这条和人脸检测无关,但排查那十分钟足够把人逼疯,先记在这。
真机数据
测试机:HUAWEI MatePad Pro(MRDI-W00),HarmonyOS 7.0.0.107,API 26。三张样片均为 540×720(合影为 640×794)。
| 样片 | 人脸数 | 耗时 | rect (l,t,w,h) | yaw | pitch | roll | probability | points |
|---|---|---|---|---|---|---|---|---|
| 标准正面 | 1 | 111 ms | 148,172,249,302 | 3.70 | 8.23 | 1.63 | 1.000 | 5 |
| 侧头 40° | 1 | 46 ms | 184,214,226,261 | -25.97 | 16.76 | 13.68 | 1.000 | 5 |
| 模糊六人合影 | 0 | 45 ms | — | — | — | — | — | — |
派生指标:
| 样片 | 人脸占比 | 水平偏离 | 头顶留白 | 结论 |
|---|---|---|---|---|
| 标准正面 | 46.1% | 0.5% | 上 23.9% / 下 34.2% | 全部通过 |
| 侧头 40° | 41.9% | 5.0% | 上 29.7% / 下 34.0% | yaw / pitch / roll 三项不合格 |
| 模糊合影 | — | — | — | 人脸数量不合格(0 张) |
合格报告:

不合格报告:

四条排查结论
结论一:probability 不是质量分,是"检出了脸"
两张图都是 1.000,一张是完美正面、一张是明显废片。
所以 probability 只能用来过滤误检(比如把衣服花纹认成脸),阈值设在 0.6 以上足够,不要拿它做质量分级。
质量要自己算:人脸占比、姿态角、头顶留白,再补曝光/虚焦检测(对 pixelMap 自己统计亮度直方图与梯度)。
结论二:姿态角不是物理角度,阈值必须实测标定
侧头约 40°,yaw 报 -25.97。符号也不对应直觉(向右侧头是负值)。
结论是:pose 是模型内部度量,不能当"度"来用。所以"±10° 算合格"这种凭常识写的阈值是错的。
正确做法是拿一批真实合格/不合格样本跑一遍,看分布再定线。本文三张样片给出的参考锚点:正脸 yaw≈3.7、明显侧头 yaw≈-26;正脸 roll≈1.6、明显歪头 roll≈13.7。
另外注意:标准正面照的 pitch 是 8.23°,不是 0。人拿手机拍照时俯仰天然不为零,把 pitch 阈值设成 ±3° 会把大量正常照片判为不合格。
结论三:points 实测 5 个,但顺序官方未声明
d.ts 只给 Array<FacePoint>。实测长度稳定为 5,按通行惯例应是左右眼、鼻尖、左右嘴角一类,但没有文档保证。
所以不要把 points[2] 硬编码成"鼻尖"来做业务判断。需要精确关键点(比如"眼睛连线是否水平")时,用 roll 角度而不是自己算两点连线;或者先用大量样本验证顺序稳定性再上生产。
结论四:检不出脸 ≠ 没有脸,模糊小脸会直接返回空数组
那张六人合影里人脸清晰可辨(人眼),但因为是翻拍老照片、模糊、单脸只有几十像素,detect() 返回空。
这不是 bug,是能力边界。产品上的含义是:"未检测到人脸"这个提示对低质量图是误导,用户明明看得见脸。建议文案改成"没检测到清晰人脸,请靠近一点或换一张清晰的"。
更实用的做法是串联超分:检测返回空时,先用 imageSuperResolution 放大 4 倍再检一次。老照片批量标人脸,这条链路是通的(超分把 320px 提到 1280px,小脸尺寸随之进入可检测范围)。
实时预览怎么做才不掉帧
证件照场景一定要边拍边提示,也就是相机预览帧持续送进 detect()。实测单次 46–111 ms,按 30 fps 出帧必然堆积。三个动作要一起做:
降频而不是全检。 每 3–4 帧检测一次,中间帧沿用上一次的 rect 做 UI 提示。人脸移动速度远慢于帧率,肉眼看不出差别。
请求排队,绝不并发。 detect() 是异步的,如果每帧都直接 await,回调会乱序返回,UI 上的框会来回跳。用一个"进行中"标志把新请求丢掉即可:
private running: boolean = false;
async tick(pm: image.PixelMap): Promise<void> {
if (this.running) {
return; // 上一帧还没检完,这一帧直接丢弃
}
this.running = true;
try {
this.rep = await this.checker.check(pm);
} finally {
this.running = false;
}
}丢帧比排队好——排队会让提示滞后好几秒,用户早就不动了框还在追。
坐标换算走比例。 rect 是像素坐标,预览控件是 vp。换算一律用 rect.width / imgW * 控件宽,不要缓存绝对值,否则切换相机分辨率就全错。
阈值该留多少余量
基于本文三张样片的锚点,给一组我认为安全的初始阈值。注意这些是起点不是终点,上线前必须用自己业务的真实样本重标:
| 项目 | 建议初始阈值 | 依据 |
|---|---|---|
| yaw | ≤ 12 | 正脸实测 3.70,明显侧头 -25.97,中间留一倍余量 |
| pitch | ≤ 14 | 正脸实测 8.23 已经不接近 0,设太严会大量误杀 |
| roll | ≤ 8 | 正脸 1.63,歪头 13.68,8 是干净的分割带 |
| 人脸占比 | 25%–65% | 两张样片分别 46.1% 与 41.9% |
| 水平偏离 | ≤ 8% | 正脸 0.5%,侧头 5.0% |
| 头顶留白 | 8%–35% | 正脸 23.9%,侧头 29.7% |
| probability | ≥ 0.6 | 只用于过滤误检,两张样片都是 1.000 |
一个反直觉但重要的点:pitch 的余量要给得比 yaw 大。人举手机拍证件照时,手臂高度几乎不可能与眼睛齐平,俯仰天然存在;而左右偏头是用户主动能控制的。把 pitch 设成 ±5° 的结果是大量正常照片被判不合格,用户只会认为 App 在故意挑刺。
上生产前还要补的
- 曝光与虚焦:
faceDetector不管,要自己统计亮度直方图 / 拉普拉斯梯度。 - 背景色合规:证件照对底色有要求,得自己采样
pixelMap边角像素判断。 init({ faceBlock: true }):只有需要block字段时才开,默认不开。- 文案兜底:返回空数组时不要写"未检测到人脸",见结论四。
总结
什么场景值得用它:证件照、门禁抓拍、入会前检查、开播前校验这类"提交前一秒给个可不可"的场景,端侧跑完人脸不出设备。
不适合:指望它判断照片质量好坏。曝光、虚焦、底色合规它一概不管,得自己在 pixelMap 上补统计。
💡 一句话:它给的是几何信息不是质量结论,
probability和pose都别按字面用,阈值一律实测标定。
参考文献
- Core Vision Kit 开发指南 · 人脸检测:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/core-vision-face-detector
- Core Vision Kit API 参考 · faceDetector(人脸检测):https://developer.huawei.com/consumer/cn/doc/harmonyos-references/core-vision-face-detector-api
- Core Vision Kit 开发指南 · 图像超分(串联升档用):https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/core-vision-image-super-resolution