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

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

算法效果图镇楼:

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

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

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

    • 主类 PatchMatchStereo
    • 公有函数
    • 私有函数
    • 成员变量

主类 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 / y x/y x/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
邮箱:[email protected]

个人微信:

欢迎交流!

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

你可能感兴趣的:(#,立体匹配,三维重建)