OpenCV 4.11.0
开源计算机视觉库
加载中…
搜索中…
未找到匹配项
解码格雷码模式教程

目标

在本教程中,您将学习如何使用GrayCodePattern类来:

  • 解码先前获取的格雷码模式。
  • 生成视差图。
  • 生成点云。

代码

/*M///////////////////////////////////////////////////////////////////////////////////////
//
// 重要:下载、复制、安装或使用前请阅读。
//
// 下载、复制、安装或使用本软件即表示您同意本许可协议。
// 如果你不同意本许可协议,请不要下载、安装、
// 复制或使用本软件。
//
//
// 许可协议
// 适用于开源计算机视觉库
//
// 版权所有 (C) 2015,OpenCV 基金会,保留所有权利。
// 第三方版权归其各自所有者所有。
//
// 允许以源代码和二进制形式重新分发和使用本软件,无论是否修改,
// 只要满足以下条件:
//
// * 源代码的重新分发必须保留上述版权声明、
// 此条件列表以及以下免责声明。
//
// * 二进制形式的重新分发必须在文档中复制上述版权声明、
// 此条件列表以及以下免责声明
// 和/或与分发提供的其他材料。
//
// * 未经事先明确的书面许可,不得使用版权持有人的名称来认可或推广
// 源于本软件的衍生产品。
//
// 本软件由版权持有人和贡献者“按原样”提供,并且
// 任何明示或暗示的担保,包括但不限于对适销性和
// 适用于特定用途的暗示担保均被排除。
// 英特尔公司或贡献者在任何情况下均不对任何直接的、
// 间接的、偶然的、特殊的、惩罚性的或后果性损害赔偿负责
// (包括但不限于替代商品或服务的采购;
// 使用损失、数据损失或利润损失;或业务中断)无论其成因如何
// 以及任何责任理论,无论是基于合同、严格责任、
// 还是侵权行为(包括疏忽或其他原因)以任何方式产生于
// 本软件的使用,即使已被告知此类损害的可能性。
//
//M*/
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/opencv_modules.hpp>
// (如果您没有构建opencv_viz模块,您将只能看到视差图像)
#ifdef HAVE_OPENCV_VIZ
#include <opencv2/viz.hpp>
#endif
using namespace std;
using namespace cv;
static const char* keys =
{ "{@images_list | | 保存已捕获图案图像的图像列表}"
"{@calib_param_path | | 校准参数}"
"{@proj_width | | 用于获取图案的投影仪宽度}"
"{@proj_height | | 用于获取图案的投影仪高度}"
"{@white_thresh | | 白色阈值高度(可选)}"
"{@black_thresh | | 黑色阈值(可选)}" };
static void help()
{
cout << "\n此示例演示如何使用“结构光模块”解码先前获取的格雷码模式,生成点云"
"\n调用:\n"
"./example_structured_light_pointcloud <images_list> <calib_param_path> <proj_width> <proj_height> <white_thresh> <black_thresh>\n"
<< endl;
}
static bool readStringList( const string& filename, vector<string>& l )
{
l.resize( 0 );
FileStorage fs( filename, FileStorage::READ );
if( !fs.isOpened() )
{
cerr << "无法打开 " << filename << endl;
return false;
}
FileNode n = fs.getFirstTopLevelNode();
if( n.type() != FileNode::SEQ )
{
cerr << "cam 1 图像不是序列!失败" << endl;
return false;
}
FileNodeIterator it = n.begin(), it_end = n.end();
for( ; it != it_end; ++it )
{
l.push_back( ( string ) *it );
}
n = fs["cam2"];
if( n.type() != FileNode::SEQ )
{
cerr << "cam 2 图像不是序列!失败" << endl;
return false;
}
it = n.begin(), it_end = n.end();
for( ; it != it_end; ++it )
{
l.push_back( ( string ) *it );
}
if( l.size() % 2 != 0 )
{
cout << "错误:图像列表包含奇数(非偶数)个元素\n";
return false;
}
return true;
}
int main( int argc, char** argv )
{
CommandLineParser parser( argc, argv, keys );
String images_file = parser.get<String>( 0 );
String calib_file = parser.get<String>( 1 );
params.width = parser.get<int>( 2 );
params.height = parser.get<int>( 3 );
如果 (images_file.empty() || calib_file.empty() || params.width < 1 || params.height < 1 || argc < 5 || argc > 7 )
{
help();
return; -1;
}
// 使用 params 设置 GraycodePattern
Ptr<structured_light::GrayCodePattern> graycode = structured_light::GrayCodePattern::create( params );
size_t white_thresh = 0;
size_t black_thresh = 0;
如果 (argc == 7)
{
// 如果传入,则设置黑白阈值,否则使用默认值
white_thresh = parser.get<unsigned>( 4 );
black_thresh = parser.get<unsigned>( 5 );
graycode->setWhiteThreshold( white_thresh );
graycode->setBlackThreshold( black_thresh );
}
vector<string> imagelist;
bool ok = readStringList( images_file, imagelist );
如果 (!ok || imagelist.empty() )
{
cout << "无法打开 " << images_file << " 或字符串列表为空" << endl;
help();
return; -1;
}
FileStorage fs( calib_file, FileStorage::READ );
if( !fs.isOpened() )
{
cout << "无法打开标定数据文件。" << endl;
help();
return; -1;
}
// 加载标定参数
Mat cam1intrinsics, cam1distCoeffs, cam2intrinsics, cam2distCoeffs, R, T;
fs["cam1_intrinsics"] >> cam1intrinsics;
fs["cam2_intrinsics"] >> cam2intrinsics;
fs["cam1_distorsion"] >> cam1distCoeffs;
fs["cam2_distorsion"] >> cam2distCoeffs;
fs["R"] >> R;
fs["T"] >> T;
cout << "cam1intrinsics" << endl << cam1intrinsics << endl;
cout << "cam1distCoeffs" << endl << cam1distCoeffs << endl;
cout << "cam2intrinsics" << endl << cam2intrinsics << endl;
cout << "cam2distCoeffs" << endl << cam2distCoeffs << endl;
cout << "T" << endl << T << endl << "R" << endl << R << endl;
如果 ((!R.data) || (!T.data) || (!cam1intrinsics.data) || (!cam2intrinsics.data) || (!cam1distCoeffs.data) || (!cam2distCoeffs.data) )
{
cout << "无法加载相机标定参数" << endl;
help();
return; -1;
}
size_t numberOfPatternImages = graycode->getNumberOfPatternImages();
vector<vector<Mat> > captured_pattern;
captured_pattern.resize( 2 );
captured_pattern[0].resize( numberOfPatternImages );
captured_pattern[1].resize( numberOfPatternImages );
Mat color = imread( imagelist[numberOfPatternImages], IMREAD_COLOR );
Size imagesSize = color.size();
// 立体校正
cout << "正在校正图像..." << endl;
Mat R1, R2, P1, P2, Q;
Rect validRoi[2];
stereoRectify( cam1intrinsics, cam1distCoeffs, cam2intrinsics, cam2distCoeffs, imagesSize, R, T, R1, R2, P1, P2, Q, 0,
-1, imagesSize, &validRoi[0], &validRoi[1] );
Mat map1x, map1y, map2x, map2y;
initUndistortRectifyMap( cam1intrinsics, cam1distCoeffs, R1, P1, imagesSize, CV_32FC1, map1x, map1y );
initUndistortRectifyMap( cam2intrinsics, cam2distCoeffs, R2, P2, imagesSize, CV_32FC1, map2x, map2y );
// 加载图案图像
for (size_t i = 0; i < numberOfPatternImages; i++)
{
captured_pattern[0][i] = imread( imagelist[i], IMREAD_GRAYSCALE );
captured_pattern[1][i] = imread( imagelist[i + numberOfPatternImages + 2], IMREAD_GRAYSCALE );
如果 ((!captured_pattern[0][i].data) || (!captured_pattern[1][i].data) )
{
cout << "图像为空" << endl;
help();
return; -1;
}
remap( captured_pattern[1][i], captured_pattern[1][i], map1x, map1y, INTER_NEAREST, BORDER_CONSTANT, Scalar() );
remap( captured_pattern[0][i], captured_pattern[0][i], map2x, map2y, INTER_NEAREST, BORDER_CONSTANT, Scalar() );
}
cout << "完成" << endl;
vector<Mat> blackImages;
vector<Mat> whiteImages;
blackImages.resize( 2 );
whiteImages.resize( 2 );
// 加载计算阴影所需的图像(全白 + 全黑)
cvtColor( color, whiteImages[0], COLOR_RGB2GRAY );
whiteImages[1] = imread( imagelist[2 * numberOfPatternImages + 2], IMREAD_GRAYSCALE );
blackImages[0] = imread( imagelist[numberOfPatternImages + 1], IMREAD_GRAYSCALE );
blackImages[1] = imread( imagelist[2 * numberOfPatternImages + 2 + 1], IMREAD_GRAYSCALE );
remap( color, color, map2x, map2y, INTER_NEAREST, BORDER_CONSTANT, Scalar() );
remap( whiteImages[0], whiteImages[0], map2x, map2y, INTER_NEAREST, BORDER_CONSTANT, Scalar() );
remap( whiteImages[1], whiteImages[1], map1x, map1y, INTER_NEAREST, BORDER_CONSTANT, Scalar() );
remap( blackImages[0], blackImages[0], map2x, map2y, INTER_NEAREST, BORDER_CONSTANT, Scalar() );
remap( blackImages[1], blackImages[1], map1x, map1y, INTER_NEAREST, BORDER_CONSTANT, Scalar() );
cout << endl << "正在解码图案..." << endl;
Mat disparityMap;
bool decoded = graycode->decode( captured_pattern, disparityMap, blackImages, whiteImages,
structured_light::DECODE_3D_UNDERWORLD );
如果 (decoded)
{
cout << endl << "图案解码完成" << endl;
// 为更好地可视化结果,将计算出的视差应用颜色映射
double min;
double max;
minMaxIdx(disparityMap, &min, &max);
Mat cm_disp, scaledDisparityMap;
cout << "disp min " << min << endl << "disp max " << max << endl;
convertScaleAbs( disparityMap, scaledDisparityMap, 255 / ( max - min ) );
applyColorMap( scaledDisparityMap, cm_disp, COLORMAP_JET );
// 显示结果
resize( cm_disp, cm_disp, Size( 640, 480 ), 0, 0, INTER_LINEAR_EXACT );
imshow( "cm disparity m", cm_disp );
// 计算点云
Mat pointcloud;
disparityMap.convertTo( disparityMap, CV_32FC1 );
reprojectImageTo3D( disparityMap, pointcloud, Q, true, -1 );
// 计算掩码以去除背景
Mat dst, thresholded_disp;
threshold( scaledDisparityMap, thresholded_disp, 0, 255, THRESH_OTSU + THRESH_BINARY );
resize( thresholded_disp, dst, Size( 640, 480 ), 0, 0, INTER_LINEAR_EXACT );
imshow( "threshold disp otsu", dst );
#ifdef HAVE_OPENCV_VIZ
// 将掩码应用于点云
Mat pointcloud_tresh, color_tresh;
pointcloud.copyTo( pointcloud_tresh, thresholded_disp );
color.copyTo( color_tresh, thresholded_disp );
// 在viz上显示点云
viz::Viz3d myWindow( "带有颜色的点云" );
myWindow.setBackgroundMeshLab();
myWindow.showWidget( "coosys", viz::WCoordinateSystem() );
myWindow.showWidget( "pointcloud", viz::WCloud( pointcloud_tresh, color_tresh ) );
myWindow.showWidget( "text2d", viz::WText( "点云", Point(20, 20), 20, viz::Color::green() ) );
myWindow.spin();
#endif // HAVE_OPENCV_VIZ
}
return; 0;
}
用于命令行解析。
定义 utility.hpp:890
用于迭代序列和映射。
定义 persistence.hpp:595
文件存储节点类。
定义 persistence.hpp:441
FileNodeIterator begin() const
返回指向第一个节点元素的迭代器
FileNodeIterator end() const
返回指向最后一个节点元素之后元素的迭代器
int type() const
返回节点的类型。
XML/YAML/JSON 文件存储类,封装了写入或读取所需的所有信息...
定义 persistence.hpp:261
n维密集数组类
定义 mat.hpp:829
MatSize size
定义 mat.hpp:2177
void copyTo(OutputArray m) const
将矩阵复制到另一个矩阵。
uchar * data
指向数据的指针
定义 mat.hpp:2157
void convertTo(OutputArray m, int rtype, double alpha=1, double beta=0) const
将数组转换为另一种数据类型,并可以选择缩放。
二维矩形的模板类。
定义 types.hpp:444
用于指定图像或矩形大小的模板类。
定义 types.hpp:335
Viz3d 类表示一个 3D 可视化窗口。此类是隐式共享的。
定义 viz3d.hpp:68
点云。
定义 widgets.hpp:681
复合部件。
定义 widgets.hpp:514
文本和图像部件。
定义 widgets.hpp:408
void reprojectImageTo3D(InputArray disparity, OutputArray _3dImage, InputArray Q, bool handleMissingValues=false, int ddepth=-1)
将视差图像重投影到 3D 空间。
void stereoRectify(InputArray cameraMatrix1, InputArray distCoeffs1, InputArray cameraMatrix2, InputArray distCoeffs2, Size imageSize, InputArray R, InputArray T, OutputArray R1, OutputArray R2, OutputArray P1, OutputArray P2, OutputArray Q, int flags=CALIB_ZERO_DISPARITY, double alpha=-1, Size newImageSize=Size(), Rect *validPixROI1=0, Rect *validPixROI2=0)
计算已校准立体摄像机的每个头的校正变换。
void initUndistortRectifyMap(InputArray cameraMatrix, InputArray distCoeffs, InputArray R, InputArray newCameraMatrix, Size size, int m1type, OutputArray map1, OutputArray map2)
计算去畸变和校正变换映射。
void convertScaleAbs(InputArray src, OutputArray dst, double alpha=1, double beta=0)
缩放、计算绝对值并将结果转换为 8 位。
void minMaxIdx(InputArray src, double *minVal, double *maxVal=0, int *minIdx=0, int *maxIdx=0, InputArray mask=noArray())
查找数组中的全局最小值和最大值。
std::string String
定义 cvstd.hpp:151
std::shared_ptr< _Tp > Ptr
定义 cvstd_wrapper.hpp:23
#define CV_32FC1
定义 interface.h:118
void imshow(const String &winname, InputArray mat)
在指定的窗口中显示图像。
int waitKey(int delay=0)
等待按下按键。
CV_EXPORTS_W Mat imread(const String &filename, int flags=IMREAD_COLOR_BGR)
从文件加载图像。
void cvtColor(InputArray src, OutputArray dst, int code, int dstCn=0, AlgorithmHint hint=cv::ALGO_HINT_DEFAULT)
将图像从一个颜色空间转换为另一个颜色空间。
void applyColorMap(InputArray src, OutputArray dst, int colormap)
在给定图像上应用 GNU Octave/MATLAB 等效颜色映射。
double threshold(InputArray src, OutputArray dst, double thresh, double maxval, int type)
将固定级别阈值应用于每个数组元素。
void resize(InputArray src, OutputArray dst, Size dsize, double fx=0, double fy=0, int interpolation=INTER_LINEAR)
调整图像大小。
void remap(InputArray src, OutputArray dst, InputArray map1, InputArray map2, int interpolation, int borderMode=BORDER_CONSTANT, const Scalar &borderValue=Scalar())
将通用几何变换应用于图像。
int main(int argc, char *argv[])
定义 highgui_qt.cpp:3
PyParams params(const std::string &tag, const std::string &model, const std::string &weights, const std::string &device)
定义 core.hpp:107
STL 命名空间。
结构光模式构造函数的参数。
定义 graycodepattern.hpp:77

