简介这份目标检测数据集面向计算机视觉学习者与有标注需求的算法工程师针对柑橘害虫检测场景提供按YOLOV5目录规范整理、无需额外处理即可投入训练的数据集。数据包含苍蝇、木虱两个类别图像为10004000分辨率的大尺寸RGB图片训练集含240张图片与240个txt标注文件验证集含60张图片与60个txt标注文件总容量约260MB。压缩包内共603个文件主要包括301个txt标签与300张jpeg原图另附1个Python可视化脚本和1张示例图片便于快速查看标注效果。该可视化脚本可随机读取图片并自动绘制边界框结果保存至当前目录无需修改即可直接运行能帮助使用者快速核对标注质量与类别分布。目前已有401人学习下载。1. 目标检测数据集不是“图片框”那么简单柑橘害虫的 2 类别也能卡住你果园里挂上诱虫板之后你很快会发现真正的瓶颈不是相机也不是模型而是数据。想用 YOLOv5 做柑橘害虫检测最理想的情况是手里已经有一份“目标检测数据集(YOLOV5目录格式)柑橘害虫检测2类别包含训练集、验证集”目录结构、标签编号、训练验证划分都规规矩矩。但现实里拿到手的往往是散落的照片、从标注工具导出的 XML 或 JSON、以及一团乱的类别编号。标题里两个容易被小看的点一个是“YOLOV5 目录格式”另一个是“2 类别”。两个类别看起来简单但只要你把类别顺序写反、验证集和训练集串了图模型照样翻车。这篇文章就是围绕这套数据集格式把目录怎么搭、标签怎么写、训练前要查什么、哪些坑最容易踩一次讲透。2. YOLOV5 目录格式的最小约定images/labels 同级、同名映射、类别数对齐2.1 同级目录不是约定俗成是 dataloader 的路径替换逻辑第一次接触 YOLO 系列的人容易犯一个错把图片放在images/train把标签放在labels/train但两者的父目录不在同一个根下结果训练时 log 一直报 “label not found”。这不是因为你命名错了而是因为 YOLOV5 的取标签逻辑写得非常“死”。看一下框架内部它基本等价于下面这段路径替换import os def img2label_paths(img_paths): # 将图片路径中的 /images/ 片段替换成 /labels/只替换第一个匹配位置 sa f{os.sep}images{os.sep} sb f{os.sep}labels{os.sep} return [p.replace(sa, sb, 1) for p in img_paths] # 示例 img /data/citrus/images/train/orchard_a_0001.jpg label img2label_paths([img])[0] print(label) # /data/citrus/labels/train/orchard_a_0001.txt所以在组织目标检测数据集(YOLOV5目录格式)时最稳的做法是保持一个统一根目录下面只有images和labels两个兄弟目录。images/train对应labels/trainimages/val对应labels/val图片和标签的主文件名完全一致。目录树像这样citrus_yolo/ ├── images/ │ ├── train/ │ │ ├── orchard_a_0001.jpg │ │ └── orchard_a_0002.jpg │ └── val/ │ ├── orchard_b_0001.jpg │ └── orchard_b_0002.jpg ├── labels/ │ ├── train/ │ │ ├── orchard_a_0001.txt │ │ └── orchard_a_0002.txt │ └── val/ │ ├── orchard_b_0001.txt │ └── orchard_b_0002.txt └── citrus.yaml每次整理数据我会先写一行命令核对两边文件数量是否一致find images/train -name *.jpg | wc -l和find labels/train -name *.txt | wc -l。数量对不上基本就是标注漏了或者图片后缀不一致比如.JPG、.jpeg混用。这种问题越早发现越省事等训练跑起来再排查心态会崩。还有一个小细节img2label_paths替换的是os.sep images os.sep也就是说你的路径里必须出现独立的images目录段。如果你把目录叫citrus_imgs而不是images那路径替换就失效了模型会拿着不存在的标签路径去读文件。血泪经验不要发明新目录名YOLOV5 就认images和labels。2.2 train/val 划分是元数据操作不是复制照片很多人拿到数据集之后第一反应是把 80% 的图片复制到images/train20% 复制到images/val再为每张图生成同名 txt。这种做法没毛病但它掩盖了一个关键问题划分训练集和验证集本质上是在切“信息边界”不是在切文件。如果完全是随机划分同一次果园巡拍里连拍的 10 张照片会同时出现在训练集和验证集里。模型在训练时已经见过同一棵树、同一个角度、同一光照条件下的目标验证时 mAP 自然好看一到新果园立刻现原形。所以常见做法是让训练集和验证集在空间或时间维度上分开。比如这批柑橘害虫数据是按果园区块采集的我就会按区块划分A、B、C 区做训练D 区做验证。这样验证集才真正代表“没见过的场景”。如果你手里的数据集只有目录结构没有区块信息那至少要按文件名前缀分组再做组级别划分而不是单张图片随机切。2.3 dataset.yaml 里 names 顺序就是标签编号的真相YOLO 的 txt 标签每一行由class_id x_center y_center width height组成其中坐标是归一化到 0~1 的浮点数而class_id是整数。这个整数本身没有意义它对应的类别名字完全由dataset.yaml里的names列表决定。下面是一个标准的配置文件# citrus.yaml train: /data/citrus/images/train val: /data/citrus/images/val nc: 2 names: 0: citrus_psyllid 1: citrus_leafminer注意这里有两个关键点。第一train和val指向的是images目录而不是labels目录标签目录会被自动替换出来。第二names列表的顺序就是类别编号的真相。如果某个 txt 里第一行是0 0.5 0.5 0.1 0.1那就代表图片中心有一个柑橘木虱如果写的是1 0.5 0.5 0.1 0.1就是潜叶蛾。这个顺序一旦和标注工具导出时的类别表对不上整个训练就变成一个黑匣子错误。所以每拿到一份数据我都会先跑一个小统计脚本看每个类别在训练集和验证集中的实例数量分布确认没有全部挤在一个编号上。这一步花不了两分钟但能拦下后面十个小时的无效训练。整理目录时最容易踩的另一个坑是 pycharm 或 Windows 复制文件时自动合并同名文件。如果同一张图片在 train 和 val 里各出现一次表面看不出来但验证集指标会虚高。检查方法也很简单对 train 和 val 的图片文件名集合取交集如果交集不为空就说明数据泄漏了。3. 把柑橘害虫标注整理成 YOLOV5 目录格式转换脚本与参数说明3.1 第一步统一中间格式把标注工具导出转成标准数组手里如果已经有整理好的 YOLO 格式数据集那第 2 章的目录树可以直接用。但大多数时候你拿到的标注是从 LabelImg、Labelme 或自研标注平台里导出的格式可能是 PASCAL VOC XML、COCO JSON 或者一张 CSV 表。我一般不会为每种格式各写一套转换逻辑而是先统一成“一张图对应多行标注”的中间格式再做坐标换算。下面这段代码假设你已经把原始标注导出成了一个 CSV每一行代表图中一个目标字段包括image_path, x, y, w, h, category_name坐标是原始像素值且为左上角加宽高的形式。这一步把 CSV 读进来并构建一个按图片分组的数据结构import csv from collections import defaultdict def load_annotations(csv_path): 读取标注 CSV按图片路径分组方便后续逐图写标签 期望字段: image_path, x, y, w, h, category_name grouped defaultdict(list) with open(csv_path, r, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: grouped[row[image_path]].append({ x: float(row[x]), y: float(row[y]), w: float(row[w]), h: float(row[h]), category: row[category_name].strip() }) return grouped这段代码做的事很简单但它的价值在于把“来源格式”和“目标格式”解耦。后续不管是 XML 还是 JSON只要转成这个中间结构再走后面的坐标换算逻辑就行。defaultdict(list)避免了手动判断键是否存在的麻烦。注意 CSV 里的表头一定要确认不同标注工具导出的字段名可能叫xmin、ymin、xmax、ymax也可能是cx、cy、width、height先统一成左上加宽高。3.2 第二步写 COCO 转 YOLO 的坐标换算函数YOLO 标签的核心规则说三遍都不嫌多坐标是中心点宽高是相对图片宽高的比例所有数值都在 0~1 之间。而 CSV 里如果存的是x, y左上角坐标就必须先加一半宽高得到中心点。下面这个换算函数是整套转换的核心同时处理了边界裁剪防止标注框超出图片边缘时写出大于 1 的坐标import os from PIL import Image def coco_to_yolo(size, box): 将左上角坐标 宽高的框转成 YOLO 格式 size: (width, height) 原始图片尺寸 box: (x, y, w, h) 左上角坐标和宽高 返回值: (x_center, y_center, w, h) 归一化坐标 dw 1.0 / size[0] dh 1.0 / size[1] x, y, w, h box x_center (x w / 2.0) * dw y_center (y h / 2.0) * dh w * dw h * dh # 防御性裁剪避免浮点误差导致越界 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) w min(max(w, 0.0), 1.0) h min(max(h, 0.0), 1.0) return x_center, y_center, w, h def write_yolo_label(grouped, img_root, label_root, category_map): grouped: load_annotations 返回的字典 category_map: {psyllid: 0, leafminer: 1} for img_rel, boxes in grouped.items(): img_path os.path.join(img_root, img_rel) image Image.open(img_path) size image.size # (width, height) txt_name os.path.splitext(os.path.basename(img_rel))[0] .txt out_path os.path.join(label_root, txt_name) lines [] for b in boxes: cls_id category_map[b[category]] xc, yc, w, h coco_to_yolo(size, (b[x], b[y], b[w], b[h])) lines.append(f{cls_id} {xc:.8f} {yc:.8f} {w:.8f} {h:.8f}) with open(out_path, w, encodingutf-8) as f: f.write(\n.join(lines))这里有两个容易忽略的参数细节。第一是格式化精度很多教程只保留六位小数但在 3000x4000 的高清果园照片上一个框宽只有 20 像素换算成归一化坐标大约是 0.005六位小数勉强够用但如果框再小一些就会产生明显量化误差。我习惯用%.8f多两位小数几乎不占存储却能省掉很多“为什么小目标检不出来”的排查时间。第二是写入模式用w而不是a避免重复运行脚本时同一个 txt 被追加内容造成训练时标签翻倍。如果你手里的数据不是 CSV也不要慌常见做法是先把 XML 或 JSON 解析成这个grouped结构后续所有逻辑完全复用。LabelImg 导出的 VOC XML 多一步xml.etree.ElementTree解析Labelme 导出的 JSON 多一步遍历shapes字段其余坐标换算代码一模一样。3.3 第三步按地块划分训练集和验证集并生成 dataset.yaml标注文件写好后下一个动作是划分 train/val。前面说过随机划分会带来数据泄漏所以我在这个步骤直接按图片文件名前缀分组。假设文件名格式是orchard_a_20240315_001.jpg前缀orchard_a代表地块加日期那划分逻辑就按前缀来import random from collections import defaultdict def split_by_prefix(img_list, train_ratio0.8, seed42): 按文件名前缀分组后划分 train/val 前缀相同的图片同地块同批次不会被同时分到两边 random.seed(seed) groups defaultdict(list) for img in img_list: prefix os.path.basename(img).split(_)[0] groups[prefix].append(img) group_names list(groups.keys()) random.shuffle(group_names) split_idx int(len(group_names) * train_ratio) train_groups set(group_names[:split_idx]) train_imgs [] val_imgs [] for prefix, imgs in groups.items(): if prefix in train_groups: train_imgs.extend(imgs) else: val_imgs.extend(imgs) return train_imgs, val_imgs这个脚本的逻辑核心是“组级别划分”。random.seed(42)是为了让划分结果可复现不然每次跑出来的训练集都不一样后面对比实验就不公平。split(_)[0]取的是文件名第一个下划线前的字段如果命名规则变了这里也要跟着调整。之后就是把train_imgs和val_imgs分别拷贝到images/train和images/val或者用软链接节省磁盘空间。最后写citrus.yaml时train和val路径用绝对路径最省心因为训练时的当前工作目录经常变动相对路径很容易闹出“训练集为空”的幺蛾子。4. 目录正确后训练前还有三关编号一致性、输入分辨率、小目标先验4.1 类别编号对齐names 顺序变了整个训练就换了一类数据集整理完标签文件里的class_id是 0 和 1但这不代表训练时你会得到正确的结果。dataset.yaml里的names顺序如果和生成标签时用的category_map不一致模型会悄无声息地学到“错误的语义”。我见过最典型的情况标注工具里的类别列表是 [leafminer,psyllid]转换脚本里category_map按这个顺序赋了 0 和 1但写 yaml 时想当然写成了[psyllid, leafminer]。训练过程一切正常loss 正常下降验证 mAP 也不差直到部署到现场才发现模型把两种害虫完全搞反了。要拦下这个问题靠眼睛是不够的。训练前我会直接对一个已知图片的标签文件做可视化检查把 YOLO 格式的框画到图上import cv2 def draw_yolo_box(img_path, txt_path, names): img cv2.imread(img_path) h, w img.shape[:2] with open(txt_path, r) as f: for line in f: parts line.split() cls_id int(parts[0]) xc, yc, bw, bh map(float, 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) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, names[cls_id], (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) cv2.imwrite(check_vis.jpg, img)如果画出图来柑橘木虱那个框上写着leafminer立刻就能发现编号写反了。这种可视化检查在单类别数据集上容易偷懒但两个类别以上一定要做等训练完了再去猜到底是数据集问题还是模型问题代价就太大了。4.2 输入图像尺寸640 还是 1280直接决定小目标是否可见柑橘木虱的成虫体型只有几毫米在 3000x4000 的照片里可能只占 20x20 像素。YOLOV5 默认推理尺寸是 640x640意思是训练时会把整张图缩放到 640 宽。原来 20 像素的害虫缩放后只剩 4 像素左右人类都很难分辨更别说让模型学。这是“小目标检测”里最经典的困境。我通常会在整理数据时做一次标签尺寸统计计算所有标注框归一化宽高的中位数然后换算成 640 尺度下的等效像素import os import numpy as np def label_box_stats(label_dir): widths, heights [], [] for txt in os.listdir(label_dir): with open(os.path.join(label_dir, txt), r) as f: for line in f: parts line.split() widths.append(float(parts[3])) heights.append(float(parts[4])) arr np.array(widths) * 640 print(640尺度下平均目标宽度(px):, int(arr.mean()))如果算出来的平均目标宽度小于 10 像素直接拿原图做 640 训练基本是浪费算力。这时有两个选择第一训练时把imgsz提高到 1280代价是显存占用大幅上升一张图占用的显存约为 640 的 4 倍第二对原图做滑窗切图把一张 3000x4000 的大图切成若干 640x640 的 patch切出来的子图直接作为新图片参与训练标签同步裁剪。后一种做法在害虫检测场景里更常见因为它既保留了小目标的原始像素又不至于让训练显存爆炸。4.3 先验框和增强参数小目标数据集的常见改法YOLOV5 默认的 anchor 是按 COCO 数据集统计出来的COCO 里的目标普遍比害虫大。好在 YOLOV5 有 autoanchor 机制训练时会基于你的标签重新聚类 anchor。但如果你在训练日志里看到 autoanchor 输出的最小 anchor 尺寸是 10x13而你的害虫框在 640 尺度下平均只有 5x5这个默认聚类结果就不够用。常见做法是先调整hyp.yaml里的增强参数。mosaic默认是 1.0对小目标来说不一定是好事因为 mosaic 会把四张图拼在一起再缩放目标进一步缩小。我一般会先把mosaic调到 0.5 左右同时把random_perspective的缩放幅度调小避免目标被随机裁出画面。另外copy_paste增强对小目标有一定帮助前提是显存足够。不管怎么调原则只有一个不要盲信默认超参数。模型训练前先在验证集上跑一小段 baseline再把增强参数调一版对比哪个高用哪个。5. 避坑整理 YOLOV5 数据集时最容易翻车的 5 个细节5.1 训练启动后提示 found 0 images现象train.py启动后日志里出现train: Scanning labels后直接提示找不到图片训练一步都没跑。原因dataset.yaml里的train路径写错或者指向了labels目录而不是images目录路径写的是相对路径而当前工作目录不在数据集根目录下。解决把dataset.yaml里的路径改成绝对路径并确认train字段指向images/train。排查命令ls /data/citrus/images/train | head先确保这个路径在终端里直接能看到文件。5.2 验证 mAP 很高部署到果林却什么都检不到现象训练集验证集 mAP 达到 0.9 以上测试一段现场视频几乎全丢。原因验证集和训练集来自同一批图片的随机划分模型实际上在“背书”而不是“推理”另一个高频原因是验证集只包含特定角度、特定光照的图片。解决严格按照时间或地块维度划分验证集。如果数据集已经按images/train、images/val组织好但来源信息丢失了就用第 3.3 节的前缀分组逻辑重新划分形成一个新的验证集合再跑一次验证通常 mAP 会掉 5 到 15 个点这才是真实水平。5.3 两个类别编号写反模型还在正常收敛现象训练全程 loss 下降正常验证曲线也很漂亮但推理时把柑橘木虱全部识别成潜叶蛾。原因category_map和dataset.yaml的names顺序不一致模型把标签“信以为真”地学了。解决从训练集里随机抽一张图用手头的可视化脚本把标注框和类别名画出来人眼核对。如果已经训完才发现也不用太懊恼把names顺序换回来重新训练的概率不高因为特征本身已经学到了更快的办法是推理时做一次类别映射而不是浪费算力重训。不过这只在“两类互换、无其他错误”时才成立生产环境老老实实重新确认标签。5.4 验证集全是“大果特写”训练集全是复杂背景现象验证集 mAP 高但模型回到复杂背景的图片上召回率特别低。原因整理数据时人下意识会挑选“清楚、目标大、好标注”的图片进验证集这让验证集成了一个简单模式完全不能代表真实场景。解决验证集一定要从所有样本里按固定比例抽取而不是人工挑选。我一般会保证验证集至少有 100 张图每个类别不低于 40 个实例否则 mAP 的置信区间太宽一次验证结果的波动会误导你做出错误调参决策。5.5 框太小被 mosaic 和 random_perspective 直接丢出图外现象训练数十轮后小目标的 recall 仍然接近 0但大目标表现很好。原因mosaic 会把图片缩小 2 倍再拼图几十像素的小框缩小后只有 10 像素甚至小于增强过程中随机裁剪的窗口目标直接消失。另一个相关原因是标签坐标没做边界保护裁剪增强把中心的框裁了一半训练时模型看到的全是残缺目标。解决在hyp.yaml里把mosaic降到 0.5 以下并检查随机仿射变换的参数不要同时开大幅度的缩放和旋转。同时写一个标签统计脚本把归一化宽高小于 0.01 的框单独打印出来确认是真实存在的极小目标还是标注错误。如果是真实目标优先做滑窗切图而不是硬扛 640 输入。6. 怎么证明这套数据集目录真的能用训练前的体检与一轮快速验证整理好的数据集不能只靠“目录结构对”就下结论我会在正式训练前做一次体检。顺序固定先查数量再查内容最后跑一轮极短训练。第一步查数量用下面的命令对比图片和标签文件数echo train images: ls images/train/*.jpg | wc -l echo train labels: ls labels/train/*.txt | wc -l echo val images: ls images/val/*.jpg | wc -l echo val labels: ls labels/val/*.txt | wc -l数量对齐后随机挑 3 张训练图做可视化检查框是否贴合目标、是否有明显标注错漏。然后跑一轮 5 个 epoch 的快速验证这一步的目的是确认数据链路是通的而不是追求精度python train.py \ --data citrus.yaml \ --epochs 5 \ --batch-size 8 \ --imgsz 640 \ --project runs/quick_check看训练日志时我重点关注三个地方。第一是all 320这类输出表示 autoanchor 的聚类结果如果最小 anchor 远大于你标签中的常见目标尺寸说明输入分辨率或增强策略需要调整。第二是前 3 个 epoch 的 box_loss 和 obj_loss如果 obj_loss 从 0.1 一路升到 0.3 以上大概率是标签错位模型在努力拟合错误的东西。第三是验证阶段每个类别的 mAP0.5两个类别差距过大时需要检查是不是某个类别的样本数太少或标注框本身不干净。这里有一个我自己常用的进阶做法训练完一轮 baseline 之后把置信度在 0.25 到 0.6 之间的预测框全部截出来按预测类别分目录保存然后人眼快速过一遍。这样能直观看出模型到底是“不会检”还是“检了但置信度低”。如果是后者常见做法是再加一个小型分类模型做二次过滤把害虫和背景的相似块再分一次这在害虫检测场景里往往比盲目堆数据更划算。之后你完全可以把这个流程复用到烟草病虫害或者其他作物数据集上目录格式和检查手段是通用的。我自己的习惯是每次整理完数据集先把 yaml 和标签统计结果截图留存这样后面训练翻车时至少知道数据集本身是体检通过的不会陷入“最后悔没留底稿”的局面。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?