【OpenCV】OpenCV-Python实现相机标定+利用棋盘格相对位姿估计

本文介绍了如何通过拍摄棋盘格图片进行相机标定,然后实时解算棋盘格相对于摄像头的位姿。首先,通过调整图片拍摄并获取高质量棋盘格图像,然后进行相机标定计算内参数矩阵和畸变系数。接着,使用OpenCV的solvePnP函数解算位姿,并将旋转矢量转换为欧拉角。在注意点中强调了解算的位姿是目标坐标系相对于相机坐标系的,并讨论了运行效率。实验结果显示,位姿解算的误差在1%以内,证明了方法的有效性。
该文章已生成可运行项目,

写在前面:

    这次要实现的功能:实时检测棋盘格相对于摄像头的距离以及位姿。为此主要步骤可分为以下三个步骤:标定图片的拍摄、相机的标定、以及棋盘格位姿的实时解算。


目录

1. 标定图片的拍摄

2. 相机的标定

3. 棋盘格位姿的实时解算

4. 需要注意的点

5. 运行效率问题


1. 标定图片的拍摄

棋盘格图片

        打印上面的图片,尽量铺满一张A4纸,边缘留出一定的空白以方便握持。这里使用的是10×7的棋盘格,内部有9×6个角点。

        然后使用摄像头来随意拍摄棋盘格的15-20张照片。这里笔者踩了一个小坑,笔者一开始用的是电脑的相机应用拍摄了20张照片,分辨率为1280×720。进行后面的步骤都没什么问题,但是测得的结果怎么都不准。这是因为这颗摄像头录像和拍照的分辨率不一致,因此编写一个小程序来获得标定所用的照片。

import cv2
camera = cv2.VideoCapture(0)
i = 1
while i < 50:
    _, frame = camera.read()
    cv2.imwrite("E:/images/"+str(i)+'.png', frame, [int(cv2.IMWRITE_PNG_COMPRESSION), 0]) 
    cv2.imshow('frame', frame)
    i += 1
    if cv2.waitKey(200) & 0xFF == 27: # 按ESC键退出
        break
cv2.destroyAllWindows()

        保存的图片经过手动筛选,挑选出清晰度和完整度较好的20张,如下所示:

利用摄像头拍摄的20张棋盘格图片

2. 相机的标定

        相机标定的原理此处略去,网上可以找到大量的相关资料。

        有一点需要注意,标定中将世界坐标系的建在标定板上,所有的z坐标均为0。但是x和y坐标需要经过测量得出。笔者所打印的棋盘格一格的边长为2.6厘米,因此每一个角点在世界坐标系中的坐标都需要以2.6厘米为倍数。

        相机标定部分的主要代码如下:

objp = np.zeros((6 * 9, 3), np.float32)
objp[:, :2] = np.mgrid[0:9, 0:6].T.reshape(-1, 2)  # 将世界坐标系建在标定板上,所有点的Z坐标全部为0,所以只需要赋值x和y
objp = 2.6 * objp   # 打印棋盘格一格的边长为2.6cm
obj_points = []     # 存储3D点
img_points = []     # 存储2D点
images=glob.glob("E:/image/*.png")  #黑白棋盘的图片路径

for fname in images:
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    size = gray.shape[::-1]
    ret, corners = cv2.findChessboardCorners(gray, (9, 6), None)
    if ret:
        obj_points.append(objp)
        corners2 = cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), (cv2.TERM_CRITERIA_MAX_ITER | cv2.TERM_CRITERIA_EPS, 30, 0.001))  
        if [corners2]:
            img_points.append(corners2)
        else:
            img_points.append(corners)
        cv2.drawChessboardCorners(img, (9, 6), corners, ret)  # 记住,OpenCV的绘制函数一般无返回值
        cv2.waitKey(1)
_, mtx, dist, _, _ = cv2.calibrateCamera(obj_points, img_points, size, None, None)

