Turtlebot3 burger инструкция на русском

Edit on GitHub

O : Available
∆ : Need to check
X : Unavailable

Features Kinetic Melodic Noetic Dashing Foxy Galactic Humble
Teleop O O O O O O O
SLAM O O O O O O O
Navigation O O O O O O O
Simulation O O O O O O O
Manipulation O O O O O O
Home Service Challenge O O O X X X X
Autonomous Driving O X O X X X X
Machine Learning O O X O X X X
Examples Kinetic Melodic Noetic Dashing Foxy Galactic Humble
Interactive Markers O X X X X X X
Obstacle Detection O X X O X X X
Position Control O X X O X X X
Point Operation O X X O X X X
Patrol O X X O X X X
Follower O X X X X X X
Panorama O X X X X X X
Auto Parking O X X O X X X
Auto Parking(Vision) O X X X X X X
Multi TurtleBot3 O X X X X X X

Quick Start Guide

PC Setup

WARNING: The contents in this chapter corresponds to the Remote PC (your desktop or laptop PC) which will control TurtleBot3. Do not apply this instruction to your TurtleBot3.

Compatibility WARNING

  • Raspberry Pi 4 does not support ROS Kinetic.
  • Jetson Nano does not support ROS Kinetic.

NOTE: This instruction was tested on Linux with Ubuntu 16.04 and ROS Kinetic Kame.

Download and Install Ubuntu on PC

  1. Download the proper Ubuntu 16.04 LTS Desktop image for your PC from the links below.
    • Ubuntu 16.04 LTS Desktop image (64-bit)
  2. Follow the instruction below to install Ubuntu on PC.
    • Install Ubuntu desktop

Install ROS on Remote PC

Open the terminal with Ctrl+Alt+T and enter below commands one at a time.
In order to check the details of the easy installation script, please refer to the script file.

$ sudo apt-get update
$ sudo apt-get upgrade
$ wget https://raw.githubusercontent.com/ROBOTIS-GIT/robotis_tools/master/install_ros_kinetic.sh
$ chmod 755 ./install_ros_kinetic.sh 
$ bash ./install_ros_kinetic.sh

If the above installation fails, please refer to the official ROS Kinetic installation guide.

Install Dependent ROS Packages

$ sudo apt-get install ros-kinetic-joy ros-kinetic-teleop-twist-joy \
  ros-kinetic-teleop-twist-keyboard ros-kinetic-laser-proc \
  ros-kinetic-rgbd-launch ros-kinetic-depthimage-to-laserscan \
  ros-kinetic-rosserial-arduino ros-kinetic-rosserial-python \
  ros-kinetic-rosserial-server ros-kinetic-rosserial-client \
  ros-kinetic-rosserial-msgs ros-kinetic-amcl ros-kinetic-map-server \
  ros-kinetic-move-base ros-kinetic-urdf ros-kinetic-xacro \
  ros-kinetic-compressed-image-transport ros-kinetic-rqt* \
  ros-kinetic-gmapping ros-kinetic-navigation ros-kinetic-interactive-markers

Install TurtleBot3 Packages

Install TurtleBot3 via Debian Packages.

$ sudo apt-get install ros-kinetic-dynamixel-sdk
$ sudo apt-get install ros-kinetic-turtlebot3-msgs
$ sudo apt-get install ros-kinetic-turtlebot3

Click here to expand more details about building TurtleBot3 package from source.

In case you need to download the source codes and build them, please use the commands below.
Make sure to remove the identical packages to avoid redundancy.

$ sudo apt-get remove ros-kinetic-dynamixel-sdk
$ sudo apt-get remove ros-kinetic-turtlebot3-msgs
$ sudo apt-get remove ros-kinetic-turtlebot3
$ mkdir -p ~/catkin_ws/src
$ cd ~/catkin_ws/src/
$ git clone -b kinetic-devel https://github.com/ROBOTIS-GIT/DynamixelSDK.git
$ git clone -b kinetic-devel https://github.com/ROBOTIS-GIT/turtlebot3_msgs.git
$ git clone -b kinetic-devel https://github.com/ROBOTIS-GIT/turtlebot3.git
$ cd ~/catkin_ws && catkin_make
$ echo "source ~/catkin_ws/devel/setup.bash" >> ~/.bashrc

Set TurtleBot3 Model Name

Set the default TURTLEBOT3_MODEL name to your model. Enter the below command to a terminal.

  • In case of TurtleBot3 Burger
    $ echo "export TURTLEBOT3_MODEL=burger" >> ~/.bashrc
    
  • In case of TurtleBot3 Waffle Pi
    $ echo "export TURTLEBOT3_MODEL=waffle_pi" >> ~/.bashrc
    

Network Configuration

  1. Connect PC to a WiFi device and find the assigned IP address with the command below.

  2. Open the file and update the ROS IP settings with the command below.
  3. Press Ctrl+END or Alt+/ to move the cursor to the end of line.
    Modify the address of localhost in the ROS_MASTER_URI and ROS_HOSTNAME with the IP address acquired from the above terminal window.

  4. Source the bashrc with below command.

Quick Start Guide

PC Setup

WARNING: The contents in this chapter corresponds to the Remote PC (your desktop or laptop PC) which will control TurtleBot3. Do not apply this instruction to your TurtleBot3.

NOTE: This instruction was tested on Linux with Ubuntu 18.04 and ROS1 Melodic Morenia.

Download and Install Ubuntu on PC

  1. Download the proper Ubuntu 18.04 LTS Desktop image for your PC from the links below.
    • Ubuntu 18.04 LTS Desktop image (64-bit)
  2. Follow the instruction below to install Ubuntu on PC.
    • Install Ubuntu desktop

Install ROS on Remote PC

Open the terminal with Ctrl+Alt+T and enter below commands one at a time.
In order to check the details of the easy installation script, please refer to the script file.

$ sudo apt update
$ sudo apt upgrade
$ wget https://raw.githubusercontent.com/ROBOTIS-GIT/robotis_tools/master/install_ros_melodic.sh
$ chmod 755 ./install_ros_melodic.sh 
$ bash ./install_ros_melodic.sh

If the above installation fails, please refer to the official ROS1 Melodic installation guide.

Install Dependent ROS Packages

$ sudo apt-get install ros-melodic-joy ros-melodic-teleop-twist-joy \
  ros-melodic-teleop-twist-keyboard ros-melodic-laser-proc \
  ros-melodic-rgbd-launch ros-melodic-depthimage-to-laserscan \
  ros-melodic-rosserial-arduino ros-melodic-rosserial-python \
  ros-melodic-rosserial-server ros-melodic-rosserial-client \
  ros-melodic-rosserial-msgs ros-melodic-amcl ros-melodic-map-server \
  ros-melodic-move-base ros-melodic-urdf ros-melodic-xacro \
  ros-melodic-compressed-image-transport ros-melodic-rqt* \
  ros-melodic-gmapping ros-melodic-navigation ros-melodic-interactive-markers

Install TurtleBot3 Packages

Install TurtleBot3 via Debian Packages.

$ sudo apt-get install ros-melodic-dynamixel-sdk
$ sudo apt-get install ros-melodic-turtlebot3-msgs
$ sudo apt-get install ros-melodic-turtlebot3

Click here to expand more details about building TurtleBot3 package from source.

In case you need to download the source codes and build them, please use the commands below.
Make sure to remove the identical packages to avoid redundancy.

$ sudo apt-get remove ros-melodic-dynamixel-sdk
$ sudo apt-get remove ros-melodic-turtlebot3-msgs
$ sudo apt-get remove ros-melodic-turtlebot3
$ mkdir -p ~/catkin_ws/src
$ cd ~/catkin_ws/src/
$ git clone -b melodic-devel https://github.com/ROBOTIS-GIT/DynamixelSDK.git
$ git clone -b melodic-devel https://github.com/ROBOTIS-GIT/turtlebot3_msgs.git
$ git clone -b melodic-devel https://github.com/ROBOTIS-GIT/turtlebot3.git
$ cd ~/catkin_ws && catkin_make
$ echo "source ~/catkin_ws/devel/setup.bash" >> ~/.bashrc

Set TurtleBot3 Model Name

Set the default TURTLEBOT3_MODEL name to your model. Enter the below command to a terminal.

  • In case of TurtleBot3 Burger
    $ echo "export TURTLEBOT3_MODEL=burger" >> ~/.bashrc
    
  • In case of TurtleBot3 Waffle Pi
    $ echo "export TURTLEBOT3_MODEL=waffle_pi" >> ~/.bashrc
    

Network Configuration

  1. Connect PC to a WiFi device and find the assigned IP address with the command below.

  2. Open the file and update the ROS IP settings with the command below.
  3. Press Ctrl+END or Alt+/ to move the cursor to the end of line.
    Modify the address of localhost in the ROS_MASTER_URI and ROS_HOSTNAME with the IP address acquired from the above terminal window.

  4. Source the bashrc with below command.

Quick Start Guide

PC Setup

WARNING: The contents in this chapter corresponds to the Remote PC (your desktop or laptop PC) which will control TurtleBot3. Do not apply this instruction to your TurtleBot3.

Compatibility WARNING

  • Jetson Nano does not support native Ubuntu 20.04. Please refer to NVIDIA developer forum for more details.

NOTE: This instruction was tested on Linux with Ubuntu 20.04 and ROS1 Noetic Ninjemys.

Download and Install Ubuntu on PC

  1. Download the proper Ubuntu 20.04 LTS Desktop image for your PC from the links below.
    • Ubuntu 20.04 LTS Desktop image (64-bit)
  2. Follow the instruction below to install Ubuntu on PC.
    • Install Ubuntu desktop

Install ROS on Remote PC

Open the terminal with Ctrl+Alt+T and enter below commands one at a time.
In order to check the details of the easy installation script, please refer to the script file.

$ sudo apt update
$ sudo apt upgrade
$ wget https://raw.githubusercontent.com/ROBOTIS-GIT/robotis_tools/master/install_ros_noetic.sh
$ chmod 755 ./install_ros_noetic.sh 
$ bash ./install_ros_noetic.sh

If the above installation fails, please refer to the official ROS1 Noetic installation guide.

Install Dependent ROS Packages

$ sudo apt-get install ros-noetic-joy ros-noetic-teleop-twist-joy \
  ros-noetic-teleop-twist-keyboard ros-noetic-laser-proc \
  ros-noetic-rgbd-launch ros-noetic-rosserial-arduino \
  ros-noetic-rosserial-python ros-noetic-rosserial-client \
  ros-noetic-rosserial-msgs ros-noetic-amcl ros-noetic-map-server \
  ros-noetic-move-base ros-noetic-urdf ros-noetic-xacro \
  ros-noetic-compressed-image-transport ros-noetic-rqt* ros-noetic-rviz \
  ros-noetic-gmapping ros-noetic-navigation ros-noetic-interactive-markers

Install TurtleBot3 Packages

Install TurtleBot3 via Debian Packages.

$ sudo apt install ros-noetic-dynamixel-sdk
$ sudo apt install ros-noetic-turtlebot3-msgs
$ sudo apt install ros-noetic-turtlebot3

Click here to expand more details about building TurtleBot3 package from source.

In case you need to download the source codes and build them, please use the commands below.
Make sure to remove the identical packages to avoid redundancy.

$ sudo apt remove ros-noetic-dynamixel-sdk
$ sudo apt remove ros-noetic-turtlebot3-msgs
$ sudo apt remove ros-noetic-turtlebot3
$ mkdir -p ~/catkin_ws/src
$ cd ~/catkin_ws/src/
$ git clone -b noetic-devel https://github.com/ROBOTIS-GIT/DynamixelSDK.git
$ git clone -b noetic-devel https://github.com/ROBOTIS-GIT/turtlebot3_msgs.git
$ git clone -b noetic-devel https://github.com/ROBOTIS-GIT/turtlebot3.git
$ cd ~/catkin_ws && catkin_make
$ echo "source ~/catkin_ws/devel/setup.bash" >> ~/.bashrc

Network Configuration

  1. Connect PC to a WiFi device and find the assigned IP address with the command below.

  2. Open the file and update the ROS IP settings with the command below.
  3. Press Ctrl+END or Alt+/ to move the cursor to the end of line.
    Modify the address of localhost in the ROS_MASTER_URI and ROS_HOSTNAME with the IP address acquired from the above terminal window.

  4. Source the bashrc with below command.

Quick Start Guide

PC Setup

WARNING: The contents in this chapter corresponds to the Remote PC (your desktop or laptop PC) which will control TurtleBot3. Do not apply this instruction to your TurtleBot3.

NOTE: This instruction was tested on Linux with Ubuntu 18.04 and ROS2 Dashing Diademata.

Download and Install Ubuntu on PC

  1. Download the proper Ubuntu 18.04 LTS Desktop image for your PC from the links below.
    • Ubuntu 18.04 LTS Desktop image (64-bit)
  2. Follow the instruction below to install Ubuntu on PC.
    • Install Ubuntu desktop

Install ROS 2 on Remote PC

Open the terminal with Ctrl+Alt+T and enter below commands one at a time.
In order to check the details of the easy installation script, please refer to the script file.

$ sudo apt-get update
$ sudo apt-get upgrade
$ wget https://raw.githubusercontent.com/ROBOTIS-GIT/robotis_tools/master/install_ros2_dashing.sh
$ chmod 755 ./install_ros2_dashing.sh
$ bash ./install_ros2_dashing.sh

If the above installation fails, please refer to the official ROS2 Dashing installation guide.

Install Dependent ROS 2 Packages

  1. Open the terminal with Ctrl+Alt+T from Remote PC.
  2. Install Colcon
    $ sudo apt install python3-colcon-common-extensions
    
  3. Install Gazebo9
    $ curl -sSL http://get.gazebosim.org | sh
    
  4. Uninstall Gazebo11 if installed previously
    $ sudo apt remove gazebo11 libgazebo11-dev
    $ sudo apt install gazebo9 libgazebo9-dev
    $ sudo apt install ros-dashing-gazebo-ros-pkgs
    
  5. Install Cartographer
    $ sudo apt install ros-dashing-cartographer
    $ sudo apt install ros-dashing-cartographer-ros
    
  6. Install Navigation2
    $ sudo apt install ros-dashing-navigation2
    $ sudo apt install ros-dashing-nav2-bringup
    
  7. Install vcstool
    $ sudo apt install python3-vcstool
    

Install TurtleBot3 Packages

Install TurtleBot3 via Debian Packages.

$ source /opt/ros/dashing/setup.bash
$ sudo apt install ros-dashing-dynamixel-sdk
$ sudo apt install ros-dashing-turtlebot3-msgs
$ sudo apt install ros-dashing-turtlebot3

Click here to expand more details about building TurtleBot3 package from source.

In case you need to build the TurtleBot3 packages with source code, please use the commands below.
Building the source code provides most up to date contents which may have resolved known issues.
Make sure to remove the binary packages to avoid redundancy.

$ sudo apt remove ros-dashing-turtlebot3-msgs
$ sudo apt remove ros-dashing-turtlebot3
$ mkdir -p ~/turtlebot3_ws/src
$ cd ~/turtlebot3_ws/src/
$ git clone -b dashing-devel https://github.com/ROBOTIS-GIT/DynamixelSDK.git
$ git clone -b dashing-devel https://github.com/ROBOTIS-GIT/turtlebot3_msgs.git
$ git clone -b dashing-devel https://github.com/ROBOTIS-GIT/turtlebot3.git
$ colcon build --symlink-install
$ source ~/.bashrc

Environment Configuration

  1. Set the ROS environment for PC.
    $ echo 'source ~/turtlebot3_ws/install/setup.bash' >> ~/.bashrc
    $ echo 'export ROS_DOMAIN_ID=30 #TURTLEBOT3' >> ~/.bashrc
    $ source ~/.bashrc
    

If you have installed TurtleBot3 using apt install command, you can ignore the warning below.

bash: /home/{$YOUR_ACCOUNT}/turtlebot3_ws/install/setup.bash: No such file or directory

Quick Start Guide

PC Setup

WARNING: The contents in this chapter corresponds to the Remote PC (your desktop or laptop PC) which will control TurtleBot3. Do not apply this instruction to your TurtleBot3.

Compatibility WARNING

  • Jetson Nano does not support Ubuntu 20.04 and later. Please refer to NVIDIA developer forum for more details.

NOTE: This instruction was tested on Linux with Ubuntu 20.04 and ROS2 Foxy Fitzroy.

Download and Install Ubuntu on PC

  1. Download the proper Ubuntu 20.04 LTS Desktop image for your PC from the links below.
    • Ubuntu 20.04 LTS Desktop image (64-bit)
  2. Follow the instruction below to install Ubuntu on PC.
    • Install Ubuntu desktop

Install ROS 2 on Remote PC

Open the terminal with Ctrl+Alt+T and enter below commands one at a time.
In order to check the details of the easy installation script, please refer to the script file.

$ wget https://raw.githubusercontent.com/ROBOTIS-GIT/robotis_tools/master/install_ros2_foxy.sh
$ sudo chmod 755 ./install_ros2_foxy.sh
$ bash ./install_ros2_foxy.sh

If the above installation fails, please refer to the official ROS2 Foxy installation guide.

Install Dependent ROS 2 Packages

  1. Open the terminal with Ctrl+Alt+T from Remote PC.
  2. Install Gazebo11
    $ sudo apt-get install ros-foxy-gazebo-*
    
  3. Install Cartographer
    $ sudo apt install ros-foxy-cartographer
    $ sudo apt install ros-foxy-cartographer-ros
    
  4. Install Navigation2
    $ sudo apt install ros-foxy-navigation2
    $ sudo apt install ros-foxy-nav2-bringup
    

Install TurtleBot3 Packages

Install TurtleBot3 via Debian Packages.

$ source ~/.bashrc
$ sudo apt install ros-foxy-dynamixel-sdk
$ sudo apt install ros-foxy-turtlebot3-msgs
$ sudo apt install ros-foxy-turtlebot3

Click here to expand more details about building TurtleBot3 package from source.

In case you need to build the TurtleBot3 packages with source code, please use the commands below.
Building the source code provides most up to date contents which may have resolved known issues.
Make sure to remove the binary packages to avoid redundancy.

$ sudo apt remove ros-foxy-turtlebot3-msgs
$ sudo apt remove ros-foxy-turtlebot3
$ mkdir -p ~/turtlebot3_ws/src
$ cd ~/turtlebot3_ws/src/
$ git clone -b foxy-devel https://github.com/ROBOTIS-GIT/DynamixelSDK.git
$ git clone -b foxy-devel https://github.com/ROBOTIS-GIT/turtlebot3_msgs.git
$ git clone -b foxy-devel https://github.com/ROBOTIS-GIT/turtlebot3.git
$ cd ~/turtlebot3_ws
$ colcon build --symlink-install
$ echo 'source ~/turtlebot3_ws/install/setup.bash' >> ~/.bashrc
$ source ~/.bashrc

Environment Configuration

  1. Set the ROS environment for PC.
    $ echo 'export ROS_DOMAIN_ID=30 #TURTLEBOT3' >> ~/.bashrc
    $ source ~/.bashrc
    

If you have installed TurtleBot3 using Debian packages with apt install command, you can ignore the warning below.

bash: /home/{$YOUR_ACCOUNT}/turtlebot3_ws/install/setup.bash: No such file or directory

Quick Start Guide

PC Setup

WARNING: The contents in this chapter corresponds to the Remote PC (your desktop or laptop PC) which will control TurtleBot3. Do not apply this instruction to your TurtleBot3.

Compatibility WARNING

  • Jetson Nano does not support Ubuntu 20.04 and later. Please refer to NVIDIA developer forum for more details.

NOTE: This instruction was tested on Linux with Ubuntu 22.04 and ROS2 Humble Hawksbill.

Download and Install Ubuntu on PC

  1. Download the proper Ubuntu 22.04 LTS Desktop image for your PC from the links below.
    • Ubuntu 22.04 LTS Desktop image (64-bit)
  2. Follow the instruction below to install Ubuntu on PC.
    • Install Ubuntu desktop

Install ROS 2 on Remote PC

Please follow the official ROS2 documentation to install the ROS2 Humble.
For most Linux users, Debian package installation method is strongly recommended.

Install Dependent ROS 2 Packages

  1. Open the terminal with Ctrl+Alt+T from Remote PC.
  2. Install Gazebo
    $ sudo apt install ros-humble-gazebo-*
    
  3. Install Cartographer
    $ sudo apt install ros-humble-cartographer
    $ sudo apt install ros-humble-cartographer-ros
    
  4. Install Navigation2
    $ sudo apt install ros-humble-navigation2
    $ sudo apt install ros-humble-nav2-bringup
    

Install TurtleBot3 Packages

Install TurtleBot3 via Debian Packages.

$ source ~/.bashrc
$ sudo apt install ros-humble-dynamixel-sdk
$ sudo apt install ros-humble-turtlebot3-msgs
$ sudo apt install ros-humble-turtlebot3

Click here to expand more details about building TurtleBot3 package from source.

In case you need to build the TurtleBot3 packages with source code, please use the commands below.
Building the source code provides most up to date contents which may have resolved known issues.
Make sure to remove the binary packages to avoid redundancy.

$ sudo apt remove ros-humble-turtlebot3-msgs
$ sudo apt remove ros-humble-turtlebot3
$ mkdir -p ~/turtlebot3_ws/src
$ cd ~/turtlebot3_ws/src/
$ git clone -b humble-devel https://github.com/ROBOTIS-GIT/DynamixelSDK.git
$ git clone -b humble-devel https://github.com/ROBOTIS-GIT/turtlebot3_msgs.git
$ git clone -b humble-devel https://github.com/ROBOTIS-GIT/turtlebot3.git
$ cd ~/turtlebot3_ws
$ colcon build --symlink-install
$ echo 'source ~/turtlebot3_ws/install/setup.bash' >> ~/.bashrc
$ source ~/.bashrc

Environment Configuration

  1. Set the ROS environment for PC.
    $ echo 'export ROS_DOMAIN_ID=30 #TURTLEBOT3' >> ~/.bashrc
    $ source ~/.bashrc
    

If you have installed TurtleBot3 using Debian packages with apt install command, you can ignore the warning below.

bash: /home/{$YOUR_ACCOUNT}/turtlebot3_ws/install/setup.bash: No such file or directory

Quick Start Guide

PC Setup

NOTE: This instruction was tested on Windows with Windows 10 IoT Enterprise and ROS1 Melodic Morenia or ROS1 Noetic Ninjemys.

WARNING

  • This SBC Setup section for Windows is tested with Intel Up² or Intel NUC on TurtleBot3.
  • This SBC will be operating as Remote PC and SBC at the same time.
  • This process may take long time. Please do not use battery while following this section.
  • An HDMI monitor and input devices such as a keyboard and a mouse will be required to complete this setup.

