特徵點估計相機運動的方法,主要是在關鍵點和描述子的計算非常耗時;而且在紋理資訊比較少的情況下,特徵點的數量會明顯減少。
解決方案:
1.保留特徵點,只計算關鍵點,不計算描述子,然後使用光流法跟蹤特徵點的運動,從而實現特徵點的匹配。
2.只計算關鍵點,不計算描述子。使用直接法計算下一時刻特徵點的位置,從而實現特徵點的匹配。
第一種方法,是把特徵點匹配換成光流法,估計相機運動時仍然採用對極幾何、pnp或icp演算法。仍然需要計算角點。
第二種方法,是通過畫素的灰度資訊,同時估計相機運動和點的投影,不要求提取到的點必須為角點,甚至可以是隨機的選點。
由於i(x,y,t) = i(x+dx,y+dy,t+dt),所以:\(\fracdx + \fracdy + \fracdt = 0\)
兩邊除dt,得:
\(\frac \frac + \frac \frac =- \frac\)
其中dx/dt為畫素在x軸上的運動速度,dy/dt為y軸上的速度,記為u,v。同時\(\frac\)為該點在x方向的梯度,另一項為y方向的梯度,記為\(i_\),\(i_\),寫成矩陣形式為:
我們計算的是u、v,所以乙個點無法計算,故假設該點附近的乙個視窗內的畫素都具有相同的運動。
考慮乙個大小為w*w的視窗,共有\(w^\)數量的畫素,有\(w^\)個方程:
這是乙個關於u,v的超定方程,傳統解法是求最小二乘解
這樣就得到畫素在影象間的運動速度u,v。由於畫素梯度僅在區域性有效,如果一次迭代不夠好,可以多迭代幾次這個方程。
影象梯度:
影象梯度一般也可以用中值差分:
dx(i,j) = [i(i+1,j) - i(i-1,j)]/2;
dy(i,j) = [i(i,j+1) - i(i,j-1)]/2;
lk光流程式:
vectorkp1;
//通過gftt來獲取角點
ptrdetector = gfttdetector::create(500, 0.01, 20); // maximum 500 keypoints
detector->detect(img1, kp1);
vectorpt1, pt2;
for (auto &kp: kp1) pt1.push_back(kp.pt);
vectorstatus;
vectorerror;
//直接呼叫lk光流函式
cv::calcopticalflowpyrlk(img1, img2, pt1, pt2, status, error);
該問題可以看成乙個優化問題:通過最小化灰度誤差來估計最優的畫素偏差。
相關**:
getpixelvalue:採用雙線性內插法,來估計乙個點的畫素:
公式為:
inline float getpixelvalue(const cv::mat &img, float x, float y)
class opticalflowtracker
void calculateopticalflow(const range &range);
private:
const mat &img1;
const mat &img2;
const vector&kp1;
vector&kp2;
vector&success;
bool inverse = true;
bool has_initial = false;
};void opticalflowsinglelevel(
const mat &img1,
const mat &img2,
const vector&kp1,
vector&kp2,
vector&success,
bool inverse, bool has_initial)
void opticalflowtracker::calculateopticalflow(const range &range)
double cost = 0, lastcost = 0;
bool succ = true; // indicate if this point succeeded
// gauss-newton iterations
eigen::matrix2d h = eigen::matrix2d::zero(); // hessian
eigen::vector2d b = eigen::vector2d::zero(); // bias
eigen::vector2d j; // jacobian
for (int iter = 0; iter < iterations; iter++) else
cost = 0;
// compute cost and jacobian
//對視窗的所有畫素進行計算
for (int x = -half_patch_size; x < half_patch_size; x++)
for (int y = -half_patch_size; y < half_patch_size; y++) else if (iter == 0)
// compute h, b and set cost;
b += -error * j;
cost += error * error;
if (inverse == false || iter == 0)
}// compute update
eigen::vector2d update = h.ldlt().solve(b); //求解方程u,v
if (std::isnan(update[0]))
if (iter > 0 && cost > lastcost)
// 更新 dx, dy
dx += update[0];
dy += update[1];
lastcost = cost;
succ = true;
if (update.norm() < 1e-2)
}success[i] = succ;
// kp2,找到了kp2和kp1的匹配點座標
kp2[i].pt = kp.pt + point2f(dx, dy);}}
上面光流只計算了單層光流,當相機運動較快的時候,單層光流容易達到乙個區域性極小值,這時可以引入影象金字塔來進行優化。
影象金字塔就是對同乙個影象進行縮放,得到不同解析度下的影象,以原始影象作為金字塔的底層,沒向上一層,就進行一定倍率的縮放。
由粗至精的好處是,當原始影象運動較大的時候,在上幾層的影象優化值裡,畫素運動會比較小,就避免了陷入區域性極小值。
void opticalflowmultilevel(
const mat &img1,
const mat &img2,
const vector&kp1,
vector&kp2,
vector&success,
bool inverse) ;
// 建立4層金字塔,圖1,圖2,每層縮小0.5
chrono::steady_clock::time_point t1 = chrono::steady_clock::now();
vectorpyr1, pyr2; // image pyramids
for (int i = 0; i < pyramids; i++) else
}chrono::steady_clock::time_point t2 = chrono::steady_clock::now();
auto time_used = chrono::duration_cast>(t2 - t1);
cout << "build pyramid time: " << time_used.count() << endl;
// coarse-to-fine lk tracking in pyramids
vectorkp1_pyr, kp2_pyr;
//最上層kp1的特徵點處理 縮小 0.5*0.5*0.5
for (auto &kp:kp1)
for (int level = pyramids - 1; level >= 0; level--)
}for (auto &kp: kp2_pyr)
kp2.push_back(kp);
}
完整**:
視覺SLAM十四講第四講
主要目標 理解李群與李代數的概念,掌握 so 3 se 3 與對應李代數的表示方式。理解 bch 近似的意義。學會在李代數上的擾動模型。使用 sophus 對李代數進行運算。旋轉矩陣自身是帶有約束的 正交且行列式為 1 它們作為優化變數時,會引入額外的約束,使優化變得困難。通過李群 李代數間的轉換關...
《視覺SLAM十四講》第2講
目錄本講主要內容 1 視覺slam中的感測器 2 經典視覺slam框架 3 slam問題的數學表述 想象乙個在室內的移動機械人在自由地探索室內的環境,那麼定位與建圖可以直觀地理解成 1 我在什麼地方?定位 2 周圍環境是怎樣的?建圖 而要完成定位和建圖則需要各種感測器的支援。感測器一般可以分為兩類,...
視覺SLAM十四講 第十二講筆記
講完了前端後端,這一講講的是回環檢測。比起之前的內容要容易理解多了 其思路是 回環檢測的作用 消除累積誤差。累積誤差是怎麼來的呢?前端根據區域性 相鄰或相近 幀給出位姿估計,後端也是根據前端已有的結果進行優化 比如p x k xk 1 p x k x p xk xk 1 這樣,當前端每一幀都存在誤差...