ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

航拍牛羊小目标检测:VOC转YOLO数据集与训练避坑实战

航拍牛羊小目标检测:VOC转YOLO数据集与训练避坑实战 简介面向智慧牧场的航拍牛羊检测数据集聚焦无人机视角下的远距离小目标识别适合目标检测算法研究人员与智慧农业开发者使用。压缩包为7z格式共2000个文件以1021个XML标注文件与979个TXT标注文件为主对应1021张航拍图像整体大小约97.58MB可直接用于VOC或YOLO系列模型训练。数据包含cattle、cow、sheep三个类别合计14047个矩形标注框其中sheep框数9518、cattle框数4128、cow框数401类别不均衡特征明显可用于小目标检测、长尾分布等方向的研究。标注使用labelImg完成每张图均配齐VOC格式XML与YOLO格式TXT省去手动转换格式的麻烦同时也保留完整图像文件便于二次筛选与清洗。目前已有379人学习数据集保证标注准确且合理但不对具体模型权重精度作任何承诺适合作为智慧牧场视觉感知任务的基准数据与算法验证基础。1. 智慧牧场航拍牛羊检测1021张图解决的是“远距离小目标”这个难题无人机在草原上空100米左右高度巡航时一头牛在画面里可能只占十几个像素羊群更是密集得连成一片——这正是智慧牧场航拍牛羊检测要面对的真实工况。标题里的这个数据集目标就是给这类场景补数据1021张航拍图像、3个类别标注同时交付VOC和YOLO两种格式最后压成7z包减少传输成本。对做智慧牧场、草畜平衡监测、无人机巡检的算法工程师和研究生来说拿到手第一件事不是急着训练而是先解压、看懂格式规范、核对自己的训练管线能不能消费这批标注再决定后续策略。2. 解开7z包VOC与YOLO两种格式的目录结构和标注差异7z是这类航拍数据集常见的分发压缩格式压缩率比zip高但坑也在格式本身新环境没有对应解压工具时习惯性双击会直接失败。标题里特意写明7z就是提醒你先过这一关再谈后续格式和训练。下面按解压、VOC侧、YOLO侧三条线讲清楚。2.1 Linux与Windows下解压7z命令、PyCharm和目录核对Linux下解压7z文件核心是装 p7zip-full 这个包。Debian/Ubuntu系列执行sudo apt update sudo apt install -y p7zip-full cd ~/datasets 7z l 智慧牧场航拍牛羊检测远距离小目标数据集VOCYOLO格式1021张3类别.7z7z l先列压缩包内部清单这一步能确认压缩包里是顶层目录还是散文件避免解压后一堆jpg直接铺在当前目录。确认没有异常后7z x 智慧牧场航拍牛羊检测远距离小目标数据集VOCYOLO格式1021张3类别.7z -o./smart_pasture参数说明x是解压并保留目录结构-o指定输出目录而且-o和目标路径之间不能有空格这是7z命令行里最容易翻车的地方。如果你解压出来的目录里多了一层嵌套多半是当初打包时顶层目录写法和现在预期不一致不是解压命令错。Windows下常见做法是装7-Zip或Bandizip右键解压即可。但不少人在PyCharm里跑训练脚本不想来回切工具想在Python里直接读7z那就不建议临时调外部exe而是装 py7zrimport py7zr archive_path rD:\datasets\smart_pasture.7z with py7zr.SevenZipFile(archive_path, moder) as z: z.extractall(pathrD:\workspace\smart_pasture)逻辑说明py7zr 是纯Python实现PyCharm里装在当前解释器后就不依赖系统装7z。注意extractall会解开全部子目录如果你只需要VOC那一半可以先用z.getnames()看清单再z.extract(targets[...])指定文件。航拍数据集的完整包动辄几个GB按需解压能省不少时间不用每次都把整个包解到底。提示解压完成后第一件事是核对目录。VOC侧应有 JPEGImages、Annotations、ImageSets/Main 三个关键目录YOLO侧一般有 images 和 labels 两个平级目录。标题写“VOCYOLO格式”完整数据集应当两类都在缺了一侧就尽早补别自己硬转。2.2 VOC格式JPEGImages、Annotations与ImageSets/Main的XML标注解析VOCPascal VOC格式是一套沿用多年的检测数据集组织方式它不限定图像编码只约定标注怎么放。这个数据集里的VOC侧常见目录是JPEGImages/所有原始航拍图像jpg或pngAnnotations/与图像同名的xml文件每张图对应一个xmlImageSets/Main/txt行式索引文件常见有train.txt、val.txt、trainval.txt典型xml内容annotation folderJPEGImages/folder filenameIMG_00001.jpg/filename size width1280/width height720/height depth3/depth /size object namecattle/name bndbox xmin45/xmin ymin120/ymin xmax105/xmax ymax180/ymax /bndbox /object /annotation参数说明size里的宽高是原图绝对像素bndbox同样是绝对像素坐标。这张图的分辨率才是之后归一化计算的分母——没有size的xml就是坏文件转换脚本如果强行缺省用图像实际读取尺寸遇到标注尺寸和真实图尺寸不一致时会出现坐标整体漂移这是VOC转YOLO的一个典型翻车点。对远距离小目标VOC标注里会大量出现xmax - xmin小于10像素的框。xml解析时要顺手把这类框筛出来做统计它们决定了后续imgsz和anchors的调参方向。航拍场景一个常见现象是一张1280x720图里大半目标都在32x32像素以下只看总mAP看不出问题要看目标尺寸分布。2.3 YOLO格式归一化txt标注与三个类别的排列YOLO格式的标注是每张图一个同名txt每一行代表一个目标类别id、中心点x、中心点y、宽w、高h全部归一化到0–1区间。以牛cattle0、羊sheep1、群flock2三个类别为例0 0.0422 0.8113 0.0156 0.0250 0 0.4231 0.3352 0.0219 0.0250 1 0.7100 0.5820 0.0203 0.0289 2 0.1209 0.4501 0.1800 0.1500x_center (xmin xmax) / 2 / widthw (xmax - xmin) / width正好是前面VOC xml里换算出来的结果。这里所有数值都应小于1如果哪行出现大于1的坐标说明转换过程把宽高搞错了——常见原因是拿VOC的width当height用或者把原始像素坐标直接写进txt没归一化。yolo训练脚本读到异常坐标不会报错而是把这行目标忽略掉大量标注行被跳过时训练全程看起来在跑实际学不到东西——所以转换完成后必须做合法性校验不能直接丢给训练器。三个类别的排列顺序直接决定训练yaml里的names顺序必须一致。如果原数据集的类别顺序是 cattle、sheep、flock你yaml里写成 sheep、cattle、flock会造成标签错配模型把羊当牛学。对来自VOC的数据集常见做法是先读一遍Annotations目录里所有xml的name去重排序以此为准生成classes列表而不是靠肉眼猜。3. 把VOC转成YOLO转换脚本、数据集划分与标注校验有的数据集把VOC和YOLO都放全但更多时候只给你其中一种常见做法是动手写一次VOC→YOLO转换脚本顺带把数据划分和合法性检查一起做掉。这三个步骤连在一起因为只转格式不划分后面训练时还得自己拼train/val路径。3.1 VOC转YOLO的最小可运行Python脚本import os import xml.etree.ElementTree as ET CLASSES [cattle, sheep, flock] def convert_voc_annotation(xml_path, output_dir): tree ET.parse(xml_path) root tree.getroot() width int(root.find(size/width).text) height int(root.find(size/height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in CLASSES: print(fskip unknown class: {name} in {xml_path}) continue cls_id CLASSES.index(name) bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) cx ((xmin xmax) / 2.0) / width cy ((ymin ymax) / 2.0) / height w (xmax - xmin) / width h (ymax - ymin) / height if w 0 or h 0: continue lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) output_path os.path.join( output_dir, os.path.splitext(os.path.basename(xml_path))[0] .txt ) with open(output_path, w, encodingutf-8) as f: f.write(\n.join(lines))逻辑说明这段代码从xml的object里逐条取类别和bndbox再用xml里size的width/height做归一化生成YOLO txt标注。中间加了两道过滤未知类别直接跳过并打印提示宽高小于等于0的退化框跳过。小目标数据集里的退化框数量其实不少标注人员在边缘目标上多标或少标1像素就会产生这类坏框跳过比写进训练集更安全因为大部分YOLO实现遇到无效框会静默丢弃。转换时另一个容易被忽略的是类别顺序要和模型配置一致。上面 CLASSES 的顺序是 cattle、sheep、flock训练脚本的 data.yaml 里 names 必须跟着这套顺序。如果原数据集类别顺序不一致先列出全部类别再定表别在这里偷懒。3.2 按比例划分train/val/test并生成data.yaml有了txt标注后划分数据集要保证图像编号不泄漏。航拍数据有时连着多张同一航线图直接按文件名顺序剖开头100张、中间100张会造成训练集和验证集高度重合。常见做法是先随机打乱再用固定seed复现。import os, random from glob import glob IMAGE_DIR images random.seed(42) images sorted(glob(os.path.join(IMAGE_DIR, *.jpg)) glob(os.path.join(IMAGE_DIR, *.png))) random.shuffle(images) n len(images) train_end int(n * 0.8) val_end int(n * 0.9) def write_split(paths, out_file): with open(out_file, w) as f: for p in paths: stem os.path.splitext(os.path.basename(p))[0] f.write(stem \n) write_split(images[:train_end], train.txt) write_split(images[train_end:val_end], val.txt) write_split(images[val_end:], test.txt)逻辑说明先把图片路径随机打乱再按8:1:1划成train/val/test每份只写文件主名不带后缀。YOLOv8的data.yaml里可以写train: images/train目录式路径也可以写train: train.txt索引式路径两条路都通。用索引式更省事不用额外移动图像文件。path: ./smart_pasture train: train.txt val: val.txt test: test.txt nc: 3 names: [cattle, sheep, flock]参数说明path是相对路径根目录train、val可以是指向目录或索引txt的路径nc必须是类别数3names顺序必须和转换脚本的CLASSES完全一致。如果跑起来报错说找不到labels或images优先检查路径根目录data.yaml里的路径是相对path解析的最容易翻车的就是把path写到别的层级。3.3 转换后必须做的标注抽查手段转换脚本跑完第一件事不是直接训练而是抽查十几张图把YOLO坐标画回原图看框是否贴合。这步不可省航拍小目标的框普遍很小肉眼逐张看xml还未必看得出问题画框后目标位置是否漂移一眼就能发现。import cv2, os def draw_yolo_labels(image_path, label_path, class_names): img cv2.imread(image_path) h_img, w_img img.shape[:2] with open(label_path) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls_id, cx, cy, w, h int(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]) x1 int((cx - w / 2) * w_img) y1 int((cy - h / 2) * h_img) x2 int((cx w / 2) * w_img) y2 int((cy h / 2) * h_img) color (0, 255, 0) if cls_id 0 else (0, 0, 255) if cls_id 1 else (255, 0, 0) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, class_names[cls_id], (x1, max(0, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) return img sample draw_yolo_labels(images/IMG_00001.jpg, labels/IMG_00001.txt, [cattle, sheep, flock]) cv2.imwrite(sample_check.jpg, sample)逻辑说明这代码把每个归一化坐标乘以图像宽高还原成像素框用不同颜色区分三个类别并写上类别名输出成 sample_check.jpg 后人工核对。对航拍小目标我更建议把目标区域局部放大23倍再输出否则十几个像素的小框在原图上小得像噪点人眼根本没法对照。另外坐标还原后明显超出图像边界的框一定要回到原xml检查是标注错误还是转换bug——小目标区域的边界框标注原始数据里常有画歪或画漏的情况遇到了就在这一步修掉而不是带病训练。4. 用YOLO训练小目标牛羊检测imgsz、anchors与损失函数的参数配置格式和划分都准备好后就进入训练环节。YOLO版本众多常见做法用Ultralytics YOLOv5或YOLOv8来跑这份数据集。航拍小目标场景里真正决定结果的不是v几而是imgsz、anchors和loss曲线这三个点。4.1 用Ultralytics YOLO训练3类数据集的起步命令最简命令yolo detect train datasmart_pasture.yaml modelyolov8s.pt epochs120 imgsz1280 batch8 device0逻辑说明data指向前一步生成的data.yamlmodelyolov8s.pt是通用预训练权重imgsz1280是关键参数。航拍小目标场景下640的默认输入分辨率往往不够1021张图里如果有大量目标长宽低于10个像素1280或1536才能让这些小框在特征图上留下足够响应。batch8在16G显存左右能跑如果爆显存就降batch或把imgsz降到960。不同显卡、不同版本的YOLO在显存占用上差别挺大先用小batch试跑一个epoch看显存占用再往上加比较稳妥。训练起来以后盯loss曲线比盯mAP更可靠。YOLO的box loss如果前50个epoch还在明显下降说明模型拟合得吃力要查原图分辨率如果loss快速收敛到平但val mAP一直上不去多数是标注噪声拖后腿回到第3章的抽查脚本把val里loss最高的图框出来看。4.2 imgsz与anchors小目标检测的两个必调参数imgsz在YOLOv8里既影响训练缩放也影响推理通常训练和推理保持一致否则模型习惯的尺度变了会导致精度掉点。对远距离小目标最常见设置是 imgsz1280 或1536代价是训练时间几乎翻倍。先统计一下数据集的真实目标尺寸分布再决定分辨率更可靠import glob import numpy as np all_wh [] for label_file in glob.glob(labels/*.txt): with open(label_file) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue _, _, _, w, h parts all_wh.append([float(w), float(h)]) arr np.array(all_wh) print(median w,h:, np.median(arr, axis0)) print(p95 w,h:, np.percentile(arr, 95, axis0)) print(boxes below 16px at 1280:, (np.max(arr, axis1) 16 / 1280).mean())逻辑说明这段脚本把所有归一化宽高汇总用中位数、95分位数和16像素占比来判断小目标程度。如果p95的宽高都小于0.025对应1280输入下32像素说明绝大多数目标很小imgsz至少得拉到1280如果p95接近0.05那imgsz960也够。这比拍脑袋定分辨率可靠也让后续调参有依据。anchors方面YOLOv5的autoanchor会自动根据数据计算锚框YOLOv8用预定义锚点和自适应分配策略。在小目标数据集上更值得检查的是特征图下采样是否过大。输入1280经32倍下采样后最小特征图是40x40一个实际只有16像素的牛羊目标在最深层特征图上只占半个网格几乎没法检出。解决办法第一是提高输入分辨率第二是看模型是否提供P2小目标检测层第三是用Tiling切片推理。4.3 类别不平衡与yolo损失函数在牛羊场景里的取舍航拍牛羊数据集里牛和羊的实例数往往不成比例。牛在草原上散得开、边界清晰、标注数量多羊群密密麻麻单羊标注少但目标小得多。YOLO的分类损失默认用BCEWithLogitsLoss类别不平衡时小类别的梯度被大类淹没羊的AP会明显低于牛。常见处理方式是在yaml里调整类别损失权重或cls_pw参数# YOLOv5 风格的类权重写法让第二类sheep的损失权重更高 cls_pw [1.0, 2.5, 1.2]逻辑说明cls_pw是示例写法实际YOLOv5里通过配置文件传入per-class loss weightYOLOv8的Ultralytics版本调权重不如v5直接常见做法是在数据增强或采样上补——用更长训练周期配合Mosaic增强让模型看到更多羊小目标或者对含羊的图做复制粘贴增强。正例少的小类在loss里的占比必须人为拉起来否则训练看不到它。box损失也是小目标场景的重点。两个框的IoU值在小目标下天生偏低因为IoU是交集除以并集小框少1像素就掉一大截所以mAP_50:95在小目标场景下会很难看只看mAP_50又容易自欺欺人。建议两个指标都记mAP_50:95作为小目标真实表现基线mAP_50作为工程可交付底线。后续有精力可以试试NWD归一化Wasserstein距离之类以分布距离替代IoU的改进损失我在个别重叠小羊群的实验里见过明显mAP提升代价是收敛慢些适合后期调优阶段不适合从零起跑就上头。5. 智慧牧场小目标训练避坑漏检、误检、标注错误的5条排查记录从VOC转格式到YOLO训练跑通坑基本集中在建模过程中。我按实际见效顺序列5条每条都按现象、原因、解决展开。5.1 训练完mAP挺高到真实航拍视频里漏掉远处小目标现象验证集mAP_50能到0.85可无人机一飞300米外的牛框全丢了。原因验证集里没有足够远距离小目标或训练时imgsz640把小目标细节直接抹掉了。mAP高不代表模型理解小目标它可能只是在数据分布的中心区域表现好。解决把推理输入分辨率提到和训练一致的1280跑一遍第3章的尺寸分布脚本确认训练集覆盖了远距离目标范围没有覆盖就做切片推理把原始航拍大图切成无重叠块每块放大到训练分辨率再检测最后合并边界框。5.2 把深色土包、石头误检成牛羊现象远处深色石块被认出“牛”阴影被认出“羊”置信度还不低。原因小目标本身可用的纹理信息极少模型学到的是“草原上的深色斑点”而不是牛羊轮廓。解决一是把置信度阈值从默认0.25提高到0.40.5误检率明显下降二是补负样本找一批不含牛羊的纯草原俯视图放进训练集留空标注文件让模型学会“没有框的区域要抑制”三是在验证时按高度分段看误检都落在哪个距离范围如果在最远段误检率突然上升宁可降低检出距离换误检率。5.3 密集羊群中的相邻羊被NMS合并成一个框现象几只羊靠在一起时检测结果只出一个框边界还骑跨在两只羊之间。原因默认NMS IoU阈值0.45偏低相邻羊框重叠度高后处理把本该保留的第二框删了。解决推理时把NMS iou改成0.6或0.65常见做法是在YOLO的推理配置或后处理参数里调。如果改完出现同一目标重复框再把置信度阈值抬高一点。训练阶段也可以用更强的Mosaic增强制造目标拥挤让模型对靠得近的同类更鲁棒。5.4 训练loss稳定下降验证mAP震荡剧烈现象每轮val mAP忽高忽低最佳权重和倒数第10轮权重结果差出一大截。原因1021张图本身不大按8:1:1划分后验证集只有约100张里面多一架次飞行的图就会出现分布波动本质还是数据集小、划分不稳。解决用固定seed严格划分并按航线或拍摄架次分组而不是按文件名简单切片有条件就做3折交叉验证把三组val mAP取均值作为模型真实水平。对外汇报结果时说明划分方式省得后面自己对不上。5.5 目标太小任何参数都救不回来现象牛羊在1280输入下也只有不到8个像素模型再怎么调也漏检一大半。原因噪点和目标尺寸已经低于模型特征表示能力的下限单阶段检测器在极小目标上的下采样损失无法用训练技巧完全弥补。解决分两步放宽。第一步提升采集尺度的约束无人机降低飞行高度或换更长焦镜头让目标在画面里至少占1520像素——这是数据采集就决定的上限不是算法能圆场的。第二步仍想靠模型硬扛就上Tiling切片把原图切成512x512或640x640的块放大后单独推理再拼接结果目标相对尺寸直接翻倍。代价是推理时间随块数线性增加最后再用TensorRT加速收尾。6. 用相机高度和远距切片验证模型先看mAP再看能飞多高mAP只能说明“在这份数据分布上表现如何”真正决定能不能上智慧牧场的是模型在实际航拍高度上还能不能稳定检出。我习惯在训练完后做一次固定航高验证选一条有草原、有散牛、有成群羊的航线无人机分别悬停到50米、80米、120米高度各采集几十帧喂给模型统计每个高度下的Recall和每帧平均误检数。然后把结果画成“高度—召回率”曲线曲线在80米还平到120米断崖那你业务最远也就按80米限飞如果曲线一直缓才有资格继续谈远距离目标。这比单看mAP数字更有说服力也方便向产品提“这块不低于多少米才真正可用”的明确指标。如果受条件限制飞不了现场退一步做法是用数据集里的远距离切片验证把1021张图里目标平均尺寸最小的一批图单独组成mini测验集跑一遍mAP_50记录小目标类别的AP再用切片推理跑同一个测验集对比两个AP差。差得越大说明原始分辨率不足以支撑小目标推理阶段就按切片方案上。这个习惯我养了很久它能在不假现场的情况下告诉你模型到底够不够远。最后说一条踩出来的经验调参顺序不要乱先查imgsz再查anchors最后才动loss和NMS每次都只改一个变量记录验证结果别把一批参数同时改了再回头看mAP那样问题在哪根本说不清。1021张的数据集不大多跑几轮实验成本可控但每一次实验的变量控制才是最终能不能给牧场用上的关键。希望帮到你。本文还有配套的精品资源点击获取
返回列表