当前位置:首页>python>不用 GPU!700 行 Python 代码实现人脸替换(二):16800 个锚点的设计哲学

不用 GPU!700 行 Python 代码实现人脸替换(二):16800 个锚点的设计哲学

  • 2026-10-11 06:40:33
不用 GPU!700 行 Python 代码实现人脸替换(二):16800 个锚点的设计哲学

系列目录

  • (一)动机与架构
  • (二)SCRFD 人脸检测 ← 本文
  • (三)关键点检测:为什么 106 个点全挤在 5×7 像素里
  • (四)规范空间对齐:从"四条眉毛"到正确换脸
  • (五)三角剖分与仿射变换:换脸的核心算法
  • (六)mask、膨胀与泊松融合:最后 10% 的细节

找脸,没那么简单

在换脸之前,你得先找到脸。

这听起来简单——谁不认识一张脸呢?但对计算机来说,"在图里找到一张脸"意味着要在几百万个像素中,判断每个位置、每种尺寸下是否存在人脸。而且人脸可以是 20 像素的小头像,也可以是占满整张图的大头照。

SCRFD(Sample and Computation Redistribution for Face Detection)用了一个巧妙的多尺度策略来解决这个问题。


三个尺度,各管一摊

SCRFD 在三个特征图层级上同时做检测。每个层级的网格密度不同:

步长 (stride)
网格尺寸
锚点数量
每个网格的感受野
8
80 × 80
12,800
~64px — 适合小脸
16
40 × 40
3,200
~128px — 适合中等脸
32
20 × 20
800
~256px — 适合大脸

总计:16,800 个锚点,均匀铺满 640×640 的输入图像。

为什么是这三个步长?因为人脸在图像中的常见尺寸范围大约是 20-600 像素。步长 8 的网格(80×80)每个格点覆盖 8×8 像素区域,适合捕捉小脸。步长 32 的网格(20×20)每个格点覆盖 32×32 像素区域,适合大脸。

三个尺度互补,几乎不会漏掉任何尺寸的人脸。


每个锚点输出什么?

对 16,800 个锚点中的每一个,SCRFD 输出:

[score] [left, top, right, bottom] [x0,y0, x1,y1, x2,y2, x3,y3, x4,y4]
  ↑              ↑                            ↑
 置信度        边界框(4个值)              5个关键点(10个值)
  • score:这个位置有人脸的概率。已过 sigmoid 激活,范围 (0, 1)
  • bbox:边界框的四个值,但不是直接坐标,而是距离格式
  • landmarks:左右眼、鼻尖、左右嘴角。同样是距离格式

距离格式:一个小而重要的设计

SCRFD 的边界框不输出 (x1, y1, x2, y2) 或 (cx, cy, w, h)。

它输出 [left_dist, top_dist, right_dist, bottom_dist]。

这四个值分别表示:从锚点中心向左、上、右、下各走多远到达边界框的边缘。

解码公式:

# 锚点中心
cx = j * stride   # j 是网格列号
cy = i * stride   # i 是网格行号

# 距离 → 坐标
x1 = cx - left_dist   * stride
y1 = cy - top_dist    * stride
x2 = cx + right_dist  * stride
y2 = cy + bottom_dist * stride

为什么用距离格式而不是直接输出坐标?

因为"距离"是一个相对于锚点的量,范围天然受限于锚点间距(stride),数值更稳定。直接输出绝对坐标的话,模型需要学习"这张图里第 537 个像素是人脸的左上角"——这比学习"从这里往左走 3 格"难得多。

同样的道理,5 个关键点也是距离格式:

landmark_x = cx + pred_dx * stride
landmark_y = cy + pred_dy * stride

NMS:去重

16,800 个锚点,同一个脸可能被几十个锚点同时"命中"。

NMS(Non-Maximum Suppression,非极大值抑制)负责去重。它的逻辑很简单:

  1. 按置信度从高到低排序
  2. 取最自信的那个框
  3. 删掉和它重叠超过 40%(IoU > 0.4)的其他框
  4. 重复,直到没有剩余
defnms(boxes, scores, iou_threshold):
    order = np.argsort(scores)[::-1]   # 按分数降序
    keep = []
while len(order) > 0:
        keep.append(order[0])           # 保留当前最高分
# 计算当前框与其余框的 IoU
        iou = compute_iou(boxes[order[0]], boxes[order[1:]])
# 保留重叠 < 阈值的框
        order = order[1:][iou <= iou_threshold]
return keep

最终,每张人脸只保留一个最可靠的框。


Letterbox:处理任意尺寸

SCRFD 的输入是 640×640。但用户的照片可能是任意尺寸——竖屏 1080×1920 或横屏 1920×1080。

直接拉伸会把人脸压扁。正确做法是 letterbox:等比缩放后居中贴在 640×640 画布上,空白处填黑。

scale = min(640/h, 640/w)
new_h, new_w = int(h*scale), int(w*scale)
resized = cv2.resize(img, (new_w, new_h))
canvas = np.zeros((640, 640, 3))  # 黑色画布
canvas[pad_top:pad_top+new_h, pad_left:pad_left+new_w] = resized

检测完成后,所有坐标要按同样的比例和偏移映射回原图尺寸:

x_orig = (x_640 - pad_left) / scale
y_orig = (y_640 - pad_top) / scale

小结

SCRFD 的人脸检测部分整体很顺利。真正让我头疼的是下一步——106 个关键点的定位。

下一篇聊聊 2d106det 模型:我是怎么花了两个小时才发现 106 个关键点全挤在 5×7 像素里的。


下一篇:(三)关键点检测:为什么 106 个点全挤在 5×7 像素里

最新文章

随机文章