简介:一套面向YOLO系列算法训练与验证的鸡蛋品质分级目标检测数据集,包含615张带标签图像,标注覆盖血染鸡蛋、棕色鸡蛋、脏污鸡蛋、白鸡蛋和钙沉积蛋五类典型对象,适合食品分拣、农畜产品质检、智能养殖等场景,也可作为目标检测课程设计或算法对比实验的数据基础。压缩包内总计1846个文件,含615张jpg原图、615个txt标注(YOLO格式)和615个xml标注(VOC格式),另有1个data.yaml配置文件;两套标签分别放在独立文件夹中,文件名末尾带有类别标识,整体目录清晰。txt标注采用<类别> <中心点x> <中心点y> <宽> <高>的归一化坐标格式,可被yolov5、yolov7、yolov8、yolov9、yolov10、yolov11直接读取;xml则适合在VOC流程或标注工具中查看。数据集已划分好训练/验证集,配合data.yaml即可直接训练和测试,无需额外整理或转换标签。整包大小仅25.41MB,轻量易获取;目前已有104人学习,可帮助快速构建鸡蛋品质分级原型,并把更多精力放在模型调参与效果验证上。
1. 鸡蛋品质检测不是玄学:这套 YOLO 数据集到底解决了什么
YOLO算法做鸡蛋品质分级,听起来有点大材小用,但真到了产线上,你才发现缺的不是模型,而是带标签的数据。这批615张图像带标签的鸡蛋品质分级数据集,正是为了解决这个问题准备的。在禽蛋加工厂,传送带上的鸡蛋每一颗都要经过人工肉眼检查。血染、脏污、钙沉积这类缺陷,不同工人判断口径还不一样。我把目标检测模型引入现场后发现,算法本身不是瓶颈,真正难得的是一份干净、可复现的带标注样例。这套资源覆盖血染、棕色、脏污、白色、钙沉积五类,同时提供YOLO的txt和VOC的xml两种标签,已经划分好train/valid/test,内置data.yaml,可以直接喂给YOLOv5/v7/v8/v9/v10/yolo11。不管你是做食品质检自动化、农业视觉落地,还是想找一份“处理数据集用于yolov8训练”的标准练习样本,都能直接拿它起步。
2. 鸡蛋数据集的底细:五类标签、双格式坐标和目录划分
拿到一个数据集,第一件事不是急着训练,而是把它拆开看。这个数据集的标签组织和常规目标检测数据集不太一样,它同时给了你YOLO的txt和VOC的xml。理解这两套格式的差异,以及它们和data.yaml的配合方式,是后面所有操作不掉链子的前提。
2.1 五类鸡蛋的视觉特征与标注难点
数据集里的五类分别是血染鸡蛋(blood_stained)、棕色鸡蛋(brown)、脏兮兮的鸡蛋(dirty)、白鸡蛋(white)、钙沉积蛋(calcium_deposit)。血染蛋表面有红色或暗红色斑块,轮廓不规则,有时和深棕色蛋壳很难区分;棕色蛋的颜色跨度很大,从浅褐到深褐都有,光照变化下容易和血染蛋混;脏污蛋表面有泥土、粪便等,遮挡了蛋壳本身的纹理;白鸡蛋比较干净,但恰恰是这种“干净”,让它和钙沉积蛋成了最难分的一对。钙沉积蛋的蛋壳上有凸起的白色钙斑,常见于气室周围,小尺度下和白鸡蛋的蛋壳表面非常相似。
标注难点就在这里:白色和钙沉积的边界是模糊的。同一个标注者过几天再标同一张图,都可能给出不同标签。所以我一直强调,这份数据集能不能用出效果,关键在标签质量。615张图不算多,你完全有条件在训练前做一轮人工抽检,把明显标错的框修正掉。具体怎么做,我放在后面的避坑章节里。
2.2 双标签格式:YOLO txt 与 VOC xml 的读取与转换
YOLO格式的txt文件,每一行是<class> <x_center> <y_center> <width> <height>,其中中心坐标和宽高都是相对原图的归一化值,范围0~1。VOC格式的xml文件则是标准的PASCAL VOC结构,<object>节点里包含<name>和<bndbox>,给出的是绝对像素坐标。
为什么同时给两种?就我接触的项目来看,老一些的质检流程还在用VOC格式喂SSD或Faster R-CNN,新项目基本都直接在ultralytics框架上走YOLO。你拿到手后,绝大多数场景用txt就够了,但偶尔需要统一格式。比如你有一个现成的xml标注工具,想把这份数据并进去,那就得把txt转成xml。反过来,如果你要跑YOLO,但手头只有xml,也一样能转。
先演示读取YOLO txt并绘制框的方法:
import cv2 def draw_yolo_boxes(img_path, label_path, class_names): img = cv2.imread(img_path) h, w = img.shape[:2] with open(label_path, 'r') as f: for line in f: parts = line.strip().split() cls_id = int(parts[0]) x_center = float(parts[1]) y_center = float(parts[2]) box_w = float(parts[3]) box_h = float(parts[4]) x1 = int((x_center - box_w / 2) * w) y1 = int((y_center - box_h / 2) * h) x2 = int((x_center + box_w / 2) * w) y2 = int((y_center + box_h / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[cls_id], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) return img names = ['blood_stained', 'brown', 'dirty', 'white', 'calcium_deposit'] img = draw_yolo_boxes('img_0642_150.jpg', 'img_0642_150.txt', names) cv2.imshow('check', img)这段代码把归一化坐标还原成像素坐标,再画框显示。注意乘以图像宽w和高h时一定要用原始图像尺寸,而不是resize后的尺寸。如果你发现画出来的框错位,先检查图像是不是被旋转了,再检查txt里的坐标有没有超过0~1范围。
读取VOC xml要简单一些,用标准库即可:
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'): name = obj.find('name').text bndbox = obj.find('bndbox') xmin = int(bndbox.find('xmin').text) ymin = int(bndbox.find('ymin').text) xmax = int(bndbox.find('xmax').text) ymax = int(bndbox.find('ymax').text) boxes.append((name, xmin, ymin, xmax, ymax)) return boxes这里返回的是绝对像素坐标和类别名。转YOLO格式时,记得用图像宽高归一化。转换脚本我会在第4章给出,但核心逻辑是:x_center = (xmin + xmax) / 2 / img_w,宽度w = (xmax - xmin) / img_w,并且保留6位小数,不要四舍五入到整数。
2.3 data.yaml 与 train/valid/test 目录结构
摘要提到数据集已经划分好,并且包含data.yaml。我猜它的目录结构大致是这样的:
egg_data/ ├── data.yaml ├── train/ │ ├── images/ │ └── labels/ ├── valid/ │ ├── images/ │ └── labels/ └── test/ ├── images/ └── labels/data.yaml是YOLO训练的关键配置,常见内容如下:
train: train/images val: valid/images test: test/images nc: 5 names: 0: blood_stained 1: brown 2: dirty 3: white 4: calcium_deposit注意names的索引顺序必须和txt里的class索引一致。很多朋友问“为什么我mAP一直是0”,十有八九是这里顺序搞反了。这份数据集既然内置了data.yaml,本来可以直接用,但我仍然建议你打开检查一遍。尤其是train和val的路径写法,有些版本喜欢相对路径,有些版本必须用绝对路径,不然会报错找不到图片。
另外,划分好的目录虽然方便,但615张图做五类检测,验证集可能只有60~80张。如果验证集太小,模型的mAP波动会很大。如果你有足够时间,可以考虑合并train和valid,然后做K折交叉验证。不过对于快速验证数据集能不能用,直接采用默认划分就够了。
2.4 标签体检:统计类别分布与坐标越界检查
训练前我习惯先跑一段“体检”脚本,看看各类别目标数量是否均衡,标签坐标是否都在图像范围内。这个步骤能帮你提前发现很多坑。
from pathlib import Path import cv2 import collections labels_dir = Path('train/labels') images_dir = Path('train/images') counter = collections.Counter() out_of_range = 0 for txt_path in labels_dir.glob('*.txt'): img_path = images_dir / (txt_path.stem + '.jpg') if not img_path.exists(): img_path = images_dir / (txt_path.stem + '.png') img = cv2.imread(str(img_path)) h, w = img.shape[:2] with open(txt_path) as f: for line in f: parts = line.strip().split() cls_id = int(parts[0]) counter[cls_id] += 1 x_center = float(parts[1]) y_center = float(parts[2]) box_w = float(parts[3]) box_h = float(parts[4]) if not (0 <= x_center <= 1 and 0 <= y_center <= 1 and 0 <= box_w <= 1 and 0 <= box_h <= 1): out_of_range += 1 print('类别分布:', counter) print('越界标签数量:', out_of_range)这段代码统计每个类别的目标数量,并检查归一化坐标是否越界。如果某类只有个位数目标,比如血染蛋只有20个,那么模型的该类AP大概率会很低。你可以考虑针对该类做简单的数据增强,或者收集更多样本。越界标签则需要回退到原始标注,重新转换。
3. 把数据集跑起来:YOLOv8 训练鸡蛋分级模型的完整步骤
训练这一步,网上教程很多,但很多都是搬官方文档,缺少实际参数调整的经验。这里我按自己的实际操作顺序来写,尽量让新手也能一次把模型跑起来。
3.1 环境准备:ultralytics 的安装与版本选择
训练YOLOv8只需要ultralytics这个包,官方pipeline已经把所有细节封装好了。安装命令:
pip install ultralytics如果你之前装过,建议卸了重装,否则可能与PyTorch版本产生冲突。我一般用ultralytics 8.x搭配Python 3.9。训练用的硬件,有N卡最好,没有N卡用CPU也能跑,只是时间会拉长。615张图,100个epoch,CPU大概要跑三四个小时,GPU通常十几分钟。
安装完成后验证一下:
yolo --help能打印出帮助信息,就说明环境没问题。
3.2 准备数据集目录并修改data.yaml
解压下载的压缩包后,把整个文件夹放到一个没有中文和空格的路径下,比如/data/。这一点很多人忽略,Windows下路径带中文,ultralytics偶尔会读取失败。
下一步是确认data.yaml里的路径。如果你和我一样把项目放在/data/egg_data/下,建议把data.yaml改成绝对路径,省得后面启动训练时还要切换工作目录。改完后的内容类似这样:
train: /data/egg_data/train/images val: /data/egg_data/valid/images test: /data/egg_data/test/images nc: 5 names: 0: blood_stained 1: brown 2: dirty 3: white 4: calcium_deposit改好后,用一小段python代码验证yaml能否正常加载:
import yaml cfg = yaml.safe_load(open('/data/egg_data/data.yaml')) print(cfg['nc'], len(cfg['names'])) assert cfg['nc'] == len(cfg['names']), 'nc和names数量不匹配'如果能打印出5 5并通过断言,说明配置没问题。
3.3 启动训练:预训练权重、batch、epochs 怎么设
训练命令:
yolo train data=/data/egg_data/data.yaml model=yolov8n.pt epochs=100 imgsz=640 batch=16 device=0解释一下关键参数:
model=yolov8n.pt:使用nano预训练权重,显存占用小,训练快。如果显存大于6G,可以换成yolov8s.pt,精度会更好。鸡蛋检测这种任务,s通常够用。imgsz=640:输入图像缩放尺寸。这份数据集的原始图像尺寸不一定统一,ultralytics会自动resize。鸡蛋不算极小目标,640够了。batch=16:根据显存调整。显存不够就降到8或4,显存充裕可以加到32。device=0:单卡GPU训练。CPU训练就改成device=cpu。
训练开始后,控制台会打印每个epoch的box_loss、cls_loss、dfl_loss以及验证集的mAP50、mAP50-95。如果你看到前几个epoch的mAP50直接跳到0.9以上,先别高兴,很可能是类别数错误或验证集太小。我一般会再等20个epoch,看cls_loss是否平稳下降,如果一直震荡,优先回去查标签。
3.4 训练输出目录与权重选择
训练完成后,结果默认保存在runs/detect/train/下。里面有两个权重文件:best.pt和last.pt。best.pt是在验证集上mAP50最高的权重,推理时用这个。last.pt是最后一个epoch的权重,用于断点续训。
你还会看到results.png和confusion_matrix.png。results.png包含loss曲线、精确率、召回率、mAP曲线,以及所有类的PR曲线。confusion_matrix.png能直观看出哪两类容易混淆。对于这份数据,重点看calcium_deposit和white之间的混淆率,如果互相认错的百分比高,后面要专门处理。
如果训练中途断了,想接着跑:
yolo train resume model=runs/detect/train/weights/last.pt这样会沿用之前的优化器状态和训练进度。
4. 验证与推理:把模型丢去测鸡蛋,还要会改标签格式
训练好模型只是第一步。你真正要回答的问题是:这个模型在实际场景里能不能用?这一章讲怎么评估、怎么推理,以及怎么把两种标签格式互相转换,顺便解决一些常见的格式问题。
4.1 用验证集评估模型性能
用训练好的权重跑验证集:
yolo val model=runs/detect/train/weights/best.pt data=/data/egg_data/data.yaml输出会包含mAP50、mAP50-95、每类的precision和recall。重点关注calcium_deposit和white这两类的AP。如果它们明显低于其他类,那么模型在产线上会把钙沉积蛋漏检或误报成白鸡蛋。这时候打开confusion_matrix.png,确认是哪种错误占主导。
如果只有个别类别差,可以尝试提高该类的样本权重,或者做针对性数据增强。但如果所有类都一般,优先回头检查标签质量。
4.2 对单张图像和视频做推理
用命令行推理最简单:
yolo predict model=runs/detect/train/weights/best.pt source=/data/egg_data/test/images/img_0642_150.jpg save=Truesave=True会在runs/detect/predict下生成带标注框的结果图。如果要批量处理,把source改成整个目录路径即可。
如果要跑视频或摄像头,source指向视频路径或摄像头ID。产线场景建议把conf阈值调高一点,比如conf=0.4,减少误检。命令行里加conf=0.4即可。
如果你要在自己的python脚本里调用,参考下面这段:
from ultralytics import YOLO model = YOLO('runs/detect/train/weights/best.pt') results = model.predict('/data/egg_data/test/images', conf=0.25, save=True) for r in results: boxes = r.boxes.xyxy.cpu().numpy() clss = r.boxes.cls.cpu().numpy() scores = r.boxes.conf.cpu().numpy() print(boxes.shape, clss, scores)返回的boxes是像素坐标的xyxy格式,clss是类别索引,scores是置信度。记得在部署脚本里要把clss映射回类别名称。
4.3 标签格式互转:xml转txt 与 txt转xml
虽然这份资源同时提供了两种格式,但你可能会遇到只有一个xml或只有一个txt的场景,这时候需要自己写转换脚本。下面是xml批量转txt的脚本:
import os import xml.etree.ElementTree as ET from pathlib import Path def convert_xml_to_yolo(xml_path, out_txt_path, class_names, img_w, img_h): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.iter('object'): name = obj.find('name').text if name not in class_names: continue cls_id = class_names.index(name) bndbox = obj.find('bndbox') xmin = float(bndbox.find('xmin').text) ymin = float(bndbox.find('ymin').text) xmax = float(bndbox.find('xmax').text) ymax = float(bndbox.find('ymax').text) dw = 1.0 / img_w dh = 1.0 / img_h x_center = (xmin + xmax) / 2.0 * dw y_center = (ymin + ymax) / 2.0 * dh w = (xmax - xmin) * dw h = (ymax - ymin) * dh lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") with open(out_txt_path, 'w') as f: f.write('\n'.join(lines)) class_names = ['blood_stained', 'brown', 'dirty', 'white', 'calcium_deposit'] convert_xml_to_yolo('sample.xml', 'sample.txt', class_names, 1920, 1080)注意几个要点:class_names.index(name)要求xml里的类别名和列表完全一致,大小写、空格都不能差。转换时用的img_w和img_h必须是xml对应的原始图像尺寸,如果你用了resize后的尺寸,坐标就会偏。另外保留6位小数,千万不要用整数。
反过来txt转xml的思路类似,但更少见。如果你的标注工具要求VOC格式,可以用labelImg直接打开txt生成xml,但我个人觉得没必要手动写,除非你要批量处理。
4.4 混淆矩阵与F1曲线:定位类间混淆
ultralytics训练结束后,会在输出目录生成confusion_matrix.png和results.png。confusion_matrix.png是一个5x5的矩阵(背景类也算的话是6x6),每一行代表真实类别,每一列代表预测类别,对角线上的值越高越好。我见过有人只看mAP,不看混淆矩阵,结果上线模型把钙沉积蛋全部误判成白鸡蛋,漏检率直接爆表。
F1曲线在results.png的右下角,它反映了不同置信度阈值下的F1分数。你可以从图中找到峰值对应的置信度阈值,用它作为推理时的conf。对于鸡蛋分级这种场景,高精确率比高召回率更重要,因为误检造成的成本比漏检大。所以我通常会把阈值定在F1曲线峰值偏右一点,也就是稍高的置信度。
5. 避坑指南:训练鸡蛋数据集最常踩的5个坑
这个数据集虽然结构干净,但如果你不提前注意几个细节,还是会把大量时间耗在排错上。下面是我自己实际踩过的坑,也是群里朋友问得最多的问题。
5.1 类别索引错位导致mAP一直为0
现象:训练20个epoch,验证集mAP始终是0,cls_loss不下降,甚至往上走。
原因:最常见的是类别索引错位。比如data.yaml里names顺序是0: blood_stained, 1: brown...,但txt文件里的class索引却是从1开始,或者某些xml转换工具把索引加了一次。ultralytics不会报错,因为它只按索引读,结果就是模型把所有目标当成另一个类别,loss自然不会收敛。
解决:用前面2.4的统计脚本,得到所有txt的类别索引分布。再检查data.yaml里的names顺序,确保两者一一对应。一个简单技巧是:把所有txt里出现的最大索引打印出来,如果等于nc-1就一般没问题,如果超过nc-1就肯定错了。转换脚本里,用class_names.index(name)而不是手动写数字索引。
5.2 EXIF方向导致推理框偏移
现象:训练时loss正常,mAP也还行,但推理画框时,框整体偏移,尤其是小目标,歪得明显。
原因:部分图像来自手机或相机,带了EXIF方向信息。cv2.imread默认不处理这条信息,读出来的图像是旋转前的样子,但标签是在旋转后的图像上标注的,坐标自然对不上。
解决:在数据准备阶段统一处理。我一般会写一个预处理脚本,把所有图像用cv2.imread后,读取EXIF方向,如果不是标准方向就旋转,然后覆盖保存。或者用PIL.Image的ImageOps.exif_transpose转正后再保存。这样保证所有图像的像素排列和标签严格一致。检测的时候也一样,先transpose再喂给模型。
5.3 数据划分泄漏导致验证集虚高
现象:验证集mAP50有0.96,但一拿到产线上测,漏检一堆,尤其脏污蛋。
原因:数据集划分时没有按图像来源分组。这套数据集的图像文件名有前缀,比如img_0642_150.jpg和img_0642_151.jpg,很可能来自同一个拍摄序列。如果随机划分,同一个拍摄序列的前后帧可能同时进入train和valid,验证集就有了“记忆”,导致指标虚高。
解决:检查文件名前缀,按前缀分组后再划分。如果没有分组信息,就用连续帧之间的间隔做划分:比如每隔10帧抽1帧作为验证集,其余训练。或者干脆手动把相似背景的图分开。这一步可以通过统计每张图的感知哈希来做,但615张图手动分也没问题。
5.4 标签噪声:背景阴影被标成目标
现象:模型把托盘边缘识别成白鸡蛋,或把传送带上的水渍识别成脏污。
原因:标注时不够严格,把蛋壳上的反光、阴影、甚至蛋托边缘也标成了目标。这种噪声在小目标检测里非常致命,因为模型会把背景特征当成正样本。
解决:做一轮标签人工清洗。写一个脚本,把所有训练图像和标注框输出到一个小视频或拼接图片,人眼快速扫一遍。615张图,半小时足够。我检查时重点看两类:white和calcium_deposit,因为它们最容易标错。发现有问题的框,直接用labelImg修正或删除。
5.5 VOC转YOLO时数值精度丢失
现象:转换后的txt文件里坐标看起来正常,但训练时小目标的框一直在抖,loss在后期不降。
原因:转换脚本用了四舍五入到百分位,比如round(x_center, 2)。YOLO格式的归一化坐标要用高精度,尤其是小目标,误差会被放大。
解决:转换时保留6位小数,使用格式化字符串f"{x_center:.6f}"。同时确保图片宽度和高度是原始数据,不是训练时的resize尺寸。转换后随机抽10张图,画框和原图重叠检查一遍,确认坐标没有偏移再训练。
6. 进阶技巧:用预训练权重和数据增强把mAP再拉高几个点
如果你已经用默认参数训练完,感觉指标卡在0.8过不去,可以试试这几个技巧。它们不复杂,但能让你的模型从“能跑”变成“好用”。
第一,把预训练权重从yolov8n.pt换成yolov8m.pt。模型容量增大,对特征能力的拟合会更好。显存不够就减小batch,比如从16降到8,或者把imgsz调成480再训练,之后推理再回到640。这个方法一般能带来1~2个点的mAP提升。
第二,调整数据增强参数。ultralytics默认开了大量增强,但有些针对小目标不友好。比如mosaic会把多张图缩放拼接,小目标缩得更小。你可以试着把mosaic关掉,或者降低mixup权重,在data.yaml里加一行:
augment: mosaic: 0.5 mixup: 0.2 hsv_h: 0.015 hsv_s: 0.7 hsv_v: 0.4具体数值需要实验。对于鸡蛋这种蛋壳颜色敏感的任务,色调扰动不宜过大,否则会把棕色改成红色,破坏语义。我个人习惯把hsv_h压到0.01以下。
第三,针对易混淆类别做困难样本挖掘。训练完第一个模型后,用它在验证集上预测,把所有预测错误或置信度低于0.5的图像挑出来。你可能会发现这些图像基本都是白色和钙沉积混标签的错误样本。把它们拎出来,人工修正标签后再加入训练集重新训练。虽然615张图不大,但这个操作能显著降低混淆率。
最后,一个不常被提但很实用的技巧:合并易混淆类别。如果产线不强制要求区分白鸡蛋和钙沉积蛋,你可以把calcium_deposit合并进white,把五类任务变成四类。类别少了,模型学习的负担小了,整体AP往往能上涨。我见过不少项目用这招后,mAP直接提高4个点以上。当然,如果产线确实需要把钙沉积单独分出来,还有另一个思路:在YOLO后面加一个专门的二分类细分头,但这已经超出这份数据集的应用范围了。
从那以后,我每拿到一份数据集都会强制走一遍“标签体检”——统计类别分布、画框检查、确认data.yaml顺序完整,然后才允许自己点下yolo train。这个习惯看起来琐碎,但确实帮我省掉了无数个深夜调bug的时间。希望这套鸡蛋品质分级数据集的完整演示,也能帮你在自己的目标检测项目里少走弯路,祝你顺利。
本文还有配套的精品资源,点击获取