Setup Remote PC

  1. If you do not already have Windows 10 on your Remote PC (Desktop, Laptop or SBC), you can download a trial of Windows 10 IoT Enterprise from the following link:
    • Download Windows 10 IoT Enterprise LTSC(Trial)
  2. Please refer to the ROS Wiki instructions for installing ROS on Windows.

  3. Install LDS-01 Lidar driver
    • CP2102 Driver
  4. Create workspace and download TurtleBot3 packages.
    For more details, please refer to Microsoft Instructions.

    > mkdir c:\ws\turtlebot3\src
    > cd c:\ws\turtlebot3
    > curl -o tb3.repos https://raw.githubusercontent.com/ms-iot/ROSOnWindows/master/docs/Turtlebot/turtlebot3_ros1.repos
    > vcs import src < tb3.repos
    
  5. Customize TurtleBot3 Launch Files.
    Modify the ROS Launch files to map the devices to the correct COM port. To determine which COM ports you require, right click on the Windows Start Menu, Select Device Manager.

    • Under the Ports (COM & LPT) node:
      • USB Serial Device (COMx) is the OpenCR board.
      • Silicon Labs CP210x USB to UART Bridge (COMy) is the Lidar.
    • Enter the COM port in the correct fields in the launch files below:

      • turtlebot3_bringup/launch/turtlebot3_core.launch
         <node pkg="rosserial_python" type="serial_node.py" name="turtlebot3_core" output="screen">
             <param name="port" value="COMx"/>
      
      • turtlebot3_bringup/launch/turtlebot3_lidar.launch
         <node pkg="hls_lfcd_lds_driver" type="hlds_laser_publisher" name="turtlebot3_lds" output="screen">
             <param name="port" value="COMy"/>
      
  6. Build the Workspace
    > cd c:\ws\turtlebot3
    > catkin_make
    > c:\ws\turtlebot3\devel\setup.bat
    

Network Configuration

To communicate from a Windows 10 system to a remote single board computer (SBC) running on the turtlebot, set the following environment variables:

> set ROS_MASTER_URI=http://<IP address of the PC>:11311
> set ROS_HOSTNAME=<name of the windows computer>

Содержание

  • 1 Установка и настройка ПО
    • 1.1 Загрузка и установка
    • 1.2 Настройка
  • 2 Симулятор Gazebo
    • 2.1 Введение
    • 2.2 Соревновательное поле
    • 2.3 Запуск симулятора с моделями и базовые действия
    • 2.4 Запуск миссий
    • 2.5 Модель робота
    • 2.6 Визуализация изображений при помощи утилиты rqt
  • 3 Детектирование
    • 3.1 Детектирования дорожной разметки
    • 3.2 Детектирование и распознавание дорожных знаков
    • 3.3 Детектирование огней светофора
    • 3.4 Детектирование шлагбаума
  • 4 Выполнение миссий
    • 4.1 Езда по линии
    • 4.2 Остановка на светофоре
    • 4.3 Парковка
    • 4.4 Остановка перед шлагбаумом
    • 4.5 Лабиринт
  • 5 Создание и запуск launch файлов, исправление возможных ошибок
    • 5.1 Launch файлы
    • 5.2 Использований rqt_graph для отслеживания ошибок
    • 5.3 Описание последовательного запуска всех элементов

Установка и настройка ПО

Загрузка и установка

Для полноценной работы с фреймворком ROS необходим дистрибутив операционной системы с ядром linux, для этого можно использовать виртуальную машину. То есть по сути оболочку для запуска операционной системы внутри операционной системы, для этого понадобится программа VirualBox. Также необходимо скачать сам образ операционной системы с предустановленным ROS, который можно будет открыть с помощью программы VirtualBox. Для этого необходимо скачать архив по этой ссылке на гугл-диск
После скачивания его нужно разархивировать в удобную вам папку. После разархивирования можно увидеть следующую структуру папок и файлов Ubuntu 64-bit:

1.png

Файлы виртуальной машины
Данные файлы нельзя как либо модифицировать и изменять во избежании всевозможных поломок ПО. В дальнейшем с помощью программы virtualBox будет открываться файл Ubuntu 64-bit с расширением Virtual Machine Disk Format (.vmdk)

Настройка

Для первого запуска виртуальной машины необходимо:
1) Запустить программу virtualBox
2) Нажать кнопку создать, чтобы создать новую виртуальную машину

12.png

3) В высветившимся окне нажать кнопку экспертный режим

13.png

4) Настраиваем виртуальную машину:

4.1) Задать любое имя машины
4.2) Выбрать папку в которой будут храниться данные виртуальной машины, предложенную папку можно не изменять
4.3) Тип машины — Linux, версия Ubuntu (64-bit)
4.4) Выбрать количество оперативной памяти, которое будет доступно виртуальной машине. ДЛЯ КОРРЕКТНОЙ РАБОТЫ НЕОБХОДИМО НЕ МЕНЕЕ 6 ГБ

5.png

4.5) В графе жесткий диск выбрать пункт использовать существующий виртуальный диск, и в качестве виртуального диска выбрать файл Ubuntu 64-bit.vmdk описанный в предыдущем пункте

6.png

7.png

8.png

9.png

5) Когда все настройки выставлены в соответствии с пунктом 4 необходимо нажать кнопку создать.
6) Перед запуском виртуальной машины, необходимо настроить количество ядер процессора, которые она сможет использовать (для комфортной работы рекомендуется не менее 4). Для этого:

6.1) Необходимо нажать кнопку настроить

10.png

6.2) Перейти в графу система

11.png

6.3) Перейти во вкладку процессор и разрешить использование 4 или более ядер (количество разрешенных к использованию ядер зависит от общего количества ядер процессора)

1222.png

13333.png

7) После всех настроек можно запустить виртуальную машину, для этого необходимо:

7.1) Нажать кнопку запустить

14.png

7.2) После загрузки системы появится следующее окно:

15.png

7.3) В данном окне нажать на пользователя tb3, после чего высветится окно для ввода пароля:

16.png

7.4) Ввести пароль tb3, после чего откроется окно системы:

17.png

Note: если окно системы слишком маленькое для работы, то в настройках ubuntu необходимо изменить разрешение экрана:

18.png

Теперь внутри данного окна можно вести полноценную работу с операционной системой Ubuntu. Далее будет описан симулятор Gazebo и запуск данного симулятора внутри виртуальной машины.

Симулятор Gazebo

Введение

Gazebo 3Dразрабатываемый некоммерческой организацией OSRF (Open Source Robotics Foundation), имеет ряд преимуществ по сравнению с другими робототехническими симуляторами. Во-первых, он бесплатный и имеет открытый код. Во-вторых, он очень популярен среди мирового робототехнического сообщества и является официальным симулятором соревнований DARPA. В-третьих, Gazebo отлично интегрируется с программной платформой ROS (Robot Operating System), а значит разработанную программу управления виртуальным роботом в Gazebo и ROS будет относительно несложно перенести на реального робота.
В данном симуляторе будет запускаться робот turtlebot3, на поле соревнований turtlebot3 autorace:

19.png

Суть данных соревнований в том, что робот должен проехать по размеченной линии параллельно выполняя различные миссии:

  • остановка на светофоре, в самом начале движения (на рисунке видно как горит зеленый сигнал светофора)
  • парковка, в одной из двух специально отведенных зон (одна из зон будет занята другим роботом)
  • остановка перед шлагбаумом
  • прохождение лабиринта (в лабиринте линии нет, поэтому он проходится при помощи лазерного дальномера)

Данная задача является очень интересной и многогранной, помогает улучшить навыки в области компьютерного зрения, алгоритмики и т.д

Соревновательное поле

Рассмотрим соревновательное поле и миссии подробнее. Поле представляет из себя квадратную поверхность на которую нанесены желтая и белая линии, которые формируют дорожное полотно. По регламенту соревнований робот не должен выезжать за пределы дорожной полосы, иначе ему будут начислены штрафные баллы. Поэтому очень важно реализовать максимально точное следование линии.
Также разберем все миссии:
1) Остановка на светофоре
Первой миссией является остановка на светофоре. Изначально на светофоре горит зеленый сигнал. Когда робот подъезжает к нему, зеленый сигнал сменяется красным и робот должен остановиться. Затем робот должен ждать повторного появления зеленого сигнала и только после этого продолжать движение. Пример того, что видит робот при подъезде к светофору:

888.png

2) Парковка
Второй миссией, которую робот должен пройти является парковка. В рамках данной миссии, робот должен заехать в одно из двух отведенных мест парковки (другое место занимает второй робот). В качестве ориентира для начала миссии используется знак парковки:

889.png

3) Шлагбаум
Третьей миссией является остановка перед шлагбаумом. При подъезде робота к перекрестку шлагбаум опустится. У робота есть два варианта действий, либо остановиться перед шлагбаумом и ждать пока тот поднимется, либо объехать его по прилегающей дороге. Также рядом с шлагбаумом расположен знак stop:

22.png

4) Туннель
Последней миссией для робота является замкнутый лабиринт, имеющий один вход и один выход. Для ориентации в тоннеле робот не может использовать камеры, но может использовать остальные имеющиеся датчики. Главная задача — выехать из туннеля через зону выхода. Зона входа же в свою очередь помечена знаком:

23.png

Конечной целью робота является прохождение всей миссий за наименьшее время.

Запуск симулятора с моделями и базовые действия

Для запуска симулятора, с моделью робота и соревновательным полем необходимо открыть терминал (ctr+alt+t) и написать следующую команду:
$roslaunch turtlebot3_gazebo turtlebot3_autorace.launch
При первой загрузке симулятору потребуется время, чтобы настроить окружение, это может занять примерно 5-10 минут.
Окно запущенного симулятора выглядит так:

24.png

Для изменения ракурса используется правая кнопка мыши. Для приближения используется колесо мыши. С помощью левой кнопки мыши можно выделять необходимые объекты.
Для того чтобы передвинуть какой либо объект необходимо воспользоваться режимом переноса, чтобы его активировать в панели управления необходимо нажать на данную иконку:

25.png

После чего левой кнопкой мыши нажать на объект, который необходимо передвинуть (категорически не рекомендуется передвигать что то кроме робота!). На объекте высветится система координат:

26.png

Чтобы передвинуть объект вдоль какой-либо оси, необходимо потянуть за эту ось с нажатой левой кнопкой мыши.
Аналогичным способом можно вращать объекты, в режиме вращения. Для этого необходимо нажать на иконку вращения в панели управления:

27.png

Если в процессе работы, что то пошло не так и необходимо внести изменения в программный код, то всю симуляцию можно перезапустить. Все выполняемые внешние исполнительные файлы завершат свою работу, а все элементы поля в том числе робот вернутся на изначальные места, для этого необходимо нажать на вкладку edit->reset world.

Запуск миссий

Изначально при запуске мира, миссии не запускаются. То есть светофор не горит, оба места для парковки остаются не занятыми и шлагбаум не опускается. Для того, чтобы активировать миссии в терминале необходимо написать следующую команду: $roslaunch turtlebot3_gazebo turtlebot3_autorace_mission.launch.
Соответственно, до запуска данной команды соревновательное поле выглядело так:

28.png

После запуска команды поле выглядит вот так:

29.png

Видно, что зеленый сигнал светофора загорелся и одно из парковочных мест оказалось занято другим роботом.
Миссии активируются, исходя из показаний топика odom робота (топик, в котором хранятся данные колесной одометрии, подробнее будет рассмотрен далее). То есть каждая миссия будет активироваться только тогда, когда робот самостоятельно доехал до нее, если просто передвигать робота в какую-либо точку то миссии активироваться не будут.

Модель робота

В симуляторе запускается точная копия робота turtlebot3, включая все топики реального робота. Чаще всего использоваться будут следующие топики:
scan: топик в который публикуются данные лазерного дальномера, в формате сообщения sensor_msgs/LaserScan. Дальномер делает 360 замеров на один оборот, то есть его разрешающая способность один градус. Все расстояния хранятся в массиве ranges, который входит в сообщение LaserScan, всего там 360 элементов. Ниже на рисунке можно увидеть как отсчитываются углы:

87.png

odom: топик в который публикуется колесная одометрия робота, то есть его перемещение от точки старт рассчитанное, исходя из показаний датчиков оборотов, установленных в привода. Так как в симулятора Gazebo также симулируется трение, показания в данном топике со временем будут накапливать ошибку, и отличаться от реального перемещения робота. В данный топик публикуются сообщения типа: nav_msgs/Odometry.
cmd_vel: топик который управляет скоростью движения робота, подробнее его работа рассмотрена в пункте Езда по линии.
Кроме основных элементов, на модель робота также установлено две камеры. Графического отображения они не имеют, схематичное расположение камер показано на рисунке ниже:

30.png

Таким образом одна из камер направлена на линию (camera_line), а вторая на дорожные знаки. Изображение полученные с камер выглядят следующим образом (слева с камеры линии, справа с камеры направленной на знаки):

889.png

8899.png

Изображение с камеры линии поступает в топик: /camera_line/image_line. Изображение с камеры направленной на знаки поступает в топик: /camera/image.
В данные топики поступает сообщение типа: sensor_msgs/Image, которое не получится визуализировать с помощью библиотеки OpenCV, поэтому для визуализации используется утилита rqt.

Визуализация изображений при помощи утилиты rqt

Чтобы визуализировать изображение необходимо запустить данную утилиту написав следующую команду в терминале:
$ rqt
На экране должно появиться пустое окно, а на панели управления иконка утилиты:

Untitled.png

После в панели управления необходимо выбрать пункт: Plugins->Visualization->Image View
После этого откроется окно визуализации, в котором необходимо выбрать топик, информацию с которого нужно визуализировать (красным выделено окно выбора топика):

34.png

В результате изображение будет в онлайн режиме транслироваться на экран в следующем виде:

888.png

Для добавление визуализаций с других топиков необходимо повторить данный процесс и рядом откроются новые окна.

Детектирование

Детектирования дорожной разметки

Для детектирования линий дорожной разметки используется довольно таки простой алгоритм — бинаризация по цветовому порогу.
Изображение напрямую с виртуальной камеры, направленной на дорогу публикуется в закодированном виде в топик /camera_line/image_line. Поиск цвета на изображении происходит по цветовой модели HSV. Данная модель очень полезна, если вам необходимо оперировать всем спектром цветов при помощи одной переменной. Название модели состоит из первых букв от слов Hue – тон, Saturation – насыщенность и Value – значение (Brightness – яркость). Тон варьируется от 0 до 255 , насыщенность и яркость так же – от 0 до 255.

36.png

Теперь изображение с камеры бинаризовано по цветовому порогу, то есть получено черно-белое изображение на котором белые пиксели — это пиксели дорожной разметки (белой и желтой), а черные пиксели — все остальное. Далее полученные белые полосы переводятся в массивы точек. После этого рассчитывается отклонение (или ошибка) от идеального положения робота в данный момент времени и это отклонение отправляется в ноду, отвечающую за управление колесами. Также во время того как белые линии конвертируются в массивы точек, на исходном изображении рисуется красная линия, показывающая какие именно точки были задетектированы, это изображение публикуется в топик /image.

37.png

Рассмотрим код программы подробнее (файл line_detect.py):

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import rospy
import numpy as np
import cv2
from sensor_msgs.msg import Image, CompressedImage
from cv_bridge import CvBridge
from time import sleep
from std_msgs.msg import Float64

Здесь подключаем необходимые библиотеки: библиотеку rospy для подключения ros окружения, numpy для работы с массивами, opencv для обработки изображений, cv_bridge для конвертирования изображения из ros сообщения в numpy массив, также модуль задержки sleep из библиотеки time. Для полноценного общения с ros окружением необходимо также добавить используемые сообщения: изображение и сжатое изображения и float64 (дробное число).

pub_image = rospy.Publisher('image', Image, queue_size=1)
pub_error = rospy.Publisher('line_error', Float64, queue_size=1)
cvBridge = CvBridge()

Создаем необходимые глобальные переменные: объект publisher для публикации обработанного изображения (это необходимо для отладки), объект publisher для публикации отклонения от идеального положения и объект cvBridge для конвертирования сообщения типа image в numpy массив.

def cbImageProjection(data):
    cv_image_original = cvBridge.imgmsg_to_cv2(data, "bgr8")
    cv_image_original = cv2.GaussianBlur(cv_image_original, (5, 5), 0)
    yellow_detect, yellow_array = mask_yellow(cv_image_original)
    white_detect, white_array = mask_white(cv_image_original)
    detected = cv2.add(white_detect, yellow_detect)
    pub_image.publish(cvBridge.cv2_to_imgmsg(detected, "bgr8"))
    error = Float64()
    error.data = calculate_error(yellow_array, white_array)
    pub_error.publish(error)

Данная функция является колбэк функцией для изображения, приходящего с камеры, она выполняет все необходимые преобразования, рассчитывает отклонение от идеального положения и отсылает это отклонение в топик “line_error”. Разберем ее поэтапно:

1) Переводим изображение из сообщения Image в numpy array, чтобы с ним можно было работать функциями библиотеки OpenCV
2) “Блюрим” изображение, то есть немного размываем его, чтобы избавиться от различных шумов и артефактов. Выполняется это с помощ Гауссиан блюра, смысл работы которого в том, что значения какой то группы соседних пикселей усредняется, таким образом мы можем добиться плавного размытия.
3) Вызываем функции детектирования желтой и белой линий соответственно. Получаем при этом бинаризованные изображения, где линии — белые пиксели. Также получаем массив точек, принадлежащих линии.
4) Затем необходимо создать изображение, использующееся для отладки, для этого объединяем изображения с белой и желтой линии. После этого публикуем данное изображение в топик “image”.
5) Финальным этапом является расчет отклонения от идеального положения и публикация этого отклонения в топик “line_error”.
def mask_yellow(img):
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    Hue_l = 27
    Hue_h = 41
    Saturation_l = 130
    Saturation_h = 255
    Lightness_l = 160
    Lightness_h = 255
    # define range of yellow color in HSV
    lower_yellow = np.array([Hue_l, Saturation_l, Lightness_l])
    upper_yellow = np.array([Hue_h, Saturation_h, Lightness_h])
    # Threshold the HSV image to get only yellow colors
    mask = cv2.inRange(hsv, lower_yellow, upper_yellow)
    # Bitwise-AND mask and original image
    res = cv2.bitwise_and(img, img, mask = mask)
    fraction_num = np.count_nonzero(mask)
    point_arr = []
    stop_flag = False
    if fraction_num > 50:
   	 k = 0
   	 jold = 0
   	 for i in range(mask.shape[0]-1,0,-15):
   		 if stop_flag == True:
   			 break
   		 for j in range(0,mask.shape[1],15):
if mask[i,j] > 0:
   				 point_arr.append([j,i])
   				 k+=1
   				 if abs(j-jold) > 80 and k > 1:
   					 point_arr.pop()
   					 stop_flag = True
   				 jold = j
   				 break
   	 if(len(point_arr) > 0):
   		 point_before = point_arr[0]
   		 for point in point_arr:
   			 res = cv2.line(res, (point[0], point[1]), (point_before[0],point_before[1]), (0,0,255),8)
   			 point_before = point
    return res, point_arr
    

def mask_white(img):
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    Hue_l = 0
    Hue_h = 25
    Saturation_l = 0
    Saturation_h = 36
    Lightness_l = 180
    Lightness_h = 255
    # define range of yellow color in HSV
    lower_white = np.array([Hue_l, Saturation_l, Lightness_l])
    upper_white = np.array([Hue_h, Saturation_h, Lightness_h])
    # Threshold the HSV image to get only yellow colors
    mask = cv2.inRange(hsv, lower_white, upper_white)
    # Bitwise-AND mask and original image
    res = cv2.bitwise_and(img, img, mask = mask)
    fraction_num = np.count_nonzero(mask)
    point_arr = []
    stop_flag = False
    if fraction_num > 50:
   	 k = 0
   	 jold = 0
   	 for i in range(mask.shape[0]-1,0,-20):
   		 if stop_flag == True:
   			 break
   		 for j in range(mask.shape[1]-1,0,-20):
   			 if mask[i,j] > 0:
   				 point_arr.append([j,i])
   				 k+=1
   				 if abs(j-jold) > 80 and k > 1:
   					 point_arr.pop()
   					 stop_flag = True
   				 jold = j
   				 break
   	 if len(point_arr) > 0:
   		 point_before = point_arr[0]
   		 for point in point_arr:
   			 res = cv2.line(res, (point[0], point[1]), (point_before[0],point_before[1]), (0,0,255),8)
   			 point_before = point
    return res, point_arr

Выше описаны функции детектирования белой и желтой линии соответственно. Они практически идентичны, поэтому разберем только одну из них, а именно функцию для белой линии. Сначала необходимо перевести изображение в HSV формат, о том что это за формат говорилось ранее. После чего создаются массивы пороговых значений цветов для бинаризации, для белого цвета это:
Минимальные значения цвета по каналам H, S и V = 0,0,180
Максимальные значения цвета по каналам H, S и V = 25, 36, 255
Затем используя эти пороги бинаризуем изображение и получаем его “маску”.
Теперь используя эту маску можно найти все точки принадлежащие линии, но перед этим создается изображение для отладки. В данном изображении белым изображены выделенные пиксели, а остальные пиксели имеют свой обычный цвет в rgb формате. Проверив, что на изображении достаточно белых пикселей можно перейти к алгоритму нахождения точек принадлежащих линии на изображении.
Искать все точки, принадлежащие линии очень затратно и не имеет смысла, поэтому используются следующие принципы:

1) Только одна точка в строке
2) Точка ищется раз в 20 строчек (то есть если изображение размерностью 320×240, то для одной линии найдется примерно 12 точек)

Для поиска точек, необходимо с помощью двух циклов пройти по всему изображению с шагом 20 (важным является то, что для белой линии поиск осуществляется справа налево и снизу вверх, а для желтой слева направо и снизу вверх). Для этого запускается 2 цикла и если значение пиксела больше нуля (то есть если пиксель белый), то он добавляется в массив точек. Также есть защита от разрыва, если на протяжении более 80 пикселей точек не обнаружено, значит линия разорвалась и необходимо прекратить поиск.
После поиска необходимо отрисовать полученные точки на изображении, для отладки. Для этого проверяем нашлась ли хоть одна точка, после чего соединяем все точки красными линиями. Затем функция возвращает полученные точки и изображение.

def calculate_error(yellow_array, white_array):
    
    error_yell = 0
    error_white = 0
    weight = 0
    i = 1
    for yel in yellow_array:
   	 #when yel[2] = 600 then weight = 0 and if yel[2] = 0 wheight = 1
   	 weight = yel[1]*0.0017 + 1
   	 error_yell = weight*(30 - yel[0]) + error_yell
   	 i+=1
    error_yell = error_yell/i
    for white in white_array:
   	 weight = white[1]*0.0017 + 1
   	 error_white = weight*(300 - white[0]) + error_white
   	 i+=1
    error_white = error_white/i
    print("white "+ str(error_white) + " yellow "+ str(error_yell))
    if error_white < 30:
   	 return error_yell
    elif error_yell < 30:
   	 return error_white
    else:
   	 return (error_white + error_yell)/2

Выше представлена функция для расчета отклонения от идеального положения. Используются следующие базовые принципы:

1) Отклонение для желтой и белой линии считаются по отдельности
2) Чем дальше находится точка, тем с меньшим весом учитывается ее отклонение. То есть если точка сильно отклоняется от идеального положения, но при этом находится далеко, то его вложение в суммарное отклонение будет небольшим, и примерно таким же как небольшое отклонение точки которая находится близко (близкими считаются точки лежащие внизу изображения, а далекими лежащие наверху изображения)

Таким образом сначала вычисляется суммарное отклонение от желтой линии (идеальное положение точки по оси y — 30), а затем от белой линии (идеальное положение точки по оси y — 300). После этого если ошибка для белой линии мала, то возвращается ошибка для желтой линии и наоборот. Если же обе ошибки велики, то возвращается среднее арифметическое между ними.

if __name__ == '__main__':
    rospy.init_node('image_projection')
    sub_image = rospy.Subscriber('/camera_line/image_line', Image, cbImageProjection, queue_size=1)
    while not rospy.is_shutdown():
   	 try:
   		 rospy.sleep(0.1)
   	 except KeyboardInterrupt:
   		 break
   		 cv2.destroyAllWindows()

