僅供參考,還未運行程序,理解部分有誤,請參考英文原版。html
綠色部分非文章內容,是我的理解。算法
轉載請註明:http://blog.csdn.net/raby_gyl/article/details/17471617編程
在這一章,咱們將討論來至運動結構(Structure from Motion,SfM)的概念,或者從一個運動的相機拍攝到的圖像中更好的推測提取出來的幾何結構,使用OpenCV的API函數能夠幫助咱們完成這個任務。首先,讓咱們將咱們使用的冗長的方法約束爲使用單個攝像機,一般稱爲單目方法,而且是一組分離的和稀疏的視頻幀而不是連續的視頻流。這兩個約束在很大程度上簡化了系統,這個系統咱們將在接下來的頁碼中進行描述,而且幫助咱們理解任何SfM方法的原理。爲了實現咱們的方法,咱們將跟隨Hartley和Zisserman的腳步(後面稱做H和Z),伴隨着他們有創意的書——《計算機視覺中的多視覺幾何》的第9章到第12章的記錄。數組
在這一章,咱們將涉及到如下內容:數據結構
一、來至運動結構的概念app
二、從一對圖像中估計攝像機的運動框架
三、重構場景iview
四、從多個視圖中重構機器學習
五、重構提純(Refinement of the reconstruction)函數
六、可視化3D點雲
本章自始自終假定使用一個標定過的相機——預先標定的相機。在計算機視覺中標定是一個廣泛存在的操做,在OpenCV中獲得了很好的支持,咱們可使用在前一章討論的命令行工具來完成標定。所以,咱們假定攝像機內參數存在,而且具體化到K矩陣中,K矩陣爲攝像機標定過程的一個結果輸出。
爲了在語言方面表達的清晰,從如今開始咱們將單個攝像機稱爲場景的單個視圖而不是光學和獲取圖像的硬件。一個攝像機在空間中有一個位置,一個觀察的方向。在兩個攝像機中,有一個平移成分(空間運動)和一個觀察方向的旋轉。咱們一樣對場景中的點統一一下術語,世界(world),現實(real),或者3D,意思是同樣的,即咱們真實世界存在的一個點。這一樣適用於圖像中的點或者2D,這些點在圖像座標系中,是在這個位置和時間上一些現實的3D點投影到攝像機傳感器上造成的。
在這一章的代碼部分,你將注意參考《計算視覺中的多視覺幾何》(Multiple View Geometry in Computer Vision),例如,//HZ9.12,這指的是這本書的第9章第12個等式。一樣的,文本僅包括代碼的摘錄,而完整的代碼包含在伴隨着這本書的材料中。
第一個咱們應當區別的是立體(Stereo or indeed any multiview),使用標準平臺的三維重建和SfM之間的差別。在兩個或多個攝像機的平臺中,假定咱們已經知道了兩個攝像機之間的運動,而在SfM中,咱們實際上不知道這個運動而且咱們但願找到它。標準平臺,來至觀察的一個過度簡單的點,能夠獲得一個更加精確的3D幾何的重構,由於在估計多個攝像機間距離和旋轉時沒有偏差(距離和旋轉已知)。實現一個SfM系統的第一步是找到相機之間的運動。OpenCV能夠幫助咱們在許多方式上得到這個運動,特別地,使用findFundamentalMat函數。
讓咱們想一下選擇一個SfM算法背後的目的。在不少狀況下,咱們但願得到場景的幾何。例如,目標相對於相機的位置和他們的形狀是什麼。假定咱們知道了捕獲同一場景的攝像機之間的運動,從觀察的一個合理的類似點,如今咱們想重構這個幾何。在計算視覺術語中稱爲三角測量(triangulation),而且有不少方法能夠作這件事。它能夠經過 射線相交的方法完成,這裏咱們構造兩個射線:來至於每一個攝像機投影中心的點和每個圖像平面上的點。理想上,這兩個射線在空間中將相交於真實世界的一個3D點(這個3D點在每一個攝像機中成像),以下面圖表展現:
實際上,射線相交很是不可靠。H和Z推薦不使用它。這是由於兩個射線一般不能相交,讓咱們回到使用鏈接兩個射線的最短線段上的中間點。相反,H和Z建議一些方法來三角化3D點(trianglulate 3D points,三角化3D點就是計算3D點的座標,能夠經過後面的內容加以理解),這些方法中咱們將在重構場景部分討論兩個。OpenCV如今的版本沒有包含三角測量(triangulation)的API,所以,這部分咱們將本身編寫代碼。
學習完如何從兩個視圖恢復3D幾何以後,咱們將看到咱們是怎麼樣加入更多的同一個場景的視圖來得到更豐富的重構。在那時,大部分的SfM方法試圖依靠束調整(Bundle Adjustment)來優化咱們的攝像機和3D點一束估計位置(the bundle of estimated positon),這部份內容在重構提純部分(Refinement of the reconstruction section)。OpenCV在新的圖像拼接的工具箱內包含了用來束調整的方法。然而,使OpenCV和C++完美結合的工做是豐富的外部工具,這些工具能夠很容易地整合到一塊兒。所以,咱們將看到如何如何整合一個外部的束調節器——靈巧的SSBA庫。
既然咱們已經描述了使用OpenCV實現咱們的SfM方法的一個歸納,咱們將看到每一個分紅是如何實現的。
事實上,在咱們開始着手兩個攝像機之間的運動以前,讓咱們檢查一下咱們手邊用來執行這個操做的輸入和工具。首先,咱們有來至(但願並非很是)不一樣空間位置的同一場景的兩個圖像。這是一個強大的資產,而且咱們將確保使用它。如今工具都有了,咱們應當看一下數學對象,來對咱們的圖像,相機和場景施加約束。
兩個很是有用的數學對象是基礎矩陣(用F表示)和本徵矩陣(用E表示)。除了本徵矩陣是假設使用的標定的相機,它們很是類似。這種狀況針對咱們,因此咱們將選着它。OpenCV函數僅容許咱們經過findFundamentalMat函數找到基礎矩陣。然而,咱們很是簡單地使用標定矩陣(calibration matrix)K從本徵矩陣中得到基礎矩陣,以下:
Mat_<double> E = K.t() * F * K; //according to HZ (9.12)
本徵矩陣,是一個3×3大小的矩陣,使用x’Ex=0在圖像中的一點和另一個圖像中的一點之間施加了一個約束,這裏x是圖像一中的的一點,x’是圖像二中與之相對應的一點。這很是有用,由於咱們將要看到。咱們使用的另外一個重要的事實是本徵矩陣是咱們用來爲咱們的圖像恢復兩個相機的全部須要,儘管只有尺度,可是咱們之後會獲得。所以,若是咱們得到了本徵矩陣,咱們知道每個相機在空間中的位置,而且知道它們的觀察方向,若是咱們有足夠這樣的約束等式,那麼咱們能夠簡單地計算出這個矩陣。簡單的由於每個等式能夠用來解決矩陣的一小部分。事實上,OpenCV容許咱們僅使用7個點對來計算它,可是咱們但願得到更多的點對來獲得一個魯棒性的解。
如今咱們將使用咱們的約束等式來計算本徵矩陣。爲了得到咱們的約束,記住對於圖像A中的每個點咱們必須在圖像B中找到一個與之相對應的點。咱們怎樣完成這個匹配呢?簡單地經過使用OpenCV的普遍的特徵匹配框架,這個框架在過去的幾年了已經很是成熟。
在計算機視覺中,特徵提取和描述子匹配是一個基礎的過程,而且用在許多方法中來執行各類各樣的操做。例如,檢測圖像中一個目標的位置和方向,或者經過給出一個查詢圖像在大數據圖像中找到類似的圖像。從本質上講,提取意味着在圖像中選擇點,使得得到好的特徵,而且爲它們計算一個描述子。一個描述子是含有多個數據的向量,用來描述在一個圖像中圍繞着特徵點的周圍環境。不一樣的方法有不一樣的長度和數據類型來表示描述子矢量。匹配是使用它的描述子從另一個圖像中找到一組與之對應的特徵。OpenCV提供了很是簡單和有效的方法支持特徵提取和匹配。關於特徵匹配的更多信息能夠在Chapter 3少許(無)標記加強現實中找到。
讓咱們檢查一個很是簡單特徵提取和匹配方案:
// detectingkeypoints SurfFeatureDetectordetector(); vector<KeyPoint> keypoints1, keypoints2; detector.detect(img1, keypoints1); detector.detect(img2, keypoints2); // computing descriptors SurfDescriptorExtractor extractor; Mat descriptors1, descriptors2; extractor.compute(img1, keypoints1, descriptors1); extractor.compute(img2, keypoints2, descriptors2); // matching descriptors BruteForceMatcher<L2<float>> matcher; vector<DMatch> matches; matcher.match(descriptors1, descriptors2, matches);
你可能已經看到相似的OpenCV代碼,可是讓咱們快速的回顧一下它。咱們的目的是得到三個元素:兩個圖像的特徵點,特徵點的描述子,和兩個特徵集的匹配。OpenCV提供了一組特徵檢測器、描述子提取器和匹配器。在這個簡單例子中,咱們使用SurfFeatureDetector函數來得到SURF(Speeded-Up-Robust Features)特徵的2D位置,而且使用SurfDescriptorExtractor函數來得到SURF描述子。咱們使用一個brute-force匹配器來得到匹配,這是一個最直接方式來匹配兩個特徵集,該方法是經過比較第一個集中的每個特徵和第一個集的每個特徵而且得到最好的匹配來實現的。
在下一個圖像中,咱們將看到兩個圖像上的特徵點的匹配:(這兩個圖像來至於the Fountain-P11 sequence,能夠在網址中找到:http://cvlab.epfl.ch/~strecha/multiview/denseMVS.html.)
實際上,像咱們執行的原始匹配(raw matching),只有到達某一程度時執行效果才比較好而且許多匹配多是錯誤的。所以,大多數SfM方法對原始匹配進行一些濾波方式來確保正確和減小錯誤。一種濾波方式叫作交叉檢驗濾波,它內置於OpenCV的brute-force匹配器中。也就是說,若是第一個圖像的一個特徵匹配第二個圖像的一個特徵,而且經過反向檢查,第二個圖像的特徵也和第一個圖像的特徵匹配,那麼這個匹配被認爲是正確的。另外一個常見的濾波機制(該機制用在提供的代碼中)是基於同一場景的兩個圖像而且在他們之間存在某一種立體視覺關係,這樣的一個事實基礎之上來濾波的。在實踐中,這個濾波器嘗試採用魯棒性的算法來計算這個基礎矩陣,咱們將會在尋找相機矩陣(Finding camera matrices)部分學習這種計算方法,而且保留由該計算獲得的帶有小偏差的特徵對。
一個替代使用豐富特徵(如SURF)匹配的方法,是使用optical flow(OF光流)進行匹配。下面的信息框提供了光流的一個簡短的概述。最近的OpenCV爲從兩個圖像得到流場擴展了API,而且如今更快,更強大。咱們將嘗試使用它做爲匹配特徵的一個替代品。
【註釋:
光流是匹配來至一幅圖像選擇的點到另一幅圖像選着點的過程,假定這兩個圖像是一個視頻序列的一部分而且它們彼此很是相近。大多數的光流方法比較一個小的區域,稱爲搜索窗口或者塊,這些塊圍繞着圖像A中的每一點和一樣區域的圖像B中的每一點。遵循計算機視覺中一個很是普通的規則,稱爲亮度恆定約束(brightness constancy constraint)(和其餘名字),圖像中的這些小塊從一個圖像到另一個圖像不會有太大的變化,所以,他們的幅值差接近於0。除了匹配塊,更新的光流方法使用一些額外的方法來得到更好的結果。其中一個方法就是使用圖像金字塔,它是圖像愈來愈小的尺寸(大小)版本,這考慮到了工做的從粗糙到精緻——計算機視覺中一個很是有用的技巧。另一個方法是定義一個流場上的全局約束,假定這些點相互靠近,向同一方向一塊兒運動。在OpenCV中,一個更加深刻的光流方法能夠在Chapter Developing Fluid Wall Using the Microsoft Kinect中找到,這個書能夠在出版社網站上訪問到。】
在OpenCV中,使用光流至關的簡單,能夠經過調用calcOpticalFlowPyrLK函數來實現。咱們想要保存光流的結果匹配,像使用豐富的特徵那樣,在將來,咱們但願兩種方法能夠互換。爲了這個目標,咱們必須安裝一個特殊的匹配方法——能夠與基於特徵的方法互換,這將在下面的代碼中看到:
Vector<KeyPoint>left_keypoints,right_keypoints; // Detect keypoints in the left and right images FastFeatureDetectorffd; ffd.detect(img1, left_keypoints); ffd.detect(img2, right_keypoints); vector<Point2f>left_points; KeyPointsToPoints(left_keypoints,left_points); vector<Point2f>right_points(left_points.size()); // making sure images are grayscale Mat prevgray,gray; if (img1.channels() == 3) { cvtColor(img1,prevgray,CV_RGB2GRAY); cvtColor(img2,gray,CV_RGB2GRAY); } else { prevgray = img1; gray = img2; } // Calculate the optical flow field: // how each left_point moved across the 2 images vector<uchar>vstatus; vector<float>verror; calcOpticalFlowPyrLK(prevgray, gray, left_points, right_points, vstatus, verror); // First, filter out the points with high error vector<Point2f>right_points_to_find; vector<int>right_points_to_find_back_index; for (unsigned inti=0; i<vstatus.size(); i++) { if (vstatus[i] &&verror[i] < 12.0) { // Keep the original index of the point in the // optical flow array, for future use right_points_to_find_back_index.push_back(i); // Keep the feature point itself right_points_to_find.push_back(j_pts[i]); } else { vstatus[i] = 0; // a bad flow } } // for each right_point see which detected feature it belongs to Mat right_points_to_find_flat = Mat(right_points_to_find). reshape(1,to_find.size()); //flatten array vector<Point2f>right_features; // detected features KeyPointsToPoints(right_keypoints,right_features); Mat right_features_flat = Mat(right_features).reshape(1,right_ features.size()); // Look around each OF point in the right image // for any features that were detected in its area // and make a match. BFMatchermatcher(CV_L2); vector<vector<DMatch>>nearest_neighbors; matcher.radiusMatch( right_points_to_find_flat, right_features_flat, nearest_neighbors, 2.0f); // Check that the found neighbors are unique (throw away neighbors // that are too close together, as they may be confusing) std::set<int>found_in_right_points; // for duplicate prevention for(inti=0;i<nearest_neighbors.size();i++) { DMatch _m; if(nearest_neighbors[i].size()==1) { _m = nearest_neighbors[i][0]; // only one neighbor } else if(nearest_neighbors[i].size()>1) { // 2 neighbors – check how close they are double ratio = nearest_neighbors[i][0].distance / nearest_neighbors[i][1].distance; if(ratio < 0.7) { // not too close // take the closest (first) one _m = nearest_neighbors[i][0]; } else { // too close – we cannot tell which is better continue; // did not pass ratio test – throw away } } else { continue; // no neighbors... :( } // prevent duplicates if (found_in_right_points.find(_m.trainIdx) == found_in_right_points. end()) { // The found neighbor was not yet used: // We should match it with the original indexing // ofthe left point _m.queryIdx = right_points_to_find_back_index[_m.queryIdx]; matches->push_back(_m); // add this match found_in_right_points.insert(_m.trainIdx); } } cout<<"pruned "<< matches->size() <<" / "<<nearest_neighbors.size() <<" matches"<<endl;
函數KeyPointsToPoints和PointsToKeyPoints是用來進行cv::Point2f和cv::KeyPoint結構體之間相互轉換的簡單方便的函數。從先前的代碼片斷咱們能夠看到一些有趣的事情。第一個注意的事情是,當咱們使用光流時,咱們的結果代表,一個特徵從左手邊圖像的一個位置移動到右手邊圖像的另一個位置。可是咱們有一組在右手邊圖像中檢測到的新的特徵,在光流中從這個圖像到左手邊圖像的特徵不必定是對齊的。咱們必須使它們對齊。爲了找到這些丟失的特徵,咱們使用一個k鄰近(KNN)半徑搜索,這給出了咱們兩個特徵,即感興趣的點落入了2個像素半徑範圍內。
咱們能夠看獲得另一個事情是用來測試KNN的比例的實現,在SfM中這是一種常見的減小錯誤的方法。實質上,當咱們對左手邊圖像上的一個特徵和右手邊圖像上的一個特徵進行匹配時,它做爲一個濾波器,用來移除混淆的匹配。若是右手邊圖像中兩個特徵太靠近,或者它們之間這個比例(the rate)太大(接近於1.0),咱們認爲它們混淆了而且不使用它們。咱們也安裝一個雙重防護濾波器來進一步修剪匹配。
下面的圖像顯示了從一幅圖像到另一幅圖像的流場。左手邊圖像中的分成色箭頭表示了塊從左手邊圖像到右手邊圖像的運動。在左邊的第二個圖像中,咱們看到一個放大了的小的流場區域。粉紅色箭頭再一次代表了塊的運動,而且咱們能夠經過右手邊的兩個原圖像的片斷看到它的意義。在左手邊圖像上可視的特徵正在向左移動穿過圖像,粉紅色箭頭的方向在下面的圖像中展現:
使用光流法代替豐富特徵的優勢是這個過程一般更快而且能夠適應更多的點,使重構更加稠密。在許多光流方法中也有一個塊總體運動的統一模型,在這個模型中,豐富的特徵匹配一般不考慮。使用光流要注意的是對於從同一個硬件獲取的連續圖像,它處理的很快,然而豐富的特徵一般不可知。它們之間的差別源於這樣的一個事實:光流法一般使用很是基礎的特徵,像圍繞着一個關鍵點的圖像塊,然而,高階豐富特徵(例如,SURF)考慮每個特徵點的較高層次的信息。使用光流或者豐富的特徵是設計師根據應用程序的輸入所作的決定。
既然咱們得到了兩個關鍵點之間的匹配,咱們能夠計算基礎矩陣而且從中得到本徵矩陣。然而,咱們必須首先調整咱們的匹配點到兩個數組,其中數組中的索引對應於另一個數組中一樣的索引。這須要經過findFundametalMat函數得到。咱們可能也須要轉換KeyPoint 結構到Point2f結構。咱們必須特別注意DMatch的queryIdx和trainIdx成員變量,在OpenCV中DMatch是容納兩個關鍵點匹配的結構,由於它們必須匹配咱們使用matche.match()函數的方式。下面的代碼部分展現瞭如何調整一個匹配到兩個相應的二維點集,以及如何使用它們來找到基礎矩陣:
vector<Point2f>imgpts1,imgpts2; for( unsigned inti = 0; i<matches.size(); i++ ) { // queryIdx is the "left" image imgpts1.push_back(keypoints1[matches[i].queryIdx].pt); // trainIdx is the "right" image imgpts2.push_back(keypoints2[matches[i].trainIdx].pt); } Mat F = findFundamentalMat(imgpts1, imgpts2, FM_RANSAC, 0.1, 0.99, status); Mat_<double> E = K.t() * F * K; //according to HZ (9.12)
稍後咱們可能使用status二值向量來修剪這些點,使這些點和恢復的基礎矩陣匹配。看下面的圖像:用來闡述使用基礎矩陣修剪後的點匹配。紅色箭頭表示特徵匹配在尋找基礎矩陣的過程當中被移除了,綠色箭頭表示被保留的特徵匹配。
如今咱們已經準備好尋找相機矩陣。這個過程在H和Z書的第九章進行了詳細的描述。然而,咱們將使用一個直接和簡單的方法來實現它,而且OpenCV很容易的爲咱們作這件事。可是首先,咱們將簡短地檢查咱們將要使用的相機矩陣的結構:
咱們的相機使用該模型,它由兩個成分組成,旋轉(表示爲R)和平移(表示爲t)。關於它的一個有趣的事情是它容納一個很是基本的等式:x=PX,這裏x是圖像上的二維點,X是空間上的三維點。還有更多,可是這個矩陣給咱們一個很是重要的關係,即圖像中點和場景中點之間的關係。所以,既然咱們有了尋找攝像機矩陣的動機,那麼咱們將看到這個過程是怎麼完成的。下面的的代碼部分展現瞭如何將本徵矩陣分解爲旋轉和平移成分。
SVD svd(E); Matx33d W(0,-1,0,//HZ 9.13 1,0,0, 0,0,1); Mat_<double> R = svd.u * Mat(W) * svd.vt; //HZ 9.19 Mat_<double> t = svd.u.col(2); //u3 Matx34d P1( R(0,0),R(0,1), R(0,2), t(0), R(1,0),R(1,1), R(1,2), t(1), R(2,0),R(2,1), R(2,2), t(2));
很是簡單。咱們須要作的工做僅是對咱們先前得到的本徵矩陣進行奇異值分解(SVD),而且乘以一個特殊的矩陣W。對於咱們所作的數學上的操做不進行過深地闡述,咱們能夠說SVD操做將咱們的矩陣E分解成爲兩部分,一個旋轉成分和一個平移成分。實時上,本徵矩陣起初是經過這兩個成分相乘構成的。徹底地是知足咱們的好奇心,咱們能夠看下面的本徵矩陣的等式,它在字面意義上表現爲:E=[t]xR(x是t的下標)。我能夠看到它是由一個平移成分和一個旋轉成分組成。
咱們注意到,咱們所作的僅是獲得了一個相機矩陣,所以另一個相機矩陣呢?好的,咱們在假定一個相機矩陣是固定的而且是標準的(沒有旋轉和平移)狀況下進行這個操做。下一個矩陣(這裏的下一個矩陣表示相對於上面的P)也是標準的:
咱們從本徵矩陣恢復的另一個相機相對於固定的相機進行了移動和旋轉。這一樣意味着咱們從這兩個相機矩陣中恢復的任何三維點都是擁有第一個相機在世界座標系中的原點(0,0,0)。
然而,這不是一個徹底解。H和Z在他們的書中展現瞭如何和爲何這樣的分解有四個可能的相機矩陣,可是僅有一個是正確的。正確的相機矩陣將產生帶有一個正Z值(點在攝像機的前面)的重構點。可是咱們僅有當學了下一步將要討論的三角測量和3維重建以後才能理解。
還有一個咱們能夠考慮添加到咱們的方法中的事情是錯誤檢測。屢次從點的匹配中計算基礎矩陣是錯誤的,並影響相機矩陣。帶有錯誤相機矩陣的進行三角測量是毫無心義的。咱們能夠安裝一個檢查來檢測是否旋轉成分是一個有效的旋轉矩陣。牢記旋轉矩陣必須是一個行列式值爲1(或者-1),咱們能夠簡單地進行以下作法:
bool CheckCoherentRotation(cv::Mat_<double>& R) { if(fabsf(determinant(R))-1.0 > 1e-07) { cerr<<"det(R) != +-1.0, this is not a rotation matrix"<<endl; return false; } return true; } We can now see how all these elements combine into a function that recovers the P matrices, as follows: void FindCameraMatrices(const Mat& K, const Mat& Kinv, const vector<KeyPoint>& imgpts1, const vector<KeyPoint>& imgpts2, Matx34d& P, Matx34d& P1, vector<DMatch>& matches, vector<CloudPoint>& outCloud ) { //Find camera matrices //Get Fundamental Matrix Mat F = GetFundamentalMat(imgpts1,imgpts2,matches); //Essential matrix: compute then extract cameras [R|t] Mat_<double> E = K.t() * F * K; //according to HZ (9.12) //decompose E to P' , HZ (9.19) SVD svd(E,SVD::MODIFY_A); Mat svd_u = svd.u; Mat svd_vt = svd.vt; Mat svd_w = svd.w; Matx33d W(0,-1,0,//HZ 9.13 1,0,0, 0,0,1); Mat_<double> R = svd_u * Mat(W) * svd_vt; //HZ 9.19 Mat_<double> t = svd_u.col(2); //u3 if (!CheckCoherentRotation(R)) { cout<<"resulting rotation is not coherent\n"; P1 = 0; return; } P1 = Matx34d(R(0,0),R(0,1),R(0,2),t(0), R(1,0),R(1,1),R(1,2),t(1), R(2,0),R(2,1),R(2,2),t(2)); }
此時,咱們擁有兩個咱們須要用來重建場景的相機。第一個相機是標準的,存儲在P變量中,第二個相機是咱們計算獲得的,構成基礎矩陣,存儲在P1變量中。下一部分咱們將揭示如何使用這些相機來得到場景的三維重建。
接下來咱們看一下從咱們已經得到的信息中恢復場景的3D結構的事情。像先前所作的,咱們應當看一下,用來完成這個事情咱們手邊所擁有的工具和信息。在前一部分,咱們從本徵矩陣和矩陣矩陣中得到了兩個相機矩陣。咱們已經討論了這些工具用來得到空間中一點的3D位置是如何的有用。那麼,我能夠返回咱們的匹配點來用數據填充等式。點對一樣對於計算咱們得到的近似計算的偏差有用。
如今咱們看一些如何使用OpenCV執行三角測量。此次咱們將會按照Tartley和Strum的三角測量的文章的步驟,文章中他們實現和比較了一些三角剖分的方法。咱們將實現他們的線性方法的一種,由於使用OpenCv很是容易編程。
回憶一下,咱們有兩個由2D點匹配和P矩陣產生的關鍵等式:x=PX和x’=P’X,x和x’是匹配的二維點,X是兩個相機進行拍照的真實世界三維點。若是咱們重寫這個等式,咱們能夠公式化爲一個線性方程系統,該系統能夠解出X的值,X正是咱們所指望尋找的值。假定X=(x,y,z,1)t(一個合理的點的假設,這些點離相機的中心不太近或者不太遠)產生一個形式爲AX=B的非齊次線性方程系統。咱們能夠編碼和解決這個問題,以下:
Mat_<double> LinearLSTriangulation( Point3d u,//homogenous image point (u,v,1) Matx34d P,//camera 1 matrix Point3d u1,//homogenous image point in 2nd camera Matx34d P1//camera 2 matrix ) { //build A matrix Matx43d A(u.x*P(2,0)-P(0,0),u.x*P(2,1)-P(0,1),u.x*P(2,2)-P(0,2), u.y*P(2,0)-P(1,0),u.y*P(2,1)-P(1,1),u.y*P(2,2)-P(1,2), u1.x*P1(2,0)-P1(0,0), u1.x*P1(2,1)-P1(0,1),u1.x*P1(2,2)-P1(0,2), u1.y*P1(2,0)-P1(1,0), u1.y*P1(2,1)-P1(1,1),u1.y*P1(2,2)-P1(1,2) ); //build B vector Matx41d B(-(u.x*P(2,3)-P(0,3)), -(u.y*P(2,3)-P(1,3)), -(u1.x*P1(2,3)-P1(0,3)), -(u1.y*P1(2,3)-P1(1,3))); //solve for X Mat_<double> X; solve(A,B,X,DECOMP_SVD); return X; }
咱們將得到由兩個2維點產生的3D點的一個近似。還有一個要注意的事情是,2D點用齊次座標表示,意味着x和y的值後面追加一個1。咱們必須確保這些點在標準化的座標系中,這意味着它們必須乘以先前的標定矩陣K.咱們可能注意到咱們簡單地利用KP矩陣來替代k矩陣和每個點相乘(每一點乘以KP),就是H和Z遍佈第9章的作法那樣。咱們如今能夠寫一個關於點匹配的循環語句來得到一個完整的三角測量,以下:
double TriangulatePoints( const vector<KeyPoint>& pt_set1, const vector<KeyPoint>& pt_set2, const Mat&Kinv, const Matx34d& P, const Matx34d& P1, vector<Point3d>& pointcloud) { vector<double> reproj_error; for (unsigned int i=0; i<pts_size; i++) { //convert to normalized homogeneous coordinates Point2f kp = pt_set1[i].pt; Point3d u(kp.x,kp.y,1.0); Mat_<double> um = Kinv * Mat_<double>(u); u = um.at<Point3d>(0); Point2f kp1 = pt_set2[i].pt; Point3d u1(kp1.x,kp1.y,1.0); Mat_<double> um1 = Kinv * Mat_<double>(u1); u1 = um1.at<Point3d>(0); //triangulate Mat_<double> X = LinearLSTriangulation(u,P,u1,P1); //calculate reprojection error Mat_<double> xPt_img = K * Mat(P1) * X; Point2f xPt_img_(xPt_img(0)/xPt_img(2),xPt_img(1)/xPt_img(2)); reproj_error.push_back(norm(xPt_img_-kp1)); //store 3D point pointcloud.push_back(Point3d(X(0),X(1),X(2))); } //return mean reprojection error Scalar me = mean(reproj_error); return me[0]; }
在下面的圖像中咱們將看到一個兩個圖像三角測量的結果。這兩個圖像來至於P-11序列:
http://cvlab.epfl.ch/~strecha/multiview/denseMVS.html.
上面的兩個圖像是原始場景的兩個視圖,下面的一對圖像是從兩個視圖重構獲得的點雲視圖,包含着估計相機朝向噴泉。咱們能夠看到右手邊紅色磚塊牆部分是如何重構的,而且也能夠看到突出於牆的噴泉。
然而,像咱們前面提到的那樣,關於重構存在着這樣的一個問題:重構僅能達到尺度上的。咱們應當花一些時間來理解達到尺度(up-to-scale)的意思。咱們得到的兩個攝像機之間的運動存在一個隨意測量的單元,也就是說,它不是用釐米或者英寸,而是簡單地給出尺度單位。咱們重構的相機將是尺度單元距離上的分開。這在很大程度上暗示咱們應當決定過會從新得到更多的相機,由於每一對相機都擁有各自的尺度單元,而不是一個通常的尺度。
如今咱們將討論咱們創建的偏差測量是如何可能的幫助咱們來找到一個更加魯棒性的重構。首先,咱們須要注意重投影意味着咱們簡單地利用三角化的3D點而且將這些點重塑到相機上以得到一個重投影的2D點。若是這個距離很大,這意味着咱們在三角測量中存在一個偏差,所以,在最後的結果中,咱們可能不想包含這個點。咱們的全局測量是平均重投影距離而且可能提供一個暗示——咱們的三角剖分整體執行的怎麼樣。高的重投影率可能代表P矩陣存在問題,所以該問題多是存在於本徵矩陣的計算或者匹配特徵點中。
咱們應當簡短地回顧一下在前一部分咱們討論的相機矩陣。咱們提到相機矩陣P1的分解能夠經過四個不一樣的方式進行分解,可是僅有一個分解是正確的。既然,咱們知道如何三角化一個點,咱們能夠增長一個檢測來看四個相機矩陣中哪個是有效地的。咱們應當跳過在一點實現上的細節,由於它們是本書隨書實例代碼中的精選(專題)。
下一步,咱們將要看一下從新得到直視場景的更多的相機,而且組合這些3D重建的結果。
既然咱們知道了如何從兩個相機中恢復運動和場景幾何,簡單地應用相同的過程得到額外相機的參數和更多場景點彷佛不重要。事實上,這件事並不簡單由於咱們僅能夠得到一個達到尺度的重構,而且每對圖像給咱們一個不一樣的尺度。
有一些方法能夠正確的從多個視場中重構3D場景數據。一個方法是後方交會(resection)或者相機姿態估計(camera pose estimation),也被稱爲PNP(Perspective-N-Point),這裏咱們將使用咱們已經找到的場景點來解決一個新相機的位置。另一個方法是三角化更多的點而且看它們是如何適應於咱們存在的場景幾何的。憑藉ICP(Iterative Closest Point )咱們能夠得到新相機的位置。在這一章,咱們將討論使用OpenCV的sovlePnP函數來完成第一個方法。
第一步咱們選擇這樣的重構類型即便用相機後方交會的增長的3D重構,來得到一個基線場景結構。由於咱們將基於一個已知的場景結構來尋找任何相機的位置,咱們須要找到要處理的一個初始化的結構和一條基線。咱們可使用先前討論的方法——例如,在第一個視頻幀和第二個視頻幀之間,經過尋找相機矩陣(使用FindCameraMatrices函數)來得到一條基線而且三角化幾何(使用TriangulatePoints函數)。
發現一個基礎結構後,咱們能夠繼續。然而,咱們的方法須要至關多的數據記錄。首先,咱們須要注意solvePnP函數須要兩個對齊的3D和2D點的矢量。對齊的矢量意味着一個矢量的第i個位置與另一個矢量的第i位置對齊。爲了得到這些矢量,咱們須要在咱們早前恢復的3D點中找到這些點,這些點與在咱們新視頻幀下的2D點是對齊的。完成這個的一個簡單的方式是,對於雲中的每個3D點,附加一個來至2D點的矢量。而後咱們可使用特徵匹配來得到一個匹配對。
讓咱們爲一個3D點引入一個新的結構,以下:
struct CloudPoint { cv::Point3d pt; std::vector<int>index_of_2d_origin; };
它容納,3D點和一個容器,容器內的元素爲每幀圖像上2D點的索引值,這些2D點用來計算3D點。當三角化一個新的3D點時,index_of_2d_origin的信息必須被初始化,來記錄在三角化中哪些相機涉及到。然而,咱們可使用它來從咱們的3D點雲追溯到每一幀上的2D點,以下:
std::vector<CloudPoint> pcloud; //our global 3D point cloud //check for matches between i'th frame and 0'th frame (and thus the current cloud) std::vector<cv::Point3f> ppcloud; std::vector<cv::Point2f> imgPoints; vector<int> pcloud_status(pcloud.size(),0); //scan the views we already used (good_views) for (set<int>::iterator done_view = good_views.begin(); done_view != good_views.end(); ++done_view) { int old_view = *done_view; //a view we already used for reconstrcution //check for matches_from_old_to_working between <working_view>'th frame and <old_view>'th frame (and thus the current cloud) std::vector<cv::DMatch> matches_from_old_to_working = matches_matrix[std::make_pair(old_view,working_view)]; //scan the 2D-2D matched-points for (unsigned int match_from_old_view=0; match_from_old_view<matches_from_old_to_working.size(); match_from_old_view++) { // the index of the matching 2D point in <old_view> int idx_in_old_view = matches_from_old_to_working[match_from_old_view].queryIdx; //scan the existing cloud to see if this point from <old_view> exists for (unsigned int pcldp=0; pcldp<pcloud.size(); pcldp++) { // see if this 2D point from <old_view> contributed to this 3D point in the cloud if (idx_in_old_view == pcloud[pcldp].index_of_2d_origin[old_view] && pcloud_status[pcldp] == 0) //prevent duplicates { //3d point in cloud ppcloud.push_back(pcloud[pcldp].pt); //2d point in image <working_view> Point2d pt_ = imgpts[working_view][matches_from_old_to_ working[match_from_old_view].trainIdx].pt; imgPoints.push_back(pt_); pcloud_status[pcldp] = 1; break; } } } } cout<<"found "<<ppcloud.size() <<" 3d-2d point correspondences"<<endl;
如今,咱們有一個場景中3D點到一個新視頻幀中2D點水平對齊對,咱們可使用他們從新獲得相機的位置,以下:
cv::Mat_<double> t,rvec,R; cv::solvePnPRansac(ppcloud, imgPoints, K, distcoeff, rvec, t, false); //get rotation in 3x3 matrix form Rodrigues(rvec, R); P1 = cv::Matx34d(R(0,0),R(0,1),R(0,2),t(0), R(1,0),R(1,1),R(1,2),t(1), R(2,0),R(2,1),R(2,2),t(2));
既然咱們正在使用sovlePnPRansac函數而不是sovlePnP函數,由於它對於異常值有更好的魯棒性。既然咱們得到了一個新的P1矩陣,咱們能夠簡單的再次使用咱們早先定義的TriangualtePoints函數而且用更多的3D點來填充咱們的3D點雲。
在下面的圖像中,咱們看到一個增長的噴泉場景的重構(訪問:http://cvlab.epfl.ch/~strecha/multiview/denseMVS.html,),從第四個圖像。左上角的圖像是使用了4個圖像的重構;參加拍攝的相機用帶有白線的紅色簡單表示,箭頭表示了方向。其餘的圖像展現了更多 的相機來添加更多的點到點雲中。
SfM方法中最重要的一個部分是提純和最優化重構場景,一般被稱做BA過程(Bundle Adjustment)。這是一個優化步驟,這裏咱們得到的全部數據適應於一個統一的模型。3D點的位置和相機的位置都獲得了最優化,所以重投影偏差最小(也就是說估計的3D點重投影到圖像上接近於起源的2D點的位置)。這個過程一般須要解決帶有幾十千個參數指令的巨大的線性方程。這個過程可能會有些費力,可是咱們前面採起的步驟容許帶有一個束調節(bundle adjuster)的簡單的整合。前面看起來奇怪的事情變的清晰了。例如,咱們保留爲點雲中的每個3D點保存原始的2D點的理由。
束調節的一個實現算法是簡單稀疏束條調節SSBA(Simple Sparse Bundle Adjustment)庫。咱們將選擇它做爲咱們的BA優化器, 由於它擁有簡單的API。它僅須要少許的輸入參數,這些輸入參數咱們能夠至關簡單的從咱們的數據結構中建立。SSBA中咱們使用的關鍵對象是CommonInternasMetricBundleOptimizer函數,這個函數執行最優化。它須要相機參數,3D點雲,點雲中每個點相對應的2D圖像點,以及直視場景的相機。到如今爲止,利用這些參數應該很直接。咱們應當注意這個BA方法假定全部圖像經過一樣的硬件獲取,所以共同的內部,其餘操做模式可能不須要假定這樣的狀況。咱們能夠執行束調節(Bundle Adjustment),以下:
voidBundleAdjuster::adjustBundle( vector<CloudPoint>&pointcloud, const Mat&cam_intrinsics, conststd::vector<std::vector<cv::KeyPoint>>&imgpts, std::map<int ,cv::Matx34d>&Pmats ) { int N = Pmats.size(), M = pointcloud.size(), K = -1; cout<<"N (cams) = "<< N <<" M (points) = "<< M <<" K (measurements) = "<< K <<endl; StdDistortionFunction distortion; // intrinsic parameters matrix Matrix3x3d KMat; makeIdentityMatrix(KMat); KMat[0][0] = cam_intrinsics.at<double>(0,0); KMat[0][1] = cam_intrinsics.at<double>(0,1); KMat[0][2] = cam_intrinsics.at<double>(0,2); KMat[1][1] = cam_intrinsics.at<double>(1,1); KMat[1][2] = cam_intrinsics.at<double>(1,2); ... // 3D point cloud vector<Vector3d >Xs(M); for (int j = 0; j < M; ++j) { Xs[j][0] = pointcloud[j].pt.x; Xs[j][1] = pointcloud[j].pt.y; Xs[j][2] = pointcloud[j].pt.z; } cout<<"Read the 3D points."<<endl; // convert cameras to BA datastructs vector<CameraMatrix> cams(N); for (inti = 0; i< N; ++i) { intcamId = i; Matrix3x3d R; Vector3d T; Matx34d& P = Pmats[i]; R[0][0] = P(0,0); R[0][1] = P(0,1); R[0][2] = P(0,2); T[0] = P(0,3); R[1][0] = P(1,0); R[1][1] = P(1,1); R[1][2] = P(1,2); T[1] = P(1,3); R[2][0] = P(2,0); R[2][1] = P(2,1); R[2][2] = P(2,2); T[2] = P(2,3); cams[i].setIntrinsic(Knorm); cams[i].setRotation(R); cams[i].setTranslation(T); } cout<<"Read the cameras."<<endl; vector<Vector2d > measurements; vector<int> correspondingView; vector<int> correspondingPoint; // 2D corresponding points for (unsigned int k = 0; k <pointcloud.size(); ++k) { for (unsigned int i=0; i<pointcloud[k].imgpt_for_img.size(); i++) { if (pointcloud[k].imgpt_for_img[i] >= 0) { int view = i, point = k; Vector3d p, np; Point cvp = imgpts[i][pointcloud[k].imgpt_for_img[i]].pt; p[0] = cvp.x; p[1] = cvp.y; p[2] = 1.0; // Normalize the measurements to match the unit focal length. scaleVectorIP(1.0/f0, p); measurements.push_back(Vector2d(p[0], p[1])); correspondingView.push_back(view); correspondingPoint.push_back(point); } } } // end for (k) K = measurements.size(); cout<<"Read "<< K <<" valid 2D measurements."<<endl; ... // perform the bundle adjustment { CommonInternalsMetricBundleOptimizeropt(V3D::FULL_BUNDLE_FOCAL_ LENGTH_PP, inlierThreshold, K0, distortion, cams, Xs, measurements, correspondingView, correspondingPoint); opt.tau = 1e-3; opt.maxIterations = 50; opt.minimize(); cout<<"optimizer status = "<<opt.status<<endl; } ... //extract 3D points for (unsigned int j = 0; j <Xs.size(); ++j) { pointcloud[j].pt.x = Xs[j][0]; pointcloud[j].pt.y = Xs[j][1]; pointcloud[j].pt.z = Xs[j][2]; } //extract adjusted cameras for (int i = 0; i< N; ++i) { Matrix3x3d R = cams[i].getRotation(); Vector3d T = cams[i].getTranslation(); Matx34d P; P(0,0) = R[0][0]; P(0,1) = R[0][1]; P(0,2) = R[0][2]; P(0,3) = T[0]; P(1,0) = R[1][0]; P(1,1) = R[1][1]; P(1,2) = R[1][2]; P(1,3) = T[1]; P(2,0) = R[2][0]; P(2,1) = R[2][1]; P(2,2) = R[2][2]; P(2,3) = T[2]; Pmats[i] = P; } }
這個代碼,雖然很長,是主要的關於轉換咱們的內部數據結構到和來至SSBA的數據結構,而且調用最優化的過程。
下面的圖像展現了BA的效果。左邊的兩個圖像調整前的點雲中的點,來至兩個視角的觀察,而且右邊的圖像展現了優化後的點雲。變化至關明顯,而且從不一樣視圖獲得三角化點之間的不重合如今大部分統一了。咱們一樣能夠注意到調整建立了一個更好的平面重建。
當操做3D數據時,經過簡單地觀察重投影偏差測量或原始點信息很難快速的理解結果是否正確。另外一方面,若是咱們觀察點雲(itself),咱們能夠當即的檢查這個點是否有意義或者存在偏差。爲了可視化,咱們將使用一個頗有前途的OpenCV的姊妹工程,稱爲點雲庫(Point Cloud Librar)(PCL)。它帶有許多可視化和分析點雲的工具,例如找到一個平面,匹配點雲,分割目標以及排除異常值。若是咱們的目標不是一個點雲,而是一些高級信息例如3D模型,這些工具將很是有用。
首先,咱們須要在PCL的數據結構中表示咱們的點雲(本質上是3D點的列表)。能夠經過以下的作法實現:
pcl::PointCloud<pcl::PointXYZRGB>::Ptr cloud; void PopulatePCLPointCloud(const vector<Point3d>& pointcloud, const std::vector<cv::Vec3b>& pointcloud_RGB ) //Populate point cloud { cout<<"Creating point cloud..."; cloud.reset(new pcl::PointCloud<pcl::PointXYZRGB>); for (unsigned int i=0; i<pointcloud.size(); i++) { // get the RGB color value for the point Vec3b rgbv(255,255,255); if (pointcloud_RGB.size() >= i) { rgbv = pointcloud_RGB[i]; } // check for erroneous coordinates (NaN, Inf, etc.) if (pointcloud[i].x != pointcloud[i].x || isnan(pointcloud[i].x) || pointcloud[i].y != pointcloud[i].y || isnan(pointcloud[i].y) || pointcloud[i].z != pointcloud[i].z || isnan(pointcloud[i].z) || fabsf(pointcloud[i].x) > 10.0 || fabsf(pointcloud[i].y) > 10.0 || fabsf(pointcloud[i].z) > 10.0) { continue; } pcl::PointXYZRGB pclp; // 3D coordinates pclp.x = pointcloud[i].x; pclp.y = pointcloud[i].y; pclp.z = pointcloud[i].z; // RGB color, needs to be represented as an integer uint32_t rgb = ((uint32_t)rgbv[2] << 16 | (uint32_t)rgbv[1] << 8 | (uint32_t)rgbv[0]); pclp.rgb = *reinterpret_cast<float*>(&rgb); cloud->push_back(pclp); } cloud->width = (uint32_t) cloud->points.size(); // number of points cloud->height = 1; // a list of points, one row of data }
爲了可視化有一個好的效果,咱們也能夠提供彩色數據如同圖像中的RGB值。咱們一樣也能夠對原始點雲應用一個濾波器,這將消除多是異常值的點,使用統計移除移除(statistical outlier removal)(SOR)工具以下:
Void SORFilter() { pcl::PointCloud<pcl::PointXYZRGB>::Ptr cloud_filtered (new pcl::PointC loud<pcl::PointXYZRGB>); std::cerr<<"Cloud before SOR filtering: "<< cloud->width * cloud->height <<" data points"<<std::endl; // Create the filtering object pcl::StatisticalOutlierRemoval<pcl::PointXYZRGB>sor; sor.setInputCloud (cloud); sor.setMeanK (50); sor.setStddevMulThresh (1.0); sor.filter (*cloud_filtered); std::cerr<<"Cloud after SOR filtering: "<<cloud_filtered->width * cloud_filtered->height <<" data points "<<std::endl; copyPointCloud(*cloud_filtered,*cloud); }
而後,咱們可使用PCL的API來運行一個簡單的點雲的可視化器,以下:
Void RunVisualization(const vector<cv::Point3d>& pointcloud, const std::vector<cv::Vec3b>& pointcloud_RGB) { PopulatePCLPointCloud(pointcloud,pointcloud_RGB); SORFilter(); copyPointCloud(*cloud,*orig_cloud); pcl::visualization::CloudViewer viewer("Cloud Viewer"); // run the cloud viewer viewer.showCloud(orig_cloud,"orig"); while (!viewer.wasStopped ()) { // NOP } }
下面的圖像展現了統計移除移除工具(statistical outlier removal tool)使用以後的輸出結果。左手邊的圖像是SfM的原始結果點雲。右手邊的圖像展現通過SOR操做濾波以後的點雲。咱們可以注意到一些離羣的點被移除了,剩下了一個更乾淨的點雲。
咱們能夠在這本書提供的材料中找到SfM的實例代碼。咱們如今看一些怎麼樣編譯,運行和利用它。代碼使用CMake,一個交叉編譯環境,相似於Maven或者SCons。咱們一樣應當確保咱們有下面的全部前提條件來編譯咱們的應用程序:
• OpenCV v2.3 or highe
• PCL v1.6 or higher
• SSBA v3.0 or higher
首先,咱們必須創建編譯環境。爲此,咱們可能建立一個文件夾,命名爲build,咱們將全部編譯相關的文件存儲在這裏。如今咱們將假定全部的命令行操做都是在build/文件夾內,雖然這個過程是相似的(取決於文件的位置),即便沒有使用build文件夾。
咱們應當確保CMake能夠找到SSBA和PCL,若是PCL正確的安裝了,那沒有問題。然而,咱們必須經過-DSSBA_LIBRARY_DIR=...編譯參數設置正確的位置來找到SSBA的預編譯庫。若是咱們正在使用Windows操做系統,咱們可使用Microsoft Visual Studio來編譯,所以,咱們應當運行下面的的命令:
cmake –G "Visual Studio 10" -DSSBA_LIBRARY_DIR=../3rdparty/SSBA-3.0/ build/ ..
若是咱們使用Linux,Mac Os,或者其餘Unix-Like操做系統,咱們執行下面的命令:
cmake –G "Unix Makefiles" -DSSBA_LIBRARY_DIR=../3rdparty/SSBA-3.0/build/ ..
若是咱們喜歡使用MacOS上的XCode,執行下面的命令:
cmake –G Xcode -DSSBA_LIBRARY_DIR=../3rdparty/SSBA-3.0/build/ ..
CMake一樣能夠爲Eclipse,Codeblocks,和更多的環境編譯宏命令。CMake完成建立編譯環境以後,咱們準備編譯。若是咱們正在使用一個Unix-like系統,咱們能夠簡單地執行這個生成工具(the make utility),不然咱們應當使用咱們開發環境的編譯過程。
編譯完成以後,咱們應當得到了一個執行程序名爲ExploringSfMExex,這用來運行SfM過程。不帶參數運行這個程序會致使以下的顯示: USAGE: ./ExploringSfMExec <path_to_images>
爲了在圖像集上執行這個過程,咱們應當提供位置做爲驅動來找到圖像文件。若是提供了有效的位置,過程開始,我應當看到這個進程和屏幕上的調試信息。程序的結束將顯示源於圖像的點雲。按1和2鍵,能夠切換到調整的(adjusted)點雲和非調整的(non-adjusted)點雲。
在這一章,咱們已經看到了OpenCV是怎樣用一個既簡單編碼又好理解的方式來幫助咱們處理來至運動的結構。OpenCV的API包含了一些有用的函數和數據結構,這使得咱們生活的更加輕鬆,一樣地協助咱們有一個更清潔的實現。
然而,最早進的SfM方法更復雜。那裏存在不少問題,咱們選擇忽略,喜歡簡單化,以及在這些地方一般有更多的錯誤檢查。對於不一樣的SfM成分,咱們選擇的方法一樣能夠再次訪問。例如,H和Z提出了一個高精度的三角測量方法,甚至使用N-視圖三角測量,曾經他們利用多個圖像理解特徵之間的關係。
若是咱們想延伸和深化熟悉SfM,固然,咱們將從觀察其餘開源SfM庫中收益。一個特別感興趣的工程是libMV,它實現了大量的SfM成分,經過互換,這可能得到最好的結果。華盛頓大學有一個偉大的做品,爲不少類型SfM(Bundler and Visual SfM)提供了工具。這項做品的靈感來至於微軟的在線產品,稱做PhotoSynth。網上有不少容易訪問到的SfM的實現,而且一我的僅需來找到更多的SfM的實現。
咱們沒有深刻討論的另一個重要的關係是SfM和可視化定位以及映射,更好的稱爲同步定位和映射(SLAM)方法。在這一章,咱們已經處理給出的圖像集和一個視頻序列,而且在這些狀況下,使用SfM是可行的。然而,一些應用沒有預記錄的數據集而且必須動態時引導重建。這個過程被成爲映射,而且當咱們正在使用特徵匹配和2D跟蹤以及三角測量後來建立世界的3D映射時,這個過程被完成。
在下一章,咱們將看到如何使用機器學習中的各類技術利用OpenCV從圖像中提出車牌數字。
• Multiple View Geometry in Computer Vision,Richard Hartley and Andrew Zisserman,Cambridge University Press• Triangulation, Richard I. Hartley and Peter Sturm, Computer vision and image understanding, Vol. 68, pp. 146-157• http://cvlab.epfl.ch/~strecha/multiview/denseMVS.html• On Benchmarking Camera Calibration and Multi-View Stereo for High Resolution Imagery,C. Strecha, W. von Hansen, L. Van Gool, P. Fua,and U. Thoennessen, CVPR• http://www.inf.ethz.ch/personal/chzach/opensource.html• http://www.ics.forth.gr/~lourakis/sba/• http://code.google.com/p/libmv/• http://www.cs.washington.edu/homes/ccwu/vsfm/• http://phototour.cs.washington.edu/bundler/• http://photosynth.net/• http://en.wikipedia.org/wiki/Simultaneous_localization_and_mapping• http://pointclouds.org• http://www.cmake.org