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