在GAZEBO中使用PX4+YOLOv5实现目标跟踪

任务流程

1、在ubuntu20.04环境下部署Yolov5算法;
2、创建目标检测节点,订阅图像信息、调用Yolov5进行识别、并发布识别结果;
3、创建目标跟踪节点,订阅识别结果、控制无人机进行目标跟踪。
4、关于px4速度控制坐标系的一些补充

1、部署Yolov5

本机的环境是ubuntu20.04和ros noetic,默认使用python3.8,为了使用方便,我们需要安装对应版本的Yolov5,否则在使用时需要频繁更换配置虚拟环境,对小白不太友好。
具体安装流程参考官方文档,相当的宝宝巴士:https://varhowto.com/install-pytorch-ubuntu-20-04/
Yolov5需要依赖GPU才能获得较好的性能,建议安装带GPU加速的版本,跟着官方教程安装完后,对Pytorch和Anaconda都会有一定程度的认识和了解。

2、目标识别

该部分内容参考了博客https://blog.csdn.net/cy1641395022/article/details/132237482
新建ros工作空间

mkdir -p ~/yolov5_ws/src
cd ~/yolov5_ws
catkin_make

创建新的ros package


然后进行编译

cd ~/yolov5_ws
catkin_make
source devel/setup.bash

创建scripts目录,并创建python脚本

cd ~/yolov5_ws/src/yolov5_ros
mkdir scripts
cd scripts
touch detect_ros.py
chmod +x detect_ros.py

随后粘贴以下代码到detect_ros.py文件中

#! /usr/bin/env python3
# -*- coding: utf-8 -*-

import rospy
import cv2
import torch
import numpy as np
import warnings
from functools import partial
from cv_bridge import CvBridge, CvBridgeError

from std_msgs.msg import Header
from geometry_msgs.msg import Twist
from sensor_msgs.msg import Image, RegionOfInterest
from yolov5_ros.msg import BoundingBox, BoundingBoxes

class Yolov5Param:
    def __init__(self):
        # load local repository(YoloV5:v6.0)
        # 指定yolov5的源码路径,位于/home/rui27/yolov5/
        yolov5_path = rospy.get_param('/yolov5_path', '')
        # 指定yolov5的权重文件路径,位于/home/rui27/yolov5/yolov5s.pt
        weight_path = rospy.get_param('~weight_path', '')
        # yolov5的某个参数,这里不深究了
        conf = rospy.get_param('~conf', '0.5')
        # 使用pytorch加载yolov5模型,torch.hub.load会从/home/rui27/yolov5/中找名为hubconf.py的文件
        # hubconf.py文件包含了模型的加载代码,负责指定加载哪个模型
        self.model = torch.hub.load(yolov5_path, 'custom', path=weight_path, source='local')
        # 一个参数,用来决定使用cpu还是gpu,这里我们使用gpu
        if (rospy.get_param('/use_cpu', 'false')):
            self.model.cpu()
        else:
            self.model.cuda()
        self.model.conf = conf

        # target publishers
        # BoundingBoxes是本样例自定义的消息类型,用来记录识别到的目标
        # 使用/yolov5/targets topic发布出去
        self.target_pub = rospy.Publisher("/yolov5/targets",  BoundingBoxes, queue_size=1)

