☰
西瓜数据集目标检测实战:VOC格式转换YOLO及训练避坑指南
2026/10/1 21:12:05 网站建设 项目流程

简介:面向计算机视觉学习者、算法工程师及课程设计学生的西瓜目标检测数据集采用Pascal VOC标注格式,包含1702张jpg原图与1702个对应xml,唯一类别watermelon共标出2812个矩形框,且不包含分割路径或YOLO格式txt,结构简洁清晰。标注由labelImg完成,框选边界紧贴目标,类别统一,可直接用于YOLO、SSD、Faster R-CNN等主流检测框架的数据加载、训练与验证,也适合练习VOC格式解析、数据集划分与标注校验。压缩包共3405个文件,除1702对图像与标注xml外,另附1个说明txt,整体体积167.78MB,文件命名规范连续,便于脚本批量读取和按序划分训练集、验证集。目前已有487人学习下载,数据集不承诺模型精度,但标注准确合理,能有效节省自行采集与标注的时间,尤其适合目标检测入门实践、算法对比测试以及VOC格式制作流程演练。

1. 当你想用西瓜练手目标检测:这份1702张VOC数据集到底值不值得下

做目标检测的同学应该都有同感:真正上手跑模型时,最难的往往不是模型本身,而是找一份「能让自己完整跑通一遍数据流程」的数据集。公开的VOC或COCO太大,下载、解析、训练一套下来光磁盘都不够用;网上随便找的图又往往标注混乱,训练出来的模型完全没法看。这份西瓜数据集恰好卡在中间:1702张jpg、1702个xml、2812个框、单一类别watermelon,Pascal VOC格式,用labelImg画矩形框标注。对我来说,它最大的价值不是"能测出多高的mAP",而是能让你在几十分钟内把「读标注 → 转格式 → 可视化检查 → 跑训练」整条链路走一遍,踩的坑还都能自己解决。适合刚入门目标检测、想跑通YOLO系列但不想跟万亿级数据集硬刚的人,也适合做课程设计或写demo时需要一个干净数据来源的人。

2. 拿到手先拆包:VOC目录结构与xml标注信息怎么读

2.1 目录结构长什么样,jpg和xml为什么一一对应

这份数据集和你从标准VOC2007里看到的目录结构不一样。标准VOC通常有JPEGImages、Annotations、ImageSets/Main三个子目录,其中ImageSets里放着train.txt、val.txt这样的划分文件。但这份西瓜数据集只给了JPEGImages和Annotations的等价物——根目录下全部是jpg和xml,而且文件名一一对应,比如firc_watermelon_1134.jpg必然对应firc_watermelon_1134.xml。没有划分好的train/val文件,意味着你要自己做数据划分。

我一般拿到这种缺失ImageSets的数据集,第一反应不是抱怨,而是先确认一件事:jpg和xml是否真的严格同名。有些公开数据集会有jpg多一张或少一个xml的问题,训练到一半报"No labels found"才后悔。你可以直接在终端里用find命令比对,或者写几行Python快速检查,我下面会给出更完整的统计代码。

xml文件本身是Pascal VOC标准的格式。每个xml里有多个<object>块,每个块包含<name>watermelon</name>、<difficult>、<bndbox>四个坐标点。标注的人用labelImg画的是矩形框,所以坐标是xmin、ymin、xmax、ymax,对应左上角和右下角的整数像素。看一个典型xml结构:

<?xml version="1.0"?> <annotation> <folder>images</folder> <filename>firc_watermelon_1134.jpg</filename> <source><database>Unknown</database></source> <size> <width>640</width> <height>480</height> <depth>3</depth> </size> <object> <name>watermelon</name> <bndbox> <xmin>120</xmin> <ymin>85</ymin> <xmax>390</xmax> <ymax>320</ymax> </bndbox> </object> </annotation>

<size>字段很重要,后续转YOLO坐标时要用它做归一化分母,而不是依靠Python读取图片的shape——虽然这两者理论上应该一致,但见过太多因为图片被压缩或误改尺寸,导致xml里的<size>和实际图片不一致、转出来坐标全部错位的情况。

2.2 用Python快速统计类别、框数、图片尺寸,验证摘要数据是否属实

拿到数据集先别急着开训,第一步是验证「标注数量是否真的和摘要一致」。我习惯写一个统计脚本,把每一张xml的object个数、类别名、图片尺寸全部打印出来,顺便检查有没有异常值。这个脚本在任何一个VOC数据集上都能复用,建议直接存成voc_stats.py。

