【立体匹配】双目相机外参自标定方法介绍

news2024/12/23 22:18:00

双目相机外参自标定方法

  • 原理
  • 实践

双目相机外参自标定方法是一种无需固定标定板,在拍摄实际场景的两张图像时,通过计算两幅图像之间的匹配特征点对,结合相机的内参矩阵,来实时求解两个相机之间相对位置(即外参)的方法。

原理

双目相机外参自标定基于双目立体视觉原理,通过匹配两幅图像中的对应点,利用这些匹配点对以及相机的内参矩阵,计算出两个相机之间的旋转矩阵和平移向量,即外参矩阵。这种方法避免了使用固定标定板的繁琐过程,提高了标定的灵活性和实时性。

具体步骤

  1. 畸变校正:
    首先,利用已知的相机内参矩阵对两幅原始图像进行畸变校正。这是因为在相机成像过程中,由于镜头制造和安装误差等原因,图像会产生畸变,影响后续的特征点匹配和参数计算。

  2. 特征点提取与匹配:
    使用特征点检测算法(如SIFT、SURF、ORB等)在两幅校正后的图像中分别提取特征点。
    然后,利用特征点匹配算法(如FLANN、BFMatcher等)找出两幅图像中的匹配点对。这些匹配点对是两幅图像中同一物点在两个相机成像平面上的投影。

  3. 本质矩阵求解:
    利用匹配点对和相机内参矩阵,通过数学方法求解本质矩阵(Essential Matrix)。本质矩阵描述了两个相机坐标系之间的旋转和平移关系,但不包含相机的内参信息。
    在OpenCV中,可以使用cv::findEssentialMat函数来求解本质矩阵。该函数通常结合随机抽样一致性算法(RANSAC)来提高求解的鲁棒性,减少误匹配点对的影响。

  4. 外参矩阵分解:
    通过对本质矩阵进行奇异值分解(SVD)等操作,可以分解出两个相机之间的旋转矩阵和平移向量,即外参矩阵。
    在OpenCV中,可以使用cv::recoverPose函数从本质矩阵中恢复出相机的旋转矩阵和平移向量。该函数同样可以结合RANSAC算法来提高结果的准确性。

实践

外参自标定步骤:

第一步:使用左右相机的对应匹配点。

第二步:依据双目极线约束条件构造本征矩阵关于关键点位置的代价函数,将匹配关键点对带入代价函数,所求的代价函数之和称之为能量函数。

第三步:使用非线性优化方法优化本征矩阵的值,使得能量函数最小,此时估计的本征矩阵为最终结果,将得到的本征矩阵分解,即可得到双目相机的相对位姿。

左右相机的匹配点:
在这里插入图片描述在这里插入图片描述

下图依次为原标定参数极线校正标定板视差图、恢复参数的标定板视差图:

标定板

下图依次为原标定参数极线校正人脸深度图、使用一段时间后校正深度图、恢复参数后校正深度图:

对比

以下是著名的八点法求解本质矩阵:

# 读取左右图像  
imgL = cv2.imread('left.jpg',0) # 左图像  
imgR = cv2.imread('right.jpg',0) # 右图像  
  
# 初始化SIFT检测器  
sift = cv2.xfeatures2d.SIFT_create()  
  
# 检测并计算关键点和描述符  
kpL, desL = sift.detectAndCompute(imgL,None)  
kpR, desR = sift.detectAndCompute(imgR,None)  
  
# 匹配描述符  
bf = cv2.BFMatcher()  
matches = bf.knnMatch(desL,desR,k=2)  
  
# 选择好的匹配点  
good = []  
for m,n in matches:  
    if m.distance < 0.75*n.distance:  
        good.append(m)  
  
# 如果匹配点数量不够,则退出程序  
if len(good)<4:  
    print("Not enough matches are found - {}/{}".format(len(good),4))  
    exit()  
  
# 提取匹配点的坐标  
src_pts = np.float32([ kpL[m.queryIdx].pt for m in good ]).reshape(-1,1,2)  
dst_pts = np.float32([ kpR[m.trainIdx].pt for m in good ]).reshape(-1,1,2)  
  
