테스트 환경
- OS: Ubuntu 22.04 LTS
- ROS2 Version: Humble Hawksbill
- 설치 참고: (ROS) Ubuntu에서 ROS2 설치
노드 및 토픽 구조
-ROS2---YOLO-패키지-개발_image_1.png)
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 -yYOLOv8 추론 코드 작성 (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_detectorYOLOv8 추론 (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_bridgeONNX 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/onnxruntimeCMakeLists.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