import os import xml.etree.ElementTree as ET xml_dir = "Annotations" # 实际路径以你解压后的目录为准 jpg_dir = "JPEGImages" class_count = {} total_boxes = 0 bad_files = [] size_set = set() for xml_name in os.listdir(xml_dir): if not xml_name.endswith(".xml"): continue xml_path = os.path.join(xml_dir, xml_name) tree = ET.parse(xml_path) root = tree.getroot() # 读图片宽高,顺便统计全数据集尺寸种类 size = root.find("size") w = int(size.find("width").text) h = int(size.find("height").text) size_set.add((w, h)) # 统计object objs = root.findall("object") total_boxes += len(objs) for obj in objs: name = obj.find("name").text class_count[name] = class_count.get(name, 0) + 1 # 检查jpg是否存在 filename = root.find("filename").text jpg_path = os.path.join(jpg_dir, filename) if not os.path.exists(jpg_path): bad_files.append(xml_name) print("类别统计:", class_count) print("总框数:", total_boxes) print("图片尺寸种类:", size_set) print("找不到jpg的xml数量:", len(bad_files))

这段代码干三件事:统计每个类别出现的框数、统计总框数、找出xml引用了但jpg不存在的坏样本。参数上有两点可以按需改:如果你把jpg和xml放在同一目录,就把jpg_dir改成和xml_dir一样;如果只想统计某几个类别,可以在循环内加一个if过滤。

我实际跑这份数据时,class_count输出的就是{'watermelon': 2812},size_set一般是{(640, 480), (960, 720)}这种多尺寸混合的情况,说明这组数据不是统一缩放到固定尺寸的。多尺寸图片训练时要注意:YOLO会把图缩放到固定输入尺寸,长宽比不同会导致letterbox补边,补边多的地方容易产生小目标误差,这点你后面训练时会体会到。

2.3 用labelImg打开看一张:box标注坐标的坐标体系

虽然统计脚本已经能说明标注格式,但我还是建议亲手用labelImg打开一张图看看。labelImg是这份数据集的标注工具,你装好之后打开一张jpg,它会自动读取同名的xml并显示框。看的目的不是确认"有没有框",而是确认坐标体系是像素坐标还是归一化坐标——VOC标准里bndbox的四个值都是绝对像素,labelImg界面左下角显示的也是实际像素值。如果有人在xml里写了归一化坐标,你的转换脚本就会出大问题。

主要看三点:框是否贴合西瓜边缘(太松或太紧都影响训练)、同一张图里多个框是否有重叠、有没有漏标(比如一个西瓜分成了两半只标了一半)。labelImg里按W切换到画框模式,D是下一张,A是上一张,空格是保存。全部过一遍不太现实,1702张图正常看两三张就够了,重点看尺寸不同、光照差异大的图,能发现不少隐藏问题。

3. 把VOC转成YOLO格式:转换脚本与四个边界坑

3.1 为什么需要转换,坐标怎么从(xmin,ymin,xmax,ymax)变成(cx,cy,w,h)

VOC格式能直接用到的场景其实不多,主流的目标检测框架,从YOLOv5到yolov8、yolov11,默认训练标签都是YOLO格式:每张图对应一个同名txt,每行一个目标,行内容为class_id cx cy w h,其中cx、cy是目标中心点坐标,w、h是目标的宽和高,全部除以图片宽高归一化到0~1。

为什么YOLO要用中心点加宽高,而不是直接给两个角点?因为YOLO的预测分支天然就是预测中心点偏移和宽高,直接归一化后模型更容易学,而且推理时直接乘以图片尺寸就能还原像素框。VOC的xmin那一套四个角点坐标虽然直观,但需要额外计算IoU时的转换成本。

这一步推荐自己写脚本而不是用现成工具——因为全网很多开源转换脚本都默认了class_id从0开始按类别名字母排序,如果你的类别不是按字母序,或者xml里有多类别,直接套用容易把标签全部搞错。这份数据只有一个类别watermelon,但你要理解脚本的逻辑,以后换成多类别数据集才不会翻车。

3.2 转换脚本与参数说明

下面的脚本把Annotations目录下所有xml转换为同名txt,输出到labels目录。核心逻辑是:解析xml的size拿到图像宽高,然后对每个object读取bndbox,计算归一化后的cx、cy、w、h,写入txt。

