气球COCO数据集:Mask R-CNN实例分割训练全流程指南
简介这是一份面向计算机视觉入门与进阶学习者的气球实例分割数据集基于 Mask R-CNN 构建并已完整转换为 COCO 格式可直接放入最新 MMDetection 框架中训练与测试免去自行标注和格式转换的繁琐步骤。资源包大小 36.89MB共包含 76 个文件其中 74 张 JPG 图片按 train2017 与 val2017 划分另有 instances_train2017.json 和 instances_val2017.json 两个标注文件压缩包内目录包含 train2017、val2017 和 annotations路径结构清晰可直接对应 COCO 标准数据加载要求。已有 512 人浏览学习该资源适合用于快速跑通 Mask R-CNN 训练流程、对比不同 backbone 的效果或开展气球检测与分割方向的实验。通过这套数据可以直观理解实例分割的数据组织方式、标注文件字段含义以及 MMDetection 的配置与调用逻辑也能借此体验从数据加载、模型训练到 mAP 评估的完整流程为后续在自定义数据集上复现 Mask R-CNN 提供完整可参考的样例。1. 做实例分割第一关是数据这份气球 COCO 数据集让 Mask R-CNN 训练省一半事做实例分割的都知道模型结构可以抄但一份带像素级掩码的标注数据很难凑。网上的开源数据集里全是规规矩矩的 COCO 格式自己手里的图却往往是散的 JPG 加一片一片的 PNG mask光写转换脚本就得调一天。第一次拿到这份「气球 Mask R-CNN 转 COCO 格式」的数据集时最大的感受是它把最脏的活干完了图片分好 train/val标注整理成 instances_train2017.json 和 instances_val2017.json类目只有一个 balloon解压后直接用 MMDetection 就能训练 Mask R-CNN甚至测试效果比从 COCO 里随机抠子集要好得多。它适合两类人一是刚入门实例分割、想用现成数据跑通 Mask R-CNN 基线的新手二是需要验证某个分割方案、不想把时间耗在标注格式上的算法工程师。这篇文章从 COCO 标注内部结构讲起一直讲到训练、避坑、自己转换同类数据最后给一套微调和评估习惯。2. 先看懂 COCO 标注再动手数据集的目录、字段与掩码表示2.1 压缩包内部结构train2017 / val2017 / annotations 三件套解压 ballon_mask_rcnn.rar 后目录是典型的 COCO 布局ballon_mask_rcnn/ ├── annotations/ │ ├── instances_train2017.json │ └── instances_val2017.json ├── train2017/ │ ├── 7308740338_591f27b631_k.jpg │ ├── 7178882742_f090f3ce56_k.jpg │ └── ... └── val2017/ ├── 8053085540_a72bd21a64_k.jpg ├── 4581425993_72b9b15fc0_b.jpg └── ...文件名里的数字是 Flickr 图片 ID不是 COCO 原始编号这个不重要重要的是目录名和 JSON 文件名是配套的。MMDetection 的 CocoDataset 默认会根据配置里的ann_file去读 JSON再根据img_prefix拼图片路径所以目录起成 train2017/val2017 是为了省事你也可以改成别的名字只要配置里对应改就行。这份资源里还有一个细节annotations 目录下只有 instances_train2017.json 和 instances_val2017.json没有 captions 或 stuff 文件因为 Mask R-CNN 训练只需要实例标注。图片是从开放图片库里挑出来的包含气球的图像总量不大正好适合在单卡上做实验而不是一上来就面对一百多 G 的完整 COCO。2.2 从 instances_train2017.json 里抽取三个关键数组先别急着开训拿 Python 把 JSON 读出来看一遍能省很多排查时间。COCO 标注 JSON 顶层有三个必填字段images、annotations、categories。我一般会写个几十行的检查脚本import json from collections import Counter ann json.load(open(annotations/instances_train2017.json)) print(images:, len(ann[images])) print(annotations:, len(ann[annotations])) print(categories:, ann[categories]) # 统计每一类别的实例数量 cnt Counter([a[category_id] for a in ann[annotations]]) print(annotations per category:, dict(cnt)) # 查看有标注的图片数量 img_with_ann set(a[image_id] for a in ann[annotations]) print(images with annotations:, len(img_with_ann)) # 看前两条 annotation 的完整内容 for a in ann[annotations][:2]: print(a)这段脚本逻辑很简单先加载 JSON然后打印各集合数量。关键是要看categories的内容。这份数据集的categories通常长这样[{id: 1, name: balloon}]只有一个类id 从 1 开始。再看 annotation 字典能看到segmentation、area、bbox等字段。注意area是掩码真实面积不是像素数乘上什么系数bbox是[x, y, width, height]的浮点列表对应的是该实例外接矩形的左上角坐标和宽高。这里容易踩的一个概念坑是COCO 的category_id是 1但 MMDetection 里如果直接用官方 COCO 预训练模型它的 80 类是从 1 到 80 的没有 id0 的类别。如果你把num_classes设成 1后台实际做的是把分类分支的输出从 81 变成 2包括背景类所以 1 类数据集的num_classes1是没问题的。真实在配置里写的 1不是 11。另一个值得做的检查是验证标注框和掩码是否对齐。因为转换脚本偶尔会把 bbox 和 segmentation 来自不同的 mask 源导致训练时 mask head 的 loss 正常、box head 的 loss 异常。可以用下面的代码做粗粒度校验from pycocotools.coco import COCO coco COCO(annotations/instances_train2017.json) for ann_id in coco.getAnnIds()[:10]: ann coco.loadAnns(ann_id)[0] mask coco.annToMask(ann) x, y, w, h [int(v) for v in ann[bbox]] # mask 里非零像素是否集中在 bbox 范围内 inside mask[y:yh, x:xw].sum() total mask.sum() if inside 0.5 * total: print(bbox and mask mismatch:, ann_id)这个脚本用coco.annToMask(ann)把标注转成二值 mask再判断 bbox 是否包住了大部分正像素。如果 mismatch 比例高说明数据源本身不可靠训练前就得止损。2.3 多边形坐标 vs RLE这份数据集里掩码长什么样COCO 的segmentation字段有两种写法多边形点列表polygon和 RLE 游程编码。标注工具导出的通常是 polygon格式是[x1, y1, x2, y2, ...]点按顺序围出目标轮廓。RLE 则更像压缩后的位图适合存储大而复杂的掩码。实例分割训练时MMDetection 的 CocoDataset 会通过 pycocotools 把 polygon 转成 mask也可以直接使用 RLE。这份气球数据集在 JSON 里用的是 RLE 还是 polygon取决于原始转换脚本。无论哪种训练前最好用 pycocotools 验证一下能不能正确解码from pycocotools.coco import COCO coco COCO(annotations/instances_train2017.json) img_ids coco.getImgIds()[:5] for img_id in img_ids: ann_ids coco.getAnnIds(imgIdsimg_id) for ann_id in ann_ids: ann coco.loadAnns(ann_id)[0] mask coco.annToMask(ann) # 返回 HxW 的 0/1 mask print(ann[image_id], mask.sum(), ann[bbox], ann[segmentation].__class__.__name__)这里的coco.annToMask(ann)是 pycocotools 提供的解码接口polygon 和 RLE 都能处理。如果mask.sum()是 0说明标注有问题需要回到转换脚本里查。看到输出里 bbox 和 mask 面积量级一致再进入训练环节就不容易出幺蛾子。这个习惯我建议保留拿到任何 COCO 数据集都先跑一遍。annotation 字段里还有一个iscrowd表示该实例是否是一堆密集物体。COCO 官方对 crowd 区域使用 RLE 且不需要多边形这份气球数据集几乎都是单体气球iscrowd基本都是 0。如果以后自己转换记得把iscrowd设 0否则 MMDetection 在训练时会走另一条 crowd 分支容易出现奇怪的行为。下表是这份 annotation 里最常见的字段说明排查数据时对照着看字段类型含义idint标注实例的唯一编号image_idint所属图片在 images 数组中的 idcategory_idint类别 id这里为 1bboxlist[float][x, y, width, height]左上角坐标加宽高areafloat掩码面积像素单位segmentationdict/listRLE 字典或多边形点列表iscrowdint0 表示普通实例1 表示群体区域2.4 为什么 COCO 格式能直接对接 MMDetection很多人问不是有 VOC 格式吗为什么非转 COCO关键原因是 MMDetection 的 CocoDataset 在数据加载阶段已经实现了 COCO JSON 的解析、mask 解码、多尺度裁剪拼接这一整套逻辑你只需要给一个ann_file路径。而 VOC 格式需要额外的 xml 解析且 mask 通常是以独立 PNG 文件存储数据加载路径多一层间接性。COCO 的单 JSON 文件把所有实例标注集中在一起训练前做 cache 也更方便尤其适合小数据集快速迭代。也正是因为这份气球数据是标准 COCO 格式CocoDataset可以拿到annotations后直接做annToMask不需要再写任何自定义 Dataset 类下游的 mask head 训练、评估阶段的 mAP 计算都能复用 COCO 官方 API。所以「转成 COCO」不是一种强迫症是节省工程成本的选择。3. 直接训练 Mask R-CNNMMDetection 配置修改与命令行实操3.1 用官方 mask_rcnn_r50_fpn 做基类覆盖类别数和数据路径MMDetection 的配置文件做了很好的继承机制。我们不需要从零写一个 Mask R-CNN而是在官方提供的mask_rcnn_r50_fpn_1x_coco.py基础上改几处。先建一个独立配置目录比如configs/balloon/新建文件mask_rcnn_r50_fpn_1x_coco.py_base_ configs/mask_rcnn/mask_rcnn_r50_fpn_1x_coco.py model dict( roi_headdict( bbox_headdict(num_classes1), mask_headdict(num_classes1) ) ) dataset_type CocoDataset data_root data/balloon/ classes (balloon,) data dict( samples_per_gpu2, workers_per_gpu2, traindict( classesclasses, ann_filedata_root annotations/instances_train2017.json, img_prefixdata_root train2017/ ), valdict( classesclasses, ann_filedata_root annotations/instances_val2017.json, img_prefixdata_root val2017/ ), testdict( classesclasses, ann_filedata_root annotations/instances_val2017.json, img_prefixdata_root val2017/ ) )这段配置做了三件事第一告诉模型num_classes1覆盖 bbox head 和 mask head 的输出维度第二指定数据格式为CocoDataset并设置classes(balloon,)让 dataset 内部的类别映射保持正确第三把ann_file和img_prefix指到实际路径。注意samples_per_gpu是每块卡上的 batch size这里设 2如果你的显卡显存有限改成 1 也能跑。为什么num_classes要写 1因为 Mask R-CNN 的分类头输出是num_classes 1通道那个 1 是背景。官方 COCO 配置里写的是 80但实际上有 80 个目标类所以这里如果写81就错了MMDetection 内部不会帮你再加 1。同理如果数据里有 3 个类这里就写 3。如果你用的是最新版 MMDetection 3.xdata这个字段被拆得更细但核心的ann_file、img_prefix、classes还在只是包在了train_dataloader、val_dataloader下。建议先按项目实际安装的版本来如果跑的是 2.x上面这份配置可以直接用。3.2 单卡训练batch size、学习率与 Epoch 怎么定数据集不大单卡训练是常态。在资源有限的情况下我一般先用 2 的 batch size 跑通流程再考虑调大。学习率方面MMDetection 默认的 1x 配置是 0.02那是基于 8 卡、每卡 2 张图的 batch size 16 算出来的。如果只有单卡 2 张图总 batch size 变成 4学习率要等比缩放常见做法是线性缩放lr_new lr_base * new_batch / old_batch。这样算出来大约是 0.005。python tools/train.py configs/balloon/mask_rcnn_r50_fpn_1x_coco.py \ --work-dir work_dirs/balloon \ --cfg-options optimizer.lr0.005 data.samples_per_gpu2--cfg-options是 MMDetection 提供的命令行覆盖入口optimizer.lr会直接改写配置里的优化器学习率。data.samples_per_gpu对应 3.1 配置文件里面设的 2如果前面已经写在配置里这里可以不加。注意samples_per_gpu在 3.x 里被移到了train_dataloader.batch_size但思路一样。Epoch 数量不要直接照抄官方 12。官方 1x 是指 COCO 的 12 epoch那个数据量大这个气球数据总计可能就几十到一百多张图训练很快收敛。我通常先跑 50 epoch观察 loss 是否稳定。如果验证集 mAP 在 20 epoch 后就不再上升说明已经过拟合可以早停。如果中途断了想接着训练用--resume-frompython tools/train.py configs/balloon/mask_rcnn_r50_fpn_1x_coco.py \ --resume-from work_dirs/balloon/epoch_30.pthresume-from会同时恢复优化器状态和模型权重而不是只加载权重。这比load-from更适合断点续训。3.3 测试与可视化test.py 参数和结果分析训练结束后在验证集上跑评估python tools/test.py configs/balloon/mask_rcnn_r50_fpn_1x_coco.py \ work_dirs/balloon/epoch_50.pth \ --eval segm--eval segm是告诉脚本用 COCO mask 评估协议输出的是 mask AP。如果只做目标检测可以用--eval bbox但这里是实例分割所以重点关注 segm。默认输出结果是一张表的摘要包括AP、AP_50、AP_75、AP_s、AP_m、AP_l这几项。对于这种单一类别的小目标数据AP_50通常是最先看的指标因为 mask 预测的 IoU 阈值在 0.5 时比较稳定。想要可视化预测结果可以加--show-dirpython tools/test.py configs/balloon/mask_rcnn_r50_fpn_1x_coco.py \ work_dirs/balloon/epoch_50.pth \ --eval segm --show-dir work_dirs/balloon/viz/脚本会把每张测试图的预测框、掩码叠加在原图上保存到viz目录。这一步很关键因为 mAP 只能告诉你整体好坏而可视化能让你一眼看出模型是不是把气球区域整个抠出来了还是只分割了半边。我第一次跑这份数据集时看 mAP 觉得还能接受看了可视化才发现模型对深色背景下的暗红气球识别率极低这才针对性加了更多数据增强。4. 避坑指南数据路径、类别数、显存和 RLE 解码4.1 训练一开始就报 FileNotFoundError路径拼错导致找不到图片现象执行训练脚本后日志里跳出 FileNotFoundError提示找不到类似于data/balloon/val2017/8053085540_a72bd21a64_k.jpg的文件或者报Directory not found。原因最常见的是三种一是data_root写成了绝对路径但实际数据放在其他盘二是解压后的目录名与配置里的img_prefix大小写不一致比如把气球数据解压成了Balloon_mask_rcnn而配置里用的是全小写三是在 Windows 上解压后产生的反斜杠路径在某些脚本拼接时出错。解决先在命令行里用一条命令确认路径能对上find data/balloon -maxdepth 2 -type f | head -5把输出和配置里的ann_file、img_prefix逐段比对。我一般会在配置里用os.path.abspath打印一遍最终路径写进 debug 脚本里。最省事的方法是直接把data_root改成相对路径并保证工作目录在mmdetection项目根目录下启动训练。另外检查一下ann_file里面的image字段的file_name是否和train2017目录里的实际文件名一致因为有些转换脚本会错误地带上子目录前缀比如train2017/xxx.jpg而img_prefix又加了一次train2017/导致重复路径。4.2 训练直接报 num_classes mismatch忘了覆盖 bbox head 和 mask head现象训练刚开始在第一次 forward 时报size mismatch或RuntimeError: Error(s) in loading state_dict for RPNHead报错里提到bbox_pred或mask_fcn的形状对不上期待维度是 82实际维度是 2。原因这份气球数据集只有 1 个类但配置里漏写了model.roi_head.bbox_head.num_classes导致模型仍然按官方 COCO 的 801 类输出分类分数。加载预训练权重时最后一层 FC 的输出维度对不上直接报错。解决回到 3.1 的配置确认以下两行都存在并且都改成num_classes1model dict( roi_headdict( bbox_headdict(num_classes1), mask_headdict(num_classes1) ) )如果用了--cfg-options临时覆盖也要同时覆盖这两个字段--cfg-options model.roi_head.bbox_head.num_classes1 model.roi_head.mask_head.num_classes1这里有一个很多人翻车的细节bbox_head和mask_head都要改只改 bbox head 会让 mask head 仍然按原来的 80 类输出。代码跑起来可能不报错但训练出来的 mask 分支完全失效。4.3 显存溢出 OOM先降 batch size不要急着换模型现象训练刚开始显存占用直接拉满日志报CUDA out of memory程序退出。原因Mask R-CNN 本身就吃显存R50 backbone 加上 FPN单图分辨率如果偏大batch size2 也可能爆卡。尤其是有些图片尺寸很大训练时 Resize 没有限制最大边导致显存峰值过高。解决先降 batch size 到 1data dict( samples_per_gpu1, workers_per_gpu2, ... )如果还是 OOM检查输入尺寸的配置。可以在data里加上 train pipeline 的Resize限制把最大边限制在(1333, 800)这是 COCO 训练的默认值。实际这份气球数据里的图片来自 Flickr长宽比各异有些原图非常大建议把img_scale设置在(800, 1333)减少显存压力。还可以用梯度累积来模拟更大的 batch size--cfg-options optimizer_config.grad_clip.max_norm35但梯度累积在 MMDetection 2.x 里没有内置参数需要改训练循环不如直接降 batch size 实在。只要模型能塞进显存单卡 batch size1 也是能训的就是收敛慢一点。4.4 测试结果全黑、没有 maskscore_thr、mask_thr 与 RLE 解码现象测试后test.py输出的 mAP 是 0或者用--show-dir保存的图片只有原图没有任何颜色覆盖看起来像模型完全没预测到东西。原因可能性很多最常见的是 score threshold 设得过高。MMDetection 测试时默认score_thr0.05如果你手动改高了比如 0.8模型输出置信度普遍在 0.5 左右时所有预测都会被过滤掉。另一种可能是数据标注里iscrowd1的实例过多模型把目标都当成 crowd 掩盖了但这份数据集很少见。解决先用默认阈值测试一次再做可视化时调低阈值python tools/test.py configs/balloon/mask_rcnn_r50_fpn_1x_coco.py \ work_dirs/balloon/epoch_50.pth \ --eval segm --show-dir work_dirs/balloon/viz/ \ --cfg-options model.test_cfg.score_thr0.3model.test_cfg.score_thr是测试阶段后处理的置信度阈值。对单一类别且图像较简单的数据集0.3 通常能看到部分预测。如果 0.05 下 mAP 仍然为 0那问题大概率出在数据加载上回到第 2 章先确认coco.annToMask()能正确输出非零 mask。RLE 解码失败通常是因为转换脚本没有把counts从 bytes 转成 strpycocotools 在 Python 3 下会处理这种差异但某些自定义 Dataset 不会。你在自己写转换脚本时一定要在maskUtils.encode()后加上rle[counts] rle[counts].decode(ascii)。5. 数据集从哪来任意分割标注转 COCO 格式的通用脚本5.1 从二值掩码到 RLE推荐 pycocotools 的 mask 编码路径这份气球数据集已经是 COCO 格式但你下次拿到的可能是 LabelMe 的 json或者一堆黑底白色的 PNG 掩码。掌握「二值掩码转 COCO annotation」的思路才是真正的一劳永逸。转换的核心是先得到二值 mask然后用 pycocotools 编码成 RLE。import numpy as np from pycocotools import mask as maskUtils def encode_mask(mask_binary): # mask_binary: HxW 的 0/1 数组dtype 可以是 bool 或 uint8 mask np.asfortranarray(mask_binary.astype(np.uint8)) rle maskUtils.encode(mask) rle[counts] rle[counts].decode(ascii) return rle这段代码里的关键点是np.asfortranarray。COCO 的 RLE 编码内部按列优先处理必须先把数组转成 Fortran 序否则编码结果和实际 mask 不一致解码后会出现掩码错位的现象。rle[counts]在 Python 3 下是 bytes 类型直接写进 JSON 会序列化失败所以要先decode(ascii)转成字符串。如果你手里的是多边形顶点坐标可以用maskUtils.frPyObjects先转成 RLEpolygons [x1, y1, x2, y2, ...] # 扁平列表 rle maskUtils.frPyObjects([polygons], height, width) rle maskUtils.merge(rle)frPyObjects的输入需要[polygon_list]即使只有一个多边形也要再包一层列表。它返回一个 RLE 对象因为 poly 转 RLE 时可能拆成多个小段所以通常还要merge合并。如果原图目标有孔洞多边形是外轮廓加内轮廓frPyObjects也能处理但要保证轮廓点的顺序正确内轮廓方向与外轮廓相反。5.2 构造 annotation 字段bbox、area、segmentation、iscrowd拿到 RLE 后不要手算 bbox 和面积直接交给 pycocotoolsdef build_coco_annotation(ann_id, image_id, category_id, mask_binary): rle encode_mask(mask_binary) bbox maskUtils.toBbox(rle).tolist() # [x, y, w, h] area float(maskUtils.area(rle)) annotation { id: ann_id, image_id: image_id, category_id: category_id, bbox: bbox, area: area, segmentation: rle, iscrowd: 0 } return annotationmaskUtils.toBbox(rle)返回的是一个 numpy 数组所以要用.tolist()转成 Python list。maskUtils.area(rle)返回的是 RLE 对应的真实像素面积单位是像素。这里不需要自己算最小外接矩形因为 COCO 的 bbox 定义就是「能包住 mask 的轴对齐矩形」pycocotools 的实现和评估时用的完全一致手工算容易在边缘情况下差一两个像素。iscrowd一般设 0。只有当一张图里有大量重叠目标且无法逐目标标注时才把整个区域标成 crowd 并设 1。注意 crowd 的segmentation必须是 RLE不能是 polygon且训练时 MMDetection 对 crowd 的处理是忽略其 loss所以普通数据不要乱设。5.3 组装 images 与 categories保证 ID 连续并做数据切分有了 annotation 列表还要组装images数组。images里的每个元素需要包含id、file_name、width、height其他字段比如license、date_captured可以省略。from PIL import Image import os images [] annotations [] ann_id 1 for img_id, filename in enumerate(os.listdir(train_imgs), start1): w, h Image.open(os.path.join(train_imgs, filename)).size images.append({ id: img_id, file_name: filename, width: w, height: h }) mask_path os.path.join(train_masks, filename.replace(.jpg, .png)) mask_binary np.array(Image.open(mask_path).convert(L)) 0 annotations.append(build_coco_annotation(ann_id, img_id, 1, mask_binary)) ann_id 1 categories [{id: 1, name: balloon}] coco_json { images: images, annotations: annotations, categories: categories } with open(instances_train2017.json, w) as f: json.dump(coco_json, f)这段脚本里img_id从 1 开始递增ann_id也从一个独立的计数器递增。注意image_id必须和images数组里的id对应上不能复用文件名里的数字。file_name只需要写文件名img_prefix由 MMDetection 配置负责拼接。如果一张图有多个 mask比如多个气球你的循环要分别对每个 mask 调用build_coco_annotation并且image_id相同但ann_id不同。切分数据时要注意不能直接按文件顺序随机分而是按场景分。气球数据集里同一张照片可能连着好几张如果同一场景的图同时进了 train 和 val验证集会虚高。这一点很多教程不会提我自己第一次做类似数据集时就吃过亏mAP 高得离谱换一批真实场景图直接掉点。6. 进阶技巧用 COCO 预训练权重微调只看 mask AP 评估效果这份气球数据量小从头训练 Backbone 很容易过拟合。正确做法是加载 COCO 预训练权重只让后面的 head 重新学。在 MMDetection 里只要在配置里加一行load_from ./checkpoints/mask_rcnn_r50_fpn_1x_coco.pth由于气球只有 1 类而预训练模型是 80 类加载时最后一个分类层和 mask 分支的维度不匹配。MMDetection 默认会忽略形状不一致的权重只加载 backbone 和 FPN 的部分这正好是我们想要的。如果你希望连 RPN 也复用直接默认即可不用额外写代码。微调时的学习率不宜过大我一般设 0.001 到 0.002是正常训练学习率的一半以下。训练 30 epoch 左右就能看到验证集 mAP 稳定。紧接着用第 3 章的测试命令评估重点看下面这行输出Average Precision (AP) [ IoU0.50:0.95 | area all | maxDets100 ] 0.672 Average Precision (AP) [ IoU0.50 | area all | maxDets100 ] 0.912 Average Precision (AP) [ IoU0.75 | area all | maxDets100 ] 0.788对单个类别的小数据集AP_50到 0.9 以上说明模型基本能框住并分割出气球AP_75也能到 0.7 以上说明掩码边缘质量不错。如果你的AP_50高但AP_75掉得很惨大概率是 mask 边缘不够精细优先检查数据集标注边缘是否平滑其次再考虑更换更强的 backbone。从那以后我每次拿到新的分割数据都会先写一个 20 行的检查脚本验证 JSON 解码再用预训练权重跑 10 个 epoch 做 smoke test确认 loss 在降、mAP 能过 0.5才开始全量调参。希望帮到你。本文还有配套的精品资源点击获取