欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

荐 【码上实战】【立体匹配系列】经典PatchMatch: (2)主类

程序员文章站 2021-12-30 07:17:21
立体匹配经典算法PatchMatchStereo在线编码教学(2),代码已开源!...

下载源码,点击进入: Github - PatchMatchStereo
欢迎同学们在Github项目里讨论,如果觉得博主代码质量不错,给颗小星星,以及Follow Me!感激不尽!

算法效果图镇楼:

荐
                                                        【码上实战】【立体匹配系列】经典PatchMatch: (2)主类
荐
                                                        【码上实战】【立体匹配系列】经典PatchMatch: (2)主类
荐
                                                        【码上实战】【立体匹配系列】经典PatchMatch: (2)主类
荐
                                                        【码上实战】【立体匹配系列】经典PatchMatch: (2)主类

上一篇博客框架中,我们已经从最顶层的角度理清了整个算法的思路、框架、步骤,本篇开始我们就进入实质性的代码分析。

本篇的内容是PatchMatchStereo(后面简称PMS)的主类分析。

【码上实战】【立体匹配系列】经典PatchMatch: (2)主类

主类 PatchMatchStereo

主类,即PMS的实现类,我们以PatchMatchStereo 给类命名,

/**
 * \brief PatchMatch类
 */
class PatchMatchStereo
{
public:
	PatchMatchStereo();
	~PatchMatchStereo();
}

公有函数

PMS类的职责是匹配,所以设计 Match 成员函数为执行匹配的接口,给调用者调用,看注释便一目了然,传入图像,传出视差图,功能很清晰。

/**
* \brief 执行匹配
* \param img_left	输入,左影像数据指针,3通道
* \param img_right	输入,右影像数据指针,3通道
* \param disp_left	输出,左影像视差图指针,预先分配和影像等尺寸的内存空间
*/
bool Match(const uint8* img_left, const uint8* img_right, float32* disp_left);

为了匹配,它需要分配一些内存,预分配往往是提高效率的常规操作,可别总是需要的时候才分配,要记住内存分配那是要耗时的。举个例子,你需要一块和图像等大的内存块存储梯度,只要图像尺寸不变,你每次都是要那么大的内存块,完全没必要频繁的分配销毁、再分配销毁,一开始分配一块后就别还给系统了,自己拿着一直用一直爽!

因此设计 Initialize 初始化函数来给内部数组预分配内存;设计 Reset 函数在影像尺寸和算法参数修改时重新预分配。

/**
* \brief 类的初始化,完成一些内存的预分配、参数的预设置等
* \param width		输入,核线像对影像宽
* \param height		输入,核线像对影像高
* \param option		输入,PatchMatchStereo参数
*/
bool Initialize(const sint32& width, const sint32& height, const PMSOption& option);

/**
* \brief 重设
* \param width		输入,核线像对影像宽
* \param height		输入,核线像对影像高
* \param option		输入,SemiGlobalMatching参数
*/
bool Reset(const uint32& width, const uint32& height, const PMSOption& option);

私有函数

以上只是上层的可开放接口,还有下层的算法步骤实现接口,它们是实现PMS各个步骤的一些子函数,对算法实现来说它们是真正的核心,根据PMS的步骤图,它们主要包括:

  1. 随机初始化 RandomInitialization
  2. 迭代传播 Propagation
  3. 一致性检查 LRCheck
  4. 视差填充 FillHolesInDispMap

还有一些其他的细枝末叶不用细说,例如计算梯度ComputeGradient、释放内存Release之类的,一看便懂。

它们统统归为私有函数,但调用者不一定关心算法的详细实现步骤,甚至可以完全隐藏它们。

private:
	/** \brief 随机初始化 */
	void RandomInitialization() const;

	/** \brief 计算灰度数据 */
	void ComputeGray() const;

	/** \brief 计算梯度数据 */
	void ComputeGradient() const;

	/** \brief 迭代传播 */
	void Propagation() const;

	/** \brief 一致性检查	 */
	void LRCheck();

	/** \brief 视差图填充 */
	void FillHolesInDispMap();

	/** \brief 平面转换成视差 */
	void PlaneToDisparity() const;

	/** \brief 内存释放	 */
	void Release();

成员变量

成员变量保存着算法需要在算法周期内完全持有的数据,数据是算法的内核,算法的运算过程便是在对数据不断的进行数学/逻辑运算及存取。

我们需要哪些数据呢?

  1. PMS算法参数
  2. 左右影像数据、尺寸等属性
  3. 影像的灰度、梯度数据(灰度是为了算梯度,梯度是为了算相似度)
  4. 聚合代价数据,存储像素的聚合代价值
  5. 视差图,存储像素的视差值
  6. 平面数据,存储像素的平面
  7. 误匹配像素,存储像素填充的对象

详见代码:

/** \brief PMS参数	 */
PMSOption option_;

/** \brief 影像宽	 */ 
sint32 width_;

/** \brief 影像高	 */
sint32 height_;

/** \brief 左影像数据	 */
const uint8* img_left_;
/** \brief 右影像数据	 */
const uint8* img_right_;

/** \brief 左影像灰度数据	 */
uint8* gray_left_;
/** \brief 右影像灰度数据	 */
uint8* gray_right_;

/** \brief 左影像梯度数据	 */
PGradient* grad_left_;
/** \brief 右影像梯度数据	 */
PGradient* grad_right_;

/** \brief 左影像聚合代价数据	 */
float32* cost_left_;
/** \brief 右影像聚合代价数据	 */
float32* cost_right_;

/** \brief 左影像视差图	*/
float32* disp_left_;
/** \brief 右影像视差图	*/
float32* disp_right_;

/** \brief 左影像平面集	*/
DisparityPlane* plane_left_;
/** \brief 右影像平面集	*/
DisparityPlane* plane_right_;

/** \brief 是否初始化标志	*/
bool is_initialized_;

/** \brief 误匹配区像素集	*/
vector<pair<int, int>> mismatches_left_;
vector<pair<int, int>> mismatches_right_;

需要关注的是,成员变量的类型中,除了一些基础类型(sint32、float32之类的),还有几个陌生的类型:

  1. PMSOption,PMS的参数结构体
  2. PGradient,梯度结构体
  3. DisparityPlane,视差平面结构体

它们三个是代码里自定义的类型,定义成结构体那自然是为了方便,它们都放在文件 pms_types.h 中,我们看看它们的具体定义:

PMSOption结构体,它的成员是PMS算法的所有参数,调用者可以通过改变这些参数来让算法得到不同的结果,不同的数据也会对应着不同的参数,参数的存在让算法变得更灵活*。

/** \brief PMS参数结构体 */
struct PMSOption {
	sint32	patch_size;			// patch尺寸,局部窗口为 patch_size*patch_size
	sint32  min_disparity;		// 最小视差
	sint32	max_disparity;		// 最大视差

	float32	gamma;				// gamma 权值因子
	float32	alpha;				// alpha 相似度平衡因子
	float32	tau_col;			// tau for color	相似度计算颜色空间的绝对差的下截断阈值
	float32	tau_grad;			// tau for gradient 相似度计算梯度空间的绝对差下截断阈值

	sint32	num_iters;			// 传播迭代次数

	bool	is_check_lr;		// 是否检查左右一致性
	float32	lrcheck_thres;		// 左右一致性约束阈值

	bool	is_fill_holes;		// 是否填充视差空洞

	bool	is_fource_fpw;		// 是否强制为Frontal-Parallel Window
	bool	is_integer_disp;	// 是否为整像素视差
	
	PMSOption() : patch_size(35), min_disparity(0), max_disparity(64), gamma(10.0f), alpha(0.9f), tau_col(10.0f),
	              tau_grad(2.0f), num_iters(3),
	              is_check_lr(false),
	              lrcheck_thres(0),
	              is_fill_holes(false), is_fource_fpw(false), is_integer_disp(false) { }
};

