카메라 영상 처리 강의에서 가장 기본이 되는 응용은 색상 검출입니다. 예를 들어 빨간색 물체를 찾는 예제를 만들 수 있습니다.
HSV 참고 사이트 : HSV 색 공간
색상 검출에서는 보통 BGR 이미지를 HSV 색공간으로 변환합니다.
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
빨간색 영역을 검출하기 위해 HSV 범위를 지정합니다.
lower_red1 = (0, 100, 100)
upper_red1 = (10, 255, 255)
lower_red2 = (160, 100, 100)
upper_red2 = (179, 255, 255)
빨간색은 HSV 색상 범위에서 양끝에 걸쳐 있으므로 두 구간으로 나눠서 처리하는 것이 일반적입니다.
mask1 = cv2.inRange(hsv, lower_red1, upper_red1)
mask2 = cv2.inRange(hsv, lower_red2, upper_red2)
mask = mask1 + mask2
검출된 영역만 원본 이미지에서 추출합니다.
result = cv2.bitwise_and(frame, frame, mask=mask)
이 내용을 ROS 2 노드로 만들면 다음과 같습니다.
cd ~/turtlebot3_ws/src/tb3_opencv_tutorial/tb3_opencv_tutorial
touch red_detect_node.py

노드를 작성합니다.
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
import cv2
class RedDetectNode(Node):
def __init__(self):
super().__init__('red_detect_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/red_detected',
10
)
self.get_logger().info('Red Detect Node started.')
self.get_logger().info('Subscribe: /camera/image_raw')
self.get_logger().info('Publish : /camera/red_detected')
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
frame = cv2.flip(frame, -1)
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
lower_red1 = (0, 100, 100)
upper_red1 = (10, 255, 255)
lower_red2 = (160, 100, 100)
upper_red2 = (179, 255, 255)
mask1 = cv2.inRange(hsv, lower_red1, upper_red1)
mask2 = cv2.inRange(hsv, lower_red2, upper_red2)
mask = mask1 + mask2
result = cv2.bitwise_and(frame, frame, mask=mask)
out_msg = self.bridge.cv2_to_imgmsg(result, encoding='bgr8')
out_msg.header = msg.header
self.image_pub.publish(out_msg)
def main(args=None):
rclpy.init(args=args)
node = RedDetectNode()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()


