실제 로봇에 카메라를 장착하다 보면 카메라 방향이 뒤집혀 설치되는 경우가 많습니다. TurtleBot3 Burger에 Pi Camera 2를 장착했을 때 영상이 상하 반전되거나, 180도 회전된 상태로 출력될 수 있습니다.
OpenCV에서는 cv2.flip() 또는 cv2.rotate()를 사용하여 이미지를 뒤집거나 회전할 수 있습니다.
대표적인 방법은 다음과 같습니다.
flipped = cv2.flip(frame, -1)
여기서 -1은 상하와 좌우를 모두 뒤집는다는 뜻입니다. 즉, 이미지를 180도 회전한 것과 같은 결과가 됩니다.
cv2.flip()의 옵션은 다음과 같습니다.
cv2.flip(frame, 0) # 상하 반전
cv2.flip(frame, 1) # 좌우 반전
cv2.flip(frame, -1) # 상하 + 좌우 반전, 180도 회전 효과
카메라가 완전히 거꾸로 설치되어 있다면 일반적으로 다음 코드를 사용하면 됩니다.
frame = cv2.flip(frame, -1)
또는 180도 회전으로 명확하게 표현하고 싶다면 다음 코드를 사용할 수도 있습니다.
frame = cv2.rotate(frame, cv2.ROTATE_180)
둘 다 결과는 거의 같습니다.
1. OpenCV 실습용 ROS 2 패키지 만들기
이제 /camera/image_raw를 구독하여 OpenCV로 처리하는 ROS 2 Python 패키지를 만듭니다.
작업 공간으로 이동합니다.
cd ~/turtlebot3_ws/src
OpenCV 실습용 패키지를 생성합니다.
ros2 pkg create tb3_opencv_tutorial \
--build-type ament_python \
--dependencies rclpy sensor_msgs sudo d image_transport

생성된 패키지 구조는 다음과 같습니다.
tb3_opencv_tutorial/
├── package.xml
├── setup.py
├── setup.cfg
├── resource/
│ └── tb3_opencv_tutorial
├── tb3_opencv_tutorial/
│ ├── __init__.py

필요한 패키지를 설치합니다.
sudo apt update
sudo apt install -y python3-opencv ros-humble-cv-bridge ros-humble-image-transport
여기서 중요한 패키지는 cv_bridge입니다. ROS 2의 sensor_msgs/msg/Image 메시지와 OpenCV의 numpy 이미지 배열 사이를 변환하는 역할을 합니다.

/camera/image_raw를 받아서 이미지를 정상 방향으로 보정한 뒤 /camera/image_flipped로 발행하는 노드를 작성합니다.
cd ~/turtlebot3_ws/src/tb3_opencv_tutorial/tb3_opencv_tutorial
touch image_flip_node.py

2. 노드 작성
아래 코드를 작성합니다.
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
import cv2
class ImageFlipNode(Node):
def __init__(self):
super().__init__('image_flip_node')
self.bridge = CvBridge()
self.image_sub = self.create_subscription(
Image,
'/camera/image_raw',
self.image_callback,
10
)
self.image_pub = self.create_publisher(
Image,
'/camera/image_flipped',
10
)
self.get_logger().info('Image Flip Node started.')
self.get_logger().info('Subscribe: /camera/image_raw')
self.get_logger().info('Publish : /camera/image_flipped')
def image_callback(self, msg):
try:
frame = self.bridge.imgmsg_to_cv2(msg, desired_encoding='bgr8')
except Exception as e:
self.get_logger().error(f'cv_bridge error: {e}')
return
flipped_frame = cv2.flip(frame, -1)
out_msg = self.bridge.cv2_to_imgmsg(flipped_frame, encoding='bgr8')
out_msg.header = msg.header
self.image_pub.publish(out_msg)
def main(args=None):
rclpy.init(args=args)
node = ImageFlipNode()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()

