Trim use-can to buildable runtime sources and standalone Orin package
This commit is contained in:
+10
-31
@@ -1,41 +1,20 @@
|
|||||||
# Python caches and local environments
|
# Generated build/cache/runtime files
|
||||||
|
/build*/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
.venv/
|
|
||||||
venv/
|
|
||||||
|
|
||||||
# Build outputs (keep the supplied vendor libraries in lib/)
|
|
||||||
build/
|
|
||||||
/build-*/
|
|
||||||
*.o
|
*.o
|
||||||
*.a
|
*.a
|
||||||
/vision-anti-pinch
|
.venv/
|
||||||
|
venv/
|
||||||
# Models and test media
|
|
||||||
models/
|
|
||||||
*.engine
|
|
||||||
*.onnx
|
|
||||||
*.pt
|
|
||||||
*.mp4
|
|
||||||
*.avi
|
|
||||||
|
|
||||||
# Runtime outputs
|
|
||||||
logs/
|
|
||||||
outputs/
|
outputs/
|
||||||
out/
|
logs/
|
||||||
frames/
|
|
||||||
*.log
|
*.log
|
||||||
*.pid
|
*.pid
|
||||||
|
*.mp4
|
||||||
# Local credentials and legacy SSH authentication helper
|
*.avi
|
||||||
|
/models/
|
||||||
|
*.onnx
|
||||||
|
# Local credentials
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
ssh_expect.exp
|
|
||||||
|
|
||||||
# The checked-in Orin deployment bundle intentionally includes its runtime assets.
|
|
||||||
!/orin/build/
|
|
||||||
!/orin/build/libvision_native.so
|
|
||||||
!/orin/models/
|
|
||||||
!/orin/models/engines/
|
|
||||||
!/orin/models/engines/*.engine
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
# 分支说明
|
|
||||||
|
|
||||||
| 分支 | 内容 |
|
|
||||||
|---|---|
|
|
||||||
| main | CMS v9.10 原始 YOLO + RTMW + risk 部署包,入口 run_risk.py,含 ONNX 模型 |
|
|
||||||
| use-can | 0923 V4L2/CAN + TensorRT + CMS 风险集成,提供最新结果变量、可选可视化及 orin/run.sh 可运行包 |
|
|
||||||
| inference-only | 原 main 的纯 C++ TensorRT 摄像头推理版本,保留原提交 26c5bfb88ddf9919ea46efcf5b12df6e09ebdfd9 |
|
|
||||||
|
|
||||||
main 和 use-can 均在原 main 历史上追加提交;原 main 历史未改写。
|
|
||||||
+6
-6
@@ -9,10 +9,10 @@ find_package(OpenCV REQUIRED COMPONENTS core imgproc)
|
|||||||
find_package(Threads REQUIRED)
|
find_package(Threads REQUIRED)
|
||||||
find_path(TRT_INCLUDE NvInfer.h PATH_SUFFIXES aarch64-linux-gnu REQUIRED)
|
find_path(TRT_INCLUDE NvInfer.h PATH_SUFFIXES aarch64-linux-gnu REQUIRED)
|
||||||
find_library(TRT_LIB nvinfer REQUIRED)
|
find_library(TRT_LIB nvinfer REQUIRED)
|
||||||
add_library(vision_native SHARED native/native_api.cpp v4l2_capture.cpp can_comm.cpp
|
add_library(vision_native SHARED native/native_api.cpp native/v4l2_capture.cpp native/can_comm.cpp
|
||||||
accelerator/src/trt_engine.cpp accelerator/src/pose_engine.cu
|
native/trt_engine.cpp native/pose_engine.cu
|
||||||
accelerator/src/gpu_preprocess.cu accelerator/src/uyvy_converter.cu accelerator/src/detector.cpp)
|
native/gpu_preprocess.cu native/uyvy_converter.cu native/detector.cpp)
|
||||||
target_include_directories(vision_native PRIVATE . include accelerator/include ${TRT_INCLUDE} ${OpenCV_INCLUDE_DIRS})
|
target_include_directories(vision_native PRIVATE native/include ${TRT_INCLUDE} ${OpenCV_INCLUDE_DIRS})
|
||||||
target_link_libraries(vision_native PRIVATE ${TRT_LIB} ${OpenCV_LIBS} Threads::Threads CUDA::cudart CUDA::nppc CUDA::nppig CUDA::nppidei CUDA::nppicc
|
target_link_libraries(vision_native PRIVATE ${TRT_LIB} ${OpenCV_LIBS} Threads::Threads CUDA::cudart CUDA::nppc CUDA::nppig CUDA::nppidei CUDA::nppicc
|
||||||
"${CMAKE_SOURCE_DIR}/lib/libusbcanfd.so" "${CMAKE_SOURCE_DIR}/lib/libusb-1.0.so")
|
"${CMAKE_SOURCE_DIR}/orin/lib/libusbcanfd.so" "${CMAKE_SOURCE_DIR}/orin/lib/libusb-1.0.so")
|
||||||
set_target_properties(vision_native PROPERTIES BUILD_WITH_INSTALL_RPATH TRUE INSTALL_RPATH "$ORIGIN/../lib;/usr/local/cuda/lib64")
|
set_target_properties(vision_native PROPERTIES BUILD_WITH_INSTALL_RPATH TRUE INSTALL_RPATH "$ORIGIN/../lib;$ORIGIN/../orin/lib;/usr/local/cuda/lib64")
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
.PHONY: all run test baseline clean
|
.PHONY: all run package clean
|
||||||
all:
|
all:
|
||||||
./scripts/build.sh
|
./scripts/build.sh
|
||||||
run: all
|
run: all
|
||||||
./run_camera.sh
|
./run_camera.sh
|
||||||
test:
|
package: all
|
||||||
python3 -m unittest discover -s tests -v
|
./scripts/package_orin.sh
|
||||||
baseline:
|
|
||||||
$(MAKE) -f Makefile.baseline
|
|
||||||
clean:
|
clean:
|
||||||
cmake --build build --target clean
|
cmake --build build --target clean
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
# Makefile - V4L2 MMAP 取图 demo (OpenCV + CAN 收发)
|
|
||||||
#
|
|
||||||
# 用法:
|
|
||||||
# make # 编译
|
|
||||||
# make clean # 清理
|
|
||||||
# make run # 编译并运行 (取 100 帧)
|
|
||||||
# make capture # 编译并取 5 帧保存到 ./out
|
|
||||||
# make no-can # 同 capture, 但关闭 CAN 收发
|
|
||||||
#
|
|
||||||
# 说明: 本 Makefile 在 Orin 上本地编译。
|
|
||||||
# CAN 模块依赖 include/zcan.h + lib/libusbcanfd.so + lib/libusb-1.0.so
|
|
||||||
# CAN 收发默认开启, 程序运行时加 --no-can 关闭
|
|
||||||
|
|
||||||
# ---- 工具链 ----
|
|
||||||
CXX ?= g++
|
|
||||||
CXXFLAGS ?= -std=c++17 -O3 -Wall -Wextra -Wno-unused-parameter -Wno-unused-result
|
|
||||||
LDFLAGS ?=
|
|
||||||
|
|
||||||
# ---- OpenCV 检测 (优先 opencv4, 回退 opencv) ----
|
|
||||||
OPENCV_PC := $(shell pkg-config --exists opencv4 && echo opencv4 || echo opencv)
|
|
||||||
OPENCV_CFLAGS := $(shell pkg-config --cflags $(OPENCV_PC))
|
|
||||||
OPENCV_LIBS := $(shell pkg-config --libs $(OPENCV_PC))
|
|
||||||
|
|
||||||
# ---- 本地头文件/库目录 (zcan.h, libusbcanfd.so, libusb-1.0.so) ----
|
|
||||||
INC_DIR := include
|
|
||||||
LIB_DIR := lib
|
|
||||||
IDIRS := -I$(INC_DIR) $(OPENCV_CFLAGS)
|
|
||||||
LDIRS := -L$(LIB_DIR)
|
|
||||||
RPATH := -Wl,-rpath,$$(pwd)/$(LIB_DIR)
|
|
||||||
|
|
||||||
CXXFLAGS += $(IDIRS)
|
|
||||||
LDLIBS += $(OPENCV_LIBS) -lm -lpthread -lusbcanfd -lusb-1.0
|
|
||||||
|
|
||||||
# ---- 目标与源文件 ----
|
|
||||||
BIN := vision-anti-pinch
|
|
||||||
SOURCES := main.cpp v4l2_capture.cpp can_comm.cpp
|
|
||||||
HEADERS := v4l2_capture.h can_comm.h $(INC_DIR)/zcan.h
|
|
||||||
OBJECTS := $(SOURCES:.cpp=.o)
|
|
||||||
|
|
||||||
# ---- 默认目标 ----
|
|
||||||
.PHONY: all clean run capture
|
|
||||||
|
|
||||||
all: $(BIN)
|
|
||||||
|
|
||||||
$(BIN): $(OBJECTS)
|
|
||||||
$(CXX) $(LDFLAGS) -o $@ $^ $(LDIRS) $(RPATH) $(LDLIBS)
|
|
||||||
|
|
||||||
# ---- 依赖规则 (头文件改动则全部重编) ----
|
|
||||||
$(OBJECTS): $(HEADERS)
|
|
||||||
|
|
||||||
%.o: %.cpp
|
|
||||||
$(CXX) $(CXXFLAGS) -c -o $@ $<
|
|
||||||
|
|
||||||
# ---- 便捷目标 ----
|
|
||||||
run: $(BIN)
|
|
||||||
./$(BIN) -d /dev/video5 -n 100 -v
|
|
||||||
|
|
||||||
capture: $(BIN)
|
|
||||||
mkdir -p out
|
|
||||||
./$(BIN) -d /dev/video5 -n 100 -s 5 -t jpg -o ./out
|
|
||||||
|
|
||||||
# CAN 收发默认开启, 与 capture 相同
|
|
||||||
can: capture
|
|
||||||
|
|
||||||
# 关闭 CAN 收发的取图
|
|
||||||
no-can: $(BIN)
|
|
||||||
mkdir -p out
|
|
||||||
./$(BIN) -d /dev/video5 -n 100 -s 5 -t jpg -o ./out --no-can -v
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -f $(OBJECTS) $(BIN)
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
{
|
|
||||||
"baseline": "orin@192.168.195.100:/home/orin/vision-anti-pinch",
|
|
||||||
"baseline_sha256": [
|
|
||||||
"a6f884d241b3b345fbdc3bbb2d0119467f6a01d6828786a7d33d7f97ad101f6e main.cpp",
|
|
||||||
"ba7ce790ec4b43403fd6b9ccc4693148414b7314060c29d9f9f92dee931509cf can_comm.cpp",
|
|
||||||
"5aaeaba6f0f25a6202ff2cf61c626b61bda2350f5bd9b36e36c828b5b8fec994 can_comm.h",
|
|
||||||
"f5c6658eb3e8362569c18d528fd4c8a2e311e5f779d9687e8ff10b79a11dc5b6 v4l2_capture.cpp",
|
|
||||||
"dd7a6f92e4d3335368691dd587715cec3d24816e9a96d44ff11c91296f5082eb v4l2_capture.h",
|
|
||||||
"65b16fe3d4549ad218679f510afe4049ad0f197912832060daaca13acc0f5f4f Makefile"
|
|
||||||
],
|
|
||||||
"risk_source": "/home/thor/文档/cms_v9_10_yolo_rtmw_risk_deploy_20260923",
|
|
||||||
"risk_pipeline": {
|
|
||||||
"occlusion_aware_pose_v2.py": {
|
|
||||||
"source_sha256": "81088b8bc072162bec13f57c65eb0d396e6d1e1074a83f92bcf29e393e27e11f",
|
|
||||||
"integrated_sha256": "81088b8bc072162bec13f57c65eb0d396e6d1e1074a83f92bcf29e393e27e11f"
|
|
||||||
},
|
|
||||||
"fuse_rtmw_l_temporal.py": {
|
|
||||||
"source_sha256": "bc74a400ce5c5608c2fa49a69ccd8b41ec8633cd6a8239d9d594b8d9e5c3732a",
|
|
||||||
"integrated_sha256": "bc74a400ce5c5608c2fa49a69ccd8b41ec8633cd6a8239d9d594b8d9e5c3732a"
|
|
||||||
},
|
|
||||||
"rtmw_l_person_mask_ablation.py": {
|
|
||||||
"source_sha256": "d983c9431237afcd7a786c6c4610be6d57a8b4d9ea8f2e9d1889fc0b0ca1d5df",
|
|
||||||
"integrated_sha256": "bf8bb754cd841a52b4410f958a651b005de36ac4d17cc0992fbe1c90ed30caca"
|
|
||||||
},
|
|
||||||
"door_mask_risk.py": {
|
|
||||||
"source_sha256": "6d6d14a972bc4e5e9f8256514169e73ba0392a301cacd1221eaa26146aad00d2",
|
|
||||||
"integrated_sha256": "6d6d14a972bc4e5e9f8256514169e73ba0392a301cacd1221eaa26146aad00d2"
|
|
||||||
},
|
|
||||||
"live_pose_engine.py": {
|
|
||||||
"source_sha256": "a15baaedf5a25b9896ff898fc1a095388483dc52b1b7000b56d5506ff73bf67e",
|
|
||||||
"integrated_sha256": "cc8ea36d543896cd90f7a00822f06fc67771a1811859a4087995ac372df30276"
|
|
||||||
},
|
|
||||||
"predict_optimized_pose_videos.py": {
|
|
||||||
"source_sha256": "36d203f257444e57f890cdc3826dad0c7a46725b938e5f8507e357a70ae2cef9",
|
|
||||||
"integrated_sha256": "059e2a67c17ce63354986d3242a5d361103ee4241c5391f18140eeb19e47c939"
|
|
||||||
},
|
|
||||||
"door_zone_risk.py": {
|
|
||||||
"source_sha256": "46d70e1e90a1f36d544b7492ef1fbf08821377a1c460a6683f42d4d7006055c7",
|
|
||||||
"integrated_sha256": "46d70e1e90a1f36d544b7492ef1fbf08821377a1c460a6683f42d4d7006055c7"
|
|
||||||
},
|
|
||||||
"rtmw_risk_pipeline.py": {
|
|
||||||
"source_sha256": "1bb58d851ba5d684527b8d1e1940db6241d2b0b2862687fa3b151899a65320d2",
|
|
||||||
"integrated_sha256": "1bb58d851ba5d684527b8d1e1940db6241d2b0b2862687fa3b151899a65320d2"
|
|
||||||
},
|
|
||||||
"compare_rtmw_ml.py": {
|
|
||||||
"source_sha256": "5de11436bb888f2f1c4c0e7ad0853e3864193fe2625cc65294b40683ebe8729a",
|
|
||||||
"integrated_sha256": "fd3ae92255abf95ef037a1060ee75897f9705b4040f9394c3130f6ab860667e1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"engines_sha256": {
|
|
||||||
"rtmw_l_384.engine": "ebcd09e8c2e35008adbb0c1d222e580b17e0df1c8093df3481ba3374cd9a8174",
|
|
||||||
"yolo11s.engine": "f6a65b926ac1144484f4aa93cd26d7997d32d62310ab93fe867f2bd4412222e9"
|
|
||||||
},
|
|
||||||
"engine_origin": "orin@192.168.195.100:/home/orin/vision-anti-pinch-0920/orin/engines",
|
|
||||||
"can_mapping_user_confirmed": [
|
|
||||||
"left_front",
|
|
||||||
"right_front",
|
|
||||||
"left_rear",
|
|
||||||
"right_rear"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,330 +0,0 @@
|
|||||||
# CMS v9.10 YOLO + RTMW 门区风险部署包
|
|
||||||
|
|
||||||
该目录是 v9.10 的精简部署版本,只保留运行 `YOLO + RTMW + risk` 所需的代码、模型、标定文件和依赖清单。历史推理结果、研发测试、标注网页、参考图片、缓存与离线分析入口均未放入部署包。
|
|
||||||
|
|
||||||
## 1. 功能链路
|
|
||||||
|
|
||||||
```text
|
|
||||||
视频帧
|
|
||||||
-> YOLO11s 人体检测与轨迹 ID
|
|
||||||
-> RTMW-L COCO-WholeBody 133 点关键点
|
|
||||||
-> 手臂/手部时序融合
|
|
||||||
-> 骨骼位置和尺度为主、人体框为辅的前后门归属
|
|
||||||
-> CAN 活跃门过滤
|
|
||||||
-> 手部区域(无有效手部时使用人体框兜底)
|
|
||||||
-> 与固定车门危险区 mask 计算二维风险
|
|
||||||
-> causal_results.jsonl
|
|
||||||
```
|
|
||||||
|
|
||||||
本版本不包含深度估计和 EdgeTAM。输出是二维图像风险,不是物理距离,也不是安全认证结论。
|
|
||||||
|
|
||||||
## 2. 目录结构
|
|
||||||
|
|
||||||
```text
|
|
||||||
run_risk.py 主入口
|
|
||||||
requirements-realtime-gpu.txt CUDA 推理依赖
|
|
||||||
requirements-tensorrt-windows.txt Windows TensorRT 可选依赖
|
|
||||||
models/
|
|
||||||
yolo11s.onnx 人体检测模型
|
|
||||||
rtmw-l-384.onnx WholeBody 关键点模型
|
|
||||||
annotations/
|
|
||||||
constant_door_mask.json 当前固定机位车门危险区
|
|
||||||
door_zone_multifeature.json 当前前门/后门多特征标定
|
|
||||||
pipeline/ 推理、融合、门区和风险计算代码
|
|
||||||
```
|
|
||||||
|
|
||||||
`annotations` 中的两个 JSON 都与摄像头机位相关。当前参数来自研发临时机位;换成实车机位后必须重新标定和验证,不能直接将临时机位结论作为量产参数。
|
|
||||||
|
|
||||||
## 3. 环境安装
|
|
||||||
|
|
||||||
推荐 Python 3.10~3.12 和 NVIDIA CUDA GPU。先安装与目标机 CUDA 或 JetPack 匹配的 PyTorch,再安装其余依赖:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
python -m venv .venv
|
|
||||||
.\.venv\Scripts\Activate.ps1
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install -r requirements-realtime-gpu.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
不要同时安装 CPU 版 `onnxruntime` 和 `onnxruntime-gpu`。Jetson Orin 应优先使用 NVIDIA 针对对应 JetPack 发布的 PyTorch/torchvision,而不是强制使用桌面 wheel。
|
|
||||||
|
|
||||||
如需在 Windows 上使用 RTMW TensorRT Execution Provider,再执行:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
pip install -r requirements-tensorrt-windows.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. 运行
|
|
||||||
|
|
||||||
### 4.1 固定车门危险区
|
|
||||||
|
|
||||||
以后门 CAN=0、前门未进入电吸范围为例:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
python run_risk.py `
|
|
||||||
--video D:\data\segment_008.mp4 `
|
|
||||||
--constant-door-mask annotations\constant_door_mask.json `
|
|
||||||
--door-zone-config annotations\door_zone_multifeature.json `
|
|
||||||
--active-door rear `
|
|
||||||
--output outputs\seg_08_rear `
|
|
||||||
--det-device 0 `
|
|
||||||
--pose-device cuda `
|
|
||||||
--write-video
|
|
||||||
```
|
|
||||||
|
|
||||||
`--active-door` 对应当前 CAN=0 的车门:
|
|
||||||
|
|
||||||
- `front`:只有前门进入电吸范围;
|
|
||||||
- `rear`:只有后门进入电吸范围;
|
|
||||||
- `both`:前后门都进入电吸范围;
|
|
||||||
- `none`:两门都不进行风险判断。
|
|
||||||
|
|
||||||
当前命令行参数对整个视频保持不变,用于离线部署验证。实车 CAN 随帧变化时,集成层需要把每帧实际 CAN 状态传给门级风险选择逻辑。
|
|
||||||
|
|
||||||
不需要叠加视频时不要传 `--write-video`,可减少编码和磁盘 I/O。完整参数说明:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
python run_risk.py --help
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 TensorRT RTMW
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
python run_risk.py `
|
|
||||||
--video D:\data\test.mp4 `
|
|
||||||
--constant-door-mask annotations\constant_door_mask.json `
|
|
||||||
--active-door rear `
|
|
||||||
--output outputs\test_trt `
|
|
||||||
--pose-provider tensorrt `
|
|
||||||
--pose-device cuda
|
|
||||||
```
|
|
||||||
|
|
||||||
首次运行会在 `models/rtmw_l_trt_cache` 生成 TensorRT 缓存,该缓存与目标 GPU、TensorRT 版本和运行环境相关,不随部署包预置。
|
|
||||||
|
|
||||||
## 5. 输出文件
|
|
||||||
|
|
||||||
输出目录包含:
|
|
||||||
|
|
||||||
- `causal_results.jsonl`:逐帧因果结果,一行一个 JSON 对象,是 Demo 对接的主要接口;
|
|
||||||
- `summary.json`:模型、帧数、吞吐、延迟及风险统计;
|
|
||||||
- `rtmw_risk_overlay.mp4`:仅在传入 `--write-video` 时生成。
|
|
||||||
|
|
||||||
`causal_results.jsonl` 每行顶层字段:
|
|
||||||
|
|
||||||
| 字段 | 含义 |
|
|
||||||
|---|---|
|
|
||||||
| `frame_index` | 从 0 开始的视频帧号 |
|
|
||||||
| `track_ids` | 当前帧人体轨迹 ID 数组 |
|
|
||||||
| `boxes` | 与 `track_ids` 同序的人体框,格式 `[x1,y1,x2,y2]` |
|
|
||||||
| `skeletons` | 与检测人体对齐的 133 点融合骨骼 |
|
|
||||||
| `active_door` | 本次运行指定的 CAN=0 门:`front/rear/both/none` |
|
|
||||||
| `door_assignments` | 每个轨迹的前门/后门归属及特征解释 |
|
|
||||||
| `door_risks` | `front` 和 `rear` 两扇门各自的最终风险 |
|
|
||||||
| `risk` | 经过 CAN 和门区过滤后供业务使用的当前风险 |
|
|
||||||
| `ungated_risk` | 未经过前后门/CAN 过滤的原始二维风险,仅用于诊断 |
|
|
||||||
|
|
||||||
数组关联规则:`boxes[i]`、`track_ids[i]`、`skeletons[i].person_index == i`、`door_assignments[i].person_index == i` 表示同一个人。跨帧关联应使用 `track_id`,不要依赖数组下标。
|
|
||||||
|
|
||||||
## 6. `risk` 的含义
|
|
||||||
|
|
||||||
### 6.1 应读取哪个字段
|
|
||||||
|
|
||||||
单门 CAN=0 时,业务防夹建议读取:
|
|
||||||
|
|
||||||
```text
|
|
||||||
risk.risk_type
|
|
||||||
risk.risk_level
|
|
||||||
risk.risk_score
|
|
||||||
risk.decision
|
|
||||||
```
|
|
||||||
|
|
||||||
需要同时展示前后门时分别读取:
|
|
||||||
|
|
||||||
```text
|
|
||||||
door_risks.front
|
|
||||||
door_risks.rear
|
|
||||||
```
|
|
||||||
|
|
||||||
`ungated_risk` 只用于排查“图像上是否靠近危险区”,不能直接代替门级最终结论,否则会绕过前后门归属和 CAN 过滤。
|
|
||||||
|
|
||||||
### 6.2 风险标签
|
|
||||||
|
|
||||||
`risk_type` 是带门位置的最终风险标签:
|
|
||||||
|
|
||||||
| `risk_type` | `risk_level` | 含义 |
|
|
||||||
|---|---|---|
|
|
||||||
| `FRONT_CLEAR` / `REAR_CLEAR` | `clear` | 对应活跃门没有风险 |
|
|
||||||
| `FRONT_WATCH` / `REAR_WATCH` | `watch` | 进入关注距离,但未达到 warning |
|
|
||||||
| `FRONT_WARNING` / `REAR_WARNING` | `warning` | 距离达到 warning 阈值 |
|
|
||||||
| `FRONT_DANGER` / `REAR_DANGER` | `danger` | 人体风险区域与车门危险区相交 |
|
|
||||||
| `FRONT_UNKNOWN` / `REAR_UNKNOWN` | `unknown` | 人物处于 `transition`,无法可靠确定前后门 |
|
|
||||||
| `FRONT_INACTIVE` / `REAR_INACTIVE` | `inactive` | 该门 CAN 不活跃,不计算防夹风险 |
|
|
||||||
| `NO_ACTIVE_DOOR` | `inactive` | 前后门均不活跃 |
|
|
||||||
|
|
||||||
对应的 `decision`:
|
|
||||||
|
|
||||||
| `decision` | 含义 |
|
|
||||||
|---|---|
|
|
||||||
| `DANGER_INTERSECTION` | 风险区域与车门 mask 有像素相交 |
|
|
||||||
| `WARNING_NEAR` | 相对距离小于等于 warning 阈值 |
|
|
||||||
| `WATCH` | 相对距离处于 warning 与 release 之间 |
|
|
||||||
| `CLEAR` | 相对距离大于 release 阈值 |
|
|
||||||
| `CLEAR_NO_PERSON_ASSIGNED_TO_ACTIVE_DOOR` | 没有人被分配给该活跃门 |
|
|
||||||
| `EVIDENCE_UNAVAILABLE_TRANSITION_ZONE` | 存在 `transition` 人员,门归属不确定 |
|
|
||||||
| `DOOR_CAN_NOT_ACTIVE` | 该门 CAN 不活跃 |
|
|
||||||
| `NO_DOOR_CAN_ACTIVE` | 没有活跃门 |
|
|
||||||
|
|
||||||
默认阈值:
|
|
||||||
|
|
||||||
```text
|
|
||||||
warning_distance_relative = 0.30
|
|
||||||
release_distance_relative = 0.45
|
|
||||||
```
|
|
||||||
|
|
||||||
对每个风险区域:
|
|
||||||
|
|
||||||
```text
|
|
||||||
distance_relative = min_distance_px / sqrt(region_area_px)
|
|
||||||
risk_score = clip(1 - distance_relative / release_distance_relative, 0, 1)
|
|
||||||
```
|
|
||||||
|
|
||||||
只要 `door_overlap_px > 0`,就强制 `risk_score=1.0` 且等级为 `danger`。因此 `risk_score` 是归一化二维接近程度,不是概率,也不是厘米/毫米距离。
|
|
||||||
|
|
||||||
### 6.3 `regions` 与证据来源
|
|
||||||
|
|
||||||
每个门风险中的 `regions` 保存独立人体区域的解释信息,常用字段包括:
|
|
||||||
|
|
||||||
- `track_id`、`side`:所属轨迹和左右手;
|
|
||||||
- `source`:`RTMW_HAND_KEYPOINTS` 或 `YOLO_BODY_BOX_FALLBACK`;
|
|
||||||
- `region_area_px`:风险区域面积;
|
|
||||||
- `door_overlap_px`、`region_overlap_ratio`:与车门 mask 的相交量;
|
|
||||||
- `min_distance_px`:区域到车门 mask 的最小像素距离;
|
|
||||||
- `region_scale_px`:`sqrt(region_area_px)`;
|
|
||||||
- `distance_relative`:尺度归一化距离;
|
|
||||||
- `risk_score`、`risk_level`、`decision`:该区域自己的风险结果。
|
|
||||||
|
|
||||||
存在可用手部时,每只手用 21 点凸包计算风险;一个人没有任何可用手部时,才使用整个人体框兜底。人体框兜底更保守,也更容易产生误报。
|
|
||||||
|
|
||||||
### 6.4 `transition` 的当前行为
|
|
||||||
|
|
||||||
`door_assignments[].door_zone` 的位置标签为:
|
|
||||||
|
|
||||||
- `front`:归属前门;
|
|
||||||
- `rear`:归属后门;
|
|
||||||
- `transition`:前后门证据不足或位于过渡区;
|
|
||||||
- `unassigned`:人体尺度低于有效阈值,不参与门级风险。
|
|
||||||
|
|
||||||
当前 v9.10 中,`transition` 人员即使在 `ungated_risk` 中距离很近,也不会直接变成 `FRONT_WARNING/REAR_WARNING` 或 `DANGER`,而是让活跃门输出 `*_UNKNOWN`,其 `risk_score` 为 `null`。Demo 必须显式展示或处理 `unknown`,不能把它当成 `clear`。这是当前版本行为,不代表最终量产安全策略。
|
|
||||||
|
|
||||||
## 7. `skeletons` 的含义与可视化对接
|
|
||||||
|
|
||||||
### 7.1 数据结构
|
|
||||||
|
|
||||||
每个人的骨骼对象如下:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"track_id": 1,
|
|
||||||
"person_index": 0,
|
|
||||||
"layout": "coco_wholebody_133",
|
|
||||||
"keypoints": [[x0, y0], [x1, y1]],
|
|
||||||
"metadata": {
|
|
||||||
"left": {},
|
|
||||||
"right": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- `keypoints` 实际固定为 `133 x 2`,示例中省略了其余点;
|
|
||||||
- 坐标单位是原视频像素,原点在左上角,x 向右、y 向下;
|
|
||||||
- 坐标保留 3 位小数;
|
|
||||||
- 这里没有逐点置信度,`metadata` 提供的是手臂/手部融合后的状态;
|
|
||||||
- `keypoints` 使用的正是当前叠加视频 `draw_fused` 使用的融合结果。
|
|
||||||
|
|
||||||
### 7.2 COCO-WholeBody 133 点索引
|
|
||||||
|
|
||||||
| 索引 | 含义 |
|
|
||||||
|---|---|
|
|
||||||
| `0` | nose |
|
|
||||||
| `1/2` | left/right eye |
|
|
||||||
| `3/4` | left/right ear |
|
|
||||||
| `5/6` | left/right shoulder |
|
|
||||||
| `7/8` | left/right elbow |
|
|
||||||
| `9/10` | left/right wrist |
|
|
||||||
| `11/12` | left/right hip |
|
|
||||||
| `13/14` | left/right knee |
|
|
||||||
| `15/16` | left/right ankle |
|
|
||||||
| `17/18/19` | left big toe / small toe / heel |
|
|
||||||
| `20/21/22` | right big toe / small toe / heel |
|
|
||||||
| `23–90` | 68 个面部关键点 |
|
|
||||||
| `91–111` | 左手 21 点 |
|
|
||||||
| `112–132` | 右手 21 点 |
|
|
||||||
|
|
||||||
每只手的局部 21 点顺序:
|
|
||||||
|
|
||||||
```text
|
|
||||||
0 wrist
|
|
||||||
1-4 thumb
|
|
||||||
5-8 index finger
|
|
||||||
9-12 middle finger
|
|
||||||
13-16 ring finger
|
|
||||||
17-20 little finger
|
|
||||||
```
|
|
||||||
|
|
||||||
左手局部索引需要加 91,右手局部索引需要加 112,才能得到 `keypoints` 中的全局索引。
|
|
||||||
|
|
||||||
### 7.3 与当前叠加视频一致的绘制规则
|
|
||||||
|
|
||||||
当前风险叠加视频只绘制手臂和双手,不绘制面部、躯干和腿部。手臂连线:
|
|
||||||
|
|
||||||
```text
|
|
||||||
(5,7), (7,9) 左肩-左肘-左腕
|
|
||||||
(6,8), (8,10) 右肩-右肘-右腕
|
|
||||||
```
|
|
||||||
|
|
||||||
每只手使用以下局部连线,分别作用到 `91–111` 和 `112–132`:
|
|
||||||
|
|
||||||
```text
|
|
||||||
(0,1),(1,2),(2,3),(3,4)
|
|
||||||
(0,5),(5,6),(6,7),(7,8)
|
|
||||||
(0,9),(9,10),(10,11),(11,12)
|
|
||||||
(0,13),(13,14),(14,15),(15,16)
|
|
||||||
(0,17),(17,18),(18,19),(19,20)
|
|
||||||
```
|
|
||||||
|
|
||||||
为了与现有视频表现一致:
|
|
||||||
|
|
||||||
- `metadata[side].limb_state == "consensus"`:手臂画实线,否则画虚线;
|
|
||||||
- `metadata[side].hand_state == "rejected"`:不画该手;
|
|
||||||
- 手未被拒绝且 `distance_eligible == true`:手部画实线;
|
|
||||||
- 手未被拒绝但 `distance_eligible == false`:手部画虚线;
|
|
||||||
- `alarm_eligible == true` 表示该手允许进入 risk 区域计算。
|
|
||||||
|
|
||||||
Demo 如果只需要复现当前可视化,读取上述 48 个点即可;如果后续需要完整人体展示,可以使用全部 133 点,但应自行补充完整 WholeBody 连线定义。
|
|
||||||
|
|
||||||
## 8. `door_assignments` 对接说明
|
|
||||||
|
|
||||||
常用字段:
|
|
||||||
|
|
||||||
- `track_id`、`person_index`:与检测框和骨骼关联;
|
|
||||||
- `door_zone`:经过最多 5 帧因果中值后的 `front/rear/transition/unassigned`;
|
|
||||||
- `raw_door_zone`:当前帧未经时序中值稳定的标签;
|
|
||||||
- `front_probability`:当前帧前门概率;
|
|
||||||
- `smoothed_front_probability`:因果时序中值;
|
|
||||||
- `confidence`:相对 0.5 的归属置信度,不是人体检测置信度;
|
|
||||||
- `pose_feature_count`:本帧可用骨骼特征数量;
|
|
||||||
- `features`、`feature_frontness`:标定特征值和各特征前门贡献;
|
|
||||||
- `history_frames`:当前轨迹已用于中值的历史帧数。
|
|
||||||
|
|
||||||
当前标定阈值为:`smoothed_front_probability >= 0.72` 判为前门,`<= 0.28` 判为后门,中间为 `transition`。这些参数应随实车机位重新验证。
|
|
||||||
|
|
||||||
## 9. Demo 集成注意事项
|
|
||||||
|
|
||||||
1. 每帧先按 `track_id` 关联检测框、骨骼、门区和风险,避免多人时按数组顺序长期跟踪。
|
|
||||||
2. 主界面风险展示使用 `risk`;需要前后门双状态时使用 `door_risks.front/rear`。
|
|
||||||
3. `unknown`、`inactive` 和 `clear` 是三种不同状态,不能合并显示。
|
|
||||||
4. `risk_score == null` 表示该状态没有可用数值风险,不应转换成 0。
|
|
||||||
5. `ungated_risk` 只用于诊断叠加,不能作为最终防夹指令。
|
|
||||||
6. 更换分辨率或裁剪策略后必须核对车门 mask、骨骼坐标与视频坐标是否仍一致。
|
|
||||||
7. 当前系统可能把玻璃倒影或反光误检成人,部署 Demo 应保留原视频、检测框、`track_id` 和风险解释字段用于问题回溯。
|
|
||||||
|
|
||||||
@@ -1,205 +1,91 @@
|
|||||||
# vision-anti-pinch-0923
|
# Vision Anti-Pinch — use-can
|
||||||
|
|
||||||
以 Orin `~/vision-anti-pinch` 的 V4L2 MMAP 摄像头及 USBCANFD 模块为基线,接入 CMS v9.10 `run_risk.py` 的完整风险流程。模型及 GPU 推理代码来自远端 `~/vision-anti-pinch-0920`。
|
V4L2 摄像头 + CAN 门状态 + YOLO/RTMW TensorRT 推理 + CMS 风险判断。
|
||||||
|
默认持续推理,将最新完整结果保存在内存;可视化和文件保存均按需开启。
|
||||||
|
|
||||||
```
|
## Orin 直接运行
|
||||||
基线 V4L2 MMAP → NPP UYVY 转 BGR → 最新帧槽位
|
|
||||||
→ YOLO11s TensorRT → RTMW-L TensorRT(含小人遮挡隔离推理)
|
|
||||||
→ Python 原版轨迹 / 手臂手部融合 / 全局手关联
|
|
||||||
→ 骨骼主导的前后门归属 → 固定 mask 风险 → 逐帧 CAN 活跃门过滤
|
|
||||||
→ 内存中的最新只读结果(可选 JSONL / 浏览器预览)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 仓库分支与 Orin 直接运行包
|
适用:Jetson AGX Orin,JetPack 5.1.2 / L4T R35.4.1、CUDA 11.4、TensorRT 8.5.x、系统 OpenCV 4.5.4;Python 3.8+,numpy、cv2。
|
||||||
|
`orin/` 包含编译库、TensorRT 引擎、CAN 库、摄像头驱动、Python 运行代码和标定文件,可以单独复制到其他目录。
|
||||||
- `main`:原 CMS v9.10 Python/ONNX 部署包。
|
|
||||||
- `inference-only`:保留原 main 的纯 C++ TensorRT 推理版本。
|
|
||||||
- `use-can`(本分支):0923 CAN 集成版本。
|
|
||||||
|
|
||||||
本分支已提供可单独复制的 `orin/` 运行包,包含预编译库和 TensorRT 模型:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone --branch use-can https://git.kendeji.fun/zmv/vision-anti-pinch.git
|
cd orin
|
||||||
cd vision-anti-pinch/orin
|
./scripts/initialize_camera.sh # 开机后无 /dev/video5 时初始化,需要 sudo
|
||||||
./scripts/initialize_camera.sh # 开机后需要初始化摄像头时执行
|
./run.sh # 默认无可视化,结果保存在内存
|
||||||
./run.sh # 默认无可视化,结果保存在内存
|
# 或开启浏览器预览:http://设备IP:8765
|
||||||
./run.sh --visualize # 可选预览
|
./run.sh --visualize
|
||||||
```
|
```
|
||||||
|
|
||||||
`orin/run.sh` 通过相对目录定位运行库、模型与标定,使用系统 Python,无需现场编译。目标环境及下游调用说明见 `orin/README.md`。
|
每次只运行一个采集进程;Ctrl+C / SIGTERM 停止。关闭浏览器不会停止推理。
|
||||||
源码变更后,在目标 Orin 构建并执行 `scripts/package_orin.sh` 重新生成运行包;`ENGINE_DIR` 和 `DRIVER_DIR` 可指定模型/驱动目录。
|
系统 CUDA/TensorRT/OpenCV/Python 运行环境由目标系统提供,不随包重复分发。TensorRT 引擎不能直接用于 Thor 或其他 JetPack/GPU 环境。
|
||||||
|
|
||||||
## 两端目录和环境
|
## 从源码编译
|
||||||
|
|
||||||
- 本机:`/home/thor/文档/vision-anti-pinch-0923`
|
在目标 Orin 上执行,依赖 CMake 3.24+、CUDA/NPP、TensorRT、OpenCV 开发库:
|
||||||
- Orin:`/home/orin/vision-anti-pinch-0923`
|
|
||||||
- 目标推理环境:Orin,JetPack R35.4.1、CUDA 11.4、系统 Python 3.8、原生 TensorRT。
|
|
||||||
- 本机保存完整源码、测试报告、模型副本;本机是 Thor / R38,未发现 `/dev/video*`。Orin 的序列化 engine 不能直接作为 Thor 的本机模型使用,需在 Thor 上另行转换。本次实时采集/推理在 Orin,浏览器可在本机查看。
|
|
||||||
- 远端 `models/engines` 软链接到 `../../vision-anti-pinch-0920/orin/engines`,避免重复占用有限磁盘。不要删除被引用的 0920 模型。
|
|
||||||
- 原有远端 `vision-anti-pinch` / `vision-anti-pinch-0920` 源码均未修改。测试时已停止占用 `/dev/video5` 的旧 0920 演示进程,0923 可独立启动。
|
|
||||||
|
|
||||||
## 编译和运行(在 Orin)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ~/vision-anti-pinch-0923
|
./scripts/build.sh # 或 make
|
||||||
./scripts/build.sh
|
./scripts/initialize_camera.sh # 摄像头尚未初始化时执行
|
||||||
# 开机后如 /dev/video5 不存在,加载已部署的 0920 摄像头驱动:
|
./run_camera.sh # 使用新编译的 build/libvision_native.so
|
||||||
./scripts/initialize_camera.sh
|
|
||||||
# 编译也可使用 make
|
|
||||||
./run_camera.sh --active-door can --can-side left
|
|
||||||
# 等价入口:python3 run_risk.py --active-door can --can-side left
|
|
||||||
```
|
```
|
||||||
|
|
||||||
默认只在内存中发布结果,不创建结果目录,不绘制或编码图像,不启动 HTTP 服务。
|
源码入口复用 `orin/models/engines/` 和 `orin/lib/`,不依赖旧版工程。
|
||||||
|
修改源码后刷新直接运行包:
|
||||||
后台启动、停止:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/start.sh # 无可视化
|
./scripts/package_orin.sh # 或 make package
|
||||||
./scripts/stop.sh
|
cd orin && sha256sum -c SHA256SUMS
|
||||||
./scripts/start.sh --visualize # 可选浏览器预览
|
|
||||||
```
|
```
|
||||||
|
|
||||||
默认从 `/dev/video5` 采集 UYVY 1920×1536,驱动当前为 30 FPS。驱动不支持 `VIDIOC_G_PARM`,程序会打印警告并继续采集,命令行 fps 不代表驱动一定支持修改。
|
打包会替换运行代码,清除已删除的旧 Python 模块,并更新文件校验值。
|
||||||
|
|
||||||
- 开启 `--visualize` 后,预览:[http://192.168.195.100:8765](http://192.168.195.100:8765)
|
## 下游读取结果
|
||||||
- 开启 `--visualize` 后,最新结果:[http://192.168.195.100:8765/result.json](http://192.168.195.100:8765/result.json)
|
|
||||||
- 开启 `--visualize` 后,最新图像:[http://192.168.195.100:8765/frame.jpg](http://192.168.195.100:8765/frame.jpg)
|
|
||||||
- 后台进程日志:`logs/live.log`,PID:`logs/camera.pid`;加保存开关后,后台结果写入 `outputs/live/`。
|
|
||||||
- `--visualize` 开启预览;仅设置 `--preview-port` 不会开启预览。监听地址默认 `0.0.0.0:8765`,可用 `--preview-host 127.0.0.1` 限制为本机。可视化在独立观察线程中绘制和编码,只保留最新待绘制帧。端口占用或绘制失败不会终止推理;关闭浏览器也不会停止推理。
|
|
||||||
|
|
||||||
摄像头限定时长、模拟门状态:
|
在同一个 Python 进程中创建流水线,然后读取单个最新只读快照:
|
||||||
|
|
||||||
```bash
|
|
||||||
./run_camera.sh --no-can --active-door both --duration 30 --save-results --output outputs/demo
|
|
||||||
```
|
|
||||||
|
|
||||||
有人视频回放(同一 TensorRT/融合/风险链路):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./run_camera.sh --video tests/person-test.mp4 --no-can --active-door both \
|
|
||||||
--max-frames 150 --save-results --save-pose-records --output outputs/replay
|
|
||||||
```
|
|
||||||
|
|
||||||
`tests/person-test.mp4` 来自用户提供的 `segment_009.mp4` 第 10 秒起的 150 帧片段。
|
|
||||||
|
|
||||||
## 下游直接读取结果变量
|
|
||||||
|
|
||||||
参考 0920 的 `LatestResult` / `CameraPipeline.latest()`,0923 提供同进程 Python 接口:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from run_camera import CameraPipeline, parse_args
|
from run_camera import CameraPipeline, parse_args
|
||||||
|
|
||||||
args = parse_args([]) # 默认 CAN 模式;不打开可视化,不保存文件
|
with CameraPipeline(parse_args([])) as pipeline:
|
||||||
with CameraPipeline(args) as pipeline:
|
result = pipeline.wait_for_result(timeout=10)
|
||||||
result = pipeline.wait_for_result(timeout=10) # 等待第一帧,可选
|
# 下游自己的循环/线程中读取;首帧前为 None:
|
||||||
# 在下游自己的循环/线程中读取当前完整快照:
|
|
||||||
result = pipeline.latest_result # 等价于 pipeline.latest()
|
result = pipeline.latest_result # 等价于 pipeline.latest()
|
||||||
if result is not None:
|
if result is not None:
|
||||||
risk = result["risk"]
|
print(result['frame_index'], result['risk'])
|
||||||
door_risks = result["door_risks"]
|
door_risks = result['door_risks']
|
||||||
skeletons = result["skeletons"]
|
skeletons = result['skeletons']
|
||||||
can = result["can"]
|
|
||||||
print(result["frame_index"], risk["risk_type"], risk["risk_score"])
|
|
||||||
```
|
```
|
||||||
|
|
||||||
完整持续读取示例:`examples/read_latest.py`,在 Orin 执行。示例会自己启动推理并占用摄像头,运行前先停止已有后台进程:
|
结果字段:`frame_index`、`track_ids`、`boxes`、`skeletons`(每人 133 点)、`door_assignments`、`active_door`、`door_risks`、`risk`、`ungated_risk`、`can`、`capture`、`capture_to_result_ms`、`calibration_verified`。
|
||||||
|
|
||||||
```bash
|
- 读取不消费结果;多个读者可以持有同一个不可变快照,旧快照不会被下一帧改写。
|
||||||
./scripts/stop.sh
|
- 只保留最新结果,慢读者可以跳过帧;`wait_for_result(last_frame_index, timeout)` 可等待更新。
|
||||||
python3 examples/read_latest.py
|
- `request_stop()` 请求停止,`join()` 等待退出并传播异常;`with` 自动回收资源。
|
||||||
# 或开启可视化:
|
- 停止后仍保留最后快照;下游应检查运行状态和帧时间,避免使用过期结果。
|
||||||
python3 examples/read_latest.py --visualize
|
- `result_to_dict(result)` 生成可修改、可序列化的副本。变量属于当前进程,不是跨进程共享内存。
|
||||||
```
|
|
||||||
|
|
||||||
- `latest_result` 是单个最新结果变量,首帧前为 `None`。每帧完成风险计算后发布,早于任何可选绘制或文件写入。
|
## CAN、风险与输出选项
|
||||||
- 快照深度只读:字典使用只读映射,数组使用 tuple;字段和索引含义与原 JSON 一致。旧快照不会随下一帧变化,多个读取线程互不消费或修改结果。
|
|
||||||
- 只保留最新一帧,没有结果队列;下游处理慢可跳过中间帧。需要每帧历史时显式开启 `--save-results`。
|
|
||||||
- `pipeline.wait_for_result(after_frame_index, timeout)` 是可选等待接口,返回更新的最新结果;超时或结束且无新帧时返回 `None`。
|
|
||||||
- `pipeline.request_stop()` 请求停止,`pipeline.join()` 等待退出并向调用方传播推理异常;停止后可读取最后快照及 `pipeline.summary`。使用 `with` 自动停止并回收资源。
|
|
||||||
- 停止后的快照只是最后结果;持续控制时应同时检查 `pipeline.running()` 及帧序号/时间戳,避免使用旧结果。
|
|
||||||
- 需要修改副本或 JSON 序列化时使用 `from run_camera import result_to_dict`,再调用 `result_to_dict(result)`。
|
|
||||||
- 变量属于创建 `CameraPipeline` 的 Python 进程。另一个进程或 C++ 程序不能直接 import 读取后台服务的变量,需要另行使用 IPC/HTTP 接口;当前没有新增 C++ 风险结果 ABI。
|
|
||||||
|
|
||||||
## 可选输出开关
|
`0x391` 的 data[0..3] 依次为左前、右前、左后、右后。默认 `--can-side left`,右侧使用 `--can-side right`。值为 0 表示对应门活跃;报文缺失或超过默认 1000 ms 时,输出 `CAN_UNKNOWN`、分数 null。当前接口不发送防夹执行器指令。
|
||||||
|
|
||||||
| 参数 | 行为 |
|
优先用融合后的有效手部区域判断风险,无有效手部时使用人体框兜底;最终读取 `risk` / `door_risks`。`ungated_risk` 仅供诊断,`unknown` 不能当作 clear。
|
||||||
|
标定文件来自研发机位,默认 `calibration_verified=false`,换机位须重新标定。
|
||||||
|
|
||||||
|
| 参数 | 作用 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 无输出参数 | 只发布内存结果,不绘制、不保存结果文件 |
|
| `--visualize` | 开启独立绘制线程及 HTTP 预览;默认不绘制、不编码、不监听端口 |
|
||||||
| `--visualize` | 开启独立可视化线程和 HTTP 预览 |
|
| `--save-results` | 保存逐帧 JSONL 及 latest.json,默认不保存 |
|
||||||
| `--save-results` | 保存逐帧 JSONL 及约每 10 秒的 latest.json |
|
| `--save-snapshots` | 每约 10 秒保存叠加图像 |
|
||||||
| `--save-snapshots` | 约每 10 秒绘制并保存 latest.jpg,无 HTTP 服务 |
|
| `--write-video` | 保存叠加视频,可配合 `--duration` 限时运行 |
|
||||||
| `--save-pose-records` | 保存诊断姿态 JSONL |
|
| `--output PATH` | 指定保存目录,本身不启用保存 |
|
||||||
| `--write-video` | 绘制并保存叠加 MP4,无需打开预览 |
|
| `--no-can --active-door both` | 显式模拟门状态 |
|
||||||
|
|
||||||
输出开关可组合。`--output` 仅指定保存目录,不会自动开启保存。未开启保存时,旧输出文件保留但不会更新。任一保存开关开启后,停止时额外保存 summary.json;默认内存模式的统计保存在 `pipeline.summary` 并打印到终端。
|
运行 `./run_camera.sh --help` 或 `orin/run.sh --help` 查看全部选项。JSONL 每 32 MiB 轮换,保留两个历史文件;视频不轮换。开启可视化后可读取 `/result.json` 和 `/frame.jpg`。
|
||||||
|
|
||||||
恢复此前的预览与文件输出行为:
|
## 目录
|
||||||
|
|
||||||
```bash
|
- `native/`:实际参与编译的摄像头/CAN、CUDA/NPP、TensorRT 源码及头文件。
|
||||||
./run_camera.sh --visualize --save-results --save-snapshots --output outputs/demo
|
- `pipeline/`:运行时姿态融合、跟踪、遮挡处理、风险、可视化和结果变量。
|
||||||
```
|
- `annotations/`:源码运行时的标定文件。
|
||||||
|
- `scripts/`:构建、打包、摄像头初始化。
|
||||||
## CAN 约定
|
- `orin/`:可单独复制并直接执行的运行包。
|
||||||
|
|
||||||
按用户确认的 `0x391` 数据布局:
|
|
||||||
|
|
||||||
| 字节 | 车门 |
|
|
||||||
|---|---|
|
|
||||||
| data[0] | 左前 |
|
|
||||||
| data[1] | 右前 |
|
|
||||||
| data[2] | 左后 |
|
|
||||||
| data[3] | 右后 |
|
|
||||||
|
|
||||||
- `--can-side left` 使用 data[0]/data[2];`right` 使用 data[1]/data[3]。默认 left,请按摄像头安装侧选择。
|
|
||||||
- 值为 0 时对应门活跃,每帧读取最新 CAN 快照。有效报文至少需要 4 字节。
|
|
||||||
- 未收到有效报文或超过 `--can-timeout-ms`(默认 1000 ms),整体输出 `CAN_UNKNOWN`,门级输出 `FRONT_UNKNOWN/REAR_UNKNOWN`,分数为 null。
|
|
||||||
- 此时 `active_door=none` 仅为内部无有效活跃门输入,业务必须读取 `risk` 和 `can.valid`,不能将其解释为已确认两门关闭。
|
|
||||||
- 显式 `--active-door front/rear/both/none` 是模拟模式,输出 `can.source=simulation`,预览显示 CAN SIMULATION。`--no-can` 要求显式选择模拟状态。
|
|
||||||
- 本项目提供最终风险结果;基线未提供防夹执行器的 CAN 指令协议,因此没有自行添加执行器报文。
|
|
||||||
- 本次 USB-CAN `VCI_OpenDevice` 返回失败,`lsusb` 中未见 USB-CAN 设备;真实 0x391 报文链路尚未验证。连接适配器后重新启动程序;字节映射及门状态组合通过软件测试。
|
|
||||||
|
|
||||||
## 风险行为与机位标定
|
|
||||||
|
|
||||||
风险算法、默认 warning=0.30 / release=0.45、133 点骨骼、track_id、前后门归属、transition→unknown、无有效手部时人体框兜底均沿用原版。详见 `README-risk-upstream.md`。
|
|
||||||
|
|
||||||
`annotations/constant_door_mask.json` 和 `door_zone_multifeature.json` 原样复制,属于原研发机位。当前室内摄像头位置与其不一致,默认输出 `calibration_verified=false`,预览显示 CALIBRATION UNVERIFIED。实车验证需更换标定文件;`--calibration-verified` 仅标记经人工验证的标定,不会自动标定。
|
|
||||||
|
|
||||||
当前实时场景没有人,约 29 FPS 是该场景的实测值,不能当作多人满负载帧率。
|
|
||||||
|
|
||||||
## 优化与输出
|
|
||||||
|
|
||||||
- 重用 0920 的常驻 TensorRT engine、CUDA/NPP 预处理、显存及锁页缓存、GPU UYVY 转换。
|
|
||||||
- 独立取帧线程只保留最新帧,主动丢弃旧帧,避免积压;不像 0920 那样单独并行检测线程,完整 Python 融合阶段按结果顺序执行。
|
|
||||||
- 风险 mask / 距离变换按原版缓存;开启预览时默认每 3 帧提交一次待绘制图像;开启快照保存时每 10 秒一次。
|
|
||||||
- 长期运行清理过期轨迹,计时统计仅保留最近 3000 帧。
|
|
||||||
- 开启 `--save-results` 后,`causal_results.jsonl` 默认每 32 MiB 轮换,保留 `.1` 和 `.2`;`--jsonl-max-mb 0` 可禁用轮换。读取全部历史时按 `.2 → .1 → 当前文件` 顺序处理。
|
|
||||||
- `--save-pose-records` 额外保存同样轮换的诊断姿态记录;`--write-video` 录制叠加 MP4(视频不轮换,建议配合 `--duration`)。
|
|
||||||
- 文件输出遵循上面的独立开关;关闭可视化仍可完整读取内存结果。
|
|
||||||
- 新增 `capture`、`can`、`capture_to_result_ms`、`calibration_verified` 字段,原版核心 JSON 字段保留。
|
|
||||||
- 驱动原始时间戳存在时钟偏移,保存在 `capture.driver_timestamp_ms` 供诊断。延迟采用主机单调时钟在 DQBUF 后取样,表示“主机取帧到风险结果”,不包含曝光到驱动交付的时间。
|
|
||||||
|
|
||||||
## 文件关系与测试
|
|
||||||
|
|
||||||
- `main.cpp`, `v4l2_capture.*`, `can_comm.*`, `include/zcan.h`, `lib/`:基线工程。新工程修正错误 V4L2 buffer 处理;旧 main 同步修正 CAN 字节解析。
|
|
||||||
- `native/native_api.cpp`:采集线程、CAN 快照、TensorRT 的 ctypes C 接口。
|
|
||||||
- `accelerator/`:远端 0920 源码快照,当前构建只选取 TensorRT/预处理/检测/姿态源文件。
|
|
||||||
- `pipeline/`:原版 Python 算法,推理适配在 `native_backend.py`;ONNX/Ultralytics 的可选导入移到旧离线入口,原版风险公式不变。
|
|
||||||
- `Makefile.baseline` 和 `make baseline`:旧基线演示程序的编译方式;默认 `make` 编译新的原生共享库。
|
|
||||||
- `pipeline/latest_result.py`:线程安全的单结果发布槽位及不可变快照。
|
|
||||||
- `examples/read_latest.py`:同进程下游读取示例。
|
|
||||||
- `tests/test_latest_result.py`:结果所有权、线程生命周期、可选输出、独立可视化及异常传播测试。
|
|
||||||
- `tests/test_risk_integration.py`:CAN 左右门组合、失效状态、手部/人体兜底、transition、轨迹回收、日志轮换测试。
|
|
||||||
- `TEST_REPORT.md`:实测结果和限制。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 -m unittest discover -s tests -v
|
|
||||||
```
|
|
||||||
|
|
||||||
本机重新部署源码(会使用 SSH 交互认证,不保存密码):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/deploy.sh orin@192.168.195.100
|
|
||||||
```
|
|
||||||
|
|
||||||
重新编译共享库前应先停止 0923 运行进程,再启动新进程加载新库。
|
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
# 2026-09-23 集成验证
|
|
||||||
|
|
||||||
执行设备:192.168.195.100 / Orin;摄像头 /dev/video5,UYVY 1920×1536。当前机位朝向室内设备,没有测试人员;用户确认暂不调整机位。
|
|
||||||
|
|
||||||
| 验证 | 结果 |
|
|
||||||
|---|---|
|
|
||||||
| 原生 C++/CUDA 共享库 | Orin Release 编译通过 |
|
|
||||||
| 摄像头 + CAN 模式,30 秒 | 882 帧,29.399 FPS,无运行异常 |
|
|
||||||
| 主机取帧到风险结果 | 均值 42.369 ms,P95 57.368 ms |
|
|
||||||
| latest-frame 策略 | 30 秒期间丢弃 16 个积压帧 |
|
|
||||||
| 真实 CAN | USB-CAN 打开失败,lsusb 未见该设备;882 帧均为 CAN_UNKNOWN,score=null |
|
|
||||||
| 有人视频 150 帧回放 | 32.898 FPS,31 帧有 1 人,119 帧无有效人体框 |
|
|
||||||
| RTMW 骨骼 | 31 个人体记录,每个 133 点 |
|
|
||||||
| 风险证据 | 22 个手部区域,9 个人体框兜底区域 |
|
|
||||||
| 视频门级风险 | clear 132,watch 5,warning 2,unknown 11 |
|
|
||||||
| 原始 run_risk.py 对照 | 同一姿态记录重放 150 帧,核心输出完全一致 |
|
|
||||||
| 行为测试 | 本机与 Orin 的 8 项测试均通过 |
|
|
||||||
| HTTP 实时预览 | 本机访问远端 /result.json 成功,连续返回新帧 |
|
|
||||||
|
|
||||||
对照方式:将远端 `outputs/person-replay/pose_records.jsonl` 下载到本机,用用户原始工程的 `run_risk.py --pose-jsonl` 运行,逐帧比较原始 JSON 的所有顶层字段,包括 skeletons、door_assignments、door_risks、risk、ungated_risk。仅排除不同工程目录导致的绝对 mask 路径差异;150 帧无差异。**此项对照验证相同姿态输入下的风险集成,不是 TensorRT 与 ONNX 输出的数值误差测试。**
|
|
||||||
|
|
||||||
证据:
|
|
||||||
|
|
||||||
- `outputs/camera-can/summary.json`
|
|
||||||
- `outputs/person-replay/summary.json` 与 `causal_results.jsonl`
|
|
||||||
- `outputs/reference-replay/summary.json` 与 `causal_results.jsonl`
|
|
||||||
- `outputs/reference-parity.json`
|
|
||||||
- `logs/build.log`, `logs/camera-can.log`, `logs/person-replay.log`
|
|
||||||
|
|
||||||
`outputs/camera-smoke/` 是初次调通记录,使用了不可靠的驱动时间戳,延迟统计无效。已修正计时来源,性能结论以 `camera-can` 为准。
|
|
||||||
|
|
||||||
未完成的硬件验证:有效 CAN 报文到达与门切换、真实摄像头下人员/手部靠近门区、实车机位 mask 与前后门参数标定。当前无法据此宣称已完成实车防夹效果验收。
|
|
||||||
|
|
||||||
本机没有摄像头设备节点;本机完成源码/模型归档、风险对照及行为测试,通过网络预览 Orin 的实时结果。
|
|
||||||
|
|
||||||
|
|
||||||
## 最新结果变量与可选可视化改造
|
|
||||||
|
|
||||||
已对照重启后远端 0920 的 `include/latest_result.hpp` 和 `src/main.cpp`,采用默认关闭可视化、单个不可变最新快照、start/latest/request_stop/join 生命周期。
|
|
||||||
|
|
||||||
- 本机与 Orin:17 项测试通过,覆盖原有风险测试及只读快照、旧快照保持、跨线程等待/停止、错误传播、默认无绘制/HTTP/结果文件、JSON 与内存结果一致、预览端口失败不影响推理、独立可视化线程。
|
|
||||||
- Orin 真实 TensorRT 视频回放:下游示例读取 90 帧,27.321 FPS;未开启可视化和文件保存。保留 watch/warning/unknown 等风险输出。
|
|
||||||
- Orin 真实摄像头:下游示例读取 90 帧,28.858 FPS;主机取帧至结果均值 32.341 ms;关闭可视化,没有创建指定输出目录。
|
|
||||||
- `--visualize` 实机运行 15 秒:成功通过 HTTP 读取逐帧结果及 JPEG 图像;未开启文件保存,没有创建指定输出目录。
|
|
||||||
- 后台运行入口默认无可视化;开启预览需显式加 `--visualize`。
|
|
||||||
- 重启后摄像头节点未自动生成。0920 初始化脚本因驱动缺少 vermagic 元数据而判定不匹配;0923 新增初始化脚本,存在 vermagic 时检查内核版本,缺失时交给正常 insmod 的内核检查,未使用强制加载参数。加载原有 0920 驱动后 /dev/video5 恢复。
|
|
||||||
|
|
||||||
日志:`logs/tests-optional-local.log`、`logs/tests-optional-orin.log`、`logs/result-api-video.log`、`logs/result-api-camera.log`、`logs/optional-preview.log`、`logs/optional-preview-probe.json`。
|
|
||||||
|
|
||||||
## use-can Orin 可运行包验证
|
|
||||||
|
|
||||||
- 独立运行包位于 `orin/`:包含预编译 ARM64 原生库、TensorRT 引擎、Python 风险代码、CAN USB 库、GMSL 驱动和 ./run.sh。
|
|
||||||
- 在 Orin 新建的独立临时目录复制整个运行包,SHA256SUMS 全部通过。
|
|
||||||
- 从临时目录执行 `./run.sh --max-frames 90`,实际摄像头 90 帧完成,29.080 FPS,主机取帧至风险结果平均 36.310 ms;无结果文件输出。
|
|
||||||
- 原生库 RUNPATH 为 `$ORIGIN/../lib:/usr/local/cuda/lib64`,启动脚本按自身位置定位所有项目资产。
|
|
||||||
- 系统依赖仍为目标 JetPack 的 CUDA / TensorRT / OpenCV 与 Python numpy/cv2,不包含操作系统或完整 Python 环境。
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
*.sh text eol=lf
|
|
||||||
*.engine -text
|
|
||||||
*.ko -text
|
|
||||||
/orin/** -text
|
|
||||||
/config/door_mask.json -text
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
/build/
|
|
||||||
/engines/
|
|
||||||
/logs/
|
|
||||||
*.tmp
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
cmake_minimum_required(VERSION 3.24)
|
|
||||||
if(NOT DEFINED CMAKE_CUDA_ARCHITECTURES)
|
|
||||||
set(CMAKE_CUDA_ARCHITECTURES native)
|
|
||||||
endif()
|
|
||||||
project(camera_risk LANGUAGES CXX CUDA)
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
||||||
set(CMAKE_CUDA_STANDARD 17)
|
|
||||||
find_package(CUDAToolkit REQUIRED)
|
|
||||||
find_package(OpenCV REQUIRED COMPONENTS core imgproc imgcodecs highgui)
|
|
||||||
find_package(Threads REQUIRED)
|
|
||||||
find_path(TENSORRT_INCLUDE_DIR NvInfer.h PATH_SUFFIXES aarch64-linux-gnu x86_64-linux-gnu REQUIRED)
|
|
||||||
find_library(TENSORRT_LIBRARY nvinfer REQUIRED)
|
|
||||||
add_library(camera_risk_core
|
|
||||||
src/trt_engine.cpp src/pose_engine.cu src/gpu_preprocess.cu src/uyvy_converter.cu
|
|
||||||
src/detector.cpp src/door_risk.cpp src/camera.cpp src/camera_pipeline.cpp src/config.cpp)
|
|
||||||
target_include_directories(camera_risk_core PUBLIC include third_party ${TENSORRT_INCLUDE_DIR} ${OpenCV_INCLUDE_DIRS})
|
|
||||||
target_link_libraries(camera_risk_core PUBLIC ${TENSORRT_LIBRARY} ${OpenCV_LIBS} Threads::Threads CUDA::cudart CUDA::nppc CUDA::nppig CUDA::nppidei CUDA::nppicc)
|
|
||||||
target_compile_options(camera_risk_core PRIVATE $<$<COMPILE_LANGUAGE:CXX>:-Wall;-Wextra;-Wpedantic>)
|
|
||||||
add_library(camera_risk_visualizer src/visualizer.cpp)
|
|
||||||
target_link_libraries(camera_risk_visualizer PUBLIC camera_risk_core)
|
|
||||||
add_executable(camera_risk src/main.cpp)
|
|
||||||
target_link_libraries(camera_risk PRIVATE camera_risk_visualizer)
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
# Vision Anti-Pinch
|
|
||||||
|
|
||||||
C++ / TensorRT 实时摄像头风险推理,算法来自最新 run_risk.py。
|
|
||||||
持续取图、推理并更新内存中的 FrameResult;可视化可选。
|
|
||||||
|
|
||||||
## Orin 直接运行
|
|
||||||
|
|
||||||
适用环境:Jetson AGX Orin、JetPack 5.1.2 / L4T R35.4.1、CUDA 11.4、TensorRT 8.5.2、OpenCV 4.5.4。
|
|
||||||
仓库已包含该板端编译的程序和重新构建的 TensorRT 引擎,无需先编译或转换。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://git.kendeji.fun/zmv/vision-anti-pinch.git
|
|
||||||
cd vision-anti-pinch/orin
|
|
||||||
./scripts/initialize_camera.sh # 开机后初始化 /dev/video5 摄像头链路
|
|
||||||
./run.sh # 持续推理,结果只保存在内存
|
|
||||||
```
|
|
||||||
|
|
||||||
可选板端本地可视化:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./run.sh --visualize
|
|
||||||
```
|
|
||||||
|
|
||||||
Ctrl+C / SIGTERM 停止程序。没有有限帧选项;正常运行和退出不输出统计。
|
|
||||||
关闭预览窗口或按 q/Esc 只关闭预览,推理继续。没有图像网络服务、端口转发、JSONL 或视频输出。
|
|
||||||
|
|
||||||
摄像头为 /dev/video5,1920×1536 UYVY,30 FPS。
|
|
||||||
当前 door mask 是指定的 constant_door_mask.json 副本,标定状态为 false;装车后应更换与实际画面匹配的门区并更新标定状态。
|
|
||||||
即使不开启可视化,风险计算仍需要门区。
|
|
||||||
|
|
||||||
## 下游获取结果
|
|
||||||
|
|
||||||
main.cpp 注释中也包含以下示例。下游通过自己的调用位置或线程读取:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
auto result = pipeline.latest();
|
|
||||||
if (result) {
|
|
||||||
const auto& risk = result->risk;
|
|
||||||
const auto& people = result->persons;
|
|
||||||
// result->frame_index / track_ids / boxes
|
|
||||||
// result->image / door_mask_pixels / capture / timing
|
|
||||||
// people[i].selected / smoothed / temporal:各 133 个关键点
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
latest() 返回 shared_ptr<const FrameResult>;首帧尚未完成时为空。
|
|
||||||
读取不删除结果,也不产生消费状态。推理不关心是否有下游读取。
|
|
||||||
下游持有的结果保持有效,后续更新不会改变旧快照;历史保存由下游自行负责。
|
|
||||||
公共定义位于 include/camera_pipeline.hpp 和 include/frame_result.hpp。
|
|
||||||
|
|
||||||
## 从源码构建
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/build.sh
|
|
||||||
./scripts/initialize_camera.sh
|
|
||||||
./run_camera.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
源码构建要求 CMake 3.24+、CUDA/NPP、TensorRT 和 OpenCV 开发依赖。
|
|
||||||
构建脚本会优先使用 Orin 上已有的 ~/miniconda3/envs/yolo/bin/cmake。
|
|
||||||
源码配置直接使用 orin/engines/ 中的板端引擎。该引擎仅适用于上述 Orin 环境;仓库不包含 ONNX 或模型转换脚本。
|
|
||||||
独立库目标为 camera_risk_core;可视化在 camera_risk_visualizer。
|
|
||||||
|
|
||||||
## 目录
|
|
||||||
|
|
||||||
- src/、include/:采集、推理、结果变量及可选绘制。
|
|
||||||
- config/:源码构建程序的配置和固定门区。
|
|
||||||
- scripts/:构建和摄像头初始化脚本。
|
|
||||||
- orin/:已编译程序、TensorRT 引擎、配置、当前 GMSL 驱动及启动脚本。
|
|
||||||
- third_party/:nlohmann JSON 及其许可证。
|
|
||||||
|
|
||||||
NPP 用于 UYVY 转换、YOLO 缩放填充、RTMW 仿射裁剪;CUDA 用于通道转换与归一化。
|
|
||||||
风险几何计算仍包含 CPU/OpenCV 操作;算法内部的临时 JSON 对象只在内存中使用。
|
|
||||||
|
|
||||||
当前摄像头驱动启动时偶发首帧 ERROR,应用会丢弃并计数;持续错误、采集超时或推理错误会明确报错并退出。
|
|
||||||
现有 GMSL 驱动停止采集时可产生 videobuf2 缓冲区告警,v4l2-ctl 同样复现;需在驱动侧完成量产修复和验证。
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"camera": {
|
|
||||||
"device": "/dev/video5",
|
|
||||||
"width": 1920,
|
|
||||||
"height": 1536,
|
|
||||||
"fps": 30,
|
|
||||||
"buffers": 4,
|
|
||||||
"timeout_ms": 2000
|
|
||||||
},
|
|
||||||
"inference": {
|
|
||||||
"engine_directory": "../orin/engines",
|
|
||||||
"detector_confidence": 0.3,
|
|
||||||
"max_people": 3,
|
|
||||||
"queue_capacity": 2
|
|
||||||
},
|
|
||||||
"door": {
|
|
||||||
"mask": "door_mask.json",
|
|
||||||
"warning_relative": 0.3,
|
|
||||||
"release_relative": 0.45,
|
|
||||||
"calibration_verified": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "x-anylabeling",
|
|
||||||
"flags": {},
|
|
||||||
"shapes": [
|
|
||||||
{
|
|
||||||
"label": "door",
|
|
||||||
"points": [
|
|
||||||
[
|
|
||||||
212.86,
|
|
||||||
145.919
|
|
||||||
],
|
|
||||||
[
|
|
||||||
378.891,
|
|
||||||
851.857
|
|
||||||
],
|
|
||||||
[
|
|
||||||
468.293,
|
|
||||||
996.446
|
|
||||||
],
|
|
||||||
[
|
|
||||||
536.408,
|
|
||||||
1294.131
|
|
||||||
],
|
|
||||||
[
|
|
||||||
553.437,
|
|
||||||
1404.7
|
|
||||||
],
|
|
||||||
[
|
|
||||||
561.951,
|
|
||||||
1532.279
|
|
||||||
],
|
|
||||||
[
|
|
||||||
51.086,
|
|
||||||
1532.279
|
|
||||||
],
|
|
||||||
[
|
|
||||||
8.514,
|
|
||||||
1515.268
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0.0,
|
|
||||||
1468.489
|
|
||||||
],
|
|
||||||
[
|
|
||||||
8.514,
|
|
||||||
26.845
|
|
||||||
],
|
|
||||||
[
|
|
||||||
25.543,
|
|
||||||
5.582
|
|
||||||
],
|
|
||||||
[
|
|
||||||
131.973,
|
|
||||||
-2.924
|
|
||||||
],
|
|
||||||
[
|
|
||||||
191.574,
|
|
||||||
1.329
|
|
||||||
],
|
|
||||||
[
|
|
||||||
208.603,
|
|
||||||
145.919
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"group_id": null,
|
|
||||||
"shape_type": "polygon",
|
|
||||||
"flags": {}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"imagePath": "constant_door_mask_reference.jpg",
|
|
||||||
"imageData": null,
|
|
||||||
"imageHeight": 1536,
|
|
||||||
"imageWidth": 1920,
|
|
||||||
"sourceVideo": "D:\\huang.xuewei\\CMS\\video\\20260921104502\\segment_009.mp4",
|
|
||||||
"sourceFrameIndex": 244,
|
|
||||||
"constantDoorMask": true
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <condition_variable>
|
|
||||||
#include <deque>
|
|
||||||
#include <mutex>
|
|
||||||
#include <optional>
|
|
||||||
#include <stdexcept>
|
|
||||||
|
|
||||||
namespace cms {
|
|
||||||
// FIFO with bounded ownership. Normal close drains; cancellation may discard.
|
|
||||||
template<class T> class BoundedQueue {
|
|
||||||
public:
|
|
||||||
explicit BoundedQueue(std::size_t capacity):capacity_(capacity) {
|
|
||||||
if(!capacity)throw std::invalid_argument("Queue capacity must be positive");
|
|
||||||
}
|
|
||||||
bool push(T item) {
|
|
||||||
std::unique_lock<std::mutex> lock(mutex_);
|
|
||||||
changed_.wait(lock,[&]{return closed_||items_.size()<capacity_;});
|
|
||||||
if(closed_)return false;
|
|
||||||
items_.push_back(std::move(item));changed_.notify_all();return true;
|
|
||||||
}
|
|
||||||
std::optional<T> pop() {
|
|
||||||
std::unique_lock<std::mutex> lock(mutex_);
|
|
||||||
changed_.wait(lock,[&]{return closed_||!items_.empty();});
|
|
||||||
if(items_.empty())return {};
|
|
||||||
T item=std::move(items_.front());items_.pop_front();changed_.notify_all();return item;
|
|
||||||
}
|
|
||||||
void close(bool discard=false) {
|
|
||||||
std::lock_guard<std::mutex> lock(mutex_);
|
|
||||||
closed_=true;if(discard)items_.clear();changed_.notify_all();
|
|
||||||
}
|
|
||||||
private:
|
|
||||||
const std::size_t capacity_;
|
|
||||||
std::mutex mutex_;std::condition_variable changed_;
|
|
||||||
std::deque<T> items_;bool closed_{};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include "image.hpp"
|
|
||||||
#include <atomic>
|
|
||||||
#include <memory>
|
|
||||||
#include <optional>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
namespace cms {
|
|
||||||
struct CameraConfig {
|
|
||||||
std::string device{"/dev/video5"};
|
|
||||||
int width{1920},height{1536},fps{30},buffers{4},timeout_ms{2000};
|
|
||||||
};
|
|
||||||
struct CapturedFrame {
|
|
||||||
CaptureMetadata metadata;
|
|
||||||
std::shared_ptr<const BgrImage> image;
|
|
||||||
};
|
|
||||||
// The capture thread exclusively owns read(); requestStop() is thread safe.
|
|
||||||
class Camera {
|
|
||||||
public:
|
|
||||||
explicit Camera(CameraConfig config);
|
|
||||||
~Camera();
|
|
||||||
Camera(Camera const&)=delete;
|
|
||||||
Camera& operator=(Camera const&)=delete;
|
|
||||||
std::optional<CapturedFrame> read();
|
|
||||||
void requestStop() noexcept;
|
|
||||||
private:
|
|
||||||
struct Impl;
|
|
||||||
std::unique_ptr<Impl> impl_;
|
|
||||||
};
|
|
||||||
std::uint64_t monotonicNs();
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include "camera.hpp"
|
|
||||||
#include "frame_result.hpp"
|
|
||||||
#include <filesystem>
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
namespace cms {
|
|
||||||
struct PipelineConfig {
|
|
||||||
CameraConfig camera;
|
|
||||||
std::filesystem::path engines,door_mask;
|
|
||||||
double detector_confidence{0.30};
|
|
||||||
int max_people{3};
|
|
||||||
double warning_relative{0.30},release_relative{0.45};
|
|
||||||
std::size_t queue_capacity{2};
|
|
||||||
bool door_mask_calibration_verified{false};
|
|
||||||
};
|
|
||||||
struct PipelineStats {
|
|
||||||
std::uint64_t captured{},published{},driver_sequence_gaps{},discarded_error_frames{};
|
|
||||||
double mean_capture_to_result_ms{};
|
|
||||||
};
|
|
||||||
// One lifecycle per instance. start() warms engines before starting capture.
|
|
||||||
// Runs continuously until requestStop() or a worker failure.
|
|
||||||
// latest() reads the immutable result variable without removing or acknowledging it.
|
|
||||||
// No result queue, consumer registration or frame-count termination.
|
|
||||||
// Stop is safe from any thread;
|
|
||||||
// destruction closes queues, interrupts capture and joins every worker.
|
|
||||||
class CameraPipeline {
|
|
||||||
public:
|
|
||||||
explicit CameraPipeline(PipelineConfig config);
|
|
||||||
~CameraPipeline();
|
|
||||||
CameraPipeline(CameraPipeline const&)=delete;
|
|
||||||
CameraPipeline& operator=(CameraPipeline const&)=delete;
|
|
||||||
void start();
|
|
||||||
FrameResultPtr latest() const;
|
|
||||||
bool running() const noexcept;
|
|
||||||
void requestStop() noexcept;
|
|
||||||
void join();
|
|
||||||
PipelineStats stats() const;
|
|
||||||
private:
|
|
||||||
struct Impl;
|
|
||||||
std::unique_ptr<Impl> impl_;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include "camera_pipeline.hpp"
|
|
||||||
namespace cms {
|
|
||||||
PipelineConfig loadConfig(std::filesystem::path const& path);
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <nlohmann/json.hpp>
|
|
||||||
#include <opencv2/core.hpp>
|
|
||||||
#include <filesystem>
|
|
||||||
#include <map>
|
|
||||||
#include <set>
|
|
||||||
using Json = nlohmann::ordered_json;
|
|
||||||
namespace cms {
|
|
||||||
struct DoorOptions {
|
|
||||||
std::filesystem::path constant, directory;
|
|
||||||
std::string format{"auto"};
|
|
||||||
std::set<std::string> labels{"door", "车门"};
|
|
||||||
int offset{};
|
|
||||||
bool require{};
|
|
||||||
double warning{0.30}, release{0.45};
|
|
||||||
};
|
|
||||||
class DoorRisk {
|
|
||||||
public:
|
|
||||||
DoorRisk(DoorOptions options, cv::Size size);
|
|
||||||
void initialize() { load(0); }
|
|
||||||
Json evaluate(int frame, Json& record);
|
|
||||||
cv::Mat const& mask() const { return mask_; }
|
|
||||||
private:
|
|
||||||
void load(int frame);
|
|
||||||
cv::Mat rasterize(std::filesystem::path const& path) const;
|
|
||||||
DoorOptions options_;
|
|
||||||
cv::Size size_;
|
|
||||||
std::string format_, status_;
|
|
||||||
std::filesystem::path loadedPath_;
|
|
||||||
std::map<int, std::filesystem::path> paths_;
|
|
||||||
cv::Mat mask_, distance_;
|
|
||||||
bool cached_{};
|
|
||||||
};
|
|
||||||
double rounded(double value, int digits);
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
// Public downstream contract. Owns its data; no GPU, JSON or network handles.
|
|
||||||
#include "image.hpp"
|
|
||||||
#include <array>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <memory>
|
|
||||||
#include <optional>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace cms {
|
|
||||||
struct DoorMaskResult {
|
|
||||||
std::string status;
|
|
||||||
int annotation_index{};
|
|
||||||
std::optional<std::string> path;
|
|
||||||
std::string format;
|
|
||||||
int area_px{};
|
|
||||||
};
|
|
||||||
struct RegionRisk {
|
|
||||||
std::string status;
|
|
||||||
std::optional<double> risk_score;
|
|
||||||
std::string risk_level;
|
|
||||||
std::string decision;
|
|
||||||
std::string source;
|
|
||||||
int track_id{};
|
|
||||||
std::string side;
|
|
||||||
std::optional<std::string> state;
|
|
||||||
// Unavailable evidence has no geometric measurements, matching the reference.
|
|
||||||
std::optional<int> region_area_px, door_overlap_px;
|
|
||||||
std::optional<double> region_overlap_ratio, min_distance_px;
|
|
||||||
std::optional<double> region_scale_px, distance_relative;
|
|
||||||
};
|
|
||||||
struct RiskResult {
|
|
||||||
std::string status;
|
|
||||||
std::optional<double> risk_score;
|
|
||||||
std::string risk_level;
|
|
||||||
std::string decision;
|
|
||||||
std::optional<std::string> source;
|
|
||||||
std::optional<int> track_id;
|
|
||||||
std::optional<std::string> side;
|
|
||||||
DoorMaskResult door_mask;
|
|
||||||
double warning_distance_relative{}, release_distance_relative{};
|
|
||||||
std::vector<RegionRisk> regions;
|
|
||||||
};
|
|
||||||
struct PosePoint { double x{},y{},score{}; };
|
|
||||||
struct HandState {
|
|
||||||
std::string state, source;
|
|
||||||
bool limb_consensus{}, distance_eligible{}, alarm_eligible{};
|
|
||||||
};
|
|
||||||
struct PersonResult {
|
|
||||||
int track_id{};
|
|
||||||
double confidence{};
|
|
||||||
std::array<double,4> box{};
|
|
||||||
std::array<PosePoint,133> selected{}, smoothed{}, temporal{};
|
|
||||||
std::array<HandState,2> hands; // left, right
|
|
||||||
};
|
|
||||||
struct HandRegion {
|
|
||||||
int person_id{},track_id{};
|
|
||||||
std::string side;
|
|
||||||
std::array<PosePoint,21> keypoints{};
|
|
||||||
};
|
|
||||||
struct StageTiming {
|
|
||||||
double detector_ms{}, pose_ms{}, fusion_risk_ms{}, capture_to_result_ms{};
|
|
||||||
};
|
|
||||||
struct FrameResult {
|
|
||||||
int frame_index{};
|
|
||||||
std::vector<int> track_ids;
|
|
||||||
std::vector<std::array<double,4>> boxes;
|
|
||||||
RiskResult risk;
|
|
||||||
CaptureMetadata capture;
|
|
||||||
StageTiming timing;
|
|
||||||
std::vector<PersonResult> persons;
|
|
||||||
std::vector<HandRegion> hand_regions;
|
|
||||||
// Both images belong to this result. The constant mask is shared across frames.
|
|
||||||
std::shared_ptr<const BgrImage> image;
|
|
||||||
std::shared_ptr<const BinaryMask> door_mask_pixels;
|
|
||||||
bool door_mask_calibration_verified{};
|
|
||||||
};
|
|
||||||
using FrameResultPtr = std::shared_ptr<const FrameResult>;
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <cstdint>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace cms {
|
|
||||||
// Immutable when published. Stride is explicit; no borrowed camera/GPU memory.
|
|
||||||
struct BgrImage {
|
|
||||||
int width{}, height{}, stride{};
|
|
||||||
std::vector<std::uint8_t> pixels;
|
|
||||||
};
|
|
||||||
struct BinaryMask {
|
|
||||||
int width{}, height{}, stride{};
|
|
||||||
std::vector<std::uint8_t> pixels;
|
|
||||||
};
|
|
||||||
struct CaptureMetadata {
|
|
||||||
std::uint64_t frame_index{};
|
|
||||||
std::uint32_t driver_sequence{};
|
|
||||||
std::uint64_t driver_timestamp_ns{};
|
|
||||||
std::uint64_t received_monotonic_ns{};
|
|
||||||
std::uint64_t capture_monotonic_ns{};
|
|
||||||
bool driver_timestamp_monotonic{};
|
|
||||||
std::uint32_t skipped_driver_frames{};
|
|
||||||
std::uint32_t discarded_error_frames{};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include "frame_result.hpp"
|
|
||||||
#include <atomic>
|
|
||||||
#include <stdexcept>
|
|
||||||
|
|
||||||
namespace cms {
|
|
||||||
// One immutable result variable. Readers neither remove nor acknowledge results.
|
|
||||||
class LatestResult {
|
|
||||||
public:
|
|
||||||
void publish(FrameResultPtr result) {
|
|
||||||
if(!result)throw std::invalid_argument("Cannot publish a null result");
|
|
||||||
std::atomic_store_explicit(&latest_,std::move(result),std::memory_order_release);
|
|
||||||
}
|
|
||||||
FrameResultPtr load() const {
|
|
||||||
return std::atomic_load_explicit(&latest_,std::memory_order_acquire);
|
|
||||||
}
|
|
||||||
private:
|
|
||||||
FrameResultPtr latest_;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <string>
|
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace fangjia {
|
|
||||||
|
|
||||||
struct Box {
|
|
||||||
float x1{};
|
|
||||||
float y1{};
|
|
||||||
float x2{};
|
|
||||||
float y2{};
|
|
||||||
float score{};
|
|
||||||
int trackId{};
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Keypoint {
|
|
||||||
float x{};
|
|
||||||
float y{};
|
|
||||||
float score{};
|
|
||||||
};
|
|
||||||
|
|
||||||
struct PersonPose {
|
|
||||||
Box box;
|
|
||||||
std::array<Keypoint, 133> keypoints{};
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class LimbState {
|
|
||||||
Observed,
|
|
||||||
Provisional,
|
|
||||||
Recovered,
|
|
||||||
Reassigned,
|
|
||||||
Interpolated,
|
|
||||||
PredictedShort,
|
|
||||||
ReachableMedium,
|
|
||||||
ReachableLong,
|
|
||||||
};
|
|
||||||
|
|
||||||
inline char const* limbStateName(LimbState state) {
|
|
||||||
switch (state) {
|
|
||||||
case LimbState::Observed: return "observed";
|
|
||||||
case LimbState::Provisional: return "provisional";
|
|
||||||
case LimbState::Recovered: return "recovered";
|
|
||||||
case LimbState::Reassigned: return "reassigned";
|
|
||||||
case LimbState::Interpolated: return "interpolated";
|
|
||||||
case LimbState::PredictedShort: return "predicted_short";
|
|
||||||
case LimbState::ReachableMedium: return "reachable_medium";
|
|
||||||
case LimbState::ReachableLong: return "reachable_long";
|
|
||||||
}
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
struct LimbObservation {
|
|
||||||
int trackId{};
|
|
||||||
bool left{};
|
|
||||||
LimbState state{LimbState::ReachableLong};
|
|
||||||
Keypoint elbow{};
|
|
||||||
Keypoint wrist{};
|
|
||||||
std::array<Keypoint, 21> hand{};
|
|
||||||
Box personBox{};
|
|
||||||
bool handValid{};
|
|
||||||
bool alarmEligible{};
|
|
||||||
float handQuality{};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Exact pre-V2 hand evidence consumed by the Python risk stage. These points
|
|
||||||
// come from raw/isolated fusion before temporal reconstruction.
|
|
||||||
struct GlobalHand {
|
|
||||||
int personId{};
|
|
||||||
int trackId{};
|
|
||||||
bool left{};
|
|
||||||
std::string source;
|
|
||||||
std::string state;
|
|
||||||
bool distanceEligible{};
|
|
||||||
bool alarmEligible{};
|
|
||||||
float score{};
|
|
||||||
float spanPx{};
|
|
||||||
std::array<Keypoint, 21> keypoints{};
|
|
||||||
std::vector<std::pair<int, bool>> aliases;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct FusionMeta {
|
|
||||||
bool limbConsensus{}, handValid{}, distanceEligible{}, alarmEligible{};
|
|
||||||
double handScore{};
|
|
||||||
std::string handSource, handState;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct PoseFrameResult {
|
|
||||||
std::vector<std::array<FusionMeta, 2>> metadata;
|
|
||||||
std::vector<PersonPose> selectedPeople;
|
|
||||||
std::vector<PersonPose> smoothedPeople;
|
|
||||||
std::vector<PersonPose> v2People;
|
|
||||||
std::vector<LimbObservation> limbs;
|
|
||||||
std::vector<GlobalHand> globalHands;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace fangjia
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "pipeline_types.hpp"
|
|
||||||
#include "trt_engine.hpp"
|
|
||||||
|
|
||||||
#include <opencv2/core.hpp>
|
|
||||||
|
|
||||||
#include <deque>
|
|
||||||
#include <array>
|
|
||||||
#include <optional>
|
|
||||||
#include <string>
|
|
||||||
#include <unordered_map>
|
|
||||||
|
|
||||||
namespace fangjia {
|
|
||||||
|
|
||||||
class BoxTracker {
|
|
||||||
public:
|
|
||||||
std::vector<int> update(std::vector<Box>& boxes, int frameIndex, int width, int height);
|
|
||||||
|
|
||||||
private:
|
|
||||||
struct Track { Box box; int lastFrame{}; };
|
|
||||||
int nextId_{1};
|
|
||||||
std::unordered_map<int, Track> tracks_;
|
|
||||||
};
|
|
||||||
|
|
||||||
class RtmwEngine {
|
|
||||||
public:
|
|
||||||
RtmwEngine(
|
|
||||||
std::filesystem::path const& enginePath,
|
|
||||||
TrtLogger& logger,
|
|
||||||
int maxSourceWidth,
|
|
||||||
int maxSourceHeight,
|
|
||||||
int maxPeople = 3);
|
|
||||||
~RtmwEngine();
|
|
||||||
|
|
||||||
RtmwEngine(RtmwEngine const&) = delete;
|
|
||||||
RtmwEngine& operator=(RtmwEngine const&) = delete;
|
|
||||||
|
|
||||||
std::vector<PersonPose> infer(
|
|
||||||
cv::Mat const& frame,
|
|
||||||
std::vector<Box> const& boxes,
|
|
||||||
cudaStream_t stream,
|
|
||||||
float* preprocessMs = nullptr,
|
|
||||||
float* inferenceMs = nullptr,
|
|
||||||
float* postprocessMs = nullptr);
|
|
||||||
|
|
||||||
void warmup(cudaStream_t stream);
|
|
||||||
|
|
||||||
private:
|
|
||||||
TrtEngine engine_;
|
|
||||||
int maxSourceWidth_{};
|
|
||||||
int maxSourceHeight_{};
|
|
||||||
int maxPeople_{};
|
|
||||||
std::uint8_t* sourceDevice_{};
|
|
||||||
std::uint8_t* cropsDevice_{};
|
|
||||||
float* networkDevice_{};
|
|
||||||
std::vector<double> centersHost_;
|
|
||||||
std::vector<double> scalesHost_;
|
|
||||||
std::vector<float> simccXHost_;
|
|
||||||
std::vector<float> simccYHost_;
|
|
||||||
};
|
|
||||||
|
|
||||||
class PoseFusionTracker {
|
|
||||||
public:
|
|
||||||
PoseFrameResult update(
|
|
||||||
std::vector<PersonPose> const& raw,
|
|
||||||
std::vector<PersonPose> const& isolated,
|
|
||||||
int frameIndex);
|
|
||||||
|
|
||||||
private:
|
|
||||||
struct Vec2 { double x{}; double y{}; };
|
|
||||||
struct SideFusion {
|
|
||||||
std::array<Vec2, 3> limbPrevious{};
|
|
||||||
std::array<Vec2, 3> limbVelocity{};
|
|
||||||
std::array<Vec2, 21> handPrevious{};
|
|
||||||
std::array<Vec2, 21> handVelocity{};
|
|
||||||
bool hasLimbPrevious{};
|
|
||||||
bool hasLimbVelocity{};
|
|
||||||
bool hasHandPrevious{};
|
|
||||||
bool hasHandVelocity{};
|
|
||||||
std::string limbSource;
|
|
||||||
std::string handSource;
|
|
||||||
};
|
|
||||||
struct Endpoint {
|
|
||||||
bool hasWrist{};
|
|
||||||
Vec2 wrist{};
|
|
||||||
Vec2 velocity{};
|
|
||||||
std::array<Vec2, 21> handShape{};
|
|
||||||
bool hasHandShape{};
|
|
||||||
int lastObservedFrame{-1};
|
|
||||||
int missedFrames{};
|
|
||||||
bool hasLastElbow{};
|
|
||||||
Vec2 lastElbow{};
|
|
||||||
bool hasLastDirection{};
|
|
||||||
Vec2 lastDirection{};
|
|
||||||
};
|
|
||||||
struct PersonState {
|
|
||||||
std::array<SideFusion, 2> fusion;
|
|
||||||
std::array<Endpoint, 2> endpoints;
|
|
||||||
std::array<std::deque<std::array<double, 2>>, 2> boneSamples;
|
|
||||||
std::array<Vec2, 133> smoothed{};
|
|
||||||
bool hasSmoothed{};
|
|
||||||
int lastFrame{-1};
|
|
||||||
};
|
|
||||||
std::unordered_map<int, PersonState> states_;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace fangjia
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include "door_risk.hpp"
|
|
||||||
#include "pipeline_types.hpp"
|
|
||||||
namespace cms {
|
|
||||||
inline Json xy(std::vector<fangjia::PersonPose> const& people, int digits=3) {
|
|
||||||
Json a=Json::array(); for(auto const& p:people) { Json k=Json::array();for(auto const& v:p.keypoints) k.push_back({rounded(v.x,digits),rounded(v.y,digits)});a.push_back(k); } return a;
|
|
||||||
}
|
|
||||||
inline Json scores(std::vector<fangjia::PersonPose> const& people) {
|
|
||||||
Json a=Json::array();for(auto const& p:people) {Json k=Json::array();for(auto const& v:p.keypoints) k.push_back(v.score);a.push_back(k);}return a;
|
|
||||||
}
|
|
||||||
inline Json makeRecord(int frame,std::vector<fangjia::PersonPose> const& raw,std::vector<fangjia::PersonPose> const& isolated,fangjia::PoseFrameResult const& fused, bool diagnostics=true, bool visual=true) {
|
|
||||||
Json r={{"frame_index",frame},{"track_ids",Json::array()},{"boxes",Json::array()},{"det_confidences",Json::array()}};
|
|
||||||
for(auto const&p:raw) {auto const&b=p.box;r["track_ids"].push_back(b.trackId);r["boxes"].push_back({rounded(b.x1,3),rounded(b.y1,3),rounded(b.x2,3),rounded(b.y2,3)});r["det_confidences"].push_back(rounded(b.score,4));}
|
|
||||||
if(diagnostics) {
|
|
||||||
r["raw_keypoints"]=xy(raw,9);r["raw_scores"]=scores(raw);r["isolated_keypoints"]=xy(isolated,9);r["isolated_scores"]=scores(isolated);
|
|
||||||
r["inference_boxes"]=Json::array();for(auto const&p:raw)r["inference_boxes"].push_back({p.box.x1,p.box.y1,p.box.x2,p.box.y2});
|
|
||||||
}
|
|
||||||
if(visual||diagnostics) {
|
|
||||||
r["temporal_selected_keypoints"]=xy(fused.selectedPeople);r["temporal_keypoints"]=xy(fused.smoothedPeople);r["v2_keypoints"]=xy(fused.v2People);
|
|
||||||
r["temporal_metadata"]=Json::array();r["v2_metadata"]=Json::array();
|
|
||||||
for(size_t p=0;p<fused.metadata.size();++p) {
|
|
||||||
Json m=Json::object(),v=Json::object();
|
|
||||||
for(int s=0;s<2;++s) {auto side=s==0?"left":"right";auto const&t=fused.metadata[p][s];
|
|
||||||
m[side]={{"limb_state",t.limbConsensus?"consensus":"inferred"},{"hand_source",t.handSource},{"hand_state",t.handState},{"distance_eligible",t.distanceEligible},{"alarm_eligible",t.alarmEligible},{"selected_hand_valid",t.handValid},{"selected_hand_score",t.handScore}};
|
|
||||||
v[side]={{"state",fangjia::limbStateName(fused.limbs[p*2+s].state)}};
|
|
||||||
} r["temporal_metadata"].push_back(m);r["v2_metadata"].push_back(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
r["global_hands"]=Json::array();
|
|
||||||
for(auto const& h:fused.globalHands) {
|
|
||||||
Json points=Json::array(),aliases=Json::array();double cx=0,cy=0;
|
|
||||||
for(auto const& k:h.keypoints) {points.push_back({rounded(k.x,3),rounded(k.y,3)});cx+=k.x;cy+=k.y;}
|
|
||||||
for(auto const&a:h.aliases) aliases.push_back({{"person_id",a.first},{"side",a.second?"left":"right"}});
|
|
||||||
r["global_hands"].push_back({{"person_id",h.personId},{"side",h.left?"left":"right"},{"source",h.source},{"state",h.state},{"distance_eligible",h.distanceEligible},{"alarm_eligible",h.alarmEligible},{"score",rounded(h.score,4)},{"center",{rounded(cx/21,3),rounded(cy/21,3)}},{"span_px",rounded(h.spanPx,3)},{"keypoints",points},{"aliases",aliases}});
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include "frame_result.hpp"
|
|
||||||
#include "door_risk.hpp"
|
|
||||||
namespace cms {
|
|
||||||
template<class T> inline std::optional<T> optionalField(Json const& value,char const* key) {
|
|
||||||
auto it=value.find(key);return it==value.end()||it->is_null()?std::nullopt:std::optional<T>(it->get<T>());
|
|
||||||
}
|
|
||||||
inline FrameResult makeFrameResult(Json const& record,Json const& risk,
|
|
||||||
CaptureMetadata capture) {
|
|
||||||
FrameResult out;
|
|
||||||
out.frame_index=record.at("frame_index").get<int>();
|
|
||||||
out.track_ids=record.at("track_ids").get<std::vector<int>>();
|
|
||||||
out.boxes=record.at("boxes").get<std::vector<std::array<double,4>>>();
|
|
||||||
out.capture=capture;
|
|
||||||
auto& r=out.risk;
|
|
||||||
r.status=risk.at("status");r.risk_score=optionalField<double>(risk,"risk_score");
|
|
||||||
r.risk_level=risk.at("risk_level");r.decision=risk.at("decision");
|
|
||||||
r.source=optionalField<std::string>(risk,"source");r.track_id=optionalField<int>(risk,"track_id");r.side=optionalField<std::string>(risk,"side");
|
|
||||||
auto const& d=risk.at("door_mask");
|
|
||||||
r.door_mask={d.at("status"),d.at("annotation_index"),optionalField<std::string>(d,"path"),d.at("format"),d.at("area_px")};
|
|
||||||
r.warning_distance_relative=risk.at("warning_distance_relative");r.release_distance_relative=risk.at("release_distance_relative");
|
|
||||||
for(auto const& j:risk.at("regions")) {
|
|
||||||
RegionRisk v;
|
|
||||||
v.status=j.at("status");v.risk_score=optionalField<double>(j,"risk_score");
|
|
||||||
v.risk_level=j.at("risk_level");v.decision=j.at("decision");v.source=j.at("source");
|
|
||||||
v.track_id=j.at("track_id");v.side=j.at("side");v.state=optionalField<std::string>(j,"state");
|
|
||||||
v.region_area_px=optionalField<int>(j,"region_area_px");v.door_overlap_px=optionalField<int>(j,"door_overlap_px");
|
|
||||||
v.region_overlap_ratio=optionalField<double>(j,"region_overlap_ratio");v.min_distance_px=optionalField<double>(j,"min_distance_px");
|
|
||||||
v.region_scale_px=optionalField<double>(j,"region_scale_px");v.distance_relative=optionalField<double>(j,"distance_relative");
|
|
||||||
r.regions.push_back(std::move(v));
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include "camera_pipeline.hpp"
|
|
||||||
#include <atomic>
|
|
||||||
#include <csignal>
|
|
||||||
#include <pthread.h>
|
|
||||||
#include <stdexcept>
|
|
||||||
#include <thread>
|
|
||||||
|
|
||||||
namespace cms {
|
|
||||||
// Construct before starting workers so all threads inherit the signal mask.
|
|
||||||
// Stop requests run in an ordinary thread, never an asynchronous signal handler.
|
|
||||||
class StopSignals {
|
|
||||||
public:
|
|
||||||
explicit StopSignals(CameraPipeline& pipeline):pipeline_(pipeline) {
|
|
||||||
sigemptyset(&signals_);sigaddset(&signals_,SIGINT);sigaddset(&signals_,SIGTERM);
|
|
||||||
if(pthread_sigmask(SIG_BLOCK,&signals_,&old_)!=0)throw std::runtime_error("Cannot block termination signals");
|
|
||||||
try {
|
|
||||||
thread_=std::thread([this]{
|
|
||||||
while(!done_) {
|
|
||||||
timespec timeout{0,100000000};
|
|
||||||
int signal=sigtimedwait(&signals_,nullptr,&timeout);
|
|
||||||
if(signal==SIGINT||signal==SIGTERM){pipeline_.requestStop();break;}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}catch(...){pthread_sigmask(SIG_SETMASK,&old_,nullptr);throw;}
|
|
||||||
}
|
|
||||||
~StopSignals(){done_=true;if(thread_.joinable())thread_.join();pthread_sigmask(SIG_SETMASK,&old_,nullptr);}
|
|
||||||
StopSignals(StopSignals const&)=delete;
|
|
||||||
StopSignals& operator=(StopSignals const&)=delete;
|
|
||||||
private:
|
|
||||||
CameraPipeline& pipeline_;
|
|
||||||
sigset_t signals_{},old_{};
|
|
||||||
std::atomic<bool> done_{false};
|
|
||||||
std::thread thread_;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include "frame_result.hpp"
|
|
||||||
#include <opencv2/core.hpp>
|
|
||||||
|
|
||||||
namespace cms {
|
|
||||||
class CameraPipeline;
|
|
||||||
// Pure drawing from the immutable result. Never re-runs inference.
|
|
||||||
cv::Mat renderResult(FrameResult const& result);
|
|
||||||
// Optional observer: closing the window returns without stopping the pipeline.
|
|
||||||
void runLocalVisualization(CameraPipeline const& pipeline);
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
exec "$root/build/camera_risk" --config "$root/config/camera.json" "$@"
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
cmake_bin="${CMAKE_COMMAND:-cmake}"
|
|
||||||
if [[ -x "$HOME/miniconda3/envs/yolo/bin/cmake" ]]; then cmake_bin="${CMAKE_COMMAND:-$HOME/miniconda3/envs/yolo/bin/cmake}"; fi
|
|
||||||
"$cmake_bin" -S "$root" -B "$root/build" -DCMAKE_BUILD_TYPE=Release -DCMAKE_CUDA_COMPILER="${CUDACXX:-/usr/local/cuda/bin/nvcc}" "$@"
|
|
||||||
"$cmake_bin" --build "$root/build" -j4
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
exec "$root/orin/scripts/initialize_camera.sh" "$@"
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
#include "camera.hpp"
|
|
||||||
#include "uyvy_converter.hpp"
|
|
||||||
#include <linux/videodev2.h>
|
|
||||||
#include <sys/eventfd.h>
|
|
||||||
#include <sys/ioctl.h>
|
|
||||||
#include <sys/mman.h>
|
|
||||||
#include <poll.h>
|
|
||||||
#include <fcntl.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
#include <time.h>
|
|
||||||
#include <cerrno>
|
|
||||||
#include <cstring>
|
|
||||||
#include <limits>
|
|
||||||
#include <stdexcept>
|
|
||||||
|
|
||||||
namespace cms {
|
|
||||||
std::uint64_t monotonicNs() {
|
|
||||||
timespec time{};
|
|
||||||
if(clock_gettime(CLOCK_MONOTONIC,&time)!=0)throw std::runtime_error("clock_gettime failed");
|
|
||||||
return std::uint64_t(time.tv_sec)*1000000000ULL+time.tv_nsec;
|
|
||||||
}
|
|
||||||
namespace {
|
|
||||||
void checkedIoctl(int fd,unsigned long request,void* argument,char const* operation) {
|
|
||||||
int status;
|
|
||||||
do {status=ioctl(fd,request,argument);} while(status<0&&errno==EINTR);
|
|
||||||
if(status<0)throw std::runtime_error(std::string(operation)+": "+std::strerror(errno));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
struct Camera::Impl {
|
|
||||||
struct Buffer {void* data{};std::size_t size{};};
|
|
||||||
CameraConfig config;
|
|
||||||
int fd{-1},stopFd{-1};bool streaming{};
|
|
||||||
std::uint32_t stride{},lastSequence{};bool haveSequence{};
|
|
||||||
std::uint64_t index{};
|
|
||||||
std::uint32_t consecutiveErrors{};
|
|
||||||
std::atomic<bool> stopped{false};
|
|
||||||
std::vector<Buffer> buffers;
|
|
||||||
std::unique_ptr<UyvyConverter> converter;
|
|
||||||
explicit Impl(CameraConfig c):config(std::move(c)) {
|
|
||||||
if(config.width<=0||config.width>8192||config.height<=0||config.height>8192||config.width%2||config.fps<=0||config.fps>240||config.buffers<2||config.buffers>32||config.timeout_ms<1)
|
|
||||||
throw std::invalid_argument("Invalid camera configuration");
|
|
||||||
try {
|
|
||||||
stopFd=eventfd(0,EFD_CLOEXEC|EFD_NONBLOCK);
|
|
||||||
if(stopFd<0)throw std::runtime_error("Cannot create camera stop event");
|
|
||||||
fd=open(config.device.c_str(),O_RDWR|O_NONBLOCK|O_CLOEXEC);
|
|
||||||
if(fd<0)throw std::runtime_error("Cannot open "+config.device+": "+std::strerror(errno));
|
|
||||||
v4l2_capability capability{};checkedIoctl(fd,VIDIOC_QUERYCAP,&capability,"VIDIOC_QUERYCAP");
|
|
||||||
auto caps=(capability.capabilities&V4L2_CAP_DEVICE_CAPS)?capability.device_caps:capability.capabilities;
|
|
||||||
if(!(caps&V4L2_CAP_VIDEO_CAPTURE)||!(caps&V4L2_CAP_STREAMING))throw std::runtime_error("Camera does not support single-plane streaming capture");
|
|
||||||
v4l2_format format{};format.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
|
||||||
format.fmt.pix.width=config.width;format.fmt.pix.height=config.height;
|
|
||||||
format.fmt.pix.pixelformat=V4L2_PIX_FMT_UYVY;format.fmt.pix.field=V4L2_FIELD_NONE;
|
|
||||||
checkedIoctl(fd,VIDIOC_S_FMT,&format,"VIDIOC_S_FMT");
|
|
||||||
if(format.fmt.pix.pixelformat!=V4L2_PIX_FMT_UYVY||format.fmt.pix.width!=static_cast<unsigned>(config.width)||format.fmt.pix.height!=static_cast<unsigned>(config.height))
|
|
||||||
throw std::runtime_error("Camera rejected the configured UYVY resolution; refusing mismatched mask coordinates");
|
|
||||||
stride=format.fmt.pix.bytesperline;
|
|
||||||
if(stride<static_cast<unsigned>(config.width*2))throw std::runtime_error("Invalid camera row stride");
|
|
||||||
v4l2_streamparm params{};params.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
|
||||||
if(ioctl(fd,VIDIOC_G_PARM,¶ms)==0&&(params.parm.capture.capability&V4L2_CAP_TIMEPERFRAME)) {
|
|
||||||
params.parm.capture.timeperframe.numerator=1;params.parm.capture.timeperframe.denominator=config.fps;
|
|
||||||
checkedIoctl(fd,VIDIOC_S_PARM,¶ms,"VIDIOC_S_PARM");
|
|
||||||
auto const& interval=params.parm.capture.timeperframe;
|
|
||||||
if(!interval.numerator||interval.denominator!=static_cast<unsigned>(config.fps)*interval.numerator)
|
|
||||||
throw std::runtime_error("Camera rejected requested frame rate");
|
|
||||||
}
|
|
||||||
v4l2_requestbuffers request{};request.count=config.buffers;
|
|
||||||
request.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;request.memory=V4L2_MEMORY_MMAP;
|
|
||||||
checkedIoctl(fd,VIDIOC_REQBUFS,&request,"VIDIOC_REQBUFS");
|
|
||||||
if(request.count<2)throw std::runtime_error("Insufficient V4L2 buffers");
|
|
||||||
buffers.resize(request.count);
|
|
||||||
for(unsigned i=0;i<request.count;++i) {
|
|
||||||
v4l2_buffer buffer{};buffer.type=request.type;buffer.memory=request.memory;buffer.index=i;
|
|
||||||
checkedIoctl(fd,VIDIOC_QUERYBUF,&buffer,"VIDIOC_QUERYBUF");
|
|
||||||
if(buffer.length<std::size_t(stride)*config.height)throw std::runtime_error("Camera buffer smaller than negotiated frame");
|
|
||||||
buffers[i].size=buffer.length;
|
|
||||||
auto mapping=mmap(nullptr,buffer.length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,buffer.m.offset);
|
|
||||||
if(mapping==MAP_FAILED)throw std::runtime_error("Camera mmap failed");
|
|
||||||
buffers[i].data=mapping;checkedIoctl(fd,VIDIOC_QBUF,&buffer,"VIDIOC_QBUF");
|
|
||||||
}
|
|
||||||
converter=std::make_unique<UyvyConverter>(config.width,config.height);
|
|
||||||
auto type=V4L2_BUF_TYPE_VIDEO_CAPTURE;checkedIoctl(fd,VIDIOC_STREAMON,&type,"VIDIOC_STREAMON");streaming=true;
|
|
||||||
} catch(...) {release();throw;}
|
|
||||||
}
|
|
||||||
~Impl(){release();}
|
|
||||||
void release() noexcept {
|
|
||||||
if(streaming){auto type=V4L2_BUF_TYPE_VIDEO_CAPTURE;ioctl(fd,VIDIOC_STREAMOFF,&type);streaming=false;}
|
|
||||||
for(auto const& b:buffers)if(b.data)munmap(b.data,b.size);
|
|
||||||
buffers.clear();
|
|
||||||
if(fd>=0){close(fd);fd=-1;}
|
|
||||||
if(stopFd>=0){close(stopFd);stopFd=-1;}
|
|
||||||
}
|
|
||||||
void stop() noexcept {
|
|
||||||
stopped=true;std::uint64_t one=1;
|
|
||||||
if(stopFd>=0){auto ignored=write(stopFd,&one,sizeof(one));(void)ignored;}
|
|
||||||
}
|
|
||||||
std::optional<CapturedFrame> readFrame() {
|
|
||||||
while(!stopped.load()) {
|
|
||||||
pollfd descriptors[2]={{fd,POLLIN,0},{stopFd,POLLIN,0}};
|
|
||||||
int ready=poll(descriptors,2,config.timeout_ms);
|
|
||||||
if(ready<0&&errno==EINTR)continue;
|
|
||||||
if(stopped.load()||(descriptors[1].revents&POLLIN))return {};
|
|
||||||
if(ready==0)throw std::runtime_error("Camera capture timeout: "+config.device);
|
|
||||||
if(ready<0||(descriptors[0].revents&(POLLERR|POLLHUP|POLLNVAL)))throw std::runtime_error("Camera disconnected or poll failed");
|
|
||||||
v4l2_buffer buffer{};buffer.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;buffer.memory=V4L2_MEMORY_MMAP;
|
|
||||||
int status=ioctl(fd,VIDIOC_DQBUF,&buffer);
|
|
||||||
if(status<0&&(errno==EAGAIN||errno==EINTR))continue;
|
|
||||||
if(status<0)throw std::runtime_error("VIDIOC_DQBUF: "+std::string(std::strerror(errno)));
|
|
||||||
if(buffer.index>=buffers.size())throw std::runtime_error("Invalid V4L2 buffer index");
|
|
||||||
// The GMSL driver can mark its startup buffer as corrupt. Requeue
|
|
||||||
// it without conversion or inference; persistent corruption is fatal.
|
|
||||||
if(buffer.flags&V4L2_BUF_FLAG_ERROR) {
|
|
||||||
checkedIoctl(fd,VIDIOC_QBUF,&buffer,"VIDIOC_QBUF(error frame)");
|
|
||||||
if(++consecutiveErrors>=static_cast<unsigned>(config.fps))
|
|
||||||
throw std::runtime_error("Camera produced one second of consecutive error frames");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
CapturedFrame frame;
|
|
||||||
try {
|
|
||||||
if(buffer.bytesused<std::size_t(stride)*config.height||buffer.bytesused>buffers[buffer.index].size)
|
|
||||||
throw std::runtime_error("Invalid camera frame: flags="+std::to_string(buffer.flags)+", used="+std::to_string(buffer.bytesused)+", required="+std::to_string(std::size_t(stride)*config.height)+", allocated="+std::to_string(buffers[buffer.index].size));
|
|
||||||
auto& m=frame.metadata;
|
|
||||||
m.discarded_error_frames=consecutiveErrors;consecutiveErrors=0;
|
|
||||||
m.driver_sequence=buffer.sequence;m.received_monotonic_ns=monotonicNs();
|
|
||||||
m.driver_timestamp_ns=std::uint64_t(buffer.timestamp.tv_sec)*1000000000ULL+std::uint64_t(buffer.timestamp.tv_usec)*1000;
|
|
||||||
m.driver_timestamp_monotonic=(buffer.flags&V4L2_BUF_FLAG_TIMESTAMP_MASK)==V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;
|
|
||||||
m.capture_monotonic_ns=m.driver_timestamp_monotonic&&m.driver_timestamp_ns>0&&m.driver_timestamp_ns<=m.received_monotonic_ns?m.driver_timestamp_ns:m.received_monotonic_ns;
|
|
||||||
if(haveSequence) {
|
|
||||||
auto delta=static_cast<std::uint32_t>(buffer.sequence-lastSequence);
|
|
||||||
if(delta==0||delta>0x7fffffffU)throw std::runtime_error("Camera sequence reset or repeated frame");
|
|
||||||
m.skipped_driver_frames=delta-1;index+=delta;
|
|
||||||
}
|
|
||||||
m.frame_index=index;
|
|
||||||
lastSequence=buffer.sequence;haveSequence=true;
|
|
||||||
// Convert before requeuing. No published result references MMAP memory.
|
|
||||||
frame.image=converter->convert(buffers[buffer.index].data,stride);
|
|
||||||
} catch(...) {ioctl(fd,VIDIOC_QBUF,&buffer);throw;}
|
|
||||||
checkedIoctl(fd,VIDIOC_QBUF,&buffer,"VIDIOC_QBUF");return frame;
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Camera::Camera(CameraConfig config):impl_(std::make_unique<Impl>(std::move(config))){}
|
|
||||||
Camera::~Camera()=default;
|
|
||||||
std::optional<CapturedFrame> Camera::read(){return impl_->readFrame();}
|
|
||||||
void Camera::requestStop() noexcept {impl_->stop();}
|
|
||||||
}
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
#include "camera_pipeline.hpp"
|
|
||||||
#include "bounded_queue.hpp"
|
|
||||||
#include "latest_result.hpp"
|
|
||||||
#include "detector.hpp"
|
|
||||||
#include "door_risk.hpp"
|
|
||||||
#include "records.hpp"
|
|
||||||
#include "result_conversion.hpp"
|
|
||||||
#include <opencv2/core.hpp>
|
|
||||||
#include <atomic>
|
|
||||||
#include <cmath>
|
|
||||||
#include <cstring>
|
|
||||||
#include <exception>
|
|
||||||
#include <mutex>
|
|
||||||
#include <limits>
|
|
||||||
#include <thread>
|
|
||||||
|
|
||||||
namespace cms {
|
|
||||||
namespace {
|
|
||||||
struct Stream {
|
|
||||||
cudaStream_t value{};
|
|
||||||
Stream(){CUDA_CHECK(cudaStreamCreateWithFlags(&value,cudaStreamNonBlocking));}
|
|
||||||
~Stream(){cudaStreamDestroy(value);}
|
|
||||||
};
|
|
||||||
cv::Mat imageView(BgrImage const& image) {
|
|
||||||
return cv::Mat(image.height,image.width,CV_8UC3,const_cast<std::uint8_t*>(image.pixels.data()),image.stride);
|
|
||||||
}
|
|
||||||
double elapsedMs(std::uint64_t start){return (monotonicNs()-start)*1e-6;}
|
|
||||||
void attachPoses(FrameResult& result,fangjia::PoseFrameResult const& fused) {
|
|
||||||
for(std::size_t i=0;i<fused.selectedPeople.size();++i) {
|
|
||||||
auto const& source=fused.selectedPeople[i];
|
|
||||||
PersonResult person;person.track_id=source.box.trackId;person.confidence=source.box.score;
|
|
||||||
person.box={source.box.x1,source.box.y1,source.box.x2,source.box.y2};
|
|
||||||
for(std::size_t k=0;k<133;++k) {
|
|
||||||
auto const&a=source.keypoints[k];auto const&b=fused.smoothedPeople[i].keypoints[k];auto const&c=fused.v2People[i].keypoints[k];
|
|
||||||
person.selected[k]={a.x,a.y,a.score};person.smoothed[k]={b.x,b.y,b.score};person.temporal[k]={c.x,c.y,c.score};
|
|
||||||
}
|
|
||||||
for(int side=0;side<2;++side){auto const& m=fused.metadata[i][side];person.hands[side]={m.handState,m.handSource,m.limbConsensus,m.distanceEligible,m.alarmEligible};}
|
|
||||||
result.persons.push_back(std::move(person));
|
|
||||||
}
|
|
||||||
for(auto const& source:fused.globalHands) {
|
|
||||||
HandRegion hand;hand.person_id=source.personId;hand.track_id=source.trackId;hand.side=source.left?"left":"right";
|
|
||||||
for(std::size_t k=0;k<21;++k)hand.keypoints[k]={source.keypoints[k].x,source.keypoints[k].y,source.keypoints[k].score};
|
|
||||||
result.hand_regions.push_back(std::move(hand));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
struct CameraPipeline::Impl {
|
|
||||||
struct Detected {CapturedFrame frame;std::vector<fangjia::Box> boxes;double elapsed_ms{};};
|
|
||||||
PipelineConfig config;
|
|
||||||
BoundedQueue<CapturedFrame> captures;
|
|
||||||
BoundedQueue<Detected> detections;
|
|
||||||
LatestResult results;
|
|
||||||
fangjia::TrtLogger logger;
|
|
||||||
std::unique_ptr<Detector> detector;
|
|
||||||
std::unique_ptr<fangjia::RtmwEngine> pose;
|
|
||||||
std::unique_ptr<DoorRisk> risk;
|
|
||||||
std::shared_ptr<const BinaryMask> doorPixels;
|
|
||||||
std::unique_ptr<Camera> camera;
|
|
||||||
std::thread captureThread,detectorThread,inferenceThread;
|
|
||||||
mutable std::mutex stateMutex;
|
|
||||||
std::exception_ptr failure;
|
|
||||||
bool started{};
|
|
||||||
std::atomic<bool> stopped{false},active{false};
|
|
||||||
std::atomic<std::uint64_t> captured{0},published{0},gaps{0},errorFrames{0};
|
|
||||||
double totalLatency{};
|
|
||||||
|
|
||||||
explicit Impl(PipelineConfig c):config(std::move(c)),captures(config.queue_capacity),detections(config.queue_capacity) {
|
|
||||||
if(config.queue_capacity>8)throw std::invalid_argument("Queue capacity must be between 1 and 8");
|
|
||||||
if(config.max_people<1||config.max_people>16||!std::isfinite(config.detector_confidence)||config.detector_confidence<=0||config.detector_confidence>=1)
|
|
||||||
throw std::invalid_argument("Invalid detector configuration");
|
|
||||||
if(!std::filesystem::is_regular_file(config.door_mask))throw std::invalid_argument("Missing door mask: "+config.door_mask.string());
|
|
||||||
}
|
|
||||||
~Impl(){stop();joinWorkers();}
|
|
||||||
void joinWorkers() noexcept {
|
|
||||||
if(captureThread.joinable())captureThread.join();
|
|
||||||
if(detectorThread.joinable())detectorThread.join();
|
|
||||||
if(inferenceThread.joinable())inferenceThread.join();
|
|
||||||
}
|
|
||||||
void stop() noexcept {
|
|
||||||
stopped=true;active=false;
|
|
||||||
// Camera stays alive until all threads have joined. start()/destruction
|
|
||||||
// are owner-thread operations; requestStop() only interrupts existing work.
|
|
||||||
{std::lock_guard<std::mutex> lock(stateMutex);if(camera)camera->requestStop();}
|
|
||||||
captures.close(true);detections.close(true);
|
|
||||||
}
|
|
||||||
void fail() noexcept {
|
|
||||||
{std::lock_guard<std::mutex> lock(stateMutex);if(!failure)failure=std::current_exception();}
|
|
||||||
stop();
|
|
||||||
}
|
|
||||||
void rethrowFailure() const {
|
|
||||||
std::lock_guard<std::mutex> lock(stateMutex);if(failure)std::rethrow_exception(failure);
|
|
||||||
}
|
|
||||||
void start() {
|
|
||||||
{std::lock_guard<std::mutex> lock(stateMutex);if(started||stopped)throw std::logic_error("CameraPipeline can only be started once");started=true;}
|
|
||||||
try {
|
|
||||||
detector=std::make_unique<Detector>(config.engines/"yolo11s.engine",logger,config.detector_confidence,config.max_people);
|
|
||||||
pose=std::make_unique<fangjia::RtmwEngine>(config.engines/"rtmw_l_384.engine",logger,config.camera.width,config.camera.height,config.max_people);
|
|
||||||
DoorOptions options;options.constant=config.door_mask;options.warning=config.warning_relative;options.release=config.release_relative;
|
|
||||||
risk=std::make_unique<DoorRisk>(options,cv::Size(config.camera.width,config.camera.height));risk->initialize();
|
|
||||||
if(risk->mask().empty()||cv::countNonZero(risk->mask())==0)throw std::runtime_error("Door mask has no usable region");
|
|
||||||
auto pixels=std::make_shared<BinaryMask>();pixels->width=config.camera.width;pixels->height=config.camera.height;pixels->stride=config.camera.width;
|
|
||||||
auto const& mask=risk->mask();pixels->pixels.resize(std::size_t(mask.cols)*mask.rows);
|
|
||||||
for(int y=0;y<mask.rows;++y)std::memcpy(pixels->pixels.data()+std::size_t(y)*mask.cols,mask.ptr(y),mask.cols);
|
|
||||||
doorPixels=std::move(pixels);
|
|
||||||
Stream warmup;
|
|
||||||
pose->warmup(warmup.value);
|
|
||||||
detector->infer(cv::Mat(config.camera.height,config.camera.width,CV_8UC3,cv::Scalar()),warmup.value);
|
|
||||||
{std::lock_guard<std::mutex> lock(stateMutex);camera=std::make_unique<Camera>(config.camera);if(stopped)camera->requestStop();}
|
|
||||||
active=!stopped.load();
|
|
||||||
captureThread=std::thread([this]{try{captureLoop();}catch(...){fail();}});
|
|
||||||
detectorThread=std::thread([this]{try{detectorLoop();}catch(...){fail();}});
|
|
||||||
inferenceThread=std::thread([this]{try{inferenceLoop();}catch(...){fail();}});
|
|
||||||
}catch(...){stop();joinWorkers();throw;}
|
|
||||||
}
|
|
||||||
void captureLoop() {
|
|
||||||
while(!stopped) {
|
|
||||||
auto frame=camera->read();if(!frame)break;
|
|
||||||
gaps+=frame->metadata.skipped_driver_frames;
|
|
||||||
errorFrames+=frame->metadata.discarded_error_frames;
|
|
||||||
if(!captures.push(std::move(*frame)))break;
|
|
||||||
++captured;
|
|
||||||
}
|
|
||||||
captures.close();
|
|
||||||
}
|
|
||||||
void detectorLoop() {
|
|
||||||
Stream stream;
|
|
||||||
while(!stopped) {
|
|
||||||
auto frame=captures.pop();if(!frame)break;
|
|
||||||
auto startedAt=monotonicNs();auto boxes=detector->infer(imageView(*frame->image),stream.value);
|
|
||||||
if(!detections.push({std::move(*frame),std::move(boxes),elapsedMs(startedAt)}))break;
|
|
||||||
}
|
|
||||||
detections.close();
|
|
||||||
}
|
|
||||||
void inferenceLoop() {
|
|
||||||
Stream stream;fangjia::BoxTracker boxesTracker;fangjia::PoseFusionTracker fusion;
|
|
||||||
while(!stopped) {
|
|
||||||
auto detected=detections.pop();if(!detected)break;
|
|
||||||
if(detected->frame.metadata.frame_index>static_cast<std::uint64_t>(std::numeric_limits<int>::max()))throw std::runtime_error("Frame index limit reached; restart pipeline");
|
|
||||||
auto index=static_cast<int>(detected->frame.metadata.frame_index);
|
|
||||||
auto& boxes=detected->boxes;
|
|
||||||
auto frame=imageView(*detected->frame.image);
|
|
||||||
boxesTracker.update(boxes,index,frame.cols,frame.rows);
|
|
||||||
auto poseStart=monotonicNs();
|
|
||||||
auto raw=pose->infer(frame,boxes,stream.value);auto isolated=raw;
|
|
||||||
for(std::size_t p=0;p<boxes.size();++p) {
|
|
||||||
auto const& box=boxes[p];double area=std::max(double(box.x2-box.x1)*(box.y2-box.y1),1.0);
|
|
||||||
bool needsIsolation=false;
|
|
||||||
for(std::size_t q=0;q<boxes.size();++q)if(q!=p&&std::max(double(boxes[q].x2-boxes[q].x1)*(boxes[q].y2-boxes[q].y1),1.0)<0.5*area)needsIsolation=true;
|
|
||||||
if(needsIsolation){auto masked=maskOtherPeople(frame,boxes,p);isolated[p]=pose->infer(masked,{box},stream.value).at(0);}
|
|
||||||
}
|
|
||||||
auto poseElapsed=elapsedMs(poseStart);auto fusionStart=monotonicNs();
|
|
||||||
auto fused=fusion.update(raw,isolated,index);
|
|
||||||
auto record=makeRecord(index,raw,isolated,fused,false,false);
|
|
||||||
auto riskData=risk->evaluate(index,record);
|
|
||||||
auto result=std::make_shared<FrameResult>(makeFrameResult(record,riskData,detected->frame.metadata));
|
|
||||||
attachPoses(*result,fused);
|
|
||||||
result->image=std::move(detected->frame.image);result->door_mask_pixels=doorPixels;
|
|
||||||
result->door_mask_calibration_verified=config.door_mask_calibration_verified;
|
|
||||||
result->timing={detected->elapsed_ms,poseElapsed,elapsedMs(fusionStart),elapsedMs(result->capture.capture_monotonic_ns)};
|
|
||||||
FrameResultPtr immutable=result;
|
|
||||||
results.publish(std::move(immutable));
|
|
||||||
{std::lock_guard<std::mutex> lock(stateMutex);totalLatency+=result->timing.capture_to_result_ms;++published;}
|
|
||||||
}
|
|
||||||
active=false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
CameraPipeline::CameraPipeline(PipelineConfig config):impl_(std::make_unique<Impl>(std::move(config))){}
|
|
||||||
CameraPipeline::~CameraPipeline()=default;
|
|
||||||
void CameraPipeline::start(){impl_->start();}
|
|
||||||
FrameResultPtr CameraPipeline::latest()const{return impl_->results.load();}
|
|
||||||
bool CameraPipeline::running()const noexcept{return impl_->active.load();}
|
|
||||||
void CameraPipeline::requestStop()noexcept{impl_->stop();}
|
|
||||||
void CameraPipeline::join(){impl_->joinWorkers();{std::lock_guard<std::mutex> lock(impl_->stateMutex);impl_->camera.reset();}impl_->rethrowFailure();}
|
|
||||||
PipelineStats CameraPipeline::stats()const {
|
|
||||||
std::lock_guard<std::mutex> lock(impl_->stateMutex);auto count=impl_->published.load();
|
|
||||||
return {impl_->captured.load(),count,impl_->gaps.load(),impl_->errorFrames.load(),count?impl_->totalLatency/count:0};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
#include "config.hpp"
|
|
||||||
#include "door_risk.hpp"
|
|
||||||
#include <fstream>
|
|
||||||
#include <stdexcept>
|
|
||||||
|
|
||||||
namespace cms {
|
|
||||||
PipelineConfig loadConfig(std::filesystem::path const& path) {
|
|
||||||
std::ifstream file(path);
|
|
||||||
if(!file)throw std::runtime_error("Cannot read configuration: "+path.string());
|
|
||||||
auto j=Json::parse(file);PipelineConfig config;
|
|
||||||
auto const& camera=j.at("camera");
|
|
||||||
config.camera.device=camera.at("device");config.camera.width=camera.at("width");
|
|
||||||
config.camera.height=camera.at("height");config.camera.fps=camera.at("fps");
|
|
||||||
config.camera.buffers=camera.at("buffers");config.camera.timeout_ms=camera.at("timeout_ms");
|
|
||||||
auto resolve=[&](std::string const& value){auto p=std::filesystem::path(value);return std::filesystem::absolute(p.is_absolute()?p:path.parent_path()/p);};
|
|
||||||
auto const& inference=j.at("inference");
|
|
||||||
config.engines=resolve(inference.at("engine_directory"));config.max_people=inference.at("max_people");
|
|
||||||
config.detector_confidence=inference.at("detector_confidence");config.queue_capacity=inference.at("queue_capacity");
|
|
||||||
auto const& door=j.at("door");config.door_mask=resolve(door.at("mask"));
|
|
||||||
config.warning_relative=door.at("warning_relative");config.release_relative=door.at("release_relative");
|
|
||||||
config.door_mask_calibration_verified=door.at("calibration_verified");
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
#include "door_risk.hpp"
|
|
||||||
#include <opencv2/imgproc.hpp>
|
|
||||||
#include <opencv2/imgcodecs.hpp>
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cmath>
|
|
||||||
#include <fstream>
|
|
||||||
#include <regex>
|
|
||||||
#include <stdexcept>
|
|
||||||
namespace cms {
|
|
||||||
double rounded(double v, int d) { double s=std::pow(10.0,d); return std::nearbyint(v*s)/s; }
|
|
||||||
static std::string lower(std::string s) { for(auto& c:s) if(static_cast<unsigned char>(c)<128) c=static_cast<char>(std::tolower(c)); return s; }
|
|
||||||
static bool raster(std::filesystem::path const& p) { auto e=lower(p.extension().string()); return e==".png"||e==".bmp"||e==".tif"||e==".tiff"; }
|
|
||||||
DoorRisk::DoorRisk(DoorOptions o, cv::Size size):options_(std::move(o)),size_(size) {
|
|
||||||
if(options_.constant.empty()==options_.directory.empty()) throw std::runtime_error("Provide exactly one constant mask or door-mask directory");
|
|
||||||
if(options_.warning<0 || options_.release<=options_.warning) throw std::runtime_error("Risk thresholds require 0 <= warning < release");
|
|
||||||
format_=options_.format;
|
|
||||||
if(!options_.constant.empty()) {
|
|
||||||
if(!std::filesystem::is_regular_file(options_.constant)) throw std::runtime_error("Missing mask: "+options_.constant.string());
|
|
||||||
if(format_=="auto") format_=lower(options_.constant.extension().string())==".json"?"xanylabeling":"raster";
|
|
||||||
} else {
|
|
||||||
std::vector<std::filesystem::path> files;
|
|
||||||
for(auto const& e:std::filesystem::recursive_directory_iterator(options_.directory)) if(e.is_regular_file()) files.push_back(e.path());
|
|
||||||
std::sort(files.begin(),files.end());
|
|
||||||
if(format_=="auto") format_=std::any_of(files.begin(),files.end(),[](auto const& p){return p.extension()==".json";})?"xanylabeling":"raster";
|
|
||||||
std::regex digits("[0-9]+");
|
|
||||||
for(auto const& p:files) {
|
|
||||||
if(format_=="xanylabeling"?p.extension()!=".json":!raster(p)) continue;
|
|
||||||
int index=-1; std::string stem=p.stem().string();
|
|
||||||
for(std::sregex_iterator i(stem.begin(),stem.end(),digits),end;i!=end;++i) index=std::stoi(i->str());
|
|
||||||
if(index<0 && format_=="xanylabeling") { std::ifstream in(p); auto j=Json::parse(in); if(j.contains("frame_index")) index=j["frame_index"].get<int>(); else if(j.contains("frameIndex")) index=j["frameIndex"].get<int>(); }
|
|
||||||
if(index<0) continue;
|
|
||||||
if(!paths_.emplace(index,p).second) throw std::runtime_error("Duplicate mask frame: "+std::to_string(index));
|
|
||||||
}
|
|
||||||
if(paths_.empty()) throw std::runtime_error("No indexed door masks in "+options_.directory.string());
|
|
||||||
}
|
|
||||||
if(format_!="raster" && format_!="xanylabeling") throw std::runtime_error("Unknown door mask format");
|
|
||||||
}
|
|
||||||
cv::Mat DoorRisk::rasterize(std::filesystem::path const& path) const {
|
|
||||||
cv::Mat mask(size_,CV_8U,cv::Scalar(0));
|
|
||||||
if(format_=="raster") {
|
|
||||||
auto img=cv::imread(path.string(),cv::IMREAD_UNCHANGED);
|
|
||||||
if(img.empty()) throw std::runtime_error("Cannot read mask: "+path.string());
|
|
||||||
if(img.channels()>1) { std::vector<cv::Mat> ch; cv::split(img,ch); img=ch[0]; for(size_t i=1;i<std::min(ch.size(),size_t(3));++i) cv::max(img,ch[i],img); }
|
|
||||||
if(img.size()!=size_) cv::resize(img,img,size_,0,0,cv::INTER_NEAREST);
|
|
||||||
return img>0;
|
|
||||||
}
|
|
||||||
std::ifstream in(path); auto j=Json::parse(in);
|
|
||||||
double w=j.contains("imageWidth")&&!j["imageWidth"].is_null()?j["imageWidth"].get<double>():size_.width;
|
|
||||||
double h=j.contains("imageHeight")&&!j["imageHeight"].is_null()?j["imageHeight"].get<double>():size_.height;
|
|
||||||
if(w==0) w=size_.width;
|
|
||||||
if(h==0) h=size_.height;
|
|
||||||
float sx=static_cast<float>(size_.width/std::max(w,1.0)),sy=static_cast<float>(size_.height/std::max(h,1.0));
|
|
||||||
for(auto const& s:j.value("shapes",Json::array())) {
|
|
||||||
if(!options_.labels.count(lower(s.value("label",std::string())))) continue;
|
|
||||||
std::vector<cv::Point> points;
|
|
||||||
for(auto const& p:s.value("points",Json::array())) points.emplace_back(static_cast<int>(std::nearbyint(p[0].get<float>()*sx)),static_cast<int>(std::nearbyint(p[1].get<float>()*sy)));
|
|
||||||
auto type=lower(s.value("shape_type",std::string("polygon")));
|
|
||||||
if(points.size()<2) continue;
|
|
||||||
if(type=="rectangle") cv::rectangle(mask,points[0],points[1],255,-1);
|
|
||||||
else if(type=="circle") cv::circle(mask,points[0],static_cast<int>(std::nearbyint(cv::norm(points[1]-points[0]))),255,-1);
|
|
||||||
else if(points.size()>=3) cv::fillPoly(mask,std::vector<std::vector<cv::Point>>{points},255);
|
|
||||||
}
|
|
||||||
return mask;
|
|
||||||
}
|
|
||||||
void DoorRisk::load(int frame) {
|
|
||||||
if(cached_) return;
|
|
||||||
loadedPath_.clear(); mask_.release(); distance_.release();
|
|
||||||
if(!options_.constant.empty()) loadedPath_=options_.constant;
|
|
||||||
else { auto it=paths_.find(frame+options_.offset); if(it!=paths_.end()) loadedPath_=it->second; }
|
|
||||||
if(loadedPath_.empty()) {
|
|
||||||
status_="missing";
|
|
||||||
if(options_.require) throw std::runtime_error("No door mask for frame "+std::to_string(frame));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mask_=rasterize(loadedPath_);
|
|
||||||
status_=cv::countNonZero(mask_)?"loaded":"empty";
|
|
||||||
if(status_=="loaded") cv::distanceTransform(mask_==0,distance_,cv::DIST_L2,cv::DIST_MASK_PRECISE);
|
|
||||||
cached_=!options_.constant.empty();
|
|
||||||
}
|
|
||||||
Json DoorRisk::evaluate(int frame, Json& record) {
|
|
||||||
load(frame);
|
|
||||||
Json regions=Json::array(),fallbacks=Json::array(),nearest=nullptr;
|
|
||||||
std::set<int> withHands;
|
|
||||||
auto measure=[&](cv::Mat const& region, std::string const& source,int track,std::string const& side,std::string const& state) {
|
|
||||||
Json m={{"status","door_mask_"+status_},{"risk_score",nullptr},{"risk_level","unavailable"},{"decision","EVIDENCE_UNAVAILABLE"},{"source",source},{"track_id",track},{"side",side}};
|
|
||||||
if(status_=="loaded") {
|
|
||||||
int area=cv::countNonZero(region); cv::Mat intersection; cv::bitwise_and(region,mask_,intersection); int overlap=cv::countNonZero(intersection);
|
|
||||||
double distance=0; cv::minMaxLoc(distance_,&distance,nullptr,nullptr,nullptr,region);
|
|
||||||
double scale=std::max(std::sqrt(double(area)),1.0),relative=distance/scale;
|
|
||||||
double score=overlap?1:std::clamp(1-relative/options_.release,0.0,1.0);
|
|
||||||
std::string level=overlap?"danger":relative<=options_.warning?"warning":relative<=options_.release?"watch":"clear";
|
|
||||||
std::string decision=overlap?"DANGER_INTERSECTION":relative<=options_.warning?"WARNING_NEAR":relative<=options_.release?"WATCH":"CLEAR";
|
|
||||||
m={{"status","calculated"},{"risk_score",rounded(score,5)},{"risk_level",level},{"decision",decision},{"source",source},{"track_id",track},{"side",side},{"state",state},{"region_area_px",area},{"door_overlap_px",overlap},{"region_overlap_ratio",rounded(double(overlap)/std::max(area,1),6)},{"min_distance_px",rounded(distance,3)},{"region_scale_px",rounded(scale,3)},{"distance_relative",rounded(relative,6)}};
|
|
||||||
if(nearest.is_null() || m["risk_score"].get<double>()>nearest["risk_score"].get<double>()) nearest=m;
|
|
||||||
}
|
|
||||||
regions.push_back(m); return m;
|
|
||||||
};
|
|
||||||
auto const ids=record.value("track_ids",Json::array());
|
|
||||||
auto trackFor=[&](int pos){return pos>=0&&pos<int(ids.size())?ids[pos].get<int>():pos+1;};
|
|
||||||
if(!record.contains("global_hands")) record["global_hands"]=Json::array();
|
|
||||||
for(auto& hand:record["global_hands"]) {
|
|
||||||
int pos=hand.value("person_id",0)-1; std::vector<cv::Point> points;
|
|
||||||
bool finite=true;
|
|
||||||
for(auto const& p:hand.value("keypoints",Json::array())) {
|
|
||||||
float x=p[0].get<float>(),y=p[1].get<float>();
|
|
||||||
if(!std::isfinite(x)||!std::isfinite(y)) {finite=false;break;}
|
|
||||||
points.emplace_back(static_cast<int>(std::nearbyint(x)),static_cast<int>(std::nearbyint(y)));
|
|
||||||
}
|
|
||||||
if(!finite||points.size()<3) continue;
|
|
||||||
std::vector<cv::Point> hull; cv::convexHull(points,hull);
|
|
||||||
cv::Mat region(size_,CV_8U,cv::Scalar(0)); cv::fillConvexPoly(region,hull,255);
|
|
||||||
if(!cv::countNonZero(region)) continue;
|
|
||||||
withHands.insert(pos);
|
|
||||||
hand["risk"]=measure(region,"RTMW_HAND_KEYPOINTS",trackFor(pos),hand.value("side",std::string("unknown")),hand.value("state",std::string("unknown")));
|
|
||||||
}
|
|
||||||
auto const boxes=record.value("boxes",Json::array());
|
|
||||||
for(int pos=0;pos<int(boxes.size());++pos) {
|
|
||||||
if(withHands.count(pos)) continue;
|
|
||||||
auto const& b=boxes[pos]; if(b.size()<4) continue;
|
|
||||||
int x1=std::clamp(int(std::nearbyint(b[0].get<double>())),0,size_.width-1),y1=std::clamp(int(std::nearbyint(b[1].get<double>())),0,size_.height-1);
|
|
||||||
int x2=std::clamp(int(std::nearbyint(b[2].get<double>())),0,size_.width-1),y2=std::clamp(int(std::nearbyint(b[3].get<double>())),0,size_.height-1);
|
|
||||||
if(x1>x2) std::swap(x1,x2);
|
|
||||||
if(y1>y2) std::swap(y1,y2);
|
|
||||||
if(x2<=x1||y2<=y1) continue;
|
|
||||||
cv::Mat region(size_,CV_8U,cv::Scalar(0)); cv::rectangle(region,{x1,y1},{x2,y2},255,-1);
|
|
||||||
Json fallback={{"track_id",trackFor(pos)},{"side","body"},{"state","rtmw_hand_unavailable"},{"body_zone_box",b}};
|
|
||||||
fallback["risk"]=measure(region,"YOLO_BODY_BOX_FALLBACK",trackFor(pos),"body","rtmw_hand_unavailable"); fallbacks.push_back(fallback);
|
|
||||||
}
|
|
||||||
record["body_fallbacks"]=fallbacks;
|
|
||||||
Json result={{"status",status_!="loaded"?"door_mask_"+status_:nearest.is_null()?"person_region_missing":"calculated"}};
|
|
||||||
for(auto const* key:{"risk_score","risk_level","decision","source","track_id","side"}) result[key]=nearest.is_null()?Json(nullptr):nearest[key];
|
|
||||||
if(nearest.is_null()) {result["risk_level"]="unavailable";result["decision"]="EVIDENCE_UNAVAILABLE";}
|
|
||||||
result["door_mask"]={{"status",status_},{"annotation_index",options_.constant.empty()?frame+options_.offset:frame},{"path",loadedPath_.empty()?Json(nullptr):Json(std::filesystem::absolute(loadedPath_).string())},{"format",format_},{"area_px",mask_.empty()?0:cv::countNonZero(mask_)}};
|
|
||||||
result["warning_distance_relative"]=options_.warning; result["release_distance_relative"]=options_.release; result["regions"]=regions;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
#include "camera_pipeline.hpp"
|
|
||||||
#include "config.hpp"
|
|
||||||
#include "stop_signals.hpp"
|
|
||||||
#include "visualizer.hpp"
|
|
||||||
#include <functional>
|
|
||||||
#include <iostream>
|
|
||||||
#include <map>
|
|
||||||
|
|
||||||
int main(int argc,char** argv)try {
|
|
||||||
auto root=std::filesystem::canonical("/proc/self/exe").parent_path().parent_path();
|
|
||||||
auto configPath=root/"config/camera.json";
|
|
||||||
bool visualize=false;
|
|
||||||
std::map<std::string,std::function<void(int&)>> arguments{
|
|
||||||
{"--visualize",[&](int&){visualize=true;}},
|
|
||||||
{"--config",[&](int&i){if(++i>=argc)throw std::invalid_argument("--config needs a file");configPath=argv[i];}},
|
|
||||||
{"--help",[](int&){std::cout<<"camera_risk [--config FILE] [--visualize]\nContinuous camera inference; latest FrameResult stays in memory. Stop with Ctrl+C or SIGTERM.\n";std::exit(0);}}
|
|
||||||
};
|
|
||||||
for(int i=1;i<argc;++i){auto handler=arguments.find(argv[i]);if(handler==arguments.end())throw std::invalid_argument("Unknown option: "+std::string(argv[i]));handler->second(i);}
|
|
||||||
|
|
||||||
cms::CameraPipeline pipeline(cms::loadConfig(configPath));
|
|
||||||
cms::StopSignals signals(pipeline);
|
|
||||||
pipeline.start();
|
|
||||||
/*
|
|
||||||
* 下游使用示例:在下游自己的线程/调用位置读取最新结果。
|
|
||||||
* 首帧尚未完成时 result 为空;读取不会影响取图和推理。
|
|
||||||
*
|
|
||||||
* auto result = pipeline.latest();
|
|
||||||
* if (result) {
|
|
||||||
* const auto& risk = result->risk;
|
|
||||||
* const auto& people = result->persons;
|
|
||||||
* // result->frame_index / track_ids / boxes
|
|
||||||
* // result->image / door_mask_pixels / capture / timing
|
|
||||||
* // people[i].selected / smoothed / temporal:各 133 个关键点
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
// Optional observer. Closing/failing the window never requests inference stop.
|
|
||||||
if(visualize) {
|
|
||||||
try {cms::runLocalVisualization(pipeline);}
|
|
||||||
catch(std::exception const& e){std::cerr<<"Visualization unavailable; inference continues: "<<e.what()<<std::endl;}
|
|
||||||
}
|
|
||||||
pipeline.join();
|
|
||||||
return 0;
|
|
||||||
}catch(std::exception const& e){std::cerr<<"Camera pipeline failed: "<<e.what()<<std::endl;return 1;}
|
|
||||||
@@ -1,875 +0,0 @@
|
|||||||
#include "pose_engine.hpp"
|
|
||||||
#include "common.hpp"
|
|
||||||
#include "gpu_preprocess.hpp"
|
|
||||||
|
|
||||||
#include <opencv2/imgproc.hpp>
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <chrono>
|
|
||||||
#include <cmath>
|
|
||||||
#include <numeric>
|
|
||||||
#include <set>
|
|
||||||
#include <tuple>
|
|
||||||
|
|
||||||
namespace fangjia {
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
constexpr int kInputWidth = 288;
|
|
||||||
constexpr int kInputHeight = 384;
|
|
||||||
constexpr int kKeypoints = 133;
|
|
||||||
constexpr int kSimccX = 576;
|
|
||||||
constexpr int kSimccY = 768;
|
|
||||||
|
|
||||||
__global__ void poseNormalizeKernel(
|
|
||||||
float* output, std::uint8_t const* input, int people, int inputStep) {
|
|
||||||
int index = blockIdx.x * blockDim.x + threadIdx.x;
|
|
||||||
int pixels = kInputWidth * kInputHeight;
|
|
||||||
int total = people * pixels;
|
|
||||||
if (index >= total) return;
|
|
||||||
int person = index / pixels;
|
|
||||||
int pixel = index - person * pixels;
|
|
||||||
int y = pixel / kInputWidth;
|
|
||||||
int x = pixel - y * kInputWidth;
|
|
||||||
auto const* value = input + person * inputStep * kInputHeight + y * inputStep + x * 3;
|
|
||||||
float* destination = output + static_cast<std::size_t>(person) * 3 * pixels;
|
|
||||||
destination[pixel] = static_cast<float>((static_cast<double>(value[0]) - 123.675) / 58.395);
|
|
||||||
destination[pixels + pixel] = static_cast<float>((static_cast<double>(value[1]) - 116.28) / 57.12);
|
|
||||||
destination[2 * pixels + pixel] = static_cast<float>((static_cast<double>(value[2]) - 103.53) / 57.375);
|
|
||||||
}
|
|
||||||
|
|
||||||
float iou(Box const& a, Box const& b) {
|
|
||||||
float x1 = std::max(a.x1, b.x1);
|
|
||||||
float y1 = std::max(a.y1, b.y1);
|
|
||||||
float x2 = std::min(a.x2, b.x2);
|
|
||||||
float y2 = std::min(a.y2, b.y2);
|
|
||||||
float intersection = std::max(x2 - x1, 0.0F) * std::max(y2 - y1, 0.0F);
|
|
||||||
float areaA = std::max(a.x2 - a.x1, 0.0F) * std::max(a.y2 - a.y1, 0.0F);
|
|
||||||
float areaB = std::max(b.x2 - b.x1, 0.0F) * std::max(b.y2 - b.y1, 0.0F);
|
|
||||||
return intersection / std::max(areaA + areaB - intersection, 1.0F);
|
|
||||||
}
|
|
||||||
|
|
||||||
float distance(float x1, float y1, float x2, float y2) {
|
|
||||||
return std::hypot(x1 - x2, y1 - y2);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
std::vector<int> BoxTracker::update(
|
|
||||||
std::vector<Box>& boxes, int frameIndex, int width, int height) {
|
|
||||||
// Tracks older than Python's max_age can never be matched again.
|
|
||||||
// Reclaim them to bound memory during continuous camera operation.
|
|
||||||
for(auto it=tracks_.begin();it!=tracks_.end();) {
|
|
||||||
if(frameIndex-it->second.lastFrame>30)it=tracks_.erase(it);
|
|
||||||
else ++it;
|
|
||||||
}
|
|
||||||
struct Pair { float score; int detection; int track; };
|
|
||||||
std::vector<Pair> pairs;
|
|
||||||
float imageDiagonal = std::hypot(static_cast<float>(width), static_cast<float>(height));
|
|
||||||
for (int d = 0; d < static_cast<int>(boxes.size()); ++d) {
|
|
||||||
float cx = 0.5F * (boxes[d].x1 + boxes[d].x2);
|
|
||||||
float cy = 0.5F * (boxes[d].y1 + boxes[d].y2);
|
|
||||||
for (auto const& [id, track] : tracks_) {
|
|
||||||
if (frameIndex - track.lastFrame > 30) continue;
|
|
||||||
float pcx = 0.5F * (track.box.x1 + track.box.x2);
|
|
||||||
float pcy = 0.5F * (track.box.y1 + track.box.y2);
|
|
||||||
float overlap = iou(boxes[d], track.box);
|
|
||||||
float centerDistance = distance(cx, cy, pcx, pcy) / imageDiagonal;
|
|
||||||
if (overlap >= 0.05F || centerDistance <= 0.18F) {
|
|
||||||
pairs.push_back({2.0F * overlap + std::max(0.0F, 1.0F - centerDistance / 0.18F), d, id});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::sort(pairs.begin(), pairs.end(), [](Pair const& a, Pair const& b) {
|
|
||||||
if (a.score != b.score) return a.score > b.score;
|
|
||||||
if (a.detection != b.detection) return a.detection > b.detection;
|
|
||||||
return a.track > b.track;
|
|
||||||
});
|
|
||||||
std::vector<int> result(boxes.size(), 0);
|
|
||||||
std::unordered_map<int, bool> assigned;
|
|
||||||
for (auto const& pair : pairs) {
|
|
||||||
if (result[pair.detection] || assigned[pair.track]) continue;
|
|
||||||
result[pair.detection] = pair.track;
|
|
||||||
assigned[pair.track] = true;
|
|
||||||
}
|
|
||||||
for (int d = 0; d < static_cast<int>(boxes.size()); ++d) {
|
|
||||||
if (!result[d]) result[d] = nextId_++;
|
|
||||||
boxes[d].trackId = result[d];
|
|
||||||
tracks_[result[d]] = {boxes[d], frameIndex};
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
RtmwEngine::RtmwEngine(
|
|
||||||
std::filesystem::path const& enginePath, TrtLogger& logger,
|
|
||||||
int maxSourceWidth, int maxSourceHeight, int maxPeople)
|
|
||||||
: engine_(enginePath, logger), maxSourceWidth_(maxSourceWidth),
|
|
||||||
maxSourceHeight_(maxSourceHeight), maxPeople_(maxPeople),
|
|
||||||
centersHost_(maxPeople * 2), scalesHost_(maxPeople * 2),
|
|
||||||
simccXHost_(static_cast<std::size_t>(maxPeople) * kKeypoints * kSimccX),
|
|
||||||
simccYHost_(static_cast<std::size_t>(maxPeople) * kKeypoints * kSimccY) {
|
|
||||||
engine_.setInputShape("input", nvinfer1::Dims4{1,3,kInputHeight,kInputWidth});
|
|
||||||
engine_.prepare();
|
|
||||||
auto xs=engine_.shape("simcc_x"),ys=engine_.shape("simcc_y");
|
|
||||||
if(xs.nbDims!=3||ys.nbDims!=3||xs.d[0]!=1||ys.d[0]!=1||xs.d[1]!=kKeypoints||ys.d[1]!=kKeypoints||xs.d[2]!=kSimccX||ys.d[2]!=kSimccY)
|
|
||||||
throw std::runtime_error("RTMW engine I/O shape does not match the 133-point model");
|
|
||||||
for(auto name:{"input","simcc_x","simcc_y"})if(engine_.dataType(name)!=nvinfer1::DataType::kFLOAT)
|
|
||||||
throw std::runtime_error("RTMW requires float32 I/O bindings");
|
|
||||||
std::size_t const values = static_cast<std::size_t>(maxPeople) * 3 * kInputWidth * kInputHeight;
|
|
||||||
CUDA_CHECK(cudaMalloc(
|
|
||||||
&cropsDevice_, static_cast<std::size_t>(maxPeople_) * kInputWidth * kInputHeight * 3));
|
|
||||||
CUDA_CHECK(cudaMalloc(&networkDevice_, values * sizeof(float)));
|
|
||||||
CUDA_CHECK(cudaMalloc(&sourceDevice_, static_cast<size_t>(maxSourceWidth_)*maxSourceHeight_*3));
|
|
||||||
}
|
|
||||||
|
|
||||||
RtmwEngine::~RtmwEngine() {
|
|
||||||
if (sourceDevice_) cudaFree(sourceDevice_);
|
|
||||||
if (cropsDevice_) cudaFree(cropsDevice_);
|
|
||||||
if (networkDevice_) cudaFree(networkDevice_);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RtmwEngine::warmup(cudaStream_t stream) {
|
|
||||||
std::vector<Box> boxes{{0, 0, static_cast<float>(maxSourceWidth_), static_cast<float>(maxSourceHeight_), 1, 1}};
|
|
||||||
cv::Mat black(maxSourceHeight_, maxSourceWidth_, CV_8UC3, cv::Scalar(0, 0, 0));
|
|
||||||
for (int i = 0; i < 2; ++i) infer(black, boxes, stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<PersonPose> RtmwEngine::infer(
|
|
||||||
cv::Mat const& frame, std::vector<Box> const& boxes, cudaStream_t stream,
|
|
||||||
float* preprocessMs, float* inferenceMs, float* postprocessMs) {
|
|
||||||
int batch = std::min(static_cast<int>(boxes.size()), maxPeople_);
|
|
||||||
if (batch <= 0) return {};
|
|
||||||
auto const preprocessStarted = std::chrono::steady_clock::now();
|
|
||||||
int const cropStep = kInputWidth * 3;
|
|
||||||
int const pixels = kInputWidth * kInputHeight;
|
|
||||||
if (frame.cols>maxSourceWidth_ || frame.rows>maxSourceHeight_) throw std::runtime_error("Pose input exceeds allocation");
|
|
||||||
auto npp = cms::nppContext(stream);
|
|
||||||
CUDA_CHECK(cudaMemcpy2DAsync(sourceDevice_, frame.cols*3, frame.data, frame.step, frame.cols*3, frame.rows, cudaMemcpyHostToDevice, stream));
|
|
||||||
for (int i = 0; i < batch; ++i) {
|
|
||||||
auto const& box = boxes[i];
|
|
||||||
double width = (static_cast<double>(box.x2) - static_cast<double>(box.x1)) * 1.25;
|
|
||||||
double height = (static_cast<double>(box.y2) - static_cast<double>(box.y1)) * 1.25;
|
|
||||||
constexpr double aspect = static_cast<double>(kInputWidth) / kInputHeight;
|
|
||||||
if (width > height * aspect) height = width / aspect;
|
|
||||||
else width = height * aspect;
|
|
||||||
centersHost_[i * 2] = 0.5 * (static_cast<double>(box.x1) + static_cast<double>(box.x2));
|
|
||||||
centersHost_[i * 2 + 1] = 0.5 * (static_cast<double>(box.y1) + static_cast<double>(box.y2));
|
|
||||||
scalesHost_[i * 2] = width;
|
|
||||||
scalesHost_[i * 2 + 1] = height;
|
|
||||||
|
|
||||||
cv::Point2f source[3]{
|
|
||||||
{static_cast<float>(centersHost_[i * 2]), static_cast<float>(centersHost_[i * 2 + 1])},
|
|
||||||
{static_cast<float>(centersHost_[i * 2]), static_cast<float>(centersHost_[i * 2 + 1] - width * 0.5)},
|
|
||||||
{static_cast<float>(centersHost_[i * 2] - width * 0.5), static_cast<float>(centersHost_[i * 2 + 1] - width * 0.5)}};
|
|
||||||
cv::Point2f destination[3]{
|
|
||||||
{kInputWidth * 0.5F, kInputHeight * 0.5F},
|
|
||||||
{kInputWidth * 0.5F, kInputHeight * 0.5F - kInputWidth * 0.5F},
|
|
||||||
{0.0F, kInputHeight * 0.5F - kInputWidth * 0.5F}};
|
|
||||||
cv::Mat affine = cv::getAffineTransform(source, destination);
|
|
||||||
auto* cropDevice=cropsDevice_+static_cast<size_t>(i)*cropStep*kInputHeight;
|
|
||||||
Npp8u fill[3]={0,0,0};
|
|
||||||
cms::nppCheck(nppiSet_8u_C3R_Ctx(fill,cropDevice,cropStep,{kInputWidth,kInputHeight},npp));
|
|
||||||
double coeff[2][3];
|
|
||||||
for(int row=0;row<2;++row) for(int col=0;col<3;++col) coeff[row][col]=affine.at<double>(row,col);
|
|
||||||
cms::nppCheck(nppiWarpAffine_8u_C3R_Ctx(sourceDevice_,{frame.cols,frame.rows},frame.cols*3,{0,0,frame.cols,frame.rows},cropDevice,cropStep,{0,0,kInputWidth,kInputHeight},coeff,NPPI_INTER_LINEAR,npp));
|
|
||||||
}
|
|
||||||
int const totalPixels = batch * pixels;
|
|
||||||
poseNormalizeKernel<<<(totalPixels + 255) / 256, 256, 0, stream>>>(
|
|
||||||
networkDevice_, cropsDevice_, batch, cropStep);
|
|
||||||
CUDA_CHECK(cudaGetLastError());
|
|
||||||
CUDA_CHECK(cudaStreamSynchronize(stream));
|
|
||||||
if (preprocessMs) {
|
|
||||||
*preprocessMs = static_cast<float>(std::chrono::duration<double, std::milli>(
|
|
||||||
std::chrono::steady_clock::now() - preprocessStarted).count());
|
|
||||||
}
|
|
||||||
|
|
||||||
// RTMPose's Python API invokes the ONNX/TensorRT session once per person.
|
|
||||||
// Preserve that execution contract instead of batching people together.
|
|
||||||
engine_.setInputShape("input", nvinfer1::Dims4{1, 3, kInputHeight, kInputWidth});
|
|
||||||
engine_.prepare();
|
|
||||||
CudaEventTimer timer;
|
|
||||||
float inferTime = 0.0F;
|
|
||||||
std::size_t const inputValues = static_cast<std::size_t>(3) * pixels;
|
|
||||||
std::size_t const xValues = static_cast<std::size_t>(kKeypoints) * kSimccX;
|
|
||||||
std::size_t const yValues = static_cast<std::size_t>(kKeypoints) * kSimccY;
|
|
||||||
for (int person = 0; person < batch; ++person) {
|
|
||||||
CUDA_CHECK(cudaMemcpyAsync(
|
|
||||||
engine_.device("input"), networkDevice_ + static_cast<std::size_t>(person) * inputValues,
|
|
||||||
inputValues * sizeof(float), cudaMemcpyDeviceToDevice, stream));
|
|
||||||
timer.start(stream);
|
|
||||||
if (!engine_.enqueue(stream)) throw std::runtime_error("RTMW enqueue failed");
|
|
||||||
inferTime += timer.stop(stream);
|
|
||||||
CUDA_CHECK(cudaMemcpyAsync(
|
|
||||||
simccXHost_.data() + static_cast<std::size_t>(person) * xValues,
|
|
||||||
engine_.device("simcc_x"), xValues * sizeof(float), cudaMemcpyDeviceToHost, stream));
|
|
||||||
CUDA_CHECK(cudaMemcpyAsync(
|
|
||||||
simccYHost_.data() + static_cast<std::size_t>(person) * yValues,
|
|
||||||
engine_.device("simcc_y"), yValues * sizeof(float), cudaMemcpyDeviceToHost, stream));
|
|
||||||
}
|
|
||||||
if (inferenceMs) *inferenceMs = inferTime;
|
|
||||||
|
|
||||||
auto const postprocessStarted = std::chrono::steady_clock::now();
|
|
||||||
CUDA_CHECK(cudaStreamSynchronize(stream));
|
|
||||||
std::vector<PersonPose> result(batch);
|
|
||||||
for (int person = 0; person < batch; ++person) {
|
|
||||||
result[person].box = boxes[person];
|
|
||||||
for (int keypoint = 0; keypoint < kKeypoints; ++keypoint) {
|
|
||||||
auto xBegin = simccXHost_.begin() + (person * kKeypoints + keypoint) * kSimccX;
|
|
||||||
auto yBegin = simccYHost_.begin() + (person * kKeypoints + keypoint) * kSimccY;
|
|
||||||
auto xIt = std::max_element(xBegin, xBegin + kSimccX);
|
|
||||||
auto yIt = std::max_element(yBegin, yBegin + kSimccY);
|
|
||||||
float score = 0.5F * (*xIt + *yIt);
|
|
||||||
if(!std::isfinite(score))throw std::runtime_error("Non-finite RTMW output");
|
|
||||||
float localX = score > 0.0F ? static_cast<float>(xIt - xBegin) * 0.5F : -0.5F;
|
|
||||||
float localY = score > 0.0F ? static_cast<float>(yIt - yBegin) * 0.5F : -0.5F;
|
|
||||||
double scaleX = scalesHost_[person * 2];
|
|
||||||
double scaleY = scalesHost_[person * 2 + 1];
|
|
||||||
result[person].keypoints[keypoint] = {
|
|
||||||
static_cast<float>(static_cast<double>(localX) / kInputWidth * scaleX + centersHost_[person * 2] - scaleX * 0.5),
|
|
||||||
static_cast<float>(static_cast<double>(localY) / kInputHeight * scaleY + centersHost_[person * 2 + 1] - scaleY * 0.5),
|
|
||||||
score};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
float post = static_cast<float>(std::chrono::duration<double, std::milli>(
|
|
||||||
std::chrono::steady_clock::now() - postprocessStarted).count());
|
|
||||||
if (postprocessMs) *postprocessMs = post;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
PoseFrameResult PoseFusionTracker::update(
|
|
||||||
std::vector<PersonPose> const& rawPeople,
|
|
||||||
std::vector<PersonPose> const& isolatedPeople,
|
|
||||||
int frameIndex) {
|
|
||||||
for(auto it=states_.begin();it!=states_.end();) {
|
|
||||||
if(it->second.lastFrame>=0&&frameIndex-it->second.lastFrame>30)it=states_.erase(it);
|
|
||||||
else ++it;
|
|
||||||
}
|
|
||||||
struct SideMeta {
|
|
||||||
bool limbConsensus{};
|
|
||||||
bool handValid{};
|
|
||||||
bool distanceEligible{};
|
|
||||||
bool alarmEligible{};
|
|
||||||
double handScore{};
|
|
||||||
double linkToSelectedLimb{};
|
|
||||||
std::string handSource;
|
|
||||||
std::string handState;
|
|
||||||
};
|
|
||||||
struct FusedPerson {
|
|
||||||
PersonPose pose;
|
|
||||||
std::array<SideMeta, 2> meta;
|
|
||||||
};
|
|
||||||
struct Measurement {
|
|
||||||
std::array<Vec2, 21> hand{};
|
|
||||||
bool precise{};
|
|
||||||
bool provisional{};
|
|
||||||
double quality{};
|
|
||||||
std::string source{"model"};
|
|
||||||
};
|
|
||||||
auto vec = [](Keypoint const& point) { return Vec2{point.x, point.y}; };
|
|
||||||
auto add = [](Vec2 a, Vec2 b) { return Vec2{a.x + b.x, a.y + b.y}; };
|
|
||||||
auto sub = [](Vec2 a, Vec2 b) { return Vec2{a.x - b.x, a.y - b.y}; };
|
|
||||||
auto mul = [](Vec2 a, double value) { return Vec2{a.x * value, a.y * value}; };
|
|
||||||
auto norm = [](Vec2 a) { return std::hypot(a.x, a.y); };
|
|
||||||
auto median = [](std::vector<double> values) {
|
|
||||||
if (values.empty()) return 0.0;
|
|
||||||
std::sort(values.begin(), values.end());
|
|
||||||
auto middle = values.size() / 2;
|
|
||||||
return values.size() % 2 ? values[middle] : 0.5 * (values[middle - 1] + values[middle]);
|
|
||||||
};
|
|
||||||
auto percentile90 = [](std::vector<double> values) {
|
|
||||||
if (values.empty()) return 0.0;
|
|
||||||
std::sort(values.begin(), values.end());
|
|
||||||
double position = 0.9 * (values.size() - 1);
|
|
||||||
auto lower = static_cast<std::size_t>(position);
|
|
||||||
auto upper = std::min(lower + 1, values.size() - 1);
|
|
||||||
return values[lower] * (upper - position) + values[upper] * (position - lower);
|
|
||||||
};
|
|
||||||
auto boxDiagonal = [](Box const& box) {
|
|
||||||
return std::max(std::hypot(box.x2 - box.x1, box.y2 - box.y1), 1.0F);
|
|
||||||
};
|
|
||||||
auto normalized = [](Keypoint const& point, Box const& box) {
|
|
||||||
return Vec2{
|
|
||||||
(point.x - box.x1) / std::max(box.x2 - box.x1, 1.0F),
|
|
||||||
(point.y - box.y1) / std::max(box.y2 - box.y1, 1.0F)};
|
|
||||||
};
|
|
||||||
auto denormalized = [](Vec2 point, Box const& box) {
|
|
||||||
return Vec2{
|
|
||||||
point.x * std::max(box.x2 - box.x1, 1.0F) + box.x1,
|
|
||||||
point.y * std::max(box.y2 - box.y1, 1.0F) + box.y1};
|
|
||||||
};
|
|
||||||
auto solveTwoBoneElbow = [&](Vec2 shoulder, Vec2 wrist, double upperLength,
|
|
||||||
double forearmLength, Vec2 reference) {
|
|
||||||
Vec2 direction = sub(wrist, shoulder);
|
|
||||||
double length = norm(direction);
|
|
||||||
if (length < 1e-6) return reference;
|
|
||||||
Vec2 unit = mul(direction, 1.0 / length);
|
|
||||||
if (length >= upperLength + forearmLength) return add(shoulder, mul(unit, upperLength));
|
|
||||||
if (length <= std::abs(upperLength - forearmLength)) {
|
|
||||||
double sign = upperLength >= forearmLength ? 1.0 : -1.0;
|
|
||||||
return add(shoulder, mul(unit, upperLength * sign));
|
|
||||||
}
|
|
||||||
double along = (upperLength * upperLength - forearmLength * forearmLength + length * length) /
|
|
||||||
(2.0 * length);
|
|
||||||
double height = std::sqrt(std::max(upperLength * upperLength - along * along, 0.0));
|
|
||||||
Vec2 base = add(shoulder, mul(unit, along));
|
|
||||||
Vec2 perpendicular{-unit.y, unit.x};
|
|
||||||
Vec2 first = add(base, mul(perpendicular, height));
|
|
||||||
Vec2 second = sub(base, mul(perpendicular, height));
|
|
||||||
return norm(sub(first, reference)) <= norm(sub(second, reference)) ? first : second;
|
|
||||||
};
|
|
||||||
struct Candidate {
|
|
||||||
PersonPose const* pose{};
|
|
||||||
std::string source;
|
|
||||||
std::array<Vec2, 3> limb{};
|
|
||||||
std::array<Vec2, 21> hand{};
|
|
||||||
double limbScore{};
|
|
||||||
double handScore{};
|
|
||||||
double handSpan{};
|
|
||||||
double wristLink{};
|
|
||||||
bool limbValid{};
|
|
||||||
bool handValid{};
|
|
||||||
};
|
|
||||||
auto buildCandidate = [&](PersonPose const& pose, int side, std::string source) {
|
|
||||||
int shoulder = side == 0 ? 5 : 6;
|
|
||||||
int elbow = side == 0 ? 7 : 8;
|
|
||||||
int wrist = side == 0 ? 9 : 10;
|
|
||||||
int handStart = side == 0 ? 91 : 112;
|
|
||||||
Candidate candidate;
|
|
||||||
candidate.pose = &pose;
|
|
||||||
candidate.source = std::move(source);
|
|
||||||
candidate.limb = {normalized(pose.keypoints[shoulder], pose.box),
|
|
||||||
normalized(pose.keypoints[elbow], pose.box),
|
|
||||||
normalized(pose.keypoints[wrist], pose.box)};
|
|
||||||
double minX = pose.keypoints[handStart].x, maxX = minX;
|
|
||||||
double minY = pose.keypoints[handStart].y, maxY = minY;
|
|
||||||
for (int index = 0; index < 21; ++index) {
|
|
||||||
candidate.hand[index] = normalized(pose.keypoints[handStart + index], pose.box);
|
|
||||||
minX = std::min<double>(minX, pose.keypoints[handStart + index].x);
|
|
||||||
maxX = std::max<double>(maxX, pose.keypoints[handStart + index].x);
|
|
||||||
minY = std::min<double>(minY, pose.keypoints[handStart + index].y);
|
|
||||||
maxY = std::max<double>(maxY, pose.keypoints[handStart + index].y);
|
|
||||||
candidate.handScore += pose.keypoints[handStart + index].score;
|
|
||||||
}
|
|
||||||
candidate.handScore /= 21.0;
|
|
||||||
candidate.limbScore = (pose.keypoints[shoulder].score + pose.keypoints[elbow].score + pose.keypoints[wrist].score) / 3.0;
|
|
||||||
double upper = norm(sub(candidate.limb[0], candidate.limb[1]));
|
|
||||||
double forearm = norm(sub(candidate.limb[1], candidate.limb[2]));
|
|
||||||
candidate.handSpan = std::hypot(maxX - minX, maxY - minY) / boxDiagonal(pose.box);
|
|
||||||
candidate.wristLink = distance(
|
|
||||||
pose.keypoints[wrist].x, pose.keypoints[wrist].y,
|
|
||||||
pose.keypoints[handStart].x, pose.keypoints[handStart].y) / boxDiagonal(pose.box);
|
|
||||||
candidate.limbValid = upper >= 0.025 && upper <= 0.8 && forearm >= 0.025 && forearm <= 0.8;
|
|
||||||
candidate.handValid = candidate.handSpan >= 0.008 && candidate.handSpan <= 0.35 && candidate.wristLink <= 0.12;
|
|
||||||
return candidate;
|
|
||||||
};
|
|
||||||
auto robustDistance = [&](auto const& points, auto const& target, bool available) {
|
|
||||||
if (!available) return 0.0;
|
|
||||||
std::vector<double> distances;
|
|
||||||
for (std::size_t index = 0; index < points.size(); ++index) {
|
|
||||||
distances.push_back(norm(sub(points[index], target[index])));
|
|
||||||
}
|
|
||||||
return median(std::move(distances));
|
|
||||||
};
|
|
||||||
|
|
||||||
std::vector<FusedPerson> fused;
|
|
||||||
fused.reserve(rawPeople.size());
|
|
||||||
for (std::size_t personIndex = 0; personIndex < rawPeople.size(); ++personIndex) {
|
|
||||||
auto const& raw = rawPeople[personIndex];
|
|
||||||
auto const& isolated = personIndex < isolatedPeople.size() ? isolatedPeople[personIndex] : raw;
|
|
||||||
auto& state = states_[raw.box.trackId];
|
|
||||||
FusedPerson output;
|
|
||||||
output.pose = raw;
|
|
||||||
for (int side = 0; side < 2; ++side) {
|
|
||||||
int shoulder = side == 0 ? 5 : 6;
|
|
||||||
int elbow = side == 0 ? 7 : 8;
|
|
||||||
int wrist = side == 0 ? 9 : 10;
|
|
||||||
int handStart = side == 0 ? 91 : 112;
|
|
||||||
auto& track = state.fusion[side];
|
|
||||||
std::array<Candidate, 2> candidates{
|
|
||||||
buildCandidate(raw, side, "raw"), buildCandidate(isolated, side, "isolated")};
|
|
||||||
int limbChoice = 0;
|
|
||||||
double bestLimbScore = -1e30;
|
|
||||||
for (int choice = 0; choice < 2; ++choice) {
|
|
||||||
std::array<Vec2, 3> target{};
|
|
||||||
if (track.hasLimbPrevious) {
|
|
||||||
for (int i = 0; i < 3; ++i) target[i] = add(track.limbPrevious[i], track.hasLimbVelocity ? mul(track.limbVelocity[i], 0.65) : Vec2{});
|
|
||||||
}
|
|
||||||
double temporal = robustDistance(candidates[choice].limb, target, track.hasLimbPrevious);
|
|
||||||
double score = candidates[choice].limbScore + (candidates[choice].limbValid ? 1.2 : -5.0)
|
|
||||||
+ (track.limbSource == candidates[choice].source ? 0.35 : 0.0) - 7.0 * temporal;
|
|
||||||
if (score > bestLimbScore) { bestLimbScore = score; limbChoice = choice; }
|
|
||||||
}
|
|
||||||
int handChoice = 0;
|
|
||||||
double bestHandScore = -1e30;
|
|
||||||
double chosenTemporal = 0.0;
|
|
||||||
double chosenLink = 1.0;
|
|
||||||
for (int choice = 0; choice < 2; ++choice) {
|
|
||||||
std::array<Vec2, 21> target{};
|
|
||||||
if (track.hasHandPrevious) {
|
|
||||||
for (int i = 0; i < 21; ++i) target[i] = add(track.handPrevious[i], track.hasHandVelocity ? mul(track.handVelocity[i], 0.65) : Vec2{});
|
|
||||||
}
|
|
||||||
double temporal = robustDistance(candidates[choice].hand, target, track.hasHandPrevious);
|
|
||||||
auto const& limbPose = *candidates[limbChoice].pose;
|
|
||||||
double link = distance(
|
|
||||||
candidates[choice].pose->keypoints[handStart].x,
|
|
||||||
candidates[choice].pose->keypoints[handStart].y,
|
|
||||||
limbPose.keypoints[wrist].x, limbPose.keypoints[wrist].y) / boxDiagonal(raw.box);
|
|
||||||
double linkPenalty = 1.5 * std::min(std::max(link - 0.12, 0.0), 0.2);
|
|
||||||
double score = candidates[choice].handScore + (candidates[choice].handValid ? 3.0 : -7.0)
|
|
||||||
+ (track.handSource == candidates[choice].source ? 0.45 : 0.0)
|
|
||||||
+ (candidates[choice].source == candidates[limbChoice].source ? 0.15 : 0.0)
|
|
||||||
- linkPenalty - 8.0 * std::min(temporal, 0.15);
|
|
||||||
if (score > bestHandScore) {
|
|
||||||
bestHandScore = score;
|
|
||||||
handChoice = choice;
|
|
||||||
chosenTemporal = temporal;
|
|
||||||
chosenLink = link;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
double endpointAdvantage = candidates[handChoice].handScore - candidates[1 - handChoice].handScore;
|
|
||||||
if (handChoice != limbChoice && candidates[handChoice].handValid && endpointAdvantage >= 0.6) {
|
|
||||||
limbChoice = handChoice;
|
|
||||||
// fuse_person() rewrites this link after its visible-endpoint
|
|
||||||
// override. Temporal recovery consumes the rewritten value.
|
|
||||||
chosenLink = candidates[handChoice].wristLink;
|
|
||||||
}
|
|
||||||
auto const& limb = candidates[limbChoice];
|
|
||||||
auto const& hand = candidates[handChoice];
|
|
||||||
for (int index : {shoulder, elbow, wrist}) output.pose.keypoints[index] = limb.pose->keypoints[index];
|
|
||||||
for (int index = 0; index < 21; ++index) output.pose.keypoints[handStart + index] = hand.pose->keypoints[handStart + index];
|
|
||||||
std::vector<double> limbDistances, handDistances;
|
|
||||||
for (int index = 0; index < 3; ++index) limbDistances.push_back(norm(sub(candidates[0].limb[index], candidates[1].limb[index])));
|
|
||||||
for (int index = 0; index < 21; ++index) handDistances.push_back(norm(sub(candidates[0].hand[index], candidates[1].hand[index])));
|
|
||||||
double handDisagreement = percentile90(handDistances);
|
|
||||||
double scoreAdvantage = candidates[0].handScore - candidates[1].handScore;
|
|
||||||
auto& meta = output.meta[side];
|
|
||||||
meta.limbConsensus = median(limbDistances) <= 0.05;
|
|
||||||
meta.handValid = hand.handValid;
|
|
||||||
meta.distanceEligible = hand.handValid && (
|
|
||||||
handDisagreement <= 0.06 ||
|
|
||||||
(hand.source == "raw" && scoreAdvantage >= 0.6 && chosenTemporal <= 0.08));
|
|
||||||
meta.alarmEligible = meta.distanceEligible || (hand.handValid && hand.source == "raw" && scoreAdvantage >= 0.6);
|
|
||||||
meta.handScore = std::nearbyint(hand.handScore * 1e4) / 1e4;
|
|
||||||
meta.linkToSelectedLimb = std::nearbyint(chosenLink * 1e5) / 1e5;
|
|
||||||
meta.handSource = hand.source;
|
|
||||||
if (!hand.handValid) meta.handState = "rejected";
|
|
||||||
else if (handDisagreement <= 0.06) meta.handState = "consensus";
|
|
||||||
else if (hand.source == "raw" && scoreAdvantage >= 0.6 && chosenTemporal <= 0.08) meta.handState = "visible_endpoint";
|
|
||||||
else if (hand.source == "isolated") meta.handState = "inferred";
|
|
||||||
else meta.handState = "conflict";
|
|
||||||
if (track.hasLimbPrevious) {
|
|
||||||
for (int i = 0; i < 3; ++i) {
|
|
||||||
auto delta = sub(limb.limb[i], track.limbPrevious[i]);
|
|
||||||
track.limbVelocity[i] = track.hasLimbVelocity ? add(mul(track.limbVelocity[i], 0.65), mul(delta, 0.35)) : delta;
|
|
||||||
}
|
|
||||||
track.hasLimbVelocity = true;
|
|
||||||
}
|
|
||||||
if (hand.handValid) {
|
|
||||||
if (track.hasHandPrevious) {
|
|
||||||
for (int i = 0; i < 21; ++i) {
|
|
||||||
auto delta = sub(hand.hand[i], track.handPrevious[i]);
|
|
||||||
track.handVelocity[i] = track.hasHandVelocity ? add(mul(track.handVelocity[i], 0.65), mul(delta, 0.35)) : delta;
|
|
||||||
}
|
|
||||||
track.hasHandVelocity = true;
|
|
||||||
}
|
|
||||||
track.handPrevious = hand.hand;
|
|
||||||
track.hasHandPrevious = true;
|
|
||||||
track.handSource = hand.source;
|
|
||||||
}
|
|
||||||
track.limbPrevious = limb.limb;
|
|
||||||
track.hasLimbPrevious = true;
|
|
||||||
track.limbSource = limb.source;
|
|
||||||
}
|
|
||||||
// Same-person duplicate suppression from the reference fusion.
|
|
||||||
auto handCenter = [&](int side) {
|
|
||||||
int start = side == 0 ? 91 : 112;
|
|
||||||
Vec2 center{};
|
|
||||||
for (int i = 0; i < 21; ++i) center = add(center, vec(output.pose.keypoints[start + i]));
|
|
||||||
return mul(center, 1.0 / 21.0);
|
|
||||||
};
|
|
||||||
if (norm(sub(handCenter(0), handCenter(1))) / boxDiagonal(raw.box) < 0.08) {
|
|
||||||
int weaker = output.meta[0].handScore <= output.meta[1].handScore ? 0 : 1;
|
|
||||||
output.meta[weaker].handState = "duplicate";
|
|
||||||
output.meta[weaker].distanceEligible = false;
|
|
||||||
output.meta[weaker].alarmEligible = false;
|
|
||||||
}
|
|
||||||
// Motion-adaptive EMA in person-box coordinates.
|
|
||||||
std::array<Vec2, 133> now{};
|
|
||||||
for (int i = 0; i < 133; ++i) now[i] = normalized(output.pose.keypoints[i], raw.box);
|
|
||||||
if (!state.hasSmoothed) { state.smoothed = now; state.hasSmoothed = true; }
|
|
||||||
else {
|
|
||||||
for (int i = 0; i < 133; ++i) {
|
|
||||||
double displacement = norm(sub(now[i], state.smoothed[i]));
|
|
||||||
double alpha = 0.32 + 0.58 * std::clamp(displacement / 0.08, 0.0, 1.0);
|
|
||||||
state.smoothed[i] = add(mul(now[i], alpha), mul(state.smoothed[i], 1.0 - alpha));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fused.push_back(std::move(output));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exact build_global_hands() evidence: selected fusion output, stable
|
|
||||||
// score ordering, then the same cross-person duplicate collapse.
|
|
||||||
std::vector<GlobalHand> globalCandidates;
|
|
||||||
for (std::size_t person = 0; person < fused.size(); ++person) {
|
|
||||||
for (int side = 0; side < 2; ++side) {
|
|
||||||
auto const& meta = fused[person].meta[side];
|
|
||||||
if (!meta.alarmEligible) continue;
|
|
||||||
int const start = side == 0 ? 91 : 112;
|
|
||||||
GlobalHand hand;
|
|
||||||
hand.personId = static_cast<int>(person) + 1;
|
|
||||||
hand.trackId = fused[person].pose.box.trackId;
|
|
||||||
hand.left = side == 0;
|
|
||||||
hand.source = meta.handSource;
|
|
||||||
hand.state = meta.handState;
|
|
||||||
hand.distanceEligible = meta.distanceEligible;
|
|
||||||
hand.alarmEligible = meta.alarmEligible;
|
|
||||||
hand.score = static_cast<float>(meta.handScore);
|
|
||||||
float minX = fused[person].pose.keypoints[start].x;
|
|
||||||
float maxX = minX;
|
|
||||||
float minY = fused[person].pose.keypoints[start].y;
|
|
||||||
float maxY = minY;
|
|
||||||
for (int point = 0; point < 21; ++point) {
|
|
||||||
hand.keypoints[point] = fused[person].pose.keypoints[start + point];
|
|
||||||
minX = std::min(minX, hand.keypoints[point].x);
|
|
||||||
maxX = std::max(maxX, hand.keypoints[point].x);
|
|
||||||
minY = std::min(minY, hand.keypoints[point].y);
|
|
||||||
maxY = std::max(maxY, hand.keypoints[point].y);
|
|
||||||
}
|
|
||||||
hand.spanPx = std::hypot(maxX - minX, maxY - minY);
|
|
||||||
globalCandidates.push_back(std::move(hand));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::stable_sort(globalCandidates.begin(), globalCandidates.end(), [](auto const& first, auto const& second) {
|
|
||||||
return first.score > second.score;
|
|
||||||
});
|
|
||||||
std::vector<GlobalHand> globalHands;
|
|
||||||
for (auto& candidate : globalCandidates) {
|
|
||||||
Vec2 candidateCenter{};
|
|
||||||
for (auto const& point : candidate.keypoints) candidateCenter = add(candidateCenter, vec(point));
|
|
||||||
candidateCenter = mul(candidateCenter, 1.0 / 21.0);
|
|
||||||
auto duplicate = globalHands.end();
|
|
||||||
for (auto iterator = globalHands.begin(); iterator != globalHands.end(); ++iterator) {
|
|
||||||
Vec2 existingCenter{};
|
|
||||||
for (auto const& point : iterator->keypoints) existingCenter = add(existingCenter, vec(point));
|
|
||||||
existingCenter = mul(existingCenter, 1.0 / 21.0);
|
|
||||||
double const threshold = std::clamp(
|
|
||||||
0.45 * std::max<double>(candidate.spanPx, iterator->spanPx), 18.0, 70.0);
|
|
||||||
if (norm(sub(candidateCenter, existingCenter)) <= threshold) {
|
|
||||||
duplicate = iterator;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (duplicate == globalHands.end()) globalHands.push_back(std::move(candidate));
|
|
||||||
else duplicate->aliases.emplace_back(candidate.personId, candidate.left);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the model measurements, then collapse cross-person duplicates.
|
|
||||||
std::vector<std::array<std::optional<Measurement>, 2>> measurements(fused.size());
|
|
||||||
for (std::size_t person = 0; person < fused.size(); ++person) {
|
|
||||||
for (int side = 0; side < 2; ++side) {
|
|
||||||
if (!fused[person].meta[side].alarmEligible) continue;
|
|
||||||
Measurement measurement;
|
|
||||||
int start = side == 0 ? 91 : 112;
|
|
||||||
for (int i = 0; i < 21; ++i) measurement.hand[i] = vec(fused[person].pose.keypoints[start + i]);
|
|
||||||
measurement.precise = fused[person].meta[side].distanceEligible;
|
|
||||||
measurement.provisional = !measurement.precise;
|
|
||||||
measurement.quality = fused[person].meta[side].handScore;
|
|
||||||
measurements[person][side] = measurement;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
auto handSpan = [&](Measurement const& measurement) {
|
|
||||||
double minX = measurement.hand[0].x, maxX = minX, minY = measurement.hand[0].y, maxY = minY;
|
|
||||||
for (auto point : measurement.hand) { minX = std::min(minX, point.x); maxX = std::max(maxX, point.x); minY = std::min(minY, point.y); maxY = std::max(maxY, point.y); }
|
|
||||||
return std::hypot(maxX - minX, maxY - minY);
|
|
||||||
};
|
|
||||||
auto handCenter = [&](Measurement const& measurement) {
|
|
||||||
Vec2 center{}; for (auto point : measurement.hand) center = add(center, point); return mul(center, 1.0 / 21.0);
|
|
||||||
};
|
|
||||||
for (std::size_t firstPerson = 0; firstPerson < fused.size(); ++firstPerson) {
|
|
||||||
for (int firstSide = 0; firstSide < 2; ++firstSide) {
|
|
||||||
if (!measurements[firstPerson][firstSide]) continue;
|
|
||||||
for (std::size_t secondPerson = firstPerson + 1; secondPerson < fused.size(); ++secondPerson) {
|
|
||||||
for (int secondSide = 0; secondSide < 2; ++secondSide) {
|
|
||||||
if (!measurements[firstPerson][firstSide] || !measurements[secondPerson][secondSide]) continue;
|
|
||||||
auto const& first = *measurements[firstPerson][firstSide];
|
|
||||||
auto const& second = *measurements[secondPerson][secondSide];
|
|
||||||
double firstSpan = handSpan(first), secondSpan = handSpan(second);
|
|
||||||
if (norm(sub(handCenter(first), handCenter(second))) > std::max(70.0, 0.5 * std::max(firstSpan, secondSpan))) continue;
|
|
||||||
double firstArea = std::max((fused[firstPerson].pose.box.x2 - fused[firstPerson].pose.box.x1) * (fused[firstPerson].pose.box.y2 - fused[firstPerson].pose.box.y1), 1.0F);
|
|
||||||
double secondArea = std::max((fused[secondPerson].pose.box.x2 - fused[secondPerson].pose.box.x1) * (fused[secondPerson].pose.box.y2 - fused[secondPerson].pose.box.y1), 1.0F);
|
|
||||||
if (firstArea >= 2.5 * secondArea &&
|
|
||||||
(firstSpan >= 1.45 * secondSpan || first.quality >= second.quality + 0.6)) {
|
|
||||||
measurements[secondPerson][secondSide].reset();
|
|
||||||
states_[fused[secondPerson].pose.box.trackId].endpoints[secondSide] = {};
|
|
||||||
} else if (secondArea >= 2.5 * firstArea &&
|
|
||||||
(secondSpan >= 1.45 * firstSpan || second.quality >= first.quality + 0.6)) {
|
|
||||||
measurements[firstPerson][firstSide].reset();
|
|
||||||
states_[fused[firstPerson].pose.box.trackId].endpoints[firstSide] = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match resolve_global_hand_assignments(): a large foreground track may reclaim
|
|
||||||
// a nearby hand that RTMW emitted only from a smaller background crop.
|
|
||||||
struct Reassignment {
|
|
||||||
double normalizedDistance{};
|
|
||||||
std::size_t targetPerson{};
|
|
||||||
int targetSide{};
|
|
||||||
std::size_t sourcePerson{};
|
|
||||||
int sourceSide{};
|
|
||||||
Vec2 sourceCenter{};
|
|
||||||
};
|
|
||||||
std::vector<Reassignment> reassignments;
|
|
||||||
for (std::size_t targetPerson = 0; targetPerson < fused.size(); ++targetPerson) {
|
|
||||||
for (int targetSide = 0; targetSide < 2; ++targetSide) {
|
|
||||||
if (measurements[targetPerson][targetSide]) continue;
|
|
||||||
auto& targetEndpoint = states_[fused[targetPerson].pose.box.trackId].endpoints[targetSide];
|
|
||||||
if (!targetEndpoint.hasWrist || !targetEndpoint.hasHandShape || targetEndpoint.missedFrames > 30) continue;
|
|
||||||
double targetArea = std::max(
|
|
||||||
(fused[targetPerson].pose.box.x2 - fused[targetPerson].pose.box.x1) *
|
|
||||||
(fused[targetPerson].pose.box.y2 - fused[targetPerson].pose.box.y1), 1.0F);
|
|
||||||
Vec2 shapeCenter{};
|
|
||||||
for (auto point : targetEndpoint.handShape) shapeCenter = add(shapeCenter, point);
|
|
||||||
shapeCenter = mul(shapeCenter, 1.0 / 21.0);
|
|
||||||
Vec2 predictedCenter = add(targetEndpoint.wrist, shapeCenter);
|
|
||||||
Measurement targetShape;
|
|
||||||
for (int point = 0; point < 21; ++point) targetShape.hand[point] = targetEndpoint.handShape[point];
|
|
||||||
double targetSpan = handSpan(targetShape);
|
|
||||||
double gate = 0.20 * boxDiagonal(fused[targetPerson].pose.box);
|
|
||||||
for (std::size_t sourcePerson = 0; sourcePerson < fused.size(); ++sourcePerson) {
|
|
||||||
if (sourcePerson == targetPerson) continue;
|
|
||||||
for (int sourceSide = 0; sourceSide < 2; ++sourceSide) {
|
|
||||||
auto const& source = measurements[sourcePerson][sourceSide];
|
|
||||||
if (!source || source->source != "model") continue;
|
|
||||||
double sourceArea = std::max(
|
|
||||||
(fused[sourcePerson].pose.box.x2 - fused[sourcePerson].pose.box.x1) *
|
|
||||||
(fused[sourcePerson].pose.box.y2 - fused[sourcePerson].pose.box.y1), 1.0F);
|
|
||||||
if (targetArea < 2.5 * sourceArea) continue;
|
|
||||||
Vec2 sourceCenter = handCenter(*source);
|
|
||||||
double sourceSpan = handSpan(*source);
|
|
||||||
double candidateDistance = norm(sub(sourceCenter, predictedCenter));
|
|
||||||
if (candidateDistance > gate || targetSpan < 1.45 * std::max(sourceSpan, 1.0)) continue;
|
|
||||||
reassignments.push_back({
|
|
||||||
candidateDistance / std::max(gate, 1e-6), targetPerson, targetSide,
|
|
||||||
sourcePerson, sourceSide, sourceCenter});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::sort(reassignments.begin(), reassignments.end(), [](auto const& first, auto const& second) {
|
|
||||||
return std::tie(first.normalizedDistance, first.targetPerson, first.targetSide,
|
|
||||||
first.sourcePerson, first.sourceSide) <
|
|
||||||
std::tie(second.normalizedDistance, second.targetPerson, second.targetSide,
|
|
||||||
second.sourcePerson, second.sourceSide);
|
|
||||||
});
|
|
||||||
std::set<std::pair<std::size_t, int>> usedTargets, usedSources;
|
|
||||||
for (auto const& item : reassignments) {
|
|
||||||
auto targetKey = std::make_pair(item.targetPerson, item.targetSide);
|
|
||||||
auto sourceKey = std::make_pair(item.sourcePerson, item.sourceSide);
|
|
||||||
if (usedTargets.count(targetKey) || usedSources.count(sourceKey) ||
|
|
||||||
measurements[item.targetPerson][item.targetSide] ||
|
|
||||||
!measurements[item.sourcePerson][item.sourceSide]) continue;
|
|
||||||
auto& targetEndpoint = states_[fused[item.targetPerson].pose.box.trackId].endpoints[item.targetSide];
|
|
||||||
Vec2 shapeCenter{};
|
|
||||||
for (auto point : targetEndpoint.handShape) shapeCenter = add(shapeCenter, point);
|
|
||||||
shapeCenter = mul(shapeCenter, 1.0 / 21.0);
|
|
||||||
Vec2 reassignedWrist = sub(item.sourceCenter, shapeCenter);
|
|
||||||
Measurement reassigned;
|
|
||||||
for (int point = 0; point < 21; ++point) {
|
|
||||||
reassigned.hand[point] = add(reassignedWrist, targetEndpoint.handShape[point]);
|
|
||||||
}
|
|
||||||
reassigned.provisional = true;
|
|
||||||
reassigned.source = "global_reassignment";
|
|
||||||
reassigned.quality = measurements[item.sourcePerson][item.sourceSide]->quality;
|
|
||||||
measurements[item.targetPerson][item.targetSide] = reassigned;
|
|
||||||
measurements[item.sourcePerson][item.sourceSide].reset();
|
|
||||||
auto& sourceEndpoint = states_[fused[item.sourcePerson].pose.box.trackId].endpoints[item.sourceSide];
|
|
||||||
sourceEndpoint = {};
|
|
||||||
usedTargets.insert(targetKey);
|
|
||||||
usedSources.insert(sourceKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
PoseFrameResult result;
|
|
||||||
result.globalHands = std::move(globalHands);
|
|
||||||
result.selectedPeople.reserve(fused.size());
|
|
||||||
result.smoothedPeople.reserve(fused.size());
|
|
||||||
result.v2People.reserve(fused.size());
|
|
||||||
for (auto const& person : fused) {
|
|
||||||
std::array<FusionMeta,2> meta{};
|
|
||||||
for (int side=0;side<2;++side) {
|
|
||||||
auto const& m=person.meta[side];
|
|
||||||
meta[side]={m.limbConsensus,m.handValid,m.distanceEligible,m.alarmEligible,m.handScore,m.handSource,m.handState};
|
|
||||||
}
|
|
||||||
result.metadata.push_back(meta);
|
|
||||||
result.selectedPeople.push_back(person.pose);
|
|
||||||
PersonPose smoothed = person.pose;
|
|
||||||
auto const& state = states_[person.pose.box.trackId];
|
|
||||||
for (int keypoint = 0; keypoint < 133; ++keypoint) {
|
|
||||||
Vec2 point = denormalized(state.smoothed[keypoint], person.pose.box);
|
|
||||||
smoothed.keypoints[keypoint].x = static_cast<float>(point.x);
|
|
||||||
smoothed.keypoints[keypoint].y = static_cast<float>(point.y);
|
|
||||||
}
|
|
||||||
result.smoothedPeople.push_back(smoothed);
|
|
||||||
result.v2People.push_back(std::move(smoothed));
|
|
||||||
}
|
|
||||||
for (std::size_t person = 0; person < fused.size(); ++person) {
|
|
||||||
auto& state = states_[fused[person].pose.box.trackId];
|
|
||||||
int frameGap = state.lastFrame < 0 ? 0 : std::max(frameIndex - state.lastFrame - 1, 0);
|
|
||||||
for (auto& endpoint : state.endpoints) endpoint.missedFrames += frameGap;
|
|
||||||
state.lastFrame = frameIndex;
|
|
||||||
for (int side = 0; side < 2; ++side) {
|
|
||||||
if (!fused[person].meta[side].limbConsensus || !fused[person].meta[side].distanceEligible) continue;
|
|
||||||
int shoulderIndex = side == 0 ? 5 : 6;
|
|
||||||
int elbowIndex = side == 0 ? 7 : 8;
|
|
||||||
int wristIndex = side == 0 ? 9 : 10;
|
|
||||||
Vec2 shoulder = vec(fused[person].pose.keypoints[shoulderIndex]);
|
|
||||||
Vec2 elbow = vec(fused[person].pose.keypoints[elbowIndex]);
|
|
||||||
Vec2 wrist = vec(fused[person].pose.keypoints[wristIndex]);
|
|
||||||
double diagonal = boxDiagonal(fused[person].pose.box);
|
|
||||||
double upper = norm(sub(shoulder, elbow)) / diagonal;
|
|
||||||
double forearm = norm(sub(elbow, wrist)) / diagonal;
|
|
||||||
if (upper >= 0.04 && upper <= 0.65 && forearm >= 0.04 && forearm <= 0.65) {
|
|
||||||
auto& samples = state.boneSamples[side];
|
|
||||||
samples.push_back({upper, forearm});
|
|
||||||
while (samples.size() > 120) samples.pop_front();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (int side = 0; side < 2; ++side) {
|
|
||||||
int shoulderIndex = side == 0 ? 5 : 6;
|
|
||||||
int elbowIndex = side == 0 ? 7 : 8;
|
|
||||||
int wristIndex = side == 0 ? 9 : 10;
|
|
||||||
int handStart = side == 0 ? 91 : 112;
|
|
||||||
auto& endpoint = state.endpoints[side];
|
|
||||||
auto measurement = measurements[person][side];
|
|
||||||
// Reference temporal recovery gate.
|
|
||||||
bool const recoverableHandState = fused[person].meta[side].handState == "conflict" ||
|
|
||||||
fused[person].meta[side].handState == "inferred";
|
|
||||||
if (!measurement && endpoint.hasWrist && endpoint.hasHandShape && endpoint.missedFrames <= 30 &&
|
|
||||||
recoverableHandState && fused[person].meta[side].handValid &&
|
|
||||||
fused[person].meta[side].handScore >= 2.0 &&
|
|
||||||
fused[person].meta[side].linkToSelectedLimb <= 0.08) {
|
|
||||||
Vec2 measured = vec(fused[person].pose.keypoints[handStart]);
|
|
||||||
Vec2 prediction = add(endpoint.wrist, endpoint.velocity);
|
|
||||||
double gate = std::min(0.20, 0.05 + 0.008 * std::max(endpoint.missedFrames, 1)) * boxDiagonal(fused[person].pose.box);
|
|
||||||
if (norm(sub(measured, prediction)) <= gate) {
|
|
||||||
Measurement recovered;
|
|
||||||
for (int i = 0; i < 21; ++i) recovered.hand[i] = vec(fused[person].pose.keypoints[handStart + i]);
|
|
||||||
recovered.provisional = true; recovered.quality = fused[person].meta[side].handScore; recovered.source = "temporal_recovery";
|
|
||||||
measurement = recovered;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LimbState outputState;
|
|
||||||
std::array<Vec2, 21> outputHand{};
|
|
||||||
bool hasOutput = false;
|
|
||||||
bool returningAfterLong = measurement && endpoint.missedFrames > 12;
|
|
||||||
if (measurement) {
|
|
||||||
Vec2 measuredWrist = measurement->hand[0];
|
|
||||||
std::array<Vec2, 21> measuredShape{};
|
|
||||||
for (int i = 0; i < 21; ++i) measuredShape[i] = sub(measurement->hand[i], measuredWrist);
|
|
||||||
if (!endpoint.hasWrist || endpoint.missedFrames > 12) {
|
|
||||||
endpoint.wrist = measuredWrist; endpoint.velocity = {}; endpoint.handShape = measuredShape;
|
|
||||||
} else {
|
|
||||||
Vec2 prediction = add(endpoint.wrist, endpoint.velocity);
|
|
||||||
Vec2 residual = sub(measuredWrist, prediction);
|
|
||||||
double alpha = measurement->precise ? 0.78 : 0.88;
|
|
||||||
double beta = measurement->precise ? 0.18 : 0.24;
|
|
||||||
endpoint.wrist = add(prediction, mul(residual, alpha));
|
|
||||||
endpoint.velocity = add(mul(endpoint.velocity, 0.82), mul(residual, beta));
|
|
||||||
double shapeAlpha = measurement->precise ? 0.55 : 0.70;
|
|
||||||
for (int i = 0; i < 21; ++i) endpoint.handShape[i] = add(mul(measuredShape[i], shapeAlpha), mul(endpoint.handShape[i], 1.0 - shapeAlpha));
|
|
||||||
}
|
|
||||||
endpoint.hasWrist = endpoint.hasHandShape = true;
|
|
||||||
endpoint.lastObservedFrame = frameIndex;
|
|
||||||
endpoint.missedFrames = 0;
|
|
||||||
outputState = measurement->source == "global_reassignment" ? LimbState::Reassigned : measurement->source == "temporal_recovery" ? LimbState::Recovered : measurement->precise ? LimbState::Observed : measurement->provisional ? LimbState::Provisional : LimbState::Interpolated;
|
|
||||||
for (int i = 0; i < 21; ++i) outputHand[i] = add(endpoint.wrist, endpoint.handShape[i]);
|
|
||||||
hasOutput = true;
|
|
||||||
} else {
|
|
||||||
++endpoint.missedFrames;
|
|
||||||
if (!endpoint.hasWrist || endpoint.missedFrames > 30) outputState = LimbState::ReachableLong;
|
|
||||||
else if (endpoint.missedFrames <= 12) {
|
|
||||||
endpoint.wrist = add(endpoint.wrist, endpoint.velocity);
|
|
||||||
endpoint.velocity = mul(endpoint.velocity, 0.86);
|
|
||||||
for (int i = 0; i < 21; ++i) outputHand[i] = add(endpoint.wrist, endpoint.handShape[i]);
|
|
||||||
hasOutput = true; outputState = LimbState::PredictedShort;
|
|
||||||
} else outputState = LimbState::ReachableMedium;
|
|
||||||
}
|
|
||||||
Vec2 smoothedShoulder = denormalized(state.smoothed[shoulderIndex], fused[person].pose.box);
|
|
||||||
Vec2 smoothedElbow = denormalized(state.smoothed[elbowIndex], fused[person].pose.box);
|
|
||||||
Keypoint shoulder = fused[person].pose.keypoints[shoulderIndex];
|
|
||||||
shoulder.x = static_cast<float>(smoothedShoulder.x);
|
|
||||||
shoulder.y = static_cast<float>(smoothedShoulder.y);
|
|
||||||
Keypoint elbow = fused[person].pose.keypoints[elbowIndex];
|
|
||||||
elbow.x = static_cast<float>(smoothedElbow.x);
|
|
||||||
elbow.y = static_cast<float>(smoothedElbow.y);
|
|
||||||
Keypoint wrist = result.smoothedPeople[person].keypoints[wristIndex];
|
|
||||||
std::array<Keypoint, 21> hand{};
|
|
||||||
for (int i = 0; i < 21; ++i) {
|
|
||||||
hand[i] = result.smoothedPeople[person].keypoints[handStart + i];
|
|
||||||
}
|
|
||||||
if (hasOutput) {
|
|
||||||
auto& ownSamples = state.boneSamples[side];
|
|
||||||
auto& oppositeSamples = state.boneSamples[1 - side];
|
|
||||||
auto const& samples = ownSamples.size() < 10 && oppositeSamples.size() >= 10
|
|
||||||
? oppositeSamples : ownSamples;
|
|
||||||
double upperRatio = 0.24, forearmRatio = 0.22;
|
|
||||||
if (!samples.empty()) {
|
|
||||||
std::vector<double> upperValues, forearmValues;
|
|
||||||
upperValues.reserve(samples.size());
|
|
||||||
forearmValues.reserve(samples.size());
|
|
||||||
for (auto const& sample : samples) {
|
|
||||||
upperValues.push_back(sample[0]);
|
|
||||||
forearmValues.push_back(sample[1]);
|
|
||||||
}
|
|
||||||
upperRatio = median(std::move(upperValues));
|
|
||||||
forearmRatio = median(std::move(forearmValues));
|
|
||||||
}
|
|
||||||
double diagonal = boxDiagonal(fused[person].pose.box);
|
|
||||||
double upperLength = upperRatio * diagonal;
|
|
||||||
double forearmLength = forearmRatio * diagonal;
|
|
||||||
double totalLength = upperLength + forearmLength;
|
|
||||||
if (totalLength > 0.95 * diagonal) {
|
|
||||||
double lengthScale = 0.95 * diagonal / totalLength;
|
|
||||||
upperLength *= lengthScale;
|
|
||||||
forearmLength *= lengthScale;
|
|
||||||
}
|
|
||||||
Vec2 referenceElbow = returningAfterLong || !endpoint.hasLastElbow
|
|
||||||
? vec(fused[person].pose.keypoints[elbowIndex]) : endpoint.lastElbow;
|
|
||||||
if (!fused[person].meta[side].limbConsensus || outputState != LimbState::Observed) {
|
|
||||||
Vec2 solved = solveTwoBoneElbow(
|
|
||||||
smoothedShoulder, outputHand[0], upperLength, forearmLength, referenceElbow);
|
|
||||||
elbow.x = static_cast<float>(solved.x);
|
|
||||||
elbow.y = static_cast<float>(solved.y);
|
|
||||||
}
|
|
||||||
endpoint.lastElbow = vec(elbow);
|
|
||||||
endpoint.hasLastElbow = true;
|
|
||||||
Vec2 armDirection = sub(outputHand[0], smoothedShoulder);
|
|
||||||
if (norm(armDirection) > 1e-6) {
|
|
||||||
endpoint.lastDirection = mul(armDirection, 1.0 / norm(armDirection));
|
|
||||||
endpoint.hasLastDirection = true;
|
|
||||||
}
|
|
||||||
wrist.x = outputHand[0].x; wrist.y = outputHand[0].y;
|
|
||||||
for (int i = 0; i < 21; ++i) {
|
|
||||||
hand[i].x = outputHand[i].x; hand[i].y = outputHand[i].y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
auto& v2Pose = result.v2People[person];
|
|
||||||
v2Pose.keypoints[shoulderIndex] = shoulder;
|
|
||||||
v2Pose.keypoints[elbowIndex] = elbow;
|
|
||||||
v2Pose.keypoints[wristIndex] = wrist;
|
|
||||||
if (hasOutput) {
|
|
||||||
for (int point = 0; point < 21; ++point) v2Pose.keypoints[handStart + point] = hand[point];
|
|
||||||
}
|
|
||||||
result.limbs.push_back({
|
|
||||||
fused[person].pose.box.trackId, side == 0, outputState,
|
|
||||||
elbow, wrist, hand, fused[person].pose.box,
|
|
||||||
fused[person].meta[side].distanceEligible,
|
|
||||||
fused[person].meta[side].alarmEligible,
|
|
||||||
static_cast<float>(fused[person].meta[side].handScore)});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace fangjia
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
#include "visualizer.hpp"
|
|
||||||
#include "camera_pipeline.hpp"
|
|
||||||
#include <opencv2/highgui.hpp>
|
|
||||||
#include <opencv2/highgui/highgui_c.h>
|
|
||||||
#include <opencv2/imgproc.hpp>
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cmath>
|
|
||||||
#include <iomanip>
|
|
||||||
#include <sstream>
|
|
||||||
#include <stdexcept>
|
|
||||||
|
|
||||||
namespace cms {
|
|
||||||
namespace {
|
|
||||||
constexpr char windowName[]="CMS Camera Risk";
|
|
||||||
cv::Point point(PosePoint const& p){return {int(std::nearbyint(p.x)),int(std::nearbyint(p.y))};}
|
|
||||||
cv::Scalar color(std::string const& level) {
|
|
||||||
if(level=="danger")return {40,40,255};
|
|
||||||
if(level=="warning")return {0,160,255};
|
|
||||||
if(level=="watch")return {0,240,240};
|
|
||||||
if(level=="clear")return {80,220,90};
|
|
||||||
return {190,190,190};
|
|
||||||
}
|
|
||||||
void segment(cv::Mat& image,cv::Point a,cv::Point b,cv::Scalar c,int thickness,bool dashed) {
|
|
||||||
if(!dashed){cv::line(image,a,b,c,thickness,cv::LINE_AA);return;}
|
|
||||||
cv::Point2d d=b-a;double length=cv::norm(d);if(length<1)return;d*=1.0/length;
|
|
||||||
for(double x=0;x<length;x+=14)cv::line(image,cv::Point2d(a)+d*x,cv::Point2d(a)+d*std::min(x+8,length),c,thickness,cv::LINE_AA);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cv::Mat renderResult(FrameResult const& r) {
|
|
||||||
if(!r.image)throw std::invalid_argument("Result has no frame image");
|
|
||||||
auto const& image=*r.image;
|
|
||||||
if(image.width<=0||image.height<=0||image.stride<image.width*3||image.pixels.size()<std::size_t(image.stride)*image.height)
|
|
||||||
throw std::invalid_argument("Malformed result image");
|
|
||||||
cv::Mat view(image.height,image.width,CV_8UC3,const_cast<std::uint8_t*>(image.pixels.data()),image.stride);
|
|
||||||
auto canvas=view.clone();
|
|
||||||
if(r.door_mask_pixels) {
|
|
||||||
auto const& m=*r.door_mask_pixels;
|
|
||||||
if(m.width!=image.width||m.height!=image.height||m.stride<m.width||m.pixels.size()<std::size_t(m.stride)*m.height)
|
|
||||||
throw std::invalid_argument("Result mask does not match image");
|
|
||||||
cv::Mat mask(m.height,m.width,CV_8U,const_cast<std::uint8_t*>(m.pixels.data()),m.stride);
|
|
||||||
auto tint=canvas.clone();tint.setTo(cv::Scalar(255,180,0),mask);
|
|
||||||
cv::addWeighted(canvas,0.75,tint,0.25,0,canvas);
|
|
||||||
std::vector<std::vector<cv::Point>> contours;cv::findContours(mask.clone(),contours,cv::RETR_EXTERNAL,cv::CHAIN_APPROX_SIMPLE);
|
|
||||||
cv::drawContours(canvas,contours,-1,{255,180,0},2);
|
|
||||||
}
|
|
||||||
for(auto const& person:r.persons) {
|
|
||||||
auto const& b=person.box;
|
|
||||||
cv::rectangle(canvas,{int(b[0]),int(b[1])},{int(b[2]),int(b[3])},{0,220,255},2);
|
|
||||||
std::ostringstream text;text<<"ID "<<person.track_id<<" "<<std::fixed<<std::setprecision(2)<<person.confidence;
|
|
||||||
cv::putText(canvas,text.str(),{int(b[0])+4,std::max(180,int(b[1])+25)},cv::FONT_HERSHEY_SIMPLEX,.8,{0,220,255},2,cv::LINE_AA);
|
|
||||||
for(int s=0;s<2;++s) {
|
|
||||||
auto const& state=person.hands[s];auto const& k=person.selected;
|
|
||||||
cv::Scalar arm=s==0?cv::Scalar(40,40,255):cv::Scalar(30,230,30);
|
|
||||||
for(auto e:std::vector<std::pair<int,int>>{{5+s,7+s},{7+s,9+s}}) {
|
|
||||||
segment(canvas,point(k[e.first]),point(k[e.second]),state.limb_consensus?arm:cv::Scalar(0,140,255),3,!state.limb_consensus);
|
|
||||||
cv::circle(canvas,point(k[e.second]),4,arm,-1,cv::LINE_AA);
|
|
||||||
}
|
|
||||||
if(state.state=="rejected")continue;
|
|
||||||
int start=s==0?91:112;
|
|
||||||
auto hand=state.distance_eligible?(s==0?cv::Scalar(255,80,220):cv::Scalar(255,190,40)):cv::Scalar(0,140,255);
|
|
||||||
for(int f=0;f<5;++f)for(int j=0;j<4;++j){int a=j==0?start:start+f*4+j;int b=start+1+f*4+j;segment(canvas,point(k[a]),point(k[b]),hand,2,!state.distance_eligible);}
|
|
||||||
for(int j=0;j<21;++j)cv::circle(canvas,point(k[start+j]),3,hand,-1,cv::LINE_AA);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for(auto const& h:r.hand_regions) {
|
|
||||||
std::vector<cv::Point> points,hull;for(auto const& p:h.keypoints)points.push_back(point(p));cv::convexHull(points,hull);
|
|
||||||
auto risk=std::find_if(r.risk.regions.begin(),r.risk.regions.end(),[&](auto const& item){return item.track_id==h.track_id&&item.side==h.side;});
|
|
||||||
auto c=color(risk==r.risk.regions.end()?"unavailable":risk->risk_level);
|
|
||||||
cv::polylines(canvas,std::vector<std::vector<cv::Point>>{hull},true,c,2,cv::LINE_AA);
|
|
||||||
}
|
|
||||||
for(auto const& region:r.risk.regions)if(region.source=="YOLO_BODY_BOX_FALLBACK") {
|
|
||||||
auto it=std::find(r.track_ids.begin(),r.track_ids.end(),region.track_id);
|
|
||||||
if(it!=r.track_ids.end()){auto const& b=r.boxes.at(it-r.track_ids.begin());cv::putText(canvas,"BODY FALLBACK",{int(b[0])+4,int(b[1])+50},cv::FONT_HERSHEY_SIMPLEX,.65,{0,160,255},2,cv::LINE_AA);}
|
|
||||||
}
|
|
||||||
cv::rectangle(canvas,{0,0},{canvas.cols,148},{20,24,30},-1);
|
|
||||||
std::ostringstream headline;headline<<"RISK ";if(r.risk.risk_score)headline<<std::fixed<<std::setprecision(3)<<*r.risk.risk_score;else headline<<"N/A";headline<<" "<<r.risk.risk_level;
|
|
||||||
cv::putText(canvas,headline.str(),{24,40},cv::FONT_HERSHEY_SIMPLEX,1.0,color(r.risk.risk_level),2,cv::LINE_AA);
|
|
||||||
std::ostringstream status;status<<"camera frame "<<r.frame_index<<" | seq "<<r.capture.driver_sequence<<" | latency "<<std::fixed<<std::setprecision(1)<<r.timing.capture_to_result_ms<<" ms";
|
|
||||||
cv::putText(canvas,status.str(),{24,82},cv::FONT_HERSHEY_SIMPLEX,.72,{235,235,235},2,cv::LINE_AA);
|
|
||||||
std::string calibration=r.door_mask_calibration_verified?"door calibration verified":"door calibration NOT verified (bring-up)";
|
|
||||||
cv::putText(canvas,calibration+" | q/Esc: close preview",{24,123},cv::FONT_HERSHEY_SIMPLEX,.70,{0,190,255},2,cv::LINE_AA);
|
|
||||||
return canvas;
|
|
||||||
}
|
|
||||||
void runLocalVisualization(CameraPipeline const& pipeline) {
|
|
||||||
struct Window {
|
|
||||||
Window(){cv::namedWindow(windowName,cv::WINDOW_NORMAL);cv::resizeWindow(windowName,1000,800);}
|
|
||||||
~Window(){try{if(cvGetWindowHandle(windowName))cv::destroyWindow(windowName);}catch(...) {}}
|
|
||||||
} window;
|
|
||||||
FrameResultPtr shown;
|
|
||||||
while(pipeline.running()) {
|
|
||||||
auto result=pipeline.latest();
|
|
||||||
if(result&&result!=shown){cv::imshow(windowName,renderResult(*result));shown=std::move(result);}
|
|
||||||
int key=cv::waitKey(16)&0xff;
|
|
||||||
if(key==27||key=='q'||key=='Q'||cvGetWindowHandle(windowName)==nullptr)return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2013-2022 Niels Lohmann
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
-24765
File diff suppressed because it is too large
Load Diff
@@ -1,101 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# build_and_run.sh - 在 Orin 上构建并运行 V4L2 取图 demo
|
|
||||||
#
|
|
||||||
# 使用方法:
|
|
||||||
# ./build_and_run.sh [选项]
|
|
||||||
#
|
|
||||||
# 选项:
|
|
||||||
# --deploy 将源码复制到 Orin 并构建
|
|
||||||
# --run 在 Orin 上运行已构建的程序
|
|
||||||
# --both 部署并运行 (默认)
|
|
||||||
#
|
|
||||||
# 示例:
|
|
||||||
# ./build_and_run.sh --both
|
|
||||||
# ./build_and_run.sh --deploy
|
|
||||||
# ./build_and_run.sh --run --args "-n 100 -v"
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# 配置
|
|
||||||
ORIN_HOST="192.168.195.100"
|
|
||||||
ORIN_USER="orin"
|
|
||||||
ORIN_PASS="${ORIN_PASS:?Set ORIN_PASS locally, or use scripts/deploy.sh for interactive SSH}"
|
|
||||||
export SSHPASS="$ORIN_PASS"
|
|
||||||
LOCAL_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
BUILD_DIR="$LOCAL_DIR/build"
|
|
||||||
BIN_NAME="vision-anti-pinch"
|
|
||||||
|
|
||||||
# 默认参数
|
|
||||||
ACTION="both"
|
|
||||||
RUN_ARGS="-n 100"
|
|
||||||
|
|
||||||
# 解析参数
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
--deploy) ACTION="deploy"; shift ;;
|
|
||||||
--run) ACTION="run"; shift ;;
|
|
||||||
--both) ACTION="both"; shift ;;
|
|
||||||
--args)
|
|
||||||
RUN_ARGS="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown option: $1"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# 部署函数
|
|
||||||
do_deploy() {
|
|
||||||
echo "=== 部署源码到 Orin ==="
|
|
||||||
|
|
||||||
# 创建临时目录用于传输
|
|
||||||
TEMP_DIR=$(mktemp -d)
|
|
||||||
cp -r "$LOCAL_DIR"/* "$TEMP_DIR/"
|
|
||||||
|
|
||||||
# 使用 sshpass 复制 (如果没有 sshpass, 手动复制)
|
|
||||||
if command -v sshpass &> /dev/null; then
|
|
||||||
echo "复制源码到 Orin..."
|
|
||||||
sshpass -e scp -o StrictHostKeyChecking=no -r "$TEMP_DIR"/* "${ORIN_USER}@${ORIN_HOST}:~/vision-anti-pinch/"
|
|
||||||
else
|
|
||||||
echo "警告: 未安装 sshpass, 请手动复制源码到 Orin:"
|
|
||||||
echo " scp -r $LOCAL_DIR/* ${ORIN_USER}@${ORIN_HOST}:~/vision-anti-pinch/"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -rf "$TEMP_DIR"
|
|
||||||
|
|
||||||
echo "=== 在 Orin 上构建 ==="
|
|
||||||
sshpass -e ssh -o StrictHostKeyChecking=no "${ORIN_USER}@${ORIN_HOST}" << 'EOF'
|
|
||||||
cd ~/vision-anti-pinch
|
|
||||||
make clean
|
|
||||||
make -j$(nproc)
|
|
||||||
echo "构建完成"
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "部署完成"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 运行函数
|
|
||||||
do_run() {
|
|
||||||
echo "=== 在 Orin 上运行 ==="
|
|
||||||
sshpass -e ssh -o StrictHostKeyChecking=no "${ORIN_USER}@${ORIN_HOST}" << EOF
|
|
||||||
cd ~/vision-anti-pinch
|
|
||||||
./${BIN_NAME} ${RUN_ARGS}
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
# 主流程
|
|
||||||
echo "V4L2 Capture Demo - Build & Run"
|
|
||||||
echo "================================"
|
|
||||||
|
|
||||||
if [[ "$ACTION" == "deploy" ]] || [[ "$ACTION" == "both" ]]; then
|
|
||||||
do_deploy
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$ACTION" == "run" ]] || [[ "$ACTION" == "both" ]]; then
|
|
||||||
do_run
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "完成"
|
|
||||||
-110
@@ -1,110 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# camera_reset.sh - 修复 GMSL2 相机"第二次运行全绿"问题
|
|
||||||
#
|
|
||||||
# 问题原因:
|
|
||||||
# quick_bring_up_64g.sh 第二次运行时,仅重新设置 sensor_mode/trig_pin,
|
|
||||||
# 但 sgx_yuv_gmsl2 链路状态未重新初始化,导致相机输出全绿。
|
|
||||||
#
|
|
||||||
# 模块依赖关系 (modinfo 确认):
|
|
||||||
# sgx-yuv-gmsl2.ko depends: max9296, max9295
|
|
||||||
# 必须按顺序加载: max9295.ko -> max9296.ko -> sgx-yuv-gmsl2.ko
|
|
||||||
# 否则 insmod sgx-yuv-gmsl2.ko 报 "Unknown symbol in module"
|
|
||||||
#
|
|
||||||
# 本脚本: 依赖模块仅在未加载时 insmod (不重载, 避免链路抖动),
|
|
||||||
# sgx_yuv_gmsl2 先 rmmod 再 insmod 完成链路复位。
|
|
||||||
# 全绿图像多由 gmsl_mode 错误引起 (本工程固定 2,2,2,2);
|
|
||||||
# 若复位后仍异常, 请重启 Orin 后运行 quick_bring_up_64g.sh。
|
|
||||||
#
|
|
||||||
# 使用方法:
|
|
||||||
# sudo ./camera_reset.sh [camera_type] [port]
|
|
||||||
#
|
|
||||||
# camera_type: 0-17 对应 quick_bring_up_64g.sh 中的相机类型 (默认 8)
|
|
||||||
# port: 0-7 对应 video 设备号 (默认 5)
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
KO_DIR="/home/orin/SG8A_ORIN_GMSL2-F_V2_AGX_Orin_YUV_JP5.1.2_L4TR35.4.1/ko"
|
|
||||||
VIDEO_PORT="${2:-5}"
|
|
||||||
CAMERA_TYPE="${1:-5}" # 默认 camera_type=5 (SG3S-ISX031C-GMSL2F)
|
|
||||||
|
|
||||||
echo "========================================================="
|
|
||||||
echo "Camera Reset Script"
|
|
||||||
echo "========================================================="
|
|
||||||
echo "Camera type: $CAMERA_TYPE"
|
|
||||||
echo "Video port: $VIDEO_PORT (/dev/video${VIDEO_PORT})"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Step 1: 停止占用 video 设备的进程
|
|
||||||
echo "[1/4] Stopping processes using video devices..."
|
|
||||||
pkill -9 gst-launch-1.0 2>/dev/null || true
|
|
||||||
pkill -9 vision-anti-pinch 2>/dev/null || true
|
|
||||||
sleep 1
|
|
||||||
|
|
||||||
# Step 2: 加载依赖模块 max9295/max9296 (未加载时), 再重载 sgx_yuv_gmsl2
|
|
||||||
echo "[2/4] Loading modules (max9295 -> max9296 -> sgx_yuv_gmsl2)..."
|
|
||||||
if ! lsmod | grep -q '^max9295'; then
|
|
||||||
sudo insmod "$KO_DIR/max9295.ko" || { echo "ERROR: insmod max9295 failed"; exit 1; }
|
|
||||||
echo " loaded max9295.ko"
|
|
||||||
else
|
|
||||||
echo " max9295 already loaded (skip)"
|
|
||||||
fi
|
|
||||||
if ! lsmod | grep -q '^max9296'; then
|
|
||||||
sudo insmod "$KO_DIR/max9296.ko" || { echo "ERROR: insmod max9296 failed"; exit 1; }
|
|
||||||
echo " loaded max9296.ko"
|
|
||||||
else
|
|
||||||
echo " max9296 already loaded (skip)"
|
|
||||||
fi
|
|
||||||
if lsmod | grep -q '^sgx_yuv_gmsl2'; then
|
|
||||||
sudo rmmod sgx_yuv_gmsl2
|
|
||||||
sleep 1
|
|
||||||
fi
|
|
||||||
sudo insmod "$KO_DIR/sgx-yuv-gmsl2.ko" gmsl_mode=2,2,2,2 || { echo "ERROR: insmod sgx-yuv-gmsl2 failed"; exit 1; }
|
|
||||||
echo " loaded sgx-yuv-gmsl2.ko (gmsl_mode=2,2,2,2)"
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
# Step 3: 验证设备节点
|
|
||||||
echo "[3/4] Verifying device nodes..."
|
|
||||||
if [ ! -c "/dev/video${VIDEO_PORT}" ]; then
|
|
||||||
echo "Error: /dev/video${VIDEO_PORT} not found!"
|
|
||||||
echo "Please reboot the system and run quick_bring_up_64g.sh again."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo " /dev/video${VIDEO_PORT} exists"
|
|
||||||
|
|
||||||
# Step 4: 根据 camera_type 配置 sensor_mode 和 trig_pin
|
|
||||||
echo "[4/4] Configuring sensor..."
|
|
||||||
case "$CAMERA_TYPE" in
|
|
||||||
0) v4l2-ctl -d /dev/video${VIDEO_PORT} -c sensor_mode=0,trig_pin=0xffff0007 ;;
|
|
||||||
1) v4l2-ctl -d /dev/video${VIDEO_PORT} -c sensor_mode=0,trig_pin=0xffff0007 ;;
|
|
||||||
2) v4l2-ctl -d /dev/video${VIDEO_PORT} -c sensor_mode=0,trig_pin=0xffff0007 ;;
|
|
||||||
3) v4l2-ctl -d /dev/video${VIDEO_PORT} -c sensor_mode=1,trig_pin=0xffff0007 ;;
|
|
||||||
4) v4l2-ctl -d /dev/video${VIDEO_PORT} -c sensor_mode=0,trig_pin=0xffff0007 ;;
|
|
||||||
5) v4l2-ctl -d /dev/video${VIDEO_PORT} -c sensor_mode=1,trig_pin=0xffff0007 ;;
|
|
||||||
6) v4l2-ctl -d /dev/video${VIDEO_PORT} -c sensor_mode=1,trig_pin=0xffff0007 ;;
|
|
||||||
7) v4l2-ctl -d /dev/video${VIDEO_PORT} -c sensor_mode=2,trig_pin=0xffff0008,trig_mode=1 ;;
|
|
||||||
8) v4l2-ctl -d /dev/video${VIDEO_PORT} -c sensor_mode=3,trig_pin=0xffff0007 ;;
|
|
||||||
9) v4l2-ctl -d /dev/video${VIDEO_PORT} -c sensor_mode=3,trig_pin=0xffff0008 ;;
|
|
||||||
10) v4l2-ctl -d /dev/video${VIDEO_PORT} -c sensor_mode=3,trig_pin=0xffff0007 ;;
|
|
||||||
11) v4l2-ctl -d /dev/video${VIDEO_PORT} -c sensor_mode=3,trig_pin=0xffff0008 ;;
|
|
||||||
12) v4l2-ctl -d /dev/video${VIDEO_PORT} -c sensor_mode=5,trig_pin=0xffff0007,trig_mode=3 ;;
|
|
||||||
13) v4l2-ctl -d /dev/video${VIDEO_PORT} -c sensor_mode=4,trig_pin=0xffff0007,trig_mode=3 ;;
|
|
||||||
14) v4l2-ctl -d /dev/video${VIDEO_PORT} -c sensor_mode=6,trig_pin=0xffff0007 ;;
|
|
||||||
15) v4l2-ctl -d /dev/video${VIDEO_PORT} -c sensor_mode=8 ;;
|
|
||||||
16) v4l2-ctl -d /dev/video${VIDEO_PORT} -c sensor_mode=9 ;;
|
|
||||||
17) v4l2-ctl -d /dev/video${VIDEO_PORT} -c sensor_mode=0 ;;
|
|
||||||
*) echo "Unknown camera_type: $CAMERA_TYPE"; exit 1 ;;
|
|
||||||
esac
|
|
||||||
sleep 1
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "========================================================="
|
|
||||||
echo "Camera reset complete!"
|
|
||||||
echo ""
|
|
||||||
echo "Now run: ./vision-anti-pinch -d /dev/video5 -W 1920 -H 1536 -n 100 -s 5 -t jpg"
|
|
||||||
echo ""
|
|
||||||
echo "⚠️ If image is still all-green, the GMSL2 link is damaged."
|
|
||||||
echo " Please REBOOT the Orin system to recover:"
|
|
||||||
echo " sudo reboot"
|
|
||||||
echo " (then run quick_bring_up_64g.sh after reboot)"
|
|
||||||
echo "========================================================="
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Same-process downstream example. Run on Orin, which owns the camera/GPU."""
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
||||||
from run_camera import CameraPipeline, parse_args
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# No visualization or disk output by default. CLI flags can enable either.
|
|
||||||
pipeline = CameraPipeline(parse_args())
|
|
||||||
last_frame = -1
|
|
||||||
try:
|
|
||||||
pipeline.start()
|
|
||||||
while True:
|
|
||||||
# Reading the variable is enough for a consumer with its own clock:
|
|
||||||
# result = pipeline.latest_result # equivalent to pipeline.latest()
|
|
||||||
# This sample waits to avoid busy polling; still only the latest
|
|
||||||
# result is returned, without queuing/acknowledging individual frames.
|
|
||||||
result = pipeline.wait_for_result(last_frame, timeout=1.0)
|
|
||||||
if result is not None:
|
|
||||||
last_frame = result['frame_index']
|
|
||||||
print(last_frame, result['risk']['risk_type'],
|
|
||||||
result['risk']['risk_score'], flush=True)
|
|
||||||
# Downstream code uses result['door_risks'], ['skeletons'], ['can'].
|
|
||||||
elif not pipeline.running():
|
|
||||||
break
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
pipeline.request_stop()
|
|
||||||
pipeline.join() # Also propagates worker errors to the caller.
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
libusb-1.0.so
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
libusbcanfd.so
|
|
||||||
@@ -1,344 +0,0 @@
|
|||||||
// vision-anti-pinch - V4L2 MMAP 取图 + USBCANFD CAN 收发 (OpenCV 版本)
|
|
||||||
//
|
|
||||||
// 使用 OpenCV 进行 UYVY/YUYV -> BGR 转换,避免手动转换公式问题
|
|
||||||
// CAN 收发默认开启, --no-can 关闭
|
|
||||||
//
|
|
||||||
// 用法:
|
|
||||||
// ./vision-anti-pinch -d /dev/video5 -n 10 -s 2 -t jpg -o ./out
|
|
||||||
|
|
||||||
#include "v4l2_capture.h"
|
|
||||||
#include "can_comm.h"
|
|
||||||
#include "send.hpp"
|
|
||||||
|
|
||||||
#include <opencv2/core.hpp>
|
|
||||||
#include <opencv2/imgproc.hpp>
|
|
||||||
#include <opencv2/imgcodecs.hpp>
|
|
||||||
|
|
||||||
#include <cstdio>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <cstring>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
#include <bitset>
|
|
||||||
#include <atomic>
|
|
||||||
#include <chrono>
|
|
||||||
#include <csignal>
|
|
||||||
#include <linux/videodev2.h>
|
|
||||||
|
|
||||||
std::atomic<uint64_t> g_Feedback_Of_The_Actual_Position_Of_The_Left_Front_Door_391_S;
|
|
||||||
std::atomic<uint64_t> g_Feedback_Of_The_Actual_Position_Of_The_Right_Front_Door_391_S;
|
|
||||||
std::atomic<uint64_t> g_Feedback_Of_The_Actual_Position_Of_The_Left_Rear_Door_391_S;
|
|
||||||
std::atomic<uint64_t> g_Feedback_Of_The_Actual_Position_Of_The_Right_Rear_Door_391_S;
|
|
||||||
|
|
||||||
// Sensing GMSL2 驱动自定义控制 ID
|
|
||||||
#define CID_SENSOR_MODE 0x009a2008
|
|
||||||
#define CID_TRIG_PIN 0x009a2084
|
|
||||||
#define CID_TRIG_MODE 0x009a2085
|
|
||||||
#define CID_GAIN 0x009a2009
|
|
||||||
#define CID_EXPOSURE 0x009a200a
|
|
||||||
|
|
||||||
static volatile sig_atomic_t g_stop = 0;
|
|
||||||
static void onSignal(int) { g_stop = 1; }
|
|
||||||
|
|
||||||
static void printUsage(const char *prog) {
|
|
||||||
printf(
|
|
||||||
"Usage: %s [options]\n"
|
|
||||||
"V4L2 MMAP capture demo for Sensing GMSL2 camera on Jetson Orin.\n"
|
|
||||||
"\n"
|
|
||||||
"Capture options:\n"
|
|
||||||
" -d <device> V4L2 device node (default: /dev/video5)\n"
|
|
||||||
" -W <width> frame width (default: 1920)\n"
|
|
||||||
" -H <height> frame height (default: 1536)\n"
|
|
||||||
" -f <fps> target frame rate (default: 30)\n"
|
|
||||||
" -F <format> pixel format: UYVY | YUYV (default: UYVY)\n"
|
|
||||||
" -m <mode> sensor_mode control value\n"
|
|
||||||
" -p <trig_pin> trig_pin control value (hex/dec)\n"
|
|
||||||
" --gain <val> sensor gain\n"
|
|
||||||
" --exposure <val> sensor exposure\n"
|
|
||||||
"\n"
|
|
||||||
"Output options:\n"
|
|
||||||
" -n <count> frames to capture, 0 = run forever\n"
|
|
||||||
" -s <count> frames to save\n"
|
|
||||||
" -o <dir> output directory (default: ./frames)\n"
|
|
||||||
" -t <type> file type: jpg | png | bmp | raw (default: jpg)\n"
|
|
||||||
" --quality <1..100> JPEG quality (default: 95)\n"
|
|
||||||
" -v verbose output\n"
|
|
||||||
" -h print this help\n"
|
|
||||||
"\n"
|
|
||||||
"CAN options:\n"
|
|
||||||
" --no-can disable USBCANFD CAN/CANFD receive (enabled by default)\n"
|
|
||||||
"\n"
|
|
||||||
"Example:\n"
|
|
||||||
" %s -d /dev/video5 -n 100 -s 5 -t jpg -o ./out\n"
|
|
||||||
" %s -d /dev/video5 -n 0 -v\n",
|
|
||||||
prog, prog, prog);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Args {
|
|
||||||
std::string device = "/dev/video5";
|
|
||||||
uint32_t width = 1920;
|
|
||||||
uint32_t height = 1536; // SG3S-ISX031C-GMSL2F 默认分辨率
|
|
||||||
uint32_t fps = 30;
|
|
||||||
std::string format = "UYVY";
|
|
||||||
long sensor_mode = -1;
|
|
||||||
long trig_pin = -1;
|
|
||||||
long trig_mode = -1;
|
|
||||||
long gain = -1;
|
|
||||||
long exposure = -1;
|
|
||||||
long count = 0;
|
|
||||||
long save = 0;
|
|
||||||
std::string out_dir = "./frames";
|
|
||||||
std::string save_type = "jpg";
|
|
||||||
int quality = 95;
|
|
||||||
bool verbose = false;
|
|
||||||
bool can_enabled = true; // 默认启用 CAN 收发, --no-can 关闭
|
|
||||||
};
|
|
||||||
|
|
||||||
// 手动解析参数
|
|
||||||
static bool parseArgs(int argc, char **argv, Args &a) {
|
|
||||||
auto need_val = [&](int &i) -> std::string {
|
|
||||||
if (i + 1 >= argc) {
|
|
||||||
printf("missing value for %s\n", argv[i]);
|
|
||||||
std::exit(1);
|
|
||||||
}
|
|
||||||
return argv[++i];
|
|
||||||
};
|
|
||||||
|
|
||||||
for (int i = 1; i < argc; ++i) {
|
|
||||||
std::string arg = argv[i];
|
|
||||||
if (arg == "-d") a.device = need_val(i);
|
|
||||||
else if (arg == "-W") a.width = (uint32_t)std::atoi(need_val(i).c_str());
|
|
||||||
else if (arg == "-H") a.height = (uint32_t)std::atoi(need_val(i).c_str());
|
|
||||||
else if (arg == "-f") a.fps = (uint32_t)std::atoi(need_val(i).c_str());
|
|
||||||
else if (arg == "-F") a.format = need_val(i);
|
|
||||||
else if (arg == "-m") a.sensor_mode = std::strtol(need_val(i).c_str(), nullptr, 0);
|
|
||||||
else if (arg == "-p") a.trig_pin = std::strtol(need_val(i).c_str(), nullptr, 0);
|
|
||||||
else if (arg == "-M") a.trig_mode = std::strtol(need_val(i).c_str(), nullptr, 0);
|
|
||||||
else if (arg == "-n") a.count = std::strtol(need_val(i).c_str(), nullptr, 0);
|
|
||||||
else if (arg == "-s") a.save = std::strtol(need_val(i).c_str(), nullptr, 0);
|
|
||||||
else if (arg == "-o") a.out_dir = need_val(i);
|
|
||||||
else if (arg == "-t") a.save_type = need_val(i);
|
|
||||||
else if (arg == "-v") a.verbose = true;
|
|
||||||
else if (arg == "-h") { printUsage(argv[0]); std::exit(0); }
|
|
||||||
else if (arg == "--gain") a.gain = std::strtol(need_val(i).c_str(), nullptr, 0);
|
|
||||||
else if (arg == "--exposure") a.exposure = std::strtol(need_val(i).c_str(), nullptr, 0);
|
|
||||||
else if (arg == "--quality") a.quality = std::atoi(need_val(i).c_str());
|
|
||||||
else if (arg == "--no-can") a.can_enabled = false;
|
|
||||||
else {
|
|
||||||
printf("unknown option: %s\n", arg.c_str());
|
|
||||||
printUsage(argv[0]);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 OpenCV 将 V4L2 帧 (UYVY/YUYV) 转为 BGR Mat
|
|
||||||
static cv::Mat frameToBgr(const V4l2Capture::Frame &frame) {
|
|
||||||
// OpenCV 原生支持 UYVY/YUYV -> BGR 转换
|
|
||||||
// UYVY = COLOR_YUV2BGR_UYVY, YUYV = COLOR_YUV2BGR_YUYV
|
|
||||||
int code = (frame.pixelformat == V4L2_PIX_FMT_UYVY)
|
|
||||||
? cv::COLOR_YUV2BGR_UYVY
|
|
||||||
: cv::COLOR_YUV2BGR_YUYV;
|
|
||||||
cv::Mat yuv((int)frame.height, (int)frame.width, CV_8UC2, (void *)frame.data);
|
|
||||||
cv::Mat bgr;
|
|
||||||
cv::cvtColor(yuv, bgr, code);
|
|
||||||
return bgr;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =====================================================================
|
|
||||||
* CAN 报文接收解析 (由 can_comm 接收线程回调)
|
|
||||||
* ===================================================================== */
|
|
||||||
|
|
||||||
// 512 位报文位域, deal_msg 中按 msg_id 拷贝后可按位解析信号
|
|
||||||
static std::bitset<512> g_bitMsg;
|
|
||||||
|
|
||||||
static uint64_t get_bit_value(int star_bit, int bit_len)
|
|
||||||
{
|
|
||||||
uint64_t signal_value = 0;
|
|
||||||
for (int i = 0; i < bit_len; ++i)
|
|
||||||
{
|
|
||||||
signal_value |= g_bitMsg[star_bit + i] << i;
|
|
||||||
}
|
|
||||||
return signal_value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 收到报文的回调点: 目前仅解析 0x391, 后续信号解析在此扩展
|
|
||||||
static void deal_msg(int64_t time, uint32_t msg_id, const uint8_t *data, uint32_t len)
|
|
||||||
{
|
|
||||||
if (msg_id == 0x391 && len >= 4 && len <= 64)
|
|
||||||
{
|
|
||||||
memcpy(&g_bitMsg, data, len);
|
|
||||||
g_Feedback_Of_The_Actual_Position_Of_The_Left_Front_Door_391_S = data[0];
|
|
||||||
g_Feedback_Of_The_Actual_Position_Of_The_Right_Front_Door_391_S = data[1];
|
|
||||||
g_Feedback_Of_The_Actual_Position_Of_The_Left_Rear_Door_391_S = data[2];
|
|
||||||
g_Feedback_Of_The_Actual_Position_Of_The_Right_Rear_Door_391_S = data[3];
|
|
||||||
// Rear_BCM_0x391,0x391,64,1.0-1.7,8,Feedback_Of_The_Actual_Position_Of_The_Left_Front_Door_391_S
|
|
||||||
// Rear_BCM_0x391,0x391,64,2.0-2.7,8,Feedback_Of_The_Actual_Position_Of_The_Right_Front_Door_391_S
|
|
||||||
// Rear_BCM_0x391,0x391,64,3.0-3.7,8,Feedback_Of_The_Actual_Position_Of_The_Left_Rear_Door_391_S
|
|
||||||
// Rear_BCM_0x391,0x391,64,4.0-4.7,8,Feedback_Of_The_Actual_Position_Of_The_Right_Rear_Door_391_S
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char **argv) {
|
|
||||||
Args args;
|
|
||||||
if (!parseArgs(argc, argv, args)) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::signal(SIGINT, onSignal);
|
|
||||||
std::signal(SIGTERM, onSignal);
|
|
||||||
|
|
||||||
uint32_t pixfmt = (args.format == "UYVY") ? V4L2_PIX_FMT_UYVY : V4L2_PIX_FMT_YUYV;
|
|
||||||
|
|
||||||
// 启动 CAN 收发(失败不致命, 可继续仅取图)
|
|
||||||
if (args.can_enabled) {
|
|
||||||
CanComm::set_msg_callback(deal_msg); // 注册报文解析回调
|
|
||||||
if (!CanComm::can_start()) {
|
|
||||||
printf("CAN start failed, continue with camera only.\n");
|
|
||||||
args.can_enabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
V4l2Capture cap;
|
|
||||||
|
|
||||||
if (!cap.open(args.device)) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置相机控制
|
|
||||||
if (args.sensor_mode >= 0) cap.setControl(CID_SENSOR_MODE, (int32_t)args.sensor_mode);
|
|
||||||
if (args.trig_pin >= 0) cap.setControl(CID_TRIG_PIN, (int32_t)args.trig_pin);
|
|
||||||
if (args.trig_mode >= 0) cap.setControl(CID_TRIG_MODE, (int32_t)args.trig_mode);
|
|
||||||
if (args.gain >= 0) cap.setControl(CID_GAIN, (int32_t)args.gain);
|
|
||||||
if (args.exposure >= 0) cap.setControl(CID_EXPOSURE, (int32_t)args.exposure);
|
|
||||||
|
|
||||||
if (!cap.setFormat(args.width, args.height, pixfmt)) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
cap.setFps(args.fps);
|
|
||||||
|
|
||||||
if (!cap.initMmap(4)) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (!cap.startStreaming()) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
printf("=========================================================\n");
|
|
||||||
printf("Capturing %s %ux%u @ %u fps from %s\n",
|
|
||||||
args.format.c_str(), cap.width(), cap.height(), args.fps, args.device.c_str());
|
|
||||||
printf("Press Ctrl+C to stop.\n");
|
|
||||||
printf("=========================================================\n");
|
|
||||||
|
|
||||||
// 创建输出目录
|
|
||||||
if (args.save > 0) {
|
|
||||||
std::string mkcmd = "mkdir -p " + args.out_dir;
|
|
||||||
std::system(mkcmd.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
using clock = std::chrono::steady_clock;
|
|
||||||
auto t_start = clock::now();
|
|
||||||
auto t_last_report = t_start;
|
|
||||||
uint64_t frame_count = 0;
|
|
||||||
uint64_t save_count = 0;
|
|
||||||
uint32_t timeout_consecutive = 0;
|
|
||||||
|
|
||||||
while (!g_stop) {
|
|
||||||
if (args.count > 0 && frame_count >= (uint64_t)args.count) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
V4l2Capture::Frame frame;
|
|
||||||
if (!cap.getFrame(frame, 2000)) {
|
|
||||||
timeout_consecutive++;
|
|
||||||
if (timeout_consecutive > 5) {
|
|
||||||
printf("getFrame: timeout/empty for %u consecutive tries, aborting\n",
|
|
||||||
timeout_consecutive);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
timeout_consecutive = 0;
|
|
||||||
frame_count++;
|
|
||||||
|
|
||||||
// 保存帧
|
|
||||||
if (save_count < (uint64_t)args.save) {
|
|
||||||
bool ok = false;
|
|
||||||
char fname[512];
|
|
||||||
|
|
||||||
// 使用 OpenCV 转换并保存
|
|
||||||
cv::Mat bgr = frameToBgr(frame);
|
|
||||||
if (!bgr.empty())
|
|
||||||
{
|
|
||||||
std::snprintf(fname, sizeof(fname), "%s/frame_%06lu.%s",
|
|
||||||
args.out_dir.c_str(), save_count, args.save_type.c_str());
|
|
||||||
|
|
||||||
std::vector<int> params;
|
|
||||||
if (args.save_type == "jpg" || args.save_type == "jpeg")
|
|
||||||
{
|
|
||||||
params = {cv::IMWRITE_JPEG_QUALITY, args.quality};
|
|
||||||
}
|
|
||||||
else if (args.save_type == "png")
|
|
||||||
{
|
|
||||||
params = {cv::IMWRITE_PNG_COMPRESSION, 3};
|
|
||||||
}
|
|
||||||
ok = cv::imwrite(fname, bgr, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ok) {
|
|
||||||
printf("[saved] %s\n", fname);
|
|
||||||
save_count++;
|
|
||||||
} else {
|
|
||||||
printf("[save failed] frame %lu\n", save_count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DebugInfo *pData = new DebugInfo();
|
|
||||||
// pData->CameraId = (ctx->cam_no == 1 ? 0 : 1);
|
|
||||||
// pData->TimeStampMs = epochTimeStamp_ms;
|
|
||||||
memcpy(pData->Img, frame.data, frame.size);
|
|
||||||
bool res = send_debuginfo(pData);
|
|
||||||
//printf("send %ld %d %d\n", pData->TimeStampMs, pData->CameraId, res);
|
|
||||||
delete pData;
|
|
||||||
pData = nullptr;
|
|
||||||
|
|
||||||
cap.releaseFrame(frame);
|
|
||||||
|
|
||||||
// if (args.verbose) {
|
|
||||||
// printf("frame #%lu seq=%u size=%zu\n",
|
|
||||||
// frame_count, frame.sequence, frame.size);
|
|
||||||
// }
|
|
||||||
|
|
||||||
auto now = clock::now();
|
|
||||||
auto since_report = std::chrono::duration_cast<std::chrono::milliseconds>(now - t_last_report);
|
|
||||||
if (since_report.count() >= 1000) {
|
|
||||||
double total_sec = std::chrono::duration_cast<std::chrono::milliseconds>(now - t_start).count() / 1000.0;
|
|
||||||
double avg_fps = total_sec > 0 ? frame_count / total_sec : 0;
|
|
||||||
printf("[stats] frames=%lu avg_fps=%.2f door_position[%ld %ld %ld %ld]\n", frame_count, avg_fps, g_Feedback_Of_The_Actual_Position_Of_The_Left_Front_Door_391_S.load(), g_Feedback_Of_The_Actual_Position_Of_The_Right_Front_Door_391_S.load(), g_Feedback_Of_The_Actual_Position_Of_The_Left_Rear_Door_391_S.load(), g_Feedback_Of_The_Actual_Position_Of_The_Right_Rear_Door_391_S.load());
|
|
||||||
t_last_report = now;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto t_end = clock::now();
|
|
||||||
double total_sec = std::chrono::duration_cast<std::chrono::milliseconds>(t_end - t_start).count() / 1000.0;
|
|
||||||
|
|
||||||
printf("=========================================================\n");
|
|
||||||
printf("Capture finished. frames=%lu duration=%.2fs avg_fps=%.2f\n",
|
|
||||||
frame_count, total_sec, total_sec > 0 ? frame_count / total_sec : 0);
|
|
||||||
if (args.save > 0) {
|
|
||||||
printf("Saved %lu frames to %s/\n", save_count, args.out_dir.c_str());
|
|
||||||
}
|
|
||||||
if (args.can_enabled && CanComm::is_open()) {
|
|
||||||
printf("CAN received: %d frames\n", CanComm::receive_count());
|
|
||||||
}
|
|
||||||
printf("=========================================================\n");
|
|
||||||
|
|
||||||
cap.stopStreaming();
|
|
||||||
cap.close();
|
|
||||||
|
|
||||||
if (args.can_enabled) {
|
|
||||||
CanComm::can_stop();
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -56,15 +56,4 @@ std::vector<Box> Detector::infer(cv::Mat const& frame,cudaStream_t stream,int fi
|
|||||||
std::stable_sort(filtered.begin(),filtered.end(),[](auto const&a,auto const&b){return a.x1<b.x1;});
|
std::stable_sort(filtered.begin(),filtered.end(),[](auto const&a,auto const&b){return a.x1<b.x1;});
|
||||||
return filtered;
|
return filtered;
|
||||||
}
|
}
|
||||||
cv::Mat maskOtherPeople(cv::Mat const& frame,std::vector<Box> const& boxes,size_t target) {
|
|
||||||
auto masked=frame.clone(); auto const&t=boxes.at(target);
|
|
||||||
double targetArea=std::max(double(t.x2-t.x1)*(t.y2-t.y1),1.0);
|
|
||||||
for(size_t i=0;i<boxes.size();++i) {
|
|
||||||
auto const& b=boxes[i]; if(i==target||std::max(double(b.x2-b.x1)*(b.y2-b.y1),1.0)>=0.5*targetArea) continue;
|
|
||||||
int x1=std::clamp(int(std::nearbyint(b.x1)),0,frame.cols-1),x2=std::clamp(int(std::nearbyint(b.x2)),0,frame.cols-1);
|
|
||||||
int y1=std::clamp(int(std::nearbyint(b.y1)),0,frame.rows-1),y2=std::clamp(int(std::nearbyint(b.y2)),0,frame.rows-1);
|
|
||||||
if(x2>=x1&&y2>=y1) cv::rectangle(masked,{x1,y1},{x2,y2},{124,116,104},-1);
|
|
||||||
}
|
|
||||||
return masked;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -61,15 +61,6 @@ inline std::vector<std::uint8_t> readBinary(std::filesystem::path const& path) {
|
|||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void writeBinary(std::filesystem::path const& path, void const* data, std::size_t size) {
|
|
||||||
std::filesystem::create_directories(path.parent_path());
|
|
||||||
std::ofstream stream(path, std::ios::binary);
|
|
||||||
if (!stream) {
|
|
||||||
throw std::runtime_error("Cannot write " + path.string());
|
|
||||||
}
|
|
||||||
stream.write(static_cast<char const*>(data), static_cast<std::streamsize>(size));
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::size_t volume(nvinfer1::Dims const& dims) {
|
inline std::size_t volume(nvinfer1::Dims const& dims) {
|
||||||
std::size_t value = 1;
|
std::size_t value = 1;
|
||||||
for (int index = 0; index < dims.nbDims; ++index) {
|
for (int index = 0; index < dims.nbDims; ++index) {
|
||||||
@@ -14,5 +14,4 @@ private:
|
|||||||
std::unique_ptr<YoloPreprocess> preprocess_;
|
std::unique_ptr<YoloPreprocess> preprocess_;
|
||||||
std::vector<float> outputHost_;
|
std::vector<float> outputHost_;
|
||||||
};
|
};
|
||||||
cv::Mat maskOtherPeople(cv::Mat const& frame,std::vector<fangjia::Box> const& boxes,size_t target);
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace cms {
|
||||||
|
// Immutable when published. Stride is explicit; no borrowed camera/GPU memory.
|
||||||
|
struct BgrImage {
|
||||||
|
int width{}, height{}, stride{};
|
||||||
|
std::vector<std::uint8_t> pixels;
|
||||||
|
};
|
||||||
|
} // namespace cms
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fangjia {
|
||||||
|
|
||||||
|
struct Box {
|
||||||
|
float x1{};
|
||||||
|
float y1{};
|
||||||
|
float x2{};
|
||||||
|
float y2{};
|
||||||
|
float score{};
|
||||||
|
int trackId{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Keypoint {
|
||||||
|
float x{};
|
||||||
|
float y{};
|
||||||
|
float score{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PersonPose {
|
||||||
|
Box box;
|
||||||
|
std::array<Keypoint, 133> keypoints{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fangjia
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "pipeline_types.hpp"
|
||||||
|
#include "trt_engine.hpp"
|
||||||
|
|
||||||
|
#include <opencv2/core.hpp>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace fangjia {
|
||||||
|
|
||||||
|
class RtmwEngine {
|
||||||
|
public:
|
||||||
|
RtmwEngine(
|
||||||
|
std::filesystem::path const& enginePath,
|
||||||
|
TrtLogger& logger,
|
||||||
|
int maxSourceWidth,
|
||||||
|
int maxSourceHeight,
|
||||||
|
int maxPeople = 3);
|
||||||
|
~RtmwEngine();
|
||||||
|
|
||||||
|
RtmwEngine(RtmwEngine const&) = delete;
|
||||||
|
RtmwEngine& operator=(RtmwEngine const&) = delete;
|
||||||
|
|
||||||
|
std::vector<PersonPose> infer(
|
||||||
|
cv::Mat const& frame,
|
||||||
|
std::vector<Box> const& boxes,
|
||||||
|
cudaStream_t stream,
|
||||||
|
float* preprocessMs = nullptr,
|
||||||
|
float* inferenceMs = nullptr,
|
||||||
|
float* postprocessMs = nullptr);
|
||||||
|
|
||||||
|
void warmup(cudaStream_t stream);
|
||||||
|
|
||||||
|
private:
|
||||||
|
TrtEngine engine_;
|
||||||
|
int maxSourceWidth_{};
|
||||||
|
int maxSourceHeight_{};
|
||||||
|
int maxPeople_{};
|
||||||
|
std::uint8_t* sourceDevice_{};
|
||||||
|
std::uint8_t* cropsDevice_{};
|
||||||
|
float* networkDevice_{};
|
||||||
|
std::vector<double> centersHost_;
|
||||||
|
std::vector<double> scalesHost_;
|
||||||
|
std::vector<float> simccXHost_;
|
||||||
|
std::vector<float> simccYHost_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fangjia
|
||||||
@@ -16,8 +16,6 @@ public:
|
|||||||
|
|
||||||
void setInputShape(std::string const& name, nvinfer1::Dims const& shape);
|
void setInputShape(std::string const& name, nvinfer1::Dims const& shape);
|
||||||
void prepare();
|
void prepare();
|
||||||
void bindExternal(std::string const& name, void* pointer, std::size_t bytes);
|
|
||||||
void useOwned(std::string const& name);
|
|
||||||
bool enqueue(cudaStream_t stream);
|
bool enqueue(cudaStream_t stream);
|
||||||
|
|
||||||
void* device(std::string const& name);
|
void* device(std::string const& name);
|
||||||
@@ -33,7 +31,6 @@ private:
|
|||||||
void* active{};
|
void* active{};
|
||||||
std::size_t capacity{};
|
std::size_t capacity{};
|
||||||
std::size_t required{};
|
std::size_t required{};
|
||||||
bool external{};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Buffer& buffer(std::string const& name);
|
Buffer& buffer(std::string const& name);
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
#include "pose_engine.hpp"
|
||||||
|
#include "common.hpp"
|
||||||
|
#include "gpu_preprocess.hpp"
|
||||||
|
|
||||||
|
#include <opencv2/imgproc.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace fangjia {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr int kInputWidth = 288;
|
||||||
|
constexpr int kInputHeight = 384;
|
||||||
|
constexpr int kKeypoints = 133;
|
||||||
|
constexpr int kSimccX = 576;
|
||||||
|
constexpr int kSimccY = 768;
|
||||||
|
|
||||||
|
__global__ void poseNormalizeKernel(
|
||||||
|
float* output, std::uint8_t const* input, int people, int inputStep) {
|
||||||
|
int index = blockIdx.x * blockDim.x + threadIdx.x;
|
||||||
|
int pixels = kInputWidth * kInputHeight;
|
||||||
|
int total = people * pixels;
|
||||||
|
if (index >= total) return;
|
||||||
|
int person = index / pixels;
|
||||||
|
int pixel = index - person * pixels;
|
||||||
|
int y = pixel / kInputWidth;
|
||||||
|
int x = pixel - y * kInputWidth;
|
||||||
|
auto const* value = input + person * inputStep * kInputHeight + y * inputStep + x * 3;
|
||||||
|
float* destination = output + static_cast<std::size_t>(person) * 3 * pixels;
|
||||||
|
destination[pixel] = static_cast<float>((static_cast<double>(value[0]) - 123.675) / 58.395);
|
||||||
|
destination[pixels + pixel] = static_cast<float>((static_cast<double>(value[1]) - 116.28) / 57.12);
|
||||||
|
destination[2 * pixels + pixel] = static_cast<float>((static_cast<double>(value[2]) - 103.53) / 57.375);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
RtmwEngine::RtmwEngine(
|
||||||
|
std::filesystem::path const& enginePath, TrtLogger& logger,
|
||||||
|
int maxSourceWidth, int maxSourceHeight, int maxPeople)
|
||||||
|
: engine_(enginePath, logger), maxSourceWidth_(maxSourceWidth),
|
||||||
|
maxSourceHeight_(maxSourceHeight), maxPeople_(maxPeople),
|
||||||
|
centersHost_(maxPeople * 2), scalesHost_(maxPeople * 2),
|
||||||
|
simccXHost_(static_cast<std::size_t>(maxPeople) * kKeypoints * kSimccX),
|
||||||
|
simccYHost_(static_cast<std::size_t>(maxPeople) * kKeypoints * kSimccY) {
|
||||||
|
engine_.setInputShape("input", nvinfer1::Dims4{1,3,kInputHeight,kInputWidth});
|
||||||
|
engine_.prepare();
|
||||||
|
auto xs=engine_.shape("simcc_x"),ys=engine_.shape("simcc_y");
|
||||||
|
if(xs.nbDims!=3||ys.nbDims!=3||xs.d[0]!=1||ys.d[0]!=1||xs.d[1]!=kKeypoints||ys.d[1]!=kKeypoints||xs.d[2]!=kSimccX||ys.d[2]!=kSimccY)
|
||||||
|
throw std::runtime_error("RTMW engine I/O shape does not match the 133-point model");
|
||||||
|
for(auto name:{"input","simcc_x","simcc_y"})if(engine_.dataType(name)!=nvinfer1::DataType::kFLOAT)
|
||||||
|
throw std::runtime_error("RTMW requires float32 I/O bindings");
|
||||||
|
std::size_t const values = static_cast<std::size_t>(maxPeople) * 3 * kInputWidth * kInputHeight;
|
||||||
|
CUDA_CHECK(cudaMalloc(
|
||||||
|
&cropsDevice_, static_cast<std::size_t>(maxPeople_) * kInputWidth * kInputHeight * 3));
|
||||||
|
CUDA_CHECK(cudaMalloc(&networkDevice_, values * sizeof(float)));
|
||||||
|
CUDA_CHECK(cudaMalloc(&sourceDevice_, static_cast<size_t>(maxSourceWidth_)*maxSourceHeight_*3));
|
||||||
|
}
|
||||||
|
|
||||||
|
RtmwEngine::~RtmwEngine() {
|
||||||
|
if (sourceDevice_) cudaFree(sourceDevice_);
|
||||||
|
if (cropsDevice_) cudaFree(cropsDevice_);
|
||||||
|
if (networkDevice_) cudaFree(networkDevice_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RtmwEngine::warmup(cudaStream_t stream) {
|
||||||
|
std::vector<Box> boxes{{0, 0, static_cast<float>(maxSourceWidth_), static_cast<float>(maxSourceHeight_), 1, 1}};
|
||||||
|
cv::Mat black(maxSourceHeight_, maxSourceWidth_, CV_8UC3, cv::Scalar(0, 0, 0));
|
||||||
|
for (int i = 0; i < 2; ++i) infer(black, boxes, stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<PersonPose> RtmwEngine::infer(
|
||||||
|
cv::Mat const& frame, std::vector<Box> const& boxes, cudaStream_t stream,
|
||||||
|
float* preprocessMs, float* inferenceMs, float* postprocessMs) {
|
||||||
|
int batch = std::min(static_cast<int>(boxes.size()), maxPeople_);
|
||||||
|
if (batch <= 0) return {};
|
||||||
|
auto const preprocessStarted = std::chrono::steady_clock::now();
|
||||||
|
int const cropStep = kInputWidth * 3;
|
||||||
|
int const pixels = kInputWidth * kInputHeight;
|
||||||
|
if (frame.cols>maxSourceWidth_ || frame.rows>maxSourceHeight_) throw std::runtime_error("Pose input exceeds allocation");
|
||||||
|
auto npp = cms::nppContext(stream);
|
||||||
|
CUDA_CHECK(cudaMemcpy2DAsync(sourceDevice_, frame.cols*3, frame.data, frame.step, frame.cols*3, frame.rows, cudaMemcpyHostToDevice, stream));
|
||||||
|
for (int i = 0; i < batch; ++i) {
|
||||||
|
auto const& box = boxes[i];
|
||||||
|
double width = (static_cast<double>(box.x2) - static_cast<double>(box.x1)) * 1.25;
|
||||||
|
double height = (static_cast<double>(box.y2) - static_cast<double>(box.y1)) * 1.25;
|
||||||
|
constexpr double aspect = static_cast<double>(kInputWidth) / kInputHeight;
|
||||||
|
if (width > height * aspect) height = width / aspect;
|
||||||
|
else width = height * aspect;
|
||||||
|
centersHost_[i * 2] = 0.5 * (static_cast<double>(box.x1) + static_cast<double>(box.x2));
|
||||||
|
centersHost_[i * 2 + 1] = 0.5 * (static_cast<double>(box.y1) + static_cast<double>(box.y2));
|
||||||
|
scalesHost_[i * 2] = width;
|
||||||
|
scalesHost_[i * 2 + 1] = height;
|
||||||
|
|
||||||
|
cv::Point2f source[3]{
|
||||||
|
{static_cast<float>(centersHost_[i * 2]), static_cast<float>(centersHost_[i * 2 + 1])},
|
||||||
|
{static_cast<float>(centersHost_[i * 2]), static_cast<float>(centersHost_[i * 2 + 1] - width * 0.5)},
|
||||||
|
{static_cast<float>(centersHost_[i * 2] - width * 0.5), static_cast<float>(centersHost_[i * 2 + 1] - width * 0.5)}};
|
||||||
|
cv::Point2f destination[3]{
|
||||||
|
{kInputWidth * 0.5F, kInputHeight * 0.5F},
|
||||||
|
{kInputWidth * 0.5F, kInputHeight * 0.5F - kInputWidth * 0.5F},
|
||||||
|
{0.0F, kInputHeight * 0.5F - kInputWidth * 0.5F}};
|
||||||
|
cv::Mat affine = cv::getAffineTransform(source, destination);
|
||||||
|
auto* cropDevice=cropsDevice_+static_cast<size_t>(i)*cropStep*kInputHeight;
|
||||||
|
Npp8u fill[3]={0,0,0};
|
||||||
|
cms::nppCheck(nppiSet_8u_C3R_Ctx(fill,cropDevice,cropStep,{kInputWidth,kInputHeight},npp));
|
||||||
|
double coeff[2][3];
|
||||||
|
for(int row=0;row<2;++row) for(int col=0;col<3;++col) coeff[row][col]=affine.at<double>(row,col);
|
||||||
|
cms::nppCheck(nppiWarpAffine_8u_C3R_Ctx(sourceDevice_,{frame.cols,frame.rows},frame.cols*3,{0,0,frame.cols,frame.rows},cropDevice,cropStep,{0,0,kInputWidth,kInputHeight},coeff,NPPI_INTER_LINEAR,npp));
|
||||||
|
}
|
||||||
|
int const totalPixels = batch * pixels;
|
||||||
|
poseNormalizeKernel<<<(totalPixels + 255) / 256, 256, 0, stream>>>(
|
||||||
|
networkDevice_, cropsDevice_, batch, cropStep);
|
||||||
|
CUDA_CHECK(cudaGetLastError());
|
||||||
|
CUDA_CHECK(cudaStreamSynchronize(stream));
|
||||||
|
if (preprocessMs) {
|
||||||
|
*preprocessMs = static_cast<float>(std::chrono::duration<double, std::milli>(
|
||||||
|
std::chrono::steady_clock::now() - preprocessStarted).count());
|
||||||
|
}
|
||||||
|
|
||||||
|
// RTMPose's Python API invokes the ONNX/TensorRT session once per person.
|
||||||
|
// Preserve that execution contract instead of batching people together.
|
||||||
|
engine_.setInputShape("input", nvinfer1::Dims4{1, 3, kInputHeight, kInputWidth});
|
||||||
|
engine_.prepare();
|
||||||
|
CudaEventTimer timer;
|
||||||
|
float inferTime = 0.0F;
|
||||||
|
std::size_t const inputValues = static_cast<std::size_t>(3) * pixels;
|
||||||
|
std::size_t const xValues = static_cast<std::size_t>(kKeypoints) * kSimccX;
|
||||||
|
std::size_t const yValues = static_cast<std::size_t>(kKeypoints) * kSimccY;
|
||||||
|
for (int person = 0; person < batch; ++person) {
|
||||||
|
CUDA_CHECK(cudaMemcpyAsync(
|
||||||
|
engine_.device("input"), networkDevice_ + static_cast<std::size_t>(person) * inputValues,
|
||||||
|
inputValues * sizeof(float), cudaMemcpyDeviceToDevice, stream));
|
||||||
|
timer.start(stream);
|
||||||
|
if (!engine_.enqueue(stream)) throw std::runtime_error("RTMW enqueue failed");
|
||||||
|
inferTime += timer.stop(stream);
|
||||||
|
CUDA_CHECK(cudaMemcpyAsync(
|
||||||
|
simccXHost_.data() + static_cast<std::size_t>(person) * xValues,
|
||||||
|
engine_.device("simcc_x"), xValues * sizeof(float), cudaMemcpyDeviceToHost, stream));
|
||||||
|
CUDA_CHECK(cudaMemcpyAsync(
|
||||||
|
simccYHost_.data() + static_cast<std::size_t>(person) * yValues,
|
||||||
|
engine_.device("simcc_y"), yValues * sizeof(float), cudaMemcpyDeviceToHost, stream));
|
||||||
|
}
|
||||||
|
if (inferenceMs) *inferenceMs = inferTime;
|
||||||
|
|
||||||
|
auto const postprocessStarted = std::chrono::steady_clock::now();
|
||||||
|
CUDA_CHECK(cudaStreamSynchronize(stream));
|
||||||
|
std::vector<PersonPose> result(batch);
|
||||||
|
for (int person = 0; person < batch; ++person) {
|
||||||
|
result[person].box = boxes[person];
|
||||||
|
for (int keypoint = 0; keypoint < kKeypoints; ++keypoint) {
|
||||||
|
auto xBegin = simccXHost_.begin() + (person * kKeypoints + keypoint) * kSimccX;
|
||||||
|
auto yBegin = simccYHost_.begin() + (person * kKeypoints + keypoint) * kSimccY;
|
||||||
|
auto xIt = std::max_element(xBegin, xBegin + kSimccX);
|
||||||
|
auto yIt = std::max_element(yBegin, yBegin + kSimccY);
|
||||||
|
float score = 0.5F * (*xIt + *yIt);
|
||||||
|
if(!std::isfinite(score))throw std::runtime_error("Non-finite RTMW output");
|
||||||
|
float localX = score > 0.0F ? static_cast<float>(xIt - xBegin) * 0.5F : -0.5F;
|
||||||
|
float localY = score > 0.0F ? static_cast<float>(yIt - yBegin) * 0.5F : -0.5F;
|
||||||
|
double scaleX = scalesHost_[person * 2];
|
||||||
|
double scaleY = scalesHost_[person * 2 + 1];
|
||||||
|
result[person].keypoints[keypoint] = {
|
||||||
|
static_cast<float>(static_cast<double>(localX) / kInputWidth * scaleX + centersHost_[person * 2] - scaleX * 0.5),
|
||||||
|
static_cast<float>(static_cast<double>(localY) / kInputHeight * scaleY + centersHost_[person * 2 + 1] - scaleY * 0.5),
|
||||||
|
score};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
float post = static_cast<float>(std::chrono::duration<double, std::milli>(
|
||||||
|
std::chrono::steady_clock::now() - postprocessStarted).count());
|
||||||
|
if (postprocessMs) *postprocessMs = post;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fangjia
|
||||||
@@ -44,12 +44,6 @@ void TrtEngine::prepare() {
|
|||||||
}
|
}
|
||||||
auto& item = buffer(name);
|
auto& item = buffer(name);
|
||||||
item.required = volume(dims) * dataTypeSize(engine_->getTensorDataType(name.c_str()));
|
item.required = volume(dims) * dataTypeSize(engine_->getTensorDataType(name.c_str()));
|
||||||
if (item.external) {
|
|
||||||
if (item.capacity < item.required) {
|
|
||||||
throw std::runtime_error("External buffer is too small for " + name);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (item.capacity < item.required) {
|
if (item.capacity < item.required) {
|
||||||
if (item.owned) CUDA_CHECK(cudaFree(item.owned));
|
if (item.owned) CUDA_CHECK(cudaFree(item.owned));
|
||||||
CUDA_CHECK(cudaMalloc(&item.owned, item.required));
|
CUDA_CHECK(cudaMalloc(&item.owned, item.required));
|
||||||
@@ -59,19 +53,6 @@ void TrtEngine::prepare() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TrtEngine::bindExternal(std::string const& name, void* pointer, std::size_t bytesValue) {
|
|
||||||
auto& item = buffer(name);
|
|
||||||
item.active = pointer;
|
|
||||||
item.capacity = bytesValue;
|
|
||||||
item.external = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TrtEngine::useOwned(std::string const& name) {
|
|
||||||
auto& item = buffer(name);
|
|
||||||
item.external = false;
|
|
||||||
item.active = item.owned;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TrtEngine::enqueue(cudaStream_t stream) {
|
bool TrtEngine::enqueue(cudaStream_t stream) {
|
||||||
for (int index = 0; index < engine_->getNbIOTensors(); ++index) {
|
for (int index = 0; index < engine_->getNbIOTensors(); ++index) {
|
||||||
std::string const name = engine_->getIOTensorName(index);
|
std::string const name = engine_->getIOTensorName(index);
|
||||||
+17
-23
@@ -1,39 +1,33 @@
|
|||||||
# Orin 可直接运行包 — use-can
|
# Orin 直接运行包
|
||||||
|
|
||||||
这个目录可以单独复制到 Orin 的任意目录,不依赖兄弟目录 0920/0923,不需要重新编译。
|
适用 Jetson AGX Orin / JetPack 5.1.2 (R35.4.1)、CUDA 11.4、TensorRT 8.5.x、OpenCV 4.5.4。
|
||||||
包含板端编译的 TensorRT/CUDA 原生库、YOLO/RTMW engine、Python 风险代码、CAN USB 库、标定文件和原有 GMSL 驱动。
|
系统需要 Python 3.8+、numpy、cv2。无需重新编译,可单独复制整个目录。
|
||||||
|
|
||||||
环境:Jetson AGX Orin,JetPack 5.1.2 / L4T R35.4.1,Linux 5.10.120-tegra,CUDA 11.4,TensorRT 8.5.x,系统 OpenCV 4.5.4 库;Python 3.8+,numpy、cv2。系统 CUDA/TensorRT/OpenCV 运行库由 JetPack 提供,未重复打包。
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd orin
|
|
||||||
sha256sum -c SHA256SUMS
|
sha256sum -c SHA256SUMS
|
||||||
./scripts/initialize_camera.sh # 开机后无 /dev/video5 时执行;需要 sudo 权限
|
./scripts/initialize_camera.sh # 开机后无 /dev/video5 时执行,需要 sudo
|
||||||
./run.sh # 默认 CAN 左侧、无可视化,结果保存在内存
|
./run.sh # 默认 CAN 模式,结果保存在内存
|
||||||
./run.sh --visualize # 可选浏览器预览,默认端口 8765
|
./run.sh --visualize # 可选浏览器预览,http://设备IP:8765
|
||||||
```
|
```
|
||||||
|
|
||||||
Ctrl+C / SIGTERM 退出。仅有一个进程可以占用同一个摄像头。
|
只运行一个摄像头采集进程,Ctrl+C / SIGTERM 停止。
|
||||||
|
|
||||||
```bash
|
- data[0..3]:左前、右前、左后、右后;`--can-side left/right` 选择摄像头侧。
|
||||||
./run.sh --no-can --active-door both --duration 10
|
- 未收到有效 CAN 或超时:CAN_UNKNOWN,score=null;不发送执行器控制报文。
|
||||||
./run.sh --save-results --output ./outputs/session
|
- `--save-results` 保存 JSONL;`--save-snapshots` 保存快照;`--write-video` 保存叠加视频。默认均关闭。
|
||||||
./run.sh --visualize --save-results --save-snapshots --output ./outputs/session
|
- `--output PATH` 指定输出目录;`--duration 30` 限时运行。完整参数见 `./run.sh --help`。
|
||||||
```
|
- 标定参数仍需匹配实际机位,默认 calibration_verified=false。
|
||||||
|
|
||||||
CAN 0x391 的 data[0..3] 顺序为左前、右前、左后、右后。`--can-side right` 切换右侧。未收到有效 CAN 或报文过期时输出 CAN_UNKNOWN,不发送执行器控制报文。
|
同进程下游示例(先停止其他采集进程):
|
||||||
标定文件仍为研发机位,默认 calibration_verified=false。
|
|
||||||
|
|
||||||
同进程下游接口:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from run_camera import CameraPipeline, parse_args
|
from run_camera import CameraPipeline, parse_args
|
||||||
with CameraPipeline(parse_args([])) as pipeline:
|
with CameraPipeline(parse_args([])) as pipeline:
|
||||||
result = pipeline.wait_for_result(timeout=10)
|
result = pipeline.wait_for_result(timeout=10)
|
||||||
result = pipeline.latest_result # 等价于 pipeline.latest()
|
result = pipeline.latest_result # 等价于 pipeline.latest()
|
||||||
if result is not None:
|
if result is not None:
|
||||||
print(result['risk'], result['door_risks'])
|
print(result['risk'], result['door_risks'], result['skeletons'])
|
||||||
```
|
```
|
||||||
|
|
||||||
持续读取示例:`python3 examples/read_latest.py`(先停止其他采集进程)。`latest_result` 是单个只读快照;它不是跨进程共享变量。
|
变量是线程安全的最新只读快照,读取不会消费结果,不是跨进程共享变量。
|
||||||
TensorRT engine 绑定目标 GPU/TRT 环境,不能直接用于 Thor、其他 JetPack 版本或桌面 GPU。
|
系统 CUDA/TensorRT/OpenCV 运行库由 JetPack 提供。引擎和驱动不能直接用于其他 GPU 或不匹配的内核。
|
||||||
|
|||||||
+11
-13
@@ -1,27 +1,25 @@
|
|||||||
4171567ecbea2f15b3c945b8d8363362d9f422b8c732d33ee1e15461b40014da README.md
|
b6295cf4b0705c5c3d83c9ba35e41b2a0b865ea67742c09cfd1690f263f556d0 README.md
|
||||||
9ba768d559fd3b64e833b50c62c70f2adfd6c169620aea5fba32cfb1b4c5b0e9 annotations/constant_door_mask.json
|
9ba768d559fd3b64e833b50c62c70f2adfd6c169620aea5fba32cfb1b4c5b0e9 annotations/constant_door_mask.json
|
||||||
3544fb1c7b1b27b39db27d8f849d5e8652f5d09a950a59c5dd0c7703086b4f6b annotations/door_zone_multifeature.json
|
3544fb1c7b1b27b39db27d8f849d5e8652f5d09a950a59c5dd0c7703086b4f6b annotations/door_zone_multifeature.json
|
||||||
6d5e874fd27fcfd75d303d7e35d9672028af71041f52dcc82a1e83676faefa65 bin/drivers/max9295.ko
|
6d5e874fd27fcfd75d303d7e35d9672028af71041f52dcc82a1e83676faefa65 bin/drivers/max9295.ko
|
||||||
8a78d04ef8be44e9b62a5e4ee315d3d6d679e34aedff02a234bbf4d40e222933 bin/drivers/max9296.ko
|
8a78d04ef8be44e9b62a5e4ee315d3d6d679e34aedff02a234bbf4d40e222933 bin/drivers/max9296.ko
|
||||||
3da628587477cc35d5e3f8aee86e718e2ddf16808e893e00ba6e164b2307e843 bin/drivers/sgx-yuv-gmsl2.ko
|
3da628587477cc35d5e3f8aee86e718e2ddf16808e893e00ba6e164b2307e843 bin/drivers/sgx-yuv-gmsl2.ko
|
||||||
cd75632beaaf81df2f59ce3e83fb335c21cc745068ef2b721c74cc3b2b03db26 build/libvision_native.so
|
5ba7e4f0371a411cc1a6dfd1275e3b824032a14d07a9ccd7ca3d5a6630b322ef build/libvision_native.so
|
||||||
49cc1067c1fa9f82a3d57dbfa77a55a54e5e5062ceae7ae84144b77745db79cf examples/read_latest.py
|
|
||||||
8dae8c557deadbe6bd7241c0dcd7025cb007077ee39a79b115e953a2b22cd9c1 lib/libusb-1.0.so
|
8dae8c557deadbe6bd7241c0dcd7025cb007077ee39a79b115e953a2b22cd9c1 lib/libusb-1.0.so
|
||||||
ab8b7f7249d5fb73d1d33d14fa7532a522088ffb6c25f37ba334daf4c6b4d313 lib/libusbcanfd.so
|
ab8b7f7249d5fb73d1d33d14fa7532a522088ffb6c25f37ba334daf4c6b4d313 lib/libusbcanfd.so
|
||||||
ebcd09e8c2e35008adbb0c1d222e580b17e0df1c8093df3481ba3374cd9a8174 models/engines/rtmw_l_384.engine
|
ebcd09e8c2e35008adbb0c1d222e580b17e0df1c8093df3481ba3374cd9a8174 models/engines/rtmw_l_384.engine
|
||||||
f6a65b926ac1144484f4aa93cd26d7997d32d62310ab93fe867f2bd4412222e9 models/engines/yolo11s.engine
|
f6a65b926ac1144484f4aa93cd26d7997d32d62310ab93fe867f2bd4412222e9 models/engines/yolo11s.engine
|
||||||
fd3ae92255abf95ef037a1060ee75897f9705b4040f9394c3130f6ab860667e1 pipeline/compare_rtmw_ml.py
|
|
||||||
6d6d14a972bc4e5e9f8256514169e73ba0392a301cacd1221eaa26146aad00d2 pipeline/door_mask_risk.py
|
6d6d14a972bc4e5e9f8256514169e73ba0392a301cacd1221eaa26146aad00d2 pipeline/door_mask_risk.py
|
||||||
46d70e1e90a1f36d544b7492ef1fbf08821377a1c460a6683f42d4d7006055c7 pipeline/door_zone_risk.py
|
46d70e1e90a1f36d544b7492ef1fbf08821377a1c460a6683f42d4d7006055c7 pipeline/door_zone_risk.py
|
||||||
bc74a400ce5c5608c2fa49a69ccd8b41ec8633cd6a8239d9d594b8d9e5c3732a pipeline/fuse_rtmw_l_temporal.py
|
|
||||||
bcf2cc99dd27c8749801a5b743656767d9d64ef03d568d227cb654c573d70b00 pipeline/latest_result.py
|
bcf2cc99dd27c8749801a5b743656767d9d64ef03d568d227cb654c573d70b00 pipeline/latest_result.py
|
||||||
cc8ea36d543896cd90f7a00822f06fc67771a1811859a4087995ac372df30276 pipeline/live_pose_engine.py
|
294cdc11aad0e5475275fd8dece2057e229b9bdedba69c26afa62176fd51a4e9 pipeline/native_backend.py
|
||||||
da299c11bd25127a09ffa738876f1de6e0d53d27add2840aeb6092ab0fe7016b pipeline/native_backend.py
|
8b8c5b8c9c76c9bd2e1efa6cba60be337a18c9366e8f748038465e9e2fba2672 pipeline/pose_engine.py
|
||||||
81088b8bc072162bec13f57c65eb0d396e6d1e1074a83f92bcf29e393e27e11f pipeline/occlusion_aware_pose_v2.py
|
93d3323ea431ed1d3c8397cfed7d800d65ec6d7700690487335861945040a486 pipeline/pose_fusion.py
|
||||||
059e2a67c17ce63354986d3242a5d361103ee4241c5391f18140eeb19e47c939 pipeline/predict_optimized_pose_videos.py
|
2ebf58114835ba7c4779d4b66aeff65b8a94107128989c0336de556a4cf95fda pipeline/pose_geometry.py
|
||||||
bf8bb754cd841a52b4410f958a651b005de36ac4d17cc0992fbe1c90ed30caca pipeline/rtmw_l_person_mask_ablation.py
|
f32fa5bebf893a3dc6b9ccfaa09a8e3a153ad5b36dde5373fdc561f10f9e526b pipeline/pose_occlusion.py
|
||||||
1bb58d851ba5d684527b8d1e1940db6241d2b0b2862687fa3b151899a65320d2 pipeline/rtmw_risk_pipeline.py
|
1a3f1f55d7b8a3f0ac96e69bf77f02f82017e8970adef96181d2bfade26e5bd5 pipeline/pose_tracking.py
|
||||||
|
dcf3cce5562ea52400ffa9842fdddaadac1efd672dabb0ed12ce34f20f779f37 pipeline/visualization.py
|
||||||
8a10e035334ed97b7480f558aff7b0464c255ac6138dd99f55a08ade68853e0e run.sh
|
8a10e035334ed97b7480f558aff7b0464c255ac6138dd99f55a08ade68853e0e run.sh
|
||||||
4393f367b4ac7e5a00908043ffd21d5573cc12ed19b956fd101897ada22afd45 run_camera.py
|
23e9af5e82999889fe77c762a77c77036d1b1b699525ff2bd5ca8a418183ba50 run_camera.py
|
||||||
11b5fbc3c17c0610ec0514a0a26cf5d92588d15c0e50e7086d9db5a5c0306e99 scripts/initialize_camera.sh
|
11b5fbc3c17c0610ec0514a0a26cf5d92588d15c0e50e7086d9db5a5c0306e99 scripts/initialize_camera.sh
|
||||||
4cc40e244495e1de60a0429a8fc58cdf383ccda471955e53360a1c36a278c822 scripts/initialize_camera_impl.sh
|
bce5a22d49633454ed2c6bc92b608edf3b718af2424c283ed8f56ba93987200a scripts/initialize_camera_impl.sh
|
||||||
|
|||||||
Binary file not shown.
@@ -1,36 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Same-process downstream example. Run on Orin, which owns the camera/GPU."""
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
||||||
from run_camera import CameraPipeline, parse_args
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# No visualization or disk output by default. CLI flags can enable either.
|
|
||||||
pipeline = CameraPipeline(parse_args())
|
|
||||||
last_frame = -1
|
|
||||||
try:
|
|
||||||
pipeline.start()
|
|
||||||
while True:
|
|
||||||
# Reading the variable is enough for a consumer with its own clock:
|
|
||||||
# result = pipeline.latest_result # equivalent to pipeline.latest()
|
|
||||||
# This sample waits to avoid busy polling; still only the latest
|
|
||||||
# result is returned, without queuing/acknowledging individual frames.
|
|
||||||
result = pipeline.wait_for_result(last_frame, timeout=1.0)
|
|
||||||
if result is not None:
|
|
||||||
last_frame = result['frame_index']
|
|
||||||
print(last_frame, result['risk']['risk_type'],
|
|
||||||
result['risk']['risk_score'], flush=True)
|
|
||||||
# Downstream code uses result['door_risks'], ['skeletons'], ['can'].
|
|
||||||
elif not pipeline.running():
|
|
||||||
break
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
pipeline.request_stop()
|
|
||||||
pipeline.join() # Also propagates worker errors to the caller.
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,384 +0,0 @@
|
|||||||
"""Compare RTMW-M 256x192 and RTMW-L 384x288 on cached YOLO boxes.
|
|
||||||
|
|
||||||
This is an experiment script. It intentionally keeps the candidate weights
|
|
||||||
outside the formal models/ directory and does not change deployment code.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import math
|
|
||||||
import time
|
|
||||||
from collections import defaultdict
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import cv2
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
|
|
||||||
BODY_EDGES = ((5, 7), (7, 9), (6, 8), (8, 10))
|
|
||||||
HAND_EDGES_LOCAL = (
|
|
||||||
(0, 1), (1, 2), (2, 3), (3, 4),
|
|
||||||
(0, 5), (5, 6), (6, 7), (7, 8),
|
|
||||||
(0, 9), (9, 10), (10, 11), (11, 12),
|
|
||||||
(0, 13), (13, 14), (14, 15), (15, 16),
|
|
||||||
(0, 17), (17, 18), (18, 19), (19, 20),
|
|
||||||
)
|
|
||||||
HAND_SLICES = {"left": (91, 112), "right": (112, 133)}
|
|
||||||
BODY_WRISTS = {"left": 9, "right": 10}
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("--video", type=Path, required=True)
|
|
||||||
parser.add_argument("--boxes", type=Path, required=True)
|
|
||||||
parser.add_argument("--model-m", type=Path, required=True)
|
|
||||||
parser.add_argument("--model-l", type=Path, required=True)
|
|
||||||
parser.add_argument("--output", type=Path, required=True)
|
|
||||||
parser.add_argument("--max-people", type=int, default=2)
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def load_boxes(path: Path, max_people: int) -> dict[int, list[list[float]]]:
|
|
||||||
frames: dict[int, list[list[float]]] = {}
|
|
||||||
with path.open("r", encoding="utf-8") as handle:
|
|
||||||
for line in handle:
|
|
||||||
item = json.loads(line)
|
|
||||||
boxes = [entry["xyxy"] for entry in item["boxes"]]
|
|
||||||
frames[int(item["frame_index"])] = sorted(boxes, key=lambda b: b[0])[:max_people]
|
|
||||||
return frames
|
|
||||||
|
|
||||||
|
|
||||||
def point(pt: np.ndarray) -> tuple[int, int]:
|
|
||||||
return int(round(float(pt[0]))), int(round(float(pt[1])))
|
|
||||||
|
|
||||||
|
|
||||||
def draw_pose(
|
|
||||||
image: np.ndarray,
|
|
||||||
boxes: list[list[float]],
|
|
||||||
keypoints: np.ndarray,
|
|
||||||
title: str,
|
|
||||||
) -> np.ndarray:
|
|
||||||
canvas = image.copy()
|
|
||||||
for person_idx, (bbox, kpts) in enumerate(zip(boxes, keypoints), start=1):
|
|
||||||
x1, y1, x2, y2 = (int(round(value)) for value in bbox)
|
|
||||||
cv2.rectangle(canvas, (x1, y1), (x2, y2), (0, 220, 255), 2)
|
|
||||||
cv2.putText(
|
|
||||||
canvas,
|
|
||||||
f"P{person_idx}",
|
|
||||||
(x1 + 4, max(22, y1 + 24)),
|
|
||||||
cv2.FONT_HERSHEY_SIMPLEX,
|
|
||||||
0.75,
|
|
||||||
(0, 220, 255),
|
|
||||||
2,
|
|
||||||
cv2.LINE_AA,
|
|
||||||
)
|
|
||||||
|
|
||||||
for edge_idx, (a, b) in enumerate(BODY_EDGES):
|
|
||||||
color = (40, 40, 255) if edge_idx < 2 else (30, 230, 30)
|
|
||||||
cv2.line(canvas, point(kpts[a]), point(kpts[b]), color, 3, cv2.LINE_AA)
|
|
||||||
for idx in (5, 6, 7, 8, 9, 10):
|
|
||||||
color = (40, 40, 255) if idx in (5, 7, 9) else (30, 230, 30)
|
|
||||||
cv2.circle(canvas, point(kpts[idx]), 5, color, -1, cv2.LINE_AA)
|
|
||||||
|
|
||||||
for side, (start, end) in HAND_SLICES.items():
|
|
||||||
hand = kpts[start:end]
|
|
||||||
color = (255, 80, 220) if side == "left" else (255, 190, 40)
|
|
||||||
for a, b in HAND_EDGES_LOCAL:
|
|
||||||
cv2.line(canvas, point(hand[a]), point(hand[b]), color, 2, cv2.LINE_AA)
|
|
||||||
for hand_point in hand:
|
|
||||||
cv2.circle(canvas, point(hand_point), 2, color, -1, cv2.LINE_AA)
|
|
||||||
|
|
||||||
cv2.rectangle(canvas, (0, 0), (610, 45), (0, 0, 0), -1)
|
|
||||||
cv2.putText(
|
|
||||||
canvas,
|
|
||||||
title,
|
|
||||||
(10, 31),
|
|
||||||
cv2.FONT_HERSHEY_SIMPLEX,
|
|
||||||
0.8,
|
|
||||||
(255, 255, 255),
|
|
||||||
2,
|
|
||||||
cv2.LINE_AA,
|
|
||||||
)
|
|
||||||
return canvas
|
|
||||||
|
|
||||||
|
|
||||||
def normalized_keypoints(kpts: np.ndarray, bbox: list[float]) -> np.ndarray:
|
|
||||||
x1, y1, x2, y2 = bbox
|
|
||||||
width = max(x2 - x1, 1.0)
|
|
||||||
height = max(y2 - y1, 1.0)
|
|
||||||
result = kpts.copy().astype(np.float64)
|
|
||||||
result[:, 0] = (result[:, 0] - x1) / width
|
|
||||||
result[:, 1] = (result[:, 1] - y1) / height
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def hand_span(kpts: np.ndarray, bbox: list[float], side: str) -> float:
|
|
||||||
start, end = HAND_SLICES[side]
|
|
||||||
hand = kpts[start:end]
|
|
||||||
span = math.hypot(
|
|
||||||
float(np.ptp(hand[:, 0])),
|
|
||||||
float(np.ptp(hand[:, 1])),
|
|
||||||
)
|
|
||||||
x1, y1, x2, y2 = bbox
|
|
||||||
return span / max(math.hypot(x2 - x1, y2 - y1), 1.0)
|
|
||||||
|
|
||||||
|
|
||||||
def summarize(values: list[float]) -> dict[str, float | int | None]:
|
|
||||||
if not values:
|
|
||||||
return {"count": 0, "median": None, "p95": None, "max": None}
|
|
||||||
array = np.asarray(values, dtype=np.float64)
|
|
||||||
return {
|
|
||||||
"count": int(array.size),
|
|
||||||
"median": round(float(np.median(array)), 5),
|
|
||||||
"p95": round(float(np.percentile(array, 95)), 5),
|
|
||||||
"max": round(float(np.max(array)), 5),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def temporal_metrics(records: list[dict]) -> dict:
|
|
||||||
by_model_person: dict[tuple[str, int], list[dict]] = defaultdict(list)
|
|
||||||
for record in records:
|
|
||||||
for model_name in ("M", "L"):
|
|
||||||
for person_idx in range(len(record[model_name]["keypoints"])):
|
|
||||||
by_model_person[(model_name, person_idx)].append(
|
|
||||||
{
|
|
||||||
"frame": record["frame_index"],
|
|
||||||
"bbox": record["boxes"][person_idx],
|
|
||||||
"kpts": np.asarray(
|
|
||||||
record[model_name]["keypoints"][person_idx], dtype=np.float64
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
result: dict[str, dict] = {"M": {}, "L": {}}
|
|
||||||
for (model_name, person_idx), sequence in by_model_person.items():
|
|
||||||
upper_steps: list[float] = []
|
|
||||||
wrist_steps = {"left": [], "right": []}
|
|
||||||
hand_steps = {"left": [], "right": []}
|
|
||||||
hand_spans = {"left": [], "right": []}
|
|
||||||
bone_lengths = {"left_upper": [], "left_forearm": [],
|
|
||||||
"right_upper": [], "right_forearm": []}
|
|
||||||
|
|
||||||
previous: np.ndarray | None = None
|
|
||||||
for entry in sequence:
|
|
||||||
normalized = normalized_keypoints(entry["kpts"], entry["bbox"])
|
|
||||||
if previous is not None:
|
|
||||||
steps = np.linalg.norm(normalized - previous, axis=1)
|
|
||||||
upper_steps.extend(float(steps[idx]) for idx in (5, 6, 7, 8, 9, 10))
|
|
||||||
for side, idx in BODY_WRISTS.items():
|
|
||||||
wrist_steps[side].append(float(steps[idx]))
|
|
||||||
for side, (start, end) in HAND_SLICES.items():
|
|
||||||
hand_steps[side].extend(float(value) for value in steps[start:end])
|
|
||||||
previous = normalized
|
|
||||||
|
|
||||||
hand_spans["left"].append(hand_span(entry["kpts"], entry["bbox"], "left"))
|
|
||||||
hand_spans["right"].append(hand_span(entry["kpts"], entry["bbox"], "right"))
|
|
||||||
bone_lengths["left_upper"].append(float(np.linalg.norm(normalized[5] - normalized[7])))
|
|
||||||
bone_lengths["left_forearm"].append(float(np.linalg.norm(normalized[7] - normalized[9])))
|
|
||||||
bone_lengths["right_upper"].append(float(np.linalg.norm(normalized[6] - normalized[8])))
|
|
||||||
bone_lengths["right_forearm"].append(float(np.linalg.norm(normalized[8] - normalized[10])))
|
|
||||||
|
|
||||||
span_summary = {}
|
|
||||||
for side, values in hand_spans.items():
|
|
||||||
median = float(np.median(values))
|
|
||||||
gross_limit = max(0.2, median * 2.5)
|
|
||||||
span_summary[side] = {
|
|
||||||
**summarize(values),
|
|
||||||
"gross_limit": round(gross_limit, 5),
|
|
||||||
"gross_outlier_frames": int(sum(value > gross_limit for value in values)),
|
|
||||||
}
|
|
||||||
|
|
||||||
bone_summary = {}
|
|
||||||
for name, values in bone_lengths.items():
|
|
||||||
array = np.asarray(values, dtype=np.float64)
|
|
||||||
mean = float(np.mean(array))
|
|
||||||
bone_summary[name] = {
|
|
||||||
**summarize(values),
|
|
||||||
"coefficient_of_variation": round(float(np.std(array) / max(mean, 1e-9)), 5),
|
|
||||||
}
|
|
||||||
|
|
||||||
result[model_name][f"P{person_idx + 1}"] = {
|
|
||||||
"upper_body_frame_step": summarize(upper_steps),
|
|
||||||
"wrist_frame_step": {side: summarize(values) for side, values in wrist_steps.items()},
|
|
||||||
"hand_landmark_frame_step": {
|
|
||||||
side: summarize(values) for side, values in hand_steps.items()
|
|
||||||
},
|
|
||||||
"hand_span_relative_to_person": span_summary,
|
|
||||||
"bone_length_relative_to_box": bone_summary,
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def model_disagreement(records: list[dict]) -> tuple[dict, list[int]]:
|
|
||||||
values: dict[str, list[float]] = defaultdict(list)
|
|
||||||
per_frame: list[tuple[float, int]] = []
|
|
||||||
for record in records:
|
|
||||||
frame_max = 0.0
|
|
||||||
for person_idx, bbox in enumerate(record["boxes"]):
|
|
||||||
m = normalized_keypoints(
|
|
||||||
np.asarray(record["M"]["keypoints"][person_idx]), bbox
|
|
||||||
)
|
|
||||||
l = normalized_keypoints(
|
|
||||||
np.asarray(record["L"]["keypoints"][person_idx]), bbox
|
|
||||||
)
|
|
||||||
distance = np.linalg.norm(m - l, axis=1)
|
|
||||||
values[f"P{person_idx + 1}_upper"].extend(
|
|
||||||
float(distance[idx]) for idx in (5, 6, 7, 8, 9, 10)
|
|
||||||
)
|
|
||||||
for side, idx in BODY_WRISTS.items():
|
|
||||||
values[f"P{person_idx + 1}_{side}_wrist"].append(float(distance[idx]))
|
|
||||||
frame_max = max(frame_max, float(np.max(distance[[5, 6, 7, 8, 9, 10]])))
|
|
||||||
per_frame.append((frame_max, record["frame_index"]))
|
|
||||||
worst_frames = [frame for _, frame in sorted(per_frame, reverse=True)[:12]]
|
|
||||||
return {name: summarize(items) for name, items in values.items()}, worst_frames
|
|
||||||
|
|
||||||
|
|
||||||
def write_montage(
|
|
||||||
video_path: Path,
|
|
||||||
records: list[dict],
|
|
||||||
selected_frames: list[int],
|
|
||||||
output_path: Path,
|
|
||||||
titles: tuple[str, str] = ("RTMW-M 256x192", "RTMW-L 384x288"),
|
|
||||||
) -> None:
|
|
||||||
selected = set(selected_frames)
|
|
||||||
record_map = {record["frame_index"]: record for record in records}
|
|
||||||
cap = cv2.VideoCapture(str(video_path))
|
|
||||||
rows: list[np.ndarray] = []
|
|
||||||
frame_idx = 0
|
|
||||||
while True:
|
|
||||||
ok, frame = cap.read()
|
|
||||||
if not ok:
|
|
||||||
break
|
|
||||||
if frame_idx in selected:
|
|
||||||
record = record_map[frame_idx]
|
|
||||||
m = draw_pose(
|
|
||||||
frame,
|
|
||||||
record["boxes"],
|
|
||||||
np.asarray(record["M"]["keypoints"]),
|
|
||||||
f"{titles[0]} | frame {frame_idx}",
|
|
||||||
)
|
|
||||||
l = draw_pose(
|
|
||||||
frame,
|
|
||||||
record["boxes"],
|
|
||||||
np.asarray(record["L"]["keypoints"]),
|
|
||||||
f"{titles[1]} | frame {frame_idx}",
|
|
||||||
)
|
|
||||||
m = cv2.resize(m, (640, 360), interpolation=cv2.INTER_AREA)
|
|
||||||
l = cv2.resize(l, (640, 360), interpolation=cv2.INTER_AREA)
|
|
||||||
rows.append(np.hstack([m, l]))
|
|
||||||
frame_idx += 1
|
|
||||||
cap.release()
|
|
||||||
if rows:
|
|
||||||
cv2.imwrite(str(output_path), np.vstack(rows))
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
from rtmlib import RTMPose
|
|
||||||
args = parse_args()
|
|
||||||
args.output.mkdir(parents=True, exist_ok=True)
|
|
||||||
cached_boxes = load_boxes(args.boxes, args.max_people)
|
|
||||||
|
|
||||||
pose_m = RTMPose(
|
|
||||||
str(args.model_m), model_input_size=(192, 256), backend="onnxruntime", device="cpu"
|
|
||||||
)
|
|
||||||
pose_l = RTMPose(
|
|
||||||
str(args.model_l), model_input_size=(288, 384), backend="onnxruntime", device="cpu"
|
|
||||||
)
|
|
||||||
|
|
||||||
cap = cv2.VideoCapture(str(args.video))
|
|
||||||
fps = float(cap.get(cv2.CAP_PROP_FPS))
|
|
||||||
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
||||||
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
||||||
video_output = args.output / "reference_video_rtmw_m_vs_l.mp4"
|
|
||||||
writer = cv2.VideoWriter(
|
|
||||||
str(video_output),
|
|
||||||
cv2.VideoWriter_fourcc(*"mp4v"),
|
|
||||||
fps,
|
|
||||||
(width, height // 2),
|
|
||||||
)
|
|
||||||
|
|
||||||
records: list[dict] = []
|
|
||||||
timings = {"M": [], "L": []}
|
|
||||||
frame_idx = 0
|
|
||||||
while True:
|
|
||||||
ok, frame = cap.read()
|
|
||||||
if not ok:
|
|
||||||
break
|
|
||||||
boxes = cached_boxes[frame_idx]
|
|
||||||
|
|
||||||
started = time.perf_counter()
|
|
||||||
kpts_m, scores_m = pose_m(frame, bboxes=boxes)
|
|
||||||
timings["M"].append((time.perf_counter() - started) * 1000.0)
|
|
||||||
|
|
||||||
started = time.perf_counter()
|
|
||||||
kpts_l, scores_l = pose_l(frame, bboxes=boxes)
|
|
||||||
timings["L"].append((time.perf_counter() - started) * 1000.0)
|
|
||||||
|
|
||||||
records.append(
|
|
||||||
{
|
|
||||||
"frame_index": frame_idx,
|
|
||||||
"boxes": boxes,
|
|
||||||
"M": {
|
|
||||||
"keypoints": np.round(kpts_m, 3).tolist(),
|
|
||||||
"raw_scores": np.round(scores_m, 4).tolist(),
|
|
||||||
},
|
|
||||||
"L": {
|
|
||||||
"keypoints": np.round(kpts_l, 3).tolist(),
|
|
||||||
"raw_scores": np.round(scores_l, 4).tolist(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
left = draw_pose(frame, boxes, kpts_m, f"RTMW-M 256x192 | frame {frame_idx}")
|
|
||||||
right = draw_pose(frame, boxes, kpts_l, f"RTMW-L 384x288 | frame {frame_idx}")
|
|
||||||
left = cv2.resize(left, (width // 2, height // 2), interpolation=cv2.INTER_AREA)
|
|
||||||
right = cv2.resize(right, (width // 2, height // 2), interpolation=cv2.INTER_AREA)
|
|
||||||
writer.write(np.hstack([left, right]))
|
|
||||||
|
|
||||||
if frame_idx % 50 == 0:
|
|
||||||
print(f"processed frame {frame_idx}", flush=True)
|
|
||||||
frame_idx += 1
|
|
||||||
|
|
||||||
cap.release()
|
|
||||||
writer.release()
|
|
||||||
|
|
||||||
jsonl_path = args.output / "reference_video_rtmw_m_vs_l.jsonl"
|
|
||||||
with jsonl_path.open("w", encoding="utf-8") as handle:
|
|
||||||
for record in records:
|
|
||||||
handle.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
||||||
|
|
||||||
disagreement, worst_frames = model_disagreement(records)
|
|
||||||
fixed_frames = [0, 60, 120, 180, 240, 252, 300, 360, 411]
|
|
||||||
montage_frames = sorted(set(fixed_frames + worst_frames))
|
|
||||||
write_montage(
|
|
||||||
args.video,
|
|
||||||
records,
|
|
||||||
montage_frames,
|
|
||||||
args.output / "rtmw_m_vs_l_full_montage.jpg",
|
|
||||||
)
|
|
||||||
|
|
||||||
summary = {
|
|
||||||
"frames": len(records),
|
|
||||||
"input_sizes_wh": {"M": [192, 256], "L": [288, 384]},
|
|
||||||
"raw_score_note": (
|
|
||||||
"M and L raw SimCC peak scores use different scales and must not be compared "
|
|
||||||
"or thresholded with the same numeric value."
|
|
||||||
),
|
|
||||||
"timing_ms_two_people_cpu": {
|
|
||||||
name: summarize(values[5:] if len(values) > 5 else values)
|
|
||||||
for name, values in timings.items()
|
|
||||||
},
|
|
||||||
"temporal_metrics_normalized_to_yolo_box": temporal_metrics(records),
|
|
||||||
"m_l_position_disagreement_normalized_to_yolo_box": disagreement,
|
|
||||||
"largest_upper_body_disagreement_frames": worst_frames,
|
|
||||||
"montage_frames": montage_frames,
|
|
||||||
}
|
|
||||||
with (args.output / "summary.json").open("w", encoding="utf-8") as handle:
|
|
||||||
json.dump(summary, handle, ensure_ascii=False, indent=2)
|
|
||||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
"""Native Jetson TensorRT adapter; temporal/risk policy stays in upstream Python."""
|
"""Native Jetson TensorRT adapter; temporal/risk policy stays in upstream Python."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import ctypes as C
|
import ctypes as C
|
||||||
from collections import defaultdict, deque
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from live_pose_engine import LivePoseEngine
|
|
||||||
from predict_optimized_pose_videos import BoxTracker
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
@@ -67,9 +64,3 @@ class NativeBackend:
|
|||||||
def close(self):
|
def close(self):
|
||||||
if self.camera:self.lib.vap_camera_close(self.camera);self.camera=None
|
if self.camera:self.lib.vap_camera_close(self.camera);self.camera=None
|
||||||
if self.engine:self.lib.vap_engine_close(self.engine);self.engine=None
|
if self.engine:self.lib.vap_engine_close(self.engine);self.engine=None
|
||||||
|
|
||||||
class NativePoseEngine(LivePoseEngine):
|
|
||||||
def __init__(self,args,native):
|
|
||||||
self.args=args;self.native=native;self.pose=native.pose
|
|
||||||
self.box_tracker=BoxTracker(max_age=30);self.people={}
|
|
||||||
self.timings=defaultdict(lambda:deque(maxlen=3000));self.isolated_inferences=0
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,113 +1,28 @@
|
|||||||
"""Resident YOLO11s + RTMW-L pose stage extracted from the v9.9 pipeline."""
|
"""Resident native TensorRT inference with causal pose fusion and tracking."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import time
|
import time
|
||||||
from collections import defaultdict
|
from collections import defaultdict, deque
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from pose_tracking import BoxTracker
|
||||||
|
|
||||||
SIDES = ("left", "right")
|
SIDES = ("left", "right")
|
||||||
|
|
||||||
|
class NativePoseEngine:
|
||||||
class LivePoseEngine:
|
def __init__(self, args, native):
|
||||||
"""Run the v9.9 YOLO/RTMW pose and temporal-fusion path without EdgeTAM."""
|
|
||||||
|
|
||||||
def __init__(self, args: argparse.Namespace):
|
|
||||||
self._trt_dll_directory = None
|
|
||||||
if args.pose_provider == "tensorrt" and os.name == "nt":
|
|
||||||
try:
|
|
||||||
import tensorrt_libs
|
|
||||||
except ImportError as exc:
|
|
||||||
raise RuntimeError(
|
|
||||||
"--pose-provider tensorrt on Windows requires the "
|
|
||||||
"tensorrt-cu12-libs package in the active Python environment."
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
trt_lib_dir = Path(tensorrt_libs.__file__).resolve().parent
|
|
||||||
self._trt_dll_directory = os.add_dll_directory(str(trt_lib_dir))
|
|
||||||
os.environ["PATH"] = (
|
|
||||||
str(trt_lib_dir) + os.pathsep + os.environ.get("PATH", "")
|
|
||||||
)
|
|
||||||
|
|
||||||
if args.pose_device.lower().startswith("cuda"):
|
|
||||||
import onnxruntime as ort
|
|
||||||
|
|
||||||
if "CUDAExecutionProvider" not in ort.get_available_providers():
|
|
||||||
raise RuntimeError(
|
|
||||||
"--pose-device cuda requires onnxruntime-gpu. Uninstall "
|
|
||||||
"onnxruntime and install onnxruntime-gpu==1.22.0."
|
|
||||||
)
|
|
||||||
|
|
||||||
from rtmlib import RTMPose
|
|
||||||
from ultralytics import YOLO
|
|
||||||
from predict_optimized_pose_videos import BoxTracker
|
|
||||||
|
|
||||||
self.args = args
|
self.args = args
|
||||||
self.detector = YOLO(str(args.det_model))
|
self.native = native
|
||||||
self.pose = RTMPose(
|
self.pose = native.pose
|
||||||
str(args.pose_model),
|
|
||||||
model_input_size=(288, 384),
|
|
||||||
backend="onnxruntime",
|
|
||||||
device=args.pose_device,
|
|
||||||
)
|
|
||||||
if args.pose_provider == "tensorrt":
|
|
||||||
import onnxruntime as ort
|
|
||||||
|
|
||||||
available = ort.get_available_providers()
|
|
||||||
if "TensorrtExecutionProvider" not in available:
|
|
||||||
raise RuntimeError(
|
|
||||||
"--pose-provider tensorrt requires ONNX Runtime with "
|
|
||||||
"TensorrtExecutionProvider. "
|
|
||||||
f"Available providers: {available}"
|
|
||||||
)
|
|
||||||
cache_dir = args.pose_trt_cache.expanduser().resolve()
|
|
||||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
device_id = (
|
|
||||||
int(str(args.pose_device).split(":")[-1])
|
|
||||||
if ":" in str(args.pose_device)
|
|
||||||
else 0
|
|
||||||
)
|
|
||||||
trt_options = {
|
|
||||||
"device_id": device_id,
|
|
||||||
"trt_fp16_enable": "True",
|
|
||||||
"trt_engine_cache_enable": "True",
|
|
||||||
"trt_engine_cache_path": str(cache_dir),
|
|
||||||
"trt_timing_cache_enable": "True",
|
|
||||||
"trt_timing_cache_path": str(cache_dir),
|
|
||||||
}
|
|
||||||
self.pose.session = ort.InferenceSession(
|
|
||||||
str(args.pose_model.resolve()),
|
|
||||||
providers=[
|
|
||||||
("TensorrtExecutionProvider", trt_options),
|
|
||||||
("CUDAExecutionProvider", {"device_id": device_id}),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
active_providers = self.pose.session.get_providers()
|
|
||||||
if not active_providers or active_providers[0] != "TensorrtExecutionProvider":
|
|
||||||
raise RuntimeError(
|
|
||||||
"RTMW TensorRT initialization failed instead of using the "
|
|
||||||
"requested provider. Active providers: "
|
|
||||||
f"{active_providers}"
|
|
||||||
)
|
|
||||||
print(f"RTMW ONNX Runtime providers: {active_providers}")
|
|
||||||
|
|
||||||
self.box_tracker = BoxTracker(max_age=30)
|
self.box_tracker = BoxTracker(max_age=30)
|
||||||
self.people = {}
|
self.people = {}
|
||||||
self.timings = defaultdict(list)
|
self.timings = defaultdict(lambda: deque(maxlen=3000))
|
||||||
self.isolated_inferences = 0
|
self.isolated_inferences = 0
|
||||||
|
|
||||||
def infer(self, frame: np.ndarray, frame_idx: int) -> dict:
|
def infer(self, frame: np.ndarray, frame_idx: int) -> dict:
|
||||||
from compare_rtmw_ml import HAND_SLICES
|
from pose_geometry import HAND_SLICES
|
||||||
from fuse_rtmw_l_temporal import adaptive_smooth, build_global_hands, fuse_person
|
from pose_fusion import adaptive_smooth, build_global_hands, fuse_person
|
||||||
from occlusion_aware_pose_v2 import Measurement, resolve_global_hand_assignments
|
from pose_occlusion import Measurement, resolve_global_hand_assignments
|
||||||
from predict_optimized_pose_videos import (
|
from pose_tracking import (
|
||||||
LivePersonState,
|
LivePersonState,
|
||||||
filter_person_boxes,
|
|
||||||
mask_other_people,
|
mask_other_people,
|
||||||
update_live_v2,
|
update_live_v2,
|
||||||
)
|
)
|
||||||
@@ -118,23 +33,8 @@ class LivePoseEngine:
|
|||||||
detection_height = max(
|
detection_height = max(
|
||||||
1, min(height, round(height * self.args.det_roi_y_max_ratio))
|
1, min(height, round(height * self.args.det_roi_y_max_ratio))
|
||||||
)
|
)
|
||||||
if hasattr(self, "native"):
|
boxes, confidences = self.native.detect(frame[:detection_height])
|
||||||
boxes, confidences = self.native.detect(frame[:detection_height])
|
self.timings["detector_ms"].append((time.perf_counter() - started) * 1000)
|
||||||
self.timings["detector_ms"].append((time.perf_counter() - started) * 1000)
|
|
||||||
else:
|
|
||||||
detection = self.detector.predict(
|
|
||||||
frame[:detection_height],
|
|
||||||
imgsz=self.args.det_imgsz,
|
|
||||||
conf=self.args.det_conf,
|
|
||||||
iou=0.30,
|
|
||||||
classes=[0],
|
|
||||||
device=self.args.det_device,
|
|
||||||
verbose=False,
|
|
||||||
)[0]
|
|
||||||
self.timings["detector_ms"].append((time.perf_counter() - started) * 1000)
|
|
||||||
boxes, confidences, _ = filter_person_boxes(
|
|
||||||
detection, width, height, self.args.max_people
|
|
||||||
)
|
|
||||||
track_ids = self.box_tracker.update(boxes, frame_idx, (height, width))
|
track_ids = self.box_tracker.update(boxes, frame_idx, (height, width))
|
||||||
|
|
||||||
# Retire disappeared people in long-running camera sessions.
|
# Retire disappeared people in long-running camera sessions.
|
||||||
@@ -1,38 +1,18 @@
|
|||||||
"""Fuse raw and person-isolated RTMW-L poses with temporal consistency.
|
"""Runtime pose fusion helpers."""
|
||||||
|
|
||||||
The fusion keeps the two inference views as hypotheses. It selects body limbs
|
|
||||||
and hands separately, rejects implausible hand geometry, adds source hysteresis
|
|
||||||
and constant-velocity consistency, then applies a motion-adaptive EMA in
|
|
||||||
person-box coordinates. Occluded/conflicting limbs remain labelled as such so
|
|
||||||
they are not accidentally consumed as precise anti-pinch distance evidence.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import math
|
import math
|
||||||
from collections import Counter, defaultdict
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from pose_geometry import BODY_WRISTS, HAND_EDGES_LOCAL, HAND_SLICES, normalized_keypoints
|
||||||
from compare_rtmw_ml import (
|
|
||||||
BODY_EDGES,
|
|
||||||
BODY_WRISTS,
|
|
||||||
HAND_EDGES_LOCAL,
|
|
||||||
HAND_SLICES,
|
|
||||||
draw_pose,
|
|
||||||
normalized_keypoints,
|
|
||||||
summarize,
|
|
||||||
temporal_metrics,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
BODY_SIDE_INDICES = {"left": (5, 7, 9), "right": (6, 8, 10)}
|
BODY_SIDE_INDICES = {"left": (5, 7, 9), "right": (6, 8, 10)}
|
||||||
|
|
||||||
|
|
||||||
SIDE_COLORS = {"left": (40, 40, 255), "right": (30, 230, 30)}
|
SIDE_COLORS = {"left": (40, 40, 255), "right": (30, 230, 30)}
|
||||||
|
|
||||||
|
|
||||||
HAND_COLORS = {"left": (255, 80, 220), "right": (255, 190, 40)}
|
HAND_COLORS = {"left": (255, 80, 220), "right": (255, 190, 40)}
|
||||||
|
|
||||||
|
|
||||||
@@ -71,15 +51,6 @@ class PersonTrack:
|
|||||||
smoothed: np.ndarray | None = None
|
smoothed: np.ndarray | None = None
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("--video", type=Path, required=True)
|
|
||||||
parser.add_argument("--raw-jsonl", type=Path, required=True)
|
|
||||||
parser.add_argument("--isolated-jsonl", type=Path, required=True)
|
|
||||||
parser.add_argument("--output", type=Path, required=True)
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def bbox_diagonal(bbox: list[float]) -> float:
|
def bbox_diagonal(bbox: list[float]) -> float:
|
||||||
return max(math.hypot(bbox[2] - bbox[0], bbox[3] - bbox[1]), 1.0)
|
return max(math.hypot(bbox[2] - bbox[0], bbox[3] - bbox[1]), 1.0)
|
||||||
|
|
||||||
@@ -557,299 +528,3 @@ def draw_fused(
|
|||||||
cv2.LINE_AA,
|
cv2.LINE_AA,
|
||||||
)
|
)
|
||||||
return canvas
|
return canvas
|
||||||
|
|
||||||
|
|
||||||
def make_montage(
|
|
||||||
video_path: Path,
|
|
||||||
records: list[dict],
|
|
||||||
selected_frames: list[int],
|
|
||||||
output_path: Path,
|
|
||||||
) -> None:
|
|
||||||
selected = set(selected_frames)
|
|
||||||
by_frame = {record["frame_index"]: record for record in records}
|
|
||||||
cap = cv2.VideoCapture(str(video_path))
|
|
||||||
rows = []
|
|
||||||
frame_idx = 0
|
|
||||||
while True:
|
|
||||||
ok, frame = cap.read()
|
|
||||||
if not ok:
|
|
||||||
break
|
|
||||||
if frame_idx in selected:
|
|
||||||
record = by_frame[frame_idx]
|
|
||||||
boxes = record["boxes"]
|
|
||||||
raw = draw_pose(
|
|
||||||
frame,
|
|
||||||
boxes,
|
|
||||||
np.asarray(record["raw_keypoints"]),
|
|
||||||
f"RTMW-L raw | frame {frame_idx}",
|
|
||||||
)
|
|
||||||
spatial = draw_fused(
|
|
||||||
frame,
|
|
||||||
boxes,
|
|
||||||
[np.asarray(points) for points in record["spatial_keypoints"]],
|
|
||||||
record["spatial_metadata"],
|
|
||||||
f"spatial fusion | frame {frame_idx}",
|
|
||||||
)
|
|
||||||
temporal = draw_fused(
|
|
||||||
frame,
|
|
||||||
boxes,
|
|
||||||
[np.asarray(points) for points in record["temporal_keypoints"]],
|
|
||||||
record["temporal_metadata"],
|
|
||||||
f"temporal fusion | frame {frame_idx}",
|
|
||||||
)
|
|
||||||
panels = [
|
|
||||||
cv2.resize(panel, (480, 270), interpolation=cv2.INTER_AREA)
|
|
||||||
for panel in (raw, spatial, temporal)
|
|
||||||
]
|
|
||||||
rows.append(np.hstack(panels))
|
|
||||||
frame_idx += 1
|
|
||||||
cap.release()
|
|
||||||
if rows:
|
|
||||||
cv2.imwrite(str(output_path), np.vstack(rows))
|
|
||||||
|
|
||||||
|
|
||||||
def active_hand_temporal_metrics(records: list[dict], variant: str) -> dict:
|
|
||||||
values: dict[str, list[float]] = defaultdict(list)
|
|
||||||
previous: dict[tuple[int, str], tuple[int, np.ndarray]] = {}
|
|
||||||
for record in records:
|
|
||||||
for person_idx, bbox in enumerate(record["boxes"]):
|
|
||||||
kpts = normalized_keypoints(
|
|
||||||
np.asarray(record[f"{variant}_keypoints"][person_idx]), bbox
|
|
||||||
)
|
|
||||||
meta = record[f"{variant}_metadata"][person_idx]
|
|
||||||
for side, (start, end) in HAND_SLICES.items():
|
|
||||||
key = (person_idx, side)
|
|
||||||
if not meta[side]["distance_eligible"]:
|
|
||||||
previous.pop(key, None)
|
|
||||||
continue
|
|
||||||
prior = previous.get(key)
|
|
||||||
if prior is not None and prior[0] == record["frame_index"] - 1:
|
|
||||||
step = np.linalg.norm(kpts[start:end] - prior[1], axis=1)
|
|
||||||
values[f"P{person_idx + 1}_{side}"].extend(float(v) for v in step)
|
|
||||||
previous[key] = (record["frame_index"], kpts[start:end])
|
|
||||||
return {name: summarize(items) for name, items in values.items()}
|
|
||||||
|
|
||||||
|
|
||||||
def metadata_summary(records: list[dict], variant: str) -> dict:
|
|
||||||
state_counts: Counter = Counter()
|
|
||||||
source_counts: Counter = Counter()
|
|
||||||
eligible_counts: Counter = Counter()
|
|
||||||
alarm_counts: Counter = Counter()
|
|
||||||
switches: Counter = Counter()
|
|
||||||
previous_source: dict[tuple[int, str, str], str] = {}
|
|
||||||
for record in records:
|
|
||||||
for person_idx, person_meta in enumerate(record[f"{variant}_metadata"]):
|
|
||||||
for side in ("left", "right"):
|
|
||||||
meta = person_meta[side]
|
|
||||||
key_prefix = f"P{person_idx + 1}_{side}"
|
|
||||||
state_counts[f"{key_prefix}_{meta['hand_state']}"] += 1
|
|
||||||
source_counts[f"{key_prefix}_{meta['hand_source']}"] += 1
|
|
||||||
if meta["distance_eligible"]:
|
|
||||||
eligible_counts[key_prefix] += 1
|
|
||||||
if meta["alarm_eligible"]:
|
|
||||||
alarm_counts[key_prefix] += 1
|
|
||||||
for kind in ("limb", "hand"):
|
|
||||||
source = meta[f"{kind}_source"]
|
|
||||||
key = (person_idx, side, kind)
|
|
||||||
if key in previous_source and previous_source[key] != source:
|
|
||||||
switches[f"{key_prefix}_{kind}"] += 1
|
|
||||||
previous_source[key] = source
|
|
||||||
return {
|
|
||||||
"hand_state_counts": dict(sorted(state_counts.items())),
|
|
||||||
"source_counts": dict(sorted(source_counts.items())),
|
|
||||||
"distance_eligible_frames": dict(sorted(eligible_counts.items())),
|
|
||||||
"alarm_eligible_frames": dict(sorted(alarm_counts.items())),
|
|
||||||
"source_switches": dict(sorted(switches.items())),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
args = parse_args()
|
|
||||||
args.output.mkdir(parents=True, exist_ok=True)
|
|
||||||
raw_records = [
|
|
||||||
json.loads(line) for line in args.raw_jsonl.read_text(encoding="utf-8").splitlines()
|
|
||||||
]
|
|
||||||
isolated_records = [
|
|
||||||
json.loads(line)
|
|
||||||
for line in args.isolated_jsonl.read_text(encoding="utf-8").splitlines()
|
|
||||||
]
|
|
||||||
|
|
||||||
cap = cv2.VideoCapture(str(args.video))
|
|
||||||
fps = float(cap.get(cv2.CAP_PROP_FPS))
|
|
||||||
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
||||||
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
||||||
writer = cv2.VideoWriter(
|
|
||||||
str(args.output / "rtmw_l_raw_spatial_temporal.mp4"),
|
|
||||||
cv2.VideoWriter_fourcc(*"mp4v"),
|
|
||||||
fps,
|
|
||||||
(1920, 360),
|
|
||||||
)
|
|
||||||
|
|
||||||
tracks = [PersonTrack(), PersonTrack()]
|
|
||||||
output_records: list[dict] = []
|
|
||||||
frame_idx = 0
|
|
||||||
while True:
|
|
||||||
ok, frame = cap.read()
|
|
||||||
if not ok:
|
|
||||||
break
|
|
||||||
raw_record = raw_records[frame_idx]
|
|
||||||
isolated_record = isolated_records[frame_idx]
|
|
||||||
boxes = raw_record["boxes"][:2]
|
|
||||||
raw_keypoints = np.asarray(raw_record["L"]["keypoints"][:2], dtype=np.float64)
|
|
||||||
raw_scores = np.asarray(raw_record["L"]["raw_scores"][:2], dtype=np.float64)
|
|
||||||
isolated_keypoints = np.asarray(
|
|
||||||
isolated_record["L"]["keypoints"][:2], dtype=np.float64
|
|
||||||
)
|
|
||||||
isolated_scores = np.asarray(
|
|
||||||
isolated_record["masked_raw_scores"][:2], dtype=np.float64
|
|
||||||
)
|
|
||||||
|
|
||||||
spatial_keypoints: list[np.ndarray] = []
|
|
||||||
spatial_metadata: list[dict] = []
|
|
||||||
temporal_keypoints: list[np.ndarray] = []
|
|
||||||
temporal_selected_keypoints: list[np.ndarray] = []
|
|
||||||
temporal_metadata: list[dict] = []
|
|
||||||
for person_idx in range(2):
|
|
||||||
spatial, spatial_meta = fuse_person(
|
|
||||||
raw_keypoints[person_idx],
|
|
||||||
raw_scores[person_idx],
|
|
||||||
isolated_keypoints[person_idx],
|
|
||||||
isolated_scores[person_idx],
|
|
||||||
boxes[person_idx],
|
|
||||||
track=None,
|
|
||||||
)
|
|
||||||
temporal_unfiltered, temporal_meta = fuse_person(
|
|
||||||
raw_keypoints[person_idx],
|
|
||||||
raw_scores[person_idx],
|
|
||||||
isolated_keypoints[person_idx],
|
|
||||||
isolated_scores[person_idx],
|
|
||||||
boxes[person_idx],
|
|
||||||
track=tracks[person_idx],
|
|
||||||
)
|
|
||||||
temporal = adaptive_smooth(
|
|
||||||
temporal_unfiltered, boxes[person_idx], tracks[person_idx]
|
|
||||||
)
|
|
||||||
spatial_keypoints.append(spatial)
|
|
||||||
spatial_metadata.append(spatial_meta)
|
|
||||||
temporal_keypoints.append(temporal)
|
|
||||||
temporal_selected_keypoints.append(temporal_unfiltered)
|
|
||||||
temporal_metadata.append(temporal_meta)
|
|
||||||
|
|
||||||
global_hands = build_global_hands(
|
|
||||||
temporal_selected_keypoints, temporal_metadata
|
|
||||||
)
|
|
||||||
|
|
||||||
output_record = {
|
|
||||||
"frame_index": frame_idx,
|
|
||||||
"boxes": boxes,
|
|
||||||
"raw_keypoints": np.round(raw_keypoints, 3).tolist(),
|
|
||||||
"spatial_keypoints": np.round(np.asarray(spatial_keypoints), 3).tolist(),
|
|
||||||
"spatial_metadata": spatial_metadata,
|
|
||||||
"temporal_keypoints": np.round(np.asarray(temporal_keypoints), 3).tolist(),
|
|
||||||
"temporal_selected_keypoints": np.round(
|
|
||||||
np.asarray(temporal_selected_keypoints), 3
|
|
||||||
).tolist(),
|
|
||||||
"temporal_metadata": temporal_metadata,
|
|
||||||
"global_hands": global_hands,
|
|
||||||
}
|
|
||||||
output_records.append(output_record)
|
|
||||||
|
|
||||||
raw_panel = draw_pose(frame, boxes, raw_keypoints, f"RTMW-L raw | frame {frame_idx}")
|
|
||||||
spatial_panel = draw_fused(
|
|
||||||
frame,
|
|
||||||
boxes,
|
|
||||||
spatial_keypoints,
|
|
||||||
spatial_metadata,
|
|
||||||
f"spatial fusion | frame {frame_idx}",
|
|
||||||
)
|
|
||||||
temporal_panel = draw_fused(
|
|
||||||
frame,
|
|
||||||
boxes,
|
|
||||||
temporal_keypoints,
|
|
||||||
temporal_metadata,
|
|
||||||
f"temporal fusion | frame {frame_idx}",
|
|
||||||
)
|
|
||||||
panels = [
|
|
||||||
cv2.resize(panel, (640, 360), interpolation=cv2.INTER_AREA)
|
|
||||||
for panel in (raw_panel, spatial_panel, temporal_panel)
|
|
||||||
]
|
|
||||||
writer.write(np.hstack(panels))
|
|
||||||
frame_idx += 1
|
|
||||||
|
|
||||||
cap.release()
|
|
||||||
writer.release()
|
|
||||||
|
|
||||||
with (args.output / "fusion.jsonl").open("w", encoding="utf-8") as handle:
|
|
||||||
for record in output_records:
|
|
||||||
handle.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
||||||
|
|
||||||
def comparison_records(variant: str) -> list[dict]:
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"frame_index": record["frame_index"],
|
|
||||||
"boxes": record["boxes"],
|
|
||||||
"M": {"keypoints": record["raw_keypoints"]},
|
|
||||||
"L": {"keypoints": record[f"{variant}_keypoints"]},
|
|
||||||
}
|
|
||||||
for record in output_records
|
|
||||||
]
|
|
||||||
|
|
||||||
summary = {
|
|
||||||
"frames": len(output_records),
|
|
||||||
"fusion_policy": {
|
|
||||||
"candidates": ["RTMW-L raw", "RTMW-L smaller-person isolated"],
|
|
||||||
"selection": [
|
|
||||||
"limb and hand selected separately",
|
|
||||||
"geometry validity",
|
|
||||||
"constant-velocity temporal consistency",
|
|
||||||
"source-switch hysteresis",
|
|
||||||
"motion-adaptive EMA in person-box coordinates",
|
|
||||||
],
|
|
||||||
"distance_rule": (
|
|
||||||
"Only consensus or temporally stable raw visible endpoints are eligible. "
|
|
||||||
"Inferred/conflict/rejected hands are excluded from precise distance."
|
|
||||||
),
|
|
||||||
"alarm_rule": (
|
|
||||||
"Alarm attack is asymmetric: a geometrically valid raw endpoint whose score "
|
|
||||||
"is clearly stronger than the isolated view is admitted immediately. Alarm "
|
|
||||||
"release should still require temporal confirmation and hysteresis."
|
|
||||||
),
|
|
||||||
"risk_distance_note": (
|
|
||||||
"Use global_hands and temporal_selected_keypoints (unfiltered) for alarm entry "
|
|
||||||
"to avoid smoothing delay. Use distance_eligible hands and temporal_keypoints "
|
|
||||||
"for stable reporting. When the door edge is available, a conservative "
|
|
||||||
"implementation can use the smaller of the unfiltered and smoothed distances."
|
|
||||||
),
|
|
||||||
},
|
|
||||||
"spatial": metadata_summary(output_records, "spatial"),
|
|
||||||
"temporal": metadata_summary(output_records, "temporal"),
|
|
||||||
"active_hand_frame_step": {
|
|
||||||
"spatial": active_hand_temporal_metrics(output_records, "spatial"),
|
|
||||||
"temporal": active_hand_temporal_metrics(output_records, "temporal"),
|
|
||||||
},
|
|
||||||
"raw_vs_spatial_temporal_metrics": temporal_metrics(comparison_records("spatial")),
|
|
||||||
"raw_vs_temporal_temporal_metrics": temporal_metrics(comparison_records("temporal")),
|
|
||||||
"global_hand_count_frames": dict(
|
|
||||||
sorted(Counter(len(record["global_hands"]) for record in output_records).items())
|
|
||||||
),
|
|
||||||
"cross_person_hand_aliases_total": sum(
|
|
||||||
len(hand["aliases"])
|
|
||||||
for record in output_records
|
|
||||||
for hand in record["global_hands"]
|
|
||||||
),
|
|
||||||
}
|
|
||||||
with (args.output / "summary.json").open("w", encoding="utf-8") as handle:
|
|
||||||
json.dump(summary, handle, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
key_frames = [0, 45, 50, 55, 120, 132, 136, 180, 240, 252, 295, 300, 304, 360, 411]
|
|
||||||
make_montage(
|
|
||||||
args.video,
|
|
||||||
output_records,
|
|
||||||
key_frames,
|
|
||||||
args.output / "rtmw_l_raw_spatial_temporal_montage.jpg",
|
|
||||||
)
|
|
||||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"""Runtime pose geometry helpers."""
|
||||||
|
from __future__ import annotations
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
HAND_EDGES_LOCAL = (
|
||||||
|
(0, 1), (1, 2), (2, 3), (3, 4),
|
||||||
|
(0, 5), (5, 6), (6, 7), (7, 8),
|
||||||
|
(0, 9), (9, 10), (10, 11), (11, 12),
|
||||||
|
(0, 13), (13, 14), (14, 15), (15, 16),
|
||||||
|
(0, 17), (17, 18), (18, 19), (19, 20),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
HAND_SLICES = {"left": (91, 112), "right": (112, 133)}
|
||||||
|
|
||||||
|
|
||||||
|
BODY_WRISTS = {"left": 9, "right": 10}
|
||||||
|
|
||||||
|
|
||||||
|
def normalized_keypoints(kpts: np.ndarray, bbox: list[float]) -> np.ndarray:
|
||||||
|
x1, y1, x2, y2 = bbox
|
||||||
|
width = max(x2 - x1, 1.0)
|
||||||
|
height = max(y2 - y1, 1.0)
|
||||||
|
result = kpts.copy().astype(np.float64)
|
||||||
|
result[:, 0] = (result[:, 0] - x1) / width
|
||||||
|
result[:, 1] = (result[:, 1] - y1) / height
|
||||||
|
return result
|
||||||
@@ -0,0 +1,513 @@
|
|||||||
|
"""Runtime pose occlusion helpers."""
|
||||||
|
from __future__ import annotations
|
||||||
|
import math
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import numpy as np
|
||||||
|
from pose_geometry import HAND_SLICES
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Measurement:
|
||||||
|
keypoints: np.ndarray
|
||||||
|
precise: bool
|
||||||
|
provisional: bool
|
||||||
|
source: str = "model"
|
||||||
|
quality: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EndpointTrack:
|
||||||
|
wrist: np.ndarray | None = None
|
||||||
|
velocity: np.ndarray | None = None
|
||||||
|
hand_shape: np.ndarray | None = None
|
||||||
|
last_observed_frame: int | None = None
|
||||||
|
missed_frames: int = 0
|
||||||
|
last_elbow: np.ndarray | None = None
|
||||||
|
last_direction: np.ndarray | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def bbox_diagonal(bbox: list[float]) -> float:
|
||||||
|
return max(math.hypot(bbox[2] - bbox[0], bbox[3] - bbox[1]), 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def solve_two_bone_elbow(
|
||||||
|
shoulder: np.ndarray,
|
||||||
|
wrist: np.ndarray,
|
||||||
|
upper_length: float,
|
||||||
|
forearm_length: float,
|
||||||
|
reference_elbow: np.ndarray,
|
||||||
|
) -> np.ndarray:
|
||||||
|
direction = wrist - shoulder
|
||||||
|
distance = float(np.linalg.norm(direction))
|
||||||
|
if distance < 1e-6:
|
||||||
|
return reference_elbow.copy()
|
||||||
|
unit = direction / distance
|
||||||
|
|
||||||
|
if distance >= upper_length + forearm_length:
|
||||||
|
return shoulder + unit * upper_length
|
||||||
|
if distance <= abs(upper_length - forearm_length):
|
||||||
|
sign = 1.0 if upper_length >= forearm_length else -1.0
|
||||||
|
return shoulder + unit * upper_length * sign
|
||||||
|
|
||||||
|
along = (
|
||||||
|
upper_length * upper_length
|
||||||
|
- forearm_length * forearm_length
|
||||||
|
+ distance * distance
|
||||||
|
) / (2.0 * distance)
|
||||||
|
height = math.sqrt(max(upper_length * upper_length - along * along, 0.0))
|
||||||
|
base = shoulder + unit * along
|
||||||
|
perpendicular = np.asarray([-unit[1], unit[0]])
|
||||||
|
candidates = (base + perpendicular * height, base - perpendicular * height)
|
||||||
|
return min(candidates, key=lambda candidate: float(np.linalg.norm(candidate - reference_elbow)))
|
||||||
|
|
||||||
|
|
||||||
|
def update_endpoint_track(
|
||||||
|
track: EndpointTrack,
|
||||||
|
measurement: Measurement | None,
|
||||||
|
frame_idx: int,
|
||||||
|
short_occlusion_frames: int,
|
||||||
|
medium_occlusion_frames: int,
|
||||||
|
) -> tuple[np.ndarray | None, np.ndarray | None, str]:
|
||||||
|
if measurement is not None:
|
||||||
|
measured_wrist = measurement.keypoints[0]
|
||||||
|
measured_shape = measurement.keypoints - measured_wrist
|
||||||
|
if track.wrist is None or track.missed_frames > short_occlusion_frames:
|
||||||
|
track.wrist = measured_wrist.copy()
|
||||||
|
track.velocity = np.zeros(2, dtype=np.float64)
|
||||||
|
track.hand_shape = measured_shape.copy()
|
||||||
|
else:
|
||||||
|
prediction = track.wrist + track.velocity
|
||||||
|
residual = measured_wrist - prediction
|
||||||
|
alpha = 0.78 if measurement.precise else 0.88
|
||||||
|
beta = 0.18 if measurement.precise else 0.24
|
||||||
|
track.wrist = prediction + alpha * residual
|
||||||
|
track.velocity = 0.82 * track.velocity + beta * residual
|
||||||
|
shape_alpha = 0.55 if measurement.precise else 0.7
|
||||||
|
track.hand_shape = (
|
||||||
|
shape_alpha * measured_shape + (1.0 - shape_alpha) * track.hand_shape
|
||||||
|
)
|
||||||
|
track.last_observed_frame = frame_idx
|
||||||
|
track.missed_frames = 0
|
||||||
|
if measurement.source == "global_reassignment":
|
||||||
|
state = "reassigned"
|
||||||
|
elif measurement.source == "temporal_recovery":
|
||||||
|
state = "recovered"
|
||||||
|
elif measurement.precise:
|
||||||
|
state = "observed"
|
||||||
|
elif measurement.provisional:
|
||||||
|
state = "provisional"
|
||||||
|
else:
|
||||||
|
state = "interpolated"
|
||||||
|
return track.wrist.copy(), track.wrist + track.hand_shape, state
|
||||||
|
|
||||||
|
track.missed_frames += 1
|
||||||
|
if track.wrist is None:
|
||||||
|
return None, None, "reachable_long"
|
||||||
|
if track.missed_frames <= short_occlusion_frames:
|
||||||
|
track.wrist = track.wrist + track.velocity
|
||||||
|
track.velocity *= 0.86
|
||||||
|
return track.wrist.copy(), track.wrist + track.hand_shape, "predicted_short"
|
||||||
|
if track.missed_frames <= medium_occlusion_frames:
|
||||||
|
return None, None, "reachable_medium"
|
||||||
|
return None, None, "reachable_long"
|
||||||
|
|
||||||
|
|
||||||
|
def temporal_recovery_measurement(
|
||||||
|
keypoints: np.ndarray,
|
||||||
|
metadata: dict,
|
||||||
|
side: str,
|
||||||
|
bbox: list[float],
|
||||||
|
track: EndpointTrack,
|
||||||
|
medium_occlusion_frames: int,
|
||||||
|
) -> Measurement | None:
|
||||||
|
"""Recover a weak current-frame hand only when history strongly supports it."""
|
||||||
|
if track.wrist is None or track.missed_frames > medium_occlusion_frames:
|
||||||
|
return None
|
||||||
|
if metadata.get("hand_state") not in {"conflict", "inferred"}:
|
||||||
|
return None
|
||||||
|
if not metadata.get("selected_hand_valid", False):
|
||||||
|
return None
|
||||||
|
if float(metadata.get("selected_hand_score", 0.0)) < 2.0:
|
||||||
|
return None
|
||||||
|
link = float(metadata.get("hand_selection", {}).get("link_to_selected_limb", 1.0))
|
||||||
|
if link > 0.08:
|
||||||
|
return None
|
||||||
|
|
||||||
|
start, end = HAND_SLICES[side]
|
||||||
|
hand = np.asarray(keypoints[start:end], dtype=np.float64)
|
||||||
|
if hand.shape != (end - start, 2) or not np.isfinite(hand).all():
|
||||||
|
return None
|
||||||
|
prediction = track.wrist + track.velocity
|
||||||
|
diagonal = bbox_diagonal(bbox)
|
||||||
|
gate = min(0.20, 0.05 + 0.008 * max(track.missed_frames, 1)) * diagonal
|
||||||
|
if float(np.linalg.norm(hand[0] - prediction)) > gate:
|
||||||
|
return None
|
||||||
|
return Measurement(
|
||||||
|
keypoints=hand,
|
||||||
|
precise=False,
|
||||||
|
provisional=True,
|
||||||
|
source="temporal_recovery",
|
||||||
|
quality=float(metadata.get("selected_hand_score", 0.0)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def hand_span_px(hand: np.ndarray) -> float:
|
||||||
|
return math.hypot(float(np.ptp(hand[:, 0])), float(np.ptp(hand[:, 1])))
|
||||||
|
|
||||||
|
|
||||||
|
def bbox_area(bbox: list[float]) -> float:
|
||||||
|
return max((bbox[2] - bbox[0]) * (bbox[3] - bbox[1]), 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def reset_endpoint_track(track: EndpointTrack) -> None:
|
||||||
|
track.wrist = None
|
||||||
|
track.velocity = None
|
||||||
|
track.hand_shape = None
|
||||||
|
track.last_observed_frame = None
|
||||||
|
track.missed_frames = 0
|
||||||
|
track.last_elbow = None
|
||||||
|
track.last_direction = None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_global_hand_assignments(
|
||||||
|
frame_measurements: dict[tuple[int, str], Measurement | None],
|
||||||
|
tracks: dict[tuple[int, str], EndpointTrack],
|
||||||
|
boxes: list[list[float]],
|
||||||
|
medium_occlusion_frames: int,
|
||||||
|
) -> tuple[dict[tuple[int, str], Measurement | None], dict[tuple[int, str], dict]]:
|
||||||
|
"""Resolve overlapping-person hands before binding them to body sides.
|
||||||
|
|
||||||
|
Large foreground top-down crops may include a smaller background person.
|
||||||
|
The same physical hand can therefore be emitted for both people, or only by
|
||||||
|
the smaller crop even though it belongs to the foreground person. Direct
|
||||||
|
duplicates are kept on the foreground track when their apparent scales
|
||||||
|
agree. A missing foreground side can then reclaim a nearby candidate using
|
||||||
|
its recent hand trajectory and historical hand scale.
|
||||||
|
"""
|
||||||
|
resolved = dict(frame_measurements)
|
||||||
|
events: dict[tuple[int, str], dict] = {}
|
||||||
|
keys = list(resolved)
|
||||||
|
|
||||||
|
# First collapse direct cross-person duplicates. Keep this deliberately
|
||||||
|
# asymmetric: a substantially larger foreground crop must also carry the
|
||||||
|
# substantially larger historical hand shape.
|
||||||
|
for index, first_key in enumerate(keys):
|
||||||
|
first = resolved[first_key]
|
||||||
|
if first is None:
|
||||||
|
continue
|
||||||
|
for second_key in keys[index + 1 :]:
|
||||||
|
second = resolved[second_key]
|
||||||
|
if second is None or first_key[0] == second_key[0]:
|
||||||
|
continue
|
||||||
|
first_hand = first.keypoints
|
||||||
|
second_hand = second.keypoints
|
||||||
|
first_span = hand_span_px(first_hand)
|
||||||
|
second_span = hand_span_px(second_hand)
|
||||||
|
center_distance = float(
|
||||||
|
np.linalg.norm(np.mean(first_hand, axis=0) - np.mean(second_hand, axis=0))
|
||||||
|
)
|
||||||
|
if center_distance > max(70.0, 0.5 * max(first_span, second_span)):
|
||||||
|
continue
|
||||||
|
|
||||||
|
first_area = bbox_area(boxes[first_key[0]])
|
||||||
|
second_area = bbox_area(boxes[second_key[0]])
|
||||||
|
if first_area >= 2.5 * second_area and (
|
||||||
|
first_span >= 1.45 * second_span
|
||||||
|
or first.quality >= second.quality + 0.6
|
||||||
|
):
|
||||||
|
owner_key, released_key = first_key, second_key
|
||||||
|
elif second_area >= 2.5 * first_area and (
|
||||||
|
second_span >= 1.45 * first_span
|
||||||
|
or second.quality >= first.quality + 0.6
|
||||||
|
):
|
||||||
|
owner_key, released_key = second_key, first_key
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
resolved[released_key] = None
|
||||||
|
reset_endpoint_track(tracks[released_key])
|
||||||
|
events[owner_key] = {
|
||||||
|
"method": "foreground_duplicate_owner",
|
||||||
|
"released_person_id": released_key[0] + 1,
|
||||||
|
"released_side": released_key[1],
|
||||||
|
}
|
||||||
|
events[released_key] = {
|
||||||
|
"method": "reassigned_away",
|
||||||
|
"assigned_person_id": owner_key[0] + 1,
|
||||||
|
"assigned_side": owner_key[1],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Then recover missing foreground hands from another crop. The candidate
|
||||||
|
# must fall inside the target's recent motion gate and be much smaller than
|
||||||
|
# the target's historical foreground hand shape.
|
||||||
|
available_sources = [key for key, value in resolved.items() if value is not None]
|
||||||
|
target_candidates = []
|
||||||
|
for target_key, target_measurement in resolved.items():
|
||||||
|
if target_measurement is not None:
|
||||||
|
continue
|
||||||
|
target_track = tracks[target_key]
|
||||||
|
if (
|
||||||
|
target_track.wrist is None
|
||||||
|
or target_track.hand_shape is None
|
||||||
|
or target_track.missed_frames > medium_occlusion_frames
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
target_area = bbox_area(boxes[target_key[0]])
|
||||||
|
predicted_center = target_track.wrist + np.mean(target_track.hand_shape, axis=0)
|
||||||
|
target_span = hand_span_px(target_track.hand_shape)
|
||||||
|
gate = 0.20 * bbox_diagonal(boxes[target_key[0]])
|
||||||
|
for source_key in available_sources:
|
||||||
|
if source_key[0] == target_key[0]:
|
||||||
|
continue
|
||||||
|
source = resolved[source_key]
|
||||||
|
if source is None or source.source != "model":
|
||||||
|
continue
|
||||||
|
source_area = bbox_area(boxes[source_key[0]])
|
||||||
|
if target_area < 2.5 * source_area:
|
||||||
|
continue
|
||||||
|
source_center = np.mean(source.keypoints, axis=0)
|
||||||
|
source_span = hand_span_px(source.keypoints)
|
||||||
|
distance = float(np.linalg.norm(source_center - predicted_center))
|
||||||
|
if distance > gate or target_span < 1.45 * max(source_span, 1.0):
|
||||||
|
continue
|
||||||
|
target_candidates.append(
|
||||||
|
(distance / gate, target_key, source_key, source_center)
|
||||||
|
)
|
||||||
|
|
||||||
|
used_targets = set()
|
||||||
|
used_sources = set()
|
||||||
|
for _, target_key, source_key, source_center in sorted(target_candidates):
|
||||||
|
if target_key in used_targets or source_key in used_sources:
|
||||||
|
continue
|
||||||
|
target_track = tracks[target_key]
|
||||||
|
shape_center = np.mean(target_track.hand_shape, axis=0)
|
||||||
|
reassigned_wrist = source_center - shape_center
|
||||||
|
reassigned_hand = reassigned_wrist + target_track.hand_shape
|
||||||
|
resolved[target_key] = Measurement(
|
||||||
|
keypoints=reassigned_hand,
|
||||||
|
precise=False,
|
||||||
|
provisional=True,
|
||||||
|
source="global_reassignment",
|
||||||
|
quality=resolved[source_key].quality,
|
||||||
|
)
|
||||||
|
resolved[source_key] = None
|
||||||
|
reset_endpoint_track(tracks[source_key])
|
||||||
|
events[target_key] = {
|
||||||
|
"method": "foreground_temporal_reassignment",
|
||||||
|
"source_person_id": source_key[0] + 1,
|
||||||
|
"source_side": source_key[1],
|
||||||
|
"observed_center": np.round(source_center, 3).tolist(),
|
||||||
|
}
|
||||||
|
events[source_key] = {
|
||||||
|
"method": "reassigned_away",
|
||||||
|
"assigned_person_id": target_key[0] + 1,
|
||||||
|
"assigned_side": target_key[1],
|
||||||
|
}
|
||||||
|
used_targets.add(target_key)
|
||||||
|
used_sources.add(source_key)
|
||||||
|
return resolved, events
|
||||||
|
|
||||||
|
|
||||||
|
def track_direction(track: EndpointTrack) -> np.ndarray:
|
||||||
|
if track.last_direction is not None:
|
||||||
|
return track.last_direction / max(float(np.linalg.norm(track.last_direction)), 1e-6)
|
||||||
|
if track.velocity is not None and float(np.linalg.norm(track.velocity)) > 1e-6:
|
||||||
|
return track.velocity / np.linalg.norm(track.velocity)
|
||||||
|
return np.asarray([1.0, 0.0], dtype=np.float64)
|
||||||
|
|
||||||
|
|
||||||
|
def ellipse_polygon(
|
||||||
|
center: np.ndarray,
|
||||||
|
major_axis: float,
|
||||||
|
minor_axis: float,
|
||||||
|
direction: np.ndarray,
|
||||||
|
point_count: int = 64,
|
||||||
|
) -> np.ndarray:
|
||||||
|
perpendicular = np.asarray([-direction[1], direction[0]], dtype=np.float64)
|
||||||
|
angles = np.linspace(0.0, 2.0 * math.pi, point_count, endpoint=False)
|
||||||
|
return (
|
||||||
|
center
|
||||||
|
+ np.cos(angles)[:, None] * major_axis * direction
|
||||||
|
+ np.sin(angles)[:, None] * minor_axis * perpendicular
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def clip_polygon_from_anchor_to_circle(
|
||||||
|
points: np.ndarray,
|
||||||
|
anchor: np.ndarray,
|
||||||
|
circle_center: np.ndarray,
|
||||||
|
radius: float,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""Clip radial polygon samples to a shoulder-centered anatomical limit."""
|
||||||
|
clipped = []
|
||||||
|
offset = anchor - circle_center
|
||||||
|
for point in points:
|
||||||
|
if float(np.linalg.norm(point - circle_center)) <= radius:
|
||||||
|
clipped.append(point)
|
||||||
|
continue
|
||||||
|
ray = point - anchor
|
||||||
|
a = float(np.dot(ray, ray))
|
||||||
|
b = 2.0 * float(np.dot(offset, ray))
|
||||||
|
c = float(np.dot(offset, offset) - radius * radius)
|
||||||
|
discriminant = max(b * b - 4.0 * a * c, 0.0)
|
||||||
|
roots = [
|
||||||
|
value
|
||||||
|
for value in (
|
||||||
|
(-b - math.sqrt(discriminant)) / max(2.0 * a, 1e-6),
|
||||||
|
(-b + math.sqrt(discriminant)) / max(2.0 * a, 1e-6),
|
||||||
|
)
|
||||||
|
if 0.0 <= value <= 1.0
|
||||||
|
]
|
||||||
|
clipped.append(anchor + (max(roots) if roots else 0.0) * ray)
|
||||||
|
return np.asarray(clipped, dtype=np.float64)
|
||||||
|
|
||||||
|
|
||||||
|
def medium_motion_region(
|
||||||
|
shoulder: np.ndarray,
|
||||||
|
upper_length: float,
|
||||||
|
forearm_length: float,
|
||||||
|
bbox: list[float],
|
||||||
|
track: EndpointTrack,
|
||||||
|
short_occlusion_frames: int,
|
||||||
|
) -> dict:
|
||||||
|
diagonal = bbox_diagonal(bbox)
|
||||||
|
direction = track_direction(track)
|
||||||
|
hand_shape = (
|
||||||
|
track.hand_shape
|
||||||
|
if track.hand_shape is not None
|
||||||
|
else np.zeros((21, 2), dtype=np.float64)
|
||||||
|
)
|
||||||
|
shape_center = np.mean(hand_shape, axis=0)
|
||||||
|
centered_shape = hand_shape - shape_center
|
||||||
|
perpendicular = np.asarray([-direction[1], direction[0]], dtype=np.float64)
|
||||||
|
hand_major = float(np.max(np.abs(centered_shape @ direction)))
|
||||||
|
hand_minor = float(np.max(np.abs(centered_shape @ perpendicular)))
|
||||||
|
|
||||||
|
extra_frames = max(track.missed_frames - short_occlusion_frames, 1)
|
||||||
|
motion_major = min(0.20, 0.04 + 0.007 * extra_frames) * diagonal
|
||||||
|
motion_minor = min(0.13, 0.03 + 0.0045 * extra_frames) * diagonal
|
||||||
|
major_axis = max(hand_major + motion_major, 0.08 * diagonal)
|
||||||
|
minor_axis = max(hand_minor + motion_minor, 0.06 * diagonal)
|
||||||
|
|
||||||
|
center = track.wrist + shape_center
|
||||||
|
tracked_hand_extent = float(np.max(np.linalg.norm(centered_shape, axis=1)))
|
||||||
|
hand_reach = max(tracked_hand_extent, 0.10 * diagonal)
|
||||||
|
anatomical_reach = upper_length + forearm_length + hand_reach + 0.02 * diagonal
|
||||||
|
shoulder_to_center = center - shoulder
|
||||||
|
center_distance = float(np.linalg.norm(shoulder_to_center))
|
||||||
|
if center_distance > 0.96 * anatomical_reach:
|
||||||
|
center = shoulder + shoulder_to_center / center_distance * 0.96 * anatomical_reach
|
||||||
|
|
||||||
|
polygon = ellipse_polygon(center, major_axis, minor_axis, direction)
|
||||||
|
polygon = clip_polygon_from_anchor_to_circle(
|
||||||
|
polygon,
|
||||||
|
center,
|
||||||
|
shoulder,
|
||||||
|
anatomical_reach,
|
||||||
|
)
|
||||||
|
direction_deg = math.degrees(math.atan2(float(direction[1]), float(direction[0])))
|
||||||
|
return {
|
||||||
|
"geometry": "motion_corridor",
|
||||||
|
"center": np.round(center, 3).tolist(),
|
||||||
|
"predicted_wrist": np.round(track.wrist, 3).tolist(),
|
||||||
|
"major_axis_px": round(major_axis, 3),
|
||||||
|
"minor_axis_px": round(minor_axis, 3),
|
||||||
|
"outer_radius_px": round(major_axis, 3),
|
||||||
|
"direction_deg": round(direction_deg, 3),
|
||||||
|
"max_reach_from_shoulder_px": round(anatomical_reach, 3),
|
||||||
|
"polygon": np.round(polygon, 3).tolist(),
|
||||||
|
"requires_door_intersection": True,
|
||||||
|
"render_full_region": True,
|
||||||
|
"distance_lower_bound_rule": "minimum distance from motion-corridor polygon to door_edge",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def long_reachable_region(
|
||||||
|
shoulder: np.ndarray,
|
||||||
|
upper_length: float,
|
||||||
|
forearm_length: float,
|
||||||
|
bbox: list[float],
|
||||||
|
track: EndpointTrack,
|
||||||
|
medium_occlusion_frames: int,
|
||||||
|
) -> dict:
|
||||||
|
diagonal = bbox_diagonal(bbox)
|
||||||
|
height = max(bbox[3] - bbox[1], 1.0)
|
||||||
|
arm_reach = upper_length + forearm_length
|
||||||
|
hand_reach = min(0.13 * diagonal, 0.17 * height)
|
||||||
|
safety_margin = 0.025 * diagonal
|
||||||
|
outer_radius = arm_reach + hand_reach + safety_margin
|
||||||
|
inner_radius = max(abs(upper_length - forearm_length) - 0.04 * diagonal, 0.0)
|
||||||
|
if track.last_direction is None:
|
||||||
|
direction_deg = 0.0
|
||||||
|
half_angle_deg = 180.0
|
||||||
|
else:
|
||||||
|
direction = track_direction(track)
|
||||||
|
direction_deg = math.degrees(math.atan2(float(direction[1]), float(direction[0])))
|
||||||
|
half_angle_deg = min(
|
||||||
|
180.0,
|
||||||
|
65.0 + 2.0 * max(track.missed_frames - medium_occlusion_frames, 0),
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"geometry": "shoulder_sector",
|
||||||
|
"center": np.round(shoulder, 3).tolist(),
|
||||||
|
"inner_radius_px": round(inner_radius, 3),
|
||||||
|
"outer_radius_px": round(outer_radius, 3),
|
||||||
|
"arm_reach_px": round(arm_reach, 3),
|
||||||
|
"hand_reach_px": round(hand_reach, 3),
|
||||||
|
"safety_margin_px": round(safety_margin, 3),
|
||||||
|
"direction_deg": round(direction_deg, 3),
|
||||||
|
"half_angle_deg": round(half_angle_deg, 3),
|
||||||
|
"requires_door_intersection": True,
|
||||||
|
"render_full_region": False,
|
||||||
|
"distance_lower_bound_rule": "intersect shoulder sector with door danger band before alarming",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def reachable_region(
|
||||||
|
shoulder: np.ndarray,
|
||||||
|
upper_length: float,
|
||||||
|
forearm_length: float,
|
||||||
|
bbox: list[float],
|
||||||
|
track: EndpointTrack,
|
||||||
|
state: str = "reachable_long",
|
||||||
|
short_occlusion_frames: int = 12,
|
||||||
|
medium_occlusion_frames: int = 30,
|
||||||
|
) -> dict:
|
||||||
|
if state == "reachable_medium" and track.wrist is not None:
|
||||||
|
return medium_motion_region(
|
||||||
|
shoulder,
|
||||||
|
upper_length,
|
||||||
|
forearm_length,
|
||||||
|
bbox,
|
||||||
|
track,
|
||||||
|
short_occlusion_frames,
|
||||||
|
)
|
||||||
|
return long_reachable_region(
|
||||||
|
shoulder,
|
||||||
|
upper_length,
|
||||||
|
forearm_length,
|
||||||
|
bbox,
|
||||||
|
track,
|
||||||
|
medium_occlusion_frames,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def calibrated_bone_lengths(
|
||||||
|
upper_ratio: float,
|
||||||
|
forearm_ratio: float,
|
||||||
|
bbox: list[float],
|
||||||
|
) -> tuple[float, float]:
|
||||||
|
diagonal = bbox_diagonal(bbox)
|
||||||
|
height = max(bbox[3] - bbox[1], 1.0)
|
||||||
|
upper = upper_ratio * diagonal
|
||||||
|
forearm = forearm_ratio * diagonal
|
||||||
|
total = upper + forearm
|
||||||
|
# Keep only a loose sanity cap. The previous 0.55 * bbox-height cap was
|
||||||
|
# unsafe for close, clipped CMS views: a valid projected arm can occupy
|
||||||
|
# almost the full detection height. The ratios are already robust medians,
|
||||||
|
# so this cap only limits catastrophic calibration outliers.
|
||||||
|
maximum_total = 0.95 * diagonal
|
||||||
|
if total > maximum_total:
|
||||||
|
scale = maximum_total / total
|
||||||
|
upper *= scale
|
||||||
|
forearm *= scale
|
||||||
|
return upper, forearm
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
"""Runtime pose tracking helpers."""
|
||||||
|
from __future__ import annotations
|
||||||
|
import math
|
||||||
|
from collections import deque
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
import numpy as np
|
||||||
|
from pose_geometry import HAND_SLICES
|
||||||
|
from pose_fusion import BODY_SIDE_INDICES, PersonTrack
|
||||||
|
from pose_occlusion import EndpointTrack, Measurement, calibrated_bone_lengths, reachable_region, solve_two_bone_elbow, temporal_recovery_measurement, update_endpoint_track
|
||||||
|
|
||||||
|
|
||||||
|
def bbox_iou(a: np.ndarray, b: np.ndarray) -> float:
|
||||||
|
x1 = max(float(a[0]), float(b[0]))
|
||||||
|
y1 = max(float(a[1]), float(b[1]))
|
||||||
|
x2 = min(float(a[2]), float(b[2]))
|
||||||
|
y2 = min(float(a[3]), float(b[3]))
|
||||||
|
intersection = max(x2 - x1, 0.0) * max(y2 - y1, 0.0)
|
||||||
|
union = (
|
||||||
|
max(float(a[2] - a[0]), 0.0) * max(float(a[3] - a[1]), 0.0)
|
||||||
|
+ max(float(b[2] - b[0]), 0.0) * max(float(b[3] - b[1]), 0.0)
|
||||||
|
- intersection
|
||||||
|
)
|
||||||
|
return intersection / max(union, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
class BoxTracker:
|
||||||
|
def __init__(self, max_age: int = 30):
|
||||||
|
self.max_age = max_age
|
||||||
|
self.next_id = 1
|
||||||
|
self.tracks: dict[int, dict] = {}
|
||||||
|
|
||||||
|
def update(
|
||||||
|
self,
|
||||||
|
boxes: list[list[float]],
|
||||||
|
frame_idx: int,
|
||||||
|
image_shape: tuple[int, int],
|
||||||
|
) -> list[int]:
|
||||||
|
height, width = image_shape
|
||||||
|
image_diagonal = math.hypot(width, height)
|
||||||
|
active_ids = [
|
||||||
|
track_id
|
||||||
|
for track_id, track in self.tracks.items()
|
||||||
|
if frame_idx - track["last_frame"] <= self.max_age
|
||||||
|
]
|
||||||
|
self.tracks = {track_id: self.tracks[track_id] for track_id in active_ids}
|
||||||
|
pairs = []
|
||||||
|
for detection_idx, box in enumerate(boxes):
|
||||||
|
box_array = np.asarray(box, dtype=np.float64)
|
||||||
|
center = (box_array[:2] + box_array[2:]) * 0.5
|
||||||
|
for track_id in active_ids:
|
||||||
|
prior = np.asarray(self.tracks[track_id]["box"], dtype=np.float64)
|
||||||
|
prior_center = (prior[:2] + prior[2:]) * 0.5
|
||||||
|
iou = bbox_iou(box_array, prior)
|
||||||
|
center_distance = float(np.linalg.norm(center - prior_center)) / image_diagonal
|
||||||
|
if iou >= 0.05 or center_distance <= 0.18:
|
||||||
|
score = 2.0 * iou + max(0.0, 1.0 - center_distance / 0.18)
|
||||||
|
pairs.append((score, detection_idx, track_id))
|
||||||
|
|
||||||
|
assigned_detections = set()
|
||||||
|
assigned_tracks = set()
|
||||||
|
result: list[int | None] = [None] * len(boxes)
|
||||||
|
for _, detection_idx, track_id in sorted(pairs, reverse=True):
|
||||||
|
if detection_idx in assigned_detections or track_id in assigned_tracks:
|
||||||
|
continue
|
||||||
|
assigned_detections.add(detection_idx)
|
||||||
|
assigned_tracks.add(track_id)
|
||||||
|
result[detection_idx] = track_id
|
||||||
|
|
||||||
|
for detection_idx, box in enumerate(boxes):
|
||||||
|
track_id = result[detection_idx]
|
||||||
|
if track_id is None:
|
||||||
|
track_id = self.next_id
|
||||||
|
self.next_id += 1
|
||||||
|
result[detection_idx] = track_id
|
||||||
|
self.tracks[track_id] = {"box": box, "last_frame": frame_idx}
|
||||||
|
return [int(track_id) for track_id in result]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LivePersonState:
|
||||||
|
fusion: PersonTrack = field(default_factory=PersonTrack)
|
||||||
|
endpoints: dict[str, EndpointTrack] = field(
|
||||||
|
default_factory=lambda: {"left": EndpointTrack(), "right": EndpointTrack()}
|
||||||
|
)
|
||||||
|
bone_samples: dict[str, deque] = field(
|
||||||
|
default_factory=lambda: {
|
||||||
|
"left": deque(maxlen=120),
|
||||||
|
"right": deque(maxlen=120),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
last_frame: int = -1
|
||||||
|
|
||||||
|
|
||||||
|
def bbox_diagonal(bbox: list[float]) -> float:
|
||||||
|
return max(math.hypot(bbox[2] - bbox[0], bbox[3] - bbox[1]), 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def update_bone_samples(
|
||||||
|
state: LivePersonState,
|
||||||
|
keypoints: np.ndarray,
|
||||||
|
metadata: dict,
|
||||||
|
bbox: list[float],
|
||||||
|
) -> None:
|
||||||
|
diagonal = bbox_diagonal(bbox)
|
||||||
|
for side, (shoulder, elbow, wrist) in BODY_SIDE_INDICES.items():
|
||||||
|
meta = metadata[side]
|
||||||
|
if meta["limb_state"] != "consensus" or not meta["distance_eligible"]:
|
||||||
|
continue
|
||||||
|
upper = float(np.linalg.norm(keypoints[shoulder] - keypoints[elbow])) / diagonal
|
||||||
|
forearm = float(np.linalg.norm(keypoints[elbow] - keypoints[wrist])) / diagonal
|
||||||
|
if 0.04 <= upper <= 0.65 and 0.04 <= forearm <= 0.65:
|
||||||
|
state.bone_samples[side].append((upper, forearm))
|
||||||
|
|
||||||
|
|
||||||
|
def current_bone_ratios(state: LivePersonState, side: str) -> tuple[float, float]:
|
||||||
|
samples = state.bone_samples[side]
|
||||||
|
if len(samples) < 10:
|
||||||
|
opposite = "right" if side == "left" else "left"
|
||||||
|
if len(state.bone_samples[opposite]) >= 10:
|
||||||
|
samples = state.bone_samples[opposite]
|
||||||
|
if not samples:
|
||||||
|
return 0.24, 0.22
|
||||||
|
values = np.asarray(samples, dtype=np.float64)
|
||||||
|
return float(np.median(values[:, 0])), float(np.median(values[:, 1]))
|
||||||
|
|
||||||
|
|
||||||
|
def update_live_v2(
|
||||||
|
frame_idx: int,
|
||||||
|
bbox: list[float],
|
||||||
|
selected_keypoints: np.ndarray,
|
||||||
|
smoothed_keypoints: np.ndarray,
|
||||||
|
fusion_metadata: dict,
|
||||||
|
state: LivePersonState,
|
||||||
|
assigned_measurements: dict[str, Measurement | None] | None,
|
||||||
|
identity_assignments: dict[str, dict] | None,
|
||||||
|
short_occlusion_frames: int,
|
||||||
|
medium_occlusion_frames: int,
|
||||||
|
) -> tuple[np.ndarray, dict]:
|
||||||
|
frame_gap = 0 if state.last_frame < 0 else max(frame_idx - state.last_frame - 1, 0)
|
||||||
|
for endpoint in state.endpoints.values():
|
||||||
|
endpoint.missed_frames += frame_gap
|
||||||
|
state.last_frame = frame_idx
|
||||||
|
update_bone_samples(state, selected_keypoints, fusion_metadata, bbox)
|
||||||
|
|
||||||
|
output = smoothed_keypoints.copy()
|
||||||
|
output_metadata = {}
|
||||||
|
diagonal = bbox_diagonal(bbox)
|
||||||
|
for side, (shoulder_idx, elbow_idx, wrist_idx) in BODY_SIDE_INDICES.items():
|
||||||
|
endpoint = state.endpoints[side]
|
||||||
|
meta = fusion_metadata[side]
|
||||||
|
start, end = HAND_SLICES[side]
|
||||||
|
measurement = (
|
||||||
|
assigned_measurements.get(side)
|
||||||
|
if assigned_measurements is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
if assigned_measurements is None and meta["alarm_eligible"]:
|
||||||
|
measurement = Measurement(
|
||||||
|
keypoints=selected_keypoints[start:end],
|
||||||
|
precise=bool(meta["distance_eligible"]),
|
||||||
|
provisional=not bool(meta["distance_eligible"]),
|
||||||
|
quality=float(meta.get("selected_hand_score", 0.0)),
|
||||||
|
)
|
||||||
|
if measurement is None:
|
||||||
|
measurement = temporal_recovery_measurement(
|
||||||
|
selected_keypoints,
|
||||||
|
meta,
|
||||||
|
side,
|
||||||
|
bbox,
|
||||||
|
endpoint,
|
||||||
|
medium_occlusion_frames,
|
||||||
|
)
|
||||||
|
returning_after_long = (
|
||||||
|
measurement is not None
|
||||||
|
and endpoint.missed_frames > short_occlusion_frames
|
||||||
|
)
|
||||||
|
wrist, hand, endpoint_state = update_endpoint_track(
|
||||||
|
endpoint,
|
||||||
|
measurement,
|
||||||
|
frame_idx,
|
||||||
|
short_occlusion_frames,
|
||||||
|
medium_occlusion_frames,
|
||||||
|
)
|
||||||
|
shoulder = smoothed_keypoints[shoulder_idx]
|
||||||
|
upper_ratio, forearm_ratio = current_bone_ratios(state, side)
|
||||||
|
upper_length, forearm_length = calibrated_bone_lengths(
|
||||||
|
upper_ratio, forearm_ratio, bbox
|
||||||
|
)
|
||||||
|
region = None
|
||||||
|
if wrist is not None and hand is not None:
|
||||||
|
reference_elbow = (
|
||||||
|
selected_keypoints[elbow_idx]
|
||||||
|
if returning_after_long or endpoint.last_elbow is None
|
||||||
|
else endpoint.last_elbow
|
||||||
|
)
|
||||||
|
use_ik = (
|
||||||
|
meta["limb_state"] != "consensus"
|
||||||
|
or endpoint_state
|
||||||
|
in ("provisional", "recovered", "reassigned", "predicted_short")
|
||||||
|
)
|
||||||
|
elbow = (
|
||||||
|
solve_two_bone_elbow(
|
||||||
|
shoulder,
|
||||||
|
wrist,
|
||||||
|
upper_length,
|
||||||
|
forearm_length,
|
||||||
|
reference_elbow,
|
||||||
|
)
|
||||||
|
if use_ik
|
||||||
|
else smoothed_keypoints[elbow_idx]
|
||||||
|
)
|
||||||
|
endpoint.last_elbow = elbow.copy()
|
||||||
|
direction = wrist - shoulder
|
||||||
|
if float(np.linalg.norm(direction)) > 1e-6:
|
||||||
|
endpoint.last_direction = direction / np.linalg.norm(direction)
|
||||||
|
output[shoulder_idx] = shoulder
|
||||||
|
output[elbow_idx] = elbow
|
||||||
|
output[wrist_idx] = wrist
|
||||||
|
output[start:end] = hand
|
||||||
|
uncertainty = (
|
||||||
|
0.025 * diagonal
|
||||||
|
if endpoint_state == "observed"
|
||||||
|
else (0.04 + 0.015 * endpoint.missed_frames) * diagonal
|
||||||
|
)
|
||||||
|
precise = endpoint_state == "observed"
|
||||||
|
else:
|
||||||
|
region = reachable_region(
|
||||||
|
shoulder,
|
||||||
|
upper_length,
|
||||||
|
forearm_length,
|
||||||
|
bbox,
|
||||||
|
endpoint,
|
||||||
|
endpoint_state,
|
||||||
|
short_occlusion_frames,
|
||||||
|
medium_occlusion_frames,
|
||||||
|
)
|
||||||
|
uncertainty = region["outer_radius_px"]
|
||||||
|
precise = False
|
||||||
|
output_metadata[side] = {
|
||||||
|
"state": endpoint_state,
|
||||||
|
"precise_distance": precise,
|
||||||
|
"fast_alarm_candidate": True,
|
||||||
|
"risk_mode": (
|
||||||
|
"precise_hand"
|
||||||
|
if endpoint_state == "observed"
|
||||||
|
else (
|
||||||
|
"motion_corridor_intersection"
|
||||||
|
if endpoint_state == "reachable_medium"
|
||||||
|
else (
|
||||||
|
"door_intersection_only"
|
||||||
|
if endpoint_state == "reachable_long"
|
||||||
|
else (
|
||||||
|
"reassigned_hand"
|
||||||
|
if endpoint_state == "reassigned"
|
||||||
|
else "temporal_hand"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"requires_door_intersection": endpoint_state
|
||||||
|
in ("reachable_medium", "reachable_long"),
|
||||||
|
"identity_assignment": (
|
||||||
|
identity_assignments.get(side)
|
||||||
|
if identity_assignments is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"missed_frames": endpoint.missed_frames,
|
||||||
|
"uncertainty_radius_px": round(float(uncertainty), 3),
|
||||||
|
"bone_lengths_px": {
|
||||||
|
"upper": round(upper_length, 3),
|
||||||
|
"forearm": round(forearm_length, 3),
|
||||||
|
},
|
||||||
|
"reachable_region": region,
|
||||||
|
}
|
||||||
|
return output, output_metadata
|
||||||
|
|
||||||
|
|
||||||
|
def mask_other_people(
|
||||||
|
frame: np.ndarray,
|
||||||
|
boxes: list[list[float]],
|
||||||
|
target_idx: int,
|
||||||
|
) -> np.ndarray:
|
||||||
|
masked = frame.copy()
|
||||||
|
height, width = frame.shape[:2]
|
||||||
|
mask = np.zeros((height, width), dtype=bool)
|
||||||
|
target = boxes[target_idx]
|
||||||
|
target_area = max((target[2] - target[0]) * (target[3] - target[1]), 1.0)
|
||||||
|
for idx, bbox in enumerate(boxes):
|
||||||
|
if idx == target_idx:
|
||||||
|
continue
|
||||||
|
other_area = max((bbox[2] - bbox[0]) * (bbox[3] - bbox[1]), 1.0)
|
||||||
|
# The failure in this video is the small background person being
|
||||||
|
# absorbed by the much larger foreground person's 3:4 pose crop.
|
||||||
|
# Mask only markedly smaller people. Masking a larger occluder while
|
||||||
|
# estimating a small person removes genuine target pixels at overlap.
|
||||||
|
if other_area >= 0.5 * target_area:
|
||||||
|
continue
|
||||||
|
x1, y1, x2, y2 = (int(round(value)) for value in bbox)
|
||||||
|
x1, x2 = max(0, x1), min(width - 1, x2)
|
||||||
|
y1, y2 = max(0, y1), min(height - 1, y2)
|
||||||
|
mask[y1:y2 + 1, x1:x2 + 1] = True
|
||||||
|
|
||||||
|
masked[mask] = (124, 116, 104)
|
||||||
|
return masked
|
||||||
@@ -1,683 +0,0 @@
|
|||||||
"""End-to-end optimized RTMW-L prediction for CMS videos.
|
|
||||||
|
|
||||||
Pipeline:
|
|
||||||
YOLO11s person -> fixed-camera false-positive filters -> lightweight track IDs
|
|
||||||
-> RTMW-L raw/smaller-person-isolated views -> temporal fusion -> causal
|
|
||||||
occlusion-aware V4 global hand assignment. Reachable regions stay in JSON and are hidden in the main
|
|
||||||
comparison video.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import math
|
|
||||||
import time
|
|
||||||
from collections import Counter, defaultdict, deque
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import cv2
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from compare_rtmw_ml import HAND_SLICES, draw_pose, summarize
|
|
||||||
from fuse_rtmw_l_temporal import (
|
|
||||||
BODY_SIDE_INDICES,
|
|
||||||
PersonTrack,
|
|
||||||
adaptive_smooth,
|
|
||||||
build_global_hands,
|
|
||||||
draw_fused,
|
|
||||||
fuse_person,
|
|
||||||
)
|
|
||||||
from occlusion_aware_pose_v2 import (
|
|
||||||
EndpointTrack,
|
|
||||||
Measurement,
|
|
||||||
calibrated_bone_lengths,
|
|
||||||
draw_v2,
|
|
||||||
reachable_region,
|
|
||||||
resolve_global_hand_assignments,
|
|
||||||
solve_two_bone_elbow,
|
|
||||||
temporal_recovery_measurement,
|
|
||||||
update_endpoint_track,
|
|
||||||
)
|
|
||||||
from rtmw_l_person_mask_ablation import mask_other_people
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("--videos", type=Path, nargs="+", required=True)
|
|
||||||
parser.add_argument("--det-model", type=Path, required=True)
|
|
||||||
parser.add_argument("--pose-model", type=Path, required=True)
|
|
||||||
parser.add_argument("--output", type=Path, required=True)
|
|
||||||
parser.add_argument(
|
|
||||||
"--det-device",
|
|
||||||
default="0",
|
|
||||||
help="Ultralytics YOLO device, for example 0, cuda:0, or cpu.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--pose-device",
|
|
||||||
default="cpu",
|
|
||||||
help="RTMW ONNX Runtime device: cpu or cuda.",
|
|
||||||
)
|
|
||||||
parser.add_argument("--det-imgsz", type=int, default=960)
|
|
||||||
parser.add_argument("--det-conf", type=float, default=0.30)
|
|
||||||
parser.add_argument("--det-roi-y-max-ratio", type=float, default=1.0)
|
|
||||||
parser.add_argument("--short-occlusion-frames", type=int, default=12)
|
|
||||||
parser.add_argument("--medium-occlusion-frames", type=int, default=30)
|
|
||||||
parser.add_argument("--max-people", type=int, default=3)
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def bbox_iou(a: np.ndarray, b: np.ndarray) -> float:
|
|
||||||
x1 = max(float(a[0]), float(b[0]))
|
|
||||||
y1 = max(float(a[1]), float(b[1]))
|
|
||||||
x2 = min(float(a[2]), float(b[2]))
|
|
||||||
y2 = min(float(a[3]), float(b[3]))
|
|
||||||
intersection = max(x2 - x1, 0.0) * max(y2 - y1, 0.0)
|
|
||||||
union = (
|
|
||||||
max(float(a[2] - a[0]), 0.0) * max(float(a[3] - a[1]), 0.0)
|
|
||||||
+ max(float(b[2] - b[0]), 0.0) * max(float(b[3] - b[1]), 0.0)
|
|
||||||
- intersection
|
|
||||||
)
|
|
||||||
return intersection / max(union, 1.0)
|
|
||||||
|
|
||||||
|
|
||||||
class BoxTracker:
|
|
||||||
def __init__(self, max_age: int = 30):
|
|
||||||
self.max_age = max_age
|
|
||||||
self.next_id = 1
|
|
||||||
self.tracks: dict[int, dict] = {}
|
|
||||||
|
|
||||||
def update(
|
|
||||||
self,
|
|
||||||
boxes: list[list[float]],
|
|
||||||
frame_idx: int,
|
|
||||||
image_shape: tuple[int, int],
|
|
||||||
) -> list[int]:
|
|
||||||
height, width = image_shape
|
|
||||||
image_diagonal = math.hypot(width, height)
|
|
||||||
active_ids = [
|
|
||||||
track_id
|
|
||||||
for track_id, track in self.tracks.items()
|
|
||||||
if frame_idx - track["last_frame"] <= self.max_age
|
|
||||||
]
|
|
||||||
self.tracks = {track_id: self.tracks[track_id] for track_id in active_ids}
|
|
||||||
pairs = []
|
|
||||||
for detection_idx, box in enumerate(boxes):
|
|
||||||
box_array = np.asarray(box, dtype=np.float64)
|
|
||||||
center = (box_array[:2] + box_array[2:]) * 0.5
|
|
||||||
for track_id in active_ids:
|
|
||||||
prior = np.asarray(self.tracks[track_id]["box"], dtype=np.float64)
|
|
||||||
prior_center = (prior[:2] + prior[2:]) * 0.5
|
|
||||||
iou = bbox_iou(box_array, prior)
|
|
||||||
center_distance = float(np.linalg.norm(center - prior_center)) / image_diagonal
|
|
||||||
if iou >= 0.05 or center_distance <= 0.18:
|
|
||||||
score = 2.0 * iou + max(0.0, 1.0 - center_distance / 0.18)
|
|
||||||
pairs.append((score, detection_idx, track_id))
|
|
||||||
|
|
||||||
assigned_detections = set()
|
|
||||||
assigned_tracks = set()
|
|
||||||
result: list[int | None] = [None] * len(boxes)
|
|
||||||
for _, detection_idx, track_id in sorted(pairs, reverse=True):
|
|
||||||
if detection_idx in assigned_detections or track_id in assigned_tracks:
|
|
||||||
continue
|
|
||||||
assigned_detections.add(detection_idx)
|
|
||||||
assigned_tracks.add(track_id)
|
|
||||||
result[detection_idx] = track_id
|
|
||||||
|
|
||||||
for detection_idx, box in enumerate(boxes):
|
|
||||||
track_id = result[detection_idx]
|
|
||||||
if track_id is None:
|
|
||||||
track_id = self.next_id
|
|
||||||
self.next_id += 1
|
|
||||||
result[detection_idx] = track_id
|
|
||||||
self.tracks[track_id] = {"box": box, "last_frame": frame_idx}
|
|
||||||
return [int(track_id) for track_id in result]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class LivePersonState:
|
|
||||||
fusion: PersonTrack = field(default_factory=PersonTrack)
|
|
||||||
endpoints: dict[str, EndpointTrack] = field(
|
|
||||||
default_factory=lambda: {"left": EndpointTrack(), "right": EndpointTrack()}
|
|
||||||
)
|
|
||||||
bone_samples: dict[str, deque] = field(
|
|
||||||
default_factory=lambda: {
|
|
||||||
"left": deque(maxlen=120),
|
|
||||||
"right": deque(maxlen=120),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
last_frame: int = -1
|
|
||||||
|
|
||||||
|
|
||||||
def bbox_diagonal(bbox: list[float]) -> float:
|
|
||||||
return max(math.hypot(bbox[2] - bbox[0], bbox[3] - bbox[1]), 1.0)
|
|
||||||
|
|
||||||
|
|
||||||
def update_bone_samples(
|
|
||||||
state: LivePersonState,
|
|
||||||
keypoints: np.ndarray,
|
|
||||||
metadata: dict,
|
|
||||||
bbox: list[float],
|
|
||||||
) -> None:
|
|
||||||
diagonal = bbox_diagonal(bbox)
|
|
||||||
for side, (shoulder, elbow, wrist) in BODY_SIDE_INDICES.items():
|
|
||||||
meta = metadata[side]
|
|
||||||
if meta["limb_state"] != "consensus" or not meta["distance_eligible"]:
|
|
||||||
continue
|
|
||||||
upper = float(np.linalg.norm(keypoints[shoulder] - keypoints[elbow])) / diagonal
|
|
||||||
forearm = float(np.linalg.norm(keypoints[elbow] - keypoints[wrist])) / diagonal
|
|
||||||
if 0.04 <= upper <= 0.65 and 0.04 <= forearm <= 0.65:
|
|
||||||
state.bone_samples[side].append((upper, forearm))
|
|
||||||
|
|
||||||
|
|
||||||
def current_bone_ratios(state: LivePersonState, side: str) -> tuple[float, float]:
|
|
||||||
samples = state.bone_samples[side]
|
|
||||||
if len(samples) < 10:
|
|
||||||
opposite = "right" if side == "left" else "left"
|
|
||||||
if len(state.bone_samples[opposite]) >= 10:
|
|
||||||
samples = state.bone_samples[opposite]
|
|
||||||
if not samples:
|
|
||||||
return 0.24, 0.22
|
|
||||||
values = np.asarray(samples, dtype=np.float64)
|
|
||||||
return float(np.median(values[:, 0])), float(np.median(values[:, 1]))
|
|
||||||
|
|
||||||
|
|
||||||
def update_live_v2(
|
|
||||||
frame_idx: int,
|
|
||||||
bbox: list[float],
|
|
||||||
selected_keypoints: np.ndarray,
|
|
||||||
smoothed_keypoints: np.ndarray,
|
|
||||||
fusion_metadata: dict,
|
|
||||||
state: LivePersonState,
|
|
||||||
assigned_measurements: dict[str, Measurement | None] | None,
|
|
||||||
identity_assignments: dict[str, dict] | None,
|
|
||||||
short_occlusion_frames: int,
|
|
||||||
medium_occlusion_frames: int,
|
|
||||||
) -> tuple[np.ndarray, dict]:
|
|
||||||
frame_gap = 0 if state.last_frame < 0 else max(frame_idx - state.last_frame - 1, 0)
|
|
||||||
for endpoint in state.endpoints.values():
|
|
||||||
endpoint.missed_frames += frame_gap
|
|
||||||
state.last_frame = frame_idx
|
|
||||||
update_bone_samples(state, selected_keypoints, fusion_metadata, bbox)
|
|
||||||
|
|
||||||
output = smoothed_keypoints.copy()
|
|
||||||
output_metadata = {}
|
|
||||||
diagonal = bbox_diagonal(bbox)
|
|
||||||
for side, (shoulder_idx, elbow_idx, wrist_idx) in BODY_SIDE_INDICES.items():
|
|
||||||
endpoint = state.endpoints[side]
|
|
||||||
meta = fusion_metadata[side]
|
|
||||||
start, end = HAND_SLICES[side]
|
|
||||||
measurement = (
|
|
||||||
assigned_measurements.get(side)
|
|
||||||
if assigned_measurements is not None
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
if assigned_measurements is None and meta["alarm_eligible"]:
|
|
||||||
measurement = Measurement(
|
|
||||||
keypoints=selected_keypoints[start:end],
|
|
||||||
precise=bool(meta["distance_eligible"]),
|
|
||||||
provisional=not bool(meta["distance_eligible"]),
|
|
||||||
quality=float(meta.get("selected_hand_score", 0.0)),
|
|
||||||
)
|
|
||||||
if measurement is None:
|
|
||||||
measurement = temporal_recovery_measurement(
|
|
||||||
selected_keypoints,
|
|
||||||
meta,
|
|
||||||
side,
|
|
||||||
bbox,
|
|
||||||
endpoint,
|
|
||||||
medium_occlusion_frames,
|
|
||||||
)
|
|
||||||
returning_after_long = (
|
|
||||||
measurement is not None
|
|
||||||
and endpoint.missed_frames > short_occlusion_frames
|
|
||||||
)
|
|
||||||
wrist, hand, endpoint_state = update_endpoint_track(
|
|
||||||
endpoint,
|
|
||||||
measurement,
|
|
||||||
frame_idx,
|
|
||||||
short_occlusion_frames,
|
|
||||||
medium_occlusion_frames,
|
|
||||||
)
|
|
||||||
shoulder = smoothed_keypoints[shoulder_idx]
|
|
||||||
upper_ratio, forearm_ratio = current_bone_ratios(state, side)
|
|
||||||
upper_length, forearm_length = calibrated_bone_lengths(
|
|
||||||
upper_ratio, forearm_ratio, bbox
|
|
||||||
)
|
|
||||||
region = None
|
|
||||||
if wrist is not None and hand is not None:
|
|
||||||
reference_elbow = (
|
|
||||||
selected_keypoints[elbow_idx]
|
|
||||||
if returning_after_long or endpoint.last_elbow is None
|
|
||||||
else endpoint.last_elbow
|
|
||||||
)
|
|
||||||
use_ik = (
|
|
||||||
meta["limb_state"] != "consensus"
|
|
||||||
or endpoint_state
|
|
||||||
in ("provisional", "recovered", "reassigned", "predicted_short")
|
|
||||||
)
|
|
||||||
elbow = (
|
|
||||||
solve_two_bone_elbow(
|
|
||||||
shoulder,
|
|
||||||
wrist,
|
|
||||||
upper_length,
|
|
||||||
forearm_length,
|
|
||||||
reference_elbow,
|
|
||||||
)
|
|
||||||
if use_ik
|
|
||||||
else smoothed_keypoints[elbow_idx]
|
|
||||||
)
|
|
||||||
endpoint.last_elbow = elbow.copy()
|
|
||||||
direction = wrist - shoulder
|
|
||||||
if float(np.linalg.norm(direction)) > 1e-6:
|
|
||||||
endpoint.last_direction = direction / np.linalg.norm(direction)
|
|
||||||
output[shoulder_idx] = shoulder
|
|
||||||
output[elbow_idx] = elbow
|
|
||||||
output[wrist_idx] = wrist
|
|
||||||
output[start:end] = hand
|
|
||||||
uncertainty = (
|
|
||||||
0.025 * diagonal
|
|
||||||
if endpoint_state == "observed"
|
|
||||||
else (0.04 + 0.015 * endpoint.missed_frames) * diagonal
|
|
||||||
)
|
|
||||||
precise = endpoint_state == "observed"
|
|
||||||
else:
|
|
||||||
region = reachable_region(
|
|
||||||
shoulder,
|
|
||||||
upper_length,
|
|
||||||
forearm_length,
|
|
||||||
bbox,
|
|
||||||
endpoint,
|
|
||||||
endpoint_state,
|
|
||||||
short_occlusion_frames,
|
|
||||||
medium_occlusion_frames,
|
|
||||||
)
|
|
||||||
uncertainty = region["outer_radius_px"]
|
|
||||||
precise = False
|
|
||||||
output_metadata[side] = {
|
|
||||||
"state": endpoint_state,
|
|
||||||
"precise_distance": precise,
|
|
||||||
"fast_alarm_candidate": True,
|
|
||||||
"risk_mode": (
|
|
||||||
"precise_hand"
|
|
||||||
if endpoint_state == "observed"
|
|
||||||
else (
|
|
||||||
"motion_corridor_intersection"
|
|
||||||
if endpoint_state == "reachable_medium"
|
|
||||||
else (
|
|
||||||
"door_intersection_only"
|
|
||||||
if endpoint_state == "reachable_long"
|
|
||||||
else (
|
|
||||||
"reassigned_hand"
|
|
||||||
if endpoint_state == "reassigned"
|
|
||||||
else "temporal_hand"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"requires_door_intersection": endpoint_state
|
|
||||||
in ("reachable_medium", "reachable_long"),
|
|
||||||
"identity_assignment": (
|
|
||||||
identity_assignments.get(side)
|
|
||||||
if identity_assignments is not None
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
"missed_frames": endpoint.missed_frames,
|
|
||||||
"uncertainty_radius_px": round(float(uncertainty), 3),
|
|
||||||
"bone_lengths_px": {
|
|
||||||
"upper": round(upper_length, 3),
|
|
||||||
"forearm": round(forearm_length, 3),
|
|
||||||
},
|
|
||||||
"reachable_region": region,
|
|
||||||
}
|
|
||||||
return output, output_metadata
|
|
||||||
|
|
||||||
|
|
||||||
def filter_person_boxes(
|
|
||||||
result,
|
|
||||||
width: int,
|
|
||||||
height: int,
|
|
||||||
max_people: int,
|
|
||||||
) -> tuple[list[list[float]], list[float], Counter]:
|
|
||||||
boxes = result.boxes.xyxy.cpu().numpy()
|
|
||||||
scores = result.boxes.conf.cpu().numpy()
|
|
||||||
rejected = Counter()
|
|
||||||
kept = []
|
|
||||||
for box, score in zip(boxes, scores):
|
|
||||||
center_x = float(box[0] + box[2]) * 0.5
|
|
||||||
box_height = float(box[3] - box[1])
|
|
||||||
if center_x > 0.80 * width:
|
|
||||||
rejected["right_reflection_roi"] += 1
|
|
||||||
continue
|
|
||||||
if box_height < 0.20 * height:
|
|
||||||
rejected["too_short"] += 1
|
|
||||||
continue
|
|
||||||
kept.append((box.tolist(), float(score)))
|
|
||||||
kept.sort(key=lambda item: item[1], reverse=True)
|
|
||||||
if len(kept) > max_people:
|
|
||||||
rejected["over_max_people"] += len(kept) - max_people
|
|
||||||
kept = kept[:max_people]
|
|
||||||
kept.sort(key=lambda item: item[0][0])
|
|
||||||
return [item[0] for item in kept], [item[1] for item in kept], rejected
|
|
||||||
|
|
||||||
|
|
||||||
def process_video(
|
|
||||||
video_path: Path,
|
|
||||||
output_dir: Path,
|
|
||||||
detector: YOLO,
|
|
||||||
pose: RTMPose,
|
|
||||||
det_device: str,
|
|
||||||
det_imgsz: int,
|
|
||||||
det_conf: float,
|
|
||||||
det_roi_y_max_ratio: float,
|
|
||||||
short_occlusion_frames: int,
|
|
||||||
medium_occlusion_frames: int,
|
|
||||||
max_people: int,
|
|
||||||
video_label: str,
|
|
||||||
) -> dict:
|
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
cap = cv2.VideoCapture(str(video_path))
|
|
||||||
fps = float(cap.get(cv2.CAP_PROP_FPS))
|
|
||||||
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
||||||
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
||||||
writer = cv2.VideoWriter(
|
|
||||||
str(output_dir / "raw_temporal_occlusion_v4.mp4"),
|
|
||||||
cv2.VideoWriter_fourcc(*"mp4v"),
|
|
||||||
fps,
|
|
||||||
(1920, 360),
|
|
||||||
)
|
|
||||||
|
|
||||||
box_tracker = BoxTracker(max_age=30)
|
|
||||||
people: dict[int, LivePersonState] = {}
|
|
||||||
records = []
|
|
||||||
detection_counts = Counter()
|
|
||||||
rejection_counts = Counter()
|
|
||||||
state_counts = Counter()
|
|
||||||
timings = defaultdict(list)
|
|
||||||
isolated_inferences = 0
|
|
||||||
frame_idx = 0
|
|
||||||
|
|
||||||
while True:
|
|
||||||
ok, frame = cap.read()
|
|
||||||
if not ok:
|
|
||||||
break
|
|
||||||
started = time.perf_counter()
|
|
||||||
detection_height = max(1, min(height, round(height * det_roi_y_max_ratio)))
|
|
||||||
detection = detector.predict(
|
|
||||||
frame[:detection_height],
|
|
||||||
imgsz=det_imgsz,
|
|
||||||
conf=det_conf,
|
|
||||||
iou=0.30,
|
|
||||||
classes=[0],
|
|
||||||
device=det_device,
|
|
||||||
verbose=False,
|
|
||||||
)[0]
|
|
||||||
timings["detector_ms"].append((time.perf_counter() - started) * 1000.0)
|
|
||||||
boxes, confidences, rejected = filter_person_boxes(
|
|
||||||
detection, width, height, max_people
|
|
||||||
)
|
|
||||||
rejection_counts.update(rejected)
|
|
||||||
detection_counts[len(boxes)] += 1
|
|
||||||
track_ids = box_tracker.update(boxes, frame_idx, (height, width))
|
|
||||||
|
|
||||||
if boxes:
|
|
||||||
started = time.perf_counter()
|
|
||||||
raw_keypoints, raw_scores = pose(frame, bboxes=boxes)
|
|
||||||
timings["raw_pose_ms"].append((time.perf_counter() - started) * 1000.0)
|
|
||||||
isolated_keypoints = raw_keypoints.copy()
|
|
||||||
isolated_scores = raw_scores.copy()
|
|
||||||
started = time.perf_counter()
|
|
||||||
for person_idx, bbox in enumerate(boxes):
|
|
||||||
target_area = max((bbox[2] - bbox[0]) * (bbox[3] - bbox[1]), 1.0)
|
|
||||||
needs_isolation = any(
|
|
||||||
idx != person_idx
|
|
||||||
and max((other[2] - other[0]) * (other[3] - other[1]), 1.0)
|
|
||||||
< 0.5 * target_area
|
|
||||||
for idx, other in enumerate(boxes)
|
|
||||||
)
|
|
||||||
if not needs_isolation:
|
|
||||||
continue
|
|
||||||
masked = mask_other_people(frame, boxes, person_idx)
|
|
||||||
kpts, scores = pose(masked, bboxes=[bbox])
|
|
||||||
isolated_keypoints[person_idx] = kpts[0]
|
|
||||||
isolated_scores[person_idx] = scores[0]
|
|
||||||
isolated_inferences += 1
|
|
||||||
timings["isolated_extra_ms"].append((time.perf_counter() - started) * 1000.0)
|
|
||||||
else:
|
|
||||||
raw_keypoints = np.empty((0, 133, 2), dtype=np.float64)
|
|
||||||
raw_scores = np.empty((0, 133), dtype=np.float64)
|
|
||||||
isolated_keypoints = raw_keypoints.copy()
|
|
||||||
isolated_scores = raw_scores.copy()
|
|
||||||
|
|
||||||
selected_list = []
|
|
||||||
smoothed_list = []
|
|
||||||
fusion_meta_list = []
|
|
||||||
person_states = []
|
|
||||||
for person_idx, (bbox, track_id) in enumerate(zip(boxes, track_ids)):
|
|
||||||
state = people.setdefault(track_id, LivePersonState())
|
|
||||||
selected, fusion_meta = fuse_person(
|
|
||||||
raw_keypoints[person_idx],
|
|
||||||
raw_scores[person_idx],
|
|
||||||
isolated_keypoints[person_idx],
|
|
||||||
isolated_scores[person_idx],
|
|
||||||
bbox,
|
|
||||||
state.fusion,
|
|
||||||
)
|
|
||||||
smoothed = adaptive_smooth(selected, bbox, state.fusion)
|
|
||||||
selected_list.append(selected)
|
|
||||||
smoothed_list.append(smoothed)
|
|
||||||
fusion_meta_list.append(fusion_meta)
|
|
||||||
person_states.append(state)
|
|
||||||
|
|
||||||
frame_measurements = {}
|
|
||||||
endpoint_tracks = {}
|
|
||||||
for person_idx, (selected, fusion_meta, state) in enumerate(
|
|
||||||
zip(selected_list, fusion_meta_list, person_states)
|
|
||||||
):
|
|
||||||
for side, (start, end) in HAND_SLICES.items():
|
|
||||||
meta = fusion_meta[side]
|
|
||||||
frame_measurements[(person_idx, side)] = (
|
|
||||||
Measurement(
|
|
||||||
keypoints=selected[start:end],
|
|
||||||
precise=bool(meta["distance_eligible"]),
|
|
||||||
provisional=not bool(meta["distance_eligible"]),
|
|
||||||
quality=float(meta.get("selected_hand_score", 0.0)),
|
|
||||||
)
|
|
||||||
if meta["alarm_eligible"]
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
endpoint_tracks[(person_idx, side)] = state.endpoints[side]
|
|
||||||
frame_measurements, assignment_events = resolve_global_hand_assignments(
|
|
||||||
frame_measurements,
|
|
||||||
endpoint_tracks,
|
|
||||||
boxes,
|
|
||||||
medium_occlusion_frames,
|
|
||||||
)
|
|
||||||
|
|
||||||
v2_list = []
|
|
||||||
v2_meta_list = []
|
|
||||||
for person_idx, (bbox, selected, smoothed, fusion_meta, state) in enumerate(
|
|
||||||
zip(
|
|
||||||
boxes,
|
|
||||||
selected_list,
|
|
||||||
smoothed_list,
|
|
||||||
fusion_meta_list,
|
|
||||||
person_states,
|
|
||||||
)
|
|
||||||
):
|
|
||||||
v2, v2_meta = update_live_v2(
|
|
||||||
frame_idx,
|
|
||||||
bbox,
|
|
||||||
selected,
|
|
||||||
smoothed,
|
|
||||||
fusion_meta,
|
|
||||||
state,
|
|
||||||
{
|
|
||||||
side: frame_measurements[(person_idx, side)]
|
|
||||||
for side in ("left", "right")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
side: assignment_events.get((person_idx, side))
|
|
||||||
for side in ("left", "right")
|
|
||||||
},
|
|
||||||
short_occlusion_frames,
|
|
||||||
medium_occlusion_frames,
|
|
||||||
)
|
|
||||||
v2_list.append(v2)
|
|
||||||
v2_meta_list.append(v2_meta)
|
|
||||||
for side in ("left", "right"):
|
|
||||||
state_counts[f"{v2_meta[side]['state']}"] += 1
|
|
||||||
|
|
||||||
global_hands = build_global_hands(selected_list, fusion_meta_list)
|
|
||||||
records.append(
|
|
||||||
{
|
|
||||||
"frame_index": frame_idx,
|
|
||||||
"timestamp_ms": round(frame_idx / fps * 1000.0, 3),
|
|
||||||
"track_ids": track_ids,
|
|
||||||
"boxes": np.round(np.asarray(boxes), 3).tolist() if boxes else [],
|
|
||||||
"det_confidences": [round(value, 4) for value in confidences],
|
|
||||||
"raw_keypoints": np.round(raw_keypoints, 3).tolist(),
|
|
||||||
"temporal_selected_keypoints": np.round(np.asarray(selected_list), 3).tolist(),
|
|
||||||
"temporal_keypoints": np.round(np.asarray(smoothed_list), 3).tolist(),
|
|
||||||
"temporal_metadata": fusion_meta_list,
|
|
||||||
"v2_keypoints": np.round(np.asarray(v2_list), 3).tolist(),
|
|
||||||
"v2_metadata": v2_meta_list,
|
|
||||||
"global_hands": global_hands,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
raw_panel = draw_pose(
|
|
||||||
frame,
|
|
||||||
boxes,
|
|
||||||
raw_keypoints,
|
|
||||||
f"{video_label} RTMW-L raw | frame {frame_idx}",
|
|
||||||
)
|
|
||||||
temporal_panel = draw_fused(
|
|
||||||
frame,
|
|
||||||
boxes,
|
|
||||||
smoothed_list,
|
|
||||||
fusion_meta_list,
|
|
||||||
f"{video_label} temporal fusion | frame {frame_idx}",
|
|
||||||
)
|
|
||||||
v2_panel = draw_v2(
|
|
||||||
frame,
|
|
||||||
boxes,
|
|
||||||
np.asarray(v2_list),
|
|
||||||
v2_meta_list,
|
|
||||||
f"{video_label} occlusion V4 | frame {frame_idx}",
|
|
||||||
show_reachable=False,
|
|
||||||
)
|
|
||||||
panels = [
|
|
||||||
cv2.resize(panel, (640, 360), interpolation=cv2.INTER_AREA)
|
|
||||||
for panel in (raw_panel, temporal_panel, v2_panel)
|
|
||||||
]
|
|
||||||
writer.write(np.hstack(panels))
|
|
||||||
if frame_idx == 0 and video_path.suffix.lower() in {
|
|
||||||
".jpg",
|
|
||||||
".jpeg",
|
|
||||||
".png",
|
|
||||||
".bmp",
|
|
||||||
".webp",
|
|
||||||
}:
|
|
||||||
cv2.imwrite(str(output_dir / "raw_pose.jpg"), raw_panel)
|
|
||||||
cv2.imwrite(str(output_dir / "spatial_fusion.jpg"), temporal_panel)
|
|
||||||
cv2.imwrite(str(output_dir / "occlusion_v2.jpg"), v2_panel)
|
|
||||||
panel_width = 960
|
|
||||||
panel_height = round(height * panel_width / width)
|
|
||||||
image_panels = [
|
|
||||||
cv2.resize(
|
|
||||||
panel,
|
|
||||||
(panel_width, panel_height),
|
|
||||||
interpolation=cv2.INTER_AREA,
|
|
||||||
)
|
|
||||||
for panel in (raw_panel, temporal_panel, v2_panel)
|
|
||||||
]
|
|
||||||
cv2.imwrite(
|
|
||||||
str(output_dir / "raw_spatial_occlusion_v2.jpg"),
|
|
||||||
np.hstack(image_panels),
|
|
||||||
)
|
|
||||||
if frame_idx % 100 == 0:
|
|
||||||
print(f"{video_label}: processed frame {frame_idx}", flush=True)
|
|
||||||
frame_idx += 1
|
|
||||||
|
|
||||||
cap.release()
|
|
||||||
writer.release()
|
|
||||||
with (output_dir / "predictions.jsonl").open("w", encoding="utf-8") as handle:
|
|
||||||
for record in records:
|
|
||||||
handle.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
||||||
|
|
||||||
summary = {
|
|
||||||
"occlusion_algorithm": "v4_global_assignment",
|
|
||||||
"video": str(video_path.resolve()),
|
|
||||||
"frames": len(records),
|
|
||||||
"fps": round(fps, 3),
|
|
||||||
"resolution": [width, height],
|
|
||||||
"person_filter": {
|
|
||||||
"center_x_max_ratio": 0.80,
|
|
||||||
"minimum_box_height_ratio": 0.20,
|
|
||||||
"max_people": max_people,
|
|
||||||
},
|
|
||||||
"detector_settings": {
|
|
||||||
"imgsz": det_imgsz,
|
|
||||||
"confidence": det_conf,
|
|
||||||
"roi_y_max_ratio": det_roi_y_max_ratio,
|
|
||||||
},
|
|
||||||
"filtered_person_count_frames": dict(sorted(detection_counts.items())),
|
|
||||||
"rejected_detection_counts": dict(sorted(rejection_counts.items())),
|
|
||||||
"track_ids_created": box_tracker.next_id - 1,
|
|
||||||
"isolated_pose_inferences": isolated_inferences,
|
|
||||||
"short_occlusion_frames": short_occlusion_frames,
|
|
||||||
"medium_occlusion_frames": medium_occlusion_frames,
|
|
||||||
"v2_state_counts": dict(sorted(state_counts.items())),
|
|
||||||
"timing_ms_cpu": {
|
|
||||||
name: summarize(values[5:] if len(values) > 5 else values)
|
|
||||||
for name, values in timings.items()
|
|
||||||
},
|
|
||||||
"output_note": (
|
|
||||||
"Main video hides reachable-region arcs. Reachable-region geometry remains in "
|
|
||||||
"predictions.jsonl for later door-distance lower-bound calculation."
|
|
||||||
),
|
|
||||||
}
|
|
||||||
with (output_dir / "summary.json").open("w", encoding="utf-8") as handle:
|
|
||||||
json.dump(summary, handle, ensure_ascii=False, indent=2)
|
|
||||||
return summary
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
from rtmlib import RTMPose
|
|
||||||
from ultralytics import YOLO
|
|
||||||
args = parse_args()
|
|
||||||
detector = YOLO(str(args.det_model))
|
|
||||||
pose = RTMPose(
|
|
||||||
str(args.pose_model),
|
|
||||||
model_input_size=(288, 384),
|
|
||||||
backend="onnxruntime",
|
|
||||||
device=args.pose_device,
|
|
||||||
)
|
|
||||||
all_summaries = {}
|
|
||||||
for video_idx, video_path in enumerate(args.videos, start=1):
|
|
||||||
label = f"video{video_idx}"
|
|
||||||
output_dir = args.output / label
|
|
||||||
all_summaries[label] = process_video(
|
|
||||||
video_path,
|
|
||||||
output_dir,
|
|
||||||
detector,
|
|
||||||
pose,
|
|
||||||
args.det_device,
|
|
||||||
args.det_imgsz,
|
|
||||||
args.det_conf,
|
|
||||||
args.det_roi_y_max_ratio,
|
|
||||||
args.short_occlusion_frames,
|
|
||||||
args.medium_occlusion_frames,
|
|
||||||
args.max_people,
|
|
||||||
label,
|
|
||||||
)
|
|
||||||
args.output.mkdir(parents=True, exist_ok=True)
|
|
||||||
with (args.output / "summary.json").open("w", encoding="utf-8") as handle:
|
|
||||||
json.dump(all_summaries, handle, ensure_ascii=False, indent=2)
|
|
||||||
print(json.dumps(all_summaries, ensure_ascii=False, indent=2))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
"""Test whether other people inside a top-down crop cause RTMW-L mix-ups."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import cv2
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from compare_rtmw_ml import (
|
|
||||||
draw_pose,
|
|
||||||
model_disagreement,
|
|
||||||
summarize,
|
|
||||||
temporal_metrics,
|
|
||||||
write_montage,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("--video", type=Path, required=True)
|
|
||||||
parser.add_argument("--baseline-jsonl", type=Path, required=True)
|
|
||||||
parser.add_argument("--model-l", type=Path, required=True)
|
|
||||||
parser.add_argument("--output", type=Path, required=True)
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def mask_other_people(
|
|
||||||
frame: np.ndarray,
|
|
||||||
boxes: list[list[float]],
|
|
||||||
target_idx: int,
|
|
||||||
) -> np.ndarray:
|
|
||||||
masked = frame.copy()
|
|
||||||
height, width = frame.shape[:2]
|
|
||||||
mask = np.zeros((height, width), dtype=bool)
|
|
||||||
target = boxes[target_idx]
|
|
||||||
target_area = max((target[2] - target[0]) * (target[3] - target[1]), 1.0)
|
|
||||||
for idx, bbox in enumerate(boxes):
|
|
||||||
if idx == target_idx:
|
|
||||||
continue
|
|
||||||
other_area = max((bbox[2] - bbox[0]) * (bbox[3] - bbox[1]), 1.0)
|
|
||||||
# The failure in this video is the small background person being
|
|
||||||
# absorbed by the much larger foreground person's 3:4 pose crop.
|
|
||||||
# Mask only markedly smaller people. Masking a larger occluder while
|
|
||||||
# estimating a small person removes genuine target pixels at overlap.
|
|
||||||
if other_area >= 0.5 * target_area:
|
|
||||||
continue
|
|
||||||
x1, y1, x2, y2 = (int(round(value)) for value in bbox)
|
|
||||||
x1, x2 = max(0, x1), min(width - 1, x2)
|
|
||||||
y1, y2 = max(0, y1), min(height - 1, y2)
|
|
||||||
mask[y1:y2 + 1, x1:x2 + 1] = True
|
|
||||||
|
|
||||||
masked[mask] = (124, 116, 104)
|
|
||||||
return masked
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
from rtmlib import RTMPose
|
|
||||||
args = parse_args()
|
|
||||||
args.output.mkdir(parents=True, exist_ok=True)
|
|
||||||
baseline = [
|
|
||||||
json.loads(line)
|
|
||||||
for line in args.baseline_jsonl.read_text(encoding="utf-8").splitlines()
|
|
||||||
]
|
|
||||||
pose_l = RTMPose(
|
|
||||||
str(args.model_l), model_input_size=(288, 384), backend="onnxruntime", device="cpu"
|
|
||||||
)
|
|
||||||
|
|
||||||
cap = cv2.VideoCapture(str(args.video))
|
|
||||||
fps = float(cap.get(cv2.CAP_PROP_FPS))
|
|
||||||
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
||||||
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
||||||
writer = cv2.VideoWriter(
|
|
||||||
str(args.output / "rtmw_l_raw_vs_person_masked.mp4"),
|
|
||||||
cv2.VideoWriter_fourcc(*"mp4v"),
|
|
||||||
fps,
|
|
||||||
(width, height // 2),
|
|
||||||
)
|
|
||||||
|
|
||||||
comparison: list[dict] = []
|
|
||||||
timings: list[float] = []
|
|
||||||
frame_idx = 0
|
|
||||||
while True:
|
|
||||||
ok, frame = cap.read()
|
|
||||||
if not ok:
|
|
||||||
break
|
|
||||||
baseline_record = baseline[frame_idx]
|
|
||||||
boxes = baseline_record["boxes"][:2]
|
|
||||||
masked_keypoints = []
|
|
||||||
masked_scores = []
|
|
||||||
started = time.perf_counter()
|
|
||||||
for person_idx, bbox in enumerate(boxes):
|
|
||||||
masked_frame = mask_other_people(frame, boxes, person_idx)
|
|
||||||
keypoints, scores = pose_l(masked_frame, bboxes=[bbox])
|
|
||||||
masked_keypoints.append(keypoints[0])
|
|
||||||
masked_scores.append(scores[0])
|
|
||||||
timings.append((time.perf_counter() - started) * 1000.0)
|
|
||||||
masked_keypoints_array = np.asarray(masked_keypoints)
|
|
||||||
|
|
||||||
raw_keypoints = np.asarray(baseline_record["L"]["keypoints"][:2])
|
|
||||||
comparison.append(
|
|
||||||
{
|
|
||||||
"frame_index": frame_idx,
|
|
||||||
"boxes": boxes,
|
|
||||||
"M": {"keypoints": raw_keypoints.tolist()},
|
|
||||||
"L": {"keypoints": np.round(masked_keypoints_array, 3).tolist()},
|
|
||||||
"masked_raw_scores": np.round(np.asarray(masked_scores), 4).tolist(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
left = draw_pose(frame, boxes, raw_keypoints, f"RTMW-L raw | frame {frame_idx}")
|
|
||||||
right = draw_pose(
|
|
||||||
frame,
|
|
||||||
boxes,
|
|
||||||
masked_keypoints_array,
|
|
||||||
f"RTMW-L mask other person | frame {frame_idx}",
|
|
||||||
)
|
|
||||||
left = cv2.resize(left, (width // 2, height // 2), interpolation=cv2.INTER_AREA)
|
|
||||||
right = cv2.resize(right, (width // 2, height // 2), interpolation=cv2.INTER_AREA)
|
|
||||||
writer.write(np.hstack([left, right]))
|
|
||||||
if frame_idx % 50 == 0:
|
|
||||||
print(f"processed frame {frame_idx}", flush=True)
|
|
||||||
frame_idx += 1
|
|
||||||
|
|
||||||
cap.release()
|
|
||||||
writer.release()
|
|
||||||
|
|
||||||
with (args.output / "rtmw_l_raw_vs_person_masked.jsonl").open(
|
|
||||||
"w", encoding="utf-8"
|
|
||||||
) as handle:
|
|
||||||
for record in comparison:
|
|
||||||
handle.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
||||||
|
|
||||||
disagreement, worst_frames = model_disagreement(comparison)
|
|
||||||
fixed_frames = [0, 60, 120, 132, 136, 180, 240, 252, 300, 360, 411]
|
|
||||||
montage_frames = sorted(set(fixed_frames + worst_frames[:8]))
|
|
||||||
write_montage(
|
|
||||||
args.video,
|
|
||||||
comparison,
|
|
||||||
montage_frames,
|
|
||||||
args.output / "rtmw_l_raw_vs_person_masked_montage.jpg",
|
|
||||||
titles=("RTMW-L raw", "RTMW-L smaller-person masked"),
|
|
||||||
)
|
|
||||||
|
|
||||||
summary = {
|
|
||||||
"frames": len(comparison),
|
|
||||||
"labels": {
|
|
||||||
"M": "RTMW-L raw crop",
|
|
||||||
"L": "RTMW-L with markedly smaller people masked",
|
|
||||||
},
|
|
||||||
"timing_ms_two_people_cpu": summarize(timings[5:]),
|
|
||||||
"temporal_metrics_normalized_to_yolo_box": temporal_metrics(comparison),
|
|
||||||
"position_change_after_masking": disagreement,
|
|
||||||
"largest_change_frames": worst_frames,
|
|
||||||
"montage_frames": montage_frames,
|
|
||||||
}
|
|
||||||
with (args.output / "summary.json").open("w", encoding="utf-8") as handle:
|
|
||||||
json.dump(summary, handle, ensure_ascii=False, indent=2)
|
|
||||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,390 +0,0 @@
|
|||||||
"""Lightweight causal door-risk inference using only YOLO11s and RTMW-L."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import cv2
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
|
|
||||||
PACKAGE_DIR = Path(__file__).resolve().parents[1]
|
|
||||||
PIPELINE_DIR = Path(__file__).resolve().parent
|
|
||||||
for import_dir in (PIPELINE_DIR, PACKAGE_DIR):
|
|
||||||
if str(import_dir) not in sys.path:
|
|
||||||
sys.path.insert(0, str(import_dir))
|
|
||||||
|
|
||||||
from door_mask_risk import DoorMaskSequence, DoorRiskEvaluator # noqa: E402
|
|
||||||
from door_zone_risk import ( # noqa: E402
|
|
||||||
DoorZoneAssigner,
|
|
||||||
DoorZoneConfig,
|
|
||||||
build_door_risks,
|
|
||||||
)
|
|
||||||
from live_pose_engine import LivePoseEngine # noqa: E402
|
|
||||||
|
|
||||||
|
|
||||||
def parse_image_size(value: str) -> int | tuple[int, int]:
|
|
||||||
parts = value.lower().replace(",", "x").split("x")
|
|
||||||
if len(parts) == 1:
|
|
||||||
return int(parts[0])
|
|
||||||
if len(parts) == 2:
|
|
||||||
return int(parts[0]), int(parts[1])
|
|
||||||
raise argparse.ArgumentTypeError(
|
|
||||||
"image size must be one integer or HEIGHTxWIDTH, for example 960 or 544x960"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
|
||||||
parser.add_argument("--video", type=Path, required=True)
|
|
||||||
door_source = parser.add_mutually_exclusive_group(required=True)
|
|
||||||
door_source.add_argument("--door-mask-dir", type=Path)
|
|
||||||
door_source.add_argument(
|
|
||||||
"--constant-door-mask",
|
|
||||||
type=Path,
|
|
||||||
help="One X-AnyLabeling JSON or raster mask reused for every video frame.",
|
|
||||||
)
|
|
||||||
parser.add_argument("--output", type=Path, required=True)
|
|
||||||
parser.add_argument(
|
|
||||||
"--det-model", type=Path, default=PACKAGE_DIR / "models" / "yolo11s.onnx"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--pose-model", type=Path, default=PACKAGE_DIR / "models" / "rtmw-l-384.onnx"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--pose-jsonl",
|
|
||||||
type=Path,
|
|
||||||
help="Diagnostic replay input; bypasses live YOLO/RTMW model execution.",
|
|
||||||
)
|
|
||||||
parser.add_argument("--det-device", default="0")
|
|
||||||
parser.add_argument("--pose-device", default="cuda")
|
|
||||||
parser.add_argument(
|
|
||||||
"--pose-provider",
|
|
||||||
choices=("cuda", "tensorrt"),
|
|
||||||
default="cuda",
|
|
||||||
help="ONNX Runtime provider for RTMW.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--pose-trt-cache",
|
|
||||||
type=Path,
|
|
||||||
default=PACKAGE_DIR / "models" / "rtmw_l_trt_cache",
|
|
||||||
)
|
|
||||||
parser.add_argument("--det-imgsz", type=parse_image_size, default=960)
|
|
||||||
parser.add_argument("--det-conf", type=float, default=0.30)
|
|
||||||
parser.add_argument("--det-roi-y-max-ratio", type=float, default=1.0)
|
|
||||||
parser.add_argument("--max-people", type=int, default=3)
|
|
||||||
parser.add_argument(
|
|
||||||
"--door-mask-format",
|
|
||||||
choices=("auto", "xanylabeling", "raster"),
|
|
||||||
default="auto",
|
|
||||||
)
|
|
||||||
parser.add_argument("--door-label", action="append")
|
|
||||||
parser.add_argument("--door-frame-offset", type=int, default=0)
|
|
||||||
parser.add_argument("--require-door-mask", action="store_true")
|
|
||||||
parser.add_argument("--risk-warning-relative", type=float, default=0.30)
|
|
||||||
parser.add_argument("--risk-release-relative", type=float, default=0.45)
|
|
||||||
parser.add_argument(
|
|
||||||
"--active-door",
|
|
||||||
choices=("front", "rear", "both", "none"),
|
|
||||||
default="both",
|
|
||||||
help="Door(s) whose CAN state is 0. Use front/rear for deployment simulation.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--door-zone-config",
|
|
||||||
type=Path,
|
|
||||||
default=PACKAGE_DIR / "annotations" / "door_zone_multifeature.json",
|
|
||||||
help="Pose-primary fixed-camera front/rear calibration JSON.",
|
|
||||||
)
|
|
||||||
parser.add_argument("--max-frames", type=int, default=0)
|
|
||||||
parser.add_argument("--write-video", action="store_true")
|
|
||||||
parser.add_argument("--progress-every", type=int, default=100)
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def load_pose_records(path: Path) -> dict[int, dict]:
|
|
||||||
records = {}
|
|
||||||
with path.open("r", encoding="utf-8") as handle:
|
|
||||||
for line in handle:
|
|
||||||
if not line.strip():
|
|
||||||
continue
|
|
||||||
record = json.loads(line)
|
|
||||||
records[int(record["frame_index"])] = record
|
|
||||||
return records
|
|
||||||
|
|
||||||
|
|
||||||
def percentile(values: list[float], p: float) -> float | None:
|
|
||||||
return round(float(np.percentile(values, p)), 3) if values else None
|
|
||||||
|
|
||||||
|
|
||||||
def timing_stats(values: list[float]) -> dict:
|
|
||||||
return {
|
|
||||||
"count": len(values),
|
|
||||||
"total": round(float(np.sum(values)), 3) if values else 0.0,
|
|
||||||
"mean": round(float(np.mean(values)), 3) if values else None,
|
|
||||||
"median": percentile(values, 50),
|
|
||||||
"p95": percentile(values, 95),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def visualization_skeletons(record: dict) -> list[dict]:
|
|
||||||
"""Return the exact fused keypoints and metadata consumed by draw_fused."""
|
|
||||||
|
|
||||||
boxes = record.get("boxes", [])
|
|
||||||
track_ids = [int(value) for value in record.get("track_ids", [])]
|
|
||||||
keypoints = record.get("temporal_selected_keypoints", [])
|
|
||||||
metadata = record.get("temporal_metadata", [])
|
|
||||||
if not (len(boxes) == len(keypoints) == len(metadata)):
|
|
||||||
return []
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"track_id": track_ids[position] if position < len(track_ids) else position + 1,
|
|
||||||
"person_index": position,
|
|
||||||
"layout": "coco_wholebody_133",
|
|
||||||
"keypoints": person_keypoints,
|
|
||||||
"metadata": metadata[position],
|
|
||||||
}
|
|
||||||
for position, person_keypoints in enumerate(keypoints)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def render_frame(
|
|
||||||
frame: np.ndarray,
|
|
||||||
record: dict,
|
|
||||||
frame_risk: dict,
|
|
||||||
door_mask: np.ndarray | None,
|
|
||||||
door_assignments: list[dict],
|
|
||||||
) -> np.ndarray:
|
|
||||||
canvas = frame.copy()
|
|
||||||
boxes = record.get("boxes", [])
|
|
||||||
keypoints = record.get("temporal_selected_keypoints", [])
|
|
||||||
metadata = record.get("temporal_metadata", [])
|
|
||||||
if len(boxes) == len(keypoints) == len(metadata) and boxes:
|
|
||||||
from fuse_rtmw_l_temporal import draw_fused
|
|
||||||
|
|
||||||
canvas = draw_fused(canvas, boxes, keypoints, metadata, "YOLO + RTMW risk")
|
|
||||||
else:
|
|
||||||
for position, box in enumerate(boxes):
|
|
||||||
x1, y1, x2, y2 = [int(round(float(value))) for value in box[:4]]
|
|
||||||
cv2.rectangle(canvas, (x1, y1), (x2, y2), (0, 220, 255), 2)
|
|
||||||
cv2.putText(
|
|
||||||
canvas,
|
|
||||||
f"P{position + 1}",
|
|
||||||
(x1 + 4, max(22, y1 + 24)),
|
|
||||||
cv2.FONT_HERSHEY_SIMPLEX,
|
|
||||||
0.7,
|
|
||||||
(0, 220, 255),
|
|
||||||
2,
|
|
||||||
cv2.LINE_AA,
|
|
||||||
)
|
|
||||||
zone_colors = {
|
|
||||||
"front": (0, 220, 255),
|
|
||||||
"rear": (255, 180, 0),
|
|
||||||
"transition": (0, 140, 255),
|
|
||||||
"unassigned": (150, 150, 150),
|
|
||||||
}
|
|
||||||
for assignment in door_assignments:
|
|
||||||
position = int(assignment["person_index"])
|
|
||||||
if not 0 <= position < len(boxes):
|
|
||||||
continue
|
|
||||||
box = boxes[position]
|
|
||||||
x1, y1 = int(round(float(box[0]))), int(round(float(box[1])))
|
|
||||||
zone = str(assignment["door_zone"])
|
|
||||||
cv2.putText(
|
|
||||||
canvas,
|
|
||||||
f"T{assignment['track_id']} {zone.upper()}",
|
|
||||||
(max(0, x1 + 4), max(24, y1 - 8)),
|
|
||||||
cv2.FONT_HERSHEY_SIMPLEX,
|
|
||||||
0.68,
|
|
||||||
zone_colors.get(zone, (160, 160, 160)),
|
|
||||||
2,
|
|
||||||
cv2.LINE_AA,
|
|
||||||
)
|
|
||||||
if door_mask is not None and np.any(door_mask):
|
|
||||||
overlay = np.zeros_like(canvas)
|
|
||||||
overlay[door_mask > 0] = (255, 180, 0)
|
|
||||||
canvas = cv2.addWeighted(canvas, 1.0, overlay, 0.22, 0)
|
|
||||||
|
|
||||||
score = frame_risk.get("risk_score")
|
|
||||||
score_text = "N/A" if score is None else f"{float(score):.3f}"
|
|
||||||
level = str(frame_risk.get("risk_level", "unavailable"))
|
|
||||||
color = (0, 0, 255) if level == "danger" else (0, 180, 255)
|
|
||||||
if level in ("clear", "unavailable", "inactive", "unknown"):
|
|
||||||
color = (40, 210, 80) if level == "clear" else (160, 160, 160)
|
|
||||||
cv2.rectangle(canvas, (12, 55), (490, 95), (0, 0, 0), -1)
|
|
||||||
cv2.putText(
|
|
||||||
canvas,
|
|
||||||
f"{frame_risk.get('risk_type', 'DOOR_RISK')} score={score_text} level={level}",
|
|
||||||
(24, 83),
|
|
||||||
cv2.FONT_HERSHEY_SIMPLEX,
|
|
||||||
0.68,
|
|
||||||
color,
|
|
||||||
2,
|
|
||||||
cv2.LINE_AA,
|
|
||||||
)
|
|
||||||
return canvas
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
args = parse_args()
|
|
||||||
args.output.mkdir(parents=True, exist_ok=True)
|
|
||||||
if args.pose_provider == "tensorrt" and not args.pose_device.lower().startswith("cuda"):
|
|
||||||
raise ValueError("--pose-provider tensorrt requires --pose-device cuda")
|
|
||||||
|
|
||||||
capture = cv2.VideoCapture(str(args.video.resolve()))
|
|
||||||
if not capture.isOpened():
|
|
||||||
raise RuntimeError(f"Cannot open video: {args.video}")
|
|
||||||
input_fps = float(capture.get(cv2.CAP_PROP_FPS)) or 30.0
|
|
||||||
width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
||||||
height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
||||||
|
|
||||||
risk_evaluator = DoorRiskEvaluator(
|
|
||||||
DoorMaskSequence(
|
|
||||||
args.door_mask_dir,
|
|
||||||
mask_format=args.door_mask_format,
|
|
||||||
labels=tuple(args.door_label or ("door", "车门")),
|
|
||||||
frame_offset=args.door_frame_offset,
|
|
||||||
require_every_frame=args.require_door_mask,
|
|
||||||
constant_mask=args.constant_door_mask,
|
|
||||||
),
|
|
||||||
warning_relative=args.risk_warning_relative,
|
|
||||||
release_relative=args.risk_release_relative,
|
|
||||||
)
|
|
||||||
zone_assigner = DoorZoneAssigner(DoorZoneConfig.load(args.door_zone_config))
|
|
||||||
print(
|
|
||||||
f"Door masks: {risk_evaluator.sequence.source_format} "
|
|
||||||
+ (
|
|
||||||
f"(constant: {risk_evaluator.sequence.constant_path})"
|
|
||||||
if risk_evaluator.sequence.constant_path is not None
|
|
||||||
else f"({len(risk_evaluator.sequence.paths)} indexed frames)"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
pose_records = load_pose_records(args.pose_jsonl) if args.pose_jsonl else None
|
|
||||||
load_started = time.perf_counter()
|
|
||||||
pose_engine = None if pose_records is not None else LivePoseEngine(args)
|
|
||||||
model_load_seconds = time.perf_counter() - load_started
|
|
||||||
|
|
||||||
writer = None
|
|
||||||
if args.write_video:
|
|
||||||
writer = cv2.VideoWriter(
|
|
||||||
str(args.output / "rtmw_risk_overlay.mp4"),
|
|
||||||
cv2.VideoWriter_fourcc(*"mp4v"),
|
|
||||||
input_fps,
|
|
||||||
(width, height),
|
|
||||||
)
|
|
||||||
|
|
||||||
frame_latencies = []
|
|
||||||
pose_latencies = []
|
|
||||||
risk_latencies = []
|
|
||||||
render_latencies = []
|
|
||||||
started_all = time.perf_counter()
|
|
||||||
frame_index = 0
|
|
||||||
output_path = args.output / "causal_results.jsonl"
|
|
||||||
with output_path.open("w", encoding="utf-8") as output_handle:
|
|
||||||
while True:
|
|
||||||
if args.max_frames > 0 and frame_index >= args.max_frames:
|
|
||||||
break
|
|
||||||
ok, frame = capture.read()
|
|
||||||
if not ok:
|
|
||||||
break
|
|
||||||
frame_started = time.perf_counter()
|
|
||||||
pose_started = time.perf_counter()
|
|
||||||
if pose_records is None:
|
|
||||||
record = pose_engine.infer(frame, frame_index)
|
|
||||||
else:
|
|
||||||
record = pose_records.get(frame_index)
|
|
||||||
if record is None:
|
|
||||||
raise RuntimeError(f"Pose JSONL has no record for frame {frame_index}")
|
|
||||||
pose_latencies.append((time.perf_counter() - pose_started) * 1000)
|
|
||||||
|
|
||||||
risk_started = time.perf_counter()
|
|
||||||
assignments = zone_assigner.update(
|
|
||||||
frame_index, record, (height, width)
|
|
||||||
)
|
|
||||||
raw_risk, door_mask = risk_evaluator.evaluate(
|
|
||||||
frame_index, record, (height, width)
|
|
||||||
)
|
|
||||||
door_risks, frame_risk = build_door_risks(
|
|
||||||
raw_risk, assignments, args.active_door
|
|
||||||
)
|
|
||||||
risk_latencies.append((time.perf_counter() - risk_started) * 1000)
|
|
||||||
output_record = {
|
|
||||||
"frame_index": frame_index,
|
|
||||||
"track_ids": record.get("track_ids", []),
|
|
||||||
"boxes": record.get("boxes", []),
|
|
||||||
"skeletons": visualization_skeletons(record),
|
|
||||||
"active_door": args.active_door,
|
|
||||||
"door_assignments": assignments,
|
|
||||||
"door_risks": door_risks,
|
|
||||||
"risk": frame_risk,
|
|
||||||
"ungated_risk": raw_risk,
|
|
||||||
}
|
|
||||||
output_handle.write(json.dumps(output_record, ensure_ascii=False) + "\n")
|
|
||||||
|
|
||||||
if writer is not None:
|
|
||||||
render_started = time.perf_counter()
|
|
||||||
writer.write(
|
|
||||||
render_frame(frame, record, frame_risk, door_mask, assignments)
|
|
||||||
)
|
|
||||||
render_latencies.append((time.perf_counter() - render_started) * 1000)
|
|
||||||
|
|
||||||
frame_latencies.append((time.perf_counter() - frame_started) * 1000)
|
|
||||||
if args.progress_every > 0 and frame_index % args.progress_every == 0:
|
|
||||||
recent = frame_latencies[-min(len(frame_latencies), args.progress_every) :]
|
|
||||||
print(
|
|
||||||
f"frame={frame_index} recent_ms={np.mean(recent):.1f} "
|
|
||||||
f"risk={frame_risk['risk_level']}",
|
|
||||||
flush=True,
|
|
||||||
)
|
|
||||||
frame_index += 1
|
|
||||||
|
|
||||||
capture.release()
|
|
||||||
if writer is not None:
|
|
||||||
writer.release()
|
|
||||||
elapsed = time.perf_counter() - started_all
|
|
||||||
summary = {
|
|
||||||
"mode": "yolo_rtmw_door_risk_multifeature_v9_10",
|
|
||||||
"video": str(args.video.resolve()),
|
|
||||||
"pose_source": "jsonl_diagnostic" if pose_records is not None else "resident_yolo_rtmw",
|
|
||||||
"models_loaded": [] if pose_records is not None else ["YOLO11s", "RTMW-L"],
|
|
||||||
"edgetam_loaded": False,
|
|
||||||
"risk_region_policy": (
|
|
||||||
"RTMW alarm-eligible hand hulls; YOLO body box only when a person "
|
|
||||||
"has no usable RTMW hand region"
|
|
||||||
),
|
|
||||||
"frames": frame_index,
|
|
||||||
"input_fps": round(input_fps, 3),
|
|
||||||
"elapsed_seconds": round(elapsed, 3),
|
|
||||||
"throughput_fps": round(frame_index / elapsed, 3) if elapsed else None,
|
|
||||||
"model_load_seconds": round(model_load_seconds, 3),
|
|
||||||
"latency_ms": {
|
|
||||||
"total": timing_stats(frame_latencies),
|
|
||||||
"pose": timing_stats(pose_latencies),
|
|
||||||
"risk": timing_stats(risk_latencies),
|
|
||||||
"render": timing_stats(render_latencies),
|
|
||||||
},
|
|
||||||
"write_video": args.write_video,
|
|
||||||
"active_door": args.active_door,
|
|
||||||
"door_zone": zone_assigner.summary(),
|
|
||||||
"door_risk": risk_evaluator.summary(),
|
|
||||||
}
|
|
||||||
if pose_engine is not None:
|
|
||||||
summary["pose"] = {
|
|
||||||
"provider": args.pose_provider,
|
|
||||||
"isolated_inferences": pose_engine.isolated_inferences,
|
|
||||||
"timing_ms": {
|
|
||||||
key: timing_stats(values) for key, values in pose_engine.timings.items()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
(args.output / "summary.json").write_text(
|
|
||||||
json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
||||||
)
|
|
||||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
"""Runtime visualization helpers."""
|
||||||
|
from __future__ import annotations
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def percentile(values: list[float], p: float) -> float | None:
|
||||||
|
return round(float(np.percentile(values, p)), 3) if values else None
|
||||||
|
|
||||||
|
|
||||||
|
def timing_stats(values: list[float]) -> dict:
|
||||||
|
return {
|
||||||
|
"count": len(values),
|
||||||
|
"total": round(float(np.sum(values)), 3) if values else 0.0,
|
||||||
|
"mean": round(float(np.mean(values)), 3) if values else None,
|
||||||
|
"median": percentile(values, 50),
|
||||||
|
"p95": percentile(values, 95),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def visualization_skeletons(record: dict) -> list[dict]:
|
||||||
|
"""Return the exact fused keypoints and metadata consumed by draw_fused."""
|
||||||
|
|
||||||
|
boxes = record.get("boxes", [])
|
||||||
|
track_ids = [int(value) for value in record.get("track_ids", [])]
|
||||||
|
keypoints = record.get("temporal_selected_keypoints", [])
|
||||||
|
metadata = record.get("temporal_metadata", [])
|
||||||
|
if not (len(boxes) == len(keypoints) == len(metadata)):
|
||||||
|
return []
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"track_id": track_ids[position] if position < len(track_ids) else position + 1,
|
||||||
|
"person_index": position,
|
||||||
|
"layout": "coco_wholebody_133",
|
||||||
|
"keypoints": person_keypoints,
|
||||||
|
"metadata": metadata[position],
|
||||||
|
}
|
||||||
|
for position, person_keypoints in enumerate(keypoints)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def render_frame(
|
||||||
|
frame: np.ndarray,
|
||||||
|
record: dict,
|
||||||
|
frame_risk: dict,
|
||||||
|
door_mask: np.ndarray | None,
|
||||||
|
door_assignments: list[dict],
|
||||||
|
) -> np.ndarray:
|
||||||
|
canvas = frame.copy()
|
||||||
|
boxes = record.get("boxes", [])
|
||||||
|
keypoints = record.get("temporal_selected_keypoints", [])
|
||||||
|
metadata = record.get("temporal_metadata", [])
|
||||||
|
if len(boxes) == len(keypoints) == len(metadata) and boxes:
|
||||||
|
from pose_fusion import draw_fused
|
||||||
|
|
||||||
|
canvas = draw_fused(canvas, boxes, keypoints, metadata, "YOLO + RTMW risk")
|
||||||
|
else:
|
||||||
|
for position, box in enumerate(boxes):
|
||||||
|
x1, y1, x2, y2 = [int(round(float(value))) for value in box[:4]]
|
||||||
|
cv2.rectangle(canvas, (x1, y1), (x2, y2), (0, 220, 255), 2)
|
||||||
|
cv2.putText(
|
||||||
|
canvas,
|
||||||
|
f"P{position + 1}",
|
||||||
|
(x1 + 4, max(22, y1 + 24)),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX,
|
||||||
|
0.7,
|
||||||
|
(0, 220, 255),
|
||||||
|
2,
|
||||||
|
cv2.LINE_AA,
|
||||||
|
)
|
||||||
|
zone_colors = {
|
||||||
|
"front": (0, 220, 255),
|
||||||
|
"rear": (255, 180, 0),
|
||||||
|
"transition": (0, 140, 255),
|
||||||
|
"unassigned": (150, 150, 150),
|
||||||
|
}
|
||||||
|
for assignment in door_assignments:
|
||||||
|
position = int(assignment["person_index"])
|
||||||
|
if not 0 <= position < len(boxes):
|
||||||
|
continue
|
||||||
|
box = boxes[position]
|
||||||
|
x1, y1 = int(round(float(box[0]))), int(round(float(box[1])))
|
||||||
|
zone = str(assignment["door_zone"])
|
||||||
|
cv2.putText(
|
||||||
|
canvas,
|
||||||
|
f"T{assignment['track_id']} {zone.upper()}",
|
||||||
|
(max(0, x1 + 4), max(24, y1 - 8)),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX,
|
||||||
|
0.68,
|
||||||
|
zone_colors.get(zone, (160, 160, 160)),
|
||||||
|
2,
|
||||||
|
cv2.LINE_AA,
|
||||||
|
)
|
||||||
|
if door_mask is not None and np.any(door_mask):
|
||||||
|
overlay = np.zeros_like(canvas)
|
||||||
|
overlay[door_mask > 0] = (255, 180, 0)
|
||||||
|
canvas = cv2.addWeighted(canvas, 1.0, overlay, 0.22, 0)
|
||||||
|
|
||||||
|
score = frame_risk.get("risk_score")
|
||||||
|
score_text = "N/A" if score is None else f"{float(score):.3f}"
|
||||||
|
level = str(frame_risk.get("risk_level", "unavailable"))
|
||||||
|
color = (0, 0, 255) if level == "danger" else (0, 180, 255)
|
||||||
|
if level in ("clear", "unavailable", "inactive", "unknown"):
|
||||||
|
color = (40, 210, 80) if level == "clear" else (160, 160, 160)
|
||||||
|
cv2.rectangle(canvas, (12, 55), (490, 95), (0, 0, 0), -1)
|
||||||
|
cv2.putText(
|
||||||
|
canvas,
|
||||||
|
f"{frame_risk.get('risk_type', 'DOOR_RISK')} score={score_text} level={level}",
|
||||||
|
(24, 83),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX,
|
||||||
|
0.68,
|
||||||
|
color,
|
||||||
|
2,
|
||||||
|
cv2.LINE_AA,
|
||||||
|
)
|
||||||
|
return canvas
|
||||||
+4
-3
@@ -16,11 +16,12 @@ import cv2
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
ROOT=Path(__file__).resolve().parent
|
ROOT=Path(__file__).resolve().parent
|
||||||
sys.path.insert(0,str(ROOT/'pipeline'))
|
sys.path.insert(0,str(ROOT/'pipeline'))
|
||||||
from native_backend import NativeBackend, NativePoseEngine
|
from native_backend import NativeBackend
|
||||||
|
from pose_engine import NativePoseEngine
|
||||||
from latest_result import LatestResult, result_to_dict
|
from latest_result import LatestResult, result_to_dict
|
||||||
from door_mask_risk import DoorMaskSequence, DoorRiskEvaluator
|
from door_mask_risk import DoorMaskSequence, DoorRiskEvaluator
|
||||||
from door_zone_risk import DoorZoneConfig, DoorZoneAssigner, build_door_risks
|
from door_zone_risk import DoorZoneConfig, DoorZoneAssigner, build_door_risks
|
||||||
from rtmw_risk_pipeline import render_frame, visualization_skeletons, timing_stats
|
from visualization import render_frame, visualization_skeletons, timing_stats
|
||||||
|
|
||||||
|
|
||||||
def select_active_door(mode, snapshot, side='left', timeout_ms=1000):
|
def select_active_door(mode, snapshot, side='left', timeout_ms=1000):
|
||||||
@@ -121,7 +122,7 @@ class Preview:
|
|||||||
def parse_args(argv=None):
|
def parse_args(argv=None):
|
||||||
p=argparse.ArgumentParser(description=__doc__)
|
p=argparse.ArgumentParser(description=__doc__)
|
||||||
p.add_argument('--video',type=Path,help='Diagnostic video replay through the same TensorRT/risk chain');p.add_argument('--camera',default='/dev/video5');p.add_argument('--width',type=int,default=1920);p.add_argument('--height',type=int,default=1536);p.add_argument('--fps',type=int,default=30)
|
p.add_argument('--video',type=Path,help='Diagnostic video replay through the same TensorRT/risk chain');p.add_argument('--camera',default='/dev/video5');p.add_argument('--width',type=int,default=1920);p.add_argument('--height',type=int,default=1536);p.add_argument('--fps',type=int,default=30)
|
||||||
p.add_argument('--engines',type=Path,default=ROOT/'models/engines')
|
p.add_argument('--engines',type=Path,default=ROOT/'orin/models/engines' if (ROOT/'orin').is_dir() else ROOT/'models/engines')
|
||||||
p.add_argument('--constant-door-mask',type=Path,default=ROOT/'annotations/constant_door_mask.json')
|
p.add_argument('--constant-door-mask',type=Path,default=ROOT/'annotations/constant_door_mask.json')
|
||||||
p.add_argument('--door-zone-config',type=Path,default=ROOT/'annotations/door_zone_multifeature.json')
|
p.add_argument('--door-zone-config',type=Path,default=ROOT/'annotations/door_zone_multifeature.json')
|
||||||
p.add_argument('--active-door',choices=['can','front','rear','both','none'],default='can')
|
p.add_argument('--active-door',choices=['can','front','rear','both','none'],default='can')
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Use the previously deployed 0920 drivers; do not force module compatibility.
|
# Initialize the camera using the packaged Orin drivers.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
root="$(cd "$(dirname "$0")/.." && pwd)"
|
root="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
driver_dir="${DRIVER_DIR:-$root/../vision-anti-pinch-0920/orin/bin/drivers}"
|
driver_dir="${DRIVER_DIR:-$root/orin/bin/drivers}"
|
||||||
device="${1:-/dev/video5}"
|
device="${1:-/dev/video5}"
|
||||||
kernel="$(uname -r)"
|
kernel="$(uname -r)"
|
||||||
for driver in max9295 max9296 sgx-yuv-gmsl2; do
|
for driver in max9295 max9296 sgx-yuv-gmsl2; do
|
||||||
|
|||||||
@@ -1,384 +0,0 @@
|
|||||||
"""Compare RTMW-M 256x192 and RTMW-L 384x288 on cached YOLO boxes.
|
|
||||||
|
|
||||||
This is an experiment script. It intentionally keeps the candidate weights
|
|
||||||
outside the formal models/ directory and does not change deployment code.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import math
|
|
||||||
import time
|
|
||||||
from collections import defaultdict
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import cv2
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
|
|
||||||
BODY_EDGES = ((5, 7), (7, 9), (6, 8), (8, 10))
|
|
||||||
HAND_EDGES_LOCAL = (
|
|
||||||
(0, 1), (1, 2), (2, 3), (3, 4),
|
|
||||||
(0, 5), (5, 6), (6, 7), (7, 8),
|
|
||||||
(0, 9), (9, 10), (10, 11), (11, 12),
|
|
||||||
(0, 13), (13, 14), (14, 15), (15, 16),
|
|
||||||
(0, 17), (17, 18), (18, 19), (19, 20),
|
|
||||||
)
|
|
||||||
HAND_SLICES = {"left": (91, 112), "right": (112, 133)}
|
|
||||||
BODY_WRISTS = {"left": 9, "right": 10}
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("--video", type=Path, required=True)
|
|
||||||
parser.add_argument("--boxes", type=Path, required=True)
|
|
||||||
parser.add_argument("--model-m", type=Path, required=True)
|
|
||||||
parser.add_argument("--model-l", type=Path, required=True)
|
|
||||||
parser.add_argument("--output", type=Path, required=True)
|
|
||||||
parser.add_argument("--max-people", type=int, default=2)
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def load_boxes(path: Path, max_people: int) -> dict[int, list[list[float]]]:
|
|
||||||
frames: dict[int, list[list[float]]] = {}
|
|
||||||
with path.open("r", encoding="utf-8") as handle:
|
|
||||||
for line in handle:
|
|
||||||
item = json.loads(line)
|
|
||||||
boxes = [entry["xyxy"] for entry in item["boxes"]]
|
|
||||||
frames[int(item["frame_index"])] = sorted(boxes, key=lambda b: b[0])[:max_people]
|
|
||||||
return frames
|
|
||||||
|
|
||||||
|
|
||||||
def point(pt: np.ndarray) -> tuple[int, int]:
|
|
||||||
return int(round(float(pt[0]))), int(round(float(pt[1])))
|
|
||||||
|
|
||||||
|
|
||||||
def draw_pose(
|
|
||||||
image: np.ndarray,
|
|
||||||
boxes: list[list[float]],
|
|
||||||
keypoints: np.ndarray,
|
|
||||||
title: str,
|
|
||||||
) -> np.ndarray:
|
|
||||||
canvas = image.copy()
|
|
||||||
for person_idx, (bbox, kpts) in enumerate(zip(boxes, keypoints), start=1):
|
|
||||||
x1, y1, x2, y2 = (int(round(value)) for value in bbox)
|
|
||||||
cv2.rectangle(canvas, (x1, y1), (x2, y2), (0, 220, 255), 2)
|
|
||||||
cv2.putText(
|
|
||||||
canvas,
|
|
||||||
f"P{person_idx}",
|
|
||||||
(x1 + 4, max(22, y1 + 24)),
|
|
||||||
cv2.FONT_HERSHEY_SIMPLEX,
|
|
||||||
0.75,
|
|
||||||
(0, 220, 255),
|
|
||||||
2,
|
|
||||||
cv2.LINE_AA,
|
|
||||||
)
|
|
||||||
|
|
||||||
for edge_idx, (a, b) in enumerate(BODY_EDGES):
|
|
||||||
color = (40, 40, 255) if edge_idx < 2 else (30, 230, 30)
|
|
||||||
cv2.line(canvas, point(kpts[a]), point(kpts[b]), color, 3, cv2.LINE_AA)
|
|
||||||
for idx in (5, 6, 7, 8, 9, 10):
|
|
||||||
color = (40, 40, 255) if idx in (5, 7, 9) else (30, 230, 30)
|
|
||||||
cv2.circle(canvas, point(kpts[idx]), 5, color, -1, cv2.LINE_AA)
|
|
||||||
|
|
||||||
for side, (start, end) in HAND_SLICES.items():
|
|
||||||
hand = kpts[start:end]
|
|
||||||
color = (255, 80, 220) if side == "left" else (255, 190, 40)
|
|
||||||
for a, b in HAND_EDGES_LOCAL:
|
|
||||||
cv2.line(canvas, point(hand[a]), point(hand[b]), color, 2, cv2.LINE_AA)
|
|
||||||
for hand_point in hand:
|
|
||||||
cv2.circle(canvas, point(hand_point), 2, color, -1, cv2.LINE_AA)
|
|
||||||
|
|
||||||
cv2.rectangle(canvas, (0, 0), (610, 45), (0, 0, 0), -1)
|
|
||||||
cv2.putText(
|
|
||||||
canvas,
|
|
||||||
title,
|
|
||||||
(10, 31),
|
|
||||||
cv2.FONT_HERSHEY_SIMPLEX,
|
|
||||||
0.8,
|
|
||||||
(255, 255, 255),
|
|
||||||
2,
|
|
||||||
cv2.LINE_AA,
|
|
||||||
)
|
|
||||||
return canvas
|
|
||||||
|
|
||||||
|
|
||||||
def normalized_keypoints(kpts: np.ndarray, bbox: list[float]) -> np.ndarray:
|
|
||||||
x1, y1, x2, y2 = bbox
|
|
||||||
width = max(x2 - x1, 1.0)
|
|
||||||
height = max(y2 - y1, 1.0)
|
|
||||||
result = kpts.copy().astype(np.float64)
|
|
||||||
result[:, 0] = (result[:, 0] - x1) / width
|
|
||||||
result[:, 1] = (result[:, 1] - y1) / height
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def hand_span(kpts: np.ndarray, bbox: list[float], side: str) -> float:
|
|
||||||
start, end = HAND_SLICES[side]
|
|
||||||
hand = kpts[start:end]
|
|
||||||
span = math.hypot(
|
|
||||||
float(np.ptp(hand[:, 0])),
|
|
||||||
float(np.ptp(hand[:, 1])),
|
|
||||||
)
|
|
||||||
x1, y1, x2, y2 = bbox
|
|
||||||
return span / max(math.hypot(x2 - x1, y2 - y1), 1.0)
|
|
||||||
|
|
||||||
|
|
||||||
def summarize(values: list[float]) -> dict[str, float | int | None]:
|
|
||||||
if not values:
|
|
||||||
return {"count": 0, "median": None, "p95": None, "max": None}
|
|
||||||
array = np.asarray(values, dtype=np.float64)
|
|
||||||
return {
|
|
||||||
"count": int(array.size),
|
|
||||||
"median": round(float(np.median(array)), 5),
|
|
||||||
"p95": round(float(np.percentile(array, 95)), 5),
|
|
||||||
"max": round(float(np.max(array)), 5),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def temporal_metrics(records: list[dict]) -> dict:
|
|
||||||
by_model_person: dict[tuple[str, int], list[dict]] = defaultdict(list)
|
|
||||||
for record in records:
|
|
||||||
for model_name in ("M", "L"):
|
|
||||||
for person_idx in range(len(record[model_name]["keypoints"])):
|
|
||||||
by_model_person[(model_name, person_idx)].append(
|
|
||||||
{
|
|
||||||
"frame": record["frame_index"],
|
|
||||||
"bbox": record["boxes"][person_idx],
|
|
||||||
"kpts": np.asarray(
|
|
||||||
record[model_name]["keypoints"][person_idx], dtype=np.float64
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
result: dict[str, dict] = {"M": {}, "L": {}}
|
|
||||||
for (model_name, person_idx), sequence in by_model_person.items():
|
|
||||||
upper_steps: list[float] = []
|
|
||||||
wrist_steps = {"left": [], "right": []}
|
|
||||||
hand_steps = {"left": [], "right": []}
|
|
||||||
hand_spans = {"left": [], "right": []}
|
|
||||||
bone_lengths = {"left_upper": [], "left_forearm": [],
|
|
||||||
"right_upper": [], "right_forearm": []}
|
|
||||||
|
|
||||||
previous: np.ndarray | None = None
|
|
||||||
for entry in sequence:
|
|
||||||
normalized = normalized_keypoints(entry["kpts"], entry["bbox"])
|
|
||||||
if previous is not None:
|
|
||||||
steps = np.linalg.norm(normalized - previous, axis=1)
|
|
||||||
upper_steps.extend(float(steps[idx]) for idx in (5, 6, 7, 8, 9, 10))
|
|
||||||
for side, idx in BODY_WRISTS.items():
|
|
||||||
wrist_steps[side].append(float(steps[idx]))
|
|
||||||
for side, (start, end) in HAND_SLICES.items():
|
|
||||||
hand_steps[side].extend(float(value) for value in steps[start:end])
|
|
||||||
previous = normalized
|
|
||||||
|
|
||||||
hand_spans["left"].append(hand_span(entry["kpts"], entry["bbox"], "left"))
|
|
||||||
hand_spans["right"].append(hand_span(entry["kpts"], entry["bbox"], "right"))
|
|
||||||
bone_lengths["left_upper"].append(float(np.linalg.norm(normalized[5] - normalized[7])))
|
|
||||||
bone_lengths["left_forearm"].append(float(np.linalg.norm(normalized[7] - normalized[9])))
|
|
||||||
bone_lengths["right_upper"].append(float(np.linalg.norm(normalized[6] - normalized[8])))
|
|
||||||
bone_lengths["right_forearm"].append(float(np.linalg.norm(normalized[8] - normalized[10])))
|
|
||||||
|
|
||||||
span_summary = {}
|
|
||||||
for side, values in hand_spans.items():
|
|
||||||
median = float(np.median(values))
|
|
||||||
gross_limit = max(0.2, median * 2.5)
|
|
||||||
span_summary[side] = {
|
|
||||||
**summarize(values),
|
|
||||||
"gross_limit": round(gross_limit, 5),
|
|
||||||
"gross_outlier_frames": int(sum(value > gross_limit for value in values)),
|
|
||||||
}
|
|
||||||
|
|
||||||
bone_summary = {}
|
|
||||||
for name, values in bone_lengths.items():
|
|
||||||
array = np.asarray(values, dtype=np.float64)
|
|
||||||
mean = float(np.mean(array))
|
|
||||||
bone_summary[name] = {
|
|
||||||
**summarize(values),
|
|
||||||
"coefficient_of_variation": round(float(np.std(array) / max(mean, 1e-9)), 5),
|
|
||||||
}
|
|
||||||
|
|
||||||
result[model_name][f"P{person_idx + 1}"] = {
|
|
||||||
"upper_body_frame_step": summarize(upper_steps),
|
|
||||||
"wrist_frame_step": {side: summarize(values) for side, values in wrist_steps.items()},
|
|
||||||
"hand_landmark_frame_step": {
|
|
||||||
side: summarize(values) for side, values in hand_steps.items()
|
|
||||||
},
|
|
||||||
"hand_span_relative_to_person": span_summary,
|
|
||||||
"bone_length_relative_to_box": bone_summary,
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def model_disagreement(records: list[dict]) -> tuple[dict, list[int]]:
|
|
||||||
values: dict[str, list[float]] = defaultdict(list)
|
|
||||||
per_frame: list[tuple[float, int]] = []
|
|
||||||
for record in records:
|
|
||||||
frame_max = 0.0
|
|
||||||
for person_idx, bbox in enumerate(record["boxes"]):
|
|
||||||
m = normalized_keypoints(
|
|
||||||
np.asarray(record["M"]["keypoints"][person_idx]), bbox
|
|
||||||
)
|
|
||||||
l = normalized_keypoints(
|
|
||||||
np.asarray(record["L"]["keypoints"][person_idx]), bbox
|
|
||||||
)
|
|
||||||
distance = np.linalg.norm(m - l, axis=1)
|
|
||||||
values[f"P{person_idx + 1}_upper"].extend(
|
|
||||||
float(distance[idx]) for idx in (5, 6, 7, 8, 9, 10)
|
|
||||||
)
|
|
||||||
for side, idx in BODY_WRISTS.items():
|
|
||||||
values[f"P{person_idx + 1}_{side}_wrist"].append(float(distance[idx]))
|
|
||||||
frame_max = max(frame_max, float(np.max(distance[[5, 6, 7, 8, 9, 10]])))
|
|
||||||
per_frame.append((frame_max, record["frame_index"]))
|
|
||||||
worst_frames = [frame for _, frame in sorted(per_frame, reverse=True)[:12]]
|
|
||||||
return {name: summarize(items) for name, items in values.items()}, worst_frames
|
|
||||||
|
|
||||||
|
|
||||||
def write_montage(
|
|
||||||
video_path: Path,
|
|
||||||
records: list[dict],
|
|
||||||
selected_frames: list[int],
|
|
||||||
output_path: Path,
|
|
||||||
titles: tuple[str, str] = ("RTMW-M 256x192", "RTMW-L 384x288"),
|
|
||||||
) -> None:
|
|
||||||
selected = set(selected_frames)
|
|
||||||
record_map = {record["frame_index"]: record for record in records}
|
|
||||||
cap = cv2.VideoCapture(str(video_path))
|
|
||||||
rows: list[np.ndarray] = []
|
|
||||||
frame_idx = 0
|
|
||||||
while True:
|
|
||||||
ok, frame = cap.read()
|
|
||||||
if not ok:
|
|
||||||
break
|
|
||||||
if frame_idx in selected:
|
|
||||||
record = record_map[frame_idx]
|
|
||||||
m = draw_pose(
|
|
||||||
frame,
|
|
||||||
record["boxes"],
|
|
||||||
np.asarray(record["M"]["keypoints"]),
|
|
||||||
f"{titles[0]} | frame {frame_idx}",
|
|
||||||
)
|
|
||||||
l = draw_pose(
|
|
||||||
frame,
|
|
||||||
record["boxes"],
|
|
||||||
np.asarray(record["L"]["keypoints"]),
|
|
||||||
f"{titles[1]} | frame {frame_idx}",
|
|
||||||
)
|
|
||||||
m = cv2.resize(m, (640, 360), interpolation=cv2.INTER_AREA)
|
|
||||||
l = cv2.resize(l, (640, 360), interpolation=cv2.INTER_AREA)
|
|
||||||
rows.append(np.hstack([m, l]))
|
|
||||||
frame_idx += 1
|
|
||||||
cap.release()
|
|
||||||
if rows:
|
|
||||||
cv2.imwrite(str(output_path), np.vstack(rows))
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
from rtmlib import RTMPose
|
|
||||||
args = parse_args()
|
|
||||||
args.output.mkdir(parents=True, exist_ok=True)
|
|
||||||
cached_boxes = load_boxes(args.boxes, args.max_people)
|
|
||||||
|
|
||||||
pose_m = RTMPose(
|
|
||||||
str(args.model_m), model_input_size=(192, 256), backend="onnxruntime", device="cpu"
|
|
||||||
)
|
|
||||||
pose_l = RTMPose(
|
|
||||||
str(args.model_l), model_input_size=(288, 384), backend="onnxruntime", device="cpu"
|
|
||||||
)
|
|
||||||
|
|
||||||
cap = cv2.VideoCapture(str(args.video))
|
|
||||||
fps = float(cap.get(cv2.CAP_PROP_FPS))
|
|
||||||
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
||||||
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
||||||
video_output = args.output / "reference_video_rtmw_m_vs_l.mp4"
|
|
||||||
writer = cv2.VideoWriter(
|
|
||||||
str(video_output),
|
|
||||||
cv2.VideoWriter_fourcc(*"mp4v"),
|
|
||||||
fps,
|
|
||||||
(width, height // 2),
|
|
||||||
)
|
|
||||||
|
|
||||||
records: list[dict] = []
|
|
||||||
timings = {"M": [], "L": []}
|
|
||||||
frame_idx = 0
|
|
||||||
while True:
|
|
||||||
ok, frame = cap.read()
|
|
||||||
if not ok:
|
|
||||||
break
|
|
||||||
boxes = cached_boxes[frame_idx]
|
|
||||||
|
|
||||||
started = time.perf_counter()
|
|
||||||
kpts_m, scores_m = pose_m(frame, bboxes=boxes)
|
|
||||||
timings["M"].append((time.perf_counter() - started) * 1000.0)
|
|
||||||
|
|
||||||
started = time.perf_counter()
|
|
||||||
kpts_l, scores_l = pose_l(frame, bboxes=boxes)
|
|
||||||
timings["L"].append((time.perf_counter() - started) * 1000.0)
|
|
||||||
|
|
||||||
records.append(
|
|
||||||
{
|
|
||||||
"frame_index": frame_idx,
|
|
||||||
"boxes": boxes,
|
|
||||||
"M": {
|
|
||||||
"keypoints": np.round(kpts_m, 3).tolist(),
|
|
||||||
"raw_scores": np.round(scores_m, 4).tolist(),
|
|
||||||
},
|
|
||||||
"L": {
|
|
||||||
"keypoints": np.round(kpts_l, 3).tolist(),
|
|
||||||
"raw_scores": np.round(scores_l, 4).tolist(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
left = draw_pose(frame, boxes, kpts_m, f"RTMW-M 256x192 | frame {frame_idx}")
|
|
||||||
right = draw_pose(frame, boxes, kpts_l, f"RTMW-L 384x288 | frame {frame_idx}")
|
|
||||||
left = cv2.resize(left, (width // 2, height // 2), interpolation=cv2.INTER_AREA)
|
|
||||||
right = cv2.resize(right, (width // 2, height // 2), interpolation=cv2.INTER_AREA)
|
|
||||||
writer.write(np.hstack([left, right]))
|
|
||||||
|
|
||||||
if frame_idx % 50 == 0:
|
|
||||||
print(f"processed frame {frame_idx}", flush=True)
|
|
||||||
frame_idx += 1
|
|
||||||
|
|
||||||
cap.release()
|
|
||||||
writer.release()
|
|
||||||
|
|
||||||
jsonl_path = args.output / "reference_video_rtmw_m_vs_l.jsonl"
|
|
||||||
with jsonl_path.open("w", encoding="utf-8") as handle:
|
|
||||||
for record in records:
|
|
||||||
handle.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
||||||
|
|
||||||
disagreement, worst_frames = model_disagreement(records)
|
|
||||||
fixed_frames = [0, 60, 120, 180, 240, 252, 300, 360, 411]
|
|
||||||
montage_frames = sorted(set(fixed_frames + worst_frames))
|
|
||||||
write_montage(
|
|
||||||
args.video,
|
|
||||||
records,
|
|
||||||
montage_frames,
|
|
||||||
args.output / "rtmw_m_vs_l_full_montage.jpg",
|
|
||||||
)
|
|
||||||
|
|
||||||
summary = {
|
|
||||||
"frames": len(records),
|
|
||||||
"input_sizes_wh": {"M": [192, 256], "L": [288, 384]},
|
|
||||||
"raw_score_note": (
|
|
||||||
"M and L raw SimCC peak scores use different scales and must not be compared "
|
|
||||||
"or thresholded with the same numeric value."
|
|
||||||
),
|
|
||||||
"timing_ms_two_people_cpu": {
|
|
||||||
name: summarize(values[5:] if len(values) > 5 else values)
|
|
||||||
for name, values in timings.items()
|
|
||||||
},
|
|
||||||
"temporal_metrics_normalized_to_yolo_box": temporal_metrics(records),
|
|
||||||
"m_l_position_disagreement_normalized_to_yolo_box": disagreement,
|
|
||||||
"largest_upper_body_disagreement_frames": worst_frames,
|
|
||||||
"montage_frames": montage_frames,
|
|
||||||
}
|
|
||||||
with (args.output / "summary.json").open("w", encoding="utf-8") as handle:
|
|
||||||
json.dump(summary, handle, ensure_ascii=False, indent=2)
|
|
||||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
"""Native Jetson TensorRT adapter; temporal/risk policy stays in upstream Python."""
|
"""Native Jetson TensorRT adapter; temporal/risk policy stays in upstream Python."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import ctypes as C
|
import ctypes as C
|
||||||
from collections import defaultdict, deque
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from live_pose_engine import LivePoseEngine
|
|
||||||
from predict_optimized_pose_videos import BoxTracker
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
@@ -67,9 +64,3 @@ class NativeBackend:
|
|||||||
def close(self):
|
def close(self):
|
||||||
if self.camera:self.lib.vap_camera_close(self.camera);self.camera=None
|
if self.camera:self.lib.vap_camera_close(self.camera);self.camera=None
|
||||||
if self.engine:self.lib.vap_engine_close(self.engine);self.engine=None
|
if self.engine:self.lib.vap_engine_close(self.engine);self.engine=None
|
||||||
|
|
||||||
class NativePoseEngine(LivePoseEngine):
|
|
||||||
def __init__(self,args,native):
|
|
||||||
self.args=args;self.native=native;self.pose=native.pose
|
|
||||||
self.box_tracker=BoxTracker(max_age=30);self.people={}
|
|
||||||
self.timings=defaultdict(lambda:deque(maxlen=3000));self.isolated_inferences=0
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,113 +1,28 @@
|
|||||||
"""Resident YOLO11s + RTMW-L pose stage extracted from the v9.9 pipeline."""
|
"""Resident native TensorRT inference with causal pose fusion and tracking."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import time
|
import time
|
||||||
from collections import defaultdict
|
from collections import defaultdict, deque
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from pose_tracking import BoxTracker
|
||||||
|
|
||||||
SIDES = ("left", "right")
|
SIDES = ("left", "right")
|
||||||
|
|
||||||
|
class NativePoseEngine:
|
||||||
class LivePoseEngine:
|
def __init__(self, args, native):
|
||||||
"""Run the v9.9 YOLO/RTMW pose and temporal-fusion path without EdgeTAM."""
|
|
||||||
|
|
||||||
def __init__(self, args: argparse.Namespace):
|
|
||||||
self._trt_dll_directory = None
|
|
||||||
if args.pose_provider == "tensorrt" and os.name == "nt":
|
|
||||||
try:
|
|
||||||
import tensorrt_libs
|
|
||||||
except ImportError as exc:
|
|
||||||
raise RuntimeError(
|
|
||||||
"--pose-provider tensorrt on Windows requires the "
|
|
||||||
"tensorrt-cu12-libs package in the active Python environment."
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
trt_lib_dir = Path(tensorrt_libs.__file__).resolve().parent
|
|
||||||
self._trt_dll_directory = os.add_dll_directory(str(trt_lib_dir))
|
|
||||||
os.environ["PATH"] = (
|
|
||||||
str(trt_lib_dir) + os.pathsep + os.environ.get("PATH", "")
|
|
||||||
)
|
|
||||||
|
|
||||||
if args.pose_device.lower().startswith("cuda"):
|
|
||||||
import onnxruntime as ort
|
|
||||||
|
|
||||||
if "CUDAExecutionProvider" not in ort.get_available_providers():
|
|
||||||
raise RuntimeError(
|
|
||||||
"--pose-device cuda requires onnxruntime-gpu. Uninstall "
|
|
||||||
"onnxruntime and install onnxruntime-gpu==1.22.0."
|
|
||||||
)
|
|
||||||
|
|
||||||
from rtmlib import RTMPose
|
|
||||||
from ultralytics import YOLO
|
|
||||||
from predict_optimized_pose_videos import BoxTracker
|
|
||||||
|
|
||||||
self.args = args
|
self.args = args
|
||||||
self.detector = YOLO(str(args.det_model))
|
self.native = native
|
||||||
self.pose = RTMPose(
|
self.pose = native.pose
|
||||||
str(args.pose_model),
|
|
||||||
model_input_size=(288, 384),
|
|
||||||
backend="onnxruntime",
|
|
||||||
device=args.pose_device,
|
|
||||||
)
|
|
||||||
if args.pose_provider == "tensorrt":
|
|
||||||
import onnxruntime as ort
|
|
||||||
|
|
||||||
available = ort.get_available_providers()
|
|
||||||
if "TensorrtExecutionProvider" not in available:
|
|
||||||
raise RuntimeError(
|
|
||||||
"--pose-provider tensorrt requires ONNX Runtime with "
|
|
||||||
"TensorrtExecutionProvider. "
|
|
||||||
f"Available providers: {available}"
|
|
||||||
)
|
|
||||||
cache_dir = args.pose_trt_cache.expanduser().resolve()
|
|
||||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
device_id = (
|
|
||||||
int(str(args.pose_device).split(":")[-1])
|
|
||||||
if ":" in str(args.pose_device)
|
|
||||||
else 0
|
|
||||||
)
|
|
||||||
trt_options = {
|
|
||||||
"device_id": device_id,
|
|
||||||
"trt_fp16_enable": "True",
|
|
||||||
"trt_engine_cache_enable": "True",
|
|
||||||
"trt_engine_cache_path": str(cache_dir),
|
|
||||||
"trt_timing_cache_enable": "True",
|
|
||||||
"trt_timing_cache_path": str(cache_dir),
|
|
||||||
}
|
|
||||||
self.pose.session = ort.InferenceSession(
|
|
||||||
str(args.pose_model.resolve()),
|
|
||||||
providers=[
|
|
||||||
("TensorrtExecutionProvider", trt_options),
|
|
||||||
("CUDAExecutionProvider", {"device_id": device_id}),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
active_providers = self.pose.session.get_providers()
|
|
||||||
if not active_providers or active_providers[0] != "TensorrtExecutionProvider":
|
|
||||||
raise RuntimeError(
|
|
||||||
"RTMW TensorRT initialization failed instead of using the "
|
|
||||||
"requested provider. Active providers: "
|
|
||||||
f"{active_providers}"
|
|
||||||
)
|
|
||||||
print(f"RTMW ONNX Runtime providers: {active_providers}")
|
|
||||||
|
|
||||||
self.box_tracker = BoxTracker(max_age=30)
|
self.box_tracker = BoxTracker(max_age=30)
|
||||||
self.people = {}
|
self.people = {}
|
||||||
self.timings = defaultdict(list)
|
self.timings = defaultdict(lambda: deque(maxlen=3000))
|
||||||
self.isolated_inferences = 0
|
self.isolated_inferences = 0
|
||||||
|
|
||||||
def infer(self, frame: np.ndarray, frame_idx: int) -> dict:
|
def infer(self, frame: np.ndarray, frame_idx: int) -> dict:
|
||||||
from compare_rtmw_ml import HAND_SLICES
|
from pose_geometry import HAND_SLICES
|
||||||
from fuse_rtmw_l_temporal import adaptive_smooth, build_global_hands, fuse_person
|
from pose_fusion import adaptive_smooth, build_global_hands, fuse_person
|
||||||
from occlusion_aware_pose_v2 import Measurement, resolve_global_hand_assignments
|
from pose_occlusion import Measurement, resolve_global_hand_assignments
|
||||||
from predict_optimized_pose_videos import (
|
from pose_tracking import (
|
||||||
LivePersonState,
|
LivePersonState,
|
||||||
filter_person_boxes,
|
|
||||||
mask_other_people,
|
mask_other_people,
|
||||||
update_live_v2,
|
update_live_v2,
|
||||||
)
|
)
|
||||||
@@ -118,23 +33,8 @@ class LivePoseEngine:
|
|||||||
detection_height = max(
|
detection_height = max(
|
||||||
1, min(height, round(height * self.args.det_roi_y_max_ratio))
|
1, min(height, round(height * self.args.det_roi_y_max_ratio))
|
||||||
)
|
)
|
||||||
if hasattr(self, "native"):
|
boxes, confidences = self.native.detect(frame[:detection_height])
|
||||||
boxes, confidences = self.native.detect(frame[:detection_height])
|
self.timings["detector_ms"].append((time.perf_counter() - started) * 1000)
|
||||||
self.timings["detector_ms"].append((time.perf_counter() - started) * 1000)
|
|
||||||
else:
|
|
||||||
detection = self.detector.predict(
|
|
||||||
frame[:detection_height],
|
|
||||||
imgsz=self.args.det_imgsz,
|
|
||||||
conf=self.args.det_conf,
|
|
||||||
iou=0.30,
|
|
||||||
classes=[0],
|
|
||||||
device=self.args.det_device,
|
|
||||||
verbose=False,
|
|
||||||
)[0]
|
|
||||||
self.timings["detector_ms"].append((time.perf_counter() - started) * 1000)
|
|
||||||
boxes, confidences, _ = filter_person_boxes(
|
|
||||||
detection, width, height, self.args.max_people
|
|
||||||
)
|
|
||||||
track_ids = self.box_tracker.update(boxes, frame_idx, (height, width))
|
track_ids = self.box_tracker.update(boxes, frame_idx, (height, width))
|
||||||
|
|
||||||
# Retire disappeared people in long-running camera sessions.
|
# Retire disappeared people in long-running camera sessions.
|
||||||
@@ -1,38 +1,18 @@
|
|||||||
"""Fuse raw and person-isolated RTMW-L poses with temporal consistency.
|
"""Runtime pose fusion helpers."""
|
||||||
|
|
||||||
The fusion keeps the two inference views as hypotheses. It selects body limbs
|
|
||||||
and hands separately, rejects implausible hand geometry, adds source hysteresis
|
|
||||||
and constant-velocity consistency, then applies a motion-adaptive EMA in
|
|
||||||
person-box coordinates. Occluded/conflicting limbs remain labelled as such so
|
|
||||||
they are not accidentally consumed as precise anti-pinch distance evidence.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import math
|
import math
|
||||||
from collections import Counter, defaultdict
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from pose_geometry import BODY_WRISTS, HAND_EDGES_LOCAL, HAND_SLICES, normalized_keypoints
|
||||||
from compare_rtmw_ml import (
|
|
||||||
BODY_EDGES,
|
|
||||||
BODY_WRISTS,
|
|
||||||
HAND_EDGES_LOCAL,
|
|
||||||
HAND_SLICES,
|
|
||||||
draw_pose,
|
|
||||||
normalized_keypoints,
|
|
||||||
summarize,
|
|
||||||
temporal_metrics,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
BODY_SIDE_INDICES = {"left": (5, 7, 9), "right": (6, 8, 10)}
|
BODY_SIDE_INDICES = {"left": (5, 7, 9), "right": (6, 8, 10)}
|
||||||
|
|
||||||
|
|
||||||
SIDE_COLORS = {"left": (40, 40, 255), "right": (30, 230, 30)}
|
SIDE_COLORS = {"left": (40, 40, 255), "right": (30, 230, 30)}
|
||||||
|
|
||||||
|
|
||||||
HAND_COLORS = {"left": (255, 80, 220), "right": (255, 190, 40)}
|
HAND_COLORS = {"left": (255, 80, 220), "right": (255, 190, 40)}
|
||||||
|
|
||||||
|
|
||||||
@@ -71,15 +51,6 @@ class PersonTrack:
|
|||||||
smoothed: np.ndarray | None = None
|
smoothed: np.ndarray | None = None
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("--video", type=Path, required=True)
|
|
||||||
parser.add_argument("--raw-jsonl", type=Path, required=True)
|
|
||||||
parser.add_argument("--isolated-jsonl", type=Path, required=True)
|
|
||||||
parser.add_argument("--output", type=Path, required=True)
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def bbox_diagonal(bbox: list[float]) -> float:
|
def bbox_diagonal(bbox: list[float]) -> float:
|
||||||
return max(math.hypot(bbox[2] - bbox[0], bbox[3] - bbox[1]), 1.0)
|
return max(math.hypot(bbox[2] - bbox[0], bbox[3] - bbox[1]), 1.0)
|
||||||
|
|
||||||
@@ -557,299 +528,3 @@ def draw_fused(
|
|||||||
cv2.LINE_AA,
|
cv2.LINE_AA,
|
||||||
)
|
)
|
||||||
return canvas
|
return canvas
|
||||||
|
|
||||||
|
|
||||||
def make_montage(
|
|
||||||
video_path: Path,
|
|
||||||
records: list[dict],
|
|
||||||
selected_frames: list[int],
|
|
||||||
output_path: Path,
|
|
||||||
) -> None:
|
|
||||||
selected = set(selected_frames)
|
|
||||||
by_frame = {record["frame_index"]: record for record in records}
|
|
||||||
cap = cv2.VideoCapture(str(video_path))
|
|
||||||
rows = []
|
|
||||||
frame_idx = 0
|
|
||||||
while True:
|
|
||||||
ok, frame = cap.read()
|
|
||||||
if not ok:
|
|
||||||
break
|
|
||||||
if frame_idx in selected:
|
|
||||||
record = by_frame[frame_idx]
|
|
||||||
boxes = record["boxes"]
|
|
||||||
raw = draw_pose(
|
|
||||||
frame,
|
|
||||||
boxes,
|
|
||||||
np.asarray(record["raw_keypoints"]),
|
|
||||||
f"RTMW-L raw | frame {frame_idx}",
|
|
||||||
)
|
|
||||||
spatial = draw_fused(
|
|
||||||
frame,
|
|
||||||
boxes,
|
|
||||||
[np.asarray(points) for points in record["spatial_keypoints"]],
|
|
||||||
record["spatial_metadata"],
|
|
||||||
f"spatial fusion | frame {frame_idx}",
|
|
||||||
)
|
|
||||||
temporal = draw_fused(
|
|
||||||
frame,
|
|
||||||
boxes,
|
|
||||||
[np.asarray(points) for points in record["temporal_keypoints"]],
|
|
||||||
record["temporal_metadata"],
|
|
||||||
f"temporal fusion | frame {frame_idx}",
|
|
||||||
)
|
|
||||||
panels = [
|
|
||||||
cv2.resize(panel, (480, 270), interpolation=cv2.INTER_AREA)
|
|
||||||
for panel in (raw, spatial, temporal)
|
|
||||||
]
|
|
||||||
rows.append(np.hstack(panels))
|
|
||||||
frame_idx += 1
|
|
||||||
cap.release()
|
|
||||||
if rows:
|
|
||||||
cv2.imwrite(str(output_path), np.vstack(rows))
|
|
||||||
|
|
||||||
|
|
||||||
def active_hand_temporal_metrics(records: list[dict], variant: str) -> dict:
|
|
||||||
values: dict[str, list[float]] = defaultdict(list)
|
|
||||||
previous: dict[tuple[int, str], tuple[int, np.ndarray]] = {}
|
|
||||||
for record in records:
|
|
||||||
for person_idx, bbox in enumerate(record["boxes"]):
|
|
||||||
kpts = normalized_keypoints(
|
|
||||||
np.asarray(record[f"{variant}_keypoints"][person_idx]), bbox
|
|
||||||
)
|
|
||||||
meta = record[f"{variant}_metadata"][person_idx]
|
|
||||||
for side, (start, end) in HAND_SLICES.items():
|
|
||||||
key = (person_idx, side)
|
|
||||||
if not meta[side]["distance_eligible"]:
|
|
||||||
previous.pop(key, None)
|
|
||||||
continue
|
|
||||||
prior = previous.get(key)
|
|
||||||
if prior is not None and prior[0] == record["frame_index"] - 1:
|
|
||||||
step = np.linalg.norm(kpts[start:end] - prior[1], axis=1)
|
|
||||||
values[f"P{person_idx + 1}_{side}"].extend(float(v) for v in step)
|
|
||||||
previous[key] = (record["frame_index"], kpts[start:end])
|
|
||||||
return {name: summarize(items) for name, items in values.items()}
|
|
||||||
|
|
||||||
|
|
||||||
def metadata_summary(records: list[dict], variant: str) -> dict:
|
|
||||||
state_counts: Counter = Counter()
|
|
||||||
source_counts: Counter = Counter()
|
|
||||||
eligible_counts: Counter = Counter()
|
|
||||||
alarm_counts: Counter = Counter()
|
|
||||||
switches: Counter = Counter()
|
|
||||||
previous_source: dict[tuple[int, str, str], str] = {}
|
|
||||||
for record in records:
|
|
||||||
for person_idx, person_meta in enumerate(record[f"{variant}_metadata"]):
|
|
||||||
for side in ("left", "right"):
|
|
||||||
meta = person_meta[side]
|
|
||||||
key_prefix = f"P{person_idx + 1}_{side}"
|
|
||||||
state_counts[f"{key_prefix}_{meta['hand_state']}"] += 1
|
|
||||||
source_counts[f"{key_prefix}_{meta['hand_source']}"] += 1
|
|
||||||
if meta["distance_eligible"]:
|
|
||||||
eligible_counts[key_prefix] += 1
|
|
||||||
if meta["alarm_eligible"]:
|
|
||||||
alarm_counts[key_prefix] += 1
|
|
||||||
for kind in ("limb", "hand"):
|
|
||||||
source = meta[f"{kind}_source"]
|
|
||||||
key = (person_idx, side, kind)
|
|
||||||
if key in previous_source and previous_source[key] != source:
|
|
||||||
switches[f"{key_prefix}_{kind}"] += 1
|
|
||||||
previous_source[key] = source
|
|
||||||
return {
|
|
||||||
"hand_state_counts": dict(sorted(state_counts.items())),
|
|
||||||
"source_counts": dict(sorted(source_counts.items())),
|
|
||||||
"distance_eligible_frames": dict(sorted(eligible_counts.items())),
|
|
||||||
"alarm_eligible_frames": dict(sorted(alarm_counts.items())),
|
|
||||||
"source_switches": dict(sorted(switches.items())),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
args = parse_args()
|
|
||||||
args.output.mkdir(parents=True, exist_ok=True)
|
|
||||||
raw_records = [
|
|
||||||
json.loads(line) for line in args.raw_jsonl.read_text(encoding="utf-8").splitlines()
|
|
||||||
]
|
|
||||||
isolated_records = [
|
|
||||||
json.loads(line)
|
|
||||||
for line in args.isolated_jsonl.read_text(encoding="utf-8").splitlines()
|
|
||||||
]
|
|
||||||
|
|
||||||
cap = cv2.VideoCapture(str(args.video))
|
|
||||||
fps = float(cap.get(cv2.CAP_PROP_FPS))
|
|
||||||
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
||||||
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
||||||
writer = cv2.VideoWriter(
|
|
||||||
str(args.output / "rtmw_l_raw_spatial_temporal.mp4"),
|
|
||||||
cv2.VideoWriter_fourcc(*"mp4v"),
|
|
||||||
fps,
|
|
||||||
(1920, 360),
|
|
||||||
)
|
|
||||||
|
|
||||||
tracks = [PersonTrack(), PersonTrack()]
|
|
||||||
output_records: list[dict] = []
|
|
||||||
frame_idx = 0
|
|
||||||
while True:
|
|
||||||
ok, frame = cap.read()
|
|
||||||
if not ok:
|
|
||||||
break
|
|
||||||
raw_record = raw_records[frame_idx]
|
|
||||||
isolated_record = isolated_records[frame_idx]
|
|
||||||
boxes = raw_record["boxes"][:2]
|
|
||||||
raw_keypoints = np.asarray(raw_record["L"]["keypoints"][:2], dtype=np.float64)
|
|
||||||
raw_scores = np.asarray(raw_record["L"]["raw_scores"][:2], dtype=np.float64)
|
|
||||||
isolated_keypoints = np.asarray(
|
|
||||||
isolated_record["L"]["keypoints"][:2], dtype=np.float64
|
|
||||||
)
|
|
||||||
isolated_scores = np.asarray(
|
|
||||||
isolated_record["masked_raw_scores"][:2], dtype=np.float64
|
|
||||||
)
|
|
||||||
|
|
||||||
spatial_keypoints: list[np.ndarray] = []
|
|
||||||
spatial_metadata: list[dict] = []
|
|
||||||
temporal_keypoints: list[np.ndarray] = []
|
|
||||||
temporal_selected_keypoints: list[np.ndarray] = []
|
|
||||||
temporal_metadata: list[dict] = []
|
|
||||||
for person_idx in range(2):
|
|
||||||
spatial, spatial_meta = fuse_person(
|
|
||||||
raw_keypoints[person_idx],
|
|
||||||
raw_scores[person_idx],
|
|
||||||
isolated_keypoints[person_idx],
|
|
||||||
isolated_scores[person_idx],
|
|
||||||
boxes[person_idx],
|
|
||||||
track=None,
|
|
||||||
)
|
|
||||||
temporal_unfiltered, temporal_meta = fuse_person(
|
|
||||||
raw_keypoints[person_idx],
|
|
||||||
raw_scores[person_idx],
|
|
||||||
isolated_keypoints[person_idx],
|
|
||||||
isolated_scores[person_idx],
|
|
||||||
boxes[person_idx],
|
|
||||||
track=tracks[person_idx],
|
|
||||||
)
|
|
||||||
temporal = adaptive_smooth(
|
|
||||||
temporal_unfiltered, boxes[person_idx], tracks[person_idx]
|
|
||||||
)
|
|
||||||
spatial_keypoints.append(spatial)
|
|
||||||
spatial_metadata.append(spatial_meta)
|
|
||||||
temporal_keypoints.append(temporal)
|
|
||||||
temporal_selected_keypoints.append(temporal_unfiltered)
|
|
||||||
temporal_metadata.append(temporal_meta)
|
|
||||||
|
|
||||||
global_hands = build_global_hands(
|
|
||||||
temporal_selected_keypoints, temporal_metadata
|
|
||||||
)
|
|
||||||
|
|
||||||
output_record = {
|
|
||||||
"frame_index": frame_idx,
|
|
||||||
"boxes": boxes,
|
|
||||||
"raw_keypoints": np.round(raw_keypoints, 3).tolist(),
|
|
||||||
"spatial_keypoints": np.round(np.asarray(spatial_keypoints), 3).tolist(),
|
|
||||||
"spatial_metadata": spatial_metadata,
|
|
||||||
"temporal_keypoints": np.round(np.asarray(temporal_keypoints), 3).tolist(),
|
|
||||||
"temporal_selected_keypoints": np.round(
|
|
||||||
np.asarray(temporal_selected_keypoints), 3
|
|
||||||
).tolist(),
|
|
||||||
"temporal_metadata": temporal_metadata,
|
|
||||||
"global_hands": global_hands,
|
|
||||||
}
|
|
||||||
output_records.append(output_record)
|
|
||||||
|
|
||||||
raw_panel = draw_pose(frame, boxes, raw_keypoints, f"RTMW-L raw | frame {frame_idx}")
|
|
||||||
spatial_panel = draw_fused(
|
|
||||||
frame,
|
|
||||||
boxes,
|
|
||||||
spatial_keypoints,
|
|
||||||
spatial_metadata,
|
|
||||||
f"spatial fusion | frame {frame_idx}",
|
|
||||||
)
|
|
||||||
temporal_panel = draw_fused(
|
|
||||||
frame,
|
|
||||||
boxes,
|
|
||||||
temporal_keypoints,
|
|
||||||
temporal_metadata,
|
|
||||||
f"temporal fusion | frame {frame_idx}",
|
|
||||||
)
|
|
||||||
panels = [
|
|
||||||
cv2.resize(panel, (640, 360), interpolation=cv2.INTER_AREA)
|
|
||||||
for panel in (raw_panel, spatial_panel, temporal_panel)
|
|
||||||
]
|
|
||||||
writer.write(np.hstack(panels))
|
|
||||||
frame_idx += 1
|
|
||||||
|
|
||||||
cap.release()
|
|
||||||
writer.release()
|
|
||||||
|
|
||||||
with (args.output / "fusion.jsonl").open("w", encoding="utf-8") as handle:
|
|
||||||
for record in output_records:
|
|
||||||
handle.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
||||||
|
|
||||||
def comparison_records(variant: str) -> list[dict]:
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"frame_index": record["frame_index"],
|
|
||||||
"boxes": record["boxes"],
|
|
||||||
"M": {"keypoints": record["raw_keypoints"]},
|
|
||||||
"L": {"keypoints": record[f"{variant}_keypoints"]},
|
|
||||||
}
|
|
||||||
for record in output_records
|
|
||||||
]
|
|
||||||
|
|
||||||
summary = {
|
|
||||||
"frames": len(output_records),
|
|
||||||
"fusion_policy": {
|
|
||||||
"candidates": ["RTMW-L raw", "RTMW-L smaller-person isolated"],
|
|
||||||
"selection": [
|
|
||||||
"limb and hand selected separately",
|
|
||||||
"geometry validity",
|
|
||||||
"constant-velocity temporal consistency",
|
|
||||||
"source-switch hysteresis",
|
|
||||||
"motion-adaptive EMA in person-box coordinates",
|
|
||||||
],
|
|
||||||
"distance_rule": (
|
|
||||||
"Only consensus or temporally stable raw visible endpoints are eligible. "
|
|
||||||
"Inferred/conflict/rejected hands are excluded from precise distance."
|
|
||||||
),
|
|
||||||
"alarm_rule": (
|
|
||||||
"Alarm attack is asymmetric: a geometrically valid raw endpoint whose score "
|
|
||||||
"is clearly stronger than the isolated view is admitted immediately. Alarm "
|
|
||||||
"release should still require temporal confirmation and hysteresis."
|
|
||||||
),
|
|
||||||
"risk_distance_note": (
|
|
||||||
"Use global_hands and temporal_selected_keypoints (unfiltered) for alarm entry "
|
|
||||||
"to avoid smoothing delay. Use distance_eligible hands and temporal_keypoints "
|
|
||||||
"for stable reporting. When the door edge is available, a conservative "
|
|
||||||
"implementation can use the smaller of the unfiltered and smoothed distances."
|
|
||||||
),
|
|
||||||
},
|
|
||||||
"spatial": metadata_summary(output_records, "spatial"),
|
|
||||||
"temporal": metadata_summary(output_records, "temporal"),
|
|
||||||
"active_hand_frame_step": {
|
|
||||||
"spatial": active_hand_temporal_metrics(output_records, "spatial"),
|
|
||||||
"temporal": active_hand_temporal_metrics(output_records, "temporal"),
|
|
||||||
},
|
|
||||||
"raw_vs_spatial_temporal_metrics": temporal_metrics(comparison_records("spatial")),
|
|
||||||
"raw_vs_temporal_temporal_metrics": temporal_metrics(comparison_records("temporal")),
|
|
||||||
"global_hand_count_frames": dict(
|
|
||||||
sorted(Counter(len(record["global_hands"]) for record in output_records).items())
|
|
||||||
),
|
|
||||||
"cross_person_hand_aliases_total": sum(
|
|
||||||
len(hand["aliases"])
|
|
||||||
for record in output_records
|
|
||||||
for hand in record["global_hands"]
|
|
||||||
),
|
|
||||||
}
|
|
||||||
with (args.output / "summary.json").open("w", encoding="utf-8") as handle:
|
|
||||||
json.dump(summary, handle, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
key_frames = [0, 45, 50, 55, 120, 132, 136, 180, 240, 252, 295, 300, 304, 360, 411]
|
|
||||||
make_montage(
|
|
||||||
args.video,
|
|
||||||
output_records,
|
|
||||||
key_frames,
|
|
||||||
args.output / "rtmw_l_raw_spatial_temporal_montage.jpg",
|
|
||||||
)
|
|
||||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"""Runtime pose geometry helpers."""
|
||||||
|
from __future__ import annotations
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
HAND_EDGES_LOCAL = (
|
||||||
|
(0, 1), (1, 2), (2, 3), (3, 4),
|
||||||
|
(0, 5), (5, 6), (6, 7), (7, 8),
|
||||||
|
(0, 9), (9, 10), (10, 11), (11, 12),
|
||||||
|
(0, 13), (13, 14), (14, 15), (15, 16),
|
||||||
|
(0, 17), (17, 18), (18, 19), (19, 20),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
HAND_SLICES = {"left": (91, 112), "right": (112, 133)}
|
||||||
|
|
||||||
|
|
||||||
|
BODY_WRISTS = {"left": 9, "right": 10}
|
||||||
|
|
||||||
|
|
||||||
|
def normalized_keypoints(kpts: np.ndarray, bbox: list[float]) -> np.ndarray:
|
||||||
|
x1, y1, x2, y2 = bbox
|
||||||
|
width = max(x2 - x1, 1.0)
|
||||||
|
height = max(y2 - y1, 1.0)
|
||||||
|
result = kpts.copy().astype(np.float64)
|
||||||
|
result[:, 0] = (result[:, 0] - x1) / width
|
||||||
|
result[:, 1] = (result[:, 1] - y1) / height
|
||||||
|
return result
|
||||||
@@ -0,0 +1,513 @@
|
|||||||
|
"""Runtime pose occlusion helpers."""
|
||||||
|
from __future__ import annotations
|
||||||
|
import math
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import numpy as np
|
||||||
|
from pose_geometry import HAND_SLICES
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Measurement:
|
||||||
|
keypoints: np.ndarray
|
||||||
|
precise: bool
|
||||||
|
provisional: bool
|
||||||
|
source: str = "model"
|
||||||
|
quality: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EndpointTrack:
|
||||||
|
wrist: np.ndarray | None = None
|
||||||
|
velocity: np.ndarray | None = None
|
||||||
|
hand_shape: np.ndarray | None = None
|
||||||
|
last_observed_frame: int | None = None
|
||||||
|
missed_frames: int = 0
|
||||||
|
last_elbow: np.ndarray | None = None
|
||||||
|
last_direction: np.ndarray | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def bbox_diagonal(bbox: list[float]) -> float:
|
||||||
|
return max(math.hypot(bbox[2] - bbox[0], bbox[3] - bbox[1]), 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def solve_two_bone_elbow(
|
||||||
|
shoulder: np.ndarray,
|
||||||
|
wrist: np.ndarray,
|
||||||
|
upper_length: float,
|
||||||
|
forearm_length: float,
|
||||||
|
reference_elbow: np.ndarray,
|
||||||
|
) -> np.ndarray:
|
||||||
|
direction = wrist - shoulder
|
||||||
|
distance = float(np.linalg.norm(direction))
|
||||||
|
if distance < 1e-6:
|
||||||
|
return reference_elbow.copy()
|
||||||
|
unit = direction / distance
|
||||||
|
|
||||||
|
if distance >= upper_length + forearm_length:
|
||||||
|
return shoulder + unit * upper_length
|
||||||
|
if distance <= abs(upper_length - forearm_length):
|
||||||
|
sign = 1.0 if upper_length >= forearm_length else -1.0
|
||||||
|
return shoulder + unit * upper_length * sign
|
||||||
|
|
||||||
|
along = (
|
||||||
|
upper_length * upper_length
|
||||||
|
- forearm_length * forearm_length
|
||||||
|
+ distance * distance
|
||||||
|
) / (2.0 * distance)
|
||||||
|
height = math.sqrt(max(upper_length * upper_length - along * along, 0.0))
|
||||||
|
base = shoulder + unit * along
|
||||||
|
perpendicular = np.asarray([-unit[1], unit[0]])
|
||||||
|
candidates = (base + perpendicular * height, base - perpendicular * height)
|
||||||
|
return min(candidates, key=lambda candidate: float(np.linalg.norm(candidate - reference_elbow)))
|
||||||
|
|
||||||
|
|
||||||
|
def update_endpoint_track(
|
||||||
|
track: EndpointTrack,
|
||||||
|
measurement: Measurement | None,
|
||||||
|
frame_idx: int,
|
||||||
|
short_occlusion_frames: int,
|
||||||
|
medium_occlusion_frames: int,
|
||||||
|
) -> tuple[np.ndarray | None, np.ndarray | None, str]:
|
||||||
|
if measurement is not None:
|
||||||
|
measured_wrist = measurement.keypoints[0]
|
||||||
|
measured_shape = measurement.keypoints - measured_wrist
|
||||||
|
if track.wrist is None or track.missed_frames > short_occlusion_frames:
|
||||||
|
track.wrist = measured_wrist.copy()
|
||||||
|
track.velocity = np.zeros(2, dtype=np.float64)
|
||||||
|
track.hand_shape = measured_shape.copy()
|
||||||
|
else:
|
||||||
|
prediction = track.wrist + track.velocity
|
||||||
|
residual = measured_wrist - prediction
|
||||||
|
alpha = 0.78 if measurement.precise else 0.88
|
||||||
|
beta = 0.18 if measurement.precise else 0.24
|
||||||
|
track.wrist = prediction + alpha * residual
|
||||||
|
track.velocity = 0.82 * track.velocity + beta * residual
|
||||||
|
shape_alpha = 0.55 if measurement.precise else 0.7
|
||||||
|
track.hand_shape = (
|
||||||
|
shape_alpha * measured_shape + (1.0 - shape_alpha) * track.hand_shape
|
||||||
|
)
|
||||||
|
track.last_observed_frame = frame_idx
|
||||||
|
track.missed_frames = 0
|
||||||
|
if measurement.source == "global_reassignment":
|
||||||
|
state = "reassigned"
|
||||||
|
elif measurement.source == "temporal_recovery":
|
||||||
|
state = "recovered"
|
||||||
|
elif measurement.precise:
|
||||||
|
state = "observed"
|
||||||
|
elif measurement.provisional:
|
||||||
|
state = "provisional"
|
||||||
|
else:
|
||||||
|
state = "interpolated"
|
||||||
|
return track.wrist.copy(), track.wrist + track.hand_shape, state
|
||||||
|
|
||||||
|
track.missed_frames += 1
|
||||||
|
if track.wrist is None:
|
||||||
|
return None, None, "reachable_long"
|
||||||
|
if track.missed_frames <= short_occlusion_frames:
|
||||||
|
track.wrist = track.wrist + track.velocity
|
||||||
|
track.velocity *= 0.86
|
||||||
|
return track.wrist.copy(), track.wrist + track.hand_shape, "predicted_short"
|
||||||
|
if track.missed_frames <= medium_occlusion_frames:
|
||||||
|
return None, None, "reachable_medium"
|
||||||
|
return None, None, "reachable_long"
|
||||||
|
|
||||||
|
|
||||||
|
def temporal_recovery_measurement(
|
||||||
|
keypoints: np.ndarray,
|
||||||
|
metadata: dict,
|
||||||
|
side: str,
|
||||||
|
bbox: list[float],
|
||||||
|
track: EndpointTrack,
|
||||||
|
medium_occlusion_frames: int,
|
||||||
|
) -> Measurement | None:
|
||||||
|
"""Recover a weak current-frame hand only when history strongly supports it."""
|
||||||
|
if track.wrist is None or track.missed_frames > medium_occlusion_frames:
|
||||||
|
return None
|
||||||
|
if metadata.get("hand_state") not in {"conflict", "inferred"}:
|
||||||
|
return None
|
||||||
|
if not metadata.get("selected_hand_valid", False):
|
||||||
|
return None
|
||||||
|
if float(metadata.get("selected_hand_score", 0.0)) < 2.0:
|
||||||
|
return None
|
||||||
|
link = float(metadata.get("hand_selection", {}).get("link_to_selected_limb", 1.0))
|
||||||
|
if link > 0.08:
|
||||||
|
return None
|
||||||
|
|
||||||
|
start, end = HAND_SLICES[side]
|
||||||
|
hand = np.asarray(keypoints[start:end], dtype=np.float64)
|
||||||
|
if hand.shape != (end - start, 2) or not np.isfinite(hand).all():
|
||||||
|
return None
|
||||||
|
prediction = track.wrist + track.velocity
|
||||||
|
diagonal = bbox_diagonal(bbox)
|
||||||
|
gate = min(0.20, 0.05 + 0.008 * max(track.missed_frames, 1)) * diagonal
|
||||||
|
if float(np.linalg.norm(hand[0] - prediction)) > gate:
|
||||||
|
return None
|
||||||
|
return Measurement(
|
||||||
|
keypoints=hand,
|
||||||
|
precise=False,
|
||||||
|
provisional=True,
|
||||||
|
source="temporal_recovery",
|
||||||
|
quality=float(metadata.get("selected_hand_score", 0.0)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def hand_span_px(hand: np.ndarray) -> float:
|
||||||
|
return math.hypot(float(np.ptp(hand[:, 0])), float(np.ptp(hand[:, 1])))
|
||||||
|
|
||||||
|
|
||||||
|
def bbox_area(bbox: list[float]) -> float:
|
||||||
|
return max((bbox[2] - bbox[0]) * (bbox[3] - bbox[1]), 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def reset_endpoint_track(track: EndpointTrack) -> None:
|
||||||
|
track.wrist = None
|
||||||
|
track.velocity = None
|
||||||
|
track.hand_shape = None
|
||||||
|
track.last_observed_frame = None
|
||||||
|
track.missed_frames = 0
|
||||||
|
track.last_elbow = None
|
||||||
|
track.last_direction = None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_global_hand_assignments(
|
||||||
|
frame_measurements: dict[tuple[int, str], Measurement | None],
|
||||||
|
tracks: dict[tuple[int, str], EndpointTrack],
|
||||||
|
boxes: list[list[float]],
|
||||||
|
medium_occlusion_frames: int,
|
||||||
|
) -> tuple[dict[tuple[int, str], Measurement | None], dict[tuple[int, str], dict]]:
|
||||||
|
"""Resolve overlapping-person hands before binding them to body sides.
|
||||||
|
|
||||||
|
Large foreground top-down crops may include a smaller background person.
|
||||||
|
The same physical hand can therefore be emitted for both people, or only by
|
||||||
|
the smaller crop even though it belongs to the foreground person. Direct
|
||||||
|
duplicates are kept on the foreground track when their apparent scales
|
||||||
|
agree. A missing foreground side can then reclaim a nearby candidate using
|
||||||
|
its recent hand trajectory and historical hand scale.
|
||||||
|
"""
|
||||||
|
resolved = dict(frame_measurements)
|
||||||
|
events: dict[tuple[int, str], dict] = {}
|
||||||
|
keys = list(resolved)
|
||||||
|
|
||||||
|
# First collapse direct cross-person duplicates. Keep this deliberately
|
||||||
|
# asymmetric: a substantially larger foreground crop must also carry the
|
||||||
|
# substantially larger historical hand shape.
|
||||||
|
for index, first_key in enumerate(keys):
|
||||||
|
first = resolved[first_key]
|
||||||
|
if first is None:
|
||||||
|
continue
|
||||||
|
for second_key in keys[index + 1 :]:
|
||||||
|
second = resolved[second_key]
|
||||||
|
if second is None or first_key[0] == second_key[0]:
|
||||||
|
continue
|
||||||
|
first_hand = first.keypoints
|
||||||
|
second_hand = second.keypoints
|
||||||
|
first_span = hand_span_px(first_hand)
|
||||||
|
second_span = hand_span_px(second_hand)
|
||||||
|
center_distance = float(
|
||||||
|
np.linalg.norm(np.mean(first_hand, axis=0) - np.mean(second_hand, axis=0))
|
||||||
|
)
|
||||||
|
if center_distance > max(70.0, 0.5 * max(first_span, second_span)):
|
||||||
|
continue
|
||||||
|
|
||||||
|
first_area = bbox_area(boxes[first_key[0]])
|
||||||
|
second_area = bbox_area(boxes[second_key[0]])
|
||||||
|
if first_area >= 2.5 * second_area and (
|
||||||
|
first_span >= 1.45 * second_span
|
||||||
|
or first.quality >= second.quality + 0.6
|
||||||
|
):
|
||||||
|
owner_key, released_key = first_key, second_key
|
||||||
|
elif second_area >= 2.5 * first_area and (
|
||||||
|
second_span >= 1.45 * first_span
|
||||||
|
or second.quality >= first.quality + 0.6
|
||||||
|
):
|
||||||
|
owner_key, released_key = second_key, first_key
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
resolved[released_key] = None
|
||||||
|
reset_endpoint_track(tracks[released_key])
|
||||||
|
events[owner_key] = {
|
||||||
|
"method": "foreground_duplicate_owner",
|
||||||
|
"released_person_id": released_key[0] + 1,
|
||||||
|
"released_side": released_key[1],
|
||||||
|
}
|
||||||
|
events[released_key] = {
|
||||||
|
"method": "reassigned_away",
|
||||||
|
"assigned_person_id": owner_key[0] + 1,
|
||||||
|
"assigned_side": owner_key[1],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Then recover missing foreground hands from another crop. The candidate
|
||||||
|
# must fall inside the target's recent motion gate and be much smaller than
|
||||||
|
# the target's historical foreground hand shape.
|
||||||
|
available_sources = [key for key, value in resolved.items() if value is not None]
|
||||||
|
target_candidates = []
|
||||||
|
for target_key, target_measurement in resolved.items():
|
||||||
|
if target_measurement is not None:
|
||||||
|
continue
|
||||||
|
target_track = tracks[target_key]
|
||||||
|
if (
|
||||||
|
target_track.wrist is None
|
||||||
|
or target_track.hand_shape is None
|
||||||
|
or target_track.missed_frames > medium_occlusion_frames
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
target_area = bbox_area(boxes[target_key[0]])
|
||||||
|
predicted_center = target_track.wrist + np.mean(target_track.hand_shape, axis=0)
|
||||||
|
target_span = hand_span_px(target_track.hand_shape)
|
||||||
|
gate = 0.20 * bbox_diagonal(boxes[target_key[0]])
|
||||||
|
for source_key in available_sources:
|
||||||
|
if source_key[0] == target_key[0]:
|
||||||
|
continue
|
||||||
|
source = resolved[source_key]
|
||||||
|
if source is None or source.source != "model":
|
||||||
|
continue
|
||||||
|
source_area = bbox_area(boxes[source_key[0]])
|
||||||
|
if target_area < 2.5 * source_area:
|
||||||
|
continue
|
||||||
|
source_center = np.mean(source.keypoints, axis=0)
|
||||||
|
source_span = hand_span_px(source.keypoints)
|
||||||
|
distance = float(np.linalg.norm(source_center - predicted_center))
|
||||||
|
if distance > gate or target_span < 1.45 * max(source_span, 1.0):
|
||||||
|
continue
|
||||||
|
target_candidates.append(
|
||||||
|
(distance / gate, target_key, source_key, source_center)
|
||||||
|
)
|
||||||
|
|
||||||
|
used_targets = set()
|
||||||
|
used_sources = set()
|
||||||
|
for _, target_key, source_key, source_center in sorted(target_candidates):
|
||||||
|
if target_key in used_targets or source_key in used_sources:
|
||||||
|
continue
|
||||||
|
target_track = tracks[target_key]
|
||||||
|
shape_center = np.mean(target_track.hand_shape, axis=0)
|
||||||
|
reassigned_wrist = source_center - shape_center
|
||||||
|
reassigned_hand = reassigned_wrist + target_track.hand_shape
|
||||||
|
resolved[target_key] = Measurement(
|
||||||
|
keypoints=reassigned_hand,
|
||||||
|
precise=False,
|
||||||
|
provisional=True,
|
||||||
|
source="global_reassignment",
|
||||||
|
quality=resolved[source_key].quality,
|
||||||
|
)
|
||||||
|
resolved[source_key] = None
|
||||||
|
reset_endpoint_track(tracks[source_key])
|
||||||
|
events[target_key] = {
|
||||||
|
"method": "foreground_temporal_reassignment",
|
||||||
|
"source_person_id": source_key[0] + 1,
|
||||||
|
"source_side": source_key[1],
|
||||||
|
"observed_center": np.round(source_center, 3).tolist(),
|
||||||
|
}
|
||||||
|
events[source_key] = {
|
||||||
|
"method": "reassigned_away",
|
||||||
|
"assigned_person_id": target_key[0] + 1,
|
||||||
|
"assigned_side": target_key[1],
|
||||||
|
}
|
||||||
|
used_targets.add(target_key)
|
||||||
|
used_sources.add(source_key)
|
||||||
|
return resolved, events
|
||||||
|
|
||||||
|
|
||||||
|
def track_direction(track: EndpointTrack) -> np.ndarray:
|
||||||
|
if track.last_direction is not None:
|
||||||
|
return track.last_direction / max(float(np.linalg.norm(track.last_direction)), 1e-6)
|
||||||
|
if track.velocity is not None and float(np.linalg.norm(track.velocity)) > 1e-6:
|
||||||
|
return track.velocity / np.linalg.norm(track.velocity)
|
||||||
|
return np.asarray([1.0, 0.0], dtype=np.float64)
|
||||||
|
|
||||||
|
|
||||||
|
def ellipse_polygon(
|
||||||
|
center: np.ndarray,
|
||||||
|
major_axis: float,
|
||||||
|
minor_axis: float,
|
||||||
|
direction: np.ndarray,
|
||||||
|
point_count: int = 64,
|
||||||
|
) -> np.ndarray:
|
||||||
|
perpendicular = np.asarray([-direction[1], direction[0]], dtype=np.float64)
|
||||||
|
angles = np.linspace(0.0, 2.0 * math.pi, point_count, endpoint=False)
|
||||||
|
return (
|
||||||
|
center
|
||||||
|
+ np.cos(angles)[:, None] * major_axis * direction
|
||||||
|
+ np.sin(angles)[:, None] * minor_axis * perpendicular
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def clip_polygon_from_anchor_to_circle(
|
||||||
|
points: np.ndarray,
|
||||||
|
anchor: np.ndarray,
|
||||||
|
circle_center: np.ndarray,
|
||||||
|
radius: float,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""Clip radial polygon samples to a shoulder-centered anatomical limit."""
|
||||||
|
clipped = []
|
||||||
|
offset = anchor - circle_center
|
||||||
|
for point in points:
|
||||||
|
if float(np.linalg.norm(point - circle_center)) <= radius:
|
||||||
|
clipped.append(point)
|
||||||
|
continue
|
||||||
|
ray = point - anchor
|
||||||
|
a = float(np.dot(ray, ray))
|
||||||
|
b = 2.0 * float(np.dot(offset, ray))
|
||||||
|
c = float(np.dot(offset, offset) - radius * radius)
|
||||||
|
discriminant = max(b * b - 4.0 * a * c, 0.0)
|
||||||
|
roots = [
|
||||||
|
value
|
||||||
|
for value in (
|
||||||
|
(-b - math.sqrt(discriminant)) / max(2.0 * a, 1e-6),
|
||||||
|
(-b + math.sqrt(discriminant)) / max(2.0 * a, 1e-6),
|
||||||
|
)
|
||||||
|
if 0.0 <= value <= 1.0
|
||||||
|
]
|
||||||
|
clipped.append(anchor + (max(roots) if roots else 0.0) * ray)
|
||||||
|
return np.asarray(clipped, dtype=np.float64)
|
||||||
|
|
||||||
|
|
||||||
|
def medium_motion_region(
|
||||||
|
shoulder: np.ndarray,
|
||||||
|
upper_length: float,
|
||||||
|
forearm_length: float,
|
||||||
|
bbox: list[float],
|
||||||
|
track: EndpointTrack,
|
||||||
|
short_occlusion_frames: int,
|
||||||
|
) -> dict:
|
||||||
|
diagonal = bbox_diagonal(bbox)
|
||||||
|
direction = track_direction(track)
|
||||||
|
hand_shape = (
|
||||||
|
track.hand_shape
|
||||||
|
if track.hand_shape is not None
|
||||||
|
else np.zeros((21, 2), dtype=np.float64)
|
||||||
|
)
|
||||||
|
shape_center = np.mean(hand_shape, axis=0)
|
||||||
|
centered_shape = hand_shape - shape_center
|
||||||
|
perpendicular = np.asarray([-direction[1], direction[0]], dtype=np.float64)
|
||||||
|
hand_major = float(np.max(np.abs(centered_shape @ direction)))
|
||||||
|
hand_minor = float(np.max(np.abs(centered_shape @ perpendicular)))
|
||||||
|
|
||||||
|
extra_frames = max(track.missed_frames - short_occlusion_frames, 1)
|
||||||
|
motion_major = min(0.20, 0.04 + 0.007 * extra_frames) * diagonal
|
||||||
|
motion_minor = min(0.13, 0.03 + 0.0045 * extra_frames) * diagonal
|
||||||
|
major_axis = max(hand_major + motion_major, 0.08 * diagonal)
|
||||||
|
minor_axis = max(hand_minor + motion_minor, 0.06 * diagonal)
|
||||||
|
|
||||||
|
center = track.wrist + shape_center
|
||||||
|
tracked_hand_extent = float(np.max(np.linalg.norm(centered_shape, axis=1)))
|
||||||
|
hand_reach = max(tracked_hand_extent, 0.10 * diagonal)
|
||||||
|
anatomical_reach = upper_length + forearm_length + hand_reach + 0.02 * diagonal
|
||||||
|
shoulder_to_center = center - shoulder
|
||||||
|
center_distance = float(np.linalg.norm(shoulder_to_center))
|
||||||
|
if center_distance > 0.96 * anatomical_reach:
|
||||||
|
center = shoulder + shoulder_to_center / center_distance * 0.96 * anatomical_reach
|
||||||
|
|
||||||
|
polygon = ellipse_polygon(center, major_axis, minor_axis, direction)
|
||||||
|
polygon = clip_polygon_from_anchor_to_circle(
|
||||||
|
polygon,
|
||||||
|
center,
|
||||||
|
shoulder,
|
||||||
|
anatomical_reach,
|
||||||
|
)
|
||||||
|
direction_deg = math.degrees(math.atan2(float(direction[1]), float(direction[0])))
|
||||||
|
return {
|
||||||
|
"geometry": "motion_corridor",
|
||||||
|
"center": np.round(center, 3).tolist(),
|
||||||
|
"predicted_wrist": np.round(track.wrist, 3).tolist(),
|
||||||
|
"major_axis_px": round(major_axis, 3),
|
||||||
|
"minor_axis_px": round(minor_axis, 3),
|
||||||
|
"outer_radius_px": round(major_axis, 3),
|
||||||
|
"direction_deg": round(direction_deg, 3),
|
||||||
|
"max_reach_from_shoulder_px": round(anatomical_reach, 3),
|
||||||
|
"polygon": np.round(polygon, 3).tolist(),
|
||||||
|
"requires_door_intersection": True,
|
||||||
|
"render_full_region": True,
|
||||||
|
"distance_lower_bound_rule": "minimum distance from motion-corridor polygon to door_edge",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def long_reachable_region(
|
||||||
|
shoulder: np.ndarray,
|
||||||
|
upper_length: float,
|
||||||
|
forearm_length: float,
|
||||||
|
bbox: list[float],
|
||||||
|
track: EndpointTrack,
|
||||||
|
medium_occlusion_frames: int,
|
||||||
|
) -> dict:
|
||||||
|
diagonal = bbox_diagonal(bbox)
|
||||||
|
height = max(bbox[3] - bbox[1], 1.0)
|
||||||
|
arm_reach = upper_length + forearm_length
|
||||||
|
hand_reach = min(0.13 * diagonal, 0.17 * height)
|
||||||
|
safety_margin = 0.025 * diagonal
|
||||||
|
outer_radius = arm_reach + hand_reach + safety_margin
|
||||||
|
inner_radius = max(abs(upper_length - forearm_length) - 0.04 * diagonal, 0.0)
|
||||||
|
if track.last_direction is None:
|
||||||
|
direction_deg = 0.0
|
||||||
|
half_angle_deg = 180.0
|
||||||
|
else:
|
||||||
|
direction = track_direction(track)
|
||||||
|
direction_deg = math.degrees(math.atan2(float(direction[1]), float(direction[0])))
|
||||||
|
half_angle_deg = min(
|
||||||
|
180.0,
|
||||||
|
65.0 + 2.0 * max(track.missed_frames - medium_occlusion_frames, 0),
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"geometry": "shoulder_sector",
|
||||||
|
"center": np.round(shoulder, 3).tolist(),
|
||||||
|
"inner_radius_px": round(inner_radius, 3),
|
||||||
|
"outer_radius_px": round(outer_radius, 3),
|
||||||
|
"arm_reach_px": round(arm_reach, 3),
|
||||||
|
"hand_reach_px": round(hand_reach, 3),
|
||||||
|
"safety_margin_px": round(safety_margin, 3),
|
||||||
|
"direction_deg": round(direction_deg, 3),
|
||||||
|
"half_angle_deg": round(half_angle_deg, 3),
|
||||||
|
"requires_door_intersection": True,
|
||||||
|
"render_full_region": False,
|
||||||
|
"distance_lower_bound_rule": "intersect shoulder sector with door danger band before alarming",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def reachable_region(
|
||||||
|
shoulder: np.ndarray,
|
||||||
|
upper_length: float,
|
||||||
|
forearm_length: float,
|
||||||
|
bbox: list[float],
|
||||||
|
track: EndpointTrack,
|
||||||
|
state: str = "reachable_long",
|
||||||
|
short_occlusion_frames: int = 12,
|
||||||
|
medium_occlusion_frames: int = 30,
|
||||||
|
) -> dict:
|
||||||
|
if state == "reachable_medium" and track.wrist is not None:
|
||||||
|
return medium_motion_region(
|
||||||
|
shoulder,
|
||||||
|
upper_length,
|
||||||
|
forearm_length,
|
||||||
|
bbox,
|
||||||
|
track,
|
||||||
|
short_occlusion_frames,
|
||||||
|
)
|
||||||
|
return long_reachable_region(
|
||||||
|
shoulder,
|
||||||
|
upper_length,
|
||||||
|
forearm_length,
|
||||||
|
bbox,
|
||||||
|
track,
|
||||||
|
medium_occlusion_frames,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def calibrated_bone_lengths(
|
||||||
|
upper_ratio: float,
|
||||||
|
forearm_ratio: float,
|
||||||
|
bbox: list[float],
|
||||||
|
) -> tuple[float, float]:
|
||||||
|
diagonal = bbox_diagonal(bbox)
|
||||||
|
height = max(bbox[3] - bbox[1], 1.0)
|
||||||
|
upper = upper_ratio * diagonal
|
||||||
|
forearm = forearm_ratio * diagonal
|
||||||
|
total = upper + forearm
|
||||||
|
# Keep only a loose sanity cap. The previous 0.55 * bbox-height cap was
|
||||||
|
# unsafe for close, clipped CMS views: a valid projected arm can occupy
|
||||||
|
# almost the full detection height. The ratios are already robust medians,
|
||||||
|
# so this cap only limits catastrophic calibration outliers.
|
||||||
|
maximum_total = 0.95 * diagonal
|
||||||
|
if total > maximum_total:
|
||||||
|
scale = maximum_total / total
|
||||||
|
upper *= scale
|
||||||
|
forearm *= scale
|
||||||
|
return upper, forearm
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
"""Runtime pose tracking helpers."""
|
||||||
|
from __future__ import annotations
|
||||||
|
import math
|
||||||
|
from collections import deque
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
import numpy as np
|
||||||
|
from pose_geometry import HAND_SLICES
|
||||||
|
from pose_fusion import BODY_SIDE_INDICES, PersonTrack
|
||||||
|
from pose_occlusion import EndpointTrack, Measurement, calibrated_bone_lengths, reachable_region, solve_two_bone_elbow, temporal_recovery_measurement, update_endpoint_track
|
||||||
|
|
||||||
|
|
||||||
|
def bbox_iou(a: np.ndarray, b: np.ndarray) -> float:
|
||||||
|
x1 = max(float(a[0]), float(b[0]))
|
||||||
|
y1 = max(float(a[1]), float(b[1]))
|
||||||
|
x2 = min(float(a[2]), float(b[2]))
|
||||||
|
y2 = min(float(a[3]), float(b[3]))
|
||||||
|
intersection = max(x2 - x1, 0.0) * max(y2 - y1, 0.0)
|
||||||
|
union = (
|
||||||
|
max(float(a[2] - a[0]), 0.0) * max(float(a[3] - a[1]), 0.0)
|
||||||
|
+ max(float(b[2] - b[0]), 0.0) * max(float(b[3] - b[1]), 0.0)
|
||||||
|
- intersection
|
||||||
|
)
|
||||||
|
return intersection / max(union, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
class BoxTracker:
|
||||||
|
def __init__(self, max_age: int = 30):
|
||||||
|
self.max_age = max_age
|
||||||
|
self.next_id = 1
|
||||||
|
self.tracks: dict[int, dict] = {}
|
||||||
|
|
||||||
|
def update(
|
||||||
|
self,
|
||||||
|
boxes: list[list[float]],
|
||||||
|
frame_idx: int,
|
||||||
|
image_shape: tuple[int, int],
|
||||||
|
) -> list[int]:
|
||||||
|
height, width = image_shape
|
||||||
|
image_diagonal = math.hypot(width, height)
|
||||||
|
active_ids = [
|
||||||
|
track_id
|
||||||
|
for track_id, track in self.tracks.items()
|
||||||
|
if frame_idx - track["last_frame"] <= self.max_age
|
||||||
|
]
|
||||||
|
self.tracks = {track_id: self.tracks[track_id] for track_id in active_ids}
|
||||||
|
pairs = []
|
||||||
|
for detection_idx, box in enumerate(boxes):
|
||||||
|
box_array = np.asarray(box, dtype=np.float64)
|
||||||
|
center = (box_array[:2] + box_array[2:]) * 0.5
|
||||||
|
for track_id in active_ids:
|
||||||
|
prior = np.asarray(self.tracks[track_id]["box"], dtype=np.float64)
|
||||||
|
prior_center = (prior[:2] + prior[2:]) * 0.5
|
||||||
|
iou = bbox_iou(box_array, prior)
|
||||||
|
center_distance = float(np.linalg.norm(center - prior_center)) / image_diagonal
|
||||||
|
if iou >= 0.05 or center_distance <= 0.18:
|
||||||
|
score = 2.0 * iou + max(0.0, 1.0 - center_distance / 0.18)
|
||||||
|
pairs.append((score, detection_idx, track_id))
|
||||||
|
|
||||||
|
assigned_detections = set()
|
||||||
|
assigned_tracks = set()
|
||||||
|
result: list[int | None] = [None] * len(boxes)
|
||||||
|
for _, detection_idx, track_id in sorted(pairs, reverse=True):
|
||||||
|
if detection_idx in assigned_detections or track_id in assigned_tracks:
|
||||||
|
continue
|
||||||
|
assigned_detections.add(detection_idx)
|
||||||
|
assigned_tracks.add(track_id)
|
||||||
|
result[detection_idx] = track_id
|
||||||
|
|
||||||
|
for detection_idx, box in enumerate(boxes):
|
||||||
|
track_id = result[detection_idx]
|
||||||
|
if track_id is None:
|
||||||
|
track_id = self.next_id
|
||||||
|
self.next_id += 1
|
||||||
|
result[detection_idx] = track_id
|
||||||
|
self.tracks[track_id] = {"box": box, "last_frame": frame_idx}
|
||||||
|
return [int(track_id) for track_id in result]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LivePersonState:
|
||||||
|
fusion: PersonTrack = field(default_factory=PersonTrack)
|
||||||
|
endpoints: dict[str, EndpointTrack] = field(
|
||||||
|
default_factory=lambda: {"left": EndpointTrack(), "right": EndpointTrack()}
|
||||||
|
)
|
||||||
|
bone_samples: dict[str, deque] = field(
|
||||||
|
default_factory=lambda: {
|
||||||
|
"left": deque(maxlen=120),
|
||||||
|
"right": deque(maxlen=120),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
last_frame: int = -1
|
||||||
|
|
||||||
|
|
||||||
|
def bbox_diagonal(bbox: list[float]) -> float:
|
||||||
|
return max(math.hypot(bbox[2] - bbox[0], bbox[3] - bbox[1]), 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def update_bone_samples(
|
||||||
|
state: LivePersonState,
|
||||||
|
keypoints: np.ndarray,
|
||||||
|
metadata: dict,
|
||||||
|
bbox: list[float],
|
||||||
|
) -> None:
|
||||||
|
diagonal = bbox_diagonal(bbox)
|
||||||
|
for side, (shoulder, elbow, wrist) in BODY_SIDE_INDICES.items():
|
||||||
|
meta = metadata[side]
|
||||||
|
if meta["limb_state"] != "consensus" or not meta["distance_eligible"]:
|
||||||
|
continue
|
||||||
|
upper = float(np.linalg.norm(keypoints[shoulder] - keypoints[elbow])) / diagonal
|
||||||
|
forearm = float(np.linalg.norm(keypoints[elbow] - keypoints[wrist])) / diagonal
|
||||||
|
if 0.04 <= upper <= 0.65 and 0.04 <= forearm <= 0.65:
|
||||||
|
state.bone_samples[side].append((upper, forearm))
|
||||||
|
|
||||||
|
|
||||||
|
def current_bone_ratios(state: LivePersonState, side: str) -> tuple[float, float]:
|
||||||
|
samples = state.bone_samples[side]
|
||||||
|
if len(samples) < 10:
|
||||||
|
opposite = "right" if side == "left" else "left"
|
||||||
|
if len(state.bone_samples[opposite]) >= 10:
|
||||||
|
samples = state.bone_samples[opposite]
|
||||||
|
if not samples:
|
||||||
|
return 0.24, 0.22
|
||||||
|
values = np.asarray(samples, dtype=np.float64)
|
||||||
|
return float(np.median(values[:, 0])), float(np.median(values[:, 1]))
|
||||||
|
|
||||||
|
|
||||||
|
def update_live_v2(
|
||||||
|
frame_idx: int,
|
||||||
|
bbox: list[float],
|
||||||
|
selected_keypoints: np.ndarray,
|
||||||
|
smoothed_keypoints: np.ndarray,
|
||||||
|
fusion_metadata: dict,
|
||||||
|
state: LivePersonState,
|
||||||
|
assigned_measurements: dict[str, Measurement | None] | None,
|
||||||
|
identity_assignments: dict[str, dict] | None,
|
||||||
|
short_occlusion_frames: int,
|
||||||
|
medium_occlusion_frames: int,
|
||||||
|
) -> tuple[np.ndarray, dict]:
|
||||||
|
frame_gap = 0 if state.last_frame < 0 else max(frame_idx - state.last_frame - 1, 0)
|
||||||
|
for endpoint in state.endpoints.values():
|
||||||
|
endpoint.missed_frames += frame_gap
|
||||||
|
state.last_frame = frame_idx
|
||||||
|
update_bone_samples(state, selected_keypoints, fusion_metadata, bbox)
|
||||||
|
|
||||||
|
output = smoothed_keypoints.copy()
|
||||||
|
output_metadata = {}
|
||||||
|
diagonal = bbox_diagonal(bbox)
|
||||||
|
for side, (shoulder_idx, elbow_idx, wrist_idx) in BODY_SIDE_INDICES.items():
|
||||||
|
endpoint = state.endpoints[side]
|
||||||
|
meta = fusion_metadata[side]
|
||||||
|
start, end = HAND_SLICES[side]
|
||||||
|
measurement = (
|
||||||
|
assigned_measurements.get(side)
|
||||||
|
if assigned_measurements is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
if assigned_measurements is None and meta["alarm_eligible"]:
|
||||||
|
measurement = Measurement(
|
||||||
|
keypoints=selected_keypoints[start:end],
|
||||||
|
precise=bool(meta["distance_eligible"]),
|
||||||
|
provisional=not bool(meta["distance_eligible"]),
|
||||||
|
quality=float(meta.get("selected_hand_score", 0.0)),
|
||||||
|
)
|
||||||
|
if measurement is None:
|
||||||
|
measurement = temporal_recovery_measurement(
|
||||||
|
selected_keypoints,
|
||||||
|
meta,
|
||||||
|
side,
|
||||||
|
bbox,
|
||||||
|
endpoint,
|
||||||
|
medium_occlusion_frames,
|
||||||
|
)
|
||||||
|
returning_after_long = (
|
||||||
|
measurement is not None
|
||||||
|
and endpoint.missed_frames > short_occlusion_frames
|
||||||
|
)
|
||||||
|
wrist, hand, endpoint_state = update_endpoint_track(
|
||||||
|
endpoint,
|
||||||
|
measurement,
|
||||||
|
frame_idx,
|
||||||
|
short_occlusion_frames,
|
||||||
|
medium_occlusion_frames,
|
||||||
|
)
|
||||||
|
shoulder = smoothed_keypoints[shoulder_idx]
|
||||||
|
upper_ratio, forearm_ratio = current_bone_ratios(state, side)
|
||||||
|
upper_length, forearm_length = calibrated_bone_lengths(
|
||||||
|
upper_ratio, forearm_ratio, bbox
|
||||||
|
)
|
||||||
|
region = None
|
||||||
|
if wrist is not None and hand is not None:
|
||||||
|
reference_elbow = (
|
||||||
|
selected_keypoints[elbow_idx]
|
||||||
|
if returning_after_long or endpoint.last_elbow is None
|
||||||
|
else endpoint.last_elbow
|
||||||
|
)
|
||||||
|
use_ik = (
|
||||||
|
meta["limb_state"] != "consensus"
|
||||||
|
or endpoint_state
|
||||||
|
in ("provisional", "recovered", "reassigned", "predicted_short")
|
||||||
|
)
|
||||||
|
elbow = (
|
||||||
|
solve_two_bone_elbow(
|
||||||
|
shoulder,
|
||||||
|
wrist,
|
||||||
|
upper_length,
|
||||||
|
forearm_length,
|
||||||
|
reference_elbow,
|
||||||
|
)
|
||||||
|
if use_ik
|
||||||
|
else smoothed_keypoints[elbow_idx]
|
||||||
|
)
|
||||||
|
endpoint.last_elbow = elbow.copy()
|
||||||
|
direction = wrist - shoulder
|
||||||
|
if float(np.linalg.norm(direction)) > 1e-6:
|
||||||
|
endpoint.last_direction = direction / np.linalg.norm(direction)
|
||||||
|
output[shoulder_idx] = shoulder
|
||||||
|
output[elbow_idx] = elbow
|
||||||
|
output[wrist_idx] = wrist
|
||||||
|
output[start:end] = hand
|
||||||
|
uncertainty = (
|
||||||
|
0.025 * diagonal
|
||||||
|
if endpoint_state == "observed"
|
||||||
|
else (0.04 + 0.015 * endpoint.missed_frames) * diagonal
|
||||||
|
)
|
||||||
|
precise = endpoint_state == "observed"
|
||||||
|
else:
|
||||||
|
region = reachable_region(
|
||||||
|
shoulder,
|
||||||
|
upper_length,
|
||||||
|
forearm_length,
|
||||||
|
bbox,
|
||||||
|
endpoint,
|
||||||
|
endpoint_state,
|
||||||
|
short_occlusion_frames,
|
||||||
|
medium_occlusion_frames,
|
||||||
|
)
|
||||||
|
uncertainty = region["outer_radius_px"]
|
||||||
|
precise = False
|
||||||
|
output_metadata[side] = {
|
||||||
|
"state": endpoint_state,
|
||||||
|
"precise_distance": precise,
|
||||||
|
"fast_alarm_candidate": True,
|
||||||
|
"risk_mode": (
|
||||||
|
"precise_hand"
|
||||||
|
if endpoint_state == "observed"
|
||||||
|
else (
|
||||||
|
"motion_corridor_intersection"
|
||||||
|
if endpoint_state == "reachable_medium"
|
||||||
|
else (
|
||||||
|
"door_intersection_only"
|
||||||
|
if endpoint_state == "reachable_long"
|
||||||
|
else (
|
||||||
|
"reassigned_hand"
|
||||||
|
if endpoint_state == "reassigned"
|
||||||
|
else "temporal_hand"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"requires_door_intersection": endpoint_state
|
||||||
|
in ("reachable_medium", "reachable_long"),
|
||||||
|
"identity_assignment": (
|
||||||
|
identity_assignments.get(side)
|
||||||
|
if identity_assignments is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
"missed_frames": endpoint.missed_frames,
|
||||||
|
"uncertainty_radius_px": round(float(uncertainty), 3),
|
||||||
|
"bone_lengths_px": {
|
||||||
|
"upper": round(upper_length, 3),
|
||||||
|
"forearm": round(forearm_length, 3),
|
||||||
|
},
|
||||||
|
"reachable_region": region,
|
||||||
|
}
|
||||||
|
return output, output_metadata
|
||||||
|
|
||||||
|
|
||||||
|
def mask_other_people(
|
||||||
|
frame: np.ndarray,
|
||||||
|
boxes: list[list[float]],
|
||||||
|
target_idx: int,
|
||||||
|
) -> np.ndarray:
|
||||||
|
masked = frame.copy()
|
||||||
|
height, width = frame.shape[:2]
|
||||||
|
mask = np.zeros((height, width), dtype=bool)
|
||||||
|
target = boxes[target_idx]
|
||||||
|
target_area = max((target[2] - target[0]) * (target[3] - target[1]), 1.0)
|
||||||
|
for idx, bbox in enumerate(boxes):
|
||||||
|
if idx == target_idx:
|
||||||
|
continue
|
||||||
|
other_area = max((bbox[2] - bbox[0]) * (bbox[3] - bbox[1]), 1.0)
|
||||||
|
# The failure in this video is the small background person being
|
||||||
|
# absorbed by the much larger foreground person's 3:4 pose crop.
|
||||||
|
# Mask only markedly smaller people. Masking a larger occluder while
|
||||||
|
# estimating a small person removes genuine target pixels at overlap.
|
||||||
|
if other_area >= 0.5 * target_area:
|
||||||
|
continue
|
||||||
|
x1, y1, x2, y2 = (int(round(value)) for value in bbox)
|
||||||
|
x1, x2 = max(0, x1), min(width - 1, x2)
|
||||||
|
y1, y2 = max(0, y1), min(height - 1, y2)
|
||||||
|
mask[y1:y2 + 1, x1:x2 + 1] = True
|
||||||
|
|
||||||
|
masked[mask] = (124, 116, 104)
|
||||||
|
return masked
|
||||||
@@ -1,683 +0,0 @@
|
|||||||
"""End-to-end optimized RTMW-L prediction for CMS videos.
|
|
||||||
|
|
||||||
Pipeline:
|
|
||||||
YOLO11s person -> fixed-camera false-positive filters -> lightweight track IDs
|
|
||||||
-> RTMW-L raw/smaller-person-isolated views -> temporal fusion -> causal
|
|
||||||
occlusion-aware V4 global hand assignment. Reachable regions stay in JSON and are hidden in the main
|
|
||||||
comparison video.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import math
|
|
||||||
import time
|
|
||||||
from collections import Counter, defaultdict, deque
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import cv2
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from compare_rtmw_ml import HAND_SLICES, draw_pose, summarize
|
|
||||||
from fuse_rtmw_l_temporal import (
|
|
||||||
BODY_SIDE_INDICES,
|
|
||||||
PersonTrack,
|
|
||||||
adaptive_smooth,
|
|
||||||
build_global_hands,
|
|
||||||
draw_fused,
|
|
||||||
fuse_person,
|
|
||||||
)
|
|
||||||
from occlusion_aware_pose_v2 import (
|
|
||||||
EndpointTrack,
|
|
||||||
Measurement,
|
|
||||||
calibrated_bone_lengths,
|
|
||||||
draw_v2,
|
|
||||||
reachable_region,
|
|
||||||
resolve_global_hand_assignments,
|
|
||||||
solve_two_bone_elbow,
|
|
||||||
temporal_recovery_measurement,
|
|
||||||
update_endpoint_track,
|
|
||||||
)
|
|
||||||
from rtmw_l_person_mask_ablation import mask_other_people
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("--videos", type=Path, nargs="+", required=True)
|
|
||||||
parser.add_argument("--det-model", type=Path, required=True)
|
|
||||||
parser.add_argument("--pose-model", type=Path, required=True)
|
|
||||||
parser.add_argument("--output", type=Path, required=True)
|
|
||||||
parser.add_argument(
|
|
||||||
"--det-device",
|
|
||||||
default="0",
|
|
||||||
help="Ultralytics YOLO device, for example 0, cuda:0, or cpu.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--pose-device",
|
|
||||||
default="cpu",
|
|
||||||
help="RTMW ONNX Runtime device: cpu or cuda.",
|
|
||||||
)
|
|
||||||
parser.add_argument("--det-imgsz", type=int, default=960)
|
|
||||||
parser.add_argument("--det-conf", type=float, default=0.30)
|
|
||||||
parser.add_argument("--det-roi-y-max-ratio", type=float, default=1.0)
|
|
||||||
parser.add_argument("--short-occlusion-frames", type=int, default=12)
|
|
||||||
parser.add_argument("--medium-occlusion-frames", type=int, default=30)
|
|
||||||
parser.add_argument("--max-people", type=int, default=3)
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def bbox_iou(a: np.ndarray, b: np.ndarray) -> float:
|
|
||||||
x1 = max(float(a[0]), float(b[0]))
|
|
||||||
y1 = max(float(a[1]), float(b[1]))
|
|
||||||
x2 = min(float(a[2]), float(b[2]))
|
|
||||||
y2 = min(float(a[3]), float(b[3]))
|
|
||||||
intersection = max(x2 - x1, 0.0) * max(y2 - y1, 0.0)
|
|
||||||
union = (
|
|
||||||
max(float(a[2] - a[0]), 0.0) * max(float(a[3] - a[1]), 0.0)
|
|
||||||
+ max(float(b[2] - b[0]), 0.0) * max(float(b[3] - b[1]), 0.0)
|
|
||||||
- intersection
|
|
||||||
)
|
|
||||||
return intersection / max(union, 1.0)
|
|
||||||
|
|
||||||
|
|
||||||
class BoxTracker:
|
|
||||||
def __init__(self, max_age: int = 30):
|
|
||||||
self.max_age = max_age
|
|
||||||
self.next_id = 1
|
|
||||||
self.tracks: dict[int, dict] = {}
|
|
||||||
|
|
||||||
def update(
|
|
||||||
self,
|
|
||||||
boxes: list[list[float]],
|
|
||||||
frame_idx: int,
|
|
||||||
image_shape: tuple[int, int],
|
|
||||||
) -> list[int]:
|
|
||||||
height, width = image_shape
|
|
||||||
image_diagonal = math.hypot(width, height)
|
|
||||||
active_ids = [
|
|
||||||
track_id
|
|
||||||
for track_id, track in self.tracks.items()
|
|
||||||
if frame_idx - track["last_frame"] <= self.max_age
|
|
||||||
]
|
|
||||||
self.tracks = {track_id: self.tracks[track_id] for track_id in active_ids}
|
|
||||||
pairs = []
|
|
||||||
for detection_idx, box in enumerate(boxes):
|
|
||||||
box_array = np.asarray(box, dtype=np.float64)
|
|
||||||
center = (box_array[:2] + box_array[2:]) * 0.5
|
|
||||||
for track_id in active_ids:
|
|
||||||
prior = np.asarray(self.tracks[track_id]["box"], dtype=np.float64)
|
|
||||||
prior_center = (prior[:2] + prior[2:]) * 0.5
|
|
||||||
iou = bbox_iou(box_array, prior)
|
|
||||||
center_distance = float(np.linalg.norm(center - prior_center)) / image_diagonal
|
|
||||||
if iou >= 0.05 or center_distance <= 0.18:
|
|
||||||
score = 2.0 * iou + max(0.0, 1.0 - center_distance / 0.18)
|
|
||||||
pairs.append((score, detection_idx, track_id))
|
|
||||||
|
|
||||||
assigned_detections = set()
|
|
||||||
assigned_tracks = set()
|
|
||||||
result: list[int | None] = [None] * len(boxes)
|
|
||||||
for _, detection_idx, track_id in sorted(pairs, reverse=True):
|
|
||||||
if detection_idx in assigned_detections or track_id in assigned_tracks:
|
|
||||||
continue
|
|
||||||
assigned_detections.add(detection_idx)
|
|
||||||
assigned_tracks.add(track_id)
|
|
||||||
result[detection_idx] = track_id
|
|
||||||
|
|
||||||
for detection_idx, box in enumerate(boxes):
|
|
||||||
track_id = result[detection_idx]
|
|
||||||
if track_id is None:
|
|
||||||
track_id = self.next_id
|
|
||||||
self.next_id += 1
|
|
||||||
result[detection_idx] = track_id
|
|
||||||
self.tracks[track_id] = {"box": box, "last_frame": frame_idx}
|
|
||||||
return [int(track_id) for track_id in result]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class LivePersonState:
|
|
||||||
fusion: PersonTrack = field(default_factory=PersonTrack)
|
|
||||||
endpoints: dict[str, EndpointTrack] = field(
|
|
||||||
default_factory=lambda: {"left": EndpointTrack(), "right": EndpointTrack()}
|
|
||||||
)
|
|
||||||
bone_samples: dict[str, deque] = field(
|
|
||||||
default_factory=lambda: {
|
|
||||||
"left": deque(maxlen=120),
|
|
||||||
"right": deque(maxlen=120),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
last_frame: int = -1
|
|
||||||
|
|
||||||
|
|
||||||
def bbox_diagonal(bbox: list[float]) -> float:
|
|
||||||
return max(math.hypot(bbox[2] - bbox[0], bbox[3] - bbox[1]), 1.0)
|
|
||||||
|
|
||||||
|
|
||||||
def update_bone_samples(
|
|
||||||
state: LivePersonState,
|
|
||||||
keypoints: np.ndarray,
|
|
||||||
metadata: dict,
|
|
||||||
bbox: list[float],
|
|
||||||
) -> None:
|
|
||||||
diagonal = bbox_diagonal(bbox)
|
|
||||||
for side, (shoulder, elbow, wrist) in BODY_SIDE_INDICES.items():
|
|
||||||
meta = metadata[side]
|
|
||||||
if meta["limb_state"] != "consensus" or not meta["distance_eligible"]:
|
|
||||||
continue
|
|
||||||
upper = float(np.linalg.norm(keypoints[shoulder] - keypoints[elbow])) / diagonal
|
|
||||||
forearm = float(np.linalg.norm(keypoints[elbow] - keypoints[wrist])) / diagonal
|
|
||||||
if 0.04 <= upper <= 0.65 and 0.04 <= forearm <= 0.65:
|
|
||||||
state.bone_samples[side].append((upper, forearm))
|
|
||||||
|
|
||||||
|
|
||||||
def current_bone_ratios(state: LivePersonState, side: str) -> tuple[float, float]:
|
|
||||||
samples = state.bone_samples[side]
|
|
||||||
if len(samples) < 10:
|
|
||||||
opposite = "right" if side == "left" else "left"
|
|
||||||
if len(state.bone_samples[opposite]) >= 10:
|
|
||||||
samples = state.bone_samples[opposite]
|
|
||||||
if not samples:
|
|
||||||
return 0.24, 0.22
|
|
||||||
values = np.asarray(samples, dtype=np.float64)
|
|
||||||
return float(np.median(values[:, 0])), float(np.median(values[:, 1]))
|
|
||||||
|
|
||||||
|
|
||||||
def update_live_v2(
|
|
||||||
frame_idx: int,
|
|
||||||
bbox: list[float],
|
|
||||||
selected_keypoints: np.ndarray,
|
|
||||||
smoothed_keypoints: np.ndarray,
|
|
||||||
fusion_metadata: dict,
|
|
||||||
state: LivePersonState,
|
|
||||||
assigned_measurements: dict[str, Measurement | None] | None,
|
|
||||||
identity_assignments: dict[str, dict] | None,
|
|
||||||
short_occlusion_frames: int,
|
|
||||||
medium_occlusion_frames: int,
|
|
||||||
) -> tuple[np.ndarray, dict]:
|
|
||||||
frame_gap = 0 if state.last_frame < 0 else max(frame_idx - state.last_frame - 1, 0)
|
|
||||||
for endpoint in state.endpoints.values():
|
|
||||||
endpoint.missed_frames += frame_gap
|
|
||||||
state.last_frame = frame_idx
|
|
||||||
update_bone_samples(state, selected_keypoints, fusion_metadata, bbox)
|
|
||||||
|
|
||||||
output = smoothed_keypoints.copy()
|
|
||||||
output_metadata = {}
|
|
||||||
diagonal = bbox_diagonal(bbox)
|
|
||||||
for side, (shoulder_idx, elbow_idx, wrist_idx) in BODY_SIDE_INDICES.items():
|
|
||||||
endpoint = state.endpoints[side]
|
|
||||||
meta = fusion_metadata[side]
|
|
||||||
start, end = HAND_SLICES[side]
|
|
||||||
measurement = (
|
|
||||||
assigned_measurements.get(side)
|
|
||||||
if assigned_measurements is not None
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
if assigned_measurements is None and meta["alarm_eligible"]:
|
|
||||||
measurement = Measurement(
|
|
||||||
keypoints=selected_keypoints[start:end],
|
|
||||||
precise=bool(meta["distance_eligible"]),
|
|
||||||
provisional=not bool(meta["distance_eligible"]),
|
|
||||||
quality=float(meta.get("selected_hand_score", 0.0)),
|
|
||||||
)
|
|
||||||
if measurement is None:
|
|
||||||
measurement = temporal_recovery_measurement(
|
|
||||||
selected_keypoints,
|
|
||||||
meta,
|
|
||||||
side,
|
|
||||||
bbox,
|
|
||||||
endpoint,
|
|
||||||
medium_occlusion_frames,
|
|
||||||
)
|
|
||||||
returning_after_long = (
|
|
||||||
measurement is not None
|
|
||||||
and endpoint.missed_frames > short_occlusion_frames
|
|
||||||
)
|
|
||||||
wrist, hand, endpoint_state = update_endpoint_track(
|
|
||||||
endpoint,
|
|
||||||
measurement,
|
|
||||||
frame_idx,
|
|
||||||
short_occlusion_frames,
|
|
||||||
medium_occlusion_frames,
|
|
||||||
)
|
|
||||||
shoulder = smoothed_keypoints[shoulder_idx]
|
|
||||||
upper_ratio, forearm_ratio = current_bone_ratios(state, side)
|
|
||||||
upper_length, forearm_length = calibrated_bone_lengths(
|
|
||||||
upper_ratio, forearm_ratio, bbox
|
|
||||||
)
|
|
||||||
region = None
|
|
||||||
if wrist is not None and hand is not None:
|
|
||||||
reference_elbow = (
|
|
||||||
selected_keypoints[elbow_idx]
|
|
||||||
if returning_after_long or endpoint.last_elbow is None
|
|
||||||
else endpoint.last_elbow
|
|
||||||
)
|
|
||||||
use_ik = (
|
|
||||||
meta["limb_state"] != "consensus"
|
|
||||||
or endpoint_state
|
|
||||||
in ("provisional", "recovered", "reassigned", "predicted_short")
|
|
||||||
)
|
|
||||||
elbow = (
|
|
||||||
solve_two_bone_elbow(
|
|
||||||
shoulder,
|
|
||||||
wrist,
|
|
||||||
upper_length,
|
|
||||||
forearm_length,
|
|
||||||
reference_elbow,
|
|
||||||
)
|
|
||||||
if use_ik
|
|
||||||
else smoothed_keypoints[elbow_idx]
|
|
||||||
)
|
|
||||||
endpoint.last_elbow = elbow.copy()
|
|
||||||
direction = wrist - shoulder
|
|
||||||
if float(np.linalg.norm(direction)) > 1e-6:
|
|
||||||
endpoint.last_direction = direction / np.linalg.norm(direction)
|
|
||||||
output[shoulder_idx] = shoulder
|
|
||||||
output[elbow_idx] = elbow
|
|
||||||
output[wrist_idx] = wrist
|
|
||||||
output[start:end] = hand
|
|
||||||
uncertainty = (
|
|
||||||
0.025 * diagonal
|
|
||||||
if endpoint_state == "observed"
|
|
||||||
else (0.04 + 0.015 * endpoint.missed_frames) * diagonal
|
|
||||||
)
|
|
||||||
precise = endpoint_state == "observed"
|
|
||||||
else:
|
|
||||||
region = reachable_region(
|
|
||||||
shoulder,
|
|
||||||
upper_length,
|
|
||||||
forearm_length,
|
|
||||||
bbox,
|
|
||||||
endpoint,
|
|
||||||
endpoint_state,
|
|
||||||
short_occlusion_frames,
|
|
||||||
medium_occlusion_frames,
|
|
||||||
)
|
|
||||||
uncertainty = region["outer_radius_px"]
|
|
||||||
precise = False
|
|
||||||
output_metadata[side] = {
|
|
||||||
"state": endpoint_state,
|
|
||||||
"precise_distance": precise,
|
|
||||||
"fast_alarm_candidate": True,
|
|
||||||
"risk_mode": (
|
|
||||||
"precise_hand"
|
|
||||||
if endpoint_state == "observed"
|
|
||||||
else (
|
|
||||||
"motion_corridor_intersection"
|
|
||||||
if endpoint_state == "reachable_medium"
|
|
||||||
else (
|
|
||||||
"door_intersection_only"
|
|
||||||
if endpoint_state == "reachable_long"
|
|
||||||
else (
|
|
||||||
"reassigned_hand"
|
|
||||||
if endpoint_state == "reassigned"
|
|
||||||
else "temporal_hand"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"requires_door_intersection": endpoint_state
|
|
||||||
in ("reachable_medium", "reachable_long"),
|
|
||||||
"identity_assignment": (
|
|
||||||
identity_assignments.get(side)
|
|
||||||
if identity_assignments is not None
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
"missed_frames": endpoint.missed_frames,
|
|
||||||
"uncertainty_radius_px": round(float(uncertainty), 3),
|
|
||||||
"bone_lengths_px": {
|
|
||||||
"upper": round(upper_length, 3),
|
|
||||||
"forearm": round(forearm_length, 3),
|
|
||||||
},
|
|
||||||
"reachable_region": region,
|
|
||||||
}
|
|
||||||
return output, output_metadata
|
|
||||||
|
|
||||||
|
|
||||||
def filter_person_boxes(
|
|
||||||
result,
|
|
||||||
width: int,
|
|
||||||
height: int,
|
|
||||||
max_people: int,
|
|
||||||
) -> tuple[list[list[float]], list[float], Counter]:
|
|
||||||
boxes = result.boxes.xyxy.cpu().numpy()
|
|
||||||
scores = result.boxes.conf.cpu().numpy()
|
|
||||||
rejected = Counter()
|
|
||||||
kept = []
|
|
||||||
for box, score in zip(boxes, scores):
|
|
||||||
center_x = float(box[0] + box[2]) * 0.5
|
|
||||||
box_height = float(box[3] - box[1])
|
|
||||||
if center_x > 0.80 * width:
|
|
||||||
rejected["right_reflection_roi"] += 1
|
|
||||||
continue
|
|
||||||
if box_height < 0.20 * height:
|
|
||||||
rejected["too_short"] += 1
|
|
||||||
continue
|
|
||||||
kept.append((box.tolist(), float(score)))
|
|
||||||
kept.sort(key=lambda item: item[1], reverse=True)
|
|
||||||
if len(kept) > max_people:
|
|
||||||
rejected["over_max_people"] += len(kept) - max_people
|
|
||||||
kept = kept[:max_people]
|
|
||||||
kept.sort(key=lambda item: item[0][0])
|
|
||||||
return [item[0] for item in kept], [item[1] for item in kept], rejected
|
|
||||||
|
|
||||||
|
|
||||||
def process_video(
|
|
||||||
video_path: Path,
|
|
||||||
output_dir: Path,
|
|
||||||
detector: YOLO,
|
|
||||||
pose: RTMPose,
|
|
||||||
det_device: str,
|
|
||||||
det_imgsz: int,
|
|
||||||
det_conf: float,
|
|
||||||
det_roi_y_max_ratio: float,
|
|
||||||
short_occlusion_frames: int,
|
|
||||||
medium_occlusion_frames: int,
|
|
||||||
max_people: int,
|
|
||||||
video_label: str,
|
|
||||||
) -> dict:
|
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
cap = cv2.VideoCapture(str(video_path))
|
|
||||||
fps = float(cap.get(cv2.CAP_PROP_FPS))
|
|
||||||
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
||||||
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
||||||
writer = cv2.VideoWriter(
|
|
||||||
str(output_dir / "raw_temporal_occlusion_v4.mp4"),
|
|
||||||
cv2.VideoWriter_fourcc(*"mp4v"),
|
|
||||||
fps,
|
|
||||||
(1920, 360),
|
|
||||||
)
|
|
||||||
|
|
||||||
box_tracker = BoxTracker(max_age=30)
|
|
||||||
people: dict[int, LivePersonState] = {}
|
|
||||||
records = []
|
|
||||||
detection_counts = Counter()
|
|
||||||
rejection_counts = Counter()
|
|
||||||
state_counts = Counter()
|
|
||||||
timings = defaultdict(list)
|
|
||||||
isolated_inferences = 0
|
|
||||||
frame_idx = 0
|
|
||||||
|
|
||||||
while True:
|
|
||||||
ok, frame = cap.read()
|
|
||||||
if not ok:
|
|
||||||
break
|
|
||||||
started = time.perf_counter()
|
|
||||||
detection_height = max(1, min(height, round(height * det_roi_y_max_ratio)))
|
|
||||||
detection = detector.predict(
|
|
||||||
frame[:detection_height],
|
|
||||||
imgsz=det_imgsz,
|
|
||||||
conf=det_conf,
|
|
||||||
iou=0.30,
|
|
||||||
classes=[0],
|
|
||||||
device=det_device,
|
|
||||||
verbose=False,
|
|
||||||
)[0]
|
|
||||||
timings["detector_ms"].append((time.perf_counter() - started) * 1000.0)
|
|
||||||
boxes, confidences, rejected = filter_person_boxes(
|
|
||||||
detection, width, height, max_people
|
|
||||||
)
|
|
||||||
rejection_counts.update(rejected)
|
|
||||||
detection_counts[len(boxes)] += 1
|
|
||||||
track_ids = box_tracker.update(boxes, frame_idx, (height, width))
|
|
||||||
|
|
||||||
if boxes:
|
|
||||||
started = time.perf_counter()
|
|
||||||
raw_keypoints, raw_scores = pose(frame, bboxes=boxes)
|
|
||||||
timings["raw_pose_ms"].append((time.perf_counter() - started) * 1000.0)
|
|
||||||
isolated_keypoints = raw_keypoints.copy()
|
|
||||||
isolated_scores = raw_scores.copy()
|
|
||||||
started = time.perf_counter()
|
|
||||||
for person_idx, bbox in enumerate(boxes):
|
|
||||||
target_area = max((bbox[2] - bbox[0]) * (bbox[3] - bbox[1]), 1.0)
|
|
||||||
needs_isolation = any(
|
|
||||||
idx != person_idx
|
|
||||||
and max((other[2] - other[0]) * (other[3] - other[1]), 1.0)
|
|
||||||
< 0.5 * target_area
|
|
||||||
for idx, other in enumerate(boxes)
|
|
||||||
)
|
|
||||||
if not needs_isolation:
|
|
||||||
continue
|
|
||||||
masked = mask_other_people(frame, boxes, person_idx)
|
|
||||||
kpts, scores = pose(masked, bboxes=[bbox])
|
|
||||||
isolated_keypoints[person_idx] = kpts[0]
|
|
||||||
isolated_scores[person_idx] = scores[0]
|
|
||||||
isolated_inferences += 1
|
|
||||||
timings["isolated_extra_ms"].append((time.perf_counter() - started) * 1000.0)
|
|
||||||
else:
|
|
||||||
raw_keypoints = np.empty((0, 133, 2), dtype=np.float64)
|
|
||||||
raw_scores = np.empty((0, 133), dtype=np.float64)
|
|
||||||
isolated_keypoints = raw_keypoints.copy()
|
|
||||||
isolated_scores = raw_scores.copy()
|
|
||||||
|
|
||||||
selected_list = []
|
|
||||||
smoothed_list = []
|
|
||||||
fusion_meta_list = []
|
|
||||||
person_states = []
|
|
||||||
for person_idx, (bbox, track_id) in enumerate(zip(boxes, track_ids)):
|
|
||||||
state = people.setdefault(track_id, LivePersonState())
|
|
||||||
selected, fusion_meta = fuse_person(
|
|
||||||
raw_keypoints[person_idx],
|
|
||||||
raw_scores[person_idx],
|
|
||||||
isolated_keypoints[person_idx],
|
|
||||||
isolated_scores[person_idx],
|
|
||||||
bbox,
|
|
||||||
state.fusion,
|
|
||||||
)
|
|
||||||
smoothed = adaptive_smooth(selected, bbox, state.fusion)
|
|
||||||
selected_list.append(selected)
|
|
||||||
smoothed_list.append(smoothed)
|
|
||||||
fusion_meta_list.append(fusion_meta)
|
|
||||||
person_states.append(state)
|
|
||||||
|
|
||||||
frame_measurements = {}
|
|
||||||
endpoint_tracks = {}
|
|
||||||
for person_idx, (selected, fusion_meta, state) in enumerate(
|
|
||||||
zip(selected_list, fusion_meta_list, person_states)
|
|
||||||
):
|
|
||||||
for side, (start, end) in HAND_SLICES.items():
|
|
||||||
meta = fusion_meta[side]
|
|
||||||
frame_measurements[(person_idx, side)] = (
|
|
||||||
Measurement(
|
|
||||||
keypoints=selected[start:end],
|
|
||||||
precise=bool(meta["distance_eligible"]),
|
|
||||||
provisional=not bool(meta["distance_eligible"]),
|
|
||||||
quality=float(meta.get("selected_hand_score", 0.0)),
|
|
||||||
)
|
|
||||||
if meta["alarm_eligible"]
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
endpoint_tracks[(person_idx, side)] = state.endpoints[side]
|
|
||||||
frame_measurements, assignment_events = resolve_global_hand_assignments(
|
|
||||||
frame_measurements,
|
|
||||||
endpoint_tracks,
|
|
||||||
boxes,
|
|
||||||
medium_occlusion_frames,
|
|
||||||
)
|
|
||||||
|
|
||||||
v2_list = []
|
|
||||||
v2_meta_list = []
|
|
||||||
for person_idx, (bbox, selected, smoothed, fusion_meta, state) in enumerate(
|
|
||||||
zip(
|
|
||||||
boxes,
|
|
||||||
selected_list,
|
|
||||||
smoothed_list,
|
|
||||||
fusion_meta_list,
|
|
||||||
person_states,
|
|
||||||
)
|
|
||||||
):
|
|
||||||
v2, v2_meta = update_live_v2(
|
|
||||||
frame_idx,
|
|
||||||
bbox,
|
|
||||||
selected,
|
|
||||||
smoothed,
|
|
||||||
fusion_meta,
|
|
||||||
state,
|
|
||||||
{
|
|
||||||
side: frame_measurements[(person_idx, side)]
|
|
||||||
for side in ("left", "right")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
side: assignment_events.get((person_idx, side))
|
|
||||||
for side in ("left", "right")
|
|
||||||
},
|
|
||||||
short_occlusion_frames,
|
|
||||||
medium_occlusion_frames,
|
|
||||||
)
|
|
||||||
v2_list.append(v2)
|
|
||||||
v2_meta_list.append(v2_meta)
|
|
||||||
for side in ("left", "right"):
|
|
||||||
state_counts[f"{v2_meta[side]['state']}"] += 1
|
|
||||||
|
|
||||||
global_hands = build_global_hands(selected_list, fusion_meta_list)
|
|
||||||
records.append(
|
|
||||||
{
|
|
||||||
"frame_index": frame_idx,
|
|
||||||
"timestamp_ms": round(frame_idx / fps * 1000.0, 3),
|
|
||||||
"track_ids": track_ids,
|
|
||||||
"boxes": np.round(np.asarray(boxes), 3).tolist() if boxes else [],
|
|
||||||
"det_confidences": [round(value, 4) for value in confidences],
|
|
||||||
"raw_keypoints": np.round(raw_keypoints, 3).tolist(),
|
|
||||||
"temporal_selected_keypoints": np.round(np.asarray(selected_list), 3).tolist(),
|
|
||||||
"temporal_keypoints": np.round(np.asarray(smoothed_list), 3).tolist(),
|
|
||||||
"temporal_metadata": fusion_meta_list,
|
|
||||||
"v2_keypoints": np.round(np.asarray(v2_list), 3).tolist(),
|
|
||||||
"v2_metadata": v2_meta_list,
|
|
||||||
"global_hands": global_hands,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
raw_panel = draw_pose(
|
|
||||||
frame,
|
|
||||||
boxes,
|
|
||||||
raw_keypoints,
|
|
||||||
f"{video_label} RTMW-L raw | frame {frame_idx}",
|
|
||||||
)
|
|
||||||
temporal_panel = draw_fused(
|
|
||||||
frame,
|
|
||||||
boxes,
|
|
||||||
smoothed_list,
|
|
||||||
fusion_meta_list,
|
|
||||||
f"{video_label} temporal fusion | frame {frame_idx}",
|
|
||||||
)
|
|
||||||
v2_panel = draw_v2(
|
|
||||||
frame,
|
|
||||||
boxes,
|
|
||||||
np.asarray(v2_list),
|
|
||||||
v2_meta_list,
|
|
||||||
f"{video_label} occlusion V4 | frame {frame_idx}",
|
|
||||||
show_reachable=False,
|
|
||||||
)
|
|
||||||
panels = [
|
|
||||||
cv2.resize(panel, (640, 360), interpolation=cv2.INTER_AREA)
|
|
||||||
for panel in (raw_panel, temporal_panel, v2_panel)
|
|
||||||
]
|
|
||||||
writer.write(np.hstack(panels))
|
|
||||||
if frame_idx == 0 and video_path.suffix.lower() in {
|
|
||||||
".jpg",
|
|
||||||
".jpeg",
|
|
||||||
".png",
|
|
||||||
".bmp",
|
|
||||||
".webp",
|
|
||||||
}:
|
|
||||||
cv2.imwrite(str(output_dir / "raw_pose.jpg"), raw_panel)
|
|
||||||
cv2.imwrite(str(output_dir / "spatial_fusion.jpg"), temporal_panel)
|
|
||||||
cv2.imwrite(str(output_dir / "occlusion_v2.jpg"), v2_panel)
|
|
||||||
panel_width = 960
|
|
||||||
panel_height = round(height * panel_width / width)
|
|
||||||
image_panels = [
|
|
||||||
cv2.resize(
|
|
||||||
panel,
|
|
||||||
(panel_width, panel_height),
|
|
||||||
interpolation=cv2.INTER_AREA,
|
|
||||||
)
|
|
||||||
for panel in (raw_panel, temporal_panel, v2_panel)
|
|
||||||
]
|
|
||||||
cv2.imwrite(
|
|
||||||
str(output_dir / "raw_spatial_occlusion_v2.jpg"),
|
|
||||||
np.hstack(image_panels),
|
|
||||||
)
|
|
||||||
if frame_idx % 100 == 0:
|
|
||||||
print(f"{video_label}: processed frame {frame_idx}", flush=True)
|
|
||||||
frame_idx += 1
|
|
||||||
|
|
||||||
cap.release()
|
|
||||||
writer.release()
|
|
||||||
with (output_dir / "predictions.jsonl").open("w", encoding="utf-8") as handle:
|
|
||||||
for record in records:
|
|
||||||
handle.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
||||||
|
|
||||||
summary = {
|
|
||||||
"occlusion_algorithm": "v4_global_assignment",
|
|
||||||
"video": str(video_path.resolve()),
|
|
||||||
"frames": len(records),
|
|
||||||
"fps": round(fps, 3),
|
|
||||||
"resolution": [width, height],
|
|
||||||
"person_filter": {
|
|
||||||
"center_x_max_ratio": 0.80,
|
|
||||||
"minimum_box_height_ratio": 0.20,
|
|
||||||
"max_people": max_people,
|
|
||||||
},
|
|
||||||
"detector_settings": {
|
|
||||||
"imgsz": det_imgsz,
|
|
||||||
"confidence": det_conf,
|
|
||||||
"roi_y_max_ratio": det_roi_y_max_ratio,
|
|
||||||
},
|
|
||||||
"filtered_person_count_frames": dict(sorted(detection_counts.items())),
|
|
||||||
"rejected_detection_counts": dict(sorted(rejection_counts.items())),
|
|
||||||
"track_ids_created": box_tracker.next_id - 1,
|
|
||||||
"isolated_pose_inferences": isolated_inferences,
|
|
||||||
"short_occlusion_frames": short_occlusion_frames,
|
|
||||||
"medium_occlusion_frames": medium_occlusion_frames,
|
|
||||||
"v2_state_counts": dict(sorted(state_counts.items())),
|
|
||||||
"timing_ms_cpu": {
|
|
||||||
name: summarize(values[5:] if len(values) > 5 else values)
|
|
||||||
for name, values in timings.items()
|
|
||||||
},
|
|
||||||
"output_note": (
|
|
||||||
"Main video hides reachable-region arcs. Reachable-region geometry remains in "
|
|
||||||
"predictions.jsonl for later door-distance lower-bound calculation."
|
|
||||||
),
|
|
||||||
}
|
|
||||||
with (output_dir / "summary.json").open("w", encoding="utf-8") as handle:
|
|
||||||
json.dump(summary, handle, ensure_ascii=False, indent=2)
|
|
||||||
return summary
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
from rtmlib import RTMPose
|
|
||||||
from ultralytics import YOLO
|
|
||||||
args = parse_args()
|
|
||||||
detector = YOLO(str(args.det_model))
|
|
||||||
pose = RTMPose(
|
|
||||||
str(args.pose_model),
|
|
||||||
model_input_size=(288, 384),
|
|
||||||
backend="onnxruntime",
|
|
||||||
device=args.pose_device,
|
|
||||||
)
|
|
||||||
all_summaries = {}
|
|
||||||
for video_idx, video_path in enumerate(args.videos, start=1):
|
|
||||||
label = f"video{video_idx}"
|
|
||||||
output_dir = args.output / label
|
|
||||||
all_summaries[label] = process_video(
|
|
||||||
video_path,
|
|
||||||
output_dir,
|
|
||||||
detector,
|
|
||||||
pose,
|
|
||||||
args.det_device,
|
|
||||||
args.det_imgsz,
|
|
||||||
args.det_conf,
|
|
||||||
args.det_roi_y_max_ratio,
|
|
||||||
args.short_occlusion_frames,
|
|
||||||
args.medium_occlusion_frames,
|
|
||||||
args.max_people,
|
|
||||||
label,
|
|
||||||
)
|
|
||||||
args.output.mkdir(parents=True, exist_ok=True)
|
|
||||||
with (args.output / "summary.json").open("w", encoding="utf-8") as handle:
|
|
||||||
json.dump(all_summaries, handle, ensure_ascii=False, indent=2)
|
|
||||||
print(json.dumps(all_summaries, ensure_ascii=False, indent=2))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
"""Test whether other people inside a top-down crop cause RTMW-L mix-ups."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import cv2
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from compare_rtmw_ml import (
|
|
||||||
draw_pose,
|
|
||||||
model_disagreement,
|
|
||||||
summarize,
|
|
||||||
temporal_metrics,
|
|
||||||
write_montage,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("--video", type=Path, required=True)
|
|
||||||
parser.add_argument("--baseline-jsonl", type=Path, required=True)
|
|
||||||
parser.add_argument("--model-l", type=Path, required=True)
|
|
||||||
parser.add_argument("--output", type=Path, required=True)
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def mask_other_people(
|
|
||||||
frame: np.ndarray,
|
|
||||||
boxes: list[list[float]],
|
|
||||||
target_idx: int,
|
|
||||||
) -> np.ndarray:
|
|
||||||
masked = frame.copy()
|
|
||||||
height, width = frame.shape[:2]
|
|
||||||
mask = np.zeros((height, width), dtype=bool)
|
|
||||||
target = boxes[target_idx]
|
|
||||||
target_area = max((target[2] - target[0]) * (target[3] - target[1]), 1.0)
|
|
||||||
for idx, bbox in enumerate(boxes):
|
|
||||||
if idx == target_idx:
|
|
||||||
continue
|
|
||||||
other_area = max((bbox[2] - bbox[0]) * (bbox[3] - bbox[1]), 1.0)
|
|
||||||
# The failure in this video is the small background person being
|
|
||||||
# absorbed by the much larger foreground person's 3:4 pose crop.
|
|
||||||
# Mask only markedly smaller people. Masking a larger occluder while
|
|
||||||
# estimating a small person removes genuine target pixels at overlap.
|
|
||||||
if other_area >= 0.5 * target_area:
|
|
||||||
continue
|
|
||||||
x1, y1, x2, y2 = (int(round(value)) for value in bbox)
|
|
||||||
x1, x2 = max(0, x1), min(width - 1, x2)
|
|
||||||
y1, y2 = max(0, y1), min(height - 1, y2)
|
|
||||||
mask[y1:y2 + 1, x1:x2 + 1] = True
|
|
||||||
|
|
||||||
masked[mask] = (124, 116, 104)
|
|
||||||
return masked
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
from rtmlib import RTMPose
|
|
||||||
args = parse_args()
|
|
||||||
args.output.mkdir(parents=True, exist_ok=True)
|
|
||||||
baseline = [
|
|
||||||
json.loads(line)
|
|
||||||
for line in args.baseline_jsonl.read_text(encoding="utf-8").splitlines()
|
|
||||||
]
|
|
||||||
pose_l = RTMPose(
|
|
||||||
str(args.model_l), model_input_size=(288, 384), backend="onnxruntime", device="cpu"
|
|
||||||
)
|
|
||||||
|
|
||||||
cap = cv2.VideoCapture(str(args.video))
|
|
||||||
fps = float(cap.get(cv2.CAP_PROP_FPS))
|
|
||||||
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
||||||
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
||||||
writer = cv2.VideoWriter(
|
|
||||||
str(args.output / "rtmw_l_raw_vs_person_masked.mp4"),
|
|
||||||
cv2.VideoWriter_fourcc(*"mp4v"),
|
|
||||||
fps,
|
|
||||||
(width, height // 2),
|
|
||||||
)
|
|
||||||
|
|
||||||
comparison: list[dict] = []
|
|
||||||
timings: list[float] = []
|
|
||||||
frame_idx = 0
|
|
||||||
while True:
|
|
||||||
ok, frame = cap.read()
|
|
||||||
if not ok:
|
|
||||||
break
|
|
||||||
baseline_record = baseline[frame_idx]
|
|
||||||
boxes = baseline_record["boxes"][:2]
|
|
||||||
masked_keypoints = []
|
|
||||||
masked_scores = []
|
|
||||||
started = time.perf_counter()
|
|
||||||
for person_idx, bbox in enumerate(boxes):
|
|
||||||
masked_frame = mask_other_people(frame, boxes, person_idx)
|
|
||||||
keypoints, scores = pose_l(masked_frame, bboxes=[bbox])
|
|
||||||
masked_keypoints.append(keypoints[0])
|
|
||||||
masked_scores.append(scores[0])
|
|
||||||
timings.append((time.perf_counter() - started) * 1000.0)
|
|
||||||
masked_keypoints_array = np.asarray(masked_keypoints)
|
|
||||||
|
|
||||||
raw_keypoints = np.asarray(baseline_record["L"]["keypoints"][:2])
|
|
||||||
comparison.append(
|
|
||||||
{
|
|
||||||
"frame_index": frame_idx,
|
|
||||||
"boxes": boxes,
|
|
||||||
"M": {"keypoints": raw_keypoints.tolist()},
|
|
||||||
"L": {"keypoints": np.round(masked_keypoints_array, 3).tolist()},
|
|
||||||
"masked_raw_scores": np.round(np.asarray(masked_scores), 4).tolist(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
left = draw_pose(frame, boxes, raw_keypoints, f"RTMW-L raw | frame {frame_idx}")
|
|
||||||
right = draw_pose(
|
|
||||||
frame,
|
|
||||||
boxes,
|
|
||||||
masked_keypoints_array,
|
|
||||||
f"RTMW-L mask other person | frame {frame_idx}",
|
|
||||||
)
|
|
||||||
left = cv2.resize(left, (width // 2, height // 2), interpolation=cv2.INTER_AREA)
|
|
||||||
right = cv2.resize(right, (width // 2, height // 2), interpolation=cv2.INTER_AREA)
|
|
||||||
writer.write(np.hstack([left, right]))
|
|
||||||
if frame_idx % 50 == 0:
|
|
||||||
print(f"processed frame {frame_idx}", flush=True)
|
|
||||||
frame_idx += 1
|
|
||||||
|
|
||||||
cap.release()
|
|
||||||
writer.release()
|
|
||||||
|
|
||||||
with (args.output / "rtmw_l_raw_vs_person_masked.jsonl").open(
|
|
||||||
"w", encoding="utf-8"
|
|
||||||
) as handle:
|
|
||||||
for record in comparison:
|
|
||||||
handle.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
||||||
|
|
||||||
disagreement, worst_frames = model_disagreement(comparison)
|
|
||||||
fixed_frames = [0, 60, 120, 132, 136, 180, 240, 252, 300, 360, 411]
|
|
||||||
montage_frames = sorted(set(fixed_frames + worst_frames[:8]))
|
|
||||||
write_montage(
|
|
||||||
args.video,
|
|
||||||
comparison,
|
|
||||||
montage_frames,
|
|
||||||
args.output / "rtmw_l_raw_vs_person_masked_montage.jpg",
|
|
||||||
titles=("RTMW-L raw", "RTMW-L smaller-person masked"),
|
|
||||||
)
|
|
||||||
|
|
||||||
summary = {
|
|
||||||
"frames": len(comparison),
|
|
||||||
"labels": {
|
|
||||||
"M": "RTMW-L raw crop",
|
|
||||||
"L": "RTMW-L with markedly smaller people masked",
|
|
||||||
},
|
|
||||||
"timing_ms_two_people_cpu": summarize(timings[5:]),
|
|
||||||
"temporal_metrics_normalized_to_yolo_box": temporal_metrics(comparison),
|
|
||||||
"position_change_after_masking": disagreement,
|
|
||||||
"largest_change_frames": worst_frames,
|
|
||||||
"montage_frames": montage_frames,
|
|
||||||
}
|
|
||||||
with (args.output / "summary.json").open("w", encoding="utf-8") as handle:
|
|
||||||
json.dump(summary, handle, ensure_ascii=False, indent=2)
|
|
||||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,390 +0,0 @@
|
|||||||
"""Lightweight causal door-risk inference using only YOLO11s and RTMW-L."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import cv2
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
|
|
||||||
PACKAGE_DIR = Path(__file__).resolve().parents[1]
|
|
||||||
PIPELINE_DIR = Path(__file__).resolve().parent
|
|
||||||
for import_dir in (PIPELINE_DIR, PACKAGE_DIR):
|
|
||||||
if str(import_dir) not in sys.path:
|
|
||||||
sys.path.insert(0, str(import_dir))
|
|
||||||
|
|
||||||
from door_mask_risk import DoorMaskSequence, DoorRiskEvaluator # noqa: E402
|
|
||||||
from door_zone_risk import ( # noqa: E402
|
|
||||||
DoorZoneAssigner,
|
|
||||||
DoorZoneConfig,
|
|
||||||
build_door_risks,
|
|
||||||
)
|
|
||||||
from live_pose_engine import LivePoseEngine # noqa: E402
|
|
||||||
|
|
||||||
|
|
||||||
def parse_image_size(value: str) -> int | tuple[int, int]:
|
|
||||||
parts = value.lower().replace(",", "x").split("x")
|
|
||||||
if len(parts) == 1:
|
|
||||||
return int(parts[0])
|
|
||||||
if len(parts) == 2:
|
|
||||||
return int(parts[0]), int(parts[1])
|
|
||||||
raise argparse.ArgumentTypeError(
|
|
||||||
"image size must be one integer or HEIGHTxWIDTH, for example 960 or 544x960"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
|
||||||
parser.add_argument("--video", type=Path, required=True)
|
|
||||||
door_source = parser.add_mutually_exclusive_group(required=True)
|
|
||||||
door_source.add_argument("--door-mask-dir", type=Path)
|
|
||||||
door_source.add_argument(
|
|
||||||
"--constant-door-mask",
|
|
||||||
type=Path,
|
|
||||||
help="One X-AnyLabeling JSON or raster mask reused for every video frame.",
|
|
||||||
)
|
|
||||||
parser.add_argument("--output", type=Path, required=True)
|
|
||||||
parser.add_argument(
|
|
||||||
"--det-model", type=Path, default=PACKAGE_DIR / "models" / "yolo11s.onnx"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--pose-model", type=Path, default=PACKAGE_DIR / "models" / "rtmw-l-384.onnx"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--pose-jsonl",
|
|
||||||
type=Path,
|
|
||||||
help="Diagnostic replay input; bypasses live YOLO/RTMW model execution.",
|
|
||||||
)
|
|
||||||
parser.add_argument("--det-device", default="0")
|
|
||||||
parser.add_argument("--pose-device", default="cuda")
|
|
||||||
parser.add_argument(
|
|
||||||
"--pose-provider",
|
|
||||||
choices=("cuda", "tensorrt"),
|
|
||||||
default="cuda",
|
|
||||||
help="ONNX Runtime provider for RTMW.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--pose-trt-cache",
|
|
||||||
type=Path,
|
|
||||||
default=PACKAGE_DIR / "models" / "rtmw_l_trt_cache",
|
|
||||||
)
|
|
||||||
parser.add_argument("--det-imgsz", type=parse_image_size, default=960)
|
|
||||||
parser.add_argument("--det-conf", type=float, default=0.30)
|
|
||||||
parser.add_argument("--det-roi-y-max-ratio", type=float, default=1.0)
|
|
||||||
parser.add_argument("--max-people", type=int, default=3)
|
|
||||||
parser.add_argument(
|
|
||||||
"--door-mask-format",
|
|
||||||
choices=("auto", "xanylabeling", "raster"),
|
|
||||||
default="auto",
|
|
||||||
)
|
|
||||||
parser.add_argument("--door-label", action="append")
|
|
||||||
parser.add_argument("--door-frame-offset", type=int, default=0)
|
|
||||||
parser.add_argument("--require-door-mask", action="store_true")
|
|
||||||
parser.add_argument("--risk-warning-relative", type=float, default=0.30)
|
|
||||||
parser.add_argument("--risk-release-relative", type=float, default=0.45)
|
|
||||||
parser.add_argument(
|
|
||||||
"--active-door",
|
|
||||||
choices=("front", "rear", "both", "none"),
|
|
||||||
default="both",
|
|
||||||
help="Door(s) whose CAN state is 0. Use front/rear for deployment simulation.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--door-zone-config",
|
|
||||||
type=Path,
|
|
||||||
default=PACKAGE_DIR / "annotations" / "door_zone_multifeature.json",
|
|
||||||
help="Pose-primary fixed-camera front/rear calibration JSON.",
|
|
||||||
)
|
|
||||||
parser.add_argument("--max-frames", type=int, default=0)
|
|
||||||
parser.add_argument("--write-video", action="store_true")
|
|
||||||
parser.add_argument("--progress-every", type=int, default=100)
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def load_pose_records(path: Path) -> dict[int, dict]:
|
|
||||||
records = {}
|
|
||||||
with path.open("r", encoding="utf-8") as handle:
|
|
||||||
for line in handle:
|
|
||||||
if not line.strip():
|
|
||||||
continue
|
|
||||||
record = json.loads(line)
|
|
||||||
records[int(record["frame_index"])] = record
|
|
||||||
return records
|
|
||||||
|
|
||||||
|
|
||||||
def percentile(values: list[float], p: float) -> float | None:
|
|
||||||
return round(float(np.percentile(values, p)), 3) if values else None
|
|
||||||
|
|
||||||
|
|
||||||
def timing_stats(values: list[float]) -> dict:
|
|
||||||
return {
|
|
||||||
"count": len(values),
|
|
||||||
"total": round(float(np.sum(values)), 3) if values else 0.0,
|
|
||||||
"mean": round(float(np.mean(values)), 3) if values else None,
|
|
||||||
"median": percentile(values, 50),
|
|
||||||
"p95": percentile(values, 95),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def visualization_skeletons(record: dict) -> list[dict]:
|
|
||||||
"""Return the exact fused keypoints and metadata consumed by draw_fused."""
|
|
||||||
|
|
||||||
boxes = record.get("boxes", [])
|
|
||||||
track_ids = [int(value) for value in record.get("track_ids", [])]
|
|
||||||
keypoints = record.get("temporal_selected_keypoints", [])
|
|
||||||
metadata = record.get("temporal_metadata", [])
|
|
||||||
if not (len(boxes) == len(keypoints) == len(metadata)):
|
|
||||||
return []
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"track_id": track_ids[position] if position < len(track_ids) else position + 1,
|
|
||||||
"person_index": position,
|
|
||||||
"layout": "coco_wholebody_133",
|
|
||||||
"keypoints": person_keypoints,
|
|
||||||
"metadata": metadata[position],
|
|
||||||
}
|
|
||||||
for position, person_keypoints in enumerate(keypoints)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def render_frame(
|
|
||||||
frame: np.ndarray,
|
|
||||||
record: dict,
|
|
||||||
frame_risk: dict,
|
|
||||||
door_mask: np.ndarray | None,
|
|
||||||
door_assignments: list[dict],
|
|
||||||
) -> np.ndarray:
|
|
||||||
canvas = frame.copy()
|
|
||||||
boxes = record.get("boxes", [])
|
|
||||||
keypoints = record.get("temporal_selected_keypoints", [])
|
|
||||||
metadata = record.get("temporal_metadata", [])
|
|
||||||
if len(boxes) == len(keypoints) == len(metadata) and boxes:
|
|
||||||
from fuse_rtmw_l_temporal import draw_fused
|
|
||||||
|
|
||||||
canvas = draw_fused(canvas, boxes, keypoints, metadata, "YOLO + RTMW risk")
|
|
||||||
else:
|
|
||||||
for position, box in enumerate(boxes):
|
|
||||||
x1, y1, x2, y2 = [int(round(float(value))) for value in box[:4]]
|
|
||||||
cv2.rectangle(canvas, (x1, y1), (x2, y2), (0, 220, 255), 2)
|
|
||||||
cv2.putText(
|
|
||||||
canvas,
|
|
||||||
f"P{position + 1}",
|
|
||||||
(x1 + 4, max(22, y1 + 24)),
|
|
||||||
cv2.FONT_HERSHEY_SIMPLEX,
|
|
||||||
0.7,
|
|
||||||
(0, 220, 255),
|
|
||||||
2,
|
|
||||||
cv2.LINE_AA,
|
|
||||||
)
|
|
||||||
zone_colors = {
|
|
||||||
"front": (0, 220, 255),
|
|
||||||
"rear": (255, 180, 0),
|
|
||||||
"transition": (0, 140, 255),
|
|
||||||
"unassigned": (150, 150, 150),
|
|
||||||
}
|
|
||||||
for assignment in door_assignments:
|
|
||||||
position = int(assignment["person_index"])
|
|
||||||
if not 0 <= position < len(boxes):
|
|
||||||
continue
|
|
||||||
box = boxes[position]
|
|
||||||
x1, y1 = int(round(float(box[0]))), int(round(float(box[1])))
|
|
||||||
zone = str(assignment["door_zone"])
|
|
||||||
cv2.putText(
|
|
||||||
canvas,
|
|
||||||
f"T{assignment['track_id']} {zone.upper()}",
|
|
||||||
(max(0, x1 + 4), max(24, y1 - 8)),
|
|
||||||
cv2.FONT_HERSHEY_SIMPLEX,
|
|
||||||
0.68,
|
|
||||||
zone_colors.get(zone, (160, 160, 160)),
|
|
||||||
2,
|
|
||||||
cv2.LINE_AA,
|
|
||||||
)
|
|
||||||
if door_mask is not None and np.any(door_mask):
|
|
||||||
overlay = np.zeros_like(canvas)
|
|
||||||
overlay[door_mask > 0] = (255, 180, 0)
|
|
||||||
canvas = cv2.addWeighted(canvas, 1.0, overlay, 0.22, 0)
|
|
||||||
|
|
||||||
score = frame_risk.get("risk_score")
|
|
||||||
score_text = "N/A" if score is None else f"{float(score):.3f}"
|
|
||||||
level = str(frame_risk.get("risk_level", "unavailable"))
|
|
||||||
color = (0, 0, 255) if level == "danger" else (0, 180, 255)
|
|
||||||
if level in ("clear", "unavailable", "inactive", "unknown"):
|
|
||||||
color = (40, 210, 80) if level == "clear" else (160, 160, 160)
|
|
||||||
cv2.rectangle(canvas, (12, 55), (490, 95), (0, 0, 0), -1)
|
|
||||||
cv2.putText(
|
|
||||||
canvas,
|
|
||||||
f"{frame_risk.get('risk_type', 'DOOR_RISK')} score={score_text} level={level}",
|
|
||||||
(24, 83),
|
|
||||||
cv2.FONT_HERSHEY_SIMPLEX,
|
|
||||||
0.68,
|
|
||||||
color,
|
|
||||||
2,
|
|
||||||
cv2.LINE_AA,
|
|
||||||
)
|
|
||||||
return canvas
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
args = parse_args()
|
|
||||||
args.output.mkdir(parents=True, exist_ok=True)
|
|
||||||
if args.pose_provider == "tensorrt" and not args.pose_device.lower().startswith("cuda"):
|
|
||||||
raise ValueError("--pose-provider tensorrt requires --pose-device cuda")
|
|
||||||
|
|
||||||
capture = cv2.VideoCapture(str(args.video.resolve()))
|
|
||||||
if not capture.isOpened():
|
|
||||||
raise RuntimeError(f"Cannot open video: {args.video}")
|
|
||||||
input_fps = float(capture.get(cv2.CAP_PROP_FPS)) or 30.0
|
|
||||||
width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
||||||
height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
||||||
|
|
||||||
risk_evaluator = DoorRiskEvaluator(
|
|
||||||
DoorMaskSequence(
|
|
||||||
args.door_mask_dir,
|
|
||||||
mask_format=args.door_mask_format,
|
|
||||||
labels=tuple(args.door_label or ("door", "车门")),
|
|
||||||
frame_offset=args.door_frame_offset,
|
|
||||||
require_every_frame=args.require_door_mask,
|
|
||||||
constant_mask=args.constant_door_mask,
|
|
||||||
),
|
|
||||||
warning_relative=args.risk_warning_relative,
|
|
||||||
release_relative=args.risk_release_relative,
|
|
||||||
)
|
|
||||||
zone_assigner = DoorZoneAssigner(DoorZoneConfig.load(args.door_zone_config))
|
|
||||||
print(
|
|
||||||
f"Door masks: {risk_evaluator.sequence.source_format} "
|
|
||||||
+ (
|
|
||||||
f"(constant: {risk_evaluator.sequence.constant_path})"
|
|
||||||
if risk_evaluator.sequence.constant_path is not None
|
|
||||||
else f"({len(risk_evaluator.sequence.paths)} indexed frames)"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
pose_records = load_pose_records(args.pose_jsonl) if args.pose_jsonl else None
|
|
||||||
load_started = time.perf_counter()
|
|
||||||
pose_engine = None if pose_records is not None else LivePoseEngine(args)
|
|
||||||
model_load_seconds = time.perf_counter() - load_started
|
|
||||||
|
|
||||||
writer = None
|
|
||||||
if args.write_video:
|
|
||||||
writer = cv2.VideoWriter(
|
|
||||||
str(args.output / "rtmw_risk_overlay.mp4"),
|
|
||||||
cv2.VideoWriter_fourcc(*"mp4v"),
|
|
||||||
input_fps,
|
|
||||||
(width, height),
|
|
||||||
)
|
|
||||||
|
|
||||||
frame_latencies = []
|
|
||||||
pose_latencies = []
|
|
||||||
risk_latencies = []
|
|
||||||
render_latencies = []
|
|
||||||
started_all = time.perf_counter()
|
|
||||||
frame_index = 0
|
|
||||||
output_path = args.output / "causal_results.jsonl"
|
|
||||||
with output_path.open("w", encoding="utf-8") as output_handle:
|
|
||||||
while True:
|
|
||||||
if args.max_frames > 0 and frame_index >= args.max_frames:
|
|
||||||
break
|
|
||||||
ok, frame = capture.read()
|
|
||||||
if not ok:
|
|
||||||
break
|
|
||||||
frame_started = time.perf_counter()
|
|
||||||
pose_started = time.perf_counter()
|
|
||||||
if pose_records is None:
|
|
||||||
record = pose_engine.infer(frame, frame_index)
|
|
||||||
else:
|
|
||||||
record = pose_records.get(frame_index)
|
|
||||||
if record is None:
|
|
||||||
raise RuntimeError(f"Pose JSONL has no record for frame {frame_index}")
|
|
||||||
pose_latencies.append((time.perf_counter() - pose_started) * 1000)
|
|
||||||
|
|
||||||
risk_started = time.perf_counter()
|
|
||||||
assignments = zone_assigner.update(
|
|
||||||
frame_index, record, (height, width)
|
|
||||||
)
|
|
||||||
raw_risk, door_mask = risk_evaluator.evaluate(
|
|
||||||
frame_index, record, (height, width)
|
|
||||||
)
|
|
||||||
door_risks, frame_risk = build_door_risks(
|
|
||||||
raw_risk, assignments, args.active_door
|
|
||||||
)
|
|
||||||
risk_latencies.append((time.perf_counter() - risk_started) * 1000)
|
|
||||||
output_record = {
|
|
||||||
"frame_index": frame_index,
|
|
||||||
"track_ids": record.get("track_ids", []),
|
|
||||||
"boxes": record.get("boxes", []),
|
|
||||||
"skeletons": visualization_skeletons(record),
|
|
||||||
"active_door": args.active_door,
|
|
||||||
"door_assignments": assignments,
|
|
||||||
"door_risks": door_risks,
|
|
||||||
"risk": frame_risk,
|
|
||||||
"ungated_risk": raw_risk,
|
|
||||||
}
|
|
||||||
output_handle.write(json.dumps(output_record, ensure_ascii=False) + "\n")
|
|
||||||
|
|
||||||
if writer is not None:
|
|
||||||
render_started = time.perf_counter()
|
|
||||||
writer.write(
|
|
||||||
render_frame(frame, record, frame_risk, door_mask, assignments)
|
|
||||||
)
|
|
||||||
render_latencies.append((time.perf_counter() - render_started) * 1000)
|
|
||||||
|
|
||||||
frame_latencies.append((time.perf_counter() - frame_started) * 1000)
|
|
||||||
if args.progress_every > 0 and frame_index % args.progress_every == 0:
|
|
||||||
recent = frame_latencies[-min(len(frame_latencies), args.progress_every) :]
|
|
||||||
print(
|
|
||||||
f"frame={frame_index} recent_ms={np.mean(recent):.1f} "
|
|
||||||
f"risk={frame_risk['risk_level']}",
|
|
||||||
flush=True,
|
|
||||||
)
|
|
||||||
frame_index += 1
|
|
||||||
|
|
||||||
capture.release()
|
|
||||||
if writer is not None:
|
|
||||||
writer.release()
|
|
||||||
elapsed = time.perf_counter() - started_all
|
|
||||||
summary = {
|
|
||||||
"mode": "yolo_rtmw_door_risk_multifeature_v9_10",
|
|
||||||
"video": str(args.video.resolve()),
|
|
||||||
"pose_source": "jsonl_diagnostic" if pose_records is not None else "resident_yolo_rtmw",
|
|
||||||
"models_loaded": [] if pose_records is not None else ["YOLO11s", "RTMW-L"],
|
|
||||||
"edgetam_loaded": False,
|
|
||||||
"risk_region_policy": (
|
|
||||||
"RTMW alarm-eligible hand hulls; YOLO body box only when a person "
|
|
||||||
"has no usable RTMW hand region"
|
|
||||||
),
|
|
||||||
"frames": frame_index,
|
|
||||||
"input_fps": round(input_fps, 3),
|
|
||||||
"elapsed_seconds": round(elapsed, 3),
|
|
||||||
"throughput_fps": round(frame_index / elapsed, 3) if elapsed else None,
|
|
||||||
"model_load_seconds": round(model_load_seconds, 3),
|
|
||||||
"latency_ms": {
|
|
||||||
"total": timing_stats(frame_latencies),
|
|
||||||
"pose": timing_stats(pose_latencies),
|
|
||||||
"risk": timing_stats(risk_latencies),
|
|
||||||
"render": timing_stats(render_latencies),
|
|
||||||
},
|
|
||||||
"write_video": args.write_video,
|
|
||||||
"active_door": args.active_door,
|
|
||||||
"door_zone": zone_assigner.summary(),
|
|
||||||
"door_risk": risk_evaluator.summary(),
|
|
||||||
}
|
|
||||||
if pose_engine is not None:
|
|
||||||
summary["pose"] = {
|
|
||||||
"provider": args.pose_provider,
|
|
||||||
"isolated_inferences": pose_engine.isolated_inferences,
|
|
||||||
"timing_ms": {
|
|
||||||
key: timing_stats(values) for key, values in pose_engine.timings.items()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
(args.output / "summary.json").write_text(
|
|
||||||
json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
||||||
)
|
|
||||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
"""Runtime visualization helpers."""
|
||||||
|
from __future__ import annotations
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def percentile(values: list[float], p: float) -> float | None:
|
||||||
|
return round(float(np.percentile(values, p)), 3) if values else None
|
||||||
|
|
||||||
|
|
||||||
|
def timing_stats(values: list[float]) -> dict:
|
||||||
|
return {
|
||||||
|
"count": len(values),
|
||||||
|
"total": round(float(np.sum(values)), 3) if values else 0.0,
|
||||||
|
"mean": round(float(np.mean(values)), 3) if values else None,
|
||||||
|
"median": percentile(values, 50),
|
||||||
|
"p95": percentile(values, 95),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def visualization_skeletons(record: dict) -> list[dict]:
|
||||||
|
"""Return the exact fused keypoints and metadata consumed by draw_fused."""
|
||||||
|
|
||||||
|
boxes = record.get("boxes", [])
|
||||||
|
track_ids = [int(value) for value in record.get("track_ids", [])]
|
||||||
|
keypoints = record.get("temporal_selected_keypoints", [])
|
||||||
|
metadata = record.get("temporal_metadata", [])
|
||||||
|
if not (len(boxes) == len(keypoints) == len(metadata)):
|
||||||
|
return []
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"track_id": track_ids[position] if position < len(track_ids) else position + 1,
|
||||||
|
"person_index": position,
|
||||||
|
"layout": "coco_wholebody_133",
|
||||||
|
"keypoints": person_keypoints,
|
||||||
|
"metadata": metadata[position],
|
||||||
|
}
|
||||||
|
for position, person_keypoints in enumerate(keypoints)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def render_frame(
|
||||||
|
frame: np.ndarray,
|
||||||
|
record: dict,
|
||||||
|
frame_risk: dict,
|
||||||
|
door_mask: np.ndarray | None,
|
||||||
|
door_assignments: list[dict],
|
||||||
|
) -> np.ndarray:
|
||||||
|
canvas = frame.copy()
|
||||||
|
boxes = record.get("boxes", [])
|
||||||
|
keypoints = record.get("temporal_selected_keypoints", [])
|
||||||
|
metadata = record.get("temporal_metadata", [])
|
||||||
|
if len(boxes) == len(keypoints) == len(metadata) and boxes:
|
||||||
|
from pose_fusion import draw_fused
|
||||||
|
|
||||||
|
canvas = draw_fused(canvas, boxes, keypoints, metadata, "YOLO + RTMW risk")
|
||||||
|
else:
|
||||||
|
for position, box in enumerate(boxes):
|
||||||
|
x1, y1, x2, y2 = [int(round(float(value))) for value in box[:4]]
|
||||||
|
cv2.rectangle(canvas, (x1, y1), (x2, y2), (0, 220, 255), 2)
|
||||||
|
cv2.putText(
|
||||||
|
canvas,
|
||||||
|
f"P{position + 1}",
|
||||||
|
(x1 + 4, max(22, y1 + 24)),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX,
|
||||||
|
0.7,
|
||||||
|
(0, 220, 255),
|
||||||
|
2,
|
||||||
|
cv2.LINE_AA,
|
||||||
|
)
|
||||||
|
zone_colors = {
|
||||||
|
"front": (0, 220, 255),
|
||||||
|
"rear": (255, 180, 0),
|
||||||
|
"transition": (0, 140, 255),
|
||||||
|
"unassigned": (150, 150, 150),
|
||||||
|
}
|
||||||
|
for assignment in door_assignments:
|
||||||
|
position = int(assignment["person_index"])
|
||||||
|
if not 0 <= position < len(boxes):
|
||||||
|
continue
|
||||||
|
box = boxes[position]
|
||||||
|
x1, y1 = int(round(float(box[0]))), int(round(float(box[1])))
|
||||||
|
zone = str(assignment["door_zone"])
|
||||||
|
cv2.putText(
|
||||||
|
canvas,
|
||||||
|
f"T{assignment['track_id']} {zone.upper()}",
|
||||||
|
(max(0, x1 + 4), max(24, y1 - 8)),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX,
|
||||||
|
0.68,
|
||||||
|
zone_colors.get(zone, (160, 160, 160)),
|
||||||
|
2,
|
||||||
|
cv2.LINE_AA,
|
||||||
|
)
|
||||||
|
if door_mask is not None and np.any(door_mask):
|
||||||
|
overlay = np.zeros_like(canvas)
|
||||||
|
overlay[door_mask > 0] = (255, 180, 0)
|
||||||
|
canvas = cv2.addWeighted(canvas, 1.0, overlay, 0.22, 0)
|
||||||
|
|
||||||
|
score = frame_risk.get("risk_score")
|
||||||
|
score_text = "N/A" if score is None else f"{float(score):.3f}"
|
||||||
|
level = str(frame_risk.get("risk_level", "unavailable"))
|
||||||
|
color = (0, 0, 255) if level == "danger" else (0, 180, 255)
|
||||||
|
if level in ("clear", "unavailable", "inactive", "unknown"):
|
||||||
|
color = (40, 210, 80) if level == "clear" else (160, 160, 160)
|
||||||
|
cv2.rectangle(canvas, (12, 55), (490, 95), (0, 0, 0), -1)
|
||||||
|
cv2.putText(
|
||||||
|
canvas,
|
||||||
|
f"{frame_risk.get('risk_type', 'DOOR_RISK')} score={score_text} level={level}",
|
||||||
|
(24, 83),
|
||||||
|
cv2.FONT_HERSHEY_SIMPLEX,
|
||||||
|
0.68,
|
||||||
|
color,
|
||||||
|
2,
|
||||||
|
cv2.LINE_AA,
|
||||||
|
)
|
||||||
|
return canvas
|
||||||
+4
-3
@@ -16,11 +16,12 @@ import cv2
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
ROOT=Path(__file__).resolve().parent
|
ROOT=Path(__file__).resolve().parent
|
||||||
sys.path.insert(0,str(ROOT/'pipeline'))
|
sys.path.insert(0,str(ROOT/'pipeline'))
|
||||||
from native_backend import NativeBackend, NativePoseEngine
|
from native_backend import NativeBackend
|
||||||
|
from pose_engine import NativePoseEngine
|
||||||
from latest_result import LatestResult, result_to_dict
|
from latest_result import LatestResult, result_to_dict
|
||||||
from door_mask_risk import DoorMaskSequence, DoorRiskEvaluator
|
from door_mask_risk import DoorMaskSequence, DoorRiskEvaluator
|
||||||
from door_zone_risk import DoorZoneConfig, DoorZoneAssigner, build_door_risks
|
from door_zone_risk import DoorZoneConfig, DoorZoneAssigner, build_door_risks
|
||||||
from rtmw_risk_pipeline import render_frame, visualization_skeletons, timing_stats
|
from visualization import render_frame, visualization_skeletons, timing_stats
|
||||||
|
|
||||||
|
|
||||||
def select_active_door(mode, snapshot, side='left', timeout_ms=1000):
|
def select_active_door(mode, snapshot, side='left', timeout_ms=1000):
|
||||||
@@ -121,7 +122,7 @@ class Preview:
|
|||||||
def parse_args(argv=None):
|
def parse_args(argv=None):
|
||||||
p=argparse.ArgumentParser(description=__doc__)
|
p=argparse.ArgumentParser(description=__doc__)
|
||||||
p.add_argument('--video',type=Path,help='Diagnostic video replay through the same TensorRT/risk chain');p.add_argument('--camera',default='/dev/video5');p.add_argument('--width',type=int,default=1920);p.add_argument('--height',type=int,default=1536);p.add_argument('--fps',type=int,default=30)
|
p.add_argument('--video',type=Path,help='Diagnostic video replay through the same TensorRT/risk chain');p.add_argument('--camera',default='/dev/video5');p.add_argument('--width',type=int,default=1920);p.add_argument('--height',type=int,default=1536);p.add_argument('--fps',type=int,default=30)
|
||||||
p.add_argument('--engines',type=Path,default=ROOT/'models/engines')
|
p.add_argument('--engines',type=Path,default=ROOT/'orin/models/engines' if (ROOT/'orin').is_dir() else ROOT/'models/engines')
|
||||||
p.add_argument('--constant-door-mask',type=Path,default=ROOT/'annotations/constant_door_mask.json')
|
p.add_argument('--constant-door-mask',type=Path,default=ROOT/'annotations/constant_door_mask.json')
|
||||||
p.add_argument('--door-zone-config',type=Path,default=ROOT/'annotations/door_zone_multifeature.json')
|
p.add_argument('--door-zone-config',type=Path,default=ROOT/'annotations/door_zone_multifeature.json')
|
||||||
p.add_argument('--active-door',choices=['can','front','rear','both','none'],default='can')
|
p.add_argument('--active-door',choices=['can','front','rear','both','none'],default='can')
|
||||||
|
|||||||
@@ -2,4 +2,5 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
root="$(cd "$(dirname "$0")" && pwd)"
|
root="$(cd "$(dirname "$0")" && pwd)"
|
||||||
export OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=2
|
export OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=2
|
||||||
|
export LD_LIBRARY_PATH="$root/orin/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||||
exec "${PYTHON:-python3}" -u "$root/run_camera.py" "$@"
|
exec "${PYTHON:-python3}" -u "$root/run_camera.py" "$@"
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
"""0923 entry point: native TensorRT camera/video inference and CMS v9.10 risk."""
|
|
||||||
from run_camera import main
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user