1、相机标定的原理
摄像机标定(Camera calibration)简单来说是从世界坐标系转换为相机坐标系,再由相机坐标系转换为图像坐标系的过程,也就是求最终的投影矩阵P的过程。

世界坐标系(world coordinate system):用户定义的三维世界的坐标系,为了描述目标物在真实世界里的位置而被引入。单位为m。

相机坐标系(camera coordinate system):在相机上建立的坐标系,为了从相机的角度描述物体位置而定义,作为沟通世界坐标系和图像/像素坐标系的中间一环。单位为m。

图像坐标系(image coordinate system):为了描述成像过程中物体从相机坐标系到图像坐标系的投影透射关系而引入,方便进一步得到像素坐标系下的坐标。 单位为m。

从世界坐标系到相机坐标系:
这一步是三维点到三维点的转换,包括R,t(相机外参)等参数;

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_相机标定

相机坐标系转换为图像坐标系:
这一步是三维点到二维点的转换,包括K(相机内参)等参数;

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_世界坐标系_02

 

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_世界坐标系_03

根据上述的关系图可以推导出下面的变换公式:

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_角点_04

 

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_角点_05

像主点的偏移:

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_相机标定_06

可以推出:

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_相机标定_07


内参矩阵K:

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_世界坐标系_08

外参矩阵[R丨t]:
表示三个方向的偏转:

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_世界坐标系_09

投影矩阵P
(在这里可以认为旋转矩阵 R 为单位矩阵 I,平移矩阵 t 都为0):

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_世界坐标系_10

根据上述变换最终可以得到一个投影矩阵P的公式为:

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_相机标定_11

总结一下公式大致如下:

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_角点_12

畸变参数
在几何光学和阴极射线管显示中,畸变是对直线投影的一种偏移。简单来说直线投影是场景内的一条直线投影到图片上也保持为一条直线。那畸变简单来说就是一条直线投影到图片上不能保持为一条直线了,这是一种光学畸变。畸变一般可以分为两大类,包括径向畸变和切向畸变。主要的一般径向畸变有时也会有轻微的切向畸变。

2、相机标定的实现
相机标定的目的:获取摄像机的内参和外参矩阵(同时也会得到每一幅标定图像的选择和平移矩阵),内参和外参系数可以对之后相机拍摄的图像就进行矫正,得到畸变相对很小的图像。

相机标定的输入:标定图像上所有内角点的图像坐标,标定板图像上所有内角点的空间三维坐标(一般情况下假定图像位于Z=0平面上)。

相机标定的输出:摄像机的内参、外参系数。

这三个基础的问题就决定了使用Opencv实现张正友法标定相机的标定流程、标定结果评价以及使用标定结果矫正原始图像的完整流程:

准备标定图片
对每一张标定图片,提取角点信息
对每一张标定图片,进一步提取亚像素角点信息
在棋盘标定图上绘制找到的内角点(非必须,仅为了显示)
相机标定
对标定结果进行评价
查看标定效果——利用标定结果对棋盘图进行矫正
我准备的是下图这种格子数为7×5,内角点为6×4的棋盘格图片,手机型号为HUAWEI Nova。

将它放在一个平面上从不同角度拍摄14张照片作为实验素材。
代码如下:

import cv2
import numpy as np
import glob

# 找棋盘格角点
# 阈值
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
#棋盘格模板规格
w = 6   #内角点个数,内角点是和其他格子连着的点
h = 4

# 世界坐标系中的棋盘格点,例如(0,0,0), (1,0,0), (2,0,0) ....,(8,5,0),去掉Z坐标,记为二维矩阵
objp = np.zeros((w*h,3), np.float32)
objp[:,:2] = np.mgrid[0:w,0:h].T.reshape(-1,2)
# 储存棋盘格角点的世界坐标和图像坐标对
objpoints = [] # 在世界坐标系中的三维点
imgpoints = [] # 在图像平面的二维点

