ncnn+PPYOLOv2首次结合!全网最详细代码解读来了(3)

简介: ncnn+PPYOLOv2首次结合!全网最详细代码解读来了

MatrixNMS

MatrixNMS为实例分割SOLO中提出的nms算法,原版MatrixNMS非常巧妙地通过一个矩阵乘法求掩码两两之间的iou,只需将求掩码两两之间的iou改成求预测框两两之间的iou,即可将MatrixNMS应用于目标检测算法的后处理。MatrixNMS的优点是不用设置nms_iou这个比较敏感的超参数;以及,理论速度比multiclass_nms快,因为它用了矩阵乘法求掩码两两之间的iou,矩阵乘法可用gpu并行高速计算;multiclass_nms对每个类别会选出1个得分最高的预测框(该预测框肯定会保留下来),然后分别与得分比它低的同类预测框计算iou,iou高于nms_iou的将会被舍弃,然后进行第二次迭代,从剩余的预测框里再次选出得分最高的,重复上述过程。multiclass_nms需要进行多次迭代,每一次迭代依赖于上一次迭代,无法做到并行,因为你不能提前预知哪个预测框会被保留。MatrixNMS就没有这种迭代过程,其理论速度要快于multiclass_nms。MatrixNMS采用了「减分」机制,对于每一个类别的每一个预测框,如果和得分比它高的同类预测框有iou(重叠),它的得分会被扣掉一些,之后,通过post_threshold分数阈值过滤掉低分数的预测框,剩下的就是最后的预测框了。「talk is cheap, show me the code」,我们来看一下ncnn中MatrixNMS的代码!

