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 去抖效果

2.8 OpenCV代码

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

#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. 最后

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