简介:基于YOLOv5的牛只识别检测完整项目,面向智慧养殖、农业视觉等领域的目标检测开发者,也适合深度学习初学者参考完整训练与评估流程。压缩包共79个文件、42.58MB,整体按源码、模型、训练输出与说明文档分模块组织;其中17个py脚本与23个pyc文件提供可运行/可调用的检测、训练与推理逻辑,3个pt权重文件可直接加载使用,yaml配置和sh脚本便于复现环境与参数,使用说明txt则帮助快速上手。另有精度-召回曲线、损失下降曲线、mAP等评估图表,以及训练/测试批次标注对比图,可直观判断模型收敛情况。模型基于4000多张图片、8000余个牛目标训练,迭代200轮,数据分布均匀,拟合较好;随包使用说明可快速对环境进行配置并完成推理,也可基于现有权重继续微调。目前已有273人学习下载,适合快速搭建牛只检测 Demo 或在此基础上做二次开发。
1. 牛识别检测不是装个 YOLO 就完事:视频分析真正的门槛在哪
养殖场装了十几路摄像头,想自动数牛、找卧地不起的病牛,或者给每头牛建个体档案。很多人一看“牛识别检测”就当成普通目标检测来做,单张图跑通后很开心,一接到视频流就翻车:同一头牛被重复计数、夜里漏检、黑白花牛和雪地背景糊成一片。标题里这个方案本质是一条完整的落地链路——用深度学习视频分析模型做目标检测,再用模型文件、评估指标曲线和使用说明把训练、调参、部署串起来。它解决的不只是“框出牛”,而是“在持续的视频流里稳定地认出牛并给出可信的评估依据”。适合牧场智能化项目的工程师、农业 AI 选型的技术负责人,以及想找一个真实场景练手深度学习的算法入门者。
2. 模型选型与数据准备:视频里的牛为什么不能当普通图片检测
2.1 选型逻辑:为什么主流做法是 YOLO 系 + 跟踪器
牛识别检测的常见从业方案基本锁在 YOLOv5/YOLOv8 这一系上,原因很直接。牛在监控画面里属于中大型目标,640 的输入分辨率已经能覆盖从几米到几十米距离的个体,不需要做切片检测;而养殖场视频分析对实时性有硬要求,YOLO 系的推理速度快且导出部署生态成熟,量化和剪枝工具链完整。相比之下,两阶段的 Faster R-CNN 系列精度略高但帧率上不去,在十几路视频流并联的场景里成本兜不住。
视频分析比静态图多出来的难点是时间维度。单帧检测会有边界抖动,一头牛从画面左侧走到右侧,检测框可能在 30 帧里跳变十几次,直接导致计数重复、轨迹断裂。所以典型方案是“检测 + 跟踪”的级联:YOLO 负责每帧出框,Sort 或 DeepSort 负责把框串成稳定的轨迹,用轨迹 ID 去重计数。如果你还要做个体识别(区分“这头牛是 27 号还是 38 号”),就得在检测框上再拉一个 ReID 分支,或者单独训练一个牛脸/花斑嵌入模型,这和单纯的类别检测是两条技术路线,别混在一起。
2.2 数据准备:视频抽帧、清洗与标注格式
训练数据极少有人直接用摄像头原始视频整段喂给模型,常见做法是抽帧。我会按 1 秒 1 帧抽监控视频,牛群快速移动的时段补到 2 帧/秒;静止画面太多会让模型过拟合到“空栏舍”背景,所以抽帧前先用帧差法做一次粗糙的运动筛选,只保留画面内容有明显变化的帧。抽出来的图片还要过一道手:删除运动模糊帧、过曝帧和牛被遮挡超过一半的帧。模糊帧的标注质量极差,留着只会让 loss 曲线莫名震荡。
标注格式直接转成 YOLO 的 txt 格式,每行一个目标,内容是类别 ID 和归一化的中心点 x、y、宽、高。标注工具用 LabelImg 或者 X-AnyLabeling 都行,后者支持半自动预标注,先用一个现成的牛检测模型打底框再人工修正,能省不少时间。类别设计这里有个关键决策:如果只做“有没有牛”的检测计数,一个类别就够了;但要注意如果你在训练时把“牛”拆成“成牛”和“犊牛”,类别之间样本量差异大容易失衡,不如先统一成一个类,后期用规则按框尺寸区分成牛和犊牛,工程上更稳。
2.3 训练集划分:按视频片段切分而不是随机抽帧
这是新手最容易踩的一个划分陷阱。随机抽帧后按 7:2:1 分训练集、验证集、测试集,看着很规范,但相邻帧几乎一模一样,验证集里充满了训练集的“近亲”,评估指标曲线会好看到不真实,实际部署立刻现原形。正确做法是按视频片段划分:把每段视频完整归入训练集或验证集,保证验证集里的牛、场景、光照都是模型没见过的。比例还是 7:2:1,但要保证训练集里包含多个不同场地、不同季节的数据,否则模型学到的只是某个圈舍的纹理。
划分可以用一个简单脚本完成,按视频文件名做分组:
import os import random from collections import defaultdict video_to_frames = defaultdict(list) frame_dir = "dataset/frames" for fname in os.listdir(frame_dir): video_id = fname.split("_frame_")[0] # 帧文件命名如 cam03_frame_0012.jpg video_to_frames[video_id].append(fname) videos = list(video_to_frames.keys()) random.seed(42) random.shuffle(videos) train_videos = videos[:int(len(videos)*0.7)] val_videos = videos[int(len(videos)*0.7):int(len(videos)*0.9)] test_videos = videos[int(len(videos)*0.9):] def write_split(split_name, video_list): with open(f"dataset/{split_name}.txt", "w") as f: for vid in video_list: for fname in video_to_frames[vid]: f.write(f"dataset/frames/{fname}\n") write_split("train", train_videos) write_split("val", val_videos) write_split("test", test_videos)这段代码的核心是按视频 ID 分组再做随机划分,而不是对每张帧直接 shuffle。逻辑说明:video_to_frames用帧文件名前缀还原出视频来源,保证同一个视频的帧永远只落在同一个集合里;random.seed(42)固定随机种子让划分可复现。参数上,7:2:1 是我常用的默认值,数据量小时可以把验证集调到 15%,但测试集最好始终保留 10%,因为最后评估模型泛化能力要用完全没参与训练和调参的数据。
3. 用源码跑通训练全流程:从环境配置到权重文件与评估曲线
3.1 环境搭建:先跑通 demo 再谈调参
拿到标题里这种“源码 + 模型文件”的项目包,第一件事不是看模型怎么设计,而是把训练和推理命令跑通。我会新建一个干净的 Python 虚拟环境,避免把系统 Python 搞乱。依赖上最核心的是 PyTorch 和 ultralytics(YOLOv8 的训练入口),如果源码是 YOLOv5 的 train.py 结构,那就要装 requirements.txt 里列的几个包。环境建议:
python -m venv venv_cow source venv_cow/bin/activate pip install --upgrade pip pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 pip install ultralytics opencv-python tqdm pandas这里单独指定 PyTorch 的下载源是为了匹配 CUDA 12.1 的预编译版本,避免 pip 默认装到 CPU 版导致训练慢到怀疑人生。安装完后先跑一个最小推理验证环境:yolo predict model=yolov8n.pt source=test.jpg,能正常出框说明 CUDA、OpenCV、模型加载链路都通。这一步别跳过,很多依赖冲突(比如 OpenCV 版本和 numpy 版本打架)会在这时暴露,比训练到一半再报错好处理得多。
3.2 训练命令与关键参数:yaml 配置和数据路径
训练前先把数据配置写成一个 YAML 文件,这是深度学习视频分析项目里的标准做法。文件里指定训练/验证图片路径和类别名:
# cow.yaml path: ./dataset train: train.txt val: val.txt test: test.txt nc: 1 names: 0: cow注意train和val指向的是第 2 章生成的 txt 文件路径,txt 里每行是图片绝对路径。nc: 1是类别数,只检测牛就是 1;如果项目里同时要做“检测 + 行为识别”(比如趴卧、站立、进食),那就把它拆成多个类别并在 names 里对应列出。训练命令用 ultralytics 的 CLI 是最省事的:
yolo detect train data=cow.yaml model=yolov8s.pt epochs=150 imgsz=640 batch=16 patience=20 project=runs_cow name=exp01参数含义逐个说:model=yolov8s.pt表示加载 COCO 预训练权重做迁移学习,而不是从零训练,牛的纹理特征和 COCO 里的常见物体有部分共享的低层特征,能显著缩短收敛时间;imgsz=640是训练输入尺寸,牛是中大型目标,640 足够,硬拉到 1280 只会增加显存开销和训练时间;batch=16根据显存调整,12GB 显存跑 YOLOv8s 这个值刚好;patience=20表示验证集指标连续 20 轮不提升就早停,防止无效训练烧电费。训练完会在runs_cow/exp01/weights/下生成best.pt和last.pt,同时在runs_cow/exp01/下生成 results.png 和一堆评估曲线图,这就是标题里“评估指标曲线”的原始来源。
3.3 训练过程中的监控:别只盯着终端 loss
训练时我一般开着两个窗口,一个跑训练命令,另一个用tail -f看日志。终端里每轮会打印当前的 box_loss、cls_loss、dfl_loss 和 mAP 指标。这里有个容易误读的点:打印的 mAP 是验证集上的,不代表最终部署效果,但它能不能稳步上升直接反映数据和配置有没有问题。如果前 20 轮 mAP 一直是 0,先别急着调模型——大概率是标注路径错了、类别 ID 不匹配,或者 YAML 里的 txt 文件格式有问题。
训练结束后验证一遍最终权重:
yolo detect val data=cow.yaml model=runs_cow/exp01/weights/best.pt这条命令会重新计算验证集上的 mAP、召回率、精确率,并输出混淆矩阵。更重要的下一步是拿测试集(第 2 章里单独留出的 10%)做一次完整评估,因为验证集已经参与了早停判断,只有测试集能给出接近真实部署的性能估计。测试集评估结果和验证集差距在 2% 以内基本正常,如果掉点多于 5%,说明验证集和测试集分布有偏差,或者训练轮数不够,模型还没收敛就早停了。
4. 读懂评估指标曲线:mAP、PR 与 loss 曲线到底在说什么
4.1 loss 曲线:判断模型有没有好好学
训练输出目录里最常被忽略也最有信息量的是 results.png,它把 loss 曲线和指标曲线拼在一张图里。先看三条 loss:box_loss(边界框回归损失)、cls_loss(分类损失)、dfl_loss(分布焦点损失)。正常情况下三条曲线都应该是训练集上持续下降、验证集上先降后趋于平稳。如果验证集的 loss 在第 80 轮开始反弹而训练集还在下降,这就是过拟合的信号,需要加大数据增强、增加 dropout 或提前早停。
一个我在牛识别检测里反复看到的异常:box_loss 降得很快,但 cls_loss 一直高位抖动。这通常意味着模型能框出“有东西”,但分不清是哪类——如果你训练的是多类别(成牛/犊牛/病牛),排查类别样本均衡;如果只有一个类别,那大概率是标注框里混入了大量背景残留,导致分类分支学到错误特征。这种时候回去检查标注质量比改模型结构更有效。
4.2 PR 曲线与 mAP 的含义:项目验收看哪个数
PR 曲线(Precision-Recall 曲线)是评估指标曲线里的核心,它展示了不同置信度阈值下精确率和召回率的权衡。mAP@0.5 是 IoU 阈值 0.5 时的平均精度,也是大多数农业项目验收时最常用的指标——框只要和真实框重叠一半就算正确检出;mAP@0.5:0.95 是 COCO 的严格标准,要求从 0.5 到 0.95 每 0.05 取一个 IoU 阈值算平均。牛识别检测项目里我一般以 mAP@0.5 为主要验收线,0.95 作为参考。原因很实际:监控场景下目标较大,IoU 0.5 已经能支撑计数和跟踪需求,追求 0.95 的高重叠对牧场管理没有实际收益,反而会逼着模型去拟合标注框的细微边界。
PR 曲线下的面积就是 mAP,曲线越靠近右上角,模型越可能在保持高精确率的同时不丢召回。看 PR 曲线时重点看右侧:如果曲线在精确率 0.8 附近突然垂直下落,说明模型在低置信度区间输出大量误检,部署时置信度阈值必须往上调。
4.3 F1 曲线与混淆矩阵:部署阈值怎么定
results.png 里还有一张 F1 曲线,横轴是置信度阈值,纵轴是 F1 分数(精确率和召回率的调和平均)。这张图直接告诉你部署时该把置信度阈值设成多少。默认的 0.25 未必最优,我在几个牛场项目里的经验是:画面干净、牛群稀疏的圈舍,最优阈值常在 0.3 到 0.4 之间;而背景杂乱、有树木阴影晃动的半开放牛场,阈值要拉到 0.5 以上才能压住误检。不要拍脑袋定阈值,直接从 F1 曲线的峰值点取,然后到现场视频里用 10 分钟实拍片段验证一遍。
混淆矩阵(confusion_matrix.png)则负责回答“误检到底来自哪”。在牛识别检测里最常见的两类误检:把树干、食槽、人误检成牛,以及漏掉黑白花牛中白色占比过高的个体。混淆矩阵能看出背景类被分到了哪个类别,如果“背景”列有大量响应,说明负样本不足,要去采集一些没有牛的圈舍空镜作为背景训练数据。
5. 牛识别检测避坑手记:5 个高频翻车现场与修复方案
5.1 现象:检测框乱跳,同一头牛被重复计数
单帧检测模型在视频流里输出不稳定,一个牛头刚露出来时框很小,走到画面中间框突然变大,跟踪算法把它判成新目标,计数直接翻倍。这是把“目标检测”当“视频分析”用的典型翻车。 原因:检测模型本身不带时序记忆,每帧独立推理,框的抖动被下游计数逻辑放大。 解决:在检测器后面接 Sort 或 DeepSort 跟踪器,用 IoU 关联相邻帧的目标,分配稳定的 track ID。计数改为按 track ID 去重,而不是数框的数量。跟踪器参数里max_age建议设 30 帧,牛走路慢、被遮挡后重新出现还能接回原来的轨迹;min_hits设 3,前 3 帧不输出 ID,避免单帧误检产生孤立轨迹。
5.2 现象:黑白花牛在雪地或强光下大面积漏检
训练时数据来自夏季的棕色地面圈舍,冬天牛场下雪后整片反光,黑白花牛的白色斑块和雪地背景融合,模型直接失明。 原因:训练集和现场环境的域差异,模型学会的是“棕色背景上的牛”,而不是“牛”这个抽象概念。 解决:三个手段叠加使用——数据增强里加随机亮度和对比度扰动,模拟强光;采集雪地场景的负样本和少量带标注的正样本补进训练集;推理时输入分辨率从 640 提到 960,小目标召回率明显改善,代价是帧率下降约三成。这里可以用代码在数据加载时强制做亮度增强:
from ultralytics.data.augment import v8_transforms import random def cow_augment(image, *args, **kwargs): brightness = random.uniform(0.7, 1.3) image = image * brightness return v8_transforms(image, *args, **kwargs)逻辑说明:这段演示了在训练流水线里注入自定义亮度扰动,把亮度系数压到 0.7 到 1.3 的区间,让模型见过更极端的明暗变化。参数上,0.7 是模拟阴天和逆光的下限,1.3 是雪地反光的上限,超过这个范围会让训练图失真严重,模型反而学坏。
5.3 现象:导出 ONNX 或 TensorRT 后精度明显下降
训练好的 best.pt 在 PyTorch 里跑 mAP 0.92,用同一批测试图导出 ONNX 后掉到 0.85,转 TensorRT 后更低。 原因:PyTorch 模型里的部分算子在导出时被替换成近似实现,尤其是上采样和某些激活函数;TensorRT 默认的 FP16 精度对权重分布敏感,直接砍半精度带来损失。 解决:导出时固定输入尺寸,用torch.onnx.export的opset_version=12以上;TensorRT 转换时先做 FP16 精度校准再保存 engine,不直接用默认量化。逐层对比 PyTorch 和 ONNX 的输出,定位偏差最大的算子。常见做法是写一个脚本对同一张图跑两个推理结果,计算特征图逐层余弦相似度,低于 0.99 的层单独处理。
5.4 现象:视频推理卡顿,帧率上不去
GPU 利用率只有 40%,但视频处理速度还是不到 10 FPS,十六路视频流根本跑不动。 原因:瓶颈不在模型推理,而在视频解码和图像预处理。OpenCV 的VideoCapture默认单线程解码,高分辨率 H.265 视频会把 CPU 拉满,GPU 闲着等数据。 解决:用 ffmpeg 硬解码把图像帧喂给推理线程,解码和推理用两个线程通过队列解耦。跳帧策略按场景调整:牛群移动慢的场景每 5 帧推理 1 帧,跟踪器在中间帧用 IOU 插值补轨迹,帧率能提上去且计数几乎不受影响。实时性优先时,把输入尺寸压到 416 并开启 TensorRT FP16,单路推理延迟能控制在 20 毫秒以内。
5.5 现象:评估指标曲线漂亮,现场实拍却一塌糊涂
验证集 mAP 0.93、PR 曲线饱满,部署到现场摄像头视角后漏检、误检全来了。 原因:训练数据多来自人工拍摄的手机图和俯拍无人机图,现场是 4 米高的低角度摄像头,视角和光照差异巨大。指标只代表“测试集分布上的表现”,不代表现场分布。 解决:收集至少 30 分钟现场摄像头视频,抽帧后人工修正标注,用微调方式在现有权重上继续训练 30 到 50 轮,学习率调低到初始值的十分之一。这是深度学习项目里常说的“域适应”里最接地气的一种做法,代价是人工标注成本,但比换模型结构有效得多。
6. 从离线推理到实时视频流:检测加跟踪的部署技巧与验证方法
视频分析项目走到最后一步,是把训练好的权重文件接进实时流。我这里给出一个经过多次项目验证的主循环骨架,它把解码、推理、跟踪、计数串在一起,也用到了前面避坑章节里的几个关键参数:
import cv2 import queue import threading import numpy as np from ultralytics import YOLO from sort import Sort # 这里用常见的 Sort 实现 model = YOLO("runs_cow/exp01/weights/best.pt") tracker = Sort(max_age=30, min_hits=3, iou_threshold=0.3) frame_queue = queue.Queue(maxsize=64) results = {} def decode_worker(stream_url): cap = cv2.VideoCapture(stream_url) while True: ok, frame = cap.read() if not ok: break # 跳帧:每 3 帧取 1 帧进队列 if cap.get(cv2.CAP_PROP_POS_FRAMES) % 3 == 0: frame_queue.put(frame) cap.release() threading.Thread(target=decode_worker, args=("rtsp://camera03",), daemon=True).start() while True: frame = frame_queue.get() dets = model(frame, conf=0.35, imgsz=640, verbose=False)[0] boxes = [] for box in dets.boxes: x1, y1, x2, y2 = box.xyxy[0].cpu().numpy() conf = box.conf[0].item() boxes.append([x1, y1, x2, y2, conf]) tracked = tracker.update(np.array(boxes)) for track in tracked: track_id = int(track[4]) results[track_id] = results.get(track_id, 0) + 1这段代码的关键参数说明:conf=0.35是置信度阈值,这个值来自训练输出目录里 F1 曲线的峰值点,不同场景要重新标定;max_age=30和min_hits=3是跟踪器的两个核心参数,前者控制目标丢失后轨迹的保留时长,后者控制轨迹输出的最小命中帧数。frame_queue设置 64 帧的缓冲是为了吸收解码端的抖动,如果解码速度跟不上推理,队列会自然阻塞,保证不会把旧帧反复喂进模型。跳帧逻辑在解码线程里做,用帧数取模的方式每 3 帧取 1 帧,牛这种慢速目标完全够用。
部署完还有个必须做的验证闭环:拿一段 10 分钟的现场视频,人工逐帧数出每帧牛的数量和总个体数,再和系统输出的 track ID 数量对比。我对这套流程有个执念——每换一个场地就要重复一遍这个人工验证,因为光照、机位、牛群密度任何一个变了,前面的参数都可能要重调。曾经有一次我自信地保留了上一个项目的置信度阈值没动,结果新场地误检率直接翻倍,后来才养成了“验证集指标只是入场券,现场抽帧核对才是验收标准”这个习惯。这套从数据到模型再到部署的链路,每一步都值得较真,希望帮到你。
本文还有配套的精品资源,点击获取