Zhejiang University RoboMaster Vision

自瞄框架

完整自瞄系统的整体框架与各模块衔接。

ANAN约 20 分钟
#自瞄#框架

本文将基于视觉组老资历的开源自瞄代码25哨兵自瞄开源,讲解HelloWorld的自瞄代码的基本框架,读到这里,我们认为你已经对ROS2,CMake,工业相机,PnP,Kalman滤波都有了一定的了解(当然不了解也完全没有关系)。

首先请阅读只做对的:从0到旋转平移靶80%命中率的自瞄指南

阅读完这篇文档,相信你已经对自瞄有了一定了解,又或许还是一头雾水,不过没关系,你并不需要完全掌握这些内容。

根据你现在了解的知识,你一定已经知道,自瞄的流程分为以下几个部分:

图像获取

通过工业相机(配合镜头、光源)连续采集图像,为每一帧打上时间戳,并用标定好的内参和外参提供后续解算所需的几何基础。

目标检测

在图像中找出“哪里有装甲板”。输入一帧图像,输出若干个候选目标的位置、角点、颜色与编号。

目标筛选

决定“打哪一个”。从多个检测结果中,结合敌我颜色、决策下发的优先级、血量与距离等因素选出唯一目标,并过滤误检与遮挡。目标选得好,操作手用起来会顺手很多。

位姿解算

PnP有关原理细节见前文,简单来说就是根据视野中的二维图像计算出装甲板的三维位置,注意算完后要转换到合适的坐标系。

状态估计与预测

对位姿观测进行时序滤波与运动建模。利用 Kalman 滤波平滑噪声、估计速度,并在目标旋转(小陀螺)时切换整车模型,预测弹丸到达时刻目标将出现的位置,补偿通讯与飞行延迟。

火控解算

将预测位置转换为云台可执行的控制量。计算 yaw/pitch 目标角度、叠加弹道下落与延迟补偿,并判断是否满足开火条件,最终通过串口发往下位机执行。

为什么是流水线?

本段含ai量极高,不看也无所谓

回顾上面六个步骤——图像获取、目标检测、目标筛选、位姿解算、状态估计与预测、火控解算——可以看到它们有一个共同特征:前一步的输出正好是后一步的输入。图像喂给检测器,检测结果喂给筛选器,筛选后的目标喂给 PnP,位姿喂给滤波器,预测位置喂给火控。整条链路是一条单向的数据流。

把一项工作拆成若干串行阶段、每段独立运行、用队列把相邻阶段衔接起来,这种结构就是流水线(pipeline),区别于把逻辑全部写进一个大函数从头调到尾的写法。在自瞄这个任务上,选择流水线主要有三个原因。

第一,各阶段耗时差异很大,串在一起会互相拖累。 相机取图只要 23ms,一次神经网络推理动辄 1530ms,PnP 解算几毫秒,卡尔曼更新不到 1ms。如果写成单线程顺序执行,相机必须等推理跑完才能取下一帧,整条链路的帧率被最慢的推理卡住——原本能跑 300FPS 的取图,被拖到 30FPS。流水线把每个阶段放到独立的执行流里,相机只管往队列里塞图,推理只管从队列里取图,彼此不阻塞。推理在处理第 N 帧时,相机已经在采集第 N+1 帧,滤波器在消化第 N-1 帧的结果。阶段之间靠队列解耦,整条线的吞吐由最慢阶段决定,但延迟不再逐段叠加,每个阶段都能跑在自己的最高频率上。

第二,自瞄对新鲜度和完整度的要求是矛盾的,流水线能在队列上做取舍。 云台控制希望每一帧都用最新的图像,不能因为上一帧还没处理完就让云台盯着过期数据;而滤波和预测又需要连续、对齐的时序。流水线的队列可以为这两种需求配置不同的策略:检测队列设成只保留最新一帧、满了就丢旧的(depth=1 的 QoS),让下游永远拿到最新观测;预测器内部则维护自己的滑窗,保证时序完整。单函数顺序执行时很难兼顾这两点,一旦阻塞,要么丢帧不可控,要么强行等齐拉高整体延迟。

