针孔相机内外参标定简单介绍
之前有一个项目需要公司标内参,之前对这方面没有接触过,网上找了很多资料,记录下相机标定的基础知识。文章是个人浅显理解。如有错误还请指正,非常感谢!
参考链接:
- 坐标系转换:相机参数标定(camera calibration)及标定结果如何使用_Aoulun的博客-CSDN博客
- 标定opencv代码:Opencv 相机内参标定及使用_Gene_2022的博客-CSDN博客_相机内参标定
- opencv标定函数介绍:【OpenCV3学习笔记 】相机标定函数 calibrateCamera( ) 使用详解(附相机标定程序和数据)_ZealCV的博客-CSDN博客_calibratecamera参数
- 标定的数学原理:相机标定之张正友标定法数学原理详解(含python源码) – 知乎 (zhihu.com)
- 流程及代码:Opencv——相机标定 – 一抹烟霞 – 博客园 (cnblogs.com)
- 相机模型介绍:相机模型-鱼眼模型/Omnidirectional Camera(1)_苏源流的博客-CSDN博客_davide scaramuzza
- 图像去畸变,对极约束之undistort,initUndistortRectifyMap,undistort
相机标定的目的是找到三维空间与二维空间转换的关系,也称为几何模型,这个几何模型就是相机标定要求解的参数。
1. 坐标系转换
首先需要了解几个坐标系:像素坐标系、图像坐标系、相机坐标系、世界坐标系。
像素坐标系
通常从相机采集到的图像以 像素
为单位。一般处理时都是以图像的左上角为原点,水平是u方向,竖直是v方向,就是像素坐标系,如下u-v坐标系,某一点在像素坐标系中的位置为 ( u i , v j ) (u_i,v_j)(u i ,v j )。
; 图像坐标系
图像坐标系以图像中心为原点,以尺寸为单位,通常一个像素的尺寸为d x ∗ d y dx*dy d x ∗d y。那么像素坐标系与图像坐标系之间就有一个转换关系,图像坐标系以x为水平方向,y为竖直方向。
像素坐标系与图像坐标系的转换关系如下:
{ x = u d x − u 0 d x y = v d y − v 0 d y \left{\begin{matrix} x=udx – u_0dx\ y=vdy – v_0dy \end{matrix}\right.{x =u d x −u 0 d x y =v d y −v 0 d y
转换为矩阵格式并转换为齐次坐标:
[ x y 1 ] = [ d x 0 0 0 d y 0 0 0 0 ] [ u v 0 ] + [ − u 0 d x − v 0 d y 1 ] \begin{bmatrix} x\ y \ 1 \end{bmatrix}=\begin{bmatrix} dx & 0 & 0 \ 0 & dy & 0 \ 0 & 0 & 0 \end{bmatrix}\begin{bmatrix} u\ v \ 0 \end{bmatrix}+\begin{bmatrix} -u_0dx\ -v_0dy \ 1 \end{bmatrix}⎣⎡x y 1 ⎦⎤=⎣⎡d x 0 0 0 d y 0 0 0 0 ⎦⎤⎣⎡u v 0 ⎦⎤+⎣⎡−u 0 d x −v 0 d y 1 ⎦⎤
齐次坐标可以区分点和向量,便于计算机做图像处理时进行放射变换。
点和向量的区分方式是最后一个数值是否为1,如果为1是点,如果是0是向量。
普通坐标系变换到齐次坐标系:
- 点(x,y,z)变换为(x,y,z,1);
- 向量(x,y,z)变换为(x,y,z,0);
齐次坐标系变换到普通坐标系:
- 点(x,y,z,1)变换为(x,y,z);
- 向量(x,y,z,0)变换为(x,y,z);
相机坐标系
相机坐标系是一个三维的坐标系,以相机的光轴为z z z轴,光线在相机光学系统的中心位置是原点O c O_c O c ,相机坐标系的x c x_c x c 轴与图像坐标系的x x x轴平行,相机坐标系的y c y_c y c 轴与图像坐标系的y y y轴平行,如下:
在相机坐标系与图像坐标系转换前要了解一下什么是虚平面。我们都知道小孔成像:用一个带小孔的板遮挡在墙体和物之间,墙上就会出现倒立的实像。
本来成像平面在小孔的后面,为了方便计算,我们在物体与小孔中间设置一个虚平面,虚平面到小孔的距离与成像平面到小孔的距离相等,在虚平面上得到的图像就是正的,且与成像平面的大小一致。
根据相似三角形可以得到:
O c O i O c B = O c C O c A = p C P A = O i p A B \frac{O_cO_i}{O_cB} = \frac{O_cC}{O_cA} = \frac{pC}{PA} = \frac{O_ip}{AB}O c B O c O i =O c A O c C =P A pC =A B O i p
=> f Z = x X c = y Y c \frac {f}{Z} = \frac {x}{X_c} = \frac{y}{Y_c}Z f =X c x =Y c y
=> X c = x Z c f X_c = \frac {xZ_c}{f}X c =f x Z c ,Y c = y Z c f Y_c = \frac {yZ_c}{f}Y c =f y Z c
=>
[ Y c X c Z c 1 ] = [ Z c f 0 0 0 Z c f 0 0 0 Z c 0 0 1 ] [ x y 1 ] \begin{bmatrix} Y_c\ X_c\ Z_c\ 1 \end{bmatrix} = \begin{bmatrix} \frac {Z_c}{f} & 0 & 0 \ 0& \frac {Z_c}{f} & 0\ 0& 0 & Z_c \ 0&0 & 1 \end{bmatrix}\begin{bmatrix} x\ y\ 1 \end{bmatrix}⎣⎡Y c X c Z c 1 ⎦⎤=⎣⎡f Z c 0 0 0 0 f Z c 0 0 0 0 Z c 1 ⎦⎤⎣⎡x y 1 ⎦⎤
; 世界坐标系
世界坐标系是现实世界的坐标。世界坐标系反映了图像与真实物体之间的一个映射关系。对于单目,是物体真实尺寸与图像尺寸之间的关系,对于双目,需要知道多个相机之间的关系。
世界坐标系的原点是O w O_w O w ,且世界坐标系的X w , Y w , Z w Xw,Y_w,Z_w Xw ,Y w ,Z w 轴与其它三个坐标系不是平行的,有一定的旋转和平移。
旋转后坐标系与原来坐标系关系如下:
旋转参数可以表示为:
{ X w = R x X c Y w = R y Y c Z w = R z Z c \left{\begin{matrix} X_w = R_x X_c\ Y_w = R_y Y_c \ Z_w = R_z Z_c \end{matrix}\right.⎩⎨⎧X w =R x X c Y w =R y Y c Z w =R z Z c
平移可以表示为:
{ X w = X c + t x Y w = Y c + t y Z w = Z c + t z \left{\begin{matrix} X_w = X_c + t_x\ Y_w = Y_c + t_y\ Z_w = Z_c + t_z \end{matrix}\right.⎩⎨⎧X w =X c +t x Y w =Y c +t y Z w =Z c +t z
旋转和平移表示为:
{ X w = R x X c + t x Y w = R y Y c + t y Z w = R z Z c + t z \left{\begin{matrix} X_w = R_x X_c + t_x\ Y_w = R_y Y_c + t_y\ Z_w = R_z Z_c + t_z \end{matrix}\right.⎩⎨⎧X w =R x X c +t x Y w =R y Y c +t y Z w =R z Z c +t z
2. 内参和外参
由上面的各个坐标系的变换我们最终可以得到世界坐标系和像素坐标系之间的转换关系:
[ X w Y w Z w 1 ] = [ R T 0 1 ] [ Z c f 0 0 0 Z c f 0 0 0 Z c 0 0 1 ] [ d x 0 − u 0 d x 0 d y − v 0 d y 0 0 1 ] [ u v 1 ] \begin{bmatrix}\ X_w \ Y_w\ Z_w\ 1 \end{bmatrix} = \begin{bmatrix} R & T \ 0 & 1 \end{bmatrix}\begin{bmatrix} \frac{Z_c}{f} & 0 & 0\ 0 & \frac{Z_c}{f} &0 \ 0 &0 &Z_c \ 0&0 &1 \end{bmatrix}\begin{bmatrix} dx & 0 & -u_0dx \ 0 & dy & -v_0dy \ 0 & 0 & 1 \end{bmatrix}\ \begin{bmatrix} u \ v \ 1 \end{bmatrix}⎣⎡X w Y w Z w 1 ⎦⎤=[R 0 T 1 ]⎣⎡f Z c 0 0 0 0 f Z c 0 0 0 0 Z c 1 ⎦⎤⎣⎡d x 0 0 0 d y 0 −u 0 d x −v 0 d y 1 ⎦⎤⎣⎡u v 1 ⎦⎤
=>
[ X w Y w Z w 1 ] = [ R T 0 1 ] [ Z c d x f 0 − Z c u 0 d x f 0 Z c d y f − Z c v 0 d y f 0 0 Z c 0 0 1 ] [ u v 1 ] \begin{bmatrix}\ X_w \ Y_w\ Z_w\ 1 \end{bmatrix} = \begin{bmatrix} R & T \ 0 & 1 \end{bmatrix} \begin{bmatrix} \frac{Z_cdx}{f} & 0 & -\frac{Z_cu_0dx}{f} \ 0 & \frac{Z_cdy}{f} & -\frac{Z_cv_0dy}{f} \ 0 &0 &Z_c \ 0&0 &1 \end{bmatrix} \begin{bmatrix} u \ v \ 1 \end{bmatrix}⎣⎡X w Y w Z w 1 ⎦⎤=[R 0 T 1 ]⎣⎡f Z c d x 0 0 0 0 f Z c d y 0 0 −f Z c u 0 d x −f Z c v 0 d y Z c 1 ⎦⎤⎣⎡u v 1 ⎦⎤
其中[ R T 0 1 ] \begin{bmatrix} R & T \ 0 & 1 \end{bmatrix}[R 0 T 1 ]表示相机外参,[ Z c d x f 0 − Z c u 0 d x f 0 Z c d y f − Z c v 0 d y f 0 0 Z c 0 0 1 ] \begin{bmatrix} \frac{Z_cdx}{f} & 0 & -\frac{Z_cu_0dx}{f} \ 0 & \frac{Z_cdy}{f} & -\frac{Z_cv_0dy}{f} \ 0 &0 &Z_c \ 0&0 &1 \end{bmatrix}⎣⎡f Z c d x 0 0 0 0 f Z c d y 0 0 −f Z c u 0 d x −f Z c v 0 d y Z c 1 ⎦⎤表示相机内参。
3. 畸变
畸变总结起来可以分为两类:径向畸变和切向畸变。
径向畸变
校正公式:
x d r = x ( 1 + k 1 r 2 + k 2 r 4 + k 3 r 6 ) y d r = y ( 1 + k 1 r 2 + k 2 r 4 + k 3 r 6 ) r 2 = x 2 + y 2 x_{dr} = x(1+k_1r^2 + k_2r^4 + k_3r^6) \ y_{dr} = y(1 + k_1r^2 + k_2r^4 + k_3r^6) \ r^2 = x^2 + y^2 x d r =x (1 +k 1 r 2 +k 2 r 4 +k 3 r 6 )y d r =y (1 +k 1 r 2 +k 2 r 4 +k 3 r 6 )r 2 =x 2 +y 2
切向畸变
校正公式:
x d t = 2 p 1 x y + p 2 ( r 2 + 2 x 2 ) + 1 y d t = 2 p 1 ( r 2 + 2 y 2 ) + 2 p 2 x y + 1 x_{dt} = 2p_1xy + p_2(r^2 + 2x^2) +1 \ y_{dt} = 2p_1(r^2+2y^2) + 2p_2xy + 1 x d t =2 p 1 x y +p 2 (r 2 +2 x 2 )+1 y d t =2 p 1 (r 2 +2 y 2 )+2 p 2 x y +1
畸变校正:
x d = x d r + x d t y d = y d r + y d t x_d = x_{dr} + x{dt} \ y_d = y_{dr} + y_{dt}x d =x d r +x d t y d =y d r +y d t
; 3. 标定
目的:获取到相机内参和每一幅图像的外参(旋转和平移参数),有了内参和外参就可以对之后拍摄的图像进行校正。
输入:图像上所有内角点的坐标,标定板上所有内角点的空间三维坐标。
输出:相机的内参和外参。
流程:
- 准备标定板/chart图,拍摄图片,建议10~20张,不能少于3张;
- 对每一张标定图片提取角点信息
findChessboardCorners()
; - 对每一张标定图片进一步提取亚像素角点信息
cornerSubPix()/ find4QuadCornerSubpix()
; - 找到每一张标定图片上的内角点(为了显示用)
find4QuadCornerSubpix()
; - 标定(调用函数)
calibrateCamera()
; - 对标定结果进行评价
calibrateCamera()
;得到内外参后,对空间的三维点进行重新投影计算,得到空间三维点在图像上新的投影点的坐标,计算投影坐标和亚像素角点坐标之间的偏差,偏差越小,标定结果越好; - 查看标定效果,利用标定结果对拍摄到的图像进行校正
**initUndistortRectifyMap()+remap()/undistort()
;
CPP
代码:
#include
#include
#include
#include
#include
#include
#include
using namespace std;
using namespace cv;
void GetAllFormatFiles(string path, vector<string>& files, string format)
{
intptr_t hFile = 0;
struct _finddata_t fileinfo;
string p;
if ((hFile = _findfirst(p.assign(path).append("\\*" + format).c_str(), &fileinfo)) != -1)
{
do
{
if ((fileinfo.attrib & _A_SUBDIR))
{
if (strcmp(fileinfo.name, ".") != 0 && strcmp(fileinfo.name, "..") != 0)
{
GetAllFormatFiles(p.assign(path).append("\\").append(fileinfo.name), files, format);
}
}
else
{
files.push_back(p.assign(fileinfo.name));
}
} while (_findnext(hFile, &fileinfo) == 0);
_findclose(hFile);
}
}
void m_calibration(vector<string> &FilesName, Size board_size, Size square_size, Mat &cameraMatrix, Mat &distCoeffs, vector<Mat> &rvecsMat, vector<Mat> &tvecsMat)
{
ofstream fout("caliberation_result.txt");
cout << "开始提取角点.................." << endl;
int image_count = 0;
Size image_size;
vector<Point2f> image_points;
vector<vector<Point2f>> image_points_seq;
for (int i = 0;i < FilesName.size();i++)
{
image_count++;
cout << "image_count = " << image_count << endl;
Mat imageInput = imread(FilesName[i]);
if (image_count == 1)
{
image_size.width = imageInput.cols;
image_size.height = imageInput.rows;
cout << "image_size.width = " << image_size.width << endl;
cout << "image_size.height = " << image_size.height << endl;
}
bool ok = findChessboardCorners(imageInput, board_size,
image_points, cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE);
if (0 == ok)
{
cout <<"第"<< image_count <<"张照片提取角点失败,请删除后,重新标定!"<<endl;
imshow("失败照片", imageInput);
waitKey(0);
}
else
{
Mat view_gray;
cout << "imageInput.channels()=" << imageInput.channels() << endl;
cvtColor(imageInput, view_gray, cv::COLOR_RGB2GRAY);
cv::cornerSubPix(view_gray, image_points, cv::Size(11, 11), cv::Size(-1, -1), cv::TermCriteria( TermCriteria::EPS + TermCriteria::COUNT, 20, 0.01));
image_points_seq.push_back(image_points);
drawChessboardCorners(view_gray, board_size, image_points, true);
}
}
cout << "角点提取完成!!!" << endl;
vector<vector<Point3f>> object_points_seq;
for (int t = 0;t < image_count;t++)
{
vector<Point3f> object_points;
for (int i = 0;i < board_size.height;i++)
{
for (int j = 0;j < board_size.width;j++)
{
Point3f realPoint;
realPoint.x = i*square_size.width;
realPoint.y = j*square_size.height;
realPoint.z = 0;
object_points.push_back(realPoint);
}
}
object_points_seq.push_back(object_points);
}
double err_first = calibrateCamera(object_points_seq, image_points_seq, image_size,
cameraMatrix, distCoeffs, rvecsMat, tvecsMat, cv::CALIB_FIX_K3);
fout << "重投影误差1:" << err_first << "像素" << endl << endl;
cout << "标定完成!!!" << endl;
cout << "开始评价标定结果..................";
double total_err = 0.0;
double err = 0.0;
double totalErr = 0.0;
double totalPoints = 0.0;
vector<Point2f> image_points_pro;
for (int i = 0;i < image_count;i++)
{
projectPoints(object_points_seq[i], rvecsMat[i], tvecsMat[i], cameraMatrix, distCoeffs, image_points_pro);
err = norm(Mat(image_points_seq[i]), Mat(image_points_pro), NORM_L2);
totalErr += err*err;
totalPoints += object_points_seq[i].size();
err /= object_points_seq[i].size();
total_err += err;
}
fout << "重投影误差2:" << sqrt(totalErr / totalPoints) << "像素" << endl << endl;
fout << "重投影误差3:" << total_err / image_count << "像素" << endl << endl;
cout << "开始保存定标结果.................." << endl;
Mat rotation_matrix = Mat(3, 3, CV_32FC1, Scalar::all(0));
fout << "相机内参数矩阵:" << endl;
fout << cameraMatrix << endl << endl;
fout << "畸变系数:\n";
fout << distCoeffs << endl << endl << endl;
for (int i = 0; i < image_count; i++)
{
fout << "第" << i + 1 << "幅图像的旋转向量:" << endl;
fout << rvecsMat[i] << endl;
Rodrigues(rvecsMat[i], rotation_matrix);
fout << "第" << i + 1 << "幅图像的旋转矩阵:" << endl;
fout << rotation_matrix << endl;
fout << "第" << i + 1 << "幅图像的平移向量:" << endl;
fout << tvecsMat[i] << endl << endl;
}
cout << "定标结果完成保存!!!" << endl;
fout << endl;
}
void m_undistort(const string &path,vector<string> &FilesName, Size image_size, Mat &cameraMatrix, Mat &distCoeffs)
{
Mat mapx = Mat(image_size, CV_32FC1);
Mat mapy = Mat(image_size, CV_32FC1);
Mat R = Mat::eye(3, 3, CV_32F);
cout << "保存矫正图像" << endl;
string imageFileName;
stringstream StrStm;
string temp;
for (int i = 0; i < FilesName.size(); i++)
{
Mat imageSource = imread(FilesName[i]);
Mat newimage = imageSource.clone();
undistort(imageSource, newimage, cameraMatrix, distCoeffs);
StrStm << i + 1;
StrStm >> temp;
imageFileName = path + temp + "_d.jpg";
imwrite(imageFileName, newimage);
StrStm.clear();
imageFileName.clear();
}
std::cout << "保存结束" << endl;
}
void main()
{
vector<string> imageFilesName;
vector<string> files;
imageFilesName.clear(); files.clear();
string filePath = "D:\\chess_image";
string format = ".jpg";
GetAllFormatFiles(filePath, imageFilesName, format);
cout << "找到的文件有" << endl;
for (int i = 0; i < imageFilesName.size(); i++)
{
files.push_back(filePath + "\\" + imageFilesName[i]);
cout << files[i] << endl;
}
string calibrateDir = filePath + "\\calibrateImage\\";
_mkdir(calibrateDir.c_str());
Size board_size = Size(7, 6);
Size square_size = Size(30, 30);
Mat cameraMatrix = Mat(3, 3, CV_32FC1, Scalar::all(0));
Mat distCoeffs = Mat(1, 5, CV_32FC1, Scalar::all(0));
vector<Mat> rvecsMat;
vector<Mat> tvecsMat;
m_calibration(files, board_size, square_size, cameraMatrix, distCoeffs, rvecsMat, tvecsMat);
cv::Size image_size(640, 480);
m_undistort(calibrateDir,files, image_size, cameraMatrix, distCoeffs);
return;
}
python
代码:
import argparse
from argparse import RawTextHelpFormatter
import numpy as np
import cv2
def cam_calib_find_corners(img, rlt_dir, img_idx, col, row):
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, corners = cv2.findChessboardCorners(gray, (col, row), None)
corners2 = cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1),
(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 10, 0.001))
if ret == True:
sav_path = rlt_dir + "\\" + str(img_idx) + "_corner.jpg"
cv2.drawChessboardCorners(img, (col, row), corners2, ret)
cv2.imwrite(sav_path, img)
return (ret, corners2)
def cam_calib_calibrate(img_dir, rlt_dir, col, row, img_num):
w = 0
h = 0
all_corners = []
patterns = []
x, y = np.meshgrid(range(col), range(row))
prod = row * col
pattern_points = np.hstack((x.reshape(prod, 1), y.reshape(prod, 1), np.zeros((prod, 1)))).astype(np.float32)
for i in range(1, img_num + 1):
img_path = img_dir + "\\" + str(i) + ".jpg"
print(img_path)
img = cv2.imread(img_path)
(h, w) = img.shape[:2]
ret, corners = cam_calib_find_corners(img, rlt_dir, i, col, row)
all_corners.append(corners)
patterns.append(pattern_points)
rms, cameraMatrix, distCoeffs, rvecs, tvecs = cv2.calibrateCamera(patterns, all_corners, (w, h), None, None)
return (cameraMatrix, distCoeffs)
def cam_calib_correct_img(crct_img_dir, cameraMatrix, distCoeffs):
for i in range(1, 3):
crct_img_path = crct_img_dir + "\\" + str(i) + ".jpg"
img = cv2.imread(crct_img_path)
(h1, w1) = img.shape[:2]
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(cameraMatrix, distCoeffs, (w1, h1), 1, (w1, h1))
dst = cv2.undistort(img, cameraMatrix, distCoeffs, None, newcameramtx)
x, y, w, h = roi
dst = dst[y:y + h, x:x + w]
rlt_path = crct_img_dir + "\\rlt\\" + str(i) + "_crct.jpg"
cv2.imwrite(rlt_path, dst)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="读取标定的图片并保存结果", formatter_class=RawTextHelpFormatter)
parser.add_argument("--img_dir", help="标定图片路径", type=str, metavar='', default="D:\\calib_img")
parser.add_argument("--rlt_dir", help="保存路径", type=str, metavar='', default="D:\\calib_img\\rlt")
parser.add_argument("--crct_img_dir", help="待矫正图像路径", type=str, metavar='', default="D:\\calib_img\\crct_img")
parser.add_argument("--row_num", help="每一行有多少个角点,边缘处的不算", type=int, metavar='', default="7")
parser.add_argument("--col_num", help="每一列有多少个角点,边缘处的不算", type=int, metavar='', default="6")
parser.add_argument("--img_num", help="多少幅图像", type=int, metavar='', default="21")
args = parser.parse_args()
cameraMatrix, distCoeffs = cam_calib_calibrate(args.img_dir, args.rlt_dir, args.row_num, args.col_num, args.img_num)
cam_calib_correct_img(args.crct_img_dir, cameraMatrix, distCoeffs)
Original: https://blog.csdn.net/sinat_41752325/article/details/127574235
Author: 小地瓜重新去华容道工作
Title: 【OpenCv】相机标定介绍及python/c++实现
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/701540/
转载文章受原作者版权保护。转载请注明原作者出处!