第三章 传感器数据采集:让机器人拥有感知能力
做避障机器人,第一步不是写算法,而是让机器人「看见」障碍物。说白了,传感器就是机器人的眼睛。这一章我带你搞定超声波和激光雷达的数据读取,再聊聊数据滤波——嗯,这步不做,后面算法再好也白搭。
本章核心目标:
- 用Arduino读取超声波传感器(HC-SR04)的距离数据
- 用Python读取激光雷达(RPLIDAR A1)的点云数据
- 掌握三种基础滤波方法:均值滤波、中值滤波、限幅滤波
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° 数据
- 三种滤波方法:均值(平滑)、中值(去尖峰)、限幅(防跳变)
- 传感器融合:取长补短,近距离用超声波,远距离用激光雷达
可可精品资料库