参考:

https://blog.csdn.net/WYXHAHAHA123/article/details/86099768
https://blog.csdn.net/w55100/article/details/88529029

这一部分主要学习源码中数据加载的部分。
先给出 https://github.com/jwyang/faster-rcnn.pytorch 的代码文件夹结构:

Faster R-CNN源码学习(一)——训练数据加载 - 图1

其中faster-rcnn-pytorch-master文件夹直接包含了trainval_net.py和test.py,cfgs文件中给出的是对于网络的配置文件,以cfgs文件夹中的res101.yml为例子:

Faster R-CNN源码学习(一)——训练数据加载 - 图2

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中找到该函数:

Faster R-CNN源码学习(一)——训练数据加载 - 图3
Faster R-CNN源码学习(一)——训练数据加载 - 图4

暂且忽略该函数内的def get_training_roidb(imdb)和def get_roidb(imdb_name),先看下面的部分。

此时传入实参在trainval_net.py中

Faster R-CNN源码学习(一)——训练数据加载 - 图5

其中def combined_roidb(imdb_names, training=True)的函数操作主要由以下4个部分组成:

Faster R-CNN源码学习(一)——训练数据加载 - 图6

我们按照顺序读下去,第一个函数是:

1. roidbs=get_roidb(imdb_name) 

下面是函数get_roidb的代码:
**
Faster R-CNN源码学习(一)——训练数据加载 - 图7

1). imdb = get_imdb(imdb_name)   ---factory.py

Screenshot from 2020-01-22 21-23-57.png

def get_imdb(name):将会返回一个pascal_voc类,在factory.py函数中有一个字典__sets,字典中的key对应与输入的训练数据集名称,value对应于实例化后的数据集类名称。

因为我们传入的实参为:
imdb_name = args.imdb_name = 'voc_2007_trainval
所以我们使用的是VOC数据集,我们看factory.py中的代码:
Screenshot from 2020-01-22 21-33-12.png

  1. __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父类的所有方法。

Screenshot from 2020-01-22 21-45-10.png,在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的方法:
Screenshot from 2020-01-22 21-57-51.png

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()函数实现。

Screenshot from 2020-01-22 22-26-02.png

3.self``._image_index = ``self``._load_image_set_index()

Screenshot from 2020-01-22 22-27-23.png

2). imdb.set_proposal_method(cfg.TRAIN.PROPOSAL_METHOD)

我们将这段代码分开看:
先是调用class imdb(object):中的def set_proposal_method():

Screenshot from 2020-01-22 23-36-10.png

传入的参数为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
和后面对类中方法的定义:
Screenshot from 2020-01-23 01-44-45.png

Screenshot from 2020-01-23 01-44-52.png
如果直接对该类进行实例化的话,会出现NotImplementedError的报错。

所以说,我们先使用该类中的def ``set_proposal_method``(``self``, ``method):的方法,使得这样赋值self.roidb_handler = self.gt_roidb之后,def roidb_handler():方法不再执行以上会报错的代码,从而完成替换。


3). roidb = get_training_roidb(imdb)

Screenshot from 2020-01-23 09-02-47.png

上面两个步骤已经说明,现在的**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的属性和方法,继承自父类的属性和方法,以及父类中被修改的新方法。

Screenshot from 2020-01-23 09-31-49.png

def append_flipped_images(self):imdb类中的方法,num_images表示当前训练数据集中的图像数。

1.num_images = self.num_images

我们可以看一下这个函数具体的实现过程,可以对上面的解释的理解更加清楚一点:

num_images = self.num_images

调用imdb类中的num_images方法:

Screenshot from 2020-01-23 17-01-37.png

返回值是len(self.image_index),我们再找到此方法:

Screenshot from 2020-01-23 17-04-20.png

返回self._image_index,那么这个属性又在哪里了?我们可以找到两个分别属于imdb类的属性和pascal_voc类的属性,那么应该是哪一个哪呢?

imdb类中的self._image_index是一个空列表:Screenshot from 2020-01-23 17-11-30.png

pascal_voc类中的self._image_index为:

Screenshot from 2020-01-23 17-12-19.png

因为现在的imdb是一个新的pascal_voc类:其包含原pascal_voc的属性和方法,继承自父类的属性和方法,以及父类中被修改的新方法。所以使用imdb中的属性和方法时,如果父类和子类具有相同名称的方法,则实例化子类后的对象调用该方法时,将会调用子类中的相应方法。

即使用self._load_image_set_index()方法:**

Screenshot from 2020-01-23 17-15-34.png

所以num_images表示当前训练数据集中的图像数。

2.widths = ``self``._get_widths()

吐槽一句读这个代码让我想起来一个词——反复横跳!!!!!!!!!
**
去找self._get_widths()方法,就在def def append_flipped_images(self):上面

