目录

1 介绍

1.1 视频稳定的方法

1.2 使用点特征匹配的视频稳定

2 算法

2.1 帧间运动信息获取

2.1.1 合适的特征点获取

2.1.2 Lucas-Kanade光流法

2.1.3 运动估计

2.2 计算帧之间的总体运动

2.2.1 计算运动轨迹

2.2.2 计算平滑轨迹

2.2.3 平滑变化计算

2.3 将平滑后的变化矩阵应用于帧

3 结果和代码

3.1总结

3.2 代码

参考


在这篇文章中,我们将学习如何使用OpenCV库中称为特征点匹配的技术以实现简单视频稳定稳像。我们将讨论该算法并共享代码,以便在OpenCV中使用此方法设计一个简单的稳定器,最好OpenCV3.4.3以上实现代码。什么是视频稳定,视频稳定是指用于减少相机运动对最终视频影响的一系列方法,理解成消除视频抖动就行了。见下图通常用拍摄会出现轻微的抖动,比如手机拍摄视频,后期需要对其进行视频稳像操作。

视频稳定的需求涉及许多领域。它在消费者和专业摄像中极为重要。因此,存在许多不同的机械,光学和算法解决方案。即使在静态图像拍摄中,稳定也可以帮助拍摄时间长的照片。在内窥镜检查和结肠镜检查等医疗诊断应用中,需要稳定视频以确定问题的确切位置和宽度。类似地,在军事应用中,飞行器在侦察飞行中捕获的视频需要稳定以进行定位,导航,目标跟踪等。这同样适用于机器人应用。

1 介绍

1.1 视频稳定的方法

视频稳定方法包括机械,光学和数字稳定方法。具体如下:

机械视频稳定:机械图像稳定系统使用陀螺仪和加速度计等特殊传感器检测到运动来移动图像传感器以补偿相机的运动。

光学视频稳定:在这种方法中,不是移动整个相机,而是通过移动镜头的部分来实现稳定。该方法采用可移动透镜组件,当透过相机的透镜系统时,可移动透镜组件可变地调节光的路径长度。

数字视频稳定:此方法不需要特殊的传感器来估算相机运动。本文就是用的这种方法。主要有三个步骤,

1)运动估计,2)运动平滑,3)图像合成。在第一阶段中导出两个连续帧之间的变换参数。第二级滤除不需要的运动,第三个阶段重建稳定的视频。

我们将在本文中学习快速而强大的实数字视频稳定算法。它基于二维运动模型,我们应用包含平移,旋转和缩放的欧几里德(又称相似性)变换。

如上图所示,在欧几里德运动模型中,图像中的正方形可以转换为具有不同大小,形状位置的任何其他四方形。它比仿射和单应变换更具限制性,但足以用于运动稳定,因为视频的连续帧之间的相机移动通常很小。

1.2 使用点特征匹配的视频稳定

该方法涉及跟踪两个连续帧之间的一些特征点。跟踪的特征允许我们估计帧之间的运动并对其进行补偿。下面的流程图显示了算法基本步骤。

1 获取多帧视频图像,获取图像角点(特征点);

2 光流法跟踪角点;根据前后两张图像角点变化得到表示运动的仿射变化矩阵。

3 根据仿射变化矩阵计算运动轨迹,并且平滑运动轨迹。

4 根据平滑后的运动轨迹,得到平滑运动后的仿射变化矩阵。

5 根据平滑运动后的仿射变化矩阵得到稳定后的图像。

2 算法

2.1 帧间运动信息获取

算法中最关键的部分是确定各帧的运动方向。我们将迭代所有帧,并找到当前帧和前一帧之间的运动。没有必要知道每个像素的运动。欧几里德运动模型要求我们知道两帧中2个点的运动信息。然而,在实践中最好找到50-100点的运动信息,然后使用它们来稳健地估计运动模型

2.1.1 合适的特征点获取

现在的问题是我们应该选择哪些特征点进行跟踪。请记住,跟踪算法会使用一个以该点为圆心的圆(小孔)来近似来模拟点的运动。这种跟踪算法受到圆直径的影响。因此,平滑区域对于跟踪是不利的,并且具有许多角点的纹理区域是好的。幸运的是,OpenCV具有快速检测函数特征点的跟踪,即函数goodFeaturesToTrack。角点个人理解是指图像中亮度变化剧烈的点或图像边缘上变化交大的点。

2.1.2 Lucas-Kanade光流法

一旦我们在前一帧中找到了好的特征(角点),我们就可以使用名为Lucas-Kanade Optical

Flow的算法在下一帧中跟踪它们,该算法以算法的发明者命名。算法详情可以见

https://blog.csdn.net/linmingan/article/details/79296963

OpenCV中的函数calcOpticalFlowPyrLK实现算法。在calcOpticalFlowPyrLK中,LK代表Lucas-

Kanade,而Pyr代表金字塔。计算机视觉中的图像金字塔用于处理不同比例(分辨率)的图像。但是由于各种原因,calcOpticalFlowPyrLK可能无法计算所有点的运动。例如,当前帧中的特征点可能被下一帧中的另一个对象遮挡。幸运的是calcOpticalFlowPyrLK中的状态标志可用于过滤掉这些值。

2.1.3 运动估计

