由于项目需要在嵌入式系统中实现并应用跟踪算法,所以对非神经网络的、低计算复杂度的跟踪算法进行研究
研究对象?
回顾跟踪算法的发展历史,较为成熟的跟踪算法有MOSSE、KCF等
什么是MOSSE?
MOSSE 算法核心基于相关滤波思想:在频域中将目标模板与当前图像进行卷积操作,找出最大响应值的位置作为目标位置。具体而言,算法会学习一个频域滤波器 𝐻,使得它与输入帧中目标区域的卷积输出能够产生一个类似高斯分布的响应峰值。最早在初始化时,MOSSE 需要收集若干带有随机仿射变换的目标样本,用于训练或初始化滤波器;随后在跟踪过程中,随着帧的更新,滤波器也会进行自适应更新以适应外界环境变化(如光照、尺度、目标外观等)。
MOSSE的计算步骤?
下面展示 伪代码
。
------------------------------------------------
函数 MOSSE_Train( x_init, g_init, augment_params ):
# x_init:初始帧中裁剪的目标区域
# g_init:理想响应(例如2D高斯)
# augment_params:用来生成扰动样本(仿射变换、光照变化等)的参数
# 1. 生成若干扰动样本
X_list = []
G_list = []
for i in 1 to N: # N 是希望生成的扰动数量
x_i = DataAugment(x_init, augment_params)
g_i = g_init # 理想响应保持不变或做相应扰动
# 转换到频域
X_freq = FFT(x_i)
G_freq = FFT(g_i)
X_list.append(X_freq)
G_list.append(G_freq)
# 2. 利用前述公式计算初始滤波器 H
numerator = 0
denominator = 0
for i in 1 to N:
numerator += G_list[i] * Conjugate(X_list[i])
denominator += X_list[i] * Conjugate(X_list[i])
H_init = numerator / denominator
return H_init
------------------------------------------------
------------------------------------------------
函数 MOSSE_Track( current_frame, H_prev, position_prev, g, eta ):
# current_frame:当前帧图像
# H_prev:上一帧或初始训练得到的滤波器
# position_prev:上一帧目标位置
# g:理想响应(可固定,也可针对新的位置重新生成)
# eta:滤波器更新的学习率
# 1. 裁剪出搜索区域
x_crop = Crop(current_frame, position_prev, search_size)
# 2. 计算频域表示
X_t = FFT(x_crop)
# 3. 计算响应图并寻找最大峰值
R_freq = H_prev * X_t # 频域中逐点相乘
R_spatial = IFFT(R_freq) # 逆FFT 得到响应图
position_curr = ArgMax(R_spatial)
# 4. 可选:更新滤波器
X_conj = Conjugate(X_t)
H_update = (G * X_conj) / (X_t * X_conj)
H_new = (1 - eta) * H_prev + eta * H_update
# 5. 返回新的滤波器和目标位置
return H_new, position_curr
------------------------------------------------
------------------------------------------------
主流程 Main( video_stream ):
# 1. 从video_stream获取第一帧 initial_frame,选定目标位置 init_pos
# 2. 生成理想响应 g_init
x_init = Crop(initial_frame, init_pos, object_size)
# 3. 训练或初始化滤波器
H = MOSSE_Train(x_init, g_init, augment_params)
# 4. 开始逐帧跟踪
position = init_pos
for each frame in video_stream:
H, position = MOSSE_Track(frame, H, position, g_init, eta)
# 在实际系统中可以在视频中绘制当前位置或输出跟踪结果
------------------------------------------------
下面展示 python实现
。
import numpy as np
import cv2 # 如果没有安装opencv,可注释或替换相应图像处理方式
def fft2(image):
"""
二维傅立叶正变换 (封装 numpy.fft.fft2,便于后续调用)
"""
return np.fft.fft2(image)
def ifft2(freq_image):
"""
二维傅立叶逆变换 (封装 numpy.fft.ifft2)
"""
return np.fft.ifft2(freq_image)
def create_gaussian_response(shape, sigma=2.0):
"""
生成与 shape 相同的 2D 高斯分布,作为理想响应 g
shape: (height, width)
sigma: 高斯分布标准差
"""
h, w = shape
y, x = np.ogrid[0:h, 0:w]
cy, cx = h // 2, w // 2 # 高斯中心
# 高斯函数
gauss = np.exp(-0.5 * ((x - cx)**2 + (y - cy)**2) / sigma**2)
# 为了数值稳定或符合MOSSE习惯,可让峰值=1
gauss = gauss / gauss.max()
return gauss
def train_mosse(x_init, g_init, augment_times=0):
"""
简单版的 MOSSE 训练过程,主要获取初始频域滤波器 H_init
- x_init: 初始帧中裁剪的目标图像块 (时域)
- g_init: 理想响应 (时域)
- augment_times: 是否生成额外扰动样本做累加 (示例中不详细实现,可自行扩展)
"""
X_init = fft2(x_init)
G_init = fft2(g_init)
# 可根据需要对 x_init 做若干随机仿射扰动等,这里简化处理
# 只使用一个样本 (x_init, g_init)
numerator = G_init * np.conjugate(X_init)
denominator = X_init * np.conjugate(X_init)
# 如果有多个样本 (X_i, G_i), 则对分子分母进行累加即可
# numerator += ...
# denominator += ...
# 避免除零
epsilon = 1e-5
H_init = numerator / (denominator + epsilon)
return H_init
def mosse_update(frame, H, center_pos, window_size, g, learning_rate=0.02):
"""
在当前帧上使用已知滤波器 H 做跟踪,并返回更新后的滤波器与目标位置
- frame: 当前帧图像(彩色或灰度均可,需保证和 x_init 同样处理方式)
- H: 之前训练好的频域滤波器
- center_pos: 上一帧目标中心坐标 (y, x)
- window_size: 裁剪窗口大小 (h, w),即在 center_pos 附近裁剪多大区域
- g: 理想响应(时域),与裁剪窗口同大小
- learning_rate: 滤波器更新的学习率
"""
# 1. 裁剪搜索区域 (此处做简单处理,需考虑边界检查)
y, x = center_pos
h, w = window_size
# 可能需要做更严格的边界处理,这里仅示意
y1, y2 = int(y - h//2), int(y + h//2)
x1, x2 = int(x - w//2), int(x + w//2)
# 若超出边界,可以在此做padding
if y1 < 0: y1 = 0
if x1 < 0: x1 = 0
if y2 > frame.shape[0]: y2 = frame.shape[0]
if x2 > frame.shape[1]: x2 = frame.shape[1]
# 转为单通道灰度图以便处理 (如已是灰度,可忽略)
if len(frame.shape) == 3:
frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
else:
frame_gray = frame
patch = frame_gray[y1:y2, x1:x2].astype(np.float32)
# 若 patch 与 g 大小不匹配,可做resize或padding
# 这里为了简单,要求 window_size 与 g 大小一致
if patch.shape != g.shape:
patch = cv2.resize(patch, (g.shape[1], g.shape[0]))
# 2. 计算当前裁剪块的频域表示
X = fft2(patch)
# 3. 在频域中计算响应图 R = IFFT( H * X )
R = ifft2(H * X)
# 取其实部
R_spatial = np.real(R)
# 4. 找到响应图中的峰值位置
# 该位置相对于 patch 的左上角
peak_idx = np.unravel_index(np.argmax(R_spatial), R_spatial.shape)
peak_y, peak_x = peak_idx[0], peak_idx[1]
# 计算在原图中的绝对坐标 (根据裁剪时的偏移)
new_center_y = y1 + peak_y
new_center_x = x1 + peak_x
new_pos = (new_center_y, new_center_x)
# 5. 使用学习率更新滤波器
# H_new = (1 - eta)*H + eta * (G * X_conj) / (X * X_conj)
X_conj = np.conjugate(X)
G = fft2(g) # g也可固定提前算好
numerator = G * X_conj
denominator = X * X_conj + 1e-5
H_update = numerator / denominator
H_new = (1 - learning_rate) * H + learning_rate * H_update
return H_new, new_pos
def demo_tracking(video_path):
"""
演示在一个视频文件上进行 MOSSE 跟踪(简单示例)。
- video_path: 视频文件路径
"""
cap = cv2.VideoCapture(video_path)
ret, frame = cap.read()
if not ret:
print("无法读取视频或视频为空")
return
# 1. 在第一帧指定一个初始目标区域 (这里手动给定或使用cv2.selectROI)
r = cv2.selectROI("Select ROI", frame, fromCenter=False, showCrosshair=True)
cv2.destroyWindow("Select ROI")
# ROI 格式一般是 (x, y, w, h)
x0, y0, w0, h0 = r
# 裁剪初始目标块
init_patch = frame[y0:y0+h0, x0:x0+w0]
# 转灰度 & float32
init_patch_gray = cv2.cvtColor(init_patch, cv2.COLOR_BGR2GRAY).astype(np.float32)
# 2. 构造理想响应
g_init = create_gaussian_response((h0, w0), sigma=2.0)
# 3. 训练初始滤波器 (可加扰动增强,这里简化)
H = train_mosse(init_patch_gray, g_init)
# 4. 逐帧跟踪
center_pos = (y0 + h0/2, x0 + w0/2) # (y, x)
while True:
ret, frame = cap.read()
if not ret:
break
# 执行跟踪
H, center_pos = mosse_update(
frame,
H,
center_pos,
(h0, w0), # 搜索区域与目标初始大小相同(实际可放大一点)
g_init,
learning_rate=0.02
)
# 在帧上画出当前跟踪到的位置
cy, cx = center_pos
# 转为整数
cy, cx = int(cy), int(cx)
# 这里简单地画一个矩形,宽高与初始化一样
top_left = (cx - w0//2, cy - h0//2)
bottom_right = (cx + w0//2, cy + h0//2)
cv2.rectangle(frame, top_left, bottom_right, (0, 255, 0), 2) # 绿色框
cv2.circle(frame, (cx, cy), 2, (0, 0, 255), -1) # 红点标中心
cv2.imshow("MOSSE Tracking Demo", frame)
key = cv2.waitKey(30)
if key == 27: # ESC 退出
break
cap.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
# 修改为本地视频文件路径,或者使用摄像头(0)
video_file_path = "test_video.mp4"
demo_tracking(video_file_path)
什么是KCF?
KCF (Kernelized Correlation Filter) 是由 João F. Henriques 等人在 2015 年的 TPAMI 论文 “High-Speed Tracking with Kernelized Correlation Filters” 中提出的一种单目标跟踪算法。它是在早期 MOSSE (Minimum Output Sum of Squared Error) 算法基础之上的改进,引入了核技巧(Kernel Trick)与多通道特征(例如 HoG、颜色等),从而显著提升了跟踪的准确度与鲁棒性。
KCF的计算步骤?
下面展示 伪代码
。
------------------------------------------------
# KCF 训练阶段
函数 KCF_Train( x_init, y_init, feature_type, lambda, sigma ):
# x_init: 初始目标图像块 (H x W x C) (C为通道数)
# y_init: 理想响应 (H x W)
# feature_type: 例如 'HOG' 或 'Raw' 等
# lambda: 正则化系数
# sigma: 高斯核的带宽参数(若使用RBF核)
# 1. 特征提取
X_spatial = FeatureExtract( x_init, feature_type )
# 2. 转到频域 (若需要在频域中进行操作)
X_freq = FFT2( X_spatial ) # KCF中往往对特征/卷积进行FFT
# 3. 计算自相关的核矩阵 K_xx
# 如果是高斯核,则 K_xx = exp(-1/(sigma^2) * ||x - x_shift||^2),
# 但利用循环移位在频域进行简化
K_xx = CalcGaussianKernel( X_spatial, X_spatial, sigma )
K_xx_freq = FFT2( K_xx )
# 4. 根据相关滤波理论,求解 alpha
# alpha_freq = Y_freq / (K_xx_freq + lambda)
Y_freq = FFT2( y_init )
alpha_freq = Y_freq / (K_xx_freq + lambda)
# 5. 返回训练得到的滤波器参数(在KCF通常需要两部分信息)
model = {}
model.alpha_freq = alpha_freq # 与目标模板对应的加权系数(频域)
model.X_freq = X_freq # 目标模板自身(特征的频域表示)
model.lambda = lambda
model.sigma = sigma
return model
------------------------------------------------
------------------------------------------------
# KCF 跟踪更新阶段(每帧)
函数 KCF_Update( frame, model, prev_position, window_size, feature_type, eta ):
# frame: 当前帧图像
# model: KCF_Train返回的现有滤波器模型
# prev_position: 上一帧目标中心坐标 (y, x)
# window_size: 从frame中裁剪搜索区域的大小 (H, W)
# feature_type: 跟训练时一致
# eta: 线上更新学习率
# 1. 根据 prev_position 裁剪搜索窗口 z
z = CropSearchWindow( frame, prev_position, window_size )
# 2. 提取特征并转到频域
Z_spatial = FeatureExtract( z, feature_type )
Z_freq = FFT2( Z_spatial )
# 3. 计算响应图 R
# R_freq = alpha_freq * kappa_freq
# 其中 kappa_freq = FFT2( CalcGaussianKernel(Z_spatial, X_spatial, sigma) )
# X_spatial 可由 model.X_freq 的逆FFT 得到或直接在频域计算
X_spatial = IFFT2( model.X_freq )
K_zx = CalcGaussianKernel( Z_spatial, X_spatial, model.sigma )
K_zx_freq = FFT2( K_zx )
R_freq = model.alpha_freq * K_zx_freq
R_spatial = IFFT2( R_freq ) # 时域响应
# 4. 找到 R_spatial 最大值处(peak)作为目标新的中心位置
peak_pos = ArgMax2D( R_spatial )
new_position = ConvertToFrameCoords( peak_pos, prev_position, window_size )
# 5. 在线更新: 以 new_position 为中心裁剪真正的目标块
new_x = CropObjectWindow( frame, new_position, window_size )
new_X_spatial = FeatureExtract( new_x, feature_type )
new_X_freq = FFT2( new_X_spatial )
# 同样地,计算新的 K_xx_freq, alpha_freq
# K_xx_new = CalcGaussianKernel(new_X_spatial, new_X_spatial, sigma)
# alpha_freq_new = FFT2(y_init) / (FFT2(K_xx_new) + lambda)
# 为了简单,这里假设原理想响应 y_init 不变
# 实际中可根据新的窗口大小/尺度进行调整
K_xx_new = CalcGaussianKernel( new_X_spatial, new_X_spatial, model.sigma )
K_xx_new_freq = FFT2( K_xx_new )
# 假设 y_init 不变并预先 FFT2 存储在 model 中
# 若没有,可再次计算 y_init_freq
y_init_freq = model.alpha_freq * (model.X_freq + model.lambda) # 仅演示,需实际存储或重新计算
alpha_freq_new = y_init_freq / (K_xx_new_freq + model.lambda)
# 融合更新
model.alpha_freq = (1 - eta)*model.alpha_freq + eta*alpha_freq_new
model.X_freq = (1 - eta)*model.X_freq + eta*new_X_freq
return new_position, model
------------------------------------------------
总结
与 MOSSE 相比,KCF (Kernelized Correlation Filter) 主要在以下几个方面进行了增强或改进:
引入核技巧(Kernel Trick)
MOSSE 使用的是线性相关滤波,直接在时域/频域与样本做点乘。
KCF 则使用高斯核或多项式核等非线性核函数,通过核技巧在高维映射空间中进行相关计算,从而能更好地捕捉目标外观的非线性变化,提升跟踪的鲁棒性。
支持多通道特征
MOSSE 原始版本通常只使用灰度或较为简单的单通道特征。
KCF 通常采用 HoG、颜色直方图等多通道特征,综合利用多个维度的外观信息,使得对光照、角度、背景变化更具适应性。
更完善的循环移位训练
虽然 MOSSE 也利用了部分“扰动”思路(如仿射变换、随机噪声),但其原理上更偏向于线性模板匹配。
KCF 在核相关滤波理论中把循环移位思想系统化,大量偏移样本在频域中等价处理,提高了训练效率和对目标平移的鲁棒性。
跟踪精度与鲁棒性更高
综合核函数与多通道特征带来的更丰富外观描述,KCF 相对于仅用灰度的 MOSSE,在遮挡、光照变化、背景干扰等情况下也能保持更稳定的跟踪效果。
相似的高效率
虽然 KCF 做了核映射与多通道特征处理,但其仍然在频域中完成主要的卷积/相关操作,时间复杂度依旧较低,可实现接近实时甚至实时的速度。