Bootstrap

相机标定及C++实现

1.简介

相机标定(Camera Calibration)是计算机视觉中的一项重要技术,其目的就是确定相机的内参和外参。它可以修正镜头畸变,如桶形畸变或枕形畸变,以获得更接近真实世界的图像;在机器人视觉系统中,准确的相机标定可以提高导航和物体识别的精度。

以下都只是简单介绍一下内参和外参,数学推导有些复杂,可以参考c++opencv相机标定(calibrateCamera)详解 - 知乎

2.内参和外参

2.1 内参

内参(Intrinsic Parameters)是指相机的内在特性,包括相机的焦距(focal length)、主点(principal point,通常是相机感光元件的中心点)以及径向和切向畸变系数。这些参数定义了3D世界坐标到2D图像坐标的映射关系。内参可以通过相机标定过程获得,并用于图像的畸变校正。概括:内参描述了相机坐标系和像素坐标系之间的映射关系。数学形式如下:

z\begin{bmatrix} u\\v\\1 \end{bmatrix}= \begin{bmatrix} f_x && 0 && c_x\\ 0 && f_y && c_y\\ 0 && 0 && 1 \end{bmatrix} \begin{bmatrix}x_c\\y_c\\z_c \end{bmatrix}

其中,f_x,f_y分别是x,y方向上的焦距,c_x,c_y是图像坐标原点到像素坐标原点平移的距离。其实从图像坐标到像素坐标也就是一个平移和缩放的事。

2.2 外参

外参(Extrinsic Parameters)描述了相机在世界坐标系中的位置和方向,即确定了世界坐标系与相机坐标系之间的旋转和平移关系。外参可以用来将3D点的坐标转换为相机坐标系中的坐标。

即点p从世界坐标(x_w,y_w,z_w)到相机坐标(x_c,y_c,z_c)的变换经历了旋转R和平移t,齐次表达式如下

\begin{bmatrix} x_c\\ y_c\\ z_c \end{bmatrix}=[R|t] \begin{bmatrix} x_w\\y_w\\z_w\\1 \end{bmatrix}

其中R是3\times3的旋转矩阵,t3\times 1的平移矩阵。

3.张正友标定法

张正友标定法是一种广泛使用的相机标定技术,这种标定方式主要目的是确定相机的内参和畸变参数,以便能够将图像中的点准确地映射到现实世界中的点。

3.1基本原理

张正友标定法利用一个具有已知几何图案的标定版(通常是棋盘格),通过拍摄标定板从不同角度得到多组图像。在每组图像中,使用计算机视觉算法检测并提取标定板上的角点的像素坐标。同时,根据标定板的物理尺寸和格子大小,计算出这些角点在世界坐标系中的理论位置。

3.2 标定步骤

  1. 拍摄标定板图像:准备一个棋盘格标定板,并从不同的角度拍摄得到一组图像。
  2. 角点检测:对每张图像进行处理,检测出棋盘格的角点,并提取其像素坐标。
  3. 求解内参和外参:使用检测到的角点像素坐标和对应的世界坐标,通过数学优化方法求解相机的内参矩阵和畸变系数。
  4. 畸变系数标定:计算径向畸变系数,这些参数描述了图像中的畸变效果。
  5. 参数优化:使用Levenberg-Marquardt等算法对求解得到的参数进行优化,以提高标定的准确性。

标定过程可以使用如OpenCV这样的计算机视觉库来简化。OpenCV提供了cv::calibrateCamera函数,它可以根据提供的角点信息自动计算相机的内参和畸变系数。

4.C++实现

以下代码均在ubuntu20.04上验证通过,使用的OpenCV C++版本为3.4.15。

首先需要创建如下图所示的文件目录

parameters.txt文件是最后写入的内参和畸变系数,现在可以不必理会,具体其他文件夹的作用可以参考我的另一篇博客ORB算法及C++实现-CSDN博客,calibration.cpp中源代码如下:

#include<opencv2/opencv.hpp>
#include<iostream>
#include<fstream>
#include<string>
using namespace std;