回顾一下,在步骤2.1.1中,我们发现在前一帧中要跟踪的特征点。在步骤2.1.2中,我们使用光流来跟踪特征点。换句话说,我们在当前帧中找到了特征点的位置,并且我们已经知道了前一帧中特征点的位置。因此,我们可以使用这两组特征点来找到将前一帧映射到当前帧的(欧几里德)变换。OpenCV使用函数estimateRigidTransform完成的。仿射变化详细见

https://blog.csdn.net/dongfang1984/article/details/52959308

一旦我们获取运动信息,我们就可以将它分解为x和y值以及平移和旋转(角度)值。我们将这些值存储在一个数组中,以便我们可以顺利更改它们。

下面的代码将介绍步骤2.1.1至2.1.3。请务必阅读要遵循的代码中的注释。

在C

++实现中,我们首先定义一些类来帮助我们存储估计的运动矢量。下面C++代码的TransformParam类存储运动信息(dx为x轴运动信息,dy为y轴运动信息,da为角度信息),并提供方法getTransform将此运动转换为变换矩阵。

  1. /**
  2. * @brief 运动信息结构体
  3. *
  4. */
  5. struct TransformParam
  6. {
  7. TransformParam() {}
  8. //x轴信息,y轴信息,角度信息
  9. TransformParam(double _dx, double _dy, double _da)
  10. {
  11. dx = _dx;
  12. dy = _dy;
  13. da = _da;
  14. }
  15. double dx;
  16. double dy;
  17. // angle
  18. double da;
  19. void getTransform(Mat &T)
  20. {
  21. // Reconstruct transformation matrix accordingly to new values 重建变换矩阵
  22. T.at<double>(0, 0) = cos(da);
  23. T.at<double>(0, 1) = -sin(da);
  24. T.at<double>(1, 0) = sin(da);
  25. T.at<double>(1, 1) = cos(da);
  26. T.at<double>(0, 2) = dx;
  27. T.at<double>(1, 2) = dy;
  28. }
  29. };

我们循环遍历帧并执行 2.1帧间运动信息获取所有代码。C++代码:

  1. //previous transformation matrix 上一张图像的仿射矩阵
  2. Mat last_T;
  3. //从第二帧开始循环遍历视频所有帧
  4. for (int i = 1; i < n_frames; i++)
  5. {
  6. // Vector from previous and current feature points 前一帧角点vector,当前帧角点vector
  7. vector<Point2f> prev_pts, curr_pts;
  8. // Detect features in previous frame 获取前一帧的角点
  9. //前一帧灰度图,前一帧角点vector, 最大角点数,检测到的角点的质量等级,两个角点之间的最小距离
  10. goodFeaturesToTrack(prev_gray, prev_pts, 200, 0.01, 30);
  11. // Read next frame 读取当前帧图像
  12. bool success = cap.read(curr);
  13. if (!success)
  14. {
  15. break;
  16. }
  17. // Convert to grayscale 将当前帧图像转换为灰度图
  18. cvtColor(curr, curr_gray, COLOR_BGR2GRAY);
  19. // Calculate optical flow (i.e. track feature points) 光流法追寻特征点
  20. //输出状态矢量(元素是无符号char类型,uchar),如果在当前帧发现前一帧角点特征则置为1,否则,为0
  21. vector<uchar> status;
  22. //输出误差矢量
  23. vector<float> err;
  24. //光流跟踪
  25. //前一帧灰度图像,当前帧灰度图像,前一帧角点,当前帧角点,状态量,误差量
  26. calcOpticalFlowPyrLK(prev_gray, curr_gray, prev_pts, curr_pts, status, err);
  27. // Filter only valid points 获取光流跟踪下有效的角点
  28. //遍历角点
  29. auto prev_it = prev_pts.begin();
  30. auto curr_it = curr_pts.begin();
  31. for (size_t k = 0; k < status.size(); k++)
  32. {
  33. if (status[k])
  34. {
  35. prev_it++;
  36. curr_it++;
  37. }
  38. //删除无效角点
  39. else
  40. {
  41. prev_it = prev_pts.erase(prev_it);
  42. curr_it = curr_pts.erase(curr_it);
  43. }
  44. }
  45. // Find transformation matrix 获得变换矩阵
  46. //false表示带几何约束的仿射变换,true则是全仿射变化,T为变换矩阵
  47. Mat T = estimateRigidTransform(prev_pts, curr_pts, false);
  48. // In rare cases no transform is found.
  49. // We'll just use the last known good transform.
  50. //极少数情况会找不到变换矩阵,取上一个变换为当前变化矩阵
  51. //当然第一次检测就没找到仿射矩阵,算法会出问题,不过概率很低
  52. if (T.data == NULL)
  53. {
  54. last_T.copyTo(T);
  55. }
  56. T.copyTo(last_T);
  57. // Extract traslation 提取仿射变化结果
  58. double dx = T.at<double>(0, 2);
  59. double dy = T.at<double>(1, 2);
  60. // Extract rotation angle 提取角度
  61. double da = atan2(T.at<double>(1, 0), T.at<double>(0, 0));
  62. // Store transformation 存储仿射变化矩阵
  63. transforms.push_back(TransformParam(dx, dy, da));
  64. // Move to next frame 进行下一次检测准测
  65. curr_gray.copyTo(prev_gray);
  66. cout << "Frame: " << i << "/" << n_frames << " - Tracked points : " << prev_pts.size() << endl;
  67. }