# 内参数矩阵
Camera_intrinsic = {"mtx": mtx,"dist": dist,}

        后面解算位姿所需要的参数为内参数矩阵mtx和畸变系数dist。据此求得的内参数矩阵和畸变系数如下:

        内参矩阵的具体表达式如下:

M=\left[\begin{matrix}\frac{1}{\text{d}x}&0&{​{u}_{0}}\\0&\frac{1}{\text{d}y}&{​{v}_{0}}\\0&0&1\\\end{matrix}\right]\left[\begin{matrix}f&0&0\\0&f&0\\0&0&1\\\end{matrix}\right]=\left[\begin{matrix}{​{f}_{x}}&0&{​{u}_{0}}\\0&{​{f}_{y}}&{​{v}_{0}}\\0&0&1 \\\end{matrix}\right]

        其中,\text{d}x\text{d}y分别是每个像素在图像平面xy方向上的物理尺寸,({​{u}_{0}},{​{v}_{0}})是图像坐标系原点在像素坐标系中的坐标,f为摄像头的焦距,{​{f}_{x}}{​{f}_{y}}为焦距f与像素物理尺寸的比值,单位为个(像素数目)。 

        据此可以得到,这台摄像头的{​{f}_{x}}\approx {​{f}_{y}}\approx 450,说明焦距f约等于450个像素的物理尺寸。{​{u}_{0}}\approx 376{​{v}_{0}}\approx 234。这台摄像头的像素为640×480,因此{​{u}_{0}}的理论值应为320,v_0的理论值应为240。误差主要是因为摄像头的分辨率太低,实际角点在像素坐标系中显示不准;此外,目标坐标系的测量时也会带来误差。

3. 棋盘格位姿的实时解算

        利用solvePnP函数可以实时解算出每一帧的旋转矢量rvec和平移矢量tvec。旋转矢量虽然简洁,但是作为结果显示不够直观,故需要将其转换为欧拉角。

        在欧拉角中,俯仰角(pitch)代表绕x轴旋转的角度, 偏航角(yaw)代表绕y轴旋转的角度,滚转角(roll)代表绕z轴旋转的角度。其中,默认逆时针选择为正,顺时针旋转为负。

        该部分的主要代码如下:

obj_points = objp   # 存储3D点
img_points = []     # 存储2D点

#从摄像头获取视频图像
camera = cv2.VideoCapture(0)

