机器人学习!(二)ROS-基于Gazebo项目(2)2026/01/12

学习路线:

第一阶段:传统视觉跟踪 ✅ 已完成

第二阶段:深度学习目标检测(YOLO)

第三阶段:模型优化与加速(TensorRT)

第四阶段:Jetson硬件部署

第五阶段:完整机器人AI系统

项目一、基于Gazebo的ROS + OpenCV 实时颜色跟踪机器人

ROS与OpenCV集成

颜色检测算法实现

视觉伺服控制逻辑

Gazebo仿真环境配置

机器人硬件接口调用

复制代码
项目流程

循环开始
  ↓
获取图像 → 检测红色 → 找到最大区域
  ↓
计算目标中心 → 计算误差(error_x)
  ↓
计算角速度:angular.z = -error_x / 300
  ↓
计算线速度:linear.x = max_speed × (1 - 面积占比×5)
  ↓
限制速度范围 → 发布到 /cmd_vel
  ↓
显示图像和调试信息
  ↓
检查退出键


启动

启动 launch 文件
    ↓
启动 Gazebo + TurtleBot3(在空世界中)
    ↓
生成红色球体(在机器人前方2米,离地0.5米)
    ↓
启动你的跟踪节点(订阅摄像头数据)
    ↓
Gazebo摄像头发布图像 → 你的节点处理 → 发布控制指令 → 机器人移动



具体步骤:

第一步:创建一个简单ROS节点,用来显示摄像头图像

1创建视觉功能包

1.1功能包包含代码、配置文件、启动文件

2、具体操作

复制代码
# 进入工作空间源代码目录
cd ~/turtle_ws/src

# 创建功能包,注意依赖项多了roscpp(C++库)和image_transport
catkin_create_pkg robot_vision_cpp roscpp cv_bridge sensor_msgs image_transport

# 进入包目录
cd robot_vision_cpp

# C++源文件通常放在src目录下
mkdir src

# 在src目录下创建show_image.cpp
touch src/show_image.cpp

打开show_image.cpp

复制代码
// 包含必要的头文件
#include <ros/ros.h>                 // ROS C++核心库
#include <image_transport/image_transport.h> // 高效传输图像
#include <cv_bridge/cv_bridge.h>     // ROS和OpenCV之间的转换器
#include <opencv2/highgui/highgui.hpp> // OpenCV显示窗口功能
#include <opencv2/imgproc/imgproc.hpp> // OpenCV图像处理功能(后续会用)

// 定义一个类来管理我们的图像查看器
class ImageViewer
{
private:
    ros::NodeHandle nh_;                    // ROS节点句柄,与ROS系统通信
    image_transport::ImageTransport it_;    // 图像传输对象,高效处理图像
    image_transport::Subscriber image_sub_; // 图像订阅者
    
    // 回调函数:当收到新图像时自动调用
    void imageCallback(const sensor_msgs::ImageConstPtr& msg)
    {
        try
        {
            // 将ROS图像消息转换为OpenCV格式
            // 使用toCvCopy()创建图像的副本,这样我们可以安全地修改它
            // "bgr8"表示颜色编码:蓝-绿-红,每个通道8位
            cv_bridge::CvImagePtr cv_ptr = cv_bridge::toCvCopy(msg, "bgr8");
            
            // 现在cv_ptr->image就是OpenCV的Mat对象,可以操作了
            
            // 显示图像
            cv::imshow("Camera View", cv_ptr->image);
            
            // 等待10毫秒,并检查是否按了'q'键
            // 如果不调用waitKey,窗口不会更新
            char key = (char)cv::waitKey(10);
            if (key == 'q' || key == 27) // 27是ESC键的ASCII码
            {
                ROS_INFO("用户按了退出键,关闭节点");
                ros::shutdown(); // 优雅地关闭ROS节点
            }
        }
        catch (cv_bridge::Exception& e)
        {
            // 如果转换失败,打印错误信息
            ROS_ERROR("cv_bridge异常: %s", e.what());
            return;
        }
    }

public:
    // 构造函数:初始化节点
    ImageViewer() : it_(nh_)
    {
        // 打印启动信息
        ROS_INFO("🟡 正在启动C++图像查看节点...");
        
        // 创建图像订阅者
        // 参数1:订阅的话题名
        // 参数2:队列大小(缓存的消息数)
        // 参数3:回调函数,使用boost::bind绑定到当前对象的imageCallback方法
        // 参数4:this指针,告诉ROS回调函数属于哪个对象
        image_sub_ = it_.subscribe("/camera/image_raw", 1, 
                                   &ImageViewer::imageCallback, this);
        
        // 创建OpenCV显示窗口
        cv::namedWindow("Camera View", cv::WINDOW_AUTOSIZE);
        
        ROS_INFO("🟢 节点已启动,等待图像数据...");
        ROS_INFO("   话题: /camera/image_raw");
        ROS_INFO("   按'q'或ESC键退出");
    }
    
