返回列表
ROS2+智能导航小车 2024年7月12日 · 69 分钟

项目一:基于yolov5的目标检测

在 ROS2 上打通树莓派相机与 PC,结合 OpenCV 与 YOLOv5 实现小车摄像头画面的实时目标检测

1. 本节最终实现效果

PC通过ssh连接到树莓派,通过ros2的键盘节点控制小车移动,然后在树莓派上发布摄像头节点的数据,PC通过ros2的通信机制接收到这个图像信息,并通过opencv和yolov5对图像数据进行处理并检测,最终将拍摄到的画面和检测到的目标显示出来,实现小车移动目标检测功能。

本节一共需要开启4个终端,第一个是开启与esp32的通信,第二个是小车的移动键盘控制节点,第三个是远程树莓派相机数据发布节点,第四个是用来目标检测的。

后续所有的项目:小车移动控制节点必须运行在树莓派端,否则两个设备之间数据传输会出现问题,而目标检测,建图等操作可以用PC来接收ROS话题来处理数据。

在这里插入图片描述 在这里插入图片描述

==用到的命令汇总:==

# 第一个是开启与esp32的通信
ros2 run micro_ros_agent micro_ros_agent udp4 --port 8888 -v6

# 第二个是小车的移动键盘控制节点
ros2 run teleop_twist_keyboard teleop_twist_keyboard

# 第三个是远程树莓派相机数据发布节点
ros2 run image_tools cam2image --ros-args -p width:=640 -p height:=480 -p frequency:=30.0 -p device_id:=0

# 第四个是用来目标检测的
ros2 run ros2_yolov5 yolo_detect_2d --ros-args -p device:=cpu -p image_topic:=/image -p show_result:=True -p pub_result_img:=True

运行效果如下: 在这里插入图片描述

下面我们开始详细说明

2. 安装依赖

首先,确保您的PC已经更新了系统并且安装了必要的依赖。以下是一些安装步骤,其中$ROS_DISTRO 是您的ROS2发行版(例如:foxy、galactic、我使用的是humble):

sudo apt update
sudo apt install python3-pip ros-$ROS_DISTRO-vision-msgs
pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple yolov5  

如果上面的运行报错 则运行下面这个

pip install yolov5 -i https://pypi.tuna.tsinghua.edu.cn/simple --break-system-packages

3. 编写代码

3.1 创建一个包

进入 ros2_car_ws/src 目录,并运行包创建命令:

ros2 pkg create --build-type ament_python ros2_yolov5

你的终端将返回一条消息,验证已创建名为 ros2_yolov5 的软件包及其所有必要的文件和文件夹。 在这里插入图片描述

软件包中包含下面的内容 在这里插入图片描述

3.2 编写代码

进入 ros2_car_ws/src/ros2_yolov5/ros2_yolov5 目录。请记住,该目录是一个与嵌套的ROS 2软件包同名的Python包。

创建一个名为yolov5_detect_2d.py的文件,复制下面的内容

from math import frexp
from traceback import print_tb
from torch import imag
from yolov5 import YOLOv5
import rclpy
from rclpy.node import Node
from ament_index_python.packages import get_package_share_directory
from rcl_interfaces.msg import ParameterDescriptor
from vision_msgs.msg import Detection2DArray, ObjectHypothesisWithPose, Detection2D
from sensor_msgs.msg import Image, CameraInfo           # ROS 2 消息类型
from cv_bridge import CvBridge          # ROS的OpenCV桥接
import cv2
import yaml         # 用于图像处理的 OpenCV 和用于配置文件处理的 YAML
from ros2_yolov5.cv_tool import px2xy
import os

# Get the ROS distribution version and set the shared directory for YoloV5 configuration files.
ros_distribution = os.environ.get("ROS_DISTRO")
package_share_directory = get_package_share_directory('ros2_yolov5')

