无人机城市分类数据集实战:从文件名解析到ResNet50微调
2026/9/15 0:43:17 网站建设 项目流程

简介:无人机城市分类数据集是一份面向计算机视觉与深度学习研究者的航拍影像分类资源,核心内容是无人机拍摄的城市景观图像,适用于图像分类、特征提取以及卷积神经网络(CNN)模型的训练与验证。压缩包内共1793个文件,包括1791张jpg图像、1个txt说明文件与1个json标注文件,整体仅26.56MB,便于快速下载和本地试验;jpg图像提供不同城市区域及天空场景的样本,txt和json可实现类别说明与标签解析。目前已有88人浏览学习,适合需要开展城市环境识别、地物分类等研究的学员和算法工程师。通过该数据集可系统练习数据预处理、训练集与验证集划分、CNN分类器构建,以及准确率、精确率、召回率等评估流程;json标注还能支撑天空分类子任务解析,为迁移学习与智慧城市应用提供数据基础。

1. 无人机城市分类数据集的真实内容:从文件名到任务边界

无人机城市分类数据集的 zip 包解压后,通常不是整齐的train/val目录,而是一批类似7c490e78-Data0_000240_2000_3600.jpg的散装文件。哈希前缀看似随机,真正有价值的是Data0后的三段数字:采集帧序号、标注宽度、标注高度。这类命名在无人机遥感、智慧城市项目里很常见,先解析命名再训练,能避开数据清洗阶段的大多数坑。

这个数据集的用途是城市景观图像分类,输入为无人机俯拍影像,输出可对应商业区、住宅区、道路、绿地等类别;zip 内的sky_classification_export又提示天空区域被单独做了子任务,训练时可以把天空先验当辅助特征。与普通场景分类比,难点是俯拍视角的尺度变化、类别不均衡,以及阴影和反光带来的纹理干扰。

对工程师来说,它的价值不只是直接拿去训练,而是当一份完整的数据协议样本:文件名解析、ignore 清单处理、尺寸校验,这些步骤在真实项目里反复出现。下面按数据解析、CNN 微调、评估指标、天空先验优化展开,代码基于 PyTorch 和 sklearn,可直接复现。

2. 还原数据地图:文件名协议、ignore.txt 与逐图校验

拿到数据集的第一步不是写模型,而是回答三个问题:每一张图对应哪个语义类别、哪些样本被明确要求跳过、图像的实际像素尺寸是否和文件名一致。这三个问题都能通过一段脚本在几分钟内解决,但大多数人会跳过,结果训练到一半才发现数据里有坏图或标签错位。

2.1 文件名里的三类信息:哈希、帧号与目标尺寸

先看文件名结构:7c490e78-Data0_000240_2000_3600.jpg。用分隔符拆开后,各部分含义如下表所示,这决定了后面解析脚本的正则怎么写。

文件片段示例含义
7c490e788 位 hex图像内容的哈希前缀,用于快速去重定位
Data0采集批次表示数据来源批次,可能与飞行架次对应
0002406 位帧号第 240 帧,通常由采集时间排序得到
2000目标宽期望的像素宽度,标注或切图时设定
3600目标高期望的像素高度,与切图滑窗相关

这里的2000_3600值得注意。它表示这批数据在导出时被规范化到该分辨率,而不是相机原图分辨率。如果实际图像尺寸和它不一致,说明数据管道里有重采样或裁切操作,特征分布可能受影响,尤其是纹理类特征。签入脚本前我先确认这批图片全部是 JPEG,避免把 PNG 混进训练管线后再做一次格式迁移。

2.2 用解析脚本把文件名变成结构化清单

我一般会在解压目录下先跑一遍正则匹配,把文件名拆成结构化字段,同时做一次无损读取。下面这段代码兼容 Windows 和 Linux 路径,能直接输出样本数量和不匹配文件。

