ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

航拍小目标检测数据集实战:从VOC/YOLO格式校验到YOLOv8训练调优

航拍小目标检测数据集实战:从VOC/YOLO格式校验到YOLOv8训练调优 简介面向智慧牧场航拍场景的牛羊检测任务该数据集提供了一千零二十一张航拍图像及对应的Pascal VOC与YOLO双格式标注文件覆盖牛、母牛、羊三个类别合计一万四千零四十七个标注框。得益于完整的标签数据开发者可以省去从原始视频中筛选并裁剪目标的工作直接用于远距离小目标的模型训练与精度评估。压缩包内文件总数为两千个以一千零二十一个XML标注文件和九百七十九个TXT标签文件为主要组成整体体积约为九十七点五八兆字节解压后即可按需划分为训练集与验证集使用起来非常便捷。目前已有三百七十九人学习下载。对于智慧农业、无人机视觉或目标检测方向的研究者这一数据集既适合用于YOLO系列、Faster R-CNN等主流检测器的效果对比也可支撑牧场牛羊的个体识别、数量统计与密度估计等实际应用是验证算法在小目标场景下鲁棒性的实用资源。1. 智慧牧场航拍牛羊检测这份远距离小目标数据集能直接拿去训练吗无人机航拍数牛羊难点从来不是认得出牛羊而是几百米高空看下去一群牛只占几十个像素人眼都要眯半天模型更是容易漏检。这份智慧牧场航拍牛羊检测数据集就是针对这个场景来的1021张jpg图片cattle、cow、sheep三个类别总共14047个标注框同时输出Pascal VOC和YOLO两种格式既能拖进labelImg复核标注也能直接喂给YOLOv5/YOLOv8训练。做智慧牧场、无人机巡检或任何航拍小目标检测项目的人值得先对照这份数据的标注分布看看自己的数据差在哪。它不保证训练精度但那14047个框是准确且合理的这是训练能起步的前提。2. 数据集结构拆解VOC与YOLO双格式、三类别的标注分布2.1 文件构成与解压后的目录组织拿到手的压缩包是7z格式Linux下用p7zip解压Windows下用7-Zip就行。解压之后是一个平铺目录jpg图片、xml文件、txt文件各1021个文件名一一对应。xml是Pascal VOC格式标注txt是YOLO格式标注两份标注描述的是同一批框。解压命令里把压缩包名换成你实际下载的那个文件7z x pasture_animal.7z -o./firc_animal7z x解压并保留压缩包内目录结构-o后面接输出目录注意-o和目录名之间没有空格。解压完成后先确认三类文件数量都是1021ls firc_animal/*.jpg | wc -l ls firc_animal/*.xml | wc -l ls firc_animal/*.txt | wc -l三行命令分别统计三类文件数量如果哪个数量对不上大概率是压缩包损坏或解压中断需要回退到完整性校验那一步。从文件名看编号并不连续firc_animal_467、479、481、482、484、489、490、491、503、504……中间跳过不少序号说明数据是从更大规模航拍素材里筛出来的原作者去掉了模糊、过暗、目标过小或完全无目标的帧。这种筛选习惯对训练是正向的模型不用在废图上浪费学习能力验证集也比较干净。每张图片平均有13.7个目标框14047/1021这个密度在航拍场景里属于中等偏上。一群羊围在一起时框与框之间大量重叠、紧挨标注上没问题但训练时会给NMS带来压力后面第5章的增强设置会专门处理。2.2 类别分布与不均衡问题三个类别的框数差异相当大直接看表类别框数占比平均每张图框数cattle412829.4%4.0cow4012.9%0.4sheep951867.7%9.3sheep接近七成cattle接近三成cow只有401框占比不到3%。这个分布带来两个直接问题第一模型对sheep的拟合会很好对cow的召回可能很低而且很难靠单纯加epoch解决因为梯度更新被多数类主导第二cow和cattle语义上都是牛cattle是牛的总称cow特指母牛标注者把一部分牛标成cattle、一部分标成cow实际使用前必须先想清楚业务到底需要几个类别。如果系统只需要牛/羊二分把cattle和cow合并是最理性的做法不用让模型浪费参数去学一个航拍视角下本身就难分清的语义差异。xml里记录的是标准VOC信息每个object节点包含name、pose、truncated、difficult和bndbox坐标。对这份数据来说name只有三种bndbox给出xmin、ymin、xmax、ymax四个像素坐标txt每行是类别id 中心x 中心y 宽度 高度归一化到0到1。两套坐标描述同一个框训练框架一般只需其中一种同时保留两份的价值在于方便交叉验证和切换工具链。2.3 远距离小目标成像对标注框尺寸的影响航拍场景下目标像素小是常态这也反映在标注框上。把全部xml里的框宽高归一化统计一遍就能量化小目标占比import xml.etree.ElementTree as ET import os xml_dir firc_animal widths, heights [], [] for name in os.listdir(xml_dir): if not name.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, name)) root tree.getroot() size root.find(size) w_img int(size.find(width).text) h_img int(size.find(height).text) for obj in root.findall(object): box obj.find(bndbox) xmin int(box.find(xmin).text) xmax int(box.find(xmax).text) ymin int(box.find(ymin).text) ymax int(box.find(ymax).text) widths.append((xmax - xmin) / w_img) heights.append((ymax - ymin) / h_img) small sum(1 for w, h in zip(widths, heights) if w 0.05 and h 0.05) print(f框总数: {len(widths)}, 相对尺寸小于5%的框: {small})这段代码遍历所有xml把框宽高归一化到图片尺寸。对一张1920x1080的航拍图来说5%意味着宽度不到96像素、高度不到54像素实际很多目标比这个还小。这个占比直接决定训练时imgsz怎么选小目标比例越高imgsz越要往1280以上提。3. 目录组织与格式衔接从7z压缩包到可训练数据集的落地路径3.1 重建标准VOC目录结构解压后的平铺目录虽然能直接用但不同训练框架对数据集目录有自己的约定YOLO系一般要求images和labels分开MMDetection习惯读VOC结构。我拿到任何VOCYOLO混标数据集后的第一步都是先重建标准VOC目录给后续操作留余地mkdir -p VOCdevkit/VOC2024/{JPEGImages,Annotations,ImageSets/Main} mv firc_animal/*.jpg VOCdevkit/VOC2024/JPEGImages/ mv firc_animal/*.xml VOCdevkit/VOC2024/Annotations/JPEGImages放原图Annotations放xmlImageSets/Main放train.txt和val.txt这些划分列表VOC工具链和大量开源脚本默认认这三层路径。txt文件先不动YOLO训练时通常有自己独立的labels目录等目录结构稳定后再统一按类别放好。3.2 VOC坐标与YOLO坐标的换算验证VOC坐标是像素级绝对坐标YOLO坐标是归一化相对坐标两者定义不同但描述同一个矩形。这份数据的txt已经生成好理论上不需要自己转换但直接信任不是好习惯。抽查几个文件把txt反算回像素坐标再和xml里的bndbox比对import xml.etree.ElementTree as ET xml_file VOCdevkit/VOC2024/Annotations/firc_animal_491.xml txt_file firc_animal/firc_animal_491.txt tree ET.parse(xml_file) root tree.getroot() size root.find(size) w_img int(size.find(width).text) h_img int(size.find(height).text) xml_boxes [] for obj in root.findall(object): box obj.find(bndbox) xml_boxes.append(( int(box.find(xmin).text), int(box.find(ymin).text), int(box.find(xmax).text), int(box.find(ymax).text), )) with open(txt_file) as f: lines [line.strip().split() for line in f if line.strip()] print(f图片尺寸: {w_img}x{h_img}, xml目标数: {len(xml_boxes)}, txt目标数: {len(lines)}) for line in lines: cls int(line[0]) cx, cy, bw, bh map(float, line[1:]) xmin int((cx - bw / 2) * w_img) xmax int((cx bw / 2) * w_img) ymin int((cy - bh / 2) * h_img) ymax int((cy bh / 2) * h_img) print(f类别{cls}: 反算像素框({xmin},{ymin},{xmax},{ymax})) print(xml第一个框:, xml_boxes[0] if xml_boxes else 无)脚本把txt的归一化中心点和宽高反算成像素坐标输出后和xml的第一个框对比数值差1到2个像素属于正常因为归一化小数保留位数会带来取整误差。如果差得多说明txt和xml不是同一批标注或者类别顺序被改过必须停下排查。随机挑三到五张做抽检就够了目的是确认双格式一致性不是重新校验每一行。3.3 划分训练集与验证集注意分层抽样训练需要train和val目录但这份数据集没有预划分。按常见做法8:2切分但我建议用分层抽样而不是纯随机因为cow只有401框、分布在少量图片上随机切分极易把带cow的图片全分到某一侧import os import random from collections import defaultdict random.seed(42) xml_dir VOCdevkit/VOC2024/Annotations class_to_files defaultdict(list) for name in os.listdir(xml_dir): if not name.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, name)) root tree.getroot() has_cow any(obj.find(name).text cow for obj in root.findall(object)) key cow if has_cow else no_cow class_to_files[key].append(name[:-4]) train_files, val_files [], [] for key in [cow, no_cow]: files class_to_files[key] random.shuffle(files) split int(len(files) * 0.8) train_files.extend(files[:split]) val_files.extend(files[split:]) random.shuffle(train_files) random.shuffle(val_files) with open(VOCdevkit/VOC2024/ImageSets/Main/train.txt, w) as f: f.write(\n.join(train_files)) with open(VOCdevkit/VOC2024/ImageSets/Main/val.txt, w) as f: f.write(\n.join(val_files)) print(ftrain: {len(train_files)}, val: {len(val_files)})先按这张图里有没有cow分组组内再按8:2切保证验证集里也有cow样本。seed固定在42让每次复现实验用同一个划分否则换模型调参时训练集都不一样对比结果没有说服力。统计cow的代价是一次全量xml遍历几秒钟的事别嫌麻烦。4. 常见问题避坑解压异常、标签映射、类别漏标的排查手册4.1 7z解压报错或文件数量对不上现象解压到一半提示CRC校验失败、文件末端错误或者解压完统计少了十几个文件。原因网络传输导致压缩包损坏下载工具多线程分段下载时某段写坏是重灾区。解决先别反复试解压用测试模式做完整性校验7z t pasture_animal.7zt是test模式只校验不释放几秒就能出结果。输出里有error就直接重新下载原文件用单线程或带断点续传的下载方式。损坏的7z在部分解压软件里能强行解压出部分文件但缺失和损坏的部分不可预知这种半拉子数据喂给训练就是定时炸弹。第一次用损坏包训练时loss曲线很正常但验证集mAP稀烂查了一下午才发现是图片和标签错位。4.2 txt里的类别id和训练yaml的names顺序对不上现象训练能跑但验证结果里cattle的框对应到羊的类别名或者loss一直不降。原因txt第一列是类别idid的含义由训练yaml的names顺序决定。如果txt是用某个脚本重建的脚本里的classes列表顺序和yaml不一致id就全乱了。解决训练前固定一份权威classes列表比如[cattle, cow, sheep]所有txt生成、yaml编写都从它派生。启动训练前随机看几个txt的第一列head -5 firc_animal/firc_animal_491.txt看到大量2开头yaml里2: sheep那说明对应关系正确。一旦中途改过类别顺序所有txt必须同步重生成不能只改一半。4.3 某个txt文件行数比xml里的object少现象xml里明明有4个object对应txt只有2行或者labelImg打开显示正常但训练时这个样本的损失异常。原因labelImg在VOC和YOLO模式之间切换时最后保存动作没完成YOLO格式txt可能是旧版或空文件标注者中途切换保存格式也会导致部分标签丢失。解决以xml为准重建txt。下面的转换脚本全量重建输出到独立labels目录避免覆盖原始文件import xml.etree.ElementTree as ET import os os.makedirs(labels, exist_okTrue) def voc_to_yolo(xml_path, txt_path, classes): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w_img int(size.find(width).text) h_img int(size.find(height).text) lines [] for obj in root.findall(object): name obj.find(name).text if name not in classes: continue cls_id classes.index(name) box obj.find(bndbox) xmin int(box.find(xmin).text) ymin int(box.find(ymin).text) xmax int(box.find(xmax).text) ymax int(box.find(ymax).text) x_center ((xmin xmax) / 2) / w_img y_center ((ymin ymax) / 2) / h_img w (xmax - xmin) / w_img h (ymax - ymin) / h_img lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) with open(txt_path, w) as f: f.write(\n.join(lines)) classes [cattle, cow, sheep] xml_dir VOCdevkit/VOC2024/Annotations for name in os.listdir(xml_dir): if not name.endswith(.xml): continue base name[:-4] voc_to_yolo(os.path.join(xml_dir, name), flabels/{base}.txt, classes)重建后再做一次全量比对把xml的目标数和txt行数对不齐的文件揪出来for base in [f[:-4] for f in os.listdir(xml_dir) if f.endswith(.xml)]: tree ET.parse(os.path.join(xml_dir, base .xml)) n_obj len(tree.getroot().findall(object)) txt_path flabels/{base}.txt if not os.path.exists(txt_path): print(f{base}: 无txt) continue with open(txt_path) as f: n_txt sum(1 for line in f if line.strip()) if n_obj ! n_txt: print(f{base}: xml{n_obj}, txt{n_txt})输出里任何xml不等于txt的文件都要单独处理。注意过滤空白行否则统计会误报。4.4 图片与标注文件对应不全现象训练时提示找不到图片或没有标签DataLoader直接跳过样本或报错。原因从大目录筛选素材时删了jpg没删xml或者拷贝过程中某个扩展名文件没拷全。解决用集合差快速定位import os jpg_set {f[:-4] for f in os.listdir(VOCdevkit/VOC2024/JPEGImages) if f.endswith(.jpg)} xml_set {f[:-4] for f in os.listdir(VOCdevkit/VOC2024/Annotations) if f.endswith(.xml)} print(有图无标注:, len(jpg_set - xml_set), list(jpg_set - xml_set)[:5]) print(有标注无图:, len(xml_set - jpg_set), list(xml_set - jpg_set)[:5])有图无标注的图片建议直接从训练列表剔除不要自己补标补的框画不准反而成为干扰样本。有标注无图的xml直接归档到backup目录留着审计用。5. 用YOLOv8训练这份数据集参数设置与小目标检测调优5.1 数据yaml与目录准备训练前准备数据yaml指定数据根路径、图像目录、标签目录和类别名path: /data/pasture/firc_animal train: images/train val: images/val names: 0: cattle 1: cow 2: sheeppath用绝对路径最稳妥相对路径在不同机器上解析结果不一样换机器训练时排查成本高。names顺序必须和txt第一列对应0是cattle、1是cow、2是sheep这就是第4章反复强调的对应关系。目录方面需要把图片按train/val分放标签也按相同命名空间组织mkdir -p images/train images/val labels/train labels/val对照第3章生成的train.txt和val.txt把jpg和txt分别拷入对应目录不要手拷写个三四行的脚本或者直接用rsync按列表同步避免漏文件。5.2 训练命令、输入尺寸与损失函数相关参数基础训练命令yolo detect train datasheep.yaml modelyolov8n.pt epochs100 imgsz1280 batch16 device0 patience20关键参数逐个说modelyolov8n.pt用nano预训练权重起步显存小优先跑通流程imgsz1280是这份数据集最重要的一个参数航拍小目标在640输入下只有十几个像素特征提取很难到位提到1280相当于目标面积放大4倍batch16按显存调OOM就降到8再不行降到4我一般先试16能跑就用patience20是早停20个epoch验证指标不涨就停省时间。损失函数方面YOLOv8的分类损失用BCE回归部分用CIoU加DFL。小目标对回归误差更敏感同样偏移几个像素小框的IoU下降远大于大框。box参数控制回归损失权重默认就是7.5改它不是首选手段。更有效的是把close_mosaic调大让训练后期更多epoch用完整图片yolo detect train datasheep.yaml modelyolov8n.pt epochs100 imgsz1280 batch16 close_mosaic15close_mosaic表示最后15个epoch关闭mosaic增强。航拍小目标对mosaic不友好一只100x100的羊被四张图切开拼接模型学的是碎片特征收官阶段用完整的自然图像精调对小目标收敛有帮助。同理航拍牛羊不存在翻转后语义改变的问题可以显式关闭随机翻转减少增强噪声yolo detect train datasheep.yaml modelyolov8n.pt epochs100 imgsz1280 batch16 close_mosaic15 fliplr0fliplr0关闭水平翻转。最后是多尺度训练无人机高度变化会直接改变目标像素尺寸固定输入会让模型对高度敏感。官方默认的scale0.5就是为这个场景准备的显式写出来方便调yolo detect train datasheep.yaml modelyolov8n.pt epochs100 imgsz1280 batch16 close_mosaic15 fliplr0 scale0.5scale0.5表示图像在0.5到1.5倍之间随机缩放等效模拟不同飞行高度。5.3 类别不均衡的解决思路cow只有401框直接硬训的预期结果是sheep精度很高、cow几乎检不出。给三条路径按业务需求选。第一业务只需区分牛羊把cattle和cow合并。合并操作就是改txt第一列cow是1改成0sed -i s/^1 /0 / labels/train/*.txt labels/val/*.txts/^1 /0 /匹配行首的1 替换成0 cow并入cattle同时yaml里names改成两个类别。这最省事也最符合数牛羊的实际业务。提示改txt前先备份sed -i是原地修改没有后悔药。第二必须区分cow做数据集层面的过采样把含cow的图片在训练列表里重复3到5次让模型更多次看到cow样本。缺点是有可能过拟合那401框需要配合早停观察。第三用few-shot思路单独微调。冻结backbone只训检测头让cow在这一轮里不被sheep的梯度淹没。这条路线适合后续补数据的场景先把流程搭好新数据到位后直接增量训练。6. 验证结果的小技巧用置信度阈值和混淆矩阵看模型真实水平6.1 低置信度先测召回上限训练完先别急着看默认mAP用低置信度阈值把验证集刷一遍yolo detect val modelruns/detect/train/weights/best.pt datasheep.yaml conf0.05 iou0.5conf0.05远低于部署阈值正常推理用0.25或0.3。这一步的目的不是评估部署指标而是回答一个问题模型到底学到目标特征没有。0.05下召回也上不去说明模型没学会小目标特征调阈值没用要回头改特征提取链路0.05召回不错但0.3掉一半说明模型对目标有感知但置信度不足可以从增强、imgsz、损失权重着手。很多人只在默认0.25下看一眼mAP就下结论信息量差太远。6.2 看混淆矩阵再决定要不要合并类别yolo detect val输出里自带confusion_matrix.png重点看cattle和cow那两行。如果cow大量被分到cattle说明两个类别在航拍视角下视觉特征高度重叠标注本身的语义区分度就不够——肉牛和母牛从几百米往下看标注者自己都要辨认半天。这种情况不要纠结模型能力合并类别是损失最小的决策。如果sheep被分到cattle那才是真问题说明牛和羊的基础特征都没学到需要回去查训练数据或增强设置。6.3 用imgsz-recall曲线选部署参数部署时imgsz不是越大越好显存和延迟都是成本。把640、960、1280、1536四档分别跑验证记录小目标类别召回率画一条曲线贴在项目文档里。通常640到1280是明显上升段1280之后收益陡峭下降这个拐点就是性价比最优的部署输入尺寸直接写进推理配置。从那以后我每拿到一份新数据集都强制先走一遍目录抽检、双格式一致性校验、imgsz曲线再开训练这套流程帮我拦下了不少看起来能跑、实际一堆隐患的数据。希望这份航拍牛羊数据集能帮你省下这些排查时间把精力放在真正影响精度的环节上。本文还有配套的精品资源点击获取
返回列表