解释

首先,必须将所需参数传递给程序。第一个是先前获取的图案图像的名称列表,存储在如下组织的 .yaml 文件中:

%YAML:1.0
cam1
- "/data/pattern_cam1_im1.png"
- "/data/pattern_cam1_im2.png"
..............
- "/data/pattern_cam1_im42.png"
- "/data/pattern_cam1_im43.png"
- "/data/pattern_cam1_im44.png"
cam2
- "/data/pattern_cam2_im1.png"
- "/data/pattern_cam2_im2.png"
..............
- "/data/pattern_cam2_im42.png"
- "/data/pattern_cam2_im43.png"
- "/data/pattern_cam2_im44.png"

例如,本教程中使用的数据集是使用分辨率为 1280x800 的投影仪获取的,因此使用两个摄像头捕获了 42 幅图案图像(从 1 到 42)+ 1 幅白色图像(43 号)和 1 幅黑色图像(44 号)。

然后,将存储在另一个 .yml 文件中的相机校准参数,以及用于投影图案的投影仪的宽度和高度,以及可选的白色和黑色阈值,传递给教程程序。

这样,可以使用投影仪在图案采集过程中使用的宽度和高度来设置GrayCodePattern类的参数,并可以创建一个指向GrayCodePattern对象的指针

....
params.width = parser.get<int>( 2 );
params.height = parser.get<int>( 3 );
....
// 使用 params 设置 GraycodePattern
Ptr<structured_light::GrayCodePattern> graycode = structured_light::GrayCodePattern::create( params );