# Create a ROS 2 Node class YoloV5Ros2.
class YoloV5Ros2(Node):
    def __init__(self):
        super().__init__('ros2_yolov5')
        self.get_logger().info(f"Current ROS 2 distribution: {ros_distribution}")

        # Declare ROS parameters.
        self.declare_parameter("device", "cuda", ParameterDescriptor(
            name="device", description="Compute device selection, default: cpu, options: cuda:0"))

        self.declare_parameter("model", "yolov5s", ParameterDescriptor(
            name="model", description="Default model selection: yolov5s"))

        self.declare_parameter("image_topic", "/image_raw", ParameterDescriptor(
            name="image_topic", description="Image topic, default: /image_raw"))
        
        self.declare_parameter("camera_info_topic", "/camera/camera_info", ParameterDescriptor(
            name="camera_info_topic", description="Camera information topic, default: /camera/camera_info"))

        # Read parameters from the camera_info topic if available, otherwise, use the file-defined parameters.
        self.declare_parameter("camera_info_file", f"{package_share_directory}/config/camera_info.yaml", ParameterDescriptor(
            name="camera_info", description=f"Camera information file path, default: {package_share_directory}/config/camera_info.yaml"))

        # Default to displaying detection results.
        self.declare_parameter("show_result", False, ParameterDescriptor(
            name="show_result", description="Whether to display detection results, default: False"))

        # Default to publishing detection result images.
        self.declare_parameter("pub_result_img", False, ParameterDescriptor(
            name="pub_result_img", description="Whether to publish detection result images, default: False"))

        # 1. Load the model.
        model_path = package_share_directory + "/config/" + self.get_parameter('model').value + ".pt"
        device = self.get_parameter('device').value
        self.yolov5 = YOLOv5(model_path=model_path, device=device)

        # 2. Create publishers.
        # 创建两个发布者:一个用于发布检测结果,另一个用于发布结果图像。
        self.yolo_result_pub = self.create_publisher(
            Detection2DArray, "yolo_result", 10)
        self.result_msg = Detection2DArray()

        self.result_img_pub = self.create_publisher(Image, "result_img", 10)

        # 3. Create an image subscriber (subscribe to depth information for 3D cameras, load camera info for 2D cameras).
        # 已设置图像主题和相机信息主题的订阅
        image_topic = self.get_parameter('image_topic').value
        self.image_sub = self.create_subscription(
            Image, image_topic, self.image_callback, 10)

        camera_info_topic = self.get_parameter('camera_info_topic').value
        self.camera_info_sub = self.create_subscription(
            CameraInfo, camera_info_topic, self.camera_info_callback, 1)

        # Get camera information.
        # 相机信息是从 YAML 文件中读取的
        with open(self.get_parameter('camera_info_file').value) as f:
            self.camera_info = yaml.full_load(f.read())
            self.get_logger().info(f"default_camera_info: {self.camera_info['k']} \n {self.camera_info['d']}")

        # 4. Image format conversion (using cv_bridge).
        # 用于图像格式转换的 CVBridge 和用于显示/发布结果的参数
        self.bridge = CvBridge()

        self.show_result = self.get_parameter('show_result').value
        self.pub_result_img = self.get_parameter('pub_result_img').value


    # 当收到新的相机信息消息时,将调用此函数。它会更新相机参数,然后取消订阅该主题
    def camera_info_callback(self, msg: CameraInfo):
        """
        Get camera parameters through a callback function.
        """
        self.camera_info['k'] = msg.k
        self.camera_info['p'] = msg.p
        self.camera_info['d'] = msg.d
        self.camera_info['r'] = msg.r
        self.camera_info['roi'] = msg.roi

        self.camera_info_sub.destroy()

    # 当收到新的图像消息时,将调用此函数。它处理图像,执行对象检测并发布结果
    def image_callback(self, msg: Image):
        # 5. Detect and publish results.
        image = self.bridge.imgmsg_to_cv2(msg)
        detect_result = self.yolov5.predict(image)
        self.get_logger().info(str(detect_result))

        self.result_msg.detections.clear()
        self.result_msg.header.frame_id = "camera"
        self.result_msg.header.stamp = self.get_clock().now().to_msg()

        # Parse the results.
        predictions = detect_result.pred[0]
        boxes = predictions[:, :4]  # x1, y1, x2, y2
        scores = predictions[:, 4]
        categories = predictions[:, 5]

        for index in range(len(categories)):
            name = detect_result.names[int(categories[index])]
            detection2d = Detection2D()
            detection2d.id = name
            x1, y1, x2, y2 = boxes[index]
            x1 = int(x1)
            y1 = int(y1)
            x2 = int(x2)
            y2 = int(y2)
            center_x = (x1+x2)/2.0
            center_y = (y1+y2)/2.0

            if ros_distribution=='galactic':
                detection2d.bbox.center.x = center_x
                detection2d.bbox.center.y = center_y
            else:
                detection2d.bbox.center.position.x = center_x
                detection2d.bbox.center.position.y = center_y

            detection2d.bbox.size_x = float(x2-x1)
            detection2d.bbox.size_y = float(y2-y1)

            obj_pose = ObjectHypothesisWithPose()
            obj_pose.hypothesis.class_id = name
            obj_pose.hypothesis.score = float(scores[index])

            # px2xy
            world_x, world_y = px2xy(
                [center_x, center_y], self.camera_info["k"], self.camera_info["d"], 1)
            obj_pose.pose.pose.position.x = world_x
            obj_pose.pose.pose.position.y = world_y
            detection2d.results.append(obj_pose)
            self.result_msg.detections.append(detection2d)

            # Draw results.
            if self.show_result or self.pub_result_img:
                cv2.rectangle(image, (x1, y1), (x2, y2), (0, 255, 0), 2)
                cv2.putText(image, f"{name}({world_x:.2f},{world_y:.2f})", (x1, y1),
                            cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)
                cv2.waitKey(1)

        # Display results if needed.
        if self.show_result:
            cv2.imshow('result', image)
            cv2.waitKey(1)

        # Publish result images if needed.
        if self.pub_result_img:
            result_img_msg = self.bridge.cv2_to_imgmsg(image, encoding="bgr8")
            result_img_msg.header = msg.header
            self.result_img_pub.publish(result_img_msg)

        if len(categories) > 0:
            self.yolo_result_pub.publish(self.result_msg)

