简介:面向钢材表面缺陷自动检测场景,这份YOLOv5资源提供了训练好的检测权重、完整数据集及训练记录,覆盖多种常见缺陷类型,适合工业质检项目、目标检测算法学习或毕业设计使用,可解决缺陷定位与分类落地难题。压缩包共164个文件,以yaml/yml配置、Python脚本、PyTorch权重pt文件、jpg图片和xml/txt标签文件为主,其中yaml/yml用于模型与训练参数配置,Python脚本负责数据加载、推理和评估;同时包含results.csv、TensorBoard事件文件、Dockerfile与tutorial.ipynb等,整体约106.77MB。数据集已用LabelImg完成标注,jpg原图与xml、txt格式标签分目录存放,兼顾VOC与YOLO训练格式,可直接加载训练或评估;模型训练过程留存了PR曲线、loss曲线等可视化结果,便于观察收敛状态。已有2275人学习,配套博客提供检测结果参考,可帮助快速上手钢材缺陷检测任务的模型推理、结果复现与二次优化,整份资料结构清晰,是一份能直接落地的工业视觉实战参考。
1. 一条产线质检的刚性需求,为什么偏选 YOLOv5
做钢材表面缺陷检测的项目,绝大多数不是从零研究算法,而是先问一句:有没有现成的模型权重和数据集能让我今天就跑通一条基线?YOLOv5能成为这个方向上的默认选择,不是因为它精度最高,而是因为它把“训练、验证、部署、迭代”这条链路做成了闭环,配合 NEU-DET 这类工业表面缺陷数据集,一个刚接触检测的工程师也能在两三天内看到稳定的 mAP 曲线。相比动不动就改配置文件、调不齐依赖的检测框架,YOLOv5 在钢材缺陷这种数据量不大、类别固定的场景里,性价比确实高。读者如果手头正好有产线缺陷图片或只有公开数据集,这篇笔记会把从数据集准备、权重选型到落地排错的全过程拆开讲,让你照着走一遍就能得到可用的权重文件,而不是停留在跑通 demo。
2. 数据集先喂明白:NEU-DET 与自制钢材缺陷集的选型逻辑
2.1 NEU-DET 起步:六类缺陷与 1800 张图的边界
钢材表面缺陷检测里最常用的公开数据集是东北大学的 NEU-DET,包含六类缺陷: crazing(裂纹)、inclusion(夹杂)、patches(斑块)、pitted_surface(麻点)、rolled-in_scale(氧化铁皮压入)、scratches(划痕)。每类 300 张,总共 1800 张灰度图,尺寸统一为 200×200 像素。不要被这个小规模吓到——它恰恰适合做两件事:验证训练链路是否通顺、判断模型在六分类任务上的基础能力。
但必须清楚这个数据集的边界。200×200 的分辨率对钢材表面缺陷来说已经算够用,因为大多数缺陷在这么小的尺寸下依然有清晰的纹理特征,但它的背景相对单一,光照和拍摄角度变化不大。实际产线里,金属表面的反光、油污、水渍都会让分布偏移。我把 NEU-DET 定位成“配方验证集”,而不是“交付模型”的来源。
用 YOLOv5 训练 NEU-DET 之前,需要把标注转成 YOLO 格式。NEU-DET 的原生标注是 XML(Pascal VOC 风格),路径通常长这样:/dataset/ANNOTATIONS/xxx.xml,而 YOLOv5 需要的是每张图对应一个 txt,每行是class x_center y_center width height,坐标全部归一化到 0-1。
我在本地跑通整个流程用的脚本大致如下:
import os import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path, out_dir, class_names): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) out_lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in class_names: continue box = obj.find('bndbox') xmin = float(box.find('xmin').text) ymin = float(box.find('ymin').text) xmax = float(box.find('xmax').text) ymax = float(box.find('ymax').text) # 归一化到 0-1 x_center = ((xmin + xmax) / 2) / img_w y_center = ((ymin + ymax) / 2) / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h cls_id = class_names.index(name) out_lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") out_name = Path(xml_path).stem + '.txt' with open(os.path.join(out_dir, out_name), 'w') as f: f.write('\n'.join(out_lines)) class_names = ['crazing', 'inclusion', 'patches', 'pitted_surface', 'rolled-in_scale', 'scratches'] xml_dir = 'NEU-DET/ANNOTATIONS' txt_dir = 'NEU-DET/labels' os.makedirs(txt_dir, exist_ok=True) for xml_file in os.listdir(xml_dir): if xml_file.endswith('.xml'): voc_to_yolo(os.path.join(xml_dir, xml_file), txt_dir, class_names)这个脚本的关键点在两点:一是从 XML 的size节点取原始图像宽高,而不是用固定值,因为后续自制数据集可能遇到不同分辨率的图片;二是归一化的坐标就是 YOLOv5 训练时 loader 直接读的格式,不需要再做 resize 换算。跑完检查一下,生成的 txt 数量和 xml 数量一致,再抽查一个文件确认类别索引和 xml 里的name对应得上。
2.2 自制数据集:采集规范与类别平衡的预处理
实际项目里,NEU-DET 覆盖不了产线上的所有表面缺陷,尤其是那种只在特定工艺段出现的“夹杂物”或“边裂”。自制数据集是绕不开的。我一般按这样的顺序推进:先定采集规范,再定标注标准,最后做数据清洗。
采集规范第一条是固定光照方向。钢材表面是典型的强反射表面,同一道划痕在角度不同的光源下,拍出来的对比度完全不同。用环形光源还是条形光源取决于缺陷形态——划痕类缺陷用低角度条形光更容易凸显;麻点类缺陷用环形光更均匀。这个选择会直接写进数据集,影响模型学到的特征,这也是为什么很多人换个场地就发现模型精度崩了。
第二条是分辨率统一。相机离钢材表面的距离要固定,或者记录像素当量(每毫米多少像素),后期裁剪时才能保证缺陷尺寸的一致性。缺陷在图像里过大或过小都会让模型偏向学习尺度特征而不是纹理特征。
标注标准里最容易翻车的是“争议样本”。一道细浅的划痕,不同标注员可能一个标一个不标。我建议把标注规范写成文档:什么程度的变色算缺陷、两条缺陷粘连时怎么切分、缺陷与背景对比度低于多少不算。这一步看似琐碎,却决定了后续 mAP 的上限。
类别平衡问题在钢材缺陷里非常典型。产线上某一类缺陷可能只占总量 5%,如果不处理,模型会把它学成背景。常见的处理方式有三种:第一是对少数类做过采样复制,简单但容易过拟合;第二是做在线数据增强时提高少数类图片的增强概率;第三是用类别权重损失,YOLOv5 里没有直接暴露这个参数,所以我一般会通过--weights训练两轮,或者干脆在数据层面控制比例。
我整理自制数据集的推荐目录结构如下,方便直接复制到 YOLOv5 项目下:
steel_defect/ ├── images/ │ ├── train/ # 训练集图片 │ ├── val/ # 验证集图片 │ └── test/ # 测试集图片 ├── labels/ │ ├── train/ # 对应的 YOLO txt │ ├── val/ │ └── test/ ├── data.yaml # 类别和路径配置 └── runs/ # 训练输出关于划分比例,我习惯按 8:1:1 划分,而不是默认的 9:1。钢材缺陷的类别本来就少,验证集太小的话,mAP 波动会很大,导致你没法判断一次改动到底是有效还是无效。
2.3 数据划分脚本与 YOLO 格式校验:别在源头埋雷
很多人训练失败的第一现场不是模型,而是数据。标签和图片对不上、类别索引错位、图片里有损坏文件,这些问题都发生在进入训练之前。因此我在每次开始训练前会跑一个数据完整性校验脚本,确保格式正确而且能对齐。
import os from pathlib import Path from PIL import Image def validate_dataset(img_dir, label_dir): img_files = sorted(Path(img_dir).glob('*')) label_files = sorted(Path(label_dir).glob('*.txt')) print(f"images: {len(img_files)}, labels: {len(label_files)}") assert len(img_files) == len(label_files), "图片和标签数量不一致" bad_txt = [] for txt_path in label_files: for line in txt_path.read_text().strip().splitlines(): parts = line.split() if len(parts) != 5: bad_txt.append((str(txt_path), "字段数不为5")) continue cls_id = int(parts[0]) if cls_id >= 6: # 注意这里改成实际的类别数 bad_txt.append((str(txt_path), f"类别索引超界: {cls_id}")) for img_path in img_files: try: img = Image.open(img_path) img.verify() except Exception as e: bad_txt.append((str(img_path), f"图片损坏: {e}")) if bad_txt: for item in bad_txt[:10]: print(item) else: print("数据校验通过") validate_dataset('steel_defect/images/train', 'steel_defect/labels/train')这个脚本里最容易被忽略的是img.verify()——它只检查文件头是否完整,不会解码整张图。如果你怀疑图片在传输或爬取过程中被截断,建议改成img.load()做完整解码,代价是速度慢几倍,但能避免训练到一半崩掉。值得。
校验通过之后,再写训练集、验证集、测试集的文件列表。YOLOv5 支持直接把图片目录作为数据集参数,但我更喜欢生成 txt 路径列表,因为可控性更强。用find命令或者脚本生成都行:
find steel_defect/images/train -name "*.jpg" > train.txt find steel_defect/images/val -name "*.jpg" > val.txt注意路径要写绝对路径或者相对 YOLOv5 项目根的路径,别写相对于当前 shell 的路径,否则换目录跑训练时找不到文件。
3. 权重选型与训练策略:从零训练还是迁移预训练
3.1 预训练权重怎么选:yolov5s 还是 yolov5m
YOLOv5 的官方权重文件是按照 COCO 数据集预训练的,同一个模型结构下提供 n/s/m/l/x 五个尺寸。钢材缺陷检测场景里,图像分辨率普遍不高,缺陷目标占图比例不小,我用得最多的是 yolov5s 和 yolov5m。这两者的差异主要体现在网络宽度和深度:yolov5s 的参数量约 7.2M,yolov5m 约 21.2M,推理速度约差一倍。
选择依据不只看精度,更要看你的算力和部署目标。如果最终要跑到 RK3568 这类边缘设备或者树莓派 4B 上,yolov5s 是更现实的起点——量化后的 INT8 模型在保持可用精度的前提下,推理延迟能压到实时边缘。如果是纯服务器端检测或者对精度要求高,yolov5m 的收益能在小样本数据集上体现得更明显。
注意一个关键点:COCO 预训练权重里的 80 个类别和钢材缺陷的 6 个类别完全不同。迁移学习时,YOLOv5 会自动把最后一层的类别数改为你数据集里的类别数,但前几层的特征提取依然保留着 COCO 学到的通用纹理、边缘、形状特征。这也是为什么哪怕缺陷形态和 COCO 物体差异很大,预训练权重仍然比随机初始化收敛快得多。
下载权重时不要随意从第三方站点拉取,直接用官方仓库的 release 文件。下载后先校验一下文件大小是否和官方一致,我踩到过一次别人重打包的权重,表面能加载,实际推理时输出全是 NaN,浪费了半天。
3.2 训练命令与超参数:batch、epoch、imgsz 的调参逻辑
训练命令是 YOLOv5 项目里最标准化的部分,但参数怎么设才是真正的经验分水岭。我第一次跑 NEU-DET 时 batch 设了 64,结果 loss 震荡得厉害;后来发现 200×200 的小图配大 batch 没问题,但 epoch 设太短导致模型没收敛完就停了。
先看一个我在钢材缺陷上常用的训练命令:
python train.py \ --data steel_defect.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 32 \ --epochs 200 \ --cache \ --patience 30--img 640的含义是把输入图 resize 到 640×640。NEU-DET 原图是 200×200,resize 到 640 相当于人为放大了 3 倍多,对缺陷这种纹理特征是有利的,因为模型能看到更多像素级细节。但代价是训练显存占用增加、速度变慢。如果你的缺陷特征在 200×200 下已经清晰可见,用--img 416就能省不少时间。
--batch 32是显存允许范围内的稳妥值。显存不够时优先降 batch 而不是降图片尺寸,因为图片尺寸影响的是模型输入分布,而 batch 只影响梯度稳定性。用 8GB 显存跑 yolov5s、640 输入,batch 16 通常是安全线。
--patience 30是早停参数,验证集 mAP 连续 30 个 epoch 没有提升就停止训练。钢材缺陷数据集小,模型通常在 80-120 个 epoch 之间收敛,如果设了 200 个 epoch,没有早停基本等同于浪费时间。我习惯把 patience 设成总 epoch 数的 15-20%,既能容忍 loss 平台期,又不会空跑。
超参数文件里还有一个参数容易被忽视:anchor。YOLOv5 默认会在训练前根据你的标注框尺寸自动重新计算 anchor,这个行为由hyp.yaml里的anchor_t控制,默认值 4.0。如果标注框尺寸分布和默认 anchor 差距过大,模型前期收敛会变慢。查看自动计算后的 anchor 值,在训练日志开头部分会打印,如果看到某个 anchor 的宽高比明显异常,说明标注数据里有噪声框。
3.3 权重控制:从 last.pt 到 best.pt 与热启动续训
训练结束后,runs/train/exp/weights/目录下会有两个权重文件:last.pt和best.pt。last.pt是最后一个 epoch 的权重,best.pt是验证集 mAP 最高的权重。千万别用反了——部署时用 best.pt,续训时用 last.pt。
续训对钢材缺陷检测特别实用,因为数据标注往往是分批到位的。第一批先标 500 张训练一版,第二批又补了 300 张,这时不用重新从预训练权重开始,直接用 last.pt 续训能省一半以上的时间:
python train.py \ --data steel_defect.yaml \ --weights runs/train/exp/weights/last.pt \ --img 640 \ --batch 32 \ --epochs 100 \ --cache续训时要注意两点。第一,--epochs是在当前 epoch 基础上继续而不是从零开始,所以如果你希望总共训练 300 轮,第一批跑了 200 轮,第二批应该设 100。第二,如果数据集的内容变化较大(比如新增了类别),继续沿用旧的 best.pt 做评估会产生偏差,建议用新的验证集重新评估后再决定是否部署。
还有一个容易被忽略的权重控制技巧:--freeze参数可以冻结模型前半部分的权重只训练后半部分。钢材缺陷数据集小,模型容易过拟合,冻结前 10 层相当于在“用小数据微调”的场景里给模型加了正则化。试一次就能感受到差异。
4. 模型训练与效果验证:从 loss 曲线到 mAP 的完整解读
4.1 训练日志怎么读:四类 loss 与过拟合判断
YOLOv5 的训练日志格式类似这样:
Epoch gpu_mem box obj cls labels img_size 99/200 4.21G 0.0243 0.0185 0.0124 4 640box是边界框回归 loss,obj是目标置信度 loss,cls是分类 loss。钢材缺陷场景里,三类缺陷——划痕、裂纹、压入氧化铁皮——外形上极易混淆,cls loss 的下降速度会明显慢于 box loss。如果你看到 cls loss 在 50 个 epoch 后还纹丝不动,优先去检查数据标注时这几类的边界是否划得足够清晰,而不是急着改网络结构。
过拟合在钢材缺陷上的表现很典型:box loss 持续下降,但验证集 mAP 在某个点后不再上升甚至开始下降。因为训练集和验证集都来自同一批产线图片,图像分布相近,模型很容易“记住”验证集图片本身而不是缺陷的泛化特征。遇到这种情况,我一般会出现一个岔路口:要么加大数据增强力度,要么缩减模型容量从 yolov5m 降到 yolov5s。
YOLOv5 的在线增强在hyp.yaml里控制,hsv_h、hsv_s、hsv_v控制颜色扰动,degrees、translate、scale控制几何扰动。钢材表面缺陷对颜色变化不敏感,但对几何方向很敏感——划痕方向不同,纹理特征差异巨大。所以我会把degrees设为 90,允许模型学习旋转不变性,同时保持hsv_*在默认值附近。
4.2 验证集评估:混淆矩阵与 PR 曲线里的业务信号
训练完成后,用val.py跑一次验证,输出里包含 Precision、Recall、mAP@0.5、mAP@0.5:0.95 四组指标。钢材缺陷检测里,我更看重 Recall 而不是 Precision,因为漏检一块有缺陷的钢材流到下游,损失远比误检一块好钢大。
混淆矩阵里最值得看的是哪两类容易互相混。我遇过的真实案例:氧化物压入(rolled-in_scale)和斑块(patches)混淆率高达 30%,本质上是因为这两类在灰度图上的纹理分布非常接近,肉眼都容易看错。解决办法不是改模型,而是回炉标注:重新定义这两类的判定标准,比如以缺陷边缘的锐利度为区分依据。
另一个容易忽略的点是 P-R 曲线的拐点。YOLOv5 的置信度阈值默认是 0.25,实际部署时会调到 0.5 甚至更高来减少误报。P-R 曲线会告诉你:阈值从 0.25 提到 0.5,Recall 掉多少、Precision 涨多少。如果曲线在 0.3-0.5 区间内 Precision 已经到 0.95 而 Recall 还在 0.9 以上,那部署阈值设 0.5 是划算的;如果 Recall 在 0.35 就开始跳水,说明模型对缺陷的置信度普遍不高,问题大概率在数据质量而不是阈值。
4.3 缺陷尺度与 anchor 自动计算:难样本的边界问题
钢材表面的某些缺陷类型天然是“难样本”。细裂纹在 640×640 的图像里可能只占 10 个像素宽、200 个像素长,长宽比达到 20:1。YOLOv5 的 anchor 机制对这种极端长宽比的目标并不友好,默认 anchor 里最大的长宽比上限是 4:1,超出的部分会被强行压缩进邻近 anchor 的匹配范围。
--img 640后,YOLOv5 会在训练开始前自动计算新 anchor,并打印在日志里。计算后的 anchor 如果最大长宽比仍然覆盖不了你的细长缺陷,可以考虑手动指定 anchor。操作方式是把models/yolov5s.yaml里的anchors字段直接改成目标尺寸。我一般会先用脚本统计训练集所有标注框的长宽比分布,再据此设计 anchor,这样比凭感觉改靠谱得多。
难样本的另一个来源是缺陷目标之间的重叠。麻点和斑块经常成片出现,相邻缺陷框的 IoU 超过 0.5 甚至 0.7。YOLOv5 默认 NMS 的 IoU 阈值是 0.45,重叠目标很容易被压制。遇到这种情况,推理时的--iou-thres需要调低到 0.3 左右,代价是可能产生少量重复检测框,但相对于漏检,重复框在钢材场景里更容易被二次筛选容忍。
5. 钢材缺陷检测避坑:五个常见事故的现场排查
5.1 漏检集中在某一类缺陷上:从数据分布找原因,不是模型结构
现象:六类缺陷的 mAP 差异巨大,最好的一类 0.95,最差的一类 0.35。按常规思路去调置信度阈值,结果依然无效。
原因:这一类的训练样本数远少于其他类,或者这类缺陷的形态在训练集和验证集里分布不一致。模型没见过足够多的形态变化,学不到泛化特征,只会记住训练集里有限几种形态。
解决:先按类别统计训练集的图片数量和标注框数量,如果确实不平衡,优先补数据,而不是换模型结构。如果暂时补不了,把这一类单独做了一个难例挖掘测试集,定向验证改动效果。这是最容易被忽略的调试起点。
5.2 训练 loss 正常但验证 mAP 为 0:标签错位的黑匣子
现象:训练过程中 box loss 和 cls loss 都在下降,但 val 阶段 mAP 始终是 0。看起来像是模型没收敛,实际上一查数据文件,发现训练集和验证集的标签类别索引映射不一致。
原因:不同批次的标注人员可能用了不同的类别编号,合并后没有统一。例如第一批数据里“划痕”是第 2 类,第二批里变成第 4 类,训练时模型学到的映射在验证集上完全不适用。
解决:强制在训练前做一次全局标签体检。写脚本统计所有标签文件中出现的类别索引和对应数量,确认和 data.yaml 里的类别顺序一致。这个检查成本很低,但能避免一整个训练周期白跑。对时间宝贵的工程师来说,这是必须养成的手艺。
5.3 推理输出大量低置信度检测框:NMS 阈值和背景误检
现象:在真实产线图片上测试,模型输出大量置信度在 0.1-0.3 之间的检测框,有些框在完全没有缺陷的均匀表面上。
原因:钢材表面纹理复杂,且有一些和缺陷相似的光影纹理,导致模型在无缺陷区域也产生较高响应。训练数据里没有充分覆盖“背景难例”。
解决:最直接的办法是提高部署时的置信度阈值,比如从 0.25 提到 0.6,能滤掉大部分低置信度误检。更根治性的做法是往训练集里加入足够的“无缺陷”负样本图片,并标注为空图,让模型学会抑制这些区域的响应。
5.4 切到边缘设备后精度下降:INT8 量化不是白拿的
现象:yolov5s 在 GPU 上验证 mAP 0.88,导出 TensorRT INT8 后在 RK3568 上实测 mAP 掉到 0.72,检测框开始抖动。
原因:INT8 量化对激活值的分布敏感,钢材缺陷很多是低频纹理特征,量化后信息损耗比自然图像更严重。这是量化到部署过程中的常见现象,不是模型本身出了问题。
解决:常见做法是先用 TensorRT 的 PTQ 量化,用验证集图片作为校准集。如果精度还不行,就需要做 QAT 感知训练,在模型里插入伪量化节点重新训练几十个 epoch,让模型权重适应量化噪声。这个步骤耗时较大,但对于钢材缺陷场景,往往是部署前的必要投入。
5.5 大图推理慢到不可用:预处理是隐藏的性能杀手
现象:产线相机输出 2000×2000 的大图,直接输入模型推理,帧率只有不到 1 FPS,根本无法满足在线检测要求。
原因:推理耗时不只在模型计算本身,把大图 resize 到 640×640 的预处理也很耗时,尤其是在 CPU 上做 BGR 转换和缩放。很多人优化模型结构,却忽略了 IO 管线的瓶颈。
解决:我一般这样处理——先做图像金字塔切片检测,把大图切分成若干 640×640 的块,每个块之间重叠 10% 防止缺陷被切断,检测后再拼回大图坐标。另一个思路是把预处理和推理放到同一进程里,避免频繁的内存拷贝。关键是用异步管线把采集、预处理、推理、后处理重叠起来,而不是串行执行。这块往往比模型本身更值得花时间。
6. 权重部署后的持续迭代:从验证集到产线的闭环设计
部署不是终点,权重文件需要持续迭代才能适应产线变化。我习惯用一个小工具脚本把每次验证结果整理成一张表格,记录训练日期、数据集版本、模型结构、平均 mAP、误检率、漏检率。这个表是后续调参的地图,没有它,所有的“感觉变好了”都是无效反馈。
import csv from datetime import datetime def log_experiment(exp_name, dataset_version, model, mAP50, precision, recall, note): row = [ datetime.now().strftime('%Y-%m-%d %H:%M'), exp_name, dataset_version, model, round(mAP50, 4), round(precision, 4), round(recall, 4), note ] with open('experiment_log.csv', 'a', newline='') as f: writer = csv.writer(f) writer.writerow(row) log_experiment( exp_name='exp38_retrain_after_add_scratches', dataset_version='v3.2', model='yolov5s', mAP50=0.913, precision=0.92, recall=0.88, note='加入新标注的划痕样本,val分布保持一致' )产线反馈回来的误检图要定期收集并加入训练集。钢材产线的环境会变——换了照明亮度、换了轧辊、换了钢材牌号,表面反光特性都会变。每一次变更都可能导致旧模型精度下降,因此最好建立一套“月度数据回流”机制:把当月所有误检框和漏检框截图,用脚本自动统计缺陷类别分布,再人工标注后合并进训练集。这个过程按月做,比一次性攒半年数据再标注高效得多。
量化部署后的监控同样重要。在 RK3568 或树莓派 4B 这类设备上跑 INT8 模型时,记得保留一份 FP16 的权重当作对照。如果现场反馈精度有明显下降,先跑一版 FP16 对比,能快速定位问题在模型本身还是量化过程。这个对照习惯帮我避过好几次“模型废了”的误判,其实只是量化校准集没选好。
最后说一个我的习惯:每次训练完,把训练命令、权重文件路径、验证集结果、部署阈值这四个信息写在一个 README 文件里,和权重文件放在一起。三个月后回来看模型,不用翻聊天记录就能复现。经常有人来问“上次那个模型怎么训练的”,有这份记录直接发文件就行。这套习惯帮我省了很多时间,希望帮到你。
本文还有配套的精品资源,点击获取