双目相机标定结果内参及外参验证

本文围绕双目相机标定结果验证展开,介绍在OpenGL中重投影目标模型到图像平面的方法。先阐述相机标定重要性及所用棋盘格标定法,接着说明拍摄图片、单独标定左右相机图片的过程,重点讲解验证标定结果,涉及坐标系转换、矩阵对应等内容。

双目相机标定结果(内参,外参)验证–在OpenGL中重投影目标模型到图像平面,与左右视角图像中的模型同时重合

相机标定是计算机视觉任务中比较基础且重要的前提步骤。单目相机的标定可以直接用来对图像进行反扭曲处理。多目相机标定稍微复杂一些,笔者这里在做多目姿态估计的任务,因此接触到了多目相机标定。

我这里用到的方法也很简单,用的是比较经典的棋盘格标定法。两个相机设置一定的拍摄角度,同时拍摄若干组标定板的图片,保证相机至少有几张是同时看到标定板的(方便之后的标定验证)。标定工具用的是GML C++ Camera Calibration Toolbox,也可以用Camera Calibration Toolbox for Matlab。两个工具标定后都会给出一个反投影误差,用来评估标定的准确性。

如果标定结果足够精确,那么在一个视角下使目标模型与图片模型重合,其他视角下也会重合。基于这个前提,做下面的任务。项目地址
1 拍摄图片
2 单独标定左右相机图片
3 验证标定结果
1 拍摄图片 (图片中的手部模型为3D打印,OpenGL中会绘制相同的模型)

简易做了一个标定版,A4纸打印,8*5 size的pattern,格子边长为27.5mm。分别拍摄两个视角下的图片若干张。我这里保证left 和right 视角下各有两张是同时拍到标定板的。
在这里插入图片描述

2 单独标定左右相机图片

笔者用GML标定工具,并且分别导出了内参矩阵和外参矩阵。具体的标定结果可在这里下载。

3 验证标定结果

接下来才是这篇博客的重点。这里我贴出几个自己参考的blog。1:3D Projection 2:Augmented Reality on libQGLViewer and OpenCV-OpenGL tips [w/code] 3:Camera Models and Augmented Reality

关于标定的原理,大家可以查询相关知识,这里不再赘述。我们的主要目的就是验证内参和外参。如下公式所示。

OpenCV中相机坐标系为右手坐标系,转换到OpenGL相机坐标系要进行z轴的反转。
相机内参矩阵对应OpenGL的投影矩阵,外参矩阵对应OpenGL的ModelView矩阵。费话不多说,直接来看代码,读取GML标定结果,并且构造Projection 和 View 矩阵。

以下为初始化变量的声明,其中用到了自己写的简单的OpenGL库,可参考项目地址查看

from OpenGL.GL import *
from OpenGL.GLUT import *
import numpy as np
from GEngine.shader import ShaderProgram
from GEngine.model import Model, ModelFromExport, generate_grid_mesh
import glm
from GEngine.camera3D import Camera3D
from GEngine.input_process import InputProcess, keys
from GEngine.window import GWindow
import cv2

camera = Camera3D(glm.vec3(0.0, 5.0, 30.0))
window = GWindow(b"demo", SCR_WIDTH, SCR_HEIGHT, InputProcess(camera, SCR_WIDTH, SCR_HEIGHT), keep_mouse_stay=False)

image_size = (1920, 1080)
light_color = (1.0, 1.0, 1.0)
hand_color = (0.9, 0.9, 0.9)
light_position = (-1000, -700, 1000)

model_position = [
    glm.vec3(1.0, 1.0, 1.0),
    [[0, glm.vec3(1.0, 0.0, 0.0)], [0, glm.vec3(0.0, 1.0, 0.0)], [0, glm.vec3(0.0, 0.0, 1.0)]],
    glm.vec3(500, 500.0, 0.0)
]
image_path = "/media/shuai/SHUAI_AHUT/calibration_same_camera/"

left_camera_intrinsic = np.loadtxt(image_path + "left/Intrinsic.txt")
right_camera_intrinsic = np.loadtxt(image_path + "/right/Intrinsic.txt")

