FRCN文本检测(转)

[源码分析]Text-Detection-with-FRCN

原创 2017年11月21日 17:58:39
        <ul class="article_tags clearfix csdn-tracking-statistics tracking-click" data-mod="popu_377" style="display: none;">
            <li class="tit">标签:</li>
            <!--          [endarticletags]-->
        </ul>
        <ul class="right_bar">
            <li><button class="btn-noborder"><i class="icon iconfont icon-read"></i><span class="txt">659</span></button></li>
            <li class="edit" style="display: none;">
                <a class="btn-noborder" href="https://mp.csdn.net/postedit/78580970">
                    <i class="icon iconfont icon-bianji"></i><span class="txt">编辑</span>
                </a>
            </li>
            <li class="del" style="display: none;">
                <a class="btn-noborder" onclick="javascript:deleteArticle(fileName);return false;">
                    <i class="icon iconfont icon-shanchu"></i><span class="txt">删除</span>
                </a>
            </li>
        </ul>
    </div>
    <div id="article_content" class="article_content csdn-tracking-statistics tracking-click" data-mod="popu_519" data-dsm="post" style="overflow: hidden;">
                        <div class="htmledit_views">
                    <p><span style="white-space:pre;"></span><span style="white-space:pre;"></span><span style="white-space:pre;"></span><span style="white-space:pre;"></span><span style="font-size:12px;"><a href="https://github.com/jugg1024/Text-Detection-with-FRCN" target="_blank">Text-Detection-with-FRCN</a>项目是基于<a href="https://github.com/rbgirshick/py-faster-rcnn" target="_blank">py-faster-rcnn</a>项目在场景文字识别领域的扩展。对Text-Detection-with-FRCN的理解过程,本质上是对py-faster-rcnn的理解过程。我个人认为,初学者,尤其是对caffe还不熟悉的时候,在理解整个项目的过程中,会有以下困惑:</span></p><p><span style="font-size:12px;">1.程序入口</span></p><p><span style="font-size:12px;">2.数据是如何准备的?</span></p><p><span style="font-size:12px;">3.整个网络是如何构建的?</span></p><p><span style="font-size:12px;">4.整个网络是如何训练的?</span></p><p><span style="font-size:12px;"><span style="white-space:pre;"></span>那么,接下来,以我的理解,结合论文和源代码,一步步进行浅析。</span></p><p><br></p><p><span style="font-size:24px;">一.程序入口</span></p><p><span style="font-size:12px;">训练阶段:</span></p><p><span style="font-size:18px;">入口一</span>:<span style="font-size:12px;">/py-faster-rcnn/experiments/scripts/faster_rcnn_end2end.sh</span></p><p><span style="font-size:12px;">-- &gt;</span></p><p><br></p><p><span style="font-size:18px;">入口二</span>: <span style="font-size:12px;">/py-faster-rcnn/tools/train_net.py</span></p><p><span style="font-size:12px;">在train_net中:</span></p><p><span style="font-size:12px;">1.定义数据格式,获得imdb,roidb;</span></p><p><span style="font-size:12px;">2.开始训练网络。</span></p><p></p><div class="dp-highlighter bg_python"><div class="bar"><div class="tools"><b>[python]</b> <a href="#" class="ViewSource" title="view plain" onclick="dp.sh.Toolbar.Command('ViewSource',this);return false;" target="_self">view plain</a><span class="tracking-ad" data-mod="popu_168"> <a href="#" class="CopyToClipboard" title="copy" onclick="dp.sh.Toolbar.Command('CopyToClipboard',this);return false;" target="_self">copy</a><div style="position: absolute; left: 245px; top: 923px; width: 16px; height: 16px; z-index: 99;"><embed id="ZeroClipboardMovie_1" src="https://csdnimg.cn/public/highlighter/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_1" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=1&amp;width=16&amp;height=16" wmode="transparent"></div></span><span class="tracking-ad" data-mod="popu_169"> <a href="#" class="PrintSource" title="print" onclick="dp.sh.Toolbar.Command('PrintSource',this);return false;" target="_self">print</a></span><a href="#" class="About" title="?" onclick="dp.sh.Toolbar.Command('About',this);return false;" target="_self">?</a></div></div><ol start="1" class="dp-py"><li class="alt"><span><span>train_net(args.solver,&nbsp;roidb,&nbsp;output_dir,&nbsp;pretrained_model,&nbsp;max_iters)&nbsp;&nbsp;</span></span></li></ol></div><pre class="python" name="code" style="display: none;">train_net(args.solver, roidb, output_dir, pretrained_model, max_iters)</pre><p></p><p><span style="font-size:12px;">train_net定义在/py-faster-rcnn/lib/fast_rcnn/train.py中</span></p><p><span style="font-size:12px;">--&gt;</span></p><p><br></p><p><span style="font-size:18px;">入口三</span>:<span style="font-size:12px;">/py-faster-rcnn/lib/fast_rcnn/train.py</span></p><p><span style="font-size:12px;">在train_net函数中:</span></p><p></p><div class="dp-highlighter bg_python"><div class="bar"><div class="tools"><b>[python]</b> <a href="#" class="ViewSource" title="view plain" onclick="dp.sh.Toolbar.Command('ViewSource',this);return false;" target="_self">view plain</a><span class="tracking-ad" data-mod="popu_168"> <a href="#" class="CopyToClipboard" title="copy" onclick="dp.sh.Toolbar.Command('CopyToClipboard',this);return false;" target="_self">copy</a><div style="position: absolute; left: 245px; top: 1207px; width: 16px; height: 16px; z-index: 99;"><embed id="ZeroClipboardMovie_2" src="https://csdnimg.cn/public/highlighter/ZeroClipboard.swf" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="16" height="16" name="ZeroClipboardMovie_2" align="middle" allowscriptaccess="always" allowfullscreen="false" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="id=2&amp;width=16&amp;height=16" wmode="transparent"></div></span><span class="tracking-ad" data-mod="popu_169"> <a href="#" class="PrintSource" title="print" onclick="dp.sh.Toolbar.Command('PrintSource',this);return false;" target="_self">print</a></span><a href="#" class="About" title="?" onclick="dp.sh.Toolbar.Command('About',this);return false;" target="_self">?</a></div></div><ol start="1" class="dp-py"><li class="alt"><span><span>roidb&nbsp;=&nbsp;filter_roidb(roidb)&nbsp;&nbsp;</span></span></li><li class=""><span>sw&nbsp;=&nbsp;SolverWrapper(solver_prototxt,&nbsp;roidb,&nbsp;output_dir,&nbsp;pretrained_model=pretrained_model)&nbsp;&nbsp;</span></li><li class="alt"><span>model_paths&nbsp;=&nbsp;sw.train_model(max_iters)&nbsp;&nbsp;</span></li><li class=""><span><span class="keyword">return</span><span>&nbsp;model_paths&nbsp;&nbsp;</span></span></li></ol></div><pre class="python" name="code" style="display: none;">roidb = filter_roidb(roidb)