2.2 计算帧之间的总体运动

在上一步中,我们获取了帧之间的运动情况并将它们存储在一个数组中。我们现在需要通过累积分析帧间运动情况来找到运动轨迹。

2.2.1 计算运动轨迹

在此步骤中,我们将累加帧之间的运动以计算轨迹。我们的最终目标是平滑这一轨迹。

在Python中,使用numpy中的cumsum(累积和)很容易实现。

在C ++中,我们定义了一个名为Trajectory的类来存储运动参数每次的累积和。

  1. /**
  2. * @brief 轨迹结构体
  3. *
  4. */
  5. struct Trajectory
  6. {
  7. Trajectory() {}
  8. Trajectory(double _x, double _y, double _a)
  9. {
  10. x = _x;
  11. y = _y;
  12. a = _a;
  13. }
  14. double x;
  15. double y;
  16. // angle
  17. double a;
  18. };

我们还定义了一个函数cumsum,输入为TransformParams结构数据,并通过dx,dy和da(角度)的累积和来返回轨迹信息。C++代码:

  1. /**
  2. * @brief 轨迹累积
  3. *
  4. * @param transforms 运动信息结构体
  5. * @return vector<Trajectory> 轨迹结构体
  6. */
  7. vector<Trajectory> cumsum(vector<TransformParam> &transforms)
  8. {
  9. // trajectory at all frames 所有帧的运动轨迹
  10. vector<Trajectory> trajectory;
  11. // Accumulated frame to frame transform 累加计算x,y以及a(角度)
  12. double a = 0;
  13. double x = 0;
  14. double y = 0;
  15. //累加
  16. for (size_t i = 0; i < transforms.size(); i++)
  17. {
  18. x += transforms[i].dx;
  19. y += transforms[i].dy;
  20. a += transforms[i].da;
  21. trajectory.push_back(Trajectory(x, y, a));
  22. }
  23. return trajectory;
  24. }

2.2.2 计算平滑轨迹

在上一步中,我们计算了运动的轨迹。因此,我们有三条曲线显示运动(x,y和角度)随时间的变化情况。

在这一步中,我们将展示如何平滑这三条曲线。

平滑任何曲线的最简单方法是使用移动平均滤波器。顾名思义,移动平均滤波器将该点处的函数值替换为邻域窗格所有点的平均值。我们来看一个例子。

比方说,我们已经存储在数组中的曲线C,曲线上的点为C [0], ... ,C [N-1]。,用窗宽为5的移动平均滤波器对曲线c滤波可以得到平滑的曲线 f

。计算公式如下:

如下图,平滑曲线的值是在小窗口上平均左侧噪声曲线的值。下图显示了左侧噪声曲线的示例,使用右侧大小为5移动平均滤波器进行平滑处理。

在C ++版本中,我们定义了一个名为smooth的函数,它计算平滑的移动平均轨迹。

  1. /**
  2. * @brief 平滑运动轨迹
  3. *
  4. * @param trajectory 运动轨迹
  5. * @param radius 窗格大小
  6. * @return vector<Trajectory>
  7. */
  8. vector<Trajectory> smooth(vector<Trajectory> &trajectory, int radius)
  9. {
  10. //平滑后的运动轨迹
  11. vector<Trajectory> smoothed_trajectory;
  12. //移动滑动窗格
  13. for (size_t i = 0; i < trajectory.size(); i++)
  14. {
  15. double sum_x = 0;
  16. double sum_y = 0;
  17. double sum_a = 0;
  18. int count = 0;
  19. for (int j = -radius; j <= radius; j++)
  20. {
  21. if (i + j >= 0 && i + j < trajectory.size())
  22. {
  23. sum_x += trajectory[i + j].x;
  24. sum_y += trajectory[i + j].y;
  25. sum_a += trajectory[i + j].a;
  26. count++;
  27. }
  28. }
  29. double avg_a = sum_a / count;
  30. double avg_x = sum_x / count;
  31. double avg_y = sum_y / count;
  32. smoothed_trajectory.push_back(Trajectory(avg_x, avg_y, avg_a));
  33. }
  34. return smoothed_trajectory;
  35. }

2.2.3 平滑变化计算

到目前为止,我们已经获得了平滑的轨迹。在此步骤中,我们将使用平滑轨迹来获得平滑变换,这些变换可应用于视频帧以使其稳定。这是通过找到平滑轨迹和原始轨迹之间的差异并将该差异添加原始变换矩阵来完成的。C++代码:

  1. //平滑后的运动信息结构体
  2. vector<TransformParam> transforms_smooth;
  3. //原始运动信息结构体
  4. for (size_t i = 0; i < transforms.size(); i++)
  5. {
  6. // Calculate difference in smoothed_trajectory and trajectory 计算平滑后的轨迹和原始轨迹差异
  7. double diff_x = smoothed_trajectory[i].x - trajectory[i].x;
  8. double diff_y = smoothed_trajectory[i].y - trajectory[i].y;
  9. double diff_a = smoothed_trajectory[i].a - trajectory[i].a;
  10. // Calculate newer transformation array 计算平滑后的运动信息结构体数据
  11. double dx = transforms[i].dx + diff_x;
  12. double dy = transforms[i].dy + diff_y;
  13. double da = transforms[i].da + diff_a;
  14. transforms_smooth.push_back(TransformParam(dx, dy, da));
  15. }