def main():
    rclpy.init()
    rclpy.spin(YoloV5Ros2())
    rclpy.shutdown()

if __name__ == "__main__":
    main()

创建一个名为cv_tool.py的文件,复制下面的的内容

# 导入所需的库
# Import the required libraries
import cv2  # OpenCV library for image processing
import numpy as np  # NumPy library for array and matrix operations

# 相机内参矩阵K,包括相机的焦距和主点坐标
# Camera intrinsic matrix K, including camera's focal length and principal point coordinates
K = [[602.7175003324863, 0, 351.305582038406],
     [0, 601.6330312976042, 240.0929104708551],
     [0, 0, 1]]

# 相机畸变参数D,用于校正图像畸变
# Camera distortion parameters D, used for correcting image distortion
D = [0.06712174262966401, -0.2636999208734844,
     0.006484443443073637, 0.01111161327049835, 0]

# 定义一个函数px2xy,用于将像素坐标转换为相机坐标系下的二维坐标
# Define a function px2xy to convert pixel coordinates to 2D coordinates in camera coordinate system
def px2xy(point, camera_k, camera_d, z=1.0):
    # 将相机内参矩阵K和相机畸变参数D转换为NumPy数组
    # Convert camera intrinsic matrix K and camera distortion parameters D to NumPy arrays
    MK = np.array(camera_k, dtype=float).reshape(3, 3)
    MD = np.array(camera_d, dtype=float)
    
    # 将输入的像素坐标点转换为NumPy数组
    # Convert the input pixel coordinate point to a NumPy array
    point = np.array(point, dtype=float)
    
    # 使用OpenCV的cv2.undistortPoints函数对输入点进行畸变矫正,并乘以深度值z
    # Use OpenCV's cv2.undistortPoints function to correct distortion of input points and multiply by depth value z
    pts_uv = cv2.undistortPoints(point, MK, MD) * z
    
    # 返回相机坐标系下的二维坐标
    # Return 2D coordinates in the camera coordinate system
    return pts_uv[0][0]

