双目相机标定校正(Qt+OpenCV+VS)
双目立体校正
计算机视觉课的第二次作业,使用给定的双目相机加标定板(纸)进行双目相机的标定+校正。
工具
qt5 + opencv4.4.0 + vs2019
程序设计
程序设计重心主要放在qt5的界面布局,槽与信号之间的传递等。
双目立体标定的程序在opencv中有一个单独的例子,可以直接拿来做参考。
(..\opencv\sources\samples\cpp\stereo_calib.cpp)
但是,想要运行成功,需要对程序进行一定的修改!
运行结果图:
整体使用上下两个group存放摄像机拍摄的画面和校正的画面,画面使用label组件进行显示。
一共设计六个按钮控制相机的打开关闭,标定校正,拍摄以及查询标定信息。
内参信息显示:
忽略右上角显示不全的logo(最新版本已经修改)
上框存放两个相机公共的信息,下面分别存放左右两个相机各自的信息。
注意点
相机的读取
老师发的双目相机需要分别读取两个摄像头,具体如下:
-
定义两个VideoCapture类
cv::VideoCapture capture_l; cv::VideoCapture capture_r;
-
打开摄像头
capture_l.open(1); capture_r.open(0);
非常奇怪的是,我这里的第1个摄像头是右摄像头,所以先读取的右边(1)后读取的左边(0)
-
测试摄像头是否正确打开
if (capture_l.isOpened() || capture_r.isOpened())
-
读取当前帧中
cv::Mat frame_l; cv::Mat frame_r; capture_l >> frame_l; capture_r >> frame_r;
-
关闭相机
capture_l.release(); capture_r.release();
对于opecv的例子,需要进行一定的修改,具体如下:
-
需要删减的部分
(1) 原程序119行,由于例子中使用的是灰度图,所以添加了此句将灰度图转换为了RGB图,但使用自己的双目相机拍摄的为RGB图,如果加上这一句会报错。所以需要去掉。
cvtColor(img, cimg, COLOR_GRAY2BGR);
同理还有304行的
cvtColor(rimg, cimg, COLOR_GRAY2BGR);
-
需要修改的部分
(1) boardSize修改为自己标定板的内点,即下图圈出的点,类型为Size(x,y)为x方向的角点个数和y方向的角点个数。
(2) square修改为一个格子的边长宽度
但是这里经过测试发现了一个问题。程序里使用的是以cm做为单位,而网上对程序的评价则认为使用mm做为单位,即1还是10的问题。然而我经过测试,无论使用多少对程序的结果都没有影响??
(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即焦距相等,但会产生以下的问题:
虽然标定成功,但是校正出现了问题。
经过漫长的测试和怀疑自我,最终将问题锁定在CALIB_SAME_FOCAL_LENGTH这个选项上,将其去掉即可成功校正。
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,需要注意以下部分。
-
qt5项目的正确创建
文件新建项目Qt Widgets Application创建
点击next后,注意这里的选项,一定要选择与Platform对应的。
-
槽函数的定义与使用
在vs中使用qt,与qtCreator最大的区别是无法直接转到槽函数,需要自己定义,具体步骤如下:
点击选项栏中的编辑信号/槽
默认是选择的Widget,需要在右边的查看器里点击添加槽函数的部件,然后点击向外拖动(注意不要拖到到其他组件上去,拖到空白的地方),由于程序比较简单,仅仅使用了点击事件,所以点击左边的clicked(),然后点击右边的编辑。
点击左下角的加号,写上自己定义的槽函数名称,点击OK。
然后在右边的框里选择刚才定义的函数,点击OK
操作之后在右下角的槽编辑器里可以看到刚刚关联的组件和槽函数
然后返回我们的类.h文件
在类内定义一个private slots专门存放槽函数,注意名称要与刚才创建的对应上。
然后进入cpp文件进行槽函数的具体实现即可
这里不需要再connect,因为刚才在ui里的操作实际上已经将按钮和槽connect起来了。
-
善用计时器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();
-
不同窗口之间传递值
我将标定的值存放在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 }
这样就实现了不同窗口之间的值传递
-
锁定子窗口
在子窗口展示时,我想要锁定主窗口无法点击切换,一句话实现:
// 锁定窗口 this->setWindowModality(Qt::ApplicationModal);
-
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();
-
调用控件
Q里调用设计的控件非常简单,直接使用ui.进行调用
例如设置关闭按钮不可用:
ui.close_btn->setEnabled(false);
-
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。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端