안녕하세요. 요즘 ROS 공부중인 맨 땅에 헤딩 '맨땅헤' 입니다.
ROS 를 아예 몰랐는데 실습해보면서 이해하고 있는 중입니다.
이번 포스팅에서는 따라할 수는 없겠지만, 코드 리뷰를 하면서 ROS 를 이용한 로봇제어 매커니즘에 대해 정리해보려고 합니다.
ros 를 다루기 전 필요한 환경세팅
gedit ~/.bashrc
bashrc 을 열어서
# ROS 2 Humble 환경 설정
alias humble="source /opt/ros/humble/setup.bash; echo \"ROS2 humble is activated!\""
을 마지막 줄에 추가해주면, humble 이라고 입력시 , setup.bash 가 활성화 됩니다.
# ROS_DOMAIN_ID 설정
export ROS_DOMAIN_ID=30
echo "ROS_DOMAIN_ID set to 30"
humble 활성화 설정 다음에 도메인 ID도 로봇과 동일하게 설정해주자.
우선, 해당 로봇에게 ssh로 연결하고, bring up 을 해줬습니다.

빨간색 글씨는 '토픽 (ex: /scan ) '입니다.
이 로봇에는 ros2 humble 환경이 설치 되어있고, 기본적인 패키지들이 있는 상태입니다.
로봇에게서 받아올 수 있는 정보는 라이다 /scan 정보 입니다.
(실제 토픽리스트로는 로봇에게 이동 명령을 줄 수 있는 /cmd_vel 도 있습니다. )
로봇과 DOMAIN ID 를 맞추고,
ros2 topic list
를 해보면,

