UNet遥感图像语义分割实战:从数据制作到调参避坑指南
简介一份面向遥感图像语义分割方向的毕业设计项目围绕UNet模型讲解从数据标注、预处理、网络搭建到训练评估的完整流程适合计算机视觉或遥感相关专业的学生开展课题研究。压缩包共69个文件约47.26MB涵盖Python源码、PyCode缓存、多个IPython Notebook、论文LaTeX及PDF文档以及PNG图像、SVG矢量图等辅助素材可对照代码与论文理解实现细节。此外还带有启动Jupyter和TensorBoard的PowerShell脚本方便直接搭建运行环境。目前已有346人学习下载具备较高的参考价值。通过该项目可掌握UNet编码器-解码器结构、遥感图像像素级分割、IoU等指标评估方法并借助自带的数据创建、训练、预测三个notebook快速复现实验论文部分还包含章节源文件便于改写和引用。整体目录清晰适合作为毕业设计或课程项目的起点也为后续改进网络或扩展数据集留出空间。1. 基于UNet的遥感图像语义分割毕设选题前先想清楚的三个问题导师抛给你一句“做一个基于UNet的遥感图像语义分割”听起来像是个成熟得不能再成熟的方向。但真把数据集下下来、把第一个epoch跑起来之后你大概率会撞上一连串没法在论文里写的事图像太大显卡装不下、标签里的类别数和你数的不一样、验证集mIoU卡在0.6上不去、预测结果边缘锯齿严重得像马赛克。这些才是毕设真正要花时间的地方。这篇笔记打算把UNet跑通遥感图像语义分割的完整路径拆开来讲从模型选型、数据制作、训练调参到排查翻车尽量做到你照着走能少走一半弯路。方向适合遥感、地信、计算机视觉方向的本科毕设也适合刚入门语义分割的工程新手。2. 为什么选UNet而不是DeepLabV3遥感图像语义分割的模型选型逻辑2.1 遥感图像的分割难点尺度差异、类别不平衡与边缘模糊遥感图像和自然图像有本质区别。一张无人机拍的地面影像尺寸动辄几千乘几千像素一栋房子的屋顶占几十个像素旁边的道路宽不过三五个像素整个画面里的地物尺度差异非常大。把整张图直接塞进UNet既不现实也没必要常规做法是切成小块来训练切块策略直接影响模型能学到什么。另一个问题是类别极端不平衡一幅影像里植被可能覆盖70%的区域建筑物只占5%如果直接按像素算交叉熵模型会把所有东西都预测成植被因为这样损失函数已经够低了。边缘模糊也是遥感影像的常见问题地物边界在影像上往往是一两个像素的渐变色带人工标注时不同人标出来的边界位置都不同这限制了语义分割的天花板。这些难点决定了选型方向模型不一定要最复杂但结构要和任务匹配。UNet的U型结构天然适合需要精确定位边界的密集预测任务跳跃连接把浅层的空间细节和高层的语义信息拼接起来恰好缓解了遥感地物边缘模糊的问题。DeepLabV3用空洞卷积扩大感受野在自然图像上往往比UNet表现好但在遥感小目标密集、边界要求高的场景下它的优势并不明显还多了不少需要调节的超参数。2.2 UNet的U型结构与跳跃连接小样本也能训练的原理UNet之所以在遥感语义分割里经久不衰核心在它的结构设计。它由一个编码器和一个解码器组成编码器逐层下采样每经过一级池化特征图分辨率减半、通道数翻倍解码器逐层上采样把分辨率恢复到原图大小最终输出每个像素的类别概率。中间的跳跃连接把编码器每一层的特征图直接拼接到解码器对应层让解码时既能看到高层的语义信息又能看到底层的边缘细节。跳跃连接带来的好处是小目标地物的空间位置信息不会在多次下采样后完全丢失编码器-解码器对称结构在标注数据有限时表现出很强的拟合能力不容易因为参数量过大而严重过拟合遥感影像普遍标注成本高一个像样的数据集几千张切片已经算不错了UNet在几百到几千张规模的数据上就能收敛到可用的精度这是它替代更复杂模型的决定性理由这背后的原理是特征复用跳跃连接相当于给解码器提供了“短路通道”梯度可以直接通过这条通道回传到浅层缓解深层网络的梯度消失问题让训练更容易收敛。这在实际操作中体感很明显同样数据集DeepLabV3可能要训练到第80个epoch才开始稳定上升UNet通常在40个epoch左右就基本定型了。2.3 备选方案对比与结论哪种任务下换其他骨架更划算做选型时不用太过纠结。下表把常见的几种语义分割模型在遥感图像场景下的表现按经验做了对比你可以根据自己毕设的数据规模和任务要求来选。模型参数量级小目标精度边缘质量训练难度适合场景UNet约34M中上好低样本量小、类别数少、通用分割DeepLabV3约43M中中中大类别、背景占比高、需要大感受野PSPNet约47M中下中中场景解析、大范围地物分类SegNet约29M偏低中低对速度要求高、对精度要求低HRNet约30-65M高好高小目标密集、边界要求极高如果你毕设的主题是“基于UNet的改进”比如加上注意力模块、残差连接或transformer块UNet依然是最合适的基线因为它的结构简单、改进点多、复现门槛低你可以在编码器、解码器、跳跃连接三处分别做文章每一处都能在论文里单独拉成一章。如果导师暗示最好蹭一下最新热点比如对比一下当前主流的分割模型那出门左转去看YOLO v26的实例分割、以及自注意力机制的语义分割模型但主力网络建议还是UNet。3. 把遥感影像变成能喂给UNet的数据集标注、切片与归一化实操3.1 标注工具怎么选LabelMe的JSON到8位PNG掩膜语义分割数据准备的核心是把多边形标注转成逐像素的类别掩膜。遥感图像标注常用的工具有LabelMe、LabelImg、QGIS配合半自动标注插件我一般推荐LabelMe原因在于它输出的是JSON格式每个多边形都带顶点坐标和标签名转成掩膜时你拥有完全的控制权不会像某些工具那样直接在图像上画掩膜导致边界区域混入误标像素。安装和启动LabelMepip install labelme labelme --labels labels.txt --nodata这是一个最简单的启动方式labels.txt里按行写类别名比如background building road vegetation water--nodata参数表示不保存原始图像的数据副本只保存标注JSON能省不少磁盘空间。LabelMe的标注界面里沿着地物边缘打点每栋房子至少打6到8个点道路要沿中心线打点后调整宽度。标注的精细程度直接决定模型上限后面所有训练都是典型的垃圾进垃圾出标注粗糙的话再怎么调参都白费。标注完成后每个JSON文件里只有多边形的顶点信息和标签名需要转成与图像同尺寸的8位PNG掩膜。转掩膜时注意类别索引从0开始且背景永远是0。这个转换脚本我放在下面import json, numpy as np, cv2, os from labelme import utils json_dir annotations/ mask_dir masks/ os.makedirs(mask_dir, exist_okTrue) for json_file in os.listdir(json_dir): if not json_file.endswith(.json): continue with open(os.path.join(json_dir, json_file), r, encodingutf-8) as f: data json.load(f) imageData data.get(imageData) img utils.img_b64_to_arr(imageData) # 从base64还原图像 label_name_to_value {_background_: 0} for shape in data[shapes]: name shape[label] if name not in label_name_to_value: label_name_to_value[name] len(label_name_to_value) lbl, _ utils.shapes_to_label(img.shape, data[shapes], label_name_to_value) mask (lbl.astype(np.uint8)) # 保存原始图像和掩膜命名保持一致 base os.path.splitext(json_file)[0] cv2.imwrite(os.path.join(mask_dir, base .png), mask)utils.shapes_to_label会把多边形在对应位置填充成类别索引值其余位置为0输出的数组尺寸和原图一致。保存时用cv2.imwrite写PNG注意掩膜必须保持8位单通道不要用JPEGJPEG的有损压缩会在类别边界产生伪彩色条纹训练时会被当成错误标签这个坑我踩过不止一次。3.2 用GDAL切瓦片重叠切片与无损坏图像格式遥感原图很大动辄上万像素没法直接训练。需要按固定尺寸切成瓦片每个瓦片同时包含图像块和对应的掩膜块。切瓦片时建议保留一定的重叠区域比如滑动步长取图像边长的一半这样目标地物不会恰好被一条切割线劈成两半。我没有编造数据但很多遥感分割项目都这么做这也符合论文里的常规做法。用GDAL切瓦片是最可靠的方式因为GDAL能直接读GeoTIFF而且支持多波段遥感影像比如红绿蓝近红外四波段普通的OpenCV读TIFF会丢波段信息。切瓦片的代码框架如下from osgeo import gdal import numpy as np, os def split_image_to_tiles(src_path, mask_path, out_dir, tile_size512, stride256): src_ds gdal.Open(src_path) mask_ds gdal.Open(mask_path) width, height src_ds.RasterXSize, src_ds.RasterYSize bands src_ds.RasterCount os.makedirs(out_dir /images, exist_okTrue) os.makedirs(out_dir /masks, exist_okTrue) count 0 for y in range(0, height - tile_size 1, stride): for x in range(0, width - tile_size 1, stride): img_win src_ds.ReadAsArray(x, y, tile_size, tile_size) # (bands, h, w) mask_win mask_ds.ReadAsArray(x, y, tile_size, tile_size) # (h, w) # 过滤掉超过80%像素是背景的瓦片减少训练样本不均衡 if (mask_win 0).sum() / mask_win.size 0.8: continue img_save np.transpose(img_win, (1, 2, 0)) # 转为(h, w, bands) np.save(f{out_dir}/images/{count:05d}.npy, img_save) np.save(f{out_dir}/masks/{count:05d}.npy, mask_win) count 1 print(fsaved {count} tiles)ReadAsArray按给定窗口偏移量读取一块数据第一个参数是行偏移第二个是列偏移后面两个是窗口宽高。np.save的好处是直接把数值原样存成二进制不会走编码压缩后续训练时直接用np.load读回来比一张张读PNG快得多。为什么滑动步长取256而不是512呢因为交叉区域的像素会出现在两个相邻瓦片里增加了有效样本量而且预测时拼回大图也需要重叠区域来做平滑过渡这一点在后面的拼接避坑章节会细讲。3.3 归一化是第一个坑逐幅统计还是逐块统计归一化策略对整个训练影响很大。遥感影像的动态范围和自然图像很不一样近红外波段的数值范围可能比可见光波段大很多如果不处理模型会天然偏向数值大的波段。最常见的问题是有人直接把每个训练瓦片分别减均值除标准差这会带来灾难性后果每张图的统计量都不同相当于输入分布一直在变模型永远学不到稳定的特征。常规做法是拿训练集里所有瓦片统一统计均值和标准差然后在训练时对每张瓦片用同一组统计量做标准化。这个过程最好在切瓦片之前对整幅影像做统计统计完再切。下面这行代码是在切瓦片前先算好全图的均值和标准差多波段各算各的mean np.zeros(bands) std np.zeros(bands) for b in range(bands): band_arr src_ds.GetRasterBand(b 1).ReadAsArray().astype(np.float32) mean[b] band_arr.mean() std[b] band_arr.std()训练时加载npy文件后这样处理img (img - mean) / std注意验证和预测时也必须用同一个mean和std不能用当前批次重新统计否则训练和推理阶段的数据分布不一致验证集的mIoU会虚高上线后效果崩掉。逐幅影像归一化是仅在使用单一影像做语义分割、且没有做跨影像泛化需求时才采用的方案毕设如果有多景影像千万别用。4. 用PyTorch搭建UNet并训练自己的数据集核心代码与关键参数4.1 UNet的核心结构双卷积块、上采样与跳跃连接的实现自己搭一个UNet并不难难的是把结构的每一处细节改对。UNet的骨架是多次“双卷积 下采样”组成的编码器和“上采样 跳跃拼接 双卷积”组成的解码器。下面这份实现是比较标准的写法里面特意加了dropout和批归一化训练遥感数据比原版UNet更稳定import torch import torch.nn as nn class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch, dropout0.1): super().__init__() self.conv nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1, biasFalse), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Dropout2d(dropout), nn.Conv2d(out_ch, out_ch, 3, padding1, biasFalse), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Dropout2d(dropout), ) def forward(self, x): return self.conv(x) class UNet(nn.Module): def __init__(self, in_channels3, num_classes5): super().__init__() # 编码器 self.enc1 DoubleConv(in_channels, 64) self.enc2 DoubleConv(64, 128) self.enc3 DoubleConv(128, 256) self.enc4 DoubleConv(256, 512) self.pool nn.MaxPool2d(2) # 瓶颈 self.bottleneck DoubleConv(512, 1024) # 解码器 self.up4 nn.ConvTranspose2d(1024, 512, kernel_size2, stride2) self.dec4 DoubleConv(1024, 512) self.up3 nn.ConvTranspose2d(512, 256, kernel_size2, stride2) self.dec3 DoubleConv(512, 256) self.up2 nn.ConvTranspose2d(256, 128, kernel_size2, stride2) self.dec2 DoubleConv(256, 128) self.up1 nn.ConvTranspose2d(128, 64, kernel_size2, stride2) self.dec1 DoubleConv(128, 64) self.out_conv nn.Conv2d(64, num_classes, kernel_size1) def forward(self, x): e1 self.enc1(x) e2 self.enc2(self.pool(e1)) e3 self.enc3(self.pool(e2)) e4 self.enc4(self.pool(e3)) b self.bottleneck(self.pool(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_conv(d1)关键点有两个跳跃连接用的是torch.cat做通道拼接不是相加这可以保留编码器浅层的空间信息让解码器同时看到原始分辨率的边缘细节ConvTranspose2d负责上采样它的参数stride2表示特征图尺寸翻倍。第一层卷积的padding1是为了保持特征图空间尺寸不变。如果你想把输入分辨率从512降到256训练只需改第一个卷积的输入尺寸对应的图像尺寸不需要动结构。dropout用0.1就够了太高会让训练收敛变慢并损害精度我见过有人把dropout设成0.5然后跑来问为什么UNet的mIoU只有0.3大概率问题出在这。另外BatchNorm2d在batch size比较小的时候会不准如果你的显卡只能跑batch size4建议把track_running_stats关掉或者换InstanceNorm否则训练集和验证集的统计量差异会变大。4.2 损失函数选型交叉熵还是Dice Loss训练语义分割最常见的损失函数是交叉熵但在遥感图像类别不平衡的场景下最好用Dice Loss或Focal Loss也可以把两类损失按比例相加。Dice Loss的公式是对每个类别的预测区域和真实区域计算Dice系数再对所有类别求平均。它的好处是直接优化和mIoU高度相关的一个指标对小类别的惩罚更加明显。我用的是两者的加权和class CombinedLoss(nn.Module): def __init__(self, num_classes, weightNone): super().__init__() self.num_classes num_classes self.register_buffer(weight, weight) def forward(self, pred, target): ce nn.functional.cross_entropy(pred, target, weightself.weight) pred_softmax torch.softmax(pred, dim1) target_onehot torch.nn.functional.one_hot(target, num_classesself.num_classes).permute(0, 3, 1, 2).float() smooth 1.0 dice 0.0 for c in range(self.num_classes): inter (pred_softmax[:, c] * target_onehot[:, c]).sum() union pred_softmax[:, c].sum() target_onehot[:, c].sum() dice 1 - (2 * inter smooth) / (union smooth) return ce dice / self.num_classes这里的self.weight是通过类别像素频率倒数计算出来的一个tensor例如某一类像素数占总像素数的1%权重就给到50左右但注意不能设太大否则会带来样本抖动过大模型可能把其他所有类别都给误分掉。实际操作中我会先跑一次无权重交叉熵统计混淆矩阵再根据混淆矩阵去向那几类错误最严重的类别加权。Dice Loss自带平滑系数Smooth1防止除以0同时当某个类别在某个batch里完全没出现时也能正常计算。4.3 训练参数batch size、学习率、早停与类别加权训练参数不是拍脑袋定的跟数据量、图像尺寸和显存有关。我通常的设置如下参数推荐值说明输入尺寸512x512太大显存不够太小地物细节丢失batch size8根据显卡显存调整低于4时考虑关BN初始学习率1e-4用Adam时推荐 1e-4 到 3e-4 之间学习率衰减CosineAnnealing或选择步长衰减每20个epoch乘0.5epoch数60-100数据量小就少训大就多训早停 patience15连续15个epoch验证集mIoU不升则停类别加权频率倒数只在类别极不平衡时用训练流程的代码写起来不长但有几个细节必须注意。第一个是混洗验证集绝不能参与训练第二个是验证集要跟训练集来自不同的地理区域不然泛化能力测试没有意义。下面是一个训练循环的骨架for epoch in range(epochs): model.train() train_loss 0 for img, mask in train_loader: img, mask img.cuda(), mask.cuda() optimizer.zero_grad() pred model(img) loss criterion(pred, mask) loss.backward() optimizer.step() train_loss loss.item() # 验证 model.eval() val_miou 0 with torch.no_grad(): for img, mask in val_loader: img, mask img.cuda(), mask.cuda() pred model(img) pred_label pred.argmax(dim1) val_miou compute_miou(pred_label, mask) scheduler.step() if val_miou best_miou: best_miou val_miou torch.save(model.state_dict(), funet_best_epoch{epoch}.pth)这里的compute_miou是自定义函数后面在评估章节会给出完整实现。训练流程中一个非常容易踩的坑是验证阶段忘了with torch.no_grad()导致显存被推理图占用batch size稍微大一点就OOM掉。注意保存模型时不要只保存最后一个epoch的权重一定要保存验证集mIoU最高的那一份。训练后期模型会过拟合最后一个epoch的权重点往往比不过第50个epoch的找个deep copy的教训就是到了答辩前才发现自己存错了权重文件。5. UNet训练遥感图像的排坑笔记五个最常见的翻车现场5.1 损失函数先降后升半精度训练叠加归一化错误现象训练loss一开始正常下降到第20个epoch左右突然开始暴涨验证集mIoU跟着崩溃而且再也回不到之前的高点。原因用了PyTorch的AMP半精度自动混合精度训练但归一化的mean和std是按全精度算的某些波段标准差接近0或者极小导致归一化后的数值范围特别大半精度下直接溢出变成Inf或NaN。一旦出现NaN反向传播的梯度全乱套模型参数就废了。解决要么放弃AMP训练全程fp32要么在归一化时给std加一个极小值做保护std np.maximum(std, 1e-6)。检查损失是否为NaN应该在每个epoch结束后跑一次if not np.isfinite(loss.item()): print(loss is not finite, stopping training) break5.2 预测图全是背景类别权重与阈值设置不当现象模型在训练集和验证集上跑出来的mIoU挺正常但拿一景新影像测试时预测结果全是背景地物一个都没识别出来。原因很多时候是因为训练时用了类别加权把背景的权重压得很低而新影像的辐射特征和训练影像差异较大模型在其他类别上的置信度都偏低。预测时直接取argmax置信度最高的还是背景。叠加归一化的mean和std是训练影像算的新影像的直方图分布不一样特征偏移直接把预测推向背景类。解决预测前先对新影像做直方图匹配让它分布接近训练影像然后不要盲信argmax对每个像素取softmax后看最大概率值如果低于0.5就判为背景这就是置信度阈值过滤能避免模型“强行”给一个不确定像素分类别。阈值通常设在0.4到0.6之间自己用一小块标注过的区域扫一遍。5.3 拼接结果出现明显的断裂纹路重叠切片策略失效现象训练时mIoU很高把预测瓦片拼回整幅遥感图之后沿瓦片边界出现一条条十字形的断裂纹路视觉效果惨不忍睹。原因切瓦片时用了无重叠的切片方式每个瓦片的边缘预测本身就不可靠边界处地物被切了一半模型只能靠猜测补全拼接时这些猜测不一致就出现了接缝。解决推理时用重叠切片策略步长取瓦片大小的四分之一让同一个像素在多个瓦片中都被预测一次最后取平均或取概率最大能明显抑制接缝。实现逻辑就是在拼接时维护一个计数矩阵和累计概率矩阵prob_accum np.zeros((H, W, num_classes), dtypenp.float32) count np.zeros((H, W), dtypenp.int32) for y0 in range(0, H - tile 1, stride): for x0 in range(0, W - tile 1, stride): tile_img img[:, y0:y0tile, x0:x0tile] prob model(tile_img).softmax(dim1).cpu().numpy() prob_accum[y0:y0tile, x0:x0tile] prob[0].transpose(1, 2, 0) count[y0:y0tile, x0:x0tile] 1 prob_final prob_accum / np.maximum(count[..., None], 1) seg prob_final.argmax(axis2)stride取tile // 4时用max取整张图上的平均概率比取单一次预测更稳。代价是推理时间增加了大约stride/tile倍512大小瓦片步长128时大约4倍的推理时间你可以用步长256做一下权衡效果已经不错了。5.4 大影像显存溢出测试时的分块预测现象一张一万乘一万的影像直接喂给模型显卡直接OOM甚至CPU内存都挤爆了。原因把整幅影像当作一个batch来计算注意力机制和中间特征图占了海量显存比如512x512输入时中间特征图大约是几百MB40000x40000的图像直接就是几何级数增长。解决没有别的办法老老实实分块。上面的重叠切片推理代码就是干这个的注意分块时要把pad成32的倍数因为UNet做了4层下采样图的长宽必须能被16整除不然边缘部分会丢像素。最后一层输出的尺寸和输入对不上时用nn.functional.interpolate插值回原尺寸或直接在padding时补齐。这一般是调通推理流程的常见麻烦。5.5 精度上不去把语义分割当成目标检测调现象mIoU一直在0.5附近徘徊试了换损失、调学习率、加数据增强都没用后来发现路上的小汽车几乎没有预测正确的。原因语义分割和目标检测是两种任务目标检测只关心目标“在哪里”、输出一个框就行语义分割要关心目标的精确轮廓。很多人用目标检测的思路调UNet加了太多检测相关的数据增强比如随机裁剪缩放、旋转90度、水平翻转把标注里的细小地物都给变换没了。遥感影像的地物是有向性的汽车的方向、建筑的朝向都有物理意义随机的水平翻转会混淆模型的朝向判断反而损害精度。解决遥感影像分割的数据增强要谨慎减少90度旋转、水平翻转这类有方向性假设的增强改用颜色扰动、高斯模糊、光照变化。如果确实想旋转就做15度、30度的小角度旋转并配合掩膜同步旋转不要做180度翻转。6. 让分割结果再进一步多尺度推理与CRF后处理技巧训练的mIoU稳定后如果想在答辩时让效果图更好看一点可以在推理阶段做两件性价比很高的事多尺度推理和条件随机场CRF后处理。多尺度推理的做法是把同一张瓦片缩放成0.75倍、1.0倍、1.25倍三个尺度分别输入模型把三次的softmax概率矩阵插值回原尺寸取平均再取argmax。这一招通常能把mIoU提升1到3个百分点代价是推理时间变成三倍。缩放时用cv2.resize配INTER_LINEAR或INTER_CUBIC在语义分割上差别不大。CRF后处理对边缘质量的改善非常明显。UNet预测的类别图在边缘处经常有椒盐噪声一样的孤立误分类像素用全连接CRF把这些孤立点修掉能让分割边界贴合影像中的真实灰度边缘。pydensecrf是个常用的CRF库但编译环境比较折腾现在很多项目直接用opencv-contrib-python里的ximgproc.segmentation接口做快速分割或者用“多数投票滤波”这种更省事的办法对每个像素取它周围3x3或5x5邻域里出现次数最多的类别作为该像素的最终类别相当于一个轻量级的后处理平滑。这种操作虽然简单但对mIoU的数值提升可能并不明显因为它同时可能消除真正的细小地物所以做不做取决于你的目标是以数值为准还是以视觉效果为准。验证一个后处理是否有用的是在验证集上跑分割、做后处理、再算mIoU和边界F1分数。不要看整幅图的主观印象去做判断人眼很容易被大片背景的正确分类带偏忽略掉细小地物的错误。就我自己的习惯而言每次拿到一个新数据集第一步一定是训练一个最朴素的UNet当基线然后把验证集里的错误预测可视化出来看哪些类别容易混、哪些边界切得不对再决定改损失函数还是改模型结构。而不是一上来就堆各种改进模块最后加了十个模块也不知道哪个起了作用。希望这篇笔记里的代码和踩坑经验能让你把毕设的UNet从能跑变成能用少熬几个深夜少掉几根头发也帮你在答辩时有底气地回答“为什么你的模型是这么设计的”。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →