码上实战立体匹配系列经典AD-Census: 多步骤视差优化
Posted 李迎松~
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了码上实战立体匹配系列经典AD-Census: 多步骤视差优化相关的知识,希望对你有一定的参考价值。
同学们好久不见!
下载完整源码,点击进入: https://github.com/ethan-li-coding/AD-Census
欢迎同学们在Github项目里讨论!
在实战的上一篇,我们对AD-Census的扫描线优化步骤的实战代码进行了介绍:
通过扫描线优化,视差图效果基本定型,我们来回顾一下:
|
|
|
|
熟悉立体匹配同学们都应该知道,后续我们一般都会做一个视差优化,以剔除错误视差以及视差填充等。AD-Census也不例外,它总共进行了4个小步骤来完成视差优化的目的,他们分别是:
- Outlier Detection离群点检测
- Iterative Region Voting迭代局部投票
- Proper Interpolation
- Depth Discontinuity Adjustment视差非连续区调整
理论部分我们就不再赘述了,大家请查看往期博文:
本篇的内容是紧跟代码,做好代码讲解。大家往下看。
文章目录
代码实现
类设计
成员函数
我们来用一个多步骤优化器类MultiStepRefiner来实现该功能。类的实现放在文件multistep_refiner.h/multistep_refiner.cpp中。
/**
* \\brief 多步骤优化器
*/
class MultiStepRefiner
public:
MultiStepRefiner();
~MultiStepRefiner();
为了完成算法运算,我们需要分配一些内存来存储数据或结果,Initialize成员函数可以帮助完成这件事:
/**
* \\brief 初始化
* \\param width 影像宽
* \\param height 影像高
* \\return true:初始化成功
*/
bool Initialize(const sint32& width, const sint32& height);
算法需要数据输入以及参数设计,所以我们设计成员函数SetData和SetParam来达到这个目的:
/**
* \\brief 设置多步优化的数据
* \\param img_left // 左影像数据,三通道
* \\param cost // 代价数据
* \\param cross_arms // 十字交叉臂数据
* \\param disp_left // 左视图视差数据
* \\param disp_right // 右视图视差数据
*/
void SetData(const uint8* img_left, float32* cost,const CrossArm* cross_arms, float32* disp_left, float32* disp_right);
/**
* \\brief 设置多步优化的参数
* \\param min_disparity // 最小视差
* \\param max_disparity // 最大视差
* \\param irv_ts // Iterative Region Voting参数ts
* \\param irv_th // Iterative Region Voting参数th
* \\param lrcheck_thres // 一致性检查阈值
* \\param do_lr_check // 是否检查左右一致性
* \\param do_region_voting // 是否做内插填充
* \\param do_interpolating // 是否局部投票填充
* \\param do_discontinuity_adjustment // 是否做非连续区调整
*/
void SetParam(const sint32& min_disparity, const sint32& max_disparity, const sint32& irv_ts, const float32& irv_th, const float32& lrcheck_thres,
const bool& do_lr_check, const bool& do_region_voting, const bool& do_interpolating, const bool& do_discontinuity_adjustment);
最后,需要一个主函数来执行优化:
/** \\brief 多步视差优化 */
void Refine();
以上便是类的公有函数列表,可以通过调用以上函数完成整个优化操作。
而我们知道Refine其实包含有四个小步骤,每个步骤都是一个独立的算法模块,所以我们来设计四个私有成员函数来分别实现这四个功能。
//------4小步视差优化------//
/** \\brief 离群点检测 */
void OutlierDetection();
/** \\brief 迭代局部投票 */
void IterativeRegionVoting();
/** \\brief 内插填充 */
void ProperInterpolation();
/** \\brief 深度非连续区视差调整 */
void DepthDiscontinuityAdjustment();
最后是类的一些成员变量,图像数据、代价数据、视差数据、算法参数等,不必一一解释。
/** \\brief 图像尺寸 */
sint32 width_;
sint32 height_;
/** \\brief 左影像数据(三通道) */
const uint8* img_left_;
/** \\brief 代价数据 */
float32* cost_;
/** \\brief 交叉臂数据 */
const CrossArm* cross_arms_;
/** \\brief 左视图视差数据 */
float* disp_left_;
/** \\brief 右视图视差数据 */
float* disp_right_;
/** \\brief 左视图边缘数据 */
vector<uint8> vec_edge_left_;
/** \\brief 最小视差值 */
sint32 min_disparity_;
/** \\brief 最大视差值 */
sint32 max_disparity_;
/** \\brief Iterative Region Voting参数ts */
sint32 irv_ts_;
/** \\brief Iterative Region Voting参数th */
float32 irv_th_;
float32 lrcheck_thres_;
/** \\brief 是否检查左右一致性 */
bool do_lr_check_;
/** \\brief 是否局部投票填充 */
bool do_region_voting_;
/** \\brief 是否做内插填充 */
bool do_interpolating_;
/** \\brief 是否做非连续区调整 */
bool do_discontinuity_adjustment_;
/** \\brief 遮挡区像素集 */
vector<pair<int, int>> occlusions_;
/** \\brief 误匹配区像素集 */
vector<pair<int, int>> mismatches_;
类实现
首先我们来看,三个非算法功能性函数Initialize、SetData、SetParam,它们代码都比较简单,都是为算法实现做准备的。
首先是Initialize,代码如下,只做了一个边缘数据的初始化,vec_edge_left_是和图像尺寸一样的数组,它保存着图像所有像素的边缘值,作用是在视差非连续调整步骤的时候,提供边缘信息。
bool MultiStepRefiner::Initialize(const sint32& width, const sint32& height)
width_ = width;
height_ = height;
if (width_ <= 0 || height_ <= 0)
return false;
// 初始化边缘数据
vec_edge_left_.clear();
vec_edge_left_.resize(width*height);
return true;
其次是SetData,传入影像数据、代价数据、十字交叉臂数据、左右视差图。
void MultiStepRefiner::SetData(const uint8* img_left, float32* cost,const CrossArm* cross_arms, float32* disp_left, float32* disp_right)
img_left_ = img_left;
cost_ = cost;
cross_arms_ = cross_arms;
disp_left_ = disp_left;
disp_right_= disp_right;
再就是SetParam,对算法的所有参数进行赋值。
void MultiStepRefiner::SetParam(const sint32& min_disparity, const sint32& max_disparity, const sint32& irv_ts, const float32& irv_th, const float32& lrcheck_thres,
const bool& do_lr_check, const bool& do_region_voting, const bool& do_interpolating, const bool& do_discontinuity_adjustment)
min_disparity_ = min_disparity;
max_disparity_ = max_disparity;
irv_ts_ = irv_ts;
irv_th_ = irv_th;
lrcheck_thres_ = lrcheck_thres;
do_lr_check_ = do_lr_check;
do_region_voting_ = do_region_voting;
do_interpolating_ = do_interpolating;
do_discontinuity_adjustment_ = do_discontinuity_adjustment;
当算法数据和参数都赋值完毕,接下来便是各个优化子步骤的实现了。我们一个个来讲解。
1. Outlier Detection离群点检测
离群点检测听上去似乎与众不同,但实际它完完全全就是一个左右一致性检查,关于左右一致性检查前面已经说得很多了,实在不是一个陌生的词儿。
在离群点检测的过程中,我们会对离群点进行遮挡点和非遮挡的的分类。这和SGM的思路是一模一样的,遮挡区和非遮挡区的判别方法为:
(1)
p
p
p的视差值和周围的背景像素视差值比较接近。
(2)
p
p
p因为遮挡而在右影像上不可见,所以它会匹配到右影像上的前景像素,而前景像素的视差值必定比背景像素大,即比
p
p
p的视差大。
这里更详细的解释,请查看博文:
我们来看代码:
void MultiStepRefiner::OutlierDetection()
const sint32 width = width_;
const sint32 height = height_;
const float32& threshold = lrcheck_thres_;
// 遮挡区像素和误匹配区像素
auto& occlusions = occlusions_;
auto& mismatches = mismatches_;
occlusions.clear();
mismatches.clear();
// ---左右一致性检查
for (sint32 y = 0; y < height; y++)
for (sint32 x = 0; x < width; x++)
// 左影像视差值
auto& disp = disp_left_[y * width + x];
if (disp == Invalid_Float)
mismatches.emplace_back(x, y);
continue;
// 根据视差值找到右影像上对应的同名像素
const auto col_right = lround(x - disp);
if (col_right >= 0 && col_right < width)
// 右影像上同名像素的视差值
const auto& disp_r = disp_right_[y * width + col_right];
// 判断两个视差值是否一致(差值在阈值内)
if (abs(disp - disp_r) > threshold)
// 区分遮挡区和误匹配区
// 通过右影像视差算出在左影像的匹配像素,并获取视差disp_rl
// if(disp_rl > disp)
// pixel in occlusions
// else
// pixel in mismatches
const sint32 col_rl = lround(col_right + disp_r);
if (col_rl > 0 && col_rl < width)
const auto& disp_l = disp_left_[y * width + col_rl];
if (disp_l > disp)
occlusions.emplace_back(x, y);
else
mismatches.emplace_back(x, y);
else
mismatches.emplace_back(x, y);
// 让视差值无效
disp = Invalid_Float;
else
// 通过视差值在右影像上找不到同名像素(超出影像范围)
disp = Invalid_Float;
mismatches.emplace_back(x, y);
我们对左视图的所有像素视差执行一致性检查,不满足一致性检查的像素,我们将视差值赋值为无效值,且区分为误匹配区像素和遮挡区像素并单独存储,目的是在后面视差插值的时候使用不同的策略。
2. Iterative Region Voting迭代局部投票
对无效像素 p p p 的十字交叉域支持区内的所有可靠像素,统计[0, d m a x d_max dmax]范围视差分布的直方图 H p H_p Hp
(直方图的值相当于视差的得票数)。占有最多像素(也就是得票最多)的视差值记为 d p ∗ d_p^* dp∗ 。可靠像素数量记为 S p S_p Sp
。如果可靠像素的数量足够多,且得票最多的视差值得票率足够多,则把 d p ∗ d_p^* dp∗ 赋给 p p p 。这里的两个“足够多”,用阈值来控制:式中, τ s τ_s τs和 τ H τ_H τH为两个预设阈值。
以上是局部投票的理论描述,所以我们对每个像素,要做的就是,把无效像素 p p p十字交叉域支持区的所有可靠像素的直方图计算出来,然后挑出出现次数最多的视差值 d p ∗ d_p^* dp∗,如果它的数量占总数的比例大于阈值,且整个支持区内可靠像素足够多,就把 d p ∗ d_p^* dp∗赋给 p p p。
以上操作,我们要重复做多次,算法推荐是5次。
我们来看代码:
void MultiStepRefiner::IterativeRegionVoting()
const sint32 width = width_;
const auto disp_range = max_disparity_ - min_disparity_;
if(disp_range <= 0)
return;
const auto arms = cross_arms_;
// 直方图
vector<sint32> histogram(disp_range,0);
// 迭代5次
const sint32 num_iters = 5;
for (sint32 it = 0; it < num_iters; it++)
for (sint32 k = 0; k < 2; k++)
auto& trg_pixels = (k == 0) ? mismatches_ : occlusions_;
for (auto& pix : trg_pixels)
const sint32& x = pix.first;
const sint32& y = pix.second;
auto& disp = disp_left_[y * width + x];
if(disp != Invalid_Float)
continue;
// init histogram
memset(&histogram[0], 0, disp_range * sizeof(sint32));
// 计算支持区的视差直方图
// 获取arm
auto& arm = arms[y * width + x];
// 遍历支持区像素视差,统计直方图
for (sint32 t = -arm.top; t <= arm.bottom; t++)
const sint32& yt = y + t;
auto& arm2 = arms[yt * width_ + x];
for (sint32 s = -arm2.left; s <= arm2.right; s++)
const auto& d = disp_left_[yt * width + x + s];
if (d != Invalid_Float)
const auto di = lround(d);
histogram[di - min_disparity_]++;
// 计算直方图峰值对应的视差
sint32 best_disp = 0, count = 0;
sint32 max_ht = 0;
for (sint32 d = 0; d < disp_range; d++)
const auto& h = histogram[d];
if (max_ht < h)
max_ht = h;
best_disp = d;
count += h;
if (max_ht > 0)
if (count > irv_ts_ && max_ht * 1.0f / count > irv_th_)
disp = best_disp + min_disparity_;
// 删除已填充像素
for (auto it = trg_pixels.begin(); it != trg_pixels.end();)
const sint32 x = it->first;
const sint32 y = it->second;
if(disp_left_[y * width + x]!=Invalid_Float)
it = trg_pixels.erase(it);
else ++it;
Proper Interpolation
此步骤其实就是视差填充。在一致性检查中无效视差被区分为遮挡区和误匹配区。首先对无效像素 p p p ,沿其邻域16个方向搜索可靠像素视差值,对于遮挡区像素,则选择所有可靠像素视差值中的最小值,因为遮挡区大概率来自于背景,背景视差往往是较小值;对于误匹配区像素,则选择和 p p p 颜色最近的像素的视差,因为颜色相近的像素往往具有相近的视差值(这里应该是要限制下搜索步长的,太远了假设大概率都失效了)。
void MultiStepRefiner::ProperInterpolation()
const sint32 width = width_;
const sint32 height = height_;
const float32 pi = 3.1415926f;
// 最大搜索行程,没有必要搜索过远的像素
const sint32 max_search_length = std::max(abs(max_disparity_), abs(min_disparity_));
std::vector<pair<sint32, float32>> disp_collects;
for (sint32 k = 0; k < 2; k++)
auto& trg_pixels = (k == 0) ? mismatches_ : occlusions_;
if (trg_pixels.empty())
continue;
std::vector<float32> fill_disps(trg_pixels.size());
// 遍历待处理像素
for (auto n = 0u; n < trg_pixels以上是关于码上实战立体匹配系列经典AD-Census: 多步骤视差优化的主要内容,如果未能解决你的问题,请参考以下文章