最近开始学习目标检测faster rcnn,首先看了很多博客讲解原理,然后从github上下载tensorflow版本的代码,代码太长看了好几天没明白,后来看到了chenyuntc的 simple-faster-rcnn-pytorch,还有作者写这份代码的心得,让我感觉很佩服,自认为目前阶段不能手写如此复杂的代码。作者是从tf版本的改为pytorch版的,我在学习的过程中也查阅了很多其他人写的讲解代码的博客,得到了很大的帮助,所以也打算把自己一些粗浅的理解记录下来,一是记录下自己的菜鸟学习之路,方便自己过后查阅,二来可以回馈网络。目前编程能力有限,且是第一次写博客,中间可能会有一些错误。
目录
第一步 跑通代码
第二步 数据预处理
第三步 模型准备
第四步 训练代码
第一步 跑通代码
作者的README.MD部分讲解的很清楚了,一步一步安装PyTorch,cupy,然后运行pip install -r requirements.txt安装各种包。我用的是自己的台式机,1070GPU,python3.6,windows环境。安装PyTorch>=0.4时上官网根据自己的环境去找对应的pip指令安装时可能下载速度巨慢,我是直接下载了torch-0.4.1-cp36-cp36m-win_amd64.whl,运行pip install torch-0.4.1-cp36-cp36m-win_amd64.whl安装的,这里我将文件放到云盘上有需要自取https://pan.baidu.com/s/1pqSpeTFH3ooh7q1M3vrtfQ,提取码:hda0。cupy安装时出错了,提示我需要安装VS,注意最好安装vs2015以上版本。接下来是build cython code nms_gpu_post
,这里我一直报错,后来发现是可选择运行的,我就直接跳过了,不过作者说最好运行这一步。然后是下载预训练模型,数据集,解压数据集操作。需要将util/config.py中voc_data_dir改为自己的路径,通过执行python misc/convert_caffe_pretrain.py,下载caffe_pretrain预训练模型,我是直接将caffe_pretrain改为True了,如果设置false则直接会下载pytorch版本的预训练模型。然后在终端下运行python -m visdom.server,弹出一个网址打开。visdom是类似tensorflow中的tensorboard的工具,可以可视化训练过程中的各种曲线或者图,然后运行python train. py train --env=‘fasterrcnn-caffe’ --plot-every=100 --caffe-pretrain,就开始了训练过程,可以看到网页中出现了5个loss曲线图,还有一个标签图和预测图。类似下面这样的
第二步 数据预处理
1.data/dataset.py文件(主代码中调用的其他重要函数会在后面按顺序讲解)
#去正则化,img维度为[[B,G,R],H,W],因为caffe预训练模型输入为BGR 0-255图片,pytorch预训练模型采用RGB 0-1图片
def inverse_normalize(img):
if opt.caffe_pretrain: #如果采用caffe预训练模型,则返回 img[::-1, :, :],如果不采用,则返回(img * 0.225 + 0.45).clip(min=0, max=1) * 255
img = img + (np.array([122.7717, 115.9465, 102.9801]).reshape(3, 1, 1)) #[122.7717, 115.9465, 102.9801]reshape为[3,1,1]与img维度相同就可以相加了,caffe_normalize之前有减均值预处理,现在还原回去。
return img[::-1, :, :] #将BGR转换为RGB图片(python [::-1]为逆序输出)
return (img * 0.225 + 0.45).clip(min=0, max=1) * 255 #pytorch_normalze中标准化为减均值除以标准差,现在乘以标准差加上均值还原回去,转换为0-255
#采用pytorch预训练模型对图片预处理,函数输入的img为0-1
def pytorch_normalze(img):
normalize = tvtsf.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) #transforms.Normalize使用如下公式进行归一化channel=(channel-mean)/std,转换为[-1,1]
img = normalize(t.from_numpy(img)) #(ndarray) → Tensor
return img.numpy()
#采用caffe预训练模型时对输入图像进行标准化,函数输入的img为0-1
def caffe_normalize(img):
img = img[[2, 1, 0], :, :] # RGB-BGR
img = img * 255
mean = np.array([122.7717, 115.9465, 102.9801]).reshape(3, 1, 1) #转换为与img维度相同
img = (img - mean).astype(np.float32, copy=True) #减均值操作
return img
#函数输入的img为0-255
def preprocess(img, min_size=600, max_size=1000): #按照论文长边不超1000,短边不超600。按此比例缩放
C, H, W = img.shape
scale1 = min_size / min(H, W)
scale2 = max_size / max(H, W)
scale = min(scale1, scale2) #选小的比例,这样长和宽都能放缩到规定的尺寸
img = img / 255 #转换为0-1
img = sktsf.resize(img, (C, H * scale, W * scale), mode='reflect',anti_aliasing=False) #resize到(H * scale, W * scale)大小,anti_aliasing为是否采用高斯滤波
#调用pytorch_normalze或者caffe_normalze对图像进行正则化
if opt.caffe_pretrain:
normalize = caffe_normalize
else:
normalize = pytorch_normalze
return normalize(img)
class Transform(object):
def __init__(self, min_size=600, max_size=1000):
self.min_size = min_size
self.max_size = max_size
def __call__(self, in_data):
img, bbox, label = in_data
_, H, W = img.shape
img = preprocess(img, self.min_size, self.max_size) #图像等比例缩放
_, o_H, o_W = img.shape
scale = o_H / H #得出缩放比因子
bbox = util.resize_bbox(bbox, (H, W), (o_H, o_W)) #bbox按照与原图等比例缩放
# horizontally flip
img, params = util.random_flip(
img, x_random=True, return_param=True) #将图片进行随机水平翻转,没有进行垂直翻转
bbox = util.flip_bbox(
bbox, (o_H, o_W), x_flip=params['x_flip']) #同样地将bbox进行与对应图片同样的水平翻转
return img, bbox, label, scale
class Dataset: #训练集样本的生成
def __init__(self, opt):
self.opt = opt
self.db = VOCBboxDataset(opt.voc_data_dir) #实例化类
self.tsf = Transform(opt.min_size, opt.max_size) ##实例化类
def __getitem__(self, idx): #__ xxx__运行Dataset类时自动运行
ori_img, bbox, label, difficult = self.db.get_example(idx) #调用VOCBboxDataset中的get_example()从数据集存储路径中将img, bbox, label, difficult 一个个的获取出来
img, bbox, label, scale = self.tsf((ori_img, bbox, label)) #调用前面的Transform函数将图片,label进行最小值最大值放缩归一化,重新调整bboxes的大小,然后随机反转,最后将数据集返回
return img.copy(), bbox.copy(), label.copy(), scale
def __len__(self):
return len(self.db)
class TestDataset: #测试集样本的生成
def __init__(self, opt, split='test', use_difficult=True):
self.opt = opt
self.db = VOCBboxDataset(opt.voc_data_dir, split=split, use_difficult=use_difficult) #此处设置了use_difficult,
def __getitem__(self, idx):
ori_img, bbox, label, difficult = self.db.get_example(idx)
img = preprocess(ori_img)
return img, ori_img.shape[1:], bbox, label, difficult
def __len__(self):
return len(self.db)
下面是data/util.py文件
def resize_bbox(bbox, in_size, out_size):
bbox = bbox.copy()
y_scale = float(out_size[0]) / in_size[0]
x_scale = float(out_size[1]) / in_size[1] #获得与原图同样的缩放比
bbox[:, 0] = y_scale * bbox[:, 0]
bbox[:, 2] = y_scale * bbox[:, 2]
bbox[:, 1] = x_scale * bbox[:, 1]
bbox[:, 3] = x_scale * bbox[:, 3] #按与原图同等比例缩放bbox
return bbox
def random_flip(img, y_random=False, x_random=False,
return_param=False, copy=False):
y_flip, x_flip = False, False
if y_random: #False
y_flip = random.choice([True, False])
if x_random: #True
x_flip = random.choice([True, False]) #随机选择图片是否进行水平翻转
if y_flip:
img = img[:, ::-1, :]
if x_flip:
img = img[:, :, ::-1] #python [::-1]为逆序输出,这里指水平翻转
if copy:
img = img.copy()
if return_param: #True
return img, {'y_flip': y_flip, 'x_flip': x_flip} #返回img和x_flip(为了让bbox有同样的水平翻转操作)
else:
return img
def flip_bbox(bbox, size, y_flip=False, x_flip=False):
H, W = size #缩放后图片的size
bbox = bbox.copy()
if y_flip: #没有进行垂直翻转
y_max = H - bbox[:, 0]
y_min = H - bbox[:, 2]
bbox[:, 0] = y_min
bbox[:, 2] = y_max
if x_flip:
x_max = W - bbox[:, 1]
x_min = W - bbox[:, 3] #计算水平翻转后左下角和右上角的坐标
bbox[:, 1] = x_min
bbox[:, 3] = x_max
return bbox
下面是data/voc_dataset.py文件
class VOCBboxDataset:
def __init__(self, data_dir, split='trainval',
use_difficult=False, return_difficult=False,
):
id_list_file = os.path.join(
data_dir, 'ImageSets/Main/{0}.txt'.format(split)) # id_list_file为split.txt,split为'trainval'或者'test'
self.ids = [id_.strip() for id_ in open(id_list_file)] #id_为每个样本文件名
self.data_dir = data_dir #写到/VOC2007/的路径
self.use_difficult = use_difficult
self.return_difficult = return_difficult
self.label_names = VOC_BBOX_LABEL_NAMES #20类
def __len__(self):
return len(self.ids) #trainval.txt有5011个,test.txt有210个
def get_example(self, i):
id_ = self.ids[i]
anno = ET.parse(
os.path.join(self.data_dir, 'Annotations', id_ + '.xml')) #读入 xml标签文件
bbox = list()
label = list()
difficult = list()
#解析xml文件
for obj in anno.findall('object'):
if not self.use_difficult and int(obj.find('difficult').text) == 1: #标为difficult的目标在测试评估中一般会被忽略
continue #xml文件中包含object name和difficult(0或者1,0代表容易检测)
difficult.append(int(obj.find('difficult').text))
bndbox_anno = obj.find('bndbox') #bndbox(xmin,ymin,xmax,ymax),表示框左下角和右上角坐标
bbox.append([
int(bndbox_anno.find(tag).text) - 1
for tag in ('ymin', 'xmin', 'ymax', 'xmax')]) #让坐标基于(0,0)
name = obj.find('name').text.lower().strip() #框中object name
label.append(VOC_BBOX_LABEL_NAMES.index(name))
bbox = np.stack(bbox).astype(np.float32) #所有object的bbox坐标存在列表里
label = np.stack(label).astype(np.int32) #所有object的label存在列表里
# When `use_difficult==False`, all elements in `difficult` are False.
difficult = np.array(difficult, dtype=np.bool).astype(np.uint8) # PyTorch 不支持 np.bool,所以这里转换为uint8
img_file = os.path.join(self.data_dir, 'JPEGImages', id_ + '.jpg') #根据图片编号在/JPEGImages/取图片
img = read_image(img_file, color=True) #如果color=True,则转换为RGB图
return img, bbox, label, difficult
__getitem__ = get_example #一般如果想使用索引访问元素时,就可以在类中定义这个方法(__getitem__(self, key) )
以上部分就是全部的数据预处理内容,这部分不太难,还是挺好理解的。下面一部分是模型准备部分。