计算机视觉大型攻略 —— 立体视觉(5)Stixel World

本文是立体视觉部分的第五篇,说一下立体视觉的一个实际应用—Stixel World。

相关论文:

[1]The Stixel World - A Compact Medium Level Representation of the 3D-World

[2]Efficient Representation of Traffic Scenes by Means of Dynamic Stixels

Stixel World

Stixel算法将物体抽象成立在地面上的一根根的Sticks, 这些Sticks将图片分割成Freespace与障碍物。Stixel是一个细长矩形,宽度固定(3px, 5px,...),高度与障碍物相同。本质上,Stixel是一种超像素抽象,介于像素与物体之间,在性能和算法复杂度上比二者有着明显的优势。

  • 与像素级的算法相比,Stixel将物体抽象成了一根根立在地面上的长方形柱子,有效降低了计算规模。
  • 与传统的目标检测相比,Stixel的一般性较强。传统的目标检测一般是对某一类的目标检测,如行人,车辆等。而Stixel并不区分类别,使用在ADAS和无人驾驶系统上,提升了一般性而且降低了系统实现复杂度。

Stixel最初由6d-vision lab提出,6d-vision隶属于戴姆勒研究院,有一整套的视觉感知方案,Stixel是这套方案的重要组成部分。更多6d-vision lab的研究成果可以从他官网上获得:http://www.6d-vision.com/

如上图,左图是视差图,右图为Stixel。图中Stixel宽度固定(5个像素),高度等于障碍物的高度。

构建一个上图的Stixel,需要几个步骤,

  • 计算视差图
  • 计算FreeSpace(找到Stixel的底部)
  • 高度分割(找到Stixel的顶部)

计算视差图

视差图的计算参考本系列前几篇文章。原作者和本文最后的代码均采用SGM算法。

https://blog.csdn.net/plateros/article/details/102392160

计算FreeSpace

上图绿色部分标注了Freespace。可见Freespace给出了深度方向上第一个不是地面的像素,也就是Stixel的底部。论文[1]采用了基于概率的占用网格加动态规划(Dynamic Programming)的算法获取freespace,论文[2]在此基础上做了改进。我们逐一解释一下。

方法一: 随机占用网格(Stochasitc Occupancy Grids)

随机占用网格论文:Free Space Computation Using Stochastic Occupancy Grids and Dynamic Programming

占用网格

顾名思义,就是把图像描述的场景划分到一个多维网格当中。由于此算法的目的是获取freespace,因此只需要一个二维网格M(mxn)。

以二维为例,Occupancy Grids的特点,

  • 两个不同网格(grid cell)之间没有交集。
  • 网格(cell)是四边形,但不一定是长方形的。
  • 网格(cell)大小不一定完全相同。
  • 每个网格关联一个占用概率D(i, j)(likelihood)。

随机占用网格认为网格的观测噪声符合高斯分布。假设mk(u, v, d)是某一点pk(x, y, z)在图像上的投影点。观测噪声符合高斯分布,因此网格的噪声模型是一个三元高斯分布,他的概率密度函数,

                            

每一个网格的likelihood就是将所有观测的概率密度相加,

                             

在一个cell上,把所有的观测(也许是整张图片)都计算一遍概率然后相加,这种做法的计算压力太大了。作者在实现时,每个cell只选了“影响”较大的观测点。

论文建模了三种不同的Occupancy Grid,定义了不同的Lij。下图为三种Grid的说明,上面这一排为真实世界的场景 ,横坐标为x,纵坐标为z。第二排定义了不同的Grid,这三种Grid的坐标轴的含义不同。

  • 笛卡尔占用网格(Cartesian Occupancy Grid)

上图(a),网格横坐标以x划分, 纵坐标以z轴划分,跟相机坐标系是一个线性变换。cell(i, j)的坐标就是Pij(xij, zij)。

这种网格的似然函数为,

                      

这个公式的含义是将pij(xij,y,zij)通过投影矩阵P转为u,v,d,然后与观测的u, v, d相减。注意,这个公式里的y,对于一个网格来说,y其实是任意的。也就是说,需要首先通过mk把y计算出来。

  • Column/Disparity占用网格(Column/Disparity Occupancy Grid)

参考上图(b),网格的横坐标按照图像的划分,纵坐标为视差。cell(i, j)的坐标为(uij, dij)。计算点(u, d)的似然函数为,

                     

  • 极坐标占用网格(Polar Occupancy Grid)

与Column/Disparity Occupancy Grid类似,Polar Occupancy Grid的横坐标按照图像的列划分,但是纵坐标按照Z划分。cell(i, j)的坐标为(uij, zij)。点(u, z)的似然函数为,

                  

                    

动态规划计算Freespace

