Opencv 摄像机模型与标定 Camera Models and Calibration

Opencv 摄像机模型与标定 Camera Models and Calibration

本文函数总结:

  • convertPointsToHomogeneous(),convertPointsFromHomogeneous():实现平面上的点与齐次坐标系下的点之间的转换
  • Rodrigues():实现三维空间变换的矩阵和向量表示之间的转换
  • findChessboardCorners():根据棋盘图片找到棋盘中的内点位置
  • cornerSubPix():获取更精确的棋盘点位置
  • drawChessboardCorners():绘制找到的棋盘点位置
  • findCirclesGrid():根据点格图查找点的位置
  • findHomography():计算齐次投影矩阵
  • calibrateCamera():相机标定,得到相机内置参数矩阵,畸变参数,旋转矩阵和平移向量
  • solvePnP():根据相机内置参数矩阵,畸变参数求解旋转矩阵和平移向量
  • solvePnPRansac():针对野值情况,给出一种基于 RANSAC 的改进算法
  • convertMaps():实现映射不同表示之间的相互转化
  • initUndistortRectifyMap():根据相机标定信息计算畸变映射
  • remap():根据给定的畸变映射,实现图像映射
  • undistort():直接根据相机内置参数矩阵,畸变参数完成图像矫正
  • undistortPoints():直接根据相机内置参数矩阵,畸变参数完成对点的矫正

人类通过光线到达物体之后的反射(不被吸收的光谱就是物体的颜色)来看到物体。为更好地理解这一过程,提出的最简单也是最有效的模型就是针孔模型。但是现实的针孔通常不能获得足够的光线来快速曝光。因此必须在之前引入镜头来达到获得更多光线的目的。不过这也造成了最终图像的扭曲。而摄像机标定的作用就是矫正由于镜头引入而带来的失真。其中就涉及相机内置的成像参数,图像畸变参数,以及真实坐标系与相机坐标系之间的变换。同时,相机标定也是真实三维空间中测量物体的基础。

摄像机模型

为了简化,这里将成像平面放到了真实物体的同侧,这样逻辑更加简单,只是存在一个上下的翻转。

图中投影平面与针孔的距离被称为焦距,投影位置的计算公式为:\overrightarrow{q} = M \cdot \overrightarrow{Q}

其中 f_x, f_y 分别为 x, y 方向上的焦距,而 c_x, c_y 是投影中心与光轴的偏差。最后的投影位置可以通过前两项除以第三项(也就是除以 w(Z))而得到。而针对 x, y 分别引入不同的焦距的原因是由于低价的摄像头其两个方向的像素通常是长方形而不是正方形的,因此这里定义的 f_x, f_y 并不是实际的焦距(单位为米),而是考虑了像素因素的“焦距”(单位是像素)。而对于真实的焦距和相机每毫米的像素点除非拆开相机,我们无法获得。好在在相机标定时,我们只需要知道他们的整体表现 f_x, f_y 就能够实现所有的功能了。

而 M 就被称为相机的内置矩阵。

对于以上变换,能够很方便地在齐次坐标系下完成,其完成 N + 1 维数据与 N 维数据之间的转换,而前 N 维数据如果成比例,在齐次坐标系下被理解为同一个点。Opencv 还提供了两个变换的函数,实现与齐次坐标系之间的变换。

void cv::convertPointsToHomogeneous(
	cv::InputArray src, // Input vector of N-dimensional points
	cv::OutputArray dst // Result vector of (N+1)-dimensional points
);

其功能就是在最后增加一维,并赋值为 1

void cv::convertPointsFromHomogeneous(
	cv::InputArray src, // Input vector of N-dimensional points
	cv::OutputArray dst // Result vector of (N-1)-dimensional points
);

其作用就是前 N - 1 维的元素除以第 N 维的元素,然后丢弃原第 N 维的元素。

罗德里格斯变换——三维空间变换