    // 析构函数:节点关闭时自动调用
    ~ImageViewer()
    {
        // 关闭OpenCV窗口
        cv::destroyWindow("Camera View");
        ROS_INFO("节点关闭");
    }
};

// 主函数:程序入口
int main(int argc, char** argv)
{
    // 初始化ROS节点
    // 参数1:命令行参数个数
    // 参数2:命令行参数数组
    // 参数3:节点名,会显示在rosnode list中
    ros::init(argc, argv, "image_viewer_cpp");
    
    // 创建ImageViewer对象
    ImageViewer viewer;
    
    // ros::spin()进入循环,等待回调函数被调用
    // 它会一直运行,直到收到关闭信号(如Ctrl+C或ros::shutdown())
    ros::spin();
    
    return 0;
}

3、配置编译系统

CMakelist.txt

复制代码
## 依赖项设置(通常在文件开头)
find_package(catkin REQUIRED COMPONENTS
  roscpp
  cv_bridge
  sensor_msgs
  image_transport
)

## 找到OpenCV(需要单独声明)
find_package(OpenCV REQUIRED)

## 包含目录设置
include_directories(
  ${catkin_INCLUDE_DIRS}
  ${OpenCV_INCLUDE_DIRS}
)

## 声明我们要生成的可执行文件
## 格式:add_executable(可执行文件名 源代码文件)
add_executable(show_image_cpp src/show_image.cpp)

## 链接库:告诉编译器需要哪些库文件
target_link_libraries(show_image_cpp
  ${catkin_LIBRARIES}
  ${OpenCV_LIBRARIES}
)

package.xml

复制代码
<!-- 编译时依赖 -->
<build_depend>roscpp</build_depend>
<build_depend>cv_bridge</build_depend>
<build_depend>sensor_msgs</build_depend>
<build_depend>image_transport</build_depend>

<!-- 运行时依赖 -->
<exec_depend>roscpp</exec_depend>
<exec_depend>cv_bridge</exec_depend>
<exec_depend>sensor_msgs</exec_depend>
<exec_depend>image_transport</exec_depend>

4、编译测试

复制代码
# 回到工作空间根目录
cd ~/turtle_ws

# 编译整个工作空间
catkin_make

# 如果只想编译这个包,可以指定
# catkin_make --pkg robot_vision_cpp

测试图像源(生成图像源 代表 虚拟摄像头 获取到的画面)

复制代码
# 1. 创建脚本文件
mkdir -p ~/ros_test
cd ~/ros_test

# 2. 创建Python脚本
cat > virtual_cam.py << 'EOF'
#!/usr/bin/env python3
import rospy
import numpy as np
import cv2
from sensor_msgs.msg import Image
from cv_bridge import CvBridge

rospy.init_node('virtual_camera')
pub = rospy.Publisher('/camera/image_raw', Image, queue_size=10)
bridge = CvBridge()
rate = rospy.Rate(30)  # 30帧/秒

width, height = 640, 480
print("✅ 虚拟摄像头已启动")
print("📡 发布话题: /camera/image_raw")
print("🖼️  分辨率: 640x480")

