☰
基于YOLOv5的明厨亮灶老鼠检测实战:数据、训练与部署
2026/9/26 3:13:39 网站建设 项目流程

简介:目标检测是计算机视觉中的基础任务,旨在定位图像中的特定目标。YOLO系列算法凭借单阶段检测的实时性优势,成为工业落地的热门选择。基于YOLOv5的训练流程,涉及数据标注格式转换、目录组织、增强策略等关键环节,直接决定模型精度。通过合理划分数据集、调整超参数,可有效解决小目标、遮挡等问题。该技术广泛应用于明厨亮灶场景下的老鼠检测,结合ONNX/TensorRT导出和阈值调优,能够在边缘设备上实现实时告警。本文围绕老鼠检测任务,完整呈现从数据准备到部署的全流程避坑经验,帮助团队快速构建可复现的检测方案。

1. 明厨亮灶与老鼠检测:为什么YOLOv5这套组合值得直接投入

明厨亮灶项目里最棘手的一类检测就是老鼠:目标小、移动快、拍摄条件差(往往是夜间红外或逆光厨房),漏检一个就是食安事故。基于YOLOv5的老鼠检测源码+模型+2018张图片及对应标签,解决的正是“有数据、有模型、有可复现训练流程”这三个卡点,让团队不用从零造轮子。如果你是负责食安算法或边缘设备落地的工程师,这套组合能直接帮你砍掉数据采集和模型调参前两个月的空窗期。下面从数据、训练、部署三个方向把这条路径拆开讲。

2. 数据集整理:2018张老鼠图片的标签格式、目录结构与划分策略

2.1 标签文件长什么样:从VOC XML到YOLO TXT

基于YOLOv5训练的老鼠检测项目,标签必须放在和图片同名的.txt文件里,每一行代表一个标注框,格式是:

class_id center_x center_y width height

其中center_x、center_y、width、height全部是相对于图片宽高的归一化值,范围 0 到 1。举个例子,一张 1920×1080 的图上有一只老鼠,框的左上角在 (480, 270),宽 640,高 360,那对应的一行就是:

0 0.5 0.375 0.3333 0.3333

因为center_x = (480+640/2)/1920 = 0.5,center_y = (270+360/2)/1080 = 0.375,width = 640/1920 ≈ 0.3333,height = 360/1080 ≈ 0.3333。class_id是整数,从 0 开始计数。对这个项目来说,类别只有rat一个,所以 class_id 恒为 0。

如果标注入手是 VOC 的 XML 文件,需要先转换。我一般用这一段 Python 脚本:

import os import xml.etree.ElementTree as ET from glob import glob def voc_to_yolo(xml_path, out_dir, class_names): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in class_names: continue cls_id = class_names.index(name) box = obj.find("bndbox") x1 = float(box.find("xmin").text) y1 = float(box.find("ymin").text) x2 = float(box.find("xmax").text) y2 = float(box.find("ymax").text) cx = (x1 + x2) / 2 / img_w cy = (y1 + y2) / 2 / img_h w = (x2 - x1) / img_w h = (y2 - y1) / img_h lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") if not lines: return out_path = os.path.join(out_dir, os.path.splitext(os.path.basename(xml_path))[0] + ".txt") with open(out_path, "w") as f: f.write("\n".join(lines)) class_names = ["rat"] for xml_file in glob("labels_voc/*.xml"): voc_to_yolo(xml_file, "labels_yolo", class_names)

这段脚本做的事就是把 XML 里的绝对坐标除以图片宽高变成归一化值。两个坑要注意:第一,有些标注工具的坐标是整数,有些是浮点,统一除以宽高即可;第二,图片尺寸必须以 XML 里<size>标签的值为准,不要自己用 PIL 去读,因为标签和图片可能来自不同批次,读出来的尺寸会对不上。如果你的数据集里混有未标注的老鼠样本,也就是空标签文件,YOLOv5 在训练时会跳过它但不报错,如果混进验证集则会把漏检算进去。我一般把空标签文件单独放到一个empty/目录,不混进训练集,避免对 loss 产生干扰。

坐标精度方面,代码里输出保留了 6 位小数,实际使用中足够。即使是一张 4000×3000 的超高清图,6 位小数对应的误差也只有 0.004 像素级别。但有一种情况需要注意:如果标签来源是 JSON 格式的 COCO 标注,比如某些标注平台导出的annotations.json,里面的坐标是[x, y, width, height]的绝对像素值,且x, y是左上角,转成 YOLO 格式时不要忘记把width/2加到 x 上算出中心点。这个“左上角坐标 vs 中心点坐标”的差异,是我见过最多的一次性转换错误。

提示:标注坐标的精度控制在小数点后 4 位即可,过高的精度不会带来可观察的提升,反而会让标签文件体积变大、读取变慢。

2.2 目录组织:让 YOLOv5 的 dataloader 不报错

用这套源码训练,数据目录要匹配 YOLOv5 的约定。官方源码里datasets的默认结构是:

dataset/ ├── images/ │ ├── train/ │ │ ├── cam01_20240518_143201.jpg │ │ └── ... │ └── val/ │ ├── cam02_20240518_143212.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── cam01_20240518_143201.txt │ │ └── ... │ └── val/ │ ├── cam02_20240518_143212.txt │ └── ... └── rat.yaml

images和labels两个目录必须严格同名配对,YOLOv5 的 dataloader 通过替换后缀来找对应 label。如果你的原始文件命名混乱,例如IMG_20240101_120000.jpg对应20240101_120000.txt,先统一重命名再进训练,不要让脚本去猜对应关系。下面的 bash 命令可以把一个乱序目录批量改成train_000001.jpg这种名字:

i=1 for img in raw_images/*.jpg; do new_name=$(printf "train_%06d" $i) mv "$img" "dataset/images/train/${new_name}.jpg" i=$((i+1)) done

注意这里只重命名图片,标签文件需要在同一步里做同样映射,否则对不上。更稳妥的做法是先导出 CSV 映射表,再让两个重命名过程读同一份映射,不要各写一遍循环。我实际处理 2018 张图时是按摄像头编号加时间戳来命名,例如cam03_20240518_143201.jpg,这样后期排查某次漏检时能直接定位到设备和时间点。如果你的标签文件名和图片文件名不一致,用下面这段小脚本检查同名文件是否成对:

for img in dataset/images/train/*.jpg; do base=$(basename "$img" .jpg) if [ ! -f "dataset/labels/train/${base}.txt" ]; then echo "missing label: $base" fi done

这个巡检脚本在我接手别人给的数据集时几乎是必跑的。很多标注平台导出的文件夹里会混有图片和多个版本的标签,名称对不齐是常态。不先把成对关系确认好,训练出来的模型会出现大量漏检,而且很难排查是模型问题还是数据问题。

2.3 划分策略:别用随机划分,按摄像头和时间切

明厨亮灶的数据有一个特殊性:同一个厨房的摄像头,背景、光照、视角完全不同。如果随机划分训练集和验证集,验证集里很可能会出现和训练集同场景的相似帧,val loss 会看起来很低,但实际换个摄像头就翻车。我一般按摄像头 ID 切分,80% 的摄像头进训练,20% 的摄像头进验证,保证验证集的数据分布接近真实部署。

划分代码大致长这样:

import os import random from collections import defaultdict src_images = "dataset/images/all" cam_map = defaultdict(list) for img in os.listdir(src_images): cam_id = img.split("_")[0] # 假设文件名以 cam03 开头 cam_map[cam_id].append(img) random.seed(42) train_imgs, val_imgs = [], [] for cam_id, imgs in cam_map.items(): random.shuffle(imgs) split = int(len(imgs) * 0.8) train_imgs.extend(imgs[:split]) val_imgs.extend(imgs[split:]) # 按摄像头统计数量,样本不足的摄像头做特殊处理 for cam_id, imgs in cam_map.items(): print(cam_id, len(imgs))

这里random.seed(42)让每次划分结果一致,方便复现;对每个摄像头内部做随机切分,而不是在全部图片上随机切。如果 2018 张图里某个摄像头只有 10 张,按 80% 切会导致验证集只有 2 张,这种情况我把它全部并入训练集,验证集只保留样本量足够的摄像头。这也是明厨亮灶这类多路视频项目和数据比赛最大的差别:数据分布跟着摄像头走,不跟着类别走。

一个更严格的做法是把同一摄像头的连续时间抽帧也区分开,比如摄像头 A 的前 80% 时间帧进训练集,后 20% 进验证集。这样连时间偏差都防住了。我在第一个明厨亮灶项目里用随机划分,上线两周后挨了一个真实场景的漏检事故,从那以后一律按摄像头加时间双重切分。

2.4 数据增强与样本平衡:2018张图怎么用出5000张的效果

2018 张图在目标检测里不算多,尤其是老鼠这种小目标。增强策略不要照搬 COCO 的默认配置,重点做三类调整。

第一是 mosaic。YOLOv5 默认开启马赛克增强,从训练集里随机拿 4 张图拼成一张。老鼠目标小,mosaic 能帮模型学会在不同背景尺度下识别,但同时会让小目标被裁掉一部分。我会把训练超参里的mosaic概率从 1.0 降到 0.5,避免太多目标被切碎。降这一点增强概率不会减少有效样本量,因为 mosaic 增加的是背景多样性,老鼠本身没有被复制,所以放心调。

第二是 HSV 抖动。不同厨房的灯光色温差异很大,有的暖黄、有的冷白。hsv_h、hsv_s、hsv_v分别控制色调、饱和度和明度的抖动范围。我会把hsv_h从默认的 0.015 调到 0.02,让模型对色温更鲁棒。注意不要调得过大,hsv_h超过 0.05 后老鼠的毛色会失真,模型会把黑色老鼠和灰色老鼠混成同一特征,反而降低区分度。

第三是翻转。fliplr=0.5随机水平翻转不是所有场景都适用。如果老鼠在画面里的运动方向有规律,例如都从下水道口向左跑,水平翻转会制造反向样本,增加学习难度。我一般先看一眼数据里标注框的运动轨迹分布再决定开不开。一个更实际的建议是:在hyp.scratch.yaml里把这些增强参数写成一段注释,标明改过哪几项、为什么改,半年后回来调参时不用重新猜。

3. 源码与训练:把 YOLOv5 老鼠检测跑起来的最小命令链

3.1 源码结构与环境配置:不要急着跑 train.py

拿到这套明厨亮灶的老鼠检测源码,不要急着跑train.py,先花十分钟确认版本和依赖。YOLOv5 的源码在 2022 年后进入维护模式,但仍然是目标检测框架里最稳定的一个。核心入口就三个文件:

  • train.py:训练入口,加载数据、初始化模型、执行前向反向传播,权重保存到runs/train/;
  • detect.py:推理入口,支持图片、视频、摄像头和 RTSP 流,输出带框图像和标签文件;
  • models/yolov5s.yaml等:模型结构配置,定义网络的深度和宽度。

YOLOv5 环境配置是我见过翻车率最高的环节。我用 conda 建一个干净的虚拟环境:

conda create -n rat-yolov5 python=3.8 -y conda activate rat-yolov5 cd yolov5-master pip install -r requirements.txt

如果你的机器上 CUDA 版本是 11.x,requirements.txt里的 torch 版本有可能被装成 CPU 版,尤其是使用国内镜像源时经常发生。我习惯装完先跑一段验证命令:

python -c "import torch; print(torch.__version__, torch.cuda.is_available())"

输出类似2.0.1+cu118 True的这样一段,才说明 GPU 可用。如果torch.cuda.is_available()返回False,说明 torch 装成了 CPU 版,需要按你的 CUDA 版本重新装对应 wheel。这一步不要跳过,我见过好几个同事卡在这里半天,训练速度肉眼可见地慢,还以为是机器性能不行。

3.2 最小训练命令:从2018张图到可用权重

数据就绪后,训练的基本命令是:

python train.py \ --data rat.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 32 \ --epochs 200 \ --workers 8 \ --project runs/rat_train \ --name v1

几个关键参数说明:

  • --data指向rat.yaml,文件里定义train、val目录路径和nc(类别数,这里为 1)以及names(类别名列表);
  • --weights yolov5s.pt表示加载 COCO 预训练权重迁移学习。老鼠检测从 COCO 权重起步比从零训练收敛快不少,这一步值得做;
  • --img 640是训练输入边长。2018 张图里老鼠大多是小目标,可以考虑把img提到 800 或 960,但显存占用明显上升;
  • --batch 32在单卡 24GB 显存下没问题,如果你的卡只有 12GB,降到 16 或把--img降到 640;
  • --epochs设 200,但 YOLOv5 默认开启早停patience=50,也就是连续 50 轮验证集指标不涨会自动停,不用怕跑到最后过拟合。

再展开rat.yaml的内容:

path: /home/user/rat_dataset # 数据集根目录 train: images/train val: images/val nc: 1 names: 0: rat

path是数据根目录,YOLOv5 会把path与train/val拼接成完整路径。注意nc: 1和names里的rat必须和标签文件里的class_id=0对应一致。实际中经常有人把nc写成数据集文件夹里子目录的个数,这是完全错误的,nc是目标的类别总数,不是文件夹个数。如果names: [rat]写成了names: [rat, mouse],模型会认为有两个类别,而标签里的 0 指向rat,训练 loss 会很高且模型学不出来。

3.3 训练过程的三个关注点:loss曲线、mAP与权重选择

训练真正要盯的是三样东西:box_loss、obj_loss和验证集 mAP。YOLOv5 每个 epoch 都会在终端输出指标,单类检测时cls_loss恒为 0,所以日志里看不到它是正常的。训练到 100 轮左右,观察终端输出:

Epoch GPU_mem box_loss obj_loss cls_loss Instances Size 99/199 9.7G 0.021 0.231 0 640

obj_loss从训练初期的 1.0 以上降到 0.2 左右,说明模型开始学到目标的存在性。如果训练结束后验证集mAP@0.5达到 0.9 以上,这个模型基本可以试部署;如果只有 0.6-0.7,常见原因包括标注框不紧贴目标、数据里目标过小、或者某个摄像头视角占比过高,先回去查数据,不要盲目加训练轮次。加轮次只会让模型在训练集上记忆得更深,对验证集的帮助非常有限。

训练结束后,runs/rat_train/v1/weights/下有两个权重:best.pt和last.pt。best.pt是验证集指标最优权重,last.pt是最后一个 epoch。多数情况下选best.pt。例外是:如果best.pt的 mAP 高是过拟合某个摄像头的结果,而last.pt泛化更均衡,那用两个权重各跑一遍验证集,对比每个摄像头上的漏检数再决定。这个对比过程不复杂,但值得做,它能让你明白选权重不是只看一个数字。

3.4 显存不足时的降级路径

用 8GB 显存的卡训练 640×640 输入,默认 batch 32 大概率爆显存。降级路径我一般按这个顺序调:

python train.py \ --data rat.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --workers 4 \ --epochs 200 \ --cache-ram

先调 batch 到 16,再调--workers到 4。最后一个--cache-ram会把图片提前读进内存,减少磁盘 IO 但增加内存占用,内存不到 16GB 就不要开。如果 batch 16 还不行,就把模型从yolov5s换成yolov5n,这是 YOLOv5 最轻量的版本,精度会降一些但训练稳。不要一上来就换yolov5m或更大模型,老鼠检测目标小、单类别,s级别已经够用。这里有个小技巧:如果只是验证代码能不能跑通,可以先--epochs 1跑一次全流程,确认数据加载和 loss 计算没报错,再正式开长训练。

4. 推理部署与超参数调优:明厨亮灶边缘设备上的模型稳定化

4.1 推理命令与置信度、IoU 阈值的联动

训练完,用detect.py跑推理:

python detect.py \ --weights runs/rat_train/v1/weights/best.pt \ --source rtsp://your-ip:554/stream1 \ --img 640 \ --conf-thres 0.35 \ --iou-thres 0.45 \ --max-det 20 \ --device 0

--conf-thres是置信度阈值,老鼠检测推荐从 0.35 起步。调低到 0.25 能提高召回,但误报会明显增多;调到 0.5 以上则会漏掉动态模糊的老鼠。--iou-thres是 NMS 的 IoU 阈值,默认 0.45。目标密集时相邻两个老鼠的框会被合并成一个,导致只出一个框,我会把--iou-thres降到 0.35 试试。实际场景里这两个阈值是联动关系,一次只调一个,不要同时大幅改动,否则出了问题你没法判断是哪个参数引起的。

明厨亮灶项目摄像头位多、预算紧,推理资源分配是个实在的问题。单路视频流如果用 GPU 推理,一张卡能跑 4-8 路,取决于帧率;如果只做抽帧检测,比如每秒抽 2 帧,CPU 也能扛住 1080p 输入。我自己的经验是:先用 GPU 把模型跑稳定,评估准确率达标后再考虑降级到 CPU 抽帧方案,不要一开始就抠算力。

不同场景下阈值选择可以参考这个表:

参数推荐值适用场景
conf-thres0.25-0.30告警优先,追求召回,容忍误报
conf-thres0.40-0.50误报敏感,追求精确率
iou-thres0.30-0.35画面中多只老鼠并存
iou-thres0.45-0.50常规单目标画面

4.2 把权重导出成 ONNX 与 TensorRT

边缘设备上直接跑 PyTorch 推理,性能通常不达标。Jetson Nano 这类设备跑 640×640 输入,PyTorch 的推理延迟可能在 200ms 左右,无法满足实时监控的需求。我一般先把best.pt转 ONNX,再在设备上转成 TensorRT engine。YOLOv5 自带导出脚本:

python export.py \ --weights runs/rat_train/v1/weights/best.pt \ --include onnx engine \ --img 640 \ --batch 1

导出后同目录下会有best.onnx和best.engine。注意,engine 文件绑定生成它的 GPU 型号和 CUDA/TensorRT 版本,换台不同型号的设备要重新生成,不能直接拷贝。如果集成方只用 ONNX Runtime 或 OpenCV DNN,只导出 ONNX 即可:

python export.py --weights best.pt --include onnx --opset 11 --simplify

--opset指定 ONNX 算子集版本。目标运行时如果只支持低版本 ONNX,比如嵌入式环境里的 onnxruntime 1.4.x,就显式指定--opset 11。--simplify会对计算图做简化,多数情况下能减少 10%-20% 的推理耗时,但个别情况下会重排输出节点的名称,集成前先用一张测试图确认输出结构。

注意:TensorRT 推理引擎与生成它的 GPU 型号强绑定,跨硬件直接拷贝推理引擎文件会报错,换设备就在该设备上重新导出。

4.3 推理前后处理:坐标缩放与实时视频流

明厨亮灶摄像头多为 1080p 或更高,模型输入是 640×640。最常用的方案是:先用 OpenCV 读取帧,缩放到 640 再送模型推理,同时保留原始帧用于画框和回传平台。这里的关键是等比例缩放,不要直接cv2.resize(frame, (640, 640))把图片拉伸变形,目标会变扁,检测框也会跟着偏移。我实际写出来的推理循环长这样:

import cv2 import torch model = torch.hub.load("yolov5", "custom", path="best.pt", source="local") cap = cv2.VideoCapture("rtsp://your-ip:554/stream1") while True: ret, frame = cap.read() if not ret: break # 等比例缩放,保持原始比例避免目标挤压变形 h, w = frame.shape[:2] scale = 640 / max(h, w) new_w, new_h = int(w * scale), int(h * scale) resized = cv2.resize(frame, (new_w, new_h)) results = model(resized) for det in results.xyxy[0].cpu().numpy(): x1, y1, x2, y2, conf, cls = det if conf < 0.35: continue # 还原到原始分辨率再画框 cv2.rectangle(frame, (int(x1/scale), int(y1/scale)), (int(x2/scale), int(y2/scale)), (0, 0, 255), 2) cv2.imshow("rat", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break

这段代码有两个容易出错的地方。第一,画框时坐标必须除以scale,因为模型看到的是缩放后的图,检测框坐标是缩放坐标系下的,直接画到原图上会偏移,我最初接手项目时吃过这个亏。第二,torch.hub.load里source="local"必须写明,否则在隔离网段里它会尝试联网拉取代码,卡住很久甚至直接抛异常。如果你的部署环境完全没有外网,建议改成直接用torch.load加载权重,绕开 hub 机制。

5. 明厨亮灶老鼠检测避坑实录:从数据、训练到部署的五个典型雷区

5.1 现象:loss 正常但 val mAP 始终不过 0.7

原因:数据里存在大量重复场景帧。同一个摄像头的视频抽帧出来的连续帧几乎一样,训练集和验证集混入了相似帧,模型记忆了场景细节而不是老鼠特征。

解决:抽帧间隔改成 2 秒一帧而不是逐帧抽取;按照 2.3 节的“摄像头加时间段”双重划分重建数据集。我曾经在一个项目里抽帧密度设为 5 帧每秒,mAP 看着很高,但实际部署到新摄像头上漏检严重,这就是血泪经验。数据的时序相关性是明厨亮灶项目最容易踩的坑,监控视频天然连续,抽帧太密等于把同一只老鼠复制了几十份给模型看,它在训练集上“背题”而不是“解题”。

5.2 现象:训练集 mAP 0.95,换一个摄像头实测几乎全漏

原因:训练集里某一类视角占比过高,模型被带偏到这个视角。比如 2018 张图里 1400 张来自吊顶俯拍视角,模型学会的是“从上面看的老鼠”,换个墙角侧拍的摄像头就失效。

解决:按摄像头划分数据,确保训练集和验证集来自不同摄像头。如果数据里视角覆盖不均衡,要补采数据或对少数视角做定向增强,包括旋转、透视变换,而不是简单复制少数类样本。复制样本不增加视角多样性,只会让模型对同一批图过拟合。

5.3 现象:训练中途报 CUDA out of memory

原因:batch 或 img 参数超出显存,入门最常见的问题。

解决:按 3.4 节的降级路径调整,把 batch 降到 16、8,直至能训练。如果换到yolov5n仍然不够,检查是不是--workers开得太大导致内存膨胀。注意一个细节:YOLOv5 默认会缓存图片到显存,训练日志里的GPU_mem会在训练一段时间后才稳定,不要在第一轮看到报错就认定是显存不足,先让训练跑 10 个 step 再看。如果只跑 1 个 epoch 测试代码,记得把--cache-ram关上,否则测试和正式训练的显存表现会有差异。

5.4 现象:推理时同一只老鼠出现大量重叠框

原因:NMS 的--iou-thres设置过高,多个相邻候选框没被抑制,或者--conf-thres过低导致大量低置信度框被保留。

解决:先把--conf-thres提到 0.5 观察误报数量,再单独调--iou-thres到 0.3-0.35。调参逻辑是固定一个动另一个,用消融的方式看效果。如果调完还是一堆框,检查是不是用了多尺度推理且没有在后处理合并结果,YOLOv5 在不同版本里对多尺度输出的拼接处理有差异,升级源码版本后行为可能变化。

5.5 现象:ONNX 输出解析错误,shape 对不上

原因:YOLOv5 的 PyTorch 输出是结构化的多头输出,导出 ONNX 后会自动压平成一个二维矩阵。如果不知道输出列的含义,就会解析错。

解决:用 onnxruntime 打印输出 shape 和样例值。单类模型的输出 shape 是[1, 25200, 6],25200 来自 80×80、40×40、20×20 三张特征图各自锚框数的总和,即 6400+1600+400=8400,每个位置 3 个 anchor 再乘 3,得到 25200。最后一维 6 对应[x_center, y_center, width, height, objectness, class_score],多类时最后一维是5 + nc。验证脚本长这样:

import onnxruntime as ort import numpy as np sess = ort.InferenceSession("best.onnx", providers=["CPUExecutionProvider"]) dummy = np.random.rand(1, 3, 640, 640).astype(np.float32) out = sess.run(None, {sess.get_inputs()[0].name: dummy}) preds = out[0] # shape: [1, 25200, 6] print(preds.shape) # 解析时把前四列乘上输入尺寸还原成像素坐标,再做一次轻量NMS

网络输出 25200 个框,直接画到画面上会有大量冗余,所以实际推理框架接入时,我会在解析层再跑一个轻量 NMS,去掉接近重复的框。这一步和后处理的--iou-thres不是一回事,前者作用在模型原始输出上,后者作用在最终候选框集合上,两层都需要。

6. 验证与进阶技巧:单图回归、帧间去抖与告警去重

6.1 最小验证:先用一张图证明模型不是黑匣子

部署前用一张训练过的图加一张完全陌生的图各跑一次:

python detect.py --weights best.pt --source test_rat.jpg --conf-thres 0.35

输出在runs/detect/exp/。确认两点:一是框的位置大致贴合老鼠轮廓,二是置信度数值与目标完整度匹配,完整目标应高于部分遮挡目标。如果框明显偏了,先查输入分辨率问题,再查标签坐标是否转换错误。不要一上来就直接接 RTSP 流,先单图验证再视频验证,能省很多排查时间。

6.2 帧间去抖:同一只老鼠别触发几十条告警

监控视频里同一只老鼠在画面中停留 3 秒,按 25fps 推理逐帧处理会产生 75 条告警日志。我在后处理环节用一个滑动时间窗口做去重:相邻帧中 IoU 大于阈值的检测框视为同一目标,只上报一次,并记录首次出现时间。可以去部署的时候参考这段:

class Track: def __init__(self, bbox, ts): self.bbox = bbox self.last_ts = ts self.alarm_sent = False def check_alarm(tracks, dets, thr=0.3, expire=2.0): for det in dets: matched = False for t in tracks: if iou(t.bbox, det) > thr: t.bbox = det t.last_ts = time.time() if not t.alarm_sent: send_alarm() t.alarm_sent = True matched = True break if not matched: tracks.append(Track(det, time.time())) tracks[:] = [t for t in tracks if time.time() - t.last_ts < expire]

这段代码的核心是按 IoU 匹配轨迹,expire秒内没有匹配就删除轨迹。注意真实场景里老鼠经常会被橱柜遮挡再出现,中断超过 2 秒就会重新触发告警,可以把expire放宽到 5-8 秒,让模型在遮挡场景下仍有二次确认能力。thr=0.3表示两帧检测框的 IoU 超过 0.3 就视为同一目标,这个值在老鼠快速移动时不要设得太高,否则两帧之间位移大导致匹配不上。

6.3 告警阈值取值:统计置信度分布再定,不拍脑袋

我自己的习惯是,拿到新场景先不设固定阈值,跑 12 小时推理,统计模型输出置信度的分布,再选一个不让背景误报进入的稳定区间作为阈值。比如统计发现 0.4 以上的检测绝大多数是真老鼠,0.25 到 0.4 之间是模糊帧和阴影误报,那就以 0.4 为告警线,0.25 到 0.4 记为“疑似”只进日志不推送。这个统计方法比任何拍脑袋阈值都可靠。

明厨亮灶里漏检一次老鼠的代价远高于误报一次,告警策略要偏向召回而不是精确率。这是我最想分享的习惯:模型能不能上线,不能只看训练集的 mAP,要在真实视频流上跑满一个昼夜再下结论,看不同时段的光线变化、厨房蒸汽影响、人员走动干扰,把这些问题都记录下来,再回去微调阈值和增强参数。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询