攝像機標定
【相機模型】和【相機參數】相關內容看這裏:https://blog.csdn.net/qq_33278461/article/details/101026259
1.什麼是相機標定:
圖像測量和機器視覺裏,爲了能確定現實世界的任意一個點到圖像上對應像素點的投影位置,需要建立一個相機成像的幾何模型。
幾何模型的參數就是相機參數,包括內參、外參和畸變參數。
得到這個參數的過程就叫相機標定。
2.相機標定的目的
1.近似得到二維到三維的函數映射,可以用在深度估計,三維重建裏;
2.去畸變,就是去掉鏡頭裏的扭曲,一條直線投影到圖片上不能保持爲一條直線了,跟攝像機鏡頭有關;
3.相機標定的方法
1.傳統相機標定法:
利用尺寸已知的標定物,建立標定物上某些點與圖像上對應點之間的映射,通過算法近似得到相機模型的內外參數。
平面標定物,如標定版或者打印的紙需要拍攝兩張以上圖像標定;三維標定物單張圖像也可以標定;
主要分爲線性標定法、非線性優化標定法、兩步標定法(【張正友標定法】、Tsai經典兩步法)
2.主動視覺相機標定方法:
已知相機某些運動信息對相機自標定,無需標定物,但需要控制相機做某些特殊運動;
優點是算法簡單,往往能夠獲得線性解,故魯棒性較高;
缺點是系統的成本高、實驗設備昂貴、實驗條件要求高,而且不適合於運動參數未知或無法控制的場合。
3.相機自標定法:
利用相機運動約束,相機運動約束一般太強,實際中不準確;
利用場景約束,利用場景中的平行正交信息,其中空間平行線在相機圖像平面上的交點被稱爲消失點,這種方法是基於消失點的自標定方法;
自標定的好處是靈活,也可以在線自標定,由於它是基於絕對二次曲線或曲面的方法,其算法魯棒性差。
4.相機標定
4.1.像素座標系-圖像座標系-相機座標系-世界座標系轉換公式:
4.2.標定步驟:
世界座標系到相機座標系,得到相機外參(R,T矩陣),確定相機在現實世界的位置和朝向,是三維到三維的轉換;
相機座標系到像素座標系,得到相機內參K(焦距和主點位置),是三維到二維的轉換;
最後得到投影矩陣 P=K [ R | t ] 是一個3×4矩陣,K是內參,[ R | T ]是外參。
雙目標定還需要得到雙目之間的平移旋轉矩陣,RT矩陣。
一般實際中標定是這個流程:
1)分別單目標定左右相機,得到相機內參,外參RT,畸變矩陣;對應cv2.calibrateCamera();
2)用1)裏得到的矩陣雙目標定得到新的內參、畸變、雙目之間RT、基本矩陣E、基礎矩陣F;對應cv2.stereoCalibrate();
3)用2)中得到的矩陣進行矯正,每個攝像頭計算立體校正的映射矩陣,這裏不是矯正圖像,而是得出進行立體矯正所需要的映射矩陣;對應cv2.stereoRectify();
4)用3)得到的矩陣計算畸變矯正和立體校正的映射變換,對應cv2.initUndistortRectifyMap() ;【這個矩陣可以用來矯正以後所拍攝的圖像】
4.3.code
標定會出現很多問題,一般出現bug的地方有:
1).左右圖反了,或者不同相機需要旋轉一下,比如有的相機拍出的圖保存下來是旋轉270度等等,需要保證圖是正的;
2).標定板不平,打印的紙貼到箱子或者什麼東西上表面不同;
3).拍攝時候板子或相機動了,左右相機延遲導致不匹配;
4).拍攝的方向角度不夠多,一般我自己弄就選三個距離(或者選一個稍遠的距離)找11-20個角度拍一下,其實標定的時候會發現有些圖加進來效果變差會刪去(可能拍攝時動了),到最後其實留下個6-10組一般都會挺準的;
5).標定板尺寸寫錯了,比如8*10的橫線組成的其實是7*9的標定板,看的是網格內點;還可能是每個小格子大小寫錯了一般寫到幾毫米就行;
6).不好標就多拍個20組圖,然後多試幾種組合刪掉一些差得圖,看誤差retval一般要小於1;
以下代碼是我在項目裏用過的版本的初始版,簡單改了下,由於沒數據就找了兩組以前的圖跑了下能直接跑通,數據多是可以用的;
整體文件保存路徑爲:
left和right文件夾如下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Google style docstrings.
Example:
<scripts-name> --help
"""
import cv2
import numpy as np
import glob
import json
def calibrate(images, objpoints, cheese_size, show_img=False, fnames=[]):
# termination criteria
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
imgpoints = []
# debug
num = 0
for img in images:
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Find the chess board corners
# import pdb; pdb.set_trace()
# cv2.imshow('img', img)
# cv2.waitKey(0)
ret, corners = cv2.findChessboardCorners(gray, cheese_size, None)
# If found, add object points, image points (after refining them)
if ret == True:
cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), criteria)
# print(corners)
imgpoints.append(corners)
if show_img:
# Draw and display the corners
img = np.copy(img)
cv2.drawChessboardCorners(img, cheese_size, corners, ret)
cv2.imshow('img', img)
cv2.waitKey(0)
else:
print("Not find corner in img! in {}".format(fnames[num]))
num = num + 1
if show_img:
cv2.destroyAllWindows()
#print("gray shape:", gray.shape)
#print("gray shape-1:", gray.shape[::-1])
# input('回車下一步')
return cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None,
flags=cv2.CALIB_RATIONAL_MODEL), imgpoints
def drawLine(img, num=16):
h, w, *_ = img.shape
for i in range(0, h, h // num):
cv2.line(img, (0, i), (w, i), (0, 255, 0), 1, 8)
# for i in range(0, w, w // num):
# cv2.line(img, (i, 0), (i, h), (0, 255, 0), 1, 8)
return img
import os
def listdir(path, list_name):
for file in os.listdir(path):
file_path = os.path.join(path, file)
list_name.append(file_path)
_DIST_ = False
# fns = glob.glob('left_*.jpg')
# print("fns0:", fns)
fns =[]
left_path = '.\\left\\'
#bgr_path = '.\\nir\\'
listdir(left_path, fns)
print("fns:", fns)
cheese_size = (7, 9)
corner_num = cheese_size[0] * cheese_size[1]
unit = 23 # 實際的格子間距這裏是 23mm
objp = np.zeros((corner_num, 3), np.float32)
objp[:, :2] = np.mgrid[0:cheese_size[0], 0:cheese_size[1]].T.reshape(-1, 2)
objp *= unit
print("objp:", objp.shape)
stereo_right_images = []
stereo_left_images = []
objpoints = []
save = False
for fn in fns:
left_img = cv2.imread(fn)
stereo_left_images.append(left_img)
right_img = cv2.imread(fn.replace('left', 'right'))
stereo_right_images.append(right_img)
objpoints.append(objp)
print(len(stereo_left_images), len(stereo_right_images))
# 單目標定
x, stereo_right_corners = calibrate(
stereo_right_images, objpoints, cheese_size=cheese_size, show_img=True, fnames=fns)
stereo_right_ret, stereo_right_mtx, stereo_right_dist, stereo_right_rvecs, stereo_right_tvecs = x
# stereo_nir_dist = np.zeros_like(stereo_nir_dist)
print('right cali done...')
x, stereo_left_corners = calibrate(
stereo_left_images, objpoints, cheese_size=cheese_size, show_img=True, fnames=fns)
stereo_left_ret, stereo_left_mtx, stereo_left_dist, stereo_left_rvecs, stereo_left_tvecs = x
print('left cali done...')
if _DIST_:
stereo_right_dist = np.zeros_like(stereo_right_dist)
stereo_left_dist = np.zeros_like(stereo_left_dist)
# 雙目標定
retval, stereo_left_mtx, stereo_left_dist, stereo_right_mtx, stereo_right_dist, R_left2right, T_left2right, E_left2right, F_left2right= \
cv2.stereoCalibrate(np.array(objpoints), np.squeeze(np.array(stereo_left_corners)),
np.squeeze(np.array(stereo_right_corners)
), stereo_left_mtx, stereo_left_dist,
stereo_right_mtx, stereo_right_dist, (stereo_left_images[0].shape[0],
stereo_left_images[0].shape[1]),
flags=cv2.CALIB_RATIONAL_MODEL) # python3 transfer stereo_left -> stereo_right
h, w, c = stereo_right_images[0].shape
print('stereo cali done...')
if _DIST_:
stereo_right_dist = np.zeros_like(stereo_right_dist)
stereo_left_dist = np.zeros_like(stereo_rgb_dist)
# 雙目矯正
R1, R2, P1, P2, Q, validPixROI1, validPixROI2 = cv2.stereoRectify(
stereo_left_mtx, stereo_left_dist, stereo_right_mtx, stereo_right_dist, (w, h), R_left2right, T_left2right, alpha=0)
R1[0, :] *= -1
R2[0, :] *= -1
print('stereo rectify done...')
# 得到映射變換
stereo_left_mapx, stereo_left_mapy = cv2.initUndistortRectifyMap(
stereo_left_mtx, stereo_left_dist, R1, P1, (w, h), 5)
stereo_right_mapx, stereo_right_mapy = cv2.initUndistortRectifyMap(
stereo_right_mtx, stereo_right_dist, R2, P2, (w, h), 5)
print('initUndistortRectifyMap done...')
if save:
np.save('R1', R1)
np.save('R2', R2)
np.save('P1', P1)
np.save('P2', P2)
np.save('Q', Q)
np.save('stereo_right_mtx', stereo_right_mtx)
np.save('stereo_right_dist', stereo_right_dist)
np.save('stereo_left_mtx', stereo_left_mtx)
np.save('stereo_left_dist', stereo_left_dist)
np.save('R_left2right', R_left2right)
np.save('T_left2right', T_left2right)
np.save('stereo_left_mapx', stereo_left_mapx)
np.save('stereo_left_mapy', stereo_left_mapy)
np.save('stereo_right_mapx', stereo_righr_mapx)
np.save('stereo_right_mapy', stereo_right_mapy)
print('save parameters done...')
print('stereo_right_mtx', stereo_right_mtx)
print('stereo_right_dist', stereo_right_dist)
print('stereo_left_mtx', stereo_left_mtx)
print('stereo_left_dist', stereo_left_dist)
print('R_left2right', R_left2right)
print('T_left2right', T_left2right)
print('P2', P2) # 內參
# 可視化驗證,看網格是否對齊
for fn in fns:
left_img = cv2.imread(fn)
right_img = cv2.imread(fn.replace('left', 'right'))
frame0 = cv2.remap(right_img, stereo_right_mapx,
stereo_right_mapy, cv2.INTER_LINEAR)
frame1 = cv2.remap(left_img, stereo_left_mapx,
stereo_left_mapy, cv2.INTER_LINEAR)
img = np.concatenate((frame0, frame1), axis=1).copy()
img = drawLine(img, 32)
cv2.imshow('img', img)
ret = cv2.waitKey(0)
4.4.標定完有什麼用?
重建任務可能需要用到內外參;
常見的雙目匹配任務需要用到內參矩陣(代碼裏的P2),裏面保存了FB,用來轉換視差和深度,disp=FB/depth,也可以利用FB大小驗證標定結果,FB=焦距*雙目間距離,距離一般我們都知道大概多遠;
雙目匹配等任務會對圖像進行矯正後使用,用到了代碼裏的cv2.remap()、mapx矩陣和mapy矩陣;
4.5.opencv相關函數說明
這裏的翻譯參考自https://blog.csdn.net/qq_36537774/article/details/85005552。
- 4.5.1立體標定函數 stereoCalibrate() :
同時標定兩個攝像頭,計算出兩個攝像頭的自己的內外參數矩陣,還能求出兩個攝像頭之間的旋轉矩陣R,平移矩陣T。
double stereoCalibrate(InputArrayOfArrays objectPoints, InputArrayOfArrays imagePoints1,
InputArrayOfArrays imagePoints2, InputOutputArray cameraMatrix1,
InputOutputArray distCoeffs1, InputOutputArray cameraMatrix2,
InputOutputArray distCoeffs2, Size imageSize, OutputArray R,
OutputArray T, OutputArray E, OutputArray F, TermCriteria criteria=
TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 30, 1e-6), int
flags=CALIB_FIX_INTRINSIC )
1.objectPoints- vector<point3f> 型的數據結構,存儲標定角點在世界座標系中的位置;
2.imagePoints1- vector<vector<point2f>> 型的數據結構,存儲標定角點在第一個攝像機下的投影后的亞像素座標;
3.imagePoints2- vector<vector<point2f>> 型的數據結構,存儲標定角點在第二個攝像機下的投影后的亞像素座標;
4.cameraMatrix1-輸入/輸出型的第一個攝像機的相機矩陣。如果CV_CALIB_USE_INTRINSIC_GUESS , CV_CALIB_FIX_ASPECT_RATIO ,CV_CALIB_FIX_INTRINSIC , or CV_CALIB_FIX_FOCAL_LENGTH其中的一個或多個標誌被設置,該攝像機矩陣的一些或全部參數需要被初始化;
5.distCoeffs1-第一個攝像機的輸入/輸出型畸變向量。根據矯正模型的不同,輸出向量長度由標誌決定;
6.cameraMatrix2-輸入/輸出型的第二個攝像機的相機矩陣。參數意義同第一個相機矩陣相似;
7.distCoeffs2-第一個攝像機的輸入/輸出型畸變向量。根據矯正模型的不同,輸出向量長度由標誌決定;
8.imageSize-圖像的大小;
9.R-輸出型,第一和第二個攝像機之間的旋轉矩陣;
10.T-輸出型,第一和第二個攝像機之間的平移矩陣;
11.E-輸出型,基本矩陣;
12.F-輸出型,基礎矩陣;
13.term_crit-迭代優化的終止條件
14.flag-
CV_CALIB_FIX_INTRINSIC 如果該標誌被設置,那麼就會固定輸入的cameraMatrix和distCoeffs不變,只求解
$R,T,E,F$.
CV_CALIB_USE_INTRINSIC_GUESS 根據用戶提供的cameraMatrix和distCoeffs爲初始值開始迭代
CV_CALIB_FIX_PRINCIPAL_POINT 迭代過程中不會改變主點的位置
CV_CALIB_FIX_FOCAL_LENGTH 迭代過程中不會改變焦距
CV_CALIB_SAME_FOCAL_LENGTH 強制保持兩個攝像機的焦距相同
CV_CALIB_ZERO_TANGENT_DIST 切向畸變保持爲零
CV_CALIB_FIX_K1,...,CV_CALIB_FIX_K6 迭代過程中不改變相應的值。如果設置了 CV_CALIB_USE_INTRINSIC_GUESS 將會使用用戶提供的初始值,否則設置爲零
CV_CALIB_RATIONAL_MODEL 畸變模型的選擇,如果設置了該參數,將會使用更精確的畸變模型,distCoeffs的長度就會變成8
- 4.5.2立體校正函數 stereoRectify() :
爲每個攝像頭計算立體校正的映射矩陣,這裏不對圖片進行立體矯正,而是計算進行立體矯正所需要的映射矩陣。
void stereoRectify(InputArray cameraMatrix1, InputArray distCoeffs1,
InputArray cameraMatrix2,InputArray distCoeffs2, Size imageSize,
InputArray R, InputArray T,OutputArray R1, OutputArray R2, OutputArray P1,
OutputArray P2, OutputArray Q, int flags=CALIB_ZERO_DISPARITY, double alpha=-1,
Size newImageSize=Size(), Rect* validPixROI1=0, Rect* validPixROI2=0 )
1.cameraMatrix1-第一個攝像機的攝像機矩陣;
2.distCoeffs1-第一個攝像機的畸變向量;
3.cameraMatrix2-第二個攝像機的攝像機矩陣;
4.distCoeffs1-第二個攝像機的畸變向量;
5.imageSize-圖像大小;
6.R- stereoCalibrate() 求得的R矩陣;
7.T- stereoCalibrate() 求得的T矩陣;
8.R1-輸出矩陣,第一個攝像機的校正變換矩陣(旋轉變換);
9.R2-輸出矩陣,第二個攝像機的校正變換矩陣(旋轉矩陣);
10.P1-輸出矩陣,第一個攝像機在新座標系下的投影矩陣;
11.P2-輸出矩陣,第二個攝像機在想座標系下的投影矩陣;
12.Q-4*4的深度差異映射矩陣;
13.flags-可選的標誌有兩種零或者 CV_CALIB_ZERO_DISPARITY ,如果設置 CV_CALIB_ZERO_DISPARITY 的話,該函數會讓兩幅校正後的圖像的主點有相同的像素座標。否則該函數會水平或垂直的移動圖像,以使得其有用的範圍最大
14.alpha-拉伸參數。如果設置爲負或忽略,將不進行拉伸。如果設置爲0,那麼校正後圖像只有有效的部分會被顯示(沒有黑色的部分),如果設置爲1,那麼就會顯示整個圖像。設置爲0~1之間的某個值,其效果也居於兩者之間。
15.newImageSize-校正後的圖像分辨率,默認爲原分辨率大小。
16.validPixROI1-可選的輸出參數,Rect型數據。其內部的所有像素都有效
17.validPixROI2-可選的輸出參數,Rect型數據。其內部的所有像素都有效
- 4.5.3映射變換計算函數 initUndistortRectifyMap():
是計算畸變矯正和立體校正的映射變換,可以用map1和map2來矯正圖像。
void initUndistortRectifyMap(InputArray cameraMatrix, InputArray
distCoeffs, InputArray R,InputArray newCameraMatrix, Size size, int
m1type, OutputArray map1, OutputArray map2)
1.cameraMatrix-攝像機參數矩陣
2.distCoeffs-畸變參數矩陣
3.R- stereoCalibrate() 求得的R矩陣
4.newCameraMatrix-矯正後的攝像機矩陣(可省略)
5.Size-沒有矯正圖像的分辨率
6.m1type-第一個輸出映射的數據類型,可以爲 CV_32FC1 或 CV_16SC2
7.map1-輸出的第一個映射變換
8.map2-輸出的第二個映射變換
- 4.5.4幾何變換函數 remap():
利用映射矩陣對一張圖進行映射。
void remap(InputArray src, OutputArray dst, InputArray map1, InputArray
map2, int interpolation,int borderMode=BORDER_CONSTANT, const Scalar&
borderValue=Scalar())
1.src-原圖像
2.dst-幾何變換後的圖像
3.map1-第一個映射,無論是點(x,y)或者單純x的值都需要是CV_16SC2 ,CV_32FC1 , 或 CV_32FC2類型
4.map2-第二個映射,y需要是CV_16UC1 , CV_32FC1類型。或者當map1是點(x,y)時,map2爲空。
5.interpolation-插值方法,但是不支持最近鄰插值
參考:
百度百科: https://baike.baidu.com/item/%E7%9B%B8%E6%9C%BA%E6%A0%87%E5%AE%9A/6912991?fr=aladdin
相機模型:https://blog.csdn.net/qq_33278461/article/details/101026259
opencv函數:https://blog.csdn.net/qq_36537774/article/details/85005552
opencv函數說明:https://blog.csdn.net/jaccen2012/article/details/51217936
opencv文檔和各種參考內容。