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仅对置信度阈值后的候选框执行。
运行与验证
- 安装依赖:
pip install onnxruntime opencv-python numpy - 下载ONNX模型至当前目录,准备一张含人脸的测试图
test_face.jpg - 运行:
python scrfd_detect.py - 通过条件:输出图像中每张正脸均有绿色框且框内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)。