本文函数总结:
人类通过光线到达物体之后的反射(不被吸收的光谱就是物体的颜色)来看到物体。为更好地理解这一过程,提出的最简单也是最有效的模型就是针孔模型。但是现实的针孔通常不能获得足够的光线来快速曝光。因此必须在之前引入镜头来达到获得更多光线的目的。不过这也造成了最终图像的扭曲。而摄像机标定的作用就是矫正由于镜头引入而带来的失真。其中就涉及相机内置的成像参数,图像畸变参数,以及真实坐标系与相机坐标系之间的变换。同时,相机标定也是真实三维空间中测量物体的基础。
为了简化,这里将成像平面放到了真实物体的同侧,这样逻辑更加简单,只是存在一个上下的翻转。
图中投影平面与针孔的距离被称为焦距,投影位置的计算公式为:,
其中 分别为 方向上的焦距,而 是投影中心与光轴的偏差。最后的投影位置可以通过前两项除以第三项(也就是除以 w(Z))而得到。而针对 分别引入不同的焦距的原因是由于低价的摄像头其两个方向的像素通常是长方形而不是正方形的,因此这里定义的 并不是实际的焦距(单位为米),而是考虑了像素因素的“焦距”(单位是像素)。而对于真实的焦距和相机每毫米的像素点除非拆开相机,我们无法获得。好在在相机标定时,我们只需要知道他们的整体表现 就能够实现所有的功能了。
而 就被称为相机的内置矩阵。
对于以上变换,能够很方便地在齐次坐标系下完成,其完成 维数据与 维数据之间的转换,而前 维数据如果成比例,在齐次坐标系下被理解为同一个点。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 的旋转矩阵可能很方便的进行计算,但却不便于人的理解和赋值。因此,人们使用了一个三维的向量来定义旋转,其中向量的方向就是旋转的轴的方向,而旋转的角度就是向量的长度。而这两种旋转表示方法之间的变换就被称为罗德里格斯变换。旋转向量 和旋转矩阵 的转换关系如下
对应的 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)
);
参数说明:
径向畸变:位于镜头中心的图像不会发生畸变,但越靠近镜头边缘,畸变越严重。通常变现为四边形“圆化”。其矫正可以使用前几级的泰勒级数。
切向畸变:由于镜头与投影平面并不绝对平行,。此畸变能够基于两个额外的参数进行矫正
针对这两种畸变,一共有五个参数,。它们通常一起作为一个畸变向量在Opencv中处理,顺序为。
旋转矩阵和变换向量
其中平移向量主要作用是补偿一个坐标系原点到另一个坐标系原点的偏差;而旋转矩阵主要实现两个坐标系轴之间方向的不匹配。对应坐标系变换公式:
至此,就介绍了标定所需要的所有参数,一共十个,分别是旋转矩阵的三个旋转角,平移向量的三个元素,以及最开始介绍的相机的四个内置参数()。首先,四个内置参数是不随图片变化的。如果使用一个平面物体,每张图将能够确定八个参数,因此至少需要两帧图像来确定所有的参数。
标定板通常选择一个平面上的规则物体,比如棋盘(黑白格),点格(交错黑点),随机模式等
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
);
参数说明:
flags 的可选值
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& blobDetector
= new SimpleBlobDetector()
);
参数说明:
对比来看,非对称的点格通常能够获得比棋盘更好的结果,同时,现代诸如 ChArUco 甚至获得的更佳的结果。
在齐次坐标系下,如果假定物体真实点为 ,而投影之后的点为 ,那么对应的投影变换能够表示为
其中 通过缩放使得最后一维保持为 1;而 能够表示为坐标轴变换和相机内置矩阵两部分,
不过由于我们并不关心真实空间中的 Z 轴位置,因此可以对其进行一些简化,而对应 变为了一个 3 * 3 的矩阵。
事实上,我们可以不计算相机内置矩阵参数,而仅依赖 完成目标到投影的计算。
Opencv 也提供了一个有效地计算 的函数
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
);
参数说明:
相机标定需要的输入就是多张(两张以上)图片的角点,输出是相机内置参数矩阵(),畸变参数(),旋转向量和平移向量。
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
)
);
参数说明:
cv::CALIB_USE_INTRINSIC_GUESS: 默认情况下将根据图像大小直接初始化,如果设置该参数,cameraMatrix 中将包含有效的信息用于初始化。
cv::CALIB_FIX_PRINCIPAL_POINT:如果没有和 cv::CALIB_USE_INTRINSIC_GUESS 一起定义,则 的初始化为图像中点。如果一起定义,将使用 cameraMatrix 中的值进行初始化
cv::CALIB_FIX_ASPECT_RATIO:如果和 cv::CALIB_USE_INTRINSIC_GUESS 一起定义,则 将只能根据 cameraMatrix 中的值进行等比例放缩。如果没有一起定义,那么 cameraMatrix 中的值是随意的,但比例应该是相关的
cv::CALIB_FIX_FOCAL_LENGTH: 将是 cameraMatrix 中的值
cv::CALIB_FIX_K1, cv::CALIB_FIX_K2, ... cv::CALIB_FIX_K6: 将是 distCoeffs 中的值
cv::CALIB_ZERO_TANGENT_DIST:针对高端摄像头, 被设置为零
cv::CALIB_RATIONAL_MODEL:是否计算
当我们已经获得了相机的内置矩阵和畸变参数之后,只需要使用以下函数计算旋转矩阵和平移向量即可。
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
);
参数说明:
但上述函数对野值敏感,为解决野值的问题可使用如下函数
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()
)
参数说明:
而不同矫正映射的变换可以通过以下函数进行
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
);
参数说明:
接下来就可以应用求得的映射矩阵,使用以下函数完成图像的映射。
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 // for windows systems
// #include // for linux systems
// #include // for linux systems
#include // cout
#include // std::sort
#include
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& 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 filenames;
int num_files = readFilenames(filenames, folder);
cout << " ... Done. Number of files = " << num_files << "\n" << endl;
// PROVIDE PPOINT STORAGE
//
vector > image_points;
vector > 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 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());
vector &opts = object_points.back();
opts.resize(board_n);
for (int j = 0; j < board_n; j++) {
opts[j] = cv::Point3f(static_cast(j / board_w),
static_cast(j % board_w), 0.0f);
}
cout << "Collected " << static_cast(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(fs["image_width"]);
cout << "\nimage height: " << static_cast(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;
}