家禽鸡小鸡检测数据集:2970张手工标注,VOC与YOLO双格式
简介面向目标检测入门与智慧养殖应用场景的家禽鸡只检测数据集涵盖母鸡、公鸡、小鸡等不同生长阶段鸡只的手工标注信息既适合训练常用的YOLO系列与SSD等检测模型也可支撑家禽计数、群养状态分析等农业智能化项目开发。资源包共2000个文件以Pascal VOC格式XML标注文件为主体同时附有YOLO格式TXT标注文件压缩后体积约50.18MB整个目录组织简洁且文件名与对应图像保持映射可直接接入多数主流检测框架也便于练习不同标注格式之间的转换。全部标注使用labelImg手工完成总标注框数达6358个统一采用chicken类别标签标注覆盖不同光照、姿态与遮挡情况既可用于模型训练与精度评估也有助于理解目标检测数据集的构建规范和质检流程。目前已有463人在线学习使用对于缺少标注数据的学生、算法工程师及智慧养殖方案开发者来说该数据集能够显著降低采集与标注的时间成本还可作为迁移学习、数据增强等实验的基准数据加快算法验证进程。1. 家禽鸡小鸡检测数据集2970 张手工标注双格式可直接开练做智慧养殖、禽舍巡检这类目标检测项目最卡进度的往往不是模型选型而是没有一套能直接开练的标注数据。这份家禽鸡小鸡检测数据集共 2970 张实地图像手工标注同时给了 VOC 和 YOLO 两种格式意味着拿到手不需要再折腾标注工具和格式转换解压后就能配置进 YOLOv5/v8/v11 开始训练。它解决的是目标检测的冷启动问题养鸡场场景里成年鸡和雏鸡的密集目标检测缺的就是这种带标签的真实数据。适合两类人。一类是做农业 AI、畜牧自动化的开发者拿它训练鸡舍数量统计、个体状态识别另一类是刚入门目标检测的新手这套数据集的标注结构和真实分布比合成数据更有参考价值。但请记住2970 张只是起步量训练之前数据划分和标注抽查一个都不能省否则后面返工的时间足够你再标一遍。2. VOC 与 YOLO 双格式标注文件结构与坐标换算逻辑2.1 VOC 标注XML 里到底存了什么VOC 格式源自 PASCAL VOC 数据集。它把每张图的标注单独放在一个 XML 文件里文件名与图像同名。一个object块代表一个目标实例。看一个缩略的标注文件结构annotation filenameIMG_0001.jpg/filename size width1280/width height720/height /size object namechicken/name bndbox xmin120/xmin ymin80/ymin xmax300/xmax ymax260/ymax /bndbox /object /annotationXML 以树的形式组织最外层是 annotation。filename 字段给出对应的图像名size 里的 width 和 height 是图像原始分辨率这两个值对后续转换至关重要。核心是 object 块name 是类别名bndbox 里四个数字是目标左上角和右下角的像素坐标单位是像素xmin 和 ymin 是左上角xmax 和 ymax 是右下角。这张图里如果有多个鸡就会出现多个object。这也引出手工标注数据集最常见的问题标注员在密集场景下漏框、两只小鸡粘连成一个框。这类错误在 XML 里只靠看数字很难发现所以拿到手先做可视化复查第 6 章会给复查脚本。VOC 格式的真正价值是通用性。R-CNN、Fast R-CNN、SSD 这些框架的训练脚本很多都以 VOC 为输入albumentations 这类数据增强工具也能直接读 XML。这份数据集同时给出 VOC 和 YOLO 格式意味着你不用绑定单一框架哪天想换检测头或者做对比实验XML 在手里就不用重新标注。2.2 YOLO 标注五个数字与归一化坐标YOLO 系列的标注文件是 txt每一行对应一个目标共五个数字用空格分开0 0.4687 0.2391 0.0860 0.1128 1 0.6034 0.3112 0.0579 0.0894第一个数字是类别索引从 0 开始。剩下四个数字是归一化后的中心点 x、中心点 y、框宽度、框高度取值范围都在 0 到 1 之间。所谓归一化就是把像素坐标除以图像宽高得到一个与原始分辨率无关的比例值。从 VOC 到 YOLO 的换算公式是固定的x_center ((xmin xmax) / 2) / image_width y_center ((ymin ymax) / 2) / image_height width (xmax - xmin) / image_width height (ymax - ymin) / image_height公式本身不难难在细节。image_width 和 image_height 一定要取自 XML 的 size 节点而不是自己假设的像素宽高。有的标注工具导出的 XML 里 size 字段缺失这时候就必须用 PIL 读取原图得到宽高否则归一化出来的坐标全是错的。另一个细节是边界情况xmin 大于 xmax 之类的异常框偶尔会出现转换前判断一下遇到就交换两个值。YOLO 选择归一化坐标而不是像素坐标原因是训练时输入图像要缩放到统一尺寸比如 640×640而真实场景的输入分辨率千差万别。如果存的是像素坐标换个分辨率整套标注就作废了归一化之后无论原图是 1280×720 还是 4000×3000比例值直接可用。这也是为什么部署时只要在代码里把检测结果乘以输入图像的宽高就能还原出像素框。VOC 和 YOLO 的对应关系可以浓缩成一张表VOC 字段YOLO 字段含义bndbox/xminx_center归一化中心点 X0~1bndbox/yminy_center归一化中心点 Y0~1xmax - xminwidth归一化框宽0~1ymax - yminheight归一化框高0~1object/nameclass_id类别索引从 0 开始2.3 自己写一份 VOC 转 YOLO 脚本虽然这份数据集已经带了 YOLO 格式但实际动手时偶尔会遇到一个情况拿到的 YOLO txt 和 VOC XML 不是同一版本标注的或者有人中途补标过几张图两边对不上。自己写一个转换脚本一方面能核对两边是否一致另一方面以后换成自己的数据也能用。常见做法是逐文件遍历解析脚本如下import xml.etree.ElementTree as ET from pathlib import Path classes [chicken, chick] # 以压缩包内 classes.txt 为准 xml_dir Path(Annotations) label_dir Path(labels) label_dir.mkdir(parentsTrue, exist_okTrue) for xml_file in sorted(xml_dir.glob(*.xml)): tree ET.parse(xml_file) 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 classes: continue # 不认识的类别直接跳过避免污染训练 cls_id classes.index(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) if xmin xmax: xmin, xmax xmax, xmin if ymin ymax: ymin, ymax ymax, ymin x_center ((xmin xmax) / 2) / img_w y_center ((ymin ymax) / 2) / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) label_path label_dir / f{xml_file.stem}.txt label_path.write_text(\n.join(lines))代码逻辑是先解析 XML 根节点拿到图像宽高再遍历所有 object逐个把像素框换算成归一化坐标最后写入同名 txt。这里有三处值得说明。第一classes 列表顺序就是最终 YOLO 的类别索引必须和训练时 data.yaml 里的 names 完全一致顺序错一位整批标注都错。第二if name not in classes这一行不是多余的手工标注里偶尔会出现类别名不一致比如有人标了个带空格的 chicken 或直接写 hen跳过并事后打印收集比硬塞进训练要安全。第三xmin 与 xmax 交换的防御性判断在手工框里偶发但能避免训练时出现奇怪的边界错误。跑完脚本抽出三个 txt 随机人工比对一下数值是否在 0~1 之间、类别索引是否在合法范围一般就没大问题了。3. 目录结构与训练接入让 2970 张图跑进 YOLOv83.1 先解开压缩包看清文件怎么摆下载解压之后第一件事不是急着训练而是确认目录结构。这类数据集最常见的组织方式如下poultry-2970/ ├── images/ │ ├── train/ # 约 2300 张 │ └── val/ # 约 600 张 ├── labels/ │ ├── train/ # 与 images/train 同名 txt │ └── val/ # 与 images/val 同名 txt ├── classes.txt # 每行一个类别名 └── data.yaml # 有的版本直接给好了命名约定是 jpg 与 txt 一一对应IMG_0001.jpg对应IMG_0001.txt。如果压缩包里是单层目录所有图和 txt 放在一起没有 train/val 之分那就需要自己划分。手工拖动图片不可取因为标注文件得跟着图走漏移一张图训练时就会报 label 缺失。划分脚本我习惯这样写import random from pathlib import Path random.seed(42) # 固定种子保证每次划分结果一致 for sub in [images, labels]: for split in [train, val]: (Path(sub) / split).mkdir(parentsTrue, exist_okTrue) images list(Path(images).glob(*.jpg)) random.shuffle(images) val_ratio 0.2 val_set set(images[: int(len(images) * val_ratio)]) for img in images: label Path(labels) / f{img.stem}.txt if not label.exists(): continue # 没有对应 txt 的图直接跳过不放进训练集 sub val if img in val_set else train img.rename(Path(images) / sub / img.name) label.rename(Path(labels) / sub / label.name)这段脚本的关键点是先固定随机种子否则每次执行划分结果都不同别人复现不了你的实验。val_ratio 设为 0.22970 张里大约 594 张进验证集对这个量级的数据够用。按图划分而不是按标签路径划分是因为一张图只对应一个 txt图动了 txt 必须跟着动脚本里就是成对 rename。至于没有 txt 的图直接过滤掉宁可少一张训练图也不要让训练器去读一个不存在标注的文件。如果压缩包里已经分好了 train/val这一步就跳过。但我仍然建议训练前做一个快速一致性检查分别统计 images 和 labels 两个目录下同名文件的数量差一个都要查清楚。命令很简单find images/train -name *.jpg | wc -l和find labels/train -name *.txt | wc -l对比一下就知道了。双格式在这里还有一个实际用途VOC 的 XML 适合做数据预处理时精确控制框坐标YOLO 的 txt 适合直接丢进训练管线。如果后续要接 albumentations 做针对性增强比如只对鸡群密集区域做随机裁剪读 XML 比解析 txt 更顺手要快速跑基线直接用 txt。3.2 配置 data.yaml跑通第一次训练YOLO 系列框架用 yaml 文件描述数据和训练设置。这份数据集是双类别多数情况下 0 是鸡1 是小鸡具体以 classes.txt 为准。data.yaml 这样写path: ../datasets/poultry-2970 train: images/train val: images/val nc: 2 names: 0: chicken 1: chickpath 是相对于 yaml 文件位置的根目录train 和 val 给相对路径框架会自行拼成绝对路径。nc 是类别总数必须与 names 的数量一致也和 txt 第一列的类别索引上界一致。如果压缩包里其实只有一个类别就把 nc 改成 1names 只留一行同时确认所有 txt 的第一列都只能是 0。训练命令我一般用 yolov8s 起步yolo detect train \ modelyolov8s.pt \ datapoultry.yaml \ epochs150 \ imgsz640 \ batch16 \ device0 \ patience30参数选择有讲究。yolov8s 是 small 版本参数量约 1100 万对这个数据量级是最稳妥的选择yolov8n 更快但精度下降明显参数量更大的 l 和 x 在 2970 张图上有过拟合风险除非迁移学习做得细否则不推荐起步就用大模型。imgsz640 是 YOLO 系列默认分辨率小目标检测可以先从 640 跑基线等 mAP 出来再往 960 试。batch16 在 8G 显存的显卡上能跑 640 分辨率如果是 4G 显存降到 8。patience30 表示连续 30 个 epoch 验证集指标不提升就自动早停省时间也防过拟合。训练完以后验证和推理yolo detect val modelruns/detect/train/weights/best.pt datapoultry.yaml yolo predict modelruns/detect/train/weights/best.pt sourcetest.jpg提示train 和 val 路径如果写反了训练不报错但验证曲线会异常难看。先确认 images 下两个子目录的图片数比例约为 8:2再开始训练。第一次训练的目的不是直接拿高 mAP而是确认目录、配置、类别这三层都通了。跑通后看 runs/detect/train 下的 results.png里面有每个 epoch 的 box_loss、cls_loss 和 mAP 曲线。曲线上升但有波动是数据量小的正常表现不用慌如果 mAP 一直是 0回到第 5 章的避坑清单排查。4. 训练前质量体检类别分布、空标注与小目标检查4.1 类别分布统计用脚本看一眼标签在说什么训练前先跑一个统计脚本看清类别分布和标注密度。这一步能避免很多“训练完才发现类别不平衡”的返工。mAP 是个全局指标两个类别框数悬殊时它会把少数类的劣化掩盖掉只看曲线根本发现不了。import collections from pathlib import Path label_dir Path(labels/train) class_boxes collections.Counter() empty_files [] total_boxes 0 valid_images 0 for txt_path in label_dir.glob(*.txt): lines [l.strip() for l in txt_path.read_text().splitlines() if l.strip()] if not lines: empty_files.append(txt_path.name) continue valid_images 1 for line in lines: cls_id int(line.split()[0]) class_boxes[cls_id] 1 total_boxes 1 print(各类别框数, dict(class_boxes)) print(有标注图片数, valid_images) print(空标注文件数, len(empty_files)) print(平均每图框数, total_boxes / max(valid_images, 1)) if empty_files: print(空标注示例, empty_files[:10])这段脚本做了四件事统计每个类别出现的框数、标记没有框的 txt、计算平均每图目标数、打印空标注样本。输出结果能回答四个问题。第一两类框数是否悬殊。如果 chick 的框数是 chicken 的四五倍说明类别严重不平衡训练时模型会偏向框多的类别导致成年鸡容易被漏检。常规做法是对多类别降采样或者给少类别提高 cls 损失权重在 YOLO 里就是调整 class weights。第二平均每图框数。如果平均超过 10说明是密集小目标场景训练分辨率要往 960 靠拢而不是守着 640。第三空 txt 的数量。这里要分情况如果图上确实没有鸡空文件是合法的如果图上有鸡却空着说明标注漏了训练时这一张图就是纯负样本会拖累收敛。第四打印出的文件名方便逐个去对应原图人工核实。如果发现空标注文件太多建议直接过滤而不是去补标。一个通用做法是给数据集做一个干净的副本只保留有非空 txt 的图片训练时用副本原包留着不动。同理对没标注的图也是这个策略。数据副本这个概念就像后悔药后面改标注或换划分方式时不用重新解压。4.2 小目标检查分辨率不够的时候模型会瞎家禽这类场景里小鸡在画面远处可能只占十几个像素。YOLO 在 640×640 下训练一个 12×12 像素的框经过缩放后几乎变成几个像素点特征提取器很难学到有效特征。所以要评估一下数据里小目标占比。from pathlib import Path label_dir Path(labels/train) img_width 1280 # 换成数据集中实际分辨率 small_count 0 total 0 for txt_path in label_dir.glob(*.txt): for line in txt_path.read_text().splitlines(): parts line.split() if len(parts) 5: continue _, _, _, w, h map(float, parts) total 1 if w * img_width 32 or h * img_width 32: small_count 1 print(f小于 32 像素的目标占比{small_count / max(total, 1):.1%})判断小目标的阈值用了 COCO 的常见约定单边小于 32 像素就算小目标。如果这个占比超过 30%训练策略就得调整。最直接的方案是把 imgsz 从 640 提到 960让目标在输入空间里占据更多像素代价是显存和训练时间翻倍另一条路是训练时保持 640推理阶段用 SAHI 一类的切图工具把大图裁成小块分别检测再合并结果。前者简单粗暴后者适合部署时要检测大尺寸原图。注意脚本里的 img_width 必须和数据集真实分辨率一致。2970 张图如果来自不同来源分辨率未必统一要先用 PIL 或 cv2 批量读一遍宽度分布不要拿一个固定值去算。还有一个容易被忽略的问题小鸡挤成一团时标注框之间会高度重叠。如果两条框的 IoU 超过 0.7 又分属两个不同目标属于正常如果同一个目标被重复标注训练时损失会被重复计算模型会对那个区域产生过置信输出。检查方法很简单随机抽几十张图可视化人工看有没有重复框这一步虽然原始但最有效。5. 避坑记录这套数据训练时踩过的四类问题这套数据本身质量不错但拿它训练时我先后踩过几个坑现象、原因和解决列在下面基本覆盖了这类手工标注数据集最常见的翻车点。5.1 训练刚开始就报 No labels found现象用 ultralytics 跑训练日志刷了一堆 INFO最后卡在WARNING No labels found in ... train set然后 validation 直接显示 0 类训练等于空跑。原因data.yaml 的 train 路径指到了 images 而不是 labels或者 labels 目录里的 txt 全是空文件。ultralytics 会同时检查 images 和同名的 label任一缺失就会跳过。解决先确认目录结构是 images/train 与 labels/train 并排再执行第 4 章的空文件统计如果空文件比例异常说明标注导出环节有遗漏。我遇到的那次是 val 路径写成了 train导致验证集没有标签可用训练照样跑但指标全是 0。这种问题只看终端信息很难定位把 data.yaml 里的路径逐行展开确认一遍最直接。5.2 训练不报错但验证集漏检小鸡现象mAP0.5 到 0.92看起来不错可实际视频里远处的小鸡基本全漏框全落在成年鸡身上。原因数据集中小鸡占比高但像素尺寸小640 分辨率输入下特征被压缩到几个像素mAP 是全局平均被占比大的鸡类别托高了掩盖了小目标的劣化。另一个辅助原因是标注漏框验证集里有些小鸡根本没被标注模型检测出来了反而算错自然学不到。解决把 imgsz 提到 960 重新训练mAP0.5 可能掉一点但小目标召回会明显提升如果显存不够就用 SAHI 切图推理用get_sliced_prediction替代原图直接预测。同时回到第 6 章的复查脚本人工确认验证集里没有大面积漏标。5.3 框检测出来了但位置明显偏移现象推理时画出的框总偏向鸡身体一侧套住半个身体或把相邻的另一只鸡也套进来。原因两种可能。第一种是原始标注本身有偏差手工标注时边界框画得随意第二种是转 YOLO 时 x_center 和 width 计算公式写错或者用了错误的图像宽高做归一化。解决先做可视化复查把标注框画回原图比对看到底是标注问题还是转换问题。如果是标注问题只能回补或过滤没有插件能自动修得完美如果是转换问题检查 XML 的 size 字段是否与实际图片一致不一致就用 PIL 重新读宽高。那种所有框都往一个方向偏移的情况九成是转换公式错了不是标注员的问题。5.4 训练完发现两个类别张冠李戴现象模型把成年鸡标成 chick把小鸡标成 chicken两个类别全反而且训练指标很漂亮。原因classes.txt 的排序和 data.yaml 的 names 顺序不一致。压缩包里 classes.txt 写的是chick, chicken而 yaml 里写成了chicken, chick导致 txt 第一列的 0、1 含义整体错位。解决以压缩包内 classes.txt 为准逐一核对 yaml 里 names 的顺序然后重新跑一遍第 2 章的转换脚本或者直接重新生成 labels。这个坑不报任何错误连训练曲线都正常是最隐蔽的一个。养成习惯拿到数据集先cat classes.txt再写 yaml顺序永远以文件为准。6. 进阶把标注画回图像给迁移学习调参训练前值得做的一步是把标注框画回原图逐张肉眼扫一遍。很多标注问题在数字里看不出来画出来一眼就明白。可视化复查脚本不复杂import cv2 from pathlib import Path img_dir Path(images/val) label_dir Path(labels/val) names [chicken, chick] for label_path in label_dir.glob(*.txt): img cv2.imread(str(img_dir / (label_path.stem .jpg))) if img is None: continue h, w img.shape[:2] for line in label_path.read_text().splitlines(): cid, xc, yc, bw, bh map(float, line.split()) x1 int((xc - bw / 2) * w) y1 int((yc - bh / 2) * h) x2 int((xc bw / 2) * w) y2 int((yc bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, names[int(cid)], (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow(check, img) if cv2.waitKey(0) 0xFF ord(q): break按 q 退出按任意键看下一张。重点看三类问题漏框、框偏、类别错。每 50 张里抽 3 张扫一圈比写任何校验代码都实在。迁移学习方面2970 张图不算大建议从 COCO 预训练权重起练。第一轮跑通后第二轮可以冻结前 10 层学习率降到 0.0005 再微调让模型专注适应家禽数据集的特征分布。验证时同时看 mAP0.5 和 mAP0.5:0.95前者高只能说明框大致对上后者高才说明定位精度好对数量统计类应用尤其重要。部署阶段直接导出 ONNXyolo export modelbest.pt formatonnx imgsz640需要更高吞吐时再转 TensorRT engine配合 Jetson 这类边缘设备跑实时推理。我第一次拿这套数据直接开训没做空标注检查也没做可视化复查结果把一批漏标的图喂给模型mAP 骗了我实际视频里漏检严重。从那以后我拿到任何标注数据集都强制先把 labels 画回图像扫一遍再进训练管线。希望帮到你。本文还有配套的精品资源点击获取