sw = SolverWrapper(solver_prototxt, roidb, output_dir, pretrained_model=pretrained_model)
model_paths = sw.train_model(max_iters)
return model_paths

这样,就开始对整个网络进行训练了。

在solver_prototxt中,定义了train_prototxt。在train_prototxt中,定义了各种层,这些层组合起来,形成了训练网络的结构。

-->


入口四/py-faster-rcnn/models/coco_text/VGG16/faster_rcnn_end2end/train.prototxt

先举例说明形式:

1.自定义Caffe Python layer

  1. layer {  
  2.   name: 'input-data'  
  3.   type: 'Python'  
  4.   top: 'data'  
  5.   top: 'im_info'  
  6.   top: 'gt_boxes'  
  7.   python_param {  
  8.     module: 'roi_data_layer.layer'  
  9.     layer: 'RoIDataLayer'  
  10.     param_str: "'num_classes': 2"  
  11.   }  
  12. }  
在自定义的caffe python layer中:

type为’python';

python_param中:

module为模块名,通常也是文件名。module: 'roi_data_layer.layer':说明这一层定义在roi_data文件夹下面的layer中

layer为模块里的类名。layer:'RoIDataLayer':说明该类的名字为'RoIDataLayer'

param_str为传入该层的参数。


2.caffe中原有的定义好的层,一般用c++定义。

  1. layer {  
  2.   name: "conv1_1"  
  3.   type: "Convolution"  
  4.   bottom: "data"  
  5.   top: "conv1_1"  
  6.   param {  
  7.     lr_mult: 0  
  8.     decay_mult: 0  
  9.   }  
  10.   param {  
  11.     lr_mult: 0  
  12.     decay_mult: 0  
  13.   }  
  14.   convolution_param {  
  15.     num_output: 64  
  16.     pad: 1  
  17.     kernel_size: 3  
  18.   }  
  19. }  

在目录:/py-faster-rcnn/caffe-fast-rcnn/include/caffe/layers文件夹下面,可以看到conv_layer.hpp的头文件定义。

了解了layer的表示方法,接下来,看一下,整个网络是如何构建的。
整个网络可以分为四个部分:
1.Conv layers。首先使用一组基础的conv+relu+pooling层提取image的feature maps。该feature maps被共享用于后续RPN层和全连接层。
2.Region Propoasl Networks。RPN网络用于生成region proposals。该层通过softmax判断anchors属于foreground或者background,再利用bounding box regression修正anchors来获得精确的proposals。
3.RoI Pooling。该层收集输入的feature maps和proposals,送入后续全连接层判定目标类别。
4.Classification。利用proposal feature maps计算proposal的类别,同时再次利用bounding box regression获得检测框最终的精确位置。

介绍到这里,相信大家对于整个程序的运行流程有了初步的了解。接下来,来看看具体的实现细节。首先,从数据的准备入手。

二.数据是如何准备的?

入口一/py-faster-rcnn/tools/train_net.py

在train_net中:

获得imdb,roidb:imdb, roidb = combined_roidb(args.imdb_name)

进入位于 /py-faster-rcnn/tools/train_net.py,combined_roidb中:
  1. def combined_roidb(imdb_names):  
  2.     def get_roidb(imdb_name):  
  3.         imdb = get_imdb(imdb_name)  
  4.         print 'Loaded dataset {:s} for training'.format(imdb.name)  
  5.         imdb.set_proposal_method(cfg.TRAIN.PROPOSAL_METHOD)  
  6.         print 'Set proposal method: {😒}'.format(cfg.TRAIN.PROPOSAL_METHOD)  
  7.         roidb = get_training_roidb(imdb)  
  8.         return roidb  
  9.   
  10.     roidbs = [get_roidb(s) for s in imdb_names.split('+')]  
  11.     roidb = roidbs[0]  
  12.     if len(roidbs) > 1:  
  13.         for r in roidbs[1:]:  
  14.             roidb.extend(r)  
  15.         imdb = datasets.imdb.imdb(imdb_names)  
  16.     else:  
  17.         imdb = get_imdb(imdb_names)  
  18.     return imdb, roidb  

看下面这段代码:
  1. # Set up voc_<year><split> using selective search "fast" mode  
  2. for year in ['2007''2012']:  
  3.     for split in ['train''val''trainval''test']:  
  4.         name = 'voc{}{}'.format(year, split)  
  5.         _sets[name] = (lambda split=split, year=year: pascal_voc(split, year))  
所以,这里实际执行的是pascal_voc函数。

