视频去抖动算法原理及代码详解

image

1. 背景

点播、直播行业的蓬勃发展,使用户生产视频(UGC)逐渐替代了专家生产和平台生产的方式,成为了主流。由于广大用户不可能全都具备专业素质和专业器材,其产出的视频往往质量较差,最明显的特征就是存在抖动。

减少视频抖动有很多方法,包括

  • 使用专业摄影辅助器材,如三脚架
  • 使用带有物理防抖功能的镜头,如iphone
  • 使用带有实时防抖功能的软件
  • 使用Premiere,AfterEffects等视频软件进行后期防抖

以上几种方式,实践中都经常被采用。然而这些方法都各自存在缺陷。辅助器材笨重、不便携,成本较高;物理防抖设备成本较高;软件防抖对硬件性能要求较高,且会使镜头移动时有一种“笨重”感,体验不佳;软件后期防抖则只有专业人士才能进行。

针对上述问题,一个较好的解决方案是使用算法自动完成视频后期抖动处理。笔者从零开始初步实现了一套类似的系统。下文将逐步介绍此系统的工作流程。

2. 算法流程

2.1 运动分析

视频抖动的本质是图像存在着微小、方向随机、频率较高的运动。首先要检测到图像帧与帧之间的运动方向。

2.2 角点检测

图像中的任何一个物体都通常含有独特的特征,但往往由大量的像素点构成。角点是能够准确描述这个物体的一个数量较少的点集。角点检测算法可以分析出图像最明显的特征点,用于物件识别和跟踪。

在这里插入图片描述

2.3 光流

由于目标对象或者摄像机的移动造成的图像对象在连续两帧图像中的移动被称为光流。它是一个2D向量场,可以用来显示一个点从第一帧图像到第二帧图像之间的移动。

在这里插入图片描述

2.4 RANSAC

RANSAC是“RANdomSAmple Consensus(随机抽样一致)”的缩写。它可以从一组包含“局外点”的观测数据集中,通过迭代方式估计数学模型的参数。

两帧连续图像有各自的角点集合,RANSAC可以从含有噪声的数据中发现相互匹配的点集,进而计算出两帧图像的变换矩阵。

在这里插入图片描述

2.5 运动平滑

2.5.1 维度选择

利用图像匹配算法,我们可以获得两幅图像之间的变换矩阵,矩阵包含了大量的信息。但在视频防抖需求中,我们需要关心的只有3个信息:水平位移、竖直位移和旋转角度。从矩阵中抽出相应的值,可以得到如下运动轨迹曲线。曲线中大量的“毛刺”就是我们要消除的抖动。

水平方向运动轨迹

水平方向运动轨迹

竖直方向运动轨迹

竖直方向运动轨迹

旋转角度

旋转角度

2.5.2 运动轨迹平滑

这里一般使用滤波、拟合或最优化等方法来对曲线进行平滑,下面是两种不同的算法得到的结果。

1. Kalman滤波

Kalman滤波在控制类场景中运用较多,使用前面的运动来预测下一个运动,消除采样噪声。

由于Kalman只依赖前面的数据,所以更适合软件实时防抖。在后期防抖中,得出的结果往往会有一些“惯性”,效果并非最佳。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

2. 中值滤波

一种最简单但有效的滤波方式。在防抖场景中的缺点是对结果缺乏掌控。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

2.5.3 修复运动计算

平滑轨迹与原始轨迹做差即可获得修复运动参数。

2.6 图像变换

仿射变换(Affine Transformation或 Affine Map)是一种二维坐标到二维坐标之间的线性变换,它可以保持图像的平直性和平行性。变换方式与矩阵参数的一些基本形式如下图。

在这里插入图片描述

2.7 去抖效果

