简介:遥感城市图像语义分割数据集面向计算机视觉初学者与遥感影像分割研究者,提供约1000张已标注的遥感图像,覆盖海陆区域等8类地物,图像与标签均已预处理,可直接用于训练语义分割模型。数据已按约800张训练集、300张验证集划分,每张原图对应同名mask模板;附带的classes.txt清晰标注0背景、1建筑等类别含义,便于快速理解标签规范。压缩包内还有Python可视化脚本,可随机抽取样本,将原始图片、GT图像及GT在原图上的蒙板叠加效果展示并保存,方便直观检查数据质量与预测效果。资源共2000个文件,以png(标签/蒙板)、jpg(原始影像)为主,另有说明txt与可视化py脚本,整体仅42.05MB,轻量易获取。目前已有129人学习,可配合U-Net、SwinUNet等分割网络改进专栏进行实践,是开展遥感图像分割实验的高性价比基础数据。
1. 这个遥感城市语义分割数据集:拿到的不是图,是训练后悔药
找训练数据这件事,做过语义分割的都懂:手动标注成本高,类别定义乱,拿到手的标签不是格式不对就是跟图像对不齐。遥感城市图像更是重灾区,建筑、道路、水体、植被挤在一起,高分辨率下逐像素标一版,人先崩溃。一个约1000张、8类别、标签已处理完、可以直接进训练流程的遥感城市图像语义分割数据集,解决的不是“缺数据”,而是把你从“标注-清洗-对格式”这条长链条里解脱出来。
这个方向适合两类人:一类是做城市遥感项目、需要快速验证分割模型效果的工程师;另一类是学生和刚入门语义分割的开发者,想拿现成数据跑通全流程。下面我会把拿到数据之后要做的事拆开讲:先体检,再转换,接着跑通训练,最后告诉你最容易翻车的几个细节。
2. 数据体检:拿到数据集先做这三件事,别急着开训
2.1 先摸清目录结构和标签格式:文件数量、尺寸、位深一次性对齐
语义分割数据集最常见的打包方式是images/和labels/两个平铺目录,images放原始影像,labels放逐像素的标签图。我拿到任何一个现成数据集,不会直接开训,先跑三条命令摸底,确认它是不是“已处理完”。
# 1) 统计图像和标签数量,确认两边是成对的 find datasets/images -type f | wc -l find datasets/labels -type f | wc -l # 2) 抽查前5张图的尺寸和位深 python -c " from PIL import Image import glob img_paths = sorted(glob.glob('datasets/images/*.png'))[:5] for p in img_paths: im = Image.open(p) lb = Image.open(p.replace('images', 'labels')) print(im.size, im.mode, lb.size, lb.mode) "第一条命令确认原图和标签数量一致。数量对不上,说明数据本身有缺失,后续 DataLoader 一跑就报错。第二条命令重点看三样:尺寸、mode、标签是否也是同样的宽高。尺寸不一致的问题在遥感数据里经常出现——有些瓦片边缘被裁掉后没做对齐,进到模型里就要么报错,要么得做 resize,而 resize 标签图会让边界类别对不准。标签图如果是P或L模式,说明是单通道索引图,可以直接用;如果是RGB模式,说明是调色板存储或彩色标注图,第3章会专门处理。
提示:不要用看图软件直接双击标签图看颜色。索引图的像素值只有 0~7,但软件会按调色板渲染成彩色,肉眼看起来“像那么回事”,实际像素值跟你以为的完全不是一回事。
2.2 8个类别到底是谁:像素值统计与类别定义核对
标题说8类别,但具体是哪8类,不同数据集定义不一样。城市遥感图像里常见做法是把地表覆盖分成建筑、道路、植被、水体、车辆、裸地等。拿到手后第一步是读一遍类别定义,我一般会把类别表整理成这样:
| 类别ID | 名称 | 说明 |
|---|---|---|
| 0 | 背景 | 未分类区域,训练时通常当作 ignore_index 或普通背景 |
| 1 | 建筑 | 含屋顶、楼房轮廓,是城市分割的主类别 |
| 2 | 道路 | 含车行道、人行道,关注细连通性 |
| 3 | 植被 | 树木、草地、绿化带 |
| 4 | 水体 | 河流、湖泊、池塘 |
| 5 | 车辆 | 轿车、公交车,小目标 |
| 6 | 裸地 | 施工地、裸土、沙地 |
| 7 | 其他 | 不归入以上类的对象 |
这一张表先立住,后面所有转换和训练都以它为准。不要相信文件名里的色彩暗示,直接对标签做像素值分布统计,看看这8类在整份数据里的占比情况。
import numpy as np import glob from PIL import Image hist = np.zeros(8, dtype=np.int64) for p in glob.glob('datasets/labels/*.png'): arr = np.array(Image.open(p)) hist += np.bincount(arr.ravel(), minlength=8)[:8] print(hist) print((hist / hist.sum()).round(4))这段代码会给你一个很直观的比例分布。以我的经验,城市遥感分割里道路和建筑通常占大头,车辆、水体这类小类别可能只占百分之零点几。如果某个类别像素占比连0.1%都不到,那基本可以肯定后面训练会把它学到消失,需要在第5章讲到的类别权重里做补救。这一步走完,你对这份数据的认知就不再是“约1000张、8类”,而是“哪些类别好分、哪些类别是弱势群体”。这个认知直接决定后面的转换和训练策略。
2.3 训练/验证集划分:按瓦片来源分组,别让验证集泄漏训练集
语义分割的 train/val 划分不能像普通分类那样随机 split。遥感影像通常是一张大图裁成若干瓦片,同一栋建筑、同一条道路会被切到相邻的几张图里。如果随机分配到两边,模型在训练时已经见过验证区域里的纹理和边缘,验证指标会虚高,mIoU 看起来很好,部署到新区域马上露馅。
我建议按文件名前缀分组后再划分。大多数瓦片数据集的命名里带着行列号,比如xx_12_08.png,前缀相同的瓦片来自于同一块大场景,应该放进同一个集合。划分脚本如下:
import glob import random paths = sorted(glob.glob('datasets/images/*.png')) groups = {} for p in paths: name = p.split('/')[-1] # 如 shenzhen_12_08.png prefix = name.rsplit('_', 2)[0] # 去掉行列号,保留大图来源前缀 groups.setdefault(prefix, []).append(p) keys = list(groups.keys()) random.Random(42).shuffle(keys) split = int(len(keys) * 0.8) train_keys, val_keys = keys[:split], keys[split:] train_files = [p for k in train_keys for p in groups[k]] val_files = [p for k in val_keys for p in groups[k]] print(f'train: {len(train_files)}, val: {len(val_files)}')这里的关键是rsplit('_', 2)[0]:把瓦片文件名最后两段“行号_列号”切掉,剩下的部分当作场景来源标识。如果你的数据有地理坐标,甚至可以按经纬度分块,那会更严格。对绝大多数情况,按目录名或文件名前缀分组就够用了。划分完之后生成train.txt、val.txt,后续训练框架直接读这两个文件。
3. 把标签转成训练能吃的索引图:从调色板PNG到单通道label
3.1 标签的真相:调色板PNG和单通道索引图的差别
训练语义分割模型时,标签要被当作CrossEntropyLoss的 target 输入。这个损失函数要求 target 是单通道整数张量,每个像素的值是类别ID,范围在[0, num_classes)之间。问题来了:很多现成遥感数据集的标签是调色板PNG,读取出来是RGB三通道的彩色图,每个类别用一组RGB值表示。
如果直接把三通道彩色图当 target 传给损失函数,PyTorch 会报维度错误,或者在维度对齐时悄悄出错。即使不出错,模型的 logits 是三通道的——height、width、num_classes,标签也是三通道的——height、width、RGB,两个“3”含义完全不同,模型学的是“这张图里哪些颜色组合更像建筑”,而不是“哪些像素是建筑”。
正确的做法是:把彩色标签通过颜色映射表转成单通道索引图,每个像素只保留一个整数ID。这就是标题里“已处理完”的真实含义——数据制作者已经帮你做了大部分工作,但你仍需验证标签到底是不是符合模型输入要求。遥感数据集的标签通常以两种形式存在:
| 存储形式 | 读取方式 | 能否直接训练 |
|---|---|---|
| 单通道索引PNG(L/P模式) | np.array(Image.open(p))出来就是(H, W) | 能,但要核对像素值范围 |
| 彩色调色板PNG(RGB模式) | np.array(Image.open(p).convert('RGB'))出来是(H, W, 3) | 不能,必须先映射成索引图 |
3.2 颜色映射表与转换脚本:把8类标签变成模型认识的整数
先把数据里出现过的颜色全部打出来,看看颜色和类别的对应关系。这一步不能省,不同数据集对同一种颜色含义的定义经常不一样,比如红色可能是建筑,也可能是裸地。打印颜色的脚本:
images = glob.glob('datasets/labels/*.png')[:200] color_set = set() for p in images: arr = np.array(Image.open(p).convert('RGB')) # 把 H*W 个像素的 RGB 三元组变成集合 color_set.update(map(tuple, arr.reshape(-1, 3))) print(sorted(color_set))跑完之后你会拿到一份类似{(0, 0, 0), (128, 0, 0), (0, 128, 0) ...}的颜色集合。接下来要做的是把这组颜色逐一对到8个类别ID上。这个过程要参照你在2.2里整理的类别定义表,如果发现颜色集合里出现第9种颜色,要么是数据标注时留下的噪声,要么是第8类“其他”的定义里有多个颜色。我的建议是:把这个颜色当作背景或 ignore 处理,不要让噪声污染训练。
根据颜色表写映射脚本,把整份标签转成单通道索引图,存到labels_index/目录下:
import numpy as np from PIL import Image import glob import os # 以实际打印出来的颜色表为准,下面只是示例 COLOR2ID = { (0, 0, 0): 0, # 背景 (128, 0, 0): 1, # 建筑 (128, 128, 0): 2, # 道路 (0, 128, 0): 3, # 植被 (0, 0, 128): 4, # 水体 (128, 128, 128): 5, # 车辆 (128, 0, 128): 6, # 裸地 (0, 128, 128): 7, # 其他 } os.makedirs('datasets/labels_index', exist_ok=True) for p in glob.glob('datasets/labels/*.png'): rgb = np.array(Image.open(p).convert('RGB')) h, w = rgb.shape[:2] idx = np.zeros((h, w), dtype=np.uint8) for color, class_id in COLOR2ID.items(): mask = (rgb == color).all(axis=-1) idx[mask] = class_id out_path = os.path.join('datasets/labels_index', os.path.basename(p)) Image.fromarray(idx, mode='L').save(out_path)这段脚本的核心逻辑是mask = (rgb == color).all(axis=-1)。rgb == color把每个像素的三通道和当前颜色逐位比较,得到一个(H, W, 3)的布尔数组,.all(axis=-1)要求三个通道同时相等,最终得到这张图里“哪些像素是当前类别”的掩膜。用uint8存索引图足够,8个类别完全用不完0~255的空间。不要用int64存标签,内存会白白浪费好几倍,后面进 DataLoader 时还影响吞吐。
3.3 转换后的三个校验等式:数量、维度、类别完整性不能少
转换完不能直接开训,先验证一遍“没翻车”。我习惯做三个校验:原图和标签数量一致、尺寸一致、索引图的类别集合等于0~7。
glob.glob('datasets/labels/*.png') == glob.glob('datasets/labels_index/*.png') # 数量一致 import numpy as np from PIL import Image import glob bad = [] for p in glob.glob('datasets/labels_index/*.png')[:200]: img = Image.open(p) arr = np.array(img) # 校验1:单通道 assert arr.ndim == 2, f'{p} has {arr.ndim} dims' # 校验2:类别ID完整 if set(np.unique(arr)) - set(range(8)): bad.append(p) print(f'{p} -> {np.unique(arr)}') print('校验完成')这里的assert arr.ndim == 2确保存出来的是单通道索引图,set(np.unique(arr)) - set(range(8))检测有没有出现 0~7 之外的像素值。出现意外值只有两种可能:颜色表漏填了某种颜色,或者原始标签有标注边界噪声。如果是漏填,去补映射表再跑一遍;如果是噪声,这个现象我要提醒你:直接去掉或把它设成背景,不要在训练时才发现问题,那会儿定位成本高得多。
把转换好的目录结构固定下来,后续所有训练脚本都从这里读数据:
datasets/ ├── images/ # 原始遥感影像 ├── labels_index/ # 转好的单通道索引图 └── splits/ ├── train.txt # 训练集文件名列表 └── val.txt # 验证集文件名列表train.txt里每行一个文件名(不带扩展名),大多数训练框架的CustomDataset都认这种格式。这一步做完,数据才是真正“可以训练”的状态。
4. 用 SegFormer 把这个数据集跑通最小训练流程
4.1 模型选型:SegFormer、DeepLabV3 还是 Mask2Former
1000张遥感城市图像,8个类别,这个体量下选模型要考虑两个因素:一是遥感图像的尺度跨度大,建筑边缘细节和小目标并存;二是数据量有限,模型不能太贪。我建议第一版直接上 SegFormer-B0 或 B1。
对比一下常见选型:
| 模型 | 特点 | 对这个数据集的适配 |
|---|---|---|
| UNet | 占用资源少,收敛快 | 作为 baseline 可以,但对大尺度遥感图像感受野不够 |
| DeepLabV3 | 空洞卷积,多尺度能力好 | 稳定,适合当对比模型 |
| SegFormer | Transformer 编码器加轻量MLP解码头,迁移动态好 | 1000张数据量下不容易过拟合,推荐先用 |
| Mask2Former | 掩码级训练,效果强但参数多 | 数据量到这个规模容易过拟合,不建议一上来就用 |
如果你之前用过 YOLO 系列做目标检测或实例分割,那要确认一点:YOLO 家族解决的是“框住目标”和“分开每个实例”,语义分割要的是逐像素的类别归属,不区分个体。所以这里不能用检测框架的思路,得走像素级分类模型。Mask2Former 虽然能做语义分割,但它本质是掩码训练范式,调参成本高,数据规模不够时收益不明显。
4.2 用 mmsegmentation 把数据塞进训练管线
这里我用 mmsegmentation 来跑,因为它是语义分割里配置化程度最高的框架,改数据集、改模型、改损失都不用动主代码。首先准备一个数据配置,告诉框架从哪里读图、从哪里读标签:
# configs/_base_/datasets/remote8.py dataset_type = 'CustomDataset' data_root = 'datasets/' train_pipeline = [ dict(type='LoadImageFromFile'), dict(type='LoadAnnotations'), dict(type='RandomFlip', prob=0.5), dict(type='RandomRotate', prob=0.5, degree=90), # 遥感图旋转不变性 dict(type='Resize', img_scale=(1024, 1024), ratio_range=(0.8, 1.2)), dict(type='Normalize', mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375]), dict(type='PackSegInputs') ] data = dict( train=dict( type=dataset_type, data_root=data_root, ann_file='splits/train.txt', img_dir='images', seg_map_dir='labels_index', pipeline=train_pipeline), val=dict( type=dataset_type, data_root=data_root, ann_file='splits/val.txt', img_dir='images', seg_map_dir='labels_index', pipeline=val_pipeline) )seg_map_dir指向的是第3章转换出来的labels_index,不是原始labels。这个位置最容易写错,写错的话框架不会报错,但训练出来的模型会认为“标签是彩色图”,推理结果一团糟。
模型配置里最关键的只有两处:num_classes=8,以及损失函数设置。SegFormer 默认的解码头来自 ADE20K 的150类,不改num_classes的话模型输出通道维度对不上,训练直接崩。辅助头也要跟着改成8类,否则主头学好了,辅助头还在往150类上输出。
# segformer_b0_1024x1024_remote8.py model = dict( type='EncoderDecoder', backbone=dict( type='MixVisionTransformer', init_cfg=dict( type='Pretrained', checkpoint='checkpoints/segformer_mit-b0_512x512_160k_ade20k.pth')), decode_head=dict( type='SegformerHead', num_classes=8, loss_decode=dict(type='CrossEntropyLoss', use_sigmoid=False, loss_weight=1.0)), auxiliary_head=dict( type='FCNHead', num_classes=8, loss_decode=dict(type='CrossEntropyLoss', loss_weight=0.4)) )训练时用分布式命令或单卡命令都行,单卡先跑通:
bash tools/dist_train.sh configs/segformer_b0_1024x1024_remote8.py 1第一次跑通训练后,先看 train 集上的 loss 是否稳定下降,再看 val 集上的 mIoU。如果 loss 降到0.2以下还继续跑,观察验证指标是否同步上涨,出现“loss降但mIoU不动”的现象,直接跳到第5章5.1。
4.3 这个数据集上最值得调的4个超参数
1000张遥感数据不算多,超参数调对能省一倍时间。我一般先盯这4个:
crop size。训练时的裁剪尺寸选1024或768。遥感图像分辨率高,如果按 ImageNet 习惯用512,建筑边缘细节和车辆等小目标在下采样两层后直接消失。显存有限就选768,不要低于512。
batch size。SegFormer-B0 + 1024裁剪,单张12G显存大概能放4张。batch小导致BN统计不稳定,用梯度累积来模拟更大的batch:每4步累积一次,等效batch=16,mIoU能比直接batch=4提高1~2个点。
学习率。用2e-4作为初始学习率,backbone 部分的 lr 乘0.1。遥感图不像自然图像那样有大量预训练特征可直接迁移,主干学习率太高会把预训练权重冲掉,太低又学不到遥感特有的纹理。
ignore_index。如果你的标签里有255或未标注区域,在损失里设ignore_index=255。不设置的话,未标注区域的像素直接参与梯度计算,把模型往错误方向拉。设置完记得验证一下标签里确实没有255以外的非法值,否则第3章的校验白做了。
这个流程跑完,你应该有一个能收敛的模型。接下来真正区别经验高低的是踩坑排查能力。
5. 遥感语义分割避坑清单:1000张数据上最容易翻车的5个地方
5.1 现象:训练 loss 一直降,val mIoU 就是不动
训练到第5个epoch,loss 从1.2掉到0.3,打印 val 集的 mIoU 却只有50%,而且连续几个epoch不涨。原因是遥感城市数据的类别分布极度不均衡,道路、建筑、背景可能占了全部像素的85%以上,交叉熵损失被大头类别主导,模型只需要把每张图的建筑和道路分对,loss 就能很低,车辆、水体这些小类别学不学无所谓。
解决方法是给损失函数加类别权重。权重用中位数频率平衡法,对占比小的类别赋予更高的权重:
import torch import torch.nn as nn # class_freq 来自第2.2节的像素直方图统计 freq = torch.tensor(class_freq, dtype=torch.float32) weights = 1.0 / torch.log(1.02 + freq) weights = weights / weights.sum() * len(weights) # 归一化 criterion = nn.CrossEntropyLoss(weight=weights, ignore_index=255)log的底数是2,1.02是平滑项,防止某类占比太低导致权重爆炸。加了权重之后 loss 数值会上升,这是正常现象,不要因为 loss 变大就回退,盯着 val mIoU 看。
5.2 现象:建筑边缘像锯齿,细道路被识成断线
预测图上建筑的轮廓边缘有明显锯齿,4~8像素宽的人行道被切成一段一段。原因是模型下采样 stride 过大,小目标和高频边界被池化层抹掉了。遥感图像的细节纹理比自然图像密集,道路边缘在原始图上可能只有3~5像素,经过2次下采样就剩1个像素,再进到注意力模块里基本没信息了。
三个手段组合解决:第一,推理时用第6章的滑窗,降低单次缩放带来的信息损失;第二,训练时开辅助损失auxiliary_head,让浅层特征也参与梯度回传,深层语义和浅层边缘各学各的;第三,crop size 不要小于768,Resize 的缩放范围控制在ratio_range=(0.8, 1.2),别把原图压得太狠。
5.3 现象:车辆和水体这类小类别直接消失,mIoU 为0
验证集里车辆类别的 IoU 算出来是0,水体也是0。原因是这两个类别在整份数据里像素占比可能都不到0.5%,连类别权重都救不回来。另一个常见原因是大图里的车辆只有几十个像素,模型下采样后直接消失了。
解决思路是过采样含小类别的图。先统计每张标签里含哪些类别,训练时把含车辆的图重复采样,保证每个批次里都出现车辆。采样逻辑可以参考:
import random # car_images 是统计后得到的“含车辆类别”的图像路径列表 def sample_batch(batch_size): batch = [] # 每个batch里强制放1~2张含小类别的图 batch += random.sample(car_images, k=2) batch += random.sample(all_images, k=batch_size - 2) return batch除了过采样,还有一个容易忽略的点:推理时把原图缩放到1024再进去,车可能只剩3个像素。如果验证指标差在这类小目标上,先停一停检查 inference 时的图像尺度,别一口咬定模型不行。
5.4 现象:val mIoU 比 train 高十来个点
一看指标,val mIoU 65%,train mIoU 52%,直觉不对劲。最可能的原因是数据泄漏——第2.3节里提到的瓦片同源问题。遥感大图切成瓦片后,同一区域的相邻瓦片有大量重叠语义,随机划分时训练集和验证集各自都包含了同一片区域的不同瓦片,模型在训练时已经见过了验证区域。
另一种可能性是验证集正巧挑到了大片连通的建筑区或大路区,这类类别好分,指标虚高。解决方法是回到第2.3节按瓦片来源重新划分,并且验证集按类别均衡抽样,至少保证每个类别在验证集里都能看到20张以上的图。
5.5 现象:可视化输出颜色混乱,和标签对不上
模型推理完,把预测数组用plt.imshow(pred)直接展示,出来的颜色乱糟糟,建筑是紫色,道路是绿色,跟训练标签的风格完全不同。原因是预测结果是类别索引,值是0~7的整数,matplotlib 把它当灰度或默认colormap映射成了伪彩色。这不代表模型训练失败,只是可视化方式不对。
正确的做法是定义一份固定的8类colormap,把索引映射成固定的RGB颜色再显示:
colormap = np.array([ [0, 0, 0], # 背景 [128, 0, 0], # 建筑 [128, 128, 0], # 道路 [0, 128, 0], # 植被 [0, 0, 128], # 水体 [128, 128, 128], # 车辆 [128, 0, 128], # 裸地 [0, 128, 128], # 其他 ], dtype=np.uint8) vis = colormap[pred_idx] # pred_idx 是预测的索引图 (H, W)可视化对比是语义分割项目里最容易被低估的环节。没有统一的colormap,你永远不知道模型到底错在哪里。这个习惯建立起来之后,踩坑速度会明显变快。
6. 重叠滑窗推理:把瓦片接缝和边界抖动一次压掉
6.1 重叠滑窗推理消除接缝
训练完的模型要在大尺寸遥感图上做推理时,不能整图直接塞进网络。显存放不下,而且大图直接缩放会把建筑、车辆这些细节压没了。常见做法是滑窗推理,每块单独预测再拼回去。问题是窗口边缘的预测质量比中心差,拼出来的图有明显接缝。
我给模型推理加一个 overlap 参数,让相邻窗口重叠64像素,重叠区域的预测结果做均值融合,接缝会明显缓解:
def slide_infer(model, img, window=512, overlap=64): h, w = img.shape[:2] step = window - overlap out = np.zeros((h, w, 8), dtype=np.float32) cnt = np.zeros((h, w, 1), dtype=np.float32) for y in range(0, h - window + 1, step): for x in range(0, w - window + 1, step): patch = img[y:y + window, x:x + window] input_tensor = preprocess(patch).unsqueeze(0).cuda() with torch.no_grad(): logits = model(input_tensor) logits = logits.squeeze(0).permute(1, 2, 0).cpu().numpy() out[y:y + window, x:x + window] += logits cnt[y:y + window, x:x + window] += 1 prob = out / cnt return prob.argmax(axis=-1).astype(np.uint8)overlap=64时,窗口中心区域会被多次预测求平均,边缘处的低质量预测被稀释。参数上,1024的窗口配64的overlap基本够用;窗口越大,单次推理越慢但上下文更全。如果你在500张以上的验证集上做评估,滑窗加 overlap 能比整图缩放推理涨1~3个 mIoU 点,尤其是建筑边缘和道路细节。
6.2 用逐类IoU给数据集和模型做体检
全局mIoU只能告诉你模型整体行不行,看不出哪个类别在拖后腿。我的习惯是推理完成后立刻逐类算IoU,把8个类别的分数打出来:
from collections import defaultdict ious = defaultdict(float) for pred, gt in zip(pred_list, gt_list): for c in range(8): inter = ((pred == c) & (gt == c)).sum() union = ((pred == c) | (gt == c)).sum() ious[c] += inter / (union + 1e-6) for c in range(8): print(f'class {c}: IoU = {ious[c] / len(pred_list):.4f}')这份表比任何单一指标都诚实。如果车辆IoU是0,先回去看第2.2节的类别占比,确认训练集里到底有几张图含车辆;如果建筑IoU只有40%,去看可视化的预测图,是边缘锯齿还是大块漏检。把每个类别的失败模式记录在案,再决定调损失、调权重还是补数据。我现在的习惯是换任何数据集第一步就把逐类IoU和瓦片归属表拉出来,否则训练多少次都可能白跑。这个数据集的坑不算多,但绕过的每一个都对应着少走的一段弯路,希望帮到你。
本文还有配套的精品资源,点击获取