fasterrcnn+fpn代码总结
Fasterrcnn的backbone部分
以resnet50为backbone并且加⼊了fpn⽹络,关于fpn就是特征⾦字塔,简单来说就是融合不同尺度的特征进⾏检测,既加⼊底层卷积特征的位置信息对检测⼩⽬标很有⽤,⼜融合⾼层卷积特征的语义信息,关于具体实现过程可以看下⾯的代码分析。
def resnet50_fpn_backbone():
resnet_backbone = ResNet(Bottleneck,[3,4,6,3],
include_top=Fal)
# 冻结layer1及其之前的所有底层权重(基础通⽤特征)
for name, parameter in resnet_backbone.named_parameters():报考公务员的学历要求
if'layer2'not in name and'layer3'not in name and'layer4'not in name:
return_layers ={'layer1':'0','layer2':'1','layer3':'2','layer4':'3'}
# in_channel 为layer4的输出特征矩阵channel = 2048
in_channels_stage2 = resnet_backbone.in_channel //8
in_channels_list =[
in_channels_stage2,# layer1 out_channel=256
in_channels_stage2 *2,# layer2 out_channel=512
in_channels_stage2 *4,# layer3 out_channel=1024
in_channels_stage2 *8,# layer4 out_channel=2048
]
out_channels =256
return BackboneWithFPN(resnet_backbone, return_layers, in_channels_list, out_channels)
可以看到这⾥是选⽤的resnet50做为backbone,return_layers字典的意思是使⽤resnet50⽹络中layer1,layer2,layer3,layer4层的输出结果做检测。in_channels_list指layer1到4的输出通道数,out_channels指希望把layer1到4的输出都变为256
然后再看到BackbonewithFPN类
class BackboneWithFPN(nn.Module):
def__init__(lf, backbone, return_layers, in_channels_list, out_channels):
super(BackboneWithFPN, lf).__init__()
lf.body = IntermediateLayerGetter(backbone, return_layers=return_layers)
lf.fpn = FeaturePyramidNetwork(
in_channels_list=in_channels_list,
out_channels=out_channels,
extra_blocks=LastLevelMaxPool(),
prove的用法)
lf.out_channels = out_channels
def forward(lf, x):
x = lf.body(x)
x = lf.fpn(x)
return x
BackbonewithFPN定义了⼀个body和fpn
body使⽤IntermediaLayerGetter类得到resnet layer1到layer4层的输出
fpn使⽤FeaturepyramidNetwork类得到fpn之后的有序字典输出。
再来看到IntermediaLayerGetter类
class IntermediateLayerGetter(nn.ModuleDict):
def__init__(lf, model, return_layers):
if not t(return_layers).issubt([name for name, _ in model.named_children()]):
rai ValueError("return_layers are not prent in model")
orig_return_layers = return_layers
return_layers ={k: v for k, v in return_layers.items()}
layers = OrderedDict()
# 遍历模型⼦模块按顺序存⼊有序字典
# 只保存layer4及其之前的结构,舍去之后不⽤的结构
商品英文
for name, module in model.named_children():
layers[name]= module
if name in return_layers:
del return_layers[name]
# 如果return_layers是空的
if not return_layers:
break
super(IntermediateLayerGetter, lf).__init__(layers)
def forward(lf, x):经济学人英文版
out = OrderedDict()
# 依次遍历模型的所有⼦模块,并进⾏正向传播,
# 收集layer1, layer2, layer3, layer4的输出
for name, module in lf.named_children():
x = module(x)
if name urn_layers:
out_name = lf.return_layers[name]
out[out_name]= x
return out
可以看到IntermediateLayerGetter类就是把模型在return_layers中的层和return_layers之前的层加⼊layers字典中,使⽤layers字典初始化⼀个ModuleDict,然后遍历该模型的所有⼦模块并进⾏正向传播,收集得到layer1到layer4的输出。
经过debug调试可以看到在这⾥IntermediateLayerGetter的输出结果为(因为后⾯的调试可能跟这⾥不是⼀次调试,加载进模型的图⽚不⼀样所以可能导致debug结果有出⼊,如果是⼀次debug是完全⼀样的)哥伦比亚人
out:{
“0”:(batch, 256, 232, 336)
“1”:(batch, 512, 116, 168)
“2”:(batch, 1024, 58, 84)
“3”:(batch, 2048, 29, 42)
}the distance
得到了resnet layer1到layer4的结果之后,再来看到featurepyramidnetwork类
特征⾦字塔插图
def forward(lf, x):
names =list(x.keys())
x =list(x.values())
# 将resnet layer4的channel调整到指定的out_channels
last_inner = lf.inner_blocks[-1](x[-1])
# result中保存着每个预测特征层
results =[]
# 将layer4调整channel后的特征矩阵,通过3x3卷积后得到对应的预测特征矩阵
results.append(lf.layer_blocks[-1](last_inner))
for idx in range(len(x)-2,-1,-1):
inner_lateral = lf.get_result_from_inner_blocks(x[idx], idx)
feat_shape = inner_lateral.shape[-2:]
inner_top_down = F.interpolate(last_inner, size=feat_shape, mode="nearest")
last_inner = inner_lateral + inner_top_down
results.inrt(0, lf.get_result_from_layer_blocks(last_inner, idx))
# 在layer4对应的预测特征层基础上⽣成预测特征矩阵5
a_blocks is not None:
results, names = lf.extra_blocks(results, names)
# make it back an OrderedDict
out = OrderedDict([(k, v)for k, v in zip(names, results)])
return out
这⾥我们直接看到featurepyramidnetwork类的forward函数
lf.inner_blocks是卷积层列表,是为了把不同的layer层的输出通道数都转化为需要的通道数256
[(256,256,1),(512,256,1),(1024,256,1),(2048,256,1)]
(256,256,1)中,第⼀个256代表输⼊channel,第⼆份256代表输出channel,1代表1✖ 1卷积。
lf.layer_blocks也是卷积层列表,是为了把转为相同通道的不同层的输出做3*3卷积得到相同通道的输出再进⾏fpn
[(256,256,3),(256,256,3),(256,256,3),(256,256,3)]
inner_blocks和layer_blocks列表中的四个卷积核分别代表backbone输出的四个层分别要做的卷积
forward函数的输⼊是x,x就是之前IntermediateLayerGetter得到的输出out有序字典,⾸先对x的最后⼀层进⾏处理把x的最后⼀层(第四层)即"3":(batch, 2048, 29, 42)送⼊inner_blocks的最后⼀个卷积层,得到结果(batch,256,29,42),然后再送⼊layer_blocls的最后⼀个卷积层,得到结果(batch,256,29,42)把结果append到result列表中,然后开始从第3个层开始倒序遍历x,先通过inner_blocks,再把刚刚第四层的输出结果使⽤interpolate⽅法上采样到该层同样的⼤⼩得到inner_top_down,然后把上⼀层上采样的结果inner_top_down加上该层经过inner_blocks的结果得到last_inner(这样就完成了特征的融合,既有⾼层的语义信息⼜有底层的较详细的位置信息),最后把
last_inner通过layer_blocks得到该层经过fpn的结果。依次类推,把x的所有层都经过类似的处理,并且为最后⼀层再增添⼀个maxpool层最后得到输出
out:{
“0”:(batch, 256, 232, 336)
“1”:(batch, 256, 116, 168)
“2”:(batch, 256, 58, 84)
“3”:(batch, 256, 29, 42)
“pool”:(batch,256,15,21)
}
backnone部分总结
总结⼀下,backbone部分主要是取resnet⽹络layer4之前的结构,并且使⽤IntermediateLayerGetter得到resnet layer1到layer4的输出,得到输出字典
out:{
“0”:(batch, 256, 232, 336)
“1”:(batch, 512, 116, 168)
“2”:(batch, 1024, 58, 84)
“3”:(batch, 2048, 29, 42)
}
再通过fpn,融合⾼尺度和低尺度的特征信息,并且把输出通道都变成256,并为最后⼀个层增添⼀个额外的pool层得到backbone部分的输出字典
out:{
“0”:(batch, 256, 232, 336)
汽车维修检测“1”:(batch, 256, 116, 168)
“2”:(batch, 256, 58, 84)
北京街舞培训“3”:(batch, 256, 29, 42)
“pool”:(batch,256,15,21)
}
(下⾯图⽚上是草稿纸⼿写的笔记,与上⽂基本相同)
transfrom部分
到这⾥就定义好了backbone部分。fasterrcnn再dataloader读⼊数据时仅仅做了to tensor转为张量的操作,因此读⼊的图像尺⼨都是不⼀样的,在送⼊⽹络之前还要进⾏transfrom操作把数据打包成⼀个batch
transform的代码部分不是很复杂,这⾥把逻辑说⼀下。
transfrom由GeneralizedRCNNTransform类定义,它是⼀个nn.Module
它的forward函数需要的输⼊参数是images和targets,其中images是tensor组成的列表,targets是字典组成的列表,每个字典包含了⼀张图⽚的标注信息。
def forward(lf, images, targets=None):
images =[img for img in images]
for i in range(len(images)):
image = images[i]
target_index = targets[i]if targets is not None el None
image = lf.normalize(image)# 对图像进⾏标准化处理
image, target_index = lf.resize(image, target_index)# 对图像和对应的bboxes缩放到指定范围
images[i]= image
if targets is not None and target_index is not None:
targets[i]= target_index
# 记录resize后的图像尺⼨
image_sizes =[img.shape[-2:]for img in images]
images = lf.batch_images(images)# 将images打包成⼀个batch
image_sizes_list = torch.jit.annotate(List[Tuple[int,int]],[])
for image_size in image_sizes:
asrt len(image_size)==2
image_sizes_list.append((image_size[0], image_size[1]))
image_list = ImageList(images, image_sizes_list)
return image_list, targets
第⼀步是遍历每张图⽚做normalize标准化处理,就是简单的把图⽚转成tensor,并减去预先提供的均值再除⽅差
第⼆步再遍历每张图⽚做resize处理,resize过程就是先得到⼀个batch中图⽚的⾼度和宽度,然后分别获得⾼度和宽度的最⼩值min_size和最⼤值max_size。再使⽤指定最⼩边长和实际图⽚最⼩边长的⽐值做缩放⽐例,把每张图⽚都按⽐例缩放到指定的⼤⼩,如果按此⽐例缩放的结果得到最⼤边长⽐指定最⼤边长⼤,那就改为使⽤指定最⼤边长与实际图⽚最⼤边长的⽐值做缩放⽐例。对图⽚做完缩放后根据相应的缩放⽐例对target中的box也要做缩放。完成resize操作后,把每张图⽚的结果分别放进images列表和targets 列表,并记录⼀下resize之后的尺⼨image_sizes,最后把resize之后的images放进batch_images⽅法,把图⽚打包成⼀个batch
def resize(lf, image, target):
# image shape is [channel, height, width]
# 得到image的⾼度和宽度
h, w = image.shape[-2:]
im_shape = sor(image.shape[-2:])
min_size =float(torch.min(im_shape))# 获取⾼宽中的最⼩值
max_size =float(torch.max(im_shape))# 获取⾼宽中的最⼤值
aining:
size =h_choice(lf.min_size))# 指定输⼊图⽚的最⼩边长,注意是lf.min_size不是min_size
wellnessel:
# FIXME assume for now that testing us the largest scale
size =float(lf.min_size[-1])# 指定输⼊图⽚的最⼩边长,注意是lf.min_size不是min_size
scale_factor = size / min_size # 根据指定最⼩边长和图⽚最⼩边长计算缩放⽐例
# 如果使⽤该缩放⽐例计算的图⽚最⼤边长⼤于指定的最⼤边长
if max_size * scale_factor > lf.max_size:
scale_factor = lf.max_size / max_size # 将缩放⽐例设为指定最⼤边长和图⽚最⼤边长之⽐
# interpolate利⽤插值的⽅法缩放图⽚
minimotor
# image[None]操作是在最前⾯添加batch维度[C, H, W] -> [1, C, H, W]
# bilinear只⽀持4D Tensor
image = functional.interpolate(
image[None], scale_factor=scale_factor, mode='bilinear', align_corners=Fal)[0]
if target is None:
return image, target
bbox = target["boxes"]
# 根据图像的缩放⽐例来缩放bbox
bbox = resize_boxes(bbox,(h, w), image.shape[-2:])
target["boxes"]= bbox
return image, target
第三步是batch_sizes⽅法把⼀批resize之后的图⽚打包成⼀个batch,⾸先获取这⼀个batch中⾼度和宽度的最⼤值,然后把⾼度和宽度的最⼤值都向上调整到32的整数倍,然后创建⼀个shape为(batch_size, 3 ,max_width,max_height)并且全0的batched_imgs,然后遍历images列表,把每张图⽚复制进batched_imgs的相应位置,对其左上⾓,其他部分就是为0的tensors,最后新建⼀个ImageList类,把这⾥得到的tensors赋值给lf.tensors,把之前得到的image_sizes赋值给lf.images_sizes.这样就可以通过sors访问到打包成⼀个batch的shape相同的图⽚数据,也可以通过imagelist.image_sizes⽅法得到图⽚的尺⼨数据。