系列目录
- (二)SCRFD 人脸检测:16800 个锚点的设计哲学
- (六)mask、膨胀与泊松融合:最后 10% 的细节
从 5 个点到 106 个点
SCRFD 给了我 5 个关键点:双眼、鼻尖、两嘴角。对于换脸来说,5 个点远远不够。
我需要知道眉毛的弧度、眼睛的轮廓、鼻梁的走向、嘴唇的形状——106 个关键点才能精确描述一张脸的几何结构。
所以我用了第二个模型:2d106det.onnx。
模型规格
看起来很简单:把人脸裁剪出来,缩放到 192×192,送进模型,拿到 106 个点。对吧?
错。
致命坑:106 个关键点全挤在一坨
模型跑通了,输出形状也正确:(1, 212)。
我把 212 个值 reshape 成 (106, 2),按像素坐标画在图上。
结果让我愣住了——
所有 106 个点挤在一个 5×7 像素的小区域里。 在人脸图上看就是一个密密麻麻的小绿点,根本分辨不出五官。
排查过程
我先怀疑是模型坏了。但模型能正常加载,推理也没报错。
然后我用一张纯黑图和一张纯白图分别跑了一遍,输出居然也差不多——范围都在 [-0.5, 0.8] 左右。
等等,[-0.5, 0.8]?
192×192 的像素坐标应该是 [0, 192]。模型输出的是 [-0.5, 0.8]……这是归一化坐标,不是像素坐标!
真相:模型的输出需要反归一化
这个 2d106det 模型,输出的是以人脸中心为原点的归一化坐标。(0, 0) 是人脸正中心,(-1, -1) 是左上角,(1, 1) 是右下角。
实际输出范围约 [-0.5, 0.8],对应的是:人脸从中心偏左 0.5 到偏右 0.8,从中心偏上 0.5 到偏下 0.8。
反归一化公式:
# 192×192 的图像,中心在 (96, 96),半宽半高也是 96
pixel_x = pred_x * 96 + 96
pixel_y = pred_y * 96 + 96
就这么一行代码,我调了两个小时。
教训:拿到一个陌生的 ONNX 模型,第一件事不是看论文、看文档——是用随机输入跑一遍,看看输出数值范围。如果输出范围不对(比如像素坐标却在 [-1, 1] 之间),那就是编码方式不同。
还有第二个坑:模型需要对齐后的人脸
写完反归一化,我用同样的方法:裁剪人脸 → 缩放 192×192 → 送模型 → 反归一化 → 得到关键点。
结果有所改善,但关键点位置仍然不准——和 SCRFD 给出的 5 个参考点相比,偏差了 60-180 像素。
原因:2d106det 不是设计来识别"随意裁剪的人脸"的。它期望输入的人脸是经过对齐的——五官在规范位置(比如眼睛在 (65, 88) 左右)。
所以流程应该是:
裁剪人脸 → 用SCRFD的5点做对齐 → warp到192×192 → 送2d106det → 得到106点(规范空间内)
而不是:
裁剪人脸 → 直接缩放192×192 → 送2d106det(❌ 人脸没对齐,关键点不准)
对齐怎么做?
用 SCRFD 的 5 个关键点(双眼、鼻尖、嘴角)计算一个相似变换(旋转 + 缩放 + 平移),把人脸 warp 到规范位置:
# 规范模板:5个关键点在192×192图像中的"标准位置"
canonical_5pts = np.array([
[65.6, 88.6], # 左眼
[126.1, 88.6], # 右眼
[96.0, 123.0], # 鼻尖
[71.2, 158.3], # 左嘴角
[121.3, 158.3], # 右嘴角
])
# 相似变换(只旋转+缩放+平移,不含剪切)
M, _ = cv2.estimateAffinePartial2D(
scrfd_5pts, # 原图中的5点
canonical_5pts, # 目标位置
method=cv2.RANSAC
)
# warp:把原图人脸"摆正"到规范192×192
aligned_face = cv2.warpAffine(face_crop, M, (192, 192))
有了对齐后的人脸,106 个关键点就准确了。
小结
这一篇讲了两个坑:
- 归一化坐标 — 模型输出不是像素值,需要
pixel = pred * 96 + 96 - 人脸对齐 — 模型需要对齐后的输入,不能直接塞裁剪图
下一个问题是更大的坑——虽然关键点对了,但换完脸后出现了四条眉毛、两张嘴。下一篇详细讲讲"规范空间"是怎么解决这个问题的。
下一篇:(四)规范空间对齐:从"四条眉毛"到正确换脸