如何进行线结构光与单目相机的联合标定

本文介绍了一种利用棋盘格图像进行特征点检测与3D重建的方法。通过OpenCV实现棋盘格角点的自动定位,并利用点条纹技术获取深度信息,最终完成特征点的三维坐标计算。

实验室同学一起劳动的成果,后期进行理论说明

#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/calib3d/calib3d.hpp>
//#include <opencv2/nonfree/nonfree.hpp>


#include <pcl/io/pcd_io.h>
#include <pcl/point_types.h>
#include <pcl/visualization/cloud_viewer.h>
#include <pcl/filters/radius_outlier_removal.h>
#include <pcl/filters/conditional_removal.h>
#include <pcl/filters/statistical_outlier_removal.h>
#include <pcl/ModelCoefficients.h>
#include <pcl/point_types.h>
#include <pcl/sample_consensus/method_types.h>
#include <pcl/sample_consensus/model_types.h>
#include <pcl/segmentation/sac_segmentation.h>


#include <PeakFinder.h>
#include <math.h>       /* sin */


#include "pcl_ros/point_cloud.h"


using namespace std;
using namespace cv;


typedef pcl::PointXYZRGB PointT;
typedef pcl::PointCloud<PointT> PointCloud;


pcl::visualization::CloudViewer viewer ("Simple Cloud Viewer");


#define fx 1204.696659
#define fy 1204.299368
#define cx 966.428218
#define cy 559.772423
#define image_width 1920
#define image_height 1080
#define theta 0.00    //in radians
#define baseline 0.1000


