拓冰建站拓冰建站
首页 / 资讯中心 / 正文

无人机城市航拍语义分割:270张小样本数据集的完整实践

简介面向无人机城市图像语义分割任务的高分辨率数据集包含建筑、公路等8个类别的像素级标注约270张图像及对应mask划分为训练集200张与验证集70张。资源共543个文件以png图片为主体541个另有类别说明txt与图像分割可视化脚本py压缩包大小263.73MB目录结构清晰便于直接用于分割模型训练与评估。配套可视化脚本可随机抽取图片将原始图、GT图、蒙板图并排展示并保存帮助直观检查标注质量与预测效果尤其适合计算机视觉初学者快速上手也便于中高级学习者基于该数据微调UNet、SwinUNet等分割网络。目前已有386人学习下载是验证语义分割算法、产出实验对比结果的优质小型数据集。1. 270张高分辨率无人机城市图凭什么也能做语义分割智慧城市、数字孪生这些词听着大落到算法团队手里往往就是一批无人机拍的城区航片。图像分割数据集高分辨率无人机城市图像语义分割数据集8类分割约270张数据和标签就是这样的起步级资产——数据量不大但图、标签、类别定义都在能直接拿来跑语义分割。它的价值不是让你刷出一个惊艳的mIoU而是让你在真实标注成本和交期约束下把整条链路跑通数据校验、裁剪增强、训练评估每一步都可复现、可追溯。适合正在做语义分割算法选型、需要快速产出基线、或者想验证标注规范是否合理的人读。2. 拿到数据先盘三件事目录对应、8类像素值与标签校验270张高分辨率影像听起来数量不小但处理不好照样翻车。我拿到这类数据的第一件事不是读论文而是把文件盘清楚。很多项目死在半路根本不是模型问题是数据和标签对不上、通道数理解错、类别定义含糊。2.1 图像与标签的目录对应先写脚本别用眼睛看这类数据集最常见的组织方式是images/和masks/两个目录原图是jpg或png标签是png文件名一一对应。另一种组织方式参考COCO那种JSON单文件标注但语义分割的像素级标签很少这么干逐图对应mask文件才是主流。import os from PIL import Image img_dir images mask_dir masks imgs sorted(os.listdir(img_dir)) masks sorted(os.listdir(mask_dir)) assert len(imgs) len(masks), f图像与标签数量不一致: {len(imgs)} vs {len(masks)} for img_name, mask_name in zip(imgs, masks): stem_img os.path.splitext(img_name)[0] stem_mask os.path.splitext(mask_name)[0] if stem_img ! stem_mask: print(f命名不对应: {img_name} vs {mask_name}) img Image.open(os.path.join(img_dir, img_name)) mask Image.open(os.path.join(mask_dir, mask_name)) if img.size ! mask.size: print(f{img_name} 尺寸不一致: 原图{img.size} 标签{mask.size}) if mask.mode ! P and mask.mode ! L: print(f{mask_name} 不是单通道索引图, 当前mode{mask.mode})这段脚本干三件事数量对齐、命名对齐、尺寸对齐。第三个检查很容易被忽略无人机航片经过后处理或拼接后原图和标签偶尔会出现一两像素的偏差不查出来训练时错位是必然的。第四个检查是看标签通道模式mode P或L才算正常如果是RGB说明标签是彩色编码图后面要单独做转换。我一般还会在跑通脚本后把文件统一重命名成航带号_序号的格式比如A01_001.jpg。这个习惯在后面划分验证集时帮了大忙——你可以按航带分组避免同一条街道同时出现在训练集和验证集里造成指标虚高。2.2 8类标签的像素值约定与彩色标签转换8类分割是个很务实的折中少于6类下游业务违建检测、绿化率统计、道路养护不够用多于10类标注成本急剧上升树荫和道路边界这种区域会让标注员吵起来。这类城市航拍数据集的8类通常是background、road、building、vegetation、water、vehicle、bareland、bridge具体类别名可能略有差异但划分逻辑相似。标签文件本身是单通道索引图每个像素值0到7对应一个类别。注意像素值是类别索引不是灰度图的灰度含义。如果你拿到的是彩色标注图很多标注平台导出RGB PNG方便人眼检查必须先把颜色映射成索引否则模型会把它当成3通道图像去拟合训练直接乱掉。import numpy as np # 先跑一次统计, 找出标签里真实出现的所有颜色, 不要凭想象写映射表 from PIL import Image mask Image.open(masks/A01_001.png) mask_np np.array(mask) print(mask_np.shape, mask_np.dtype) print(np.unique(mask_np.reshape(-1, 3), axis0)) # 彩色标签用这个 print(np.unique(mask_np)) # 单通道索引标签用这个统计结果出来后把每个RGB颜色对应到类别索引建立映射字典再逐像素替换。这里最容易踩的坑是标签里出现了一个你没想到的颜色比如标注员用灰色128,128,128画了阴影区域你漏掉这个键值转换后那块变成默认的0类别统计就脏了。所以转换脚本里必须要有一行断言所有颜色都在映射表里不在就抛异常。提示不管是单通道还是彩色标签训练前统一约定ignore_index255作为忽略区。无人机影像边缘的拼接黑边、标注员明确标记的未知区域都可以填充255让损失函数跳过不参与梯度回传。2.3 可视化叠加与类别频率统计训练前必须先做训练前我会把每张原图和标签做半透明叠加导出成一张大图依次扫一遍。这一步花20分钟省的是后面调模型的好几天。Show me the label before training这句英文在遥感圈流传很久了。import cv2 import numpy as np from PIL import Image # 每类给一个BGR颜色用于可视化, 和训练标签无关 class_colors np.array([ [0, 0, 0], # 0 background [128, 64, 128], # 1 road [70, 70, 70], # 2 building [107, 142, 35], # 3 vegetation [70, 130, 180], # 4 water [0, 0, 142], # 5 vehicle [145, 170, 100], # 6 bareland [180, 130, 70], # 7 bridge ], dtypenp.uint8) img cv2.imread(images/A01_001.jpg) mask np.array(Image.open(masks/A01_001.png)).astype(np.int64) vis class_colors[mask] # (H, W, 3) blend cv2.addWeighted(img, 0.6, vis, 0.4, 0) # 原图压暗, 标签叠上去 cv2.imwrite(check_A01_001.png, np.hstack([cv2.resize(img, (1024, 768)), cv2.resize(blend, (1024, 768))])) unique, counts np.unique(mask, return_countsTrue) print(类别像素占比:) for cls, cnt in zip(unique, counts): print(f class {cls}: {cnt / mask.size * 100:.2f}%)叠加图主要看三类问题一是标签偏移楼顶轮廓和屋顶边缘错了几像素二是漏标车辆、桥这类小目标在标注时最容易遗漏一幢楼可能标了楼前停的几辆车全被忽略三是类别边界含糊人行道算road还是background树荫算vegetation还是background。这些矛盾如果没在训练前统一口径模型学到的就是一个震荡的决策边界。类别频率统计更要认真对待。城市航拍里building和road经常占到总像素的50%以上vehicle、bridge可能不到2%。这个比例直接决定后面要不要用加权损失也决定了验证时哪些单类IoU会长期趴在低位。3. 图像加载与增强裁剪尺寸、增强开关和类别权重怎么定数据盘完下一步是把数据喂进模型。高分辨率无人机影像一个显著的物理现实是单张图可能4000x3000甚至更大直接整图进网络显存先炸BatchNorm统计也会被一张图的内部方差带偏。所以加载策略本身就是这个项目的第一个算法决策。3.1 滑窗裁剪为什么不能直接缩放裁剪尺寸选多大直接resize到512x512会损失大量细节车辆这种20x40像素的小目标会缩成几个像素连人眼都分不清直接整图进模型除了显存问题还有接受野问题——高层特征图上一个点对应原图的感受野是有限的你希望模型能区分相邻两栋楼的边界而不是把整片街区糊成一个语义块。常见做法是训练时做随机裁剪把crop尺寸定在512到768之间。512兼容性好8G显存也能跑768能保留更多上下文小目标漏检率更低但显存和训练时间都要加。我一般以512起步跑通流程确认没bug后再用768精调。import cv2 import numpy as np from torch.utils.data import Dataset CROP_SIZE 512 class CityCropDataset(Dataset): def __init__(self, img_dir, mask_dir, crop_sizeCROP_SIZE, trainTrue): self.img_dir img_dir self.mask_dir mask_dir self.crop_size crop_size self.train train self.names sorted(os.listdir(img_dir)) def __getitem__(self, idx): img cv2.imread(os.path.join(self.img_dir, self.names[idx])) # BGR mask cv2.imread(os.path.join(self.mask_dir, self.names[idx]), cv2.IMREAD_UNCHANGED).astype(np.int64) h, w img.shape[:2] crop self.crop_size if self.train: # 随机裁剪, 样本空间大了很多 if h crop or w crop: img cv2.copyMakeBorder(img, 0, max(0, crop - h), 0, max(0, crop - w), cv2.BORDER_REFLECT_101) mask cv2.copyMakeBorder(mask, 0, max(0, crop - h), 0, max(0, crop - w), cv2.BORDER_CONSTANT, value255) h, w img.shape[:2] y np.random.randint(0, h - crop 1) x np.random.randint(0, w - crop 1) else: # 验证集固定中心裁剪, 结果可复现 y (h - crop) // 2 if h crop else 0 x (w - crop) // 2 if w crop else 0 img img[y:y crop, x:x crop] mask mask[y:y crop, x:x crop] return img, mask这个Dataset的关键点有两个。第一小于裁剪尺寸的图像用BORDER_REFLECT_101做边缘padding标签padding用255ignore因为反射填充对语义分割来说是更好的边界外推纯零填充会在影像边缘引入一圈假黑色区域。第二训练时随机裁剪让模型每轮看到的都是同一张图的不同区域等效于把270张图放大了几十倍这是小样本语义分割最重要的一层“增强”。3.2 Albumentations增强管线哪些开关必须开哪些建议关数据增强库我基本只用Albumentations它对mask和bbox自动同步变换比自己手写np操作靠实得多。针对无人机城市影像增强管线有明确的开与关。import albumentations as A from albumentations.pytorch import ToTensorV2 train_transform A.Compose([ A.HorizontalFlip(p0.5), A.RandomRotate90(p0.5), A.ShiftScaleRotate(shift_limit0.05, scale_limit0.15, rotate_limit15, border_mode0, p0.5), A.RandomBrightnessContrast(brightness_limit0.2, contrast_limit0.2, p0.5), A.GaussNoise(var_limit(10.0, 30.0), p0.2), ]) # Normalize单独放, 只作用于图像, 不作用于mask val_transform A.Compose([ A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ])逐个说参数。HorizontalFlip对航片几乎无脑开无人机拍的是俯视图水平翻转不改变语义。RandomRotate90对正射影像可以开但如果你手里的图是倾斜摄影或者带明显朝向的旋转后道路、桥梁的几何关系会变奇怪建议只保留90度的倍数旋转不要用任意角度的随机旋转。ShiftScaleRotate里的rotate_limit15是给倾斜影像留的余量正射影像可以调到0或保持小角度。RandomBrightnessContrast干的是白平衡差异补偿——不同航带、不同时间拍的图亮度差很大这个增强能让模型对光照鲁棒。GaussNoise用来模拟传感器噪声p值别调太高。绝对不要开的是ElasticTransform、GridDistortion这类非线性形变。语义分割的几何结构屋顶边界、道路边缘是硬约束扭曲之后标签本身就不准了270张的训练集撑不起这种自由度过高的增强。3.3 类别权重先看频率统计再决定要不要加权前面统计完类别占比如果最大类和最小类相差超过20倍交叉熵损失几乎全部被大类主导车辆、桥梁这些小类学不到东西。加权交叉熵是最直接的手段权重计算方式我建议用中位数频率平衡比简单的反频率更平滑。import numpy as np def compute_class_weights(mask_paths): counts np.zeros(8, dtypenp.float64) total 0 for p in mask_paths: mask cv2.imread(p, cv2.IMREAD_UNCHANGED).astype(np.int64) valid mask 8 # 忽略255 counts np.bincount(mask[valid], minlength8)[:8] total valid.sum() # 中位数频率平衡: 每个类权重 中位数频率 / 该类频率 freq counts / max(total, 1) median np.median(freq[freq 0]) weights np.zeros(8, dtypenp.float32) for i in range(8): if freq[i] 0: weights[i] median / freq[i] else: weights[i] 0.0 return weights weights compute_class_weights(mask_paths)看似数学上合理但有两个注意点。第一权重不能极端如果某些类别权重超过30训练初期loss会剧烈震荡建议对所有权重做裁剪比如限制在0.5到10之间。第二加权是补救手段不是根治手段。如果vehicle类IoU长期为0优先检查是不是标注本身就把车漏标了大半而不是先调权重。4. 用U-Net跑通基线最小训练脚本、损失函数与调参顺序数据管线就绪后先选一个简单可靠的模型把整个闭环跑通。我见过太多团队上来就同时上DeepLabV3、SegFormer、OCRNet三四个模型对比结果都在调bug根本没产出过一个可信的指标。基线只需要一个U-Net。4.1 为什么基线选U-Net而不是DeepLabV3或SegFormer270张小样本条件下U-Net的优势不是精度而是可控性。它结构简单编码器-解码器对称解码器的skip connection天然保留高分辨率边界信息收敛快显存占用比带ASPP模块的DeepLabV3小不少。DeepLabV3的ASPP用多尺度空洞卷积捕捉上下文理论上对城市大尺度地物更友好但空洞卷积在小数据集上容易把注意力学到图像角落的无意义特征上。Transformer系更依赖大数据和大batch270张图训不出一篇论文里的效果它更适合数据积累到2000张以后再上。医学图像分割的小样本实践提供了同样的经验U-Net系只要预训练权重选对、增强管够通常能够稳定收敛并给出有参考价值的baseline。这里说句掏心话如果是做目标检测而不是语义分割那就该切到yolov8训练自己的数据集那条链路U-Net解决的是像素级分类问题不要混用。4.2 最小训练脚本模型定义、损失、优化器与AMP模型直接用segmentation-models-pytorch库省得自己实现U-Netbackbone选resnet34的ImageNet预训练权重。270张图不足以让编码器从零学出通用特征预训练权重的帮助是显著的。pip install segmentation-models-pytorchimport torch import torch.nn as nn from torch.cuda.amp import autocast, GradScaler from segmentation_models_pytorch import Unet model Unet( encoder_nameresnet34, encoder_weightsimagenet, in_channels3, classes8, ).cuda() class_weights torch.tensor(weights, dtypetorch.float32).cuda() criterion nn.CrossEntropyLoss(weightclass_weights, ignore_index255) optimizer torch.optim.AdamW(model.parameters(), lr3e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max80) scaler GradScaler() for epoch in range(80): model.train() for img, mask in train_loader: img img.cuda().float() mask mask.cuda().long() optimizer.zero_grad() with autocast(): logits model(img) # (B, 8, H, W) loss criterion(logits, mask) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() scheduler.step()参数表按我跑这类数据集的经验整理超参数建议值说明crop_size512起步768精调显存够用就上768小目标检出更快batch_size8512尺寸8G显存爆显存就降到4配梯度累积optimizerAdamWweight_decay设1e-4正则化小样本过拟合base_lr3e-4AdamW常用范围1e-4到1e-3从3e-4开始schedulerCosineAnnealingLRT_max和训练总epoch数一致lossCrossEntropy(weight, ignore_index255)类别极不平衡时加权epochs60到100按验证集mIoU选checkpoint不按最后epochT_max这个参数要和你打算训练的总epoch数一致否则cosine退火会在训练中途提前把学习率降到接近0后面的epoch都在原地踏步。AMP混合精度建议直接开8G显存下能省接近一半的显存代价是训练loss偶尔出现波动这是正常的观察验证集mIoU的趋势就好。4.3 评估指标不要只盯准确率要看混淆矩阵和单类IoU分割任务的准确率几乎没有参考价值如果building占60%像素模型把所有像素都预测成building准确率也有60%但语义上完全不可用。mIoU才是语义分割的硬指标先算每个类别的IoU再取平均。def compute_miou(pred, target, num_classes8, ignore_index255): ious [] for c in range(num_classes): p (pred c) t (target c) inter (p t).sum() union (p | t).sum() if union 0: ious.append(inter.item() / union.item()) else: ious.append(float(nan)) return float(np.nanmean(ious))这个函数每轮验证都跑并且把每一类的IoU都打印出来。只看平均mIoU会掩盖问题可能road和building都到0.85vehicle却是0.1平均出来还不错但只要业务上需要识别车辆这个模型就不能上线。我的习惯是每个epoch结束时保存三样东西按mIoU排序的最佳checkpoint、最近一个epoch的checkpoint、以及各类IoU曲线图。5. 270张图的小样本避坑过拟合、空间泄漏与标注噪声排查小样本数据集的坑很多不是靠调参能解决的而是数据本身的陷阱。我列五个翻车场景都是实际跑这类城市航拍数据时反复出现的。5.1 训练loss降到很低验证mIoU原地不动现象训练集交叉熵loss降到0.12验证集mIoU在0.28到0.33之间震荡继续训练也不涨。原因270张图对一个resnet34编码器的U-Net来说偏少模型把训练集背下来了典型的过拟合。具体表现是训练loss一路下降验证loss不降反升。解决第一把增强强度拉起来重点加ShiftScaleRotate和亮度对比度这两个变换。第二把编码器从resnet34换成resnet18参数量直接减少接近一半小数据集上精度往往不降反升。第三确认weight_decay生效AdamW的weight_decay和Adam不一样它是对权重直接做解耦衰减1e-4是个合理的起点。最后用验证集mIoU做早停不要傻傻跑满80个epoch。5.2 验证集指标虚高换一块区域直接崩现象随机划分训练集和验证集mIoU能到0.72换一个新航带或者新城区去测mIoU掉到0.4。原因空间自相关泄漏。无人机影像里同一条道路、同一片连绵的屋顶会被切成多张图随机划分时训练集和验证集可能来自同一栋楼、同一条街模型其实在重复见相似像素。解决划分数据时按航带号或地理范围分组保证同一个空间的图全部进训练集或全部进验证集。用GroupKFold按航带分组是最快的落地方式。指标降下来是好事那才是模型真实的泛化水平。5.3 标签是RGB彩色图训练直接当3通道分类现象训练loss下降缓慢验证mIoU直接是0或者大类别能分出来小类别全部丢失。原因很多人没意识到彩色标注PNG和索引PNG的区别。彩色图每个像素是RGB三个值模型把它当成3通道输入输出却是8类这个映射本身就是错的。正确的标签只有单通道像素值直接等于类别索引。解决训练前先跑一遍2.2节的统计脚本确认标签是L模式或P模式的单通道图。如果是RGB彩色标注先把所有唯一颜色导出、建映射表、转成索引图再进Dataset。这个转换写一次就够但每次换新数据源都要自查一遍。5.4 显存不够模型根本跑不起来现象GPU是8G的Turing架构卡crop_size设768、batch设8直接OOM。原因高分辨率航片裁剪到768后中间特征图仍然很大编码器越深显存占用越集中在早期层。U-Net的skip connection还会额外缓存解码器各层输出显存压力比纯编码器模型更大。解决调整顺序很重要——先把batch_size降到4如果还OOM就开启AMP前面代码里已经用GradScaler包了再不行把crop降到512最后才考虑换resnet18编码器。梯度累积可以模拟更大的batch但BN统计会受影响小数据集上不建议主训用它。5.5 大类IoU不错vehicle这类小目标IoU长期为0现象road和building的IoU分别到0.83和0.79vehicle的IoU是0偶尔跳到0.05。原因车辆像素占比往往低于0.5%交叉熵对大类的梯度压制了小类的学习信号。另一个可能因素是标注问题——很多标注员对散落停放的车辆会习惯性忽略标签里车本来就不全模型无从学起。解决先回去看标注抽10张图数一下漏标车辆的数量。如果标注没问题再上类别权重并考虑把crop_size从512提到768让模型看到更大范围的上下文。如果还不够对含车辆的样本做过采样统计每个裁剪窗口里vehicle类像素数量少于阈值就重新裁让车辆类在训练样本中出现的频率提高。6. 让270张图得出可信结论K折交叉验证与伪标签迭代单次划分的训练验证测试在270张这种规模下运气成分很大。你手动切一次验证集mIoU可能差3到5个点。想要说服自己说服项目评审K折交叉验证是更可信的做法。6.1 用5折交叉验证替代单次划分270张按航带分组后做5折每折拿约54张做验证训练约216张跑5轮。最终报告的结果是5折mIoU的均值和标准差比如mIoU 0.61 /- 0.03。这组数字比单次划分的0.64更能反映真实水平标准差如果超过0.05说明数据本身不一致性大优先去补难例的标注而不是继续调模型。6.2 伪标签迭代扩充数据270张的一个尴尬之处是模型刚训到勉强可用数据就见底了。一个安全的扩充办法是伪标签用5折里最好的模型对未标注的航片做推理把softmax置信度高于0.9的像素当作标签人工抽检少量图后并入训练集。一轮扩充通常能带来1到2个点的mIoU提升再继续迭代收益就递减了。6.3 大图推理是个工作量不小的收尾最终应用场景是整张城郊大图的分割训练时的裁剪要在推理时补回来。滑窗重叠推理加概率平均比非重叠硬拼接好得多def predict_large_image(model, image, crop512, stride256): h, w image.shape[:2] probs np.zeros((8, h, w), dtypenp.float32) count np.zeros((h, w), dtypenp.float32) for y in range(0, h - crop 1, stride): for x in range(0, w - crop 1, stride): patch image[y:y crop, x:x crop] with torch.no_grad(): logits model(torch.from_numpy(patch).unsqueeze(0).cuda()) prob torch.softmax(logits, dim1).squeeze(0).cpu().numpy() probs[:, y:y crop, x:x crop] prob count[y:y crop, x:x crop] 1 # 重叠区域取平均概率, 边缘有stride覆盖不到的地方要额外处理 probs / np.maximum(count, 1) return probs.argmax(0)stride选crop的一半重叠区域做概率平均。这个脚本写出来容易跑起来慢——4000x3000的图在单卡上要跑几十秒这是正常的。检查输出时先看边缘有没有条带效应如果有说明边缘patch的均值计算被count的0值污染了把分母里的np.maximum阈值提高到1以上通常能解决。我的习惯是每次跑完都把checkpoint、预测图、mIoU报告按日期存一份复盘的时候只读当时写的一行备注就能想起来卡在过拟合还是标注漏标。270张图做语义分割结论的可靠性不是靠堆模型而是靠把数据检查、分组验证、伪标签迭代这四件事做扎实。希望帮到你。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门