本节将学习以下内容:
- 相机产生的畸变类型,以及相机的内参和外参等概念。
- 如何计算这些参数,并对图像进行去畸变处理。
基本原理
如今廉价的针孔相机会给图像带来严重的畸变,其中最主要的两种畸变是径向畸变和切向畸变。
径向畸变会导致直线在图像中呈现弯曲状,且越远离图像中心,畸变效果越明显。 例如,下图展示了一个棋盘格的两条边缘(用红线标出),但可以看到实际边框并非直线,与红线并不吻合。 所有本应是直线的部分都出现了向外凸出的变形。 更多细节可参考《光学畸变》相关文献。
此畸变的解决方法如下:
$$\begin{aligned} x_{corrected} = x( 1 + k_1 r^2 + k_2 r^4 + k_3 r^6) \\ y_{corrected} = y( 1 + k_1 r^2 + k_2 r^4 + k_3 r^6) \end{aligned}$$
同样,另一种畸变是切向畸变,它是由于镜头与成像平面不完全平行所导致的。 这种畸变会使图像中的某些区域看起来比实际更近。其计算公式如下:
$$\begin{aligned} x_{corrected} = x + [ 2p_1xy + p_2(r^2+2x^2)] \\ y_{corrected} = y + [ p_1(r^2+ 2y^2)+ 2p_2xy] \end{aligned}$$
简而言之,需要计算五个参数(即畸变系数),其表达式为:
$$Distortion \; coefficients=(k_1 \hspace{10pt} k_2 \hspace{10pt} p_1 \hspace{10pt} p_2 \hspace{10pt} k_3)$$
此外还需要获取相机的内参(intrinsic parameters)和外参(extrinsic parameters)。 内参是相机本身的固有属性,包含焦距 ($f_x,f_y$)、光心坐标 ($c_x, c_y$)等信息,通常用一个3×3的相机矩阵表示。 由于内参仅取决于相机硬件,因此计算完成后可保存供后续使用。
$$\begin{aligned} camera \; matrix = \left [ \begin{matrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{matrix} \right ] \end{aligned}$$
外参对应于旋转和平移向量,它们将3D点的坐标转换到一个坐标系中。
对于立体视觉应用,首先需要校正这些畸变。为了找到所有这些参数,需要提供一些定义良好的图案样本图像(例如棋盘格)。 在图像中找到一些特定点(如棋盘格的方格角点)。知道这些点在真实世界空间中的坐标,也知道它们在图像中的坐标。 利用这些数据,通过后台的数学计算来获得畸变系数。这就是整个过程的概要。 为了获得更好的结果,至少需要10张测试图案。
代码实现
如前所述,相机标定至少需要10张测试图案。OpenCV自带了一些棋盘格图像(参见samples/cpp/left01.jpg -- left14.jpg
),可以直接使用。
为了便于理解,先以一张棋盘格图像为例。
相机标定所需的重要输入数据是一组3D真实世界点及其对应的2D图像点。
2D图像点可以直接从图像中获取(这些图像点是棋盘格上两个黑色方格相接的位置)。
那么真实世界空间中的3D点呢?这些图像是由固定相机拍摄的,棋盘格被放置在不同的位置和方向。 因此需要知道($f_x,f_y$),值。但为了简化,可以假设棋盘格始终位于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个内部角点)。
该函数返回角点坐标和一个返回值retval,如果找到图案则retval为True。
这些角点按从左到右、从上到下的顺序排列。
此函数可能无法在所有图像中找到所需的图案。 因此一个好的做法是编写代码让相机启动后检查每一帧是否包含所需图案。 一旦找到图案,就提取角点并存入列表。同时设置一定的间隔时间再读取下一帧,以便调整棋盘格的方向。 持续此过程直到获得足够数量的有效图案。 即使在本例提供的14张图像中,也不确定有多少是有效的,因此需要读取所有图像并筛选出有效的。
除了棋盘格,也可以使用圆形网格,但需要使用cv2.findCirclesGrid()
来查找图案。
据说使用圆形网格时所需的图像数量更少。
找到角点后,可以使用cv2.cornerSubPix()
来提高其精度。
还可以使用cv2.drawChessboardCorners()
绘制图案。所有这些步骤都包含在以下代码中:
%matplotlib inline
import matplotlib.pyplot as plt
import os
import numpy as np
import cv2
import glob
# 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.
os.chdir('/data/cvdata/')
images = glob.glob('*.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 found, add object points, image points (after refining them)
if ret == True:
objpoints.append(objp)
corners2 = cv2.cornerSubPix(gray,corners,(11,11),(-1,-1),criteria)
imgpoints.append(corners2)
# Draw and display the corners
img = cv2.drawChessboardCorners(img, (7,6), corners2,ret)
plt.imshow(img)
# cv2.imshow('img',img)
# cv2.waitKey(500)
# cv2.destroyAllWindows()
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1],None,None
)
import os
import numpy as np
# 尝试在临时目录保存(跨平台兼容)
try:
save_path = os.path.join(os.getcwd(), 'xx_B.npz') # 当前目录
np.savez(save_path, ret=ret, mtx=mtx, dist=dist, rvecs=rvecs, tvecs=tvecs)
except OSError:
# 如果失败,改用系统临时目录
import tempfile
save_path = os.path.join(tempfile.gettempdir(), 'xx_B.npz')
np.savez(save_path, ret=ret, mtx=mtx, dist=dist, rvecs=rvecs, tvecs=tvecs)
print(f"文件保存在临时目录: {save_path}")
文件保存在临时目录: /tmp/xx_B.npz
img = cv2.imread('/data/cvdata/left12.jpg')
h, w = img.shape[:2]
newcameramtx, roi=cv2.getOptimalNewCameraMatrix(mtx,dist,(w,h),1,(w,h))
# undistort
dst = cv2.undistort(img, mtx, dist, None, newcameramtx)
# crop the image
x,y,w,h = roi
dst = dst[y:y+h, x:x+w]
cv2.imwrite('xx_calibresult.png',dst)
False
# undistort
mapx,mapy = cv2.initUndistortRectifyMap(mtx,dist,None,newcameramtx,(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.imwrite('xx_calibresult.png',dst)
False
plt.imshow(dst)
<matplotlib.image.AxesImage at 0x7ff4e0519df0>
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.027718074946637413