人脸检测算法——SCRFD

SCRFD算法核心解析

1. 算法定义与背景

SCRFD(Sample and Computation Redistribution for Efficient Face Detection)由Jia Guo等人于2021年在arXiv提出,是一种高效、高精度的人脸检测算法,其核心创新在于:

  • 双重重分配策略
    • 样本重分配(SR) :动态增强关键训练阶段的样本数据。
    • 计算重分配(CR) :通过神经架构搜索(NAS)优化骨干网络(Backbone)、颈部(Neck)和头部(Head)的计算负载。
  • 轻量化设计:支持从0.5GF到34GF的多规格模型,兼顾移动端与高性能设备。

2. 技术架构与创新
架构组成

  • 骨干网络(Backbone)
    • 基于ResNet变体(如CSPDarknet),通过残差连接解决深层网络训练难题。
    • 支持量化(FP16/INT8)降低计算量。
  • 颈部(Neck)
    • 采用 特征金字塔网络(FPN) 实现多尺度特征融合,增强小目标检测。
    • 引入PANet优化特征传递路径。
  • 头部(Head)
    • 基于FCOS(Focal Loss)实现无锚框检测,简化输出层。
    • 采用八参数回归直接预测边界框坐标。
核心创新机制
  • 计算重分配搜索算法
    • 训练阶段:通过NAS动态分配Backbone/Neck/Head的计算比例,最大化资源利用率。
    • 硬件加速
      • 存内计算:权重映射至硬件加速单元,减少数据传输。
      • 异构加速:支持GPU/TPU/NPU多平台部署。
      • 量化优化:FP16/INT8量化降低推理延迟。
3. 性能基准
WIDER FACE数据集表现
模型Easy AP (%)Medium AP (%)Hard AP (%)推理延迟 (ms)
SCRFD-34GF96.0694.9285.2911.7 (V100)
RetinaFace-R5094.9291.9064.1721.7 (V100)
数据来源:
  • 效率优势
    • SCRFD-34GF比RetinaFace-R50快 47%(11.7ms vs 21.7ms)。
    • SCRFD-0.5GF比RetinaFace-MobileNet0.25快 45.57%
跨设备优化
  • 边缘设备适配
    • 深度可分离卷积(如MobileNet)降低参数量。
    • 模型压缩:剪枝/蒸馏技术减少冗余计算。
  • 部署实践
    • NVIDIA Jetson系列:支持120 FPS实时推理。
    • 内存优化:Memssqueezer架构提升缓存命中率。

4. 与RetinaFace的深度对比
技术差异
维度SCRFDRetinaFace
检测机制无锚框(FCOS)基于锚框
计算分配动态重分配(CR策略)固定计算结构
硬件适配支持FP16/INT8量化依赖ResNet/MobileNet骨干
小目标检测FPN+多尺度融合优化传统特征金字塔
实战性能
  • 精度:SCRFD在Hard AP上领先RetinaFace 21.12%(85.29% vs 64.17%)。
  • 资源效率:SCRFD-34GF计算量仅为竞品TinaFace的 20%
  • 局限性:批量处理时RetinaFace可能更快。

5. 实现与应用
开源生态
论文资源

结论

SCRFD通过三重革新——动态计算分配无锚框检测量化硬件适配——解决了人脸检测在精度与效率间的矛盾。其在WIDER FACE上85.29%的Hard AP及11.7ms的V100推理延迟,显著优于传统方案(如RetinaFace)。未来方向包括:

  1. 与Transformer架构结合强化遮挡处理。
  2. 自适应计算分配支持动态场景。
  3. 跨平台编译优化进一步压缩延迟。

代码:

class ONNXRuntimeModel:
    """一个通用的ONNX模型推理封装类"""
    def __init__(self, onnx_path):
        # 使用ONNX Runtime的CUDA执行器
        providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
        self.session = ort.InferenceSession(onnx_path, providers=providers)
        self.input_name = self.session.get_inputs()[0].name
        self.output_names = [output.name for output in self.session.get_outputs()]

    def __call__(self, input_tensor):
        # ONNX Runtime 需要 numpy array 作为输入
        input_feed = {self.input_name: input_tensor.cpu().numpy()}
        outputs = self.session.run(self.output_names, input_feed)
        return outputs


