在 ROS2 中处理相机图像涉及一整条数据链路:从传感器采集的原始像素,经过编码、传输、解压、畸变校正、色彩转换,最终送到感知算法。每一步都有性能陷阱和工程细节。本文以 ROS2 Humble 为基准,完整剖析这条链路上的每个关键环节。
一、sensor_msgs/Image:图像数据的标准容器 1.1 消息结构 ROS2 中所有图像数据都通过 sensor_msgs/msg/Image 消息传输:
1 2 3 4 5 6 7 8 9 std_msgs/Header header # 时间戳 + 坐标系 builtin_interfaces/Time stamp string frame_id # 相机光学坐标系(z 朝前、x 朝右、y 朝下) uint32 height # 图像高度(行数) uint32 width # 图像宽度(列数) string encoding # 像素编码格式 uint8 is_bigendian # 字节序(通常为 0) uint32 step # 每行字节数 = width × 每像素字节数 uint8[] data # 原始像素数据
一帧 1920×1080 的 bgr8 图像,data 字段大小为 1920 × 1080 × 3 = 6,220,800 字节(约 5.93 MB) 。这就是为什么图像传输的带宽优化如此重要。
1.2 编码格式速查
编码
通道数
位深
说明
典型场景
rgb8
3
8
R-G-B 顺序
渲染、显示
bgr8
3
8
B-G-R 顺序
OpenCV 默认格式
rgba8
4
8
含 Alpha 通道
合成、叠加
mono8
1
8
8 位灰度
特征检测、光流
mono16
1
16
16 位灰度
高动态范围灰度
16UC1
1
16
16 位无符号整数
深度图(毫米)
32FC1
1
32
32 位浮点
深度图(米)
bayer_rggb8
1
8
Bayer RGGB 模式
相机原始数据
bayer_bggr8
1
8
Bayer BGGR 模式
相机原始数据
yuv422
-
8
YUV 4:2:2
视频流
nv12
-
8
YUV 4:2:0 半平面
硬件编解码
1.3 编码选择指南
flowchart TD
A{需要深度学习推理?} -->|是| B["rgb8(大多数模型训练用 RGB)"]
A -->|否| C{需要 OpenCV 处理?}
C -->|是| D["bgr8(避免额外转换)"]
C -->|否| E{深度相机?}
E -->|是| F["16UC1(毫米)或 32FC1(米)"]
E -->|否| G{来自 Bayer 传感器?}
G -->|是| H["bayer_*(让 image_proc 去拜耳化)"]
G -->|否| I["mono8(灰度足够时减少带宽)"]
1.4 CompressedImage 消息 除了原始图像,ROS2 还提供压缩格式:
1 2 3 std_msgs/Header header string format # "jpeg" / "png" / "tiff" uint8[] data # 压缩后的字节流
一帧 1080p JPEG(quality=80)通常只有 100-300 KB ,比原始 bgr8 小 20-60 倍。
二、CameraInfo:相机的”身份证” 2.1 消息结构 每个相机除了发布图像,还应该同步发布 sensor_msgs/msg/CameraInfo:
1 2 3 4 5 6 7 8 9 10 11 12 13 std_msgs/Header header uint32 height uint32 width string distortion_model # "plumb_bob"(针孔)/ "equidistant"(鱼眼) float64[] d # 畸变系数 float64[9] k # 3×3 内参矩阵(行优先) float64[9] r # 3×3 矫正矩阵 float64[12] p # 3×4 投影矩阵 uint32 binning_x # 像素合并 uint32 binning_y sensor_msgs/RegionOfInterest roi
2.2 内参矩阵 K K = [ f x 0 c x 0 f y c y 0 0 1 ] K = \begin{bmatrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{bmatrix} K = f x 0 0 0 f y 0 c x c y 1
f x , f y f_x, f_y f x , f y :焦距(像素单位)
c x , c y c_x, c_y c x , c y :主点坐标(通常接近图像中心)
2.3 畸变模型 plumb_bob (针孔相机,5 参数):
D = [ k 1 , k 2 , p 1 , p 2 , k 3 ] D = [k_1, k_2, p_1, p_2, k_3] D = [ k 1 , k 2 , p 1 , p 2 , k 3 ]
k 1 , k 2 , k 3 k_1, k_2, k_3 k 1 , k 2 , k 3 :径向畸变系数
p 1 , p 2 p_1, p_2 p 1 , p 2 :切向畸变系数
equidistant (鱼眼相机,4 参数):
D = [ k 1 , k 2 , k 3 , k 4 ] D = [k_1, k_2, k_3, k_4] D = [ k 1 , k 2 , k 3 , k 4 ]
2.4 YAML 标定文件格式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 image_width: 1920 image_height: 1080 camera_name: front_camera camera_matrix: rows: 3 cols: 3 data: [1396.34 , 0.0 , 960.12 , 0.0 , 1396.34 , 540.08 , 0.0 , 0.0 , 1.0 ] distortion_model: plumb_bob distortion_coefficients: rows: 1 cols: 5 data: [-0.1728 , 0.0268 , -0.0003 , 0.0001 , 0.0 ] rectification_matrix: rows: 3 cols: 3 data: [1.0 , 0.0 , 0.0 , 0.0 , 1.0 , 0.0 , 0.0 , 0.0 , 1.0 ] projection_matrix: rows: 3 cols: 4 data: [1396.34 , 0.0 , 960.12 , 0.0 , 0.0 , 1396.34 , 540.08 , 0.0 , 0.0 , 0.0 , 1.0 , 0.0 ]
三、相机标定 3.1 标定流程 1 2 3 4 5 6 7 8 9 sudo apt install ros-humble-camera-calibrationros2 run camera_calibration cameracalibrator \ --size 8x6 \ --square 0.108 \ image:=/camera/image_raw \ camera:=/camera
标定界面中需要在不同位置、角度、距离下移动棋盘格,直到 X、Y、Size、Skew 四个指标的进度条都变为绿色。
3.2 鱼眼相机标定 1 2 3 4 5 6 ros2 run camera_calibration cameracalibrator \ --size 8x6 \ --square 0.108 \ --fisheye-k-coefficients=4 \ image:=/fisheye_camera/image_raw \ camera:=/fisheye_camera
注意:标定后 YAML 文件中应使用 distortion_model: equidistant(不是 fisheye),否则 image_proc 无法正确矫正。
3.3 加载标定文件 1 2 3 ros2 run camera_driver camera_node \ --ros-args -p camera_info_url:=file:///home/user/calibration.yaml
四、cv_bridge:ROS2 ↔ OpenCV 桥梁 4.1 核心概念 cv_bridge 负责 sensor_msgs/msg/Image 和 cv::Mat 之间的转换。有两种模式:
方法
行为
适用场景
toCvCopy()
深拷贝,返回独立副本
需要修改图像
toCvShare()
共享内存,零拷贝
只读访问(性能优先)
4.2 C++ 完整示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include <rclcpp/rclcpp.hpp> #include <sensor_msgs/msg/image.hpp> #include <cv_bridge/cv_bridge.h> #include <opencv2/opencv.hpp> class ImageProcessor : public rclcpp::Node{ public : ImageProcessor () : Node ("image_processor" ) { sub_ = create_subscription <sensor_msgs::msg::Image>( "/camera/image_raw" , rclcpp::SensorDataQoS (), process (msg); }); pub_ = create_publisher <sensor_msgs::msg::Image>( "/camera/image_processed" , 10 ); } private : void process (sensor_msgs::msg::Image::ConstSharedPtr msg) { cv_bridge::CvImagePtr cv_ptr; try { cv_ptr = cv_bridge::toCvCopy (msg, "bgr8" ); } catch (cv_bridge::Exception & e) { RCLCPP_ERROR (get_logger (), "cv_bridge: %s" , e.what ()); return ; } cv::Mat & img = cv_ptr->image; cv::GaussianBlur (img, img, cv::Size (5 , 5 ), 1.5 ); cv::Canny (img, img, 50 , 150 ); pub_->publish (*cv_ptr->toImageMsg ()); } void read_only_process (sensor_msgs::msg::Image::ConstSharedPtr msg) { cv_bridge::CvImageConstPtr cv_ptr = cv_bridge::toCvShare (msg, "bgr8" ); const cv::Mat & img = cv_ptr->image; double brightness = cv::mean (img)[0 ]; RCLCPP_INFO (get_logger (), "Mean brightness: %.1f" , brightness); } rclcpp::Subscription<sensor_msgs::msg::Image>::SharedPtr sub_; rclcpp::Publisher<sensor_msgs::msg::Image>::SharedPtr pub_; };
4.3 Python 完整示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import rclpyfrom rclpy.node import Nodefrom sensor_msgs.msg import Imagefrom cv_bridge import CvBridgeimport cv2class ImageProcessor (Node ): def __init__ (self ): super ().__init__('image_processor' ) self .bridge = CvBridge() self .sub = self .create_subscription( Image, '/camera/image_raw' , self .callback, rclpy.qos.qos_profile_sensor_data) self .pub = self .create_publisher(Image, '/camera/image_processed' , 10 ) def callback (self, msg ): cv_img = self .bridge.imgmsg_to_cv2(msg, 'bgr8' ) gray = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY) edges = cv2.Canny(gray, 50 , 150 ) out_msg = self .bridge.cv2_to_imgmsg(edges, 'mono8' ) out_msg.header = msg.header self .pub.publish(out_msg)
4.4 处理深度图 1 2 3 4 5 6 7 cv_bridge::CvImagePtr depth_ptr = cv_bridge::toCvCopy (depth_msg, sensor_msgs::image_encodings::TYPE_32FC1); float depth_at_center = depth_ptr->image.at <float >( depth_msg->height / 2 , depth_msg->width / 2 ); RCLCPP_INFO (get_logger (), "Center depth: %.2f m" , depth_at_center);
4.5 处理压缩图像 1 2 3 4 5 6 7 from sensor_msgs.msg import CompressedImagecv_img = bridge.compressed_imgmsg_to_cv2(compressed_msg, 'bgr8' ) compressed_msg = bridge.cv2_to_compressed_imgmsg(cv_img, dst_format='jpeg' )
4.6 常见编码转换陷阱
操作
正确做法
常见错误
RGB→BGR
toCvCopy(msg, "bgr8")
直接用 rgb8 后忘记转换
Bayer→BGR
toCvCopy(msg, "bgr8") 自动转换
手动用 cvtColor 但 Bayer 模式搞错
深度→float
使用 "32FC1" 或 "passthrough"
用 "mono8" 导致精度丢失
大端序
检查 is_bigendian 字段
假设总是小端(嵌入式设备可能是大端)
五、image_transport:智能图像传输 5.1 为什么需要 image_transport 直接用 rclcpp::Publisher<sensor_msgs::msg::Image> 发布图像,每帧都是未压缩的原始数据。对于 1080p 30fps 的相机:
带宽 = 1920 × 1080 × 3 × 30 = 186.6 MB/s \text{带宽} = 1920 \times 1080 \times 3 \times 30 = 186.6 \text{ MB/s} 带宽 = 1920 × 1080 × 3 × 30 = 186.6 MB/s
这在网络传输或跨进程通信中是不可接受的。image_transport 通过插件机制,自动在发布端压缩、订阅端解压。
5.2 安装和可用传输插件 1 2 3 4 5 sudo apt install ros-humble-image-transport-pluginsros2 run image_transport list_transports
输出:
1 2 3 4 5 Declared transports: image_transport/raw - 原始传输(默认) image_transport/compressed - JPEG/PNG 压缩 image_transport/compressedDepth - 深度图 PNG 压缩 image_transport/theora - Theora 视频编码
5.3 使用 image_transport 发布 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <image_transport/image_transport.hpp> class CameraNode : public rclcpp::Node{ public : CameraNode () : Node ("camera_node" ) { it_pub_ = image_transport::create_publisher ( this , "/camera/image" ); } void publish_frame (const cv::Mat & frame) { auto msg = cv_bridge::CvImage ( std_msgs::msg::Header (), "bgr8" , frame).toImageMsg (); msg->header.stamp = this ->now (); msg->header.frame_id = "camera_optical_frame" ; it_pub_.publish (*msg); } private : image_transport::Publisher it_pub_; };
5.4 使用 image_transport 订阅 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class DetectorNode : public rclcpp::Node{ public : DetectorNode () : Node ("detector_node" ) { this ->declare_parameter ("image_transport" , "compressed" ); it_sub_ = image_transport::create_subscription ( this , "/camera/image" , auto cv_ptr = cv_bridge::toCvShare (msg, "bgr8" ); detect (cv_ptr->image); }, "compressed" ); } private : image_transport::Subscriber it_sub_; };
5.5 传输方式对比
传输方式
带宽(1080p@30fps)
延迟
CPU 开销
适用场景
raw
~187 MB/s
最低
无
同进程/零拷贝
compressed (JPEG 80%)
~3-9 MB/s
低
中等
网络传输(默认首选)
compressed (PNG)
~30-60 MB/s
中等
较高
需要无损压缩
theora
~1-3 MB/s
高(帧间依赖)
高
视频录制/远程监控
compressedDepth
~5-15 MB/s
低
中等
深度图传输
5.6 调整压缩参数 1 2 3 4 5 6 7 8 9 10 11 ros2 param list /camera_node ros2 param set /camera_node /camera/image/compressed.jpeg_quality 50 ros2 param set /camera_node /camera/image/compressed.format png ros2 param set /camera_node /camera/image/compressed.png_level 1
5.7 republish 工具 将一种传输格式转换为另一种:
1 2 3 4 5 6 7 8 9 10 11 ros2 run image_transport republish raw compressed \ --ros-args \ --remap in :=/camera/image_raw \ --remap out/compressed:=/camera/image/compressed ros2 run image_transport republish compressed raw \ --ros-args \ --remap in /compressed:=/camera/image/compressed \ --remap out:=/camera/image_decompressed
六、image_proc:标准图像预处理 6.1 功能概览 image_proc 提供 ROS2 标准的图像预处理节点:
节点
输入
输出
功能
RectifyNode
image_raw + camera_info
image_rect
畸变校正
DebayerNode
image_raw(Bayer 编码)
image_mono / image_color
去拜耳化
ResizeNode
image + camera_info
resize/image + resize/camera_info
缩放
CropDecimateNode
image + camera_info
裁剪后的图像
裁剪+降采样
6.2 启动畸变校正 1 2 3 4 5 6 7 ros2 run image_proc rectify_node \ --ros-args \ --remap image:=/camera/image_raw \ --remap camera_info:=/camera/camera_info
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from launch_ros.actions import ComposableNodeContainerfrom launch_ros.descriptions import ComposableNodecontainer = ComposableNodeContainer( name='image_proc_container' , namespace='' , package='rclcpp_components' , executable='component_container' , composable_node_descriptions=[ ComposableNode( package='image_proc' , plugin='image_proc::RectifyNode' , name='rectify' , remappings=[ ('image' , '/camera/image_raw' ), ('camera_info' , '/camera/camera_info' ), ], extra_arguments=[{'use_intra_process_comms' : True }], ), ], )
6.3 完整图像预处理管线 对于 Bayer 编码的工业相机,典型的预处理管线是:
flowchart TD
A["相机驱动"] -->|"image_raw (bayer_rggb8)"| B["DebayerNode"]
B -->|"image_mono (mono8)"| C1["灰度处理节点"]
B -->|"image_color (bgr8)"| D["RectifyNode + camera_info"]
D -->|"image_rect_color (bgr8, 已校正)"| E["ResizeNode"]
E -->|"resize/image (320×240)"| F["推理节点(YOLO 等)"]
七、零拷贝图像传输 7.1 性能问题 图像数据量大,每次跨节点传输如果都要拷贝 data[] 数组,CPU 和内存带宽消耗巨大。以 1080p bgr8 30fps 为例:
传输方式
每帧拷贝次数
每秒拷贝数据量
跨进程(DDS)
至少 2 次(序列化 + 反序列化)
~374 MB/s
同进程(无优化)
1 次
~187 MB/s
同进程(零拷贝)
0 次
0
7.2 实现零拷贝的三个条件
使用 std::unique_ptr 发布
节点在同一进程 (通过 component_container 加载)
启用进程内通信 (use_intra_process_comms: true)
7.3 零拷贝图像发布者 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class ZeroCopyCamera : public rclcpp::Node{ public : ZeroCopyCamera () : Node ("zero_copy_camera" ) { pub_ = create_publisher <sensor_msgs::msg::Image>("/camera/image" , 10 ); timer_ = create_wall_timer (33 ms, [this ]() { capture (); }); } private : void capture () { auto msg = std::make_unique <sensor_msgs::msg::Image>(); msg->header.stamp = now (); msg->header.frame_id = "camera_optical_frame" ; msg->height = 1080 ; msg->width = 1920 ; msg->encoding = "bgr8" ; msg->step = 1920 * 3 ; msg->data.resize (1920 * 1080 * 3 ); camera_.grab (msg->data.data (), msg->data.size ()); pub_->publish (std::move (msg)); } rclcpp::Publisher<sensor_msgs::msg::Image>::SharedPtr pub_; rclcpp::TimerBase::SharedPtr timer_; Camera camera_; };
7.4 零拷贝图像订阅者 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class ZeroCopyProcessor : public rclcpp::Node{ public : ZeroCopyProcessor () : Node ("zero_copy_processor" ) { sub_ = create_subscription <sensor_msgs::msg::Image>( "/camera/image" , 10 , RCLCPP_INFO (get_logger (), "Received image at address: %p" , (void *)msg->data.data ()); cv::Mat img (msg->height, msg->width, CV_8UC3, msg->data.data (), msg->step); cv::GaussianBlur (img, img, cv::Size (5 , 5 ), 1.5 ); pub_->publish (std::move (msg)); }); pub_ = create_publisher <sensor_msgs::msg::Image>( "/camera/image_processed" , 10 ); } };
7.5 Launch 配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 ComposableNodeContainer( name='camera_pipeline' , namespace='' , package='rclcpp_components' , executable='component_container' , composable_node_descriptions=[ ComposableNode( package='my_camera' , plugin='ZeroCopyCamera' , name='camera' , extra_arguments=[{'use_intra_process_comms' : True }]), ComposableNode( package='my_processor' , plugin='ZeroCopyProcessor' , name='processor' , extra_arguments=[{'use_intra_process_comms' : True }]), ], )
八、深度图像与点云 8.1 深度图编码
编码
数据类型
单位
说明
16UC1
uint16_t
毫米
RealSense/Kinect 默认格式
32FC1
float
米
浮点精度,无效值为 NaN
8.2 深度图 → 点云 depth_image_proc 包提供深度图到 3D 点云的转换:
1 2 3 4 5 6 7 8 9 10 sudo apt install ros-humble-depth-image-procros2 run depth_image_proc point_cloud_xyzrgb_node \ --ros-args \ --remap depth/image_rect:=/camera/depth/image_rect \ --remap depth/camera_info:=/camera/depth/camera_info \ --remap rgb/image_rect_color:=/camera/color/image_rect \ --remap rgb/camera_info:=/camera/color/camera_info
8.3 手动转换(Python) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import numpy as npfrom sensor_msgs.msg import PointCloud2, PointFieldfrom sensor_msgs_py import point_cloud2def depth_to_pointcloud (depth_msg, camera_info_msg ): """将深度图转换为点云""" fx = camera_info_msg.k[0 ] fy = camera_info_msg.k[4 ] cx = camera_info_msg.k[2 ] cy = camera_info_msg.k[5 ] depth = np.frombuffer(depth_msg.data, dtype=np.uint16).reshape( depth_msg.height, depth_msg.width) depth_m = depth.astype(np.float32) / 1000.0 v, u = np.mgrid[0 :depth_msg.height, 0 :depth_msg.width] z = depth_m x = (u - cx) * z / fx y = (v - cy) * z / fy valid = z > 0 points = np.stack([x[valid], y[valid], z[valid]], axis=-1 ) return point_cloud2.create_cloud_xyz32(depth_msg.header, points)
九、GPU 加速图像处理 9.1 NVIDIA Isaac ROS Image Pipeline NVIDIA 提供了 isaac_ros_image_pipeline,是 CPU 版 image_pipeline 的 GPU 加速替代品:
操作
CPU (image_pipeline)
GPU (Isaac ROS)
加速比
畸变校正 1080p
~5ms
~0.6ms
8×
立体视差 1080p
~200ms
~1.3ms
150×
色彩转换 1080p
~3ms
~0.4ms
7×
安装:
1 sudo apt install ros-humble-isaac-ros-image-proc
使用方式与 image_proc 完全兼容——替换包名即可:
1 2 3 4 5 6 7 8 9 ComposableNode( package='isaac_ros_image_proc' , plugin='nvidia::isaac_ros::image_proc::RectifyNode' , name='rectify' , remappings=[ ('image_raw' , '/camera/image_raw' ), ('camera_info' , '/camera/camera_info' ), ], )
9.2 NITROS 零拷贝加速 Isaac ROS 使用 NITROS(NVIDIA Isaac Transport for ROS)实现 GPU 内存的零拷贝传输。图像数据始终保留在 GPU 显存中,不需要 GPU↔CPU 之间的数据搬运:
flowchart LR
A["相机"] -->|"上传"| B["GPU 显存"]
B -->|"零拷贝"| C["畸变校正\n(GPU)"]
C -->|"零拷贝"| D["色彩转换\n(GPU)"]
D -->|"零拷贝"| E["推理\n(GPU)"]
style B fill:#76b900,color:#fff
style C fill:#76b900,color:#fff
style D fill:#76b900,color:#fff
style E fill:#76b900,color:#fff
9.3 使用 OpenCV CUDA 不依赖 Isaac ROS 也可以手动使用 OpenCV 的 CUDA 模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <opencv2/cudaimgproc.hpp> #include <opencv2/cudawarping.hpp> void process_gpu (const cv::Mat & input) { cv::cuda::GpuMat d_input, d_output; d_input.upload (input); cv::cuda::remap (d_input, d_output, d_map1, d_map2, cv::INTER_LINEAR); cv::cuda::cvtColor (d_output, d_output, cv::COLOR_BGR2GRAY); d_output.download (output_cpu); }
十、性能优化最佳实践 10.1 带宽优化清单
策略
节省带宽
代价
使用 compressed 传输 (JPEG 80%)
~95%
CPU 编解码开销
降低分辨率(1080p → 480p)
~80%
感知精度下降
降低帧率(30fps → 15fps)
~50%
时间分辨率下降
使用灰度 mono8 替代 bgr8
~67%
丢失色彩信息
使用 ROI 裁剪
取决于 ROI 大小
视场角缩小
10.2 延迟优化清单
策略
效果
说明
同进程零拷贝
消除拷贝延迟
使用 component_container + unique_ptr
SensorDataQoS
允许丢旧帧
BEST_EFFORT + KEEP_LAST(1)
image_transport 解耦
压缩不阻塞驱动
republish 在单独进程压缩
避免不必要的编码转换
减少 CPU 开销
发布端直接用目标格式
GPU 管线
消除 CPU↔GPU 拷贝
Isaac ROS NITROS
10.3 QoS 配置建议 1 2 3 4 5 6 7 8 9 auto camera_qos = rclcpp::SensorDataQoS ();auto info_qos = rclcpp::QoS (1 ).reliable ().transient_local ();auto result_qos = rclcpp::QoS (5 ).reliable ();
10.4 调试工具 1 2 3 4 5 6 7 8 9 10 11 12 ros2 topic bw /camera/image_raw ros2 topic hz /camera/image_raw ros2 topic list | grep image ros2 run rqt_image_view rqt_image_view ros2 topic info /camera/image_raw --verbose
十一、完整实战:无人机视觉管线 将以上所有知识串联起来,构建一个完整的无人机前视相机处理管线:
11.1 Launch 文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 import osfrom launch import LaunchDescriptionfrom launch_ros.actions import ComposableNodeContainerfrom launch_ros.descriptions import ComposableNodefrom ament_index_python.packages import get_package_share_directorydef generate_launch_description (): calib_file = os.path.join( get_package_share_directory('drone_perception' ), 'config' , 'front_camera.yaml' ) return LaunchDescription([ ComposableNodeContainer( name='vision_pipeline' , namespace='drone' , package='rclcpp_components' , executable='component_container_mt' , composable_node_descriptions=[ ComposableNode( package='usb_cam' , plugin='usb_cam::UsbCamNode' , name='front_camera' , parameters=[{ 'video_device' : '/dev/video0' , 'image_width' : 1920 , 'image_height' : 1080 , 'pixel_format' : 'mjpeg2rgb' , 'framerate' : 30.0 , 'camera_info_url' : f'file://{calib_file} ' , }], extra_arguments=[{'use_intra_process_comms' : True }], ), ComposableNode( package='image_proc' , plugin='image_proc::RectifyNode' , name='rectify' , remappings=[ ('image' , '/drone/front_camera/image_raw' ), ('camera_info' , '/drone/front_camera/camera_info' ), ], extra_arguments=[{'use_intra_process_comms' : True }], ), ComposableNode( package='image_proc' , plugin='image_proc::ResizeNode' , name='resize' , remappings=[ ('image/image_raw' , '/drone/rectify/image_rect' ), ('image/camera_info' , '/drone/front_camera/camera_info' ), ], parameters=[{ 'scale_width' : 0.333 , 'scale_height' : 0.444 , }], extra_arguments=[{'use_intra_process_comms' : True }], ), ComposableNode( package='drone_detector' , plugin='drone_detector::YoloNode' , name='detector' , remappings=[ ('image' , '/drone/resize/resize/image' ), ], parameters=[{ 'model_path' : '/models/yolov8n.onnx' , 'confidence_threshold' : 0.5 , }], extra_arguments=[{'use_intra_process_comms' : True }], ), ], ), ])
11.2 架构图
flowchart TD
A["USB 相机 (V4L2)"] --> B["usb_cam 驱动"]
B -->|"image_raw\n(bgr8, 1920×1080)"| C["RectifyNode"]
B -->|"camera_info\n(标定参数)"| C
C -->|"image_rect\n(bgr8, 1920×1080, 已校正)"| D["ResizeNode"]
D -->|"image\n(bgr8, 640×480)"| E["YoloNode"]
E -->|"detections"| F["检测结果"]
style A fill:#607d8b,color:#fff
style B fill:#2196f3,color:#fff
style C fill:#2196f3,color:#fff
style D fill:#2196f3,color:#fff
style E fill:#ff9800,color:#fff
style F fill:#4caf50,color:#fff
全部节点在同一 component_container 进程中运行,图像传输零拷贝。
十二、参考资源
sensor_msgs/Image 消息定义 : docs.ros2.org/sensor_msgs/msg/Image
image_encodings.hpp 源码 : github.com/ros2/common_interfaces
cv_bridge 文档 : docs.ros.org/cv_bridge
image_transport 教程 : github.com/ros-perception/image_transport_tutorials
image_transport_plugins : github.com/ros-perception/image_transport_plugins
image_proc 文档 : docs.ros.org/image_proc
camera_calibration : docs.ros.org/camera_calibration
image_geometry PinholeCameraModel : docs.ros.org/image_geometry
depth_image_proc : github.com/ros-perception/image_pipeline
Isaac ROS Image Pipeline : github.com/NVIDIA-ISAAC-ROS/isaac_ros_image_pipeline
ROS2 零拷贝通信 Demo : docs.ros.org/Intra-Process-Communication
OpenCV 编码参考 : docs.opencv.org