1) 필요한 라이브러리 import
from sensor_msgs.msg import Image
sensor_msgs.msg.Image는 ROS 2에서 이미지 데이터를 주고받을 때 사용하는 메시지 타입입니다.
카메라에서 나오는 이미지 토픽은 보통 이 Image 메시지 형식을 사용합니다.
예를 들어 일반적인 카메라 토픽은 다음과 같은 형태입니다.
/camera/image_raw
이 토픽에는 카메라 프레임 이미지가 sensor_msgs/Image 타입으로 계속 발행됩니다.
from cv_bridge import CvBridge
cv_bridge는 ROS 이미지 메시지와 OpenCV 이미지 형식 사이를 변환해 주는 도구입니다.
ROS 2의 Image 메시지는 OpenCV에서 바로 사용할 수 없습니다.
반대로 OpenCV에서 처리한 이미지도 ROS 2 토픽으로 바로 발행할 수 없습니다.
그래서 중간에서 변환이 필요합니다.
ROS Image 메시지 → OpenCV 이미지
OpenCV 이미지 → ROS Image 메시지
이 변환을 담당하는 것이 CvBridge입니다.
import cv2
cv2는 OpenCV 라이브러리입니다.
이 코드에서는 OpenCV의 cv2.flip() 함수를 사용해서 이미지를 뒤집습니다.
2) ImageFlipNode 클래스
class ImageFlipNode(Node):
ImageFlipNode는 실제 이미지 처리를 수행하는 ROS 2 노드 클래스입니다.
이 클래스는 Node를 상속받고 있기 때문에 ROS 2 노드로 동작할 수 있습니다.
3) CvBridge 객체 생성
self.bridge = CvBridge()
이 코드는 CvBridge 객체를 생성합니다.
이 객체는 이미지 메시지 변환에 사용됩니다.
이 코드에서 self.bridge는 두 가지 작업에 사용됩니다.
첫 번째는 ROS 이미지 메시지를 OpenCV 이미지로 변환하는 것입니다.
self.bridge.imgmsg_to_cv2()
두 번째는 OpenCV 이미지를 ROS 이미지 메시지로 변환하는 것입니다.
self.bridge.cv2_to_imgmsg()
4) 이미지 구독자 생성
self.image_sub = self.create_subscription(
Image,
'/camera/image_raw',
self.image_callback,
10
)
이 부분은 카메라 이미지 토픽을 구독하는 코드입니다.
각 인자의 의미는 다음과 같습니다.
Image
구독할 메시지 타입입니다.
여기서는 sensor_msgs.msg.Image 타입의 메시지를 받습니다.
'/camera/image_raw'
구독할 토픽 이름입니다.
즉, 이 노드는 /camera/image_raw 토픽에서 카메라 이미지를 받습니다.
self.image_callback
이미지 메시지가 들어왔을 때 실행될 콜백 함수입니다.
새 이미지가 수신될 때마다 image_callback() 함수가 자동으로 호출됩니다.
10
QoS 큐 크기입니다.
간단히 말하면, 처리 대기 중인 메시지를 최대 10개까지 저장할 수 있다는 의미입니다.
카메라 이미지는 실시간성이 중요하기 때문에 큐 크기를 너무 크게 잡을 필요는 없습니다.
일반적인 예제에서는 10 정도를 자주 사용합니다.
5) 이미지 발행자 생성
self.image_pub = self.create_publisher(
Image,
'/camera/image_flipped',
10
)
이 부분은 뒤집힌 이미지를 발행하는 Publisher를 생성하는 코드입니다.
각 인자의 의미는 다음과 같습니다.
Image
발행할 메시지 타입입니다.
출력 이미지도 sensor_msgs.msg.Image 타입입니다.
'/camera/image_flipped'
뒤집힌 이미지를 발행할 토픽 이름입니다.
즉, 이 노드는 처리된 이미지를 다음 토픽으로 내보냅니다.
/camera/image_flipped
10
발행 큐 크기입니다.
6) 로그 출력
self.get_logger().info('Image Flip Node started.')
self.get_logger().info('Subscribe: /camera/image_raw')
self.get_logger().info('Publish : /camera/image_flipped')
이 부분은 노드가 정상적으로 시작되었는지 확인하기 위한 로그 메시지입니다.
노드를 실행하면 터미널에 다음과 같은 메시지가 출력됩니다.
Image Flip Node started.
Subscribe: /camera/image_raw
Publish : /camera/image_flipped
이 로그를 보면 노드가 어떤 토픽을 구독하고, 어떤 토픽으로 발행하는지 바로 확인할 수 있습니다.
7) 이미지 콜백 함수
def image_callback(self, msg):
이 함수는 /camera/image_raw 토픽에서 새로운 이미지 메시지를 받을 때마다 실행됩니다.
여기서 msg는 수신된 ROS 이미지 메시지입니다.
즉, msg의 타입은 다음과 같습니다.
sensor_msgs.msg.Image
8) ROS Image 메시지를 OpenCV 이미지로 변환
try:
frame = self.bridge.imgmsg_to_cv2(msg, desired_encoding='bgr8')
except Exception as e:
self.get_logger().error(f'cv_bridge error: {e}')
return
이 부분은 ROS 이미지 메시지를 OpenCV에서 사용할 수 있는 이미지 배열로 변환합니다.
frame = self.bridge.imgmsg_to_cv2(msg, desired_encoding='bgr8')
imgmsg_to_cv2() 함수는 ROS 이미지 메시지를 OpenCV 이미지로 바꿉니다.
여기서 desired_encoding='bgr8'은 이미지 색상 형식을 BGR 8비트 3채널로 변환하겠다는 의미입니다.
OpenCV는 일반적으로 컬러 이미지를 RGB가 아니라 BGR 순서로 사용합니다.
RGB: Red, Green, Blue
BGR: Blue, Green, Red
따라서 OpenCV에서 일반적인 컬러 이미지 처리를 하려면 bgr8을 사용하는 것이 보통입니다.
9) 예외 처리
except Exception as e:
self.get_logger().error(f'cv_bridge error: {e}')
return
이미지 변환 과정에서 문제가 생기면 예외가 발생할 수 있습니다.
예를 들어 다음과 같은 상황에서 문제가 생길 수 있습니다.
입력 이미지 인코딩이 맞지 않는 경우
cv_bridge가 정상 설치되지 않은 경우
이미지 메시지 데이터가 깨진 경우
토픽에서 예상과 다른 형식의 데이터가 들어온 경우
예외가 발생하면 에러 로그를 출력하고 함수 실행을 중단합니다.
return
이 return이 없으면 변환에 실패했는데도 아래 코드가 계속 실행되면서 추가 에러가 발생할 수 있습니다.
10) 이미지 뒤집기
flipped_frame = cv2.flip(frame, -1)
이 코드가 실제 이미지 처리의 핵심입니다.
cv2.flip() 함수는 이미지를 뒤집을 때 사용합니다.
형식은 다음과 같습니다.
cv2.flip(src, flipCode)
여기서 src는 원본 이미지입니다.
이 코드에서는 원본 이미지가 frame입니다.
frame
flipCode는 이미지를 어떤 방향으로 뒤집을지를 결정합니다.
OpenCV에서 flipCode의 의미는 다음과 같습니다.
| flipCode | 의미 |
|---|---|
0 | 상하 반전 |
1 | 좌우 반전 |
-1 | 상하좌우 반전 |
현재 코드는 다음과 같이 되어 있습니다.
cv2.flip(frame, -1)
따라서 이미지를 상하좌우 모두 반전합니다.
쉽게 말하면 이미지를 180도 돌린 것과 거의 같은 결과가 나옵니다.
예를 들어 카메라가 거꾸로 장착되어 있을 때 유용합니다.
11) OpenCV 이미지를 ROS Image 메시지로 변환
out_msg = self.bridge.cv2_to_imgmsg(flipped_frame, encoding='bgr8')
이미지를 뒤집은 뒤에는 다시 ROS 2 토픽으로 발행해야 합니다.
그런데 flipped_frame은 OpenCV 이미지 형식입니다.
ROS 2 토픽으로 발행하려면 다시 sensor_msgs/Image 메시지로 변환해야 합니다.
이 변환을 수행하는 함수가 다음입니다.
cv2_to_imgmsg()
여기서는 뒤집힌 이미지 flipped_frame을 bgr8 형식의 ROS 이미지 메시지로 변환합니다.
12) 헤더 정보 복사
out_msg.header = msg.header
이 코드는 원본 이미지 메시지의 헤더 정보를 출력 이미지 메시지에 그대로 복사합니다.
ROS 메시지의 header에는 보통 다음 정보가 들어 있습니다.
timestamp
frame_id
예를 들어 원본 이미지의 헤더에는 이런 정보가 있을 수 있습니다.
stamp: 이미지가 촬영된 시간
frame_id: 카메라 좌표계 이름
이 정보를 유지하는 것은 중요합니다.
특히 RViz, TF, SLAM, Visual Odometry, 로봇 비전 시스템에서는 이미지가 어느 시간에, 어떤 좌표계 기준으로 촬영되었는지가 중요합니다.
따라서 뒤집힌 이미지도 원본 이미지와 같은 시간 정보와 좌표계 정보를 갖도록 헤더를 복사합니다.
13) 뒤집힌 이미지 발행
self.image_pub.publish(out_msg)
이 코드는 최종적으로 뒤집힌 이미지를 /camera/image_flipped 토픽으로 발행합니다.
이제 다른 노드에서는 다음 토픽을 구독해서 뒤집힌 이미지를 사용할 수 있습니다.
/camera/image_flipped
예를 들어 RViz2나 rqt_image_view에서 이 토픽을 보면 반전된 카메라 영상을 확인할 수 있습니다.
14) main 함수
def main(args=None):
rclpy.init(args=args)
main() 함수는 프로그램이 시작될 때 실행되는 메인 함수입니다.
rclpy.init(args=args)
이 코드는 ROS 2 Python 클라이언트 라이브러리를 초기화합니다.
ROS 2 노드를 사용하려면 반드시 먼저 rclpy.init()을 호출해야 합니다.
node = ImageFlipNode()
이 코드는 앞에서 만든 ImageFlipNode 객체를 생성합니다.
즉, 이 시점에 다음 작업들이 실행됩니다.
노드 생성
CvBridge 생성
/camera/image_raw 구독자 생성
/camera/image_flipped 발행자 생성
로그 출력
rclpy.spin(node)는 노드를 계속 실행 상태로 유지합니다.
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
ROS 2 노드는 한 번 실행하고 끝나는 프로그램이 아니라, 토픽을 계속 기다리면서 콜백을 처리해야 합니다.
따라서 spin()을 호출하면 노드는 계속 살아 있으면서 /camera/image_raw 토픽에 이미지가 들어오는지 기다립니다.
이미지가 들어오면 자동으로 image_callback() 함수가 실행됩니다.
except KeyboardInterrupt:
pass
이 부분은 사용자가 Ctrl + C로 노드를 종료했을 때 발생하는 예외를 처리합니다.
터미널에서 ROS 2 노드를 실행하다가 종료할 때 보통 다음 키를 누릅니다.
Ctrl + C
이때 KeyboardInterrupt가 발생하는데, 코드에서는 이것을 잡아서 특별한 에러 메시지 없이 종료되도록 했습니다.
15) 노드 정리 및 종료
node.destroy_node()
rclpy.shutdown()
rclpy.spin(node)가 끝나면 노드를 정리합니다.
node.destroy_node()
이 코드는 생성된 ROS 2 노드를 명시적으로 제거합니다.
rclpy.shutdown()
이 코드는 ROS 2 Python 클라이언트 라이브러리를 종료합니다.
즉, 프로그램 종료 전에 ROS 2 관련 리소스를 정리하는 과정입니다.
이 노드는 다음 동작을 수행합니다.
/camera/image_raw토픽 구독- ROS 2 이미지 메시지를 OpenCV 이미지로 변환
cv2.flip(frame, -1)로 이미지 180도 보정- 보정된 이미지를 ROS 2 이미지 메시지로 변환
/camera/image_flipped토픽으로 발행
3. image_flip_node 실행 파일 등록하기
setup.py를 다시 수정합니다.
cd ~/turtlebot3_ws/src/tb3_opencv_tutorial
nano setup.py
entry_points를 다음과 같이 수정합니다.
entry_points={
'console_scripts': [
'image_flip_node = tb3_opencv_tutorial.image_flip_node:main',
],
},

