【项目一、xxx病虫害检测项目】1、SSD原理和源码分析
目录
- 前言
- 一、SSD backbone
- 1.1、总体结构
- 1.2、修改vgg
- 1.3、额外添加层
- 1.4、需要注意的点
- 二、SSD head
- 2.1、检测头predictor
- 2.2、生成default box
- 2.3、计算分类回归损失
- 2.3.1、正负样本匹配:MaxIOUAssign
- 2.3.2、平衡正负样本:在线困难样本挖掘
- 2.5、后处理
- 2.5.1、回归参数编解码:
- 2.5.2、NMS
- 三、数据增强
- Reference
前言
马上要找工作了,想总结下自己做过的几个小项目。
先总结下实验室之前的一个病虫害检测相关的项目。选用的baseline是SSD,代码是在这个仓库的基础上改的 lufficc/SSD.这个仓库写的ssd还是很牛的,github有1.3k个star。
选择这个版本的代码,主要有两个原因:
- 它的backbone代码是支持直接加载pytorch官方预训练权重的,所以很方便我做实验
- 代码高度模块化,类似mmdetection和Detectron2,写的很高级,不过对初学者不是很友好,但是很能提高工程代码能力。
原仓库主要实现了SSD-VGG16、SSD-Mobilenet-V2、SSD-Mobilenet-V3、SSD-EfficientNet等网络,在我数据集上几个改进版本都还不如SSD-VGG16效果好,所以我在原仓库的基础上进行了自己的实验,加了一些也不算很高级的trick吧,主要是在我的数据集上确实好使,疯狂调参,哈哈哈。
后续代码也会上传到github上的。
同系列讲解:
【项目一、xxx病虫害检测项目】2、网络结构尝试改进:Resnet50、SE、CBAM、Feature Fusion.
【项目一、xxx病虫害检测项目】3、损失函数尝试:Focal loss.
第一篇,先介绍下SSD的原理和源码吧。
代码已全部上传GitHub: HuKai-cv/FFSSD-ResNet..
一、SSD backbone
1.1、总体结构
总体结构:
backbone主要由两个部分构成:第一个部分是原先的vgg结构(conv1_1->conv_53)和替换vgg的部分(pool5->conv7);第二个部分是一些额外的添加层结构:conv8_1->conv11_2。总共有6个预测特征层,分别是Conv4_3、Conv7、Conv8_2、Conv9_2、Conv10_2、Conv11_2,前面两个是backbone生成的,后面4个是额外添加层生成的。
对应代码在ssd/modeling/backbone/vgg.py中:
@registry.BACKBONES.register('vgg')
def vgg(cfg, pretrained=True):"""搭建vgg模型+加载预训练权重Args:cfg: vgg配置文件pretrained: 是否加载预训练模型Returns:返回加载完预训练权重的vgg模型"""model = VGG(cfg) # 搭建vgg模型if pretrained: # 加载预训练权重# 这里默认会从amazonaws平台下载vgg模型预训练权重文件model.init_from_pretrain(load_state_dict_from_url(model_urls['vgg']))return modelvgg_base = {'300': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'C', 512, 512, 512, 'M',512, 512, 512],'512': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'C', 512, 512, 512, 'M',512, 512, 512],
}
extras_base = {'300': [256, 'S', 512, 128, 'S', 256, 128, 256, 128, 256],'512': [256, 'S', 512, 128, 'S', 256, 128, 'S', 256, 128, 'S', 256],
}class VGG(nn.Module):def __init__(self, cfg):super().__init__()size = cfg.INPUT.IMAGE_SIZE # 输入图片大小# ssd-vgg16 backbone配置信息 [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'C', 512, 512, 512, 'M', 512, 512, 512]vgg_config = vgg_base[str(size)]# vgg之后额外的一些特征提取层配置信息 [256, 'S', 512, 128, 'S', 256, 128, 'S', 256, 128, 'S', 256]extras_config = extras_base[str(size)]# 初始化backboneself.vgg = nn.ModuleList(add_vgg(vgg_config))# 初始化额外特征提取层self.extras = nn.ModuleList(add_extras(extras_config, i=1024, size=size))self.l2_norm = L2Norm(512, scale=20)self.reset_parameters() # 参数初始化def reset_parameters(self): # 参数初始化for m in self.extras.modules():if isinstance(m, nn.Conv2d):nn.init.xavier_uniform_(m.weight)nn.init.zeros_(m.bias)def init_from_pretrain(self, state_dict): # 载入backbone预训练权重self.vgg.load_state_dict(state_dict)def forward(self, x):features = [] # 存放6个预测特征层# 前23层conv1_1->conv4_3(包括relu层)前序传播for i in range(23):x = self.vgg[i](x)s = self.l2_norm(x) # Conv4_3 L2 normalization 论文里这样说的 不知道为什么features.append(s) # append conv4_3# apply vgg up to fc7for i in range(23, len(self.vgg)):x = self.vgg[i](x)features.append(x) # append conv7for k, v in enumerate(self.extras):x = F.relu(v(x), inplace=True)if k % 2 == 1:features.append(x) # append conv8_2 conv9_2 conv10_2 conv11_2# ssd300返回六个预测特征层 ssd512返回7个预测特征层return tuple(features)
1.2、修改vgg
模型结构的部分对应的是vgg中的Conv1_1->Conv5_3,如下图所示(图片来自b站霹雳吧啦Wz:2.1SSD算法理论.):
而Conv4_3得到的特征图就是我们的第一个预测特征图,Conv5_3之后接一个pool5,和原论文不同的是,这里将pool5从原来的2x2/2变为3x3/1,这样经过pool5之后特征层尺寸不变。再之和再接一个3x3的卷积Conv6和1x1卷积Conv7代替vgg16中的两个全连接层fc6、fc7,到此backbone-vgg部分就全部构建完成了。而且Conv7的输出特征层就算第二个预测特征图。
对比下(图片来自just_sort: 目标检测算法之SSD代码解析(万字长文超详细).):
对应代码在ssd/modeling/backbone/vgg.py中:
# borrowed from https://github.com/amdegroot/ssd.pytorch/blob/master/ssd.py
def add_vgg(cfg, batch_norm=False):"""根据配置文件搭建SSD中的backbone(Conv1_1->Conv7)模型结构pool3x3/1替换vgg中的pool2x2/2 conv6(空洞卷积 r=6)、conv7替换fc6、fc7 其中conv4_3、conv7都得到预测特征图Args:cfg: Conv1_1->Conv7配置文件 [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'C', 512, 512, 512, 'M', 512, 512, 512]数字表示当前卷积层的输出channel 'M'表示maxpooling采用向下取整的形式如9x9->4x4 'C'相反表示向上取整如9x9->5x5batch_norm: True conv+bn+reluFalse conv relu 原论文是没有bn层的Returns: backbone层结构 卷积层+maxpool共17层"""layers = []in_channels = 3for v in cfg:if v == 'M':layers += [nn.MaxPool2d(kernel_size=2, stride=2)]elif v == 'C':# ceil mode feature map size是奇数时 最大池化下采样向上取整 如 9x9->5x5layers += [nn.MaxPool2d(kernel_size=2, stride=2, ceil_mode=True)]else:conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)if batch_norm:layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]else:layers += [conv2d, nn.ReLU(inplace=True)]in_channels = v # 下一层的输入=这一层的输出# 论文里有说将vgg中的pool5的2x2/2变为3x3/1 这样这层的池化后特征图尺寸不变pool5 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)# conv6使用了空洞卷积 原论文也是这样说的conv6 = nn.Conv2d(512, 1024, kernel_size=3, padding=6, dilation=6)conv7 = nn.Conv2d(1024, 1024, kernel_size=1)layers += [pool5, conv6,nn.ReLU(inplace=True), conv7, nn.ReLU(inplace=True)]return layers
1.3、额外添加层
额外添加层主要由4个部分构成,Conv8:conv1x1x256 + conv3x3x512/2;Conv9:conv1x1x128 + conv3x3x256/2;Conv10:conv1x1x128 + conv3x3x256/1;Conv11:conv1x1x128 + conv3x3x256/1
对应代码在ssd/modeling/backbone/vgg.py中:
def add_extras(cfg, i, size=300):"""backbone之后的4个额外添加层 生成4个预测特征图Extra layers added to VGG for feature scalingArgs:cfg: 4个额外添加层的配置文件 [256, 'S', 512, 128, 'S', 256, 128, 'S', 256, 128, 'S', 256]数字代表当前卷积层输出channel当前卷积层s=1 'S'代表当前卷积层s=2i: 1024 第一个额外特征层 也就是conv8_1的输入channelsize: 输入图片大小Returns: 4个额外添加层结构"""layers = []in_channels = iflag = Falsefor k, v in enumerate(cfg):if in_channels != 'S':if v == 'S':layers += [nn.Conv2d(in_channels, cfg[k + 1], kernel_size=(1, 3)[flag], stride=2, padding=1)]else:layers += [nn.Conv2d(in_channels, v, kernel_size=(1, 3)[flag])]flag = not flagin_channels = vif size == 512:layers.append(nn.Conv2d(in_channels, 128, kernel_size=1, stride=1))layers.append(nn.Conv2d(128, 256, kernel_size=4, stride=1, padding=1))return layers
好了整个网络结构介绍完了。
1.4、需要注意的点
- ssd整个网络没有bn
- ssd的conv4_3输出的特征图进行了L2 norm操作,因为网络层靠前,方差比较大,需要加一个L2标准化,以保证和后面的检测层差异不是很大。
- 3、conv6 用了3x3空洞卷积 p=6 r=6
二、SSD head
SSD head分为三部分,第一部分将6/7个预测特征层输入检测头得到最终的预测回归参数和预测分类结果;第二部分训练时根据最终的预测回归参数和预测分类结果和gt计算损失;第三部分测试时对预测分类结果进行softmax处理,对预测回归参数利用default box进行解码,nms等处理。
对应代码在ssd/modeling/box_head/box_head.py中:
@registry.BOX_HEADS.register('SSDBoxHead')
class SSDBoxHead(nn.Module):def __init__(self, cfg):super().__init__()self.cfg = cfgself.predictor = make_box_predictor(cfg) # 预测器 类型SSDBoxPredictorself.loss_evaluator = MultiBoxLoss(neg_pos_ratio=cfg.MODEL.NEG_POS_RATIO) # 损失函数 类型MutiBoxLoss# self.loss_evaluator = Focal_loss(neg_pos_ratio=cfg.MODEL.NEG_POS_RATIO)self.post_processor = PostProcessor(cfg) # 后处理self.priors = None # default boxdef forward(self, features, targets=None):"""Args:features: tuple7 [bs,512,64,64] [bs,1024,32,32] [bs,512,16,16] [bs,256,8,8] [bs,256,4,4] [bs,256,2,2] [bs,256,1,1]targets: 'boxes': [bs, 24564, 4] 'labels': [bs, 24564]Returns:"""# cls_logits: [bs,all_anchors,num_classes]=[bs, 24564, 6]# bbox_pred: [bs,all_anchors,xywh]=[bs, 24564, 4]cls_logits, bbox_pred = self.predictor(features) # 预测器前线传播if self.training: # 训练返回预测结果和lossreturn self._forward_train(cls_logits, bbox_pred, targets)else: # 测试和验证返回预测结果和{}return self._forward_test(cls_logits, bbox_pred)def _forward_train(self, cls_logits, bbox_pred, targets):"""Args:cls_logits: 分类预测结果 [bs,all_anchors,num_classes]=[bs, 24564, 6] 0代表背景类 真实类别=num_classes-1bbox_pred: 回归预测结果 [bs,all_anchors,xywh]=[bs, 24564, 4]targets: gt 'boxes': [bs, 24564, 4] 'labels': [bs, 24564]Returns: 预测结果+loss"""# gt_boxes: [bs, 24564, 4] gt_labels: [bs, 24564]gt_boxes, gt_labels = targets['boxes'], targets['labels']# 计算损失reg_loss, cls_loss = self.loss_evaluator(cls_logits, bbox_pred, gt_labels, gt_boxes) # 回归损失 + 分类损失loss_dict = dict(reg_loss=reg_loss,cls_loss=cls_loss,)detections = (cls_logits, bbox_pred)return detections, loss_dict # 返回预测结果和lossdef _forward_test(self, cls_logits, bbox_pred):"""Args:cls_logits: 分类预测结果 [bs,all_anchors,num_classes]=[bs, 24564, 6]bbox_pred: 回归预测结果 [bs,all_anchors,xywh]=[bs, 24564, 4]Returns:"""if self.priors is None:self.priors = PriorBox(self.cfg)().to(bbox_pred.device)scores = F.softmax(cls_logits, dim=2) # 分类结果在classes_num维度进行softmax [bs,24564,num_classes]# 利用default box将预测回归参数进行解码 [bs, num_anchors, xywh(归一化后的真实的预测坐标)]boxes = box_utils.convert_locations_to_boxes(bbox_pred, self.priors, self.cfg.MODEL.CENTER_VARIANCE, self.cfg.MODEL.SIZE_VARIANCE)# xywh->xyxy 将归一化后的预测坐标由xywh->xyxyboxes = box_utils.center_form_to_corner_form(boxes)detections = (scores, boxes)detections = self.post_processor(detections) # 后处理return detections, {}
2.1、检测头predictor
第一部分网络结构总共得到6个特征图,分别是Conv4_3,Conv7、Conv8_2、Conv9_2、Conv10_2、Conv11_2的输出特征图。接下来就是就这些预测特征层送入检测头中了,其实就是一个3x3个Conv直接对default box进行分类预测和回归预测。
base检测头对应代码在ssd/modeling/box_head/box_predictor.py中:
class BoxPredictor(nn.Module): # base box predictordef __init__(self, cfg):super().__init__()self.cfg = cfg # predictor配置文件self.cls_headers = nn.ModuleList() # 7个预测特征层对应的7个分类头 全是3x3conv s=1 p=1self.reg_headers = nn.ModuleList() # 7个预测特征层对应的7个回归头 全是3x3cong s=1 p=1# 每层default box个数 [4, 6, 6, 6, 6, 4, 4]# 7个预测特征层输出channel数=7个分类头/回归头的输入channel (512, 1024, 512, 256, 256, 256, 256)for level, (boxes_per_location, out_channels) in enumerate(zip(cfg.MODEL.PRIORS.BOXES_PER_LOCATION, cfg.MODEL.BACKBONE.OUT_CHANNELS)):# conv3x3 s=1 p=1 in_channel=(512, 1024, 512, 256, 256, 256, 256) out_channel=[4, 6, 6, 6, 6, 4, 4]*num_classesself.cls_headers.append(self.cls_block(level, out_channels, boxes_per_location))# conv3x3 s=1 p=1 in_channel=(512, 1024, 512, 256, 256, 256, 256) out_channel=[4, 6, 6, 6, 6, 4, 4]*4self.reg_headers.append(self.reg_block(level, out_channels, boxes_per_location))self.reset_parameters()def cls_block(self, level, out_channels, boxes_per_location):raise NotImplementedErrordef reg_block(self, level, out_channels, boxes_per_location):raise NotImplementedErrordef reset_parameters(self): # 初始化权重for m in self.modules():if isinstance(m, nn.Conv2d):nn.init.xavier_uniform_(m.weight)nn.init.zeros_(m.bias)def forward(self, features):cls_logits = [] # 存放所有head分类预测结果bbox_pred = [] # 存放所有head回归预测结果for feature, cls_header, reg_header in zip(features, self.cls_headers, self.reg_headers):# [bs,h,w,num_classes*box_nums] 其中num_classes=6 box_nums=[4, 6, 6, 6, 6, 4, 4]# [bs,64,64,24] [bs,32,32,32] [bs,16,16,32] [bs,64,64,32] [bs,64,64,32] [bs,64,64,24] [bs,64,64,24]cls_logits.append(cls_header(feature).permute(0, 2, 3, 1).contiguous())# [bs,h,w,num_classes*4] 其中num_classes=6 4=xywh# [bs,64,64,16] [bs,32,32,24] [bs,16,16,24] [bs,64,64,24] [bs,64,64,24] [bs,64,64,16] [bs,64,64,16]bbox_pred.append(reg_header(feature).permute(0, 2, 3, 1).contiguous())batch_size = features[0].shape[0]# [bs,all_anchors,num_classes]=[2,24564,6]cls_logits = torch.cat([c.view(c.shape[0], -1) for c in cls_logits], dim=1).view(batch_size, -1, self.cfg.MODEL.NUM_CLASSES)# [bs,all_anchors,xywh]=[bs,24564,4]bbox_pred = torch.cat([l.view(l.shape[0], -1) for l in bbox_pred], dim=1).view(batch_size, -1, 4)return cls_logits, bbox_pred
可以看到默认是没有检测头的,但是SSD继承base head的同时重写了检测头对应代码在ssd/modeling/box_head/box_predictor.py中::
@registry.BOX_PREDICTORS.register('SSDBoxPredictor')
class SSDBoxPredictor(BoxPredictor):# 继承自base box predict BoxPredictor# 重写分类预测头def cls_block(self, level, out_channels, boxes_per_location):return nn.Conv2d(out_channels, boxes_per_location * self.cfg.MODEL.NUM_CLASSES, kernel_size=3, stride=1, padding=1)# 重写回归预测头def reg_block(self, level, out_channels, boxes_per_location):return nn.Conv2d(out_channels, boxes_per_location * 4, kernel_size=3, stride=1, padding=1)
2.2、生成default box
论文生成过程:
Default box的设置包含尺度min_size、max_size和长宽比两个方面。
尺度公式如下:
m代表特征层层数,这里是5,因为第一层单独设置。s_k表示anchor尺寸相对于原图的比例,s_min和s_max表示比例的最小值和最大值,论文中分别为0.2和0.9。第一层单独设置比例为s_min/2=0.1,所以它的尺度为300x0.1=30,后面的特征图按照线性公式计算,且下一层的min_size=这一层的max_size。最后可以得到每个特征图的min_size和max_size。
再为每一层人为设置了长宽比;
根据每层的min_size和max_size和长宽比生成各层的default box(1个min_size的1:1;1个min_size*max_size的1:1;2n)。
但是实际代码中default box的尺度和长宽比都是人为设置的。
对应代码在ssd/modeling/anchors/prior_box.py中:
class PriorBox:def __init__(self, cfg):# 这里的min_size和max_size是直接给出的 论文是公式求出来的self.image_size = cfg.INPUT.IMAGE_SIZE # 512 图片大小prior_config = cfg.MODEL.PRIORSself.feature_maps = prior_config.FEATURE_MAPS # 所有层的featue map size [64, 32, 16, 8, 4, 2, 1]self.min_sizes = prior_config.MIN_SIZES # 所有层的min_size[35.84, 76.8, 153.6, 230.4, 307.2, 384.0, 460.8]self.max_sizes = prior_config.MAX_SIZES # 所有层的max_size[76.8, 153.6, 230.4, 307.2, 384.0, 460.8, 537.65]self.strides = prior_config.STRIDES # 所有层的stride[8, 16, 32, 64, 128, 256, 512]self.aspect_ratios = prior_config.ASPECT_RATIOS # 所有层的aspect ratio[[2], [2, 3], [2, 3], [2, 3], [2, 3], [2], [2]]self.clip = prior_config.CLIP # Truedef __call__(self):"""Generate SSD Prior Boxes.It returns the center, height and width of the priors. The values are relative to the image sizeReturns:priors (num_priors, 4): The prior boxes represented as [[center_x, center_y, w, h]]. All the valuesare relative to the image size."""priors = []for k, f in enumerate(self.feature_maps):scale = self.image_size / self.strides[k]for i, j in product(range(f), repeat=2):# unit center x,y default box中心点坐标(相对特征图)cx = (j + 0.5) / scalecy = (i + 0.5) / scale# 生成default box# small sized square box w/h=1:1size = self.min_sizes[k]h = w = size / self.image_sizepriors.append([cx, cy, w, h])# big sized square box w/h=1:1size = sqrt(self.min_sizes[k] * self.max_sizes[k])h = w = size / self.image_sizepriors.append([cx, cy, w, h])# change h/w ratio of the small sized box w/h=ratio and 1/ratiosize = self.min_sizes[k]h = w = size / self.image_sizefor ratio in self.aspect_ratios[k]:ratio = sqrt(ratio)priors.append([cx, cy, w * ratio, h / ratio])priors.append([cx, cy, w / ratio, h * ratio])priors = torch.tensor(priors)if self.clip:priors.clamp_(max=1, min=0)return priors
2.3、计算分类回归损失
损失分为两个部分分类损失+回归损失,分类损失针对正样本+负样本(困难样本挖掘负样本:正样本=3:1)用的是softmax+cross entropy。在ssd和faster rcnn中分类损失其实也包含了置信度(预测框包含物体的概率)损失,因为ssd分类通常会将背景也当作一个类别,最后输出接softmax得到20个类别分数和1个置信度分数。;回归损失针对所有正样本使用smooth l1损失函数。
对应代码在ssd/modeling/box_head/loss.py中:
class MultiBoxLoss(nn.Module):def __init__(self, neg_pos_ratio):"""Implement SSD MultiBox Loss.Basically, MultiBox loss combines classification lossand Smooth L1 regression loss."""super(MultiBoxLoss, self).__init__()self.neg_pos_ratio = neg_pos_ratio # 负样本/正样本=3:1def forward(self, confidence, predicted_locations, labels, gt_locations):"""计算分类损失和回归损失Args:confidence: 分类预测结果 [bs,all_anchors,num_classes]=[bs, 24564, 6] 0代表背景类 真实类别=num_classes-1predicted_locations: 回归预测结果 [bs,all_anchors,xywh]=[bs, 24564, 4]labels: gt_labels 所有anchor的真实框类别标签 0表示背景类 [bs, 24564]gt_locations: 所有anchor的真实框回归标签 [bs, 24564, 4]Returns:"""num_classes = confidence.size(2) # 6with torch.no_grad(): # 困难样本挖掘# derived from cross_entropy=sum(log(p))# log_softmax(confidence, dim=2): [bs,24564,6] 对confidence的num_classes维度进行softmax出来再log# -F.log_softmax(confidence, dim=2)[:, :, 0]: [bs,24564] 所有anchor属于背景的损失loss = -F.log_softmax(confidence, dim=2)[:, :, 0] # [bs,anchor_nums]mask = box_utils.hard_negative_mining(loss, labels, self.neg_pos_ratio)confidence = confidence[mask, :] # [104,6] 正样本[1,25] 负样本[3,75] 正样本+负样本=1+25 + 3+75 = 104# 分类损失 sum 正样本:负样本=3:1 预测输入: [pos+neg, num_classes] gt输入: [pos+neg]# F.cross_entropy: 1、对预测输入进行softmax+log运算 -> [pos+neg, num_classes]# 2、对gt输入进行one-hot编码 [pos+neg] -> [pos+neg, num_classes]# 3、再对预测和gt进行交叉熵损失计算classification_loss = F.cross_entropy(confidence.view(-1, num_classes), labels[mask], reduction='sum')pos_mask = labels > 0predicted_locations = predicted_locations[pos_mask, :].view(-1, 4) # 正样本预测回归参数 [26,4]gt_locations = gt_locations[pos_mask, :].view(-1, 4) # 正样本gt label [26,4]# 回归loss smooth l1 sum(只计算正样本)smooth_l1_loss = F.smooth_l1_loss(predicted_locations, gt_locations, reduction='sum')num_pos = gt_locations.size(0) # 正样本个数 26# 回归总损失(正样本)/正样本个数 分类总损失(正样本+负样本)/正样本个数return smooth_l1_loss / num_pos, classification_loss / num_pos
2.3.1、正负样本匹配:MaxIOUAssign
正负样本匹配规则:
- 正样本:对每个gt找到和它iou最大的default box,作为这个gt的正样本,这样可以保证每个gt至少有一个正样本;一个default box和所有gt的最大IOU大于0.5时,这个default box为正样本;(1和2同时起作用,可能某个gt会匹配到多个正样本,但是一个default box只能是一个gt的正样本。)
- 负样本:一个default box和所有gt的最大IOU小于0.5时,该default box为负样本;
- 没有忽略样本
对应代码在ssd/utils/box_utils.py中:
def assign_priors(gt_boxes, gt_labels, corner_form_priors, iou_threshold):"""把每个prior box进行正负样本匹配Assign ground truth boxes and targets to priors.Args:gt_boxes (num_targets, 4): ground truth boxes. 图片的所有类别gt_labels (num_targets): labels of targets. gt图片的所有类别priors (num_priors, 4): corner form priors 先验框 default box xyxyiou_threshold: 0.5 IOU阈值,小于阈值设为背景Returns:boxes (num_priors, 4): real values for priors.labels (num_priros): labels for priors."""# size: num_priors x num_targets gt和default box的iouious = iou_of(gt_boxes.unsqueeze(0), corner_form_priors.unsqueeze(1))# size: num_priors 和每个ground truth box 交集最大的 prior boxbest_target_per_prior, best_target_per_prior_index = ious.max(1)# size: num_targets 和每个prior box 交集最大的 ground truth boxbest_prior_per_target, best_prior_per_target_index = ious.max(0)# 保证每一个ground truth 匹配它的都是具有最大IOU的prior# 根据 best_prior_dix 锁定 best_truth_idx里面的最大IOU priorfor target_index, prior_index in enumerate(best_prior_per_target_index):best_target_per_prior_index[prior_index] = target_index# 2.0 is used to make sure every target has a prior assigned# 保证每个ground truth box 与某一个prior box 匹配,固定值为 2 > thresholdbest_target_per_prior.index_fill_(0, best_prior_per_target_index, 2)# size: num_priors 提取出所有匹配的ground truth boxlabels = gt_labels[best_target_per_prior_index]# 把 iou < threshold 的框类别设置为 bg,即为0labels[best_target_per_prior < iou_threshold] = 0 # the backgournd id# 匹配好boxesboxes = gt_boxes[best_target_per_prior_index]return boxes, labels
2.3.2、平衡正负样本:在线困难样本挖掘
困难样本挖掘主要是为了针对负样本太多的情况,如果全部负样本都参与计算损失,那么一定会造成正负样本的比例失衡,所以这里主要计算一些较为困难的负样本的损失,即loss较大的负样本。
具体做法:将所有负样本的分类损失按降序排序,取前正样本3个负样本作为最终的分类负样本,参与分类损失计算。
正样本个数=所有正样本个数 负样本个数=正样本个数3
对应代码在ssd/utils/box_utils.py中:
def hard_negative_mining(loss, labels, neg_pos_ratio):"""It used to suppress the presence of a large number of negative prediction.It works on image level not batch level.For any example/image, it keeps all the positive predictions andcut the number of negative predictions to make sure the ratiobetween the negative examples and positive examples is no morethe given ratio for an image.Args:loss (N, num_priors): the loss for each example. [bs,24564] 所有anchor属于背景的损失labels (N, num_priors): the labels. 所有anchor的真实框类别标签 0表示背景类 [bs, 24564]neg_pos_ratio: the ratio between the negative examples and positive examples. 负样本:正样本=3:1"""pos_mask = labels > 0 # 正类True 负类背景类Falsenum_pos = pos_mask.long().sum(dim=1, keepdim=True) # [1个,25个] 第一张图1个正样本 第二张图25个正样本num_neg = num_pos * neg_pos_ratio # [3, 75] 第一张图3个负样本 第二张图75个负样本loss[pos_mask] = -math.inf_, indexes = loss.sort(dim=1, descending=True) # 对负样本损失进行排序_, orders = indexes.sort(dim=1)neg_mask = orders < num_neg # 选出损失最高的前num_neg个负样本return pos_mask | neg_mask
2.5、后处理
后处理主要是针对预测得到的回归参数,利用上一步生成的default box进行解码,得到所有anchor的预测结果,再对这些预测结果进行nms等处理,得到最终的预测结果。
2.5.1、回归参数编解码:
解码:利用default box,将边界框回归参数解码为真实坐标。一般用在验证测试时,将预测框解码为真实坐标,再进行nms,显示在原图上。公式:
对应代码在ssd/utils/box_utils.py中:
def convert_locations_to_boxes(locations, priors, center_variance,size_variance):"""Convert regressional location results of SSD into boxes in the form of (center_x, center_y, h, w).The conversion:$$predicted\_center * center_variance = \frac {real\_center - prior\_center} {prior\_hw}$$$$exp(predicted\_hw * size_variance) = \frac {real\_hw} {prior\_hw}$$We do it in the inverse direction here.Args:locations (batch_size, num_priors, 4): the regression output of SSD. It will contain the outputs as well.回归预测结果 [bs,all_anchors,xywh]=[bs, 24564, 4]priors (num_priors, 4) or (batch_size/1, num_priors, 4): prior boxes.[all_anchor, 4]center_variance: a float used to change the scale of center. 0.1size_variance: a float used to change of scale of size. 0.2Returns:boxes: priors: [[center_x, center_y, w, h]]. All the valuesare relative to the image size."""# priors can have one dimension less.if priors.dim() + 1 == locations.dim():priors = priors.unsqueeze(0) # [num_anchors, 4] -> [1, num_anchors, 4]return torch.cat([# xy 利用default box将xy回归参数进行解码# 解码后xy坐标=预测的xy参数*default box的wh坐标*default box的xy坐标方差+default box的xy坐标locations[..., :2] * center_variance * priors[..., 2:] + priors[..., :2],# wh 利用default box将wh回归参数进行解码# 解码后的wh坐标=e^(预测的wh参数*default box的wh坐标方差) * default box的wh坐标torch.exp(locations[..., 2:] * size_variance) * priors[..., 2:]], dim=locations.dim() - 1)
编码:利用default box,将真实坐标编码为边界框参数,一般用在训练时,对gt label编码,将gt的坐标编码为回归参数,再与预测得到的预测框回归参数一起计算损失。公式(和上面解码过程刚好相反):
对应代码在ssd/utils/box_utils.py中:
def convert_boxes_to_locations(center_form_boxes, center_form_priors, center_variance, size_variance):# priors can have one dimension lessif center_form_priors.dim() + 1 == center_form_boxes.dim():center_form_priors = center_form_priors.unsqueeze(0)return torch.cat([(center_form_boxes[..., :2] - center_form_priors[..., :2]) / center_form_priors[..., 2:] / center_variance,torch.log(center_form_boxes[..., 2:] / center_form_priors[..., 2:]) / size_variance], dim=center_form_boxes.dim() - 1)
2.5.2、NMS
对应代码在ssd/modeling/box_head/inference.py中:
class PostProcessor:def __init__(self, cfg):super().__init__()self.cfg = cfgself.width = cfg.INPUT.IMAGE_SIZEself.height = cfg.INPUT.IMAGE_SIZEdef __call__(self, detections):batches_scores, batches_boxes = detectionsdevice = batches_scores.devicebatch_size = batches_scores.size(0)results = []for batch_id in range(batch_size):scores, boxes = batches_scores[batch_id], batches_boxes[batch_id] # (N, #CLS) (N, 4)num_boxes = scores.shape[0]num_classes = scores.shape[1]boxes = boxes.view(num_boxes, 1, 4).expand(num_boxes, num_classes, 4)labels = torch.arange(num_classes, device=device)labels = labels.view(1, num_classes).expand_as(scores)# remove predictions with the background labelboxes = boxes[:, 1:]scores = scores[:, 1:]labels = labels[:, 1:]# batch everything, by making every class prediction be a separate instanceboxes = boxes.reshape(-1, 4)scores = scores.reshape(-1)labels = labels.reshape(-1)# remove low scoring boxesindices = torch.nonzero(scores > self.cfg.TEST.CONFIDENCE_THRESHOLD).squeeze(1)boxes, scores, labels = boxes[indices], scores[indices], labels[indices]boxes[:, 0::2] *= self.widthboxes[:, 1::2] *= self.height# nmskeep = batched_nms(boxes, scores, labels, self.cfg.TEST.NMS_THRESHOLD)# keep only topk scoring predictionskeep = keep[:self.cfg.TEST.MAX_PER_IMAGE]boxes, scores, labels = boxes[keep], scores[keep], labels[keep]container = Container(boxes=boxes, labels=labels, scores=scores)container.img_width = self.widthcontainer.img_height = self.heightresults.append(container)return results
三、数据增强
SSD的效果好,还有一个很重要的原因:SSD做了丰富的数据增强策略,这部分为模型mAP带来了8.8%的提升,尤其是小物体和遮挡问题等难点问题,强大的数据增强很重要。
这部分代码在ssd/data/transforms/transformers.py中,自己看下就ok了
Reference
b站霹雳吧啦Wz:2.1SSD算法理论.
CSDN just_sort:目标检测算法之SSD代码解析(万字长文超详细).
【项目一、xxx病虫害检测项目】1、SSD原理和源码分析相关推荐
- 【项目二、蜂巢检测项目】一、串讲各类经典的卷积网络:InceptionV1-V4、ResNetV1-V2、MobileNetV1-V3、ShuffleNetV1-V2、ResNeXt、Xception
目录 前言 一.InceptionV1-V4 1.1.InceptionV1(GoogLeNet) - 2014 1.2.InceptionV2.InceptionV3 - 2015 1.3.Ince ...
- 人脸检测(九)--检测器源码分析
原文: http://blog.csdn.net/xidianzhimeng/article/details/41851569 还有几篇蛮好: http://blog.csdn.net/delltdk ...
- xxx Java基础视频教程(含课件和源码)
本套视频是传智播客毕向东老师Java基础班的25天全程实录视频教程,适合零基础同学学习的Java基础视频教程. 内容涉及到的知识点: 1.计算机基本原理,Java语言发展简史,Java开发环境的搭建, ...
- 【项目三、车牌检测+识别项目】四、使用LPRNet进行车牌识别
目录 前言 一.数据集 二.训练 三.验证 四.测试结果 五.推理代码 Reference 前言 马上要找工作了,想总结下自己做过的几个小项目. 之前已经总结过了我做的第一个项目:xxx病虫害检测项目 ...
- 【项目三、车牌检测+识别项目】一、CCPD车牌数据集转为YOLOv5格式和LPRNet格式
目录 前言 一.CCPD数据集介绍 二.CCPD数据集下载 三.划分训练集.验证集和测试集 四.车牌检测数据集制作 五.车牌识别数据集制作 六.我的车牌检测+识别数据集 Reference 前言 马上 ...
- 【项目三、车牌检测+识别项目】二、使用YOLOV5进行车牌检测
目录 前言 一.数据集 二.ccpd.yaml 三.训练 四.验证 五.测试结果 Reference 前言 马上要找工作了,想总结下自己做过的几个小项目. 之前已经总结过了我做的第一个项目:xxx病虫 ...
- 用PaddleDetection做一个完整的目标检测项目(上)
文章转载自:微信公众号:飞桨PaddlePaddle的微信文章 原文章中由于排版问题,导致文字遮挡,不便阅读,因此对文章格式稍作更改,增加了一些关键词加粗,便于后续阅读. PaddleDetectio ...
- Python OpenCV --Drowsiness Detector 睡意检测--项目记录
睡意检测是一项安全技术,可以防止驾驶员在驾驶中入睡而导致的事故. 目的是建立一个睡意检测系统,该系统将检测人的眼睛闭合几秒钟. 当检测到困倦时,该系统将警告驾驶员. 睡意检测版本1.0 睡眠检测关键步 ...
- 仅用CPU就能跑到1000FPS,这是开源的C++跨平台人脸检测项目
点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送达 来源:机器之心 总是被各种依赖环境蹂躏?看看这个 C++编写的跨平 ...
最新文章
- python模块 - re模块使用演示样例
- android 视频开发sd卡,Android开发之SD卡文件操作分析
- 反射实体自动生成EasyUi DataGrid模板 第二版--附项目源码
- sql server序列_SQL Server中的序列对象
- yolov5-detect.py解析与重写
- mongodb 分组聚合_MongoDB干货总结
- php中的 i详解,浅析PHP中的i++与++i的区别及效率
- ios 对日期的处理(包括计算昨天时间、明天时间)
- QQ通过xml卡片自动探测对方ip
- CentOS修改tomcat端口
- pandas之透视表
- Chemex 打印标签二维码扫不出信息,跳转地址不正确404,如何改代码?
- APP设计:(一)app界面常用设计规范
- 计算机奥赛金牌排名,2019五大学科竞赛含金量排名
- 仿微信视频通话大小视图切换(SurfaceView实现)
- 利用CMake编译OpenCV-4.1.2源码,使其可以在VS2012下进行图像处理开发的记录(因缺少OpenBLAS未成功)
- COVID-19检测方法汇总
- Python爬取《冰雪奇缘2》豆瓣影评
- SSM框架报错分析(一)——There is no getter for property named 'XXX' in 'class java.lang.String'...
- 创建工作站vmware workstation时,提示before you can run vmware several modules must be compiled