c_cpp DeepLabv3 +系列分割算法总结

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了c_cpp DeepLabv3 +系列分割算法总结相关的知识,希望对你有一定的参考价值。

[TOC]

# 分割的含义
## 语义分割和实例分割
`mIOU_category`,`mIOU_class`:
- 两者的度量指标是不同的.
- 前者是大的类别,而后者需要把每个目标都分割出来。
- 通常前者大于后者

# 车道线分割预研
## VPGNet
- 框架:Caffe
- 显存:813
- 前向时间:15-18ms
- 网络较浅,多任务,拓展性较差
- 使用8x8网格对标签像素进行扩充,防止下采样时特征消失

## SCNN
- 框架:Torch
- backbone:resnet101
- 显存:2605
- 前向时间:66ms
- 解决车道线断裂问题,主要提出空间卷积spatial CNN的想法

## MaskRCNN
- 框架:Pytorch  --  e2e_mask_rcnn_R-101-FPN_2x.yaml
- 显存:1200-1400
- 前向时间:241ms
- 先检测后分割,两条分支一条进行RPN一条进行分割

## DeepLabv3+
- 框架:TensorFlow  --  xception-65 mobilenetv2  resnet101
- 显存:513 771 1026输入分别对应1865,2889,5446(不准确,由于TensorFlow总是尽可能多申请显存)
- 前向时间:110ms 200ms 321ms   另外mobilenetv2对应显存836Mb前向时间146ms(可接受拓展性强)
`空间平移不变性,一个 kernel 就需要同时学习 spatial correlations 和 cross-channel correlations,spatial correlations 学习的是某个特征在空间中的分布,cross-channel correlations 学习的是这些不同特征的组合方式。 如果在 1x1 卷积后不加以激活直接进行 depthwise separable convolution,无论是在收敛速度还是效果上都优于在 1x1 卷积后加以 ReLU 之类激活函数的做法。
这可能是因为,在对很浅的 feature(比如这里的 1-channel feature)进行激活会导致一定的信息损失,而对很深的 feature,比如 Inception module 提取出来的特征,进行激活是有益于特征的学习的,个人理解是这一部分特征中有大量冗余信息。ASPP 方法的优点是该种结构可以提取比较 dense 的特征,因为参考了不同尺度的 feature,并且 atrous convolution 的使用加强了提取 dense 特征的能力。 
decoder 中就可以起到修复尖锐物体边界的作用.`

# 车道线方案
## 经验总结
`前期数据有限,直接测试训练集,最后训练集和测试集分别达到94%,61%(标注和泛化原因)`
1. 加入vpgnet标签扩充(8*8网格化)

- 直接从30%多涨到76%,主要原因是因为车道线太窄,下采样后没有了特征 

2. 将mobilenet_v2最后output_stride设置为16的方式改为直接对网络结构第13个depthwise conv结构处的stride从2改为1(原始是在内部通过dilation rate从1设置为2,stride从2设置为1来达到不下采样的目的,这样引入了SpaceToBatchND和BatchToSpaceND来实现dilation conv)

- 两点原因:1.dilate conv不能int8量化 2.有一定的涨点到80%(可能原因,车道线并不需要特大的感受野)

3. 去掉aspp和decoder 

- 降低2%,aspp有dilate conv不能量化。
- ~~decoder结构是前期对网络框架不够了解,实际上这个结构对边缘信息的提取至关重要,后来重新加回来,涨点到94%,不需要扩充边标签~~

4. 去掉image_pool

- 去掉对精度无影响。主要是把image下采样到1*1再上采样,与feature map相减,相当于减均值平均分布

5. ~~添加水平方向BiLSTM隐藏层大小为32,输出为2x32,LSTM太占资源。其中feature_w为时间步,隐藏层为feature_h x 32,(对的,在车牌中h为1了,但是这里为49所以乘以32,已经很大的HiddenSize了),但是效果不佳,最高miou_1.0只到了58.5%~~

- 想引入Spatial CNN解决,车道线不连续问题,失败。

6. 反复finetune涨点到88%

7. 后来又将decoder结构加回去,低层分支特征对边缘比较敏感,直接从88%提到94%,而且不扩充标签,效果也很精细。

# 总结
最后对分割网络结果进行多级分块和ransac拟合


1. 之前mobilenetv2为主干

- fp32	前向23.89ms	显存:529	origin 159 layers
- int8	前向19.58ms	显存:477	after reformat 195 layers
- TensorRT出现太多转换数据的层reformer layer

2.resnet101主干,精度更高些2%点的样子
- 测试总耗时:25ms(前向时间:19.6694ms+分块拟合2-3ms)