对于三维的旋转变换,3 * 3 的旋转矩阵可能很方便的进行计算,但却不便于人的理解和赋值。因此,人们使用了一个三维的向量来定义旋转,其中向量的方向就是旋转的轴的方向,而旋转的角度就是向量的长度。而这两种旋转表示方法之间的变换就被称为罗德里格斯变换。旋转向量 \vec{r} 和旋转矩阵 R 的转换关系如下

对应的 Opencv 函数如下

void cv::Rodrigues(
	cv::InputArray src, // Input rotation vector or matrix
	cv::OutputArray dst, // Output rotation matrix or vector
	cv::OutputArray jacobian = cv::noArray() // Optional Jacobian (3x9 or 9x3)
);

参数说明:

  • src,dst:输入和输出既可以是旋转向量,也可以是旋转矩阵;函数能够根据用户对于变量的定义进行判断
  • jacobian:其值为输出矩阵对于输入矩阵的偏导数,主要被用于 cv::solvePnP() 和 cv::calibrateCamera() 两个函数的优化。通常情况下可以使用默认值

镜头畸变

径向畸变:位于镜头中心的图像不会发生畸变,但越靠近镜头边缘,畸变越严重。通常变现为四边形“圆化”。其矫正可以使用前几级的泰勒级数。

x_{corrected} = x \cdot (1+ k_1 r^2 + k_2 r^4 + k_3 r^6)

y_{corrected} = y \cdot (1+ k_1 r^2 + k_2 r^4 + k_3 r^6)

切向畸变:由于镜头与投影平面并不绝对平行,。此畸变能够基于两个额外的参数进行矫正

x_{corrected} = x + [2p_1xy + p_2(r^2+2x^2)]

y_{corrected} = y + [p_1(r^2 + 2y^2) + 2p_2xy]

针对这两种畸变,一共有五个参数,k_1, k_2, k_3, p_1, p_2。它们通常一起作为一个畸变向量在Opencv中处理,顺序为k_1, k_2, p_1, p_2, k_3

标定

  • 标定函数:cv::calibrateCamera()
  • 查找棋盘角(标定函数所需要的参数获取):cv::findChessboardCorners()
  • 针对鱼眼镜头的矫正:cv::fisheye
  • 针对全向或多摄像头标定,可参考 opencv_contrib/modules/ccalib 中有关的 omnidir 和 multiCameraCalibration 的内容

旋转矩阵和变换向量

其中平移向量主要作用是补偿一个坐标系原点到另一个坐标系原点的偏差;而旋转矩阵主要实现两个坐标系轴之间方向的不匹配。对应坐标系变换公式:\vec{P}_c = R \cdot (\vec{P}_0 - \vec{T})

至此,就介绍了标定所需要的所有参数,一共十个,分别是旋转矩阵的三个旋转角,平移向量的三个元素,以及最开始介绍的相机的四个内置参数(f_x, f_y, c_x, c_y)。首先,四个内置参数是不随图片变化的。如果使用一个平面物体,每张图将能够确定八个参数,因此至少需要两帧图像来确定所有的参数。

标定板

标定板通常选择一个平面上的规则物体,比如棋盘(黑白格),点格(交错黑点),随机模式等

棋盘

bool cv::findChessboardCorners( // Return true if corners were found
	cv::InputArray image, // Input chessboard image, 8UC1 or 8UC3
	cv::Size patternSize, // corners per row, and per column
	cv::OutputArray corners, // Output array of detected corners
	int flags = cv::CALIB_CB_ADAPTIVE_THRESH 
			| cv::CALIB_CB_NORMALIZE_IMAGE
);

参数说明:

  • image:包含棋盘的图片,必须为 8 bit
  • patternSize:行和列角点(四块黑白格的交点,两黑两白)的数量。其只统计内部角的数量,因此对于一个标准国际象棋棋盘,其值应该等于 cv::Size(7,7)
  • corners:检测到的每个角的位置
  • flags:选择一个或更多的过滤步骤来帮助在棋盘上寻找角
  • 返回值:如果棋盘中所有的角都被找到了,将返回 True;否则返回 False。

