🔲 第24课:深度相机

感知与控制 ✅ Docker验证通过

📋 课程目标

🧠 深度相机原理

深度相机(RGB-D Camera)同时输出彩色图像和每像素的深度信息。常见方案包括结构光(RealSense)、飞行时间(ToF)和双目立体视觉。

RGB-D数据流: ┌────────────┐ Color ┌──────────┐ │ RGB-D │─/color──►│ 2D检测 │ │ Camera │ │ YOLO等 │ │(RealSense) │ └──────────┘ │ │ │ │ Depth ┌──────────┐ │ │─/depth──►│ 3D重建 │ │ │ │ 点云生成 │ │ │ └──────────┘ │ │ │ │ Points ┌──────────┐ │ │─/points─►│ 障碍检测 │ └────────────┘ └──────────┘

🐍 Python:PointCloud2处理节点

#!/usr/bin/env python3
# pointcloud_processor.py - 点云处理节点
# ✅ Docker验证通过
import rclpy, struct, numpy as np
from rclpy.node import Node
from sensor_msgs.msg import PointCloud2, PointField
from geometry_msgs.msg import Twist
from std_msgs.msg import Header

class PointCloudProcessor(Node):
    def __init__(self):
        super().__init__('pointcloud_processor')
        self.declare_parameter('min_height', 0.1)     # 最小高度(过滤地面)
        self.declare_parameter('max_height', 2.0)     # 最大高度
        self.declare_parameter('min_distance', 0.3)   # 最小距离
        self.declare_parameter('max_distance', 5.0)   # 最大距离
        self.declare_parameter('voxel_size', 0.05)    # 体素滤波大小
        self.declare_parameter('safe_distance', 0.8)  # 安全距离
        
        self.sub = self.create_subscription(
            PointCloud2, '/camera/depth/points', self._cb, 10)
        self.filtered_pub = self.create_publisher(PointCloud2, '/points_filtered', 10)
        self.cmd_pub = self.create_publisher(Twist, '/cmd_vel', 10)
        self.get_logger().info('🔲 点云处理器已启动')
    
    def _cb(self, msg: PointCloud2):
        # 解析PointCloud2
        points = self._parse_cloud(msg)
        if points is None: return
        
        # 过滤:高度范围 + 距离范围
        min_h = self.get_parameter('min_height').value
        max_h = self.get_parameter('max_height').value
        min_d = self.get_parameter('min_distance').value
        max_d = self.get_parameter('max_distance').value
        
        # 地面分割:保留高于地面的点
        mask = (points[:,2] > min_h) & (points[:,2] < max_h)
        filtered = points[mask]
        
        # 距离过滤
        dist = np.sqrt(filtered[:,0]**2 + filtered[:,1]**2)
        mask2 = (dist > min_d) & (dist < max_d)
        filtered = filtered[mask2]
        
        if len(filtered) == 0: return
        
        # 前方障碍物检测
        front = filtered[(np.abs(filtered[:,1]) < 0.5)]  # ±0.5m范围
        if len(front) > 0:
            min_front_dist = np.min(np.sqrt(front[:,0]**2 + front[:,1]**2))
            safe_d = self.get_parameter('safe_distance').value
            if min_front_dist < safe_d:
                self.get_logger().warn(f'⚠️ 前方3D障碍: {min_front_dist:.2f}m')
        
        # 发布过滤后的点云
        filtered_msg = self._create_cloud(msg.header, filtered)
        self.filtered_pub.publish(filtered_msg)
    
    def _parse_cloud(self, msg):
        # 解析xyz字段
        fmt = 'fff'  # x, y, z
        point_step = msg.point_step
        points = []
        for i in range(0, len(msg.data), point_step):
            x, y, z = struct.unpack_from(fmt, msg.data, i)
            if np.isfinite(x) and np.isfinite(y) and np.isfinite(z):
                points.append([x, y, z])
        return np.array(points) if points else None
    
    def _create_cloud(self, header, points):
        msg = PointCloud2()
        msg.header = header
        msg.height = 1
        msg.width = len(points)
        msg.fields = [
            PointField(name='x', offset=0, datatype=PointField.FLOAT32, count=1),
            PointField(name='y', offset=4, datatype=PointField.FLOAT32, count=1),
            PointField(name='z', offset=8, datatype=PointField.FLOAT32, count=1),
        ]
        msg.is_bigendian = False
        msg.point_step = 12
        msg.row_step = 12 * len(points)
        msg.data = points.astype(np.float32).tobytes()
        msg.is_dense = True
        return msg

def main(args=None):
    rclpy.init(args=args); rclpy.spin(PointCloudProcessor()); rclpy.shutdown()

🐍 Python:深度图像转2D代价地图

#!/usr/bin/env python3
# depth_to_costmap.py - 深度图像转局部代价地图
import rclpy, numpy as np
from rclpy.node import Node
from sensor_msgs.msg import Image
from nav_msgs.msg import OccupancyGrid
from cv_bridge import CvBridge
import math

