ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

无人机城市分类数据集实战:从文件名解析到ResNet50微调

2026/9/15 0:43:20 拓冰建站 浏览量
无人机城市分类数据集实战:从文件名解析到ResNet50微调 简介无人机城市分类数据集是一份面向计算机视觉与深度学习研究者的航拍影像分类资源核心内容是无人机拍摄的城市景观图像适用于图像分类、特征提取以及卷积神经网络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)否则会漏样本。w和h在下一步会被拿来与实际图像尺寸对照。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(encodingutf-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.jpg或abc.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, transformNone): 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), antialiasTrue), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.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), antialiasTrue), 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(weightsmodels.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 而只微调layer4和fc是迁移学习里降低过拟合的标准做法。layer4的特征图已经有高层语义而底层权重保存着边缘、颜色等通用特征没必要重新更新。如果训练集超过几万张可以再多解冻layer3并把对应学习率降到5e-5。AdamW与weight_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_splits5, shuffleTrue, random_state42) for fold, (train_idx, val_idx) in enumerate(skf.split(labels_df, labels_df[label])): train_df labels_df.iloc[train_idx].reset_index(dropTrue) val_df labels_df.iloc[val_idx].reset_index(dropTrue) train_ds CityDataset(train_df, DATASET_DIR, train_transform) val_ds CityDataset(val_df, DATASET_DIR, val_transform) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4) model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) model.fc nn.Linear(2048, num_classes) optimizer torch.optim.AdamW(model.fc.parameters(), lr1e-3, weight_decay1e-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(dim1).tolist()) truths.extend(targets.tolist()) print(ffold {fold} loss {total_loss:.4f})上面的循环对每一折都重新初始化模型保证折间独立。argmax(dim1)得到预测类别索引tolist()是为了后续直接喂给 sklearn 的指标函数。运行结束会得到 5 组验证预测把每组拼起来就能得到完整报告。print(classification_report(truths, preds, target_namescategories, digits3)) 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(weightsmodels.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, threshold0.6): model.eval() with torch.no_grad(): logits model(img_tensor.unsqueeze(0)) prob torch.softmax(logits, dim1) top2 torch.topk(prob, k2, dim1) 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 逐步下调对比每个类别在混淆矩阵对角线上的数字就能确定当前数据分布下最合适的回退边界。本文还有配套的精品资源点击获取