相機標定筆記和python實現

攝像機標定

【相機模型】和【相機參數】相關內容看這裏:https://blog.csdn.net/qq_33278461/article/details/101026259

1.什麼是相機標定:

圖像測量和機器視覺裏,爲了能確定現實世界的任意一個點到圖像上對應像素點的投影位置,需要建立一個相機成像的幾何模型。
幾何模型的參數就是相機參數,包括內參、外參和畸變參數。
得到這個參數的過程就叫相機標定。

2.相機標定的目的

1.近似得到二維到三維的函數映射,可以用在深度估計,三維重建裏;
2.去畸變,就是去掉鏡頭裏的扭曲,一條直線投影到圖片上不能保持爲一條直線了,跟攝像機鏡頭有關;

3.相機標定的方法

1.傳統相機標定法:
利用尺寸已知的標定物,建立標定物上某些點與圖像上對應點之間的映射,通過算法近似得到相機模型的內外參數。
平面標定物,如標定版或者打印的紙需要拍攝兩張以上圖像標定;三維標定物單張圖像也可以標定;
主要分爲線性標定法、非線性優化標定法、兩步標定法(【張正友標定法】、Tsai經典兩步法)

2.主動視覺相機標定方法:
已知相機某些運動信息對相機自標定,無需標定物,但需要控制相機做某些特殊運動;
優點是算法簡單,往往能夠獲得線性解,故魯棒性較高;
缺點是系統的成本高、實驗設備昂貴、實驗條件要求高,而且不適合於運動參數未知或無法控制的場合。

3.相機自標定法:
利用相機運動約束,相機運動約束一般太強,實際中不準確;
利用場景約束,利用場景中的平行正交信息,其中空間平行線在相機圖像平面上的交點被稱爲消失點,這種方法是基於消失點的自標定方法;
自標定的好處是靈活,也可以在線自標定,由於它是基於絕對二次曲線或曲面的方法,其算法魯棒性差。

4.相機標定

4.1.像素座標系-圖像座標系-相機座標系-世界座標系轉換公式:

Zc[uv1]=[1dxγcx01dycy001][f000f0001][Rt0T1][XwYwZw1]=[fx0cx0fycy001][Rt0T1][XwYwZw1] Z_c\left[\begin{array}{c}{u } \\ {v} \\ {1}\end{array}\right] =\left[\begin{array}{ccc}{\frac{1}{dx}} & {\gamma} & {c_x} \\ {0} & {\frac{1}{dy}} & {c_y} \\ {0} & {0} & {1}\end{array}\right] \left[\begin{array}{ccc}{f} & {0} & {0} \\ {0} & {f} & {0} \\ {0} & {0} & {1}\end{array}\right] \left[\begin{array}{cc}{\boldsymbol{R}} & {\boldsymbol{t}} \\ {\boldsymbol{0}^{T}} & {1}\end{array}\right] \left[\begin{array}{c}{X_w } \\ {Y_w} \\ {Z_w} \\{1}\end{array}\right]=\left[\begin{array}{ccc}{f_x} & {0} & {c_x} \\ {0} & {f_y} & {c_y} \\ {0} & {0} & {1}\end{array}\right] \left[\begin{array}{cc}{\boldsymbol{R}} & {\boldsymbol{t}} \\ {\boldsymbol{0}^{T}} & {1}\end{array}\right] \left[\begin{array}{c}{X_w } \\ {Y_w} \\ {Z_w} \\{1}\end{array}\right]

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;

以下代碼是我在項目裏用過的版本的初始版,簡單改了下,由於沒數據就找了兩組以前的圖跑了下能直接跑通,數據多是可以用的;
整體文件保存路徑爲:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-wtnyOXXg-1587459692085)(_v_images/20200421160747869_4878.png)]
left和right文件夾如下:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-l04qLOzN-1587459692089)(_v_images/20200421160822795_6015.png)]

#!/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文檔和各種參考內容。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章