Tích hợp ROS2 + YOLO: đóng gói nhận diện vật thể thành node
Atlasbot · 18/8/2026 · 12 phút đọc
Chạy được YOLO độc lập trên một file ảnh/video là bước khởi đầu (đã học ở bài YOLO là gì?) — nhưng để dùng thật trong robot, kết quả nhận diện cần trở thành một phần của hệ thống ROS2: đọc ảnh trực tiếp từ camera node, và publish kết quả ra topic để các node khác (Nav2 né vật cản động, node giám sát...) sử dụng được.
Khái niệm chính
camera node (v4l2_camera) → topic /image_raw
│
▼
node YOLO detection (mới viết)
│
▼
topic /detections (danh sách bounding box + nhãn)
Node YOLO đóng vai trò vừa là subscriber (nhận ảnh từ camera) vừa là publisher (gửi kết quả detection đi) — mẫu thiết kế phổ biến cho mọi node "xử lý rồi chuyển tiếp" trong ROS2, đúng vai trò một node trong ROS2 Layer đã học ở bài ROS2 Layer trong AMR.
Nguyên lý hoạt động
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
from ultralytics import YOLO
class YoloDetectionNode(Node):
def __init__(self):
super().__init__("yolo_detection_node")
self.model = YOLO("yolov8n.pt")
self.bridge = CvBridge()
self.subscription = self.create_subscription(
Image, "/image_raw", self.image_callback, 10
)
# Thực tế nên publish sensor_msgs/Image (đã vẽ box) hoặc
# vision_msgs/Detection2DArray (dữ liệu có cấu trúc) thay vì tự định nghĩa
self.publisher = self.create_publisher(Image, "/detections/image", 10)
def image_callback(self, msg):
frame = self.bridge.imgmsg_to_cv2(msg, desired_encoding="bgr8")
results = self.model(frame, verbose=False)
annotated = results[0].plot() # vẽ sẵn bounding box lên ảnh
out_msg = self.bridge.cv2_to_imgmsg(annotated, encoding="bgr8")
self.publisher.publish(out_msg)
def main():
rclpy.init()
node = YoloDetectionNode()
rclpy.spin(node)
cv_bridge là gói chuyển đổi qua lại giữa ảnh dạng OpenCV (numpy array) và message ROS2 (sensor_msgs/Image) — cầu nối bắt buộc phải có mỗi khi xử lý ảnh ROS2 bằng OpenCV/mô hình AI, đúng bước tiền xử lý đã học ở bài OpenCV cơ bản.
Kiến thức kỹ thuật
Công thức
Chạy inference AI trên mỗi khung hình camera (thường 30 khung hình/giây) có thể vượt quá khả năng xử lý của máy tính nhúng, gây tắc nghẽn (backlog) callback — ước lượng số khung hình bị bỏ lỡ:
Nếu Thời_gian_inference > Chu_kỳ_khung_hình_camera:
Số_khung_hình_tồn_đọng_mỗi_giây ≈ FPS_camera − (1/Thời_gian_inference)
VD: camera 30fps (chu kỳ 33ms), inference mất 100ms/khung hình (CPU thuần)
FPS_xử_lý_được ≈ 1/0.1 = 10fps
Số_khung_hình_tồn_đọng ≈ 30 − 10 = 20 khung hình/giây bị dồn lại hoặc bỏ qua
→ Cần giới hạn tần số xử lý hoặc dùng QoS depth=1 để luôn lấy khung hình
mới nhất thay vì xử lý tuần tự các khung hình cũ đã lỗi thời
Thông số
| Cách xử lý | Ưu điểm | Nhược điểm |
|---|---|---|
| Xử lý mọi khung hình (không giới hạn) | Không bỏ sót dữ liệu | Dễ tắc nghẽn nếu inference chậm hơn camera |
| Giới hạn tần số (VD: mỗi 3-5 khung hình) | Giảm tải tính toán | Bỏ qua một số khung hình giữa các lần xử lý |
| QoS depth=1 | Luôn xử lý khung hình mới nhất | Bỏ qua khung hình cũ hoàn toàn, không xử lý tuần tự |
Ứng dụng thực tế
Cách xử lý phổ biến trong thực tế: giới hạn tần số xử lý (VD: chỉ chạy YOLO mỗi 3-5 khung hình thay vì mọi khung hình), hoặc dùng qos_profile với depth=1 để tự động bỏ qua khung hình cũ khi node xử lý không kịp, ưu tiên luôn xử lý khung hình mới nhất — đúng nguyên tắc QoS cho dữ liệu cảm biến tần số cao đã học ở bài ROS2 Layer trong AMR (ưu tiên dữ liệu mới nhất hơn đảm bảo nhận đủ mọi message cũ).
Ví dụ thực tế
class YoloDetectionNode(Node):
def __init__(self):
super().__init__("yolo_detection_node")
self.model = YOLO("yolov8n.pt")
self.bridge = CvBridge()
self.frame_count = 0
self.SKIP_FRAMES = 3 # chỉ xử lý 1/3 khung hình
self.subscription = self.create_subscription(
Image, "/image_raw", self.image_callback, 10
)
self.publisher = self.create_publisher(Image, "/detections/image", 10)
def image_callback(self, msg):
self.frame_count += 1
if self.frame_count % self.SKIP_FRAMES != 0:
return # bo qua khung hinh nay, giam tai tinh toan
frame = self.bridge.imgmsg_to_cv2(msg, desired_encoding="bgr8")
results = self.model(frame, verbose=False)
annotated = results[0].plot()
out_msg = self.bridge.cv2_to_imgmsg(annotated, encoding="bgr8")
self.publisher.publish(out_msg)
Với camera 30fps và SKIP_FRAMES=3:
Tần số xử lý thực tế ≈ 30/3 = 10fps
Đủ cho nhiều ứng dụng phát hiện vật cản (không cần phản ứng ở 30fps),
trong khi giảm 3 lần tải tính toán so với xử lý mọi khung hình
Lỗi thường gặp
- Xử lý mọi khung hình trên phần cứng không đủ nhanh — như tính ở mục Công thức, gây tồn đọng callback, độ trễ tăng dần theo thời gian thay vì ổn định.
- Không dùng
cv_bridgemà tự viết chuyển đổi ảnh — dễ sai định dạng encoding (BGR/RGB, đã học ở bài OpenCV cơ bản),cv_bridgeđã xử lý đúng các trường hợp phổ biến. - Publish kết quả detection dưới dạng ảnh đã vẽ box thay vì dữ liệu có cấu trúc — hạn chế khả năng các node khác (Nav2, logic ứng dụng) sử dụng kết quả detection theo chương trình; nên cân nhắc
vision_msgs/Detection2DArraycho dữ liệu có cấu trúc dùng được trực tiếp. - Không giới hạn tần số xử lý khi phần cứng không đủ mạnh — dẫn tới hiện tượng đã tính ở mục Công thức, độ trễ ngày càng tăng, ảnh hưởng tới cả các node khác chờ dữ liệu từ node YOLO.
Kết luận
Việc tích hợp YOLO vào ROS2 về bản chất chỉ là một node subscriber/publisher thông thường, với cv_bridge làm cầu nối giữa dữ liệu ảnh OpenCV và message ROS2. Điểm cần lưu ý nhất khi triển khai thực tế là quản lý hiệu năng — đảm bảo pipeline AI không làm nghẽn các node khác đang cần dữ liệu thời gian thực, bằng cách giới hạn tần số xử lý hoặc dùng QoS phù hợp. Đây là bài cuối của chuyên mục Sensor & Perception — từ LiDAR, Camera, IMU, tới Computer Vision/AI, toàn bộ các cảm biến nền tảng của một AMR đã được trình bày đầy đủ, sẵn sàng ghép vào kiến trúc phần cứng và phần mềm đã học ở chuyên mục AMR/AGV.