简介:本资源是一套面向计算机视觉初学者与自动驾驶算法实践者的MATLAB道路语义分割实战方案,聚焦图像像素级分类任务,解决无人驾驶场景中道路区域精准识别的核心问题。资源包共164个文件,包含101张PNG与54张JPG格式的道路实拍图像(含多角度、光照及天气变化样本)、5个MAT格式标注数据与模型权重、2个核心M脚本(含数据预处理与训练主流程)及2个交互式MLX教学笔记,总容量59.58MB,结构清晰、开箱即用。已有6428人学习下载,覆盖高校课程实践、智能交通项目原型开发与深度学习入门训练。读者可直接复现基于ResNet-18的端到端语义分割流程:从图像与像素标签加载、网络微调、交叉熵损失训练,到IoU指标评估与可视化预测结果,配套代码完整、注释详尽,并提供典型道路图像测试集与标准评估逻辑。 我自己的第一款正统语义分割模型,就是在MATLAB里跑的。当年被课程作业推着去啃DeepLabV3+的时候,最大的感受是:网上讲语义分割的教程,十个里有九个是Python生态,MATLAB相关的要么是官网文档那种干巴巴的demo,要么就是零散的代码片段,没人告诉你中间那些坑怎么绕。这篇文章把我做道路语义分割这一路的思路、代码骨架和踩坑记录都写下来,给同样被困在MATLAB生态里的同学做个参考。
先说清楚这篇文章解决的“问题”是什么:给你一张街景照片或行车记录仪画面,让模型逐像素判断每个点属于道路、天空、车辆、行人、建筑还是其他。这套能力在自动驾驶感知、地图制作、安防监控里都有实际用途。如果你手上正好有MATLAB,想快速跑通一个语义分割实例,不想折腾环境,也不想写几百行底层网络,那这篇就是给你准备的。新手能照着把流程走通,有基础的也能在训练策略和后处理这几节里捞到点东西。
1. 为什么在MATLAB里做道路语义分割:三条路线我先帮你捋清楚
很多人的第一反应是:语义分割不是该用PyTorch或者TensorFlow吗,为什么要用MATLAB?这问题我当初也纠结过。如果你是要发论文、上顶会,那确实没必要跟MATLAB死磕,Python那套生态在模型自定义和部署上灵活太多。但如果你的场景是课程设计、本科毕设、或者一个需要快速验证的工程demo,MATLAB反而有它的独特优势:内置的深度网络工具箱把数据标注、训练、评估、可视化全都包在同一个环境里了,不需要自己去拼装各个库的接口。再加上它自带的Ground Truth Labeler和图像标注器,做像素级标注比在Python里手写工具省事不少。
1.1 大多数教程没告诉你的“入口问题”
在动手之前,得先搞清楚版本和工具箱。道路语义分割至少需要以下三个工具箱:Deep Learning Toolbox(深度学习工具箱,提供网络层、训练器)、Image Processing Toolbox(图像处理工具箱,用来读图、做后处理)、Computer Vision Toolbox(计算机视觉工具箱,提供标定、标注工具和部分网络层)。如果你用的是R2018b之后的版本,还可以直接用deeplabv3plusLayers这个内置函数,连网络结构都不用自己拼。
很多新手的第一个坎其实是安装。MATLAB的组件安装界面里,工具箱不是默认全选的,如果你安装时装了几个基础的模块,后面一调用函数就报“Undefined function or variable”,十有八九是工具箱没装全。解决方式很简单:在MATLAB的“主页—预设—工具箱”里查看已安装的组件,缺哪个补安哪个,或者直接重新运行安装程序选择“修改”。这步不解决,后面前功尽弃。
1.2 三项路线怎么选,直接决定你的工作量
在MATLAB里做完道路语义分割,主流的实现路线有三条:
路线A:使用内置的
deeplabv3plusLayers和segnetLayers快速搭建网络,配合训练数据直接训练。优点是代码量极少,5分钟就能把网络定义出来;缺点是模型结构相对固定,想改网络深度和膨胀率得看文档。路线B:自己定义层(用
layerGraph拼接卷积层、反卷积层、残差连接),实现U-Net或自研网络。优点是灵活性高,能完全掌控网络行为;缺点是需要对MATLAB的层语法熟悉,调试也要耐着性子看网络结构图。路线C:加载预训练模型,用
trainNetwork做迁移学习或直接进行推理。如果你有与目标场景接近的预训练模型,可以用它来做冷启动,只微调最后一两层。道路分割中比较常见的是用vgg16、resnet50等分类网络作为Encoder骨干,然后接解码器。
我自己的建议是:如果只是为了把道路分割跑通、出结果,优先选路线A;如果你想深入学习语义分割的原理,或者你的数据场景比较特殊(比如无人机视角、夜间道路),那就走路线B。路线C更适合用来理解迁移学习的机制,真正解决实际问题时反而显得别扭——预训练的Encoder可能跟你手里的设备视角差异太大。
2. 数据准备四板斧:图像数据源、像素级标签和Labeler工具的隐藏坑
训练语义分割模型,最繁琐的环节不是网络设计,是数据。道路分割的数据需要两部分:原始图像,以及和原始图像一一对应的像素级标签图。标签图里每个像素的取值是类别编号(比如0代表背景、1代表道路、2代表车辆),而不是RGB颜色。这一点很多人一开始没理解,会导致后面训练时维度对不上报错。
2.1 手头没有数据集时的三条出路
如果你只是做练习,不需要自己上街采集数据。常见的开源数据集有Cityscapes、CamVid、BDD100K,其中CamVid是早期比较经典的城市道路场景集,图像尺寸不大,下载也方便,特别适合在MATLAB里做流程验证。Cityscapes的分辨率更高,场景更复杂,但数据量太大,训练时间也更长,不太适合实验室电脑跑。
如果你的场景是“毕设自拟题目”,想针对某条具体道路做分割,那么可以用手机或行车记录仪采集一批画面。需要注意的细节是:采集时尽量让光照、天气、角度多样化,不要一直对着一条笔直大路拍,否则模型学到的道路特征非常单一,换个路口效果就崩。
还有一种方式,是使用MATLAB官方的示例数据集。在命令行敲openExample('vision/...')或者查看Image Labeler自带的示例图片,可以快速获得几张已经标注好的道路图片。虽然数量少,但用来理解数据格式、跑通整个流程绰绰有余。
2.2 pixelLabelDatastore的格式陷阱
在MATLAB里,训练数据通常用imageDatastore存放原始图像,用pixelLabelDatastore存放像素标签图。这两者配合pixelLabelImageDatastore可以非常方便地把图像和标签对应起来,喂给训练器。
这里有一个极容易踩的坑:标签图的文件格式要满足特定的约定。pixelLabelDatastore要求标签图像是索引图像,或者至少是类别ID存成的图像矩阵,而且最好保存为PNG格式。如果标签图是24位RGB图(比如标注工具默认导出的彩色标注图),你需要先把颜色映射到类别ID,再保存成单通道的灰度图。用MATLAB代码实现大概是这样:
% 假设 labelRGB 是标注工具导出的彩色标签图 % cmap 是类别对应的颜色映射表 % 这里用 rgb2ind 将RGB图转为索引图 [labelIndexed, cmap] = rgb2ind(labelRGB, numClasses); % +1 是因为MATLAB索引从1开始,而类别ID可能从0开始 labelIndex = double(labelIndexed) + 1; imwrite(labelIndex, strcat('labels', filesep, name), 'png');千万别小看这个+1,我当初就因为它训练到一半报“标签包含超出范围的像素值”错误,排查了半天才发现索引从0变成1的规则。
2.3 手工标注的实操细节
如果你没有现成数据集,需要自己标注道路区域,MATLAB里有两个工具可以用:imageLabeler(图像标注器)和groundTruthLabeler(真值标注器,偏视频标注)。前者适合单张图片,后者可以处理序列,用在视频帧标注里效率更高。
打开这两个工具的方式很简单,命令行敲imageLabeler回车就进去了。关键的实操经验是:标注道路时别用矩形框,要用多边形工具沿着道路边界逐段框选。用矩形框会把路面以外的楼房、天空、行人全包进去,这样的标签喂给网络,模型学到的是“只要在矩形框里就都是道路”,结果会非常离谱。
标注选项里有一个“像素标签”模式(Pixel Label),进入之后可以在原图上涂抹或框选区域,逐类别标记。过程确实很枯燥,一两张图还好,标几十张图会很崩溃。我建议的折中方案是:前10张图仔细标,后面用Labeler自带的插值功能配合视频序列半自动生成标签,再手修边缘。
2.4 imageDatastore 和 pixelLabelDatastore 的配对加载
准备完图像和标签图后,把它们配对成训练输入,标准的代码大概长这样:
classes = ["背景" "道路" "车辆" "行人"]; pixelLabelIDs = [0 1 2 3]; % 每个类别对应的像素值 imds = imageDatastore('路径/图像文件夹'); pxds = pixelLabelDatastore('路径/标签文件夹', classes, pixelLabelIDs); % 分割训练集和验证集 numImages = numel(imds.Files); idx = randperm(numImages, floor(numImages * 0.8)); imdsTrain = subset(imds, idx); imdsVal = subset(imds, setdiff(1:numImages, idx)); pxdsTrain = subset(pxds, idx); pxdsVal = subset(pxds, setdiff(1:numImages, idx)); % 合并成训练器认识的组合数据 dsTrain = combine(imdsTrain, pxdsTrain); dsVal = combine(imdsVal, pxdsVal);这里有个细节值得注意:图像的尺寸可以不一样,但尽量在训练前把尺寸统一。trainNetwork允许不同尺寸的输入,但那样会增加数据预处理难度,而且网络如果带pooling层,不同尺寸的特征图维度对不上,后期调试会比较绕。实际做法是先用resize或者augmentedImageDatastore统一缩放到固定大小,比如480×640。
3. 网络搭建:到底用内置函数还是自己写网络层
数据齐了以后就是搭网络。这节我不打算贴几百行完整代码,因为MATLAB文档里的DeepLabV3+示例已经写得够清楚,我重点说你容易忽略的那几个设计决策,以及为什么这样选。
3.1 内置函数5分钟搭建DeepLabV3+
DeepLabV3+是目前文档支持最完善、默认推荐的分割网络之一。在MATLAB里搭一个基础版,代码非常短:
numClasses = numel(classes); imageSize = [480 640 3]; % 用ResNet-18作为Encoder骨干,8是DownsamplingFactor lgraph = deeplabv3plusLayers(imageSize, numClasses, 'resnet18'); opts = trainingOptions('adam', ... 'InitialLearnRate', 0.001, ... 'MaxEpochs', 30, ... 'MiniBatchSize', 8, ... 'ValidationData', dsVal, ... 'Plots', 'training-progress', ... 'VerboseFrequency', 20, ... 'ExecutionEnvironment', 'auto');看到没有,真正和网络结构相关的代码就一行。deeplabv3plusLayers内部自动完成了Encoder特征提取、ASPP空洞卷积池化、Decoder高层特征融合这些工作。这里选择resnet18而不用resnet50,原因有两个:一是道路分割不需要很大很深的骨干网络,因为道路在图像中的结构相对简单,边缘清晰、纹理单一,过深的网络反而容易过拟合你的训练集;二是显存和训练时间,resnet50配上ASPP模块对显存要求高出一截,用校园机房或普通笔记本的GPU跑会非常吃力。
3.2 想完全掌控网络结构,怎么自己拼一个U-Net
如果你不想用内置函数,想自己定义网络,那么最常见的语义分割骨架是U-Net。它的核心思想是编码器(下采样提取深层语义)加解码器(上采样逐步恢复分辨率),并且通过跳跃连接把浅层细节和深层语义融合起来。
在MATLAB里用layerGraph写U-Net的骨架时,有几个容易出问题的点要提醒你:
- 自定义层的尺寸要严格对应,尤其注意最后一层卷积的滤波器个数必须是
numClasses。因为输出层的每个通道对应一个类别。 - 跳跃连接在MATLAB里用
addLayers把两层的输出连接起来,再用connectLayers手动连。我当时拼网络时经常看错层次之间的维度,导致Graph可视化时出现断点连接。调试时最快的办法是用analyzeNetwork(lgraph),它会帮你逐个检查每一层的输入输出尺寸是否匹配,还能自动指出哪一层有维度冲突。 - 上采样可以使用
transposedConv2dLayer,其Stride和Cropping要配合下采样倍数。比如编码器做了4次双倍下采样,解码器就要做4次双倍上采样,输出尺寸才能和输入图像一致。
下面是一段简化版的U-Net编码器起始部分,给没写过的人一点手感:
inputLayer = imageInputLayer([480 640 3], 'Name', 'input'); conv1 = convolution2dLayer(3, 64, 'Padding', 'same', 'Name', 'conv1_1'); bn1 = batchNormalizationLayer('Name', 'bn1_1'); relu1 = reluLayer('Name', 'relu1_1'); pool1 = maxPooling2dLayer(2, 'Stride', 2, 'Name', 'pool1'); lgraph = layerGraph(); lgraph = addLayers(lgraph, [inputLayer; conv1; bn1; relu1; pool1]);如果你不需要搞科研级别的网络创新,老老实实用内置函数或者通用结构就够了。自写网络最大的价值在于帮自己理解语义分割的细节,以及方便在课程设计答辩时讲清楚每一层的作用。
3.3 输入尺寸、通道数和ROI设置的原则
图像输入尺寸不是随便定的。如果你的图像分辨率是1280×720,直接训练可能显存爆掉,性价比也低。常见的做法是把宽度缩放到640,高度按比例缩放到480或360。deeplabv3plusLayers要求输入尺寸可以被8整除,因为网络的Downsampling Factor是8或16,如果达不到,最后输出的特征图尺寸和标签图尺寸对不上,训练时就会报错。如果输入是640×480,完全没问题。
还有一个很多人忽略的点:通道数必须是3,也就是RGB。如果你的图像是灰度图,不能用imageInputLayer直接给3通道的网络输入,要么复制成3通道,要么修改网络输入层为单通道。但道路分割中颜色信息非常重要——柏油路、白线、黄线、阴影的区分都依赖颜色,所以建议保持RGB。
4. 训练阶段的隐藏陷阱:类别不平衡、学习率衰减和过拟合
网络定义完之后,训练环节其实比你想的要脆弱。道路分割有几个很典型的失效模式,不是模型结构问题,而是优化策略和数据分布问题。
4.1 类别不平衡:道路占了大半张图,背景却分类不准
道路语义分割里有两个非常典型的类别不平衡问题。第一种是道路占图像面积过高,比如一张街景图里道路可能占60%以上,于是网络发现"只要把所有像素都预测成道路,Loss也不会太大",最后得到了一个几乎全图都是区域的模型。第二种是行人、车辆、交通标志这类小目标占比极低,这些类别在Loss里被淹没。
解决办法有两个层面。一是数据层面:不要直接用原始图片做训练,做一点随机裁剪,让裁剪之后的图像中道路占比各不相同,这样网络不会只见过道路占比高的样本。二是损失函数层面:在MATLAB的trainingOptions里,可以通过'ClassWeights'指定类别权重。比如给行人、车辆这些占比小的类别更高的权重,让它们对Loss有更大的贡献。
如果使用deeplabv3plusLayers直接搭建网络,它的默认损失函数是交叉熵,并且没有自动处理类别不平衡。你可以训练完之后计算一下每个类别的IoU(Intersection over Union),大概率会发现道路的IoU很高,但行人和车辆的IoU低得可怜。然后把ClassWeights按占比的倒数设置,重新训练一轮,效果会立竿见影。
4.2 网络陷入“全预测为道路”模式的排查方法
你可能会遇到一个经典现象:训练几轮后,验证集的分割结果几乎全成了道路类,其他类别完全消失。这通常意味着优化过程陷入了局部最优,网络学到的策略非常偷懒。
我的排查顺序是这样的:第一,看训练Loss曲线,如果Loss前期下降很快,后面快速收敛到一个平台,那就是类别不平衡导致网络只学了大类别的特征;第二,检查数据集里是不是某些图片标签本身有问题,比如标签图里道路区域过大、像素值范围不对;第三,降低学习率再试一次,有时候学习率过大也会导致网络对次要类别“学不进去”。
另外训练中我比较推荐开早停('Plots','training-progress'配合'ValidationPatience')来观察验证集精度。不要一味追求把训练集Loss压到接近0,那样基本就是过拟合了。
4.3 数据增强参数的具体配置
数据增强对道路分割的作用非常明显。道路图像受拍摄时间、朝阳性、季节、天气影响很大,同样的马路,中午和傍晚的光线完全不同,而数据增强可以在不增加采集成本的情况下,让网络见到更多变化。
MATLAB里直接用imageDataAugmenter就能实现,我常用的配置是:
augmenter = imageDataAugmenter(... 'RandXTranslation', [-10 10], ... 'RandYTranslation', [-10 10], ... 'RandScale', [0.9 1.1], ... 'RandRotation', [-5 5], ... 'RandXShear', [-2 2], ... 'FillValue', 0);注意一点:数据增强不能只作用在原始图像上,标签图也要做一模一样的变换,否则会错位。MATLAB的pixelLabelImageDatastore在augmenter参数里会自动做配对变换,但如果你手动写增强,必须用transform同时处理图像和标签。这里面最容易出的问题是水平翻转忘了翻转标签,结果模型学到的是道路永远在图像右侧。
还有一个关于FillValue的细节。填充值默认是0,但标签图的像素值0可能对应背景类,这样生成的新标签图边界处会被填成背景,本身没有大错,却可能引入噪声。如果你手动增强标签,填充值要设置成标签集中的某个合理值,最好是背景类ID,不要填一个不存在的ID。
5. 训练完成后的推理、可视化与道路mask生成
训练完模型后怎么用,是很多人忽视的另一半工作。城市道路语义分割如果只是得到一个五颜六色的分割可视化图,那还不够;往往我们需要把它变成一张二值道路mask:哪些像素是道路,哪些不是,用来做后续的路径规划、障碍物检测等。
5.1 一整套标准推理流程
用训练好的模型对一张新图片做推理,代码路径大概是:
% 加载训练好的网络 net = trainedNetwork_1; % 读入测试图像并按训练时的尺寸预处理 I = imread('test.jpg'); I = imresize(I, [480 640]); % 执行语义分割,返回标签矩阵 [label, score] = semanticseg(I, net); % 将标签矩阵映射为彩色分割图 B = labeloverlay(I, label, 'Colormap', cmap, 'Transparency', 0.6); % 可视化原图、分割图、以及得分图 figure; subplot(1,3,1); imshow(I); title('原图'); subplot(1,3,2); imshow(B); title('语义分割结果'); subplot(1,3,3); imagesc(score(:, :, 2)); title('道路类得分'); colorbar;semanticseg的返回值里,label是每个像素的最大概率类别索引,score是H×W×C的概率矩阵,其中第C个通道对应第C个类别的得分。如果你只需要道路目标,可以直接做如下操作:
% 假设道路是第2类(索引2) roadMask = (label == 2); % 去掉误检的小碎块 roadMask = bwareaopen(roadMask, 200); % 填补道路内部的小孔洞 roadMask = imfill(roadMask, 'holes'); % 可视化为红色覆盖 showOverlay = imoverlay(I, roadMask, [1 0 0]);这段后处理里有两个小技巧可以记一下:
bwareaopen会把小于指定像素数的连通区域去掉,用它来清除被误判成道路的小块阴影或远处小片地面,效果非常好。imfill会把道路内部的孔洞(比如被树干、行人截断的部分)填上,但要注意不能乱用。如果道路中间有花坛、隔离带,填孔会把它们误认成道路。实际场景里应该先判断你的应用是否允许“道路区域允许内部有障碍物”,如果是自动驾驶,应该保留这些孔洞。
5.2 把语义分割结果反向映射回原图尺寸
推理时为了训练方便,输入被resize到了480×640,但实际使用时的原图通常是1920×1080甚至更大。那就需要把分割结果映射回原图尺寸。
最简单粗暴的做法是直接把label矩阵用imresize缩放到原图大小,并指定'nearest'最近邻插值。千万别用默认的双线性插值,因为语义分割的标签是离散类别编号,双线性插值会插出像1.8这样不存在的中间类别。下面这行是安全的:
labelFull = imresize(label, [size(I,1), size(I,2)], 'nearest');但要注意,这样放大后边缘会有锯齿感。如果想要光滑一点,可以先对每个类别的得分图做双线性放大,再对放大的得分矩阵做argmax取类别,这样得到的边界会更平滑自然:
scoreFull = imresize(score, [size(I,1), size(I,2)], 'bilinear'); [~, labelFull] = max(scoreFull, [], 3);这招在可视化时特别管用,生成的mask边缘不会出现明显马赛克。
6. 评估指标与实测调优:为什么你的图像总是边缘错位
训练完模型后,别急着拿它到处跑。先做一次定量评估,搞清楚模型到底好在哪里、崩在哪里。
6.1 mIoU和Global Accuracy的正确计算方式
语义分割最主流的两个指标是Global Accuracy(像素级总体精度)和mIoU(各类别IoU的平均值)。
Global Accuracy很容易理解,就是正确分类的像素总数除以总像素数。但单独看这个指标没有意义,因为道路区域占比大的时候,哪怕模型把其他类别全忽略,Global Accuracy也可能高达85%以上。
mIoU更有参考价值。每个类别的IoU定义为该类别预测区域和真实标注区域的交集大小除以并集大小。在MATLAB里可以用evaluateSemanticSegmentation直接算:
metrics = evaluateSemanticSegmentation(pxdsResults, pxdsTruth, 'Metrics', 'all'); metrics.ClassMetrics metrics.NormalizedConfusionMatrix我见过很多人在答辩时只报一个Global Accuracy,然后模型效果表面看起来很高,却被老师一语道破“你的模型是不是把所有像素都预测成道路了”。所以给自己看的时候,重点是mIoU和每一类的IoU列表。道路、天空、建筑的IoU如果超过90%,但行人、自行车只有20%不到,那这个模型还不能算靠谱。
6.2 边缘错位问题:为什么路面和路缘石的交界总是锯齿状
实测中出现的另一类高频问题是边缘错位——道路和路缘石、道路和行人过街横线的交界处,分割结果总是不够干净。这背后的原因很复杂,但有几个直接因素:
一是标签本身的标注精度不够。手工标注时多边形只框了大概轮廓,路缘石的边缘非常细,标注员手一抖,标签就偏了一个两个像素。模型学到的是有噪声的边缘,输出自然不稳定。
二是训练图像的尺寸问题。如果把1920×1080原图缩到480×640训练,路缘石的宽度可能就剩下一两个像素,模型根本没有足够信息去学习这条边缘线。
三是后处理过度。某些场景下为了平滑分割结果,我们会对概率图做高斯平滑或中值滤波,但道路的边缘本来就应该是锐利的,平滑反而破坏了边缘。
针对边缘问题,一个实用的改进方法是混合损失或者训练时增加边缘加权。不过MATLAB内置的损失函数不好直接改。简单的替代方案是:在训练后处理中,把概率图小于某阈值的像素留到下一步处理,不直接置信;或者对得分图做一个imgaussfilt的低通处理,但权值要小,只去掉孤立噪点,不破坏边界。
就我个人的实际体验来说,在道路语义分割这个任务里,把mIoU从70%提到85%,大部分功劳来自数据均衡和数据增强,网络结构的贡献反而没那么大。先花力气把训练数据整理得分布合理,再把后处理做成一个稳定的流程,比盲目换大模型更见效果。
7. 最后分享一个很实用的小技巧:用距离变换修正道路区域断裂
实战里最让人抓狂的场景之一就是:明明是一条连续的大马路,分割结果却因为路面上的树干阴影、车辆遮挡出现了一堆细长的空洞或断裂。这个时候不需要重新训练,用距离变换加形态学重构就能补回来。
具体思路是:对当前预测得到的roadMask做一次距离变换,找出离道路区域较远的“内部空洞”,然后从这些空洞的边缘向外生长,把和道路连通的部分(通过种子点)填上。MATLAB里可以直接用imimposemin配合形态学重构从标记点填充区域。这个技巧我没有在官方dome里见过,属于在做项目填坑时总结出来的土办法,但实测效果很好,尤其适合遮挡场景。
% 假设 roadMask 是初始预测的道路二值图 D = bwdist(~roadMask); % 构造内部种子点:道路内部较大空洞的中心点 openMask = imopen(roadMask, strel('disk', 5)); % 从这些种子点重构,填补较难恢复的断裂区域 fillMask = imreconstruct(...);把这个小流程封装成一个postprocessRoadMask脚本放进项目里,每次出掩膜后自动跑一遍,能省掉很多手工修图的时间。
做道路语义分割的过程,本质上是在数据质量、模型复杂度、训练策略和后处理之间反复权衡。MATLAB的好处是这些环节都被封装成了一个完整流程,坏处是出了问题不容易像Python那样直接扒开源码看。希望我这篇经验能帮你少走点弯路,尤其是数据格式和类别权重那两块,提前排掉雷,后面就顺了。
本文还有配套的精品资源,点击获取