while True:
    _, frame = camera.read()
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    size = gray.shape[::-1]
    ret, corners = cv2.findChessboardCorners(gray, (9, 6), None)
    if ret:    # 画面中有棋盘格
        img_points = np.array(corners)
        cv2.drawChessboardCorners(frame, (9, 6), corners, ret)
        # rvec: 旋转向量 tvec: 平移向量
        _, rvec, tvec = cv2.solvePnP(obj_points, img_points, Camera_intrinsic["mtx"], Camera_intrinsic["dist"])    # 解算位姿
        distance = math.sqrt(tvec[0]**2+tvec[1]**2+tvec[2]**2)  # 计算距离
        rvec_matrix = cv2.Rodrigues(rvec)[0]    # 旋转向量->旋转矩阵
        proj_matrix = np.hstack((rvec_matrix, tvec))    # hstack: 水平合并
        eulerAngles = cv2.decomposeProjectionMatrix(proj_matrix)[6]  # 欧拉角
        pitch, yaw, roll = eulerAngles[0], eulerAngles[1], eulerAngles[2]
        cv2.putText(frame, "dist: %.2fcm, yaw: %.2f, pitch: %.2f, roll: %.2f" % (distance, yaw, pitch, roll), (10, frame.shape[0] - 20), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
        cv2.imshow('frame', frame)
        if cv2.waitKey(1) & 0xFF == 27: # 按ESC键退出
            break
    else:   # 画面中没有棋盘格
        cv2.putText(frame, "Unable to Detect Chessboard", (20, frame.shape[0] - 20), cv2.FONT_HERSHEY_SIMPLEX, 1.3, (0, 0, 255), 3) 
        cv2.imshow('frame', frame)
        if cv2.waitKey(1) & 0xFF == 27: # 按ESC键退出
            break
cv2.destroyAllWindows()

        当画面中检测不到棋盘格,或距离过远无法检测棋盘格的角点时,显示结果如下:

        当画面中能正常检测棋盘格的角点时,显示结果如下:

        左上角第一个红点为标定时所确定的世界坐标系的原点,沿棋盘格向右为x轴正方向,沿棋盘格向下为y轴正方向。 

        此时该棋盘格的坐标原点与摄像头的距离为43.18cm,偏航角为-1.3°,俯仰角为-26.48°,滚转角为3.92°。经过验证,该结果与实际的误差在1%以内,证明了结果的正确性。

4. 需要注意的点

        相对位姿估计的基本问题

  • 输入:相机内参数;多个空间上的特征点在目标坐标系(3D)和相平面坐标系(2D)坐标
  • 输出:目标坐标系相对相机坐标系的位置和姿态

        也就是说,solvePnP函数求解的是目标坐标系相对相机坐标系的位置和姿态。为了提高结果的可读性,最好将初始位置的目标坐标系与相机坐标系的方向同一化。

        相机坐标系的x轴和y轴对应着相平面坐标系的u轴和v轴,因此在实际操作中,确定目标坐标系时按照像素坐标系的方向来确定即可。具体如下:

        此处目标坐标系的坐标原点确定在第一个角点处,目的是为了在编程中简化目标坐标系的设置。实际上将目标坐标系的坐标原点确定在棋盘格的中心更为合理。

        为了验证输出结果为目标坐标系相对于相机坐标系的位姿(顺序很重要),将棋盘格绕x轴逆时针旋转45°,输出俯仰角pitch也约为45°;将棋盘格绕y轴逆时针旋转45°,输出偏航角yaw也约为45°,将棋盘格绕z轴逆时针旋转45°,输出滚转角roll也约为45°。由此证明了输出结果为目标坐标系相对于相机坐标系的位姿。(如果结果符号不对,说明在编程中目标坐标系的设置有误)

5. 运行效率问题

        在程序中加入时钟检测代码,可以得到每运行一帧所需要的计算时间。

逐帧运行时间

        该程序运行于i7-8550U低压CPU平台,性能没有很高。当正常解算位姿时,一帧的平均运行时间在0.01秒以内,可以支持60Hz或90Hz的摄像头运行。但是当画面中没有棋盘格时,一帧的平均运行时间更长,原因是findChessboardCorners在寻找不到角点的情况下运算量更大。但是我们关注的只是正常解算位姿时的运行效率,无法检测棋盘格时的运行效率可以不予考虑。

本文章已经生成可运行项目
<think>在MATLAB中,矩阵可以存储为.mat文件(MAT文件),这是一种二进制文件格式,用于保存工作区变量。以下是关于如何将矩阵存储为.mat文件以及如何加载.mat文件的详细说明。 ### 1. 存储单个矩阵到.mat文件 使用`save`函数将矩阵存储到.mat文件中。 ```matlab % 创建一个矩阵 A = magic(3); % 将矩阵A存储到文件 'matrix.mat' 中 save('matrix.mat', 'A'); % 可选:指定存储格式('-v7.3'支持大于2GB的文件,兼容性更好) save('matrix_v7.mat', 'A', '-v7.3'); ``` ### 2. 存储多个矩阵到同一个.mat文件 可以将多个变量同时存储到一个.mat文件中。 ```matlab % 创建多个矩阵 B = rand(4); C = eye(5); % 存储多个变量 save('multiple_matrices.mat', 'A', 'B', 'C'); % 或者使用通配符存储工作区所有变量 save('all_variables.mat'); ``` ### 3. 加载.mat文件 使用`load`函数加载.mat文件中的变量。 ```matlab % 加载单个变量(如果文件中有多个变量,则全部加载到工作区) load('matrix.mat'); % 加载后,变量A将出现在工作区 % 加载特定变量(避免加载全部) load('multiple_matrices.mat', 'A', 'B'); % 只加载A和B % 将加载的变量赋给新的变量名(避免覆盖当前工作区变量) loadedData = load('multiple_matrices.mat'); % 然后通过结构体访问,例如:loadedData.A, loadedData.B % 加载大型文件时,可以先查看文件内容再决定加载哪些变量 whos -file 'multiple_matrices.mat'; % 显示文件中包含的变量及其大小、类型 ``` ### 4. 处理压缩和加密 MATLAB在存储.mat文件时默认进行压缩,但也可以选择不压缩以加快保存速度(适用于大型数据)。 ```matlab % 不压缩存储(加快保存速度,但文件较大) save('no_compression.mat', 'A', '-nocompression'); ``` ### 5. 将结构体或元胞数组存储为.mat文件 结构体和元胞数组也可以直接存储。 ```matlab % 创建结构体 person.name = 'Alice'; person.age = 30; person.scores = [95, 88, 92]; % 创建元胞数组 cellArray = {1, 'text', magic(2)}; % 存储结构体和元胞数组 save('complex_data.mat', 'person', 'cellArray'); ``` ### 6. 从.mat文件中读取部分数据(适用于大型文件) 对于非常大的.mat文件,可以使用`matfile`函数创建内存映射,从而只读取部分数据。 ```matlab % 创建一个可映射的matfile对象 m = matfile('large_data.mat'); % 查看变量信息 disp(m.Properties.Writable); % 显示文件是否可写 disp(m.Properties.VariableNames); % 显示变量名 % 读取部分数据(假设large_data.mat中有一个名为bigMatrix的10000x10000矩阵) partialData = m.bigMatrix(1:100, 1:100); % 只读取前100行和前100列 % 修改部分数据(需要文件以可写方式创建) m.bigMatrix(1:10, 1:10) = zeros(10); % 将前10x10区域置零 ``` ### 7. 跨平台兼容性 MATLAB的.mat文件在不同操作系统(Windows, Linux, macOS)之间是兼容的,但需要注意版本: - 使用`-v7.3`格式可以保证更好的兼容性,特别是当文件大于2GB或需要与较新版本的MATLAB兼容时。 - 旧版本MATLAB可能无法读取高版本保存的.mat文件(使用`-vX`选项指定版本)。 ### 8. 保存和加载函数封装示例 下面是一个封装保存和加载操作的函数示例: ```matlab function saveMatrixToFile(matrix, filename, varargin) % SAVEMATRIXTOFILE 保存矩阵到.mat文件 % saveMatrixToFile(matrix, filename) 将矩阵保存到指定文件 % saveMatrixToFile(matrix, filename, 'v7.3') 指定版本 if nargin < 2 error('需要至少两个参数:矩阵和文件名'); end if nargin == 3 save(filename, 'matrix', varargin{1}); else save(filename, 'matrix'); end end function matrix = loadMatrixFromFile(filename, varname) % LOADMATRIXFROMFILE 从.mat文件中加载矩阵 % matrix = loadMatrixFromFile(filename) 加载文件中的第一个变量 % matrix = loadMatrixFromFile(filename, varname) 加载指定变量名 if nargin < 1 error('需要文件名'); end data = load(filename); if nargin == 1 % 获取第一个变量名 vars = fieldnames(data); varname = vars{1}; end matrix = data.(varname); end ``` ### 9. 注意事项 - 当存储和加载大型矩阵时,使用`-v7.3`格式以避免文件大小限制。 - 使用`whos -file`可以在加载前查看文件内容,避免覆盖工作区变量。 - 使用`matfile`函数处理大型文件,避免内存溢出。 §§相关问题§§: 1. 如何将多个.mat文件合并为一个? 2. 如何将.mat文件转换为CSV或Excel格式? 3. 在MATLAB中如何加密.mat文件以保护数据? 4. 如何在不加载整个文件的情况下读取.mat文件中的元数据(如变量名、大小)?</think># MATLAB 中矩阵存储为 MAT 文件的完整指南 在 MATLAB 中,`.mat` 文件是用于存储工作区变量的标准二进制文件格式。这种格式高效、紧凑,特别适合存储大型矩阵和多维数组。下面我将详细介绍如何将矩阵存储为 MAT 文件,以及如何高效地操作这些文件。 ## 1. 基本保存与加载操作 ### 保存单个矩阵到 MAT 文件 ```matlab % 创建一个示例矩阵 A = magic(5); % 5x5 魔方阵 % 将矩阵保存到 MAT 文件 save('my_matrix.mat', 'A'); % 验证文件已创建 disp('已保存文件:'); dir('*.mat') ``` ### 加载 MAT 文件中的矩阵 ```matlab % 清除工作区中的变量 clear A % 加载 MAT 文件 load('my_matrix.mat'); % 验证矩阵已加载 disp('加载的矩阵 A:'); disp(A); ``` ## 2. 保存多个矩阵 ```matlab % 创建多个矩阵 B = rand(3, 4); % 3x4 随机矩阵 C = eye(6); % 6x6 单矩阵 D = linspace(0, 1, 100); % 1x100 线性间距向量 % 保存所有矩阵到单个文件 save('multiple_matrices.mat', 'B', 'C', 'D'); % 加载所有矩阵 clear all load('multiple_matrices.mat'); % 验证加载的矩阵 whos ``` ## 3. 选择保存格式(版本控制) MATLAB 支持多种 MAT 文件格式,不同版本有不同特点: ```matlab % 创建大型矩阵 big_matrix = rand(5000); % 5000x5000 双精度矩阵 (约200MB) % 保存为不同版本 save('v7.mat', 'big_matrix', '-v7'); % 标准MATLAB 7.3版本之前的格式 save('v7_3.mat', 'big_matrix', '-v7.3'); % 支持大于2GB的文件,HDF5格式 save('v6.mat', 'big_matrix', '-v6'); % 兼容旧版MATLAB(R2006b之前) % 比较文件大小 file_info = dir('*.mat'); disp('文件大小比较:'); for i = 1:length(file_info) fprintf('%s: %.2f MB\n', file_info(i).name, file_info(i).bytes/1e6); end ``` ## 4. 部分加载与保存 ### 部分加载大型文件 ```matlab % 创建大型数据 large_data = rand(10000, 1000); % 10000x1000 矩阵 (约80MB) % 保存到文件 save('large_data.mat', 'large_data', '-v7.3'); % 部分加载 - 只加载前100行 partial_data = load('large_data.mat', 'large_data'); partial_data = partial_data.large_data(1:100, :); % 显示部分数据 disp('部分加载的数据大小:'); disp(size(partial_data)); ``` ### 使用 matfile 对象处理大文件 ```matlab % 创建可映射的mat文件对象 m = matfile('big_data.mat', 'Writable', true); % 创建并保存数据分块 for i = 1:10 chunk = rand(1000, 1000); m.data(i*1000-999:i*1000, :) = chunk; end % 读取特定部分 disp('第5000-5001行,第1-5列:'); disp(m.data(5000:5001, 1:5)); ``` ## 5. 压缩选项 ```matlab % 创建测试矩阵 test_matrix = rand(1000, 1000); % 保存时不压缩 save('uncompressed.mat', 'test_matrix', '-v7.3', '-nocompression'); % 保存时压缩 save('compressed.mat', 'test_matrix', '-v7.3'); % 比较文件大小 uncompressed = dir('uncompressed.mat'); compressed = dir('compressed.mat'); fprintf('未压缩大小: %.2f MB\n', uncompressed.bytes/1e6); fprintf('压缩后大小: %.2f MB\n', compressed.bytes/1e6); fprintf('压缩率: %.1f%%\n', 100*(1-compressed.bytes/uncompressed.bytes)); ``` ## 6. 结构体和元胞数组的存储 ```matlab % 创建包含多种数据类型的结构体 data_struct = struct(); data_struct.matrix = magic(4); data_struct.text = 'MAT文件示例'; data_struct.cell_array = {1, 2, 3; 'a', 'b', 'c'}; data_struct.nested_struct.value = rand(3); % 保存结构体 save('structured_data.mat', 'data_struct'); % 加载并显示 clear data_struct load('structured_data.mat'); disp('加载的结构体内容:'); disp(data_struct); ``` ## 7. 跨平台兼容性处理 ```matlab % 创建示例数据 cross_data = randi([1, 100], 5, 5); % 保存为跨平台兼容格式 save('cross_platform.mat', 'cross_data', '-v7', '-nocompression'); % 在Python中读取MAT文件的示例代码 % 注意:以下代码Python代码,仅作参考 """ # Python中读取MAT文件 import scipy.io mat_data = scipy.io.loadmat('cross_platform.mat') print(mat_data['cross_data']) """ ``` ## 8. 高级技巧:自定义保存函数 ```matlab function saveMatrixWithMetadata(matrix, filename, description) % SAVEMATRIXWITHMETADATA 保存矩阵并添加元数据 % 输入: % matrix - 要保存的矩阵 % filename - 保存的文件名 % description - 矩阵的描述文本 % 创建包含矩阵和元数据的结构体 data.matrix = matrix; data.metadata.date = datetime('now'); data.metadata.description = description; data.metadata.size = size(matrix); data.metadata.class = class(matrix); % 保存到文件 save(filename, 'data'); fprintf('矩阵已保存到 %s\n', filename); end % 使用自定义函数保存矩阵 test_mat = hilb(10); % 10x10 Hilbert矩阵 saveMatrixWithMetadata(test_mat, 'hilbert_matrix.mat', '10x10 Hilbert矩阵'); ``` ## 9. 性能优化建议 1. **批量操作**:尽量减少保存/加载操作的次数 2. **使用适当版本**:对于>2GB的文件使用`-v7.3` 3. **避免压缩大文件**:压缩会增加保存时间 4. **预分配空间**:对于大型矩阵,使用`matfile`预分配空间 5. **部分加载**:只加载需要的部分数据 ```matlab % 性能比较:部分加载 vs 完整加载 large_mat = rand(10000); % 约800MB % 保存文件 save('perf_test.mat', 'large_mat', '-v7.3'); % 完整加载时间 tic; load('perf_test.mat'); t_full = toc; % 部分加载时间 tic; m = matfile('perf_test.mat'); partial = m.large_mat(1:100, 1:100); t_partial = toc; fprintf('完整加载时间: %.2f 秒\n', t_full); fprintf('部分加载时间: %.2f 秒\n', t_partial); fprintf('时间比: %.1f:1\n', t_full/t_partial); ``` ## 10. 错误处理与验证 ```matlab % 尝试加载不存在的文件 try load('non_existent_file.mat'); catch ME fprintf('错误: %s\n', ME.message); end % 验证加载的数据 function isValid = validateMatrix(filename, varName) % VALIDATEMATRIX 验证MAT文件中的矩阵是否有效 try data = load(filename, varName); if ~isfield(data, varName) error('变量 %s 不存在于文件中', varName); end matrix = data.(varName); % 检查是否为矩阵 if ~ismatrix(matrix) error('变量 %s 不是一个矩阵', varName); end % 检查是否包含NaN或Inf if any(isnan(matrix(:))) || any(isinf(matrix(:))) warning('矩阵包含NaN或Inf值'); end isValid = true; catch ME fprintf('验证失败: %s\n', ME.message); isValid = false; end end % 使用验证函数 isValid = validateMatrix('my_matrix.mat', 'A'); fprintf('矩阵验证结果: %d\n', isValid); ```
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Quentin_HIT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值