Выше функция main, которая вызывается при старте программы. В ней:

1) Инициализируется нода
2) Происходит подписка на топик, содержащий в себе изображение с камеры и указание, что в качестве колбэк функции использовать функцию cbImageProjection

Детектирование и распознавание дорожных знаков

Для детектирования дорожных знаков используется алгоритм SIFT (Scale-invariant feature transform). SIFT — очень мощный инструмент для распознавания необходимых объектов на изображении. Его суть заключается в том, что на изображении ищутся “особые точки”, после чего найденные точки, можно сравнить с такими же точками на идеальном изображении и сделать вывод о наличии/отсутствии необходимого объекта на изображении.
Ниже представлено изображение и найденные на нем особые точки:

21.png

Для сопоставления особых точек используются дескрипторы. Дескриптор — идентификатор ключевой точки, выделяющий её из остальной массы особых точек. В свою очередь, дескрипторы должны обеспечивать инвариантность нахождения соответствия между особыми точками относительно преобразований изображений.

В итоге получается следующая схема решения задачи детектирования дорожных знаков:

1. Изображение переводится в черно-белый формат, для уменьшения количества вычислений.
2. На идеальном изображении выделяются особые точки, а дескрипторы этих точек сохраняются в массив
3. Выделяются особые точки на текущем изображении с камеры
4. По совпадению дескрипторов выделяются соответствующие друг другу ключевые точки.
5. На основе набора совпадающих ключевых точек строится вывод о наличии в данный момент времени дорожного знака на изображении.

Идеальные изображения находятся в папке data_set. Они используются для вычисления идеальных контрольных точек, после чего их можно сравнить с точками на текущем изображении.

39.png

Таким образом нода постоянно публикует в топик “/sign”, имя текущего наблюдаемого дорожного знака, если он есть на изображении и строку “none” если не видит ничего.
Имена знаков:

  • “stop”
  • “parking”
  • “tunnel”

Перейдем к программе (файл sign_detect.py):

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import rospy
import numpy as np
import cv2
from sensor_msgs.msg import Image, CompressedImage
from cv_bridge import CvBridge
import math
import os
from time import sleep
from std_msgs.msg import String

Здесь подключаются основные библиотеки и используемые сообщения. Для работы с изображением все также используется OpenCV.

pub_image = rospy.Publisher('sign_image', Image, queue_size=1)
pub_sign = rospy.Publisher('sign', String, queue_size=1)
cvBridge = CvBridge()
counter = 1
FLANN_INDEX_KDTREE = 0
index_params = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5)
search_params = dict(checks = 50)
flann = cv2.FlannBasedMatcher(index_params, search_params)

Здесь создаются глобальные переменные, необходимые паблишеры и объявляются необходимые классы. Класс для перевода изображения из сообщения типа Image в numpy массив. Также создается элемент класса FlannBasedMatcher, данный класс используется для автоматического сравнивания дескрипторов ключевых точек. Соответственно переменные index_params и search_params необходимы для инициализации элемента класса.

def cbImageProjection(data):
	global kp_ideal, des_ideal, sift, counter, flann
# drop the frame to 1/5 (6fps) because of the processing speed
	if counter % 3 != 0:
		counter += 1
		return
     else:
           counter = 1
	cv_image_original = cvBridge.imgmsg_to_cv2(data, "bgr8")
	cv_image_gray = cv2.cvtColor(cv_image_original, cv2.COLOR_BGR2GRAY)
	kp,des = sift.detectAndCompute(cv_image_gray, None)
	cv_image_original = cv2.drawKeypoints(cv_image_gray,kp,None,(255,0,0),4)
	for i in range(0,3):
		matches = flann.knnMatch(des,des_ideal[i],k=2)
		result = compare_matches(kp, kp_ideal[i], matches)
		sign_msg = String()
		if result == True:
			if i == 0:
				sign_msg.data = "stop"
			elif i == 1:
				sign_msg.data = "parking"
			else:
				sign_msg.data = "tunnel"
			break
		else:
			sign_msg.data = "none"
	print sign_msg.data
	pub_sign.publish(sign_msg)
	pub_image.publish(cvBridge.cv2_to_imgmsg(cv_image_original, "rgb8"))

Выше — основная колбэк функция для изображения. Так как процесс детектирования ключевых точек изображения требует много вычислительных мощностей, а изображение публикуется 30 раз в секунду некоторые кадры изображения необходимо отбрасывать. Для этого будет рассматриваться только каждые третий приходящий кадр.
После отбрасывания лишних кадров, сначала полученное изображение переводится в numpy формат, с которым может работать библиотека OpenCV. После этого изображение переводится в черно белый формат.
Затем функция detectAndCompute класса sift (объявление элемента класса происхоидит в функции ниже) находит ключевые точки и их дескрипторы. После этого найденные ключевые точки визуализируются на изображении для отладки. Теперь в цикле необходимо сопоставить найденные ключевые точки с идеальными ключевыми точками, после чего если найдено много совпадений, то в топик “sign” отправляется имя знака, если же ничего не найдено, то отправляется строка “none”. Цикл необходим для того, чтобы по очереди сопоставить найденные ключевые точки с ключевыми точками идеальных знаков.
Также в конце исполнительной функции в топик “sign_image” публикуется изображение с отмеченным на нем ключевыми точками.

def compare_matches(kp,kp_ideal,matches):
	MATCHES_ERR = 50000
	MATCHES_DIST_MIN = 7
	# print("matches count: ",len(matches))
	good = []
	for m,n in matches:
		if m.distance < 0.7*n.distance:
			good.append(m)
	if len(good) > MATCHES_DIST_MIN:
		src_pts = np.float32([kp[m.queryIdx].pt for m in good])
		dst_pts = np.float32([kp_ideal[m.trainIdx].pt for m in good])
		# print("distance matches count: ",len(good))
		mse_err = find_mse(src_pts,dst_pts)
		print("mse_err: ", mse_err)
		if mse_err < MATCHES_ERR:
			return True
	return False

Выше функция, которая сравнивает дескрипторы ключевых точек. Есть два основных параметра для сравнивания. Первый — это расстояние между соседними ключевыми точками (ведь ключевые точки могут совпадать и отдельным фактором для идентификации является именно расстояние между соседними ключевыми точками). Второй — суммарная квадратичная ошибка в расстояниях между точками MATCHES_ERR.
Функция принимает: kp — найденные на изображении ключевые точки, kp_ideal — ключевые точки с идеального изображения, matches — набор совпавших ключевых точек (совпадения рассчитываются knnMaycher-ом в функции cbImageProjection).
Сначала необходимо найти какие точки подходят нам по параметру расстояния, для этого сравниваем расстояния для каждой ключевой точки изображения и соответствующей совпавшей точки на идеальном изображении. Удовлетворяющие точки заносятся в список good.
Затем если таких совпавших точек оказалось больше 7, то идет расчет среднеквадратичной ошибки между дескрипторами совпадающих ключевых точек. Если эта ошибка оказывается меньше порогового значения, значит мы можем сделать вывод о том что в данный момент знак есть на изображении.

def find_mse(arr1, arr2):
	err = (arr1-arr2)**2
	sum_err = err.sum()
	size = arr1.shape[0]
	sum_err = sum_err/size
	return sum_err

Здесь представлена функция рассчитывания среднеквадратичной ошибки между двумя списками. Сначала вычисляется квадратичная ошибка между двумя соответствующими элементами массивов, затем эти ошибки суммируются и сумма делится на количество элементов массива

def standart_signs():
	dir_path = os.path.dirname(os.path.realpath(__file__))
	dir_path = dir_path.replace('myRace/src', 'myRace/')
	dir_path += 'data_set/detect_sign/'
	img1 = cv2.imread(dir_path + 'stop.png',0)         # trainImage1
	img2 = cv2.imread(dir_path + 'parking.png',0)      # trainImage2
	img3 = cv2.imread(dir_path + 'tunnel.png',0)    
	sift = cv2.xfeatures2d.SIFT_create()
	kp1,des1 = sift.detectAndCompute(img1, None)
	kp2, des2 = sift.detectAndCompute(img2,None)
	kp3, des3 = sift.detectAndCompute(img3,None)
	# img1 = cv2.drawKeypoints(img1,kp1,None,(255,0,0),4)
	kp_ideal = [kp1,kp2,kp3]
	des_ideal = [des1,des2,des3]
	return kp_ideal, des_ideal, sift#, img1

Выше представлена функция вычисления ключевых точек и их дескрипторов из идеальных изображений. Сначала из папки data_set подгружаются необходимые изображения, после чего создается элемент класса sift, который впоследствии будет использоваться в функции cbImageProjection. Затем для идеальных изображений вычисляются ключевые точки и их дескрипторы и записываются в списки kp_ideal — ключевые точки (0 элемент — знак стоп, 1 элемент — знак парковки, 2 элемент — знак въезда в туннель), des_ideal — дескрипторы идеальных точек.

if __name__ == '__main__':
	rospy.init_node('image_projection')
	sub_image = rospy.Subscriber('/camera/image', Image, cbImageProjection, queue_size=1)
	kp_ideal, des_ideal, sift = standart_signs()
	while not rospy.is_shutdown():
		try:
			rospy.sleep(0.1)
			# cv2.imshow('image',img1)
			# if cv2.waitKey(0) == 27:
				# cv2.destroyAllWindows()
				# break
		except KeyboardInterrupt:
			break

Main функция, вызываемая при запуске ноды. Здесь происходит инициализация ноды, создаются необходимые объекты подписчики и указываются колбэк функции, а также запускается цикл работы ROS-а.

Детектирование огней светофора

Алгоритм детектирования сигнала светофора очень похож на алгоритм детектирования линии. Также проводится бинаризация по различным порогам (для желтого, зеленого и красного цветов). Но так как таких цветов на поле достаточно много (например, вездесущая желтая линия), то необходимо также проверять форму задетектированнного бинаризованного объекта. Для детектирования окружности используется Hough circle detector, использующий преобразования Хафа. В простейшем случае преобразование Хафа является линейным преобразованием для обнаружения прямых. Прямая может быть задана уравнением y = mx + b и может быть вычислена по любой паре точек (x, y) на изображении. Главная идея преобразования Хафа — учесть характеристики прямой не как уравнение, построенное по паре точек изображения, а в терминах её параметров, то есть m — углового коэффициента и b — точки пересечения с осью ординат. Исходя из этого прямая, заданная уравнением y = mx + b, может быть представлена в виде точки с координатами (b, m) в пространстве параметров.
Принцип детектирования окружности с помощью детектирования линий очень прост: если прямые перпендикулярные какой либо поверхности пересекаются в одной точке, то скорее всего эта поверхность является окружностью.

40.png

Естественно это очень упрощенное представления, ведь вряд ли на реальном изображении линии сойдутся в одной точке, поэтому рассматривается некоторая окрестность возможных пересечений и проверяется насколько в этой окрестности линии близки друг к другу.
Рассмотрим подробнее функцию HoughCircles(image, method, dp, minDist, param1, param2, minRadius, maxRadius)

● image: бесцветное черно-белое изображение, на котором необходимо найти окружности.
● method: Определяет метод с помощью которого будет происходить детектирование, в нашем случае используется cv2.HOUGH_GRADIENT
● dp: Параметр характеризующий насколько идеальной должна быть найденная окружность.
● minDist: минимальное расстояние между центрами задетектированных окружностей (в пикселях).
● param1, param2: параметры с помощью которых регулируется точность детектирования.
● minRadius: минимальный радиус детектируемой окружности.
● maxRadius: максимальный радиус детектируемой окружности.

Задетектированный сигнал светофора выглядит следующим образом:

20.png

Перейдем к программной части (файл traffic_light_controller.py):

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import rospy
import numpy as np
import cv2
from sensor_msgs.msg import Image, CompressedImage
from cv_bridge import CvBridge
from time import sleep
from std_msgs.msg import String
pub_image = rospy.Publisher('image_traffic_light', Image, queue_size=1)
pub_traffic_light = rospy.Publisher('traffic_light', String, queue_size=1)
cvBridge = CvBridge()
counter = 1

Здесь подключаются необходимые библиотеки. Также создается два паблишера в топики: image_traffic_light и топик traffic_light. В данные топики публикуется обработанное изображение для отладки и цвет сигнала светофора, который видится в данный момент (публикуется строка, если никакой их сигналов не виден публикуется none). По аналогии с обработкой дорожных знаков данная нода также затрачивает много вычислительных мощностей, поэтому частоту обработки кадров необходимо ограничить. Будет обрабатываться каждый третий кадр, для этого создается переменная counter.

def cbImageProjection(data):
	global counter
	if counter % 3 != 0:
		counter += 1
		return
     else: 
           couner = 1	
	cv_image_original = cvBridge.imgmsg_to_cv2(data, "bgr8")
	cv_image_original = cv2.GaussianBlur(cv_image_original, (3, 3), 0)
	cv_image_gray = cv2.cvtColor(cv_image_original, cv2.COLOR_BGR2GRAY)
	circles = cv2.HoughCircles(cv_image_gray, cv2.HOUGH_GRADIENT, 1, 50, param2 = 20, minRadius = 8, maxRadius = 15 ) 
	green_x, green_y  = mask_green(cv_image_original)
	yellow_x, yellow_y = mask_yellow(cv_image_original)
	red_x, red_y  = mask_red(cv_image_original)
	light_msg = String()
	light_msg.data = "None"
	if circles is not None:
		circles = np.round(circles[0,:]).astype("int")
		for x,y,r in circles:
			cv2.circle(cv_image_gray, (x,y),r,(0,255,0), 3)
			green_err = calc_err(green_x, green_y, x, y) 
			red_err = calc_err(red_x, red_y, x, y)
			yellow_err =  calc_err(yellow_x, yellow_y, x, y)
			if green_err < 400:
				light_msg.data = "green" 
			if red_err < 400:
				light_msg.data = "red"
			if yellow_err < 400:
				light_msg.data = "yellow"
	pub_traffic_light.publish(light_msg)
	temp = np.hsplit(cv_image_gray,2) 
	cv_image_gray = temp[1]
	pub_image.publish(cvBridge.cv2_to_imgmsg(cv_image_gray, "8UC1")) 

Выше описана основная колбэк функция топика изображения с камеры. Рассмотрим ее работу поэтапно:

1) Проверяем какой по счету кадр пришел в обработку, если третий то обрабатываем, иначе завершаем работу функции
2) Размываем изображение с помощью GaussianBlur
3) Переводим изображение в серый формат
4) С помощью метода HoughCircles описанного ранее находим все окружности на изображении и добавляем их в переменную circles. В ней они хранятся в виде списка, где у каждой окружности есть ряд характеристик: координаты центра и радиус.
5) На изображение по очереди накладываются зеленая, желтая и красная маска. Затем на данных изображениях ищутся белые пиксели и координаты этих пикселей возвращаются
6) Проводится сравнение координат найденных окружностей и координат найденных белых пикселей, если есть совпадения то делается вывод о том что наблюдается конкретный сигнал светофора
7) Распознанные окружности переносятся на изображение для отладки
8) Объекты паблишеры публикуют рассчитанные данные

Перейдем к детальному рассмотрению каждой функции в отдельности.

def calc_err(color_x, color_y, circle_x, circle_y):
	if color_x*color_y*circle_y*circle_x > 0:
		x_err = (color_x - circle_x)**2
		y_err = (color_y - circle_y)**2
		err = x_err+y_err / 2
		return err
	return 100000

Выше указана функция для расчета ошибки между координатами белого пиксела и координатами найденной окружности. Сначала проводится проверка не равна ли нулю какая-либо координата, если она ноль то рассчитывать ошибку не имеет смысла. Сама ошибка рассчитывается как среднеквадратичное отклонение.

def mask_yellow(img):
	hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
	Hue_l = 20
	Hue_h = 35
	Saturation_l = 100
	Saturation_h = 255
	Lightness_l = 50
	Lightness_h = 255
	# define range of yellow color in HSV
	lower_yellow = np.array([Hue_l, Saturation_l, Lightness_l])
	upper_yellow = np.array([Hue_h, Saturation_h, Lightness_h])
	# Threshold the HSV image to get only yellow colors
	mask = cv2.inRange(hsv, lower_yellow, upper_yellow)
	temp = np.hsplit(mask,2)
	mask = temp[1]
	fraction_num = np.count_nonzero(mask)
	if fraction_num > 100:
		for y in range(0, mask.shape[0]-1,5):
			for x in range(0,mask.shape[1]-1,5):
				if mask[y,x]>0:
					return(x+160,y) 
	return 0, 0
def mask_red(img):
	hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
	Hue_l = 0
	Hue_h = 10
	Saturation_l = 30
	Saturation_h = 255
	Lightness_l = 48
	Lightness_h = 255
	# define range of yellow color in HSV
	lower_red = np.array([Hue_l, Saturation_l, Lightness_l])
	upper_red = np.array([Hue_h, Saturation_h, Lightness_h])
	# Threshold the HSV image to get only yellow colors
	mask = cv2.inRange(hsv, lower_red, upper_red)
	temp = np.hsplit(mask,2) 
	mask = temp[1]
	fraction_num = np.count_nonzero(mask)
	if fraction_num > 100:
		for y in range(0, mask.shape[0]-1,5):
			for x in range(0,mask.shape[1]-1,5):
				if mask[y,x]>0:
					return(x+160,y) 
	return 0, 0

def mask_green(img):
	hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
	Hue_l = 46
	Hue_h = 76
	Saturation_l = 86
	Saturation_h = 255
	Lightness_l = 50
	Lightness_h = 255
	# define range of yellow color in HSV
	lower_green = np.array([Hue_l, Saturation_l, Lightness_l])
	upper_green = np.array([Hue_h, Saturation_h, Lightness_h])
	# Threshold the HSV image to get only yellow colors
	mask = cv2.inRange(hsv, lower_green, upper_green)
	temp = np.hsplit(mask,2) 	mask = temp[1]
	fraction_num = np.count_nonzero(mask)
	if fraction_num > 100:
		for y in range(0, mask.shape[0]-1,5):
			for x in range(0,mask.shape[1]-1,5):
				if mask[y,x]>0:
					return(x+160,y) 
	return 0, 0

Выше представлены функции бинаризации и поиска белых пикселей: для желтого, красного и зеленого сигналов светофора соответственно. Рассмотрим подробнее одну из них, остальные являются похожими за исключением порогов бинаризации. Сама бинаризация проводится по аналогии с бинаризацией в случае детектирования линии. После бинаризации изображение разбивается на две одинаковые части, в дальнейшем будет рассматриваться только правая часть изображения, так как светофор расположен справа. Затем проводится проверка на количество белых пикселей, если их больше 100, то проводится поиск по всему изображению с шагом 5. Если белый пиксель найден, то его координаты (уже в изображении нормального размера) возвращаются функцией.

if __name__ == '__main__':
	rospy.init_node('traffic_light_detector')
	sub_image = rospy.Subscriber('/camera/image', Image, cbImageProjection, queue_size=1)
	while not rospy.is_shutdown():
		try:
			rospy.sleep(0.1)
		except KeyboardInterrupt:
			break

Здесь происходит инициализация ноды и объявление объекта подписчика.

Детектирование шлагбаума

Детектирование шлагбаума производится алгоритмом бинаризации по порогу красного цвета. После чего проверяется общее количество белых пикселей, если оно больше порогового значения, значит в данный момент шлагбаум опущен перед нами. В данном случае также анализируется только каждый третий кадр. Процесс детектирования можно наблюдать ниже:

42.png

Перейдем к коду программы (файл bar_detect.py):

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import rospy
import numpy as np
import cv2
from sensor_msgs.msg import Image, CompressedImage
from cv_bridge import CvBridge
from time import sleep
from std_msgs.msg import Bool
pub_image = rospy.Publisher('image_bar', Image, queue_size=1)
pub_bar = rospy.Publisher('bar', Bool, queue_size=1)
cvBridge = CvBridge()
counter = 1

Выше происходит подключение необходимых библиотек и объявление паблишеров. В данном случае один из них в топик bar публикует True если шлагбаум в данный момент виден и False если шлагбаума перед роботом нет. В топик image_bar публикуется бинаризованное изображение для отладки.

def cbImageProjection(data):
	global counter
	if counter % 3 != 0:
		counter += 1
		return
      else:
           counter = 1
	cv_image_original = cvBridge.imgmsg_to_cv2(data, "bgr8")
	cv_image_original = cv2.GaussianBlur(cv_image_original, (3, 3), 0)
	bar_msg = Bool()
	bar_msg.data, res = mask_red(cv_image_original)
	pub_bar.publish(bar_msg)
	pub_image.publish(cvBridge.cv2_to_imgmsg(res, "8UC1"))

Выше основная колбэк функция. Первой идет проверка на кадр, если данный кадр третий, то начинается его обработка. Сначала изображение переводится из сообщения в тип numpy array. Затем изображение размывается с помощью GaussianBlur, после чего проводится бинаризация и делается вывод о том есть ли шлагбаум на изображении. После чего публикуется бинаризованное изображение и наличие шлагбаума.

def mask_red(img):
	hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
	Hue_l = 0
	Hue_h = 10
	Saturation_l = 30
	Saturation_h = 255
	Lightness_l = 48
	Lightness_h = 255
	# define range of yellow color in HSV
	lower_red = np.array([Hue_l, Saturation_l, Lightness_l])
	upper_red = np.array([Hue_h, Saturation_h, Lightness_h])
	# Threshold the HSV image to get only yellow colors
	mask = cv2.inRange(hsv, lower_red, upper_red)
	mask = cv2.erode(mask, (4,4), iterations = 6) #for detection of big rectangle
	fraction_num = np.count_nonzero(mask)
	if fraction_num > 3000:
		return True, mask
	else:
		return False, mask

Выше описана функция бинаризации. Бинаризация проводится по аналогии с прошлыми случаями. После бинаризации убираются шумы, с помощью функции erode. Данная функция превращает одиночные белые пиксели в черные. Если количество белых пикселей больше 3000, то шлагбаум есть на изображении.

if __name__ == '__main__':
	rospy.init_node('bar_detector')
	sub_image = rospy.Subscriber('/camera/image', Image, cbImageProjection, queue_size=1)
	while not rospy.is_shutdown():
		try:
			rospy.sleep(0.1)
		except KeyboardInterrupt:
			break
			cv2.destroyAllWindows()

Здесь инициализируется ноды и подписчик на топик камеры.

Выполнение миссий

Как говорилось ранее выделяется 5 основных миссии:

● Остановка на светофоре
● Парковка
● Остановка перед шлагбаумом
● Лабиринт
● Езда по линии

Архитектура программ, с помощью которых выполняются миссии практически одинаковы, блок схема представлена ниже (не относится к езде по линии):

43.png

Нода каждой миссии подписывается на необходимый ей топик результата ноды детектирования. После того как “инициатор” ноды задетектирован, отключается движение по линии и начинается исполнение миссии. Затем робот снова начинает двигаться по линии, а нода миссии отключается.

Езда по линии

Управление моделью робота осуществляется при помощи задания необходимой линейной и угловой скорости движения. В топик cmd_vel, публикуется сообщение типа geometry_msgs/Twist, которое состоит из двух векторов:
linear: — линейная скорость (м/c)

x — по оси Х
y — по оси У
z — по оси Z

angular: — угловая скорость (скорость вращения вокруг своей оси, рад/c)

x — вокруг оси х
y — вокруг оси У
z -вокруг оси Z

