攝像機標定
單孔攝像機(照相機)會給圖像帶來很多畸變。畸變主要有兩種:徑向畸變和切想畸變。如下圖所示,用紅色直線將棋盤的兩個邊標註出來,但是你會發現棋盤的邊界並不和紅線重合。所有我們認爲應該是直線的也都凸出來了。
所以需要進行相機標定
棋盤格標定
OpenCV使用棋盤格板進行標定,如下圖所示。爲了標定相機,我們需要輸入一系列三維點和它們對應的二維圖像點。在黑白相間的棋盤格上,二維圖像點很容易通過角點檢測找到。而對於真實世界中的三維點呢?由於我們採集中,是將相機放在一個地方,而將棋盤格定標板進行移動變換不同的位置,然後對其進行拍攝。所以我們需要知道(X,Y,Z)的值。但是簡單來說,我們定義棋盤格所在平面爲XY平面,即Z=0。對於定標板來說,我們可以知道棋盤格的方塊尺寸,例如30mm,這樣我們就可以把棋盤格上的角點座標定義爲(0,0,0),(30,0,0),(60,0,0),···,這個結果的單位是mm。
3D點稱爲object points,2D圖像點稱爲image points。
檢測棋盤格角點
爲了找到棋盤格模板,我們使用openCV中的函數cv2.findChessboardCorners()。我們也需要告訴程序我們使用的模板是什麼規格的,例如88的棋盤格或者55棋盤格等,建議使用x方向和y方向個數不相等的棋盤格模板。下面實驗中,我們使用的是86的棋盤格,每個方格邊長是20mm,即含有75的內部角點。這個函數如果檢測到模板,會返回對應的角點,並返回true。當然不一定所有的圖像都能找到需要的模板,所以我們可以使用多幅圖像進行定標。除了使用棋盤格,我們還可以使用圓點陣,對應的函數爲cv2.findCirclesGrid()。
找到角點後,我們可以使用cv2.cornerSubPix()可以得到更爲準確的角點像素座標。我們也可以使用cv2.drawChessboardCorners()將角點繪製到圖像上顯示。如下圖所示:
用手機拍攝了14張棋盤格圖片,並將分辨率調整到832x624,既將原分辨率縮小20%
標定
通過上面的步驟,我們得到了用於標定的三維點和與其對應的圖像上的二維點對。我們使用cv2.calibrateCamera()進行標定,這個函數會返回標定結果、相機的內參數矩陣、畸變係數、旋轉矩陣和平移向量。
ret: 0.49780289785568144
內參數矩陣:
[[662.65760958 0. 411.7554067 ]
[ 0. 643.62036576 312.36138692]
[ 0. 0. 1. ]]
畸變係數:
[[ 1.75605608e-01 -7.32319628e-01 1.74170917e-04 1.07586378e-04
8.15249028e-01]]
旋轉向量: [array([[ 0.06848192],
[-0.11021601],
[ 0.19342032]]), array([[ 0.08989725],
[-0.2535112 ],
[-0.02463974]]), array([[0.09910906],
[0.25820579],
[0.00510581]]), array([[-0.14623837],
[ 0.00945762],
[ 0.001026 ]]), array([[ 0.20322661],
[-0.02586824],
[-0.00751255]]), array([[ 0.20643026],
[-0.087326 ],
[-0.03735129]]), array([[ 0.05928905],
[-0.31920575],
[-0.01057067]]), array([[0.07565014],
[0.21659039],
[0.00056359]]), array([[0.20951298],
[0.23124036],
[0.05598759]]), array([[0.02498131],
[0.05687095],
[0.00956028]]), array([[-0.1802921 ],
[-0.03419934],
[-0.00123688]])]
平移向量: [array([[-2.36878258],
[-2.90199425],
[ 7.32577822]]), array([[-2.62155616],
[-1.99853277],
[ 6.49172398]]), array([[-3.28628108],
[-2.26570619],
[ 7.62271742]]), array([[-3.06893891],
[-2.59407061],
[ 7.86211726]]), array([[-3.09682038],
[-2.22304886],
[ 7.07652815]]), array([[-2.73779139],
[-1.77450487],
[ 6.88388611]]), array([[-2.28058289],
[-2.10824111],
[ 6.15841957]]), array([[-3.35984547],
[-2.08592 ],
[ 7.32862568]]), array([[-3.70854886],
[-2.24924643],
[ 7.31569625]]), array([[-3.26049871],
[-2.235856 ],
[ 7.17300778]]), array([[-3.03386901],
[-2.40116135],
[ 7.3427078 ]])]
由於手機拍攝的圖片分辨率過高,原圖:4160X3120的分辨率,相當於把圖片分辨率縮小20%。
根據攝像頭自身參數
焦距f:4mm
像素大小Δx=Δy:1.12um
計算可知:fx=fΔx=3571.42857、fy=fΔy=3571.42857
u0=4160/2=2080、v0=3120/2=1560
通過相機標定的值:
fx=662.65760958 、fy=643.62036576
u0=411.7554067、v0=312.36138692
換算以後:
fx=3313.28805 、 fy=3218.10183
u0=2058.77703 、 v0=1561.80693
由於預先圖片分辨率縮減了20%,再加上攝像頭的誤差,加上我自己拍攝的因素,雖然誤差達到了200多,但是最後得到的這個結果,我覺得也是合理的。
去畸變
第三步我們已經得到了相機內參和畸變係數,在將圖像去畸變之前,我們還可以使用cv.getOptimalNewCameraMatrix()優化內參數和畸變係數,通過設定自由自由比例因子alpha。當alpha設爲0的時候,將會返回一個剪裁過的將去畸變後不想要的像素去掉的內參數和畸變係數;當alpha設爲1的時候,將會返回一個包含額外黑色像素點的內參數和畸變係數,並返回一個ROI用於將其剪裁掉。
然後我們就可以使用新得到的內參數矩陣和畸變係數對圖像進行去畸變了。
反投影誤差
通過反投影誤差,我們可以來評估結果的好壞。越接近0,說明結果越理想。通過之前計算的內參數矩陣、畸變係數、旋轉矩陣和平移向量,使用cv2.projectPoints()計算三維點到二維圖像的投影,然後計算反投影得到的點與圖像上檢測到的點的誤差,最後計算一個對於所有標定圖像的平均誤差,這個值就是反投影誤差。
total error: 0.07951334176924868
完整代碼如下:
#coding:utf-8
import cv2
import numpy as np
import glob
# 找棋盤格角點
# 閾值
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
#棋盤格模板規格
w = 9
h = 6
# 世界座標系中的棋盤格點,例如(0,0,0), (1,0,0), (2,0,0) ....,(8,5,0),去掉Z座標,記爲二維矩陣
objp = np.zeros((w*h,3), np.float32)
objp[:,:2] = np.mgrid[0:w,0:h].T.reshape(-1,2)
# 儲存棋盤格角點的世界座標和圖像座標對
objpoints = [] # 在世界座標系中的三維點
imgpoints = [] # 在圖像平面的二維點
images = glob.glob('test_pic/*.jpg')
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) #圖像二值化
# 找到棋盤格角點
ret, corners = cv2.findChessboardCorners(gray, (w,h),None)
# 如果找到足夠點對,將其存儲起來
if ret == True:
cv2.cornerSubPix(gray,corners,(11,11),(-1,-1),criteria)
objpoints.append(objp)
imgpoints.append(corners)
# 將角點在圖像上顯示
cv2.drawChessboardCorners(img, (w,h), corners, ret)
cv2.imshow('findCorners',img)
cv2.waitKey(500)
#cv2.destroyAllWindows()#刪除建立的全部窗口
# 標定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)
#相機標定 返回標定結果、相機的內參數矩陣、畸變係數、旋轉矩陣和平移向量
# 保存相機參數
np.savez('C.npz', mtx=mtx, dist=dist, rvecs=rvecs, tvecs=tvecs)
print("ret:", ret)
print("內參數矩陣:\n", mtx)
print("畸變係數:\n", dist)
print("旋轉向量:", rvecs) # 外參數
print("平移向量:", tvecs) # 外參數
# 去畸變
img2 = cv2.imread('test_pic/left05.jpg')
h, w = img2.shape[:2]
newcameramtx, roi=cv2.getOptimalNewCameraMatrix(mtx,dist,(w,h),0,(w,h)) # 自由比例參數
dst = cv2.undistort(img2, mtx, dist, None, newcameramtx)
# 根據前面ROI區域裁剪圖片
#x,y,w,h = roi
#dst = dst[y:y+h, x:x+w]
cv2.imwrite('calibresult.jpg',dst)
# 反投影誤差
total_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)
total_error += error
print("total error: {}".format(total_error/len(objpoints)))
- 在1基礎上,使用同一相機,將棋盤格放在前方1m左右固定,然後使用線性方法進行相對位姿估計,然後評價結果的合理性。
在上問的攝像機標定中,我們已經得到了攝像機矩陣,畸變係數等。有了這些信息我們就可以估計圖像中圖案的姿勢,比如目標對象是如何擺放,如何旋轉等。對一個平面對象來說,我們可以假設 Z=0,這樣問題就轉化成攝像機在空間中是如何擺放(然後拍攝)的。所以,如果我們知道對象在空間中的姿勢,我們就可以在圖像中繪製一些 2D 的線條來產生 3D 的效果。
完整代碼:
import cv2
import numpy as np
import glob
# 加載相機標定的數據
with np.load('C.npz') as X:
mtx, dist, _, _ = [X[i] for i in ('mtx', 'dist', 'rvecs', 'tvecs')]
def draw(img, corners, imgpts):
"""
在圖片上畫出三維座標軸
:param img: 圖片原數據
:param corners: 圖像平面點座標點
:param imgpts: 三維點投影到二維圖像平面上的座標
:return:
"""
corner = tuple(corners[0].ravel())
cv2.line(img, corner, tuple(imgpts[0].ravel()), (255, 0, 0), 5)
cv2.line(img, corner, tuple(imgpts[1].ravel()), (0, 255, 0), 5)
cv2.line(img, corner, tuple(imgpts[2].ravel()), (0, 0, 255), 5)
return img
# 初始化目標座標系的3D點
objp = np.zeros((6 * 7, 3), np.float32)
objp[:, :2] = np.mgrid[0:7, 0:6].T.reshape(-1, 2)
# 初始化三維座標系
axis = np.float32([[3, 0, 0], [0, 3, 0], [0, 0, -3]]).reshape(-1, 3) # 座標軸
# 加載打包所有圖片數據
images = glob.glob('test_pic/*.jpg')
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 找到圖像平面點座標點
ret, corners = cv2.findChessboardCorners(gray, (7, 6), None)
if ret:
# PnP計算得出旋轉向量和平移向量
_, rvecs, tvecs, _ = cv2.solvePnPRansac(objp, corners, mtx, dist)
print("旋轉變量", rvecs)
print("平移變量", tvecs)
# 計算三維點投影到二維圖像平面上的座標
imgpts, jac = cv2.projectPoints(axis, rvecs, tvecs, mtx, dist)
# 把座標顯示圖片上
img = draw(img, corners, imgpts)
cv2.imshow('img', img)
cv2.waitKey(500)
#cv2.destroyAllWindows()