int main(int argc, char** argv)
{
    cv::Mat black;
    cv::Mat frame;
    cv::Mat temp;
    cv::Mat diff;


    cv::Mat imageGrayblack;
    cv::Mat imageGray;


    cv::Mat imgundistortedblack;
    cv::Mat imgundistorted;


    PointCloud::Ptr cloud ( new PointCloud );
    vector<Point3f> point3d;
    double scale=100000.0;






    float col[image_width/2]={0.0};
    soundtouch::PeakFinder pf;
    double pp=0.0;           //accurate peak postion


    cv::Matx33f K (fx,0,cx,
                   0,fy,cy,
                   0,0,1);    //calibration matrix


    vector<float> distCoeffs = {-0.400622, 0.134591, 0.001135, -0.001642, 0.000000};//c++ 11 in CMakeLists.txt!!!
    cv::Size patternsize(11,8); //interior number of corners
    vector<Point2f> corners; //this will be filled by the detected corners
    bool patternfound=false;
    int num=270;//0.25*1080
    vector<double> vpp(image_height,0.0);


    for(int kk=0;kk<3;kk++)
    {
        if(kk==0)
        {
            frame=cv::imread("/home/kylefan/Pictures/Webcam/2017-03-10-140619.jpg");
            black=cv::imread("/home/kylefan/Pictures/Webcam/2017-03-10-140626.jpg");
        }
        if(kk==1)
        {
            frame=cv::imread("/home/kylefan/Pictures/Webcam/2017-03-10-141006.jpg");
            black=cv::imread("/home/kylefan/Pictures/Webcam/2017-03-10-141014.jpg");
        }
        if(kk==2)
        {
            frame=cv::imread("/home/kylefan/Pictures/Webcam/2017-03-10-140913.jpg");
            black=cv::imread("/home/kylefan/Pictures/Webcam/2017-03-10-140921.jpg");
        }


        cvtColor(frame, imageGray, CV_BGR2GRAY);
        cvtColor(black, imageGrayblack, CV_BGR2GRAY);


        cv::undistort(imageGray,imgundistorted,K, distCoeffs, K );
        cv::undistort(imageGrayblack,imgundistortedblack,K, distCoeffs, K );


        cv::absdiff(imgundistorted,imgundistortedblack,diff);


        cv::GaussianBlur(imgundistorted,imgundistorted,cv::Size(5,5),2,2);


    //1.find corners in image without stripe
        //CALIB_CB_FAST_CHECK saves a lot of time on images
        //that do not contain any chessboard corners
        patternfound = findChessboardCorners(imgundistortedblack, patternsize, corners,CALIB_CB_ADAPTIVE_THRESH);
        cout<<"patternfound:"<<patternfound<<endl;
        if(patternfound)cornerSubPix(imgundistortedblack, corners, Size(11, 11), Size(-1, -1),TermCriteria(CV_TERMCRIT_EPS + CV_TERMCRIT_ITER, 30, 0.1));
        drawChessboardCorners(imgundistortedblack, patternsize, Mat(corners), patternfound);
        //for(int i=0;i<corners.size();i++)cout<<"("<<corners[i].x<<","<<corners[i].y<<")"<<endl;


    //2,calculate 3-d of corners in camera coordinate.remember :2d corners must be in the undistorted images !!!!!!!  .but why?
        for(int i=0;i<corners.size();i++)
        {
            Point3f p3d;
            //cv::Matx31f point (corners[i].x-image_width/2,image_height/2-corners[i].y,1);
            cv::Matx31f point (corners[i].x,corners[i].y,1);
            cv::Mat_<double> point3=cv::Mat(K.inv())*cv::Mat(point);


            //cout<<"K.inv()="<<K.inv()<<endl;
            p3d.x=point3.at<double>(0,0);
            p3d.y=point3.at<double>(1,0);
            p3d.z=point3.at<double>(2,0);
            point3d.push_back(p3d);
        }


    //3,calculate scale
        double distance = cv::norm(point3d[0]-point3d[10]);
        //cout<<"distance="<<distance<<endl;
        scale=distance/0.200;


        for(int i=0;i<corners.size();i++)
        {
            point3d[i].x/=scale;
            point3d[i].y/=scale;
            point3d[i].z/=scale;
            //cout<<point3d[i].x<<","<<point3d[i].y<<","<<point3d[i].z<<endl;
        }




    //4,get stripe point


        for(int i=num;i<2*num;i++)
        {
            for(int j=image_width/2;j<image_width;j++)
            {
                col[j-image_width/2]=(float)(diff.at<uchar>(i,j));
            }
            pp=pf.detectPeak(col,0,image_width/2);
            diff.at<uchar>(i,(int)pp+image_width/2)=0;
            PointT p;
            //cv::Matx31f point (pp+(double)image_width/2.0-image_width/2,image_height/2-(i+num),1);
            cv::Matx31f point (pp+(double)image_width/2.0,i+num,1);
            cv::Mat_<double> point3=cv::Mat(K.inv())*cv::Mat(point);
            p.x=point3.at<double>(0,0)/scale;
            p.y=point3.at<double>(1,0)/scale;
            p.z=point3.at<double>(2,0)/scale;


            //cout<<p.x<<","<<p.y<<","<<p.z<<endl;


            p.r=55;
            p.g=250;
            p.b=55;
            cloud->push_back(p);
        }
        cout<<"cloud num="<<cloud->width<<endl;
        point3d.clear();
        corners.clear();
        patternfound=false;
        scale=100000.0;


    }


    pcl::PCLHeader header;
    header.frame_id="map";
    cloud->header=header;
    cloud->height = 1;
    cloud->width = cloud->points.size();
    cloud->is_dense = false;
    cloud->points.resize (cloud->width * cloud->height);


    pcl::ModelCoefficients::Ptr coefficients (new pcl::ModelCoefficients);
    pcl::PointIndices::Ptr inliers (new pcl::PointIndices);
    // Create the segmentation object
    pcl::SACSegmentation<PointT> seg;
    // Optional
    seg.setOptimizeCoefficients (true);
    // Mandatory
    seg.setModelType (pcl::SACMODEL_PLANE);
    seg.setMethodType (pcl::SAC_RANSAC);
    seg.setDistanceThreshold (0.01);


    seg.setInputCloud (cloud);
    seg.segment (*inliers, *coefficients);


    if (inliers->indices.size () == 0)
    {
      PCL_ERROR ("Could not estimate a planar model for the given dataset.");
      return (-1);
    }


    std::cerr << "Plane : " <<coefficients->values[0] << "x+"<< coefficients->values[1] << "y+"<< coefficients->values[2] << "z+"<< coefficients->values[3] <<"=0"<< std::endl;
    viewer.showCloud(cloud);
    while (!viewer.wasStopped ());
}

