OpenCV实战(25)——3D场景重建

news2024/12/25 9:03:48

OpenCV实战(25)——3D场景重建

    • 0. 前言
    • 1. 重建 3D 场景
      • 1.1 3D 场景点重建
      • 1.2 算法原理
    • 2. 分解单应性
    • 3. 光束平差法
    • 4. 完整代码
    • 小结
    • 系列链接

0. 前言

在《相机姿态估计》一节中,我们学习了如何在校准相机时恢复观察 3D 场景的相机的位置。算法应用了以下事实,即有时场景中可见的某些 3D 点的坐标可能是已知的。而如果能够从多个角度观察场景,即使没有关于 3D 场景的信息可用,也可以重建 3D 姿势和结构。在本节中,我们将使用不同视图中图像点之间的对应关系来推断 3D 信息,同时介绍一个新的数学实体用于校准相机的两个视图之间的关系,并将讨论三角测量的原理以便从 2D 图像重建 3D 点。

1. 重建 3D 场景

我们使用校准后的相机,拍摄两张场景照片,然后使用 SIFT (Scale Invariant Feature Transform) 检测器和描述符来匹配这两个视图之间的特征点。
相机的校准参数为我们在世界坐标系中的计算奠定了基础,需要在相机姿态和相应点的位置之间建立物理约束。我们引入了一个新的数学实体,本质矩阵 (essential matrix),它基本矩阵的校准版本, cv::findEssentialMat 函数可用于计算本质矩阵,估计图像中的投影关系。
我们可以使用已建立的点对应调用 cv::findEssentialMat 函数,并通过随机抽样一致算法 (RANdom SAmple Consensus, RANSAC) 算法过滤掉异常点以保留符合找到的几何形状的匹配项。

1.1 3D 场景点重建

(1) 首先,使用 SIFT 检测器和描述符在每个图像中获取关键点:

// 关键点和描述符向量
std::vector<cv::KeyPoint> keypoints1;
std::vector<cv::KeyPoint> keypoints2;
cv::Mat descriptors1, descriptors2;
// SIFT特征检测器
cv::Ptr<cv::Feature2D> ptrFeature2D = cv::xfeatures2d::SIFT::create(500);
// SIFT 特征及其描述符
ptrFeature2D->detectAndCompute(image1, cv::noArray(), keypoints1, descriptors1);
ptrFeature2D->detectAndCompute(image2, cv::noArray(), keypoints2, descriptors2);

(2) 使用 cv::BFMatcher 匹配使用描述符检测到的关键点并将它们存储为浮点数:

// 匹配两张图像的描述符
// 带有交叉检查的匹配器
cv::BFMatcher matcher(cv::NORM_L2, true);
// 匹配
std::vector<cv::DMatch> matches;
matcher.match(descriptors1, descriptors2, matches);
// 将关键点转换为 Point2f
std::vector<cv::Point2f> points1, points2;
for (std::vector<cv::DMatch>::const_iterator it = matches.begin();
it != matches.end(); ++it) {
    float x = keypoints1[it->queryIdx].pt.x;
    float y = keypoints1[it->queryIdx].pt.y;
    points1.push_back(cv::Point2f(x, y));
    x = keypoints2[it->trainIdx].pt.x;
    y = keypoints2[it->trainIdx].pt.y;
    points2.push_back(cv::Point2f(x, y));
}

(3) 使用 cv::findEssentialMat 函数,获得两个图像中匹配的图像关键点的相机矩阵:

// 图像 1 和图像 2 之间的本质矩阵 
cv::Mat inliers;
cv::Mat essential = cv::findEssentialMat(
        points1, points2, 
        cMatrix,                // 固有参数
        cv::RANSAC, 0.9, 1.0,   // RANSAC 方法 
        inliers);

生成的 inliers 匹配集如下:

生成匹配集
(4) 基本矩阵封装了两个视图的旋转和平移组件。因此,可以直接从这个矩阵中恢复两个视图之间的相对姿态,这可以利用 OpenCV 函数 cv::recoverPose 完成:

// 从本质矩阵中恢复相对相机姿态
cv::Mat rotation, translation;
cv::recoverPose(essential,      // 本质矩阵
        points1, points2,       // 匹配关键点
        cameraMatrix,           // 内在矩阵
        rotation, translation,  // 姿态估计
        inliers);               // inliers 匹配

