테스트 환경

노드 및 토픽 구조

ROS2 - YOLO 패키지 개발 및 테스트

공통

카메라 입력 및 디렉토리 설정

# ROS2용 작업 공간(Workspace) 생성
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
 
# 카메라 노드 패키지 설치
sudo apt install -y ros-humble-v4l2-camera

카메라 동작 테스트

ros2 run v4l2_camera v4l2_camera_node --ros-args -p video_device:="/dev/video0"

YOLOv8 추론 - Python 버전

python 개발 환경 설치

# YOLOv8 패키지 및 opencv 설치 (로컬 환경 또는 가상환경 에서)
pip install ultralytics
pip install opencv-python

패키지 생성

  • ~/ros2_ws/src 에서 아래 명령어를 통해 패키지 생성 및 의존성 지정
# 패키지 생성 (의존성 미리 지정)
ros2 pkg create --build-type ament_python my_vision_package --dependencies rclpy sensor_msgs cv_bridge

의존성 검사 및 설치

  • 패키지 안의 package.xml에 명시된 의존성 패키지들이 내 시스템에 전부 깔려있는지 rosdep으로 검증한다.
# 워크스페이스 루트로 이동 후 rosdep 실행
cd ~/robot_ws
rosdep update
rosdep install --from-paths src --ignore-src -r -y

YOLOv8 추론 코드 작성 (Python)

  • ~/ros2_ws/src/my_vision_package/my_vision_package/yolo_detector.py 파일 생성
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
import cv2
from ultralytics import YOLO
 
class YoloDetectorNode(Node):
    def __init__(self):
        super().__init__('yolo_detector_node')
        
        # /image_raw 토픽 구독 (v4l2_camera가 발행하는 토픽)
        self.subscription = self.create_subscription(
            Image,
            '/image_raw',
            self.image_callback,
            10
        )
        self.bridge = CvBridge()
        
        # YOLOv8 Model 로드 (PC에 외장 GPU가 있다면 자동으로 CUDA 선택)
        self.get_logger().info('YOLOv8 모델을 로드 중입니다...')
        self.model = YOLO('yolov8n.pt') 
        self.get_logger().info('YOLOv8 로드 완료. 추론을 시작합니다.')
 
    def image_callback(self, msg):
        # ROS Image 메시지를 OpenCV 이미지로 변환
        cv_image = self.bridge.imgmsg_to_cv2(msg, desired_encoding='bgr8')
        
        # YOLOv8 추론 실행
        results = self.model(cv_image, verbose=False)
        
        # 이미지에 결과 시각화
        annotated_frame = results[0].plot()
        
        # 화면 출력
        cv2.imshow("PC Development Env - YOLOv8", annotated_frame)
        cv2.waitKey(1)
 
def main(args=None):
    rclpy.init(args=args)
    node = YoloDetectorNode()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()
        cv2.destroyAllWindows()
 
if __name__ == '__main__':
    main()

빌드 및 반영

  • python 스크립트를 ros2 run 명령어로 실행할 수 있도록 진입점(Entry Point)을 등록한다.
  • ~/robot_ws/src/my_vision_package/setup.py 파일을 열어 entry_points 부분을 다음과 같이 수정한다.
entry_points={
        'console_scripts': [
            'yolo_detector = my_vision_package.yolo_detector:main',
        ],
    },
  • 루트 경로로 이동하여 colcon을 통해 빌드.
cd ~/ros2_ws
colcon build
# --symlink-install 옵션 : 비컴파일(Python 등)코드를 수정해도 다시 빌드할 필요 없음
# colcon build --symlink-install
 
# 빌드가 끝나면 현재 터미널에 워크스페이스 환경 반영
source install/setup.bash

실행 및 테스트

터미널 1 (카메라 노드 실행)

ros2 run v4l2_camera v4l2_camera_node

터미널 2 (YOLOv8 추론 노드 실행)

cd ~/robot_ws
source install/setup.bash
ros2 run my_vision_package yolo_detector


YOLOv8 추론 (onnx) - CPP 버전

개발 환경 설치

  • OpenCV 개발 라이브러리 설치
sudo apt update
# Ubuntu 22.04용 정식 OpenCV C++ 개발 라이브러리 설치
sudo apt install -y libopencv-dev

패키지 생성

  • 기존 워크스페이스의 src 폴더로 이동하여 ament_cmake 빌드 타입을 사용하는 C++ 패키지를 생성한다.
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_cmake my_vision_package_cpp --dependencies rclcpp sensor_msgs cv_bridge

ONNX Runtime 다운로드

  • linux x64 환경에 맞는 onnx runtime 라이브러리 다운로드
wget https://github.com/microsoft/onnxruntime/releases/download/v1.16.3/onnxruntime-linux-x64-1.16.3.tgz
tar -zxvf onnxruntime-linux-x64-1.16.3.tgz
  • 패키지 내부로 라이브러리 폴더 이동
