pytorch实现faster_rcnn 1 之实现到到RPN和proposalTargetCreator

在研究了yolov3和ssd之后,现在又开始研究一下faster_rcnn了,百度了一下,对于pytorch实现faster_rcnn的代码解析的文章貌似不是很多,而且也不像yolov3和ssd那么详细,可能真的跟faster_rcnn现在用的人比较少的原因有关了吧,但是,可能是由于强迫症的原因,我总想把faster_rcnn的原理像ssd和yolov3那样用代码理解一遍,这里,推荐一篇文章:https://zhuanlan.zhihu.com/p/32404424,这里面作者将整个代码的流程讲的还是很详细了,大家看我下面的文字代码之前,建议去看一看这篇文章,这个作者代码流程写的很详细了,但是作者放到github上的代码,我看着有些难受,比我之前看ssd和yolov3的源代码都难受,可能跟我水平有关,反正之前也把ssd和yolov3的原理用代码复现了,这些目标检测很多东西都是互通的,所以,决定直接根据这篇作者对代码的流程的描述来自己敲代码了,最后,我的代码,只能说让你对整个faster_rcnn的原理理解更清楚,真的要拿去训练模型,我没有那个把整个流程都写出来的想法,只想着把核心的部分写出来,梳理自己的思路,锻炼自己的代码能力,而且我也不能保证对上面作者的说法理解全部正确,所以,你要是用faster_rcnn训练模型,你还是自己去github找大佬写的很完备代码来训练模型吧,其实,换句话,现在faster_rcnn的优化版本已经很多了,也没有太多的必要真用最初版的faster_rcnn也就是我下面写的这个版本来训练模型。顺便说明一下,一下的截图大多数都来自上面的作者。

好了,正题开始,我们直接看看整个模型的结构:
在这里插入图片描述
在第一个feaature输出之前的部分,都是vgg16的网络模型中的一部分,所以我们到时候直接利用pytorch自带的vgg模型来创建主体部分的网络,然后对于第二部分的rpn,其实就是对于预测的anchor进行筛选,对这些anchor进行删选之后,我们要开始进行一个类似抠图的操作,这操作在roi pooling里面,不属于这篇文章的讨论内容,下一篇再说,我们今年就来展示怎么把网络搭建到sample_rois这里。

我们直接看这一部分代码:

'''author:nike hu'''
class Faster_rcnn(nn.Module):
    def __init__(self):
        super(Faster_rcnn, self).__init__()
        self.net_body = Model.vgg16().features[0:30] # 这里截取官方给的vgg模型的前30层,到这一层就是主体部分
        '''接下来构建RPN网络,这里的网络有分支,所以不好用一个nn.sequential直接操作'''
        self.RPN_1 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
            nn.ReLU(inplace=True)
        )
        self.RPN_2_1 = nn.Sequential(
            nn.Conv2d(512, 18, kernel_size=1), # 输出维度为[b, 18, w, h], 18=9×2(9个anchor,每个anchor二分类,使用交叉熵损失),
            nn.ReLU(True) # 这个必须得跟上,否者最后模型跑出的值会有负数
        )
        self.RPN_2_2 = nn.Sequential(
            nn.Conv2d(512, 36, kernel_size=1), # 进行回归的卷积核通道数为36=9×4(9个anchor,每个anchor有4个位置参数)
            nn.ReLU(True)
        )



    def forward(self, x):
        x = self.net_body(x)
        x = self.RPN_1(x)
        x1 = self.RPN_2_1(x)
        x2 = self.RPN_2_2(x)

这里会输出两个值,对应到上面那张图,这里得网络对应RPN网络的第二层,那么,接下来我们就要另外采取操作对这两个输出的值进行处理了。对于rpn中的rpn_loss部分,我们后续进行处理,咱们先进行RPN中的proposalCreator的处理。

咱们先回忆一下这一部分的处理流程,对流程完全没有印象的去看看我推荐的那篇文章,整个流程是这样:我们经过网络的向前推进后会输出一个维度为[b, 18, w, h]和[b, 36, w, h]的维度的数据,里面18=92, 36=94, 9代表每个anchor有九个尺寸,2代表背景和前景的概率,4代表每个anchor对应的中心位置和长宽,但是注意,这里得中心位置和长宽不代表anchor在featuremap中的真正的中心位置和长宽,你可以把他理解为中心位置和长宽转化的一个关系,那么,问题来了,在筛选这些anchor之前,我们必须把输出数据中的中心坐标和长宽转化为真正的坐标和长宽,所以,我们得处理以下几个问题:
1:每个anchor有9个尺寸,我们必须把尺寸求出来
2:将真正的坐标和长宽求出来

