简介面向无人机城市图像语义分割任务的高分辨率数据集包含建筑、公路、树木等8类常见地物像素级标注数据来源于无人机航拍场景分辨率较高贴近实际应用中的地物分布与尺度变化适合计算机视觉初学者及遥感、智慧城市方向研究者用于模型训练和效果验证。数据按训练集与验证集划分训练集约200张、验证集约70张每张图像均配有对应掩膜压缩包共543个文件以PNG格式的原始图像和标签图为主另含类别说明的TXT文件和一个Python可视化脚本便于一次性查看原图、GT及叠加效果整体大小263.73MB目录结构清晰易用。可视化脚本支持随机抽取样本并生成对比图有助于快速检查标注质量、理解模型预测差异。目前已有386人学习下载适合作为语义分割入门实践和算法对比实验的数据支撑。1. 高分辨率无人机城市图像语义分割数据集270张图怎么撑起8类细分花小半天把一套高分辨率无人机城市图像语义分割数据集拆开看了一遍第一反应是“就270张”——8类分割、每类还得有像样的边缘这个规模听起来像是个玩具。但真正上手跑过几次遥感分割之后你会发现卡住你的从来不是数据量而是类别定义没对齐、标签格式读错、验证集划分漏了空间相关性这类问题。这套数据集的定位很明确高分辨率正射视角下的城市地物8个语义类别训练标签齐全够你把一整套“图像分割”的pipeline从数据加载、切片增强跑到训练评估也够你在上面验证U-Net、DeepLabV3这类模型对城市地物的适应度。它的受众不是要刷超大比赛分数的人而是想在地物提取、数字孪生、应急评估乃至地物面积估算方向快速落地验证的工程师和学生。270张图不大但恰好能让你把每一步操作的原因和坑都看明白。2. 数据从哪来、长什么样8类标签约定与验证集划分2.1 8类标注到底分了什么先别急着训练把类别ID对齐标题说了“8类分割”但到手的数据包里类别未必按你熟悉的顺序排列。常见做法是背景、道路、建筑、植被、车辆、水体、行人、空地这八类也有的把行人和车辆合并换成“桥梁”或“其它硬化地面”。第一件事不是读图而是把标签图的唯一像素值全部打出来和随包提供的类别说明逐项对一遍。我习惯写一段非常短的探针代码import numpy as np import cv2 label_path data/labels/0001_label.png label cv2.imread(label_path, cv2.IMREAD_UNCHANGED) cls_ids, counts np.unique(label, return_countsTrue) print(unique ids:, cls_ids) print(pixel counts:, counts) print(num classes:, len(cls_ids))这里的逻辑说明很关键cv2.IMREAD_UNCHANGED是以原始位深读取保留单通道的索引值。如果你用cv2.imread(path, cv2.IMREAD_COLOR)或 PIL 的默认 RGB 模式调色板索引图会被展开成三通道类别 ID 直接错乱。参数方面np.unique返回的是像素值列表正常情况应该是[0, 1, 2, ..., 7]共 8 个值如果看到连续的大数或者 0 到 255 乱跳说明标签不是索引图而是 RGB 编码的掩码后续映射逻辑要改。像素计数那一行是为了看类别分布counts里如果某个类别占比不到 1%第 4 章的类别权重就有必要了。2.2 高分辨率RGB与标签文件的对应关系读图之前先做的三项检查无人机数据最常见的坑是影像和标签分辨率不一致。有的标签是基于旧版正射影像标注的后来影像重出过一版文件和标注就没对齐。我一般先做三项检查全部通过才继续一是文件名一一对应二是影像与标签尺寸完全一致三是坐标范围肉眼吻合。import cv2 import os img_dir data/images label_dir data/labels img_names sorted(os.listdir(img_dir)) label_names sorted(os.listdir(label_dir)) assert [n.replace(.jpg, .png) for n in img_names] label_names, 文件名不匹配 for img_name, label_name in zip(img_names, label_names): img cv2.imread(os.path.join(img_dir, img_name)) label cv2.imread(os.path.join(label_dir, label_name), cv2.IMREAD_UNCHANGED) assert img.shape[:2] label.shape[:2], f{img_name} 与标签尺寸不一致 assert len(label.shape) 2, f{label_name} 不是单通道索引图这段代码的逻辑是逐文件核对任何一张对不上就报错停住。img.shape[:2]取高和宽不取通道数因为 RGB 图三通道、标签图单通道通道数本来就不该相同。len(label.shape) 2这个断言很重要一旦标签被读成三通道说明读图方式或者标签存储格式有问题后面所有训练都是错的。这类检查看着笨但能拦住 80% 的“模型不收敛”式翻车。2.3 只有270张怎么划分训练验证测试才不算浪费270 张总量不大划分策略直接影响你对模型能力的判断。很多人随手random_split结果无人机影像里同一栋楼、同一条路的照片同时出现在训练集和验证集验证分数虚高到没有参考意义。正确做法是按航带或采集区域分组后划分。比如数据是按飞行架次组织的那就按架次分前 4 个架次做训练第 5 个架次做验证第 6 个架次做测试。如果没有任何分组信息就把文件名按地理位置编号排序后再隔点采样尽量避免相邻帧落在不同集合。我见过太多次随机划分导致的“高分低能”模型验证集 mIoU 看着 0.85换一组新影像直接掉到 0.6问题就出在这里。合理的比例建议是训练 170 张、验证 50 张、测试 50 张。验证集用来调超参数和Early Stopping测试集只许碰一次最后才拿出来报数。很多人为了省事把验证和测试合并成一个集合这对 270 张的小数据来说能理解但代价是你无法判断早停时机是否过拟合了测试分布。考虑到高分辨率图像后续要切片90 张验证集下每个 512×512 的 patch 可能膨胀到几千张数量完全够用。3. 用U-Net把数据集跑通最小可复现流程与参数含义3.1 数据切片与样本准备把高分辨率图拆成能塞进显存的patch高分辨率无人机影像动辄 3000×4000 像素直接整图送进分割网络显存瞬间爆炸即便 Resize 进去车辆、行道树这类小目标也已经被压没了。所以分割第一步永远是切片。我用 512×512 的窗口、256 步长做滑动裁剪重叠区域能缓解边缘目标被切断的问题也等于给数据做了隐式增广。import cv2 import os K 512 STRIDE 256 os.makedirs(patches/images, exist_okTrue) os.makedirs(patches/labels, exist_okTrue) for img_name in sorted(os.listdir(data/images)): img cv2.imread(os.path.join(data/images, img_name)) label cv2.imread(os.path.join(data/labels, img_name.replace(.jpg, .png)), cv2.IMREAD_UNCHANGED) h, w img.shape[:2] for i in range(0, h - K 1, STRIDE): for j in range(0, w - K 1, STRIDE): patch_img img[i:iK, j:jK] patch_label label[i:iK, j:jK] cv2.imwrite(fpatches/images/{img_name[:-4]}_{i}_{j}.png, patch_img) cv2.imwrite(fpatches/labels/{img_name[:-4]}_{i}_{j}.png, patch_label)逻辑说明循环范围range(0, h - K 1, STRIDE)保证窗口不越界但意味着底部和右侧会有一条约 256 像素宽的边缘不参与切片。如果不想丢边缘可以先做镜像填充再在切片时同步裁剪回原坐标。这里一个容易被忽略的参数是STRIDE步长越小重叠越密样本越多但相邻 patch 高度相似会让验证集出现严重的数据泄露我用 256 即窗口一半折中效果比较稳。cv2.imwrite默认按文件后缀判断编码PNG 无损适合保存标签。注意我保存标签时仍然保留了单通道索引值这是训练前最重要的一步。3.2 类别映射与批次加载用PyTorch读入并实时做翻转增强切片之后标签依然是原始像素 ID绝大多数分割模型的输出是B, C, H, W的概率图和标签做交叉熵时要求标签是0到C-1的整数张量。如果原始标签恰好是 0-7省事如果类别 ID 是 3、5、9 这种稀疏值就得做一次映射。我一般把映射表写进 Dataset 的构造函数这样训练代码不用跟着改。import torch from torch.utils.data import Dataset import cv2 import glob class DroneSegDataset(Dataset): def __init__(self, img_dir, label_dir, id_mapNone, augFalse): self.img_paths sorted(glob.glob(img_dir /*.png)) self.label_paths sorted(glob.glob(label_dir /*.png)) self.id_map id_map # 例如 {3: 0, 5: 1, ...} self.aug aug def __len__(self): return len(self.img_paths) def __getitem__(self, idx): img cv2.imread(self.img_paths[idx]) label cv2.imread(self.label_paths[idx], cv2.IMREAD_UNCHANGED) if self.id_map is not None: mapped label.copy() for src, dst in self.id_map.items(): mapped[label src] dst label mapped if self.aug: import random if random.random() 0.5: img cv2.flip(img, 1) label cv2.flip(label, 1) img torch.from_numpy(img.transpose(2, 0, 1)).float() / 255.0 label torch.from_numpy(label).long() return img, label这段代码里id_map是以字典形式传入的类别映射循环里mapped[label src] dst逐个把原始 ID 替换成连续 ID。cv2.flip(img, 1)是水平翻转1表示沿 y 轴翻转这种几何变换不会破坏像素级标注的对应关系是语义分割最安全的增强方式。最后transpose(2, 0, 1)把 HWC 转成 CHW 以满足 PyTorch 的输入约定float()/255.0做归一化。需要注意归一化用的是全局固定除 255而不是 ImageNet 的 mean/std因为无人机影像的像素分布和自然图像差异大用预训练权重时再考虑均值方差归一化即可从零训练时前者更稳。3.3 训练与监控学习率、loss和mIoU怎么配合着看在 U-Net 这种 encoder-decoder 结构下270 张原始图切片后的样本量足够从零训练不必非得加载 ImageNet 预训练权重。预训练权重当然能加速收敛但遥感影像的域差异会让底层特征不太适用有时反而拖后腿。我用交叉熵 Dice loss 的组合交叉熵负责稳定梯度Dice loss 负责减轻类别不平衡的干扰系数取 0.5 左右即可。import torch import torch.nn as nn def soft_dice_loss(preds, labels, eps1e-6): probs torch.softmax(preds, dim1) onehot torch.zeros_like(probs).scatter_(1, labels.unsqueeze(1), 1) intersection (probs * onehot).sum(dim(2, 3)) union probs.sum(dim(2, 3)) onehot.sum(dim(2, 3)) dice (2.0 * intersection eps) / (union eps) return 1.0 - dice.mean() for epoch in range(60): model.train() for imgs, labels in loader: imgs, labels imgs.cuda(), labels.cuda() preds model(imgs) ce nn.functional.cross_entropy(preds, labels, ignore_index-1) dice soft_dice_loss(preds, labels) loss ce 0.5 * dice optimizer.zero_grad() loss.backward() optimizer.step()参数说明ignore_index-1是给标签里的未知像素留的出口切片时如果边界处有填充造成的伪标签可以先用 -1 标记计算 loss 时自动跳过。Dice loss 用softmax后的概率图计算而不是对 logits 直接算为的是让梯度经过概率层后更平滑。学习率初始值我习惯取3e-4优化器用 AdamW权重衰减设1e-4防过拟合。训练中每 2 个 epoch 算一次验证集 mIoU不只看 lossloss 下降但 mIoU 不动大概率是类别分布偏了得回查第 4 章的重采样策略。4. 类别不平衡与高分辨率细节预处理和训练参数的5个调整点4.1 车辆、行人这类小目标为什么总是漏靠类别权重兜底城市无人机视角下建筑和道路可能占图像面积的 70% 以上车辆、行人往往只占百分之零点几。模型天然倾向于把像素判给高频类别因为这样能让整体 loss 降得最快。这就是为什么车辆在预测图里经常“消失”——不是模型没看到是它在梯度层面上不值得优化。解决思路有两个一是按类别频率反过来设置权重让少数类样本的梯度放大二是在采样时让每个 patch 都包含车辆或行人。我常用中值频率均衡的简化版本计算权重import numpy as np import torch # freq 为每个类别在整个训练集中的像素占比长度 8 freq np.array([0.52, 0.18, 0.15, 0.08, 0.03, 0.02, 0.01, 0.01]) weights 1.0 / np.log(1.02 freq) weights torch.tensor(weights, dtypetorch.float32, devicecuda)逻辑说明1.02是个平滑项防止占比极小的类别权重爆炸到不合理的数值。log起压缩作用让高频类和低频类的权重差距从几十倍缩小到三四倍避免梯度被少数类劫持。把weights传给交叉熵的weight参数后低频类别的梯度贡献会被放大但不会彻底主导训练。配合训练时的单类 IoU 观察可以判断权重是否过猛——比如建筑这类高频类的 IoU 突然掉了 10 个点说明权重压过头了。4.2 从512到1024这步跨不跨显存与细节的取舍切片窗口越大模型能看到的地物上下文越完整楼栋轮廓和道路走向更容易判对但显存消耗按面积平方上涨而且小数据集下大 patch 会让有效样本数变少。我一般先用 512×512 跑通整个流程确认代码没问题之后再上 1024×1024。同样的 U-Net 在 512 下批量大小可以开 8到 1024 时通常只能开 2这时需要开启混合精度来保住批大小。scaler torch.cuda.amp.GradScaler() with torch.autocast(device_typecuda, dtypetorch.float16): preds model(imgs) loss ce_loss(preds, labels) 0.5 * dice_loss(preds, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()这段代码是混合精度的标准框架。autocast让卷积和矩阵运算自动走半精度显著降低显存占用且对分割这类对精度不极端敏感的任务效果几乎无损。GradScaler负责梯度缩放避免半精度下的梯度下溢。要注意的是BatchNorm 在混合精度下应保持 float32 运行PyTorch 会自动处理但你如果自己实现归一化层就得留心。转 1024 之后学习率可以微调低一点因为每个 patch 包含的类别更多梯度噪声相对小学习率稍低能让边界更稳。4.3 后处理与边界优化CRF、连通域过滤在什么时候值得上U-Net 输出的概率图直接做argmax得到的结果通常有几种毛病边缘毛糙、细小孔洞、个别孤立像素点成块。后处理不是必须的但在高分辨率无人机影像上目标边缘和真实地物轮廓高度相关简单的后处理能带来 1 到 3 个点的边界 IoU 提升。最实用的两个操作是连通域过滤和形态学开闭运算CRF 这种全局优化反而要谨慎。连通域过滤的思路是预测图里面积小于某个阈值的连通块大概率是噪声直接替换为周围多数类。形态学闭运算可以填补建筑内部的细小空洞但核不能大3×3 就够大核会把相邻的小目标直接吞并得不偿失。CRF 对标注质量高的数据集有正向效果但 270 张数据的标注边缘本身可能存在噪声时CRF 会把标注噪声的结构学进去之后换到新数据上表现反而更差。我现在的习惯是先跑一版无后处理的结果作为下限再跑一版 3×3 中值滤波的结果对比之后再决定要不要上更重的后处理。后处理永远不能成为模型性能的救命稻草只能是锦上添花。5. 实战避坑用这套数据集训练最容易踩翻车的5个点5.1 现象训练时 loss 降得很快验证 mIoU 也到了 0.8 以上可视化发现预测图的颜色区域和原图对不上比如马路被涂成了建筑色原因标签格式没吃透。这类数据集里标签文件如果是调色板索引图用cv2.imread不带IMREAD_UNCHANGED读进来会变成三通道 RGB每个像素变成三维向量训练代码把它当三通道输入np.unique返回的是颜色元组而不是类别 ID映射表自然对不上。解决统一用IMREAD_UNCHANGED读标签并在数据加载后用np.unique断言类别数量等于 8。这条是我见过最高频的翻车点没有之一。5.2 现象整图 Resize 到 512×512 后直接训练车辆和行人的分割结果几乎全糊建筑轮廓也歪歪扭扭原因高分辨率无人机影像把原图压到 1/6 甚至 1/8 后小目标只剩下几个像素语义信息已经丢失不是模型能力问题。解决必须切片训练推理时也要切片后拼接。我之前图省事直接 Resize省下了切片代码的时间却在调模型参数上浪费了三天最后回头补切片。无人机影像分割没有捷径切片是第一步。5.3 现象loss 稳定下降但预测图的边缘总有一圈细碎的锯齿甚至标出了标注里根本没有的“伪类别”原因标签边缘本身存在标注误差比如楼栋边界没有完全贴合影像像素或者路沿石那条线在标注时被涂成了背景。模型努力拟合标注反而学到了标注的噪声。解决在数据加载时对标签做 3×3 的形态学闭运算或者在 loss 里额外加一个边界感知项让网络更关注边缘区域而不是死磕噪点。更直接的做法是把明显错误的标签挑出来重新修270 张图里这种错误通常不超过 10 处人工修这几张比折腾模型划算得多。5.4 现象验证集 mIoU 很高把模型放到另外一片区域的无人机影像上mIoU 骤降 15 个点以上原因验证集划分没有考虑空间相关性。同一栋楼、同一条路在相邻航带的重叠区几乎一模一样随机划分导致重复地物同时进了训练集和验证集模型相当于“开卷考试”。解决按采集区域或航带划分数据保证验证集和训练集没有地理位置重叠。我一般会把原始 270 张按文件名里隐含的航带编号排序再按 6:2:2 切分宁可让验证集分布和训练集略有差异也不换虚高的分数。5.5 现象加了类别权重后车辆 IoU 确实涨了但建筑、道路这种原本 90 的类别掉到了 70 以下整体 mIoU 反而下降原因权重设过头了。车辆像素占比 1%权重放到 20 倍以上时模型每一轮都被少数类样本拉着走多数类的拟合被压制。解决权重公式里保留log压缩和平滑项权重上限建议不要超过 5。同时要盯着单类 IoU 曲线而不是只看 mIoU 总和。mIoU 是平均结果掩盖了类别间的此消彼长单类 IoU 才能暴露真实短板。6. 验证与进阶用边界质量指标和单类IoU给这套数据打分训练结束后最忌讳的事情是只报一个 mIoU 数字就算完事。对这套 8 类的高分辨率无人机城市数据集我习惯额外看两个东西单类 IoU 明细和边界方向的指标。单类 IoU 一眼能看出模型对“车辆”“行人”这种小目标类别的处理水平。做法是逐类计算混淆矩阵并输出类似class_06_vehicle_iou 0.72这样的清单。如果车辆 IoU 明显偏低下一轮迭代的方向就很明确增加该类别的切片采样密度或者把切片窗口缩小让每个 patch 里车辆的占比增高。边界质量是无人机分割里容易被忽视的维度。普通 IoU 对边缘偏移不敏感即使整个建筑边界整体内缩 3 个像素IoU 可能只掉 1 个点。但对后续要做面积估算、矢量化的应用来说边界偏移直接决定输出精度。可以在验证阶段加一个边界加权 IoU先对预测和标签分别提取边缘只在边缘一定邻域内计算交并比这个指标专门反映轮廓贴合度。如果你的训练流程里没算这个指标建议抽时间补上它对标注质量、后处理效果的敏感度非常高。还有一个我认为值得延伸的方向地物面积估算。这套数据集的 8 类语义分割结果天然可以换算成面积比例——已知无人机飞行高度和相机参数时每像素对应一个地面尺寸模型输出的各类像素数乘以单像素面积就能估算出建筑占地、绿地覆盖、道路面积等指标。把分割结果和地理坐标对齐后配合 GDP 空间分布网格这类社会经济数据做关联分析这已经接近遥感语义分割的实际落地场景。它不需要换模型只需要在现有预测图上做像素计数和坐标换算单类 IoU 的高低决定了面积估算的误差上限。如果你的诉求不只是跑通分割还想把结果量化成报告里的数字那建议在测试集上逐张统计各类别的像素占比输出一个类别面积表。这个表的意义比单张图的可视化大得多——它直接验证模型在区域尺度上的稳定性和可用性。我自己的习惯是每次训练收尾时把单类 IoU、边界加权 IoU 和类别面积比例三个结果记在同一张表里下次换模型或调参数时直接对照。这套 270 张的数据集虽然量小但流程一旦走顺后续接入更大的城市级数据只是换数据路径的事。希望这些踩过的坑和验证习惯能帮到你。本文还有配套的精品资源点击获取