(5) 有了两个相机之间的相对位姿,可以估计我们在两个视图之间建立对应关系的点的位置。下图显示了两台摄像机的估计位置(左边的一台位于原点)。我们选择一对对应点,并根据投影几何模型,追踪对应于相关 3D 点所有可能位置的一条射线:

投影集合模型
由于这两个图像点是由同一个 3D 点生成的,所以两条射线必定会相交于 3D 点所在位置。当两个相机的相对位置已知时,将两个对应图像点的投影线相交的方法称为三角测量 (triangulation)。这个过程首先需要两个投影矩阵,并且可以重复应用于所有匹配,但必须使用世界坐标表示。可以通过使用 cv::undistortPoints 函数来完成以上过程。

(6) 最后,调用 triangulate 函数计算三角点的位置:

// 由 R, T 组成投影矩阵 
cv::Mat projection2(3, 4, CV_64F);      // 3x4 投影矩阵
rotation.copyTo(projection2(cv::Rect(0, 0, 3, 3)));
translation.copyTo(projection2.colRange(3, 4));
// 合成通用投影矩阵
cv::Mat projection1(3, 4, CV_64F, 0.);  // 3x4 投影矩阵
cv::Mat diag(cv::Mat::eye(3, 3, CV_64F));
diag.copyTo(projection1(cv::Rect(0, 0, 3, 3)));
// inliers
std::vector<cv::Vec2d> inlierPts1;
std::vector<cv::Vec2d> inlierPts2;
// 为三角测量创建 inliers 输入点向量 
int j(0); 
for (int i = 0; i < inliers.rows; i++) {
    if (inliers.at<uchar>(i)) {
        inlierPts1.push_back(cv::Vec2d(points1[i].x, points1[i].y));
        inlierPts2.push_back(cv::Vec2d(points2[i].x, points2[i].y));
    }
}
// 矫正并归一化图像点
std::vector<cv::Vec2d> points1u;
cv::undistortPoints(inlierPts1, points1u, cameraMatrix, cameraDistCoeffs);
std::vector<cv::Vec2d> points2u;
cv::undistortPoints(inlierPts2, points2u, cameraMatrix, cameraDistCoeffs);
// 三角测量
std::vector<cv::Vec3d> points3D;
triangulate(projection1, projection2, points1u, points2u, points3D);

(7) 计算得到位于场景元素上的 3D 点,如下所示:

3D 场景点
需要注意的是,在上图的角度来看,我们绘制的两条射线并没有按照预期相交。

1.2 算法原理

校准矩阵可以用于将像素坐标转换为世界坐标。应用该矩阵可以将图像点与产生它们的 3D 点相关联。在下图中演示了物理世界点与其图像之间的关系:

物理世界点与其图像之间的关系
上图显示了由旋转 R R R 和平移 T T T 变换的两个相机,平移向量 T T T 连接了两个相机的投影中心,还有一个 x x x 向量将第一个相机中心连接到一个图像点,以及一个 x ′ x' x 向量将第二个相机中心连接到相应的图像点。由于有两个相机之间的相对位置,我们可以根据第二个相机参考将 x x x 的方向表示为 R x R_x Rx。观察所示图像点的几何形状,可以发现 T T T R x R_x Rx x ′ x' x 向量都共面,可以用以下数学方程表示这种情况:
x ′ ⋅ ( T × R x ) = x ′ E x = 0 x'\cdot(T\times R_x)=x'Ex=0 x(T×Rx)=xEx=0
可以将第一个关系简化为一个 3x3 矩阵 E E E,因为叉积也可以通过矩阵运算来表示。这个 E E E 矩阵被称为本质矩阵,相关的方程是校准的对极约束。我们可以根据图像对应估计这个基本矩阵,计算过程与基本矩阵类似,区别在于需要使用世界坐标计算。此外,基本矩阵是根据两个相机之间的旋转和平移分量构建的,一旦估计了这个矩阵,就可以对其进行分解以获得相机之间的相对位姿。以上过程可以使用 cv::recoverPose 函数实现。cv::recoverPose 函数会调用 cv::decomposeEssentialMat 函数,cv::decomposeEssentialMat 函数为相对姿势生成四种可能的解,通过计算一组匹配来确定正确的解,从而确定物理上的可能解。
一旦获得了相机之间的相对位姿,就可以通过三角测量恢复与匹配对对应的点的位置。三角剖分问题最简单的解决方案需要考虑两个投影矩阵 P P P P ′ P' P,在齐次坐标中寻找 3D 点可以表示为 X = [ X , Y , Z , 1 ] T X=[X,Y,Z,1]^T X=[X,Y,Z,1]T,因为我们已经知道 x = P X x=PX x=PX x ′ = P ′ X x'=P'X x=PX。这两个齐次方程决定了两个独立方程,足以解决 3D 点位置的三个未知数。这个超定方程系统可以使用最小二乘法求解,以上过程可以通过 OpenCV 函数 cv::solve 完成:

// 使用线性LS方法进行三角测量
cv::Vec3d triangulate(const cv::Mat &p1, 
            const cv::Mat &p2, 
            const cv::Vec2d &u1, 
            const cv::Vec2d &u2) {
    // 假设方程组image=[u,v], X=[X,y,z,1],其中u(p3.X)=p1.X和v(p3.X)=p2.X
    cv::Matx43d A(u1(0)*p1.at<double>(2, 0) - p1.at<double>(0, 0), u1(0)*p1.at<double>(2, 1) - p1.at<double>(0, 1), u1(0)*p1.at<double>(2, 2) - p1.at<double>(0, 2),
                u1(1)*p1.at<double>(2, 0) - p1.at<double>(1, 0), u1(1)*p1.at<double>(2, 1) - p1.at<double>(1, 1), u1(1)*p1.at<double>(2, 2) - p1.at<double>(1, 2),
                u2(0)*p2.at<double>(2, 0) - p2.at<double>(0, 0), u2(0)*p2.at<double>(2, 1) - p2.at<double>(0, 1), u2(0)*p2.at<double>(2, 2) - p2.at<double>(0, 2),
                u2(1)*p2.at<double>(2, 0) - p2.at<double>(1, 0), u2(1)*p2.at<double>(2, 1) - p2.at<double>(1, 1), u2(1)*p2.at<double>(2, 2) - p2.at<double>(1, 2));
    cv::Matx41d B(p1.at<double>(0, 3) - u1(0)*p1.at<double>(2, 3),
                p1.at<double>(1, 3) - u1(1)*p1.at<double>(2, 3),
                p2.at<double>(0, 3) - u2(0)*p2.at<double>(2, 3),
                p2.at<double>(1, 3) - u2(1)*p2.at<double>(2, 3));
    // X 包含重建点的三维坐标 
    cv::Vec3d X;
    // 求解 AX=B
    cv::solve(A, B, X, cv::DECOMP_SVD);
    return X;
}

在上一小节得到的结果图像中我们已经看到,由于噪声和数字化的影响,预期应该相交的投影线实际上并不相交。因此,最小二乘解会在交点附近得到解。此外,如果尝试在无穷远处重建一个点,则此方法并不起作用,这是因为,对于这样的点,齐次坐标的第四个元素应该为 0 而不是 1
最后,重要的是需要知道比例因子才能完成 3D 重建,如果需要进行实际测量,至少需要知道一个物理距离,例如,两个相机之间的实际距离或某一物体的高度。
3D 重建是计算机视觉中一个重要的研究领域,接下来,我们介绍在 OpenCV 库中相关的一些其他内容。

2. 分解单应性

在本节中,我们了解到可以分解通过一个基本矩阵恢复两个相机之间的旋转和平移分量。我们知道平面的两个视图之间存在单应性,该单应性还包含旋转和平移分量。此外,它还包含有关平面的信息,即它相对于每个相机的法线。cv::decomposeHomographyMat 函数可以用来分解这个矩阵;但是,我们需要有一台校准过的相机。

3. 光束平差法

在本节中,我们首先从匹配中估计相机位置,然后通过三角测量重建关联的 3D 点。可以通过使用任意数量的视图来完成这个过,检测每一视图的特征点并将其与其他视图匹配。基于此,可以得到与视图、3D 点集和校准信息之间的旋转和平移相关方程。所有这些未知数都可以通过优化过程一起优化,优化过程旨在最小化每个视图中所有可见点的重投影误差。这种组合优化过程称为捆集调整或光束平差法 (bundle adjustment)。cv::detail::BundleAdjusterReproj 类实现了相机参数计算算法,其最小化重投影误差平方之和。

4. 完整代码

头文件 (triangulate.h) 完整代码如下所示:

#include <opencv2/core/core.hpp>
#include <vector>

// 使用线性LS方法进行三角测量
cv::Vec3d triangulate(const cv::Mat &p1, 
            const cv::Mat &p2, 
            const cv::Vec2d &u1, 
            const cv::Vec2d &u2);
