02-标定实现
内参标定(棋盘格)¶
仅考虑棋盘的一个图像的情况下。相机校准所需的重要输入数据是一组3D现实世界点及其对应的2D图像点。可以从图像中轻松找到2D图像点。 (这些图像点是棋盘上两个黑色正方形相互接触的位置)
问题是,如何获取现实世界中的3D点呢? 由于这些图像是从静态相机拍摄的,而棋盘放置在不同的位置和方向。我们需要知道(X,Y,Z)的值。但是换个角度思考,假如我们定义棋盘在X、Y平面上保持静止(Z始终为0),让照相机发生移动了。这种思考仅有助于我们找到X,Y值。现在对于X,Y值,我们可以简单地将点记为(0,0),(1,0),(2,0),...,这表示实际点的位置。在这种情况下,我们得到的结果只是棋盘正方形的大小比例。但是,如果我们知道正方形尺寸(例如20 mm),则可以将值记为(0,0),(20,0),(40,0),...,且得到的结果以mm为单位。(在这种情况下,我们不知道正方形尺寸,因为我们没有拍摄图像,因此我们以正方形尺寸表示)。
计算中,我们将3D对象点取名object points
,2D图像点取名image points
1. 加载图片¶
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
// 保存多张图片对象点列表
vector<vector<Point3f>> objectPoints;
// 保存多张图片的角点列表
vector<vector<Point2f>> cornerPoints;
int main(){
// 图片像素尺寸
Size imgSize;
// 图片路径
cv::String src_path = "./assets/camerargb_*.jpg";
std::vector<String> filenames;
cv::glob(src_path, filenames);//获取路径下所有文件名
cout << "filenames.size:" << filenames.size() << endl;
for (auto& imgName : filenames) {
// 读取图片
Mat img = imread(imgName, IMREAD_COLOR);
// 获取图片像素尺寸
imgSize = img.size();
std::cout << "name: " << imgName<< " imgSize: " << imgSize << std::endl;
//...
}
return 0;
}
2. 查找角点¶
由于OpenCV提供的函数参数为灰度图,所以要提前将彩图转为灰度图
- 首先定义交点查找函数
// 棋盘格的尺寸(宽6,高9)
const Size patternSize(6, 9);
// 黑方格的大小 20mm
const int squareSize = 20;
/**
* 在指定图片中查找角点,并将结果输出到corners中
* @param img 待检测图片
* @param corners 检测到的焦点列表
* @return 是否检测到角点(两个黑方格的交点)
*/
bool findCorners(Mat &img, vector<Point2f> &corners) {
Mat gray;
// 将图片转成灰度图
cvtColor(img, gray, COLOR_RGB2GRAY);
// 查找当前图片所有的角点
bool patternWasFound = findChessboardCorners(gray, patternSize, corners);
if (patternWasFound) { // 找到角点
// 提高角点的精确度
// 原理:https://docs.opencv.org/4.1.0/dd/d1a/group__imgproc__feature.html#ga354e0d7c86d0d9da75de9b9701a9a87e
cornerSubPix(gray, corners, Size(11, 11), Size(-1, -1),
TermCriteria(TermCriteria::EPS + TermCriteria::COUNT, 30, 0.1));
}
// 将所有的焦点在原图中绘制出来
drawChessboardCorners(img, patternSize, corners, patternWasFound);
// 绘制完角点之后,显示原图
imshow("src", img);
if (!patternWasFound){
cout << "角点检测失败!" << endl;
}
return patternWasFound;
}
- 查找角点,创建其对象点
// 保存多张图片对象点列表
vector<vector<Point3f>> objectPoints;
// 保存多张图片的角点列表
vector<vector<Point2f>> cornerPoints;
void calcObjectPoints(vector<Point3f> &objPoint) {
// 计算uv空间中角点对应的相机坐标系坐标值,设Z为0
for (int i = 0; i < patternSize.height; ++i)
for (int j = 0; j < patternSize.width; ++j)
objPoint.emplace_back(j * squareSize, i * squareSize, 0);
}
// 图片像素尺寸
Size imgSize;
int main(){
// 图片路径
cv::String src_path = "./assets/camerargb_*.jpg";
std::vector<String> filenames;
cv::glob(src_path, filenames);//获取路径下所有文件名
cout << "filenames.size:" << filenames.size() << endl;
for (auto& imgName : filenames) {
// 读取图片
Mat img = imread(imgName, IMREAD_COLOR);
// 获取图片像素尺寸
imgSize = img.size();
std::cout << "name: " << imgName<< " imgSize: " << imgSize << std::endl;
// 声明每张图片的角点
vector<Point2f> corners;
bool found = findCorners(img, corners);
if (found) {
vector<Point3f> objPoints;
calcObjectPoints(objPoints);
// 找到角点,证明这张图是有效的
objectPoints.push_back(objPoints);
cornerPoints.push_back(corners);
}
}
return 0;
}
3. 执行相机标定¶
Mat cameraMatrix; // 相机参数矩阵
Mat disCoffes; // 失真系数 distortion coefficients
Mat rvecs; // 图片旋转向量
Mat tvecs; // 图片平移向量
calibrateCamera(objectPoints, cornerPoints, imgSize, cameraMatrix, disCoffes, rvecs, tvecs);
cout << "标定矩阵:" << cameraMatrix << endl;
cout << "畸变矩阵:" << disCoffes << endl;
// save2xml(cameraMatrix, distCoffes);
waitKey();
4. 保存标定结果¶
void save2xml(const Mat &cameraMatrix, const Mat &disCoffes) {
// 获取当前时间
time_t tm;
time(&tm);
struct tm *t2 = localtime(&tm);
char buf[1024];
strftime(buf, sizeof(buf), "%c", t2);
// 写出数据
String inCailFilePath = "./inCailFilePath.xml";
FileStorage inCailFs(inCailFilePath, FileStorage::WRITE);
inCailFs << "calibration_time" << buf;
inCailFs << "cameraMatrix" << cameraMatrix;
inCailFs << "distCoffes" << disCoffes;
inCailFs.release();
}
标定结果示例¶
- xml版本
<?xml version="1.0"?>
<opencv_storage>
<calibration_time>"2019年10月15日 星期二 17时25分25秒"</calibration_time>
<cameraMatrix type_id="opencv-matrix">
<rows>3</rows>
<cols>3</cols>
<dt>d</dt>
<data>
1.0743566574494685e+03 0. 9.5548426688037193e+02 0.
1.0758663741535415e+03 5.4946692912295123e+02 0. 0. 1.</data></cameraMatrix>
<disCoffes type_id="opencv-matrix">
<rows>1</rows>
<cols>5</cols>
<dt>d</dt>
<data>
7.2163806117565163e-02 -1.1302001810933321e-01
1.4988711762593146e-03 -3.0609919169149657e-03
4.2575786627597728e-02</data></disCoffes>
</opencv_storage>
- yml版本
%YAML:1.0
---
calibration_time: "2019年10月20日 星期日 23时25分26秒"
frame_count: 15
image_width: 640
image_height: 480
board_width: 6
board_height: 9
square_size: 1.9999999552965164e-02
rms: 1.2085572726037488e-01
cameraMatrix: !!opencv-matrix
rows: 3
cols: 3
dt: d
data: [ 6.0447700370270286e+02, 0., 3.3181544620019122e+02, 0.,
6.0479737087406238e+02, 2.9128742694561294e+02, 0., 0., 1. ]
distCoeffs: !!opencv-matrix
rows: 5
cols: 1
dt: d
data: [ 1.9175212705577635e-01, -7.0334052488796239e-01,
6.9245817187035201e-04, -1.5403756810363311e-03,
4.2337569654123880e-01 ]
内参标定(圆网格)¶
将“棋盘格”标定中,查找角点的步骤更改为findCirclesGrid
即可
bool found = findCirclesGrid(image, board_sz, corners);
摄像头实时标定¶
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
const int ACTION_ESC = 27;
const int ACTION_SPACE = 32;
static void print_help() {
printf("通过棋盘格标定相机\n");
printf("参数:CalibrationChessboard <board_width> n<board_height> <square_size> [number_of_boards] "
"[--delay=<delay>] [-s=<scale_factor>]\n");
printf("例子:CalibrationACircle2 6 9 30 500 1.0\n");
}
/**
* 执行标定并保存标定结果
* @param square_size 格子尺寸
* @param board_sz 格子尺寸Size
* @param image_size 图片尺寸Size
* @param image_points 图片角点集合
* @param cameraMatrix 相机参数
* @param distCoeffs 畸变参数
*/
void
runAndSave(float square_size, const Size board_sz, const Size image_size,
const vector<vector<Point2f>> &image_points,
Mat &cameraMatrix, Mat &distCoeffs) {
vector<vector<Point3f>> object_points;
vector<Point3f> objPoints;
for (int i = 0; i < board_sz.height; ++i) {
for (int j = 0; j < board_sz.width; ++j) {
// 注意非对称圆网格的对象坐标计算方式
objPoints.push_back(Point3f(float((2*j + i%2) * square_size),
float(i * square_size), 0));
}
}
object_points.resize(image_points.size(), objPoints);
vector<Mat> rvecs, tvecs;
// 执行标定
double rms = calibrateCamera(object_points, image_points, image_size, cameraMatrix, distCoeffs, rvecs, tvecs);
// 均方根值(RMS)
printf("RMS error reported by calibrateCamera: %g\n", rms);
// 检查标定结果误差
bool ok = checkRange(cameraMatrix) && checkRange(distCoeffs);
if (ok) {
cout << "标定参数:" << endl;
cout << cameraMatrix << endl;
cout << "畸变参数:" << endl;
cout << distCoeffs << endl;
// 时间、图片个数、图片尺寸、标定板宽高、标定板块尺寸、RMS
// 标定参数、畸变参数
// 获取当前时间
time_t tm;
time(&tm);
struct tm *t2 = localtime(&tm);
char buf[1024];
strftime(buf, sizeof(buf), "%c", t2);
// 写出数据
// String inCailFilePath = "./calibration_in_params.xml";
String inCailFilePath = "./calibration_in_params2.yml";
FileStorage inCailFs(inCailFilePath, FileStorage::WRITE);
inCailFs << "calibration_time" << buf;
inCailFs << "frame_count" << (int)image_points.size();
inCailFs << "image_width" << image_size.width;
inCailFs << "image_height" << image_size.height;
inCailFs << "board_width" << board_sz.width;
inCailFs << "board_height" << board_sz.height;
inCailFs << "square_size" << square_size;
inCailFs << "rms" << rms;
inCailFs << "cameraMatrix" << cameraMatrix;
inCailFs << "distCoeffs" << distCoeffs;
inCailFs.release();
std::cout << "标定结果已保存:"<< inCailFilePath << std::endl;
}else {
std::cout << "标定结果有误,请重新标定!" << std::endl;
}
}
int main(int argc, char **argv) {
std::cout << cv::getVersionString() << std::endl;
bool flipHorizontal = false;
// 解析参数
cv::CommandLineParser parser(argc, argv,
"{@arg1||}{@arg2||}{@arg3|20|}{@arg4|15|}"
"{help h||}{delay d|500|}{scale s|1.0|}{f||}");
if (parser.has("help")) {
print_help();
return 0;
}
int board_width = parser.get<int>(0);
int board_height = parser.get<int>(1);
float square_size = parser.get<float>(2);
int num_boards = parser.get<int>(3);
int delay = parser.get<int>("delay");
auto image_sf = parser.get<float>("scale");
if (board_width < 1 || board_height < 1) {
printf("Command-line parameter error: both image of width and height must be specified\n");
print_help();
return -1;
}
if (parser.has("f")) {
flipHorizontal = true;
}
std::cout << "board_width: " << board_width << std::endl;
std::cout << "board_height: " << board_height << std::endl;
std::cout << "square_size: " << square_size << std::endl;
std::cout << "num_boards: " << num_boards << std::endl;
std::cout << "delay: " << delay << std::endl;
std::cout << "image_sf: " << image_sf << std::endl;
cv::Size board_sz = cv::Size(board_width, board_height);
cv::VideoCapture capture(0);
if (!capture.isOpened()) {
std::cout << "无法开启摄像头!" << std::endl;
return -1;
}
vector<vector<Point2f>> image_points;
vector<vector<Point3f>> object_points;
cv::Size image_size;
while (image_points.size() < num_boards){
Mat image0, image;
capture >> image0;
image_size = image0.size();
// 将图像复制到image
image0.copyTo(image);
// 缩放
if (image_sf != 1.0) {
resize(image0, image, Size(), image_sf, image_sf, cv::INTER_LINEAR);
}
// 水平翻转
if (flipHorizontal) {
flip(image, image, 1);
}
// 查找标定板(不对称圆网格板)
vector<Point2f> corners;
bool found = findCirclesGrid(image, board_sz, corners, CALIB_CB_ASYMMETRIC_GRID);
// 画上去
drawChessboardCorners(image, board_sz, corners, found);
int action = waitKey(30) & 255;
// 判断动作
if (action == ACTION_SPACE) { // 用户按下了空格
if (found) {
// 闪屏
bitwise_not(image, image);
// 保存角点
printf("%s: %d/%d \n", "save角点", (int)image_points.size() + 1, num_boards);
image_points.push_back(corners);
// 保存图片
}else {
printf("%s\n", "未检测到角点");
}
} else if (action == ACTION_ESC) { // 用户按下了ESC
break;
}
cv::imshow("Calibration", image);
}
if (image_points.size() < num_boards) {
printf("角点未达到目标个数,标定已终止!");
return -1;
}
printf("角点收集完毕, 执行标定中... 图片尺寸 %d:%d\n", image_size.width, image_size.height);
Mat cameraMatrix = Mat::eye(3, 3, CV_64F);
Mat distCoeffs = Mat::zeros(8, 1, CV_64F);
runAndSave(square_size, board_sz, image_size, image_points, cameraMatrix, distCoeffs);
cv::destroyWindow("Calibration");
return 0;
}
标定参数及优化¶
findChessboardCorners¶
通常在执行cv::findChessboardCorners
棋盘格角点查找时,会在最后一个参数flags设置一些参数进行角点查找优化,默认参数是CALIB_CB_ADAPTIVE_THRESH + CALIB_CB_NORMALIZE_IMAGE
,以下是这些参数的意义,这些参数可以单数使用也可以组合使用:
CALIB_CB_ADAPTIVE_THRESH
:使用自适应阈值将图像转换为黑色和白色,而不是一个固定的阈值水平(从图像的平均亮度计算出来的阈值)。CALIB_CB_NORMALIZE_IMAGE
:在自适应二值化之前,对图片的gamma值进行直方图均衡化-
CALIB_CB_FAST_CHECK
:运行一个快速棋盘格角点检查,如果没有找到则尽快返回。这可以大大加快在界面中没有棋盘格时候的查找速度。 -
CALIB_CB_FILTER_QUADS
:使用其他条件(例如轮廓区域,周长,正方形形状)过滤掉在轮廓检索阶段提取的假四边形。
推荐使用组合:CALIB_CB_ADAPTIVE_THRESH + CALIB_CB_NORMALIZE_IMAGE + CALIB_CB_FAST_CHECK
该功能需要在木板周围留有空白(如正方形的边框,越宽越好),以使检测在各种环境中都更加可靠。否则,如果没有边框且背景较暗,则无法正确分割外部黑色正方形。
cornerSubPix¶
// 提高角点精度
TermCriteria criteria(cv::TermCriteria::EPS + cv::TermCriteria::COUNT, 30, 0.1);
cv::cornerSubPix(gray, corners, cv::Size(5, 5), cv::Size(-1, -1), criteria);
函数参数说明如下:
-
image
:输入图像 -
corners
:输入角点的初始坐标以及精准化后的坐标用于输出。 -
winSize
:搜索窗口边长的一半,例如如果winSize=Size(5,5)
,则一个大小为11的搜索窗口将被使用。 -
zeroZone
:搜索区域中间的dead region边长的一半,有时用于避免自相关矩阵的奇异性。如果值设为(-1,-1)则表示没有这个区域。 criteria
:角点精准化迭代过程的终止条件。也就是当迭代次数超过criteria.maxCount
,或者角点位置变化小于criteria.epsilon
时,停止迭代过程。
calibrateCamera¶
使用示例:
cv::Mat cameraMatrix = cv::Mat::eye(3, 3, CV_64F);
cv::Mat distCoeffs = cv::Mat::zeros(8, 1, CV_64F);
vector<cv::Mat> rvecs, tvecs;
double rms = cv::calibrateCamera(objectPoints, imagePoints, imgSize, cameraMatrix, distCoeffs, rvecs, tvecs);
objectPoints
: 对象坐标点列表imagePoints
:图像像素点列表-
imageSize
:图像的大小,在计算相机的内参数和畸变矩阵需要用到 -
cameraMatrix
:内参矩阵。输入cv::Mat cameraMatrix
即可 -
distCoeffs
:畸变矩阵。输入cv::Mat distCoeffs
即可 -
rvecs
:旋转向量vector<cv::Mat> rvecs
-
tvecs
:位移向量vector<cv::Mat> tvecs
-
flags为标定是所采用的算法。可如下一个或者多个参数,通过+号连接即可:
CV_CALIB_USE_INTRINSIC_GUESS
:使用该参数时,在cameraMatrix
矩阵中应该有fx,fy,cx,cy
的估计值。否则将初始化(cx,cy
)图像的中心点,使用最小二乘估算出fx,fy
。如果内参矩阵和畸变矩阵已知,应使用标定模块中的solvePnP()
函数计算外参数矩阵。CV_CALIB_FIX_PRINCIPAL_POINT
:在进行优化时会固定光轴点。当CV_CALIB_USE_INTRINSIC_GUESS
参数被设置,光轴点将保持在中心或者某个输入的值。CV_CALIB_FIX_ASPECT_RATIO
:固定fx/fy
的比值,只将fy
作为可变量,进行优化计算。当CV_CALIB_USE_INTRINSIC_GUESS
没有被设置,fx
和fy
将会被忽略。只有fx/fy
的比值在计算中会被用到。CV_CALIB_ZERO_TANGENT_DIST
:设定切向畸变参数(p1,p2)
为零。CV_CALIB_FIX_K1,...,CV_CALIB_FIX_K6
:对应的径向畸变在优化中保持不变。CV_CALIB_RATIONAL_MODEL
:计算k4,k5,k6
三个畸变参数。如果没有设置,则只计算其它5个参数。
如果对calibrateCamera
的详细算法感兴趣,可以阅读张正友的标定算法《A Flexible New Technique for Camera Calibration》