frame_count = 0
while not rospy.is_shutdown():
    # 创建蓝色渐变背景
    img = np.zeros((height, width, 3), dtype=np.uint8)
    
    # 水平渐变:左边蓝色,右边绿色
    for col in range(width):
        blue_intensity = int((1.0 - col/width) * 255)
        green_intensity = int((col/width) * 255)
        img[:, col, 0] = blue_intensity    # 蓝色通道
        img[:, col, 1] = green_intensity   # 绿色通道
    
    # 添加一个移动的红色圆形
    center_x = int(width/2 + 100 * np.sin(frame_count * 0.1))
    center_y = int(height/2 + 80 * np.cos(frame_count * 0.07))
    cv2.circle(img, (center_x, center_y), 30, (0, 0, 255), -1)  # 红色实心圆
    
    # 添加一个移动的黄色矩形
    rect_x = int(100 + 50 * np.sin(frame_count * 0.05))
    rect_y = int(100 + 40 * np.cos(frame_count * 0.08))
    cv2.rectangle(img, (rect_x, rect_y), (rect_x+60, rect_y+40), (0, 255, 255), 2)  # 黄色边框
    
    # 添加说明文字
    cv2.putText(img, 'ROS Virtual Camera', (30, 30), 
                cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2)
    cv2.putText(img, 'Red circle and yellow rectangle move', (30, 70),
                cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
    cv2.putText(img, f'Frame: {frame_count}', (30, 100),
                cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1)
    
    # 转换为ROS消息
    msg = bridge.cv2_to_imgmsg(img, encoding='bgr8')
    msg.header.stamp = rospy.Time.now()
    msg.header.frame_id = 'virtual_camera'
    pub.publish(msg)
    
    frame_count += 1
    rate.sleep()

cv2.destroyAllWindows()
EOF

# 3. 设置执行权限
chmod +x virtual_cam.py

验证调试

复制代码
roscore

cd ~/ros_test
python3 virtual_cam.py

# 列出所有话题,应该看到 /camera/image_raw
rostopic list

# 查看图像消息的发布频率
rostopic hz /camera/image_raw

cd ~/turtle_ws
source devel/setup.bash
rosrun robot_vision_cpp show_image_cpp

第二步、检测图像中的红色物体

  1. 找到图像中所有红色的区域

  2. 用绿色框标记出来

  3. 在终端显示红色物体的位置和大小

    cd ~/turtle_ws/src/robot_vision_cpp/src
    touch color_detector.cpp

    // 颜色检测节点 - 检测图像中的红色物体
    #include <ros/ros.h>
    #include <image_transport/image_transport.h>
    #include <cv_bridge/cv_bridge.h>
    #include <opencv2/highgui/highgui.hpp>
    #include <opencv2/imgproc/imgproc.hpp> // 新增:图像处理功能

    class ColorDetector
    {
    private:
    ros::NodeHandle nh_;
    image_transport::ImageTransport it_;
    image_transport::Subscriber image_sub_;

    复制代码
     // 新增:用于显示处理结果的窗口
     const std::string ORIGINAL_WINDOW = "Original Image";
     const std::string MASK_WINDOW = "Red Mask";
     const std::string RESULT_WINDOW = "Detection Result";
     
     // 颜色范围定义(HSV颜色空间)
     // 注意:OpenCV中H的范围是0-179(不是0-360)
     cv::Scalar lower_red1 = cv::Scalar(0, 100, 100);    // 红色范围1(H: 0-10)
     cv::Scalar upper_red1 = cv::Scalar(10, 255, 255);
     cv::Scalar lower_red2 = cv::Scalar(160, 100, 100);  // 红色范围2(H: 160-180)
     cv::Scalar upper_red2 = cv::Scalar(180, 255, 255);
     
     // 图像回调函数
     void imageCallback(const sensor_msgs::ImageConstPtr& msg)
     {
         ROS_INFO_ONCE("开始处理图像数据...");
         
         try
         {
             // 1. 转换图像格式:ROS消息 -> OpenCV图像
             cv_bridge::CvImagePtr cv_ptr = cv_bridge::toCvCopy(msg, "bgr8");
             cv::Mat original = cv_ptr->image;  // 原始图像
             
             // 2. 转换到HSV颜色空间(对光照变化更鲁棒)
             cv::Mat hsv_image;
             cv::cvtColor(original, hsv_image, cv::COLOR_BGR2HSV);
             
             // 3. 创建红色掩码(mask)
             cv::Mat mask1, mask2, red_mask;
             
             // 检测红色范围1(0-10度)
             cv::inRange(hsv_image, lower_red1, upper_red1, mask1);
             
             // 检测红色范围2(160-180度)  
             cv::inRange(hsv_image, lower_red2, upper_red2, mask2);
             
             // 合并两个范围的掩码
             cv::bitwise_or(mask1, mask2, red_mask);
             
             // 4. 形态学操作:去除噪声,填充空洞
             cv::Mat kernel = cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(5, 5));
             cv::morphologyEx(red_mask, red_mask, cv::MORPH_OPEN, kernel);   // 开运算:去噪
             cv::morphologyEx(red_mask, red_mask, cv::MORPH_CLOSE, kernel);  // 闭运算:填充
             
             // 5. 寻找红色区域的轮廓
             std::vector<std::vector<cv::Point>> contours;
             cv::findContours(red_mask, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
             
             // 6. 在原始图像上标记检测结果
             cv::Mat result = original.clone();
             
             // 如果没有找到红色区域
             if (contours.empty()) {
                 cv::putText(result, "No red object detected", cv::Point(30, 50),
                            cv::FONT_HERSHEY_SIMPLEX, 0.8, cv::Scalar(0, 0, 255), 2);
             } 
             else {
                 // 遍历所有轮廓
                 for (size_t i = 0; i < contours.size(); i++) {
                     // 计算轮廓的面积,忽略太小的噪点
                     double area = cv::contourArea(contours[i]);
                     if (area < 500) continue;  // 面积小于500像素的忽略
                     
                     // 获取轮廓的外接矩形
                     cv::Rect boundRect = cv::boundingRect(contours[i]);
                     
                     // 绘制绿色矩形框
                     cv::rectangle(result, boundRect, cv::Scalar(0, 255, 0), 2);
                     
                     // 在框上方显示面积信息
                     std::string text = "Red Area: " + std::to_string((int)area);
                     cv::putText(result, text, 
                                cv::Point(boundRect.x, boundRect.y - 5),
                                cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 255, 0), 1);
                     
                     // 在终端输出检测信息
                     ROS_INFO("检测到红色物体 %zu: 位置(%d, %d), 大小(%dx%d), 面积%.0f像素", 
                             i+1, boundRect.x, boundRect.y, 
                             boundRect.width, boundRect.height, area);
                 }
                 
                 // 显示检测到的物体数量
                 std::string count_text = "Red objects: " + std::to_string(contours.size());
                 cv::putText(result, count_text, cv::Point(30, 50),
                            cv::FONT_HERSHEY_SIMPLEX, 0.8, cv::Scalar(0, 255, 0), 2);
             }
             
             // 7. 显示所有窗口
             cv::imshow(ORIGINAL_WINDOW, original);
             cv::imshow(MASK_WINDOW, red_mask);
             cv::imshow(RESULT_WINDOW, result);
             
             // 8. 检查按键
             char key = (char)cv::waitKey(10);
             if (key == 'q' || key == 27) {
                 ROS_INFO("用户按了退出键,关闭节点");
                 ros::shutdown();
             }
         }
         catch (cv_bridge::Exception& e)
         {
             ROS_ERROR("cv_bridge异常: %s", e.what());
         }
     }

    public:
    ColorDetector() : it_(nh_)
    {
    ROS_INFO("🟡 启动红色物体检测节点...");

    复制代码
         // 订阅图像话题
         image_sub_ = it_.subscribe("/camera/image_raw", 1, 
                                    &ColorDetector::imageCallback, this);
         
         // 创建显示窗口
         cv::namedWindow(ORIGINAL_WINDOW, cv::WINDOW_AUTOSIZE);
         cv::namedWindow(MASK_WINDOW, cv::WINDOW_AUTOSIZE);
         cv::namedWindow(RESULT_WINDOW, cv::WINDOW_AUTOSIZE);
         
         ROS_INFO("🟢 节点已启动");
         ROS_INFO("   正在监听话题: /camera/image_raw");
         ROS_INFO("   显示3个窗口:");
         ROS_INFO("     1. Original Image - 原始图像");
         ROS_INFO("     2. Red Mask - 红色区域掩码(白色=红色区域)");
         ROS_INFO("     3. Detection Result - 检测结果(绿色框标记)");
         ROS_INFO("   按'q'或ESC键退出");
     }
     
     ~ColorDetector()
     {
         cv::destroyWindow(ORIGINAL_WINDOW);
         cv::destroyWindow(MASK_WINDOW);
         cv::destroyWindow(RESULT_WINDOW);
         ROS_INFO("节点关闭");
     }

    };

    int main(int argc, char** argv)
    {
    ros::init(argc, argv, "color_detector");
    ColorDetector detector;
    ros::spin();
    return 0;
    }

