
简介基于KDE核密度估计与密度估计方法实现行人检测和行人追踪的MATLAB项目全套源码面向刚入门及有一定基础的开发者可直接用于学习计算机视觉中的目标检测与跟踪流程。压缩包共26个文件其中24张jpg训练/测试帧图像包含不同时刻的连续帧1个m源码文件为核心算法实现另有1份docx文档说明项目结构、运行步骤与参数设置整体仅7.6MB轻量易部署。目前已有306人浏览学习代码经作者亲测校正百分百成功运行下载后若遇到环境或运行问题还可联系作者指导。整个项目提供完整可执行代码、配套图像数据集与文字说明既能帮助新手结合连续帧理解KDE密度估计原理和行人检测追踪的工程实现也可作为有经验开发者二次开发、算法调优与结果对比的起点。1. 为什么用 KDE 做行人检测与追踪一张密度图解决两个任务把行人检测和行人追踪同时做的思路其实很多但 KDE核密度估计Kernel Density Estimation这套密度估计方法常被忽略。它不需要你下载行人检测数据集去做训练也不需要 GPU仅凭 MATLAB 就能把固定摄像头画面里的行人位置“糊”成一张连续概率密度场密度场的每个峰就是行人峰在时间轴上的位移就是轨迹。于是检测与追踪不再分成两个独立模块而是共用一张密度图接口断层的麻烦直接少了一半。适合正在做算法原型、毕设或小型客流系统的工程师尤其适合场景相对固定、行人数量不爆炸的监控区域。2. KDE 与密度估计在行人检测里的角色从像素帧到概率密度场2.1 核密度估计的基本原理用核函数把稀疏观测“糊”成连续曲面核密度估计最早是统计学里估计随机变量概率密度的工具。给定一组观测样本它在每个样本位置放一个核函数再叠加归一化就得到一条平滑的密度曲线。一维公式写出来是f(x) 1 / (N·h) · Σ K((x - x_i) / h)其中 N 是样本数h 是带宽K 是核函数。行人检测里不做一维做二维样本是“可能是行人的前景像素坐标 (x, y)”核函数变成二维高斯核。二维形式等价于在每个坐标上叠一个小山包山包叠多了就连成起伏的地形。这个地形就是密度估计场。直方图也能做密度估计但直方图有箱体边界行人跨过箱体边界时密度值会跳变。KDE 的核函数是逐个样本叠加的输出连续峰值位置不容易受网格边界影响。这就是我做这个方案不选直方图反投影的原因。MATLAB 里可以用ksdensity先感受一维效果方便理解带宽的作用% 一维核密度估计示例生成双峰数据用 ksdensity 估计密度曲线 rng(42); x [randn(500,1) 5; randn(300,1) 12]; [fi, t] ksdensity(x, Bandwidth, 2); figure; plot(t, fi, LineWidth, 1.5); grid on; title(KDE Density Curve);rng(42)让随机序列可复现Bandwidth是核函数的带宽越大曲线越平滑越小越容易保留局部起伏。图像里的二维 KDE 不建议直接用ksdensity因为它是逐点计算对一张 1080p 图像而言样本点数和网格点数都太大会卡到怀疑人生。图像上的二维核密度估计有更快的工程做法我放到第三章讲。2.2 为什么选高斯核带宽矩阵与多维密度估计的取舍核函数类型很多矩形核、Epanechnikov 核、高斯核。矩形核实现最简单但密度场有硬边界峰值不平滑Epanechnikov 核理论效率高但 MATLAB 里没有现成函数。我一般默认用高斯核原因有三条。第一高斯核光滑可微后续做峰值检测时不容易出现局部毛刺。第二两个高斯卷积还是高斯做多尺度密度估计时性质稳定。第三二维高斯核滤波等价于 MATLAB 的imgaussfilt可以直接复用工具箱的加速实现不用手写卷积循环。多维密度估计的关键参数是带宽矩阵。二维场景下理论上要估计一个 2×2 矩阵能表达椭圆形的分布。但工程上调 2×2 矩阵很难直观掌控我通常直接简化成对角线矩阵也就是 x 和 y 方向各一个标量带宽。如果行人的宽高比比较固定再进一步简化成单一标量 h。带宽 h 决定密度图上峰的数量h 太小每个前景像素都成峰噪声会被当成行人h 太大相邻行人峰融合行人密度高时漏检严重。从计算复杂度看直接对 N 个样本点求 M 个网格点的密度是 O(N·M)处理一帧几万前景像素非常吃力。高斯核有一个特殊性质频域相乘等价于时域卷积。所以可以把样本点先累积到计数网格上再用 FFT 或者imgaussfilt做卷积复杂度降到 O(M log M)。这也是这个方案能用 MATLAB 在非实时但可交互的速度下跑起来的核心原因。3. 用 MATLAB 实现基于 KDE 的行人检测从背景建模到检测框3.1 第一步背景差分或帧差提取运动前景KDE 处理的是“哪些像素可能是行人”所以第一步要产出一张前景 mask。我没有直接在原图上做密度估计那样背景纹理也会形成大量假峰。常见做法是背景差分。MATLAB 的 Computer Vision Toolbox 里有一个vision.ForegroundDetector内置混合高斯背景模型能处理树叶晃动和光线渐变比帧差鲁棒。% 读取视频并创建前景检测器 vr VideoReader(walkway.mp4); fgDetector vision.ForegroundDetector(... NumGaussians, 3, ... NumTrainingFrames, 30, ... MinimumBackgroundRatio, 0.7); % 逐帧读取并提取前景 mask frameIdx 0; while hasFrame(vr) frame readFrame(vr); fgMask fgDetector(frame); % 逻辑型矩阵1表示前景 frameIdx frameIdx 1; if frameIdx 50 break; end endNumGaussians3表示背景模型用 3 个高斯分量能适应树枝晃动NumTrainingFrames30表示用前 30 帧训练背景模型MinimumBackgroundRatio0.7表示像素被判定为背景的最低概率阈值。如果摄像头有轻微抖动建议先用imregister或者 ECC 配准稳像否则大量边缘抖动量会被当成前景噪声送进 KDE密度场会变花。没有 Computer Vision Toolbox 时退而求其次可以用帧差% 帧差法保底方案无额外工具箱依赖 prevGray im2gray(frame); for idx 1:200 frame readFrame(vr); gray im2gray(frame); diffMask abs(gray - prevGray) 25; prevGray gray; % 形态学处理填补身体内部空洞滤除孤立噪点 diffMask imopen(diffMask, strel(disk, 2)); diffMask imclose(diffMask, strel(disk, 5)); end帧差法的问题是只能拿到行人的轮廓边缘身体中间因为前后帧变化小形成空洞。但这个问题到 KDE 这层会被弱化前景点虽然不完整密度估计仍能把这些点聚成一个峰只是峰的位置会偏向运动边缘的几何中心。所以帧差法更适合快速验证 KDE 流程正式项目我还是建议用混合高斯背景。3.2 第二步把前景像素映射为密度场二维 KDE 的 MATLAB 实现与参数选择拿到前景 mask 后把每个前景像素坐标当作一个“事件”在网格上做核密度估计。为了计算量可控不直接在原图分辨率做而是把 mask 投到一个固定尺寸网格上。% 输入fgMask 逻辑矩阵HxW [H, W] size(fgMask); gridSize [80, 120]; % 网格尺寸高度80宽度120 % 把前景像素坐标映射到网格坐标 [rows, cols] find(fgMask); % rows对应ycols对应x xBin round((cols - 1) / (W - 1) * (gridSize(2) - 1)) 1; yBin round((rows - 1) / (H - 1) * (gridSize(1) - 1)) 1; % 累积计数网格 densityGrid zeros(gridSize); for k 1:numel(xBin) densityGrid(yBin(k), xBin(k)) densityGrid(yBin(k), xBin(k)) 1; end % 二维高斯核估计用高斯滤波等效叠加 sigmaGrid 3.0; densityGrid imgaussfilt(densityGrid, sigmaGrid, Padding, replicate);imgaussfilt在这里就是核密度估计的高斯核叠加过程。sigmaGrid3是网格坐标系下的带宽如果原图是 1080p网格是 80×120那一个网格宽约 16 像素sigma3 等效于在原图上约 48 像素的高斯平滑。具体值要看视频里行人的宽度通常行人身体宽度在 30 到 60 像素时网格 sigma 取 2 到 3 比较合适。这个实现是“计数网格 高斯滤波”和严格意义上的核密度估计有什么区别严格做法要对每个样本用核函数在网格上直接赋值耗时而高斯滤波是对所有样本同时做核平滑数学上是同一个线性叠加过程前提是高斯核在网格上平移不变。网格足够密时误差可以忽略。同样要注意计数网格是 80×120比原图小得多所以内存占用很小。如果想在严格统计意义上归一化成概率密度可以在最后除以网格面积和样本总数但检测任务里我们只关注峰的位置归一化并不影响峰值坐标。3.3 第三步峰值检测与行人定位从密度图到 bounding box密度图上行人就是局部极大值峰。用形态学膨胀找局部极大值然后用阈值滤除低置信度峰。% 阈值最大峰值的20%以下不产生检测 thresh 0.2 * max(densityGrid(:)); binaryMap densityGrid thresh; % 局部极大值密度图与它的“局部膨胀结果”相等的点 maxMap imdilate(densityGrid, strel(disk, 5)); peakMask (densityGrid maxMap) binaryMap; [peakY, peakX] find(peakMask); % 将峰值坐标映射回原图并生成检测框 scaleX W / gridSize(2); scaleY H / gridSize(1); boxes zeros(0, 4); for k 1:numel(peakX) cx (peakX(k) - 1) * scaleX scaleX / 2; cy (peakY(k) - 1) * scaleY scaleY / 2; boxW sigmaGrid * scaleX * 4; % 宽度带宽换算到原图后×4 boxH sigmaGrid * scaleY * 7; % 高度按行人宽高比拉长 boxes(end1, :) [cx - boxW/2, cy - boxH/2, boxW, boxH]; %#okAGROW endstrel(disk, 5)的半径决定了峰值检测邻域大小半径太小会在一个行人身上出现多个峰太大又会把紧密相邻的行人合并。thresh设为最大峰值的 20%基本能滤掉背景残留的弱峰。检测框宽高用带宽经验倍数估算后续还可以用行人身高先验修正。这里缺少一步非极大值抑制因为同一个密度峰在边缘位置可能出现两个相邻极大值。用一段贪心 NMS 可以去掉重叠框% 贪心非极大值抑制按面积排序滤掉IoU0.5的重复框 [~, idx] sort(boxes(:,3) .* boxes(:,4), descend); keep true(size(boxes,1), 1); for i 1:size(boxes,1) if ~keep(idx(i)), continue; end for j i1:size(boxes,1) if ~keep(idx(j)), continue; end % 计算交并比IoU iou computeIoU(boxes(idx(i),:), boxes(idx(j),:)); if iou 0.5 keep(idx(j)) false; end end end boxes boxes(keep, :);computeIoU是自定义函数按标准交集除以并集计算。这一步虽然代码简单但直接影响检测框数量稳定性。4. 从检测到追踪用 KDE 密度场做数据关联与轨迹平滑4.1 用密度图替代检测框做追踪峰值跟踪 vs 聚类跟踪检测出来的峰值可以直接当观测值进追踪器。传统“检测框 IoU 匹配”的追踪在检测框抖动时 IoU 波动很大而密度峰值来自高斯滤波后的局部极大值天然有亚网格稳定性。每一帧的峰值坐标就是这个人当前最可能的位置追踪问题变成“如何把前后两帧的峰配对”。峰值数量少的时候直接做最近邻匹配。峰值数量多、密度高时最近邻容易串 ID。我更推荐把每个峰的位置当作卡尔曼滤波的观测值用状态预测做门控。卡尔曼滤波的好处是它显式建模了速度和位置在短时遮挡时能继续预测位置不会立刻丢轨迹。4.2 数据关联基于最近邻峰值匹配的 MATLAB 实现追踪器的核心是一个结构体数组每条轨迹包含卡尔曼状态、观测协方差和未匹配计数。初始化轨迹时用峰值位置[cx, cy]作为初始状态。% 创建一条轨迹 track.id nextId; track.state [cx; cy; 0; 0]; % x, y, vx, vy track.A [1 0 1 0; 0 1 0 1; 0 0 1 0; 0 0 0 1]; track.H [1 0 0 0; 0 1 0 0]; track.Q eye(4) * 0.01; % 过程噪声速度变化的容忍度 track.R eye(2) * 1.0; % 观测噪声峰值定位的精度 track.P eye(4) * 10; track.age 1;Q越大卡尔曼越相信观测轨迹预测越灵活但也更容易被噪声带偏。R越小越相信观测峰值位置。KDE 密度峰值通常比较稳定R1是合理起点。逐帧预测和匹配的伪代码流程% 预测所有已有轨迹的状态 for tIdx 1:numel(tracks) tr tracks(tIdx); tr.state tr.A * tr.state; tr.P tr.A * tr.P * tr.A tr.Q; end % 当前帧峰值坐标 curPeaks: Nx2 predPos cell2mat(cellfun((tr) tr.state(1:2), tracks, UniformOutput, false)); D pdist2(predPos, curPeaks); % 轨迹预测位置到峰值的距离矩阵 % 门控最大距离超过该距离的特征不考虑匹配 maxDist 30; [assignments, unassignedTracks, unassignedDets] greedyAssign(D, maxDist);greedyAssign把距离矩阵从小到大排序逐对配对。它的优点是没有匈牙利算法那么严格但胜在代码直观匹配结果也能接受。maxDist30是按网格坐标设置的如果场景中行人移动快可以放宽到 50如果移动慢收紧到 20避免不同行人抢同一条轨迹。4.3 处理遮挡与短暂丢失轨迹预测与密度残差补偿行人遮挡是追踪最大的坑。两个人交错时KDE 峰值会先合并成一个再分裂成两个。合并那几帧里轨迹找不到自己的观测峰值。我的处理方式是未匹配轨迹先不删除用卡尔曼预测位置继续参与下一轮匹配同时给轨迹的age加一。只有连续 6 帧以上没有匹配才删除这条轨迹。这是最直接也最有效的短时遮挡兜底。再深一层可以在密度图上做峰分离。两个峰合并时峰值位置偏到两人中间直接拿这个峰做观测会把轨迹拉偏。此时用分水岭对密度图做粘连峰切割% 把密度图取反分水岭找谷底切分开合并峰 D -densityGrid; L watershed(D); densitySplit densityGrid; densitySplit(L 0) 0; % 把分水岭边界处的密度值置零形成两个独立峰分水岭容易过度分割尤其是在密度图噪声较多时。所以我只在检测峰数量明显小于历史平均数量时启用这步。用imimposemin可以强制指定局部最小值让分水岭只在两个峰之间切一刀效果更可控。5. 避坑KDE 行人检测的 5 个翻车现场与排查清单5.1 密度图糊成一片检测框乱跳现象KDE 密度图上所有行人融为一体峰值点不在人身上检测框位置在几帧内大幅跳动。原因带宽太大或者网格降采样太粗。高斯核的覆盖范围大于行人间距时每个峰都被平滑到彼此连成一片局部极大值失去意义。解决把sigmaGrid调小。做法是先观察一帧密度图测量峰间距在网格上有多少个格子让sigmaGrid不超过峰间距的一半。比如网格 120×80两个人峰间距约 8 个网格那sigmaGrid取 23 比较安全。调整后跑一段 20 帧视频看峰值数量是否和目视行人数量接近。如果峰值仍然偏少继续减小。5.2 静止或缓慢移动的行人永远检不到现象行人站着打电话或排队不动背景差分把 ta 归入背景密度图上完全没有峰。原因这个方案的检测入口是运动前景KDE 本身只能聚拢前景点不能凭空创造静止目标。背景差分模型会逐步把静止时间过长的行人“学习”成背景。解决在背景差分之外并联一个静态目标检测通道。固定摄像头下可以用长时间前景累积维护一张浮点型“静止置信图”每次fgMask为真的位置加 1为假的位置衰减。当置信度超过阈值时也把这些像素纳入前景点送进 KDE。这样站着的人即使背景差分丢了置信图还能保留痕迹。要注意定期重置置信图否则场景里搬进来的新物体也会被当成行人。5.3 两个行人擦肩而过时轨迹发生 ID 互换现象追踪画面里两个人的标注 ID 在擦肩后对调了视觉上看起来像是“穿模”。原因擦肩瞬间两个峰短暂合并最近邻匹配把轨迹 A 匹配到了原本属于 B 的峰再把 B 匹配到 A 的峰导致 ID 交换。解决引入“外观特征”辅助匹配。在峰值附近的原图局部窗口提取灰度直方图或 HOG 特征计算特征距离与空间距离的加权和。空间距离权重 0.7直方图距离权重 0.3能有效减少 ID Swap。如果不方便加特征至少要给卡尔曼预测状态加门控只有新峰值连续两帧落在某轨迹的预测门控内才允许重新分配 ID。5.4 直接调用 ksdensity 处理整帧导致 MATLAB 卡死现象把上一个模块得到的前景坐标直接塞给ksdensityMATLAB 长时间无响应或内存暴涨。原因ksdensity默认会在每个评估点对每个样本计算核函数前景点几千上万个评估点再多几倍复杂度直接爆炸。这个问题我在刚开始实现时也踩过以为是 MATLAB 慢其实是算法复杂度不对。解决不要用ksdensity处理图像级数据。第三章的网格 imgaussfilt方案是更合适的工程简化。网格尺寸控制在 120×80 左右高斯滤波由 MATLAB 原生加速完成一帧处理时间能从几十秒降到几十毫秒级别。只有在离线分析小规模坐标点分布时才用ksdensity。5.5 人群密集时三个人被统计成两个现象三人并排走时峰值数变成两个中间人的位置被掩没。原因固定带宽无法适配局部密度差异。行人间距小于核宽时多个峰融合成一个大峰峰值数量偏少。解决对密度图做多尺度检测。第一遍用大带宽找出人群簇区域第二遍在这些区域内用小带宽重新估计密度并找峰。另一种做法是用分水岭切开融合峰但可能出现过分割需要配合追踪器的时间一致性过滤。实际项目中密集场景我一般把sigmaGrid降到 1.5并接受偶尔的过分割让追踪器在时间维度上做平滑。没有一劳永逸的参数密集程度不同需要重新调。6. 进阶用 MOT 指标验证检测追踪精度和参数自整定技巧6.1 用中心点距离评估多目标追踪精度KDE 检测追踪做完了怎么证明它靠谱建议用简化版 MOTA、MOTP。先手动标一小段视频的帧中心点再与 KDE 峰值位置配对计算平均中心点距离。% truthPts: Mx2 手动标注中心 estPts: Nx2 KDE峰值 D pdist2(estPts, truthPts); [minDist, matchIdx] min(D, [], 1); meanError mean(minDist); % 平均中心点像素误差meanError小于行人宽度的 30%说明检测定位是可用的。再统计追踪 ID 切换次数能直接暴露前面提到的 ID Swap 问题。6.2 用热力图离线调带宽我不会直接在监控现场碰运气调参数而是录一段 2 分钟视频每 30 帧暂停一次记下真实行人数目再跑 KDE 峰值统计。画一条“带宽 vs 峰值偏差”曲线选择偏差最小的带宽值。这个离线标定过程只需一次之后放到相似场景都能稳定复用。记住KDE 的带宽不是越大越好也不是越小越好它只是在你的场景里让峰数和真实人数最接近的那个值。我一般会在交付前专门录一段包含静止行人、双人交会和密集小组的测试视频把前五节里的坑各复现一遍再上现场。这个方法胜在原理透明、参数可控出了问题能一层层查下去不会像黑匣子模型那样只能干瞪眼。希望帮到你。本文还有配套的精品资源点击获取