开发跟踪
Some checks are pending
CI / host-build (push) Waiting to run
CI / rk3588-cross-build (push) Waiting to run

This commit is contained in:
sladro 2026-01-11 20:15:27 +08:00
parent 97b76f45cb
commit 9c59c5d310
4 changed files with 939 additions and 2 deletions

View File

@ -0,0 +1,192 @@
# PRD_05Device 侧轻量级 Tracker 节点(可插拔)
## 1. 背景与问题
当前 Device 侧媒体服务采用“配置驱动 DAGGraph+ 插件节点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 loopTracker 节点不应引入额外线程(除非明确需求)。
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 成本。

View File

@ -279,6 +279,16 @@ set_target_properties(det_post PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${RK_PLUGIN_OUTPUT_DIR} 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) # osd plugin (on-screen display for detection results)
add_library(osd SHARED osd/osd_node.cpp) add_library(osd SHARED osd/osd_node.cpp)
target_include_directories(osd PRIVATE ${CMAKE_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/third_party) 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() 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 LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/rk3588-media-server/plugins
RUNTIME DESTINATION ${CMAKE_INSTALL_LIBDIR}/rk3588-media-server/plugins RUNTIME DESTINATION ${CMAKE_INSTALL_LIBDIR}/rk3588-media-server/plugins
) )

View File

@ -0,0 +1,676 @@
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <map>
#include <mutex>
#include <set>
#include <string>
#include <unordered_set>
#include <utility>
#include <vector>
#include "node.h"
#include "utils/logger.h"
namespace rk3588 {
namespace {
using Clock = std::chrono::steady_clock;
static inline uint64_t NowUsSteady() {
return static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::microseconds>(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<int> track_classes; // whitelist
std::set<int> ignore_classes; // blacklist
std::unordered_set<std::string> 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<int>& 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<std::string>& 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<const ConfigSnapshot>& out,
std::string& err) {
auto snap = std::make_shared<ConfigSnapshot>();
snap->id = config.ValueOr<std::string>("id", "tracker");
snap->mode = config.ValueOr<std::string>("mode", "bytetrack_lite");
snap->per_class = config.ValueOr<bool>("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<float>("high_th", 0.5f);
snap->low_th = config.ValueOr<float>("low_th", 0.1f);
snap->iou_th = config.ValueOr<float>("iou_th", 0.3f);
snap->max_age_ms = static_cast<int64_t>(config.ValueOr<int>("max_age_ms", 1500));
snap->min_hits = config.ValueOr<int>("min_hits", 2);
snap->max_tracks = config.ValueOr<int>("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<bool>("stats", false);
snap->stats_interval_ms = static_cast<int64_t>(dbg->ValueOr<int>("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<Track>& tracks, const std::vector<int>& track_indices,
const std::vector<Detection>& dets, const std::vector<int>& det_indices, float iou_th,
std::vector<std::pair<int, int>>& out_matches, std::vector<int>& out_unmatched_tracks,
std::vector<int>& 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<MatchPair> pairs;
pairs.reserve(track_indices.size() * det_indices.size());
for (int ti : track_indices) {
const auto& trk = tracks[static_cast<size_t>(ti)];
for (int di : det_indices) {
const float iou = IoU(trk.bbox, dets[static_cast<size_t>(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<int> used_tracks;
std::unordered_set<int> 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<const ConfigSnapshot> 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<std::mutex> 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<const ConfigSnapshot> 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<std::mutex> 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<double>(tracks_active_.load()));
o["tracks_created_total"] = SimpleJson(static_cast<double>(tracks_created_total_.load()));
o["tracks_removed_total"] = SimpleJson(static_cast<double>(tracks_removed_total_.load()));
o["matched_total"] = SimpleJson(static_cast<double>(matched_total_.load()));
o["unmatched_dets_total"] = SimpleJson(static_cast<double>(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<double>(total_us) / 1000.0 / static_cast<double>(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<const ConfigSnapshot> cfg;
{
std::lock_guard<std::mutex> 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<std::mutex> 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<int> high_dets;
std::vector<int> 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<int>(i));
} else if (d.score >= cfg->low_th) {
low_dets.push_back(static_cast<int>(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<std::mutex> lk(mu_);
// Build track indices by group.
std::vector<int> 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<int>(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<int, std::vector<int>> tracks_by_group;
for (int ti : all_track_indices) {
const auto& trk = tracks_[static_cast<size_t>(ti)];
tracks_by_group[group_key_track(trk)].push_back(ti);
}
// Group dets.
std::map<int, std::vector<int>> high_by_group;
std::map<int, std::vector<int>> low_by_group;
for (int di : high_dets) {
high_by_group[group_key_det(dets[static_cast<size_t>(di)])].push_back(di);
}
for (int di : low_dets) {
low_by_group[group_key_det(dets[static_cast<size_t>(di)])].push_back(di);
}
// Stage 1: high det match.
std::vector<std::pair<int, int>> matches;
std::vector<int> un_tracks;
std::vector<int> un_dets;
std::vector<int> 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<int> 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<size_t>(m.first)];
Detection& det = dets[static_cast<size_t>(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<int, std::vector<int>> stage1_un_tracks_by_group;
for (int ti : stage1_unmatched_tracks) {
const auto& trk = tracks_[static_cast<size_t>(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<size_t>(m.first)];
Detection& det = dets[static_cast<size_t>(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<int>(tracks_.size()) >= cfg->max_tracks) {
// Capacity reached.
++unmatched_dets_local;
continue;
}
Detection& det = dets[static_cast<size_t>(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<uint64_t>(std::max<int64_t>(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<uint64_t>(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<uint64_t>(before - after);
}
void MaybeLogStats(uint64_t now_us, const ConfigSnapshot& cfg) {
if (!cfg.stats_log) return;
uint64_t last = 0;
{
std::lock_guard<std::mutex> lk(mu_);
last = last_stats_us_;
if (last_stats_us_ == 0 || (now_us > last_stats_us_ && (now_us - last_stats_us_) >=
static_cast<uint64_t>(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<SpscQueue<FramePtr>> input_queue_;
std::vector<std::shared_ptr<SpscQueue<FramePtr>>> output_queues_;
mutable std::mutex mu_;
std::shared_ptr<const ConfigSnapshot> cfg_;
std::vector<Track> tracks_;
int next_track_id_ = 0;
uint64_t last_time_us_ = 0;
uint64_t last_stats_us_ = 0;
std::atomic<uint64_t> tracks_active_{0};
std::atomic<uint64_t> tracks_created_total_{0};
std::atomic<uint64_t> tracks_removed_total_{0};
std::atomic<uint64_t> matched_total_{0};
std::atomic<uint64_t> unmatched_dets_total_{0};
std::atomic<uint64_t> processed_frames_{0};
std::atomic<uint64_t> total_process_us_{0};
};
REGISTER_NODE(TrackerNode, "tracker");
} // namespace rk3588

View File

@ -54,3 +54,62 @@ cd /d D:\App\C++\Rk3588Sys\agent
ls -l ./rk3588-agent_linux_arm64 ls -l ./rk3588-agent_linux_arm64
chmod +x ./rk3588-agent_linux_arm64 chmod +x ./rk3588-agent_linux_arm64
./rk3588-agent_linux_arm64 --config agent_cam1.config.json ./rk3588-agent_linux_arm64 --config agent_cam1.config.json
用 systemdUbuntu 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/ 并按配置调整。