# 利用非线性优化求解本质矩阵和旋转矩阵  
E, mask = cv2.findEssentialMat(src_pts, dst_pts, None, method=cv2.RANSAC, prob=0.999, maxIters=1000)  
R, t, mask = cv2.recoverPose(E, src_pts, dst_pts)  
print("Rotation matrix: \n" + str(R))  
print("Translation vector: \n" + str(t))

但求解结果随缘,待优化。

以下是利用Scipy库进行非线性优化:

def skew_matrix(t):
    '''
    计算平移向量 T 的反对称矩阵'''
    return np.array([[0, -t[2], t[1]],
                     [t[2], 0, -t[0]],
                     [-t[1], t[0], 0]])

def essential_matrix(r, t):
    R = cv2.Rodrigues(r)[0]
    skew_T = skew_matrix(t)
    E = np.dot(skew_T, R)
    return E

def error_function(params, left_pts, right_pts, mtx_left, mtx_right):
    r = params[:3]
    t = T#params[3:]#T#

    E = essential_matrix(r, t)

    error = []
    # constraint = 0
    for i in range(len(left_pts)):
        X1_normalized = np.dot(np.linalg.inv(mtx_left), np.array([left_pts[i][0], left_pts[i][1], 1]))
        X2_normalized = np.dot(np.linalg.inv(mtx_right), np.array([right_pts[i][0], right_pts[i][1], 1]))

        # 极线约束
        # constraint = abs(np.dot(np.dot(np.dot(np.dot(X2_normalized.T, np.linalg.inv(mtx_right).T), E), np.linalg.inv(mtx_left)), X1_normalized))
        constraint = np.dot(np.dot(X2_normalized.T, E), X1_normalized)
        print(constraint)
        
        # 添加到误差向量
        error.append(constraint)

    return np.array(error).flatten()
def nonlinear_optimization(left_pts, right_pts, mtx_left, mtx_right, initial_params):
    result = least_squares(error_function, initial_params, args=(left_pts, right_pts, mtx_left, mtx_right))
    # result = minimize(error_function, initial_params, args=(left_pts, right_pts, mtx_left, mtx_right))
    optimized_params = result.x

    r_optimized = optimized_params[:3]
    t_optimized = T#optimized_params[3:]
    print('optimized_params', optimized_params)

    R_optimized = cv2.Rodrigues(r_optimized)[0]
    E_optimized = essential_matrix(r_optimized, t_optimized)

    return R_optimized, E_optimized
	# 调用非线性优化函数
    R_optimized, E_optimized = nonlinear_optimization(left_pts, right_pts, mtx_left, mtx_right, initial_params)

    print("Optimized Rotation Matrix:")
    print(R_optimized)
    print('\nori R', R1)
    print("\nOptimized Essential Matrix:")
    print(E_optimized) 
    print("\nori Essential Matrix:")

    # 矫正图像
    if 'dist' in src:
        dist_left = np.zeros((5, 1))
        dist_right = np.zeros((5, 1))
    R1, R2, P1, P2, Q, roi_left, roi_right = cv2.stereoRectify(mtx_left, dist_left, mtx_right, dist_right, image_size, R_optimized, T, flags=0, alpha=0.1) # cv2.CALIB_ZERO_DISPARITY
    # Undistortion and Rectification(计算畸变矫正和立体校正的映射变换)
    # map_x: The first map of y values; map_y: The second map of y values
    left_map_re = cv2.initUndistortRectifyMap(mtx_left, dist_left, R1, P1, image_size, cv2.CV_32FC1)
    right_map_re = cv2.initUndistortRectifyMap(mtx_right, dist_right, R2, P2, image_size, cv2.CV_32FC1)
    leftrec, rightrec, leftrec_re, rightrec_re = show(left_map, right_map, left_map_re, right_map_re, src)

    # 构建StereoSGBM参数
    stereo_sgbm = cv2.StereoSGBM_create(
        minDisparity=0,
        numDisparities=16 * 2,  # 这应该是16的倍数
        blockSize=5,
        P1=8 * 3 * 5**2,
        P2=32 * 3 * 5**2,
        disp12MaxDiff=1,
        uniquenessRatio=15,
        speckleWindowSize=0,
        speckleRange=2,
        mode=cv2.StereoSGBM_MODE_SGBM_3WAY
    )

    # 计算深度图
    disparity_map = stereo_sgbm.compute(leftrec, rightrec)
    disparity_map_re = stereo_sgbm.compute(leftrec_re, rightrec_re)

    # 将视差图转换为深度图
    baseline = 0.1  # 相机基线(baseline)值,需要根据实际情况调整
    focal_length = mtx_left[0][0]  # 相机焦距,需要根据实际情况调整
    depth_map = (baseline * focal_length) / disparity_map
    depth_map_re = (baseline * focal_length) / disparity_map_re

    # 显示深度图
    cv2.imshow("Depth Map", depth_map)
    cv2.imshow("Depth Map recov", depth_map_re)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