import os import xml.etree.ElementTree as ET xml_dir = "Annotations" txt_dir = "labels" os.makedirs(txt_dir, exist_ok=True) # 只有watermelon这一个类别,class_id固定为0 # 如果之后有多个类别,这里需要维护一个"类别名 -> id"的映射字典 class_id_map = {"watermelon": 0} for xml_name in os.listdir(xml_dir): if not xml_name.endswith(".xml"): continue tree = ET.parse(os.path.join(xml_dir, xml_name)) root = tree.getroot() w_img = int(root.find("size/width").text) h_img = int(root.find("size/height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in class_id_map: continue # 跳过未定义类别的框,避免写坏标签 cls_id = class_id_map[name] bbox = obj.find("bndbox") xmin = int(bbox.find("xmin").text) ymin = int(bbox.find("ymin").text) xmax = int(bbox.find("xmax").text) ymax = int(bbox.find("ymax").text) # 计算中心点和宽高,除以图片宽高完成归一化 cx = (xmin + xmax) / 2.0 / w_img cy = (ymin + ymax) / 2.0 / h_img w = (xmax - xmin) / w_img h = (ymax - ymin) / h_img lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") txt_name = xml_name.replace(".xml", ".txt") with open(os.path.join(txt_dir, txt_name), "w") as f: f.write("\n".join(lines))

逻辑说明:先拿size/width和size/height,这是归一化的分母;xmin+xmax除以2得到中心点像素坐标,再除以宽高得到归一化中心点。这里有个坑值得注意——bndbox的四个值都是整数,所以计算xmin+xmax时,如果两个奇数相加除以2结果可能是.5,YOLO训练时对精度不敏感,保留6位小数足够。脚本里class_id_map定义了类别名到ID的映射,目前只有watermelon,你如果扩展成多类别,务必维护好这个字典,不能直接往txt里写类别名。

3.3 四个边界坑:转换时最容易出的问题

第一个坑,也是最常见的:直接用w_img作为分母时,如果xml里的<size>写错了(比如width写了960但实际jpg是640),转换出来的坐标看起来在0~1之间,但画回原图会发现框整体偏移或缩放错误。解决方法是转换前额外用OpenCV检查一下图片真实尺寸,发现和xml不一致时以实际图片为准。

第二个坑:某个框的xmin和xmax相等,或者ymin==ymax,导致w或h为0。YOLO训练时遇到w=0的标注会直接报NaN loss或者跳过该样本。这种一般来自标注时的失误,转换脚本里应该加一个过滤:if w <= 0 or h <= 0: continue,并且单独打日志,方便回头找是哪张图出了问题。

第三个坑:filename标签和实际文件名大小写不一致。比如xml里写的是firc_watermelon_1134.JPG,但实际文件是.jpg,在Windows上没事,在Linux上就会找不到图片。这份数据本身没有这个问题,但你要知道一旦部署到Linux服务器,这类问题会突然冒出来。

第四个坑:转换后的txt不要和jpg放在同一目录,除非你明确知道框架会怎么读取。YOLOv5和v8的默认数据组织方式都是images和labels两个并列目录,你需要把转换出的txt放到与images同级的labels下,否则训练时会出现"找不到标签,自动退化为无监督"这种静默失败,损失曲线直接不收敛。

4. 做训练前的基础检查:标注质量、类别平衡与数据划分

4.1 可视化检查框:用OpenCV把标注画回原图

统计脚本只能证明"有标注",不能证明"标得对"。我每次拿到新数据集,都会抽10张图,把xml里的框用OpenCV画回原图,肉眼确认标注是否贴合真实物体边界。这一步能发现统计脚本发现不了的问题:框偏移、框太小、把背景也框进去、一个西瓜被裂成两个框。

import cv2 import xml.etree.ElementTree as ET import random # 随机抽10张xml画框可视化 sample_xmls = random.sample(os.listdir("Annotations"), 10) for xml_name in sample_xmls: tree = ET.parse(os.path.join("Annotations", xml_name)) root = tree.getroot() img_path = os.path.join("JPEGImages", root.find("filename").text) img = cv2.imread(img_path) if img is None: print("读取失败:", img_path) continue for obj in root.findall("object"): bbox = obj.find("bndbox") xmin = int(bbox.find("xmin").text) ymin = int(bbox.find("ymin").text) xmax = int(bbox.find("xmax").text) ymax = int(bbox.find("ymax").text) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 0, 255), 2) cv2.imshow("check", img) cv2.waitKey(0) cv2.destroyAllWindows()

这里用的是随机抽样,你如果想覆盖更多场景,可以手工挑一些尺寸特别大的图、特别小的图、多目标的图分别看。注意cv2读出来的图片是BGR通道,但画框不受影响。如果你在无GUI的服务器上跑,可以改成cv2.imwrite("check_" + xml_name.replace(".xml", ".jpg"), img)把画好的图存下来再远程看,如果遇到图片读取失败,基本就是jpg损坏或者路径不对,先处理这个再谈训练。

