"""demo_track_control.py —— 视觉跟踪控制链路集成演示(胶水层)。 对应 C++ 参考实现 reference-c++/visual_track_control/examples/track_control_demo.cpp。 串联三个组件(target_tracker 与 visual_track_control 互不 import, 本脚本是唯一的转换点): 模拟检测序列 → TargetTracker(8 维恒速卡尔曼多目标追踪) → TrackResult 转 TrackState(解耦边界) → TrackingControlLaw(三通道控制律 + 限幅 + 状态机) → CommandSender(mock 记录 / MAVSDK offboard 实发) 场景时间线(20 Hz,--speedup 可加速): 0~ 6s 正常跟踪:目标匀速横移 + 双目距离 30→10 m 前向逼近 6~10s 目标消失:追踪器 Coast 预测(60 帧 = 3 s 后轨道判丢, 控制律自动切换零速刹车) 10~13s 持续判丢:维持零速刹车 用法: python utils/demo_track_control.py # mock 传输 python utils/demo_track_control.py --transport mavsdk # MAVSDK(需飞控/SITL) python utils/demo_track_control.py --speedup 4 # 4 倍速 """ import argparse import asyncio import logging from pathlib import Path from target_tracker.target_tracker import Detection, TargetTracker, TrackerConfig from visual_track_control.config import load_from_json from visual_track_control.mavsdk_gateway import MavsdkCommandGateway from visual_track_control.tracking_control_law import TrackingControlLaw from visual_track_control.types import (ControlMode, ControlSnapshot, ControlOutput, TrackState) logger = logging.getLogger("demo") _DEMO_DIR = Path(__file__).resolve().parent _DEFAULT_CONFIG = _DEMO_DIR / "config" / "track_control.json" # 模拟场景参数 _NORMAL_SECS = 6.0 # 正常跟踪阶段 _COAST_SECS = 4.0 # 目标消失 → Coast(含轨道判亡) _LOST_SECS = 3.0 # 判丢后持续刹车阶段 _DEMO_MAX_LOST_FRAMES = 60 # 追踪器 Coast 窗口(20 Hz 下 = 3 s) _STATUS_EVERY_TICKS = 10 # 每 0.5 s 打印一行状态 def track_result_to_state(result) -> TrackState: """两包之间的唯一转换点:TrackResult(box 为对象)→ TrackState(plain tuple)。""" box = result.box raw = result.raw return TrackState( tracked=result.tracked, primary_id=result.primary_id, center=result.center, box=(box.x1, box.y1, box.x2, box.y2) if box is not None else None, confidence=result.confidence, is_predicted=result.is_predicted, lost_frames=result.lost_frames, n_active=result.n_active, raw=(raw.x1, raw.y1, raw.x2, raw.y2) if raw is not None else None, ) def simulate_detections(tick: int, normal_ticks: int, period_s: float): """场景检测源:正常阶段目标匀速横移,之后消失。""" if tick >= normal_ticks: return [] cx = 960.0 + 3.0 * tick # 3 px/帧 匀速右移 cy = 540.0 w, h = 120.0, 160.0 return [Detection(cx - w / 2.0, cy - h / 2.0, cx + w / 2.0, cy + h / 2.0, 0.9)] def simulate_distance(tick: int, normal_ticks: int, period_s: float): """场景双目距离源:正常阶段有效、距离 30→10 m;之后无效。""" if tick >= normal_ticks: return 0.0, False distance = max(10.0, 30.0 - tick * period_s * 3.0) return distance, True class MockCommandSender: """CommandSender 的 mock 实现:记录最近一条指令描述(无实机验证能力)。""" def __init__(self) -> None: self.last_desc = "-" async def start(self) -> bool: logger.info("[MockTransport] offboard 启动(模拟)") return True async def send_velocity_heading(self, output: ControlOutput, time_boot_ms=None) -> bool: self.last_desc = (f"vx={output.vx_mps:+.2f} vy={output.vy_mps:+.2f} " f"vz={output.vz_mps:+.2f} " f"yaw_rate={output.yaw_rate_dps:+.2f}°/s " f"yaw={output.yaw_deg:+.2f}°") return True async def send_brake_hover(self, time_boot_ms=None) -> bool: self.last_desc = "零速刹车(全零机体速度设定点)" return True async def stop(self) -> None: logger.info("[MockTransport] offboard 停止(模拟)") async def make_sender(args) -> object: if args.transport == "mock": return MockCommandSender() from mavsdk import System drone = System() logger.info("连接飞控 %s ...", args.conn) await drone.connect(system_address=args.conn) async for state in drone.core.connection_state(): if state.is_connected: logger.info("飞控已连接") break return MavsdkCommandGateway(drone) async def dispatch(sender, out: ControlOutput, now_ms: int): """按模式分发指令;DEGRADED_HOLD 不发送(禁止正常控制)。""" if out.mode is ControlMode.VELOCITY_HEADING and out.valid: return await sender.send_velocity_heading(out, now_ms) if out.mode is ControlMode.BRAKE_HOVER and out.valid: return await sender.send_brake_hover(now_ms) return None async def run_demo(args) -> None: cfg = load_from_json(args.config) period_s = 1.0 / cfg.control.frequency_hz period_ms = int(round(period_s * 1000.0)) tracker = TargetTracker(TrackerConfig(max_lost_frames=_DEMO_MAX_LOST_FRAMES)) law = TrackingControlLaw(cfg) sender = await make_sender(args) normal_ticks = int(round(_NORMAL_SECS / period_s)) coast_ticks = int(round(_COAST_SECS / period_s)) lost_ticks = int(round(_LOST_SECS / period_s)) total_ticks = normal_ticks + coast_ticks + lost_ticks if not await sender.start(): logger.error("offboard 启动失败,退出") return logger.info("开始 %d 拍(%.1f s,%.1f Hz):正常 %.1fs → Coast %.1fs → 判丢 %.1fs", total_ticks, total_ticks * period_s, cfg.control.frequency_hz, _NORMAL_SECS, _COAST_SECS, _LOST_SECS) try: for tick in range(total_ticks): now_ms = 1000 + tick * period_ms detections = simulate_detections(tick, normal_ticks, period_s) distance_m, distance_valid = simulate_distance(tick, normal_ticks, period_s) result = tracker.update(detections) snap = ControlSnapshot( track=track_result_to_state(result), track_time_ms=now_ms, # 每拍取样即打戳(视觉始终新鲜) target_distance_m=distance_m, distance_valid=distance_valid, distance_time_ms=now_ms, attitude_present=True, attitude_time_ms=now_ms, # 模拟姿态源持续可用 yaw_rad=0.0, # 机头朝北(NED 旋转恒等) ) out = law.update(snap, now_ms) await dispatch(sender, out, now_ms) if tick % _STATUS_EVERY_TICKS == 0: logger.info( "t=%5.2fs 检测=%d 追踪[trk=%d pred=%d lost=%d] | " "[%s] vx=%+.2f vy=%+.2f vz=%+.2f yaw_rate=%+.2f coast=%d | 发送: %s", (tick * period_s), len(detections), result.tracked, result.is_predicted, result.lost_frames, out.mode.name, out.vx_mps, out.vy_mps, out.vz_mps, out.yaw_rate_dps, out.coast_active, sender.last_desc) await asyncio.sleep(period_s / args.speedup) finally: await sender.stop() logger.info("演示结束") def main() -> None: parser = argparse.ArgumentParser(description="视觉跟踪控制链路集成演示") parser.add_argument("--transport", choices=["mock", "mavsdk"], default="mock", help="指令发送方式:mock=本地记录,mavsdk=offboard 实发") parser.add_argument("--conn", default="udp://:14540", help="MAVSDK 连接串(仅 --transport mavsdk 生效)") parser.add_argument("--speedup", type=float, default=1.0, help="时间倍速(>1 加速演示)") parser.add_argument("--config", default=str(_DEFAULT_CONFIG), help="控制律配置文件路径") args = parser.parse_args() logging.basicConfig(level=logging.INFO, format="%(asctime)s.%(msecs)03d %(levelname)s %(message)s", datefmt="%H:%M:%S") asyncio.run(run_demo(args)) if __name__ == "__main__": main()