如果白色和黑色阈值作为参数传递(这些阈值会影响解码像素的数量),则可以设置它们的值,否则算法将使用默认值。

size_t white_thresh = 0;
size_t black_thresh = 0;
如果 (argc == 7)
{
// 如果传入,则设置黑白阈值,否则使用默认值
white_thresh = parser.get<size_t>( 4 );
black_thresh = parser.get<size_t>( 5 );
graycode->setWhiteThreshold( white_thresh );
graycode->setBlackThreshold( black_thresh );
}

此时,要使用GrayCodePattern类的decode方法,必须将获取的图案图像存储在Mat的向量向量中。外部向量的大小为二,因为有两个摄像头:第一个向量存储从左侧摄像头捕获的图案图像,第二个向量存储从右侧摄像头捕获的图案图像。显然,两个摄像头的图案图像数量相同,可以使用 getNumberOfPatternImages() 方法检索。

size_t numberOfPatternImages = graycode->getNumberOfPatternImages();
vector<vector<Mat> > captured_pattern;
captured_pattern.resize( 2 );
captured_pattern[0].resize( numberOfPatternImages );
captured_pattern[1].resize( numberOfPatternImages );
.....
for( size_t i = 0; i < numberOfPatternImages; i++ )
{
captured_pattern[0][i] = imread( imagelist[i], IMREAD_GRAYSCALE );
captured_pattern[1][i] = imread( imagelist[i + numberOfPatternImages + 2], IMREAD_GRAYSCALE );
......
}

