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平台均已成功预测。

相关文章
|
2月前
|
人工智能
KOALA图像AI模型发布,低配电脑2秒生图
【2月更文挑战第13天】KOALA图像AI模型发布,低配电脑2秒生图
37 2
KOALA图像AI模型发布,低配电脑2秒生图
|
3月前
|
自然语言处理 安全 API
MT5/MT4外汇跟单交易所系统开发指南教程/海外版/多语言/详细步骤/源码策略
The development of the MT5/MT4 foreign exchange documentary trading system requires consideration of the following detailed functions and intelligence:
|
3月前
|
人工智能 自然语言处理 搜索推荐
阿里云推出企业级大模型RAG系统,几次点击即可连接PB级知识库
阿里云推出企业级大模型RAG系统,几次点击即可连接PB级知识库
757 1
|
6月前
|
存储 人工智能 数据可视化
元象开源650亿参数高性能大模型,无条件免费商用!魔搭最佳实践来了!
为推动国产大模型开源生态繁荣与产业应用快速发展,元象XVERSE公司宣布 开源650亿参数高性能通用大模型XVERSE-65B,无条件免费商用,业界尚属首次。
|
9月前
|
机器学习/深度学习 传感器 数据采集
接入Llama 2等33个大模型,上线Prompt模板,百度智能云千帆大模型平台重磅升级
接入Llama 2等33个大模型,上线Prompt模板,百度智能云千帆大模型平台重磅升级
495 0
|
11月前
|
机器学习/深度学习 人工智能 安全
谷歌大模型云服务上线,代码生成、PaLM for Chat首次亮相
谷歌大模型云服务上线,代码生成、PaLM for Chat首次亮相
121 0
谷歌大模型云服务上线,代码生成、PaLM for Chat首次亮相
|
11月前
|
机器学习/深度学习 人工智能 自然语言处理
LLM系列 | 13: 亲测ChatGPT的重磅功能:函数调用 (以天气问答为例)
本文以天气问答为例,实践方式介绍ChatGPT的函数调用
LLM系列 | 13:  亲测ChatGPT的重磅功能:函数调用 (以天气问答为例)
|
12月前
|
机器学习/深度学习 算法 PyTorch
ncnn+PPYOLOv2首次结合!全网最详细代码解读来了(1)
ncnn+PPYOLOv2首次结合!全网最详细代码解读来了
150 0
|
12月前
|
算法 数据挖掘
ncnn+PPYOLOv2首次结合!全网最详细代码解读来了(2)
ncnn+PPYOLOv2首次结合!全网最详细代码解读来了
|
12月前
|
机器学习/深度学习 人工智能 测试技术
神经引擎这回行了吗?iPhone 14 Core ML性能测评已出
神经引擎这回行了吗?iPhone 14 Core ML性能测评已出
141 0