C++参考资料 1

/* -*-c++-*- StereoV3D - Copyright (C) 2021.
* Author	: Ethan Li<ethan.li.whu@gmail.com>
* https://github.com/ethan-li-coding/StereoV3DCode
*/
#include "essential_solver.h"

void sv3d::EssentialSolver::Solve(const Mat3X p1, const Mat3X p2, const Mat3 k1_mat, const Mat3 k2_mat, const SOLVE_TYPE& solver_type)
{
	assert(p1.cols() >= 8);
	assert(p1.rows() == p2.rows());
	assert(p1.cols() == p2.cols());
	
	// 通过内参矩阵k将p转换到x,x = k_inv*p
	Mat3X x1(3,p1.cols()), x2(3,p2.cols());

	x1 = k1_mat.inverse() * p1;
	x2 = k2_mat.inverse() * p2;

	// 求解
	Solve(x1, x2, solver_type);
}

void sv3d::EssentialSolver::Solve(const Mat3X x1, const Mat3X x2, const SOLVE_TYPE& solver_type)
{
	switch (solver_type) {
	case EIGHT_POINTS:
		Solve_EightPoints(x1, x2);
	default:
		break;
	}
}

sv3d::Mat3 sv3d::EssentialSolver::Value()
{
	return data_;
}

void sv3d::EssentialSolver::Solve_EightPoints(const Mat3X x1, const Mat3X x2)
{
	assert(x1.cols() >= 8);
	assert(x1.rows() == x2.rows());
	assert(x1.cols() == x2.cols());

	// 构建线性方程组的系数矩阵A
	auto np = x1.cols();
	RMatX9 a_mat(np, 9);
	for (int n = 0; n < np; n++) {
		const auto x1_x = x1.data()[3 * n];
		const auto x1_y = x1.data()[3 * n + 1];
		const auto x2_x = x2.data()[3 * n];
		const auto x2_y = x2.data()[3 * n + 1];
		const auto dat = a_mat.data() + 9 * n;
		dat[0] = x1_x * x2_x; dat[1] = x2_x * x1_y; dat[2] = x2_x;
		dat[3] = x1_x * x2_y; dat[4] = x1_y * x2_y; dat[5] = x2_y;
		dat[6] = x1_x; dat[7] = x1_y; dat[8] = 1;
	}
	
	// 求解ATA的最小特征值对应的特征向量即矢量 e
	Eigen::SelfAdjointEigenSolver<Eigen::Matrix<double,9, 9>> solver(a_mat.transpose()*a_mat);
	const Vec9 e = solver.eigenvectors().col(0);
	
	// 矢量 e 构造本质矩阵 E
	data_ = Eigen::Map<const RMat3>(e.data());
	
	// 调整 E 矩阵使满足内在性质:奇异值为[σ σ 0]
	Eigen::JacobiSVD<Mat3> usv(data_, Eigen::ComputeFullU | Eigen::ComputeFullV);
	auto s = usv.singularValues();
	const auto a = s[0];
	const auto b = s[1];
	s << (a + b) / 2.0, (a + b) / 2.0, 0.0;
	data_ = usv.matrixU() * s.asDiagonal() * usv.matrixV().transpose();
}


  1. https://github.com/ethan-li-coding/StereoV3DCode ↩︎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2093060.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ThermoParser 介绍