至于黑白图像,必须将它们存储在两个不同的Mat向量中。

vector<Mat> blackImages;
vector<Mat> whiteImages;
blackImages.resize( 2 );
whiteImages.resize( 2 );
// 加载计算阴影所需的图像(全白 + 全黑)
cvtColor( color, whiteImages[0], COLOR_RGB2GRAY );
whiteImages[1] = imread( imagelist[2 * numberOfPatternImages + 2], IMREAD_GRAYSCALE );
blackImages[0] = imread( imagelist[numberOfPatternImages + 1], IMREAD_GRAYSCALE );
blackImages[1] = imread( imagelist[2 * numberOfPatternImages + 2 + 1], IMREAD_GRAYSCALE );

重要的是要强调,所有图像(图案图像、黑白图像)都必须加载为灰度图像,并在传递给 decode 方法之前进行校正。

// 立体校正
cout << "正在校正图像..." << endl;
Mat R1, R2, P1, P2, Q;
Rect validRoi[2];
stereoRectify( cam1intrinsics, cam1distCoeffs, cam2intrinsics, cam2distCoeffs, imagesSize, R, T, R1, R2, P1, P2, Q, 0,
-1, imagesSize, &validRoi[0], &validRoi[1] );
Mat map1x, map1y, map2x, map2y;
initUndistortRectifyMap( cam1intrinsics, cam1distCoeffs, R1, P1, imagesSize, CV_32FC1, map1x, map1y );
initUndistortRectifyMap( cam2intrinsics, cam2distCoeffs, R2, P2, imagesSize, CV_32FC1, map2x, map2y );
........
for (size_t i = 0; i < numberOfPatternImages; i++)
{
........
remap( captured_pattern[1][i], captured_pattern[1][i], map1x, map1y, INTER_NEAREST, BORDER_CONSTANT, Scalar() );
remap( captured_pattern[0][i], captured_pattern[0][i], map2x, map2y, INTER_NEAREST, BORDER_CONSTANT, Scalar() );
}
........
remap( color, color, map2x, map2y, INTER_NEAREST, BORDER_CONSTANT, Scalar() );
remap( whiteImages[0], whiteImages[0], map2x, map2y, INTER_NEAREST, BORDER_CONSTANT, Scalar() );
remap( whiteImages[1], whiteImages[1], map1x, map1y, INTER_NEAREST, BORDER_CONSTANT, Scalar() );
remap( blackImages[0], blackImages[0], map2x, map2y, INTER_NEAREST, BORDER_CONSTANT, Scalar() );
remap( blackImages[1], blackImages[1], map1x, map1y, INTER_NEAREST, BORDER_CONSTANT, Scalar() );

