
简介这是一份面向毕业设计场景的基于UNet的遥感图像语义分割完整工程包适合计算机视觉方向的学生或需要快速搭建分割网络的开发者。资源围绕UNet经典编码器-解码器架构涵盖数据准备、预处理、模型设计、训练评估到推理应用全流程并附带相关PDF论文与毕业论文LaTeX源码可直接用于复现实验或扩展改进。包内共69个文件压缩包约47.26MB。其中6个py源码文件负责模型定义、数据处理与训练流程3个ipynb笔记本演示数据集创建、训练与预测操作png与bmp图像为可视化示例tex与bib为论文写作素材另有svg、ttf等辅助资源目录结构清晰便于按模块使用。目前已有346人学习下载。对于需要完成遥感图像分割课题或理解UNet实现细节的读者这份资料提供了从环境启动脚本、模型实现到毕业设计论文框架的一站式参考能显著减少从零搭建的时间成本。1. 遥感图像语义分割为什么绕不开UNet先搞清楚这个课题值不值得做如果你的毕设题目是“基于UNet的遥感图像语义分割”先别急着焦虑。这件事反过来说是一次性价比很高的选择UNet虽然是2015年前后提出的网络在今天一堆更强的语义分割模型面前已经不算新但在遥感图像语义分割这个方向上它依然是绝大多数基线方案的首选。遥感影像的特点是地物尺度跨度大、背景占比极高、边界细节复杂UNet的对称编码-解码结构和skip connection天然贴合这类需求。这个课题要解决的事也很清楚输入一张几万乘几万的卫星或航拍影像输出一张逐像素标注了地物类别建筑、道路、水体、耕地等的mask图。适合谁适合需要在一个学期内跑通实验、写出论文、并通过答辩的本科或硕士生。2. UNet结构拆解skip connection为什么是遥感分割的事实标准这一章不堆公式只讲UNet每个部件在遥感任务里实际干了什么。理解到这里后面抄代码和调参时就不会把网络当成黑匣子。2.1 编码器-解码器骨架下采样让网络学会“看全局”语义分割本质上是对每个像素做分类所以UNet的输出尺寸必须和输入一致。它靠编码器逐步下采样把图像压缩成高维语义特征再用解码器逐级恢复到原分辨率。编码器里是两次卷积加一次池化每下采样一次特征图尺寸减半、通道数翻倍解码器则用反卷积或双线性上采样把特征图一层层放大。问题在于池化和带有步长的卷积会丢掉高频空间信息建筑边界的锐利度、道路的连通性这些细节恰恰是遥感分割里最“贵”的像素。下采样次数的选择是第一个要拍板的参数。常见做法是下采样4次输入512×512的切片最深层特征图是输入的1/16。太浅则感受野不够一片几百米宽的农田和一片同样纹理的林地只看局部根本分不开太深则显存吃紧训练速度也扛不住。遥感图像的特点是目标尺度差距极大耕地、水域这类地物动辄占据大片区域而独立房屋、车辆可能只有几十个像素。编码器下采样到1/16时深层的每个特征点对应原图16×16的区域模型就有了“看全局”的能力但小目标的信息在下采样过程中已经被压没了一半。这是UNet架构自带的矛盾也是后面skip connection存在的理由。2.2 skip connection不只是拼接特征图而是给解码器送“详细地图”UNet和早期FCN语义分割模型最大的差异就是skip connection编码器每一层下采样前的特征图被直接送到解码器对应层在通道维度上拼接起来。相比FCN那种一路下采样再一路硬上采样的结构UNet的解码器手里同时拿着“全局语义图”和“局部细节图”上采样时不会凭空瞎猜。skip connection在遥感分割里实际起三个作用。第一是空间细节保真。MaxPool和stride卷积是信息压缩压缩过程不可逆深层的低分辨率特征里只剩“这里是城区”这类抽象语义建筑屋檐的具体边界基本丢了。skip connection把浅层高分辨率特征直接接到解码器相当于给上采样过程补了一份详细地图。第二是梯度通路。浅层卷积的梯度可以从loss沿着skip直接传回网络深了也不容易梯度消失。第三是多尺度融合。浅层特征管小目标细节深层特征管上下文结构两者拼接后模型能同时看到一栋房子的轮廓和它周围是大片农田还是建成区。还有一个常被忽略的点skip connection用的是拼接concat不是相加add。concat保留了通道维度的完整信息解码器里的卷积可以自己学着怎么权衡浅层和深层特征的权重。如果图省事改成add通道数直接减半浅层细节会被深层语义“平均”掉边界质量会明显下降。遥感图像语义分割里道路、水体这类连通性地物最怕的就是边界断裂concat的冗余表达能力是有价值的。2.3 遥感影像的特殊性输入通道、标签形态、数据分布都跟自然图像不一样遥感图像语义分割不能直接套自然图像那套流程至少有四个地方要单独处理。一是图像尺寸遥感原图经常是几万像素见方的大影像没法直接进显存必须先切成patch后面整图预测时还要考虑拼接策略。二是类别不平衡一张512×512切片里背景或“未分类”区域经常占八成以上建筑、道路占很小比例普通交叉熵会把模型带偏。三是多波段输入遥感影像除了RGB还常有近红外、红外乃至十几个波段当面对多光谱与全色遥感图像融合出来的高分辨率影像时通道数更多。UNet第一层卷积的in_channels是参数改成4、8甚至更多都可以很多毕设直接丢掉近红外波段只取RGB白白浪费信息。四是标注噪声遥感标注成本很高公开数据集的mask存在错标漏标直接用可能让模型学到错误边界。训练前先做一次标签体检是个值得养成的习惯。下面这段脚本用来检查训练集里mask的类别值和各类别比例import numpy as np from PIL import Image mask np.array(Image.open(train/labels/patch_00000.png)) classes np.unique(mask) print(类别值:, classes) print(各类像素占比:, {c: float((mask c).mean()) for c in classes})这个脚本的逻辑是先用np.unique()看mask里实际用了哪些灰度值。语义分割训练时label应该是单通道PNG像素灰度值就是类别ID0表示背景1表示建筑依次类推不是一张彩色的可视化图。如果打印出来的类别值包含0到255的连续大范围数值或者类别数和你的预设对不上说明数据转换环节已经错了。各类像素占比则是给后面选loss函数用的如果某一类占比低于1%就需要用类别权重或DiceLoss而不是裸用交叉熵。这一步我先提个醒第4章还会细说。3. 遥感图像标注与训练集构建数据准备好训练才没有返工训练模型只占毕设工作量的一小部分数据准备才是大头。很多学生花了两周把网络跑通结果发现mIoU上不去最后排查半天是训练集和验证集切片串了、或者mask的类别值读错了。这一章把数据从原始影像到训练样本的完整链路拆开讲。3.1 公开数据集与类别体系先决定“分几类”再决定“怎么训练”遥感语义分割常见公开数据集有几种选哪个取决于毕设题目要求。DeepGlobe Land Cover是一种多类别地物分类数据集适合做“土地利用分类”这类题目Inria Aerial Image Labeling是建筑/背景二分类标注质量较高Massachusetts Buildings/Roads则偏向单类目标提取。如果题目要求“多种地物分类”用DeepGlobe如果题目只是“建筑物提取”或“道路提取”二分类数据集更容易跑出好看的mIoU答辩也好讲。数据集类别构成图像形式适合题目DeepGlobe Land Cover6类耕地/森林/牧场/水域/城市/未知卫星影像切片单图尺寸约1024×1024多类别土地利用分类Inria Aerial Image Labeling建筑/背景2类航拍正射影像大图切分建筑物提取、二分类分割Massachusetts Buildings建筑/背景2类航拍影像大图二分类分割、UNet入门类别数直接决定模型输出层的通道数。第4章的UNet实现里num_classes就是按这个设定的。另一个要注意的是“未知”类很多遥感数据集的“未分类”区域在训练时是当作一个类别算的预测时这个类别没有实际意义常见做法是在评估mIoU时把它忽略掉或者在训练时给它的loss权重设成0。如果题目里根本不需要“未知”类预处理时可以把该类别像素统一重映射成背景。自建标注数据时标注工具可以用LabelMe或精灵标注输出GeoJSON或VOC格式后面再转成训练用的mask PNG。遥感图像标注有一个天然优势是遮挡少地物顶部视角清晰但类内差异大是主要难点同一片耕地在不同季节、不同光照下的纹理差异可能超过不同类别之间的差异。标注时建议定一套明确的类别边界规则比如“树冠阴影算不算植被”否则两个标注员标出来的结果会互相矛盾模型学到的是噪声。3.2 大影像切片patch_size、stride与边缘处理三个参数定生死遥感原图太大不能直接训练切片是绕不开的一步。下面是一个标准的滑动窗口切图脚本把大图和对应的大mask同步切成训练样本import numpy as np from PIL import Image def slide_crop(image_path, label_path, out_dir, patch512, stride384): img np.array(Image.open(image_path)) lbl np.array(Image.open(label_path)) os.makedirs(os.path.join(out_dir, images), exist_okTrue) os.makedirs(os.path.join(out_dir, labels), exist_okTrue) idx 0 h, w img.shape[:2] for y in range(0, h - patch 1, stride): for x in range(0, w - patch 1, stride): img_patch img[y:ypatch, x:xpatch] lbl_patch lbl[y:ypatch, x:xpatch] Image.fromarray(img_patch).save(f{out_dir}/images/patch_{idx:05d}.png) Image.fromarray(lbl_patch).save(f{out_dir}/labels/patch_{idx:05d}.png) idx 1 print(生成切片数:, idx)这段脚本的逻辑是先用PIL把大影像和mask读进来然后以patch为窗口、stride为步长滑动裁剪。步长小于窗口边长时相邻切片有重叠区域等于对训练样本做了扩充也能缓解地物被切断的问题。参数选择上patch512是显存和上下文信息的常见折中8G显存加batch size为4时勉强够用显存紧张可以降到256。stride建议取patch的75%左右也就是512的patch配384的stride保留128像素的重叠如果训练样本量本身很大stride可以提高到512减少样本冗余。边缘处理是这里最容易出问题的地方当图像尺寸不能被patch整除时脚本会把右侧和下侧不足patch大小的区域直接丢弃导致地物被截断。常见做法是对边缘做零填充zero padding或者镜像padding后裁掉多余部分更省事的方案是把每次切片的起点做随机偏移让边缘信息有机会出现在不同切片里。另外训练集和验证集一定要按原图划分不要让同一张原图的切片同时出现在训练集和验证集里。这个坑在第5章的“数据泄漏”里会专门讲现在先记住这个原则。3.3 从矢量标注到mask PNG格式转换与hdict式的标签组织方式公开数据集的标注格式五花八门GeoJSON矢量、COCO多边形、RLE编码都有但UNet训练时统一要的是单通道灰度mask像素图。栅格化这一步核心是坐标对齐矢量里的坐标是地理坐标系而影像像素坐标是行列号必须先通过影像自带的GeoTransform把地理坐标换算成像素行列号再在对应的像素位置上画多边形。# GeoJSON多边形转mask PNG简化版 import numpy as np from PIL import Image, ImageDraw polygons [...] # 已解析并完成坐标变换的多边形点集 mask Image.new(L, (1024, 1024), 0) draw ImageDraw.Draw(mask) for poly in polygons: draw.polygon(poly, fill1) # 类别ID 1 mask.save(label.png)这段代码的逻辑是先生成一张全背景灰度值0的单通道图再把每个类别对应的多边形区域内填充成类别ID。要注意fill1这一步多类别数据要按类别ID逐类填充最后得到的灰度值范围必须是从0到类别数减1的连续整数。如果画出了浮点坐标ImageDraw会自动取整边界会略有偏差但只要原始标注的精度不是特别高这个误差可以接受。标签组织方式上训练数据最常见的做法是把image和label配对存放在两个目录里同名文件一一对应或者组织成字典结构类似HuggingFace生态中常见的hdict语义分割标签组织方式一个样本是一个键值对image键指向输入图像label键指向对应的mask。PyTorch的Dataset类可以写成这样from torch.utils.data import Dataset from glob import glob import numpy as np from PIL import Image class RSDataset(Dataset): def __init__(self, img_dir, lbl_dir, transformNone): self.img_paths sorted(glob(f{img_dir}/*.png)) self.lbl_paths sorted(glob(f{lbl_dir}/*.png)) assert len(self.img_paths) len(self.lbl_paths), 图像和标签数量不一致 self.transform transform def __getitem__(self, i): img np.array(Image.open(self.img_paths[i])).astype(np.float32) / 255.0 lbl np.array(Image.open(self.lbl_paths[i])).astype(np.int64) if self.transform: img, lbl self.transform(img, lbl) return torch.tensor(img).permute(2, 0, 1), torch.tensor(lbl)这段代码的关键点有两个。一是sorted()排序必须保证图像和标签按照同样的顺序配对如果文件名前缀相同但后缀不同排序结果通常一致但如果文件名是a_2.png和a_10.png这类直接排序会按字符串比较导致顺序错位要提前固定编号位数二是标签的dtype必须是int64因为PyTorch的交叉熵和one_hot都要求label是整数索引而图像要归一化到float。transform里对图像和mask要同步做同样的空间变换比如翻转和旋转这个同步操作也容易写漏。数据增强方面遥感影像做随机翻转、90度旋转、色彩抖动和亮度对比度调整就够用高斯模糊之类的作用有限因为遥感影像本身光照一致性较好。4. 基于UNet的训练落地最小可跑通脚本与五组关键参数数据就绪后模型训练反而是流程里最机械的部分。这一章给出一份能直接复用的UNet实现和训练脚本然后解释每个参数为什么这么设。4.1 UNet模型定义把in_channels和num_classes改成你的数据下面是一个标准UNet的PyTorch实现结构做了适度的规范化加了BatchNorm适合遥感分割这种多类别任务import torch import torch.nn as nn class DoubleConv(nn.Module): def __init__(self, in_c, out_c): super().__init__() self.conv nn.Sequential( nn.Conv2d(in_c, out_c, 3, padding1), nn.BatchNorm2d(out_c), nn.ReLU(inplaceTrue), nn.Conv2d(out_c, out_c, 3, padding1), nn.BatchNorm2d(out_c), nn.ReLU(inplaceTrue), ) def forward(self, x): return self.conv(x) class UNet(nn.Module): def __init__(self, in_channels3, num_classes6): super().__init__() self.enc1 DoubleConv(in_channels, 64) self.pool1 nn.MaxPool2d(2) self.enc2 DoubleConv(64, 128) self.pool2 nn.MaxPool2d(2) self.enc3 DoubleConv(128, 256) self.pool3 nn.MaxPool2d(2) self.enc4 DoubleConv(256, 512) self.pool4 nn.MaxPool2d(2) self.bridge DoubleConv(512, 1024) self.up4 nn.ConvTranspose2d(1024, 512, 2, stride2) self.dec4 DoubleConv(1024, 512) self.up3 nn.ConvTranspose2d(512, 256, 2, stride2) self.dec3 DoubleConv(512, 256) self.up2 nn.ConvTranspose2d(256, 128, 2, stride2) self.dec2 DoubleConv(256, 128) self.up1 nn.ConvTranspose2d(128, 64, 2, stride2) self.dec1 DoubleConv(128, 64) self.out nn.Conv2d(64, num_classes, 1) def forward(self, x): e1 self.enc1(x) e2 self.enc2(self.pool1(e1)) e3 self.enc3(self.pool2(e2)) e4 self.enc4(self.pool3(e3)) b self.bridge(self.pool4(e4)) d4 self.dec4(torch.cat([self.up4(b), e4], dim1)) d3 self.dec3(torch.cat([self.up3(d4), e3], dim1)) d2 self.dec2(torch.cat([self.up2(d3), e2], dim1)) d1 self.dec1(torch.cat([self.up1(d2), e1], dim1)) return self.out(d1)forward里的四次torch.cat就是skip connection的落地把上采样结果和编码器对应层的特征在通道维度上拼起来。in_channels对应输入图像的波段数RGB是3多光谱是4或更多num_classes对应类别数在3.1节的数据集表格里已经确定了。输出层的1×1卷积把64通道特征压缩成num_classes通道得到的是未经过softmax的logit后续计算loss时再处理。注意BatchNorm在batch size比较小时4以下统计量会抖动如果显存只允许小batch可以把BatchNorm2d换成GroupNorm。4.2 损失函数与评估指标为什么遥感任务推荐DiceLoss第2章提过遥感数据类别不平衡的问题。交叉熵对每个像素的loss贡献一视同仁当背景占95%时模型只要把所有像素都预测成背景loss就已经很低但目标类别一个都出不来。DiceLoss直接优化分割区域的重叠度天然缓解这个问题class DiceLoss(nn.Module): def __init__(self, smooth1.0): super().__init__() self.smooth smooth def forward(self, logits, targets): probs torch.softmax(logits, dim1) onehot torch.nn.functional.one_hot( targets, num_classesprobs.shape[1] ).permute(0, 3, 1, 2).float() intersection (probs * onehot).sum(dim(2, 3)) union probs.sum(dim(2, 3)) onehot.sum(dim(2, 3)) dice (2.0 * intersection self.smooth) / (union self.smooth) return 1.0 - dice.mean()这段代码的逻辑是先把模型输出的logits通过softmax转成概率再把每个像素的标签转成one-hot向量然后逐类别计算预测概率和真实标签的交集与并集得到每个类别的Dice系数最后取平均。smooth参数是平滑项防止某个类别在batch里完全没有正样本时分母为零。DiceLoss的缺点是训练初期梯度不够平滑loss曲线可能先升后降常见做法是前20个epoch用交叉熵之后再切到DiceLoss或两者加权相加。评估指标建议使用mIoU平均交并比这是遥感语义分割论文里最通用的指标。每个类别分别计算IoU再取平均。训练过程中每个epoch结束要在验证集上跑一遍把验证mIoU存下来。mIoU的计算要注意“未知”类的处理如果数据集里有这个类别但你不关心它计算时要把它的IoU排除掉否则数值会被拉低。4.3 训练配置基准学习率、batch size、epoch与调度策略训练参数没有放之四海而皆准的取值但有一套合理的起点。以下是我常用的基准配置参数建议值说明optimizerAdam收敛快适合分割任务初始学习率1e-4高于1e-3容易震荡batch size48由显存决定小batch注意BNepoch80120遥感分割一般比自然图像更需要长训schedulerReduceLROnPlateaumIoU连续5个epoch不升lr减半weight decay1e-4防止过拟合训练循环的结构通常是固定的optimizer torch.optim.Adam(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemax, patience5 ) criterion DiceLoss() for epoch in range(epochs): model.train() for img, lbl in train_loader: img, lbl img.cuda(), lbl.cuda() logits model(img) loss criterion(logits, lbl) optimizer.zero_grad() loss.backward() optimizer.step() model.eval() miou evaluate(model, val_loader) scheduler.step(miou) print(fepoch {epoch}: loss {loss.item():.4f}, val_mIoU {miou:.4f})这里的关键是ReduceLROnPlateau的modemax因为传入的监控值是mIoU越大越好。patience5表示连续5个epoch mIoU没有上升才降低学习率。UNet使用时的注意事项里最容易被忽略的一条是如果换了数据集或改了backbone学习率必须重新试不要迷信1e-4先跑5个epoch看loss趋势梯度方向正确再放开全量训练。另外训练时记录每个epoch的验证mIoU前20个epoch mIoU在0.3以下不必慌遥感分割的收敛曲线通常比较慢但50个epoch后还在0.2附近打转就要回头查数据了。5. 遥感图像语义分割常见问题避坑五个让UNet翻车的场景这一章是整篇笔记里最值得先读的部分。以下是训练和评估阶段最常见的五个问题按“现象→原因→解决”的方式记录都是踩过坑之后沉淀下来的经验。5.1 训练阶段的三类典型症状loss卡住、结果抖动、小类丢失loss卡在0.6-0.7附近不再下降mIoU约等于0.1现象训练loss前10个epoch从0.7掉到0.65之后几乎走平验证集mIoU只有0.1左右。原因最常见的是mask读取错误。很多遥感数据集的mask是调色板PNG直接Image.open再转numpy得到的是调色板索引而不是像素灰度值或者是图像和label文件没有对齐模型一直在拟合错乱的标签。这类问题不看数据永远发现不了。解决训练前跑一遍第2.3节的标签体检脚本打印训练集所有mask的np.unique()值确认类别ID范围再随机取3-5个样本把图像和mask用matplotlib叠在一起可视化。这步只需要5分钟但能排除掉一大半训练玄学问题。训练loss正常下降但验证集mIoU忽高忽低振幅达到5个点以上现象同一epoch的验证mIoU在不同运行之间波动很大曲线像锯齿。原因batch size太小BatchNorm统计量不稳定。遥感切片之间类别分布差异极大一片全是农田、一片全是城区batch size只有2-4时BN的均值和方差估计根本稳不住。解决优先把batch size提高到8以上显存不够就换GroupNorm分组归一化替代BatchNorm或者使用梯度累积模拟更大的batch。验证时务必调用model.eval()否则BN层会用训练时的统计量继续更新结果自然不稳定。道路、车辆这类小目标类别在预测结果里几乎全黑现象背景类mIoU很高但个别类别mIoU稳定在0模型完全没有预测出这类目标。原因正样本像素占比太低无论是交叉熵还是DiceLoss梯度都被背景类淹没。解决在交叉熵里给稀有类加权比如把道路类别的loss权重设为背景类的5-10倍更有效的办法是在每个batch里强制混入含稀有类别样本的切片保证每个batch都能见到正样本。DiceLoss本身对不平衡有抑制作用但如果某个类别在整个训练集里占比低于0.5%单靠Dice还是不够需要配合过采样。5.2 评估与推理阶段的两个隐藏炸弹数据泄漏、拼接棋盘格切片验证集mIoU有0.7但整图预测结果惨不忍睹现象把所有patch的验证指标汇总起来看很高但把整景影像切成patch推理再拼回去大片地物漏检、边界错乱。原因数据泄漏。切训练集和验证集时没有按原图划分同一个原图的切片同时混进了两边验证指标虚高或者用随机种子对整个数据集切片没有先分景再切图。一旦丢掉“未见过”的完整原图模型的真实泛化能力就暴露了。解决严格按原图划分数据集。以数据集的“景”scene为单位8景用来训练切patch2景用来验证切patch切片阶段绝不混用。另外推理时对大图采用镜像padding的滑动窗口预测能缓解边缘切片的效果下降。预测结果出现周期性网格线或棋盘格纹理现象整图预测结果的边界处出现规律性黑线、网状纹理重叠拼接区域尤其明显。原因两个来源叠加。一是ConvTranspose2d在kernel_size不可整除时存在重叠写入产生棋盘效应这在反卷积上采样里很常见二是滑动窗口推理时相邻patch的预测结果不一致直接硬拼接产生接缝。解决上采样改用双线性插值普通卷积的组合棋盘格会明显缓解推理时让滑窗重叠128像素对重叠区域的预测概率做高斯加权平均再接argmax。这个技巧对最终分割图的观感提升很大答辩展示时不容易被问住。6. 从“跑通”到“答辩能讲”UNet模型改进的落地路径与验证方法把基线UNet跑通之后毕设的下一关是“有什么自己的改进”。这里给三个低风险、效果稳定、且答辩时容易讲清楚的改进方向都在遥感图像语义分割里被反复验证过。6.1 换编码器backbone或加注意力模块最省事的改进是把UNet的编码器换成预训练ResNet34直接用segmentation_models_pytorch库可以少写很多代码。要注意的是预训练权重是在ImageNet上训的要求输入是三通道RGB如果用了多光谱数据预训练权重就用不了需要从头训练。不想引入额外库时可以自己在解码器里加SCSE注意力模块。这个模块同时做通道注意力让网络更关注地物类别区分度强的通道和空间注意力让网络聚焦在目标区域改动量小在遥感分割里提升稳定。要验证改进是否有效务必在同一训练配置下对比只改模型结构其他参数保持一致。6.2 用深监督加速收敛并展示多尺度特征深监督是在解码器的中间层也接上1x1卷积输出预测和最终输出一起计算loss。这样强制浅层特征也具有可判别性训练收敛更快aux_out3 self.aux3(d3) # 中间层预测 loss criterion(logits, lbl) 0.4 * criterion(aux_out3, lbl)辅助loss的权重一般取0.30.4推理时只用最终输出。答辩时可以把中间层的预测可视化证明网络在浅层学到了轮廓、深层学到了语义这也是一个很好展示的“改进可视化”。我在自己的毕设里走过最关键的一次弯路是把所有修改都堆到一个模型里结果出了问题根本定位不到环节。后来改成每加一个改动就跑一遍完整对比实验参数和结果记成表格效率反而最高。技巧这东西始终要自己试过才知道边界在哪希望这篇笔记能帮你少走一段弯路。本文还有配套的精品资源点击获取