ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

智慧牧场牛羊检测数据集:三格式标签解析与YOLO训练实践

智慧牧场牛羊检测数据集:三格式标签解析与YOLO训练实践 简介面向智慧牧场、畜牧业智能化与目标检测算法落地场景这份牛羊检测数据集包含3538张真实场景图片标注目标涵盖“牛”“羊”两类适合课程作业、算法比赛、毕业设计及实际农场系统中的牛群羊群识别与计数任务。压缩包内提供jpg原图以及voc(xml)、yolo(txt)、json三种主流标签格式其中txt标签3539个、xml标签3538个整体约706.2MB解压后可根据训练框架选用对应格式几乎无需格式转换。数据集源自博主实际项目沉淀图像分布均匀、背景丰富、角度多样目标尺寸与遮挡情况贴近真实环境标注精准且经过算法验证在常见检测模型上拟合良好能有效支撑模型训练与评估。目前已有471人学习下载无论用于入门练手还是推动智慧农业项目落地都是可直接上手的高质量数据集。1. 智慧牧场牛羊检测数据集三份标签把训练准备期从三天压到三小时智慧牧场牛羊检测数据集核心词在“3538张”和“voc(xml)yolo(txt)json三种格式标签”。对天天和标注打交道的工程师来说这等于拿到了一个标准的检测数据集包图片数量能支撑一轮像样的预训练微调三种标签覆盖了从传统检测流程到 YOLO 系训练再到 JSON 交换的全部口味。但压缩包解压只是开始真正值钱的是接下来怎么把这三份标签对齐、划分、转格式、跑通第一轮训练。这篇笔记就沿着一条可复现的路径走先解析三种格式再做转换和训练最后讲踩坑和验证适合刚拿到数据集就要出指标、或者准备把模型部署到牧场监控设备上的从业者。2. 读懂三份标签VOC XML、YOLO TXT、JSON 的坐标系与解析脚本2.1 同一只牛羊在三种标注里的坐标系表述完全不同一张 1280×720 的牧场图像里有一只牛VOC 的 XML 会用 xmin、ymin、xmax、ymax 给出绝对像素框YOLO 的 TXT 给出的是类别 id 和 center_x、center_y、width、height全部除以图像宽高做了归一化JSON 则可能是 COCO 那种 image_id 加 bbox 加 area 的组装方式也可能是一个更自由的数组结构。它们描述同一个目标但坐标系和字段名完全不同这就是为什么很多人解压后第一反应是“直接用”第二反应是被某个格式卡住。我在处理这类多格式数据集时先把三种格式的读取脚本各写一遍然后随机抽 50 张图做交叉验证。核心思路是不管最终用 YOLO 还是 Detectron2 训练都要先把三份标签统一到同一个数学空间中也就是像素坐标。YOLO 归一化坐标乘以真实宽高、VOC 绝对坐标直接用、JSON 里的 bbox 按它的定义换算三组框在图上叠加后用 OpenCV 画出来人工核对这一步能挡掉后面 80% 的诡异精度问题。2.2 解析 VOC XML重点不在读取在于 bndbox 的缺失保护import xml.etree.ElementTree as ET from collections import Counter def parse_voc_xml(xml_path): tree ET.parse(xml_path) root tree.getroot() filename root.findtext(filename) width int(root.findtext(size/width)) height int(root.findtext(size/height)) objs [] for obj in root.findall(object): name obj.findtext(name) if name is None: continue bndbox obj.find(bndbox) if bndbox is None: print(f[warn] {xml_path} 里有 object 但没有 bndbox) continue xmin float(bndbox.findtext(xmin)) ymin float(bndbox.findtext(ymin)) xmax float(bndbox.findtext(xmax)) ymax float(bndbox.findtext(ymax)) objs.append({name: name, bbox: [xmin, ymin, xmax, ymax]}) return {filename: filename, width: width, height: height, objs: objs} # 先抽 100 张统计类别名确认名字到底是 cow 还是 cattle counts Counter() xml_files os.listdir(voc_annotations)[:100] for xml_name in xml_files: d parse_voc_xml(fvoc_annotations/{xml_name}) counts.update([o[name] for o in d[objs]]) print(counts)解析逻辑不复杂真正的保护点在于findtext的 None 判断。标注环节经常埋雷某个object缺了bndbox某个 name 写成了Cow而不是全小写cow某个坐标写成了字符串带逗号。用 ET 的 findtext 比 find 再取 text 更稳因为前者天然返回 None 而不是抛异常。代码最后的 Counter 是推荐步骤先统计类别名再决定要不要在转换脚本里做映射避免出现cow、Cattle、牛三个名字并存的情况。抽样 100 张而不是全部跑是为了先快速确认标签口径全量跑会刷屏反而看不清。2.3 解析 YOLO TXT归一化坐标必须拿真实宽高当分母from PIL import Image def parse_yolo_txt(txt_path, img_path): img Image.open(img_path) img_w, img_h img.size objs [] with open(txt_path, r, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) 5: continue cls_id, cx, cy, bw, bh [float(p) for p in parts[:5]] if cx 0 or cy 0 or bw 0 or bh 0: print(f[warn] {txt_path} 有非法框: {line}) continue if cx 1.001 or bw 1.001: print(f[warn] {txt_path} 疑似未归一化坐标: {line}) continue xmin (cx - bw / 2) * img_w ymin (cy - bh / 2) * img_h xmax (cx bw / 2) * img_w ymax (cy bh / 2) * img_h objs.append({cls_id: int(cls_id), bbox: [xmin, ymin, xmax, ymax]}) return objs这段代码里最容易忽略的是把图片路径传进来而不是手写死一组宽高。数据集里如果混了 1920×1080 和 1280×720 两种分辨率归一化坐标本身没问题但还原成绝对坐标时用错一张图的分母框就会整体右移或下移。所以解析函数必须用 PIL 读 real size。值域检查cx 1.001是第二道防线有的标注工具在 json 转换或导出时忘了归一化会直接写绝对像素这类行越早拦下来越省时间。2.4 解析 JSON先看结构再写代码COCO 段式不是唯一形态import json def probe_json(path, max_depth3): with open(path, r, encodingutf-8) as f: data json.load(f) def walk(obj, depth): if depth max_depth: return if isinstance(obj, dict): for k, v in obj.items(): hint type(v).__name__ if isinstance(v, list) and len(v) 0: hint f{len(v)} of {type(v[0]).__name__} print( * depth f{k}: {hint}) walk(v, depth 1) elif isinstance(obj, list) and len(obj) 0: print( * depth f[list] len{len(obj)}) walk(obj[0], depth 1) walk(data, 0) probe_json(annotations.json)JSON 是所有格式里最自由也最容易写坏的。它可能是标准 COCO 的images、annotations、categories三段式也可能只是把每张图的框直接塞进一个顶层数组。如果拿 COCO API 直接加载一个非 COCO 结构报错会非常抽象。上面这个探针脚本就是训练前的快速体检打了键名和类型三秒内判断能不能用 pycocotools。确认是 COCO 结构后再解析 categories 拿类别 id 对照表把 id 和 VOC 的 name 对齐这是后面做 json 转换和跨框架评测的基础。2.5 三种格式的类别对齐谁来决定类别 id这个坑不显眼但影响致命。YOLO TXT 里只有整数 id类别名被压缩掉了VOC 的 XML 里只有字符串名字JSON 的 categories 又可能和两者都不一样。三份标签如果各自定义了一套 id训练时类别就错位了。我在拿到数据集后会先做一个 category mappingvoc_names [cow, sheep] # 从 XML 里统计出来 yolo_id_to_name {0: cow, 1: sheep} # 从训练类别文件抄出 json_categories {1: cow, 2: sheep} # 从 JSON categories 段解析 for name in voc_names: if name not in yolo_id_to_name.values(): print(f[err] YOLO 里缺少类别 {name}) if name not in json_categories.values(): print(f[err] JSON 里缺少类别 {name})这段脚本本身不是算法但它是所有转换的前提。只要有一个格式的类别名不一致后面转换出的训练集就是错的。判断哪一份是对的我一般以 VOC XML 为准因为名字可读性最强错了容易肉眼发现以 JSON 为交换基准因为 COCO 的 categories 能承载更多信息。两边核对完再把 YOLO 的 id 映射表同步过来保证整个项目里只有一个权威类别表。这套对齐动作做完三种格式才能真的互相翻译。3. 用 3538 张图片跑通第一轮训练VOC 转 YOLO 与最小命令3.1 目录结构哪种摆放方式能同时喂给 YOLOv8 和 Detectron2smart-pasture/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ ├── voc_annotations/ │ ├── train/ │ └── val/ ├── json_annotations/ │ ├── train/ │ └── val/ ├── data.yaml └── category_map.json训练只吃labels下的 TXT但不要因为只吃 TXT 就把 XML 和 JSON 删掉。后续做模型对比评测时很多检测框架要 VOC 格式的 XML 作为评估输入交付给上游系统时对方往往只收 JSON。保留原始格式是为了不再转一圈也为了给category_map.json留一份权威的类别映射记录。目录结构上我习惯让 images 和 labels 严格对齐train 里出现的每张图都必须有对应的同名 txt否则训练脚本会静默跳过。3.2 用脚本批量把 VOC XML 转成 YOLO TXTimport os from PIL import Image import xml.etree.ElementTree as ET CLASS_MAP {cow: 0, sheep: 1} def voc_xml_to_yolo_txt(xml_path, txt_path, img_w, img_h): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): name obj.findtext(name) if name not in CLASS_MAP: print(f[skip] {xml_path} 里不认识的类别 {name}) continue bndbox obj.find(bndbox) xmin float(bndbox.findtext(xmin)) ymin float(bndbox.findtext(ymin)) xmax float(bndbox.findtext(xmax)) ymax float(bndbox.findtext(ymax)) if xmin xmax or ymin ymax: print(f[warn] {xml_path} 存在宽高为 0 的框已跳过) 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{CLASS_MAP[name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) with open(txt_path, w, encodingutf-8) as f: f.write(\n.join(lines)) xml_dir voc_annotations/train txt_dir labels/train os.makedirs(txt_dir, exist_okTrue) for xml_name in os.listdir(xml_dir): if not xml_name.endswith(.xml): continue base xml_name[:-4] img Image.open(fimages/train/{base}.jpg) w, h img.size voc_xml_to_yolo_txt( os.path.join(xml_dir, xml_name), os.path.join(txt_dir, base .txt), w, h, )转换逻辑是VOC 绝对坐标先算出中心点和宽高再分别除以图像真实宽高得到 YOLO 的归一化五元组。两个地方不要自作主张改成固定 640训练时图像会被缩放到输入尺寸但标签归一化是对原始图做的和网络输入无关。CLASS_MAP必须和 data.yaml 的类别顺序一致否则训练出来的类别语义是错位的。脚本里遇到不认识的名字直接 skip 并打日志方便回头检查哪些图被漏了。转换完随手数一下生成的文件个数和 XML 数量对得上再继续。3.3 划分 train/val按文件名不留重复import os import random import hashlib images [f for f in os.listdir(images) if f.endswith(.jpg)] random.seed(42) random.shuffle(images) val_count int(len(images) * 0.15) val_set set(images[:val_count]) train_list images[val_count:] # 检查是否有完全重复的图防止同图同时进 train 和 val all_bases {} for img_name in images: content open(fimages/{img_name}, rb).read() md5 hashlib.md5(content).hexdigest() all_bases.setdefault(md5, []).append(img_name) dups {k: v for k, v in all_bases.items() if len(v) 1} print(f重复图片组数: {len(dups)}) for md5, names in list(dups.items())[:5]: print(names)这里有个血泪经验同一个场景连拍的图文件大小几乎一样但内容不同肉眼难分直接吃进训练集和验证集就会造成数据泄漏。所以我会加一层 md5 查重把完全一样的图识别出来确保同一份图不会既在 train 又在 val。划分比例上3538 张按 85:15 差不多够用如果要更严谨可以把 val 再拆一半当 test最后跑推理时验证泛化。文件名按base去重这个步骤也不能省防止同一张图带了_1.jpg和_2.jpg两个副本。3.4 写 data.yaml 并跑通 yolov8n 最小训练path: smart-pasture train: images/train val: images/val names: 0: cow 1: sheepcd smart-pasture # 首次运行需要准备预训练权重yolov8n.pt 可以提前下载放到项目目录 yolo detect train \ datadata.yaml \ modelyolov8n.pt \ epochs50 \ imgsz640 \ batch16 \ projectruns/pasture_cow_sheep \ nameexp1这一条命令能跑通前提是 images 和 labels 目录严格按 YOLO 习惯摆放train 的标签 txt 文件名必须和图片名一致且扩展名不同一张都不能差。data.yaml 里的 names 顺序必须和 TXT 里的 id 完全对应差一位整个训练结果就是反的。batch16在 3538 张规模下属于稳妥值如果显存只有 6G降到 8imgsz640是按检测速度与精度的折中远景羊群小目标多的时候可以试 960但训练时间会涨很多。这轮跑完后看runs/pasture_cow_sheep/exp1里的 results.csvmAP50 能到多少心里有数了。4. 数据划分与类不均衡牧场场景最影响 mAP 的细节4.1 划分策略随机划分没问题但要防同场景连拍图串集牧场的图经常是从视频里抽帧出来的同一群羊连续几十帧高度相似。如果随机划分train 和 val 里可能都出现同一时刻的帧指标虚高换个时段换个相机位就崩。解决办法是按文件名前缀分组划分import os import random # 假设文件名格式是 cam01_20240115_143200_001.jpg前三段是相机、日期、分钟 groups {} for img in os.listdir(images): parts img.split(_) group_key f{parts[0]}_{parts[1]}_{parts[2]} groups.setdefault(group_key, []).append(img) group_names list(groups.keys()) random.seed(7) random.shuffle(group_names) val_groups set(group_names[: int(len(group_names) * 0.15)]) train_imgs [] val_imgs [] for g, imgs in groups.items(): if g in val_groups: val_imgs.extend(imgs) else: train_imgs.extend(imgs) print(ftrain {len(train_imgs)} val {len(val_imgs)})核心思路是把划分的单位从“单张图”升级为“一个场景组”因为视频抽帧连拍会让验证集被同一场景的相似背景“白嫖”出一个虚高 mAP。命名规则不是cam01_日期_分钟的话就换成其他分组键比如按拍摄时段分或者按区域分。3538 张的数据量下宁可牺牲一点训练集规模也要把划分单位变成场景组这是投入产出比最高的一层防护。4.2 羊多牛少mAP 怎么解读才靠谱训练时 loss 会被多数类主导羊的帧占比高牛的 recall 偏低。再加上牛通常离镜头更近、框更大羊群多在远处、框小且互相遮挡这两个类别已经不只是数量差异而是尺度差异。python - EOF from ultralytics import YOLO model YOLO(runs/pasture_cow_sheep/exp1/weights/best.pt) metrics model.val(datadata.yaml) for i, name in metrics.names.items(): print(f{name}: mAP50 {metrics.box.ap50[i]:.3f}) EOF这段用训练完的最佳权重在 val 上重新评估直接按类别输出 ap50。如果 cow 的 ap50 是 0.6 而 sheep 是 0.9说明这个模型上线后对牛的漏检风险更高只看总 mAP 会被羊的表现掩盖。针对类别不均衡第一优先不是改损失函数而是先确认数据增强和尺度牛框大但不代表好检测背对镜头的牛很容易只框住一半。可以在训练命令里加class_weights参数给牛加权或者对牛类做更强的随机裁剪我一般先从前者试成本最低。4.3 图像预处理EXIF 旋转与灰度图先统一手机和无人机拍的 JPG 经常带 Orientation 标签OpenCV 读出来是横的但标签坐标是按竖图标的直接训练等于所有框都旋转了 90 度。灰度图和彩色图混存也会影响输入通道的一致性。from PIL import Image, ImageOps import os def normalize_image(src, dst): img Image.open(src) img ImageOps.exif_transpose(img) # 按 EXIF 方向扶正 if img.mode ! RGB: img img.convert(RGB) # 灰度图统一转 RGB img.save(dst, quality95) os.makedirs(images, exist_okTrue) for img_name in os.listdir(images_raw): normalize_image(fimages_raw/{img_name}, fimages/{img_name})这一步我一般放在最前面做因为如果 EXIF 旋转没处理后面的标签转换、可视化、训练全部建立在错误方向上。PIL 的exif_transpose会读取 JPEG 的 Orientation 字段把图转正转完后标签坐标依然按原图标注的像素值但图本身方向变了所以必须用转正后的图重新算标签。灰度图转 RGB 不转也能训但会增加很多莫名其妙的波动统一转出去最省心。5. 避坑用这张数据集时常见的五个翻车现场5.1 现象训练时 loss 正常掉但验证集 mAP 一直趴在地上原因是划分泄漏的反面train 和 val 里的图片虽然是不同的文件但很多是同一场景连续抽帧视觉上几乎一样。训练集学到的背景特征在验证集里也大量存在可一旦换个时段、换个相机位精度立刻崩。解决方法是先做 md5 查重再按文件名分组划分保证同一个拍摄片段整体进 train 或整体进 val。3538 张的数量下先查重再划分是必须的不要跳过。5.2 现象XML 和 TXT 框对不上图上的牛位移了半个身位原因是部分图像被预处理过尺寸但只有一份标签跟着做了缩放另一份还停留在原图坐标。特别是 JSON 里如果混了两种标注口径转出来的框就会整体偏移。解决方法是建立一个“以像素绝对坐标为准”的校验管线把 XML 还原的框、TXT 还原的框、JSON 还原的框分别画在同一张图上逐一目检错位图。脚本里对位移超过 5 像素的样本自动报警再回原图确认哪一份标签才是基准。5.3 现象一张图上 30 只羊训练时 batch 直接 OOM原因不一定是显存不够而是默认的 mosaic 增强会把 4 张小图拼成一张等于一张训练图上同时出现上百个目标框损失函数计算量和显存占用暴涨。在羊群密集场景里这是最常见的翻车点。解决方法是把mosaic关掉或者调低batch在训练命令里加mosaic0.0先跑通再用小步长从 0.5 开始试。3538 张的数据量下mosaic 带来的增益并没有网上说的那么夸张密集小目标场景宁可先保 batch。5.4 现象JSON 里出现空数组数据加载器当场抛错原因是对应的图片可能没有目标标注工具生成了空 list也可能是 json 转换过程中把某些行过滤掉了。最隐蔽的是 JSON 的 annotations 数组里有一条记录其 image_id 指向的图片不在 images 里跑数据检查时经常被忽略。解决方法是遍历 JSON 所有 image_id和实际文件列表做差集把断链的样本清出去。空标注图根据情况处理如果训练框架支持空图保留作为背景样本如果不支持就移除并在数据列表里标记。5.5 现象换机器训练后精度下降看起来像玄学原因多数是环境相关OpenCV 版本变了解码出来的像素有细微差异也可能是换了机器后预训练权重路径没配训练从随机初始化开始而不是沿用之前的权重。解决方法是固定 requirements 版本把预训练权重和训练出的 best.pt 都放进项目目录不要依赖全局缓存。排查时先把验证阶段的可视化图打出来对比相同一张图的预测框是否稳定能快速缩小范围。这类问题不是模型的问题环境一致性和权重路径先确认别急着调学习率。6. 训练前的质量门禁三格式可视化验证与多任务进阶6.1 画框可视化先看 50 张再谈训不训from PIL import Image, ImageDraw def draw_boxes(img_path, boxes, out_path): img Image.open(img_path).convert(RGB) draw ImageDraw.Draw(img) for box in boxes: x1, y1, x2, y2 [int(v) for v in box] draw.rectangle([x1, y1, x2, y2], outline(0, 255, 0), width3) draw.text((x1, y1 - 10), cow, fill(255, 0, 0)) img.save(out_path)这个脚本不需要训练直接在数据集上跑。我习惯抽查三个地方密集羊群边缘的羊有没有漏标、牛栏里背对镜头的牛框是否夹住身体、远景小目标的框有没有只框住半边。漏标比错标更危险因为训练时漏标的区域会被当成背景模型学出来就是漏检。抽查 50 张没问题再进训练比训练完再返工省一个晚上。6.2 用抽检脚本监督三份标签的一致性def get_iou(a, b): ix1, iy1 max(a[0], b[0]), max(a[1], b[1]) ix2, iy2 min(a[2], b[2]), min(a[3], b[3]) inter max(0, ix2 - ix1) * max(0, iy2 - iy1) area_a (a[2] - a[0]) * (a[3] - a[1]) area_b (b[2] - b[0]) * (b[3] - b[1]) union area_a area_b - inter return inter / union if union 0 else 0 # xml_box 来自 2.2yolo_box 来自 2.3json_box 来自 2.4 iou_xy get_iou(xml_box, yolo_box) iou_js get_iou(xml_box, json_box) if iou_xy 0.9 or iou_js 0.9: print(f[error] img {img_id} 三格式不一致 IOU: {iou_xy:.3f} {iou_js:.3f})三份标签对同一个框应视为同一目标进行匹配最快捷方式是中心点最近匹配。IOU 阈值用 0.9低于它就人工介入。这一步能在训练前把噪音清掉是我每次拿到新数据集都要走一遍的流程。6.3 进阶把三种标注喂给多任务网络三份标签的长期价值在于TXT 用来做主检测任务JSON 里的附加字段做属性或遮挡的辅助头XML 做跨框架评测或导出。我现在做一个牧场项目习惯是保留三份原始标签不删训练脚本只吃 TXT评测脚本吃 XML交付接口吃 JSON。将来要加“羊只计数 个体状态识别”直接在 JSON 上加一个 key不用再动标注流水线。这个习惯帮我省过好几次返工——当你以为 XML 删了就删了结果评测方突然要 PASCAL VOC 格式的结果就得重新转。保留原格式就是后悔药这个原则我现在放在所有数据项目的第一位。希望帮到你。本文还有配套的精品资源点击获取
返回列表