OpenCV-Python——第35章:摄像头标定

目录

1 基础

2 代码

2.0 获取棋盘图像

2.1 设置

2.2 标定

2.3 畸变校正

3 反向投影误差


1 基础

今天的低价单孔摄像机(照相机)会给图像带来很多畸变。畸变主要有两种:径向畸变和切想畸变。如下图所示,用红色直线将棋盘的两个边标注出来, 但是你会发现棋盘的边界并不和红线重合。所有我们认为应该是直线的也都凸出来了。

calib_radial.jpg

这种畸变可以通过下面的方程组进行纠正:

                                                              x_{distorted}=x(1+k_1r^2+k_2r^4+k_3r^6)

                                                              y_{distorted}=y(1+k_1r^2+k_2r^4+k_3r^6)

于此相似,另外一个畸变是切向畸变,这是由于透镜与成像平面不可能绝对平行造成的。这种畸变会造成图像中的某些点看上去的位置会比我们认为的位置要近一些。它可以通过下列方程组进行校正:

                                                           x_{\text {corrected}}=x+\left[2 p_{1} x y+p_{2}\left(r^{2}+2 x^{2}\right)\right]

                                                            y_{\text {corrected}}=y+\left[2 p_{1} x y+p_{2}\left(r^{2}+2 x^{2}\right)\right]

简单来说,如果我们想对畸变的图像进行校正就必须找到五个造成畸变的系数:

                                                        Distortion \, \, cofficients=\left(k_{1}, k_{2}, p_{1}, p_{2}, k_{3}\right)

除此之外,我们还需要再找到一些信息,比如摄像机的内部和外部参数。内部参数是摄像机特异的。它包括的信息有焦距(fx, fy),光学中心(cx, cy) 等。这也被称为摄像机矩阵。它完全取决于摄像机自身,只需要计算一次,以 后就可以已知使用了。可以用下面的 3x3 的矩阵表示:

                                                            camera\,\, matrix=\left[ \begin{array}{ccc}{f_{x}} & {0} & {c_{x}} \\ {0} & {f_{y}} & {c_{y}} \\ {0} & {0} & {1}\end{array}\right]

外部参数与旋转和变换向量相对应,它可以将 3D 点的坐标转换到坐标系统中。 在 3D 相关应用中,必须要先校正这些畸变。为了找到这些参数,我们必须要提供一些包含明显图案模式的样本图片(比如说棋盘)。我们可以在上面找到一些特殊点(如棋盘的四个角点)。我们起到这些特殊点在图片中的位置以及 它们的真是位置。有了这些信息,我们就可以使用数学方法求解畸变系数。这 就是整个故事的摘要了。为了得到更好的结果,我们至少需要 10 个这样的图案模式。

2 代码

如上所述,我们至少需要 10 图案模式来进行摄像机标定。为了便于理解,我们可以认为仅有一张棋盘图像。重要的是在进行摄 像机标定时我们要输入一组 3D 真实世界中的点以及与它们对应 2D 图像中的点。2D 图像的点可以在图像中很容易的找到。(这些点在图像中的位置是棋盘上两个黑色方块相互接触的地方)

那么真实世界中的 3D 的点呢?这些图像来源与静态摄像机和棋盘不同的摆放位置和朝向。所以我们需要知道(X,Y,Z)的值。但是为了简单,我 们可以说棋盘在 XY 平面是静止的,(所以 Z 总是等于 0)摄像机在围着棋 盘移动。这种假设让我们只需要知道 X,Y 的值就可以了。现在为了求 X, Y 的值,我们只需要传入这些点(0,0),(1,0),(2,0)...,它们代表了点的 位置。在这个例子中,我们的结果的单位就是棋盘(单个)方块的大小。但 是如果我们知道单个方块的大小(加入说 30mm),我们输入的值就可以是(0,0),(30,0),(60,0)...,结果的单位就是 mm。(在本例中我们不知道方 块的大小,因为不是我们拍的,所以只能用前一种方法了)。 3D 点被称为对象点,2D 图像点被称为图像点。

2.0 获取棋盘图像

下面的程序可以按设置的时间间隔保存摄像头的图像。

import cv2
import numpy as np

cap = cv2.VideoCapture(0)