import re from pathlib import Path from PIL import Image DATASET_DIR = Path("无人机城市分类数据集") pattern = re.compile(r"^([0-9a-f]{8})-Data0_(\d{6})_(\d{4})_(\d{4})\.jpg$") rows = [] for p in DATASET_DIR.glob("*.jpg"): m = pattern.match(p.name) if not m: print("[namenotmatch]", p.name) continue prefix, frame, w, h = m.groups() rows.append({ "file": p.name, "prefix": prefix, "frame": int(frame), "w": int(w), "h": int(h), }) print("matched:", len(rows))

正则^([0-9a-f]{8})-Data0_(\d{6})_(\d{4})_(\d{4})\.jpg$把文件名分成四组,glob("*.jpg")只筛选一级目录下的图片。如果数据集被拆成多个子目录,这里要改成rglob("**/*.jpg"),否则会漏样本。wh在下一步会被拿来与实际图像尺寸对照。

2.3 ignore.txt 的正确用法:黑名单而不是标签文件

很多用户会误把ignore.txt当作类别标注表,实际上它通常只是数据准备阶段筛出来的黑名单。文件中每一行记录一个应被忽略的文件名,可能带#注释,也可能直接写相对路径。解析时要先去掉注释和空行,再统一成Path.name格式做匹配,否则 Windows 下反斜杠路径会对不上。

ignore_set = set() ignore_file = DATASET_DIR / "ignore.txt" if ignore_file.exists(): for line in ignore_file.read_text(encoding="utf-8").splitlines(): line = line.strip() if line and not line.startswith("#"): ignore_set.add(line.replace("\\", "/").split("/")[-1]) print("ignore items:", len(ignore_set))

这段代码先将路径分隔符统一为/,再取最后一段文件名。这么做的理由很直接:ignore.txt里可能写的是images/abc.jpgabc.jpg两种格式,直接比较行内容会漏掉一部分。再用name not in ignore_set过滤后,得到的rows才是真正的可用样本列表。

2.4 逐图校验尺寸、格式和损坏情况

清单建立后,下一步是打开每一张图验证。计算机视觉数据集里最常见的错误不是标签错,而是某张图片只有文件名是.jpg,实际编码是 PNG 或已经损坏。校验逻辑如下。

valid_rows = [] for row in rows: if row["file"] in ignore_set: continue img_path = DATASET_DIR / row["file"] try: with Image.open(img_path) as im: actual_w, actual_h = im.size fmt = im.format if (actual_w, actual_h) == (row["w"], row["h"]) and fmt == "JPEG": valid_rows.append(row) else: print("[sizemismatch]", row["file"], (actual_w, actual_h), fmt) except Exception as exc: print("[corrupt]", row["file"], exc) print("usable:", len(valid_rows))

用 PIL 的Image.open做轻量校验,不会把整图加载进内存,速度足够快。im.format能识别真实编码,避免伪装成 JPEG 的 PNG。如果发现大量尺寸不匹配,我通常会重新审视数据集说明里的切图方式;少量不匹配则直接淘汰,因为这些样本即使能训练,也会影响 Batch 维度上的归一化统计。

提示:ImageFile.LOAD_TRUNCATED_IMAGES = True会让损坏图片被强行读取,校验阶段不要开。训练阶段如果内存报错再单独处理。

3. 训练 ResNet50 城市分类 Baseline:预处理、交叉验证与评估指标

数据地图整理完,才进入模型搭建。对城市俯拍图像分类,CNN 仍然是最稳的起点。ResNet50 在 ImageNet 上有很强的纹理和边缘先验,直接微调比从零训练收敛快、数据需求低。下面给出一套可直接运行的 Baseline 方案,涵盖自定义 Dataset 类、预处理策略、冻结微调和分层 K 折验证。

3.1 数据组织:从文件清单到带标签的数据集对象

城市分类任务要求每个样本有 label,这一步依赖打包时提供的类别映射表。常见形式是 CSV 或子目录结构,image_name,category两列。我先把类别转成整数编码,再写一个轻量Dataset子类,避免每次迭代都读 CSV。

