OpenCV实战(23)——相机标定

news2024/11/30 14:27:52

OpenCV实战(23)——相机标定

    • 0. 前言
    • 1. 数字图像成像原理
    • 2. 相机标定
      • 2.1 执行相机校准
      • 2.2 使用已知相机参数校准
      • 2.3 使用圆网格进行校准
    • 3. 完整代码
    • 小结
    • 系列链接

0. 前言

我们已经了解了相机如何通过在 2D 传感器平面上投射光线来拍摄 3D 场景,生成的图像准确地表示了在捕获图像的瞬间从特定视点观察场景。然而,图像形成过程消除了与其所表示场景元素的深度有关的所有信息。为了恢复场景的 3D 结构和摄像机的 3D 姿态,我们需要对相机参数进行校准,在本节中,我们将介绍如何执行相机标定,为了更好的校准相机,我们首先简单回顾图像形成原理。

1. 数字图像成像原理

回顾在图像投影关系中介绍的图像形成过程,我们学习了针孔相机模型的原理。具体而言,该模型描述了在 3D 场景中位于位置 ( X , Y , Z ) (X, Y, Z) (X,Y,Z) 的点与其在相机图像位置 ( x , y ) (x, y) (x,y) 之间的关系:

针孔相机模型
为了更加深入了解坐标变换,我们添加了一个位于投影中心的参考系,并且令 y 轴指向下方,以确保坐标系与将图像原点放置在图像左上角的约定兼容。最后,我们还确定了图像平面上的一个特殊点——穿过焦点的线与图像平面正交,那么点 ( u 0 , v 0 ) (u_0, v_0) (u0,v0) 就是这条线穿过图像平面的像素位置,该特殊点称为主点 (principal point)。我们可以假设这个主要点位于图像平面的中心,但实际上,这个点可能会偏离中心几个像素,这取决于相机的制造精度。
在学习如何估计图像中的投影关系时,我们了解到针孔模型中相机的基本参数是其焦距和图像平面的大小。我们还了解如何将 3D ( X , Y , Z ) (X, Y, Z) (X,Y,Z) 投影到图像平面上 ( f X Z , f Y Z ) (f\frac XZ, f\frac YZ) (fZX,fZY) 处。此外,由于我们处理的是数字图像,因此图像平面上的像素数(即其分辨率)是相机的另一个重要特性。
如果我们想将此坐标转换为像素,需要分别将 2D 图像位置除以像素宽度 ( p x p_x px) 和像素高度 ( p y p_y py)。通过将以物理长度单位(通常以毫米为单位)给出的焦距除以 p x px px,我们可以得到以(水平)像素表示的焦距,我们将此定义为 f x f_x fx;类似地, f y = f p y f_y = \frac f{p_y} fy=pyf 定义为以垂直像素单位表示的焦距。因此,完整的投影方程如下:
y = f y Y Z + v 0 y=\frac {f_yY} Z+v_0 y=ZfyY+v0
其中, ( u 0 , v 0 ) (u_0, v_0) (u0,v0) 是添加到结果中的主点,以便将原点移动到图像的左上角。需要注意的是,像素的物理尺寸可以通过将图像传感器的尺寸(通常以毫米为单位)除以像素数量(水平或垂直)来获得。在现代传感器中,像素一般是方形的,即它们具有相同的水平和垂直尺寸。
我们可以使用矩阵形式重写以上方程,得到最一般形式的完整射影方程:
S [ x y 1 ] = [ f x 0 u 0 0 f y v 0 0 0 1 ] [ r 1 r 2 r 3 t 1 r 4 r 5 r 6 t 2 r 7 r 8 r 9 t 3 ] [ X Y Z 1 ] S\left[ \begin{array}{ccc} x\\ y\\ 1\\\end{array}\right]=\left[ \begin{array}{ccc} f_x&0&u_0\\ 0&f_y&v_0\\ 0&0&1\\\end{array}\right]\left[ \begin{array}{ccc} r_1&r_2&r_3&t_1\\ r_4&r_5&r_6&t_2\\ r_7&r_8&r_9&t_3\\\end{array}\right]\left[ \begin{array}{ccc} X\\ Y\\ Z\\ 1\\\end{array}\right] S xy1 = fx000fy0u0v01 r1r4r7r2r5r8r3r6r9t1t2t3 XYZ1