[video(video-MFoQNvqI-1575803817039)(type-tencent)(url-https://v.qq.com/txp/iframe/player.html?vid=i0905uaprzt)(image-http://puui.qpic.cn/vpic/0/i0905uaprzt.png/0)(title-)]

2.8 OpenCV代码

OpneCV3.x中提供了专门应用于视频稳像技术的模块,该模块包含一系列用于全局运动图像估计的函数和类。结构体videostab::RansacParams实现了RANSAC算法,这个算法用来实现连续帧间的运动估计。videostab::MotionEstimatorBase是基类中所有全局运动估计方法,videostab::MotionEstimatorRansacL2描述了一个健壮的RANSAC-based全局二维估计方法的最小化L2误差。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
#include <opencv2/opencv.hpp>

#include <opencv2/videostab.hpp>

#include <string>

#include <iostream>

 

using namespace std;

using namespace cv;

using namespace cv::videostab;

 

string inputPath = "inputVideo.avi";

string outputPath = "outputVideo.avi";

 

// 视频稳定输出

void videoOutput(Ptr<IFrameSource> stabFrames, string outputPath)

{

 VideoWriter writer;

 cv::Mat stabFrame;

 int nframes = 0;

 // 设置输出帧率

 double outputFps = 25;

 // 遍历搜索视频帧

 while (!(stabFrame = stabFrames->nextFrame()).empty())

 {

  nframes++;

  // 输出视频稳定帧

  if (!outputPath.empty())

  {

   if (!writer.isOpened())

    writer.open(outputPath, VideoWriter::fourcc('X', 'V', 'I', 'D'),

    outputFps, stabFrame.size());

   writer << stabFrame;

  }

  imshow("stabFrame", stabFrame);

  // esc键退出

  char key = static_cast<char>(waitKey(100));

  if (key == 27)

  {

   cout << endl;

   break;

  }

 }

 std::cout << "nFrames: " << nframes << endl;

 std::cout << "finished " << endl;

}

 

void cacStabVideo(Ptr<IFrameSource> stabFrames, string srcVideoFile)

{

 try

 {

 

  Ptr<VideoFileSource> srcVideo = makePtr<VideoFileSource>(inputPath);

  cout << "frame count: " << srcVideo->count() << endl;

 

  // 运动估计

  double estPara = 0.1;

  Ptr<MotionEstimatorRansacL2> est =

   makePtr<MotionEstimatorRansacL2>(MM_AFFINE);

 

  // Ransac参数设置

  RansacParams ransac = est->ransacParams();

  ransac.size = 3;

  ransac.thresh = 5;

  ransac.eps = 0.5;

 

  // Ransac计算

  est->setRansacParams(ransac);

  est->setMinInlierRatio(estPara);

 

  // Fast特征检测

  Ptr<FastFeatureDetector> feature_detector =

   FastFeatureDetector::create();

 

  // 运动估计关键点匹配

  Ptr<KeypointBasedMotionEstimator> motionEstBuilder =

   makePtr<KeypointBasedMotionEstimator>(est);

 

  // 设置特征检测器

  motionEstBuilder->setDetector(feature_detector);

  Ptr<IOutlierRejector> outlierRejector = makePtr<NullOutlierRejector>();

  motionEstBuilder->setOutlierRejector(outlierRejector);

 

  // 3-Prepare the stabilizer

  StabilizerBase *stabilizer = 0;

  // first, prepare the one or two pass stabilizer

  bool isTwoPass = 1;

  int radius_pass = 15;

  if (isTwoPass)

  {

   // with a two pass stabilizer

   bool est_trim = true;

   TwoPassStabilizer *twoPassStabilizer = new TwoPassStabilizer();

   twoPassStabilizer->setEstimateTrimRatio(est_trim);

   twoPassStabilizer->setMotionStabilizer(

    makePtr<GaussianMotionFilter>(radius_pass));

   stabilizer = twoPassStabilizer;

  }

  else

  {

   // with an one pass stabilizer

   OnePassStabilizer *onePassStabilizer = new OnePassStabilizer();

   onePassStabilizer->setMotionFilter(

    makePtr<GaussianMotionFilter>(radius_pass));

   stabilizer = onePassStabilizer;

  }

 

  // second, set up the parameters

  int radius = 15;

  double trim_ratio = 0.1;

  bool incl_constr = false;

  stabilizer->setFrameSource(srcVideo);

  stabilizer->setMotionEstimator(motionEstBuilder);

  stabilizer->setRadius(radius);

  stabilizer->setTrimRatio(trim_ratio);

  stabilizer->setCorrectionForInclusion(incl_constr);

  stabilizer->setBorderMode(BORDER_REPLICATE);

  // cast stabilizer to simple frame source interface to read stabilized frames

  stabFrames.reset(dynamic_cast<IFrameSource*>(stabilizer));

  // 4-videoOutput the stabilized frames. The results are showed and saved.

  videoOutput(stabFrames, outputPath);

 }

 

 catch (const exception &e)

 {

  cout << "error: " << e.what() << endl;

  stabFrames.release();

 }

}

 

int main(int argc, char* argv[])

{

 Ptr<IFrameSource> stabFrames;

 // 输入输出视频准备

 

 cacStabVideo(stabFrames, inputPath);

 stabFrames.release();

 

 return 0;

}

3. 最后

image