下载源码,点击进入: Github - PatchMatchStereo
欢迎同学们在Github项目里讨论,如果觉得博主代码质量不错,给颗小星星,以及Follow Me!感激不尽!
算法效果图镇楼:
![]() |
![]() |
![]() |
![]() |
上一篇博客框架中,我们已经从最顶层的角度理清了整个算法的思路、框架、步骤,本篇开始我们就进入实质性的代码分析。
本篇的内容是PatchMatchStereo(后面简称PMS)的主类分析。
主类,即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的步骤图,它们主要包括:
还有一些其他的细枝末叶不用细说,例如计算梯度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();
成员变量保存着算法需要在算法周期内完全持有的数据,数据是算法的内核,算法的运算过程便是在对数据不断的进行数学/逻辑运算及存取。
我们需要哪些数据呢?
详见代码:
/** \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之类的),还有几个陌生的类型:
它们三个是代码里自定义的类型,定义成结构体那自然是为了方便,它们都放在文件 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;
}
};
视差平面是一个较为核心的结构体,贯穿全代码,它可以通过视差和法线来构建,并包含以下功能:
将视差平面设计成一个结构体会增加代码的可读性,因为代码中会频繁的获取像素的视差、较频繁的获取平面的法线,把他们都写成一个函数,让代码更加简洁和易懂。
/**
* \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