1.逐行扫描图像,把每一行连续的非零目标像素组成一个序列称为一个块(block),并加下它的起点start、它的终点end以及它所在的行号。
void fillBlockVectors(const Mat& img,Lanes& lanes){
    for(int i=0; i < img.rows; ++i){
        const uchar* rowData = img.ptr<uchar>(i);
        if(rowData[0] != 0){
            lanes.NumOfBlocks++;
            lanes.stBlock.push_back(0);
            lanes.rowBlock.push_back(i);
            lanes.lane_type.push_back(rowData[0]);
        }
        for(int j=1; j < img.cols; ++j){
            if(rowData[j-1] == 0 && rowData[j] != 0){// new block
                lanes.NumOfBlocks++;
                lanes.stBlock.push_back(j);
                lanes.rowBlock.push_back(i);
                lanes.lane_type.push_back(rowData[j]);
            }else if(rowData[j-1] != 0 && rowData[j] == 0){//end block:prev node != 0, cur node == 0
                lanes.enBlock.push_back(j-1);
            }
        }
        if(rowData[img.cols-1]){
            lanes.enBlock.push_back(img.cols-1);
        }//last col
    }
}
团是比小块更小的区域,小块为最初每行填入的块,大块为重叠区域合并过的块。
2.对于除了第一行(第一次出现块的那一行称为第一行)外的所有行里的小块,如果他与前一行的所有小块都没有重合区域,则给该小块一个新的标号blockLabels,标签行的同时记下该新大块的左上角坐标,以及线的类别,然后新大块与四周近距离大块的标签计入等价对equivalences用于合并;如果它仅与上一行中的一个块有重合,则将上一行的那个块的标签赋给它,同时记录该块的右下角坐标;如果它与上一行的2个以上的块有重叠区域,则给当前块赋一个相连块的最小标号,并将上一行的这几个块写入等价对,说明它们属于一类块。
void firstPass(Lanes& lanes,int col_offset,int row_offset)
{
    lanes.blockLabels.assign(lanes.NumOfBlocks,0);
    int idxLabel = 1;
    int curRowIdx = 0;

    int firstBlockOnCur = 0;
    int firstBlockOnPre = 0;
    int lastBlockOnPre = -1;
    for(int i=0;i < lanes.NumOfBlocks;++i){
        if(lanes.rowBlock[i] != curRowIdx){//一行的所有块遍历完,进入重新保存当前行号
            curRowIdx = lanes.rowBlock[i]; // every block row id
            firstBlockOnPre = firstBlockOnCur;//上一次遍历的块尾+1
            lastBlockOnPre = i - 1;//新的一行
            firstBlockOnCur = i;
        }
        for(int j=firstBlockOnPre; j <= lastBlockOnPre; ++j){

            if (lanes.stBlock[i] <= lanes.enBlock[j] + col_offset && lanes.enBlock[i] >= lanes.stBlock[j] - col_offset /*&& lanes.rowBlock[i] == lanes.rowBlock[j]+1*/)
            {//判断是否与上一行左右两侧的其他blockLabels有重合
                if (lanes.blockLabels[i] == 0) // 没有被标号过,1个块有重叠
                {
                    lanes.blockLabels[i] = lanes.blockLabels[j];
                    lanes.blockLabels_berc[lanes.blockLabels[i]-1][1] = Point(lanes.enBlock[i],lanes.rowBlock[i]);
                }
                else if (lanes.blockLabels[i] != lanes.blockLabels[j])// 已经被标号,2个以上的块有重叠
                    lanes.equivalences.push_back(make_pair(lanes.blockLabels[i], lanes.blockLabels[j])); // 保存等价对
            }
        }
        if (lanes.blockLabels[i] == 0) // 与前一行+offset的任何block没有重合
        {
            lanes.blockLabels_berc.push_back(vector<Point>(2,Point(lanes.stBlock[i],lanes.rowBlock[i])));//新块左上角坐标
            lanes.blockLabels_type.push_back(lanes.lane_type[i]);
            lanes.blockLabels[i] = idxLabel++;//新块

            int lastBlockSize = lanes.blockLabels_berc.size()-1;
            for(int k=0;k<lastBlockSize;++k){//新块与四周近距离块合并
                if(lanes.blockLabels_berc[lastBlockSize][0].y >= lanes.blockLabels_berc[k][1].y-row_offset
                        && lanes.blockLabels_berc[lastBlockSize][0].y <= lanes.blockLabels_berc[k][1].y+row_offset
                        && lanes.blockLabels_berc[lastBlockSize][0].x >= lanes.blockLabels_berc[k][1].x-col_offset
                        && lanes.blockLabels_berc[lastBlockSize][0].x <= lanes.blockLabels_berc[k][1].x+col_offset){
                    lanes.equivalences.push_back(make_pair(lanes.blockLabels[i], k+1));
                }
            }
        }
    }

    //统计线的点集
    int maxLabel = *max_element(lanes.blockLabels.begin(),lanes.blockLabels.end());
    lanes.lane_num = maxLabel;
    lanes.pts.assign(maxLabel,vector<Point>(0));
    for (int i = 1; i <= lanes.lane_num; ++i)
    {
        for(int j=0;j<lanes.blockLabels.size();++j){
            if(lanes.blockLabels[j] == i){//总共有lane_num根线,匹配这个范围内的线,blockLabels值是相同,则放入同一个线的点集中
                for(int k=lanes.stBlock[j];k<=lanes.enBlock[j];++k)
                    //                        lanes.pts[i-1].push_back(Point(k,lanes.rowBlock[j])); // x is var
                    lanes.pts[i-1].push_back(Point(lanes.rowBlock[j],k));  // y is var
            }
        }
    }
}