flags 的可选值

  • cv::CALIB_CB_ADAPTIVE_THRESH:cv::findChessboardCorners() 默认首先基于平均亮度进行二值化。但如果设置这个值,那么将使用自适应阈值
  • cv::CALIB_CB_NORMALIZE_IMAGE:如果设置,那么在二值化之前将通过 cv::equalizeHist() 进行直方图均衡化
  • cv::CALIB_CB_FILTER_QUADS:在二值化之后,算法假定黑框的四边形是直的。但如果存在径向畸变,这是不满足的,因此如果设置了这个参数,将有一些额外的限制被用于这些黑框,避免错误的黑框。
  • cv::CALIB_CV_FAST_CHECK:如果设置,首先将快速判断图像中有有没有棋盘,如果没有整张图将被直接跳过。不过,如果你确定每张图中都有棋盘,完全没有必要设置这个选项。

cv::cornerSubPix():不过 cv::findChessboardCorners() 只能给出角点的近似位置,因此 cv::cornerSubPix() 将被自动调用来获取更精确的角点位置。不过如果想要获得更精确的位置,可以自定调用该函数给出更严格的终止条件。

cv::drawChessboardCorners():将 cv::findChessboardCorners() 找到的交点绘制到原图上。如果未找到所有交点,所有角点将以小红点的形式给出;如果所有角点都已找到,那么每一行角点将使用不同的颜色。

void cv::drawChessboardCorners(
	cv::InputOutputArray image, // Input/output chessboard image, 8UC3
	cv::Size patternSize, // Corners per row, and per column
	cv::InputArray corners, // corners from findChessboardCorners()
	bool patternWasFound // Returned from findChessboardCorners()
);

点格

bool cv::findCirclesGrid(// Return true if corners were found
	cv::InputArray image, // Input chessboard image, 8UC1 or 8UC3
	cv::Size patternSize, // corners per row, and per column
	cv::OutputArray centers, // Output array of detected circle centers
	int flags = cv::CALIB_CB_SYMMETRIC_GRID,
	const cv::Ptr<cv::FeatureDetector>& blobDetector
	= new SimpleBlobDetector()
);

参数说明:

  • flags:默认算法假定点格式对称的(每一行都是整体排列的,没有错开一位),而对于不对称的点格(cv::CALIB_CB_ASYMMETRIC_GRID),其将看似的两行作为一行处理,列保持原来的计算方式。还有一个额外的选项可以同时选择 cv::CALIB_CB_CLUSTERING,其在降低对场景复杂性鲁棒性的前提下,提高了对于透视畸变的鲁棒性。

对比来看,非对称的点格通常能够获得比棋盘更好的结果,同时,现代诸如 ChArUco 甚至获得的更佳的结果。

单应性

在齐次坐标系下,如果假定物体真实点为 \vec{Q},而投影之后的点为 \vec{q},那么对应的投影变换能够表示为 \vec{q} = s \cdot H \cdot \vec{Q}

其中 s 通过缩放使得最后一维保持为 1;而 H 能够表示为坐标轴变换和相机内置矩阵两部分,\vec{q} = s \cdot M \cdot W \cdot \vec{Q}

不过由于我们并不关心真实空间中的 Z 轴位置,因此可以对其进行一些简化,而对应 H 变为了一个 3 * 3 的矩阵。

事实上,我们可以不计算相机内置矩阵参数,而仅依赖 H 完成目标到投影的计算。

Opencv 也提供了一个有效地计算 H 的函数

cv::Mat cv::findHomography(
	cv::InputArray srcPoints, // Input array source points (2-d)
	cv::InputArray dstPoints, // Input array result points (2-d)
	cv::int method = 0, // 0, cv::RANSAC, cv::LMEDS, etc.
	double ransacReprojThreshold = 3, // Max reprojection error
	cv::OutputArray mask = cv::noArray() // use only non-zero pts
);

