【摄像头标定】双目摄像头标定及矫正-opencv(python)

双目摄像头标定及矫正

    • 棋盘格标定板
    • 标定
    • 矫正

棋盘格标定板

本文使用棋盘格标定板,可以到这篇博客中下载:https://blog.csdn.net/qq_39330520/article/details/107864568
在这里插入图片描述

标定

要进行标定首先需要双目拍的棋盘格图片,20张左右,由于本文的双目摄像头嵌入在开发板底板中,并且使用的是ros进行开发,所以对于大部分人拍照这里是没有参考价值的,对于也是使用ros开发的小伙伴,需要写一个节点发布双目摄像头的图像数据,然后再写一个节点订阅双目摄像头数据进行拍照保存。本文重点也不在拍照,对于其他小伙伴可以直接搜索一些适用的拍照方法,只要能获得到图片即可。
左摄像头图片如下:
在这里插入图片描述
右摄像头图片如下:
在这里插入图片描述
由于摄像头底层代码有问题,所以图像很暗,但不影响标定。
标定代码如下:

import cv2
import os
import numpy as np
import itertools
import yaml

# 定义文件夹路径
left_folder = "C:/new_pycharm_project/yolov10-main/shuangmu_left_pic"
right_folder = "C:/new_pycharm_project/yolov10-main/shuangmu_right_pic"

# 获取图像文件列表并排序
left_images = sorted(os.listdir(left_folder))
right_images = sorted(os.listdir(right_folder))

# 确保左右相机图像数量一致
assert len(left_images) == len(right_images), "左右相机图像数量不一致"

# 加载两个摄像头图片文件夹并将里面的彩图转换为灰度图
def load_images(folder, images):
    img_list = []
    for img_name in images:
        img_path = os.path.join(folder, img_name)
        frame = cv2.imread(img_path)
        if frame is not None:
            gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
            img_list.append((frame, gray))
        else:
            print(f"无法读取图像: {img_path}")
    return img_list



# 检测棋盘格角点
def get_corners(imgs, pattern_size):
    corners = []
    for frame, gray in imgs:
        ret, c = cv2.findChessboardCorners(gray, pattern_size)     #ret 表示是否成功找到棋盘格角点,c 是一个数组,包含了检测到的角点的坐标
        if not ret:
            print("未能检测到棋盘格角点")
            continue
        c = cv2.cornerSubPix(gray, c, (5, 5), (-1, -1),
                             (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001))     #cv2.cornerSubPix 函数用于提高棋盘格角点的精确度,对初始检测到的角点坐标 c 进行优化
        corners.append(c)      #将优化后的角点坐标 c 添加到 corners 列表中

        # 绘制角点并显示
        vis = frame.copy()
        cv2.drawChessboardCorners(vis, pattern_size, c, ret)
        new_size = (1280, 800)
        resized_img = cv2.resize(vis, new_size)
        cv2.imshow('Corners', resized_img)
        cv2.waitKey(150)

    return corners

# 相机标定
def calibrate_camera(object_points, corners, imgsize):
    cm_input = np.eye(3, dtype=np.float32)
    ret = cv2.calibrateCamera(object_points, corners, imgsize, cm_input, None)
    return ret

