简介基于YOLOv5与Coco预训练的person类权重进行目标检测实现室内外不拥堵场景的实时人群计数与阈值报警主要面向安防行业开发者、计算机视觉学习者及需要快速部署人数统计功能的项目团队。压缩包共116个文件包含可直接调用的Python源码与pyc字节码、PyTorch模型权重pt、yaml参数配置、Dockerfile与shell环境脚本、ipynb交互式教程、docx图文安装说明、mp4演示视频以及jpg/png测试素材从环境搭建到推理计数均有对应文件支撑。包体大小约489.69MB并附有在限时免费GPU云平台上运行的完整教程无需自建高性能服务器可帮助读者低成本完成环境准备、模型加载和实时监控报警。目前已有2885人学习下载适合希望边看教程边实践、尽快掌握YOLOv5实际部署并落地安防人数统计方案的读者。压缩包目录结构清晰按源码、配置、文档与素材分类便于按需查阅和二次开发。1. 人群计数不靠“数人头”YOLOv5 目标检测与阀值报警方案到底解决什么一个监控画面里出现多少人算“人多”数人头看起来很直接实际做起来却完全是另一回事遮挡、俯视、逆光、排队密集、走动穿插任何一项都能让纯视觉计数方案当场翻车。把 yolov5 目标检测和阀值报警塞进同一个 zip 包里本质上是换一条更稳的路——先用检测模型把人“框”出来再统计框的数量超过设定阈值就触发报警。这个方案适合三类人做园区、门店、工厂安全监控的集成商想在树莓派5上部署自己训练的yolov5模型、跑轻量识别的硬件玩家以及刚接触 yolov5 训练自己的数据集、需要一个完整落地样例的开发者。它的核心价值不是把人头数得精确到个位数而是用可配置的报警逻辑把“人群密度异常”这件事变成一条稳定、可复现的告警信号。2. 把 YOLOv5 检测结果变成人群计数计数口径、最小脚本与后处理边界人群计数看起来就是“数框”但“数哪种框”直接决定报警的可靠性。这一章先说口径怎么选再给一个能直接跑通的最小脚本最后把 yolov5 后处理里最容易踩的边界讲透。2.1 三种计数口径怎么选单帧、区域、跨帧跟踪常见的人群计数口径有三种我第一次做的时候直接选了单帧全图计数结果报警一塌糊涂画面远端的人只有十几像素高检测框跳来跳去计数结果每秒都在抖。后来换成区域计数才把误报压下来。计数口径实时性准确度算力开销适用场景单帧检测计数最高受遮挡影响大低实时人数展示、瞬时统计区域检测计数高中上低报警场景、区域密度监控检测跟踪计数中高ID 稳定时高精确人流、进出方向、停留时间标题里的“人群计数及阀值报警”重心明显在“报警”而不是“精确人流”。安防关心的是电梯口、闸机、广场某个区域内的人是不是超过安全密度而不是今天总共经过多少人。所以我一般建议默认选区域检测计数先画一个 ROI 区域只统计检测框中心落在区域内的 person。跨帧跟踪只有在需要统计“人群是否滞留”“某个 ID 待了多久”时才引入代价是需要额外跑 ByteTrack 或 DeepSORTID 跳变本身又会引入新的误差报警场景慎用。区域计数的另一个好处是天然规避“画面里出现但跟本区域无关的人”。比如一楼大厅的玻璃幕墙外走过路人如果不加 ROI这些人会全被计入加了区域掩码报警逻辑只看框的中心点是否落在监控区域内。这个设计不是优化项而是必要项。2.2 用本地模型跑通最小计数脚本torch.hub 与权重加载的差别先给一个能直接跑的最小脚本。它做的事情很简单加载 yolov5 模型逐帧推理过滤出 person 类只统计 ROI 里的框。import cv2 import torch # 方式一zip 里带 yolov5 源码时用本地加载不依赖联网 # model torch.hub.load(本地路径/yolov5, custom, path权重/best.pt, sourcelocal) # 方式二先跑通逻辑时用官方预训练权重 model torch.hub.load(ultralytics/yolov5, yolov5s, pretrainedTrue) model.conf 0.35 # 置信度阈值低于这个值的框直接丢弃 model.iou 0.45 # NMS 的 IoU 阈值控制重叠框的保留 model.max_det 200 # 单帧最多保留 200 个框防止密集场景后处理耗时过长 # ROI 区域四边形的四个顶点坐标为原图像素坐标 roi np.array([[100, 200], [600, 200], [600, 700], [100, 700]]) cap cv2.VideoCapture(0) # 替换为 RTSP 地址或本地视频文件 while True: ret, frame cap.read() if not ret: break results model(frame) # 推理 NMS 后处理已经内置 boxes results.xyxy[0].cpu().numpy() # 每行: x1, y1, x2, y2, conf, cls person_count 0 for box in boxes: x1, y1, x2, y2, conf, cls box if int(cls) ! 0: # COCO 80 类里 person 的 id 是 0 continue # 取检测框底边中点作为落点比用框中心更贴近“人站在哪里” foot ((x1 x2) / 2, y2) if cv2.pointPolygonTest(roi, foot, False) 0: person_count 1 print(f当前帧人数{person_count})代码里的两个细节值得展开。第一torch.hub.load首次运行会联网拉取源码和权重如果你拿到的 zip 包里已经带了 yolov5 源码和 best.pt建议用注释里的sourcelocal方式加载否则每次初始化都要走网络在断网环境直接起不来。第二我用的落点是检测框底边中点而不是框中心。人群场景里人通常是站立或行走的框底边中点比中心点更接近“脚踩的位置”配合 ROI 判断区域归属时更准确。model.conf、model.iou、model.max_det这三个参数是 yolov5 对外暴露的推理闸门。conf 调高误检减少但漏检增加iou 调高重叠框保留更多密集人群里计数更接近真实但也会把同一个人的重复框算进来。初次调试我建议 conf 从 0.35 起步iou 固定在 0.45先看计数曲线再决定往哪边调。2.3 YOLOv5 后处理里最容易被忽略的四个闸门很多人把 yolov5 当成一个“输入图片、输出框”的黑匣子但报警逻辑对后处理参数极其敏感这里值得单独讲。yolov5 的后处理核心是non_max_suppression它在推理之后自动执行你拿到的results.xyxy已经是 NMS 之后的结果。关于后处理我踩过四个坑第一类别过滤必须放在 NMS 之后不要自己重写 NMS。YOLOv5 的 NMS 是按类别独立做的这意味着 person 类别的框不会因为和 bus 类别的框重叠而被压掉。你只要对结果做cls过滤即可。COCO 里 person 的类别 id 是 0不是 15。我见过好几份笔记把 person 写成 15那是把 COCO 的排序记混了。正确做法是打印一次model.names确认。第二置信度阈值是后处理的一部分不是模型训练参数。detect.py 里有--conf-thresmodel.conf只是在 Python API 里改同一个值。人群计数场景里置信度阈值太低会把柱子、广告牌上的人影都算进来太高又会漏掉被遮挡大半的人。我通常先统计一段正常时间段的人数均值再反推合适的 conf而不是凭感觉定。第三max_det 会截断计数结果。默认 300 在大多数场景够用但如果监控区域是个千人规模的候车广场NMS 之后保留的框数量超过 max_det后面的人就被静默丢弃了计数结果会离真实值越来越远。报警场景不需要精确到千人所以 max_det 设 200 通常足够真到了超大规模场景应该做分区域 ROI而不是寄希望于单模型数清所有人。第四坐标映射关系。results.xyxy返回的坐标已经映射回原图尺寸模型内部的 letterbox 填充不会影响你做 ROI 判断。但如果你自己写预处理把图片 resize 后再塞进模型那就要手动把框坐标缩放回原图否则 ROI 判断的位置全是偏的。这也是我在整理 yolov5 基础笔记时最常提醒自己的一条后处理里的“原图坐标”和“输入坐标”不是一回事。3. 阀值报警参数怎么设三层阈值、自适应基线与防抖状态机“超过阈值报警”听起来只有一句话实际工程里要调的是三层阈值每一层管的东西不一样漏掉任何一层报警系统都会变成要么瞎响、要么聋了的状态。这一章讲三层阈值的意义、自适应阈值的实现以及一个能直接抄走的防抖状态机。3.1 三层阈值各自管什么置信度、计数、持续时间第一层是置信度阈值管的是“这一个框是不是人”。它决定检测质量上一章已经讲了不再重复。第二层是计数阈值管的是“这一帧里有多少人算超限”。第三层是持续时间阈值管的是“超限状态持续多久才报警”。层级参数典型值作用检测层conf_thres0.25 ~ 0.5过滤低置信度检测框计数层count_thres按区域密度标定每帧人数超过多少触发候选报警时间层duration_thres3 ~ 10 秒连续超限多久才真正报警最常见的设计失误是只设计数阈值忽略持续时间。人群在画面里是有波动的一个人快速跑过 ROI 边缘可能带进来一个误检框两个人并肩走到电梯口又分开计数会瞬间超过阈值又立刻回落。如果只按单帧计数报警一天能响几百次。我一般把持续时间设成 5 秒即连续 5 帧以上都超过计数阈值才触发报警。计数阈值本身怎么标定一个可复现的做法是找一段该区域正常运营的视频按每秒一帧采样统计 30 分钟内的人数分布。取“正常时段最大值”作为基线报警阈值设在基线的 1.2 到 1.5 倍。比如早高峰电梯厅平时最多 22 人那计数阈值设在 28 到 33 之间比较合理。阈值设得太低日常波动就报警设得太高真正需要预警的时刻已经来不及疏散。3.2 自适应阈值滑动窗口基线与 3 倍标准差报警固定阈值最大的问题是“基线会漂移”。同一个商场中庭工作日上午人数基线和周末下午完全不同同一个地铁站早高峰和午休时段能差三倍。用一套固定计数阈值全年跑必然出现“晚上没人误报、白天真超限却不报”的怪现象。自适应阈值的基本思路是维护一个滑动窗口窗口里放最近 N 秒的人数统计值然后以“均值 K 倍标准差”作为动态报警线。from collections import deque import numpy as np class AdaptiveCounter: def __init__(self, window30, min_count5, z_thresh3.0): self.window deque(maxlenwindow) # 滑动窗口存放每秒人数 self.min_count min_count # 绝对下限防止样本太少时误报 self.z_thresh z_thresh # 标准差倍数越大越难触发 def update(self, count): self.window.append(count) if len(self.window) 20: # 样本量不足时退化为固定阈值 return count self.min_count arr np.array(self.window) mean arr.mean() std arr.std() # 动态阀值阈值 基线均值 3 倍标准差 dynamic_thresh mean self.z_thresh * max(std, 1.0) dynamic_thresh max(dynamic_thresh, self.min_count) return count dynamic_thresh这个实现里有两个值得注意的参数。第一max(std, 1.0)是防呆如果人数一直很稳定比如始终在 20 人左右波动std 会接近 0此时任何一次轻微波动都会触发“3 倍标准差”报警。把标准差下限设为 1.0动态阈值就有了一个最低的浮动范围不会在人数稳定时发疯。第二窗口长度 30 意味着它记住的是最近 30 秒的基线周末下午的人数突增要持续约 30 秒才会被当作“新常态”这正好和持续时间阈值形成互补。提示自适应阈值适合“基线慢变”的场景比如商场客流随节假日变化。但如果你的场景是“从 0 突然涨到 100”滑动窗口反应不够快仍然要靠固定阈值兜底。两种方式不是二选一而是取较大者作为最终报警判定。3.3 报警状态机滞回比较与恢复阈值自适应阈值解决的是“什么时候该报”但报警系统还有一个同样重要的问题什么时候该恢复。如果人数回落到报警线以下就立即恢复在阈值边缘反复横跳的场景里报警状态会一秒钟切换三四次下游的短信、推送、大屏告警全被打爆。正确的做法是滞回比较报警触发和报警恢复使用两个不同的阈值触发阈值高恢复阈值低。比如 30 人触发报警等人数降到 20 以下才恢复。中间这段 20 到 30 的区间叫滞回带它把“偶发波动”和“真正缓解”区分开。class AlarmStateMachine: def __init__(self, count_thresh30, duration5, recover_gap10): self.count_thresh count_thresh # 触发阈值 self.duration duration # 连续超限多少帧才触发 self.recover_gap recover_gap # 恢复阈值 触发阈值 - recover_gap self.over_count 0 # 已经连续超限的帧数 self.alarm_active False # 当前是否处于报警状态 def update(self, count): # 超过触发阈值累计 if count self.count_thresh: self.over_count 1 else: # 落在滞回带内不立即清零而是衰减 if count self.count_thresh - self.recover_gap: self.over_count 0 else: self.over_count max(self.over_count - 1, 0) # 达到持续时间且未报警触发 if self.over_count self.duration and not self.alarm_active: self.alarm_active True return ALARM # 报警状态下人数降到恢复阈值以下才恢复 if self.alarm_active and count self.count_thresh - self.recover_gap: self.alarm_active False return RECOVER return None这里的关键是“落在滞回带内时超限计数只减一不清零”。如果一个人在 28 人和 32 人之间反复横跳over_count会被不断拉回触发线附近但不会彻底归零只有真正降到 20 以下状态机才愿意回到正常态。这个设计比单纯加持续时间阈值更抗抖因为它同时考虑了“进入报警的持续时间”和“退出报警的深度”。我在实际项目里通常把状态机放在一个独立线程里跑检测线程只负责把每帧的人数推给它状态机输出ALARM或RECOVER事件。这样报警逻辑可以脱离视频帧率独立测试——把录好的人数列回放一遍就能验证状态机参数是否合理不用每次调参都去改检测代码。4. 从 conda 环境到树莓派5部署训练自己的数据集并跑通报警链路前两章讲的是怎么用现成模型把计数和报警逻辑跑通。但真实项目里监控场景大概率和你拿到的预训练权重不完全匹配比如你的摄像头是高空俯视人只有很小的头肩特征比如场景是夜间地铁口补光条件很差。这时候就需要 yolov5 训练自己的数据集。这一章从环境配置开始一直讲到树莓派5上部署自己训练的yolov5模型。4.1 conda 环境配置 YOLOv5一条命令创建、依赖安装与推理验证yolov5 环境配置在社区里口碑不错最大的原因是官方 requirements 写得很清楚不折腾。但“不折腾”的前提是 Python 和 PyTorch 版本别乱配。我踩过的坑是拿 Python 3.8 配新版 torch编译期报了一堆错。现在我的固定做法是 conda 创建独立环境指定 Python 3.10。# 创建独立虚拟环境避免污染系统 Python conda create -n yolov5 python3.10 -y conda activate yolov5 # 克隆 yolov5 源码如果 zip 已自带源码跳过这一步 git clone https://github.com/ultralytics/yolov5.git cd yolov5 # 安装依赖torch 版本会自动和本机 CUDA 匹配 pip install -r requirements.txt # 验证环境跑一张官方示例图 python detect.py --weights yolov5s.pt --source data/images/bus.jpg依赖安装这一步最容易出问题的是 PyTorch。如果你的机器有 NVIDIA 显卡先执行nvidia-smi看 CUDA 版本再决定装哪个 torch 版本。没有显卡的话requirements.txt 会默认装 CPU 版 PyTorch这也能跑只是推理慢。如果下载速度不理想可以先把 pip 源换成国内 PyPI 镜像再执行安装命令。detect.py跑通一张图不代表环境没问题。我建议多跑一步用 Python API 加载同一个权重做一次推理确认torch.hub这条路也通。因为后面写计数脚本用的是 Python API不是命令行。这一步能提前暴露类似“torch.hub 拉不下来源码”“权重路径不对”这类问题等写到业务代码再调就晚了。4.2 训练自己的数据集标注格式、data.yaml 与超参数调整准备自己的数据集核心是三件事标注、组织目录、写 data.yaml。标注格式用 YOLO 的 txt每行是class_id x_center y_center width height坐标全部是相对原图的 0 到 1 浮点数。标注工具用 LabelImg 或者 x-anylabeling 都行导出格式选 YOLO。人群计数场景里类别一般只有 person 一个所以class_id始终是 0。数据集目录建议直接采用 yolov5 约定的 layoutdatasets/crowd/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/然后写一个 data.yaml内容非常短但路径写错是整个训练流程里最常见的翻车点。# 训练集和验证集的图片路径相对路径是相对 yolov5 源码目录 train: ./datasets/crowd/images/train val: ./datasets/crowd/images/val # 类别数这里只有 person nc: 1 names: [person]训练命令python train.py \ --data data.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --hyp hyp.scratch-low.yaml这里面--hyp hyp.scratch-low.yaml是我比较推荐的人群计数配置。yolov5 超参数里默认的hyp.scratch-high.yaml数据增强很强但对密集人群场景容易把小目标增强糊掉low 的增强幅度更保守训练更稳定。如果你后续发现模型在夜间、逆光下掉点严重优先改hyp.scratch-low.yaml里的hsv_h、hsv_s、degrees这些增强参数而不是急着换大模型。--img 640是训练输入分辨率。如果你的监控画面里人很小比如俯视镜头下一个人只占 30 像素建议把 img 提到 960。代价是显存占用变大训练时间变长但检测小目标的收益非常明显。--batch 16在大多数 8G 显存的卡上比较稳如果显存不够先降 batch不要先降分辨率。训练结束看runs/train/exp*/weights/best.pt和last.pt。best.pt 是验证集上指标最好的权重部署时用这个。val 集最好单独从真实监控视频里抽帧标注不要用训练集的同源视频否则验证分数虚高部署到现场会被现实狠狠打脸。4.3 树莓派5 上部署自己训练的 YOLOv5 模型NCNN 转换与跳帧树莓派5 的 CPU 是四核 Cortex-A76比树莓派4 强不少但和中端显卡仍然没法比。直接跑 PyTorch 的 yolov5s推理一帧大约要 2 到 4 秒做实时报警完全不可用。我试过的可行方案是训练完导出 ONNX再转成 NCNN 格式在树莓派上用 NCNN 推理。# 第一步导出 ONNX python export.py --weights best.pt --include onnx --img 640 # 第二步用 NCNN 工具把 ONNX 转成 param bin # 常见做法是使用 ncnn 的 onnx2ncnn 命令行工具 onnx2ncnn best.onnx best_ncnn.param best_ncnn.bin转换完后在 Python 里加载 NCNN 模型做推理import ncnn import cv2 import numpy as np net ncnn.Net() net.load_param(best_ncnn.param) net.load_model(best_ncnn.bin) # 构造推理输入YOLOv5 的 NCNN 版通常需要自己处理 letterbox image cv2.imread(frame.jpg) h, w image.shape[:2] resized cv2.resize(image, (640, 640)) in_mat ncnn.Mat.from_pixels_resize(resized.transpose(2, 0, 1), 0, 640, 640)树莓派5 上用 NCNN 跑 yolov5s实测大约能到 8 到 12 帧每秒前提是输入分辨率 640 且不做太多预处理。这个帧率做人群计数报警非常够用因为人群密度的变化是秒级甚至分钟级的不需要追逐每一帧。我一般会做一个跳帧策略把推理线程固定在每秒 2 帧报警状态机每 0.5 秒更新一次。这样 CPU 占用能控制在 60% 以下留给视频解码和系统其他服务。还有一个更激进的优化换用 yolov5n 或 yolov5n6。这个模型比 s 小近一半在树莓派5 上能跑到每秒 15 帧以上精度损失在“检测可不可以报警”这个粒度上完全可接受。毕竟你要的是“这块区域超过 30 人了”不是“每一个人都要框得完美”。注意树莓派上不要用 torch.hub 加载模型启动时间和内存开销都扛不住。正确姿势是训练时在电脑上导出 ONNX/NCNN再把模型文件拷到树莓派。部署包里带上 NCNN 的 Python 绑定即可运行时不需要安装 PyTorch。5. 避坑指南人群计数的五个翻车现场与排查方法这一章全部来自实际交付里的血泪经验。每一条我都按“现象 → 原因 → 解决”写清楚你照着条目排查能省掉大量试错时间。5.1 误报广告牌上的人脸被当成真人现象商场中庭的巨幅促销海报上印着模特人像检测模型稳定地把海报上的模特框出来计入人数。海报一换计数立刻跳高报警被触发。原因模型训练时见过的“人”绝大多数是真实照片它学的是人的纹理和结构不知道这是一个平面印刷品。只要画面上的人像足够大、足够清晰模型就会给出很高的置信度。解决最粗暴但有效的办法是给 ROI 画掩码把海报所在的墙体区域直接排除。如果海报区域不在监控重点范围内这是零成本方案。如果海报就在人群后方没法排除就加一道“最大框面积”过滤真实人体在给定监控距离下的检测框面积有上限超过这个面积的框大概率是广告牌或墙面装饰。还有一个进阶做法是用边缘响应做二次过滤但工程上不划算不推荐为这个单独上模型。5.2 漏检密集遮挡让计数直接砍半现象早高峰写字楼闸机口人群挤成一片一个人只露出半个肩膀。检测框大量重叠NMS 后只剩一半的框计数结果严重偏低。最极端的例子画面里实际有 30 人系统只数出 14 人。原因NMS 的作用是抑制重叠框当两个人靠得极近两个框的 IoU 超过阈值模型会把置信度较低的那个框丢掉。密集人群恰恰是重叠高发场景NMS 一压缩计数就失真。解决优先级从高到低有三条路。一是提高输入分辨率到 960小目标和密集重叠目标的分辨能力会明显改善代价是推理速度下降但报警场景每秒两帧足够。二是把 NMS 的 iou 阈值从 0.45 调到 0.4让重叠框更容易被保留下来。三是训练数据里加入大量密集人群的 mosaic 增强让模型见过“人贴人”的形态。注意 iou 不要低于 0.3否则同一个人的重复框也会被放进来计数反而虚高。5.3 报警抖动人数在阈值边缘反复横跳现象计数阈值设 30画面里人数在 28 到 32 之间波动。报警触发后 3 秒恢复然后再次触发短信和推送每几分钟就来一条运维同事直接把这个告警规则拉黑了。原因典型阈值边缘抖动没有滞回区间。触发和恢复用的是同一个 30 人阈值人数一超过就报一低于就恢复完全跟着单帧波动走。解决用第 3.3 章的状态机触发阈值设 30恢复阈值设 20持续时间设 5 秒。这样人数必须在 30 以上停留 5 秒才报警也必须降到 20 以下才恢复。中间的 10 人滞回带提供了足够的抗干扰空间。这个改动不改任何检测逻辑只改报警判定效果立竿见影。5.4 逆光与夜间检测率断崖式下跌现象白天正常到了傍晚太阳直射摄像头或者夜间灯光逆光时计数从每小时 300 人掉到 30 人几乎不报警了。原因光照是目标检测最大的环境变量。逆光时人脸的对比度极低模型输入的画面里人形和背景融为一体夜间普通摄像头没有补光远处的人只有几个亮点。解决分三类处理。第一类训练时在 hyp 超参数里调亮度和色彩增强让模型见过更多低光图像这在夜间场景的收益最大。第二类部署前做 CLAHE 对比度增强cv2.createCLAHE(clipLimit2.0, tileGridSize(8,8))然后转 LAB 通道处理能有效拉回逆光下的人形轮廓。第三类物理层面解决换带红外的摄像头或者增加补光。做报警项目时我建议先确认现场有没有红外条件再决定要不要在算法上死磕。5.5 树莓派部署卡顿CPU 占用 100% 与线程解耦现象树莓派5 上把上一章的计数脚本直接跑起来视频流越来越卡延迟从 2 秒涨到 10 秒最后画面直接冻住。用htop一看Python 进程吃满了四个核。原因采集、推理、报警全挤在同一个循环里。cap.read()在读下一帧时推理线程正在跑模型模型跑完视频帧已经积压了好几帧。读视频的延迟被推理阻塞是典型的“采集和计算没解耦”。解决把采集、推理、报警拆成三个线程用队列或最新帧覆盖的方式传递数据。推理线程永远只处理最新的一帧不处理积压帧如果上一帧还没算完就丢弃当前帧保证推理节奏固定。报警线程只消费人数结果不碰视频帧。树莓派上还要注意把推理线程的 CPU 亲和性绑定到两个核上给视频解码留出余量这样系统能稳定跑 24 小时不卡死。6. 进阶技巧报警可靠性的验证方法与业务对接调试完阈值、部署完模型事情还没结束。报警系统的可靠性必须用真实数据验证不能靠“我看了一会儿觉得挺准”这种主观感觉。我的验证方法是录一段 30 分钟的现场视频逐秒或者按 10 秒间隔抽帧人工标出“这一帧是否应该处于报警状态”然后回放这段视频让系统跑一遍对比系统报警和人工标注。验证时盯两个指标误报率和漏报率。误报率高说明阈值偏低或 ROI 边界需要收紧漏报率高说明检测漏检太严重或持续时间阈值偏长。不要单独优化一个指标——把误报压到零的阈值往往会让真正该报的也报不出来。我会把报警判定结果和人数曲线画在同一张图上一眼就能看出阈值位置是不是合理。报警触发的下一步是把告警送到业务系统。一个完整的告警推送至少包含三样东西报警帧截图、当前计数、发生时间。截图是事后回溯的铁证计数和时间是判断严重程度的依据。import time import requests def push_alarm(image, count, api_urlhttp://your-server/api/alarm): # 保存报警现场截图 filename falarm_{time.strftime(%Y%m%d_%H%M%S)}_{count}.jpg cv2.imwrite(filename, image) # 推送结构化告警到业务系统 payload { count: count, time: time.time(), image: filename, level: warning if count 50 else critical } resp requests.post(api_url, jsonpayload, timeout5) return resp.status_code 200推送逻辑注意两点第一必须放在报警状态机的ALARM事件里触发不要在每一帧都推第二要加去重同一个报警事件只推一次直到RECOVER事件出现后才允许下一次ALARM再次推送。这样既保证了“不漏报”也保证了“不轰炸”。这套方案做到最后我对它的定位已经不只是“人群计数报警”而是一套可以巡检、可以回放、可以调参的监控组件。每次交付前我都会拿现场半小时的视频回放跑一遍把误报和漏报逐条记录再回头调阈值。这个习惯救了我很多次报警方案最怕的就是上线后没人信它。希望帮到你。本文还有配套的精品资源点击获取