当前位置:首页>python>不用 GPU!700 行 Python 代码实现人脸替换(三):106 个点全挤在 5*7 像素里

不用 GPU!700 行 Python 代码实现人脸替换(三):106 个点全挤在 5*7 像素里

  • 2026-10-11 06:13:54
不用 GPU!700 行 Python 代码实现人脸替换(三):106 个点全挤在 5*7 像素里

系列目录

  • (一)动机与架构
  • (二)SCRFD 人脸检测:16800 个锚点的设计哲学
  • (三)关键点检测 ← 本文
  • (四)规范空间对齐:从"四条眉毛"到正确换脸
  • (五)三角剖分与仿射变换:换脸的核心算法
  • (六)mask、膨胀与泊松融合:最后 10% 的细节

从 5 个点到 106 个点

SCRFD 给了我 5 个关键点:双眼、鼻尖、两嘴角。对于换脸来说,5 个点远远不够。

我需要知道眉毛的弧度、眼睛的轮廓、鼻梁的走向、嘴唇的形状——106 个关键点才能精确描述一张脸的几何结构。

所以我用了第二个模型:2d106det.onnx。


模型规格

属性
值
输入
(1, 3, 192, 192)
 RGB,[0, 1] 归一化
输出
(1, 212)
 = 106 个 (x, y)
大小
4.8 MB

看起来很简单:把人脸裁剪出来,缩放到 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 个关键点就准确了。


小结

这一篇讲了两个坑:

  1. 归一化坐标 — 模型输出不是像素值,需要 pixel = pred * 96 + 96
  2. 人脸对齐 — 模型需要对齐后的输入,不能直接塞裁剪图

下一个问题是更大的坑——虽然关键点对了,但换完脸后出现了四条眉毛、两张嘴。下一篇详细讲讲"规范空间"是怎么解决这个问题的。


下一篇:(四)规范空间对齐:从"四条眉毛"到正确换脸

最新文章

随机文章