CvBridge 객체 생성
self.bridge = CvBridge()
ROS 2의 이미지 메시지는 sensor_msgs/msg/Image 형식입니다. 하지만 OpenCV에서 이미지를 처리하려면 NumPy 배열 형태의 OpenCV 이미지 형식이 필요합니다.
CvBridge는 ROS 이미지 메시지와 OpenCV 이미지 사이를 변환해 주는 도구입니다. 이 코드에서는 카메라 이미지 메시지를 OpenCV 이미지로 바꾸고, 처리된 OpenCV 이미지를 다시 ROS 이미지 메시지로 변환할 때 사용됩니다.
즉, CvBridge는 ROS 2와 OpenCV를 연결하는 중간 변환기 역할을 합니다.
이미지 토픽 구독 설정
self.image_sub = self.create_subscription(
Image,
'/camera/image_raw',
self.image_callback,
10
)
이 부분은 /camera/image_raw 토픽으로 들어오는 카메라 이미지를 구독하는 코드입니다.
Image는 수신할 메시지 타입입니다. /camera/image_raw는 카메라 원본 이미지가 발행되는 토픽 이름입니다. self.image_callback은 이미지가 들어올 때마다 자동으로 실행되는 콜백 함수입니다. 마지막의 10은 QoS 큐 크기이며, 처리 대기 중인 메시지를 최대 10개까지 저장할 수 있다는 의미입니다.
즉, 카메라에서 새로운 이미지가 들어오면 image_callback() 함수가 실행되어 이미지 처리를 시작합니다.
처리 결과 이미지 발행 설정
self.image_pub = self.create_publisher(
Image,
'/camera/red_detected',
10
)
이 부분은 빨간색이 검출된 결과 이미지를 발행하기 위한 퍼블리셔입니다.
처리된 이미지는 /camera/red_detected 토픽으로 발행됩니다. 다른 ROS 2 노드나 rqt_image_view 같은 시각화 도구에서 이 토픽을 구독하면 빨간색 영역만 남은 결과 이미지를 확인할 수 있습니다.
즉, 이 노드는 원본 이미지를 입력받고, 빨간색 검출 결과 이미지를 출력하는 구조입니다.
ROS 이미지 메시지를 OpenCV 이미지로 변환
frame = self.bridge.imgmsg_to_cv2(msg, desired_encoding='bgr8')
카메라에서 들어온 msg는 ROS 2 이미지 메시지입니다. OpenCV로 색상 검출을 하려면 이 메시지를 OpenCV에서 사용할 수 있는 이미지 형식으로 바꿔야 합니다.
imgmsg_to_cv2() 함수는 ROS 이미지 메시지를 OpenCV 이미지로 변환합니다. desired_encoding='bgr8'은 이미지를 BGR 색상 형식으로 변환하겠다는 의미입니다.
OpenCV는 기본적으로 RGB가 아니라 BGR 순서로 색상을 사용합니다. 그래서 이 코드에서는 OpenCV 처리에 맞게 bgr8 형식을 사용합니다.
이미지 상하좌우 반전
frame = cv2.flip(frame, -1)
이 부분은 이미지를 뒤집는 코드입니다.
cv2.flip() 함수에서 두 번째 값이 -1이면 이미지를 상하좌우 모두 반전합니다. 즉, 180도 회전한 것과 같은 결과가 됩니다.
이 처리는 카메라가 물리적으로 거꾸로 장착되어 있거나, 영상 방향이 실제 환경과 맞지 않을 때 사용합니다. 만약 카메라 영상 방향이 정상이라면 이 부분은 제거하거나 수정할 수 있습니다.
BGR 이미지를 HSV 색상 공간으로 변환
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
빨간색을 안정적으로 검출하기 위해 BGR 이미지를 HSV 색상 공간으로 변환합니다.
BGR은 파란색, 초록색, 빨간색의 조합으로 색을 표현합니다. 하지만 조명 변화가 있으면 특정 색상을 구분하기 어려울 수 있습니다.
HSV는 색상, 채도, 명도를 기준으로 색을 표현합니다. 여기서 색상 값인 Hue를 이용하면 빨간색, 파란색, 초록색 같은 색상 범위를 비교적 쉽게 분리할 수 있습니다.
그래서 색상 검출 작업에서는 BGR보다 HSV를 많이 사용합니다.
빨간색 HSV 범위 설정
lower_red1 = (0, 100, 100)
upper_red1 = (10, 255, 255)
lower_red2 = (160, 100, 100)
upper_red2 = (179, 255, 255)
이 부분은 빨간색으로 판단할 HSV 범위를 설정하는 코드입니다.
HSV에서 빨간색은 Hue 값이 0도 근처와 180도 근처에 걸쳐 있습니다. 그래서 빨간색을 한 구간만으로 잡으면 일부 빨간색이 검출되지 않을 수 있습니다.
이 코드에서는 빨간색 범위를 두 개로 나누어 검출합니다.
첫 번째 범위는 Hue 값이 0에서 10 사이인 빨간색입니다.
lower_red1 = (0, 100, 100)
upper_red1 = (10, 255, 255)
두 번째 범위는 Hue 값이 160에서 179 사이인 빨간색입니다.
lower_red2 = (160, 100, 100)
upper_red2 = (179, 255, 255)
여기서 두 번째 값은 채도, 세 번째 값은 명도입니다. 채도와 명도의 최소값을 100으로 설정한 이유는 너무 어둡거나 색이 흐린 영역까지 빨간색으로 잘못 검출되는 것을 줄이기 위해서입니다.
빨간색 영역 마스크 생성
mask1 = cv2.inRange(hsv, lower_red1, upper_red1)
mask2 = cv2.inRange(hsv, lower_red2, upper_red2)
mask = mask1 + mask2
cv2.inRange() 함수는 이미지에서 특정 색상 범위에 들어가는 픽셀만 흰색으로 표시하고, 나머지는 검은색으로 표시하는 마스크를 만듭니다.
mask1은 첫 번째 빨간색 범위에 해당하는 영역을 검출합니다. mask2는 두 번째 빨간색 범위에 해당하는 영역을 검출합니다.
그 후 두 마스크를 더해서 전체 빨간색 영역을 하나의 마스크로 만듭니다.
mask = mask1 + mask2
결과적으로 mask에는 빨간색으로 판단된 영역만 흰색으로 표시되고, 나머지 영역은 검은색으로 표시됩니다.
빨간색 영역만 원본 이미지에서 추출
result = cv2.bitwise_and(frame, frame, mask=mask)
이 부분은 생성된 마스크를 이용해 원본 이미지에서 빨간색 영역만 남기는 코드입니다.
cv2.bitwise_and() 함수는 마스크에서 흰색으로 표시된 부분만 원본 이미지에서 살리고, 검은색 부분은 제거합니다.
즉, 빨간색 영역은 원래 색상으로 보이고, 빨간색이 아닌 부분은 검은색으로 처리됩니다.
이 결과 이미지가 최종적으로 /camera/red_detected 토픽으로 발행됩니다.
OpenCV 이미지를 ROS 이미지 메시지로 변환
out_msg = self.bridge.cv2_to_imgmsg(result, encoding='bgr8')
out_msg.header = msg.header
OpenCV로 처리한 result 이미지는 그대로 ROS 2 토픽으로 발행할 수 없습니다. 다시 ROS 이미지 메시지 형식으로 변환해야 합니다.
cv2_to_imgmsg() 함수는 OpenCV 이미지를 ROS 이미지 메시지로 바꿔 줍니다.
out_msg.header = msg.header
이 코드는 원본 이미지 메시지의 헤더 정보를 결과 이미지에도 그대로 복사합니다. 헤더에는 시간 정보와 프레임 ID 같은 정보가 들어 있습니다.
이 정보를 유지하면, 나중에 다른 센서 데이터나 TF 좌표계와 동기화할 때 유리합니다.
결과 이미지 발행
self.image_pub.publish(out_msg)
이 부분은 빨간색 검출 결과 이미지를 /camera/red_detected 토픽으로 발행합니다.
즉, 이 코드의 전체 흐름은 다음과 같습니다.
카메라 이미지 수신 → OpenCV 이미지로 변환 → 이미지 반전 → HSV 변환 → 빨간색 마스크 생성 → 빨간색 영역 추출 → ROS 이미지 메시지로 변환 → 결과 토픽 발행
main 함수와 노드 실행
rclpy.init(args=args)
node = RedDetectNode()
rclpy.spin(node)
main() 함수는 ROS 2 노드를 실제로 실행하는 부분입니다.
rclpy.init()은 ROS 2 Python 클라이언트 라이브러리를 초기화합니다. 그다음 RedDetectNode() 객체를 생성하여 빨간색 검출 노드를 실행할 준비를 합니다.
rclpy.spin(node)는 노드가 계속 실행되도록 유지하는 함수입니다. 이 함수가 실행되는 동안 /camera/image_raw 토픽으로 이미지가 들어오면 계속해서 image_callback() 함수가 호출됩니다.
즉, spin()이 없으면 노드는 한 번 생성되고 바로 종료되기 때문에 지속적인 이미지 처리가 불가능합니다.
setup.py에 추가합니다.
'red_detect_node = tb3_opencv_tutorial.red_detect_node:main',
빌드합니다.
cd ~/turtlebot3_ws
colcon build --packages-select tb3_opencv_tutorial
source install/setup.bash

먼저 로봇에서 카메라 노드를 실행하고 원격 PC에서 색상 검출 노드를 실행합니다.
ros2 run tb3_opencv_tutorial red_detect_node

원격 PC에서 /camera/red_detected 토픽을 확인합니다.
rqt_image_view
빨간색 물체만 강조되어 보이면 정상입니다.

