在 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 编码选择指南

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=[fx0cx0fycy001]K = \begin{bmatrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{bmatrix}
  • fx,fyf_x, f_y:焦距(像素单位)
  • cx,cyc_x, c_y:主点坐标(通常接近图像中心)

2.3 畸变模型

plumb_bob(针孔相机,5 参数):

D=[k1,k2,p1,p2,k3]D = [k_1, k_2, p_1, p_2, k_3]
  • k1,k2,k3k_1, k_2, k_3:径向畸变系数
  • p1,p2p_1, p_2:切向畸变系数

equidistant(鱼眼相机,4 参数):

D=[k1,k2,k3,k4]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-calibration

# 启动标定(8×6 棋盘格,每格 108mm)
ros2 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/Imagecv::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;
}

// OpenCV 处理
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; // 只读引用
// 注意:不能修改 img,否则 UB
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 rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
import cv2

class 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):
# ROS Image → OpenCV Mat
cv_img = self.bridge.imgmsg_to_cv2(msg, 'bgr8')

# 处理
gray = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, 50, 150)

# OpenCV Mat → ROS Image
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
// 深度图通常是 16UC1(毫米)或 32FC1(米)
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 CompressedImage

# 压缩图像 → OpenCV
cv_img = bridge.compressed_imgmsg_to_cv2(compressed_msg, 'bgr8')

# OpenCV → 压缩图像
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}

这在网络传输或跨进程通信中是不可接受的。image_transport 通过插件机制,自动在发布端压缩、订阅端解压。

5.2 安装和可用传输插件

1
2
3
4
5
# 安装所有传输插件
sudo apt install ros-humble-image-transport-plugins

# 查看可用的传输方式
ros2 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")
{
// 创建 image_transport 发布者
// 自动创建多个主题:
// /camera/image (raw)
// /camera/image/compressed
// /camera/image/compressedDepth
// /camera/image/theora
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");

// 创建 image_transport 订阅者
it_sub_ = image_transport::create_subscription(
this, "/camera/image",
// 无论发布端用什么格式,这里收到的都是解压后的 raw Image
auto cv_ptr = cv_bridge::toCvShare(msg, "bgr8");
detect(cv_ptr->image);
},
"compressed"); // 指定使用 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

# 调整 JPEG 质量(1-100,默认 80)
ros2 param set /camera_node /camera/image/compressed.jpeg_quality 50

# 切换为 PNG 压缩
ros2 param set /camera_node /camera/image/compressed.format png

# 调整 PNG 压缩级别(1-9,默认 3)
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
# raw → compressed(解耦压缩和驱动)
ros2 run image_transport republish raw compressed \
--ros-args \
--remap in:=/camera/image_raw \
--remap out/compressed:=/camera/image/compressed

# compressed → raw(解压后用于处理)
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

# 方式二:Launch 文件(推荐)
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 ComposableNodeContainer
from launch_ros.descriptions import ComposableNode

container = 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 编码的工业相机,典型的预处理管线是:


七、零拷贝图像传输

7.1 性能问题

图像数据量大,每次跨节点传输如果都要拷贝 data[] 数组,CPU 和内存带宽消耗巨大。以 1080p bgr8 30fps 为例:

传输方式 每帧拷贝次数 每秒拷贝数据量
跨进程(DDS) 至少 2 次(序列化 + 反序列化) ~374 MB/s
同进程(无优化) 1 次 ~187 MB/s
同进程(零拷贝) 0 次 0

7.2 实现零拷贝的三个条件

  1. 使用 std::unique_ptr 发布
  2. 节点在同一进程(通过 component_container 加载)
  3. 启用进程内通信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(33ms, [this]() { capture(); });
}

private:
void capture()
{
// 创建 unique_ptr 消息
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);

// 填充图像数据(从相机 SDK 获取)
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")
{
// 用 unique_ptr 回调签名接收零拷贝消息
sub_ = create_subscription<sensor_msgs::msg::Image>(
"/camera/image", 10,
// msg 是 unique_ptr——独占所有权,零拷贝
RCLCPP_INFO(get_logger(),
"Received image at address: %p", (void*)msg->data.data());

// 可以直接修改 msg,因为拥有独占所有权
// 用 cv_bridge 包装(不拷贝)
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-proc

# 启动
ros2 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 np
from sensor_msgs.msg import PointCloud2, PointField
from sensor_msgs_py import point_cloud2

def 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]

# 深度图 → numpy
depth = np.frombuffer(depth_msg.data, dtype=np.uint16).reshape(
depth_msg.height, depth_msg.width)
depth_m = depth.astype(np.float32) / 1000.0 # mm → m

# 生成像素坐标网格
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
立体视差 1080p ~200ms ~1.3ms 150×
色彩转换 1080p ~3ms ~0.4ms

安装:

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', # 替换 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 之间的数据搬运:

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);

// GPU 畸变校正
cv::cuda::remap(d_input, d_output, d_map1, d_map2,
cv::INTER_LINEAR);

// GPU 色彩转换
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();
// → BEST_EFFORT, KEEP_LAST(5), VOLATILE

// 标定信息:可靠传输 + 后来者也能收到
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

# 查看 image_transport 主题
ros2 topic list | grep image

# 可视化图像
ros2 run rqt_image_view rqt_image_view

# 查看完整的 QoS 信息
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 os
from launch import LaunchDescription
from launch_ros.actions import ComposableNodeContainer
from launch_ros.descriptions import ComposableNode
from ament_index_python.packages import get_package_share_directory

def 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=[
# 1. 相机驱动
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}],
),
# 2. 畸变校正
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}],
),
# 3. 缩放(为推理准备 640×480 输入)
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}],
),
# 4. 目标检测
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 架构图

全部节点在同一 component_container 进程中运行,图像传输零拷贝。


十二、参考资源

  1. sensor_msgs/Image 消息定义: docs.ros2.org/sensor_msgs/msg/Image
  2. image_encodings.hpp 源码: github.com/ros2/common_interfaces
  3. cv_bridge 文档: docs.ros.org/cv_bridge
  4. image_transport 教程: github.com/ros-perception/image_transport_tutorials
  5. image_transport_plugins: github.com/ros-perception/image_transport_plugins
  6. image_proc 文档: docs.ros.org/image_proc
  7. camera_calibration: docs.ros.org/camera_calibration
  8. image_geometry PinholeCameraModel: docs.ros.org/image_geometry
  9. depth_image_proc: github.com/ros-perception/image_pipeline
  10. Isaac ROS Image Pipeline: github.com/NVIDIA-ISAAC-ROS/isaac_ros_image_pipeline
  11. ROS2 零拷贝通信 Demo: docs.ros.org/Intra-Process-Communication
  12. OpenCV 编码参考: docs.opencv.org