class DepthToCostmap(Node):
    def __init__(self):
        super().__init__('depth_to_costmap')
        self.declare_parameter('map_resolution', 0.05)  # m/cell
        self.declare_parameter('map_width', 3.0)         # m
        self.declare_parameter('map_height', 3.0)        # m
        self.declare_parameter('obstacle_height', 0.15)   # m
        self.declare_parameter('camera_height', 0.5)      # m
        self.declare_parameter('fov_h', 1.047)            # 60° horizontal
        
        self.bridge = CvBridge()
        self.res = self.get_parameter('map_resolution').value
        self.w = int(self.get_parameter('map_width').value / self.res)
        self.h = int(self.get_parameter('map_height').value / self.res)
        self.obs_h = self.get_parameter('obstacle_height').value
        self.cam_h = self.get_parameter('camera_height').value
        
        self.depth_sub = self.create_subscription(
            Image, '/camera/depth/image_raw', self._cb, 10)
        self.costmap_pub = self.create_publisher(OccupancyGrid, '/depth_costmap', 10)
        self.get_logger().info('🔲 深度代价地图生成器已启动')
    
    def _cb(self, msg: Image):
        depth = self.bridge.imgmsg_to_cv2(msg, desired_encoding='16UC1')
        # 生成代价地图
        grid = OccupancyGrid()
        grid.header = msg.header
        grid.info.resolution = self.res
        grid.info.width = self.w
        grid.info.height = self.h
        grid.info.origin.position.x = -self.get_parameter('map_width').value / 2
        grid.info.origin.position.y = -self.get_parameter('map_height').value / 2
        
        data = [-1] * (self.w * self.h)  # 初始未知
        
        for v in range(0, depth.shape[0], 4):  # 4倍降采样
            for u in range(0, depth.shape[1], 4):
                d = depth[v, u] / 1000.0  # mm→m
                if d < 0.1 or d > 5.0: continue
                
                # 像素→3D坐标
                fov = self.get_parameter('fov_h').value
                angle_x = (u / depth.shape[1] - 0.5) * fov
                angle_y = (v / depth.shape[0] - 0.5) * fov
                
                x3d = d * math.cos(angle_y) * math.sin(angle_x)
                y3d = d * math.sin(angle_y)
                z3d = d * math.cos(angle_y) * math.cos(angle_x)
                
                # 地面过滤
                world_z = self.cam_h + y3d
                if world_z < self.obs_h: continue  # 地面点,跳过
                
                # 3D→栅格坐标
                gx = int((x3d - grid.info.origin.position.x) / self.res)
                gy = int((z3d - grid.info.origin.position.y) / self.res)
                if 0 <= gx < self.w and 0 <= gy < self.h:
                    data[gy * self.w + gx] = 100  # 障碍物
        
        grid.data = data
        self.costmap_pub.publish(grid)

def main(args=None):
    rclpy.init(args=args); rclpy.spin(DepthToCostmap()); rclpy.shutdown()

🔧 C++:点云处理核心

// pointcloud_core.cpp
#include "rclcpp/rclcpp.hpp"
#include "sensor_msgs/msg/point_cloud2.hpp"
#include <vector><cmath>
class PointCloudCore : public rclcpp::Node {
public:
    PointCloudCore() : Node("pointcloud_core") {
        sub_ = create_subscription<sensor_msgs::msg::PointCloud2>(
            "/camera/depth/points", 10,
            [this](sensor_msgs::msg::PointCloud2::SharedPtr msg) {
                analyze(msg);
            });
        RCLCPP_INFO(get_logger(), "C++点云处理器已启动");
    }
private:
    void analyze(const sensor_msgs::msg::PointCloud2::SharedPtr& msg) {
        // 找到x,y,z字段的偏移
        size_t x_off=0, y_off=0, z_off=0;
        for (auto& f : msg->fields) {
            if (f.name == "x") x_off = f.offset;
            if (f.name == "y") y_off = f.offset;
            if (f.name == "z") z_off = f.offset;
        }
        int obs_count = 0;
        double min_front = 999.0;
        for (size_t i = 0; i < msg->width; i++) {
            const float* xp = reinterpret_cast<const float*>(
                msg->data.data() + i * msg->point_step + x_off);
            const float* yp = reinterpret_cast<const float*>(
                msg->data.data() + i * msg->point_step + y_off);
            const float* zp = reinterpret_cast<const float*>(
                msg->data.data() + i * msg->point_step + z_off);
            if (std::isfinite(*xp) && *yp > 0.1) {  // 高于地面
                double dist = std::sqrt((*xp)*(*xp) + (*zp)*(*zp));
                if (dist < min_front && std::abs(*yp) < 0.5)
                    min_front = dist;
                obs_count++;
            }
        }
        if (min_front < 0.8)
            RCLCPP_WARN(get_logger(), "前方3D障碍: %.2fm", min_front);
    }
    rclcpp::Subscription<sensor_msgs::msg::PointCloud2>::SharedPtr sub_;
};

🎯 练习题

📝 练习1:地面分割

调整min_height参数,观察地面分割效果。太高会怎样?太低呢?

📝 练习2:深度代价地图

运行DepthToCostmap,在rviz2中查看生成的代价地图。

📝 练习3:体素滤波

实现体素下采样滤波,对比原始和滤波后的点云数量。

🏆 成就解锁

🏅 深度感知专家

经验值:+250 XP