概要
- KCF (Kernelized Correlation Filters)是一种基于模板的目标跟踪算法. 你可以在KCF项目主页找到论文与开源的代码实现.
- KCF的实现大致有两类 : 一类是基于Matlab的, 一类是基于OpenCV的C或Python实现. 这两类存在的问题是依赖一些基础库及数据类型, 实际工程需要无依赖的C语言实现.
- 由于OpenCV库异常复杂, 这篇文章介绍从Matlab到C的敏捷开发. 它采用了Matlab Coder进行快速开发, 思路适用于但不限于KCF算法.
- 采用了集成测试、单元测试、镜像测试来快速定位Bug.
- 过程自主原创. 暂时不提供开源代码.
开发
Step 1 Matlab Coder
Matlab Coder的目标是完成由M文件到C文件. 这个功能非常强大, 建议采用较新的Matlab版本, 这里采用的是2018a. Coder需要函数的输入定义, 可以通过编写一个调用此函数的函数, 来自动获取函数输入数据类型.
如果有很多个m文件, 建议全部写到一个m文件中,再进行转换.
容易出现的问题:
这种输入im是(width * height *3), 输出im是(width * height)的, C语言无法实现.
可以写成两个变量就好:
1 2 3
| im = zeros(width,height,3); im1 = zeros(width,height); im1 = rgb2gray(im);
|
另外条件定义,也无法实现:
1 2 3
| if(width>100) im = ones(width,height); end
|
这种情况,在开头处先进行初始化即可:
1 2 3 4
| im = zeros(width,height); if(width>100) im = ones(width,height); end
|
其他一些情况,请参照Matlab Coder的错误提示.
Matlab Coder 存在的问题
有些同学可能会想, 用这个Coder一转, 所有工作就完成了. 然后就可以去😄啦~
但是,总有个但是. Matlab Coder转换出来的代码虽正确,但是存在这样几个问题:
- 代码冗长,经常有两大长段代码, 差异很小, 但是它没有提取共同项.
- 代码只实现此次输入会运行到的分支, 其他分支全部删除.
- 代码会有非常长的数组, 与代码交织在一起.
总结来说, 就是可读性差、可改性差. 如果改一组参数作为输入, 代码大量部分需要修改.
即代码与输入的耦合性太高. 代码模块化混乱. 这样的代码面对改变, 是极其脆弱的.
Step 2 模块化
重构是门艺术. 相对来讲, 本项目的复杂度不算太高 ( C语言1000行以内 ), 在重构之前, 我们必须有集成测试.我们不断地改,改完就把代码扔过去进行集成测试, 以确保我们的代码还没有变质.
我们采用VOT数据库中的这组视频序列作为输入,记录序列帧的目标跟踪位置. 将目标位置真值、MATLAB计算结果、C计算结果, 绘制成图对比, 相差不是太多的话, 就意味着C版实现仍然工作着.
对于集成测试, 一定要自动化. 要简化到按一次鼠标, 或敲几次键盘就可后台运行测试. 而测试结果是当前时间为名字的图片.我们用shell脚本完成这个测试.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| rm ../Debug/* rm ../output/res.txt rm test.sh
frame_begin=1; frame_num=700;
matlab -nodisplay -r "gen_line;quit" sh test.sh chmod 777 ../Debug/kcfs ./../Debug/kcfs $frame_begin $frame_num
cd ../auto_test cd /home/z/github_repo/KCF_detail/matlab_implementation matlab -nodisplay -r "run_a($frame_begin,$frame_num);quit"
mv pos.txt /home/z/Desktop/c/myfft/myfft/kcfs/output cd /home/z/Desktop/c/myfft/myfft/kcfs/auto_test matlab -nodisplay -r "add_rect($frame_begin,$frame_num);quit"
|
frame_begin, frame_num是函数的输入参数, 即从第几帧开始与总共多少帧.
第三段调用gen_line脚本, 生成gcc控制参数,之后运行C实现.
第四段调用run_a脚本,运行Matlab实现.
第五段对跟踪结果进行处理, 叠加矩形框进行观察及跟踪结果对比图绘制并保存.
模块化细节
需要一个复数的数据结构,自己实现,不要用很大的系统头文件.
1 2 3 4 5
| typedef struct { double re; double im; }creal_T1;
|
FFT的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| void fft(creal_T1 vector[],int N) { int dig,k,n,m,i,j1,dist,idx,idx1,r; creal_T1 coef,tmp,mul_res;
change(N,vector); dig = 0; k = N; while(k > 1){ dig = dig + 1; k = k / 2; } n = N / 2; for(m=1;m<=dig;m++) { dist = pow(2, (m - 1) ); idx = 1; for(i = 1;i<=n;i++){ idx1 = idx; for(j1 = 1; j1 <= (N / (2 * n)); j1++){ r = (idx - 1) * pow(2, (dig - m)); if(N==256) { coef.re = cos_256[(r % N) / N]; coef.im = -sin_256[(r % N) / N]; } else if(N==128) { coef.re = cos_128[(r % N) / N]; coef.im = -sin_128[(r % N) / N]; } else { coef.re = cos(2 * M_PI * r / N); coef.im = -sin(2 * M_PI * r / N); } coef.re = cos(2 * M_PI * r / N); coef.im = -sin(2 * M_PI * r / N); tmp.re = vector[idx-1].re; tmp.im = vector[idx-1].im; mul(vector[idx + dist-1], coef, &mul_res); add(tmp, mul_res, &vector[idx-1]); sub(tmp, mul_res, &vector[idx + dist-1]); idx = idx + 1; } idx = idx1 + 2 * dist; } n = n / 2; } }
|
这里要求点数必须是2的整数次幂,相当于搜索范围可以弹性调整一下.Matlab的FFT在非2的整数次幂时,进行了DFT运算. 这里和Matlab不一致.为了加快FFT的速度,可以优化成用查表法实现.
FFT2的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| void fft2(creal_T1 vector[],creal_T1 vector1[],int M,int N) { creal_T1 hang[N]; creal_T1 lie[M]; int i,j;
for(j=1;j<=N;j++){ for(i=1;i<=M;i++){ lie[i-1].re = vector[ ((j-1)+ (i-1)*N) ].re; lie[i-1].im = vector[ ((j-1)+ (i-1)*N) ].im; } fft(lie,M); for(i=1;i<=M;i++){ vector[ ((j-1)+ (i-1)*N) ].re = lie[i-1].re; vector[ ((j-1)+ (i-1)*N) ].im = lie[i-1].im; } } for(i=1;i<=M;i++){ for(j=1;j<=N;j++){ hang[j-1].re =vector[ (j-1)+ (i-1)*N ].re; hang[j-1].im =vector[ (j-1)+ (i-1)*N ].im; } fft(hang,N); for(j=1;j<=N;j++){ vector[ (j-1)+ (i-1)*N ].re = hang[j-1].re; vector[ (j-1)+ (i-1)*N ].im = hang[j-1].im; } }
for(i=1;i<=M;i++){ for(j=1;j<=N;j++){ vector1[ (j-1)+ (i-1)*N ].re = vector[ (j-1)+ (i-1)*N ].re; vector1[ (j-1)+ (i-1)*N ].im = vector[ (j-1)+ (i-1)*N ].im; } } }
|
二维FFT就是将图像按每行进行一遍FFT后,在按每列进行一遍FFT.
kcf C实现汇总
文件名称 |
行数 |
描述 |
.c |
main.c |
261 |
主程序 |
circshift.c |
30 |
循环移位 |
conj_f.c |
11 |
取共轭 |
data_input.c |
17 |
数据读入 |
div_f.c |
16 |
复数矩阵除法 |
exp.c |
25 |
矩阵e指数次幂计算 |
mul_f.c |
16 |
矩阵复数乘法 |
myfft.c |
215 |
fft相关 |
ndgrid.c |
18 |
N维网络插值 |
power.c |
9 |
矩阵平方计算 |
.h |
p_def.h |
35 |
参数预定义 |
测试
单元测试
随着代码复杂度的提升,对于bug定位及代码质量保证提出挑战.模块化的代码就是一层一层搭积木的过程,确保每一层可靠,非常关键.
对于C语言而言,传统的单元测试是在C平台构建测试激励,与预想进行比对.考虑到C语言是一种抽象层次较低的语言,在C平台下进行测试费时(代码写的慢)费力(代码写的长).这里才用Matlab平台进行测试,而C语言的代码,通过mex函数,转换成MATLAB可运行的mexw64(64位机器下).
我们以c_exp这一复数矩阵指数次幂计算为例,c语言函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| void c_exp(creal_T1 x[box_p]) { int k; double x_re; double r; for (k = 0; k < box_p; k++) { r = exp(x[k].re / 2.0); x_re = r * (r * cos(x[k].im)); r *= r * sin(x[k].im); x[k].re = x_re; x[k].im = r; } }
|
对应编写的mex函数如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| #include "mex.h" #include "p_def.h" #include <math.h>
void c_exp(creal_T1 x[box_p]) { int k; double x_re; double r; for (k = 0; k < box_p; k++) { r = exp(x[k].re / 2.0); x_re = r * (r * cos(x[k].im)); r *= r * sin(x[k].im); x[k].re = x_re; x[k].im = r; } } void mexFunction(int nlhs,mxArray *plhs[],int nrhs,const mxArray *prhs[]){ creal_T1 *input1; double* input1_re;double* input1_im; double* out_re; double* out_im; int RowA,ColA,i; if(nlhs != 1){ mexErrMsgTxt("One output required."); } else if(nrhs !=1){ mexErrMsgTxt("One input required."); } RowA = mxGetM(prhs[0]); ColA = mxGetN(prhs[0]); input1_re = mxGetPr(prhs[0]); input1_im = mxGetPi(prhs[0]); input1 = malloc(sizeof(creal_T1)*RowA*ColA); for(i=0;i<RowA*ColA;i++) { input1[i].re = input1_re[i]; input1[i].im = input1_im[i]; } plhs[0] = mxCreateDoubleMatrix(RowA,ColA,mxCOMPLEX); out_re = mxGetPr(plhs[0]); out_im = mxGetPi(plhs[0]); c_exp(input1); for(i=0;i<RowA*ColA;i++) { out_re[i] = input1[i].re; out_im[i] = input1[i].im; } free(input1); }
|
通常的mex函数的写法,网上介绍的比较多,这里只重点介绍数据类型为复数的情形.
通过mxGetPr和mxGetPi分别获取输入的实部和虚部.用mxCreateDoubleMatrix创建复数矩阵.
mex函数主要只是要完成C语言与m语言输入输出的转换和绑定,应该说是比较简单的.
在此基础上,我们看一下单元测试的过程:主要由atest与stest两部分组成.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| clc; close all;clear all;
p1 = mfilename('fullpath'); j=findstr(p1,'/'); p1=p1(1:j(end)); cd(p1); addpath('../');
input1 = zeros(2,2); input1 = [1+i,2-i;3+i,4-i]; stest_c_exp(input1,1);
input1 = zeros(256,128); for j=1:256 for k=1:128 input1(j,k) = j*0.1+2i; end end
stest_c_exp(input1,2);
input1 = zeros(1024,1024);
for j=1:1024 for k=1:1024 input1(j,k) = j*0.01+3i; end end
stest_c_exp(input1,3);
|
atest主要完成测试用例的生成,并调用stest进行测试.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| function stest_c_exp(input1,seq)
fid = fopen(['c_exp_' num2str(seq) '.txt'],'r'); A = fscanf(fid,'%f,');
gt0 = A(1); gt1 = A(2); gt2 = A(3); gt3 = A(4); padding = A(5); ifr = A(6); cell_size = A(7); lambda = A(8); sigma = A(9); output_sigma_factor = A(10); im_w = A(11); im_h = A(12); box_w = A(13); box_h = A(14); box_p = A(15);
if(exist('p_def.h')) delete('p_def.h'); end create('p_def.h', A); delete('mex_c_exp.mexa64'); mex('mex_c_exp.c');
output1 = exp(input1); output2 = mex_c_exp(input1);
res = compare(output1,output2);
fprintf('%s',"c_exp's testcase "); fprintf('%d',uint8(seq)); if(res == 1) fprintf('%s\n'," success"); else fprintf('%s\n'," fail"); end
|
stest完成某组参数下的C实现及Matlab实现对比.如果误差足够小,打印success.否则打印 fail.
镜像测试
在进行完所有的单元测试,均通过的前提下,程序还是不正确(集成测试无法通过).那就要靠强大的镜像测试(名字自定义的).所谓镜像测试,就是跑一步m, 跑一步C,然后对比结果.main的每一行都进行对照,直到能够找到问题所在.镜像测试看起来是这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| pos1= [gt1 + floor(gt3/2) gt0 + floor(gt2/2)]; pos2= [gt1 + floor(gt3/2) gt0 + floor(gt2/2)];
output_sigma = sqrt(gt2*gt3) * output_sigma_factor / cell_size;
labels1 = gaussian_shaped_labels(output_sigma, ceil(window_sz / cell_size)); labels2 = mex_gaussian_shaped_labels(gauss); step_i = 1; res = compare(labels1,labels2); if(res == 1) fprintf('%s %d %s\n',"step",step_i,"success"); else fprintf('%s %d %s\n',"step",step_i,"fail"); end step_i = step_i + 1;
yf1 = fft2(labels1); yf2 = mex_fft2_d(labels2,box_w,box_h); res = compare(yf1,yf2); if(res == 1) fprintf('%s %d %s\n',"step",step_i,"success"); else fprintf('%s %d %s\n',"step",step_i,"fail"); end step_i = step_i + 1;
|
后续
- 完成完整的KCF (Hog特征).
- 优化C版KCF的速度.
- 在C6678上进行真实测试.
总结
这篇文章主要介绍了KCF由M到C的开发过程.采用了测试与开发并重的敏捷实践.提出的方法具有普适性:基于Matlab Coder的使用,完成了m设计开发,C运行应用的设想.在开发者的时间越来越宝贵的今天,让开发者不再纠结于C语言的disgusting的细节,而又能享受到C语言的跨平台、高速度、适用于嵌入式的优点,是很有必要的。