import pandas as pd import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader from torchvision import transforms, models from pathlib import Path from PIL import Image labels_df = pd.read_csv("labels.csv") labels_df["file"] = labels_df["file"].apply(lambda x: x.replace("\\", "/").split("/")[-1]) categories = sorted(labels_df["category"].unique()) cat2idx = {c: i for i, c in enumerate(categories)} labels_df["label"] = labels_df["category"].map(cat2idx) class CityDataset(Dataset): def __init__(self, df, root, transform=None): self.df = df self.root = Path(root) self.transform = transform def __len__(self): return len(self.df) def __getitem__(self, idx): row = self.df.iloc[idx] img = Image.open(self.root / row["file"]).convert("RGB") if self.transform: img = self.transform(img) return img, row["label"]

convert("RGB")是把灰度图或带透明通道的图统一成三通道,避免 ResNet50 输入维度报错。self.root / row["file"]使用pathlib拼接,杜绝字符串路径跨平台问题。类别编码用排序后的categories,保证类别顺序在训练和推理阶段完全一致。

3.2 预处理与微调参数:冻结浅层、类别均衡与增强策略

无人机俯拍图的光照和视角变化比普通街景更剧烈,所以增强策略不能只做水平翻转。常用做法是随机旋转、颜色抖动加缩放裁剪,其中颜色抖动对阴影和反光区域最有效。

train_transform = transforms.Compose([ transforms.Resize((512, 512), antialias=True), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) val_transform = transforms.Compose([ transforms.Resize((512, 512), antialias=True), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ])

Resize((512, 512))是训练分辨率和最终精度之间的折中。切图尺寸是2000x3600时,直接整图缩放会丢失细节,常见做法是先随机裁剪出 512x512 区域再做缩放;如果数据量不足,再用整图缩放。Normalize使用 ImageNet 的均值和标准差,微调时不要改成自己的统计值,否则预训练权重分布会被打破。

模型结构和优化器设置如下。

num_classes = len(categories) model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2) for p in model.parameters(): p.requires_grad = False for p in model.layer4.parameters(): p.requires_grad = True model.fc = nn.Linear(2048, num_classes) optimizer = torch.optim.AdamW([ {"params": model.layer4.parameters(), "lr": 1e-4, "weight_decay": 1e-4}, {"params": model.fc.parameters(), "lr": 1e-3, "weight_decay": 1e-4}, ]) criterion = nn.CrossEntropyLoss()

冻结前 3 个 stage 而只微调layer4fc,是迁移学习里降低过拟合的标准做法。layer4的特征图已经有高层语义,而底层权重保存着边缘、颜色等通用特征,没必要重新更新。如果训练集超过几万张,可以再多解冻layer3,并把对应学习率降到5e-5AdamWweight_decay搭配是当前分类任务里偏稳的组合。

提示:如果显存吃紧,把Resize改成(384, 384),Batch Size 调成 16。精度会下降约 1 到 2 个百分点,但训练时间能缩短一半。

3.3 分层 K 折验证与混淆矩阵定位差错

城市分类的类别分布通常不均衡,住宅区和道路可能占六成以上。简单的留出验证会让少数类在验证集里只出现几次,指标波动非常大。分层 K 折让每一折都保持与全量数据相同的类别比例。下面用 5 折交叉验证给出稳定评估。

from sklearn.model_selection import StratifiedKFold from sklearn.metrics import classification_report, confusion_matrix skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) for fold, (train_idx, val_idx) in enumerate(skf.split(labels_df, labels_df["label"])): train_df = labels_df.iloc[train_idx].reset_index(drop=True) val_df = labels_df.iloc[val_idx].reset_index(drop=True) train_ds = CityDataset(train_df, DATASET_DIR, train_transform) val_ds = CityDataset(val_df, DATASET_DIR, val_transform) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4) model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2) model.fc = nn.Linear(2048, num_classes) optimizer = torch.optim.AdamW(model.fc.parameters(), lr=1e-3, weight_decay=1e-4) for epoch in range(15): model.train() total_loss = 0.0 for images, targets in train_loader: optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, targets) loss.backward() optimizer.step() total_loss += loss.item() model.eval() preds, truths = [], [] with torch.no_grad(): for images, targets in val_loader: outputs = model(images) preds.extend(outputs.argmax(dim=1).tolist()) truths.extend(targets.tolist()) print(f"fold {fold} loss {total_loss:.4f}")

