在之前的学习过程中,我从 ROS 的基础通信机制开始,逐步掌握了节点、话题、服务等核心概念。为了将这些知识应用到实际场景中,我尝试了一个结合视觉与机器人控制的综合项目——用二维码控制小海龟。如果大家感兴趣可以,动手实践一下~

项目简介

本项目通过摄像头识别二维码,控制 turtlesim 仿真器中的小海龟自动切换背景颜色和轨迹颜色,并执行对应的图形绘制动作(正方形、圆形、菱形等)。项目还实现了动作录制与回放、紧急停止等扩展功能,适合 ROS 初学者学习。

我的项目开源地址:https://github.com/yyyw-w/ros-turtle-projects.git


一、项目效果展示

1.1 整体效果图

摄像头识别二维码,小海龟根据指令绘制图形
(图中:摄像头识别二维码,小海龟根据指令绘制图形)

二、环境配置

2.1 开发环境

项目版本
操作系统Ubuntu 18.04 LTS
ROS版本ROS Melodic
Python版本Python 2.7
摄像头USB摄像头

2.2 安装依赖

# 更新软件源
sudo apt update

# 安装ROS基础包
sudo apt install ros-melodic-turtlesim

# 安装OpenCV
sudo apt install python-opencv

# 安装二维码识别库
sudo apt install python-zbar

2.3 创建工作空间

# 创建并初始化工作空间
mkdir -p ~/hyw_ws/src
cd ~/hyw_ws
catkin_make
source devel/setup.bash

2.4 创建功能包

cd ~/hyw_ws/src
catkin_create_pkg hyw_cv rospy std_msgs geometry_msgs turtlesim cv_bridge image_transport

三、核心代码实现

3.1 项目结构

hyw_ws/
├── src/
│   └── hyw_cv/
│       ├── CMakeLists.txt
│       ├── package.xml
│       ├── scripts/
│       │   └── qr_turtle.py      # 主程序
│       └── launch/
│           └── hyw_cv.launch      # 启动文件

3.2 主程序:qr_turtle.py

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

import rospy
import cv2
import zbar
import math
import threading
import time
from PIL import Image
from geometry_msgs.msg import Twist
from turtlesim.srv import SetPen
from std_srvs.srv import Empty

