雙目立體校正
計算機視覺課的第二次作業,使用給定的雙目相機加標定板(紙)進行雙目相機的標定+校正。
工具
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項目的正確創建
文件\(\longrightarrow\)新建\(\longrightarrow\)項目\(\longrightarrow\)Qt Widgets Application\(\longrightarrow\)創建
點擊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。