为了确定空间物体表面某点的三维几何位置与其在相机生成的图像中对应点之间的相互关系,必须建立相机成像的几何模型,这些几何模型参数就是相机参数。
这些参数需要通过实验与计算得到,求解参数的过程就称之为相机标定。标定结果的精度及算法的稳定性会直接影响相机工作成像的准确性。
相机标定的方法:
1、自标定:找图像中特征点
2、标定板标定: 特征点易求,稳定性好,常采用的方法
相机标定的类型,按照相机是否静止,可分为:
静态相机标定(标定板动,相机静止)
动态相机标定(标定板静止,相机运动)
相机标定过程中涉及的坐标系类型:世界坐标系,相机坐标系,图像坐标系,像素坐标系。
世界坐标系(xw,yw,zw):摄像机与被摄物体可以放置在环境中任意位置,这样就需要在环境中建立一个坐标系,来表示摄像机和被摄物体的位置,这个坐标系就成为世界坐标系。世界坐标系可以任意选择,为假想坐标系,在被指定后随即 不变且唯一,即为绝对坐标系
为什么需要世界坐标系:
将不同视点/视角拍摄的图像信息整合在一起就必须将所有的信息放在同一个坐标系下,这个坐标系应与各张图像的 相机/物体/像素 这些相对坐标系无关,在确定后应不变且唯一,即应为绝对坐标系,即是所说的 世界坐标系 。
- 在单目相机中,通常选择拍摄第一张图像时的相机坐标系作为世界坐标系,即以拍摄第一张图像时相机的光心(小孔)作为原点,X轴为水平方向,Y轴为竖直方向,Z轴指向拍摄第一张图像时相机所观察的方向。选定后世界坐标系便不再发生变化,即不变且唯一。
- 在双目相机(A,B)中,与单目相机大同小异,可选取其中一个相机A拍摄第一张图像时的相机坐标系为世界坐标系,即以相机A拍摄第一张图像时相机的光心(小孔)作为原点,X轴为水平方向,Y轴为竖直方向,Z轴指向拍摄第一张图像时相机A所观察的方向。
相机坐标系(xc,yc,zc):以相机的光心(小孔)作为原点,X轴为水平方向,Y轴为竖直方向,Z轴指向相机所观察的方向(即是与成像平面垂直)形成的三维直角坐标系,其随相机的移动而变化,即为相对坐标系。
图像坐标系(x,y):也叫平面坐标系,为引出像素坐标系而过渡引入,原点为透镜光轴与成像平面的交点,X轴与Y轴分别平行于相机坐标系的Xc与Yc轴,是二维平面直角坐标系,单位为毫米mm,其依托于相机坐标系,为相对坐标系。
坐标系转换就是为了将空间的三维世界坐标系转换至图像处理的二维像素坐标系。
1、世界坐标系->相机坐标系
世界坐标系是以物体的中心作为原点,相机坐标系是以相机位置作为原点,如果将一个相机放在我们认为的世界坐标系的原点上,此时,相机坐标==世界坐标,但是实际这样的情况几乎很少发生。
实际情况下相机与世界坐标原点不一定重合,不管相机与世界坐标系原点距离如何,存在一个平移向量,使用 T表示;同时相机 和物体也不一定是水平对齐的姿态,所以坐标转换还包括以x,y,z轴为轴的旋转角度,用R来表示。
t 来表示平移, 任意一个平移包括 (x,y,z)三个方向;
表示 R 旋转向量时,如果绕着 某一个轴 进行旋转,如沿着z轴旋转,那么其实三维坐标点的z值是不会变的,唯一改变的是x和y方向的值。
通过图上的位置关系,由三角形相似可以得到任意一点的坐标转换关系,其过程为:
引入齐次坐标,既能够用来明确区分向量和点,同时也更易用于进行仿射几何变换。齐次坐标的左右是用(N+1)维来代表 N 维坐标。
若将三维坐标视为一个列向量,那么矩阵*列向量得到的新向量的每一个分量,都是旧的列向量的线性函数,因而三维笛卡尔坐标与矩阵的乘法只能实现三维坐标的缩放和旋转,而无法实现坐标平移。
将三维的笛卡尔坐标添加一个额外坐标,就可以实现坐标平移了,而且保持了三维向量与矩阵乘法具有的缩放和旋转操作。这个就称为齐次坐标。而这种变换也称为仿射变换(affine transformation),不属于线性变换。
仿射变换是:“线性变换”+“平移”。
2、相机坐标系->图像(平面)坐标系
两个不同的相机,即使在所处的位置一样的情况下,拍摄的两张照片,很大概率是不一样的。主要是相机自身的相关的参数,需要解相机小孔成像的原理。
针孔相机模型中,只要确定相机参数和畸变参数就可以唯一的确定针孔相机模型, 这个过程就称为「相机标定」。
对单目视觉而言,求得内参和畸变参数后,就可以对拍摄的图像做变换和矫正。矫正完拍摄的图像之后,对图像做其他任务处理。
对于双目视觉而言,需要用到世界坐标系。对单目视觉做完内参和畸变参数的矫正之后,就可以用这些变换后的图像,同时结合世界坐标系实现定位或者其他用途了。
分享链接:https://pan.baidu.com/s/1P4jl30-EK_nch_-et0-9XA .
提取码:xqq4
说明:
标定图片需要使用标定板在不同位置、不同角度、不同姿态下拍摄的多张图片进行标定,10~20张左右。
为什么不用一张?
根据张正友标定法,只需要求得B矩阵和H矩阵就可以换算出参数矩阵。而B矩阵和H矩阵的求解是通过最大似然估计来优化得到的。
# 使用FindChessboardCorners()函数提取角点信息
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))
# 使用 drawChessboardCorners函数用于绘制被成功标定的内角点
cv2.drawChessboardCorners(img, (col,row), corners2, ret)
cv2.imwrite(save_path, img)
print("保存找到角点的图像地址为: ",save_path)
# 相机标定过程
def cam_calib_calibrate(img_dir, rlt_dir1, 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_dir1, i, col, row)
#合并所有角点
all_corners.append(corners)
patterns.append(pattern_points)
# 获取到棋盘标定图的内角点图像坐标之后,使用calibrateCamera()函数进行标定,计算相机内参和外参系数
rms, cameraMatrix, distCoeffs, rvecs, tvecs = cv2.calibrateCamera(patterns, all_corners, (w, h), None, None)
print("rms",rms) # 残差rms,表示得到参数后,经过校准实际点和投影点位置的差异
print("cameraMatrix",cameraMatrix) # cameraMatrix为内参数矩阵
print("distCoeffs",distCoeffs) # distCoeffs为畸变矩阵
print("rvecs",rvecs) # rvecs为旋转向量
print("tvecs",tvecs) # tvecs为位移向量
return (cameraMatrix, distCoeffs)
#对参数做处理,使得最后的输出的矫正图像去表不必要的边缘。
newcameramtx,roi = cv2.getOptimalNewCameraMatrix(cameraMatrix,distCoeffs,(w1,h1),1,(w1,h1))
#对测试图像进行矫正
dst = cv2.undistort(img, cameraMatrix, distCoeffs, None, newcameramtx)
- python+opencv
import argparse
from argparse import RawTextHelpFormatter
import numpy as np
import os
import cv2
import yaml
# 对每一张标定图片,提取角点信息
def cam_calib_find_corners(img, rlt_dir1, img_idx, col, row):
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 使用FindChessboardCorners()函数提取角点信息
ret, corners = cv2.findChessboardCorners(gray,(col,row), None)
#为了得到稍微精确一点的角点坐标,进一步对角点进行亚像素寻找;# 使用cornerSubPix函数在角点检测中精确化角点位置
corners2 = cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 10, 0.001))
if ret == True:
#保存角点图像
# res_find_corners_img_path = img_dir + "\\res\\"
save_path = rlt_dir1 + "\\" + str(img_idx) + "_corner.jpg"
if not os.path.exists(rlt_dir1):
os.mkdir(rlt_dir1)
print("rlt_dir1 directory made")
# 使用 drawChessboardCorners函数用于绘制被成功标定的内角点
cv2.drawChessboardCorners(img, (col,row), corners2, ret)
cv2.imwrite(save_path, img)
print("保存找到角点的图像地址为: ",save_path)
return (ret, corners2)
# 相机标定过程
def cam_calib_calibrate(img_dir, rlt_dir1, 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_dir1, i, col, row)
#合并所有角点
all_corners.append(corners)
patterns.append(pattern_points)
# 获取到棋盘标定图的内角点图像坐标之后,使用calibrateCamera()函数进行标定,计算相机内参和外参系数
rms, cameraMatrix, distCoeffs, rvecs, tvecs = cv2.calibrateCamera(patterns, all_corners, (w, h), None, None)
print("rms",rms) # 残差rms,表示得到参数后,经过校准实际点和投影点位置的差异
print("cameraMatrix",cameraMatrix) # cameraMatrix为内参数矩阵
print("distCoeffs",distCoeffs) # distCoeffs为畸变矩阵
print("rvecs",rvecs) # rvecs为旋转向量
print("tvecs",tvecs) # tvecs为位移向量
return (cameraMatrix, distCoeffs)
#write the matrix to yaml
mtx=cameraMatrix.tolist()
dist=distCoeffs.tolist()
data={"camera_matrix":mtx,"dist_coeff":dist}
with open("parameter.yaml","w") as file:
yaml.dump(data,file)
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]
res_path = crct_img_dir + "\\rlt\\"
rlt_path2 = res_path + str(i) + "_crct.jpg"
if not os.path.exists(res_path):
os.mkdir(res_path)
print("res_path directory made")
# elif not os.path.exists(rlt_path2):
# os.mkdir(rlt_path2)
else:
print("res_path directory existed")
cv2.imwrite(rlt_path2, dst)
print()
print("保存校正后的图像,地址:",rlt_path2)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="读取标定的图片并保存结果",formatter_class=RawTextHelpFormatter)
parser.add_argument("--img_dir",help="输入的标定图片路径",type=str,metavar='', default="E:\\002_GIT\\cam_calibration\\camer2\\calib_img")
parser.add_argument("--rlt_dir1",help="保存绘画角点后的标定图路径",type=str,metavar='',default="E:\\002_GIT\\cam_calibration\\camer2\\calib_img\\rlt")
parser.add_argument("--crct_img_dir",help="待矫正待测试图像路径",type=str,metavar='',default="E:\\002_GIT\\cam_calibration\\camer2\\crct_img")
parser.add_argument("--rlt_dir2",help="矫正后图像路径",type=str,metavar='',default="E:\\002_GIT\\cam_calibration\\camer2\\crct_img\\rlt")
parser.add_argument("--row_num",help="每一行有多少个角点,边缘处的不算",type=int,metavar='',default="9")
parser.add_argument("--col_num",help="每一列有多少个角点,边缘处的不算",type=int,metavar='',default="6")
parser.add_argument("--img_num",help="多少幅图像",type=int,metavar='',default="19")
args=parser.parse_args()
# 标定相机
cameraMatrix, distCoeffs = cam_calib_calibrate(args.img_dir, args.rlt_dir1, args.row_num, args.col_num, args.img_num)
#矫正图片
cam_calib_correct_img(args.crct_img_dir, cameraMatrix, distCoeffs)
import numpy as np
import os
import cv2
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import pickle
def calibrate_camera():
# 棋盘格个数及其横纵内角点个数
objp_dict = {
1: (9, 6),
2: (9, 6),
3: (9, 6),
4: (9, 6),
5: (9, 6),
6: (9, 6),
7: (9, 6),
8: (9, 6),
9: (9, 6),
10: (9, 6),
11: (9, 6),
12: (9, 6),
13: (9, 6),
14: (9, 6),
15: (9, 6),
16: (9, 6),
17: (9, 6),
18: (9, 6),
19: (9, 6),
20: (9, 6),
}
objp_list = []
corners_list = []
for k in objp_dict:
nx, ny = objp_dict[k]
objp = np.zeros((nx*ny,3), np.float32)
objp[:,:2] = np.mgrid[0:nx, 0:ny].T.reshape(-1, 2)
fname = 'mark2/%s.jpg' % str(k)
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 棋盘格角点
ret, corners = cv2.findChessboardCorners(gray, (nx, ny), None)
if ret == True:
objp_list.append(objp)
corners_list.append(corners)
else:
print('Warning: ret = %s for %s' % (ret, fname))
# 相机标定
img = cv2.imread('mark2/1.jpg') # 储存选取好的标定板照片的文件夹路径,并选定到第一张
img_size = (img.shape[1], img.shape[0])
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objp_list, corners_list, img_size,None,None)
return mtx, dist
if __name__ == '__main__':
mtx, dist = calibrate_camera()
# 采用Matlab相机标定工具包相机标定所得内参矩阵,畸变系数
# mtx = np.array([[1.5596e+03, 0, 1.2790e+03], [0, 1.5652e+03, 1.0674e+03], [0, 0, 1]])
# dist = np.array([[-0.3571, 0.2105, 5.4513e-04, 5.9984e-04, 0]])
save_dict = {'mtx': mtx, 'dist': dist}
print(mtx, dist)
np.savez('calibrate_camera\\In_mtx2', mtx=mtx, dist=dist)
#读取畸变图片
img = mpimg.imread('mark2/1.jpg') # 储存选取好的标定板照片的文件夹路径,并选定到第一张
dst = cv2.undistort(img, mtx, dist, None, mtx)
plt.imshow(dst)
plt.savefig('undistort_calibration.jpg')
参考文章 1: 相机模型中的世界坐标系究竟指什么?.
参考文章 2: 世界坐标系,相机坐标系,图像坐标系,像素坐标系的转换.
参考文章 3: 如何通俗地讲解「仿射变换」这个概念?.
参考文章 4: 深入探索透视投影变换.
参考文章 5: 相机参数标定(camera calibration)及标定结果如何使用.
参考文章 6: 单目相机标定实践(完整过程).
参考文章 7: 相机标定(Camera calibration).
参考文章 8: 相机标定(理论推导+具体实现).