进入位于 /py-faster-rcnn/lib/datasets/pascal_voc.py。可以看到,pascal_voc是一个类,这里是调用了该类的构造函数,返回的也是该类的一个实例,所以,imdb实际上就是pascal_voc类的一个实例。
那么,来看这个类的构造函数是如何实现的,以及输入的图片数据在里面是如何组织的。
该类的构造函数如下:设置了imdb的一些属性,比如图片的路径,图片名称的索引,没有放入真实的图片数据。

  1. class pascal_voc(imdb):  
  2.     def init(self, image_set, year, devkit_path=None):  
  3.         imdb.init(self'voc' + year + '' + image_set)  
  4.         self._year = year  
  5.         self._image_set = image_set  
  6.         # self._devkit_path = self._get_default_path() if devkit_path is None </span>  
  7.         #                    else devkit_path  
  8.         self._devkit_path = os.path.join(cfg.ROOT_DIR, '..''datasets''train_data')  
  9.         self._data_path = os.path.join(self._devkit_path, 'formatted_dataset')  
  10.         self._classes = ('background'# always index 0  
  11.                          'text')   
  12.         self._class_to_ind = dict(zip(self.classes, xrange(self.num_classes)))  
  13.         self._image_ext = '.jpg'  
  14.         self._image_index = self._load_image_set_index()  
  15.         # Default to roidb handler  
  16.         self._roidb_handler = self.selective_search_roidb  
  17.         self._salt = str(uuid.uuid4())  
  18.         self._comp_id = 'comp4'  
  19.   
  20.         # PASCAL specific config options  
  21.         self.config = {'cleanup'     : True,  
  22.                        'use_salt'    : True,  
  23.                        'use_diff'    : False,  
  24.                        'matlab_eval' : False,  
  25.                        'rpn_file'    : None,  
  26.                        'min_size'    : 2}  
  27.   
  28.         assert os.path.exists(self._devkit_path), &nbsp; 
  29.                 'VOCdevkit path does not exist: {}'.format(self._devkit_path)  
  30.         assert os.path.exists(self.data_path), &nbsp; 
  31.                 'Path does not exist: {}'.format(self.data_path)  

也就是,对于一副任意大小的P×Q图像(假设P为短边,Q为长边),首先reshape到M×N,其中M由cfg.TRAIN.SCALES决定,N由cfg.TRAIN.MAX_SIZE决定。blob中的data为reshape后的图像。im_info=[M,N,scale_factor]则保存了此次缩放的所有信息。

至此,介绍了数据是如何准备和进入网络的。


三.整个网络是如何构建的?

网络主要分为四个部分:
1.Conv layers
2.Region Proposal Networks
3.RoI Pooling
4.Classfication

1.Conv layers
Conv layers包含了conv, pooling, relu三种层。
在Conv layers中:
1)所有的conv层都是:kernel_size = 3, pad = 1, stride = 1
2)所有的pooling层都是:kernel_size = 2, pad = 0, stride = 2

在Faster-rcnn中所有的conv layers中,其对所有的卷积都做了扩边处理(pad=1),导致原图变为(M+2)(N+2)大小,再做3×3卷积后输出M×N。正是这种设置,导致Conv layers中的conv层不改变输入和输出矩阵大小
类似,在所有的pooling层中,kernel_size=2, stride=2, 这样每个经过pooling层的M×N矩阵,都会变成(M/2)×(N/2)大小。综上所述,在整个Conv layers中,conv和relu层不改变输入输出的大小,只有pooling层使得输出的长宽都变为输入的1/2。
因此,一个M×N大小的矩阵经过Conv layers固定变为(M/16)×(N/16)。这样Conv layers生成的feature map都可以和原图对应起来。
注意:以下如无特殊声明,说的原图都是指的reshape后的M×N大小的图像。


2.Region Proposal Networks

1)使用nn的滑动窗口在最后一个共享卷积层上提取信息

论文原文:“To generate region proposals, we slide a small network over the convolutional feature map output by the last shared convolutional layer. This small network takes as input an n × n spatial windowthe input convolutional feature map. ”
也就是使用nn的滑动窗口在最后一个共享卷积层上提取信息。论文后面提到n=3。
这一部分的实现,对应rpn_conv/3×3卷积。

在train.prototxt中的定义为:
  1. layer {  
  2.   name: "rpn_conv/3x3"  
  3.   type: "Convolution"  
  4.   bottom: "conv5_3"  
  5.   top: "rpn/output"  
  6.   param { lr_mult: 1.0 }  
  7.   param { lr_mult: 2.0 }  
  8.   convolution_param {  
  9.     num_output: 512  
  10.     kernel_size: 3 pad: 1 stride: 1  
  11.     weight_filler { type: "gaussian" std: 0.01 }  
  12.     bias_filler { type: "constant" value: 0 }  
  13.   }  
  14. }  
2)得到box-classification层和box-regression层

论文原文:“This feature is fed into two sibling fully-connected layers—a box-regression layer (reg) and box-classification layer (cls). ”
“This architecture is naturally implemented with an n × n convolutional layer followed by two sibling 1 × 1 convolutional layers (for reg and cls, respectively).”

在前面得到的rpn_conv层的基础上,分别通过1×1卷积,得到两个层,box-regression层box-classification层。

在train.prototxt中的定义为:
  1. layer {  
  2.   name: "rpn_cls_score"  
  3.   type: "Convolution"  
  4.   bottom: "rpn/output"  
  5.   top: "rpn_cls_score"  
  6.   param { lr_mult: 1.0 }  
  7.   param { lr_mult: 2.0 }  
  8.   convolution_param {  
  9.     num_output: 18   # 2(bg/fg)  9(anchors)  
  10.     kernel_size: 1 pad: 0 stride: 1  
  11.     weight_filler { type: "gaussian" std: 0.01 }  
  12.     bias_filler { type: "constant" value: 0 }  
  13.   }  
  14. }  
  15.   
  16. layer {  
  17.   name: "rpn_bbox_pred"  
  18.   type: "Convolution"  
  19.   bottom: "rpn/output"  
  20.   top: "rpn_bbox_pred"  
  21.   param { lr_mult: 1.0 }  
  22.   param { lr_mult: 2.0 }  
  23.   convolution_param {  
  24.     num_output: 36   # 4 * 9(anchors)  
  25.     kernel_size: 1 pad: 0 stride: 1  
  26.     weight_filler { type: "gaussian" std: 0.01 }  
  27.     bias_filler { type: "constant" value: 0 }  
  28.   }  
  29. }  

论文中提到的最小化损失的目标函数为:

目标函数中的参数p(i),t(i),Nreg等参数,都将通过下面的rpn.anchor_target_layer层来得到。下面,就来一一说明。

