# PRD_05:Device 侧轻量级 Tracker 节点(可插拔) ## 1. 背景与问题 当前 Device 侧媒体服务采用“配置驱动 DAG(Graph)+ 插件节点(Node)+ SPSC 队列”的流水线架构:输入(RTSP/文件)→ 预处理 → AI 推理(检测/识别等)→ OSD/推流/告警。 在实际部署中常见三类痛点: 1) **重复报警**:同一目标在画面连续出现时,规则可能在采样频率下重复触发。 2) **重复上传(MinIO/HTTP 等)**:报警动作触发后在短时间内重复上传同类素材。 3) **推理开销过高**:虽然已有 `infer_fps` 等限频,但在低推理频率下希望仍保持事件稳定性与可解释性。 现有代码中: - `Frame::det`(`DetectionResult`)包含 `Detection{cls_id, score, bbox, track_id}`,其中 `track_id` 当前默认 `-1`(未做追踪)。 - `alarm` 侧已有 `rules.cooldown_ms`、`min_duration_ms` 与 `actions.*.min_interval_ms` 的时间窗限流,但属于“按规则/动作的时间窗”粒度。 ## 2. 目标(Goals) 提供一个 **可选、可插拔** 的 `tracker` 插件节点,用于为检测结果补充稳定的 `track_id`,以支持: 1) **按目标实例去重**(基于 `track_id`):同一目标在持续存在期间不重复触发/上传。 2) **稳定性/精准性优先,资源占用尽可能低**:不引入 ReID/特征网络,主要使用几何与置信度关联。 3) **按类别选择性追踪**:并非所有类别都追踪,可由配置指定哪些 `cls_id` 参与追踪。 4) **架构适配**:完全符合现有 Graph/Node 插件机制,插入与否由 `configs/*.json` 决定;不插入则不改变任何行为。 ## 3. 非目标(Non-Goals) 1) 不做跨摄像头/跨 Graph 的关联(不做全局 ID)。 2) 不做基于外观特征(ReID)的强一致性追踪(如 DeepSORT+ReID)。 3) 不保证对极端遮挡/快速运动/密集重叠场景达到 SOTA 跟踪指标;本 PRD 目标是“工程可用+低资源”。 ## 4. 现有架构约束(必须遵守) 1) Node 单输入、多输出:Graph 当前限制每个节点 **只能有一个 input queue**。 2) Node 的 `Process(FramePtr)` 由框架线程调用(`Graph::Start()` 中的 worker loop),Tracker 节点不应引入额外线程(除非明确需求)。 3) Tracker 节点不得破坏 `Frame` 上其他元信息(尤其是 `publish` 节点可能写入 `frame->user_meta` 用于 clip)。 ## 5. 功能设计 ### 5.1 节点定义 - Node type:`tracker` - role:`filter` - 输入:`FramePtr`(读取 `frame->det`) - 输出:同一个 `FramePtr`(仅可能修改 `frame->det->items[*].track_id`) - 当 `frame->det == nullptr` 或 `items` 为空:直接透传。 ### 5.2 追踪算法(推荐:ByteTrack-lite + IOU 关联) 在不引入外观特征的前提下,为提高稳定性,采用“两段式关联”策略: **核心思想**: 1) 将 det 按置信度分成高置信(`score >= high_th`)与低置信(`low_th <= score < high_th`)。 2) 优先用高置信 det 去匹配已有 tracks;未匹配的 tracks 再用低置信 det 做二次匹配(减少断轨)。 3) 匹配度量使用 IOU(可加入 gating:中心点距离/面积比等轻量约束)。 **匹配策略**: - 为降低资源占用:默认使用 greedy matching(按 IOU 从高到低选择不冲突匹配),不强制使用匈牙利算法。 - 允许后续通过配置切换到 Hungarian(可选增强项),但不作为本 PRD 必需。 **Track 生命周期**: - 创建:未匹配到现有 track 的高置信 det → 创建新 track。 - 更新:匹配成功 → 更新 bbox、last_seen、hit_streak。 - 丢失:超过 `max_age_ms`(或 `max_age_frames`)未匹配 → 删除。 **分类策略**: - 默认按 `cls_id` 分组匹配(避免不同类别互相抢 track)。 - 可配置 `per_class=false` 以允许跨类别匹配(默认不建议)。 ### 5.3 按类别选择性追踪 支持以下配置(至少实现其一,推荐两者都实现): - `track_classes`: `int[]`:白名单。非空时仅追踪这些 `cls_id`。 - `ignore_classes`: `int[]`:黑名单。用于排除某些 `cls_id`。 规则: - 若 `track_classes` 非空,则优先按白名单过滤。 - 否则按 `ignore_classes` 排除。 - 未参与追踪的 det 必须保持 `track_id = -1`。 ### 5.4 按模型类型/推理类型启用(可选,但推荐) `DetectionResult.model_name` 已存在,Tracker 支持: - `allowed_models`: `string[]`:当非空时,仅对 `model_name` 在集合内的帧启用。 用途:同一 pipeline 可能挂多种推理节点(yolo/face_det/自定义模型),避免对不需要的输出做追踪。 ### 5.5 插入位置建议(不强制) 1) 仅为“告警去重/上传去重”服务: - 推荐 `publish -> tracker -> alarm` - 原因:不影响推流;不破坏 `publish` 写入的 `frame->user_meta`(clip 仍可用)。 2) 需要 OSD 展示稳定 ID: - 推荐 `ai_* -> tracker -> osd -> ...` ## 6. 配置(Config Schema) ### 6.1 tracker 节点配置字段 节点 JSON 示例: ```json { "id": "trk_cam1", "type": "tracker", "role": "filter", "enable": true, "mode": "bytetrack_lite", "per_class": true, "track_classes": [0, 2, 3], "ignore_classes": [], "allowed_models": ["yolov5", "yolov8"], "high_th": 0.5, "low_th": 0.1, "iou_th": 0.3, "max_age_ms": 1500, "min_hits": 2, "max_tracks": 128, "debug": { "stats": true, "stats_interval": 200 } } ``` 字段说明: - `mode`:`"off" | "bytetrack_lite"`(最少实现这两种) - `per_class`:默认 `true`。 - `track_classes`:白名单;默认空(表示全类别)。 - `ignore_classes`:黑名单;默认空。 - `allowed_models`:默认空(表示不过滤)。 - `high_th/low_th`:置信度分段阈值。 - `iou_th`:匹配阈值。 - `max_age_ms`:track 允许丢失的时间窗口(基于 `frame->pts` 计算;若 pts 无效则可退化为帧计数)。 - `min_hits`:一个 track 连续命中次数达到后才“稳定输出”(用于减少误检带来的短暂 track)。 - `max_tracks`:上限保护,避免极端场景内存增长。 ### 6.2 Graph 接入示例 在现有 graph 中插入(示例:`pub -> tracker -> alarm`): ```json "nodes": [ {"id":"pub_cam1","type":"publish","role":"filter","enable":true, ...}, {"id":"trk_cam1","type":"tracker","role":"filter","enable":true, ...}, {"id":"alarm_cam1","type":"alarm","role":"sink","enable":true, ...} ], "edges": [ ["post_cam1","pub_cam1"], ["pub_cam1","trk_cam1"], ["trk_cam1","alarm_cam1"] ] ``` ## 7. 对外接口与数据契约 ### 7.1 输入输出契约 - 输入:`frame->det` 必须由上游检测节点填充;Tracker 不负责生成 det。 - 输出: - `frame` 本体不变; - `frame->det->items[*].track_id` 可能从 `-1` 变为 `>=0`; - 不修改 `frame->user_meta`、不修改 `frame->data/planes`。 ### 7.2 与 alarm/http/minio 的配合 - `plugins/alarm/actions/http_action.cpp` 已会输出 `track_id` 字段(现有实现),因此 Tracker 生效后 HTTP 报警可以自然携带 `track_id`。 - MinIO 上传去重当前依赖 `actions.*.min_interval_ms`;后续可扩展为 per-track 去重(见第 10 节增强建议)。 ## 8. 指标与可观测性 ### 8.1 Custom Metrics Tracker 节点建议实现 `GetCustomMetrics()` 输出: - `tracks_active` - `tracks_created_total` - `tracks_removed_total` - `matched_total` - `unmatched_dets_total` - `avg_process_time_ms` ### 8.2 Debug - `debug.stats`:周期打印关键统计。 - 不输出过多 per-frame 日志,避免影响实时性。 ## 9. 性能与资源约束 目标(以单路为单位,具体需实测校准): - `N_det <= 64` 时,每帧 Tracker 处理耗时应为毫秒级以下(通常 < 1ms,依平台不同)。 - 内存:track 状态结构固定上限 `max_tracks`,避免无界增长。 ## 10. 验收标准(Acceptance Criteria) 1) **可插拔**:不配置 `tracker` 节点时,系统行为与当前版本一致。 2) **正确写入 track_id**:配置启用时,目标类别的 det `track_id` 在连续帧中稳定且可复用。 3) **按类别控制有效**:不在白名单/在黑名单的 `cls_id` 必须保持 `track_id=-1`。 4) **稳定性**:短暂漏检场景下(`max_age_ms` 内),track 不应频繁抖动创建新 ID。 5) **资源可控**:`max_tracks` 生效;极端场景不会导致内存持续增长。 ## 11. 开发拆解(实现步骤) 1) 新增插件目录与编译接入:`plugins/tracker/`,更新 `plugins/CMakeLists.txt` 注册编译。 2) 实现 `TrackerNode`: - `Init()` 解析配置。 - `Process()`:过滤模型/类别 → 运行 ByteTrack-lite → 写 `track_id` → 推送下游。 - `UpdateConfig()`:支持热更新(可退化为“关键字段变更则拒绝原地更新”)。 - `GetCustomMetrics()`:输出统计。 3) 增加一个示例配置(不强制提交到 README):可在现有 `configs/*` 中添加一份测试 json(若产品流程允许)。 ## 12. 增强建议(不属于本 PRD 必做,但推荐路线) 1) **Alarm 去重升级为 per-track**:在 `alarm` 节点内部维护 `(rule_name, track_id)` 的冷却/状态机(enter/stay/leave)。 2) **自适应推理频率**:结合最近 N 秒的命中情况动态调整 `infer_fps`(需与现有热更新机制配合)。 3) **Hungarian 可选开关**:当 det 数量大且遮挡多时提升一致性,但要评估 CPU 成本。