简介:这是一份面向计算机视觉目标检测任务的苍蝇检测数据集,共532张图片并配有完整标注,适合训练苍蝇等小型害虫检测模型,也可作为目标检测入门练习数据。数据集同时提供Pascal VOC与YOLO两种主流标注格式,无需格式转换即可直接接入常见检测框架;标注由labelImg手工完成,图片经爬取与去重后筛选,矩形框定位准确,整体质量有保障。压缩包共1598个文件,包含532张jpg原图、532个xml标注文件与534个txt文件,其中txt为YOLO格式标签文件,整体大小仅16.67MB,便于快速下载与部署。全部标注合计689个flies实例框,单一类别、规则统一,适合复现批量标注流程或验证数据增强方法。目前已吸引242人学习浏览,对于需要高质量小目标检测数据的研究者是一份即取即用的理想资源。
1. 苍蝇检测数据集VOC+YOLO格式530张.zip:一张小样本数据集的全部身家
做卫生防疫、养殖场监测或者食品加工质检的人,迟早会撞上一个尴尬:模型要用,数据没有。通用目标检测数据集里找不到苍蝇这种小目标,真要去拍、去标,成本又高得劝退。所以我看到「苍蝇检测数据集VOC+YOLO格式530张.zip」这个压缩包时,第一反应是它能不能直接用来训,而不是要不要下载——530张,说多不多,说少也够把YOLO流程跑通,关键是它把VOC和YOLO两种标注都给了,省掉了最枯燥的格式转换。这篇就把这份数据集的用法、参数和坑从头捋一遍,新手能跟着跑出第一个模型,熟手可以直接跳到第4章的排查清单。
2. 数据集结构与格式转换:VOC和YOLO双格式到底怎么用
2.1 解压后的目录长什么样
拿到zip,先别急着解压丢进训练脚本。常见做法是先看一眼压缩包里的目录组织,因为这决定了后面data.yaml里路径怎么写。按这类双格式数据集的惯例,解压后一般有这几个目录:
JPEGImages/:存放所有jpg原图;Annotations/:存放VOC格式的xml标注,每个xml对应一张同名jpg;labels/(有些叫YOLOLabels/):存放YOLO格式的txt标注;ImageSets/Main/:存放train.txt、val.txt这类划分文件,也可能没有,那就需要自己切分。
这里有个容易踩的坑:VOC的xml里记录的是绝对路径还是相对路径。很多公开数据集在xml的<path>字段里写的是打包者自己电脑上的路径,比如C:\Users\xxx\Desktop\...,直接复制到Linux服务器上训练,OpenCV读图时就会报找不到文件。我的习惯是拿到数据先跑一条命令,把xml里的路径字段全部改成相对路径,省得后面排查半天。
cd /path/to/dataset grep -l "<path>" Annotations/*.xml | xargs sed -i 's|<path>.*</path>|<path>JPEGImages/</path>|g'grep -l列出所有含<path>标签的xml文件,sed -i把路径替换成相对形式。因为YOLO训练时实际是按data.yaml里的train/val路径去拼接图片路径,xml里的路径只影响VOC格式验证脚本的展现,统一成相对路径最干净。注意如果xml里没有<path>字段,这条命令不会误伤任何文件。
2.2 VOC到YOLO的标注转换逻辑与脚本
VOC格式用<bndbox>存坐标,单位是像素绝对值;YOLO格式用归一化坐标,单位是0到1之间的相对值,顺序是class x_center y_center width height。转换公式很简单:
x_center = (xmin + xmax) / 2 / W y_center = (ymin + ymax) / 2 / H width = (xmax - xmin) / W height = (ymax - ymin) / H
但这个公式里有三个隐藏坑。第一,W和H必须从xml的<size>标签读取,不能直接用图片的shape,因为某些标注工具会把标注尺寸和图片实际尺寸做成不同的值(多见于旋转标注转矩形标注时)。第二,YOLO格式的宽高必须是缩放后的相对值,很多新手写脚本时忘了除以W或H,导致训练时目标框全都在图片边缘。第三,txt文件名必须和图片名完全一致,包括后缀前的部分,比如IMG_001.xml对应IMG_001.txt,yolov5是直接按图片名去拼接找txt的,不一致就会当成无标注图片跳过。
转换脚本我用下面这个,已经跑过几次,稳定不翻车:
import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_file, out_dir, class_list): tree = ET.parse(xml_file) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) txt_name = os.path.basename(xml_file).replace('.xml', '.txt') txt_path = os.path.join(out_dir, txt_name) with open(txt_path, 'w') as f: for obj in root.findall('object'): name = obj.find('name').text if name not in class_list: continue class_id = class_list.index(name) bnd = obj.find('bndbox') xmin = float(bnd.find('xmin').text) ymin = float(bnd.find('ymin').text) xmax = float(bnd.find('xmax').text) ymax = float(bnd.find('ymax').text) # 防止标注工具留下的越界坐标 xmin = max(0, xmin); ymin = max(0, ymin) xmax = min(img_w, xmax); ymax = min(img_h, ymax) x_center = (xmin + xmax) / 2 / img_w y_center = (ymin + ymax) / 2 / img_h width = (xmax - xmin) / img_w height = (ymax - ymin) / img_h f.write(f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}\n") # 请按你的实际标签顺序填写 CLASSES = ['fly'] for xml_file in os.listdir('Annotations'): if xml_file.endswith('.xml'): voc_to_yolo(os.path.join('Annotations', xml_file), 'labels', CLASSES)代码逐个解析xml,把bndbox里的像素坐标读出来,经过边界裁剪后做归一化。特别说明xmax = min(img_w, xmax)这一步:很多标注工具在物体贴边时会标出比图片尺寸更大的坐标值,不裁剪算出来的中心点会超出图片范围,训练时yolov5的utils/datasets.py会拒绝这类标注,表现为loss一直不下降。CLASSES列表的顺序就是你训练时的类别顺序,必须和最终data.yaml里的names完全一致,否则同一张图会被当成另一种物体。
2.3 数据划分:train/val/test怎么切
530张图对于目标检测来说是名副其实的小样本,划分策略比大数据集讲究得多。如果压缩包自带了ImageSets/Main的划分文件,直接用,但建议先看一下它是不是随机划分——很多打包者是从文件夹顺序里按前70%切分的,场景分布完全偏掉。我的做法是重新做一次划分,尤其注意把记录相同场景、连续帧的图片放进同一个集合,否则val里的图片和train里的图片几乎是同一帧的不同角度,验证指标虚高,真实场景全现原形。
import os, random, shutil random.seed(42) img_dir = 'JPEGImages' train_ratio, val_ratio = 0.7, 0.2 imgs = [f for f in os.listdir(img_dir) if f.endswith('.jpg')] random.shuffle(imgs) train_num = int(len(imgs) * train_ratio) val_num = int(len(imgs) * val_ratio) train_imgs = imgs[:train_num] val_imgs = imgs[train_num:train_num + val_num] test_imgs = imgs[train_num + val_num:] for split, split_imgs in [('train', train_imgs), ('val', val_imgs), ('test', test_imgs)]: with open(f'{split}.txt', 'w') as f: for img in split_imgs: f.write(os.path.join(img_dir, img) + '\n')random.shuffle(imgs)前设置了固定随机种子,这样才能保证每次划分结果一致——不然调试时同样的超参数,今天loss曲线和明天完全对不上。划分后拿到的是train.txt、val.txt、test.txt三个清单文件,yolov5训练时data.yaml里的train和val参数可以直接指向这三个txt路径。
这里还有一个新手容易懵的点:YOLO格式的数据集和VOC格式的划分方式不太一样。VOC时代用ImageSets/Main/trainval.txt来切,YOLO训练更常用的是图片清单txt。如果你用的是mmdetection或者detectron2那套,它们又偏好COCO的ann_file结构。但默认就是yolov5/yolov8,按照上面脚本生成txt清单是最省事的路径。
3. 用数据跑通YOLO训练:从零到第一个验收模型
3.1 训练前的文件准备与目录规范
yolov5或者yolov8对目录结构有硬性要求,它的datasets加载逻辑是按固定格式去找图片和标签文件的。默认情况下,images和labels需要在同一个根目录下且目录名严格一致,否则会在Creating dataset...阶段直接报错。常见的标准结构是:
dataset/ ├── images/ │ ├── train/ (或直接放全部图片,用txt清单划分) │ └── val/ ├── labels/ │ ├── train/ │ └── val/ ├── train.txt ├── val.txt └── data.yaml如果压缩包解压后是JPEGImages和labels这种不对应结构,你需要在训练前把它们改成上述规范。我一般会写一个很小的重排脚本:
mkdir -p dataset/images dataset/labels cp JPEGImages/*.jpg dataset/images/ cp labels/*.txt dataset/labels/ # 把前面生成的train.txt、val.txt按清单把图片和标签分到对应子目录 while read line; do cp "$line" dataset/images/train/ cp "${line%.jpg}.txt" dataset/labels/train/ done < train.txt这个脚本如果train.txt里的路径本来就带着JPEGImages/前缀,basename后能正确找到图片。但有个细节要注意:生成的train.txt行尾不要有空格或者换行符残留,read line会连空白一起读进去,导致cp找不到文件。Windows上编辑过的txt经常带\r\n,在Linux上跑会报No such file or directory但文件名看着又没错,这个玄学问题能让你排查半小时。
data.yaml的内容按下面这样写就足够了:
train: dataset/images/train val: dataset/images/val nc: 1 names: ['fly']如果各类别数量严重不均衡,names里的顺序要和labels里txt的第一列数字对齐,这个顺序在转换标注时就已经固定了,中途改顺序不会自动帮你重写txt,只会让模型把所有框都标记成另一个类。
3.2 训练参数怎么定:img、batch、epochs、hyp
530张的小样本,超参数和大数据集完全是两套逻辑。先说img,也就是训练分辨率。苍蝇是典型的小目标,整张图里可能只占几十个像素,直接img=640会让目标区域缩小成一个点,特征基本丢光。我建议跑一个两阶段策略:先用img=640跑40个epoch观察loss是否收敛,然后切到img=1280继续微调。img不是越大越好,它直接决定batch size——显存有限时,1280分辨率下的batch只能开很小,梯度噪声会变大。
python train.py --data data.yaml --weights yolov5s.pt --img 640 --batch 16 --epochs 60 --device 0yolov5s.pt是官方预训练权重,目标检测领域里有个共识叫迁移学习命中第一——你的苍蝇数据集和COCO差得再远,用预训练权重初始化也比从头训练收敛快得多。如果只有530张图,从头训练几乎注定过拟合,训练集loss能降到0.02,验证集mAP半天上不去,这就是典型的把参数背下来了。
batch=16是一个比较稳妥的起点。显存不够时先降batch而不是降img,原因在于小目标对分辨率更敏感,640降到416可能把苍蝇彻底抹平;batch从16降到8,只是梯度震荡大一点,多跑20个epoch能补回来。epochs在小样本上不能照搬大数据的300轮,我一般用--patience 20让它在验证集指标连续20轮不涨时自动停。
3.3 损失函数在小样本上的表现:为什么loss不降不是幻觉
很多人在yolov5训练日志里盯着box_loss、obj_loss、cls_loss三个数字,看到某一行box_loss突然从0.05跳到0.08就慌了。这不是模型坏了,yolov5的总loss是三个loss的加权和:box_loss用的是CIoU损失,负责回归框的位置和宽高;obj_loss是置信度损失,区分当前位置是否有目标;cls_loss是分类损失,判断目标类别。在小样本上,box_loss降得最快,因为苍蝇的框形单一;cls_loss只有一类物体,基本在0.01附近震荡;最需要盯的是obj_loss,它降不下来说明模型在区分背景和目标上没学会。
还需要注意yolo的loss里天然存在的正负样本不平衡问题:一张640的图会被分成上万个网格,但只有几十个网格真正负责预测目标。小样本数据集会放大这个问题——如果苍蝇框又小又密集,几个相邻网格都在预测同一个目标,yolov5的loss设计会通过obj层的IoU阈值把它们区分开,但训练日志里的obj_loss可能一直偏高。这种时候不要急着调loss权重,先去查标注框是不是有重叠、有没有漏标。小样本场景里,一个漏标框比一百张图片的缺失更伤模型。
3.4 验证与推理:用一条命令看到检测结果
训练结束后的验证环节,在yolov5里不需要单独写脚本。用下面这条命令可以一次完成验证和可视化:
python val.py --data data.yaml --weights runs/train/exp/weights/best.pt --img 640 --task val --save-txt --save-conf--save-txt会把预测结果存成txt,--save-conf会把置信度一并写入,这两个参数对后面做结果分析很有用。val结束后,你会得到一个confusion_matrix.png,这个图能直接看出模型在哪类目标上犯傻。这里有个反复出现的现象:混淆矩阵的行和列总和对不上,这不是bug——yolov5的混淆矩阵里有background这一列,所有被模型预测为背景的空网格都算进这一列,人眼看到数字对不上正常,去看Precision、Recall和mAP@0.5这三个指标就行。
python detect.py --weights runs/train/exp/weights/best.pt --source test.jpg --conf 0.4 --img 640--img 640必须和训练时的--img一致,否则检测头的感受野会对不上。--conf 0.4是置信度阈值,小目标本身得分偏低,经验上0.25到0.3更合理,但实际部署时可以按业务需求调,没有万能值。
4. 苍蝇检测的标注质量排查与常见问题:肉眼不可见的坑
4.1 漏标与误标:小样本数据集的隐形杀手
这种垂直领域数据集,标注质量参差不齐几乎是必然。苍蝇目标小、数量多、容易重叠,标注员漏标是常态。漏标对训练的影响远大于误标——一个没框出来的苍蝇,在训练时会被当成背景,模型学到的是「这个位置的苍蝇是背景」,以后推理到这里就直接忽略。误标顶多是框大一点小一点,漏标是直接把正样本变成负样本。
排查方法是把训练集里每张图的标注框叠加回图片上看一眼,不需要每张都看,随机抽30张就行。用下面这段代码可以快速生成带框预览,自带的调试图功能在训练时看一次就够:
import cv2 import os img_dir = 'dataset/images/train' label_dir = 'dataset/labels/train' for img_file in os.listdir(img_dir)[:30]: img = cv2.imread(os.path.join(img_dir, img_file)) h, w = img.shape[:2] txt_file = os.path.join(label_dir, img_file.replace('.jpg', '.txt')) if not os.path.exists(txt_file): continue with open(txt_file) as f: for line in f: cls, x_center, y_center, bw, bh = map(float, line.split()) x1 = int((x_center - bw/2) * w) y1 = int((y_center - bh/2) * h) x2 = int((x_center + bw/2) * w) y2 = int((y_center + bh/2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 1) cv2.imwrite(os.path.join('check_vis', img_file), img)因为cv2.imread读取的通道顺序是BGR,直接画框没问题。如果发现大量标注框把苍蝇的翅膀或腿切掉一半,可以接受;如果发现一张图里有十几只苍蝇只框了三五个,这个数据集必须重新清理才能用,否则模型精确率再高,召回到实际场景直接崩。
4.2 zip伪加密:解压报错不一定是文件坏了
如果你在Windows上解压这个zip时弹出了密码框,而你确认作者没给过密码,这个zip很可能是伪加密。zip伪加密是一种把加密标志位改掉的文件结构,很多打包工具为了防盗版或者防止直接解压复制,会在zip的目录区设置general purpose bit flag的第0位为1,但文件内容本身没有加密。表现是WinRAR能正常打开但解压时报密码错误,7-Zip则在资源管理器里直接弹出密码输入框。
遇到这种文件,不要急着找密码或者删除重下,先在命令行里用7z试一下:
7z l archive.zip # 正常会列出文件,伪加密zip在"Encrypted"列显示"+"号 7z x archive.zip -y第一条7z l输出中如果看到文件列表后面有+标注,又是伪加密,直接用7z x不带密码就能解压出来。有些伪加密zip连7z都会弹错,可以试试Python的zipfile库:
import zipfile try: with zipfile.ZipFile('archive.zip') as zf: zf.extractall() except RuntimeError as e: if 'password' in str(e).lower(): print('当前为伪加密zip, 尝试绕过密码...') # 强制读出来, 伪加密文件无需密码也能读取内容 with zipfile.ZipFile('archive.zip') as zf: for info in zf.infolist(): # 伪加密时, 直接把加密标志位清零后重新写入 info.flag_bits &= ~0x1 zf.extractall()这里的原理是flag_bits里的0x1是加密标记位,如果数据目录里没有加密头且压缩方式是store或deflate,清除标志位后就能正常读出文件。如果你解压后文件夹能打开但图片打不开,十有八九是下载过程中zip文件截断了,重新下载一遍比研究修复容易得多。
4.3 训练中loss值NaN:全是标注坐标的锅
yolov5训练时loss突然变成nan,第一反应不是去调学习率,而是去查xml里有没有非法坐标。最常见的现象是xml里出现了<xmin>Infinity</xmin>或<xmax>-1</xmax>这种脏数据,VOC格式本身允许负值存在,但转换脚本把它clamp到0后,宽度变成0,模型对宽度为0的框做IoU计算就直接nan。另一个容易忽略的场景是图片本身有EXIF旋转信息——手机拍摄的图片在读取时被OpenCV自动旋转了90度,但xml里的坐标没有跟着转,导致框和物体对不上,训练时不断产出异常梯度。转成YOLO格式后发现很多框的坐标值超过1,基本就是这类问题。
排查方法很简单:
# 找出labels里坐标异常的行 awk '$2<0 || $3<0 || $4<0 || $5<0 || $2>1 || $3>1 || $4>1 || $5>1 {print FILENAME": "$0}' dataset/labels/*.txt如果输出为空,再检查w和h是否为0:
find dataset/labels -name "*.txt" -exec awk '{if ($4==0 || $5==0) print FILENAME}' {} \;把这两个命令跑完没有报错,才能放心地进入训练环节。另外,batch_size过小也可能模拟出loss=nan的假象——在V100上开batch=32,learning rate按默认的0.01跑可能正常;到了本地单卡batch=4,同样的学习率把梯度放大到溢出,loss就nan了。建议把--lr0从0.01降到0.002,小batch配小学习率在小样本上是安全组合。
4.4 不同训练框架的兼容性差异
如果你不只跑yolov5,还想用mmrotate或者DOTA系模型测一下苍蝇检测,注意VOC和YOLO格式并不能直接喂给所有框架。mmrotate用的是COCO格式的JSON标注,DOTA格式则是左上角、右上角、右下角、左下角四个点的坐标序列。转换时,VOC的矩形框转成旋转框的四边形,需要额外把旋转角度算出来,这个步骤有很多现成脚本但都比较脆弱,因为苍蝇的姿态各异,有时候芯片标注的矩形框其实是斜着的。
如果是从零做,我建议先锁定一个框架,比如yolov5或者yolov8,跑通再说。中途换框架,光格式转换就能消耗掉大半天,而且530张小样本在两个框架间的mAP差异往往小于标注噪声本身。想要提升精度,把精力花在补标注和质量排查上,比换框架性价比高得多。
5. 进阶玩法:小样本苍蝇检测的结果验证与部署扩展
模型训出来了,接下来最该做的是验证它在你自己的场景里到底能不能用。yolov5的val.py会生成P_curve.png和R_curve.png两条曲线,但真正部署时还要多想一步——苍蝇检测往往是非单帧场景,视频流里一只苍蝇可能横跨几十帧,单帧检测结果的漏检和误检会累积。我一般会在detect.py输出后加一个追踪逻辑,用简单的IoU框匹配把连续帧里靠得很近的检测框合并成一条轨迹,能有效过滤掉闪烁的假阳性框。这一步在yolov5自带的detect.py里没有直接实现,需要自己写几行追踪代码,或者直接上ByteTrack这类轻量追踪器,把每帧的检测框按置信度排序再匹配,效果立竿见影。
另一个值得试的方向是把数据集的530张当种子数据,做半自动标注扩展。具体做法是用训练好的模型去预测新的无标注视频帧,抽出置信度高于0.8的检测框作为伪标签,然后人工只修低置信度的部分。这个策略能把标注成本压到原来的三分之一,但对模型初始质量有要求——如果现在mAP@0.5还没到0.7,伪标签里的错框太多,扩出来的数据只会放大错误。
最后提一个经常被忽略的验证方法:把模型导出成ONNX再转TensorRT或OpenVINO,用CPU跑一遍看推理耗时。苍蝇检测很多时候要部署在边缘设备上,一张640x640的图在GPU上跑1ms没有意义,在树莓派或Jetson Nano上同样图要跑几百毫秒,这就逼着你考虑把检测区域裁剪到苍蝇经常出现的局部区域,或者把img降到416。这套小样本数据集本身的价值不只是那一套标注,而是把一个完整的目标检测工作流从数据到部署暴露在你面前,530张的规模恰恰让每一环节的问题都清晰可见。
我习惯在每个新数据集上做一件事:先随机抽30%的图手工看一遍标注,再跑一次从头训练。这个习惯帮我避开过不少看起来还行、实际标注稀碎的压缩包。希望这篇能帮你在苍蝇检测这个垂直方向上少走一些弯路,把530张数据用出它的底线和上限。
本文还有配套的精品资源,点击获取