如上图所示,作者采用Polar Occpancy Grid,按照射线方向(对Polar网格来说就是逐列)寻找第一个被占用的网格,由此完成freespace的划分。

这个问题可以看作是从左到右找到一条线,把网格分成两个部分。我们可以直接采用一个高度门槛,很容易完成这个划分,但是考虑到相邻的网格存在平滑约束,采用全局优化的算法会更好。论文采用了动态规划的算法完成这个全局优化。

首先定义一个图G(V, E)。V代表了图中的顶点集,每一个网格都有一个对应的顶点。E为边集,相邻两列的所有顶点彼此存在边。边上定义了两个顶点之间的代价(cost)。动态规划的目的就是找到一条路径,最小化他的代价。边的代价包含了数据项和平滑项。

连接Vij与Vkl的边的代价可以定义为,

                                              

数据项是D(i, j)的倒数,

                                              

平滑项包含了空间平滑和时间平滑,

                                            

S(j, l)定义了空间平滑性,d(i, j)表示了第i行与第l行的距离(米),Cs是一个常量, 惩罚行与行之间的差异。Ts是一个以距离为单位的阈值。也就是说,当第Vij与Vlk的行间距d小于Ts是,S=Cs*d,当d>=Ts时,S=CsTs.

T(i, j)定义了时间平滑性, Ct, Tt同样是参数常量,与Cs, Ts的作用类似。而d(j, j')表示上一帧Vij叠加了运动信息后的位置,与这一帧实际位置的差。

方法二:Score Image

论文[2]在随机占有网格的基础上做了改进。与[1]类似,[2]同样可以理解为一种极坐标占有网格(Polar OccupancyGrid),但是每一个网格不再关联一个概率值,而是关联一个得分(Score),形成了Score Image。最后在这个Score Image使用动态规划算法提取Freespace。

如上图,对图像中的某一列来说,视差可以分为三个部分 ,Road, Object, Background。对于任一列的像素,根据其视差值,定义一个Score,记作\Gamma _{v} 。\Gamma _{v}为两部分的和,road evidence  \Gamma _{v}^{R} 和 obstacle evidence \Gamma _{v}^{O} 。

                                      

某一列,像素P的Road evidence指的是从当前行到Vmax行(图片方向往下)每一行d_{R}(v) 与dv的差的和。d_{R}(v)代表当前行是地面时的视差值,这个值可以由相机参数算出来。

Obstacle evidence计算的是,从当前行开始往图片上方(v减小的方向)找hv行,计算每一行的视差dv与当前行视差,再求和。hv需要计算一下,即以此为地面,往上找H=1米的行数。

最终的代价等于二者之和,代价小,也就意味着既像Road,又像Obstacle,那么就是二者的交界点了。

最后与方法一采用同样的动态规划算法,求得最终的FreeSpace.

高度分割

由于这个算法只是讨论了一层Stixel的获取, 这里的高度分割算法只是给出了第一个障碍物的高度。 后续的论文通过建模概率问题,给出了多层Stixel的算法,以后有时间再详细写一下。 如果只考虑一层Stixel, 只需要将Freespace“后"面的像素分为前景,背景两类即可,算法与计算Freespace如出一辙。首先计算像素代价,之后通过动态规划算法就可以做出最优化划分。

代价

由于之前做Freespace的时候,可以得到地面与第一个障碍物交接点的视差,我们实际上可以获得第一个障碍物的期望视差\hat{d}。很容易想到我们可以计算这一列所有d与\hat{d}的差,然后施加一个门槛,评估相似度。论文使用下面这个公式计算相似度,

                                      

                                         

从这个公式可以看到,与期望视差\hat{d}的差异越大,M越小。

代价公式,假设(u,v)为Stixel的顶部(Top), 背景像素就是(0~v-1),前景像素就是从(v~vf)。vf就是从Freespace得到的Stixel底部(Base)。

                                         

从这个公式可以看出,代价最小,就是要背景与\hat{d}差异最大且前景与\hat{d}差异最小。

动态规划

与Freespace类似,寻找最优代价,仍然需要考虑像素之间的平滑性约束,定义能量函数,

                                    

C就是代价,S定义了平滑项,

                                       

最后采用动态规划的算法计算最优划分。

代码实例

https://github.com/linusyue/stixel

核心算法来自gish523的branch,虽有些问题但是对论文还原的最好。

运行参数

./stixel ../data/ ../camera.xml

程序流程

首先计算视差,sgm算法就是上一篇文章采用的算法,最后转换成float型。

        Mat I0_Gray, I1_Gray;
        cvtColor(I0, I0_Gray, cv::COLOR_BGR2GRAY);
        cvtColor(I1, I1_Gray, cv::COLOR_BGR2GRAY);

        imshow("I0", I0);
        imshow("I1", I1);

        const auto t1 = std::chrono::system_clock::now();

        sgm.compute(I0_Gray, I1_Gray, D0, D1);

        const auto t2 = std::chrono::system_clock::now();
        const auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count();
        std::cout << "disparity computation time: " << duration << "[msec]" << std::endl;

        D0.convertTo(draw, CV_8U, 255. / (SemiGlobalMatching::DISP_SCALE * param.numDisparities));

调用Stixel_world类的compute函数,输入相机参数。

        stix_param.camera.fu = node["FocalLengthX"];
        stix_param.camera.fv = node["FocalLengthY"];
        stix_param.camera.u0 = node["CenterX"];
        stix_param.camera.v0 = node["CenterY"];
        stix_param.camera.baseline = node["BaseLine"];
        stix_param.camera.height = node["Height"];
        stix_param.camera.tilt = node["Tilt"];
        stix_param.minDisparity = -1;
        stix_param.maxDisparity = param.numDisparities;

        StixelWorld stixelWorld(stix_param);

compute函数按照stixel_width,计算每一个stixel列的视差。这里取了中值。注意columns的行数是umax,列是vmax,相当于把视差图做了90度翻转。我之前写GPU代码的时候也做了这个翻转,以方便索引,不过这里其实没必要。

486     const int stixelWidth = param_.stixelWidth;
487     const int umax = disparity.cols / stixelWidth;
488     const int vmax = disparity.rows;
489     CameraParameters camera = param_.camera;
490 
491     // compute horizontal median of each column
492     cv::Mat1f columns(umax, vmax);
493     std::vector<float> buf(stixelWidth);
494     for (int v = 0; v < vmax; v++)
495     {
496         for (int u = 0; u < umax; u++)
497         {
498             // compute horizontal median
499             for (int du = 0; du < stixelWidth; du++)
500                 buf[du] = disparity.at<float>(v, u * stixelWidth + du);
501             std::sort(std::begin(buf), std::end(buf));
502             const float m = buf[stixelWidth / 2];
503 
504             // store with transposed
505             columns.ptr<float>(u)[v] = m;
506         }
507     }

之后计算道路模型,也就是相机高度与和水平地面的倾斜角。这个算法的思路上文没有提及。因为我们水平路面与相机一般都会有个夹角,而这个夹角会因为地面起伏而发生变化,所以最好能够动态获取这个角度信息。算法大概的意思是通过v-disparity在视差图上估计了一条直线,这条直线与相机坐标系存在一个数学关系。可以算出水平路面与相机夹角,以及相机高度。目前gishi523的这个算法健壮性不太好。经常出现计算错误的情况。我回头重写一下这部分代码,再针对这个算法写一篇文章。

509     // compute road model (assumes planar surface)
510     Line line;
511     if (param_.roadEstimation == ROAD_ESTIMATION_AUTO)
512     {
513         line = calcRoadModelVD(columns, camera);
514 
515         // when AUTO mode, update camera tilt and height
516         camera.tilt = atanf((line.a * camera.v0 + line.b) / (camera.fu * lin    e.a));
517         camera.height = camera.baseline * cosf(camera.tilt) / line.a;
518 
519         std::cout<<"(tilt, height): "<<camera.tilt<<","<<camera.height<<std:    :endl;
520     }
521     else if (param_.roadEstimation == ROAD_ESTIMATION_CAMERA)
522     {
523         line = calcRoadModelCamera(camera);
524     }
525     else
526     {
527         CV_Error(cv::Error::StsInternal, "No such mode");
528     }
529 

roadDisp就是上文提及的d_{R}(v)。假设第v行是地面的视差值。从第vhor行开始往上,路面的视差是非法的。

530     // compute expected road disparity
531     std::vector<float> roadDisp(vmax);
532     for (int v = 0; v < vmax; v++)
533     {
534         roadDisp[v] = line.a * v + line.b;
535     }
536 
537     // horizontal row from which road dispaliry becomes negative
538     const int vhor = cvRound(-line.b / line.a);

后面计算freespace和高度划分,实现与正文中完全一致。

540     // free space computation
541     FreeSpace freeSpace;
542     std::vector<int> oPath;
543     freeSpace.compute(columns, roadDisp, vhor, lowerPath_, oPath, camera);
544 
545     // height segmentation
546     HeightSegmentation heightSegmentation;
547     heightSegmentation.compute(columns, lowerPath_, upperPath_, camera);

运行结果,

注意到每一列只有一层Stixel,而有时我们会关注被遮挡的部分。这时候需要多层Stixel。6-D lab建模了概率模型计算多层Stixel。将来单独写一篇文章吧。

 

  • 3
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值