视觉SLAM-光流法

文章目录

目录

前言

光流法的基本原理

OpenCV中的光流法实现

光流法的应用

代码实现

OpenCV代码

OpenCV代码

总结


前言

        当我们观察一个视频或连续帧的图像时,我们经常想知道图像中的物体是如何移动的。光流法(Optical Flow)就是一种用于估计图像中像素点在时间上的运动的计算机视觉技术。它可以帮助我们跟踪物体的移动,并提供关于物体速度和运动方向的信息。

源码在后面,需要的自行下载。


光流法的基本原理

光流法基于以下假设:在相邻帧之间,一个像素点的亮度不会发生大的变化。基于这个假设,光流法通过比较两帧之间像素的亮度变化,推断像素点的运动。

在实际应用中,光流法通常使用Lucas-Kanade算法,该算法假设图像中的运动是局部的,即在一个小的邻域内,像素点的运动是相似的。该算法通过以下步骤计算光流:

  1. 检测特征点:在第一帧图像中,通过特征点检测算法(如Harris角点检测)找到一些具有显著变化的点,作为跟踪的起点。
  2. 选择跟踪窗口:对于每个特征点,在第一帧图像中选择一个小的窗口作为跟踪窗口。
  3. 在相邻帧中跟踪特征点:在第二帧图像中,根据第一帧图像中的特征点位置,通过最小化特征点窗口内的亮度变化,计算第二帧中对应的特征点位置。
  4. 计算光流:根据第一帧和第二帧中特征点的位置差异,计算特征点的光流向量,表示特征点的运动方向和速度。

OpenCV中的光流法实现

OpenCV是一个流行的计算机视觉库,提供了用于光流估计的函数cv::calcOpticalFlowPyrLK。该函数使用了基于Lucas-Kanade算法的金字塔方法,可以在图像金字塔的不同尺度上进行光流估计,提高了算法的鲁棒性和准确性。

使用OpenCV中的cv::calcOpticalFlowPyrLK函数,你可以传入两帧图像、特征点位置,并计算出特征点的光流向量。这个函数还会返回一个状态向量,指示光流是否成功计算,并提供一个误差向量,表示光流估计的精度。

光流法的应用

光流法在计算机视觉和计算机图形学中有广泛的应用。以下是一些常见的应用领域:

  1. 物体跟踪:光流法可以用于跟踪视频序列中的物体。通过跟踪物体的光流向量,可以实现目标检测、运动分析和行为识别等任务。

  2. 视频稳定:通过计算视频序列中的光流,可以检测到相机的运动和抖动,并进行图像稳定化,以提高视频质量和观看体验。

  3. 动作分析:通过跟踪人体的光流,可以分析人的动作和姿态。这在行为分析、运动捕捉和虚拟现实等领域具有重要意义。

  4. 三维重建:通过光流法估计图像中的运动,可以从多个视角的图像中恢复场景的三维结构,实现三维重建和立体视觉。

  5. 视频编码:光流法在视频编码中扮演着重要的角色。通过估计图像序列之间的运动,可以提供更高的压缩率和更好的视频质量。

  6. 虚拟现实和增强现实:光流法可以用于虚拟现实和增强现实应用中,实现对虚拟对象或信息的准确定位和交互。

总结起来,光流法是一种用于估计图像中像素点运动的技术。它在计算机视觉领域有广泛的应用,包括物体跟踪、视频稳定、动作分析、三维重建、视频编码以及虚拟现实和增强现实等方面。通过OpenCV提供的函数,我们可以方便地实现光流法,并应用于各种视觉任务中。

代码实现

OpenCV代码

代码如下:

#include 

int main() {
    cv::Mat prevImg, nextImg;
    std::vector prevPts, nextPts;
    std::vector status;
    std::vector err;

    // Load previous and current frames

    // Detect features in the previous frame and store them in prevPts

    // Calculate optical flow
    cv::calcOpticalFlowPyrLK(prevImg, nextImg, prevPts, nextPts, status, err);

    // Print the tracked points
    for (int i = 0; i < prevPts.size(); i++) {
        if (status[i]) {
            std::cout << "Tracked point: (" << prevPts[i].x << ", " << prevPts[i].y << ") -> "
                      << "(" << nextPts[i].x << ", " << nextPts[i].y << ")" << std::endl;
        } else {
            std::cout << "Tracking failed for point: (" << prevPts[i].x << ", " << prevPts[i].y << ")" << std::endl;
        }
    }

    return 0;
}