参数说明:

  • srcPoints, dstPoints:原始点和投影点
  • method:最优化的方法,默认为 0,将最小化投影的均方根误差。不过,虽然这种方法速度较快,但是如果存在野值,估计结果将存在较大偏差。一种鲁邦方法是 cv::RANSAC(random sampling),其随机选择估计数据,并根据估计结果对数据进行一致性划分,能够使最多数据保持一致性的估计结果就是最后的结果。另一种方式是 cv::LMEDS(least median of squares),其最小化中值误差。最后是一种 RHO 方法,被称为 PROSAC,其是一种加权的 RANSAC 算法,速度更快
  • ransacReprojThreshold:使用 RANSAC 将被使用的一致的最大距离,其以像素为单位
  • mask:将返回计算结果应该使用的最优数据
  • 返回值:3 * 3 的矩阵,由于只有八个自由度,因此通常通过缩放使得 H_{33} 为 1

相机标定

相机标定需要的输入就是多张(两张以上)图片的角点,输出是相机内置参数矩阵(f_x, f_y, c_x, c_y),畸变参数(k_1, k_2, p_1, p_2, k_3),旋转向量和平移向量。

double cv::calibrateCamera(
	cv::InputArrayOfArrays objectPoints, // K vecs (N pts each, object frame)
	cv::InputArrayOfArrays imagePoints, // K vecs (N pts each, image frame)
	cv::Size imageSize, // Size of input images (pixels)
	cv::InputOutputArray cameraMatrix, // Resulting 3-by-3 camera matrix
	cv::InputOutputArray distCoeffs, // Vector of 4, 5, or 8 coefficients
	cv::OutputArrayOfArrays rvecs, // Vector of K rotation vectors
	cv::OutputArrayOfArrays tvecs, // Vector of K translation vectors
	int flags = 0, // Flags control calibration options
	cv::TermCriteria criteria = cv::TermCriteria(
		cv::TermCriteria::COUNT | cv::TermCriteria::EPS,
		30, // ...after this many iterations
		DBL_EPSILON // ...at this total reprojection error
	)
);

参数说明:

  • objectPoints:真实物体在空间坐标系下的位置,Z 向可以被直接赋值为零。不过其度量单位将直接影响最后估计结果的单位
  • imagePoints:图像坐标系下找到的每个点的位置。如果使用棋盘,那么每个向量就是输出的 corners
  • imageSize:图像的大小,以像素为单位
  • cameraMatrix:相机内置参数,3 * 3 的矩阵
  • distCoeffs:畸变参数,长度可以是 4, 5或者 8。4(k_1, k_2, p_1, p_2),5(k_1, k_2, p_1, p_2, k_3)用于鱼眼镜头,8(k_1, k_2, p_1, p_2, k_3, k_4, k_5, k_6)只当设置了 cv::CALIB_RATIONAL_MODEL,也只针对奇异镜头的高精度标定。最后需要注意的是随着估计参数的增加,所需要的图像的数量也将大幅增加
  • rvecs:旋转矩阵,以罗德里格斯形式的三维向量
  • tvecs:平移向量
  • flags:一些控制计算工程的参数,可选值

cv::CALIB_USE_INTRINSIC_GUESS:c_x, c_y 默认情况下将根据图像大小直接初始化,如果设置该参数,cameraMatrix 中将包含有效的信息用于初始化。

cv::CALIB_FIX_PRINCIPAL_POINT:如果没有和 cv::CALIB_USE_INTRINSIC_GUESS 一起定义,则 c_x, c_y 的初始化为图像中点。如果一起定义,将使用 cameraMatrix 中的值进行初始化

cv::CALIB_FIX_ASPECT_RATIO:如果和 cv::CALIB_USE_INTRINSIC_GUESS 一起定义,则 f_x, f_y 将只能根据 cameraMatrix 中的值进行等比例放缩。如果没有一起定义,那么 cameraMatrix 中的值是随意的,但比例应该是相关的

