視覺十四講:第八講_光流法(特征點追蹤)


1.直接法的引出

特征點估計相機運動的方法,主要是在關鍵點和描述子的計算非常耗時;而且在紋理信息比較少的情況下,特征點的數量會明顯減少。
解決方案:
1.保留特征點,只計算關鍵點,不計算描述子,然后使用光流法跟蹤特征點的運動,從而實現特征點的匹配。
2.只計算關鍵點,不計算描述子。使用直接法計算下一時刻特征點的位置,從而實現特征點的匹配。

第一種方法,是把特征點匹配換成光流法,估計相機運動時仍然采用對極幾何、PnP或ICP算法。仍然需要計算角點。
第二種方法,是通過像素的灰度信息,同時估計相機運動和點的投影,不要求提取到的點必須為角點,甚至可以是隨機的選點。

2.LK光流法

光流法基於灰度不變強假設。
對於t時刻位於(x,y)處的像素,設t+dt時刻它運動到了(x+dx,y+dy)處,由於灰度不變,所以有:I(x,y,t) = I(x+dx,y+dy,t+dt).
將右邊進行泰勒展開,保留一階項:
\(I(x+dx,y+dy,t+dt) \approx I(x,y,t)+ \frac{\partial I}{\partial x}dx + \frac{\partial I}{\partial y}dy + \frac{\partial I}{\partial t}dt\)
由於I(x,y,t) = I(x+dx,y+dy,t+dt),所以:\(\frac{\partial I}{\partial x}dx + \frac{\partial I}{\partial y}dy + \frac{\partial I}{\partial t}dt = 0\)
兩邊除dt,得:
\(\frac{\partial I}{\partial x} \frac{dx}{dt} + \frac{\partial I}{\partial y} \frac{dy}{dt} =- \frac{\partial I}{\partial t}\)

其中dx/dt為像素在x軸上的運動速度,dy/dt為y軸上的速度,記為u,v。同時\(\frac{\partial I}{\partial x}\)為該點在x方向的梯度,另一項為y方向的梯度,記為\(I_{x}\),\(I_{y}\),寫成矩陣形式為:

我們計算的是u、v,所以一個點無法計算,故假設該點附近的一個窗口內的像素都具有相同的運動。
考慮一個大小為w*w的窗口,共有\(w^{2}\)數量的像素,有\(w^{2}\)個方程:

這是一個關於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光流程序:

    vector<KeyPoint> kp1;
    //通過GFTT來獲取角點
    Ptr<GFTTDetector> detector = GFTTDetector::create(500, 0.01, 20); // maximum 500 keypoints
    detector->detect(img1, kp1);

    vector<Point2f> pt1, pt2;
    for (auto &kp: kp1) pt1.push_back(kp.pt);
    vector<uchar> status;
    vector<float> error;
    //直接調用LK光流函數
    cv::calcOpticalFlowPyrLK(img1, img2, pt1, pt2, status, error);

3.高斯牛頓法實現光流

該問題可以看成一個優化問題:通過最小化灰度誤差來估計最優的像素偏差。
相關代碼:
GetPixelValue:采用雙線性內插法,來估計一個點的像素:
公式為:

inline float GetPixelValue(const cv::Mat &img, float x, float y) {
    // boundary check
    if (x < 0) x = 0;
    if (y < 0) y = 0;
    if (x >= img.cols) x = img.cols - 1;
    if (y >= img.rows) y = img.rows - 1;
    uchar *data = &img.data[int(y) * img.step + int(x)];
    float xx = x - floor(x);
    float yy = y - floor(y);
    return float(
        (1 - xx) * (1 - yy) * data[0] +
        xx * (1 - yy) * data[1] +
        (1 - xx) * yy * data[img.step] +
        xx * yy * data[img.step + 1]
    );
}

class OpticalFlowTracker {
public:
    OpticalFlowTracker(
        const Mat &img1_,
        const Mat &img2_,
        const vector<KeyPoint> &kp1_,
        vector<KeyPoint> &kp2_,
        vector<bool> &success_,
        bool inverse_ = true, bool has_initial_ = false) :
        img1(img1_), img2(img2_), kp1(kp1_), kp2(kp2_), success(success_), inverse(inverse_),
        has_initial(has_initial_) {}

