目前目标检测点主流算法分为二种类型:
目标检测的算法,通常都是对图片上的四个参数做处理:分别是中心点x轴、y轴坐标,框的高和宽。
对图片做目标检测,通常是通过卷积,将原始图片,卷积成不同尺寸大小的图片,例如一张cat的图片,通过卷积可以生成如下尺寸的图像。
通常尺寸越小的越容易检测大的物体,尺寸越大适合检测小的物体。
先验框功能通常是帮助我们定好常见目标的宽和高,在进行预测的时候,我们可以利用这个已经定好的宽和高处理,可以帮助我们进行预测。
在进行模型训练的时候,通过分割网格,来生成不同的先验框,并先验框对图像进行处理,通过训练调整先验框位置,来正确框出图像中的事物。
每一层网格都对应一层先验框对图像中的事物做处理,可以理解为每一层卷积后对图像中的事物做一次先验框框出目标事物,随着更深层的网格训练,先验框也在不断调整位置和大小,使得框出的事物越来越精确。
过程大致如下图所示:
对于检测COCO数据集,它输出的就是一个(13x13,(80+5)*5)的数据,13x13对应图像的网格点,*5表示每个网格点上有五个先验框,每个先验框有85个参数,其中80对应着有80个网格通道数,5分别对应着先验框中心坐标x、中心点坐标y、先验框高h和宽先验框w、置信度(即分类结果属于那种类型的概率)。
SSD算法是一种单阶段的目标检测算法,通过输入一张图片,神经网络可以预测出对应物体的边框以及对应边框下物体的种类信息。
在图像分类网络构架中,通常使用VGG16算法作为特征提取的骨架网络,而SSD模型与VGG16模型在网络架构有很多相似的,
SSD架构:
SSD算法网络中,通过输入图像进过VGG16的conv1~conv5计算,并保留conv4,conv5的中间特征输出用于后续的预测。
回溯到上面的先验框知识点,这里在SSD算法网络中利用Conv4、fc7、Conv6、Conv7、Conv8、Conv9各层生成的特征,增加先验框处理,从Conv4到Conv9实现先验框不断的优化。
先验框操作:
Conv4到Conv9中对应为(4、6、6、6、4、4)
classes为 21
最后根据得到的预测置信度,进行得分排序与非极大抑制筛选(去除多余的边框)
VGG-16代码实现:
这里只实现Conv1~Conv4层代码
class VGG16(nn.Module):
def __init__(self):
super(VGG16,self).__init__()
self.layers=self.make_layers()
def forward(self,x):
y=self.layers(x)
return y
def make_layers(self):
cfg=[64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'C', 512, 512, 512,'M',512, 512, 512]
layers=[]
in_channels=3
for x in cfg:
if x=='M':
layers+=[nn.MaxPool2d(kernel_size=2,stride=2)]
elif x=='c':
layers=[nn.MaxPool2d(kernel_size=2, stride=2, ceil_mode=True)]
else:
layers+=[nn.Conv2d(in_channels,x,kernel_size=3,padding=1),nn.ReLU(True)]
in_channels=x
return nn.Sequential(*layers)
SSD模型特征提取代码实现:
这里实现Conv5~Conv9代码
class SSD(nn.Module):
def __init__(self):
super(SSD,self).__init__()
#导入上面的模块
self.features=VGG16()
self.norm4=L2Norm(512,20)
self.conv5_1=nn.Conv2d(512,512,kernel_size=3,padding=1,dilation=1)
self.conv5_2=nn.Conv2d(512,512,kernel_size=3,padding=1,dilation=1)
self.conv5_3=nn.Conv2d(512,512,kernel_size=3,padding=1,dilation=1)
self.fc6=nn.Conv2d(512,1024,kernel_size=3,padding=6,dilation=6)
self.fc7=nn.Conv2d(1024,1024,kernel_size=1)
self.conv6_1=nn.Conv2d(1024,256,kernel_size=1,stride=1)
self.conv6_2=nn.Conv2d(256,512,kernel_size=3,stride=2,padding=1)
self.conv7_1=nn.Conv2d(512,128,kernel_size=1, stride=1)
self.conv7_2=nn.Conv2d(128, 256, kernel_size=3, stride=2, padding=1))
self.conv8_1=nn.Conv2d(256, 128, kernel_size=1, stride=1)
self.conv8_2=nn.Conv2d(128, 256, kernel_size=3, stride=1)
self.conv9_1=nn.Conv2d(256, 128, kernel_size=1, stride=1)
self.conv9_2=nn.Conv2d(128, 256, kernel_size=3, stride=1)
def forward(self,x):
hs=[]
h=self.features(x)
hs.append(self.norm4(h))
h=F.max_pool2d(kernel_size=2, stride=2, ceil_mode=True)
h=F.relu(self.conv5_1(h))
h=F.relu(self.conv5_2(h))
h=F.relu(self.conv5_3(h))
h=F.max_pool2d(h,kernel_size=3, stride=1, padding=1,ceil_mode=True)
h=F.relu(self.fc6(h))
h=F.relu(self.fc7(h))
hs.append(h) #第一个先验框
h=F.relu(self.conv6_1(h))
h=F.relu(self.conv6_2(h))
hs.append(h) #第二个先验框
h=F.relu(self.conv7_1(h))
h=F.relu(self.conv7_2(h))
hs.append(h) #第三个先验框
h=F.relu(self.conv8_1(h))
h=F.relu(self.conv8_2(h))
hs.append(h) #第四个先验框
h=F.relu(self.conv9_1(h))
h=F.relu(self.conv9_2(h))
hs.append(h) #第五个先验框
return hs
class L2Norm(nn.Module):
def __init__(self,in_features,scale):
super(L2Norm,self).__init__()
self.weight=nn.Parameter(torch.Tensor(in_features))
self.reset_parameters(scale)
def reset_parameters(self,scale):
nn.init.constant(self.weight,scale)
def forward(self,x):
x=F.normalize(x,dim=1)
scale=self.weight(None,:,None,None)
return scale*x
先验框代码实现(获取特征):
class get_ssd(nn.Module):
#定义步长
steps=(8,16,32,64,100,300)
#定义先验框的基础大小
box_sizes=(30,60,111,162,213,264,315)
#定义先验框的高宽比
aspect_ratios=((2,),(2,3),(2,3),(2,3),(2,),(2,))
fm_sizes=(38,19,10,5,3,1)
def __init__(self,num_classes):
super(get_ssd,self).__init()
self.num_classes=num_classes
self.num_anchors=(4,6,6,6,4,4)
self.in_channels=(512,1024,512,256,256,256)
self.extractor=SSD()
self.loc_layers=nn.ModuleList()
self.cls_layers=nn.ModuleList()
for i in range(len(self.in_channels)):
self.loc_layers+=[nn.Conv2d(self.in_channels[i],self.num_anchors[i]*4,kernel_size=3,padding=1)]
self.cls_layers+=[nn.Conv2d(self.in_channels[i],self.num_anchors[i]*self.num_classes,kernel_size=3,padding=1)]
def forward(self,x):
loc_preds=[]
cls_preds=[]
xs=self.extractor(x)
for i,x in enumerate(xs):
loc_pred=self.loc_layers[i][x]
loc_pred=loc_pred.permute(0,2,3,1).contiguous()
loc_preds.append(loc_pred.view(loc_pred.size(0),-1,4))
cls_pred=self.cls_layers[i][x]
cls_pred=cls_pred.permute(0,2,3,1).contiguous()
cls_preds.append(cls_pred.view(cls_pred.size(0),-1,self.num_classes))
los_preds=torch.cat(loc_preds,1)
cls_preds=torch.cat(cls_preds,1)
return loc_preds,cls_preds
先验框函数:
def default_prior_box():
mean_layer = []
for k,f in enumerate(Config.feature_map):
mean = []
for i,j in product(range(f),repeat=2):
f_k = Config.image_size/Config.steps[k]
cx = (j+0.5)/f_k
cy = (i+0.5)/f_k
s_k = Config.sk[k]/Config.image_size
mean += [cx,cy,s_k,s_k]
s_k_prime = sqrt(s_k * Config.sk[k+1]/Config.image_size)
mean += [cx,cy,s_k_prime,s_k_prime]
for ar in Config.aspect_ratios[k]:
mean += [cx, cy, s_k * sqrt(ar), s_k/sqrt(ar)]
mean += [cx, cy, s_k / sqrt(ar), s_k * sqrt(ar)]
if Config.use_cuda:
mean = torch.Tensor(mean).cuda().view(Config.feature_map[k], Config.feature_map[k], -1).contiguous()
else:
mean = torch.Tensor(mean).view( Config.feature_map[k],Config.feature_map[k],-1).contiguous()
mean.clamp_(max=1, min=0)
mean_layer.append(mean)
return mean_layer
损失函数计算:
class LossFun(nn.Module):
def __init__(self):
super(LossFun,self).__init__()
def forward(self, prediction,targets,priors_boxes):
loc_data , conf_data = prediction
loc_data = torch.cat([o.view(o.size(0),-1,4) for o in loc_data] ,1)
conf_data = torch.cat([o.view(o.size(0),-1,21) for o in conf_data],1)
priors_boxes = torch.cat([o.view(-1,4) for o in priors_boxes],0)
if Config.use_cuda:
loc_data = loc_data.cuda()
conf_data = conf_data.cuda()
priors_boxes = priors_boxes.cuda()
# batch_size
batch_num = loc_data.size(0)
# default_box数量
box_num = loc_data.size(1)
# 存储targets根据每一个prior_box变换后的数据
target_loc = torch.Tensor(batch_num,box_num,4)
target_loc.requires_grad_(requires_grad=False)
# 存储每一个default_box预测的种类
target_conf = torch.LongTensor(batch_num,box_num)
target_conf.requires_grad_(requires_grad=False)
if Config.use_cuda:
target_loc = target_loc.cuda()
target_conf = target_conf.cuda()
# 因为一次batch可能有多个图,每次循环计算出一个图中的box,即8732个box的loc和conf,存放在target_loc和target_conf中
for batch_id in range(batch_num):
target_truths = targets[batch_id][:,:-1].data
target_labels = targets[batch_id][:,-1].data
if Config.use_cuda:
target_truths = target_truths.cuda()
target_labels = target_labels.cuda()
# 计算box函数,即公式中loc损失函数的计算公式
utils.match(0.5,target_truths,priors_boxes,target_labels,target_loc,target_conf,batch_id)
pos = target_conf > 0
pos_idx = pos.unsqueeze(pos.dim()).expand_as(loc_data)
# 相当于论文中L1损失函数乘xij的操作
pre_loc_xij = loc_data[pos_idx].view(-1,4)
tar_loc_xij = target_loc[pos_idx].view(-1,4)
# 将计算好的loc和预测进行smooth_li损失函数
loss_loc = F.smooth_l1_loss(pre_loc_xij,tar_loc_xij,size_average=False)
batch_conf = conf_data.view(-1,21)
# 参照论文中conf计算方式,求出ci
loss_c = utils.log_sum_exp(batch_conf) - batch_conf.gather(1, target_conf.view(-1, 1))
loss_c = loss_c.view(batch_num, -1)
# 将正样本设定为0
loss_c[pos] = 0
# 将剩下的负样本排序,选出目标数量的负样本
_, loss_idx = loss_c.sort(1, descending=True)
_, idx_rank = loss_idx.sort(1)
num_pos = pos.long().sum(1, keepdim=True)
num_neg = torch.clamp(3*num_pos, max=pos.size(1)-1)
# 提取出正负样本
neg = idx_rank < num_neg.expand_as(idx_rank)
pos_idx = pos.unsqueeze(2).expand_as(conf_data)
neg_idx = neg.unsqueeze(2).expand_as(conf_data)
conf_p = conf_data[(pos_idx+neg_idx).gt(0)].view(-1, 21)
targets_weighted = target_conf[(pos+neg).gt(0)]
loss_c = F.cross_entropy(conf_p, targets_weighted, size_average=False)
N = num_pos.data.sum().double()
loss_l = loss_loc.double()
loss_c = loss_c.double()
loss_l /= N
loss_c /= N
return loss_l, loss_c