2. 相机标定

相机标定 (Camera calibration,也称相机校准)是获取不同相机参数的过程,我们可以使用相机制造商提供的规格数据,但是对于某些任务而言(例如 3D 重建),这些规格数据不够准确。相机校准通过向相机显示已知图案并分析获得的图像来工作,然后优化过程将确定解释观察结果的最佳参数值。这是一个复杂的过程,但我们可以利用 OpenCV 的标定函数来简化此过程。
要校准相机,需要向它展示一组已知 3D 位置的场景点。然后,需要观察这些点在图像上的投影位置。有了足够数量的 3D 点和相关的 2D 图像点,就可以从投影方程中推断出准确的相机参数,显然,为了获得准确的结果,我们需要观察尽可能多的点。实现此目的的一种方法是拍摄具有许多已知 3D 点的场景照片,但在实践中,可行性并不高。更方便的方法是从不同的视点拍摄一组 3D 点的多张图像,但是这种方法除了计算相机内部参数之外,还需要计算每个相机视点的位置,这种方法具有很高的可行性。
OpenCV 建议使用棋盘模式来生成校准所需的 3D 场景点集。棋盘模式在每个正方形的角上创建点,并且由于这种模式是平坦的,我们可以假设板 (board) 位于 Z = 0 Z = 0 Z=0x 轴和 y 轴根据网格对齐。在这种情况下,校准过程从不同视点向相机显示棋盘图案。以下是 6x4 校准图案图像的示例:

校准图像示例

OpenCV 中包含可以自动检测这个棋盘图案的角点的函数。

2.1 执行相机校准

在本节中,我们将使用 OpenCV 算法来检测棋盘点并计算相机校准。

(1) 只需提供图像和所用棋盘的大小(即水平和垂直内角点的数量),cv::findChessboardCorners 函数将返回这些棋盘角在图像上的位置,如果函数未能得到相机参数模型,则函数返回 false

// 输出图像点向量
std::vector<cv::Point2f> imageCorners;
// 棋盘内角点数
cv::Size boardSize(7, 5);
// 获取棋盘角点
bool found = cv::findChessboardCorners(image, boardSize, imageCorners);

(2) 输出参数 imageCorners 包含检测到的图案内角点的像素坐标,可以使用额外的参数对算法进行微调。函数 drawChessboardCorners 可以在棋盘图像上绘制检测到的角点,并用线将它们按顺序连接起来:

// 绘制棋盘角点
cv::drawChessboardCorners(image, 
            boardSize, imageCorners, 
            found); // 角点是否已检测

得到结果图像如下所示:

角点检测结果

连接点的线显示了点在检测到的图像点向量中的顺序。要执行相机校准,我们需要指定相应的 3D 点。

(3) 可以以所选择的单位(例如,以厘米或英寸为单位)指定这些点;然而,但最简单的方法是假设每个方块代表一个单位。在这种情况下,假设板的深度为 Z = 0 Z = 0 Z=0,则第一个点的坐标为 ( 0 , 0 , 0 ) (0, 0, 0) (0,0,0),第二个点将为 ( 1 , 0 , 0 ) (1, 0, 0) (1,0,0),依此类推,直到最后一个点 ( 7 , 5 , 0 ) (7, 5, 0) (7,5,0)。此模式中共有 48 个点,不足以获得准确的校准。

(4) 要获得更多的点,需要从不同的角度展示更多相同校准图案的图像。为此,可以在相机前移动图案或在图案前移动相机,从数学角度来看,两者是完全等价的。OpenCV 校准函数假设参考系固定在校准模式上,并将计算相机相对于参考系的旋转和平移。我们将校准过程封装在 CameraCalibrator 类中,类属性如下:

class CameraCalibrator {
    private:
        // 输入点:这些点位于世界坐标系
        // 每个方形是一个单元
        std::vector<std::vector<cv::Point3f> > objectPoints;
        // 图像点像素位置
        std::vector<std::vector<cv::Point2f> > imagePoints;
        // 输出矩阵
        cv::Mat cameraMatrix;
        cv::Mat distCoeffs;
        // 用于指定校准是否完成的标志 
        int flag;

(5) 场景和图像点输入向量,实际上是 std::vector 类的向量,每个元素都是一个视图中点的向量。我们通过指定棋盘图像文件名的向量作为 addChessBoardPoints 类函数的输入来添加校准点:

// 打开棋盘文件并提取角点
int CameraCalibrator::addChessboardPoints(
            const std::vector<std::string>& filelist,   // 包含棋盘图像的文件名列表
            cv::Size& boardSize,                        // 板尺寸
            std::string windowName) {

(6)addChessBoardPoints 函数中,我们首先必须初始化向量并设置棋盘的 3D 场景点:

// 棋盘上的点
std::vector<cv::Point2f> imageCorners;
std::vector<cv::Point3f> objectCorners;
// 3D 场景点
// 初始化在棋盘参考帧中的棋盘角点
// 角点在 3D 场景的位置 (X, Y, Z) = (i, j, 0)
for (int i=0; i<boardSize.height; i++) {
    for (int j=0; j<boardSize.width; j++) {
        objectCorners.push_back(cv::Point3f(i, j, 0.0f));
    }
}

(7) 我们必须读取输入列表的每个图像并使用 cv::findChessboardCorners 函数找到棋盘的角点:

// 2D 图像点——棋盘图像
cv::Mat image;
int successes = 0;
// 对于所有视点
for (int i=0; i<filelist.size(); i++) {
    image = cv::imread(filelist[i], 0);
    // 棋盘角点
    bool found = cv::findChessboardCorners(image,   // 棋盘图像
                    boardSize,                      // 棋盘尺寸
                    imageCorners);                  // 角点列表

(8) 此外,为了获得更准确的图像点位置,可以使用 cv::cornerSubPix 函数,然后将图像点定位在亚像素级别的精度上。由 cv::TermCriteria 对象指定的终止标准定义了子像素坐标中的最大迭代次数和最小精度级别,只要其中任一条件得到满足就会停止角点检测过程:

        // 在角点上获取亚像素精度
        if (found) {
            cv::cornerSubPix(image, imageCorners,
                    cv::Size(5, 5),         // 搜索窗口的一半大小
                    cv::Size(-1, -1),
                    cv::TermCriteria(cv::TermCriteria::MAX_ITER+cv::TermCriteria::EPS, 
                            30,             // 最大迭代次数
                            0.1));          // 最小准确率
            if (imageCorners.size()==boardSize.area()) {
                // 根据视图添加图像和场景点
                addPoints(imageCorners, objectCorners);
                successes++;
            }
        }
        if (windowName.length()>0 && imageCorners.size()==boardSize.area()) {
            // 绘制角点
            cv::drawChessboardCorners(image, boardSize, imageCorners, found);
            cv::imshow(windowName, image);
            cv::waitKey(100);
        }
    }
    return successes;
}

(9) 成功检测到一组棋盘角点后,使用 addPoints 方法将这些点添加到图像和场景点的向量中。 一旦处理了足够数量的棋盘图像(因此,有大量的 3D 场景点或 2D 图像点),就可以启动校准参数的计算:

// 相机标定
double CameraCalibrator::calibrate(const cv::Size imageSize) {
    // 初始化
    mustInitUndistort= true;
    // 输出旋转和平移
    std::vector<cv::Mat> rvecs, tvecs;
    // 相机标定
    return calibrateCamera(objectPoints,    // 3D 点
                    imagePoints,            // 图像点
                    imageSize,              // 图像尺寸
                    cameraMatrix,           // 输出相机矩阵
                    distCoeffs,             // 输出失真矩阵
                    rvecs, tvecs,           // Rs, Ts 
                    flag);                  // 设置选项
                    // ,CV_CALIB_USE_INTRINSIC_GUESS);
}

在实践中,1020 个棋盘图像就足够了,但这些图像必须从不同深度/不同视角拍摄,函数的两个重要输出是相机矩阵和失真参数。
为了解释校准的结果,我们需要回顾投影方程,该方程描述了通过连续应用两个矩阵将 3D 点转换为 2D 点的过程。第一个矩阵包含所有的相机参数,称为相机的内在参数 (intrinsic parameters),这个 3x3 矩阵是 cv::calibrateCamera 函数返回的输出矩阵之一。也可以使用另一个函数 cv::calibrationMatrixValues 显式返回校准矩阵给出的内在参数值。
第二个矩阵用于将输入点表示为以相机为中心的坐标,它由一个旋转向量(一个 3x3 矩阵)和一个平移向量(一个 3x1 矩阵)组成。在我们的校准示例中,参考系放置在棋盘上,因此,必须为每个视图计算刚性变换(由从 r 1 r_1 r1 r 9 r_9 r9 的矩阵项表示的旋转分量和由 t 1 t_1 t1 t 2 t_2 t2 t 3 t_3 t3 表示的平移分量组成)。这些值由 cv::calibrateCamera 函数的输出参数列表给出。旋转和平移分量通常被称为校准的外部参数 (extrinsic parameters),它们对于每个视图都是不同的。
对于给定的相机或镜头系统,内在参数保持不变。cv::calibrateCamera 提供的校准结果是通过优化过程获得的,该过程旨在计算内在和外在参数,以最小化根据 3D 场景点的投影计算的预测图像点位置与在图像上观察到的实际图像点位置之间的差异。校准期间指定的所有点的差异之和称为重投影误差。
以上测试相机从基于 48 个棋盘图像的校准中获得的内在参数为 f x = 409.272 f_x=409.272 fx=409.272 像素、 f y = 408.706 f_y=408.706 fy=408.706 像素、 u 0 = 237.248 u_0=237.248 u0=237.248 像素和 v 0 = 171.287 v0=171.287 v0=171.287 像素。校准图像大小为 536x356 像素。从校准结果中,可以看到,主点靠近图像的中心,但偏离了几个像素。查看拍摄校准图像所用相机的制造商规格,这款相机的传感器尺寸为 23.5mmx15.7mm,因此像素尺寸为 0.0438mm。计算出的焦距以像素表示,因此将结果乘以像素大小得出的近似焦距为 17.8mm,这与我们实际使用的镜头焦距一致。
接下来,我们考虑失真参数。在我们提到在针孔相机模型中可以忽略镜头的影响,但这是以用于捕获图像的镜头不会引入光学畸变为前提的。对于质量较低或焦距很短的镜头,并不能简单的忽略镜头的影响。可以看到,在示例中图像中显示的棋盘图案明显扭曲,且距离图像中心越远,这种失真变得越明显,这种畸变称为径向畸变 (radial distortion)。
可以通过引入适当的改进模型来补偿这些畸变。通过使用一组数学方程来表示由镜头引入的失真,逆向应用这些方程可以消除图像上可见的失真。可以在校准阶段使用其他相机参数获得校正失真的准确转换参数。完成此操作后,来自新校准相机的图像不会再失真。因此,我们需要在校准类 CameraCalibrator 中添加一个额外的方法:

// 移除图像失真
cv::Mat CameraCalibrator::remap(const cv::Mat &image, cv::Size &outputSize) {
    cv::Mat undistorted;
    if (outputSize.height == -1)
        outputSize = image.size();
    if (mustInitUndistort) {        // 每次校准调用一次
        cv::initUndistortRectifyMap(
                cameraMatrix,       // 计算摄像机矩阵
                distCoeffs,         // 计算失真矩阵
                cv::Mat(),          // 可选矫正
                cv::Mat(),          // 生成不失真的相机矩阵
                outputSize,         // 未失真尺寸
                CV_32FC1,           // 输出图类型
                map1, map2);        // x 和 y 映射函数 
        mustInitUndistort= false;
    }
    // 应用映射
    cv::remap(image, undistorted, map1, map2, 
                cv::INTER_LINEAR);  // 插值类型
    return undistorted;
}

运行此代码可以得到以下结果:

代码运行结果
如上图所示,一旦图像没有失真,我们就可以得到规则的透视图像。
为了纠正失真,OpenCV 使用多项式函数将图像点移动到未失真的位置。默认情况下,使用 5 个系数;也可以使用由 8 个系数组成的模型。一旦得到这些系数,就可以根据 cv::initUndistortRectifyMap 函数计算两个 cv::Mat 映射函数(一个用于 x 坐标,一个用于 y 坐标),给出失真图像上图像点在新图像中的未失真位置。cv::remap 函数将输入图像的所有点重新映射到新图像,由于映射为非线性变换,输入图像的某些像素可能映射在输出图像的边界之外,我们可以扩大输出图像的大小以减少这种像素损失。
在相机校准方面有更多可用选项。接下来,我们将对其进行讲解。

2.2 使用已知相机参数校准

当已知相机内在参数的估计值时,可以直接将它们输入到 cv::calibrateCamera 函数,然后它们将用作优化过程中的初始值。只需添加 CALIB_USE_INTRINSIC_GUESS 标志并在校准矩阵参数中输入这些值;也可以为主点 (CALIB_FIX_PRINCIPAL_POINT) 强加一个固定值,通常可以假设它是中心像素;还可以对 f x f_x fx f y f_y fy (CALIB_FIX_RATIO) 的焦距施加固定比率,例如假设像素为方形。

2.3 使用圆网格进行校准

OpenCV 还提供了使用实心圆网格校准相机的方案,作为棋盘图案的替代方法。在这种情况下,圆心用作校准点。所用的函数与我们用来定位棋盘角点的函数非常相似:

cv::Size boardSize(7, 7);
std::vector<cv::Point2f> centers;
bool found = cv::findCirclesGrid(image, boardSize, centers);

3. 完整代码

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

#if !defined CAMERACALIBRATOR_H
#define CAMERACALIBRATOR_H

#include <vector>
#include <iostream>

#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/calib3d/calib3d.hpp>
#include <opencv2/highgui/highgui.hpp>

class CameraCalibrator {
    private:
        // 输入点:这些点位于世界坐标系
        // 每个方形是一个单元
        std::vector<std::vector<cv::Point3f> > objectPoints;
        // 图像点像素位置
        std::vector<std::vector<cv::Point2f> > imagePoints;
        // 输出矩阵
        cv::Mat cameraMatrix;
        cv::Mat distCoeffs;
        // 用于指定校准是否完成的标志 
        int flag;
        // 用于矫正图像失真
        cv::Mat map1, map2;
        bool mustInitUndistort;
    public:
        CameraCalibrator() : flag(0), mustInitUndistort(true) {}
        // 打开棋盘文件并提取角点
        int addChessboardPoints(const std::vector<std::string>& filelist, cv::Size&boardSize, std::string windowName="");
        // 添加场景点及其对应图像点
        void addPoints(const std::vector<cv::Point2f>& imageCorners, const std::vector<cv::Point3f>& objectCorners);
        // 相机标定
        double calibrate(const cv::Size imageSize);
        // 设定标定标志
        void setCalibrationFlag(bool radial8CoeffEnabled=false, bool tangentialParamEnabled=false);
        // 移除图像失真
        cv::Mat remap(const cv::Mat& image, cv::Size& outputSize);
        cv::Mat getCameraMatrix() {return cameraMatrix;}
        cv::Mat getDistCoeffs() {return distCoeffs;}
};

#endif

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

#include <iostream>
#include <iomanip>
#include <vector>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/features2d.hpp>

#include "CameraCalibrator.h"

// 打开棋盘文件并提取角点
int CameraCalibrator::addChessboardPoints(
            const std::vector<std::string>& filelist,   // 包含棋盘图像的文件名列表
            cv::Size& boardSize,                        // 板尺寸
            std::string windowName) {
    // 棋盘上的点
    std::vector<cv::Point2f> imageCorners;
    std::vector<cv::Point3f> objectCorners;
    // 3D 场景点
    // 初始化在棋盘参考帧中的棋盘角点
    // 角点在 3D 场景的位置 (X, Y, Z) = (i, j, 0)
    for (int i=0; i<boardSize.height; i++) {
        for (int j=0; j<boardSize.width; j++) {
            objectCorners.push_back(cv::Point3f(i, j, 0.0f));
        }
    }
    // 2D 图像点——棋盘图像
    cv::Mat image;
    int successes = 0;
    // 对于所有视点
    for (int i=0; i<filelist.size(); i++) {
        image = cv::imread(filelist[i], 0);
        // 棋盘角点
        bool found = cv::findChessboardCorners(image,   // 棋盘图像
                        boardSize,                      // 棋盘尺寸
                        imageCorners);                  // 角点列表
        // 在角点上获取亚像素精度
        if (found) {
            cv::cornerSubPix(image, imageCorners,
                    cv::Size(5, 5),         // 搜索窗口的一半大小
                    cv::Size(-1, -1),
                    cv::TermCriteria(cv::TermCriteria::MAX_ITER+cv::TermCriteria::EPS, 
                            30,             // 最大迭代次数
                            0.1));          // 最小准确率
            if (imageCorners.size()==boardSize.area()) {
                // 根据视图添加图像和场景点
                addPoints(imageCorners, objectCorners);
                successes++;
            }
        }
        if (windowName.length()>0 && imageCorners.size()==boardSize.area()) {
            // 绘制角点
            cv::drawChessboardCorners(image, boardSize, imageCorners, found);
            cv::imshow(windowName, image);
            cv::waitKey(100);
        }
    }
    return successes;
}

// 添加场景点和相应的图像点
void CameraCalibrator::addPoints(const std::vector<cv::Point2f>& imageCorners, const std::vector<cv::Point3f>& objectCorners) {
    // 2D 图像点
    imagePoints.push_back(imageCorners);          
    // 对应 3D 场景点
    objectPoints.push_back(objectCorners);
}

// 相机标定
double CameraCalibrator::calibrate(const cv::Size imageSize) {
    // 初始化
    mustInitUndistort= true;
    // 输出旋转和平移
    std::vector<cv::Mat> rvecs, tvecs;
    // 相机标定
    return calibrateCamera(objectPoints,    // 3D 点
                    imagePoints,            // 图像点
                    imageSize,              // 图像尺寸
                    cameraMatrix,           // 输出相机矩阵
                    distCoeffs,             // 输出失真矩阵
                    rvecs, tvecs,           // Rs, Ts 
                    flag);                  // 设置选项
                    // ,CV_CALIB_USE_INTRINSIC_GUESS);
}

// 移除图像失真
cv::Mat CameraCalibrator::remap(const cv::Mat &image, cv::Size &outputSize) {
    cv::Mat undistorted;
    if (outputSize.height == -1)
        outputSize = image.size();
    if (mustInitUndistort) {        // 每次校准调用一次
        cv::initUndistortRectifyMap(
                cameraMatrix,       // 计算摄像机矩阵
                distCoeffs,         // 计算失真矩阵
                cv::Mat(),          // 可选矫正
                cv::Mat(),          // 生成不失真的相机矩阵
                outputSize,         // 未失真尺寸
                CV_32FC1,           // 输出图类型
                map1, map2);        // x 和 y 映射函数 
        mustInitUndistort= false;
    }
    // 应用映射
    cv::remap(image, undistorted, map1, map2, 
                cv::INTER_LINEAR);  // 插值类型
    return undistorted;
}

// 设置标定选项
void CameraCalibrator::setCalibrationFlag(bool radial8CoeffEnabled, bool tangentialParamEnabled) {
    // 设置用于 cv::calibrateCamera() 函数的标志
    flag = 0;
    if (!tangentialParamEnabled) flag += cv::CALIB_ZERO_TANGENT_DIST;
	if (radial8CoeffEnabled) flag += cv::CALIB_RATIONAL_MODEL;
}

int main() {
    cv::Mat image;
    std::vector<std::string> filelist;
    // 生成棋盘文件列表
    for (int i=1; i<=27; i++) {
        std::stringstream str;
        str << "chessboards/chessboard" << std::setw(2) << std::setfill('0') << i << ".jpg";
        std::cout << str.str() << std::endl;
        filelist.push_back(str.str());
        image= cv::imread(str.str(),0);
    }
    // 创建标定器对象
    CameraCalibrator cameraCalibrator;
    // 在棋盘中添加角点
    cv::Size boardSize(7,5);
    cameraCalibrator.addChessboardPoints(
            filelist,               // 棋盘文件列表
            boardSize, "Detected points");	// 棋盘尺寸
    // 相机标定
    cameraCalibrator.setCalibrationFlag(true, true);
    cameraCalibrator.calibrate(image.size());
    // 矫正图像失真
    image = cv::imread(filelist[14],0);
    cv::Size newSize(static_cast<int>(image.cols*1.5), static_cast<int>(image.rows*1.5));
    cv::Mat uImage= cameraCalibrator.remap(image, newSize);
    // 相机矩阵
    cv::Mat cameraMatrix= cameraCalibrator.getCameraMatrix();
    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;
    cv::namedWindow("Original Image");
    cv::imshow("Original Image", image);
    cv::namedWindow("Undistorted Image");
    cv::imshow("Undistorted Image", uImage);
    // 存储计算出的矩阵
    cv::FileStorage fs("calib.xml", cv::FileStorage::WRITE);
    fs << "Intrinsic" << cameraMatrix;
    fs << "Distortion" << cameraCalibrator.getDistCoeffs();
    cv::waitKey();
    return 0;
}

小结

相机标定,也称相机校准,是恢复场景的 3D 结构和摄像机的 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)——单应性及其应用

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

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

