CFNet视频目标跟踪核心源码分析——网络结构设计及实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/discoverer100/article/details/81118005

1. 论文信息

2. 网络结构设计及实现

根据官方实际代码,更加详细一点的网络结构如下图所示,可以看出,与SiamFC的网络结构类似,CFNet也包含两个分支——z和x,其中z分支对应目标物体模板,可以理解为目标在第 t 帧之内所有帧的模板数据加权融合(利用学习率来进行,与KCF算法类似),x分支对应目标物体搜索图像,它是目标周围的一大片区域,用于在区域内利用滑动窗口进行搜索,从而确定目标的真正位置。

3. join_cf_window层的实现

论文定义的join_cf_window层,其主要作用是对模板图像进行加窗,抑制边缘部分从而尽量减轻因样本循环移位带来的边缘失真(边界效应),join_cf_window层的定义位于make_net.m源码中:

join = dagnn.DagNN();
% Apply window before correlation filter.
join.addLayer('cf_window', MulConst(), ...
    {'in1'}, {'cf_example'}, {'window'});
p = join.getParamIndex('window');
join.params(p).value = single(make_window(in_sz, join_opts.window));
join.params(p).learningRate = join_opts.window_lr;

其中MulConst是作者自己定义的计算函数,其具体定义位于src/util目录下的MulConst.mmul_const.m文件中。

3.1 join_cf_window层的前向传播

mul_const.m文件中,关于前向传播时,其实现细节为:

y = bsxfun(@times, x, h);
varargout = {y};

其中,x表示join_cf_window层的输入数据,h表示窗,可以看出加窗的过程实质是矩阵元素级乘法,这一点与KCF算法相同。

这里需要注意的是:窗口h的定义位于src/training目录下的make_window.m源码文件中,如下所示:

function h = make_window(sz, type)
% sz = [m1, m2]

switch type
    case ''
        h = ones(sz(1:2));
    case 'cos'
        h = bsxfun(@times, reshape(hann(sz(1)), [sz(1), 1]), ...
                           reshape(hann(sz(2)), [1, sz(2)]));
    otherwise
        error(sprintf('unknown window: ''%s''', type));
end

end

3.2 join_cf_window层的反向传播

mul_const.m文件中,利用join_cf_window层进行反向传播时,其实现细节为:

der_x = bsxfun(@times, der_y, h);
der_h = sum(sum(der_y .* x, 3), 4);
varargout = {der_x, der_h};

这里面有一个细节需要注意:作者在设计该层的方向传播时,用的也是矩阵元素级乘法,与正向传播的计算模式相同,并没有求偏导的过程。个人推测这里面的原因是:join_cf_window层的计算任务是对数据进行加窗,这样的任务对正向和反向传播是对等的,因此反向传播计算方式与正向传播类似。

4. join_cf层的实现

join_cf层是论文中非常关键的一层,文章的核心内容几乎全体现在这一层里面了。该层的定义位于make_net.m源码中:

join.addLayer('cf', ...
    CorrFilter('lambda', join_opts.lambda, 'bias', join_opts.bias), ...
    {'cf_example'}, cf_outputs, {'cf_target'});

其中CorrFilter是作者自己定义的函数,它的具体定义位于src/util目录下的CorrFilter.mcorr_filter.m文件中,两者的关系是:CorrFilter.m调用corr_filter.m。首先分析CorrFilter.m,其关键代码为:

function outputs = forward(obj, inputs, params)
    assert(numel(inputs) == 1, 'one input is needed');
    assert(numel(params) == 1, 'one param is needed');
    args = {'lambda', obj.lambda};
    if obj.bias
        [outputs{1}, outputs{2}] = corr_filter_bias(...
            inputs{1}, params{1}, [], [], args{:});
    else
        outputs{1} = corr_filter(inputs{1}, params{1}, [], args{:});
    end
end