# 3rdparty 폴더 생성
mkdir -p ~/ros2_ws/src/my_vision_package_cpp/3rdparty
# 라이브러리 이동
mv onnxruntime-linux-x64-1.16.3 ~/robot_ws/src/my_vision_package_cpp/3rdparty/onnxruntime

CMakeLists.txt 설정

  • C++ 패키지는 컴파일이 필요하므로 CMakeLists.txt에 OpenCV 라이브러리와 실행 파일 빌드 규칙을 적어주어야 한다.
  • ~/ros2_ws/src/my_vision_package_cpp/CMakeLists.txt 파일 하단에 아래 내용을 추가한다.
# OpenCV 패키지 찾아오기
find_package(OpenCV REQUIRED)
 
# 수동으로 배치한 3rdparty ONNX Runtime 경로 정의
set(ONNXRUNTIME_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/onnxruntime")
set(ONNXRUNTIME_INCLUDE_DIRS "${ONNXRUNTIME_DIR}/include")
set(ONNXRUNTIME_LIBRARIES "${ONNXRUNTIME_DIR}/lib/libonnxruntime.so")
 
###################################
 
# 실행 파일 구성 및 의존성 링크
add_executable(yolo_detector_cpp src/yolo_detector.cpp)
ament_target_dependencies(yolo_detector_cpp
  rclcpp
  sensor_msgs
  cv_bridge
)
target_link_libraries(yolo_detector_cpp ${OpenCV_LIBRARIES})
 
# 실행 파일을 install 디렉토리로 내보내기 (ros2 run 명령 인식용)
install(TARGETS
  yolo_detector_cpp
  DESTINATION lib/${PROJECT_NAME}
)

YOLOv8s 추론 코드 작성 (CPP)

  • YOLOv8 모델의 onnx 버전 추론을 위한 코드 작성. (onnx로export할 때, opset 권장)
  • 이미지 전처리 및 후처리, 토픽 발행 등은 포함하지 않음.
#include <rclcpp/rclcpp.hpp> // ROS2 C++ 클라이언트 라이브러리 (노드 생성, 토픽 발행/구독, 로그 출력)
#include <sensor_msgs/msg/image.hpp> // ROS2 표준 카메라 메시지 타입
#include <cv_bridge/cv_bridge.h> // ROS2 이미지 메시지를 OpenCV Mat으로 변환
#include <opencv2/opencv.hpp>
#include <onnxruntime_cxx_api.h>
#include <vector>
#include <string>
#include <numeric>
#include <algorithm>
 
// 공통 후처리 유틸 헤더 포함
#include "utils/yolo_utils.hpp"
 
// 노드 상속 : 다른 노드들과 통신할 수 있는 자격을 갖게됨
class YoloOrtDetectorNode : public rclcpp::Node {
public:
    // 노드 이름 : yolo_ort_detector_node
    YoloOrtDetectorNode() : Node("yolo_ort_detector_node"), env_(ORT_LOGGING_LEVEL_WARNING, "YOLOv8_ORT") {
        std::string model_path = "./yolov8s.onnx";
        
        // 로그 출력
        RCLCPP_INFO(this->get_logger(), "ONNX Runtime Load Model: %s", model_path.c_str());
 
        // ONNX Runtime : 세션 옵션 설정
        session_options_.SetIntraOpNumThreads(4);
        session_options_.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL);
 
        // ONNX Runtime : 세션 생성
        session_ = std::make_unique<Ort::Session>(env_, model_path.c_str(), session_options_);
        
        // ONNX 모델에서 입력 크기(Shape) 동적 추출 프로세스
        Ort::TypeInfo input_type_info = session_->GetInputTypeInfo(0);
        auto input_tensor_info = input_type_info.GetTensorTypeAndShapeInfo();
        std::vector<int64_t> input_shape = input_tensor_info.GetShape();
 
        input_width_ = (input_shape[3] > 0) ? input_shape[3] : 640; // 기본값 640
        input_height_ = (input_shape[2] > 0) ? input_shape[2] : 640; // 기본값 640
 
        RCLCPP_INFO(this->get_logger(), "ONNX Runtime Loaded");
        RCLCPP_INFO(this->get_logger(), "Model Input Size ── Width: %ld, Height: %ld", input_width_, input_height_);
        
        // 구독자(Subscriber) 생성 ("/image_raw" 토픽을 구독하고, 콜백 함수 image_callback 호출, 버퍼 큐 사이즈: 10)
        // 토픽이 들어왔을 때 실행할 함수(콜백 함수)를 ROS2 엔진에 binding 해줌
        subscription_ = this->create_subscription<sensor_msgs::msg::Image>(
            "/image_raw", 10,
            std::bind(&YoloOrtDetectorNode::image_callback, this, std::placeholders::_1));
    }
 
private:
    // 콜백함수, 인자로 받는 msg는 sensor_msgs::msg::Image 타입의 공유 포인터
    // msg 객체 안에 카메라의 이미지, 촬영 시간, 프레임 ID 등이 있음
    void image_callback(const sensor_msgs::msg::Image::SharedPtr msg) {
        try {
            // 8비트 3채널 BGR 이미지로 변환
            cv::Mat frame = cv_bridge::toCvCopy(msg, "bgr8")->image;
            
            // 딥러닝 입력 전처리: Letterbox 전처리 수행 및 스케일/패딩 정보 획득
            float current_scale = 1.0f;
            int current_pad_x = 0;
            int current_pad_y = 0;
            
            cv::Mat lb_frame = yolo::make_letterbox(frame, cv::Size(input_width_, input_height_), current_scale, current_pad_x, current_pad_y);
            //cv::imshow("Letterbox Preprocessed Image", lb_frame);
 
            // 딥러닝 입력 전처리: 640x640로 리사이즈, 0~1 범위로 정규화
            cv::Mat input_image;
            lb_frame.convertTo(input_image, CV_32FC3, 1.0 / 255.0); 
            std::vector<float> input_tensor_values(1 * 3 * input_width_ * input_height_);
            for (int c = 0; c < 3; ++c) {
                for (int h = 0; h < input_height_; ++h) {
                    for (int w = 0; w < input_width_; ++w) {
                        input_tensor_values[c * input_width_ * input_height_ + h * input_width_ + w] = input_image.at<cv::Vec3f>(h, w)[c];
                    }
                }
            }
            std::vector<int64_t> input_shape = {1, 3, input_height_, input_width_};
            auto memory_info = Ort::MemoryInfo::CreateCpu(OrtAllocatorType::OrtArenaAllocator, OrtMemType::OrtMemTypeDefault);
            
            Ort::Value input_tensor = Ort::Value::CreateTensor<float>(
                memory_info, input_tensor_values.data(), input_tensor_values.size(), input_shape.data(), input_shape.size()
            );
 
            const char* input_names[] = {"images"};
            const char* output_names[] = {"output0"};
 
            auto output_tensors = session_->Run(
                Ort::RunOptions{nullptr}, input_names, &input_tensor, 1, output_names, 1
            );
 
            float* output_data = output_tensors[0].GetTensorMutableData<float>();
            auto output_shape = output_tensors[0].GetTensorTypeAndShapeInfo().GetShape();
 
            // 딥러닝 출력 후처리: 출력 데이터 정리
            std::vector<cv::Rect> local_boxes;
            std::vector<float> local_confidences;
            std::vector<int> local_class_ids;
            yolo::parse_raw_output(output_data, output_shape, local_boxes, local_confidences, local_class_ids);
 
            std::vector<int> final_indices = yolo::NMS(local_boxes, local_confidences, 0.6f, 0.7f);
 
            for (int idx : final_indices) {
                // 딥러닝 출력 후처리: Letterbox 좌표를 원본 이미지 좌표로 역산
                cv::Rect restored_box = yolo::restore_coords(local_boxes[idx], current_scale, current_pad_x, current_pad_y, frame.cols, frame.rows);
 
                // (Renderer) OpenCV 이미지 렌더링 드로잉
                cv::rectangle(frame, restored_box, cv::Scalar(0, 255, 0), 2);
                std::string label = "ID " + std::to_string(local_class_ids[idx]) + ": " + std::to_string(local_confidences[idx]).substr(0, 4);
                cv::putText(frame, label, cv::Point(restored_box.x, restored_box.y - 5), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 255, 0), 1);
            }
 
            cv::imshow("ONNX Runtime YOLO Inference", frame);
            cv::waitKey(1);
 
        } catch (const std::exception& e) {
            RCLCPP_ERROR(this->get_logger(), "추론 루프 예외 발생: %s", e.what());
        }
    }
    
    // 구독자 멤버 변수 선언
    rclcpp::Subscription<sensor_msgs::msg::Image>::SharedPtr subscription_;
 
    Ort::Env env_;
    Ort::SessionOptions session_options_;
    std::unique_ptr<Ort::Session> session_;
 
    // 크기 멤버 변수
    int64_t input_width_;
    int64_t input_height_;
};
 
int main(int argc, char** argv) {
    // ROS2 통신 시작 (DDS 통신 백그라운드 세팅)
    rclcpp::init(argc, argv);
    // 노드 클래스를 메모리 인스턴스(스마트 포인터)로 할당함
    auto node = std::make_shared<YoloOrtDetectorNode>();
    // 노드를 무한 루프 돌면서 DDS 통신을 통해 토픽을 구독하고 콜백함수 실행
    rclcpp::spin(node);
    // 노드가 종료될 때 안전하게 ROS2 통신 종료
    rclcpp::shutdown();
    return 0;
}

빌드 및 반영

cd ~/ros2_ws
# 새롭게 생성한 C++ 패키지만 타겟팅하여 빌드 가능.
colcon build --packages-select my_vision_package_cpp
 
# 빌드 환경 반영
source install/setup.bash

실행 및 테스트

터미널 1 (카메라 노드 실행)

ros2 run v4l2_camera v4l2_camera_node

터미널 2 (YOLOv8 추론 노드 실행)

cd ~/ros2_ws
source install/setup.bash
ros2 run my_vision_package_cpp yolo_detector_cpp