// examples/test2_06_ppyolo_ncnn.cpp
...
struct Bbox
{
    float x0;
    float y0;
    float x1;
    float y1;
    int clsid;
    float score;
};
bool compare_desc(Bbox bbox1, Bbox bbox2)
{
    return bbox1.score > bbox2.score;
}
float calc_iou(Bbox bbox1, Bbox bbox2)
{
    float area_1 = (bbox1.y1 - bbox1.y0) * (bbox1.x1 - bbox1.x0);
    float area_2 = (bbox2.y1 - bbox2.y0) * (bbox2.x1 - bbox2.x0);
    float inter_x0 = std::max(bbox1.x0, bbox2.x0);
    float inter_y0 = std::max(bbox1.y0, bbox2.y0);
    float inter_x1 = std::min(bbox1.x1, bbox2.x1);
    float inter_y1 = std::min(bbox1.y1, bbox2.y1);
    float inter_w = std::max(0.f, inter_x1 - inter_x0);
    float inter_h = std::max(0.f, inter_y1 - inter_y0);
    float inter_area = inter_w * inter_h;
    float union_area = area_1 + area_2 - inter_area + 0.000000001f;
    return inter_area / union_area;
}
...
class PPYOLODecodeMatrixNMS : public ncnn::Layer
{
public:
...
    virtual int forward(const std::vector<ncnn::Mat>& bottom_blobs, std::vector<ncnn::Mat>& top_blobs, const ncnn::Option& opt) const
    {
  ...
        // keep bbox whose score > score_threshold
        std::vector<Bbox> bboxes_vec;
        for (int i = 0; i < out_num; i++)
        {
            float x0 = bboxes[i * 4];
            float y0 = bboxes[i * 4 + 1];
            float x1 = bboxes[i * 4 + 2];
            float y1 = bboxes[i * 4 + 3];
            for (int j = 0; j < num_classes; j++)
            {
                float score = scores[i * num_classes + j];
                if (score > score_threshold)
                {
                    Bbox bbox;
                    bbox.x0 = x0;
                    bbox.y0 = y0;
                    bbox.x1 = x1;
                    bbox.y1 = y1;
                    bbox.clsid = j;
                    bbox.score = score;
                    bboxes_vec.push_back(bbox);
                }
            }
        }
        if (bboxes_vec.size() == 0)
        {
            ncnn::Mat& pred = top_blobs[0];
            pred.create(0, 0, elemsize, opt.blob_allocator);
            if (pred.empty())
                return -100;
            return 0;
        }
        // sort and keep top nms_top_k
        int nms_top_k_ = nms_top_k;
        if (bboxes_vec.size() < nms_top_k)
            nms_top_k_ = bboxes_vec.size();
        size_t count {(size_t)nms_top_k_};
        std::partial_sort(std::begin(bboxes_vec), std::begin(bboxes_vec) + count, std::end(bboxes_vec), compare_desc);
        if (bboxes_vec.size() > nms_top_k)
            bboxes_vec.resize(nms_top_k);
        // ---------------------- Matrix NMS ----------------------
        // calc a iou matrix whose shape is [n, n], n is bboxes_vec.size()
        int n = bboxes_vec.size();
        float* decay_iou = new float[n * n];
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < n; j++)
            {
                if (j < i + 1)
                {
                    decay_iou[i * n + j] = 0.f;
                }else
                {
                    bool same_clsid = bboxes_vec[i].clsid == bboxes_vec[j].clsid;
                    if (same_clsid)
                    {
                        float iou = calc_iou(bboxes_vec[i], bboxes_vec[j]);
                        decay_iou[i * n + j] = iou;
                    }else
                    {
                        decay_iou[i * n + j] = 0.f;
                    }
                }
            }
        }
        // get max iou of each col
        float* compensate_iou = new float[n];
        for (int i = 0; i < n; i++)
        {
            float max_iou = decay_iou[i];
            for (int j = 0; j < n; j++)
            {
                if (decay_iou[j * n + i] > max_iou)
                    max_iou = decay_iou[j * n + i];
            }
            compensate_iou[i] = max_iou;
        }
        float* decay_matrix = new float[n * n];
        // get min decay_value of each col
        float* decay_coefficient = new float[n];
        if (kernel == 0) // gaussian
        {
            for (int i = 0; i < n; i++)
            {
                for (int j = 0; j < n; j++)
                {
                    decay_matrix[i * n + j] = static_cast<float>(expf(gaussian_sigma * (compensate_iou[i] * compensate_iou[i] - decay_iou[i * n + j] * decay_iou[i * n + j])));
                }
            }
        }else if (kernel == 1) // linear
        {
            for (int i = 0; i < n; i++)
            {
                for (int j = 0; j < n; j++)
                {
                    decay_matrix[i * n + j] = (1.f  - decay_iou[i * n + j]) / (1.f  - compensate_iou[i]);
                }
            }
        }
        for (int i = 0; i < n; i++)
        {
            float min_v = decay_matrix[i];
            for (int j = 0; j < n; j++)
            {
                if (decay_matrix[j * n + i] < min_v)
                    min_v = decay_matrix[j * n + i];
            }
            decay_coefficient[i] = min_v;
        }
        for (int i = 0; i < n; i++)
        {
            bboxes_vec[i].score *= decay_coefficient[i];
        }
        // ---------------------- Matrix NMS (end) ----------------------
        std::vector<Bbox> bboxes_vec_keep;
        for (int i = 0; i < n; i++)
        {
            if (bboxes_vec[i].score > post_threshold)
            {
                bboxes_vec_keep.push_back(bboxes_vec[i]);
            }
        }
        n = bboxes_vec_keep.size();
        if (n == 0)
        {
            ncnn::Mat& pred = top_blobs[0];
            pred.create(0, 0, elemsize, opt.blob_allocator);
            if (pred.empty())
                return -100;
            return 0;
        }
        // sort and keep keep_top_k
        int keep_top_k_ = keep_top_k;
        if (n < keep_top_k)
            keep_top_k_ = n;
        size_t keep_count {(size_t)keep_top_k_};
        std::partial_sort(std::begin(bboxes_vec_keep), std::begin(bboxes_vec_keep) + keep_count, std::end(bboxes_vec_keep), compare_desc);
        if (bboxes_vec_keep.size() > keep_top_k)
            bboxes_vec_keep.resize(keep_top_k);
        ncnn::Mat& pred = top_blobs[0];
        pred.create(6 * n, elemsize, opt.blob_allocator);
        if (pred.empty())
            return -100;
        float* pred_ptr = pred;
        for (int i = 0; i < n; i++)
        {
            pred_ptr[i * 6] = (float)bboxes_vec_keep[i].clsid;
            pred_ptr[i * 6 + 1] = bboxes_vec_keep[i].score;
            pred_ptr[i * 6 + 2] = bboxes_vec_keep[i].x0;
            pred_ptr[i * 6 + 3] = bboxes_vec_keep[i].y0;
            pred_ptr[i * 6 + 4] = bboxes_vec_keep[i].x1;
            pred_ptr[i * 6 + 5] = bboxes_vec_keep[i].y1;
        }
        pred = pred.reshape(6, n);
        return 0;
    }


...