    void calculateOpticalFlow(const Range &range);

private:
    const Mat &img1;
    const Mat &img2;
    const vector<KeyPoint> &kp1;
    vector<KeyPoint> &kp2;
    vector<bool> &success;
    bool inverse = true;
    bool has_initial = false;
};
void OpticalFlowSingleLevel(
    const Mat &img1,
    const Mat &img2,
    const vector<KeyPoint> &kp1,
    vector<KeyPoint> &kp2,
    vector<bool> &success,
    bool inverse, bool has_initial) {
    kp2.resize(kp1.size());
    success.resize(kp1.size());
    OpticalFlowTracker tracker(img1, img2, kp1, kp2, success, inverse, has_initial);
    //通過cv::parallel_for_並行調用了calculateOpticalFlow函數,循環次數為kp1.size(),傳入了tracker數據。
    parallel_for_(Range(0, kp1.size()),
                  std::bind(&OpticalFlowTracker::calculateOpticalFlow, &tracker, placeholders::_1));
}
void OpticalFlowTracker::calculateOpticalFlow(const Range &range) {
    // parameters
    int half_patch_size = 4;  //窗口的大小為8*8
    int iterations = 10;   //每一個角點迭代10次
    //總共的循環次數為 kp1.size()
    for (size_t i = range.start; i < range.end; i++) {
        auto kp = kp1[i];
        double dx = 0, dy = 0; // dx,dy need to be estimated
        if (has_initial) {
            dx = kp2[i].pt.x - kp.pt.x;
            dy = kp2[i].pt.y - kp.pt.y;
        }

        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++) {
            if (inverse == false) {  
                H = Eigen::Matrix2d::Zero();
                b = Eigen::Vector2d::Zero();
            } else {  //反向光流,H矩陣不變,只計算一次,每次迭代計算殘差b
                // only reset b
                b = Eigen::Vector2d::Zero();
            }

            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++) {
                    //計算誤差
                    double error = GetPixelValue(img1, kp.pt.x + x, kp.pt.y + y) -
                                   GetPixelValue(img2, kp.pt.x + x + dx, kp.pt.y + y + dy);;  
                    if (inverse == false) {  //雅可比矩陣,為第二個圖像在x+dx,y+dy處的梯度。梯度上面已講
                        J = -1.0 * Eigen::Vector2d(
                            0.5 * (GetPixelValue(img2, kp.pt.x + dx + x + 1, kp.pt.y + dy + y) -
                                   GetPixelValue(img2, kp.pt.x + dx + x - 1, kp.pt.y + dy + y)),
                            0.5 * (GetPixelValue(img2, kp.pt.x + dx + x, kp.pt.y + dy + y + 1) -
                                   GetPixelValue(img2, kp.pt.x + dx + x, kp.pt.y + dy + y - 1))
                        );
                    } else if (iter == 0) { //反向光流,計算第一張圖像的雅可比矩陣,10次迭代只計算一次
                        // in inverse mode, J keeps same for all iterations
                        // NOTE this J does not change when dx, dy is updated, so we can store it and only compute error
                        J = -1.0 * Eigen::Vector2d(
                            0.5 * (GetPixelValue(img1, kp.pt.x + x + 1, kp.pt.y + y) -
                                   GetPixelValue(img1, kp.pt.x + x - 1, kp.pt.y + y)),
                            0.5 * (GetPixelValue(img1, kp.pt.x + x, kp.pt.y + y + 1) -
                                   GetPixelValue(img1, kp.pt.x + x, kp.pt.y + y - 1))
                        );
                    }
                    // compute H, b and set cost;
                    b += -error * J;
                    cost += error * error;
                    if (inverse == false || iter == 0) {  
                        // also update H
                        H += J * J.transpose();
                    }
                }

            // compute update
            Eigen::Vector2d update = H.ldlt().solve(b);  //求解方程u,v

            if (std::isnan(update[0])) {  //解失敗
                // sometimes occurred when we have a black or white patch and H is irreversible
                cout << "update is nan" << endl;
                succ = false;
                break;
            }

            if (iter > 0 && cost > lastCost) {  //找到最小
                break;
            }

            // 更新 dx, dy
            dx += update[0];
            dy += update[1];
            lastCost = cost;
            succ = true;

            if (update.norm() < 1e-2) {  //更新值范數已較小,直接退出
                // converge
                break;
            }
        }

        success[i] = succ;

        // kp2,找到了kp2和kp1的匹配點坐標
        kp2[i].pt = kp.pt + Point2f(dx, dy);
    }
}

