From 9c59c5d310c7e4bea7ec2d02fdb587fea331eba5 Mon Sep 17 00:00:00 2001 From: sladro Date: Sun, 11 Jan 2026 20:15:27 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=80=E5=8F=91=E8=B7=9F=E8=B8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PRD_05_Device_Tracker_Node.md | 192 +++++++++ plugins/CMakeLists.txt | 12 +- plugins/tracker/tracker_node.cpp | 676 +++++++++++++++++++++++++++++++ 命令.md | 61 ++- 4 files changed, 939 insertions(+), 2 deletions(-) create mode 100644 PRD_05_Device_Tracker_Node.md create mode 100644 plugins/tracker/tracker_node.cpp diff --git a/PRD_05_Device_Tracker_Node.md b/PRD_05_Device_Tracker_Node.md new file mode 100644 index 0000000..d72c914 --- /dev/null +++ b/PRD_05_Device_Tracker_Node.md @@ -0,0 +1,192 @@ +# 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 成本。 diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 841f649..182e57b 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -279,6 +279,16 @@ set_target_properties(det_post PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${RK_PLUGIN_OUTPUT_DIR} ) +# tracker plugin (ByteTrack-lite style IOU association) +add_library(tracker SHARED tracker/tracker_node.cpp) +target_include_directories(tracker PRIVATE ${CMAKE_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/third_party) +target_link_libraries(tracker PRIVATE project_options Threads::Threads) +set_target_properties(tracker PROPERTIES + OUTPUT_NAME "tracker" + LIBRARY_OUTPUT_DIRECTORY ${RK_PLUGIN_OUTPUT_DIR} + RUNTIME_OUTPUT_DIRECTORY ${RK_PLUGIN_OUTPUT_DIR} +) + # osd plugin (on-screen display for detection results) add_library(osd SHARED osd/osd_node.cpp) target_include_directories(osd PRIVATE ${CMAKE_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/third_party) @@ -388,7 +398,7 @@ if(RK3588_ENABLE_ZLMEDIAKIT AND RK_ZLMK_API_LIB) ) endif() -install(TARGETS input_rtsp input_file publish preprocess ai_yolo ai_face_det ai_face_recog osd alarm storage ai_scheduler +install(TARGETS input_rtsp input_file publish preprocess ai_yolo ai_face_det ai_face_recog tracker osd alarm storage ai_scheduler LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/rk3588-media-server/plugins RUNTIME DESTINATION ${CMAKE_INSTALL_LIBDIR}/rk3588-media-server/plugins ) diff --git a/plugins/tracker/tracker_node.cpp b/plugins/tracker/tracker_node.cpp new file mode 100644 index 0000000..2102892 --- /dev/null +++ b/plugins/tracker/tracker_node.cpp @@ -0,0 +1,676 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "node.h" +#include "utils/logger.h" + +namespace rk3588 { +namespace { + +using Clock = std::chrono::steady_clock; + +static inline uint64_t NowUsSteady() { + return static_cast( + std::chrono::duration_cast(Clock::now().time_since_epoch()).count()); +} + +static inline float Area(const Rect& r) { + const float w = std::max(0.0f, r.w); + const float h = std::max(0.0f, r.h); + return w * h; +} + +static inline float IoU(const Rect& a, const Rect& b) { + const float ax1 = a.x; + const float ay1 = a.y; + const float ax2 = a.x + a.w; + const float ay2 = a.y + a.h; + + const float bx1 = b.x; + const float by1 = b.y; + const float bx2 = b.x + b.w; + const float by2 = b.y + b.h; + + const float ix1 = std::max(ax1, bx1); + const float iy1 = std::max(ay1, by1); + const float ix2 = std::min(ax2, bx2); + const float iy2 = std::min(ay2, by2); + + const float iw = std::max(0.0f, ix2 - ix1); + const float ih = std::max(0.0f, iy2 - iy1); + const float inter = iw * ih; + if (inter <= 0.0f) return 0.0f; + + const float ua = std::max(0.0f, Area(a)); + const float ub = std::max(0.0f, Area(b)); + const float uni = ua + ub - inter; + if (uni <= 0.0f) return 0.0f; + return inter / uni; +} + +struct ConfigSnapshot { + std::string id; + std::string mode; // off | bytetrack_lite + bool per_class = true; + + std::set track_classes; // whitelist + std::set ignore_classes; // blacklist + std::unordered_set allowed_models; + + float high_th = 0.5f; + float low_th = 0.1f; + float iou_th = 0.3f; + + int64_t max_age_ms = 1500; + int min_hits = 2; + int max_tracks = 128; + + bool stats_log = false; + int64_t stats_interval_ms = 200; +}; + +static bool ParseIntSet(const SimpleJson& arr, std::set& out, std::string& err) { + if (!arr.IsArray()) { + err = "expected array"; + return false; + } + out.clear(); + for (const auto& it : arr.AsArray()) { + const int v = it.AsInt(-1); + if (v < 0) continue; + out.insert(v); + } + return true; +} + +static bool ParseStrSet(const SimpleJson& arr, std::unordered_set& out, std::string& err) { + if (!arr.IsArray()) { + err = "expected array"; + return false; + } + out.clear(); + for (const auto& it : arr.AsArray()) { + const std::string s = it.AsString(""); + if (!s.empty()) out.insert(s); + } + return true; +} + +static bool BuildConfigSnapshot(const SimpleJson& config, std::shared_ptr& out, + std::string& err) { + auto snap = std::make_shared(); + snap->id = config.ValueOr("id", "tracker"); + snap->mode = config.ValueOr("mode", "bytetrack_lite"); + snap->per_class = config.ValueOr("per_class", true); + + if (const SimpleJson* tc = config.Find("track_classes")) { + if (!ParseIntSet(*tc, snap->track_classes, err)) { + err = "track_classes: " + err; + return false; + } + } + if (const SimpleJson* ic = config.Find("ignore_classes")) { + if (!ParseIntSet(*ic, snap->ignore_classes, err)) { + err = "ignore_classes: " + err; + return false; + } + } + if (const SimpleJson* am = config.Find("allowed_models")) { + if (!ParseStrSet(*am, snap->allowed_models, err)) { + err = "allowed_models: " + err; + return false; + } + } + + snap->high_th = config.ValueOr("high_th", 0.5f); + snap->low_th = config.ValueOr("low_th", 0.1f); + snap->iou_th = config.ValueOr("iou_th", 0.3f); + + snap->max_age_ms = static_cast(config.ValueOr("max_age_ms", 1500)); + snap->min_hits = config.ValueOr("min_hits", 2); + snap->max_tracks = config.ValueOr("max_tracks", 128); + + if (snap->high_th < 0.0f || snap->high_th > 1.0f) { + err = "high_th must be in [0,1]"; + return false; + } + if (snap->low_th < 0.0f || snap->low_th > 1.0f) { + err = "low_th must be in [0,1]"; + return false; + } + if (snap->low_th > snap->high_th) { + err = "low_th must be <= high_th"; + return false; + } + if (snap->iou_th < 0.0f || snap->iou_th > 1.0f) { + err = "iou_th must be in [0,1]"; + return false; + } + if (snap->max_age_ms < 0) snap->max_age_ms = 0; + if (snap->min_hits < 1) snap->min_hits = 1; + if (snap->max_tracks < 1) snap->max_tracks = 1; + + if (const SimpleJson* dbg = config.Find("debug")) { + if (!dbg->IsObject()) { + err = "debug must be object"; + return false; + } + snap->stats_log = dbg->ValueOr("stats", false); + snap->stats_interval_ms = static_cast(dbg->ValueOr("stats_interval", 200)); + if (snap->stats_interval_ms < 1) snap->stats_interval_ms = 1; + } + + if (snap->mode != "off" && snap->mode != "bytetrack_lite") { + err = "mode must be 'off' or 'bytetrack_lite'"; + return false; + } + + out = std::move(snap); + return true; +} + +struct Track { + int id = -1; + int cls_id = -1; + Rect bbox{}; + + uint64_t last_seen_us = 0; + uint64_t last_seen_frame_id = 0; + + int hit_streak = 0; + int total_hits = 0; + bool confirmed = false; +}; + +struct MatchPair { + float iou = 0.0f; + int track_idx = -1; + int det_idx = -1; +}; + +static void GreedyMatch(const std::vector& tracks, const std::vector& track_indices, + const std::vector& dets, const std::vector& det_indices, float iou_th, + std::vector>& out_matches, std::vector& out_unmatched_tracks, + std::vector& out_unmatched_dets) { + out_matches.clear(); + out_unmatched_tracks = track_indices; + out_unmatched_dets = det_indices; + if (track_indices.empty() || det_indices.empty()) return; + + std::vector pairs; + pairs.reserve(track_indices.size() * det_indices.size()); + for (int ti : track_indices) { + const auto& trk = tracks[static_cast(ti)]; + for (int di : det_indices) { + const float iou = IoU(trk.bbox, dets[static_cast(di)].bbox); + if (iou >= iou_th) { + pairs.push_back(MatchPair{iou, ti, di}); + } + } + } + if (pairs.empty()) return; + + std::sort(pairs.begin(), pairs.end(), [](const MatchPair& a, const MatchPair& b) { + if (a.iou != b.iou) return a.iou > b.iou; + if (a.track_idx != b.track_idx) return a.track_idx < b.track_idx; + return a.det_idx < b.det_idx; + }); + + std::unordered_set used_tracks; + std::unordered_set used_dets; + used_tracks.reserve(track_indices.size()); + used_dets.reserve(det_indices.size()); + + for (const auto& p : pairs) { + if (used_tracks.count(p.track_idx) || used_dets.count(p.det_idx)) continue; + used_tracks.insert(p.track_idx); + used_dets.insert(p.det_idx); + out_matches.emplace_back(p.track_idx, p.det_idx); + } + + out_unmatched_tracks.clear(); + out_unmatched_tracks.reserve(track_indices.size()); + for (int ti : track_indices) { + if (!used_tracks.count(ti)) out_unmatched_tracks.push_back(ti); + } + out_unmatched_dets.clear(); + out_unmatched_dets.reserve(det_indices.size()); + for (int di : det_indices) { + if (!used_dets.count(di)) out_unmatched_dets.push_back(di); + } +} + +} // namespace + +class TrackerNode final : public INode { +public: + std::string Id() const override { return id_; } + std::string Type() const override { return "tracker"; } + + bool Init(const SimpleJson& config, const NodeContext& ctx) override { + std::shared_ptr snap; + std::string err; + if (!BuildConfigSnapshot(config, snap, err)) { + LogError("[tracker] invalid config: " + err); + return false; + } + id_ = snap->id; + + input_queue_ = ctx.input_queue; + if (!input_queue_) { + LogError("[tracker] no input queue for node " + id_); + return false; + } + if (ctx.output_queues.empty()) { + LogError("[tracker] no output queue for node " + id_); + return false; + } + output_queues_ = ctx.output_queues; + + { + std::lock_guard lk(mu_); + cfg_ = std::move(snap); + tracks_.clear(); + next_track_id_ = 0; + last_stats_us_ = 0; + } + return true; + } + + bool Start() override { + LogInfo("[tracker] started id=" + id_); + return true; + } + + void Stop() override { + LogInfo("[tracker] stopped id=" + id_); + } + + bool UpdateConfig(const SimpleJson& new_config) override { + std::shared_ptr snap; + std::string err; + if (!BuildConfigSnapshot(new_config, snap, err)) { + LogWarn("[tracker] UpdateConfig rejected: " + err); + return false; + } + if (!id_.empty() && !snap->id.empty() && snap->id != id_) { + return false; + } + + bool should_reset = false; + { + std::lock_guard lk(mu_); + if (cfg_) { + should_reset = (cfg_->mode != snap->mode) || (cfg_->per_class != snap->per_class) || + (cfg_->track_classes != snap->track_classes) || + (cfg_->ignore_classes != snap->ignore_classes) || + (cfg_->allowed_models != snap->allowed_models) || (cfg_->high_th != snap->high_th) || + (cfg_->low_th != snap->low_th) || (cfg_->iou_th != snap->iou_th) || + (cfg_->max_age_ms != snap->max_age_ms) || (cfg_->min_hits != snap->min_hits) || + (cfg_->max_tracks != snap->max_tracks); + } + cfg_ = std::move(snap); + if (should_reset) { + tracks_.clear(); + } + } + return true; + } + + bool GetCustomMetrics(SimpleJson& out) const override { + SimpleJson::Object o; + o["tracks_active"] = SimpleJson(static_cast(tracks_active_.load())); + o["tracks_created_total"] = SimpleJson(static_cast(tracks_created_total_.load())); + o["tracks_removed_total"] = SimpleJson(static_cast(tracks_removed_total_.load())); + o["matched_total"] = SimpleJson(static_cast(matched_total_.load())); + o["unmatched_dets_total"] = SimpleJson(static_cast(unmatched_dets_total_.load())); + + const uint64_t frames = processed_frames_.load(); + const uint64_t total_us = total_process_us_.load(); + const double avg_ms = frames > 0 ? (static_cast(total_us) / 1000.0 / static_cast(frames)) : 0.0; + o["avg_process_time_ms"] = SimpleJson(avg_ms); + + out = SimpleJson(std::move(o)); + return true; + } + + NodeStatus Process(FramePtr frame) override { + if (!frame) return NodeStatus::DROP; + + const uint64_t t0 = NowUsSteady(); + + std::shared_ptr cfg; + { + std::lock_guard lk(mu_); + cfg = cfg_; + } + if (!cfg || cfg->mode == "off") { + PushToDownstream(frame); + return NodeStatus::OK; + } + + uint64_t now_us = 0; + uint64_t removed_prune = 0; + { + std::lock_guard lk(mu_); + now_us = ResolveNowUsLocked(*frame); + removed_prune = PruneExpiredLocked(now_us, *cfg); + } + if (removed_prune) tracks_removed_total_.fetch_add(removed_prune); + + if (!frame->det || frame->det->items.empty()) { + MaybeLogStats(now_us, *cfg); + PushToDownstream(frame); + total_process_us_.fetch_add(NowUsSteady() - t0); + processed_frames_.fetch_add(1); + return NodeStatus::OK; + } + + if (!cfg->allowed_models.empty()) { + if (cfg->allowed_models.find(frame->det->model_name) == cfg->allowed_models.end()) { + // Not an enabled model: pass-through (do not rewrite track_id). + MaybeLogStats(now_us, *cfg); + PushToDownstream(frame); + total_process_us_.fetch_add(NowUsSteady() - t0); + processed_frames_.fetch_add(1); + return NodeStatus::OK; + } + } + + auto& dets = frame->det->items; + + // Collect det indices. + std::vector high_dets; + std::vector low_dets; + high_dets.reserve(dets.size()); + low_dets.reserve(dets.size()); + + for (size_t i = 0; i < dets.size(); ++i) { + auto& d = dets[i]; + d.track_id = -1; + if (!IsTrackClass(*cfg, d.cls_id)) { + d.track_id = -1; + continue; + } + if (d.score >= cfg->high_th) { + high_dets.push_back(static_cast(i)); + } else if (d.score >= cfg->low_th) { + low_dets.push_back(static_cast(i)); + } + } + + if (high_dets.empty() && low_dets.empty()) { + MaybeLogStats(now_us, *cfg); + PushToDownstream(frame); + total_process_us_.fetch_add(NowUsSteady() - t0); + processed_frames_.fetch_add(1); + return NodeStatus::OK; + } + + uint64_t matched_local = 0; + uint64_t unmatched_dets_local = 0; + uint64_t created_local = 0; + + { + std::lock_guard lk(mu_); + // Build track indices by group. + std::vector all_track_indices; + all_track_indices.reserve(tracks_.size()); + for (size_t i = 0; i < tracks_.size(); ++i) all_track_indices.push_back(static_cast(i)); + + auto group_key_track = [&](const Track& t) -> int { return cfg->per_class ? t.cls_id : 0; }; + auto group_key_det = [&](const Detection& d) -> int { return cfg->per_class ? d.cls_id : 0; }; + + // Group tracks. + std::map> tracks_by_group; + for (int ti : all_track_indices) { + const auto& trk = tracks_[static_cast(ti)]; + tracks_by_group[group_key_track(trk)].push_back(ti); + } + + // Group dets. + std::map> high_by_group; + std::map> low_by_group; + for (int di : high_dets) { + high_by_group[group_key_det(dets[static_cast(di)])].push_back(di); + } + for (int di : low_dets) { + low_by_group[group_key_det(dets[static_cast(di)])].push_back(di); + } + + // Stage 1: high det match. + std::vector> matches; + std::vector un_tracks; + std::vector un_dets; + std::vector stage1_unmatched_tracks; + stage1_unmatched_tracks.reserve(tracks_.size()); + + for (auto& kv : tracks_by_group) { + const int g = kv.first; + const auto& t_idx = kv.second; + auto itD = high_by_group.find(g); + const std::vector empty; + const auto& d_idx = (itD != high_by_group.end()) ? itD->second : empty; + + GreedyMatch(tracks_, t_idx, dets, d_idx, cfg->iou_th, matches, un_tracks, un_dets); + + // Apply matches + for (const auto& m : matches) { + Track& trk = tracks_[static_cast(m.first)]; + Detection& det = dets[static_cast(m.second)]; + UpdateTrackLocked(trk, det, now_us, frame->frame_id, *cfg); + if (trk.confirmed) det.track_id = trk.id; + ++matched_local; + } + + // Accumulate unmatched tracks for stage2. + for (int ti : un_tracks) { + stage1_unmatched_tracks.push_back(ti); + } + // We'll create new tracks from high det unmatched below. + high_by_group[g] = un_dets; + } + + // Stage 2: low det match for unmatched tracks. + // Re-group stage1 unmatched tracks. + std::map> stage1_un_tracks_by_group; + for (int ti : stage1_unmatched_tracks) { + const auto& trk = tracks_[static_cast(ti)]; + stage1_un_tracks_by_group[group_key_track(trk)].push_back(ti); + } + + for (auto& kv : stage1_un_tracks_by_group) { + const int g = kv.first; + const auto& t_idx = kv.second; + auto itD = low_by_group.find(g); + if (itD == low_by_group.end()) continue; + auto& d_idx = itD->second; + if (d_idx.empty()) continue; + + GreedyMatch(tracks_, t_idx, dets, d_idx, cfg->iou_th, matches, un_tracks, un_dets); + for (const auto& m : matches) { + Track& trk = tracks_[static_cast(m.first)]; + Detection& det = dets[static_cast(m.second)]; + UpdateTrackLocked(trk, det, now_us, frame->frame_id, *cfg); + if (trk.confirmed) det.track_id = trk.id; + ++matched_local; + } + d_idx = un_dets; + } + + // Create new tracks from remaining unmatched HIGH dets. + for (auto& kv : high_by_group) { + auto& remain_high = kv.second; + for (int di : remain_high) { + if (static_cast(tracks_.size()) >= cfg->max_tracks) { + // Capacity reached. + ++unmatched_dets_local; + continue; + } + Detection& det = dets[static_cast(di)]; + Track trk; + trk.id = next_track_id_++; + trk.cls_id = det.cls_id; + trk.bbox = det.bbox; + trk.last_seen_us = now_us; + trk.last_seen_frame_id = frame->frame_id; + trk.hit_streak = 1; + trk.total_hits = 1; + trk.confirmed = (cfg->min_hits <= 1); + if (trk.confirmed) det.track_id = trk.id; + tracks_.push_back(std::move(trk)); + ++created_local; + } + } + + // Unmatched low dets are just counted. + for (const auto& kv : low_by_group) { + unmatched_dets_local += kv.second.size(); + } + + // Tracks not updated this frame are left as-is; they will be removed by time-based pruning. + tracks_active_.store(tracks_.size()); + } + + if (created_local) tracks_created_total_.fetch_add(created_local); + if (matched_local) matched_total_.fetch_add(matched_local); + if (unmatched_dets_local) unmatched_dets_total_.fetch_add(unmatched_dets_local); + + MaybeLogStats(now_us, *cfg); + PushToDownstream(frame); + + total_process_us_.fetch_add(NowUsSteady() - t0); + processed_frames_.fetch_add(1); + return NodeStatus::OK; + } + +private: + uint64_t ResolveNowUsLocked(const Frame& frame) { + uint64_t t = frame.pts; + if (t == 0) { + t = (last_time_us_ == 0) ? NowUsSteady() : (last_time_us_ + 1); + } else if (last_time_us_ != 0) { + // Clamp non-monotonic timestamps to avoid sudden large backward jumps. + if (t + 2000000ULL < last_time_us_) { + t = last_time_us_ + 1; + } else if (t < last_time_us_) { + t = last_time_us_; + } + } + last_time_us_ = t; + return t; + } + + static bool IsTrackClass(const ConfigSnapshot& cfg, int cls_id) { + if (cls_id < 0) return false; + if (!cfg.track_classes.empty()) { + return cfg.track_classes.count(cls_id) > 0; + } + if (!cfg.ignore_classes.empty()) { + return cfg.ignore_classes.count(cls_id) == 0; + } + return true; + } + + static void UpdateTrackLocked(Track& trk, const Detection& det, uint64_t now_us, uint64_t frame_id, + const ConfigSnapshot& cfg) { + trk.bbox = det.bbox; + trk.last_seen_us = now_us; + trk.last_seen_frame_id = frame_id; + trk.hit_streak += 1; + trk.total_hits += 1; + if (!trk.confirmed && trk.hit_streak >= cfg.min_hits) { + trk.confirmed = true; + } + if (!cfg.per_class) { + trk.cls_id = det.cls_id; + } + } + + uint64_t PruneExpiredLocked(uint64_t now_us, const ConfigSnapshot& cfg) { + if (tracks_.empty()) return 0; + const uint64_t max_age_us = static_cast(std::max(0, cfg.max_age_ms)) * 1000ULL; + + const size_t before = tracks_.size(); + if (max_age_us == 0) { + tracks_.clear(); + tracks_active_.store(0); + return static_cast(before); + } + + tracks_.erase(std::remove_if(tracks_.begin(), tracks_.end(), [&](const Track& t) { + if (t.last_seen_us == 0) return true; + return (now_us > t.last_seen_us) && ((now_us - t.last_seen_us) > max_age_us); + }), + tracks_.end()); + + const size_t after = tracks_.size(); + tracks_active_.store(after); + return static_cast(before - after); + } + + void MaybeLogStats(uint64_t now_us, const ConfigSnapshot& cfg) { + if (!cfg.stats_log) return; + + uint64_t last = 0; + { + std::lock_guard lk(mu_); + last = last_stats_us_; + if (last_stats_us_ == 0 || (now_us > last_stats_us_ && (now_us - last_stats_us_) >= + static_cast(cfg.stats_interval_ms) * 1000ULL)) { + last_stats_us_ = now_us; + } else { + return; + } + } + + (void)last; + LogInfo("[tracker] id=" + id_ + + " tracks=" + std::to_string(tracks_active_.load()) + + " created=" + std::to_string(tracks_created_total_.load()) + + " removed=" + std::to_string(tracks_removed_total_.load()) + + " matched=" + std::to_string(matched_total_.load()) + + " unmatch_det=" + std::to_string(unmatched_dets_total_.load())); + } + + void PushToDownstream(FramePtr frame) { + for (auto& q : output_queues_) { + q->Push(frame); + } + } + + std::string id_; + + std::shared_ptr> input_queue_; + std::vector>> output_queues_; + + mutable std::mutex mu_; + std::shared_ptr cfg_; + + std::vector tracks_; + int next_track_id_ = 0; + + uint64_t last_time_us_ = 0; + + uint64_t last_stats_us_ = 0; + + std::atomic tracks_active_{0}; + std::atomic tracks_created_total_{0}; + std::atomic tracks_removed_total_{0}; + std::atomic matched_total_{0}; + std::atomic unmatched_dets_total_{0}; + std::atomic processed_frames_{0}; + std::atomic total_process_us_{0}; +}; + +REGISTER_NODE(TrackerNode, "tracker"); + +} // namespace rk3588 diff --git a/命令.md b/命令.md index 9b542ab..77fb974 100644 --- a/命令.md +++ b/命令.md @@ -53,4 +53,63 @@ cd /d D:\App\C++\Rk3588Sys\agent ls -l ./rk3588-agent_linux_arm64 chmod +x ./rk3588-agent_linux_arm64 - ./rk3588-agent_linux_arm64 --config agent_cam1.config.json \ No newline at end of file + ./rk3588-agent_linux_arm64 --config agent_cam1.config.json + +用 systemd(Ubuntu 22.04 / root)部署并后台常驻运行 agent + + 1) 放置文件 + + 在板子上执行(按你实际文件名/路径调整): + + sh + sudo mkdir -p /opt/rk3588-agent + sudo cp /path/to/rk3588-agent /opt/rk3588-agent/rk3588-agent + sudo cp /path/to/agent_cam1.config.json /opt/rk3588-agent/agent.config.json + sudo chmod +x /opt/rk3588-agent/rk3588-agent + + 关键点:运行命令需要 `--config`,这里统一放成 `/opt/rk3588-agent/agent.config.json` + + 2) 创建 systemd 服务文件 + + 创建 /etc/systemd/system/rk3588-agent.service: + + sh + sudo tee /etc/systemd/system/rk3588-agent.service >/dev/null <<'EOF' + [Unit] + Description=RK3588 Agent + After=network-online.target + Wants=network-online.target + + [Service] + Type=simple + User=root + WorkingDirectory=/opt/rk3588-agent + ExecStart=/opt/rk3588-agent/rk3588-agent --config /opt/rk3588-agent/agent.config.json + Restart=always + RestartSec=2 + LimitNOFILE=65535 + + [Install] + WantedBy=multi-user.target + EOF + + 3) 启动 + 开机自启 + + sh + sudo systemctl daemon-reload + sudo systemctl enable --now rk3588-agent + + 4) 查看状态与日志 + + sh + sudo systemctl status rk3588-agent --no-pager + sudo journalctl -u rk3588-agent -f + + 5) 停止/重启 + + sh + sudo systemctl stop rk3588-agent + sudo systemctl restart rk3588-agent + + 这样运行后,SSH 断开不会影响进程(由 systemd 托管)。如果你的 agent.config.json 里有相对路径(如 models 目录),记得写成绝对路径,或放到 + /opt/rk3588-agent/ 并按配置调整。 \ No newline at end of file