http://blog.youkuaiyun.com/fk1174/article/details/61201231

<think>我们面对的问题是如何联合标定一个已经标定好的双目相机和另一个单目相机标定的核心在于求解两个相机之间的相对位姿(旋转矩阵R和平移向量t)。由于双目相机已经标定,我们可以利用其已知的内部参数和相对位姿。 主要思路: 1. 获取双目相机的内外参数:包括左右相机的内参矩阵K_left, K_right,以及左右相机之间的外参(R_lr, t_lr)。 2. 将双目相机作为一个整体,我们通常以左相机为参考坐标系。现在要标定的是这个双目相机(左相机坐标系)另一个单目相机(我们称之为cam2)之间的外参(R_l2, t_l2)。 步骤: 1. 数据采集:同时用双目相机和单目相机拍摄同一标定板(如棋盘格)的多组图像。确保标定板在两种相机的视野中都能清晰可见。 2. 分别检测标定板角点:对于双目相机,我们分别得到左图和右图的角点;对于单目相机,得到其图像的角点。 3. 利用双目相机已经标定好的参数,可以计算出每个角点在左相机坐标系下的三维坐标(因为双目相机可以通过立体匹配得到深度)。 4. 同时,单目相机图像上的角点提供了二维坐标。这样,我们就得到了一组3D-2D的对应点(3D点在双目相机的左相机坐标系下,2D点在单目相机图像上)。 5. 利用PnP(Perspective-n-Point)算法求解单目相机相对于双目相机左相机坐标系的外参(R_l2, t_l2)。 具体步骤详解: 步骤1:数据采集 - 同时采集双目相机的左右图像和单目相机的图像。需要多组不同姿态的标定板图像。 步骤2:角点检测 - 对双目相机的左图、右图以及单目相机的图像分别进行角点检测(如棋盘格角点)。假设我们得到了: - 左图角点:points_left - 右图角点:points_right - 单目相机角点:points_mono 步骤3:计算左相机坐标系下的3D点 - 由于双目相机已经标定,我们可以利用左右相机的投影矩阵(P_left, P_right)或者通过三角测量的方法计算每个角点在左相机坐标系下的三维坐标。 - 具体地,对于每一对匹配的角点(左图和右图的同一角点),我们可以通过以下公式计算深度(Z): $$ Z = \frac{f \cdot b}{d} $$ 其中,f为焦距(以像素为单位),b为基线长度(左右相机光心之间的距离),d为视差(d = x_left - x_right,注意这里假设图像已经校正过,使得对应点在同一水平线上)。 - 然后,利用左相机的内参矩阵,我们可以将像素坐标转换为左相机坐标系下的三维坐标: $$ X = \frac{(u - c_x) \cdot Z}{f_x} $$ $$ Y = \frac{(v - c_y) \cdot Z}{f_y} $$ 其中,(u,v)为左图像上的角点坐标,(c_x, c_y)为主点坐标,f_x, f_y为焦距。 步骤4:建立3D-2D对应关系 - 对于每一幅标定板图像,我们得到了左相机坐标系下的一组3D点(标定板角点),以及单目相机图像上对应的2D点(角点)。这样,我们就有了多组3D-2D对应点。 步骤5:求解外参 - 使用PnP算法(如OpenCV中的solvePnP函数)来求解单目相机相对于左相机坐标系的外参(R_l2, t_l2)。 - 注意:PnP算法需要至少4组点(实际上我们使用更多点以提高精度)。 步骤6:优化(可选) - 可以使用Bundle Adjustment(光束法平差)进一步优化外参,同时优化单目相机的内参(如果单目相机内参未知,但这里假设单目相机已经单独标定过内参,则只优化外参)。 注意事项: 1. 如果单目相机尚未标定内参,那么我们需要同时标定内参和外参。这可以通过在PnP之前或同时进行相机标定来实现(例如使用OpenCV的calibrateCamera函数,但通常我们会在联合标定之前先单独标定单目相机的内参)。 2. 如果单目相机已经标定过内参,那么在PnP中我们可以使用已知的内参,这样求解更准确。 流程总结: - 前提:双目相机已标定(已知内参和左右相机之间的外参),单目相机标定内参(如果未标定,则需要同时标定)。 - 采集多组图像(每组包含双目相机的左右图像和单目相机图像)。 - 检测每幅图像上的标定板角点。 - 利用双目相机的视差计算标定板角点在左相机坐标系下的3D坐标。 - 将3D点(左相机坐标系)单目相机图像上的2D点对应。 - 使用solvePnP求解外参。 - 优化(可选)。 引用说明: - 在相机标定方面,我们可以参考经典的标定方法,如张正友标定法[^3]。 - 对于PnP算法,OpenCV提供了多种求解方法(如迭代法、EPnP等)[^1]。 代码示例(伪代码): 假设我们已经有了双目相机的参数(内参和基线等),以及单目相机的内参矩阵K_mono。 ```python import cv2 import numpy as np # 假设我们有n组图像 # 每组包括:left_img, right_img, mono_img # 已知参数 # 双目相机左相机内参 K_left = np.array([[fx_left, 0, cx_left], [0, fy_left, cy_left], [0, 0, 1]]) # 双目相机右相机内参 K_right = np.array([[fx_right, 0, cx_right], [0, fy_right, cy_right], [0, 0, 1]]) # 双目相机左右相机之间的外参:R_lr, t_lr(从右相机到左相机) # 注意:通常我们通过立体标定得到这些参数,并且已经进行了立体校正。 # 单目相机内参 K_mono = np.array([[fx_mono, 0, cx_mono], [0, fy_mono, cy_mono], [0, 0, 1]]) dist_coeffs_mono = ... # 单目相机的畸变系数 # 标定板参数(例如棋盘格) pattern_size = (9, 6) # 内角点数量 square_size = 0.025 # 方格大小(单位:米) # 存储3D点和对应的2D点 object_points = [] # 左相机坐标系下的3D点 image_points = [] # 单目相机图像上的2D点 # 循环处理每组图像 for i in range(num_images): # 读取图像 left_img = cv2.imread(f'left_{i}.png') right_img = cv2.imread(f'right_{i}.png') mono_img = cv2.imread(f'mono_{i}.png') # 检测左、右、单目图像的角点 found_left, corners_left = cv2.findChessboardCorners(left_img, pattern_size) found_right, corners_right = cv2.findChessboardCorners(right_img, pattern_size) found_mono, corners_mono = cv2.findChessboardCorners(mono_img, pattern_size) if found_left and found_right and found_mono: # 亚像素精确化 corners_left = cv2.cornerSubPix(left_img, corners_left, (11,11), (-1,-1), criteria) corners_right = cv2.cornerSubPix(right_img, corners_right, (11,11), (-1,-1), criteria) corners_mono = cv2.cornerSubPix(mono_img, corners_mono, (11,11), (-1,-1), criteria) # 利用双目视差计算3D坐标(在左相机坐标系下) # 注意:这里假设图像已经校正,所以左右图像在同一个水平线上 # 计算视差:校正后,对应点的v坐标相同,所以只需要计算u坐标的差值 # 注意:校正后的图像,左右相机的内参相同(通常为K_left),且没有畸变 # 这里我们使用校正后的内参(如果是校正后的图像,则使用校正后的内参矩阵,这里简化为K_left) # 基线长度(单位:米)已知,或者通过t_lr计算得到(基线长度b = ||t_lr||) b = np.linalg.norm(t_lr) # 或者直接使用标定得到的基线值 # 初始化当前标定板角点的3D坐标(左相机坐标系) points_3d = [] for j in range(len(corners_left)): u_left, v_left = corners_left[j][0] u_right, v_right = corners_right[j][0] # 视差(单位:像素) disparity = u_left - u_right # 计算深度Z(注意:这里f_x是校正后的左相机焦距,通常取K_left[0,0]) f = K_left[0,0] Z = (f * b) / disparity # 计算X, Y X = (u_left - K_left[0,2]) * Z / K_left[0,0] Y = (v_left - K_left[1,2]) * Z / K_left[1,1] points_3d.append([X, Y, Z]) points_3d = np.array(points_3d, dtype=np.float32) # 将当前组的3D点添加到object_points object_points.append(points_3d) # 将单目相机图像上的角点添加到image_points image_points.append(corners_mono.reshape(-1,2)) # 确保形状为(n,2) # 现在,我们得到了多组对应点,使用solvePnP求解外参 # 注意:object_points和image_points都是列表,每个元素是一组点(对应一帧) # 我们需要将object_points和image_points转换为np数组 # 但是solvePnP一次只能处理一帧,所以我们可以使用多帧一起求解(使用solvePnPRansac或solvePnPGeneric)或者取一帧(如果多帧,可以先用一帧初始化,然后优化) # 更常用的做法是使用所有点一起求解(将所有帧的点合并成一帧) all_object_points = np.vstack(object_points) # (N,3) all_image_points = np.vstack(image_points) # (N,2) # 使用solvePnP ret, rvec, tvec = cv2.solvePnP(all_object_points, all_image_points, K_mono, dist_coeffs_mono) # 将旋转向量转换为旋转矩阵 R_l2, _ = cv2.Rodrigues(rvec) # R_l2是从左相机坐标系到单目相机坐标系的旋转矩阵 t_l2 = tvec # 平移向量,从左相机坐标系到单目相机坐标系 # 这样我们就得到了单目相机相对于双目相机左相机的外参:R_l2, t_l2 # 注意:这个外参表示的是将左相机坐标系下的点变换到单目相机坐标系:P_mono = R_l2 * P_left + t_l2 # 如果需要,可以使用Bundle Adjustment进行优化(这里略) ``` 注意:上面的代码是一个简化的示例,实际中需要根据具体的相机参数和图像是否校正进行调整。特别是,如果图像没有进行立体校正,那么我们需要先进行校正,或者使用未校正的图像进行三角测量(需要用到左右相机的投影矩阵)。 另外,如果单目相机有畸变,在角点检测后,我们也可以先对单目相机的角点进行去畸变,然后再用于PnP。或者,在solvePnP中传入畸变系数。 引用: [^1]: 相机IMU的标定中提到了数据文件结构,类似地,我们可以组织双目相机和单目相机的图像数据。 [^2]: 联合标定方法可以参考激光雷达相机联合标定的思想,即建立3D点2D点的对应关系。 [^3]: 相机标定算法原理,如张正友标定法,以及PnP算法。 相关问题: 1. 如何验证联合标定的精度? 2. 如果单目相机内参未知,如何同时标定内参和外参? 3. 除了棋盘格,还有哪些标定板可以用于相机标定?</think>### 双目相机单目相机联合标定方案 #### 核心原理 联合标定的本质是求解双目相机(左/右相机)新增单目相机之间的**相对位姿关系**(旋转矩阵 $R$ 和平移向量 $t$)。主要步骤是通过共享的标定板特征点,建立双目相机坐标系下的 3D 点单目相机 2D 像点的对应关系,利用 PnP 算法求解外参。 #### 标定流程 1. **数据采集** - 同步采集双目相机的左右图像和单目相机图像 - 使用棋盘格/CharUco 等标定板,确保同时出现在所有相机视野中 - 采集 10-20 组不同姿态的图像(示例文件结构): ``` dataset/ ├── stereo/ # 双目相机 │ ├── left/ # 左相机图像 │ └── right/ # 右相机图像 └── mono/ # 单目相机图像 ``` 2. **特征点提取** - 对双目相机图像进行立体匹配,获取标定板角点的 3D 坐标 $$ P_{\text{world}} = (X_i, Y_i, Z_i)^{\top} \quad (\text{以左相机为坐标系原点}) $$ - 检测单目相机图像中的 2D 角点坐标: $$ p_{\text{mono}} = (u_i, v_i)^{\top} $$ 3. **外参求解(关键步骤)** 通过 PnP 算法求解单目相机相对于双目相机坐标系的外参: ```python # Python 伪代码(OpenCV) import cv2 import numpy as np # 输入:双目相机计算的3D点 + 单目相机检测的2D点 object_points = [...] # 3D点坐标 (N×3) image_points = [...] # 2D点坐标 (N×2) K_mono = [...] # 单目相机内参矩阵(需预先标定) # PnP求解外参 success, rvec, tvec = cv2.solvePnP( object_points, image_points, K_mono, None, flags=cv2.SOLVEPNP_ITERATIVE ) # 转换为旋转矩阵 R, _ = cv2.Rodrigues(rvec) # 旋转矩阵 (3×3) t = tvec # 平移向量 (3×1) ``` $$ \begin{bmatrix} x_{\text{mono}} \\ y_{\text{mono}} \\ 1 \end{bmatrix} = K_{\text{mono}} \begin{bmatrix} R & t \end{bmatrix} \begin{bmatrix} X_{\text{stereo}} \\ Y_{\text{stereo}} \\ Z_{\text{stereo}} \\ 1 \end{bmatrix} $$ 4. **精度优化** - 光束法平差(Bundle Adjustment)优化外参 - 重投影误差评估: $$ \text{error} = \sum \left\| p_{\text{detected}} - \pi(R, t, P_{\text{world}}) \right\|^2 $$ ($\pi$ 为投影函数,目标误差 < 0.5 像素) #### 注意事项 1. **标定板要求** - 推荐使用 CharUco 板(融合棋盘格 ArUco 标记),提升角点检测鲁棒性[^3] - 标定板尺寸应覆盖 50% 以上视野 2. **坐标系统一** - 始终以双目相机的**左相机**为世界坐标系原点 - 单目相机外参 $[R|t]$ 表示从双目坐标系到单目坐标系的变换 3. **时间同步** - 硬件触发确保图像采集同步(误差 < 1ms) - 若无法硬件同步,需标定相机间的时间偏移 #### 验证方法 1. **重投影可视化** - 将双目重建的 3D 点投影到单目图像,检查对齐情况 2. **手眼标定验证** - 固定相机组,移动标定板,验证外参一致性 3. **深度图融合** - 将双目深度图投影到单目视角,检查边缘对齐度 > **关键优势**:利用双目相机已标定的优势,避免重新标定双目内部参数,显著提升效率[^1]。 --- ### 相关问题 1. **如何解决标定板在部分视野不可见的问题?** 2. **当相机间基线距离较大时,标定精度如何保证?** 3. **除了 PnP,还有哪些算法可用于多相机外参标定?** [^1]: 相机IMU的标定文件结构示例,适用于多传感器标定场景 [^2]: 激光雷达相机联合标定方案,提供3D-2D标定思想参考 [^3]: 相机标定算法原理详解,包含特征检测优化方法
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值