4.多層光流

上面光流只計算了單層光流,當相機運動較快的時候,單層光流容易達到一個局部極小值,這時可以引入圖像金字塔來進行優化。

圖像金字塔就是對同一個圖像進行縮放,得到不同分辨率下的圖像,以原始圖像作為金字塔的底層,沒向上一層,就進行一定倍率的縮放。

計算金字塔光流時,先從頂層的圖像開始計算,然后把上一層的追蹤結果,作為下一層光流的初始值。這個過程稱為由粗至精光流。

由粗至精的好處是,當原始圖像運動較大的時候,在上幾層的圖像優化值里,像素運動會比較小,就避免了陷入局部極小值。

void OpticalFlowMultiLevel(
    const Mat &img1,
    const Mat &img2,
    const vector<KeyPoint> &kp1,
    vector<KeyPoint> &kp2,
    vector<bool> &success,
    bool inverse) {

    // parameters
    int pyramids = 4;  //4層金字塔
    double pyramid_scale = 0.5;
    double scales[] = {1.0, 0.5, 0.25, 0.125};

    // 創建4層金字塔,圖1,圖2,每層縮小0.5
    chrono::steady_clock::time_point t1 = chrono::steady_clock::now();
    vector<Mat> pyr1, pyr2; // image pyramids
    for (int i = 0; i < pyramids; i++) {
        if (i == 0) {
            pyr1.push_back(img1);
            pyr2.push_back(img2);
        } else {
            Mat img1_pyr, img2_pyr;
			//進行縮放 參數:原圖,輸出圖像,輸出圖像的大小
			//dsize = Size(round(fxsrc.cols), round(fysrc.rows))
            cv::resize(pyr1[i - 1], img1_pyr,
                       cv::Size(pyr1[i - 1].cols * pyramid_scale, pyr1[i - 1].rows * pyramid_scale));
            cv::resize(pyr2[i - 1], img2_pyr,
                       cv::Size(pyr2[i - 1].cols * pyramid_scale, pyr2[i - 1].rows * pyramid_scale));
            pyr1.push_back(img1_pyr);
            pyr2.push_back(img2_pyr);
        }
    }
    chrono::steady_clock::time_point t2 = chrono::steady_clock::now();
    auto time_used = chrono::duration_cast<chrono::duration<double>>(t2 - t1);
    cout << "build pyramid time: " << time_used.count() << endl;

    // coarse-to-fine LK tracking in pyramids
    vector<KeyPoint> kp1_pyr, kp2_pyr;
	//最上層kp1的特征點處理 縮小 0.5*0.5*0.5
    for (auto &kp:kp1) {
        auto kp_top = kp;
        kp_top.pt *= scales[pyramids - 1];
        kp1_pyr.push_back(kp_top);
        kp2_pyr.push_back(kp_top);
    }

    for (int level = pyramids - 1; level >= 0; level--) {
        // from coarse to fine
        success.clear();
        t1 = chrono::steady_clock::now();
		
        OpticalFlowSingleLevel(pyr1[level], pyr2[level], kp1_pyr, kp2_pyr, success, inverse, true);
        t2 = chrono::steady_clock::now();
        auto time_used = chrono::duration_cast<chrono::duration<double>>(t2 - t1);
        cout << "track pyr " << level << " cost time: " << time_used.count() << endl;

        if (level > 0) {
            for (auto &kp: kp1_pyr)
                kp.pt /= pyramid_scale;
            for (auto &kp: kp2_pyr)
                kp.pt /= pyramid_scale;
        }
    }

    for (auto &kp: kp2_pyr)
        kp2.push_back(kp);
}

完整代碼:



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM