原文：Comparing YOLOv5 and YOLOv8: Which one should you use?YOLOv5 vs YOLOv8相同点Backbone: 均是CSPDarknet53 Anchor Boxes： 均使用 anchor boxes 提升检测精度Non-Maximum Suppression (NMS)：均使用了NMS来滤除同一类别的多个检测框Post-processing：均采用了 post-processing 技术来提升检测精度Optimizer：均采用 Adam 进行模型训练Activation Function：均采用了 Mish 激活函数精度对比Yolov5 在 COCO 数据集的 AP(average precision) 是 50.5%. Yolov5 对于小目标检测同样表现优良，超过了其之前版本的 Yolo.Yolov5 对于真实世界的应用，同样表现不错，例如，视频流里的行人检测.Yolov8 在精度方面是超过 Yolov5 的，其AP是 51.4%，Yolov8m 的 AP 是 54.2%.Yolov8 对于小目标检测有着更优的表现，而且解决了一些 Yolov5 存在的不足.速度FPS对比总结对于小目标检测，Yolov5和Yolo8 均能处理；但，Yolov5 可能更适用于部署到没有 GPU 的设备. 如果有 GPU 的话，Yolov8 会是更好的选择.
出处：Paddle文档平台 - OCR1. OCR综述1.1. 什么是OCROCR(Optical Character Recognition,光学字符识别)是指对图像进行分析识别处理，获取文字和版面信息的过程，是典型的计算机视觉任务，通常由文本检测和文本识别两个子任务构成。文字检测：将图片中的文字区域位置检测出来（如图1(b)所示）；文字识别：对文字区域中的文字进行识别（如图1(c)所示）。1.2. OCR发展历程早在60、70年代，人们就开始对OCR进行研究。在研究的初期，主要以文字识别方法研究为主，而且识别的文字仅为0-9的数字。数字识别主要应用在识别邮件上的邮政编码。在深度学习出现之前，传统算法(如积分投影、腐蚀膨胀、旋转等)在OCR领域占据主导地位。其标准的处理流程包括：图像预处理、文本行检测、单字符分割、单字符识别、后处理。其中：图像预处理主要是对图像的成像问题进行修正，包括几何变换（透视、扭曲、旋转等），去模糊、光线矫正等；文本检测通常使用连通域、滑动窗口两个方向；字符识别算法主要包括图像分类、模版匹配等。受传统算法的局限性，传统OCR仅在比较规整的印刷文档上表现比较好，但在复杂场景(图像模糊、低分辨率、干扰信息)之下，文字检测、识别性能都不够理想。自2012年AlexNet在ImageNet竞赛夺冠以来，深度学习方法开始在图像视频领域大幅超越传统算法，OCR领域也引入了深度学习，包括基于卷积神经网络（Convolutional Neural Network, CNN）来取代传统算法提取特征。深度学习OCR主要分为2步，首先是检测出图像中的文本行、接着进行文本识别。1.3. OCR 常用检测方法OCR文字检测就是将图片中的文字区域检测出来。常用的基于深度学习的文字检测方法一般可以分为基于回归的、基于分割的两大类，当然还有一些将两者进行结合的方法。1.3.1. 基于回归的方法基于回归的方法又分为box回归和像素值回归：[1] - box回归采用box回归的方法主要有CTPN、Textbox系列和EAST优点：对规则形状文本检测效果较好缺点：无法准确检测不规则形状文本。[2] - 像素值回归采用像素值回归的方法主要有CRAFT和SA-Text，这类算法能够检测弯曲文本且对小文本效果优秀但是实时性能不够。1.3.2. 基于分割的算法如PSENet，这类算法不受文本形状的限制，对各种形状的文本都能取得较好的效果，但是往往后处理比较复杂，导致耗时严重。目前也有一些算法专门针对这个问题进行改进，如DB，将二值化进行近似，使其可导，融入训练，从而获取更准确的边界，大大降低了后处理的耗时。1.4. OCR常用识别方法RosettaCRNNSTAR-NetRARESRN1.5. OCR常用评估指标[1] - 检测阶段：先按照检测框和标注框的IOU评估，IOU大于某个阈值判断为检测准确。这里检测框和标注框不同于一般的通用目标检测框，是采用多边形进行表示。检测准确率：正确的检测框个数在全部检测框的占比，主要是判断检测指标。检测召回率：正确的检测框个数在全部标注框的占比，主要是判断漏检的指标。[2] - 识别阶段： 字符识别准确率，即正确识别的文本行占标注的文本行数量的比例，只有整行文本识别对才算正确识别。[3] - 端到端统计： 端对端召回率：准确检测并正确识别文本行在全部标注文本行的占比；端到端准确率：准确检测并正确识别文本行在 检测到的文本行数量 的占比； 准确检测的标准是检测框与标注框的IOU大于某个阈值，正确识别的的检测框中的文本与标注的文本相同。1.6. 应用场景在日常生活中，文字内容无处不在，根据拍摄的内容，自动分析图像中的文字信息已经成为人们的广泛诉求。而通过深度学习技术，可以自动的定位文字区域，并且学习包含丰富语义信息的特征，识别出图像中的文字内容。当前这一技术已经广泛应用于金融、交通等各行各业中。通用场景：办公文档、广告图、表格、手写数字、自然场景图等；卡证：身份证、银行卡、营业执照、名片等；汽车：车牌、驾驶证、合格证等；财务票据：火车票、飞机票、银行支票等；医疗票据：医疗发票、病例首页等；...1.7. OCR面临的挑战对应到OCR技术实现问题上，则一般面临仿射变换、尺度问题、光照不足、拍摄模糊等技术难点； 另外OCR应用常对接海量数据，因此要求数据能够得到实时处理；并且OCR应用常部署在移动端或嵌入式硬件，而端侧的存储空间和计算能力有限，因此对OCR模型的大小和预测速度有很高的要求。2. OCR 检测方法2.1. CTPN2.1.1. 模型介绍CTPN（Connectionist Text Proposal Network，Detecting Text in Natural Image with Connectionist Text Proposal Network是目标检测算法Faster R-CNN 的改进算法，用于文字检测。CTPN根据文本区域的特点做了专门的优化：使用更加符合自然场景文字检测特点的anchor（相比于物体，文字尺寸小）；引入RNN用于处理场景文字检测中存在的序列特征；引入Side-refinement(边界优化)提升文本框边界预测精度。2.1.2. 模型结构CTPN采用的方法是将文本行分割成一个个小块（长度是固定的），然后去检测这些小块，最后使用一种文本行构造法将所有块连起来，如 图1 所示。图1 CTPN 序列特征CTPN网络结构如 图2 所示：图2 CTPN网络结构示意图整个检测分为5步：[1] - 准备数据集、并获取锚点anchor；[2] - 输入图片，经过backbone（用VGG16的前5个Conv stage，即Conv5），提取图片feature map。经过VGG16之后得到的特征图长宽是原图的1/16，通道是512。[3] - 在Conv5得到的feature map上再做卷积核大小为3，步长为1的卷积，进一步提取特征，用于预测当前卷积核所在位置k个anchor对应的类别信息、位置信息。其中，C表示通道数。[4] - 把每一行的所有窗口对应的3*3*C的特征输入到BiLSTM（双向LSTM）网络中，提取文字的序列特征，这时候得到的特征是图像特征和序列特征的融合。[5] - 将第三步得到的特征输入到FC全连接层，并将FC层特征输入两个分类或者回归层中。CTPN任务1的输出是 $ 2k $ ，用于预测候选区域box的起始 $y$ 坐标和高度 $h$；任务2是用来对前景和背景两个任务的分类评分；任务3是 $k$ 个输出的side-refinement的偏移（offset）预测。2.1.3. 模型lossCTPN 的 loss 分为三部分：[1] - Ls：预测每个 anchor 是否包含文本区域的classification loss，采用交叉熵损失；[2] - Lv：文本区域中每个 anchor 的中心y坐标cy与高度h的regression loss，采用Smooth L1；[3] - Lo：文本区域两侧 anchor 的中心x坐标cx 的regression loss，采用Smooth L1。公式如下：$$L(s_{i},V_{j},O_{k})=\frac{1}{N_{s}}\sum\limits_{i}L^{cl}_{S}(S_{i},S^{*}_{i})+\frac{λ_{1}}{N_{v}}\sum\limits_{j}L^{re}_{V}(V_{j},V^{*}_{j})+\frac{λ_{2}}{N_{o}}\sum\limits_{k}L^{re}_{O}(O_{k},O^{*}_{k})$$其中，i 表示预测的所有pn_anchor中的第 i 个 ，Ns表示 pn_anchor 的数量。j 表示 IoU>0.5 的所有 pn_anchor 中的第 j 个，$v_{j}$ 为判断有文本的pn_anchor，Nv 表示和 groudtruth 的 vertical IOU>0.5 的 pn_anchor 的数量。λ1 为多任务的平衡参数，一般取值1.0。Lo 只针对位于在文本区域的左边界和右边界的pn_anchor，来精修边缘。2.1.4. 模型缺点对于非水平的文本的检测效果不好2.2. EAST2.2.1. 模型介绍CTPN在水平文本的检测方面效果比较好，但是对于竖直方向的文本，或者多方向的文本，CTPN检测就很差。然而，在实际场景中，会遇到多种存在竖直方向文本的情况，例如很多书本封面的文本，如 图1 所示。图1 多方案文本示意图因此，很多学者也提出了各种改进方法，其中，比较经典一篇的就是旷世科技在2017年提出来的EAST （EAST: An Efficient and Accurate Scene Text Detector）模型。2.2.2. 模型结构EAST的网络结构总共包含三个部分：Feature extractor stem（特征提取分支）, Feature-merging branch（特征合并分支） 以及 Output layer（输出层），网络结构如 图2 所示：图2 EAST网络结构示意图每一部分网络结构：[1] - 特征提取分支使用backbone网络提取特征，论文默认backbone为PVANet；也可以使用其他的卷积网络，比如VGG16等[2] - 特征合并分支由于在一张图片中，各个文字尺寸大小不一致，所以需要融合不同层次的特征图，小文字的预测需要用到底层的语义信息，大文字的预测要用到高层的语义信息。上一步提取的feature map f1被最先送入unpool层(将原特征图放大２倍)；然后与前一层的feature map f2进行拼接；接着依次送入卷积核大小为1×1和3×3的卷积层，核数通道数随着层递减，依次为128，64，32；重复上面三个步骤２次；最后将经过一个卷积核大小为3×3，核数通道数为32个的卷积层；[3] - 输出层网络层的输出包含文本得分和文本形状，根据不同的文本形状又分为RBOX和QUAD两种情况：RBOX：主要用来预测旋转矩形的文本，包含文本得分和文本形状(AABB boundingbox 和rotate angle)，一共有６个输出，这里AABB分别表示相对于top，right，bottom，left的偏移；QUAD：用来预测不规则四边形的文本，包含文本得分和文本形状(８个相对于corner vertices的偏移)，一共有９个输出，其中QUAD有８个，分别为 $(x_{i},y_{i}), i\in[1,2,3,4]$。2.2.3. 模型lossEAST损失函数由两部分组成，具体公式如下：$$L=L_{s}+λ_{g}L_{g}$$其中，$L_{s}$ 为分数图损失，$L_{g}$ 为几何损失，$λ_{g}$ 表示两个损失之间的重要性。[1] - 分数图损失使用类平衡交叉熵：$$L_{s} = -\beta Y^{*}log(\hat{Y})-(1-\beta)（1-Y^*）log(1-\hat{Y})$$[3] - 几何损失RBOX：IOU损失$$L_{AABB} = -log\frac{\hat{R}\cap R^*}{\hat{R}\cup R^*}$$选转角度损失计算：$L_{\theta}(\hat{\theta},\theta^*) = 1-cos(\hat{\theta}-\theta^*)$$$L_g=L_{AABB} + \lambda L_\theta$$其中 $\hat{R}$ 代表预测的AABB几何形状，$R^*$为其对应的地面真实情况。QUAD：smooth L1损失$$ L_g = \min\limits_{\tilde{Q}\in P_{Q^*}} \sum\limits_{c_i \in C_Q, \tilde c_i \in C_\tilde Q}\frac{smoothed_{L_1}(c_i, \tilde{c}_i)}{8*N_{Q^*}} $$其中$N_{Q^*}$是四边形的短边长度，公式如下：$$N_{Q^*} = \min\limits_{i=1}^{4} D(p_i, p_{(i mode 4)+1})$$2.2.4. 模型优缺点优点可以检测多方向的文本缺点不能检测弯曲文本2.3. DBNet一般分割算法流程是先通过网络输出文本分割的概率图，然后使用预先设定好的阈值将概率图转换为二值图，最后使用后处理操作将像素级的结果转换为检测结果。然而，这样就会使得算法性能很大程度上取决于二值化时阈值的选择。DBNe（Real-time Scene Text Detection with Differentiable Binarization）对这个流程进行了优化，对每个像素点进行自适应二值化，二值化的阈值由网络学习得到，彻底将二值化这一步骤加入到网络里一起训练，这样最终的输出图对于阈值就会非常鲁棒。2.3.1. 模型输入标签DB网络中，网络的输出为3个部分：概率图、阈值图和近似二值图：概率图：图中每个像素点的值为该位置属于文本区域的概率。阈值图：图中每个像素点的值为该位置的二值化阈值。二值图：由概率图和阈值图通过DB算法计算得到，图中像素的值为0或1。在构造损失函数时，我们需要获取对应的真实标签。阈值图的标签 $G_{d}$， 概率图标签$G_{s}$。DB网络中，标签的获取方式参考了PSENet（Progressive Scale Expansion Network,渐进式尺度扩展网络）中的方法，使用扩张和收缩的方式分别获取上述两个真实标签。标签的构造过程如 图1 所示：图1 DB标签构造示意图在该方法中，对于一幅文字图像，文本区域的每个多边形使用一组线段$G=\{S_k\}^n_{k=1}$来进行描述，n为线段个数。如 图1 所示，$n=14$。概率图标签$G_s$的获取方法使用了Vatti clipping算法，该算法常用于收缩多边形，其中，收缩的偏移量D可以使用周长L和面积A计算得到，公式如下：$$ D = \frac{A(1-r^2)}{L} $$其中，r为收缩因子，实验中根据经验设置为0.4。在阈值图标签$G_d$的计算中，首先使用概率图标签$G_s$的计算过程中得到的偏移量D进行多边形的扩充，然后计算$G_d$与$G_s$之间的像素到原始框最近边的归一化距离，最后将其中的值进行缩放，得到的就是最终的阈值图标签$G_d$。2.3.2. 模型介绍DB文本检测模型使用了标准的FPN结构，网络结构如 图2 所示。图2 DB模型网络结构示意图[1] - 第一模块：使用CNN网络，对输入图像提取特征，得到特征图，本实验使用的骨干网络是轻量化网络MobileNetv3，同时使用了FPN结构，获取多尺度的特征，在本实验中，我们提取4个不同尺度下的特征图做拼接。[2] - 第二模块：使用一个卷积层和两个转置卷积层的结构获取预测的概率图和阈值图；[3] - 第三模块：使用DB方法获取近似二值图。注：这里的DB方法和本文的DB模型是有差别的，本文提出的DB方法为可微二值化（Differentiable Binarization），接下来将会详细解释。在传统的图像分割算法中，我们获取概率图后，会使用标准二值化（Standard Binarize）方法进行处理，将低于阈值的像素点置0，高于阈值的像素点置1，公式如下：$$ B_{i,j} = 1,if P_{i,j} >= t, otherwise \ 0 $$但是标准的二值化方法是不可微的，所以也就无法放入到网络中进行优化。因此，本文中提出了可微二值化（Differentiable Binarization），简称DB方法。可微二值化也就是将标准二值化中的阶跃函数进行了近似，使用如下公式进行代替：$$\hat{B} = \frac{1}{1 + e^{-k(P_{i,j}-T_{i,j})}}$$其中，P是上文中获取的概率图，T是上文中获取的阈值图，k是增益因子，在实验中，根据经验选取为50。标准二值化和可微二值化的对比图如 图3(a) 所示。之所以DB方法会改善算法性能，我们可以在反向传播时梯度的计算上进行观察。当使用交叉熵损失时，正负样本的loss分别为 $l_+$ 和 $l_-$ $$ l_+ = -log(\frac{1}{1 + e^{-k(P_{i,j}-T_{i,j})}})$$$$l_- = -log(1-\frac{1}{1 + e^{-k(P_{i,j}-T_{i,j})}})$$我们对输入x求偏导，则会得到：$$\frac{\delta{l_+}}{\delta{x}} = -kf(x)e^{-kx}$$$$\frac{\delta{l_-}}{\delta{x}} = -kf(x)$$此时，由于有了增强因子k，错误预测对梯度的影响也就被放大了，从而可以促进模型的优化过程并产生更为清晰的预测结果。图3(b) 中，$x<0$ 的部分为正样本预测为负样本的情况，可以看到，增益因子k将梯度进行了放大；而 图3(c) 中$x>0$ 的部分为负样本预测为正样本的情况，梯度同样也被放大了。图3 DB算法示意图在训练阶段，使用3个预测图与真实标签共同完成损失函数的计算以及模型训练；在预测阶段，只需要使用概率图，通过一系列的后处理方式即可获得最终的预测结果。由于网络预测的概率图是经过收缩后的结果，所以在后处理步骤中，使用相同的偏移值将预测的多边形区域进行扩张，即可得到最终的文本框。2.3.3. 模型loss由于训练阶段获取了3个预测图，所以在损失函数中，也需要结合这3个预测图与它们对应的真实标签分别构建3部分损失函数。总的损失函数的公式定义如下:$$L = L_b + \alpha \times L_s + \beta \times L_t$$其中，$L$为总的损失，$L_b$为近似二值图的损失，使用 Dice 损失；$L_s$为概率图损失，使用带 OHEM 的 Dice 损失；$L_t$为阈值图损失，使用预测值和标签间的$L_1$距离。其中，$\alpha$和$\beta$为权重系数。接下来分析这3个loss：[1] - 首先是Dice Loss，Dice Loss是比较预测结果跟标签之间的相似度，常用于二值图像分割。$dice\_loss = 1 - \frac{2 \times intersection\_area}{total\_area}$[2] - 其次是MaskL1 Loss，是计算预测值和标签间的$L_1$距离[3] - 最后是Balance Loss，是带OHEM的Dice Loss，目的是为了改善正负样本不均衡的问题。OHEM为一种特殊的自动采样方式，可以自动的选择难样本进行loss的计算，从而提升模型的训练效果。2.3.4. 模型优缺点优点可以同时检测水平、多方向和弯曲文字；在性能和速度上都获取不错的效果。3. OCR 识别方法3.1. CRNN3.1.1. 模型介绍传统的文本识别方法需要先对单个文字进行切割，然后再对单个文字进行识别。本实验使用的是图像文本识别的经典算法CRNN（An End-to-End Trainable Neural Network for Image-based SequenceRecognition and Its Application to Scene Text Recognition）。CRNN是2015年被提出的，到目前为止还是被广泛应用。该算法的主要思想是认为文本识别其实需要对序列进行预测，所以采用了预测序列常用的RNN网络。算法通过CNN提取图片特征，然后采用RNN对序列进行预测，最终使用CTC方法得到最终结果。3.1.2. 模型结构CRNN的主要结构包括基于CNN的图像特征提取模块以及基于多层双向LSTM的文字序列特征提取模块。CRNN的网络结构如 图1 所示：图1 CRNN模型网络结构示意图[1] - 第一模块：使用CNN网络，对输入图像进行特征提取，从而得到特征图。本实验使用的CNN网络同样是轻量化网络MobileNetv3，其中输入图像的高度统一设置为32，宽度可以为任意长度，经过CNN网络后，特征图的高度缩放为1；[2] - 第二模块：Im2Seq，将CNN获取的特征图变换为RNN需要的特征向量序列的形状；[3] - 第三模块：使用双向LSTM（BiLSTM）对特征序列进行预测，学习序列中的每个特征向量并输出预测标签分布。这里其实相当于把特征向量的宽度视为LSTM中的时间维度；[4] - 第四模块：使用全连接层获取模型的预测结果；[5] - 第五模块：CTC转录层，解码模型输出的预测结果，得到最终输出。3.1.3. 模型loss为了解决预测标签与真实标签无法对齐的问题，这里使用了CTC loss进行模型，具体参考：CTC算法3.1.4. 模型优缺点优点可以进行端到端的训练；可以进行不定长文本的识别；模型简单，效果好。缺点受CTC算法对速度的要求，输出长度受到限制，识别文本不能太长。
出处：Paddle文档平台 - 非极大值抑制在实际的目标检测过程中，不管是用什么方式获取候选区域，都会存在一个通用的问题，那就是网络对同一个目标可能会进行多次检测。这也就导致对于同一个物体，会产生多个预测框。因此需要消除重叠较大的冗余预测框。具体的处理方法就是非极大值抑制（NMS）。假设使用模型对图片进行预测，一共输出了11个预测框及其得分，在图上画出预测框如 图1 所示。在每个人像周围，都出现了多个预测框，需要消除冗余的预测框以得到最终的预测结果。图1 预测框示意图输出11个预测框及其得分的代码实现如下：# 画图展示目标物体边界框 import numpy as np import matplotlib.pyplot as plt import matplotlib.patches as patches from matplotlib.image import imread import math # 定义画矩形框的程序 def draw_rectangle(currentAxis, bbox, edgecolor = 'k', facecolor = 'y', fill=False, linestyle='-'): # currentAxis，坐标轴，通过plt.gca()获取 # bbox，边界框，包含四个数值的list， [x1, y1, x2, y2] # edgecolor，边框线条颜色 # facecolor，填充颜色 # fill, 是否填充 # linestype，边框线型 # patches.Rectangle(xy, width, height,linewidth,edgecolor,facecolor,fill, linestyle) # xy:左下角坐标; width:矩形框的宽; height:矩形框的高; linewidth:线宽; edgecolor:边界颜色; facecolor:填充颜色; fill:是否填充; linestyle:线断类型 rect=patches.Rectangle((bbox[0], bbox[1]), bbox[2]-bbox[0]+1, bbox[3]-bbox[1]+1, linewidth=1, edgecolor=edgecolor,facecolor=facecolor,fill=fill, linestyle=linestyle) currentAxis.add_patch(rect) plt.figure(figsize=(10, 10)) # 传入图片路径 filename = '/home/aistudio/work/images/section3/000000086956.jpg' im = imread(filename) plt.imshow(im) currentAxis=plt.gca() # 预测框位置，由网络预测得到 boxes = np.array([[4.21716537e+01, 1.28230896e+02, 2.26547668e+02, 6.00434631e+02], [3.18562988e+02, 1.23168472e+02, 4.79000000e+02, 6.05688416e+02], [2.62704697e+01, 1.39430557e+02, 2.20587097e+02, 6.38959656e+02], [4.24965363e+01, 1.42706665e+02, 2.25955185e+02, 6.35671204e+02], [2.37462646e+02, 1.35731537e+02, 4.79000000e+02, 6.31451294e+02], [3.19390472e+02, 1.29295090e+02, 4.79000000e+02, 6.33003845e+02], [3.28933838e+02, 1.22736115e+02, 4.79000000e+02, 6.39000000e+02], [4.44292603e+01, 1.70438187e+02, 2.26841858e+02, 6.39000000e+02], [2.17988785e+02, 3.02472412e+02, 4.06062927e+02, 6.29106628e+02], [2.00241089e+02, 3.23755096e+02, 3.96929321e+02, 6.36386108e+02], [2.14310303e+02, 3.23443665e+02, 4.06732849e+02, 6.35775269e+02]]) # 预测框得分，由网络预测得到 scores = np.array([0.5247661 , 0.51759845, 0.86075854, 0.9910175 , 0.39170712, 0.9297706 , 0.5115228 , 0.270992 , 0.19087596, 0.64201415, 0.879036]) # 画出所有预测框 for box in boxes: draw_rectangle(currentAxis, box)这里使用非极大值抑制（Non-Maximum Suppression, NMS）来消除冗余框。基本思想是，如果有多个预测框都对应同一个物体，则只选出得分最高的那个预测框，剩下的预测框被丢弃掉。如何判断两个预测框对应的是同一个物体呢，标准该怎么设置？如果两个预测框的类别一样，而且他们的位置重合度比较大，则可以认为他们是在预测同一个目标。非极大值抑制的做法是，选出某个类别得分最高的预测框，然后看哪些预测框跟它的IoU大于阈值，就把这些预测框给丢弃掉。这里IoU的阈值是超参数，需要提前设置，这里我们参考YOLOv3算法，里面设置的是0.5。比如在上面的程序中，boxes里面一共对应11个预测框，scores给出了它们预测"人"这一类别的得分，NMS的具体做法如下。[1] - Step0：创建选中列表，keep_list = [][2] - Step1：对得分进行排序，remain_list = [ 3, 5, 10, 2, 9, 0, 1, 6, 4, 7, 8]， [3] - Step2：选出boxes[3]，此时keep_list为空，不需要计算IoU，直接将其放入keep_list，keep_list = [3]， remain_list=[5, 10, 2, 9, 0, 1, 6, 4, 7, 8][4] - Step3：选出boxes[5]，此时keep_list中已经存在boxes[3]，计算出IoU(boxes[3], boxes[5]) = 0.0，显然小于阈值，则keep_list=[3, 5], remain_list = [10, 2, 9, 0, 1, 6, 4, 7, 8][5] - Step4：选出boxes[10]，此时keep_list=[3, 5]，计算IoU(boxes[3], boxes[10])=0.0268，IoU(boxes[5], boxes[10])=0.0268 = 0.24，都小于阈值，则keep_list = [3, 5, 10]，remain_list=[2, 9, 0, 1, 6, 4, 7, 8][6] - Step5：选出boxes[2]，此时keep_list = [3, 5, 10]，计算IoU(boxes[3], boxes[2]) = 0.88，超过了阈值，直接将boxes[2]丢弃，keep_list=[3, 5, 10]，remain_list=[9, 0, 1, 6, 4, 7, 8][7] - Step6：选出boxes[9]，此时keep_list = [3, 5, 10]，计算IoU(boxes[3], boxes[9]) = 0.0577，IoU(boxes[5], boxes[9]) = 0.205，IoU(boxes[10], boxes[9]) = 0.88，超过了阈值，将boxes[9]丢弃掉。keep_list=[3, 5, 10]，remain_list=[0, 1, 6, 4, 7, 8][8] - Step7：重复上述Step6直到remain_list为空。非极大值抑制的具体实现代码如下面的nms函数的定义。# 非极大值抑制 def nms(bboxes, scores, score_thresh, nms_thresh): """ nms """ inds = np.argsort(scores) inds = inds[::-1] keep_inds = [] while(len(inds) > 0): cur_ind = inds[0] cur_score = scores[cur_ind] # if score of the box is less than score_thresh, just drop it if cur_score < score_thresh: break keep = True for ind in keep_inds: current_box = bboxes[cur_ind] remain_box = bboxes[ind] iou = box_iou_xyxy(current_box, remain_box) if iou > nms_thresh: keep = False break if keep: keep_inds.append(cur_ind) inds = inds[1:] return np.array(keep_inds)最终得到keep_list=[3, 5, 10]，也就是预测框3、5、10被最终挑选出来了，如 图2 所示。图2 NMS结果示意图整个过程的实现代码如下：# 画图展示目标物体边界框 import numpy as np import matplotlib.pyplot as plt import matplotlib.patches as patches from matplotlib.image import imread import math # 定义画矩形框的程序 def draw_rectangle(currentAxis, bbox, edgecolor = 'k', facecolor = 'y', fill=False, linestyle='-'): # currentAxis，坐标轴，通过plt.gca()获取 # bbox，边界框，包含四个数值的list， [x1, y1, x2, y2] # edgecolor，边框线条颜色 # facecolor，填充颜色 # fill, 是否填充 # linestype，边框线型 # patches.Rectangle需要传入左上角坐标、矩形区域的宽度、高度等参数 rect=patches.Rectangle((bbox[0], bbox[1]), bbox[2]-bbox[0]+1, bbox[3]-bbox[1]+1, linewidth=1, edgecolor=edgecolor,facecolor=facecolor,fill=fill, linestyle=linestyle) currentAxis.add_patch(rect) plt.figure(figsize=(10, 10)) filename = '/home/aistudio/work/images/section3/000000086956.jpg' im = imread(filename) plt.imshow(im) currentAxis=plt.gca() boxes = np.array([[4.21716537e+01, 1.28230896e+02, 2.26547668e+02, 6.00434631e+02], [3.18562988e+02, 1.23168472e+02, 4.79000000e+02, 6.05688416e+02], [2.62704697e+01, 1.39430557e+02, 2.20587097e+02, 6.38959656e+02], [4.24965363e+01, 1.42706665e+02, 2.25955185e+02, 6.35671204e+02], [2.37462646e+02, 1.35731537e+02, 4.79000000e+02, 6.31451294e+02], [3.19390472e+02, 1.29295090e+02, 4.79000000e+02, 6.33003845e+02], [3.28933838e+02, 1.22736115e+02, 4.79000000e+02, 6.39000000e+02], [4.44292603e+01, 1.70438187e+02, 2.26841858e+02, 6.39000000e+02], [2.17988785e+02, 3.02472412e+02, 4.06062927e+02, 6.29106628e+02], [2.00241089e+02, 3.23755096e+02, 3.96929321e+02, 6.36386108e+02], [2.14310303e+02, 3.23443665e+02, 4.06732849e+02, 6.35775269e+02]]) scores = np.array([0.5247661 , 0.51759845, 0.86075854, 0.9910175 , 0.39170712, 0.9297706 , 0.5115228 , 0.270992 , 0.19087596, 0.64201415, 0.879036]) left_ind = np.where((boxes[:, 0]<60) * (boxes[:, 0]>20)) left_boxes = boxes[left_ind] left_scores = scores[left_ind] colors = ['r', 'g', 'b', 'k'] # 画出最终保留的预测框 inds = nms(boxes, scores, score_thresh=0.01, nms_thresh=0.5) # 打印最终保留的预测框是哪几个 print(inds) for i in range(len(inds)): box = boxes[inds[i]] draw_rectangle(currentAxis, box, edgecolor=colors[i]) 多分类NMS需要说明的是当数据集中含有多个类别的物体时，需要做多分类非极大值抑制，其实现原理与非极大值抑制相同，区别在于需要对每个类别都做非极大值抑制，实现代码如下面的multiclass_nms所示。# 多分类非极大值抑制 def multiclass_nms(bboxes, scores, score_thresh=0.01, nms_thresh=0.45, pre_nms_topk=1000, pos_nms_topk=100): """ This is for multiclass_nms """ batch_size = bboxes.shape[0] class_num = scores.shape[1] rets = [] for i in range(batch_size): bboxes_i = bboxes[i] scores_i = scores[i] ret = [] # 对每个类别都进行NMS操作 for c in range(class_num): scores_i_c = scores_i[c] keep_inds = nms(bboxes_i, scores_i_c, score_thresh, nms_thresh) if len(keep_inds) < 1: continue keep_bboxes = bboxes_i[keep_inds] keep_scores = scores_i_c[keep_inds] keep_results = np.zeros([keep_scores.shape[0], 6]) keep_results[:, 0] = c keep_results[:, 1] = keep_scores[:] keep_results[:, 2:6] = keep_bboxes[:, :] ret.append(keep_results) if len(ret) < 1: rets.append(ret) continue ret_i = np.concatenate(ret, axis=0) scores_i = ret_i[:, 1] if len(scores_i) > pos_nms_topk: inds = np.argsort(scores_i)[::-1] inds = inds[:pos_nms_topk] ret_i = ret_i[inds] rets.append(ret_i) return rets
出处：Paddle文档平台 - IoU（交并比）在目标检测任务中，通常会使用交并比（Intersection of Union，IoU）作为衡量指标，来衡量两个矩形框之间的关系。例如在基于锚框的目标检测算法中，我们知道当锚框中包含物体时，我们需要预测物体类别并微调锚框的坐标，从而获得最终的预测框。此时，判断锚框中是否包含物体就需要用到交并比，当锚框与真实框交并比足够大时，我们就可以认为锚框中包含了该物体；而锚框与真实框交并比很小时，我们就可以认为锚框中不包含该物体。此外，在后面NMS的计算过程中，同样也要使用交并比来判断不同矩形框是否重叠。交并比这一概念来源于数学中的集合，用来描述两个集合$A$和$B$之间的关系，它等于两个集合的交集里面所包含的元素个数，除以它们的并集里面所包含的元素个数，具体计算公式如下：$$IoU = \frac{A\cap B}{A \cup B}$$我们将用这个概念来描述两个框之间的重合度。两个框可以看成是两个像素的集合，它们的交并比等于两个框重合部分的面积除以它们合并起来的面积。下图“交集”中青色区域是两个框的重合面积，下图“并集”中蓝色区域是两个框的相并面积。用这两个面积相除即可得到它们之间的交并比，如 图1 所示。图1 交并比假设两个矩形框A和B的位置分别为：$$A: [x_{a1}, y_{a1}, x_{a2}, y_{a2}]$$$$B: [x_{b1}, y_{b1}, x_{b2}, y_{b2}]$$假如位置关系如 图2 所示：图2 计算交并比如果二者有相交部分，则相交部分左上角坐标为：$$x_1 = max(x_{a1}, x_{b1}), \ \ \ \ \ y_1 = max(y_{a1}, y_{b1})$$相交部分右下角坐标为：$$x_2 = min(x_{a2}, x_{b2}), \ \ \ \ \ y_2 = min(y_{a2}, y_{b2})$$计算先交部分面积：$$intersection = max(x_2 - x_1 + 1.0, 0) \cdot max(y_2 - y_1 + 1.0, 0)$$矩形框A和B的面积分别是：$$S_A = (x_{a2} - x_{a1} + 1.0) \cdot (y_{a2} - y_{a1} + 1.0)$$$$S_B = (x_{b2} - x_{b1} + 1.0) \cdot (y_{b2} - y_{b1} + 1.0)$$计算相并部分面积：$$union = S_A + S_B - intersection$$计算交并比：$$IoU = \frac{intersection}{union}$$交并比实现代码如下：[1] - 当矩形框的坐标形式为xyxy时import numpy as np # 计算IoU，矩形框的坐标形式为xyxy def box_iou_xyxy(box1, box2): # 获取box1左上角和右下角的坐标 x1min, y1min, x1max, y1max = box1[0], box1[1], box1[2], box1[3] # 计算box1的面积 s1 = (y1max - y1min + 1.) * (x1max - x1min + 1.) # 获取box2左上角和右下角的坐标 x2min, y2min, x2max, y2max = box2[0], box2[1], box2[2], box2[3] # 计算box2的面积 s2 = (y2max - y2min + 1.) * (x2max - x2min + 1.) # 计算相交矩形框的坐标 xmin = np.maximum(x1min, x2min) ymin = np.maximum(y1min, y2min) xmax = np.minimum(x1max, x2max) ymax = np.minimum(y1max, y2max) # 计算相交矩形行的高度、宽度、面积 inter_h = np.maximum(ymax - ymin + 1., 0.) inter_w = np.maximum(xmax - xmin + 1., 0.) intersection = inter_h * inter_w # 计算相并面积 union = s1 + s2 - intersection # 计算交并比 iou = intersection / union return iou bbox1 = [100., 100., 200., 200.] bbox2 = [120., 120., 220., 220.] iou = box_iou_xyxy(bbox1, bbox2) print('IoU is {}'.format(iou)) [2] - 当矩形框的坐标形式为xywh时import numpy as np # 计算IoU，矩形框的坐标形式为xywh def box_iou_xywh(box1, box2): x1min, y1min = box1[0] - box1[2]/2.0, box1[1] - box1[3]/2.0 x1max, y1max = box1[0] + box1[2]/2.0, box1[1] + box1[3]/2.0 s1 = box1[2] * box1[3] x2min, y2min = box2[0] - box2[2]/2.0, box2[1] - box2[3]/2.0 x2max, y2max = box2[0] + box2[2]/2.0, box2[1] + box2[3]/2.0 s2 = box2[2] * box2[3] xmin = np.maximum(x1min, x2min) ymin = np.maximum(y1min, y2min) xmax = np.minimum(x1max, x2max) ymax = np.minimum(y1max, y2max) inter_h = np.maximum(ymax - ymin, 0.) inter_w = np.maximum(xmax - xmin, 0.) intersection = inter_h * inter_w union = s1 + s2 - intersection iou = intersection / union return iou bbox1 = [100., 100., 200., 200.] bbox2 = [120., 120., 220., 220.] iou = box_iou_xywh(bbox1, bbox2) print('IoU is {}'.format(iou)) 为了直观的展示交并比的大小跟重合程度之间的关系，图3 示意了不同交并比下两个框之间的相对位置关系，从 IoU = 0.95 到 IoU = 0。图3 不同交并比下两个框之间相对位置示意图问题：什么情况下两个矩形框的IoU等于1？ 答案：两个矩形框完全重合。什么情况下两个矩形框的IoU等于0？答案：两个矩形框完全不相交。
出处：Paddle文档平台 - 边界框（bounding box）1. 边界框（bounding box）在检测任务中，需要同时预测物体的类别和位置，因此需要引入一些跟位置相关的概念。通常使用边界框（bounding box，bbox）来表示物体的位置，边界框是正好能包含物体的矩形框，如 图1 所示，图中3个人分别对应3个边界框。图1 边界框通常表示边界框的位置有两种方式：[1] - 即$(x_1, y_1, x_2, y_2)$，其中$(x_1, y_1)$是矩形框左上角的坐标，$(x_2, y_2)$是矩形框右下角的坐标。如，图1 中3个红色矩形框用$xyxy$格式表示如下：左：$(40.93, 141.1, 226.99, 515.73)$。中：$(214.29, 325.03, 399.82, 631.37)$。右：$(247.2, 131.62, 480.0, 639.32)$。[2] - $xywh$，即$(x, y, w, h)$，其中$(x, y)$是矩形框中心点的坐标，$w$是矩形框的宽度，$h$是矩形框的高度。在检测任务中，训练数据集的标签里会给出目标物体真实边界框所对应的$(x_1, y_1, x_2, y_2)$，这样的边界框也被称为真实框（ground truth box），图1 画出了3个人像所对应的真实框。模型会对目标物体可能出现的位置进行预测，由模型预测出的边界框则称为预测框（prediction box）。要完成一项检测任务，通常希望模型能够根据输入的图片，输出一些预测的边界框，以及边界框中所包含的物体的类别或者说属于某个类别的概率，例如这种格式: $[L, P, x_1, y_1, x_2, y_2]$，其中$L$是预测出的类别标签，$P$是预测物体属于该类别的概率。一张输入图片可能会产生多个预测框，接下来让我们一起学习如何完成这项任务。注意：在阅读代码时，请注意使用的是哪一种格式的表示方式。图片坐标的原点在左上角，$x$轴向右为正方向，$y$轴向下为正方向。2. 锚框（Anchor box）目标检测算法通常会在输入图像中采样大量的区域，然后判断这些区域中是否包含我们感兴趣的目标，并调整区域边缘从而更准确地预测目标的真实边界框（ground-truth bounding box）。不同的模型使用的区域采样方法可能不同。这里我们介绍其中的一种方法：它以每个像素为中心生成多个大小和宽高比（aspect ratio）不同的边界框。这些边界框被称为锚框（anchor box）。在目标检测任务中，我们会先设定好锚框的大小和形状，再以图像上某一个点为中心画出这些锚框，将这些锚框当成可能的候选区域。目前，常用的锚框尺寸选择方法有：[1] - 人为经验选取[2] - k-means聚类[3] - 作为超参数进行学习模型对这些候选区域是否包含物体进行预测，如果包含目标物体，则还需要进一步预测出物体所属的类别。还有更为重要的一点是，模型需要预测出微调的幅度。这是因为锚框位置是固定的，它不大可能刚好跟物体边界框重合，所以需要在锚框的基础上进行微调以形成能准确描述物体位置的预测框。在训练过程中，模型通过学习不断的调整参数，最终能学会如何判别出锚框所代表的候选区域是否包含物体，如果包含物体的话，物体属于哪个类别，以及物体边界框相对于锚框位置需要调整的幅度。而不同的模型往往有着不同的生成锚框的方式。在下图中，以像素点[300, 500]为中心可以使用下面的程序生成3个框，如 图2 中蓝色框所示，其中锚框A1跟人像区域非常接近。图2 锚框# 画图展示如何绘制边界框和锚框 import numpy as np import matplotlib.pyplot as plt import matplotlib.patches as patches from matplotlib.image import imread import math # 定义画矩形框的程序 def draw_rectangle(currentAxis, bbox, edgecolor = 'k', facecolor = 'y', fill=False, linestyle='-'): # currentAxis，坐标轴，通过plt.gca()获取 # bbox，边界框，包含四个数值的list， [x1, y1, x2, y2] # edgecolor，边框线条颜色 # facecolor，填充颜色 # fill, 是否填充 # linestype，边框线型 # patches.Rectangle(xy, width, height,linewidth,edgecolor,facecolor,fill, linestyle) # xy:左下角坐标; width:矩形框的宽; height:矩形框的高; linewidth:线宽; edgecolor:边界颜色; facecolor:填充颜色; fill:是否填充; linestyle:线断类型 rect=patches.Rectangle((bbox[0], bbox[1]), bbox[2]-bbox[0]+1, bbox[3]-bbox[1]+1, linewidth=1, edgecolor=edgecolor,facecolor=facecolor,fill=fill, linestyle=linestyle) currentAxis.add_patch(rect) plt.figure(figsize=(10, 10)) # 传入图片路径 filename = '/home/aistudio/work/images/section3/000000086956.jpg' im = imread(filename) plt.imshow(im) # 使用xyxy格式表示物体真实框 bbox1 = [214.29, 325.03, 399.82, 631.37] bbox2 = [40.93, 141.1, 226.99, 515.73] bbox3 = [247.2, 131.62, 480.0, 639.32] currentAxis=plt.gca() # 绘制3个真实框 draw_rectangle(currentAxis, bbox1, edgecolor='r') draw_rectangle(currentAxis, bbox2, edgecolor='r') draw_rectangle(currentAxis, bbox3,edgecolor='r') # 绘制锚框 def draw_anchor_box(center, length, scales, ratios, img_height, img_width): """ 以center为中心，产生一系列锚框 其中length指定了一个基准的长度 scales是包含多种尺寸比例的list ratios是包含多种长宽比的list img_height和img_width是图片的尺寸，生成的锚框范围不能超出图片尺寸之外 """ bboxes = [] for scale in scales: for ratio in ratios: h = length*scale*math.sqrt(ratio) w = length*scale/math.sqrt(ratio) x1 = max(center[0] - w/2., 0.) y1 = max(center[1] - h/2., 0.) x2 = min(center[0] + w/2. - 1.0, img_width - 1.0) y2 = min(center[1] + h/2. - 1.0, img_height - 1.0) print(center[0], center[1], w, h) bboxes.append([x1, y1, x2, y2]) for bbox in bboxes: draw_rectangle(currentAxis, bbox, edgecolor = 'b') img_height = im.shape[0] img_width = im.shape[1] # 绘制锚框 draw_anchor_box([300., 500.], 100., [2.0], [0.5, 1.0, 2.0], img_height, img_width) ################# 以下为添加上图中的文字说明和箭头############################### plt.text(285, 285, 'G1', color='red', fontsize=20) plt.arrow(300, 288, 30, 40, color='red', width=0.001, length_includes_head=True, \ head_width=5, head_length=10, shape='full') plt.text(190, 320, 'A1', color='blue', fontsize=20) plt.arrow(200, 320, 30, 40, color='blue', width=0.001, length_includes_head=True, \ head_width=5, head_length=10, shape='full') plt.text(160, 370, 'A2', color='blue', fontsize=20) plt.arrow(170, 370, 30, 40, color='blue', width=0.001, length_includes_head=True, \ head_width=5, head_length=10, shape='full') plt.text(115, 420, 'A3', color='blue', fontsize=20) plt.arrow(127, 420, 30, 40, color='blue', width=0.001, length_includes_head=True, \ head_width=5, head_length=10, shape='full') plt.show()锚框的概念最早在Faster rcnn[1]目标检测算法中被提出，后来被YOLOv2[2]等各种目标检测算法借鉴。对比于早期目标检测算法中使用的滑动窗口或Selective Search方法，使用锚框来提取候选区域大大减少了时间开销。而对比YOLOv1[3]中直接回归坐标值来计算检测框，使用锚框可以简化目标检测问题，使得网络仅仅学习锚框的位置偏移量即可，从而使得网络模型更容易学习。参考文献[1] - Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks[2] - YOLO9000: Better, Faster, Stronger[3] - You Only Look Once: Unified, Real-Time Object Detection
出处：Paddle文档平台 - 目标检测对计算机而言，能够“看到”的是图像被编码之后的数字，它很难理解高层语义概念，比如图像或者视频帧中出现的目标是人还是物体，更无法定位目标出现在图像中哪个区域。目标检测的主要目的是让计算机可以自动识别图片或者视频帧中所有目标的类别，并在该目标周围绘制边界框，标示出每个目标的位置，如 图1 所示。图1 图像分类和目标检测示意图图1(a)是图像分类任务，只需对这张图片进行类别识别。图1(b)是目标检测任务，不仅要识别出这一张图片中的类别为斑马，还要标出图中斑马的位置。1. 应用场景如 图2 所示，如今的目标检测不论在日常生活中还是工业生产中都有着非常多的应用场景。消费娱乐：智能手机的人脸解锁以及支付APP中的人脸支付；自动售货机使用的商品检测；视频网站中图片、视频审核等；智慧交通：自动驾驶中的行人检测、车辆检测、红绿灯检测等；工业生产：工业生产中的零件计数、缺陷检测；设备巡检场景下的设备状态监控；厂区中的烟火检测、安全帽检测等；智慧医疗：眼底、肺部等器官病变检测；新冠疫情中的口罩检测等。图2 目标检测应用场景2. 目标检测发展历程在图像分类任务中，会先使用卷积神经网络提取图像特征，然后再用这些特征预测分类概率，根据训练样本标签建立起分类损失函数，开启端到端的训练，如 图3 所示。图3 图像分类流程示意图但对于目标检测问题，按照 图3 的流程则行不通。因为在对整张图提取特征的过程中无法体现出不同目标之间的区别，最终也就没法分别标示出每个物体所在的位置。为了解决这个问题，结合图片分类任务取得的成功经验，我们可以将目标检测任务进行拆分。假设我们使用某种方式在输入图片上生成一系列可能包含物体的区域，这些区域称为候选区域。对于每个候选区域，可以单独当成一幅图像来看待，使用图像分类模型对候选区域进行分类，看它属于哪个类别或者背景（即不包含任何物体的类别）。那么，现在问题的关键就是如何产生候选区域？比如我们可以使用穷举法来产生候选区域，如 图4 所示。图4 候选区域A为图像上的某个像素点，B为A右下方另外一个像素点，A、B两点可以确定一个矩形框，记作AB。如图4(a)所示：A在图片左上角位置，B遍历除A之外的所有位置，生成矩形框$A_1B_1, …, A_1B_n, …$如图4(b)所示：A在图片中间某个位置，B遍历A右下方所有位置，生成矩形框$A_kB_1, …, A_kB_n, …$当A遍历图像上所有像素点，B则遍历它右下方所有的像素点，最终生成的矩形框集合${A_iB_j}$将会包含图像上所有可以选择的区域。只要我们对每个候选区域的分类足够的准确，则一定能找到跟实际物体足够接近的区域来。穷举法也许能得到正确的预测结果，但其计算量也是非常巨大的，其所生成的总候选区域数目约为$\frac{W^2 H^2}{4}$，假设$H=W=100$，总数将会达到$2.5 \times 10^{7}$个，如此多的候选区域使得这种方法几乎没有什么实用性。但是通过这种方式，我们可以看出，假设分类任务完成的足够完美，从理论上来讲检测任务也是可以解决的，亟待解决的问题是如何设计出合适的方法来产生候选区域。科学家们开始思考，是否可以应用传统图像算法先产生候选区域，然后再用卷积神经网络对这些区域进行分类？2013年，Ross Girshick等人于首次将CNN的方法应用在目标检测任务上，他们使用传统图像算法Selective Search产生候选区域，取得了极大的成功，这就是对目标检测领域影响深远的区域卷积神经网络(R-CNN[1])模型。2015年，Ross Girshick对此方法进行了改进，提出了Fast R-CNN[2]模型。通过将不同区域的物体共用卷积层的计算，大大缩减了计算量，提高了处理速度，而且还引入了调整目标物体位置的回归方法，进一步提高了位置预测的准确性。2015年，Shaoqing Ren等人提出了Faster R-CNN[3]模型，提出了RPN的方法来产生物体的候选区域，这一方法不再需要使用传统的图像处理算法来产生候选区域，进一步提升了处理速度。2017年，Kaiming He等人提出了Mask R-CNN[4]模型，只需要在Faster R-CNN模型上添加比较少的计算量，就可以同时实现目标检测和物体实例分割两个任务。以上都是基于R-CNN系列的著名模型，对目标检测方向的发展有着较大的影响力。此外，还有一些其他模型，比如SSD[5]、YOLO[6,7,8]、R-FCN[9]等也都是目标检测领域流行的模型结构。图5 为目标检测综述文章[10]中的一幅图，梳理了近些年目标检测算法的发展流程。图5 目标检测算法发展流程其中，由于上文所述的R-CNN的系列算法将目标检测任务分成两个阶段，先在图像上产生候选区域，再对候选区域进行分类并预测目标物体位置，所以它们通常被叫做两阶段检测算法。而SSD和YOLO系列算法则是使用一个网络同时产生候选区域并预测出物体的类别和位置，所以它们通常被叫做单阶段检测算法。上文中提到，穷举法来获取候选区域是不现实的。因此在后来的经典算法中，常用的一个思路是使用Anchor提取候选目标框，Anchor是预先设定好比例的一组候选框集合，在图片上进行滑动就可以获取候选区域了。由于这类算法都是使用Anchor提取候选目标框。在特征图的每一个点上，对Anchor进行分类和回归。所以这些算法也统称为基于Anchor的算法。但是这种基于Anchor的方法，在实际应用中存在一些问题:Anchor是人为手工设计的，那我们换个数据集，应该设置多少？设置多大？长宽比如何设置？Anchor这种密集框，数量多，训练时如何选择正负样本？Anchor设置也导致超参数较多，实际业务扩展中，相对来说，就有点麻烦。由于上述缺点的存在，近些年研究者们还提出了另外一类效果优异的算法，这些算法不再使用anchor回归预测框，因此也称作Anchor-free的算法，例如：CornerNet[11]和CenterNet[12]等。图6 为大家简单罗列了经典的Anchor-base和Anchor-free的算法。图6 基于深度学习的目标检测算法发展流程Anchor-base和Anchor-free的算法也各具优势，下表为大家简单对比了几类算法各自的优缺点。 Anchor-Based单阶段Anchor-Based两阶段Anchor-Free网络结构简单复杂简单精度优更优较优预测速度快稍慢快超参数较多多相对少扩展性一般一般较好3. 常用数据集在目标检测领域，常用的开源数据集主要包含以下4个：Pascal VOC[13]、COCO[14]、Object365[15]、OpenImages[16]。这些数据集的类别数、图片数、目标框的总数量各不相同，因此难易也各不相同。这里整理了4个数据集的具体情况，如下表所示。数据集类别数train图片数，box数val图片数，box数boxes/ImagePascal VOC-2012205717, 1.3万+5823， 1.3万+2.4COCO80118287， 4万+5000，3.6万+7.3Object365365600k, 9623k38k, 479k16OpenImages185001643042, 86万+100000，69.6万+7.0Pascal VOC-2012：VOC数据集是 PASCAL VOC挑战赛使用的数据集，包含了20种常见类别的图片，是目标检测领域的经典学术数据集之一。COCO：COCO数据集是一个经典的大规模目标检测、分割、姿态估计数据集，图片数据主要从复杂的日常场景中截取，共80类。目前的学术论文经常会使用COCO数据集进行精度评测。Object365：旷世科技发布的大规模通用物体检测数据集，共365类。OpenImages18：谷歌发布的超大规模数据集，共500类。参考文献[1] - Rich feature hierarchies for accurate object detection and semantic segmentation [2] - Fast R-CNN [3] - Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks [4] - Mask R-CNN [5] - SSD: Single Shot MultiBox Detector[6] - You Only Look Once: Unified, Real-Time Object Detection[7] - YOLO9000: Better, Faster, Stronger[8] - YOLOv3: An Incremental Improvement[9] - R-FCN: Object Detection via Region-based Fully Convolutional Networks[10] - Object Detection in 20 Years: A Survey [11] - CornerNet: Detecting Objects as Paired Keypoints[12] - Objects as Points[13] - Pascal VOC[14] - COCO[15] - Object365[16] - OpenImages
目标追踪 - 监控领域必须了解的算法[转]
原文：多目标跟踪：监控领域你必须要了解的算法 - 2011.11.01英文：The Surveillance phenomenon you must know about : Multi Object Tracking出处：AI 公园 - 微信公众号作者：Deval Shah编译：ronghuaiyang在深入到目标跟踪的世界之前，必须理解监控世界的“为什么”和“是什么”. 全球各国都安装了数百万个摄像头，但几乎没有人监控它们. 人与摄像机的比例很小. 由于深度学习技术在监控领域的广泛应用，智能软件在过去十年中接管了监控领域. 复杂的问题，如人员跟踪，交通密度估计，盗窃保护等，已经被世界各地的杰出研究人员解决了，并显示了巨大的前景1. 目标追踪介绍目标跟踪是指根据之前的信息估计场景中目标物体的状态. 在high level的抽象中，主要有两个层次的物体跟踪.单目标跟踪(SOT)多目标跟踪(MOT)目标跟踪不局限于二维序列数据，可以应用于三维领域. 这里，将深入研究目标跟踪范式的一个子领域，即，使用深度学习的二维视频序列中的多目标跟踪(MOT). 深度神经网络(DNN)的强大之处在于它们能够学习丰富的表示，并从输入中提取复杂和抽象的特征. 多目标跟踪(MOT)，也称为多目标跟踪(MTT)，是一种计算机视觉任务，旨在分析视频，以识别和跟踪属于一个或多个类别的对象，如行人，汽车，动物和无生命的物体，没有任何先验的目标外观和目标数量. 在单目标跟踪中，目标的出现是预先知道的，而在多目标跟踪中，需要一个检测步骤来识别出进入或离开场景的目标. 同时跟踪多个目标的主要困难来自于各种各样的遮挡以及物体之间的相互重叠，有时物体间也会有相似的外观. 因此，仅仅使用SOT模型直接解决MOT问题，往往会导致较差的结果，往往会导致目标漂移和大量的ID切换错误，因为此类模型往往难以区分外观相似的类内物体. 近年来，由于深度学习方法的研究呈指数级增长，检测和跟踪方法在准确性和性能方面取得了巨大的进步. 大多数先进的跟踪方法遵循“检测跟踪”的方案，他们首先在场景中找到对象，然后找到对象对应的tracklets(它在下一帧的位置). 如今，这种探测器的性能异常出色，能够适应不同的场景. 因此，它定义了跟踪算法的标准输入. 还有其他的方法，比如Lucas Kanade的光流，基于排序的跟踪等，它们在传统的计算机视觉方法的基础上表现也都很出色.2. 目标追踪挑战在解决目标跟踪问题时，会出现一些问题，这些问题会导致糟糕的结果. 多年来，算法试图解决这些问题，但到目前为止，还没有找到一个被充分证明的解决办法，这仍然是一个开放的研究领域. [1] - 由于几何变化导致的多样性，如：姿态，关节，物体的尺度[2] - 由于光学因素的差异. 例如：照明、外观[3] - 非线性运动[4] - 有限的分辨率，如从低端手机采集的视频[5] - 场景中有相似的物体，例如：相同颜色的衣服，配饰等[6] - 高度拥挤的场景，如街道，音乐会，体育馆，市场. [7] - 跟踪起始和终止. 在开始任何跟踪算法之前，你需要想要跟踪的物体的先验信息. 但是并不是一定可以使用目标对象初始化算法. [8] - 跟踪轨迹可能由于运动的突然变化导致合并和切换，比如相机成像质量的急剧变化等. [9] - 由于相似的特征，如相似的衣服，面部结构，眼镜，肤色，身高等，目标物体的ID可能被切换. [10] - 目标模型更新错误导致漂移. 一次错误的更新可能会导致在错误的方向上持续的更新，从而在整个视频中遗忘掉正确的目标.3. 目标追踪文章这些只是精心挑选出来的一些非常好的跟踪领域的研究论文：[1] - Tracking without bells and whistles.论文：https://arxiv.org/pdf/1903.05625.pdf代码：https://github.com/phil-bergmann/tracking_wo_bnw[2] - Extending IOU Based Multi-Object Tracking by Visual Information.论文：http://elvera.nue.tu-berlin.de/files/1547Bochinski2018.pdf代码：https://github.com/bochinski/iou-tracker[3] - Tracking Objects as Points.论文：https://arxiv.org/pdf/2004.01177.pdf代码：https://github.com/xingyizhou/CenterTrack[4] - Fast Visual Object Tracking with Rotated Bounding Boxes论文：https://arxiv.org/pdf/1907.03892v5.pdf代码：https://github.com/baoxinchen/siammask_e[5] - ODESA: Object Descriptor that is Smooth Appearance-wise for object tracking tasks还没release，CVPR MOT 2019冠军. [6] - Online Multiple Pedestrian Tracking using Deep Temporal Appearance Matching Association.论文：https://arxiv.org/pdf/1907.00831.pdf代码：暂无
Github - ultralytics/yolov5yolov5/detect.py - https://github.com/ultralytics/yolov5/blob/master/detect.pyYolov5 的检测示例, 如：python detect.py --weights yolov5s.pt --img 416 --conf 0.4 --source inference/images/结果例图：1. 数据加载yolov5 detect.py 支持多种输入，如：images, videos, directories, webcams, rtsp 和 http streams.python detect.py \ --source file.jpg # image file.mp4 # video dir/ # directory 0 # webcam 'rtsp://170.93.143.139/rtplive/470011e600ef003a004ee33696235daa' # rtsp 'http://112.50.243.8/PLTV/88888888/224/3221225900/1.m3u8' # http对应的包含两个数据加载 Dataloader 函数：LoadStreams() 和 LoadImages().1.1. LoadStreamsclass LoadStreams: # multiple IP or RTSP cameras def __init__(self, sources='streams.txt', img_size=640): self.mode = 'images' self.img_size = img_size # 多个 sources 源 if os.path.isfile(sources): with open(sources, 'r') as f: sources = [x.strip() for x in f.read().splitlines() if len(x.strip())] else: sources = [sources] n = len(sources) self.imgs = [None] * n self.sources = sources for i, s in enumerate(sources): # 启动视频流读取线程. print('%g/%g: %s... ' % (i + 1, n, s), end='') cap = cv2.VideoCapture(eval(s) if s.isnumeric() else s) assert cap.isOpened(), 'Failed to open %s' % s w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) fps = cap.get(cv2.CAP_PROP_FPS) % 100 _, self.imgs[i] = cap.read() # guarantee first frame thread = Thread(target=self.update, args=([i, cap]), daemon=True) print(' success (%gx%g at %.2f FPS).' % (w, h, fps)) thread.start() print('') # newline # check for common shapes # 检验所有输入图像的shapes s = np.stack([letterbox(x, new_shape=self.img_size)[0].shape for x in self.imgs], 0) # inference shapes self.rect = np.unique(s, axis=0).shape[0] == 1 # rect inference if all shapes equal if not self.rect: print('WARNING: Different stream shapes detected. For optimal performance supply similarly-shaped streams.') def update(self, index, cap): # 守护线程中读取下一帧 # Read next stream frame in a daemon thread n = 0 while cap.isOpened(): n += 1 # _, self.imgs[index] = cap.read() cap.grab() #抓取视频帧 if n == 4: # read every 4th frame _, self.imgs[index] = cap.retrieve() n = 0 time.sleep(0.01) # wait time def __iter__(self): self.count = -1 return self def __next__(self): self.count += 1 img0 = self.imgs.copy() if cv2.waitKey(1) == ord('q'): # q to quit cv2.destroyAllWindows() raise StopIteration # Letterbox img = [letterbox(x, new_shape=self.img_size, auto=self.rect)[0] for x in img0] # Stack img = np.stack(img, 0) # Convert img = img[:, :, :, ::-1].transpose(0, 3, 1, 2) # BGR to RGB, to bsx3x416x416 img = np.ascontiguousarray(img) return self.sources, img, img0, None def __len__(self): return 0 # 1E12 frames = 32 streams at 30 FPS for 30 years1.2. LoadImages用于模型预测.img_formats = ['.bmp', '.jpg', '.jpeg', '.png', '.tif', '.tiff', '.dng'] vid_formats = ['.mov', '.avi', '.mp4', '.mpg', '.mpeg', '.m4v', '.wmv', '.mkv'] class LoadImages: # for inference def __init__(self, path, img_size=640): p = str(Path(path)) # os-agnostic p = os.path.abspath(p) # absolute path if '*' in p: files = sorted(glob.glob(p, recursive=True)) # glob elif os.path.isdir(p): files = sorted(glob.glob(os.path.join(p, '*.*'))) # dir elif os.path.isfile(p): files = [p] # files else: raise Exception('ERROR: %s does not exist' % p) images = [x for x in files if os.path.splitext(x)[-1].lower() in img_formats] videos = [x for x in files if os.path.splitext(x)[-1].lower() in vid_formats] ni, nv = len(images), len(videos) self.img_size = img_size self.files = images + videos self.nf = ni + nv # number of files self.video_flag = [False] * ni + [True] * nv self.mode = 'images' if any(videos): self.new_video(videos[0]) # new video else: self.cap = None assert self.nf > 0, 'No images or videos found in %s. Supported formats are:\nimages: %s\nvideos: %s' % \ (p, img_formats, vid_formats) def __iter__(self): self.count = 0 return self def __next__(self): if self.count == self.nf: raise StopIteration path = self.files[self.count] if self.video_flag[self.count]: # 视频文件读取 self.mode = 'video' ret_val, img0 = self.cap.read() if not ret_val: self.count += 1 self.cap.release() if self.count == self.nf: # last video raise StopIteration else: path = self.files[self.count] self.new_video(path) ret_val, img0 = self.cap.read() self.frame += 1 print('video %g/%g (%g/%g) %s: ' % (self.count + 1, self.nf, self.frame, self.nframes, path), end='') else: # Read image self.count += 1 img0 = cv2.imread(path) # BGR assert img0 is not None, 'Image Not Found ' + path print('image %g/%g %s: ' % (self.count, self.nf, path), end='') # Padded resize img = letterbox(img0, new_shape=self.img_size)[0] # Convert img = img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, to 3x416x416 img = np.ascontiguousarray(img) # cv2.imwrite(path + '.letterbox.jpg', 255 * img.transpose((1, 2, 0))[:, :, ::-1]) # save letterbox image return path, img, img0, self.cap def new_video(self, path): self.frame = 0 self.cap = cv2.VideoCapture(path) self.nframes = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) def __len__(self): return self.nf # number of files1.3. 辅助函数def load_image(self, index): # loads 1 image from dataset, returns img, original hw, resized hw img = self.imgs[index] if img is None: # not cached path = self.img_files[index] img = cv2.imread(path) # BGR assert img is not None, 'Image Not Found ' + path h0, w0 = img.shape[:2] # orig hw r = self.img_size / max(h0, w0) # resize image to img_size if r != 1: # always resize down, only resize up if training with augmentation interp = cv2.INTER_AREA if r < 1 and not self.augment else cv2.INTER_LINEAR img = cv2.resize(img, (int(w0 * r), int(h0 * r)), interpolation=interp) return img, (h0, w0), img.shape[:2] # img, hw_original, hw_resized else: return self.imgs[index], self.img_hw0[index], self.img_hw[index] # img, hw_original, hw_resized # def letterbox(img, new_shape=(640, 640), color=(114, 114, 114), auto=True, scaleFill=False, scaleup=True): # 将图像尺寸调整为 32像素倍数的矩形. # Resize image to a 32-pixel-multiple rectangle https://github.com/ultralytics/yolov3/issues/232 shape = img.shape[:2] # current shape [height, width] if isinstance(new_shape, int): new_shape = (new_shape, new_shape) # Scale ratio (new / old) r = min(new_shape[0] / shape[0], new_shape[1] / shape[1]) if not scaleup: # 仅缩小，不放大 only scale down, do not scale up (for better test mAP) r = min(r, 1.0) # Compute padding ratio = r, r # width, height ratios new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r)) dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1] # wh padding if auto: # minimum rectangle dw, dh = np.mod(dw, 32), np.mod(dh, 32) # wh padding elif scaleFill: # stretch dw, dh = 0.0, 0.0 new_unpad = (new_shape[1], new_shape[0]) ratio = new_shape[1] / shape[1], new_shape[0] / shape[0] # width, height ratios dw /= 2 # divide padding into 2 sides dh /= 2 if shape[::-1] != new_unpad: # resize img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR) top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1)) left, right = int(round(dw - 0.1)), int(round(dw + 0.1)) img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color) # add border return img, ratio, (dw, dh)2. 模型加载根据给定 weights 权重文件，加载模型：# Load model model = attempt_load(weights, map_location=device) # load FP32 model if half: model.half() # to FP162.1. attempt_load 模型加载函数class Ensemble(nn.ModuleList): # Ensemble of models def __init__(self): super(Ensemble, self).__init__() def forward(self, x, augment=False): y = [] for module in self: y.append(module(x, augment)[0]) # y = torch.stack(y).max(0)[0] # max ensemble # y = torch.cat(y, 1) # nms ensemble y = torch.stack(y).mean(0) # mean ensemble return y, None # inference, train output def attempt_load(weights, map_location=None): # Loads an ensemble of models weights=[a,b,c] or a single model weights=[a] or weights=a model = Ensemble() for w in weights if isinstance(weights, list) else [weights]: # attempt_download(w) # 权重文件下载 model.append(torch.load(w, map_location=map_location)['model'].float().fuse().eval()) # load FP32 model # Compatibility updates for m in model.modules(): if type(m) in [nn.Hardswish, nn.LeakyReLU, nn.ReLU, nn.ReLU6]: m.inplace = True # pytorch 1.7.0 compatibility elif type(m) is Conv: m._non_persistent_buffers_set = set() # pytorch 1.6.0 compatibility if len(model) == 1: return model[-1] # return model else: print('Ensemble created with %s\n' % weights) for k in ['names', 'stride']: setattr(model, k, getattr(model[-1], k)) return model # return ensemble3. 模型预测在数据 Dataloader 和模型加载后，进行模型预测.3.1. 预跑一次预测在对测试数据进行正式预测前，yolov5 还有一些简单的处理，主要为：# 验证输入图像的尺寸 imgsz = check_img_size(imgsz, s=model.stride.max()) # check img_size # 预跑一次预测 img = torch.zeros((1, 3, imgsz, imgsz), device=device) # init img _ = model(img.half() if half else img) if device.type != 'cpu' else None # run once其中，check_img_size 函数实现如：def make_divisible(x, divisor): # Returns x evenly divisible by divisor return math.ceil(x / divisor) * divisor def check_img_size(img_size, s=32): # 验证 img_size 是否为步长 s 的倍数. # Verify img_size is a multiple of stride s new_size = make_divisible(img_size, int(s)) # ceil gs-multiple if new_size != img_size: print('WARNING: --img-size %g must be multiple of max stride %g, updating to %g' % (img_size, s, new_size)) return new_size3.2. 正式预测# dataset = LoadStreams(source, img_size=imgsz) #or: # dataset = LoadImages(source, img_size=imgsz) for path, img, im0s, vid_cap in dataset: img = torch.from_numpy(img).to(device) img = img.half() if half else img.float() # uint8 to fp16/32 img /= 255.0 # 0 - 255 to 0.0 - 1.0 if img.ndimension() == 3: img = img.unsqueeze(0) # Inference pred = model(img, augment=opt.augment)[0] # Apply NMS pred = non_max_suppression(pred, opt.conf_thres, opt.iou_thres, classes=opt.classes, agnostic=opt.agnostic_nms) # Process detections for i, det in enumerate(pred): # detections per image if webcam: # batch_size >= 1 p, s, im0 = path[i], '%g: ' % i, im0s[i].copy() else: p, s, im0 = path, '', im0s gn = torch.tensor(im0.shape)[[1, 0, 1, 0]] # normalization gain whwh if det is not None and len(det): # Rescale boxes from img_size to im0 size det[:, :4] = scale_coords(img.shape[2:], det[:, :4], im0.shape).round() # vis results for *xyxy, conf, cls in reversed(det): label = '%s %.2f' % (names[int(cls)], conf) # Plots one bounding box on image img tl = line_thickness or round(0.002 * (img.shape[0] + img.shape[1]) / 2) + 1 # line/font thickness color = color or [random.randint(0, 255) for _ in range(3)] c1, c2 = (int(x[0]), int(x[1])), (int(x[2]), int(x[3])) cv2.rectangle(img, c1, c2, color, thickness=tl, lineType=cv2.LINE_AA) if label: tf = max(tl - 1, 1) # font thickness t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0] c2 = c1[0] + t_size[0], c1[1] - t_size[1] - 3 cv2.rectangle(img, c1, c2, color, -1, cv2.LINE_AA) # filled cv2.putText(img, label, (c1[0], c1[1] - 2), 0, tl / 3, [225, 255, 255], thickness=tf, lineType=cv2.LINE_AA)3.3. non_max_suppressionyolov5 中的 NMS 处理def non_max_suppression(prediction, conf_thres=0.1, iou_thres=0.6, merge=False, classes=None, agnostic=False): """ Performs Non-Maximum Suppression (NMS) on inference results Returns: detections with shape: nx6 (x1, y1, x2, y2, conf, cls) """ nc = prediction[0].shape[1] - 5 # number of classes xc = prediction[..., 4] > conf_thres # candidates # Settings min_wh, max_wh = 2, 4096 # (pixels) minimum and maximum box width and height max_det = 300 # maximum number of detections per image time_limit = 10.0 # seconds to quit after redundant = True # require redundant detections multi_label = nc > 1 # multiple labels per box (adds 0.5ms/img) t = time.time() output = [None] * prediction.shape[0] for xi, x in enumerate(prediction): # image index, image inference # Apply constraints # x[((x[..., 2:4] < min_wh) | (x[..., 2:4] > max_wh)).any(1), 4] = 0 # width-height x = x[xc[xi]] # confidence # If none remain process next image if not x.shape[0]: continue # Compute conf x[:, 5:] *= x[:, 4:5] # conf = obj_conf * cls_conf # Box (center x, center y, width, height) to (x1, y1, x2, y2) box = xywh2xyxy(x[:, :4]) # Detections matrix nx6 (xyxy, conf, cls) if multi_label: i, j = (x[:, 5:] > conf_thres).nonzero(as_tuple=False).T x = torch.cat((box[i], x[i, j + 5, None], j[:, None].float()), 1) else: # best class only conf, j = x[:, 5:].max(1, keepdim=True) x = torch.cat((box, conf, j.float()), 1)[conf.view(-1) > conf_thres] # Filter by class if classes: x = x[(x[:, 5:6] == torch.tensor(classes, device=x.device)).any(1)] # Apply finite constraint # if not torch.isfinite(x).all(): # x = x[torch.isfinite(x).all(1)] # If none remain process next image n = x.shape[0] # number of boxes if not n: continue # Sort by confidence # x = x[x[:, 4].argsort(descending=True)] # Batched NMS c = x[:, 5:6] * (0 if agnostic else max_wh) # classes boxes, scores = x[:, :4] + c, x[:, 4] # boxes (offset by class), scores i = torch.ops.torchvision.nms(boxes, scores, iou_thres) if i.shape[0] > max_det: # limit detections i = i[:max_det] if merge and (1 < n < 3E3): # Merge NMS (boxes merged using weighted mean) try: # update boxes as boxes(i,4) = weights(i,n) * boxes(n,4) iou = box_iou(boxes[i], boxes) > iou_thres # iou matrix weights = iou * scores[None] # box weights x[i, :4] = torch.mm(weights, x[:, :4]).float() / weights.sum(1, keepdim=True) # merged boxes if redundant: i = i[iou.sum(1) > 1] # require redundancy except: # possible CUDA error https://github.com/ultralytics/yolov3/issues/1139 print(x, i, x.shape, i.shape) pass output[xi] = x[i] if (time.time() - t) > time_limit: break # time limit exceeded return output
原文：一文打尽目标检测NMS——精度提升篇 - 2020.07.01 - 知乎作者：曲終人不散丶众所周知，非极大值抑制NMS是目标检测常用的后处理算法，用于剔除冗余检测框，本文将对可以提升精度的各种NMS方法及其变体进行阶段性总结. 总体概要：对NMS进行分类，大致可分为以下六种，这里是依据它们在各自论文中的核心论点进行分类，这些算法可以同时属于多种类别.分类优先：传统NMS，Soft-NMS (ICCV 2017)定位优先：IoU-Guided NMS (ECCV 2018)加权平均：Weighted NMS (ICME Workshop 2017)方差加权平均：Softer-NMS (CVPR 2019)自适应阈值：Adaptive NMS (CVPR 2019)+中心点距离：DIoU-NMS (AAAI 2020)1. 分类优先1.1. 传统 NMS传统NMS有多个名称，据不完全统计可以被称为：Traditional/Original/Standard/Greedy NMS，为统一起见，下称Traditional NMS. Traditional NMS算法是最为经典的版本，伪代码如下：作为最为人所知晓的版本，Traditional NMS不仅有CPU版本，并且现已有GPU版本，GPU版本为Ross Girshick大神编写，俗称CUDA NMS. 多个深度学习框架也已将CUDA NMS纳入，作为基本函数使用，如Pytorch在Torchvision 0.3中正式集成了CUDA NMS. 缺点：顺序处理的模式，计算IoU拖累了运算效率.剔除机制太严格，依据NMS阈值暴力剔除.阈值是经验选取的.评判标准是IoU，即只考虑两个框的重叠面积，这对描述box重叠关系或许不够全面.1.2. Soft-NMSSoft-NMS是Traditional NMS的推广，主要旨在缓解Traditional NMS的第二条缺点. 数学上看，Traditional NMS的剔除机制可视为：$$ s_i = \begin{cases} 0 & IoU(M, B_i) \geq thresh \\ s_i & IoU(M, B_i) <thresh \end{cases} $$显然，对于 IoU≥NMS阈值的相邻框，Traditional NMS的做法是将其得分暴力置0. 这对于有遮挡的案例较不友好. 因此Soft-NMS的做法是采取得分惩罚机制，使用一个与IoU正相关的惩罚函数对得分 $s$ 进行惩罚. 线性惩罚：$$ s_i = \begin{cases} s_i(1 - IoU(M, B_i)) & DIoU(M, B_i) \geq thresh \\ s_i & DIoU(M, B_i) <thresh \end{cases} $$其中，$M$ 代表当前的最大得分框.线性惩罚有不光滑的地方，因而还有一种高斯惩罚：$$ s_i = s_i e ^{- \frac{IoU(M, B_i)^2}{\sigma}} $$在迭代终止之后，Soft-NMS依据预先设定的得分阈值来保留幸存的检测框，通常设为0.0001.该文对两种惩罚方法的超参数也进行了实验，结果验证了超参数的不敏感性. 经作者实测，Soft-NMS在Faster R-CNN中的提升约有0.5-0.8个点的AP提升. 缺点：仍然是顺序处理的模式，运算效率比Traditional NMS更低.对双阶段算法友好，而在一些单阶段算法上可能失效.如果存在定位与得分不一致的情况，则可能导致定位好而得分低的框比定位差得分高的框惩罚更多(遮挡情况下).评判标准是IoU，即只考虑两个框的重叠面积，这对描述box重叠关系或许不够全面.2. 定位优先IoU-Guided NMS出现于 IoU-Net 一文中，研究者认为框的定位与分类得分可能出现不一致的情况，特别是框的边界有模棱两可的情形时. 因而该文提出了IoU预测分支，来学习定位置信度，进而使用定位置信度来引导NMS. 具体来说，就是使用定位置信度作为NMS的筛选依据，每次迭代挑选出最大定位置信度的框M，然后将IoU≥NMS阈值的相邻框剔除，但把冗余框及其自身的最大分类得分直接赋予M，这样一来，最终输出的框必定是同时具有最大分类得分与最大定位置信度的框. 优点：IoU-Guided NMS有助于提高严格指标下的精度，如AP75, AP90. 缺点：顺序处理的模式，运算效率与Traditional NMS相同.需要额外添加IoU预测分支，造成计算开销.评判标准是IoU，即只考虑两个框的重叠面积，这对描述box重叠关系或许不够全面.3. 加权平均多框共同决定一框Weighted NMS出现于ICME Workshop 2017 Inception Single Shot MultiBox Detector for object detection 一文中. 论文认为Traditional NMS每次迭代所选出的最大得分框未必是精确定位的，冗余框也有可能是定位良好的. 那么与直接剔除机制不同，Weighted NMS顾名思义是对坐标加权平均，加权平均的对象包括M 自身以及IoU≥NMS阈值的相邻框.$$ M = \frac{\sum_i w_i B_i}{\sum_i w_i} $$$$ B_i \in \lbrace B|IoU(M, B) \geq thresh \rbrace \cup \lbrace M \rbrace $$加权的权重为 $w_i = s_i IoU(M, B_i)$，表示得分与 Iou 的乘积.优点：Weighted NMS通常能够获得更高的Precision和Recall，以作者的使用情况来看，只要NMS阈值选取得当，Weighted NMS 均能稳定提高AP与AR，无论是AP50还是AP75，也不论所使用的检测模型是什么. 缺点：顺序处理模式，且运算效率比Traditional NMS更低.加权因子是IoU与得分，前者只考虑两个框的重叠面积，这对描述box重叠关系或许不够全面；而后者受到定位与得分不一致问题的限制.4. 方差加权平均Softer-NMS同样是坐标加权平均的思想，不同在于权重 $w_i$ 发生变化，以及引入了box边界的不确定度. 关于目标检测box不确定度，可参考笔者的另一篇文章《一文了解目标检测边界框概率分布》加权公式如下：在加权平均的过程中，权重越大有两种情形：1. 与 M 的IoU越大；2. 方差越小，代表定位不确定度越低. var voting表示方差加权平均优点：可以与Traditional NMS或Soft-NMS结合使用.通常可以稳定提升AP与AR.缺点：顺序处理模式，且运算效率比Traditional NMS更低.需要修改模型来预测方差.加权因子是IoU与方差，前者依然只考虑两个框的重叠面积，这对描述box重叠关系或许不够全面.5. 自适应阈值以上这些NMS都基于这样的假设：与当前最高得分框重叠越大，越有可能是冗余框. Adaptive NMS的研究者认为这在物体之间有严重遮挡时可能带来不好的结果. 我们期望当物体分布稀疏时，NMS大可选用小阈值以剔除更多冗余框；而在物体分布密集时，NMS选用大阈值，以获得更高的召回. 既然如此，该文提出了密度预测模块，来学习一个框的密度. 一个 GT 框 $B_i$ 的密度标签定义如下：$$ d_i = max_{B_i, Bj \in GT} IoU (B_i, B_j), i \neq j $$模型的输出将变为 (x, y, w, h, s, d)，分别代表box坐标，宽高，分类得分，密度，其中密度 d 越大，代表该框所处的位置的物体分布越密集，越有可能是遮挡严重的地方；反之密度 d 越小，代表该框所处的位置的物体分布越稀疏，不太可能有遮挡. 论文以Traditionnal NMS和Soft-NMS的线性惩罚为基础，将每次迭代的NMS阈值更改如下：$$ N_t = max \lbrace thresh, d_M \rbrace $$其中，$thresh$ 代表最小的 NMS 阈值.优点：可以与前面所述的各种NMS结合使用.对遮挡案例更加友好.缺点：与Soft-NMS结合使用，效果可能倒退 (受低分检测框的影响).顺序处理模式，运算效率低.需要额外添加密度预测模块，造成计算开销.评判标准是IoU，即只考虑两个框的重叠面积，这对描述box重叠关系或许不够全面.6. +中心距离DIoU-NMS出现于Distance-IoU一文，研究者认为若相邻框的中心点越靠近当前最大得分框 M 的中心点，则其更有可能是冗余框. 也就是说，考虑IoU相同的情况，如下所示第一种相比于第三种越不太可能是冗余框. 基于该观点，研究者使用所提出的DIoU替代IoU作为NMS的评判准则，公式如下：$$ s_i = \begin{cases} 0 & DIoU(M, B_i) \geq thresh \\ s_i & DIoU(M, B_i) <thresh \end{cases} $$DIoU 的定义为：$$ DIoU = IoU - \frac{d^2}{c^2} $$而在实际操作中，研究者还引入了参数 $\beta$，用于控制 $\frac{d^2}{c^2}$ 的惩罚程度, 即：$$ DIoU = IoU - (\frac{d^2}{c^2})^{\beta} $$由公式可以看出，当 $\beta \rightarrow \propto$ 时，DIou 退化为 IoU，此时，DIoU-NMS 与Traditional NMS效果相当.当 $\beta \rightarrow 0$ 时，此时，几乎所有中心店不与 M 重合的框被保留了.研究者进一步比较了Traditional NMS和DIoU-NMS的性能，在YOLOv3和SSD上，选取NMS阈值为[0.43,0.48]. 可以看到DIoU-NMS在每个阈值上都优于Traditional NMS，此外还值得一提的是，即便是性能最差的DIoU-NMS也比性能最好的Traditional NMS相当或更优，说明即便不仔细调整NMS阈值，DIoU-NMS也通常能够表现更好. YOLOv3(左)和SSD(右)在VOC 2007 test集这里顺便一提，既然都比了[0.43, 0.48]的阈值，就让人比较好奇更宽的阈值范围会怎样？Traditional NMS会不会有反超DIoU-NMS的情况？当然我个人比较认同DIoU-NMS更优的范围会大一些，也就是NMS阈值不必精调也可放心使用DIoU-NMS. 优点：从几何直观的角度，将中心点考虑进来有助于缓解遮挡案例.可以与前述NMS变体结合使用.保持NMS阈值不变的情况下，必然能够获得更高recall (因为保留的框增多了)，至于precision就需要调 !公式(https://www.zhihu.com/equation?tex=%5Cbeta) 来平衡了.个人认为+中心点距离的后处理可以与DIoU/CIoU损失结合使用，这两个损失一方面优化IoU，一方面指引中心点的学习，而中心点距离学得越好，应该对这种后处理思想的执行越有利.缺点：依然是顺序处理模式，运算效率低.DIoU的计算比IoU更复杂一些，这会降低运算效率.在保持NMS阈值不变的情况下，使用DIoU-NMS会导致每次迭代剩余更多的框，这会增加迭代轮数，进一步降低运算效率. (经本人实测，DIoU-NMS是Traditional NMS 起码1.5倍耗时)7. 总结加权平均法通常能够稳定获得精度与召回的提升.定位优先法，方差加权平均法与自适应阈值法需要修改模型，不够灵活.中心点距离法可作为额外惩罚因子与其他NMS变体结合.得分惩罚法会改变box的得分，打破了模型校准机制.运算效率的低下可能会限制它们的实时应用性.8. 参考文献Soft-NMS --Improving Object Detection With One Line of Code. ICCV 2017Acquisition of Localization Confidence for Accurate Object Detection. ECCV 2018Inception Single Shot MultiBox Detector for object detection. ICME Workshop 2017Bounding Box Regression with Uncertainty for Accurate Object Detection. CVPR 2019Adaptive NMS: Refining Pedestrian Detection in a Crowd. CVPR 2019Distance-IoU Loss: Faster and Better Learning for Bounding Box Regression. AAAI 2020
FocalLoss 用于 one-stage 目标检测算法(Retinanet)，提升检测效果.也可以被用于分类任务中，解决数据不平衡问题.1. Github - DeepLabV3Plus-PytorchDeepLabV3Plus-Pytorch/utils/loss.pyimport torch import torch.nn as nn import torch.nn.functional as F class FocalLoss(nn.Module): def __init__(self, alpha=1, gamma=0, size_average=True, ignore_index=255): super(FocalLoss, self).__init__() self.alpha = alpha self.gamma = gamma self.ignore_index = ignore_index self.size_average = size_average def forward(self, inputs, targets): ce_loss = F.cross_entropy(inputs, targets, reduction='none', ignore_index=self.ignore_index) pt = torch.exp(-ce_loss) focal_loss = self.alpha * (1-pt)**self.gamma * ce_loss if self.size_average: return focal_loss.mean() else: return focal_loss.sum()2. Github - Focal-Loss-PytorchGithub - yatengLG/Focal-Loss-Pytorch基于Pytorch实现Focal loss基于 Pytorch 的实现如：#!-*- coding: utf-8 -*- # @Author : LG from torch import nn import torch from torch.nn import functional as F class focal_loss(nn.Module): def __init__(self, alpha=0.25, gamma=2, num_classes = 3, size_average=True): """ focal_loss损失函数, -α(1-yi)**γ *ce_loss(xi,yi) 步骤详细的实现了 focal_loss损失函数. :param alpha: 阿尔法α,类别权重. 当α是列表时,为各类别权重； 当α为常数时,类别权重为[α, 1-α, 1-α, ....], 常用于目标检测算法中抑制背景类, retainnet中设置为0.25 :param gamma: 伽马γ,难易样本调节参数. retainnet中设置为2 :param num_classes: 类别数量 :param size_average: 损失计算方式,默认取均值 """ super(focal_loss,self).__init__() self.size_average = size_average if isinstance(alpha,list): assert len(alpha)==num_classes # α可以以list方式输入, # size:[num_classes] 用于对不同类别精细地赋予权重 print("Focal_loss alpha = {}, 将对每一类权重进行精细化赋值".format(alpha)) self.alpha = torch.Tensor(alpha) else: assert alpha<1 #如果α为一个常数,则降低第一类的影响,在目标检测中为第一类 print("Focal_loss alpha = {} ,将对背景类进行衰减,请在目标检测任务中使用.".format(alpha)) self.alpha = torch.zeros(num_classes) self.alpha[0] += alpha self.alpha[1:] += (1-alpha) # α 最终为 [ α, 1-α, 1-α, 1-α, 1-α, ...] size:[num_classes] self.gamma = gamma def forward(self, preds, labels): """ focal_loss损失计算 :param preds: 预测类别. size:[B,N,C] or [B,C] 分 别对应与检测与分类任务, B 批次, N检测框数, C类别数 :param labels: 实际类别. size:[B,N] or [B] :return: """ # assert preds.dim()==2 and labels.dim()==1 preds = preds.view(-1,preds.size(-1)) self.alpha = self.alpha.to(preds.device) # 这里并没有直接使用log_softmax, 因为后面会用到softmax的结果(当然也可以使用log_softmax,然后进行exp操作) preds_softmax = F.softmax(preds, dim=1) preds_logsoft = torch.log(preds_softmax) # 这部分实现nll_loss ( crossempty = log_softmax + nll ) preds_softmax = preds_softmax.gather(1,labels.view(-1,1)) preds_logsoft = preds_logsoft.gather(1,labels.view(-1,1)) self.alpha = self.alpha.gather(0,labels.view(-1)) loss = -torch.mul(torch.pow((1-preds_softmax), self.gamma), preds_logsoft) # torch.pow((1-preds_softmax), self.gamma) 为focal loss中 (1-pt)**γ loss = torch.mul(self.alpha, loss.t()) if self.size_average: loss = loss.mean() else: loss = loss.sum() return loss3. 相关[1] - Focal Loss 论文理解及公式推导 - AIUAI[2] - 5分钟理解Focal Loss与GHM——解决样本不平衡利器