2.3 将平滑后的变化矩阵应用于帧

我们差不多完成了。我们现在需要做的就是遍历帧并应用我们刚刚计算的变换。

如果我们将运动指定为 x , y , θ ,则相应的变换矩阵由下式给出:

当我们稳定视频时,我们可能会看到一些黑色边界。这是预期的,因为要稳定视频,视频帧原图像可能不得不缩小(不是图的尺寸缩小,图像尺寸不变。有两种情况,一种实际先缩小图像,然后从中截取原图大小的区域,缺少图像区域用黑色填充,起到图像增大作用;另外将原图扩大,然后截取原图尺寸相等大小区域,起到图像缩小作用)。我们可以通过以其视频中点为中心缩放图像(例如4%)来缓解该问题。下面的函数fixBorder显示了实现。我们使用getRotationMatrix2D,因为它可以在不移动图像中心的情况下缩放和旋转图像。我们需要做的就是调用此函数,旋转0和缩放1.04(将原图扩大为1.04倍,然后截取原图尺寸相等大小区域)。C++代码如下:

  1. /**
  2. * @brief
  3. *
  4. * @param frame_stabilized
  5. */
  6. void fixBorder(Mat &frame_stabilized)
  7. {
  8. //将原图扩大为1.04倍,然后截取原图尺寸相等大小区域
  9. Mat T = getRotationMatrix2D(Point2f(frame_stabilized.cols / 2, frame_stabilized.rows / 2), 0, 1.04);
  10. //仿射变换
  11. warpAffine(frame_stabilized, frame_stabilized, T, frame_stabilized.size());
  12. }

3 结果和代码

3.1总结

优点:

  • 该方法对低频运动(较慢的振动)提供了良好的稳定性。
  • 该方法具有低内存消耗,因此非常适用于嵌入式设备(如Raspberry Pi)。
  • 此方法可以很好地防止视频中的缩放(缩放)抖动。

缺点:

  • 该方法对高频扰动的影响很小。
  • 速度过慢。
  • 如果运动模糊,则功能跟踪将失败,结果将不是最佳。
  • 滚动快门失真也不适合这种方法。

3.2 代码

代码地址:

https://download.csdn.net/download/luohenyj/11007133

https://github.com/luohenyueji/OpenCV-Practical-Exercise

如果没有积分(系统自动设定资源分数)看看参考链接。我搬运过来的,大修改没有。

代码提供了C++和Python版本,代码都有详细的注释。

但是需要注意的是提供的示例detect.mp4在1300帧后有丢帧现象,有些OpenCV版本会出问题,所以设定n_frames=1300只读取前1300帧,运行其他视频需要注意n_frames

的手动设定。