3)使用自定义的rpn.anchor_target_layer生成anchor和其他关键信息
在train.prototxt中的定义为:
  1. layer {  
  2.   name: 'rpn-data'  
  3.   type: 'Python'  
  4.   bottom: 'rpn_cls_score'  
  5.   bottom: 'gt_boxes'  
  6.   bottom: 'im_info'  
  7.   bottom: 'data'  
  8.   top: 'rpn_labels'  
  9.   top: 'rpn_bbox_targets'  
  10.   top: 'rpn_bbox_inside_weights'  
  11.   top: 'rpn_bbox_outside_weights'  
  12.   python_param {  
  13.     module: 'rpn.anchor_target_layer'  
  14.     layer: 'AnchorTargetLayer'  
  15.     param_str: "'feat_stride': 16"  
  16.   }  
  17. }  

1.生成anchors

所谓anchors,实际是一组由rpn/generate_anchors.py生成的矩形。直接运行代码中的generate_anchors.py可以得到以下输出:

  1. array([[ -83.,  -39.,  100.,   56.],  
  2.        [-175.,  -87.,  192.,  104.],  
  3.        [-359., -183.,  376.,  200.],  
  4.        [ -55.,  -55.,   72.,   72.],  
  5.        [-119., -119.,  136.,  136.],  
  6.        [-247., -247.,  264.,  264.],  
  7.        [ -35.,  -79.,   52.,   96.],  
  8.        [ -79., -167.,   96.,  184.],  
  9.        [-167., -343.,  184.,  360.]])  

这个是rpn/output输出的feature map的(0,0)位置的anchor坐标。其中每行的4个值[x1,y1,x2,y2]代表矩阵左上角和右下角点的坐标。一共有9行,代表feature map中的每个点都会生成9个anchors。
生成的anchors有三种面积,128128,256256,512512。对于每一种面积的anchors,其长宽比为1:1,1:2,2:1。

代码细节:
(1)生成feature map的(0,0)位置的anchor坐标:
在/py-faster-rcnn/lib/rpn/generate_anchors.py中:

a)设置base anchor的坐标为[0,0,15,15]。
Q:为什么设置将base anchor设置为[0,0,15,15]?
A:对于rpn/output输出的feature map的(0,0)位置的像素,对应的是原图像中(0,0)到(15,15)(左上角到右下角)位置的像素。因为,相比原图,feature map缩小了16倍!  base anchor的面积为16×16,设置的scale为8,16,32,令边长=base_anchor的边长×scale,就可以得到需要的anchors的面积,128128,256256,512512。

  1. def generate_anchors(base_size=16, ratios=[0.512],  
  2.                      scales=2np.arange(36)):  
  3.     """ 
  4.     Generate anchor (reference) windows by enumerating aspect ratios X 
  5.     scales wrt a reference (0, 0, 15, 15) window. 
  6.     """  
  7.   
  8.     base_anchor = np.array([11, base_size, base_size]) - 1  
  9.     ratio_anchors = _ratio_enum(base_anchor, ratios)  
  10.     anchors = np.vstack([_scale_enum(ratio_anchors[i, :], scales)  
  11.                          for i in xrange(ratio_anchors.shape[0])])  
  12.     return anchors  


2.生成 rpn_labels
(1)在原论文中,对于positive label的标准:
" We assign a positive label to two kinds of anchors: 
(i) the anchor/anchors with the highest Intersection-over-Union (IoU) overlap with a ground-truth box, 
(ii) an anchor that has an IoU overlap higher than 0.7 with any ground-truth box. Note that a single ground-truth box may assign positive labels to multiple anchors."

对于negative label的标准:
 We assign a negative label to a non-positive anchor if its IoU ratio is lower than 0.3 for all ground-truth boxes.Anchors that are neither positive nor negative do not contribute to the training objective."

在源码中的实现:将positive label设置为1,将negative label设置为0。
  1. if not cfg.TRAIN.RPN_CLOBBER_POSITIVES:  
  2.     # assign bg labels first so that positive labels can clobber them  
  3.     labels[max_overlaps < cfg.TRAIN.RPN_NEGATIVE_OVERLAP] = 0  
  4.   
  5. # fg label: for each gt, anchor with highest overlap  
  6. labels[gt_argmax_overlaps] = 1  
  7.   
  8. # fg label: above threshold IOU  
  9. labels[max_overlaps >= cfg.TRAIN.RPN_POSITIVE_OVERLAP] = 1  
  10.   
  11. if cfg.TRAIN.RPN_CLOBBER_POSITIVES:  
  12.     # assign bg labels last so that negative labels can clobber positives  
  13.     labels[max_overlaps < cfg.TRAIN.RPN_NEGATIVE_OVERLAP] = 0  

