相机标定原理与实战【python-opencv】

文章目录

为什么要进行相机标定?

相机的功能就是将真实的三维世界拍摄形成二维的图片。所以可以将相机成像的过程看做一个函数,输入是一个三维的场景,输出是二维的图片。但是,当我们想将二维的图片反映射成三维场景时,很明显,我们无法仅通过一张二维图来得到真实的三维场景。也就是说,上述的函数是不可逆的。

相机标定的目标是用一个具体的数学模型来模拟复杂的成像过程,并且求解出该数学模型中的一些参数,包括相机的内参,畸变系数和外参。这样我们便能够近似这个三维到二维的过程,进而找到这个函数的反函数,便可以从获取二维重建出三维。

相机标定原理与实战【python-opencv】

一旦标定得到这些参数,我们就得到了一个完整的相机成像的数学模型,其在在机器视觉、图像测量、摄影测量、三维重建等应用普遍应用。如

  • (1)对相机拍摄的图片进行畸变校正;
  • (2)确定空间物体表面某点的三维几何位置与其在图像中对应点之间的相互关系;
  • (3)确定物理尺寸和像素间的换算关系;
  • (4)用多个相机拍摄图片来进行三维重建;
  • (5)以及其他的计算机视觉的应用。

; 1. 成像几何

1.1 坐标系统

相机标定原理与实战【python-opencv】
  • 世界坐标系(O W − X W Y W Z W O_W-X_WY_WZ_W O W ​−X W ​Y W ​Z W ​):用户定义的三维世界的坐标系,为了描述目标物在真实世界里的位置而被引入。世界坐标系的原点位置可以根据需要调整。单位m m m;
  • 相机坐标系(O C − X C Y C Z C O_C-X_CY_CZ_C O C ​−X C ​Y C ​Z C ​):以摄像机光心为原点(在针孔模型中也就是针孔为光心),Z Z Z轴与光轴重合(与成像平面垂直),X 轴和Y 轴分别平行于图像物理坐标系(CCD)的X轴和 Y 轴,f f f为摄像机的焦距。单位m m m;
  • 图像物理坐标系(o − x y o-xy o −x y):用物理单位表示像素的位置,坐标原点为摄像机光轴与图像物理坐标系的交点位置。单位是m m mm m m。单位毫米的原因是此时由于相机内部的CCD传感器是很小的,比如8mm x 6mm。
  • 像素坐标系(u v uv u v):以像素为单位,坐标原点在左上角。举个例子,CCD传感上上面的8mm x 6mm,转换到像素大小是640×480,此时dx表示像素坐标系中每个像素的物理大小就是1/80mm,也就是说毫米与像素点的之间关系是piexl/mm.

; 1.2 坐标转换

1.2.1 世界坐标系转换到相机坐标系

相机标定原理与实战【python-opencv】

从世界坐标系变换到相机坐标系属于刚体变换,即物体不会发生形变,只需要进行旋转和平移。

[ X C Y C Z C ] = R [ X W Y W Z W ] + T \begin{bmatrix} X_C \ Y_C \ Z_C \end{bmatrix} = R \begin{bmatrix} X_W \ Y_W \ Z_W \end{bmatrix} + T ⎣⎡​X C ​Y C ​Z C ​​⎦⎤​=R ⎣⎡​X W ​Y W ​Z W ​​⎦⎤​+T

R ∈ R 3 × 3 R \in R^{3 \times3}R ∈R 3 ×3: 旋转矩阵;
T ∈ R 3 × 1 T \in R^{3 \times 1}T ∈R 3 ×1: 平移向量;

齐次表达:
[ X C Y C Z C 1 ] = [ R T 0 1 ] [ X W Y W Z W 1 ] \begin{bmatrix} X_C \ Y_C \ Z_C \ 1 \end{bmatrix} = \begin{bmatrix} R & T \ 0 & 1 \ \end{bmatrix} \begin{bmatrix} X_W \ Y_W \ Z_W \ 1 \end{bmatrix}⎣⎢⎢⎡​X C ​Y C ​Z C ​1 ​⎦⎥⎥⎤​=[R 0 ​T 1 ​]⎣⎢⎢⎡​X W ​Y W ​Z W ​1 ​⎦⎥⎥⎤​

相机标定原理与实战【python-opencv】

; 1.1.3 相机坐标系到图像坐标系

