百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

基于Tensorflow的目标检测(Detection)的代码案例详解

bigegpt 2024-08-02 11:03 3 浏览

我主要阐述了基于Tensorflow的Faster RCNN在Windows上的一个Demo程序,其中,分为两个部分,一个是训练数据导入部分,一个是网络架构部分开始。源程序git地址我会放在文章最后,下载后可以参考对应看一下。

一、程序运行环境说明

首先,我想阐述一堆巨坑,下面只要有一条没有环境或条件达到或做到,你的程序将无法运行:

Windows10 家庭版:

Python3.5+Windows+Visual Studio 2015+cuda9.1

这里,本人踩过几个坑,忘后来人应用这个版本的Demo不要再走:

① Python3.6无法编译该程序。因为作者编译时环境为3.5

② 如果你的电脑是Windows家庭版,不要用Anaconda进行安装Python3.5,直接装上Python3.5即可,因为家庭版的Windows10系统无法安装Anaconda3+Python3.5的环境,Anaconda3默认3.6版或2.7版。

③ 除Visual Studio2015外的版本将无法执行符合要求的编译Python所需的C++环境。(不要问我为什么,我也不知道)

Windows10 企业版:

Anaconda3+Python3.5+Cuda9.1

① Anaconda与Python对应的版本可以百度搜索清华Python镜像中下载。

② 如果用Anaconda搭载python3.5将不需要Visual Studio环境,无需安装。反之,如果没有用Anaconda搭载python,而是直接安装python,就必须要安装Visual Studio 2015的环境。

好了,坑到此结束,说完这些,按照ReadMe编译程序之后,应该程序可以运行了。

我的IDE用的是Pycharm Jetbrain。

二、训练数据导入部分

那么,我们先来看数据导入的环节:

由于物体检测是回归和分类任务,那么导入的数据就要包括物体的位置以及他的类别,那么在程序中,这些信息的根目录在:

...\FasterRcnn\Faster-RCNN-TensorFlow-Python3.5-master\data\VOCDevkit2007\VOC2007\Annotations

图像信息由xml文件读取。

而图像与图像信息xml文件是一一对应的,这些训练集中图像的根目录在:

...\Desktop\FasterRcnn\Faster-RCNN-TensorFlow-Python3.5-master\data\VOCDevkit2007\VOC2007\JPEGImages

现在,我们回到代码train.py:

可以明显的发现,train文件中主函数中一共就有两句话:

train = Train()
train.train()

第一句就是我们网络训练数据集导入的过程,而第二句主要就是真正的训练数据集的过程,那么我们还是从第一句开始:

首先,我们跳入这句Train(),再跳入VGG16.py中的初始化过程,具体在network.py中:

self._feat_stride = [16, ]
self._feat_compress = [1. / 16., ]
self._batch_size = batch_size
self._predictions = {}
self._losses = {}
self._anchor_targets = {}
self._proposal_targets = {}
self._layers = {}
self._act_summaries = []
self._score_summaries = {}
self._train_summaries = []
self._event_summaries = {}
self._variables_to_fix = {}

一开始包括了一些参数的指定,例如feat_stride,为后续说到的锚点和原始图像对应的区域。

我们回到train.py接下去看:

self.imdb, self.roidb = combined_roidb("voc_2007_trainval")

这一句,把训练的图像信息全部读入到了roidb这样一个变量中,跳入combined_roidb():

def get_roidb(imdb_name):
imdb = get_imdb(imdb_name)
print('Loaded dataset `{:s}` for training'.format(imdb.name))
imdb.set_proposal_method("gt")
print('Set proposal method: {:s}'.format("gt"))
roidb = get_training_roidb(imdb)
return roidb

以上代码,表示了通过名字把roidb读入进来的过程,最后返回了roidb这个变量:

注意到,代码中有这样一句:

roidbs = [get_roidb(s) for s in imdb_names.split('+')]

这句的意思就是数据源可能是从多个源头进行导入的,所以假如真的是从多个数据源进行导入,则用加号把各种数据集连起来,到了用到的时候再用split函数把各种数据集的名字分开。

但事实上,程序中只用到了一个数据集,所以下一句是:

roidb = roidbs[0]

由于程序确定只有一个数据集的数据,所以只需要取0位置上的数据集即可,这里如果后续有修改,则可以按照具体情况修改。

那么具体的数据集操作是怎么进行的呢?我们跳入get_imdb():

再跳一次,到了factory.py

# Set up voc_<year>_<split>
for year in ['2007', '2012']:
 for split in ['train', 'val', 'trainval', 'test']:
 name = 'voc_{}_{}'.format(year, split)
 __sets[name] = (lambda split=split, year=year: pascal_voc(split, year))
 
# Set up coco_2014_<split>
for year in ['2014']:
 for split in ['train', 'val', 'minival', 'valminusminival', 'trainval']:
 name = 'coco_{}_{}'.format(year, split)
 __sets[name] = (lambda split=split, year=year: coco(split, year))
 
# Set up coco_2015_<split>
for year in ['2015']:
 for split in ['test', 'test-dev']:
 name = 'coco_{}_{}'.format(year, split)
 __sets[name] = (lambda split=split, year=year: coco(split, year))

我们发现,会有三个循环,怕是coco数据集和pascal_voc数据集在不同年份,他内部的格式也不同,所以要经过这样的处理吧。

先从pascal_voc数据集看起,跳入imdb.init函数,下面代码位于imdb.py:

 def __init__(self, name, classes=None):
 self._name = name
 self._num_classes = 0
 if not classes:
 self._classes = []
 else:
 self._classes = classes
 self._image_index = []
 self._obj_proposer = 'gt'
 self._roidb = None
 self._roidb_handler = self.default_roidb
 # Use this dict for storing dataset specific config options
 self.config = {}

imdb.py这个文件主要就是对读入的数据进行一系列的操作:

初始化部分指定了数据集的名字,初始化类的数量,初始化类的索引标签。指定了proposal的名字为gt,roidb是我们最终得到的结果,先设为NULL,同时,程序设置了一个handler,进行一些操作,一会儿会详细说到。

现在回到pascal_voc.py继续看初始化后的过程:

self._year = year
self._image_set = image_set

先指定了数据集年份,然后指定了要用到的东西的Annotation在哪里,我们现在用到的就只有Val和Train,即训练数据和我们的真实数据,就是ground truth:

其中PascalVOC的标注文件在:

...\Desktop\FasterRcnn\Faster-RCNN-TensorFlow-Python3.5-master\data\VOCDevkit2007\VOC2007\ImageSets\Main

其中可以打开看一下,trainval这个文件:

000005
000007
000009
000012
000016
000017
000019
000020
000021
000023
000024
000026
000030

文件中是以这样的形式出现的数据,一共五千条,测试了五千组需要用的案例。trainval中的这些数据就是我们接下来需要训练的数据的一个标签,即对应的图片的名字以及对应的xml信息。

接下来就是指定路径读入相关的信息了。

self._devkit_path = self._get_default_path() if devkit_path is None \
 else devkit_path
self._data_path = os.path.join(self._devkit_path, 'VOC' + self._year)

再后面指定了我们做分类的类别,一共21个类,二十个前景加上一个背景。之后,给每个类的字符串设置一个固定的索引值,这样更加方便接下来的一系列操作:

self._class_to_ind = dict(list(zip(self.classes, list(range(self.num_classes)))))

实际上,pascalVOC这么多文件中,这个程序中用到的怕是只有valtrain这一个txt文件了,之后,load一下我们的数据,根据ImageSet中指定的数据,从_data_path路径中读出,并通过x.strip一条一条读出,并把读到的东西以image_index的参数形式返回:

 def _load_image_set_index(self):
 """
 Load the indexes listed in this dataset's image set file.
 """
 # Example path to image set file:
 # self._devkit_path + /VOCdevkit2007/VOC2007/ImageSets/Main/val.txt
 image_set_file = os.path.join(self._data_path, 'ImageSets', 'Main',
 self._image_set + '.txt')
 assert os.path.exists(image_set_file), \
 'Path does not exist: {}'.format(image_set_file)
 with open(image_set_file) as f:
 image_index = [x.strip() for x in f.readlines()]
 return image_index

接下来,我们已经看完了pascalVOC的读入过程了,coco数据集也是同理,所以不作赘述,继续回到train.py:

其中set_proposal_method(“gt”),这句话指定了读入的信息就是我们的ground truth。

以下的一句话,有点意思哦:

roidb = get_training_roidb(imdb)

然后我们跳入这个方法来看一下:

def get_training_roidb(imdb):
 """Returns a roidb (Region of Interest database) for use in training."""
 if True:
 print('Appending horizontally-flipped training examples...')
 imdb.append_flipped_images()
 print('done')
 
 print('Preparing training data...')
 rdl_roidb.prepare_roidb(imdb)
 print('done')
 
 return imdb.roidb

这里将得到的图像都反转了一下,其实就是将图像做了一个镜面对称,这样我们一开始的数据量有5000,翻转之后,我们的数据量就有了一万。

我们仔细来看一下这个翻转的过程,具体再imdb.py中:

 def append_flipped_images(self):
 num_images = self.num_images
 widths = self._get_widths()
 for i in range(num_images):
 boxes = self.roidb[i]['boxes'].copy()
 oldx1 = boxes[:, 0].copy()
 oldx2 = boxes[:, 2].copy()
 boxes[:, 0] = widths[i] - oldx2 - 1
 boxes[:, 2] = widths[i] - oldx1 - 1
 assert (boxes[:, 2] >= boxes[:, 0]).all()
 entry = {'boxes': boxes,
 'gt_overlaps': self.roidb[i]['gt_overlaps'],
 'gt_classes': self.roidb[i]['gt_classes'],
 'flipped': True}
 self.roidb.append(entry)
 self._image_index = self._image_index * 2

好了,到此,我们的数据就算是基本加载完毕了,有一些其他的处理要说明一下,就比如pascalVOC中的:

 def gt_roidb(self):
 """
 Return the database of ground-truth regions of interest.
 This function loads/saves from/to a cache file to speed up future calls.
 """
 cache_file = os.path.join(self.cache_path, self.name + '_gt_roidb.pkl')
 if os.path.exists(cache_file):
 with open(cache_file, 'rb') as fid:
 try:
 roidb = pickle.load(fid)
 except:
 roidb = pickle.load(fid, encoding='bytes')
 print('{} gt roidb loaded from {}'.format(self.name, cache_file))
 return roidb

这个函数的目的是加载数据之后形成一个pickle文件,以后再运行程序的时候,如果数据已经加载就直接从pickle文件中读取,如果没有加载,就继续加载。

...\Desktop\FasterRcnn\Faster-RCNN-TensorFlow-Python3.5-master\data\cache

这是缓存的根目录,可以尝试删除试试会出现什么效果哦。

看代码中,指定缓存目录和名字,如果名字存在就先加载完已有的再加载新的数据,如果不存在就从头加载。

好,那么到现在为止,我们已经知道了,选用哪个数据集,加载哪些数据,那些固定的数据在什么位置,以何种形式加载进来,但是,还有一个重要的问题就是,这个数据是怎么以标签的形式具体加载进来的呢?

XML文件是通过解析器解析出来的:

 def _load_pascal_annotation(self, index):
 """
 Load image and bounding boxes info from XML file in the PASCAL VOC
 format.
 """
 filename = os.path.join(self._data_path, 'Annotations', index + '.xml')
 tree = ET.parse(filename)
 objs = tree.findall('object')
 if not self.config['use_diff']:
 # Exclude the samples labeled as difficult
 non_diff_objs = [
 obj for obj in objs if int(obj.find('difficult').text) == 0]
 # if len(non_diff_objs) != len(objs):
 # print 'Removed {} difficult objects'.format(
 # len(objs) - len(non_diff_objs))
 objs = non_diff_objs
 num_objs = len(objs)
 
 boxes = np.zeros((num_objs, 4), dtype=np.uint16)
 gt_classes = np.zeros((num_objs), dtype=np.int32)
 overlaps = np.zeros((num_objs, self.num_classes), dtype=np.float32)
 # "Seg" area for pascal is just the box area
 seg_areas = np.zeros((num_objs), dtype=np.float32)
 
 # Load object bounding boxes into a data frame.
 for ix, obj in enumerate(objs):
 bbox = obj.find('bndbox')
 # Make pixel indexes 0-based
 x1 = float(bbox.find('xmin').text) - 1
 y1 = float(bbox.find('ymin').text) - 1
 x2 = float(bbox.find('xmax').text) - 1
 y2 = float(bbox.find('ymax').text) - 1
 cls = self._class_to_ind[obj.find('name').text.lower().strip()]
 boxes[ix, :] = [x1, y1, x2, y2]
 gt_classes[ix] = cls
 overlaps[ix, cls] = 1.0
 seg_areas[ix] = (x2 - x1 + 1) * (y2 - y1 + 1)
 
 overlaps = scipy.sparse.csr_matrix(overlaps)
 
 return {'boxes': boxes,
 'gt_classes': gt_classes,
 'gt_overlaps': overlaps,
 'flipped': False,
 'seg_areas': seg_areas}

boxes = np.zeros((num_objs, 4), dtype=np.uint16)

其中,boxes是一个回归框,两个坐标,有n个物体,就是4×n个位置。

gt_classes = np.zeros((num_objs), dtype=np.int32)

其中,有几类,就加载几类进来。overlaps做one hold recording。

seg_areas求面积,暂时还没有用到。

然后就是循环了:这里循环的是一张图片上的n个物体。

现在翻转也做了,数量加倍了,指定了相应的数据了,也都提取出来了。

下面还有一句:

 rdl_roidb.prepare_roidb(imdb)

再跳一次,到roidb中的prepare_roidb函数中:

def prepare_roidb(imdb):
 """Enrich the imdb's roidb by adding some derived quantities that
 are useful for training. This function precomputes the maximum
 overlap, taken over ground-truth boxes, between each ROI and
 each ground-truth box. The class with maximum overlap is also
 recorded.
 """
 roidb = imdb.roidb
 if not (imdb.name.startswith('coco')):
 sizes = [PIL.Image.open(imdb.image_path_at(i)).size
 for i in range(imdb.num_images)]
 for i in range(len(imdb.image_index)):
 roidb[i]['image'] = imdb.image_path_at(i)
 if not (imdb.name.startswith('coco')):
 roidb[i]['width'] = sizes[i][0]
 roidb[i]['height'] = sizes[i][1]
 # need gt_overlaps as a dense array for argmax
 gt_overlaps = roidb[i]['gt_overlaps'].toarray()
 # max overlap with gt over classes (columns)
 max_overlaps = gt_overlaps.max(axis=1)
 # gt class that had the max overlap
 max_classes = gt_overlaps.argmax(axis=1)
 roidb[i]['max_classes'] = max_classes
 roidb[i]['max_overlaps'] = max_overlaps
 # sanity checks
 # max overlap of 0 => class should be zero (background)
 zero_inds = np.where(max_overlaps == 0)[0]
 assert all(max_classes[zero_inds] == 0)
 # max overlap > 0 => class should not be zero (must be a fg class)
 nonzero_inds = np.where(max_overlaps > 0)[0]
 assert all(max_classes[nonzero_inds] != 0)

这里,主要做了什么样的工作呢?

把所有的数据集合到了roidb上并返回。分别指定了路径,图片宽度,高度,重叠率,重叠最大的类别等等。

self.data_layer = RoIDataLayer(self.roidb, self.imdb.num_classes)
self.output_dir = cfg.get_output_dir(self.imdb, 'default')

最后,output_dir设置了pickel的默认路径。

datalayer传入了roidb处理完之后的相关数据,和相应类别,并做了一个洗牌操作shuffle。

三、网络架构搭建部分

好,现在先来总结一下Faster RCNN中网络的搭建架构:

图1

① 搭建了一个conv layers,即一个全卷积网络,在Tensorflow代码中为一个VGG16的结构。

② 从①中迭代几次后的卷积,池化操作后的Feature Map送入RPN(RegionProposal Network)层。

③ 用一个3×3的滑动窗口在②中得到的Feature Map中,(从左到右)滑动,以中间点为锚点,对应到原图,设置三个图像大小,和三个不同的长宽比例,经排列组合,一个锚点位置得到9个不同的对应图像,设所有锚点共计k个对应图像。

④ 用③中得到的k个对应图像,分别执行下述两个操作:回归和分类。回归操作为区分前景背景所用,进行一个二分类操作,故得到2k scores;当回归操作区分出是背景,则无需进行分类操作,如是前景则进行分类操作,得到4k coordinates,每个图像得到的四个值分别是,中心点坐标(x,y),以及该图像的具体长和宽(h,w)。

⑤ 经过回归和分类操作之后,进行框的筛选操作,即proposal层做的主要事情。首先,筛掉的框走以下几个步骤:第一,IOU>0.7,即产生的框和原始图像的ground truth的对比,如果重叠率大于0.7,则保留,否则筛掉;第二,NMS非极大值抑制筛选,通过二分类得到的scores值(即为前景的概率值),筛选前n个从大到小的框;第三,越界框筛选。第四,经过以上步骤后,继续筛选score值前m个从大到小的框。

⑥ 对得到的框进行Roi Pooling操作之后,连接一个全连接网络,并在此做一个分类任务,一个回归任务,分类任务为二十一分类,即二十个前景和一个背景,完成整个操作。

好了,到现在为止,回忆结束。

下面,我们正式进入代码:

① 网络结构搭建的大部分代码都位于VGG16.py这个网络中,进入主函数中,第一个Train()交代了数据的部分读入操作,第二个train()交代了网络的训练过程。我们先来解释网络的训练过程。核心代码为第85行:

layers = self.net.create_architecture(sess,"TRAIN", self.imdb.num_classes, tag='default')

其中,create_architecture()函数建立了所有的网络结构。下面,我们跳入该函数。

② 前面指定了一系列卷积,反卷积的参数,核心代码为295行:

rois, cls_prob, bbox_pred = self.build_network(sess,training)

rois为roi pooling层得到的框,cls_prob得到的是最后全连接层的分类score,bbox_pred得到的是二十一分类之后的分类标签。我们继续跳入build_network();

③ 跳入VGG16.py中的第18行同名函数。

好,我们来仔细研究一下这个同名函数:

 def build_network(self, sess, is_training=True):
 with tf.variable_scope('vgg_16', 'vgg_16'):
 
 # select initializer
 if cfg.FLAGS.initializer == "truncated":
 initializer = tf.truncated_normal_initializer(mean=0.0, stddev=0.01)
 initializer_bbox = tf.truncated_normal_initializer(mean=0.0, stddev=0.001)
 else:
 initializer = tf.random_normal_initializer(mean=0.0, stddev=0.01)
 initializer_bbox = tf.random_normal_initializer(mean=0.0, stddev=0.001)
 
 # Build head
 net = self.build_head(is_training)
 
 # Build rpn
 rpn_cls_prob, rpn_bbox_pred, rpn_cls_score, rpn_cls_score_reshape = self.build_rpn(net, is_training, initializer)
 
 # Build proposals
 rois = self.build_proposals(is_training, rpn_cls_prob, rpn_bbox_pred, rpn_cls_score)
 
 # Build predictions
 cls_score, cls_prob, bbox_pred = self.build_predictions(net, rois, is_training, initializer, initializer_bbox)
 
 self._predictions["rpn_cls_score"] = rpn_cls_score
 self._predictions["rpn_cls_score_reshape"] = rpn_cls_score_reshape
 self._predictions["rpn_cls_prob"] = rpn_cls_prob
 self._predictions["rpn_bbox_pred"] = rpn_bbox_pred
 self._predictions["cls_score"] = cls_score
 self._predictions["cls_prob"] = cls_prob
 self._predictions["bbox_pred"] = bbox_pred
 self._predictions["rois"] = rois
 
 self._score_summaries.update(self._predictions)
 
 return rois, cls_prob, bbox_pred

④ 该函数分为了几段,build head,buildrpn,build proposals,build predictions对应的刚好是我们所刚刚叙述的全卷积层,RPN层,Proposal Layer,和最后经过的全连接层。大体结构已有,那么我们就来逐步分析这个这几个函数:

⑤ 全卷积网络层的建立(build head)。在这个Demo中,全卷积网络为五个层,每层有一个卷积,一个池化操作,但是,最后一层操作中,仅有一个卷积操作,无池化操作。

 # Main network
 # Layer 1
 net = slim.repeat(self._image, 2, slim.conv2d, 64, [3, 3], trainable=False, scope='conv1')
 net = slim.max_pool2d(net, [2, 2], padding='SAME', scope='pool1')
 
 # Layer 2
 net = slim.repeat(net, 2, slim.conv2d, 128, [3, 3], trainable=False, scope='conv2')
 net = slim.max_pool2d(net, [2, 2], padding='SAME', scope='pool2')
 
 # Layer 3
 net = slim.repeat(net, 3, slim.conv2d, 256, [3, 3], trainable=is_training, scope='conv3')
 net = slim.max_pool2d(net, [2, 2], padding='SAME', scope='pool3')
 
 # Layer 4
 net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3], trainable=is_training, scope='conv4')
 net = slim.max_pool2d(net, [2, 2], padding='SAME', scope='pool4')
 
 # Layer 5
 net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3], trainable=is_training, scope='conv5')

由代码中可以看出,这里作者用的silm.conv2d函数进行卷积操作,传统卷积操作为nn模块下的conv2d,max_pool2d进行池化操作。池化用2×2的方格进行,由于卷积层操作不能够缩小图像大小,池化层变为原来的二分之一,所以四个池化层最终变为原来的1/16。

⑦RPN层的建立(build rpn)。_anchor_component()是用来生成九个框的函数。我们继续进入,其中设定了参数,height和width,在这里,都为3,然后通过,tf.py_func()生成9个候选框,generate_anchors_pre中,产生框的具体函数是generate_anchor();generate_anchors()产生位置。建立了位置关系之后,需要映射到原始图像,所以feat_stride为原始图像与这里图像的倍数关系,feat_stride在这里为16。

network.py文件相关代码(从Vgg16.py)跳转来:

 def _anchor_component(self):
 with tf.variable_scope('ANCHOR_' + 'default'):
 # just to get the shape right
 height = tf.to_int32(tf.ceil(self._im_info[0, 0] / np.float32(self._feat_stride[0])))
 width = tf.to_int32(tf.ceil(self._im_info[0, 1] / np.float32(self._feat_stride[0])))
 anchors, anchor_length = tf.py_func(generate_anchors_pre,
 [height, width,
 self._feat_stride, self._anchor_scales, self._anchor_ratios],
 [tf.float32, tf.int32], name="generate_anchors")
 anchors.set_shape([None, 4])
 anchor_length.set_shape([])
 self._anchors = anchors
 self._anchor_length = anchor_length

snippit()中相关代码:

def generate_anchors_pre(height, width, feat_stride, anchor_scales=(8, 16, 32), anchor_ratios=(0.5, 1, 2)):
 """ A wrapper function to generate anchors given different scales
 Also return the number of anchors in variable 'length'
 """
 anchors = generate_anchors(ratios=np.array(anchor_ratios), scales=np.array(anchor_scales))
 A = anchors.shape[0]
 shift_x = np.arange(0, width) * feat_stride
 shift_y = np.arange(0, height) * feat_stride
 shift_x, shift_y = np.meshgrid(shift_x, shift_y)
 shifts = np.vstack((shift_x.ravel(), shift_y.ravel(), shift_x.ravel(), shift_y.ravel())).transpose()
 K = shifts.shape[0]
 # width changes faster, so here it is H, W, C
 anchors = anchors.reshape((1, A, 4)) + shifts.reshape((1, K, 4)).transpose((1, 0, 2))
 anchors = anchors.reshape((K * A, 4)).astype(np.float32, copy=False)
 length = np.int32(anchors.shape[0])
 
 return anchors, length

我们现在再回到vgg16.py中的build_rpn()函数,看产生完9个候选框之后的操作。首先经过了一个3×3的卷积,之后用1×1的卷积去进行回归操作,分出前景或是背景,形成分数值,即rpn_cls_score_reshape。再通过softmax函数,得到rpn_clas_prob_reshape,之后,通过reshape化成了标准型,则,变为rpn_bbox_prob。

进行二分类操作和回归操作是并行的,于是用同样1×1的卷积去操作原来的future map,生成长度为4×k,即_num_anchors×4的长度。

最后,将二分类产生的参数以及回归任务产生的参数进行返回,Rpn层就建立好了。

① Proposal层的建立(build proposal)。

 def build_proposals(self, is_training, rpn_cls_prob, rpn_bbox_pred, rpn_cls_score):
 
 if is_training:
 rois, roi_scores = self._proposal_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
 rpn_labels = self._anchor_target_layer(rpn_cls_score, "anchor")
 
 # Try to have a deterministic order for the computing graph, for reproducibility
 with tf.control_dependencies([rpn_labels]):
 rois, _ = self._proposal_target_layer(rois, roi_scores, "rpn_rois")
 else:
 if cfg.FLAGS.test_mode == 'nms':
 rois, _ = self._proposal_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
 elif cfg.FLAGS.test_mode == 'top':
 rois, _ = self._proposal_top_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
 else:
 raise NotImplementedError
 return rois

依然是vgg16.py中的build_proposal函数,我们跳到_proposal_layer的函数中:

network.py:

 def _proposal_layer(self, rpn_cls_prob, rpn_bbox_pred, name):
 with tf.variable_scope(name):
 rois, rpn_scores = tf.py_func(proposal_layer,
 [rpn_cls_prob, rpn_bbox_pred, self._im_info, self._mode,
 self._feat_stride, self._anchors, self._num_anchors],
 [tf.float32, tf.float32])
 rois.set_shape([None, 5])
 rpn_scores.set_shape([None, 1])
 
 return rois, rpn_scores

其中核心代码为,tf.func()中的proposal_layer,我们继续跳入,proposal_layer.py中:

def proposal_layer(rpn_cls_prob, rpn_bbox_pred, im_info, cfg_key, _feat_stride, anchors, num_anchors):
 """A simplified version compared to fast/er RCNN
 For details please see the technical report
 """
 if type(cfg_key) == bytes:
 cfg_key = cfg_key.decode('utf-8')
 
 if cfg_key == "TRAIN":
 pre_nms_topN = cfg.FLAGS.rpn_train_pre_nms_top_n
 post_nms_topN = cfg.FLAGS.rpn_train_post_nms_top_n
 nms_thresh = cfg.FLAGS.rpn_train_nms_thresh
 else:
 pre_nms_topN = cfg.FLAGS.rpn_test_pre_nms_top_n
 post_nms_topN = cfg.FLAGS.rpn_test_post_nms_top_n
 nms_thresh = cfg.FLAGS.rpn_test_nms_thresh
 
 im_info = im_info[0]
 # Get the scores and bounding boxes
 scores = rpn_cls_prob[:, :, :, num_anchors:]
 rpn_bbox_pred = rpn_bbox_pred.reshape((-1, 4))
 scores = scores.reshape((-1, 1))
 proposals = bbox_transform_inv(anchors, rpn_bbox_pred)
 proposals = clip_boxes(proposals, im_info[:2])
 
 # Pick the top region proposals
 order = scores.ravel().argsort()[::-1]
 if pre_nms_topN > 0:
 order = order[:pre_nms_topN]
 proposals = proposals[order, :]
 scores = scores[order]
 
 # Non-maximal suppression
 keep = nms(np.hstack((proposals, scores)), nms_thresh)
 
 # Pick th top region proposals after NMS
 if post_nms_topN > 0:
 keep = keep[:post_nms_topN]
 proposals = proposals[keep, :]
 scores = scores[keep]
 
 # Only support single image as input
 batch_inds = np.zeros((proposals.shape[0], 1), dtype=np.float32)
 blob = np.hstack((batch_inds, proposals.astype(np.float32, copy=False)))
 
 return blob, scores

再来回忆一下,我们proposal_layer中做的事情:实际上,再proposal_layer中的任务主要就是筛选合适的框,缩小检测范围,那么,在前文回忆部分的步骤⑤中我们已经说到:第一,筛选与ground truth中,重叠率大于70%的候选框,筛掉其他的候选框,缩小范围;第二,用NMS非极大值抑制,筛选二分类中前n个score值的候选框;第三,筛掉越界框后,再来从前n个从大到小排序的值中筛选一次。好了,那么现在就严格按照这个步骤开始操作:

一开始先指定参数,我们刚才说进行了两次topN操作,所以设定两个参数,一个pre_num_topN和post_num_topN。bbox_transform中为调整框和ground truth大小位置的操作。进入bbox_transform函数:

可以看出,该公式调整的时候,先进行了整体平移,再进行了整体缩放,所以,在求出变换因子之后,求出,pred_ctr_x, pred_ctr_y, pred_w以及pred_h。然后返回两个坐标,(x1y1),(x2y2)。其中,变换调整到和ground truth差不多的大小。调整办法对应的是论文的上图部分。

代码如下:

bbox_transform.py:

def bbox_transform_inv(boxes, deltas):
 if boxes.shape[0] == 0:
 return np.zeros((0, deltas.shape[1]), dtype=deltas.dtype)
 
 boxes = boxes.astype(deltas.dtype, copy=False)
 widths = boxes[:, 2] - boxes[:, 0] + 1.0
 heights = boxes[:, 3] - boxes[:, 1] + 1.0
 ctr_x = boxes[:, 0] + 0.5 * widths
 ctr_y = boxes[:, 1] + 0.5 * heights
 
 dx = deltas[:, 0::4]
 dy = deltas[:, 1::4]
 dw = deltas[:, 2::4]
 dh = deltas[:, 3::4]
 
 pred_ctr_x = dx * widths[:, np.newaxis] + ctr_x[:, np.newaxis]
 pred_ctr_y = dy * heights[:, np.newaxis] + ctr_y[:, np.newaxis]
 pred_w = np.exp(dw) * widths[:, np.newaxis]
 pred_h = np.exp(dh) * heights[:, np.newaxis]
 
 pred_boxes = np.zeros(deltas.shape, dtype=deltas.dtype)
 # x1
 pred_boxes[:, 0::4] = pred_ctr_x - 0.5 * pred_w
 # y1
 pred_boxes[:, 1::4] = pred_ctr_y - 0.5 * pred_h
 # x2
 pred_boxes[:, 2::4] = pred_ctr_x + 0.5 * pred_w
 # y2
 pred_boxes[:, 3::4] = pred_ctr_y + 0.5 * pred_h
 
 return pred_boxes

之后,代码对框先进行了一下出界清除操作,筛掉出界的框,对应代码中clip_transform(),同时选取了前n个框。再接下来nms函数得到keep,之后,在通过topN操作得到非极大值抑制筛选后的框。

 # Non-maximal suppression
 keep = nms(np.hstack((proposals, scores)), nms_thresh)
 
 # Pick th top region proposals after NMS
 if post_nms_topN > 0:
 keep = keep[:post_nms_topN]
 proposals = proposals[keep, :]
 scores = scores[keep]

最后将所得到剩下的框返回,便得到了proposal层之后的留下的框。

接下来,就是筛出来IOU大于70%的框,于是:代码中,_anchor_target_layer()函数中,

 def _anchor_target_layer(self, rpn_cls_score, name):
 with tf.variable_scope(name):
 rpn_labels, rpn_bbox_targets, rpn_bbox_inside_weights, rpn_bbox_outside_weights = tf.py_func(
 anchor_target_layer,
 [rpn_cls_score, self._gt_boxes, self._im_info, self._feat_stride, self._anchors, self._num_anchors],
 [tf.float32, tf.float32, tf.float32, tf.float32])

在进入,anchor_target_layer.py中看一看相关的代码:

def anchor_target_layer(rpn_cls_score, gt_boxes, im_info, _feat_stride, all_anchors, num_anchors):
 """Same as the anchor target layer in original Fast/er RCNN """
 A = num_anchors
 total_anchors = all_anchors.shape[0]
 K = total_anchors / num_anchors
 im_info = im_info[0]
 
 # allow boxes to sit over the edge by a small amount
 _allowed_border = 0
 
 # map of shape (..., H, W)
 height, width = rpn_cls_score.shape[1:3]
 
 # only keep anchors inside the image
 inds_inside = np.where(
 (all_anchors[:, 0] >= -_allowed_border) &
 (all_anchors[:, 1] >= -_allowed_border) &
 (all_anchors[:, 2] < im_info[1] + _allowed_border) & # width
 (all_anchors[:, 3] < im_info[0] + _allowed_border) # height
 )[0]
 
 # keep only inside anchors
 anchors = all_anchors[inds_inside, :]
 
 # label: 1 is positive, 0 is negative, -1 is dont care
 labels = np.empty((len(inds_inside),), dtype=np.float32)
 labels.fill(-1)
 
 # overlaps between the anchors and the gt boxes
 # overlaps (ex, gt)
 overlaps = bbox_overlaps(
 np.ascontiguousarray(anchors, dtype=np.float),
 np.ascontiguousarray(gt_boxes, dtype=np.float))
 argmax_overlaps = overlaps.argmax(axis=1)
 max_overlaps = overlaps[np.arange(len(inds_inside)), argmax_overlaps]
 gt_argmax_overlaps = overlaps.argmax(axis=0)
 gt_max_overlaps = overlaps[gt_argmax_overlaps,
 np.arange(overlaps.shape[1])]
 gt_argmax_overlaps = np.where(overlaps == gt_max_overlaps)[0]
 
 if not cfg.FLAGS.rpn_clobber_positives:
 # assign bg labels first so that positive labels can clobber them
 # first set the negatives
 labels[max_overlaps < cfg.FLAGS.rpn_negative_overlap] = 0
 
 # fg label: for each gt, anchor with highest overlap
 labels[gt_argmax_overlaps] = 1
 
 # fg label: above threshold IOU
 labels[max_overlaps >= cfg.FLAGS.rpn_positive_overlap] = 1
 
 if cfg.FLAGS.rpn_clobber_positives:
 # assign bg labels last so that negative labels can clobber positives
 labels[max_overlaps < cfg.FLAGS.rpn_negative_overlap] = 0
 
 # subsample positive labels if we have too many
 num_fg = int(cfg.FLAGS.rpn_fg_fraction * cfg.FLAGS.rpn_batchsize)
 fg_inds = np.where(labels == 1)[0]
 if len(fg_inds) > num_fg:
 disable_inds = npr.choice(
 fg_inds, size=(len(fg_inds) - num_fg), replace=False)
 labels[disable_inds] = -1
 
 # subsample negative labels if we have too many
 num_bg = cfg.FLAGS.rpn_batchsize - np.sum(labels == 1)
 bg_inds = np.where(labels == 0)[0]
 if len(bg_inds) > num_bg:
 disable_inds = npr.choice(
 bg_inds, size=(len(bg_inds) - num_bg), replace=False)
 labels[disable_inds] = -1
 
 bbox_targets = _compute_targets(anchors, gt_boxes[argmax_overlaps, :])
 
 bbox_inside_weights = np.zeros((len(inds_inside), 4), dtype=np.float32)
 # only the positive ones have regression targets
 bbox_inside_weights[labels == 1, :] = np.array(cfg.FLAGS2["bbox_inside_weights"])
 
 bbox_outside_weights = np.zeros((len(inds_inside), 4), dtype=np.float32)
 if cfg.FLAGS.rpn_positive_weight < 0:
 # uniform weighting of examples (given non-uniform sampling)
 num_examples = np.sum(labels >= 0)
 positive_weights = np.ones((1, 4)) * 1.0 / num_examples
 negative_weights = np.ones((1, 4)) * 1.0 / num_examples
 else:
 assert ((cfg.FLAGS.rpn_positive_weight > 0) &
 (cfg.FLAGS.rpn_positive_weight < 1))
 positive_weights = (cfg.FLAGS.rpn_positive_weight /
 np.sum(labels == 1))
 negative_weights = ((1.0 - cfg.FLAGS.rpn_positive_weight) /
 np.sum(labels == 0))
 bbox_outside_weights[labels == 1, :] = positive_weights
 bbox_outside_weights[labels == 0, :] = negative_weights
 
 # map up to original set of anchors
 labels = _unmap(labels, total_anchors, inds_inside, fill=-1)
 bbox_targets = _unmap(bbox_targets, total_anchors, inds_inside, fill=0)
 bbox_inside_weights = _unmap(bbox_inside_weights, total_anchors, inds_inside, fill=0)
 bbox_outside_weights = _unmap(bbox_outside_weights, total_anchors, inds_inside, fill=0)
 
 # labels
 labels = labels.reshape((1, height, width, A)).transpose(0, 3, 1, 2)
 labels = labels.reshape((1, 1, A * height, width))
 rpn_labels = labels
 
 # bbox_targets
 bbox_targets = bbox_targets \
 .reshape((1, height, width, A * 4))
 
 rpn_bbox_targets = bbox_targets
 # bbox_inside_weights
 bbox_inside_weights = bbox_inside_weights \
 .reshape((1, height, width, A * 4))
 
 rpn_bbox_inside_weights = bbox_inside_weights
 
 # bbox_outside_weights
 bbox_outside_weights = bbox_outside_weights \
 .reshape((1, height, width, A * 4))
 
 rpn_bbox_outside_weights = bbox_outside_weights
 return rpn_labels, rpn_bbox_targets, rpn_bbox_inside_weights, rpn_bbox_outside_weights

相关推荐

得物可观测平台架构升级:基于GreptimeDB的全新监控体系实践

一、摘要在前端可观测分析场景中,需要实时观测并处理多地、多环境的运行情况,以保障Web应用和移动端的可用性与性能。传统方案往往依赖代理Agent→消息队列→流计算引擎→OLAP存储...

warm-flow新春版:网关直连和流程图重构

本期主要解决了网关直连和流程图重构,可以自此之后可支持各种复杂的网关混合、多网关直连使用。-新增Ruoyi-Vue-Plus优秀开源集成案例更新日志[feat]导入、导出和保存等新增json格式支持...

扣子空间体验报告

在数字化时代,智能工具的应用正不断拓展到我们工作和生活的各个角落。从任务规划到项目执行,再到任务管理,作者深入探讨了这款工具在不同场景下的表现和潜力。通过具体的应用实例,文章展示了扣子空间如何帮助用户...

spider-flow:开源的可视化方式定义爬虫方案

spider-flow简介spider-flow是一个爬虫平台,以可视化推拽方式定义爬取流程,无需代码即可实现一个爬虫服务。spider-flow特性支持css选择器、正则提取支持JSON/XML格式...

solon-flow 你好世界!

solon-flow是一个基础级的流处理引擎(可用于业务规则、决策处理、计算编排、流程审批等......)。提供有“开放式”驱动定制支持,像jdbc有mysql或pgsql等驱动,可...

新一代开源爬虫平台:SpiderFlow

SpiderFlow:新一代爬虫平台,以图形化方式定义爬虫流程,不写代码即可完成爬虫。-精选真开源,释放新价值。概览Spider-Flow是一个开源的、面向所有用户的Web端爬虫构建平台,它使用Ja...

通过 SQL 训练机器学习模型的引擎

关注薪资待遇的同学应该知道,机器学习相关的岗位工资普遍偏高啊。同时随着各种通用机器学习框架的出现,机器学习的门槛也在逐渐降低,训练一个简单的机器学习模型变得不那么难。但是不得不承认对于一些数据相关的工...

鼠须管输入法rime for Mac

鼠须管输入法forMac是一款十分新颖的跨平台输入法软件,全名是中州韵输入法引擎,鼠须管输入法mac版不仅仅是一个输入法,而是一个输入法算法框架。Rime的基础架构十分精良,一套算法支持了拼音、...

Go语言 1.20 版本正式发布:新版详细介绍

Go1.20简介最新的Go版本1.20在Go1.19发布六个月后发布。它的大部分更改都在工具链、运行时和库的实现中。一如既往,该版本保持了Go1的兼容性承诺。我们期望几乎所...

iOS 10平台SpriteKit新特性之Tile Maps(上)

简介苹果公司在WWDC2016大会上向人们展示了一大批新的好东西。其中之一就是SpriteKitTileEditor。这款工具易于上手,而且看起来速度特别快。在本教程中,你将了解关于TileE...

程序员简历例句—范例Java、Python、C++模板

个人简介通用简介:有良好的代码风格,通过添加注释提高代码可读性,注重代码质量,研读过XXX,XXX等多个开源项目源码从而学习增强代码的健壮性与扩展性。具备良好的代码编程习惯及文档编写能力,参与多个高...

Telerik UI for iOS Q3 2015正式发布

近日,TelerikUIforiOS正式发布了Q32015。新版本新增对XCode7、Swift2.0和iOS9的支持,同时还新增了对数轴、不连续的日期时间轴等;改进TKDataPoin...

ios使用ijkplayer+nginx进行视频直播

上两节,我们讲到使用nginx和ngixn的rtmp模块搭建直播的服务器,接着我们讲解了在Android使用ijkplayer来作为我们的视频直播播放器,整个过程中,需要注意的就是ijlplayer编...

IOS技术分享|iOS快速生成开发文档(一)

前言对于开发人员而言,文档的作用不言而喻。文档不仅可以提高软件开发效率,还能便于以后的软件开发、使用和维护。本文主要讲述Objective-C快速生成开发文档工具appledoc。简介apple...

macOS下配置VS Code C++开发环境

本文介绍在苹果macOS操作系统下,配置VisualStudioCode的C/C++开发环境的过程,本环境使用Clang/LLVM编译器和调试器。一、前置条件本文默认前置条件是,您的开发设备已...