void triangulate(const cv::Mat &p1, 
            const cv::Mat &p2, 
            const std::vector<cv::Vec2d> &pts1, 
            const std::vector<cv::Vec2d> &pts2, 
            std::vector<cv::Vec3d> &pts3D);

// 使用线性LS方法进行三角测量
cv::Vec3d triangulate(const cv::Mat &p1, 
            const cv::Mat &p2, 
            const cv::Vec2d &u1, 
            const cv::Vec2d &u2) {
    // 假设方程组image=[u,v], X=[X,y,z,1],其中u(p3.X)=p1.X和v(p3.X)=p2.X
    cv::Matx43d A(u1(0)*p1.at<double>(2, 0) - p1.at<double>(0, 0), u1(0)*p1.at<double>(2, 1) - p1.at<double>(0, 1), u1(0)*p1.at<double>(2, 2) - p1.at<double>(0, 2),
                u1(1)*p1.at<double>(2, 0) - p1.at<double>(1, 0), u1(1)*p1.at<double>(2, 1) - p1.at<double>(1, 1), u1(1)*p1.at<double>(2, 2) - p1.at<double>(1, 2),
                u2(0)*p2.at<double>(2, 0) - p2.at<double>(0, 0), u2(0)*p2.at<double>(2, 1) - p2.at<double>(0, 1), u2(0)*p2.at<double>(2, 2) - p2.at<double>(0, 2),
                u2(1)*p2.at<double>(2, 0) - p2.at<double>(1, 0), u2(1)*p2.at<double>(2, 1) - p2.at<double>(1, 1), u2(1)*p2.at<double>(2, 2) - p2.at<double>(1, 2));
    cv::Matx41d B(p1.at<double>(0, 3) - u1(0)*p1.at<double>(2, 3),
                p1.at<double>(1, 3) - u1(1)*p1.at<double>(2, 3),
                p2.at<double>(0, 3) - u2(0)*p2.at<double>(2, 3),
                p2.at<double>(1, 3) - u2(1)*p2.at<double>(2, 3));
    // X 包含重建点的三维坐标 
    cv::Vec3d X;
    // 求解 AX=B
    cv::solve(A, B, X, cv::DECOMP_SVD);
    return X;
}

// 三角化图像点的矢量 
void triangulate(const cv::Mat &p1,
            const cv::Mat &p2,
            const std::vector<cv::Vec2d> &pts1,
            const std::vector<cv::Vec2d> &pts2,
            std::vector<cv::Vec3d> &pts3D) {
    for (int i = 0; i < pts1.size(); i++) {
        pts3D.push_back(triangulate(p1, p2, pts1[i], pts2[i]));
    }
}

主函数文件 (estimateE.cpp) 完整代码如下所示:

#include <iostream>
#include <vector>
#include <numeric>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/calib3d/calib3d.hpp>
#include <opencv2/objdetect/objdetect.hpp>
#include <opencv2/xfeatures2d.hpp>
#include <opencv2/viz.hpp>
#include "triangulate.h"