上面的循环对每一折都重新初始化模型,保证折间独立。argmax(dim=1)得到预测类别索引,tolist()是为了后续直接喂给 sklearn 的指标函数。运行结束会得到 5 组验证预测,把每组拼起来就能得到完整报告。

print(classification_report(truths, preds, target_names=categories, digits=3)) print(confusion_matrix(truths, preds))

classification_report同时输出 precision、recall、F1 和各类别样本数。只看 accuracy 很容易被住宅区这类大类掩盖问题,而confusion_matrix能直接把商业区与道路互相混淆的位置标出来,后续再做针对性优化时就有了方向。常用超参数整理如下。

配置项取值说明
输入分辨率512x512切图后缩放,保留城市纹理细节
Batch Size32单卡 11GB 显存可运行
优化器AdamW权重衰减放在参数组里更稳
fc 学习率1e-3新分类头收敛快
layer4 学习率1e-4微调高层语义特征
Epochs15交叉验证每折独立训练
增强策略翻转/旋转/ColorJitter应对光照和视角变化

4. 用天空先验和置信度回退压榨分类精度

城市分类基线跑通后,精度往往卡在某个类别上,最常见的是把浅色楼顶识别成天空,或把天空误判成道路。zip 内的sky_classification_export提示天空分类被单独拆成子任务,这说明天空区域对俯拍图像语义有很强的区分作用,可以把它作为先验注入分类器。

最直接的做法是多任务学习:在共享主干上加一个天空分类头。该分支输出1表示图像存在大面积天空,0表示无天空或占比极低。定义 PyTorch 模块时不复用整个 ResNet50,而是用轻量主干来控制计算量。

class CityWithSkyHead(nn.Module): def __init__(self, num_classes): super().__init__() base = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) self.features = nn.Sequential(*list(base.children())[:-2]) self.pool = nn.AdaptiveAvgPool2d(1) self.classifier = nn.Linear(512, num_classes) self.sky_head = nn.Linear(512, 1) def forward(self, x): feat = self.pool(self.features(x)).flatten(1) logits = self.classifier(feat) sky_logit = self.sky_head(feat) return logits, sky_logit.squeeze(1)

训练时损失函数改为CE + λ * BCEWithLogitsλ取 0.2 到 0.5 之间。这样让分类头学会把“天空面积大”和“空旷城市景观”关联起来,而不是单纯记住颜色统计。若数据集没有提供天空标签,只能退而求其次,在一部分图上用蓝色通道比例做弱监督标注,噪声较大,效果会打折扣。

另一个更通用的推理技巧是置信度阈值回退。城市类别本身有层级关系,比如住宅区、商业区可以归为建成区,当 softmax 最大概率低于 0.6 时,模型极有可能把细分类别混淆。此时把预测结果回退到最高层级的粗类别,能在不重训模型的情况下减少离谱错误。实现时只需在推理出口包一层逻辑。

def predict_with_fallback(model, img_tensor, idx2cat, threshold=0.6): model.eval() with torch.no_grad(): logits = model(img_tensor.unsqueeze(0)) prob = torch.softmax(logits, dim=1) top2 = torch.topk(prob, k=2, dim=1) if top2.values[0, 0].item() < threshold: return "coarse_category" return idx2cat[top2.indices[0, 0].item()]

topk同时拿到最大概率和对应索引,当概率过低时不再信任细粒度输出。阈值 0.6 是一个安全起点,如果验证集上回退数量超过 10%,说明模型整体置信度偏低,应当回看预处理或增加训练轮次,而不是继续调阈值。把阈值从 0.7 往 0.55 逐步下调,对比每个类别在混淆矩阵对角线上的数字,就能确定当前数据分布下最合适的回退边界。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询