# 调用函数并打印结果(如果需要)
# Call the function and print the result (if needed)
# print(px2xy([0, 0], K, D, 1))

3.3 参数配置

package.xml中添加下面的内容

 <depend>rclpy</depend>
 <depend>vision_msgs</depend>
 <depend>yolov5</depend>

添加后的完整代码如下:

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>ros2_yolov5</name>
  <version>0.0.0</version>
  <description>TODO: Package description</description>
  <maintainer email="lll@todo.todo">lll</maintainer>
  <license>TODO: License declaration</license>
 
  <depend>rclpy</depend>
  <depend>vision_msgs</depend>
  <depend>yolov5</depend>
 
  <test_depend>ament_copyright</test_depend>
  <test_depend>ament_flake8</test_depend>
  <test_depend>ament_pep257</test_depend>
  <test_depend>python3-pytest</test_depend>
 
  <export>
    <build_type>ament_python</build_type>
  </export>
</package>

setup.py中添加

  "yolo_detect_2d=ros2_yolov5.yolov5_detect_2d:main"

如下:

from setuptools import find_packages, setup
from glob import glob
import os

package_name = 'ros2_yolov5'

setup(
    name=package_name,
    version='0.0.0',
    packages=find_packages(exclude=['test']),
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
        (os.path.join('share', package_name, 'config'), glob('config/**')),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='lll',
    maintainer_email='lll@todo.todo',
    description='TODO: Package description',
    license='TODO: License declaration',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            "yolo_detect_2d=ros2_yolov5.yolov5_detect_2d:main"
        ],
    },
)

3.4 创建config文件夹

yolov5s.pt模型文件放这里,可以随意换别的模型

创建一个相机参数文件camera_info.yaml,代码如下

height: 1000
width: 800
distortion_model: plumb_bob
d: [0.0, 0.0, 0.0, 0.0, 0.0]
k: [476.7030836014194, 0.0, 400.5, 0.0, 476.7030836014194, 400.5, 0.0, 0.0, 1.0]
r: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]
p:
  [
    476.7030836014194,
    0.0,
    400.5,
    -0.0,
    0.0,
    476.7030836014194,
    400.5,
    0.0,
    0.0,
    0.0,
    1.0,
    0.0,
  ]

所有的代码以及完成,接下来就可以运行了。

4. 编译和运行

4.1 第一个终端:

ssh远程登录到树莓派后的终端

ssh user@192.168.1.100
# 然后输入密码即可登录

启动摄像头节点发布消息

ros2 run image_tools cam2image --ros-args -p width:=640 -p height:=480 -p frequency:=30.0 -p device_id:=0

最后的这里 device_id:=0 或者 device_id:=-1 都可以

此时大概率会出现如下的报错:

lll@laj:~/ros2_ws$ ros2 run image_tools cam2image --ros-args -p width:=640 -p height:=480 -p frequency:=30.0 -p device_id:=0
[ WARN:0@0.449] global ./modules/videoio/src/cap_gstreamer.cpp (2401) handleMessage OpenCV | GStreamer warning: Embedded video playback halted; module v4l2src0 reported: Could not open device '/dev/video0' for reading and writing.
[ WARN:0@0.449] global ./modules/videoio/src/cap_gstreamer.cpp (1356) open OpenCV | GStreamer warning: unable to start pipeline
[ WARN:0@0.449] global ./modules/videoio/src/cap_gstreamer.cpp (862) isPipelinePlaying OpenCV | GStreamer warning: GStreamer: pipeline have not been created
[ WARN:0@0.451] global ./modules/videoio/src/cap_v4l.cpp (902) open VIDEOIO(V4L2:/dev/video0): can't open camera by index
[ERROR] [1720852535.682295122] [cam2image]: Could not open video stream
terminate called after throwing an instance of 'std::runtime_error'
  what():  Could not open video stream