C++:

  1. // video_stabilization.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
  2. //
  3. #include "pch.h"
  4. #include <opencv2/opencv.hpp>
  5. #include <iostream>
  6. #include <cassert>
  7. #include <cmath>
  8. #include <fstream>
  9. using namespace std;
  10. using namespace cv;
  11. // In frames. The larger the more stable the video, but less reactive to sudden panning 移动平均滑动窗口大小
  12. const int SMOOTHING_RADIUS = 50;
  13. /**
  14. * @brief 运动信息结构体
  15. *
  16. */
  17. struct TransformParam
  18. {
  19. TransformParam() {}
  20. //x轴信息,y轴信息,角度信息
  21. TransformParam(double _dx, double _dy, double _da)
  22. {
  23. dx = _dx;
  24. dy = _dy;
  25. da = _da;
  26. }
  27. double dx;
  28. double dy;
  29. // angle
  30. double da;
  31. void getTransform(Mat &T)
  32. {
  33. // Reconstruct transformation matrix accordingly to new values 重建变换矩阵
  34. T.at<double>(0, 0) = cos(da);
  35. T.at<double>(0, 1) = -sin(da);
  36. T.at<double>(1, 0) = sin(da);
  37. T.at<double>(1, 1) = cos(da);
  38. T.at<double>(0, 2) = dx;
  39. T.at<double>(1, 2) = dy;
  40. }
  41. };
  42. /**
  43. * @brief 轨迹结构体
  44. *
  45. */
  46. struct Trajectory
  47. {
  48. Trajectory() {}
  49. Trajectory(double _x, double _y, double _a)
  50. {
  51. x = _x;
  52. y = _y;
  53. a = _a;
  54. }
  55. double x;
  56. double y;
  57. // angle
  58. double a;
  59. };
  60. /**
  61. * @brief 轨迹累积
  62. *
  63. * @param transforms 运动信息结构体
  64. * @return vector<Trajectory> 轨迹结构体
  65. */
  66. vector<Trajectory> cumsum(vector<TransformParam> &transforms)
  67. {
  68. // trajectory at all frames 所有帧的运动轨迹
  69. vector<Trajectory> trajectory;
  70. // Accumulated frame to frame transform 累加计算x,y以及a(角度)
  71. double a = 0;
  72. double x = 0;
  73. double y = 0;
  74. //累加
  75. for (size_t i = 0; i < transforms.size(); i++)
  76. {
  77. x += transforms[i].dx;
  78. y += transforms[i].dy;
  79. a += transforms[i].da;
  80. trajectory.push_back(Trajectory(x, y, a));
  81. }
  82. return trajectory;
  83. }
  84. /**
  85. * @brief 平滑运动轨迹
  86. *
  87. * @param trajectory 运动轨迹
  88. * @param radius 窗格大小
  89. * @return vector<Trajectory>
  90. */
  91. vector<Trajectory> smooth(vector<Trajectory> &trajectory, int radius)
  92. {
  93. //平滑后的运动轨迹
  94. vector<Trajectory> smoothed_trajectory;
  95. //移动滑动窗格
  96. for (size_t i = 0; i < trajectory.size(); i++)
  97. {
  98. double sum_x = 0;
  99. double sum_y = 0;
  100. double sum_a = 0;
  101. int count = 0;
  102. for (int j = -radius; j <= radius; j++)
  103. {
  104. if (i + j >= 0 && i + j < trajectory.size())
  105. {
  106. sum_x += trajectory[i + j].x;
  107. sum_y += trajectory[i + j].y;
  108. sum_a += trajectory[i + j].a;
  109. count++;
  110. }
  111. }
  112. double avg_a = sum_a / count;
  113. double avg_x = sum_x / count;
  114. double avg_y = sum_y / count;
  115. smoothed_trajectory.push_back(Trajectory(avg_x, avg_y, avg_a));
  116. }
  117. return smoothed_trajectory;
  118. }
  119. /**
  120. * @brief
  121. *
  122. * @param frame_stabilized
  123. */
  124. void fixBorder(Mat &frame_stabilized)
  125. {
  126. //将原图扩大为1.04倍,然后截取原图尺寸相等大小区域
  127. Mat T = getRotationMatrix2D(Point2f(frame_stabilized.cols / 2, frame_stabilized.rows / 2), 0, 1.04);
  128. //仿射变换
  129. warpAffine(frame_stabilized, frame_stabilized, T, frame_stabilized.size());
  130. }
  131. int main(int argc, char **argv)
  132. {
  133. // Read input video 读取视频
  134. VideoCapture cap("./video/detect.mp4");
  135. // Get frame count 读取视频总帧数
  136. int n_frames = int(cap.get(CAP_PROP_FRAME_COUNT));
  137. // Our test video may be wrong to read the frame after frame 1300
  138. n_frames = 1300;
  139. // Get width and height of video stream 获取视频图像宽高
  140. int w = int(cap.get(CAP_PROP_FRAME_WIDTH));
  141. int h = int(cap.get(CAP_PROP_FRAME_HEIGHT));
  142. // Get frames per second (fps) 获取视频每秒帧数
  143. double fps = cap.get(CV_CAP_PROP_FPS);
  144. // Set up output video 设置输出视频
  145. VideoWriter out("video_out.avi", CV_FOURCC('M', 'J', 'P', 'G'), fps, Size(2 * w, h));
  146. // Define variable for storing frames 定义存储帧的相关变量
  147. //当前帧RGB图像和灰度图
  148. Mat curr, curr_gray;
  149. //前一帧RGB图像和灰度图
  150. Mat prev, prev_gray;
  151. // Read first frame 获得视频一张图象
  152. cap >> prev;
  153. // Convert frame to grayscale 转换为灰度图
  154. cvtColor(prev, prev_gray, COLOR_BGR2GRAY);
  155. // Pre-define transformation-store array 仿射变化参数结构体
  156. vector<TransformParam> transforms;
  157. //previous transformation matrix 上一张图像的仿射矩阵
  158. Mat last_T;
  159. //从第二帧开始循环遍历视频所有帧
  160. for (int i = 1; i < n_frames; i++)
  161. {
  162. // Vector from previous and current feature points 前一帧角点vector,当前帧角点vector
  163. vector<Point2f> prev_pts, curr_pts;
  164. // Detect features in previous frame 获取前一帧的角点
  165. //前一帧灰度图,前一帧角点vector, 最大角点数,检测到的角点的质量等级,两个角点之间的最小距离
  166. goodFeaturesToTrack(prev_gray, prev_pts, 200, 0.01, 30);
  167. // Read next frame 读取当前帧图像
  168. bool success = cap.read(curr);
  169. if (!success)
  170. {
  171. break;
  172. }
  173. // Convert to grayscale 将当前帧图像转换为灰度图
  174. cvtColor(curr, curr_gray, COLOR_BGR2GRAY);
  175. // Calculate optical flow (i.e. track feature points) 光流法追寻特征点
  176. //输出状态矢量(元素是无符号char类型,uchar),如果在当前帧发现前一帧角点特征则置为1,否则,为0
  177. vector<uchar> status;
  178. //输出误差矢量
  179. vector<float> err;
  180. //光流跟踪
  181. //前一帧灰度图像,当前帧灰度图像,前一帧角点,当前帧角点,状态量,误差量
  182. calcOpticalFlowPyrLK(prev_gray, curr_gray, prev_pts, curr_pts, status, err);
  183. // Filter only valid points 获取光流跟踪下有效的角点
  184. //遍历角点
  185. auto prev_it = prev_pts.begin();
  186. auto curr_it = curr_pts.begin();
  187. for (size_t k = 0; k < status.size(); k++)
  188. {
  189. if (status[k])
  190. {
  191. prev_it++;
  192. curr_it++;
  193. }
  194. //删除无效角点
  195. else
  196. {
  197. prev_it = prev_pts.erase(prev_it);
  198. curr_it = curr_pts.erase(curr_it);
  199. }
  200. }
  201. // Find transformation matrix 获得变换矩阵
  202. //false表示带几何约束的仿射变换,true则是全仿射变化,T为变换矩阵
  203. Mat T = estimateRigidTransform(prev_pts, curr_pts, false);
  204. // In rare cases no transform is found.
  205. // We'll just use the last known good transform.
  206. //极少数情况会找不到变换矩阵,取上一个变换为当前变化矩阵
  207. //当然第一次检测就没找到仿射矩阵,算法会出问题,不过概率很低
  208. if (T.data == NULL)
  209. {
  210. last_T.copyTo(T);
  211. }
  212. T.copyTo(last_T);
  213. // Extract traslation 提取仿射变化结果
  214. double dx = T.at<double>(0, 2);
  215. double dy = T.at<double>(1, 2);
  216. // Extract rotation angle 提取角度
  217. double da = atan2(T.at<double>(1, 0), T.at<double>(0, 0));
  218. // Store transformation 存储仿射变化矩阵
  219. transforms.push_back(TransformParam(dx, dy, da));
  220. // Move to next frame 进行下一次检测准测
  221. curr_gray.copyTo(prev_gray);
  222. cout << "Frame: " << i << "/" << n_frames << " - Tracked points : " << prev_pts.size() << endl;
  223. }
  224. // Compute trajectory using cumulative sum of transformations 获取累加轨迹
  225. vector<Trajectory> trajectory = cumsum(transforms);
  226. // Smooth trajectory using moving average filter 获取平滑后的轨迹
  227. vector<Trajectory> smoothed_trajectory = smooth(trajectory, SMOOTHING_RADIUS);
  228. //平滑后的运动信息结构体
  229. vector<TransformParam> transforms_smooth;
  230. //原始运动信息结构体
  231. for (size_t i = 0; i < transforms.size(); i++)
  232. {
  233. // Calculate difference in smoothed_trajectory and trajectory 计算平滑后的轨迹和原始轨迹差异
  234. double diff_x = smoothed_trajectory[i].x - trajectory[i].x;
  235. double diff_y = smoothed_trajectory[i].y - trajectory[i].y;
  236. double diff_a = smoothed_trajectory[i].a - trajectory[i].a;
  237. // Calculate newer transformation array 计算平滑后的运动信息结构体数据
  238. double dx = transforms[i].dx + diff_x;
  239. double dy = transforms[i].dy + diff_y;
  240. double da = transforms[i].da + diff_a;
  241. transforms_smooth.push_back(TransformParam(dx, dy, da));
  242. }
  243. //定位当前帧为第1帧
  244. cap.set(CV_CAP_PROP_POS_FRAMES, 0);
  245. //平滑后的变化矩阵
  246. Mat T(2, 3, CV_64F);
  247. Mat frame, frame_stabilized, frame_out;
  248. //对所有帧进行变化得到稳像结果
  249. //跳过第一帧
  250. cap.read(frame);
  251. for (int i = 0; i < n_frames - 1; i++)
  252. {
  253. bool success = cap.read(frame);
  254. if (!success)
  255. {
  256. break;
  257. }
  258. // Extract transform from translation and rotation angle. 提取平滑后的仿射变化矩阵
  259. transforms_smooth[i].getTransform(T);
  260. // Apply affine wrapping to the given frame 应用仿射变化
  261. warpAffine(frame, frame_stabilized, T, frame.size());
  262. // Scale image to remove black border artifact 去除黑边
  263. fixBorder(frame_stabilized);
  264. // Now draw the original and stablised side by side for coolness 将原图和变化后的图横向排列输出到视频
  265. hconcat(frame, frame_stabilized, frame_out);
  266. // If the image is too big, resize it.
  267. if (frame_out.cols > 1920)
  268. {
  269. resize(frame_out, frame_out, Size(frame_out.cols / 2, frame_out.rows / 2));
  270. }
  271. //imshow("Before and After", frame_out);
  272. out.write(frame_out);
  273. cout << "out frame:" << i << endl;
  274. //waitKey(10);
  275. }
  276. // Release video
  277. cap.release();
  278. out.release();
  279. // Close windows
  280. destroyAllWindows();
  281. return 0;
  282. }