3.将等价对转换为等价序列,每个序列需要给一相同的标号,因为它们都是等价的。从1开始,给每个等价序列一个标号。并对同一个等价序列内的点集,左上右下坐标集进行合并。相当于小块生成大块
4.第一次合并,遍历开始块的标记,查找等价序列,给予它们新的标号。
5.第二次合并相当于排错,对合并以后的大块再进行一次合并,块不同(其实可能是同一类块,但是在前期被分到不同的块了),线类别相同,合并块的点少于阈值的合并
6.将每个块的所有像素的坐标点分别计入每个线lane中
//块合并 lanes.equivalences.size() != 0时执行
void mergeSameLabel(Lanes& lanes){
    cout<< "equivalences"<<endl;
    int maxLabel = *max_element(lanes.blockLabels.begin(),lanes.blockLabels.end());//标签数量
    vector<vector<bool>> eqTab(maxLabel, vector<bool>(maxLabel, false));
    vector<pair<int, int>>::iterator vecPairIt = lanes.equivalences.begin();
    while (vecPairIt != lanes.equivalences.end())//要合并的标签方阵,相互置为True
    {
        eqTab[vecPairIt->first - 1][vecPairIt->second - 1] = true;
        eqTab[vecPairIt->second - 1][vecPairIt->first - 1] = true;
        vecPairIt++;
    }
    vector<int> labelFlag(maxLabel, 0);
    vector<vector<int>> equaList;
    vector<int> tempList;
    cout << maxLabel << endl;
    for (int i = 1; i <= maxLabel; i++)//转成列表容器(同一个容器索引号中报错要合并的块的所有标签)
    {
        if (labelFlag[i - 1])//已经有标签,则进入下一个标签的设置
        {
            continue;
        }
        labelFlag[i - 1] = equaList.size() + 1;//"新标签"
        tempList.push_back(i);
        for (vector<int>::size_type j = 0; j < tempList.size(); j++)
        {
            for (vector<bool>::size_type k = 0; k != eqTab[tempList[j] - 1].size(); k++)
            {
                if (eqTab[tempList[j] - 1][k] && !labelFlag[k])//满足eqTab为true和labelFlag为0,则为同一个类别的标签
                {
                    tempList.push_back(k + 1);//标签和索引相差1,所以加1
                    labelFlag[k] = equaList.size() + 1;//合并的标签与上面的"新标签"相同
                }
            }
        }
        equaList.push_back(tempList);
        tempList.clear();
    }
    cout << equaList.size() << endl;
    for (vector<int>::size_type i = 0; i != lanes.blockLabels.size(); i++)
    {
        lanes.blockLabels[i] = labelFlag[lanes.blockLabels[i] - 1];
    }
    //合并块的点进行合并
    for(int i=0;i<equaList.size();++i){
        int equaListSize = equaList[i].size();
        if(equaListSize != 1){//大于1代表有两个及以上标签的大块要合并
            auto it_pts = lanes.pts.begin()+equaList[i][0]-1; //要合并的第一个大块的索引
            auto it_blockLabels_berc = lanes.blockLabels_berc.begin()+equaList[i][0]-1;
            auto it_blockLabels_type = lanes.blockLabels_type.begin()+equaList[i][0]-1;
            for(int j=1;j<equaListSize;++j){
                auto it_pts_obj = it_pts+equaList[i][j]-equaList[i][0];//要合并的后面大块的索引
                auto it_blockLabels_berc_obj = it_blockLabels_berc+equaList[i][j]-equaList[i][0];
                auto it_blockLabels_type_obj = it_blockLabels_type+equaList[i][j]-equaList[i][0];
                it_pts->insert(it_pts->end(),it_pts_obj->begin(),it_pts_obj->end());//将后面的大块的点集插入到第一个大块的末尾
                it_pts_obj->resize(0);//已经合并的后面的大块的点集数量设置为0,在后面如果容器容积为0剔除该线
                it_blockLabels_berc->at(1) = it_blockLabels_berc_obj->at(1);//合并的后面的大块的左下角赋值给第一个大块的左下角,代表生成了一个新的更大的块
                it_blockLabels_berc_obj->resize(0);//已经合并的后面的大块的左上右下坐标集数量设置为0,在后面如果容器容积为0剔除该线
                *it_blockLabels_type = *it_blockLabels_type_obj;//这里是合并线类型,但是好像不必要赋值,因为如果线类型不同就不会合并
                *it_blockLabels_type_obj = 0;
            }
        }
    }
    for(auto it=lanes.blockLabels_type.begin();it!=lanes.blockLabels_type.end();){
        if(*it == 0){
            it=lanes.blockLabels_type.erase(it);
        }else{
            ++it;
        }
    }
    for(auto it=lanes.blockLabels_berc.begin();it!=lanes.blockLabels_berc.end();){
        if(it->size() == 0){
            it=lanes.blockLabels_berc.erase(it);
        }else{
            ++it;
        }
    }

}
void secondReplaceSameLabel(Lanes& lanes,int col_offset,int row_offset,int point_num_thresh)
{

    if(lanes.equivalences.size() != 0){
        mergeSameLabel(lanes);
        //剔除点数小于阈值的线
        for(auto it=lanes.pts.begin();it!=lanes.pts.end();){
            if(it->size() == 0){
                it=lanes.pts.erase(it);
                lanes.lane_num = lanes.pts.size();
            }else{
                ++it;
            }
        }
    }

    //行方向块合并
    lanes.equivalences.resize(0);
    for(int i=0; i < lanes.blockLabels_berc.size();++i){
        //float cur_k = (blockLabels_berc[i][1].y-blockLabels_berc[i][0].y+1e-10)/(blockLabels_berc[i][1].x-blockLabels_berc[i][0].x+1e-10);
        int cur_x_end = lanes.blockLabels_berc[i][1].x;
        int cur_y_end = lanes.blockLabels_berc[i][1].y;
        for(int j=i+1;j< lanes.blockLabels_berc.size();++j){
            //float pre_k =(blockLabels_berc[j][1].y-blockLabels_berc[j][0].y+1e-10)/(blockLabels_berc[j][1].x-blockLabels_berc[j][0].x+1e-10);
            if(/*(cur_k <= pre_k + 0.2 || cur_k>=pre_k-0.2) &&*/cur_y_end >=lanes.blockLabels_berc[j][0].y-row_offset
                    && cur_y_end<=lanes.blockLabels_berc[j][0].y+row_offset
                    && cur_x_end>=lanes.blockLabels_berc[j][0].x-col_offset
                    && cur_x_end<=lanes.blockLabels_berc[j][0].x+col_offset){
                //                cout <<" "<<i <<" "<<j <<"min: "<< min(lanes.pts[i].size(),lanes.pts[j].size()) <<endl;
                if (i+1 != j+1 && lanes.blockLabels_type[i] == lanes.blockLabels_type[j] && min(lanes.pts[i].size(),lanes.pts[j].size()) < row_offset){//块不同,线类别相同,合并块的点少于阈值,才合并
                    //                    cout <<" pair "<< lanes.pts[i].size()<<" "<<lanes.pts[j].size() <<endl;
                    lanes.equivalences.push_back(make_pair(i+1, j+1)); // 保存等价对
                    //                    lanes.blockLabels_berc[i][1].x = lanes.blockLabels_berc[j][1].x;
                    //                    lanes.blockLabels_berc[i][1].y = lanes.blockLabels_berc[j][1].y;
                    //                    lanes.blockLabels_berc[j][0] = Point(0,0);
                    //                    lanes.blockLabels_berc[j][1] = Point(0,0);
                }
            }
        }
    }

    if(lanes.equivalences.size() != 0){
        mergeSameLabel(lanes);
        //剔除点数小于阈值的线
        for(auto it=lanes.pts.begin();it!=lanes.pts.end();){
            if(it->size() < point_num_thresh){
                it=lanes.pts.erase(it);
                lanes.lane_num = lanes.pts.size();
            }else{
                ++it;
            }
        }
    }else{
        //剔除点数小于阈值的线
        for(auto it=lanes.pts.begin();it!=lanes.pts.end();){
            if(it->size() < 3){
                it=lanes.pts.erase(it);
                lanes.lane_num = lanes.pts.size();
            }else{
                ++it;
            }
        }
    }
}
7.结束

以上是关于c_cpp DeepLabv3 +系列分割算法总结的主要内容,如果未能解决你的问题,请参考以下文章

自动驾驶感知算法实战4——语义分割网络详解(DeepLabV3FCNUNet等)

搭建自己的语义分割平台deeplabV3+

DeepLabV3,珊瑚的分割和分类/检测

DeepLabv3+图像语义分割实战:训练自己的数据集

DeepLabv3 +分割图像边界

c_cpp 分割