ThermoParser是一个工具包&#xff0c;用于简化专业材料科学代码产生的数据分析&#xff0c;以热电学为中心&#xff0c;但也适用于任何与电子和/或声子传输有关的内容。ThermoParser是一个Python库&#xff0c;它包含数据检索、操作和绘图的函数&#xff0c;只需几行代码就可以…

HashMap 链表转红黑树的阈值为何为 8

与一个重要的统计学原理——泊松分布密切相关&#xff1a;该原理阐明了在单位时间&#xff08;或面积、体积&#xff09;内&#xff0c;随机事件的平均发生次数遵循泊松分布 为什么这因子设定为0.5呢&#xff1f; 在忽略方差的情况下&#xff0c;哈希表容量占比的期望值约为 0.…

揭秘扩散模型:DDPM的数学基础与代码实现全攻略!

(DDPM) denoising diffusion probabilistic models 理论学习 本文价值 本文是 Diffusion 这一类模型的开山之作&#xff0c;首次证明 diffusion 模型能够生成高质量的图片&#xff0c;且奠定了所有后续模型的基本原理&#xff1a;加噪 --> 去噪。DDPM 模型的效果如下&#x…

springboot+vue+mybatis计算机毕业设计飞机订票系统+PPT+论文+讲解+售后

快速发展的社会中&#xff0c;人们的生活水平都在提高&#xff0c;生活节奏也在逐渐加快。为了节省时间和提高工作效率&#xff0c;越来越多的人选择利用互联网进行线上打理各种事务&#xff0c;然后线上管理系统也就相继涌现。与此同时&#xff0c;人们开始接受方便的生活方式…

IDEA向mysql写入中文字符时出现乱码问题

可参考该博客&#xff1a;https://www.cnblogs.com/bb1008/p/7704458.html 第一步是将IDEA软件中的编码方式全部改为utf8 File -> Settings -> Editor -> File Encodings 第二步是在数据库链接中加入 ?characterEncodingUTF-8

备战2024年全国大学生数学建模竞赛:蔬菜类商品的自动定价与补货决策

目录 一、引言 二、问题分析 三、解题思路 问题1&#xff1a;销售量分布规律及相互关系 问题2&#xff1a;品类级别的补货计划与定价策略 问题3&#xff1a;单品级别的补货计划与定价策略 问题4&#xff1a;补充数据的建议与分析 四、知识点解析 五、模型建立与求解 1…

没有永远免费的加速器,但是永远有免费的加速器【20240831更新】

没有永远免费的加速器&#xff0c;但是永远有免费的加速器【每日更新】 一、迅雷加速器&#xff08;免费时长最高38天&#xff09; 可免费时长&#xff1a;8天 如果是迅雷会员&#xff0c;则免费时长为38天 官网下载链接&#xff1a;迅雷加速器—迅雷官方出品&#xff0c;为快…

关于数字存储和byte[]数组的一些心得

前言 最近做项目&#xff0c;发现一些规律&#xff0c;比如数字的存储和字符串的存储&#xff0c;先说数字吧&#xff0c;最常见的整数&#xff0c;就涉及反码和补码&#xff0c;根据这些规则&#xff0c;甚至我们自己也能造一种数据存储结构&#xff0c;比如1个字节8bit&…

bbr 和 inflight 守恒的收敛原理

先看 bbr&#xff0c;以 2 条流 bw 收敛为例&#xff0c;微分方程组如下&#xff1a; { d x d t C ⋅ g ⋅ x g ⋅ x y − x d y d t C ⋅ g ⋅ y g ⋅ y x − y \begin{cases} \dfrac{dx}{dt}C\cdot\dfrac{g\cdot x}{g\cdot xy}-x\\\ \dfrac{dy}{dt}C\cdot\dfrac{g\cdot y…

秋风送爽,夏意未央|VELO Prevail Revo坐垫,一骑绿动起来吧~

夏末秋初&#xff0c;当第一片落叶缓缓飘落&#xff0c;是时候骑上你的自行车&#xff0c;迎接新的季节啦。带上维乐Prevail Revo坐垫&#xff0c;因为它独树一帜地采用EVA与回收咖啡渣精制而成的轻量发泡提升了减震性能&#xff0c;可以让你的每一次骑行都充满意义。    “…

