Topic에 대해 배워보기 전에, 실습 시스템을 구축해봅시다.
이번 ROS 2 강의의 실습들은 Road Balance의 차량형 로봇 SRC를 통해 진행하고자 합니다. 실습을 진행하기 전 Gazebo 환경을 함께 세팅해보겠습니다.
cd ~/ros2_ws/src/du2023-ros2
./setup_scripts.sh
# CMakeLists.txt 수정 (주석)
colcon build --packages-select src_gazebo
source install/local_setup.bash
# CMakeLists.txt 수정 (주석 해제)
colcon build --packages-select src_gazebo
colcon build --packages-select src_odometry
colcon build --packages-select src_controller
source install/local_setup.bash
ros2 launch src_gazebo empty_world.launch.py
sudo apt dist-upgrade
를 입력하면 됩니다.로봇에 부착되어 있는 센서 데이터들과 제어 데이터를 ros2 topic list로 확인해봅시다.
강의를 열심히 따라오면서 ROS 개념을 잊어버렸을 수도 있으므로 간단히 개념 복습을 해보겠습니다.
image from : docs.ros.org
topic을 통해 데이터가 전달되는 과정은 다음과 같습니다.
로봇 프로그래밍 시 센서, 제어 데이터를 비롯하여 다양한 유형의 데이터들이 Topic을 통해 오고 갑니다. ROS에서는 이러한 데이터 형식을 msg - Message라는 형태로 제공하며, 원하는 데이터에 적합한 topic msg를 사용하면 더욱 효율적인 로봇 프로그래밍이 가능해집니다.
geometry_msgs/Twist와 같은 message는 ROS 2에서도 동일하게 사용할 수 있습니다. 다만, ROS 2에서는 msg/srv/action 과 같이 필드의 구분이 하나 더 추가됩니다.
특정 message가 어떻게 구성되어 있는지 알고 싶을 때는 다음과 같은 커멘드 라인을 사용합니다.
$ ros2 interface show geometry_msgs/msg/Twist
# This expresses velocity in free space broken into its linear and angular parts.
Vector3 linear
Vector3 angular
혹은 앞서 제가 살펴본 것 처럼 검색을 해 보아도 됩니다.
ROS 2로 넘어오면서, topic 커멘드들은 다음과 같이 살짝 바뀌었습니다.
ROS 2에서도 GUI툴 RQT를 사용할 수 있습니다. Topic과 관련된 rqt tools들을 사용해봅시다.
ROS 1과 ROS 2의 rqt는 다른 프로그램이며 ROS 1의 plugin과 ROS 2의 plugin이 완전 동일하지는 않습니다.
# Terminal 1
ros2 launch src_gazebo empty_world.launch.py
# Terminal 2
rqt
rqt 상단의 메뉴바에서 Plugins ⇒ Topics ⇒ Topic Monitor를 클릭합니다.
topic monitor를 통해 현재 오가고 있는 topic들을 한눈에 볼 수 있습니다.
이번에는 rqt 상단의 메뉴바에서 Plugins ⇒ Topics ⇒ Message Publisher를 클릭하고, + 버튼을 눌러 publish를 원하는 topic을 추가합니다.
강의노트를 작성하고 있는 2023년 지금도 ROS 1으로 개발된 시스템에 종속성을 가진 기업들과 프로젝트들이 많습니다. ROS 1과 ROS 2를 같이 사용할 수는 없을까요? ros1 bridge를 실습해봅시다.
roslaunch smb_gazebo smb_gazebo.launch
sudo apt install ros-foxy-ros1-bridge -y
# Terminal 1 - ros bridge
ros2 run ros1_bridge dynamic_bridge
# Terminal 2 - ros2 foxy
ros2 run teleop_twist_keyboard teleop_twist_keyboard
참고로, /opt/ros/<ros-version>/setup.bash
는 ROS 시스템을 사용하기 위해서 필요한 설정이 담긴 파일입니다. 혹여나 galactic, humble과 같이 최신 버전을 사용하고 싶을 때 참고하시기 바랍니다.
ROS 1과 ROS 2를 모두 관리하기에는 많은 노력이 필요하고, 원하는 기능이 모두 동작하지 않을 수 있습니다. 이전 버전 legacy가 있는 경우, 잘 판단하여 시스템을 유지/보수합시다.
Topic의 응용으로 Gazebo를 사용하여 나만의 장애물을 만들고, 충돌을 회피하는 코드를 작성해보고자 합니다. 이를 위해 Gazebo의 Building Editor 사용법을 알려드리겠습니다.
├─ wall
├── model.config
└── model.sdf
작성한 물체를 Gazebo에서 두고두고 사용하는 몇가지 방법들이 있습니다.
$ gedit ~/.gazebo/gui.ini
[geometry]
x=0
y=0
[model_paths]
filenames=/home/kimsooyoung/<your-folder-location>
if 'GAZEBO_MODEL_PATH' in os.environ:
os.environ['GAZEBO_MODEL_PATH'] += ":" + gazebo_model_path
else:
os.environ['GAZEBO_MODEL_PATH'] = gazebo_model_path
이제 Gazebo를 실행시키면, 사진과 같이 Insert 부분에 building이 추가되어있음을 확인 가능합니다. 이를 클릭 후 gazebo 환경으로 커서를 옮기면 원하는 위치에 wall을 위치시킬 수 있습니다.
wall이 추가된 world는 별도로 저장해줘야 합니다. File ⇒ Save World As를 클릭하면 완성된 world 파일을 저장합시다.
생성한 world 파일을 launch 파일로 실행해봅시다.
저장해두었던 Gazebo world 파일을 src_gazebo의 worlds 폴더에 위치시킵니다.
launch 파일을 수정하여 새로운 world 파일의 위치를 전달합니다.
def generate_launch_description():
...
# gazebo
pkg_gazebo_ros = FindPackageShare(package='gazebo_ros').find('gazebo_ros')
pkg_path = os.path.join(get_package_share_directory('src_gazebo'))
world_path = os.path.join(pkg_path, 'worlds', '여러분의-world-file.world')
...
# Start Gazebo server
start_gazebo_server_cmd = IncludeLaunchDescription(
PythonLaunchDescriptionSource(os.path.join(pkg_gazebo_ros, 'launch', 'gzserver.launch.py')),
launch_arguments={'world': world_path}.items()
)
colcon build --packages-select src_gazebo
source install/local_setup.bash
ros2 launch src_gazebo empty_world.launch.py
최대한 여러분들만의 장애물을 만들어보시고, 혹시나 이 과정에서 도저히 모르겠는 오류가 발생했거나, 시간이 없는 경우, 우선 제가 제작한 world와 launch file을 사용하도록 합니다. ⇒ ros2 launch src_gazebo wall_world.launch.py
# Terminal 1
ros2 launch src_gazebo empty_world.launch.py
# Terminal 2
cd ~/ros2_ws
colcon build --packages-select py_topic_tutorial
source install/local_setup.bash
ros2 run py_topic_tutorial topic_pub_node
import random
from geometry_msgs.msg import Twist
import rclpy
from rclpy.node import Node
class TwistPubNode(Node):
def __init__(self):
super().__init__('twist_pub_node')
self.get_logger().info(
f'TwistPubNode Created at {self.get_clock().now().to_msg().sec}'
)
# self.twist_publisher = self.create_publisher(Twist, "twist_topic", 10)
self.twist_publisher = self.create_publisher(Twist, '/cmd_vel', 10)
create_publisher는 topic publisher를 생성하는 함수로 3개의 매개변수를 받으며 각각에 대한 설명은 다음과 같습니다.
...
self.create_timer(0.2, self.timer_callback)
def timer_callback(self):
"""Timer will run this function periodically."""
msg = Twist()
# Fill in msg with compatible values
msg.linear.x = 0.5
msg.angular.z = 1.0
self.twist_publisher.publish(msg)
# Terminal 1
$ ros2 launch src_gazebo empty_world.launch.py
# Terminal 2
$ ros2 run py_topic_tutorial topic_sub_node
[INFO] [1672494602.141200628] [scan_sub_node]:
msg.ranges[0] : inf
msg.ranges[30] : inf
msg.ranges[60] : 0.7975894212722778
msg.ranges[90] : inf
msg.ranges[119] : inf
...
예시를 실행시킨 상황에서 Gazebo 상의 로봇 주변으로 물체를 등장시켜봅시다.
scan data의 ranges는 사진과 같은 거리 데이터를 담고 있답니다.
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import LaserScan
class ScanSubNode(Node):
def __init__(self):
super().__init__('scan_sub_node')
queue_size = 10 # Queue Size
self.pose_subscriber = self.create_subscription(
LaserScan, 'scan', self.sub_callback, queue_size
)
create_subscription은 4개의 매개변수를 받으며 각각에 대한 설명은 다음과 같습니다.
sub_callback의 첫번쨰 매개변수는 항상 topic message data가 됩니다. 해당 message에서 원하는 데이터만 추출한 다음, logger를 통해 콘솔 출력을 진행합니다.
def sub_callback(self, msg):
"""Timer will run this function periodically."""
self.get_logger().info(f' \
\nmsg.ranges[0] : {msg.ranges[0]} \
\nmsg.ranges[30] : {msg.ranges[30]} \
\nmsg.ranges[60] : {msg.ranges[60]} \
\nmsg.ranges[90] : {msg.ranges[90]} \
\nmsg.ranges[119] : {msg.ranges[119]} \
')
# gazebo
pkg_gazebo_ros = FindPackageShare(package='gazebo_ros').find('gazebo_ros')
pkg_path = os.path.join(get_package_share_directory('src_gazebo'))
# world_path = os.path.join(pkg_path, 'worlds', 'empty_world.world')
world_path = os.path.join(pkg_path, 'worlds', 'wall_world.world')
# Terminal 1
$ ros2 launch src_gazebo empty_world.launch.py
# Terminal 2
$ ros2 run py_topic_tutorial parking_node
...
[INFO] [1672563255.067421128] [parking_node]: Distance from Front Object : 0.6280616521835327
[INFO] [1672563255.173734128] [parking_node]: Distance from Front Object : 0.5546504259109497
[INFO] [1672563255.281276729] [parking_node]: Distance from Front Object : 0.5124648809432983
[INFO] [1672563255.389067729] [parking_node]: ==== Parking Done!!! ====
...
이 기능을 구현하기 위해서는 Topic Publish와 Subscribe가 모두 필요합니다.
⇒ 따라서, Node의 생성자에서도 Publisher와 Subscriber를 모두 생성합니다.
class ParkingNode(Node):
def __init__(self):
super().__init__('parking_node')
queue_size = 10 # Queue Size
self.twist_publisher = self.create_publisher(Twist, 'cmd_vel', queue_size)
self.scan_subscriber = self.create_subscription(
LaserScan, 'scan', self.sub_callback, queue_size
)
def sub_callback(self, msg):
twist_msg = Twist()
distance_forward = msg.ranges[60]
if distance_forward > 0.5:
self.get_logger().info(f'Distance from Front Object : {distance_forward}')
twist_msg.linear.x = 0.5
self.twist_publisher.publish(twist_msg)
else:
self.get_logger().info('==== Parking Done!!! ====\n')
twist_msg.linear.x = 0.0
self.twist_publisher.publish(twist_msg)
Topic과 ROS 2 Bag을 사용한 예시를 실행해보고자 합니다.
이번 실습을 위해 제가 미리 준비한 데이터셋을 제공드리니, 기억하기 쉬운 위치에 다운받아 주세요.
Windows + WSL2 유저의 경우 explorer.exe
명령어를 통해 파일 탐색기를 열 수 있습니다.
# 예제 실행 준비
sudo apt install ros-foxy-rosbag2
cd ~/ros2_ws
colcon build --packages-select py_cv_tutorial
source install/local_setup.bash
# 터미널 1 – 데이터 위치로 이동하여 ros2 bag 실행
cd <데이터를 저장한 위치>
ros2 bag play quadrupped_train.bag_0.db3
# 터미널 2 – image_view 실행
ros2 run rqt_image_view rqt_image_view
해당 데이터셋은 기차 선로에서 주행하는 4족 보행 로봇으로부터 취득한 것입니다. 기차 선로는 주기적으로 관리가 필요하며, 길고 긴 선로를 로봇이 자동으로 검사해준다면 아주 유용할 것입니다. 이러한 상상을 해보면서 실습에 임해봅시다.
이미지를 다루기 위한 오픈소스 라이브러리인 OpenCV를 사용해보려 합니다. 준비된 예시와, ros2 bag 파일을 실행하면 cv2.imshow를 통해 연속된 이미지를 확인할 수 있습니다.
# 터미널 1
$ cd <bag 파일 위치>
$ ros2 bag play quadruped_train.bag_0.db3
[INFO] [1667077909.362223100] [rosbag2_storage]: Opened database 'quadrupped_train/quadrupped_train.bag_0.db3' for READ_ONLY.
# 터미널 2
$ ros2 run py_cv_tutorial img_sub
[INFO] [1667077909.613586300] [image_subscriber]: Receiving video frame
[INFO] [1667077910.934147000] [image_subscriber]: Receiving video frame
...
from cv_bridge import CvBridge # Package to convert between ROS and OpenCV Images
import cv2 # OpenCV library
class ImageSubscriber(Node):
def __init__(self):
# Used to convert between ROS and OpenCV images
self.br = CvBridge()
...
def listener_callback(self, data):
...
# Convert ROS Image message to OpenCV image
current_frame = self.br.imgmsg_to_cv2(data, "bgr8")
edge_frame = self.hough_transform(current_frame)
직선 검출을 위해 사용된 OpenCV 기능들은 아래와 같습니다. 로직을 업그레이드하여 여러분만의 선로 검출 알고리즘을 만들어 보세요!
컴퓨터 비전만으로 완벽한 선로 인식을 구현하기는 너무나 힘듭니다. 딥러닝을 사용해서 이를 극복할 수 있을 것이며, 데이터셋의 제작을 위해 rosbag 데이터에서 이미지를 추출하는 예시를 준비하였습니다.
$ ros2 run py_cv_tutorial rosbag2_to_timedimg
/home/kimsooyoung/djhrd_ws/quadrupped_train/quadrupped_train.bag_0.db3
saved: color_1666796292992515592.png
saved: color_1666796293035428479.png
saved: color_1666796293079139778.png
saved: color_1666796293120768031.png
def main(args=None):
# Change below roots to your ros2 bag locations
ROOT_DIR = "/home/kimsooyoung/djhrd_ws/quadrupped_train"
FILENAME = "/quadrupped_train.bag_0.db3"
DESCRIPTION = "color_"