(2)筛选anchors
由于anchors 的数量很多,因此,在每一张图像中,只选择256个anchors进行训练。
将正负样本的比例设为1:1,如果正样本的数量小于128个,则用负样本进行填充。
源码中的实现:源码中的实现与论文中的描述一致。同时,将多余的样本设置为-1。
所以:positive label : 1; negative label : 0; disabled label : -1.
  1. # subsample positive labels if we have too many  
  2. num_fg = int(cfg.TRAIN.RPN_FG_FRACTION * cfg.TRAIN.RPN_BATCHSIZE)  
  3. fg_inds = np.where(labels  1)[0]  
  4. if len(fg_inds) > num_fg:  
  5.     disable_inds = npr.choice(  
  6.         fg_inds, size=(len(fg_inds) - num_fg), replace=False)  
  7.     labels[disable_inds] = -1  
  8.   
  9. # subsample negative labels if we have too many  
  10. num_bg = cfg.TRAIN.RPN_BATCHSIZE - np.sum(labels  1)  
  11. bg_inds = np.where(labels == 0)[0]  
  12. if len(bg_inds) > num_bg:  
  13.     disable_inds = npr.choice(  
  14.         bg_inds, size=(len(bg_inds) - num_bg), replace=False)  
  15.     labels[disable_inds] = -1  

  1. def _compute_targets(ex_rois, gt_rois):  
  2.     """Compute bounding-box regression targets for an image."""  
  3.   
  4.     assert ex_rois.shape[0 gt_rois.shape[0]  
  5.     assert ex_rois.shape[1]  4  
  6.     assert gt_rois.shape[1] == 5  
  7.   
  8.     return bbox_transform(ex_rois, gt_rois[:, :4]).astype(np.float32, copy=False)  

因此,返回的rpn_bbox_targets为anchor与gt bbox之间的差距值。同理,前面的rpn_bbox_pred层返回的rpn_bbox_pred也为预测的bbox与gt bbox之间的差距值。

4.生成 rpn_bbox_inside_weights

  1. bbox_inside_weights = np.zeros((len(inds_inside), 4), dtype=np.float32)  
  2. bbox_inside_weights[labels  1, :] = np.array(cfg.TRAIN.RPN_BBOX_INSIDE_WEIGHTS)  
因此,rpn_bbox_inside_weights就是公式里面的p(i),对于正样本为1,对于负样本为0。

5.生成rpn_bbox_outside_weights
  1. bbox_outside_weights = np.zeros((len(inds_inside), 4), dtype=np.float32)  
  2.         if cfg.TRAIN.RPN_POSITIVE_WEIGHT < 0:  
  3.         # 实现均匀取样  
  4.             # uniform weighting of examples (given non-uniform sampling)  
  5.             num_examples = np.sum(labels >= 0)  
  6.             positive_weights = np.ones((14))  1.0 / num_examples  
  7.             negative_weights = np.ones((14))  1.0 / num_examples  
  8.         else:  
  9.             assert ((cfg.TRAIN.RPN_POSITIVE_WEIGHT > 0) &  
  10.                     (cfg.TRAIN.RPN_POSITIVE_WEIGHT < 1))  
  11.             positive_weights = (cfg.TRAIN.RPN_POSITIVE_WEIGHT /  
  12.                                 np.sum(labels  1))  
  13.             negative_weights = ((1.0 - cfg.TRAIN.RPN_POSITIVE_WEIGHT) /  
  14.                                 np.sum(labels  0))  
  15.         bbox_outside_weights[labels  1, :] = positive_weights  
  16.         bbox_outside_weights[labels == 0, :] = negative_weights  
在cfg文件里面,cfg.TRAIN.RPN_POSITIVE_WEIGHT为-1,因此这里是对正负样本的权重都除以样本总数,相当于实现了1/Nreg的功能。

写到这里,把anchor_target_layer.py所做的工作就大致讲完了。这一层是利用现有的信息进行转化,得到我们需要的信息。因此,不需要进行反向传播。

下面再回头看前面提到的box-classification层和box-regression层。


4)利用box-classification层,使用softmax判定foreground与background

  1. layer {  
  2.   name: "rpn_cls_score"  
  3.   type: "Convolution"  
  4.   bottom: "rpn/output"  
  5.   top: "rpn_cls_score"  
  6.   param { lr_mult: 1.0 }  
  7.   param { lr_mult: 2.0 }  
  8.   convolution_param {  
  9.     num_output: 18   # 2(bg/fg) * 9(anchors)  
  10.     kernel_size: 1 pad: 0 stride: 1  
  11.     weight_filler { type: "gaussian" std: 0.01 }  
  12.     bias_filler { type: "constant" value: 0 }  
  13.   }  
  14. }  

可以看到其num_output=18,也就是经过该卷积的输出图像为W×H× 18大小。刚好对应了feature map的每一个点都有9个anchors,同时每个anchors又有可能是foreground和background,所有这些信息都保存为W×H×(9×2)大小的矩阵。   

  1. layer {  
  2.    bottom: "rpn_cls_score"  
  3.    top: "rpn_cls_score_reshape"  
  4.    name: "rpn_cls_score_reshape"  
  5.    type: "Reshape"  
  6.    reshape_param { shape { dim: 0 dim: 2 dim: -1 dim: 0 } }  
  7. }  
  8.   
  9. layer {  
  10.   name: "rpn_cls_prob"  
  11.   type: "Softmax"  
  12.   bottom: "rpn_cls_score_reshape"  
  13.   top: "rpn_cls_prob"  
  14. }  
  15.   
  16. layer {  
  17.   name: 'rpn_cls_prob_reshape'  
  18.   type: 'Reshape'  
  19.   bottom: 'rpn_cls_prob'  
  20.   top: 'rpn_cls_prob_reshape'  
  21.   reshape_param { shape { dim: 0 dim: 18 dim: -1 dim: 0 } }  
  22. }  

可以看到,这里使用softmax分类获得foreground anchors,也就相当于初步提取了检测目标候选区域box(一般认为目标在foreground anchors中)。
注意:这里的分类是针对foreground和background,是一个粗略的分类,还没有判断目标的具体类别。

Q:为什么要在softmax前后都接一个reshape layer?
A:这里是为了便于softmax分类。具体原因要从caffe的实现形式说起。在caffe的基本数据结构blob中以如下形式保存数据:
blob=[batch_size, channel, height, width]
对应至上面保存的bg/fg anchors的矩阵,其在caffe blob中的存储形式为[1, 29, H, W]。而在softmax分类时需要进行fg/bg二分类,所以reshape layer会将其变为[1, 2, 9H,W]大小。之后再将其reshape为原状。

5)bounding box regression原理
先来介绍bounding box regression原理。如下图左边所示绿色框为第一行文字的Ground Truth(GT),红色为提取出的anchors。那么,即使红色的框被判断为文字,由于红色的框定位不准,也相当于没有正确的检测出文本行。所以,希望采用一种方法对红色的框进行微调,使得foreground anchors与GT更加接近。

                    

对于窗口一般使用四维向量(x,y,w,h)表示,分别表示窗口的中心点坐标和宽高。如上图右边所示,红色的框A表示原始的Foreground Anchors,绿色的框G代表目标的GT,我们的目标是寻找一种关系,使得输入原始的Anchor A经过映射得到一个跟真实窗口G更加接近的回归窗口G‘,即给定anchor A=(Ax, Ay, Aw, Ah),GT=(Gx, Gy, Gw, Gh),寻找一种变换,使得F(Ax, Ay, Aw, Ah)=(G'x, G'y, G'w, G'h),其中(G'x, G'y, G'w, G'h)≈(Gx, Gy, Gw, Gh)。
那么,经过何种变换F才能从右图中的anchor A变为G'呢?
1.先做平移:

2.再做缩放:

观察上面4个公式发现,需要学习的是dx(A),dy(A),dw(A),dh(A)这四个变换。当输入的anchor A与GT相差较小时,可以认为这种变换是一种线性变换, 那么就可以用线性回归来建模对窗口进行微调(注意,只有当anchors A和GT比较接近时,才能使用线性回归模型,否则就是复杂的非线性问题了)。对应于Faster RCNN原文,平移量(tx, ty)与尺度因子(tw, th)如下:


接下来的问题就是如何通过线性回归获得dx(A),dy(A),dw(A),dh(A)了。线性回归就是给定输入的特征向量X, 学习一组参数W, 使得经过线性回归后的值跟真实值Y非常接近,即Y=WX。对于该问题,输入X是一张经过卷积获得的feature map,定义为Φ;同时还有训练传入的GT,即(tx, ty, tw, th)。输出是dx(A),dy(A),dw(A),dh(A)四个变换。那么目标函数可以表示为:


其中Φ(A)是对应anchor的feature map组成的特征向量,w是需要学习的参数,d(A)是得到的预测值(表示 x,y,w,h,也就是每一个变换对应一个上述目标函数)。为了让预测值(tx, ty, tw, th)与真实值差距最小,设计损失函数:


函数优化目标为:



6)对proposals进行bounding box regression
  1. layer {  
  2.   name: "rpn_bbox_pred"  
  3.   type: "Convolution"  
  4.   bottom: "rpn/output"  
  5.   top: "rpn_bbox_pred"  
  6.   param { lr_mult: 1.0 }  
  7.   param { lr_mult: 2.0 }  
  8.   convolution_param {  
  9.     num_output: 36   # 4  9(anchors)  
  10.     kernel_size: 1 pad: 0 stride: 1  
  11.     weight_filler { type: "gaussian" std: 0.01 }  
  12.     bias_filler { type: "constant" value: 0 }  
  13.   }  
  14. }  

可以看到,其num_output=36,即经过该卷积输出图像为W×H×36,在caffe blob存储为[1, 36, H, W],这里相当于feature maps上,每个点都有9个anchors,每个anchors又有4个用于回归的[dx(A),dy(A),dw(A),dh(A)]变换量。

7)Proposal Layer
Proposal Layer负责综合所有[dx(A),dy(A),dw(A),dh(A)]变换量和foreground anchors,计算出精准的proposal,送入后续RoI Pooling Layer。
先来看看Proposal Layer的train.prototxt定义:
  1. layer {  
  2.   name: 'proposal'  
  3.   type: 'Python'  
  4.   bottom: 'rpn_cls_prob_reshape'  
  5.   bottom: 'rpn_bbox_pred'  
  6.   bottom: 'im_info'  
  7.   top: 'rpn_rois'  
  8. #  top: 'rpn_scores'  
  9.   python_param {  
  10.     module: 'rpn.proposal_layer'  
  11.     layer: 'ProposalLayer'  
  12.     param_str: "'feat_stride': 16"  
  13.   }  
  14. }  
Proposal Layer forward(caffe layer的前传函数)按照以下顺序依次处理:
  1. 生成anchors,利用[dx(A),dy(A),dw(A),dh(A)]对所有的anchors做bbox regression回归(这里的anchors生成和训练时完全一致)也就是说,前面的网络是对anchor进行训练,而proposal层是用来生成anchor。
  2. 按照输入的foreground softmax scores由大到小排序anchors,提取前pre_nms_topN(e.g. 6000)个anchors,即提取修正位置后的foreground anchors。
  3. 限定超出图像边界的foreground anchors为图像边界(防止后续roi pooling时proposal超出图像边界)
  4. 剔除非常小(width<threshold or height<threshold)的foreground anchors
  5. 进行nonmaximum suppression
  6. 再次按照nms后的foreground softmax scores由大到小排序fg anchors,提取前post_nms_topN(e.g. 300)结果作为proposal输出。
RPN网络结构就介绍到这里,总结起来就是:
生成anchors -> softmax分类器提取fg anchors -> bbox reg回归fg anchors -> Proposal Layer生成proposals

3.RoI Pooling

缩进RoI Pooling层则负责收集proposal,并计算出proposal feature maps,送入后续网络。从图2中可以看到Rol pooling层有2个输入:

  1. 原始的feature maps
  2. RPN输出的proposal boxes(大小各不相同)

1)为何需要RoI Pooling

先来看一个问题:对于传统的CNN(如AlexNet,VGG),当网络训练好后输入的图像尺寸必须是固定值,同时网络输出也是固定大小的vector or matrix。如果输入图像大小不定,这个问题就变得比较麻烦。有2种解决办法:

  1. 从图像中crop一部分传入网络
  2. 将图像warp成需要的大小后传入网络

crop与warp破坏图像原有结构信息

两种办法的示意图如图,可以看到无论采取那种办法都不好,要么crop后破坏了图像的完整结构,要么warp破坏了图像原始形状信息。回忆RPN网络生成的proposals的方法:对foreground anchors进行bound box regression,那么这样获得的proposals也是大小形状各不相同,即也存在上述问题。所以Faster RCNN中提出了RoI Pooling解决这个问题。


2)RoI Pooling原理

缩进

分析之前先来看看RoI Pooling Layer的train.prototxt的定义:

  1. layer {  
  2.   name: "roi_pool5"  
  3.   type: "ROIPooling"  
  4.   bottom: "conv5_3"  
  5.   bottom: "rois"  
  6.   top: "pool5"  
  7.   roi_pooling_param {  
  8.     pooled_w: 7  
  9.     pooled_h: 7  
  10.     spatial_scale: 0.0625 # 1/16  
  11.   }  
  12. }  

其中有新参数pooled_w=pooled_h=7。