4.2 按8:1:1划分train/val/test,用随机种子保证可复现

原始数据集没有给划分文件,所以你要自己划分。划分的原则是:训练集要足够大(占80%),验证集用来监控训练过程和调参(占10%~15%),测试集只用于最终评估(占10%),且三者的图片不能有交叉。这里有个很容易被人忽略的点:划分前一定先打乱,而且要用固定的随机种子,否则每次运行脚本划分结果都不一样,后面你复现实验结果时没法对账。

import os import random jpg_dir = "JPEGImages" images = [f for f in os.listdir(jpg_dir) if f.endswith(".jpg")] random.seed(42) # 固定随机种子,保证每次划分结果一致 random.shuffle(images) n_train = int(len(images) * 0.8) n_val = int(len(images) * 0.1) train_set = images[:n_train] val_set = images[n_train:n_train + n_val] test_set = images[n_train + n_val:] # 写YOLO格式的划分文件 with open("train.txt", "w") as f: for img in train_set: f.write(os.path.join(jpg_dir, img) + "\n") with open("val.txt", "w") as f: for img in val_set: f.write(os.path.join(jpg_dir, img) + "\n") with open("test.txt", "w") as f: for img in test_set: f.write(os.path.join(jpg_dir, img) + "\n")

random.seed(42)就是只可复现性的关键,42是我自己的习惯值,你可以换成任意整数,只要你记得就行。注意shuffle是在切片之前做的,所以train/val/test的交集为空。如果你以后用YOLOv8自带的yaml格式训练,通常不直接读取这三个txt,而是要求你的数据集目录下有train和val两个子目录,分别存放对应的图片和标签,这点在6.1里会补上。

4.3 数据增强策略:1702张单类别数据集要不要加强

西瓜数据集中图片数量不算少,但类别只有一类,场景相对单一。我建议基础训练时先不开过多增强,用YOLOv8默认的增强参数跑一版baseline,看mAP多少。如果你发现过拟合明显(训练集损失降得低,验证集mAP上不去),再逐步加上:水平翻转(fliplr=0.5)、缩放(scale=0.5)、HSV颜色扰动(hsv_h=0.015, hsv_s=0.7, hsv_v=0.4)。

单类别数据增强有个反向风险:西瓜本身是圆形绿色的,如果你把色调扰动调得太大,模型会学到"只要是绿色圆的就是西瓜",遇到青椒或哈密瓜也误检。所以颜色扰动参数要比通用数据集更保守。至于mosaic增强,YOLOv8默认是开启的,它会随机把4张图拼在一起,对小目标训练很有效,但也会让西瓜这种大目标的标注边界变模糊,我一般先保持mosaic=1.0,观察验证集中西瓜的置信度分布再决定是否降低。

5. 避坑:VOC数据集使用中的五条常见问题

5.1 现象:训练时损失不降,准确率为0

原因:xml中存在<object>块,但bndbox的四个值全是0,或者在转YOLO时被过滤成了空txt,导致该图片没有有效标签。

解决:训练开始前,运行一次统计脚本,找出object数为0的xml以及转换后txt文件大小为0的样本。对这份西瓜数据集,理论上不会出现,但如果你从其他地方合并了数据,这属于第一优先排查项。

5.2 现象:转YOLO后损失直接变NaN

原因:归一化时用了xmax - xmin,但某些标注的xmax小于xmin,或者xmin等于xmax,造成w为负数或0,模型计算loss时出现除零或负对数。

解决:转换脚本里加保护:max(w, 1e-6),或者直接跳过这种框。另外检查一下坐标是否是从1开始计数导致的偏移,VOC的像素坐标如果是1-based,转成0-based时应该先减1,不过这份数据集的标注工具是labelImg,坐标天然是0-based,不用额外处理。

5.3 现象:训练时提示类别数不匹配或自动跳过所有标签

原因:你写的yaml里nc=1,names是["watermelon"]这没错,但txt文件里的class_id写成了1而不是0。多类别数据集常犯的错误是把类别名直接当ID写进txt,或者漏写class_id_map字典导致所有类别都变成0。

解决:写一个快速校验脚本,随机读5个txt,看看class_id是否都小于nc,并且和yaml里的names顺序一一对应。单类别数据集class_id恒为0,但也值得打印确认一次。