images = glob.glob('picture/*.jpg')
for fname in images:
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
    # 找到棋盘格角点
    # 棋盘图像(8位灰度或彩色图像)  棋盘尺寸  存放角点的位置
    ret, corners = cv2.findChessboardCorners(gray, (w,h),None)
    # 如果找到足够点对,将其存储起来
    if ret == True:
        # 角点精确检测
        # 输入图像 角点初始坐标 搜索窗口为2*winsize+1 死区 求角点的迭代终止条件
        cv2.cornerSubPix(gray,corners,(11,11),(-1,-1),criteria)
        objpoints.append(objp)
        imgpoints.append(corners)
        # 将角点在图像上显示
        cv2.drawChessboardCorners(img, (w,h), corners, ret)
        cv2.imshow('findCorners',img)
        cv2.waitKey(1000)
cv2.destroyAllWindows()
#标定、去畸变
# 输入:世界坐标系里的位置 像素坐标 图像的像素尺寸大小 3*3矩阵,相机内参数矩阵 畸变矩阵
# 输出:标定结果 相机的内参数矩阵 畸变系数 旋转矩阵 平移向量
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)
# mtx:内参数矩阵
# dist:畸变系数
# rvecs:旋转向量 (外参数)
# tvecs :平移向量 (外参数)
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)    # 平移向量  # 外参数
# 去畸变
img2 = cv2.imread('picture/13.jpg')
h,w = img2.shape[:2]
# 我们已经得到了相机内参和畸变系数,在将图像去畸变之前,
# 我们还可以使用cv.getOptimalNewCameraMatrix()优化内参数和畸变系数,
# 通过设定自由自由比例因子alpha。当alpha设为0的时候,
# 将会返回一个剪裁过的将去畸变后不想要的像素去掉的内参数和畸变系数;
# 当alpha设为1的时候,将会返回一个包含额外黑色像素点的内参数和畸变系数,并返回一个ROI用于将其剪裁掉
newcameramtx, roi=cv2.getOptimalNewCameraMatrix(mtx,dist,(w,h),0,(w,h)) # 自由比例参数

dst = cv2.undistort(img2, mtx, dist, None, newcameramtx)
# 根据前面ROI区域裁剪图片
x,y,w,h = roi
dst = dst[y:y+h, x:x+w]
cv2.imwrite('calibresult.jpg',dst)
print ("dst:", dst.shape)
# 反投影误差
# 通过反投影误差,我们可以来评估结果的好坏。越接近0,说明结果越理想。
# 通过之前计算的内参数矩阵、畸变系数、旋转矩阵和平移向量,使用cv2.projectPoints()计算三维点到二维图像的投影,
# 然后计算反投影得到的点与图像上检测到的点的误差,最后计算一个对于所有标定图像的平均误差,这个值就是反投影误差。
total_error = 0
for i in range(len(objpoints)):
    imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
    error = cv2.norm(imgpoints[i],imgpoints2, cv2.NORM_L2)/len(imgpoints2)
    total_error += error
print (("total error: "), total_error/len(objpoints))

 

实验分为两步:

  1.对每张图片进行角点的提取

  2.实现相机的标定

运行结果如下:

mtx为内参数矩阵,dist为畸变系数:

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_角点_13

rvecs为旋转向量:

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_角点_14

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_角点_15

tvecs为平移向量:

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_角点_16

 

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_世界坐标系_17

畸变矫正:

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_角点_18

 反投影误差:

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_相机标定_19

矫正前后的图片

 

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_角点_20

 

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_相机标定_21

总结
通过标定得到HUAWEI Nova的后置摄像头的内置参数矩阵为:

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_角点_22

矫正后的黑白格和原图通过对比发现已经有点畸形,在边缘上有一点拉伸,反误差参数达到0.1,效果并不是很理想,应该是黑白格仍不够平坦的原因。

 

故借用另一组较为规范的图做了实验:

运行结果如下:

mtx为内参数矩阵,dist为畸变系数

 

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_角点_23

rvecs为旋转向量:

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_相机标定_24

 

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_角点_25

tvecs为平移向量:

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_角点_26

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_角点_27

畸变矫正:

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_相机标定_28

 反投影误差:

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_角点_29

矫正前后的图片

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_相机标定_30

 

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_世界坐标系_31

总结
通过标定得到该手机的后置摄像头的内置参数矩阵为:

单目相机 将相机坐标系 转换为 世界坐标系 python 相机标定坐标系_世界坐标系_32

 黑白格前后对比发现几乎一致,反误差参数0.03,可以说是效果较为理想。但边缘区域仍出现畸变,这应该是算法本身的缺陷。