简介:荔枝成熟检测数据集包含579张实拍标注图片,覆盖绿、半红、红三种成熟度类别,共2911个目标框(green 1387、half 892、red 632),适合目标检测入门者、农业AI开发者及算法评测使用。数据采用labelImg人工标注,同时提供Pascal VOC格式xml与YOLO格式txt,可直接导入YOLOv5、MMDetection等主流框架。整个zip压缩包共1739个文件,包含579张jpg原图、579个xml标注、579个yolo txt以及少量辅助txt,包体约29.5MB,结构简洁,已有297人浏览学习。该数据集可帮助使用者省去自行采集与标注的耗时,专注模型调优和成熟度判别逻辑;类别覆盖从青果到全红的完整过渡,适合用于果园产量预估、采摘机器人视觉等场景,整体标注框数充足,为训练稳定的检测模型提供了基础数据支持。
1. 荔枝成熟检测数据集:579张图为什么够你跑通一个农业检测项目
579张图的荔枝成熟检测数据集,绿、红、半红三个类别,看起来规模不大,但如果你只想验证目标检测在农业场景下的可行性,这个体量刚好卡在“能跑通”和“不会浪费算力”之间。做农业视觉的人都知道,果园现场的数据不是缺,是乱——光照不均、枝叶遮挡严重、同一串荔枝上三种成熟度并存,这种数据拿通用目标检测模型直接跑,十有八九会翻车。这套数据集的实用价值在于让你用最短时间跑通“解压→看标注→转格式→训练→验证”这条链路,而不是追求一个虚高的mAP。适合三类人:刚入门目标检测的学生、要做农产品分级demo的工程师、以及想评估成熟度检测项目投入产出比的团队。接下来我会把这些格式细节和训练路径拆开讲。
2. 拆开这份VOC+YOLO格式的数据集:目录结构、标注文件与3类标签
拿到zip包第一件事不是解压就训练,而是先摸清目录结构。目标检测数据集压缩包解压后,要么是VOC风格,要么是YOLO风格,这份同时给了两种,省去了格式互转的第一步。但两类格式的存放约定差异很大,搞混了目录,训练脚本连图片都找不到,数据加载阶段就会报错。
2.1 数据集目录长什么样?先搞清VOC和YOLO两套标注的存放方式
VOC格式的约定来自PASCAL VOC竞赛,目录通常长这样(解压后一般是一个VOCdevkit或VOC2007命名的顶层目录):
VOC2007/ ├── Annotations/ # 每张图片对应的xml标注文件 │ ├── 000001.xml │ └── 000002.xml ├── JPEGImages/ # 原始图片 │ ├── 000001.jpg │ └── 000002.jpg └── ImageSets/ └── Main/ # 按任务划分的图片名清单 ├── train.txt ├── val.txt └── ...这里ImageSets/Main下的txt文件每行存一个不带扩展名的文件名,比如“000001”而不是“000001.jpg”。训练时按清单去JPEGImages里找图、去Annotations里找标注,所以清单里的文件名写错,即使图片和xml都存在,也会被跳过。很多新手把jpg文件放到了Annotations目录、xml放到JPEGImages,或者把文件名带扩展名写进ImageSets,都会导致训练时数据加载异常。
YOLO格式的目录约定和VOC不一样,它把图片和标注分成images和labels两个顶层目录:
dataset/ ├── images/ │ ├── train/ # 训练图片 │ └── val/ # 验证图片 └── labels/ ├── train/ # 训练标注txt └── val/ # 验证标注txtYOLO的txt标注文件名和图片文件名必须完全一致(不含扩展名),一张图片对应一个txt。txt里每行描述一个目标,格式是“class_id x_center y_center width height”,坐标全部归一化到0到1之间。注意class_id是从0开始的整数,而VOC的xml里存的是类别名字符串,这一步映射关系是后续很多坑的源头。
拿到双格式数据集,我一般会先跑两条命令确认图片和标注数量对得上:
find JPEGImages -name "*.jpg" | wc -l find Annotations -name "*.xml" | wc -l如果图片数和xml数不一致,说明标注不完整,得先把缺标注的图片挑出来,不能直接训练。这个动作在579张图的小数据集上几秒钟跑完,但能省掉后面排查数据加载异常的大量时间。如果两套格式都存在,我还习惯额外对比一下VOC里的ImageSets/Main/train.txt和YOLO里images/train目录的图片数,确认两套划分是否一致。
提示:VOC的xml文件名和图片文件名一致是PASCAL的硬性约定。解压后先随机抽查三个xml,确认object节点里没有空name或空bndbox,再做格式转换。
2.2 XML和TXT标注互转:一个脚本看懂两类格式的字段映射
如果数据集提供的是VOC标注,而要喂给YOLO系列模型训练,就必须把xml转成txt。转换的核心是把xml里的绝对像素坐标换算成归一化的相对坐标。这里给出一段我常用的批量转换脚本:
import xml.etree.ElementTree as ET import os class_names = ['green', 'red', 'half_red'] # 顺序必须和data.yaml里的names一致 def convert_voc_to_yolo(xml_path, out_txt_path): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in class_names: continue class_id = class_names.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) # 四个坐标分别clip到图片边界内,防止转换后越界 xmin = max(0, min(xmin, img_w)) xmax = max(0, min(xmax, img_w)) ymin = max(0, min(ymin, img_h)) ymax = max(0, min(ymax, img_h)) # 中心点坐标和宽高都归一化到[0,1] x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}") with open(out_txt_path, 'w') as f: f.write('\n'.join(lines)) xml_folder = 'Annotations' out_folder = 'labels' os.makedirs(out_folder, exist_ok=True) for xml_file in os.listdir(xml_folder): if xml_file.endswith('.xml'): txt_name = xml_file.replace('.xml', '.txt') convert_voc_to_yolo(os.path.join(xml_folder, xml_file), os.path.join(out_folder, txt_name))这段脚本的核心逻辑是:先读xml里的图片宽高,遍历object节点取类别名和bndbox四个像素坐标,再把坐标换算成中心点加宽高的归一化表示。我特别加了一步clip操作,原因是标注工具偶尔会留下越界的框,YOLO训练时对超出0到1区间的坐标会告警,大量越界框还会拉低前几个epoch的稳定性。class_names的顺序必须和最终训练用的data.yaml里names的顺序一致。比如你在yaml里把names写成['green', 'red', 'half_red'],转换脚本里也必须是这个顺序,否则类别索引错位,模型会把绿荔枝当成红荔枝来学。我在实际转多类别数据集时踩过一次这种坑,教训是转换前先看一眼yaml和脚本里的names,对不上就先改脚本再跑。
还有一个经常被忽视的小问题,就是xml文件的编码。有些标注工具导出的是UTF-8带BOM格式,ElementTree默认解析一般没问题,但如果你在Windows上用记事本改过xml,有时候会出现“xml.parsers.expat.ExpatError: not well-formed”这类错误。遇到这种报错,先确认文件内容里没有特殊字符,或者用Python以errors='replace'方式重读,再不行就换个文本编辑器重新保存成无BOM的UTF-8。
2.3 标签分布与类别平衡:绿、红、半红三类在实际果园里的比例
579张图、3类目标,看起来不多,但每张图里可能有多个目标,先统计每个类别的标注框总数,判断数据是否平衡:
import glob import os txt_files = glob.glob('labels/train/*.txt') counts = {0: 0, 1: 0, 2: 0} for txt in txt_files: with open(txt) as f: for line in f: line = line.strip() if not line: continue cls_id = int(line.split()[0]) counts[cls_id] += 1 print(counts)如果某个类别只有几十个框,说明样本严重不足,模型大概率学不好这一类。荔枝成熟检测里最典型的是“半红”类别又少又难标注,它介于绿和红之间,标注员的判定标准经常不一致,同一张图不同人标出的类别可能不一样。我的做法是先把判定模糊的样本单独挑出来,和标注源头统一一套标准。比如约定:果实表面红色区域占比超过80%算红,低于20%算绿,其余算半红。标准定下来之后,再看哪一类样本不够,考虑补充数据还是做重采样。
类别不平衡时,不要急着上focal loss或调loss权重。579张图的小数据集,先试两类操作:一是对少数类做数据增强,比如针对半红样本做小幅亮度扰动;二是把少数类图片在训练集里按一定倍率重复采样进去,让模型每个epoch多看到几次。这样比直接改损失函数更可控,效果也更直观。还有一种思路是用类别权重把少数类的loss放大,但在小数据集上容易放大噪声标注的影响,我一般放在最后考虑。
在这个体量下,我还建议做一个图片级别而不是框级别的分析。比如某一类图片特别多,但每张图只有1个框;另一类图片很少,但每张图有10个框,统计框数并不完全等价于样本丰富度。实操里我同时统计图片数和框数两个指标,如果少数类别的图片数少于50张,即使框数看着还行,也要小心模型学不稳。
3. 用YOLOv8在本地跑通荔枝成熟检测:从数据集划分到训练启动
数据集确认无误后,下一步就是让检测模型跑起来。我优先用YOLOv8系来做小数据集验证,原因是ultralytics的流程足够简单,数据加载、增强、训练、验证都封装好了,579张图从启动到收敛只需要一个命令。YOLOv5、YOLOv6、YOLO11也都可以,做法大同小异,核心是先把数据和配置文件准备对。
3.1 准备环境与数据集目录:yaml文件怎么写才不会报错
YOLOv8运行时要求你准备一个data.yaml描述数据集路径和类别名称。写错一个路径或类别名,训练命令跑不到第二个epoch就会崩。下面是我针对这份荔枝数据集的yaml模板:
path: /home/user/lychee_dataset # 数据集根目录,绝对路径或相对路径 train: images/train # 训练图片目录,相对于path的路径 val: images/val # 验证图片目录 nc: 3 # 类别总数:绿、红、半红 names: 0: green 1: red 2: half_redpath字段在YOLOv8里是所有相对路径的基准,train和val都写相对于path的子路径。如果你习惯用绝对路径,可以把train和val写成完整的绝对路径,同时把path留空或用“.”,但这样在换机器时容易踩路径不一致的坑,我通常统一用相对路径加根目录path。names的顺序和转换脚本里的class_names顺序要严格对应。另外,yaml里不要有多余的空格,尤其是“0: green”这种键值对,缩进错误会让YAML解析报错,报错信息有时候还不容易看懂。
在第一次跑训练之前,我习惯先确认环境和设备状态:
python -c "import torch, ultralytics; print(torch.__version__, ultralytics.__version__)" nvidia-smi如果输出正常再执行训练命令。如果nvidia-smi没有输出或torch.cuda.is_available()为False,说明CUDA没配对,纯CPU训练579张图虽然也能跑,但会慢很多,建议先把驱动和PyTorch版本对齐。很多新手一上来就报错,其实不是代码问题,而是显卡驱动太老或者PyTorch的CUDA版本匹配不上。
3.2 划分训练集/验证集:随机划分与按场景分组划分的差别
数据划分直接决定验证集是否可信。随机划分适合图片来源同质、场景差异小的数据;但果园数据往往来自不同果园、不同树、不同时间,随机划分可能把同一棵树的连续帧同时分进训练集和验证集,让验证分数虚高。如果图片文件名里带着拍摄位置或时间信息,推荐按场景做分组划分。下面先给一个随机划分脚本:
import random import os import shutil images = [f for f in os.listdir('images') if f.endswith('.jpg')] random.seed(42) random.shuffle(images) train_imgs = images[:int(len(images) * 0.8)] val_imgs = images[int(len(images) * 0.8):] os.makedirs('images/train', exist_ok=True) os.makedirs('images/val', exist_ok=True) os.makedirs('labels/train', exist_ok=True) os.makedirs('labels/val', exist_ok=True) for img in train_imgs: shutil.move(f'images/{img}', 'images/train/') shutil.move(f'labels/{img.replace(".jpg", ".txt")}', 'labels/train/') for img in val_imgs: shutil.move(f'images/{img}', 'images/val/') shutil.move(f'labels/{img.replace(".jpg", ".txt")}', 'labels/val/')脚本把图片按8:2随机分成训练集和验证集,同时把对应的txt标注同步移动到labels目录下。注意seed=42固定住随机数,保证可复现。如果你的图片名包含果园或果树编号,比如farm01_tree02_001.jpg这样的命名,建议先按farm分组再划分,而不是全局随机。因为同一棵树的图片背景非常相似,一旦同时出现在训练集和验证集,模型很可能通过背景就能认出目标,验证集mAP看着很高,一到新果园就掉一半。
按组划分的思路是把图片按farm编号分组,再把farm列表随机打乱后按比例分配,代码逻辑比随机划分稍长,但能换来更可信的验证指标。如果你的数据里没有这类分组信息,那就只能用随机划分,同时接受mAP可能虚高的风险,并在后续做视频抽帧测试时重点验证场景泛化能力。
3.3 训练命令与参数:imgsz、epochs、batch对不同显存怎么配
训练命令在Ultralytics里收敛得很简洁,我一般用下面的模板:
yolo train data=data.yaml model=yolov8n.pt imgsz=640 epochs=100 batch=16 device=0关键参数逐个说:
- model:yolov8n.pt是nano版,权重文件小、训练快,最适合只有579张图的小数据集。如果你的显存足够且想要更高精度,可以换yolov8s.pt,但训练时间会明显上涨,而且小数据集上从nano换到s收益有限,因为瓶颈在数据量不在模型容量。
- imgsz:训练输入尺寸,默认640。荔枝果实在整张图里可能只占几十个像素,如果漏检严重,试试imgsz=960,但显存占用会显著上升。
- epochs:小数据集上100轮起步,但不要无脑跑300轮。数据少模型容易过拟合,训练到后期验证指标可能先升后降,建议打开早停。
- batch:显存不够就调小batch,比如8或4,同时调低workers。batch越大梯度越稳定,但小数据集上batch过大反而容易过拟合,我用16居多。
不同显存下我一般按这个表来配参数:
| 参数 | 6GB显存 | 8GB显存 | 12GB以上 |
|---|---|---|---|
| imgsz | 640 | 960 | 1280 |
| batch | 8 | 8-16 | 16-32 |
| model | yolov8n | yolov8n/s | yolov8s/m |
训练开始后,前10个epoch盯着loss曲线看。如果loss直线不降或者干脆发散,立刻停下排查,别让它跑完100轮才发现白跑。训练过程中每轮会输出Box P、R、mAP50、mAP50-95等指标。小数据集上mAP50-95和mAP50的差距通常比较大,比如mAP50有0.85、mAP50-95只有0.5,这很正常,因为IoU阈值提高后小目标的定位偏差会被放大。如果你的mAP50-95明显偏低,优先考虑提高标注框的贴合度,而不是加训练轮数。
训练结束后用runs/detect/train/weights/best.pt做验证推理,别用last.pt,小数据集上last.pt在后期往往已经过拟合。
4. 训练前先做数据检查:标注可视化与3类常见标注错误
不管数据集是买来的、下载的还是自己标的,先可视化标注再进训练,这一步能省的时间远比你想的多。579张图不多,全量可视化也就十几分钟,但你能直观看到三类成熟度的框标得准不准、有没有把树叶当成荔枝、有没有漏标背景里的果实。
4.1 用OpenCV画框检查:一眼看出错标、漏标、越界
写个脚本把标注画回原图,人工扫一遍最有说服力:
import cv2 img_path = 'JPEGImages/000001.jpg' txt_path = 'labels/000001.txt' img = cv2.imread(img_path) h, w = img.shape[:2] with open(txt_path) as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue cls_id, xc, yc, bw, bh = [float(p) if i > 0 else int(p) for i, p in enumerate(parts)] 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 = (0, 255, 0) if cls_id == 0 else ((0, 0, 255) if cls_id == 1 else (0, 165, 255)) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText(img, str(cls_id), (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) cv2.imwrite('check_000001.jpg', img)这段脚本把归一化坐标还原成像素坐标,在原图上画框和类别id。可视化检查重点看四类问题:一是框是否贴着果实边缘,还是把整簇枝叶包进去;二是一个果实是否被画了两个重叠框,这是标注重复;三是框的边界是否超出图片范围,这是转换时坐标没clip;四是类别id和框内果实的实际成熟度是否对得上,尤其是把半红标成红这类错标。只要发现其中任何一类问题,就先修数据再训练。宁可花半天修标注,也不要让模型用错误标注学100轮,后者纯属浪费算力。
抽检时不要只看训练集,验证集也要看一遍。后续的mAP评估只看验证集,如果验证集本身有错标,模型的正确预测反而会被当成错误框拉低mAP。这种假性掉点最让人抓狂,因为你会误以为是模型能力问题,实际是评估集脏了。
4.2 检测小目标的技巧:荔枝在树冠场景下究竟算不算小目标
目标检测社区里有个粗略约定:目标边长小于32像素算小目标。荔枝挂在树上,远距离拍摄时果实直径经常只有20到30像素,所以这份数据很可能包含不少小目标样本。小目标检测最常见的问题是漏检和定位偏差,解决方向有三个:
第一是用带P2层的高分辨率输出模型,YOLOv8的P2模型在特征图中增加了浅层,对小目标更友好;第二是训练时把imgsz加到960或1280,相当于把目标在输入端放大;第三是切图推理,把2048x1536的大图切成若干640x640的分块,分别推理再合并结果。
分辨目标尺寸的另一个办法是跑一条统计脚本,把所有标注框的像素面积分布打出来。如果超过一半的框边长小于32像素,就可以确定这是小目标场景。在小目标为主的数据集上,输出分辨率不足往往比模型容量不足更致命,所以先加imgsz再考虑换模型是更稳的顺序。
我用这份小数据集的经验是:P2模型因为浅层参数多,在579张图上更容易过拟合,不建议一上来就用。先跑imgsz=640的nano模型,如果漏检率高再考虑imgsz=960或者切图推理。切图推理不需要重新训练,只影响推理阶段,实施成本最低,适合快速验证到底是不是小目标引起的漏检。
4.3 数据增强要不要做?针对成熟度检测的增强策略
YOLOv8默认开启了马赛克、随机仿射、HSV扰动等增强,小数据集上这些增强能明显提升泛化能力。但荔枝成熟度检测有一个特殊点:颜色是核心判据。半红这个类别完全靠绿和红的比例区分,如果HSV增强里的色调和饱和度变化幅度太大,会把红荔枝扰成橙荔枝、绿荔枝扰成黄荔枝,反而模糊类别边界。
我建议做两件事:一是把HSV增强的强度调低,尤其是hsv_h和hsv_s,保留hsv_v(亮度变化)为主;二是自己加一组亮度对比度增强,模拟果园里不同时段的光照。在Ultralytics的配置里通常这样改:
hsv_h: 0.01 hsv_s: 0.3 hsv_v: 0.5 scale: 0.5 fliplr: 0.5 mosaic: 0.8这里hsv_h调得很低是为了防止色相偏移打乱红/绿判定,hsv_v保留较大幅度来模拟顺光逆光变化。mosaic保留0.8,但如果你发现训练早期loss降不下去,可以把mosaic降到0.5,因为四图拼接会让小数据集上的样本分布变化太剧烈,有些图片里的荔枝甚至被截掉大半。
补充一个实践细节:增强策略可以按类别来调。半红类别因为样本少,可以给它单独做一点裁剪或缩放增强,而绿和红类别保持默认。虽然YOLOv8的增强配置是全局的,但你可以通过喂入数据时复制少数类样本的方式,变相提高少数类在增强中的出现概率。
注意:数据增强不是越多越好。579张图的数据集,增强太猛会让模型看到大量失真样本,经常表现为训练loss降到一定程度就卡住不动,验证指标也跟着停滞。这时候把增强强度降一档,往往立竿见影。
5. 荔枝成熟检测避坑指南:5个让人翻车的真实问题
下面的问题我在农业检测项目里不止一次遇到过,每一条都按“现象→原因→解决”来写,基本覆盖了小数据集训练成熟度模型时最容易翻车的地方。
5.1 现象:训练前10轮loss不降或者剧烈震荡
新数据集上经常遇到这种情况:loss一直挂在5到7之间,epoch之间的波动肉眼可见,模型看起来完全没在学。原因通常有两个:一是学习率初始值太大,优化器在损失面里跳来跳去;二是labels目录里混入了空标注文件,也就是某张图没有对应txt,或者txt内容为空,模型在这些样本上拿到的是负样本噪声。
解决方法是先统计空txt文件数量,把空文件对应的图片从训练集里剔除,然后用小学习率重新跑,比如在命令里加lr0=0.001。YOLOv8默认lr0是0.01,对579张图这种小规模数据集其实偏高,直接降到0.001能解决大部分不收敛的问题。排查时先看日志里前10个epoch的box_loss和cls_loss曲线,如果cls_loss完全不动而box_loss在降,问题多半在标注或类别定义;如果两个loss都在震荡,优先调低学习率。
5.2 现象:类别标签错乱,绿色果实被模型判成红色
训练过程一切正常,loss也降了,但验证结果里明显看到模型把绿荔枝成片地判成红荔枝。最常见的原因就是转换脚本里的class_names顺序和data.yaml里names的顺序不一致。比如转换时用的是['red', 'green', 'half_red'],yaml里写的是['green', 'red', 'half_red'],那么所有标注的类别索引全部错位。
解决办法其实不复杂但很考验细心程度:统一转换脚本、yaml、训练代码三处的类别顺序,转换后随机抽5个txt文件,打开看txt里的class_id,再打开对应的xml看name字段,人工核对每一行是否对应。为了降低这类风险,我建议在目录里固定一份class_names.txt,把类别顺序写死,所有脚本都从这份文件读类别,而不是各自维护一份列表。这个习惯看起来笨,但在2到3个类别的小项目里确实能救命。
5.3 现象:半红样本几乎全部被漏检或误判
半红类别的样本数量本来就少,模型容易把它当成过渡状态,既不往红上靠也不往绿上靠,表现为验证集上半红类别的precision和recall都明显低于另外两类。原因有两个层面:一是样本数量不足,模型见过的半红实例太少;二是标注标准不统一,标注员A认为果面30%变红就算半红,标注员B认为必须一半以上变红才算,边界样本的标注互相矛盾,模型很难学到稳定特征。
我的建议是先把所有半红样本拉出来重新过一遍原图,按统一标准重新标注边界样本。如果时间不允许,至少把分歧严重的样本剔出训练集,保留标注置信度高的样本,再用重复采样补充半红类别的出现频次。如果真的不想重标,还有一种退路:把半红和目标颜色最接近的类别合并,比如把半红并入红,把3类检测降成2类检测。这种操作适合业务只关心红不红、不需要精确区分半红的场景。
5.4 现象:验证集mAP很高,到新场景里几乎失效
这是小数据集过拟合的典型表现:验证集mAP能到0.8以上,但把模型拿到另一片果园或另一天拍的视频上,检测框数量锐减,置信度全部掉到0.3以下。原因通常是训练图和验证图来自同一场景,模型的底层特征更多依赖背景纹理而不是荔枝本身的颜色和形状,可以直接理解成把背景记住了。
解决方法是重新划分数据集或新增外部数据,确保验证集里的果园、树和拍摄角度尽可能和训练集没有重叠。另一个验证技巧是挑几段不同时间段拍摄的视频做抽帧测试,静态图片集测不出场景泛化能力。快速验证过拟合的方法是用模型跑训练集自己,如果训练集mAP接近1.0而验证集mAP明显低于训练集,过拟合基本坐实。这种时候增加数据多样性比增加模型复杂度更有用,比如补充不同果园、不同光照、不同拍摄距离的图片。
5.5 现象:训练能跑,但画出来的框位置偏左上或右下
这类问题的典型场景是模型能检出目标,但可视化时框和目标有系统性偏移。原因几乎都出在坐标归一化环节:转换脚本里如果用了全局平均的图片宽高,而不是每张xml里size字段的宽度高度,那么分辨率不一致时框就会整体偏移。
解决方式是检查转换脚本,确认读的是当前xml的size值,并加一轮全量可视化抽检。如果用我前面给的脚本,计算x_center和y_center时都用的是img_w和img_h,这个bug天然规避掉了。但如果是手工改过或半自动生成的标注,必须复查这一步。再补充一个检查点:如果txt坐标都在0到1之间,但画框位置整体偏移,注意看是不是归一化时x用了width、y用了height,但坐标计算时乘反了。比如把x_center乘了图像高度,框就会在竖直方向偏。这类手误很常见,可视化抽检是最好的防线。
6. 让模型更贴近果园现场:两类数据重采样与一个验证技巧
当你把上面这套流程跑完、模型在验证集上mAP也能看之后,别急着收工。579张图训练出来的模型,离真正到果园里扛住各种光照变化还有距离。这里分享一个我常用的收尾技巧:先把全部样本按场景维度重新组织,然后做两类重采样。
第一类是困难样本重采样。把模型预测置信度低于0.5的样本全部挑出来,人工确认是不是错标或特殊光照样本。这份数据里最容易出问题的往往是逆光下拍的半红荔枝,颜色失真严重,模型置信度天然就低。把这些样本复制三份放回训练集,再做一次微调,比直接加训练轮数有效得多,因为模型看到的是它最不擅长的样本,收敛方向更明确。
第二类是验证集场景分桶。不要只保留一个验证集,而要把图片按光照条件分桶:顺光、逆光、阴影、雨天共四桶,分别跑mAP并记录。这样你会发现验证集总分也许不错,但逆光桶的mAP可能只有0.3。找到短板桶之后,把对应样本挑出来做亮度增强微调,效果提升肉眼可见。如果某些桶里样本太少,至少做一个正检率抽样检查,记录漏检框个数,为后续补数据提供依据。
验证技巧上,用视频抽帧代替静态图集。拿手机在果园拍一段30秒视频,抽帧50张,加进验证集跑一遍。静态数据集容易让人高估模型现场表现,视频抽帧会暴露目标在不同角度、不同遮挡程度下的漏检。我有一个习惯:最终交付前一定会做一次视频抽帧测试,因为真的在果园里翻过车——当时静态验证mAP有0.86,到了现场视频里漏掉了一半小目标。这套数据集本身质量不错,VOC和YOLO双格式也省事,但数据量就摆在那里,不要指望一次训练出完美模型,多做两轮样本筛选和重采样,比换更大的模型更划算。希望这些经验能帮你少走弯路,也祝你的荔枝成熟检测项目顺利落地。
本文还有配套的精品资源,点击获取