Screenshot from 2020-01-23 17-28-06.png

So,go to(pascal_voc.py) :def image_path_at(self, i):

Screenshot from 2020-01-23 17-30-06.png

Then go to :def image_path_from_index(self, index):

Screenshot from 2020-01-23 17-31-59.png

这段代码整个的意思就是:获取每一张图片的绝对路径,然后使用PIL库读取图像的width,组成一个列表。

3.接下来我们来看这个for循环
Screenshot from 2020-01-23 09-31-49.png

这里有一个问题:self.roidb是什么时候被填充内容的呢?
此时我们看imdb类中的这段代码:

Screenshot from 2020-01-23 18-44-18.png

@property 具有将类中的方法封装成属性的功能,虽然封装成属性,但是本质还是方法,属性一般在实例化的时候 如果默认调用的初始化函数没有赋值 那么根据语法给值。虽然方法被封装成了属性,但是每次访问这个属性的时候,都会调用类中的方法。准确的说应该是用了@property 只是让方法能够以属性的语法操作,所以你访问这个属性,其实解释器会调用该方法。每次访问属性,都会调用相应的方法,属性的值是方法的返回值。

如果现在训练数据集中有886张图像,而且每次调用这个@property 方法时都进行计数,则一共会调用886*4=3544次方法

之前提到imdb对象中self.roidb_handler(imdb类的方法)= self.gt_roidb() (pascal_voc类的方法)

Screenshot from 2020-01-23 18-56-36.png

以上代码的意思是:从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):

  1. def _load_pascal_annotation(self, index):
  2. """
  3. Load image and bounding boxes info from XML file in the PASCAL VOC
  4. format.
  5. """
  6. filename = os.path.join(self._data_path, 'Annotations', index + '.xml')
  7. tree = ET.parse(filename)
  8. objs = tree.findall('object')
  9. num_objs = len(objs)
  10. boxes = np.zeros((num_objs, 4), dtype=np.uint16)
  11. gt_classes = np.zeros((num_objs), dtype=np.int32)
  12. overlaps = np.zeros((num_objs, self.num_classes), dtype=np.float32)
  13. # "Seg" area for pascal is just the box area
  14. seg_areas = np.zeros((num_objs), dtype=np.float32)
  15. ishards = np.zeros((num_objs), dtype=np.int32)
  16. # Load object bounding boxes into a data frame.
  17. for ix, obj in enumerate(objs):
  18. bbox = obj.find('bndbox')
  19. # Make pixel indexes 0-based
  20. x1 = float(bbox.find('xmin').text) - 1
  21. y1 = float(bbox.find('ymin').text) - 1
  22. x2 = float(bbox.find('xmax').text) - 1
  23. y2 = float(bbox.find('ymax').text) - 1
  24. diffc = obj.find('difficult')
  25. difficult = 0 if diffc == None else int(diffc.text)
  26. ishards[ix] = difficult
  27. cls = self._class_to_ind[obj.find('name').text.lower().strip()]
  28. boxes[ix, :] = [x1, y1, x2, y2]
  29. gt_classes[ix] = cls
  30. overlaps[ix, cls] = 1.0
  31. seg_areas[ix] = (x2 - x1 + 1) * (y2 - y1 + 1)
  32. overlaps = scipy.sparse.csr_matrix(overlaps)
  33. return {'boxes': boxes,
  34. 'gt_classes': gt_classes,
  35. 'gt_ishard': ishards,
  36. 'gt_overlaps': overlaps,
  37. 'flipped': False,
  38. '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 图像高度

Screenshot from 2020-01-23 09-02-47.png

经过上述的学习,我们终于把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.

Faster R-CNN源码学习(一)——训练数据加载 - 图31


上述步骤结束后,imdb.roidb就会被赋值给roidb

2.imdb = get_imdb(imdb_names)

这个imdn就是包含所有训练集的图片数据。

3.roidb = filter_roidb(roidb)

Screenshot from 2020-01-25 15-58-16.png

过滤掉所有没有Bounding Box 的图片,排除人工录入txt等环节的错误,注意经过这个环节之后,roidb开始与self._image_index不再一一对应。

4.ratio_list, ratio_index = rank_roidb_ratio(roidb)

Screenshot from 2020-01-25 16-05-43.png

方法的功能是希望训练时,所有输入图像的宽高比能够介于0.5到2之间。

ratio_list返回升序后的宽高比数值,ratio_index为升序后的图像编号(在长度为len(roidb)的编号)。


5.总结

  1. 上面四个函数组成了完整的预处理roidb功能
  2. 回顾一下,首先是get_roidb(),在这个函数内调用get_imdb实例化一个Myim类
  3. 并对这个imdb进行训练化处理get_training_roidb(),在此过程中会prepare_roidb()加几个字段
  4. 处理好之后,就返回了imdb.roidb