精华内容
下载资源
问答
  • 双目视觉opencv opengl三维重建双目视觉opencv opengl三维重建双目视觉opencv opengl三维重建,请修改代码中opencv对应版本号
  • OpenCV实现SfM(二):双目三维重建

    万次阅读 多人点赞 2015-09-02 14:33:18
    使用OpenCV3.0实现双目三维重建,原理清晰,实践有效。

    注意:本文中的代码必须使用OpenCV3.0或以上版本进行编译,因为很多函数是3.0以后才加入的。
    目录:

    文章目录


    #极线约束与本征矩阵

    在三维重建前,我们先研究一下同一点在两个相机中的像的关系。假设在世界坐标系中有一点pp,坐标为XX,它在1相机中的像为x1x_1,在2相机中的像为x2x_2(注意x1x_1x2x_2为齐次坐标,最后一个元素是1),如下图。
    这里写图片描述
    XX到两个相机像面的垂直距离分别为s1s_1s2s_2,且这两个相机具有相同的内参矩阵KK,与世界坐标系之间的变换关系分别为[R1  T1][R_1\ \ T_1][R2  T2][R_2\ \ T_2],那么我们可以得到下面两个等式
    s1x1=K(R1X+T1)s2x2=K(R2X+T2) s_1x_1 = K(R_1X + T_1) \\ s_2x_2 = K(R_2X + T_2)
    由于K是可逆矩阵,两式坐乘K的逆,有
    s1K1x1=R1X+T1s2K1x2=R2X+T2 s_1K^{-1}x_1 = R_1X + T_1 \\ s_2K^{-1}x_2 = R_2X + T_2
    K1x1=x1K^{-1}x_1 = x_1^{'}K1x2=x2K^{-1}x_2 = x_2^{'},则有
    s1x1=R1X+T1s2x2=R2X+T2 s_1x_1^{'} = R_1X + T_1 \\ s_2x_2^{'} = R_2X + T_2
    我们一般称x1x_1^{'}x2x_2^{'}为归一化后的像坐标,它们和图像的大小没有关系,且原点位于图像中心。
    由于世界坐标系可以任意选择,我们将世界坐标系选为第一个相机的相机坐标系,这时R1=I, T1=0R_1 = I,\ T_1 = 0。上式则变为
    s1x1=Xs2x2=R2X+T2 s_1x_1^{'} = X \\ s_2x_2^{'} = R_2X + T_2
    将第一式带入第二式,有
    s2x2=s1R2x1+T2 s_2x_2^{'} = s_1R_2x_1^{'} + T_2
    x2x_2^{'}T2T_2都是三维向量,它们做外积(叉积)之后得到另外一个三维向量T2^x2\widehat{T_2}x_2^{'}(其中T2^\widehat{T_2}为外积的矩阵形式,T2^x2\widehat{T_2}x_2^{'}代表T2×x2T_2\times x_2^{'}),且该向量垂直于x2x_2^{'}T2T_2,再用该向量对等式两边做内积,有
    0=s1(T2^x2)TR2x1 0 = s_1(\widehat{T_2}x_2^{'})^TR_2x_1^{'}

    x2T2^R2x1=0 x_2^{'}\widehat{T_2}R_2x_1^{'} = 0
    E=T2^R2E = \widehat{T_2}R_2
    x2Ex1=0 x_2^{'}Ex_1^{'} = 0
    可以看出,上式是同一点在两个相机中的像所满足的关系,它和点的空间坐标、点到相机的距离均没有关系,我们称之为极线约束,而矩阵EE则称为关于这两个相机的本征矩阵。如果我们知道两幅图像中的多个对应点(至少5对),则可以通过上式解出矩阵EE,又由于EE是由T2T_2R2R_2构成的,可以从E中分解出T2T_2R2R_2
    如何从EE中分解出两个相机的相对变换关系(即T2T_2R2R_2),背后的数学原理比较复杂,好在OpenCV为我们提供了这样的方法,在此就不谈原理了。

    #特征点提取与匹配
    从上面的分析可知,要求取两个相机的相对关系,需要两幅图像中的对应点,这就变成的特征点的提取和匹配问题。对于图像差别较大的情况,推荐使用SIFT特征,因为SIFT对旋转、尺度、透视都有较好的鲁棒性。如果差别不大,可以考虑其他更快速的特征,比如SURF、ORB等。
    本文中使用SIFT特征,由于OpenCV3.0将SIFT包含在了扩展部分中,所以官网上下载的版本是没有SIFT的,为此需要到这里下载扩展包,并按照里面的说明重新编译OpenCV(哎~真麻烦,-_-!)。如果你使用其他特征,就不必为此辛劳了。
    下面的代码负责提取图像特征,并进行匹配。

    void extract_features(
    	vector<string>& image_names,
    	vector<vector<KeyPoint>>& key_points_for_all,
    	vector<Mat>& descriptor_for_all,
    	vector<vector<Vec3b>>& colors_for_all
    	)
    {
    	key_points_for_all.clear();
    	descriptor_for_all.clear();
    	Mat image;
    
    	//读取图像,获取图像特征点,并保存
    	Ptr<Feature2D> sift = xfeatures2d::SIFT::create(0, 3, 0.04, 10);
    	for (auto it = image_names.begin(); it != image_names.end(); ++it)
    	{
    		image = imread(*it);
    		if (image.empty()) continue;
    
    		vector<KeyPoint> key_points;
    		Mat descriptor;
    		//偶尔出现内存分配失败的错误
    		sift->detectAndCompute(image, noArray(), key_points, descriptor);
    
    		//特征点过少,则排除该图像
    		if (key_points.size() <= 10) continue;
    
    		key_points_for_all.push_back(key_points);
    		descriptor_for_all.push_back(descriptor);
    
    		vector<Vec3b> colors(key_points.size());
    		for (int i = 0; i < key_points.size(); ++i)
    		{
    			Point2f& p = key_points[i].pt;
    			colors[i] = image.at<Vec3b>(p.y, p.x);
    		}
    		colors_for_all.push_back(colors);
    	}
    }
    
    void match_features(Mat& query, Mat& train, vector<DMatch>& matches)
    {
    	vector<vector<DMatch>> knn_matches;
    	BFMatcher matcher(NORM_L2);
    	matcher.knnMatch(query, train, knn_matches, 2);
    
    	//获取满足Ratio Test的最小匹配的距离
    	float min_dist = FLT_MAX;
    	for (int r = 0; r < knn_matches.size(); ++r)
    	{
    		//Ratio Test
    		if (knn_matches[r][0].distance > 0.6*knn_matches[r][1].distance)
    			continue;
    
    		float dist = knn_matches[r][0].distance;
    		if (dist < min_dist) min_dist = dist;
    	}
    
    	matches.clear();
    	for (size_t r = 0; r < knn_matches.size(); ++r)
    	{
    		//排除不满足Ratio Test的点和匹配距离过大的点
    		if (
    			knn_matches[r][0].distance > 0.6*knn_matches[r][1].distance ||
    			knn_matches[r][0].distance > 5 * max(min_dist, 10.0f)
    			)
    			continue;
    
    		//保存匹配点
    		matches.push_back(knn_matches[r][0]);
    	}
    }
    

    需要重点说明的是,匹配结果往往有很多误匹配,为了排除这些错误,这里使用了Ratio Test方法,即使用KNN算法寻找与该特征最匹配的2个特征,若第一个特征的匹配距离与第二个特征的匹配距离之比小于某一阈值,就接受该匹配,否则视为误匹配。当然,也可以使用Cross Test(交叉验证)方法来排除错误。

    得到匹配点后,就可以使用OpenCV3.0中新加入的函数findEssentialMat()来求取本征矩阵了。得到本征矩阵后,再使用另一个函数对本征矩阵进行分解,并返回两相机之间的相对变换R和T。注意这里的T是在第二个相机的坐标系下表示的,也就是说,其方向从第二个相机指向第一个相机(即世界坐标系所在的相机),且它的长度等于1。

    bool find_transform(Mat& K, vector<Point2f>& p1, vector<Point2f>& p2, Mat& R, Mat& T, Mat& mask)
    {
    	//根据内参矩阵获取相机的焦距和光心坐标(主点坐标)
    	double focal_length = 0.5*(K.at<double>(0) + K.at<double>(4));
    	Point2d principle_point(K.at<double>(2), K.at<double>(5));
    
    	//根据匹配点求取本征矩阵,使用RANSAC,进一步排除失配点
    	Mat E = findEssentialMat(p1, p2, focal_length, principle_point, RANSAC, 0.999, 1.0, mask);
    	if (E.empty()) return false;
    
    	double feasible_count = countNonZero(mask);
    	cout << (int)feasible_count << " -in- " << p1.size() << endl;
    	//对于RANSAC而言,outlier数量大于50%时,结果是不可靠的
    	if (feasible_count <= 15 || (feasible_count / p1.size()) < 0.6)
    		return false;
    
    	//分解本征矩阵,获取相对变换
    	int pass_count = recoverPose(E, p1, p2, R, T, focal_length, principle_point, mask);
    
    	//同时位于两个相机前方的点的数量要足够大
    	if (((double)pass_count) / feasible_count < 0.7)
    		return false;
    
    	return true;
    }
    

    #三维重建
    现在已经知道了两个相机之间的变换矩阵,还有每一对匹配点的坐标。三维重建就是通过这些已知信息还原匹配点在空间当中的坐标。在前面的推导中,我们有
    s2x2=K(R2X+T2)s_2x_2 = K(R_2X + T_2)
    这个等式中有两个未知量,分别是s2s_2XX。用x2x_2对等式两边做外积,可以消去s2s_2,得
    0=x2^K(R2X+T2) 0 = \widehat{x_2}K(R_2X+T_2)
    整理一下可以得到一个关于空间坐标X的线性方程
    x2^KR2X=x2^KT2 \widehat{x_2}KR_2X = -\widehat{x_2}KT_2
    上面的方程不能直接取逆求解,因此化为其次方程
    x2^K(R2  T)(X1)=0 \widehat{x_2}K(R_2\ \ T)\left(\begin{matrix}X \\ 1\end{matrix}\right) = 0
    用SVD求X左边矩阵的零空间,再将最后一个元素归一化到1,即可求得X。其几何意义相当于分别从两个相机的光心作过x1x_1x2x_2的延长线,延长线的焦点即为方程的解,如文章最上方的图所示。由于这种方法和三角测距类似,因此这种重建方式也被称为三角化(triangulate)。OpenCV提供了该方法,可以直接使用。

    void reconstruct(Mat& K, Mat& R, Mat& T, vector<Point2f>& p1, vector<Point2f>& p2, Mat& structure)
    {
    	//两个相机的投影矩阵[R T],triangulatePoints只支持float型
    	Mat proj1(3, 4, CV_32FC1);
    	Mat proj2(3, 4, CV_32FC1);
    
    	proj1(Range(0, 3), Range(0, 3)) = Mat::eye(3, 3, CV_32FC1);
    	proj1.col(3) = Mat::zeros(3, 1, CV_32FC1);
    
    	R.convertTo(proj2(Range(0, 3), Range(0, 3)), CV_32FC1);
    	T.convertTo(proj2.col(3), CV_32FC1);
    
    	Mat fK;
    	K.convertTo(fK, CV_32FC1);
    	proj1 = fK*proj1;
    	proj2 = fK*proj2;
    
    	//三角化重建
    	triangulatePoints(proj1, proj2, p1, p2, structure);
    }
    

    #测试
    我用了下面两幅图像进行测试
    这里写图片描述

    得到了着色后的稀疏点云,是否能看出一点轮廓呢?!

    这里写图片描述
    这里写图片描述

    图片中的两个彩色坐标系分别代表两个相机的位置。
    在接下来的文章中,会将相机的个数推广到任意多个,成为一个真正的SfM系统。

    关于源代码的使用
    代码是用VS2013写的,OpenCV版本为3.0且包含扩展部分,如果不使用SIFT特征,可以修改源代码,然后使用官方未包含扩展部分的库。软件运行后会将三维重建的结果写入Viewer目录下的structure.yml文件中,在Viewer目录下有一个SfMViewer程序,直接运行即可读取yml文件并显示三维结构。

    代码下载

    展开全文
  • OpenCV实现SfM:双目三维重建

    千次下载 热门讨论 2015-09-02 14:29:19
    使用OpenCV3.0进行双目三维重建。 代码是用VS2013写的,OpenCV版本为3.0且包含扩展部分,如果不使用SIFT特征,可以修改源代码,然后使用官方未包含扩展部分的库。软件运行后会将三维重建的结果写入Viewer目录下的...
  • 我在做双目立体视觉问题时,主要关注的点是立体匹配,本文主要关注最后一个步骤三维重建中的:三角剖分和纹理贴图以及对应的OpenCV+OpenGL代码实现。1.视差计算1.1基于视差信息的三维重建特征...

    0.绪论

    这篇文章主要为了研究双目立体视觉的最终目标——三维重建,系统的介绍了三维重建的整体步骤。双目立体视觉的整体流程包括:图像获取,摄像机标定,特征提取(稠密匹配中这一步可以省略),立体匹配,三维重建。我在做双目立体视觉问题时,主要关注的点是立体匹配,本文主要关注最后一个步骤三维重建中的:三角剖分和纹理贴图以及对应的OpenCV+OpenGL代码实现。

    1.视差计算

    1.1基于视差信息的三维重建

    特征提取

    由双目立体视觉进行三位重建的第一步是立体匹配,通过寻找两幅图像中的对应点获取视差。OpenCV 中的features2d库中包含了很多常用的算法,其中特征点定位的算法有FAST, SIFT, SURF ,MSER, HARRIS等,特征点描述算法有SURF, SIFT等,还有若干种特征点匹配算法。这三个步骤的算法可以任选其一,自由组合,非常方便。经过实验,选择了一种速度、特征点数量和精度都比较好的组合方案:FAST角点检测算法+SURF特征描述子+FLANN(Fast Library for Approximate Nearest Neighbors) 匹配算法。

    在匹配过程中需要有一些措施来过滤误匹配。一种比较常用的方法是比较第一匹配结果和第二匹配结果的得分差距是否足够大,这种方法可以过滤掉一些由于相似造成的误匹配。还有一种方法是利用已经找到的匹配点,使用RANSAC算法求得两幅视图之间的单应矩阵,然后将左视图中的坐标P用单应矩阵映射到右视图的Q点,观察与匹配结果Q’的欧氏距离是否足够小。当然由于图像是具有深度的,Q与Q’必定会有差距,因此距离阈值可以设置的稍微宽松一些。我使用了这两种过滤方法。

    另外,由于图像有些部分的纹理较多,有些地方则没有什么纹理,造成特征点疏密分布不均匀,影响最终重建的效果,因此我还采取了一个措施:限制特征点不能取的太密。如果新加入的特征点与已有的某一特征点距离太小,就舍弃之。最终匹配结果如下图所示,精度和均匀程度都较好。

    代码:

    // choose the corresponding points in the stereo images for 3d reconstruction

    void GetPair( Mat &imgL, Mat &imgR, vector &ptsL, vector &ptsR )

    {

    Mat descriptorsL, descriptorsR;

    double tt = (double)getTickCount();

    Ptr detector = FeatureDetector::create( DETECTOR_TYPE ); // factory mode

    vector keypointsL, keypointsR;

    detector->detect( imgL, keypointsL );

    detector->detect( imgR, keypointsR );

    Ptr de = DescriptorExtractor::create( DESCRIPTOR_TYPE );

    //SurfDescriptorExtractor de(4,2,true);

    de->compute( imgL, keypointsL, descriptorsL );

    de->compute( imgR, keypointsR, descriptorsR );

    tt = ((double)getTickCount() - tt)/getTickFrequency(); // 620*555 pic, about 2s for SURF, 120s for SIFT

    Ptr matcher = DescriptorMatcher::create( MATCHER_TYPE );

    vector> matches;

    matcher->knnMatch( descriptorsL, descriptorsR, matches, 2 ); // L:query, R:train

    vector passedMatches; // save for drawing

    DMatch m1, m2;

    vector ptsRtemp, ptsLtemp;

    for( size_t i = 0; i < matches.size(); i++ )

    {

    m1 = matches[i][0];

    m2 = matches[i][1];

    if (m1.distance < MAXM_FILTER_TH * m2.distance)

    {

    ptsRtemp.push_back(keypointsR[m1.trainIdx].pt);

    ptsLtemp.push_back(keypointsL[i].pt);

    passedMatches.push_back(m1);

    }

    }

    Mat HLR;

    HLR = findHomography( Mat(ptsLtemp), Mat(ptsRtemp), CV_RANSAC, 3 );

    cout<

    Mat ptsLt;

    perspectiveTransform(Mat(ptsLtemp), ptsLt, HLR);

    vector matchesMask( passedMatches.size(), 0 );

    int cnt = 0;

    for( size_t i1 = 0; i1 < ptsLtemp.size(); i1++ )

    {

    Point2f prjPtR = ptsLt.at((int)i1,0); // prjx = ptsLt.at((int)i1,0), prjy = ptsLt.at((int)i1,1);

    // inlier

    if( abs(ptsRtemp[i1].x - prjPtR.x) < HOMO_FILTER_TH &&

    abs(ptsRtemp[i1].y - prjPtR.y) < 2) // restriction on y is more strict

    {

    vector::iterator iter = ptsL.begin();

    for (;iter!=ptsL.end();iter++)

    {

    Point2f diff = *iter - ptsLtemp[i1];

    float dist = abs(diff.x)+abs(diff.y);

    if (dist < NEAR_FILTER_TH) break;

    }

    if (iter != ptsL.end()) continue;

    ptsL.push_back(ptsLtemp[i1]);

    ptsR.push_back(ptsRtemp[i1]);

    cnt++;

    if (cnt%1 == 0) matchesMask[i1] = 1; // don't want to draw to many matches

    }

    }

    Mat outImg;

    drawMatches(imgL, keypointsL, imgR, keypointsR, passedMatches, outImg,

    Scalar::all(-1), Scalar::all(-1), matchesMask, DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);

    char title[50];

    sprintf_s(title, 50, "%.3f s, %d matches, %d passed", tt, matches.size(), cnt);

    imshow(title, outImg);

    waitKey();

    }

    p.s. 源代码中的基于特征点的视差计算有点问题,还在调试中,希望有经验的大牛共同解决一下。

    1.2基于块匹配的视差计算

    上面提取特征点的过程中实际上忽略了一个辅助信息:对应点应当是取在对应极线上的一个区间内的。利用这个信息可以大幅简化对应点的匹配,事实上只要用L1距离对一个像素周围的block计算匹配距离就可以了,也就是OpenCV中实现的块匹配算法的基本思路。比起特征点匹配,这是一种“稠密”的匹配算法,精度也可以接受。下图中浅色表示视差较大,对应深度较浅。左侧有一块区域是左右视图不相交的部分,因此无法计算视差。

    可以发现视差计算结果中有很多噪声。事实上在纹理平滑的区域,还有左右视图中不同遮挡的区域,是很难计算视差的。因此我利用最近邻插值和数学形态学平滑的方法对视差图进行了修复(见cvFuncs2.cpp中的FixDisparity函数):

    // roughly smooth the glitches on the disparity map

    void FixDisparity( Mat_ & disp, int numberOfDisparities )

    {

    Mat_ disp1;

    float lastPixel = 10;

    float minDisparity = 23;// algorithm parameters that can be modified

    for (int i = 0; i < disp.rows; i++)

    {

    for (int j = numberOfDisparities; j < disp.cols; j++)

    {

    if (disp(i,j) <= minDisparity) disp(i,j) = lastPixel;

    else lastPixel = disp(i,j);

    }

    }

    int an = 4; // algorithm parameters that can be modified

    copyMakeBorder(disp, disp1, an,an,an,an, BORDER_REPLICATE);

    Mat element = getStructuringElement(MORPH_ELLIPSE, Size(an*2+1, an*2+1));

    morphologyEx(disp1, disp1, CV_MOP_OPEN, element);

    morphologyEx(disp1, disp1, CV_MOP_CLOSE, element);

    disp = disp1(Range(an, disp.rows-an), Range(an, disp.cols-an)).clone();

    }

    对应点的选取

    上面提到,为了获得较好的重构效果,特征点最好取在深度变化较大的区域。基于这种猜想,我首先对上面的视差图求梯度,然后找到梯度最大的点,观察梯度的方向,如果是偏x方向,就在该点左右若干像素各取一个点;否则就在上下若干像素各取一个点。然后根据这两个点的视差值就可以计算出另外一个视图中的对应点的坐标。特征点还不能分布过密,因此我取完一对特征点后,将其周围一圈像素的梯度置零,然后在寻找下一个梯度最大值,这样一直下去,直到取够特征点数。

    特征点也不能全取在深度变化剧烈的区域,在平坦的区域也可以取一些。最终我取的特征点如下图:

    其中紫色的点是在较平坦的区域取到的,其他颜色是在边界区域取到的。这些算法实现在ChooseKeyPointsBM函数中。

    2.计算世界坐标

    一般双目立体视觉中使用的实验图像都是经过外极线矫正的,计算3D坐标也比较方便,其实利用外极线约束(以及其他的约束条件)可以极大的降低立体匹配的计算量。见下图:

    如果(x1,y1),(x2,y2)用各自图像上的像素坐标表示,L和(X,Y,Z)用毫米表示,f用像素表示的话,用相似三角形的知识就可以推出:

    其中W和H是图像的宽高(像素数),y是y1和y2的均值,Z加负号是为了保持右手坐标系,而Y加负号是由于图像成像过程中上下发生了倒转。三维世界原点取为左摄像机的焦点。计算的代码见cvFunc.cpp中的StereoTo3D函数。

    // calculate 3d coordinates.

    // for rectified stereos: pointLeft.y == pointRight.y

    // the origin for both image is the top-left corner of the left image.

    // the x-axis points to the right and the y-axis points downward on the image.

    // the origin for the 3d real world is the optical center of the left camera

    // object -> optical center -> image, the z value decreases.

    void StereoTo3D( vector ptsL, vector ptsR, vector &pts3D,

    float focalLenInPixel, float baselineInMM, Mat img,

    Point3f &center3D, Vec3f &size3D) // output variable, the center coordinate and the size of the object described by pts3D

    {

    vector::iterator iterL = ptsL.begin(),

    iterR = ptsR.begin();

    float xl, xr, ylr;

    float imgH = float(img.rows), imgW = float(img.cols);

    Point3f pt3D;

    float minX = 1e9, maxX = -1e9;

    float minY = 1e9, maxY = -1e9;

    float minZ = 1e9, maxZ = -1e9;

    Mat imgShow = img.clone();

    char str[100];

    int ptCnt = ptsL.size(), showPtNum = 30, cnt = 0;

    int showIntv = max(ptCnt/showPtNum, 1);

    for ( ; iterL != ptsL.end(); iterL++, iterR++)

    {

    xl = iterL->x;

    xr = iterR->x; // need not add baseline

    ylr = (iterL->y + iterR->y)/2;

    //if (yl-yr>5 || yr-yl>5) // may be wrong correspondence, discard. But vector can't be changed during iteration

    //{}

    pt3D.z = -focalLenInPixel * baselineInMM / (xl-xr); // xl should be larger than xr, if xl is shot by the left camera

    pt3D.y = -(-ylr + imgH/2) * pt3D.z / focalLenInPixel;

    pt3D.x = (imgW/2 - xl) * pt3D.z / focalLenInPixel;

    minX = min(minX, pt3D.x); maxX = max(maxX, pt3D.x);

    minY = min(minY, pt3D.y); maxY = max(maxY, pt3D.y);

    minZ = min(minZ, pt3D.z); maxZ = max(maxZ, pt3D.z);

    pts3D.push_back(pt3D);

    if ((cnt++)%showIntv == 0)

    {

    Scalar color = CV_RGB(rand()&64,rand()&64,rand()&64);

    sprintf_s(str, 100, "%.0f,%.0f,%.0f", pt3D.x, pt3D.y, pt3D.z);

    putText(imgShow, str, Point(xl-13,ylr-3), FONT_HERSHEY_SIMPLEX, .3, color);

    circle(imgShow, *iterL, 2, color, 3);

    }

    }

    imshow("back project", imgShow);

    waitKey();

    center3D.x = (minX+maxX)/2;

    center3D.y = (minY+maxY)/2;

    center3D.z = (minZ+maxZ)/2;

    size3D[0] = maxX-minX;

    size3D[1] = maxY-minY;

    size3D[2] = maxZ-minZ;

    }

    3.三角剖分

    3.1 三角剖分简介

    三角剖分是为了之后的纹理贴图,我用了OpenCV中的Delaunay三角剖分函数,这种剖分算法的可以使所形成的三角形的最小角最大。剖分的示例如下:

    OpenCV使用Delaunay算法将平面分割成小的三角形区域(该三角形确保包括所有的分割点)开始不断迭代完成。在这种情况下,对偶划分就是输入的二维点集的Voronoi图表。这种划分可以用于对一个平面进行三维分段变换、形态变换、平面点的快速 定位以及建立特定的图结构(如NNG,RNG)。

    同时由表可以看出,三角网生成法的时间效率最低,分治算法的时间效率最高,逐点插入法效率居中。

    3.2 Bowyer-Watson算法

    目前采用逐点插入方式生成的Delaunay三角网的算法主要基于Bowyer-Watson算法,Bowyer-Watson算法的主要步骤如下:

    1)建立初始三角网格:针对给定的点集V,找到一个包含该点集的矩形R,我们称R为辅助窗口。连接R的任意一条对角线,形成两个三角形,作为初始Delaunay三角网格。

    2)逐点插入:假设目前已经有一个Delaunay三角网格T,现在在它里面再插入一个点P,需要找到该点P所在的三角形。从P所在的三角形开始,搜索该三角形的邻近三角形,并进行空外接圆检测。找到外接圆包含点P的所有的三角形并删除这些三角形,形成一个包含P的多边形空腔,我们称之为Delaunay空腔。然后连接P与Delaunay腔的每一个顶点,形成新的Delaunay三角网格。

    3)删除辅助窗口R:重复步骤2),当点集V中所有点都已经插入到三角形网格中后,将顶点包含辅助窗口R的三角形全部删除。

    在这些步骤中,快速定位点所在的三角形、确定点的影响并构建Delaunay腔的过程是每插入一个点都会进行的。随着点数的增加,三角形数目增加很快,因此缩短这两个过程的计算时间,是提高算法效率的关键。

    算法执行图示如下:

    3.3 三角剖分代码分析

    三角剖分的代码见cvFuncs.cpp中的TriSubDiv函数,我将特征点存储到一个vector变量中,剖分结果存储到一个vector变量中,Vec3i中存储的是3个表示顶点编号的整数。

    我们需要存储Delaunay的内存空间和一个外接矩形(该矩形盒子用来确定虚拟三角形)

    // STORAGE AND STRUCTURE FOR DELAUNAY SUBDIVISION //存储和结构 for三角剖分

    //

    CvRect rect = { 0, 0, 600, 600 }; //Our outer bounding box //我们的外接边界盒子

    CvMemStorage* storage; //Storage for the Delaunay subdivsion //用来存储三角剖分

    storage = cvCreateMemStorage(0); //Initialize the storage //初始化存储器

    CvSubdiv2D* subdiv; //The subdivision itself // 细分

    subdiv = init_delaunay( storage, rect); //See this function below //函数返回CvSubdiv类型指针

    init_delaunay函数如下,它是一个OpenCV函数,是一个包含一些OpenCV函数的函数包。

    //INITIALIZATION CONVENIENCE FUNCTION FOR DELAUNAY SUBDIVISION //为三角剖分初始化便利函数

    //

    CvSubdiv2D* init_delaunay(CvMemStorage* storage,CvRect rect) {

    CvSubdiv2D* subdiv;

    subdiv = cvCreateSubdiv2D(CV_SEQ_KIND_SUBDIV2D,sizeof(*subdiv),sizeof(CvSubdiv2DPoint),sizeof(CvQuadEdge2D),storage);//为数据申请空间

    cvInitSubdivDelaunay2D( subdiv, rect ); //rect sets the bounds

    return subdiv;//返回申请空间的指针

    }

    我们知道三角剖分是对散点集进行处理的,我们知道了散点集就可以获得点集的三角剖分。如何传入(插入)散点集呢?

    这些点必须是32位浮点型,并通过下面的方式插入点:

    CvPoint2D32f fp; //This is our point holder//这是我们点的持有者(容器)

    for( i = 0; i < as_many_points_as_you_want; i++ ) {

    // However you want to set points //如果我们的点集不是32位的,在这里我们将其转为CvPoint2D32f,如下两种方法。

    //

    fp = your_32f_point_list[i];

    cvSubdivDelaunay2DInsert( subdiv, fp );

    }

    转换为CvPoint2D32f的两种方法:

    1)通过宏cvPoint2D32f(double x,double y)

    2)通过cxtype.h下的cvPointTo32f(CvPoint point)函数将整形点方便的转换为32位浮点型。

    当可以通过输入点(散点集)得到Delaunay三角剖分后,接下来,我们用一下两个函数设置和清除相关的Voronoi划分:

    cvCalcSubdivVoronoi2D( subdiv ); // Fill out Voronoi data in subdiv //在subdiv中填充Vornoi的数据

    cvClearSubdivVoronoi2D( subdiv ); // Clear the Voronoi from subdiv//从subdiv中清除Voronoi的数据

    CvSubdiv2D结构如下:

    #define CV_SUBDIV2D_FIELDS() \

    CV_GRAPH_FIELDS() \

    int quad_edges; \

    int is_geometry_valid; \

    CvSubdiv2DEdge recent_edge; \

    CvPoint2D32f topleft; \

    CvPoint2D32f bottomright;

    typedef struct CvSubdiv2D

    {

    CV_SUBDIV2D_FIELDS()

    }

    CvSubdiv2D;

    #define CV_GRAPH_FIELDS() \

    CV_SET_FIELDS() /* set of vertices */ \

    CvSet *edges; /* set of edges */

    #define CV_SET_FIELDS() \

    CV_SEQUENCE_FIELDS() /*inherits from [#CvSeq CvSeq] */ \

    struct CvSetElem* free_elems; /*list of free nodes */

    整体代码如下:

    void TriSubDiv( vector &pts, Mat &img, vector &tri )

    {

    CvSubdiv2D* subdiv;//The subdivision itself // 细分

    CvMemStorage* storage = cvCreateMemStorage(0); ;//Storage for the Delaunay subdivsion //用来存储三角剖分

    Rect rc = Rect(0,0, img.cols, img.rows); //Our outer bounding box //我们的外接边界盒子

    subdiv = cvCreateSubdiv2D( CV_SEQ_KIND_SUBDIV2D, sizeof(*subdiv),

    sizeof(CvSubdiv2DPoint),

    sizeof(CvQuadEdge2D),

    storage );//为数据申请空间

    cvInitSubdivDelaunay2D( subdiv, rc );//rect sets the bounds

    //如果我们的点集不是32位的,在这里我们将其转为CvPoint2D32f,如下两种方法。

    for (size_t i = 0; i < pts.size(); i++)

    {

    CvSubdiv2DPoint *pt = cvSubdivDelaunay2DInsert( subdiv, pts[i] );

    pt->id = i;

    }

    CvSeqReader reader;

    int total = subdiv->edges->total;

    int elem_size = subdiv->edges->elem_size;

    cvStartReadSeq( (CvSeq*)(subdiv->edges), &reader, 0 );

    Point buf[3];

    const Point *pBuf = buf;

    Vec3i verticesIdx;

    Mat imgShow = img.clone();

    srand( (unsigned)time( NULL ) );

    for( int i = 0; i < total; i++ )

    {

    CvQuadEdge2D* edge = (CvQuadEdge2D*)(reader.ptr);

    if( CV_IS_SET_ELEM( edge ))

    {

    CvSubdiv2DEdge t = (CvSubdiv2DEdge)edge;

    int iPointNum = 3;

    Scalar color = CV_RGB(rand()&255,rand()&255,rand()&255);

    //bool isNeg = false;

    int j;

    for(j = 0; j < iPointNum; j++ )

    {

    CvSubdiv2DPoint* pt = cvSubdiv2DEdgeOrg( t );

    if( !pt ) break;

    buf[j] = pt->pt;

    //if (pt->id == -1) isNeg = true;

    verticesIdx[j] = pt->id;

    t = cvSubdiv2DGetEdge( t, CV_NEXT_AROUND_LEFT );

    }

    if (j != iPointNum) continue;

    if (isGoodTri(verticesIdx, tri))

    {

    //tri.push_back(verticesIdx);

    polylines( imgShow, &pBuf, &iPointNum,

    1, true, color,

    1, CV_AA, 0);

    //printf("(%d, %d)-(%d, %d)-(%d, %d)\n", buf[0].x, buf[0].y, buf[1].x, buf[1].y, buf[2].x, buf[2].y);

    //printf("%d\t%d\t%d\n", verticesIdx[0], verticesIdx[1], verticesIdx[2]);

    //imshow("Delaunay", imgShow);

    //waitKey();

    }

    t = (CvSubdiv2DEdge)edge+2;

    for(j = 0; j < iPointNum; j++ )

    {

    CvSubdiv2DPoint* pt = cvSubdiv2DEdgeOrg( t );

    if( !pt ) break;

    buf[j] = pt->pt;

    verticesIdx[j] = pt->id;

    t = cvSubdiv2DGetEdge( t, CV_NEXT_AROUND_LEFT );

    }

    if (j != iPointNum) continue;

    if (isGoodTri(verticesIdx, tri))

    {

    //tri.push_back(verticesIdx);

    polylines( imgShow, &pBuf, &iPointNum,

    1, true, color,

    1, CV_AA, 0);

    //printf("(%d, %d)-(%d, %d)-(%d, %d)\n", buf[0].x, buf[0].y, buf[1].x, buf[1].y, buf[2].x, buf[2].y);

    //printf("%d\t%d\t%d\n", verticesIdx[0], verticesIdx[1], verticesIdx[2]);

    //imshow("Delaunay", imgShow);

    //waitKey();

    }

    }

    CV_NEXT_SEQ_ELEM( elem_size, reader );

    }

    //RemoveDuplicate(tri);

    char title[100];

    sprintf_s(title, 100, "Delaunay: %d Triangles", tri.size());

    imshow(title, imgShow);

    waitKey();

    }

    平面划分是将一个平面分割为一组不重叠的、能够覆盖整个平面的区域。结构CvSubdiv2D描述了建立在二维点集上的划分结构,其中点集互相连接且构成平面图形,该图形通过结合一些无线连接外部划分点(称为凸形点)的边缘,将一个平面用按照其边缘划分成很多小区域。

    对于每一个划分操作,都有一个对偶划分与之对应。对偶的意思是小区域与点(划分的顶点)变换角色,即在对偶划分中,小区域被当做一个顶点(以下称为虚拟点)而原始的划分顶点被当做小区域。如下图所示,原始的划分用实线表示,而对偶划分用虚线表示。

    4.三维重构

    为了保证三维重建的效果,一般地要对深度图像进行后续处理。要从深度图像中恢复高质量的视差图,对深度图像的要求有:

    ①深度图像中,物体的边界必需与图像中物体的边界对齐;

    ②在场景图中,深度图像要尽可能均勻和平滑,即对图像进行平滑处理。

    三维重构的思路很简单,用OpenGL中纹理贴图功能,将平面图像中的三角形逐个贴到计算出的三维坐标上去就可以了。为了便于观察3D效果,我还设计了交互功能:用方向键可以上下左右旋转重构的模型,用鼠标滚轮可以放大或缩小。用gluLookAt函数可以实现视点旋转的功能。三维重构的代码实现在glFuncs.cpp中。

    纹理贴图:

    GLuint Create3DTexture( Mat &img, vector &tri,

    vector pts2DTex, vector &pts3D,

    Point3f center3D, Vec3f size3D )

    {

    GLuint tex = glGenLists(1);

    int error = glGetError();

    if (error != GL_NO_ERROR)

    cout << "An OpenGL error has occured: " << gluErrorString(error) << endl;

    if (tex == 0) return 0;

    Mat texImg;

    cvtColor(img, img, CV_BGR2RGB);

    resize(img, texImg, Size(512,512)); // seems no need to do this

    glNewList(tex, GL_COMPILE);

    vector::iterator iterTri = tri.begin();

    //vector::iterator iterPts3D = pts3D.begin();

    Point2f pt2D[3];

    Point3f pt3D[3];

    glDisable(GL_BLEND);

    glEnable(GL_TEXTURE_2D);

    for ( ; iterTri != tri.end(); iterTri++)

    {

    Vec3i &vertices = *iterTri;

    int ptIdx;

    for (int i = 0; i < 3; i++)

    {

    ptIdx = vertices[i];

    if (ptIdx == -1) break;

    //else cout<

    pt2D[i].x = pts2DTex[ptIdx].x / img.cols;

    pt2D[i].y = pts2DTex[ptIdx].y / img.rows;

    pt3D[i] = (pts3D[ptIdx] - center3D) * (1.f / max(size3D[0],size3D[1]));

    //pt3D[i].z -= offset;

    }

    if (ptIdx != -1)

    {

    MapTexTri(texImg, pt2D, pt3D);

    //cout<

    }

    }

    glDisable(GL_TEXTURE_2D);

    glEndList();

    return tex;

    }

    效果展示及不足

    Cloth图像是重构效果比较好的一组:

    可以比较明显的看出3D效果,也比较符合直觉。然而其他图像效果就差强人意了:

    仔细分析造成这种效果的原因,一方面,特征点的匹配可能有些误差,造成3D坐标的计算不太精确,但大部分坐标还是准确的。另一方面,左右视图可能会有不同的遮挡、偏移等情况,因此匹配得到的特征点可能实际上并不是3维世界中的同一点,这种误差是无法消除的。但造成效果变差的最重要的原因,还是图像中深度变化较大,而特征点选取的比较稀疏,因此正面看还比较正常,一旦旋转纹理就显得扭曲变形了。为了解决这个问题,应当试图把特征点取到深度变化较剧烈的地方,一般是图像中的边界处。然而特征点检测一般都检测出的是角点和纹理密集的区域,因此可以考虑更换对应点匹配的方法。

    如果要进一步改进效果,可以先对视差图像进行分割,将图像分成视差比较连续的几块区域分别贴图,视差变化剧烈的区域就不必把扭曲的纹理贴上去了。我尝试了以下分割的效果,如下图所示,应该可以达到更好的效果,不过由于时间所限,就没有进一步实现下去了。

    关于上面实现的两种求取视差的算法,在main函数的前面设置了一个变量g_algo,可以用来切换不同的算法。

    参考文献:

    代码下载

    展开全文
  • 文章转自:http://blog.csdn.net/zhubaohua_bupt/article/details/74172831利用相机进行三维重建已经不是一个新鲜的话题,重建的三维环境用途很广泛,比如检测识别目标,作为深度学习的输入,视觉SLAM。目前,比较...

    文章转自:http://blog.csdn.net/zhubaohua_bupt/article/details/74172831

    利用相机进行三维重建已经不是一个新鲜的话题,重建的三维环境用途很广泛,

    比如检测识别目标,作为深度学习的输入,视觉SLAM。

    目前,比较流行的是单、双目的重建。根据重建的稀疏程度不同,可以分为以下以下几类:

    稀疏重建:

    通常是重建一些图像特征点的深度,这个在基于特征的视觉SLAM里比较常见,得到的特征点的深度可以用来计算相机位姿。稀疏重建在实际应用,比如检测,避障,不能满足需求。

    半稠密重建:

    通常是重建图像纹理或梯度比较明显的区域,这些区域特征比较鲜明。半稠密重建在直接法视觉SLAM里比较常见。重建的三维点云相对稠密,可以满足部分应用需求。

    稠密重建:

    稠密重建是对整个图像或者图像中的绝大部分像素进行重建。与稀疏、半稠密相比,稠密重建对场景的三维信息理解更全面,更能符合应用需求。但是,由于要重建的点云数量太多,相对耗时。

    1 双目重建:

    双目重建通常又称之为,立体匹配、双目匹配、双目立体视觉、静态匹配等。

    根据所用的相机差异,比如针孔相机、鱼眼相机,实现略有差别。根据重建时匹配方式的不同,又可以分为全局、本全局、局部匹配。OpenCV的GM,SGBM,BM就分别实现了上述算法。

    想要了解这方面知识,以上述关键字关键字(立体匹配、双目匹配、双目立体视觉)或(Stereo Matching)搜论文。比如[1][2][3]。其过程可描述如下:

    利用左右相机得到的两幅矫正图像,通过一幅图在另一幅图上找匹配,然后根据三角测量原理恢复出环境三维信息。在鱼眼相机的匹配中,也有不矫正图像,直接匹配的做法,这样做需要计算图像极线。

    由于整个匹配的过程只需一个时刻的左右图像,所以也有人称为静态立体视觉。

    2 单目重建

    传统的视觉方法(不包括深度学习)单目重建,利用单幅图像不能完成重建,需要时间域上一系列图像。

    所以有人也称之为动态立体视觉。根据重建的实时性不同,可以分为离线重建和在线重建。

    2.1离线重建:

    比如 SFM技术,此技术根据在一段时间内获得的连续图像来重建一个三维环境。中文文献搜(运动恢复结构),英文搜(Structurefrom motion)。

    2.2在线重建:在线重建可以分为渐进式重建和直接式重建。在线重建或多或少都和VO或者(SLAM)有联系,因为重建的时候需要相机的位姿。

    2.2.1渐进式重建

    渐进式重建利用下一时刻的图像不断融合之前的三维信息,类似于卡尔曼滤波思想,而且三维重建实际上也是深度重建,因此,渐进式重建也称之为深度滤波。

    比如:[4]SVO和[5]REMODE,这两个论文是一个作者,SVO和REMODE有深度滤波详细的过程,并且有开源实现代码。

    REMODE:https://github.com/uzh-rpg/rpg_open_remode

    2.2.2直接式重建

    直接式重建,利用若干个时刻(一般几帧至几十帧)的图像,一次性完成对同一个场景的三维重建。

    直接式重建也有人称之为深度融合,有点类似于SFM,与SFM不同的是,参与计算的图像少,实时性较高。

    文章[7]是这方面的方法,但代码没有开源,如果比较了解深度滤波原理,这个也容易实现。

    [1]    Semi-Global-Matching

    [2]    Stereo Processing by Semi-Global Matchingand Mutual Information

    [3]    基于鱼眼相机的立体匹配

    [4]   C.Forster, M. Pizzoli, and D. Scaramuzza, “SVO: Fast Semi-Direct Monocular VisualOdometry,” in Proc. IEEE Intl. Conf. on Robotics and Automation, 2014.

    [5]   MatiaPizzoli, Christian Forster, and Davide Scaramuzza. REMODE: Probabilistic,monocular dense reconstruction in real time. In International Conference onRobotics and Automation (ICRA), pages 2609–2616, Hong Kong,China, June 2014.

    [6]   V. Usenko,J. Engel, J. Stuckler, and D. Cremers. Reconstructing Street-Scenes inReal-Time From a Driving Car

    [7]    Ra´ulMur-Artal and Juan D. Tard´os Probabilistic Semi-Dense Mapping from HighlyAccurate Feature-Based Monocular SLAM,2015

    原文出处:https://blog.csdn.net/xinbolai1993/article/details/81236371

    展开全文
  • OpenCV学习笔记(18)双目测距与三维重建OpenCV实现问题集锦(三)立体匹配与视差计算4. cvFindStereoCorrespondenceBM的输出结果好像不是以像素点为单位的视差? “@scyscyao:在OpenCV2.0中,BM函数得出的结果是以...

    OpenCV学习笔记(18)双目测距与三维重建的OpenCV实现问题集锦(三)立体匹配与视差计算

    4. cvFindStereoCorrespondenceBM的输出结果好像不是以像素点为单位的视差? “@scyscyao:在OpenCV2.0中,BM函数得出的结果是以16位符号数的形式的存储的,出于精度需要,所有的视差在输出时都扩大了16倍(2^4)。其具体代码表示如下:

    dptr[y*dstep] = (short)(((ndisp - mind - 1 + mindisp)*256 + (d != 0 ? (p-n)*128/d : 0) +

    15) >> 4);

    可以看到,原始视差在左移8位(256)并且加上一个修正值之后又右移了4位,最终的结果就是左移4位。

    因此,在实际求距离时,cvReprojectTo3D出来的X/W,Y/W,Z/W都要乘以16 (也就是W除以16),才能得到正确的三维坐标信息。”

    在OpenCV2.1中,BM算法可以用 CV_16S 或者 CV_32F 的方式输出视差数据,使用32位float格式可以得到真实的视差值,而CV_16S 格式得到的视差矩阵则需要 除以16 才能得到正确的视差。另外,OpenCV2.1另外两种立体匹配算法 SGBM 和 GC 只支持 CV_16S 格式的 disparity 矩阵。

    5. 如何设置BM、SGBM和GC算法的状态参数?

    (1)StereoBMState

    // 预处理滤波参数

    preFilterType:预处理滤波器的类型,主要是用于降低亮度失真(photometric distortions)、消除噪声和增强纹理等, 有两种可选类型:CV_STEREO_BM_NORMALIZED_RESPONSE(归一化响应) 或者 CV_STEREO_BM_XSOBEL(水平方向Sobel算子,默认类型), 该参数为 int 型; preFilterSize:预处理滤波器窗口大小,容许范围是[5,255],一般应该在 5x5..21x21 之间,参数必须为奇数值, int 型 preFilterCap:预处理滤波器的截断值,预处理的输出值仅保留[-preFilterCap, preFilterCap]范围内的值,参数范围:1 - 31(文档中是31,但代码中是 63), int

    // SAD 参数

    SADWindowSize:SAD窗口大小,容许范围是[5,255],一般应该在 5x5 至 21x21 之间,参数必须是奇数,int 型 minDisparity:最小视差,默认值为 0, 可以是负值,int 型 numberOfDisparities:视差窗口,即最大视差值与最小视差值之差, 窗口大小必须是 16 的整数倍,int 型 // 后处理参数

    textureThreshold:低纹理区域的判断阈值。如果当前 SAD窗口内所有邻居像素点的x导数绝对值之和小于指定阈值,则该窗口对应的像素点的视差值为 0(That is, if the sum of absolute values of x-derivatives computed over SADWindowSize by SADWindowSize pixel neighborhood is smaller than the parameter, no disparity is computed at the pixel),该参数不能为负值,int 型 uniquenessRatio:视差唯一性百分比, 视差窗口范围内最低代价是次低代价的(1 + uniquenessRatio/100)倍时,最低代价对应的视差值才是该像素点的视差,否则该像素点的视差为 0 (the minimum margin in percents between the best (minimum) cost function value and the second best value to accept the computed disparity, that

    展开全文
  • 这里特别感谢博主shiter的原创文章:OpenCV+OpenGL 双目立体视觉三维重建 本博文参考了该博主的的核心代码,并针对该博主博文中声明的一些BUG进行了修正: 本文代码下载地址(已修正相关问题问题):...
  • 基于结构光的重建包括了双目三维重建和单目三维重建,双目的重建方法主要采用双目立体视觉算法来匹配两幅图片的相位信息,可以参考我的多频外差双目重建,这里不做介绍了。主要还是介绍下我做的单目三维重建,当然...
  • OpenCV+OpenGL 双目立体视觉三维重建

    万次阅读 多人点赞 2016-08-08 00:02:47
    我在做双目立体视觉问题时,主要关注的点是立体匹配,本文主要关注最后一个步骤三维重建中的:三角剖分和纹理贴图以及对应的OpenCV+OpenGL代码实现。1.视差计算1.1基于视差信息的三维重建特征提
  • Wayne:CVPR2020 Oral:立体视觉Cost Volume构建新方法,性能SoA,显存和运行时间大幅降低单目:双目重建双目立体视差图进行三维点云重建OpenCV3.4.1+PCL1.8】用VS+Opencv3.1从双目立体视差图中重建三维点云使用...
  • 【计算机视觉】 opencv双目视觉 立体视觉 三维重建

    万次阅读 热门讨论 2017-05-07 21:41:02
    双目 MATLAB标定 ,查阅博主的【计算机视觉】摄像机标定 matlab toolbox_calib工具箱(单目标定和双目标定) 1 基本原理   得到了立体标定参数之后,就可以把参数放入xml文件,然后用cvLoad读入OpenCV了。具体...
  • 转载 双目视觉三维重建 作者tiemaxiaosu http://blog.csdn.net/tiemaxiaosu/article/details/51734667 一、三维重建概述  三维重建主要是研究如何从得到的匹配点中计算出相机的投影矩阵(如果是外部标定的话,...
  • 三维重建是指根据基于一个视图或者多个视图所获得的物体或者场景的图像重建三维模型的过程。由于单视图的信息很单一,因此三维重建需要更复杂的算法和过程。相比之下,多视图的三维重建(模仿人类观察世界的方式)就...
  • opencv写的双目视觉摄像机标定和三维重建代码opencv写的双目视觉摄像机标定和三维重建代码opencv写的双目视觉摄像机标定和三维重建代码
  • OpenCV2.2+OpenGL双目立体三维重建

    热门讨论 2011-07-02 23:06:44
    使用OpenCV2.2(c++版)中的两种算法:SURF特征点匹配和双目视觉块匹配算法,计算对应点;Delaunay三角剖分;然后计算3D坐标;最后用OpenGL进行3D纹理贴图。 可以用来演示OpenCV2.2中c++函数的使用和OpenGL的简单...
  • OpenCV3】基于双目视觉的三维重建

    万次阅读 2017-05-11 16:57:40
    opencv双目视觉立体重建根据的是三角形原理,在经过摄像机立体标定之后获取到单个摄像机的参数和双目系统的立体参数,根据三角形原理,我们即可实现对点云的三维重建,这里我们只介绍对单个点的三维重建
  • opencv1写的双目视觉摄像机标定和三维重建代码——opencv1写的双目视觉摄像机标定和三维重建代码
  • 1.修改为 VS2015 Debug win32 版本,支持利用特征点和 OpenCV 立体匹配算法进行进行三维重建及显示,相关代码需要自行修改,代码中添加了修改注释。 2.工程依赖库为 OpenCV2.4.8,内部已完成 OpenCV 相关配置。无论...
  • /* 使用matlab标定工具箱得到的相机参数*/ /* 两张图片尺寸,640*360. 下载地址  ... */ #include &...opencv2/opencv.hpp&gt; #include &lt;iostream&gt; using namespace s...

空空如也

空空如也

1 2 3 4 5 ... 10
收藏数 182
精华内容 72
关键字:

opencv双目三维重建