Pi Camera 2를 이용한 OpenCV 응용 : 색상 검출 예제

카메라 영상 처리 강의에서 가장 기본이 되는 응용은 색상 검출입니다. 예를 들어 빨간색 물체를 찾는 예제를 만들 수 있습니다.

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

빨간색 물체만 강조되어 보이면 정상입니다.

Leave a Comment