点击上方“计算机视觉life”,选择“星标”
快速获得最新干货
作者李迎松授权发布,武汉大学 摄影测量与遥感专业 博士
https://ethanli.blog.csdn.net/article/details/105065660
一直就想做这样的专题,因为自己是一名算法工程师,而算法落地对算法工程师来说是职责和能力之体现,前面有一个专题是专门介绍Semi-Global Matching(SGM)双目立体匹配算法的理论知识,再做一个编码的专题我觉得算是一个必要的补充。话不多少,咱们开始吧!
(注1:代码已实时更新到GitHub上,随着专题的完成,Github代码也将同步完成。仓库地址:
https://github.com/ethan-li-coding/SemiGlobalMatching.git,感兴趣的话,点下star,有更新会实时通知到你的个人中心!)
(注2:这是一个专题,我会一步步介绍SGM的实现,让读者循序渐进的掌握SGM编码实现)
码上教学系列
【立体匹配系列】经典SGM:(1)框架与类设计
【立体匹配系列】经典SGM:(2)代价计算
【立体匹配系列】经典SGM:(3)代价聚合
【立体匹配系列】经典SGM:(4)代价聚合2
【立体匹配系列】经典SGM:(5)视差优化
【立体匹配系列】经典SGM:(6)视差填充
【立体匹配系列】经典SGM:(7)弱纹理优化
代码已同步于Github开源项目:
Github/SemiGlobalMatching
在编码前,我不能急于动手,先在脑中思考下算法模型,然后搭建一个大概的框架,无论框架是否合理,这一步我想是合理的,他是编码的指南,也是思维的整理和复习。我在编写了多年代码后,慢慢形成了这个习惯,这让我少走一些弯路,编码思路更清晰。
首先是SGM的算法步骤:
其次是代码的框架:
框架的解释:既然是基于C++,那肯定是以类的方式,咱们把SGM匹配当做一个对象类,匹配所需要的数据和参数为成员变量,匹配所执行的步骤当做成员函数,这里分为公有和私有,公有成员函数是调用层可调用的接口,我开放了三个:初始化、执行匹配以及重设置,这当然是调用层所喜欢的方式,完成功能的前提下调用越简单越好。具体的算法步骤我放到私有成员函数里,表示这些接口调用层不需要关心,而算法层需要包装成接口以让代码逻辑清晰而整洁。当然,如果是实际工作中,我的建议是:外面再以类的方式封装一个接口层,让调用者只能看到两个公有函数,其他全部看不见,这样可以做到算法保密,而简洁的接口调用者也用的方便舒心。(见此前博客C++学习 | C++ Implement的使用)
有了算法步骤和编码框架,我们就可以愉快的开始编码啦!
前面我们确定了框架和步骤,这一步,当然趁热把类给设计出来,不管设计的好不好,后面我们可以即时修改,但是有一个初步的类设计,会让编码逻辑更清晰。
系统:Windows 10
编码软件(IDE):Microsoft Visual Studio 2015(版本无所谓,不过建议2010以上,后面我简称VS)
第一步:打开VS,新建一个控制台工程(当然也可以新建动态库工程,不过一开始用控制台会更方便测试,后面可以随意修改成动态库工程),然后新建一个空的类:SemiGlobalMatching【头文件SemiGlobalMatching.h/源文件SemiGlobalMatching.cpp】。
如下所示:
头文件SemiGlobalMatching.h
#pragma once
class SemiGlobalMatching
{
public:
SemiGlobalMatching();
~SemiGlobalMatching();
};
源文件SemiGlobalMatching.cpp
#include "stdafx.h"
#include "SemiGlobalMatching.h"
SemiGlobalMatching::SemiGlobalMatching()
{
}
SemiGlobalMatching::~SemiGlobalMatching()
{
}
第二步:在类中添加相关结构体、成员变量、成员函数。
在写功能性代码之前,我要为一些基本类型创建别名,如下:
typedef int8_t sint8; // 有符号8位整数
typedef uint8_t uint8; // 无符号8位整数
typedef int16_t sint16; // 有符号16位整数
typedef uint16_t uint16; // 无符号16位整数
typedef int32_t sint32; // 有符号32位整数
typedef uint32_t uint32; // 无符号32位整数
typedef int64_t sint64; // 有符号64位整数
typedef uint64_t uint64; // 无符号64位整数
typedef float float32; // 单精度浮点
typedef double float64; // 双精度浮点
它的主要用处是防止不同编译器或平台的基础类型位数不一致而需要大量修改代码(实际上这种情况很少见),以及便于理解。大家写代码不用这样的方式也OK。
好了,开始设计类吧!
首先是相关结构体,对一个算法来说,参数是一个很重要的部分,他是调用层可控的点之一,通过设置不同的参数,调用层可以得到不同的算法结果,研究人员希望参数可调性尽量高,而产品前端则希望参数在完成目标的情况下尽可能简洁。
我们为SGM设计了一个参数结构体,包含有5个参数,如下:
/** \brief SGM参数结构体 */
struct SGMOption {
uint8 num_paths; // 聚合路径数
sint32 min_disparity; // 最小视差
sint32 max_disparity; // 最大视差
// P1,P2
// P2 = P2_int / (Ip-Iq)
sint32 p1; // 惩罚项参数P1
sint32 p2_int; // 惩罚项参数P2
SGMOption(): num_paths(8), min_disparity(0), max_disparity(640), p1(10), p2_int(150) {
}
};
注释中含有参数的解释,后面在代码编写说明的过程中我也会进行解释。在此就不在赘述。
接下来看成员函数部分:
首先是公有成员函数,这部分是对调用端开放的:
第一个成员函数:初始化Initialize。有人肯定有疑问,为什么要加初始化呢,直接匹配不就完事了么?其实原因在于一方面算法需要有一些准备工作,比如参数初始化、加密检测之类的,有一个初始化接口可以逻辑上更加清晰,可以放入很多不适用于算法执行阶段做的工作;另一个很重要的原因是我们算法一般讲究效率,很多算法他都会有内存分配的操作,而有部分内存分配其实是可以复用的,比如说如果算法内部需要使用一个和影像等尺寸的标记位数组,用来标记像素的状态,而算法执行第一个像对和第二个像对其实都可以重复利用这个标记位数组,只要我们开始前把标记位清零就行,而如果我们不在初始化函数里预先开辟好,那么每次执行匹配都要重新开辟这块内存,无疑会造成效率降低。所以这形成了我的一个习惯,给算法都会加一个初始化步骤。
我们为SemiGlobalMatching类初始化函数设计的参数有影像高、宽、SGM参数。用来预分配可复用的内存块,及预设定SGM匹配的一些参数。
/**
* \brief 类的初始化,完成一些内存的预分配、参数的预设置等
* \param width 输入,核线像对影像宽
* \param height 输入,核线像对影像高
* \param option 输入,SemiGlobalMatching参数
*/
bool Initialize(const uint32& width, const uint32& height, const SGMOption& option);
第二个成员函数:执行匹配Match。这个就不用多说了,它就是匹配类的核心了。
匹配函数的参数是左右影像的数据指针、输出的左影像视差图指针(需要预先开辟内存)。
/**
* \brief 执行匹配
* \param img_left 输入,左影像数据指针
* \param img_right 输入,右影像数据指针
* \param disp_left 输出,左影像视差图指针,预先分配和影像等尺寸的内存空间
*/
bool Match(const uint8* img_left, const uint8* img_right, float32* disp_left);
第三个成员函数:重设Reset。有同学可能有疑问,为啥放个这函数,原因是Initialize/Match方式虽然可以明显避免可复用内存的重复分配,当影像尺寸不变时(比如工程中只涉及到一种宽高影像)很有用,但是遇到影像尺寸会变化的项目时,就必须得提供接口来修改影像尺寸了,所以我们就加入了Reset,在影像尺寸或者匹配参数变化时重新做一些预备工作。
/**
* \brief 重设
* \param width 输入,核线像对影像宽
* \param height 输入,核线像对影像高
* \param option 输入,SemiGlobalMatching参数
*/
bool Reset(const uint32& width, const uint32& height, const SGMOption& option);
其次是私有成员函数,这部分是不对调用端开放的,主要是SGM的子步骤函数:
/** \brief Census变换 */
void CensusTransform() const;
/** \brief 代价计算 */
void ComputeCost() const;
/** \brief 代价聚合 */
void CostAggregation() const;
/** \brief 视差计算 */
void ComputeDisparity() const;
/** \brief 一致性检查 */
void LRCheck() const;
最后是成员变量,这部分设计成私有类型对类的保护会更好,如果需要获取/设置某个成员的值,可以通过增加公有的成员函数get/set来获取或者设置,如下:
/** \brief SGM参数 */
SGMOption option_;
/** \brief 影像宽 */
sint32 width_;
/** \brief 影像高 */
sint32 height_;
/** \brief 左影像数据 */
uint8* img_left_;
/** \brief 右影像数据 */
uint8* img_right_;
/** \brief 左影像census值 */
uint32* census_left_;
/** \brief 右影像census值 */
uint32* census_right_;
/** \brief 初始匹配代价 */
uint16* cost_init_;
/** \brief 聚合匹配代价 */
uint16* cost_aggr_;
/** \brief 左影像视差图 */
float32* disp_left_;
/** \brief 是否初始化标志 */
bool is_initialized_;
好了,SemiGlobalMatching类的头文件就基本完成了,接下来就是各个函数实现了。
最终的头文件如下:
#pragma once
#include <cstdint>
typedef int8_t sint8; // 有符号8位整数
typedef uint8_t uint8; // 无符号8位整数
typedef int16_t sint16; // 有符号16位整数
typedef uint16_t uint16; // 无符号16位整数
typedef int32_t sint32; // 有符号32位整数
typedef uint32_t uint32; // 无符号32位整数
typedef int64_t sint64; // 有符号64位整数
typedef uint64_t uint64; // 无符号64位整数
typedef float float32; // 单精度浮点
typedef double float64; // 双精度浮点
class SemiGlobalMatching
{
public:
SemiGlobalMatching();
~SemiGlobalMatching();
/** \brief SGM参数结构体 */
struct SGMOption {
uint8 num_paths; // 聚合路径数
sint32 min_disparity; // 最小视差
sint32 max_disparity; // 最大视差
// P1,P2
// P2 = P2_int / (Ip-Iq)
sint32 p1; // 惩罚项参数P1
sint32 p2_int; // 惩罚项参数P2
SGMOption(): num_paths(8), min_disparity(0), max_disparity(640), p1(10), p2_int(150) {
}
};
public:
/**
* \brief 类的初始化,完成一些内存的预分配、参数的预设置等
* \param width 输入,核线像对影像宽
* \param height 输入,核线像对影像高
* \param option 输入,SemiGlobalMatching参数
*/
bool Initialize(const uint32& width, const uint32& height, const SGMOption& option);
/**
* \brief 执行匹配
* \param img_left 输入,左影像数据指针
* \param img_right 输入,右影像数据指针
* \param disp_left 输出,左影像深度图指针,预先分配和影像等尺寸的内存空间
*/
bool Match(const uint8* img_left, const uint8* img_right, float32* disp_left);
/**
* \brief 重设
* \param width 输入,核线像对影像宽
* \param height 输入,核线像对影像高
* \param option 输入,SemiGlobalMatching参数
*/
bool Reset(const uint32& width, const uint32& height, const SGMOption& option);
private:
/** \brief Census变换 */
void CensusTransform() const;
/** \brief 代价计算 */
void ComputeCost() const;
/** \brief 代价聚合 */
void CostAggregation() const;
/** \brief 视差计算 */
void ComputeDisparity() const;
/** \brief 一致性检查 */
void LRCheck() const;
private:
/** \brief SGM参数 */
SGMOption option_;
/** \brief 影像宽 */
sint32 width_;
/** \brief 影像高 */
sint32 height_;
/** \brief 左影像数据 */
uint8* img_left_;
/** \brief 右影像数据 */
uint8* img_right_;
/** \brief 左影像census值 */
uint32* census_left_;
/** \brief 右影像census值 */
uint32* census_right_;
/** \brief 初始匹配代价 */
uint8* cost_init_;
/** \brief 聚合匹配代价 */
uint16* cost_aggr_;
/** \brief 左影像视差图 */
float32* disp_left_;
/** \brief 是否初始化标志 */
bool is_initialized_;
};
本章就介绍到这里,接下来我们将学习如何实现SGM立体匹配类的成员函数,让类真正起作用,并做一些匹配实验,直观的感受立体匹配的魅力。
欢迎加入公众号读者群一起和同行交流,目前有SLAM、三维视觉、传感器、自动驾驶、计算摄影、检测、分割、识别、医学影像、GAN、算法竞赛等微信群(以后会逐渐细分),请扫描下面微信号加群,备注:”昵称+学校/公司+研究方向“,例如:”张三 + 上海交大 + 视觉SLAM“。请按照格式备注,否则不予通过。添加成功后会根据研究方向邀请进入相关微信群。请勿在群内发送广告,否则会请出群,谢谢理解~
投稿、合作也欢迎联系:simiter@126.com
长按关注计算机视觉life