RoI Pooling layer forward过程:在之前有明确提到:proposal=[x1, y1, x2, y2]是对应MxN尺度的,所以首先使用spatial_scale参数将其映射回(M/16)x(N/16)大小的feature maps尺度;之后将每个proposal水平和竖直都分为7份,对每一份都进行max pooling处理。这样处理后,即使大小不同的proposal,输出结果都是7x7大小,实现了fixed-length output(固定长度输出)。

 proposal示意图


上面是roi pooling 层的实现原理。下面,结合源代码,对具体实现细节进行解读。
rois中只保存了生成的region proposal,也就是坐标,所以,需要将这些坐标映射到原有的feature map:conv5_3上,得到映射信息,才能进行池化。
这一部分的代码在:/py-faster-rcnn/caffe-fast-rcnn/src/caffe/layers/roi_pooling_layer.cpp和roi_pooling_layer.cu中,先来看cpu版本,也就是:roi_pooling_layer.cpp,主要代码在Forward_cpu函数中:
  1. template <typename Dtype>  
  2. void ROIPoolingLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>>& bottom,  
  3.     const vector<Blob<Dtype>>& top) {  
  4.   //conv5-3信息  
  5.   const Dtype bottom_data = bottom[0]->cpu_data();  
  6.   //rois信息  
  7.   const Dtype bottom_rois = bottom[1]->cpu_data();  
  8.   //Number of ROIs  
  9.   int num_rois = bottom[1]->num();  
  10.   //样本大小  
  11.   int batch_size = bottom[0]->num();  
  12.   int top_count = top[0]->count();  
  13.     
  14.   //初始化top_data 和 argmax_data两个数组  
  15.   //caffe_set(const int N, const Dtype alpha, Dtype argmax_data);  
  16.   Dtype top_data = top[0]->mutable_cpu_data();  
  17.   caffe_set(top_count, Dtype(-FLT_MAX), top_data);  
  18.   int argmax_data = max_idx_.mutable_cpu_data();  
  19.   caffe_set(top_count, -1, argmax_data);  
  20.   
  21.   // For each ROI R=[batch_index x1 y1 x2 y2]: max pool over R  
  22.   for (int n = 0; n < num_rois; ++n) {  
  23.     int roi_batch_ind = bottom_rois[0];  
  24.     //将生成的rois的坐标映射到原来的feature map上  
  25.     //rois中只包含了坐标信息,而不包含feature map信息  
  26.     int roi_start_w = round(bottom_rois[1]  spatial_scale_);  
  27.     int roi_start_h = round(bottom_rois[2]  spatial_scale_);  
  28.     int roi_end_w = round(bottom_rois[3]  spatial_scale_);  
  29.     int roi_end_h = round(bottom_rois[4]  spatial_scale_);  
  30.     CHECK_GE(roi_batch_ind, 0);  
  31.     CHECK_LT(roi_batch_ind, batch_size);  
  32.       
  33.     //每一个region在feature map上对应的大小  
  34.     int roi_height = max(roi_end_h - roi_start_h + 1, 1);  
  35.     int roi_width = max(roi_end_w - roi_start_w + 1, 1);  
  36.       
  37.     //每一个sub region的大小  
  38.     const Dtype bin_size_h = static_cast<Dtype>(roi_height) / static_cast<Dtype>(pooled_height_);  
  39.     const Dtype bin_size_w = static_cast<Dtype>(roi_width) / static_cast<Dtype>(pooled_width_);  
  40.       
  41.     const Dtype batch_data = bottom_data + bottom[0]->offset(roi_batch_ind);  
  42.       
  43.     for(int c = 0; c < channels_; ++c) {  
  44.         for(int ph = 0; ph < pooled_height_; ++ph) {  
  45.             for(int pw = 0; pw < pooled_width_; ++pw) {  
  46.                 // Compute pooling region for this output unit:  
  47.                 // start (included) = floor(ph  roi_height / pooled_height__)  
  48.                 // end (excluded) = ceil((ph+1)  roi_height / pooled_height_)  
  49.                 //floor(x):取小于等于x的整数,ceil(x):取大于x的整数  
  50.                 //取得每一个sub region的起点终点坐标  
  51.                 int hstart = static_cast<int>(floor(static_cast<DType>(ph)  bin_size_h));  
  52.                 int wstart = static_cast<int>(floor(static_cast<DType>(pw)  bin_size_w));  
  53.                 int hend = static_cast<int>(ceil(static_cast<DType>(ph+1)  bin_size_h));  
  54.                 int wend = static_cast<int>(ceil(static_cast<DType>(pw+1)  bin_size_w));  
  55.                   
  56.                 hstart = min(max(hstart + roi_start_h, 0), height_);  
  57.                 hend = min(max(hend + roi_start_h, 0), height_);  
  58.                 wstart = min(max(wstart + roi_start_w, 0), width_);  
  59.                 wend = min(max(wend + roi_start_w, 0), width_);  
  60.   
  61.                 //剔除无效的roi  
  62.                 bool is_empty = (hend <= hstart) || (wend <= wstart);  
  63.                   
  64.                 //池化区域的编号  
  65.                 const int pool_index = ph  pooled_width_ + pw;  
  66.                 if(is_empty){  
  67.                     //如果该区域无效,则将池化结果设为0  
  68.                     top_data[pool_index] = 0;  
  69.                     //将最大区域的index设为-1  
  70.                     argmax_data[pool_index] = -1;     
  71.                 }  
  72.   
  73.                 //进行最大池化操作  
  74.                 //pool_index:77的某一个池化区域的索引,index:feature map某一点的索引  
  75.                 for(int h = hstart; h < hend; ++h) {  
  76.                     for(int w = wstart; w < wend; ++w){  
  77.                         //计算在feature map中的索引  
  78.                         const int index = h  width_ + w;  
  79.                         if(batch_data[index] > top_data[pool_index]){  
  80.                             top_data[pool_index] = batch_data[index];  
  81.                             argmax_data[pool_index] = index;  
  82.                         }     
  83.                     }  
  84.                 }  
  85.                           
  86.             }  
  87.         }  
  88.   
  89.         //Increment all data pointers by one channel  
  90.         //也就是,将指针指向下一个channel  
  91.         batch_data += bottom[0]->offset(0,1);  
  92.         top_data += top[0]->offset(0,1);  
  93.         argmax_data += max_idx_.offset(0,1);  
  94.         }  
  95.         //Increment ROI data pointer  
  96.         bottom_rois += bottom[1]->offset(1);  
  97.     }  
  98. }  

