工业相机标定(张正友标定法)

article/2025/9/22 2:44:33

目录

相机标定的概念

a. 相机标定的定义

b. 相机标定的目的

相机标定的过程

a. 标定板选择

b. 标定板摆放及拍摄

c. 标定板角点提取

张正友标定法

a. 反解相机矩阵

b.反解畸变系数

使用Python进行相机标定

a. 安装OpenCV

b. 准备标定板图片

c. 利用OpenCV进行角点检测

d. 求解单应矩阵

e. 内外矩阵参数的计算

f. 畸变矫正系数求解

g. 相机标定程序的实现

h. 主函数求解

相机标定的误差

a. 误差来源

b. 误差分析

c. 误差修正

参考文章


相机标定的概念

a. 相机标定的定义

相机标定是指确定相机的内部参数和外部参数的过程。

  • 内部参数包括相机的焦距、像素尺寸、主点位置等;
  • 外部参数包括相机的位置、朝向等。

通过相机标定,可以将像素坐标与实际物理坐标建立对应关系,从而实现机器视觉应用,例如三维重建、姿态估计、目标跟踪等。相机标定的目的是获得准确的相机参数,从而保证图像处理算法的精度和鲁棒性。

b. 相机标定的目的

相机标定的主要目的是获得相机的内部参数和外部参数,建立像素坐标与实际物理坐标之间的对应关系,以实现机器视觉应用。具体来说,相机标定的目的包括以下几个方面:

  • 精确测量:通过相机标定可以精确地测量物体的尺寸、形状、位置和姿态等信息。

  • 三维重建:通过相机标定可以获得三维场景中物体的坐标信息,从而实现三维重建。

  • 目标跟踪:通过相机标定可以将像素坐标与实际物理坐标建立对应关系,实现目标在不同帧之间的跟踪。

  • 机器人导航:通过相机标定可以获取机器人相对于环境的位置和姿态信息,从而实现机器人的导航和控制。

  • 自动化生产:通过相机标定可以实现自动化生产线中的物体检测、计量和分拣等功能。

总之,相机标定的目的是为了准确地获取相机内部参数和外部参数,从而实现机器视觉应用,提高生产效率和产品质量。再直白点就是相机标定的目的是为了获得相机矩阵和畸变系数。

相机标定的过程

a. 标定板选择

在相机标定中,选择合适的标定板是非常重要的。

在尺寸方面,应该根据相机的视场角和最小像素大小来选择,通常标定板至少填满相机视场角的80%;在形状方面有矩形、原型、棋盘格等,这里我们采用的是棋盘格,它可以提供足够的标定点,标定位置也相对于比较容易确定。

b. 标定板摆放及拍摄

标定板的摆放和拍摄过程对相机标定的精度和可靠性有着很大的影响。

在摆放位置上,在相机的视场角内摆放,并要保证标定板的平整度和稳定性,避免标定板出现扭曲、弯曲等情况。同时,标定板应该与相机光轴垂直放置。在拍摄角度上,标定板应该以不同的角度拍摄,包括俯视、仰视、左右侧视等。同时,标定板拍摄时要避免出现阴影和反光等情况,保证标定板表面光滑和均匀。在拍摄数量上,通常情况下,至少需要拍摄10张不同角度和位置的标定板照片来进行相机标定。在拍摄条件上,标定板拍摄时要保证拍摄环境的稳定性,避免光线变化、物体移动等因素对标定结果产生影响。同时,拍摄时要保证相机的曝光、对焦等设置一致。

c. 标定板角点提取

这里我们采用的Python中OpenCV的方法,使用cv2.findChessboardCorners()函数来自动提取标定板的角点。

其技术路线如下所示:

  1. 图像预处理:在进行角点提取之前,通常需要对标定板的图像进行预处理,包括灰度化、平滑处理、二值化等。这些处理可以提高角点提取的准确度和稳定性。

  2. 标定板参数设置:在使用cv2.findChessboardCorners()函数时,需要设置标定板的参数,包括标定板大小、每行每列的角点数量等。这些参数需要根据实际情况进行设置,以确保角点提取的准确度和稳定性。

  3. 角点提取算法选择:OpenCV中提供了多种角点提取算法,包括Harris、Shi-Tomasi、FAST等。在进行相机标定时,通常选择适合于标定板的角点提取算法,以提高角点提取的准确度和稳定性。

  4. 角点坐标优化:cv2.findChessboardCorners()函数可以自动提取标定板的角点坐标,但是这些坐标可能存在一定的误差。为了提高标定精度,可以对角点坐标进行优化处理,例如使用cv2.cornerSubPix()函数对角点进行亚像素级别的优化。

所以,在进行标定板角点提取时需要注意预处理、参数设置、算法选择和坐标优化等问题,以确保角点提取的准确度和稳定性。


张正友标定法

张正友标定法是相机标定的经典方法,这里我们从数学的角度来推理它。

a. 反解相机矩阵

在不考虑畸变的情况下,对图像中标定板进行角点检测,得到角点像素坐标值[u \quad v \quad 1]^{T},根据棋盘格的大小,转换到世界坐标系下标定板角点的坐标。

[^{W}X\quad ^{W}Y\quad ^{W}Z\quad 1]^{T}

这样我们就可以得到这样的一个式子:

\left [ \begin {array}{ccc}\hat{u} \\ \hat{v}\\ 1 \end{array}\right ]^{T}=K[^{C}_{W}R \quad ^{C}P_{w_{0}}]\left [ \begin {array}{ccc}^{W}X \\ ^{W}Y\\ ^{W}Z\\1 \end{array}\right ]^{T}

但为了方便计算,我们常常会让标定板处于^{W}Z=0,简单点来说就是将标定板放置在X—Y平面中。

这个式子久简化为了:

