当前位置: 首页 > news >正文

人脸检测算法——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. 实现与应用
开源生态
  • 代码仓库:`https://github.com/deepinsight/insightface/tree/master/detection/scrfd。
  • 训练流程
    1. 数据加载:WIDER FACE/MS1M数据集。
    2. NAS搜索:优化Backbone/Neck/Head计算比例。
    3. 量化训练:FP16/INT8转换提升推理速度。
  • 部署示例
    # FastDeploy示例[[223]]
    from fastdeploy.vision import SCRFD
    model = SCRFD("scrfd_10g_fp32.onnx")
    results = model.predict(image)
    
论文资源
  • 核心文献
    Sample and Computation Redistribution for Efficient Face Detection(arXiv:2105.04714, 2021)。
  • 扩展研究
    后续改进融入Transformer架构及遥感检测。

结论

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].nameself.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 outputsclass SCRFD():def __init__(self, onnxmodel, confThreshold=0.5, nmsThreshold=0.5):self.inpWidth = 640self.inpHeight = 640self.confThreshold = confThresholdself.nmsThreshold = nmsThresholdself.session = ort.InferenceSession(onnxmodel, providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])self.keep_ratio = Trueself.fmc = 3self._feat_stride_fpn = [8, 16, 32]self._num_anchors = 2def resize_image(self, srcimg):padh, padw, newh, neww = 0, 0, self.inpHeight, self.inpWidthif 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 borderelse:newh, neww = int(self.inpHeight * hw_scale) + 1, self.inpWidthimg = 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, padwdef 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输入通常是NCHWort_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] * stridekps_preds = outs[idx + self.fmc * 2][0] * strideheight = blob.shape[2] // stridewidth = blob.shape[3] // strideanchor_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] / newwbboxes[:, 0] = (bboxes[:, 0] - padw) * ratiowbboxes[:, 1] = (bboxes[:, 1] - padh) * ratiohbboxes[:, 2] = bboxes[:, 2] * ratiowbboxes[:, 3] = bboxes[:, 3] * ratiohkpss[:, :, 0] = (kpss[:, :, 0] - padw) * ratiowkpss[:, :, 1] = (kpss[:, :, 1] - padh) * ratioh# NMS# 可用PyTorch或onnxruntime自带的NMS,也可以保留cv2.dnn.NMSBoxesif 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 = 640self.inpHeight = 640# 2. 设置阈值self.confThreshold = confThreshold  # 置信度阈值self.nmsThreshold = nmsThreshold    # NMS阈值# 3. 加载ONNX模型self.session = ort.InferenceSession(onnxmodel, providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])# 4. 设置模型相关参数self.keep_ratio = Trueself.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()
http://www.lryc.cn/news/592009.html

相关文章:

  • Ubuntu系统下快速体验iperf3工具(网络性能测试)
  • 2G和3G网络关闭/退网状态(截止2025年7月)
  • Python技术题1
  • 【RK3576】【Android14】开发环境搭建
  • 基于现代R语言【Tidyverse、Tidymodel】的机器学习方法与案例分析
  • 用 React-Three-Fiber 实现雪花下落与堆积效果:从零开始的 3D 雪景模拟
  • 前端迟迟收不到响应,登录拦截器踩坑!
  • 小结:Spring MVC 的 XML 的经典配置方式
  • ASP.NET Core Web API 内存缓存(IMemoryCache)入门指南
  • untiy之导入插件(文件方式,适用于git克隆失败)
  • Instagram千号矩阵:亚矩阵云手机破解设备指纹检测的终极方案
  • 【.net core】支持通过属性名称索引的泛型包装类
  • 五国联动!德法意西荷 ASIN 同步上架成泛欧计划硬性门槛
  • 构建智能客服Agent:从需求分析到生产部署
  • 持续同调文章阅读(四)
  • 推荐 1 款 4.5k stars 的AI 大模型驱动的开源知识库搭建系统
  • A33-vstar笔记及资料分享:搭建交叉编译环境
  • Linux云计算基础篇(9)-文本处理工具和变量
  • 无符号乘法运算的硬件逻辑实现 ————取自《湖科大教书匠》
  • 【PTA数据结构 | C语言版】多叉堆的上下调整
  • Python MP3 归一化器和长度分割器实用工具开发指南
  • SQL映射文件
  • Android 应用保活思路
  • 树(Tree)
  • 【C++基础】--多态
  • web域名解析
  • 信息论至AI实践:交叉熵的原理全景与应用深度解析
  • Github库镜像到本地私有Gitlab服务器
  • 您的企业需要服务台经理吗?-ManageEngine卓豪
  • 《5分钟开发订单微服务!飞算JavaAI实战:IDEA插件安装→空指针修复→K8s部署全流程》