【计算机视觉】OpenCV实现单目相机标定
文章目錄
- 單目相機標定(基于Python OpenCV)
- 1.上期填坑
- 2.單目相機標定
- 2.1 數據采集
- 2.2 角點提取
- 2.3 參數求解
- 2.4 參數評估(重投影誤差)
- 2.5 相機位姿(棋盤位姿)可視化
- 2.6 同Matlab標定結果比較
單目相機標定(基于Python OpenCV)
1.上期填坑
在開始本篇博客之前,先填一下上一篇博客【計算機視覺】基于ORB角點+RANSAC算法實現圖像全景拼接的坑(不算填吧,算做一個記錄,因為并沒有解決問題,留著看以后有沒有空解決👀),不想看的可以直接跳到下一節。
首先解決如何拼接多張圖像(上篇博客只能拼接兩張圖像,多張圖像需要保存兩張圖像的匹配結果,再重新讀取計算角點)
改進的方法的基本思路是,僅計算當前兩張圖像的單應變換M_current,通過先前的單應矩陣M_before再計算一個累積變換,最終通過矩陣乘法得到的M的變換就延續了當前變換的累積變換矩陣:
# 一開始無先前變換,因此設置為單位陣M_before = np.eye(3,3)result = cv2.imread('datas/1.jpg')# result = CalcUndistort(result, mtx, dist)result,_,_ = utils.auto_reshape(result, 1080)img2 = resultcors2, desc2= extraORBfromImg(orb, img2)for i in range(1,6):print(i)img1 = cv2.imread('datas/'+str(i+1)+'.jpg')# img1 = CalcUndistort(img1, mtx, dist)img1,_,_ = utils.auto_reshape(img1, 1080)cors1, desc1= extraORBfromImg(orb, img1)match_dist, match_idx = ORBMatch(match, desc1, desc2)# 得到匹配點對的坐標match_pts = findMatchCord(match_idx, cors1, cors2)# 可視化匹配點# utils.drawMatches(img1, img2, cors1, cors2, match_idx)# RANSAC迭代去除異常點對update_match_pts = RANSAC(match_pts)# 最小二乘方法計算單應矩陣M = calc_homography(update_match_pts)# 圖像拼接結果可視化, 并傳遞累積單應變換矩陣M ,返回先前累積拼接結果resultresult, M = homography_trans(M, M_before, img1, result)M_before = M# 不用再提取一遍:img2 = img1cors2, desc2 = cors1, desc1值得注意的是,代碼采用的匹配順序是從右至左,為了保證每次只提取最左側圖像的角點,img1對于圖像的拍攝時序應該要在img2的左側。
相應的,函數homography_trans也要做修改:
# 可視化圖像對映射效果 def homography_trans(M, M_before, img1, img2):M = M_before @ M# out_img 第一張圖像映射到第二張x_min, x_max, y_min, y_max, M2 = calc_border(M, img1.shape)# 透視變換+平移變換(使得圖像在正中央)M = M2 @ Mw, h = int(round(x_max)-round(x_min)), int(round(y_max)-round(y_min))out_img = cv2.warpPerspective(img1, M, (w, h))# print(out_img.shape)# cv2.imshow('ww',out_img)# cv2.waitKey(0)# 調整兩張圖像位姿一致:# x方向out_img_blank_x = np.zeros((out_img.shape[0], abs(round(x_min)), 3)).astype(np.uint8)img2_blank_x = np.zeros((img2.shape[0], abs(round(x_min)), 3)).astype(np.uint8)if(x_min>0):# print(1)out_img = cv2.hconcat((out_img_blank_x, out_img))if(x_min<0):# print(2)img2 = cv2.hconcat((img2_blank_x, img2))# y方向out_img_blank_y = np.zeros((abs(round(y_min)), out_img.shape[1], 3)).astype(np.uint8)img2_blank_y = np.zeros((abs(round(y_min)), img2.shape[1], 3)).astype(np.uint8)if(y_min>0):# print(3)out_img = cv2.vconcat((out_img, out_img_blank_y))if(y_min<0):# print(4)img2 = cv2.vconcat((img2_blank_y, img2))# 調整兩張圖像尺度一致(邊緣填充):if(img2.shape[0]<out_img.shape[0]):blank_y = np.zeros((out_img.shape[0]-img2.shape[0], img2.shape[1], 3)).astype(np.uint8)img2 = cv2.vconcat((img2, blank_y)) else:blank_y = np.zeros((img2.shape[0]-out_img.shape[0], out_img.shape[1], 3)).astype(np.uint8)out_img = cv2.vconcat((out_img, blank_y)) if(img2.shape[1]<out_img.shape[1]):blank_x = np.zeros((img2.shape[0], out_img.shape[1]-img2.shape[1], 3)).astype(np.uint8)img2 = cv2.hconcat((img2, blank_x)) else:blank_x = np.zeros((out_img.shape[0], img2.shape[1]-out_img.shape[1], 3)).astype(np.uint8)out_img = cv2.hconcat((out_img, blank_x)) # cv2.imwrite('out_img.jpg',out_img)# 疊加result = addMatches(out_img, img2)# 圖像背景白mask = 255*np.ones(result.shape).astype(np.uint8)gray_res = cv2.cvtColor(result, cv2.COLOR_BGR2GRAY)mask[gray_res==0]=0cv2.imwrite('mask.jpg',mask)# result[result==0]=255cv2.imwrite('result.jpg',result)return result, M重點是改了這里(實現單應變換的傳遞):
... ... M = M_before @ M ... ... M = M2 @ M(Ps:如果從左至右拼接,好像有點問題…)
然后另一個問題是:往后的拼接圖像計算出的單應矩陣就會越扭曲:整一個視覺效果就會像這樣
五張圖像的拼接效果:
事實上,當前拼的結果還不完整,但一整個圖像的像素已經非常大了,由于單應變換導致的圖像扭曲占據了較大的圖像空間,因此到后面系統直接報了內存分配不足的錯誤:
numpy.core._exceptions.MemoryError: Unable to allocate 7.71 GiB for an array with shape (17705, 19486, 3) and data type float64
還記得的上一篇博客提出的解決方案是,利用相機矩陣的畸變矩陣對圖像進行一個徑向畸變扭曲,目前本人已經初略了解了相機矩陣以及畸變參數,對圖像做一個畸變處理(需要自己相機的內參)并不是很大問題,但是發現仍然不能很好的解決問題:
# 為圖像添加徑向畸變, 避免透視作用下可視化效果不美觀 def CalcUndistort(img, mtx, dist):return cv2.undistort(img, mtx, dist*12, dst=None, newCameraMatrix=None)少量圖片下,問題不大,但是拼接圖像一多,仍然會有明顯的透視效果:
原以為是畸變得不夠,增加畸變系數后,拼接效果反而變差,(可能是因為圖像畸變破壞了單應變換的基本假設):
所以,OpenCV, 還得是你
OpenCV為圖像拼接專門封裝了一個stitching類,最終能夠實現可以看的效果的拼接大致需要經過以下流程:
(上次博客中提到的論文中的rectangling矩形化方法就是解決整個圖像拼接中扭曲圖像這一小步)
(記得課上蔡老師說過,這學期學了計算機視覺這門課程,就可以去網上接項目賺錢,現在看起來,和小時候我看個宇宙紀錄片,就想成為物理學家有點類似。當然,如果是單純只學了課上的內容)
對于計算機視覺而言,實踐是必要的,如果沒有親手實現,單純的被動輸入或許永遠也不會知道某種看似可行的方法會存在的缺陷。
關于具體實現或者相關函數的API,可以觀摩這位大佬的博客OpenCV總結3——圖像拼接Stitching,我上面那張圖也是從他那偷的,respect!
2.單目相機標定
進入正題
在圖像測量過程以及機器視覺應用中,為確定空間物體表面某點的三維幾何位置與其在圖像中對應點之間的相互關系,必須建立相機成像的幾何模型,這些幾何模型參數就是相機參數。在大多數條件下這些參數必須通過實驗與計算才能得到,這個求解參數(內參、外參、畸變參數)的過程就稱之為相機標定(或攝像機標定)。無論是在圖像測量或者機器視覺應用中,相機參數的標定都是非常關鍵的環節,其標定結果的精度及算法的穩定性直接影響相機工作產生結果的準確性。因此,做好相機標定是做好后續工作的前提,提高標定精度是科研工作的重點所在。
OpenCV封裝了單目相機標定所需的各種方法。本篇博客的實現將基于這些封裝好的函數。
本次單目相機的標定方法基于張正友博士于1998年提出的棋盤格標定方法,
論文鏈接:A Flexible New Technique for Camera Calibration,收錄于2000年PAMI期刊。
具體的數學原理以及公式的推導可以參考網絡上各類詳細的博客以及教程,本篇博客不再贅述。
2.1 數據采集
標定板規格: GP340 12x9 25mm
相機: HUAWEI MATE 30 PRO(LIO AL00)
焦距: 27mm
分辨率: 3648x2736
拍攝數量: 20
2.2 角點提取
(獲取角點兩個坐標系下的坐標,作為求解方程中的已知量)
首先,基于cv2.findChessboardCorners尋找棋盤格角點:
# 定位角點 def find_chessboard_cor(img):# 轉為灰度圖gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)# OpenCV內置函數提取棋盤格角點, (11,8)為棋盤格尺寸-1(12x9)is_success, corner = cv2.findChessboardCorners(gray_img, (11,8), None)# 計算亞像素時停止迭代的標準# 后者表示迭代次數達到了最大次數時停止,前者表示角點位置變化的最小值已經達到最小時停止迭代criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 0.001)# 亞像素角點檢測,提高角點定位精度, (7, 7)為考慮角點周圍區域的大小corner = cv2.cornerSubPix(gray_img, corner, (7, 7), (-1, -1), criteria) return is_success, corner值得注意的是,這里我們必須使用cv2.cornerSubPix再進行一次迭代定位亞像素角點,使得角點的定位更為精確。
基于cv2.drawChessboardCorners可視化棋盤格角點:
# 可視化角點 def draw_chessboard_cor(img, cor, is_success):cv2.drawChessboardCorners(img, (11,8), cor, is_success)cv2.imshow('cor', img)cv2.waitKey(50)OpenCV提取棋盤角點可視化:
別忘了我們提取了棋盤格角點坐標的目的:這一步是為了獲取角點在圖像坐標系中的坐標。為了求解相機內外參矩陣,緊接著我們還需要獲取棋盤格在原始世界坐標系中的坐標。
值得一提的是,無論是固定相機移動棋盤格進行拍攝,還是固定棋盤格移動相機進行拍攝,在張正友標定法的假設中,棋盤格永遠處在世界坐標系Z=0的平面上,棋盤格最左上角的角點為世界坐標系原點,往右為x軸正方向,往下為y軸正方向:
由于同一張標定板的規格不會改變,因此,世界坐標系通過人為定義即可:
# 注:相機參數的計算只要求角點之間的世界坐標比例一致,因此可以單位化world_coord = np.zeros((w * h, 3), np.float32)world_coord[:, :2] = np.mgrid[0:w, 0:h].T.reshape(-1, 2)世界坐標(單位化):
[[ 0. 0. 0.]
[ 1. 0. 0.]
[ 2. 0. 0.]
[ 3. 0. 0.]
[ 4. 0. 0.]
[ 5. 0. 0.]
[ 6. 0. 0.]
[ 7. 0. 0.]
… …
]]
shape = (88, 3) # 每個棋盤格上提取11*8=88個角點
一個特別值得注意且容易迷惑的地方是,由于相機參數的求解只依賴于角點與角點之間世界坐標的比例關系以及圖像的大小(分辨率),而與具體的尺度無關,因此,角點世界坐標系的設置無需嚴格按照實際的尺度(25mm),只需按照角點之間距離的比例(單位化)(相鄰角點的x,y坐標差均為1:1)
如果不信可以試試將角點的世界坐標賦予不同但比例一致的值,最終的求解結果都是完全一致的(len可以表示具體的尺度):
# world_coord[:,:2] = np.mgrid[0:w*len:len,0:h*len:len].T.reshape(-1,2)所以說,后續求得的相機內外參數也并不代表實際的尺度,它的單位是像素,如果要轉化為實際的尺度,還需要知道相機每個像素代表現實中的實際尺度大小。
2.3 參數求解
通過cv2.calibrateCamera求解相機內外參數
# 求解攝像機的內在參數和外在參數# ret 非0表示標定成功 mtx 內參數矩陣,dist 畸變系數,rvecs 旋轉向量,tvecs 平移向量# 注:求解的結果的單位為像素,若想化為度量單位還需乘上每個像素代表的實際尺寸(如:毫米/像素)ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(world, cam, (img.shape[1], img.shape[0]), None, None)當然,只基于一張圖像的標定結果并不可靠,因此,需要自定義一個函數進行批量操作:
# 求解內外參數 def CamCalibrate(w, h, num, root):# 圖像縮放比例(如果你的圖像進行了縮放,與實際拍攝的分辨率不一致,最終求得的參數需要乘上這個比例進行校正)ratio = 1 # 3648 / 1920 world, cam = [], []# 多張圖像進行標定,減小誤差:for i in range(num):img = cv2.imread(root + str(i+1)+'.jpg')# img,_,_ = utils.auto_reshape(img, 1920)# 定位角點is_success, cam_coord = find_chessboard_cor(img)print('第'+str(i+1)+'張角點提取完畢, 角點數 =',cam_coord.shape[0])# 可視化角點# draw_chessboard_cor(img, cam_coord, is_success)# 角點的世界坐標:# 注:相機參數的計算只要求角點之間的世界坐標比例一致,因此可以單位化world_coord = np.zeros((w * h, 3), np.float32)world_coord[:, :2] = np.mgrid[0:w, 0:h].T.reshape(-1, 2)world_coord[:,1] = -world_coord[:,1]# world_coord[:,:2] = np.mgrid[0:w*len:len,0:h*len:len].T.reshape(-1,2)# 將世界坐標與像素坐標加入待求解系數矩陣world.append(world_coord)cam.append(cam_coord)# 求解攝像機的內在參數和外在參數# ret 非0表示標定成功 mtx 內參數矩陣,dist 畸變系數,rvecs 旋轉向量,tvecs 平移向量# 注:求解的結果的單位為像素,若想化為度量單位還需乘上每個像素代表的實際尺寸(如:毫米/像素)ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(world, cam, (img.shape[1], img.shape[0]), None, None)rvecs = np.array(rvecs).reshape(-1,3)tvecs = np.array(tvecs).reshape(-1,3)# 單位:像素(1像素=??mm)print("標定結果 ret:", ret)print("內參矩陣 mtx:\n", mtx) # 內參數矩陣print("畸變系數 dist:\n", dist) # 畸變系數 distortion cofficients = (k_1,k_2,p_1,p_2,k_3)print("旋轉向量(外參) rvecs:\n", rvecs) # 旋轉向量 # 外參數(歐拉角)print("平移向量(外參) tvecs:\n", tvecs) # 平移向量 # 外參數np.save('./param/mtx.npy',mtx)np.save('./param/dist.npy',dist)np.save('./param/rvecs.npy',rvecs)np.save('./param/tvecs.npy',tvecs)np.save('./param/world.npy',np.array(world))np.save('./param/cam.npy',np.array(cam))return ret, mtx, dist, rvecs, tvecs最終結果:
標定結果 ret: 1.4146770040984205
內參矩陣 mtx:
[[2.88760072e+03 0.00000000e+00 1.82151647e+03]
[0.00000000e+00 2.88741538e+03 1.37233666e+03]
[0.00000000e+00 0.00000000e+00 1.00000000e+00]]
畸變系數 dist:
[[ 0.01488259 0.03388964 -0.00044845 -0.00178608 0.03024054]]
旋轉向量(外參) rvecs:
[[ 2.77705978 -0.46989843 0.1686412 ]
[-2.8145649 -0.91512141 -0.43737601]
… …
[ 2.67447733 0.66658815 -0.11254641]
[ 2.818184 -0.02291704 0.51413859]]
平移向量(外參) tvecs:
[[-5.06957615 -2.43006408 16.33928058]
[ 0.29829902 -4.17287918 13.05665933]
… …
[-3.8811355 -4.39040007 19.95916291]
[-4.53034049 -4.16521408 14.12011733]]
當然,單目相機標定的流程到這里就可以結束了
但是,評估求得的相機矩陣是否可靠也同樣重要。一個方法是計算重投影誤差,將角點的世界坐標通過已經求得的外參矩陣和相機內參矩陣重新轉化到像素坐標下,與原始提取的圖像角點坐標計算RMSE,得到的結果作為重投影誤差,誤差越小,結果越可靠。
2.4 參數評估(重投影誤差)
我們可以使用OpenCV內置的cv2.projectPoints()直接計算重投影誤差
當然,也可以自己實現:
# 計算重投影坐標 def CalcProjectPoints(world, rvecs, tvecs, mtx, dist):# 旋轉向量轉旋轉矩陣M = Rodriguez(rvecs)# c = Rw + t (世界坐標系轉相機坐標系)R_t = (M @ world.T).T + tvecs# (相機坐標系到圖像坐標系)# print(R_t)plain_pts = (mtx @ R_t.T)plain_pts = (plain_pts / plain_pts[2,:]).T[:,:2]# 去畸變c_xy = np.array([mtx[0,2], mtx[1,2]])f_xy = np.array([mtx[0,0], mtx[1,1]])k1, k2, p1, p2, k3 = dist[0]x_y = (plain_pts - c_xy) / f_xyr = np.sum(x_y * x_y, 1)x_distorted = x_y[:,0] * (1 + k1*r + k2*r*r + k3*r*r*r) + 2*p1*x_y[:,0]*x_y[:,1] + p2*(r + 2*x_y[:,0]*x_y[:,0])y_distorted = x_y[:,1] * (1 + k1*r + k2*r*r + k3*r*r*r) + 2*p2*x_y[:,0]*x_y[:,1] + p1*(r + 2*x_y[:,1]*x_y[:,1])u_distorted = f_xy[0]*x_distorted + c_xy[0]v_distorted = f_xy[1]*y_distorted + c_xy[1]plain_pts = np.array([u_distorted,v_distorted]).Treturn plain_pts這里面有很多細節需要了解,就比如OpenCV的旋轉向量轉旋轉矩陣基于Rodriguez變換(我之前一直以為是歐拉角,算出來的結果讓我迷了一段時間):
# 旋轉向量轉旋轉矩陣 def Rodriguez(rvecs):# 旋轉向量模長θ = (rvecs[0] * rvecs[0] + rvecs[1] * rvecs[1] + rvecs[2] * rvecs[2])**(1/2)# 旋轉向量的單位向量r = rvecs / θ# 旋轉向量單位向量的反對稱矩陣anti_r = np.array([[0, -r[2], r[1]],[r[2], 0, -r[0]],[-r[1], r[0], 0]])# 旋轉向量轉旋轉矩陣(Rodriguez公式) # np.outer(r, r) = r @ r.T 向量外積M = np.eye(3) * np.cos(θ) + (1 - np.cos(θ)) * np.outer(r, r) + np.sin(θ) * anti_rreturn M還有就是如何重投影時添加畸變,這些網上都可以查閱得到,這里不再贅述。
OpenCV同樣內置cv2.norm()求解RMSE,當然也可以自己實現:
# RMSEerror = original_pts - reprojec_ptserror = np.sum(error*error)**(1/2) / reproj.shape[0]我們也可以將重投影結果在圖像中可視化出來:
# 重投影可視化 def drawReprojCor(img, original_pts, reprojec_pts, idx):r,g = (0,0,255),(0,255,0)for i in range(original_pts.shape[0]):# 原始角點x0, y0 = int(round(original_pts[i,0])), int(round(original_pts[i,1]))cv2.circle(img, (x0, y0), 10, g, 2, lineType=cv2.LINE_AA)# 重投影角點x1, y1 = int(round(reprojec_pts[i,0])), int(round(reprojec_pts[i,1]))cv2.circle(img, (x1, y1), 10, r, 2, lineType=cv2.LINE_AA)cv2.imwrite('./reproject_result/'+ str(idx+1)+'.jpg', img)紅圈表示重投影點,綠圈表示原始提取的角點,來可視化兩張誤差比較大的:
對應序列第6張: RMSE=0.2501498893076572
對應序列第9張:RMSE=0.32640338317469253
同樣,封裝成批處理函數:
# 計算重投影誤差: def CalcReprojError(num, world, cam, mtx, dist, rvecs, tvecs):errors = []for i in range(num):# 計算重投影坐標(dist=0不考慮畸變)# reproj, _ = cv2.projectPoints(world[i], rvecs[i], tvecs[i], mtx, dist)reproj = CalcProjectPoints(world[i], rvecs[i], tvecs[i], mtx, dist)# 原始坐標original_pts = cam[i].reshape(cam[i].shape[0], 2)# 重投影坐標reprojec_pts = reproj.reshape(reproj.shape[0], 2)# RMSEerror = original_pts - reprojec_ptserror = np.sum(error*error)**(1/2) / reproj.shape[0]# 等價:# error = cv2.norm(cam[i],reproj, cv2.NORM_L2) / reproj.shape[0]errors.append(error)# 重投影可視化img = cv2.imread('./0/' + str(i+1)+'.jpg')# img,_,_ = utils.auto_reshape(img, 1920)drawReprojCor(img, original_pts, reprojec_pts, i)如果想讓可視化更直觀些,可以用條形圖統計每一張的誤差:
# 誤差條形圖# 可視化每張圖像的誤差(單位:像素)plt.bar(range(num), errors, width=0.8, label='reproject error', color='#87cefa')# 誤差平均值(單位:像素)mean_error = sum(errors) / numplt.plot([-1,num], [mean_error,mean_error], color='r', linestyle='--',label='overall RMSE:%.3f'%(mean_error))plt.xticks(range(num), range(1,num+1))plt.ylabel('RMSE Error in Pixels')plt.xlabel('Images')plt.legend()那這樣標定的完整流程應該就可以結束了吧
好吧,求解的參數中還有外參數沒用,為什么不來可視化看看相機位姿呢👀
2.5 相機位姿(棋盤位姿)可視化
可視化相機位姿的核心思路就是將相機坐標系下的相機坐標通過外參矩陣(旋轉+平移)轉換到世界坐標系:
# 可視化標定過程中的相機位姿 def show_cam_pose(rvecs, tvecs):# 相機坐標系下基向量vec = np.array([[1,0,0],[0,1,0],[0,0,1]]) # 相機位姿模型cam = (2/3)*np.array([[ 1, 1, 2],[-1, 1, 2],[-1,-1, 2],[ 1,-1, 2],[ 1, 1, 2],])fig = plt.figure()ax = fig.add_subplot(111, projection='3d')# 繪制每一個角度拍攝的相機for i in range(rvecs.shape[0]): # 旋轉向量轉旋轉矩陣M = Rodriguez(rvecs[i,:])# 相機原點 w = R^(-1)(0 - t)x0,y0,z0 = M.T @ (-tvecs[i,:])c = ['r','g','b']# 隨機顏色hex = '0123456789abcdef'rand_c = '#'+''.join([hex[random.randint(0,15)] for _ in range(6)])# 繪制相機坐標系for j in range(3):# 相機位姿(相機坐標系轉世界坐標系)# w = R^(-1)(c - t)x1,y1,z1 = M.T @ (vec[j,:] - tvecs[i,:])# 相機坐標系ax.plot([x0,x1],[y0,y1],[z0,z1],color=c[j])C = (M.T @ (cam - tvecs[i,:]).T).T # 繪制相機位姿for k in range(4):ax.plot([C[k,0],C[k+1,0]],[C[k,1],C[k+1,1]],[C[k,2],C[k+1,2]], color=rand_c)ax.plot([x0,C[k+1,0]],[y0,C[k+1,1]],[z0,C[k+1,2]], color=rand_c)# 相機編號ax.text(x0,y0,z0,i+1)# 繪制棋盤格for i in range(9):ax.plot([0, 11],[-i, -i],[0,0],color="black")for i in range(12):ax.plot([i, i],[-8, 0],[0,0],color="black")# 繪制世界(棋盤格)坐標系for i in range(3):ax.plot([0, 3*vec[i,0]],[0, -3*vec[i,1]],[0, 2*vec[i,2]],color=c[i],linewidth=3)plt.xlim(-3,14)plt.ylim(-13,4)值得注意的是,我們需要在自定義的標定函數中將世界坐標系的y軸改為負的,可視化結果才是正確的,否則可視化的結果是錯誤的(如下圖),原因其實在2.2節就已給出,大家可以想一想這是為什么
同樣的,可視化棋盤格位姿的核心思路就是將世界坐標系下的棋盤坐標通過外參矩陣(旋轉+平移)的逆變換轉換到相機坐標系:
# 可視化標定過程中的棋盤位姿 def show_chessboard_pose(rvecs, tvecs):# 棋盤坐標系下基向量vec = np.array([[1,0,0],[0,1,0],[0,0,1]]) # 相機位姿模型C = np.array([[ 1, 1, 3],[-1, 1, 3],[-1,-1, 3],[ 1,-1, 3],[ 1, 1, 3],])fig = plt.figure()ax = fig.add_subplot(111, projection='3d')c = ['r','g','b'] # 坐標軸顏色# 繪制每一個角度拍攝的棋盤格for i in range(rvecs.shape[0]): # 旋轉向量轉旋轉矩陣M = Rodriguez(rvecs[i,:])# 棋盤原點 c = Rw + tx0,y0,z0 = tvecs[i,:]# 隨機顏色hex = '0123456789abcdef'rand_c = '#'+''.join([hex[random.randint(0,15)] for _ in range(6)])# 繪制棋盤位姿for k in range(0,9):b = np.array([[0, -k, 0],[11, -k, 0]])b = (M @ b.T + tvecs[i,:].reshape(-1,1)).Tax.plot([b[0,0], b[1,0]],[b[0,1], b[1,1]],[b[0,2], b[1,2]],color=rand_c)for k in range(0,12):b = np.array([[k, -8, 0],[k, 0, 0]])b = (M @ b.T + tvecs[i,:].reshape(-1,1)).Tax.plot([b[0,0], b[1,0]],[b[0,1], b[1,1]],[b[0,2], b[1,2]],color=rand_c)k += 11# 繪制棋盤坐標系for j in range(3):# (世界坐標系轉相機坐標系)# c = Rw + tx1,y1,z1 = M @ vec[j,:] + tvecs[i,:]ax.plot([x0,x1],[y0,y1],[z0,z1],color=c[j])# 棋盤編號ax.text(x0,y0,z0,i+1)# 繪制世界(相機)坐標系for i in range(3):ax.plot([0, vec[i,0]],[0, -vec[i,1]],[0, 2*vec[i,2]],color=c[i],linewidth=2)# 繪制相機位姿for i in range(4):ax.plot([C[i,0],C[i+1,0]],[C[i,1],C[i+1,1]],[C[i,2],C[i+1,2]], color="black")ax.plot([0,C[i+1,0]],[0,C[i+1,1]],[0,C[i+1,2]], color="black")2.6 同Matlab標定結果比較
一般可以認為,Matlab內置算法的標定結果更為精確,因此,我們可以再通過Matlab標定得到一組參數,來驗證我們的參數是否靠譜:
基于Matlab的標定:
在標定過程中,程序自動舍棄了兩張誤差較大的圖像,正好就是序列第6張和第9張。
注意到Matlab的Reprojection Errors和我們自己標定的重投影誤差有些不太一樣,這是因為Matlab使用MSE,我們的代碼使用RMSE。
Matlab給出的標定內參矩陣(是轉置的形式),和:
我們的標定結果:
內參矩陣 mtx:
[[2.88760072e+03 0.00000000e+00 1.82151647e+03]
[0.00000000e+00 2.88741538e+03 1.37233666e+03]
[0.00000000e+00 0.00000000e+00 1.00000000e+00]]
是有一定的誤差但也不至于太離譜,至少可以認為結果是相對可靠的。
(完)
總結
以上是生活随笔為你收集整理的【计算机视觉】OpenCV实现单目相机标定的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 8 款浏览器兼容性测试工具,看你了解几个
- 下一篇: QGraphicsView图形视图框架使