这样就可以调用decode方法来解码图案并生成相应的视差图,该视差图在第一个摄像头(左侧)上计算。

Mat disparityMap;
bool decoded = graycode->decode(captured_pattern, disparityMap, blackImages, whiteImages,
structured_light::DECODE_3D_UNDERWORLD);

为了更好地显示结果,将颜色图应用于计算出的视差。

double min;
double max;
minMaxIdx(disparityMap, &min, &max);
Mat cm_disp, scaledDisparityMap;
cout << "disp min " << min << endl << "disp max " << max << endl;
convertScaleAbs( disparityMap, scaledDisparityMap, 255 / ( max - min ) );
applyColorMap( scaledDisparityMap, cm_disp, COLORMAP_JET );
// 显示结果
resize( cm_disp, cm_disp, Size( 640, 480 ) );
imshow( "cm disparity m", cm_disp )

此时,可以使用reprojectImageTo3D方法生成点云,注意将计算出的视差转换为CV_32FC1 Mat(decode方法计算CV_64FC1视差图)。

Mat pointcloud;
disparityMap.convertTo( disparityMap, CV_32FC1 );
reprojectImageTo3D( disparityMap, pointcloud, Q, true, -1 );

然后计算一个去除不需要的背景的掩码。

Mat dst, thresholded_disp;
threshold( scaledDisparityMap, thresholded_disp, 0, 255, THRESH_OTSU + THRESH_BINARY );
resize( thresholded_disp, dst, Size( 640, 480 ) );
imshow( "threshold disp otsu", dst );

之前也已将cam1的白色图像加载为彩色图像,以便将物体的颜色映射到其重建的点云上。

Mat color = imread( imagelist[numberOfPatternImages], IMREAD_COLOR );

因此,将背景去除掩码应用于点云和彩色图像。

Mat pointcloud_tresh, color_tresh;
pointcloud.copyTo(pointcloud_tresh, thresholded_disp);
color.copyTo(color_tresh, thresholded_disp);

最后,可以在viz上显示扫描对象的计算出的点云。

viz::Viz3d myWindow( "Point cloud with color");
myWindow.setBackgroundMeshLab();
myWindow.showWidget("coosys", viz::WCoordinateSystem());
myWindow.showWidget( "pointcloud", viz::WCloud( pointcloud_tresh, color_tresh ) );
myWindow.showWidget( "text2d", viz::WText( "点云", Point(20, 20), 20, viz::Color::green() ) );
myWindow.spin();