11.功能模块
3.1 推理结果分析
3.1.1 解码预测结果
模型解码预测结果是指将神经网络模型输出的预测结果进行解析和转换,以得到最终的预测结果。
具体来说,模型解码预测结果包括以下步骤:
- 获取预测结果:神经网络模型在推理过程中会输出预测结果,这些结果通常以概率或得分的形式表示。
- 解码分类预测:对于分类任务,神经网络模型会输出每个类别的概率。我们需要将这些概率解码为具体的类别标签。通常,解码的方法是将概率最高的类别作为预测结果。
- 解码位置信息:对于回归任务,神经网络模型会输出预测的位置信息,如边界框的坐标、尺寸等。我们需要将这些位置信息解码为具体的数值。
下面我们以基于锚框的yolo算法为例,我们来看其模型的输入和输出内容。
模型的输入节点,接收的tensor为[1,3,640,640]表示[batchsize,channel,height,weight],即批次大小为1,也就是一次读1张图像;channel为3表示通道数,3通道彩色图像;高度和宽度则是单张图像的大小320*320像素。
模型的输出节点,输出的tensor为[1,25200,85],其中1表示批次大小为1,每次输出一张图像的数据;25200表示有25k的锚框数量;85表示数据集有80个类/4个框坐标/1个类别置信度。我们来看这个85中的数据集有80个类/4个框坐标/1个类别置信度。由于yolov5使用的coco数据集,其中的图片的类别数是80个类。4个框坐标表示:中心坐标x,中心坐标y,宽度w,高度y。1个类别置信度表示:所有锚框对应的类别的检测概率值。
3.1.2 非极大值抑制
非极大值抑制(NMS)是一种在目标检测中常用的后处理技术。它的主要目的是去除冗余的检测框,保留最优的检测结果。
在目标检测中,通常会使用滑动窗口或类似的方法来生成候选框,然后对这些候选框进行分类和定位精度的评估。但是,由于这种方法会产生大量的候选框,其中很多可能是重叠的,因此需要进行后处理来去除冗余和低质量的候选框,以得到最终的目标检测结果。
NMS的基本思想是保留分类得分最高的候选框,并将其与周围候选框进行比较,去除与它重叠面积超过阈值的候选框。这个过程不断重复,直到所有候选框都被处理完。NMS可以 有效地去除冗余和低质量的候选框,提高目标检测的准确率和效率。它通常被应用于目标检测的最后一步,用于生成最终的目标检测结果。
在NMS中,需要设定一个阈值来决定哪些候选框需要被去除。这个阈值的选择需要根据具体的应用场景和数据集来确定。如果阈值设置得过高,可能会导致漏检;如果阈值设置得过低,可能会导致误检。因此,选择合适的阈值是NMS的关键。
非极大值抑制NMS是一种在目标检测中常用的后处理技术,可以有效地去除冗余和低质量的候选框,提高目标检测的准确率和效率。常见的NMS算法有:最基本的贪心式NMS算法,改进的NMS算法,有Soft-NMS1,Matrix NMS2,Adaptive NMS3,DIoU-NMS4,IoU-guided NMS5等。
在开始学习NMS之前我们需要了解并集交叉点(Intersection Over Union) 或简称 IOU。通常用于量化真实值 BBox(边界框)和预测 BBox 之间重叠百分比的方法。由于目前很多目标检测算法都是基于锚框来进行预测的,也就是将图像分成多个网格,可以是密集的网格或者在感兴趣区域内的规则网格。每个网格的中心点是放置锚框的位置。锚框就是我们预先定义好的框,我们就可以将图像划分为一个个网格,基于每个网格放锚框,进行预测。
现在我们就需要考虑IOU的问题就是考虑真实框和预测框重叠的百分比,其数学公式为:
其中交集与并集的区域可如下图所示:
真实框和预测框的交集区域是蓝色部分,并集区域是紫色部分,IOU的值可以通过将蓝色区域除以紫色区域得到。IOU就可以衡量真实框和预测框之间的相似性。它的值域再[0,1],其中0表示没有重叠,1表示全部重叠。
下面我们使用C++来实现一个简单IOU的计算,我们计算两个Box的交并比,假设A框的左下角的坐标为(10,10),右上角坐标为(100,100)。B框的左下角的坐标为(30,30), 右上角坐标为(150,150)。那么就可以计算出对应交集框的坐标值。A框和B框在坐标轴下的表示,如下图所示:
下面展示C++程序源码:
#include <iostream>
#include <algorithm>
using namespace std;
// 定义一个矩形框的结构体,包含左上角和右下角的坐标,以及置信度
struct Bbox {
int x1;
int y1;
int x2;
int y2;
float score;
};
// 计算两个矩形框的IOU
float iou(const Bbox& a, const Bbox& b) {
// 计算两个矩形框的交集的左上角和右下角的坐标
int inter_x1 = max(a.x1, b.x1);
int inter_y1 = max(a.y1, b.y1);
int inter_x2 = min(a.x2, b.x2);
int inter_y2 = min(a.y2, b.y2);
// 判断是否有交集,如果没有,返回0
if (inter_x1 >= inter_x2 || inter_y1 >= inter_y2) {
return 0;
}
// 计算交集的面积
int inter_area = (inter_x2 - inter_x1) * (inter_y2 - inter_y1);
// 计算两个矩形框的面积
int area_a = (a.x2 - a.x1) * (a.y2 - a.y1);
int area_b = (b.x2 - b.x1) * (b.y2 - b.y1);
// 计算并集的面积
int union_area = area_a + area_b - inter_area;
// 计算IOU
return inter_area * 1.0 / union_area;
}
//主函数
int main() {
// 创建两个矩形框
Bbox a = {10, 10, 100, 100, 0.9};
Bbox b = {30, 30, 150, 150, 0.8};
// 计算IOU
float iou_value = iou(a, b);
// 输出结果
cout << "IOU = " << iou_value << endl;
return 0;
}
将矩阵A和矩阵B的参数传入iou函数中计算交并比,在iouh函数中分别计算交集的面积和并集的面积,最后计算IOU的值并返回。
下面我们使用YOLOV5目标检测算法中的开源NMS示例来讲解NMS。前面我们学习了IOU交并比,nms就是在其基础上,按照预测的置信度分数对矩形框进行排序,然后依次选择最高置信度的矩形框,并删除与它重叠度超过一定阈值的其他矩形框。这样可以有效地减少重复或冗余的预测框,提高检测的效率和精度。我们需要定义提前好nms阈值,用于判断是否要删除一个矩形框的标准,一般在0.3到0.5之间。如果一个矩形框与已经选择的最高置信度的矩形框的iou大于阈值,那么它就被认为是多余的,需要被删除。
下面展示nms核心代码:
#include "nms.hpp"
#include <algorithm>
float iou(const std::vector<float>& boxA, const std::vector<float>& boxB)
{
// The format of box is [top_left_x, top_left_y, bottom_right_x, bottom_right_y]
const float eps = 1e-6;
float iou = 0.f;
float areaA = (boxA[2] - boxA[0]) * (boxA[3] - boxA[1]);
float areaB = (boxB[2] - boxB[0]) * (boxB[3] - boxB[1]);
float x1 = std::max(boxA[0], boxB[0]);
float y1 = std::max(boxA[1], boxB[1]);
float x2 = std::min(boxA[2], boxB[2]);
float y2 = std::min(boxA[3], boxB[3]);
float w = std::max(0.f, x2 - x1);
float h = std::max(0.f, y2 - y1);
float inter = w * h;
iou = inter / (areaA + areaB - inter + eps);
return iou;
}
void nms(std::vector<std::vector<float>>& boxes, const float iou_threshold)
{
// The format of boxes is [[top_left_x, top_left_y, bottom_right_x, bottom_right_y, score, class_id], ...]
//“分数+class_id”排序是为了确保将具有相同class_id的框分组在一起,并按分数排序
std::sort(boxes.begin(), boxes.end(), [](const std::vector<float>& boxA, const std::vector<float>& boxB) { return boxA[4] + boxA[5] > boxB[4] + boxB[5];});
for (int i = 0; i < boxes.size(); ++i)
{
if (boxes[i][4] == 0.f)
{
continue;
}
for (int j = i + 1; j < boxes.size(); ++j)
{
if (boxes[i][5] != boxes[j][5])
{
break;
}
if (iou(boxes[i], boxes[j]) > iou_threshold)
{
boxes[j][4] = 0.f;
}
}
}
std::erase_if(boxes, [](const std::vector<float>& box) { return box[4] == 0.f; });
}