diff --git a/best_cxn.onnx b/best_cxn.onnx
new file mode 100644
index 0000000..2485b00
Binary files /dev/null and b/best_cxn.onnx differ
diff --git a/check0_base_optimize.onnx b/check0_base_optimize.onnx
new file mode 100644
index 0000000..a928d19
Binary files /dev/null and b/check0_base_optimize.onnx differ
diff --git a/check1_fold_constant.onnx b/check1_fold_constant.onnx
new file mode 100644
index 0000000..906df31
Binary files /dev/null and b/check1_fold_constant.onnx differ
diff --git a/check2_correct_ops.onnx b/check2_correct_ops.onnx
new file mode 100644
index 0000000..8ad780c
Binary files /dev/null and b/check2_correct_ops.onnx differ
diff --git a/check3_fuse_ops.onnx b/check3_fuse_ops.onnx
new file mode 100644
index 0000000..d04958b
Binary files /dev/null and b/check3_fuse_ops.onnx differ
diff --git a/configs/sample_cam2.json.last_good.json b/configs/sample_cam2.json.last_good.json
new file mode 100644
index 0000000..8436eaa
--- /dev/null
+++ b/configs/sample_cam2.json.last_good.json
@@ -0,0 +1 @@
+{"graphs":[{"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"]],"name":"cam1_sample_full_pipeline","nodes":[{"enable":true,"force_tcp":true,"fps":30,"height":720,"id":"in_cam1","reconnect_backoff_max_sec":30,"reconnect_sec":5,"role":"source","type":"input_rtsp","url":"rtsp://10.0.0.49:8554/cam","use_ffmpeg":false,"use_mpp":true,"width":1280},{"dst_format":"rgb","dst_h":640,"dst_packed":true,"dst_w":640,"enable":true,"id":"pre_cam1","keep_ratio":false,"rga_gate":"cam1_sample_full_pipeline","role":"filter","type":"preprocess","use_rga":true},{"class_filter":[],"conf":0.35,"enable":true,"id":"yolo_cam1","infer_fps":10,"model_path":"./models/yolov5s-640-640.rknn","model_version":"v5","nms":0.45,"num_classes":80,"role":"filter","type":"ai_yolo"},{"conf":0.7,"enable":true,"id":"face_det_cam1","input_format":"rgb","max_faces":10,"model_path":"./models/RetinaFace_mobile320.rknn","nms":0.4,"output_landmarks":true,"role":"filter","type":"ai_face_det"},{"align":true,"emit_embedding":false,"enable":true,"gallery":{"backend":"sqlite","dtype":"auto","expected_dim":512,"load_on_start":true,"path":"./models/face_gallery.db"},"id":"face_recog_cam1","input_dtype":"uint8","input_format":"rgb","max_faces":10,"model_path":"./models/mobilefacenet_arcface.rknn","role":"filter","threshold":{"accept":0.45,"margin":0.05},"type":"ai_face_recog"},{"allowed_models":["yolov5","yolov8"],"debug":{"stats":false,"stats_interval":200},"enable":true,"high_th":0.5,"id":"trk_cam1","ignore_classes":[],"iou_th":0.3,"low_th":0.1,"max_age_ms":1500,"max_tracks":128,"min_hits":2,"mode":"bytetrack_lite","per_class":true,"role":"filter","state_key":"cam1_sample_full_pipeline","track_classes":[0],"type":"tracker"},{"dst_format":"rgb","dst_h":0,"dst_packed":true,"dst_w":0,"enable":true,"id":"pre_face_cam1","keep_ratio":false,"rga_gate":"cam1_sample_full_pipeline","role":"filter","type":"preprocess","use_rga":true},{"draw_bbox":true,"draw_face_bbox":false,"draw_face_det":false,"draw_text":true,"enable":true,"font_scale":1,"id":"osd_cam1","labels":[],"line_width":2,"role":"filter","type":"osd"},{"dst_format":"nv12","dst_h":720,"dst_w":1280,"enable":true,"id":"post_cam1","keep_ratio":false,"rga_gate":"cam1_sample_full_pipeline","role":"filter","type":"preprocess","use_rga":true},{"bitrate_kbps":2000,"codec":"h264","enable":true,"fps":30,"gop":60,"id":"pub_cam1","outputs":[{"path":"/live/cam1","port":8555,"proto":"rtsp_server"},{"path":"./web/hls/cam1/index.m3u8","proto":"hls","segment_sec":2}],"role":"filter","type":"publish","use_ffmpeg_mux":true,"use_mpp":true},{"actions":{"clip":{"enable":true,"format":"mp4","fps":30,"post_sec":10,"pre_sec":5,"upload":{"access_key":"your-access-key","bucket":"vi","endpoint":"http://10.0.0.49:9000","region":"us-east-1","secret_key":"your-secret-key","type":"minio"}},"external_api":{"channelNo":"${vod_channelNo}","enable":true,"getTokenUrl":"http://127.0.0.1:8080/api/getToken","include_media_url":true,"putMessageUrl":"http://127.0.0.1:8080/api/putMessage","tenantCode":"32","timeout_ms":3000,"token_cache_sec":1200,"token_header":"X-Access-Token","token_json_path":"responseBody.token"},"http":{"enable":false,"include_media_url":true,"method":"POST","timeout_ms":3000,"url":"http://127.0.0.1:8080/api/alarm"},"log":{"enable":true,"level":"info"},"snapshot":{"enable":true,"format":"jpg","quality":85,"upload":{"access_key":"your-access-key","bucket":"vi","endpoint":"http://10.0.0.49:9000","region":"us-east-1","secret_key":"your-secret-key","type":"minio"}}},"enable":true,"eval_fps":10,"face_rules":[],"id":"alarm_cam1","labels":[],"role":"sink","rules":[{"class_ids":[0],"cooldown_ms":5000,"hit_window_ms":1500,"min_box_area_ratio":0.02,"min_duration_ms":1500,"min_hits":3,"min_score":0.4,"name":"person_in_view","per_track_cooldown_ms":5000,"require_track_id":true,"roi":{"h":1,"w":1,"x":0,"y":0}}],"type":"alarm"},{"actions":{"clip":{"enable":false},"http":{"enable":true,"include_media_url":true,"method":"POST","timeout_ms":3000,"url":"http://127.0.0.1:8080/api/alarm"},"log":{"enable":false,"level":"info"},"snapshot":{"enable":true,"format":"jpg","quality":85,"upload":{"access_key":"your-access-key","bucket":"vi","endpoint":"http://10.0.0.49:9000","region":"us-east-1","secret_key":"your-secret-key","type":"minio"}}},"enable":true,"eval_fps":5,"face_rules":[{"cooldown_ms":7000,"hit_window_ms":1500,"max_face_aspect":1.6,"min_face_area_ratio":0.01,"min_face_aspect":0.6,"min_hits":2,"min_sim":0.35,"name":"unknown_face","type":"unknown"},{"cooldown_ms":7000,"hit_window_ms":1500,"max_face_aspect":1.6,"min_face_area_ratio":0.01,"min_face_aspect":0.6,"min_hits":2,"min_sim":0.6,"name":"known_person","type":"person"}],"id":"alarm_face_cam1","labels":[],"role":"sink","rules":[],"type":"alarm"}]}],"queue":{"size":8,"strategy":"drop_oldest"}}
\ No newline at end of file
diff --git a/configs/sample_cam3.json b/configs/sample_cam3.json
new file mode 100644
index 0000000..99df80b
--- /dev/null
+++ b/configs/sample_cam3.json
@@ -0,0 +1,281 @@
+{
+ "queue": { "size": 8, "strategy": "drop_oldest" },
+ "graphs": [
+ {
+ "name": "cam1_sample_full_pipeline",
+ "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": 640,
+ "dst_h": 640,
+ "dst_format": "rgb",
+ "dst_packed": true,
+ "keep_ratio": false,
+ "rga_gate": "cam1_sample_full_pipeline",
+ "use_rga": true
+ },
+ {
+ "id": "yolo_cam1",
+ "type": "ai_yolo",
+ "role": "filter",
+ "enable": true,
+ "infer_fps": 10,
+ "model_path": "./models/yolov5s-640-640.rknn",
+ "model_version": "v5",
+ "num_classes": 80,
+ "conf": 0.35,
+ "nms": 0.45,
+ "class_filter": []
+ },
+ {
+ "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": "cam1_sample_full_pipeline",
+ "track_classes": [0],
+ "ignore_classes": [],
+ "allowed_models": ["yolov5", "yolov8"],
+ "high_th": 0.5,
+ "low_th": 0.1,
+ "iou_th": 0.3,
+ "max_age_ms": 1500,
+ "min_hits": 2,
+ "max_tracks": 128,
+ "debug": { "stats": false, "stats_interval": 200 }
+ },
+ {
+ "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": "cam1_sample_full_pipeline",
+ "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": []
+ },
+ {
+ "id": "post_cam1",
+ "type": "preprocess",
+ "role": "filter",
+ "enable": true,
+ "dst_w": 1280,
+ "dst_h": 720,
+ "dst_format": "nv12",
+ "keep_ratio": false,
+ "rga_gate": "cam1_sample_full_pipeline",
+ "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": "rtsp_server", "port": 8555, "path": "/live/cam1" },
+ { "proto": "hls", "path": "./web/hls/cam1/index.m3u8", "segment_sec": 2 }
+ ]
+ },
+ {
+ "id": "alarm_cam1",
+ "type": "alarm",
+ "role": "sink",
+ "enable": true,
+ "eval_fps": 10,
+ "labels": [],
+ "rules": [
+ {
+ "name": "person_in_view",
+ "class_ids": [0],
+ "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://127.0.0.1:8080/api/getToken",
+ "putMessageUrl": "http://127.0.0.1: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/configs/sample_cam3.json.last_good.json b/configs/sample_cam3.json.last_good.json
new file mode 100644
index 0000000..6f544f9
--- /dev/null
+++ b/configs/sample_cam3.json.last_good.json
@@ -0,0 +1 @@
+{"graphs":[{"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"]],"name":"cam1_sample_full_pipeline","nodes":[{"enable":true,"force_tcp":true,"fps":30,"height":720,"id":"in_cam1","reconnect_backoff_max_sec":30,"reconnect_sec":5,"role":"source","type":"input_rtsp","url":"rtsp://10.0.0.49:8554/cam","use_ffmpeg":false,"use_mpp":true,"width":1280},{"dst_format":"rgb","dst_h":640,"dst_packed":true,"dst_w":640,"enable":true,"id":"pre_cam1","keep_ratio":false,"rga_gate":"cam1_sample_full_pipeline","role":"filter","type":"preprocess","use_rga":true},{"class_filter":[],"conf":0.35,"enable":true,"id":"yolo_cam1","infer_fps":10,"model_path":"./models/yolov5s-640-640.rknn","model_version":"v5","nms":0.45,"num_classes":80,"role":"filter","type":"ai_yolo"},{"conf":0.7,"enable":true,"id":"face_det_cam1","input_format":"rgb","max_faces":10,"model_path":"./models/RetinaFace_mobile320.rknn","nms":0.4,"output_landmarks":true,"role":"filter","type":"ai_face_det"},{"align":true,"emit_embedding":false,"enable":true,"gallery":{"backend":"sqlite","dtype":"auto","expected_dim":512,"load_on_start":true,"path":"./models/face_gallery.db"},"id":"face_recog_cam1","input_dtype":"uint8","input_format":"rgb","max_faces":10,"model_path":"./models/mobilefacenet_arcface.rknn","role":"filter","threshold":{"accept":0.45,"margin":0.05},"type":"ai_face_recog"},{"allowed_models":["yolov5","yolov8"],"debug":{"stats":false,"stats_interval":200},"enable":true,"high_th":0.5,"id":"trk_cam1","ignore_classes":[],"iou_th":0.3,"low_th":0.1,"max_age_ms":1500,"max_tracks":128,"min_hits":2,"mode":"bytetrack_lite","per_class":true,"role":"filter","state_key":"cam1_sample_full_pipeline","track_classes":[0],"type":"tracker"},{"dst_format":"rgb","dst_h":0,"dst_packed":true,"dst_w":0,"enable":true,"id":"pre_face_cam1","keep_ratio":false,"rga_gate":"cam1_sample_full_pipeline","role":"filter","type":"preprocess","use_rga":true},{"draw_bbox":true,"draw_face_bbox":false,"draw_face_det":false,"draw_text":true,"enable":true,"font_scale":1,"id":"osd_cam1","labels":[],"line_width":2,"role":"filter","type":"osd","use_rga_bbox":false},{"dst_format":"nv12","dst_h":720,"dst_w":1280,"enable":true,"id":"post_cam1","keep_ratio":false,"rga_gate":"cam1_sample_full_pipeline","role":"filter","type":"preprocess","use_rga":true},{"bitrate_kbps":2000,"codec":"h264","enable":true,"fps":30,"gop":60,"id":"pub_cam1","outputs":[{"path":"/live/cam1","port":8555,"proto":"rtsp_server"},{"path":"./web/hls/cam1/index.m3u8","proto":"hls","segment_sec":2}],"role":"filter","type":"publish","use_ffmpeg_mux":true,"use_mpp":true},{"actions":{"clip":{"enable":true,"format":"mp4","fps":30,"post_sec":10,"pre_sec":5,"upload":{"access_key":"minioadmin","bucket":"myminio","endpoint":"http://10.0.0.49:9000","region":"us-east-1","secret_key":"minioadmin","type":"minio"}},"external_api":{"channelNo":"${vod_channelNo}","enable":true,"getTokenUrl":"http://127.0.0.1:8080/api/getToken","include_media_url":true,"putMessageUrl":"http://127.0.0.1:8080/api/putMessage","tenantCode":"32","timeout_ms":3000,"token_cache_sec":1200,"token_header":"X-Access-Token","token_json_path":"responseBody.token"},"http":{"enable":false,"include_media_url":true,"method":"POST","timeout_ms":3000,"url":"http://127.0.0.1:8080/api/alarm"},"log":{"enable":true,"level":"info"},"snapshot":{"enable":true,"format":"jpg","quality":85,"upload":{"access_key":"minioadmin","bucket":"myminio","endpoint":"http://10.0.0.49:9000","region":"us-east-1","secret_key":"minioadmin","type":"minio"}}},"enable":true,"eval_fps":10,"face_rules":[],"id":"alarm_cam1","labels":[],"role":"sink","rules":[{"class_ids":[0],"cooldown_ms":5000,"hit_window_ms":1500,"min_box_area_ratio":0.02,"min_duration_ms":1500,"min_hits":3,"min_score":0.4,"name":"person_in_view","per_track_cooldown_ms":5000,"require_track_id":true,"roi":{"h":1,"w":1,"x":0,"y":0}}],"type":"alarm"},{"actions":{"clip":{"enable":false},"http":{"enable":true,"include_media_url":true,"method":"POST","timeout_ms":3000,"url":"http://127.0.0.1:8080/api/alarm"},"log":{"enable":false,"level":"info"},"snapshot":{"enable":true,"format":"jpg","quality":85,"upload":{"access_key":"minioadmin","bucket":"myminio","endpoint":"http://10.0.0.49:9000","region":"us-east-1","secret_key":"minioadmin","type":"minio"}}},"enable":true,"eval_fps":5,"face_rules":[{"cooldown_ms":7000,"hit_window_ms":1500,"max_face_aspect":1.6,"min_face_area_ratio":0.01,"min_face_aspect":0.6,"min_hits":2,"min_sim":0.35,"name":"unknown_face","type":"unknown"},{"cooldown_ms":7000,"hit_window_ms":1500,"max_face_aspect":1.6,"min_face_area_ratio":0.01,"min_face_aspect":0.6,"min_hits":2,"min_sim":0.6,"name":"known_person","type":"person"}],"id":"alarm_face_cam1","labels":[],"role":"sink","rules":[],"type":"alarm"}]}],"queue":{"size":8,"strategy":"drop_oldest"}}
\ No newline at end of file
diff --git a/configs/stress_test_4ch_shared_source.json b/configs/stress_test_4ch_shared_source.json
new file mode 100644
index 0000000..6af1891
--- /dev/null
+++ b/configs/stress_test_4ch_shared_source.json
@@ -0,0 +1,1105 @@
+{
+ "queue": { "size": 8, "strategy": "drop_oldest" },
+ "graphs": [
+ {
+ "name": "stress_cam1",
+ "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": 640,
+ "dst_h": 640,
+ "dst_format": "rgb",
+ "dst_packed": true,
+ "keep_ratio": false,
+ "rga_gate": "stress_cam1",
+ "use_rga": true
+ },
+ {
+ "id": "yolo_cam1",
+ "type": "ai_yolo",
+ "role": "filter",
+ "enable": true,
+ "infer_fps": 10,
+ "model_path": "./models/yolov5s-640-640.rknn",
+ "model_version": "v8",
+ "num_classes": 80,
+ "conf": 0.35,
+ "nms": 0.45,
+ "class_filter": []
+ },
+ {
+ "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": "stress_cam1",
+ "track_classes": [0],
+ "ignore_classes": [],
+ "allowed_models": ["yolov5", "yolov8"],
+ "high_th": 0.5,
+ "low_th": 0.1,
+ "iou_th": 0.3,
+ "max_age_ms": 1500,
+ "min_hits": 2,
+ "max_tracks": 128,
+ "debug": { "stats": false, "stats_interval": 200 }
+ },
+ {
+ "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": "stress_cam1",
+ "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,
+ "labels": []
+ },
+ {
+ "id": "post_cam1",
+ "type": "preprocess",
+ "role": "filter",
+ "enable": true,
+ "dst_w": 1280,
+ "dst_h": 720,
+ "dst_format": "nv12",
+ "keep_ratio": false,
+ "rga_gate": "stress_cam1",
+ "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": "rtsp_server", "port": 8555, "path": "/live/cam1" },
+ { "proto": "hls", "path": "./web/hls/cam1/index.m3u8", "segment_sec": 2 }
+ ]
+ },
+ {
+ "id": "alarm_cam1",
+ "type": "alarm",
+ "role": "sink",
+ "enable": true,
+ "eval_fps": 10,
+ "labels": [],
+ "rules": [
+ {
+ "name": "person_in_view",
+ "class_ids": [0],
+ "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": "stress-test",
+ "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": "stress-test",
+ "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": false,
+ "getTokenUrl": "http://127.0.0.1:8080/api/getToken",
+ "putMessageUrl": "http://127.0.0.1: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": "stress-test",
+ "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"]
+ ]
+ },
+ {
+ "name": "stress_cam2",
+ "nodes": [
+ {
+ "id": "in_cam2",
+ "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_cam2",
+ "type": "preprocess",
+ "role": "filter",
+ "enable": true,
+ "dst_w": 640,
+ "dst_h": 640,
+ "dst_format": "rgb",
+ "dst_packed": true,
+ "keep_ratio": false,
+ "rga_gate": "stress_cam2",
+ "use_rga": true
+ },
+ {
+ "id": "yolo_cam2",
+ "type": "ai_yolo",
+ "role": "filter",
+ "enable": true,
+ "infer_fps": 10,
+ "model_path": "./models/yolov5s-640-640.rknn",
+ "model_version": "v8",
+ "num_classes": 80,
+ "conf": 0.35,
+ "nms": 0.45,
+ "class_filter": []
+ },
+ {
+ "id": "face_det_cam2",
+ "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_cam2",
+ "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_cam2",
+ "type": "tracker",
+ "role": "filter",
+ "enable": true,
+ "mode": "bytetrack_lite",
+ "per_class": true,
+ "state_key": "stress_cam2",
+ "track_classes": [0],
+ "ignore_classes": [],
+ "allowed_models": ["yolov5", "yolov8"],
+ "high_th": 0.5,
+ "low_th": 0.1,
+ "iou_th": 0.3,
+ "max_age_ms": 1500,
+ "min_hits": 2,
+ "max_tracks": 128,
+ "debug": { "stats": false, "stats_interval": 200 }
+ },
+ {
+ "id": "pre_face_cam2",
+ "type": "preprocess",
+ "role": "filter",
+ "enable": true,
+ "dst_w": 0,
+ "dst_h": 0,
+ "dst_format": "rgb",
+ "dst_packed": true,
+ "keep_ratio": false,
+ "rga_gate": "stress_cam2",
+ "use_rga": true
+ },
+ {
+ "id": "osd_cam2",
+ "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,
+ "labels": []
+ },
+ {
+ "id": "post_cam2",
+ "type": "preprocess",
+ "role": "filter",
+ "enable": true,
+ "dst_w": 1280,
+ "dst_h": 720,
+ "dst_format": "nv12",
+ "keep_ratio": false,
+ "rga_gate": "stress_cam2",
+ "use_rga": true
+ },
+ {
+ "id": "pub_cam2",
+ "type": "publish",
+ "role": "filter",
+ "enable": true,
+ "codec": "h264",
+ "fps": 30,
+ "gop": 60,
+ "bitrate_kbps": 2000,
+ "use_mpp": true,
+ "use_ffmpeg_mux": true,
+ "outputs": [
+ { "proto": "rtsp_server", "port": 8556, "path": "/live/cam2" },
+ { "proto": "hls", "path": "./web/hls/cam2/index.m3u8", "segment_sec": 2 }
+ ]
+ },
+ {
+ "id": "alarm_cam2",
+ "type": "alarm",
+ "role": "sink",
+ "enable": true,
+ "eval_fps": 10,
+ "labels": [],
+ "rules": [
+ {
+ "name": "person_in_view",
+ "class_ids": [0],
+ "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": "stress-test",
+ "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": "stress-test",
+ "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": false,
+ "getTokenUrl": "http://127.0.0.1:8080/api/getToken",
+ "putMessageUrl": "http://127.0.0.1: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_cam2",
+ "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": "stress-test",
+ "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_cam2", "pre_cam2"],
+ ["in_cam2", "pre_face_cam2"],
+ ["pre_cam2", "yolo_cam2"],
+ ["yolo_cam2", "trk_cam2"],
+ ["trk_cam2", "osd_cam2"],
+ ["osd_cam2", "post_cam2"],
+ ["post_cam2", "pub_cam2"],
+ ["pub_cam2", "alarm_cam2"],
+ ["pre_face_cam2", "face_det_cam2"],
+ ["face_det_cam2", "face_recog_cam2"],
+ ["face_recog_cam2", "alarm_face_cam2"]
+ ]
+ },
+ {
+ "name": "stress_cam3",
+ "nodes": [
+ {
+ "id": "in_cam3",
+ "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_cam3",
+ "type": "preprocess",
+ "role": "filter",
+ "enable": true,
+ "dst_w": 640,
+ "dst_h": 640,
+ "dst_format": "rgb",
+ "dst_packed": true,
+ "keep_ratio": false,
+ "rga_gate": "stress_cam3",
+ "use_rga": true
+ },
+ {
+ "id": "yolo_cam3",
+ "type": "ai_yolo",
+ "role": "filter",
+ "enable": true,
+ "infer_fps": 10,
+ "model_path": "./models/yolov5s-640-640.rknn",
+ "model_version": "v8",
+ "num_classes": 80,
+ "conf": 0.35,
+ "nms": 0.45,
+ "class_filter": []
+ },
+ {
+ "id": "face_det_cam3",
+ "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_cam3",
+ "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_cam3",
+ "type": "tracker",
+ "role": "filter",
+ "enable": true,
+ "mode": "bytetrack_lite",
+ "per_class": true,
+ "state_key": "stress_cam3",
+ "track_classes": [0],
+ "ignore_classes": [],
+ "allowed_models": ["yolov5", "yolov8"],
+ "high_th": 0.5,
+ "low_th": 0.1,
+ "iou_th": 0.3,
+ "max_age_ms": 1500,
+ "min_hits": 2,
+ "max_tracks": 128,
+ "debug": { "stats": false, "stats_interval": 200 }
+ },
+ {
+ "id": "pre_face_cam3",
+ "type": "preprocess",
+ "role": "filter",
+ "enable": true,
+ "dst_w": 0,
+ "dst_h": 0,
+ "dst_format": "rgb",
+ "dst_packed": true,
+ "keep_ratio": false,
+ "rga_gate": "stress_cam3",
+ "use_rga": true
+ },
+ {
+ "id": "osd_cam3",
+ "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,
+ "labels": []
+ },
+ {
+ "id": "post_cam3",
+ "type": "preprocess",
+ "role": "filter",
+ "enable": true,
+ "dst_w": 1280,
+ "dst_h": 720,
+ "dst_format": "nv12",
+ "keep_ratio": false,
+ "rga_gate": "stress_cam3",
+ "use_rga": true
+ },
+ {
+ "id": "pub_cam3",
+ "type": "publish",
+ "role": "filter",
+ "enable": true,
+ "codec": "h264",
+ "fps": 30,
+ "gop": 60,
+ "bitrate_kbps": 2000,
+ "use_mpp": true,
+ "use_ffmpeg_mux": true,
+ "outputs": [
+ { "proto": "rtsp_server", "port": 8557, "path": "/live/cam3" },
+ { "proto": "hls", "path": "./web/hls/cam3/index.m3u8", "segment_sec": 2 }
+ ]
+ },
+ {
+ "id": "alarm_cam3",
+ "type": "alarm",
+ "role": "sink",
+ "enable": true,
+ "eval_fps": 10,
+ "labels": [],
+ "rules": [
+ {
+ "name": "person_in_view",
+ "class_ids": [0],
+ "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": "stress-test",
+ "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": "stress-test",
+ "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": false,
+ "getTokenUrl": "http://127.0.0.1:8080/api/getToken",
+ "putMessageUrl": "http://127.0.0.1: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_cam3",
+ "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": "stress-test",
+ "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_cam3", "pre_cam3"],
+ ["in_cam3", "pre_face_cam3"],
+ ["pre_cam3", "yolo_cam3"],
+ ["yolo_cam3", "trk_cam3"],
+ ["trk_cam3", "osd_cam3"],
+ ["osd_cam3", "post_cam3"],
+ ["post_cam3", "pub_cam3"],
+ ["pub_cam3", "alarm_cam3"],
+ ["pre_face_cam3", "face_det_cam3"],
+ ["face_det_cam3", "face_recog_cam3"],
+ ["face_recog_cam3", "alarm_face_cam3"]
+ ]
+ },
+ {
+ "name": "stress_cam4",
+ "nodes": [
+ {
+ "id": "in_cam4",
+ "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_cam4",
+ "type": "preprocess",
+ "role": "filter",
+ "enable": true,
+ "dst_w": 640,
+ "dst_h": 640,
+ "dst_format": "rgb",
+ "dst_packed": true,
+ "keep_ratio": false,
+ "rga_gate": "stress_cam4",
+ "use_rga": true
+ },
+ {
+ "id": "yolo_cam4",
+ "type": "ai_yolo",
+ "role": "filter",
+ "enable": true,
+ "infer_fps": 10,
+ "model_path": "./models/yolov5s-640-640.rknn",
+ "model_version": "v8",
+ "num_classes": 80,
+ "conf": 0.35,
+ "nms": 0.45,
+ "class_filter": []
+ },
+ {
+ "id": "face_det_cam4",
+ "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_cam4",
+ "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_cam4",
+ "type": "tracker",
+ "role": "filter",
+ "enable": true,
+ "mode": "bytetrack_lite",
+ "per_class": true,
+ "state_key": "stress_cam4",
+ "track_classes": [0],
+ "ignore_classes": [],
+ "allowed_models": ["yolov5", "yolov8"],
+ "high_th": 0.5,
+ "low_th": 0.1,
+ "iou_th": 0.3,
+ "max_age_ms": 1500,
+ "min_hits": 2,
+ "max_tracks": 128,
+ "debug": { "stats": false, "stats_interval": 200 }
+ },
+ {
+ "id": "pre_face_cam4",
+ "type": "preprocess",
+ "role": "filter",
+ "enable": true,
+ "dst_w": 0,
+ "dst_h": 0,
+ "dst_format": "rgb",
+ "dst_packed": true,
+ "keep_ratio": false,
+ "rga_gate": "stress_cam4",
+ "use_rga": true
+ },
+ {
+ "id": "osd_cam4",
+ "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,
+ "labels": []
+ },
+ {
+ "id": "post_cam4",
+ "type": "preprocess",
+ "role": "filter",
+ "enable": true,
+ "dst_w": 1280,
+ "dst_h": 720,
+ "dst_format": "nv12",
+ "keep_ratio": false,
+ "rga_gate": "stress_cam4",
+ "use_rga": true
+ },
+ {
+ "id": "pub_cam4",
+ "type": "publish",
+ "role": "filter",
+ "enable": true,
+ "codec": "h264",
+ "fps": 30,
+ "gop": 60,
+ "bitrate_kbps": 2000,
+ "use_mpp": true,
+ "use_ffmpeg_mux": true,
+ "outputs": [
+ { "proto": "rtsp_server", "port": 8558, "path": "/live/cam4" },
+ { "proto": "hls", "path": "./web/hls/cam4/index.m3u8", "segment_sec": 2 }
+ ]
+ },
+ {
+ "id": "alarm_cam4",
+ "type": "alarm",
+ "role": "sink",
+ "enable": true,
+ "eval_fps": 10,
+ "labels": [],
+ "rules": [
+ {
+ "name": "person_in_view",
+ "class_ids": [0],
+ "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": "stress-test",
+ "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": "stress-test",
+ "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": false,
+ "getTokenUrl": "http://127.0.0.1:8080/api/getToken",
+ "putMessageUrl": "http://127.0.0.1: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_cam4",
+ "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": "stress-test",
+ "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_cam4", "pre_cam4"],
+ ["in_cam4", "pre_face_cam4"],
+ ["pre_cam4", "yolo_cam4"],
+ ["yolo_cam4", "trk_cam4"],
+ ["trk_cam4", "osd_cam4"],
+ ["osd_cam4", "post_cam4"],
+ ["post_cam4", "pub_cam4"],
+ ["pub_cam4", "alarm_cam4"],
+ ["pre_face_cam4", "face_det_cam4"],
+ ["face_det_cam4", "face_recog_cam4"],
+ ["face_recog_cam4", "alarm_face_cam4"]
+ ]
+ }
+ ]
+}
diff --git a/configs/stress_test_4ch_shared_source.json.last_good.json b/configs/stress_test_4ch_shared_source.json.last_good.json
new file mode 100644
index 0000000..db0cc8a
--- /dev/null
+++ b/configs/stress_test_4ch_shared_source.json.last_good.json
@@ -0,0 +1 @@
+{"graphs":[{"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"]],"name":"stress_cam1","nodes":[{"enable":true,"force_tcp":true,"fps":30,"height":720,"id":"in_cam1","reconnect_backoff_max_sec":30,"reconnect_sec":5,"role":"source","type":"input_rtsp","url":"rtsp://10.0.0.49:8554/cam","use_ffmpeg":false,"use_mpp":true,"width":1280},{"dst_format":"rgb","dst_h":640,"dst_packed":true,"dst_w":640,"enable":true,"id":"pre_cam1","keep_ratio":false,"rga_gate":"stress_cam1","role":"filter","type":"preprocess","use_rga":true},{"class_filter":[],"conf":0.35,"enable":true,"id":"yolo_cam1","infer_fps":10,"model_path":"./models/yolov5s-640-640.rknn","model_version":"v8","nms":0.45,"num_classes":80,"role":"filter","type":"ai_yolo"},{"conf":0.7,"enable":true,"id":"face_det_cam1","input_format":"rgb","max_faces":10,"model_path":"./models/RetinaFace_mobile320.rknn","nms":0.4,"output_landmarks":true,"role":"filter","type":"ai_face_det"},{"align":true,"emit_embedding":false,"enable":true,"gallery":{"backend":"sqlite","dtype":"auto","expected_dim":512,"load_on_start":true,"path":"./models/face_gallery.db"},"id":"face_recog_cam1","input_dtype":"uint8","input_format":"rgb","max_faces":10,"model_path":"./models/mobilefacenet_arcface.rknn","role":"filter","threshold":{"accept":0.45,"margin":0.05},"type":"ai_face_recog"},{"allowed_models":["yolov5","yolov8"],"debug":{"stats":false,"stats_interval":200},"enable":true,"high_th":0.5,"id":"trk_cam1","ignore_classes":[],"iou_th":0.3,"low_th":0.1,"max_age_ms":1500,"max_tracks":128,"min_hits":2,"mode":"bytetrack_lite","per_class":true,"role":"filter","state_key":"stress_cam1","track_classes":[0],"type":"tracker"},{"dst_format":"rgb","dst_h":0,"dst_packed":true,"dst_w":0,"enable":true,"id":"pre_face_cam1","keep_ratio":false,"rga_gate":"stress_cam1","role":"filter","type":"preprocess","use_rga":true},{"draw_bbox":true,"draw_face_bbox":false,"draw_face_det":false,"draw_text":true,"enable":true,"font_scale":1,"id":"osd_cam1","labels":[],"line_width":2,"role":"filter","type":"osd"},{"dst_format":"nv12","dst_h":720,"dst_w":1280,"enable":true,"id":"post_cam1","keep_ratio":false,"rga_gate":"stress_cam1","role":"filter","type":"preprocess","use_rga":true},{"bitrate_kbps":2000,"codec":"h264","enable":true,"fps":30,"gop":60,"id":"pub_cam1","outputs":[{"path":"/live/cam1","port":8555,"proto":"rtsp_server"},{"path":"./web/hls/cam1/index.m3u8","proto":"hls","segment_sec":2}],"role":"filter","type":"publish","use_ffmpeg_mux":true,"use_mpp":true},{"actions":{"clip":{"enable":true,"format":"mp4","fps":30,"post_sec":10,"pre_sec":5,"upload":{"access_key":"minioadmin","bucket":"stress-test","endpoint":"http://10.0.0.49:9000","region":"us-east-1","secret_key":"minioadmin","type":"minio"}},"external_api":{"channelNo":"${vod_channelNo}","enable":false,"getTokenUrl":"http://127.0.0.1:8080/api/getToken","include_media_url":true,"putMessageUrl":"http://127.0.0.1:8080/api/putMessage","tenantCode":"32","timeout_ms":3000,"token_cache_sec":1200,"token_header":"X-Access-Token","token_json_path":"responseBody.token"},"http":{"enable":false,"include_media_url":true,"method":"POST","timeout_ms":3000,"url":"http://127.0.0.1:8080/api/alarm"},"log":{"enable":true,"level":"info"},"snapshot":{"enable":true,"format":"jpg","quality":85,"upload":{"access_key":"minioadmin","bucket":"stress-test","endpoint":"http://10.0.0.49:9000","region":"us-east-1","secret_key":"minioadmin","type":"minio"}}},"enable":true,"eval_fps":10,"face_rules":[],"id":"alarm_cam1","labels":[],"role":"sink","rules":[{"class_ids":[0],"cooldown_ms":5000,"hit_window_ms":1500,"min_box_area_ratio":0.02,"min_duration_ms":1500,"min_hits":3,"min_score":0.4,"name":"person_in_view","per_track_cooldown_ms":5000,"require_track_id":true,"roi":{"h":1,"w":1,"x":0,"y":0}}],"type":"alarm"},{"actions":{"clip":{"enable":false},"http":{"enable":true,"include_media_url":true,"method":"POST","timeout_ms":3000,"url":"http://127.0.0.1:8080/api/alarm"},"log":{"enable":false,"level":"info"},"snapshot":{"enable":true,"format":"jpg","quality":85,"upload":{"access_key":"minioadmin","bucket":"stress-test","endpoint":"http://10.0.0.49:9000","region":"us-east-1","secret_key":"minioadmin","type":"minio"}}},"enable":true,"eval_fps":5,"face_rules":[{"cooldown_ms":7000,"hit_window_ms":1500,"max_face_aspect":1.6,"min_face_area_ratio":0.01,"min_face_aspect":0.6,"min_hits":2,"min_sim":0.35,"name":"unknown_face","type":"unknown"},{"cooldown_ms":7000,"hit_window_ms":1500,"max_face_aspect":1.6,"min_face_area_ratio":0.01,"min_face_aspect":0.6,"min_hits":2,"min_sim":0.6,"name":"known_person","type":"person"}],"id":"alarm_face_cam1","labels":[],"role":"sink","rules":[],"type":"alarm"}]},{"edges":[["in_cam2","pre_cam2"],["in_cam2","pre_face_cam2"],["pre_cam2","yolo_cam2"],["yolo_cam2","trk_cam2"],["trk_cam2","osd_cam2"],["osd_cam2","post_cam2"],["post_cam2","pub_cam2"],["pub_cam2","alarm_cam2"],["pre_face_cam2","face_det_cam2"],["face_det_cam2","face_recog_cam2"],["face_recog_cam2","alarm_face_cam2"]],"name":"stress_cam2","nodes":[{"enable":true,"force_tcp":true,"fps":30,"height":720,"id":"in_cam2","reconnect_backoff_max_sec":30,"reconnect_sec":5,"role":"source","type":"input_rtsp","url":"rtsp://10.0.0.49:8554/cam","use_ffmpeg":false,"use_mpp":true,"width":1280},{"dst_format":"rgb","dst_h":640,"dst_packed":true,"dst_w":640,"enable":true,"id":"pre_cam2","keep_ratio":false,"rga_gate":"stress_cam2","role":"filter","type":"preprocess","use_rga":true},{"class_filter":[],"conf":0.35,"enable":true,"id":"yolo_cam2","infer_fps":10,"model_path":"./models/yolov5s-640-640.rknn","model_version":"v8","nms":0.45,"num_classes":80,"role":"filter","type":"ai_yolo"},{"conf":0.7,"enable":true,"id":"face_det_cam2","input_format":"rgb","max_faces":10,"model_path":"./models/RetinaFace_mobile320.rknn","nms":0.4,"output_landmarks":true,"role":"filter","type":"ai_face_det"},{"align":true,"emit_embedding":false,"enable":true,"gallery":{"backend":"sqlite","dtype":"auto","expected_dim":512,"load_on_start":true,"path":"./models/face_gallery.db"},"id":"face_recog_cam2","input_dtype":"uint8","input_format":"rgb","max_faces":10,"model_path":"./models/mobilefacenet_arcface.rknn","role":"filter","threshold":{"accept":0.45,"margin":0.05},"type":"ai_face_recog"},{"allowed_models":["yolov5","yolov8"],"debug":{"stats":false,"stats_interval":200},"enable":true,"high_th":0.5,"id":"trk_cam2","ignore_classes":[],"iou_th":0.3,"low_th":0.1,"max_age_ms":1500,"max_tracks":128,"min_hits":2,"mode":"bytetrack_lite","per_class":true,"role":"filter","state_key":"stress_cam2","track_classes":[0],"type":"tracker"},{"dst_format":"rgb","dst_h":0,"dst_packed":true,"dst_w":0,"enable":true,"id":"pre_face_cam2","keep_ratio":false,"rga_gate":"stress_cam2","role":"filter","type":"preprocess","use_rga":true},{"draw_bbox":true,"draw_face_bbox":false,"draw_face_det":false,"draw_text":true,"enable":true,"font_scale":1,"id":"osd_cam2","labels":[],"line_width":2,"role":"filter","type":"osd"},{"dst_format":"nv12","dst_h":720,"dst_w":1280,"enable":true,"id":"post_cam2","keep_ratio":false,"rga_gate":"stress_cam2","role":"filter","type":"preprocess","use_rga":true},{"bitrate_kbps":2000,"codec":"h264","enable":true,"fps":30,"gop":60,"id":"pub_cam2","outputs":[{"path":"/live/cam2","port":8556,"proto":"rtsp_server"},{"path":"./web/hls/cam2/index.m3u8","proto":"hls","segment_sec":2}],"role":"filter","type":"publish","use_ffmpeg_mux":true,"use_mpp":true},{"actions":{"clip":{"enable":true,"format":"mp4","fps":30,"post_sec":10,"pre_sec":5,"upload":{"access_key":"minioadmin","bucket":"stress-test","endpoint":"http://10.0.0.49:9000","region":"us-east-1","secret_key":"minioadmin","type":"minio"}},"external_api":{"channelNo":"${vod_channelNo}","enable":false,"getTokenUrl":"http://127.0.0.1:8080/api/getToken","include_media_url":true,"putMessageUrl":"http://127.0.0.1:8080/api/putMessage","tenantCode":"32","timeout_ms":3000,"token_cache_sec":1200,"token_header":"X-Access-Token","token_json_path":"responseBody.token"},"http":{"enable":false,"include_media_url":true,"method":"POST","timeout_ms":3000,"url":"http://127.0.0.1:8080/api/alarm"},"log":{"enable":true,"level":"info"},"snapshot":{"enable":true,"format":"jpg","quality":85,"upload":{"access_key":"minioadmin","bucket":"stress-test","endpoint":"http://10.0.0.49:9000","region":"us-east-1","secret_key":"minioadmin","type":"minio"}}},"enable":true,"eval_fps":10,"face_rules":[],"id":"alarm_cam2","labels":[],"role":"sink","rules":[{"class_ids":[0],"cooldown_ms":5000,"hit_window_ms":1500,"min_box_area_ratio":0.02,"min_duration_ms":1500,"min_hits":3,"min_score":0.4,"name":"person_in_view","per_track_cooldown_ms":5000,"require_track_id":true,"roi":{"h":1,"w":1,"x":0,"y":0}}],"type":"alarm"},{"actions":{"clip":{"enable":false},"http":{"enable":true,"include_media_url":true,"method":"POST","timeout_ms":3000,"url":"http://127.0.0.1:8080/api/alarm"},"log":{"enable":false,"level":"info"},"snapshot":{"enable":true,"format":"jpg","quality":85,"upload":{"access_key":"minioadmin","bucket":"stress-test","endpoint":"http://10.0.0.49:9000","region":"us-east-1","secret_key":"minioadmin","type":"minio"}}},"enable":true,"eval_fps":5,"face_rules":[{"cooldown_ms":7000,"hit_window_ms":1500,"max_face_aspect":1.6,"min_face_area_ratio":0.01,"min_face_aspect":0.6,"min_hits":2,"min_sim":0.35,"name":"unknown_face","type":"unknown"},{"cooldown_ms":7000,"hit_window_ms":1500,"max_face_aspect":1.6,"min_face_area_ratio":0.01,"min_face_aspect":0.6,"min_hits":2,"min_sim":0.6,"name":"known_person","type":"person"}],"id":"alarm_face_cam2","labels":[],"role":"sink","rules":[],"type":"alarm"}]},{"edges":[["in_cam3","pre_cam3"],["in_cam3","pre_face_cam3"],["pre_cam3","yolo_cam3"],["yolo_cam3","trk_cam3"],["trk_cam3","osd_cam3"],["osd_cam3","post_cam3"],["post_cam3","pub_cam3"],["pub_cam3","alarm_cam3"],["pre_face_cam3","face_det_cam3"],["face_det_cam3","face_recog_cam3"],["face_recog_cam3","alarm_face_cam3"]],"name":"stress_cam3","nodes":[{"enable":true,"force_tcp":true,"fps":30,"height":720,"id":"in_cam3","reconnect_backoff_max_sec":30,"reconnect_sec":5,"role":"source","type":"input_rtsp","url":"rtsp://10.0.0.49:8554/cam","use_ffmpeg":false,"use_mpp":true,"width":1280},{"dst_format":"rgb","dst_h":640,"dst_packed":true,"dst_w":640,"enable":true,"id":"pre_cam3","keep_ratio":false,"rga_gate":"stress_cam3","role":"filter","type":"preprocess","use_rga":true},{"class_filter":[],"conf":0.35,"enable":true,"id":"yolo_cam3","infer_fps":10,"model_path":"./models/yolov5s-640-640.rknn","model_version":"v8","nms":0.45,"num_classes":80,"role":"filter","type":"ai_yolo"},{"conf":0.7,"enable":true,"id":"face_det_cam3","input_format":"rgb","max_faces":10,"model_path":"./models/RetinaFace_mobile320.rknn","nms":0.4,"output_landmarks":true,"role":"filter","type":"ai_face_det"},{"align":true,"emit_embedding":false,"enable":true,"gallery":{"backend":"sqlite","dtype":"auto","expected_dim":512,"load_on_start":true,"path":"./models/face_gallery.db"},"id":"face_recog_cam3","input_dtype":"uint8","input_format":"rgb","max_faces":10,"model_path":"./models/mobilefacenet_arcface.rknn","role":"filter","threshold":{"accept":0.45,"margin":0.05},"type":"ai_face_recog"},{"allowed_models":["yolov5","yolov8"],"debug":{"stats":false,"stats_interval":200},"enable":true,"high_th":0.5,"id":"trk_cam3","ignore_classes":[],"iou_th":0.3,"low_th":0.1,"max_age_ms":1500,"max_tracks":128,"min_hits":2,"mode":"bytetrack_lite","per_class":true,"role":"filter","state_key":"stress_cam3","track_classes":[0],"type":"tracker"},{"dst_format":"rgb","dst_h":0,"dst_packed":true,"dst_w":0,"enable":true,"id":"pre_face_cam3","keep_ratio":false,"rga_gate":"stress_cam3","role":"filter","type":"preprocess","use_rga":true},{"draw_bbox":true,"draw_face_bbox":false,"draw_face_det":false,"draw_text":true,"enable":true,"font_scale":1,"id":"osd_cam3","labels":[],"line_width":2,"role":"filter","type":"osd"},{"dst_format":"nv12","dst_h":720,"dst_w":1280,"enable":true,"id":"post_cam3","keep_ratio":false,"rga_gate":"stress_cam3","role":"filter","type":"preprocess","use_rga":true},{"bitrate_kbps":2000,"codec":"h264","enable":true,"fps":30,"gop":60,"id":"pub_cam3","outputs":[{"path":"/live/cam3","port":8557,"proto":"rtsp_server"},{"path":"./web/hls/cam3/index.m3u8","proto":"hls","segment_sec":2}],"role":"filter","type":"publish","use_ffmpeg_mux":true,"use_mpp":true},{"actions":{"clip":{"enable":true,"format":"mp4","fps":30,"post_sec":10,"pre_sec":5,"upload":{"access_key":"minioadmin","bucket":"stress-test","endpoint":"http://10.0.0.49:9000","region":"us-east-1","secret_key":"minioadmin","type":"minio"}},"external_api":{"channelNo":"${vod_channelNo}","enable":false,"getTokenUrl":"http://127.0.0.1:8080/api/getToken","include_media_url":true,"putMessageUrl":"http://127.0.0.1:8080/api/putMessage","tenantCode":"32","timeout_ms":3000,"token_cache_sec":1200,"token_header":"X-Access-Token","token_json_path":"responseBody.token"},"http":{"enable":false,"include_media_url":true,"method":"POST","timeout_ms":3000,"url":"http://127.0.0.1:8080/api/alarm"},"log":{"enable":true,"level":"info"},"snapshot":{"enable":true,"format":"jpg","quality":85,"upload":{"access_key":"minioadmin","bucket":"stress-test","endpoint":"http://10.0.0.49:9000","region":"us-east-1","secret_key":"minioadmin","type":"minio"}}},"enable":true,"eval_fps":10,"face_rules":[],"id":"alarm_cam3","labels":[],"role":"sink","rules":[{"class_ids":[0],"cooldown_ms":5000,"hit_window_ms":1500,"min_box_area_ratio":0.02,"min_duration_ms":1500,"min_hits":3,"min_score":0.4,"name":"person_in_view","per_track_cooldown_ms":5000,"require_track_id":true,"roi":{"h":1,"w":1,"x":0,"y":0}}],"type":"alarm"},{"actions":{"clip":{"enable":false},"http":{"enable":true,"include_media_url":true,"method":"POST","timeout_ms":3000,"url":"http://127.0.0.1:8080/api/alarm"},"log":{"enable":false,"level":"info"},"snapshot":{"enable":true,"format":"jpg","quality":85,"upload":{"access_key":"minioadmin","bucket":"stress-test","endpoint":"http://10.0.0.49:9000","region":"us-east-1","secret_key":"minioadmin","type":"minio"}}},"enable":true,"eval_fps":5,"face_rules":[{"cooldown_ms":7000,"hit_window_ms":1500,"max_face_aspect":1.6,"min_face_area_ratio":0.01,"min_face_aspect":0.6,"min_hits":2,"min_sim":0.35,"name":"unknown_face","type":"unknown"},{"cooldown_ms":7000,"hit_window_ms":1500,"max_face_aspect":1.6,"min_face_area_ratio":0.01,"min_face_aspect":0.6,"min_hits":2,"min_sim":0.6,"name":"known_person","type":"person"}],"id":"alarm_face_cam3","labels":[],"role":"sink","rules":[],"type":"alarm"}]},{"edges":[["in_cam4","pre_cam4"],["in_cam4","pre_face_cam4"],["pre_cam4","yolo_cam4"],["yolo_cam4","trk_cam4"],["trk_cam4","osd_cam4"],["osd_cam4","post_cam4"],["post_cam4","pub_cam4"],["pub_cam4","alarm_cam4"],["pre_face_cam4","face_det_cam4"],["face_det_cam4","face_recog_cam4"],["face_recog_cam4","alarm_face_cam4"]],"name":"stress_cam4","nodes":[{"enable":true,"force_tcp":true,"fps":30,"height":720,"id":"in_cam4","reconnect_backoff_max_sec":30,"reconnect_sec":5,"role":"source","type":"input_rtsp","url":"rtsp://10.0.0.49:8554/cam","use_ffmpeg":false,"use_mpp":true,"width":1280},{"dst_format":"rgb","dst_h":640,"dst_packed":true,"dst_w":640,"enable":true,"id":"pre_cam4","keep_ratio":false,"rga_gate":"stress_cam4","role":"filter","type":"preprocess","use_rga":true},{"class_filter":[],"conf":0.35,"enable":true,"id":"yolo_cam4","infer_fps":10,"model_path":"./models/yolov5s-640-640.rknn","model_version":"v8","nms":0.45,"num_classes":80,"role":"filter","type":"ai_yolo"},{"conf":0.7,"enable":true,"id":"face_det_cam4","input_format":"rgb","max_faces":10,"model_path":"./models/RetinaFace_mobile320.rknn","nms":0.4,"output_landmarks":true,"role":"filter","type":"ai_face_det"},{"align":true,"emit_embedding":false,"enable":true,"gallery":{"backend":"sqlite","dtype":"auto","expected_dim":512,"load_on_start":true,"path":"./models/face_gallery.db"},"id":"face_recog_cam4","input_dtype":"uint8","input_format":"rgb","max_faces":10,"model_path":"./models/mobilefacenet_arcface.rknn","role":"filter","threshold":{"accept":0.45,"margin":0.05},"type":"ai_face_recog"},{"allowed_models":["yolov5","yolov8"],"debug":{"stats":false,"stats_interval":200},"enable":true,"high_th":0.5,"id":"trk_cam4","ignore_classes":[],"iou_th":0.3,"low_th":0.1,"max_age_ms":1500,"max_tracks":128,"min_hits":2,"mode":"bytetrack_lite","per_class":true,"role":"filter","state_key":"stress_cam4","track_classes":[0],"type":"tracker"},{"dst_format":"rgb","dst_h":0,"dst_packed":true,"dst_w":0,"enable":true,"id":"pre_face_cam4","keep_ratio":false,"rga_gate":"stress_cam4","role":"filter","type":"preprocess","use_rga":true},{"draw_bbox":true,"draw_face_bbox":false,"draw_face_det":false,"draw_text":true,"enable":true,"font_scale":1,"id":"osd_cam4","labels":[],"line_width":2,"role":"filter","type":"osd"},{"dst_format":"nv12","dst_h":720,"dst_w":1280,"enable":true,"id":"post_cam4","keep_ratio":false,"rga_gate":"stress_cam4","role":"filter","type":"preprocess","use_rga":true},{"bitrate_kbps":2000,"codec":"h264","enable":true,"fps":30,"gop":60,"id":"pub_cam4","outputs":[{"path":"/live/cam4","port":8558,"proto":"rtsp_server"},{"path":"./web/hls/cam4/index.m3u8","proto":"hls","segment_sec":2}],"role":"filter","type":"publish","use_ffmpeg_mux":true,"use_mpp":true},{"actions":{"clip":{"enable":true,"format":"mp4","fps":30,"post_sec":10,"pre_sec":5,"upload":{"access_key":"minioadmin","bucket":"stress-test","endpoint":"http://10.0.0.49:9000","region":"us-east-1","secret_key":"minioadmin","type":"minio"}},"external_api":{"channelNo":"${vod_channelNo}","enable":false,"getTokenUrl":"http://127.0.0.1:8080/api/getToken","include_media_url":true,"putMessageUrl":"http://127.0.0.1:8080/api/putMessage","tenantCode":"32","timeout_ms":3000,"token_cache_sec":1200,"token_header":"X-Access-Token","token_json_path":"responseBody.token"},"http":{"enable":false,"include_media_url":true,"method":"POST","timeout_ms":3000,"url":"http://127.0.0.1:8080/api/alarm"},"log":{"enable":true,"level":"info"},"snapshot":{"enable":true,"format":"jpg","quality":85,"upload":{"access_key":"minioadmin","bucket":"stress-test","endpoint":"http://10.0.0.49:9000","region":"us-east-1","secret_key":"minioadmin","type":"minio"}}},"enable":true,"eval_fps":10,"face_rules":[],"id":"alarm_cam4","labels":[],"role":"sink","rules":[{"class_ids":[0],"cooldown_ms":5000,"hit_window_ms":1500,"min_box_area_ratio":0.02,"min_duration_ms":1500,"min_hits":3,"min_score":0.4,"name":"person_in_view","per_track_cooldown_ms":5000,"require_track_id":true,"roi":{"h":1,"w":1,"x":0,"y":0}}],"type":"alarm"},{"actions":{"clip":{"enable":false},"http":{"enable":true,"include_media_url":true,"method":"POST","timeout_ms":3000,"url":"http://127.0.0.1:8080/api/alarm"},"log":{"enable":false,"level":"info"},"snapshot":{"enable":true,"format":"jpg","quality":85,"upload":{"access_key":"minioadmin","bucket":"stress-test","endpoint":"http://10.0.0.49:9000","region":"us-east-1","secret_key":"minioadmin","type":"minio"}}},"enable":true,"eval_fps":5,"face_rules":[{"cooldown_ms":7000,"hit_window_ms":1500,"max_face_aspect":1.6,"min_face_area_ratio":0.01,"min_face_aspect":0.6,"min_hits":2,"min_sim":0.35,"name":"unknown_face","type":"unknown"},{"cooldown_ms":7000,"hit_window_ms":1500,"max_face_aspect":1.6,"min_face_area_ratio":0.01,"min_face_aspect":0.6,"min_hits":2,"min_sim":0.6,"name":"known_person","type":"person"}],"id":"alarm_face_cam4","labels":[],"role":"sink","rules":[],"type":"alarm"}]}],"queue":{"size":8,"strategy":"drop_oldest"}}
\ No newline at end of file
diff --git a/docs/requirements/32视频内容识别集成方案3.docx b/docs/requirements/32视频内容识别集成方案3.docx
new file mode 100644
index 0000000..3b3345a
Binary files /dev/null and b/docs/requirements/32视频内容识别集成方案3.docx differ
diff --git a/docs/requirements/guide.md b/docs/requirements/guide.md
index 8e70142..6fe01c3 100644
--- a/docs/requirements/guide.md
+++ b/docs/requirements/guide.md
@@ -8,6 +8,12 @@ winget install Gyan.FFmpeg
- 安装完成后,关闭并重新打开终端,验证:
ffmpeg -version
+-查看本地摄像头信息
+ffmpeg -list_devices true -f dshow -i dummy
+
+- 本地运行RTSP服务器
+mediamtx.exe
+
- 推流到RTSP服务器(设置摄像头的分辨率为720P)
ffmpeg -f dshow -rtbufsize 100M -video_size 1280x720 -framerate 30 -vcodec mjpeg -i video="4K AutoFocus Webcam" -c:v libx264 -preset ultrafast -pix_fmt yuv420p -f rtsp rtsp://localhost:8554/cam
diff --git a/docs/stress_test_4ch_full_pipeline.md b/docs/stress_test_4ch_full_pipeline.md
new file mode 100644
index 0000000..c4b73ba
--- /dev/null
+++ b/docs/stress_test_4ch_full_pipeline.md
@@ -0,0 +1,526 @@
+# RK3588 4路全流程压力测试方案
+
+本文档描述在单台 RK3588 设备上进行 4 路全流程视频处理的压力测试方案。
+
+**方案说明:** 本方案使用单一 RTSP 视频源 (`rtsp://10.0.0.49:8554/test`),4 条流水线独立处理同一路输入,模拟 4 路摄像头并发场景。配置与 `sample_cam2.json` 完全一致。
+
+---
+
+## 1. 测试目标
+
+### 1.1 核心指标
+
+| 指标项 | 目标值 | 说明 |
+|--------|--------|------|
+| **并发路数** | 4 路 | 单设备同时处理 4 路视频 |
+| **输入分辨率** | 1280×720 @ 30fps | 每路输入为 720p 实时视频 |
+| **端到端延迟** | ≤ 500ms | 从采集到输出的完整延迟 |
+| **NPU 利用率** | ≥ 70% | NPU 高效利用,但不超载 |
+| **CPU 占用** | ≤ 50% | 预留系统余量 |
+| **内存占用** | ≤ 2.5GB | 避免内存压力 |
+| **运行稳定性** | 24 小时无崩溃 | 长稳测试 |
+
+### 1.2 全流程覆盖
+
+测试需覆盖所有节点类型(与 `sample_cam2.json` 一致的拓扑):
+
+```
+ [共享输入源]
+ │
+ ┌────────────────┼────────────────┐
+ │ │ │
+ [Pipeline 1] [Pipeline 2] [Pipeline 3] [Pipeline 4]
+ │ │ │ │
+ input_rtsp input_rtsp input_rtsp input_rtsp
+ │ │ │ │
+ preprocess preprocess preprocess preprocess
+ │ │ │ │
+ ai_yolo ──→ tracker ─┤ ai_yolo ──→ tracker (目标检测+跟踪)
+ │ │ │ │
+ ai_face_det ──→ ai_face_recog (人脸识别)
+ │ │ │ │
+ └──────────→ osd ←─────────────────────────────┘
+ │
+ preprocess (后处理)
+ │
+ publish (RTSP/HLS 输出)
+ │
+ ┌──────────┴──────────┐
+ │ │
+ alarm (目标) alarm_face (人脸)
+```
+
+**节点类型统计(每路,与 sample_cam2.json 一致):**
+- **Source (1)**:`input_rtsp`
+- **Filter (7)**:`preprocess`×2, `ai_yolo`, `ai_face_det`, `ai_face_recog`, `tracker`, `osd`
+- **Sink (3)**:`alarm`×2, `publish`
+
+**4路总计:**
+- 4 个 input_rtsp 节点(连接同一 RTSP 源)
+- 28 个 Filter 节点
+- 12 个 Sink 节点
+
+---
+
+## 2. 测试环境要求
+
+### 2.1 硬件要求
+
+| 项目 | 规格 | 说明 |
+|------|------|------|
+| **主控芯片** | RK3588 | 4×A76@2.4GHz + 4×A55@1.8GHz |
+| **NPU** | 6 TOPS@INT8 | 用于 AI 推理 |
+| **内存** | ≥ 8GB LPDDR4/LPDDR5 | 建议 8GB 或以上 |
+| **存储** | ≥ 32GB eMMC/SSD | 用于 HLS 切片存储 |
+| **网络** | 千兆以太网 | 单路 4-8Mbps 输入,4路约 16-32Mbps |
+| **散热** | 主动散热 | 长时间高负载运行 |
+
+### 2.2 软件要求
+
+| 项目 | 版本/配置 | 说明 |
+|------|-----------|------|
+| **操作系统** | Ubuntu 22.04 / Debian 11 | ARM64 架构 |
+| **内核** | Linux 5.10+ | 需支持 RGA、MPP、DMA-BUF |
+| **NPU 驱动** | rknpu driver 0.9.6+ | 建议最新版本 |
+| **CMake** | ≥ 3.20 | 构建工具 |
+| **GCC** | ≥ 10.0 | 编译器 |
+
+### 2.3 依赖库检查
+
+```bash
+# 检查 NPU 驱动
+ls /dev/rknpu
+
+# 检查 RGA 驱动
+ls /dev/rga
+
+# 检查内存
+free -h
+
+# 检查存储空间
+df -h
+```
+
+### 2.4 视频源要求
+
+| 项目 | 要求 | 说明 |
+|------|------|------|
+| **视频源类型** | RTSP Server | 建议使用 ffmpeg 或 ZLMediaKit 推流 |
+| **视频源数量** | 1 路共享流 | 4 条流水线共用同一输入 |
+| **视频规格** | 1280×720 @ 30fps, H.264 | 模拟真实摄像头 |
+| **码率** | 4-8 Mbps | 总输入带宽 4-8 Mbps |
+| **内容要求** | 包含行人和人脸 | 用于验证检测和识别功能 |
+
+**视频源准备命令(在 10.0.0.49 服务器执行):**
+
+```bash
+# 使用 ffmpeg 推送 1 路 720p 测试流
+ffmpeg -re -stream_loop -1 -i test_video.mp4 \
+ -c:v libx264 -preset fast -b:v 4M -r 30 -s 1280x720 \
+ -f rtsp rtsp://0.0.0.0:8554/test
+
+# 或使用 ZLMediaKit 等 RTSP 服务器
+```
+
+**测试视频内容建议:**
+- 包含行人移动(验证目标检测和跟踪)
+- 包含正脸人像(验证人脸识别)
+- 时长 5-10 分钟,循环播放
+
+### 2.5 外部服务要求
+
+| 服务 | 用途 | 配置 |
+|------|------|------|
+| **MinIO** | 报警截图/录像存储 | 地址: `10.0.0.49:9000`
Bucket: `stress-test`
账号: `minioadmin/minioadmin` |
+
+**MinIO 准备(在 10.0.0.49 上执行):**
+
+```bash
+# 创建 bucket
+mc alias set myminio http://10.0.0.49:9000 minioadmin minioadmin
+mc mb myminio/stress-test
+```
+
+---
+
+## 3. 测试配置文件
+
+### 3.1 主配置文件
+
+配置文件路径:`configs/stress_test_4ch_shared_source.json`
+
+**配置特点:**
+- 4 个独立 Graph(stress_cam1 ~ stress_cam4)
+- 所有 Graph 的输入 RTSP URL 相同:`rtsp://10.0.0.49:8554/test`
+- 每路独立输出到不同端口(8555-8558)
+- **节点拓扑和参数与 `sample_cam2.json` 完全一致**
+
+**节点配置详情(每路,与 sample_cam2.json 一致):**
+
+| 节点 | 类型 | 作用 | 关键参数 |
+|------|------|------|----------|
+| `input_rtsp` | source | 拉取 RTSP 流 | 1280×720@30fps, use_mpp=true |
+| `preprocess` | filter | 图像预处理 | 720p → 640×640 RGB, use_rga=true |
+| `ai_yolo` | filter | 目标检测 | yolov8n-640.rknn, v8, conf=0.35, nms=0.45, infer_fps=10 |
+| `ai_face_det` | filter | 人脸检测 | RetinaFace_mobile320.rknn, conf=0.7, max_faces=10 |
+| `ai_face_recog` | filter | 人脸识别 | mobilefacenet_arcface.rknn, align=true, max_faces=10 |
+| `tracker` | filter | 目标跟踪 | bytetrack_lite, max_age_ms=1500, track_classes=[0] |
+| `preprocess` | filter | 人脸分支预处理 | 原始分辨率 → RGB (pre_face) |
+| `osd` | filter | 屏幕显示 | draw_bbox=true, draw_text=true, line_width=2 |
+| `preprocess` | filter | 后处理 | 640×640 → 720p NV12, use_rga=true |
+| `publish` | sink | 视频输出 | h264, 30fps, 2000kbps, RTSP+HLS |
+| `alarm` | sink | 目标报警 | eval_fps=10, MinIO上传截图+录像 |
+| `alarm_face` | sink | 人脸报警 | eval_fps=5, MinIO上传截图 |
+
+**输出端口分配:**
+
+| Graph | RTSP 端口 | HLS 路径 |
+|-------|-----------|----------|
+| stress_cam1 | 8555 | `./web/hls/cam1/index.m3u8` |
+| stress_cam2 | 8556 | `./web/hls/cam2/index.m3u8` |
+| stress_cam3 | 8557 | `./web/hls/cam3/index.m3u8` |
+| stress_cam4 | 8558 | `./web/hls/cam4/index.m3u8` |
+
+**模型文件清单:**
+
+| 模型文件 | 路径 | 说明 |
+|----------|------|------|
+| yolov8n-640.rknn | `./models/yolov8n-640.rknn` | YOLOv8 目标检测 |
+| RetinaFace_mobile320.rknn | `./models/RetinaFace_mobile320.rknn` | 人脸检测 |
+| mobilefacenet_arcface.rknn | `./models/mobilefacenet_arcface.rknn` | 人脸识别 |
+| face_gallery.db | `./models/face_gallery.db` | 人脸库 |
+
+---
+
+## 4. 测试步骤
+
+### 4.1 环境准备
+
+```bash
+# 1. 检查 NPU 驱动
+ls /dev/rknpu
+
+# 2. 检查内存
+free -h
+
+# 3. 检查存储空间
+df -h
+
+# 4. 准备模型文件
+ls models/
+# 应有:yolov8n-640.rknn, RetinaFace_mobile320.rknn, mobilefacenet_arcface.rknn, face_gallery.db
+
+# 5. 创建 HLS 输出目录
+mkdir -p web/hls/cam{1,2,3,4}
+
+# 6. 检查网络连接
+ping 10.0.0.49
+```
+
+### 4.2 启动视频源
+
+在服务器(10.0.0.49)上执行:
+
+```bash
+# 推送单路 720p 测试流
+ffmpeg -re -stream_loop -1 -i test_video.mp4 \
+ -c:v libx264 -preset fast -b:v 4M -r 30 -s 1280x720 \
+ -f rtsp rtsp://0.0.0.0:8554/cam1
+```
+
+### 4.3 启动测试程序
+
+```bash
+cd /home/orangepi/apps/OrangePi3588Media
+
+# 启动 media-server
+./build/media-server --config configs/stress_test_4ch_shared_source.json
+```
+
+### 4.4 监控脚本
+
+创建监控脚本 `monitor_4ch.sh`:
+
+```bash
+#!/bin/bash
+
+LOG_FILE="stress_test_4ch_$(date +%Y%m%d_%H%M%S).log"
+
+echo "Time,CPU%,MemMB,NPU%,Temp°C" > $LOG_FILE
+
+while true; do
+ TIME=$(date '+%H:%M:%S')
+
+ # CPU 使用率
+ CPU=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1)
+
+ # 内存使用 (MB)
+ MEM=$(free -m | awk 'NR==2{printf "%.0f", $3}')
+
+ # NPU 使用率
+ NPU=$(cat /sys/kernel/debug/rknpu/load 2>/dev/null || echo "0")
+
+ # CPU 温度
+ TEMP=$(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null | awk '{print $1/1000}')
+
+ echo "$TIME,$CPU,$MEM,$NPU,$TEMP" >> $LOG_FILE
+ echo "[$TIME] CPU:${CPU}% Mem:${MEM}MB NPU:${NPU}% Temp:${TEMP}°C"
+
+ sleep 5
+done
+```
+
+运行监控:
+```bash
+chmod +x monitor_4ch.sh
+./monitor_4ch.sh
+```
+
+---
+
+## 5. 测试结果验证方法
+
+### 5.1 功能验证
+
+| 验证项 | 方法 | 通过标准 |
+|--------|------|----------|
+| **输入连接** | 查看日志 | 4 路都显示 "connected" |
+| **视频输出** | VLC 播放 | `rtsp://:8555/live/cam1` ~ `:8558/live/cam4` 都能正常播放 |
+| **HLS 输出** | 浏览器/播放器 | 4 路 HLS 流可正常播放 |
+| **目标检测** | 观察 OSD 输出 | 画面中出现检测框和 "person" 标签 |
+| **人脸识别** | 观察 OSD 输出 | 人脸被框选,显示姓名或 "unknown" |
+| **目标跟踪** | 观察 OSD 输出 | 同一目标的 ID 保持稳定 |
+| **报警触发** | 查看日志 | alarm 节点输出 "person_detect" 或 "unknown_face" |
+| **MinIO 上传** | 检查 MinIO | `stress-test` bucket 中有截图/录像文件 |
+| **4路并发** | 同时播放4路输出 | 4路输出画面都流畅,无明显卡顿 |
+
+### 5.2 性能验证
+
+| 指标 | 测量方法 | 通过标准 |
+|------|----------|----------|
+| **帧率** | 日志中 node_output_fps | ≥ 25 fps(每路) |
+| **延迟** | 对比输入输出时间戳 | ≤ 500ms |
+| **NPU 利用率** | `/sys/kernel/debug/rknpu/load` | 50-90% |
+| **CPU 占用** | `top` 命令 | ≤ 50% |
+| **内存占用** | `free` 命令 | ≤ 2.5GB |
+| **队列积压** | 日志中 queue_length | 不持续增长 |
+
+### 5.3 稳定性验证
+
+| 测试项 | 时长 | 通过标准 |
+|--------|------|----------|
+| **短稳测试** | 1 小时 | 无崩溃,无内存泄漏 |
+| **中稳测试** | 8 小时 | 性能指标稳定,无异常重启 |
+| **长稳测试** | 24 小时 | 无崩溃,内存增长 < 100MB |
+
+### 5.4 验证命令示例
+
+```bash
+# 1. 检查 4 路进程状态
+ps aux | grep media-server
+
+# 2. 查看 NPU 负载
+watch -n 1 cat /sys/kernel/debug/rknpu/load
+
+# 3. 查看资源使用
+htop
+
+# 4. 播放测试(另开终端)
+ffplay rtsp://localhost:8555/live/cam1 &
+ffplay rtsp://localhost:8556/live/cam2 &
+ffplay rtsp://localhost:8557/live/cam3 &
+ffplay rtsp://localhost:8558/live/cam4 &
+
+# 5. 检查 HLS 文件生成
+ls -la web/hls/cam*/
+
+# 6. 查看日志中的报警
+tail -f media-server.log | grep alarm
+
+# 7. 检查 MinIO 上传(在 10.0.0.49 上)
+mc ls myminio/stress-test
+```
+
+---
+
+## 6. 预期结果
+
+### 6.1 正常情况
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ 4 路 720p 全流程测试预期结果 │
+├─────────────────────────────────────────────────────────┤
+│ • 4 路视频正常解码、处理、输出 │
+│ • 端到端延迟: 200-400ms │
+│ • NPU 利用率: 70-90% │
+│ • CPU 占用: 30-50% │
+│ • 内存占用: 1.5-2.5GB │
+│ • 无丢帧或轻微丢帧 (< 1%) │
+│ • 报警功能正常触发,MinIO上传成功 │
+│ • 4路输出画面质量一致 │
+└─────────────────────────────────────────────────────────┘
+```
+
+### 6.2 性能数据记录表
+
+| 指标 | 测试前 | 1小时 | 8小时 | 24小时 |
+|------|--------|-------|-------|--------|
+| CPU 平均占用 | - | | | |
+| CPU 峰值占用 | - | | | |
+| 内存占用 (MB) | - | | | |
+| NPU 平均利用率 | - | | | |
+| NPU 峰值利用率 | - | | | |
+| 平均延迟 (ms) | - | | | |
+| 帧率 cam1 (fps) | - | | | |
+| 帧率 cam2 (fps) | - | | | |
+| 帧率 cam3 (fps) | - | | | |
+| 帧率 cam4 (fps) | - | | | |
+| 报警次数 (目标) | - | | | |
+| 报警次数 (人脸) | - | | | |
+| MinIO 文件数 | - | | | |
+| 异常重启次数 | - | | | |
+
+### 6.3 输出验证清单
+
+- [ ] 4 路 RTSP 流均可正常播放
+- [ ] 4 路 HLS 流均可正常播放
+- [ ] 画面中有检测框(OSD 正常工作)
+- [ ] 检测到人脸时显示姓名或 "unknown"
+- [ ] 目标 ID 保持稳定(跟踪正常工作)
+- [ ] 日志中有报警输出
+- [ ] MinIO `stress-test` bucket 中有文件上传
+- [ ] 无 Error/Warning 刷屏
+
+---
+
+## 7. 故障排查
+
+### 7.1 常见问题
+
+| 现象 | 可能原因 | 解决方法 |
+|------|----------|----------|
+| **启动失败** | NPU 驱动未加载 | `ls /dev/rknpu` 检查,重新加载驱动 |
+| **拉流失败** | 网络不通/视频源问题 | 检查网络,`ping 10.0.0.49`,验证 RTSP 流 |
+| **高 CPU 占用** | RGA 未启用 | 检查配置 `use_rga: true`,`use_mpp: true` |
+| **高延迟** | 队列积压 | 检查 `queue.size`,适当调小 |
+| **NPU 利用率低** | 推理频率设置过低 | 调整 `infer_fps` |
+| **4路中某路无输出** | 端口冲突 | 检查端口 8555-8558 是否被占用 |
+| **OSD 不显示** | 格式不支持 | 确保 OSD 输入为 RGB/BGR/NV12 格式 |
+| **MinIO 上传失败** | 网络/认证问题 | 检查 MinIO 地址和账号密码 |
+
+### 7.2 调试命令
+
+```bash
+# 查看 NPU 状态
+cat /sys/kernel/debug/rknpu/version
+cat /sys/kernel/debug/rknpu/load
+
+# 查看 RGA 状态
+cat /sys/kernel/debug/rga/info
+
+# 查看进程资源使用
+pidof media-server | xargs -I {} ps -p {} -o pid,ppid,cmd,%cpu,%mem
+
+# 查看线程数
+ps -eLf | grep media-server | wc -l
+
+# 查看端口占用
+netstat -tlnp | grep media-server
+
+# 查看系统日志
+sudo dmesg | tail -50
+```
+
+### 7.3 日志分析
+
+```bash
+# 检查错误
+grep -i "error\|fail\|warn" media-server.log | head -20
+
+# 检查各路口连接状态
+grep "input_rtsp\|connected\|disconnected" media-server.log
+
+# 检查帧率
+grep "output_fps" media-server.log | tail -20
+
+# 检查报警触发
+grep "alarm" media-server.log | tail -20
+
+# 检查 MinIO 上传
+grep "minio\|snapshot\|clip" media-server.log | tail -20
+```
+
+---
+
+## 8. 测试报告模板
+
+### 8.1 基本信息
+
+- **测试时间**:
+- **测试人员**:
+- **设备型号**:
+- **固件版本**:
+- **软件版本**:
+- **视频源**: `rtsp://10.0.0.49:8554/test` (1280×720@30fps)
+- **MinIO**: `http://10.0.0.49:9000`, bucket: `stress-test`
+
+### 8.2 测试结论
+
+- [ ] 4 路全流程测试通过
+- [ ] 性能指标达标
+- [ ] 稳定性测试通过
+- [ ] MinIO 上传功能正常
+
+### 8.3 详细数据
+
+(填入第 6 章的表格数据)
+
+### 8.4 问题记录
+
+| 序号 | 问题描述 | 严重程度 | 状态 |
+|------|----------|----------|------|
+| 1 | | | |
+| 2 | | | |
+
+### 8.5 优化建议
+
+(根据测试结果填写)
+
+---
+
+## 9. 附录
+
+### 9.1 快速开始
+
+```bash
+# 1. 准备环境
+cd /home/orangepi/apps/OrangePi3588Media
+mkdir -p web/hls/cam{1,2,3,4}
+
+# 2. 确认模型存在
+ls models/yolov8n-640.rknn models/RetinaFace_mobile320.rknn \
+ models/mobilefacenet_arcface.rknn models/face_gallery.db
+
+# 3. 启动测试
+./build/media-server --config configs/stress_test_4ch_shared_source.json
+
+# 4. 另开终端监控
+./monitor_4ch.sh
+
+# 5. 验证输出
+ffplay rtsp://localhost:8555/live/cam1
+```
+
+### 9.2 相关文档
+
+- [Readme.md](../Readme.md) - 项目总览
+- [deployment.md](deployment.md) - 部署说明
+- [dag_graph_node_edge.md](architecture/dag_graph_node_edge.md) - 架构说明
+
+### 9.3 配置文件清单
+
+| 文件 | 说明 |
+|------|------|
+| `configs/stress_test_4ch_shared_source.json` | 4路全流程测试配置(共享源,与 sample_cam2.json 一致) |
+| `monitor_4ch.sh` | 资源监控脚本 |
diff --git a/models/best-640.rknn b/models/best-640.rknn
new file mode 100644
index 0000000..bfd23a4
Binary files /dev/null and b/models/best-640.rknn differ
diff --git a/models/yolov5s-640-640.rknn b/models/yolov5s-640-640.rknn
new file mode 100644
index 0000000..87419fd
Binary files /dev/null and b/models/yolov5s-640-640.rknn differ
diff --git a/models/yolov8n-640.rknn b/models/yolov8n-640.rknn
new file mode 100644
index 0000000..7439f41
Binary files /dev/null and b/models/yolov8n-640.rknn differ
diff --git a/scripts/d8_1.py b/scripts/d8_1.py
new file mode 100644
index 0000000..bb7368c
--- /dev/null
+++ b/scripts/d8_1.py
@@ -0,0 +1,1206 @@
+# 开发者 haotian
+# 开发时间: 2024/9/20 21:14
+'''
+ v7_p1_1.5.1 的 改进版
+ 将文件路径等做成可配置的
+'''
+
+'''
+
+ 在1.5 的基础上修改了判断人脸的逻辑
+'''
+
+'''
+ 人脸识别直接新开一个线程算了
+
+'''
+
+'''
+ 添加人脸识别,新的告警逻辑。
+ 改进了告警的逻辑。
+ 没写上传minio
+ 加上上传minio
+ 加上post警告,测试环境注释掉了。
+
+
+ 测试环境
+ 1.注释掉获取get_token
+ 2.注释掉minio上传文件 self.minio_client.put_object
+ 3.注释掉 send_post_request
+ 4.注释掉 os.remove
+'''
+
+
+
+
+import ctypes
+import os
+import shutil
+import random
+import sys
+import threading
+import time
+import cv2
+import numpy as np
+import pycuda.autoinit
+import pycuda.driver as cuda
+import tensorrt as trt
+import queue
+from minio import Minio
+import yaml
+import threading
+import subprocess
+import uuid
+import requests
+import json
+import datetime
+
+
+from compreface import CompreFace
+from compreface.service import RecognitionService
+# from PIL import Image
+
+CONF_THRESH = 0.65
+IOU_THRESHOLD = 0.4
+
+with open('config.yaml', 'r') as file:
+ configData = yaml.safe_load(file)
+
+# Minio实例化, 用于云端存储文件
+client = Minio(
+ endpoint=configData['minioConfig']['endpoint'],
+ access_key=configData['minioConfig']['access_key'],
+ secret_key=configData['minioConfig']['secret_key'],
+ secure=configData['minioConfig']['secure']
+)
+
+# 保存token
+tokenResult = {}
+getTokenUrl = configData['dataConfig']['getTokenUrl']
+
+# 告警信息url
+putMessageUrl = configData['dataConfig']['putMessageUrl']
+
+# ip和文件目录标识符
+ip = configData['video_config']['v0_ip']
+
+# 所有模型类别
+categories = configData['video_config']['categories']
+
+# 输出m3u8文件地址
+m3u8_path = configData['video_config']['m3u8_path']
+
+# 图片/视频文件的暂存路径
+save_path = configData['video_config']['save_path']
+
+# 视频源地址
+vod_path = configData['video_config']['v0_path']
+
+# 人脸识别暂存文件地址
+people_save_path = configData['video_config']['people_save_path']
+
+
+# 模型文件地址
+engine_path = configData['engine_path']
+
+vod_channelNo = configData['video_config']['v0_channelNo']
+
+# 要检测的类别
+testclasses = configData['video_config']['v0_testclasses']
+
+# 请求超时时间
+time_interval = configData['dataConfig']['timeInterval']
+command_mid = [
+ 'ffmpeg',
+ '-i', '-', # 从标准输入读取视频帧
+ '-c:v', 'libx264', # 使用 H.264 编码
+ '-b:v', '500k', # 设置视频比特率
+ '-preset', 'superfast', # 编码速度
+ '-tune', 'zerolatency', # 低延迟
+ '-crf', '23', # 使用 CRF 模式来控制视频质量
+ '-s', '1280x720', # 设置分辨率
+ '-an', # 禁用音频
+ '-loglevel', 'error',
+ # '-f', 'flv', # 输出格式
+ # 'rtmp://127.0.0.1:1935/live/1' # 输出到 RTMP 服务器
+ '-hls_time', '4',
+ '-hls_list_size', '2',
+ '-hls_flags', 'delete_segments',
+ '-f', 'hls',
+ f'{m3u8_path}'+ ip + '/index.m3u8'
+]
+
+# 人脸识别部分
+
+DOMAIN: str = configData['compreface_service']['domain']
+PORT: str = configData['compreface_service']['port']
+API_KEY: str = configData['compreface_service']['api_key']
+LIMIT: str = configData['compreface_service']['limit']
+Det_prob_threshold: str = configData['compreface_service']['det_prob_threshold']
+
+# 人脸识别客户端
+compre_face: CompreFace = CompreFace(DOMAIN, PORT,options={'limit': LIMIT,"det_prob_threshold":Det_prob_threshold})
+recognition: RecognitionService = compre_face.init_face_recognition(API_KEY)
+
+
+pipeline_mid = subprocess.Popen(command_mid, shell=False, stdin=subprocess.PIPE)
+
+frames = [None] * 6
+# 拉流缓存
+rtsp_frame_buffer = queue.Queue(maxsize=300)
+
+# 全局人名字典,每天0点清空??
+d_face = dict()
+
+
+# 获取token和对应时间 存入字典
+def get_token(tokenResult):
+ if 'token' in tokenResult and 'current_time' in tokenResult:
+ token_time = datetime.datetime.strptime(tokenResult['current_time'],
+ "%Y-%m-%d %H:%M:%S")
+ current_time = datetime.datetime.now()
+ time_diff = current_time - token_time
+ if time_diff.total_seconds() > 20 * 60:
+ # 过期重新请求 token
+ # print("token 已过期")
+ response = requests.post(getTokenUrl)
+ if response.status_code == 200:
+ data = json.loads(response.text)
+ if 'retCode' in data and data['retCode'] == '200':
+ token = data['responseBody']['token']
+ current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ tokenResult['token'] = token
+ tokenResult['current_time'] = current_time
+ else:
+ tokenResult['error'] = data['errorDesc']
+ else:
+ tokenResult['error'] = response.status_code
+ token = tokenResult['token']
+ return token
+
+
+def send_post_request(url, token, msg, picUrl, videoUrl):
+ payload = {
+ "tenantCode": "32",
+ "channelNo": vod_channelNo,
+ "alarmContent": msg,
+ "alarmTime": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+ "picInfo": [
+ {"url": picUrl}
+ ],
+ "videoInfo": [
+ {"url": videoUrl}
+ ]
+ }
+ headers = {
+ 'X-Access-Token': token,
+ 'Content-Type': 'application/json'
+ }
+ # print(url)
+ # print(headers)
+ # print(payload)
+ response = requests.post(url, headers=headers, data=json.dumps(payload))
+ #print(response)
+
+
+def clear_folder(ip):
+ folder_path1 = m3u8_path+ip
+ # 判断文件夹是否存在
+ if not os.path.exists(folder_path1):
+ print(f"文件夹 {folder_path1} 不存在!")
+ os.mkdir(folder_path1)
+ # return
+
+ # 判断文件夹是否为空
+ if not os.listdir(folder_path1):
+ print(f"文件夹 {folder_path1} 为空,无需清空!")
+ else:
+ # 清空文件夹1
+ for filename in os.listdir(folder_path1):
+ file_path = os.path.join(folder_path1, filename)
+ if os.path.isfile(file_path):
+ os.remove(file_path)
+ elif os.path.isdir(file_path):
+ shutil.rmtree(file_path)
+ print(f"已清空文件夹 {folder_path1} 的内容!")
+
+
+def restart_program():
+ """重新启动当前程序"""
+ python = sys.executable # 获取当前 Python 解释器的路径
+ # print("Restarting program...")
+ time.sleep(1) # 可选延迟,确保用户看到提示
+ os.execl(python, python, *sys.argv) # 使用相同的参数重新启动当前脚本
+
+
+def verify_bbox_class(classid_list, save_flag):
+ if "face" in classid_list:
+ save_flag.append("face")
+ return True
+ if "shoe" in classid_list:
+ save_flag.append("shoe")
+ return True
+ if "phone" in classid_list:
+ save_flag.append("phone")
+ return True
+ return False
+
+
+# after_time 距离 before_time 是否在 time_num秒 之内,若在 返回true 不在返回false
+def verify_timenum(before_time, after_time, time_num):
+ if before_time is None:
+ return True
+ time_difference = after_time - before_time
+ if time_difference < time_num:
+ return True
+ else:
+ return False
+
+
+# 该方法用于判断两个帧是否相同
+def compare_frames(frame1, frame2, threshold):
+ difference = cv2.absdiff(frame1, frame2)
+ diff_gray = cv2.cvtColor(difference, cv2.COLOR_BGR2GRAY)
+ _, thresholded_diff = cv2.threshold(diff_gray, threshold, 350, cv2.THRESH_BINARY)
+
+ return np.sum(thresholded_diff) == 0
+
+
+def white_color_ratio(image):
+ image = image.copy()
+ # 将图像从BGR颜色空间转换为HSV颜色空间
+ hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
+ # 定义白色的HSV范围(在此示例中使用了一组简单的范围)
+ lower_white = np.array([0, 0, 200], dtype=np.uint8)
+ upper_white = np.array([180, 30, 255], dtype=np.uint8)
+ # 创建遮罩,将白色和白灰色区域设置为白色,其他区域设置为黑色
+ white_mask = cv2.inRange(hsv_image, lower_white, upper_white)
+ # 计算白色和白灰色区域的像素数
+ white_pixels = np.count_nonzero(white_mask)
+ # 计算图像中白色和白灰色的占比
+ total_pixels = image.shape[0] * image.shape[1]
+ white_ratio = white_pixels / total_pixels
+ return white_ratio
+
+
+def shoe_color_ratio(image):
+ # 将图像从BGR颜色空间转换为HSV颜色空间
+ hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
+ # 定义黑色的HSV范围(在此示例中使用了一组简单的范围)
+ lower_black = np.array([0, 0, 0], dtype=np.uint8)
+ upper_black = np.array([180, 255, 80], dtype=np.uint8)
+ # 创建遮罩,将黑色区域设置为白色,其他区域设置为黑色
+ black_mask = cv2.inRange(hsv_image, lower_black, upper_black)
+ # 计算黑色区域的像素数
+ black_pixels = np.count_nonzero(black_mask)
+ # 计算图像中黑色的占比
+ total_pixels = image.shape[0] * image.shape[1]
+ black_ratio = black_pixels / total_pixels
+ return black_ratio
+
+def skin_color_ratio(image):
+ # 将图像从BGR颜色空间转换为HSV颜色空间
+ # 定义肤色的HSV范围(在此示例中使用了一组简单的范围)
+ hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
+ lower_skin = np.array([0, 20, 70], dtype=np.uint8)
+ upper_skin = np.array([20, 255, 255], dtype=np.uint8)
+ # 创建遮罩,将肤色区域设置为白色,其他区域设置为黑色
+ skin_mask = cv2.inRange(hsv_image, lower_skin, upper_skin)
+ # 计算肤色区域的像素数
+ skin_pixels = np.count_nonzero(skin_mask)
+ # 计算图像中人体肤色的占比
+ total_pixels = image.shape[0] * image.shape[1]
+ skin_ratio = skin_pixels / total_pixels
+ return skin_ratio
+
+
+def get_img_path_batches(batch_size, img_dir):
+ ret = []
+ batch = []
+ for root, dirs, files in os.walk(img_dir):
+ for name in files:
+ if len(batch) == batch_size:
+ ret.append(batch)
+ batch = []
+ batch.append(os.path.join(root, name))
+ if len(batch) > 0:
+ ret.append(batch)
+ return ret
+
+
+# 画框 返回True 为需要报警的选项,返回Flase,为不需要报警的
+def plot_one_box(x, img, color=[0, 255, 0], label=None, line_thickness=2):
+
+ # print("label: ", label)
+
+ c1, c2 = (int(x[0]), int(x[1])), (int(x[2]), int(x[3]))
+ # msg = ''
+
+ if label == 'shoe':
+ print('识别到鞋子')
+ region_of_interest = img[c1[1]:c2[1], c1[0]:c2[0]]
+ source = shoe_color_ratio(region_of_interest)
+ print(f'识别到鞋子, 比率:{source}')
+ # white_sorce = white_color_ratio(region_of_interest)
+ # 鞋子黑色面积必须大于整体框的百分之50 并且框的宽度小于40,高度小于30
+ if source < 0.4:
+ # 画框
+ color = [0, 0, 255]
+ cv2.rectangle(img, c1, c2, color, thickness=line_thickness, lineType=cv2.LINE_AA)
+ return [1, '未穿戴劳保鞋']
+
+ if label == 'face':
+
+ region_of_interest = img[c1[1]:c2[1], c1[0]:c2[0]]
+ face_source = skin_color_ratio(region_of_interest)
+
+ print("识别到人脸, 人脸比率: ", face_source)
+ # white_sorce = white_color_ratio(region_of_interest)
+ # print("判断存在脸,且肤色为" + str(face_source))
+ # 肤色面积必须大于整体框的百分之70 框的宽度大于10
+ if face_source < 0.8:
+ # 画框
+ color = [0, 0, 255]
+ cv2.rectangle(img, c1, c2, color, thickness=1, lineType=cv2.LINE_AA)
+
+ print("face")
+
+ return [1, '未佩戴口罩']
+ if label == 'phone':
+
+ print("phone")
+ color = [0, 0, 255]
+ cv2.rectangle(img, c1, c2, color, thickness=line_thickness, lineType=cv2.LINE_AA)
+ return [1, '识别到手机']
+
+ if label == 'e-bike':
+ print("e-bike")
+ color = [0, 0, 255]
+ cv2.rectangle(img, c1, c2, color, thickness=line_thickness, lineType=cv2.LINE_AA)
+ return [1, '识别到电动车']
+
+ if label == 'non-Helmet':
+ print("non-Helmet")
+ color = [0, 0, 255]
+ cv2.rectangle(img, c1, c2, color, thickness=line_thickness, lineType=cv2.LINE_AA)
+ return [1, '识别到未佩戴安全帽']
+
+ return [0, ""]
+
+
+# def check_save_flag(save_flag):
+# # 定义需要检查的类别
+# categories = ["shoe"]
+# # 找出在save_flag中的类别
+# matched_categories = [category for category in categories if category in save_flag]
+# # 用'-'连接匹配的类别并返回
+# return "-".join(matched_categories)
+
+
+# 计算图像的模糊度
+def calculate_blur(frame):
+ # 将图片转换为灰度图
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
+ # 计算灰度图的方差
+ blur_value = cv2.Laplacian(gray, cv2.CV_64F).var()
+ return blur_value
+
+
+# 先前陌生人数
+p_s_num = 0
+class FaceRecUpload(threading.Thread):
+ def __init__(self, ip, frame, token):
+ threading.Thread.__init__(self)
+ self.ip = ip
+ self.frame = frame.copy()
+ # self.result = result
+ self.new_face = False
+ self.token = token
+ self.minio_client = client
+
+ def run(self):
+ global p_s_num
+
+ # blur = calculate_blur(self.frame)
+
+ # print(f'\n模糊度:{blur}')
+ et, jpeg_frame = cv2.imencode('.jpg', self.frame)
+ # self.ffmpeg_frame_buffer.put(jpeg_frame.tobytes())
+ now_time = time.time()
+ result = recognition.recognize(jpeg_frame.tobytes())
+
+ if 'message' not in result:
+
+ warning = list()
+
+ # 人脸识别检测
+ for i in range(len(result['result'])):
+ # print(self.result['result'][i]['subjects'][0]['subject'], self.result['result'][i]['subjects'][0]['similarity'])
+ # print(self.result['result'][i]['box'])
+ x1, y1, x2, y2 = result['result'][i]['box']['x_min'], result['result'][i]['box']['y_min'], \
+ result['result'][i]['box']['x_max'], result['result'][i]['box']['y_max']
+
+ if self.frame[x1:x2+1, y1:y2+1].size > 0:
+ blur = calculate_blur(self.frame[x1:x2+1, y1:y2+1])
+ else:
+
+ blur = 0
+ # print(f'人脸模糊度{blur}')
+ if blur > 10:
+ if 0.3 < result['result'][i]['subjects'][0]['similarity'] < 0.4:
+ # 设置文本参数
+ font = cv2.FONT_HERSHEY_SIMPLEX
+ font_scale = 1
+ color = (255, 0, 0) # BGR颜色
+ thickness = 2
+ cv2.rectangle(self.frame, (x1, y1), (x2, y2), (255, 0, 0), 2)
+ cv2.putText(self.frame, f'warning {result["result"][i]["subjects"][0]["similarity"]}',
+ (x1, y1 - 20), font, font_scale, color, thickness,
+ cv2.LINE_AA)
+ warning.append([(x1, y1), (x2, y2)])
+ # 垃圾帧丢弃
+ elif result['result'][i]['subjects'][0]['similarity'] <= 0.3:
+ # print('\n丢弃的人脸识别帧')
+ pass
+ else:
+ if result['result'][i]['subjects'][0]['subject'] not in d_face:
+ # 记录每人第一次人脸识别记录
+ d_face[result['result'][i]['subjects'][0]['subject']] = time.time()
+ self.new_face = True
+ print(
+ f'\n新人打卡, 员工名:{result["result"][i]["subjects"][0]["subject"]} ,相似度:{result["result"][i]["subjects"][0]["similarity"]},打卡时间{datetime.datetime.now()}')
+ uuid_str = str(uuid.uuid4())[:6] + str(int(time.time()))
+ img_object_name = f"{save_path}{result['result'][i]['subjects'][0]['subject']}_{uuid_str}_{vod_channelNo}.jpg"
+ cv2.imwrite(img_object_name, self.frame)
+ # print(f'\n/home/admin-root/haotian/jingzhu1.1/tensorrtx-master/yolov8/attendance/{datetime.datetime.now().date()}.txt')
+ with open(
+ f'{people_save_path}{datetime.datetime.now().date()}.txt',
+ 'a') as f:
+ f.write(
+ f'新人打卡, 员工名:{result["result"][i]["subjects"][0]["subject"]} ,相似度:{result["result"][i]["subjects"][0]["similarity"]},打卡时间{datetime.datetime.now()} \n')
+ else:
+ pass
+ # print('\r模糊帧丢弃', end='')
+
+ if len(warning) > p_s_num:
+ p_s_num = len(warning)
+ uuid_str = str(uuid.uuid4())[:6] + str(int(time.time())) # 生成UUID的前6位,前6位,有不小的概率重复,再加上时间戳。
+ img_object_name = f"{save_path}face_{uuid_str}_{vod_channelNo}.jpg"
+ cv2.imwrite(img_object_name, self.frame)
+
+ # with open(img_object_name, 'rb') as file_data:
+ # file_data.seek(0, os.SEEK_END)
+ # file_size = file_data.tell()
+ # file_data.seek(0)
+ # self.minio_client.put_object(configData['minioConfig']['bucket_name'],
+ # f'{uuid_str}_{vod_channelNo}_stranger_.jpg', file_data,
+ # file_size)
+
+ # os.remove(img_object_name)
+ upload_http_url_img = configData['minioConfig'][
+ 'bucket_name'] + f'/{uuid_str}_{vod_channelNo}_stranger_.jpg'
+ # send_post_request(putMessageUrl, self.token, '陌生人警告', upload_http_url_img, '')
+ print('\n陌生人警告')
+ #print('陌生人图片保存完成')
+ elif len(warning) < p_s_num:
+ p_s_num = len(warning)
+
+ self.new_face = False
+
+ #print(f'\r人脸识别任务完成,完成时间:{time.time() - now_time}', end='')
+ # if blur > 750:
+ #
+ # else:
+ # print('\r模糊帧不做人脸识别处理', end='')
+
+
+
+class NewSaveAndUploadMP4Thread(threading.Thread):
+ def __init__(self, ip, frame_buffer, save_type, token, msg_list = None):
+ '''
+
+ :param ip: 摄像头标识
+ :param frame_buffer: 异常帧缓存
+ :param save_type: 保存类型
+ '''
+ threading.Thread.__init__(self)
+ self.ip = ip
+ # 初始化时将异常帧转换为队列,你单纯的赋值其实是一个对象引用,若源对象变了,这里也会变。
+ # 线程start后可能不会立即得到执行,若没执行,源队列被清空了,这里的队列也会变空?
+ # 所以这里先将其转换成list存储。
+ self.frames = list(frame_buffer.queue)
+ self.save_type = save_type
+ self.minio_client = client
+ self.token = token
+ self.msg_list = msg_list
+
+ def run(self):
+ # 所以这里是可能出现重复的啊,300次生成有10次会重复。。加个时间戳吧
+ now_time = str(int(time.time()))
+ uuid_str = str(uuid.uuid4())[:6] + now_time # 生成UUID的前6位
+
+
+
+ # 将异常帧存储到列表中
+ if self.save_type == 'picture':
+ print("图片上传消息", self.msg_list, "视频长度: ", len(self.frames))
+
+ first_frame = self.frames[0]
+ img_file_path = f"{save_path}{uuid_str}_{vod_channelNo}_.jpg"
+ cv2.imwrite(img_file_path, first_frame)
+
+ # with open(img_file_path, 'rb') as file_data:
+ # file_data.seek(0, os.SEEK_END)
+ # file_size = file_data.tell()
+ # file_data.seek(0)
+ # self.minio_client.put_object(configData['minioConfig']['bucket_name'], f'{uuid_str}_{vod_channelNo}_.jpg', file_data,
+ # file_size)
+
+ upload_http_url_img = configData['minioConfig']['bucket_name'] + f'/{uuid_str}_{vod_channelNo}_.jpg'
+
+ msg = self.msg_list[0] if len(self.msg_list) > 0 else ""
+
+ # upload_http_url_img = configData['minioConfig']['bucket_name'] + f'/{uuid_str}_{vod_channelNo}_.jpg'
+ # send_post_request(putMessageUrl, self.token, msg, upload_http_url_img, '')
+ # os.remove(img_object_name)
+ print('\n上传图片完成')
+ self.msg_list.clear()
+ elif self.save_type == 'video':
+ print("视频上传消息", self.msg_list, "视频长度: ", len(self.frames))
+
+ mp4_file_path = f'{save_path}{uuid_str}_{vod_channelNo}_.mp4'
+ height, width, _ = self.frames[0].shape
+
+ # 保存视频
+ out = cv2.VideoWriter(mp4_file_path, cv2.VideoWriter_fourcc(*'mp4v'), 25, (width, height), isColor=True)
+ for frame in self.frames:
+ out.write(frame)
+ out.release()
+
+ mp4_object_name = f"{uuid_str}_{vod_channelNo}_.mp4"
+
+ # self.temp_file_path = os.path.abspath(mp4_file_path)
+ self.temp_file_size = os.path.getsize(mp4_file_path)
+
+ # with open(mp4_file_path, 'rb') as file_data:
+ # #上传到minio
+ # self.minio_client.put_object(configData['minioConfig']['bucket_name'], mp4_object_name, file_data,
+ # self.temp_file_size)
+
+
+ upload_http_url_mp4 = configData['minioConfig']['bucket_name'] + f'/{uuid_str}_{vod_channelNo}_.mp4'
+ msg = self.msg_list[0]
+ # 上传警告。
+ # send_post_request(putMessageUrl, self.token, msg, '', upload_http_url_mp4)
+ # os.remove(mp4_file_path)
+ print('\n上传视频成功')
+ self.msg_list.clear()
+
+ else:
+ print('\n异常类型')
+
+
+
+
+class FramePushThread(threading.Thread):
+ def __init__(self, frame_buffer, process_mid):
+ threading.Thread.__init__(self)
+ self.frame_buffer = frame_buffer
+ self.process_mid = process_mid
+
+ def run(self):
+ while True:
+ frame_data = self.frame_buffer.get()
+ self.process_mid.stdin.write(frame_data)
+
+
+class YoLov8TRT(object):
+
+ def __init__(self, engine_file_path):
+ # Create a Context on this device,
+ self.ctx = cuda.Device(0).make_context()
+ stream = cuda.Stream()
+ TRT_LOGGER = trt.Logger(trt.Logger.INFO)
+ runtime = trt.Runtime(TRT_LOGGER)
+
+ # Deserialize the engine from file
+ with open(engine_file_path, "rb") as f:
+ engine = runtime.deserialize_cuda_engine(f.read())
+ context = engine.create_execution_context()
+
+ host_inputs = []
+ cuda_inputs = []
+ host_outputs = []
+ cuda_outputs = []
+ bindings = []
+
+ for binding in engine:
+ print('bingding:', binding, engine.get_binding_shape(binding))
+ size = trt.volume(engine.get_binding_shape(binding)) * engine.max_batch_size
+ dtype = trt.nptype(engine.get_binding_dtype(binding))
+ # Allocate host and device buffers
+ host_mem = cuda.pagelocked_empty(size, dtype)
+ cuda_mem = cuda.mem_alloc(host_mem.nbytes)
+ # Append the device buffer to device bindings.
+ bindings.append(int(cuda_mem))
+ # Append to the appropriate list.
+ if engine.binding_is_input(binding):
+ self.input_w = engine.get_binding_shape(binding)[-1]
+ self.input_h = engine.get_binding_shape(binding)[-2]
+ host_inputs.append(host_mem)
+ cuda_inputs.append(cuda_mem)
+ else:
+ host_outputs.append(host_mem)
+ cuda_outputs.append(cuda_mem)
+
+ # Store
+ self.stream = stream
+ self.context = context
+ self.engine = engine
+ self.host_inputs = host_inputs
+ self.cuda_inputs = cuda_inputs
+ self.host_outputs = host_outputs
+ self.cuda_outputs = cuda_outputs
+ self.bindings = bindings
+ self.batch_size = engine.max_batch_size
+
+ def infer(self, image_frame):
+ threading.Thread.__init__(self)
+ # Make self the active context, pushing it on top of the context stack.
+ self.ctx.push()
+ # Restore
+ stream = self.stream
+ context = self.context
+ engine = self.engine
+ host_inputs = self.host_inputs
+ cuda_inputs = self.cuda_inputs
+ host_outputs = self.host_outputs
+ cuda_outputs = self.cuda_outputs
+ bindings = self.bindings
+ # Do image preprocess
+ batch_image_raw = []
+ batch_origin_h = []
+ batch_origin_w = []
+ batch_input_image = np.empty(shape=[self.batch_size, 3, self.input_h, self.input_w])
+ # 方法遍历由 raw_image_generator 生成的图像,对每个图像执行预处理,并收集处理后的图像数据以及原始图像的尺寸。
+ # 预处理可能包括调整大小、标准化等步骤,为推理准备适当的输入格式。
+ # for i, image_raw in enumerate(image_frame):
+ input_image, image_raw, origin_h, origin_w = self.preprocess_image(image_frame)
+ batch_image_raw.append(image_frame)
+ batch_origin_h.append(origin_h)
+ batch_origin_w.append(origin_w)
+ np.copyto(batch_input_image[0], input_image)
+ batch_input_image = np.ascontiguousarray(batch_input_image)
+
+ # Copy input image to host buffer
+ np.copyto(host_inputs[0], batch_input_image.ravel())
+ # 记录推理开始时间
+ # start = time.time()
+ # 将输入数据传输到GPU
+ cuda.memcpy_htod_async(cuda_inputs[0], host_inputs[0], stream)
+ # 执行异步推理
+ context.execute_async(batch_size=self.batch_size, bindings=bindings, stream_handle=stream.handle)
+ # 将预测结果传回主机
+ cuda.memcpy_dtoh_async(host_outputs[0], cuda_outputs[0], stream)
+ # 同步CUDA流
+ stream.synchronize()
+
+ # 并移除上下文
+ self.ctx.pop()
+ output = host_outputs[0]
+
+ # 后处理和结果解析:
+ for i in range(self.batch_size):
+ # for循环中 调用self.post_process方法进行后处理,提取结果框、得分和类别ID。 此处不需要特别注意 了解获得返回结果即可
+ result_boxes, result_scores, result_classid = self.post_process(
+ output[i * 38001: (i + 1) * 38001], batch_origin_h[i], batch_origin_w[i]
+ )
+ result_list = 0
+ result_boxes_list = list()
+ msg_list = list()
+ for j in range(len(result_boxes)):
+ box = result_boxes[j]
+ # !!!!!!!!!!!这个推理的地方 后续可判断一张图片中是否需要保存 若保存调用保存方法
+ # plot_one_box 方法在原始图像上绘制检测到的每个对象的边界框和标签
+ t = int(result_classid[j])
+ # print("t: ", t)
+ if t in testclasses:
+ result, msg = plot_one_box(
+ box,
+ batch_image_raw[i],
+ label="{}".format(categories[t])
+
+ )
+ # print("result: ", result)
+ # 将检测结果保存到minio中
+ result_list += result
+ if result == 1:
+ result_boxes_list.append(box)
+ msg_list.append(msg)
+ return batch_image_raw, result_list, result_boxes_list, msg_list
+
+ def destroy(self):
+ # Remove any context from the top of the context stack, deactivating it.
+ self.ctx.pop()
+
+ def get_raw_image_zeros(self, image_path_batch=None):
+ """
+ description: Ready data for warmup
+ """
+ for _ in range(self.batch_size):
+ yield np.zeros([self.input_h, self.input_w, 3], dtype=np.uint8)
+
+ def preprocess_image(self, raw_bgr_image):
+ image_raw = raw_bgr_image
+ h, w, c = image_raw.shape
+ image = cv2.cvtColor(image_raw, cv2.COLOR_BGR2RGB)
+ # Calculate widht and height and paddings
+ r_w = self.input_w / w
+ r_h = self.input_h / h
+ if r_h > r_w:
+ tw = self.input_w
+ th = int(r_w * h)
+ tx1 = tx2 = 0
+ ty1 = int((self.input_h - th) / 2)
+ ty2 = self.input_h - th - ty1
+ else:
+ tw = int(r_h * w)
+ th = self.input_h
+ tx1 = int((self.input_w - tw) / 2)
+ tx2 = self.input_w - tw - tx1
+ ty1 = ty2 = 0
+ # Resize the image with long side while maintaining ratio
+ image = cv2.resize(image, (tw, th))
+ # Pad the short side with (128,128,128)
+ image = cv2.copyMakeBorder(
+ image, ty1, ty2, tx1, tx2, cv2.BORDER_CONSTANT, None, (128, 128, 128)
+ )
+ image = image.astype(np.float32)
+ # Normalize to [0,1]
+ image /= 255.0
+ # HWC to CHW format:
+ image = np.transpose(image, [2, 0, 1])
+ # CHW to NCHW format
+ image = np.expand_dims(image, axis=0)
+ # Convert the image to row-major order, also known as "C order":
+ image = np.ascontiguousarray(image)
+ return image, image_raw, h, w
+
+ def xywh2xyxy(self, origin_h, origin_w, x):
+ """
+ description: Convert nx4 boxes from [x, y, w, h] to [x1, y1, x2, y2] where xy1=top-left, xy2=bottom-right
+ param:
+ origin_h: height of original image
+ origin_w: width of original image
+ x: A boxes numpy, each row is a box [center_x, center_y, w, h]
+ return:
+ y: A boxes numpy, each row is a box [x1, y1, x2, y2]
+ """
+ y = np.zeros_like(x)
+ r_w = self.input_w / origin_w
+ r_h = self.input_h / origin_h
+ if r_h > r_w:
+ y[:, 0] = x[:, 0]
+ y[:, 2] = x[:, 2]
+ y[:, 1] = x[:, 1] - (self.input_h - r_w * origin_h) / 2
+ y[:, 3] = x[:, 3] - (self.input_h - r_w * origin_h) / 2
+ y /= r_w
+ else:
+ y[:, 0] = x[:, 0] - (self.input_w - r_h * origin_w) / 2
+ y[:, 2] = x[:, 2] - (self.input_w - r_h * origin_w) / 2
+ y[:, 1] = x[:, 1]
+ y[:, 3] = x[:, 3]
+ y /= r_h
+
+ return y
+
+ def post_process(self, output, origin_h, origin_w):
+ # Get the num of boxes detected
+ num = int(output[0])
+ # Reshape to a two dimentional ndarray
+ pred = np.reshape(output[1:], (-1, 38))[:num, :]
+ # Do nms
+ boxes = self.non_max_suppression(pred, origin_h, origin_w, conf_thres=CONF_THRESH, nms_thres=IOU_THRESHOLD)
+ result_boxes = boxes[:, :4] if len(boxes) else np.array([])
+ result_scores = boxes[:, 4] if len(boxes) else np.array([])
+ result_classid = boxes[:, 5] if len(boxes) else np.array([])
+ return result_boxes, result_scores, result_classid
+
+ def bbox_iou(self, box1, box2, x1y1x2y2=True):
+ """
+ description: compute the IoU of two bounding boxes
+ param:
+ box1: A box coordinate (can be (x1, y1, x2, y2) or (x, y, w, h))
+ box2: A box coordinate (can be (x1, y1, x2, y2) or (x, y, w, h))
+ x1y1x2y2: select the coordinate format
+ return:
+ iou: computed iou
+ box_iou
+ """
+ if not x1y1x2y2:
+ # Transform from center and width to exact coordinates
+ b1_x1, b1_x2 = box1[:, 0] - box1[:, 2] / 2, box1[:, 0] + box1[:, 2] / 2
+ b1_y1, b1_y2 = box1[:, 1] - box1[:, 3] / 2, box1[:, 1] + box1[:, 3] / 2
+ b2_x1, b2_x2 = box2[:, 0] - box2[:, 2] / 2, box2[:, 0] + box2[:, 2] / 2
+ b2_y1, b2_y2 = box2[:, 1] - box2[:, 3] / 2, box2[:, 1] + box2[:, 3] / 2
+ else:
+ # Get the coordinates of bounding boxes
+ b1_x1, b1_y1, b1_x2, b1_y2 = box1[:, 0], box1[:, 1], box1[:, 2], box1[:, 3]
+ b2_x1, b2_y1, b2_x2, b2_y2 = box2[:, 0], box2[:, 1], box2[:, 2], box2[:, 3]
+
+ # Get the coordinates of the intersection rectangle
+ inter_rect_x1 = np.maximum(b1_x1, b2_x1)
+ inter_rect_y1 = np.maximum(b1_y1, b2_y1)
+ inter_rect_x2 = np.minimum(b1_x2, b2_x2)
+ inter_rect_y2 = np.minimum(b1_y2, b2_y2)
+ # Intersection area
+ inter_area = np.clip(inter_rect_x2 - inter_rect_x1 + 1, 0, None) * \
+ np.clip(inter_rect_y2 - inter_rect_y1 + 1, 0, None)
+ # Union Area
+ b1_area = (b1_x2 - b1_x1 + 1) * (b1_y2 - b1_y1 + 1)
+ b2_area = (b2_x2 - b2_x1 + 1) * (b2_y2 - b2_y1 + 1)
+
+ iou = inter_area / (b1_area + b2_area - inter_area + 1e-16)
+
+ return iou
+
+ def non_max_suppression(self, prediction, origin_h, origin_w, conf_thres=0.5, nms_thres=0.4):
+ boxes = prediction[prediction[:, 4] >= conf_thres]
+ boxes[:, :4] = self.xywh2xyxy(origin_h, origin_w, boxes[:, :4])
+ boxes[:, 0] = np.clip(boxes[:, 0], 0, origin_w - 1)
+ boxes[:, 2] = np.clip(boxes[:, 2], 0, origin_w - 1)
+ boxes[:, 1] = np.clip(boxes[:, 1], 0, origin_h - 1)
+ boxes[:, 3] = np.clip(boxes[:, 3], 0, origin_h - 1)
+ confs = boxes[:, 4]
+ boxes = boxes[np.argsort(-confs)]
+
+ keep_boxes = []
+ while boxes.shape[0]:
+ large_overlap = self.bbox_iou(np.expand_dims(boxes[0, :4], 0), boxes[:, :4]) > nms_thres
+ label_match = boxes[0, -1] == boxes[:, -1]
+ # Indices of boxes with lower confidence scores, large IOUs and matching labels
+ invalid = large_overlap & label_match
+ keep_boxes += [boxes[0]]
+ boxes = boxes[~invalid]
+ boxes = np.stack(keep_boxes, 0) if len(keep_boxes) else np.array([])
+ return boxes
+
+def get_attendance_p():
+ # 万一今天重启了,直接从文件中读取以打卡的人数,避免重复打卡
+ global d_face
+ try:
+ with open(f'{people_save_path}{datetime.datetime.now().date()}.txt','r') as f:
+ for line in f:
+ d_face[line.strip().split(' ')[1][4:]] = 1
+ except:
+ pass
+
+
+def connect_to_rtsp_stream(url):
+ """ 尝试连接到 RTSP 流 """
+ cap = cv2.VideoCapture(url)
+ cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'HEVC'))
+ if not cap.isOpened():
+ print(f"Failed to connect to {url}")
+ return None
+ return cap
+
+
+class rtspInputFrame(threading.Thread):
+ def __init__(self, video_path, rtsp_frame_buffer):
+ threading.Thread.__init__(self)
+ self.video_path = video_path
+ self.rtsp_frame_buffer = rtsp_frame_buffer # rtsp输入缓冲区的队列
+
+ def run(self):
+ cap = connect_to_rtsp_stream(self.video_path)
+ while True:
+
+ if cap is None:
+ cap = connect_to_rtsp_stream(self.video_path)
+ time.sleep(5)
+ else:
+ # 从视频流中读取一帧
+ ret, frame = cap.read()
+ # 如果帧读取成功,则 ret 为 True
+ if not ret:
+ print("无法读取帧,尝试重新连接...")
+ # 关闭当前的 VideoCapture 对象
+ cap.release()
+ # 尝试重新连接
+ while True:
+ cap = connect_to_rtsp_stream(self.video_path)
+ if cap is not None:
+ break
+ # 等待一段时间后再尝试连接
+ time.sleep(5) # 休眠5秒
+
+ else:
+ if rtsp_frame_buffer.full():
+ continue # 如果队列满了,就丢弃最早的帧 这个地方可能有线程安全问题
+ frame = cv2.resize(frame, (1280, 720))
+ rtsp_frame_buffer.put(frame)
+ # 释放资源
+ cap.release()
+ cv2.destroyAllWindows()
+
+
+class inferThread(threading.Thread):
+ def __init__(self, yolov8_wrapper, video_path, ip, rtsp_frame_buffer, tokenResult, ffmpeg_buffer_size=300,
+ save_buffer_size=2500):
+ threading.Thread.__init__(self)
+ self.ip = ip
+ self.yolov8_wrapper = yolov8_wrapper
+ self.video_path = video_path
+ self.rtsp_frame_buffer = rtsp_frame_buffer
+ self.tokenResult = tokenResult
+ self.ffmpeg_buffer_size = ffmpeg_buffer_size
+ self.ffmpeg_frame_buffer = queue.Queue(maxsize=ffmpeg_buffer_size) # ffmpeg输出到m3u6的队列,输出切片缓冲区用于存储视频帧
+ self.save_frame_buffer = queue.Queue(maxsize=save_buffer_size) # 用于保存视频的缓存
+
+ self.msg_buffer = list() # 保存报错原因
+
+ self.frame_count = 0 # 帧计数器
+ self.start_warning_time = None # 记录告警开始时间
+ self.latest_warning_time = None # 记录告警结束时间
+ self.old_update_warning_time = time.time() - 3600 # 上一次成功告警的时间
+
+ # 两帧一次目标检测,5帧一次人脸识别,时间不够啊
+ self.det_gap = 0
+ self.face_gap = 0
+
+ self.p_num_error = 0
+
+ # 上一帧的检测框作为没有检测帧的默认结果。只要位置不发生太大变化,结果不会差别太大。
+ self.p_box_list = list()
+
+ def run(self):
+
+ print('检测线程启动')
+
+ frame_push_thread = FramePushThread(self.ffmpeg_frame_buffer, pipeline_mid)
+ frame_push_thread.start()
+
+ # 存的是目标检测的上一次结果图片
+ previous_frame = None
+ previous_fram_face = None
+ # save_flag = []
+ self.tokenResult['token'] = 'token'
+ self.tokenResult['current_time'] = '2024-01-01 00:00:00'
+
+ # 这里没必要每一帧都检测吧,直接隔1帧检测一帧.
+ while True:
+ frame = rtsp_frame_buffer.get()
+ self.face_gap += 1
+ self.det_gap += 1
+ # 人脸识别
+ if self.face_gap > 4:
+ # 上传token
+ # token = get_token(self.tokenResult)
+ token = {}
+ # 直接将帧加入到ffmpeg缓存中
+ ret, jpeg_frame = cv2.imencode('.jpg', frame)
+ self.ffmpeg_frame_buffer.put(jpeg_frame.tobytes())
+ if previous_fram_face is not None:
+ result_same = compare_frames(previous_fram_face, frame, 100)
+ else:
+ result_same = False
+ if not result_same:
+ face_save = FaceRecUpload(self.ip, frame, token)
+ face_save.start()
+ # print('\r 人脸识别任务', end='')
+ self.face_gap = 0
+ # pass
+ # 异常检测
+ elif self.det_gap > 1:
+ self.det_gap = 0
+ if previous_frame is not None:
+ result_same = compare_frames(previous_frame, frame, 100)
+ else:
+ result_same = False
+
+ if not result_same:
+
+ # token = get_token(self.tokenResult)
+ # print(token)
+ token = {}
+
+ batch_image_raw, r_list, box_list, msg_list = self.yolov8_wrapper.infer(frame)
+
+ # print("r_list: ", r_list)
+ # print("p_num_error", self.p_num_error)
+
+ # 上一帧检测结果
+ self.p_box_list = box_list
+
+ previous_frame = frame
+
+ # 将处理完的帧加入到 ffmpeg缓存中
+ ret, jpeg_frame = cv2.imencode('.jpg', frame)
+ self.ffmpeg_frame_buffer.put(jpeg_frame.tobytes())
+
+ now_time = time.time()
+
+ if r_list > self.p_num_error:
+ self.p_num_error = r_list
+ self.save_frame_buffer.put(frame)
+ self.msg_buffer.append(msg_list[0])
+ self.frame_count += 1
+ print(f'\n异常人数增长, 当前异常帧数量: {self.frame_count}', end='')
+
+
+ # 开始时间记录
+ self.start_warning_time = now_time
+
+ if self.save_frame_buffer.full():
+
+ print('\n告警队列满-执行保存')
+ save_thread = NewSaveAndUploadMP4Thread(self.ip, self.save_frame_buffer, 'video', token, self.msg_buffer)
+ save_thread.start()
+
+ self.frame_count = 0
+ self.save_frame_buffer.queue.clear()
+ # self.msg_buffer.clear()
+
+ self.start_warning_time = None
+
+ # 记录当前警告时间
+ self.latest_warning_time = now_time
+
+ # 当前异常人数不变。
+ elif r_list == self.p_num_error and self.p_num_error != 0:
+
+ # token = get_token(self.tokenResult)
+ token = {}
+
+ # 记录当前警告时间
+ self.latest_warning_time = now_time
+
+ # 判断当前的报警时间是否在10秒内
+ upload = verify_timenum(self.start_warning_time, now_time, 10)
+
+ # 在10秒内,将当前异常帧加入异常缓存
+ if upload:
+ self.save_frame_buffer.put(frame)
+
+ self.frame_count += 1
+ print(f'\r检测到异常,异常开始时间{self.start_warning_time} ,将当前异常帧加入异常帧缓存,当前异常数量:{self.p_num_error},当前异常帧数量: {self.frame_count} '
+ f'', end='')
+
+ # 异常帧缓存满了
+ if self.save_frame_buffer.full():
+ print('\n告警缓存满,保存为视频')
+
+ # 上传视频/图片
+ save_thread = NewSaveAndUploadMP4Thread(self.ip, self.save_frame_buffer, 'video', token, self.msg_buffer)
+ save_thread.start()
+ # save_flag.clear()
+ self.frame_count = 0
+ self.save_frame_buffer.queue.clear()
+ # self.msg_buffer.clear()
+ # self.old_update_warning_time = now_time
+ self.start_warning_time = now_time
+
+ else:
+
+ #print(f'\r检测到异常帧,但异常人数没变化,当前异常人数为{r_list}。', end='')
+
+ if not self.save_frame_buffer.empty():
+ print('\n10s 视频开始保存')
+ save_thread = NewSaveAndUploadMP4Thread(self.ip, self.save_frame_buffer,
+ 'video', token, self.msg_buffer)
+ save_thread.start()
+ # save_flag.clear()
+ self.frame_count = 0
+ self.save_frame_buffer.queue.clear()
+ # self.msg_buffer.clear()
+ # self.old_update_warning_time = now_time
+ self.start_warning_time = now_time - 11
+
+ elif r_list == self.p_num_error and self.p_num_error == 0:
+ pass
+ #print(f'\r无异常', end='')
+ elif r_list < self.p_num_error:
+ # 间隔小于4秒的认为是误判
+ change_flag = verify_timenum(self.latest_warning_time, now_time, 4)
+ if not change_flag:
+
+ self.p_num_error = r_list
+ print(f'\n异常人数减少,当前异常人数:{self.p_num_error},当前异常帧数量:{self.frame_count}')
+
+ # 将当前缓存中的异常帧上传
+ if self.save_frame_buffer.qsize()> 20:
+
+ # 4秒后才保存视频,这时msg_list已经是空的了.
+ save_thread = NewSaveAndUploadMP4Thread(self.ip, self.save_frame_buffer, 'video', token, self.msg_buffer)
+ save_thread.start()
+ elif self.save_frame_buffer.qsize()> 0:
+ save_thread = NewSaveAndUploadMP4Thread(self.ip, self.save_frame_buffer, 'picture', token, self.msg_buffer)
+ save_thread.start()
+
+
+ self.frame_count = 0
+ self.save_frame_buffer.queue.clear()
+ # self.msg_buffer.clear()
+
+ self.start_warning_time = None
+ else:
+ pass
+ # print('\r程序可能的误判', end='')
+ else:
+ # 一样的帧直接加入到ffmpeg缓存中
+ # print('\r相同帧不需要推理', end='')
+ ret, jpeg_frame = cv2.imencode('.jpg', frame)
+ self.ffmpeg_frame_buffer.put(jpeg_frame.tobytes())
+ else:
+ # 什么都不干的帧,直接以上一次的推理结果画框。
+ for i in range(len(self.p_box_list)):
+ c1, c2 = (int(self.p_box_list[i][0]), int(self.p_box_list[i][1])), (int(self.p_box_list[i][2]), int(self.p_box_list[i][3]))
+ cv2.rectangle(frame, c1, c2, (0, 0, 255), 2)
+ uuid_str = str(uuid.uuid4())[:6] + str(int(time.time()))
+ # img_object_name = f"/home/admin-root/haotian/jingzhu1.1/tensorrtx-master/yolov8/mp4/skip_{uuid_str}_{vod_channelNo}.jpg"
+ # cv2.imwrite(img_object_name, frame)
+ ret, jpeg_frame = cv2.imencode('.jpg', frame)
+ self.ffmpeg_frame_buffer.put(jpeg_frame.tobytes())
+
+
+
+
+
+if __name__ == "__main__":
+ print(
+ "=============================================================================================================")
+ # load custom plugin and engine
+ PLUGIN_LIBRARY = f"{engine_path}libmyplugins.so"
+ engine_file_path = f"{engine_path}best.engine"
+ # time.sleep()
+
+ # 执行python代码命令行 参数判断操作(不做处理)
+ if len(sys.argv) > 1:
+ engine_file_path = sys.argv[1]
+ if len(sys.argv) > 2:
+ PLUGIN_LIBRARY = sys.argv[2]
+ clear_folder(ip)
+ # 加载动态链接库
+ ctypes.CDLL(PLUGIN_LIBRARY)
+ # 手动输入训练时的类别, 不需要了直接从配置文件中读
+ print("当前类别", categories)
+ # categories = ["shoe"]
+ # 加载模型文件
+ yolov8_wrapper1 = YoLov8TRT(engine_file_path)
+
+ # 重启后自动根据文件,加载当天打卡信息。根据程序设置,每天凌晨0点更新打卡数据
+ get_attendance_p()
+
+ try:
+ thread_rtsp = rtspInputFrame(vod_path,
+ rtsp_frame_buffer)
+ thread_det_1 = inferThread(yolov8_wrapper1, vod_path, ip, rtsp_frame_buffer, tokenResult)
+ thread_rtsp.start()
+ thread_det_1.start()
+ thread_rtsp.join()
+ thread_det_1.join()
+
+ finally:
+ # destroy the instance
+ thread_det_1.destroy()
diff --git a/scripts/mock_alarm_server.py b/scripts/mock_alarm_server.py
new file mode 100755
index 0000000..717420e
--- /dev/null
+++ b/scripts/mock_alarm_server.py
@@ -0,0 +1,186 @@
+#!/usr/bin/env python3
+"""
+模拟告警服务器
+用于测试 RK3588 Media Server 的 alarm 节点 external_api 功能
+
+提供两个接口:
+1. POST /api/getToken - 获取访问令牌
+2. POST /api/putMessage - 接收告警信息
+
+使用方法:
+ python3 mock_alarm_server.py
+
+默认监听:0.0.0.0:8080
+"""
+
+from flask import Flask, request, jsonify
+from datetime import datetime
+import json
+import sys
+
+app = Flask(__name__)
+
+# 模拟 token 存储
+mock_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.mock_token_for_testing"
+token_expire_time = 30 * 60 # 30分钟过期时间(秒)
+
+# 统计信息
+stats = {
+ "token_requests": 0,
+ "alarm_requests": 0,
+ "last_alarm": None
+}
+
+
+@app.route('/api/getToken', methods=['POST'])
+def get_token():
+ """
+ 模拟获取 token 接口
+ 返回格式与 d8_1.py 中一致
+ """
+ stats["token_requests"] += 1
+
+ # 打印请求信息
+ print(f"\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 收到 Token 请求")
+ print(f" 请求头: {dict(request.headers)}")
+ print(f" 请求体: {request.data.decode('utf-8') if request.data else 'None'}")
+
+ response = {
+ "errorDesc": None,
+ "message": None,
+ "responseBody": {
+ "userInfo": {
+ "username": "szls",
+ "realname": "数字孪生",
+ "deptName": "精密铸造厂",
+ "id": "8a746b7d91deb3270191df35f42e000e"
+ },
+ "expireTime": str(token_expire_time),
+ "token": mock_token,
+ "refreshToken": mock_token + "_refresh"
+ },
+ "retCode": "200"
+ }
+
+ print(f" 响应: 返回 mock token")
+ return jsonify(response)
+
+
+@app.route('/api/putMessage', methods=['POST'])
+def put_message():
+ """
+ 模拟接收告警信息接口
+ 接收格式与 d8_1.py 中 send_post_request 一致
+ """
+ stats["alarm_requests"] += 1
+
+ # 获取请求头中的 token
+ token = request.headers.get('X-Access-Token', 'None')
+
+ # 解析请求体
+ try:
+ data = request.json if request.is_json else json.loads(request.data)
+ except:
+ data = {}
+
+ # 打印告警信息
+ print(f"\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 收到告警信息")
+ print(f" Token: {token[:30]}...")
+ print(f" 租户代码: {data.get('tenantCode', 'N/A')}")
+ print(f" 频道号: {data.get('channelNo', 'N/A')}")
+ print(f" 告警内容: {data.get('alarmContent', 'N/A')}")
+ print(f" 告警时间: {data.get('alarmTime', 'N/A')}")
+
+ # 打印图片/视频信息
+ pic_info = data.get('picInfo', [])
+ video_info = data.get('videoInfo', [])
+
+ if pic_info:
+ print(f" 图片地址: {len(pic_info)} 张")
+ for i, pic in enumerate(pic_info[:3]): # 只打印前3个
+ print(f" [{i+1}] {pic.get('url', 'N/A')}")
+ if len(pic_info) > 3:
+ print(f" ... 还有 {len(pic_info) - 3} 张")
+
+ if video_info:
+ print(f" 视频地址: {len(video_info)} 个")
+ for i, video in enumerate(video_info[:3]):
+ print(f" [{i+1}] {video.get('url', 'N/A')}")
+
+ # 更新统计
+ stats["last_alarm"] = {
+ "time": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ "content": data.get('alarmContent', 'N/A'),
+ "channel": data.get('channelNo', 'N/A')
+ }
+
+ # 返回成功响应
+ response = {
+ "responseBody": "1",
+ "message": None,
+ "retCode": "200",
+ "errorDesc": None
+ }
+
+ print(f" 响应: 告警接收成功")
+ return jsonify(response)
+
+
+@app.route('/stats', methods=['GET'])
+def get_stats():
+ """查看统计信息"""
+ return jsonify({
+ "token_requests": stats["token_requests"],
+ "alarm_requests": stats["alarm_requests"],
+ "last_alarm": stats["last_alarm"]
+ })
+
+
+@app.route('/', methods=['GET'])
+def index():
+ """首页说明"""
+ return """
+ RK3588 模拟告警服务器
+ 可用接口:
+
+ - POST /api/getToken - 获取访问令牌
+ - POST /api/putMessage - 接收告警信息
+ - GET /stats - 查看统计信息
+
+ 当前统计:
+
+ - Token 请求次数: {token_requests}
+ - 告警请求次数: {alarm_requests}
+
+ """.format(**stats)
+
+
+def main():
+ host = "0.0.0.0"
+ port = 8080
+
+ print("=" * 60)
+ print("RK3588 模拟告警服务器")
+ print("=" * 60)
+ print(f"启动时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
+ print(f"监听地址: http://{host}:{port}")
+ print("")
+ print("接口列表:")
+ print(f" 1. POST http://{host}:{port}/api/getToken")
+ print(f" 2. POST http://{host}:{port}/api/putMessage")
+ print(f" 3. GET http://{host}:{port}/stats")
+ print("")
+ print("按 Ctrl+C 停止服务")
+ print("=" * 60)
+
+ try:
+ app.run(host=host, port=port, debug=False, threaded=True)
+ except KeyboardInterrupt:
+ print("\n\n服务已停止")
+ print(f"总计 Token 请求: {stats['token_requests']}")
+ print(f"总计告警请求: {stats['alarm_requests']}")
+ sys.exit(0)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/yolov8n_cxn.onnx b/yolov8n_cxn.onnx
index 8c802a1..c64cd0e 100644
Binary files a/yolov8n_cxn.onnx and b/yolov8n_cxn.onnx differ