OpenCV实现SfM(二):双目三维重建

http://blog.csdn.net/AIchipmunk/article/details/48157369

目录(?)[+]

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

极线约束与本征矩阵

在三维重建前,我们先研究一下同一点在两个相机中的像的关系。假设在世界坐标系中有一点 p ,坐标为 X ,它在1相机中的像为 x1 ,在2相机中的像为 x2 (注意 x1 x2 为齐次坐标,最后一个元素是1),如下图。 
这里写图片描述 
X 到两个相机像面的垂直距离分别为 s1 s2 ,且这两个相机具有相同的内参矩阵 K ,与世界坐标系之间的变换关系分别为 [R1  T1] [R2  T2] ,那么我们可以得到下面两个等式 

s1x1=K(R1X+T1)s2x2=K(R2X+T2)

由于K是可逆矩阵,两式坐乘K的逆,有 
s1K1x1=R1X+T1s2K1x2=R2X+T2

K1x1=x1 K1x2=x2 ,则有 
s1x1=R1X+T1s2x2=R2X+T2

我们一般称 x1 x2 为归一化后的像坐标,它们和图像的大小没有关系,且原点位于图像中心。 
由于世界坐标系可以任意选择,我们将世界坐标系选为第一个相机的相机坐标系,这时 R1=I, T1=0 。上式则变为 
s1x1=Xs2x2=R2X+T2

将第一式带入第二式,有 
s2x2=s1R2x1+T2

x2 T2 都是三维向量,它们做外积(叉积)之后得到另外一个三维向量 T2ˆx2 (其中 T2ˆ 为外积的矩阵形式, T2ˆx2 代表 T2×x2 ),且该向量垂直于 x2 T2 ,再用该向量对等式两边做内积,有 
0=s1(T2ˆx2)TR2x1

即 
x2T2ˆR2x1=0

E=T2ˆR2  有 
x2Ex1=0

可以看出,上式是同一点在两个相机中的像所满足的关系,它和点的空间坐标、点到相机的距离均没有关系,我们称之为极线约束,而矩阵 E 则称为关于这两个相机的本征矩阵。如果我们知道两幅图像中的多个对应点(至少5对),则可以通过上式解出矩阵 E ,又由于 E 是由 T2 R2 构成的,可以从E中分解出 T2 R2 。 
如何从 E 中分解出两个相机的相对变换关系(即 T2 R2 ),背后的数学原理比较复杂,好在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]);
    }
}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

需要重点说明的是,匹配结果往往有很多误匹配,为了排除这些错误,这里使用了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;
}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

三维重建

现在已经知道了两个相机之间的变换矩阵,还有每一对匹配点的坐标。三维重建就是通过这些已知信息还原匹配点在空间当中的坐标。在前面的推导中,我们有 

s2x2=K(R2X+T2)

这个等式中有两个未知量,分别是 s2 X 。用 x2 对等式两边做外积,可以消去 s2 ,得 
0=x2ˆK(R2X+T2)

整理一下可以得到一个关于空间坐标X的线性方程 
x2ˆKR2X=x2ˆKT2

上面的方程不能直接取逆求解,因此化为其次方程 
x2ˆK(R2  T)(X1)=0

用SVD求X左边矩阵的零空间,再将最后一个元素归一化到1,即可求得X。其几何意义相当于分别从两个相机的光心作过 x1 x2 的延长线,延长线的焦点即为方程的解,如文章最上方的图所示。由于这种方法和三角测距类似,因此这种重建方式也被称为三角化(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);
}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

测试

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

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

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

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

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

代码下载


  • 2
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
<项目介绍> 该资源内项目源码是个人的毕设,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到94.5分,放心下载使用! 该资源适合计算机相关专业(如人工智能、通信工程、自动化、软件工程等)的在校学生、老师或者企业员工下载,适合小白学习或者实际项目借鉴参考! 当然也可作为毕业设计、课程设计、课程作业、项目初期立项演示等。如果基础还行,可以在此代码基础之上做改动以实现更多功能。 双目测距理论及其python运用 一、双目测距基本流程 Stereo Vision, 也叫双目立体视觉,它的研究可以帮助我们更好的理解人类的双眼是如何进行深度感知的。双目视觉在许多领域得到了应用,例如城市三维重建、3D模型构建(如kinect fusion)、视角合成、3D跟踪、机器人导航(自动驾驶)、人类运动捕捉(Microsoft Kinect)等等。双目测距也属于双目立体视觉的一个应用领域,双目测距的基本原理主要是三角测量原理,即通过视差来判定物体的远近。 那么总结起来,双目测距的大致流程就是: **双目标定 --> 立体校正(含消除畸变) --> 立体匹配 --> 视差计算 --> 深度计算(3D坐标)计算** linux下安装opencv-python: ```python pip install opencv-python ``` 、相机畸变 光线经过相机的光学系统往往不能按照理想的情况投射到传感器上,也就是会产生所谓的畸变。畸变有两种情况:一种是由透镜形状引起的畸变称之为径向畸变。在针孔模型中,一条直线投影到像素平面上还是一条直线。可是,在实际拍摄的照片中,摄像机的透镜往往使得真实环境中的一条直线在图片中变成了曲线。越靠近图像的边缘,这种现象越明显。由于实际加工制作的透镜往往是中心对称的,这使得不规则的畸变通常径向对称。它们主要分为两大类,桶形畸变 和 枕形畸变(摘自《SLAM十四讲》)如图所示: <div align=center><img src="https://img-blog.csdnimg.cn/20190907184815326.PNG" width="324" height="100" /></div> 桶形畸变是由于图像放大率随着离光轴的距离增加而减小,而枕形畸变却恰好相反。 在这两种畸变中,穿过图像中心和光轴有交点的直线还能保持形状不变。
YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明 YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明YOLO高分设计资源源码,详情请查看资源内容中使用说明

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值