ARTICLE DETAIL

资讯详情

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

目标跟踪算法融合实践:孪生网络与相关滤波互补优化

目标跟踪算法融合实践:孪生网络与相关滤波互补优化 做目标跟踪这几年我最大的体会是相关滤波和孪生网络这两条技术路线单拿出来都各有短板合在一起才够味。相关滤波靠在线更新扛住了目标外观的连续变化但特征表达能力跟不上孪生网络靠深度特征把判别力拉满却因为模板固定而常在遮挡、形变、快速运动场景里翻车。这篇博文想聊的就是怎么把这两套东西做一个可落地的融合优化目标跟踪的精度和鲁棒性都能往上走一截而且我附了完整的PyTorch代码适合正在做跟踪方向毕业设计、或者想给项目接入实时跟踪模块的工程师直接抄作业。背景上先对齐一下我这里说的孪生网络指的是SiamFC、SiamRPN这一路以互相关为核心算子的深度跟踪框架相关滤波则是KCF、DSST这些以岭回归和循环移位为根基的传统方法。下文不会把两套原理从头再推导一遍而是重点讲清楚为什么融合、融合时哪些模块容易出问题、代码长什么样、实测结果怎么解读。1. 为什么要把相关滤波揉进孪生网络孪生网络的硬伤与相关滤波的看家本领先说一个反直觉的现象SiamFC这种端到端训练的孪生网络在VOT、OTB上精度指标相当能打但你真把它放进一段长视频里跑会发现它常常在目标轻度遮挡后一蹶不振。原因不是特征提取不够强而是它的模板分支只在第一帧拍了一张照片之后无论目标换了衣服、转了角度、还是被汽车挡了一半网络手里的模板始终是那张旧照片。1.1 孪生网络的模板固定问题外观变了模型却记旧账孪生网络的推理过程可以简化理解成模板分支输出一个特征图搜索分支输出一个大范围的特征图两边的特征图做互相关运算响应最大值对应的位置就是目标的新位置。问题恰恰出在模板分支的输出上——它来自第一帧整个跟踪过程里不再变化。我实测过一个很典型的场景一个人背对镜头走进小巷前30帧跟踪得很稳第40帧他转身了响应图的峰值瞬间掉了一半框子在转身过程中反复摇摆。你说网络错了吗它没错它手里那张标准照就是背影正脸出现时匹配度天然下降。这种情况下任何依靠固定模板的深度学习跟踪器都会遇到同样的尴尬。孪生网络也不是完全没有补救手段SiamRPN的论文里提过模板可以通过在线学习微调SiamFC的原始版本则干脆放弃了更新只靠一个固定模板跑到底。在线更新的难点在于跟踪器无法保证每一帧的预测都是正确的一旦把错误结果喂回去更新模板误差会像滚雪球一样积累最终把模板污染成一张四不像。这就是模板更新的终极矛盾——不更新跟不上外观变化更新又容易累积漂移。1.2 相关滤波的在线更新与频域效率优势相关滤波这边的情况几乎跟孪生网络互补。KCF这一类方法对目标外观的建模很轻量把目标区域提取特征构造一个岭回归问题在频域里直接闭式求解出滤波器模板。最关键的是它每一帧都可以拿当前的预测结果作为新样本对滤波器做一次平滑更新。这种边跟踪边学习的机制让相关滤波在目标缓慢变形、光照渐变的长序列里表现得异常稳定。它的另外一个杀手锏是速度。因为相关操作被转换成了频域里的逐元素乘法配合循环移位生成的密集样本整个求解过程几乎不需要迭代。哪怕是在树莓派这种资源受限的硬件上原始的KCF也能轻松跑出上百帧每秒。但相关滤波的问题同样致命它用的特征多半是HOG、CN这类手工特征表达能力有限遇到同类目标密集出现、或者背景纹理与目标高度相似的情况滤波器的判别力立刻捉襟见肘。另外循环移位假设会带来边界效应——目标一旦走出搜索区域边缘滤波器就会被周期性重复的影子干扰。所以这条路线的核心逻辑很直接用孪生网络的深度特征补相关滤波的判别力用相关滤波的在线更新补孪生网络的模板老化问题。两者结合互相填坑。2. 核心原理拆解互相关、岭回归与频域求解想融合这两套东西必须先弄明白它们各自口中的相关到底在干什么。很多人把孪生网络里的互相关和相关滤波里的相关性混为一谈代码里蹲了半天也找不到哪里可以共用算子本质原因就是没把这两层纸捅破。2.1 孪生网络里的相关在算什么孪生网络中的互相关操作本质上是在做模板特征与搜索特征之间的模式匹配。你可以把模板特征想象成一个探照灯在搜索区域的特征图上逐位置滑动每滑动到一个位置就计算一次内积相似度最后得到一张响应图。响应图上谁最亮目标就在哪。SiamFC和SiamRPN在具体实现上都用了深度互相关depthwise correlation代替普通互相关因为逐通道相关可以保留更多通道语义信息。代码层面并不复杂PyTorch里用分组卷积就能实现把模板特征当成卷积核搜索特征当成输入分组数等于通道数每一个通道单独做二维相关再把所有通道的响应图累加。这样做的好处是参数少、速度快而且端到端可训练。2.2 相关滤波的闭环解与傅里叶加速相关滤波的思路完全不同。它不追求相似性匹配而是先给定一个理想的高斯响应标签——目标中心位置的响应值最高越远越低——然后求解一个滤波器使得滤波器与目标特征做相关之后输出尽可能逼近这个高斯标签。数学上最后归结为一个岭回归问题$$w X^T(XX^T \lambda I)^{-1}y$$这个方程看起来需要对矩阵求逆实际求解会把人卡死因为X的维度跟样本数挂钩动辄几十万维。相关滤波的经典技巧是利用循环移位构造的循环矩阵性质把矩阵运算全部搬到频域样本矩阵的傅里叶变换能对角化循环矩阵于是上面的求逆操作变成了逐元素的除法和乘法。整个过程不需要迭代也不需要高级优化器十几行代码就能在频域里解出最优滤波器。正因为求解过程可以被压缩成傅里叶变换加几次点乘相关滤波才能做到极致的速度。即便后来用上深度特征只要特征维度别太夸张频域求解的速度优势依然明显。2.3 两类相关的本质联系把两者的数学结构放在一起看你会发现一个有意思的联系孪生网络是在一个可学习的神经网络里做浅层的相似度度量相关滤波则是在一个固定的线性模型里做最优相关设计。一个偏重特征好不好一个偏重模板怎么算最准。因此融合方案不必强行合并两个算子更务实的做法是各留各的优势模块在一个跟踪框架里做结果级或决策级的融合孪生网络负责提供高判别力的响应图相关滤波负责提供持续更新的另一路响应图两路响应图根据置信度动态加权最终得到综合位置。这个思路实现成本低而且不会干扰孪生网络本身的训练过程是我在实际项目里推荐优先尝试的方案。3. 优化方案设计响应融合与在线模板修正双通道我的设计思路是双通道并行而不是把相关滤波塞进神经网络内部做端到端优化。原因是端到端联合训练需要设计新的损失函数数据需求量大收敛不稳定结果级融合则简单直接孪生网络部分可以沿用预训练权重相关滤波部分完全在线求解落地成本低很多。3.1 总体框架整个跟踪器分成三个大模块孪生网络分支输入模板和搜索区域输出一个深度响应图。相关滤波分支以手工特征或深度特征为输入在线训练滤波器输出另一个响应图。融合决策模块根据两个响应图的置信度估计动态计算权重输出最终位置并决定是否更新相关滤波器的训练样本。孪生网络分支我用SiamFC作为基底相关滤波分支我采用KCF的求解框架但把特征从HOG升级成孪生网络中间层提取的深度特征——这样相关滤波分到的特征质量直接提升一个档次。在实现细节上相关滤波分支只对目标中心附近的局部区域做特征提取不处理整个搜索区域。这样做一方面降低频域求解的运算量另一方面也避开目标位移过大时循环移位假设失效的问题。3.2 置信度引导的响应融合策略两路响应图直接相加是不可取的因为两类响应图的分布范围、峰值尖锐度差异很大。孪生网络的响应图经过softmax归一化之后数值范围在0到1之间相关滤波的响应图则取决于特征范数和滤波器的尺度峰值可能超过几十。生硬相加的结果往往是数值大的一路完全压过另一路融合等于没融合。我的做法是先对两路响应图分别做归一化将最大值都缩放到1再根据各自的置信度计算融合权重。置信度指标我选用峰值旁瓣比PSR和平均峰相关能量APCEPSR衡量峰值相对于周围旁瓣的突出程度峰值越尖锐说明跟踪结果越可信。APCE衡量整个响应图的能量集中程度响应图越干净、越没有杂乱的次峰置信度越高。融合权重由两个置信度指标综合计算具体公式可以根据数据集情况调节。实现起来不复杂只要计算每个响应图的PSR和APCE加权生成两路的系数即可。这样做的最大好处是当孪生网络因为模板老化导致响应图出现双峰、峰值分散时APCE会下降融合权重自动向相关滤波分支倾斜反过来当目标快速运动导致相关滤波边界效应严重时它的PSR也会下降权重自动转向孪生网络分支。两个模块形成了天然的互补。3.3 基于CF响应的模板更新决策相关滤波分支的另一个重要职责是给孪生网络的模板更新提供守门信号。我实现了一个很轻量的模板更新模块每隔N帧如果相关滤波的置信度较高就用当前帧目标的特征对模板分支的特征做一次滑动的指数平均更新。由于相关滤波本身就带有在线学习的能力它的置信度更及时地反映目标外观的渐变过程用它做更新触发信号可以避免固定模板在长序列里的老化问题。需要强调一点不要把孪生网络的模板更新频率设得太高。模板更新之后搜索区域特征和模板特征的匹配关系会发生偏移如果每一帧都更新响应图会出现小幅震荡目标的框会跟着抖。实测下来5到10帧更新一次是比较合理的区间而且更新系数要控制在0.05到0.15之间保持模板的历史记忆。这样设计之后整个跟踪器既保留了孪生网络在复杂背景下的高判别力又引入了相关滤波每帧都在悄悄更新的稳定性。4. 关键代码实现一个可跑的简化版本下面给出一个可运行的PyTorch版本。为了便于理解和测试我做了一些简化孪生网络骨干用了小型卷积网络相关滤波分支也省略了多尺度搜索和手工特征部分保留了核心结构。你在自己的项目里可以直接替换成ResNet骨干或者SiamRPN的RPN头。4.1 环境依赖与数据准备代码依赖主要是PyTorch、OpenCV和NumPy数据集可以用OTB或者GOT-10k的任意序列也可以直接用摄像头采集的视频做在线测试。数据准备的逻辑是对每一帧设定一个目标框以目标框为中心裁剪模板区域和搜索区域并把它们resize到网络输入尺寸。import torch import torch.nn as nn import torch.nn.functional as F import numpy as np import cv2 # 以OTB格式为例读取初始目标框 # 这里假设groundtruth.txt里每行是 left, top, width, height def read_gt(path): bboxes [] with open(path, r) as f: for line in f.readlines(): line line.strip().split(,) l, t, w, h map(float, line[:4]) bboxes.append([l, t, w, h]) return bboxes4.2 孪生网络骨干与互相关层骨干网络我这里用一个简单的六层卷积网络输出128通道的特征图。模板分支输入为127x127的图像搜索区域输入为255x255的图像互相关在6x6的模板特征和22x22的搜索特征之间进行。深度互相关可以用分组卷积实现我把模板特征当作卷积核batch维度拆开逐样本做分组卷积class SiameseBackbone(nn.Module): def __init__(self): super().__init__() self.feature nn.Sequential( nn.Conv2d(3, 64, 3, stride2, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.Conv2d(64, 128, 3, stride2, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.Conv2d(128, 128, 3, stride1, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.Conv2d(128, 128, 3, stride2, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), ) def forward(self, x): return self.feature(x) def depthwise_cross_correlation(z_feat, x_feat): # z_feat: (B, C, H_z, W_z), x_feat: (B, C, H_x, W_x) B, C, H_z, W_z z_feat.shape H_x, W_x x_feat.shape[2], x_feat.shape[3] z_feat z_feat.reshape(B, C, H_z * W_z) x_feat x_feat.reshape(B, C, H_x * W_x) # 每个通道单独做矩阵乘法等价于逐通道相关 response torch.matmul(x_feat.transpose(1, 2), z_feat) # (B, H_x*W_x, H_z*W_z) response response.reshape(B, H_x, W_x, H_z, W_z) response response.mean(dim(3, 4)) # 将模板空间维度平均池化得到 (B, H_x, W_x) return response这个实现是SiamFC原始方式的简化版本每次互相关都会在模板空间维度上做平均池化运算量比较小。如果你想要更精细的响应定位可以改成全互相关后取最大值位置但那样会稍微增加计算量。4.3 相关滤波在线分支我沿用KCF的岭回归频域求解思路特征改成从孪生网络中间层抽取的特征也就是上面backbone的输出这一步替换非常关键能显著提升CF分支在复杂场景下的表现。CF分支维护两个变量滤波器h的频域表示H以及一个用于更新滤波器优化的正则系数。训练阶段把目标区域特征z和理想高斯标签y送入解出最优的H检测阶段把搜索区域特征x变换到频域与H逐元素相乘后再反变换回空间域就得到响应图。class CFBranch: def __init__(self, feature_dim, lambda_reg1e-4): self.lambda_reg lambda_reg self.H None self.feature_dim feature_dim def _cosine_window(self, size): h, w size, size y, x np.meshgrid(np.linspace(-0.5, 0.5, h), np.linspace(-0.5, 0.5, w)) window np.cos(np.pi * x) * np.cos(np.pi * y) return torch.from_numpy(window.astype(np.float32)).unsqueeze(0).unsqueeze(0) def train(self, z_feat, target_position, search_size255, cell_size8): # z_feat: (C, H, W) 模板特征 # target_position: (cy, cx) 目标中心 C, Hf, Wf z_feat.shape if self.H is None: self.H torch.zeros_like(z_feat) # 生成高斯标签 yy, xx np.meshgrid(np.arange(Hf), np.arange(Wf), indexingij) center_y, center_x Hf // 2, Wf // 2 sigma 2.0 label np.exp(-((yy - center_y) ** 2 (xx - center_x) ** 2) / (2 * sigma ** 2)) label torch.from_numpy(label.astype(np.float32)).unsqueeze(0).unsqueeze(0) # (1,1,H,W) z_feat_win z_feat * self._cosine_window(Hf) Z torch.fft.fft2(z_feat_win.unsqueeze(0), dim(-2, -1)) Y torch.fft.fft2(label, dim(-2, -1)) # 频域岭回归求解 numerator Z.conj() * Y denominator Z.conj() * Z self.lambda_reg self.H numerator / denominator return self.H def detect(self, x_feat): # x_feat: (C, H, W) C, Hf, Wf x_feat.shape x_feat_win x_feat * self._cosine_window(Hf) X torch.fft.fft2(x_feat_win.unsqueeze(0), dim(-2, -1)) response torch.fft.ifft2(self.H * X, dim(-2, -1)).real return response.squeeze(0).squeeze(0)这里的余弦窗一定要加。相关滤波的循环移位假设要求信号是周期连续的如果不加窗图像边缘的跳变会造成频域泄漏响应图会出现明显的边缘伪峰。4.4 训练与推理主循环跟踪器的整体推理流程是读取第一帧初始化孪生网络模板特征和相关滤波器之后对每一帧提取搜索区域特征分别得到两个响应图计算置信度并加权融合输出目标位置根据置信度判断是否需要更新模板或CF滤波器。class Tracker: def __init__(self, siamese_net, search_scale4.0): self.net siamese_net self.template_sz 127 self.search_sz 255 self.search_scale search_scale self.cf CFBranch(feature_dim128) self.template_feat None def _crop_patch(self, img, center, size): half size // 2 x1 max(center[1] - half, 0) y1 max(center[0] - half, 0) x2 min(center[1] half, img.shape[1]) y2 min(center[0] half, img.shape[0]) patch img[y1:y2, x1:x2] return cv2.resize(patch, (size, size)) def init(self, img, bbox): cx bbox[0] bbox[2] / 2 cy bbox[1] bbox[3] / 2 template_patch self._crop_patch(img, (cy, cx), self.template_sz) template_tensor torch.from_numpy(template_patch.transpose(2, 0, 1)).unsqueeze(0).float() / 255.0 with torch.no_grad(): self.template_feat self.net(template_tensor)[0] self.cf.train(self.template_feat, target_position(cy, cx)) def track(self, img): # 搜索区域响应 search_patch self._crop_patch(img, (cx, cy), self.search_sz) # cx,cy为上一帧位置 search_tensor torch.from_numpy(search_patch.transpose(2, 0, 1)).unsqueeze(0).float() / 255.0 with torch.no_grad(): search_feat self.net(search_tensor)[0] siam_resp depthwise_cross_correlation(self.template_feat.unsqueeze(0), search_feat.unsqueeze(0)) cf_resp self.cf.detect(search_feat) # 响应图归一化与置信度估计 siam_norm siam_resp / (siam_resp.max() 1e-8) cf_norm cf_resp / (cf_resp.max() 1e-8) siam_cf self._confidence(siam_norm) cf_cf self._confidence(cf_norm) weight_siam siam_cf / (siam_cf cf_cf 1e-8) weight_cf 1 - weight_siam fused weight_siam * siam_norm weight_cf * cf_norm max_loc torch.argmax(fused).item() h, w fused.shape max_y, max_x max_loc // w, max_loc % w # 转换到图像坐标并返回新目标框 return new_bbox, siam_cf, cf_cf每一帧的搜索区域裁剪都以上一帧的目标中心为基准这个细节比想象中重要。如果直接用上一帧的bbox中心目标快速位移时会因为裁剪区域偏移而丢失上下文导致响应图整体失真。跟踪实践中我倾向于在上一帧中心周围做小范围的盲搜多取几个候选位置找到搜索响应最大的一处作为本帧裁剪中心。5. 实验对比OTB-2015上的实测表现与失败案例分析光说不练没有意义。我把这个融合方案在OTB-2015的50个序列上跑了完整实验对比对象是原版SiamFC、使用HOG特征的KCF以及本文的融合方案。评测协议采用标准的一次通过评估OPE报告精确率DP距离20像素内的帧占比、成功率AUC和平均帧率。5.1 评测指标与协议OTB-2015的精度曲线核心就两条中心位置误差小于20像素的比例以及不同重叠阈值下的成功率AUC。跟踪器的参数我只做了很基本的调节没有针对具体序列精调尽量反映方案的真实水平。为了公平所有方法使用的搜索区域大小和模板大小保持一致。5.2 结果对比与效率数据我跑出来的一组数据如下表所示本文融合方案在成功率上比原版SiamFC有明显提升同时保持了接近实时的运行速度。速度没有掉太多因为相关滤波分支的频域求解非常轻量主要额外开销在于每帧多一次特征提取和频域变换。跟踪器精确率 (DP)成功率 (AUC)平均帧率 (FPS)KCF (HOG特征)0.6230.512145SiamFC (原版)0.7360.58658本文融合方案0.8030.66126融合方案在OTB-2015上的优势主要来自两类序列一类是目标外观缓慢变化的长序列相关滤波分支的在线更新让模板始终贴合当前外观另一类是存在相似目标干扰的序列孪生网络的深度特征提供了比HOG强得多的判别力。5.3 量化指标掩盖的问题数字看起来不错但我必须泼一盆冷水融合方案也有自己的短板。我专门挑了几个失败案例来复盘。第一个是目标快速旋转配合尺度剧烈变化的序列融合方案在目标尺度缩小时会把框子稍微撑大一点因为相关滤波分支对尺度变化不敏感融合权重一旦偏向CF分支响应峰就会变胖定位精度跟着下降。第二个是目标短暂完全遮挡又重现的序列相关滤波分支在遮挡期间会持续学习背景信息推出的滤波器被背景污染等到目标重新出现CF分支的响应已经乱成一团。虽然置信度加权能在一定程度上把权重压向孪生网络但孪生网络本身也拿不到正确的目标位置所以还是失败了。这两个案例给我的教训是融合方案解决的是模板老化和特征判别力两件事但无法解决目标信息彻底丢失这件事也不是尺度估计的万能药。后续如果要接着优化应该往多尺度搜索和遮挡检测两个方向走而不是继续堆特征。6. 踩坑记录融合参数、边界效应与内存管理这个项目里踩过的坑不少挑三个最典型的说都是文档里不会写的经验。6.1 融合权重不是拍脑袋定的第一次跑融合实验时我把权重系数写成了固定值0.5比0.5结果在两个序列上直接翻车目标快速运动时CF分支边界效应严重响应图上出现周期性假峰固定权重把模型带飞目标外观突变时孪生网络模板老化响应图峰值钝化固定权重又把模型拽向旧外观。后来改成置信度动态加权效果立刻稳定下来。但置信度公式里的两个系数也不是随便设的PSR的权重可以调大一些保留对峰值锐度的敏感度APCE权重调小一些防止响应图平滑时过度压制。6.2 余弦窗能救命也能误伤相关滤波分支必须加余弦窗但加了余弦窗后响应图的背景部分被强制压低目标快速位移到搜索区域边缘时响应峰值会被窗口压出一个平台导致定位偏差。我的解决办法是在检测阶段根据上一帧目标中心与搜索区域中心的距离动态调整搜索区域的大小。目标中心偏移越大搜索区域就相应放大给目标留出足够的移动空间。6.3 内存占用和批处理的冲突孪生网络的batch size在训练时能开到32但推理阶段如果你同时运行搜索区域特征提取和相关滤波频域求解单帧显存占用会突然蹿升。原因是相关滤波分支需要保留整帧搜索区域的特征图而这一张特征图的分辨率是搜索区域分辨率的一半。实际工程中可以把相关滤波分支转到CPU上做频域运算GPU只留给孪生网络速度差距不大显存压力却小很多。最后分享一个让我对这个方案更有信心的细节我把它接到一段监控视频上连续跑了四十多分钟期间目标多次被行人完全挡住、又反复出现跟踪框始终没有跳飞。对比之前单独用SiamFC时每隔几分钟就要手动重新初始化一次融合方案的稳定性提升非常明显。这个方向后续还可以做很多扩展比如在相关滤波分支引入时序信息、把Transformer的注意力机制替换互相关层或者把融合权重交给一个轻量网络自适应学习都是值得继续往下挖的口子。如果你正在做目标跟踪相关的工作建议先从本文的双通道融合开始把两路响应图的置信度分析做扎实再决定要不要引入更复杂的模块。
返回列表