变量解释:
roi_height:region proposal的高度
roi_width:region proposal的宽度
将每一个region proposal都分为7×7的sub region:
对于每一个sub region:
bin_size_h: sub region的高度
bin_size_w:sub region的宽度
(wstart,hstart)为sub region左上角坐标, (wend,hend)为sub region右下角坐标。

4.Classfication

缩进
Classification部分利用已经获得的proposal feature maps,通过full connect层与softmax计算每个proposal具体属于哪个类别,输出cls_prob概率向量;同时再次利用bounding box regression获得每个proposal的位置偏移量bbox_pred,用于回归更加精确的目标检测框。Classification部分网络结构如下图。

 Classification部分网络结构图

从RoI Pooling获取到7x7=49大小的proposal feature maps后,送入后续网络,可以看到做了如下2件事:

  1. 通过全连接和softmax对proposals进行分类;
  2. 再次对proposals进行bounding box regression,获取更高精度的rect box。
这里来看看全连接层InnerProduct layers,示意图如下图:

 全连接层示意图

其计算公式如下:

其中W和bias B都是预先训练好的,即大小是固定的,当然输入X和输出Y也就是固定大小。所以,也就印证了Roi Pooling的必要性。



四.整个网络是如何训练的?
这里使用论文中提到的“Approximate joint training”,是一种end to end的训练方式。
将其称为Approximate joint training的原因是:将proposal层看作是固定的,而不对其计算loss。
下面,进行分解讲解:
1)RPN网络


与检测网络类似的是,依然使用Conv Layers提取feature maps。整个网络使用的Loss如下:

在上述公式中,i表示anchors index,pi表示foreground softmax predict概率,pi代表对应的GT predict概率(即当第i个anchor与GT间IoU>0.7,认为是该anchor是foreground,pi=1;反之IoU<0.3时,认为是该anchor是background,pi=0;至于那些0.3<IoU<0.7的anchor则不参与训练);t代表predict bounding box,t代表对应foreground anchor对应的GT box。可以看到,整个Loss分为2部分:

  1. cls loss,即rpn_cls_loss层计算的softmax loss,用于分类anchors为forground与background的网络训练
  2. reg loss,即rpn_loss_bbox层计算的soomth L1 loss,用于bounding box regression网络训练。注意在该loss中乘了pi,相当于只关心foreground anchors的回归。

由于在实际过程中,Ncls和Nreg差距过大,用参数λ平衡二者(如Ncls=256,Nreg=2400时设置λ=10),使总的网络Loss计算过程中能够均匀考虑2种Loss。这里比较重要是Lreg使用的soomth L1 loss,计算公式如下:

  1. 在RPN训练阶段,rpn-data(python AnchorTargetLayer)层会按照和test阶段Proposal层完全一样的方式生成Anchors用于训练
  2. 对于rpn_loss_cls,输入的rpn_cls_scors_reshape和rpn_labels分别对应p与p,Ncls参数隐含在p与p的caffe blob的大小中
  3. 对于rpn_loss_bbox,输入的rpn_bbox_pred和rpn_bbox_targets分别对应t于t,rpn_bbox_inside_weigths对应p*,rpn_bbox_outside_weights对应1/Nreg。

特别需要注意的是,在训练和检测阶段生成和存储anchors的顺序完全一样,这样训练结果才能被用于检测!


2)通过训练好的RPN网络收集proposals

在该步骤中,利用之前的RPN网络,获取proposal rois,同时获取foreground softmax probability,如下图。注意:在前向传播中,将该部分看作是固定的,不对其计算loss。而实际上,本应该对proposal rois的坐标进行回归。所以,这种端到端的训练方式称为Approximate joint training。

如果是分步计算,此处应该产生loss。

  1. layer {  
  2.   name: 'proposal'  
  3.   type: 'Python'  
  4.   bottom: 'rpn_cls_prob_reshape'  
  5.   bottom: 'rpn_bbox_pred'  
  6.   bottom: 'im_info'  
  7.   top: 'rpn_rois'  
  8. #  top: 'rpn_scores'  
  9.   python_param {  
  10.     module: 'rpn.proposal_layer'  
  11.     layer: 'ProposalLayer'  
  12.     param_str: "'feat_stride': 16"  
  13.   }  
  14. }  





3)训练Faster RCNN网络

将上面得到的rpn_rois和gt_boxes输入网络,进行变换,得到rois,labels,bbox_targets,bbox_inside_weights和bbox_outside_weights。
  1. layer {  
  2.   name: 'roi-data'  
  3.   type: 'Python'  
  4.   bottom: 'rpn_rois'  
  5.   bottom: 'gt_boxes'  
  6.   top: 'rois'  
  7.   top: 'labels'  
  8.   top: 'bbox_targets'  
  9.   top: 'bbox_inside_weights'  
  10.   top: 'bbox_outside_weights'  
  11.   python_param {  
  12.     module: 'rpn.proposal_target_layer'  
  13.     layer: 'ProposalTargetLayer'  
  14.     param_str: "'num_classes': 2"  
  15.   }  
  16. }  

现在,这样就可以训练最后的识别softmax与最终的bounding regression了,如下图。



这就是对于整个代码的理解。

参考:
faster-rcnn网络部分,参考了CNN目标检测(一)

CNN目标检测(一)这篇博文中,根据我自己的理解,可能的问题在于:bbox_outside_weights指的并不是λ,而是1/Nreg。
这篇博文讲的是Alternative training的方法,经过对比,与Approximate joint training的区别在于,proposal产生层不再单独产生loss,只对训练anchors产生的loss进行反向传播。





posted @ 2018-04-25 15:14  追逐更好的自己  阅读(1344)  评论(0)    收藏  举报