第一步,将得分超过score_threshold的预测框保存到bboxes_vec里,这是第一次分数过滤;如果没有预测框的得分超过score_threshold,直接返回1个形状是(0, 0)的Mat代表没有物体。

第二步,将bboxes_vec中的前nms_top_k个预测框按照得分降序排列,bboxes_vec中只保留前nms_top_k个预测框。

第三步,进入MatrixNMS,设此时bboxes_vec里有n个预测框,我们计算一个n * n的矩阵decay_iou,下三角部分(包括对角线)是0,表示的是bboxes_vec中的预测框两两之间的iou,而且,只计算同类别预测框的iou,非同类的预测框iou置为0;

接下来的代码比较难以理解,我举个例子说明,比如经过第一次分数过滤和得分降序排列后,剩下编号为0、1、2的3个同类的预测框,假设此时的decay_iou值为:

如果某个预测框与比它分高的同类预测框有较高的iou,它应该减去更多的分,这该怎么实现呢?一个比较简单的做法是对矩阵1-decay_iou每一列求最小值,即对矩阵:

每一列求最小,得到衰减系数向量decay_coefficient=[1, 0.1, 0.2],然后每个bbox的得分再和衰减系数向量里相应的值相乘,就实现减分的效果了!

比如0号预测框,它的得分应该乘以1,这很好理解,它是得分最高的预测框,应该被保留,不应该减分。对于1号预测框,它的得分应该乘以0.1,这很好理解,它与0号预测框的iou高达0.9,应该减去很多分。对于2号预测框,它的得分应该乘以0.2,这很好理解,它与1号预测框的iou高达0.8,应该减去很多分。但是这样做真的正确吗?如果用multiclass_nms做nms算法,假设设定的nms_iou=0.6,第0次迭代,首先保留得分最高的0号预测框,发现1号预测框和0号预测框的iou高达0.9,所以舍弃1号预测框,发现2号预测框和0号预测框的iou是0.2,保留2号预测框;第1次迭代,首先保留得分最高的2号预测框,发现没有预测框了,nms算法结束。所以最后保留的是0号预测框和2号预测框。上面的分析中,仅仅是因为2号预测框与1号预测框的iou高达0.8,就让2号预测框的分数乘以0.2,是非常不正确的做法,因为1号预测框与0号预测框的iou高达0.9,1号预测框有很大概率是会被舍弃的,不能因为2号预测框与可能被舍弃的1号预测框的iou高达0.8,就让2号预测框减去很多分。那么怎么解决这个问题呢?补偿!1-0.8没有什么参考意义,我们应该将它放大,可以让它除以(1-0.9)实现,0.9表示1号预测框与0号预测框的iou高达0.9,这样逐列取最小的时候就可能取不到它了。而且,不应该只有1号预测框与2号预测框这么做,预测框两两之间都应该这么做。我们看接下来的代码,逐列取decay_iou的最大值得到补偿向量compensate_iou,在这个示例中compensate_iou=[0, 0.9, 0.8],然后求一个n * n的矩阵decay_matrix,当kernel == 1时,是linear,它的计算公式是(1-decay_iou)矩阵的每一行元素都除以(1-compensate_iou的第i个值)(假设当前行id是i),所以在这个示例中,decay_matrix的值是:

逐列取decay_matrix的最小值,即可得到decay_coefficient=[1, 0.1, 0.8],你看,2号预测框的得分应该乘以0.8,是由于它和0号预测框的iou是0.2导致的,它减去的分数就比较少。而此时1号预测框和2号预测框在decay_matrix中的值被补偿(被放大)到2,参考意义不大,逐列取最小时取不到它。

现在你应该能更好地理解代码中decay_matrix的计算公式了吗?

decay_matrix[i * n + j] = (1.f  - decay_iou[i * n + j]) / (1.f  - compensate_iou[i]);

第i个预测框和第j个预测框的iou是decay_iou[i * n + j],第i个预测框它觉得第j个预测框的衰减系数应该是(1.f - decay_iou[i * n + j]),但是第i个预测框它觉得的就是对的吗?

还要看第i个预测框是否被抑制,第i个预测框如果没有被抑制,那么(1.f - decay_iou[i * n + j])就有参考意义,第i个预测框如果被抑制,那么(1.f - decay_iou[i * n + j])就没有什么参考意义。

所以需要除以(1.f - compensate_iou[i])作为补偿,compensate_iou[i]表示的是第i个预测框与比它分高的预测框的最高iou:

如果这个max_iou很大,衰减系数就会被放大,第i个预测框它觉得第j个预测框的衰减系数是xxx就没什么参考意义;如果这个max_iou很小,衰减系数就会放大得很小(max_iou==0时不放大),第i个预测框它觉得第j个预测框的衰减系数是xxx就有参考意义。

然后,逐列取decay_matrix的最小值,第j列的最小值应该是decay_iou[i * n + j]越大越好、compensate_iou[i]越小越好的那个第i个预测框提供。

当kernel == 0,也仅仅表示用其它的函数表示衰减系数和补偿而已。所有的预测框的得分乘以decay_coefficient相应的值实现减分,MatrixNMS结束。

第四步,将得分超过post_threshold的预测框保存到bboxes_vec_keep里,这是第二次分数过滤;如果没有预测框的得分超过post_threshold,直接返回1个形状是(0, 0)的Mat代表没有物体。

第五步,将bboxes_vec_keep中的前keep_top_k个预测框按照得分降序排列,bboxes_vec_keep中只保留前keep_top_k个预测框。

最后,写1个形状是(n, 6)的Mat表示最终所有的预测框后处理结束。

如何导出


(1)第一步,在miemiedetection根目录下输入这些命令下载paddle模型:

wget https://paddledet.bj.bcebos.com/models/ppyolo_r50vd_dcn_2x_coco.pdparams
wget https://paddledet.bj.bcebos.com/models/ppyolo_r18vd_coco.pdparams
wget https://paddledet.bj.bcebos.com/models/ppyolov2_r50vd_dcn_365e_coco.pdparams
wget https://paddledet.bj.bcebos.com/models/ppyolov2_r101vd_dcn_365e_coco.pdparams

(2)第二步,在miemiedetection根目录下输入这些命令将paddle模型转pytorch模型:

python tools/convert_weights.py -f exps/ppyolo/ppyolo_r50vd_2x.py -c ppyolo_r50vd_dcn_2x_coco.pdparams -oc ppyolo_r50vd_2x.pth -nc 80
python tools/convert_weights.py -f exps/ppyolo/ppyolo_r18vd.py -c ppyolo_r18vd_coco.pdparams -oc ppyolo_r18vd.pth -nc 80
python tools/convert_weights.py -f exps/ppyolo/ppyolov2_r50vd_365e.py -c ppyolov2_r50vd_dcn_365e_coco.pdparams -oc ppyolov2_r50vd_365e.pth -nc 80
python tools/convert_weights.py -f exps/ppyolo/ppyolov2_r101vd_365e.py -c ppyolov2_r101vd_dcn_365e_coco.pdparams -oc ppyolov2_r101vd_365e.pth -nc 80

(3)第三步,在miemiedetection根目录下输入这些命令将pytorch模型转ncnn模型:

python tools/demo.py ncnn -f exps/ppyolo/ppyolo_r18vd.py -c ppyolo_r18vd.pth --ncnn_output_path ppyolo_r18vd --conf 0.15
python tools/demo.py ncnn -f exps/ppyolo/ppyolo_r50vd_2x.py -c ppyolo_r50vd_2x.pth --ncnn_output_path ppyolo_r50vd_2x --conf 0.15
python tools/demo.py ncnn -f exps/ppyolo/ppyolov2_r50vd_365e.py -c ppyolov2_r50vd_365e.pth --ncnn_output_path ppyolov2_r50vd_365e --conf 0.15
python tools/demo.py ncnn -f exps/ppyolo/ppyolov2_r101vd_365e.py -c ppyolov2_r101vd_365e.pth --ncnn_output_path ppyolov2_r101vd_365e --conf 0.15

-c代表读取的权重,--ncnn_output_path表示的是保存为NCNN所用的 *.param 和 *.bin 文件的文件名,--conf 0.15表示的是在PPYOLODecodeMatrixNMS层中将score_threshold和post_threshold设置为0.15,你可以在导出的 *.param 中修改score_threshold和post_threshold,分别是PPYOLODecodeMatrixNMS层的5=xxx 7=xxx属性。

然后,下载ncnn_ppyolov2 这个仓库(它自带了glslang和实现了ppyolov2推理),按照官方how-to-build 文档进行编译ncnn。

编译完成后, 将上文得到的ppyolov2_r50vd_365e.param、ppyolov2_r50vd_365e.bin、...这些文件复制到ncnn_ppyolov2的build/examples/目录下,最后在ncnn_ppyolov2根目录下运行以下命令进行ppyolov2的预测:

cd build/examples

