简介智慧牧场牛羊检测数据集面向目标检测初学者、算法竞赛选手及智慧农业开发者可用于牛/羊个体识别、群体计数与牧场巡检等任务适配课程作业、毕业设计及实际系统落地。压缩包约706.2MB文件总数逾万个包含3538张JPG原图以及配套的YOLO txt、VOC xml、JSON三种标签格式主流检测框架均可直接读取免去格式转换。数据集标注规范、分布均衡背景多样且仅含“牛”“羊”两类目标能够支撑模型快速训练与评估。标注文件与图片一一对应便于划分训练集、验证集与测试集流程清晰。目前已有471人浏览学习。下载后即可获得全部图片和三种格式标注既适合快速验证目标检测算法也能直接用于智慧牧场示范项目是一份省心的高质量数据资源。1. 牛羊检测数据集3538张图、两类目标、三种标签一步到位智慧牧场里的牛羊检测听起来只是“找牛找羊”四个字真做起来全是细节。牛棚里光线忽明忽暗草料颜色和牛毛接近羊群聚在一起互相遮挡俯拍镜头下的目标尺度忽大忽小模型在测试集上翻车是常事最后多半都要回头查数据质量。这套牛羊检测数据集一共3538张图片目标类别就是“牛”和“羊”两类背景丰富、分布均匀同一套标注同时给出了VOC(xml)、YOLO(txt)、JSON三种格式等于把训练目标检测要用的原料一次配齐了。做课程设计、打比赛、或者搭一个智慧农场检测计数demo的从业者拿它起步能少踩一批格式转换和标注脏数据的坑。2. 标签格式底层逻辑XML 的框、TXT 的归一化、JSON 的分组结构拿到数据集先别急着训练第一步是把三种标注格式看明白。很多人直接解压就扔给训练脚本结果类别对不上、坐标越界、评估接口报错来回折腾大半天。这一章把三种格式的坐标口径和类别编码讲透后面无论换哪种算法都不会懵。2.1 VOC XML人类可读的标注档案VOC 格式源于 Pascal VOC 比赛一个 xml 文件对应一张图片结构是树状的人眼直接能读。这套数据集的 xml 文件里根节点annotation下依次是图片目录、文件名、尺寸以及一个或多个object节点每个object代表一个标注框。annotation folderimages/folder filename000000054594.jpg/filename size width1920/width height1080/height depth3/depth /size object name牛/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin420/xmin ymin310/ymin xmax860/xmax ymax700/ymax /bndbox /object /annotation注意bndbox里存的是xmin, ymin, xmax, ymax单位是像素坐标值直接对应原图分辨率。读取size里的width和height要格外小心如果之前用脚本批量压缩过图片却忘了回写 xml这里就成了坑后面转 YOLO 格式时归一化会全错。truncated和difficult两个字段在这套数据里基本是 0但它们的语义是“目标被截断”和“目标难以辨认”属于标注质量的元信息自己标注数据集时建议保留。我一般习惯把 xml 当作原始档案保留txt 只当作生成产物因为 xml 可读性好、出问题能溯源。2.2 YOLO TXT模型真正读到的格式YOLO 系列的训练格式不是像素坐标而是归一化后的中心点坐标加宽高。一行对应一个目标五个数字依次是class_id, x_center, y_center, width, height前四个都是 0 到 1 之间的小数类别编号从 0 开始。0 0.546875 0.535156 0.234375 0.312500 1 0.125000 0.620000 0.110000 0.280000第一行表示类别 0牛框中心在图片宽度的 54.6875%、高度的 53.5156% 处框宽是整图宽度的 23.4375%高是整图高度的 31.25%。这个数值怎么来的从 xml 的像素坐标换算公式是x_center (xmin xmax) / 2 / img_widthw (xmax - xmin) / img_widthy 方向同理。之所以要归一化是因为模型输入图片会被缩放如果标签是绝对像素值缩放后还得跟着改归一化之后标签就和输入分辨率解耦了。txt 文件名和图片名完全一致只是后缀不同训练时框架按名字自动去找标签文件。刚接触的人最容易漏掉的一点YOLO 格式里宽高必须是正数如果标注框描反了转换脚本算出来是负值模型训练直接崩。2.3 JSONCOCO 风格的结构化表达JSON 格式走的是 COCO 数据集那套组织方式适合用 pycocotools 做评估或者做实例分割扩展。它不再是“一图一文件”而是把整个数据集的图片信息、标注信息、类别信息分别放进三个数组里。{ images: [ {id: 1, file_name: 000000054594.jpg, width: 1920, height: 1080} ], annotations: [ {id: 1, image_id: 1, category_id: 1, bbox: [420, 310, 440, 390], area: 171600, iscrowd: 0} ], categories: [ {id: 1, name: 牛}, {id: 2, name: 羊} ] }注意bbox字段是[x, y, width, height]也是像素坐标但和 VOC 的xmin, ymin, xmax, ymax表达方式不同。category_id从 1 开始编号而 YOLO 的类别编号从 0 开始这两套体系转换时如果不做加减一处理评估阶段会出现全部类别错位的诡异结果。这套数据把三种格式都备齐意味着做 COCO 风格实验、用官方评估脚本都不用自己写转换器直接加载 JSON 就能跑。2.4 三个格式互相转换时的坐标口径三种格式的差异集中在一张表里转换时照着核对就不会出错。格式坐标字段坐标系类别编号典型用途VOC XMLxmin, ymin, xmax, ymax像素字符串“牛”“羊”人工检查、二次标注YOLO TXTx_center, y_center, w, h归一化 0-10、1目标检测训练COCO JSONx, y, w, h像素1、2模型评估、实例分割像素坐标和归一化坐标的转换很简单归一化 像素值 ÷ 图片宽或高反过来像素值 归一化值 × 图片宽或高但要注意乘完以后一定要取整。我自己踩过最玄学的一次是转换后坐标全部带了一堆小数位可视化时框看起来没差别实际算 mAP 时因为浮点误差叠加边界框的 IoU 计算出现抖动结果集体偏低。所以凡是从 txt 转回 xml 或 json 做可视化坐标一律round()成整数。另外这套数据集的类别名在标注文件里是中文的“牛”“羊”写转换脚本时class_names列表务必用中文字符串别顺手敲成cow、sheep否则匹配不上就成了空标签。3. 把标注整理成可训练的工程转换脚本、校验函数与数据集划分数据集到手后第一步不是训练而是把标签整理成自己能掌控的工程结构。哪怕它已经自带三种格式我也强烈建议你跑一遍校验和划分因为你可能要合并自己的数据、调整类别、或者换目录结构。这一章给出我常用的三个脚本xml 转 yolo 的转换器、标签质量校验器、train/val 划分器全部可以直接跑。3.1 从 XML 生成 YOLO TXT一套能跑的转换脚本虽然数据集已带 txt但实际项目中改标签、加类别、合并数据集的场景太多保留 xml 当源头、随时能重新生成 txt 才是正确姿势。这个脚本用 Python 标准库xml.etree.ElementTree解析 xml不需要装额外依赖。import xml.etree.ElementTree as ET import os def convert_xml_to_yolo(xml_path, txt_path, class_names): 把 VOC 格式的 xml 转成 YOLO 格式的 txt class_names 的列表顺序决定了 YOLO 类别编号比如 [牛, 羊] tree ET.parse(xml_path) root tree.getroot() # 图片尺寸必须从 xml 里读归一化依赖它 size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_names: # 不在类别清单里的目标直接跳过避免脏标签 continue class_id class_names.index(name) box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) # 像素坐标转归一化中心点坐标 x_center ((xmin xmax) / 2) / img_w y_center ((ymin ymax) / 2) / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h # 防止除零或负宽高出现脏数据时直接报错提示 if w 0 or h 0: raise ValueError(f{xml_path} 里有非法坐标: {xmin},{ymin},{xmax},{ymax}) lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) # 一个 xml 对应一个 txt覆盖写 with open(txt_path, w) as f: f.write(\n.join(lines)) if __name__ __main__: class_names [牛, 羊] xml_dir annotations/xml txt_dir annotations/txt os.makedirs(txt_dir, exist_okTrue) for xml_name in os.listdir(xml_dir): if not xml_name.endswith(.xml): continue xml_path os.path.join(xml_dir, xml_name) txt_path os.path.join(txt_dir, xml_name.replace(.xml, .txt)) convert_xml_to_yolo(xml_path, txt_path, class_names) print(转换完成)逻辑说明root.iter(object)会遍历所有层级的 object 节点比findall(object)更稳妥因为有的标注工具会多包一层节点。class_names.index(name)把中文字符串映射到数字编号所以列表顺序定死以后就不能改改了就是灾难。归一化的六个小数位是够用的不需要输出太多位反而会让文件体积变大。参数说明xml_dir指向 xml 文件夹txt_dir是输出目录class_names必须和 yaml 配置文件里的names顺序完全一致否则训练时类别错位。如果你手里的 xml 里类别本来存的是英文 cow/sheep把class_names改成[cow, sheep]即可其他逻辑不用动。3.2 校验脚本检查类别名、坐标越界、空标签转换完标签别急着训练先跑一遍校验。我见过太多人在这里栽跟头标注工具抽风导致框跑到图片外、txt 里混入纯空行、某些图片一个标签都没有。这些问题在训练时不会立刻报错而是表现为 loss 不降、mAP 诡异排查起来非常痛苦。这个脚本能提前把脏数据挑出来。import os from collections import Counter def validate_txt(txt_dir, class_num2): 检查 YOLO txt 标签的格式、类别编号和坐标范围 issues [] class_stats Counter() total_boxes 0 for txt_name in os.listdir(txt_dir): if not txt_name.endswith(.txt): continue path os.path.join(txt_dir, txt_name) if os.path.getsize(path) 0: issues.append(f{txt_name}: 空标签文件建议检查对应图片) continue for line in open(path, encodingutf-8): parts line.strip().split() if len(parts) ! 5: issues.append(f{txt_name}: 行格式错误 - {line.strip()}) continue cid int(parts[0]) if cid class_num: issues.append(f{txt_name}: 类别id越界 - {cid}) coords list(map(float, parts[1:])) # 归一化坐标必须在 0~1 之间超一点都算越界 if any(v 0 or v 1 for v in coords): issues.append(f{txt_name}: 坐标越界 - {line.strip()}) class_stats[cid] 1 total_boxes 1 print(f总框数: {total_boxes}) print(f类别分布: {dict(class_stats)}) if issues: print(f发现 {len(issues)} 个问题:) for item in issues[:20]: print( , item) else: print(校验通过) return issues if __name__ __main__: validate_txt(annotations/txt)逻辑说明空 txt 文件在训练时会被忽略但说明对应图片可能漏标了要不要剔除由你决定。坐标越界的判断用的是严格边界只要小于 0 或大于 1 都会被抓出来因为归一化坐标即使只超出零点几模型推理时都会在图像外产生无效预测框。参数说明class_num设成 2 对应牛、羊两类如果以后扩展类别记得改。encodingutf-8是必须的因为类别名虽然是数字写入 txt但文件本身可能带 BOM 头不加编码参数在 Windows 上容易误读。这个脚本的输出结果里类别分布那一行尤其要盯紧如果牛和羊的数量差距超过 3 倍就要考虑类别不平衡的问题。3.3 划分 train/val随机的谎言与固定随机种子数据集划分看似简单实际有两个隐藏陷阱一是每次运行随机结果不同导致训练不可复现二是只按 txt 划分却忘了同步图片文件训练时 image not found。下面这段代码把图片和标签一对一同步划分用固定随机种子保证结果一致。import os import random import shutil def split_dataset(file_list, train_dir, val_dir, train_ratio0.9): 按图片名划分 train/val并同步移动 jpg 和 txt random.seed(42) # 固定随机种子保证划分可复现 random.shuffle(file_list) split_idx int(len(file_list) * train_ratio) train_files file_list[:split_idx] val_files file_list[split_idx:] for split, files in [(train, train_files), (val, val_files)]: img_dst os.path.join(train_dir if split train else val_dir, images) txt_dst os.path.join(train_dir if split train else val_dir, labels) os.makedirs(img_dst, exist_okTrue) os.makedirs(txt_dst, exist_okTrue) for f in files: # 假设图片是 jpg改成 png 也没问题关键是同步移动 shutil.move(os.path.join(images, f .jpg), os.path.join(img_dst, f .jpg)) shutil.move(os.path.join(labels, f .txt), os.path.join(txt_dst, f .txt)) if __name__ __main__: # 假设 images 目录下是 000000054594.jpg 这种命名 names [f[:-4] for f in os.listdir(images) if f.endswith(.jpg)] split_dataset(names, datasets/train, datasets/val)逻辑说明先把所有文件名的主体部分不含后缀收集起来随机打乱后按比例切片再循环移动图片和标签。这样 train 和 val 两边一定是完整的图片标签对不会出现图有标无、标有图无的情况。参数说明train_ratio0.9是常见做法3538 张图按这个比例划分大约是 3184 张训练、354 张验证验证集规模不算大但够用。如果想更稳可以改成 0.85。随机种子 42 这个数字没有魔法只是为了复现时和别人的划分结果对齐。划分完以后建议打印一下两边的类别分布确认牛和羊在 train、val 里都有足够数量别出现验证集里一类目标只有个位数的情况。4. YOLOv8 训练配置data.yaml、预训练权重与增强参数的选择整理好数据集接下来就是训练。这一章我以 YOLOv8 为例因为它是目前目标检测里上手最快、文档最全的框架之一这套数据集直接喂给 YOLOv5、YOLOv8、YOLOv11 都能用。重点讲三个容易出错的地方data.yaml 怎么写、预训练权重怎么选、增强参数怎么调。4.1 data.yaml 的写法与路径陷阱YOLO 训练的第一步是写一个数据配置文件告诉框架去哪里找图、找标签以及类别叫什么。这里有个高频翻车点names的顺序必须和 txt 里的class_id严格对应txt 里 0 是牛、1 是羊yaml 里 0 就必须是牛。# data.yaml path: /home/user/datasets/cattle_sheep train: images/train val: images/val names: 0: 牛 1: 羊路径配置有三条规则path是数据集根目录train和val相对于根目录写如果path用的是相对路径那它相对于你执行训练命令的终端目录换个终端跑就可能找不到图中文目录名不是不能用但 Windows 下概率性出编码问题我一般直接避开所有路径都用英文。names的键可以写 0、1也可以省略键直接写列表形式names: [牛, 羊]效果相同注意这个数据集里类别名是中文yaml 文件保存时务必选 UTF-8 编码。4.2 预训练权重与训练命令从零训练一个检测头收敛慢而且精度往往不如迁移学习。YOLOv8 官方提供在 COCO 上预训练好的权重文件下载下来当起点训练新数据集时有先验特征收敛速度快很多。这套数据集只有两个类不需要用最大的模型从yolov8s起步最合适。yolo detect train \ datadata.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ device0 \ projectruns \ namecattle_sheep_v1参数说明modelyolov8s.pt是官方预训练模型下载第一次运行会自动拉到本地如果网络环境受限就手动下载后放在当前目录。imgsz640是训练输入分辨率牛棚图片多数是俯拍目标中等偏小如果显存允许可以提到 768对小目标更友好。batch16在显存 12GB 左右的卡上跑 yolov8s 比较宽裕拿不准就设batch-1让框架自动探测最大安全批次。先说清楚一个常见误区epochs100不是越多越好。这套数据集的规模和复杂度正常 60 到 80 个 epoch 就能收敛训练时盯着验证集 mAP连续 20 个 epoch 不再上升就可以提前停。device0指定第一块 GPU没有 GPU 就删掉这个参数用 CPU但训练时间会显著变长。4.3 训练过程里真正要盯的指标训练日志里会刷出一堆指标不用全盯重点看四个。loss 在下降就不用慌mAP50 是最直观的“框得准不准”指标mAP50-95 更严格混淆矩阵则能看出牛和羊互相误检的情况。指标含义这套数据集的合理预期loss训练损失下降趋势比绝对值重要前 20 个 epoch 明显下降mAP50IoU 阈值 0.5 时的平均精度0.85 以上mAP50-95IoU 从 0.5 到 0.95 的平均0.60 以上混淆矩阵每类的查全率和误检情况牛羊互检比例低于 5%关于混淆矩阵有个容易误会的地方矩阵里每行加起来不等于 100%因为一个预测框可能被 NMS 合并也可能一个真实目标产生多个预测框。所以看到“总和不唯一”是正常现象别去纠结归一化。真正要看的是对角线的数值够不够大以及牛和羊之间有没有明显的串检。4.4 数据增强参数的取舍YOLOv8 的增强默认值在大多数场景下表现不错但牛羊检测有自己的特殊性牛羊是群居动物图片里动辄十几只挤在一起Mosaic 增强会把四张图拼在一起加剧遮挡和重叠模型容易学到残缺特征。我一般会调一下增强参数。# hyp.yaml 中抽出的常用增强参数 hsv_h: 0.015 # 色调增强幅度牛棚光线偏黄小幅增强帮助泛化 hsv_s: 0.7 # 饱和度增强幅度 hsv_v: 0.4 # 亮度增强幅度 fliplr: 0.5 # 水平翻转概率 flipud: 0.0 # 垂直翻转概率俯拍牛羊有方向性不建议开 mosaic: 0.8 # 从默认 1.0 调低减轻密集遮挡 scale: 0.5 # 随机缩放比例 translate: 0.1 # 随机平移比例参数说明flipud设成 0 是因为牛羊本身有朝向垂直翻转等于把牛头朝下模型会学到不符合实际的特征。mosaic从 1.0 降到 0.8 是密集遮挡场景的常见做法如果训练时发现小目标召回一直上不去可以再降到 0.5。hsv_v亮度增强保持 0.4 左右就够了牛棚本来光线就不稳定增强过猛会让模型对光照变化过度敏感。这些参数写在 yaml 文件里训练命令加一行hyphyp.yaml即可生效。5. 训练避坑指南五条血泪经验覆盖坐标、类别与增强这一章写训练这套牛羊数据集时最常踩的五个坑每个都是真实翻车现场按“现象 → 原因 → 解决”的顺序写照着排查能省下大半天时间。5.1 图片路径读不到训练直接崩现象训练刚开始就报FileNotFoundError说找不到某张图片或者训练能跑但 val 阶段不停报image not found最后 mAP 全是 0。原因最常见的情况是数据集解压后目录层级变了yaml 里写的相对路径指向了错误的文件夹。另一个原因是划分脚本只移动了 txt 忘了移动 jpg导致标签和图片对不上。解决先跑一遍find . -name *.jpg | head -20看图片实际位置再对照 yaml 里的train和val路径。我一般直接把path写成绝对路径一劳永逸不要省那几步键盘操作。划分完数据集后随手统计一下子目录里的 jpg 和 txt 数量两边数字不一致就是同步出了问题。5.2 坐标越界但 xml 看起来很正常现象训练 loss 降得很慢可视化验证集时发现有的框跑到图片外头或者某些框的大小明显异常。原因xml 里图片尺寸字段和图片实际分辨率不一致典型场景是标注时用 1920×1080 的图后来压缩成 1280×720 忘了回写size。转换脚本拿旧的宽高做归一化算出来的坐标自然偏了。解决写一个脚本遍历所有 xml用 PIL 读取实际图片尺寸和 xml 里的width、height对比不一致的单独列出来。处理方式很简单以实际图片尺寸为准重新生成 txt。从那以后我每次拿到数据集第一步永远是跑一遍“xml 尺寸 vs 实际尺寸”的核对脚本这招帮我挡掉了至少三次训练事故。5.3 牛和羊的标签顺序搞反现象训练完模型后把牛识别成羊、把羊识别成牛但 val mAP 却不算太低整体看起来“能用又不对劲”。原因txt 里的class_id和 yaml 里的names顺序对不上。比如你重新转换过标签class_names写成了[羊, 牛]但 yaml 里还是0: 牛, 1: 羊模型学到的 0 类实际是羊预测时全错位。解决训练前打开任意一张图的 txt 文件对照 yaml 的 names 人工验证一遍再跑一次可视化把预测框画到图上直接看。这个检查只需要两分钟但能避免训练十几个小时后才发现白跑了。我现在的习惯是训练命令一执行马上打开第一轮验证输出图看一眼框上的类别名。5.4 远景羊群漏检近景牛栏误检现象图片中远处的羊群基本检测不到近处单独一头牛反而频繁出现重复框或误检框conf 调高漏检、调低错检怎么都不舒服。原因典型的目标尺度问题。图片里远处羊群可能只占几十个像素属于小目标而imgsz640把原图压缩后小目标特征更弱。同时 NMS 阈值设置不当近处大目标会产生多个高置信度预测框互相竞争。解决把imgsz提到 768 或 1280显存足够时给小目标多留点像素。推理时不要死磕一个 conf 阈值先用conf0.25, iou0.5跑一遍看漏检和误检的分布再决定往哪个方向调。如果近处大目标误检为主把 conf 提到 0.4如果远处漏检为主把 conf 降到 0.15 同时把 iou 提到 0.6。5.5 Mosaic 增强导致小目标质量下降甚至 loss 突跳现象前 30 个 epoch 训练正常后面 loss 开始反复震荡或者验证集上小目标召回率一直上不去怎么看都像是欠拟合。原因密集牛羊场景下Mosaic 把四张图拼接大量标注框被裁剪或重叠遮挡模型在增强后的数据里看到的“牛”很多是不完整的碎片。极端情况下增强过度还会导致 loss 突然跳到 NaN有经验的同行会把这个现象叫做 yolo 训练里的 BN 层崩溃本质是输入分布被增强破坏后 BatchNorm 统计量失稳。解决训练配置里把mosaic从 1.0 降到 0.5并在最后 10 个 epoch 强制关闭YOLOv8 支持设置close_mosaic10让模型在最后阶段回到真实分布微调。如果 loss 已经出现 NaN先把增强全部关掉重跑最后的微调段再逐步加回。这套数据集的牛棚场景我实测mosaic0.5比默认 1.0 的 mAP50-95 高出约 3 到 5 个点代价是训练速度略慢但完全值得。6. 计数逻辑落地推理脚本、跨帧去重与 ONNX 导出训练完模型只是第一步实际项目里真正要解决的是“数清楚棚里有多少牛羊”。这一章讲推理命令、跨帧去重计数逻辑以及导出 ONNX 部署时的关键坑。6.1 用训练好的权重跑视频和图片训练完的权重在runs/cattle_sheep_v1/weights/best.pt拿来做推理只需要一条命令yolo predict \ modelruns/cattle_sheep_v1/weights/best.pt \ sourcetest_video.mp4 \ conf0.25 \ saveTruesource可以是单张图片、图片文件夹、视频文件也可以直接接摄像头设备号。conf按 5.4 节说的方法去调saveTrue会把标注后的结果保存下来。第一次跑完一定要打开结果看几帧确认框的位置和类别都对别直接进计数环节。6.2 跨帧去重别把同一头牛数三遍视频检测里最经典的坑是同一只牛羊在连续帧里被重复计数。简单做法是做一个基于 IoU 的帧间追踪from ultralytics import YOLO model YOLO(runs/cattle_sheep_v1/weights/best.pt) def iou(box1, box2): # box 格式: [x1, y1, x2, y2] xx1 max(box1[0], box2[0]) yy1 max(box1[1], box2[1]) xx2 min(box1[2], box2[2]) yy2 min(box1[3], box2[3]) inter max(0, xx2 - xx1) * max(0, yy2 - yy1) area1 (box1[2] - box1[0]) * (box1[3] - box1[1]) area2 (box2[2] - box2[0]) * (box2[3] - box2[1]) return inter / (area1 area2 - inter 1e-6) prev_boxes [] total_count 0 for frame in video_stream: results model(frame, conf0.25) boxes results[0].boxes.xyxy.cpu().numpy() new_boxes [] for box in boxes: # 与上一帧的框比较匹配上了就认为是同一目标 matched any(iou(box, prev) 0.3 for prev in prev_boxes) if not matched: total_count 1 new_boxes.append(box) prev_boxes new_boxes逻辑说明每一帧检测出的框和上一帧的框做 IoU大于 0.3 就认为是同一个目标不重复计数新出现的框才加一。iou的实现没有用任何库纯 Python 就能算方便你移植到别的项目里。参数说明prev_boxes只保留了最近一帧的框实现简单但有极限如果牛羊走出画面再走回来会被二次计数。要更稳的做法是引入 ByteTrack 这类追踪器但代码量会上去。对课程设计和 demo 来说单帧去重已经够用。iou_threshold0.3适合牛羊这种移动不算快的目标如果场景是快速奔跑的动物阈值要降到 0.2否则相邻帧位移太大匹配不上。6.3 导出 ONNX 部署的边界条件实际部署时一般不用 PyTorch 权重而是导出成 ONNX 格式。命令很简单坑在导出参数yolo export \ modelruns/cattle_sheep_v1/weights/best.pt \ formatonnx \ dynamicTrue \ opset12dynamicTrue允许动态输入尺寸但会牺牲一点推理速度如果你的部署平台固定输入尺寸就别开 dynamic。导出后要确认一件事输出的后处理节点是原样保留还是被移除了YOLOv8 导出的 ONNX 通常带 NMS 节点但不同版本的 ultralytics 导出的输出结构会有差异部署前用 onnxruntime 跑一张图验证别等接进服务才发现维度对不上。说句实在话我在牧场项目里栽过最大的跟头不是模型精度而是计数逻辑和部署链路。模型 mAP 再高去重不过关后台统计的存栏数照样对不上账。从那以后我每次接检测项目都会强制走一遍标准流程先可视化验证框、再跑计数去重、最后 ONNX 导出做单图冒烟测试全过了才算完事。这套流程帮我把交付后的返工率降了大半希望帮到你。本文还有配套的精品资源点击获取