前视声呐目标识别定位(二)- YOLO与ROS2融合的实时识别定位引擎

📅 发布时间:2026/7/7 22:32:01 👁️ 浏览次数:
前视声呐目标识别定位(二)- YOLO与ROS2融合的实时识别定位引擎
1. 从“看见”到“知道在哪”为什么需要YOLOROS2大家好我是老陈在AI和机器人领域摸爬滚打了十几年。今天咱们接着聊前视声呐的目标识别定位。上一期我们聊了基础原理知道了声呐图像里一个亮点可能代表水下的一根柱子、一块礁石或者一条鱼。但光“看见”亮点还不够对于水下机器人来说关键是要“知道”这个目标是什么以及它“在哪里”。这就像你开车摄像头告诉你前面有个东西识别但你还得知道它离你多远、在路的左边还是右边定位才能决定是绕过去还是刹车。水下环境更复杂可见光几乎没用声呐就是机器人的“眼睛”。而我们要做的就是给这双“眼睛”装上大脑让它能实时看懂声呐图像并报出目标的位置。这个“大脑”就是咱们今天要搭建的实时识别定位引擎。它的核心思路非常直接用当下最流行、速度最快的目标检测算法YOLOv5来“认东西”用机器人领域最主流的通信框架ROS2来“传消息”。两者一结合一个稳定、高效、能实时跑在机器人上的感知模块就诞生了。我选择YOLOv5是因为它在精度和速度之间取得了非常好的平衡特别适合对实时性要求高的机器人应用。你总不希望机器人看到障碍物后算半天才告诉你要转向吧而ROS2尤其是Humble版本提供了可靠的通信机制、节点管理以及丰富的工具链能让我们的识别模块像乐高积木一样轻松地和机器人的其他部分比如控制系统、导航模块拼接在一起。简单来说这个模块的输入是一张张来自声呐的“扇形”图像输出则是一个个结构化的信息“前方10米左偏15度发现一个‘沉船’目标置信度92%”。有了这些信息机器人就能自主规划路径、执行任务了。接下来我就带你一步步把这个引擎搭建起来我会把我在实际项目中踩过的坑、优化的技巧都分享出来。2. 引擎核心设计消息、数据流与解算逻辑在动手写代码之前咱们得先把整个系统的“图纸”画好。一个健壮的软件模块清晰的数据流和接口定义是成功的一半。这里的设计思路很多都是我在实际项目中反复迭代优化后的结果。2.1 定义“目标”的身份证自定义ROS2消息在ROS2的世界里节点之间通过“消息”来聊天。我们首先要定义一种专用的“语言”用来描述声呐发现的目标。这就像给每个目标发一张“身份证”上面要写明关键信息。在原始文章里作者定义了两个消息ObjectAzimuthRange.msg单个目标和ObjectsAzimuthRange.msg一帧图像中的所有目标。这个设计非常经典我基本沿用了下来但可以做一些更细致的解释和补充。# ObjectAzimuthRange.msg string class_name # 目标是什么比如“rov”水下机器人、“pipe”管道 float64 probability # 有多确信比如0.92代表92%的置信度 float64 object_azimuth # 水平开角度目标在声呐视野的左还是右负值左正值右 float64 object_range # 距离米目标离我们多远 int64 xmin, ymin, xmax, ymax # 目标框在图像中的像素坐标用于可视化或后续处理为什么需要这些字段class_name和probability来自YOLO的识别结果。而object_azimuth和object_range则是我们系统的核心价值所在是通过声呐图像几何关系解算出来的真实世界位置。xmin, ymin等框坐标非常有用比如你可以用它把识别结果画回原图看看效果或者在后期需要更精细分析目标时能回溯到图像的原始区域。而ObjectsAzimuthRange.msg就是一个“数组”或“列表”包含一个标准ROS消息头std_msgs/Header有时间戳和坐标系信息和一堆ObjectAzimuthRange。这样发布者一次就能发出一帧里所有的目标订阅者处理起来也非常方便。2.2 声呐图像的“隐形”参数巧用Frame_id这是一个非常巧妙且实用的设计点也是新手容易困惑的地方。声呐图像本身是一个二维像素矩阵但要知道每个像素点对应真实世界中的哪个位置我们必须知道拍摄这张图像时声呐的两个关键参数水平视场角和最大量程。比如一个120度视场角、量程50米的声呐图像中心点对应正前方最左侧像素对应左偏60度最右侧像素对应右偏60度图像最顶部像素对应50米处最底部对应0米声呐本身。那么这两个参数怎么随着图像一起传递呢最规范的做法是定义一个全新的自定义消息比如SonarImage.msg里面包含图像数据、视场角、量程。但这需要所有用到图像的节点都去理解这个新消息增加了复杂度。原始文章作者采用了一种更快捷、对现有工具链更友好的“Hack”方法利用ROS标准图像消息sensor_msgs/Image中header.frame_id字段。这个字段原本是用来指定坐标系名称的比如“sonar_frame”。我们可以把它“借用”过来存储参数字符串。例如视场角120度量程10米就写成frame_id: “[120 10]”。这样做的好处是任何能处理标准图像消息的工具如rqt_image_view、ros2 bag record都能直接使用我们的数据无需修改。在识别节点里我们只需要从frame_id中解析出这两个数字即可。当然这算是一种对字段的“超常规”使用在更大型、规范的系统中我建议还是定义专属消息。但对于我们快速原型开发和验证来说这个方法非常高效。2.3 位置解算从像素坐标到真实世界这是整个定位环节的数学核心原理不复杂但必须理解透彻。我们已知图像宽度image_width像素高度image_height像素。声呐水平总视场角sonar_azimuth度最大量程sonar_range米。目标框的中心点像素坐标(object_x, object_y)。水平开角解算我们把图像宽度映射到整个水平视场角。假设图像中心点object_x image_width / 2对应正前方开角为0度。那么目标像素相对于中心点的归一化偏移为(object_x / (image_width / 2.0)) - 1.0。这个值的范围是[-1, 1]-1对应图像最左视场角最左1对应最右。最后乘以半个视场角(sonar_azimuth / 2.0)就得到了目标相对于正前方的水平开角object_azimuth。结果为负表示目标在左侧为正则在右侧。距离解算我们把图像高度映射到整个量程。通常声呐图像顶部object_y 0对应最远距离sonar_range底部object_y image_height对应最近距离0米。因此归一化的高度比例为1.0 - object_y / image_height。在顶部时为1底部时为0。直接乘以最大量程sonar_range就得到了目标的直线距离object_range。用代码表示就是核心的那两行object_azimuth (object_x / (image_width / 2.0) - 1.0) * (sonar_azimuth / 2.0) object_range (1.0 - object_y / image_height) * (sonar_range)这里一定要注意坐标原点的定义图像左上角通常是(0,0)和映射关系不同的声呐或图像处理库可能有差异需要根据实际情况调整。我在第一次集成时就因为映射关系搞反了导致算出的距离全是错的机器人对着远处的目标猛冲教训深刻。3. 手把手搭建YOLOv5与ROS2 Humble的深度集成理论说清楚了咱们就来真刀真枪地干。这一部分我会带你一步步把YOLOv5模型“塞进”ROS2节点里并处理好所有的数据流。我假设你已经有一个安装好ROS2 Humble和基本Python开发环境的Ubuntu系统。3.1 创建ROS2工作空间与功能包首先为我们的项目安个家。mkdir -p ~/fls_ws/src cd ~/fls_ws/src接着创建一个新的ROS2功能包。这里我依赖rclpyROS2的Python客户端库和sensor_msgs标准图像消息。ros2 pkg create yolov5_humble_fls --build-type ament_python --dependencies rclpy sensor_msgs cv_bridge std_msgs cd yolov5_humble_flscv_bridge是关键它负责在ROS的sensor_msgs/Image和OpenCV的numpy数组之间进行转换是我们连接图像处理和ROS的桥梁。3.2 集成YOLOv5模型我们需要把YOLOv5的代码放到我们的功能包里。推荐使用Ultralytics官方维护的v7.0版本比较稳定。cd ~/fls_ws/src/yolov5_humble_fls git clone -b v7.0 https://github.com/ultralytics/yolov5.git这样yolov5目录就和我们的Python包目录并列了。接下来安装YOLOv5所需的Python依赖。建议在功能包目录下创建一个requirements.txt文件列出核心依赖torch1.7.0 torchvision0.8.0 opencv-python4.5.0 numpy1.18.5 PyYAML5.3.1 tqdm4.41.0然后使用pip安装pip install -r requirements.txt。注意如果你有NVIDIA显卡并希望使用GPU加速请确保安装的是CUDA版本的PyTorch这能极大提升推理速度。3.3 编写核心识别节点yolov5_sonar.py这个文件是我们的“大脑”。我把它放在yolov5_humble_fls/yolov5_humble_fls/目录下。下面我拆解关键部分来讲。初始化与参数加载节点启动时我们需要从ROS2的参数服务器读取配置比如模型路径、置信度阈值、订阅的图像话题等。这比把参数硬编码在代码里要灵活得多。import rclpy from rclpy.node import Node import cv2 from cv_bridge import CvBridge import torch from yolov5.models.common import DetectMultiBackend from yolov5.utils.general import non_max_suppression, scale_boxes class YOLOv5SonarNode(Node): def __init__(self): super().__init__(yolov5_sonar_node) self.declare_parameters( namespace, parameters[ (yolov5_path, ), (weight_path, ), (image_topic, /sonar/image_raw), (pub_topic, /sonar/objects), (conf_thres, 0.5), (use_cpu, False), (display_image, True) ]) # 获取参数 self.weight_path self.get_parameter(weight_path).value self.conf_thres self.get_parameter(conf_thres).value # ... 其他参数获取 # 初始化CV桥、YOLO模型、发布器等 self.bridge CvBridge() self.load_model() self.publisher self.create_publisher(ObjectsAzimuthRange, self.pub_topic, 10) self.subscription self.create_subscription(Image, self.image_topic, self.image_callback, 10)图像回调与参数解析当有新的声呐图像到来时这个函数被触发。首先将ROS图像消息转换为OpenCV格式然后从frame_id中解析出声呐参数。def image_callback(self, msg): try: cv_image self.bridge.imgmsg_to_cv2(msg, desired_encodingbgr8) # 解析frame_id中的声呐参数例如“[120 10]” sonar_info msg.header.frame_id # 去除括号按空格分割 sonar_info sonar_info.strip([]) parts sonar_info.split() if len(parts) 2: sonar_azimuth float(parts[0]) # 水平视场角单位度 sonar_range float(parts[1]) # 最大量程单位米 else: self.get_logger().warn(Frame_id format error, using default parameters.) sonar_azimuth 120.0 sonar_range 50.0 # 进行目标检测和位置解算 results self.detect(cv_image) objects_msg self.process_results(results, cv_image, sonar_azimuth, sonar_range) self.publisher.publish(objects_msg) if self.display_image: self.show_detection(cv_image, results, objects_msg) except Exception as e: self.get_logger().error(fError in image callback: {e})目标检测与位置解算这是调用YOLO模型和进行几何计算的核心。self.detect(cv_image)内部会调用加载好的YOLO模型进行推理并返回检测框。在process_results函数中我们遍历每一个检测到的框def process_results(self, detections, cv_image, sonar_azimuth, sonar_range): height, width cv_image.shape[:2] objects_msg ObjectsAzimuthRange() objects_msg.header.stamp self.get_clock().now().to_msg() objects_msg.header.frame_id sonar_frame # 设定坐标系 for *xyxy, conf, cls in detections: # 计算边界框中心点 x_center (xyxy[0] xyxy[2]) / 2.0 y_center (xyxy[1] xyxy[3]) / 2.0 # 位置解算核心公式 object_azimuth (x_center / (width / 2.0) - 1.0) * (sonar_azimuth / 2.0) object_range (1.0 - y_center / height) * sonar_range # 填充单个目标消息 obj ObjectAzimuthRange() obj.class_name self.model.names[int(cls)] # 获取类别名 obj.probability float(conf) obj.object_azimuth object_azimuth obj.object_range object_range obj.xmin, obj.ymin, obj.xmax, obj.ymax map(int, xyxy) objects_msg.object_azimuth_range.append(obj) return objects_msg可视化可选但重要在开发调试阶段把识别和定位结果实时显示出来至关重要。我们可以用OpenCV在原图上画出检测框并把计算出的距离和开角标注上去。def show_detection(self, image, detections, objects_msg): for i, det in enumerate(detections): *xyxy, conf, cls det obj_info objects_msg.object_azimuth_range[i] label f{obj_info.class_name} {conf:.2f}: ({obj_info.object_azimuth:.1f}°, {obj_info.object_range:.1f}m) # 画矩形框 cv2.rectangle(image, (int(xyxy[0]), int(xyxy[1])), (int(xyxy[2]), int(xyxy[3])), (0, 255, 0), 2) # 添加文本标签 cv2.putText(image, label, (int(xyxy[0]), int(xyxy[1])-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) cv2.imshow(YOLOv5 Sonar Detection, image) cv2.waitKey(1) # 必须要有否则窗口不更新3.4 配置与启动让节点跑起来我们需要创建两个关键的配置文件yolov5_humble_fls.yaml参数配置和start.launch.py启动文件。参数配置文件 (config/yolov5_humble_fls.yaml):/yolov5_humble_fls: ros__parameters: yolov5_path: /home/your_user/fls_ws/src/yolov5_humble_fls/yolov5 weight_path: /home/your_user/fls_ws/src/yolov5_humble_fls/best.pt # 你的训练权重 image_topic: /sonar/image_raw pub_topic: /sonar/detected_objects conf_thres: 0.6 # 置信度阈值高于此值才认为是目标 use_cpu: false # 如果为true强制使用CPU否则尝试用GPU display_image: true启动文件 (launch/start.launch.py):启动文件可以方便地设置节点、加载参数是ROS2中的标准做法。from launch import LaunchDescription from launch_ros.actions import Node from ament_index_python.packages import get_package_share_directory import os def generate_launch_description(): config os.path.join( get_package_share_directory(yolov5_humble_fls), config, yolov5_humble_fls.yaml ) yolov5_sonar_node Node( packageyolov5_humble_fls, executableyolov5_sonar_node, nameyolov5_sonar_node, outputscreen, parameters[config] ) return LaunchDescription([ yolov5_sonar_node ])最后别忘了在setup.py中注册节点入口点和安装配置文件然后编译工作空间cd ~/fls_ws colcon build --packages-select yolov5_humble_fls source install/setup.bash4. 实战测试从数据包到可视化结果模块搭好了是骡子是马得拉出来溜溜。测试是验证我们工作最关键的一步。这里我提供两种测试方法用录制好的数据包进行离线回放测试以及连接真实的声呐进行在线测试。4.1 离线回放测试快速验证流程对于没有真实声呐硬件或者想快速验证算法流程的朋友使用ROS2 bag数据包进行回放是最佳选择。原始文章的作者提供了一个测试数据包里面包含了模拟的声呐图像流图像帧的frame_id已经按照我们的约定写入了参数。第一步播放数据包。假设你已经下载了sonar_mono.bag或其他名称数据包并放在了~/bags目录下。ros2 bag play ~/bags/sonar_mono.bag --loop--loop参数会让数据包循环播放方便反复观察。第二步启动我们的识别节点。打开一个新的终端激活ROS2环境并启动节点。cd ~/fls_ws source install/setup.bash ros2 launch yolov5_humble_fls start.launch.py如果一切正常你应该能在终端看到节点启动的日志并且弹出一个OpenCV窗口实时显示声呐图像以及用绿色框标出的检测目标框的旁边会标注类别、置信度、计算出的水平开角和距离。第三步查看发布的目标信息。再打开一个终端我们可以订阅节点发布的目标话题看看具体的数据。ros2 topic echo /sonar/detected_objects你会看到一串结构化的数据流出来里面包含了每个目标的详细信息。这证明我们的整个数据链路——从图像订阅、解析、检测、解算到发布——已经完全打通了。4.2 在线真实测试连接你的声呐离线测试通过后就可以挑战真实环境了。这一步需要你有一个能输出图像的前视声呐并且已经有一个ROS2节点在发布sensor_msgs/Image话题。这个节点可能是声呐厂商提供的驱动也可能是你自己写的。关键调整修改话题名确保你的识别节点订阅的话题名image_topic参数与声呐驱动节点发布的话题名一致。比如你的声呐驱动发布的话题是/blueview/image那么你就需要修改yolov5_humble_fls.yaml中的image_topic为/blueview/image。核对参数格式确保你的声呐驱动节点按照约定将水平视场角和量程以[azimuth range]的格式写入每帧图像的header.frame_id中。这是位置解算正确的基石。如果驱动没有这个功能你可能需要修改驱动代码或者通过一个小的“翻译”节点来为图像消息添加正确的frame_id。模型适配你离线测试用的模型best.pt是在特定数据集上训练的。如果真实水下的目标比如某种特定的海草、岩石不在训练集中识别效果可能会下降。这就需要你收集真实数据对YOLO模型进行微调训练这是一个迭代的过程。我刚开始做水下项目时用一个在通用数据集上训练的模型把珊瑚礁识别成了“船”闹了不少笑话。4.3 性能调优与踩坑经验在实际部署中你可能会遇到性能或精度问题。这里分享几个我踩过的坑和解决办法1. 实时性不够帧率低检查硬件首先确认是否使用了GPU进行推理。在参数文件中设置use_cpu: false并确保系统安装了CUDA和对应版本的PyTorch。GPU推理通常比CPU快几十倍。优化图像尺寸YOLO模型默认的输入尺寸是640x640。如果原始声呐图像很大如2000x1000缩放过程会消耗时间。你可以尝试在预处理阶段将图像缩放到更小的尺寸如320x320但要注意这会降低小目标的检测精度需要权衡。简化模型YOLOv5提供了不同大小的模型n, s, m, l, x。yolov5n纳米级模型速度最快但精度稍低yolov5x精度高但速度慢。根据你的机器人的算力比如是Jetson Nano还是高性能工控机选择合适的模型。2. 定位不准距离或角度偏差大校准声呐参数frame_id里的视场角和量程必须绝对准确。有些声呐在不同量程档位下实际视场角会有微小变化。最好通过实测数据对这两个参数进行校准。比如让机器人对着一个已知距离的固定目标看解算出的距离是否匹配。检查图像畸变有些声呐的图像可能存在几何畸变不是完美的扇形。这会导致我们的线性映射公式产生误差。如果精度要求极高可能需要先对声呐图像进行畸变校正然后再应用我们的解算公式。目标框中心点我们默认用检测框的中心点代表目标位置。这对于点状目标如浮标是合适的。但对于大型、不规则目标如一片礁石中心点可能不代表其“特征点”的位置。这时可能需要更复杂的策略比如用框的底部中心假设目标接触海底。3. 虚警太多或漏检严重调整置信度阈值conf_thres参数是控制检测“松紧度”的阀门。调高它如0.7可以减少虚警但可能漏检模糊目标调低它如0.3可以增加检出率但虚警也会变多。需要根据实际场景在测试数据上反复调整找到一个平衡点。重新训练模型这是治本的方法。用你实际作业水域的数据包含各种目标、不同距离、不同角度去微调YOLO模型。哪怕只有几百张精心标注的图像也能让模型的性能在该场景下大幅提升。数据标注可以使用LabelImg等工具记得标注框要尽可能紧贴目标。把YOLO和ROS2这样深度集成构建出一个稳定运行的实时感知模块是让水下机器人真正拥有“智能眼睛”的关键一步。这个过程就像搭积木把视觉算法、机器人中间件、传感器数据流这些模块严丝合缝地拼接在一起。我至今还记得第一次看到机器人根据这个模块的输出自动绕开障碍物时的兴奋感。当然每个水下环境都是独特的你可能需要根据你的声呐型号、目标类型和任务需求对这里的每一个步骤进行微调和优化。希望这份详细的指南能帮你少走弯路更快地让你的水下伙伴“看”得更清楚“想”得更明白。如果在实际操作中遇到具体问题不妨多看看ROS2节点的日志输出那里面往往藏着解决问题的线索。