def image_cb(msg, cv_bridge, yolov5_param, color_classes, image_pub):
    # 使用cv_bridge将ROS的图像数据转换成OpenCV的图像格式
    try:
        # 将Opencv图像转换numpy数组形式,数据类型是uint8(0~255)
        # numpy提供了大量的操作数组的函数,可以方便高效地进行图像处理    
        cv_image = cv_bridge.imgmsg_to_cv2(msg, "bgr8")
        frame = np.array(cv_image, dtype=np.uint8)
    except (CvBridgeError, e):
        print(e)
    # 实例化BoundingBoxes,存储本次识别到的所有目标信息
    bounding_boxes = BoundingBoxes()
    bounding_boxes.header = msg.header

    # 将BGR图像转换为RGB图像, 给yolov5,其返回识别到的目标信息
    rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    results = yolov5_param.model(rgb_image)
    boxs = results.pandas().xyxy[0].values
    for box in boxs:
        bounding_box = BoundingBox()
        # 置信度,因为是基于统计,因此每个目标都有一个置信度,标识可能性
        bounding_box.probability =np.float64(box[4])
        # (xmin, ymin)是目标的左上角,(xmax,ymax)是目标的右上角
        bounding_box.xmin = np.int64(box[0])
        bounding_box.ymin = np.int64(box[1])
        bounding_box.xmax = np.int64(box[2])
        bounding_box.ymax = np.int64(box[3])
        # 本地识别到的目标个数
        bounding_box.num = np.int16(len(boxs))
        # box[-1]是目标的类型名,比如person
        bounding_box.Class = box[-1]
        
        # 放入box队列中
        bounding_boxes.bounding_boxes.append(bounding_box)
        # 同一类目标,用同一个颜色的线条画框
        if box[-1] in color_classes.keys():
            color = color_classes[box[-1]]
        else:
            color = np.random.randint(0, 183, 3)
            color_classes[box[-1]] = color
    
        # 用框把目标圈出来
        cv2.rectangle(cv_image, (int(box[0]), int(box[1])),
                        (int(box[2]), int(box[3])), (int(color[0]),int(color[1]), int(color[2])), 2)    
		# 在框上, 打印物体类型信息Class
        if box[1] < 20:
            text_pos_y = box[1] + 30
        else:
            text_pos_y = box[1] - 10    
        cv2.putText(cv_image, box[-1],
                    (int(box[0]), int(text_pos_y)-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2, cv2.LINE_AA)    

    # 发布目标数据,topic为:/yolov5/targets
    # 可以使用命令查看:rotopic echo /yolov5/targets
    yolov5_param.target_pub.publish(bounding_boxes)
    # 将标识了识别目标的图像转换成ROS消息并发布
    image_pub.publish(cv_bridge.cv2_to_imgmsg(cv_image, "bgr8"))

def main():
    warnings.simplefilter("ignore", category=FutureWarning)

    rospy.init_node("detect_ros")
    rospy.loginfo("starting detect_ros node")

    bridge = CvBridge()
    yolov5_param = Yolov5Param()
    color_classes = {}
    image_pub = rospy.Publisher("/yolov5/detection_image", Image, queue_size=1)
    bind_image_cb = partial(image_cb, cv_bridge=bridge, yolov5_param=yolov5_param, color_classes=color_classes, image_pub=image_pub)
    rospy.Subscriber("/camera/color/image_raw", Image, bind_image_cb)
   
    rospy.spin()
    cv2.destroyAllWindows()
if __name__ == "__main__":
    main()

该脚本创建了一个ros节点,订阅了相机发布的话题/camera/color/image_raw,随后调用yolov5进行识别,并把识别到的目标用在图像中框出,打上类别标签。
此时代码报错,因为在框选目标时用到了自定义的消息类型BoundingBox.msg和BoundingBoxes.msg,我们还需要在功能包中添加上该自定义的消息类型,并进行编译。
在yolov5_ros功能包中新建msg文件夹,并在文件夹中创建这两个消息

cd ~/yolov5_ws/src/yolov5_ros
mkdir msg
cd msg
touch BoundingBox.msg
touch BoundingBoxes.msg

定义BoundingBox.msg消息

float64 probability
int64 xmin
int64 ymin
int64 xmax
int64 ymax
int16 num
string Class

定义BoundingBoxes.msg消息

Header header
BoundingBox[] bounding_boxes

其中的BoundingBox.msg消息定义了一个检测框的信息,包括识别到的物体的长度、宽度、类别等信息。
而BoundingBoxes.msg这是在BoundingBox.msg的基础上创建了一个实例化的数组,用于框选一张图片中识别到的多个目标。
自定完消息后需要修改CMakeLists文件和package.xml文件,再重新编译。
CMakeLists需要添加的部分如下:

find_package(catkin REQUIRED COMPONENTS
cv_bridge
image_transport
roscpp
rospy
sensor_msgs
std_msgs
geometry_msgs
message_generation  //用于生成自定义消息
)
//自定义消息文件
add_message_files(
  FILES
  BoundingBox.msg
  BoundingBoxes.msg
)
//生成自定义消息所依赖的消息
generate_messages(
  DEPENDENCIES
  std_msgs
)
catkin_package(
CATKIN_DEPENDS message_runtime
)
catkin_install_python(PROGRAMS
  scripts/detect_ros.py  
  DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)

package.xml需要添加的部分如下:

<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>

修改完后能够成功编译代码
最后需要创建launch文件来启动该节点,在yolov5_ros功能包中新建launch文件夹,并在其中创建detect_ros.launch文件

mkdir launch
touch detect_ros.launch

在launch文件中复制如下代码:

<launch>
  <param name="yolov5_path"       value="/home/rui27/yolov5"/>  //这里填写yolov5安装的路径
  <param name="use_cpu"           value="false" />

  <!-- Start yolov5 and ros wrapper -->
  <node pkg="yolov5_ros" name="detect_ros" type="detect_ros.py" output="screen" >
    <param name="weight_path"       value="/home/rui27/yolov5/yolov5s.pt"/>  //这里填写yolov5训练权重文件的路径
    <param name="image_topic"       value="/camera/color/image_raw" />  //这里填写相机发布的ros话题名称
    <param name="pub_topic"         value="/yolov5/targets" />
    <param name="conf"              value="0.3" />
  </node>
  <!-- 使用launch-prefix让rqt_image_view比yolov5_detector晚七秒启动,因为yolov5启动比较慢 -->
  <node
      pkg="rqt_image_view"
      type="rqt_image_view"
      name="rqt_image_view"
      launch-prefix="bash -c 'sleep 7; $0 $@'"
      output="screen"
  />
</launch>

最后重新编译功能包,通过launch文件运行该节点
此外,添加上这些编译后缀可以生成供vscode识别的编译文件(vscode有时候报错或是无法进行函数跳转是因为无法认识工程,这样编译以后能解决此问题)

catkin_make -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=Yes 
roslaunch yolov5_ros detect_ros.launch

启动该节点前需要先运行roscore,并且有对应的相机话题发布。
在终端中使用rostopic echo /yolov5/targets无法查看到该话题是因为该话题使用了我们自定义的ros消息,该消息只在该工作空间能被识别,所以需要先source该工作空间的setup.bash文件。

3、创建目标跟踪节点

新建target_track功能包
(因为我有一个专门负责无人机控制的工作空间uav_ws,所以我把该功能包创建在另外一个工作空间。一般情况下这个功能包可以也创建在yolov5_ws工作空间,这样比较方便用launch文件一起启动,而且也可以直接使用自定义的BoundingBox.msg消息。如果创建在了其他工作空间,也要在那个工作空间创建相同的自定义消息,否则在订阅与该消息相关的话题时会出错)

cd ~/uav_ws/src
catkin_create_pkg target_track roscpp std_msgs geometry_msgs mavros_msgs
cd target_track/src
gedit yolov5_track.cpp

复制如下代码到yolov5_track.cpp文件中

#include <ros/ros.h>
#include <geometry_msgs/PoseStamped.h>
#include<geometry_msgs/TwistStamped.h>
#include <mavros_msgs/CommandBool.h>
#include <mavros_msgs/SetMode.h>
#include <mavros_msgs/State.h>
#include "target_track/BoundingBoxes.h"

ros::Publisher vel_pub;

int lostCount = 0;
bool captured = false;
int x, y;
float d;
float kp_yaw = 1.5;
float kp_x = 1.5;
float kp_z = 1;
geometry_msgs::TwistStamped target_vel;
target_track::BoundingBoxes boxes;
void target_cb(const target_track::BoundingBoxes::ConstPtr& msg){
    // boxes = *msg;
    for(const auto& box : msg->bounding_boxes){
        if(box.Class == "person"){
            captured = true;
            lostCount = 0;
            x = (box.xmin + box.xmax) / 2;
            y = (box.ymin + box.ymax) / 2;
            d = box.ymax - box.ymin;
            ROS_INFO("Detected Target Person");
            ROS_INFO("x: %d", x);
            ROS_INFO("y: %d", y);
            ROS_INFO("d: %f", d);
        }
    }
}

int main(int argc, char **argv)
{
    ros::init(argc, argv, "yolov5_track_node");
    ros::NodeHandle nh;
    ros::Rate rate(20);

    ros::Subscriber target_sub = nh.subscribe<target_track::BoundingBoxes>("yolov5/targets", 10, target_cb);
    ros::Publisher pos_pub = nh.advertise<geometry_msgs::PoseStamped>("mavros/setpoint_position/local", 10);
    vel_pub = nh.advertise<geometry_msgs::TwistStamped>("mavros/setpoint_velocity/cmd_vel", 10);\

    while(ros::ok()){
        ROS_INFO("captured: %d", captured);
        lostCount++;
        if(lostCount > 1000) lostCount = 1000;
        if(lostCount > 10)  captured = false;
        if(captured == false){
            target_vel.twist.angular.z = 0;
            target_vel.twist.linear.z = 0;
            target_vel.twist.linear.x = 0;
        }else if(captured == true){
            target_vel.twist.angular.z = kp_yaw * (x - 320) * -0.005;
            target_vel.twist.linear.z = kp_z * (y - 240) * -0.005;
            target_vel.twist.linear.x = kp_x * (1.0 - d/240.0);
        }
        vel_pub.publish(target_vel);
        ros::spinOnce();
        rate.sleep();
    }
}

在该工作空间创建自定义消息BoundingBox.msg和BoundingBoxes.msg,方法同前文。
接着修改CMakeLists.txt文件,在最后加上该节点的相关信息

add_executable(${PROJECT_NAME}_node src/yolov5_track.cpp)
target_link_libraries(${PROJECT_NAME}_node
  ${catkin_LIBRARIES}

完成后进行编译,并可以使用rosrun运行该节点(需要启动roscore,并且有前文detect_ros节点发布的yolov5/targets话题)

catkin_make
rosrun target_track target_track_node

4、修改mavros/setpoint_velocity/cmd_vel坐标系

完成上面内容就已经能够识别并控制px4运动,但是可能px4并不会按照设想方位运动,无法跟踪到目标。
这涉及到mavros消息坐标系的问题,mavros/setpoint_velocity/cmd_vel是用于对px4进行速度控制的话题,该话题默认使用全局坐标系,而我们发布的速度是无人机机体坐标系下的速度,下面进行修改。
在本机的环境中,我们运行PX4_Firmware/launch/mavros_posix_sitl.launch文件来进行px4无人机的仿真。
打开该文件,我们发现他调用了mavros/launch/px4.launch文件来启动仿真无人机,如下图所示。

因此我们需要先找到mavros功能包的位置
打开终端,输入以下指令:

rospack find mavros

终端显示mavros功能包的安装路径,如下图所示:

我们找到该路径下的px4.launch文件(/opt/ros/noetic/share/mavros/launch/px4.launch)

可以看到,启动该节点时的配置参数都从该目录下的px4_config.yaml文件中导入
于是我们去修改px4_config.yaml文件

图中的mav_frame参数用来设置该话题的坐标系
我们将他从LOCAL_END改为BODY_END
修改完成后保存即可,若无法保存的话,通过sudo nautilus指令以管理员权限打开文件管理器就有权限修改该文件了
修改完成后,再次运行之前的程序,无人机能够正确跟踪目标。

posted @ 2025-04-03 23:04  Rui27  阅读(672)  评论(0)    收藏  举报