39 KiB
39 KiB
车间人脸识别系统 - 三分区距离检测方案 v2.3
文档版本: v2.3 适用场景: 4-6米安装高度,3-9米检测距离,SCRFD_640 + 三分区 目标平台: RK3588 (6TOPS NPU) 检测模型: RetinaFace-MobileNetV3 320×320(先用现有模型跑通) 姿态估计: 5点关键点近似 更新日期: 2026-03-10
注意: v2.3使用现有face_det_scrfd_500m_640_rk3588.rknn,如需更高精度可升级到640模型
1. 方案概述
1.1 设计目标
针对车间环境远距离人脸识别需求,本方案在现有插件化架构基础上,引入距离分区检测机制:
- 检测范围: 3~8米(放弃10米极端距离,保证检测可靠性)
- 核心策略: 以6米(相机对焦距离)为中心,将画面分为远近两个检测区
- 算力优化: ROI裁剪 + 自适应缩放,节省约60%算力
- 姿态过滤: 利用现有5点关键点,过滤过度低头情况
1.2 核心特性
| 特性 | 实现方式 | 预期收益 |
|---|---|---|
| SCRFD_640 | 320×320输入(现有模型) | 快速部署,5-7米检测率>90% |
| ROI裁剪 | 基于3-9米距离范围裁剪画面 | 节省40-50%算力 |
| 三分区检测 | 近区1.0x,中区1.3x,远区1.8x | 目标人脸20-40px(320最佳范围) |
| 距离过滤 | 像素y坐标→距离映射,过滤范围外人脸 | 减少误检 |
| 姿态过滤 | 5点关键点估计俯仰角,过滤<-15° | 提升识别准确率,无需额外模型 |
| 独立标定 | 每相机独立Python标定脚本 | 适配不同安装高度/角度 |
| 可升级 | 320→640模型 | 预留接口,后续无缝升级 |
1.3 非目标(明确排除)
为控制复杂度,以下功能不在本版本范围内:
- ❌ 9米以上超远距离(320模型在此距离检测率<50%)
- ❌ PFLD独立姿态估计模型(使用5点关键点近似已足够)
- ❌ RetinaFace_640模型(v2.4版本升级,当前先用320跑通)
- ❌ Batch推理优化
- ❌ 自动在线标定
- ❌ 畸变校正(假设畸变较小或可忽略)
2. 系统架构
2.1 数据流架构
┌─────────────────────────────────────────────────────────────┐
│ 每路相机独立配置(configs/zone_a/cam_001.json) │
├─────────────────────────────────────────────────────────────┤
│ │
│ [input_rtsp] ──→ [preprocess] ──→ [ai_face_det] ──→ [ai_face_recog] │
│ │ │ │
│ ROI裁剪(节省算力) 双分区检测 │
│ 距离估算 │
│ ↓ │
│ [osd]/[publish] │
│ │
└─────────────────────────────────────────────────────────────┘
2.2 三分区检测原理
画面垂直方向(y坐标,2560×1440示例):
┌─────────────────────────────┐ y=0 (画面顶部)
│ │
│ 3-5米 (近区) │ ← scale=1.0x
│ 人脸88-117px │ 原图处理
│ 320输入: 27-36px │ (320模型最佳范围)
│ ↑ │
│ y = boundary_5m │ 5米分界线
│ ↓ │
│ 5-7米 (中区) │ ← scale=1.3x
│ 人脸63-88px │ 放大后处理
│ 320输入: 25-35px │ (对焦最佳)
│ ↑ │
│ y = boundary_7m │ 7米分界线
│ ↓ │
│ 7-9米 (远区) │ ← scale=1.8x
│ 人脸49-63px │ 大幅放大补偿
│ 320输入: 27-35px │ (9米可能降至60%检测率)
│ │
└─────────────────────────────┘ y=1440 (画面底部)
↑
ROI裁剪边界 (y_min ~ y_max)
只保留3-9米对应画面区域
目标: 三个区处理后的人脸在320输入中占25-35px(320模型最佳)
2.3 距离-像素映射
基于针孔相机模型,预计算LUT表实现O(1)查询:
距离 D → 像素 y:
y = cy + f × tan(arctan(H/D) - θ)
像素 y → 距离 D:
D = H / tan(θ + arctan((y-cy)/f))
其中:
- H: 安装高度(米)
- θ: 俯仰角(弧度)
- f: 像素焦距(像素)
- cy: 主点y坐标(通常img_h/2)
3. 相机模型设计
3.1 简化相机模型类
// include/utils/camera_model.h
#pragma once
#include <vector>
namespace rk3588 {
struct CameraModelParams {
float height; // 安装高度(米)
float pitch_deg; // 俯仰角(度)
float focal_px; // 像素焦距(像素)
int img_w, img_h; // 图像尺寸
int cx, cy; // 主点坐标
float min_dist = 3.0f; // 最小检测距离
float max_dist = 8.0f; // 最大检测距离
};
class SimpleCameraModel {
public:
explicit SimpleCameraModel(const CameraModelParams& p);
// O(1)距离查询(核心功能)
float PixelToDistance(int y) const;
// 计算ROI裁剪区域
struct ROI { int x, y, w, h; float saving_ratio; };
ROI ComputeRoi() const;
// 计算分区线像素位置
int GetZoneSplitY(float split_distance = 6.0f) const;
// 估算人脸像素大小(用于验证)
float EstimateFacePixelSize(float distance,
float real_face_width = 0.16f) const;
private:
CameraModelParams p_;
std::vector<float> distance_lut_; // 预计算查找表
void BuildLut();
};
} // namespace rk3588
3.2 实现要点
- LUT预计算: 初始化时计算整张图的距离表(img_h个float,约5-10KB)
- 无三角函数运行时计算: 查询时直接查表
- 边界处理: y越界时返回inf,由调用方处理
4. 分区检测策略
4.1 检测节点扩展(SCRFD_640)
ai_face_det 节点配置更新:
{
"id": "face_det",
"type": "ai_face_det",
"model_path": "./models/face_det_scrfd_500m_640_rk3588.rknn",
"conf": 0.6,
"nms": 0.4,
"max_faces": 10,
"output_landmarks": true,
"input_format": "rgb",
"model_w": 320, // 320×320输入
"model_h": 320,
"distance_zones": {
"enabled": true,
"boundaries": [416, 672], // 5米和7米分界线y坐标
"scales": [1.0, 1.3, 1.8] // 320模型:放大为主,目标25-35px
}
}
模型现状:
- ✅ 已有:
face_det_scrfd_500m_640_rk3588.rknn(1.6MB) - ✅ 输入:320×320,5点关键点输出
- ⚠️ 限制:8-9米检测率可能60-70%(可先跑通功能)
- 🔄 升级:v2.4无缝切换到640模型(只需改model_path和scales)
4.2 检测流程(三分区)
void DetectWithZones(FramePtr frame) {
if (!zone_cfg_.enabled) {
RunSingleScale(frame); // 回退到原有逻辑
return;
}
const int h = frame->height;
const int y_5m = zone_cfg_.boundaries[0]; // 5米分界线
const int y_7m = zone_cfg_.boundaries[1]; // 7米分界线
Detections all_dets;
// 近区检测 (3-5米,画面上部) - 320模型:1.0x
if (y_5m > 0) {
Mat roi = Crop(frame, 0, 0, w, y_5m);
Mat scaled;
resize(roi, scaled, Size(roi.w*1.0, roi.h*1.0)); // 原图处理
auto dets = RunInference(scaled);
MapBack(dets, scale=1.0, offset_y=0);
all_dets.insert(all_dets.end(), dets.begin(), dets.end());
}
// 中区检测 (5-7米,画面中部) - 320模型:1.3x放大
if (y_7m > y_5m) {
Mat roi = Crop(frame, 0, y_5m, w, y_7m - y_5m);
Mat scaled;
resize(roi, scaled, Size(roi.w*1.3, roi.h*1.3)); // 放大补偿
auto dets = RunInference(scaled);
MapBack(dets, scale=1.3, offset_y=y_5m);
all_dets.insert(all_dets.end(), dets.begin(), dets.end());
}
// 远区检测 (7-9米,画面下部) - 320模型:1.8x大幅放大
if (y_7m < h) {
Mat roi = Crop(frame, 0, y_7m, w, h - y_7m);
Mat scaled;
resize(roi, scaled, Size(roi.w*1.8, roi.h*1.8)); // 大幅放大
auto dets = RunInference(scaled);
MapBack(dets, scale=1.8, offset_y=y_7m);
all_dets.insert(all_dets.end(), dets.begin(), dets.end());
}
// NMS去重(三区可能有重叠)
Nms(all_dets);
frame->face_det = make_shared<FaceDetResult>(all_dets);
}
4.3 缩放因子选择(320模型)
| 分区 | 距离 | 原始人脸 | 缩放 | 320输入 | 占比 | 检测率预期 |
|---|---|---|---|---|---|---|
| 近区 | 3-5m | 88-117px | 1.0x | 88-117px→320 | 27-37% | >90% |
| 中区 | 5-7m | 63-88px | 1.3x | 82-114px→320 | 26-36% | >90% |
| 远区 | 7-9m | 49-63px | 1.8x | 88-113px→320 | 28-35% | 70-80% |
注:
- SCRFD_640最佳检测范围:25-45px(占320输入的8-14%)
- 实际处理后:27-37px,在最佳范围内
- 9米处检测率可能降至70%,如不满足可升级到640模型
- 升级到640只需改:model_path + scales改为[0.7, 1.0, 1.4]
5. 距离与姿态过滤
5.1 识别节点扩展(5点关键点姿态估计)
{
"id": "face_recog",
"type": "ai_face_recog",
"model_path": "./models/face_recog_mobilefacenet_arcface_112_rk3588.rknn",
"align": true,
"gallery": {
"backend": "sqlite",
"path": "./models/face_gallery.db"
},
"filters": {
"distance": {
"enabled": true,
"min": 3.0,
"max": 9.0
},
"pose": {
"enabled": true,
"min_pitch": -15, // 真实仰角低于-15度不识别
"camera_pitch": 45, // 相机俯仰角(标定参数)
"use_landmarks": true // 使用RetinaFace输出的5点关键点
}
}
}
5.2 过滤逻辑
void ApplyFilters(FaceRecogItem& item, const FaceDetItem& det) {
// 1. 距离过滤
if (filters_.distance.enabled && camera_model_) {
int center_y = det.bbox.y + det.bbox.h / 2;
float dist = camera_model_->PixelToDistance(center_y);
if (dist < filters_.distance.min || dist > filters_.distance.max) {
item.unknown = true;
item.best_name = "out_of_range";
return;
}
item.distance = dist; // 记录距离供后续使用
}
// 2. 姿态过滤(5点关键点近似)
if (filters_.pose.enabled && det.has_landmarks) {
float face_pitch = EstimatePitch(det.landmarks);
float real_pitch = face_pitch - filters_.pose.camera_pitch;
if (real_pitch < filters_.pose.min_pitch) {
item.unknown = true;
item.best_name = "low_head";
return;
}
}
}
// 俯仰角估计:鼻尖相对于眼睛中心的垂直偏移
float EstimatePitch(const array<Point2f, 5>& lm) {
float eye_y = (lm[0].y + lm[1].y) / 2; // 左右眼中心
float nose_y = lm[2].y; // 鼻尖
float eye_dist = abs(lm[1].x - lm[0].x);
if (eye_dist < 1.0f) return 0.0f;
float dy = nose_y - eye_y;
return atan2(dy, eye_dist) * 180.0f / M_PI;
}
6. 标定工具设计
6.1 工具定位
- 独立运行: 不耦合主系统,手工执行
- 输出参考: 生成推荐配置值,供手工复制到配置文件
- 批量支持: 可选批量生成多路相机配置
6.2 使用方法
# 单相机标定(三分区)
python tools/calibrate_camera.py \
--height 5.0 \
--pitch 45 \
--focal-estimate 2200 \
--image-size 2560 1440 \
--range 3.0 9.0 \
--zones 3 5 7 9 \
--scales 0.7 1.0 1.4 \
--output configs/calibrations/cam_zone_a_001.json \
--report
# 输出示例
# [INFO] ROI: y=240, h=880 (saving: 38.9%)
# [INFO] Zone boundaries: y=416 (5m), y=672 (7m)
# [INFO] 近区(3-5m): scale=0.7x
# [INFO] 中区(5-7m): scale=1.0x
# [INFO] 远区(7-9m): scale=1.4x
# [INFO] Config saved to cam_zone_a_001.json
6.3 输出配置片段(三分区)
工具输出JSON格式配置,可直接复制到主配置:
{
"preprocess": {
"roi": {
"enabled": true,
"crop": {"x": 0, "y": 240, "w": 2560, "h": 880}
}
},
"face_det": {
"distance_zones": {
"enabled": true,
"boundaries": [416, 672],
"zones": [
{
"name": "near",
"distance_range": [3, 5],
"scale": 0.7,
"y_range": [0, 416]
},
{
"name": "mid",
"distance_range": [5, 7],
"scale": 1.0,
"y_range": [416, 672]
},
{
"name": "far",
"distance_range": [7, 9],
"scale": 1.4,
"y_range": [672, 1440]
}
]
}
},
"face_recog": {
"filters": {
"distance": {"enabled": true, "min": 3.0, "max": 9.0},
"pose": {"enabled": true, "min_pitch": -15, "camera_pitch": 45}
}
},
"calibration_params": {
"height": 5.0,
"pitch": 45.0,
"focal_px": 2200,
"zones": "3-5m(0.7x), 5-7m(1.0x), 7-9m(1.4x)"
}
}
6.4 焦距估算方法
若无法精确测量焦距,可用以下方法估算:
方法1: 公式估算
f ≈ (sensor_width_mm / image_width_px) * focal_length_mm
例如: 1/2.8"传感器(5.6mm宽), 4mm镜头, 2560px
f ≈ (5.6 / 2560) * 4 ≈ 2200 px
方法2: 现场测量反推
在已知距离D处测量人脸像素宽度W
f = W * D / 0.16
例如: 6米处人脸100px
f = 100 * 6 / 0.16 = 3750 px
建议: 先用方法1估算,再用方法2验证,偏差较大时以方法2为准。
7. 配置文件集成方案
7.1 配置结构说明
项目使用统一配置文件,支持两种模式:
templates+instances模式:模板定义流水线,实例传入参数graphs直接模式:直接定义完整的节点和边
7.2 Templates + Instances 模式(推荐用于多相机)
{
"global": {
"metrics_port": 9000,
"web_root": "web"
},
"queue": { "size": 8, "strategy": "drop_oldest" },
"templates": {
"face_recog_distanced_pipeline": {
"nodes": [
{
"id": "in",
"type": "input_rtsp",
"role": "source",
"enable": true,
"url": "${url}",
"fps": 30,
"width": 2560,
"height": 1440
},
{
"id": "pre",
"type": "preprocess",
"role": "filter",
"enable": true,
// ROI裁剪参数(由标定工具生成,通过params传入)
"roi": {
"enabled": "${roi_enabled}",
"crop": {
"x": 0,
"y": "${roi_y}",
"w": 2560,
"h": "${roi_h}"
}
},
"dst_w": 1280,
"dst_h": 720,
"dst_format": "rgb"
},
{
"id": "face_det",
"type": "ai_face_det",
"role": "filter",
"enable": true,
"model_path": "./models/face_det_scrfd_500m_640_rk3588.rknn",
"conf": 0.6,
"nms": 0.4,
"max_faces": 10,
"output_landmarks": true,
// 三分区检测参数(通过params传入)
"distance_zones": {
"enabled": "${zones_enabled}",
"boundaries": ["${zone_boundary_5m}", "${zone_boundary_7m}"],
"scales": ["${zone_scale_near}", "${zone_scale_mid}", "${zone_scale_far}"]
}
},
{
"id": "face_recog",
"type": "ai_face_recog",
"role": "filter",
"enable": true,
"model_path": "./models/face_recog_mobilefacenet_arcface_112_rk3588.rknn",
"align": true,
"gallery": {
"backend": "sqlite",
"path": "${face_gallery_path}"
},
// 距离和姿态过滤参数
"filters": {
"distance": {
"enabled": true,
"min": 3.0,
"max": 9.0
},
"pose": {
"enabled": true,
"min_pitch": -15,
"camera_pitch": "${camera_pitch}"
}
}
},
{
"id": "osd",
"type": "osd",
"role": "filter",
"enable": true,
"draw_face_det": true,
"draw_face_recog": true
},
{
"id": "pub",
"type": "publish",
"role": "sink",
"enable": true,
"outputs": [{ "proto": "rtsp_server", "port": "${rtsp_port}", "path": "/live/${name}" }]
}
],
"edges": [
["in", "pre"],
["pre", "face_det"],
["face_det", "face_recog"],
["face_recog", "osd"],
["osd", "pub"]
]
}
},
// 30+路相机实例,每路传入不同的标定参数
"instances": [
{
"name": "workshop_zoneA_cam01",
"template": "face_recog_distanced_pipeline",
"params": {
"name": "zoneA_cam01",
"url": "rtsp://192.168.1.101/stream1",
"rtsp_port": 8554,
"face_gallery_path": "./models/face_gallery.db",
// 相机安装参数
"camera_pitch": 45,
// ROI参数(标定工具生成)
"roi_enabled": true,
"roi_y": 240,
"roi_h": 880,
// 三分区参数(标定工具生成)
"zones_enabled": true,
"zone_boundary_5m": 416,
"zone_boundary_7m": 672,
"zone_scale_near": 0.7,
"zone_scale_mid": 1.0,
"zone_scale_far": 1.4
}
},
{
"name": "workshop_zoneA_cam02",
"template": "face_recog_distanced_pipeline",
"params": {
"name": "zoneA_cam02",
"url": "rtsp://192.168.1.102/stream1",
"rtsp_port": 8555,
"face_gallery_path": "./models/face_gallery.db",
// 不同安装高度和角度,不同标定参数
"camera_pitch": 42,
"roi_enabled": true,
"roi_y": 280,
"roi_h": 820,
"zones_enabled": true,
"zone_boundary_5m": 432,
"zone_boundary_7m": 688,
"zone_scale_near": 0.7,
"zone_scale_mid": 1.0,
"zone_scale_far": 1.4
}
}
// ... 更多相机实例
]
}
7.3 Graphs 直接模式(单相机测试)
{
"queue": { "size": 8, "strategy": "drop_oldest" },
"graphs": [
{
"name": "cam1_face_recog_distanced",
"nodes": [
{
"id": "in_cam1",
"type": "input_rtsp",
"role": "source",
"enable": true,
"url": "rtsp://192.168.1.101/stream1",
"width": 2560,
"height": 1440
},
{
"id": "pre_cam1",
"type": "preprocess",
"role": "filter",
"enable": true,
"roi": {
"enabled": true,
"crop": { "x": 0, "y": 240, "w": 2560, "h": 880 }
},
"dst_w": 1280,
"dst_h": 720,
"dst_format": "rgb"
},
{
"id": "face_det_cam1",
"type": "ai_face_det",
"role": "filter",
"enable": true,
"model_path": "./models/face_det_scrfd_500m_640_rk3588.rknn",
"conf": 0.6,
"nms": 0.4,
"max_faces": 10,
"output_landmarks": true,
"distance_zones": {
"enabled": true,
"boundaries": [416, 672],
"scales": [0.7, 1.0, 1.4]
}
},
{
"id": "face_recog_cam1",
"type": "ai_face_recog",
"role": "filter",
"enable": true,
"model_path": "./models/face_recog_mobilefacenet_arcface_112_rk3588.rknn",
"align": true,
"gallery": {
"backend": "sqlite",
"path": "./models/face_gallery.db"
},
"filters": {
"distance": { "enabled": true, "min": 3.0, "max": 9.0 },
"pose": { "enabled": true, "min_pitch": -15, "camera_pitch": 45 }
}
},
{
"id": "osd_cam1",
"type": "osd",
"role": "filter",
"enable": true,
"draw_face_det": true,
"draw_face_recog": true
},
{
"id": "pub_cam1",
"type": "publish",
"role": "sink",
"enable": true,
"outputs": [
{ "proto": "rtsp_server", "port": 8554, "path": "/live/cam1" }
]
}
],
"edges": [
["in_cam1", "pre_cam1"],
["pre_cam1", "face_det_cam1"],
["face_det_cam1", "face_recog_cam1"],
["face_recog_cam1", "osd_cam1"],
["osd_cam1", "pub_cam1"]
]
}
]
}
8. 部署与验证流程
8.1 部署流程(统一配置文件)
Step 1: 准备统一配置文件(configs/workshop_face_recog.json)
├─ 定义 template "face_recog_distanced_pipeline"
└─ 预留 ${params} 占位符
Step 2: 为每路相机运行标定工具
├─ python tools/calibrate_camera.py --height 5.0 --pitch 45 ...
└─ 记录输出的 params 参数
Step 3: 在统一配置文件的 instances 中添加相机
{
"name": "workshop_zoneA_cam01",
"template": "face_recog_distanced_pipeline",
"params": {
"name": "zoneA_cam01",
"url": "rtsp://...",
// 复制标定工具输出的 params 到这里
"camera_pitch": 45,
"roi_y": 240,
"roi_h": 880,
"zone_boundary_5m": 416,
...
}
}
Step 4: 现场验证
├─ 在3米、5米、7米、9米处站立测试人员
├─ 检查检测框是否正常
├─ 检查距离估算是否准确
└─ 如有偏差,调整该相机的 focal_px 重新标定
Step 5: 批量部署
└─ 重复Step 2-4为所有30+路相机添加 instances
8.2 验证检查清单
| 检查项 | 方法 | 通过标准 |
|---|---|---|
| ROI裁剪范围 | 观察OSD输出 | 3-9米范围人脸可见,9米外被裁 |
| 5米分界线 | 站在5米处观察 | y坐标应与zone_boundary_5m匹配 |
| 7米分界线 | 站在7米处观察 | y坐标应与zone_boundary_7m匹配 |
| 近区检测 | 站在4米处 | 人脸25-35px(320输入),检测率>90% |
| 中区检测 | 站在6米处 | 人脸25-35px(320输入),检测率>90% |
| 远区检测 | 站在8米处 | 人脸25-35px(320输入),检测率~80% |
| 远区极限 | 站在9米处 | 人脸~25px,检测率~70%(可接受或升级640) |
| 距离估算 | 对比激光测距 | 误差<0.3米 |
| 姿态过滤 | 低头 vs 抬头 | 过度低头(-15°以下)标记为low_head |
9. 性能预期
9.1 理论计算(SCRFD_640)
| 优化项 | 参数 | 实际收益 |
|---|---|---|
| SCRFD_640 | 320×320输入(现有模型) | 快速部署,5-7米检测率>90% |
| ROI裁剪 | ~40%算力节省 | 单路NPU占用从20%→12% |
| 三分区检测 | 各区域25-35px | 全范围检测率80-90%(9米可能70%) |
| 距离过滤 | 3-9米范围 | 减少范围外误检 |
| 姿态过滤 | 5点关键点 | 误识率降低50%,无需额外模型 |
| 可升级 | 320→640 | 预留接口,后续无缝升级 |
9.2 资源占用估算(320模型)
- 单路NPU占用: ~15-20%(SCRFD_640 + MobileFaceNet)
- SCRFD_640: ~8-10ms/帧(640的1/4)
- MobileFaceNet: ~10-15ms/人
- 8台RK3588承载: 每台5-6路(320模型轻量)
- 内存占用:
- 相机模型LUT: ~10KB/路
- 三分区检测缓冲: ~3×320×320×3 ≈ 0.9MB/路
- 延迟: 三分区检测约增加8-10ms
注: 320模型计算量小,快速部署验证功能。如8-9米检测率不满足,可无缝升级到640。
10. 风险评估与回退方案
10.1 潜在风险与缓解
| 风险 | 可能性 | 影响 | 缓解措施 |
|---|---|---|---|
| 8-9米检测率不足 | 中 | 320模型远距离检测能力有限 | 可提高far_scale到2.0x,或升级到640 |
| 标定参数不准 | 中 | 测距误差大 | 提供现场微调指引 |
| 5米/7米分界线不准 | 中 | 分区检测率不均 | 可手工调整boundary_y |
| 姿态估计误差 | 低 | 5点近似±5°误差 | 放宽min_pitch到-20°容忍 |
| 升级到640兼容性 | 低 | 接口预留,已考虑 | 只需改model_path和scales |
10.2 回退方案
若分区检测效果不佳,可快速回退:
{
"face_det": {
"distance_zones": {
"enabled": false // 关闭分区,回退单尺度
}
},
"pre": {
"roi": {
"enabled": false // 关闭ROI,全图检测
}
}
}
11. 模型准备清单
11.1 当前版本模型(v2.3)
| 模型 | 文件名 | 输入尺寸 | 来源 | 状态 |
|---|---|---|---|---|
| SCRFD_640 | face_det_scrfd_500m_640_rk3588.rknn |
320×320 | 项目已有 | ✅ 已有 |
| MobileFaceNet | face_recog_mobilefacenet_arcface_112_rk3588.rknn |
112×112 | 项目已有 | ✅ 已有 |
11.2 升级到640(v2.4预留)
当320模型精度不满足时,无缝升级:
// 只需修改配置
{
"face_det": {
"model_path": "./models/face_det_scrfd_500m_640_rk3588.rknn",
"model_w": 640,
"model_h": 640,
"distance_zones": {
"scales": [0.7, 1.0, 1.4] // 640模型用此参数
}
}
}
640模型获取:
- 方案1:自行转换ONNX到RKNN
- 方案2:寻找社区预转换模型
11.3 320 vs 640 对比
| 指标 | SCRFD_640 | RetinaFace_640 | 建议 |
|---|---|---|---|
| 输入尺寸 | 320×320 | 640×640 | - |
| NPU耗时 | ~8-10ms | ~25-30ms | 320快3倍 |
| 5-7米检测率 | >90% | >95% | 相近 |
| 8-9米检测率 | ~70% | >85% | 640优势明显 |
| 部署难度 | 已有模型 | 需准备 | 先用320 |
11.4 模型验证
# 验证320模型输出
# 预期输出:
# - loc: [1, 16800, 4] (检测框回归)
# - conf: [1, 16800, 2] (人脸/背景分类)
# - landms: [1, 16800, 10] (5点关键点)
附录A: 标定工具完整代码(三分区版)
#!/usr/bin/env python3
"""
calibrate_camera.py - 相机标定工具(三分区版)
生成ROI和三分区参数供手工复制到配置文件
Usage:
python calibrate_camera.py --height 5.0 --pitch 45 -o cam_calib.json --report
"""
import json
import argparse
import numpy as np
from dataclasses import dataclass
from typing import List, Tuple
@dataclass
class CameraParams:
height: float # 安装高度(m)
pitch_deg: float # 俯仰角(°)
focal_px: float # 像素焦距(px)
img_w: int # 图像宽度
img_h: int # 图像高度
class CameraCalibrator:
"""相机标定器 - 支持三分区"""
def __init__(self, p: CameraParams):
self.p = p
self.cy = p.img_h // 2
self.cx = p.img_w // 2
self.theta = np.radians(p.pitch_deg)
self._build_lut()
def _build_lut(self):
"""预计算距离查找表 LUT[y] = distance(m)"""
self.lut = np.zeros(self.p.img_h, dtype=np.float32)
for y in range(self.p.img_h):
dy = y - self.cy
angle = self.theta + np.arctan2(dy, self.p.focal_px)
if abs(angle) > 1e-6 and np.tan(angle) > 0:
self.lut[y] = self.p.height / np.tan(angle)
else:
self.lut[y] = np.inf
def pixel_to_distance(self, y: int) -> float:
"""像素y坐标 -> 距离(m)"""
if 0 <= y < self.p.img_h:
return float(self.lut[y])
return np.inf
def distance_to_pixel(self, d: float) -> int:
"""距离(m) -> 像素y坐标"""
if d <= 0:
return self.cy
angle = np.arctan2(self.p.height, d)
offset = self.p.focal_px * np.tan(angle - self.theta)
return int(np.clip(self.cy + offset, 0, self.p.img_h - 1))
def calculate_roi(self, min_d: float, max_d: float, margin: int = 20) -> dict:
"""计算ROI裁剪区域"""
# 注意:距离越远,y坐标越大(画面下方)
y_min = self.distance_to_pixel(max_d) # 远距在下
y_max = self.distance_to_pixel(min_d) # 近距在上
# 添加边界余量
y_min = max(0, y_min - margin)
y_max = min(self.p.img_h, y_max + margin)
saving = 1 - (y_max - y_min) / self.p.img_h
return {
"crop": {
"x": 0,
"y": y_min,
"w": self.p.img_w,
"h": y_max - y_min
},
"saving_percent": round(saving * 100, 1)
}
def get_zone_boundaries(self, boundaries_m: List[float]) -> List[int]:
"""获取分区边界像素坐标"""
return [self.distance_to_pixel(d) for d in boundaries_m]
def estimate_face_size(self, distance: float, real_width: float = 0.16) -> float:
"""估算给定距离的人脸像素大小"""
if distance <= 0:
return 0
return self.p.focal_px * real_width / distance
def generate_zones_config(self,
boundaries_m: List[float],
scales: List[float]) -> dict:
"""生成分区配置"""
boundaries_y = self.get_zone_boundaries(boundaries_m)
zones = []
zone_names = ["near", "mid", "far"]
for i, (name, scale) in enumerate(zip(zone_names, scales)):
y_start = boundaries_y[i] if i > 0 else 0
y_end = boundaries_y[i] if i < len(boundaries_y) else self.p.img_h
# 如果是最后一个区
if i == len(scales) - 1:
y_end = self.p.img_h
else:
y_end = boundaries_y[i]
# 重新计算正确的y范围
if i == 0:
y_range = [0, boundaries_y[0]]
d_range = [3.0, boundaries_m[0]]
elif i == len(scales) - 1:
y_range = [boundaries_y[-1], self.p.img_h]
d_range = [boundaries_m[-1], 9.0]
else:
y_range = [boundaries_y[i-1], boundaries_y[i]]
d_range = [boundaries_m[i-1], boundaries_m[i]]
zones.append({
"name": name,
"distance_range": d_range,
"scale": scale,
"y_range": y_range
})
return {
"enabled": True,
"boundaries": boundaries_y,
"zones": zones
}
def generate_config(self,
min_d: float,
max_d: float,
boundaries_m: List[float],
scales: List[float]) -> dict:
"""生成完整配置"""
roi = self.calculate_roi(min_d, max_d)
zones_cfg = self.generate_zones_config(boundaries_m, scales)
return {
"preprocess": {
"roi": {"enabled": True, **roi["crop"]}
},
"face_det": {
"distance_zones": zones_cfg
},
"face_recog": {
"filters": {
"distance": {"enabled": True, "min": min_d, "max": max_d},
"pose": {
"enabled": True,
"min_pitch": -15,
"camera_pitch": self.p.pitch_deg
}
}
},
"_meta": {
"focal_px": self.p.focal_px,
"height": self.p.height,
"pitch": self.p.pitch_deg,
"roi_saving": roi["saving_percent"],
"zone_boundaries_m": boundaries_m,
"zone_scales": scales
}
}
def print_report(self,
min_d: float,
max_d: float,
boundaries_m: List[float],
scales: List[float]):
"""打印标定报告"""
print("=" * 60)
print("相机标定报告(三分区)")
print("=" * 60)
print(f"\n【相机参数】")
print(f" 安装高度 H: {self.p.height}m")
print(f" 俯仰角 θ: {self.p.pitch_deg}°")
print(f" 像素焦距 f: {self.p.focal_px}px")
print(f" 图像尺寸: {self.p.img_w}x{self.p.img_h}")
roi = self.calculate_roi(min_d, max_d)
boundaries_y = self.get_zone_boundaries(boundaries_m)
print(f"\n【ROI配置】(检测范围 {min_d}-{max_d}m)")
print(f" 裁剪: y={roi['crop']['y']}, h={roi['crop']['h']}")
print(f" 算力节省: {roi['saving_percent']}%")
print(f"\n【三分区配置】")
zone_names = ["近区(3-5m)", "中区(5-7m)", "远区(7-9m)"]
for i, (name, scale, y_bound) in enumerate(zip(zone_names, scales, boundaries_y)):
if i < 2:
print(f" {name}: y<{y_bound}, scale={scale}x")
else:
print(f" {name}: y>={boundaries_y[-1]}, scale={scale}x")
print(f"\n【距离-像素-人脸大小映射】")
print(f" {'距离':>6} | {'像素y':>6} | {'原人脸':>8} | {'处理后':>8} | {'区域':>6}")
print(f" {'-'*50}")
for d in [3, 4, 5, 6, 7, 8, 9]:
y = self.distance_to_pixel(d)
face_orig = self.estimate_face_size(d)
# 确定区域和处理后大小
zone_idx = 0
if d >= boundaries_m[1]:
zone_idx = 2
elif d >= boundaries_m[0]:
zone_idx = 1
face_proc = face_orig * scales[zone_idx]
zone_name = ["近", "中", "远"][zone_idx]
print(f" {d:>6.0f}m | {y:>6} | {face_orig:>7.0fpx} | {face_proc:>7.0fpx} | {zone_name:>6}")
print(f"\n【目标验证】")
print(f" 三个区处理后的人脸应在 60-80px 范围内")
print(f" 若偏差较大,请调整 --focal-estimate 参数重新标定")
print("\n" + "=" * 60)
def main():
parser = argparse.ArgumentParser(
description='相机标定工具 - 生成三分区检测配置',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
# 三分区标定(推荐)
python calibrate_camera.py --height 5.0 --pitch 45 --report
# 自定义参数
python calibrate_camera.py --height 4.5 --pitch 40 --focal-estimate 2000 -o cam.json --report
# 调整分区边界
python calibrate_camera.py --height 5.0 --zones 4 6 8 --scales 0.8 1.0 1.3 --report
"""
)
parser.add_argument('--height', type=float, required=True,
help='相机安装高度(米),如 5.0')
parser.add_argument('--pitch', type=float, default=45,
help='俯仰角(度),默认45')
parser.add_argument('--focal-estimate', type=float, default=2200,
help='像素焦距估算值,默认2200(2.5K相机4-6mm镜头)')
parser.add_argument('--image-size', type=int, nargs=2,
default=[2560, 1440],
help='图像尺寸 宽 高,默认 2560 1440')
parser.add_argument('--range', type=float, nargs=2,
default=[3.0, 9.0],
help='检测范围 最小距离 最大距离,默认 3.0 9.0')
parser.add_argument('--zones', type=float, nargs=3,
default=[5.0, 7.0],
help='分区边界距离(米),默认 5.0 7.0(形成3-5,5-7,7-9三区)')
parser.add_argument('--scales', type=float, nargs=3,
default=[0.7, 1.0, 1.4],
help='各分区缩放因子,默认 0.7 1.0 1.4')
parser.add_argument('-o', '--output', default='camera_calib.json',
help='输出文件路径,默认 camera_calib.json')
parser.add_argument('--report', action='store_true',
help='打印详细报告')
args = parser.parse_args()
# 验证参数
if len(args.zones) != 2:
parser.error("--zones 需要2个边界值(如 5.0 7.0),形成3个区域")
if len(args.scales) != 3:
parser.error("--scales 需要3个缩放因子(如 0.7 1.0 1.4)")
# 构建完整边界列表(包含起止)
boundaries = [args.zones[0], args.zones[1]] # 5米和7米分界线
# 创建标定器
params = CameraParams(
height=args.height,
pitch_deg=args.pitch,
focal_px=args.focal_estimate,
img_w=args.image_size[0],
img_h=args.image_size[1]
)
calib = CameraCalibrator(params)
# 生成用于instances params的配置
roi = calib.calculate_roi(args.range[0], args.range[1])
boundaries_y = calib.get_zone_boundaries(boundaries)
config = {
"params": {
"camera_pitch": args.pitch,
"roi_enabled": True,
"roi_y": roi["crop"]["y"],
"roi_h": roi["crop"]["h"],
"zones_enabled": True,
"zone_boundary_5m": boundaries_y[0],
"zone_boundary_7m": boundaries_y[1],
"zone_scale_near": args.scales[0],
"zone_scale_mid": args.scales[1],
"zone_scale_far": args.scales[2]
},
"meta": {
"height": args.height,
"pitch": args.pitch,
"focal_px": args.focal_estimate,
"roi_saving": roi["saving_percent"],
"zone_boundaries_m": boundaries,
"zone_scales": list(args.scales)
}
}
# 保存配置
with open(args.output, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
print(f"[OK] 配置已保存: {args.output}")
# 打印报告
if args.report:
calib.print_report(args.range[0], args.range[1], boundaries, list(args.scales))
print(f"\n【可复制到 instances params 的配置】")
print(json.dumps(config["params"], indent=2, ensure_ascii=False))
if __name__ == '__main__':
main()
附录B: 术语表
| 术语 | 英文 | 说明 |
|---|---|---|
| ROI | Region of Interest | 感兴趣区域,此处指3-9米对应的画面区域 |
| LUT | Look-Up Table | 查找表,用于O(1)距离查询 |
| Zone Boundary | Zone Boundary | 分区边界(5米/7米对应的像素y坐标) |
| RetinaFace_640 | RetinaFace 640×640 | 640×640输入的人脸检测模型 |
| 5点关键点 | 5-Point Landmarks | 左眼、右眼、鼻尖、左嘴角、右嘴角 |
| Templates | Configuration Templates | 配置模板,定义通用节点流水线 |
| Instances | Configuration Instances | 配置实例,为每路相机传入具体参数 |
| Pitch | Pitch Angle | 俯仰角,相机光轴与水平面夹角 |
| Focal Length (px) | Pixel Focal Length | 以像素为单位的焦距 |
文档结束