基础
今天的针孔摄像头对图像做了很多扭曲,两个主要的扭曲是径向畸变和切向畸变。
由于径向畸变,直线会显示成曲线,当直线离图像中心越远时越明显。比如下面显示的这张图,棋盘的两个用红色标出来的边缘,你可以看到棋盘不是直线,也不和红线匹配。所有的直线都凸了。
扭曲可以用下面的来解决:
类似的,另一个切向畸变是因为成像的光线不是完全平行的到达镜像平面。所以有些区域比期望的要看上去离的近。可以用下面的方式解决:
简单说,我们需要找到5个参数,叫做畸变参数:
除此之外,我们需要找到更多的信息,比如摄像头的内部和外部参数,内部参数是摄像头特定的参数。包括焦距(fx, fy)。光学中心(cx, cy)。也叫摄像机矩阵。它只依赖摄像头本身。一旦算出来就可以保存下来为以后使用,它应该是一个3x3的矩阵:
外部参数对应了旋转和平移向量来反应一个3维的点到2维的系统里。
对于立体的应用,这些扭曲需要首先被矫正。要找到所有的这些参数,我们得做的是提供一些有良好定义模式的样例图像(比如棋盘)。我们找到特定的点(棋盘的四个角),我们知道他们的真实世界的坐标,我们知道他们在图像里的坐标。通过这些数据,后台就能解决一些数学问题以得到畸变参数。
编码
上面提到的,我们需要10个测试模式来做摄像机矫正。重要的输入数据是3D真实世界的点和他们对应的2D图像的点。2D图像点好办我们可以很容易的从图像里的得到。
3D真实世界的点呢?那些图像是从静态摄像机拍摄,棋盘放在另一个位置和方向。所以我们需要知道(X, Y, Z)的值。但是为了简单,我们可以说棋盘静止在XY平面。(所以Z=0)且摄像机相应的移动。这个考虑帮我们找到X,Y值,现在对于X,Y值,我们可以简单的传入点(0, 0), (1, 0), (2, 0),... 表示点的位置。在这种情况下,我们得到的结果是棋盘的大小量度。但是如果我们知道面积,(比如30毫米),我们可以传入值(0,0), (30,0), (60, 0),...,我们可以用mm来表示结果。
3D的点被叫做物体点,而2D的图像点被叫做图像点。
设置
要找到棋盘的模式,我们用函数cv2.findChessboardCorners()。我们也需要传我们要找的模式的类型,比如8x8网格,5x5网格等,在这个例子里,我们使用7x6网格(一般来说棋盘都是8x8的方块7x7的内角),它返回角点。这些角点会按照从左到右,从上到下的顺序放好。
这个函数可能没法在所有图像里找到需要的模式,所以一个号的选择是写代码,启动摄像机,然后检查每帧,找需要的模式,当取得了模式,找到角点,并存在列表里。同时提供一些间隔,然后在读下面的帧的时候我们可以调整我们的棋盘的方向。不断进行这个过程知道需要的好的模式都获取到了。即使在这个例子里,我们也不知道多少是好的,所以我们读入所有的图像取里面好的。
除了棋盘,我们可以使用一些环形滤线。但是之后使用函数cv2.findCirclesGrid()来找模式,据说使用环形滤线的时候回用更少的图像。
当我们找到了角点,我们用cv2.cornerSubPix()函数增加他们的准确度.我们也可以用cv2.drawChessboardCorners()来画出模式,所有这些步骤用下面的代码:
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.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)
cv2.imshow('img',img)
cv2.waitKey(500)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, 所有像素都返回。
img=cv2.imread('left12.jpg')
h,w=img.shape[:2]
newcameramtx,roi=cv2.getOptimalNewCameraMatrix(mtx,dist,(w,h),1,(w,h))
1. 使用cv2.undistort()
这是个捷径。只用调用函数,使用ROI
# 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('calibresult.png', dst)
2.使用重测图
这是曲线救国,首先找到从扭曲图像到非扭曲图像的映射函数。然后使用重测函数。
# 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('calibresult.png', dst)
两个方法都返回同样的结果。看下面:
你可以看到在结果里所有的边都是直的
现在你可以把摄像机矩阵和畸变参数存下来,使用Numpy的写函数(np.savez, np.savetxt等),为以后使用
重投影差
重投影差给了找到的参数是否准确的一个好的估计。这个应该越接近0越好。对于内在的,扭曲的,旋转和平移矩阵,我们首先用cv2.projectPoints()转换物体点到图像点,然后我们计算转换和找角点算法之间的绝对范数。要找到平均差我们计算所有校对图像的算术平均值。
mean_error = 0
for i in xrange(len(objpoints)):
imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2)/len(imgpoints2)
tot_error += error
print "total error: ", mean_error/len(objpoints)