相机标定原理与实战【python-opencv】

从相机坐标系到图像坐标系,属于透视投影关系,从3D转换到2D。根据图中的相似三角形可以得出以下对应关系:
Δ A B O C ∼ Δ o C O C Δ P B O C ∼ Δ p C O C \Delta ABO_C \sim \Delta oCO_C \ \Delta PBO_C \sim \Delta pCO_C \ΔA B O C ​∼Δo C O C ​ΔP B O C ​∼Δp C O C ​
A B o C = A O C o O C = P B p C = X C x = Z C f = Y C y \frac{AB}{oC} = \frac{AO_C}{oO_C} = \frac{PB}{pC}=\frac{X_C}{x} = \frac{Z_C}{f} = \frac{Y_C}{y}o C A B ​=o O C ​A O C ​​=p C P B ​=x X C ​​=f Z C ​​=y Y C ​​

此处的 f f f对应相机焦距,最终,图像物理坐标系中的一点和相机坐标系中的对应点的转换关系便可以表示如下:

{ x = f X C Z C y = f Y C Z C \left{ \begin{gathered} x = f \frac{X_C}{Z_C} \ y = f \frac{Y_C}{Z_C} \end{gathered} \right.⎩⎪⎪⎨⎪⎪⎧​x =f Z C ​X C ​​y =f Z C ​Y C ​​​

其矩阵表示如下:

Z C [ x y 1 ] = [ f 0 0 0 0 f 0 0 0 0 1 0 ] [ X C Y C Z C 1 ] Z_C \begin{bmatrix} x \ y \ 1 \end{bmatrix} = \begin{bmatrix} f & 0 & 0 & 0 \ 0 & f & 0 & 0 \ 0 & 0 & 1 & 0 \end{bmatrix} \begin{bmatrix} X_C \ Y_C \ Z_C \ 1 \end{bmatrix}Z C ​⎣⎡​x y 1 ​⎦⎤​=⎣⎡​f 0 0 ​0 f 0 ​0 0 1 ​0 0 0 ​⎦⎤​⎣⎢⎢⎡​X C ​Y C ​Z C ​1 ​⎦⎥⎥⎤​

1.1.4 图像物理坐标系到像素坐标系

图像物理坐标系与像素坐标系只是坐标原点位置不一致,且单位长度(mm和pixel)不一致,因此,只需要进行伸缩变换及平移变换。

相机标定原理与实战【python-opencv】

{ u = x d x + u 0 v = y d y + v 0 \left{ \begin{gathered} u=\frac{x}{dx}+u_0 \ v=\frac{y}{dy} + v_0 \end{gathered} \right.⎩⎪⎨⎪⎧​u =d x x ​+u 0 ​v =d y y ​+v 0 ​​
表达为矩阵形式如下:
[ u v 1 ] = [ 1 d x 0 u 0 0 1 d y v 0 0 0 1 ] [ x y 1 ] \begin{bmatrix} u \ v \ 1 \end{bmatrix} = \begin{bmatrix} \frac{1}{dx} & 0 & u_0 \ 0 & \frac{1}{dy} & v_0 \ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \ y \ 1 \end{bmatrix}⎣⎡​u v 1 ​⎦⎤​=⎣⎡​d x 1 ​0 0 ​0 d y 1 ​0 ​u 0 ​v 0 ​1 ​⎦⎤​⎣⎡​x y 1 ​⎦⎤​

; 1.1.5 世界坐标系到像素坐标系

相机标定原理与实战【python-opencv】

根据以上四个坐标系之间的转换关系,在不考虑相机畸变的情况下,物体从世界坐标系投影到像素坐标系的过程如下:

相机标定原理与实战【python-opencv】

1.2 相机畸变

畸变(distortion)是对直线投影(rectilinear projection)的一种偏移。简单来说直线投影是场景内的一条直线投影到图片上也保持为一条直线。畸变简单来说就是一条直线投影到图片上不能保持为一条直线了,这是一种光学畸变(optical aberration),由于 镜头不规整和镜头与感光片不平行导致的

畸变一般可以分为: 径向畸变、切向畸变

  • 径向畸变来自于透镜形状;
  • 切向畸变来自于整个相机的组装过程;
    相机标定原理与实战【python-opencv】

d r dr d r: 径向畸变
d t dt d t: 切向畸变

; 1.2.1 径向畸变

径向畸变是由于镜头不规整造成的。图像径向畸变是图像像素点以畸变中心为中心点,沿着径向产生的位置偏差,从而导致图像中所成的像发生形变。径向畸变分为桶形畸变和枕形畸变。

  • 桶形畸变,对应于负径向位移, 多见于变焦镜头的广角端或者鱼眼镜头;
  • 枕形畸变,对应于正径向位移,多见于长焦镜头;
    相机标定原理与实战【python-opencv】

1.2.2 切向畸变

切向畸变是由于透镜本身与相机传感器平面(感光面)不平行而产生的,这种情况多是由于透镜被粘贴到镜头模组上的安装偏差导致。

1.2.3 径向畸变模型

相机主光轴中心的畸变为0,沿着镜头半径方向向边缘移动,畸变越来越严重。畸变的数学模型可以用主点(principle point)周围的泰勒级数展开式的前几项进行描述,通常使用前两项,即k1和k2,对于畸变很大的镜头,如鱼眼镜头,可以增加使用第三项k3来进行描述,相机上某点根据其在径向方向上的分布位置,校正公式为:

相机标定原理与实战【python-opencv】
公式里( x 0 , y 0 ) (x_0, y_0)(x 0 ​,y 0 ​)是畸变点在相机上的原始位置,( x , y ) (x, y)(x ,y )是畸变较正后新的位置。

; 1.2.4 切向畸变模型

畸变模型可以用两个额外的参数p1和p2来描述:

相机标定原理与实战【python-opencv】

1.2.5 畸变系数

  • k 1 , k 2 , k 3 k_1,k_2,k_3 k 1 ​,k 2 ​,k 3 ​ 径向畸变系数,p 1 , p 2 p_1,p_2 p 1 ​,p 2 ​ 是切向畸变系数。
  • 在Opencv中他们被排列成一个51的矩阵,依次包含[ k 1 、 k 2 、 p 1 、 p 2 、 k 3 ] [k_1、k_2、p_1、p_2、k_3][k 1 ​、k 2 ​、p 1 ​、p 2 ​、k 3 ​] 。

  • 相机标定

2.1 标定参数

2.1.1 相机内参

相机内参:f , u 0 , v 0 , 1 / d x , 1 / d y f, u_0, v_0, 1/dx, 1/dy f ,u 0 ​,v 0 ​,1 /d x ,1 /d y

f f f: 焦距
u 0 , v 0 u_0, v_0 u 0 ​,v 0 ​: 主点在像素坐标系下的偏移
d x , d y dx, dy d x ,d y: 表示x方向和y方向的一个像素分别占多少个单位

相机标定原理与实战【python-opencv】

2.1.2 相机外参

指相机相对于某个世界坐标系的方向(旋转和平移)。

相机的外参是6个;
R: 三个轴的旋转参数分别是( w 、 δ 、 θ ) (w、δ、θ)(w 、δ、θ),把每个轴的3×3旋转矩阵进行组合(矩阵之间相乘),得到集合三个轴旋转信息的R,其大小还是3×3;
T: 三个轴的平移参数( T x 、 T y 、 T z ) (Tx、Ty、Tz)(T x 、T y 、T z )。

opencv中标定得到直接是( w 、 δ 、 θ ) (w、δ、θ)(w 、δ、θ)和( T x 、 T y 、 T z ) (Tx、Ty、Tz)(T x 、T y 、T z ),进而通过下面的转换得到外参矩阵。

mR, _ = cv2.Rodrigues([w、δ、θ])
mT = [Tx、Ty、Tz]
exmat = np.concatenate([mR, mT], axis=1)
exmat = np.vstack([exmat, np.array([0, 0, 0, 1])])

2.1.3 畸变系数

k 1 , k 2 , k 3 k_1,k_2,k_3 k 1 ​,k 2 ​,k 3 ​ 径向畸变系数,p 1 , p 2 p_1,p_2 p 1 ​,p 2 ​ 是切向畸变系数。
在Opencv中他们被排列成一个5*1的矩阵,依次包含[ k 1 、 k 2 、 p 1 、 p 2 、 k 3 ] [k_1、k_2、p_1、p_2、k_3][k 1 ​、k 2 ​、p 1 ​、p 2 ​、k 3 ​] 。

2.2 标定流程

相机标定就是为了标定出上述的内参,外参和畸变系数。所以一旦相机结构固定,包括镜头结构固定,对焦距离固定,相机的内参就是固定的。但是一旦相机的位置发生移动,外参就会变化。因此,我们现在的任务就是 找出一些点的像素坐标和对应的世界坐标,来求解出内参,外参和畸变系数。

目前最常用的获取这些点对的方式是采用标定板,其标定流程如下:

  • (1)首先从calib.io生成棋盘格pdf。棋盘格大小尽可能与拍摄主体大小接近,以确保最终获取的拍摄主体上某点映射得到的世界坐标尽可能准确。
    相机标定原理与实战【python-opencv】
  • (2) 打印棋盘格,把它贴在一个平面上,作为标定物,世界坐标系原点固定在棋盘格左上角点处,则棋盘格每个角点的XY世界坐标可以通过格子大小得到,Z坐标始终为0。 注意,棋盘格的高度与待测物体保持在同一高度。
    相机标定原理与实战【python-opencv】
  • (3) 通过调整标定物或相机的方向,为标定物拍摄一些不同方向的照片。
  • (4) 利用 cv2.findChessboardCorners从照片中提取棋盘格角点的像素坐标。
  • (5) 利用 cv2.cornerSubPix进行角点坐标亚像素优化。
  • (6) 已知棋盘格角点的世界坐标和对应的像素坐标,利用 cv2.calibrateCamera求解相机内参,外参和畸变系数;

; 2.3 opencv 相机标定代码

https://github.com/dyfcalid/CameraCalibration
上述代码包含了普通相机和鱼眼相机的内参和外参标定代码。

import argparse
import cv2
import numpy as np
import os

parser = argparse.ArgumentParser(description="Camera Intrinsic Calibration")
parser.add_argument('-fw','--FRAME_WIDTH', default=1280, type=int, help='Camera Frame Width')
parser.add_argument('-fh','--FRAME_HEIGHT', default=1024, type=int, help='Camera Frame Height')
parser.add_argument('-bw','--BORAD_WIDTH', default=6, type=int, help='Chess Board Width (corners number)')
parser.add_argument('-bh','--BORAD_HEIGHT', default=7, type=int, help='Chess Board Height (corners number)')
parser.add_argument('-size','--SQUARE_SIZE', default=100, type=int, help='Chess Board Square Size (mm)')
parser.add_argument('-num','--CALIB_NUMBER', default=5, type=int, help='Least Required Calibration Frame Number')
parser.add_argument('-subpix','--SUBPIX_REGION', default=5, type=int, help='Corners Subpix Optimization Region')
parser.add_argument('-fs', '--FOCAL_SCALE', default=1, type=float, help='Camera Undistort Focal Scale')
parser.add_argument('-ss', '--SIZE_SCALE', default=1, type=float, help='Camera Undistort Size Scale')
args = parser.parse_args([])

class CalibData:
    def __init__(self):
        self.camera_mat = None
        self.dist_coeff = None
        self.rvecs = None
        self.tvecs = None
        self.map1 = None
        self.map2 = None
        self.reproj_err = None
        self.ok = False
        self.camera_mat_dst = None

class Normal:
    def __init__(self):
        self.data = CalibData()
        self.inited = False
        self.BOARD = np.array([ [(j * args.SQUARE_SIZE, i * args.SQUARE_SIZE, 0.)]
                               for i in range(args.BORAD_HEIGHT)
                               for j in range(args.BORAD_WIDTH) ],dtype=np.float32)

    def update(self, corners, frame_size):
        board = [self.BOARD] * len(corners)
        if not self.inited:
            self._update_init(board, corners, frame_size)
            self.inited = True
        else:
            self._update_refine(board, corners, frame_size)
        self._calc_reproj_err(corners)
        self._get_undistort_maps()

    def _update_init(self, board, corners, frame_size):
        data = self.data
        data.camera_mat = np.eye(3, 3)
        data.dist_coeff = np.zeros((5, 1))
        data.ok, data.camera_mat, data.dist_coeff, data.rvecs, data.tvecs = cv2.calibrateCamera(
            board, corners, frame_size, data.camera_mat, data.dist_coeff,
            criteria=(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 30, 1e-6))
        data.ok = data.ok and cv2.checkRange(data.camera_mat) and cv2.checkRange(data.dist_coeff)

    def _update_refine(self, board, corners, frame_size):
        data = self.data
        data.ok, data.camera_mat, data.dist_coeff, data.rvecs, data.tvecs = cv2.calibrateCamera(
            board, corners, frame_size, data.camera_mat, data.dist_coeff,
            flags = cv2.CALIB_USE_INTRINSIC_GUESS,
            criteria=(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_COUNT, 10, 1e-6))
        data.ok = data.ok and cv2.checkRange(data.camera_mat) and cv2.checkRange(data.dist_coeff)

    def _calc_reproj_err(self, corners):
        if not self.inited: return
        data = self.data
        data.reproj_err = []
        for i in range(len(corners)):
            corners_reproj, _ = cv2.projectPoints(self.BOARD, data.rvecs[i], data.tvecs[i], data.camera_mat, data.dist_coeff)
            err = cv2.norm(corners_reproj, corners[i], cv2.NORM_L2) / len(corners_reproj)
            data.reproj_err.append(err)

    def _get_camera_mat_dst(self, camera_mat):
        camera_mat_dst = camera_mat.copy()
        camera_mat_dst[0][0] *= args.FOCAL_SCALE
        camera_mat_dst[1][1] *= args.FOCAL_SCALE
        camera_mat_dst[0][2] = args.FRAME_WIDTH / 2 * args.SIZE_SCALE
        camera_mat_dst[1][2] = args.FRAME_HEIGHT / 2 * args.SIZE_SCALE
        return camera_mat_dst

    def _get_undistort_maps(self):
        data = self.data
        data.camera_mat_dst = self._get_camera_mat_dst(data.camera_mat)
        data.map1, data.map2 = cv2.initUndistortRectifyMap(
                                 data.camera_mat, data.dist_coeff, np.eye(3, 3), data.camera_mat_dst,
                                 (int(args.FRAME_WIDTH * args.SIZE_SCALE), int(args.FRAME_HEIGHT * args.SIZE_SCALE)), cv2.CV_16SC2)

class InCalibrator:
    def __init__(self):
        self.camera = Normal()
        self.corners = []

    @staticmethod
    def get_args():
        return args

    def get_corners(self, img):
        ok, corners = cv2.findChessboardCorners(img, (args.BORAD_WIDTH, args.BORAD_HEIGHT),
                      flags = cv2.CALIB_CB_ADAPTIVE_THRESH|cv2.CALIB_CB_NORMALIZE_IMAGE|cv2.CALIB_CB_FAST_CHECK)
        if ok:
            gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

            corners = cv2.cornerSubPix(gray, corners, (args.SUBPIX_REGION, args.SUBPIX_REGION), (-1, -1),
                                       (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.01))
        return ok, corners

    def draw_corners(self, img):
        ok, corners = self.get_corners(img)
        cv2.drawChessboardCorners(img, (args.BORAD_WIDTH, args.BORAD_HEIGHT), corners, ok)
        return img

    def undistort(self, img):
        data = self.camera.data
        return cv2.remap(img, data.map1, data.map2, cv2.INTER_LINEAR)

    def calibrate(self, img):
        if len(self.corners) >= args.CALIB_NUMBER:
            self.camera.update(self.corners, img.shape[1::-1])
        return self.camera.data

    def __call__(self, raw_frame):
        ok, corners = self.get_corners(raw_frame)
        result = self.camera.data
        if ok:
            self.corners.append(corners)
            result = self.calibrate(raw_frame)
        return result

2.3.1 相机标定(内参,畸变系数,以及这些图像对应的外参)

from glob import glob
from tqdm import tqdm
import matplotlib.pyplot as plt

calibrator = InCalibrator()
image_list = glob(os.path.join('/Users/nickccnie/Desktop/能力沉淀/10.代码库/CameraCalibration-master/IntrinsicCalibration/data', '*.jpg'))
for img_path in tqdm(image_list):
    image = cv2.imread(img_path)
    result = calibrator(image)

print('相机内参:')
print(calibrator.camera.data.camera_mat)
print('畸变系数:')
print(calibrator.camera.data.dist_coeff)
print('重投影误差:')
print(calibrator.camera.data.reproj_err)
相机内参:
[[429.01201174   0.         567.41969891]
 [  0.         419.76447848 467.46234827]
 [  0.           0.           1.        ]]
畸变系数:
[[-0.28372365]
 [ 0.06597315]
 [ 0.01174763]
 [ 0.00297211]
 [-0.0063206 ]]
重投影误差:
[0.27869147314646936, 0.4886522637692187, 0.17598982232576374, 0.48923469676289055, 0.514708116953903, 0.5678517808250879, 0.4997095543126862, 0.48520367988999785, 0.5357485071819145, 0.429865668423967]

2.3.2 整图畸变校正

img = cv2.imread(image_list[0])
undistort_img = calibrator.undistort(img)
plt.figure(figsize=(20, 20))
plt.subplot(121)
plt.imshow(img)
plt.subplot(122)
plt.imshow(undistort_img)
plt.show()

相机标定原理与实战【python-opencv】

2.3.3 2D点转3D点

(1) 获取相关参数

inmat = calibrator.camera.data.camera_mat
distCoeffs = calibrator.camera.data.dist_coeff
inmat_dst = calibrator.camera.data.camera_mat_dst

rvec = calibrator.camera.data.rvecs[0]
tvec = calibrator.camera.data.tvecs[0]
mR, _ = cv2.Rodrigues(rvec)
mT = tvec
exmat = np.concatenate([mR, mT], axis=1)
exmat = np.vstack([exmat, np.array([0, 0, 0, 1])])

print('第一张图对应的相机外参--旋转:')
print(calibrator.camera.data.rvecs[-1])
print('第一张图对应的相机外参--平移:')
print(calibrator.camera.data.tvecs[-1])
print('第一张图对应的相机外参:')
print(exmat)
第一张图对应的相机外参--旋转:
[[-1.23794909]
 [ 0.0842647 ]
 [ 0.01222559]]
第一张图对应的相机外参--平移:
[[-317.82200853]
 [ 355.12147718]
 [1161.07445248]]
第一张图对应的相机外参:
[[ 9.97197679e-01 -1.29569903e-02  7.36811127e-02 -1.54700207e+02]
 [-2.47796673e-02  8.72085682e-01  4.88725415e-01 -1.87299319e+02]
 [-7.05886538e-02 -4.89181643e-01  8.69320747e-01  1.05852036e+03]
 [ 0.00000000e+00  0.00000000e+00  0.00000000e+00  1.00000000e+00]]

(2) 构造测试用2D点

box_tl = [510, 398]
box_tr = [704, 394]
box_dl = [489, 548]
box_dr = [735, 541]

pts = np.array([box_tl, box_tr, box_dr, box_dl], np.int32)
pts = pts.reshape((-1,1,2))
draw_img = cv2.polylines(img.copy(),[pts],True,(0,255,0), 2)

plt.figure(figsize=(20, 20))
plt.imshow(draw_img)
plt.show()

相机标定原理与实战【python-opencv】

(3) 测试点畸变校正

undistort_pts = cv2.undistortPoints(pts.astype(np.float32), inmat, distCoeffs, P=inmat_dst)
print(undistort_pts)
draw_undistort_img = cv2.polylines(undistort_img.copy(),[undistort_pts.astype(np.int32)],True,(0,255,0), 2)
plt.figure(figsize=(20, 20))
plt.imshow(draw_undistort_img)
plt.show()
[[[581.46466 441.02515]]
 [[782.36176 434.583  ]]
 [[815.69165 588.1761 ]]
 [[560.1518  593.54535]]]

相机标定原理与实战【python-opencv】

(4) 2D转3D

def camera2world(point2D, rVec, tVec, cameraMat, height):
"""
       Function used to convert given 2D points back to real-world 3D points
       point2D  : An array of 2D points
       rVec     : Rotation vector
       tVec     : Translation vector
       cameraMat: Camera Matrix used in solvePnP
       height   : Height in real-world 3D space
       Return   : output_array: Output array of 3D points

"""
    point3D = []
    point2D = (np.array(point2D, dtype='float32')).reshape(-1, 2)
    numPts = point2D.shape[0]
    point2D_op = np.hstack((point2D, np.ones((numPts, 1))))
    rMat = cv2.Rodrigues(rVec)[0]
    rMat_inv = np.linalg.inv(rMat)
    kMat_inv = np.linalg.inv(cameraMat)
    for point in range(numPts):
        uvPoint = point2D_op[point, :].reshape(3, 1)
        tempMat = np.matmul(rMat_inv, kMat_inv)
        tempMat1 = np.matmul(tempMat, uvPoint)
        tempMat2 = np.matmul(rMat_inv, tVec)
        s = (height + tempMat2[2]) / tempMat1[2]
        p = tempMat1 * s - tempMat2
        point3D.append(p)

    point3D = (np.array(point3D, dtype='float32')).reshape([-1, 1, 3])
    return point3D
pts_world = camera2world(undistort_pts, rvec, tvec, inmat_dst, height=0).reshape(-1, 3)
print(pts_world)

x = pts_world[:, 0]
y = pts_world[:, 1]
plt.plot(x, y, 'bo')
plt.fill(x, y, 'r')
plt.show()
[[ 1.1295952e+01  1.1067908e+01 -1.1368684e-13]
 [ 4.9372278e+02  1.3734733e+01  0.0000000e+00]
 [ 4.9998770e+02  4.0107578e+02  0.0000000e+00]
 [-5.7722647e-02  4.0629202e+02  0.0000000e+00]]

相机标定原理与实战【python-opencv】

2.3.4 3D点转2D点

def world2camera(points):
        if points.size == 0:
            return np.empty((0, 3), dtype=np.int)

        points = np.concatenate([points, np.ones((points.shape[0], 1))], axis=1)

        points = np.dot(inmat_dst, np.dot(exmat[:-1], points.T)).T
        points = points / points[:, -1].reshape(-1, 1)
        points = points[:, :2]

        return points
point_camera_reprj = world2camera(pts_world)
print(point_camera_reprj)

draw_undistort_img = cv2.polylines(undistort_img.copy(),[point_camera_reprj[:, None, :].astype(np.int32)],True,(255,0,0), 2)
plt.figure(figsize=(20, 20))
plt.imshow(draw_undistort_img)
plt.show()
[[581.46466074 441.02514653]
 [782.36175752 434.58300772]
 [815.69165941 588.17609256]
 [560.15179407 593.54535244]]

相机标定原理与实战【python-opencv】

2.4 标定相关函数

2.4.1 相机标定

cv2.findChessboardCorners ( image,         # 棋盘图像
                            patternSize,   # 棋盘格行和列的【内角点】数量
                            corners,       # 输出数组
                            flags          # 操作标志
                            )
flags:
    CV_CALIB_CB_ADAPTIVE_THRESH            # 使用自适应阈值处理将图像转换为黑白图像
    CV_CALIB_CB_NORMALIZE_IMAGE            # 对图像进行归一化。
    CV_CALIB_CB_FILTER_QUADS               # 过滤在轮廓检索阶段提取的假四边形。
    CALIB_CB_FAST_CHECK                    # 对查找棋盘角的图像进行快速检查
cv2.cornerSubPix (image,                        # 棋盘图像
                  corners,                      # 棋盘角点
                  winSize,                      # 搜索窗口边长的一半
                  zeroZone,                     # 搜索区域死区大小的一半, (-1,-1)代表无
                  criteria                      # 迭代停止标准
                 )
cv2.calibrateCamera (objectPoints,         # 角点在棋盘中的空间坐标向量
                     imagePoints,          # 角点在图像中的坐标向量
                     image_size,           # 图片大小
                     K,                    # 相机内参矩阵
                     D,                    # 畸变参数向量
                     rvecs,                # 旋转向量
                     tvecs,                # 平移向量
                     flags,                # 操作标志
                     criteria              # 迭代优化算法的停止标准
                    )

flags:
    cv2.CALIB_USE_INTRINSIC_GUESS          # 当相机内参矩阵包含有效的fx,fy,cx,cy初始值时,这些值会进一步进行优化
                                           # 否则,(cx,cy)初始化设置为图像中心(使用imageSize),并且以最小二乘法计算焦距
    cv2.CALIB_FIX_PRINCIPAL_POINT          # 固定光轴点(当设置CALIB_USE_INTRINSIC_GUESS时可以使用)
    cv2.CALIB_FIX_ASPECT_RATIO             # 固定fx/fy的值,函数仅将fy视为自由参数
    cv2.CALIB_ZERO_TANGENT_DIST            # 切向畸变系数(p1,p2) 设置为零并保持为零
    cv2.CALIB_FIX_FOCAL_LENGTH             # 如果设置了CALIB_USE_INTRINSIC_GUESS,则在全局优化过程中不会更改焦距
    cv2.CALIB_FIX_K1 (K1-K6)               # 固定相应的径向畸变系数为0或给定的初始值
    cv2.CALIB_RATIONAL_MODEL               # 理想模型:启用系数k4,k5和k6。此时返回8个或更多的系数
    cv2.CALIB_THIN_PRISM_MODEL             # 薄棱镜模型:启用系数s1,s2,s3和s4。此时返回12个或更多的系数
    cv2.CALIB_FIX_S1_S2_S3_S4              # 固定薄棱镜畸变系数为0或给定的初始值
    cv2.CALIB_TILTED_MODEL                 # 倾斜模型:启用系数tauX和tauY。此时返回14个系数
    cv2.CALIB_FIX_TAUX_TAUY                # 固定倾斜传感器模型的系数为0或给定的初始值
  • drawChessboardCorners:绘制棋盘格检测结果
  • projectPoints: 计算重投影误差

2.4.2 畸变校正

OpenCV 针对不同的使用场景提供了几个不同用法的畸变校正函数。https://docs.opencv.org/3.4.6/da/d54/group__imgproc__transform.html#ga55c716492470bfe86b0ee9bf3a1f0f7e

主要有以下几种:

initUndistortRectifyMap() remap()组合
undistort()
undistortPoints()

(1) initUndistortRectifyMap() undistort()组合

  • 通过映射的方式逐个找出理想点在有畸变原图的位置。initUndistortRectifyMap()用于产生映射表,remap()用于执行映射。
  • 适用场景:当要进行多次畸变校正时,使用initUndistortRectifyMap() remap()组合比较有效率,只需要执行一次initUndistortRectifyMap(),后面畸变校正只需要执行remap()即可。

(2)undistort()

  • 本质是initUndistortRectifyMap() remap()组合,写在了一个函数里,方便调用。
  • 适用场景:当只需要执行一次畸变校正时,用undistort()比用组合形式更方便一些。

(3)undistortPoints()

  • 适用场景:当只需要找出有畸变原图中的少数几个点经过畸变校正后的理想位置时,使用undistortPoints()可达到目的。 *如对目标检测直接在有畸变图像进行box检测,进而将box左上角和右下角进行畸变校正,避免了对全图进行畸变校正(计算量大一点)后在进行目标检测。
cv2.initUndistortRectifyMap (K,         # 相机内参矩阵
                             D,         # 畸变向量
                             R,         # 旋转矩阵
                             P,         # 新的相机矩阵
                             size,      # 输出图像大小
                             m1type,    # 映射矩阵类型
                             map1,      # 输出映射矩阵1
                             map2       # 输出映射矩阵2
                            )
def remap(src,                 # 源图像数据
          map1,                # 用于插值的X坐标
          map2,                # 用于插值的Y坐标
          interpolation,       # 插值算法
          dst=None,
          borderMode=None,     # 边界模式,有默认值BORDER_CONSTANT,表示目标图像中“离群点(outliers)”的像素值不会被此函数修改。
          borderValue=None     # 当有常数边界时使用的值,其有默认值Scalar( ),即默认值为0。
          )
cv2.undistort(src,                      # 输入原图
              dst,                      # 输出矫正后的图像
              cameraMatrix,             # 内参矩阵
              distCoeffs,               # 畸变系数
              newCameraMatrix           # 默认情况下,它与 cameraMatrix 相同
              )
cv.undistortPoints(src,             # 待校正像素点坐标,1xN 或 Nx1 2channel
                   cameraMatrix,    # 内参矩阵
                   distCoeffs,      # 畸变系数
                   R,               # R参数是用在双目里的,单目里置为空矩阵;
                   P                # P矩阵值为空时,得到的点坐标是归一化坐标,这时候数值就会明显很小;
                                    # 通常使用时是想得到在同一个相机下的真实像素,所以P设置为内参就可以了
                   )

2.4.3 外参求解

  • cvFindExtrinsicCameraParams2: 已知内参求外参
  • solvePnP

2.4.4 2D-3D映射

`

参考

Original: https://blog.csdn.net/xijuezhu8128/article/details/122944975
Author: Nick Blog
Title: 相机标定原理与实战【python-opencv】

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/702347/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球