相关文章

使用RobustPCA 进行时间序列的异常检测

鲁棒主成分分析(Robust Principal Component Analysis, RobustPCA)是一种将时间序列矩阵分解为低秩分量和稀疏分量的技术。这种分解能够识别潜在的趋势&#xff0c;以及检测异常和异常值。在本中我们将研究RobustPCA的数学基础&#xff0c;介绍它与传统的PCA之间的区别&#xf…

GcDataViewer for JavaScript 6.1.0 Crack

GcDataViewer是跨平台 JavaScript 数据查看器,使用我们的 JavaScript 数据查看器在 Web 上打开和查看数据文档。 Load Excel &#xff08;XLSX&#xff09;、CSV 和SPREADJS&#xff08;SSJSON&#xff09; 文件&#xff0c;全部在一个查看器中 支持使用排序、筛选器和切片器进…

单词拼写检查textblob模块spellcheck方法

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 单词拼写检查 textblob模块 spellcheck方法 [太阳]选择题 关于python代码说法错误的一项是&#xff1a; from textblob import Word myWordsuccessfulee print("【显示】myWord …

【Spring事务】Spring事务事件控制,解决业务异步操作

使用背景 在业务中&#xff0c;经常会有这样的需求&#xff0c;在数据库事务提交之后&#xff0c;发送异步消息或者进行其他的事务操作。 例如当用户注册成功之后&#xff0c;发送激活码&#xff0c;如果用户注册后就执行发送激活码&#xff0c;但是在用户保存时出现提交事务…

【利用AI让知识体系化】入门Express框架

思维导图 文章目录 思维导图第一章&#xff1a;介绍Express什么是ExpressExpress优点Express应用场景 第二章&#xff1a;安装和基础用法安装Express搭建第一个Express应用中间件的使用 第三章&#xff1a;路由和控制器路由的原理路由的基本用法路由的进阶用法控制器的使用 第四…

BM 34 判断是否是二叉搜索树

判断是不是二叉搜索树_牛客题霸_牛客网 (nowcoder.com) 二叉搜索树满足每个节点的左子树上的所有节点均小于当前节点且右子树上的所有节点均大于当前节点。 递归去做 &#xff0c;一段一段的去判断是否满足条件 /*** struct TreeNode {* int val;* struct TreeNode *left;* str…

c++ 11标准模板(STL) std::set(二)

定义于头文件 <set> template< class Key, class Compare std::less<Key>, class Allocator std::allocator<Key> > class set;(1)namespace pmr { template <class Key, class Compare std::less<Key>> using se…

超实用!50+个ChatGPT提示词助你成为高效Web开发者(上)

如果你已经感到编写代码的重复和繁琐让你疲惫不堪&#xff0c;想要提高自己的效率&#xff0c;那么你来对地方了。ChatGPT是一款能够帮助你优化工作流程、减少错误并获得提高代码的见解的强大工具。 在这篇博客文章中&#xff0c;我们将向你提供超过50个提示和策略&#xff0c;…

OpenCV教程——形态学操作。膨胀,腐蚀,开操作,闭操作,形态学梯度,顶帽,黑帽

1.形态学操作 图像形态学操作&#xff1a;基于形状的一系列图像处理操作的合集&#xff0c;主要是基于集合论基础上的形态学数学。 形态学有四个基本操作&#xff1a;膨胀、腐蚀、开、闭。 2.膨胀与腐蚀 2.1.膨胀 跟卷积操作类似&#xff0c;假设有图像A和结构元素B&#…

路径规划算法:基于灰狼优化的路径规划算法- 附代码

路径规划算法&#xff1a;基于灰狼优化的路径规划算法- 附代码 文章目录 路径规划算法&#xff1a;基于灰狼优化的路径规划算法- 附代码1.算法原理1.1 环境设定1.2 约束条件1.3 适应度函数 2.算法结果3.MATLAB代码4.参考文献 摘要&#xff1a;本文主要介绍利用智能优化算法灰狼…

自媒体的孔雀效应:插根鸡毛还是专业才华?

自媒体时代&#xff0c;让许多原本默默无闻的人找到了表达自己的平台。有人声称&#xff0c;现在这个时代&#xff0c;“随便什么人身上插根鸡毛就可以当孔雀了”。可是&#xff0c;事实真的如此吗&#xff1f; 首先&#xff0c;我们不能否认的是&#xff0c;自媒体确实为大众提…

【大数据】通过 docker-compose 快速部署 Presto(Trino)保姆级教程

文章目录 一、概述二、前期准备1&#xff09;部署 docker2&#xff09;部署 docker-compose 三、创建网络四、Trino 编排部署1&#xff09;下载 trino2&#xff09;配置1、coordinator 配置2、worker 配置 3&#xff09;启动脚本 bootstrap.sh4&#xff09;构建镜像 Dockerfile…

多尺度深度特征(下):多尺度特征学习才是目标检测精髓(论文免费下载)...

计算机视觉研究院专栏 作者&#xff1a;Edison_G 深度特征学习方案将重点从具有细节的具体特征转移到具有语义信息的抽象特征。它通过构建多尺度深度特征学习网络 (MDFN) 不仅考虑单个对象和局部上下文&#xff0c;还考虑它们之间的关系。 公众号ID&#xff5c;ComputerVisionG…

MySQL- 存储引擎

MySQL体系结构 连接层 最上层是一些客户端和链接服务&#xff0c;包含本地sock 通信和大多数基于客户端/服务端工具实现的类似于 TCP/IP的通信。主要完成一些类似于连接处理、授权认证、及相关的安全方案。在该层上引入了线程 池的概念&#xff0c;为通过认证安全接入的客户端…

通过自动装箱和拆箱解释所定义基础数据类型和其对应封装类的区别

文章目录 前言一、拆装箱的实质二、拓展1.数值超过128的Integer装箱2.Java内存分配 前言 在刷软中的时候涉及到了值传递和地址传递传参的区别&#xff0c;其中提到不管是将基础数据类型的变量传值给对象数据类型的变量还是反过来都属于值传递&#xff0c;究其原因就是期间发生了…

【本地模式】第一个Mapreduce程序-wordcount

【本地模式】&#xff1a;也就是在windows环境下通过hadoop-client相关jar包进行开发的&#xff0c;我们只需要通过本地自己写好MapReduce程序即可在本地运行。 一个Maprduce程序主要包括三部分&#xff1a;Mapper类、Reducer类、执行类。 map阶段&#xff1a;将每一行单词提…

XShell远程连接

xshell 是一个强大的安全终端模拟软件&#xff0c;它支持SSH1,SSH2以及microsoft windows 平台的TELNET协议。xshell通过互联网到远程主机的安全连接。 xshell可以在windows界面下来访问远程终端不同系统下的服务器&#xff0c;从而比较好的达到远程控制终端的目的。 步骤一 …

MySQL- 索引

索引是帮助MySQL高效获取数据的数据结构(有序)。在数据之外, 数据库系统还维护着满足特定查找算法的数据结构, 这些数据结构以某种方式引用数据, 这样就可以在这些数据结构上实现高级查找算法, 这种数据结构就是索引。 索引结构 MySQL的索引是在存储层实现的, 不同的存储引擎有…

网友总结:面试超过一个小时,通过概率更低;面试时长在半小时以内,通过概率更高!...

面试时长跟通过概率有关系吗&#xff1f; 一位网友分享了自己的求职感想&#xff1a; 面试过程越长&#xff0c;差不多一个小时或者超过一个小时&#xff0c;问得越详细&#xff0c;通过的可能性越低。因为问得越细&#xff0c;说明这个公司越挑&#xff0c;需要候选人匹配度越…

【C++ 入坑指南】(05)数据类型

文章目录 一、整型sizeof 关键字 二、实型&#xff08;浮点型&#xff09;三、字符型四、字符串型4.1 C 风格字符串4.2 C 引入的 string 类类型 五、布尔类型&#xff08;bool&#xff09;六、类型转换6.1 静态转换&#xff08;Static Cast&#xff09;6.2 动态转换&#xff08…