diff --git a/configs/sample_cam_ppe11.json b/configs/sample_cam_ppe11.json new file mode 100644 index 0000000..06eb393 --- /dev/null +++ b/configs/sample_cam_ppe11.json @@ -0,0 +1,329 @@ +{ + "queue": { + "size": 8, + "strategy": "drop_oldest" + }, + "graphs": [ + { + "name": "cam_ppe11_detection", + "nodes": [ + { + "id": "in_cam1", + "type": "input_rtsp", + "role": "source", + "enable": true, + "url": "rtsp://10.0.0.49:8554/cam", + "fps": 30, + "width": 1280, + "height": 720, + "use_mpp": true, + "use_ffmpeg": false, + "force_tcp": true, + "reconnect_sec": 5, + "reconnect_backoff_max_sec": 30 + }, + { + "id": "pre_cam1", + "type": "preprocess", + "role": "filter", + "enable": true, + "dst_w": 768, + "dst_h": 768, + "dst_format": "rgb", + "dst_packed": true, + "keep_ratio": false, + "rga_gate": "cam_ppe11_detection", + "use_rga": true + }, + { + "id": "yolo_cam1", + "type": "ai_yolo", + "role": "filter", + "enable": true, + "infer_fps": 10, + "model_path": "./models/best-768.rknn", + "model_version": "v8", + "model_w": 768, + "model_h": 768, + "num_classes": 11, + "conf": 0.35, + "nms": 0.45, + "class_filter": [3, 6] + }, + { + "id": "face_det_cam1", + "type": "ai_face_det", + "role": "filter", + "enable": true, + "model_path": "./models/RetinaFace_mobile320.rknn", + "conf": 0.7, + "nms": 0.4, + "max_faces": 10, + "output_landmarks": true, + "input_format": "rgb" + }, + { + "id": "face_recog_cam1", + "type": "ai_face_recog", + "role": "filter", + "enable": true, + "model_path": "./models/mobilefacenet_arcface.rknn", + "align": true, + "emit_embedding": false, + "max_faces": 10, + "input_format": "rgb", + "input_dtype": "uint8", + "threshold": { + "accept": 0.45, + "margin": 0.05 + }, + "gallery": { + "backend": "sqlite", + "path": "./models/face_gallery.db", + "load_on_start": true, + "expected_dim": 512, + "dtype": "auto" + } + }, + { + "id": "trk_cam1", + "type": "tracker", + "role": "filter", + "enable": true, + "mode": "bytetrack_lite", + "per_class": true, + "state_key": "cam_ppe11_detection", + "track_classes": [3, 6], + "ignore_classes": [], + "allowed_models": ["yolov8"], + "high_th": 0.5, + "low_th": 0.1, + "iou_th": 0.3, + "max_age_ms": 1500, + "min_hits": 2, + "max_tracks": 128 + }, + { + "id": "pre_face_cam1", + "type": "preprocess", + "role": "filter", + "enable": true, + "dst_w": 0, + "dst_h": 0, + "dst_format": "rgb", + "dst_packed": true, + "keep_ratio": false, + "rga_gate": "cam_ppe11_detection", + "use_rga": true + }, + { + "id": "osd_cam1", + "type": "osd", + "role": "filter", + "enable": true, + "draw_bbox": true, + "draw_text": true, + "draw_face_det": false, + "draw_face_bbox": false, + "line_width": 2, + "font_scale": 1, + "use_rga_bbox": false, + "labels": ["helmet", "gloves", "vest", "boots", "goggles", "none", "Person", "no_helmet", "no_goggle", "no_gloves", "no_boots"] + }, + { + "id": "post_cam1", + "type": "preprocess", + "role": "filter", + "enable": true, + "dst_w": 1280, + "dst_h": 720, + "dst_format": "nv12", + "keep_ratio": false, + "rga_gate": "cam_ppe11_detection", + "use_rga": true + }, + { + "id": "pub_cam1", + "type": "publish", + "role": "filter", + "enable": true, + "codec": "h264", + "fps": 30, + "gop": 60, + "bitrate_kbps": 2000, + "use_mpp": true, + "use_ffmpeg_mux": true, + "outputs": [ + { + "proto": "hls", + "path": "./web/hls/cam1/index.m3u8", + "segment_sec": 2 + }, + { + "proto": "rtsp_server", + "port": 8555, + "path": "/live/cam1" + } + ] + }, + { + "id": "alarm_cam1", + "type": "alarm", + "role": "sink", + "enable": true, + "eval_fps": 10, + "labels": ["helmet", "gloves", "vest", "boots", "goggles", "none", "Person", "no_helmet", "no_goggle", "no_gloves", "no_boots"], + "rules": [ + { + "name": "ppe_violation", + "class_ids": [3, 6], + "roi": { + "x": 0.0, + "y": 0.0, + "w": 1.0, + "h": 1.0 + }, + "min_score": 0.4, + "min_box_area_ratio": 0.02, + "require_track_id": true, + "min_duration_ms": 1500, + "min_hits": 3, + "hit_window_ms": 1500, + "cooldown_ms": 5000, + "per_track_cooldown_ms": 5000 + } + ], + "face_rules": [], + "actions": { + "log": { + "enable": true, + "level": "info" + }, + "snapshot": { + "enable": true, + "format": "jpg", + "quality": 85, + "upload": { + "type": "minio", + "endpoint": "http://10.0.0.49:9000", + "bucket": "myminio", + "region": "us-east-1", + "access_key": "minioadmin", + "secret_key": "minioadmin" + } + }, + "clip": { + "enable": true, + "pre_sec": 5, + "post_sec": 10, + "format": "mp4", + "fps": 30, + "upload": { + "type": "minio", + "endpoint": "http://10.0.0.49:9000", + "bucket": "myminio", + "region": "us-east-1", + "access_key": "minioadmin", + "secret_key": "minioadmin" + } + }, + "http": { + "enable": false, + "url": "http://127.0.0.1:8080/api/alarm", + "timeout_ms": 3000, + "include_media_url": true, + "method": "POST" + }, + "external_api": { + "enable": true, + "getTokenUrl": "http://10.0.0.49:8080/api/getToken", + "putMessageUrl": "http://10.0.0.49:8080/api/putMessage", + "tenantCode": "32", + "channelNo": "${vod_channelNo}", + "timeout_ms": 3000, + "include_media_url": true, + "token_header": "X-Access-Token", + "token_json_path": "responseBody.token", + "token_cache_sec": 1200 + } + } + }, + { + "id": "alarm_face_cam1", + "type": "alarm", + "role": "sink", + "enable": true, + "eval_fps": 5, + "labels": [], + "rules": [], + "face_rules": [ + { + "name": "unknown_face", + "type": "unknown", + "cooldown_ms": 7000, + "min_sim": 0.35, + "min_hits": 2, + "hit_window_ms": 1500, + "min_face_area_ratio": 0.01, + "min_face_aspect": 0.6, + "max_face_aspect": 1.6 + }, + { + "name": "known_person", + "type": "person", + "cooldown_ms": 7000, + "min_sim": 0.6, + "min_hits": 2, + "hit_window_ms": 1500, + "min_face_area_ratio": 0.01, + "min_face_aspect": 0.6, + "max_face_aspect": 1.6 + } + ], + "actions": { + "log": { + "enable": false, + "level": "info" + }, + "snapshot": { + "enable": true, + "format": "jpg", + "quality": 85, + "upload": { + "type": "minio", + "endpoint": "http://10.0.0.49:9000", + "bucket": "myminio", + "region": "us-east-1", + "access_key": "minioadmin", + "secret_key": "minioadmin" + } + }, + "clip": { + "enable": false + }, + "http": { + "enable": true, + "url": "http://127.0.0.1:8080/api/alarm", + "timeout_ms": 3000, + "include_media_url": true, + "method": "POST" + } + } + } + ], + "edges": [ + ["in_cam1", "pre_cam1"], + ["in_cam1", "pre_face_cam1"], + ["pre_cam1", "yolo_cam1"], + ["yolo_cam1", "trk_cam1"], + ["trk_cam1", "osd_cam1"], + ["osd_cam1", "post_cam1"], + ["post_cam1", "pub_cam1"], + ["pub_cam1", "alarm_cam1"], + ["pre_face_cam1", "face_det_cam1"], + ["face_det_cam1", "face_recog_cam1"], + ["face_recog_cam1", "alarm_face_cam1"] + ] + } + ] +} diff --git a/docs/models.md b/docs/models.md new file mode 100644 index 0000000..eb4bda8 --- /dev/null +++ b/docs/models.md @@ -0,0 +1,198 @@ +# AI 模型说明文档 + +本文档说明 `models/` 目录中各 AI 模型的用途、类别映射及配置参数。 + +## 目录结构 + +``` +models/ +├── best-640.rknn # YOLO 通用检测模型 (3类) +├── best-768.rknn # YOLO PPE检测模型 (11类) +├── yolov5s-640-640.rknn # YOLOv5 COCO预训练模型 (80类) +├── yolov8n-640.rknn # YOLOv8n COCO预训练模型 (80类) +├── RetinaFace_mobile320.rknn # 人脸检测模型 +├── mobilefacenet_arcface.rknn # 人脸识别模型 +└── face_gallery.db # 人脸特征库 (SQLite) +``` + +## 模型详情 + +### 1. best-640.rknn + +**类型**: YOLOv8 目标检测 +**输入尺寸**: 640x640 +**类别数**: 3 + +| 索引 | 类别名 | 说明 | +|------|--------|------| +| 0 | person | 人 | +| 1 | vest | 反光背心 | +| 2 | boots | 安全靴 | + +**配置示例**: +```json +{ + "type": "ai_yolo", + "model_path": "./models/best-640.rknn", + "model_version": "v8", + "num_classes": 3, + "conf": 0.35, + "nms": 0.45 +} +``` + +--- + +### 2. best-768.rknn (PPE11) + +**类型**: YOLOv8 PPE检测模型 +**输入尺寸**: 768x768 +**类别数**: 11 +**模型名**: ppe11_person_boots_boost + +| 索引 | 类别名 | 业务使用 | 说明 | +|------|--------|----------|------| +| 0 | helmet | ❌ | 安全帽 | +| 1 | gloves | ❌ | 手套 | +| 2 | vest | ❌ | 反光背心 | +| 3 | **boots** | ✅ | 安全靴 | +| 4 | goggles | ❌ | 护目镜 | +| 5 | none | ❌ | 无/背景 | +| 6 | **Person** | ✅ | 人 | +| 7 | no_helmet | ❌ | 未戴安全帽 | +| 8 | no_goggle | ❌ | 未戴护目镜 | +| 9 | no_gloves | ❌ | 未戴手套 | +| 10 | no_boots | ❌ | 未穿安全靴 (不作为告警依据) | + +**业务侧只消费**: `class_id in {3, 6}` (boots 和 Person) + +**配置示例**: +```json +{ + "type": "ai_yolo", + "model_path": "./models/best-768.rknn", + "model_version": "v8", + "model_w": 768, + "model_h": 768, + "num_classes": 11, + "conf": 0.35, + "nms": 0.45, + "class_filter": [3, 6] +} +``` + +**OSD标签配置**: +```json +{ + "type": "osd", + "labels": ["helmet", "gloves", "vest", "boots", "goggles", "none", "Person", "no_helmet", "no_goggle", "no_gloves", "no_boots"] +} +``` + +--- + +### 3. yolov5s-640-640.rknn + +**类型**: YOLOv5s COCO预训练模型 +**输入尺寸**: 640x640 +**类别数**: 80 (COCO标准类别) + +使用标准 COCO 类别映射,详见代码中 `kCocoLabels` 数组。 + +**配置示例**: +```json +{ + "type": "ai_yolo", + "model_path": "./models/yolov5s-640-640.rknn", + "model_version": "v5", + "num_classes": 80, + "conf": 0.25, + "nms": 0.45 +} +``` + +--- + +### 4. yolov8n-640.rknn + +**类型**: YOLOv8n COCO预训练模型 +**输入尺寸**: 640x640 +**类别数**: 80 (COCO标准类别) + +使用标准 COCO 类别映射。 + +**配置示例**: +```json +{ + "type": "ai_yolo", + "model_path": "./models/yolov8n-640.rknn", + "model_version": "v8", + "num_classes": 80, + "conf": 0.25, + "nms": 0.45 +} +``` + +--- + +### 5. RetinaFace_mobile320.rknn + +**类型**: RetinaFace 人脸检测 +**输入尺寸**: 320x320 +**输出**: 人脸框 + 5点关键点 + +**配置示例**: +```json +{ + "type": "ai_face_det", + "model_path": "./models/RetinaFace_mobile320.rknn", + "conf": 0.7, + "nms": 0.4, + "max_faces": 10, + "output_landmarks": true +} +``` + +--- + +### 6. mobilefacenet_arcface.rknn + +**类型**: MobileFaceNet + ArcFace 人脸识别 +**输入尺寸**: 112x112 +**输出维度**: 512 + +**配置示例**: +```json +{ + "type": "ai_face_recog", + "model_path": "./models/mobilefacenet_arcface.rknn", + "align": true, + "threshold": { + "accept": 0.45, + "margin": 0.05 + }, + "gallery": { + "backend": "sqlite", + "path": "./models/face_gallery.db", + "expected_dim": 512 + } +} +``` + +--- + +## 模型选择建议 + +| 场景 | 推荐模型 | 说明 | +|------|----------|------| +| 通用目标检测 | yolov8n-640.rknn | 80类COCO,通用性强 | +| 工地安全检测 | best-640.rknn | 人/背心/安全靴 | +| PPE完整检测 | best-768.rknn | 11类PPE,含正反例 | +| 人脸检测+识别 | RetinaFace + MobileFaceNet | 人脸识别链路 | + +## 注意事项 + +1. **模型尺寸匹配**: `preprocess` 节点的 `dst_w/dst_h` 必须与模型输入尺寸一致 +2. **num_classes**: 必须与模型实际类别数一致,否则后处理会出错 +3. **class_filter**: 用于过滤只关心的类别,减少后续处理负载 +4. **best-768.rknn**: 只消费类别 3(boots) 和 6(Person),其他类别可忽略 diff --git a/models/best-768.rknn b/models/best-768.rknn new file mode 100644 index 0000000..436c916 Binary files /dev/null and b/models/best-768.rknn differ