接上篇博文:
YOLOv3网络在三个特征图中预测出( 4 + 1 + c ) × k 个值,即特征图上每个点对应4个边界坐标(实际为偏移量),1个置信度值,C个类别值(C个类别置信度);数量k为特征图上每个点预测出的边框数量,默认为3:
下图先验框由聚类算法得到:
特征图上的预测目标为:
上图中蓝色字样为最终预测目标,tx及ty为网格中心至网格左上角的偏移量;bx及by为预测看中心点坐标;etw及wth为边框长度缩放因子,pw及ph为先验框的高宽,bw,bh为最终预测框的高宽。sigmod函数将预测框中心坐标偏移量限制在当前网格中(坐标范围为0至1之间),加速收敛。
这样,cx及cy,pw及ph已知,只用预测出tw,th及tx及ty四个值,就能通过计算确定最终目标框的位置。注意,预测出的所有的b值均是经过归一化处理的。
对于分类的类别,使用logistic激活函数(将特征值代入sigmoid函数内部),而不使用softmax激活函数,logistic激活函数彼此相互独立,不同类别之间不会相互抑制,这就方便了对同一目标的多标签检测,比如一个目标既是人,又是男人。
代码实现的主要难点集中在tensor的多维运算。
使用pytorch实现如下:
import os.path
from typing import Iterator
import numpy as np
import torch
import cv2
import matplotlib.pyplot as plt
from PIL import Image
from torch.utils.data import Dataset, DataLoader, Subset, random_split
import re
from functools import reduce
from torch.utils.tensorboard import SummaryWriter as Writer
from torchvision import transforms, datasets
import torchvision as tv
from torch import nn
import torch.nn.functional as F
import time
import math
class DecodeBoxGeneratedFromFeatures():
# 13x13的特征层对应的anchor是[116,90],[156,198],[373,326]
# 26x26的特征层对应的anchor是[30,61],[62,45],[59,119]
# 52x52的特征层对应的anchor是[10,13],[16,30],[33,23]
anchors=\
{ 13:[[116,90],[156,198],[373,326]],
26: [[30,61],[62,45],[59,119]],
52: [[10,13],[16,30],[33,23]],
}
'''由特征图生成对应的预测结果,FeaturesMap为darknet35输出的其中一种特征图
形状为:(batchsize,(5+classNum)*3,width,hight)
'''
def __init__(self,FeaturesMap,classNum):
self.__dict__.update(self.anchors)
self.FeaturesMap=FeaturesMap
#目前高宽总是相等的
self.imgH=FeaturesMap.shape[2]
self.imgW = FeaturesMap.shape[2]
if self.imgH not in [13,26,52]:
raise Exception('输入的特征图尺寸只能是13,26,52中的一种')
self.batchSize=FeaturesMap.shape[0]
#预测的属性
self.predictAttars=FeaturesMap.shape[1]
self.classNum=classNum
#变换原特征图至理想输出向量,默认特征图上每个点输出3个预测框
self.FeaturesMap=self.FeaturesMap.view(self.batchSize,3,5+self.classNum,self.imgH,self.imgH)
#为了便于解析,将输出预测值换到最后一维:
self.FeaturesMap=self.FeaturesMap.permute(0, 1, 3, 4, 2).contiguous()
#提取出相关的预测值:最后一维按顺序为 中心偏移量(tx,ty),宽高缩放比(tw,th),置信度conf,以及classnum的分类向量
'''训练前的原始输出不具有实际意义,需要人为进行指定:同一尺寸特征图的每一个点,都有3个对应的预测输出值,下面
torch.Size([1, 3, 26, 26])中的3,是指每个特征图上的点有3个预测,而不是3通道图像
'''
#tx的形状为:torch.Size([1, 3, 26, 26]),该特征图共计3*26*26个tx值;
self.tx=torch.sigmoid(self.FeaturesMap[...,0])
self.ty = torch.sigmoid(self.FeaturesMap[..., 1])
self.tw=self.FeaturesMap[..., 2]
self.th = self.FeaturesMap[..., 3]
#下面两者配合损失函数使用sigmoid函数:
self.conf = torch.sigmoid(self.FeaturesMap[..., 4])
self.classPrediction=torch.sigmoid(self.FeaturesMap[..., 5:])
#坐标公式中,所有值是以特征图长宽为依据的,下面要对原始尺寸归一化:
#特征图上每点相对于原图来看是多长:
self.scale=416.0/self.imgH
#由聚类产生的Anchor是相对于原图的,将其也相对于特征图:
self.anchorChoised=[[anchor[0]/self.scale,anchor[1]/self.scale] for anchor in self.anchors[self.imgH]]
#生成网格值:即图中的cx及cy:生成类似(1,3,26,26)一样的结构,便于同位置相加
'''其中torch.linspace(start, end, steps=100);repeat将通道1复制self.imgH个,通道2不变,再通过repeat新增第一维,为
self.batchSize * 3,再通过view的方法,将该维分散成self.batchSize,3,同self.tx的形状一致
'''
self.gridx=torch.linspace(0, self.imgW - 1, self.imgW).repeat(self.imgH, 1).repeat(
self.batchSize * 3, 1, 1).view(self.tx.shape).type(torch.FloatTensor)
self.gridy=torch.linspace(0, self.imgW - 1, self.imgW).repeat(self.imgH, 1).repeat(
self.batchSize * 3, 1, 1).view(self.ty.shape).type(torch.FloatTensor)
'''
将self.anchorChoised也处理成与self.tw一致,即torch.Size([1, 3, 26, 26]),方便与缩放系数self.tw相乘:
(注:index_select用法:
dim:表示从第几维挑选数据,类型为int值;
index:表示从第一个参数维度中的哪个位置挑选数据,类型为torch.Tensor类的实例;
使用后原tensor不降维。)
待操作anchorChoised有0维行和1维列,形如:[[116,90],[156,198],[373,326]]
对于生成与self.tw同shape的tensor,先取出第1维中的下标为0的元素:
[[116],[156],[373]],size(3,1):
#'''
step1=torch.FloatTensor(self.anchorChoised).index_select(1, torch.LongTensor([0]))
'''
按行复制self.batchSize个,size(self.batchSize*3,1)
'''
step2=step1.repeat(self.batchSize, 1)
'''
扩充至3维,将列扩充self.imgH * self.imgW倍,size(1,self.batchSize*3,self.imgH * self.imgW)
'''
step3=step2.repeat(1, 1, self.imgH * self.imgW)
'''
调整至size(batchsize,3,imgH, imgW):view函数是将待变换tensor从第0维至第一维展平成1维后再从最后一维开始向第0维按目标shape依次排列而成的,
由于矩阵元素都一致,变换只用关注shape,即可按维度顺序进行重分配,即(1,self.batchSize*3,self.imgH * self.imgW)-》(batchsize,3,imgH, imgW)
'''
anchor_w=step3.view(self.tw.shape)
#下面相同的手法得到anchor_h:
step1 = torch.FloatTensor(self.anchorChoised).index_select(1, torch.LongTensor([1]))
step2 = step1.repeat(self.batchSize, 1)
step3 = step2.repeat(1, 1, self.imgH * self.imgW)
anchor_h = step3.view(self.th.shape)
#盒子位置:x,y,w,h
#首先创建大小一致的容器
self.boxPosition=torch.FloatTensor(self.FeaturesMap[...,0:4].shape)
print(self.boxPosition.shape)
self.boxPosition[...,0]=self.tx+self.gridx
self.boxPosition[..., 1] = self.ty + self.gridy
self.boxPosition[..., 2] = anchor_w*torch.exp(self.tw)
self.boxPosition[..., 3] = anchor_h*torch.exp(self.th)
#输出该尺寸特征图的所有预测Boxs
def givePredictedBoxs(self):
'''注1:self.boxPosition.view(self.batchSize, -1, 4) / _scale这里输出的x.y.w.h为这四个点坐标值相对于特征图的比例值,该比例值乘以特征图长度,就为特征图中box大小位置。
该比例乘以原图,就能得到原图图上box的大小位置。这是因为网络输出特征图和原图差了一个缩放比self.scale,而比例值在各自图中具有不变性,
'''
_scale = torch.Tensor([self.imgH, self.imgW,self.imgH, self.imgW]).type(torch.FloatTensor)
'''注2:这里使用了pytorch的广播机制:
(self.batchSize, -1, 4) / (1,4),从最后一维开始对照,相同维度取最大的一维,再逐元素相乘。
(self.batchSize, -1, 4)
( 1, 4)
按最大取值:
(self.batchSize, -1, 4)
将待乘两向量对应元素相除。
得出最终结果。
'''
output = torch.cat((self.boxPosition.view(self.batchSize, -1, 4) / _scale,
self.conf.view(self.batchSize, -1, 1), self.classPrediction.view(self.batchSize, -1, self.classNum)), -1)
return output.detach()
#测试:用k模拟特征图
k=torch.rand(2,24,26,26)
#设为3分类
a=DecodeBoxGeneratedFromFeatures(k,3)
print(a.givePredictedBoxs().shape)
测试输出结果为:
torch.Size([2, 2028, 8])