第三章 传感器数据采集:让机器人拥有感知能力

做避障机器人,第一步不是写算法,而是让机器人「看见」障碍物。说白了,传感器就是机器人的眼睛。这一章我带你搞定超声波和激光雷达的数据读取,再聊聊数据滤波——嗯,这步不做,后面算法再好也白搭。

本章核心目标:

  • 用Arduino读取超声波传感器(HC-SR04)的距离数据
  • 用Python读取激光雷达(RPLIDAR A1)的点云数据
  • 掌握三种基础滤波方法:均值滤波、中值滤波、限幅滤波
传感器数据采集与滤波知识体系 传感器数据采集 超声波 (HC-SR04) Arduino 读取 → 串口发送 Python 串口接收 → 解析数据 激光雷达 (RPLIDAR) Python SDK 驱动 → 点云数据 角度 + 距离 → 极坐标转直角坐标 数据滤波基础 均值滤波 | 中值滤波 | 限幅滤波 去除噪声 → 平滑数据 → 提高稳定性 输出:稳定、可靠的障碍物距离数据

3.1 超声波传感器:Arduino 读取与串口通信

超声波传感器,原理很简单——发射声波,等它弹回来,算时间差。HC-SR04 这玩意儿便宜又好用,我最早做避障小车用的就是它。不过要注意,它有个 15° 左右的波束角,测窄柱子容易翻车。

先看 Arduino 端的代码。我个人习惯把读取逻辑封装成一个函数,这样主循环里调用起来清爽:

// Arduino 端:读取 HC-SR04 并发送到串口
#define TRIG_PIN 9
#define ECHO_PIN 10

void setup() {
  Serial.begin(115200);
  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);
}

float readDistance() {
  // 发送 10us 的触发脉冲
  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);
  
  // 读取回波时间(单位:微秒)
  long duration = pulseIn(ECHO_PIN, HIGH, 30000); // 超时 30ms
  if (duration == 0) return -1.0; // 超时返回 -1
  
  // 声速 340m/s,距离 = 时间 * 声速 / 2
  float distance = duration * 0.034 / 2.0;
  return distance;
}

void loop() {
  float dist = readDistance();
  if (dist > 0) {
    Serial.print("DIST:");
    Serial.println(dist);
  }
  delay(50); // 20Hz 采样率
}

我的经验:采样率别设太高。超声波有回波余震,20Hz 左右比较稳。我曾经贪快设到 50Hz,结果数据跳得像心电图。

Arduino 这边搞定了,接下来 Python 端要接收串口数据。这里有个坑——串口数据是文本流,你得自己解析。我习惯用 pyserial 库,按行读取:

# Python 端:读取 Arduino 串口数据
import serial
import time

ser = serial.Serial('/dev/ttyUSB0', 115200, timeout=1)
time.sleep(2)  # 等 Arduino 重启

def get_ultrasonic():
    while True:
        line = ser.readline().decode('utf-8').strip()
        if line.startswith("DIST:"):
            try:
                dist = float(line.split(":")[1])
                return dist
            except:
                return -1.0

# 测试循环
for i in range(10):
    d = get_ultrasonic()
    print(f"距离: {d:.2f} cm")
    time.sleep(0.1)

注意:串口波特率要一致。Arduino 设 115200,Python 也得 115200。我见过有人 Arduino 用 9600,Python 用 115200,结果读出来全是乱码——嗯,排查了半天才发现。

3.2 激光雷达:Python 读取 RPLIDAR 数据

超声波只能测一个方向,激光雷达就不一样了——它转一圈能扫 360°。RPLIDAR A1 是入门首选,性价比高。我用 Python 的 rplidar 库来驱动它。

先装库:

pip install rplidar-roboticia

然后看代码。读取激光雷达数据,说白了就是拿到「角度 + 距离」的配对:

# Python 端:读取 RPLIDAR A1 数据
from rplidar import RPLidar
import math

lidar = RPLidar('/dev/ttyUSB1')
lidar.start_motor()

def get_lidar_scan():
    """获取一次完整扫描(360°)"""
    scan = []
    for i, measurement in enumerate(lidar.iter_scans()):
        # measurement 格式: [(quality, angle, distance), ...]
        for (quality, angle, dist) in measurement:
            if dist > 0 and quality > 10:  # 过滤低质量点
                # 极坐标转直角坐标(可选)
                rad = math.radians(angle)
                x = dist * math.cos(rad)
                y = dist * math.sin(rad)
                scan.append((angle, dist, x, y))
        if i > 0:  # 只取一次完整扫描
            break
    return scan

# 测试
scan_data = get_lidar_scan()
print(f"采集到 {len(scan_data)} 个点")
for ang, dist, x, y in scan_data[:5]:  # 打印前5个
    print(f"角度: {ang:.1f}°, 距离: {dist:.2f}mm")

关键点:激光雷达的数据质量参差不齐。我一般会过滤掉 quality < 10 的点,这些通常是噪声或者反射太弱。你想想看,如果玻璃或者黑色物体,激光打上去反射很弱,数据就不靠谱。

为什么要把极坐标转成直角坐标?因为后面做避障算法时,用 (x, y) 坐标更方便计算障碍物的位置。比如你要判断障碍物在机器人左边还是右边,直接看 x 的正负就行。