修改CMakelist.txt

复制代码
# 在现有内容后添加

# 添加新的可执行文件
add_executable(color_detector src/color_detector.cpp)

# 链接同样的库
target_link_libraries(color_detector
  ${catkin_LIBRARIES}
  ${OpenCV_LIBRARIES}
)

核心概念总结

概念 作用 类比
HSV颜色空间 分离颜色和亮度,对光照变化更鲁棒 像把"红色"从"亮红色"和"暗红色"中抽象出来
掩码(Mask) 二值图像,标记感兴趣区域 像用红色荧光笔在照片上标出所有红色物体
形态学操作 去除噪声,连接断裂区域 像用橡皮擦掉小污点,用笔连接断线
轮廓检测 找到白色区域的边界 像沿着荧光笔标记的边缘画线
外接矩形 用矩形框住检测到的物体 像给标记的区域画个绿色方框

第三步、让机器人自动跟踪检测到的红色物体

1,创建视觉跟踪节点

目标:

复制代码
如果红色物体在图像左侧 → 机器人左转
如果红色物体在图像右侧 → 机器人右转
如果红色物体在图像中心 → 机器人直行
复制代码
cd ~/turtle_ws/src/robot_vision_cpp/src
touch red_tracker.cpp

// 红色物体跟踪节点
#include <ros/ros.h>
#include <image_transport/image_transport.h>
#include <cv_bridge/cv_bridge.h>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <geometry_msgs/Twist.h>  // 新增:速度控制消息