对于第1个问题,代码如下:

'''author:nike hu'''
# 这个函数是用来获取9个anchor的各个尺寸的
def get_anchor():
    anchor = (128, 256, 512) # 这是每个anchor的总面积等于里面数值的平方,每种anchor的三个尺寸围成的面积一样
    Aspect_ratio = (1, 0.5, 2) # 这是长宽比
    anchors = []
    for anchor_long in anchor:
        for ratio in Aspect_ratio:
            if ratio == 1: # 根据不同的长宽比来进行处理
                anchors.append((anchor_long / 16, anchor_long / 16))
            if ratio == 0.5:
                x = math.sqrt((anchor_long * anchor_long) / 2)
                anchors.append((x / 16, 2 * (x /16)))
            if ratio == 2:
                x = math.sqrt((anchor_long * anchor_long) / 2)
                anchors.append((2 * (x / 16), x / 16))
             # 除以16是由于在主干网络输出的时候,图像尺寸缩小了16倍
    return anchors

对于第二个问题,代码如下:

'''author:nike hu'''
# 这个函数是在RPN模块中对预测出来的四个坐标值进行转化
def get_loc(prediction):
    '''这里得prediction的维度是[b, 36, w, h],36是9个anchor乘以四个坐标'''
    pre_loc = prediction.permute(0, 2, 3, 1).contiguous() # [b, 36, w, h]-》[b, w, h, 36]
    pre_loc = pre_loc.unsqueeze(-1).view(pre_loc.size(0), pre_loc.size(1), pre_loc.size(2), -1, 4) # [b, w, h, 36]->[b, w, h, 9, 4]
    feature_map_w = pre_loc.size(1) # 这个是特征图的宽
    feature_map_h = pre_loc.size(2)
    a, b = np.meshgrid(np.arange(feature_map_w) + 0.5, np.arange(feature_map_h) + 0.5) # 这里是构建中心点坐标系
    x_offset = torch.from_numpy(a).view(-1, 1)
    y_offset = torch.from_numpy(b).view(-1, 1)
    xy_offset = torch.cat((x_offset, y_offset), dim=1).unsqueeze(-1).view(feature_map_w, feature_map_h, -1)\
        .unsqueeze(-2).unsqueeze(0).expand_as(pre_loc[..., :2])
    # 将整个坐标系的坐标集合, 维度是[batchsize, w, h, 9, 2]
    '''接下来开始讲真正预测的坐标求出来,根据公式'''
    anchors = get_anchor() # 首先把每个anchor对应的尺寸表示
    anchors = torch.tensor(anchors).unsqueeze(0).unsqueeze(0).unsqueeze(0).expand_as(pre_loc[..., :2]) # 转化数据类型,维度是[batchsize, w,h, 9, 2]
    pre_loc[..., :2] = anchors * pre_loc[..., :2] + xy_offset
    # G^x=Pwdx(P)+Px,(1),这是公式,复制过来有变形
    pre_loc[..., 2:] = anchors * torch.exp(pre_loc[..., 2:])
    # G^w=Pwexp(dw(P)),这是公式
    return pre_loc # 这里返回的数据都是大于0的没问题,而且数值也正常

解决上面这些问题之后,我们可以开始考虑怎么进行筛选了,在进行筛选的时候,我们要用到iou和nms的概念,这两个概念我在我的yolov3和ssd的文章中已经说了,这里不再累述,不明白的可以自己去百度一下或者去看我的相关文章,那么进行iou或者nms操作的时候,我们使用的坐标是左上角和右下角,但是在上面我们得到的是中心点的坐标和长宽,所以,这里又涉及到一个将中心点坐标转化为左上角和右下角的坐标的操作了,代码如下:

'''author:nike hu'''
def change_to_conner(image_array, imageW, imageH):
    change_array = torch.zeros_like(image_array)
    change_array[:,0] = (image_array[:,0] - image_array[:,2] / 2) * imageW # 左上角x
    change_array[:,1] = (image_array[:,1] - image_array[:,3] / 2) * imageH # 左上角y
    change_array[:,2] = (image_array[:,0] + image_array[:,2] / 2) * imageW# 右下角x
    change_array[:,3] = (image_array[:,1] + image_array[:,3] / 2) * imageH # 右下角y
    change_array = change_array.long()
    return change_array

这个函数是我用的以前写的代码,当时这里得代码是针对数值在0到1之间的数据转化,但是我们经过get_loc函数,数值已经根据featuremap的尺寸放大了的,所以经过get_loss后的数据进行坐标变化的时候,imagew和imageh这两个参数设置为1,但是针对标签数据,那些数据数值在0到1之间,那时候注意imagew和imageh的大小跟featuremap的尺寸要保持一致。还有一点要注意,经过上面处理之后,坐标会有负值,暂时不用处理,后面再处理,因为当中心点靠图像边界,anchor的尺寸又比较大时,肯定会有负值。

好了,经过以上处理,我们终于可以开始筛选anchor了,这里筛选的步骤有两步,一步是根据前景的概率大小来筛选,一步是根据nms进一步进行筛选,代码如下:

'''author:nike hu'''
# 这里是在rpn网络中的rois操作来筛选预测框
def do_rios(pred_class, pre_loc):
    '''这里有两个参数,第一个参数是预测的类别,这里只预测两类,背景和前景,维度为[b, 18, w, h],
    第二个参数是预测的坐标参数,维度是[b, 36, w, h]'''
    # 先把维度调整过来, [b, w, h, 9, 2]和[b, w, h, 9, 4]
    pred_class = pred_class.permute(0, 2, 3, 1).contiguous()
    pred_class = pred_class.unsqueeze(-1).view(pred_class.size(0), pred_class.size(1), pred_class.size(2), -1, 2)
    pre_loc = get_loc(pre_loc) # 先把坐标转化过来,pre_loc不需要提前转化,调用函数,里面会自动转化
    prediction = torch.cat((pred_class, pre_loc), dim=4) # 将类别预测和坐标进行合并-》[b, w, h, 9, 6],最后维度分别对应每个anchor背景,前景概率, x,y,w,h
    prediction = prediction.view(pre_loc.size(0), -1, 6) # 将一张图片所有的预测框放到一块来处理,维度变为[b, w*h*9, 6]
    prediction_id = torch.sort(prediction[..., 1], descending=True)[1] # 获取前景概率重大到小的排序id,输出维度是[b, w*h*9]
    # 这里弄循环的话可能造成速度很慢,因为训练的时候一次性拿几十张图片
    for i in range(prediction.size(0)):
        prediction[i] = prediction[i][prediction_id[i]]
    top_size = int(0.6 * prediction.size(1)) # 取概率值靠前的多少个
    pre_out = prediction[:, :top_size] # 取出概率较大的那部分, 维度为[b, top_size, 6]
    '''接下来是使用nms进行筛选,这里直接使用opencv的代码吧'''
    boxes_id = []
    for i in range(pre_out.size(0)): # 由于openv自带的nms一次只能处理一张图,所以这里需要循环
        pre_out[i, :, 2:] = change_to_conner(pre_out[i, :, 2:], 1, 1) # 我直接转化为左上角和右下角坐标了,注意,这里最后连个参数是1,因为在上面个get_loc函数中已经扩大倍数了,擦,搞半天问题在这
        # print('pre_out->', pre_out[i, :, 2:].shape)
        box_id = cv2.dnn.NMSBoxes(pre_out[i, :, 2:].tolist(), pre_out[i, :, 1].tolist(), 0, 0.7)
        # 找了倒数第二个参数是根据分类的得分来判别是否保留,但是我们这里再上门那一步已经筛选了,这里不再需要,所以设置为0,而且第一个参数我们需要将坐标转化为左上角和右下角
        # box_id = nms(pre_out[i, :, 2:], pre_out[i, :, 1], 300, 1)[0]
        # print('box_id', box_id.shape)
        if len(box_id) > 300: # 最后经过nms筛选后我们只取300个,这里可能最后的预测框还没有300个,这里存在潜在的风险
            boxes_id.append(box_id[:300])
        else:
            boxes_id.append(box_id)
    boxes_id = torch.tensor(boxes_id).long() # 数据类型转化一下
    out_last = []
    for i in range(boxes_id.size(0)):
        out = pre_out[i][boxes_id[i].t().squeeze(0)] # 注意这里要转置,还要去掉一个维度,不然最后维度要增加
        out_last.append(out.tolist()) # 这里要转化为list来存储,不然后面会出错
    out_last = torch.tensor(out_last)
    return out_last[:, :, 2:] # 这里就返回符合要求的预测框坐标数据

