3. 轻量级人脸检测与关键点定位

任务目标

在嵌入式设备(如RK3588、Jetson Orin)上,使用轻量级人脸检测网络SCRFD实现人脸框与5点关键点(双眼、鼻尖、双嘴角)的联合输出,并完成模型转换与推理速度优化。核心指标:输入分辨率640×640时,单帧推理耗时控制在30ms以内(以实际硬件为准)。

环境与依赖

  • Python 3.8+ / C++11(部署端)
  • PyTorch 1.9+(仅用于模型导出)
  • ONNX Runtime 1.14+ 或 RKNN-Toolkit2(RK3588)
  • OpenCV 4.x(图像预处理与后处理)

模型与权重准备

SCRFD(ICLR 2021)为单阶段anchor-free检测器,主干使用MobileNetV3或ResNet50(本案例选用MobileNetV3-small变体)。权重文件从官方仓库(insightface/scrfd)下载 scrfd_500m_bnkps_shape640x640.onnx,该模型同时输出检测框、置信度与5点关键点。

关键完整代码(ONNX Runtime CPU/GPU部署)

import cv2
import numpy as np
import onnxruntime as ort

class SCRFDDetector:
    def __init__(self, onnx_path, input_size=(640, 640), conf_thres=0.5, nms_thres=0.4):
        self.input_size = input_size
        self.conf_thres = conf_thres
        self.nms_thres = nms_thres
        so = ort.SessionOptions()
        so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
        so.intra_op_num_threads = 4  # 嵌入式多核优化
        self.session = ort.InferenceSession(onnx_path, so, providers=['CPUExecutionProvider'])
        self.input_name = self.session.get_inputs()[0].name
        # SCRFD输出: scores(B,1,H,W), bboxes(B,4,H,W), kps(B,10,H,W)
        self.output_names = [o.name for o in self.session.get_outputs()]

    def preprocess(self, img_bgr):
        img = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
        h, w = img.shape[:2]
        scale = min(self.input_size[0] / h, self.input_size[1] / w)
        new_h, new_w = int(h * scale), int(w * scale)
        img = cv2.resize(img, (new_w, new_h))
        canvas = np.zeros((self.input_size[0], self.input_size[1], 3), dtype=np.uint8)
        canvas[:new_h, :new_w] = img
        # 归一化至[-1,1]
        tensor = (canvas.astype(np.float32) / 255.0 - 0.5) / 0.5
        tensor = tensor.transpose(2, 0, 1)[None, ...]
        return tensor, scale, (new_w, new_h)

    def postprocess(self, outputs, scale, pad_size):
        scores = outputs[0][0, 0]  # (H,W)
        bboxes = outputs[1][0]     # (4,H,W)
        kps = outputs[2][0]        # (10,H,W)
        H, W = scores.shape
        stride = self.input_size[0] / H
        anchors = np.mgrid[0:H, 0:W].transpose(1, 2, 0) * stride + stride / 2

        score_mask = scores > self.conf_thres
        ys, xs = np.where(score_mask)
        if len(xs) == 0:
            return []

        boxes = []
        keypoints = []
        confs = []
        for x, y in zip(xs, ys):
            conf = scores[y, x]
            # 解码中心点+宽高
            cx = anchors[y, x, 1] + bboxes[0, y, x] * stride
            cy = anchors[y, x, 0] + bboxes[1, y, x] * stride
            bw = bboxes[2, y, x] * stride
            bh = bboxes[3, y, x] * stride
            x1 = cx - bw / 2
            y1 = cy - bh / 2
            x2 = cx + bw / 2
            y2 = cy + bh / 2
            # 关键点解码 (10个值: x0,y0,...,x4,y4)
            kp = np.zeros((5, 2))
            for i in range(5):
                kp[i, 0] = anchors[y, x, 1] + kps[2*i, y, x] * stride
                kp[i, 1] = anchors[y, x, 0] + kps[2*i+1, y, x] * stride
            boxes.append([x1, y1, x2, y2])
            keypoints.append(kp)
            confs.append(conf)

        boxes = np.array(boxes)
        keypoints = np.array(keypoints)
        confs = np.array(confs)

        # NMS
        keep = self._nms(boxes, confs, self.nms_thres)
        results = []
        for idx in keep:
            # 坐标映射回原图
            x1, y1, x2, y2 = boxes[idx] / scale
            kp = keypoints[idx] / scale
            results.append({
                'box': [x1, y1, x2, y2],
                'score': float(confs[idx]),
                'kps': kp.tolist()
            })
        return results

    def _nms(self, boxes, scores, thres):
        x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
        areas = (x2 - x1) * (y2 - y1)
        order = scores.argsort()[::-1]
        keep = []
        while order.size > 0:
            i = order[0]
            keep.append(i)
            xx1 = np.maximum(x1[i], x1[order[1:]])
            yy1 = np.maximum(y1[i], y1[order[1:]])
            xx2 = np.minimum(x2[i], x2[order[1:]])
            yy2 = np.minimum(y2[i], y2[order[1:]])
            inter = np.maximum(0, xx2 - xx1) * np.maximum(0, yy2 - yy1)
            iou = inter / (areas[i] + areas[order[1:]] - inter)
            order = order[1:][iou <= thres]
        return keep

    def detect(self, img_bgr):
        tensor, scale, pad_size = self.preprocess(img_bgr)
        outputs = self.session.run(self.output_names, {self.input_name: tensor})
        return self.postprocess(outputs, scale, pad_size)

