zoukankan      html  css  js  c++  java
  • 双目相机标定校正(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。

  • 相关阅读:
    mybatis date类型比较
    搭建 c 语言环境 1_centos6 minimal 配置 c/c++ 编译环境
    2_eclipse配置c/c++环境
    1_eclipse导入hibernate 的DTD 文件
    1_eclipse配置c/c++开发环境
    2_修改Eclipse里面的快捷键
    1_修改注释内容
    8_对象创建、static 关键字、静态变量和成员变量的区别、文档
    7_匿名对象、封装(private)、this 关键字、构造方法
    6_面向对象基础、成员变量和局部变量的区别
  • 原文地址:https://www.cnblogs.com/linzzz98/p/13881063.html
Copyright © 2011-2022 走看看