该方法出自2016年的一篇ECCV的oral paper,SSD: Single Shot MultiBoxDetector,算是一个革命性的方法了,非常值得学习和研究。

 论文解析


SSD的特殊之处主要体现在以下3点:


(1)多尺度的特征图检测(Multi-scale),如SSD同时使用了上图所示的8_8的特征图和4_4特征图。


(2)相比于YOLO,作者使用的是卷积层来代替了YOLO的全连接层做预测。(如下图所示)


(3)SSD使用了默认的边界框+(1,2/1,3/1,1/2,1/3)6个框来做检测(aspect ratios)


训练过程提出了Smooth L1 loss+softmax loss,将位置定位的准确度值和得分置信度融合起来,从而使得对目标物的检测和识别都表现出state-of-the-art的效果。


整体损失函数公式如下,第一项为置信度的损失,第二项为位置的损失,N为匹配的默认边框的数目,a为平衡因子,交叉验证的时候取值为1。

位置损失的详细公式如下:


置信度损失的公式如下:


该方法包括SSD300和SSD512,2个尺度的训练模型,SSD300的速度更快,SSD512的检测效果更好。相比与其他方法,优势在于SSD的mAP高于YOLO,faster RCNN,速度虽然弱于YOLO,但是完全满足实时应用。不足之处在于对小物体的检测效果不好。VOC2007上的测试效果如下:


作者github提供的是Linux 的版本,对caffe的源码做了很大的改动,加进了很多的层,像NormalizeLayer,PermuteLayer,FlattenLayer,PriorBoxLayer,ConcatLayer,ReshapeLayer,DetectionOutputLayer等。所以linux的童鞋最好直接下载作者的caffe进行编译。这里就不在赘述。linux下的运行效果如下,


训练模型(作者VOC2007&VOC2012数据集):


1,VGGNet下载,


wget http://cs.unc.edu/~wliu/projects/ParseNet/VGG_ILSVRC_16_layers_fc_reduced.caffemodel

2,VOC2007,VOC2012数据集下载,并解压


# Download the data.
cd $HOME/data
wget http://host.robots.ox.ac.uk/pascal/VOC/voc2012/VOCtrainval_11-May-2012.tar
wget http://host.robots.ox.ac.uk/pascal/VOC/voc2007/VOCtrainval_06-Nov-2007.tar
wget http://host.robots.ox.ac.uk/pascal/VOC/voc2007/VOCtest_06-Nov-2007.tar
# Extract the data.
tar -xvf VOCtrainval_11-May-2012.tar
tar -xvf VOCtrainval_06-Nov-2007.tar
tar -xvf VOCtest_06-Nov-2007.tar

3,生成LMDB数据


cd $CAFFE_ROOT
# Create the trainval.txt, test.txt, and test_name_size.txt in data/VOC0712/
./data/VOC0712/create_list.sh
# You can modify the parameters in create_data.sh if needed.
# It will create lmdb files for trainval and test with encoded original image:
# - $HOME/data/VOCdevkit/VOC0712/lmdb/VOC0712_trainval_lmdb
# - $HOME/data/VOCdevkit/VOC0712/lmdb/VOC0712_test_lmdb
# and make soft links at examples/VOC0712/
./data/VOC0712/create_data.sh

这里可能会出现一个错误,AttributeError: ‘module’ object has noattribute ‘LabelMap’ error

解决方法,export PYTHON PATH=$CAFFE_ROOT/python:$PYTHONPATH


4,训练,


# It will create model definition files and save snapshot models in:
# - $CAFFE_ROOT/models/VGGNet/VOC0712/SSD_300x300/
# and job file, log file, and the python script in:
# - $CAFFE_ROOT/jobs/VGGNet/VOC0712/SSD_300x300/
# and save temporary evaluation results in:
# - $HOME/data/VOCdevkit/results/VOC2007/SSD_300x300/
# It should reach 72.* mAP at 60k iterations.
python examples/ssd/ssd_pascal.py

可能的错误,Check failed: error == cudaSuccess (10 vs. 0)  invalid device ordinal,

解决方法,


vim examples/ssd/ssd_pascal.py
gg 285

将,gpus = “0,1,2,3”,改为,gpus = “0”,因为本人只有一块显卡


到此,就可以静静的等待训练结果了。