python:

  1. # Import numpy and OpenCV
  2. import numpy as np
  3. import cv2
  4. def movingAverage(curve, radius):
  5. window_size = 2 * radius + 1
  6. # Define the filter
  7. f = np.ones(window_size)/window_size
  8. # Add padding to the boundaries
  9. curve_pad = np.lib.pad(curve, (radius, radius), 'edge')
  10. # Apply convolution
  11. curve_smoothed = np.convolve(curve_pad, f, mode='same')
  12. # Remove padding
  13. curve_smoothed = curve_smoothed[radius:-radius]
  14. # return smoothed curve
  15. return curve_smoothed
  16. def smooth(trajectory):
  17. smoothed_trajectory = np.copy(trajectory)
  18. # Filter the x, y and angle curves
  19. for i in range(3):
  20. smoothed_trajectory[:, i] = movingAverage(
  21. trajectory[:, i], radius=SMOOTHING_RADIUS)
  22. return smoothed_trajectory
  23. def fixBorder(frame):
  24. s = frame.shape
  25. # Scale the image 4% without moving the center
  26. T = cv2.getRotationMatrix2D((s[1]/2, s[0]/2), 0, 1.04)
  27. frame = cv2.warpAffine(frame, T, (s[1], s[0]))
  28. return frame
  29. # The larger the more stable the video, but less reactive to sudden panning
  30. SMOOTHING_RADIUS = 50
  31. # Read input video
  32. cap = cv2.VideoCapture('video/detect.mp4')
  33. # Get frame count
  34. n_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
  35. # Our test video may be wrong to read the frame after frame 1300
  36. n_frames = 1300
  37. # Get width and height of video stream
  38. w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
  39. h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
  40. # Get frames per second (fps)
  41. fps = cap.get(cv2.CAP_PROP_FPS)
  42. # Define the codec for output video
  43. fourcc = cv2.VideoWriter_fourcc(*'MJPG')
  44. # Set up output video
  45. out = cv2.VideoWriter('video_out.avi', fourcc, fps, (2 * w, h))
  46. # Read first frame
  47. _, prev = cap.read()
  48. # Convert frame to grayscale
  49. prev_gray = cv2.cvtColor(prev, cv2.COLOR_BGR2GRAY)
  50. # Pre-define transformation-store array
  51. transforms = np.zeros((n_frames-1, 3), np.float32)
  52. for i in range(n_frames-2):
  53. # Detect feature points in previous frame
  54. prev_pts = cv2.goodFeaturesToTrack(prev_gray,
  55. maxCorners=200,
  56. qualityLevel=0.01,
  57. minDistance=30,
  58. blockSize=3)
  59. # Read next frame
  60. success, curr = cap.read()
  61. if not success:
  62. break
  63. # Convert to grayscale
  64. curr_gray = cv2.cvtColor(curr, cv2.COLOR_BGR2GRAY)
  65. # Calculate optical flow (i.e. track feature points)
  66. curr_pts, status, err = cv2.calcOpticalFlowPyrLK(
  67. prev_gray, curr_gray, prev_pts, None)
  68. # Sanity check
  69. assert prev_pts.shape == curr_pts.shape
  70. # Filter only valid points
  71. idx = np.where(status == 1)[0]
  72. prev_pts = prev_pts[idx]
  73. curr_pts = curr_pts[idx]
  74. # Find transformation matrix
  75. # will only work with OpenCV-3 or less
  76. m = cv2.estimateRigidTransform(prev_pts, curr_pts, fullAffine=False)
  77. # Extract traslation
  78. dx = m[0, 2]
  79. dy = m[1, 2]
  80. # Extract rotation angle
  81. da = np.arctan2(m[1, 0], m[0, 0])
  82. # Store transformation
  83. transforms[i] = [dx, dy, da]
  84. # Move to next frame
  85. prev_gray = curr_gray
  86. print("Frame: " + str(i) + "/" + str(n_frames) +
  87. " - Tracked points : " + str(len(prev_pts)))
  88. # Compute trajectory using cumulative sum of transformations
  89. trajectory = np.cumsum(transforms, axis=0)
  90. # Create variable to store smoothed trajectory
  91. smoothed_trajectory = smooth(trajectory)
  92. # Calculate difference in smoothed_trajectory and trajectory
  93. difference = smoothed_trajectory - trajectory
  94. # Calculate newer transformation array
  95. transforms_smooth = transforms + difference
  96. # Reset stream to first frame
  97. cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
  98. # Write n_frames-1 transformed frames
  99. for i in range(n_frames-2):
  100. # Read next frame
  101. success, frame = cap.read()
  102. if not success:
  103. break
  104. # Extract transformations from the new transformation array
  105. dx = transforms_smooth[i, 0]
  106. dy = transforms_smooth[i, 1]
  107. da = transforms_smooth[i, 2]
  108. # Reconstruct transformation matrix accordingly to new values
  109. m = np.zeros((2, 3), np.float32)
  110. m[0, 0] = np.cos(da)
  111. m[0, 1] = -np.sin(da)
  112. m[1, 0] = np.sin(da)
  113. m[1, 1] = np.cos(da)
  114. m[0, 2] = dx
  115. m[1, 2] = dy
  116. # Apply affine wrapping to the given frame
  117. frame_stabilized = cv2.warpAffine(frame, m, (w, h))
  118. # Fix border artifacts
  119. frame_stabilized = fixBorder(frame_stabilized)
  120. # Write the frame to the file
  121. frame_out = cv2.hconcat([frame, frame_stabilized])
  122. # If the image is too big, resize it.
  123. if(frame_out.shape[1] > 1920):
  124. frame_out = cv2.resize(
  125. frame_out, (frame_out.shape[1]/2, frame_out.shape[0]/2))
  126. #cv2.imshow("Before and After", frame_out)
  127. # cv2.waitKey(10)
  128. out.write(frame_out)
  129. # Release video
  130. cap.release()
  131. out.release()
  132. # Close windows
  133. cv2.destroyAllWindows()