def save_calibration_to_yaml(file_path, cameraMatrix_l, distCoeffs_l, cameraMatrix_r, distCoeffs_r, R, T, E, F):
    data = {
        'camera_matrix_left': {
            'rows': 3,
            'cols': 3,
            'dt': 'd',
            'data': cameraMatrix_l.flatten().tolist()
        },
        'dist_coeff_left': {
            'rows': 1,
            'cols': 5,
            'dt': 'd',
            'data': distCoeffs_l.flatten().tolist()
        },
        'camera_matrix_right': {
            'rows': 3,
            'cols': 3,
            'dt': 'd',
            'data': cameraMatrix_r.flatten().tolist()
        },
        'dist_coeff_right': {
            'rows': 1,
            'cols': 5,
            'dt': 'd',
            'data': distCoeffs_r.flatten().tolist()
        },
        'R': {
            'rows': 3,
            'cols': 3,
            'dt': 'd',
            'data': R.flatten().tolist()
        },
        'T': {
            'rows': 3,
            'cols': 1,
            'dt': 'd',
            'data': T.flatten().tolist()
        },
        'E': {
            'rows': 3,
            'cols': 3,
            'dt': 'd',
            'data': E.flatten().tolist()
        },
        'F': {
            'rows': 3,
            'cols': 3,
            'dt': 'd',
            'data': F.flatten().tolist()
        }
    }

    with open(file_path, 'w') as file:
        yaml.dump(data, file, default_flow_style=False)
    print(f"Calibration parameters saved to {file_path}")



img_left = load_images(left_folder, left_images)      #img_left是个列表,存放左摄像头所有的灰度图片。
img_right = load_images(right_folder, right_images)
pattern_size = (8, 5)
corners_left = get_corners(img_left, pattern_size)       #corners_left的长度表示检测到棋盘格角点的图像数量。corners_left[i] 和 corners_right[i] 中存储了第 i 张图像检测到的棋盘格角点的二维坐标。
corners_right = get_corners(img_right, pattern_size)
cv2.destroyAllWindows()

# 断言,确保所有图像都检测到角点
assert len(corners_left) == len(img_left), "有图像未检测到左相机的角点"
assert len(corners_right) == len(img_right), "有图像未检测到右相机的角点"

# 准备标定所需数据
points = np.zeros((8 * 5, 3), dtype=np.float32)   #创建40 行 3 列的零矩阵,用于存储棋盘格的三维坐标点。棋盘格的大小是 8 行 5 列,40 个角点。数据类型为 np.float32,这是一张图的,因为一个角点对应一个三维坐标
points[:, :2] = np.mgrid[0:8, 0:5].T.reshape(-1, 2) * 21  #给这些点赋予实际的物理坐标,* 21 是因为每个棋盘格的大小为 21mm

object_points = [points] * len(corners_left)     #包含了所有图像中棋盘格的三维物理坐标点 points。这里假设所有图像中棋盘格的物理坐标是相同的,因此用 points 复制 len(corners_left) 次。
imgsize = img_left[0][1].shape[::-1]     #img_left[0] 是左相机图像列表中的第一张图像。img_left[0][1] 是该图像的灰度图像。shape[::-1] 取灰度图像的宽度和高度,并反转顺序,以符合 calibrateCamera 函数的要求。

print('开始左相机标定')
ret_l = calibrate_camera(object_points, corners_left, imgsize)    #object_points表示标定板上检测到的棋盘格角点的三维坐标;corners_left[i]表示棋盘格角点在图像中的二维坐标;imgsize表示图像大小
retval_l, cameraMatrix_l, distCoeffs_l, rvecs_l, tvecs_l = ret_l[:5]    #返回值里就包含了标定的参数

print('开始右相机标定')
ret_r = calibrate_camera(object_points, corners_right, imgsize)
retval_r, cameraMatrix_r, distCoeffs_r, rvecs_r, tvecs_r = ret_r[:5]

# 立体标定,得到左右相机的外参:旋转矩阵、平移矩阵、本质矩阵、基本矩阵
print('开始立体标定')
criteria_stereo = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-5)
ret_stereo = cv2.stereoCalibrate(object_points, corners_left, corners_right,
                                 cameraMatrix_l, distCoeffs_l,
                                 cameraMatrix_r, distCoeffs_r,
                                 imgsize, criteria=criteria_stereo,
                                 flags=cv2.CALIB_FIX_INTRINSIC)
ret, _, _, _, _, R, T, E, F = ret_stereo

