简介:一份面向智能养殖与动物行为学研究的猪只姿态识别数据集,聚焦猪只健康监测与福利评估场景。数据覆盖躺卧、睡眠、探索、进食、行走、骑跨六类常见姿态,训练集共9744张标注图片,验证集2518张,可作为姿态分类与行为检测模型的标准化训练和评测基准。压缩包共2000个JSON文件,含逐图标注结果与标签数量统计文件,整体约809MB,便于直接解析并接入深度学习流程。已有262人浏览学习。依托清晰的行为类别划分与统一标注格式,读者可快速开展猪只行为识别模型训练与对比实验,并应用于异常行为预警、精准饲喂优化等场景,为智慧养殖和动物福利研究提供可靠数据基础。
1. 猪姿态检测不是图像分类:PigBehaviorRecognitionDataset要解决什么
把猪的姿态和行为识别做成数据集,很多人第一反应是“这不就是给猪拍照片标个框吗”。真正进场后才发现,猪姿态检测面对的问题和常规目标检测完全不同:猪是活的、会动、会转身、会叠在一起,趴卧和倒地的区别直接决定养殖员要不要跑一趟猪舍。PigBehaviorRecognitionDataset这类数据集,核心价值就是把“站、卧、走、跑、食、斗”这类行为语义转换成带坐标标注的样本,让检测模型不光能回答“这里有一头猪”,还能回答“这头猪在干什么”。适合的读者很明确:做智慧养殖、畜牧AI、边缘端行为分析的同学,以及想用自己的数据微调行为检测模型的一线算法工程师。这篇笔记从数据集结构讲到训练落地,最后给到验证和避坑路径。
2. 先摸透行为标签体系:进PigBehaviorRecognitionDataset的第一件事
拿到任何一个行为识别数据集,先不要急着写加载脚本。第一步永远是读标签体系,因为它直接决定后续模型输出层的设计。猪姿态检测的行为类别通常不是单一维度的,它至少包含两个层级:姿态层和行为层。
2.1 行为类别怎么定:姿态层和行为层要分开看
姿态层描述的是空间形态,比如站立、趴卧、躺卧、坐立;行为层描述的是带语义的动作,比如行走、奔跑、进食、饮水、争斗、爬跨。姿态可以靠单张图像判断,行为很多时候必须依赖连续帧。PigBehaviorRecognitionDataset这类数据集,通常把两者混在标签文件里,但训练时要分开处理。我的习惯是先统计类别分布,看哪些类样本太少,再决定是合并类别还是做重采样。
import json from collections import Counter with open("annotations.json", "r", encoding="utf-8") as f: anns = json.load(f) label_counter = Counter() for ann in anns["annotations"]: label_counter[ann["category_id"]] += 1 print("类别分布:", label_counter.most_common())这个脚本的核心是把标注文件里每个类别出现的总次数统计出来。如果发现“奔跑”只有几十条而“趴卧”有几千条,训练出来的模型基本会对奔跑视而不见。后续处理无非两条路:要么用类别权重给少数类更高的loss权重,要么把奔跑和行走合并成“移动”。我在实际项目里更倾向于后者,因为猪的奔跑和行走在二维图像上本来就容易混淆,合并后模型稳定性更好。
2.2 标注坐标的两种主流约定:边界框还是关键点
猪姿态检测数据集的标注格式,目前常见的有两种。第一种是常规的边界框(x_center, y_center, width, height),适合做目标检测的迁移学习;第二种是关键点格式,会额外标出猪的头部、躯干、四肢位置,适合做姿态估计。PigBehaviorRecognitionDataset如果带关键点,你会发现它比纯框标注复杂得多,因为猪的四肢在趴卧姿态下大量自我遮挡,标错一个关节,模型学到的就是错误的空间关系。
这里有一个非常实际的边界坑:边界框坐标系,YOLO格式是归一化到0到1的(中心点x、中心点y、宽度w、高度h),而COCO格式是像素坐标(左上角x、左上角y、宽度w、高度h)。同样一份标注,用错解析方式,模型训练出来的mAP直接掉一半,而且很难排查,因为loss曲线看起来是正常的。我一般会在数据加载脚本里写一个强断言,检查所有坐标是否在合法范围内,把这个坑提前暴露出来。
2.3 标签文件解析范例:把框和类别映射到可训练样本
不管原始标注是JSON还是XML,落地的时候我一般会统一转成YOLO格式的txt文件,因为YOLO系列的训练生态对这个格式支持最好。下面这段是把COCO风格JSON转成YOLO训练目录的参考实现。
import os, json import numpy as np def coco_to_yolo(coco_path, img_dir, out_dir): with open(coco_path, "r", encoding="utf-8") as f: coco = json.load(f) img_id_to_name = {im["id"]: im["file_name"] for im in coco["images"]} cat_id_to_label = {cat["id"]: idx for idx, cat in enumerate(coco["categories"])} os.makedirs(out_dir, exist_ok=True) for ann in coco["annotations"]: img_name = img_id_to_name[ann["image_id"]] img_path = os.path.join(img_dir, img_name) h = coco["images"][0]["height"] # 注意:实际要按image_id查 w = coco["images"][0]["width"] x, y, bw, bh = ann["bbox"] x_center = (x + bw / 2) / w y_center = (y + bh / 2) / h bw_norm = bw / w bh_norm = bh / h label = cat_id_to_label[ann["category_id"]] out_txt = os.path.join(out_dir, img_name.replace(".jpg", ".txt")) with open(out_txt, "a", encoding="utf-8") as f: f.write(f"{label} {x_center:.6f} {y_center:.6f} {bw_norm:.6f} {bh_norm:.6f}\n")注意这里我故意留了一个bug风险提示:coco["images"][0]只取了第一张图的尺寸,如果数据集里图像分辨率不统一,所有标注都会错位。正确的做法是先用image_id去查对应图像的宽高,再计算归一化坐标。这也是猪姿态数据集最容易踩的暗坑之一——猪舍监控画面常常是不同摄像头的分辨率混合在一起。出现这个问题时,训练loss不降或者精度极低,但标注文件看起来又没问题,非常难排查。
3. 把猪姿态数据集喂进YOLO:数据清洗、划分与训练全流程
数据集解析通了,下一步就是训练。这里我不讲从零写网络,而是直接走YOLOv8这条最稳妥的路线。猪姿态检测的样本量通常只有几千到几万张,从头训练一个检测器不现实,迁移学习用预训练权重是唯一靠谱的做法。
3.1 数据划分不能随机切:按猪舍或视频片段分组
猪姿态检测最隐蔽的坑就是数据泄漏。同一头猪在连续视频帧里几乎一模一样,如果随机划分train和val,模型实际上是在“背答案”,验证集精度虚高。正确的划分方式是按视频片段或者按猪舍分组,保证同一头猪的样本不会同时出现在训练集和验证集里。这个原则在行人重识别领域是老规矩,但在猪姿态检测里很多人会忽略。
import os import random from collections import defaultdict video_to_images = defaultdict(list) for fname in os.listdir("images"): video_id = fname.split("_frame_")[0] # 假设文件名格式是 videoID_frame_timestamp.jpg video_to_images[video_id].append(fname) video_ids = list(video_to_images.keys()) random.shuffle(video_ids) train_videos = video_ids[:int(len(video_ids) * 0.7)] val_videos = video_ids[int(len(video_ids) * 0.7):int(len(video_ids) * 0.85)] test_videos = video_ids[int(len(video_ids) * 0.85):] def write_split(video_list, split_name): with open(f"{split_name}.txt", "w") as f: for vid in video_list: for img in video_to_images[vid]: f.write(f"images/{img}\n") write_split(train_videos, "train.txt") write_split(val_videos, "val.txt") write_split(test_videos, "test.txt")这段划分脚本的核心是把同视频帧绑在一起,而不是逐张随机抽。如果你的数据集文件名没有明确的分组信息,建议回看数据集的readme里有没有提供拍摄批次、猪舍编号这类元数据。没有的话,只能按时间戳聚类——连续时间内出现在同一镜头的样本视为同组。
3.2 训练命令和关键参数:epochs、batch和imgsz怎么定
数据划分完成,YOLOv8的训练命令很短,但参数选择有讲究。我一般会先把图像统一缩放到640或768,batch size根据显存来定。猪姿态数据集的图像往往来自监控摄像头,画质不高,盲目用1280的大分辨率反而会让模型过拟合到噪点上。
yolo detect train \ data=behavior.yaml \ model=yolov8m.pt \ epochs=100 \ batch=16 \ imgsz=640 \ patience=15 \ device=0 \ workers=4 \ pretrained=Truepretrained=True是关键,用COCO预训练权重做初始化。patience=15表示验证集指标连续15轮不提升就早停,防止猪姿态这种小数据集过拟合。如果你的显存只有8G,把batch降到8,imgsz降到512,优先保证跑得完一个epoch再看loss曲线。
3.3 训练前要检查的behavior.yaml配置
behavior.yaml是YOLO训练的数据集描述文件,内容直接决定训练能不能起来。最容易出错的地方是路径写错和类别索引不一致。标注文件里的类别ID是从0开始连续编号,yaml里names的列表顺序必须和它完全对应。
path: /data/pig_behavior train: train.txt val: val.txt test: test.txt nc: 8 names: 0: standing 1: lying 2: walking 3: running 4: eating 5: drinking 6: fighting 7: mountingpath建议写绝对路径,相对路径在YOLO里经常因为运行目录不同而报错,这个错误困扰了很多人很久,明明文件都在就是找不到。训练开始后第一步不是看mAP,而是看第一个epoch的loss有没有正常下降。如果loss直接是nan,检查标注坐标是不是有负数或者大于图像尺寸的异常值。
4. 猪姿态检测常见问题与避坑:五个翻车现场复盘
数据集质量决定模型上限,这句话在猪姿态检测上体现得格外明显。以下五条都是实际项目中反复出现的坑,按现象到原因到解决的顺序记录。
4.1 趴卧姿态的框总是漏检
现象是模型对站立和行走检测得很好,但趴卧的猪经常漏检,召回率掉到30%以下。原因是趴卧时猪的轮廓和地面背景融为一体,边界框包含大量背景像素,特征不够显著。另一个隐藏原因是部分趴卧样本被错误标注成了“躺卧”或“休息”,标签内部不一致。
解决办法分两步。第一步是做标签质量审计,随机抽200张趴卧图人工复核,确认标注类别没有漂移。第二步是做数据增强:对趴卧类样本加随机光照扰动和对比度扰动,让模型学会在不依赖颜色纹理差异的情况下识别轮廓。
4.2 训练loss正常下降,验证mAP却纹丝不动
现象是训练集loss在20轮内从2.0降到0.5,验证集mAP一直卡在0.3上下。这大概率是数据泄漏的反向问题——训练集和验证集太相似,模型学的是“记住画面”而不是“学会识别”。也可能恰好相反:验证集太难,包含了大量训练集没有的极端角度。
我的排查顺序是先看验证集图像和训练集图像是否来自同一批视频的相邻帧,如果是,重做数据划分;如果划分没问题,检查验证集中是否有遮挡严重的样本,把遮挡面积超过60%的标注框列为hard case单独统计,不要混在总体指标里。
4.3 猪叠在一起时检测框打架
现象是两头猪重叠时,模型只输出一个框,或者两个框剧烈抖动。原因是NMS(非极大值抑制)阈值设置太激进,重叠区域大的两个同类目标被当成一个目标抑制掉了。
解决方法是把NMS的IoU阈值从默认的0.7调低到0.4到0.5,让同一区域的重叠框被更激进地抑制是不对的——恰恰相反,调低IoU阈值意味着更严格的抑制条件,反而会杀掉更多框。这里要讲清楚:NMS的IoU阈值代表“两个框重叠多少才认为指向同一物体”,阈值越低,对重叠越敏感,两个猪框只要稍有重叠就会被合并。所以实际做法是把阈值调高到0.6左右,允许部分重叠的框同时保留,再配合置信度过滤。
yolo detect val \ model=runs/detect/train/weights/best.pt \ data=behavior.yaml \ conf=0.35 \ iou=0.6验证时把iou调到0.6,置信度阈值conf放到0.35。如果框的数量明显增多但误检也增多,就按场景反复试0.5到0.7之间的值。这个参数在猪舍场景里属于玄学范围,没有最优解,只能结合监控画面的实际密度做调优。
4.4 夜间红外画面下模型全面失效
现象是模型在白天画面的mAP有0.85,切到夜间的红外监控画面直接掉到0.2。原因是训练集里红外样本数量极少,模型学到的全是彩色纹理特征。我在处理这类情况时,会先统计数据集中红外和可见光图像的比例。如果红外样本确实稀缺,第一选择不是硬调参,而是引入红外图像的伪彩色变换做数据增强,将可见光样本模拟成灰度或热成像风格。
另外一个原因是摄像头切换时分辨率变了,比如白天用400万像素,夜间自动切成200万像素。模型对尺度突变非常敏感,训练时用多尺度增强可以有效缓解:在YOLO的训练参数里把scale=0.5下调到scale=0.3,减少尺度的随机扰动范围,保证模型看到的尺寸分布更稳定。
4.5 标注类别分布严重不均衡,少数类直接学不动
现象是“爬跨”这类行为只占全部样本的1%到2%,模型除了漏检,还会把站立误报成爬跨。解决这类问题我推荐一个组合拳。第一步是类别重加权,在loss计算里给少数类更高的权重;第二步是过采样,把少数类样本复制到3到5倍,但要注意不要在同一步骤里和随机裁剪增强叠加,那样会产生太多高度相似的重复样本。第三步是收集更多hard case——专门筛出容易混淆的站姿和爬跨姿态的过渡帧,这比盲目扩充总数更有效。
5. 验证模型到底检测得准不准:mAP之外的检查方法
训练结束看mAP只是第一步。猪姿态检测的落地价值在于行为统计的准确性,而mAP衡量的是单帧框定位能力,和“行为识别准不准”是两个维度。这一节给出三组更贴近实际使用的验证手段。
5.1 按行为类别看混淆矩阵,别只看整体指标
整体mAP高不代表每个类别都好。按类别输出混淆矩阵,才能真正看到哪个行为被模型混淆了。YOLO在验证后会在runs/detect/val目录里生成混淆矩阵图,但我更喜欢自己写一行代码输出数值矩阵,方便对比版本迭代。
from pathlib import Path import numpy as np confusion = np.load("runs/detect/val/confusion_matrix.npy") class_names = ["standing", "lying", "walking", "running", "eating", "drinking", "fighting", "mounting"] for i, name in enumerate(class_names): top_confused = np.argsort(confusion[i])[-3:][::-1] confused_names = [(class_names[j], round(confusion[i][j], 3)) for j in top_confused if j != i] print(f"{name}: 最容易混淆 -> {confused_names}")这个脚本把每一行里除了自己之外最高的三个混淆类别打出来。如果“running”和“walking”互相混淆严重,说明类别定义本身边界不清,需要回看视频重新讨论行为标注口径,而不是继续调模型。
5.2 视频序列上的时间一致性检查
单帧检测对了不算对,行为识别在视频里必须有时序一致性。一头猪在连续50帧里不应该出现“趴卧、站立、趴卧、站立、趴卧”这种高频跳变。检查方法很简单:取一段连续的验证视频,逐帧跑模型,统计每头猪的行为标记切换次数。
import cv2 from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") cap = cv2.VideoCapture("val_videos/pig_pen_03.mp4") prev_labels = {} switch_count = 0 total_frames = 0 while cap.isOpened(): ret, frame = cap.read() if not ret: break results = model(frame, conf=0.35, verbose=False) frame_labels = {} for box in results[0].boxes: cls_id = int(box.cls[0]) track_id = int(box.id[0]) if box.id is not None else -1 frame_labels[track_id] = cls_id for track_id, cls_id in frame_labels.items(): if track_id in prev_labels and prev_labels[track_id] != cls_id: switch_count += 1 prev_labels[track_id] = cls_id total_frames += 1 cap.release() print(f"行为切换次数: {switch_count}, 总帧数: {total_frames}")注意这个脚本里box.id只有在开启了跟踪模式model.track()时才会有值,普通model()检测不会输出track_id。实际操作是把模型换成model.track(frame, persist=True)。如果行为切换频率超过了人类标注员的切换频率,说明模型过拟合了纹理变化,需要回到数据层面补充中间状态的训练帧。
5.3 用伪标签迭代扩充hard case样本
验证阶段往往能收集到大量模型置信度低、但人工一眼能分辨的样本。把这些样本挑出来,人工修正后加入训练集,这个闭环比换网络结构有用得多。伪标签的逻辑是:让模型先跑一遍,输出置信度在0.3到0.6之间的检测框,框出那些模型犹豫不决的猪只。人工只用看一小批被筛选出来的片段,不用从头标注全部数据。
from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") results = model.predict( source="unlabeled_frames/", conf=0.3, save_txt=True, save_crop=True, project="hard_cases" )关键在于conf=0.3这个低置信度阈值——低于生产环境的0.35到0.5。这样筛选出来的不是模型能轻松搞定的样本,而是模糊地带。我一般从这批hard case里再人工挑有效样本,通常500到1000张就能带来几个点的mAP提升,比多训练100个epoch划算得多。
6. 进阶技巧:把单帧检测延展成行为时长序列
检测模型给的是单帧结果,但养殖场景真正需要的是行为持续时长、频次这类统计指标。这一节讲从检测结果到行为序列的具体做法。
6.1 用IOU匹配给猪只分配ID,做帧间关联
最简单的关联方案就是基于IoU的贪心匹配:对相邻两帧的检测框,计算IoU矩阵,每次取IoU最大的配对,超过阈值则认为同一头猪。这个方案的优点是没有额外依赖,单张显卡就能跑;缺点是在猪叠在一起时会跟丢。要进一步提升稳定性,正式的方案是引入ByteTrack这类跟踪器,它们的核心思路是把低置信度框也纳入匹配代价计算,利用物体运动的连续性做补偿。
import numpy as np from scipy.optimize import linear_sum_assignment def match_detections(prev_boxes, curr_boxes, iou_thresh=0.3): iou_matrix = compute_iou(prev_boxes, curr_boxes) row_ind, col_ind = linear_sum_assignment(-iou_matrix) matches = [] for r, c in zip(row_ind, col_ind): if iou_matrix[r, c] >= iou_thresh: matches.append((r, c)) return matcheslinear_sum_assignment求解的是全局最优匹配,比贪心逐个取最大值要稳定。iou_thresh建议设在0.3到0.5,猪的移动速度越快,这个阈值就要越低,因为相邻帧之间的位移变大,框的重叠变少。
6.2 行为片段的切分与去抖动
有了稳定的ID序列,行为统计就变成对每个ID的标签序列做后处理。常见做法是中值滤波去掉单帧抖动,再合并连续的相同行为段。注意中值滤波的窗口不能太大,通常取5到7帧,窗口太大会把真实的短时行为(比如一次快速的站立)抹掉。
到这里想分享一个实际教训:最初我给猪做行为时长统计时,直接按模型输出的每帧标签累计时长,结果“站立”时长被高估了约20%,原因是模型在站立和趴卧的过渡帧上跳来跳去,每次跳变都算成了一次站立。后来加入状态机约束——只有连续5帧保持在同一个行为标签,才认为行为真正切换——数值一下子贴合人工记录了。这个阈值也就是上面提到的中值滤波与状态机结合的思路。希望这篇笔记能帮你在猪姿态检测的数据处理和模型训练上少走弯路。
本文还有配套的精品资源,点击获取