参考

https://www.learnopencv.com/video-stabilization-using-point-feature-matching-in-opencv/

[OpenCV实战]6 基于特征点匹配的视频稳像的更多相关文章

  1. OpenCV使用FLANN进行特征点匹配

    使用FLANN进行特征点匹配 目标 在本教程中我们将涉及以下内容: 使用 FlannBasedMatcher 接口以及函数 FLANN 实现快速高效匹配( 快速最近邻逼近搜索函数库(Fast Appr ...

  2. [OpenCV实战]48 基于OpenCV实现图像质量评价

    本文主要介绍基于OpenCV contrib中的quality模块实现图像质量评价.图像质量评估Image Quality Analysis简称IQA,主要通过数学度量方法来评价图像质量的好坏. 本文 ...

  3. [OpenCV实战]26 基于OpenCV实现选择性搜索算法

    目录 1 背景 1.1 目标检测与目标识别 1.2 滑动窗口算法 1.3 候选区域选择算法 2 选择性搜索算法 2.1 什么是选择性搜索? 2.2 选择性搜索相似性度量 2.3 结果 3 代码 4 参 ...

  4. [OpenCV实战]17 基于卷积神经网络的OpenCV图像着色

    目录 1 彩色图像着色 1.1 定义着色问题 1.2 CNN彩色化结构 1.3 从 中恢复彩色图像 1.4 具有颜色再平衡的多项式损失函数 1.5 着色结果 2 OpenCV中实现着色 2.1 模型下 ...

  5. [OpenCV实战]47 基于OpenCV实现视觉显著性检测

    人类具有一种视觉注意机制,即当面对一个场景时,会选择性地忽略不感兴趣的区域,聚焦于感兴趣的区域.这些感兴趣的区域称为显著性区域.视觉显著性检测(Visual Saliency Detection,VS ...

  6. [OpenCV实战]45 基于OpenCV实现图像哈希算法

    目前有许多算法来衡量两幅图像的相似性,本文主要介绍在工程领域最常用的图像相似性算法评价算法:图像哈希算法(img hash).图像哈希算法通过获取图像的哈希值并比较两幅图像的哈希值的汉明距离来衡量两幅 ...

  7. [OpenCV实战]28 基于OpenCV的GUI库cvui

    目录 1 cvui的使用 1.1 如何在您的应用程序中添加cvui 1.2 基本的"hello world"应用程序 2 更高级的应用 3 代码 4 参考 有很多很棒的GUI库,例 ...

  8. [OpenCV实战]15 基于深度学习的目标跟踪算法GOTURN

    目录 1 什么是对象跟踪和GOTURN 2 在OpenCV中使用GOTURN 3 GOTURN优缺点 4 参考 在这篇文章中,我们将学习一种基于深度学习的目标跟踪算法GOTURN.GOTURN在Caf ...

  9. [OpenCV实战]5 基于深度学习的文本检测

    目录 1 网络加载 2 读取图像 3 前向传播 4 处理输出 3结果和代码 3.1结果 3.2 代码 参考 在这篇文章中,我们将逐字逐句地尝试找到图片中的单词!基于最近的一篇论文进行文字检测. EAS ...

随机推荐

  1. 论文解读(GGD)《Rethinking and Scaling Up Graph Contrastive Learning: An Extremely Efficient Approach with Group Discrimination》

    论文信息 论文标题:Rethinking and Scaling Up Graph Contrastive Learning: An Extremely Efficient Approach with ...

  2. 如何使用python程序打包工具pyinstaller

    **通过**```pythonpyinstaller -F demo.py```**即可把demo.py打包成可独立运行的demo.exe** **pyinstaller在cmd使用格式:***pyi ...

  3. settings.py 配置汇总

    数据库配置: DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', # 数据库引擎 'NAME': ' ', #数据库名称 ...

  4. 22.-CSRF攻击

    一.CSRF-跨站伪造请求攻击 某些恶意网站上包含链接.表单按钮或者JavaScript,它们会利用登录过的用户在浏览器中的认证信息视图在你的网站上完成某些操作 这就是跨站请求伪造(CSRF,即Cro ...

  5. 六、模型层(ORM)

    六.模型层(ORM) Django中内嵌了ORM框架,不需要直接编写SQL语句进行数据库操作,而是通过定义模型类,操作模型类来完成对数据库中表的增删改查和创建等操作. O是object,也就类对象的意 ...

  6. 转载:Python 实现百度翻译

    来源: https://blog.csdn.net/qq_44814439/article/details/105642066 作者: Chloemxc 功能: Python 实现百度翻译 from ...

  7. Day13 note

    super注意点: 1.super调用父类的构造方法,必须在构造方法的第一行 2.super必须只能出现在子类的方法或者构造方法中 3.super和this不能同时调用构造方法对比this: 1.代表 ...

  8. TypeScript(基础篇)day01

    一.TS介绍 1.1 简介 ts是2012年由微软开发,在js的基础上添加了类型支持 1.2 优劣势 优势 :任何位置都有代码提示,增加效率:类型系统重构更容易:使用最新的ECMAscript语法 劣 ...

  9. 实现Swaggera的在线接口调试

    1.访问Swagger的路径是:http://localhost:8080/swagger-ui.html 如果项目正常,则可看到如下界面: 2.点开下面的随意一个方法 如add添加数据的方法,展开: ...

  10. Kubernetes核心技术-Controller

    Kubernetes核心技术-Controller 内容 什么是Controller Pod和Controller的关系 Deployment控制器应用场景Deployment控制器应用 yaml文件 ...