简介:这是一份面向目标检测与农业害虫识别的芒果害虫标注数据集,覆盖Weevil、beetle、grasshopper、mango_hopper等10个害虫类别,整体采用Pascal VOC格式组织,可直接用于YOLO等主流检测框架的训练与评估。压缩包大小约191.04MB,共包含2000个文件,其中1999个为XML标注文件,记录了每张图片中的目标类别、边界框坐标等关键信息,另附1个txt说明文件,方便使用者快速理解目录结构与标注规则;本次发布对应3575张芒果害虫图片的标注,数据量较为充足,文件名采用统一编号,便于批量读取和划分训练、验证及测试集,整体组织清晰。该数据集适合农业智能监测、果园虫情预警、科研实验以及算法课程作业等场景,当前已有224人浏览学习,具备一定参考价值,尤其适合需要现成标注数据、希望节省人工标注时间的研究者、算法工程师或相关专业学生直接取用。无论是快速搭建检测基线,还是开展算法对比实验,都能节省大量前期准备时间。
1. 拿到「芒果害虫检测数据集VOC+YOLO格式3575张10类别」这份压缩包,先别急着训练
第一次拿到「芒果害虫检测数据集VOC+YOLO格式3575张10类别」这份压缩包,多数人的第一反应是解压、改一下 yaml、然后直接把 yolov8 的训练命令甩上去。我在农业视觉项目里的习惯是先花十分钟把数据读干净——3575 张图片、10 个类别,规模不算大,但覆盖果园场景里常见的害虫形态与光照条件;同时提供 VOC 和 YOLO 两种标注,意味着你可以用 XML 做精细检查,用归一化的 txt 直接训模型。这篇文章写给两类人:正在做 yolo 入门学习、想拿真实农业数据跑通“yolov8 训练自己的数据集”全流程的初学者,以及做智慧农业落地、需要评估这份数据能不能撑起一个可上线模型的工程师。前者照步骤抄作业,后者重点看边界和踩坑。
2. 先看货:VOC 与 YOLO 双格式标注结构里藏着哪些约束
2.1 3575 张、10 类别的数据规模,对模型意味着什么
3575 张图摊到 10 个类别上,平均每类不到 360 张。做目标检测的人看到这个数字应该心里有数:它属于典型的中小型数据集,适合拿来验证算法流程、对比训练策略、跑通部署链路,但不要指望它能支撑一个从零训练的大规模检测模型。对农业场景来说,这个量级其实很真实——果园现场能采集到的有效样本有限,人工标注成本又高,能凑出 3500 多张已经是不小的工程。
更关键的是 10 个类别这个设定。按虫种划分时,芒果害虫之间存在大量形态相似的情况,比如蓟马和叶蝉在低分辨率图像里几乎长得一样,模型很容易把两个类别横跳;如果混入了病害类别,则更考验纹理细节的区分能力。所以这份数据做 yolo 入门学习非常合适,因为它足够小、跑一轮训练不会让你等太久,又足够让新手体会到“类别相似导致混淆矩阵不干净”的真实项目感觉。用这份数据练手 yolov8 训练自己的数据集,你能完整经历从数据校验到模型推理的全过程,这是纯合成数据集给不了的体验。
还有一个容易忽略的点:数据集里有没有负样本类。有些农业数据集会在 10 个类别之外单列一个“健康叶片”或者“背景”,用来告诉模型哪些东西不该框。如果类别列表里没有负样本类,训练时模型会把所有看起来像虫的东西都框出来,误检率会明显偏高。拿到类别清单后先确认这一点,比先跑训练更重要。
2.2 双格式组织方式:JPEGImages、Annotations、labels 的对应规则
这类数据集解压后常见的目录结构这样分布,虽然不同发布者组织方式略有差异,但双格式数据集的核心骨架通常是相通的:
dataset/ ├── JPEGImages/ # 原图,jpg 或 png,共 3575 张 ├── Annotations/ # VOC 格式的 XML,一个目标一个 <object> ├── ImageSets/Main/ # 官方划分文件,train.txt / val.txt(部分数据集有) ├── labels/ # YOLO 格式的 txt,一行一个目标 ├── classes.txt # 类别名清单,按行写 └── data.yaml # 给训练框架用的配置文件(可选)这条目录对应关系是整个数据集的第一条生命线:JPEGImages/xxx.jpg必须精确对应Annotations/xxx.xml和labels/xxx.txt,文件名除扩展名外完全一致。任何一级出现多余文件、缺失文件,或者文件名前后有多余空格,都会在训练时表现为“图片数量对不上”或者“读取标签失败”。
拿到压缩包后的第一个动作,先做文件数量核对,命令很简单:
ls JPEGImages/ | sed 's/\.jpg$//' | sort > /tmp/img_names.txt ls Annotations/ | sed 's/\.xml$//' | sort > /tmp/xml_names.txt ls labels/ | sed 's/\.txt$//' | sort > /tmp/txt_names.txt wc -l /tmp/img_names.txt /tmp/xml_names.txt /tmp/txt_names.txt comm -3 /tmp/img_names.txt /tmp/xml_names.txt comm -3 /tmp/img_names.txt /tmp/txt_names.txtcomm -3打印只在其中一个文件里出现的名字,如果没有任何输出,说明图片、XML、txt 三个集合一一对应。这一步看起来啰嗦,但真能拦住至少三成翻车——尤其压缩包是从 Windows 或 macOS 环境打包的,经常混入Thumbs.db、._xxx.jpg这类系统垃圾文件,导致文件数对不上。注意sed 's/\.jpg$//'只处理了.jpg后缀,如果图片是.png或.jpeg结尾,记得把后缀替换改成对应的正则,否则会出现一堆“缺失匹配”的假警报。
2.3 VOC 坐标转 YOLO 坐标的换算公式与检查顺序
VOC 的 XML 标注记录的是目标在图像上的绝对像素坐标,核心字段是xmin、ymin、xmax、ymax和类别名name。YOLO 的 txt 标注则是归一化坐标,五个数字分别是class_id cx cy w h,其中cx、cy是中心点相对图像宽高的比例,w、h是框宽高相对图像宽高的比例,所有值都在 0 到 1 之间。两者换算公式如下:
w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h cx = (xmin + xmax) / 2 / img_w cy = (ymin + ymax) / 2 / img_h这里最容易踩的一个坑是类别编号。VOC 里目标通过name字符串标识,YOLO 里则通过整数编号标识,编号的对应关系由classes.txt的行号决定——第 0 行是编号 0,第 1 行是编号 1,依此类推。如果classes.txt的排序和发布者生成 YOLO 标注时的类别排序不一致,整份标注的编号就会集体漂移。更隐蔽的情况是同一个类别在 XML 里叫“thrips”,在 classes.txt 里写成了“thrip”,转换脚本匹配不到这个字符串,于是这条目标被静默跳过,训练数据无声无息地少了一批框。
检查顺序上,我一般随机抽 3 张图做人工对照:先看 XML 里目标数量,再数对应 txt 里的行数,接着把 XML 坐标代入公式算一遍,确认和 txt 里的数值对得上。这三张图全部通过,才做全量转换。这份数据既然同时给了 VOC 和 YOLO 双格式,大概率已经有一套生成好的 YOLO 标签,你仍然值得做这一步,因为谁也无法保证发布者用的转换脚本没有边界 bug。
3. 把压缩包变成可训练数据:体检、转换、划分三件事
3.1 数据体检:先核对文件数量和文件名,再用 OpenCV 读一遍图片
文件数量核对通过只是第一关,第二步必须用 OpenCV 把每张图真实读一遍。很多数据集发布时图片已经损坏但扩展名还在,或者图片是零字节文件,训练框架读取时直接报错。批量体检脚本不长,但能把“训练到一半突然崩”这个隐性炸弹提前拆除:
import cv2 import glob for path in glob.glob('JPEGImages/*.jpg'): img = cv2.imread(path) if img is None: print(f'[损坏] {path}') continue h, w = img.shape[:2] if h < 32 or w < 32: print(f'[过小] {path} {w}x{h}')这段代码做了两件事:cv2.imread返回None说明图片无法解码,直接标记损坏;能解码但尺寸小于 32×32 的图,后面送入 YOLO 时会被 letterbox 放大,插值出来的像素对训练贡献很小甚至起反作用,提前过滤掉比让模型硬学更明智。注意glob的路径模式要和实际目录结构匹配,如果图片是.png就把后缀改成*.png,也可以用glob.glob('JPEGImages/*.*')一把抓,再通过os.path.splitext判断扩展名。
这一步失败时看什么?如果大量图片报损坏,优先怀疑压缩包传输不完整,重传或者换解压工具;如果只有个别图片损坏,直接把这些文件名记录到一个broken.txt,后续转换脚本和训练配置都避开它。顺便提一句,某些数据集为了控制压缩包体积,会把大图压得特别狠,导致图片尺寸参差不齐,这类问题在使用时也要留意。
3.2 写一个稳妥的 VOC 到 YOLO 转换脚本:带边界截断的版本
如果发布者只给了 VOC 格式,或者你手里的 YOLO 标签生成得不够可靠,自己写转换脚本是最安心的方式。一个带边界截断和坐标过滤的版本如下:
import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, out_dir, class2id): root = ET.parse(xml_path).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.strip() if name not in class2id: continue # 类别不在清单里,跳过而不是报错 cid = class2id[name] box = obj.find('bndbox') xmin = float(box.find('xmin').text) ymin = float(box.find('ymin').text) xmax = float(box.find('xmax').text) ymax = float(box.find('ymax').text) # 边界截断:把越界的像素坐标拉回图像范围内 xmin = max(0.0, min(xmin, img_w)) xmax = max(0.0, min(xmax, img_w)) ymin = max(0.0, min(ymin, img_h)) ymax = max(0.0, min(ymax, img_h)) if xmax - xmin < 2 or ymax - ymin < 2: continue # 太小的框,留着只会变成噪声 cx = (xmin + xmax) / 2 / img_w cy = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f'{cid} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}') out_path = os.path.join(out_dir, os.path.basename(xml_path).replace('.xml', '.txt')) with open(out_path, 'w') as f: f.write('\n'.join(lines))这段脚本有几个值得注意的细节,恰好也是数据转换里最容易出问题的点。第一,root.iter('object')比root.findall('object')更稳,能兼容 XML 里嵌套结构;第二,name必须strip(),否则类别名对不上;第三,框的坐标做了 clip 截断,避免标注越界后生成的归一化宽高大于 1,这种脏数据会让训练时的损失函数产生奇怪的行为;第四,宽高小于 2 像素的框直接丢弃,这类目标经过下采样后基本不可见,硬留着只会让模型学出一堆噪声梯度。
调用这个函数前,class2id的构建方式是从classes.txt逐行读取并建立字符串到编号的映射,行号就是从 0 开始的编号,不要用字典的插入顺序去替代行号。转换完成后,随机挑 5 个 txt 文件人工核对一遍,重点看坐标取值范围是否都在 0 到 1 之间、前几行的类别编号是否和classes.txt一致。
3.3 训练集与验证集划分:比例、随机种子、同源防污染
数据划分看起来是最没技术含量的一步,实际翻车率极高。最经典的错误是随机打乱后直接切分,完全没考虑“同源图片污染”的问题——同一棵芒果树上同一批害虫拍的 30 张照片,被拆成了 20 张训练、10 张验证,模型在训练时已经见过高度相似的画面,验证分数虚高得离谱,一到现场就露馅。
我一般按 8:1:1 切分训练、验证、测试,并且固定随机种子保证可复现:
import random from pathlib import Path random.seed(42) # 固定种子,复现实验 names = [p.stem for p in Path('JPEGImages').glob('*.jpg')] if Path('ImageSets/Main').exists(): # 官方已有划分时,优先用官方的 train = [x.strip() for x in Path('ImageSets/Main/train.txt').read_text().split()] val = [x.strip() for x in Path('ImageSets/Main/val.txt').read_text().split()] else: random.shuffle(names) n = len(names) train = names[:int(n * 0.8)] val = names[int(n * 0.8):int(n * 0.9)] test = names[int(n * 0.9):]这里有一个取舍:优先采用发布者给的ImageSets/Main官方划分。因为发布者划分时通常会照顾到现场拍摄的分组关系,不会把同一场景的照片拆到两个集合里,这一点比你自己随机切更可信。如果官方划分不存在,则自己切分时尽量按照片文件名里的场景前缀分组——比如文件名如果包含拍摄日期或果树编号,把相同前缀的图片全部归入同一集合,再用 sklearn 的GroupShuffleSplit做分组划分,而不是简单random.shuffle。
另外,val和test的区别要有意识。val用于训练过程中的早停和调参,会被模型间接“看到”;test是最终验收,只在模型定稿后跑一次。很多人只切了训练和验证,最后拿验证集分数当上线指标,这在数据量小的时候特别危险。建议从第一天就保留一个不参与任何决策的测试集。
4. 用 YOLOv8 训练这份数据集:最小可跑配置
4.1 data.yaml:类别顺序和路径必须对齐
在 yolov8 里训练自己的数据集,第一件事是写data.yaml。它的核心内容只有几行,但错误往往出在最不起眼的地方:
# data.yaml path: /absolute/path/to/dataset # 数据集根目录,建议用绝对路径 train: images/train # 训练图片目录,相对 path val: images/val # 验证图片目录,相对 path nc: 10 # 类别总数 names: # 类别名列表,顺序就是编号 0: class_a 1: class_b 2: class_c 3: class_d 4: class_e 5: class_f 6: class_g 7: class_h 8: class_i 9: class_jnames里每一行的键值对顺序,直接决定了类别编号的语义。假如labels/xxx.txt里一行写的是3 0.5 0.5 0.2 0.3,那这个目标的类别就是names里编号为 3 的那个名字,不是classes.txt里第 3 行的名字。这一步我没有写死成真实类别,是因为不同发布者对 10 个类别的排列方式可能不同,你要以压缩包内实际的classes.txt为准。目录结构方面,如果数据集原本就是JPEGImages平铺组织,YOLO 训练时你也可以直接指定train: JPEGImages、val: JPEGImages,配合labels: labels的默认约定,框架会自动去找和图片同名的 txt;但更推荐在复制数据集时就顺手排成images/train、images/val、labels/train、labels/val的标准布局,减少后续踩坑。
4.2 训练命令与关键超参数:epochs、imgsz、batch 的边界
环境装好之后,一个能直接从命令行跑的训练命令如下:
yolo detect train \ data=data.yaml \ model=yolov8s.pt \ imgsz=640 \ epochs=100 \ batch=16 \ device=0 \ patience=20 \ cos_lr=Truemodel=yolov8s.pt选的是 small 版本。害虫检测属于小目标密集型任务,用yolov8n这种轻量骨架很容易因为特征提取能力不足导致漏检;反过来也不建议直接上yolov8x,因为这个数据集只有 3575 张图,大模型在这个量级上几乎必然过拟合,收敛速度还慢。imgsz=640是 YOLO 系列训练的常见输入尺寸,但对芒果害虫这种小目标来说,640 可能仍不够。显存允许时优先提到 960 或 1280,代价是训练时间几乎翻倍;显存不够时,先减batch而不是减imgsz,因为小目标检测对分辨率的敏感度远高于对 batch 的敏感度。
batch=16在 16GB 显存显卡上搭配imgsz=640基本能跑动,显存吃紧就降到 8,千万不要硬顶。patience=20表示验证集指标连续 20 个 epoch 不提升就早停,对这个规模的数据集是合理的;cos_lr=True用余弦退火学习率,比默认的线性下降更适合中等规模数据集,让损失函数在训练后期收敛得更平滑。另外,数据增强默认开着的mosaic=1.0对这个数据集有利——它能把四张图拼成一张输入,相当于把小目标出现的密度提高,模型见过的目标上下文更丰富。如果害虫形态是长条形的细长身体,建议顺手把fliplr=0.5调成 0,镜像翻转会让左右不对称的虫体形态学特征出现冲突。
4.3 学会看训练日志:损失函数曲线和混淆矩阵
训练结束之后,在你的输出目录runs/detect/train/下会生成results.png和confusion_matrix.png等图表。新手最容易犯的错是把results.png里的曲线当成广告,看到一路走低就宣布胜利,实际上至少要看懂三件事。
第一是 yolo 损失函数的三条主要曲线:box_loss代表边框回归损失,cls_loss代表分类损失,dfl_loss是分布焦点损失,负责让框的分布更集中。三条曲线在训练集上都应该平滑下降,如果cls_loss下降得很慢或者中途反弹,大概率是类别编号错位或者数据里有冲突标注,回第 5 章排查。第二是验证集上的mAP50和mAP50-95,两者都是越高越好,但mAP50-95对框的精度要求更苛刻,在害虫这种小目标上通常比mAP50低不少。第三是混淆矩阵,它能直接告诉你哪两个类别最容易互相认错,这在后续做难例挖掘时是明确的方向指引。
一个提醒:这个数据集规模小,训练 100 个 epoch 可能 40 个 epoch 就早停了。早停不是坏事,brand 眼睁睁看着验证 mAP 开始下滑还不打断训练,才是真的浪费时间。
5. 避坑报告:这份数据集最常见的 5 个翻车现场
5.1 类别编号错位,训练时 cls_loss 掉不下去
现象是训练日志里cls_loss从头到尾几乎不下降,验证集mAP50在 0 附近抖动,像一条心电图。原因几乎都是labels里的类别编号和data.yaml里的names顺序对不上。比如转换脚本读取classes.txt时用了字典的哈希顺序而不是行号顺序,导致编号整体漂移了一位。
解决这个问题的第一刀,是用一个命令打印标签文件里出现过的最大类别编号,和nc对照:
cat labels/*.txt | awk '{print $1}' | sort -n | uniq -c输出结果应该是一列从 0 开始、到 9 结束的编号分布。如果最大编号是 9,而nc: 10,说明标签文件里没有越界的编号,问题大概率出在语义顺序上;如果最大编号是 10 或者出现了负数,说明标签文件本身已经脏了,需要回归 VOC 原始标注重新转换。之后再随机挑一个 label 文件,按编号去classes.txt里查对应的名字,再找到对应图片人工确认画框的是不是同一个东西。
5.2 训练集指标不错,手机照片测试时标注全歪
现象是模型在验证集上表现正常,但拿手机在果园里拍的照片测试时,检测框整体平移或者旋转了一个角度,看起来像是模型“喝醉了”。原因是手机拍摄的 JPEG 图片常带 EXIF Orientation 信息,OpenCV 的imread默认不处理它,而某些图片处理库(比如 PIL 的exif_transpose)会自动把图转正。如果数据发布环节有一步用过自动转正,但训练和推理代码统一用 OpenCV 读图,那一部分标注坐标就会和图像内容错位。
解决方法是全流程统一图像读取策略。我个人的做法是禁用一切自动 EXIF 旋转,在数据预处理阶段把所有图片先统一转正,写回文件,再生成标注。这样训练、验证、推理三条路径看到的图像方向始终一致。你甚至可以写一个脚本对比数据集里每张图在cv2.imread和PIL.ImageOps.exif_transpose两种方式下读出的尺寸,如果宽高对调了,说明这张图存在旋转信息,需要重点检查。
5.3 双格式转换后坐标漂移,VOC 和 YOLO 各说各话
现象是同一个数据集,用 VOC XML 训练一个模型和用 YOLO txt 训练一个模型,最终 mAP 能差出 10 个百分点。原因是两套标注并非完全等价——有些转换脚本为了数据清洗方便,会把坐标 clip 到图像边界,或者把过小目标过滤掉,两个格式的数据自然就不一致了。训练时用 txt,验证时用 XML,前后标准都不一样,分数低是必然的。
解决思路是以 VOC XML 为基准,对转换后的 txt 做一次逆向回读校验。读取 txt 里的归一化坐标,反推回像素坐标,再和对应 XML 里的bndbox计算 IoU,把 IoU 低于 0.95 的文件全部打印出来。如果发现某个文件大面积漂移,直接回到原始 XML 重新生成一份标签,不要手改 txt。这一步虽然繁琐,但能避免你在整个训练调参过程中反复被“玄学指标”误导。
5.4 害虫目标太小,mAP50 好看但现场漏检一大片
现象是验证集上mAP50已经到 0.85 以上,看起来不错,但拿到果园拍摄的远距离画面里一测,原先那些小虫全都检测不到。原因是害虫目标在图像里往往只有十几个像素,经过 YOLO 的多次下采样后可能只剩一两个像素,深层特征图里几乎没有可分辨的信息。目标越小,对输入分辨率的依赖越强。
解决思路分两步。第一步,把imgsz从 640 提高到 960 甚至 1280,让模型在更高的分辨率下看到目标。第二步,如果显卡承受不了 1280 分辨率,就改用切图策略:把一张 4K 大图切成若干个 640 子图,训练和推理都基于子图进行。这和遥感领域用 mmrotate 训练 DOTA 数据集时的切图方案是同一套思路,先把大图切小,解决目标在全局尺度上的稀疏性问题。注意切图推理时要保留每个子图在原图上的坐标偏移,最后把检测框坐标加回偏移量,再合并重叠框。
5.5 类别严重不均衡,损失函数曲线反复横跳
现象是训练到 80 个 epoch 之后,损失函数曲线没有继续平滑下降,而是在一个区间里反复跳动,验证集 mAP 也不见涨。原因很可能是 10 个类别里样本数量差异太大,多的类别几百张,少的只有几十张,模型把多数类学会后就陷入了局部最优,少数类的梯度被淹没在多数类的梯度里。
解决做法有几个可选。一是给每个类别按样本数量的倒数设置权重,YOLOv8 支持在data.yaml的names之外额外配置类别权重,让少数类的分类损失放大。二是对少数类做离线过采样,把它们的图片复制几份再参与训练,注意复制时要做随机增强,否则模型反而背下来了。三是干脆把样本数特别少的类别从 10 类里抽出来,单独训练一个二分类模型,往往比硬塞进多分类模型里效果好得多。我个人的经验是,低于 50 张的少数类在多分类模型里基本是陪跑,单独治理才是出路。
5.6 使用了带方向的图片增强,长条害虫形态学特征被破坏
现象是训练集上损失函数下降很快,验证集上却始终上不去,两者差距越拉越大。原因是数据增强里的水平翻转对“左高右低”的害虫形态产生了语义冲突——某些害虫的头部朝向在镜像后和真实分布恰好相反,模型学到的是矛盾的特征。
解决方法是检查data.yaml之外训练命令里的增强参数,把fliplr=0.5改成fliplr=0.0,同时可以保留mosaic和mixup。很多农业检测项目里,“翻转是否会破坏语义”这个问题被忽略,直到验证集分数长期卡住才回头排查。为了确认问题,你可以做一组对照实验:同一份数据、同一个种子,只改fliplr一个参数,看验证集 mAP 的差异。
6. 从训练集跑到果园:盲测和难例挖掘才是落地关键
模型在测试集上跑出不错的分数,离真正“能用”还差一步。无论mAP50多漂亮,我都建议你在第一次训练结束后,独立留出一批模型从未见过的照片做盲测。做法是:把所有图片按拍摄时间或者果树编号排序,把某几棵树的照片全部抽出,不参与训练和验证,只做最终验收。这种按场景来源划分的盲测集,比随机抽样更能暴露模型的真实泛化水平。我自己见过太多在验证集上全绿的模型,一到真实果园就漏了一半,原因就是验证集和训练集过于相似。
第二个值得投入的动作是难例挖掘。把盲测集的推理结果导出,专门看置信度在 0.3 到 0.7 之间的预测框:这些框大多数是真目标,只是模型信心不足。人工复核后把这些框补写进训练集对应的 txt 里,下一轮训练的效果往往立竿见影,比盲目调数据增强策略更高效。做法用一小段脚本就能完成:
from ultralytics import YOLO model = YOLO('runs/detect/train/weights/best.pt') results = model.predict(source='blind_test/', conf=0.3, save_txt=False, save=True) for r in results: boxes = r.boxes names = [model.names[int(c)] for c in boxes.cls] confs = boxes.conf.tolist() # 只保留犹豫的框,人工复核 for name, conf in zip(names, confs): if 0.3 <= conf <= 0.7: print(f'{r.path}: {name} {conf:.3f}')这段脚本里conf=0.3是保留低置信度预测的门槛,数值越低漏掉的少,但人工复核工作量会变大;打印结果里的每一个难例都值得对应到原图看一眼。
说句实话,当年我第一次拿农业数据集做检测时,只顾着盯验证集 mAP 调参,模型在温室测试视频里看着还行,拿到果园里测真实照片直接漏了一半小虫。那之后我把盲测当成验收门槛,任何超参修改都必须过盲测这一关。数据集的 mAP 只能证明你的模型在“见过类似图”时表现不错,证明不了它在现场也能扛住。这个习惯,希望帮到你。
本文还有配套的精品资源,点击获取