class RedTracker
{
private:
    ros::NodeHandle nh_;
    image_transport::ImageTransport it_;
    image_transport::Subscriber image_sub_;
    
    // 新增:速度发布者
    ros::Publisher cmd_vel_pub_;
    
    // 窗口名称
    const std::string WINDOW_NAME = "Red Tracker";
    
    // 颜色范围(和之前一样)
    cv::Scalar lower_red1 = cv::Scalar(0, 100, 100);
    cv::Scalar upper_red1 = cv::Scalar(10, 255, 255);
    cv::Scalar lower_red2 = cv::Scalar(160, 100, 100);
    cv::Scalar upper_red2 = cv::Scalar(180, 255, 255);
    
    // 控制参数
    double max_linear_speed = 0.2;   // 最大线速度 m/s
    double max_angular_speed = 1.0;  // 最大角速度 rad/s
    int image_center_x = 320;        // 图像中心X坐标(640x480)
    
    void imageCallback(const sensor_msgs::ImageConstPtr& msg)
    {
        try
        {
            // 1. 转换图像
            cv_bridge::CvImagePtr cv_ptr = cv_bridge::toCvCopy(msg, "bgr8");
            cv::Mat original = cv_ptr->image;
            cv::Mat hsv_image;
            cv::cvtColor(original, hsv_image, cv::COLOR_BGR2HSV);
            
            // 2. 创建红色掩码
            cv::Mat mask1, mask2, red_mask;
            cv::inRange(hsv_image, lower_red1, upper_red1, mask1);
            cv::inRange(hsv_image, lower_red2, upper_red2, mask2);
            cv::bitwise_or(mask1, mask2, red_mask);
            
            // 3. 形态学处理
            cv::Mat kernel = cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(5, 5));
            cv::morphologyEx(red_mask, red_mask, cv::MORPH_OPEN, kernel);
            cv::morphologyEx(red_mask, red_mask, cv::MORPH_CLOSE, kernel);
            
            // 4. 寻找轮廓
            std::vector<std::vector<cv::Point>> contours;
            cv::findContours(red_mask, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
            
            // 5. 初始化控制指令
            geometry_msgs::Twist cmd_vel;
            cmd_vel.linear.x = 0.0;
            cmd_vel.angular.z = 0.0;
            
            // 6. 如果没有检测到红色物体,停止
            if (contours.empty()) {
                ROS_WARN_THROTTLE(1, "没有检测到红色物体,机器人停止");
                cmd_vel_pub_.publish(cmd_vel);
                
                // 显示提示
                cv::putText(original, "No red object - STOP", cv::Point(30, 50),
                           cv::FONT_HERSHEY_SIMPLEX, 0.8, cv::Scalar(0, 0, 255), 2);
                cv::imshow(WINDOW_NAME, original);
                cv::waitKey(10);
                return;
            }
            
            // 7. 找到最大的红色区域
            double max_area = 0;
            cv::Rect target_rect;
            
            for (size_t i = 0; i < contours.size(); i++) {
                double area = cv::contourArea(contours[i]);
                if (area > max_area && area > 500) {
                    max_area = area;
                    target_rect = cv::boundingRect(contours[i]);
                }
            }
            
            // 8. 如果找到有效目标,计算控制指令
            if (max_area > 0) {
                // 计算目标的中心点
                int target_center_x = target_rect.x + target_rect.width / 2;
                int target_center_y = target_rect.y + target_rect.height / 2;
                
                // 绘制目标框和中心点
                cv::rectangle(original, target_rect, cv::Scalar(0, 255, 0), 2);
                cv::circle(original, cv::Point(target_center_x, target_center_y), 
                          5, cv::Scalar(0, 255, 255), -1);  // 黄色中心点
                
                // 绘制图像中心线
                cv::line(original, cv::Point(image_center_x, 0), 
                        cv::Point(image_center_x, 480), cv::Scalar(255, 0, 0), 2);
                
                // 9. 【核心控制逻辑】计算误差并生成控制指令
                //    误差 = 目标中心X - 图像中心X
                double error_x = target_center_x - image_center_x;
                
                // 比例控制:角速度与误差成正比
                cmd_vel.angular.z = -error_x / 500.0;  // 比例系数1/500
                
                // 线速度:目标越大(越近),速度越慢
                double normalized_area = max_area / (640.0 * 480.0);  // 面积占比
                cmd_vel.linear.x = max_linear_speed * (1.0 - normalized_area * 5.0);
                cmd_vel.linear.x = std::max(0.0, cmd_vel.linear.x);  // 不小于0
                
                // 限制角速度范围
                if (cmd_vel.angular.z > max_angular_speed)
                    cmd_vel.angular.z = max_angular_speed;
                if (cmd_vel.angular.z < -max_angular_speed)
                    cmd_vel.angular.z = -max_angular_speed;
                
                // 10. 显示信息
                std::string info = "Target: (" + std::to_string(target_center_x) + 
                                  ", " + std::to_string(target_center_y) + ")";
                cv::putText(original, info, cv::Point(30, 30),
                           cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(255, 255, 255), 2);
                
                std::string control = "Control: lin=" + std::to_string(cmd_vel.linear.x).substr(0,4) +
                                     " ang=" + std::to_string(cmd_vel.angular.z).substr(0,4);
                cv::putText(original, control, cv::Point(30, 60),
                           cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(255, 255, 255), 2);
                
                std::string error_str = "Error: " + std::to_string((int)error_x);
                cv::putText(original, error_str, cv::Point(30, 90),
                           cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(255, 255, 255), 2);
                
                ROS_INFO("目标位置: (%d, %d), 误差: %.1f, 控制: lin=%.2f, ang=%.2f",
                        target_center_x, target_center_y, error_x, 
                        cmd_vel.linear.x, cmd_vel.angular.z);
            }
            
            // 11. 发布控制指令
            cmd_vel_pub_.publish(cmd_vel);
            
            // 12. 显示图像
            cv::imshow(WINDOW_NAME, original);
            
            // 检查退出键
            char key = (char)cv::waitKey(10);
            if (key == 'q' || key == 27) {
                // 停止机器人后退出
                cmd_vel.linear.x = 0;
                cmd_vel.angular.z = 0;
                cmd_vel_pub_.publish(cmd_vel);
                ROS_INFO("停止机器人并退出");
                ros::shutdown();
            }
        }
        catch (cv_bridge::Exception& e)
        {
            ROS_ERROR("cv_bridge异常: %s", e.what());
        }
    }

public:
    RedTracker() : it_(nh_)
    {
        ROS_INFO("🟡 启动红色物体跟踪节点...");
        
        // 订阅图像
        image_sub_ = it_.subscribe("/camera/image_raw", 1, 
                                   &RedTracker::imageCallback, this);
        
        // 初始化速度发布者(重要:控制TurtleBot的话题)
        cmd_vel_pub_ = nh_.advertise<geometry_msgs::Twist>("/cmd_vel", 10);
        
        // 创建显示窗口
        cv::namedWindow(WINDOW_NAME, cv::WINDOW_AUTOSIZE);
        
        ROS_INFO("🟢 跟踪节点已启动");
        ROS_INFO("   订阅图像: /camera/image_raw");
        ROS_INFO("   发布控制: /cmd_vel");
        ROS_INFO("   按'q'或ESC键退出");
        ROS_INFO("");
        ROS_INFO("控制逻辑说明:");
        ROS_INFO("  1. 蓝色竖线: 图像中心线");
        ROS_INFO("  2. 绿色框: 检测到的红色物体");
        ROS_INFO("  3. 黄点: 物体中心点");
        ROS_INFO("  4. 误差 = 黄点X坐标 - 蓝线X坐标");
        ROS_INFO("  5. 误差>0: 物体在右侧 → 机器人右转(负角速度)");
        ROS_INFO("  6. 误差<0: 物体在左侧 → 机器人左转(正角速度)");
    }
    