3.3 数据滤波基础:让数据不再「抽风」

传感器数据天生带噪声。超声波可能被环境回波干扰,激光雷达可能打到灰尘上。不做滤波,你的避障算法会疯掉。我最早做项目时没滤波,机器人原地转圈——因为数据跳来跳去,算法以为到处是障碍物。

这里介绍三种最常用的滤波方法,简单但有效:

滤波方法 原理 适用场景 缺点
均值滤波 取 N 个数据的平均值 缓慢变化的数据,如静态障碍物 对突变不敏感,有延迟
中值滤波 取 N 个数据的中间值 有尖峰噪声的场景,如偶尔的误测 计算量稍大
限幅滤波 限制相邻两次数据的最大变化量 防止数据跳变,如运动中的障碍物 阈值需要手动调

看代码实现。我习惯把滤波函数写在一起,方便复用:

# 三种基础滤波实现
class DataFilter:
    def __init__(self, window_size=5, max_delta=20.0):
        self.window_size = window_size
        self.max_delta = max_delta
        self.buffer = []
        self.last_value = None
    
    def mean_filter(self, value):
        """均值滤波"""
        self.buffer.append(value)
        if len(self.buffer) > self.window_size:
            self.buffer.pop(0)
        return sum(self.buffer) / len(self.buffer)
    
    def median_filter(self, value):
        """中值滤波"""
        self.buffer.append(value)
        if len(self.buffer) > self.window_size:
            self.buffer.pop(0)
        sorted_buf = sorted(self.buffer)
        return sorted_buf[len(sorted_buf) // 2]
    
    def limit_filter(self, value):
        """限幅滤波"""
        if self.last_value is None:
            self.last_value = value
            return value
        if abs(value - self.last_value) > self.max_delta:
            # 超过阈值,用上次值代替
            return self.last_value
        self.last_value = value
        return value

# 使用示例
filter = DataFilter(window_size=5, max_delta=10.0)
raw_data = [50, 52, 48, 150, 51, 49, 53]  # 150 是噪声
for d in raw_data:
    filtered = filter.median_filter(d)
    print(f"原始: {d}, 滤波后: {filtered}")

避坑指南:我曾经在动态避障场景里只用均值滤波,结果机器人反应慢半拍,撞上了。后来改成中值滤波 + 限幅滤波的组合,效果好了很多。说白了,没有万能滤波,得根据场景组合着用。

3.4 实战:把超声波和激光雷达数据融合起来

超声波和激光雷达各有优缺点。超声波近距离准(0.02m-4m),但方向性差;激光雷达精度高,但怕玻璃和黑色物体。我一般这样搭配:

  • 前方 0-0.5m:优先用超声波,因为激光雷达在这个距离容易有盲区
  • 前方 0.5m-6m:用激光雷达,精度更高
  • 侧面:只用激光雷达,超声波波束角太大,测侧面不靠谱

来看一个简单的数据融合示例:

# 超声波 + 激光雷达 数据融合
class SensorFusion:
    def __init__(self):
        self.ultrasonic_filter = DataFilter(window_size=3)
        self.lidar_filter = DataFilter(window_size=5)
    
    def fuse_front_distance(self, us_dist, lidar_scans):
        """
        融合前方障碍物距离
        us_dist: 超声波测得的距离 (cm)
        lidar_scans: 激光雷达扫描数据 [(angle, dist), ...]
        """
        # 1. 滤波
        us_filtered = self.ultrasonic_filter.median_filter(us_dist)
        
        # 2. 从激光雷达中提取前方 ±15° 的数据
        front_dists = []
        for angle, dist in lidar_scans:
            if -15 <= angle <= 15 or 345 <= angle <= 360:
                front_dists.append(dist)
        
        if not front_dists:
            return us_filtered  # 激光雷达没数据,用超声波
        
        lidar_front = min(front_dists) / 10.0  # 转成 cm
        lidar_filtered = self.lidar_filter.mean_filter(lidar_front)
        
        # 3. 融合策略:近距离信任超声波,远距离信任激光雷达
        if us_filtered < 50:
            return us_filtered
        else:
            return lidar_filtered

注意单位:超声波通常用 cm,激光雷达通常用 mm。我吃过这个亏——融合时没统一单位,结果距离忽远忽近。统一成 cm 或者 m,别混着用。

嗯,到这里传感器数据采集和滤波的基础就讲完了。你可能会问:为什么不做卡尔曼滤波?那个更高级。别急,后面章节会专门讲。这一章先把基础打牢——数据都读不准,后面算法再牛也没用。

我个人建议你动手试一下:拿个纸箱当障碍物,分别用超声波和激光雷达测距,对比原始数据和滤波后的数据。你会发现,滤波后的数据曲线平滑多了,机器人决策也会更稳定。

本章小结:

  • Arduino 读取超声波:TRIG/ECHO 引脚 + pulseIn() 测时间
  • Python 读取激光雷达:rplidar 库 + iter_scans() 获取 360° 数据
  • 三种滤波方法:均值(平滑)、中值(去尖峰)、限幅(防跳变)
  • 传感器融合:取长补短,近距离用超声波,远距离用激光雷达

可可精品资料库