# 输出结果
print("左相机内参:\n", cameraMatrix_l)
print("左相机畸变系数:\n", distCoeffs_l)
print("右相机内参:\n", cameraMatrix_r)
print("右相机畸变系数:\n", distCoeffs_r)
print("旋转矩阵 R:\n", R)
print("平移向量 T:\n", T)
print("本质矩阵 E:\n", E)
print("基本矩阵 F:\n", F)
print("标定完成")

# 保存标定结果
save_calibration_to_yaml('calibration_parameters.yaml', cameraMatrix_l, distCoeffs_l, cameraMatrix_r, distCoeffs_r, R, T, E, F)


# 计算重投影误差
def compute_reprojection_errors(objpoints, imgpoints, rvecs, tvecs, mtx, dist):
    total_error = 0
    total_points = 0
    for i in range(len(objpoints)):
        imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
        error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2)
        total_error += error
        total_points += len(imgpoints2)
    mean_error = total_error / total_points
    return mean_error

# 计算并打印左相机和右相机的重投影误差
print("左相机重投影误差: ", compute_reprojection_errors(object_points, corners_left, rvecs_l, tvecs_l, cameraMatrix_l, distCoeffs_l))
print("右相机重投影误差: ", compute_reprojection_errors(object_points, corners_right, rvecs_r, tvecs_r, cameraMatrix_r, distCoeffs_r))