    ~RedTracker()
    {
        cv::destroyWindow(WINDOW_NAME);
        
        // 节点关闭前停止机器人
        geometry_msgs::Twist stop_cmd;
        stop_cmd.linear.x = 0;
        stop_cmd.angular.z = 0;
        cmd_vel_pub_.publish(stop_cmd);
        
        ROS_INFO("跟踪节点关闭,机器人已停止");
    }
};

int main(int argc, char** argv)
{
    ros::init(argc, argv, "red_tracker");
    RedTracker tracker;
    ros::spin();
    return 0;
}

第四步、在Gazebo中仿真,将视觉跟踪系统部署到TurtleBot3中

1。在世界文件中添加红色球体

复制代码
cd ~/turtle_ws/src/robot_vision_cpp
mkdir launch
touch launch/red_tracking_sim.launch


<launch>
    <!-- 红色物体跟踪仿真系统 -->
    
    <!-- 设置TurtleBot3型号 -->
    <arg name="model" default="waffle_pi" />
    
    <!-- 启动Gazebo空世界 -->
    <include file="$(find turtlebot3_gazebo)/launch/turtlebot3_empty_world.launch">
        <arg name="model" value="$(arg model)" />
        <!-- 注意:参数名可能是world_file而不是world_name -->
        <!-- 我们先注释掉,用默认空世界 -->
        <!-- <arg name="world_file" value="$(find turtlebot3_gazebo)/worlds/empty.world" /> -->
    </include>
    
    <!-- 在Gazebo中生成红色球体(作为跟踪目标) -->
    <node name="spawn_red_ball" pkg="gazebo_ros" type="spawn_model" 
          args="-file $(find turtlebot3_gazebo)/models/red_ball/model.sdf
                -sdf
                -model red_ball
                -x 2.0
                -y 0.0
                -z 0.5"
          output="screen" />
    
    <!-- 启动你的红色跟踪节点 -->
    <node name="red_tracker" pkg="robot_vision_cpp" type="red_tracker"
          output="screen">
        <!-- 重映射话题:仿真摄像头的话题名不同 -->
        <remap from="/camera/image_raw" to="/camera/rgb/image_raw" />
    </node>
    
    <!-- 可选:启动RViz观察 -->
    <!-- <node name="rviz" pkg="rviz" type="rviz"
          args="-d $(find turtlebot3_gazebo)/rviz/turtlebot3_gazebo_model.rviz" /> -->
    
