计算机视觉
计算机视觉
一.计算机视觉任务
计算机视觉主要任务包括以下五种:
1.图像分类
给定一组各自被标记为单一类别的图像作为训练集,然后对新的测试图像的类别进行分类,比如MINIST手写图像识别
2.目标检测
识别图像中的对象目标,输出目标的边界框坐标和分类标签
比如鱼类检测,绝缘子检测(之前做过的)
类似下面这样
3.目标跟踪
在特定场景中跟踪一个或多个特定感兴趣对象的过程,在无人驾驶领域很重要
4.语义分割
(semantic segemention)
先把图像分别为一个个像素然后对像素进行分类
与图像分类不同的是:图像分类将整个图片分为一类且不需要在图中标记什么,而语义分割则需要将图中不同类别的物体都进行分类且进行标记
比如这张图中有三个不同类别的物体:人,天空,草地
5.实例分割
实例的意思是不同的个体,比如小明和小红是两个人,他们就是不同的实例
是目标检测和语义分割的结合
和语义分割的不同之处在于:实例分割区分同类别的不同实例,语义分割则只区分类别不区分实例
二.计算机视觉方法
1.传统方法
之前,计算机视觉一般采用特征提取(用于提取特征)+浅层模型的方法(用于决策)
- 特征提取:提取图像中的梯度方向直方图
(HOG)
、尺度不变特征变换(SIFT)
等 - 浅层模型:
SVM、Random Forest
等
2.深度学习模型
计算机视觉逐渐转向以CNN为代表的端到端的深度学习模型。
目前主流的目标检测算法主要是基于深度学习模型,其可以分成两大类:two-stage
检测算法;one-stage
检测算法。目标检测模型的主要性能指标是检测准确度和速度,对于准确度,目标检测要考虑物体的定位准确性,而不单单是分类准确度。一般情况下,two-stage
算法在准确度上有优势,而one-stage
算法在速度上有优势。
(1)two-stage检测算法
two-stage检测算法将检测问题划分为两个阶段,首先产生候选区域(region proposals)
,然后对候选区域分类(一般还需要对位置精修),这类算法的典型代表是基于region proposal
的R-CNN
系算法,如R-CNN,SPPNet ,Fast R-CNN,Faster R-CNN,FPN,R-FCN
,Cascade R-CNN
等;
(2)one-stage
不需要region proposal
(候选区域提取)阶段,直接产生物体的类别概率和位置坐标值,经过单次检测即可直接得到最终的检测结果,因此有着更快的检测速度,比较典型的算法如YOLO,SSD,Retina-Net
(3)上述两种模型的差异
单步模型没有独立的、显示的候选区域提取过程(region proposal),直接由输入图像得到去中存在的物体类别和位置信息的模型。
两步模型是先在输入图像上筛选出一些可能存在物体的候选区域,然后针对每个候选区域判断是否存在物体,如果存在就给出物体的类别和位置修正信息。
- 速度上为什么有差异
两步模型在第二步进行对候选区的分类和位置回归时,是针对每个候选区域独立进行的,因此该部分的算法复杂度线性正比于预设的候选区域数目,这一般是非常巨大的,相比于但不模型存在计算量大速度慢的问题。
- 精度上为什么有差异
多数单步模型利用预设的锚框(Anchor Box)来捕捉可能存在于图像中各个位置的物体,因此但不模型会对数量巨大的锚框是否包含物体以及物体最终类别进行分类,但是一幅图像中实际含有的物体数目远少于锚框数目,使得训练这个分类器时正负样本数目及其不均衡,导致效果不佳。
而在两步模型中由于独立的候选区提取这个步骤已经刷选掉大部分不含有待检测物体的区域(负样本),因此在后续进行分类和候选框位置/大小修正时,正负样本数比较均衡,不存在类似的问题。
三.不同任务(模型)的评价指标
1.交并比(Intersection-Over-Union, IOU)__目标检测任务
用于计算两个边界框之间的重叠,需要一个标注的真实边界框Bgt和一个预测的边界框Bp,IOU就是预测边界框和真实边界框之间的重叠面积初一他们之间的并集面积:IOU = area(Bp ∩ Bgt) / area(Bp ∪ Bgt)
2.关于分类的评价指标__目标检测任务
- TP(True Positive,真阳性):正确分类的正样本数,预测是正样本,实际也是正样本
检测正确,那些 *IOU>= 阈值* 的边界框
- FP(假阳性):被错误的标记为正样本的负样本数,即预测为正样本,但是实际为负样本
检测错误,IOU <= 阈值
的边界框(只要模型给出边界框就代表模型认为边界框里的东西是一个正样本)
-
TN(True Negative,真阴性):不适用于目标检测的指标。表示正确分类的负样本数,即一个正确的无须检测框,在目标检测任务中代表背景框。
-
FN(假阴性):被错误的标记为负样本的正样本数,也就是未检测到的真实边界框。
(1)准确率(Accuracy,ACC)
- 目标检测中的ACC:
TP / (TP + FP + FN)
- 机器学习中分类任务的准确率:
(TP + TN ) / (TP + FP + TN + FN)
(2)查准率(精确率,Precision,P)
精确率是指分类正确的正样本个数占分类器判定为正样本的样本个数的比例
TP / (TP + FP)
(3)召回率(Recall,R)
召回率是指分类正确的正样本个数占真正的正样本个数的比例
TP / (TP + FN)
Precision值和Recall值是既矛盾又统一的两个指标, 为了提高Precision值, 分类器需要尽量在“更有把握”时才把样本预测为正样本, 但此时往往会因为过于保
守而漏掉很多“没有把握”的正样本, 导致Recall值降低 。
(4)P-R曲线计算AP值
- P-R曲线的作法:在不同阈值下获得当前阈值的查准率P和召回率R,并根据各个类别的PR值绘制PR曲线,注意一个类别对应一个PR曲线,比如三个类别:猫、狗、人对应三条PR曲线,分别是将猫都当做正样本其余为负样本、狗当做正样本...
- 横轴代表召回率,纵轴为查准率
得出某一个类别的P-R曲线后,将“峰值点”向左画一条直线直到与上一个峰值点向横轴的垂线相交。这样线段与坐标轴围成的面积就是该类别的AP值。
一个例子:
详细的计算可以参考AP与mAP的详解
(5)mAP
将每个类别对应的AP值计算出来之后取平均值即可
3.F1 score __目标检测任务
F1 score
是精准率和召回率的调和平均值 (变量倒数的算术平均的倒数):2*P*F / (P+F)
4.均方根误差__回归模型
RMSE常用来衡量回归模型的好坏 :
RMSE能够很好地反映回归模型预测值与真实值的偏离程度。 但在实际问题中, 如果存在个别偏离程度非常大的离群点( Outlier) 时, 即使离群点数量非常少, 也会让RMSE指标变得很差。 (因为模型对这些离散点的预测能力很差)。
解决办法:
- 如果认定这些离群点是“噪声点”的话, 就需要在数据预处理的阶段把这些噪声点过滤掉 。
- 可以找一个更合适的指标来评估该模型。 关于评估指标, 其实是存在比RMSE的鲁棒性更好的指标, 比如平均绝对百分比误差(Mean Absolute
Percent Error, MAPE) , 它定义为
5.ROC曲线和AUC
可以参考:目标检测 — 评价指标
ROC曲线的横坐标是假阳性率(FPR):FP / N
(N是真实的负样本的数量)
纵坐标是真阳性率(TRP):TP / P
(P是真实的正样本的数量)
和P-R曲线类似,ROC曲线的做法也是通过不断地移动阈值(区分正负预测结果的阈值),每一个阈值对应一组假阳性率和真阳性率,相当于坐标轴上的一个点。
通过动态地调整截断点, 从最高的得分开始(实际上是从正无穷开始, 对应着ROC曲线的零点) , 逐渐调整到最低得分, 每一个截断点都会对应一个FPR和TPR, 在ROC图上绘制出每个截断点对应的位置, 再连接所有点就得到最终的ROC曲线 。
其实, 还有一种更直观地绘制ROC曲线的方法。 首先, 根据样本标签统计出正负样本的数量, 假设正样本数量为P, 负样本数量为N; 接下来, 把横轴的刻度
间隔设置为1/N, 纵轴的刻度间隔设置为1/P; 再根据模型输出的预测概率对样本进行排序(从高到低) ; 依次遍历样本, 同时从零点开始绘制ROC曲线, 每遇到一个正样本就沿纵轴方向绘制一个刻度间隔的曲线, 每遇到一个负样本就沿横轴方向绘制一个刻度间隔的曲线, 直到遍历完所有样本, 曲线最终停在(1,1) 这个点, 整个ROC曲线绘制完成。(这个也和P-R曲线类似)
ROC曲线和P-R曲线的区别:
相比P-R曲线, ROC曲线有一个特点, 当正负样本的分布发生变化时, ROC曲线的形状能够基本保持不变, 而P-R曲线的形状一般会发生较剧烈的变化。
所以, ROC曲线的适用场景更多, 被广泛用于排序、 推荐、 广告等领域。
四.区域卷积神经网络系列算法
区域卷积神经网络(Region CNN,R-CNN
)首次将深度学习引入目标检测领域。
R-CNN
还启发了一系列的目标检测算法,如Fast R-CNN、Faster R-CNN、Mask R-CNN,R-CNN
代表two-stage
目标检测的源头,从此卷积神经网络称为目标检测任务的常规方法。
1.R-CNN
R-CNN是第一个将卷积神经网络用于目标检测的深度学习模型。主要思路是:
- 使用无监督的选择性搜索方法
(Selectiver Search,SS)
,将输入图像中具有相似颜色直方图特征的区域进行递归合并,选取约2000个候选区域(和 锚框类似),这些候选区通常是在多个尺度下选取的,具有不同大小和形状。然后根据真实边界框为这些候选区域标注类别和相对真实边界框的偏移量(先分配后标注) - 将候选区域进行固定尺寸缩放到合适的尺寸,以满足卷积神经网络对对输入图像大小的限制
- 选择一个预训练的卷积神经网络,将其在输出层之前截断。利用卷积神经网络提取每一个缩放后的候选区域图像的特征
- 将每个提议区域(候选区域)的特征连同其标注的类别作为一个样本。训练多个支持向量机对目标分类,其中每个支持向量机用来判断样本是否属于某一个类别;将每个提议区域的特征连同其标注的边界框作为一个样本,训练线性回归模型来预测真实边界框
- 预测阶段时:对最终检测结果进行非极大抑制(Non-Maximum Suppression,NMS)进行筛选得到最终结果
缺点:
- 选择候选区域对候选区域进行特征提取的速度太慢
- 一张图可能产生上千个候选区域,这就需要上千次的卷积神经网络的前向传播来执行目标检测;并需要额外的空间保存提取到的特征信息,浪费很多空间和时间
2.Fast R-CNN
主要解决了R-CNN的两个问题:
- 对每个提议区域,卷积神经网络的前向传播是独立的,而没有共享计算。 由于这些区域通常有重叠,独立的特征抽取会导致重复的计算
- 目标分类和定位回归的特征存储需要占用大量内存
- 感兴趣区域池化(Region of Interest Pooling,ROI pooling)方法生成固定尺寸特征代替了R-CNN算法中的区域图像缩放,避免了像素损失,巧妙地避免了尺度不一的问题
Fast R-CNN 对单个候选框的处理流程如下:
具体步骤:
兴趣区域汇聚层(ROI
)的计算步骤:
3.Faster R-CNN
在Fast R-CNN中使用的目标检测网络,存在以下缺点:
- 选择搜索算法提取候选区域耗时比较长,并且和目标检测网络是分离的,不是端到端的。
Faster R-CNN
算法设计了以全卷积神经网络为基础的RPN(Region Proposal Network
,区域提议网络)用来提取候选区域,在减少候选区域数量的情况下仍保证目标检测的精度
与Fast R-CNN
相比,Faster R-CNN
只将生成提议区域方法从选择性搜索改为了区域提议网络,模型的其余部分保持不变
整体工作流程:
- 利用卷积层CNN等基础网络提取输入图片的特征图(Feature Map)
- RPN网络按照固定尺寸和面积生成9个Anchors,故在特征图中生成大量的锚框
- RPN网络利用1×1卷积对每个锚框左耳分类和初步定位回归,输出比较精确的RoIs(也叫proposals,region proposal)
- 将RoIs映射到卷积神经网络生成的特征图上,并用感兴趣区域池化(RoI Pooling)将region proposals 修改为固定尺寸
- 对固定尺寸的特征图进行分类和边界框回归修正
相对于Fast R-CNN最大的改进是FPN网络来生成候选区域,其中Conv Layers,RoI池化层以及分类和边框回归修正改动不大
FPN(区域提议网络)具体工作流程:
以经过卷积网络提取到的特征图作为输入,输出一组矩形的候选区域,并且给每个候选区域打上一个分数。如下图:
关于锚框的知识:
参考链接:目标检测之R-CNN系列
4.Mask R-CNN
(实例分割,前三个是目标检测)
相比于Faster R-CNN有两个主要的不同:
- 用RoI Align(兴趣区域对齐层)代替了感兴趣区域池化RoI Pooling(兴趣区域汇聚层)
- 在RoI Align之后添加了一个与分类和回归并行的mask分支(全卷积网络),来预测目标的像素级位置
RoI Align 介绍:
RoI Pooling 首先把候选框边界量化为整数点坐标值,然后将量化后的边界区域分别在宽高尺度上划分成K×K
个单元(具体多少按实际情况来),再对每一个单元的边界进行取整,这两次取整量化造成了区域不匹配(mis-alignment
)问题,会让实例分割有较大的重叠。
RoI Align
取消了取整量化操作,使用双线性内插值的方法获取坐标为浮点数的像素点上的特征值。
特征金字塔网络结构FPN(Feature Pyramid Network):
后续在针对小物体不易检测的问题上(因为很多网络使用高层特征图来检测物体,但是由于小物体包含的像素小,在下采样过程中到了高层特征图上就有可能丢失小物体的信息,因为高层特征感受野很大)可以在Mask R-CNN、Faster R-CNN
等网络基础上加上FPN
网络结构
具体过程参考:
5.Cascade R-CNN
级联区域卷积神经网络,采用Muti-stage级联的结构,每个stage都有一个不同的IOU阈值,这让每一个stage都专注于检测IOU在某一范围内的候选框,由于输出的IOU(proposal经过box reg的新的IoU)普遍大于输入的IOU(RPN输出proposal的IoU),因此检测效果会越来越好。级联的结构是为了调整边界框给下一阶段找到一个IOU更高的正样本来训练。
五.One-stage 目标检测算法
1.YOLOv1
- 将输入图片划分为S×S的方格,每个方格需要检测出中心点位于该方格内的物体。具体的,会预测B(论文中是2个)个边界框(包括位置参数,置信度参数,以及总的类别数)
- YOLO的主体网络结构参考GoogLeNet,由24个卷积层和2个全连接层组成
2.YOLOv2
针对YOLOv1的两个缺点:低定位准确率和低召回率进行了以下改进:
- 在卷积层后家里批量归一化(BN)层,以加快收敛速度,放置过拟合
- 采用k-means算法进行聚类获取先验锚框,且聚类距离不是欧氏距离而是一个特定的适合目标检测任务的指标
- 这里的聚类是对图片标注的真实边界框进行聚类来获取锚框的尺寸设定
- YOLOv2直接在预先设定的锚框上提取特征,用卷积网络来预测偏移量和置信度(置信度是YOLO系列特有的指标)
- 使用的主干特征提取网络是DarkNet-19,相比于常用的VGGNet-16,其速度更快
3.YOLOv3
- 采用了DarkNet-53作为主干网络,并借鉴残差网络的快捷连接结构
- 采用3个不同大小的特征图进行联合训练,使其在小物体上也能获得很好的检测效果
5.SSD(单发多框检测)
在不同的阶段产生不同大小的特征图一共6个,在大的特征图上检测小目标,小的特征图上检测大目标(多尺度目标检测)
在不同尺度的特征图上 产生(实际是在原图上产生)不同大小和比例的default box(类似于锚框)
参考:
六.常用技术代码实现
参考链接:
1.图像增广
对训练数据(测试数据不需要)进行图像增广,可以提高训练数据容量,使得魔性的泛华能力更强
常见的方法有如下,可以通过torchvision.transforms
模块来实现
# 翻转图像 torchvision.transforms.RandomHorizontalFlip() torchvision.transforms.RandomVerticalFlip() # 裁剪图像 torchvision.transforms.RandomResizedCrop() # 改变颜色 torchvision.transforms.ColorJitter() # 还可以将上述操作组合 torchvision.transforms.Compose([ torchvision.transforms.RandomHorizontalFlip(), torchvision.transforms.RandomVerticalFlip()])
2. 微调
2.1.基本步骤
迁移学习中常见的技巧。
迁移学习:将从源数据集(这里指的是象ImageNet
·这种很大的数据集)上学到的模型参数迁移到目标数据集。
微调(fine-tuning
)包括四个步骤:
- 在源数据集(例如
ImageNet
数据集)上预训练神经网络模型,即源模型 - 创建一个新的神经网络称为目标模型,注意这个和源模型只有输出层不同,其他都相同。这将复制源模型上的所有模型设计及其参数(输出层除外)。我们假定这些模型参数包含从源数据集中学到的知识,这些知识也将适用于目标数据集
- 向目标模型添加输出层,其输出数是目标数据集中的类别数。然后随机初始化该层的模型参数
- 在目标数据集上训练目标模型。输出层将从头开始进行训练,而所有其他层的参数将根据源模型的参数进行微调(其实指的就是利用较小学习率来调整这些参数使其适应我们自己的数据集)
当目标数据集比源数据集小得多时,微调有助于提高模型的泛化能力
# 将在源数据集(这里指的是ImageNet)上训练好的resnet18模型下载下来 # 将输出层替换为适合自己数据集的输出层,就改一下输出类别即可 # 初始化输出层参数 finetune_net = torchvision.models.resnet18(pretrained=True) finetune_net.fc = nn.Linear(finetune_net.fc.in_features, 2) nn.init.xavier_uniform_(finetune_net.fc.weight);
2.2.微调代码
# 如果param_group=True,输出层中的模型参数将使用十倍的学习率 def train_fine_tuning(net, learning_rate, batch_size=128, num_epochs=5, param_group=True): train_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder( os.path.join(data_dir, 'train'), transform=train_augs), batch_size=batch_size, shuffle=True) test_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder( os.path.join(data_dir, 'test'), transform=test_augs), batch_size=batch_size) devices = d2l.try_all_gpus() loss = nn.CrossEntropyLoss(reduction="none") if param_group: # 分开设置输出层和需要微调的层的学习率 params_1x = [param for name, param in net.named_parameters() if name not in ["fc.weight", "fc.bias"]] trainer = torch.optim.SGD([{'params': params_1x}, {'params': net.fc.parameters(), 'lr': learning_rate * 10}], lr=learning_rate, weight_decay=0.001) else: trainer = torch.optim.SGD(net.parameters(), lr=learning_rate, weight_decay=0.001) d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices)
2.3.总结
微调就是对迁移过来的模型,在我们自己的数据集上训练时,设置更小的学习率更新这些参数,使其符合我们的数据集
3. 边界框和锚框
3.1.边界框(bounding box)
在目标检测中,我们通常使用边界框(bounding box
)来描述对象的空间位置。 边界框是矩形的,由矩形左上角的以及右下角的x
和y
坐标决定。 另一种常用的边界框表示方法是边界框中心的(x,y
)轴坐标以及框的宽度和高度
def box_corner_to_center(boxes): """从(左上,右下)转换到(中间,宽度,高度)""" # 这里的boxs有好多个 x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] cx = (x1 + x2) / 2 cy = (y1 + y2) / 2 w = x2 - x1 h = y2 - y1 boxes = torch.stack((cx, cy, w, h), axis=-1) return boxes def box_center_to_corner(boxes): """从(中间,宽度,高度)转换到(左上,右下)""" cx, cy, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] x1 = cx - 0.5 * w y1 = cy - 0.5 * h x2 = cx + 0.5 * w y2 = cy + 0.5 * h boxes = torch.stack((x1, y1, x2, y2), axis=-1) return boxes
3.2.锚框
锚框是从输入图像中采集大量感兴趣的区域的方法,其实还有很多别的采集区域的方法,不过大都和这个相似。
注意这里计算锚框大小,宽度是,上面写错了。下面代码也有提现。宽高比是锚框的宽高比
# 输入图片数据data,最后两纬是height、weight def multibox_prior(data, sizes, ratios): """生成以每个像素为中心具有不同形状的锚框""" in_height, in_width = data.shape[-2:] device, num_sizes, num_ratios = data.device, len(sizes), len(ratios) boxes_per_pixel = (num_sizes + num_ratios - 1) # 每个像素锚框数量 size_tensor = torch.tensor(sizes, device=device) ratio_tensor = torch.tensor(ratios, device=device) # 为了将锚点移动到像素的中心,需要设置偏移量 # 因为一个像素的的高为1且宽为1,我们选择偏移我们的中心0.5,像素不是一个点而是宽高为1的矩形 offset_h, offset_w = 0.5, 0.5 # 对高和宽进行归一化 steps_h = 1.0 / in_height # 在y轴上缩放步长 steps_w = 1.0 / in_width # 在x轴上缩放步长 # 生成锚框的所有中心点(每个像素的中心点) center_h = (torch.arange(in_height, device=device) + offset_h) * steps_h center_w = (torch.arange(in_width, device=device) + offset_w) * steps_w shift_y, shift_x = torch.meshgrid(center_h, center_w) shift_y, shift_x = shift_y.reshape(-1), shift_x.reshape(-1) # 生成“boxes_per_pixel”个高和宽, # 之后用于创建锚框的四角坐标(xmin,xmax,ymin,ymax) # 也要分别处以w和h进行归一化 w = torch.cat((size_tensor * torch.sqrt(ratio_tensor[0]), sizes[0] * torch.sqrt(ratio_tensor[1:])))\ * in_height / in_width # 处理矩形输入 h = torch.cat((size_tensor / torch.sqrt(ratio_tensor[0]), sizes[0] / torch.sqrt(ratio_tensor[1:]))) # 除以2来获得半高和半宽 anchor_manipulations = torch.stack((-w, -h, w, h)).T.repeat( in_height * in_width, 1) / 2 # 每个中心点都将有“boxes_per_pixel”个锚框, # 所以生成含所有锚框中心的网格,重复了“boxes_per_pixel”次 out_grid = torch.stack([shift_x, shift_y, shift_x, shift_y], dim=1).repeat_interleave(boxes_per_pixel, dim=0) output = out_grid + anchor_manipulations return output.unsqueeze(0)
4.交并比(IOU)
用来衡量锚框与真实边界框(就是我们自己标注的)之间的相似性。
就是两个边界框相交面积与相并面积之比
我们将使用交并比来衡量锚框和真实边界框之间、以及不同锚框之间的相似度,下面是
# 给定连个锚框或者变 def box_iou(boxes1, boxes2): """计算两个锚框或边界框列表中成对的交并比""" box_area = lambda boxes: ((boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1])) # boxes1,boxes2,areas1,areas2的形状: # boxes1:(boxes1的数量,4), # boxes2:(boxes2的数量,4), # areas1:(boxes1的数量,), # areas2:(boxes2的数量,) areas1 = box_area(boxes1) areas2 = box_area(boxes2) # inter_upperlefts,inter_lowerrights,inters的形状: # (boxes1的数量,boxes2的数量,2) # 会生成boxes1与boxes2中所有框的组合的交集区域左上角和右下角的坐标 inter_upperlefts = torch.max(boxes1[:, None, :2], boxes2[:, :2]) inter_lowerrights = torch.min(boxes1[:, None, 2:], boxes2[:, 2:]) # clamp(min=0)是把小于0的数变为0 inters = (inter_lowerrights - inter_upperlefts).clamp(min=0) # inter_areasandunion_areas的形状:(boxes1的数量,boxes2的数量) inter_areas = inters[:, :, 0] * inters[:, :, 1] union_areas = areas1[:, None] + areas2 - inter_areas return inter_areas / union_areas
4.1.锚框在训练和测试时的作用
在训练集中将每一个锚框视为一个训练样本(并不是一张图片是一个样本),这样就需要每个锚框的类别和偏移量标签。偏移量标签指的是真实边界框相对于锚框的偏移量。
在预测时,我们为每个图像生成多个锚框,预测所有锚框的类别和偏移量,根据预测的偏移量调整它们的位置以获得预测的边界框,最后只输出符合特定条件的预测边界框。
目标检测训练集带有“真实边界框”的位置及其包围物体类别的标签。 要标记任何生成的锚框,我们可以参考分配到的最接近此锚框的真实边界框的位置和类别标签。
4.2.将真实的边界框分给锚框
前三步是为每个真实的边界框匹配了一个锚框,最后一步是将剩余的锚框与每个真实边界框相比,根据IOU的阈值
为其分配真实边界框,其中小于阈值的边界框不会被分配真实边界框,如果一个锚框没有被分配真实边界框,我们只需将锚框的类别标记为“背景”(background
)。 背景类别的锚框通常被称为“负类”锚框,其余的被称为“正类”锚框。
def assign_anchor_to_bbox(ground_truth, anchors, device, iou_threshold=0.5): """将最接近的真实边界框分配给锚框""" num_anchors, num_gt_boxes = anchors.shape[0], ground_truth.shape[0] # 位于第i行和第j列的元素x_ij是锚框i和真实边界框j的IoU jaccard = box_iou(anchors, ground_truth) # 对于每个锚框,分配的真实边界框的张量 anchors_bbox_map = torch.full((num_anchors,), -1, dtype=torch.long, device=device) # 根据阈值,决定是否分配真实边界框 max_ious, indices = torch.max(jaccard, dim=1) anc_i = torch.nonzero(max_ious >= 0.5).reshape(-1) box_j = indices[max_ious >= 0.5] anchors_bbox_map[anc_i] = box_j col_discard = torch.full((num_anchors,), -1) row_discard = torch.full((num_gt_boxes,), -1) # 选出最大IOU分配边界框 for _ in range(num_gt_boxes): max_idx = torch.argmax(jaccard) box_idx = (max_idx % num_gt_boxes).long() anc_idx = (max_idx / num_gt_boxes).long() anchors_bbox_map[anc_idx] = box_idx jaccard[:, box_idx] = col_discard jaccard[anc_idx, :] = row_discard return anchors_bbox_map
在上述代码中与前面介绍的文字描述有些出入,代码中是先选出IOU
大于0.5
的锚框,为其分配相应的边界框,然后再选出最大的IOU
值,也就是步骤1
。
4.3.为每个锚框标记类别和偏移量
下面是计算锚框与真实框的偏移量并进行转换使其易于训练的代码:
def offset_boxes(anchors, assigned_bb, eps=1e-6): """对锚框偏移量的转换""" c_anc = d2l.box_corner_to_center(anchors) c_assigned_bb = d2l.box_corner_to_center(assigned_bb) offset_xy = 10 * (c_assigned_bb[:, :2] - c_anc[:, :2]) / c_anc[:, 2:] offset_wh = 5 * torch.log(eps + c_assigned_bb[:, 2:] / c_anc[:, 2:]) offset = torch.cat([offset_xy, offset_wh], axis=1) return offset
def multibox_target(anchors, labels): """使用真实边界框标记锚框""" batch_size, anchors = labels.shape[0], anchors.squeeze(0) batch_offset, batch_mask, batch_class_labels = [], [], [] device, num_anchors = anchors.device, anchors.shape[0] for i in range(batch_size): label = labels[i, :, :] # 开始为每个锚框分配真实边界框 anchors_bbox_map = assign_anchor_to_bbox( label[:, 1:], anchors, device) # 因为labels第一个元素是类别,这里就不传入分配边界框函数了 bbox_mask = ((anchors_bbox_map >= 0).float().unsqueeze(-1)).repeat( 1, 4) # 将类标签和分配的边界框坐标初始化为零 class_labels = torch.zeros(num_anchors, dtype=torch.long, device=device) assigned_bb = torch.zeros((num_anchors, 4), dtype=torch.float32, device=device) # 使用真实边界框来标记锚框的类别。 # 如果一个锚框没有被分配,我们标记其为背景(值为零) indices_true = torch.nonzero(anchors_bbox_map >= 0) bb_idx = anchors_bbox_map[indices_true] # 背景类为0,其他的类加1 class_labels[indices_true] = label[bb_idx, 0].long() + 1 assigned_bb[indices_true] = label[bb_idx, 1:] # 计算偏移量并转换 offset = offset_boxes(anchors, assigned_bb) * bbox_mask batch_offset.append(offset.reshape(-1)) batch_mask.append(bbox_mask.reshape(-1)) batch_class_labels.append(class_labels) # 输出每个锚框的类别,相对真实框的偏移量,以及掩码(分辨哪个锚框是背景) bbox_offset = torch.stack(batch_offset) bbox_mask = torch.stack(batch_mask) class_labels = torch.stack(batch_class_labels) return (bbox_offset, bbox_mask, class_labels)
5.非极大抑制预测边界框
在预测时,我们先为图像生成多个锚框
再为这些锚框分别预测类别和偏移量,
然后根据偏移量来将非背景类的锚框变换为预测边界框,
再对这些预测边界框进行非极大抑制,“合并“属于同一目标的类似的边界框,留下的就是最终的输出边界框。
5.1.根据锚框和偏移量输出边界框
一个“预测好的边界框”则根据其中某个带有预测偏移量的锚框而生成。 下面我们实现了offset_inverse
函数,该函数将锚框和偏移量预测作为输入,并应用逆偏移变换来返回预测的边界框坐标。就是根据带有预测偏移量的锚框来对锚框进行变换(缩放和平移)生成边界框
下面的代码输入锚框和预测偏移量,输出预测边界框:
def offset_inverse(anchors, offset_preds): """根据带有预测偏移量的锚框来预测边界框""" anc = d2l.box_corner_to_center(anchors) pred_bbox_xy = (offset_preds[:, :2] * anc[:, 2:] / 10) + anc[:, :2] pred_bbox_wh = torch.exp(offset_preds[:, 2:] / 5) * anc[:, 2:] pred_bbox = torch.cat((pred_bbox_xy, pred_bbox_wh), axis=1) predicted_bbox = d2l.box_center_to_corner(pred_bbox) return predicted_bbox
5.2.非极大抑制
当有许多锚框时,可能会输出许多相似的具有明显重叠的预测边界框,都围绕着同一目标。 为了简化输出,我们可以使用非极大值抑制(non-maximum suppression,NMS
)合并属于同一目标的类似的预测边界框。
def multibox_detection(cls_probs, offset_preds, anchors, nms_threshold=0.5, pos_threshold=0.009999999): """使用非极大值抑制来预测边界框""" device, batch_size = cls_probs.device, cls_probs.shape[0] anchors = anchors.squeeze(0) num_classes, num_anchors = cls_probs.shape[1], cls_probs.shape[2] out = [] for i in range(batch_size): cls_prob, offset_pred = cls_probs[i], offset_preds[i].reshape(-1, 4) # 确定每个锚框的置信度也就是最大预测概率,以及对应的类别 conf, class_id = torch.max(cls_prob[1:], 0) # 根据锚框和偏移量计算真实边界框位置 predicted_bb = offset_inverse(anchors, offset_pred) # 根据每个预测边界框的置信度以及nms阈值选出基准边界框 keep = nms(predicted_bb, conf, nms_threshold) # 找到所有的non_keep索引,并将类设置为背景 all_idx = torch.arange(num_anchors, dtype=torch.long, device=device) combined = torch.cat((keep, all_idx)) uniques, counts = combined.unique(return_counts=True) non_keep = uniques[counts == 1] all_id_sorted = torch.cat((keep, non_keep)) class_id[non_keep] = -1 class_id = class_id[all_id_sorted] conf, predicted_bb = conf[all_id_sorted], predicted_bb[all_id_sorted] # pos_threshold是一个用于非背景预测的阈值 # 在经过nms_threshold的筛选后,会删除一些相似的边界框, # 但是可能还有一些边界框与每个基准边界框的IOU都小于IOU_threshold,并且这些边界框置信度小于pos_threshold,说明这些边界框是背景类,也要将类别置为-1 below_min_idx = (conf < pos_threshold) class_id[below_min_idx] = -1 # 比如之前的置信度是0.05,现在变为0.95 conf[below_min_idx] = 1 - conf[below_min_idx] # 将类别信息,置信度,边界框坐标堆叠在一起 pred_info = torch.cat((class_id.unsqueeze(1), conf.unsqueeze(1), predicted_bb), dim=1) out.append(pred_info) return torch.stack(out)
删除-1
类别(背景)的预测边界框后,我们可以输出由非极大值抑制保存的最终预测边界框
实践中,在执行非极大值抑制前,我们甚至可以将置信度较低的预测边界框移除,从而减少此算法中的计算量。
我们也可以对非极大值抑制的输出结果,进行后处理(上面的代码就是这样,先非极大抑制删除相似边界框,然后将置信度低于pos_threshold
的边界框删除,这样计算量有点大)
6.多尺度目标检测
感受野
在卷积神经网络中,对于某一层的任意元素x
(该层输出特征图上的元素),其感受野(receptive field
)是指在前向传播期间可能影响x计算的所有元素(来自所有先前层)。
比如上图输出层19元素
的感受野就是输入层的阴影部分
多尺度目标检测
当不同层的特征图在输入图像上分别拥有不同大小的感受野时,它们可以用于检测不同大小的目标。 例如,我们可以设计一个神经网络,其中靠近输出层的特征图单元具有更宽的感受野,这样它们就可以从输入图像中检测到较大的目标。
在多个不同尺度下的特征图上,我们可以生成不同尺寸的锚框来检测不同尺寸的目标
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性