ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

深度视觉实时异常行为检测:从目标检测到骨架时序建模完整链路

深度视觉实时异常行为检测:从目标检测到骨架时序建模完整链路 简介一份基于深度视觉的实时异常行为检测项目压缩包面向安防监控、智能交通和工业自动化领域的开发者与研究人员。包内整合了异常检测、机器学习与深度学习核心知识涵盖数据预处理、模型训练、算法实现到实时系统集成针对盗窃、事故、非法入侵等异常事件给出了可运行的参考方案。资源共15个文件以10个Python脚本为主线涉及模型定义、跟踪可视化、窗口展示等功能模块另含4张效果示意图和1份README说明文档整体大小仅1.05MB目录划分清晰便于理解工程结构并按需复用。项目方案涵盖无监督/半监督学习、CNN/RNN以及YOLO、SSD与LSTM等组件配合model目录下的模型文件可直接服务于实时视频流异常识别与演示。目前已有217人学习下载适合希望从零搭建视频异常行为检测系统、需要完整代码组织与可视化参考的中高级开发者和算法工程师。1. 深度视觉实时异常行为检测从 zip 包到完整行为分析链路这个 zip 包解压之后不是那种只能跑出几个窗口的玩具 demo而是一条完整的“检测 → 骨架 → 时序评分 → 告警”链路MyWindow.py 负责界面与视频流cv_viewer 负责画框和骨架model 目录里是 nets.py、attentions.py、process.py、modules.py 这些真正的模型与流水线代码README.md 里写了依赖安装和启动步骤。基于深度视觉做实时异常行为检测难点从来不是把某个网络跑通而是在 20~30 帧的实时约束下仍能保留行为上下文、控制误报。这套代码适合正在做视频行为分析想从单帧检测往时间序列模型迈一步的工程师拆开看。2. 从单帧检测到骨架序列nets.py、utils.py 的行为建模链路基于深度学习的视频分析里单帧目标检测只能告诉你“这里有个人”而异常行为是跨帧定义的摔倒、斗殴、伸手盗窃、在库房门口长时间徘徊全都依赖姿态随时间的演化。所以这个包的核心不在某个检测网络而在“怎么把连续帧组织成可学的时间序列”。model 目录里 nets.py、utils.py、attentions.py、blocks.py 的分工恰好对应这条建模链路。2.1 为什么先用骨架表示而不是直接学原始帧常见做法是先做目标检测YOLO 这类一阶段检测器再用姿态估计网络提取每个人的 COCO 17 关键点行为模型只吃骨架序列。原因有三个第一原始帧对衣服颜色、光照、相机视角极其敏感直接学原始像素容易把“场景”当“行为”换一个监控点位误报率立刻飙升第二骨架把每帧压缩成 17×3 的向量坐标加置信度时序模型的输入规模小一个数量级实时推理才扛得住第三如果部署环境对隐私有要求只上传骨架流、不传原图也是个可行方案。image 目录下的 skeleton.png 就是用来肉眼验收骨架提取质量的这个文件如果连线都画不对后面的行为模型基本不用看。2.2 utils.py 的关键点归一化与滑动窗口构造行为建模的第一步是归一化。直接拿像素坐标喂模型人物离镜头近时坐标值大、离得远时小网络会学到“尺寸”而不是“动作”。所以一般把关键点相对于检测框做归一化# utils.py 中的关键点归一化结构示意 def normalize_keypoints(kps, box): kps: (17, 3) 的 COCO 关键点最后一维是置信度 box: [x1, y1, x2, y2] 检测框 w max(box[2] - box[0], 1) h max(box[3] - box[1], 1) out kps.copy() out[:, 0] (kps[:, 0] - box[0]) / w out[:, 1] (kps[:, 1] - box[1]) / h return out这里除以检测框宽高而不是图像宽高目的是把人物的尺度变化解耦掉同一个动作不管人站在 5 米外还是 2 米外归一化后的坐标分布是接近的。置信度通道保留下来训练时可以用它做 mask让低置信度关键点不参与损失计算避免把姿态估计的抖动当成行为特征学进去。归一化之后要把连续帧攒成一个时间窗口。常规实现是维护一个 per-track 的缓冲队列满了之后按步长抽帧def build_clip(buffer, window_size32, stride4): 从滑动窗口里按 stride 抽帧返回 (1, T, 17*3) 的序列 T (window_size stride - 1) // stride if len(buffer) window_size: return None # 预热期序列不足直接跳过 idx list(range(len(buffer) - window_size, len(buffer), stride)) clip np.stack([buffer[i] for i in idx]) return clip[np.newaxis, :T, :]window_size 决定模型能感知的动作时长30fps 下 32 帧约等于 1 秒摔倒这种动作通常在 0.5~1 秒内完成窗口太短只能看到半个动作太长则把多个无关动作混在一起。stride 控制送入模型的序列长度32 帧按 4 抽后变成 8 帧计算量小很多。注意函数在缓冲不满时返回 None所以系统启动后的前 0.5~1 秒不会有任何评分输出这是正常预热不是 bug。表 2-1 序列构造关键参数参数含义建议区间调参说明window_size滑动窗口包含的原始帧数16~32决定行为上下文长度快速动作取小值缓慢徘徊类行为取大值stride窗口内抽帧步长2~4越大序列越短、计算越快但会丢失快速姿态变化pose_conf关键点置信度下限0.3~0.5低于阈值的点置零降低姿态估计抖动对时序模型的影响2.3 attentions.py 与 blocks.py给序列加上时间上下文骨架序列进入模型后一般先经过一个空间编码器把关键点坐标映射到隐层维度再进入时间模块。项目里既放了 blocks.py 又放了 attentions.py说明时间建模同时保留了卷积块和注意力两条路线。LSTM 的思路是按时间步逐步更新隐状态参数少、部署简单但长序列的梯度传播和并行性都不理想自注意力的做法是让任意两帧直接交互长距离依赖建模更直接# attentions.py 中的时序自注意力块结构示意 class TemporalAttention(nn.Module): def __init__(self, dim, heads4): super().__init__() self.attn nn.MultiheadAttention(dim, heads, batch_firstTrue) self.norm nn.LayerNorm(dim) def forward(self, x): # x: (B, T, D)T 是抽帧后的序列长度 a, _ self.attn(x, x, x) return self.norm(x a)这里的多头注意力让自己和自己做 attention让每一帧都能根据前后帧调整自己的表示比如“倒地”这个动作单独看某一帧只是横躺加上前后 0.5 秒的快速下坠上下文才能被判定为异常。残差连接加 LayerNorm 是为了让注意力块可以堆叠多层而梯度不退化。我的建议是先用注意力这条线跑通再对比 LSTM 版本的精度和延迟不要一开始就并行维护两个版本。nets.py 负责把 blocks 和 attentions 按“空间编码 → 时间建模 → 全局池化 → 打分头”的顺序拼起来打出来的分数经过 sigmoid 变成 0~1 的异常评分。3. process.py 与 modules.py把模型编排成实时检测流水线模型结构只解决“单条序列怎么打分”真正让系统实时跑起来的是 process.py 和 modules.py。这两个文件经常被当成“胶水代码”跳过但异常检测系统的绝大多数问题——ID 频繁跳变、告警抖动、预热期误报——都出在这一层。3.1 modules.py 的职责拆分以及为什么要拆modules.py 一般把三件事拆开目标检测器、姿态估计器、行为评分器。这样拆不是形式主义是为了能单独替换和单独调参。实际部署时检测器可能从 YOLOv5 换成 NanoDet 以换取速度姿态估计可能在低端设备上换轻量版本而行为评分模型完全不动反过来换场景时只想调阈值不想碰网络。模块之间只通过接口对接输入是一帧图像输出是 track_id、骨架和异常评分UI 层完全不知道背后跑了几个网络。这种解耦也方便离线评测直接跳过检测环节把人工标注好的骨架序列喂给行为模型单独评估时序模型的准确率。3.2 process.py 主循环检测、关联、打分一次走完process.py 里最核心的是每帧的处理函数剥离 UI 之后大致长这样def step(self, frame): boxes self.detector(frame, conf_thres0.45) # 目标检测得到若干检测框 skels [self.pose_extractor(frame, b) for b in boxes] # 每人一套 17 关键点 tracks self.tracker.update(skels, boxes) # 跨帧身份关联解决谁是谁 for track_id, skel in tracks: buf self.buffers[track_id] buf.append(skel) if len(buf) self.window_size: buf.pop(0) clip build_clip(buf) if clip is None: continue # 该 ID 还在预热期 score float(self.model(clip).sigmoid()) if score self.threshold: self.confirm_alert(track_id, score) # 连续帧确认后才告警conf_thres 控制在 0.4~0.5 比较合适摄像头噪点多、有遮挡时调低一些宁可多出几个检测框也别把关键动作漏掉检测框少了骨架提取根本没有输入行为评分再准也救不回来。tracker.update 是关键步骤它用交并比或中心点距离把当前帧的人和上一帧的人做匹配新出现的人自动分配新 ID 并创建独立缓冲队列长时间消失的 ID 会被回收避免内存无限增长。clip 为 None 时跳过推理既省算力又避免了用半截序列做出错误判断。3.3 多人场景的跟踪关联与告警确认多人场景最隐蔽的坑是 ID 交换两个人擦肩而过后跟踪器偶尔会把两人身份互换行为缓冲队列里就混进了两个人的骨架评分立刻变得不可信。排查时盯住 tracker.png 这类可视化看 ID 是否频繁闪变、框是否在两个人之间跳动。过程层的告警也要做确认逻辑def confirm_alert(self, track_id, score): self.alert_count[track_id] self.alert_count.get(track_id, 0) 1 if self.alert_count[track_id] 3: self.emit_alert(track_id, score) self.alert_count[track_id] 0连续 3 帧都超过阈值才告警能把单帧的关键点抖动、检测框瞬移这类随机噪声滤掉。只要分数低于阈值alert_count 就归零避免累积出“延迟告警”。这比单纯调高 threshold 更有效因为 threshold 调高会整体推后告警时间而连续帧确认只惩罚孤立尖峰。表 3-1 process 层可调参数参数作用典型值conf_thres检测框置信度平衡漏检与误检0.4~0.5track_max_age目标消失多少帧后回收 ID15~30alert_consecutive_frames连续超阈值帧数抑制单帧误报3~5abnormal_score_threshold异常评分阈值越界才进入确认流程0.5~0.74. cv_viewer 与 MyWindow.py实时推流、骨架绘制与延迟控制模型和流水线都通了最后的体验卡在可视化这一层。MyWindow.py 和 cv_viewer 看起来只是“把结果画出来”但在实时监控场景里画面刷新是否流畅、告警是否跟手直接决定这个系统能不能给业务人员用。4.1 MyWindow.py 的线程模型与画面刷新常见做法是把 UI 线程和推理线程分开一个采集线程从摄像头取帧推理在线程池里做结果放进队列UI 线程用定时器定时从队列取最新结果画到界面上。如果 MyWindow.py 用的是 Qt定时器一般设 30ms 左右对应约 33fps 的刷新上限# MyWindow.py 的定时刷新逻辑结构示意 def on_timer(self): try: item self.result_queue.get_nowait() except queue.Empty: return # 推理跟不上时宁可等下一轮 frame, tracks item annotated tracking_viewer.draw(frame, tracks) self.label.setPixmap(to_qpixmap(annotated))get_nowait 拿不到结果就直接返回不会阻塞 UI画面顶多暂时停住不会整个窗口无响应。推理线程和 UI 线程之间只通过队列传数据不要共享可变状态。当队列积压超过两三帧时应该主动丢旧帧、只保留最新结果因为实时监控里看的是“现在”不是历史帧重放。4.2 tracking_viewer.py 的骨架绘制与静态图排查骨架绘制是排查问题的第一现场。COCO 17 关键点的连线顺序是固定的画的时候要按骨骼连接关系画而不是把点按编号顺序连起来# tracking_viewer.py 中的骨架连线定义 SKELETON_PAIRS [ (0, 1), (0, 2), (1, 3), (2, 4), # 头顶到肩 (5, 6), (5, 7), (7, 9), (6, 8), (8, 10), # 手臂 (5, 11), (6, 12), (11, 12), # 躯干 (11, 13), (13, 15), (12, 14), (14, 16), # 双腿 ] def draw_skeleton(img, kps, color(0, 255, 255)): for a, b in SKELETON_PAIRS: if kps[a][2] 0.3 and kps[b][2] 0.3: # 置信度过滤 cv2.line(img, tuple(kps[a][:2].astype(int)), tuple(kps[b][:2].astype(int)), color, 2) return img置信度过滤非常关键姿态估计器对遮挡关节会输出低置信度坐标这些坐标经常乱飘不滤掉就会在画面上出现横穿身体的乱线。image 目录下的 test1.png、test2.png 和 skeleton.png 就是干这个用的单张图片喂进去如果画出来的线是乱的先检查关键点索引顺序是否和模型输出约定一致再检查归一化是否写反了坐标轴。静态图排查通过之后再上视频流否则时序模型吃到的全是错误骨架评分没有任何意义。注意p95 延迟比平均 FPS 更值得盯。平均帧率达标但偶尔卡顿一两秒告警就是不可用的优先检查采集线程和推理线程是否在争抢同一把锁。4.3 端到端延迟从哪来怎么压实时异常行为检测的延迟不是模型推理那几十毫秒而是从事件发生到画面告警的完整链路包括采集排队、检测、骨架提取、滑窗缓冲和评分。最容易被忽略的是滑窗缓冲的固定延迟window_size32、30fps 下模型至少要等约 1 秒才能攒够一个窗口。这个延迟是时序模型的固有代价窗口越长告警越可靠但也越迟钝。压延迟的手段按性价比排序见表 4-1。表 4-1 实时性优化手段与代价手段收益代价隔帧推理中间帧复用上一帧骨架推理量减半快速动作有轻微顿挫输入分辨率降到 416 或 320检测与骨架延迟下降 30%~50%小目标漏检变多FP16/ONNX 导出后部署延迟明显下降极端光照下精度可能掉一点检测和骨架提取分两个线程吞吐提升延迟波动减小代码复杂度上升要处理队列积压另外注意对工业落地来说只看平均 FPS 不够要看 p95 延迟。平均帧率达标但偶尔卡顿一两秒告警就是不可用的。p95 延迟超标时优先检查是不是摄像头采集线程和推理线程争抢 GIL 或锁资源而不是急着换更快的模型。5. 用一小段正常视频标定异常阈值并验证告警延迟模型输出的异常评分是一个 0~1 的连续值不标定就直接用必然出现“阈值设高了一个不报、设低了满屏报警”的情况。这套项目里没有给你现成的通用阈值是因为异常行为本质上是一个分布极不均衡的问题正常行为千奇百怪异常行为却没有标准样本。我的做法是拿目标场景的 10~30 分钟正常监控视频用现成权重批量打分把分数的 99 分位作为 threshold 的起点再加上 0.05 的余量。这样保证正常视频中 99% 的时间不会触发告警剩下的 1% 交给连续帧确认机制去压。分数平滑也要放进验证流程。单帧分数天然有抖动一个常见做法是对分数做指数滑动平均def smooth_score(cur, prev0.0, alpha0.3): alpha 越大越信任当前帧越小越平滑但告警越迟钝 return alpha * cur (1 - alpha) * prevalpha 取 0.3 左右比较平衡也可以在确认逻辑里并行用 5 帧中位数滤波效果类似。注意平滑和连续帧确认是两层不同的机制前者滤的是分数本身的随机波动后者滤的是孤立尖峰事件。验证时要区分两个链路用 test1.png、test2.png 这类静态图只能验证检测、骨架绘制和窗口渲染这些单帧链路行为评分必须用连续视频验证。如果没有标注视频可以把静态图和骨架图交替重复拼成一个伪序列先确认模型能输出分数正式验收再录一段真实监控统计三个指标告警延迟事件发生后多少帧出告警、每小时误报次数、漏报比例。延迟超标就缩短 window_size 或 stride误报超标就调 threshold 和确认帧数。这个项目里最值得保留的一个习惯是每次换摄像头视角或换场景都重新录一段正常视频做阈值重标上一场景的数字在新点位几乎一定失效。本文还有配套的精品资源点击获取
返回列表