DonkeyCar进阶调校:从能跑到跑得快
深入DonkeyCar的PID调参、神经网络模型优化、数据采集策略,用实战经验帮你把模型车的圈速提升50%。
DonkeyCar 调校 PID 深度学习 自动驾驶
DonkeyCar进阶调校:从能跑到跑得快
你的DonkeyCar已经能沿着赛道跑了,但总是冲出弯道、速度上不去、或者在某些路段表现不稳定?这篇文章分享从”能跑”到”跑得快”的实战调校经验,涵盖硬件调整、PID参数、数据采集策略和模型优化。
硬件层面的优化
舵机响应校准
舵机的响应速度和精度直接影响循线性能。很多新手忽略舵机的中位校准:
# donkeycar的config.py中配置舵机参数
STEERING_CHANNEL = 1
STEERING_LEFT_PWM = 460 # 左转最大角度对应的PWM
STEERING_RIGHT_PWM = 290 # 右转最大角度对应的PWM
# 关键:确保中位准确
STEERING_CENTER = 370 # 中位PWM值
# 测试脚本:校准舵机中位
import Adafruit_PCA9685
import time
pwm = Adafruit_PCA9685.PCA9685()
pwm.set_pwm_freq(60)
# 逐步测试中位
for center in range(360, 380):
pwm.set_pwm(1, 0, center)
print(f"测试中位: {center}")
time.sleep(1)
# 观察车轮是否真正直行
电机驱动优化
ESC(电子调速器)的校准同样重要:
# 在config.py中
THROTTLE_FORWARD_PWM = 400 # 前进油门
THROTTLE_STOPPED_PWM = 370 # 停止
THROTTLE_REVERSE_PWM = 340 # 后退(如果需要)
# 校准ESC流程
import Adafruit_PCA9685
import time
pwm = Adafruit_PCA9685.PCA9685()
pwm.set_pwm_freq(60)
print("ESC校准开始")
print("1. 发送最大油门,按住ESC校准按钮")
pwm.set_pwm(0, 0, 500)
time.sleep(3)
print("2. 发送最小油门")
pwm.set_pwm(0, 0, 200)
time.sleep(2)
print("3. 校准完成,测试前进")
pwm.set_pwm(0, 0, 370) # 停止
time.sleep(1)
pwm.set_pwm(0, 0, 400) # 前进
time.sleep(2)
pwm.set_pwm(0, 0, 370) # 停止
摄像头位置调整
摄像头的高度和角度对视觉输入影响巨大:
| 位置 | 优点 | 缺点 |
|---|---|---|
| 低+前倾 | 近距离细节清晰 | 视野窄,弯道预判差 |
| 高+平视 | 视野广,弯道预判好 | 近距离细节模糊 |
| 推荐:中等高度+15°前倾 | 平衡视野和细节 | — |
用3D打印可调支架,测试不同高度(5cm、8cm、12cm)和角度(0°、15°、30°),记录每种配置下的圈速。
数据采集策略
数据质量决定模型上限。DonkeyCar默认采集方式太简单,需要改进:
改进的采集脚本
# custom_data_collection.py
import donkeycar as dk
from donkeycar.parts.camera import PiCamera
from donkeycar.parts.throttle_actuator import PCA9685_PWM_STEERING_THROTTLE
import time
import numpy as np
cfg = dk.load_config()
V = dk.vehicle.Vehicle()
# 摄像头
cam = PiCamera(resolution=cfg.CAMERA_RESOLUTION)
V.add(cam, outputs=['cam/image_array'])
# 用户输入(遥控手柄)
from donkeycar.parts.controller import get_js_controller
ctr = get_js_controller(cfg)
V.add(ctr,
inputs=['cam/image_array'],
outputs=['user/angle', 'user/throttle', 'user/mode', 'recording'],
threaded=True)
# 自定义数据增强和过滤
class SmartRecorder:
def __init__(self, max_samples=20000):
self.samples = []
self.max_samples = max_samples
self.last_record_time = 0
self.min_interval = 0.05 # 最小采集间隔
def run(self, img_array, angle, throttle, recording):
if not recording:
return
now = time.time()
if now - self.last_record_time < self.min_interval:
return
# 过滤:速度太慢或太快的样本不采集
if abs(throttle) < 0.1 or abs(throttle) > 0.9:
return
# 数据增强:添加噪声版本
self.samples.append((img_array.copy(), angle, throttle))
# 轻微角度扰动(增加鲁棒性)
if np.random.random() < 0.3:
angle_noise = angle + np.random.normal(0, 0.02)
self.samples.append((img_array.copy(), angle_noise, throttle))
self.last_record_time = now
if len(self.samples) >= self.max_samples:
self.save()
self.samples = []
def save(self):
print(f"保存 {len(self.samples)} 个样本")
# 保存到tub
tub = dk.parts.Tub(cfg.DATA_PATH + '/tub_' + str(int(time.time())))
for img, angle, throttle in self.samples:
tub.put_record({
'cam/image_array': img,
'user/angle': angle,
'user/throttle': throttle
})
recorder = SmartRecorder()
V.add(recorder, inputs=['cam/image_array', 'user/angle', 'user/throttle', 'recording'])
# 启动
V.start(rate_hz=20)
数据采集的最佳实践
- 多圈数:至少采集20-30圈完整赛道数据
- 多速度:慢速(0.3油门)、中速(0.5)、快速(0.7)各采集几圈
- 多路线:不要每次都走同一条线,尝试赛道内侧、外侧、中间
- 错误恢复:故意让车偏离,然后修正,采集恢复数据
- 平衡数据:左转、右转、直行的样本数量要大致相等
PID控制——超越纯神经网络
纯端到端神经网络(图像→转向角)在DonkeyCar中容易过拟合。混合PID+神经网络的方式更稳定:
# pid_lane_controller.py
class PIDController:
def __init__(self, kp, ki, kd):
self.kp = kp
self.ki = ki
self.kd = kd
self.integral = 0
self.prev_error = 0
self.last_time = None
def compute(self, error, dt):
"""
error: 当前误差(比如车道中心偏移)
dt: 时间间隔
"""
self.integral += error * dt
derivative = (error - self.prev_error) / dt if dt > 0 else 0
output = self.kp * error + self.ki * self.integral + self.kd * derivative
self.prev_error = error
return output
def reset(self):
self.integral = 0
self.prev_error = 0
# 结合视觉检测和PID
class PIDLaneFollower:
def __init__(self):
self.pid = PIDController(kp=0.8, ki=0.01, kd=0.2)
self.last_time = time.time()
def run(self, lane_offset):
"""
lane_offset: 车道中心偏移量(像素)
返回: 转向角(-1到1)
"""
now = time.time()
dt = now - self.last_time
self.last_time = now
# 归一化偏移量到-1~1
normalized_error = lane_offset / 160.0 # 假设图像宽度160
steering = self.pid.compute(normalized_error, dt)
# 限制输出范围
return max(-1.0, min(1.0, steering))
# 使用
pid_follower = PIDLaneFollower()
# 在主循环中
while True:
ret, frame = cam.read()
left_line, right_line = detect_lane_lines(frame)
if left_line and right_line:
# 计算车道中心偏移
lane_center = (left_line[0][0] + right_line[0][0]) // 2
offset = lane_center - frame.shape[1] // 2
steering = pid_follower.run(offset)
car.set_steering(steering)
PID参数调优方法
Ziegler-Nichols方法:
# 1. 先只设P,逐渐增大直到系统振荡
kp_critical = 1.2 # 找到临界增益
oscillation_period = 0.5 # 振荡周期(秒)
# 2. 计算PID参数
kp = 0.6 * kp_critical
ki = 2 * kp / oscillation_period
kd = kp * oscillation_period / 8
print(f"推荐参数: Kp={kp:.2f}, Ki={ki:.2f}, Kd={kd:.2f}")
# 输出: 推荐参数: Kp=0.72, Ki=2.88, Kd=0.09
手动调参流程:
- 先调P:增大直到响应快但不过冲
- 加D:抑制振荡,让响应平滑
- 最后加I:消除稳态误差(通常I很小或为0)
神经网络模型优化
模型架构选择
DonkeyCar默认模型是简单的CNN,可以尝试更复杂的架构:
# models.py - 自定义模型
from tensorflow.python.keras.models import Model
from tensorflow.python.keras.layers import Input, Dense, Dropout, Conv2D, BatchNormalization
from tensorflow.python.keras.layers import MaxPooling2D, Flatten, Concatenate
def build_advanced_model(input_shape=(120, 160, 3)):
"""改进的CNN模型,加入BatchNorm和Dropout"""
img_in = Input(shape=input_shape, name='img_in')
# 特征提取
x = Conv2D(24, (5, 5), strides=(2, 2), activation='relu')(img_in)
x = BatchNormalization()(x)
x = Conv2D(32, (5, 5), strides=(2, 2), activation='relu')(x)
x = BatchNormalization()(x)
x = Conv2D(64, (5, 5), strides=(2, 2), activation='relu')(x)
x = BatchNormalization()(x)
x = Conv2D(64, (3, 3), strides=(2, 2), activation='relu')(x)
x = BatchNormalization()(x)
x = Conv2D(64, (3, 3), strides=(1, 1), activation='relu')(x)
x = BatchNormalization()(x)
x = Flatten()(x)
x = Dense(100, activation='relu')(x)
x = Dropout(0.3)(x)
x = Dense(50, activation='relu')(x)
x = Dropout(0.2)(x)
# 输出
angle_out = Dense(1, activation='linear', name='angle_out')(x)
throttle_out = Dense(1, activation='linear', name='throttle_out')(x)
model = Model(inputs=[img_in], outputs=[angle_out, throttle_out])
model.compile(optimizer='adam', loss='mean_squared_error')
return model
# 训练
model = build_advanced_model()
model.fit(
train_generator,
validation_data=val_generator,
epochs=50,
callbacks=[
tf.keras.callbacks.EarlyStopping(patience=5),
tf.keras.callbacks.ReduceLROnPlateau(factor=0.5, patience=2)
]
)
数据增强策略
# 在donkeycar的parts/augment.py中添加
import cv2
import numpy as np
def augment_image(img):
"""多策略数据增强"""
# 1. 亮度调整
brightness = np.random.uniform(0.7, 1.3)
img_hsv = cv2.cvtColor(img, cv2.COLOR_RGB2HSV)
img_hsv[:, :, 2] = img_hsv[:, :, 2] * brightness
img = cv2.cvtColor(img_hsv, cv2.COLOR_HSV2RGB)
# 2. 轻微旋转(模拟摄像头抖动)
angle = np.random.uniform(-5, 5)
M = cv2.getRotationMatrix2D((img.shape[1]//2, img.shape[0]//2), angle, 1)
img = cv2.warpAffine(img, M, (img.shape[1], img.shape[0]))
# 3. 水平翻转(同时翻转角度标签)
if np.random.random() < 0.5:
img = cv2.flip(img, 1)
# 注意:角度标签也要取反
# 4. 添加高斯噪声
noise = np.random.normal(0, 10, img.shape).astype(np.uint8)
img = cv2.add(img, noise)
return img
实战调校清单
按优先级排序:
- 硬件校准(1天):舵机中位、ESC校准、摄像头位置
- 数据采集(2-3天):多圈数、多速度、多路线
- PID调参(1天):先跑纯PID,找到基线性能
- 模型训练(1天):用增强数据训练,对比PID+NN混合方案
- 迭代优化(持续):分析失败案例,补充针对性数据
常见坑
- 过拟合:训练集loss很低但实车表现差→增加数据多样性,加Dropout
- 舵机抖动:PWM频率不对或电源不稳→检查60Hz频率,加电容滤波
- 弯道冲出:速度太快或转向不足→降低弯道速度,增大转向范围
- 直线画龙:PID的I项太大或摄像头角度不对→减小Ki,调整摄像头
DonkeyCar的调校是一个迭代过程。不要期望一次就完美,记录每次修改的参数和效果,逐步逼近最优配置。