# 立体矫正和显示
def stereo_rectify_and_display(img_l, img_r, cameraMatrix_l, distCoeffs_l, cameraMatrix_r, distCoeffs_r, R, T):
    img_size = img_l.shape[:2][::-1]

    # 立体校正
    R1, R2, P1, P2, Q, _, _ = cv2.stereoRectify(cameraMatrix_l, distCoeffs_l, cameraMatrix_r, distCoeffs_r, img_size, R, T)
    map1x, map1y = cv2.initUndistortRectifyMap(cameraMatrix_l, distCoeffs_l, R1, P1, img_size, cv2.CV_32FC1)
    map2x, map2y = cv2.initUndistortRectifyMap(cameraMatrix_r, distCoeffs_r, R2, P2, img_size, cv2.CV_32FC1)

    # 图像矫正
    rectified_img_l = cv2.remap(img_l, map1x, map1y, cv2.INTER_LINEAR)
    rectified_img_r = cv2.remap(img_r, map2x, map2y, cv2.INTER_LINEAR)

    # 显示矫正后的图像
    combined_img = np.hstack((rectified_img_l, rectified_img_r))
    cv2.imshow('Rectified Images', combined_img)
    cv2.imwrite("stereo_jiaozheng.png",combined_img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

# 加载并矫正示例图像
example_idx = 0
img_l = img_left[example_idx][0]
img_r = img_right[example_idx][0]
stereo_rectify_and_display(img_l, img_r, cameraMatrix_l, distCoeffs_l, cameraMatrix_r, distCoeffs_r, R, T)

标定完成后会显示一张矫正后的图像。代码重要的地方都给出了注释,主要流程就是分别对左右相机进行标定,然后对两个相机进行联合标定(立体标定),最后得到的参数会保存到yaml文件中:

---
camera_matrix_left:
  rows: 3
  cols: 3
  dt: d
  data:
    - 531.7200210313852
    - 0
    - 642.0170539101581
    - 0
    - 533.6471323984354
    - 420.4033045027399
    - 0
    - 0
    - 1
dist_coeff_left:
  rows: 1
  cols: 5
  dt: d
  data:
    - -0.1670007968198256
    - 0.04560028196221921
    - 0.0011938487550718078
    - -0.000866537907860316
    - -0.00805042100882671
camera_matrix_right:
  rows: 3
  cols: 3
  dt: d
  data:
    - 525.9058345430292
    - 0
    - 628.7761214904813
    - 0
    - 528.2078922687268
    - 381.8575789135264
    - 0
    - 0
    - 1
dist_coeff_right:
  rows: 1
  cols: 5
  dt: d
  data:
    - -0.15320688387351564
    - 0.03439886104586617
    - -0.0003732170677440928
    - -0.0024909528446780153
    - -0.005138400994014348
R:
  rows: 3
  cols: 3
  dt: d
  data:
    - 0.9999847004116569
    - -0.00041406631566505544
    - 0.005516112008926496
    - 0.0003183979929468572
    - 0.9998497209492369
    - 0.017333036100216304
    - -0.005522460079247196
    - -0.017331014592906722
    - 0.9998345554979852
T:
  rows: 3
  cols: 1
  dt: d
  data:
    - -55.849260376265015
    - 2.1715925432988743
    - 0.46949841441903933
E:
  rows: 3
  cols: 3
  dt: d
  data:
    - -0.012142020481601675
    - -0.5070637607007459
    - 2.1630954322858496
    - 0.1610659204031652
    - -0.9681187500627653
    - 55.84261022903612
    - -2.189341611238282
    - -55.83996821910631
    - -0.9800159939787676
F:
  rows: 3
  cols: 3
  dt: d
  data:
    - -2.4239149875305048e-8
    - -0.0000010085973649868748
    - 0.0027356495714066175
    - 3.2013501988129346e-7
    - -0.0000019172863951399893
    - 0.05961765359743852
    - -0.002405523166325036
    - -0.057046539240958545
    - 1

分别是左相机的内参矩阵、畸变系数,右相机的内参矩阵和畸变系数,两个相机之间的旋转矩阵、平移矩阵、本质矩阵、基本矩阵。

矫正

import cv2
import yaml
import numpy as np

# 定义函数读取标定数据
def read_calibration_data(calibration_file):
    with open(calibration_file, 'r') as f:
        calib_data = yaml.safe_load(f)
        cameraMatrix_l = np.array(calib_data['camera_matrix_left']['data']).reshape(3, 3)
        distCoeffs_l = np.array(calib_data['dist_coeff_left']['data'])
        cameraMatrix_r = np.array(calib_data['camera_matrix_right']['data']).reshape(3, 3)
        distCoeffs_r = np.array(calib_data['dist_coeff_right']['data'])
        R = np.array(calib_data['R']['data']).reshape(3, 3)
        T = np.array(calib_data['T']['data']).reshape(3, 1)
    return cameraMatrix_l, distCoeffs_l, cameraMatrix_r, distCoeffs_r, R, T

# 定义函数对图像进行矫正
def rectify_images(left_image_path, right_image_path, calibration_file):
    # 读取标定数据
    cameraMatrix_l, distCoeffs_l, cameraMatrix_r, distCoeffs_r, R, T = read_calibration_data(calibration_file)

    # 读取左右图像
    img_left = cv2.imread(left_image_path)
    img_right = cv2.imread(right_image_path)

    # 获取图像尺寸(假设左右图像尺寸相同)
    img_size = img_left.shape[:2][::-1]

    # 立体校正
    R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify(cameraMatrix_l, distCoeffs_l,
                                                     cameraMatrix_r, distCoeffs_r,
                                                     img_size, R, T)

    # 计算映射参数
    map1_l, map2_l = cv2.initUndistortRectifyMap(cameraMatrix_l, distCoeffs_l, R1, P1, img_size, cv2.CV_32FC1)
    map1_r, map2_r = cv2.initUndistortRectifyMap(cameraMatrix_r, distCoeffs_r, R2, P2, img_size, cv2.CV_32FC1)

    # 应用映射并显示结果
    rectified_img_l = cv2.remap(img_left, map1_l, map2_l, cv2.INTER_LINEAR)
    rectified_img_r = cv2.remap(img_right, map1_r, map2_r, cv2.INTER_LINEAR)

    # 合并图像显示
    combined_img = np.hstack((rectified_img_l, rectified_img_r))
    cv2.imshow('Rectified Images', combined_img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

# 设置路径和文件名
left_image_path = "C:/new_pycharm_project/yolov10-main/shuangmu_left_pic/left_image0.png"
right_image_path = "C:/new_pycharm_project/yolov10-main/shuangmu_right_pic/right_image0.png"
calibration_file = "C:/new_pycharm_project/yolov10-main/calibration_parameters.yaml"

# 调用函数进行图像矫正
rectify_images(left_image_path, right_image_path, calibration_file)

结果对比:
在这里插入图片描述
在这里插入图片描述
第一张是矫正前的左右相机图像,第二张是矫正后的。可以看到去除了畸变,并且两图像基本出于同一水平线。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

易天智能eHR管理平台 CreateUser 任意用户添加漏洞复现

0x01 产品简介 易天智能eHR管理平台是一款功能全面、智能化的人力资源管理软件,旨在帮助企业提高人力资源管理效率和管理水平。该平台通过集成员工信息、薪酬管理、档案人事管理、绩效管理和招聘管理等多个模块,实现了人力资源管理的全面智能化管理。 0x02 漏洞概述 易天智…

Windows11环境下安装Vmware Workstation 16的方法

1、下载VMWare 从网盘下载 https://pan.baidu.com/share/init?surlUpcnqiRv6nUuzO0EOZ22zg 提取码:8888 2、安装VMware虚拟机   第1步:双击上面准备好的Vmware Workstation 16虚拟机软件安装包,即可看到如图所示的安装向导初始界面&#x…

为什么嵌入式驱动开发工程师可以拿高薪?

嵌入式驱动开发是技术密集型的工作。想象一下,每一个硬件设备都需要与之匹配的驱动程序,才能在操作系统中正常工作。刚好我有一些资料,是我根据网友给的问题精心整理了一份「嵌入式的资料从专业入门到高级教程」, 点个关注在评论…

【知识学习】阐述Unity3D中MaterialTexture的概念及使用方法示例

在Unity3D中,Material和Texture是渲染过程中非常重要的两个概念,它们共同工作以实现丰富的视觉效果。 Material Material是Unity中的一个组件,用于定义物体表面的视觉属性。一个Material可以包含多种属性,如颜色、纹理、反射率等…

道路救援入驻派单小程序开源版开发

道路救援入驻派单小程序开源版开发 1、用户立即救援 2、后台收到救援通知,派单救援师傅. 道路救援入驻派单小程序通常会包含一系列功能,旨在方便救援服务提供商、用户和后台管理系统之间的交互。以下是一个可能的功能列表: 用户端功能&…

第1章 物联网模式简介---独特要求和体系结构原则

物联网用例的独特要求 物联网用例往往在功耗、带宽、分析等方面具有非常独特的要求。此外,物联网实施的固有复杂性(一端的现场设备在计算上受到挑战,另一端的云容量几乎无限)迫使架构师做出艰难的架构决策和实施选择。可用实现技…

秋招Java后端开发冲刺——非关系型数据库篇(MongoDB)

MongoDB 本文介绍非关系型数据库MongoDB的基础知识和常见面试题。 (一)基础知识 1. 介绍:MongoDB是一个基于分布式文件存储的数据库,由C语言编写,旨在为WEB应用提供可扩展的高性能数据存储解决方案。 2.特点 特点…

Rust日常开发三方库精选

日常开发三方库精选 对计算机、编程、架构的理解决定一个程序员的上限,而工具则决定了他的下限,三尺森寒利剑在手,问世间谁敢一战。 本文就分门别类的精心挑选了一些非常适合日常开发使用的三方库,同时针对优缺点、社区活跃等进…

聚类距离度量(保姆级讲解,包学会~)

在机器学习的聚类中,我们通常需要使用距离来进行类的划分,或者比较不同类之间的各种距离,这里我们介绍西瓜书上所提出的一些距离计算方式。 首先介绍一下距离的一些性质: 西瓜书上给出了四条性质,第一个是非负性&#…

《高考择校择专业:权衡与抉择的智慧》

分数限制下,选好专业还是选好学校? 2024 年高考的大幕已然落下,然而对于众多考生而言,新的挑战才刚刚开始。在分数既定的情况下,是优先选择心仪的专业,还是更看重知名度高的学校?这无疑是一个令…

BW:CP里添加信息对象小问题记录

之前做视图直接添加进CP里,以为不能直接往CP里加信息对象,还专门建了一个带信息对象的模型,把信息对象拖到CP里,然后再链接视图的字段 今天发现原来不用这样,直接加就可以,小记一下 如图直接诶创建&#x…

网络安全学习路线图(2024版详解)

近期,大家在网上对于网络安全讨论比较多,想要学习的人也不少,但是需要学习哪些内容,按照什么顺序去学习呢?其实我们已经出国多版本的网络安全学习路线图,一直以来效果也比较不错,本次我们针对市…

uniapp横屏移动端卡片缩进轮播图

uniapp横屏移动端卡片缩进轮播图 效果&#xff1a; 代码&#xff1a; <!-- 简单封装轮播图组件:swiperCard --> <template><swiper class"swiper" circular :indicator-dots"true" :autoplay"true" :interval"10000&quo…

红队内网攻防渗透:内网渗透之内网对抗:横向移动篇Kerberos委派安全RBCD资源Operators组成员HTLMRelay结合

红队内网攻防渗透 1. 内网横向移动1.1 横向移动-资源约束委派-利用域用户主机加入1.1.1 利用思路1.1.2 利用条件1.1.3 利用过程1.2 横向移动-资源约束委派-Acount Operators组1.2.1 利用思路:1.2.2 利用条件:1.2.3 利用过程:1.3 横向移动-资源约束委派-CVE结合HTLMRelay1.3.…

Echarts 图表添加点击事件跳转页面,但只有图表部分点击才会跳转页面,坐标轴,区域缩放等点击不跳转。

默认的点击事件是这样的&#xff1a; myChart.on(click, function (param) {console.log(param) }) 这个事件需要点击具体图形才会触发&#xff0c;例如我上面的图&#xff0c;想选择a柱子&#xff0c;就需要明确点击到柱体才行&#xff0c;明显不符合正常的预期&#xff0c;正…

SolidWorks薄壁等厚实体转换成钣金方法

1. 打开SolidWorks软件&#xff0c;新建一个零件。选前视基准面绘制草图&#xff0c;二次创建凸台拉伸特征&#xff0c;如图所示。 2. 创建抽壳特征&#xff0c;厚度“2 mm”&#xff0c;如图所示。 3. 添加切口草图&#xff0c;根据钣金加工工艺在所选面上创建切口草图&#x…

SAP-SD-修改字段描述

在销售订单中,想修改某字段名的描述,以客户组12为例,如下图 现在想把这个字段修改为客户组1,选择-F1 双击“数据元素” 双击 域 转到-翻译

如何提升投资伦敦金的分析能力:回测

对伦敦金行情的分析能力&#xff0c;在我们做伦敦金交易的整个体系中是有很重要的作用的。但要提升对伦敦金行情的分析和把握是不太容易的&#xff0c;那有没有方法可以提升&#xff1f;那答案是有的&#xff0c;下面我们就来讨论一下&#xff0c;这个方法就是回测。 回测就是把…

Hi3861 OpenHarmony嵌入式应用入门--LiteOS semaphore作为锁

CMSIS 2.0 接口中的 Semaphore&#xff08;信号量&#xff09;是用于嵌入式系统中多线程或中断服务例程&#xff08;ISR&#xff09;之间同步和共享资源保护的重要机制。Semaphore 是一种用于控制对多个共享资源访问的同步机制。它可以被看作是一个计数器&#xff0c;用于跟踪可…

准备篇(三)网页相关知识

Java script小脚本 - 爬取 bilibili 表情Java script 小脚本 - 爬取 bilibili 表情 随便点开一个视频,注意这个页面 URL 对应的 HTML 代码中没有表情的代码, 需要先点一下评论区,然后再在这个页面 URL 对应的元素中找到表情所在的源码。(但是我不知道这个带表情 <pic…