简介:一套基于 MATLAB 实现的实时人体异常行为检测识别系统,附有图形用户界面,面向视频监控与安全防范场景,也适合计算机视觉方向的项目实战学习。资源压缩包共包含 769 个文件,其中 6 个 .m 文件是主程序与函数源码,1 个 .fig 保存 GUI 界面布局,3 个 .avi 与 1 个 .mp4 提供步行、慢跑、快跑等测试视频,755 个 .jpg 图片可用于人体样本与行为分类训练测试,此外还有说明文档,整体约 18.16MB,便于快速下载与解压使用。目前已有 265 人浏览学习。可运行主界面并调用摄像头或视频文件进行检测,通过源码可以理解实时视频帧读取、人体目标检测、运动特征提取,以及异常行为判别的完整流程,同时还能借鉴 GUI 中视频输入控制、结果展示、参数调节等模块的编写方法。对希望尽快搭建人体异常行为预警原型,或想在 MATLAB 环境下深入掌握计算机视觉与 GUI 开发的读者,都是一份不错的实践资料。
1. 实时人体异常行为检测:为什么说 MATLAB + GUI 是快速验证的优选路径
监控大屏前坐一天,真正需要关注的异常往往只有几秒——跌倒、奔跑、挥手、打架。实时人体异常行为检测要做的,就是从连续的图像流里把它们自动挑出来并给出提示。这套带 GUI 的 MATLAB 完整工程,就是围绕这个场景落地的:读取摄像头或视频文件,实时检测异常行为,在界面里框出目标并触发报警,全程不需要部署推理服务器,一台装有 MATLAB 的普通电脑就能跑通。资源适合三类人:正在做 MATLAB 图像处理大作业的学生、需要快速验证算法的研究员、以及想把人体检测 demo 改造成可演示原型的一线工程师。代码结构清楚,换摄像头、调阈值、加行为类别都留了明确的修改入口。
2. 姿态提取与动作特征:算法选型和技术底座
2.1 背景差分:做人形检测前先把“动起来的东西”找出来
这套系统的前置任务是把人体从背景里分离出来。常见做法是用混合高斯背景模型,也就是 MATLAB 里vision.ForegroundDetector封装好的那个实现。它会对画面里每个像素维护多个高斯分布,用来描述该位置在不同时刻的颜色波动,哪些分布占比高就判定为背景,偏离背景模型的像素就标记为前景,也就是“当前帧有什么东西在动”。
% 创建前景检测器,这里建议保留句柄方便后续调参 fgDetector = vision.ForegroundDetector( ... 'NumTrainingFrames', 50, ... % 取前50帧做背景训练 'AdaptLearningRate', 0.005, ... % 背景更新速度,调小则更“记旧账” 'NumGaussians', 3, ... % 每个像素用3个高斯分量描述 'MinimumBackgroundRatio', 0.6); % 某分布累计占比到0.6即视为背景这段代码是系统的第一道闸门。NumTrainingFrames决定系统开机后需要多少帧来建立干净的背景模型,值设大一点能减少初始阶段的鬼影,但开机后等待时间变长。AdaptLearningRate是背景模型对场景变化的学习速率,0.005 属于中档:室内固定摄像头下表现平稳,室外有树叶晃动、水面反光时需要降到 0.001~0.002,否则这些“非人”运动会被持续吸收为背景。NumGaussians用 3 个分量就能覆盖大多数固定摄像头场景,分量越多越能处理多模态背景,但计算量也会上浮。
前景掩码输出后通常要接一次形态学闭运算,把人体内部因为衣服纹理产生的破洞连通,也把边缘毛刺去掉。这个步骤在代码里用vision.MorphologicalClose做,结构元素选 7×7 矩形,太小连通不完整,太大容易把相邻两个人连成一个目标。调参时可以先看单帧掩码效果再批量跑,不要上来就调分类器。
2.2 人体检测与关键点:优先考虑计算量而非精度
背景差分能拿到“有东西在动”,但拿不到“这是一整个人”。下一步要在前景区域上做人体框检测。MATLAB 自带peopleDetectorACF、peopleDetectorCascade和基于深度学习的检测器,CPU 环境下优先用 ACF,帧率比深度学习模型高一个数量级,检测精度对监控场景足够。
% 对当前帧做人形检测,这里选用ACF检测器 detector = peopleDetectorACF('caltech'); [bboxes, scores] = detect(detector, I); % ACF分数大于阈值的框才保留,阈值取决于场景和采样间隔 keep = scores > 2; bboxes = bboxes(keep, :); scores = scores(keep);这段代码的要点在于“分数阈值”并没有通用值。ACF 返回的 scores 在不同视频源、不同分辨率下分布差异很大,我一般会先跑一段带标注的录像,统计正常帧和异常帧的分数分布再定阈值。阈值设太低,背景里一些类人纹理会被框出来;设太高,晚上低照度下真实的人反而漏检。这里不做深度学习关键点检测,是因为本场景只需要人的整体框和重心位置,不需要精确到脚踝、手腕的骨骼坐标。实时性优先,精度够用就行。
2.3 动作特征设计:把框的变化翻译成可判别的数值
拿到人体框后,异常行为的判别依据来自框本身的几何特征。跌倒最典型的特征是人体高宽比从站姿的 4~5 急剧掉到 0.3 以下,同时重心快速下移并在地面高度附近停留。奔跑的特征是重心水平位移速度大且连续若干帧保持高速。挥拳这类上半身行为,则表现为框内上部区域的像素变化频繁。把这几个信号组织成特征向量,分类器才有东西可学。
% 从人体框与历史信息中提取单帧特征,prevBbox来自上一帧 function feat = extractFrameFeat(bbox, prevBbox, ratioThreshold) w = bbox(3); h = bbox(4); cx = bbox(1) + w / 2; cy = bbox(2) + h / 2; ratio = h / max(w, eps); % 高宽比,跌倒时剧烈下降 speed = hypot(cx - prevBbox(1), cy - prevBbox(2)); % 重心水平位移速度 areaChange = (w * h) / max(prevBbox(3) * prevBbox(4), eps); % 低高宽比 + 低速 + 重心偏低,是“倒地静止”的联合信号 fallHint = double(ratio < 1.2 && speed < 0.08 && cy > size(prevBbox, 1) * 0.6); feat = [ratio, speed, areaChange, cx / 640, cy / 480, fallHint]; end特征里最后汇总了 5 个连续量加 1 个逻辑量。ratio直接反映姿势,speed反映运动剧烈程度,areaChange反映目标在前进或站立时的面积扩张。cx/640、cy/480这两个归一化位置分量很有用:监控画面里不同位置发生异常,后续分类器能借助位置信息区分“在墙边滑倒”和“在通道里快跑”。fallHint是我在调试中发现非常有效的经验特征,它把“低高宽比+低速+位于画面下半部”三个条件合并成一个维度,比让 SVM 自己去组合这三个量更容易收敛。实际项目中我还会追加一个“地面停留帧数”特征,用于区分倒地不起和弯腰捡东西。
2.4 滑动窗口与统计压缩:避免把时序问题当成单帧问题
单帧特征的缺陷是抖动太大。站姿的人弯一下腰,高宽比瞬间就变了。所以系统里要引入时间维度,做法是维护一个滑动窗口,存储近 30 帧的特征向量,再对窗口内的每个特征取均值、最大值、最小值和标准差,最终压成一个低维向量再送进分类器。
这里有个工程取舍:直接把 30×6=180 维数据丢给分类器,理论上可行,但训练集不大时特别容易过拟合。压缩成统计量后特征维度降到 24 维,SVM、随机森林都能在几百个样本上稳定收敛。窗口长度选 30 帧对应 1 秒,既足够覆盖一次跌倒的完整过程,又不会因为窗口过长把跑步这类快速行为平滑掉。窗口滑动的实现不做逐帧重算,用队列结构先进先出,每来一帧就更新队尾、弹出队首,再重算统计量,计算开销很小。
3. 系统实现:GUI 界面、实时视频流与检测链路接线
3.1 App Designer 搭建界面:用 UIAxes 而不是 axes
这套系统的图形界面用 App Designer 实现。主界面包含一块视频显示区、开始/停止按钮、异常状态指示灯、以及一个显示当前行为类别的文本框。组件选择上有一个容易被忽视的点:视频显示必须用 UIAxes,不能用传统 axes。UIAxes 设计目标就是支持高频的imshow刷新和交互回调,传统 axes 在新版 MATLAB 中只是兼容存在,实时刷新时性能瓶颈会很突出。
界面的回调逻辑并不复杂。开始按钮的回调里创建视频读取对象和定时器,停止按钮的回调里清空定时器并释放摄像头占用。状态指示灯用一个 Lamp 组件,正常时亮绿色,异常时亮红色,同时文本框打印异常类型和置信度。参数区放三个可编辑字段:检测阈值、报警帧数、视频源编号,方便现场调试。
3.2 用 timer 驱动采集循环:让 UI 在检测过程中保持可操作
实时系统最忌讳在主线程里写死循环。很多第一次接触这个资源的人会习惯性写成while hasFrame(vidObj) ... end,结果界面一运行就卡死,按钮点不动、窗口拖不动。原因很简单:MATLAB 的 UI 事件循环被 while 占用,根本没机会处理鼠标消息。正确做法是让定时器对象按固定周期回调,每次回调只处理一帧。
% 在“开始检测”回调中创建定时器,Period单位是秒 vidSrc = webcam(app.CameraIDEditField.Value); % 打开指定摄像头 tImg = timer( ... 'TimerFcn', @(~,~) frameLoop(app), ... % 每周期执行一次帧处理 'Period', 1/30, ... % 目标30帧/秒 'ExecutionMode', 'fixedRate'); % 固定速率,不累计延迟 start(tImg);这段代码里有几个细节。ExecutionMode设为fixedRate后,每帧间隔按绝对时间计算,不会像fixedSpacing那样因为回调执行时间波动而漂移。回调中只做三件事:读取一帧、调检测函数、刷新画面。如果检测函数跑得太慢,宁可让Period放大到 1/20 也不要让 UI 卡顿。摄像头对象在停止回调中要显式clear vidSrc,否则下次启动会报“摄像头已被占用”,这是 MATLAB 图像采集最常见的翻车点之一。
3.3 检测与显示解耦:跳帧检测的接线方式
实时链路里最大的性能矛盾来自检测耗时。背景差分加 ACF 检测每帧大约需要 40~80ms,这就注定无法在每帧都完整检测。我的做法是:每 3 帧做一次完整检测,中间两帧只刷新画面不重新检测。画面刷新用上一帧的结果叠加框和标签,视觉上基本无感,但系统整体帧率能稳定在 20fps 以上。
定时器回调里用一个帧计数变量frameIdx,模 3 为 0 时执行检测流程,否则直接显示原始帧。检测函数内部还要做一步“滞后处理”:当前检测不到人体而上一帧检测到时,不立刻清空画面,而是用vision.KalmanFilter预测上一帧的框位置继续显示 5 帧,等目标重新出现。这一步能显著减少跳绳、拍手这类动作产生的目标闪烁。真实项目里,“检测目标在画面里跳进跳出”比“检测不到”更影响观感,卡尔曼平滑是性价比很高的补丁。
4. 分类器训练与调参:从特征到异常判定的完整闭环
4.1 构造训练集:正负样本怎么采集最省力
分类器的输入是 2.4 节生成的统计压缩特征。单样本的结构可以描述为下表:
| 特征组 | 包含内容 | 维度 |
|---|---|---|
| 高宽比统计 | 均值、最大值、最小值、标准差 | 4 |
| 重心速度统计 | 均值、最大值、最小值、标准差 | 4 |
| 面积变化统计 | 均值、最大值、最小值、标准差 | 4 |
| 位置统计 | 归一化 x、y 的均值与标准差 | 4 |
| 倒地提示统计 | 滑动窗口内 fallHint 的均值 | 1 |
| 地面停留帧数 | 窗口内低重心且低速的总帧数 | 1 |
| 首尾帧特征 | 窗口首帧与尾帧的高宽比、速度 | 4 |
合计 22 维。正样本收集方式建议直接录制异常演示视频,人工标注行为开始和结束的帧号,然后截取窗口生成样本。负样本来源更广:正常行走、站立聊天、弯腰整理东西、拿快递等日常动作都要覆盖。容易犯的错误是只录“站着不动”作为负样本,导致分类器把“任何运动”都视为异常,误报率爆炸。负样本里一定要包含弯腰、坐下、下蹲这类与跌倒外观接近的动作。
4.2 SVM 训练与参数语义
样本准备好后,训练 SVM 二分类器。MATLAB 的fitcsvm是核心入口,参数注释如下:
% 训练二分类支持向量机,检测异常行为 mdl = fitcsvm(featMat, labels, ... 'KernelFunction', 'rbf', ... % RBF核能刻画非线性决策边界 'BoxConstraint', 10, ... % 增大约束减小训练误差,但防过拟合需配合交叉验证 'Standardize', true, ... % 标准化各维度,防止高宽比压倒速度特征 'KernelScale', 'auto', ... % 自动估计RBF核宽度 'CrossVal', 'on', ... % 开启10折交叉验证 'KFold', 10); accuracy = 1 - kfoldLoss(mdl);BoxConstraint是这条代码里最值得解释的参数。它控制对误分类样本的惩罚力度,值越大,训练时越努力把每一个样本都分对,但决策边界会变得复杂,泛化能力下降;值越小,边界更平滑,能容忍少量难分样本。10 这个初值是我在类似特征维度下的常用起点,训练完先看交叉验证准确率,再在 1~100 之间对数扫描两轮。Standardize必须设为 true,否则高宽比这个量纲在几十的量级上会直接支配速度特征。RBF 核默认适合大多数情况,不要急着换多项式核或线性核。
4.3 随机森林作为备选:什么时候不该迷信 SVM
资源里同时提供了随机森林的备选实现,用fitcensemble打包 Bagging 树。SVM 在小样本、特征维度低时速度优势明显,但它的决策边界依赖核函数的选择,调参经验不足的人容易在核宽度上踩坑。随机森林几乎没有需要精细调整的参数,NumLearningCycles设 100,各树用随机特征子集,默认设置下就能得到稳定结果,缺点是在本场景中推理速度略慢,单帧大约多 2~5ms。我的经验做法是:样本量少于 300 时优先 SVM,样本量过千后优先随机森林;这两类分类器在这个任务上的准确率差距通常不超过 3 个百分点,不必追求“最优算法”。
4.4 时序投票:在分类器后加一道后悔药
分类器输出的是单窗口的决策,但一两个异常窗口并不能构成真正的行为。工程上我习惯在分类器后面再加投票逻辑:连续 5 个滑动窗口中,有 3 个判定为异常,才真正触发报警。
% 维护一个5元素的环形缓冲,记录最近5个窗口的判定结果 function isAlarm = voteAlarm(app, currentVote) app.voteBuf = [app.voteBuf(2:end), currentVote]; % 队列滚动 if sum(app.voteBuf) >= 3 % 5帧中有3帧异常 isAlarm = true; else isAlarm = false; end end这段代码把瞬时误判挡在了报警门外。voteBuf初始化为全 0 的 1×5 数组,每来一个新判定就推进一位。阈值为什么选 3 而不是 5?因为真实跌倒过程中,前期有低高宽比特征的帧数通常只有 20 帧左右,窗口切分的边界效应会导致部分窗口落在动作前段、特征不够典型。5 取 3 既保证连续性,又保留一定容错。报警后系统会自动保存触发前后各 10 秒的视频片段,方便事后复核,这个功能在演示时特别加分。
5. 实时系统的常见坑:性能瓶颈、误检与工程细节排查
5.1 点击开始按钮后界面卡死
现象:点击“开始检测”后,整个窗口失去响应,拖动窗口出现“白框”,只能强制关闭 MATLAB。
原因:回调函数里用了while循环逐帧读视频,循环体内没有让出 UI 事件处理,MATLAB 界面线程被完全阻塞。这个坑几乎所有第一次实现实时 GUI 的人都会遇到,不是代码逻辑错误,而是事件模型用错了。
解决:把 while 循环改成 timer 定时驱动,每周期只处理一帧。回调函数保持短小,不要在回调里做耗时的模型推理、特征可视化等操作,把耗时工作拆到独立的检测函数中,回调只负责取结果和刷新 UI。
5.2 弯腰捡东西被判定为跌倒
现象:画面里有人正常弯腰捡文件,系统连续触发跌倒报警,频繁误报。
原因:只用了单帧高宽比特征。弯腰和跌倒前期的外观非常接近,高宽比都明显下降,仅靠形状无法区分。
解决:给判定增加时序条件:跌倒必须满足重心先快速下移、随后 5 帧以上维持在画面低处且几乎不移动。也就是说,只把“倒地”而不是“弯腰”判为异常。特征向量中需要加入地面停留帧数与重心下降速度两个维度,并在投票缓冲里要求连续窗口满足低速条件,误报就能压下去。
5.3 接入深度学习姿态模型后帧率暴跌
现象:把检测段换成 OpenPose 一类关键点模型后,帧率从 25fps 跌到 3fps,实时性完全丧失。
原因:深度学习关键点模型的单帧推理耗时 200~400ms,而定时器周期只有 33ms,计算量远超系统吞吐能力。
解决:放弃在每帧上做关键点提取,只对 ACF 人体框做轻量特征计算;确有骨骼需求时,每 10 帧做一次关键点推理,中间用速度与位置插值补齐。用深度学习不是不行,但要把它放到“低频辅助”的位置,而不是主链路里。另外我一般会检查gpuDevice是否可用,MATLAB 的深度学习推理在 CPU 和 GPU 上耗时差距近 10 倍,偶尔是环境配置不到位导致速度异常。
5.4 人体静坐 40 秒后从前景中消失
现象:监控画面中有人坐在椅子上长时间不动,几十秒后前景掩码里再也找不到这个人,系统认为这是一张空画面。
原因:背景模型的AdaptLearningRate设得偏大,坐姿的人被缓慢吸收为背景。前景检测器本身没有“这曾经是个人”的记忆,像素颜色一旦被纳入背景分布,再想分离出来就需要下一次运动。
解决:把AdaptLearningRate降到 0.002 左右,同时定期重置背景模型。更稳妥的方法是叠加一层基于 ACF 的人体区域检测:即使前景掩码为空,只要 ACF 能检测到人形轮廓,就强制保留该区域的检测状态。这个兜底逻辑对防漏检非常有效,代价是每帧多几十毫秒计算。
5.5 换台电脑后报错找不到工具箱或函数
现象:代码在自己的机器上跑得好好的,换一台电脑就报Undefined function 'vision.ForegroundDetector'。
原因:目标机没有安装 Computer Vision Toolbox 或 Image Processing Toolbox。MATLAB 的 license 是按工具箱授权的,脚本本身不携带依赖,这个问题在把代码发给别人或找人代跑程序时频繁出现。
解决:交付前在命令行执行ver查看已安装工具箱列表,明确标注依赖项。如果目标用户没有完整 MATLAB 授权,就用 MATLAB Compiler 打独立 exe,目标机只需要安装对应版本的 MATLAB Runtime。需要特别确认的是 Runtime 的版本号必须与打包时一致,R2024a 打包的程序在 R2023b 的 Runtime 上无法运行。
6. 进阶:把原型推向实时与交付的最后一公里
6.1 用卡尔曼跟踪补足跳帧空窗
跳帧检测虽然保住了帧率,但检测结果天然带有间隔。与其等待每 3 帧做一次完整检测,不如在中间帧用卡尔曼滤波器预测目标的运动轨迹。vision.KalmanFilter的构造需要指定运动模型和数据维度,对矩形框跟踪用 4 维状态就可以:中心点 x、y,框宽、框高。
% 卡尔曼滤波器的初始化与单步预测 kal = vision.KalmanFilter('ConstantVelocity', ... 'StateCovariance', 10*eye(4), ... 'ProcessNoise', 1e-2, ... % 过程噪声,越大越“相信测量” 'MeasurementNoise', 1e-1); % 测量噪声,越大越“怀疑检测框” predBox = kal(bboxes(1, :)'); % 每次调用同时完成预测和修正过程噪声与测量噪声是一对相互作用的值:测量噪声调大,滤波器会更平滑地跟随检测框,但响应延迟增加;过程噪声调大,跟踪更积极,但也更容易被误检框带偏。经验上先固定测量噪声为 1e-1,再扫描过程噪声。跟踪器的输出不参与特征计算,只用于画面叠加和报警连续性判断,能避免检测器本身的框抖动放大分类器误判。
6.2 把 demo 打包成独立 exe
原型验证完成后,如果要在现场电脑上演示,直接用 MATLAB 打开工程是最省事的方式,但对方不一定有完整工具箱。打包是更稳妥的交付形态。在 MATLAB 里进入compiler.build.standaloneApplication流程,主函数包含创建 GUI 的入口,工具会自动拉取依赖。需要提前检查两点:一是摄像头采集支持包是否在依赖列表里,二是确认打包机与目标机 Runtime 版本一致。我踩过一次 Runtime 版本不匹配的坑,打包后 exe 一运行就闪退,排查了近一个小时才发现是 Runtime 装错了版本,从那以后每次打包前都强制检查version -runtime,并在交付说明里写明 Runtime 版本号。希望这套系统的拆解记录能帮你在复现和改造时少走弯路,尤其是 GUI 实时性这个最隐蔽的坑,早绕开早省心。
本文还有配套的精品资源,点击获取