if cap.isOpened() is True:
    i = 0
    while(True):
        ret, frame = cap.read()
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)  # 转换为灰色通道
        cv2.imshow('frame', gray)
        if i % 50 == 0:
            cv2.imwrite('E:\\Program\\Python\\OpenCV-test\\Chessboard\\' + str(i) + '.png', gray)
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
        i += 1
    cap.release()
    cv2.destroyAllWindows()

结果如下:

 其中一张:

2.1 设置

为了找到棋盘的图案,我们要使用函数cv2.findChessboardCorners()。我们还需要传入图案的类型,比如说 8x8 的格子或 5x5 的格子等。它会返 回角点,如果得到图像的话返回值类型(Retval)就会是 True。这些角点会 按顺序排列(从左到右,从上到下)。

retval, corners = cv2.findChessboardCorners(image, patternSize, corners, flags)

  • image:源棋盘图像。它必须是8位灰度或彩色图像
  • patternSize:每个棋盘行和列的内角数
  • corners:输出检测到的角点阵列
  • flags:类型
  •   CALIB_CB_ADAPTIVE_THRESH使用自适应阈值将图像转换为黑白,而不是固定的阈值水平(根据图像的平均亮度计算)。
  •   CALIB_CB_NORMALIZE_IMAGE 在应用固定阈值或自适应阈值之前,先用等化器对图像伽马进行归一化。
  •   CALIB_CB_FILTER_QUADS使用额外的标准(如轮廓面积、周长、方形形状)来过滤轮廓检索阶段提取的假四分位。
  •   CALIB_CB_FAST_CHECK对寻找棋盘角的图像运行快速检查,如果没有找到,则快捷调用。在退化条件下,当没有观察到棋盘时,这可以极大地加快调用。

其他:这个函数可能不会找出所有图像中应有的图案。所以一个好的方法是编写代码,启动摄像机并在每一帧中检查是否有应有的图案。在我们获得图案之后我们要找到角点并把它们保存成一个列表。在读取下一帧图像之前要设置一 定的间隔,这样我们就有足够的时间调整棋盘的方向。继续这个过程直到我们 得到足够多好的图案。就算是我们举得这个例子,在所有的 14 幅图像中也不 知道有几幅是好的。所以我们要读取每一张图像从其中找到好的能用的。
其他:除了使用棋盘之外,我们还可以使用环形格子,但是要使用函数 cv2.findCirclesGrid() 来找图案。据说使用环形格子只需要很少的图像 就可以了。

在找到这些角点之后我们可以使用函数 cv2.cornerSubPix() 增加准确度。我们使用函数 cv2.drawChessboardCorners() 绘制图案。

image = cv2.drawChessboardCorners(image, patternSize, corners, patternWasFound)

  • image:目标图像。它必须是8位灰度或彩色图像
  • patternSize:每个棋盘行和列的内角数
  • corners:输出检测到的角点阵列
  • patternWasFound:参数指示是否找到完整的板。 findChessboardCorners的返回值应该在这里传递。

所有的这些步骤都被包含在下面的代码中了:

 

import numpy as np
import cv2 as cv
import glob
# termination criteria
criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 30, 0.001)
# prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0)
objp = np.zeros((6*6, 3), np.float32)
objp[:, :2] = np.mgrid[0:6, 0:6].T.reshape(-1, 2)
# Arrays to store object points and image points from all the images.
objpoints = []  # 3d point in real world space
imgpoints = []  # 2d points in image plane.
images = glob.glob('E:\\Program\\Python\\OpenCV-test\\Chessboard\\*.png')
i = 0
for fname in images:
    img = cv.imread(fname)
    gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
    # Find the chess board corners
    ret, corners = cv.findChessboardCorners(gray, (6, 6), None)
    # If found, add object points, image points (after refining them)
    if ret:
        i += 1
        objpoints.append(objp)
        corners2 = cv.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
        imgpoints.append(corners)
        # Draw and display the corners
        cv.drawChessboardCorners(img, (6, 6), corners2, ret)
        cv.imshow('img', img)
        cv.imwrite('E:\\Program\\Python\\OpenCV-test\\\chessresult\\00'+str(i)+'.jpg', img)
        cv.waitKey(50)
cv.destroyAllWindows()

结果会保存在指定文件夹下:

其中一张

2.2 标定

