
简介面向自动驾驶与数学建模学习者的MATLAB源码包聚焦环境感知、路径规划、决策控制等核心环节内置强化学习、CNN车辆检测等典型算法脚本。包内共6个文件以.m脚本为主4个辅以README说明与配置管理文件整体仅5KB轻量易读。已有739人学习。源码模块涵盖智能体决策训练、视觉目标检测、仿真数据生成与结果可视化能够帮助读者串联数据处理、模型训练到控制验证的完整流程。通过研读代码可理解自动驾驶中机器学习与数学建模的落地实现适合作为课程设计、竞赛准备或入门实践的参考基础。1. MATLAB 人工智能与自动驾驶源代码从能跑的 Demo 到能交付的工程关键在整条链路搜索“MATLAB 人工智能与自动驾驶源代码”的人多半不是来听概念而是带着一个具体交付任务来的课程设计要验收毕业设计要答辩横向项目要在仿真里跑通一版感知或控制演示。真正做过一遍的人都知道难点不在深度学习理论而在于把自动驾驶数据集、标注格式、模型训练和仿真验证串成一条能稳定复现的流水线——这也是 MATLAB 这套生态和纯 Python 项目最大的差异。下文按我常用的落地路径拆成四段先把自动驾驶数据集转成 MATLAB 能识别的结构再实现目标检测与车道线分割两个 AI 模块接着放进驾驶场景做闭环验证最后把源码整理成能继续迭代的 OOP 工程。新手可以对着步骤照抄熟手可以直接跳到第 4 章的翻车清单那部分全是血泪经验。2. 把自动驾驶数据集转成 MATLAB 能吃的格式标签、表格与 datastore2.1 用什么数据集起步KITTI、BDD100K 还是自定义标注很多初学者第一步就卡在数据上网上找的自动驾驶数据集大多是 KITTI 或 BDD100K 的原始结构直接丢给 MATLAB 的检测器训练函数会报一堆“标签类型不支持”“表字段缺失”之类的错。这不是 MATLAB 不好用而是它要求先拿到一个固化的表格结构每一行是一张图第二列是这个图上的所有矩形框第三列是这些框对应的类别。所有官方训练接口都认这个结构所以第一步不是写网络而是把任意数据集“掰”成这种形状。数据集标注格式我常用在哪个环节MATLAB 接入成本KITTI纯文本逐行给出类别、截断度、遮挡、2D 框和 3D 信息早期验证目标检测与相机几何低写一个解析函数即可BDD100KJSON带天气、时段、区域属性共 10 万帧多类别、多气候迁移验证中需要筛选类别并处理 JSON 层级自采图像 imageLabelerMATLAB 原生 groundTruth固定场景专用模型低但样本量小容易过拟合KITTI 的 label 文件看起来像这样每行前面是Car 0.00 0 1.67 -2.13后面跟着 8 个浮点数其中第 5 到第 8 个是 2D 框的左上角和右下角坐标。BDD100K 则是层层嵌套的 JSON一个文件里既有目标框也有可行驶区域和车道线标注对新手来说解析成本略高。我的建议很直接如果只是为了先跑通优先 KITTI如果要做“恶劣天气下模型不崩”这类说服力更强的验证再上 BDD100K。自采数据适合做特定厂区、收费站、停车场这类封闭场景缺点是标 200 张图就要半天且很容易过拟合。2.2 用转换脚本把 KITTI 标签喂给 MATLAB 的 groundTruth常见做法是写一个独立的解析函数把 KITTI 文本标签转成两个 MATLAB table再合并成一个boxLabelDatastore能直接消费的训练表。下面这个函数是我反复用过很多次的骨架按你自己的目录结构改路径即可。function [imgTbl, boxTbl] kitti2matlab(imgDir, labelDir, targetClasses) imds imageDatastore(imgDir); n numel(imds.Files); boxesCell cell(n, 1); labelsCell cell(n, 1); parfor k 1:n [~, name, ~] fileparts(imds.Files{k}); f fullfile(labelDir, [name .txt]); boxes zeros(0, 4); labels categorical.empty(0, 1); if exist(f, file) fid fopen(f, r); C textscan(fid, %s %f %f %f %f %f %f %f, ... Delimiter, , MultipleDelimsAsOne, 1); fclose(fid); cls C{1}; x1 C{5}; y1 C{6}; x2 C{7}; y2 C{8}; hit ismember(cls, targetClasses); if any(hit) boxes [x1(hit), y1(hit), x2(hit) - x1(hit), y2(hit) - y1(hit)]; labels categorical(cls(hit), targetClasses); end end boxesCell{k} boxes; labelsCell{k} labels; end imgTbl table(imds.Files, VariableNames, {imageFilename}); boxTbl table(boxesCell, labelsCell, VariableNames, {box, label}); end逻辑说明imageDatastore负责把文件夹里所有图片路径管理起来后续训练接口直接从imds.Files读文件不需要手动拼路径。用fileparts取出当前图片名再按同名规则找到 KITTI 的 txt 标签文件。textscan按空格拆分每行%s取类别字符串后面 7 个%f依次吸收截断度、遮挡、角度和四个坐标值所以C{5}到C{8}就是左上角和右下角的 x、y。KITTI 的框是“左上右下”格式而 MATLAB 的检测器统一要[x y w h]因此必须做x2-x1和y2-y1换算。参数说明targetClasses用 cell array 传比如{Car, Pedestrian}只保留你关心的类别其余类别直接丢掉。categorical(cls(hit), targetClasses)里的第二个参数必须和后面网络numClasses的数量、顺序完全一致否则训练阶段会出现“找不到类别”的玄学报错。parfor在有 Parallel Computing Toolbox 时可以加速文件量大但每张图处理很轻时特别合适如果担心并行导致机器卡死改成普通for也完全没问题。2.3 用 boxLabelDatastore 做批处理别让自己手写数据循环数据转换完以后不要把表格存成一堆mat文件就以为完事了。接下来要把合并后的表包进boxLabelDatastore它的作用是训练时按批次读图、解析框、对齐类别并且自动过滤掉异常行。人工写循环逐张读图再喂给trainYOLOv2ObjectDetector是很常见的错误做法性能差且容易漏样本。[imgTbl, boxTbl] ... kitti2matlab(data/image_2, data/label_2, {Car, Pedestrian}); fullTbl [imgTbl, boxTbl]; % 去掉完全没有标注框的样本减少无效负样本 hasBox cellfun((b) ~isempty(b), fullTbl.box); fullTbl fullTbl(hasBox, :); % 看一眼类别分布防止 Car 和 Pedestrian 比例严重失衡 summary(fullTbl.label) blds boxLabelDatastore(fullTbl);逻辑说明cellfun检测每张图对应的框矩阵是否为空空样本在检测训练里基本不提供正梯度数量多了还会拖慢收敛。summary能快速列出每个类别的样本数如果 Pedestrian 只有 50 个框而 Car 有 5000 个后面训练出来的模型基本等于瞎子这时要先做类别重采样或增加对应类别样本。参数说明boxLabelDatastore要求box列必须是数值矩阵label列必须是categorical类型。表格里不能混入NaN或负数坐标KITTI 偶尔会标出完全在图外的框如果不放心可以在喂给 datastore 之前加一步过滤fullTbl.box{k} fullTbl.box{k}(fullTbl.box{k}(:,3) 0, :)把宽度或高度小于等于零的框清掉。这个坑我踩过一次当时怎么查都找不到原因最后是用dbstop if error一行行定位到某张图的框宽为零。3. 写自动驾驶感知代码YOLO 检测与车道线分割的最小实现3.1 感知任务怎么分工自动驾驶里的“人工智能”落到 MATLAB 代码上最少需要两条感知线目标检测用来找车、人、交通标志语义分割用来找车道线和可行驶区域。检测输出的是离散框适合做避障和跟车决策分割输出的是逐像素类别适合做车道保持和路径规划。做课程设计或毕设演示时我一般先做检测因为它对标注噪声容忍度更高复现出来“能看到框在动”汇报效果更直观。任务典型模型输出形式在自动驾驶里的用途目标检测YOLO v2 / SSD / Faster R-CNN矩形框 类别 置信度车辆识别、行人预警、标志检测语义分割DeepLabv3 / 自定义分割网络每个像素一个类别 ID车道线、可行驶区域、障碍物轮廓实例分割Mask R-CNN需额外工具箱每个实例独立掩码精确避障但实现成本高MATLAB 的 Computer Vision Toolbox 对 YOLO v2 和 DeepLabv3 都有官方支持层不需要自己从零写卷积和反向传播。这一点比很多人想象中省事你要写的是数据整理、参数配置、训练循环和结果验证这几段“胶水代码”而不是重新发明网络结构。3.2 用 YOLO v2 训练目标检测最小可跑源代码下面这段是我在本地跑通的最小实现。它假设你已经生成了fullTbl并包好了boxLabelDatastore接下来直接搭网络并训练。% 从上一章的数据表生成 datastore data load(kitti_train_table.mat); blds boxLabelDatastore(data.fullTbl); % 网络输入尺寸要能被 32 整除416x416 是速度和精度的折中 inputSize [416 416 3]; numClasses 2; % 特征层选 resnet50 中语义信息够但不算最深的层 featureLayer activation_40_relu; % 先用一组粗 anchor 起步后面用统计方法重新聚类 anchorBoxes [30 30; 60 45; 90 80; 160 120; 220 180]; lgraph yolov2Layers(inputSize, numClasses, ... resnet50, featureLayer, anchorBoxes); options trainingOptions(sgdm, ... MiniBatchSize, 8, ... InitialLearnRate, 1e-4, ... MaxEpochs, 15, ... Shuffle, every-epoch, ... VerboseFrequency, 50, ... Plots, training-progress); detector trainYOLOv2ObjectDetector(blds, lgraph, options); save(detector.mat, detector);逻辑说明yolov2Layers会自动读入预训练的 ResNet-50 作为骨干从activation_40_relu这一层开始接入检测头。选这个层而不是更浅层是因为浅层特征关注边缘纹理适合小目标但不适合中等大小的车辆选更深层又会导致特征图太小小目标直接消失。trainYOLOv2ObjectDetector内部完成 anchor 匹配、正负样本采样、loss 计算和反向传播训练过程在Plots里可以实时看到 loss 下降曲线。参数说明MiniBatchSize8在 8G 显存上比较稳妥如果显存只有 4G改成 4 或 2否则第一步就报“Out of memory on GPU”。InitialLearnRate1e-4是迁移学习的常用起点因为骨干网络权重已经是训练好的学习率太大会把前面的特征全冲乱。featureLayer的具体名称可以用analyzeNetwork(lgraph)查看不同 MATLAB 版本或不同预训练模型层名可能有差异。3.3 用 DeepLabv3 训练车道线分割最小可跑源代码车道线分割需要逐像素标注数据。如果你没有现成 mask可以用imageLabeler打开图片用Pixel Label工具把路面刷成road类其余刷成background导出成一个像素标签 datastore。下面代码假设你已经拿到了pxds。% 假设像素类别是 road 和 backgroundlabelID 分别是 1 和 2 pxds pixelLabelDatastore(train_img_list.txt, ... {road, background}, [1 2]); % DeepLabv3 在 resnet50 上搭 ASPP 模块 lgraph deeplabv3plusLayers([480 640 3], 2, resnet50); opts trainingOptions(adam, ... InitialLearnRate, 1e-3, ... MaxEpochs, 12, ... MiniBatchSize, 4, ... Shuffle, every-epoch, ... Plots, training-progress); net trainNetwork(pxds, lgraph, opts); save(lane_net.mat, net);逻辑说明deeplabv3plusLayers的第二个参数2是类别总数必须和pixelLabelDatastore里类别列表的长度一致。这里用trainNetwork而不是trainYOLOv2ObjectDetector因为分割任务是逐像素分类接口更通用。DeepLabv3 内部结构是编码器-解码器ASPP 用不同膨胀率的空洞卷积抓多尺度上下文非常适合车道线这种细长目标。参数说明输入尺寸[480 640 3]我一般按实际相机分辨率的一半取太大显存吃不住太小车道线会断成虚线。MiniBatchSize4是因为分割每张图的内存占用比检测高很多8G 显存也只能勉强跑这个数。adam优化器在分割任务里收敛比sgdm快学习率可以给到1e-3前提是骨干网络用了预训练权重。3.4 训练参数调整的底层逻辑anchor、学习率、批次之间的关联很多人训练翻车后第一反应是换网络其实 80% 的问题出在 anchor 和学习率上。anchor 应该从你训练集的真实框尺寸统计出来而不是胡乱猜。MATLAB 提供了estimateAnchorBoxes它会基于所有标注框的宽高做聚类得到一组和你的数据分布匹配的初始框。% 从 boxLabelDatastore 读出所有框统计宽高并聚类出 anchor tbl readall(blds); boxData tbl.box; if ~isempty(boxData) anchorBoxes estimateAnchorBoxes(boxData, 5); disp(anchorBoxes); end逻辑说明readall会把整个训练集的框一次性读进内存返回的是一个 cell 数组每个 cell 对应一张图上的所有框。estimateAnchorBoxes内部对这些框的宽高做 k-means 聚类聚成几个簇每个簇的中心就是一个 anchor。把这个结果替换掉 3.2 里的手写 anchor训练会更稳尤其当你的目标主要是行人这种瘦长形状时默认正方形 anchor 会非常吃亏。参数说明第二个参数5表示聚成 5 类。类别多、目标尺寸跨度大时可以加到 9 或 12但 anchor 太多会拖慢训练因为每个 anchor 都要参与正负样本匹配。调 anchor 时记得同步调trainingOptions里的InitialLearnRate和MaxEpochsanchor 越贴近真实分布可以用稍大的学习率anchor 不准时学习率大了容易震荡表现为 loss 前期下降快后期突然弹回去。4. 常见问题排查与避坑自动驾驶 MATLAB 源码的五处高频翻车点4.1 现象检测框全部堆在图片左上角且宽度高度明显不对我刚转 KITTI 数据时遇到过这个现象训练出的模型在每张图左上角画出一堆边长一两个像素的小框。原因几乎都是坐标列读错了。KITTI 每行的第 5、6 列是 x1、y1第 7、8 列才是 x2、y2如果按C{4}到C{7}解析读进去的是角度和截断度数值范围完全对不上训练时模型只能学到“框永远在左上角附近最安全”。解决方法是先随机找一张图disp(C)把整个 cell 内容打印出来人工确认第 5 到第 8 个字段是不是符合 2D 框坐标的整数范围再继续往下跑。4.2 现象训练 loss 一开始就是 NaN或者第一轮就中断这类报错在换数据集时特别常见。原因通常是类别列表顺序和标注里的字符串对不上比如targetClasses写的是{Car,Pedestrian}但 KITTI 文件里写的是Pedestrian和Carcategorical函数依然会生成类别可排序后和网络分类头的顺序不一致logits 和 label 对不上loss 计算直接溢出。解决方法是训练前先执行一次unique(fullTbl.label)打印出所有类别确认和yolov2Layers里的numClasses、以及targetClasses的顺序完全一致。这个检查看起来笨但能省掉半天排查时间。4.3 现象GPU 显存报错或者 CPU 训练“一晚上一个 epoch 都没跑完”显存不够的经典组合是输入图片 1280 宽、MiniBatchSize16、Plotstraining-progress同时开。训练曲线绘图本身也会占用额外显存而且每轮都刷新图形界面CPU 模式下尤其拖慢速度。解决方法是先降MiniBatchSize到 4输入尺寸改成 416 或 224再关掉Plots改看VerboseFrequency打印的 loss。如果机器完全没 GPUExecutionEnvironment设成cpu同时把MaxEpochs降到 5 先验证代码通路等逻辑通了再上大机器训练。我一般会在代码里加一行gpuDevice判断有卡才走 GPU 分支没卡也不至于崩。4.4 现象DeepLabv3 分割结果像马赛克路面区域空洞多分割模型跑出来乱码先检查像素标签 datastore 的 labelID 映射。pixelLabelDatastore的第三个参数[1 2]是期望的像素值但如果你的标注图里用的是[0 255]或类别顺序反了网络就会学到完全错误的映射。常见的还有灰度图输入有的样例数据是单通道灰度而deeplabv3plusLayers默认输入是 RGB 三通道少一个维度直接报“输入通道不匹配”。解决方法是训练前先preview(pxds)把读取到的像素标签可视化一遍确认类别颜色和语义对得上再开训练。4.5 现象仿真里集成检测器结果detect返回空矩阵训练时表现不错的模型放进drivingScenario仿真后经常一帧都检测不到。原因有两类一是仿真相机渲染的图像和训练集风格差距太大模型没见过这种“干净”的网格路面特征分布偏移二是detect默认置信度阈值 0.5 太高仿真里物体纹理简单置信度普遍偏低。解决方法是先显式设置Threshold, 0.1看有没有低置信度输出如果 0.1 都没有再去查仿真相机的分辨率、焦距和安装高度是否和训练图尺度接近。这一步能用insertObjectAnnotation把框直接画到仿真图像上每帧实时看比盯着 loss 曲线直观得多。5. 代码跑起来之后仿真回环验证与检测指标怎么量5.1 用 drivingScenario 快速搭一条可重复的验证路模型训练完成后不要只在测试集上看几张图就收工。我会把它塞进 MATLAB 的drivingScenario里做仿真验证原因是仿真场景完全可控可以设置同一路段、不同车距、不同光照得到一套可重复测试数据集。下面是最小的建场景代码sc drivingScenario; % 一条 180 米的双向直路两个车道 roadCenters [0 0 0; 180 0 0]; road(sc, roadCenters, Lanes, 2); % 自车放到起点前方 40 米放一辆目标车 ego vehicle(sc, Position, [5 0 0], Yaw, 0); target vehicle(sc, Position, [40 0 0], Velocity, [10 0 0]); % 给自车装一个前视相机模拟真实安装位 cam camera(ego, Position, [1.5 0 1.4], ... Orientation, [0 0 0], FocalLength, 600); plot(sc, Waypoints, on);逻辑说明drivingScenario是 MATLAB 自动驾驶仿真回环的地基road函数只要给一系列路径点就能生成路段vehicle用来定义自车和目标车camera把相机挂在自车坐标系的某个安装点上。这里的关键是camera的参数要和真实采集系统的内外参接近否则仿真图像训练出来的模型迁移不到实车。参数说明FocalLength600是像素单位相当于一个中等焦距的前视相机视角不够宽但远处目标清晰。如果你想验证近距离行人可以把焦距降到 400 左右视野更宽但目标变小。Position的 y 坐标决定车辆在哪个车道两条车道分别是 y±1.75我习惯把自车放 y0目标车放 y0走同一车道做追尾场景。5.2 纯 MATLAB 闭环检测结果接一个最简单的转向控制仿真里只看到框还不够我会把检测结果接一个最简单的 Pure Pursuit 控制让车能跟着前车或车道走。下面的代码演示了在仿真推进循环里做检测、画框、计算前轮转角的最小闭环detector coder.loadDeepLearningNetwork(detector.mat); while advance(sc) img snapshot(cam); % 当前相机帧 [bboxes, scores, labels] detect(detector, img, Threshold, 0.3); % 只保留置信度大于 0.5 的目标避免噪声框 keep scores 0.5; bboxes bboxes(keep, :); labels labels(keep); % 取最近的检测目标用它的横向位置计算转向指令 if ~isempty(bboxes) nearestIdx find(abs(bboxes(:,1) bboxes(:,3)/2 - size(img,2)/2) ... min(abs(bboxes(:,1) bboxes(:,3)/2 - size(img,2)/2))); steerCmd - (bboxes(nearestIdx(1),1) bboxes(nearestIdx(1),3)/2 - size(img,2)/2) ... / size(img,2) * 0.5; else steerCmd 0; end % 可视化当前帧 img insertObjectAnnotation(img, rectangle, bboxes, labels); imshow(img); title(sprintf(Steer Cmd: %.2f, steerCmd)); drawnow; end逻辑说明coder.loadDeepLearningNetwork加载保存的检测器比每次循环都重新load快很多它只加载一次网络结构后续detect调用都走同一份内存。snapshot(cam)拿到当前仿真帧detect返回框、分数和类别再按分数过滤。转向指令用“目标中心离图像中心的偏差”计算属于比例控制的最简写法不做动力学建模只验证感知到控制这条链路通不通。参数说明Threshold在仿真里建议给 0.3 而不是 0.5因为仿真渲染的图像纹理偏干净模型输出的置信度普遍没有真实数据高。steerCmd的 0.5 是比例增益这个值取决于你想要的转向灵敏度200ms 仿真步长下 0.5 比较稳改成 1.0 会出现明显蛇形摆动。5.3 mAP 和 IoU怎么量化一个检测模型是否真的合格闭环跑通之后还要回答一个问题这模型到底行不行我一般用evaluateDetectionPrecision计算测试集 mAP同时单独看几个典型难例。它计算的是每个类别的平均精度代码量很小% testGTruth 是测试集的 groundTruth 表结构和训练表一致 results detect(detector, testImds, Threshold, 0.01); metrics evaluateDetectionPrecision(results, testGTruth, 0.5); disp(metrics.ClassMetrics); fprintf(mAP0.5 %.3f\n, mean(metrics.ClassMetrics.AP));逻辑说明detect的Threshold要设得极低比如 0.01否则高阈值会拦掉一部分漏检框mAP 会被虚高。evaluateDetectionPrecision会逐个类别绘制 PR 曲线返回的ClassMetrics.AP是每个类别的平均精度。这里的 0.5 是 IoU 阈值意思是检测框和真值框的交并比超过 0.5 才算命中了。参数说明IoU 阈值从 0.5 改到 0.75mAP 往往会掉 10 个百分点以上这是正常现象。如果你要汇报“模型达到多少 mAP”必须写清楚 IoU 阈值否则数字没有可比性。更关键的是测试集不能从训练集里抽我见过有人把训练数据 shuffle 后直接拿来评估mAP 高达 0.95换到仿真场景立刻归零这个“好评测”没有任何交付价值。测试集要么专门留出一段未参与训练的视频要么用仿真场景生成全新数据。6. 把源代码变成能继续迭代的工程OOP 封装、参数隔离与模型管理6.1 用 handle 类把感知链路包起来项目做到第三天就会意识到直接裸写脚本是在给自己埋雷检测器、阈值、类别列表散落在十几个脚本里改一个参数要在全部代码里搜索替换。我现在会先用一个 handle 类把感知模块封装成只有step接口的黑匣子调用方完全不关心内部是 YOLO 还是语义分割只负责喂图和取结果。classdef Perception handle properties (Access private) Detector ConfThr end methods function obj Perception(modelFile, confThr) obj.Detector coder.loadDeepLearningNetwork(modelFile); obj.ConfThr confThr; end function out step(obj, img) [bboxes, scores, labels] detect(obj.Detector, img, ... Threshold, obj.ConfThr); out struct(Boxes, bboxes, Scores, scores, Labels, labels); end end end逻辑说明handle类之所以重要是因为 MATLAB 默认做值拷贝普通的classdef对象在传入函数时会把整个网络复制一份训练好的模型动辄几十 MB每次调用都会卡。handle类是引用语义所有地方共享同一个对象消灭了隐性拷贝。Access private把模型内部字段藏起来调用方只能通过step获取结果防止有人手欠改了模型参数。6.2 把 anchor、阈值、路径全部收进一个配置结构训练参数是项目里最容易失控的部分。我现在规定所有可调参数都必须登记在一个配置函数里任何实验都对应一份参数快照跑完直接存成params_20250601.mat。这件事做起来很简单但对后续复现实验是救命级的。function p defaultParams() p.inputSize [416 416 3]; p.numClasses 2; p.anchorBoxes [30 30; 60 45; 90 80; 160 120; 220 180]; p.confThr 0.3; p.iouThr 0.5; p.trainDataDir data/kitti_train/; p.valDataDir data/kitti_val/; end这样做的直接收益是复现实验只需要改defaultParams一行而不用翻遍所有脚本。我吃过最深刻的教训是调大 anchor 后忘了同步yolov2Layers里的参数网络结构里用的还是旧 anchor训练了两天精度毫无提升最后发现是这种低级失误。从那以后所有涉及训练的参数一律先进配置结构体再传给网络函数训练命令永远只写一行trainFromParams(defaultParams())出了问题先看参数再查代码。希望这篇笔记能帮你绕开我走过的弯路也希望你跑通之后能再往前一步——把源码整理成一份能交给下一个人的工程才算是真正把这个方向吃透了。本文还有配套的精品资源点击获取