class QRTurtle:
    def __init__(self):
        rospy.init_node('qr_turtle', anonymous=False)
        
        # 动作映射表:二维码内容 -> (动作函数, 背景色RGB, 轨迹色RGB, 画笔粗细)
        self.action_map = {
            "square":   (self.draw_square,   (255, 0, 0),   (0, 255, 255), 3),
            "circle":   (self.draw_circle,   (255, 165, 0), (0, 90, 255),   3),
            "rhombus":  (self.draw_rhombus,  (255, 255, 0), (0, 0, 255),    3),
            "forward":  (self.move_forward,  (0, 255, 0),   (255, 0, 255),  3),
            "backward": (self.move_backward, (0, 255, 255), (255, 0, 0),    3),
            "left":     (self.rotate_left,   (0, 0, 255),   (255, 255, 0),  3),
            "right":    (self.rotate_right,  (128, 0, 128), (127, 255, 127),3),
            "record":   (self.cmd_record,    (100, 100, 100), (200, 200, 200), 1),
            "replay":   (self.cmd_replay,    (100, 100, 100), (200, 200, 200), 1),
            "stop":     (self.cmd_stop,      (100, 100, 100), (200, 200, 200), 1),
        }
        
        # 服务客户端
        rospy.wait_for_service('/clear')
        self.clear_srv = rospy.ServiceProxy('/clear', Empty)
        rospy.wait_for_service('/reset')
        self.reset_srv = rospy.ServiceProxy('/reset', Empty)
        rospy.wait_for_service('/turtle1/set_pen')
        self.set_pen_srv = rospy.ServiceProxy('/turtle1/set_pen', SetPen)
        
        # 速度发布器
        self.cmd_pub = rospy.Publisher('/turtle1/cmd_vel', Twist, queue_size=10)
        
        # 状态变量
        self.stop_flag = False
        self.recording = False
        self.recorded_actions = []
        self.action_thread = None
        
        # 去抖处理
        self.last_detected_word = ""
        self.last_detected_time = 0
        self.debounce_interval = 1.0
        
        # 初始化摄像头
        self.cap = cv2.VideoCapture(0)
        if not self.cap.isOpened():
            rospy.logerr("Cannot open camera")
            return
        
        self.scanner = zbar.ImageScanner()
        self.scanner.parse_config('enable')
        
        rospy.loginfo("QR Turtle started, waiting for QR code...")
        
        # 启动二维码识别线程
        thread = threading.Thread(target=self.qr_thread)
        thread.daemon = True
        thread.start()
        rospy.spin()
    
    def set_background(self, r, g, b):
        """设置背景颜色"""
        rospy.set_param('/turtlesim/background_r', r)
        rospy.set_param('/turtlesim/background_g', g)
        rospy.set_param('/turtlesim/background_b', b)
        rospy.sleep(0.1)
        self.clear_srv()
    
    def set_pen(self, r, g, b, width):
        """设置画笔颜色"""
        try:
            self.set_pen_srv(r, g, b, width, 0)
        except Exception as e:
            rospy.logerr("Pen set failed: %s", e)
    
    def reset_turtle(self):
        self.reset_srv()
    
    def stop_movement(self):
        twist = Twist()
        twist.linear.x = 0
        twist.angular.z = 0
        self.cmd_pub.publish(twist)
    
    def move_by_duration(self, linear_x, angular_z, duration, check_interval=0.05):
        twist = Twist()
        twist.linear.x = linear_x
        twist.angular.z = angular_z
        remaining = duration
        rate = rospy.Rate(int(1.0 / check_interval))
        while remaining > 0 and not self.stop_flag:
            self.cmd_pub.publish(twist)
            rate.sleep()
            remaining -= check_interval
        return not self.stop_flag
    
    def draw_square(self):
        for _ in range(4):
            if self.stop_flag: return
            if not self.move_by_duration(1.0, 0, 2.0): return
            if self.stop_flag: return
            if not self.move_by_duration(0, math.radians(90), 1.0): return
    
    def draw_circle(self):
        self.move_by_duration(1.0, math.radians(90), 6.28)
    
    def draw_rhombus(self):
        for i in range(4):
            if self.stop_flag: return
            if not self.move_by_duration(1.0, 0, 2.0): return
            if self.stop_flag: return
            if i % 2 == 0:
                if not self.move_by_duration(0, math.radians(60), 1.0): return
            else:
                if not self.move_by_duration(0, math.radians(120), 1.0): return
    
    def move_forward(self):
        self.move_by_duration(1.0, 0, 2.0)
    
    def move_backward(self):
        self.move_by_duration(-1.0, 0, 2.0)
    
    def rotate_left(self):
        if self.move_by_duration(0, math.radians(45), 1.0):
            self.move_by_duration(1.0, 0, 1.5)
    
    def rotate_right(self):
        if self.move_by_duration(0, -math.radians(45), 1.0):
            self.move_by_duration(1.0, 0, 1.5)
    
    def cmd_record(self):
        if not self.recording:
            self.recording = True
            self.recorded_actions = []
            rospy.loginfo("========== RECORDING START ==========")
        else:
            self.recording = False
            rospy.loginfo("========== RECORDING STOP ==========")
            rospy.loginfo("Recorded %d actions", len(self.recorded_actions))
    
    def cmd_replay(self):
        if not self.recorded_actions:
            rospy.loginfo("No recorded actions")
            return
        rospy.loginfo("========== REPLAY START ==========")
        self.stop_flag = False
        for action_name in self.recorded_actions:
            if self.stop_flag:
                rospy.loginfo("Replay interrupted by stop")
                break
            if action_name in self.action_map:
                action_func, bg, pen, width = self.action_map[action_name]
                self.set_background(bg[0], bg[1], bg[2])
                self.set_pen(pen[0], pen[1], pen[2], width)
                self.reset_turtle()
                action_func()
        rospy.loginfo("========== REPLAY END ==========")
    
    def cmd_stop(self):
        rospy.loginfo("========== STOP ==========")
        self.stop_flag = True
        self.stop_movement()
        self.reset_turtle()
    
    def execute_action(self, word):
        self.stop_flag = False
        action_func, bg, pen, width = self.action_map[word]
        self.set_background(bg[0], bg[1], bg[2])
        self.reset_turtle()
        self.set_pen(pen[0], pen[1], pen[2], width)
        action_func()
    
    def qr_thread(self):
        while not rospy.is_shutdown():
            ret, frame = self.cap.read()
            if ret:
                gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
                pil = Image.fromarray(gray)
                zbars = zbar.Image(pil.size[0], pil.size[1], 'Y800', pil.tobytes())
                self.scanner.scan(zbars)
                for symbol in zbars:
                    word = symbol.data.decode('utf-8').lower()
                    current_time = time.time()
                    
                    if word == self.last_detected_word and (current_time - self.last_detected_time) < self.debounce_interval:
                        continue
                    
                    self.last_detected_word = word
                    self.last_detected_time = current_time
                    rospy.loginfo("Detected: %s", word)
                    
                    if word == "stop":
                        self.cmd_stop()
                        continue
                    if word == "record":
                        self.cmd_record()
                        continue
                    if word == "replay":
                        if self.action_thread is None or not self.action_thread.is_alive():
                            self.action_thread = threading.Thread(target=self.cmd_replay)
                            self.action_thread.start()
                        continue
                    
                    if self.recording:
                        if word in self.action_map:
                            self.recorded_actions.append(word)
                            rospy.loginfo("Recorded: %s (total: %d)", word, len(self.recorded_actions))
                        continue
                    
                    if word in self.action_map:
                        if self.action_thread is None or not self.action_thread.is_alive():
                            self.action_thread = threading.Thread(target=self.execute_action, args=(word,))
                            self.action_thread.start()