建议训练配置,内存8G+,显卡8G+,(本人训练内存占用7.8G,显卡占用6.8G),训练过程大概24个小时(单卡titanx),模型效果和作者提供的模型的效果一样。

训练模型(自己的数据集):


(1)在/home/data/VOCdevkit/目录下


mkdir VOCmy
cd VOCmy
mkdir Annotations Imagesets JPEGImages
cd Imagesets/
mkdir Main

其中,


Annotations 为图片对应的xml信息文件,里面主要存放图像中objects的位置类别等信息


Imagesets中存放Layout,Main,Segmentation三个文件夹选项,这里我们主要使用Main 文件夹,里面用于存放train.txt,train_val.txt,val.txt,test.txt


JPEGImages中存放我们自己的图片


(2)将训练用到的图片都copy到JPEGImages目录下


(3)制作每个图片对应的xml文件,这里提供一个可以将txt信息转化为xml信息的程序,


txt中的信息格式如下:

000001.jpg dog48 240 195 371


000001.jpgperson 8 12 352 498


000003.jpg sofa123 155 215 195


000003.jpg chair239 156 307 205


000002.jpg train139 200 207 301


matlab转化程序如下:


%%
%该代码可以做voc2007数据集中的xml文件,
%txt文件每行格式为:000001.jpg dog 48 240 195 371
%即每行由图片名、目标类型、包围框坐标组成,空格隔开
%如果一张图片有多个目标,则格式如下:(比如两个目标)
% 000001.jpg dog 48 240 195 371
% 000001.jpg person 8 12 352 498
% 000002.jpg train 139 200 207 301
% 000003.jpg sofa 123 155 215 195
% 000003.jpg chair 239 156 307 205
%包围框坐标为左上角和右下角
%%
clc;
clear;
%注意修改下面四个变量
imgpath=‘img\’;%图像存放文件夹
txtpath=‘img\output.txt’;%txt文件
xmlpath_new=‘Annotations/‘;%修改后的xml保存文件夹
foldername=‘VOC2007’;%xml的folder字段名


fidin=fopen(txtpath,‘r’);
lastname=‘begin’;

while ~feof(fidin)
tline=fgetl(fidin);
str = regexp(tline, ‘ ‘,‘split’);
filepath=[imgpath,str{1}];
img=imread(filepath);
[h,w,d]=size(img);
imshow(img);
rectangle(‘Position’,[str2double(str{3}),str2double(str{4}),str2double(str{5})-str2double(str{3}),str2double(str{6})-str2double(str{4})],‘LineWidth’,4,‘EdgeColor’,‘r’);
pause(0.1);

if strcmp(str{1},lastname)%如果文件名相等,只需增加object
object_node=Createnode.createElement(‘object’);
Root.appendChild(object_node);
node=Createnode.createElement(‘name’);
node.appendChild(Createnode.createTextNode(sprintf(‘%s’,str{2})));
object_node.appendChild(node);

node=Createnode.createElement(‘pose’);
node.appendChild(Createnode.createTextNode(sprintf(‘%s’,‘Unspecified’)));
object_node.appendChild(node);

node=Createnode.createElement(‘truncated’);
node.appendChild(Createnode.createTextNode(sprintf(‘%s’,‘0’)));
object_node.appendChild(node);

node=Createnode.createElement(‘difficult’);
node.appendChild(Createnode.createTextNode(sprintf(‘%s’,‘0’)));
object_node.appendChild(node);

bndbox_node=Createnode.createElement(‘bndbox’);
object_node.appendChild(bndbox_node);

node=Createnode.createElement(‘xmin’);
node.appendChild(Createnode.createTextNode(sprintf(‘%s’,num2str(str{3}))));
bndbox_node.appendChild(node);

node=Createnode.createElement(‘ymin’);
node.appendChild(Createnode.createTextNode(sprintf(‘%s’,num2str(str{4}))));
bndbox_node.appendChild(node);

node=Createnode.createElement(‘xmax’);
node.appendChild(Createnode.createTextNode(sprintf(‘%s’,num2str(str{5}))));
bndbox_node.appendChild(node);

node=Createnode.createElement(‘ymax’);
node.appendChild(Createnode.createTextNode(sprintf(‘%s’,num2str(str{6}))));
bndbox_node.appendChild(node);
else %如果文件名不等,则需要新建xml
copyfile(filepath, ‘JPEGImages’);
%先保存上一次的xml
if exist(‘Createnode’,‘var’)
tempname=lastname;
tempname=strrep(tempname,‘.jpg’,‘.xml’);
xmlwrite(tempname,Createnode);
end