left_camera_extrinsic = np.loadtxt(image_path + "left/ExtrinsicCameraPars.txt")
right_camera_extrinsic = np.loadtxt(image_path + "right/ExtrinsicCameraPars.txt")
# 构造projection矩阵
def build_projection_matrix(camera_intrinsic_matrix, width, height):
    d_near = 0.1  # Near clipping distance
    d_far = 1000.0  # Far clipping distance

    # Camera parameters
    fx = camera_intrinsic_matrix[0, 0]  # Focal length in x axis
    fy = camera_intrinsic_matrix[1, 1]  # Focal length in y axis (usually the same?)
    cx = camera_intrinsic_matrix[0, 2]  # Camera primary point x
    cy = camera_intrinsic_matrix[1, 2]  # Camera primary point y

    projection_matrix = np.array([[fx / cx, 0.0, 0.0, 0.0],
                                  [0.0, fy / cy, 0.0, 0.0],
                                  [0, 0, -(d_far + d_near) / (d_far - d_near), -1.0],
                                  [0.0, 0.0, -2.0 * d_far * d_near / (d_far - d_near), 0.0]], dtype=np.float32)


    return projection_matrix

# 构造view矩阵
def build_model_view_matrix(camera_extrinsic_matrix):
    r, t = camera_extrinsic_matrix[3:], camera_extrinsic_matrix[:3]
    R = cv2.Rodrigues(r)[0]
	
    inverse = np.array([[1, 0, 0],
                        [0, -1, 0],
                        [0, 0, -1]])
	# 奇异值分解,消除标定误差导致的旋转矩阵不合格的情况。
    u, _, v = np.linalg.svd(R)
    R = u @ v
	
	# OpenGL相机坐标系下z轴负向为正方向
    rotation = inverse @ R
    translation = inverse @ t
    
    model_view_matrix = np.identity(4, dtype=np.float32)
    model_view_matrix[:3, :3] = rotation
    model_view_matrix[:3, 3] = translation
	
	# OpengGL中矩阵为列优先存储
    model_view_matrix = model_view_matrix.T
    return model_view_matrix

接下来是绘制背景图像,这个就很简单了。

def render_background_image(bg_model):
    glDisable(GL_DEPTH_TEST)
    bg_model.draw(bg_shader_program, draw_type=GL_TRIANGLES)
    glEnable(GL_DEPTH_TEST)

整个OpenGL视图分成两块,分别对应left和right。

def render():
    glClearColor(1.0, 1.0, 1.0, 0.0)
    glClearDepth(1.0)
    glPointSize(5)
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

    glViewport(0, 0, int(SCR_WIDTH / 2), SCR_HEIGHT)
    render_background_image(bg_model_left)
    render_side_view(left_camera_intrinsic, left_camera_extrinsic, SCR_WIDTH / 2, SCR_HEIGHT, stereo_left_index)

    glViewport(int(SCR_WIDTH / 2), 0, int(SCR_WIDTH / 2), SCR_HEIGHT)
    render_background_image(bg_model_right)
    render_side_view(right_camera_intrinsic, right_camera_extrinsic, SCR_WIDTH / 2, SCR_HEIGHT, stereo_right_index)

最后就是效果图(不小心上镜了???)
在这里插入图片描述