class SCRFD():
    def __init__(self, onnxmodel, confThreshold=0.5, nmsThreshold=0.5):
        self.inpWidth = 640
        self.inpHeight = 640
        self.confThreshold = confThreshold
        self.nmsThreshold = nmsThreshold
        self.session = ort.InferenceSession(onnxmodel, providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
        self.keep_ratio = True
        self.fmc = 3
        self._feat_stride_fpn = [8, 16, 32]
        self._num_anchors = 2
    def resize_image(self, srcimg):
        padh, padw, newh, neww = 0, 0, self.inpHeight, self.inpWidth
        if self.keep_ratio and srcimg.shape[0] != srcimg.shape[1]:
            hw_scale = srcimg.shape[0] / srcimg.shape[1]
            if hw_scale > 1:
                newh, neww = self.inpHeight, int(self.inpWidth / hw_scale)
                img = cv2.resize(srcimg, (neww, newh), interpolation=cv2.INTER_AREA)
                padw = int((self.inpWidth - neww) * 0.5)
                img = cv2.copyMakeBorder(img, 0, 0, padw, self.inpWidth - neww - padw, cv2.BORDER_CONSTANT,
                                         value=0)  # add border
            else:
                newh, neww = int(self.inpHeight * hw_scale) + 1, self.inpWidth
                img = cv2.resize(srcimg, (neww, newh), interpolation=cv2.INTER_AREA)
                padh = int((self.inpHeight - newh) * 0.5)
                img = cv2.copyMakeBorder(img, padh, self.inpHeight - newh - padh, 0, 0, cv2.BORDER_CONSTANT, value=0)
        else:
            img = cv2.resize(srcimg, (self.inpWidth, self.inpHeight), interpolation=cv2.INTER_AREA)
        return img, newh, neww, padh, padw
    def distance2bbox(self, points, distance, max_shape=None):
        x1 = points[:, 0] - distance[:, 0]
        y1 = points[:, 1] - distance[:, 1]
        x2 = points[:, 0] + distance[:, 2]
        y2 = points[:, 1] + distance[:, 3]
        if max_shape is not None:
            x1 = x1.clamp(min=0, max=max_shape[1])
            y1 = y1.clamp(min=0, max=max_shape[0])
            x2 = x2.clamp(min=0, max=max_shape[1])
            y2 = y2.clamp(min=0, max=max_shape[0])
        return np.stack([x1, y1, x2, y2], axis=-1)
    def distance2kps(self, points, distance, max_shape=None):
        preds = []
        for i in range(0, distance.shape[1], 2):
            px = points[:, i % 2] + distance[:, i]
            py = points[:, i % 2 + 1] + distance[:, i + 1]
            if max_shape is not None:
                px = px.clamp(min=0, max=max_shape[1])
                py = py.clamp(min=0, max=max_shape[0])
            preds.append(px)
            preds.append(py)
        return np.stack(preds, axis=-1)
    def detect(self, srcimg):
        img, newh, neww, padh, padw = self.resize_image(srcimg)
        blob = cv2.dnn.blobFromImage(img, 1.0 / 128, (self.inpWidth, self.inpHeight), (127.5, 127.5, 127.5), swapRB=True)
        blob = np.ascontiguousarray(blob)
        # onnxruntime输入通常是NCHW
        ort_inputs = {self.session.get_inputs()[0].name: blob}
        ort_outs = self.session.run(None, ort_inputs)
        outs = ort_outs  # 结构与原OpenCV DNN输出一致

        # 后续逻辑保持不变
        scores_list, bboxes_list, kpss_list = [], [], []
        for idx, stride in enumerate(self._feat_stride_fpn):
            scores = outs[idx][0]
            bbox_preds = outs[idx + self.fmc * 1][0] * stride
            kps_preds = outs[idx + self.fmc * 2][0] * stride
            height = blob.shape[2] // stride
            width = blob.shape[3] // stride
            anchor_centers = np.stack(np.mgrid[:height, :width][::-1], axis=-1).astype(np.float32)
            anchor_centers = (anchor_centers * stride).reshape((-1, 2))
            if self._num_anchors > 1:
                anchor_centers = np.stack([anchor_centers] * self._num_anchors, axis=1).reshape((-1, 2))

            pos_inds = np.where(scores >= self.confThreshold)[0]
            bboxes = self.distance2bbox(anchor_centers, bbox_preds)
            pos_scores = scores[pos_inds]
            pos_bboxes = bboxes[pos_inds]
            scores_list.append(pos_scores)
            bboxes_list.append(pos_bboxes)

            kpss = self.distance2kps(anchor_centers, kps_preds)
            kpss = kpss.reshape((kpss.shape[0], -1, 2))
            pos_kpss = kpss[pos_inds]
            kpss_list.append(pos_kpss)

        scores = np.vstack(scores_list).ravel()
        bboxes = np.vstack(bboxes_list)
        kpss = np.vstack(kpss_list)
        bboxes[:, 2:4] = bboxes[:, 2:4] - bboxes[:, 0:2]
        ratioh, ratiow = srcimg.shape[0] / newh, srcimg.shape[1] / neww
        bboxes[:, 0] = (bboxes[:, 0] - padw) * ratiow
        bboxes[:, 1] = (bboxes[:, 1] - padh) * ratioh
        bboxes[:, 2] = bboxes[:, 2] * ratiow
        bboxes[:, 3] = bboxes[:, 3] * ratioh
        kpss[:, :, 0] = (kpss[:, :, 0] - padw) * ratiow
        kpss[:, :, 1] = (kpss[:, :, 1] - padh) * ratioh

        # NMS
        # 可用PyTorch或onnxruntime自带的NMS,也可以保留cv2.dnn.NMSBoxes
        if len(bboxes) == 0:
            return []
        bboxes_xyxy = np.stack([bboxes[:,0], bboxes[:,1], bboxes[:,0]+bboxes[:,2], bboxes[:,1]+bboxes[:,3]], axis=1)
        keep = torch.ops.torchvision.nms(torch.tensor(bboxes_xyxy, dtype=torch.float32), torch.tensor(scores, dtype=torch.float32), self.nmsThreshold)
        detections = []
        for i in keep:
            i = i.item()
            x1, y1 = int(bboxes[i, 0]), int(bboxes[i, 1])
            x2, y2 = int(bboxes[i, 0] + bboxes[i, 2]), int(bboxes[i, 1] + bboxes[i, 3])
            box = [x1, y1, x2, y2]
            score = float(scores[i])
            kps = kpss[i]
            detections.append([box, score, kps])
        return detections

这段代码实现了一个功能完整的人脸检测器。它使用了名为 SCRFD 的先进人脸检测模型,并通过 ONNX Runtime 库来运行这个模型,从而可以在不同的硬件(CPU或GPU)上高效执行。

代码的核心目标是:输入一张图片,输出图片中所有检测到的人脸信息,包括位置(边界框)、置信度分数和五官关键点


1. 整体结构概览

这个 SCRFD 类可以看作一个“黑盒子”。你只需要关心两件事:

  1. 初始化 (__init__): 告诉它你的模型文件在哪里,以及你对检测结果的要求(置信度、重叠框合并的阈值)。

  2. 检测 (detect): 给它一张图片,它会返回检测结果。

所有复杂的中间步骤,比如图像预处理、模型推理、结果后处理,都被封装在了类的内部方法中。


2. __init__ 构造函数:初始化检测器

class SCRFD():
    def __init__(self, onnxmodel, confThreshold=0.5, nmsThreshold=0.5):
        # 1. 设置模型输入尺寸
        self.inpWidth = 640
        self.inpHeight = 640
        # 2. 设置阈值
        self.confThreshold = confThreshold  # 置信度阈值
        self.nmsThreshold = nmsThreshold    # NMS阈值
        # 3. 加载ONNX模型
        self.session = ort.InferenceSession(onnxmodel, providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
        # 4. 设置模型相关参数
        self.keep_ratio = True
        self.fmc = 3  # Feature Map Count, 每个输出头(score/bbox/kps)有3个特征图
        self._feat_stride_fpn = [8, 16, 32] # FPN的步长
        self._num_anchors = 2 # 每个位置的锚点数量

逐行解释:

  1. self.inpWidth, self.inpHeight: 定义了模型需要的标准输入图像尺寸为 640x640 像素。任何输入图片都会被调整到这个尺寸。

  2. self.confThreshold: 置信度阈值。模型会对每个可能的人脸位置给出一个分数(0到1之间),表示“这里是人脸”的可信度。只有分数高于这个阈值(默认0.5)的才会被认为是有效检测。

  3. self.nmsThreshold: 非极大值抑制 (NMS) 阈值。模型可能会对同一个脸输出多个高度重叠的边界框。NMS算法会移除多余的框,只保留最好的一个。这个阈值决定了两个框重叠到什么程度才算“多余”。

  4. self.session = ort.InferenceSession(...): 这是核心部分。它使用 onnxruntime 库加载你提供的 .onnx 模型文件。providers 参数指定了推理后端,'CUDAExecutionProvider' 表示优先使用NVIDIA GPU(如果可用),否则回退到 'CPUExecutionProvider' 使用CPU。

  5. self.keep_ratio: 一个布尔标志,决定在调整图像大小时是否保持原始图像的宽高比。True 表示保持,这可以防止人脸变形,提高检测精度。

  6. self.fmc, _feat_stride_fpn, _num_anchors: 这些是与SCRFD模型架构紧密相关的超参数。

    • _feat_stride_fpn = [8, 16, 32]: SCRFD使用了特征金字塔网络(FPN),它在不同大小的特征图上进行预测。步长(stride)为8的特征图用来检测小人脸,16的检测中等大小人脸,32的检测大人脸。

    • _num_anchors = 2: 在特征图的每个位置,模型会预设2个不同尺寸的“锚点框”(anchor),并基于这些锚点框进行预测。


3. resize_image 方法:图像预处理

def resize_image(self, srcimg):
    # ... (代码)

这个函数负责将你输入的任意尺寸的原始图像 (srcimg) 转换成模型需要的 640x640 格式。

  • 如果 self.keep_ratio 为 True (默认):

    1. 计算原始图像的宽高比。

    2. 将图像等比例缩放,使其最长边等于640。

    3. 为了补足到 640x640,在较短的那一边上下或左右填充黑边 (cv2.copyMakeBorder)。

    4. 返回处理后的图像 (img) 以及填充的尺寸信息 (padh, padw),这些信息在后续恢复坐标时至关重要。

  • 如果 self.keep_ratio 为 False:

    • 它会粗暴地将图像直接拉伸或压缩到 640x640,这可能会导致人脸变形。


4. distance2bbox 和 distance2kps 方法:解码模型输出

def distance2bbox(self, points, distance, max_shape=None):
    # ... (代码)
def distance2kps(self, points, distance, max_shape=None):
    # ... (代码)

SCRFD模型的一个特点是,它不直接预测边界框的坐标 (x1, y1, x2, y2)。相反,它预测的是:

  • 对于每个“锚点”,其中心到真实人脸边界框四条边的距离。

  • 对于每个“锚点”,其中心到真实人脸每个关键点的位移。

这两个函数就是用来做逆运算的,将模型输出的“距离/位移”解码成我们能理解的“坐标”。

  • distance2bbox: 输入锚点中心坐标 (points) 和模型预测的4个距离值 (distance),计算出最终的边界框坐标 [x1, y1, x2, y2]。

  • distance2kps: 输入锚点中心坐标 (points) 和模型预测的关键点位移 (distance),计算出最终的5个关键点坐标 [x_eye1, y_eye1, x_eye2, y_eye2, ...]。


5. detect 方法:核心检测流程

这是整个类的“主心骨”,它串联起了所有步骤。

def detect(self, srcimg):
    # 步骤1: 图像预处理
    img, newh, neww, padh, padw = self.resize_image(srcimg)
    blob = cv2.dnn.blobFromImage(img, 1.0 / 128, (self.inpWidth, self.inpHeight), (127.5, 127.5, 127.5), swapRB=True)
    
    # 步骤2: 模型推理
    ort_inputs = {self.session.get_inputs()[0].name: blob}
    ort_outs = self.session.run(None, ort_inputs)

    # 步骤3: 结果后处理与解码
    # ... (循环处理不同stride的输出)
    for idx, stride in enumerate(self._feat_stride_fpn):
        # 从模型输出中提取 score, bbox, kps
        # 创建锚点网格
        # 筛选出分数 > confThreshold 的结果
        # 使用 distance2bbox 和 distance2kps 解码
        # ...
    
    # 步骤4: 整合与坐标还原
    # ... (合并所有stride的结果)
    # 将坐标从 640x640 的填充图映射回原始图像
    
    # 步骤5: 非极大值抑制 (NMS)
    # ...
    keep = torch.ops.torchvision.nms(...) # 使用PyTorch的NMS
    
    # 步骤6: 格式化输出
    # ...
    # 将最终保留下来的结果整理成 [box, score, kps] 的格式并返回
    return detections

详细流程分解:

  1. 图像预处理:

    • 调用 resize_image 将图片调整为带黑边填充的 640x640 图像。

    • cv2.dnn.blobFromImage: 这是非常关键的一步。它将图像转换为一个“Blob”,也就是模型需要的4D张量格式 (N, C, H, W)。同时,它还做了两件事:

      • 归一化: 1.0 / 128 和 (127.5, 127.5, 127.5) 将像素值从 [0, 255] 范围转换到 [-1, 1] 范围。

      • 通道转换: swapRB=True 将OpenCV默认的BGR顺序转换为模型需要的RGB顺序。

  2. 模型推理:

    • 将 blob 作为输入,调用 self.session.run 执行模型推理,得到模型的原始输出 ort_outs。ort_outs 是一个包含多个Numpy数组的列表,分别对应人脸分数、边界框距离和关键点位移。

  3. 结果后处理与解码:

    • 这是一个循环,分别处理来自步长8、16、32的三个特征图的预测结果。

    • 对于每个特征图:

      • 它首先生成该特征图上所有的锚点中心坐标 (anchor_centers)。

      • 然后,它用 self.confThreshold 过滤掉所有置信度低的结果。

      • 最后,对保留下来的高置信度结果,调用 distance2bbox 和 distance2kps 将其解码为实际的边界框和关键点坐标。

  4. 整合与坐标还原:

    • 将三个特征图解码出的所有结果(框、分数、关键点)合并在一起。

    • 坐标还原:这是至关重要的一步。因为之前的预测都是基于640x640的填充图像,现在需要利用 resize_image 返回的 ratioh, ratiow, padh, padw,通过数学计算,将坐标映射回原始输入图像的尺寸和位置。

  5. 非极大值抑制 (NMS):

    • 此时,我们可能有很多重叠的框。torch.ops.torchvision.nms 函数会根据 self.nmsThreshold 剔除冗余的检测框,只为每个真实人脸保留一个得分最高的框。

  6. 格式化输出:

    • 遍历NMS后保留下来的结果索引 keep。

    • 将每个检测结果整理成一个包含 [边界框, 分数, 关键点] 的列表。

    • 返回这个包含所有检测结果的列表。


总结

这个 SCRFD 类是一个高度封装、流程清晰的人脸检测器。其工作流可以总结为:

输入原图 -> 预处理(缩放/填充/归一化) -> 模型推理 -> 解码(将模型输出转为坐标) -> 坐标还原(映射回原图) -> NMS(去重) -> 输出最终结果

你只需要准备好一个 scrfd_..._.onnx 模型文件(已发布在我的资源),然后像下面这样使用它:

import cv2
import numpy as np
import onnxruntime as ort
import torch # NMS需要torch

# 假设你的 SCRFD 类代码保存在 scrfd.py 文件中

# 1. 初始化检测器
model_path = "scrfd_2.5g_kps.onnx" # 你的模型路径
face_detector = SCRFD(onnxmodel=model_path, confThreshold=0.5, nmsThreshold=0.5)

# 2. 读取图片
image = cv2.imread("your_image.jpg")

# 3. 执行检测
detections = face_detector.detect(image)

# 4. 可视化结果
if detections:
    print(f"检测到 {len(detections)} 张人脸。")
    for box, score, kps in detections:
        x1, y1, x2, y2 = box
        # 画框
        cv2.rectangle(image, (x1, y1), (x2, y2), (0, 255, 0), 2)
        # 画关键点
        for i in range(kps.shape[0]):
            cv2.circle(image, (int(kps[i, 0]), int(kps[i, 1])), 2, (0, 0, 255), -1)

# 显示结果
cv2.imshow("Detected Faces", image)
cv2.waitKey(0)
cv2.destroyAllWindows()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

海绵波波107

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值