双目相机标定校正(Qt+OpenCV+VS)

双目立体校正

计算机视觉课的第二次作业,使用给定的双目相机加标定板(纸)进行双目相机的标定+校正。

工具

qt5 + opencv4.4.0 + vs2019

程序设计

程序设计重心主要放在qt5的界面布局,槽与信号之间的传递等。

双目立体标定的程序在opencv中有一个单独的例子,可以直接拿来做参考。

(..opencvsourcessamplescppstereo_calib.cpp)

但是,想要运行成功,需要对程序进行一定的修改!

运行结果图:

整体使用上下两个group存放摄像机拍摄的画面和校正的画面,画面使用label组件进行显示。

一共设计六个按钮控制相机的打开关闭,标定校正,拍摄以及查询标定信息。

11

内参信息显示:

忽略右上角显示不全的logo(最新版本已经修改)

22

上框存放两个相机公共的信息,下面分别存放左右两个相机各自的信息。

注意点

相机的读取

老师发的双目相机需要分别读取两个摄像头,具体如下:

  1. 定义两个VideoCapture类

    cv::VideoCapture capture_l;
    cv::VideoCapture capture_r;
    
  2. 打开摄像头

    capture_l.open(1);
    capture_r.open(0);
    

    非常奇怪的是,我这里的第1个摄像头是右摄像头,所以先读取的右边(1)后读取的左边(0)

  3. 测试摄像头是否正确打开

    if (capture_l.isOpened() || capture_r.isOpened())
    
  4. 读取当前帧中

    cv::Mat frame_l;
    cv::Mat frame_r;
    capture_l >> frame_l;
    capture_r >> frame_r;
    
  5. 关闭相机

    capture_l.release();
    capture_r.release();
    

对于opecv的例子,需要进行一定的修改,具体如下:

  1. 需要删减的部分

    (1) 原程序119行,由于例子中使用的是灰度图,所以添加了此句将灰度图转换为了RGB图,但使用自己的双目相机拍摄的为RGB图,如果加上这一句会报错。所以需要去掉。

    cvtColor(img, cimg, COLOR_GRAY2BGR);
    

    同理还有304行的

    cvtColor(rimg, cimg, COLOR_GRAY2BGR);
    
  2. 需要修改的部分

    (1) boardSize修改为自己标定板的内点,即下图圈出的点,类型为Size(x,y)为x方向的角点个数和y方向的角点个数。

    image-20201026191958358

    (2) square修改为一个格子的边长宽度

    ​ 但是这里经过测试发现了一个问题。程序里使用的是以cm做为单位,而网上对程序的评价则认为使用mm做为单位,即1还是10的问题。然而我经过测试,无论使用多少对程序的结果都没有影响??

    image-20201026194630169

    (3) clone()

    ​ Mat cimg = img;实际上cimg是img的引用,对cimg进行修改也就等于对img进行了修改。

    ​ 所以drawChessboardCorners(cimg, boardSize, corners, found);这里对cimg画上了圈和线,也就是将原本的img进行了修改。程序的本意并不是这样, 仅仅只想对cimg进行画角点的标注。那么就需要将这句话修改为:

    cv::Mat cimg = img.clone();
    

    ​ 使用clone()就只是得到了img的副本,而不是引用。

    (4) 删除选项CALIB_SAME_FOCAL_LENGTH

    ​ 在使用像素点和相机内参计算畸变稀疏和RTEF时,opencv已经提供了封装好的函数stereoCalibrate,这个函数需要传递一个CALIB_的选项,opencv的原例子里使用了CALIB_SAME_FOCAL_LENGTH即焦距相等,但会产生以下的问题:

    虽然标定成功,但是校正出现了问题。

    33

    经过漫长的测试和怀疑自我,最终将问题锁定在CALIB_SAME_FOCAL_LENGTH这个选项上,将其去掉即可成功校正。

    44

    double rms = stereoCalibrate(objectPoints, imagePoints[0], imagePoints[1],
                        cameraMatrix[0], distCoeffs[0],
                        cameraMatrix[1], distCoeffs[1],
                        imageSize, R, T, E, F,
                        CALIB_FIX_ASPECT_RATIO +
                        CALIB_ZERO_TANGENT_DIST +
                        CALIB_USE_INTRINSIC_GUESS +
                        CALIB_SAME_FOCAL_LENGTH +
                        CALIB_RATIONAL_MODEL +
                        CALIB_FIX_K3 + CALIB_FIX_K4 + CALIB_FIX_K5,
                        TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 100, 1e-5) );
    

    !!!!注意!!!!

    非常重要的一点,上述函数是有返回值的,返回double型的重投影误差,经过大量的数据测试,重投影误差一旦大于1.5以上时,几乎就无法正确校正,所以我在程序中设计了rms判断,一旦大于1.5的阈值直接跳出,提示用户重新拍摄。