if __name__ == '__main__':
    try:
        QRTurtle()
    except rospy.ROSInterruptException:
        pass

3.3 Launch文件

<?xml version="1.0"?>
<launch>
    <node name="turtlesim" pkg="turtlesim" type="turtlesim_node" output="screen" />
    <node name="qr_turtle" pkg="hyw_cv" type="qr_turtle.py" output="screen" />
</launch>

3.4 CMakeLists.txt

cmake_minimum_required(VERSION 3.0.2)
project(hyw_cv)

find_package(catkin REQUIRED COMPONENTS
  rospy
  std_msgs
  geometry_msgs
  turtlesim
  cv_bridge
  image_transport
)

catkin_package()

catkin_install_python(PROGRAMS
  scripts/qr_turtle.py
  DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)

四、编译与运行

cd ~/hyw_ws
catkin_make
source devel/setup.bash
chmod +x src/hyw_cv/scripts/qr_turtle.py
roslaunch hyw_cv hyw_cv.launch

五、二维码生成

使用 qrencode 工具生成测试二维码:

# 安装工具
sudo apt install qrencode

# 生成所有动作二维码
qrencode -o square.png "square"
qrencode -o circle.png "circle"
qrencode -o rhombus.png "rhombus"
qrencode -o forward.png "forward"
qrencode -o backward.png "backward"
qrencode -o left.png "left"
qrencode -o right.png "right"
qrencode -o record.png "record"
qrencode -o replay.png "replay"
qrencode -o stop.png "stop"

六、动作映射表

二维码内容背景颜色轨迹颜色对应动作
square红 #FF0000青 #00FFFF画正方形
circle橙 #FFA500蓝绿 #005AFF画圆
rhombus黄 #FFFF00蓝 #0000FF画菱形
forward绿 #00FF00紫红 #FF00FF前进
backward青 #00FFFF红 #FF0000后退
left蓝 #0000FF黄 #FFFF00左转45°+前进
right紫 #800080浅绿 #7FFF7F右转45°+前进
record开始/停止录制
replay回放录制动作
stop紧急停止

七、技术要点

技术点实现方式
摄像头驱动OpenCV cv2.VideoCapture
二维码识别Zbar 库
去抖处理1秒内相同单词只触发一次
多线程识别线程与动作执行分离
中断控制stop_flag 高频检查

八、参考资料


📌 项目开源地址

GitHub: https://github.com/yyyw-w/ros-turtle-projects.git

如果觉得项目对你有帮助,欢迎点个Star ⭐

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