📡 第22课:激光雷达处理

感知与控制 ✅ Docker验证通过

📋 课程目标

🧠 激光雷达工作原理

激光雷达(LiDAR)通过发射激光束并测量反射时间来计算距离。2D激光雷达在一个平面上旋转扫描,生成一圈距离数据,即LaserScan消息。

激光雷达扫描示意: 0°(前方) │ ╱────┼────╲ ╱ │ ╲ ●───扫描区域───● ← 障碍物(反射点) ╲ │ ╱ ╲────┼────╱ │ 180°(后方) LaserScan数据结构: ranges[0] → angle_min方向距离 ranges[1] → angle_min + angle_increment方向距离 ... ranges[N-1] → angle_max方向距离

📐 LaserScan消息详解

# sensor_msgs/msg/LaserScan
std_msgs/Header header
float32 angle_min          # 起始角度 rad (-π)
float32 angle_max          # 终止角度 rad (π)
float32 angle_increment    # 角度增量 rad
float32 time_increment     # 时间增量 s
float32 scan_time          # 扫描周期 s
float32 range_min          # 最小有效距离 m
float32 range_max          # 最大有效距离 m
float32[] ranges           # 距离数据数组
float32[] intensities      # 反射强度数组

🐍 Python:扇区分析器

#!/usr/bin/env python3
# laser_sector_analyzer.py - 激光雷达扇区分析
# ✅ Docker验证通过
import math, rclpy, numpy as np
from rclpy.node import Node
from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy
from sensor_msgs.msg import LaserScan
from geometry_msgs.msg import Twist
from std_msgs.msg import String

class LaserSectorAnalyzer(Node):
    def __init__(self):
        super().__init__('laser_sector_analyzer')
        self.declare_parameter('num_sectors', 8)         # 扇区数量
        self.declare_parameter('safe_distance', 1.0)      # 安全距离 m
        self.declare_parameter('stop_distance', 0.5)      # 停止距离 m
        self.declare_parameter('front_sector_angle', 60)   # 前方扇区角度 °
        
        n_sec = self.get_parameter('num_sectors').value
        self.safe_dist = self.get_parameter('safe_distance').value
        self.stop_dist = self.get_parameter('stop_distance').value
        self.front_angle = math.radians(self.get_parameter('front_sector_angle').value)
        
        # 扇区标签
        self.sector_names = ['前','右前','右','右后','后','左后','左','左前'][:n_sec]
        
        # QoS
        scan_qos = QoSProfile(
            reliability=ReliabilityPolicy.BEST_EFFORT,
            history=HistoryPolicy.KEEP_LAST, depth=5)
        
        self.scan_sub = self.create_subscription(
            LaserScan, '/scan', self._scan_cb, scan_qos)
        self.alert_pub = self.create_publisher(String, '/laser_alert', 10)
        self.cmd_pub = self.create_publisher(Twist, '/cmd_vel', 10)
        self.timer = self.create_timer(0.5, self._report_status)
        
        self.sector_mins = [float('inf')] * n_sec
        self.scan_count = 0
        self.get_logger().info(f'📡 激光扇区分析器启动 - {n_sec}个扇区')
    
    def _scan_cb(self, msg: LaserScan):
        self.scan_count += 1
        n_sec = len(self.sector_mins)
        sector_size = len(msg.ranges) // n_sec
        
        for i in range(n_sec):
            start = i * sector_size
            end = start + sector_size if i < n_sec-1 else len(msg.ranges)
            sector_ranges = [r for r in msg.ranges[start:end]
                           if msg.range_min < r < msg.range_max]
            self.sector_mins[i] = min(sector_ranges) if sector_ranges else float('inf')
        
        # 前方障碍物检测
        front_idx = 0  # 前方扇区
        if self.sector_mins[front_idx] < self.stop_dist:
            alert = String()
            alert.data = f'🛑 前方障碍! {self.sector_mins[front_idx]:.2f}m'
            self.alert_pub.publish(alert)
            self.get_logger().warn(alert.data)
    
    def _report_status(self):
        if self.scan_count == 0: return
        status_parts = []
        for i, (name, dist) in enumerate(zip(self.sector_names, self.sector_mins)):
            d = dist if dist != float('inf') else 999
            emoji = '🔴' if d < self.stop_dist else '🟡' if d < self.safe_dist else '🟢'
            status_parts.append(f'{emoji}{name}:{d:.1f}m')
        self.get_logger().info(' '.join(status_parts))

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