Оси координат модели робота, расположены следующим образом:

44.png

Здесь синяя ось — ось Z, красная — ось X, зеленая — ось Y.
Из-за того что робот имеет дифференциальную колесную базу, он может перемещаться только вдоль оси Х и вокруг оси Z. Соответственно, для того, чтобы робот поехал вперед, необходимо отправить следующее сообщение:
msg:

linear:

x = 0.1
y = 0
z = 0
angular:

x = 0
y = 0
z = 0

Для того, чтобы робот крутился на месте:
msg:

linear:

x = 0
y = 0
z = 0
angular:

x = 0
y = 0
z = 0.1

А также для того чтобы крутился по дуге:
msg:

linear:

x = 0.1
y = 0
z = 0
angular:

x = 0
y = 0
z = 0.1

Перейдем к алгоритму управления. Отклонение от идеального положения уже известно (алгоритм расчета рассмотрен ранее), осталось реализовать регулятор. В качестве регулятора используется ПИ регулятор, который управляет как угловой, так и линейной скоростью робота. Например на поворотах линейную скорость необходимо уменьшать, а на прямых участках наоборот увеличивать. Структуру программы можно представить следующим образом:

45.png

Так как при работе данной ноды, ничто больше не может управлять скоростью робота (канал управления скоростью занят), необходимо иметь флаг, с помощью которого ноду можно будет включать и выключать. При запуске нода является включенной, для отключения необходимо в топик line_move_flag отправить сообщение типа std_msgs Bool, содержащее False. Для повторного запуска в этот же топик нужно отправить True.
Перейдем к коду программы (файл line_control.py):

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import rospy
from time import sleep
from std_msgs.msg import Float64, Bool
from geometry_msgs.msg import Twist
pub_vel = rospy.Publisher('cmd_vel', Twist, queue_size=1)
integral = 0
move_flag = True

Итак, здесь подключаются необходимые библиотеки и сообщения. Создается объект паблишер, для топика cmd_vel. Который будет отправлять скорость на робота. Создается переменная integral хранящая в себе интегральную составляющую регулятора. Данную переменную необходимо сделать глобальной, для того, чтобы она не обнулялась после каждого исполнения функции. Также создается переменная move_flag, которая в свою очередь и является переключателем между рабочим и выключенными состояниями. Изначально ее значение True, поэтому робот сразу начнет движение (если запущена нода детектирования линии).

def cbError(error):
	global integral, move_flag
	if(move_flag == True):
		velocity = Twist()
		integral = integral + 0.000005*error.data
		proportional = 0.01*error.data
		up = proportional+integral
		velocity.angular.z = up
		velocity.linear.x = 0.22 - 0.09*abs(up)
		pub_vel.publish(velocity)

Данная функция — колбэк отклонения от идеального положения на линии. Если движение разрешено, то создается сообщение типа Twist. После чего рассчитываются интегральная и пропорциональная составляющие регулятора. Затем рассчитывается управляющее воздействие и в свою очередь подается на линейную и угловую составляющие скорости, после чего необходимая скорость публикуется в топик cmd_vel.

def cb_flag(data):
	global move_flag
	move_flag = data.data

Данная функция — колбэк включения/выключения ноды. Здесь значение пришедшего сообщения переписывается в переменную move_flag

if __name__ == '__main__':
	rospy.init_node('line_control')
	sub_image = rospy.Subscriber('line_error', Float64, cbError, queue_size=1)
	sub_move_flag = rospy.Subscriber('line_move_flag', Bool, cb_flag, queue_size=1)
	while not rospy.is_shutdown():
		try:
			rospy.sleep(0.1)
		except KeyboardInterrupt:
			break

Main функция, вызываемая при запуске ноды. Здесь происходит инициализация ноды, создаются необходимые объекты подписчики и указываются колбэк функции, а также запускается цикл работы ROS-а.

Остановка на светофоре

В рамках данной миссии робот должен остановиться на красный сигнал светофора, и продолжить свое движение только на зеленый сигнал
Начнем с управляющей ноды миссии светофора (файл traffic_light_controller.py):

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import rospy
from time import sleep
from std_msgs.msg import  String, Bool
from geometry_msgs.msg import Twist
light = False

Здесь подключаются необходимые библиотеки. Также создается глобальная переменная light. Если значение переменной False, то робот продолжает ехать по линии. Если же значение True, то активируется миссия светофора.

def cb_traffic_light(data):
    global light
    if(data.data == "yellow" or data.data == "red"):
   	 light = True
    elif(data.data == "green"):
   	 light = False

Выше колбэк топика traffic_light в который публикуется информация о том какой сигнал светофора виден в данный момент. Если виден желтый или красный сигнал то значение переменной light становится равным True и активируется миссия.

def pub_velocity(x, z, time):
    pub_vel = rospy.Publisher('cmd_vel', Twist, queue_size=1)
    vel = Twist()
    for i in range(0, int(time*10)):
   	 vel.linear.x = x
   	 vel.angular.z = z
   	 pub_vel.publish(vel)
   	 rospy.sleep(0.1)

Здесь описана функция публикации заданной скорости какое то количество времени, она принимает необходимую линейную, угловую скорости и время в течении которого необходимо поддерживать данную скорость. В ней объявляется объект паблишер в топик cmd_vel (топик скорости). Затем создается сообщение типа Twist. Для стабильности работы необходимая скорость будет отправляться в цикле. Количество итеарций цикла рассчитывается исходя из времени в течении которого нужно ехать, каждая итерация длится 0.1 секунду.

def do_traffic_light():
    global light
    pub_line_move = rospy.Publisher('line_move_flag', Bool, queue_size=1)
    flag_move_line = Bool()
    flag_move_line.data = False
    rospy.sleep(0.1)
    pub_line_move.publish(flag_move_line)
    print("published stop msg")
    while( light == True):
   	 pub_velocity(0, 0, 0.1)
    flag_move_line.data = True
    pub_line_move.publish(flag_move_line)

Выше функция, описывающая выполнение миссии светофора. Сначала в топик line_move_flag отправляется значение False — это останавливает процесс движения по линии и топик cmd_vel становится не занятым. Затем в топик cmd_vel посылаются нулевые значения и робот останавливается, начинается процесс ожидания смены сигнала светофора. Как только значение переменной light сменяется с True на False, в топик line_move_flag отправляется значение True и продолжается движение по линии.

if __name__ == '__main__':
    rospy.init_node('traffic_light_controller')
    sub_sign = rospy.Subscriber('traffic_light', String, cb_traffic_light, queue_size=1)
    while not rospy.is_shutdown():
   	 try:
   		 if(light == True):
   			 print("start traffic light mission")
   			 do_traffic_light()
   			 break
   	 except KeyboardInterrupt:
   		 break

Здесь происходит инициализация ноды, и подписка на топик traffic_light. В основном цикле, постоянно происходит проверка переменной light, если ее значение True (то есть замечены желтый или красный сигнал светфоора), то вызывается функция исполнения мисии. После ее завершения цикл завершается вызовом break и нода уничтожается.

Парковка

Миссия парковки активируется, когда впервые увиден знак парковки. Проблема в том что знак видится задолго до парковочного места, поэтому после того как знак задетектирован необходимо прождать 9 секунд, прежде чем робот войдет в парковочную зону и начнется выполнение миссии.
Алгоритм выполнения миссии строится по следующей блок схеме:

46.png

Проверка на то свободно ли место, осуществляется с помощью лидара — датчика расстояния. Если расстояния меньше критического значения, значит место свободно.
Первое детектирование знака происходит в этом месте:

48.png

На этом фото видно, что первое парковочное место было свободно и робот занял его:

26.png

Перейдем к коду программы (файл parking.py):

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import rospy
from time import sleep
from std_msgs.msg import Float64, Bool, String
from geometry_msgs.msg import Twist
from sensor_msgs.msg import 
distance = 0
parking = False

Здесь подключаются все необходимые библиотеки и создаются глобальные переменные. Переменная parking является переменной активирующей миссию, при значении True начнется исполнение миссии. Переменная distance хранит в себе расстояние, необходимое для проверки того, свободно ли место.

def cb_sign(data):
    global parking
    if(data.data == "parking"):
   	 parking = True

Выше колбэк функция топика “sign”, если задетектирован знак парковки, то значение переменной parking изменяется с False на True.

def cb_scan(data):
    global distance
    counter = 0
    for i in range(200,300):
   	 if(data.ranges[i] < 1):
   		 distance = distance + data.ranges[i]
   		 counter = counter + 1
    if(counter != 0):
   	 distance = distance/counter
    else:
   	 distance = 0

Данная функция является колбэком топика “scan”, в который приходят измерения лазерного дальномера. В данный топик приходит сообщения типа LaserScan, в данном сообщении хранится множество переменных, но нас интересует массив ranges в котором и хранятся расстояния. Расстояния до объектов расположенных справа от робота хранятся в позициях ranges[200] — ranges[300]. В цикле выполняется проход по этому диапазону списка и если текущий элемент меньше 1 (то есть расстояние замеренное данным лучем меньше 1 метра), данное расстояние учитывается. Таким образом если хоть одно расстояние из всего диапазона будет меньше 1, значение переменной distance будет ненулевое.

def pub_velocity(x, z, time):
    pub_vel = rospy.Publisher('cmd_vel', Twist, queue_size=1)
    vel = Twist()
    for i in range(0, int(time*10)):
   	 vel.linear.x = x
   	 vel.angular.z = z
   	 pub_vel.publish(vel)
   	 rospy.sleep(0.1)

Данная функция отправки сообщений в топик cmd_vel уже была рассмотрена выше.

def do_parking():
    pub_line_move = rospy.Publisher('line_move_flag', Bool, queue_size=1)
    flag_move_line = Bool()
    flag_move_line.data = False
    rospy.sleep(0.1)
    pub_line_move.publish(flag_move_line)    
    print("published stop msg")
    pub_velocity(0, 0, 0.5)
    print("published vel")
    pub_velocity(0, -0.4,4)
    pub_velocity(0, 0, 0.3)
    pub_velocity(0.13, 0,2)
    pub_velocity(0, 0, 0.3)
    pub_velocity(0, 0.4,4)
    pub_velocity(0, 0, 0.5)
    pub_velocity(0, -0.4,4)
    pub_velocity(0, 0, 0.3)
    pub_velocity(-0.13, 0,2)
    pub_velocity(0, 0, 0.3)
    pub_velocity(0, 0.4,4)
    pub_velocity(0,0,0.5)
    flag_move_line.data = True
    pub_line_move.publish(flag_move

Здесь описывается выполнение самой миссии, рассмотрим эту функцию поэтапно:

● отключение движения по линии
● остановка
● поворот направо на 90°
● остановка (остановка после каждого движения нужна для того, чтобы движения робота получались наиболее четкими)
● проезд вперед на 0.26 м
● остановка
● поворот налево на 90° (этим движением фиксируется элемент парковки, так как робот должен быть направлен не в сторону линии)
● остановка
● поворот направо на 90°
● остановка
● проезд назад на 0.26 м
● поворот налево на 90° (поворот по направлению линии)
● возврат к движению по линии
if __name__ == '__main__':
    rospy.init_node('parking')
    sub_sign = rospy.Subscriber('sign', String, cb_sign, queue_size=1)
    sub_scan = rospy.Subscriber('scan', LaserScan, cb_scan, queue_size=1)
    while not rospy.is_shutdown():
   	 try:
   		 if(parking == True):
   			 print("start parking mission")
   			 rospy.sleep(9)
   			 print(distance)
   			 if(distance > 1 or distance == 0):    			           	 do_parking()
   			 else:
   				 rospy.sleep(2)
   				 do_parking()
   			 break
   	 except KeyboardInterrupt:
   		 break

Здесь происходит инициализация ноды и подписка на топик sign и scan. В основном цикле постоянно производится проверка переменной parking, если ее значение True, то начинается выполнение миссии по алгоритму:

● 9-секундное ожидание, того что робот доехал до первого парковочного места
● если переменная distance больше 1 или равна нулю (то есть если препятствий не обнаружено), то вызывается функция do_parking()
● если же первое место было занято, то необходимо подождать 2 секунды (столько занимает доезд до второго места) и вызвать функцию do_parking

Остановка перед шлагбаумом

В данной миссии есть два варианта действий:

1. объехать шлагбаум по объездному пути
2. остановиться перед шлагбаумом и проехать прямо

Остановиться перед шлагбаумом гораздо быстрее, поэтому выбирается этот вариант действий. В данной миссии есть одна важная особенность. При подъезде к шлагбауму желтая линия пропадает и появляется белая линия слева, поэтому очень важно, чтобы белая линия справа оставалась видимой, иначе робота развернет. Поэтому необходимо останавливаться и продолжать движение плавно во избежании искривления траектории робота.

22.png

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import rospy
from time import sleep
from std_msgs.msg import Float64, Bool, String
from geometry_msgs.msg import Twist
stop_bar = False

Подключение необходимых библиотек, объявление глобальной переменной stop_bar. Выполнение миссии начнется когда stop_bar примет значение True.

def cb_bar(data):
	global stop_bar
	stop_bar = data.data

Колбэк функция топика bar. Здесь данные топика переписываются в переменную stop_bar. Таким образом как только положение шлагбаума изменится, изменится и значение переменной stop_bar.

def pub_velocity(x, z, time):
    pub_vel = rospy.Publisher('cmd_vel', Twist, queue_size=1)
    vel = Twist()
    for i in range(0, int(time*10)):
   	 vel.linear.x = x
   	 vel.angular.z = z
   	 pub_vel.publish(vel)
   	 rospy.sleep(0.1)

Функция отправки сообщений в топик cmd_vel, описывалась ранее.

def do_stop():
	pub_line_move = rospy.Publisher('line_move_flag', Bool, queue_size=1)
	flag_move_line = Bool()
	flag_move_line.data = False
	rospy.sleep(0.1)
	pub_line_move.publish(flag_move_line)
	# pub_velocity(0.2,0,1)
	for i in range(20,0, -2):
		pub_velocity(i/100,0,0.2)
	while stop_bar == True:
		pub_velocity(0,0,0.1)
	flag_move_line.data = True
	pub_line_move.publish(flag_move_line)

Основная функция исполнения миссии. Разберем ее поэтапно:

• отключение движение по линии
● плавная остановка (необходима для того, чтобы при остановке из за инерции робота не повело в сторону)
● пока значение переменной stop_bar — True, то есть шлагбаум опущен, стоим на месте
● продолжение движения по линии
if __name__ == '__main__':
	rospy.init_node('stop_bar')
	sub_bar = rospy.Subscriber('bar', Bool, cb_bar, queue_size=1)
	while not rospy.is_shutdown():
		try:
			if(stop_bar == True):
				print("stop detected")
				do_stop()
				break
		except KeyboardInterrupt:
			break

Здесь инициализируется нода, и происходит подписка на топик bar. В основном цикле производиться постоянная проверка переменной stop_bar. Если ее значение меняется на True, то начинается исполнение миссии. После того как миссия выполнена, исполнительный файл завершается.

Лабиринт

Исполнение этой миссии разделяется на два этапа:

1) Детектирование знака и доезд до лабиринта
2) Нахождение выхода из лабиринта

Первый этап выполняется довольно прост, после того как знак туннеля задетектирован робот продолжает движение вперед в течении 3 секунд.

51.png

Второй этап заключается в создании алгоритма для прохождения лабиринта. Лабиринт представляет из себя коробку с цилиндрическими препятствиями внутри:

52.png

Для его прохождения достаточно ехать вдоль правой стены, параллельно огибая встречающиеся препятствия. Блок схему алгоритма можно представить следующим образом:

53.png

Перейдем к программе (файл tunnel_start.py):

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import rospy
from time import sleep
from std_msgs.msg import Float64, Bool, String
from geometry_msgs.msg import Twist
from sensor_msgs.msg import LaserScan
ranges = list()
tunnel = False
in_tunnel = False

Здесь подключаются необходимые библиотеки и создаются глобальные переменные. Переменная ranges будет хранить показания лазерного дальномера в формате списка. Переменная tunnel активирует начало миссии.
Переменная in_tunnel активирует алгоритм прохождения лабиринта.

def cb_sign(data):
	global tunnel
	if(data.data == "tunnel"):
		tunnel = True

Выше описана колбэк функция топика sign. Если задетектирован знак въезда в туннель, то есть в топик пришло сообщение содержащее строку “tunnel”, то значение переменной tunnel изменяется с False на True.

def cb_scan(data):
	global ranges
	ranges = data.ranges

В колбэке топика scan, замеры лазерного дальномера переписываются в переменную ranges, которая будет использоваться в дальнейшем.

def pub_velocity(x, z, time):
    pub_vel = rospy.Publisher('cmd_vel', Twist, queue_size=1)
    vel = Twist()
    for i in range(0, int(time*10)):
   	 vel.linear.x = x
   	 vel.angular.z = z
   	 pub_vel.publish(vel)
   	 rospy.sleep(0.1)

Выше описана функция отправки сообщений в топик cmd_vel.

def do_tunnel():
	pub_line_move = rospy.Publisher('line_move_flag', Bool, queue_size=1)
	flag_move_line = Bool()
	flag_move_line.data = False
	rospy.sleep(3)
	rospy.sleep(0.1)
	pub_line_move.publish(flag_move_line)
	pub_velocity(0.2,0,2)
	for i in range(20,0, -2):
		pub_velocity(i/100,0,0.3)

Здесь описана функция активации миссии туннеля. Разберем ее поэтапно:

● происходит отключение движения по линии
● робот движется вперед 2 секунды, чтобы заехать в лабиринт
● плавная остановка
def sign(data):
	if(data >= 0):
		return 1
	else:
		return -1

Функция sign является функцией определения “знака” числа. То есть если в нее отправить число -29, то она вернет -1, если же в нее отправить положительное число, то она вернет 1.

def in_tunnel_go():
	global ranges
	vel_x = 0.12
	vel_z = 0
	error = 4*(0.23 - ranges[300])
	if(abs(error) > 1.5):
		error = sign(error)*1.5
	vel_z = error
	for i in range(0,360,1):
		if(ranges[i] < 0.15):
			if(i <= 30 or i >= 330):
				vel_x = -0.09
				vel_z = 0.4		
	pub_velocity(vel_x, vel_z, 0.1)

Здесь описывается функция прохождения лабиринта. Сначала создаются переменные хранящие в себе скорость. Линейная скорость при движении вдоль стены равна 0.12.

В качестве регулятора для движения вдоль стены используется пропорциональный регулятор. В качетсве источника данных используется 300-ый луч лазерного дальномера, его направление показано на рисунке ниже:

54.png

Луч, смотрящий точно направо не подходит для реализации регулятора, так как система имеет небольшое запаздывание. Поэтому выбирается луч, смотрящий направо и немного вперед.
Робот должен держаться на расстоянии 0.23 м от стены, а коэффициент усиления равен 4, поэтому ошибка рассчитывается по следующей формуле:
error = 4*(0.23 — ranges[300])
После расчета ошибки проверяется нет ли препятствий вокруг робота. Если они есть, то роботу необходимо отъехать назад, поворачивая направо, если же препятствий нет, то скорости не изменяются.
На последнем этапе рассчитанные скорости публикуются в топик cmd_vel.

if __name__ == '__main__':
	rospy.init_node('tunnel')
	sub_sign = rospy.Subscriber('sign', String, cb_sign, queue_size=1)
	sub_bar = rospy.Subscriber('scan', LaserScan, cb_scan, queue_size=1)
	while not rospy.is_shutdown():
		try:
			if(tunnel == True and in_tunnel == False):
				print("tunnel detected")
				do_tunnel()
				in_tunnel = True
			elif(in_tunnel == True):
				in_tunnel_go()
		except KeyboardInterrupt:
			break

Здесь происходит инициализация ноды, а также подписка на топики sign и scan. В основном цикле идет проверка переменных tunnel и in_tunnel. Если знак туннеля задетектирован (то есть значение переменной tunnel True, а переменной in_tunnel False), то выполняется подъезд к туннелю и значение переменной in_tunnel меняется на True. При следующих итерациях цикла вызывается функция in_tunnel_go(). Таким образом она продолжит вызываться, пока нода не будет выключена принудительно.

Создание и запуск launch файлов, исправление возможных ошибок

Launch файлы

Запускать все вышеописанные файлы по отдельности очень долго, поэтому для запуска нескольких нод одновременно используются launch файлы. Создано два лаунч файла:

● запуск нод детектирования (detection.launch)
● запуск нод выполнения миссий (control.launch)

Рассмотрим структуру launch файла, на примере detection.launch:

<launch>
    <node pkg="myRace" name="bar_detect" type="bar_detect.py"/>

    <node pkg="myRace" name="line_detect" type="line_detect.py"/>

    <node pkg="myRace" name="sign_detect" type="sign_detect.py"/>

    <node pkg="myRace" name="traffic_light_detect" type="traffic_light_detector.py"/>

</launch>

Первая строка, объявляет launch файл, а последняя его закрывает.
Вызов необходимого исполнительного файла производится по следующей структуре:

● pkg — имя пакета, в котором расположен файл
● name — то как будет называться нода при инициализации (то есть по какому имени к ней обращаться с помощью команды rosnode)
● type — имя исполнительного файла

Launch файлы хранятся в отдельной папке launch внутри пакета myRace и имеют расширение .launch. Для их запуска используется команда roslaunch, например:
$ roslaunch myRace detection.launch
Одной из распространенных ошибок при запуске лаунч файла, который в свою очередь запускает ноды написанные на языке python, является следующая:
ERROR: cannot launch node of type [turtlebot3_teleop/turtlebot3_teleop_key]: can’t locate node [turtlebot3_teleop_key] in package [turtlebot3_teleop]
Данная ошибка говорит о том, что команда roslaunch не смогла найти указанной в лаунч файле ноды, это может быть вызвано двумя причинами:

1. неправильно указано имя исполнительного файла ноды
2. окружение ROS не знает о существовании данного исполнительного файла с расширением .py (то есть написанного на языке python)

Первая ошибка исправляется, соответствующим изменением имени файла.
Чтобы исправить вторую ошибку необходимо дать окружению ROS знать, о существовании исполнительного файла, для этого необходимо перейти в директорию, в которой находится исполнительный файл:
$ cd ~/tb_ws/src/myRace/src
И затем выполнить команду chmod +x на данный исполнительный файл:
$ chmod +x “имя файла”
Например:
$chmod +x traffic_light_controller.py

Использований rqt_graph для отслеживания ошибок

Для отслеживания связи между нодами и топиками, может использоваться утилита rqt, а именно ее плагин rqt_graph. Для его вызова в терминале необходимо прописать следующую команду:
$ rqt_graph
Визуализация нормально работающих процессов выглядит следующим образом:

55.png

56.png

Здесь в овал обведены имена нод, а топики представлены либо прямоугольниками либо текстом расположенным на линии связи.

Таким образом визуализирую все связи нод и топиков в системе, можно найти архитектурную ошибку (например если нода не читает информацию из необходимого топика).

Описание последовательного запуска всех элементов

Итак, для того, чтобы запустить робота для выполнения всего задания целиком необходимо:

1) Запустить симулятор, с моделью робота и соревновательного поля

команда: $ roslaunch turtlebot3_gazebo turtlebot3_autorace.launch

2) Активировать миссии

команда: $ roslaunch turtlebot3_gazebo turtlebot3_autorace_mission.launch

3) Активировать все ноды детектирования

команда: $ roslaunch myRace detection.launch

4) Активировать все управляющие ноды, после чего робот начнет двигаться и выполнять миссии