[ros2run]: Aborted

这个问题通常是因为通过 SSH 远程登录到树莓派时,无法直接访问连接在树莓派上的摄像头设备。摄像头设备通常需要物理访问权限,而通过 SSH 远程登录时,默认情况下不会授予这种权限。

只需要通过下面的命令确保当前用户对 /dev/video0 设备具有读写权限

sudo chmod 666 /dev/video0

如下图所示,查看此时的话题,相机发布的话题为/image

ros2 topic list

在这里插入图片描述

此时摄像头节点会发布图像消息到ROS2的话题上,我们在PC端接通过订阅这个话题/image来接收摄像头发布的图像消息。

4.2 第二个终端:

首先要编译项目:

cd ros2_car_ws
colcon build  ## colcon build --packages-select ros2_yolov5
source install/setup.bash

现在,您可以运行Yolo_ROS2节点。默认情况下,它将使用CPU来进行检测,您可以根据需要更改这些参数:

ros2 run ros2_yolov5 yolo_detect_2d --ros-args -p device:=cpu -p image_topic:=/image

此时显示,只有命令行输出, 在这里插入图片描述 我们这里取出其中一条 在这里插入图片描述 可以看到有检测到的目标框的大小和类别数据

此时我们继续查看话题,发现多了我们编写的两个话题

ros2 topic list

在这里插入图片描述

/result_img /yolo_result

后续我们只需要将其绘制出来就完成了。

4.3 绘制识别到的目标图像

命令如下:

ros2 run ros2_yolov5 yolo_detect_2d --ros-args -p device:=cpu -p image_topic:=/image -p show_result:=True -p pub_result_img:=True

此时就可以看到类似的画面了 在这里插入图片描述

4.4 参数解释

Yolo_ROS2将检测结果发布到/yolo_result话题中,包括原始像素坐标以及归一化后的相机坐标系下的x和y坐标。您可以使用以下命令查看检测结果:

ros2 topic echo /yolo_result

在这里插入图片描述

4.4.1 参数设置

在运行Yolo_ROS2节点时,您可以使用 -p name:=value 的方式来修改参数值。

4.4.2 图像话题

您可以通过指定以下参数来更改图像话题:

image_topic:=/image

4.4.3 计算设备设置

如果您有CUDA支持的显卡,可以选择以下参数来配置计算设备:

device:=cpu

4.4.4 是否实时显示结果

您可以使用以下参数来控制是否实时显示检测结果。设置为True将实时显示结果,设置为False则不会显示:

show_result:=False

请注意,实时显示中的cv2.imshow可能会卡住。如果只需要验证结果,可以将此参数设置为False。

4.4.5 切换不同Yolov5模型

默认情况下,Yolo_ROS2使用yolov5s模型。您可以通过以下参数来更改模型:

model:=yolov5m

4.4.6 相机参数文件

功能包默认从 /camera/camera_info 话题获取相机参数,在获取成功前,相机参数文件路径可以通过参数进行设置,参数为:camera_info_file,通过该参数可以设置文件路径,注意需要使用绝对目录:

-p camera_info_file:=/home/lll/ros2_ws/src/ros2_yolov5/config/camera_info.yaml

5. 控制小车移动

运行agent,点击RST按键。

sudo docker run -it --rm -v /dev:/dev -v /dev/shm:/dev/shm --privileged --net=host microros/micro-ros-agent:$ROS_DISTRO udp4 --port 8888 -v6

因为esp32的wifi有时不太稳定,只有当出现下图右侧所示,才可以稳定控制小车, 在这里插入图片描述

看到连接建立表示通信成功,接着用ros2 topic list

ros2 topic list

看到/cmd_vel表示正常,接着我们使用teleop_twist_keyboard进行键盘控制

ros2 run teleop_twist_keyboard teleop_twist_keyboard

先调整下速度,降低到0.05左右(50cm/s),然后使用i\j\j\k,测试。