# lie **Repository Path**: JousenZhou/lie ## Basic Information - **Project Name**: lie - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-14 - **Last Updated**: 2026-10-15 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 小游戏目标追踪系统 实时追踪小游戏中的白色实心真目标:倒计时后目标褪色至与透明轮廓误导项一致,并开始自转 + 自由走位,系统靠**自转角速度 ω** 这个特征持续锁定它。 技术路线:`YOLO 检测 → OC-SORT 多目标追踪 → 状态机管理 → 自转特征验证 → 可视化输出` ## 安装 ```bash python -m venv .venv .venv/Scripts/python.exe -m pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple ``` 模型随项目走:`tracker-app/best.pt`(已放入,50MB)。要用别的 checkpoint 设环境变量 `YOLO_MODEL_PATH`,代码里的默认值就是项目内这个路径(`config.py:6`)。 ## CPU / GPU(TensorRT INT8) 界面上「计算设备」下拉框两档:CPU / GPU · TensorRT INT8,只在新分析会话(点「开始」)时生效。GPU 档要求三样齐全,缺一则该项**禁用**并给出原因(tooltip):NVIDIA 驱动(`nvidia-smi` 可用)、CUDA 版 PyTorch、`tensorrt` 包。装法(RTX 50 系需 cu128 以上轮子): ```bash .venv/Scripts/python.exe -m pip install torch torchvision --index-url https://download.pytorch.org/whl/cu130 .venv/Scripts/python.exe -m pip install tensorrt onnx onnxslim -i https://pypi.tuna.tsinghua.edu.cn/simple ``` GPU 档是**最低延迟方案 TensorRT INT8**:`batch=1`、静态形状引擎;INT8 校准集直接从被分析视频均匀抽帧(`config.TRT_CALIB_MAX_IMAGES=256` 张,量化范围对齐游戏实际像素分布)。引擎按(checkpoint 内容 hash + imgsz)缓存(`best_int8416_.engine`),首次构建约 1-5 分钟,之后毫秒级加载;换 checkpoint 或改 `imgsz` 会自动失效重建。命令行等价参数:`tools/run_offline.py --device gpu`(默认 `auto`:栈齐则走 GPU)。 降级链显式且留痕:TensorRT INT8 → torch CUDA(INT8 构建失败时)→ CPU,实际后端显示在信息栏「设备:」。 实测(RTX 5060 Ti,TensorRT 11.4,torch 2.14.1+cu130,754x510 @30fps 录屏,imgsz 416):引擎首次构建 156-265s,之后 ~0.3s 加载缓存;同视频同帧数 INT8 **p50 13.7ms / p95 17.0ms** vs CPU p50 72.2ms(~5.3x),UI 逐帧档实测 **30.5fps 达到实时、丢帧 0**(CPU 档 ~12fps)。INT8 量化带来轻微检测数差异(均值 16.4 vs CPU 17.6),状态机逐帧判定 120/120 与 CPU 一致。 ## 运行 ```bash # 图形界面(导入视频 → 开始/暂停/单帧步进/停止,进度条可拖动预览) .venv/Scripts/python.exe main.py [<录屏>.mp4] [--autoplay] # 无界面批处理:用于验证与调参 .venv/Scripts/python.exe tools/run_offline.py --video ../<录屏>.mp4 --imgsz 416 --csv runs/stats.csv --out runs/out.mp4 .venv/Scripts/python.exe tools/run_offline.py --camera 0 # CWG 采集卡(见下) # 阶段耗时分解 / 结果汇总 / 逐帧轨迹 / UI 自检 .venv/Scripts/python.exe tools/bench_stages.py --imgsz 416 .venv/Scripts/python.exe tools/summarize_csv.py runs/stats.csv .venv/Scripts/python.exe tools/trace_tracks.py --start 230 --end 250 .venv/Scripts/python.exe tools/ui_smoke_test.py --video ../<录屏>.mp4 --frames 200 # 验证集闭环评估:身份正确率 / 首锁延迟 / 换轨次数(见「验证集评估」一节) .venv/Scripts/python.exe tools/eval_valset.py --root ../<验证视频带运动估计json> --device gpu --tag run ``` ## 单元与合成数据测试 ```bash .venv/Scripts/python.exe -m pytest tests -q # 79 passed ``` `tests/` 不需要真实视频或模型:合成旋转图形验证 ω 的符号与幅值、轨迹三条生命周期规则、状态机全部转换、以及只有真实视频才暴露出的身份连续性问题(易变的 OC-SORT id、漏检、褪色时机)。 ## 模块 | 目录 | 职责 | |------|------| | `core/detector.py` | YOLO 封装。启动时把逻辑形状名映射到 `model.names` 的**实际**类别 id,不硬编码文档里的 id 表 | | `core/tracker.py` | 用 ultralytics 内置 OC-SORT(配置见 `ocsort.yaml`);卡尔曼状态在库内,本层维护轨迹历史 | | `core/kinematics.py` | 运动学档案:每轨迹测量帧鲁棒速度(中位数斜率)、群体速度(全轨迹中位数)、群体背离度 cos、惯性外推、出生/近旁查询——重叠身份判定的证据来源 | | `core/tracks.py` | 轨迹生命周期:质心出画面即删除、被遮挡继续外推、超 `max_age` 删除 | | `core/state_machine.py` | IDLE/COUNTDOWN/TRANSITION/TRACKING/TARGET_LOST/GAME_OVER,非法转换被拒绝;持有目标身份证书(`target_bbox` / `target_omega`) | | `core/pipeline.py` | 每帧:检测/追踪 → 旋转 → 身份锚定 → 状态机 → 结果 | | `core/rotation_analyzer.py` | ROI 内重新分割轮廓 → 径向角向签名 → 圆互相关求**帧间角增量** → 累加角 polyfit 求 ω | | `core/recovery.py` | 丢失后候选评分:ω 与目标证书的一致性 + 运动方向 + 面积,并受**可达距离**约束 | | `video_io/` | 视频文件读取、原帧率节流;`cwg_source.py` 采集卡源 | | `visualization/` | 旋转框、轨迹线、质心、速度箭头、角度线、ID/ω 标注、状态字 | | `ui/` | PyQt5 主窗口,管线跑在 QThread | ## 跟踪算法(逐帧在做什么) 一次推理两用:`model.track(persist=True)` 返回的检测框同时就是本帧检测阶段的输出(`core/detector.py:82`),CPU 上每帧只跑一遍 YOLO。 **1. 检测 + OC-SORT**(ultralytics 内置,参数在 `ocsort.yaml`) `track_high_thresh 0.3 / track_low_thresh 0.1 / new_track_thresh 0.6 / match_thresh 0.8 / track_buffer 45 / delta_t 3 / inertia 0.2 / fuse_score true / use_byte false`。两个值是实测踩出来的:星星的框在自转中会 ±20px 抖动,生成阈值放宽就会为同一颗星开出重复轨迹互相抢,所以 `new_track_thresh` 取 0.6;`track_buffer 45 ≥ OC_SORT_MAX_AGE 30`,保证被遮住的星以**原 id** 复活而不是新建一条。 **2. 轨迹簿记**(`core/tracks.py`):质心出画面即删;丢失期间卡尔曼外推继续给框(面积/ω 会冻结,这是"只有预测没有测量"的特征,排查时很好认);`lost_frames > 30` 删除。 **3. 自转角速度 ω**(`core/rotation_analyzer.py`):bbox 内重新分割轮廓(白填充优先,否则取边缘)→ 96 bin 径向角向签名(留 8 阶谐波)→ 与上一帧做**限幅圆互相关**求帧间角增量(搜索窗 `MAX_SHIFT_DEG=25°`,远小于五角星 72° 的对称周期,因此永远选不到对称假解,累加角天然连续)→ 累加角对**真实帧号**最小二乘(`OMEGA_WINDOW=15`)。拟合样本不足时返回 `None` 而非 0,`omega_valid=False` 表示"还没测出来",绝不能被下游读成"它没在转"——这个区分救过一次符号翻转。 **4. 身份维护**(三层,`core/pipeline.py` + `core/kinematics.py`,按验证集实测校准) 实测(`验证视频带运动估计json`,750x500@30fps,14 星,OC-SORT+旋转分析器对齐 GT 逐帧测量):**干扰项是群体运动**——与群体速度中位数的 cos ≈ +0.97,目标自由漫游 cos ≈ +0.1 甚至为负;重叠是常态(~80% 追踪帧有交叠),不是事件。据此身份维护以「群体背离度」为主证据: - *锁定 id 本帧有测量*:锚框直接跟它的 bbox,不重新评分。深重叠(IoU ≥ `OVERLAP_OMEGA_IOU 0.15`)帧的角度被挂起(不入 ω 拟合、不写证书)——两个轮廓进一个 ROI 只会污染签名。 - *id 换号*(`_reanchor_target`):先过 **grace**(缺测 ≤ `TARGET_GRACE_FRAMES 3` 帧不交接,等 OC-SORT 自己关联回来);候选四道门:**出生/近旁门**(`was_near_at`:要么比目标最后测量晚出生=重打标签,要么当时就在锚框 1.5 身位内=blob 分裂,否则是别的物体飘进来);**群体门**(与群体 cos ≥ `KIN_FLOCK_COS_HI 0.8` = 干扰项,直接拒——f232 的误交接即被此门拦截);**白填充门**(目标可见期间填充比不得掉);**ω 证书门带下限**(|证书| < `TARGET_OMEGA_CERT_FLOOR 0.02` 时视为无证据,退回 `TARGET_HANDOFF_BLIND_IOU(0.55)` 判据——实测褪色瞬间证书恰为 None 的窗口曾让任何 IoU≥0.30 的候选长驱直入)。 - *整条轨迹消失*(`core/recovery.py`):硬门 = 本帧有测量、可达距离内、出生/近旁门、群体门;软评分 = 群体背离(1.0) + 惯性外推命中(0.8,丢失时刻位置+速度的匀速预测,高斯核 σ=可达半径/2) + ω 证书吻合(0.6) + 面积(0.3) + IoU(0.3),按**实际可用证据质量归一化**后需 ≥ `RECOVERY_MIN_RATIO 0.55`。ω 未收敛不再一票否决——真目标重现身 carrier 常是未收敛的新轨迹(实测曾因此全军覆没);圆形目标 ω 项自动退出,由群体+路径承担。 - *静默换轨自检*(TRACKING 期间):锁定轨迹连续 `SWAP_SUSPECT_FRAMES 10` 帧与群体同向、且存在**合格**独立运动者落在惯性预测路径可达范围内 → 判定 OC-SORT 在重叠 blob 内把 id 交给了干扰项,主动重锚。候选必须过与交接一致的合理门:confirmed(≥3 次命中,噪声碎片活不到)、类别、面积 ≥ 锁定轨迹的 `SWAP_MIN_AREA_RATIO(0.3)` 倍(双向)、出生/近旁门——15px 微小碎片的抖动也会读作"独立运动",无这些门时它会从健康的锁定轨迹手里抢走锚框(score1.00_high.mp4 f659 实测)。 **5. 状态机**(`core/state_machine.py`):COUNTDOWN 每帧用白色填充比重新确认目标(id 不可信,且倒计时数字本身会被检成星,只按 id 锁会锁到数字上);TRANSITION 以实测填充比连续 `FADE_CONFIRM_FRAMES(5)` 帧 < `FADE_DONE_RATIO(0.12)` 判褪色完成,测不出来就超时强制离开而不是卡死;TRACKING 期间把每次收敛的 ω 记进**身份证书** = 最近 `TARGET_OMEGA_HISTORY(7)` 次拟合的**中位数**(单帧被重叠轮廓污染不会翻转符号);`TARGET_LOST_TOLERANCE_FRAMES(3)` 容忍短暂漏检。 ## 验证集评估(重叠场景,2026-10) `验证视频带运动估计json/`(10 个场景 × 812 帧:750x500@30fps、14 星、含 GT 逐帧目标 bbox)驱动的闭环评估,工具 `tools/eval_valset.py`,指标 = **身份正确率**(跟随框与 GT IoU ≥ 0.5 的帧占比,计分期): | 版本 | 平均身份正确率 | 最差场景 | 说明 | |---|---|---|---| | 基线(旧身份逻辑) | 21.6% | 0% | 褪色瞬间/重叠后跟错星且不回头 | | v1(群体+惯性+出生门+grace+证书下限) | 88.1% | 25.8% | ω 未收敛硬过滤仍在误杀新生载体 | | v2(ω 过滤软化) | 96.0% | 76.8% | 保守等待窗口仍损失无目标帧 | | v5(换轨检测器加合理性门) | **98.3%** | **94.8%** | 真实录像 score1.00_high.mp4 的 f659 误换轨(15px 噪声碎片被当"独立运动者"收养)暴露换轨候选缺合理性门;补齐面积/类别/confirmed/近旁四门后,该录像 id 连续锁定 517 帧无 GAME_OVER,验证集最弱场景 76.8%→100% | | v6(丢失帧快照) | 98.3% | 94.8% | score1.00_medium.mp4 f570 丢失后永不重锁:`motion_history` 只留 17 条,丢失 ~17 帧后 `was_near_at` 查不到"丢失时刻在哪",老载体被判 near=False 永久出局。改为丢失瞬间快照全体轨迹位置(`near_at_loss`),`lost_velocity` 无值时回退到最后测量帧的平滑速度。该录像 27 帧后正确重锁,无 GAME_OVER;验证集不变 | | v7(褪色期身份链,最终) | **98.4%** | 94.8% | score1.00_medium(010155).mp4 f280 起跟错 390 帧,三因叠加:①`OVERLAP_OMEGA_IOU 0.15` 把浅接触也判深重叠,倒计时 ω 被饿死(f120-240 零角度样本,系重叠保护引入的回归);②ω 证书只在 TRACKING 写入,褪色期全程 cert=None;③TRANSITION 锚死后空滑 120 帧超时盲猜。修复:阈值 0.15→0.35、证书/最后速度全状态写入、锚死立即转 TARGET_LOST 走恢复机制(本例 2 帧内以最小可达半径重锁)。三条真实录像回归通过 | 复现:`.venv/Scripts/python.exe tools/eval_valset.py --root <验证集目录> --device gpu --tag <名字>`;报告落 `runs/valset_.json`。 已知边界:目标某段恰好与群体同向漫游时(wander 方向撞上 flock 方向),群体门无法区分目标与干扰项,系统选择**等待而非猜**(短暂无目标帧);此时换轨检测器同样不该触发——除非存在过全部合理门的独立运动者,否则保持锁定(v5 四门保证噪声碎片不再有资格)。blob 内两星完全重合时身份不可判,等分离。一次实验性地让群体门随目标自身对齐状态自适应开关(v3)在另一场景引入 89 个百分点的回归(门关掉时干扰项长驱直入),已回退——正确修法是给换轨候选加合理门(v5),而不是削弱群体判据。 ## 与需求文档不同的三处实现 1. **`video_io/` 而非文档的 `io/`**:顶层包名 `io` 会遮蔽 Python 标准库 `io`,导入 cv2/numpy 时会随机崩。 2. **角度估计不能用 `minAreaRect`**:对轴对齐 bbox 只会返回 0°/90° 噪声;喂真实轮廓时宽高互换又会让角度在 `[-90,0)` 内跳变,没有分支选择能恢复真实朝向(已实测)。改为**径向角向签名 + 限幅圆互相关**:每帧只测帧间增量,搜索窗 `MAX_SHIFT_DEG` 远小于对称周期,所以累加角天然连续、无需解模糊。 3. **目标身份不能绑定 OC-SORT 的 id**:实测 id 每几十帧就因重叠/框抖动而换号,若把换号当丢失则全程处于误报丢失。因此 `target_id` 只是当前标签,身份由**几何 + 白色填充 + ω 连续性**三重门控维护(`_reanchor_target` 的 handoff),丢失恢复再叠加可达距离约束。 ## 真实录屏上的实测校准 测试素材:`c97745d272323671904eedca78afcf5b.mp4`,768x508 @ 30fps,560 帧 / 18.67s,CPU(torch 2.14+cpu,16 线程)。 | 项 | 实测值 | 采用的阈值 | |----|--------|-----------| | 类别映射 | 全部图形都被判为 `pentagram`(3),类别**不是**可用判别特征 | `SHAPE_ALIASES` | | 白色填充比 | 真目标 0.26–0.43(呼吸式波动)、倒计时数字 0.12–0.21、轮廓 <0.15 | `WHITE_FILL_RATIO=0.20` | | 褪色完成 | 倒计时结束 f150 后再约 42 帧 | `FADE_DONE_RATIO=0.12`, `FADE_CONFIRM_FRAMES=5` | | ω | 倒计时期间稳定 +0.067 rad/帧,褪色后幅值升到 ~0.14 且会变号 | `OMEGA_THRESHOLD=0.02`, `RECOVERY_OMEGA_TOLERANCE=0.06` | | 签名调制深度 | 星形 0.4–2.5,圆形接近 0 | `MIN_ROTATION_MODULATION=0.05` | | 延迟 | 640→p50 161ms,**416→p50 91ms**,320→p50 70ms | `YOLO_INPUT_SIZE=(416,416)` | 416 是唯一满足文档 §4.4 `<100ms` 预算的档位,而且**目标连续性反而最好**(TARGET_LOST 39 帧 vs 640 的 43 帧、320 的 72 帧):框更小 → OC-SORT 生成的重复轨迹更少。320 虽更快,但漏检窗口变长,出现过 35 帧未能恢复。 最终一轮(默认配置)的状态时间线:`f1 COUNTDOWN → f150 TRANSITION → f192 TRACKING → f518 TARGET_LOST → f521 TRACKING → f525 TARGET_LOST`,即褪色后连续追踪 326 帧;目标 id 链 `[11, 5, 11, 35, 11, 5]`,560/560 帧都有目标身份,无跨屏瞬移。 **这条链在 f232 就断了**:f232 之后跟的是干扰星,详见下面「现有问题」第 1 条。`summarize_csv` 报的"560/560 有目标身份"只统计"有 id 在跟",掩盖了跟错星。 ## 播放模式(精度档 / 实时档)与进度条 UI 里 `播放模式` 下拉框两档,信息框第三行给出丢帧统计: | 模式 | 语义 | 实测(本视频 768x508@30fps,imgsz 416) | |---|---|---| | 逐帧播放(最高精度) | 每个解码帧都进算法,永不丢帧;`FrameRateController` 只在快于 33ms 时等待 | 已处理 44 / 丢帧 0 (0.0%) / 实际 11.8fps | | 原视频速度(测实时) | 贴源帧率的墙上时钟;算法跟不上就**读而不分析**地跳过,落后量计入丢帧 | 已处理 44 / 丢帧 104 (70.3%) / 落后峰值 39 帧 | 两档的差别只在"要不要为了赶时间扔掉帧":逐帧档验证算法上限,原速档暴露实时上限。CPU 单帧 ~85ms 时原速档必然丢七成;TensorRT INT8 档单帧 ~14ms,30fps 实时不再丢帧(见「CPU / GPU」一节的实测)。暂停/单帧步进在两档下语义一致:暂停时挂起时钟(等待时间不计入视频进度),单帧步进直接重锚调度,既不会被当成落后丢掉也不会多等一个间隔。 进度条可拖动:拖动即暂停并显示该帧**原始画面**(信息栏标注"预览(未分析)"),因为它绕过了算法;按 `开始` 会从该帧起重开一个分析会话(状态机以该帧为倒计时起点,`分析起点` 显示在信息框)。注意从褪色之后重启不会锁定目标——倒计时阶段的白填充是唯一的目标 ground truth,跳过半程就没有倒计时可用,这是设计而非缺陷。 ## 现有问题与限制(按严重度) ### 1. 【已修·2026-10 验证集迭代】f232 之后跟的是干扰星,且再没回来 原失败链:f231 目标轨迹只缺 1 帧测量,`_reanchor_target` 就把锚框交给旁边一条两百多帧的干扰星轨迹。根因是 ω 证书滑到 ~0 时 ±0.06 的容差等于全场接受,且证书为 `None` 的窗口(f1–f191)**完全没有证据门**。 当时在案的三条修法(grace / 来源过滤 / 证书下限)已随重叠身份重构全部落地,并按验证集实测升级为四道门:**grace**(缺测 ≤3 帧不交接)、**出生/近旁门**(`was_near_at`,取代朴素的 born-after:允许"丢失后新出现的 id"和"丢失时就在锚框 1.5 身位内的老轨迹"=blob 分裂,拒绝"远处飘来的老轨迹")、**群体门**(与干扰项群体速度 cos ≥ 0.8 直接拒——这是实测中分离度最大的判据:干扰项 +0.97 vs 目标 +0.11)、**证书下限**(|证书| < 0.02 退回 0.55 盲 IoU 判据)。10 场景验证集身份正确率 21.6% → 96.0%(见「验证集评估」一节)。 ### 2. 【未修·校准】倒计时窗口按墙上时间推算,比实际晚约 55 帧 `countdown_end_frame = 起播帧 + COUNTDOWN_DURATION_SEC(5.0) × fps`,而抽帧实测数字在 **≈f95** 就消失了(f60 是"3",f93/f94 的"1"正在溶解,f96 已无)。后果不是状态错误(TRANSITION 靠实测白填充判定,仍在 f192 正确落地),而是**把 ω 证书的空窗期从 3.2s 拉长到 6.4s**,正好覆盖倒计时收尾阶段两星并框的高发窗口——问题 1 的同源抖动都落在这段里。 ### 3. 【已修·验收口径】"目标在场"不等于跟对星 `tools/eval_valset.py` 以"跟随框与 GT IoU ≥ 0.5"为口径逐帧判定(见「验证集评估」一节);`summarize_csv.py` 的旧统计仅作运行期参考。 ### 4. 【设计限制】两颗星完全重合时身份不可判 只能等分离;此时按文档 §15.2 走 TARGET_LOST + 超时 GAME_OVER。 ### 5. 【设计限制】圆形目标没有旋转可观测量 签名调制深度接近 0,ω 判据自动退出(`omega_observable=False`),恢复改走位置/运动/面积。 ### 6. 【性能】CPU 原速档丢帧 70% CPU 单帧 ~85ms 是 30fps 预算的 2.5 倍,CPU 下只有逐帧档能一帧不丢。已修:TensorRT INT8 档 p50 ~14ms,实时不丢帧(见「CPU / GPU」一节)。 ### 7. 【未验证】CWG 采集卡(P6) `video_io/cwg_source.py` 按"采集卡在 Windows 上就是 DirectShow 设备"实现,`--camera ` 可直接验证,但**没有硬件时无法测**。若厂商 SDK 必需,只需替换 `CWGSource._open()`,下游全部沿用 `VideoSource` 接口。 ### 8. 【已知代价】身份绑定不依赖类别 实测所有图形都被判为 `pentagram`(3),类别不是可用判别特征(见上表),所以目标身份全部压在几何 + 白填充 + ω 上。