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 +系列分割算法总结的主要内容,如果未能解决你的问题,请参考以下文章