OpenCV代码

        我们首先导入所需的头文件和库,包括OpenCV、Eigen等。然后,我们定义了两幅图像的文件路径和一些辅助函数。

        接下来,我们实现了两个函数OpticalFlowSingleLevelOpticalFlowMultiLevel,用于单层和多层光流估计。这两个函数使用Lucas-Kanade算法来估计图像中关键点的光流向量。OpticalFlowSingleLevel函数用于单层光流估计,而OpticalFlowMultiLevel函数使用金字塔方法在多个尺度上进行光流估计。

        在主函数中,我们加载了两幅图像并使用Good Features to Track (GFTT)检测算法检测了第一幅图像中的关键点。然后,我们分别使用单层光流估计和多层光流估计方法跟踪这些关键点在第二幅图像中的位置。我们还使用OpenCV的calcOpticalFlowPyrLK函数进行验证。

        最后,我们将结果进行可视化,并显示了单层光流估计、多层光流估计和OpenCV光流估计的对比图像。通过这些图像,我们可以观察到不同方法的跟踪效果。

#include 
#include 
#include 
#include 

using namespace std;
using namespace cv;

// this program shows how to use optical flow

string file_1 = "./1.png";  // 第一幅图像
string file_2 = "./2.png";  // 第二幅图像

// TODO 实现这个函数
/**
 * 单层光流估计
 * @param [in] img1 第一幅图像
 * @param [in] img2 第二幅图像
 * @param [in] kp1 第一幅图像中的关键点
 * @param [in|out] kp2 第二幅图像中的关键点,如果为空,则使用kp1的初始值
 * @param [out] success 关键点是否成功跟踪的标志
 * @param [in] inverse 是否使用逆向公式?
 */
void OpticalFlowSingleLevel(
        const Mat &img1,
        const Mat &img2,
        const vector &kp1,
        vector &kp2,
        vector &success,
        bool inverse = false
);

// TODO 实现这个函数
/**
 * 多层光流估计,默认金字塔的尺度为2
 * 图像金字塔将在函数内部创建
 * @param [in] img1 第一幅图像
 * @param [in] img2 第二幅图像
 * @param [in] kp1 第一幅图像中的关键点
 * @param [out] kp2 第二幅图像中的关键点
 * @param [out] success 关键点是否成功跟踪的标志
 * @param [in] inverse 设置为true以启用逆向公式
 */
void OpticalFlowMultiLevel(
        const Mat &img1,
        const Mat &img2,
        const vector &kp1,
        vector &kp2,
        vector &success,
        bool inverse = false
);

/**
 * 根据参考图像获取灰度值(双线性插值)
 * @param img 图像
 * @param x x坐标
 * @param y y坐标
 * @return 像素的灰度值
 */
inline float GetPixelValue(const cv::Mat &img, float x, float y) {
    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]
    );
}


int main(int argc, char **argv) {

    // 图像,注意这里使用的是CV_8UC1而不是CV_8UC3
    Mat img1 = imread(file_1, 0);
    Mat img2 = imread(file_2, 0);

    // 关键点,这里使用GFTT(Good Features to Track)算法
    vector kp1;
    Ptr detector =GFTTDetector::create(500, 0.01, 20); // 最多检测500个关键点
    detector->detect(img1, kp1);

    // 现在让我们在第二幅图像中跟踪这些关键点
    // 首先在验证图片中使用单层LK
    vector kp2_single;
    vector success_single;
    OpticalFlowSingleLevel(img1, img2, kp1, kp2_single, success_single);

    // 然后测试多层LK
    vector kp2_multi;
    vector success_multi;
    OpticalFlowMultiLevel(img1, img2, kp1, kp2_multi, success_multi);

    // 使用OpenCV的光流进行验证
    vector pt1, pt2;
    for (auto &kp: kp1) pt1.push_back(kp.pt);
    vector status;
    vector error;
    cv::calcOpticalFlowPyrLK(img1, img2, pt1, pt2, status, error, cv::Size(8, 8));

    // 绘制这些函数的差异
    Mat img2_single;
    cv::cvtColor(img2, img2_single, CV_GRAY2BGR);
    for (int i = 0; i < kp2_single.size(); i++) {
        if (success_single[i]) {
            cv::circle(img2_single, kp2_single[i].pt, 2, cv::Scalar(0, 250, 0), 2);
            cv::line(img2_single, kp1[i].pt, kp2_single[i].pt, cv::Scalar(0, 250, 0));
        }
    }

    Mat img2_multi;
    cv::cvtColor(img2, img2_multi, CV_GRAY2BGR);
    for (int i = 0; i < kp2_multi.size(); i++) {
        if (success_multi[i]) {
            cv::circle(img2_multi, kp2_multi[i].pt, 2, cv::Scalar(0, 250, 0), 2);
            cv::line(img2_multi, kp1[i].pt, kp2_multi[i].pt, cv::Scalar(0, 250, 0));
        }
    }

    Mat img2_CV;
    cv::cvtColor(img2, img2_CV, CV_GRAY2BGR);
    for (int i = 0; i < pt2.size(); i++) {
        if (status[i]) {
            cv::circle(img2_CV, pt2[i], 2, cv::Scalar(0, 250, 0), 2);
            cv::line(img2_CV, pt1[i], pt2[i], cv::Scalar(0, 250, 0));
        }
    }

    cv::imshow("tracked single level", img2_single);
    cv::imshow("tracked multi level", img2_multi);
    cv::imshow("tracked by opencv", img2_CV);
    cv::waitKey(0);

    return 0;
}

