简介:这份无人机视角多类别目标检测数据集面向从事航拍视觉算法、无人机自动巡检与智慧城市研究的开发者,提供可直接用于YOLO系列模型训练与验证的标注样本。数据按训练720张、验证192张、测试103张划分,覆盖Bridge、Airplane、Bicycle、Boat、Broccoli、Building、Bus、Car、Crossroad、Helipad、Light、Person、River、Road、Solarfarm、Stadium、Tank、Truck、Volcano共19类目标,兼顾基础设施、交通工具与自然地貌三大维度,并针对低空拍摄的尺度变化、遮挡及河流多边形轮廓等复杂地形做了专门标注。压缩包共2000个文件,以983张jpg图像与1015个txt标注为主,另含1个yaml配置和1份docx说明,整体约57.08MB,边界框坐标经几何验证,可即插即用。目前已有124人学习。借助该数据集,读者可快速搭建基础设施检测、地形勘测、环境监测与应急调度等场景的检测基线,并开展智慧城市与生态监测的交叉研究。
1. 无人机视角多类别目标检测数据集:从标注格式到训练落地的完整链路
拿到「无人机视角多类别目标检测数据集.zip」这个标题,多数人第一反应是解压看图片,然后直接丢进 YOLO 训练脚本。我见过太多人卡在这一步:图片能看,标签打不开,类别对不上,训练 loss 不降。无人机视角的目标检测和常规地面视角有本质差异——小目标密集、尺度跨度大、俯视遮挡严重,数据集的组织方式直接决定模型能不能收敛。这个标题背后真正要解决的问题是:如何把一份无人机视角的多类别标注数据,从压缩包状态变成可训练、可复现、可评估的工程资产。适合已经跑通过至少一次检测训练、但对数据侧细节没系统梳理过的从业者,也适合刚拿到类似数据集、不知道从哪下手的新手。下面按「先看懂数据长什么样,再动手转换,最后避坑和验证」的顺序拆开讲。
2. 先搞懂无人机视角数据集的三个特殊之处
2.1 俯视小目标为什么让常规 anchor 失效
无人机在 50 到 150 米高度拍摄时,一辆车在 4K 画面里可能只占 20×15 像素,一个人可能只有 12×8 像素。常规 COCO 预训练模型的 anchor 尺寸基于地面视角设计,最小 anchor 通常在 32×32 以上,直接迁移到无人机数据上,小目标召回率会掉到 30% 以下。我一般会先统计数据集中所有标注框的宽高分布,用 K-means 重新聚类 anchor,而不是沿用默认配置。
统计脚本不复杂,但必须做:
import os import numpy as np from pathlib import Path # 假设标签是 YOLO 格式:class x_center y_center width height(归一化) label_dir = Path("labels/train") wh_list = [] for txt_file in label_dir.glob("*.txt"): with open(txt_file, "r") as f: for line in f.readlines(): parts = line.strip().split() if len(parts) == 5: # width 和 height 是归一化值,需要乘回图像尺寸 # 这里先记录归一化值,后续统一乘 1920×1080 wh_list.append([float(parts[3]), float(parts[4])]) wh_array = np.array(wh_list) # 还原到像素尺度(假设图像统一为 1920×1080) wh_pixels = wh_array * np.array([1920, 1080]) print(f"标注框总数: {len(wh_pixels)}") print(f"宽度分布: min={wh_pixels[:,0].min():.1f}, " f"median={np.median(wh_pixels[:,0]):.1f}, " f"max={wh_pixels[:,0].max():.1f}") print(f"高度分布: min={wh_pixels[:,1].min():.1f}, " f"median={np.median(wh_pixels[:,1]):.1f}, " f"max={wh_pixels[:,1].max():.1f}") # 统计小于 16×16 像素的框占比 small_mask = (wh_pixels[:,0] < 16) & (wh_pixels[:,1] < 16) print(f"极小目标占比: {small_mask.sum() / len(wh_pixels) * 100:.1f}%")这段代码的逻辑是先收集所有归一化宽高,再还原到像素尺度看真实分布。参数上注意两点:图像尺寸要按数据集实际分辨率改,如果数据集中混合了不同分辨率,需要先按每张图的原始尺寸还原再统计。输出里如果极小目标占比超过 40%,就必须在训练配置里增加更小的 anchor 尺度,或者改用无 anchor 的检测头。
2.2 多类别标注的类别不平衡怎么量化
无人机数据集常见类别包括人、车、自行车、卡车、公交车、三轮车等,但分布极不均匀。某次我处理的一份数据里,「人」有 12000 个框,「三轮车」只有 87 个框,比例超过 100:1。这种不平衡直接训练会导致模型对尾部类别几乎无响应。
量化方法很简单,但必须做在划分训练集之前:
from collections import Counter import json # 假设有一个 classes.txt 和对应的 YOLO 标签 class_names = [] with open("classes.txt", "r") as f: class_names = [line.strip() for line in f.readlines()] counter = Counter() for txt_file in Path("labels/all").glob("*.txt"): with open(txt_file, "r") as f: for line in f.readlines(): parts = line.strip().split() if len(parts) == 5: counter[int(parts[0])] += 1 total = sum(counter.values()) print(f"{'类别':<12} {'框数':>8} {'占比':>8}") for cls_id, count in sorted(counter.items()): name = class_names[cls_id] if cls_id < len(class_names) else f"未知_{cls_id}" print(f"{name:<12} {count:>8} {count/total*100:>7.2f}%")输出表格里如果最大类别和最小类别差距超过 50 倍,训练时就要考虑类别加权损失或者对尾部类别做过采样。注意不要在划分验证集之前做重采样,否则验证指标会虚高。
2.3 标注格式决定了你后面能省多少事
无人机视角数据集常见的标注格式有三种:YOLO txt、COCO json、VOC xml。这个标题下的压缩包大概率是 YOLO 格式,因为无人机检测社区普遍用 YOLO 系框架。但也不排除是 COCO 格式,尤其是从标注平台直接导出的。
判断方法:解压后看目录结构。如果有images/和labels/两个平行目录,且 labels 里是 txt 文件,每行 5 个值,那就是 YOLO。如果有annotations/目录且里面是 json,那就是 COCO。如果有Annotations/和JPEGImages/,那就是 VOC。
不同格式的转换成本差异很大。YOLO 转 COCO 需要写脚本,COCO 转 YOLO 也需要写脚本,VOC 转 YOLO 相对简单但要注意坐标是左上角加宽高还是绝对坐标。我一般会先写一个格式探测脚本,自动判断当前数据集属于哪种格式,再决定后续流程。
3. 把压缩包变成可训练数据:目录划分与格式转换
3.1 训练验证测试集划分的固定随机种子写法
拿到数据集后第一件事不是转换格式,而是划分训练集、验证集、测试集。很多人直接按 8:1:1 随机分,但无人机数据有一个坑:同一段视频抽帧得到的图片高度相似,如果随机划分,验证集里会出现和训练集几乎一样的帧,导致验证指标虚高。
正确做法是按拍摄架次或视频片段划分,而不是按图片随机划分。如果数据集没有提供架次信息,至少要用固定随机种子保证可复现:
import random import shutil from pathlib import Path random.seed(42) # 固定种子,保证每次划分结果一致 image_dir = Path("images/all") all_images = sorted(image_dir.glob("*.jpg")) random.shuffle(all_images) n = len(all_images) n_train = int(n * 0.8) n_val = int(n * 0.1) splits = { "train": all_images[:n_train], "val": all_images[n_train:n_train + n_val], "test": all_images[n_train + n_val:] } for split_name, files in splits.items(): img_out = Path(f"dataset/images/{split_name}") lbl_out = Path(f"dataset/labels/{split_name}") img_out.mkdir(parents=True, exist_ok=True) lbl_out.mkdir(parents=True, exist_ok=True) for img_path in files: shutil.copy(img_path, img_out / img_path.name) lbl_path = Path("labels/all") / (img_path.stem + ".txt") if lbl_path.exists(): shutil.copy(lbl_path, lbl_out / lbl_path.name) print(f"训练集: {len(splits['train'])} 张") print(f"验证集: {len(splits['val'])} 张") print(f"测试集: {len(splits['test'])} 张")参数说明:random.seed(42)是固定种子,换成任何整数都可以,关键是整个团队统一。0.8/0.1/0.1是常见比例,如果数据量小于 2000 张,建议改成0.7/0.15/0.15,保证验证集有足够样本。注意复制标签时要检查对应关系,图片和标签文件名必须一致,否则训练时会被当成无标签图片跳过。
3.2 COCO 转 YOLO 的坐标换算与边界处理
如果数据集是 COCO 格式,需要把[x_min, y_min, width, height]转成 YOLO 的[x_center, y_center, width, height]归一化格式。换算本身简单,但边界处理容易翻车:
import json from pathlib import Path def coco_to_yolo(coco_json_path, output_label_dir, img_width, img_height): with open(coco_json_path, "r") as f: data = json.load(f) # 建立 image_id 到文件名的映射 img_id_to_name = {img["id"]: img["file_name"] for img in data["images"]} # 建立 category_id 到连续 id 的映射 cat_ids = sorted([cat["id"] for cat in data["categories"]]) cat_id_to_idx = {cat_id: idx for idx, cat_id in enumerate(cat_ids)} output_dir = Path(output_label_dir) output_dir.mkdir(parents=True, exist_ok=True) # 按图片聚合标注 from collections import defaultdict img_anns = defaultdict(list) for ann in data["annotations"]: img_anns[ann["image_id"]].append(ann) for img_id, anns in img_anns.items(): file_name = img_id_to_name[img_id] stem = Path(file_name).stem lines = [] for ann in anns: x, y, w, h = ann["bbox"] # 裁剪到图像边界内,防止越界 x = max(0, x) y = max(0, y) w = min(w, img_width - x) h = min(h, img_height - y) if w <= 1 or h <= 1: continue # 跳过无效框 x_center = (x + w / 2) / img_width y_center = (y + h / 2) / img_height w_norm = w / img_width h_norm = h / img_height cls_idx = cat_id_to_idx[ann["category_id"]] lines.append(f"{cls_idx} {x_center:.6f} {y_center:.6f} " f"{w_norm:.6f} {h_norm:.6f}") with open(output_dir / f"{stem}.txt", "w") as f: f.write("\n".join(lines)) # 调用示例 coco_to_yolo("annotations/instances.json", "labels/converted", 1920, 1080)逻辑说明:先建立 image_id 和 category_id 的映射,再逐图聚合标注。关键参数是img_width和img_height,必须和实际图片尺寸一致,如果数据集中图片尺寸不统一,需要先读取每张图的尺寸再传入。边界处理里w <= 1 or h <= 1是过滤掉宽高小于 1 像素的无效框,这类框在标注质量差的数据集里很常见,不滤掉会导致训练时 loss 异常。
3.3 生成训练配置文件的最小可用模板
转换完格式后,需要生成数据配置文件。以 YOLO 系框架为例,最小配置如下:
# data.yaml path: ./dataset train: images/train val: images/val test: images/test nc: 6 names: 0: person 1: car 2: bicycle 3: truck 4: bus 5: tricycle参数说明:path是数据集根目录,train/val/test是相对路径。nc是类别数,必须和names长度一致。names的顺序必须和标签里的类别 id 严格对应,错一个位置整个训练就废了。我一般会写一个校验脚本,随机抽 20 张图,把标签框画回图上,人工确认类别和位置都对。
4. 训练前必须做的数据校验与可视化
4.1 用脚本检查标签越界和类别错位
标签越界是无人机数据集的高频问题,尤其是从标注平台导出时,坐标可能超出图像范围。越界框在训练时会被裁剪或忽略,但如果不提前发现,你会误以为模型学不会。
from pathlib import Path def validate_labels(label_dir, img_dir, num_classes): issues = [] for txt_file in Path(label_dir).glob("*.txt"): img_file = Path(img_dir) / (txt_file.stem + ".jpg") if not img_file.exists(): issues.append(f"缺失图片: {txt_file.stem}") continue with open(txt_file, "r") as f: for line_no, line in enumerate(f.readlines(), 1): parts = line.strip().split() if len(parts) != 5: issues.append(f"{txt_file.name}:{line_no} 字段数错误") continue cls_id = int(parts[0]) if cls_id < 0 or cls_id >= num_classes: issues.append(f"{txt_file.name}:{line_no} 类别越界 {cls_id}") vals = [float(v) for v in parts[1:]] if any(v < 0 or v > 1 for v in vals): issues.append(f"{txt_file.name}:{line_no} 坐标越界 {vals}") return issues issues = validate_labels("dataset/labels/train", "dataset/images/train", 6) print(f"发现 {len(issues)} 个问题") for issue in issues[:20]: print(issue)这段代码检查三类问题:图片缺失、字段数错误、类别和坐标越界。参数num_classes要和 data.yaml 里的nc一致。输出前 20 条问题足够定位系统性错误,如果问题数量超过总标签数的 5%,建议退回标注环节重新处理,而不是在训练侧硬扛。
4.2 把标注框画回原图做人工抽检
脚本检查只能发现格式问题,发现不了语义错误,比如把公交车标成卡车、框只框了半个车身。抽检方法是随机选 20 张图,把标签画回去,人工看一遍。
import cv2 import random from pathlib import Path random.seed(0) img_dir = Path("dataset/images/train") all_imgs = list(img_dir.glob("*.jpg")) samples = random.sample(all_imgs, min(20, len(all_imgs))) class_names = ["person", "car", "bicycle", "truck", "bus", "tricycle"] colors = [(255,0,0), (0,255,0), (0,0,255), (255,255,0), (255,0,255), (0,255,255)] for img_path in samples: img = cv2.imread(str(img_path)) h, w = img.shape[:2] lbl_path = Path("dataset/labels/train") / (img_path.stem + ".txt") if lbl_path.exists(): with open(lbl_path, "r") as f: for line in f.readlines(): parts = line.strip().split() if len(parts) != 5: continue cls_id = int(parts[0]) xc, yc, bw, bh = [float(v) for v in parts[1:]] x1 = int((xc - bw/2) * w) y1 = int((yc - bh/2) * h) x2 = int((xc + bw/2) * w) y2 = int((yc + bh/2) * h) color = colors[cls_id % len(colors)] cv2.rectangle(img, (x1,y1), (x2,y2), color, 2) cv2.putText(img, class_names[cls_id], (x1, y1-5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) cv2.imwrite(f"vis_{img_path.name}", img) print("可视化结果已保存到当前目录")逻辑说明:读取图片和对应标签,把归一化坐标还原成像素坐标,画框并标注类别名。参数colors按类别数循环使用,类别多的时候颜色会重复,但不影响检查。抽检重点看三类问题:框是否贴合目标、类别是否正确、有没有漏标。漏标在无人机数据里很常见,尤其是密集小目标区域。
4.3 统计每类目标在训练集中的实际分布
训练前最后一步是确认训练集和验证集的类别分布是否一致。如果训练集里「公交车」有 2000 个框,验证集里只有 5 个,验证指标对公交车就没有参考意义。
from collections import Counter from pathlib import Path def count_per_class(label_dir, num_classes): counter = Counter() for txt_file in Path(label_dir).glob("*.txt"): with open(txt_file, "r") as f: for line in f.readlines(): parts = line.strip().split() if len(parts) == 5: counter[int(parts[0])] += 1 return counter train_counter = count_per_class("dataset/labels/train", 6) val_counter = count_per_class("dataset/labels/val", 6) print(f"{'类别':<12} {'训练集':>8} {'验证集':>8} {'验证/训练':>10}") for cls_id in range(6): tr = train_counter.get(cls_id, 0) va = val_counter.get(cls_id, 0) ratio = va / tr if tr > 0 else 0 print(f"{class_names[cls_id]:<12} {tr:>8} {va:>8} {ratio:>10.3f}")输出里如果某个类别的验证/训练比例远低于整体比例,说明验证集对这个类别的评估不可靠。解决办法是在划分时做分层抽样,保证每个类别的验证集占比接近 10%。
5. 避坑:无人机数据集处理中最容易翻车的五个点
5.1 图片和标签文件名不一致导致静默跳过
现象:训练时 loss 正常下降,但验证指标极低,检查发现大量图片没有对应标签。原因:图片是DJI_0001.jpg,标签是DJI_0001.txt,但中间有空格或大小写差异,训练框架匹配不上就跳过。解决:写脚本统一重命名,去掉空格、统一小写,并校验每个图片都有对应标签。
5.2 类别 id 从 1 开始导致训练时类别数对不上
现象:训练启动时报类别越界或 loss 为 nan。原因:标注工具导出的类别 id 从 1 开始,但配置文件里nc设的是类别数,框架按 0 到 nc-1 索引。解决:转换时统一把类别 id 减 1,或者把nc设成最大 id 加 1。我一般会在转换脚本里强制重映射。
5.3 图像尺寸不统一导致归一化坐标错乱
现象:可视化时框的位置整体偏移。原因:数据集里混合了 1920×1080 和 3840×2160 两种分辨率,但转换时统一按 1920×1080 归一化。解决:转换前先扫描所有图片尺寸,按每张图的实际尺寸分别归一化,或者统一缩放到同一尺寸再转换。
5.4 验证集包含训练集同源帧导致指标虚高
现象:验证集 mAP 比测试集高 20 个点以上。原因:同一段视频抽帧的图片被随机分到了训练集和验证集,两边的画面几乎一样。解决:按视频片段或拍摄架次划分,同一架次的数据只出现在一个 split 里。
5.5 标签文件里有空行或多余空格导致解析失败
现象:训练时部分标签被跳过,对应图片变成负样本。原因:标注工具导出时在文件末尾留了空行,或者行内有多个连续空格。解决:转换时用line.strip().split()而不是line.split(),并过滤掉字段数不等于 5 的行。
6. 进阶:用分层抽样和锚框重聚类把 mAP 再提一截
数据侧的处理做完后,如果 mAP 还不理想,有两个低成本高回报的优化方向。第一个是分层抽样划分数据集,保证每个类别在验证集里的占比和训练集一致。实现方法是在划分前按类别对图片分组,每个类别按比例抽取验证集图片,最后合并去重。这样验证指标才能真实反映模型在每个类别上的表现。
第二个是锚框重聚类。前面统计了宽高分布,现在用 K-means 聚出 9 组锚框:
from sklearn.cluster import KMeans import numpy as np # wh_pixels 来自 2.1 节的统计结果 kmeans = KMeans(n_clusters=9, random_state=42, n_init=10) kmeans.fit(wh_pixels) anchors = kmeans.cluster_centers_ # 按面积从小到大排序 anchors = anchors[np.argsort(anchors[:,0] * anchors[:,1])] print("重聚类锚框(宽, 高):") for a in anchors: print(f" {a[0]:.1f}, {a[1]:.1f}")把输出替换到模型配置的anchors字段里,小目标召回率通常能提升 5 到 10 个点。注意聚类前要对宽高做对数变换,否则大框会主导聚类结果。我一般会跑三次不同随机种子,取平均结果,避免单次聚类的偶然性。
最后一个技巧是训练时开启马赛克增强,但无人机视角下马赛克增强的概率不要设太高,0.5 左右比较合适。太高会让小目标在拼接后变得更小,反而伤害召回。这些参数没有绝对最优,需要在验证集上试两到三组。我自己踩过的最大坑是拿到数据直接训练,跑了三天才发现验证集和训练集同源,指标全是假的。后来养成习惯:任何数据集先做划分校验和可视化抽检,确认数据没问题再开训练。希望帮到你。
本文还有配套的精品资源,点击获取