在得到了这些对象点和图像点之后,我们已经准备好来做摄像机标定了。我们要使用的函数是 cv2.calibrateCamera()。它会返回摄像机矩阵,畸变系数,旋转和变换向量等。

retval, cameraMatrix, distCoeffs, rvecs, tvecs  =  cv2.calibrateCamera(objectPoints, imagePoints, imageSize, cameraMatrix, distCoeffs, rvecs, tvecs, flags, criteria)

  • objectPoints:在新界面中,它是校准模式坐标空间中校准模式点的向量的向量(如std::vector>)。外部向量包含的元素与模式视图的数量相同。如果在每个视图中显示相同的校准模式,并且它是完全可见的,那么所有的向量都是相同的。不过,可以使用部分遮挡的模式,甚至在不同视图中使用不同的模式。那么,向量就不一样了。这些点是三维的,但是由于它们是在一个模式坐标系中,那么,如果钻机是平面的,那么将模型放在XY坐标平面上可能是有意义的,这样每个输入对象点的z坐标就是0。在旧的接口中,来自不同视图的对象点的所有向量被连接在一起。
  • imagePoints:新界面中它是一个矢量的矢量投影的校准模式点(如 std::vector<std::vector<cv::Vec2f>>)。size()和objectPoints.size()以及imagePoints[i].size()对于每个i必须等于objectPoints[i].size()。
  • imageSize:图像大小,只用于初始化固有的相机矩阵。
  • cameraMatrix:输出相机矩阵\left[ \begin{array}{ccc}{f_{x}} & {0} & {c_{x}} \\ {0} & {f_{y}} & {c_{y}} \\ {0} & {0} & {1}\end{array}\right]如果指定了CV_CALIB_USE_INTRINSIC_GUESS和/或CALIB_FIX_ASPECT_RATIO,则必须在调用函数之前初始化部分或全部fx、fy、cx和cy。
  • distCoeffs:输出失真系数向量(k_1,k_2,p_1,p_2[,k_3[,k_4,k_5,k_6[,s_1,s_2,s_3,s_4[,\tau_x,\tau_y,]]]])
  • rvecs:为每个模式视图估计旋转向量的输出向量(参见Rodrigues)(例如std::vector>)。即,每个k旋转矢量与相应的k翻译(见下一个输出参数描述)将校准模式从模型坐标空间(对象指定点)向世界坐标空间,也就是说,一个真正的位置校准模式在k模式视图(k = 0 . .1)。
  • tvecs:每个模式视图估计的平移向量的tvecs输出向量。
  • flags:
  • CALIB_USE_INTRINSIC_GUESS cameraMatrix包含有效的初始值fx、fy、cx、cy,这些值经过进一步优化。否则,(cx, cy)初始设置为图像中心(使用imageSize),并以最小二乘方式计算焦距。注意,如果已知内部参数,就不需要使用这个函数来估计外部参数。使用solvePnP代替。
  • CALIB_FIX_PRINCIPAL_POINT全局优化过程中不改变主点。当CALIB_USE_INTRINSIC_GUESS也被设置时,它会停留在中心或指定的不同位置。
  • CALIB_FIX_ASPECT_RATIO函数只将fy作为一个自由参数。fx/fy的比例与输入的camera amatrix相同。不设置CALIB_USE_INTRINSIC_GUESS时,忽略fx和fy的实际输入值,只计算它们的比值并进一步使用。
  • CALIB_ZERO_TANGENT_DIST切向畸变系数(p1,p2)设置为零并保持为零。
  • CALIB_FIX_K1,……,CALIB_FIX_K6在优化过程中不改变相应的径向畸变系数。如果设置了CALIB_USE_INTRINSIC_GUESS,则使用提供的distCoeffs矩阵的系数。否则,它被设置为0。
  • CALIB_RATIONAL_MODEL启用了系数k4、k5和k6。为了提供向后兼容性,应该显式指定这个额外的标志,使校准函数使用rational模型并返回8个系数。如果没有设置该标志,该函数只计算并返回5个失真系数。
  • CALIB_THIN_PRISM_MODEL启用系数s1、s2、s3和s4。为了提供向后兼容性,应该显式指定这个额外的标志,使校准函数使用瘦棱镜模型并返回12个系数。如果没有设置该标志,该函数只计算并返回5个失真系数。
  • CALIB_FIX_S1_S2_S3_S4优化过程中不改变薄棱镜的畸变系数。如果设置了CALIB_USE_INTRINSIC_GUESS,则使用提供的distCoeffs矩阵的系数。否则,它被设置为0。
  • CALIB_TILTED_MODEL启用系数tauX和tauY。为了提供向后兼容性,应该显式指定这个额外的标志,使校准函数使用倾斜的传感器模型并返回14个系数。如果没有设置该标志,该函数只计算并返回5个失真系数。
  • CALIB_FIX_TAUX_TAUY 优化过程中不改变倾斜传感器模型的系数。如果设置了CALIB_USE_INTRINSIC_GUESS,则使用提供的distCoeffs矩阵的系数。否则,它被设置为0。
  • criteria:迭代优化算法的终止标准。

