参考:
https://blog.csdn.net/WYXHAHAHA123/article/details/86099768
https://blog.csdn.net/w55100/article/details/88529029
这一部分主要学习源码中数据加载的部分。
先给出 https://github.com/jwyang/faster-rcnn.pytorch 的代码文件夹结构:
其中faster-rcnn-pytorch-master文件夹直接包含了trainval_net.py和test.py,cfgs文件中给出的是对于网络的配置文件,以cfgs文件夹中的res101.yml为例子:
1.RPN_POSITIVE_OVERLAP: 0.7
表示训练RPN时,anchor boxes与ground truth之间的IOU大于0.7则标记为正样本。
2.RPN_BATCHSIZE: 256
表示训练RPN计算损失函数时,将按照faster R-CNN论文中所说,先按照anchor boxes与gt框之间的IOU进行对所有anchor boxes的正负样本划分,所有IOU阈值(overlap)大于0.7被标记为正样本,IOU阈值小于0.3则被标记为负样本,然后随机从所有正样本集和所有负样本集中分别选出128个正样本和128个负样本(比例1:1),计算损失函数。也就是说,对于RPN而言如果是输入一张图像进行训练,则训练RPN的batch size=256,相当于是输入了256个anchor框,而那一张训练图像仅仅是提供所有anchor 框抠取特征的shared feature map。
3.BATCH_SIZE: 128
表示一个batch训练128张图片。
4.POOLING_SIZE: 7
将所有尺寸不同的bounding boxes(这里指的是经过RPN输出后,可以认为此时RPN已经训练好,RPN的功能就类似于selective search算法的作用,为后面fast R-CNN的训练提供大约2000个region proposal)映射到shared feature map,从共享卷积特征图上抠取出特征图后,进行7*7的ROI align或者ROI pooling。
一、def combined_roidb(imdb_names, training=True):
接下来直接看trainval_net.py的main函数部分中的这个函数实现过程:**
imdb, roidb, ratio_list, ratio_index = combined_roidb(args.imdb_name)
使用pycharm的话可以直接用快捷键Ctrl + shift + F 全局查找函数名。
在roidb.py中找到该函数:
暂且忽略该函数内的def get_training_roidb(imdb)和def get_roidb(imdb_name),先看下面的部分。
此时传入实参在trainval_net.py中
其中def combined_roidb(imdb_names, training=True)的函数操作主要由以下4个部分组成:
我们按照顺序读下去,第一个函数是:
1. roidbs=get_roidb(imdb_name)
下面是函数get_roidb的代码:
**
1). imdb = get_imdb(imdb_name) ---factory.py
def get_imdb(name):将会返回一个pascal_voc类,在factory.py函数中有一个字典__sets,字典中的key对应与输入的训练数据集名称,value对应于实例化后的数据集类名称。
因为我们传入的实参为:imdb_name = args.imdb_name = 'voc_2007_trainval
所以我们使用的是VOC数据集,我们看factory.py中的代码:
__sets[name] = (lambda split=split, year=year:pascal_voc(split,year))
其中pascal_voc(split, year)
就是value所对应的数据集类的名称。
所以imdb = get_imdb(imdb_name)
使所有数据集的类class都是imdb class的子
类,能够继承imdb父类的所有方法。
,在pascal_voc.py中的class pascal_voc(imdb)是class imdb的子类。
注意:对于python中的class类,一旦实例化了这个类之后,就会调用class的init方法,在实例化之后的对象的相应属性位置填充content,而不会调用class中所包含的其他方法。
所以我们在imdb.py
中找到class imdb(object):
所以class pascal_voc(imdb)
要继承的是其中的init的方法:
class pascal_voc(imdb):
中重要的几个attribution是:
类的属性 | 数据类型 | 含义 |
---|---|---|
self._data_path | string | 包含训练数据集图像的绝对路径 |
self._class_to_ind | dict | 在python中将两个列表转换成一个字典将self.classes和xrange(self.numclasses)两个列表合并成一个字典,{‘__background‘:0,aeroplane’:1,’bicycle’:2} |
self._image_ext | string | 图像后缀名 ‘.jpg’ |
self._image_index | list | 从包含训练数据集所有图像的txt文件中读取每一行,向列表中append一个表项,列表中的元素顺序与txt文件中行数顺序一致,列表长度为训练数据集中图像数 |
我们看这几个重要的attribution的代码:
1.self._data_path = os.path.join(self._devkit_path, 'VOC' + self._year)
self._devkit_path
这个属性的方法是找到数据集存储位置的绝对路径,具体实现方法可以看def _get_default_path(self):
的实现方法。再将这些路径合起来之后就是训练数据集所有图像的绝对路径。
2.self._class_to_ind = dict(zip(self.classes, xrange(self.num_classes)))
xrange(self.num_classes)
是生成一个和_classes长度相同的range,具体实现过程在imdb.py
中的def num_classes()
函数实现。
3.self``._image_index = ``self``._load_image_set_index()
2). imdb.set_proposal_method(cfg.TRAIN.PROPOSAL_METHOD)
我们将这段代码分开看:
先是调用class imdb(object):
中的def set_proposal_method():
传入的参数为cfg.TRAIN.PROPOSAL_METHOD
,我们打开文件夹,可以看见cfgs文件夹中的.yml文件,例如打开res101.yml 文件,可以看见PROPOSAL_METHOD: gt
,所以整个代码可以改为:
imdb.set_proposal_method(gt)
所以最终该代码执行结果为:self.roidb_handler = self.gt_roidb
这里需要注意,由于self.gt_roidb是类中的方法,则这样的赋值之后是说self.roidb_handler方法与self.gt_roidb方法相同,如果改写成:self.roidb_handler = self.gt_roidb()
则所要表达的意思是self.roidb_handler的值是self.gt_roidb()方法的返回值,则self.roidb_handler可能就是属性而不是方法。
这段代码在阅读时,发现了一个开始没有弄明白的问题,我们仔细看:self.roidb_handler = self.gt_roidb
时,发现在父类中self.roidb_handler
是调用了其子类pascal_voc(imdb):
中的def ``gt_roidb``(``self``):
方法,这个好像和之间子类可以继承父类的属性,和调用父类中的方法不太一样,关于这一点可以这样解释:
本身来说class imdb(object):
是一个抽象基类,这意味着imbd
类只是用作其他类的基础,而不是直接实例化,因此你可以看见在imdb.py
代码中的第35行:
self._roidb_handler = self.default_roidb
和后面对类中方法的定义:
如果直接对该类进行实例化的话,会出现NotImplementedError
的报错。
所以说,我们先使用该类中的def ``set_proposal_method``(``self``, ``method):
的方法,使得这样赋值self.roidb_handler = self.gt_roidb
之后,def roidb_handler():
方法不再执行以上会报错的代码,从而完成替换。
3). roidb = get_training_roidb(imdb)
上面两个步骤已经说明,现在的**imdb**
对象是**pascal voc**
类,也是**imdb**
类的子类。
这句话我是这样理解的,现在的`imdb`类由于之前的赋值(Python中能进行类对类的覆盖赋值,应该是这样?)这段代码:imdb = get_imdb(imdb_name)
,使得`imdb`对象是`pascal_voc类`,即`imdb`的子类,又由于该代码imdb.set_proposal_method(cfg.TRAIN.PROPOSAL_METHOD)
**
使得原imdb的一些方法被改变,所以说现在的imdb类可以看做一个新的pascal_voc
类,其包含原pascal_voc
的属性和方法,继承自父类的属性和方法,以及父类中被修改的新方法。
def append_flipped_images(self):
是imdb
类中的方法,num_images
表示当前训练数据集中的图像数。
1.num_images = self.num_images
我们可以看一下这个函数具体的实现过程,可以对上面的解释的理解更加清楚一点:
num_images = self.num_images
调用imdb
类中的num_images
方法:
返回值是len(self.image_index)
,我们再找到此方法:
返回self._image_index
,那么这个属性又在哪里了?我们可以找到两个分别属于imdb
类的属性和pascal_voc
类的属性,那么应该是哪一个哪呢?
imdb
类中的self._image_index
是一个空列表:
pascal_voc
类中的self._image_index
为:
因为现在的imdb是一个新的pascal_voc类
:其包含原pascal_voc
的属性和方法,继承自父类的属性和方法,以及父类中被修改的新方法。所以使用imdb中的属性和方法时,如果父类和子类具有相同名称的方法,则实例化子类后的对象调用该方法时,将会调用子类中的相应方法。
即使用self._load_image_set_index()
方法:**
所以num_images表示当前训练数据集中的图像数。
2.widths = ``self``._get_widths()
吐槽一句读这个代码让我想起来一个词——反复横跳!!!!!!!!!
**
去找self._get_widths()方法
,就在def def append_flipped_images(self):上面
:
So,go to(pascal_voc.py
) :def image_path_at(self, i):
Then go to :def image_path_from_index(self, index):
这段代码整个的意思就是:获取每一张图片的绝对路径,然后使用PIL库读取图像的width,组成一个列表。
3.接下来我们来看这个for循环
这里有一个问题:self.roidb是什么时候被填充内容的呢?
此时我们看imdb类中的这段代码:
@property 具有将类中的方法封装成属性的功能,虽然封装成属性,但是本质还是方法,属性一般在实例化的时候 如果默认调用的初始化函数没有赋值 那么根据语法给值。虽然方法被封装成了属性,但是每次访问这个属性的时候,都会调用类中的方法。准确的说应该是用了@property 只是让方法能够以属性的语法操作,所以你访问这个属性,其实解释器会调用该方法。每次访问属性,都会调用相应的方法,属性的值是方法的返回值。
如果现在训练数据集中有886张图像,而且每次调用这个@property 方法时都进行计数,则一共会调用886*4=3544次方法。
之前提到imdb
对象中self.roidb_handler(imdb类的方法)= self.gt_roidb()
(pascal_voc类的方法)
以上代码的意思是:从XML文件中 加载出ground truth的标签数据。我们直接直接看其中的这一段代码:gt_roidb = [self._load_pascal_annotation(index) for index in self.image_index]
So go to: def ``_load_pascal_annotation``(``self``, ``index):
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')
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)
ishards = np.zeros((num_objs), dtype=np.int32)
# 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
diffc = obj.find('difficult')
difficult = 0 if diffc == None else int(diffc.text)
ishards[ix] = difficult
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_ishard': ishards,
'gt_overlaps': overlaps,
'flipped': False,
'seg_areas': seg_areas}
返回值self.roidb是一个字典列表,即还是列表类型,列表中的每个元素是一个字典,列表索引顺序对应的数据集中图像顺序与self.image_index列表一致。列表中的每个元素给出了当前训练图像的ground truth标签信息。
imdb.roidb[i] dict中key value(加入水平flip操作后)
key | data_type & shape | value |
---|---|---|
‘boxes’ | numpy.ndarray [num_objs, 4] | 表示当前训练图像中所有ground truth包围框的坐标信息,在原始输入图像分辨率上,坐标格式[xmin,ymin,xmax,ymax] |
‘gt_classes’ | numpy.ndarray [num_objs] | 表示训练图像中所有ground truth包围框的类别信息,其中数值与imdb._class_to_ind相应数值对应 |
‘gt_overlaps’ | numpy.ndarray [num_objs, self.num_classes] | 表示训练图像中每个ground truth包围框的类别,如果某个gt框属于该类别,则数值为1,否则为0,稀疏矩阵 |
‘seg_areas’ | numpy.ndarray [num_objs] | 表示训练图像中每个ground truth包围框的面积 |
‘flipped’ | bool | 表示训练图像是否需要水平翻转 |
--------------- | 下面的key和value是在def prepare_roidb(imdb) 中产生 |
-------------- |
‘img_id’ | scalar | 图像在image_index列表中的序号,经过水平翻转后,self.image_index列表长度是之前的2倍,相当于将之前的列表复制一份,再append到原始的列表后面 |
‘image’ | string | 当前训练图像的完整绝对路径(包含后缀名) |
‘width’ | scalar | 图像宽度 |
‘height’ | scalar | 图像高度 |
经过上述的学习,我们终于把append_flipped_image()
这个方法阐述完成了。
下面我们来看prepare_roidb(imd)
,其作用在函数注释中写的很清楚了:
"""Enrich the imdb's roidb by adding some derived quantities thatare useful for training. This function precomputes the maximumoverlap, taken over ground-truth boxes, between each ROI andeach ground-truth box. The class with maximum overlap is alsorecorded."""
就是对字典列表再增加一些key和value,在以后训练用。
总结:以上是对这个函数的阐述,其实其最终的目的就是ROI 中的database 赋值给roidb.
上述步骤结束后,imdb.roidb就会被赋值给roidb
2.imdb = get_imdb(imdb_names)
这个imdn
就是包含所有训练集的图片数据。
3.roidb = filter_roidb(roidb)
过滤掉所有没有Bounding Box 的图片,排除人工录入txt等环节的错误,注意经过这个环节之后,roidb
开始与self._image_index
不再一一对应。
4.ratio_list, ratio_index = rank_roidb_ratio(roidb)
方法的功能是希望训练时,所有输入图像的宽高比能够介于0.5到2之间。
ratio_list返回升序后的宽高比数值,ratio_index为升序后的图像编号(在长度为len(roidb)的编号)。
5.总结
- 上面四个函数组成了完整的预处理roidb功能
- 回顾一下,首先是get_roidb(),在这个函数内调用get_imdb实例化一个Myim类
- 并对这个imdb进行训练化处理get_training_roidb(),在此过程中会prepare_roidb()加几个字段
- 处理好之后,就返回了imdb.roidb