4、数据预处理流水线:人脸检测、对齐与归一化

数据预处理,说白了就是给模型「喂饭」前的最后一道工序。你想想看,模型再厉害,吃进去的是垃圾,吐出来的也只会是垃圾。我在这个环节踩过的坑,比模型训练本身还多。今天咱们就把这条流水线彻底捋清楚。

4.1 人脸检测:MTCNN vs RetinaFace

人脸检测是整个流水线的第一步。选错了检测器,后面全是白费功夫。我个人习惯把检测器分成两类:轻量级和重量级。

特性 MTCNN RetinaFace
速度 快(CPU可跑) 较慢(需要GPU)
精度 中等 高(尤其遮挡场景)
关键点 5点(眼鼻嘴) 5点/106点可选
适用场景 实时、移动端 高精度、离线处理

MTCNN 是我最早用的检测器。它用三级级联网络(P-Net、R-Net、O-Net)逐步精修人脸框。优点是轻量,缺点嘛——遇到大角度侧脸或者戴口罩的情况,经常漏检。我记得有一次做门禁项目,用户戴着口罩站在逆光下,MTCNN 愣是没检测出来,后来换成 RetinaFace 才搞定。

RetinaFace 就不一样了。它基于特征金字塔和多任务学习,不仅能检测人脸,还能输出密集关键点。我建议你在训练人脸比对模型时,优先用 RetinaFace。为什么?因为后续的人脸对齐需要精确的关键点,RetinaFace 的 5 点输出比 MTCNN 稳定得多。

我的经验法则:

  • 训练阶段:用 RetinaFace,精度优先
  • 推理阶段:看部署环境,CPU 用 MTCNN,GPU 用 RetinaFace

4.2 人脸对齐:仿射变换与关键点归一化

检测到人脸后,下一步就是对齐。为什么要对齐?因为模型对旋转、缩放非常敏感。你想想看,一张歪着头的照片和一张正脸照,在特征空间里可能差出十万八千里。

对齐的核心是仿射变换。说白了,就是通过一个 2x3 的矩阵,把人脸的关键点映射到标准位置。我通常的做法是:

  1. 用检测器得到 5 个关键点(左眼、右眼、鼻尖、左嘴角、右嘴角)
  2. 计算这 5 个点到标准位置的仿射变换矩阵
  3. 用 OpenCV 的 warpAffine 函数做变换
import cv2
import numpy as np

def align_face(img, landmarks, target_size=(112, 112)):
    # 标准关键点位置(112x112 图像)
    std_landmarks = np.array([
        [38.2946, 51.6963],
        [73.5318, 51.5014],
        [60.0, 71.7366],
        [41.5493, 92.3655],
        [70.7299, 92.2041]
    ], dtype=np.float32)
    
    # 计算仿射变换矩阵
    M, _ = cv2.estimateAffinePartial2D(landmarks, std_landmarks)
    
    # 应用变换
    aligned = cv2.warpAffine(img, M, target_size)
    return aligned

避坑指南:我曾经直接用 cv2.getAffineTransform 只取 3 个点做变换,结果遇到大角度人脸时变形严重。后来改用 estimateAffinePartial2D 用全部 5 个点做最小二乘拟合,效果稳定多了。

关键点归一化也很重要。我习惯在仿射变换后,把关键点坐标归一化到 [0, 1] 区间。这样做的好处是:无论输入图像多大,模型看到的都是相对位置。

4.3 图像归一化:均值与方差

对齐之后,图像归一化是最后一道预处理。这一步看似简单,但参数选错了,模型收敛速度能差好几倍。

我常用的归一化方式有两种:

方式 公式 适用场景
ImageNet 归一化 (x - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225] 迁移学习、预训练模型
人脸专用归一化 (x - 127.5) / 128.0 人脸识别专用模型

我个人更推荐人脸专用归一化。为什么?因为人脸图像的颜色分布和 ImageNet 完全不同。人脸主要是肤色,方差小,用 ImageNet 的均值和方差反而会引入偏差。我记得有一次用 ResNet 做人脸识别,用 ImageNet 归一化训练了 50 个 epoch 还没收敛,换成 (x - 127.5) / 128.0 后,20 个 epoch 就出效果了。

注意:归一化参数必须和训练时保持一致。如果你用预训练模型,一定要查清楚它用的什么归一化。我曾经因为归一化不匹配,导致模型在测试集上准确率直接掉了 15%。

4.4 数据加载器优化

数据加载器是流水线的最后一环,也是最容易被忽视的一环。很多人在 GPU 空转等数据,就是因为 DataLoader 没优化好。

我总结了几条优化原则:

  • 多进程加载:num_workers 设为 CPU 核心数的一半。设太多反而会因为进程切换开销导致性能下降。
  • 预取机制:用 prefetch_factor=2,让 DataLoader 提前加载下一批数据。
  • 内存固定:用 pin_memory=True,把数据固定在 CPU 内存中,加速 GPU 传输。
  • 数据增强在线做:不要在硬盘上存增强后的数据,而是在 DataLoader 里用 transforms 实时做。
from torch.utils.data import DataLoader
from torchvision import transforms

transform = transforms.Compose([
    transforms.ToPILImage(),
    transforms.RandomHorizontalFlip(p=0.5),
    transforms.ColorJitter(brightness=0.1, contrast=0.1),
    transforms.ToTensor(),
    transforms.Normalize(mean=[127.5, 127.5, 127.5], std=[128.0, 128.0, 128.0])
])

dataloader = DataLoader(
    dataset,
    batch_size=256,
    shuffle=True,
    num_workers=8,
    pin_memory=True,
    prefetch_factor=2,
    drop_last=True
)

我的调优经验:如果 GPU 利用率低于 80%,先检查 DataLoader。我曾经用 num_workers=0 跑了一个晚上,GPU 利用率只有 30%,改成 8 个 worker 后直接飙到 95%。

4.5 完整流水线架构

下面这张图是我在实际项目中用的预处理流水线,你可以直接参考:

人脸比对预处理流水线 原始图像 人脸检测 (RetinaFace) 人脸对齐 (仿射变换) 图像归一化 (均值/方差) DataLoader 输出 关键参数 检测置信度阈值: 0.9 对齐尺寸: 112×112 归一化: (x-127.5)/128 DataLoader 配置 batch_size: 256 num_workers: 8 pin_memory: True

这条流水线看起来步骤多,但每一步都不可或缺。我见过太多人跳过对齐直接训练,结果模型在测试集上表现一塌糊涂。记住:预处理花的时间,会在训练效率和模型精度上加倍还给你。