OpenCV  4.10.0
开源计算机视觉
正在加载...
正在搜索...
无匹配项
解码格雷编码模式教程

目标

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

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

代码

/*M///////////////////////////////////////////////////////////////////////////////////////
//
// 重要事项:在下载、复制、安装或使用前请阅读。
//
// 下载、复制、安装或使用该软件,即表示您同意此许可证。
// 如果你不同意此许可证,请勿下载、安装、
// 复制或使用该软件。
//
//
// 许可协议
// 适用于开源计算机视觉库
//
// Copyright (C) 2015,OpenCV Foundation,保留所有权利。
// 第三方版权归其各自所有者所有。
//
// 允许重新分发和使用源代码和二进制代码,无论是否修改,
// 前提是满足以下条件
//
// * 重新分发源代码时必须保留上述版权声明、
// 此条件列表和以下免责声明。
//
// * 重新分发二进制代码时必须在文档
// 和/或随发行物提供的其他材料中复制上述版权声明、
// 此条件列表和以下免责声明。
//
// * 未经事先书面明确许可,不得使用版权所有者的名称为
// 从本软件派生出的产品背书或做广告。
//
// 此软件由版权所有者和贡献者按“原样”提供,并且
// 任何明示或暗示的保证,包括但不限于隐含的
// 适销性保证和特定用途适用性保证,均被免除。
// 英特尔公司或贡献者在任何情况下均不对任何直接、
// 间接、偶然、特殊、惩罚性或后果性损害负责
// (包括但不限于替代商品或服务采购;
// 使用损失、数据或利润损失;或业务中断),无论其原因如何
// 以及基于任何责任理论,无论是合同、严格责任,
// 或侵权行为(包括疏忽或其他)以任何方式产生的
// 使用本软件,即使已告知此类损害的可能性。
//
//M*/
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/opencv_modules.hpp>
//(如果您没有构建 opencv_viz 模块,您只能看到视差图像)
#ifdef HAVE_OPENCV_VIZ
#include <opencv2/viz.hpp>
#endif
使用命名空间 std;
使用命名空间 cv;
static const char* keys =
{ "{@images_list | | 图像列表,已保存捕获的模式图像}"
"{@calib_param_path | | 校准参数 }"
"{@proj_width | | 用来获取模式的投影仪宽度 }"
"{@proj_height | | 用来获取模式的投影仪高度}"
"{@white_thresh | | 白色阈值高度(可选)}"
"{@black_thresh | | 黑色阈值(可选)}" };
static void help()
{
cout << "\n此示例显示如何使用“Structured Light 模块”解码先前获取的格雷码模式,生成点云"
"\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 );
如果( !fs.isOpened() )
{
cerr << "打开 " << filename << endl 失败;
返回 false;
}
FileNode n = fs.getFirstTopLevelNode();
如果( n.type() != FileNode::SEQ )
{
cerr << "cam 1 图像不是一个序列!失败" << endl;
返回 false;
}
FileNodeIterator it = n.begin(), it_end = n.end();
对于( ; it != it_end; ++it )
{
l.push_back( ( string ) *it );
}
n = fs["cam2"];
如果( n.type() != FileNode::SEQ )
{
cerr << "cam 2 图像不是一个序列!失败" << endl;
返回 false;
}
it = n.begin(), it_end = n.end();
对于( ; it != it_end; ++it )
{
l.push_back( ( string ) *it );
}
如果( l.size() % 2 != 0 )
{
cout << "错误: 图像列表包含奇数个(偶数个)元素\n";
返回 false;
}
返回 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 )
{
帮助();
返回 -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;
帮助();
返回 -1;
}
FileStorage fs( calib_file, FileStorage::READ );
如果( !fs.isOpened() )
{
cout << "打开校准数据文件失败。" << endl;
帮助();
返回 -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;
帮助();
返回 -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 );
// 加载图案图像
对于( 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;
帮助();
返回 -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 视差图像;
bool decoded = 灰度码->解码( 捕获模式, 视差图像, 黑图像, 白图像,
结构光::解码_3D_地下世界 );
if( decoded )
{
cout << endl << "模式解码" << endl;
// 为了更好地展示结果, 向计算的视差应用颜色映射
double min;
double max;
minMaxIdx(视差图像, &min, &max);
Mat cm_disp, scaledDisparityMap;
cout << "disp min " << min << endl << "disp max " << max << endl;
convertScaleAbs( 视差图像, 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;
视差图像.convertTo( 视差图像, CV_32FC1 );
reprojectImageTo3D( 视差图像, 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::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
}
返回 0;
}
设计用于命令行解析。
定义 utility.hpp:820
用于遍历序列和映射。
定义 persistence.hpp:634
文件存储结点类。
定义 persistence.hpp:482
FileNodeIterator begin() const
返回指向第一个结点元素的迭代器
FileNodeIterator end() const
返回指向最后一个结点元素之后的元素的迭代器
int type() const
返回结点的类型。
XML/YAML/JSON 文件存储类,封装了所有必要的写入或读取信息...
定义 persistence.hpp:304
n 维稠密数组类
定义 mat.hpp:812
MatSize size
定义 mat.hpp:2160
void copyTo(OutputArray m) const
将矩阵复制到其他阵列。
uchar * data
指向数据的指针
定义 mat.hpp:2140
void convertTo(OutputArray m, int rtype, double alpha=1, double beta=0) const
将阵列转换为另一种数据类型,并可以进行缩放。
用于 2D 矩形的模板类。
定义 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())
在一个数组中查找全局最小值和最大值。
void min(InputArray src1, InputArray src2, OutputArray dst)
计算两个数组或一个数组和标量的每个元素的最小值。
void max(InputArray src1, InputArray src2, OutputArray dst)
计算两个数组或一个数组和标量的每个元素的最大值。
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)
从文件中加载图像。
void cvtColor(InputArray src, OutputArray dst, int code, int dstCn=0)
将图像从一种颜色空间转换为另一种颜色空间。
void applyColorMap(InputArray src, OutputArray dst, int colormap)
在给定的图像上应用 GNU Octave/MATLAB 等效的 colormap。
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:102
STL 命名空间。
StructuredLightPattern 构造函数的参数。
定义 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 矢量的矢量中。外部矢量的大小为 2,因为有两个摄像机:第一个矢量存储从左摄像机捕获的模式图像,第二个矢量存储从右摄像机获取的模式图像。模式图像的数量对于两个摄像机显然是相同的,可以使用 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 );

需要注意,所有图像(图案图像、黑白图像)必须加载为灰度图像,并在传递到解码方法之前进行校正

// 立体校正
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 );
........
对于( 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 视差图像;
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(解码方法计算 CV_64FC1 视差图)

Mat pointcloud;
视差图像.convertTo( 视差图像, 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::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();