cv::CALIB_FIX_FOCAL_LENGTH:f_x, f_y 将是 cameraMatrix 中的值

cv::CALIB_FIX_K1, cv::CALIB_FIX_K2, ... cv::CALIB_FIX_K6:k_1, k_2, p_1, p_2, k_3, k_4, k_5, k_6 将是 distCoeffs 中的值

cv::CALIB_ZERO_TANGENT_DIST:针对高端摄像头,p_1, p_2 被设置为零

cv::CALIB_RATIONAL_MODEL:是否计算 k_4, k_5, k_6

  • criteria:给定迭代终止条件

当我们已经获得了相机的内置矩阵和畸变参数之后,只需要使用以下函数计算旋转矩阵和平移向量即可。

bool cv::solvePnP(
	cv::InputArray objectPoints, // Object points (object frame)
	cv::InputArray imagePoints, // Found pt locations (img frame)
	cv::InputArray cameraMatrix, // 3-by-3 camera matrix
	cv::InputArray distCoeffs, // Vector of 4, 5, or 8 coeffs
	cv::OutputArray rvec, // Result rotation vector
	cv::OutputArray tvec, // Result translation vector
	bool useExtrinsicGuess = false, // true='use vals in rvec and tvec'
	int flags = cv::ITERATIVE
);

参数说明:

  • useExtrinsicGuess:如果设置,则 rvec 和 tvec 中的值将被用于初始化
  • flags:cv::ITERATIVE(Levenberg-Marquardt optimization),cv::P3P 和 cv::EPNP 为非迭代方法,因此快很多。

但上述函数对野值敏感,为解决野值的问题可使用如下函数

bool cv::solvePnPRansac(
	cv::InputArray objectPoints, // Object points (object frame)
	cv::InputArray imagePoints, // Found pt locations (img frame)
	cv::InputArray cameraMatrix, // 3-by-3 camera matrix
	cv::InputArray distCoeffs, // Vector of 4, 5, or 8 coeffs
	cv::OutputArray rvec, // Result rotation vector
	cv::OutputArray tvec, // Result translation vector
	bool useExtrinsicGuess = false, // read vals in rvec and tvec ?
	int iterationsCount = 100, // RANSAC iterations
	float reprojectionError = 8.0, // Max error for inclusion
	int minInliersCount = 100, // terminate if this many found
	cv::OutputArray inliers = cv::noArray(), // Contains inlier indices
	int flags = cv::ITERATIVE // same as solvePnP()
)

参数说明:

  • iterationsCount:设置迭代次数
  • reprojectionError:判定为一致的最大距离
  • minInliersCount:一般某组估计结果的一致数量操作这个值,将直接终止算法
  • inliers:标记为一致点的标示位

矫正

  • cv::undistort():输入原始图像和畸变参数直接输出矫正之后的图像
  • cv::initUndistortRectifyMap() 和 cv::remap():处理多张来自相同相机的图像

矫正映射

  • 双通道浮点表示:每个点的两通道的浮点数分别表示了映射之后点的位置,其中使用插值
  • 两个浮点矩阵:同上
  • 双通道有符号整数:同上,其中插值可以使用额外的单通道无符号整数表示

而不同矫正映射的变换可以通过以下函数进行

void cv::convertMaps(
	cv::InputArray map1, // First in map: CV_16SC2/CV_32FC1/CV_32FC2
	cv::InputArray map2, // Second in map: CV_16UC1/CV_32FC1 or none
	cv::OutputArray dstmap1, // First out map
	cv::OutputArray dstmap2, // Second out map
	int dstmap1type, // dstmap1 type: CV_16SC2/CV_32FC1/CV_32FC2
	bool nninterpolation = false // For conversion to fixed point types
);

参数说明:

从相机标定信息计算畸变映射