int main(int argc, char **argv)
{
    // 定义棋盘格的尺寸,这里假设棋盘格是 8x6
    int boardWidth = 8;
    int boardHeight = 6;
    // 定义棋盘格每个方格的大小,单位是米
    float squareSize = 1.f;
    // 创建棋盘格尺寸的 cv::Size 对象
    cv::Size boardSize(boardWidth, boardHeight);

    // 存储棋盘格角点对应的 3D 点
    vector<vector<cv::Point3f>> objectPoints;
    // 存储图像中检测到的棋盘格角点的 2D 点
    vector<vector<cv::Point2f>> imagePoints;
    // 临时存储当前图像中检测到的棋盘格角点
    vector<cv::Point2f> corners;

    // 声明图像的 cv::Mat 对象
    cv::Mat image, gray;
    // 创建一个窗口来显示图像
    cv::namedWindow("image", cv::WINDOW_NORMAL);
    // 存储图像文件名的 vector
    vector<cv::String> filNames;
    // 使用 glob 函数获取指定格式的图像文件名
    glob("./../images/left*.jpg", filNames);

    // 遍历所有图像文件
    for(size_t i = 0; i < filNames.size(); i++)
    {
        // 读取当前图像文件
        image = cv::imread(filNames[i], cv::IMREAD_COLOR);
        // 将图像从 BGR 颜色空间转换为灰度空间
        cvtColor(image, gray, cv::COLOR_BGR2GRAY);

        // 使用 findChessboardCorners 函数检测棋盘格角点
        // 第二个参数是棋盘格的尺寸
        // 第三个参数是输出的角点集
        // 第四个参数是检测棋盘格时使用的参数
        bool found = cv::findChessboardCorners(image, boardSize, corners,
         CV_CALIB_CB_ADAPTIVE_THRESH + CV_CALIB_CB_NORMALIZE_IMAGE + CV_CALIB_CB_FAST_CHECK);
        
        // 如果角点被成功检测到
        if(found)
        {
            // 使用 cornerSubPix 对角点位置进行精确化处理
            cv::cornerSubPix(gray, corners, cv::Size(11,11), cv::Size(-1,-1), cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::COUNT, 30, 0.1));
            // 在图像上绘制棋盘格角点
            cv::drawChessboardCorners(image, boardSize, corners, found);
            // 显示处理后的图像
            cv::imshow("image",image);
            // 等待用户按键操作
            cv::waitKey(0);

            // 为每个 3D 点生成棋盘格角点的 vector
            vector<cv::Point3f> objectCorners;
            // 填充 objectCorners,每个棋盘格角点的 Z 坐标为 0.0f
            for(int j = 0; j < boardHeight; j++)
            {
                for(int k = 0; k < boardWidth; k++)
                {
                    objectCorners.push_back(cv::Point3f(k * squareSize, j * squareSize, 0.0f));
                }
            }
            // 将 objectCorners 添加到 objectPoints 中
            objectPoints.push_back(objectCorners);
            // 将检测到的 2D 角点 corners 添加到 imagePoints 中
            imagePoints.push_back(corners);
        }
    }

    // 声明相机内参矩阵和畸变系数向量
    cv::Mat cameraMatrix, distCoeffs;
    // 声明存储每个图像的旋转向量和平移向量的 vector
    vector<cv::Mat> rvecs, tvecs;
    // 使用 calibrateCamera 函数计算相机的内参和畸变系数
    // objectPoints 和 imagePoints 是之前存储的 3D 和 2D 角点
    // image.size() 是图像的尺寸
    // rvecs 和 tvecs 存储每个图像的旋转和平移向量
    cv::calibrateCamera(objectPoints, imagePoints, image.size(), cameraMatrix, distCoeffs, rvecs, tvecs);
    // 输出相机内参矩阵
    cout<<"camera matrix:"<<endl<<cameraMatrix<<endl;
    // 输出畸变系数
    cout<<"Distortion coefficients:"<<endl<<distCoeffs<<endl;

    //将参数写入parameters.txt文件中

    ofstream outfile("./../parameters.txt");  //打开一个文件流,准备写入

    if(!outfile.is_open())  //检查文件是否成功打开
    {
        cerr<<"Failed to open the output file!"<<endl;
        return -1;
    }

    try{
        //写入相机内参矩阵
        outfile<<"Camera Matrix:"<<endl<<cameraMatrix<<endl;
        //写入畸变系数
        outfile<<"Distortion Coefficients:"<<endl<<distCoeffs<<endl;
    }

    catch(exception& e)
    {
        cerr<<"An exception occurred!"<<endl;
        cerr<<e.what()<<endl;
        return -1;
    }
    
    outfile.close();  //关闭文件流

    return 0;
}

编写CMakeLists.txt文件,代码如下:

#设置当前cmake运行的最低版本
cmake_minimum_required(VERSION 3.2)

# 设置项目名称
project(calibration)

# 设置C++标准为11
set(CMAKE_CXX_STANDARD 11)

# 查找OpenCV库
find_package(OpenCV REQUIRED)

#包含OpenCV的头文件
include_directories(${OpenCV_INCLUDE_DIRS})

# 添加可执行文件
add_executable(calibration src/calibration.cpp)

# 链接OpenCV库

target_link_libraries(calibration ${OpenCV_LIBS})

在build文件夹下通过终端运行以下命令:

cmake ..
make

编译结果如下:

运行结果:

更多详细的信息可以参考(1.简介)里面的知乎链接。

;