</launch>

启动Gazebo,并若无红色球体,则手动添加红色方块

复制代码
# 终端1
export TURTLEBOT3_MODEL=waffle_pi
roslaunch turtlebot3_gazebo turtlebot3_empty_world.launch

# 1. 先删除现有模型(如果存在)
rosservice call /gazebo/delete_model '{model_name: red_ball}'

# 2. 重新生成一个更明显的红色立方体(不是球体)
echo '<?xml version="1.0"?>
<sdf version="1.6">
<model name="red_box">
<pose>1.5 0.0 0.5 0 0 0</pose>
<link name="link">
<visual name="visual">
<geometry><box><size>0.6 0.6 0.6</size></box></geometry>
<material>
  <ambient>1 0 0 1</ambient>
  <diffuse>1 0 0 1</diffuse>
  <emissive>0.5 0 0 1</emissive>  <!-- 自发光,更明显 -->
</material>
</visual>
<collision name="collision">
<geometry><box><size>0.6 0.6 0.6</size></box></geometry>
</collision>
</link>
</model>
</sdf>' | rosrun gazebo_ros spawn_model -sdf -stdin -model red_box

运行跟踪节点

复制代码
# 终端3
# 先测试摄像头是否工作
rosrun image_view image_view image:=/camera/rgb/image_raw