void OpticalFlowSingleLevel(
        const Mat &img1,
        const Mat &img2,
        const vector &kp1,
        vector &kp2,
        vector &success,
        bool inverse
) {

    // 参数
    int half_patch_size = 4;
    int iterations = 10;
    bool have_initial = !kp2.empty();

   ```cpp
    for (size_t i = 0; i < kp1.size(); i++) {
        auto kp = kp1[i];
        double dx = 0, dy = 0; // dx,dy 需要估计
        if (have_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; // 标志关键点是否成功跟踪

        // Gauss-Newton 迭代
        for (int iter = 0; iter < iterations; iter++) {
            Eigen::Matrix2d H = Eigen::Matrix2d::Zero();
            Eigen::Vector2d b = Eigen::Vector2d::Zero();
            cost = 0;

            if (kp.pt.x + dx <= half_patch_size || kp.pt.x + dx >= img1.cols - half_patch_size ||
                kp.pt.y + dy <= half_patch_size || kp.pt.y + dy >= img1.rows - half_patch_size) {   // 超出图像边界
                succ = false;
                break;
            }

            // 计算误差和雅可比矩阵
            for (int x = -half_patch_size; x < half_patch_size; x++)
                for (int y = -half_patch_size; y < half_patch_size; y++) {

                    // TODO 从这里开始编写代码 (~8 行)
                    double error = 0;
                    Eigen::Vector2d J;  // 雅可比矩阵
                    if (inverse == false) {
                        // 正向雅可比矩阵
                    } else {
                        // 逆向雅可比矩阵
                        // 注意,当 dx、dy 更新时,J 不会改变,所以我们可以存储它并且只计算误差
                    }

                    // 计算 H、b 和设置 cost;
                    H;
                    b;
                    cost;
                    // TODO 编写代码结束
                }

            // 计算更新量
            // TODO 从这里开始编写代码 (~1 行)
            Eigen::Vector2d update;
            // TODO 编写代码结束

            if (isnan(update[0])) {
                // 有时会出现当我们拥有黑色或白色的补丁时,H 是不可逆的情况
                cout << "update is nan" << endl;
                succ = false;
                break;
            }
            if (iter > 0 && cost > lastCost) {
                cout << "cost increased: " << cost << ", " << lastCost << endl;
                break;
            }

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

        success.push_back(succ);

        // 设置 kp2
        if (have_initial) {
            kp2[i].pt = kp.pt + Point2f(dx, dy);
        } else {
            KeyPoint tracked = kp;
            tracked.pt += cv::Point2f(dx, dy);
            kp2.push_back(tracked);
        }
    }
}

void OpticalFlowMultiLevel(
        const Mat &img1,
        const Mat &img2,
        const vector &kp1,
        vector &kp2,
```cpp
        vector &success,
        bool inverse
) {
    // 参数
    int half_patch_size = 4;
    int iterations = 10;
    bool have_initial = !kp2.empty();

    // 创建图像金字塔
    vector pyramid1, pyramid2;
    buildOpticalFlowPyramid(img1, pyramid1, cv::Size(2, 2));
    buildOpticalFlowPyramid(img2, pyramid2, cv::Size(2, 2));

    for (size_t i = 0; i < kp1.size(); i++) {
        auto kp = kp1[i];
        double dx = 0, dy = 0; // dx,dy 需要估计
        if (have_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; // 标志关键点是否成功跟踪

        // Gauss-Newton 迭代
        for (int level = pyramid1.size() - 1; level >= 0; level--) {
            Mat img1_level = pyramid1[level];
            Mat img2_level = pyramid2[level];

            // 根据金字塔的尺度调整 dx、dy
            dx *= 2;
            dy *= 2;

            cost = 0;
            lastCost = 0;

            for (int iter = 0; iter < iterations; iter++) {
                Eigen::Matrix2d H = Eigen::Matrix2d::Zero();
                Eigen::Vector2d b = Eigen::Vector2d::Zero();
                cost = 0;

                if (kp.pt.x + dx <= half_patch_size || kp.pt.x + dx >= img1_level.cols - half_patch_size ||
                    kp.pt.y + dy <= half_patch_size || kp.pt.y + dy >= img1_level.rows - half_patch_size) {   // 超出图像边界
                    succ = false;
                    break;
                }

                // 计算误差和雅可比矩阵
                for (int x = -half_patch_size; x < half_patch_size; x++)
                    for (int y = -half_patch_size; y < half_patch_size; y++) {

                        // TODO 从这里开始编写代码 (~8 行)
                        double error = 0;
                        Eigen::Vector2d J;  // 雅可比矩阵
                        if (inverse == false) {
                            // 正向雅可比矩阵
                        } else {
                            // 逆向雅可比矩阵
                            // 注意,当 dx、dy 更新时,J 不会改变,所以我们可以存储它并且只计算误差
                        }

                        // 计算 H、b 和设置 cost;
                        H;
                        b;
                        cost;
                        // TODO 编写代码结束
                    }

                // 计算更新量
                // TODO 从这里开始编写代码 (~1 行)
                Eigen::Vector2d update;
                // TODO 编写代码结束

                if (isnan(update[0])) {
                    // 有时会出现当我们拥有黑色或白色的补丁时,H 是不可逆的情况
                    cout << "update is nan" << endl```cpp
                    succ = false;
                    break;
                }
                if (iter > 0 && cost > lastCost) {
                    cout << "cost increased: " << cost << ", " << lastCost << endl;
                    break;
                }

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

            if (level > 0) {
                dx /= 2;
                dy /= 2;
            }
        }

        success.push_back(succ);

        // 设置 kp2
        if (have_initial) {
            kp2[i].pt = kp.pt + Point2f(dx, dy);
        } else {
            KeyPoint tracked = kp;
            tracked.pt += cv::Point2f(dx, dy);
            kp2.push_back(tracked);
        }
    }
}

总结

源码

        光流法是计算机视觉领域中用于估计图像中像素点运动的一种技术。它通过分析图像序列中像素点的灰度值变化,推断出像素点的运动方向和速度。光流法基于以下两个假设:

  1. 亮度恒定假设(Brightness Constancy Assumption):在一个像素点的邻域内,随着时间的推移,像素点的灰度值保持不变。
  2. 空间连续性假设(Spatial Coherence Assumption):邻近的像素点在图像中的运动方向和速度相似。

        光流法的主要思想是通过比较图像序列中的相邻帧来计算像素点的位移向量。常见的光流算法包括Lucas-Kanade算法和Horn-Schunck算法。这些算法基于局部像素点间的亮度差异,建立了一个优化问题,并使用最小二乘法或其他优化方法求解。

        光流法在计算机视觉中有广泛的应用,包括物体跟踪、运动分析、视频稳定、三维重建、虚拟现实和增强现实等领域。它可以用于目标检测和跟踪,分析人体动作和姿态,实现视频稳定化和运动捕捉,以及构建三维场景模型等。

        然而,光流法也存在一些限制。首先,它假设像素点在邻域内亮度恒定,这在存在光照变化或纹理缺失的情况下可能不成立。其次,光流法对于大的位移或快速运动的物体可能无法准确估计。此外,光流法对图像噪声和运动模糊也比较敏感。

        为了克服这些问题,研究人员提出了许多改进的光流算法和技术,如金字塔光流、密集光流、稠密光流和基于学习的光流等方法。这些方法利用多尺度信息、稠密采样和机器学习等技术来提高光流估计的准确性和鲁棒性。

总结起来,光流法是一种用于估计图像中像素点运动的重要技术。它在计算机视觉领域有广泛的应用,并为物体跟踪、运动分析、视频稳定、三维重建和虚拟现实等任务提供了基础。然而,光流法也存在一些局限性,需要根据具体的应用场景和需求选择合适的算法和技术。同时,随着计算机视觉领域的不断发展,新的光流算法和改进技术不断涌现,为光流法的性能和应用提供了更多的可能性。

视觉SLAM-光流法_第1张图片视觉SLAM-光流法_第2张图片

你可能感兴趣的:(SLAM基础,SLAM,光流法)