
简介本资源面向计算机视觉入门与进阶开发者提供一套基于YOLOv5的牛只识别检测完整工程可直接用于畜牧场景的智能计数与目标检测实践。压缩包共79个文件约42.58MB包含17个Python源码文件、17个YAML配置、3个pt权重文件以及pyc缓存、sh脚本、Dockerfile、说明文档与效果图等覆盖训练、推理、部署全流程。资源附有迭代200次的训练结果含loss下降曲线、Recall召回率曲线、Precision精确度曲线与mAP等评估指标图模型拟合较好训练集使用4000多张图片、8000多个“牛”目标数据分布均匀便于复现与二次调优。目前已有273人学习关注。读者可据此掌握数据配置、模型训练、指标分析与推理检测的完整链路并借助使用说明快速跑通项目适合课程设计、竞赛baseline或畜牧视觉应用的原型验证。1. 牛脸识别到底难在哪从一段牧场监控视频说起牧场主老周给我看了一段夜间监控几十头西门塔尔牛排队走进挤奶厅画面里牛脸一晃而过有的低头、有的侧脸、有的被前面牛屁股挡住大半。他问能不能像人脸识别那样自动把每头牛的采食量、挤奶量、健康状态对上号。这就是「基于深度学习视频分析实现牛识别检测」要解决的真实问题——不是识别「这是一头牛」而是识别「这是哪一头牛」。牛脸识别和猫狗分类完全不是一回事。牛脸花纹相似度高、姿态变化大、没有配合意识加上牧场光照忽明忽暗、镜头常被水汽和饲料粉尘糊住直接套用现成的人脸识别模型基本会翻车。这个方案的价值在于把视频抽帧、牛脸检测、个体特征比对串成一条可落地的流水线配套源码、模型文件、评估指标曲线和使用说明让做智慧畜牧的团队能快速跑通第一版。适合有 Python 和深度学习基础、手头有牧场视频或摄像头流的工程师也适合想切入农业视觉赛道的算法同学。2. 视频分析流水线怎么搭抽帧、检测、跟踪、识别四步走2.1 为什么不能直接拿视频喂给模型视频本质是连续帧序列直接送进网络会带来两个问题一是相邻帧高度冗余算力浪费在几乎相同的画面上二是牛在移动同一头牛在不同帧里的位置、尺度、角度都在变模型需要稳定的输入才能学到个体特征。常见做法是先按固定间隔抽帧把视频降维成图像集合再在图像上做检测和识别。抽帧间隔是关键参数。间隔太小冗余帧多推理慢间隔太大同一头牛可能只出现一两帧跟踪容易断。我一般按视频帧率来定25fps 的视频每 5 到 10 帧抽一帧也就是 2.5 到 5fps 的采样率。对于牛这种移动速度不快的对象3fps 左右通常够用。如果要做行为分析比如爬跨、争斗采样率要提到 10fps 以上否则动作会被抽没。import cv2 import os def extract_frames(video_path, output_dir, interval5): 按固定间隔抽帧 video_path: 输入视频路径 output_dir: 输出帧目录 interval: 每隔多少帧抽一帧 os.makedirs(output_dir, exist_okTrue) cap cv2.VideoCapture(video_path) fps cap.get(cv2.CAP_PROP_FPS) total int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) idx 0 saved 0 while True: ret, frame cap.read() if not ret: break if idx % interval 0: # 帧名带时间戳方便后续和视频时间对齐 ts idx / fps name fframe_{idx:06d}_{ts:.2f}s.jpg cv2.imwrite(os.path.join(output_dir, name), frame) saved 1 idx 1 cap.release() print(f总帧数 {total}抽帧 {saved} 张采样率约 {fps/interval:.1f}fps) extract_frames(cattle.mp4, frames, interval5)这段代码的逻辑很直白逐帧读取按interval取模决定是否保存文件名里带上帧号和时间戳。参数interval是唯一需要调的视频帧率高就调大牛移动快就调小。保存成 jpg 而不是 png是因为 jpg 体积小、读写快对后续训练影响可以忽略。如果视频有旋转元数据OpenCV 读出来可能是横的需要额外处理这个坑后面会讲。2.2 牛脸检测先框出来再谈识别抽完帧下一步是把牛脸从画面里框出来。牛脸检测和通用目标检测的区别在于牛脸在画面里占比小、纹理弱、经常被遮挡用 COCO 预训练的 YOLO 直接检测「牛」只能框出整头牛框不出脸。所以需要专门标注牛脸数据或者用「整牛检测 关键点定位」两步走。我一般推荐直接训练一个牛脸检测模型类别就一个cattle_face。数据标注时框到牛眼、鼻镜、耳朵这个区域不要框整头牛。标注量方面单个牧场场景 2000 到 3000 张标注图通常能到可用水平多场景混合需要 5000 张以上。模型选型上YOLO 系列在速度和精度平衡上比较成熟nano 或 small 版本在边缘设备上也能跑。from ultralytics import YOLO # 加载预训练权重替换检测头类别数为 1 model YOLO(yolov8n.pt) model.train( datacattle_face.yaml, # 数据集配置 epochs100, imgsz640, batch16, lr00.01, patience20, # 20 轮无提升就早停 device0 )cattle_face.yaml里写清楚训练集、验证集路径和类别名。imgsz640是检测常用输入尺寸牛脸小的话可以提到 960 或 1280但推理速度会下降。patience20是早停防止过拟合。训练完看评估指标曲线重点看 mAP50 和 mAP50-95 两条线是否收敛、验证损失是否反弹。如果 mAP50 到 0.9 但 mAP50-95 只有 0.5说明框的位置不够准可能是标注框松紧不一致导致的。2.3 多目标跟踪把同一头牛的帧串起来检测只给出每一帧的框不知道哪一帧的框属于同一头牛。跟踪的作用就是给每个框分配一个临时 ID让同一头牛在连续帧里保持同一个 ID。常用算法是 ByteTrack 或 BoT-SORT它们基于检测框做运动预测和匹配不需要额外训练。跟踪质量直接影响识别效果。如果 ID 频繁切换同一头牛会被当成多头识别结果就乱了。牛群密集、互相遮挡时跟踪最容易断。调参时重点关注track_high_thresh和track_buffer前者是检测框置信度阈值太低会引入误检太高会漏掉遮挡时的框后者是丢失后保留轨迹的帧数牛被挡住几帧再出现这个值要够大才能接上。from ultralytics import YOLO import numpy as np model YOLO(cattle_face_best.pt) # 对抽帧目录做跟踪persistTrue 表示跨帧保持轨迹 results model.track( sourceframes, trackerbytetrack.yaml, conf0.3, iou0.5, persistTrue, saveTrue ) # 统计每个 track_id 出现的帧数过滤掉太短的轨迹 from collections import defaultdict track_len defaultdict(int) for r in results: if r.boxes.id is not None: for tid in r.boxes.id.cpu().numpy(): track_len[int(tid)] 1 print(sorted(track_len.items(), keylambda x: -x[1])[:10])conf0.3是检测置信度阈值跟踪场景下可以比纯检测低一点因为跟踪能靠运动信息补回来。persistTrue让跟踪器在视频序列上保持状态。最后统计每个 ID 的帧数出现帧数太少的轨迹通常是误检或短暂遮挡可以过滤掉不参与后续识别。3. 个体识别模型怎么训从牛脸到牛号3.1 识别和检测的区别分类还是度量学习检测回答「牛脸在哪」识别回答「这是哪头牛」。如果牧场牛的数量固定且不多比如 50 头以内可以当成分类问题每个牛号一个类别用 Softmax 分类。但牧场经常进出牛、淘汰牛类别会变每次变都要重新训练不现实。更稳妥的做法是度量学习训练一个特征提取网络让同一头牛的特征向量距离近、不同牛的距离远识别时用最近邻比对。度量学习的经典损失是 Triplet Loss 或 ArcFace。Triplet Loss 需要构造三元组锚点、正样本、负样本训练时容易不稳定ArcFace 在分类基础上加角度间隔训练更稳推理时取特征向量做比对。我一般用 ArcFace因为它在牛脸这种类内差异大的场景下表现更稳。import torch import torch.nn as nn import torch.nn.functional as F from torchvision import models class CattleEmbedding(nn.Module): def __init__(self, num_classes, emb_dim512): super().__init__() # 用 ResNet50 做骨干去掉最后的分类层 backbone models.resnet50(pretrainedTrue) self.features nn.Sequential(*list(backbone.children())[:-1]) self.fc nn.Linear(2048, emb_dim) self.arc ArcFace(emb_dim, num_classes) def forward(self, x, labelNone): feat self.features(x).flatten(1) emb F.normalize(self.fc(feat), dim1) if label is not None: return self.arc(emb, label) return emb class ArcFace(nn.Module): def __init__(self, emb_dim, num_classes, s30.0, m0.5): super().__init__() self.weight nn.Parameter(torch.randn(num_classes, emb_dim)) self.s s # 缩放因子 self.m m # 角度间隔 def forward(self, emb, label): cosine F.linear(emb, F.normalize(self.weight)) theta torch.acos(torch.clamp(cosine, -11e-7, 1-1e-7)) target torch.cos(theta self.m) one_hot F.one_hot(label, cosine.size(1)).float() logits cosine * (1 - one_hot) target * one_hot return logits * self.s骨干网络用 ResNet50 是常见起点牛脸数据少的话可以换 ResNet18 或 MobileNet减少过拟合。emb_dim512是特征维度太大容易过拟合太小区分度不够512 是常用值。ArcFace 的s30和m0.5是两个关键参数s控制 logits 尺度太小梯度弱太大训练震荡m控制类间间隔越大类间分得越开但太大训练困难。牛脸场景我一般从m0.3开始试稳定后再加到 0.5。3.2 训练数据怎么组织每头牛至少 20 张识别模型对数据的要求比检测高。检测只要框出脸就行识别要求同一头牛有多角度、多光照的样本。经验值是每头牛至少 20 张有效牛脸图低于这个数特征学不稳。采集时尽量覆盖正面、左右侧、低头、抬头几种姿态光照上白天和夜间都要有。数据组织成牛号/图片的目录结构训练时按牛号划分训练集和验证集注意同一头牛的图不能同时出现在两边否则验证指标虚高。如果某头牛只有 5 张图可以用数据增强扩充但增强不能替代真实多样性旋转、亮度变化对牛脸识别帮助有限因为牛脸本身纹理就弱。from torch.utils.data import Dataset, DataLoader from PIL import Image import os, random class CattleFaceDataset(Dataset): def __init__(self, root, transformNone): self.samples [] self.transform transform self.classes sorted(os.listdir(root)) self.class_to_idx {c: i for i, c in enumerate(self.classes)} for c in self.classes: d os.path.join(root, c) for f in os.listdir(d): self.samples.append((os.path.join(d, f), self.class_to_idx[c])) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] img Image.open(path).convert(RGB) if self.transform: img self.transform(img) return img, label这个 Dataset 按目录读图类别名就是牛号。划分训练验证集时建议按牛号划分而不是按图片随机划分比如 80% 的牛做训练20% 的牛做验证这样验证的是模型对「没见过的牛」的区分能力更接近实际使用。如果按图片随机划分同一头牛的图两边都有验证指标会偏高上线后容易翻车。3.3 评估指标怎么看mAP、CMC、ROC 三条曲线评估指标曲线是判断模型能不能用的核心依据。检测看 mAP识别看 CMC 和 ROC。CMC 曲线横轴是 rank纵轴是命中率rank-1 表示第一次比对就命中rank-5 表示前五次里命中。牧场场景 rank-1 到 0.85 以上、rank-5 到 0.95 以上基本可用。ROC 曲线看 AUCAUC 越接近 1 越好低于 0.9 说明特征区分度不够。看曲线时注意两点一是验证集是否和训练集同分布如果验证集全是白天图训练集有夜间图曲线好看但夜间实际会掉二是曲线是否过拟合训练损失一直降、验证损失先降后升说明模型记住了训练牛而不是学到了通用特征。遇到这种情况加数据、加正则、减模型容量三选一。4. 避坑与排查牛脸识别落地时最容易翻车的五件事4.1 夜间红外画面训练集缺失白天模型夜间全瞎现象白天识别准确率 90% 以上一到夜间就掉到 50% 以下甚至检测框都出不来。原因训练集几乎全是白天彩色图夜间红外图是灰度、对比度低、噪点多模型没见过这种分布。解决采集时白天夜间都要覆盖夜间图至少占 30%训练时对灰度图做直方图均衡化或者把彩色图随机转灰度做增强让模型对颜色不敏感。4.2 牛脸标注框太松识别特征混入背景现象检测 mAP 看着不错但识别 rank-1 上不去同一头牛不同帧的特征距离忽大忽小。原因标注时框到了脖子、耳朵甚至背景栏杆识别网络学到的特征里混入了背景信息换一个角度背景变了特征就变了。解决标注规范写清楚框到牛眼、鼻镜、脸颊这个区域框边留一点余量即可训练识别模型时把检测框往外扩 10% 再裁剪避免框太紧切掉边缘特征。4.3 跟踪 ID 频繁切换同一头牛被算成多头现象识别结果里同一头牛出现多个 ID采食量统计翻倍。原因牛群密集遮挡时跟踪器匹配失败新框被分配新 ID。解决调大track_buffer让轨迹在遮挡期间保留更久降低track_high_thresh让低置信度框也参与匹配如果遮挡特别严重可以加 ReID 特征做跟踪匹配但会增加算力。4.4 模型文件加载报错版本不匹配现象拿到源码和模型文件运行时报KeyError或size mismatch。原因训练时的网络结构和推理时的代码不一致比如训练用了 ResNet50推理代码里写的是 ResNet18或者 PyTorch 版本不同导致权重键名有差异。解决先看使用说明里的环境要求对齐 PyTorch 和 ultralytics 版本加载权重时用strictFalse先跑通再逐层核对键名模型文件不要改名有些代码按文件名判断结构。4.5 评估曲线好看但实际部署掉点现象评估指标 rank-1 到 0.95部署到牧场摄像头后实际识别率不到 70%。原因评估集和实际场景分布不一致评估集可能是精选的清晰图实际摄像头有畸变、水汽、粉尘、角度偏。解决评估集要从实际摄像头流里抽不要用训练时挑出来的好图部署前做一轮现场测试统计每个牛号的识别率找出掉点最多的牛号单独补数据。5. 把识别结果用起来从牛号到采食量统计的一个具体技巧识别出牛号只是第一步牧场真正要的是每头牛的采食量、挤奶量、活动量。这里分享一个我在实际项目里用过的技巧把识别结果和区域检测结合起来做「牛号 区域 时长」的统计。具体做法是在画面里划出采食区、挤奶区、休息区几个多边形区域跟踪轨迹进入某个区域后开始计时离开时结束把时长累加到对应牛号上。import cv2 import numpy as np from shapely.geometry import Point, Polygon # 定义区域多边形坐标按实际画面比例写 zones { feeding: Polygon([(100, 200), (500, 200), (500, 600), (100, 600)]), milking: Polygon([(600, 200), (900, 200), (900, 600), (600, 600)]), } # track_id - {zone: 累计秒数} time_log {} def update_zone(track_id, center, fps): pt Point(center) for zone_name, poly in zones.items(): if poly.contains(pt): time_log.setdefault(track_id, {}).setdefault(zone_name, 0) time_log[track_id][zone_name] 1.0 / fps break # 假设已有跟踪结果center 是牛脸框中心 # for frame in results: # for box, tid in zip(frame.boxes.xyxy, frame.boxes.id): # cx (box[0] box[2]) / 2 # cy (box[1] box[3]) / 2 # update_zone(int(tid), (cx, cy), fps3)这段代码的核心是 Shapely 的多边形包含判断比用矩形区域灵活能贴合实际栏舍形状。fps是抽帧后的采样率不是原视频帧率算时长时要用采样率否则时间会偏。区域坐标要按实际画面分辨率标定建议在画面上叠加显示区域边界确认无误后再跑统计。这个技巧的边界在于牛脸框中心不一定代表牛的身体位置低头采食时脸在食槽里身体可能在区域外。更准的做法是用整牛框的中心或者用牛脸框底部中心。另外区域边界附近的抖动会导致时长统计跳变可以加一个滞回判断进入区域后连续 3 帧都在区域内才开始计时离开时连续 3 帧都在区域外才停止。我自己的习惯是每上一个新牧场先跑一周的纯识别不统计把识别日志和人工观察对一遍确认每个牛号的识别率都稳定了再开统计。这一步多花一周能省掉后面反复调参的很多麻烦。牛脸识别这个方向模型只是一半另一半是现场的数据质量和流程设计。希望帮到你。本文还有配套的精品资源点击获取