./test2_06_ppyolo_ncnn ../../my_tests/000000013659.jpg ppyolo_r18vd.param ppyolo_r18vd.bin 416

./test2_06_ppyolo_ncnn ../../my_tests/000000013659.jpg ppyolo_r50vd_2x.param ppyolo_r50vd_2x.bin 608

./test2_06_ppyolo_ncnn ../../my_tests/000000013659.jpg ppyolov2_r50vd_365e.param ppyolov2_r50vd_365e.bin 640

./test2_06_ppyolo_ncnn ../../my_tests/000000013659.jpg ppyolov2_r101vd_365e.param ppyolov2_r101vd_365e.bin 640

每条命令最后1个参数416、608、640表示的是将图片resize到416、608、640进行推理,即target_size参数。会弹出一个这样的窗口展示预测结果:

test2_06_ppyolo_ncnn的源码位于ncnn_ppyolov2仓库的examples/test2_06_ppyolo_ncnn.cpp。PPYOLOv2和PPYOLO算法目前在Linux和Windows平台均已成功预测。

相关文章
|
Oracle Java 关系型数据库
Oracle jdk 的国内下载镜像
Oracle jdk 的国内下载镜像
56655 0
|
Arthas Java 测试技术
超好用的自带火焰图的 Java 性能分析工具 Async-profiler 了解一下
超好用的自带火焰图的 Java 性能分析工具 Async-profiler 了解一下
3097 0
超好用的自带火焰图的 Java 性能分析工具 Async-profiler 了解一下
|
10月前
|
人工智能 物联网 测试技术
FireRedASR:精准识别普通话、方言和歌曲歌词!小红书开源工业级自动语音识别模型
小红书开源的工业级自动语音识别模型,支持普通话、中文方言和英语,采用 Encoder-Adapter-LLM 和 AED 架构,实现 SOTA 性能。
3375 17
FireRedASR:精准识别普通话、方言和歌曲歌词!小红书开源工业级自动语音识别模型
|
Ubuntu 开发工具 C++
Ubuntu 22.04上编译安装c++ libconfig library
通过本文的介绍,我们详细讲解了如何在Ubuntu 22.04上编译和安装libconfig库,并通过编写和运行一个简单的测试程序来验证安装是否成功。libconfig库的安装过程相对简单,主要包括环境准备、下载源码、编译和安装几个步骤。希望本文对您在项目中使用libconfig库有所帮助。
687 13
|
人工智能 弹性计算 架构师
如何推进软硬件协同优化,点亮 AI 新时代?看看这些大咖怎么说
围绕 AI、操作系统、 Arm 生态等关键技术和领域,深入探讨了 AI 技术与操作系统的融合。
|
机器学习/深度学习 算法 物联网
大模型进阶微调篇(一):以定制化3B模型为例,各种微调方法对比-选LoRA还是PPO,所需显存内存资源为多少?
本文介绍了两种大模型微调方法——LoRA(低秩适应)和PPO(近端策略优化)。LoRA通过引入低秩矩阵微调部分权重,适合资源受限环境,具有资源节省和训练速度快的优势,适用于监督学习和简单交互场景。PPO基于策略优化,适合需要用户交互反馈的场景,能够适应复杂反馈并动态调整策略,适用于强化学习和复杂用户交互。文章还对比了两者的资源消耗和适用数据规模,帮助读者根据具体需求选择最合适的微调策略。
4000 5
|
存储 弹性计算 固态存储
阿里云服务器租用价格参考,云服务器最新活动价格与收费标准分享
2023年阿里云服务器租用费用更新了,阿里云轻量应用服务器2核2G3M带宽轻量服务器一年87元,2核8G4M带宽轻量服务器一年165元;云服务器经济型e实例2核8G3M配置99元1年;除此之外,通用算力型u1实例、计算型c7、通用型g7、计算型c8a与c8i、通用型g8a与g8i均有优惠活动,小编整理了一份2023阿里云服务器最新版收费标准与优惠价格,以供参考。
1885 0
阿里云服务器租用价格参考,云服务器最新活动价格与收费标准分享
|
前端开发 JavaScript UED
探索现代Web开发中的响应式设计原则与实践
【10月更文挑战第9天】在移动互联网的浪潮中,响应式设计已成为Web开发的必备技能。本文旨在深入解析响应式设计的核心原则,并结合实战案例,展示如何运用这些原则构建灵活、高效的Web应用界面。文章不仅涵盖理论探讨,更提供具体代码示例,帮助读者从概念到实现全面掌握响应式设计。
176 0