在这里,我其实对上面文章作者所说的根据Nms选出概率最大的多少个 这句话有些疑惑,因为在我的理解中,nms的操作是直接将iou较大的边框进行删去,没有所谓的概率最大的这个说法,顶多有个每个anchor和其他anchor有个iou值得说法,这里作者就一笔带过了,我也就按照我的理解,直接进行nms进行操作了,然后把nms的阈值调大点进行筛选了,我也不清楚我这样理解对不对,先就这样做了吧。

当我们这样做了之后,我们就可以输出一个维度类似[300, 4]的数据了,300代表符合要求的anchor,4就代表这些anchor的左上角和右下角的数据了。这时候,我们就可以对最初的模型进行更新了,更新如下:

'''author:nike hu'''
class Faster_rcnn(nn.Module):
    def __init__(self):
        super(Faster_rcnn, self).__init__()
        self.net_body = Model.vgg16().features[0:30] # 这里截取官方给的vgg模型的前30层,到这一层就是主体部分
        '''接下来构建RPN网络,这里的网络有分支,所以不好用一个nn.sequential直接操作'''
        self.RPN_1 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
            nn.ReLU(inplace=True)
        )
        self.RPN_2_1 = nn.Sequential(
            nn.Conv2d(512, 18, kernel_size=1), # 输出维度为[b, 18, w, h], 18=9×2(9个anchor,每个anchor二分类,使用交叉熵损失),
            nn.ReLU(True) # 这个必须得跟上,否者最后模型跑出的值会有负数
        )
        self.RPN_2_2 = nn.Sequential(
            nn.Conv2d(512, 36, kernel_size=1), # 进行回归的卷积核通道数为36=9×4(9个anchor,每个anchor有4个位置参数)
            nn.ReLU(True)
        )



    def forward(self, x):
        x = self.net_body(x)
        x = self.RPN_1(x)
        x1 = self.RPN_2_1(x) # rpn网络第二次输出的数据
        x2 = self.RPN_2_2(x)  # rpn网络第二次输出的数据
        out1 = torch.cat((x1, x2), dim=1) # 这里是rpn网络输出的数据,后续用来计算rpn部分的loss
        rois = do_rios(x1, x2)

更新的其实就是最后的几行。

接下来,我们再回忆一下我们接下来的流程,当我们输出类似[300, 4]维度的rois之后,我们要结合图片中标签的数据对这些anchor进一步筛选,让最后的输出维度达到[128, 4],这里得128,我看作者的意思是确定了的,然后这个128里面有32个正样本和96个负样本,我们看看这部分的处理思路,我是另外一位博主那里找的,如下:
在这里插入图片描述在这图片中我们又涉及到了图片的anchor和标签的尺寸求得的iou的操作,代码如下:

'''author:nike hu'''
def get_iou(box1, box2):
    '''我们这里批量处理box1和box2,处理方式为维度的扩充,box1指的是边框,维度是[n, 4],box2指的是box, 维度是[m, 4],最后输出[n,m]'''
    box2 = box2.float()
    box1 = box1.float() # 将数据类型再转化一下,否则torch.max那里可能报错
    box1_num = box1.size(0) # 这是box1的个数,相当于有多少个边框
    box2_num = box2.size(0) # 这是box2的个数,相当于有多少个box,默认是8732个
    box_unit1 = torch.max(box1[:, :2].unsqueeze(1).expand(box1_num, box2_num, 2),
                        box2[:, :2].unsqueeze(0).expand(box1_num, box2_num, 2)) # 两边框相交的左上角
    box_unit2 = torch.min(box1[:, 2:].unsqueeze(1).expand(box1_num, box2_num, 2),
                        box2[:, 2:].unsqueeze(0).expand(box1_num, box2_num, 2)) # 两边框相交的右下角
    inter_area = box_unit2 - box_unit1 # 求出交叉面积的长和宽
    inter_area = inter_area.clamp(0) # 把小于0的数值全部变为0
    intersection_area = inter_area[:, :, 0] * inter_area[..., 1] # 这个就是交叉面积
    box1_area = ((box1[:, 2] - box1[:, 0]) * (box1[:, 3] - box1[:, 1])).unsqueeze(1).expand(box1_num, box2_num)
    box2_area = ((box2[:, 2] - box2[:, 0]) * (box2[:, 3] - box2[:, 1])).unsqueeze(0).expand(box1_num, box2_num)
    iou = intersection_area/ (box2_area + box1_area - intersection_area)
    return iou

知道iou的求法之后,我们按照上一张图片进行操作,其实,上面的流程也不是很详细,有些地方有些模糊,我只能根据我的想法实现了,代码如下:

'''author:nike hu'''
# 这个函数是再次筛选正负样本
def proposal_target_creator(rios, gt_groud):
    '''这里得参数,rios是上一个函数最后返回的,维度是[bachsize, n, 4], 第二个参数是标签信息,维度是[batchsize, m, 4]'''
    outs = torch.zeros(1, 128, 4)
    for i in range(rios.shape[0]): # 一张一张照片来处理
        ious = get_iou(change_to_conner(gt_groud[i], 62, 37), rios[i]) # 先计算每个标签和每个anchor的iou,维度为[m,n],代表一个标签对应的n个anchor的iou
        positive_riou_id = ious > 0.4 # 这里是阈值,大于0.4的为正样本
        negtive_riou_id = ious < 0.002 # iou接近0的为负样本
        positive_sample = torch.zeros(1, 4) # 正样本
        negtive_sample = torch.zeros(1, 4) # 负样本
        for j in range(positive_riou_id.shape[0]): # 一个一个标签来选择
            positive = rios[i][positive_riou_id[j].unsqueeze(-1).expand_as(rios[i])].view(-1, 4) # 获取正样本
            positive_sample = torch.cat((positive_sample, positive), dim=0)
            negative = rios[i][negtive_riou_id[j].unsqueeze(-1).expand_as(rios[i])].view(-1, 4) # 获取负样本
            negtive_sample = torch.cat((negtive_sample, negative), dim=0)
        positive_sample = positive_sample[1:] # 第一行全为0,要去掉
        negtive_sample = negtive_sample[1:]
        positive_num = positive_sample.shape[0]
        if positive_num > 32: # 正负样本的总数为128,正样本最多为32个
            positive_sample = positive_sample[:32]
            positive_num = 32
        if negtive_sample.shape[0] > 96:
            negtive_sample = negtive_sample[:128 - positive_num]
        # print('============>', positive_sample.shape, negtive_sample.shape)
        out = torch.cat((positive_sample, negtive_sample), dim=0) # 合并数据
        # print(out.shape, outs.shape)
        outs = torch.cat((outs, out.unsqueeze(0)), dim=0) # 合并,记得out要扩大维度
    outs = outs[1:]
    outs = torch.clamp(outs, min=0) # 将坐标中的负数去掉
    return outs

最后我们再讲上面这个函数用到模型搭建中,改一下构建模型的代码,改几行就行,如下:

'''author:nike hu'''
class Faster_rcnn(nn.Module):
    def __init__(self, gt_groud): # 这里将坐标的信息加进来
        super(Faster_rcnn, self).__init__()
        self.gt_groud = gt_groud # 这个是标签的信息
        self.net_body = Model.vgg16().features[0:30] # 这里截取官方给的vgg模型的前30层,到这一层就是主体部分
        '''接下来构建RPN网络,这里的网络有分支,所以不好用一个nn.sequential直接操作'''
        self.RPN_1 = nn.Sequential(
            nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
            nn.ReLU(inplace=True)
        )
        self.RPN_2_1 = nn.Sequential(
            nn.Conv2d(512, 18, kernel_size=1), # 输出维度为[b, 18, w, h], 18=9×2(9个anchor,每个anchor二分类,使用交叉熵损失),
            nn.ReLU(True) # 这个必须得跟上,否者最后模型跑出的值会有负数
        )
        self.RPN_2_2 = nn.Sequential(
            nn.Conv2d(512, 36, kernel_size=1), # 进行回归的卷积核通道数为36=9×4(9个anchor,每个anchor有4个位置参数)
            nn.ReLU(True)
        )



    def forward(self, x):
        x = self.net_body(x)
        x = self.RPN_1(x)
        x1 = self.RPN_2_1(x)
        x2 = self.RPN_2_2(x)
        print(x1.shape, x2.shape)
        out1 = torch.cat((x1, x2), dim=1) # 这里是rpn网络输出的数据,后续用来计算rpn部分的loss
        rois = do_rios(x1, x2)
        last_rois = proposal_target_creator(rois, self.gt_groud) # 进一步筛选anchor,输出维度为[batchsize, 128, 1]

好了,这篇文章所介绍的内容就到这了,至于整个流程的后续步骤,我后面应该会写文章补上,算给自己一个交代吧。代码有些多,我相信能看到最后的人不会太多,希望能帮到各位吧,各位要是觉得对你们理解faster_rcnn有点用处,点个赞再走吧,毕竟我也写了挺久的。

2020 4.28

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值