例如:

ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)

可以使用 Numpy 提供写函数(np.savez,np.savetxt 等) 将摄像机矩阵和畸变系数保存以便以后使用。

例如:

np.savez('B.npz', mtx=mtx, dist=dist, rvecs=rvecs, tvecs=tvecs)

整个程序如下:

import numpy as np
import cv2
import glob
# termination criteria
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
# prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0)
objp = np.zeros((6*6, 3), np.float32)
objp[:, :2] = np.mgrid[0:6, 0:6].T.reshape(-1, 2)
# Arrays to store object points and image points from all the images.
objpoints = []  # 3d point in real world space
imgpoints = []  # 2d points in image plane.
images = glob.glob('E:\\Program\\Python\\OpenCV-test\\Chessboard\\*.png')
i = 0
for fname in images:
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # Find the chess board corners
    ret, corners = cv2.findChessboardCorners(gray, (6, 6), None)
    # If found, add object points, image points (after refining them)
    if ret:
        i += 1
        objpoints.append(objp)
        corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
        imgpoints.append(corners)
        # Draw and display the corners
        cv2.drawChessboardCorners(img, (6, 6), corners2, ret)
        cv2.imshow('img', img)
        cv2.imwrite('E:\\Program\\Python\\OpenCV-test\\\chessresult\\00'+str(i)+'.jpg', img)
        cv2.waitKey(50)
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)
np.savez('B.npz', mtx=mtx, dist=dist, rvecs=rvecs, tvecs=tvecs)
cv2.destroyAllWindows()

2.3 畸变校正

现在我们找到我们想要的东西了,我们可以找到一幅图像来对他进行校正了。OpenCV 提供了两种方法,我们都学习一下。不过在那之前我们可以使用 从函数 cv2.getOptimalNewCameraMatrix() 得到的自由缩放系数对摄 像机矩阵进行优化。如果缩放系数 alpha = 0,返回的非畸变图像会带有最少量 的不想要的像素。它甚至有可能在图像角点去除一些像素。如果 alpha = 1,所 有的像素都会被返回,还有一些黑图像。它还会返回一个 ROI 图像,我们可以 用来对结果进行裁剪。

img = cv2.imread('400.jpg')
h,  w = img.shape[:2]
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h))

2.3.1 使用 cv2.undistort()

这是最简单的方法。只需使用这个函数和上边得到 的 ROI 对结果进行裁剪。

# undistort
dst = cv2.undistort(img, mtx, dist, None, newcameramtx)
# crop the image
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]
cv2.imwrite('calibresult.png', dst)

2.3.2 使用 remapping

这比较难了。首先我们要找到从畸变图像到 非畸变图像的映射方程。再使用重映射方程。

# undistort
mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w,h), 5)
dst = cv2.remap(img, mapx, mapy, cv.INTER_LINEAR)
# crop the image
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]
cv2.imwrite('calibresult.png', dst)

3 反向投影误差

我们可以利用反向投影误差对我们找到的参数的准确性进行估计。得到的结果越接近 0 越好。有了内部参数,畸变参数和旋转变换矩阵,我们就可以使 用 cv2.projectPoints() 将对象点转换到图像点。然后就可以计算变换得到 图像与角点检测算法的绝对差了。然后我们计算所有标定图像的误差平均值。

mean_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, cv.NORM_L2)/len(imgpoints2)
    mean_error += error
print( "total error: {}".format(mean_error/len(objpoints)) )

 

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值