команда: $ roslaunch myRace control.launch

Содержание

  • 1 Установка и настройка ПО
    • 1.1 Загрузка и установка
    • 1.2 Настройка
  • 2 Симулятор Gazebo
    • 2.1 Введение
    • 2.2 Соревновательное поле
    • 2.3 Запуск симулятора с моделями и базовые действия
    • 2.4 Запуск миссий
    • 2.5 Модель робота
    • 2.6 Визуализация изображений при помощи утилиты rqt
  • 3 Детектирование
    • 3.1 Детектирования дорожной разметки
    • 3.2 Детектирование и распознавание дорожных знаков
    • 3.3 Детектирование огней светофора
    • 3.4 Детектирование шлагбаума
  • 4 Выполнение миссий
    • 4.1 Езда по линии
    • 4.2 Остановка на светофоре
    • 4.3 Парковка
    • 4.4 Остановка перед шлагбаумом
    • 4.5 Лабиринт
  • 5 Создание и запуск launch файлов, исправление возможных ошибок
    • 5.1 Launch файлы
    • 5.2 Использований rqt_graph для отслеживания ошибок
    • 5.3 Описание последовательного запуска всех элементов

Установка и настройка ПО

Загрузка и установка

Для полноценной работы с фреймворком ROS необходим дистрибутив операционной системы с ядром linux, для этого можно использовать виртуальную машину. То есть по сути оболочку для запуска операционной системы внутри операционной системы, для этого понадобится программа VirualBox. Также необходимо скачать сам образ операционной системы с предустановленным ROS, который можно будет открыть с помощью программы VirtualBox. Для этого необходимо скачать архив по этой ссылке на гугл-диск
После скачивания его нужно разархивировать в удобную вам папку. После разархивирования можно увидеть следующую структуру папок и файлов Ubuntu 64-bit:

1.png

Файлы виртуальной машины
Данные файлы нельзя как либо модифицировать и изменять во избежании всевозможных поломок ПО. В дальнейшем с помощью программы virtualBox будет открываться файл Ubuntu 64-bit с расширением Virtual Machine Disk Format (.vmdk)

Настройка

Для первого запуска виртуальной машины необходимо:
1) Запустить программу virtualBox
2) Нажать кнопку создать, чтобы создать новую виртуальную машину

12.png

3) В высветившимся окне нажать кнопку экспертный режим

13.png

4) Настраиваем виртуальную машину:

4.1) Задать любое имя машины
4.2) Выбрать папку в которой будут храниться данные виртуальной машины, предложенную папку можно не изменять
4.3) Тип машины — Linux, версия Ubuntu (64-bit)
4.4) Выбрать количество оперативной памяти, которое будет доступно виртуальной машине. ДЛЯ КОРРЕКТНОЙ РАБОТЫ НЕОБХОДИМО НЕ МЕНЕЕ 6 ГБ

5.png

4.5) В графе жесткий диск выбрать пункт использовать существующий виртуальный диск, и в качестве виртуального диска выбрать файл Ubuntu 64-bit.vmdk описанный в предыдущем пункте

6.png

7.png

8.png

9.png

5) Когда все настройки выставлены в соответствии с пунктом 4 необходимо нажать кнопку создать.
6) Перед запуском виртуальной машины, необходимо настроить количество ядер процессора, которые она сможет использовать (для комфортной работы рекомендуется не менее 4). Для этого:

6.1) Необходимо нажать кнопку настроить

10.png

6.2) Перейти в графу система

11.png

6.3) Перейти во вкладку процессор и разрешить использование 4 или более ядер (количество разрешенных к использованию ядер зависит от общего количества ядер процессора)

1222.png

13333.png

7) После всех настроек можно запустить виртуальную машину, для этого необходимо:

7.1) Нажать кнопку запустить

14.png

7.2) После загрузки системы появится следующее окно:

15.png

7.3) В данном окне нажать на пользователя tb3, после чего высветится окно для ввода пароля:

16.png

7.4) Ввести пароль tb3, после чего откроется окно системы:

17.png

Note: если окно системы слишком маленькое для работы, то в настройках ubuntu необходимо изменить разрешение экрана:

18.png

Теперь внутри данного окна можно вести полноценную работу с операционной системой Ubuntu. Далее будет описан симулятор Gazebo и запуск данного симулятора внутри виртуальной машины.

Симулятор Gazebo

Введение

Gazebo 3Dразрабатываемый некоммерческой организацией OSRF (Open Source Robotics Foundation), имеет ряд преимуществ по сравнению с другими робототехническими симуляторами. Во-первых, он бесплатный и имеет открытый код. Во-вторых, он очень популярен среди мирового робототехнического сообщества и является официальным симулятором соревнований DARPA. В-третьих, Gazebo отлично интегрируется с программной платформой ROS (Robot Operating System), а значит разработанную программу управления виртуальным роботом в Gazebo и ROS будет относительно несложно перенести на реального робота.
В данном симуляторе будет запускаться робот turtlebot3, на поле соревнований turtlebot3 autorace:

19.png

Суть данных соревнований в том, что робот должен проехать по размеченной линии параллельно выполняя различные миссии:

  • остановка на светофоре, в самом начале движения (на рисунке видно как горит зеленый сигнал светофора)
  • парковка, в одной из двух специально отведенных зон (одна из зон будет занята другим роботом)
  • остановка перед шлагбаумом
  • прохождение лабиринта (в лабиринте линии нет, поэтому он проходится при помощи лазерного дальномера)

Данная задача является очень интересной и многогранной, помогает улучшить навыки в области компьютерного зрения, алгоритмики и т.д

Соревновательное поле

Рассмотрим соревновательное поле и миссии подробнее. Поле представляет из себя квадратную поверхность на которую нанесены желтая и белая линии, которые формируют дорожное полотно. По регламенту соревнований робот не должен выезжать за пределы дорожной полосы, иначе ему будут начислены штрафные баллы. Поэтому очень важно реализовать максимально точное следование линии.
Также разберем все миссии:
1) Остановка на светофоре
Первой миссией является остановка на светофоре. Изначально на светофоре горит зеленый сигнал. Когда робот подъезжает к нему, зеленый сигнал сменяется красным и робот должен остановиться. Затем робот должен ждать повторного появления зеленого сигнала и только после этого продолжать движение. Пример того, что видит робот при подъезде к светофору:

888.png

2) Парковка
Второй миссией, которую робот должен пройти является парковка. В рамках данной миссии, робот должен заехать в одно из двух отведенных мест парковки (другое место занимает второй робот). В качестве ориентира для начала миссии используется знак парковки:

889.png

3) Шлагбаум
Третьей миссией является остановка перед шлагбаумом. При подъезде робота к перекрестку шлагбаум опустится. У робота есть два варианта действий, либо остановиться перед шлагбаумом и ждать пока тот поднимется, либо объехать его по прилегающей дороге. Также рядом с шлагбаумом расположен знак stop:

22.png

4) Туннель
Последней миссией для робота является замкнутый лабиринт, имеющий один вход и один выход. Для ориентации в тоннеле робот не может использовать камеры, но может использовать остальные имеющиеся датчики. Главная задача — выехать из туннеля через зону выхода. Зона входа же в свою очередь помечена знаком:

23.png

Конечной целью робота является прохождение всей миссий за наименьшее время.

Запуск симулятора с моделями и базовые действия

Для запуска симулятора, с моделью робота и соревновательным полем необходимо открыть терминал (ctr+alt+t) и написать следующую команду:
$roslaunch turtlebot3_gazebo turtlebot3_autorace.launch
При первой загрузке симулятору потребуется время, чтобы настроить окружение, это может занять примерно 5-10 минут.
Окно запущенного симулятора выглядит так:

24.png

Для изменения ракурса используется правая кнопка мыши. Для приближения используется колесо мыши. С помощью левой кнопки мыши можно выделять необходимые объекты.
Для того чтобы передвинуть какой либо объект необходимо воспользоваться режимом переноса, чтобы его активировать в панели управления необходимо нажать на данную иконку:

25.png

После чего левой кнопкой мыши нажать на объект, который необходимо передвинуть (категорически не рекомендуется передвигать что то кроме робота!). На объекте высветится система координат:

26.png

Чтобы передвинуть объект вдоль какой-либо оси, необходимо потянуть за эту ось с нажатой левой кнопкой мыши.
Аналогичным способом можно вращать объекты, в режиме вращения. Для этого необходимо нажать на иконку вращения в панели управления:

27.png

Если в процессе работы, что то пошло не так и необходимо внести изменения в программный код, то всю симуляцию можно перезапустить. Все выполняемые внешние исполнительные файлы завершат свою работу, а все элементы поля в том числе робот вернутся на изначальные места, для этого необходимо нажать на вкладку edit->reset world.

Запуск миссий

Изначально при запуске мира, миссии не запускаются. То есть светофор не горит, оба места для парковки остаются не занятыми и шлагбаум не опускается. Для того, чтобы активировать миссии в терминале необходимо написать следующую команду: $roslaunch turtlebot3_gazebo turtlebot3_autorace_mission.launch.
Соответственно, до запуска данной команды соревновательное поле выглядело так:

28.png

После запуска команды поле выглядит вот так:

29.png

Видно, что зеленый сигнал светофора загорелся и одно из парковочных мест оказалось занято другим роботом.
Миссии активируются, исходя из показаний топика odom робота (топик, в котором хранятся данные колесной одометрии, подробнее будет рассмотрен далее). То есть каждая миссия будет активироваться только тогда, когда робот самостоятельно доехал до нее, если просто передвигать робота в какую-либо точку то миссии активироваться не будут.

Модель робота

В симуляторе запускается точная копия робота turtlebot3, включая все топики реального робота. Чаще всего использоваться будут следующие топики:
scan: топик в который публикуются данные лазерного дальномера, в формате сообщения sensor_msgs/LaserScan. Дальномер делает 360 замеров на один оборот, то есть его разрешающая способность один градус. Все расстояния хранятся в массиве ranges, который входит в сообщение LaserScan, всего там 360 элементов. Ниже на рисунке можно увидеть как отсчитываются углы:

87.png

odom: топик в который публикуется колесная одометрия робота, то есть его перемещение от точки старт рассчитанное, исходя из показаний датчиков оборотов, установленных в привода. Так как в симулятора Gazebo также симулируется трение, показания в данном топике со временем будут накапливать ошибку, и отличаться от реального перемещения робота. В данный топик публикуются сообщения типа: nav_msgs/Odometry.
cmd_vel: топик который управляет скоростью движения робота, подробнее его работа рассмотрена в пункте Езда по линии.
Кроме основных элементов, на модель робота также установлено две камеры. Графического отображения они не имеют, схематичное расположение камер показано на рисунке ниже:

30.png

Таким образом одна из камер направлена на линию (camera_line), а вторая на дорожные знаки. Изображение полученные с камер выглядят следующим образом (слева с камеры линии, справа с камеры направленной на знаки):

889.png

8899.png

Изображение с камеры линии поступает в топик: /camera_line/image_line. Изображение с камеры направленной на знаки поступает в топик: /camera/image.
В данные топики поступает сообщение типа: sensor_msgs/Image, которое не получится визуализировать с помощью библиотеки OpenCV, поэтому для визуализации используется утилита rqt.

Визуализация изображений при помощи утилиты rqt

Чтобы визуализировать изображение необходимо запустить данную утилиту написав следующую команду в терминале:
$ rqt
На экране должно появиться пустое окно, а на панели управления иконка утилиты:

Untitled.png

После в панели управления необходимо выбрать пункт: Plugins->Visualization->Image View
После этого откроется окно визуализации, в котором необходимо выбрать топик, информацию с которого нужно визуализировать (красным выделено окно выбора топика):

34.png

В результате изображение будет в онлайн режиме транслироваться на экран в следующем виде:

888.png

Для добавление визуализаций с других топиков необходимо повторить данный процесс и рядом откроются новые окна.

Детектирование

Детектирования дорожной разметки

Для детектирования линий дорожной разметки используется довольно таки простой алгоритм — бинаризация по цветовому порогу.
Изображение напрямую с виртуальной камеры, направленной на дорогу публикуется в закодированном виде в топик /camera_line/image_line. Поиск цвета на изображении происходит по цветовой модели HSV. Данная модель очень полезна, если вам необходимо оперировать всем спектром цветов при помощи одной переменной. Название модели состоит из первых букв от слов Hue – тон, Saturation – насыщенность и Value – значение (Brightness – яркость). Тон варьируется от 0 до 255 , насыщенность и яркость так же – от 0 до 255.

36.png

Теперь изображение с камеры бинаризовано по цветовому порогу, то есть получено черно-белое изображение на котором белые пиксели — это пиксели дорожной разметки (белой и желтой), а черные пиксели — все остальное. Далее полученные белые полосы переводятся в массивы точек. После этого рассчитывается отклонение (или ошибка) от идеального положения робота в данный момент времени и это отклонение отправляется в ноду, отвечающую за управление колесами. Также во время того как белые линии конвертируются в массивы точек, на исходном изображении рисуется красная линия, показывающая какие именно точки были задетектированы, это изображение публикуется в топик /image.

37.png

Рассмотрим код программы подробнее (файл line_detect.py):

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import rospy
import numpy as np
import cv2
from sensor_msgs.msg import Image, CompressedImage
from cv_bridge import CvBridge
from time import sleep
from std_msgs.msg import Float64

Здесь подключаем необходимые библиотеки: библиотеку rospy для подключения ros окружения, numpy для работы с массивами, opencv для обработки изображений, cv_bridge для конвертирования изображения из ros сообщения в numpy массив, также модуль задержки sleep из библиотеки time. Для полноценного общения с ros окружением необходимо также добавить используемые сообщения: изображение и сжатое изображения и float64 (дробное число).

pub_image = rospy.Publisher('image', Image, queue_size=1)
pub_error = rospy.Publisher('line_error', Float64, queue_size=1)
cvBridge = CvBridge()

Создаем необходимые глобальные переменные: объект publisher для публикации обработанного изображения (это необходимо для отладки), объект publisher для публикации отклонения от идеального положения и объект cvBridge для конвертирования сообщения типа image в numpy массив.

def cbImageProjection(data):
    cv_image_original = cvBridge.imgmsg_to_cv2(data, "bgr8")
    cv_image_original = cv2.GaussianBlur(cv_image_original, (5, 5), 0)
    yellow_detect, yellow_array = mask_yellow(cv_image_original)
    white_detect, white_array = mask_white(cv_image_original)
    detected = cv2.add(white_detect, yellow_detect)
    pub_image.publish(cvBridge.cv2_to_imgmsg(detected, "bgr8"))
    error = Float64()
    error.data = calculate_error(yellow_array, white_array)
    pub_error.publish(error)

Данная функция является колбэк функцией для изображения, приходящего с камеры, она выполняет все необходимые преобразования, рассчитывает отклонение от идеального положения и отсылает это отклонение в топик “line_error”. Разберем ее поэтапно:

1) Переводим изображение из сообщения Image в numpy array, чтобы с ним можно было работать функциями библиотеки OpenCV
2) “Блюрим” изображение, то есть немного размываем его, чтобы избавиться от различных шумов и артефактов. Выполняется это с помощ Гауссиан блюра, смысл работы которого в том, что значения какой то группы соседних пикселей усредняется, таким образом мы можем добиться плавного размытия.
3) Вызываем функции детектирования желтой и белой линий соответственно. Получаем при этом бинаризованные изображения, где линии — белые пиксели. Также получаем массив точек, принадлежащих линии.
4) Затем необходимо создать изображение, использующееся для отладки, для этого объединяем изображения с белой и желтой линии. После этого публикуем данное изображение в топик “image”.
5) Финальным этапом является расчет отклонения от идеального положения и публикация этого отклонения в топик “line_error”.
def mask_yellow(img):
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    Hue_l = 27
    Hue_h = 41
    Saturation_l = 130
    Saturation_h = 255
    Lightness_l = 160
    Lightness_h = 255
    # define range of yellow color in HSV
    lower_yellow = np.array([Hue_l, Saturation_l, Lightness_l])
    upper_yellow = np.array([Hue_h, Saturation_h, Lightness_h])
    # Threshold the HSV image to get only yellow colors
    mask = cv2.inRange(hsv, lower_yellow, upper_yellow)
    # Bitwise-AND mask and original image
    res = cv2.bitwise_and(img, img, mask = mask)
    fraction_num = np.count_nonzero(mask)
    point_arr = []
    stop_flag = False
    if fraction_num > 50:
   	 k = 0
   	 jold = 0
   	 for i in range(mask.shape[0]-1,0,-15):
   		 if stop_flag == True:
   			 break
   		 for j in range(0,mask.shape[1],15):
if mask[i,j] > 0:
   				 point_arr.append([j,i])
   				 k+=1
   				 if abs(j-jold) > 80 and k > 1:
   					 point_arr.pop()
   					 stop_flag = True
   				 jold = j
   				 break
   	 if(len(point_arr) > 0):
   		 point_before = point_arr[0]
   		 for point in point_arr:
   			 res = cv2.line(res, (point[0], point[1]), (point_before[0],point_before[1]), (0,0,255),8)
   			 point_before = point
    return res, point_arr
    

def mask_white(img):
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    Hue_l = 0
    Hue_h = 25
    Saturation_l = 0
    Saturation_h = 36
    Lightness_l = 180
    Lightness_h = 255
    # define range of yellow color in HSV
    lower_white = np.array([Hue_l, Saturation_l, Lightness_l])
    upper_white = np.array([Hue_h, Saturation_h, Lightness_h])
    # Threshold the HSV image to get only yellow colors
    mask = cv2.inRange(hsv, lower_white, upper_white)
    # Bitwise-AND mask and original image
    res = cv2.bitwise_and(img, img, mask = mask)
    fraction_num = np.count_nonzero(mask)
    point_arr = []
    stop_flag = False
    if fraction_num > 50:
   	 k = 0
   	 jold = 0
   	 for i in range(mask.shape[0]-1,0,-20):
   		 if stop_flag == True:
   			 break
   		 for j in range(mask.shape[1]-1,0,-20):
   			 if mask[i,j] > 0:
   				 point_arr.append([j,i])
   				 k+=1
   				 if abs(j-jold) > 80 and k > 1:
   					 point_arr.pop()
   					 stop_flag = True
   				 jold = j
   				 break
   	 if len(point_arr) > 0:
   		 point_before = point_arr[0]
   		 for point in point_arr:
   			 res = cv2.line(res, (point[0], point[1]), (point_before[0],point_before[1]), (0,0,255),8)
   			 point_before = point
    return res, point_arr

Выше описаны функции детектирования белой и желтой линии соответственно. Они практически идентичны, поэтому разберем только одну из них, а именно функцию для белой линии. Сначала необходимо перевести изображение в HSV формат, о том что это за формат говорилось ранее. После чего создаются массивы пороговых значений цветов для бинаризации, для белого цвета это:
Минимальные значения цвета по каналам H, S и V = 0,0,180
Максимальные значения цвета по каналам H, S и V = 25, 36, 255
Затем используя эти пороги бинаризуем изображение и получаем его “маску”.
Теперь используя эту маску можно найти все точки принадлежащие линии, но перед этим создается изображение для отладки. В данном изображении белым изображены выделенные пиксели, а остальные пиксели имеют свой обычный цвет в rgb формате. Проверив, что на изображении достаточно белых пикселей можно перейти к алгоритму нахождения точек принадлежащих линии на изображении.
Искать все точки, принадлежащие линии очень затратно и не имеет смысла, поэтому используются следующие принципы:

1) Только одна точка в строке
2) Точка ищется раз в 20 строчек (то есть если изображение размерностью 320×240, то для одной линии найдется примерно 12 точек)

Для поиска точек, необходимо с помощью двух циклов пройти по всему изображению с шагом 20 (важным является то, что для белой линии поиск осуществляется справа налево и снизу вверх, а для желтой слева направо и снизу вверх). Для этого запускается 2 цикла и если значение пиксела больше нуля (то есть если пиксель белый), то он добавляется в массив точек. Также есть защита от разрыва, если на протяжении более 80 пикселей точек не обнаружено, значит линия разорвалась и необходимо прекратить поиск.
После поиска необходимо отрисовать полученные точки на изображении, для отладки. Для этого проверяем нашлась ли хоть одна точка, после чего соединяем все точки красными линиями. Затем функция возвращает полученные точки и изображение.

def calculate_error(yellow_array, white_array):
    
    error_yell = 0
    error_white = 0
    weight = 0
    i = 1
    for yel in yellow_array:
   	 #when yel[2] = 600 then weight = 0 and if yel[2] = 0 wheight = 1
   	 weight = yel[1]*0.0017 + 1
   	 error_yell = weight*(30 - yel[0]) + error_yell
   	 i+=1
    error_yell = error_yell/i
    for white in white_array:
   	 weight = white[1]*0.0017 + 1
   	 error_white = weight*(300 - white[0]) + error_white
   	 i+=1
    error_white = error_white/i
    print("white "+ str(error_white) + " yellow "+ str(error_yell))
    if error_white < 30:
   	 return error_yell
    elif error_yell < 30:
   	 return error_white
    else:
   	 return (error_white + error_yell)/2

Выше представлена функция для расчета отклонения от идеального положения. Используются следующие базовые принципы:

1) Отклонение для желтой и белой линии считаются по отдельности
2) Чем дальше находится точка, тем с меньшим весом учитывается ее отклонение. То есть если точка сильно отклоняется от идеального положения, но при этом находится далеко, то его вложение в суммарное отклонение будет небольшим, и примерно таким же как небольшое отклонение точки которая находится близко (близкими считаются точки лежащие внизу изображения, а далекими лежащие наверху изображения)

Таким образом сначала вычисляется суммарное отклонение от желтой линии (идеальное положение точки по оси y — 30), а затем от белой линии (идеальное положение точки по оси y — 300). После этого если ошибка для белой линии мала, то возвращается ошибка для желтой линии и наоборот. Если же обе ошибки велики, то возвращается среднее арифметическое между ними.

if __name__ == '__main__':
    rospy.init_node('image_projection')
    sub_image = rospy.Subscriber('/camera_line/image_line', Image, cbImageProjection, queue_size=1)
    while not rospy.is_shutdown():
   	 try:
   		 rospy.sleep(0.1)
   	 except KeyboardInterrupt:
   		 break
   		 cv2.destroyAllWindows()

Выше функция main, которая вызывается при старте программы. В ней:

1) Инициализируется нода
2) Происходит подписка на топик, содержащий в себе изображение с камеры и указание, что в качестве колбэк функции использовать функцию cbImageProjection

Детектирование и распознавание дорожных знаков

Для детектирования дорожных знаков используется алгоритм SIFT (Scale-invariant feature transform). SIFT — очень мощный инструмент для распознавания необходимых объектов на изображении. Его суть заключается в том, что на изображении ищутся “особые точки”, после чего найденные точки, можно сравнить с такими же точками на идеальном изображении и сделать вывод о наличии/отсутствии необходимого объекта на изображении.
Ниже представлено изображение и найденные на нем особые точки:

21.png

Для сопоставления особых точек используются дескрипторы. Дескриптор — идентификатор ключевой точки, выделяющий её из остальной массы особых точек. В свою очередь, дескрипторы должны обеспечивать инвариантность нахождения соответствия между особыми точками относительно преобразований изображений.

В итоге получается следующая схема решения задачи детектирования дорожных знаков:

1. Изображение переводится в черно-белый формат, для уменьшения количества вычислений.
2. На идеальном изображении выделяются особые точки, а дескрипторы этих точек сохраняются в массив
3. Выделяются особые точки на текущем изображении с камеры
4. По совпадению дескрипторов выделяются соответствующие друг другу ключевые точки.
5. На основе набора совпадающих ключевых точек строится вывод о наличии в данный момент времени дорожного знака на изображении.

Идеальные изображения находятся в папке data_set. Они используются для вычисления идеальных контрольных точек, после чего их можно сравнить с точками на текущем изображении.

39.png

Таким образом нода постоянно публикует в топик “/sign”, имя текущего наблюдаемого дорожного знака, если он есть на изображении и строку “none” если не видит ничего.
Имена знаков:

  • “stop”
  • “parking”
  • “tunnel”

Перейдем к программе (файл sign_detect.py):

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import rospy
import numpy as np
import cv2
from sensor_msgs.msg import Image, CompressedImage
from cv_bridge import CvBridge
import math
import os
from time import sleep
from std_msgs.msg import String

Здесь подключаются основные библиотеки и используемые сообщения. Для работы с изображением все также используется OpenCV.

pub_image = rospy.Publisher('sign_image', Image, queue_size=1)
pub_sign = rospy.Publisher('sign', String, queue_size=1)
cvBridge = CvBridge()
counter = 1
FLANN_INDEX_KDTREE = 0
index_params = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5)
search_params = dict(checks = 50)
flann = cv2.FlannBasedMatcher(index_params, search_params)

Здесь создаются глобальные переменные, необходимые паблишеры и объявляются необходимые классы. Класс для перевода изображения из сообщения типа Image в numpy массив. Также создается элемент класса FlannBasedMatcher, данный класс используется для автоматического сравнивания дескрипторов ключевых точек. Соответственно переменные index_params и search_params необходимы для инициализации элемента класса.

def cbImageProjection(data):
	global kp_ideal, des_ideal, sift, counter, flann
# drop the frame to 1/5 (6fps) because of the processing speed
	if counter % 3 != 0:
		counter += 1
		return
     else:
           counter = 1
	cv_image_original = cvBridge.imgmsg_to_cv2(data, "bgr8")
	cv_image_gray = cv2.cvtColor(cv_image_original, cv2.COLOR_BGR2GRAY)
	kp,des = sift.detectAndCompute(cv_image_gray, None)
	cv_image_original = cv2.drawKeypoints(cv_image_gray,kp,None,(255,0,0),4)
	for i in range(0,3):
		matches = flann.knnMatch(des,des_ideal[i],k=2)
		result = compare_matches(kp, kp_ideal[i], matches)
		sign_msg = String()
		if result == True:
			if i == 0:
				sign_msg.data = "stop"
			elif i == 1:
				sign_msg.data = "parking"
			else:
				sign_msg.data = "tunnel"
			break
		else:
			sign_msg.data = "none"
	print sign_msg.data
	pub_sign.publish(sign_msg)
	pub_image.publish(cvBridge.cv2_to_imgmsg(cv_image_original, "rgb8"))

Выше — основная колбэк функция для изображения. Так как процесс детектирования ключевых точек изображения требует много вычислительных мощностей, а изображение публикуется 30 раз в секунду некоторые кадры изображения необходимо отбрасывать. Для этого будет рассматриваться только каждые третий приходящий кадр.
После отбрасывания лишних кадров, сначала полученное изображение переводится в numpy формат, с которым может работать библиотека OpenCV. После этого изображение переводится в черно белый формат.
Затем функция detectAndCompute класса sift (объявление элемента класса происхоидит в функции ниже) находит ключевые точки и их дескрипторы. После этого найденные ключевые точки визуализируются на изображении для отладки. Теперь в цикле необходимо сопоставить найденные ключевые точки с идеальными ключевыми точками, после чего если найдено много совпадений, то в топик “sign” отправляется имя знака, если же ничего не найдено, то отправляется строка “none”. Цикл необходим для того, чтобы по очереди сопоставить найденные ключевые точки с ключевыми точками идеальных знаков.
Также в конце исполнительной функции в топик “sign_image” публикуется изображение с отмеченным на нем ключевыми точками.

def compare_matches(kp,kp_ideal,matches):
	MATCHES_ERR = 50000
	MATCHES_DIST_MIN = 7
	# print("matches count: ",len(matches))
	good = []
	for m,n in matches:
		if m.distance < 0.7*n.distance:
			good.append(m)
	if len(good) > MATCHES_DIST_MIN:
		src_pts = np.float32([kp[m.queryIdx].pt for m in good])
		dst_pts = np.float32([kp_ideal[m.trainIdx].pt for m in good])
		# print("distance matches count: ",len(good))
		mse_err = find_mse(src_pts,dst_pts)
		print("mse_err: ", mse_err)
		if mse_err < MATCHES_ERR:
			return True
	return False

Выше функция, которая сравнивает дескрипторы ключевых точек. Есть два основных параметра для сравнивания. Первый — это расстояние между соседними ключевыми точками (ведь ключевые точки могут совпадать и отдельным фактором для идентификации является именно расстояние между соседними ключевыми точками). Второй — суммарная квадратичная ошибка в расстояниях между точками MATCHES_ERR.
Функция принимает: kp — найденные на изображении ключевые точки, kp_ideal — ключевые точки с идеального изображения, matches — набор совпавших ключевых точек (совпадения рассчитываются knnMaycher-ом в функции cbImageProjection).
Сначала необходимо найти какие точки подходят нам по параметру расстояния, для этого сравниваем расстояния для каждой ключевой точки изображения и соответствующей совпавшей точки на идеальном изображении. Удовлетворяющие точки заносятся в список good.
Затем если таких совпавших точек оказалось больше 7, то идет расчет среднеквадратичной ошибки между дескрипторами совпадающих ключевых точек. Если эта ошибка оказывается меньше порогового значения, значит мы можем сделать вывод о том что в данный момент знак есть на изображении.

def find_mse(arr1, arr2):
	err = (arr1-arr2)**2
	sum_err = err.sum()
	size = arr1.shape[0]
	sum_err = sum_err/size
	return sum_err

Здесь представлена функция рассчитывания среднеквадратичной ошибки между двумя списками. Сначала вычисляется квадратичная ошибка между двумя соответствующими элементами массивов, затем эти ошибки суммируются и сумма делится на количество элементов массива

def standart_signs():
	dir_path = os.path.dirname(os.path.realpath(__file__))
	dir_path = dir_path.replace('myRace/src', 'myRace/')
	dir_path += 'data_set/detect_sign/'
	img1 = cv2.imread(dir_path + 'stop.png',0)         # trainImage1
	img2 = cv2.imread(dir_path + 'parking.png',0)      # trainImage2
	img3 = cv2.imread(dir_path + 'tunnel.png',0)    
	sift = cv2.xfeatures2d.SIFT_create()
	kp1,des1 = sift.detectAndCompute(img1, None)
	kp2, des2 = sift.detectAndCompute(img2,None)
	kp3, des3 = sift.detectAndCompute(img3,None)
	# img1 = cv2.drawKeypoints(img1,kp1,None,(255,0,0),4)
	kp_ideal = [kp1,kp2,kp3]
	des_ideal = [des1,des2,des3]
	return kp_ideal, des_ideal, sift#, img1

Выше представлена функция вычисления ключевых точек и их дескрипторов из идеальных изображений. Сначала из папки data_set подгружаются необходимые изображения, после чего создается элемент класса sift, который впоследствии будет использоваться в функции cbImageProjection. Затем для идеальных изображений вычисляются ключевые точки и их дескрипторы и записываются в списки kp_ideal — ключевые точки (0 элемент — знак стоп, 1 элемент — знак парковки, 2 элемент — знак въезда в туннель), des_ideal — дескрипторы идеальных точек.

if __name__ == '__main__':
	rospy.init_node('image_projection')
	sub_image = rospy.Subscriber('/camera/image', Image, cbImageProjection, queue_size=1)
	kp_ideal, des_ideal, sift = standart_signs()
	while not rospy.is_shutdown():
		try:
			rospy.sleep(0.1)
			# cv2.imshow('image',img1)
			# if cv2.waitKey(0) == 27:
				# cv2.destroyAllWindows()
				# break
		except KeyboardInterrupt:
			break

Main функция, вызываемая при запуске ноды. Здесь происходит инициализация ноды, создаются необходимые объекты подписчики и указываются колбэк функции, а также запускается цикл работы ROS-а.

Детектирование огней светофора

Алгоритм детектирования сигнала светофора очень похож на алгоритм детектирования линии. Также проводится бинаризация по различным порогам (для желтого, зеленого и красного цветов). Но так как таких цветов на поле достаточно много (например, вездесущая желтая линия), то необходимо также проверять форму задетектированнного бинаризованного объекта. Для детектирования окружности используется Hough circle detector, использующий преобразования Хафа. В простейшем случае преобразование Хафа является линейным преобразованием для обнаружения прямых. Прямая может быть задана уравнением y = mx + b и может быть вычислена по любой паре точек (x, y) на изображении. Главная идея преобразования Хафа — учесть характеристики прямой не как уравнение, построенное по паре точек изображения, а в терминах её параметров, то есть m — углового коэффициента и b — точки пересечения с осью ординат. Исходя из этого прямая, заданная уравнением y = mx + b, может быть представлена в виде точки с координатами (b, m) в пространстве параметров.
Принцип детектирования окружности с помощью детектирования линий очень прост: если прямые перпендикулярные какой либо поверхности пересекаются в одной точке, то скорее всего эта поверхность является окружностью.

40.png

Естественно это очень упрощенное представления, ведь вряд ли на реальном изображении линии сойдутся в одной точке, поэтому рассматривается некоторая окрестность возможных пересечений и проверяется насколько в этой окрестности линии близки друг к другу.
Рассмотрим подробнее функцию HoughCircles(image, method, dp, minDist, param1, param2, minRadius, maxRadius)

● image: бесцветное черно-белое изображение, на котором необходимо найти окружности.
● method: Определяет метод с помощью которого будет происходить детектирование, в нашем случае используется cv2.HOUGH_GRADIENT
● dp: Параметр характеризующий насколько идеальной должна быть найденная окружность.
● minDist: минимальное расстояние между центрами задетектированных окружностей (в пикселях).
● param1, param2: параметры с помощью которых регулируется точность детектирования.
● minRadius: минимальный радиус детектируемой окружности.
● maxRadius: максимальный радиус детектируемой окружности.

Задетектированный сигнал светофора выглядит следующим образом:

20.png

Перейдем к программной части (файл traffic_light_controller.py):

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import rospy
import numpy as np
import cv2
from sensor_msgs.msg import Image, CompressedImage
from cv_bridge import CvBridge
from time import sleep
from std_msgs.msg import String
pub_image = rospy.Publisher('image_traffic_light', Image, queue_size=1)
pub_traffic_light = rospy.Publisher('traffic_light', String, queue_size=1)
cvBridge = CvBridge()
counter = 1

Здесь подключаются необходимые библиотеки. Также создается два паблишера в топики: image_traffic_light и топик traffic_light. В данные топики публикуется обработанное изображение для отладки и цвет сигнала светофора, который видится в данный момент (публикуется строка, если никакой их сигналов не виден публикуется none). По аналогии с обработкой дорожных знаков данная нода также затрачивает много вычислительных мощностей, поэтому частоту обработки кадров необходимо ограничить. Будет обрабатываться каждый третий кадр, для этого создается переменная counter.

def cbImageProjection(data):
	global counter
	if counter % 3 != 0:
		counter += 1
		return
     else: 
           couner = 1	
	cv_image_original = cvBridge.imgmsg_to_cv2(data, "bgr8")
	cv_image_original = cv2.GaussianBlur(cv_image_original, (3, 3), 0)
	cv_image_gray = cv2.cvtColor(cv_image_original, cv2.COLOR_BGR2GRAY)
	circles = cv2.HoughCircles(cv_image_gray, cv2.HOUGH_GRADIENT, 1, 50, param2 = 20, minRadius = 8, maxRadius = 15 ) 
	green_x, green_y  = mask_green(cv_image_original)
	yellow_x, yellow_y = mask_yellow(cv_image_original)
	red_x, red_y  = mask_red(cv_image_original)
	light_msg = String()
	light_msg.data = "None"
	if circles is not None:
		circles = np.round(circles[0,:]).astype("int")
		for x,y,r in circles:
			cv2.circle(cv_image_gray, (x,y),r,(0,255,0), 3)
			green_err = calc_err(green_x, green_y, x, y) 
			red_err = calc_err(red_x, red_y, x, y)
			yellow_err =  calc_err(yellow_x, yellow_y, x, y)
			if green_err < 400:
				light_msg.data = "green" 
			if red_err < 400:
				light_msg.data = "red"
			if yellow_err < 400:
				light_msg.data = "yellow"
	pub_traffic_light.publish(light_msg)
	temp = np.hsplit(cv_image_gray,2) 
	cv_image_gray = temp[1]
	pub_image.publish(cvBridge.cv2_to_imgmsg(cv_image_gray, "8UC1")) 

Выше описана основная колбэк функция топика изображения с камеры. Рассмотрим ее работу поэтапно:

1) Проверяем какой по счету кадр пришел в обработку, если третий то обрабатываем, иначе завершаем работу функции
2) Размываем изображение с помощью GaussianBlur
3) Переводим изображение в серый формат
4) С помощью метода HoughCircles описанного ранее находим все окружности на изображении и добавляем их в переменную circles. В ней они хранятся в виде списка, где у каждой окружности есть ряд характеристик: координаты центра и радиус.
5) На изображение по очереди накладываются зеленая, желтая и красная маска. Затем на данных изображениях ищутся белые пиксели и координаты этих пикселей возвращаются
6) Проводится сравнение координат найденных окружностей и координат найденных белых пикселей, если есть совпадения то делается вывод о том что наблюдается конкретный сигнал светофора
7) Распознанные окружности переносятся на изображение для отладки
8) Объекты паблишеры публикуют рассчитанные данные

Перейдем к детальному рассмотрению каждой функции в отдельности.

def calc_err(color_x, color_y, circle_x, circle_y):
	if color_x*color_y*circle_y*circle_x > 0:
		x_err = (color_x - circle_x)**2
		y_err = (color_y - circle_y)**2
		err = x_err+y_err / 2
		return err
	return 100000

Выше указана функция для расчета ошибки между координатами белого пиксела и координатами найденной окружности. Сначала проводится проверка не равна ли нулю какая-либо координата, если она ноль то рассчитывать ошибку не имеет смысла. Сама ошибка рассчитывается как среднеквадратичное отклонение.

def mask_yellow(img):
	hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
	Hue_l = 20
	Hue_h = 35
	Saturation_l = 100
	Saturation_h = 255
	Lightness_l = 50
	Lightness_h = 255
	# define range of yellow color in HSV
	lower_yellow = np.array([Hue_l, Saturation_l, Lightness_l])
	upper_yellow = np.array([Hue_h, Saturation_h, Lightness_h])
	# Threshold the HSV image to get only yellow colors
	mask = cv2.inRange(hsv, lower_yellow, upper_yellow)
	temp = np.hsplit(mask,2)
	mask = temp[1]
	fraction_num = np.count_nonzero(mask)
	if fraction_num > 100:
		for y in range(0, mask.shape[0]-1,5):
			for x in range(0,mask.shape[1]-1,5):
				if mask[y,x]>0:
					return(x+160,y) 
	return 0, 0
def mask_red(img):
	hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
	Hue_l = 0
	Hue_h = 10
	Saturation_l = 30
	Saturation_h = 255
	Lightness_l = 48
	Lightness_h = 255
	# define range of yellow color in HSV
	lower_red = np.array([Hue_l, Saturation_l, Lightness_l])
	upper_red = np.array([Hue_h, Saturation_h, Lightness_h])
	# Threshold the HSV image to get only yellow colors
	mask = cv2.inRange(hsv, lower_red, upper_red)
	temp = np.hsplit(mask,2) 
	mask = temp[1]
	fraction_num = np.count_nonzero(mask)
	if fraction_num > 100:
		for y in range(0, mask.shape[0]-1,5):
			for x in range(0,mask.shape[1]-1,5):
				if mask[y,x]>0:
					return(x+160,y) 
	return 0, 0

def mask_green(img):
	hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
	Hue_l = 46
	Hue_h = 76
	Saturation_l = 86
	Saturation_h = 255
	Lightness_l = 50
	Lightness_h = 255
	# define range of yellow color in HSV
	lower_green = np.array([Hue_l, Saturation_l, Lightness_l])
	upper_green = np.array([Hue_h, Saturation_h, Lightness_h])
	# Threshold the HSV image to get only yellow colors
	mask = cv2.inRange(hsv, lower_green, upper_green)
	temp = np.hsplit(mask,2) 	mask = temp[1]
	fraction_num = np.count_nonzero(mask)
	if fraction_num > 100:
		for y in range(0, mask.shape[0]-1,5):
			for x in range(0,mask.shape[1]-1,5):
				if mask[y,x]>0:
					return(x+160,y) 
	return 0, 0

Выше представлены функции бинаризации и поиска белых пикселей: для желтого, красного и зеленого сигналов светофора соответственно. Рассмотрим подробнее одну из них, остальные являются похожими за исключением порогов бинаризации. Сама бинаризация проводится по аналогии с бинаризацией в случае детектирования линии. После бинаризации изображение разбивается на две одинаковые части, в дальнейшем будет рассматриваться только правая часть изображения, так как светофор расположен справа. Затем проводится проверка на количество белых пикселей, если их больше 100, то проводится поиск по всему изображению с шагом 5. Если белый пиксель найден, то его координаты (уже в изображении нормального размера) возвращаются функцией.

if __name__ == '__main__':
	rospy.init_node('traffic_light_detector')
	sub_image = rospy.Subscriber('/camera/image', Image, cbImageProjection, queue_size=1)
	while not rospy.is_shutdown():
		try:
			rospy.sleep(0.1)
		except KeyboardInterrupt:
			break

Здесь происходит инициализация ноды и объявление объекта подписчика.

Детектирование шлагбаума

Детектирование шлагбаума производится алгоритмом бинаризации по порогу красного цвета. После чего проверяется общее количество белых пикселей, если оно больше порогового значения, значит в данный момент шлагбаум опущен перед нами. В данном случае также анализируется только каждый третий кадр. Процесс детектирования можно наблюдать ниже:

42.png

Перейдем к коду программы (файл bar_detect.py):

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import rospy
import numpy as np
import cv2
from sensor_msgs.msg import Image, CompressedImage
from cv_bridge import CvBridge
from time import sleep
from std_msgs.msg import Bool
pub_image = rospy.Publisher('image_bar', Image, queue_size=1)
pub_bar = rospy.Publisher('bar', Bool, queue_size=1)
cvBridge = CvBridge()
counter = 1

Выше происходит подключение необходимых библиотек и объявление паблишеров. В данном случае один из них в топик bar публикует True если шлагбаум в данный момент виден и False если шлагбаума перед роботом нет. В топик image_bar публикуется бинаризованное изображение для отладки.

def cbImageProjection(data):
	global counter
	if counter % 3 != 0:
		counter += 1
		return
      else:
           counter = 1
	cv_image_original = cvBridge.imgmsg_to_cv2(data, "bgr8")
	cv_image_original = cv2.GaussianBlur(cv_image_original, (3, 3), 0)
	bar_msg = Bool()
	bar_msg.data, res = mask_red(cv_image_original)
	pub_bar.publish(bar_msg)
	pub_image.publish(cvBridge.cv2_to_imgmsg(res, "8UC1"))

Выше основная колбэк функция. Первой идет проверка на кадр, если данный кадр третий, то начинается его обработка. Сначала изображение переводится из сообщения в тип numpy array. Затем изображение размывается с помощью GaussianBlur, после чего проводится бинаризация и делается вывод о том есть ли шлагбаум на изображении. После чего публикуется бинаризованное изображение и наличие шлагбаума.

def mask_red(img):
	hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
	Hue_l = 0
	Hue_h = 10
	Saturation_l = 30
	Saturation_h = 255
	Lightness_l = 48
	Lightness_h = 255
	# define range of yellow color in HSV
	lower_red = np.array([Hue_l, Saturation_l, Lightness_l])
	upper_red = np.array([Hue_h, Saturation_h, Lightness_h])
	# Threshold the HSV image to get only yellow colors
	mask = cv2.inRange(hsv, lower_red, upper_red)
	mask = cv2.erode(mask, (4,4), iterations = 6) #for detection of big rectangle
	fraction_num = np.count_nonzero(mask)
	if fraction_num > 3000:
		return True, mask
	else:
		return False, mask

Выше описана функция бинаризации. Бинаризация проводится по аналогии с прошлыми случаями. После бинаризации убираются шумы, с помощью функции erode. Данная функция превращает одиночные белые пиксели в черные. Если количество белых пикселей больше 3000, то шлагбаум есть на изображении.

if __name__ == '__main__':
	rospy.init_node('bar_detector')
	sub_image = rospy.Subscriber('/camera/image', Image, cbImageProjection, queue_size=1)
	while not rospy.is_shutdown():
		try:
			rospy.sleep(0.1)
		except KeyboardInterrupt:
			break
			cv2.destroyAllWindows()

Здесь инициализируется ноды и подписчик на топик камеры.

Выполнение миссий

Как говорилось ранее выделяется 5 основных миссии:

● Остановка на светофоре
● Парковка
● Остановка перед шлагбаумом
● Лабиринт
● Езда по линии

Архитектура программ, с помощью которых выполняются миссии практически одинаковы, блок схема представлена ниже (не относится к езде по линии):

43.png

Нода каждой миссии подписывается на необходимый ей топик результата ноды детектирования. После того как “инициатор” ноды задетектирован, отключается движение по линии и начинается исполнение миссии. Затем робот снова начинает двигаться по линии, а нода миссии отключается.

Езда по линии

Управление моделью робота осуществляется при помощи задания необходимой линейной и угловой скорости движения. В топик cmd_vel, публикуется сообщение типа geometry_msgs/Twist, которое состоит из двух векторов:
linear: — линейная скорость (м/c)

x — по оси Х
y — по оси У
z — по оси Z

angular: — угловая скорость (скорость вращения вокруг своей оси, рад/c)

x — вокруг оси х
y — вокруг оси У
z -вокруг оси Z

Оси координат модели робота, расположены следующим образом:

44.png

Здесь синяя ось — ось Z, красная — ось X, зеленая — ось Y.
Из-за того что робот имеет дифференциальную колесную базу, он может перемещаться только вдоль оси Х и вокруг оси Z. Соответственно, для того, чтобы робот поехал вперед, необходимо отправить следующее сообщение:
msg:

linear:

x = 0.1
y = 0
z = 0
angular:

x = 0
y = 0
z = 0

Для того, чтобы робот крутился на месте:
msg:

linear:

x = 0
y = 0
z = 0
angular:

x = 0
y = 0
z = 0.1

А также для того чтобы крутился по дуге:
msg:

linear:

x = 0.1
y = 0
z = 0
angular:

x = 0
y = 0
z = 0.1

Перейдем к алгоритму управления. Отклонение от идеального положения уже известно (алгоритм расчета рассмотрен ранее), осталось реализовать регулятор. В качестве регулятора используется ПИ регулятор, который управляет как угловой, так и линейной скоростью робота. Например на поворотах линейную скорость необходимо уменьшать, а на прямых участках наоборот увеличивать. Структуру программы можно представить следующим образом:

45.png