<think>我们正在讨论使用OpenCV和C++进行相机内参标定。根据用户提供的引用内容,我们可以总结出标定过程的关键步骤。引用[1]解释了相机内参的基本概念,包括fx, fy, u0, v0。引用[2]描述了单目相机标定的步骤,包括角点检测、亚像素精确化、计算内参和畸变系数以及生成校正映射表。引用[3]则提到标定结果会生成一些文件,并且最后进行图像矫正。 用户的需求是使用C++结合OpenCV实现相机内参标定。这里我们主要关注单目标定内参)和双目标定,即双目标定中会得到两个相机之间的相对位置,这也属于的一部分)。下面我将详细说明标定步骤。 ### 相机内参标定(单目标定相机内参标定通常使用棋盘格作为标定板。主要步骤包括: 1. **准备标定图像**:使用相机从不同角度拍摄棋盘格(通常需要10-20张图像)。 2. **检测角点**:在每张图像中检测棋盘格的角点(内角点)。 3. **亚像素优化**:提高角点位置的精度。 4. **标定计算**:调用OpenCV的`calibrateCamera`函数计算内参矩阵和畸变系数。 #### 代码示例 以下是一个使用C++和OpenCV进行单目相机内参标定的代码框架: ```cpp #include <opencv2/opencv.hpp> #include <vector> using namespace cv; using namespace std; int main() { // 设置棋盘格数 Size boardSize(9, 6); // 棋盘格内部角点数量(宽高方向) float squareSize = 25.0f; // 棋盘格每个方格的实际大小(单位:毫米) vector<vector<Point3f>> objectPoints; // 世界坐标系中的点 vector<vector<Point2f>> imagePoints; // 图像坐标系中的点 // 生成世界坐标系中的点 (0,0,0), (1,0,0), ..., (8,5,0) vector<Point3f> obj; for (int i = 0; i < boardSize.height; i++) { for (int j = 0; j < boardSize.width; j++) { obj.push_back(Point3f(j * squareSize, i * squareSize, 0)); } } // 读取标定图像 vector<String> imagePaths; glob("calibration_images/*.jpg", imagePaths); // 假设图像放在calibration_images文件夹下 Mat img, gray; vector<Point2f> corners; for (const auto& path : imagePaths) { img = imread(path); cvtColor(img, gray, COLOR_BGR2GRAY); // 检测角点 bool found = findChessboardCorners(gray, boardSize, corners, CALIB_CB_ADAPTIVE_THRESH | CALIB_CB_NORMALIZE_IMAGE); if (found) { // 亚像素精确化 cornerSubPix(gray, corners, Size(11, 11), Size(-1, -1), TermCriteria(TermCriteria::EPS + TermCriteria::COUNT, 30, 0.1)); imagePoints.push_back(corners); objectPoints.push_back(obj); } } // 进行相机标定 Mat cameraMatrix = Mat::eye(3, 3, CV_64F); // 内参矩阵 Mat distCoeffs = Mat::zeros(8, 1, CV_64F); // 畸变系数 vector<Mat> rvecs, tvecs; // 每张图像的旋转向量和平移向量() double rms = calibrateCamera(objectPoints, imagePoints, img.size(), cameraMatrix, distCoeffs, rvecs, tvecs); cout << "RMS: " << rms << endl; cout << "Camera Matrix: " << endl << cameraMatrix << endl; cout << "Distortion Coefficients: " << distCoeffs << endl; // 保存标定结果 FileStorage fs("calibration_result.yml", FileStorage::WRITE); fs << "cameraMatrix" << cameraMatrix; fs << "distCoeffs" << distCoeffs; fs.release(); return 0; } ``` ### 相机标定(双目标定) 双目标定(立体标定)除了得到每个相机内参和畸变系数,还可以得到两个相机之间的旋转矩阵和平移向量(即)。步骤包括: 1. **分别标定左右相机内参**(或者使用上述单目标定方法分别标定)。 2. **立体标定**:使用左右相机同时拍摄同一棋盘格,然后计算两个相机之间的几何关系。 #### 代码示例 双目标定的代码框架如下: ```cpp #include <opencv2/opencv.hpp> #include <vector> using namespace cv; using namespace std; int main() { // 读取左右相机标定图像(左右相机拍摄的同一棋盘格的多组图像) vector<String> leftImagePaths, rightImagePaths; glob("left_images/*.jpg", leftImagePaths); glob("right_images/*.jpg", rightImagePaths); // 设置棋盘格数 Size boardSize(9, 6); float squareSize = 25.0f; // 准备世界坐标点(同上) vector<vector<Point3f>> objectPoints; vector<vector<Point2f>> leftImagePoints, rightImagePoints; // 检测左右图像的角点并保存 // ...(同单目标定,分别对左右图像进行角点检测和亚像素优化,此处省略) // 分别标定左右相机内参) Mat leftCameraMatrix, leftDistCoeffs; Mat rightCameraMatrix, rightDistCoeffs; vector<Mat> leftRvecs, leftTvecs, rightRvecs, rightTvecs; calibrateCamera(objectPoints, leftImagePoints, leftImg.size(), leftCameraMatrix, leftDistCoeffs, leftRvecs, leftTvecs); calibrateCamera(objectPoints, rightImagePoints, rightImg.size(), rightCameraMatrix, rightDistCoeffs, rightRvecs, rightTvecs); // 立体标定 Mat R, T, E, F; // R: 旋转矩阵,T: 平移向量,E: 本质矩阵,F: 基础矩阵 double stereoRMS = stereoCalibrate(objectPoints, leftImagePoints, rightImagePoints, leftCameraMatrix, leftDistCoeffs, rightCameraMatrix, rightDistCoeffs, leftImg.size(), R, T, E, F, CALIB_FIX_INTRINSIC, // 固定内参,因为我们已单独标定 TermCriteria(TermCriteria::COUNT + TermCriteria::EPS, 100, 1e-5)); cout << "Stereo RMS: " << stereoRMS << endl; cout << "Rotation Matrix: " << endl << R << endl; cout << "Translation Vector: " << endl << T << endl; // 保存立体标定结果 FileStorage fs("stereo_calibration.yml", FileStorage::WRITE); fs << "R" << R; fs << "T" << T; fs << "E" << E; fs << "F" << F; fs.release(); return 0; } ``` ### 注意事项 1. **图像数量**:通常需要15-20张不同角度的棋盘格图像。 2. **棋盘格方向**:尽量让棋盘格覆盖整个图像区域,并且有不同的倾斜角度。 3. **标定板平整**:棋盘格需要保持平整。 4. **畸变校正**:标定后,可以使用`initUndistortRectifyMap`和`remap`函数对图像进行畸变校正。 ### 相关问题 1. 如何评估相机标定的精度? 2. 除了棋盘格,还有哪些标定板可以使用? 3. 相机标定中常见的畸变类型有哪些?如何进行校正? 4. 如何在实时视频流中进行相机标定和畸变校正? 以上代码和步骤考了引用[1][2][3]的内容,并结合了OpenCV官方文档的推荐做法。 </think>### 使用OpenCV和C++进行相机内参标定的方法 #### **核心步骤** 1. **准备标定板** - 使用棋盘格(如8×6黑白方格) - 方格尺寸需精确测量(例如25mm×25mm) - 采集15-20张不同角度的标定板图像 2. **检测角点** ```cpp #include <opencv2/opencv.hpp> using namespace cv; Size boardSize(8, 6); // 棋盘格内角点数量 vector<Point2f> corners; bool found = findChessboardCorners(image, boardSize, corners, CALIB_CB_ADAPTIVE_THRESH + CALIB_CB_NORMALIZE_IMAGE); ``` 3. **亚像素精确化** ```cpp if (found) { Mat gray; cvtColor(image, gray, COLOR_BGR2GRAY); cornerSubPix(gray, corners, Size(11,11), Size(-1,-1), TermCriteria(TermCriteria::EPS + TermCriteria::COUNT, 30, 0.1)); } ``` 4. **计算内参** ```cpp vector<vector<Point3f>> objectPoints; // 世界坐标 vector<vector<Point2f>> imagePoints; // 图像坐标 // 生成世界坐标 (Z=0) vector<Point3f> obj; for (int i=0; i<boardSize.height; i++) for (int j=0; j<boardSize.width; j++) obj.push_back(Point3f(j*squareSize, i*squareSize, 0)); // 标定计算 Mat cameraMatrix, distCoeffs; vector<Mat> rvecs, tvecs; // (旋转向量和平移向量) double rms = calibrateCamera(objectPoints, imagePoints, image.size(), cameraMatrix, distCoeffs, rvecs, tvecs); ``` 5. **输出结果** - `cameraMatrix`:内参矩阵 $K = \begin{bmatrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{bmatrix}$ - `distCoeffs`:畸变系数 $(k_1,k_2,p_1,p_2,k_3)$ - `rvecs/tvecs`:每张图像的相机坐标系到世界坐标系的变换) #### **关键概念** 1. **内参**(Intrinsics): - $f_x, f_y$:焦距(像素单位) - $c_x, c_y$:主点坐标 - 反映相机内部光学特性[^1] 2. ****(Extrinsics): - 旋转矩阵 $R$(通过罗德里格斯公式从`rvec`转换) - 平移向量 $T$ - 描述相机在世界坐标系中的位姿[^1] 3. **畸变校正**: ```cpp Mat undistorted; undistort(image, undistorted, cameraMatrix, distCoeffs); ``` #### **完整流程示例** ```cpp // 1. 读取所有标定图像 vector<String> imagePaths; glob("calib_images/*.jpg", imagePaths); // 2. 角点检测与存储 for (const auto& path : imagePaths) { Mat img = imread(path); vector<Point2f> corners; if (findChessboardCorners(img, boardSize, corners)) { // 亚像素优化... imagePoints.push_back(corners); objectPoints.push_back(obj); // 相同世界坐标 } } // 3. 标定计算 calibrateCamera(objectPoints, imagePoints, img.size(), cameraMatrix, distCoeffs, rvecs, tvecs); // 4. 保存结果 FileStorage fs("calib_result.yml", FileStorage::WRITE); fs << "camera_matrix" << cameraMatrix; fs << "distortion_coefficients" << distCoeffs; fs.release(); ``` #### **验证与评估** - 重投影误差:RMS值应小于0.5像素(理想值<0.3) - 可视化角点重投影: ```cpp vector<Point2f> projectedPoints; projectPoints(objectPoints[i], rvecs[i], tvecs[i], cameraMatrix, distCoeffs, projectedPoints); // 绘制观测点vs重投影点 ``` - OpenCV自动生成的评估文件(如`evaluateCalibrationResult.txt`[^3]) ### 相关问题 1. 如何选择标定板的尺寸和图案? 2. 重投影误差过大时应如何调整标定过程? 3. 鱼眼镜头相机标定与普通相机标定有何不同? 4. 如何将标定结果应用于实际三维重建任务? 5. 动态场景中如何进行在线相机标定
评论 11
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值