目标
在本节中,将学习
- 由相机引起的失真类型
- 如何找到相机的固有和非固有特性
- 如何基于这些特性使图像不失真
基础
一些针孔相机会给图像带来明显的失真。两种主要的变形:
- 径向变形( radial distortion)
- 切向变形( tangential distortion)
径向变形
径向变形会导致直线出现弯曲。距图像中心越远,径向畸变越大。例如,下面显示一个图像,其中棋盘的两个边缘用红线标记。但是,会看到棋盘的边框不是直线,并且与红线不匹配。所有预期的直线都凸出。
径向变形的变形量可以用下面的式子表示:
x d i s t o r t e d = x ( 1 + k 1 r 2 + k 2 r 4 + k 3 r 6 ) x_{distorted} = x( 1 + k_1 r^2 + k_2 r^4 + k_3 r^6) xdistorted=x(1+k1r2+k2r4+k3r6)
y d i s t o r t e d = y ( 1 + k 1 r 2 + k 2 r 4 + k 3 r 6 ) y_{distorted} = y( 1 + k_1 r^2 + k_2 r^4 + k_3 r^6) ydistorted=y(1+k1r2+k2r4+k3r6)
切向畸变
由于摄像镜头未完全平行于成像平面对齐,因此会发生切向畸变。因此,图像中的某些区域看起来可能比预期的要近。切向畸变的变化量可以表示为:
x d i s t o r t e d = x + [ 2 p 1 x y + p 2 ( r 2 + 2 x 2 ) ] y d i s t o r t e d = y + [ p 1 ( r 2 + 2 y 2 ) + 2 p 2 x y ] x_{distorted} = x + [ 2p_1xy + p_2(r^2+2x^2)] \\ y_{distorted} = y + [ p_1(r^2+ 2y^2)+ 2p_2xy] xdistorted=x+[2p1xy+p2(r2+2x2)]ydistorted=y+[p1(r2+2y2)+2p2xy]
简而言之,需要找到五个参数,这些参数也被称为失真系数:
D i s t o r t i o n c o e f f i c i e n t s = ( k 1 k 2 p 1 p 2 k 3 ) Distortion \; coefficients=(k_1 \hspace{10pt} k_2 \hspace{10pt} p_1 \hspace{10pt} p_2 \hspace{10pt} k_3) Distortioncoefficients=(k1k2p1p2k3)
除此之外,还需要其他一些信息,相机的内在和外在参数.
- 内部参数特定于摄像机,他们包括诸如焦距 ( f x , f y ) (f_x,f_y) (fx,fy)和光学中心$(c_x,c_y)之类的信息。焦距和光学中心可用于创建相机矩阵,该相机矩阵可用于消除由于特定相机镜头而引起的畸变。相机矩阵对于特定相机而言是唯一的,因此一旦计算出,就可以在同一相机拍摄的其他图像上重复使用。它表示为3x3矩阵:
c a m e r a m a t r i x = [ f x 0 c x 0 f y c y 0 0 1 ] camera \; matrix = \left [ \begin{matrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{matrix} \right ] cameramatrix=⎣⎡fx000fy0cxcy1⎦⎤
- 外在参数对应于旋转和平移矢量,其将3D点的坐标平移为坐标系。
对于3D应用,首先需要纠正这些失真,就要找到这些参数。以国际象棋棋盘为示例图像,找到一些已经知道其相对位置的特定点(例如棋盘上的四角)。假设知道了现实世界空间中这些点的坐标,也知道图像中的坐标,就可以求解失真系数了。
代码
如上所述,相机校准至少需要10个测试图案。OpenCV项目中附带了一些国际象棋棋盘的图像,因此将利用这些图像进行讲解。假设给定一张棋盘图像,相机校准所需的重要输入数据是3D真实点集以及图像中这些点的相应2D坐标。2D图像点可以从图像中轻松找到点(这些图像点是国际象棋棋盘中两个黑色正方形相互接触的位置)。
真实3D点如何处理?这些图像是从静态相机拍摄的,而国际象棋棋盘放置在不同的位置和方向。因此,需要知道 ( X , Y , Z ) (X, Y, Z) (X,Y,Z)值。但是为简单起见,可以说棋盘在 X Y XY XY平面上保持静止(因此Z始终为0),并且照相机也相应地移动了。这种考虑有助于仅找到X,Y值。现在对于X,Y值,可以简单地将点传递为(0,0),(1,0),(2,0),…,这表示点的位置。在这种情况下,得到的结果将是棋盘正方形的大小比例。但是,当我们知道正方形大小(例如30毫米),则可以将值传递为(0,0),(30,0),(60,0),…。因此,得到的结果以毫米为单位。(在这种情况下,我们不知道正方形的大小,因为我们没有拍摄那些图像,因此我们以正方形的大小进行传递)。
3D点称为对象点,而2D图像点称为图像点。
开始
为了在国际象棋棋盘图像中查找图案,可以使用函数cv2.findChessboardCorners()
。该函数需要传递所需的图案,例如8x8网格,5x5网格等。在此示例中使用7x6网格。(通常,棋盘有8x8的正方形和7x7的内部角)。它返回角点和返回值,如果获得图案,则返回值为True。这些角将按顺序放置(从左到右,从上到下)
此函数可能无法在所有图像中找到所需的图案。因此一个不错的选择是编写代码,使它启动相机并检查每帧所需的图案。获得图案后,找到角并将其存储在列表中。另外,在读下一帧之前请设置一些时间间隔,以便可以在不同方向上调整棋盘。继续此过程,直到获得所需数量的良好图案为止。即使在此处提供的示例中,也不确定给出的14张图像中有多少张是好的。因此,必须阅读所有图像并仅保存好的团。 除了棋盘之外,还可以使用圆形网格。 在这种情况下,必须使用函数
cv2.findCirclesGrid()
来找到模式。 较少的图像足以使用圆形网格执行相机校准。
一旦找到拐角,就可以使用cv2.cornerSubPix()
来提高其精度。还可以使用cv2.drawChessboardCorners()
绘制图案。所有这些步骤都包含在以下代码中:
# termination criteria
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
# prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0)
objp = np.zeros((6*7,3), np.float32)
objp[:, :2] = np.mgrid[0:7,0:6].T.reshape(-1,2)
# # Arrays to store object points and image points from all the images.
objpoints = [] # 3d point in real world space
imgpoints = [] # 2d points in image plane.
images = glob.glob(r'right*.jpg')
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# find the chess board corners
ret, corners = cv2.findChessboardCorners(gray, (7,6), None)
if ret:
objpoints.append(objp)
corners2 = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria)
imgpoints.append(corners)
# draw and display the corners
cv2.drawChessboardCorners(img, (7, 6), corners2, ret)
cv2.imshow('img', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
校准
有了目标点和图像点,就可以进行校准。可以使用函数cv2.calibrateCamera()
返回相机矩阵,失真系数,旋转和平移矢量等。
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)
不畸变
现在,可以对拍摄图像进行不畸变处理。OpenCV提供了两种方法来执行此操作。但首先,可以使用cv2.getOptimalNewCameraMatrix()
基于自由缩放参数来优化相机矩阵。如果缩放参数alpha = 0
,则返回具有最少不需要像素的未失真图像。因此,它甚至可能会删除图像角落的一些像素。如果alpha = 1,则所有像素都保留有一些额外的黑色图像。此函数还返回可用于裁剪结果的图像ROI。
实例:
img = cv2.imread('right12.jpg')
h, w, _ = img.shape
newcameratx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))
1使用cv2.undistort
只需调用该函数并使用上面获得的ROI裁剪结果即可。
# undistort
dst = cv2.undistort(img, mtx, dist, None, newcameratx)
# copy the image
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]
cv2.imshow('calibresult.png', dst)
cv2.waitKey(0)
cv2.destroyAllWindows()
2. 使用remapping
该方式有点困难。首先,找到从扭曲图像到未扭曲图像的映射函数。然后使用重映射函数。
尽管如此,两种方法都给出相同的结果。看到下面的结果:
# undistort
mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, newcameratx, (w, h), 5)
dst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
# crop the image
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]
cv2.imshow('calibresult.png', dst)
cv2.waitKey(0)
cv2.destroyAllWindows()
可以看到所有边缘都是笔直的。 另外,可以使用NumPy中的写入功能(np.savez,np.savetxt
等)存储相机矩阵和失真系数,以备将来使用。
重投影误差|Re-projection Error
重投影误差可以很好地估计找到的参数的精确程度。重投影误差越接近零,表明发现的参数越准
确。给定固有,失真,旋转和平移矩阵,必须首先使用cv2.projectPoints()
将对象点转换为
图像点。然后,可以计算出通过变换得到的绝对值和拐角发现算法之间的绝对值范数。为了
找到平均误差,计算为所有校准图像计算的误差的算术平均值。
# re projection error
mean_error = 0
for i in range(len(objpoints)):
imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2)/len(imgpoints2)
mean_error += error
print( "total error: {}".format(mean_error/len(objpoints)) )
total error: 0.025365426097625716
保存相机矩阵和失真系数与加载
可以以json或者pickle等方式保存相机矩阵和失真系数
import pickle
# save data
cam_calib = {"cam_matrix": mtx, "dist_coeffs": dist}
with open("cam_calib.p", "wb") as f:
pickle.dump(cam_calib, f)
# load previously save data
with open(r"cam_calib.p", "rb") as f:
data = pickle.load(f)
mtx = data['cam_matrix']
dist = data['dist_coeffs']