自己做猪舍巡检项目的时候,最头疼往往不是模型结构,而是标注数据。尤其是“猪只行为识别”这种偏垂直的场景,公开数据集本来就少,想找一份带像素级标注、又是实际圈舍环境拍摄的料,基本靠运气。最近从一个朋友那边拿到一份“智慧养殖猪只行为识别分割数据集,labelme格式,1085张,4类别”,翻来覆去用了一周,给我的感觉是:这类数据集的参考价值,很多时候比模型框架本身还要稀缺。这篇就聊点实在的——这个数据集背后到底怎么用、labelme格式怎么解析、怎么转成训练能用的格式,以及我在实际过程中踩过的那些坑。
1. 这个数据集到底解决什么问题
1.1 智慧养殖里为什么要做“行为识别”
养猪这个行业,过去靠人的眼睛和经验,现在靠摄像头和算法。猪只的行为状态,比如说站立、行走、躺卧、进食,直接关系到它的健康指数、应激反应、发情状态,甚至圈舍环境是否合适。举个例子,一头猪长时间躺着不动,可能是发烧或者肢蹄有问题;一头猪反复站起又卧下,可能是环境温度不合适;一头猪频繁靠近饲槽但不进食,可能是饲料适口性或饮水系统出了问题。这些信号如果靠人工去盯监控画面,24小时不间断,不现实,所以需要用算法去做持续的行为分析。
行为识别本质上是一个“分类+定位”的任务:既要判断这是什么行为,还要在画面里找到对应的猪只个体。但如果只做检测级别的矩形框,问题马上就来了——圈舍里猪与猪之间经常叠在一起,栏位栅栏会遮挡身体,一个矩形框里通常包含好几头猪的部分区域。框这个东西,用在个体计数上还行,用在行为判断上就太粗了。所以很多落地项目里,我推荐的做法是把任务升级为实例分割:在像素级上把每一头猪的形状圈出来,再基于轮廓去判断行为。
1.2 为什么偏偏用“分割”标注,而不是目标框
这份数据集选择labelme格式、使用多边形轮廓标注,背后的逻辑其实不复杂。分割标注相比检测框,多出来的信息量非常大:
- 猪的身体轮廓能直接反映姿态,站立和躺卧在轮廓上的长宽比差异非常明显。
- 行为状态的判定往往依赖局部细节,比如“进食”这个类别,头部区域伸向饲槽的部分必须被精确区分出来,矩形框会把饲槽、栏杆一起框进目标里,反而干扰分类。
- 分割mask可以进一步计算面积、中心点、方向角度、形变程度等维度,这些是后续做行为时序分析的特征基础。
所以在实际项目中,我拿到一份分割数据集,哪怕第一版模型跑的是检测,最终也一定会往分割方向迁移。这就是这类“精细标注”数据集的价值所在。
2. 数据集的底细:1085张、4类别怎么落实到项目里
2.1 “4类别”的行为定义与边界
“4类别”是这份数据集最核心的信息。根据不同养殖场景的标注习惯,常见的行为分成以下几类:
| 类别 | 行为定义 | 标注要点 |
|---|---|---|
| standing | 站立状态,四肢支撑身体,身体基本垂直地面,没有明显位移 | 轮廓必须包含四肢的支撑区域,避免和躺卧混淆 |
| walking | 行走状态,四肢交替移动,位置发生明显变化 | 常伴随前后腿前后分开的轮廓特征 |
| lying | 躺卧状态,身体一侧或腹部接触地面,四肢弯曲或伸展 | 轮廓通常为扁平状,注意和阴影区域区分 |
| feeding | 进食状态,头部伸向饲槽,或正在咀嚼、吞咽 | 头部和颈部区域必须完整,这个类别的分割难度最高 |
我在实际标注中遇到过一个问题:“站立”和“行走”在单帧图像上其实不好区分。严格来讲,行走是一个时间序列动作,单帧里可能出现四肢支撑、身体微倾的瞬间,却看不出位移。所以这类数据集在制作时,通常会基于视频抽帧来辅助判断:如果这一帧前后几帧猪只位置发生变化,就标成walking;如果持续不动,标成standing。使用数据集时要注意,如果你自己也做时序模型,可以考虑把这两类合并成“active状态”,再单独用帧间差分去细分。
2.2 1085张到底算多还是少
1085张,说多不多,说少其实也不算太少。它属于典型的“中小规模专用数据集”。深度学习图像分割模型,如果用COCO这种大规模预训练权重做迁移学习,几千张级别就能微调出可用的效果;但如果是完全从零训练,这个量级肯定不够。
要判断这个规模是否够用,得看两个因素:
- 场景多样性。如果这1085张都来自同一个摄像头、同一个栏位、同一个时间段,那模型泛化到其他圈舍时会明显变差。这种数据集适合做“场景内验证”,不适合直接跨场部署。
- 标注的实例密度。一张图里如果有5到10头猪,那么总实例数可能在5000到10000个以上,单类别实例量还算可观。我拿到数据后第一件事就是统计每个类别的实例数量,而不是只看图片张数,因为分割模型学的是实例特征,不是图片特征。
如果实例数量偏少,后续可以用视频抽帧扩充、复制粘贴增强、跨场景微调等方式补数据。1085张本身不是天花板,而是一个起点。
2.3 训练集/验证集/测试集的划分策略
很多人拿到数据集直接按8:1:1随机切分,这是不推荐的。原因是同一段视频里抽出来的连续帧非常相似,随机划分会让验证集和训练集之间的信息重叠过高,导致指标虚高,模型实际泛化性被高估。
我的做法是:
- 先把所有图片按视频时间段或拍摄批次分组。
- 以组为单位划分,保证同一个时间段的图片只出现在一个集合里。
- 比例建议训练集70%、验证集15%、测试集15%,如果有条件,测试集最好来自完全不同的圈舍或日期。
这样划分出来的性能指标,才能真实反映模型在新场景下的表现。
3. labelme格式的完整拆解与格式转换实操
3.1 labelme标注文件长什么样
拿到数据后,你会发现每张图片旁边都带有一个同名.json文件,这就是labelme的标准输出格式。打开一个典型的文件,结构大致如下:
{ "version": "4.5.6", "flags": {}, "shapes": [ { "label": "lying", "points": [[512.3, 384.1], [521.7, 389.2], [533.2, 396.8]], "group_id": 1, "description": "", "shape_type": "polygon", "flags": {} }, { "label": "standing", "points": [[701.0, 302.4], [715.2, 310.6], [728.9, 312.3]], "group_id": 2, "shape_type": "polygon", "flags": {} } ], "imagePath": "frame_00123.jpg", "imageData": null, "imageHeight": 1080, "imageWidth": 1920 }几个关键字段值得注意:
shapes数组里每个元素就是一个实例对象。每个实例的label字段是类别名,points是多边形顶点坐标,shape_type一般是polygon。group_id表示实例的编号,同一头猪在不同帧里的group_id如果一致,说明标注者做了跨帧关联,这个信息很有价值。imageData通常为null,图片内容不会内嵌到JSON里,所以图片和JSON的路径匹配就变得非常重要。imageHeight和imageWidth是标注时的图像尺寸,但我在实际转换时不太信任这两个字段,因为可能存在缩放、裁剪后没更新的情况。
3.2 转成COCO JSON格式的脚本
现在的实例分割框架,不管是MMDetection还是Detectron2,都原生支持COCO格式。所以我拿到labelme数据后的第一步,通常是转成COCO JSON。这里给出一份可以直接改造的参考脚本:
import json import os import glob from PIL import Image from pycocotools import mask as maskUtils def labelme_to_coco(labelme_dir, output_path, category_mapping): json_files = glob.glob(os.path.join(labelme_dir, "*.json")) images = [] annotations = [] img_id = 1 ann_id = 1 for jf in json_files: with open(jf, encoding="utf-8") as f: label = json.load(f) img_path = os.path.join(labelme_dir, label["imagePath"]) img = Image.open(img_path) width, height = img.size images.append({ "id": img_id, "file_name": os.path.basename(img_path), "width": width, "height": height }) for shape in label["shapes"]: if shape["shape_type"] != "polygon": continue category_name = shape["label"] if category_name not in category_mapping: continue category_id = category_mapping[category_name] points = shape["points"] # COCO segmentation需要扁平化坐标 segmentation = [coord for pt in points for coord in pt] xs = [pt[0] for pt in points] ys = [pt[1] for pt in points] x_min, x_max = min(xs), max(xs) y_min, y_max = min(ys), max(ys) bbox = [x_min, y_min, x_max - x_min, y_max - y_min] # 用RLE计算面积比直接算多边形面积更稳 rle = maskUtils.frPyObjects([segmentation], height, width) area = float(maskUtils.area(rle[0])) annotations.append({ "id": ann_id, "image_id": img_id, "category_id": category_id, "bbox": bbox, "area": area, "segmentation": [segmentation], "iscrowd": 0 }) ann_id += 1 img_id += 1 coco_json = { "images": images, "annotations": annotations, "categories": [ {"id": idx, "name": name} for name, idx in category_mapping.items() ] } with open(output_path, "w", encoding="utf-8") as f: json.dump(coco_json, f) if __name__ == "__main__": # 类别名按JSON里的实际标注情况修改 mapping = { "standing": 1, "walking": 2, "lying": 3, "feeding": 4 } labelme_to_coco("./labelme_data", "./output_coco.json", mapping)这段脚本有几个容易忽略的细节:
- 我使用的是
Image.open重新读取图片宽高,而不是直接读JSON里的imageHeight和imageWidth。原因前面说了,JSON里的尺寸可能没同步更新。 - 用
pycocotools的RLE方式计算面积,可以避免多边形退化、自相交时面积计算不准的问题。 segmentation要求是二维列表的坐标展平形式,这里写的是[segmentation],形状是“单实例多边形”的标准格式,别漏了这一层括号。
3.3 转成YOLO分割格式的脚本
如果你用的是YOLOv8-seg这类模型,需要的是目标检测式的txt标注格式。每个实例一行,格式为:
class_id x1 y1 x2 y2 ... xn yn坐标全部归一化到0到1。转换代码如下:
def labelme_to_yolo_seg(labelme_dir, output_dir, category_mapping): os.makedirs(output_dir, exist_ok=True) for jf in glob.glob(os.path.join(labelme_dir, "*.json")): with open(jf, encoding="utf-8") as f: label = json.load(f) img_path = os.path.join(labelme_dir, label["imagePath"]) img = Image.open(img_path) w, h = img.size txt_name = os.path.splitext(os.path.basename(jf))[0] + ".txt" out_txt = os.path.join(output_dir, txt_name) with open(out_txt, "w") as f: for shape in label["shapes"]: if shape["shape_type"] != "polygon": continue category_name = shape["label"] if category_name not in category_mapping: continue class_id = category_mapping[category_name] norm_points = [] for x, y in shape["points"]: nx = round(x / w, 6) ny = round(y / h, 6) norm_points.extend([nx, ny]) line = f"{class_id} " + " ".join(map(str, norm_points)) f.write(line + "\n")YOLO分割格式有个小坑:多边形顶点顺序必须按顺序连接,并且第一个点和最后一个点不要重复存储。labelme里的polygon顶点通常已经是按边界顺序排列的,直接转换即可,但如果发现mask出现“打结”情况,要考虑检查顶点顺序是否逆时针或者是否出现了交叉点。
4. 从数据集到模型:训练和评估的实操路线
4.1 直接能跑起来的模型选择
在拿到这份labelme数据集之后,训练一个分割模型有几条路线可以选择,我按部署场景分别说下:
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Mask R-CNN | 服务器端离线分析 | 成熟稳定,社区资料多 | 推理速度一般,边缘设备跑不动 |
| SOLOv2 | 对实例数量敏感的场景 | 不需要框分支,掩码质量较高 | 对重叠严重区域偶尔会出现漏检 |
| YOLOv8-seg | 边缘设备实时检测 | 速度快,部署方便 | 分割边缘精细度略弱 |
| SAM微调 | 零样本预标注后精修 | 标注成本低,适合做新类别发现 | 训练成本高,微调流程复杂 |
我个人经验是:如果是刚拿到数据集做可行性验证,先用Mask R-CNN或YOLOv8-seg快速跑通一个baseline。不要一上来就堆结构复杂的模型,因为数据集质量还没有验证过,模型再复杂也补不了标注错误。先用baseline把问题暴露出来,再针对性地优化模型和清洗数据,这才是高效路径。
4.2 数据增强是做分割的关键
1085张的规模,直接训练很容易过拟合。分割任务的数据增强和检测不太一样,有个核心要求:图像变换和mask变换必须同步,否则标注就错位了。
生效的增强组合我建议这样配置:
- 随机水平翻转:猪只身体左右对称性高,翻转不会破坏语义,但注意不要翻转文本、水印等非对称区域。
- 小角度随机旋转(±10度以内):圈舍摄像头安装角度会有偏差,旋转增强能提升泛化性。
- 随机亮度对比度调整:猪舍的光照条件一天内变化明显,尤其是早晚侧光、走廊灯光反射。
- 随机裁剪缩放:模拟摄像头距离变化和画面构图差异。
- 马赛克增强:把多张图拼接成一张训练样本,对小数据集非常有效。
不推荐的增强包括:大幅度旋转(猪只会变成倒着的,姿态语义被破坏)、随机擦除(会把猪身体的一部分擦掉,行为类别信息丢失)、颜色反转(破坏物体视觉语义)。
4.3 训练参数与评估指标怎么设
训练分割模型,输入尺寸建议统一到模型可接受的范围。比如Mask R-CNN通常用1333×800的动态尺寸,YOLOv8-seg可以用1280×1280。对1085张的数据集来说,训练轮次不宜太少,至少要40到60轮,配合早停机制防止过拟合。初始学习率用2e-4附近,配合warmup策略,冻结骨干网络前几轮是标准做法。
评估指标方面,不要只看mAP@0.5。行为识别的核心诉求是“知道每头猪在干什么”,所以我会额外关注:
- 每个类别的mask AP,重点关注feeding是否显著低于其他类别,如果是,说明头部靠近饲槽的遮挡区域需要额外补充训练数据。
- 分类混淆矩阵,特别区分standing和walking之间、lying和standing之间的错误判断。
- mask边缘的IoU,分割结果边缘粗糙会把面积信息带歪,影响后续行为特征提取。
我在训练后习惯做一个可视化输出:把模型预测的mask叠加到原图上,与标注mask对照,按类别分别打印错误样本。这一步看起来费时间,但能帮你快速发现训练数据里的系统性问题,比盯着指标看有效得多。
5. 实际用这个数据集踩过的坑
5.1 标注和图像对不上的老问题
第一个踩到的坑是文件名匹配。labelme导出的JSON文件名和图片文件名通常是一致的,但偶尔会遇到图片被重命名、或者路径前缀修改的情况。加载数据时报错,一查才发现JSON里的imagePath还是旧路径。
解决办法很简单:在写数据加载脚本时,不要直接使用label["imagePath"],而是用JSON文件名去掉扩展名,再去对应目录找同名图片。如果找不到,再回退到JSON里的路径字段。这个双保险能避免绝大多数文件匹配错误。
5.2 类别不平衡怎么解决
在这类行为识别数据集中,类别不平衡几乎是必然的。躺卧类往往占了将近一半,因为猪群在舒适环境下大部分时间在睡觉;进食和行走类占比可能只有百分之十几。直接拿原始分布训练,模型会对躺着这个类别产生强烈偏置。
我处理类别不平衡有三招:
- 训练时对每个类别做采样器加权,让batch里各类别实例数量尽量均衡。
- 对少数类别的训练图做额外增强,比如把walking类的图片多复制几次副本,增强比例提高。
- 行为类别可以重新组织宏观类别,比如先训练一个“静态/动态”二分类器,再细分具体行为,这样每个子任务的数据分布会更平衡。
5.3 光照、遮挡、栏位干扰
圈舍环境里的分割难度,比想象中要高。猪栏的金属栅栏会横穿过猪的身体,牛羊猪腿被栅栏隔断是常见现象;地面水渍会反射灯光,在mask边缘形成高亮区域;红外摄像头图像缺乏颜色信息,纹理对比度低,分割模型在边缘区域的预测容易抖动。
应对这些干扰,我的建议是:
- 训练时不要把原图直接resize得太小,比如用1280分辨率,让栅栏横条和猪腿的边界保留足够像素。
- 后处理阶段可以采用条件随机场做边缘平滑,或者用形态学操作去除小块噪点。
- 对有严重遮挡的个体,优先保证分割主体躯干区域的准确性,四肢局部被遮挡可以阈值放宽,不要为了抠出被栅栏遮挡的腿而牺牲整体轮廓。
5.4 labelme工具本身的几个坑
labelme标注工具本身很轻量,但大规模使用时也会遇到一些琐碎问题:
| 问题 | 现象 | 解决方式 |
|---|---|---|
| 标注点过多 | 多边形顶点动辄几百个,文件体积大,转换慢 | 使用Douglas-Peucker算法做轮廓简化为20~50点 |
| JSON和图片不同名 | 加载时报缺失 | 按文件名匹配并校验 |
| 中文路径报错 | 读取失败或写入乱码 | 统一用英文目录,不要用中文和空格 |
| 标注遗漏目标 | 个别猪只没被标注 | 训练前做可视化抽查,对照原图逐张检查 |
| 标签名不统一 | 有的叫standing,有的叫stand | 用脚本把同义标签合并成固定词汇表 |
如果你打算用这个数据集做项目而我前面提到的那份转换脚本,建议先做一步可视化检查:把每个mask用随机颜色叠加到原图上,人工抽看30到50张,确认类别名、多边形贴合度、遮挡区域都合理后再进入训练流程。这一步看似增加时间,实际上能省下后面调试模型的大量精力。
我个人在拿到数据集之后,通常不会急着开训。我会先花半天时间做数据清洗、格式转换、可视化验证,把数据集的底摸清楚。这种小规模专用数据集,最大的价值不是“量大”,而是类别定义和场景针对性够强。只要你把标注解析、格式转换、类别平衡这些基础工作做扎实了,后面训练模型的过程会顺畅很多。如果后续有条件,建议基于视频流把单帧的mask关联成时序轨迹,那样做出来的行为识别系统,在落地场景里的实用性会有质的提升。