브링업만 했는데, 자동적으로 라이다센서가 작동되고 /scan 토픽을 발행하고 있는 것을 알 수 있다.
이를 이용해보려고 한 코드입니다.
obstacle_avoid.py
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import LaserScan
from geometry_msgs.msg import Twist
class ObstacleAvoidanceNode(Node):
def __init__(self):
super().__init__('obstacle_avoidance')
self.get_logger().info('Obstacle Avoidance Node Initialized')
# 라이다 데이터 구독
self.subscription = self.create_subscription(
LaserScan,
'/scan',
self.scan_callback,
10
)
self.subscription # prevent unused variable warning
# 속도 명령 퍼블리셔
self.publisher = self.create_publisher(Twist, '/cmd_vel', 1)
# 속도 명령 메시지
self.cmd_msg = Twist()
# 장애물 탐지 범위 설정
self.obstacle_distance_threshold = 0.35 # 20cm
self.min_intensity_threshold = 1000.0 # 반사 강도가 너무 낮은 값 필터링
def scan_callback(self, msg):
# 라이다 데이터에서 유효한 거리 값 필터링
filtered_ranges = self.filter_noise(msg.ranges, msg.intensities, msg.range_min, msg.range_max)
# 가장 가까운 장애물 거리 확인
closest_distance = min(filtered_ranges) if filtered_ranges else float('inf')
# 충돌 회피 논리
if closest_distance < self.obstacle_distance_threshold:
self.avoid_obstacle()
else:
self.move_forward()
def filter_noise(self, ranges, intensities, range_min, range_max):
"""
라이다 데이터의 노이즈를 필터링.
- 최소/최대 거리 필터링
- 반사 강도(intensity) 기반 필터링
"""
filtered = []
for r, i in zip(ranges, intensities):
if range_min <= r <= range_max and i >= self.min_intensity_threshold:
filtered.append(r)
return filtered
def avoid_obstacle(self):
"""
충돌 회피 동작.
- 후진 또는 회전 명령을 발행.
"""
self.get_logger().info("Obstacle detected! Avoiding...")
self.cmd_msg.linear.x = -0.1 # 뒤로 이동
self.cmd_msg.angular.z = 0.5 # 회전
self.publisher.publish(self.cmd_msg)
def move_forward(self):
"""
장애물이 없을 때 앞으로 이동.
"""
self.cmd_msg.linear.x = 0.2 # 직진
self.cmd_msg.angular.z = 0.0
self.publisher.publish(self.cmd_msg)
def main(args=None):
rclpy.init(args=args)
node = ObstacleAvoidanceNode()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
def_init_(self):
1. 초기화
def __init__(self):
super().__init__('obstacle_avoidance')
self.get_logger().info('Obstacle Avoidance Node Initialized')
- Node 클래스를 상속하여 ROS 2 노드를 정의합니다. 노드 이름은 'obstacle_avoidance'입니다.
- 노드 초기화 시, 로거를 통해 'Obstacle Avoidance Node Initialized'라는 메시지를 출력합니다.
여기서 노드란?
그냥 쉽게 말해서 어떠한 '기능' 이라고 생각하면 쉽습니다. 이전 포스팅에서 썼던 걸 인용하자면,
rqt_graph 로 확인해보자
rqt_graph
우리가 실행시켰던
ros2 run usb_cam usb_cam_node_exe
usb_cam 이 노드 이름이고( 카메라 실행 기능 ), 얘가 발행하고 있는 데이터 타입 (=토픽 ) 의 형태가 /image_raw
이 데이터( 토픽) 을 구독하고 있는 노드가 camera_subscriber ( 카메라 데이터 구독 기능 )
2. 라이다 데이터 구독 (/scan 을 구독 )
self.subscription = self.create_subscription(
LaserScan,
'/scan',
self.scan_callback,
10
)
- 이 부분은 라이다 데이터(/scan 토픽)를 구독합니다.
- 메시지 타입은 LaserScan이며, 구독된 데이터는 self.scan_callback 메서드로 전달됩니다.
- 메시지 큐 크기는 10으로 설정되었습니다.
이 부분은 라이다 데이터(/scan) 를 가져왔다 라고 생각하면 편합니다.
메시지 형태는 LaserScan 입니다.
3. 속도 명령 퍼블리셔
self.publisher = self.create_publisher(Twist, '/cmd_vel', 1)
self.cmd_msg = Twist()
로봇에게 움직이라고 명령 ( /cmd_vel 토픽 ) 을 발행합니다
메시지 형태는 Twsit 입니다.
4. 장애물 탐지 범위
self.obstacle_distance_threshold = 0.35
self.min_intensity_threshold = 1000.0
장애물 탐지 거리 임계값은 0.35m (35cm)로 설정되었습니다.
반사 강도 값이 1000.0 미만인 데이터는 노이즈로 간주하여 필터링합니다.
def scan_callback(self, msg):
- 라이다 데이터 처리 후 명령
def scan_callback(self, msg):
filtered_ranges = self.filter_noise(msg.ranges, msg.intensities, msg.range_min, msg.range_max)
closest_distance = min(filtered_ranges) if filtered_ranges else float('inf')
if closest_distance < self.obstacle_distance_threshold:
self.avoid_obstacle()
else:
self.move_forward()
msg의 의미
msg는 LaserScan 메시지로, 라이다 센서에서 발행한 데이터를 담고 있습니다. 이 데이터는 ROS 2의 sensor_msgs.msg.LaserScan 타입이며, 라이다 스캔의 결과를 포함
filter_noise의 역할
- 거리 유효성 검증:
- msg.ranges 값 중에서 range_min <= 거리 값 <= range_max 범위 내에 있는 값만 유효하다고 간주.
- 반사 강도 기반 필터링:
- msg.intensities 값이 설정된 최소 강도(min_intensity_threshold) 이상인 데이터만 통과.
obstacle_distance_threshold = 0.35 ( def__init__ 에서 선언 )
즉 , 로봇과의 앞에 장애물 거리 ( 라이다 센서로 감지한 거리 ) 가 35cm 이하 일 경우,
avoid_obstacle() 실행 ( 밑에 나옴 )
아니면 move_forward() 실행 ( 앞으로 계속 가는 명령 )
def filter_noise(self, ranges, intensities, range_min, range_max):
ranges : 라이다가 각 박향(각도)에서 측정한 거리 값
ex) [0.5, 1.2, inf, 0.8] ( 단위 : m ) inf : 무한
intensities : 라이다로 감지한 반사 강도 값 -> 반사 강도가 낮으면 데이터를 신뢰할 수 없거나, 장애물 없다고 판단
ex) [1200.0, 900.0, 0.0, 1500.0]
range_min : 라이다의 최소 유효거리
range_max : 라이다의 최대 유효거리
self.min_intensity_threshold (float) : 반사 강도의 최소값(일정 값 이상만 유효 데이터로 간주)
ex) 800.0.
if 조건이 만족되면 filtered.append(r)
거리(r) 값이 두 조건을 만족하면 유효 데이터로 간주하고 filtered 리스트에 추가.
반복이 끝나면 return filtered
유효 데이터로만 구성된 리스트를 반환.
def avoid_obstacle(self):
회피 기동 함수
self.get_logger().info("Obstacle detected! Avoiding...")
self.cmd_msg.linear.x = -0.1 # 뒤로 이동
self.cmd_msg.angular.z = 0.5 # 회전
self.publisher.publish(self.cmd_msg)
cmd_msg 메시지 발행
이 메시지는 def__init__ 에서
self.cmd_msg = Twist() 라고 정의되었고,
그 뒤에
self.cmd_msg.linear.x = 0.1
여기서 Twist() 는 메시지 타입, 얘는
/cmd_vel 토픽으로 발행
정리하자면,
(신문) : (/cmd_vel) 을 발행할꺼야.
근데 무슨 언어로?? -> ( 영어,한국어,일본어... ) : ( Twist ) 로 발행할거야.
무슨 내용을 담고 있는데? -> ( 기삿거리 ) : ( cmd_msg )
def move_forward(self):
앞으로 이동 함수
avoid_obstacle 와 구조가 같으므로 생략.
'ROS2' 카테고리의 다른 글
| ROS2 Humble 에서 내가 발행하고 내가 구독해보자 (USB_CAM) (0) | 2024.12.10 |
|---|---|
| Ubuntu 22.04 에 ROS2 Humble 설치하기 (0) | 2024.12.09 |