\left [ \begin {array}{ccc}\hat{u} \\ \hat{v}\\ 1 \end{array}\right ]^{T}=K[r_{1}\quad r_{2} \quad p ]\left [ \begin {array}{ccc}^{W}X \\ ^{W}Y\\1 \end{array}\right ]^{T}

这里的r_{1}r_{2}是旋转矩阵^{C}_{W}R的前两列,设从标定板到成像平面的单应性矩阵为

H=\left [ \begin {array}{ccc} h_{11} \quad h_{12} \quad h_{13} \\ h_{21} \quad h_{22} \quad h_{23} \\ h_{31} \quad h_{32} \quad h_{33} \\ \end {array} \right ]= [h_{1} \quad h_{2} \quad h_{3} ]

H是一个非奇异3×3矩阵,并且含有比例因子。

基于已知的H反解内参数矩阵K,代入h_{i}=Kr_{i}(i=1,2)可知

\small \left\{\begin{matrix} h_{1}^{T}Qh_{2}=0\\ h_{1}^{T}Qh_{1}=h_{2}^{T}Qh_{2}=1 \end{matrix}\right.

其中对称矩阵\small Q=K^{-T}K^{-1},矩阵K包含5个元素,需要3组H方程可解出K的唯一封闭解,因此在标定时需要拍摄三组以上的图片。由K可计算出相应的外参矩阵。

h_{i}^{T}Qh_{j}=[ h_{1} \quad h_{2} \quad h_{3} ]\left [ \begin {array}{ccc} q_{1} \quad q_{2} \quad q_{3} \\ q_{2} \quad q_{4} \quad q_{5} \\ q_{3} \quad q_{4} \quad q_{5} \\ \end {array} \right ]= \left[ \begin {array}{ccc} h_{1} \\ h_{2} \\ h_{3} \end {array} \right ]=v_{ij}^{T}q

可以改写为

\left [ \begin {array}{ccc} v_{12}^{T} \\ v_{11}^{T} -v_{22}^{T} \ \end {array} \right ]q=0

其中矩阵V完全由已知的H导出,qQ的向量形式。由于一个H最多只能贡献两个线性方程,因此确定内参矩阵至少要3张标定板(工程上一般取10~20张为宜),这些方程共同构成V_{q}=0 ,使用单应性矩阵估计的归一化直接线性变换解算,解出来就是相机内参矩阵。

上述计算中,忽略了相机畸变的影响(当然这也是最开始给出的条件下),对内外参应用最小二乘法估计实际存在的径向畸变的畸变系数(忽略切向畸变),最后是通过极大似然估计法进行优化,得出较高精度的值。

b.反解畸变系数

(张正友标定法只考虑了径向畸变,没有考虑切向畸变)用求得的内参矩阵反解畸变参数。

设:

  • 无畸变像素坐标为[\hat{u} \quad \hat{v}]^{T}
  • 成像平面坐标为[\hat{x} \quad \hat{y}]^{T}
  • 畸变像素坐标为[u \quad v]^{T}
  • 成像平面坐标为[x \quad y]^{T}
  • 内参s=0

则有:

\begin{cases} u=f_{u}x+c_{u}\\ v=f_{v}y+c_{v} \end{cases}  \begin{cases} \hat{u}=f_{u}\hat{x}+c_{u}\\ \hat{v}=f_{v}\hat{y}+c_{v} \end{cases}

两式相减,并代入f_{u}x=u-c_{u}f_{v}y=v-c_{v}以及径向畸变公式

\begin{cases} \hat{x}=x(1+k_{1}r^{2}+k_{2}r^{4})\\ \hat{y}=y(1+k_{1}r^{2}+k_{2}r^{4}) \end{cases}

得出

\left [ \begin {array}{ccc} (u-c_{u})r^{2} \quad (u-c_{u})r^{4}\\ (v-c_{v})r^{2} \quad (v-c_{v})r^{4} \\ \end {array} \right ]\left [ \begin {array}{ccc} k_{1} \\ k_{2} \\ \end {array} \right ]=\left [ \begin {array}{ccc} \hat{u}-u \\ \hat{v}-v \\ \end {array} \right ]

但由于

\left [ \begin {array}{ccc}\hat{u} \\ \hat{v}\\ 1 \end{array}\right ]^{T}=K[^{C}_{W}R \quad ^{C}P_{w_{0}}]\left [ \begin {array}{ccc}^{W}X \\ ^{W}Y\\ ^{W}Z\\1 \end{array}\right ]^{T}

对于M幅标定板图片,每张图片取N个角点,则可构造非齐次线性方程。

D_{2MN\times 2}k=d_{2MN\times 1}

 其中k=[k_{1}\quad k_{2}]^{T}为畸变系数。最后通过最小二乘法进行回归

 k=(D^{T}D)^{-1}D^{T}d

使用Python进行相机标定

a. 安装OpenCV

首先需要安装OpenCV库,可以使用pip进行安装:

pip install opencv-python
pip install opencv-contrib-python

b. 准备标定板图片

这一部分是针对没有购买标定板的情况,可以使用代码生成,然后打印拍照。

import cv2
import numpy as np# 定义棋盘格的尺寸
size = 140
# 定义标定板尺寸
boardx = size * 10
boardy = size * 7canvas = np.zeros((boardy, boardx, 1), np.uint8) # 创建画布
for i in range(0, boardx):for j in range(0, boardy):if (int(i/size) + int(j/size)) % 2 != 0: # 判定是否为奇数格canvas[j, i] = 255
cv2.imwrite("chessboard.png", canvas)

这里生成的图片大小为1400*980。如果你想要修改大小,使用ps进行修改是最简单的方法。

c. 利用OpenCV进行角点检测

在进行相机标定时,需要检测标定板上的角点,并将其用于后续的标定计算。OpenCV提供了一个函数  'findChessboardCorners'  来进行角点检测。

下面是一个使用OpenCV进行角点检测的示例代码:

import cv2
import numpy as nppic_name='chessboard.png'
img = cv2.imread(pic_name)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 设置棋盘格尺寸和数量
board_size = (6, 9)
# 找到棋盘格角点
_, corners = cv2.findChessboardCorners(gray, board_size, None)if _:# 提取亚像素角点criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)corners = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)# 在图像上绘制角点img = cv2.drawChessboardCorners(img, board_size, corners, _)cv2.imwrite('corners.png', img)
else:print(f'Unable to find corners in the {pic_name} image.')

这个是单独的一个检测的效果,但在实际拍摄的棋盘格照片中,效果不是很好,暂待改进。

d. 求解单应矩阵

下面的代码是参考了Eating Lee大佬的博客,这里我给出仔细地讲解与分析,尽量保证我是有在这篇博客中添加了自己的东西。

#homography.pyimport numpy as np
from scipy import optimize as opt# 求输入数据的归一化矩阵
def normalizing_input_data(coor_data):x_avg = np.mean(coor_data[:, 0])y_avg = np.mean(coor_data[:, 1])sx = np.sqrt(2) / np.std(coor_data[:, 0])sy = np.sqrt(2) / np.std(coor_data[:, 1])norm_matrix = np.matrix([[sx, 0, -sx * x_avg],[0, sy, -sy * y_avg],[0, 0, 1]])return norm_matrix# 求取初始估计的单应矩阵
def get_initial_H(pic_coor, real_coor):# 获得归一化矩阵pic_norm_mat = normalizing_input_data(pic_coor)real_norm_mat = normalizing_input_data(real_coor)M = []for i in range(len(pic_coor)):# 转换为齐次坐标single_pic_coor = np.array([pic_coor[i][0], pic_coor[i][1], 1])single_real_coor = np.array([real_coor[i][0], real_coor[i][1], 1])# 坐标归一化pic_norm = np.dot(pic_norm_mat, single_pic_coor)real_norm = np.dot(real_norm_mat, single_real_coor)# 构造M矩阵M.append(np.array([-real_norm.item(0), -real_norm.item(1), -1,0, 0, 0,pic_norm.item(0) * real_norm.item(0), pic_norm.item(0) * real_norm.item(1),pic_norm.item(0)]))M.append(np.array([0, 0, 0,-real_norm.item(0), -real_norm.item(1), -1,pic_norm.item(1) * real_norm.item(0), pic_norm.item(1) * real_norm.item(1),pic_norm.item(1)]))# 利用SVD求解M * h = 0中h的解U, S, VT = np.linalg.svd((np.array(M, dtype='float')).reshape((-1, 9)))# 最小的奇异值对应的奇异向量,S求出来按大小排列的,最后的最小H = VT[-1].reshape((3, 3))H = np.dot(np.dot(np.linalg.inv(pic_norm_mat), H), real_norm_mat)H /= H[-1, -1]return H# 返回估计坐标与真实坐标偏差
def value(H, pic_coor, real_coor):Y = np.array([])for i in range(len(real_coor)):single_real_coor = np.array([real_coor[i, 0], real_coor[i, 1], 1])U = np.dot(H.reshape(3, 3), single_real_coor)U /= U[-1]Y = np.append(Y, U[:2])Y_NEW = (pic_coor.reshape(-1) - Y)return Y_NEW# 返回对应jacobian矩阵
def jacobian(H, pic_coor, real_coor):J = []for i in range(len(real_coor)):sx = H[0] * real_coor[i][0] + H[1] * real_coor[i][1] + H[2]sy = H[3] * real_coor[i][0] + H[4] * real_coor[i][1] + H[5]w = H[6] * real_coor[i][0] + H[7] * real_coor[i][1] + H[8]w2 = w * wJ.append(np.array([real_coor[i][0] / w, real_coor[i][1] / w, 1 / w,0, 0, 0,-sx * real_coor[i][0] / w2, -sx * real_coor[i][1] / w2, -sx / w2]))J.append(np.array([0, 0, 0,real_coor[i][0] / w, real_coor[i][1] / w, 1 / w,-sy * real_coor[i][0] / w2, -sy * real_coor[i][1] / w2, -sy / w2]))return np.array(J)# 利用Levenberg Marquart算法微调H
def refine_H(pic_coor, real_coor, initial_H):initial_H = np.array(initial_H)final_H = opt.leastsq(value,initial_H,Dfun=jacobian,args=(pic_coor, real_coor))[0]final_H /= np.array(final_H[-1])return final_H# 返回微调后的H
def get_homography(pic_coor, real_coor):refined_homographies = []error = []for i in range(len(pic_coor)):initial_H = get_initial_H(pic_coor[i], real_coor[i])final_H = refine_H(pic_coor[i], real_coor[i], initial_H)refined_homographies.append(final_H)return np.array(refined_homographies)

实现的是图像配准中的单应性变换求解。

具体来说,实现步骤:

  1. 使用归一化矩阵对输入的图像和真实坐标数据进行归一化。
  2. 构造M矩阵,并通过SVD求解M * h = 0中h的解。
  3. 对估计的单应矩阵进行微调,得到最终的单应矩阵。

对于这一部分,可以结合着上面反解相机矩阵的公式理解。

e. 内外矩阵参数的计算

#InExparam.pyimport numpy as np# 返回每一幅图的外参矩阵[R|t]
def get_ex_param(H, intrinsics_param):ex_param = []inv_intrinsics_param = np.linalg.inv(intrinsics_param)for i in range(len(H)):h0 = (H[i].reshape(3, 3))[:, 0]h1 = (H[i].reshape(3, 3))[:, 1]h2 = (H[i].reshape(3, 3))[:, 2]scale_factor = 1 / np.linalg.norm(np.dot(inv_intrinsics_param, h0))r0 = scale_factor * np.dot(inv_intrinsics_param, h0)r1 = scale_factor * np.dot(inv_intrinsics_param, h1)t = scale_factor * np.dot(inv_intrinsics_param, h2)r2 = np.cross(r0, r1)R = np.array([r0, r1, r2, t]).transpose()ex_param.append(R)return ex_param# 返回pq位置对应的v向量
def create_v(p, q, H):H = H.reshape(3, 3)return np.array([H[0, p] * H[0, q],H[0, p] * H[1, q] + H[1, p] * H[0, q],H[1, p] * H[1, q],H[2, p] * H[0, q] + H[0, p] * H[2, q],H[2, p] * H[1, q] + H[1, p] * H[2, q],H[2, p] * H[2, q]])# 返回相机内参矩阵A
def get_in_param(H):# 构建V矩阵V = np.array([])for i in range(len(H)):V = np.append(V, np.array([create_v(0, 1, H[i]), create_v(0, 0, H[i]) - create_v(1, 1, H[i])]))# 求解V*b = 0中的bU, S, VT = np.linalg.svd((np.array(V, dtype='float')).reshape((-1, 6)))# 最小的奇异值对应的奇异向量,S求出来按大小排列的,最后的最小b = VT[-1]# 求取相机内参w = b[0] * b[2] * b[5] - b[1] * b[1] * b[5] - b[0] * b[4] * b[4] + 2 * b[1] * b[3] * b[4] - b[2] * b[3] * b[3]d = b[0] * b[2] - b[1] * b[1]alpha = np.sqrt(w / (d * b[0]))beta = np.sqrt(w / d ** 2 * b[0])gamma = np.sqrt(w / (d ** 2 * b[0])) * b[1]uc = (b[1] * b[4] - b[2] * b[3]) / dvc = (b[1] * b[3] - b[0] * b[4]) / dreturn np.array([[alpha, gamma, uc],[0, beta, vc],[0, 0, 1]])

get_ex_param实现了从单应矩阵H计算出每张图片的外参矩阵[R|t],其中R表示旋转矩阵,t表示平移向量。

具体实现流程如下:

  1. 首先对相机内参矩阵进行求逆,记为inv_intrinsics_param。

  2. 对于每张图片的单应矩阵H,提取出三个列向量h0、h1、h2。

  3. 计算缩放因子scale_factor,其计算方法为将inv_intrinsics_param与h0相乘的2范数的倒数。

  4. 根据缩放因子计算旋转矩阵R的三列r0、r1、r2,其中r0和r1分别为inv_intrinsics_param与h0、h1相乘并乘以缩放因子,r2通过向量叉积计算得到。

  5. 计算平移向量t,其计算方法为inv_intrinsics_param与h2相乘并乘以缩放因子。

  6. 将R和t拼接成外参矩阵[R|t],并将其添加到列表ex_param中。

  7. 对于所有的单应矩阵H都完成以上计算后,返回ex_param列表。

create_v给定一个单应性矩阵H和两个点p、q的下标,计算出这两个点在图像上对应的特征向量v。这里的特征向量v是由两个点在图像上的位置关系所决定的,它反映了两个点之间的几何信息,例如旋转、平移、尺度等。

具体来说,这段代码实现了以下步骤:

  1. 将H矩阵重构为3x3的形式;

  2. 根据p、q的下标计算出特征向量v,其中v是一个6维向量;

  3. 将v返回。

get_in_param实现了从多幅图像的单应性矩阵中估计相机的内部参数矩阵A,其中A是一个3x3的矩阵,包含相机的焦距、主点位置和相机的畸变参数等。

主要流程如下:

  1. 根据输入的单应矩阵 H ,构建 V 矩阵, V 的大小为 2n 行 6 列,其中 n 为输入单应矩阵 H 的个数。

  2. 对 V 进行奇异值分解,得到奇异值矩阵 S 和右奇异向量矩阵 VT 。

  3. 取 S 的最后一个奇异值对应的右奇异向量,即 VT 的最后一行,作为 Vb=0 的解 b 。

  4. 根据 b 计算相机内参矩阵 A 。

  5. 返回相机内参矩阵 A 。

f. 畸变矫正系数求解

#distortion.pyimport numpy as np# 返回畸变矫正系数k0,k1
def get_distortion(intrinsic_param, extrinsic_param, pic_coor, real_coor):D = []d = []for i in range(len(pic_coor)):for j in range(len(pic_coor[i])):# 转换为齐次坐标single_coor = np.array([(real_coor[i])[j, 0], (real_coor[i])[j, 1], 0, 1])# 利用现有内参及外参求出估计图像坐标u = np.dot(np.dot(intrinsic_param, extrinsic_param[i]), single_coor)[u_estim, v_estim] = [u[0] / u[2], u[1] / u[2]]coor_norm = np.dot(extrinsic_param[i], single_coor)coor_norm /= coor_norm[-1]# r = np.linalg.norm((real_coor[i])[j])r = np.linalg.norm(coor_norm)D.append(np.array([(u_estim - intrinsic_param[0, 2]) * r ** 2, (u_estim - intrinsic_param[0, 2]) * r ** 4]))D.append(np.array([(v_estim - intrinsic_param[1, 2]) * r ** 2, (v_estim - intrinsic_param[1, 2]) * r ** 4]))# 求出估计坐标与真实坐标的残差d.append(pic_coor[i][j, 0] - u_estim)d.append(pic_coor[i][j, 1] - v_estim)D = np.array(D)temp = np.dot(np.linalg.inv(np.dot(D.T, D)), D.T)k = np.dot(temp, d)return k

实现了摄像机的畸变矫正,返回畸变矫正系数k0和k1。

具体实现流程如下:

  1. 输入摄像机的内参矩阵(intrinsic_param),外参矩阵(extrinsic_param),真实的图像坐标(real_coor)和畸变图像坐标(pic_coor)。

  2. 对于每一个真实坐标(real_coor[i])[j],将其转换为齐次坐标并利用内参矩阵和外参矩阵求出估计图像坐标(u_estim, v_estim)。

  3. 根据真实坐标的归一化坐标(coor_norm)计算半径r,并利用估计图像坐标和内参矩阵计算畸变系数D,并将其存入D数组中。

  4. 计算估计坐标与真实坐标的残差,并将其存入d数组中。

  5. 将D数组和d数组用最小二乘法求出畸变矫正系数k0和k1,返回结果。

g. 相机标定程序的实现

#refine_all.pyimport numpy as np
import math
from scipy import optimize as opt# 微调所有参数
def refinall_all_param(A, k, W, real_coor, pic_coor):# 整合参数P_init = compose_paramter_vector(A, k, W)# 复制一份真实坐标X_double = np.zeros((2 * len(real_coor) * len(real_coor[0]), 3))Y = np.zeros((2 * len(real_coor) * len(real_coor[0])))M = len(real_coor)N = len(real_coor[0])for i in range(M):for j in range(N):X_double[(i * N + j) * 2] = (real_coor[i])[j]X_double[(i * N + j) * 2 + 1] = (real_coor[i])[j]Y[(i * N + j) * 2] = (pic_coor[i])[j, 0]Y[(i * N + j) * 2 + 1] = (pic_coor[i])[j, 1]# 微调所有参数P = opt.leastsq(value,P_init,args=(W, real_coor, pic_coor),Dfun=jacobian)[0]# raial_error表示利用标定后的参数计算得到的图像坐标与真实图像坐标点的平均像素距离error = value(P, W, real_coor, pic_coor)raial_error = [np.sqrt(error[2 * i] ** 2 + error[2 * i + 1] ** 2) for i in range(len(error) // 2)]print("total max error:\t", np.max(raial_error))# 返回拆解后参数,分别为内参矩阵,畸变矫正系数,每幅图对应外参矩阵return decompose_paramter_vector(P)# 把所有参数整合到一个数组内
def compose_paramter_vector(A, k, W):alpha = np.array([A[0, 0], A[1, 1], A[0, 1], A[0, 2], A[1, 2], k[0], k[1]])P = alphafor i in range(len(W)):R, t = (W[i])[:, :3], (W[i])[:, 3]# 旋转矩阵转换为一维向量形式zrou = to_rodrigues_vector(R)w = np.append(zrou, t)P = np.append(P, w)return P# 分解参数集合,得到对应的内参,外参,畸变矫正系数
def decompose_paramter_vector(P):[alpha, beta, gamma, uc, vc, k0, k1] = P[0:7]A = np.array([[alpha, gamma, uc],[0, beta, vc],[0, 0, 1]])k = np.array([k0, k1])W = []M = (len(P) - 7) // 6for i in range(M):m = 7 + 6 * izrou = P[m:m + 3]t = (P[m + 3:m + 6]).reshape(3, -1)# 将旋转矩阵一维向量形式还原为矩阵形式R = to_rotation_matrix(zrou)# 依次拼接每幅图的外参w = np.concatenate((R, t), axis=1)W.append(w)W = np.array(W)return A, k, W# 返回从真实世界坐标映射的图像坐标
def get_single_project_coor(A, W, k, coor):single_coor = np.array([coor[0], coor[1], coor[2], 1])# '''coor_norm = np.dot(W, single_coor)coor_norm /= coor_norm[-1]# r = np.linalg.norm(coor)r = np.linalg.norm(coor_norm)uv = np.dot(np.dot(A, W), single_coor)uv /= uv[-1]# 畸变u0 = uv[0]v0 = uv[1]uc = A[0, 2]vc = A[1, 2]# u = (uc * r**2 * k[0] + uc * r**4 * k[1] - u0) / (r**2 * k[0] + r**4 * k[1] - 1)# v = (vc * r**2 * k[0] + vc * r**4 * k[1] - v0) / (r**2 * k[0] + r**4 * k[1] - 1)u = u0 + (u0 - uc) * r ** 2 * k[0] + (u0 - uc) * r ** 4 * k[1]v = v0 + (v0 - vc) * r ** 2 * k[0] + (v0 - vc) * r ** 4 * k[1]'''uv = np.dot(W, single_coor)uv /= uv[-1]# 透镜矫正x0 = uv[0]y0 = uv[1]r = np.linalg.norm(np.array([x0, y0]))k0 = 0k1 = 0x = x0 * (1 + r ** 2 * k0 + r ** 4 * k1)y = y0 * (1 + r ** 2 * k0 + r ** 4 * k1)#u = A[0, 0] * x + A[0, 2]#v = A[1, 1] * y + A[1, 2][u, v, _] = np.dot(A, np.array([x, y, 1]))'''return np.array([u, v])# 返回所有点的真实世界坐标映射到的图像坐标与真实图像坐标的残差
def value(P, org_W, X, Y_real):M = (len(P) - 7) // 6N = len(X[0])A = np.array([[P[0], P[2], P[3]],[0, P[1], P[4]],[0, 0, 1]])Y = np.array([])for i in range(M):m = 7 + 6 * i# 取出当前图像对应的外参w = P[m:m + 6]# 不用旋转矩阵的变换是因为会有精度损失'''R = to_rotation_matrix(w[:3])t = w[3:].reshape(3, 1)W = np.concatenate((R, t), axis=1)'''W = org_W[i]# 计算每幅图的坐标残差for j in range(N):Y = np.append(Y, get_single_project_coor(A, W, np.array([P[5], P[6]]), (X[i])[j]))error_Y = np.array(Y_real).reshape(-1) - Yreturn error_Y# 计算对应jacobian矩阵
def jacobian(P, WW, X, Y_real):M = (len(P) - 7) // 6N = len(X[0])K = len(P)A = np.array([[P[0], P[2], P[3]],[0, P[1], P[4]],[0, 0, 1]])res = np.array([])for i in range(M):m = 7 + 6 * iw = P[m:m + 6]R = to_rotation_matrix(w[:3])t = w[3:].reshape(3, 1)W = np.concatenate((R, t), axis=1)for j in range(N):res = np.append(res, get_single_project_coor(A, W, np.array([P[5], P[6]]), (X[i])[j]))# 求得x, y方向对P[k]的偏导J = np.zeros((K, 2 * M * N))for k in range(K):J[k] = np.gradient(res, P[k])return J.T# 将旋转矩阵分解为一个向量并返回,Rodrigues旋转向量与矩阵的变换,最后计算坐标时并未用到,因为会有精度损失
def to_rodrigues_vector(R):p = 0.5 * np.array([[R[2, 1] - R[1, 2]],[R[0, 2] - R[2, 0]],[R[1, 0] - R[0, 1]]])c = 0.5 * (np.trace(R) - 1)if np.linalg.norm(p) == 0:if c == 1:zrou = np.array([0, 0, 0])elif c == -1:R_plus = R + np.eye(3, dtype='float')norm_array = np.array([np.linalg.norm(R_plus[:, 0]),np.linalg.norm(R_plus[:, 1]),np.linalg.norm(R_plus[:, 2])])v = R_plus[:, np.where(norm_array == max(norm_array))]u = v / np.linalg.norm(v)if u[0] < 0 or (u[0] == 0 and u[1] < 0) or (u[0] == u[1] and u[0] == 0 and u[2] < 0):u = -uzrou = math.pi * uelse:zrou = []else:u = p / np.linalg.norm(p)theata = math.atan2(np.linalg.norm(p), c)zrou = theata * ureturn zrou# 把旋转矩阵的一维向量形式还原为旋转矩阵并返回
def to_rotation_matrix(zrou):theta = np.linalg.norm(zrou)zrou_prime = zrou / thetaW = np.array([[0, -zrou_prime[2], zrou_prime[1]],[zrou_prime[2], 0, -zrou_prime[0]],[-zrou_prime[1], zrou_prime[0], 0]])R = np.eye(3, dtype='float') + W * math.sin(theta) + np.dot(W, W) * (1 - math.cos(theta))return R

标定相机的目的是确定相机内参(包括焦距、像素间距、主点坐标等参数)和畸变参数(包括径向畸变和切向畸变),以便将从相机拍摄得到的图像坐标转换为真实世界坐标。

具体实现过程为:根据给定的真实世界坐标和对应的图像坐标,使用最小二乘法,通过优化算法求解相机内参、畸变参数和每幅图像的外参,得到最优的相机标定参数。其中,相机内参和畸变参数是所有图像共享的,而每幅图像的外参是单独计算的。

该程序实现了以下几个函数:

  1. refinall_all_param:微调所有参数
  2. compose_paramter_vector:把所有参数整合到一个数组内
  3. decompose_paramter_vector:分解参数集合,得到对应的内参、外参和畸变矫正系数
  4. get_single_project_coor:根据相机内参、畸变参数和外参,返回从真实世界坐标映射的图像坐标

h. 主函数求解

import cv2
import numpy as np
import osfrom parameter.homography import get_homography
from parameter.InExparam import get_ex_param,get_in_param
from parameter.distortion import get_distortion
from parameter.refine_all import refinall_all_paramdef calibrate():# 求单应矩阵H = get_homography(pic_points, real_points_x_y)# 求内参intrinsics_param = get_in_param(H)# 求对应每幅图外参extrinsics_param = get_ex_param(H, intrinsics_param)# 畸变矫正k = get_distortion(intrinsics_param, extrinsics_param, pic_points, real_points_x_y)# 微调所有参数[new_intrinsics_param, new_k, new_extrinsics_param] = refinall_all_param(intrinsics_param,k, extrinsics_param, real_points,pic_points)print("intrinsics_parm:\t", new_intrinsics_param)print("distortionk:\t", new_k)print("extrinsics_parm:\t", new_extrinsics_param)if __name__ == "__main__":file_dir = r'E:\pythonconda2\demarcate\calibration_pic'# 标定所用图像pic_name = os.listdir(file_dir)# 由于棋盘为二维平面,设定世界坐标系在棋盘上,一个单位代表一个棋盘宽度,产生世界坐标系三维坐标cross_corners = [7, 4]  # 棋盘方块交界点排列real_coor = np.zeros((cross_corners[0] * cross_corners[1], 3), np.float32)real_coor[:, :2] = np.mgrid[0:7, 0:4].T.reshape(-1, 2)real_points = []real_points_x_y = []pic_points = []for pic in pic_name:pic_path = os.path.join(file_dir, pic)pic_data = cv2.imread(pic_path)# 寻找到棋盘角点succ, pic_coor = cv2.findChessboardCorners(pic_data, (cross_corners[0], cross_corners[1]), None)if succ:# 添加每幅图的对应3D-2D坐标pic_coor = pic_coor.reshape(-1, 2)pic_points.append(pic_coor)real_points.append(real_coor)real_points_x_y.append(real_coor[:, :2])calibrate()

通过标定一组摄像机的内部参数(包括焦距、主点等)和外部参数(包括相机在空间中的位置和方向等),使得摄像机拍摄的图像能够准确地反映出真实场景的几何信息。具体实现中,程序通过找到棋盘格的角点来建立图像和真实场景之间的对应关系,并使用多个图像中的对应点来计算摄像机的内部和外部参数,同时还进行了畸变校正和参数微调等处理。

具体的流程按如下进行:

  1. 定义标定所需的文件夹路径和棋盘格角点排列方式。
  2. 创建一个3D的世界坐标系,设定每个方格的大小,生成对应的坐标。
  3. 遍历标定所用的图片文件夹,读取每一幅图像。
  4. 对于每幅图像,通过OpenCV中的findChessboardCorners函数寻找棋盘格角点,并将其存储在 pic_points 数组中。
  5. 将世界坐标系的3D坐标存储在 real_points 数组中。
  6. 将世界坐标系的x和y坐标存储在 real_points_x_y 数组中。
  7. 调用函数 calibrate() 进行标定,该函数的实现流程如下:
  • 通过findChessboardCorners函数获取所有图片的角点坐标和对应的世界坐标系的3D坐标。
  • 使用get_homography函数获取所有图片的单应矩阵H。
  • 使用get_in_param函数求解内参。
  • 使用get_ex_param函数求解每幅图像的外参。
  • 使用get_distortion函数对图片进行畸变矫正。
  • 使用refinall_all_param函数对内参、畸变参数和外参进行微调。
  • 输出标定结果的内参、畸变参数和外参。

相机标定的误差

a. 误差来源

  • 棋盘标定板制作的精度不高,导致棋盘格子大小不规则,格点位置不准确等问题。
  • 相机成像的非线性畸变:相机成像过程中会受到光学因素、机械因素等影响,导致图像中像素的位置和实际位置之间存在误差。
  • 相机姿态的误差:相机标定需要拍摄多张图片来计算相机的内参和外参,而不同图片拍摄时相机姿态的差异也会导致误差的存在。
  • 拍摄过程中环境的变化:例如光照变化、相机移动、标定板移动等,都会对标定结果产生影响。

b. 误差分析

可以通过重投影误差和畸变校正后的误差进行分析。重投影误差是指将标定后的参数应用于新的图像上,计算出新图像中点的投影位置与实际点的位置之间的误差。

重投影误差越小,则标定结果越准确。

畸变校正后的误差是指在应用畸变校正后,计算新图像中点的投影位置与实际点的位置之间的误差。如果畸变校正后的误差比重投影误差更小,则说明畸变校正起到了一定的作用。

c. 误差修正

  • 改进标定板制作的精度,使得棋盘格子大小更规则,格点位置更准确。
  • 使用更高精度的相机,减小相机成像的非线性畸变。
  • 提高拍摄图片的质量,保证相机姿态的稳定性。
  • 进行多组标定,求平均值或者进行优化,提高标定精度。
  • 对于畸变问题,可以进行畸变校正,通过计算得到畸变矫正系数,从而对图像进行畸变矫正,减小畸变对标定结果的影响。

参考文章

这里推荐大家也看看,本人也是第一次学习,参考了各位大佬的文章进行学习,并自己查找资料进行撰写。

(9条消息) 利用Python-OpenCV及PS制作棋盘格标定板_opencv创建一个棋盘_None072的博客-CSDN博客(7条消息) 计算机视觉教程0-4:手推张正友标定法,详解图像去畸变(附代码)_张正友标定法代码_Mr.Winter`的博客-CSDN博客

(7条消息) (超详细)张正友标定法原理及公式推导_张正友标定法公式推理_jiayuzhang128的博客-CSDN博客最详细、最完整的相机标定讲解(6条消息) 张正友相机标定法原理与实现_张正友标定法_Eating Lee的博客-CSDN博客


http://chatgpt.dhexx.cn/article/N9ODsBdW.shtml

相关文章

详解机器人标定

相机固定不动, 上往下看引导机器人移动 机器人与视觉标定理论详解 相机固定不动, 上往下看引导机器人移动 1.相机非线性校正 使用标定板做非线性校正 2.相机与机器人做9点标定 可以使用机器人扎9个点&#xff0c;或者机器人抓住工件摆放9个位置&#xff0c;得到9个机械坐标…

标定系列二、9点标定以及5点圆心标定过程(代码详解)

一、九点标定过程 1.算法原理 9点标定就是通过9个点计算出相机坐标系到机械手坐标系下的一个仿射变换&#xff0c;&#xff08;实际上空间中的二维平面的仿射变换只需要3个点就足够了&#xff09;。在实际应用过程中&#xff0c;需要获取像素下特征点的坐标和对应机械手的坐标…

相机标定-张正友棋盘格标定法

目录 1.针孔相机模型 2.相机成像过程 2.1 各个坐标系之间的转换 2.1.1 图像坐标系到像素坐标系 2.1.2 相机坐标系到图像坐标系 2.1.3世界坐标系到相机坐标系 2.1.4世界坐标系到像素坐标系 3.畸变与畸变矫正 3.1 畸变 3.2 畸变公式 4.相机标定原理 5.张正友标定法介…

相机标定原理———标定原理介绍

声明&#xff1a;欢迎任何人和组织转载本blog中文章&#xff0c;但必须标记文章原始链接和作者信息。 本文链接&#xff1a;http://blog.csdn.net/li_007/archive/2010/10/30/5976261.aspx 开拓进取的小乌龟------->CSDN点滴点点滴滴Blog 由于在word中包含大量的公式和矩…

VisionMaster标定板标定

选择工具 标定板生成工具我比较喜欢用海康自己研发的标2定板 具体参数看自己需求 最后选择0 导出图像就行 一般不需要cad图纸 保存 去路径下打印看看你的按照路径 我的是D:\VisionMasterV4.2.0\VisionMaster4.2.0\Applications\Tools 找到这张图片 不要改变比例直接打印 …

9点标定方法

9点标定&#xff0c;旋转标定 1.9点标定2.旋转标定3.公式推导过程 1.9点标定 夹具夹取产品或者标定块&#xff0c;选取一个特征&#xff0c;开始进行标定 X轴、Y轴移动一个位置&#xff0c;记录轴的物理坐标&#xff1a;&#xff08;Qx1&#xff0c;Qy1&#xff09; 提取图像中…

相机标定(一)

相机标定 相机成像模型参考坐标系针孔模型畸变模型相机参数 相机成像模型 参考坐标系 通常畸变分为两种&#xff0c;径向畸变和切向畸变。 图像像素坐标系&#xff1a;表示场景中三维点在图像平面上的投影&#xff0c;其坐标原点在CCD图像平面的左上角&#xff0c;u轴平行于…

张正友相机标定Opencv实现以及标定流程标定结果评价图像矫正流程解析(附标定程序和棋盘图)

使用Opencv实现张正友法相机标定之前&#xff0c;有几个问题事先要确认一下&#xff0c;那就是相机为什么需要标定&#xff0c;标定需要的输入和输出分别是哪些&#xff1f; 相机标定的目的&#xff1a;获取摄像机的内参和外参矩阵&#xff08;同时也会得到每一幅标定图像的选择…

标定工具介绍

作者 | WenDao_Engineer 微信公众号 | 闻道工程师之家 在前面标定相关的系列文章文对标定的基本介绍、标定的实现过程以及标定所涉及的相关协议都进行了介绍&#xff0c;从今天开始我们介绍一下标定实现过程中的标定工具相关知识。 标定系统组成 我们已经知道通过CCP或者XCP…

单目相机标定实现--张正友标定法

文章目录 一&#xff1a;相机坐标系&#xff0c;像素平面坐标系&#xff0c;世界坐标系&#xff0c;归一化坐标系介绍1&#xff1a;概述公式 二:实现1&#xff1a;整体流程4&#xff1a;求出每张图像的单应性矩阵并用LMA优化5&#xff1a;求解理想无畸变情况下的摄像机的内参数…

摄像机标定和立体标定

尝试用OpenCV来实现立体视觉也有一段时间了&#xff0c;主要的参考资料就是Learning OpenCV十一、十二章和OpenCV论坛上一些前辈的讨论。过程中磕磕碰碰&#xff0c;走了不少弯路&#xff0c;终于在前不久解决了最头大的问题&#xff0c;把整个标定、校准、匹配的流程调试成功。…

相机标定——张正友棋盘格标定法

目录 为什么需要相机标定&#xff1f; 相机标定可以做什么&#xff1f; 相机标定后可以得到什么&#xff1f; 什么情况下需要借助相机标定的方法&#xff1f; 相机标定的原理 实现相机标定的方法 为什么需要相机标定&#xff1f; 一个是由于每个镜头的在生产和组装过程中的…

标定方法——张正友标定法

标定 标定是联系世界坐标与像素坐标的环节&#xff0c;目的是求出相机和投影仪的内外参数&#xff0c;对于3D成像来说至关重要 张正友标定法 通过各种方法的对比&#xff0c;为了方便&#xff0c;我们采用的是张正友标定。我们主要对张正友标定法的原理进行介绍&#xff0c;…

笔记总结-相机标定(Camera calibration)原理、步骤

这已经是我第三次找资料看关于相机标定的原理和步骤&#xff0c;以及如何用几何模型&#xff0c;我想十分有必要留下这些资料备以后使用。这属于笔记总结。 1.为什么要相机标定&#xff1f; 在图像测量过程以及机器视觉应用中&#xff0c;为确定空间物体表面某点的三维几何位置…

如何实现标定?

上一篇《什么是标定》对标定进行了初步的介绍&#xff0c;让大家有了一个感性的认识。标定是一项非常复杂的工作的&#xff0c;涉及方方面面的知识非常多&#xff0c;本文将对标定具体实现的过程进行介绍。 控制器对标定的支持 在前面的文章中介绍了控制算法是在软件编程的时候…

标定的分类(一)

关于标定的分类及说明(一) 现在工业机器视觉和计算机视觉大量应用标定算法&#xff0c;但是对于初学者来说&#xff0c;存在概念模糊&#xff0c;理论理解错误的现状&#xff0c;因此&#xff0c;需要对标定进行梳理&#xff0c;防止大家在学习过程中混淆各种标定概念。话不多…

什么是标定?

标定这两个字在汽车行业里的工程师基本都听过&#xff0c;但是在其他行业里大部分人都不知道什么是标定&#xff0c;甚至都没有听说过标定。什么是标定&#xff1f;举一个常见的例子&#xff0c;家里买了电视&#xff0c;连接网络就可以看节目了&#xff0c;与其他任何环境影响…

JMS及其API介绍

Java Message Service是java ee的规范之一&#xff0c;可以用来发送异步消息&#xff0c;在某些场景下&#xff0c;可以作为不同系统&#xff0c;或者不同模块之间的集成方式。 可以类比为通过数据库来集成的方式&#xff0c;模块A完成逻辑以后&#xff0c;往数据库插…

Springboot 整合 JMS

ActiveMQ JMS 仅支持 Java 平台。 由于 JMS 是一套标准&#xff0c;所以 SpringBoot 整合 JMS 必然是整合 JMS 的某一个实现。 Apache ActiveMQ 是一个开源的消息中间件&#xff0c;完全支持 JMS 1.1 规范&#xff0c;支持多种编程语言( C、C、C#、Delphi、Erlang、AdobeFla…

1.JMS规范介绍

目录 1.什么是JMS规范 2.什么是MOM 3.MOM的特点 4.JMS和MOM的关联 5.JMS的体系结构 6.JMS常见基本概念 7.JMS 的事务性会话和非事务性会话 8.JMS消息的可靠性机制 1.什么是JMS规范 Java 消息服务&#xff08;Java Message Service&#xff09;是 java 平台中关于面向消息…