function [derInputs, derParams] = backward(obj, inputs, params, derOutputs)
    assert(numel(inputs) == 1, 'one input is needed');
    assert(numel(params) == 1, 'one param is needed');
    args = {'lambda', obj.lambda};
    if obj.bias
        assert(numel(derOutputs) == 2, 'expect two gradients');
        [derInputs{1}, derParams{1}] = corr_filter_bias(...
            inputs{1}, params{1}, derOutputs{1}, derOutputs{2}, args{:});
    else
        assert(numel(derOutputs) == 1, 'expect one gradient');
        [derInputs{1}, derParams{1}] = corr_filter(...
            inputs{1}, params{1}, derOutputs{1}, args{:});
    end
end

从上述代码可以看出,CorrFilter.m源码文件中定义了join_cf层的前向传播函数(对应tracking过程)和反向传播函数(对应training过程),其中判断分支if obj.bias无需理会,因为代码默认配置的该参数值为false。前向传播和反向传播的详细实现均位于src/util目录下的corr_filter.m文件中,现将该源代码完整贴出:

function varargout = corr_filter(x, y, der_w, varargin)

opts.lambda = nan;
opts = vl_argparse(opts, varargin);

% x is [m1, m2, p, b]
% y is [m1, m2]
% der_w is same size as x

sz = size_min_ndims(x, 4);
n = prod(sz(1:2));

y_f = fft2(y);
x_f = fft2(x);
% k = 1/n sum_i x_i corr x_i + lambda delta
assert(~isnan(opts.lambda), 'lambda must be specified');
k_f = 1/n*sum(conj(x_f).*x_f, 3) + opts.lambda;
% a must satisfy n (k conv a) = y
% The signal a contains a weight per example (shift)
a_f = 1/n*bsxfun(@times, y_f, 1./k_f);

if isempty(der_w)
    % Use same weight a for all channels i.
    % w[i] = a corr x[i]
    w_f = bsxfun(@times, conj(a_f), x_f);
    % w = ifft2(w_f, 'symmetric');
    w = real(ifft2(w_f));
    varargout = {w};
else
    der_w_f = fft2(der_w);
    % a, x -> w
    % w[i] = a corr x[i]
    % dw[i] = da corr x[i] + a corr dx[i]
    % F dw[i] = conj(F da) .* F x[i] + conj(F a) .* F dx[i]
    % <der_w, dw> = sum_i <der_w[i], dw[i]> = sum_i <F der_w[i], F dw[i]>
    %   = sum_i <F der_w[i], conj(F da) .* F x[i] + conj(F a) .* F dx[i]>
    %   = <F da, sum_i conj(F der_w[i]) .* F x[i]> + sum_i <F der_w[i] .* F a, F dx[i]>
    der_a_f = sum(x_f .* conj(der_w_f), 3);
    der_x_f = bsxfun(@times, a_f, der_w_f);
    % k, y -> a
    % k conv a = 1/n y
    % dk conv a + k conv da = 1/n dy
    % dk_f .* a_f + k_f .* da_f = 1/n dy_f
    % <der_a, da> = <der_a_f, da_f>
    %   = <der_a_f, k_f^-1 .* (1/n dy_f - dk_f .* a_f)>
    %   = <1/n der_a_f .* conj(k_f^-1), dy_f> + <-der_a_f .* conj(k_f^-1 .* a_f), dk_f>
    %   = <der_y_f, dy_f> + <der_k_f, dk_f>
    der_y_f = 1/n*sum(der_a_f .* conj(1 ./ k_f), 4); % accumulate gradients over batch
    der_y = real(ifft2(der_y_f));
    der_k_f = -der_a_f .* conj(a_f ./ k_f);
    % x -> k
    % k = 1/n sum_i x_i corr x_i + lambda delta
    % dk = 1/n sum_i {dx[i] corr x[i] + x[i] corr dx[i]}
    % F dk = 1/n sum_i {conj(F dx[i]) .* F x[i] + conj(F x[i]) .* F dx[i]}
    % <der_k, dk> = <der_k, 1/n sum_i {dx[i] corr x[i] + x[i] corr dx[i]}>
    %   = sum_i <F der_k, 1/n conj(F dx[i]) .* F x[i] + conj(F x[i]) .* F dx[i]>
    %   = sum_i <F dx[i], 1/n conj(F der_k) .* F x[i]> + <1/n F der_k .* F x[i], F dx[i]>
    %   = sum_i <F dx[i], 1/n [F der_k + conj(F der_k)] .* F x[i]>
    %   = sum_i <F dx[i], 2/n real(F der_k) .* F x[i]>
    %   = sum_i <F der_x[i], F dx[i]>
    der_x_f = der_x_f + 2/n*bsxfun(@times, real(der_k_f), x_f);
    % der_x = ifft2(der_x_f, 'symmetric');
    der_x = real(ifft2(der_x_f));
    varargout = {der_x, der_y};
