ROS2 整合 YOLO 物體偵測
- ros2
- yolo
- object-detection
- deep-learning
- perception
問題定義
上一節的邊緣偵測只能找出物體的輪廓線,沒辦法回答「這是什麼東西」。YOLO(You Only Look Once)系列模型是即時物體偵測領域的常見選擇,能在一張影像裡同時標出多個物體的邊界框與類別。這一節要處理的核心問題,不是 YOLO 模型本身怎麼訓練或推論(這部分屬於深度學習框架的範疇),而是「怎麼把一個現成的深度學習模型,包裝成能跟 ROS2 系統其他部分自然溝通的節點」——這是感知與 AI 整合章節真正的重點。
核心概念說明
把模型包裝成節點:輸入輸出都是標準 ROS2 訊息
整合深度學習模型到 ROS2 系統的核心模式,是把「模型推論」包裝在一個節點的 callback 裡:訂閱影像主題(用上一節介紹的 cv_bridge 轉換成 OpenCV 格式),餵給模型推論,把推論結果轉換成 ROS2 標準訊息型別再發布出去。這個模式的價值在於——一旦包裝完成,下游任何節點都可以像消費普通感測器資料一樣訂閱偵測結果,不需要知道底層跑的是哪個深度學習框架、模型架構是什麼。
vision_msgs 套件提供了一組專門用來表達偵測結果的標準訊息型別,最常用的是 Detection2DArray(一張影像裡多個 2D 偵測結果的陣列),每個 Detection2D 包含邊界框位置、類別標籤、信心分數。用標準訊息型別而不是自訂格式的好處,是能跟其他官方或社群套件(例如視覺化工具、追蹤演算法)直接相容。
推論延遲跟影像佇列堆積:即時系統的核心挑戰
延續上一節提過的「callback 執行時間過長導致堆積」的問題,YOLO 推論的耗時通常比單純的邊緣偵測高出一到兩個數量級,這讓佇列堆積問題變得更加突出:如果訂閱端的 QoS depth 設定較深、又用了會保留所有訊息的 QoS 策略,來不及處理的影像會持續堆積在佇列裡,導致系統看到的偵測結果,其實是好幾秒前的舊畫面,而不是「當下」的偵測結果,這對需要即時反應的應用(例如配合手臂抓取,第 7 章提到的抓取姿態依賴當下的偵測結果)是嚴重問題。
實作範例:把 YOLO 包裝成偵測節點
檔案位置:~/ros2_ws/src/my_package/my_package/yolo_detector_node.py
import cv2
import rclpy
from rclpy.node import Node
from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy
from sensor_msgs.msg import Image
from vision_msgs.msg import Detection2DArray, Detection2D, ObjectHypothesisWithPose
from cv_bridge import CvBridge
from ultralytics import YOLO
class YoloDetectorNode(Node):
def __init__(self):
super().__init__("yolo_detector_node")
self.bridge = CvBridge()
self.model = YOLO("yolov8n.pt")
image_qos = QoSProfile(
reliability=ReliabilityPolicy.BEST_EFFORT,
history=HistoryPolicy.KEEP_LAST,
depth=1,
)
self.subscription = self.create_subscription(
Image, "/camera/image_raw", self.on_image, image_qos
)
self.publisher = self.create_publisher(Detection2DArray, "/detections", 10)
self.processing = False
def on_image(self, msg: Image):
if self.processing:
return
self.processing = True
try:
cv_image = self.bridge.imgmsg_to_cv2(msg, desired_encoding="bgr8")
results = self.model(cv_image, verbose=False)[0]
detection_array = Detection2DArray()
detection_array.header = msg.header
for box in results.boxes:
detection = Detection2D()
x1, y1, x2, y2 = box.xyxy[0].tolist()
detection.bbox.center.position.x = (x1 + x2) / 2
detection.bbox.center.position.y = (y1 + y2) / 2
detection.bbox.size_x = x2 - x1
detection.bbox.size_y = y2 - y1
hypothesis = ObjectHypothesisWithPose()
hypothesis.hypothesis.class_id = self.model.names[int(box.cls[0])]
hypothesis.hypothesis.score = float(box.conf[0])
detection.results.append(hypothesis)
detection_array.detections.append(detection)
self.publisher.publish(detection_array)
finally:
self.processing = False
def main():
rclpy.init()
node = YoloDetectorNode()
try:
rclpy.spin(node)
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == "__main__":
main()
這段程式碼有兩個地方直接對應前面提到的核心問題:
image_qos用BEST_EFFORT+depth=1:呼應第 2.2 節介紹的 QoS 概念——只保留最新一張影像,來不及處理的舊影像直接被捨棄,而不是排隊等待,確保推論永遠針對「最近可取得」的畫面,而不是累積的舊資料。self.processing旗標:這是一個簡化版的忙碌判斷機制——如果上一張影像還在推論中,新進來的影像直接跳過,避免多個推論同時搶佔 GPU 資源導致互相拖慢,這在單一 GPU、單一模型實例的場景下是常見的簡單防護,更嚴謹的做法會用獨立執行緒搭配佇列管理,但這裡先用最直接的方式示範核心概念。
執行並確認結果
ros2 run my_package yolo_detector_node
ros2 topic echo /detections --once
預期輸出
header:
frame_id: camera_link
detections:
- bbox:
center:
position: {x: 320.5, y: 210.3}
size_x: 84.0
size_y: 96.0
results:
- hypothesis:
class_id: 'bottle'
score: 0.87
常見錯誤與除錯技巧
錯誤一:/detections 主題的發布頻率遠低於影像本身的發布頻率,且持續變慢
現象:影像以 30Hz 發布,但 /detections 只有零星幾個結果,而且系統跑越久延遲越明顯。
原因:如果本文範例裡的 self.processing 忙碌判斷邏輯沒有正確生效(例如漏掉 finally 區塊、或改成多執行緒 executor 卻沒有做好互斥保護),舊的推論還沒結束,新的影像又進來排隊等待,會造成典型的佇列堆積問題,而不是簡單的忙碌跳過。
排除方式:確認忙碌判斷邏輯確實生效,用 ros2 topic hz /detections 觀察實際的偵測頻率是否穩定(而不是持續下降):
ros2 topic hz /detections
average rate: 4.812
如果頻率穩定但偏低(例如本例的 4.8Hz),代表系統確實是「忠實地按照模型推論速度」在運作,沒有堆積問題,只是硬體效能限制了頻率上限——這種情況下要考慮換更輕量的模型、降低輸入解析度,或評估是否有 GPU 可用。
錯誤二:偵測結果的類別看起來合理,但邊界框位置或大小完全不對
原因:常見是輸入模型的影像跟原始影像的解析度或長寬比不一致(例如模型內部會自動 resize 輸入影像做推論,但輸出的邊界框座標是相對於 resize 後的尺寸,如果沒有正確轉換回原始影像座標系,位置就會跑掉)。
排除方式:確認使用的推論函式庫版本,其輸出的邊界框座標是否已經自動轉換回原始輸入影像的座標系(多數現代 YOLO 封裝函式庫,例如本文範例用的 ultralytics,預設會自動處理這個轉換),如果懷疑轉換有問題,可以用 cv2.rectangle 直接把偵測結果畫回原始影像上肉眼確認,而不是只看數字是否「看起來合理」。
小結
整合深度學習模型到 ROS2 系統的核心模式,是把推論邏輯包裝在節點的訂閱 callback 裡,用標準訊息型別(如 vision_msgs/Detection2DArray)發布結果,讓下游模組不需要知道底層模型細節。QoS 設定與忙碌判斷機制是避免佇列堆積、確保系統維持即時性的關鍵。到這裡,第 8 章感知與 AI 整合支線(影像處理、點雲處理、深度學習物體偵測)已經涵蓋了感知系統從原始感測資料到語意理解的完整流程——這條支線跟第 7 章的抓取任務、第 6 章的動態避障,都是可以進一步組合串接的方向,取決於你的專案實際需求。
延伸閱讀
常見問題
- YOLO 推論一定要在 GPU 上跑嗎?
- 不一定,但強烈建議——YOLO 這類即時物體偵測模型雖然設計上已經比很多深度學習模型輕量,純 CPU 推論的延遲通常還是遠高於 GPU(可能從幾毫秒暴增到數百毫秒),如果應用場景需要接近即時的偵測頻率(例如配合第 6 章的避障邏輯),CPU 推論的延遲很可能成為系統瓶頸。若目標平台沒有 GPU(例如某些邊緣運算板),需要額外評估模型輕量化(例如換更小的模型版本)或降低輸入解析度來換取可接受的延遲。
- 偵測到的 2D 邊界框,要怎麼變成三維空間裡的實際位置?
- 純粹的 2D 影像偵測只能告訴你物體在畫面上的哪個像素範圍,不包含深度資訊。常見做法是搭配深度相機——用邊界框範圍去查詢對應的深度資料(第 8.2 節介紹的點雲,或深度相機額外提供的對齊深度圖),取得該範圍內的距離資訊,再透過相機的內參數(焦距、主點)反投影回三維空間座標,這也是第 7.3 節提到『抓取姿態怎麼決定』這個問題實務上常見的其中一種解法路徑。
- 每一幀影像都要重新做一次完整推論嗎?
- 不一定,取決於應用需求對即時性與準確度的權衡。如果物體移動不快、系統對延遲不敏感,可以只在感興趣的頻率下做偵測(例如每秒 5 次),中間的影格可以用較輕量的追蹤演算法補間;如果每幀都要有最新結果,就需要確保推論速度真的能跟上影像發布頻率,否則會發生本節會詳細討論的佇列堆積問題。