int main() {
    // 读取输入图像
    cv::Mat image1= cv::imread("1.png",0);
    cv::Mat image2= cv::imread("2.png",0);
    if (!image1.data || !image2.data)
        return 0; 
    cv::namedWindow("Right Image");
    cv::imshow("Right Image",image1);
    cv::namedWindow("Left Image");
    cv::imshow("Left Image",image2);
    // 读取相机标定参数
    cv::Mat cameraMatrix;
    cv::Mat cameraDistCoeffs;
    cv::FileStorage fs("calib.xml", cv::FileStorage::READ);
    fs["Intrinsic"] >> cameraMatrix;
    fs["Distortion"] >> cameraDistCoeffs;
    cameraMatrix.at<double>(0, 2) = 268.;
    cameraMatrix.at<double>(1, 2) = 178;
    std::cout << " Camera intrinsic: " << cameraMatrix.rows << "x" << cameraMatrix.cols << std::endl;
    std::cout << cameraMatrix.at<double>(0, 0) << " " << cameraMatrix.at<double>(0, 1) << " " << cameraMatrix.at<double>(0, 2) << std::endl;
    std::cout << cameraMatrix.at<double>(1, 0) << " " << cameraMatrix.at<double>(1, 1) << " " << cameraMatrix.at<double>(1, 2) << std::endl;
    std::cout << cameraMatrix.at<double>(2, 0) << " " << cameraMatrix.at<double>(2, 1) << " " << cameraMatrix.at<double>(2, 2) << std::endl << std::endl;
    cv::Matx33f cMatrix(cameraMatrix);
    // 关键点和描述符向量
    std::vector<cv::KeyPoint> keypoints1;
    std::vector<cv::KeyPoint> keypoints2;
    cv::Mat descriptors1, descriptors2;
    // SIFT特征检测器
    cv::Ptr<cv::Feature2D> ptrFeature2D = cv::xfeatures2d::SIFT::create(500);
    // SIFT 特征及其描述符
    ptrFeature2D->detectAndCompute(image1, cv::noArray(), keypoints1, descriptors1);
    ptrFeature2D->detectAndCompute(image2, cv::noArray(), keypoints2, descriptors2);
    std::cout << "Number of feature points (1): " << keypoints1.size() << std::endl;
    std::cout << "Number of feature points (2): " << keypoints2.size() << std::endl;
    // 匹配两张图像的描述符
    // 带有交叉检查的匹配器
    cv::BFMatcher matcher(cv::NORM_L2, true);
    // 匹配
    std::vector<cv::DMatch> matches;
    matcher.match(descriptors1, descriptors2, matches);
    // 绘制匹配
    cv::Mat imageMatches;
    cv::drawMatches(image1, keypoints1, // 第一张图像及其关键点
            image2, keypoints2,         // 第二张图像及其关键点
            matches,                    // 匹配
            imageMatches,               // 图像结果
            cv::Scalar(255, 255, 255),
            cv::Scalar(255, 255, 255),
            std::vector<char>(),
            cv::DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
    cv::namedWindow("Matches");
    cv::imshow("Matches", imageMatches);
    // 将关键点转换为 Point2f
    std::vector<cv::Point2f> points1, points2;
    for (std::vector<cv::DMatch>::const_iterator it = matches.begin();
    it != matches.end(); ++it) {
        float x = keypoints1[it->queryIdx].pt.x;
        float y = keypoints1[it->queryIdx].pt.y;
        points1.push_back(cv::Point2f(x, y));
        x = keypoints2[it->trainIdx].pt.x;
        y = keypoints2[it->trainIdx].pt.y;
        points2.push_back(cv::Point2f(x, y));
    }
    std::cout << "Number of matches: " << points2.size() << std::endl;
    // 图像 1 和图像 2 之间的本质矩阵 
    cv::Mat inliers;
    cv::Mat essential = cv::findEssentialMat(
            points1, points2, 
            cMatrix,                // 固有参数
            cv::RANSAC, 0.9, 1.0,   // RANSAC 方法 
            inliers);
    int numberOfPts(cv::sum(inliers)[0]);
    std::cout << "Number of inliers: " << numberOfPts << std::endl;
    // 绘制 inliers 匹配
    cv::drawMatches(image1, keypoints1, // 第一张图像及其关键点
            image2, keypoints2,         // 第二张图像及其关键点
            matches,                    // 匹配
            imageMatches,               // 图像结果
            cv::Scalar(255, 255, 255),
            cv::Scalar(255, 255, 255),
            inliers,
            cv::DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
    cv::namedWindow("Inliers matches");
    cv::imshow("Inliers matches", imageMatches);
    // 从本质矩阵中恢复相对相机姿态
    cv::Mat rotation, translation;
    cv::recoverPose(essential,      // 本质矩阵
            points1, points2,       // 匹配关键点
            cameraMatrix,           // 内在矩阵
            rotation, translation,  // 姿态估计
            inliers);               // inliers 匹配
    std::cout << "rotation:" << rotation << std::endl;
    std::cout << "translation:" << translation << std::endl;
    // 由 R, T 组成投影矩阵 
    cv::Mat projection2(3, 4, CV_64F);      // 3x4 投影矩阵
    rotation.copyTo(projection2(cv::Rect(0, 0, 3, 3)));
    translation.copyTo(projection2.colRange(3, 4));
    // 合成通用投影矩阵
    cv::Mat projection1(3, 4, CV_64F, 0.);  // 3x4 投影矩阵
    cv::Mat diag(cv::Mat::eye(3, 3, CV_64F));
    diag.copyTo(projection1(cv::Rect(0, 0, 3, 3)));
    std::cout << "First Projection matrix=" << projection1 << std::endl;
    std::cout << "Second Projection matrix=" << projection2 << std::endl;
    // inliers
    std::vector<cv::Vec2d> inlierPts1;
    std::vector<cv::Vec2d> inlierPts2;
    // 为三角测量创建 inliers 输入点向量 
    int j(0); 
    for (int i = 0; i < inliers.rows; i++) {
        if (inliers.at<uchar>(i)) {
            inlierPts1.push_back(cv::Vec2d(points1[i].x, points1[i].y));
            inlierPts2.push_back(cv::Vec2d(points2[i].x, points2[i].y));
        }
    }
    // 矫正并归一化图像点
    std::vector<cv::Vec2d> points1u;
    cv::undistortPoints(inlierPts1, points1u, cameraMatrix, cameraDistCoeffs);
    std::vector<cv::Vec2d> points2u;
    cv::undistortPoints(inlierPts2, points2u, cameraMatrix, cameraDistCoeffs);
    // 三角测量
    std::vector<cv::Vec3d> points3D;
    triangulate(projection1, projection2, points1u, points2u, points3D);
    // 创建 viz 窗口
    cv::viz::Viz3d visualizer("Viz window");
    visualizer.setBackgroundColor(cv::viz::Color::white());
    // 场景重建
    // 创建第 1 个虚拟相机
    cv::viz::WCameraPosition cam1(cMatrix,      // 内在矩阵
            image1,                             // 平面图像
            1.0,                                // 缩放因子
            cv::viz::Color::black());
    // 创建第 2 个虚拟相机
    cv::viz::WCameraPosition cam2(cMatrix,      // 内在矩阵
            image2,                             // 平面图像
            1.0,                                // 缩放因子
            cv::viz::Color::black());
    // 选择可视化点
    cv::Vec3d testPoint = triangulate(projection1, projection2, points1u[124], points2u[124]);
    cv::viz::WSphere point3D(testPoint, 0.05, 10, cv::viz::Color::red());
    // 相关投影线
    double lenght(4.);
    cv::viz::WLine line1(cv::Point3d(0., 0., 0.), cv::Point3d(lenght*points1u[124](0), lenght*points1u[124](1), lenght), cv::viz::Color::green());
    cv::viz::WLine line2(cv::Point3d(0., 0., 0.), cv::Point3d(lenght*points2u[124](0), lenght*points2u[124](1), lenght), cv::viz::Color::green());
    // 重建 3D 点云
    cv::viz::WCloud cloud(points3D, cv::viz::Color::blue());
    cloud.setRenderingProperty(cv::viz::POINT_SIZE, 3.);
    // 添加虚拟对象
    visualizer.showWidget("Camera1", cam1);
    visualizer.showWidget("Camera2", cam2);
    visualizer.showWidget("Cloud", cloud);
    visualizer.showWidget("Line1", line1);
    visualizer.showWidget("Line2", line2);
    visualizer.showWidget("Triangulated", point3D);
    // 移动第二张图像
    cv::Affine3d pose(rotation, translation);
    visualizer.setWidgetPose("Camera2", pose);
    visualizer.setWidgetPose("Line2", pose);
    // 可视化动画
    while (cv::waitKey(100) == -1 && !visualizer.wasStopped())
    {
        visualizer.spinOnce(1,  // 暂停 1ms 
                true);          // 重绘
    }
    cv::waitKey();
    return 0;
}

小结

3D 重建是计算机视觉中一个重要的研究领域,在本节中,我们使用不同视图中图像点之间的对应关系来推断 3D 信息,使用 cv::findEssentialMat 函数计算本质矩阵,并介绍了三角测量的原理以从 2D 图像重建 3D 点。

系列链接

OpenCV实战(1)——OpenCV与图像处理基础
OpenCV实战(2)——OpenCV核心数据结构
OpenCV实战(3)——图像感兴趣区域
OpenCV实战(4)——像素操作
OpenCV实战(5)——图像运算详解
OpenCV实战(6)——OpenCV策略设计模式
OpenCV实战(7)——OpenCV色彩空间转换
OpenCV实战(8)——直方图详解
OpenCV实战(9)——基于反向投影直方图检测图像内容
OpenCV实战(10)——积分图像详解
OpenCV实战(11)——形态学变换详解
OpenCV实战(12)——图像滤波详解
OpenCV实战(13)——高通滤波器及其应用
OpenCV实战(14)——图像线条提取
OpenCV实战(15)——轮廓检测详解
OpenCV实战(16)——角点检测详解
OpenCV实战(17)——FAST特征点检测
OpenCV实战(18)——特征匹配
OpenCV实战(19)——特征描述符
OpenCV实战(20)——图像投影关系
OpenCV实战(21)——基于随机样本一致匹配图像
OpenCV实战(22)——单应性及其应用
OpenCV实战(23)——相机标定
OpenCV实战(24)——相机姿态估计

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/591673.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

TypeScript的10个缺点

文章目录 1. 语法繁琐2. 难以集成到一些工作流程3. 学习成本高4. 代码量多5. 编译时间长6. 在小型项目中无必要性7. 可读性降低8. 抽象层次增加9. 缺少类型定义10. 生态系统 1. 语法繁琐 TypeScript 的类型注解、泛型等语法增加了代码的复杂度和学习难度&#xff0c;对小型项目…

LC-1130. 叶值的最小代价生成树(贪心、区间DP、单调栈)

1130. 叶值的最小代价生成树 难度中等272 给你一个正整数数组 arr&#xff0c;考虑所有满足以下条件的二叉树&#xff1a; 每个节点都有 0 个或是 2 个子节点。数组 arr 中的值与树的中序遍历中每个叶节点的值一一对应。每个非叶节点的值等于其左子树和右子树中叶节点的最大…

chatgpt赋能python:Python中的逆序操作

Python 中的逆序操作 在 Python 中&#xff0c;逆序&#xff08;reverse&#xff09;操作指的是将一个序列的元素顺序反转&#xff0c;也即将序列中最后一个元素变成第一个&#xff0c;倒数第二个元素变成第二个&#xff0c;以此类推。逆序有很多实际用途&#xff0c;比如根据…

基于C语言的平衡二叉树操作(包含完整代码)

平衡二叉树的定义: 为避免树的高度增长过快&#xff0c;降低二叉排序树的性能&#xff0c;规定在插入和删除二叉树结点时&#xff0c;要保证任意结点的左、右子树高度差的绝对值不超过1&#xff0c;将这样的二义树称为平衡二叉树AVL (Balanced Binary Tree),简称平衡树。 平衡…

【源码解析】流控框架Sentinel源码深度解析

前言 前面写了一篇Sentinel的源码解析&#xff0c;主要侧重点在于Sentinel流程的运转原理。流控框架Sentinel源码解析&#xff0c;侧重点在整个流程。该篇文章将对里面的细节做深入剖析。 统计数据 StatisticSlot用来统计节点访问次数 SpiOrder(-7000) public class Statis…

PCL 改进点云双边滤波算法

文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 我们先来回顾一下之前该算法的计算过程,在二维图像领域中,双边滤波算法是通过考虑中心像素点到邻域像素点的距离(一边)以及像素亮度差值所确定的权重(另一边)来修正当前采样中心点的位置,从而达到平滑滤波效果。…

PHPMySQL基础(五):模拟登录后跳转+会话存储功能实现

PHP&MySQL基础&#xff08;一&#xff09;:创建数据库并通过PHP进行连接_长风沛雨的博客-CSDN博客 PHP&MySQL基础&#xff08;二&#xff09;:通过PHP对MySQL进行增、删、改、查_长风沛雨的博客-CSDN博客 PHP&MySQL基础&#xff08;三&#xff09;:处理查询SQL返…

一图看懂 tqdm 模块:一个可在循环和命令行中使用的快速、可扩展的进度条,资料整理+笔记(大全)

本文由 大侠(AhcaoZhu)原创&#xff0c;转载请声明。 链接: https://blog.csdn.net/Ahcao2008 一图看懂 tqdm 模块&#xff1a;一个可在循环和命令行中使用的快速、可扩展的进度条&#xff0c;资料整理笔记&#xff08;大全&#xff09; &#x1f9ca;摘要&#x1f9ca;模块图&…

软考高级架构师笔记-5计算机网络

目录 1. 前言 & 考情分析2. 网络功能和分类2.1 通信技术3. OSI七层模型及协议3. 1 局域网和广域网协议3. 2 协议3. 3 交换技术、路由、传输介质4 IP地址5 网络存储技术6 其它考点8. 结语1. 前言 & 考情分析 前文回顾: 软考高级架构师笔记-1计算机硬件软考高级架构师笔…

chatgpt赋能python:Python中未定义变量的默认值

Python中未定义变量的默认值 在Python编程中&#xff0c;有时候我们会使用未经定义的变量。如果这些变量没有被定义&#xff0c;那么它们将没有任何值。在这篇文章中&#xff0c;我们将讨论Python中未定义变量默认值的问题&#xff0c;并深入研究为什么这些默认值如此重要。 …

华为OD机试真题B卷 Java 实现【寻找关键钥匙】,附详细解题思路

一、题目描述 小强正在参加《密室逃生》游戏&#xff0c;当前关卡要求找到符合给定 密码K&#xff08;升序的不重复小写字母组成&#xff09;的箱子&#xff0c;并给出箱子编号&#xff0c;箱子编号为1~N。 每个箱子中都有一个字符串s&#xff0c;字符串由大写字母&#xff0…

改进YOLOv5,利用HRNet高分辨率特征金字塔的全新物体检测突破

目录 一、介绍1、物体检测的背景与重要性2、HRNet和YOLOv5的概述&#xff08;1&#xff09;HRNet的概述&#xff08;2&#xff09;YOLOv5的概述 二、HRNet的架构1、HRNet的基本单元2、HRNet的高分辨率特征金字塔3、HRNet的体系结构4、HRNet的特点5、HRNet的局限性 三、YOLOv5的…

chatgpt赋能python:Python中转化为列表的详细介绍

Python中转化为列表的详细介绍 Python是一门高级编程语言&#xff0c;它使用起来简单易学&#xff0c;被广泛应用于大数据处理、科学计算、机器学习等领域。在Python编程中&#xff0c;列表是一种非常重要的数据结构&#xff0c;它允许我们存储和操作一组数据&#xff0c;并且…

jenkins —— pipeline基础语法与示例

一、Jenkins介绍 二、Jenkins Pipeline介绍 Jenkins Pipeline总体介绍 1.Pipeline 是Jenkins 2.X核心特性&#xff0c;帮助Jenkins实现从CI到CD与DevOps的转变 2.Pipeline 简而言之&#xff0c;就是一套运行于Jenkins上的工作流框架&#xff0c;将原本独立 运行于单个或者多个…

GPT-4 的 6 个最佳使用场景

https://www.howtogeek.com/884077/best-uses-for-chatgpt-4/ 作者&#xff1a;SYDNEY BUTLER 无论是在 ChatGPT 中还是通过 API&#xff0c;对 OpenAI 的 GPT-4 模型的访问比 GPT-3.5 限制更多。这意味着你需要慎重考虑在何种情况下使用 GPT-4&#xff0c;并选择性地将最适合…

浙大知识图谱基础:学习笔记

0 基础知识 知识图谱中&#xff0c;知识的结构化表示主要有符号表示和向量表示两类方法。符号表示包括&#xff1a;一阶谓词逻辑&#xff0c;语义网络&#xff0c;描述逻辑和框架系统等。当前主要采用基于图的符号化知识表示&#xff0c;最常用的是有向标记图。 有向标记图分为…

SpringBoot统一功能处理(统一处理用户登陆权限验证、统一异常处理以及统一数据返回格式)

目录 1. SpringBoot统一功能处理简介 2. 统一处理用户登陆验证 2.1 原生SpringAOP实现统一登陆验证的问题 2.2 Spring拦截器实现用户统一登陆验证 2.3 扩展: 统一访问前缀添加 3. 统一异常处理 4. 统一数据返回格式 4.1 统一数据返回格式的必要性 4.2 实现统一数据返…

C++ vector类成员函数介绍

目录 &#x1f914;vector模板介绍&#xff1a; &#x1f914;特点&#xff1a; &#x1f914;vector的成员函数&#xff1a; &#x1f50d;vector构造函数&#xff1a; &#x1f50d;vector赋值函数 &#x1f50d;vector容器的判断函数 resize函数的重点内容&#xff1a; …

chatgpt赋能python:Python中的并:优化代码执行效率的利器

Python中的并&#xff1a;优化代码执行效率的利器 Python作为一种优秀的编程语言&#xff0c;被广泛使用于各种各样的项目中。然而&#xff0c;随着程序的不断扩张&#xff0c;代码的复杂度日益增加&#xff0c;执行效率也愈加遇到了严峻的挑战。在这种情况下&#xff0c;Pyth…

Linux系统下imx6ull QT编程—— Ubuntu 下编写程序(一)

Linux QT编程 文章目录 Linux QT编程前言一、C简介二、C环境设置1.安装编译 C 语言和 C的环境。2.创建文件编写代码3.编译运行代码 总结 前言 绍在 Ubuntu 在终端窗口下使用 vi/vim 编辑一个 C源文件。通过编写最简单的示例“Hello,World QCX”。 一、C简介 C &#xff08;c…