第三,流水线让每个阶段独立可测、可换、可并行开发,这在工程上最重要。 识别器、解算器、预测器是三套完全不同的算法,往往由不同的人负责。如果揉在一个函数里,任何一处改动都可能牵连全局,调试时也只能整体跑一遍看效果。拆成流水线后,每一段有清晰的输入输出契约(给一帧图、还一组检测框),可以单独写测试、单独离线跑录制的视频、单独替换实现——今年把传统检测换成 YOLO,只要保证输出格式一致,下游一行代码都不用改。阶段之间的这种松耦合,是大型项目长期可维护的关键。

自瞄是一个多阶段、耗时不均、实时性强、多人协作的任务,流水线正好适合这类任务:用队列换来并行,用契约换来解耦,用丢帧策略换来实时性。理解了这一点,再去看任何一支队伍的自瞄代码,无论它用 ROS2 节点、C++ 线程池还是 Python 协程实现,都能看清它的骨架仍然是一条流水线。

以25哨兵自瞄开源为例

25哨兵自瞄开源每个步骤拆成一个独立的 ROS2 节点,节点之间用话题(topic)串联,整条自瞄链路就是一条由话题连接的流水线。

它的核心节点和相互关系如下(对应仓库 src/ 下的各个包):

autoaim_camera  ──image──▶  autoaim_detector  ──detections──▶  autoaim_selector

                                                          selected_detections

                              autoaim_predictor  ◀──poses──  autoaim_locator

                                 shoot_pos(云台目标 + 开火标志)

                                  下位机 / 电控

逐个对应到前文的六个步骤:

流水线阶段对应节点输入话题输出话题做的事
图像获取autoaim_cameraIMU 时间戳image_raw海康相机取图、配时间戳、发布图像
目标检测autoaim_detectorimage_rawdetectionsOpenVINO 推理,输出装甲板/符的角点与编号
目标筛选autoaim_selectordetectionsselected_detections按敌我颜色、编号优先级、血量过滤
位姿解算autoaim_locatorselected_detectionsposesPnP 解算三维位姿,转世界系
状态估计与预测autoaim_predictorposesshoot_pos卡尔曼滤波、轨迹预测、延迟补偿
火控解算(并入 predictorshoot_pos计算 yaw/pitch、判断开火,下发下位机

这条链路是真正的流水线,而不只是几个函数的堆叠,关键在三点设计:

本段为ai生成,稍微看看即可

1. 每个节点是独立的执行单元,靠话题解耦。autoaim_camera 为例,它内部起了一个独立的 capture_thread 死循环取图(见 camera_node.cppcapture_thread()),取到一帧就 camera_pub_.publish(image_msg) 发出去,不关心下游有没有人接收。autoaim_detector 则订阅 image_raw,在回调 img_callback 里推理完再发 detections。相机不会因为推理慢而阻塞取图,推理也不会因为相机快而被迫丢帧,两者各跑各的,话题就充当中间的队列。这正是前文说的阶段间用队列解耦、互不阻塞。

2. 用 QoS depth=1 实现只保最新帧的丢帧策略。 每个订阅都用了 rclcpp::QoS(1)(见 detector_node.cppimage_sub_ 的创建),也就是话题对应的队列深度为 1:推理还在处理上一帧时,相机又发来新帧,旧帧会被直接丢弃,下游永远拿到最新的图像。在自瞄场景里,一帧过期的图像对云台控制没有价值,与其排队处理不如立刻扔掉。同时 autoaim_predictor 内部的卡尔曼滤波器维护着自己的状态滑窗,保证时序预测的完整。新鲜度和时序这对矛盾,就由这两层策略分别处理。

3. 用可组合节点(composable node)加进程内通信避免拷贝开销。 图像是大块数据(640×384×3 ≈ 0.7MB),如果在节点间做真正的进程间消息序列化,拷贝和 IPC 的开销会吃掉流水线带来的并行收益。该项目通过 autoaim_launcher 把所有节点加载进同一个 component_container_mt,并利用 ROS2 的 intra-process comms(进程内通信),让同一进程内的发布者和订阅者直接传递指针、零拷贝。这样既保留了节点级流水线的解耦与并行,又不必付出进程间通信的代价,是把流水线做快的关键一步。

该项目还体现了流水线的可扩展性。autoaim_send_enemy 是一个旁路节点,从 detections 分叉出去,独立维护一个轻量跟踪器,把视野里有哪些敌人发给决策端,用于哨兵的自主目标选择;autoaim_recorder 是另一个旁路,从多个话题订阅数据做内录。这些挂在流水线上的支线节点,可以按兵种需要增删——步兵不需要自主决策,直接删掉 send_enemy 包即可,主链路一行不动。主干稳定、支线可裁的形态,正是阶段解耦带来的好处。

在本地把项目搭起来(学习用)

入门阶段,我们以这份 25 哨兵自瞄开源 为学习对象——先在自己电脑上把它搭到能编译,对照代码把前面讲的六个阶段一个个看明白。重点是读懂,不是急着跑通;完整运行需要相机和推理硬件,是学有余力再做的事。

具体的 clone、下载接口包、编译步骤,已经在 07-ROS2基础 第 3 章「先把参考项目搭起来」讲清楚了,这里只提三个要点:

  1. git clone 仓库到本地。
  2. 下载并放入接口包:项目靠自定义消息通信,但定义消息的 hw_sentry_interfaces 包没随仓库发布,缺了它编译不过。从 07 章下载我们补好的接口包,解压后放进项目的 src/ 目录。
  3. colcon build 编译autoaim_detectorautoaim_locator 依赖 OpenVINO,没装可以先 --packages-skip 跳过,不影响你读懂主链路。

搭好之后,建议按本文前面的流水线顺序,逐个打开 src/ 下每个包的 *_node.cpp,看它订阅什么话题、发布什么话题、在回调里做了什么——这就是把「六阶段」从文字落到代码的过程。

快速开始

本节及以后是上车部署的内容,面向已经能看懂项目、要让代码在车上跑起来的同学。刚入门只做本地学习的话,可以先略读。

参与自瞄代码维护、让代码能在 miniPC 上正常运行,并不需要你先完整掌握全部原理和实现细节(虽然长远看你应该掌握)。刚上手时对很多地方感到疑惑很正常,眼下最要紧的事就是把自瞄代码在车上跑通。

代码部署

通过ssh连接上小电脑,按照惯例老的电脑上都已经连接过HelloWorld_1897,密码是12345678,如果你嫌使用屏幕键鼠来给小电脑联网比较麻烦,可以把自己手机的热点设置成如上的网络,然后在手机上获取小电脑的ip,用ssh连接

用 git 或 scp 命令把代码拷到小电脑上,确认小电脑已装好 ROS 等所需环境后再编译(通常需要 5 分钟左右)。

与电控确认通信情况

老黄历,按规划今年视觉组将接管云台控制

了解通信相关代码的情况,比如收发的角度单位是度还是弧度、坐标系是否和电控侧一致,把这类约定逐项对齐。

调参

我将偷懒让ai写这部分,在真正上手实操之前,只需要略微了解这些内容即可

调参的总原则是:一次只改一个量,带着量化指标调,不要凭感觉。准备好 Foxglove(订阅 ROS2 话题实时看曲线,配合项目的 enable_print_state/enable_visualization_marker),准备好能稳定转动的靶子(平移靶、小陀螺、前哨站各一),每改一个参数就打一组弹统计命中率,否则很难判断这次改动是好是坏。

1. 相机参数(autoaim_camera/config/params.yaml

相机是整条流水线的源头,源头出错后面全错。这里要调的核心是曝光 exposure、增益 gain、伽马 gamma 三个参数,目标是让装甲板灯条在画面里亮但不溢出。

  • 曝光 exposure(单位 μs,默认 2500):首要参数。太暗,灯条和背景混淆、检测召回下降;太亮,灯条中心像素饱和(BGR 三通道都顶到 255),PnP 用的角点会偏移,位姿解算跟着发飘。判断标准:用 MVS 或 enable_labeled_image 看画面,装甲板灯条中心亮度落在 200~230 之间、没有大块纯白溢出区。
  • 增益 gain(默认 16.8):曝光不够时的补偿,代价是放大噪声。优先调曝光,曝光到上限还不够亮再加 gain,不要用 gain 硬拉暗画面。
  • 伽马 gamma(默认 0.5):调整整体明暗对比,影响灯条和装甲板底色的分离度。场地光照变了(场馆顶灯、太阳侧光)就要重调。
  • 帧率 frame_rate(默认 100):连续触发模式下生效。注意它要和推理速度匹配,推理跑不到 100FPS 时设再高也没用,反而让流水线队列频繁丢帧。

2. 检测器参数(autoaim_detector/config/params.yaml

检测器决定看到的装甲板有没有被识别出来。要调的主要是两个阈值:

  • confidence_threshold(默认 0.4):检测置信度门槛。调低,召回高但误检多(会把背景里的亮斑当装甲板送给下游);调高,误检少但容易漏检。自瞄场景下漏检比误检更麻烦,漏一帧滤波器还能补,持续漏检就会丢目标,所以一般偏向调低,让下游 selector 去兜底。
  • nms_threshold(默认 0.4):非极大值抑制 IOU 阈值。调低,重叠框更容易被合并(可能把两块相邻装甲板合成一个);调高,保留更多重叠框(误检增多)。默认 0.4 通常不用动。
  • device_name(默认 “NPU”):推理后端,可选 GPU/CPU/NPU。该项目面向 Intel GPU/NPU(如 AX650)做加速,没有对应硬件时退回 GPU 或 CPU,注意 CPU 推理会拖慢整条流水线。

判断检测器是否调好:对着静止装甲板,detections 话题稳定输出、没有闪烁;对着旋转靶,四块装甲板都能被持续检出(可以用 enable_labeled_image: true 录一段肉眼确认)。

3. 选择器参数(autoaim_selector/config/params.yaml

选择器决定多个装甲板里打哪个、什么时候切换目标。这里调的是目标切换的果断程度,以及对歪斜装甲板的过滤:

  • current_target_max_lost_frames(默认 50):当前目标连续丢失多少帧后彻底清除。注意 README 里那条警告:它必须大于 predictor 里的 max_temp_lost_frames(车 10、前哨站 30),否则 selector 已经放弃目标、predictor 还在预测,两边状态对不上。这是最容易踩的坑之一。
  • switch_new_target_appear_frames(默认 5):更高优先级的目标出现多少帧后才切换。调小,切换果断但容易被误检骗过去;调大,切换迟钝、打当前目标更稳定。这是决定操作手感的核心参数,要和操作手一起调。
  • big_armor_length_height_threshold / small_armor_length_height_threshold(默认 2.0 / 1.4):装甲板长宽比的下限,用来过滤太斜的装甲板。一块装甲板因透视显得长宽比偏低(接近正方形)时,说明它已经歪到不可靠,直接丢弃、不送给下游。注意这两个阈值不决定装甲板是大是小:大小判定来自检测器输出的 labelArmorType),PnP 据此选择对应的物理尺寸(is_big_armor 仅对 1 号步兵和基地为真,用大装甲板尺寸;哨兵/2/3/4/前哨站用小装甲板尺寸)。所以这两个阈值基本不用动,只有发现歪斜装甲板把 PnP 带歪时才考虑收紧。

4. 位姿解算(autoaim_locator/config/params.yaml

这里几乎不需要调,PnP 用的相机内参来自标定文件 camera_info.yaml,解算逻辑本身是确定性的。唯一要确认的是内参标定准不准。判断方法:把装甲板固定在已知距离(用尺量),看解算出来的距离对不对;转动装甲板,看 yaw 角和实际转角是否一致。解算飘了大多是标定问题,回去重标。

5. 预测器参数(autoaim_predictor/config/params.yaml + armor_tracker_params.yaml

这是调参的重头戏,也是最能拉开队伍差距的地方,分两层来调。

第一层:弹道与延迟补偿(params.yaml

这些参数把预测的命中点换算成实际发给云台的角度,单位全是 SI(m/s、s、rad):

  • bullet_speed(默认 24.0 m/s):弹速,必须和电控实际测的弹速一致,否则所有延迟补偿都错。赛前用测速枪量一下写进去。运行时电控也会从串口发过来(serial/bullet_speed),yaml 里只是调试默认值;注意代码里会对接收到的弹速做 [21, 25] 范围校验并 EMA 平滑,超出范围的值会被忽略,所以电控发的值要落在合理区间。
  • control_to_fire_time_armor / control_to_fire_time_buff(默认 0.03 / 0.047 s):从视觉发出指令到弹丸真正出膛这段延迟(通信、云台响应、拨弹),也就是控制到开火的时间。它和图像到当前的延迟是分开算的,代码里总预测时长是 now() - img_time + control_to_fire_time,前者补偿图像采集到处理完成的处理延迟,后者补偿指令下发到弹丸离膛的执行延迟。这是延迟补偿里最关键的参数:调小,预测点落后(打在目标身后);调大,预测点超前(打在目标前面)。调法:让靶子匀速横移,打一组看弹着点系统性偏前还是偏后,按偏差微调,直到弹着点不再偏向一边。
  • shoot_compensate_pitch / shoot_compensate_yaw(默认 0.015 / 0.007 rad):静态补偿量,用来修云台零点机械误差和枪管安装偏差。把机器人固定、对准静止靶打一组,弹着点系统性偏哪边就往反方向补。

第二层:整车观测器与卡尔曼滤波(armor_tracker_params.yaml

这里参数最多,核心围绕小陀螺(反陀螺)这一场景。状态机有 CONVERGING → TRACKING → TEMP_LOST → LOST 四态,参数控制各态的停留时长和切换条件:

  • max_converging_frames(车 10 / 前哨站 15):刚看到目标时进入 CONVERGING,累计这么多帧后才进入 TRACKING(滤波器收敛)。调小,锁定快但滤波还没稳;调大,锁定慢但更稳。
  • max_temp_lost_frames(车 10 / 前哨站 30):目标短暂消失时进入 TEMP_LOST,靠预测撑这么多帧,超过则转 LOST。调小,丢目标快、重新锁定频繁;调大,抗遮挡强但目标真跑了还会空打。记住要小于 selector 的 current_target_max_lost_frames
  • enter_antispin_palstance / exit_antispin_palstance(默认 2.5 / 1.0 rad/s):判定目标开始、停止小陀螺的角速度阈值。进入用高阈值(2.5,避免正常转弯误判),退出用低阈值(1.0,避免抖动反复横跳),这是迟滞设计。判定进入反陀螺后,预测器切换到整车圆周模型。
  • antispin_shoot_angle(默认 0.35 rad):反陀螺模式下,装甲板转到多大角度窗口内才开火。调小,只在装甲板正对时开火,命中率高但射速低;调大,侧着也打,射速高但命中率降。这是命中率和击杀时间之间的核心权衡。
  • initial_radius(默认 0.26 m):整车圆周模型初始半径,对应常规步兵两装甲板间距。偏离实际半径会让反陀螺预测点系统性偏前或偏后。
  • switch_armor_angle(默认 1.0 rad):主要观测装甲板切换的触发角度。四装甲板每隔约 π/2 切换一次,1.0 rad 是合理默认值。
  • 滤波噪声参数 process_noise / measurement_noise:每组卡尔曼滤波都有这两个矩阵。过程噪声大,滤波更相信观测、少相信预测模型(响应快、抖动大);测量噪声大,更相信模型、少相信观测(平滑但滞后)。调法:先固定其中一个看曲线——目标静止时估计位置抖不抖(抖就加测量噪声),目标加速时估计跟不跟得上(跟不上就加过程噪声)。两者此消彼长,没有完美值,只有具体场景下的折中。kf_main_observing_armor 的位置过程噪声(2e+1)比速度项(1e-0)大一个量级,是因为速度项更可信,这是该项目的调参经验,可作为起点。

调参流程建议

把上面零散的参数串成一个可操作的流程:

  1. 先把源头调准:相机曝光、弹速、静态补偿。这些是地基,错了后面全白调。
  2. 静止靶打准:固定靶打一组,调 shoot_compensate_* 让弹着点不偏,验证标定、PnP 和静态补偿这条链路。
  3. 匀速横移靶:调 control_to_fire_time 让弹着点不前不后,验证延迟补偿。
  4. 小陀螺:调 antispin_**_shoot_angle 和滤波噪声。这一步最难也最关键,量化指标是击杀时间。
  5. 前哨站:前哨站转速高、可重复,是检验反陀螺参数的好靶子,而且 outpost_observer 有独立的一组参数要单独调。
  6. 回归测试:每改一个参数,把第 2 到 5 步全过一遍,确认没把别的场景调坏。

这里是以25 哨兵自瞄开源为例,实际参数情况请自行阅读当前使用的代码。

参考资料

25 哨兵自瞄开源、上海科技大学 2026 视觉技术方案,站内 07-ROS2基础、11-工业相机、12-相机标定与PnP、13-Kalman滤波、15-上下位机通讯。

返回入组培训
|