影像處理:image_transport 與 cv_bridge
- ros2
- cv-bridge
- image-transport
- opencv
- perception
問題定義
第 5.3 節示範了怎麼在 Gazebo 裡模擬雷射雷達,相機是另一種常見感測器,但影像資料的處理牽涉到一個額外的轉換問題:ROS2 的 sensor_msgs/msg/Image 是通用、跨語言的訊息格式,而實際做影像處理的演算法(無論是傳統電腦視覺還是後面要介紹的深度學習模型)幾乎都是基於 OpenCV 的陣列格式運作。這一節要處理的就是這兩者之間的轉換,以及在頻寬有限的情況下怎麼有效率地傳輸影像資料。
核心概念說明
cv_bridge:ROS2 訊息格式與 OpenCV 格式之間的橋樑
sensor_msgs/msg/Image 把影像存成一維攤平的位元組陣列,搭配 width、height、encoding(顏色格式字串,例如 bgr8、rgb8、mono8)等欄位描述怎麼解讀這些位元組。cv_bridge 這個套件負責把這種格式,轉換成 OpenCV(Python 裡就是 numpy 陣列)習慣操作的格式,反之亦然:
from cv_bridge import CvBridge
bridge = CvBridge()
cv_image = bridge.imgmsg_to_cv2(ros_image_msg, desired_encoding="bgr8")
留意 desired_encoding 參數——如果來源影像實際編碼是 rgb8,但你要求轉換成 bgr8,cv_bridge 會自動幫你做顏色通道順序的轉換,不是單純的位元組複製。這也是為什麼理解 encoding 欄位的實際意義很重要,直接關係到下面會提到的一個常見錯誤。
image_transport:不只是傳輸,還處理壓縮與多格式發布
直接用一般的 publisher/subscriber 傳輸未壓縮的原始影像,在頻寬有限的環境(例如透過 Wi-Fi 連線的機器人)下可能會造成明顯延遲。image_transport 套件提供一層抽象,讓影像可以用多種傳輸方式(原始、JPEG 壓縮、PNG 壓縮等)發布,同時對外表現為同一個邏輯上的影像來源,訂閱端可以按自己的需求選擇要訂閱哪一種實際格式,不需要發布端額外寫程式碼支援每一種壓縮方式。
實作範例:訂閱模擬相機、做簡單影像處理、重新發布結果
延續第 5 章的 Gazebo 模擬架構,這裡假設已經有一個橋接好的相機主題 /camera/image_raw(設定方式與第 5.3 節的雷射雷達插件橋接概念一致,只是換成相機感測器插件)。
檔案位置:~/ros2_ws/src/my_package/my_package/image_processor.py
import cv2
import numpy as np
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
class ImageProcessor(Node):
def __init__(self):
super().__init__("image_processor")
self.bridge = CvBridge()
self.subscription = self.create_subscription(
Image, "/camera/image_raw", self.on_image, 10
)
self.publisher = self.create_publisher(Image, "/camera/edges", 10)
def on_image(self, msg: Image):
try:
cv_image = self.bridge.imgmsg_to_cv2(msg, desired_encoding="bgr8")
except Exception as e:
self.get_logger().error(f"影像轉換失敗: {e}")
return
gray = cv2.cvtColor(cv_image, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, 100, 200)
edges_bgr = cv2.cvtColor(edges, cv2.COLOR_GRAY2BGR)
out_msg = self.bridge.cv2_to_imgmsg(edges_bgr, encoding="bgr8")
out_msg.header = msg.header
self.publisher.publish(out_msg)
def main():
rclpy.init()
node = ImageProcessor()
try:
rclpy.spin(node)
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == "__main__":
main()
留意 out_msg.header = msg.header——把輸出訊息的時間戳記與座標系,沿用輸入影像原本的 header,而不是用處理完成當下的新時間戳記。這在下游需要做時間同步(例如比對這張處理結果對應到哪個時刻的 TF 姿態)時很重要,如果每次都蓋成新的時間戳記,會讓輸出結果失去跟原始擷取時刻的關聯。
執行並確認結果
ros2 run my_package image_processor
另開終端機用 rqt_image_view(rqt 的一個 Display 外掛,呼應第 3.1 節介紹的 rqt 外掛概念)檢視結果:
ros2 run rqt_image_view rqt_image_view
預期畫面
在 rqt_image_view 的下拉選單選擇 /camera/edges,畫面會顯示原始相機影像經過邊緣偵測處理後的黑白線稿,物體的輪廓會被標示成白色線條,其餘區域是黑色。
常見錯誤與除錯技巧
錯誤一:影像顯示出來,但顏色明顯不對(紅藍色調對調)
現象:畫面上原本應該是藍色的物體,變成了橘紅色,反之亦然。
原因:這是最典型的 encoding 誤用——OpenCV 內部慣例用 BGR 通道順序,但很多相機驅動或其他工具鏈預設用 RGB。如果 imgmsg_to_cv2 呼叫時 desired_encoding 沒有正確反映實際想要的格式,或是後續 cv2_to_imgmsg 呼叫時 encoding 參數跟實際陣列的通道順序不一致,就會出現色彩對調的顯示異常。
排除方式:明確在轉換時指定正確的 desired_encoding/encoding,並且用一個已知顏色的物體(例如場景裡放一個純紅色方塊)測試,肉眼直接確認轉換結果的色彩是否正確,而不是依賴預設值蒙混過去:
cv_image = self.bridge.imgmsg_to_cv2(msg, desired_encoding="bgr8")
錯誤二:訂閱端的 callback 執行時間過長,導致影像處理明顯延遲、畫面卡頓
現象:rqt_image_view 裡的畫面更新速度明顯跟不上相機實際的發布頻率,出現肉眼可見的延遲與跳格。
原因:影像處理(尤其是牽涉到深度學習模型推論,下一節會介紹)往往比一般的資料處理耗時得多,如果這個運算量大的處理邏輯直接寫在 subscription 的 callback 裡,且節點用的是第 2.1 節介紹過的 SingleThreadedExecutor,處理一張影像的時間如果超過影像發布間隔,新進來的影像訊息會在 callback 還沒執行完之前持續堆積,造成延遲越來越大。
排除方式:確認影像的 QoS 設定使用適合即時資料流的 profile(例如較淺的 depth 搭配 BEST_EFFORT,讓來不及處理的舊訊息直接被捨棄,而不是無限堆積),這呼應第 2.2 節介紹過的 QoS 概念;如果處理邏輯本身確實需要優化,考慮是否能降低訂閱端處理的頻率(例如每兩張只處理一張),或用第 2.1 節介紹的多執行緒 executor 搭配獨立的 callback group,避免影像處理阻塞到其他更即時性的 callback。
小結
cv_bridge 負責 ROS2 影像訊息與 OpenCV 慣用格式之間的轉換,encoding 欄位的正確性直接決定色彩顯示是否正確;image_transport 讓影像可以用壓縮格式高效傳輸,適應頻寬受限的場景。影像處理往往比一般資料處理耗時,QoS 設定與 executor 選擇對維持即時性很關鍵。下一節要處理另一種常見的感知資料型態:由深度相機或光達產生的三維點雲。
延伸閱讀
常見問題
- 可以直接訂閱 sensor_msgs/Image 然後自己手動解析 data 欄位嗎,不用 cv_bridge?
- 技術上可以,但沒有必要且容易出錯——sensor_msgs/Image 的 data 是攤平成一維陣列的原始位元組,手動解析需要自己處理 step(每列的位元組數,可能包含記憶體對齊的填充位元組)、encoding(顏色格式)等細節,容易在邊界情況出錯。cv_bridge 已經把這些轉換邏輯封裝好,除非有特殊效能考量,直接用 cv_bridge 轉換成 OpenCV 熟悉的 numpy 陣列格式是標準做法。
- image_transport 的壓縮傳輸,接收端收到的還是原始畫質嗎?
- 不是,壓縮傳輸(例如常用的 JPEG 壓縮)本質上是有損壓縮,會犧牲一些畫質換取頻寬與延遲的降低。如果下游演算法對畫質細節敏感(例如需要精確的像素級量測),應該評估壓縮是否會影響結果;如果只是用來做人眼確認或畫面粗略判斷,壓縮傳輸帶來的效能提升通常值得這個畫質犧牲。
- 為什麼有些相機驅動發布的是 sensor_msgs/CompressedImage,不是 sensor_msgs/Image?
- 這是 image_transport 機制底層實際發布的其中一種格式——驅動節點透過 image_transport 發布『原始』影像時,image_transport 會同時建立多個對應的主題(例如 /image_raw 本身,以及 /image_raw/compressed),讓訂閱端可以按需求選擇要訂閱原始格式還是壓縮格式,不需要驅動節點自己額外實作壓縮邏輯。