简介:猕猴桃检测数据集是一份面向目标检测任务的专业标注数据,适合计算机视觉入门者与农业AI开发者用于训练猕猴桃识别模型。数据同时提供Pascal VOC格式的XML标注和YOLO格式的TXT标注,覆盖1838张猕猴桃图像,共包含2000个文件,其中1838个XML标注文件与162个TXT文件,压缩包整体约84MB,文件命名规律,便于脚本批量读取。类别仅有‘mihoutao’一种,标注框总数达19278个,全部由labelImg工具按矩形框规则勾画,标注准确且合理,适合单类目标检测场景的快速落地。已有722人学习下载,使用时可省去格式转换和手工标注时间,直接用于YOLO、SSD、Faster R-CNN等模型训练;同时规范的目录结构和统一的标注规则,也让这份数据成为学习构建检测数据集、开展数据增强或模型对比实验的实用参考,对农业采摘机器人、水果分级系统等应用开发具有直接帮助。
1. 这个单类猕猴桃检测数据集好在哪
要快速验证 yolo目标检测流程,真正卡人的不是训练命令,而是找不到标注干净、格式可对比的数据。这个猕猴桃检测数据集把“画框”做完了:1838张jpg图片配齐VOC格式xml和YOLO格式txt双套标注,单类别mihoutao,19278个矩形框,平均每张图约10.5个目标,是典型的高密度果实检测场景。做采摘机器人视觉或果园计数的工程师可以用它快速搭baseline;第一次接触目标检测的学习者,也能借这份数据把格式解析、数据划分、模型训练、指标验证整条链路走通。不管之后选YOLOv8、YOLO11还是换检测头,这个体量都足够跑出一个有参考意义的结果。
2. VOC与YOLO双格式的字段对齐与坐标换算
2.1 解压、哈希校验与文件数核对
这个数据集压成了7z包,先别急着解压,有几个动作比解压本身更重要。7z压缩文件的坑往往不是内容错,而是传输过程中文件损坏,直接解压出来的图可能缺一半。我拿到.7z文件后的习惯是先算哈希、再解压、最后对数。Linux下用7zip命令,Windows下除了7-Zip图形界面,还可以用PowerShell里的certutil -hashfile。
sha256sum firc_mihoutao.7z 7z x firc_mihoutao.7z -o/home/user/data/kiwi -ysha256sum输出的64位十六进制字符串要和文件来源给出的哈希比对,一致后再继续,这一步可以排除“下载到一半文件已坏”这类问题。7z x表示解压,-o指定输出根目录,注意-o和目标路径之间不要留空格,-y是遇到同名文件自动覆盖,避免命令在交互提示处卡住。解压路径尽量用绝对路径,相对路径在后续切换工作目录时容易引发标签加载错误。
解压完成后先做一次文件数核对,看三套文件是否数量一致:
find . -name "*.jpg" | wc -l find . -name "*.xml" | wc -l find . -name "*.txt" | wc -l三个数字都应该是1838,这样才能说明图片、VOC标注、YOLO标注三套文件一一对应。像firc_mihoutao_18.jpg、firc_mihoutao_18.xml、firc_mihoutao_18.txt这样的同名文件,后缀不同但主名必须一致。如果实际解压后txt数量少于jpg,说明标注过程有漏存,直接拿去训练会在数据集加载时静默丢图,训练损失曲线会莫名抖动,这种问题通常比模型结构更难排查。
2.2 VOC格式xml解析与字段语义
VOC格式的每张图对应一个xml文件,里面按object节点存放每一个标注框。labelImg画矩形框导出的VOC标注文件里,名称字段是mihoutao,框坐标由xmin、ymin、xmax、ymax四个标签给出,它们都是原图上的像素绝对值。一个典型的object节点内容大致如下:
<object> <name>mihoutao</name> <bndbox> <xmin>101</xmin> <ymin>27</ymin> <xmax>290</xmax> <ymax>222</ymax> </bndbox> </object>这个例子里,框的像素宽是xmax - xmin,像素高是ymax - ymin,对应原始图像中的实际目标区域。用xml.etree.ElementTree解析不需要额外安装依赖,代码也比较直白:
import xml.etree.ElementTree as ET def parse_voc(xml_path): tree = ET.parse(xml_path) root = tree.getroot() boxes = [] for obj in root.iter("object"): bnd = obj.find("bndbox") boxes.append({ "name": obj.findtext("name"), "xmin": int(bnd.findtext("xmin")), "ymin": int(bnd.findtext("ymin")), "xmax": int(bnd.findtext("xmax")), "ymax": int(bnd.findtext("ymax")) }) return boxesroot.iter("object")会递归找出所有object节点,比findall更稳健,因为不同版本的labelImg生成xml时嵌套层级不完全一致。数值转int前最好确认标签里没有空字符串,否则int(None)会抛TypeError,这种脏数据在后续校验脚本里要单独归类。解析结果里保留name字段是有意的,后续校验类别名时直接比对即可。
2.3 YOLO格式txt的归一化坐标与互转
YOLO格式的标注文件和VOC最大的不同是坐标全部归一化。每个txt文件放多个框,一行一个,字段结构是类别id cx cy w h。例如0 0.4531 0.3244 0.2013 0.1598,表示目标类别索引是0,目标中心位于图片宽高的53.1%和32.4%处,框宽约20.1%,框高约16.0%。归一化的好处是标签与输入图片分辨率解耦,不管训练时把图缩放到640还是1280,标签都能直接用。
从VOC格式转换到YOLO格式时,容易写错的点是中心点公式。中心点不是直接拿(xmin+xmax)/2,还要再除以图片宽高:
def voc_to_yolo(xml_path, txt_path, img_w, img_h): boxes = parse_voc(xml_path) lines = [] for b in boxes: cx = (b["xmin"] + b["xmax"]) / 2.0 / img_w cy = (b["ymin"] + b["ymax"]) / 2.0 / img_h bw = (b["xmax"] - b["xmin"]) / img_w bh = (b["ymax"] - b["ymin"]) / img_h lines.append(f"0 {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") with open(txt_path, "w", encoding="utf-8") as f: f.write("\n".join(lines))这段函数里的img_w和img_h必须是原图尺寸,不是训练时的输入尺寸。如果不确定,可以用cv2.imread读图后取shape[1]和shape[0]。类别id这里固定写0,因为这个数据集只有一个mihoutao类别;如果后续自己合并了其他种类,id要与模型yaml里的names顺序严格对应,不然模型训练出来的类别名全部错位。
VOC与YOLO两种格式的差异,我常列成一张对照表给团队里新人看:
| 维度 | VOC xml | YOLO txt |
|---|---|---|
| 坐标基准 | 原图像素绝对值 | 归一化相对坐标 |
| 框的表示方式 | xmin/ymin/xmax/ymax | cx/cy/w/h |
| 类别表示 | 名称字符串 | 整数索引 |
| 常见存放目录 | annotations/ | labels/ |
| 适用读取方 | 标注工具、评测协议 | 主流检测框架原生加载 |
两种格式同时存在的最大价值是互相校准:同一张图的xml和txt解析出来的框应当完全一致,这也是下一章校验脚本的核心思路。
3. 标注合规性校验、框分布统计与train/val划分脚本
3.1 为什么对“别人标好的数据”还要再校验
数据集页面写着“提供准确且合理标注”,但真实的网络下载场景里,这类描述只代表对方尽力了,不代表没有脏数据。从网上下载的数据集常见三类问题:xml和txt数量对不上、某个框坐标越界、图片本身损坏。这些问题单张很难发现,却会在训练时表现为loss不下降或验证指标异常波动,排查起来更耗时。尤其是 yolo目标检测流程 里,数据加载出错往往不会直接中断训练,而是跳过坏图,看起来只是曲线抖动。
单类别数据集的校验有一个特殊性:不用处理类间不平衡,但框的质量直接影响yolo损失函数。一个坐标越界的框在loss里会被模型当作大误差目标强行学习,造成附近位置的预测也偏移。所以训练前把每个框的坐标合理性检查一遍,比调模型参数更划算。
3.2 图片、xml、txt三套文件的一致性检查
我一般写一个脚本一次性完成检查,不引入额外依赖,opencv-python就足够。脚本逻辑分成三块:图片能否正常读、txt每行能否解析成合法的归一化坐标、xml和txt是否一一存在。校验时不需要解析完整xml,只查对应文件在不在,因为标注框数值是否合理统一看txt就够。
import cv2 from pathlib import Path img_dir = Path("images") txt_dir = Path("labels") xml_dir = Path("annotations") errors = [] for img_path in sorted(img_dir.glob("*.jpg")): img = cv2.imread(str(img_path)) if img is None: errors.append(f"broken image: {img_path.name}") continue txt_path = txt_dir / (img_path.stem + ".txt") xml_path = xml_dir / (img_path.stem + ".xml") if not txt_path.exists() or not xml_path.exists(): errors.append(f"missing label: {img_path.name}") continue for line_no, line in enumerate(txt_path.read_text(encoding="utf-8").splitlines(), 1): parts = line.strip().split() if len(parts) != 5: errors.append(f"{txt_path.name}:{line_no} col!=5") continue try: cid, cx, cy, w, h = map(float, parts) except ValueError: errors.append(f"{txt_path.name}:{line_no} not float") continue if cid != 0: errors.append(f"{txt_path.name}:{line_no} class_id={cid}") if not (0 <= cx <= 1 and 0 <= cy <= 1 and 0 < w <= 1 and 0 < h <= 1): errors.append(f"{txt_path.name}:{line_no} coord range") print("images checked:", len(list(img_dir.glob("*.jpg")))) print("error lines:", len(errors)) for e in errors[:50]: print(e)脚本里对yolo坐标的判断比较严格:中心点cx、cy必须在0到1之间,宽w、高h必须大于0且不超过1。这里有一个容易忽略的细节:0 < w <= 1要求宽不能等于0,因为xml里xmax等于xmin时转换出的w就是0,这样的框在训练时会被当作无效框,最好是提前发现而不是让框架悄悄过滤。如果错误列表为空,三套文件1838张齐整,这份数据才能进入下一步。
3.3 框尺寸分布与密度分析
格式通过校验后,我还会做一次更精细的统计,看框的尺寸分布和图片内部的密集程度。具体做法是读取所有txt,把每一行的归一化宽高收集起来,批量计算均值和分位数:
import numpy as np all_wh = [] for txt_path in txt_dir.glob("*.txt"): for line in txt_path.read_text(encoding="utf-8").splitlines(): parts = line.strip().split() if len(parts) == 5: all_wh.append((float(parts[3]), float(parts[4]))) wh = np.array(all_wh) print("total boxes:", len(wh)) print("mean w/h:", wh[:, 0].mean(), wh[:, 1].mean()) print("median w/h:", np.median(wh[:, 0]), np.median(wh[:, 1]))这个统计主要解决两个判断:目标尺度和目标密度。如果中位框宽在0.05以下,说明目标普遍偏小,训练时imgsz=640对每个目标的像素占比不足,可能要考虑imgsz=896或做切片推理;如果框宽度中位数在0.2以上,说明目标在画面里占比很大,网络很容易学,后处理NMS的阈值要适当调高。这份数据总框数19278、图片数1838,平均每张约10.5个框,属于密集场景,模型输出端会同时出现大量候选框,需要关注NMS的IoU阈值设置。
3.4 train/val划分脚本与随机种子
训练检测模型前要先把数据划分开。这个数据集没有附带划分用的txt文件,所以自己准备脚本是绕不开的一步。划分原则只有一条:以图像为单位分,不能把同一张图的多个框跨到训练集和验证集。一张图平均10.5个框,如果按框行数随机切分,会把同一张图的信息泄露到验证集,评估出来的mAP虚高,完全没有参考意义。
import random, shutil from pathlib import Path random.seed(42) imgs = sorted(Path("images").glob("*.jpg")) random.shuffle(imgs) val_cnt = int(len(imgs) * 0.15) val_imgs, train_imgs = imgs[:val_cnt], imgs[val_cnt:] for split, img_list in [("train", train_imgs), ("val", val_imgs)]: for img in img_list: dst_img_dir = Path(f"split/{split}/images") dst_lab_dir = Path(f"split/{split}/labels") dst_img_dir.mkdir(parents=True, exist_ok=True) dst_lab_dir.mkdir(parents=True, exist_ok=True) shutil.copy2(img, dst_img_dir / img.name) label_src = Path("labels") / (img.stem + ".txt") shutil.copy2(label_src, dst_lab_dir / label_src.name)脚本用copy2而不是move,是为了保留原始目录在出错时可以重新切分。random.seed(42)固定随机序列,确保每次执行脚本得到的train/val组合完全一样,这对后续模型对照实验是必须的。若是复用同一个数据集调参,两次划分不同会导致训练集与验证集分别变化,最后比较模型A和模型B时,分不清差异是模型带来的还是数据划分带来的。
划分后的组成可以用一张表表达:
| 划分 | 比例 | 估算张数 | 用途 |
|---|---|---|---|
| train | 85% | 1562 | 更新权重 |
| val | 15% | 276 | 早停与选best.pt |
| test | 需要时再划分 | 50~100 | 最终结果对比 |
单类别数据集不需要按类别做分层抽样;如果后面换成一个多类别数据集,我会用sklearn的train_test_split加stratify=y保持类别比例在划分前后一致,否则少数类可能全部掉进训练集或验证集。
4. YOLOv8训练自己的数据集:猕猴桃单类检测配置与参数
4.1 写一份干净的数据集yaml
把yolo训练链路整理顺畅,第一步是准备一个最简单的yaml文件。这个数据集单类,内容非常清晰:
# mht.yaml path: /home/user/data/mihoutao/split train: images/train val: images/val nc: 1 names: 0: mihoutaopath字段指向数据集根目录,train和val是相对路径,分别指向上一章划分出来的split目录。nc是类别数,names给出类别名列表。写yaml时注意两点:冒号后面必须有空格;path建议用绝对路径,因为训练脚本如果由其他目录启动,相对路径会根据当前工作目录变化而找不到数据。
如果使用的是YOLOv8或者更新的YOLO11,这个yaml可以直接被DataLoader读取;即使是更早的YOLOv5,结构也大体一致。类别名必须和数据集中的标注对得上,这里是mihoutao,如果写成kiwi,训练不会报错但模型输出的标签名会变成kiwi,视觉上会很奇怪。
4.2 训练命令与参数选择
单类别检测的训练命令我一直用yolo CLI方式,简洁且日志输出完整。加载COCO上的预训练权重做fine-tune,比从随机初始化训快得多,这也是 yolov8训练自己的数据集 时最常用的做法:
yolo detect train \ data=mht.yaml \ model=yolov8s.pt \ epochs=120 \ imgsz=640 \ batch=16 \ device=0 \ patience=20 \ project=runs/mihoutao \ name=exp1model=yolov8s.pt指定的预训练权重如果本地不存在,yolo会自动下载;如果环境无法访问外网,就手动下好放到当前目录。参数选择上,这个数据集的目标是中等偏大,19278个框分散在1838张图里,用yolov8s作为主干已经足够,直接上yolov8x在小数据集很容易过拟合。epochs设120,配合patience=20做早停,val指标连续20轮不升自动停止,实际训练出来的最佳轮次通常在60到100之间。
提示:显存不够时优先把batch从16改成8,不要先降imgsz。输入尺寸直接决定小目标的分辨率表达能力,batch降低只是增加训练时长,不太影响最终精度。
训练过程中常见的问题集中在路径身上,而不是模型参数。yaml里的path写错了,训练会报Dataset not found;Windows路径带反斜杠时yaml解析容易把转义符吃掉,我一般会在代码里先print(yaml.safe_load(open("mht.yaml")))检查一遍。batch大小改成8或4时,学习率不需要手动调,yolo默认会自动按batch缩放,这点和早期YOLOv5保持一致。
各参数的微调参考,总结成一张表:
| 参数 | 建议值 | 调整方向 |
|---|---|---|
| model | yolov8s.pt | 数据少用s/m,数据多用l/x |
| epochs | 120 | 观察val_loss再增减 |
| imgsz | 640 | 小目标多时升896 |
| batch | 8~16 | 以显存不溢出为准 |
| device | 0/cpu | 多卡时0,1 |
| patience | 20 | 防止过拟合 |
| plots | True | 生成results曲线 |
4.3 单类别场景下yolo损失函数的观察重点
训练日志里会实时输出box_loss、cls_loss、dfl_loss三项损失,这是理解 yolo损失函数 最容易上手的入口。box_loss使用的是CIoU类损失,负责回归预测框与标注框的位置;cls_loss是分类损失,因为这里只有一个类别,通常前10轮就快速收敛到一个很小的值;dfl_loss全称是Distribution Focal Loss,负责边框回归的分布学习,对边界细节影响比较大。
对这个数据集来说,真正要盯的是box_loss和dfl_loss曲线的分离程度。如果train/box_loss持续下降但val/box_loss在第70轮后掉头向上,说明模型开始记住训练集里果实的角度和光照,过拟合已经发生。这时把epochs从120降下来,或者在yaml里加weight_decay=0.0005做L2约束,比继续硬跑更有效。单类场景下如果看到cls_loss迟迟不降,优先去查标注txt的类别id是不是混入了其他数字。
训练完成后在runs/mihoutao/exp1目录下会自动保存最后权重和最佳权重,一般评估和部署都用best.pt而不是last.pt。算法对比实验只要保证固定随机种子、固定预训练权重和固定数据划分,跑出来的结果才有横向可比性。
5. 验证指标解读与两个提高标注可信度的技巧
5.1 用val命令评估而不是只看训练loss
训练日志的loss只能说明模型在训练集上的拟合情况,最终可不可用要单独跑一遍验证集。常用命令是:
yolo detect val \ model=runs/mihoutao/exp1/weights/best.pt \ data=mht.yaml \ batch=16输出会给出每个类别的precision、recall、mAP50和mAP50-95。对这个数据集,mAP50应该在0.9以上,mAP50-95相对低一些是正常的,因为它对IoU更敏感。如果mAP50低于0.85,先回去翻校验脚本,重点检查是不是有一部分标注框明显比真实果实小一圈,这种情况在手工标注矩形框时最容易发生在遮挡果实上。
5.2 用预测框与标注框叠加检查边界
验证指标正常后,我还习惯把预测结果和原始标注画在同一张图上抽查几十张。代码可以直接用ultralytics输出预测框,再解析VOC的xml把原始标注框画一遍:
import cv2 import xml.etree.ElementTree as ET from pathlib import Path from ultralytics import YOLO model = YOLO("runs/mihoutao/exp1/weights/best.pt") img_path = "split/val/images/firc_mihoutao_1513.jpg" img = cv2.imread(img_path) res = model.predict(img_path, conf=0.25)[0] for box in res.boxes: x0, y0, x1, y1 = map(int, box.xyxy[0]) cv2.rectangle(img, (x0, y0), (x1, y1), (0, 255, 0), 2) xml_path = Path("annotations") / (Path(img_path).stem + ".xml") root = ET.parse(xml_path).getroot() for obj in root.iter("object"): bnd = obj.find("bndbox") x0 = int(bnd.findtext("xmin")); y0 = int(bnd.findtext("ymin")) x1 = int(bnd.findtext("xmax")); y1 = int(bnd.findtext("ymax")) cv2.rectangle(img, (x0, y0), (x1, y1), (0, 0, 255), 2) cv2.imwrite("check_1513.jpg", img)绿色框是模型预测,红色框是原始标注。xml_path 这里指向原始annotations目录,如果划分时把xml也复制进split/val,就换成对应路径。大量对比后如果发现预测框总比标注框大一圈,往往是标注时把果实的边界框收得过紧,模型学到的是真实边缘的保守位置。更隐蔽的问题是xml文件名不匹配,路径写错时ET.parse直接抛出FileNotFoundError,说明目录组织方式和脚本预期不一致,需要回解压目录核对实际结构。
5.3 7z归档的哈希校验与重新打包
最后分享一个和资源本身直接相关的工作习惯。7z压缩文件获取哈希值不只是解压前校验用,实验结束后回传数据时也应该归档一次,并把这个哈希写进实验记录,确保后续复现实验下载的副本和当前训练用的数据完全一致。重新打包并计算哈希一行就可以完成:
7z a mihoutao_splits.7z split/ -mx=5 sha256sum mihoutao_splits.7z-mx=5是压缩等级参数,取值范围0到9,0是纯存储不压缩,9是极限压缩。对图片数据集没有必要用9,压缩时间和解压时间都会明显变长,-mx=5是比较平衡的选择;如果只是存档不常解压,-mx=1解压速度会更快。归档前再把jpg、xml、txt各自计数一次,和1838对上,这个副本就可以作为实验基线长期保存。
本文还有配套的精品资源,点击获取