Createnode=com.mathworks.xml.XMLUtils.createDocument(‘annotation’);
Root=Createnode.getDocumentElement;%根节点
node=Createnode.createElement(‘folder’);
node.appendChild(Createnode.createTextNode(sprintf(‘%s’,foldername)));
Root.appendChild(node);
node=Createnode.createElement(‘filename’);
node.appendChild(Createnode.createTextNode(sprintf(‘%s’,str{1})));
Root.appendChild(node);
source_node=Createnode.createElement(‘source’);
Root.appendChild(source_node);
node=Createnode.createElement(‘database’);
node.appendChild(Createnode.createTextNode(sprintf(‘The VOC2007 Database’)));
source_node.appendChild(node);
node=Createnode.createElement(‘annotation’);
node.appendChild(Createnode.createTextNode(sprintf(‘PASCAL VOC2007’)));
source_node.appendChild(node);

node=Createnode.createElement(‘image’);
node.appendChild(Createnode.createTextNode(sprintf(‘flickr’)));
source_node.appendChild(node);

node=Createnode.createElement(‘flickrid’);
node.appendChild(Createnode.createTextNode(sprintf(‘NULL’)));
source_node.appendChild(node);
owner_node=Createnode.createElement(‘owner’);
Root.appendChild(owner_node);
node=Createnode.createElement(‘flickrid’);
node.appendChild(Createnode.createTextNode(sprintf(‘NULL’)));
owner_node.appendChild(node);

node=Createnode.createElement(‘name’);
node.appendChild(Createnode.createTextNode(sprintf(‘watersink’)));
owner_node.appendChild(node);
size_node=Createnode.createElement(‘size’);
Root.appendChild(size_node);

node=Createnode.createElement(‘width’);
node.appendChild(Createnode.createTextNode(sprintf(‘%s’,num2str(w))));
size_node.appendChild(node);

node=Createnode.createElement(‘height’);
node.appendChild(Createnode.createTextNode(sprintf(‘%s’,num2str(h))));
size_node.appendChild(node);

node=Createnode.createElement(‘depth’);
node.appendChild(Createnode.createTextNode(sprintf(‘%s’,num2str(d))));
size_node.appendChild(node);

node=Createnode.createElement(‘segmented’);
node.appendChild(Createnode.createTextNode(sprintf(‘%s’,‘0’)));
Root.appendChild(node);
object_node=Createnode.createElement(‘object’);
Root.appendChild(object_node);
node=Createnode.createElement(‘name’);
node.appendChild(Createnode.createTextNode(sprintf(‘%s’,str{2})));
object_node.appendChild(node);

node=Createnode.createElement(‘pose’);
node.appendChild(Createnode.createTextNode(sprintf(‘%s’,‘Unspecified’)));
object_node.appendChild(node);

node=Createnode.createElement(‘truncated’);
node.appendChild(Createnode.createTextNode(sprintf(‘%s’,‘0’)));
object_node.appendChild(node);

node=Createnode.createElement(‘difficult’);
node.appendChild(Createnode.createTextNode(sprintf(‘%s’,‘0’)));
object_node.appendChild(node);

bndbox_node=Createnode.createElement(‘bndbox’);
object_node.appendChild(bndbox_node);

node=Createnode.createElement(‘xmin’);
node.appendChild(Createnode.createTextNode(sprintf(‘%s’,num2str(str{3}))));
bndbox_node.appendChild(node);

node=Createnode.createElement(‘ymin’);
node.appendChild(Createnode.createTextNode(sprintf(‘%s’,num2str(str{4}))));
bndbox_node.appendChild(node);

node=Createnode.createElement(‘xmax’);
node.appendChild(Createnode.createTextNode(sprintf(‘%s’,num2str(str{5}))));
bndbox_node.appendChild(node);

node=Createnode.createElement(‘ymax’);
node.appendChild(Createnode.createTextNode(sprintf(‘%s’,num2str(str{6}))));
bndbox_node.appendChild(node);

lastname=str{1};
end
%处理最后一行
if feof(fidin)
tempname=lastname;
tempname=strrep(tempname,‘.jpg’,‘.xml’);
xmlwrite(tempname,Createnode);
end
end
fclose(fidin);

