1. 项目背景与核心需求拆解1.1 智慧养殖里的“猪只行为识别”到底要解决什么问题做了几年计算机视觉我从人脸检测、安防监控一路做到农业场景最后一头扎进猪场。说实话猪只行为识别这个方向外行听起来很“偏门”但实际落地的价值非常大。规模化养殖里每头母猪、育肥猪的采食量、饮水量、活动状态、躺卧姿势都是健康和生产性能的“指标”。比如猪发烧或者感染疫病最先表现出来的往往不是体温而是行为异常——不爱吃食、不爱活动、长时间卧地不起。靠饲养员肉眼去盯几千头猪根本不现实。这时候就需要视觉模型来自动识别猪的行为再结合饲喂系统、环控系统实现精准干预。“智慧养殖”喊了很多年真正卡脖子的不是传感器而是算法要的训练数据。猪场环境光线暗、粉尘多、栏舍拥挤猪又长得差不多通用目标检测模型很难直接迁移。所以“猪只行为识别分割数据集”就特别抢手。我手里这套是labelme格式、1085张图像、覆盖4类行为的实例分割数据专门用来训练行为分割模型。1.2 为什么选“分割”而不是单纯的目标检测同样是对采食、躺卧这类行为做识别目标检测给的是框分割给的是像素级掩膜。这两者的差别表面看只是输出形式实际使用体验差很多。猪群经常挤在一起彼此遮挡严重目标检测的框会把好几头猪框成一个“大框”导致计数错误、行为判断串味。如果用Mask类型的分割模型模型可以输出每头猪的轮廓哪怕猪身被挡住一半只要边缘可见就能按像素归到具体某头猪上。另外很多行为本身就是姿态相关的比如“躺卧”和“采食”如果只有一个矩形框框里可能同时包含猪头、食槽和地面模型很难说清楚这头猪到底在干嘛。有了分割掩膜模型直接判断轮廓内部的姿态和交互准确率就上去了。在我实际测试中同样一批标注数据纯检测模型mAP能到0.75左右但行为分类的错误率挺高换成Mask R-CNN或者YOLOv8-seg这类分割模型后行为判断的准确率能稳定到0.85以上。这也是为什么现在农业视觉项目越来越倾向于直接上分割模型。2. 数据集构成与标注规范解析2.1 1085张图像的采集环境与内容分布先说说这套数据的构成。1085张图像不是随便拍的来源最好是标准化养殖栏舍。常见配置是猪舍内有保温灯、不锈钢食槽、鸭嘴式饮水器地面是漏缝板或者水泥地。图像分辨率一般不低于1280×720因为分割任务对细节要求高太低的分辨率会把小目标轮廓糊掉。我最开始拿到这批数据时第一件事不是看标注而是看图像的时间分布和场景分布。1085张数量看起来不大但如果全是同一时间点、同一个栏舍拍的模型泛化能力会非常差。合理的做法是包含不同的光照条件白天自然光、夜间照明灯、红外补光不同的栏舍位置靠窗、中间、角落还要包含猪只稀疏和拥挤两类场景。我通常会按png/jpg抽取每张图像里平均有3到15头猪这样模型才能学会处理遮挡和群体交互。这套数据集里面常见的情况是训练图像里会混入少量不完整的猪只比如猪头伸出栏杆、猪身被柱子挡住。这些数据用目标检测很难处理正好是分割模型的用武之地。2.2 四类行为定义与标注标准拿到数据后最麻烦的其实是类别定义。同样是“采食”有的标注员会把猪嘴碰到食槽才算有的把猪在食槽附近站着也算。所以一套数据能不能用先看标注规范是否统一。我手上这套数据集采用的四类划分是采食feeding、饮水drinking、躺卧lying、活动active。这四类基本覆盖了育肥猪日常行为的80%以上。采食猪头伸入食槽或者嘴部与食槽接触轮廓包含猪头到肩部区域。饮水猪嘴接触饮水器颈部上扬这时候猪身常呈站立或半站立状态。躺卧猪身体侧面或腹部大面积接触地面四肢不能明显支撑体重。活动除上述三类外的正常行走、站立、探究行为属于“兜底类”。这个方法学起来不难但实际操作时有很多灰色地带。比如猪侧躺着吃食它到底是采食还是躺卧我一般会约定“行为优先级”采食部分紧贴食槽即使半卧也算采食饮水如果只是站着碰了一下出水口不算。这种优先级规则必须在标注前写进规范文档不然每个标注员都有自己的判断返工成本高到吓人。2.3 Labelme格式详解JSON结构与小技巧用labelme标注后每张图像对应生成一个同名JSON文件。一个标准JSON包含这些字段{ version: 5.2.0.post1, flags: {}, shapes: [ { label: pig_feeding, points: [[231, 432], [256, 430], ...], group_id: 1, description: , shape_type: polygon, flags: {} } ], imagePath: frame_0051.jpg, imageData: null, imageHeight: 720, imageWidth: 1280 }关键点在于shapes里的points它是一圈多边形的顶点坐标顶点数从几个到几十个不等。顶点的疏密直接决定标注精度顶点太少猪耳朵、尾巴这些细节会丢顶点太多标注员效率低训练时点集占内存也大。我最常用的labelme做法是先把图像放大到150%用“编辑多边形”模式添加顶点。猪这种目标边缘不是特别锐利毛和阴影容易干扰所以标注时不需要抠到每根毛但关键的“猪头轮廓”“蹄子位置”必须标准。标注完成后还要用labelme自带的labelme_json_to_dataset命令检查掩膜效果这一步能在训练前发现多边形自交、顶点错乱等问题。3. 数据清洗与转换从Labelme到训练格式3.1 为什么要转换以及常见转换目标labelme的JSON格式只适合“人看”不适合“模型训练”。主流分割框架比如MMDetection、Detectron2、YOLOv8用的都是COCO或YOLO格式。所以我们训练前必须做格式转换。COCO格式的标注是用JSON存储的支持多类别、实例分割还有一个segmentation字段保存多边形坐标对Mask R-CNN这类模型特别友好。YOLO格式则是每张图像一个对应txt文件里面每行表示一个目标的类别、归一化后的中心点坐标和归一化后的多边形点坐标存储在yolo_seg目录下。我一般会同时导出两种格式。COCO格式便于用Detectron2和MMDetection做精细化调参YOLO格式便于快速跑YOLOv8-seg拿baseline。3.2 Labelme转COCO代码与关键参数我现在用的是一套自己改过的转换脚本逻辑很简单但踩坑不少。这里放一段核心代码只处理polygon类型的标注其他如rectangle、circle可以先过滤掉。import json import os import glob from PIL import Image import numpy as np def labelme_to_coco(img_dir, json_dir, output_json): images [] annotations [] categories [ {id: 1, name: feeding, supercategory: behavior}, {id: 2, name: drinking, supercategory: behavior}, {id: 3, name: lying, supercategory: behavior}, {id: 4, name: active, supercategory: behavior}, ] cat_id_map {cat[name]: cat[id] for cat in categories} image_id 0 ann_id 0 json_files glob.glob(os.path.join(json_dir, *.json)) for json_path in json_files: with open(json_path, r, encodingutf-8) as f: data json.load(f) img_path os.path.join(img_dir, data[imagePath]) img_w data[imageWidth] img_h data[imageHeight] image_id 1 images.append({ id: image_id, file_name: os.path.basename(img_path), width: img_w, height: img_h, }) for shape in data[shapes]: if shape[shape_type] ! polygon: continue label shape[label] if label not in cat_id_map: continue points shape[points] if len(points) 3: continue seg [float(coord) for point in points for coord in point] # COCO要求闭合并扁平化后的列表 ann_id 1 annotations.append({ id: ann_id, image_id: image_id, category_id: cat_id_map[label], segmentation: [seg], area: 1.0, # 之后用RLE或polygon面积计算 bbox: [], iscrowd: 0, }) coco { images: images, annotations: annotations, categories: categories, } with open(output_json, w, encodingutf-8) as f: json.dump(coco, f, ensure_asciiFalse, indent2)这个脚本看起来短但有两个很重要的细节。第一area和bbox不能真的写成1.0和空列表。我刚开始图省事用占位符填进去Mask R-CNN训练时损失直接NaN。正确做法是用pycocotools的polygon面积计算函数from pycocotools import mask as maskUtils import numpy as np def polygon_area(points_xy): rle maskUtils.frPyObjects([points_xy], image_h, image_w) return float(maskUtils.area(rle))第二segmentation列表里的坐标必须是“闭包式”的即最后一个点和第一个点相同。labelme直接导出的多边形首尾不闭合模型训练时会出现掩膜缺口。我都是在上面的脚本里强制追加第一个点坐标。3.3 数据划分与文件组织转换完之后还要做训练集、验证集、测试集划分。我的习惯是7:2:1而且必须保证同一栏舍的连续帧不要同时进入训练集和测试集否则模型会“作弊”性能虚高。文件组织上我建议这样放dataset/ ├── images/ │ ├── train/ # 758 张 │ ├── val/ # 218 张 │ └── test/ # 109 张 ├── annotations/ │ ├── train_coco.json │ ├── val_coco.json │ └── test_coco.json ├── yolo_labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yamlYOLO的data.yaml内容也很直接train: ../dataset/yolo_labels/train.txt val: ../dataset/yolo_labels/val.txt nc: 4 names: [feeding, drinking, lying, active]注意YOLO训练时train.txt里是图片路径不是标签路径标签文件会自动在同目录的labels子目录下找。如果转换脚本没处理好路径训练时经常会报“Image not found”。4. 模型训练与调参实战4.1 Mask R-CNN vs YOLOv8-seg 选择训练分割模型我的原则是“先跑通再调优”。建议先用YOLOv8-seg快速出一个baseline因为YOLO生态对格式要求最简单训练速度也快。1085张图像用一张普通显卡12GB显存跑YOLOv8s-seg300个epoch大概三四十分钟就能完。训完之后看mAP和类别AP如果采食、饮水的AP明显低于躺卧、活动说明这两类行为在标注或图像上容易被混淆需要针对性处理。Mask R-CNN适合做深度调优。它的实例分割质量更高尤其在猪只密集遮挡场景下掩膜的边界会细腻一些。但Mask R-CNN训练要慢很多而且对显存的要求更高。我的经验是如果YOLOv8-seg的mAP已经到了0.82以上项目对边缘精度要求又不极致直接部署YOLO省心又稳定。4.2 数据增强策略数据增强是这种小数据集项目的“救星”。1085张图像不算多模型很容易过拟合尤其对猪只行为这种类内差异大的任务增强策略必须花心思。我常用的增强组合是随机翻转注意水平翻转影响很小可以加垂直翻转会改变猪只重心位置通常不加。随机旋转范围控制在正负15度以内旋转角度太大会导致多边形标注边界失真。亮度、对比度调整猪舍光线变化大这两个增强对夜间图像特别有帮助。随机遮挡/擦除模拟猪只被栏杆、其他猪身挡住的情况提升模型对遮挡的鲁棒性。Mosaic增强YOLOv8自带将四张图拼成一张对提升小目标检测能力很有效。做增强时要注意分割标签会跟着图像一起几何变换比如旋转、翻转、缩放时多边形坐标要同步变换不能只改图和bbox。如果用的是YOLOv8提供的增强管线它会自动处理分割掩膜但用自定义pipeline时就很容易漏掉标签变换。4.3 训练细节与模型导出我用YOLO格式跑的分割训练超参数一般这样配置yolo segment train datadata.yaml modelyolov8s-seg.pt epochs300 imgsz640 batch8 device0这里有个经验imgsz不要一味追求大。猪只目标本身很大640完全够用改成1280虽然可能小目标更好认但训练时间和显存占用暴涨对精度的提升常常不足1个百分点。我用640训练出来的模型在1080P图像上直接推理长边尺寸和640相近速度还能跑30 FPS以上。训练结束后导出成部署格式yolo export modelbest.pt formatengine device0 # TensorRT # 或 yolo export modelbest.pt formatonnx opset12如果是放到边缘盒子Jetson这类上跑TensorRT的engine格式效率最高。我实际跑过TensorRT比ONNX Runtime快大约1.5倍内存占用还能降30%。5. 常见问题与排查技巧实录5.1 标注边界粗糙导致掩膜边缘锯齿这是我在项目里遇到最头疼的问题。labelme标注时为了省力多边形顶点画少了猪耳朵边缘就会变成“一刀切”的直线训练出来的分割掩膜边缘特别毛糙。解决方法是标注完成后批量检查所有多边形的顶点数量少于10个的要重点回看。另外转换脚本里可以加一个简单的后处理用cv2.approxPolyDP对多边形做轻微平滑。但注意不要过度使用否则模型学不到真实轮廓。def smooth_polygon(points, epsilon1.0): import cv2 cnt np.array(points, dtypenp.float32).reshape((-1, 1, 2)) approx cv2.approxPolyDP(cnt, epsilon, True) return approx.reshape(-1, 2).tolist()加这样一个平滑步骤我可以把掩膜边缘的锯齿度降低不少mAP里Mask AP能提升0.03左右。5.2 类别不平衡与行为混淆这套数据里躺卧和活动图像数量通常比采食和饮水多因为猪一天中有大量时间是在休息的。表现在训练上就是采食、饮水的AP显著偏低。我的处理办法有两个。第一个是给YOLO训练加class_weights或者直接按类别重新抽样。更简单的方法是复制少类别的标注掩膜所在图像做几次重叠增强。第二个是修改损失函数生成伪标签。我之前用MMDetection训练Mask R-CNN时会把饮食类的loss权重调高到1.5躺卧类保持1.0效果明显。行为混淆的典型案例是“饮水和采食”。猪拱水嘴和猪拱食槽时头部动作在低分辨率图像下非常相似。我的做法是追加语义约束引入图像中的区域标注把食槽和饮水器在标注阶段额外标成两个“背景区域”模型在计算损失时直接排除这些区域让行为分类更专注于猪只自身姿态。这一步在labelme里可以作为一个新的类别标成两外两类转换时再过滤掉。5.3 部署环境中的性能优化边缘设备上跑分割模型最直观的问题是帧率不够。我做过一个实时推流检测的demo在一台低功耗盒子上YOLOv8m-seg只有12 FPS根本没法用。优化后达到25 FPS手段主要是三项。第一推理输入尺寸降到480然后把图像按比例缩放避免让模型处理无用的背景区域。第二使用TensorRT并开启FP16精度。分割模型对精度损失不敏感FP16完全能用速度提升接近一倍。第三只对“猪只所在区域”做二次行为判断不做全图密集预测。具体来说先用目标检测模型快速框出猪的位置再对每个框内的区域送入分割网络。这个“检测分割”级联方式在GPU和嵌入式设备上都比全图分割快很多。6. 项目经验与后续扩展方向6.1 我踩过的几个坑先帮你避开这个项目最大的坑不是模型而是数据。1085张图片听起来不多但在猪只行为场景里已经够用前提是类别平衡、场景丰富。我最初拿到数据后没做清洗就直接训练结果发现训练集里有几张图像是“空注释”也就是没有任何猪只的原始图模型训练时容易把背景误判成目标导致precision下降。必须保证每张训练图像都至少有一个掩膜注释。另外labelme标注时如果两个猪只的掩膜重叠有些标注员会直接把它们标成一个大掩膜。这在实例分割里是不允许的因为COCO和YOLO格式中每个目标实例必须独立。清洗脚本里一定要检测重叠掩膜若面积重叠超过5%要么手工修正要么直接删除该样本。我当时就漏了这一步模型预测结果里经常出现“一头猪抱着一头猪”的诡异掩膜。6.2 从分割到行为时序分析如果只做图像分割你能得到的是“某帧里猪在采食/饮水/躺卧/活动”。但养殖场景真正有意义的是“这只猪今天一共采食了多长时间”“饮水次数是否异常”。所以下一步一定要把单帧分割结果做成时序分析。我通常会给每头猪生成一个ID用跟踪算法关联连续帧中的掩膜。然后统计每个ID的分割掩膜面积、行为类别变化频率、活动轨迹长度。这些特征输入到简单逻辑回归模型就能做早期健康预警。事实上我遇到过一头猪连续六个小时都躺着且没有采食记录分割模型早早就识别出了异常但直到下午饲养员才发现它发烧。6.3 扩展思路少样本学习与多模态融合1085张图只是起点等下一批新猪舍数据过来标注成本依然很高。我目前在尝试利用这个预训练分割模型做半自动标注加载模型用它对新的未标注图像生成初步掩膜人工只需要修正错误边界即可。这能让标注产量提高两三倍。还可以把图像和声音数据结合。猪只咳嗽、打喷嚏的声音特征和图像行为特征互补图像分割结果负责“它在做什么”声音模型负责“它是否不舒服”两者融合后预警准确率会比单一视觉模型高很多。这个方向我还在实验但目前看是猪只行为识别项目最有可能产生新价值的出口。这套数据集的投产价值不在于1085张图本身而在于它把猪只行为从“模糊经验”变成了“可计算的像素”。我做完这个项目后最大的体会是农业视觉项目最难的部分不是算法而是你是否愿意花时间去理解猪的行为、环境和标注细节。模型结构可以抄坑只能自己踩。