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

文章目录

  • 为什么要进行相机标定?
  • 1. 成像几何
    • 1.1 坐标系统
    • 1.2 坐标转换
      • 1.2.1 世界坐标系转换到相机坐标系
      • 1.1.3 相机坐标系到图像坐标系
      • 1.1.4 图像物理坐标系到像素坐标系
      • 1.1.5 世界坐标系到像素坐标系
    • 1.2 相机畸变
      • 1.2.1 径向畸变
      • 1.2.2 切向畸变
      • 1.2.3 径向畸变模型
      • 1.2.4 切向畸变模型
      • 1.2.5 畸变系数
  • 2. 相机标定
    • 2.1 标定参数
      • 2.1.1 相机内参
      • 2.1.2 相机外参
      • 2.1.3 畸变系数
    • 2.2 标定流程
    • 2.3 opencv 相机标定代码
      • 2.3.1 相机标定(内参,畸变系数,以及这些图像对应的外参)
      • 2.3.2 整图畸变校正
      • 2.3.3 2D点转3D点
        • (1) 获取相关参数
        • (2) 构造测试用2D点
        • (3) 测试点畸变校正
        • (4) 2D转3D
      • 2.3.4 3D点转2D点
    • 2.4 标定相关函数
      • 2.4.1 相机标定
      • 2.4.2 畸变校正
      • 2.4.3 外参求解
      • 2.4.4 2D-3D映射
  • 参考

为什么要进行相机标定?

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

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

相机标定原理与实战【python-opencv】_第1张图片

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

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

1. 成像几何

1.1 坐标系统

相机标定原理与实战【python-opencv】_第2张图片

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

1.2 坐标转换

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

相机标定原理与实战【python-opencv】_第3张图片

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

[ 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 XCYCZC=RXWYWZW+T

R ∈ R 3 × 3 R \in R^{3 \times3} RR3×3: 旋转矩阵;
T ∈ R 3 × 1 T \in R^{3 \times 1} TR3×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} XCYCZC1=[R0T1]XWYWZW1

这里相机标定原理与实战【python-opencv】_第4张图片就是相机的外参矩阵,一旦世界坐标系发生变化,外参矩阵也要随之变化。

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

相机标定原理与实战【python-opencv】_第5张图片

从相机坐标系到图像坐标系,属于透视投影关系,从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 \\ ΔABOCΔoCOCΔPBOCΔpCOC
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} oCAB=oOCAOC=pCPB=xXC=fZC=yYC

此处的 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=fZCXCy=fZCYC

其矩阵表示如下:

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} ZCxy1=f000f0001000XCYCZC1

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

图像物理坐标系与像素坐标系只是坐标原点位置不一致,且单位长度(mm和pixel)不一致,因此,只需要进行伸缩变换及平移变换。
相机标定原理与实战【python-opencv】_第6张图片

{ 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=dxx+u0v=dyy+v0
表达为矩阵形式如下:
[ 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} uv1=dx1000dy10u0v01xy1

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

在这里插入图片描述

根据以上四个坐标系之间的转换关系,在不考虑相机畸变的情况下,物体从世界坐标系投影到像素坐标系的过程如下:
相机标定原理与实战【python-opencv】_第7张图片

1.2 相机畸变

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

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

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

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

1.2.1 径向畸变

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

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

1.2.2 切向畸变

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

1.2.3 径向畸变模型

相机主光轴中心的畸变为0,沿着镜头半径方向向边缘移动,畸变越来越严重。畸变的数学模型可以用主点(principle point)周围的泰勒级数展开式的前几项进行描述,通常使用前两项,即k1和k2,对于畸变很大的镜头,如鱼眼镜头,可以增加使用第三项k3来进行描述,相机上某点根据其在径向方向上的分布位置,校正公式为:
相机标定原理与实战【python-opencv】_第10张图片
公式里 ( x 0 , y 0 ) (x_0, y_0) (x0,y0)是畸变点在相机上的原始位置, ( x , y ) (x, y) (x,y)是畸变较正后新的位置。

1.2.4 切向畸变模型

畸变模型可以用两个额外的参数p1和p2来描述:
相机标定原理与实战【python-opencv】_第11张图片

1.2.5 畸变系数

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

2. 相机标定

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,u0,v0,1/dx,1/dy

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

在OpenCV及Matlab标定工具箱中,标定直接得到的是内参矩阵相机标定原理与实战【python-opencv】_第12张图片。因此在标定过程是得不到物理焦距 f f f的 。

2.1.2 相机外参

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

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

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

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 k1,k2,k3径向畸变系数, p 1 , p 2 p_1,p_2 p1,p2是切向畸变系数。

在Opencv中他们被排列成一个5*1的矩阵,依次包含 [ k 1 、 k 2 、 p 1 、 p 2 、 k 3 ] [k_1、k_2、p_1、p_2、k_3] [k1k2p1p2k3]

2.2 标定流程

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

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

  • (1)首先从calib.io生成棋盘格pdf。棋盘格大小尽可能与拍摄主体大小接近,以确保最终获取的拍摄主体上某点映射得到的世界坐标尽可能准确。
    相机标定原理与实战【python-opencv】_第13张图片
  • (2) 打印棋盘格,把它贴在一个平面上,作为标定物,世界坐标系原点固定在棋盘格左上角点处,则棋盘格每个角点的XY世界坐标可以通过格子大小得到,Z坐标始终为0。注意,棋盘格的高度与待测物体保持在同一高度。
    相机标定原理与实战【python-opencv】_第14张图片
  • (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([])                 # Jupyter Notebook中直接运行时要加[], py文件则去掉


class CalibData:                             # 标定数据类
    def __init__(self):
        self.camera_mat = None               # 相机内参
        self.dist_coeff = None               # 畸变参数
        self.rvecs = None                    # 旋转向量
        self.tvecs = None                    # 平移向量
        self.map1 = None                     # 映射矩阵1
        self.map2 = None                     # 映射矩阵2
        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)

    # 多图的2D3D点对标定相机参数
    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()

    # 首图的2D3D点对进行标定初始化
    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)
        
    # 其余图的2D3D点对进行初始化标定结果的优化
    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     # FOCAL_SCALE < 1, 则畸变校正后的图会放大,否则图会缩小
        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 = []
    
    # 获取args参数,供外部调用修改参数
    @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)
    # img = calibrator.draw_corners(image)
    # plt.figure(figsize=(10, 10))
    # plt.imshow(img)
    # plt.show()

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】_第15张图片

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)  # 每个点都是(x, y)
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()

(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]]]

(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()  # 可以看出,世界坐标系中的box基本是个矩形
[[ 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】_第16张图片

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]]

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映射

  • cvProjectPoints2: 2D点映射到3D点
  • cvProjectPoints:3D点映射到2D点
  • 像素坐标转到世界坐标时相机坐标系中的Zc值求解

`

参考

  • 相机标定——张正友棋盘格标定法
  • 相机标定之张正友标定法数学原理详解(含python源码)
  • 相机标定(Camera calibration)原理、步骤
  • 【相机标定】四个坐标系之间的变换关系
  • python利用opencv进行相机标定(完全版)

你可能感兴趣的:(python,3D视觉,相机标定,棋盘格,opencv,python)