🐍 Python:LaserScan过滤器

#!/usr/bin/env python3
# scan_filter_node.py - 激光扫描数据过滤器
import math, rclpy, numpy as np
from rclpy.node import Node
from rclpy.qos import QoSProfile, ReliabilityPolicy
from sensor_msgs.msg import LaserScan

class ScanFilterNode(Node):
    def __init__(self):
        super().__init__('scan_filter')
        self.declare_parameter('min_range', 0.3)
        self.declare_parameter('max_range', 8.0)
        self.declare_parameter('filter_angle_min', -2.09)  # -120°
        self.declare_parameter('filter_angle_max', 2.09)   # +120°
        self.declare_parameter('median_window', 3)
        
        self.min_r = self.get_parameter('min_range').value
        self.max_r = self.get_parameter('max_range').value
        self.filt_min = self.get_parameter('filter_angle_min').value
        self.filt_max = self.get_parameter('filter_angle_max').value
        self.med_w = self.get_parameter('median_window').value
        
        qos = QoSProfile(reliability=ReliabilityPolicy.BEST_EFFORT, depth=5)
        self.sub = self.create_subscription(LaserScan, '/scan', self._cb, qos)
        self.pub = self.create_publisher(LaserScan, '/scan_filtered', qos)
        self.get_logger().info('🔍 扫描过滤器已启动')
    
    def _cb(self, msg: LaserScan):
        filtered = LaserScan()
        filtered.header = msg.header
        filtered.angle_min = msg.angle_min
        filtered.angle_max = msg.angle_max
        filtered.angle_increment = msg.angle_increment
        filtered.time_increment = msg.time_increment
        filtered.scan_time = msg.scan_time
        filtered.range_min = msg.range_min
        filtered.range_max = msg.range_max
        filtered.ranges = list(msg.ranges)
        filtered.intensities = list(msg.intensities) if msg.intensities else []
        
        # 距离过滤
        for i in range(len(filtered.ranges)):
            r = filtered.ranges[i]
            if r < self.min_r or r > self.max_r:
                filtered.ranges[i] = float('inf')
        
        # 中值滤波去噪
        if self.med_w >= 3:
            orig = list(filtered.ranges)
            half = self.med_w // 2
            for i in range(half, len(orig) - half):
                window = [v for v in orig[i-half:i+half+1] if math.isfinite(v)]
                if window:
                    filtered.ranges[i] = sorted(window)[len(window)//2]
        
        # 角度范围过滤
        for i in range(len(filtered.ranges)):
            angle = msg.angle_min + i * msg.angle_increment
            if angle < self.filt_min or angle > self.filt_max:
                filtered.ranges[i] = float('inf')
        
        self.pub.publish(filtered)

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

🐍 Python:障碍物距离监测

#!/usr/bin/env python3
# obstacle_monitor.py - 基于激光的障碍物距离监测
import math, rclpy
from rclpy.node import Node
from sensor_msgs.msg import LaserScan
from geometry_msgs.msg import Twist
from std_msgs.msg import Bool

class ObstacleMonitor(Node):
    def __init__(self):
        super().__init__('obstacle_monitor')
        self.declare_parameter('safe_distance', 0.8)
        self.declare_parameter('front_angle_range', 60)  # degrees
        self.declare_parameter('auto_stop', True)
        
        self.safe_d = self.get_parameter('safe_distance').value
        self.front_range = math.radians(self.get_parameter('front_angle_range').value)
        self.auto_stop = self.get_parameter('auto_stop').value
        
        from rclpy.qos import QoSProfile, ReliabilityPolicy
        qos = QoSProfile(reliability=ReliabilityPolicy.BEST_EFFORT, depth=5)
        self.scan_sub = self.create_subscription(LaserScan, '/scan', self._scan_cb, qos)
        self.cmd_sub = self.create_subscription(Twist, '/cmd_vel_raw', self._cmd_cb, 10)
        self.cmd_pub = self.create_publisher(Twist, '/cmd_vel', 10)
        self.safety_pub = self.create_publisher(Bool, '/safety_stop', 10)
        self.is_safe = True
        self.get_logger().info('🛡️ 障碍物监测已启动')
    
    def _scan_cb(self, msg):
        front_ranges = []
        for i, r in enumerate(msg.ranges):
            angle = msg.angle_min + i * msg.angle_increment
            if abs(angle) <= self.front_range/2 and msg.range_min < r < msg.range_max:
                front_ranges.append(r)
        
        min_dist = min(front_ranges) if front_ranges else float('inf')
        was_safe = self.is_safe
        self.is_safe = min_dist > self.safe_d
        
        if was_safe and not self.is_safe:
            self.get_logger().warn(f'⚠️ 障碍物接近! {min_dist:.2f}m')
        elif not was_safe and self.is_safe:
            self.get_logger().info('✅ 障碍物已清除')
        
        safety = Bool(); safety.data = not self.is_safe
        self.safety_pub.publish(safety)
    
    def _cmd_cb(self, msg):
        if self.auto_stop and not self.is_safe and msg.linear.x > 0:
            # 前方有障碍,阻止前进
            safe_cmd = Twist()
            safe_cmd.linear.x = 0.0
            safe_cmd.angular.z = msg.angular.z
            self.cmd_pub.publish(safe_cmd)
        else:
            self.cmd_pub.publish(msg)

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

🔧 C++:LaserScan处理

// scan_processor.cpp - C++激光扫描处理
#include "rclcpp/rclcpp.hpp"
#include "sensor_msgs/msg/laser_scan.hpp"
#include <vector><algorithm><cmath>
class ScanProcessor : public rclcpp::Node {
public:
    ScanProcessor() : Node("scan_processor") {
        declare_parameter("min_range", 0.3);
        declare_parameter("max_range", 8.0);
        declare_parameter("num_sectors", 8);
        sub_ = create_subscription<sensor_msgs::msg::LaserScan>(
            "/scan", rclcpp::SensorDataQoS(),
            [this](sensor_msgs::msg::LaserScan::SharedPtr msg) {
                process_scan(*msg);
            });
        RCLCPP_INFO(get_logger(), "C++扫描处理器已启动");
    }
private:
    void process_scan(const sensor_msgs::msg::LaserScan& msg) {
        int n_sec = get_parameter("num_sectors").as_int();
        double min_r = get_parameter("min_range").as_double();
        double max_r = get_parameter("max_range").as_double();
        int sector_sz = static_cast<int>(msg.ranges.size()) / n_sec;
        for (int s = 0; s < n_sec; s++) {
            double min_dist = std::numeric_limits<double>::infinity();
            for (int i = s*sector_sz; i < (s+1)*sector_sz && i < static_cast<int>(msg.ranges.size()); i++) {
                double r = msg.ranges[i];
                if (r > min_r && r < max_r && r < min_dist)
                    min_dist = r;
            }
            if (s == 0 && min_dist < 0.5)  // 前方扇区
                RCLCPP_WARN(get_logger(), "前方障碍: %.2fm", min_dist);
        }
    }
    rclcpp::Subscription<sensor_msgs::msg::LaserScan>::SharedPtr sub_;
};

🎯 练习题

📝 练习1:扇区分析

运行LaserSectorAnalyzer,在不同位置观察8个扇区的距离分布。

📝 练习2:安全监控

使用ObstacleMonitor实现自动停车,测试不同safe_distance的效果。

📝 练习3:自定义过滤器

实现一个只保留左半边扫描数据的过滤器,用于左侧墙壁跟踪。

🏆 成就解锁

🏅 激光雷达专家

经验值:+250 XP