DonkeyCar进阶调校:从能跑到跑得快

深入DonkeyCar的PID调参、神经网络模型优化、数据采集策略,用实战经验帮你把模型车的圈速提升50%。

DonkeyCar 调校 PID 深度学习 自动驾驶
DonkeyCar进阶调校:从能跑到跑得快

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)

数据采集的最佳实践

  1. 多圈数:至少采集20-30圈完整赛道数据
  2. 多速度:慢速(0.3油门)、中速(0.5)、快速(0.7)各采集几圈
  3. 多路线:不要每次都走同一条线,尝试赛道内侧、外侧、中间
  4. 错误恢复:故意让车偏离,然后修正,采集恢复数据
  5. 平衡数据:左转、右转、直行的样本数量要大致相等

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

手动调参流程

  1. 先调P:增大直到响应快但不过冲
  2. 加D:抑制振荡,让响应平滑
  3. 最后加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. 硬件校准(1天):舵机中位、ESC校准、摄像头位置
  2. 数据采集(2-3天):多圈数、多速度、多路线
  3. PID调参(1天):先跑纯PID,找到基线性能
  4. 模型训练(1天):用增强数据训练,对比PID+NN混合方案
  5. 迭代优化(持续):分析失败案例,补充针对性数据

常见坑

  • 过拟合:训练集loss很低但实车表现差→增加数据多样性,加Dropout
  • 舵机抖动:PWM频率不对或电源不稳→检查60Hz频率,加电容滤波
  • 弯道冲出:速度太快或转向不足→降低弯道速度,增大转向范围
  • 直线画龙:PID的I项太大或摄像头角度不对→减小Ki,调整摄像头

DonkeyCar的调校是一个迭代过程。不要期望一次就完美,记录每次修改的参数和效果,逐步逼近最优配置。