Files
code-test-drone/reference-c++/target_tracker/target_tracker.md
T
2026-09-02 15:13:13 +08:00

218 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# target_tracker 视觉追踪库实现文档
> 独立动态库(`libtarget_tracker.so`),实现轻量级多目标追踪过滤器:
> 恒定速度卡尔曼滤波平滑 + Coast 抗短暂丢失 + ID 稳定与主目标锁定。
> 算法行为的唯一权威规格为 `docs/视觉追踪技术路线.md`(下称"技术路线"),
> 本文档不重复抄录算法全文,仅描述实现结构与对照关系。
---
## 一、功能职责与边界
**做什么**:
- 每帧接收 YOLO 检测框列表,输出**一个主目标**的平滑中心点/边界框/状态标志
(`TrackResult`),供下游速度闭环(PID)消费。
- 平滑:8 维恒定速度卡尔曼滤波(位置/尺寸 + 速度),抑制检测框逐帧抖动。
- Coast 预测:目标短暂消失时用滤波器预测值继续输出(`is_predicted=true`),
避免控制输出突变;连续丢失超过 `max_lost_frames`(严格大于)才判丢。
- ID 稳定:贪婪最邻近关联(中心距离 + IoU 加权代价)维持轨道 ID;
主目标锁定后不被高置信度新目标抢占,仅在轨道死亡或 `Reset()` 时解除。
**不做什么**(边界):
- 不做光流估计、不做激光/惯导等多源融合(属 perception 模块职责);
- 不做 Topic 订阅/发布接线、不依赖 spdlog/Eigen/OpenCV,零第三方依赖;
- 不做画面边界钳制与负尺寸约束(与技术路线 §4.5 注记保持一致);
- 不要求检测置信度下限过滤(上游检测器已完成阈值过滤)。
## 二、接口与数据流
命名空间 `drone::tracker`,头文件保护 `#pragma once`。
| 头文件 | 内容 |
|--------|------|
| `include/target_tracker/kalman_box_filter.h` | `BoxCXCYWH`、`KalmanBoxFilter` |
| `include/target_tracker/tracklet.h` | `BoxXYXY`、`ToCenterSize`/`ToXYXY`/`ComputeIoU`、`Tracklet` |
| `include/target_tracker/target_tracker.h` | `Detection`、`FrameShape`、`TrackResult`、`TargetTracker` |
主要接口签名:
```cpp
class KalmanBoxFilter {
void Initiate(const BoxCXCYWH& z); // 首次观测初始化(§4.3)
std::optional<BoxCXCYWH> Predict(); // 未初始化返回 nullopt(§4.4)
void Update(const BoxCXCYWH& z); // 观测更新(§4.5)
bool Initialized() const;
const std::array<double, 8>& State() const; // [cx,cy,w,h,vx,vy,vw,vh]
};
struct Tracklet {
Tracklet(int id, const BoxCXCYWH& measurement, double conf, int cls,
int max_lost_frames);
bool IsDead() const; // lost_count > max_lost(严格大于)
bool IsCoasting() const; // lost_count > 0
void Predict(); // kf.predict 非空时更新 last_center/last_box、age+1
void Update(const BoxXYXY& box, double conf, int cls); // 匹配更新
void MarkMiss(); // lost+1、age+1
// 字段:track_id / kf / lost_count / max_lost / confidence / cls_id /
// age / hits / last_center / last_box / last_raw_box
};
class TargetTracker {
struct Config { // §8.1 默认值
int max_lost_frames = 8;
double max_association_dist = 200.0;
int min_hits = 3;
double dist_weight = 0.5;
double iou_weight = 0.5;
};
TargetTracker(); // 默认配置
explicit TargetTracker(const Config& cfg);
TrackResult Update(const std::vector<Detection>& detections,
std::optional<FrameShape> frame_shape = std::nullopt);
TrackResult LastResult() const; // 线程安全拷贝
void Reset(); // 清全部状态且 next_id 归零
int ActiveCount() const;
std::unordered_map<int, Tracklet> ActiveTracks() const; // 只读快照
};
```
数据流:调用方每帧将检测列表(像素坐标 `[x1,y1,x2,y2,conf,cls_id]`)传入
`Update()`,得到 `TrackResult`(九字段:tracked / primary_id / center / box /
confidence / is_predicted / lost_frames / n_active / raw)。丢失时
`tracked=false`、定位字段空、`lost_frames=0`(技术路线 §2.3)。
## 三、关键实现点
### 3.1 卡尔曼滤波(对照技术路线 §4)
- 状态 `x[8]=[cx,cy,w,h,vx,vy,vw,vh]`,观测 `z[4]=[cx,cy,w,h]`,dt=1 帧。
- 常量矩阵:`F`(8×8,I4 + 右上 I4)、`H`(4×8,[I4|0])为 `constexpr`;
矩阵乘法/转置用 `std::array` 定长手写,全程 `double`(§9.3)。
- 常量:`kStdPos=1/50`、`kStdVel=1/200`、`kIniPos=2.0`、`kIniVel=100.0`、
`kRScale=2.0`(§4.2/§8.2)。
- **动态噪声**:`Predict()` 时 Q=diag(σ_pos²,σ_vel²),σ 直接用状态中的 w/h
缩放;`Update()` 时 R 用 `abs(w)/abs(h)` 缩放并乘 2——两处差异与规格
§4.2 注记严格保持一致。
- `Initiate()`:速度置零,P=diag(2,2,2,2,100,100,100,100)(§4.3)。
- `Update()` 中 S=HPHᵀ+R 为 4×4,用**高斯-约当消元 + 部分主元**求逆,
消元前对角加 1e-9 正则化防奇异(§9.3);随后 K=PHᵀS⁻¹、
x←x+K(z−Hx)、P←(I−KH)P(§4.5)。
### 3.2 贪婪关联(对照 §5)
- `cost = dist_weight·dist/max(Dmax,1e-3) + iou_weight·(1−IoU)`,
距离用 `std::hypot`;每条轨道 `best_cost` 初始化为 1.0,**仅 cost 严格
小于才匹配**(cost=1.0 恰好不匹配,§5.1)。
- 按轨道 confidence 降序(平局按 ID 升序保证确定性)逐一贪心匹配;
排序时复制 ID 到 `std::vector` 后 `std::sort`,不直接排哈希表(§9.2)。
### 3.3 主目标选举三级回退(对照 §6.3)
1. **锁定延续优先**:`locked_primary_id` 存活则直接输出(Coast 中照样输出
`is_predicted=true`),不被高置信度目标抢占;
2. **重新选举**(无锁定/锁定已死亡):measured(lost==0)中 hits≥min_hits
的已确认池取 confidence 最大;池空则退回全部 measured;
3. 无 measured 时从 coasting 中选离画面中心最近者(欧氏距离平方,
`frame_shape` 缺省按 640×480 中心 (320,240)),`is_predicted=true`;
都没有则 `tracked=false`。
锁定仅在轨道死亡删除处或 `Reset()` 时解除。
### 3.4 主流程六步(对照 §7)
`Update()` 严格按:①全体轨道 predict ②检测中心表 + 贪婪关联 ③匹配轨道
update(raw_box 由检测重建)④未匹配轨道 mark_miss、死亡即删、若为锁定
目标同时清锁 ⑤未匹配检测新建轨道(创建当帧赋 last_box/last_raw_box,
保证当帧可参与输出;next_id 从 0 递增永不复用)⑥主目标选举组装输出并
缓存 last_result。
### 3.5 线程模型(对照 §9.5)
单线程写 `Update()`(推理主循环);库内仅对 `last_result` 缓存加一把
`std::mutex`,`LastResult()` 返回受保护拷贝。内部轨道状态不加锁;
若未来改为多线程调用 `Update()` 需整体加锁。
### 3.6 符号导出说明
当前不加 `-fvisibility=hidden`、不使用导出宏,共享库默认导出全部公共符号。
日后若启用隐藏可见性(如 `CXX_VISIBILITY_PRESET hidden`),需补
`TARGET_TRACKER_API` 宏(导出/导入双态)并标注所有公共类与函数,
否则下游链接将缺失符号。
## 四、日志行为
本库为**纯算法层,不打任何日志**(不依赖 spdlog)。若未来接入主工程,
接线层(订阅/发布适配部件)按主工程日志纪律负责关键路径日志与节流,
本库内部不新增日志。
## 五、测试方式
构建环境:WSL2 Ubuntu(AGENTS.md 规定的开发机构建环境)。
```bash
cd /mnt/d/ProgramData/drone_test/drone_test
cmake -S target_tracker -B target_tracker/build
cmake --build target_tracker/build -j$(nproc)
ctest --test-dir target_tracker/build --output-on-failure
```
测试共 27 条(kalman_box_filter_test 5 条 / tracklet_test 12 条 /
target_tracker_test 10 条),其中 `target_tracker_test.cpp` 逐条覆盖
技术路线 §9.4 六条验收用例:
1. `EmptyFramesCoastThenLost`:空检测帧 Coast 持续 max_lost_frames 帧
(is_predicted=true),第 max_lost_frames+1 帧 tracked=false;
2. `IdStrictlyIncreasingNoReuse`:ID 严格递增不复用,Reset 后归零;
3. `LockNotStolenAndSwitchAfterDeath`:Coast 期间高置信度新目标不抢占锁定,
锁定目标死亡后切换;
4. `MinHitsFallbackAllowsNewTrack`:无已确认实测轨道时新轨道(hits<min_hits)
可当选主目标;
5. `CostExactlyOneDoesNotMatch`:纯距离项构造 cost=1.0 恰好不匹配,
并附 cost<1.0 对照帧;
6. `HigherConfidenceTrackWinsDetection`:两轨道竞争同一检测,置信度高者获胜。
其余为补充行为测试(丢失输出字段约定、Coast 兜底选举、锁定延续、
默认画面中心、IoU 边界、滤波器收敛性等)。
## 六、排查 / 修改要点
- **主目标"甩头"**:确认是否误改锁定解除逻辑——锁定只能在轨道死亡删除处
或 `Reset()` 解除;选举分支不得覆盖存活锁定。
- **Coast 时间不对**:死亡判定为 `lost_count > max_lost`(严格大于),
默认允许 Coast 8 帧、第 9 帧判丢;改成 `>=` 属于行为变更。
- **关联不上 / 乱匹配**:检查 `max_association_dist` 与目标帧间位移是否
匹配;cost 判据为严格小于 1.0;dist/iou 权重之和建议保持 1.0。
- **滤波发散或跳变**:检查 Q/R 动态噪声是否按状态 w/h 缩放(R 用 abs 且
乘 2、Q 不用 abs,该差异是规格要求);4×4 求逆已加 1e-9 正则化。
- **测试链接失败(找不到 so)**:测试可执行文件通过 `CMAKE_BUILD_RPATH`
定位库;若把测试挪到别处运行需设置 `LD_LIBRARY_PATH`。
- 修改任何行为后必须同步更新本文档与 `docs/视觉追踪技术路线.md` 的对照注释。
## 七、未来接线指引(接入主工程视频链路)
本库以独立动态库交付,主工程接入建议采用:
1. **订阅解码帧主题作为每帧节拍**:接线部件订阅解码帧 Topic,以解码帧
到达为帧节拍驱动追踪(保证与推理节奏一致);
2. **按 frame_sequence 聚合 DetectionResult**:YOLO 每目标发布一条
DetectionResult(同帧共享 frame_sequence),接线部件按 frame_sequence
聚合成 `std::vector<Detection>` 后调用 `Update()`;
3. **主工程 `include/common/types.h` 新增 `TrackedTarget` 消息与
`kTrackedTarget` 主题**:由接线部件把 `TrackResult` 转换为
`TrackedTarget` 发布,供控制线程(20Hz PID)订阅消费;Coast
(is_predicted=true)时调用方清 PID 积分、tracked=false 时零速刹车
(技术路线 §1.2 上游行为约定)。
主工程链接该 so 的 CMake 片段示例:
```cmake
# 主工程 CMakeLists.txt 中引入视觉追踪库(独立子工程)
add_subdirectory(target_tracker)
target_link_libraries(drone_control PRIVATE target_tracker)
# 部署时需将 libtarget_tracker.so.1 一并打包到可执行文件目录
# (或设置 INSTALL_RPATH=$ORIGIN,与 docs/部署与打包.md 目录式部署约定一致)
```