從運動中恢復結構以便能更好的通過攝像機移動來提取圖像幾何結構。在書中爲了使用單目相機,一個離散且稀疏的視頻幀集合,而不是連續的視頻流。這在後面兩兩循環組合配對提供了方便性。
主要內容:
1:從兩幅圖像估計攝像機的運動姿態。
2:重構場景
3:從視圖中重構
4:重構細化
5:可視化三維點雲
I:假定使用一個標定過的攝像機——一個先前標定過的攝像機。前面的博客也提到如何進行相機標定。因此,我們假定攝像機內參數存在,並且具體化到K矩陣中,K矩陣爲攝像機標定過程的一個結果輸出。
在程序中初始化相機標定內參數:
//load calibration matrix
cv::FileStorage fs;
if(fs.open(imgs_path_+ "\\out_camera_data.yml",cv::FileStorage::READ)) //打開校正參數文件
{
fs["camera_matrix"]>>cam_matrix;
fs["distortion_coefficients"]>>distortion_coeff;
} else {
//若沒有標定文件,就組合一個標定內參數
cv::Size imgs_size = imgs_[0].size();
double max_w_h = MAX(imgs_size.height,imgs_size.width);
cam_matrix = (cv::Mat_<double>(3,3) << max_w_h , 0 , imgs_size.width/2.0,
0, max_w_h, imgs_size.height/2.0,
0, 0, 1);
distortion_coeff = cv::Mat_<double>::zeros(1,4);
}
K = cam_matrix;
cv::invert(K, Kinv); //對內參數進行取反
distortion_coeff.convertTo(distcoeff_32f,CV_32FC1);
K.convertTo(K_32f,CV_32FC1);
若有標定文件,就從文件中導入,若沒有標定文件,就組合一個相機內參K,根據圖像大小就可以組合,畸變參數設爲0,對相機內參取反得Kinv,轉換成32FC1精度。
II:獲取圖像
書中在獲取圖像時,採用給定目錄,逐個讀取目錄中的圖像,保存在一個std::vector& imgs中。我們也可以挨個讀取,或者截取視頻幀。
我覺得這是一個常規代碼片,放在下面供以後隨便調用。
//給定目錄的路徑,讀取目錄下圖像文件,保存圖片名,設置圖像縮放比率
void open_imgs_dir(char* dir_name, std::vector<cv::Mat>& images, std::vector<std::string>& images_names, double downscale_factor) {
if (dir_name == NULL) {
return;
}
string dir_name_ = string(dir_name);
vector<string> files_;
#ifndef WIN32
//open a directory the POSIX way
//linux或者macos下面讀取圖片,需要包含#include <dirent.h>
DIR *dp;
struct dirent *ep;
dp = opendir (dir_name);
if (dp != NULL)
{
while (ep = readdir (dp)) {
if (ep->d_name[0] != '.')
files_.push_back(ep->d_name);
}
(void) closedir (dp);
}
else {
cerr << ("Couldn't open the directory");
return;
}
#else
//open a directory the WIN32 way
HANDLE hFind = INVALID_HANDLE_VALUE;
WIN32_FIND_DATA fdata;
if(dir_name_[dir_name_.size()-1] == '\\' || dir_name_[dir_name_.size()-1] == '/') {
dir_name_ = dir_name_.substr(0,dir_name_.size()-1);
}
hFind = FindFirstFile(string(dir_name_).append("\\*").c_str(), &fdata);
if (hFind != INVALID_HANDLE_VALUE)
{
do
{
if (strcmp(fdata.cFileName, ".") != 0 &&
strcmp(fdata.cFileName, "..") != 0)
{
if (fdata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
continue; // a diretory
}
else
{
files_.push_back(fdata.cFileName);
}
}
}
while (FindNextFile(hFind, &fdata) != 0);
} else {
cerr << "can't open directory\n";
return;
}
if (GetLastError() != ERROR_NO_MORE_FILES)
{
FindClose(hFind);
cerr << "some other error with opening directory: " << GetLastError() << endl;
return;
}
FindClose(hFind);
hFind = INVALID_HANDLE_VALUE;
#endif
for (unsigned int i=0; i<files_.size(); i++) {
if (files_[i][0] == '.' || !(hasEndingLower(files_[i],"jpg")||hasEndingLower(files_[i],"png"))) {
continue;
}
cv::Mat m_ = cv::imread(string(dir_name_).append("/").append(files_[i]));
if(downscale_factor != 1.0)
cv::resize(m_,m_,Size(),downscale_factor,downscale_factor);//是否進行縮放
images_names.push_back(files_[i]);//存儲圖片名稱
images.push_back(m_);//保存圖片成Mat
}
}
這段代碼片調用了兩個函數進行圖片名稱拼接。
bool hasEndingLower (string const &fullString_, string const &_ending)
{
string fullstring = fullString_, ending = _ending;
transform(fullString_.begin(),fullString_.end(),fullstring.begin(),::tolower); // 放在最後
return hasEnding(fullstring,ending);
}
bool hasEnding (std::string const &fullString, std::string const &ending)
{
if (fullString.length() >= ending.length()) {
return (0 == fullString.compare (fullString.length() - ending.length(), ending.length(), ending));
} else {
return false;
}
}
接下來就是進行了一步轉化,先確保圖像是8UC3的,然後再灰度化,就是預處理步驟。
//確保圖像是CV_8UC3
for (unsigned int i=0; i<imgs_.size(); i++) {
imgs_orig.push_back(cv::Mat_<cv::Vec3b>());//cv::Vec3b是8UC3的代替,typedef Vec<uchar, 3> Vec3b;
if (!imgs_[i].empty()) {
if (imgs_[i].type() == CV_8UC1) {
cvtColor(imgs_[i], imgs_orig[i], CV_GRAY2BGR);
} else if (imgs_[i].type() == CV_32FC3 || imgs_[i].type() == CV_64FC3) {
imgs_[i].convertTo(imgs_orig[i],CV_8UC3,255.0);
} else {
imgs_[i].copyTo(imgs_orig[i]);
}
}
imgs.push_back(cv::Mat());
cvtColor(imgs_orig[i],imgs[i], CV_BGR2GRAY);
//定義vector<std::vector<cv::KeyPoint> > imgpts,imgpts_good;用於保存圖像特徵點。
imgpts.push_back(std::vector<cv::KeyPoint>());//
imgpts_good.push_back(std::vector<cv::KeyPoint>());
std::cout << ".";
}
III:特徵提取
有了圖像後,對圖像進行特徵匹配,計算兩幅圖像的基礎矩陣,本質矩陣。這也是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個點對來計算它,但是我們希望獲得更多的點對來得到一個魯棒性的解。
在計算機視覺中,特徵提取和描述子匹配是一個基礎的過程,並且用在許多方法中來執行各種各樣的操作。例如,檢測圖像中一個目標的位置和方向,或者通過給出一個查詢圖像在大數據圖像中找到相似的圖像。從本質上講,提取意味着在圖像中選擇點,使得獲得好的特徵,並且爲它們計算一個描述子。一個描述子是含有多個數據的向量,用來描述在一個圖像中圍繞着特徵點的周圍環境。不同的方法有不同的長度和數據類型來表示描述子矢量。匹配是使用它的描述子從另外一個圖像中找到一組與之對應的特徵。OpenCV提供了非常簡單和有效的方法支持特徵提取和匹配。關於特徵匹配的更多信息可以在Chapter 3少量(無)標記增強現實中找到。
本書採用三種特徵提取方法:一般的富特徵提取,基於GPU的特徵提取,使用光流法進行特徵提取。
1:一般的富特徵提取
detector = FeatureDetector::create("PyramidFAST");
extractor = DescriptorExtractor::create("ORB");
std::cout << " -------------------- extract feature points for all images -------------------\n";
detector->detect(imgs, imgpts);//對所有圖像進行特徵點檢測
extractor->compute(imgs, imgpts, descriptors);//計算所有特徵點的描述子
std::cout << " ------------------------------------- done -----------------------------------\n";
2:使用GPU的富特徵提取
extractor = new SURF();//這個類可以直接提取圖像的特徵點和計算描述子。
std::cout << " -------------------- extract feature points for all images (GPU) -------------------\n";
imgpts.resize(imgs_.size());
descriptors.resize(imgs_.size());
CV_PROFILE("extract",
for(int img_i=0;img_i<imgs_.size();img_i++) {
GpuMat _m; _m.upload(imgs_[img_i]);
(*extractor)(_m,GpuMat(),imgpts[img_i],descriptors[img_i]);//這一行就是直接提取每幅圖像的特徵點和描述子
cout << ".";
}
)
3:光流法:
光流是匹配來至一幅圖像選擇的點到另外一幅圖像選擇點的過程,假定這兩個圖像是一個視頻序列的一部分並且它們彼此非常相近。大多數的光流方法比較一個小的區域,稱爲搜索窗口或者塊,這些塊圍繞着圖像A中的每一點和同樣區域的圖像B中的每一點。遵循計算機視覺中一個非常普通的規則,稱爲亮度恆定約束(brightness constancy constraint)(和其他名字),圖像中的這些小塊從一個圖像到另外一個圖像不會有太大的變化,因此,他們的幅值差接近於0。除了匹配塊,更新的光流方法使用一些額外的方法來獲得更好的結果。其中一個方法就是使用圖像金字塔,它是圖像越來越小的尺寸(大小)版本,這考慮到了工作的從粗糙到精緻——計算機視覺中一個非常有用的技巧。另外一個方法是定義一個流場上的全局約束,假定這些點相互靠近,向同一方向一起運動。
爲了和富特徵的特徵提取方法進行兼容,這裏先用Fast特徵提取算法只計算特徵點。之後用光流法進行特徵匹配。
//detect keypoints for all images
FastFeatureDetector ffd;
// DenseFeatureDetector ffd;
ffd.detect(imgs, imgpts);
IV:特徵匹配
特徵提取完畢後,就要進行特徵匹配。書中使用openmp進行並行for運算,需要安裝openmp。這裏使用循環組合的方法每兩幅圖像都要進行匹配。增加精度,但速度超慢。
int loop1_top = imgs.size() - 1, loop2_top = imgs.size();
int frame_num_i = 0;
#pragma omp parallel for //並行計算
for (frame_num_i = 0; frame_num_i < loop1_top; frame_num_i++) {
for (int frame_num_j = frame_num_i + 1; frame_num_j < loop2_top; frame_num_j++)
{
std::cout << "------------ Match " << imgs_names[frame_num_i] << ","<<imgs_names[frame_num_j]<<" ------------\n";//打印當前是哪兩幀進行匹配
std::vector<cv::DMatch> matches_tmp;
feature_matcher->MatchFeatures(frame_num_i,frame_num_j,&matches_tmp);
//std::map<std::pair<int,int> ,std::vector<cv::DMatch> > matches_matrix;這個變量保存的是那兩幀進行匹配,及匹配的結果。 matches_matrix[std::make_pair(frame_num_i,frame_num_j)] = matches_tmp;
std::vector<cv::DMatch> matches_tmp_flip = FlipMatches(matches_tmp);//對兩幅圖像進行調換位置,完成交叉匹配檢查過濾
matches_matrix[std::make_pair(frame_num_j,frame_num_i)] = matches_tmp_flip;
}
}
接下來就分析下這個裏面最關鍵的函數feature_matcher->MatchFeatures。這個feature_matcher是前面特徵提取所介紹的三種方法。每一種方法都可以進行匹配。
1:一般的富特徵匹配。
採用暴力BF匹配法加上HAMMING距進行匹配,得到的匹配組很多,當然錯誤的也很多。
//輸入當前匹配的幀以及得到的匹配
void RichFeatureMatcher::MatchFeatures(int idx_i, int idx_j, vector<DMatch>* matches) {
#ifdef __SFM__DEBUG__
const Mat& img_1 = imgs[idx_i];
const Mat& img_2 = imgs[idx_j];
#endif
//獲取當前匹配幀的關鍵點和描述子
const vector<KeyPoint>& imgpts1 = imgpts[idx_i];
const vector<KeyPoint>& imgpts2 = imgpts[idx_j];
const Mat& descriptors_1 = descriptors[idx_i];
const Mat& descriptors_2 = descriptors[idx_j];
std::vector< DMatch > good_matches_,very_good_matches_;
std::vector<KeyPoint> keypoints_1, keypoints_2;
//打印關鍵點的數量
stringstream ss; ss << "imgpts1 has " << imgpts1.size() << " points (descriptors " << descriptors_1.rows << ")" << endl;
cout << ss.str();
stringstream ss1; ss1 << "imgpts2 has " << imgpts2.size() << " points (descriptors " << descriptors_2.rows << ")" << endl;
cout << ss1.str();
keypoints_1 = imgpts1;
keypoints_2 = imgpts2;
if(descriptors_1.empty()) {
CV_Error(0,"descriptors_1 is empty");
}
if(descriptors_2.empty()) {
CV_Error(0,"descriptors_2 is empty");
}
//使用BF暴力匹配法進行HAMMING匹配,允許交叉檢查
BFMatcher matcher(NORM_HAMMING,true); //allow cross-check. use Hamming distance for binary descriptor (ORB)
std::vector< DMatch > matches_;
if (matches == NULL) {
matches = &matches_;
}
if (matches->size() == 0) {
matcher.match( descriptors_1, descriptors_2, *matches );
}
assert(matches->size() > 0);
// double max_dist = 0; double min_dist = 1000.0;
// //-- Quick calculation of max and min distances between keypoints
// for(unsigned int i = 0; i < matches->size(); i++ )
// {
// double dist = (*matches)[i].distance;
// if (dist>1000.0) { dist = 1000.0; }
// if( dist < min_dist ) min_dist = dist;
// if( dist > max_dist ) max_dist = dist;
// }
//
//#ifdef __SFM__DEBUG__
// printf("-- Max dist : %f \n", max_dist );
// printf("-- Min dist : %f \n", min_dist );
//#endif
vector<KeyPoint> imgpts1_good,imgpts2_good;
// if (min_dist <= 0) {
// min_dist = 10.0;
// }
//去除每一個重複匹配的訓練點,即一個訓練點有多個查詢點 Eliminate any re-matching of training points (multiple queries to one training)
// double cutoff = 4.0*min_dist;
std::set<int> existing_trainIdx;
for(unsigned int i = 0; i < matches->size(); i++ )
{
//歸一化匹配:有時圖像數下標就是訓練數的下標"normalize" matching: somtimes imgIdx is the one holding the trainIdx
if ((*matches)[i].trainIdx <= 0) {
(*matches)[i].trainIdx = (*matches)[i].imgIdx;
}
//這裏set::find的意思是在set中查找鍵值,若找到則返回鍵值迭代器的位置,若找不到就返回set::end,這是爲了
//防止有一個訓練值對應多個查詢值。每個訓練值檢測過後,就存入set中,下次這個訓練值再出現在匹配對中時,就判斷爲多對一去除。
//另外這裏的if中也要判斷訓練值要在0和關鍵點數量之間。
if( existing_trainIdx.find((*matches)[i].trainIdx) == existing_trainIdx.end() &&
(*matches)[i].trainIdx >= 0 && (*matches)[i].trainIdx < (int)(keypoints_2.size()) /*&&
(*matches)[i].distance > 0.0 && (*matches)[i].distance < cutoff*/ )
{
good_matches_.push_back( (*matches)[i]);//符合條件的匹配組
imgpts1_good.push_back(keypoints_1[(*matches)[i].queryIdx]);//符合條件的匹配組的查詢關鍵點
imgpts2_good.push_back(keypoints_2[(*matches)[i].trainIdx]);//符合條件的匹配組的訓練關鍵點
existing_trainIdx.insert((*matches)[i].trainIdx);
}
}
//這裏第一步的匹配就完成了,這時會有很多錯誤的匹配。
#ifdef __SFM__DEBUG__
cout << "keypoints_1.size() " << keypoints_1.size() << " imgpts1_good.size() " << imgpts1_good.size() << endl;
cout << "keypoints_2.size() " << keypoints_2.size() << " imgpts2_good.size() " << imgpts2_good.size() << endl;
{
//-- Draw only "good" matches
Mat img_matches;
drawMatches( img_1, keypoints_1, img_2, keypoints_2,
good_matches_, img_matches, Scalar::all(-1), Scalar::all(-1),
vector<char>(), DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS );
//-- Show detected matches
stringstream ss; ss << "Feature Matches " << idx_i << "-" << idx_j;
imshow(ss.str() , img_matches );
waitKey(500);
destroyWindow(ss.str());
}
#endif
//下面將用計算基礎矩陣的方式再次優化匹配組。
vector<uchar> status;
vector<KeyPoint> imgpts2_very_good,imgpts1_very_good;
assert(imgpts1_good.size() > 0);
assert(imgpts2_good.size() > 0);
assert(good_matches_.size() > 0);
assert(imgpts1_good.size() == imgpts2_good.size() && imgpts1_good.size() == good_matches_.size());
//Select features that make epipolar sense
//計算匹配組的基礎的矩陣,再次優化匹配組,去除錯誤匹配。
GetFundamentalMat(keypoints_1,keypoints_2,imgpts1_very_good,imgpts2_very_good,good_matches_);
//Draw matches
#ifdef __SFM__DEBUG__
{
//-- Draw only "good" matches
Mat img_matches;
drawMatches( img_1, keypoints_1, img_2, keypoints_2,
good_matches_, img_matches, Scalar::all(-1), Scalar::all(-1),
vector<char>(), DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS );
//-- Show detected matches
imshow( "Good Matches", img_matches );
waitKey(100);
destroyWindow("Good Matches");
}
#endif
}
2:使用GPU加速的特徵匹配
使用knn比率測試進行匹配,即最優匹配的距離/次優大於0.7,去除錯誤匹配。這時還會有錯誤匹配。
//同樣使用暴力匹配,只不過有gpu加速。matching descriptor vectors using Brute Force matcher
BruteForceMatcher_GPU<L2<float> > matcher;
std::vector< DMatch > matches_;
if (matches == NULL) {
matches = &matches_;
}
if (matches->size() == 0) {
cout << "match " << descriptors_1.rows << " vs. " << descriptors_2.rows << " ...";
if(use_ratio_test) {
vector<vector<DMatch> > knn_matches;
GpuMat trainIdx,distance,allDist;
CV_PROFILE("match",
matcher.knnMatchSingle(descriptors_1,descriptors_2,trainIdx,distance,allDist,2); //使用knn比率測試進行匹配
matcher.knnMatchDownload(trainIdx,distance,knn_matches);
)
(*matches).clear();
//ratio test
for(int i=0;i<knn_matches.size();i++) {
if(knn_matches[i][0].distance / knn_matches[i][1].distance < 0.7) {//knn比率判別,去除匹配不好的匹配組
(*matches).push_back(knn_matches[i][0]);
}
}
cout << "kept " << (*matches).size() << " features after ratio test"<<endl;
} else {
CV_PROFILE("match",matcher.match( descriptors_1, descriptors_2, *matches );)
}
}
3:光流法進行匹配。大致就是每個左圖點在兩幅圖中是如何移動的,在右圖中找每一個光流點,在小區域進行匹配。
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);//關鍵點轉化成Point2f點
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;
一個特徵從左手邊圖像的一個位置移動到右手邊圖像的另外一個位置。但是我們有一組在右手邊圖像中檢測到的新的特徵,在光流中從這個圖像到左手邊圖像的特徵不一定是對齊的。我們必須使它們對齊。爲了找到這些丟失的特徵,我們使用一個k鄰近(KNN)半徑搜索,這給出了我們兩個特徵,即感興趣的點落入了2個像素半徑範圍內。
KNN比率測試實現,在SfM中這是一種常見的減少錯誤的方法。實質上,當我們對左手邊圖像上的一個特徵和右手邊圖像上的一個特徵進行匹配時,它作爲一個濾波器,用來移除混淆的匹配。如果右手邊圖像中兩個特徵太靠近,或者它們之間這個比例(the rate)太大(接近於1.0),我們認爲它們混淆了並且不使用它們。我們也安裝一個雙重防禦濾波器來進一步修剪匹配。
使用光流法代替豐富特徵的優點是這個過程通常更快並且可以適應更多的點,使重構更加稠密。在許多光流方法中也有一個塊整體運動的統一模型,在這個模型中,豐富的特徵匹配通常不考慮。使用光流要注意的是對於從同一個硬件獲取的連續圖像,它處理的很快,然而豐富的特徵通常不可知。它們之間的差異源於這樣的一個事實:光流法通常使用非常基礎的特徵,像圍繞着一個關鍵點的圖像塊,然而,高階豐富特徵(例如,SURF)考慮每一個特徵點的較高層次的信息。使用光流或者豐富的特徵是設計師根據應用程序的輸入所做的決定。
接下來將要求解攝像機矩陣,下一篇再分析。