本文中代码来自:https://github.com/bubbliiiing/faster-rcnn-keras Two - stage 和One - stage目标检测算法的综述。
Two-stage目标检测算法:先生成一系列的候选框,在通过卷积神经网络进行样本分类。(在精度上占优势) One-stage目标检测算法:不生成候选框直接将目标边框的定位问题转化成回归问题。(在算法速度上占优势)
首先将原图片输入到特征提取网络中,特征提取网络可以看做将图像划分成不同大小的网格,每个网格上有若干个先验框;利用RPN(Region proposal network)区域建议网络进行粗略的筛选,获得建议框在共享特征层上进行截取,将截取到的建议框输入到ROI Pooling网络进行细致的调整,将建议框resize 成相同的大小,再利用分类回归网络判断框中是否存在目标,并对建议框进行调整。
Faster RCNN网络对输入图片的大小没有限制,在输入到特征提取网络之前,对图片进行resize 操作,将图片中的较短边resize成600(保持原图片的长宽比,图片不会失真),输入到backbone特征提取网络(ResNet50),看作将图片划分成不同个大小的网格,输出特征feature(共享特征层,后面得到的建议框也是在共享特征层上进行截取。)此时,得到的共享特征层有两个去向:1、进行一次3×3的卷积,再分别进行两次通道数不同的1×1卷积(通道数分别为9和36),这里每一个网格中默认的先验框个数为9,那么9和36分别代表什么含义呢?
9和36分别代表的含义:每一个网格中默认先验框的个数为9,而两个1×1卷积输出的结果分别表示先验框中是否存在物体、先验框的调整参数(中心点坐标和宽高)。9代表每个网格中每一个先验框中是否存在物体;36代表每个网格中每一个先验框的调整参数。对先验框调整后的结果就是经过粗略筛选后的建议框。
利用建议框在共享特征层上进行截取,截取下来的建议框输入到ROI Pooling中。这也就是共享特征层的第二个去向:用建议框对共享特征层进行截取,用于进一步更精细的调整。 在Roi Pooling中,首先将截取到的建议框resize成相同大小,然后1、(bbox_pred)获得建议框的调整参数;2、(class_pred)经过softmax获得建议框中物体的种类。得到最终的目标检测结果。
在ResNet中有两种结构:Conv_Block和Identity_Block Conv_Block的残差边上有卷积操作,该结构的输入和输出的通道数改变了,所以不能够连续串联。 Identity_Block的残差边上没有卷积操作,该结构的输入、输出的维度相同,可以连续串联,作用是增加网络的深度。
Conv_Block默认的步长为(2,2) 首先进行卷积核大小为1×1的卷积操作,使用默认的步长,宽高减半 BatchNormalization归一化操作 relu非线性激活函数
卷积核大小为3×3的卷积操作,宽高不变 BatchNormalization归一化操作 relu非线性激活函数
卷积核大小为1×1的卷积操作,宽高不变 BatchNormalization归一化操作
残差边是将输入的特征经过一次卷积核大小为1×1的卷积操作,步长为2,输出宽高减半 和一次BatchNormalization归一化操作
然后将残差边和主干网络的结果叠加,经过一个非线性激活函数,最后得到conv_Block的输出结果。
首先进行一次卷积核大小为1×1的卷积,宽高不变 BatchNormalization归一化 relu非线性激活函数
卷积核大小为3×3的卷积,宽高不变 BatchNormalization归一化 relu非线性激活函数
卷积核大小为1×1的卷积,宽高不变 BatchNormalization归一化
残差边将输入直接引入到主干的输出特征层,进行叠加,再经过一个relu非线性激活函数得到Identity_Block的结果。
调用Identity_Block和Conv_Block构建残差网络结构
def ResNet50(inputs): img_input = inputs x = ZeroPadding2D((3, 3))(img_input) x = Conv2D(64, (7, 7), strides=(2, 2), name='conv1')(x) x = BatchNormalization(name='bn_conv1')(x) x = Activation('relu')(x) x = MaxPooling2D((3, 3), strides=(2, 2), padding="same")(x) x = conv_block(x, 3, [64, 64, 256], stage=2, block='a', strides=(1, 1)) x = identity_block(x, 3, [64, 64, 256], stage=2, block='b') x = identity_block(x, 3, [64, 64, 256], stage=2, block='c') x = conv_block(x, 3, [128, 128, 512], stage=3, block='a') x = identity_block(x, 3, [128, 128, 512], stage=3, block='b') x = identity_block(x, 3, [128, 128, 512], stage=3, block='c') x = identity_block(x, 3, [128, 128, 512], stage=3, block='d') x = conv_block(x, 3, [256, 256, 1024], stage=4, block='a') x = identity_block(x, 3, [256, 256, 1024], stage=4, block='b') x = identity_block(x, 3, [256, 256, 1024], stage=4, block='c') x = identity_block(x, 3, [256, 256, 1024], stage=4, block='d') x = identity_block(x, 3, [256, 256, 1024], stage=4, block='e') x = identity_block(x, 3, [256, 256, 1024], stage=4, block='f') return x首先用0填充边缘,使提取到的特征具有较好的鲁棒性。 卷积核大小为(7,7),输出通道数为64,步长为2的卷积操作 BatchNormalization归一化 relu非线性激活函数
最大池化操作,步长为2.
经过一次Conv_Block,步长为1,宽高不变。 经过两次Identity_Block Conv_Block和Identity_Block中都有三次卷积,在这部分中所有Conv_Block和Identity_Block卷积的通道数分别为64,64,256
经过一次Conv_Block,通道数减半。 经过三次Identity_Block 所有Conv_Block和Identity_Block的三次卷积,输出通道数分别为128,128, 512
经过一次Conv_Block,通道数减半。 经过5次Identity_Block 所有Conv_Block和Identity_Block的三次卷积,输出通道数分别为256,256,1024
ResNet50网络对出入的特征层宽高压缩了4次
而且Conv_Block和Identity_Block都包含瓶颈结构(Bottle Neck): 首先用1×1卷积减少通道数,起到减少参数量的作用,通过3×3的卷积进行特征提取,在通过1×1的卷积操作将通道数还原。 瓶颈结构有两个作用:增加网络深度(提取到更具有鲁棒性的特征层)、减少网络参数
Faster_RCNN网络中每一个网格中默认有9个先验框。 利用共享特征层获得建议框。 frcnn.py
def get_model(config,num_classes): inputs = Input(shape=(None, None, 3)) roi_input = Input(shape=(None, 4)) # 公共特征层bese_layers base_layers = ResNet50(inputs) num_anchors = len(config.anchor_box_scales) * len(config.anchor_box_ratios) rpn = get_rpn(base_layers, num_anchors) model_rpn = Model(inputs, rpn[:2]) classifier = get_classifier(base_layers, roi_input, config.num_rois, nb_classes=num_classes, trainable=True) model_classifier = Model([inputs, roi_input], classifier) model_all = Model([inputs, roi_input], rpn[:2]+classifier) return model_rpn,model_classifier,model_all输入一张3通道的图片,作为inputs的内容,将inputs输入到特征提取网络中得到base_layers; 将roi_input输入到ROI Pooling中。
首先通过3×3的卷积操作进行特征提取,将输出通过两个1×1的卷积,一个的通道数为9,另一个通道数为36。前面提到过,当前网络中每一个网格上有9个先验框,输出通道数为9的特征层x_class表示的是每一个网格中每一个先验框是否存在物体,输出通道数为36的特征层x_regr表示的是每一个网格每一个先验框的调整参数。 这一步得到的结果就是建议框,也就是对先验框 粗略筛选的结果
建议框是通过RPN网络对先验框进行调整得到的粗略筛选的结果。
首先将将输入的图片进行resize ,输入到特征增提取网络中,输出为共享特征层。 然后提取所有的先验框,调用detection_out函数,返回的结果是建议框。 代码如下:
anchors = get_anchors(self.get_img_output_length(width,height),width,height) rpn_results =self.bbox_util.detection_out(preds,anchors,1,confidence_threshold=0) R = rpn_results[0][:, 2:]函数detection_out是对建议框进行解码的过程,(其实很多目标检测网络的先验框解码过程是类似的) 计算先验框的宽和高,计算先验框中心点的坐标。根据真实框和先验框中心点的偏离情况,解码得到调整后的先验框中心点的坐标。同理解码得到调整以后先验框的宽和高。计算出调整以后先验框的左上角和右下角的坐标。然后将所有先验框的左上角右下角的坐标进行堆叠。非极大抑制,然后用置信度,对当前的到的先验框进行筛选,按照置信度由高到低进行排序,只保留前keep_top_k个先验框。
接着回到detect_image函数的代码中。detection_out函数输出的结果是解码后的经过粗略筛选的先验框。
R = rpn_results[0][:, 2:] R[:, 0] = np.array(np.round(R[:, 0]*width/self.config.rpn_stride),dtype=np.int32) R[:, 1] = np.array(np.round(R[:, 1]*height/self.config.rpn_stride),dtype=np.int32) R[:, 2] = np.array(np.round(R[:, 2]*width/self.config.rpn_stride),dtype=np.int32) R[:, 3] = np.array(np.round(R[:, 3]*height/self.config.rpn_stride),dtype=np.int32) R[:, 2] -= R[:, 0] R[:, 3] -= R[:, 1]R中存放的是小数形式的调整以后的先验框解码的结果!!! 然后得到建议框左上角和右下角的坐标,最后两行代码,把R中存放的内容转化成建议框左上角的坐标和建议框的宽和高
RPN网络是对先验框进行粗略的筛选,获得先验框。下一步,需要利用建议框在共享特征层截取,输入到Roi网络中进行更精细的筛选。
输入到Roi_Pooling的特征层 RoiPoolingConv.py中的函数call代码如下
def call(self, x, mask=None): assert(len(x) == 2) img = x[0]# 共享特征层 rois = x[1]# 建议框 outputs = [] for roi_idx in range(self.num_rois): # 获得建议框左上角的坐标还有建议框的宽高 x = rois[0, roi_idx, 0] y = rois[0, roi_idx, 1] w = rois[0, roi_idx, 2] h = rois[0, roi_idx, 3] # 格式转换 x = K.cast(x, 'int32') y = K.cast(y, 'int32') w = K.cast(w, 'int32') h = K.cast(h, 'int32') # 在共享特征层上截取建议框,而且把截取下来的内容resize成self.pool_size×self.pool_size的大小 rs = tf.image.resize_images(img[:, y:y+h, x:x+w, :], (self.pool_size, self.pool_size)) outputs.append(rs) final_output = K.concatenate(outputs, axis=0) final_output = K.reshape(final_output, (1, self.num_rois, self.pool_size, self.pool_size, self.nb_channels)) final_output = K.permute_dimensions(final_output, (0, 1, 2, 3, 4)) return final_output在参数中可以设置每次输入到Roi Pooling中建议框的数量。 Config.py
self.num_rois = 32img = x[0]表示共享特征层,rois = x[1]表示建议框。 上一节的代码已经将建议框格式转化为左上角的坐标和建议框的宽高。将建议框的内容从共享特征层上截取出来。并且resize成self.pool_size×self.pool_size的大小,然后把结果堆叠,reshape成以下格式(每一个建议框中物体的置信度,每一个建议框的宽、高和通道数)。
代码如下:
out = classifier_layers(out_roi_pool, input_shape=input_shape, trainable=True) # 对应ResNet50对长和宽的第五次压缩 def classifier_layers(x, input_shape, trainable=False): x = conv_block_td(x, 3, [512, 512, 2048], stage=5, block='a', input_shape=input_shape, strides=(2, 2), trainable=trainable) x = identity_block_td(x, 3, [512, 512, 2048], stage=5, block='b', trainable=trainable) x = identity_block_td(x, 3, [512, 512, 2048], stage=5, block='c', trainable=trainable) x = TimeDistributed(AveragePooling2D((7, 7)), name='avg_pool')(x) return xtd表示TimeDistributed是指对32个局部共享特征层分别进行Conv_Block卷积 经过一次Conv_Block和两次Identity_Block,建议框经过Roi_Pooling输出宽高减半,对应的是ResNet50对长和宽的第五次压缩。 然后将提取到的特征进一步操作 代码如下:
out = TimeDistributed(Flatten())(out) out_class = TimeDistributed(Dense(nb_classes, activation='softmax', kernel_initializer='zero'), name='dense_class_{}'.format(nb_classes))(out) out_regr = TimeDistributed(Dense(4 * (nb_classes-1), activation='linear', kernel_initializer='zero'), name='dense_regress_{}'.format(nb_classes))(out) return [out_class, out_regr]把输出的特征层平铺为一行, 一次输出神经元个数为待识别的物体种类数out_class的全连接,用softmax分类器判断框内物体的置信度。 又一次输出神经元个数为4 * (nb_classes-1)的全连接,框内物体为除背景以外物体时,得到框的调整参数。
将此时的建议框再进行解码就得到了最终显示在图片上的框。
由于每次输入到Roi_Pooling中的建议框个数是固定的,设置成32个。需要用到循环来遍历每一个建议框。 frcnn.py 对于总的建议框个数不能够被32(参数中设置好的每一次输入到Roi_pooling中建议框的个数)整除的情况,需要对最后一次输入的建议框进行填充。 代码如下:
for jk in range(R.shape[0]//self.config.num_rois + 1): ROIs = np.expand_dims(R[self.config.num_rois*jk:self.config.num_rois*(jk+1), :], axis=0) if ROIs.shape[1] == 0: break if jk == R.shape[0]//self.config.num_rois: #pad R curr_shape = ROIs.shape target_shape = (curr_shape[0],self.config.num_rois,curr_shape[2]) ROIs_padded = np.zeros(target_shape).astype(ROIs.dtype) ROIs_padded[:, :curr_shape[1], :] = ROIs ROIs_padded[0, curr_shape[1]:, :] = ROIs[0, 0, :] ROIs = ROIs_padded然后是建议框的解码过程 首先通过共享特征层和建议框获得预测结果:
[P_cls, P_regr] = self.model_classifier.predict([base_layer,ROIs])得到: P_cls代表建议框中物体所属类别的置信度 P_regr代表建议框的调整参数
然后利用物体所属类别的置信度对建议框进行筛选:
for ii in range(P_cls.shape[1]): if np.max(P_cls[0, ii, :-1]) < self.confidence: continue提取出筛选后的建议框左上角的坐标和宽高,计算调整后的左上角坐标还有宽高 代码如下:
(x, y, w, h) = ROIs[0, ii, :] cls_num = np.argmax(P_cls[0, ii, :-1]) # 获得建议框左上角坐标和宽高的调整参数 (tx, ty, tw, th) = P_regr[0, ii, 4*cls_num:4*(cls_num+1)] # 前两个值除以8,后两个值除以4,得到建议框左上角坐标和宽高的调整参数 tx /= self.config.classifier_regr_std[0] ty /= self.config.classifier_regr_std[1] tw /= self.config.classifier_regr_std[2] th /= self.config.classifier_regr_std[3]利用以上的计算结果计算出调整以后的建议框中心点的坐标和宽高 代码如下:
cx = x + w/2. cy = y + h/2. cx1 = tx * w + cx cy1 = ty * h + cy w1 = math.exp(tw) * w h1 = math.exp(th) * h x1 = cx1 - w1/2. y1 = cy1 - h1/2.最后得到建议框左上角和右下角的坐标
x2 = cx1 + w1/2 y2 = cy1 + h1/2 x1 = int(round(x1)) y1 = int(round(y1)) x2 = int(round(x2)) y2 = int(round(y2)) bboxes.append([x1,y1,x2,y2]) probs.append(np.max(P_cls[0, ii, :-1])) labels.append(label)bboxes存放建议框左上角和右下角的坐标 probs存放的是建议框中物体所属类别的置信度 labels存放标签(建议框内部物体的标签)
首先将结果转化成numpy的格式
labels = np.array(labels) probs = np.array(probs) boxes = np.array(bboxes,dtype=np.float32)把bbox的内容转化成小数的格式,并且获得建议框在原始图片上的坐标
boxes[:,0] = boxes[:,0]*self.config.rpn_stride/width boxes[:,1] = boxes[:,1]*self.config.rpn_stride/height boxes[:,2] = boxes[:,2]*self.config.rpn_stride/width boxes[:,3] = boxes[:,3]*self.config.rpn_stride/height进行非极大值抑制
results = np.array(self.bbox_util.nms_for_out(np.array(labels),np.array(probs),np.array(boxes),self.num_classes-1,0.4))最后将结果绘制在原始图像上
top_label_indices = results[:,0] top_conf = results[:,1] boxes = results[:,2:] boxes[:,0] = boxes[:,0]*old_width boxes[:,1] = boxes[:,1]*old_height boxes[:,2] = boxes[:,2]*old_width boxes[:,3] = boxes[:,3]*old_height font = ImageFont.truetype(font='model_data/simhei.ttf',size=np.floor(3e-2 * np.shape(image)[1] + 0.5).astype('int32')) thickness = (np.shape(old_image)[0] + np.shape(old_image)[1]) // old_width * 2 image = old_image for i, c in enumerate(top_label_indices): predicted_class = self.class_names[int(c)] score = top_conf[i] left, top, right, bottom = boxes[i] top = top - 5 left = left - 5 bottom = bottom + 5 right = right + 5 top = max(0, np.floor(top + 0.5).astype('int32')) left = max(0, np.floor(left + 0.5).astype('int32')) bottom = min(np.shape(image)[0], np.floor(bottom + 0.5).astype('int32')) right = min(np.shape(image)[1], np.floor(right + 0.5).astype('int32')) # 画框框 label = '{} {:.2f}'.format(predicted_class, score) draw = ImageDraw.Draw(image) label_size = draw.textsize(label, font) label = label.encode('utf-8') print(label) if top - label_size[1] >= 0: text_origin = np.array([left, top - label_size[1]]) else: text_origin = np.array([left, top + 1]) for i in range(thickness): draw.rectangle( [left + i, top + i, right - i, bottom - i], outline=self.colors[int(c)]) draw.rectangle( [tuple(text_origin), tuple(text_origin + label_size)], fill=self.colors[int(c)]) draw.text(text_origin, str(label,'UTF-8'), fill=(0, 0, 0), font=font) del draw return image由于先验框是事先在较大数据集上生成的框,所以给定先验框的大小和长宽比是固定的。将固定的先验框大小分别与ratio相乘,得到不同长宽比的先验框。 代码如下:
def generate_anchors(sizes=None, ratios=None): if sizes is None: # 使用默认的先验框大小[128,256,512] sizes = config.anchor_box_scales if ratios is None: # 使用默认的先验框的长宽比,[1,1][1,2][2,1] ratios = config.anchor_box_ratios # 本例中有9个先验框 num_anchors = len(sizes) * len(ratios) anchors = np.zeros((num_anchors, 4)) # print(anchors) anchors[:, 2:] = np.tile(sizes, (2, len(ratios))).T for i in range(len(ratios)): anchors[3*i:3*i+3, 2] = anchors[3*i:3*i+3, 2]*ratios[i][0] anchors[3*i:3*i+3, 3] = anchors[3*i:3*i+3, 3]*ratios[i][1] anchors[:, 0::2] -= np.tile(anchors[:, 2] * 0.5, (2, 1)).T anchors[:, 1::2] -= np.tile(anchors[:, 3] * 0.5, (2, 1)).T print(anchors) return anchors返回的结果anchors中存放的是每一个先验框[先验框中心点的横坐标,先验框中心点的纵坐标,先验框的宽,先验框的高]
输入特征层的大小和generate_anchors函数生成的先验框,将特征层和在该特征层上划分网格进行可视化:
def shift(shape, anchors, stride=config.rpn_stride): # [0,1,2,3,4,5……37] # [0.5,1.5,2.5……37.5] # [8,24,……] # arange函数生成0-37之间的数,因为特征层的大小为38×38,所以可以看做将原图像划分为38×38的网格 print('shape[0]:', shape[0]) shift_x = (np.arange(0, shape[0], dtype=keras.backend.floatx()) + 0.5) * stride # 每一个数加上0.5,生成0.5-37.5之间的数,得到的是每一个网格中心点得坐标 shift_y = (np.arange(0, shape[1], dtype=keras.backend.floatx()) + 0.5) * stride # 输出的结果表示对于输入一张(600, 600, 3)的图片,划分38×38的网格,每个网格中心点的坐标 shift_x, shift_y = np.meshgrid(shift_x, shift_y) shift_x = np.reshape(shift_x, [-1]) shift_y = np.reshape(shift_y, [-1]) print('shift_xshift_y', shift_x,shift_y) shifts = np.stack([ shift_x, shift_y, shift_x, shift_y ], axis=0) shifts = np.transpose(shifts) number_of_anchors = np.shape(anchors)[0] k = np.shape(shifts)[0] shifted_anchors = np.reshape(anchors, [1, number_of_anchors, 4]) + np.array(np.reshape(shifts, [k, 1, 4]), keras.backend.floatx()) shifted_anchors = np.reshape(shifted_anchors, [k * number_of_anchors, 4]) fig = plt.figure() ax = fig.add_subplot(111) plt.ylim(-300, 900) plt.xlim(-300, 900) plt.scatter(shift_x, shift_y) box_widths = shifted_anchors[:, 2]-shifted_anchors[:, 0] box_heights = shifted_anchors[:, 3]-shifted_anchors[:, 1] initial = 0 for i in [initial+0, initial+1, initial+2, initial+3, initial+4, initial+5, initial+6, initial+7, initial+8]: rect = plt.Rectangle([shifted_anchors[i, 0], shifted_anchors[i, 1]], box_widths[i], box_heights[i], color="r", fill=False) ax.add_patch(rect) plt.show() print('shifted_anchors', shifted_anchors) return shifted_anchors对于38×38大小的特征层,输出[0,1,2……37],也就是把每一个网格的大小看作是一个单位,然后对序列中的每一个数加上0.5,得到[0.5, 1.5, 2.5……37.5],看作使每一个网格的中心点。然后根据这些中心点的坐标划分网格(图片大小为600×600,特征层大小为38×38,每一个网格大小约为16)。 对于一张给定的图片,输入特征层的大小,输入图片的宽和高,调用函数generate_anchors生成先验框。 特征图上某一网格点对应的9个先验框如图所示:
一共有38×38×9=12996个先验框。
至此Faster-RCNN代码实现目标检测任务完成
感谢 https://blog.csdn.net/daodanxiansheng/article/details/83340773 https://blog.csdn.net/weixin_44791964/article/details/104451667?ops_request_misc=%7B%22request%5Fid%22%3A%22159361450619195264562265%22%2C%22scm%22%3A%2220140713.130102334.pc%5Fblog.%22%7D&request_id=159361450619195264562265&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2blogfirst_rank_v2~rank_blog_default-2-104451667.pc_v2_rank_blog_default&utm_term=Faster+CNN https://github.com/bubbliiiing/faster-rcnn-keras