简介:KCF(核相关滤波)目标跟踪算法的 MATLAB 实现,面向计算机视觉初学者、算法研究人员及需要快速验证跟踪效果的工程实践者,提供彩色原图追踪界面与帧数显示,便于直观观察跟踪过程。资源共含 2684 个文件,压缩包约 111.02MB;其中 2658 张 JPG 图片主要用于测试序列与中间结果,14 个 .m 文件覆盖特征提取、循环卷积、滤波器更新与可视化等核心逻辑,另有 .mat 数据文件和 .mex 加速编译文件,便于直接调用与二次开发。已有 2657 人学习下载,是目标跟踪入门与对比实验常用的参考实现。相比基础版 KCF,代码补充了彩色图像处理、结果保存与一键运行入口 run_tracker.m,通过阅读主跟踪类、HOG 特征提取和核相关滤波器更新代码,可清晰理解“特征提取—循环卷积—位置更新—滤波器更新”的完整流程,适合作为课程设计、算法对比或竞赛备战的起点。
1. KCF目标跟踪MATLAB代码:一个可复现的相关滤波基线
做目标跟踪的人,迟早会跟KCF这个名字打照面。不管是论文里需要对标的相关滤波方法,还是视频监控里要对检测框做持续跟住的模块,KCF几乎都是第一站。这份KCF目标跟踪MATLAB代码,就是把论文里那套核相关滤波算法完整落地的工程实现:HOG特征提取、岭回归训练、高斯核相关计算、模型在线更新,一个环节都不缺,跑通之后还能直接画出Precision曲线和成功率曲线,用来做实验对比非常顺手。适合刚接触相关滤波的学生复现论文结果,也适合需要快速拿一个可靠baseline做对照实验的开发者。代码是纯MATLAB写的,不依赖任何深度学习框架,打开工程就能定位到关键函数,改起来远比看公式直观——这也是为什么直到现在,提到KCF的MATLAB实现,大家还是愿意找这份代码当蓝本。
2. 技术选型与项目结构:十个文件组成的加速跟踪框架
2.1 核相关滤波的加速原理:循环矩阵与FFT对角化
KCF之前的相关滤波方法,比如MOSSE,只能处理单通道灰度图,特征表达能力有限。KCF的核心改进是引入了核方法:把多通道的HOG特征通过高斯核映射到高维空间,在保证表达能力的同时,依然保持样本的循环结构。这个"循环结构"是整个算法的命根子。
所谓循环结构,是指以目标为中心,在周围做密集采样得到的训练样本,可以通过对基准样本的循环移位来生成。这些样本构成的矩阵是循环矩阵,而循环矩阵有一个非常好的性质——可以被离散傅里叶变换对角化。换句话说,矩阵求逆这种在图像领域代价极高的运算,在频域里变成了逐元素的除法,复杂度从O(n³)直接降到O(n log n)。这也是为什么这套代码里大量出现fft2、ifft2,因为整个训练和检测过程都在频域里完成。
MATLAB做这件事有天然优势:矩阵运算是语言原生能力,FFT、逐元素运算、bsxfun这类操作写起来跟伪代码一样短,而且调试时可以直接在命令行里查看中间变量的维度。换成C++实现,光是把循环移位和傅里叶变换的边界条件处理好就要多写几百行。所以这套代码用MATLAB实现,不是随手选的,是这类算法最合适的载体。
2.2 目录结构:每个文件在整条链路里担任什么角色
这套工程的文件数量不多,但每个文件对应一个明确职责。先看整体清单:
| 文件 | 职责 |
|---|---|
| run_tracker.m | 运行入口,配置参数,调用单序列跟踪 |
| tracker.m | 主循环,初始化、训练、检测、更新的完整流程 |
| get_subwindow.m | 按位置和尺寸从原图截取子窗口 |
| fhog.m | HOG特征提取(带bin映射) |
| gaussian_correlation.m | 高斯核相关计算 |
| linear_correlation.m | 线性核相关计算 |
| load_video_info.m | 解析序列目录、读取真值文件 |
| download_videos.m | 下载公开测试视频序列 |
| evaluate_trackers.m | 批量跑多个序列并汇总结果 |
| precision_plot.m | 绘制Precision曲线图 |
其中tracker.m是核心,其他文件都是它的零部件。get_subwindow.m负责从当前帧抠出搜索区域,fhog.m把抠出来的图像块转成多通道特征,gaussian_correlation.m在频域算核相关矩阵,load_video_info.m把数据集提供的真值框转成算法需要的初始位置和尺寸。evaluate_trackers.m和precision_plot.m则是实验工具链,用来批量验证和出图。
我建议拿到代码先从run_tracker.m读起,因为它把参数、数据、评估三件事串起来了,读懂了入口再往tracker.m里钻,不会迷路。
2.3 运行环境与依赖:MATLAB版本、工具箱、路径准备
这套代码不挑版本,我一般建议R2016b以上,太老的版本对imresize和bsxfun的行为略有差异,可能导致边界处理结果不一致。工具箱方面,Image Processing Toolbox是必要的,因为代码里大量使用imresize、rgb2gray这类图像函数。不需要并行计算工具箱,单线程跑就够,KCF本身的效率在纯CPU上就很能打。
需要注意的一点:fhog.m这个文件依赖图像预处理里的一些辅助函数,它是随压缩包一起给出的,所以只要解压完整,不需要额外下载外部工具箱。但如果你是自己从论文作者主页拼凑源码,经常会漏掉这个文件导致报错,这点我在第5章的避坑清单里会展开讲。
路径方面,常见做法是在MATLAB里用addpath把代码根目录加进搜索路径,然后cd到视频序列所在目录。数据集目录建议用纯英文路径,中文路径下imread读取图片的兼容性一直是个玄学问题,能避开就避开。
提示:拿到代码后第一件事不是跑,而是先确认解压后的完整文件数。缺了
fhog.m或者gaussian_correlation.m,后面所有步骤都白搭。
3. 核心模块拆解:从输入帧到目标位置的完整链路
3.1 子窗口提取与余弦窗预处理
跟踪器每一帧要做的第一件事,是从当前帧里把以目标位置为中心的图像块抠出来。这个操作被封装在get_subwindow.m里:
function subwindow = get_subwindow(im, pos, sz, hann) % im: 当前帧图像 % pos: 目标中心位置 [y, x] % sz: 子窗口尺寸 [height, width],可以乘padding后的尺寸 % hann: 余弦窗,用于抑制边界效应,可为空 if isscalar(sz), sz = [sz, sz]; end xs = floor(pos(2)) + (1:sz(2)) - floor(sz(2)/2); ys = floor(pos(1)) + (1:sz(1)) - floor(sz(1)/2); % 越界像素用边界值重复填充,避免索引报错 xs(xs < 1 | xs > size(im,2)) = 1; ys(ys < 1 | ys > size(im,1)) = 1; subwindow = im(ys, xs, :); if nargin >= 4 && ~isempty(hann) subwindow = bsxfun(@times, subwindow, hann); end end这里的关键是xs和ys的构造方式:以目标中心为基准,向两边各伸展sz/2的像素范围。越界时统一取边界像素值而不是报错退出,这个处理保证目标靠近画面边缘时跟踪不会崩溃。如果传入了余弦窗hann,就用bsxfun(@times, ...)把窗口乘上去,这个操作会弱化图像块边缘的像素,避免循环移位时在边界产生剧烈的信号跳变。
参数上需要注意,sz传入的通常是floor(target_sz * (1 + padding)),也就是目标尺寸乘以搜索范围系数。hann窗的尺寸必须和sz一致,否则维度对不上,这是新手最容易踩的坑之一。
3.2 岭回归训练:从循环样本到频域闭式解
拿到第一帧的子窗口并提取特征后,就进入训练阶段。先要构造一个高斯形状的回归标签,然后用岭回归求解滤波器:
% 特征图尺寸是窗口尺寸除以HOG cell_size feature_sz = floor(window_sz / cell_size); % 高斯标签的带宽和目标尺寸、output_sigma_factor挂钩 output_sigma = sqrt(prod(target_sz)) * output_sigma_factor / cell_size; yf = fft2(gaussian_shaped_labels(output_sigma, feature_sz)); % 提取首帧特征并转换到频域 x = get_features(im, pos, window_sz, features); xf = fft2(x); % 高斯核相关矩阵 K = gaussian_correlation(xf, xf) k = gaussian_correlation(xf, xf, kernel.sigma); % 岭回归闭式解:alpha = yf / (k + lambda),在频域是逐元素相除 alphaf = yf ./ (k + lambda);gaussian_shaped_labels生成一个二维高斯分布,峰值在特征图中心,表示目标出现在中心位置的概率最高,越往边缘概率越低。之所以要求解岭回归的闭式解,是因为所有循环移位样本构成的样本矩阵可以被FFT对角化,原本的矩阵求逆变成了频域里的逐元素除法,alphaf一行就算完。
lambda是正则化系数,默认取1e-4,作用是防止某些频率分量接近零时除出来数值爆炸。kernel.sigma控制高斯核的带宽,这个值太小会让核相关矩阵几乎变成单位阵,学不到样本间的相似性;太大又会让所有样本的响应都差不多,目标定位就模糊了。实际操作里,0.5是一个比较稳的起点。
3.3 检测阶段:响应图最大值定位目标
训练完成之后,后续每一帧都要用当前模型去匹配新图像块。检测的本质是计算新样本和模型样本之间的核相关,然后在频域做一次反变换,得到空间域的响应图:
% 提取当前帧搜索区域特征 z = get_features(im, pos, window_sz, features); zf = fft2(z); % 与模型计算高斯核相关 k = gaussian_correlation(zf, model_xf, kernel.sigma); % 频域相乘后反变换回空间域,得到响应图 responses = real(ifft2(model_alphaf .* k)); % 响应图最大值位置就是目标的新位置 [vert_delta, horiz_delta] = find(responses == max(responses(:)), 1); % 注意:响应图是循环的,峰值出现在左上角等价于目标没动,要做中心化修正 if vert_delta > size(responses,1) / 2 vert_delta = vert_delta - size(responses,1); end if horiz_delta > size(responses,2) / 2 horiz_delta = horiz_delta - size(responses,2); end % 特征图上的位移要乘回cell_size,换算成像素位移 pos = pos + cell_size * [vert_delta - 1, horiz_delta - 1];model_alphaf是训练阶段得到的滤波器,和k在频域逐元素相乘,再反变换得到的就是响应图。响应图的尺寸和特征图一致,每个像素位置对应一种循环移位,值越大表示目标越可能出现在那个偏移量上。
这里最容易理解错的是峰值位置修正。因为循环矩阵的定义,目标停在原处时峰值会出现在响应图的左上角而不是中心。所以代码里做了一个判断:如果峰值在图的右半侧或下半侧,就减去整张图的尺寸,把位置映射回以中心为原点的相对位移。这个细节不处理,目标每帧都会往左上角漂。
3.4 模型更新:插值因子与遗忘速度的权衡
跟踪是一个持续学习的过程。如果模型永远固定在第一帧,目标发生光照变化、形变、遮挡后很快就跟丢了。所以每一帧检测完,都要用当前帧的样本去更新模型,KCF采用的是线性插值更新:
% 用当前帧重新计算训练样本和滤波器 xf = fft2(x); k = gaussian_correlation(xf, xf, kernel.sigma); alphaf = yf ./ (k + lambda); % 插值更新:新模型 = (1 - interp_factor) * 旧模型 + interp_factor * 新样本 model_xf = (1 - interp_factor) * model_xf + interp_factor * xf; model_alphaf = (1 - interp_factor) * model_alphaf + interp_factor * alphaf;interp_factor默认取0.02,含义是每帧只吸收当前样本2%的贡献。这个值取得非常保守,换来的是长期稳定性:即使某一帧检测结果不准,对模型的污染也有限。反过来,如果目标外观变化很快,比如骑车人转弯导致侧面对变背面的瞬间,0.02的更新速率可能跟不上,这时候可以适当调到0.03到0.05,但调高之后抗遮挡能力会下降,需要自己权衡。
还有一个隐蔽的细节:模型更新用的xf和alphaf是逐帧独立算的,但插值对象是模型参数本身。也就是说更新前先基于当前帧的旧位置重新训练一次,再和旧模型融合。这个"每帧都重新训练再融合"的做法,保证了更新过程始终对齐到目标的最新位置。
4. 运行复现与参数面板:把公开序列跑出Precision曲线
4.1 数据集准备:序列目录与真值文件格式
这套代码的输入不是单个视频文件,而是一个标准序列目录。以跟踪领域常用的公开评测序列(OTB格式)为例,每个序列是一个独立文件夹,里面放着按帧号排列的图片,以及一个groundtruth_rect.txt真值文件。目录结构长这样:
Basketball/ img/0001.jpg img/0002.jpg ... groundtruth_rect.txtgroundtruth_rect.txt每行对应一帧的真值框,常用格式是left, top, width, height四个整数,有些序列也用八个数表示四个角点坐标。load_video_info.m做的就是这件事:扫描文件夹、排序图片列表、解析真值文件,并把第一帧的真值框转换成算法需要的[x, y, width, height]格式。
这里我吃过大亏:有些公开序列的真值文件是四点坐标,而load_video_info默认按矩形格式解析。如果序列用的是四点坐标,解析出来的width和height会变成坐标差值,目标框尺寸翻倍还带偏移,第一帧基本就锁错了位置。遇到这种情况,我会先看一眼真值文件前两行的数据量,一行两个数是矩形格式,一行四个数(共八个数)就是四点格式,需要先做格式转换。
4.2 单序列运行:入口脚本与参数面板
run_tracker.m是唯一需要手动改的入口。运行方式是在MATLAB命令行里先addpath代码目录,然后调用函数:
% 指定序列名和视频根目录 seq_name = 'Basketball'; base_path = 'D:/datasets/OTB100'; % 运行跟踪器,返回每一帧的预测位置和平均耗时 [positions, fps] = run_tracker(seq_name, ... 'kernel', 'gaussian', ... 'features', 'hog', ... true, true);参数面板集中在tracker.m开头,几个关键参数的作用如下:
| 参数 | 默认值 | 作用 | 调参建议 |
|---|---|---|---|
| padding | 1.5 | 搜索区域相对目标尺寸的扩大倍数 | 目标运动剧烈可加到2.0,越小越快但容易丢 |
| output_sigma_factor | 1/16 | 高斯标签带宽系数 | 目标尺寸较大时适当减小 |
| lambda | 1e-4 | 岭回归正则化系数 | 数值不稳定时调大到1e-3 |
| kernel.sigma | 0.5 | 高斯核带宽 | 特征差异大时可以调到0.2~0.8 |
| interp_factor | 0.02 | 模型学习率 | 快速形变场景调大到0.05 |
| hog_cell_size | 4 | HOG cell的像素尺寸 | 调大提速但降低精度 |
改完参数直接在命令行跑,首帧会自动初始化,后续帧的检测结果会实时在figure窗口里画出目标框。fps在函数返回时打印出来,这个数字就是跟踪速度的直接体现,HOG特征下一般能到几十帧每秒。
4.3 批量评估与曲线绘制:验证算法改动的唯一标准
单序列跑通只说明代码没病,要验证算法级改动,必须走批量评估。evaluate_trackers.m负责这个事:指定一个序列列表,所有序列挨个跑,每帧的预测位置和真值的中心误差被记录下来,最后用precision_plot.m画出Precision曲线。
Precision曲线的横坐标是像素距离阈值,纵坐标是跟踪结果落在阈值内的帧数占比。曲线越高,说明定位越准。成功率曲线则是另一个维度的指标,用预测框和真值框的重叠率判定是否成功。
我个人的实验习惯是:任何改动,先在10个序列上跑完再说话,不要用一两个序列的效果下结论。相关滤波这类方法对序列特性很敏感,某个序列上提升1个百分点,换个序列可能掉3个点。批量跑完之后,除了看平均精度,还要单独看每个序列的差异,判断改动是普适改进还是只能打某些场景。
提示:跑批量评估前确认
evaluate_trackers.m里的序列列表路径和视频根目录一致,否则它会自动触发download_videos.m去联网下载,下到一半断网就白等了。
5. 避坑清单:KCF的MATLAB实现最容易翻车的五个细节
5.1 现象:运行报错,提示找不到函数或未定义变量
第一种常见情况是fhog未定义。原因通常是解压不完整,或者从网上零散拼凑代码时漏掉了fhog.m。这个文件依赖一组图像预处理辅助函数,是整个HOG特征通道的关键。解决方法是重新完整解压整个压缩包,确认根目录下fhog.m存在,然后把根目录addpath到MATLAB路径里。
第二种情况是bsxfun报维度错误。原因基本是get_subwindow返回的图像块和余弦窗尺寸对不上。解决方法是检查window_sz在传入时有没有经过floor取整,特征图尺寸必须是cell_size的整数倍,否则fhog输出的行列数和预期不符,后续所有矩阵运算全部串位。
5.2 现象:第一帧目标框就偏了,或者框的尺寸明显不对
如果初始化画出的框和视频里的目标位置不一致,先怀疑真值文件解析。load_video_info.m默认按left, top, width, height解析,但不少序列的真值文件是四点坐标。先打开groundtruth_rect.txt看一行里是4个数还是8个数,8个数就是四点格式,需要修改解析逻辑:取四个点的最小外接矩形作为初始化框。
还有一种情况是框的中心对但尺寸不对劲,常见原因是target_sz被当成了[width, height],而代码里用的是[height, width]的坐标顺序。真值文件里第一项是left对应x,第二项是top对应y,映射到矩阵索引时要做一次转置,顺序搞反了在宽高不等的目标上会明显看出框是横竖颠倒的。
5.3 现象:跟踪到中途目标丢失,框漂到背景上
框漂移是相关滤波跟踪器最典型的翻车现场。常见原因有两个:一是padding设得太小,搜索区域只比目标大一点点,目标快速移动时,新位置直接超出搜索范围,响应图根本找不到正确峰值。二是output_sigma_factor设得太大,高斯标签过于平缓,背景和目标的响应区分度不够。
解决方向上,先确认padding在1.5以上,再检查output_sigma_factor是否在1/16上下。如果都正常,那么问题多半出在模型更新上——interp_factor默认0.02,如果中途目标形变大,模型跟不上外观变化,需要适当调大学习率。我一般会先打印一帧响应图的最大值和峰值位置,看失联前几帧响应值是不是在持续下降,这个趋势比单帧判断更有说服力。
5.4 现象:响应图出现NaN或者全黑
响应图出现NaN,十有八九是高斯核计算里的指数项溢出了。gaussian_correlation.m里计算的是exp(-d / sigma^2),如果输入特征的值域特别大,d的数值会超出浮点数范围,指数结果变成Inf然后被归一化成NaN。这个问题在灰度特征上更容易出现,因为灰度值范围0到255直接参与距离计算,数值量级比HOG特征大得多。
排查方法是先检查特征提取后有没有做归一化。HOG特征一般值域可控,灰度特征则建议在进入gaussian_correlation前除以255做一个简单缩放。另外kernel.sigma太小时,比如低于0.1,同样会放大距离项导致数值溢出,先把sigma拉回0.5附近再排查别的。
5.5 现象:图片读取失败或者读进来的图像是黑的
这类问题集中在数据集路径上。MATLAB的imread对中文路径的支持一直是老大难,序列目录里只要有一个中文字符,图片读取就可能静默失败或者报错。解决方法是把整个数据集放到纯英文路径下,目录名也用英文。视频读取同理,走的是VideoReader,它对编码格式的兼容性更挑剔,所以我建议数据集尽量用图片序列而不是单个视频文件。
另外,如果图片是16位PNG或者带透明通道的格式,imshow显示正常但特征提取时可能拿到异常值。把图片统一转成8位三通道JPG或PNG再跑,能省掉很多隐性问题。检查方法很简单:在tracker.m里添加一行imshow(im),看第一帧显示是否正常。
6. 二次开发实战:把KCF改造成带多尺度估计的跟踪器
KCF最被人诟病的短板就是固定框尺寸,目标放大缩小它就锁不住。第二个开发方向,是给它加一个最简单的尺度估计。
思路是在检测阶段不再只用一个窗口尺寸去搜索目标,而是用一个尺度池,对每个候选尺度分别计算响应图,选择响应最大值对应的尺度作为当前目标框的缩放因子。改造点集中在tracker.m的检测环节:
% 尺度池:以1.05为步长,在当前尺度上下各取两档 scale_factors = 1.05 .^ (-2:2); best_scale = 1; best_score = -Inf; for s = scale_factors % 窗口尺寸随尺度因子缩放,注意取整 window_sz_scaled = floor(window_sz * s); z = get_features(im, pos, window_sz_scaled, features); zf = fft2(z); k = gaussian_correlation(zf, model_xf, kernel.sigma); response = real(ifft2(model_alphaf .* k)); score = max(response(:)); if score > best_score best_score = score; best_scale = s; best_response = response; end end % 用最优尺度的响应图做峰值定位 [vert_delta, horiz_delta] = find(best_response == max(best_response(:)), 1);这个改造的代价是检测耗时乘以尺度池的大小,5个尺度就是5倍计算量。实际项目里如果对速度有要求,可以缩到3个尺度(-1:1),或者把尺度因子改成1.03这种更细的粒度但只遍历3档。
验证改动是否有效,我的习惯是先不比较任何曲线,而是单独观察目标尺度变化明显的序列,每一帧打印出预测框的面积变化,看它能不能跟上目标由远及近的趋势。然后再回到批量评估里对比改动前后的平均精度,如果尺度相关的序列明显提升、其他序列不掉点,这个改动就值得保留。
从那以后我每次拿到别人的跟踪代码,都强制先原样跑通三组序列,记录每一帧的耗时和响应图最大值,再动手改任何一行。改动前留好baseline数据,改完逐序列对比,能避免掉入"调参调出幻觉"的坑。希望帮到你。
本文还有配套的精品资源,点击获取