4. 빌드 및 실행
수정 후 다시 빌드합니다.
cd ~/turtlebot3_ws
colcon build --packages-select tb3_opencv_tutorial
source install/setup.bash
실행 파일이 등록되었는지 확인합니다.
ros2 pkg executables tb3_opencv_tutorial
다음과 같이 출력되면 정상입니다.
tb3_opencv_tutorial image_flip_node

5. 정상 방향 카메라 영상 확인하기
먼저 카메라를 실행합니다.
ros2 launch turtlebot3_bringup camera.launch.py format:=BGR888

다른 터미널에서 이미지 보정 노드를 실행합니다.
ros2 run tb3_opencv_tutorial image_flip_node

토픽 목록을 확인합니다.
ros2 topic list
다음 토픽이 보이면 정상입니다.
/camera/image_flipped

원격 PC에서 rqt_image_view를 실행합니다.
rqt_image_view

토픽 선택 메뉴에서 다음 토픽을 선택합니다.
/camera/image_flipped
이제 거꾸로 나오던 카메라 영상이 정상 방향으로 출력됩니다.

강의에서는 원본 영상과 보정 영상을 비교해서 보여주면 이해가 빠릅니다.
/camera/image_raw → 거꾸로 된 원본 영상
/camera/image_flipped → 정상 방향으로 보정된 영상
6. 보정된 영상을 기준으로 OpenCV 처리하기
카메라가 거꾸로 설치된 상태라면 앞으로의 모든 OpenCV 처리는 /camera/image_raw가 아니라 보정된 이미지 기준으로 하는 것이 좋습니다.
방법은 두 가지입니다.
첫 번째 방법은 보정 노드에서 /camera/image_flipped를 만들고, 다른 OpenCV 노드가 /camera/image_flipped를 구독하는 방식입니다.
/camera/image_raw
↓
image_flip_node
↓
/camera/image_flipped
↓
OpenCV 응용 노드
↓
/camera/image_processed
두 번째 방법은 OpenCV 응용 노드 안에서 처음부터 이미지를 뒤집은 뒤 처리하는 방식입니다.
frame = self.bridge.imgmsg_to_cv2(msg, desired_encoding='bgr8')
frame = cv2.flip(frame, -1)
7. 정상 방향 보정 후 흑백 변환 노드 작성하기
이번에는 카메라 영상을 정상 방향으로 보정한 뒤 흑백으로 변환하여 /camera/image_processed로 발행하는 노드를 작성합니다.
cd ~/turtlebot3_ws/src/tb3_opencv_tutorial/tb3_opencv_tutorial
touch corrected_gray_node.py

