双目相机标定结果(内参,外参)验证–在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)
最后就是效果图(不小心上镜了???)