本文是立体视觉部分的第五篇,说一下立体视觉的一个实际应用—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算法将物体抽象成立在地面上的一根根的Sticks, 这些Sticks将图片分割成Freespace与障碍物。Stixel是一个细长矩形,宽度固定(3px, 5px,...),高度与障碍物相同。本质上,Stixel是一种超像素抽象,介于像素与物体之间,在性能和算法复杂度上比二者有着明显的优势。
Stixel最初由6d-vision lab提出,6d-vision隶属于戴姆勒研究院,有一整套的视觉感知方案,Stixel是这套方案的重要组成部分。更多6d-vision lab的研究成果可以从他官网上获得:http://www.6d-vision.com/
如上图,左图是视差图,右图为Stixel。图中Stixel宽度固定(5个像素),高度等于障碍物的高度。
构建一个上图的Stixel,需要几个步骤,
视差图的计算参考本系列前几篇文章。原作者和本文最后的代码均采用SGM算法。
https://blog.csdn.net/plateros/article/details/102392160
上图绿色部分标注了Freespace。可见Freespace给出了深度方向上第一个不是地面的像素,也就是Stixel的底部。论文[1]采用了基于概率的占用网格加动态规划(Dynamic Programming)的算法获取freespace,论文[2]在此基础上做了改进。我们逐一解释一下。
随机占用网格论文:Free Space Computation Using Stochastic Occupancy Grids and Dynamic Programming
顾名思义,就是把图像描述的场景划分到一个多维网格当中。由于此算法的目的是获取freespace,因此只需要一个二维网格M(mxn)。
以二维为例,Occupancy Grids的特点,
随机占用网格认为网格的观测噪声符合高斯分布。假设mk(u, v, d)是某一点pk(x, y, z)在图像上的投影点。观测噪声符合高斯分布,因此网格的噪声模型是一个三元高斯分布,他的概率密度函数,
每一个网格的likelihood就是将所有观测的概率密度相加,
在一个cell上,把所有的观测(也许是整张图片)都计算一遍概率然后相加,这种做法的计算压力太大了。作者在实现时,每个cell只选了“影响”较大的观测点。
论文建模了三种不同的Occupancy Grid,定义了不同的Lij。下图为三种Grid的说明,上面这一排为真实世界的场景 ,横坐标为x,纵坐标为z。第二排定义了不同的Grid,这三种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计算出来。
参考上图(b),网格的横坐标按照图像的列划分,纵坐标为视差。cell(i, j)的坐标为(uij, dij)。计算点(u, d)的似然函数为,
与Column/Disparity Occupancy Grid类似,Polar Occupancy Grid的横坐标按照图像的列划分,但是纵坐标按照Z划分。cell(i, j)的坐标为(uij, zij)。点(u, z)的似然函数为,
如上图所示,作者采用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叠加了运动信息后的位置,与这一帧实际位置的差。
论文[2]在随机占有网格的基础上做了改进。与[1]类似,[2]同样可以理解为一种极坐标占有网格(Polar OccupancyGrid),但是每一个网格不再关联一个概率值,而是关联一个得分(Score),形成了Score Image。最后在这个Score Image使用动态规划算法提取Freespace。
如上图,对图像中的某一列来说,视差可以分为三个部分 ,Road, Object, Background。对于任一列的像素,根据其视差值,定义一个Score,记作 。
为两部分的和,road evidence
和 obstacle evidence
。
某一列,像素P的Road evidence指的是从当前行到Vmax行(图片方向往下)每一行 与dv的差的和。
代表当前行是地面时的视差值,这个值可以由相机参数算出来。
Obstacle evidence计算的是,从当前行开始往图片上方(v减小的方向)找hv行,计算每一行的视差dv与当前行视差,再求和。hv需要计算一下,即以此为地面,往上找H=1米的行数。
最终的代价等于二者之和,代价小,也就意味着既像Road,又像Obstacle,那么就是二者的交界点了。
最后与方法一采用同样的动态规划算法,求得最终的FreeSpace.
由于这个算法只是讨论了一层Stixel的获取, 这里的高度分割算法只是给出了第一个障碍物的高度。 后续的论文通过建模概率问题,给出了多层Stixel的算法,以后有时间再详细写一下。 如果只考虑一层Stixel, 只需要将Freespace“后"面的像素分为前景,背景两类即可,算法与计算Freespace如出一辙。首先计算像素代价,之后通过动态规划算法就可以做出最优化划分。
由于之前做Freespace的时候,可以得到地面与第一个障碍物交接点的视差,我们实际上可以获得第一个障碍物的期望视差。很容易想到我们可以计算这一列所有d与
的差,然后施加一个门槛,评估相似度。论文使用下面这个公式计算相似度,
从这个公式可以看到,与期望视差的差异越大,M越小。
代价公式,假设(u,v)为Stixel的顶部(Top), 背景像素就是(0~v-1),前景像素就是从(v~vf)。vf就是从Freespace得到的Stixel底部(Base)。
从这个公式可以看出,代价最小,就是要背景与差异最大且前景与
差异最小。
动态规划
与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(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 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(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(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): "<
roadDisp就是上文提及的
。假设第v行是地面的视差值。从第vhor行开始往上,路面的视差是非法的。
530 // compute expected road disparity
531 std::vector 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 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。将来单独写一篇文章吧。
你可能感兴趣的:(立体视觉,立体视觉,Freespace,stixel,动态规划)