아래 코드를 작성합니다.
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
import cv2
class CorrectedGrayNode(Node):
def __init__(self):
super().__init__('corrected_gray_node')
self.bridge = CvBridge()
self.image_sub = self.create_subscription(
Image,
'/camera/image_raw',
self.image_callback,
10
)
self.image_pub = self.create_publisher(
Image,
'/camera/image_processed',
10
)
self.get_logger().info('Corrected Gray Node started.')
self.get_logger().info('Subscribe: /camera/image_raw')
self.get_logger().info('Publish : /camera/image_processed')
def image_callback(self, msg):
try:
frame = self.bridge.imgmsg_to_cv2(msg, desired_encoding='bgr8')
except Exception as e:
self.get_logger().error(f'cv_bridge error: {e}')
return
corrected = cv2.flip(frame, -1)
gray = cv2.cvtColor(corrected, cv2.COLOR_BGR2GRAY)
processed = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)
out_msg = self.bridge.cv2_to_imgmsg(processed, encoding='bgr8')
out_msg.header = msg.header
self.image_pub.publish(out_msg)
def main(args=None):
rclpy.init(args=args)
node = CorrectedGrayNode()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()

setup.py에 실행 파일을 추가합니다.
entry_points={
'console_scripts': [
'image_flip_node = tb3_opencv_tutorial.image_flip_node:main',
'corrected_gray_node = tb3_opencv_tutorial.corrected_gray_node:main',
],
},

다시 빌드합니다.
cd ~/turtlebot3_ws
colcon build --packages-select tb3_opencv_tutorial
source install/setup.bash

먼저 카메라를 실행합니다.
ros2 launch turtlebot3_bringup camera.launch.py format:=BGR888

실행합니다.
ros2 run tb3_opencv_tutorial corrected_gray_node

원격 PC의 rqt_image_view에서 다음 토픽을 선택합니다.
/camera/image_processed
이제 정상 방향으로 보정된 흑백 영상을 확인할 수 있습니다.