对于qt5,需要注意以下部分。

  1. qt5项目的正确创建

    文件(longrightarrow)新建(longrightarrow)项目(longrightarrow)Qt Widgets Application(longrightarrow)创建

    image-20201026200130626

    点击next后,注意这里的选项,一定要选择与Platform对应的。

    image-20201026200223428

  2. 槽函数的定义与使用

    在vs中使用qt,与qtCreator最大的区别是无法直接转到槽函数,需要自己定义,具体步骤如下:

    点击选项栏中的编辑信号/槽

    image-20201026200608462

    image-20201026200643478

    默认是选择的Widget,需要在右边的查看器里点击添加槽函数的部件,然后点击向外拖动(注意不要拖到到其他组件上去,拖到空白的地方),由于程序比较简单,仅仅使用了点击事件,所以点击左边的clicked(),然后点击右边的编辑。

    image-20201026200749418

    点击左下角的加号,写上自己定义的槽函数名称,点击OK。

    image-20201026200836759

    然后在右边的框里选择刚才定义的函数,点击OK

    image-20201026200957578

    操作之后在右下角的槽编辑器里可以看到刚刚关联的组件和槽函数

    image-20201026201123398

    然后返回我们的类.h文件

    在类内定义一个private slots专门存放槽函数,注意名称要与刚才创建的对应上。

    image-20201026201202333

    然后进入cpp文件进行槽函数的具体实现即可

    image-20201026201249629

    这里不需要再connect,因为刚才在ui里的操作实际上已经将按钮和槽connect起来了。

  3. 善用计时器QTimer

    在程序里我设计了2秒一次显示校正后的图片和角点图片,如何实现这个功能呢?

    使用了Qt自带的QTimer类。具体用法这里不多赘述,仅仅提供一个简单的思路:

    定义一个指针型的QTimer变量

    QTimer* timer_camera;
    

    在构造函数中使用Connect将QTimer和需要间隔调用的函数联系起来,比如我想每2秒调用一次readFrame函数:

    timer_camera = new QTimer(this);
    connect(timer_camera, SIGNAL(timeout()), this, SLOT(readFrame()));
    

    然后需要在一个特定的函数中将timer触发(即开始)

    程序中我点击打开相机后,每帧读取相机的画面输出到label中,因此在打开相机按钮的槽函数中设置timer的开启。

    这里start()里的25是指间隔,1000为1s

    timer_camera->start(25);
    

    当不需要继续调用时,需要关闭timer,我在关闭相机的槽函数在中进行关闭

    timer_camera->stop();
    
  4. 不同窗口之间传递值

    我将标定的值存放在Info类中,作为主窗口的私有成员变量,但是我想在新的窗口中显示这些值,就需要将主窗口的变量传递到子窗口中去,具体操作如下:

    在主窗口的.h文件中定义sendInfo传递信号:

    signals:
        void sendInfo(Info info);
    

    在子窗口中定义槽函数receiveInfo接收信号:

    private slots:
    	void receiveInfo(Info ifo);
    

    在主函数的构造函数中将两者连接:

    connect(this, SIGNAL(sendInfo(Info)), CAMERA_INFO, SLOT(receiveInfo(Info)));
    

    在主窗口的查看信息的槽函数中进行传递,并显示子窗口

    void CameraCalibrate::checkInfo()
    {
        emit sendInfo(info);
        CAMERA_INFO->show();
    }
    

    在子窗口中实现receiveInfo函数

    void CameraInfo::receiveInfo(Info info_)
    {
    	info = info_;
    	/// todo 
    }
    

    这样就实现了不同窗口之间的值传递

  5. 锁定子窗口

    在子窗口展示时,我想要锁定主窗口无法点击切换,一句话实现:

    // 锁定窗口
    this->setWindowModality(Qt::ApplicationModal);
    
  6. QString与其他类型的转换

    该部分参考自:https://blog.csdn.net/qq_35223389/article/details/83112753

    Qt的label里显示字符串是QString类型,如果是其他类型需要进行转换,具体如下:

    (1) int 与 QString

    //int转QString
    int a = 123456;
    QString b;
    b = QString::number(a,10,5);//QString::number(a,基底,精度)
    //方法2,利用arg()
    int a = 123456;
    QString b = QString("%1").arg(a);
    
    //QString转int
    QString c = "123456";
    int d;
    d = c.toInt();
    

    (2) double 与 QString

    //double转QString
    double a = 123.456;
    QString b;
    b =  QString::number(a,10,5);//同int
    
    //QString转double
    QString c = "123.456";
    double d;
    d = c.toDouble();//类似int
    

    (3) string 与 QString

    //string转QString
    string a = "123.456";
    QString b;
    b = QString::fromStdString(a);
    
    //QString转string
    QString c = "123,456";
    string d;
    d = c.toStdString();
    
  7. 调用控件

    Q里调用设计的控件非常简单,直接使用ui.进行调用

    例如设置关闭按钮不可用:

    ui.close_btn->setEnabled(false);
    
  8. label显示图片

    最重要的放在最后说!

    首先是由于label的大小有限,需要将相机实时拍摄到的画面进行resize到与label同大小

    然后定义一个QImage类,具体见下面的实现

    然后将其变为QPixmap类显示在label上

    // 可选项
    cv::resize(frame, frame, cv::Size(xx, yy));
    
    // 必写
    QImage image = QImage((const uchar*)frame.data, frame.cols, frame.rows, QImage::Format_RGB888).rgbSwapped();
    ui.label->setPixmap(QPixmap::fromImage(image));
    

问题

目前没有解决的问题是,由于我是使用A4纸打印的棋盘格单人测量,所以只能将A4纸放在桌面上,然后变换相机,但是在实际测试中,大多数时间都是拍摄好的数据集由于重投影误差过大( > 1.5,一般在13左右)无法使用,很奇怪。程序是正确的,opecncv的数据集跑的完全正确,但是自己拍摄的数据集,大部分时间都不可用,这个问题还需要进一步研究。

好消息是,终于不用再傻傻举着相机一举一上午了,解放了!

源码将会在之后发布。虽然程序非常简单,但是花了我四天时间:半天速学qt,半天写完,三天debug。

原文地址:https://www.cnblogs.com/linzzz98/p/13881063.html