基于DS18B20的温度检测

前言 DS18B20是DALLAS半导体公司生产的单总线数字温度传感器&#xff0c;其输出的是数字信号&#xff0c;具有体积小&#xff0c;功耗低&#xff0c;抗干扰能力强&#xff0c;精度高的特点。 温度范围-55摄氏度至125摄氏度&#xff0c;在-10摄氏度至85摄氏度可以达到不超过 0.5…

Redis进阶(五):集群

1.概念 集群&#xff0c;从广义来讲&#xff1a;只要是多个机器&#xff0c;构成了分布式系统&#xff0c;都可以称为一个集群 狭义的集群&#xff1a;redis提供的集群模式&#xff0c;这个集群模式之下&#xff0c;主要是要解决存储空间不足的问题&#xff08;拓展存储空间&…

分歧时间估计与被子植物的年代-文献精读43

Ad fontes: divergence-time estimation and the age of angiosperms 回归本源&#xff1a;分歧时间估计与被子植物的年代 摘要 准确的分歧时间对于解释和理解谱系演化的背景至关重要。在过去的几十年里&#xff0c;有关冠被子植物推测的分子年龄&#xff08;通常估计为晚侏罗…

Python入门全解析丨Part3-Python的循环语句

(一)while循环的基础应用 一.while的条件 >>>需要是布尔类型&#xff0c;True表示继续循环&#xff0c;False表示循环结束 >>>需要设置循环终止的条件 >>>需要空格缩进 二.while循环使用举例 sum0; i1; while i <100:sumi;i1; print(f&quo…

华阳珠珍娘娘宝圣祖庙文化董事会隆重举办

中国广东省汕头市华阳珠珍宝圣祖庙文化董事会隆重举办海内外两岸护婴女神妈祖回娘家启动仪式&#xff01;风暖日丽的初春天&#xff0c;纷纷鼓乐赛华阳。护婴妙化在此地&#xff0c;莲岛故里起瓣香。这首诗是清代华阳乡人游殷享&#xff0c;时任湖北郧西县知县赞叹赛护婴&#…

VLAN 基本配置

一. 实验拓扑 二. 实验简介 交换机的VLAN端口可以分为Access、Trunk和Hybrid3种类型。Access 端口是交换机上用来直接连接用户终端的端口&#xff0c;它只允许属于该端口的缺省VLAN的帧通过。Access端口发往用户终端的帧一定不带VLAN标签。Trunk端口是交换机上用来连接其他交…

【C语言】十六进制、二进制、字节、位

【C语言】十六进制、二进制、字节、位 文章目录 [TOC](文章目录) 前言一、十六进制、二进制、字节、位二、变量、指针、指针变量三、参考文献总结 前言 使用工具&#xff1a; 1.控制器&#xff1a;STM32F103C8T6 2.仿真器&#xff1a;STLINK 提示&#xff1a;以下是本篇文章正…

修改Apollo的依赖版本包,并制作arm版本的镜像

由于一些安全因素&#xff0c;Apollo组件扫描出一些依赖插件存在安全漏洞&#xff0c;因此要修改部分依赖组件的版本&#xff0c;重新制作镜像&#xff0c;我们来看一下如何实现 1. 修改源码 1.1 拉取源码&#xff0c;并切换到我们需要的分支 # 拉取源码项目 git clone gitgi…

TeamTalk消息服务器(群组相关)

具体的流程如下介绍&#xff0c;后续需要着重研究数据库相关表的结构设计。 群组信令和协议设计 enum GroupCmdID {CID_GROUP_NORMAL_LIST_REQUEST 1025,CID_GROUP_NORMAL_LIST_RESPONSE 1026,CID_GROUP_INFO_REQUEST 1027,CID_GROUP_INFO_RESPONSE 1028,// ...... 暂时省…

pm2 + linux + nginx

pm2 pm2是一个用于管理node项目的工具 前言 有如下两个文件 index.js const express require("express"); const app express(); const port 9999;app.get("/index", (req, res) > {res.json({code:200,msg:"songzx001"}) });app.lis…