5.4 现象:训练集mAP很高,验证集mAP很低

原因:划分数据集时没有打乱,比如把文件列表按文件名排序后直接前80%当训练集、后20%当验证集,导致训练集全是某一块田里的西瓜,验证集全是另一场景的,分布偏差大。

解决:回到4.2的脚本,务必random.shuffle后再切分。另外验证集样本数不能太少,1702张的10%只有170张,单类别下mAP波动会比较大,建议评估时用mAP@0.5:0.95的均值,或者把验证集比例提到15%。

5.5 现象:用自己的预训练模型推理时,西瓜框永远偏大

原因:这不是数据集的锅,而是你把模型输入尺寸和原始分辨率搞混了。VOC原图如果是960x720,你在YOLO里设置imgsz=640,模型是在letterbox后的图上跑推理,后处理时要把预测的归一化坐标映射回原图,如果忘记考虑letterbox的填充边距,框就会整体向右下偏移并放大。

解决:推理时使用YOLO框架自带的Results.plot()或者把conf=0.3调低一点观察,而不是自己手写坐标变换。如果你非要手写,就记牢公式:x_original = (x_norm * imgsz - pad_x) / scale,其中pad_x是letterbox补边的宽度。

6. 进阶玩法:把这份西瓜数据集变成自己的小项目

6.1 用yolov8快速跑通custom数据训练

转换好YOLO格式、划分好数据集之后,就可以用Ultralytics YOLOv8来训了。先安装依赖,再写一个watermelon.yaml:

path: ./watermelon_dataset train: images/train val: images/val test: images/test nc: 1 names: ["watermelon"]

注意path是数据集的绝对或相对路径,train和val填写的是存放图片的目录路径(不是txt)。YOLOv8会按相同目录名的labels找到对应标注,所以你要把图片放在watermelon_dataset/images/train/下,把txt放在watermelon_dataset/labels/train/下。

训练命令很简单:

yolo detect train data=watermelon.yaml model=yolov8n.pt epochs=100 imgsz=640 batch=16

model=yolov8n.pt是nano版本,参数量最小、最容易收敛,对西瓜数据集这种单类别来说足够;如果你GPU显存小于6G,用nano即可。训练完后推理:

yolo detect predict model=runs/detect/train/weights/best.pt source=test_images

观察runs/detect/train里的results.csv,重点看metrics/mAP50(B)曲线——单类别数据集如果训练稳定,mAP50最后一般能在0.85以上,如果低于0.7,回头检查4.1的可视化是否出现过系统性的标注偏移。

6.2 给数据集补一个labels.txt描述文件,方便其他工具链读取

很多标签转换工具、导览脚本、模型格式转换脚本都期望有一个labels.txt或classes.txt文件,里面一行一个类别名顺序对应ID。这份数据集原始信息里没有这个文件,你转换时应该自己补上:

echo "watermelon" > labels.txt

然后把这个文件放在数据集根目录,或者和annotations平级。这样后面如果你要转成COCO格式、或者用MMDetection、Detectron2,都能直接引用它,不用再去翻xml里的name标签。细节虽小,但在团队协作时能省掉很多沟通成本。

6.3 一个具体技巧:用这份数据做迁移学习或小目标实验时的收敛检查

很多同学拿到数据集就无脑跑100个epoch,结果训练集mAP很高但根本没收敛彻底就停了。我的习惯是:训练到一半就去看results.csv里的val/box_loss,如果它还在稳步下降,说明还没到收敛点,加大epochs=200;如果val/cls_loss已经持平但train/box_loss还在掉,说明过拟合开始,赶紧降学习率或加增强。

作为迁移学习起点,这个西瓜数据集很适合测试你写的新注意力模块或者loss函数——单类别、背景不算极端复杂,跑一轮实验只要十几分钟,迭代速度快。如果你要做小目标实验,可以故意选那些尺寸小的西瓜图(比如尺寸集里最小的一组)单独做一个子集,比对模型在大目标和小目标上的精度差异,这种对比实验在学术论文里非常常见。

最后说一个我自己的教训:拿到任何VOC格式数据集,我第一件事永远是跑统计脚本 + 随机画10张图 + 转YOLO格式 + 试训10个epoch,四步全走完才敢正式训练。有一回偷懒没做可视化,结果几百张图的xml里坐标全被人手动偏移了5个像素,训练出来模型检测框总是偏在西瓜边上一截,排查了两天才定位到是数据问题而不是模型问题。从那以后我每拿到一份新数据集,无论标题写得多干净,都强制走一遍这四步。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询