CFNet視頻目標跟蹤核心源碼分析——網絡結構設計及實現

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的訓練,在網絡結構設計上具有自己的特色,瞭解這些設計思想和方法,對我們的學習和工作具有較強的實用價值。


更多內容,歡迎掃碼關注“視覺邊疆”微信訂閱號

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章