# 使用示例
if __name__ == '__main__':
    detector = SCRFDDetector('scrfd_500m_bnkps_shape640x640.onnx')
    img = cv2.imread('test_face.jpg')
    faces = detector.detect(img)
    for face in faces:
        x1, y1, x2, y2 = map(int, face['box'])
        cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2)
        for px, py in face['kps']:
            cv2.circle(img, (int(px), int(py)), 2, (0, 0, 255), -1)
    cv2.imwrite('result.jpg', img)
    print(f'检测到 {len(faces)} 张人脸')

推理速度优化要点

  • ONNX Runtime线程配置:intra_op_num_threads=4匹配嵌入式CPU核心数,开启ORT_ENABLE_ALL图优化。
  • 输入分辨率:640×640为精度/速度平衡点;若需更快可降至320×320(精度下降约2% mAP)。
  • RK3588 NPU加速:使用RKNN-Toolkit2将ONNX转为RKNN格式,NPU推理耗时约8-12ms(640×640)。转换命令:python tools/rknn_convert.py --onnx scrfd_500m.onnx --output scrfd.rknn --target rk3588
  • 后处理优化:上述代码使用向量化NumPy操作替代Python循环,避免逐像素遍历;NMS仅对置信度阈值后的候选框执行。

运行与验证

  1. 安装依赖:pip install onnxruntime opencv-python numpy
  2. 下载ONNX模型至当前目录,准备一张含人脸的测试图test_face.jpg
  3. 运行:python scrfd_detect.py
  4. 通过条件:输出图像中每张正脸均有绿色框且框内5个红色关键点(双眼、鼻尖、嘴角)位置基本准确(误差≤5像素);终端打印检测数量与实际人脸数一致。
  5. 速度验证:在代码中循环执行detect()50次,取平均耗时。CPU(4核ARM A76@2.4GHz)应≤30ms/帧;若使用RKNN NPU应≤15ms/帧。

常见问题

  • 输出全为零:检查输入归一化是否为[-1,1](而非[0,1]),SCRFD训练时采用此范围。
  • 关键点偏移:确认后处理中关键点解码的stride与bbox一致,且坐标除以scale时使用同一缩放因子。
  • 模型输出顺序:不同版本SCRFD输出顺序可能为(scores, bboxes, kps)或(bboxes, scores, kps),需通过session.get_outputs()的shape确认(scores为1×1×H×W)。
SCRFD轻量级人脸检测与关键点定位流程 输入图像 任意尺寸 BGR 预处理 Resize + Pad至640×640 归一化[-1,1] + RGB SCRFD-MobileNetV3 共享特征提取 3个输出头 scores / bboxes / kps 后处理 阈值过滤 NMS + 坐标映射 输出结果 人脸框+5关键点 速度优化 • ONNX Runtime线程/图优化 • RKNN NPU转换 • 输入分辨率缩放 5点关键点定义 0 1 2 3 4