《相机标定及python实现》
文章目录相机标定标定方法标定板开源标定OpenCV标定单目标定立体标定畸变校正手眼标定已知内参标定外参QA参考文献相机标定方法及实现相机标定标定方法传统标定方法自标定基于主动视觉标定标定板棋盘格rowNum = 9colNum = 9DPI = 96# dot per inchinch2cm = 2.54K= int(blockSize / inch2cm * DPI)img = np.zeros
相机标定方法及实现
相机标定
标定方法
- 传统标定方法
- 自标定
- 基于主动视觉标定
标定板
- 棋盘格
rowNum = 9
colNum = 9
DPI = 96 # dot per inch
inch2cm = 2.54
K= int(blockSize / inch2cm * DPI)
img = np.zeros((rowNum *K, colNum *K, 3), "uint8")
for i in range(rowNum):
for j in range(colNum):
if (i+j) % 2 != 0:
img[i*K:i*K+K, j*K:j*K+K] = 255
- 圆点阵列
开源标定
- OpenCV标定
- Halcon
- Matlab Calibration Toolbox标定工具箱
OpenCV标定
- 主要以张正友标定法来实现
- 获取相机内参 f x , f y , u 0 , v 0 , k 1 , k 2 , k 3 , p 1 , p 2 f_x, f_y, u_0, v_0, k_1, k_2, k_3, p_1, p_2 fx,fy,u0,v0,k1,k2,k3,p1,p2
- 得到相机外参,每张标定图片对应一组外参
单目标定
-
准备标定板(平整,尺寸已知)
-
不同角度拍摄一组照片(>=4张)
-
根据标定板,生成一组对应世界坐标
- 实际标定板行数和列数,设置行-1 列-1,识别内部的点,所以拍摄棋盘格时,最外的行列遮挡也可;
- 实际尺寸影响对相机的像素焦距、像主点、畸变系数、外参中的旋转矩阵没有影响,dx\dy改变,外参中的平移矩阵、双目标定中的基线有影响
-
(亚像素)角点提取
findChessboardCorners
角点检测cornerSubPix
亚像素角点检测
-
标定
-
calibrateCamera
calibrateCamera(objectPoints, imagePoints, imageSize, cameraMatrix, distCoeffs, rvecs=None, tvecs=None, flags=None, criteria=None) CV_CALIB_USE_INTRINSIC_GUESS:使用该参数时,在cameraMatrix矩阵中应该有fx,fy,u0,v0的估计值。否则的话,将初始化(u0,v0)图像的中心点,使用最小二乘估算出fx,fy。 CV_CALIB_FIX_PRINCIPAL_POINT:在进行优化时会固定光轴点。当CV_CALIB_USE_INTRINSIC_GUESS参数被设置,光轴点将保持在中心或者某个输入的值。 CV_CALIB_FIX_ASPECT_RATIO:固定fx/fy的比值,只将fy作为可变量,进行优化计算。当CV_CALIB_USE_INTRINSIC_GUESS没有被设置,fx和fy将会被忽略。只有fx/fy的比值在计算中会被用到。 CV_CALIB_ZERO_TANGENT_DIST:设定切向畸变参数(p1,p2)为零。 CV_CALIB_FIX_K1,…,CV_CALIB_FIX_K6:对应的径向畸变在优化中保持不变。 CV_CALIB_RATIONAL_MODEL:计算k4,k5,k6三个畸变参数。如果没有设置,则只计算其它5个畸变参数。
-
-
评价,重投影误差
projectPoints
-
内参优化
-
getOptimalNewCameraMatrix
alpha=1,视场不变,所有像素都保留,有黑色像素混入 alpha=0,尽可能裁剪不想要的像素,都是有效,这是个scale 建议采用alpha=1,保留黑边和ROI
-
class ZZY_Calib:
def __init__(self, rows, cols, length_chess=1):
self.rows = rows
self.cols = cols
self.length_chess = length_chess # 实际标定板棋盘格尺寸,影响到dx\dy,外参中的平移矩阵、双目标定中的基线,其余没有影响
self.criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) ### find corner iter stop
self.obj_pts = self.gen_world_coor(self.rows, self.cols)
def gen_world_coor(self, rows, cols):
obj_pts = np.zeros((rows * cols, 3), np.float32)
obj_pts[:, :2] = np.mgrid[0:rows, 0:cols].T.reshape(-1, 2)
return obj_pts
def show_chessboard_corner(self, img, corners, ret=1):
# 在棋盘上绘制角点,可视化工具
img = cv2.drawChessboardCorners(img,(self.rows, self.cols), corners, ret)
cv2.namedWindow('img', 0)
cv2.resizeWindow('img', 500, 500)
cv2.imshow('img',img)
cv2.waitKey(0)
cv2.destroyAllWindows()
def calib(self, path_img, path_campara, show_corner=False):
self.l_obj_pts=[]
self.l_img_pts=[]
obj_pts = self.gen_world_coor(self.rows, self.cols)
l_file = glob.glob(os.path.join(path_img, "*.jpg"))
print("Loading [%d] calib Images" % len(l_file))
for ind, file in enumerate(l_file):
print("calib [%d]:%s ..." % (ind, file))
img = cv2.imdecode(np.fromfile(file, dtype='uint8'), -1)
if len(img.shape) == 3:
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
else:
img_gray = img
ret, corners = cv2.findChessboardCorners(img_gray, (self.rows, self.cols), None)
if ret:
if show_corner:
self.show_chessboard_corner(img, corners, ret)
self.l_obj_pts.append(obj_pts)
corners2 = cv2.cornerSubPix(img_gray, corners, (11,11), (-1,-1), self.criteria) # 执行亚像素级角点检测
self.l_img_pts.append(corners2)
'''
传入所有图片各自角点的三维、二维坐标,相机标定。
每张图片都有自己的旋转和平移矩阵,但是相机内参和畸变系数只有一组
mtx,相机内参;dist,畸变系数;revcs,旋转矩阵;tvecs,平移矩阵。
'''
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(self.l_obj_pts, self.l_img_pts, img_gray.shape[::-1], None, None)
# Calibration Error
tot_error = 0
for i in range(len(self.l_obj_pts)):
imgpoints2, _ = cv2.projectPoints(self.l_obj_pts[i], rvecs[i], tvecs[i], mtx, dist)
error = cv2.norm(self.l_img_pts[i],imgpoints2, cv2.NORM_L2)/len(imgpoints2)
tot_error += error
print ("total error: ", tot_error/len(self.l_obj_pts))
# np.savez(self.path_campara, mtx=mtx, dist=dist)
print('ret', ret)
print('内参矩阵:', mtx)
print('畸变系数:', dist)
print('旋转矩阵:', rvecs)
print('平移矩阵:', tvecs)
'''
优化相机内参(camera matrix),可选,提高精度。
alpha= 1, 所有像素都保留,有黑色像素混入
alpha=0, 尽可能裁剪不想要的像素,都是有效,这是个scale
'''
alpha=1
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (img_gray.shape[1], img_gray.shape[0]), alpha, (img_gray.shape[1], img_gray.shape[0]))
print('优化后的相机内参:', newcameramtx)
print("ROI:", roi)
np.savez(path_campara, mtx=mtx, dist=dist, new_mtx=newcameramtx, roi=roi)
def read_campara(self, path):
# 读取相机内参数
with np.load(path) as X:
mtx, dist = [X[i] for i in ('mtx', 'dist')]
return mtx, dist
立体标定
-
获取两个相机的相对位姿关系:包括旋转和平移矩阵
-
分别标定左右相机
-
立体标定
-
stereoCalibrate
stereoCalibrate(objectPoints, imagePoints1, imagePoints2, cameraMatrix1, distCoeffs1, cameraMatrix2, distCoeffs2, imageSize, R=None, T=None, E=None, F=None, flags=None, criteria=None) objectPoints 标定角点在世界坐标系中的位置; imagePoints1 标定角点在第一个摄像机下的投影后的亚像素坐标; imagePoints2 标定角点在第二个摄像机下的投影后的亚像素坐标; cameraMatrix1 第一个摄像机的相机矩阵; distCoeffs1 第一个摄像机的输入/输出型畸变向量。根据矫正模型的不同,输出向量长度由标志决定; cameraMatrix2 第二个摄像机的相机矩阵。参数意义同第一个相机矩阵相似; distCoeffs2 第一个摄像机的输入/输出型畸变向量。根据矫正模型的不同,输出向量长度由标志决定; imageSize 图像的大小; R 第一和第二个摄像机之间的旋转矩阵; T 第一和第二个摄像机之间的平移矩阵; E 基本矩阵; F 基础矩阵; term_crit 迭代优化的终止条件
-
flags
CV_CALIB_FIX_INTRINSIC 如果该标志被设置,那么就会固定输入的cameraMatrix和distCoeffs不变,只求解 $R,T,E,F$. CV_CALIB_USE_INTRINSIC_GUESS 根据用户提供的cameraMatrix和distCoeffs为初始值开始迭代 CV_CALIB_FIX_PRINCIPAL_POINT 迭代过程中不会改变主点的位置 CV_CALIB_FIX_FOCAL_LENGTH 迭代过程中不会改变焦距 CV_CALIB_SAME_FOCAL_LENGTH 强制保持两个摄像机的焦距相同 CV_CALIB_ZERO_TANGENT_DIST 切向畸变保持为零 CV_CALIB_FIX_K1,...,CV_CALIB_FIX_K6 迭代过程中不改变相应的值。如果设置了 CV_CALIB_USE_INTRINSIC_GUESS 将会使用用户提供的初始值,否则设置为零 CV_CALIB_RATIONAL_MODEL 畸变模型的选择,如果设置了该参数,将会使用更精确的畸变模型,distCoeffs的长度就会变成8
-
-
立体校正
stereoRectify
initUndistortRectifyMap
remap
-
获取视差图像
StereoSGBM_create
stereo.compute
reprojectImageTo3D
def calib2(self, path, path_l, path_r, obj_pts, img_pts_l, img_pts_r):
print("[Calib 2Cam]")
with np.load(path_l) as X:
Otmx_l, dist_l, size = [X[i] for i in ('Otmx', 'dist', 'size')]
with np.load(path_r) as X:
Otmx_r, dist_r = [X[i] for i in ('Otmx', 'dist')]
flags = 0
flags |= cv2.CALIB_FIX_INTRINSIC # 固定cam 和 dist不变,只求解R,T,E,F
# 立体标定
retS, MLS, dLS, MRS, dRS, R, T, E, F = cv2.stereoCalibrate(obj_pts, img_pts_l, img_pts_r, Otmx_l, dist_l, Otmx_r, dist_r,
(size[0],size[1]), criteria=self.criteria_stero, flags=flags)
- 立体校正
def stereoRectify(self, path, imgL, imgR):
with np.load(path) as X:
steroMap_l0, steroMap_l1, steroMap_r0, steroMap_r1 = [X[i] for i in ("steroMapL0", "steroMapL1", "steroMapR0", "steroMapR1")]
imgL_rect = cv2.remap(imgL, steroMap_l0, steroMap_l1, cv2.INTER_LANCZOS4, cv2.BORDER_CONSTANT, 0)
imgR_rect = cv2.remap(imgR, steroMap_r0, steroMap_r1, cv2.INTER_LANCZOS4, cv2.BORDER_CONSTANT, 0)
imgHsatck = np.hstack([imgL_rect, imgR_rect])
imgHstack0 = np.hstack([imgL, imgR])
imgVstack = np.vstack([imgHstack0, imgHsatck])
cv2.imencode(".jpg", imgHsatck)[1].tofile(r'D:\Programs\StereoVision\SteroVision\Data\stereoRectify.jpg')
cv2.namedWindow("hstack", cv2.WINDOW_NORMAL)
cv2.imshow("hstack", imgVstack)
cv2.waitKey(0)
畸变校正
- (1) 径向畸变
沿着透镜半径方向分布的畸变,靠近透镜中心畸变较明显,表现在短焦镜头,主要包括桶形畸变和枕形畸变。
x ′ = x ( 1 + k 1 r 2 + k 2 r 4 + k 3 r 6 ) y ′ = y ( 1 + k 1 r 2 + k 2 r 4 + k 3 r 6 ) \begin{aligned} x' &= x(1+{k_1}r^2+{k_2}r^4+{k_3}r^6) \\ y' &=y(1+{k_1}r^2+{k_2}r^4+{k_3}r^6) \end{aligned} x′y′=x(1+k1r2+k2r4+k3r6)=y(1+k1r2+k2r4+k3r6)
- (2) 切向畸变
由于透镜本身与相机传感器平面(成像平面)不平行产生,由于透镜粘贴到镜头模组偏差导致。
x ′ = x + [ 2 p 1 y + p 2 ( r 2 + 2 x 2 ) ] y ′ = y + [ 2 p 2 x + p 1 ( r 2 + 2 y 2 ) ] \begin{aligned} x' = x + [2p_1y + p_2(r^2 + 2x^2)] \\ y' = y + [2p_2x + p_1(r^2 + 2y^2)] \end{aligned} x′=x+[2p1y+p2(r2+2x2)]y′=y+[2p2x+p1(r2+2y2)]
- (3) 薄棱镜畸变
一般由镜头设计加工安装误差导致,一般情况可忽略
x ′ = x + s 1 ( x 2 + y 2 ) y ′ = y + s 2 ( x 2 + y 2 ) \begin{aligned} x' = x + s_1(x^2 + y^2) \\ y' = y + s_2(x^2 + y^2) \end{aligned} x′=x+s1(x2+y2)y′=y+s2(x2+y2)
-
畸变参数(一般考虑OpenCV前五个参数, k1, k2, p1, p2, k3)
- 径向畸变 k1 k2 k3
- 切向畸变 p1 p2
- 薄棱镜畸变 s1 s2
-
畸变校正有两种方法
-
UndistortImage
-
initUndistortRectifyMap() + remap()
-
单独使用几次时,差别不大,当多次图片畸变校正时,建议使用一次
initUndistortRectifyMap
,获取映射矩阵mapx
和mapy
后,作为remap
输入,再使用多次的remap校正
-
def undistort_img(self, img, mtx, dist, newcameramtx, roi):
img_dst = cv2.undistort(img, mtx, dist, None, newcameramtx)
# 这步只是输出纠正畸变以后的图片
x, y, w, h = roi
dst = img_dst[y:y + h, x:x + w]
cv2.imwrite('calibresult.png', dst)
手眼标定
- 交互保存标定图片
def grab_one_cam(path_img):
VideoCapture = cv2.VideoCapture(0) # USB摄像头
# VideoCapture = cv2.VideoCapture(rtsp) # RTSP
if not VideoCapture.isOpened():
print("Error open video!")
exit()
frame_no = 0
while VideoCapture.isOpened():
ret, frame = VideoCapture.read()
if not ret:
break
k = show_image("frame", frame, 1)
if k == ord("Q"):
break
elif k == ord("S"):
cv2.imencode(".jpg", frame)[1].tofile(os.path.join(path_img, "img_%04d.jpg" % frame_no))
frame_no += 1
VideoCapture.release()
已知内参标定外参
- 标定已知相机内参
- 通过对应点计算投影矩阵
- 根据内参和对应点计算外参矩阵
cv2.solvePnPRansac
cv2.Rodrigues
, 旋转向量转化为旋转矩阵
def calib_image(self, img, path_cam):
# 已知相机内参和标定图片,输出外参数
mtx, dist = self.read_campara(path_cam)
if len(img.shape) == 3:
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
else:
img_gray = img.copy()
ret, corners = cv2.findChessboardCorners(img_gray, (self.rows, self.cols), None)
if ret:
exact_corners = cv2.cornerSubPix(img_gray, corners, (11, 11), (-1, -1), self.criteria)
_, rvec, tvec, inliers = cv2.solvePnPRansac(self.obj_pts, exact_corners, mtx, dist)
rotation_m, _ = cv2.Rodrigues(rvec) # 罗德里格斯变换,从旋转向量到旋转矩阵
rotation_t = np.hstack([rotation_m, tvec])
rotation_t_Homogeneous_matrix = np.vstack([rotation_t, np.array([[0, 0, 0, 1]])])
QA
- Q:像素焦距与毫米焦距(标定出来像素焦距)
fu = fx * dx
fv = fy * dy
fx、fy内参矩阵中的像素焦距
fu、fv为毫米焦距
dx、dy为像素到实际尺寸的转换关系,像素<->毫米
- Q:实际棋盘格尺寸设置对标定结果的影响
**实际尺寸**影响对相机的像素焦距、像主点、畸变系数、外参中的旋转矩阵没有影响,dx\dy改变,外参中的平移矩阵、双目标定中的基线有影响
- Q:标定棋盘格的行数和列数设置
实际标定板行数和列数,设置行-1 列-1,识别内部的点,所以拍摄棋盘格时,最外的行列遮挡也可;
参考文献

GitCode 天启AI是一款由 GitCode 团队打造的智能助手,基于先进的LLM(大语言模型)与多智能体 Agent 技术构建,致力于为用户提供高效、智能、多模态的创作与开发支持。它不仅支持自然语言对话,还具备处理文件、生成 PPT、撰写分析报告、开发 Web 应用等多项能力,真正做到“一句话,让 Al帮你完成复杂任务”。
更多推荐
所有评论(0)