void cv::initUndistortRectifyMap(
	cv::InputArray cameraMatrix, // 3-by-3 camera matrix
	cv::InputArray distCoeffs, // Vector of 4, 5, or 8 coefficients
	cv::InputArray R, // Rectification transformation
	cv::InputArray newCameraMatrix, // New camera matrix (3-by-3)
	cv::Size size, // Undistorted image size
	int m1type, // 'map1' type: 16SC2, 32FC1, or 32FC2
	cv::OutputArray map1, // First output map
	cv::OutputArray map2, // Second output map
);

参数说明:

  • cameraMatrix,distCoeffs:cv::calibrateCamera() 获得的相机内置参数和畸变参数
  • R:旋转矩阵,在矫正之前被使用。主要用于修正相机相对于内置坐标系的旋转,可直接设置为 cv::noArray()
  • newCameraMatrix:对于立体图像适用。否则,可直接赋值为 cv::noArray()
  • size:图像的大小
  • m1type, map1 和 map2 :定义最后映射的数据类型和输出数据

接下来就可以应用求得的映射矩阵,使用以下函数完成图像的映射。

void cv::remap(
	cv::InputArray src, // Input image
	cv::OutputArray dst, // Output image
	cv::InputArray map1, // target x for src pix
	cv::InputArray map2, // target y for src pix
	int interpolation = cv::INTER_LINEAR, // Interpolation, inverse
	int borderMode = cv::BORDER_CONSTANT, // Extrapolation method
	const cv::Scalar& borderValue = cv::Scalar() // For constant borders
);

如果需要直接完成图像的映射,而不需要映射矩阵,可以直接使用一下函数完成图像映射。

void cv::undistort(
	cv::InputArray src, // Input distorted image
	cv::OutputArray dst, // Result corrected image
	cv::InputArray cameraMatrix, // 3-by-3 camera matrix
	cv::InputArray distCoeffs, // Vector of 4, 5, or 8 coeffs
	cv::InputArray newCameraMatrix = noArray() // Optional new camera matrix
);

当然处理映射整幅图像,可以使用以下函数只映射其中的某些点。

void cv::undistortPoints(
	cv::InputArray src, // Input array, N pts. (2-d)
	cv::OutputArray dst, // Result array, N pts. (2-d)
	cv::InputArray cameraMatrix, // 3-by-3 camera matrix
	cv::InputArray distCoeffs, // Vector of 4, 5, or 8 coeffs
	cv::InputArray R = cv::noArray(), // 3-by-3 rectification mtx.
	cv::InputArray P = cv::noArray() // 3-by-3 or 3-by-4 new camera
									 // or new projection matrix
);

整体程序

// Example 18-1. Reading a chessboard’s width and height, reading them from disk and calibrating 
// the requested number of views, and calibrating the camera 

// You need these includes for the function
#include <windows.h>  // for windows systems
// #include <dirent.h>     // for linux systems
// #include <sys/stat.h>   // for linux systems
#include <iostream>     // cout
#include <algorithm>    // std::sort
#include <opencv2/opencv.hpp>

using std::string;
using std::vector;
using std::cout;
using std::cerr;
using std::endl;

