码上实战立体匹配系列经典AD-Census: 多步骤视差优化

Posted 李迎松~

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了码上实战立体匹配系列经典AD-Census: 多步骤视差优化相关的知识,希望对你有一定的参考价值。

同学们好久不见!

下载完整源码,点击进入: https://github.com/ethan-li-coding/AD-Census
欢迎同学们在Github项目里讨论!

在实战的上一篇,我们对AD-Census的扫描线优化步骤的实战代码进行了介绍:

【码上实战】【立体匹配系列】经典AD-Census: (5)扫描线优化

通过扫描线优化,视差图效果基本定型,我们来回顾一下:

代价计算
代价计算
代价聚合
扫描线优化

熟悉立体匹配同学们都应该知道,后续我们一般都会做一个视差优化,以剔除错误视差以及视差填充等。AD-Census也不例外,它总共进行了4个小步骤来完成视差优化的目的,他们分别是:

  1. Outlier Detection离群点检测
  2. Iterative Region Voting迭代局部投票
  3. Proper Interpolation
  4. Depth Discontinuity Adjustment视差非连续区调整

理论部分我们就不再赘述了,大家请查看往期博文:

【理论恒叨】【立体匹配系列】经典AD-Census: (4)多步骤视差优化

本篇的内容是紧跟代码,做好代码讲解。大家往下看。

文章目录

代码实现

类设计

成员函数

我们来用一个多步骤优化器类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的视差大。

这里更详细的解释,请查看博文:

【码上实战】【立体匹配系列】经典SGM:(6)视差填充

我们来看代码:

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: 多步骤视差优化的主要内容,如果未能解决你的问题,请参考以下文章

码上实战立体匹配系列经典AD-Census: 代价计算

码上实战立体匹配系列经典AD-Census: 扫描线优化

码上实战立体匹配系列经典AD-Census: 主类

码上实战立体匹配系列经典AD-Census: 十字交叉域代价聚合

码上实战立体匹配系列经典SGM:代价计算

码上实战立体匹配系列经典PatchMatch: 框架