Так как при работе данной ноды, ничто больше не может управлять скоростью робота (канал управления скоростью занят), необходимо иметь флаг, с помощью которого ноду можно будет включать и выключать. При запуске нода является включенной, для отключения необходимо в топик line_move_flag отправить сообщение типа std_msgs Bool, содержащее False. Для повторного запуска в этот же топик нужно отправить True.
Перейдем к коду программы (файл line_control.py):

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import rospy
from time import sleep
from std_msgs.msg import Float64, Bool
from geometry_msgs.msg import Twist
pub_vel = rospy.Publisher('cmd_vel', Twist, queue_size=1)
integral = 0
move_flag = True

Итак, здесь подключаются необходимые библиотеки и сообщения. Создается объект паблишер, для топика cmd_vel. Который будет отправлять скорость на робота. Создается переменная integral хранящая в себе интегральную составляющую регулятора. Данную переменную необходимо сделать глобальной, для того, чтобы она не обнулялась после каждого исполнения функции. Также создается переменная move_flag, которая в свою очередь и является переключателем между рабочим и выключенными состояниями. Изначально ее значение True, поэтому робот сразу начнет движение (если запущена нода детектирования линии).

def cbError(error):
	global integral, move_flag
	if(move_flag == True):
		velocity = Twist()
		integral = integral + 0.000005*error.data
		proportional = 0.01*error.data
		up = proportional+integral
		velocity.angular.z = up
		velocity.linear.x = 0.22 - 0.09*abs(up)
		pub_vel.publish(velocity)

Данная функция — колбэк отклонения от идеального положения на линии. Если движение разрешено, то создается сообщение типа Twist. После чего рассчитываются интегральная и пропорциональная составляющие регулятора. Затем рассчитывается управляющее воздействие и в свою очередь подается на линейную и угловую составляющие скорости, после чего необходимая скорость публикуется в топик cmd_vel.

def cb_flag(data):
	global move_flag
	move_flag = data.data

Данная функция — колбэк включения/выключения ноды. Здесь значение пришедшего сообщения переписывается в переменную move_flag

if __name__ == '__main__':
	rospy.init_node('line_control')
	sub_image = rospy.Subscriber('line_error', Float64, cbError, queue_size=1)
	sub_move_flag = rospy.Subscriber('line_move_flag', Bool, cb_flag, queue_size=1)
	while not rospy.is_shutdown():
		try:
			rospy.sleep(0.1)
		except KeyboardInterrupt:
			break

Main функция, вызываемая при запуске ноды. Здесь происходит инициализация ноды, создаются необходимые объекты подписчики и указываются колбэк функции, а также запускается цикл работы ROS-а.

Остановка на светофоре

В рамках данной миссии робот должен остановиться на красный сигнал светофора, и продолжить свое движение только на зеленый сигнал
Начнем с управляющей ноды миссии светофора (файл traffic_light_controller.py):

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import rospy
from time import sleep
from std_msgs.msg import  String, Bool
from geometry_msgs.msg import Twist
light = False

Здесь подключаются необходимые библиотеки. Также создается глобальная переменная light. Если значение переменной False, то робот продолжает ехать по линии. Если же значение True, то активируется миссия светофора.

def cb_traffic_light(data):
    global light
    if(data.data == "yellow" or data.data == "red"):
   	 light = True
    elif(data.data == "green"):
   	 light = False

Выше колбэк топика traffic_light в который публикуется информация о том какой сигнал светофора виден в данный момент. Если виден желтый или красный сигнал то значение переменной light становится равным True и активируется миссия.

def pub_velocity(x, z, time):
    pub_vel = rospy.Publisher('cmd_vel', Twist, queue_size=1)
    vel = Twist()
    for i in range(0, int(time*10)):
   	 vel.linear.x = x
   	 vel.angular.z = z
   	 pub_vel.publish(vel)
   	 rospy.sleep(0.1)

Здесь описана функция публикации заданной скорости какое то количество времени, она принимает необходимую линейную, угловую скорости и время в течении которого необходимо поддерживать данную скорость. В ней объявляется объект паблишер в топик cmd_vel (топик скорости). Затем создается сообщение типа Twist. Для стабильности работы необходимая скорость будет отправляться в цикле. Количество итеарций цикла рассчитывается исходя из времени в течении которого нужно ехать, каждая итерация длится 0.1 секунду.

def do_traffic_light():
    global light
    pub_line_move = rospy.Publisher('line_move_flag', Bool, queue_size=1)
    flag_move_line = Bool()
    flag_move_line.data = False
    rospy.sleep(0.1)
    pub_line_move.publish(flag_move_line)
    print("published stop msg")
    while( light == True):
   	 pub_velocity(0, 0, 0.1)
    flag_move_line.data = True
    pub_line_move.publish(flag_move_line)

Выше функция, описывающая выполнение миссии светофора. Сначала в топик line_move_flag отправляется значение False — это останавливает процесс движения по линии и топик cmd_vel становится не занятым. Затем в топик cmd_vel посылаются нулевые значения и робот останавливается, начинается процесс ожидания смены сигнала светофора. Как только значение переменной light сменяется с True на False, в топик line_move_flag отправляется значение True и продолжается движение по линии.

if __name__ == '__main__':
    rospy.init_node('traffic_light_controller')
    sub_sign = rospy.Subscriber('traffic_light', String, cb_traffic_light, queue_size=1)
    while not rospy.is_shutdown():
   	 try:
   		 if(light == True):
   			 print("start traffic light mission")
   			 do_traffic_light()
   			 break
   	 except KeyboardInterrupt:
   		 break

Здесь происходит инициализация ноды, и подписка на топик traffic_light. В основном цикле, постоянно происходит проверка переменной light, если ее значение True (то есть замечены желтый или красный сигнал светфоора), то вызывается функция исполнения мисии. После ее завершения цикл завершается вызовом break и нода уничтожается.

Парковка

Миссия парковки активируется, когда впервые увиден знак парковки. Проблема в том что знак видится задолго до парковочного места, поэтому после того как знак задетектирован необходимо прождать 9 секунд, прежде чем робот войдет в парковочную зону и начнется выполнение миссии.
Алгоритм выполнения миссии строится по следующей блок схеме:

46.png

Проверка на то свободно ли место, осуществляется с помощью лидара — датчика расстояния. Если расстояния меньше критического значения, значит место свободно.
Первое детектирование знака происходит в этом месте:

48.png

На этом фото видно, что первое парковочное место было свободно и робот занял его:

26.png

Перейдем к коду программы (файл parking.py):

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import rospy
from time import sleep
from std_msgs.msg import Float64, Bool, String
from geometry_msgs.msg import Twist
from sensor_msgs.msg import 
distance = 0
parking = False

Здесь подключаются все необходимые библиотеки и создаются глобальные переменные. Переменная parking является переменной активирующей миссию, при значении True начнется исполнение миссии. Переменная distance хранит в себе расстояние, необходимое для проверки того, свободно ли место.

def cb_sign(data):
    global parking
    if(data.data == "parking"):
   	 parking = True

Выше колбэк функция топика “sign”, если задетектирован знак парковки, то значение переменной parking изменяется с False на True.

def cb_scan(data):
    global distance
    counter = 0
    for i in range(200,300):
   	 if(data.ranges[i] < 1):
   		 distance = distance + data.ranges[i]
   		 counter = counter + 1
    if(counter != 0):
   	 distance = distance/counter
    else:
   	 distance = 0

Данная функция является колбэком топика “scan”, в который приходят измерения лазерного дальномера. В данный топик приходит сообщения типа LaserScan, в данном сообщении хранится множество переменных, но нас интересует массив ranges в котором и хранятся расстояния. Расстояния до объектов расположенных справа от робота хранятся в позициях ranges[200] — ranges[300]. В цикле выполняется проход по этому диапазону списка и если текущий элемент меньше 1 (то есть расстояние замеренное данным лучем меньше 1 метра), данное расстояние учитывается. Таким образом если хоть одно расстояние из всего диапазона будет меньше 1, значение переменной distance будет ненулевое.

def pub_velocity(x, z, time):
    pub_vel = rospy.Publisher('cmd_vel', Twist, queue_size=1)
    vel = Twist()
    for i in range(0, int(time*10)):
   	 vel.linear.x = x
   	 vel.angular.z = z
   	 pub_vel.publish(vel)
   	 rospy.sleep(0.1)

Данная функция отправки сообщений в топик cmd_vel уже была рассмотрена выше.

def do_parking():
    pub_line_move = rospy.Publisher('line_move_flag', Bool, queue_size=1)
    flag_move_line = Bool()
    flag_move_line.data = False
    rospy.sleep(0.1)
    pub_line_move.publish(flag_move_line)    
    print("published stop msg")
    pub_velocity(0, 0, 0.5)
    print("published vel")
    pub_velocity(0, -0.4,4)
    pub_velocity(0, 0, 0.3)
    pub_velocity(0.13, 0,2)
    pub_velocity(0, 0, 0.3)
    pub_velocity(0, 0.4,4)
    pub_velocity(0, 0, 0.5)
    pub_velocity(0, -0.4,4)
    pub_velocity(0, 0, 0.3)
    pub_velocity(-0.13, 0,2)
    pub_velocity(0, 0, 0.3)
    pub_velocity(0, 0.4,4)
    pub_velocity(0,0,0.5)
    flag_move_line.data = True
    pub_line_move.publish(flag_move

Здесь описывается выполнение самой миссии, рассмотрим эту функцию поэтапно:

● отключение движения по линии
● остановка
● поворот направо на 90°
● остановка (остановка после каждого движения нужна для того, чтобы движения робота получались наиболее четкими)
● проезд вперед на 0.26 м
● остановка
● поворот налево на 90° (этим движением фиксируется элемент парковки, так как робот должен быть направлен не в сторону линии)
● остановка
● поворот направо на 90°
● остановка
● проезд назад на 0.26 м
● поворот налево на 90° (поворот по направлению линии)
● возврат к движению по линии
if __name__ == '__main__':
    rospy.init_node('parking')
    sub_sign = rospy.Subscriber('sign', String, cb_sign, queue_size=1)
    sub_scan = rospy.Subscriber('scan', LaserScan, cb_scan, queue_size=1)
    while not rospy.is_shutdown():
   	 try:
   		 if(parking == True):
   			 print("start parking mission")
   			 rospy.sleep(9)
   			 print(distance)
   			 if(distance > 1 or distance == 0):    			           	 do_parking()
   			 else:
   				 rospy.sleep(2)
   				 do_parking()
   			 break
   	 except KeyboardInterrupt:
   		 break

Здесь происходит инициализация ноды и подписка на топик sign и scan. В основном цикле постоянно производится проверка переменной parking, если ее значение True, то начинается выполнение миссии по алгоритму:

● 9-секундное ожидание, того что робот доехал до первого парковочного места
● если переменная distance больше 1 или равна нулю (то есть если препятствий не обнаружено), то вызывается функция do_parking()
● если же первое место было занято, то необходимо подождать 2 секунды (столько занимает доезд до второго места) и вызвать функцию do_parking

Остановка перед шлагбаумом

В данной миссии есть два варианта действий:

1. объехать шлагбаум по объездному пути
2. остановиться перед шлагбаумом и проехать прямо

Остановиться перед шлагбаумом гораздо быстрее, поэтому выбирается этот вариант действий. В данной миссии есть одна важная особенность. При подъезде к шлагбауму желтая линия пропадает и появляется белая линия слева, поэтому очень важно, чтобы белая линия справа оставалась видимой, иначе робота развернет. Поэтому необходимо останавливаться и продолжать движение плавно во избежании искривления траектории робота.

22.png

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import rospy
from time import sleep
from std_msgs.msg import Float64, Bool, String
from geometry_msgs.msg import Twist
stop_bar = False

Подключение необходимых библиотек, объявление глобальной переменной stop_bar. Выполнение миссии начнется когда stop_bar примет значение True.

def cb_bar(data):
	global stop_bar
	stop_bar = data.data

Колбэк функция топика bar. Здесь данные топика переписываются в переменную stop_bar. Таким образом как только положение шлагбаума изменится, изменится и значение переменной stop_bar.

def pub_velocity(x, z, time):
	pub_vel = rospy.Publisher('cmd_vel', Twist, queue_size=1)
	vel = Twist()
	for i in range(0, int(time*10)):
		vel.linear.x = x
		vel.angular.z = z
		pub_vel.publish(vel)
		rospy.sleep(0.1)

Функция отправки сообщений в топик cmd_vel, описывалась ранее.

def do_stop():
	pub_line_move = rospy.Publisher('line_move_flag', Bool, queue_size=1)
	flag_move_line = Bool()
	flag_move_line.data = False
	rospy.sleep(0.1)
	pub_line_move.publish(flag_move_line)
	# pub_velocity(0.2,0,1)
	for i in range(20,0, -2):
		pub_velocity(i/100,0,0.2)
	while stop_bar == True:
		pub_velocity(0,0,0.1)
	flag_move_line.data = True
	pub_line_move.publish(flag_move_line)

Основная функция исполнения миссии. Разберем ее поэтапно:

• отключение движение по линии
● плавная остановка (необходима для того, чтобы при остановке из за инерции робота не повело в сторону)
● пока значение переменной stop_bar — True, то есть шлагбаум опущен, стоим на месте
● продолжение движения по линии
if __name__ == '__main__':
	rospy.init_node('stop_bar')
	sub_bar = rospy.Subscriber('bar', Bool, cb_bar, queue_size=1)
	while not rospy.is_shutdown():
		try:
			if(stop_bar == True):
				print("stop detected")
				do_stop()
				break
		except KeyboardInterrupt:
			break

Здесь инициализируется нода, и происходит подписка на топик bar. В основном цикле производиться постоянная проверка переменной stop_bar. Если ее значение меняется на True, то начинается исполнение миссии. После того как миссия выполнена, исполнительный файл завершается.

Лабиринт

Исполнение этой миссии разделяется на два этапа:

1) Детектирование знака и доезд до лабиринта
2) Нахождение выхода из лабиринта

Первый этап выполняется довольно прост, после того как знак туннеля задетектирован робот продолжает движение вперед в течении 3 секунд.

51.png

Второй этап заключается в создании алгоритма для прохождения лабиринта. Лабиринт представляет из себя коробку с цилиндрическими препятствиями внутри:

52.png

Для его прохождения достаточно ехать вдоль правой стены, параллельно огибая встречающиеся препятствия. Блок схему алгоритма можно представить следующим образом:

53.png

Перейдем к программе (файл tunnel_start.py):

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import rospy
from time import sleep
from std_msgs.msg import Float64, Bool, String
from geometry_msgs.msg import Twist
from sensor_msgs.msg import LaserScan
ranges = list()
tunnel = False
in_tunnel = False

Здесь подключаются необходимые библиотеки и создаются глобальные переменные. Переменная ranges будет хранить показания лазерного дальномера в формате списка. Переменная tunnel активирует начало миссии.
Переменная in_tunnel активирует алгоритм прохождения лабиринта.

def cb_sign(data):
	global tunnel
	if(data.data == "tunnel"):
		tunnel = True

Выше описана колбэк функция топика sign. Если задетектирован знак въезда в туннель, то есть в топик пришло сообщение содержащее строку “tunnel”, то значение переменной tunnel изменяется с False на True.

def cb_scan(data):
	global ranges
	ranges = data.ranges

В колбэке топика scan, замеры лазерного дальномера переписываются в переменную ranges, которая будет использоваться в дальнейшем.

def pub_velocity(x, z, time):
	pub_vel = rospy.Publisher('cmd_vel', Twist, queue_size=1)
	vel = Twist()
	for i in range(0, int(time*10)):
		vel.linear.x = x
		vel.angular.z = z
		pub_vel.publish(vel)
		rospy.sleep(0.1)

Выше описана функция отправки сообщений в топик cmd_vel.

def do_tunnel():
	pub_line_move = rospy.Publisher('line_move_flag', Bool, queue_size=1)
	flag_move_line = Bool()
	flag_move_line.data = False
	rospy.sleep(3)
	rospy.sleep(0.1)
	pub_line_move.publish(flag_move_line)
	pub_velocity(0.2,0,2)
	for i in range(20,0, -2):
		pub_velocity(i/100,0,0.3)

Здесь описана функция активации миссии туннеля. Разберем ее поэтапно:

● происходит отключение движения по линии
● робот движется вперед 2 секунды, чтобы заехать в лабиринт
● плавная остановка
def sign(data):
	if(data >= 0):
		return 1
	else:
		return -1

Функция sign является функцией определения “знака” числа. То есть если в нее отправить число -29, то она вернет -1, если же в нее отправить положительное число, то она вернет 1.

def in_tunnel_go():
	global ranges
	vel_x = 0.12
	vel_z = 0
	error = 4*(0.23 - ranges[300])
	if(abs(error) > 1.5):
		error = sign(error)*1.5
	vel_z = error
	for i in range(0,360,1):
		if(ranges[i] < 0.15):
			if(i <= 30 or i >= 330):
				vel_x = -0.09
				vel_z = 0.4		
	pub_velocity(vel_x, vel_z, 0.1)

Здесь описывается функция прохождения лабиринта. Сначала создаются переменные хранящие в себе скорость. Линейная скорость при движении вдоль стены равна 0.12.

В качестве регулятора для движения вдоль стены используется пропорциональный регулятор. В качетсве источника данных используется 300-ый луч лазерного дальномера, его направление показано на рисунке ниже:

54.png

Луч, смотрящий точно направо не подходит для реализации регулятора, так как система имеет небольшое запаздывание. Поэтому выбирается луч, смотрящий направо и немного вперед.
Робот должен держаться на расстоянии 0.23 м от стены, а коэффициент усиления равен 4, поэтому ошибка рассчитывается по следующей формуле:
error = 4*(0.23 — ranges[300])
После расчета ошибки проверяется нет ли препятствий вокруг робота. Если они есть, то роботу необходимо отъехать назад, поворачивая направо, если же препятствий нет, то скорости не изменяются.
На последнем этапе рассчитанные скорости публикуются в топик cmd_vel.

if __name__ == '__main__':
	rospy.init_node('tunnel')
	sub_sign = rospy.Subscriber('sign', String, cb_sign, queue_size=1)
	sub_bar = rospy.Subscriber('scan', LaserScan, cb_scan, queue_size=1)
	while not rospy.is_shutdown():
		try:
			if(tunnel == True and in_tunnel == False):
				print("tunnel detected")
				do_tunnel()
				in_tunnel = True
			elif(in_tunnel == True):
				in_tunnel_go()
		except KeyboardInterrupt:
			break

Здесь происходит инициализация ноды, а также подписка на топики sign и scan. В основном цикле идет проверка переменных tunnel и in_tunnel. Если знак туннеля задетектирован (то есть значение переменной tunnel True, а переменной in_tunnel False), то выполняется подъезд к туннелю и значение переменной in_tunnel меняется на True. При следующих итерациях цикла вызывается функция in_tunnel_go(). Таким образом она продолжит вызываться, пока нода не будет выключена принудительно.

Создание и запуск launch файлов, исправление возможных ошибок

Launch файлы

Запускать все вышеописанные файлы по отдельности очень долго, поэтому для запуска нескольких нод одновременно используются launch файлы. Создано два лаунч файла:

● запуск нод детектирования (detection.launch)
● запуск нод выполнения миссий (control.launch)

Рассмотрим структуру launch файла, на примере detection.launch:

<launch>
    <node pkg="myRace" name="bar_detect" type="bar_detect.py"/>

    <node pkg="myRace" name="line_detect" type="line_detect.py"/>

    <node pkg="myRace" name="sign_detect" type="sign_detect.py"/>

    <node pkg="myRace" name="traffic_light_detect" type="traffic_light_detector.py"/>

</launch>

Первая строка, объявляет launch файл, а последняя его закрывает.
Вызов необходимого исполнительного файла производится по следующей структуре:

● pkg — имя пакета, в котором расположен файл
● name — то как будет называться нода при инициализации (то есть по какому имени к ней обращаться с помощью команды rosnode)
● type — имя исполнительного файла

Launch файлы хранятся в отдельной папке launch внутри пакета myRace и имеют расширение .launch. Для их запуска используется команда roslaunch, например:
$ roslaunch myRace detection.launch
Одной из распространенных ошибок при запуске лаунч файла, который в свою очередь запускает ноды написанные на языке python, является следующая:
ERROR: cannot launch node of type [turtlebot3_teleop/turtlebot3_teleop_key]: can’t locate node [turtlebot3_teleop_key] in package [turtlebot3_teleop]
Данная ошибка говорит о том, что команда roslaunch не смогла найти указанной в лаунч файле ноды, это может быть вызвано двумя причинами:

1. неправильно указано имя исполнительного файла ноды
2. окружение ROS не знает о существовании данного исполнительного файла с расширением .py (то есть написанного на языке python)

Первая ошибка исправляется, соответствующим изменением имени файла.
Чтобы исправить вторую ошибку необходимо дать окружению ROS знать, о существовании исполнительного файла, для этого необходимо перейти в директорию, в которой находится исполнительный файл:
$ cd ~/tb_ws/src/myRace/src
И затем выполнить команду chmod +x на данный исполнительный файл:
$ chmod +x “имя файла”
Например:
$chmod +x traffic_light_controller.py

Использований rqt_graph для отслеживания ошибок

Для отслеживания связи между нодами и топиками, может использоваться утилита rqt, а именно ее плагин rqt_graph. Для его вызова в терминале необходимо прописать следующую команду:
$ rqt_graph
Визуализация нормально работающих процессов выглядит следующим образом:

55.png

56.png

Здесь в овал обведены имена нод, а топики представлены либо прямоугольниками либо текстом расположенным на линии связи.

Таким образом визуализирую все связи нод и топиков в системе, можно найти архитектурную ошибку (например если нода не читает информацию из необходимого топика).

Описание последовательного запуска всех элементов

Итак, для того, чтобы запустить робота для выполнения всего задания целиком необходимо:

1) Запустить симулятор, с моделью робота и соревновательного поля

команда: $ roslaunch turtlebot3_gazebo turtlebot3_autorace.launch

2) Активировать миссии

команда: $ roslaunch turtlebot3_gazebo turtlebot3_autorace_mission.launch

3) Активировать все ноды детектирования

команда: $ roslaunch myRace detection.launch

4) Активировать все управляющие ноды, после чего робот начнет двигаться и выполнять миссии

команда: $ roslaunch myRace control.launch

иллюстрировать
Пишите небрежно, организуйте свои мысли, нет намерения

Школа малины можно использовать для удаленного подключения к Ubuntu: можно использовать следующие методы:
ssh [имя пользователя]@[IP]
 Затем введите пароль

1. Установка программного обеспечения для ПК системы

(1) Установить Ubuntu16.04

немного

(2) Установить рос-кинетику

Установка исходного кода
1. Источник установки:

sudo sh - c ' . /etc/ lsb- release && echo "deb ht tp: / /mi r rors.ustc.edu.cn/ ros/ubuntu/$DISTRIB_CODENAME main" > /etc/apt/sources.list.d/ros-latest.list'

2. Добавьте ключ:

sudo apt-key adv --keyserver hkp://ha.pool.sks-keyservers.net:80 --recv-key 0xB01FA116

3. Обновление:

sudo apt-get update

4. Установка, здесь описана на установке настольных компьютеров:

sudo apt-get install ros-kinetic-desktop-full

5. Решение зависимостей:

sudo rosdep init
rosdep update

6. Настройки окружающей среды:

echo "source /opt/ros/kinetic/setup.bash" >> ~/.bashrc
source ~/.bashrc

(3) Установите Turtlebot3 и зависимости:

1. Установите пакет зависимости Turtlebot3, создайте новый терминал (Ctrl+Alt+T), скопируйте вход следующим образом:

sudo apt-get install ros-kinetic-joy ros-kinetic-teleop-twist-joy ros-kinetic-teleop-twist-keyboard roskinetic-
laser-proc ros-kinetic-rgbd-launch ros-kinetic-depthimage-to-laserscan ros-kinetic-rosserialarduino
ros-kinetic-rosserial-python ros-kinetic-rosserial-server ros-kinetic-rosserial-client ros-kineticrosserial-
msgs ros-kinetic-amcl ros-kinetic-map-server ros-kinetic-move-base ros-kinetic-urdf roskinetic-
xacro ros-kinetic-compressed-image-transport ros-kinetic-rqt-image-view ros-kinetic-gmapping
ros-kinetic-navigation

2. Установите исходный код Turtlebot3:

cd ~/catkin_ws/src/
git clone https://github.com/ROBOTIS-GIT/turtlebot3_msgs.git
git clone https://github.com/ROBOTIS-GIT/turtlebot3.git
cd ~/catkin_ws && catkin_make

Если Catkin_make не допускает никаких ошибок, подготовка к ПК использованию черепаховой коты3 была завершена.

2. Настройки сети

иллюстрировать
• ROS требует, чтобы IP -адрес связи между беспроводной сетью в Turtlebot3 и удаленным ПК.
• Этот раздел представляет, как установить основной режим TurtleBot3

(1) Синхронизация времени

1. В Turtlebot3 и ПК, откройте терминал (Ctrl+Alt+T) и выполните следующие команды для установки Chrony:

sudo apt-get install chrony

2. В Turtlebot3 и PCS выполните следующие команды для установки NTPDate:

sudo apt-get install ntpdate

3. В Turtlebot3 и PCS выполните следующее время синхронизации команды:

sudo ntpdate ntp.ubuntu.com

4. В Turtlebot3 и PCS выполняется следующее время обнаружения команд: Синхронизация:

date

(2) Конфигурация сети


1. В Turtlebot и PC откройте терминал (Ctrl+Alt+T) и выполните следующую команду, чтобы получить соответствующий IP -адрес:

ifconfig

2. Выполните следующую команду, чтобы открыть документ сценария и изменить IP -адрес в соответствии с рисунком выше:

gedit ~/.bashrc

3. Отключите документацию сценария и соблюдайте следующие команды, чтобы окружающая среда вступила в силу:

source ~/.bashrc

В -третьих, начальный тест

иллюстрировать
Перед началом, пожалуйста, сделайте настройки сети, чтобы убедиться, что хост ПК и TurtleBot3 можно правильно сообщить.

(1) Тестовый старт Turtlebot3

[Я называю две инструкции здесь в качестве инструкции запуска, и она будет часто использоваться позже]

1. На хосте ПК откройте терминал (Ctrl+Alt+T) и запустите инструкцию Roscore:

roscore

2. На Turtlebot3 откройте терминал (Ctrl+Alt+T) и запустите инструкции для операции:

roslaunch turtlebot3_bringup turtlebot3_robot.launch

Если установка и запуск успешны, в процессе терминала нет ошибок, и LDS Lidar нормально вращается.

【обращать внимание】
Если возникает ошибка: ROS «не является ни файлом запуска в пакете» …
Решение:

cd catkin_wc/
source devel/setup.bash

(2) Пульт дистанционного управления (клавиатура)

На хосте ПК откройте терминал (Ctrl+Alt+T), запустите инструкцию управления клавиатурой:

roslaunch turtlebot3_teleop turtlebot3_teleop_key.launch

После успеха пробега, w Advances, x шаг назад, поворот налево, D Поверните направо, S останавливается

Четыре, удар

иллюстрировать
Синхронное позиционирование и построение карты (одновременная локализация и картирование) — это концепция: я надеюсь, что у робота будет неизвестное место с неизвестной средой. В процессе упражнений характеристики карты (такие как стены, столбы и т. Д.) посредством повторного наблюдения. Затем создайте карту повышенного уровня в соответствии с вашим собственным местоположением, чтобы достичь цели позиционирования и построения карт одновременно.

1. Сначала запустите инструкции по запуску
2. На хосте ПК создайте новый терминал (Ctrl+Alt+T), а затем запустите файл запуска Slam:

export TURTLEBOT3_MODEL=burger
roslaunch turtlebot3_slam turtlebot3_slam.launch

3. На хосте ПК создайте новый терминал (CTRL+ALT+T), запустите наблюдение за программным обеспечением RVIZ Визуализация:

rosrun rviz rviz -d `rospack find turtlebot3_slam`/rviz/turtlebot3_slam.rviz

4. На хосте ПК новый терминал (CTRL+ALT+T) запустите инструкцию по управлению клавиатурой, управляйте мобильным зданием TurtleBot3:

roslaunch turtlebot3_teleop turtlebot3_teleop_key.launch

5. После завершения карты сохраните карту на локальный хост ПК, создайте новый терминал и запустите следующие команды

rosrun map_server map_saver -f ~/map

5. Навигация

иллюстрировать
• Основная цель навигационной технологии состоит в том, чтобы позволить роботу вступить в ожидаемую позицию.
• Данные карты с предыдущего шага используются при навигации.

1. Сначала запустите инструкции по запуску
2. На хосте ПК создайте новый терминал (Ctrl+Alt+T), запустите навигационный файл:

export TURTLEBOT3_MODEL = burger
roslaunch turtlebot3_navigation turtlebot3_navigation.launch map_file:=/home/map.yaml

[Примечание] MAP_FILE должен быть изменен, чтобы сохранить его самостоятельно
3. На хосте ПК создайте новый терминал (CTRL+ALT+T), запустите программное обеспечение для визуализации RVIZ:

rosrun rviz rviz -d `rospack find turtlebot3_navigation`/rviz/turtlebot3_nav.rviz

• Перед началом навигации Turtlebot3 должен знать свою позицию и осанку. В программном обеспечении визуализации RVIZ на хосте ПК, пожалуйста, следуйте инструкциям.
• Нажмите кнопку «Оценка 2D позы»
• Установите общую позицию на карте, щелкнув и перетаскивая направление на карте.

Примечание: Данная стрелка означает начальное положение и направление черепахи3. Лидар будет нарисован на относительном положении на карте. Если дисплей не верен, повторите вышеуказанный процесс.

• Отправьте целевую позицию в визуальном программном обеспечении RVIZ на хосте ПК. TurtleBot3 автоматически перейдет к:
• Нажмите кнопку 2D NAV Целью
• Нажмите на целевую позицию на карте, чтобы перетащить точку стрелки в направлении ожидания TurtleBot3, чтобы достичь позиции.

Примечание: Если путь к целевой позиции не может быть запланирован, навигация не работает. Чтобы немедленно сделать остановку робота, пожалуйста, отправьте текущую позицию Turtlebot3 в качестве целевой позиции.

6. Камера

(1) Rasbon Pi запускает камеру

1. Запустите программное обеспечение для конфигурации Raspi-Config из командной строки

sudo raspi-config

2. Выберите параметры интерфейса
3. Выберите камеру
4. Выберите IS, а затем выберите «Закончить»,Перезагрузите малину
5. Проверьте камеру, запустите:

raspistill -o cam.jpg

Фотография cam.jpg будет сохранена в текущем каталоге, чтобы проверить, успешно ли она.

(2) калибровка изображения

  1. [Удаленный ПК] Новый терминал, Start Roscore
roscore

2. [Turtlebot SBC] Новый терминал, запустите камеру

roslaunch turtlebot3_autorace_camera turtlebot3_autorace_camera_pi.launch

3. [Удаленный ПК] Новый терминал, просмотрите интерфейс изображения

qt_image_view

Выберите ТЕМУ:/Камера/Изображение/Сжатие или/Камера/Изображение/Если нормально, вы можете увидеть изображения
5. [Удаленный ПК] Новый терминал, откройте интерфейс конфигурации

rosrun rqt_reconfigure rqt_reconfigure

• Выберите камеру, чтобы настроить значение параметра, чтобы сделать камеру чистым, достаточно ярким. После этого покройте каждое значение в папке Turtlebot3_autorage_camera/калибровки/Camera_calibtion в Turtorbot3_autorage 3_autorage 3_autorace_camera/Калибровка/Камера_калибровка
• Новые значения параметров будут использоваться для достижения лучших эффектов отображения в следующий раз.
• Если параметры не могут быть загружены нормально, вы можете попытаться изменить Paramera.cfg в папке Raspicam_Node/CFG на Raspberry

Introduction: Build Your Own Turtlebot Robot!

EDIT:

Further informations related on software and control are availables at this link:

https://hackaday.io/project/167074-build-your-own-turtlebot-3-backbone

The direct link to the code is:

https://github.com/MattMgn/foxbot_core

Why this project?

Turtlebot 3 is the perfect platform to deep into electronics, robotics and even AI! I propose you to build your own turtlebot step-by-step with affordable components without sacrificing features and performance. With one thing in mind: keeping the best from the initial robot, its modularity, simplicity and the huge number of packages for autonomous navigation and AI from the open-source community.

This project is an opportunity for beginners to acquire notions of electronics, mechanics and computer sciences, and for the more experienced to get a powerful platform to test and develop artificial intelligence algorithms.

What you will discover in this project?

You are about to discover which essential mechanical and electronic parts must be kept from the original bot to guarantee complete compatibility.

The whole build process will be detailed: going from 3D parts printing, assembling and the several components, soldering and integrating electronics to finally code compiling on Arduino. This instructable will conclude on a ‘hello world’ example to familiarize you with ROS. If anything seems unclear, feel free to ask question!

Supplies

Electronics:

1 x Single Board Computer to run ROS, could be a Raspberry Pi or a Jetson Nano for example

1 x Arduino DUE, you could also use a UNO or a MEGA

1 x Proto-board that fits Arduino DUE pin-out available here

2 x 12V DC motors with encoders (100 RPM option)

1 x L298N motor driver

2 x 5V regulator

1 x Battery (3S/4S LiPo battery for example)

2 x ON/OFF switches

2 x LED

2 x 470kOhm Resistors

3 x 4 pins JST connectors

1 x USB cable (at least one between the SBC and the Arduino)

Sensors:

1 x Current sensor (optional)

1 x 9 Degrees of Freedom IMU (optional)

1 x LIDAR (optional)

Chassis:

16 x Turtlebot modular plates (which can also be 3D printed)

2 x Wheels 65mm diameter (6mm width option)

4 x Nylon spacers 30mm (optional)

20 x M3 inserts (optional)

Others:

Wires

M2.5 and M3 screws and inserts

3D printer or someone who can print the parts for you

A hand drill with a set a drill bits like this one

Step 1: Description

This robot is a simple differential drive that uses 2 wheels directly mounted on their motor and a roller caster which is placed in the rear to prevent the robot from falling over. The robot is divided into two layers:

  • the Bottom Layer: with the propulsion group (battery, motor controller and motors), and the ‘low level’ electronics: Arduino microcontroller, voltage regulator, switches…
  • the Upper Layer: with the ‘high level’ electronic namely the Single Board Computer and the LIDAR

Those layers are linked with printed parts and screws to ensure the robustness of the structure.

Electronic schematic

The schematic might appear a bit messy. It’s a schematic drawing and it doesn’t represent all wires, connectors and the proto-board but it can be read as follow:

A 3S Litihum Ion Polymer battery with 3000mAh capacity powers the first circuit, it powers both the motor controller board (L298N) and a first 5V regulator for motor encoders and Arduino. This circuit is enabled through a switch with a LED that indicates its ON/OFF state.

The same battery powers a second circuit, the input voltage is converted to 5V to power the Single Board Computer. Here also, the circuit is enabled through a switch and a LED.

Additional sensors like a LIDAR or a camera can then be added directly on the Raspberry Pi through USB or the CSI port.

Mechanical design

The robot frame is composed of 16 identical parts that formed 2 squared layers (28cm width). The many holes allow to mount additional parts wherever you need it and offer a complete modular design. For this project, I decided to get the originals TurtleBot3 plates but you can also 3D printed them as their design is open source.

Step 2: Motor Block Assembly

Motor preparation

The first step is to add 1mm thick foam tape around each motor to prevent vibrations and noise when motor will spinning.

Printed parts

The motor holder results in two parts that grip the motor like a vice. 4 screws achieved to tight the motor in the holder.

Each holder is composed of several holes that host M3 inserts to be mounted on the structure. There are more holes than actually needed, the extra holes could eventually be used to mount extra part.

3D printer settings : all parts are printed with the following parameters

  • 0.4mm diameter nozzle
  • 15% material infill
  • 0.2 mm height layer

Wheel

Wheels chosen are covered with rubber to maximize adhesion and ensure slip free rolling condition. A clamping screw maintains the wheel mounted on the motor shaft. The diameter of the wheel should be large enough to cross minor step and ground irregularity (those wheels are 65mm diameter).

Fixation

When you have done with one motor block, repeat the previous operations and then simply fix them into the layer with M3 screws.

Step 3: Switches and Cable Preparation

Motor cable preparation

Generally the motor-encoder comes with a cable including on one side a 6pin connector that connects the back of the encoder PCB, and naked wires on the other side .

You have the possibility to directly solder them on your proto-board or even your Arduino, but I recommend you to use female pin headers and JST-XH connectors instead. Thus you can plug/unplugged them on your proto-board and make your assembly easier.

Tips : you can add expandable sleeving braid around your wires and pieces of shrink tube near connectors, doing so you will get a ‘clean’ cable.

Switch and LED

To enable the two power circuits, prepare 2 LED and switches cables: at first solder a 470kOhm resistor on one of the LED pin, then solder the LED on one the switch pin. Here also, you can use a piece of shrink tube to hide the resistor inside. Be careful to solder the LED in the right direction! Repeat this operation to get two switch/led cables.

Assembly

Assemble the previously made cables on the corresponding 3D printed part. Use a nut to maintain the switch, the LEDs don’t require glue, just force enough to fit it in the hole.

Step 4: Electronic Boards Wiring

Boards layout

A proto-board fitting the Arduino board layout is used to reduce the number of wires. On the top of the proto-board, the L298N is stacked with Dupont female header (Dupont are ‘Arduino like’ headers).

L298N preparation

Originally, L298N board doesn’t come with corresponding male Dupont header, you need to add a 9 pins row below the board. You need to realize 9 holes with 1mm diameter drill bit in parallel of the existing holes as you can see on the picture. Then link the corresponding pins of the 2 rows with soldering materials and short wires.

L298N pin-out

The L298N is composed of 2 channels allowing speed and direction control:

  • direction through 2 digital outputs, called IN1, IN2 for the first channel, and IN3 and IN4 for the second
  • speed through 1 digital outputs, called ENA for the first channel and ENB for the second

I chose the following pin-out with the Arduino:

  • left motor: IN1 on pin 3, IN2 on pin 4, ENA on pin 2
  • right motor: IN3 on pin 5, IN4 on pin 6, ENB on pin 7

5V regulator

Even if the l298N is normally able to provide 5V, I still add a small regulator. It powers the Arduino through VIN port and the 2 encoders on the motors. You could skip this step by directly using the built-in L298N 5V regulator.

JST connectors and Encoder pin-out

Use 4 pins female JST-XH connector adapters, each connector is then linked to:

  • 5V from regulator
  • a Ground
  • two digital input ports (for exemple: 34 and 38 for the right encoder and 26 and 30 for the left one)

Extra I2C

As you may have noticed, there is an extra 4pin JST connector on the proto-board. It is used for connecting I2C device like an IMU, you can do the same and even add your own port.

Step 5: Motor Group and Arduino on the Bottom Layer

Motor blocks fixation

Once the bottom layer is assembled with the 8 Turtlebot’s plates, simply use 4 M3 screws directly in the inserts to maintain motor blocks. Then you can plug motor power wires to the L298N outputs and the previously made cables to the proto-board JST connectors.

Power distribution

Power distribution is simply realized with a barrier terminal block. On one side of the barrier, a cable with a XT60 female plug is screwed to connect to LiPo battery. On the other side, our two LED/switch cables previously soldered are screwed. Thus each circuit (Motor and Arduino) could be enabled with its own switch and the corresponding green LED.

Cable management

Quickly you will have to deal with a lot of cables! To reduce the messy aspect, you can use the ‘table’ previously 3D printed. On the table, maintain your electronic boards with double sided tape, and under the table let the wires freely flow.

Battery maintaining

To avoid the ejection of the battery when driving your robot, you can simply use a hair elastic band.

Roller caster

Not really a roller caster but a simple half sphere fixed with 4 screws on the bottom layer. It is enough to ensure stability of the robot.

Step 6: Single Board Computer and Sensors on the Upper Layer

Which Single Board Computer to choose?

I don’t need to present you the famous Raspberry Pi, its number of use cases largely exceeds the robotics field. But there is a much more powerful challenger for the Raspberry Pi that you might ignore. Indeed the Jetson Nano from Nvidia embeds a powerful 128-core graphical card in addition to its processor. This particular graphical card has been develop to accelerate computational expensive tasks such as image processing or neural network inference.

For this project I chose the Jetson Nano and you can find the corresponding 3D part among the attached files, but if you want to go with the Raspberry Pi there are many printable cases here.

5V Regulator

Whatever board you decided to bring on your robot, you need a 5V regulator. The latest Raspberry Pi 4 requires 1.25A max but Jetson Nano requires up to 3A on stress so I opted for the Pololu 5V 6A to have a power reserve for future components (sensors, lights, steppers…), but any cheap 5V 2A should do the job. The Jetson use a 5.5mm DC barrel and the Pi a micro USB, grab the corresponding cable and solder it to the regulator output.

LIDAR layout

The LIDAR used here is the LDS-01, there are various others 2D LIDAR that could be used like RPLidar A1/A2/A3, YDLidar X4/G4 or even Hokuyo LIDARs. The only requirement is that it needs to be plugged through USB and be placed centered above the structure. Indeed if the LIDAR is not well centered, the map created by the SLAM algorithm may shift the estimated position of walls and obstacles from their real position. Also if any obstacles from the robot cross the laser beam, it is going to reduce range and field of view.

LIDAR mounting

The LIDAR is mounted on a 3D printed part that follow its shape, the part itself is hold on a rectangular plate (actually in plywood on the picture but could be 3D printed as well). Then an adapter part allows the ensemble to be fixed on the upper turtlebot plate with nylon spacers.

Camera as additional sensor or LIDAR replacement

If you don’t want to spend too much money into a LIDAR (which cost around 100$), go for a camera: there also exist SLAM algorithms that run only with a monocular RGB camera. Both SBC accept USB or CSI camera.

Moreover the camera will let you run computer vision and object detection scripts!

Assembly

Before closing the robot, pass cables through the bigger holes in the upper plate:

  • the corresponding cable from the 5V regulator to your SBC
  • the USB cable from the Programming Port of the Arduino DUE (the closest to the DC barrel) to a USB port of your SBC

Then hold the upper plate in position with a dozen of screws. Your robot is now ready to be programmed, WELL DONE!

Step 7: Make It Move!

Compile the Arduino

Open your favorite Arduino IDE, and import the project folder called own_turtlebot_core, then select your board and the corresponding port, you can refer to this excellent tutorial.

Adjust the Core settings

The project is composed of two files, and one needs to be adapted to your robot. So let’s open own_turtlebot_config.h, and discover which lines require our attention:

#define ARDUINO_DUE            // ** COMMENT THIS LINE IF YOUR NOT USING A DUE **

Should be used only with Arduino DUE, if not comment the line.

#define RATE_CONTROLLER_KP           130.0               // ** TUNE THIS VALUE **
#define RATE_CONTROLLER_KD           5000000000000.0     // ** TUNE THIS VALUE **
#define RATE_CONTROLLER_KI           0.00005             // ** TUNE THIS VALUE **

Those 3 parameters correspond to the rate controller gains used by the PID to maintain the desired speed. Depending on the battery voltage, the mass of the robot, the wheel diameter and mechanical gear of your motor, you will need to adapt their values. PID is a classic controller and you will be not detailed here but this link should give you enough inputs to tune your own.

/* Define pins */
// motor A (right)
const byte motorRightEncoderPinA = 38;      // ** MODIFY WITH YOUR PIN NB **
const byte motorRightEncoderPinB = 34;      // ** MODIFY WITH YOUR PIN NB **
const byte enMotorRight = 2;                // ** MODIFY WITH YOUR PIN NB **
const byte in1MotorRight = 4;               // ** MODIFY WITH YOUR PIN NB **
const byte in2MotorRight = 3;               // ** MODIFY WITH YOUR PIN NB **
// motor B (left)
const byte motorLeftEncoderPinA = 26;       // ** MODIFY WITH YOUR PIN NB **
const byte motorLeftEncoderPinB = 30;       // ** MODIFY WITH YOUR PIN NB **
const byte enMotorLeft = 7;                 // ** MODIFY WITH YOUR PIN NB **
const byte in1MotorLeft = 6;                // ** MODIFY WITH YOUR PIN NB **
const byte in2MotorLeft = 5;                // ** MODIFY WITH YOUR PIN NB **

This block defines the pinout between the L298N and the Arduino, simply modify the pin number to match yours. When you have done with the config file, compile and upload the code!

Install and configure ROS

Once you have reach this step, the instructions are exactly the same as the ones detailed on the excellent TurtleBot3’s manual , you need to scrupulously follow

well done TurtleBot 3 is now yours and you can run all the existing packages and tutorials with ROS.

Ok but what is ROS?

ROS stands for Robots Operating System, it might seems quite complex at first but it’s not, just imagine a way of communication between hardware (sensors and actuators) and software (algorithms for navigation, control, computer vision…). For example, you can easily swap your current LIDAR with an other model without break your setup, because each LIDAR publish the same LaserScan message. ROS is widely used is robotics,

Run your first example

The ‘hello world’ equivalent for ROS consist in teleoperate your robot through the remote computer. What you want to do is to send velocity commands to make the motors spin, the commands follow this pipe:

  • a turtlebot_teleop node, running on the remote computer, publish a «/cmd_vel» topic including a Twist message
  • this message is forwarded through the ROS messages network to the SBC
  • a serial node allows the «/cmd_vel» to be received on the Arduino
  • the Arduino reads the message and set the angular rate on each motor to match the desired linear and angular velocity of the robot

This operation is simple and can be achieved by running the command lines listed above! If you want more detailed information just watch the video.

[SBC]

roscore

[SBC]

rosrun rosserial_python serial_node.py _port:=/dev/ttyACM0 _baud:=115200

[Remote computer]

export TURTLEBOT3_MODEL=${TB3_MODEL}
roslaunch turtlebot3_teleop turtlebot3_teleop_key.launch

To go further

You need to know a last thing before trying all the official examples, in the manual each time you face this command:

roslaunch turtlebot3_bringup turtlebot3_robot.launch

you need to run this command on your SBC instead:

rosrun rosserial_python serial_node.py _port:=/dev/ttyACM0 _baud:=115200

And if you have a LIDAR run the associated command on your SBC, in my case I run a LDS01 with the line below:

roslaunch hls_lfcd_lds_driver hlds_laser.launch

And that’s all, you have definitively built your own turtlebot :) You are ready to discover the fantastic capabilities of ROS, and to code vision and machine learning algorithms.

Понравилась статья? Поделить с друзьями:
  • Trupulse 200b инструкция на русском
  • Turtle wax полироль для кузова инструкция по применению
  • Truma therme tt2 инструкция бойлер
  • Turtle wax горячий воск инструкция
  • Turtle air 2 инструкция на русском