file=dir(pwd);
for i=1:length(file)
if length(file(i).name)>=4 && strcmp(file(i).name(end-3:end),‘.xml’)
fold=fopen(file(i).name,‘r’);
fnew=fopen([xmlpath_new file(i).name],‘w’);
line=1;
while ~feof(fold)
tline=fgetl(fold);
if line==1
line=2;
continue;
end
expression = ‘ ‘;
replace=char(9);
newStr=regexprep(tline,expression,replace);
fprintf(fnew,‘%s\n’,newStr);
end
fprintf(‘已处理%s\n’,file(i).name);
fclose(fold);
fclose(fnew);
delete(file(i).name);
end
end


 


转化前后比对效果如下,左面为原始图像,中间为转化后的,右面为原始VOC的,


 



转化完毕,将其都copy到Annotations目录下


(4)生成Imagesets/Main/,下的train.txt,train_val.txt,val.txt,test.txt,matlab程序如下,


%%
%
该代码根据已生成的xml,制作<span class=”hljs-variable constant_“>VOC2007数据集中的trainval.txt;train.txt;test.txt和val.txt
%trainval占总数据集的50%,test占总数据集的50%;train占trainval的50%,val占trainval的50%;
%上面所占百分比可根据自己的数据集修改,如果数据集比较少,test和val可少一些
%
%注意修改下面四个值
%%
xmlfilepath=’E:\Annotations’;
txtsavepath=’E:\ImageSets\Main\’;
trainval_percent=0.5;%
trainval占整个数据集的百分比,剩下部分就是test所占百分比
train_percent=0.5;%train占trainval的百分比,剩下部分就是val所占百分比

xmlfile=dir(xmlfilepath);
numOfxml=length(xmlfile)-2;%减去.和.. ?总的数据集大小


trainval=sort(randperm(numOfxml,floor(numOfxml_trainval_percent)));
test=sort(setdiff(1:numOfxml,trainval));


trainvalsize=length(trainval);%trainval的大小
train=sort(trainval(randperm(trainvalsize,floor(trainvalsize_train_percent))));
val=sort(setdiff(trainval,train));


ftrainval=fopen([txtsavepath ‘trainval.txt’],‘w’);
ftest=fopen([txtsavepath ‘test.txt’],‘w’);
ftrain=fopen([txtsavepath ‘train.txt’],‘w’);
fval=fopen([txtsavepath ‘val.txt’],‘w’);


for i=1:numOfxml
if ismember(i,trainval)
fprintf(ftrainval,‘%s\n’,xmlfile(i+2).name(1:end-4));
if ismember(i,train)
fprintf(ftrain,‘%s\n’,xmlfile(i+2).name(1:end-4));
else
fprintf(fval,‘%s\n’,xmlfile(i+2).name(1:end-4));
end
else
fprintf(ftest,‘%s\n’,xmlfile(i+2).name(1:end-4));
end
end
fclose(ftrainval);
fclose(ftrain);
fclose(fval);
fclose(ftest);


 


(5)到此所有数据就准备完毕,第(3)(4)的程序下载链接:http://download.csdn.net/detail/qq_14845119/9700102


 


同时建议,上面的操作最好在Linux下完成,如果在windows下完成,还需要涉及一下格式的转化。因为DOS的编辑器和Linux对文末eneter的处理规则不一样,这里送上2个锦囊,帮助众童鞋解除困扰,


去除txt中所有的^M指令: :%s/^M//g#


vim 中替换指令: :%s/sour/dst/g  (将sour替换为dst)


如此这般,create_list.sh就可以生成正确的 trainval.txt,test.txt,test_name_size.txt


然后执行,create_data.sh就可以生成正确的test_lmdb和trainval_lmdb,并在caffe-root/examples下面生成相应的symbolic link,


然后,修改,caffe-root/examples/ssd/pascal.py


57行:训练数据路径


59行:测试数据路径


197-203行:save_dir,snapshot_dir,job_dir,output_result_dir路径


216-220行:name_size_file,label_map_file路径


223行:类别数目(1+类别数)


315行:测试图片数目


上面的这些修改完毕,就可以执行 python examples/ssd/ssd_pascal.py进行训练,小伙伴们又可以静静的等待了。