# 终端4
cd ~/turtle_ws
source devel/setup.bash
export TURTLEBOT3_MODEL=waffle_pi

# 运行跟踪节点
rosrun robot_vision_cpp red_tracker /camera/image_raw:=/camera/rgb/image_raw

手动移动方块,机器人会跟随移动,小项目已完成。

相关推荐
zhangrelay1 小时前
ROS 2 Lyrical 第4章 URDF机器人建模与Gazebo Garden仿真
linux·笔记·学习·ubuntu·机器人·ros2
Dave1205461 小时前
AI Agent学习 Day14:LangGraph进阶 —— 条件分支、Tool Node与Memory持久化
人工智能·学习
red_redemption2 小时前
自由学习记录(214)
学习
吴少风2 小时前
机器人仿真软件比较
机器人·仿真软件
QQ5416451213 小时前
【驿帮查件助手python开源】开源聚合查件机器人,实现 24 类驿站批量查件取代短信通知。技术方案分享
python·机器人·开源·驿站查件机器人
祈禾4 小时前
计算机组成原理之编译、汇编、链接、解释
开发语言·汇编·笔记·学习
知识分享小能手4 小时前
线性代数学习教程,从入门到精通,矩阵的初等变换与线性方程组(6)
学习·线性代数·矩阵
我命由我123455 小时前
四流一致(合同流、业务流、资金流、发票流)
经验分享·学习·职场和发展·求职招聘·职场发展·产品经理·学习方法
知识分享小能手5 小时前
线性代数学习教程,从入门到精通,矩阵的初等变换与线性方程组(5)
学习·线性代数·矩阵
xiaoxiangsiyan6 小时前
企业园区局域网交换技术完整版手册主打华为设备
运维·网络·学习·华为·路由·交换机·企业网