// Returns a list of files in a directory (except the ones that begin with a dot)
int readFilenames(vector<string>& filenames, const string& directory) {
#ifdef WINDOWS
    HANDLE dir;
    WIN32_FIND_DATA file_data;

    if ((dir = FindFirstFile((directory + "/*").c_str(), &file_data)) == INVALID_HANDLE_VALUE)
        return;  // no files found
    do {
        const string file_name = file_data.cFileName;
        const string full_file_name = directory + "/" + file_name;
        const bool is_directory = (file_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;

        if (file_name[0] == '.')
            continue;

        if (is_directory)
            continue;

        filenames.push_back(full_file_name);
    } while (FindNextFile(dir, &file_data));

    FindClose(dir);
#else
    DIR *dir;
    class dirent *ent;
    class stat st;

    dir = opendir(directory.c_str());
    while ((ent = readdir(dir)) != NULL) {
        const string file_name = ent->d_name;
        const string full_file_name = directory + "/" + file_name;

        if (file_name[0] == '.')
            continue;

        if (stat(full_file_name.c_str(), &st) == -1)
            continue;

        const bool is_directory = (st.st_mode & S_IFDIR) != 0;

        if (is_directory)
            continue;

        // filenames.push_back(full_file_name);  // returns full path
        filenames.push_back(file_name);  // returns just filename
    }
    closedir(dir);
#endif
    std::sort(filenames.begin(), filenames.end());  // optional, sort the filenames
    return(filenames.size());  // return how many we found
}  // GetFilesInDirectory

void help(const char **argv) {
    cout << "\n\n"
         << "Example 18-1 (from disk):\Enter a chessboard’s width and height,\n"
         << "              reading in a directory of chessboard images,\n"
         << "              and calibrating the camera\n\n"
         << "Call:\n" << argv[0] << " <1board_width> <2board_height> <3number_of_boards>"
         << " <4ms_delay_framee_capture> <5image_scaling_factor> <6path_to_calibration_images>\n\n"
         << "\nExample:\n"
         << "./example_18-01_from_disk 9 6 14 100 1.0 ../stereoData/\nor:\n"
         << "./example_18-01_from_disk 12 12 28 100 0.5 ../calibration/\n\n"
         << " * First it reads in checker boards and calibrates itself\n"
         << " * Then it saves and reloads the calibration matricies\n"
         << " * Then it creates an undistortion map and finaly\n"
         << " * It displays an undistorted image\n"
         << endl;
}


int main(int argc, const char *argv[]) {
    float image_sf = 0.5f;    // image scaling factor
    int delay = 250;          // miliseconds
    int board_w = 0;
    int board_h = 0;

    if (argc != 7) {
        cout << "\nERROR: Wrong number (" << argc - 1
             << ") of arguments, should be (6) input parameters\n";
        help(argv);
        return -1;
    }

    board_w = atoi(argv[1]);
    board_h = atoi(argv[2]);
    int n_boards = atoi(argv[3]);  // how many boards max to read
    delay = atof(argv[4]);         // milisecond delay
    image_sf = atof(argv[5]);
    int board_n = board_w * board_h;  // number of corners
    cv::Size board_sz = cv::Size(board_w, board_h);  // width and height of the board

    string folder = argv[6];
    cout << "Reading in directory " << folder << endl;
    vector<string> filenames;
    int num_files = readFilenames(filenames, folder);
    cout << "   ... Done. Number of files = " << num_files << "\n" << endl;

    // PROVIDE PPOINT STORAGE
    //
    vector<vector<cv::Point2f> > image_points;
    vector<vector<cv::Point3f> > object_points;

    /////////// COLLECT //////////////////////////////////////////////
    // Capture corner views: loop through all calibration images
    // collecting all corners on the boards that are found
    //
    cv::Size image_size;
    int board_count = 0;
    for (size_t i = 0; (i < filenames.size()) && (board_count < n_boards); ++i) {
        cv::Mat image, image0 = cv::imread(folder + filenames[i]);
        board_count += 1;
        if (!image0.data) {  // protect against no file
            cerr << folder + filenames[i] << ", file #" << i << ", is not an image" << endl;
            continue;
        }
        image_size = image0.size();
        cv::resize(image0, image, cv::Size(), image_sf, image_sf, cv::INTER_LINEAR);

        // Find the board
        //
        vector<cv::Point2f> corners;
        bool found = cv::findChessboardCorners(image, board_sz, corners);

        // Draw it
        //
        drawChessboardCorners(image, board_sz, corners, found);  // will draw only if found

        // If we got a good board, add it to our data
        //
        if (found) {
            image ^= cv::Scalar::all(255);
            cv::Mat mcorners(corners);

            // do not copy the data
            mcorners *= (1.0 / image_sf);

            // scale the corner coordinates
            image_points.push_back(corners);
            object_points.push_back(vector<cv::Point3f>());
            vector<cv::Point3f> &opts = object_points.back();

            opts.resize(board_n);
            for (int j = 0; j < board_n; j++) {
                opts[j] = cv::Point3f(static_cast<float>(j / board_w),
                                      static_cast<float>(j % board_w), 0.0f);
            }
            cout << "Collected " << static_cast<int>(image_points.size())
                 << "total boards. This one from chessboard image #"
                 << i << ", " << folder + filenames[i] << endl;
        }
        cv::imshow("Calibration", image);

        // show in color if we did collect the image
        if ((cv::waitKey(delay) & 255) == 27) {
            return -1;
        }
    }

    // END COLLECTION WHILE LOOP.
    cv::destroyWindow("Calibration");
    cout << "\n\n*** CALIBRATING THE CAMERA...\n" << endl;

    /////////// CALIBRATE //////////////////////////////////////////////
    // CALIBRATE THE CAMERA!
    //
    cv::Mat intrinsic_matrix, distortion_coeffs;
    double err = cv::calibrateCamera(
      object_points, image_points, image_size, intrinsic_matrix,
      distortion_coeffs, cv::noArray(), cv::noArray(),
      cv::CALIB_ZERO_TANGENT_DIST | cv::CALIB_FIX_PRINCIPAL_POINT);

    // SAVE THE INTRINSICS AND DISTORTIONS
    cout << " *** DONE!\n\nReprojection error is " << err
       << "\nStoring Intrinsics.xml and Distortions.xml files\n\n";
    cv::FileStorage fs("intrinsics.xml", cv::FileStorage::WRITE);
    fs << "image_width" << image_size.width << "image_height" << image_size.height
     << "camera_matrix" << intrinsic_matrix << "distortion_coefficients"
     << distortion_coeffs;
    fs.release();

    // EXAMPLE OF LOADING THESE MATRICES BACK IN:
    fs.open("intrinsics.xml", cv::FileStorage::READ);
    cout << "\nimage width: " << static_cast<int>(fs["image_width"]);
    cout << "\nimage height: " << static_cast<int>(fs["image_height"]);
    cv::Mat intrinsic_matrix_loaded, distortion_coeffs_loaded;
    fs["camera_matrix"] >> intrinsic_matrix_loaded;
    fs["distortion_coefficients"] >> distortion_coeffs_loaded;
    cout << "\nintrinsic matrix:" << intrinsic_matrix_loaded;
    cout << "\ndistortion coefficients: " << distortion_coeffs_loaded << "\n" << endl;

    // Build the undistort map which we will use for all
    // subsequent frames.
    //
    cv::Mat map1, map2;
    cv::initUndistortRectifyMap(intrinsic_matrix_loaded, distortion_coeffs_loaded,
                              cv::Mat(), intrinsic_matrix_loaded, image_size,
                              CV_16SC2, map1, map2);

    ////////// DISPLAY //////////////////////////////////////////////
    cout << "*****************\nPRESS A KEY TO SEE THE NEXT IMAGE, ESQ TO QUIT\n"
         << "****************\n" << endl;
    board_count = 0;  // resent max boards to read
    for (size_t i = 0; (i < filenames.size()) && (board_count < n_boards); ++i) {
        cv::Mat image, image0 = cv::imread(folder + filenames[i]);
        ++board_count;
        if (!image0.data) {  // protect against no file
            cerr << folder + filenames[i] << ", file #" << i << ", is not an image" << endl;
            continue;
        }
        // Just run the camera to the screen, now showing the raw and
        // the undistorted image.
        //
        cv::remap(image0, image, map1, map2, cv::INTER_LINEAR,
            cv::BORDER_CONSTANT, cv::Scalar());
        cv::imshow("Original", image0);
        cv::imshow("Undistorted", image);
        if ((cv::waitKey(0) & 255) == 27) {
            break;
        }
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/a40850273/article/details/83009801