(6)如果你没有自己的图片,或者不想花时间处理了,可以直接使用VOC里面的图片,下面的程序可以实现解析VOC XML,从中提取出你需要的类别的图片。当然需要xml_io_tools这个matlab版本的解析库,下载地址,http://download.csdn.net/detail/qq_14845119/9711846


 


trainval=<span class=”hljs-title function_ invoke“>importdata(‘/home/data/VOCdevkit/VOC2007/ImageSets/Main/test.txt’,‘0’,6000);
tv=<span class=”hljs-title function_ invoke
“>fopen(‘te.txt’,‘w’);
tval=fopen(‘test.txt’,‘w’);
for i=1:size(trainval,1)
i
xmlpath=strcat(‘/home/data/VOCdevkit/VOC2007/Annotationsori/‘,strcat(trainval{i},‘.xml’));
info=xml_read(xmlpath);
for j=1:<span class=”hljs-title function_ invoke“>size(info.object,1)%下面加上需要的类别,例如,车,人等
if (<span class=”hljs-title function_ invoke
“>strcmp(info.object(j).name,‘car’)==1||…
strcmp(info.object(j).name,‘person’)==1)
fprintf(tv, ‘%s %s %g %g %g %g\n’, info.filename,info.object(j).name,…
info.object(j).bndbox.xmin,info.object(j).bndbox.ymin,info.object(j).bndbox.xmax,info.object(j).bndbox.ymax);
fprintf(tval,‘%s\n’,info.filename(1:length(info.filename)-4));
end
end

end



最后生成te.txt和text.txt,分别为下面左右图,


 


                                 


生成这样的文件,就可以继续按照上面的步骤进行处理了。


 


windows下SSD的一安装指南:


 


安装SSD-caffe步骤:


 


准备资源:


官方SSD-CAFFE:https://github.com/weiliu89/caffe/tree/ssd


官方WINDOWS-CAFFE:https://github.com/BVLC/caffe/tree/windows


boost_1_59_0(regex):http://download.csdn.net/detail/qq_14845119/9693187


 


安装步骤:


1,首先mv一个WINDOWS-CAFFE的F:\caffe-windows\windows下面的CommonSettings.props.example为CommonSettings.props,并对里面进行修改


 


CPU版本配置:



 


 


GPU版本配置:



2,用SSD-CAFFE目录下的src,include,tools,examples替换掉WINDOWS-CAFFE的相应目录,然后进行编译,将会生成15个组件。


 



 


 


这个环节是最重要的一个环节,期间可能出现的问题总结如下:


问题1



 


 


 


解决方法,双击该错误,分别定位到bbox_util.cpp出错的地方,将snprintf改为_snprintf。


 



 


 


问题2



 


解决方法,右键caffe,libcaffe,test_all,配置属性,C/C++,常规,将警告视为错误改为否。



 


问题3



 


解决方法,将下载好的boost里面的libboost_regex-vc120-mt-1_59.lib,libboost_regex-vc120-mt-gd-1_59.lib复制到F:\NugetPackages\boost_chrono-vc120.1.59.0.0\lib\native\address-model-64\lib目录下VS即可检测到。


问题4:


在用SSD-CAFFE替换WINDOWS-CAFFE的过程中,遇到#if defined(_MSC_VER)类似这样的代码要全部保留下来(可以用compare软件修改)。例如,src/caffe/util/db_lmdb.cpp



问题5:



解决方法,双击定位到错误位置,将kBNLL_THRESHOLD改为50即可。



 


问题6


test_lrn_layer的错误,本人使用的是cudnn4版本,估计是一些这问题吧



 


 


解决方法,修改为原版caffe中的样子,



3,SSD测试



 


CPU版本用时(至强E5-2687)



 


GPU版本用时(大将gtx-750Ti)



稳定后为120ms的样子


提供一个热心网友分享的他自己翻译的SSD资料,http://download.csdn.net/detail/qq_14845119/9698597


 


由于本人配置这个windows的ssd也是折腾了4天,因此友情提示,坑多,初学者慎入。由于本人疏忽,未能整理的各种错误,Bug,欢迎下面留言。


夫学者,传道授业解惑也,博客亦然。如果不能真正帮助广大热心学习的小伙伴解决问题,那博客写的再好也没意义,因此,本人将会近期整理完成,上传一个全部配置好的可以直接编译完就运行ssd的windows-caffe,敬请期待后续更新……