end

end

 

4.1 join_cf层的前向传播

上述代码即为论文join_cf层的核心实现部分,函数内部通过isempty(der w)判断来分别实现正向传播和反向传播过程,首先分析正向传播过程,其对应于is empty(der w)=true的分支中,在该if判断之前及其内部,下面四行代码最为关键:

k_f = 1/n*sum(conj(x_f).*x_f, 3) + opts.lambda;
a_f = 1/n*bsxfun(@times, y_f, 1./k_f);
w_f = bsxfun(@times, conj(a_f), x_f);
w = real(ifft2(w_f));
varargout = {w};

 上述前三行代码对应论文原文中的公式7(a)-7(c),如下所示:

\left\{ \begin{array}{l} \hat k = \frac{1}{n}{{\hat x}^*} \circ \hat x + \lambda I\;\;\;\;\;\;\;\;\;{\rm{7(a)}}\\ \hat \alpha = \frac{1}{n}{{\hat k}^{ - 1}} \circ \hat y\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;7(b)\\ \hat w = {{\hat \alpha }^*} \circ \hat x\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;7(c) \end{array} \right.

其中x表示目标物体模板(在代码实现中它是一个融合了多帧图像的移动平均值,因此叫做模板),\circ符号表示矩阵元素级乘法,\lambda表示正则项系数,I表示单位矩阵,k表示目标物体自相关,y表示期望的响应,\alpha表示期望响应与自相关的关联,w是join_cf层最终的输出,它表示求解得到的滤波器

4.2 join_cf层的反向传播

现在分析join_cf层的反向传播。由于论文将相关滤波作为一个layer嵌入到网络中并且还需要进行end-to-end训练,因此有必要为join_cf层设计反向传播函数。同样地,在corr_filter.m文件中,反向传播最为关键的核心代码如下所示:

der_a_f = sum(x_f .* conj(der_w_f), 3);
der_y_f = 1/n*sum(der_a_f .* conj(1 ./ k_f), 4); % accumulate gradients over batch
der_k_f = -der_a_f .* conj(a_f ./ k_f);
der_x_f = der_x_f + 2/n*bsxfun(@times, real(der_k_f), x_f);
varargout = {der_x, der_y};

上述四行代码对应论文原文中的公式10,如下所示:

\left\{ \begin{array}{l} \widehat {{\nabla _\alpha }\ell } = \hat x \circ {\left( {\widehat {{\nabla _w}\ell }} \right)^ * }\\ \widehat {{\nabla _y}\ell } = \frac{1}{n}{{\hat k}^{ - * }} \circ \widehat {{\nabla _\alpha }\ell }\\ \widehat {{\nabla _k}\ell } = - {{\hat k}^{ - * }} \circ {{\hat \alpha }^ * } \circ \widehat {{\nabla _\alpha }\ell }\\ \widehat {{\nabla _x}\ell } = \hat \alpha \circ \widehat {{\nabla _w}\ell } + \frac{2}{n}\hat x \circ {\mathop{\rm Re}\nolimits} \left\{ {\widehat {{\nabla _k}\ell }} \right\} \end{array} \right.

反向传播的最终输出,是变量der_x和der_y。

关于join_cf层的正向传播和反向传播的详细推导过程,可以参考本人的另一篇博客文章:CFNet视频目标跟踪推导笔记

5. join_crop_z层的实现

join_crop_z层的主要作用是对滤波器进行裁切。个人推测作者设计这一层的原因:只有进行了裁切,保留滤波器的中央核心区域,才能进行Siamese网络最终的匹配模式进行跟踪。

join_crop_z层的最初定义位于make_net.m源码中:

join.addLayer('crop_z', ...
               CropMargin('margin', 16), ...
               cf_outputs, xcorr_inputs{1});

 其中CropMargin是作者自己定义的函数,其内部包含了正向传播函数和反向传播函数。

5.1 join_crop_z层的前向传播

首先看正向传播代码,位于src/util/CropMargin.m源码文件中,如下所示:

function outputs = forward(obj, inputs, params)
    assert(numel(inputs) == 1);
    assert(numel(params) == 0);
    x = inputs{1};
    sz = size_min_ndims(x, 4);
    p = obj.margin;
    y = x(1+p:end-p, 1+p:end-p, :, :);
    outputs = {y};
end

从上述代码可以看出,变量p即为需要裁掉的边缘大小,起裁切作用是语句y = x(1+p:end-p, 1+p:end-p, :, :);,由于网络中的数据是四维的,裁切只针对第一维和第二维,也就是平面视觉部分,因此后面两个参数都没有进行配置。

5.2 join_crop_z层的反向传播

join_crop_z层的反向传播代码如下所示:

function [derInputs, derParams] = backward(obj, inputs, params, derOutputs)
    assert(numel(inputs) == 1);
    assert(numel(params) == 0);
    assert(numel(derOutputs) == 1);
    x = inputs{1};
    dldy = derOutputs{1};
    if isa(x, 'gpuArray')
        dldx = gpuArray(zeros(size(x), classUnderlying(x)));
    else
        dldx = zeros(size(x), class(x));
    end
    
    p = obj.margin;
    dldx(1+p:end-p, 1+p:end-p, :, :) = dldy;
    derInputs = {dldx};
    derParams = {};
end

从上述代码可以看出,在进行反向传播时,其最终要求得的变量derInputs相当于对变量dldy的周围进行0元素填充,因此该处的反向传播代码也不涉及求偏导。

6. join_xcorr层的实现

join_xcorr层的主要作用是通过滑动窗口方式进行匹配,并形成response map用于最后的目标判别,它的最初定义位于src/training目录下的make_net.m源码文件的make_join_corr_filt函数中,如下所示:

join.addLayer('xcorr', XCorr('bias', join_opts.bias), ...
               xcorr_inputs, {'out'});

其中XCorr是作者自己定义的函数,它位于src/util/XCorr.m源码文件中,下面将分析该层的正向传播和反向传播实现。

6.1 join_xcorr层的前向传播

src/util/XCorr.m源码文件中,join_xcorr层的前向传播代码如下所示:

function outputs = forward(obj, inputs, params)
    if obj.bias
        assert(numel(inputs) == 3, 'three inputs are needed');
    else
        assert(numel(inputs) == 2, 'two inputs are needed');
    end

    if obj.bias
        outputs{1} = cross_corr(inputs{1:3});
    else
        outputs{1} = cross_corr(inputs{1:2}, []);
    end
end

由于在CFNet源码中,参数obj.bias的值默认被赋为false,因此只需要关注代码outputs{1} = cross_corr(inputs{1:2}, []);即可。在这一行代码中,调用了cross_corr(z, x, c, der_y)函数,调用时传递的参数是inputs{1:2},它们的含义如下:

  • inputs{1}:join_tmpl_cropped,对应分支z的输出(目标模板)
  • inputs{2}:br2_out,对应分支x的输出(搜索图像)

该函数位于src/util/cross_corr.m源码文件中,源码内包含了前向传播和反向传播的具体实现逻辑,其中前向传播部分如下所示:

z_sz = size_min_ndims(z, 4);
x_sz = size_min_ndims(x, 4);
r_sz = [x_sz(1:2) - z_sz(1:2) + 1, 1, x_sz(4)];
x_ = reshape(x, [x_sz(1:2), prod(x_sz(3:4)), 1]);

r_ = vl_nnconv(x_, z, []);
assert(isequal(size_min_ndims(r_, 4), [r_sz(1:2), r_sz(4), 1]));
r = reshape(r_, r_sz);

y = r;

varargout = {y};

从上述源码可以看出,滑动窗口卷积的核心代码是r_ = vl_nnconv(x_, z, []);作者调用matconvnet里面的函数实现了这一过程。

6.2 join_xcorr层的反向传播

src/util/XCorr.m源码文件中,join_xcorr层的反向传播代码如下所示:

function [derInputs, derParams] = backward(obj, inputs, params, derOutputs)
    if obj.bias
        assert(numel(inputs) == 3, 'three inputs are needed');
    else
        assert(numel(inputs) == 2, 'two inputs are needed');
    end
    assert(numel(derOutputs) == 1, 'only one gradient should be flowing in this layer (dldy)');

    if obj.bias
        [derInputs{1:3}] = cross_corr(inputs{1:3}, derOutputs{1});
    else
        [derInputs{1:2}] = cross_corr(inputs{1:2}, [], derOutputs{1});
    end
    derParams = {};
end

前文已经提及,参数obj.bias的值默认被赋为false,因此在进行反向传播时重点关注代码[derInputs{1:2}] = cross_corr(inputs{1:2}, [], derOutputs{1});即可,其传入的参数中inputs{1}对应分支z的输出(目标模板),inputs{2}对应分支x的输出(搜索图像),函数cross_corr位于src/util/cross_corr.m源码文件中,其中反向传播部分如下所示:

z_sz = size_min_ndims(z, 4);
x_sz = size_min_ndims(x, 4);

r_sz = [x_sz(1:2) - z_sz(1:2) + 1, 1, x_sz(4)];
x_ = reshape(x, [x_sz(1:2), prod(x_sz(3:4)), 1]);

der_r = der_y;

der_r_ = reshape(der_r, [r_sz(1:2), r_sz(4), 1]);
[der_x_, der_z] = vl_nnconv(x_, z, [], der_r_);
der_x = reshape(der_x_, x_sz);

varargout = {der_z, der_x};

从上述源码可以看出,作者通过调用vl_nnconv函数来实现join_xcorr层中变量z和x的偏导数计算。

7. fin_adjust层的实现

fin_adjust层的主要作用是对join_xcorr层计算出来的response map矩阵进行校准,形成更加规范的response map,其实现主要通过MatConvNet来进行,其代码如下所示:

final.layers = {};

final.layers{end+1} = struct(...
    'type', 'conv', 'name', 'adjust', ...
    'weights', {{single(1), single(-0.5)}}, ...
    'learningRate', [1, 2], ...
    'weightDecay', [0 0], ...
    'opts', {convOpts});          

8. 总结

CFNet论文在SiamFC跟踪算法的基础上,将相关滤波引入,形成一个独立的网络层参与end-to-end的训练,在网络结构设计上具有自己的特色,了解这些设计思想和方法,对我们的学习和工作具有较强的实用价值。


更多内容,欢迎扫码关注“视觉边疆”微信订阅号

猜你喜欢

转载自blog.csdn.net/discoverer100/article/details/81118005