梯度结构体,保存着x/yx/y两个方向的梯度值,代码里采用的是Sobel这类带方向的边缘提取算法,所以梯度有两个维度。

/**
 * \brief 梯度结构体
 */
struct PGradient {
	sint16 x, y;
	PGradient() : x(0), y(0) {}
	PGradient(sint16 _x, sint16 _y) {
		x = _x; y = _y;
	}
};

视差平面是一个较为核心的结构体,贯穿全代码,它可以通过视差和法线来构建,并包含以下功能:

  1. 获取像素(x,y)的视差
  2. 获取平面法线
  3. 在两个视图中相互转换

将视差平面设计成一个结构体会增加代码的可读性,因为代码中会频繁的获取像素的视差、较频繁的获取平面的法线,把他们都写成一个函数,让代码更加简洁和易懂。

/**
 * \brief 视差平面
 */
struct DisparityPlane {
	PVector3f p;
	DisparityPlane() = default;
	DisparityPlane(const float32& x,const float32& y,const float32& z) {
		p.x = x; p.y = y; p.z = z;
	}
	DisparityPlane(const sint32& x, const sint32& y, const PVector3f& n, const float32& d) {
		p.x = -n.x / n.z;
		p.y = -n.y / n.z;
		p.z = (n.x * x + n.y * y + n.z * d) / n.z;
	}

	/**
	 * \brief 获取该平面下像素(x,y)的视差
	 * \param x		像素x坐标
	 * \param y		像素y坐标
	 * \return 像素(x,y)的视差
	 */
	float32 to_disparity(const sint32& x,const sint32& y) const
	{
		return p.dot(PVector3f(float32(x), float32(y), 1.0f));
	}

	/** \brief 获取平面的法线 */
	PVector3f to_normal() const
	{
		PVector3f n(p.x, p.y, -1.0f);
		n.normalize();
		return n;
	}

	/**
	 * \brief 将视差平面转换到另一视图
	 * 假设左视图平面方程为 d = a_p*xl + b_p*yl + c_p
	 * 左右视图满足:(1) xr = xl - d_p; (2) yr = yl; (3) 视差符号相反(本代码左视差为正值,右视差为负值)
	 * 代入左视图视差平面方程就可得到右视图坐标系下的平面方程: d = -a_p*xr - b_p*yr - (c_p+a_p*d_p)
	 * 右至左同理
	 * \param x		像素x坐标
	 * \param y 	像素y坐标
	 * \return 转换后的平面
	 */
	DisparityPlane to_another_view(const sint32& x, const sint32& y) const
	{
		const float32 d = to_disparity(x, y);
		return { -p.x, -p.y, -p.z - p.x * d };
	}

	// operator ==
	bool operator==(const DisparityPlane& v) const {
		return p == v.p;
	}
	// operator !=
	bool operator!=(const DisparityPlane& v) const {
		return p != v.p;
	}
};

好了同学们,本篇就到这吧,虽然篇幅较长,但是似乎文字并不多,对着代码来看,我想不会占用多少时间,咱们下篇来解读算法的具体实现代码,博主还会做一些实验,借助实验图来帮助大家加深理解。

同学们拜拜!

博主简介:
Ethan Li 李迎松
武汉大学 摄影测量与遥感专业博士

主方向立体匹配、三维重建

2019年获测绘科技进步一等奖(省部级)

爱三维,爱分享,爱开源
GitHub: https://github.com/ethan-li-coding
邮箱:ethan.li.whu@gmail.com

个人微信:
荐
                                                        【码上实战】【立体匹配系列】经典PatchMatch: (2)主类
欢迎交流!

喜欢博主的文章不妨关注一下博主的博客,感谢!
博客主页:https://blog.csdn.net/rs_lys

本文地址:https://blog.csdn.net/rs_lys/article/details/107251788