视频稳定算法
视频稳定是指一组用来减少摄像机运动对最终视频的影响的方法。摄像机的运动可以是平移(即在x, y, z方向上的运动)或旋转(偏航,俯仰,滚动)。
视频稳定方法
包括机械、光学和数字稳定方法
机械稳像:
机械稳像系统利用陀螺仪和加速度计等特殊传感器检测到的运动来移动图像传感器,以补偿摄像机的运动。
光学视频稳定 :
在这种方法中,稳定不是移动整个相机,而是通过移动镜头的部分来实现。这种方法使用了一个可移动的透镜组件,当光通过时,它可以可变地调整光的路径长度。
数字视频稳定:
这种方法不需要特殊的传感器来估计摄像机的运动。有三个主要步骤:1)运动估计2)运动平滑3)图像合成。第一阶段导出了连续两帧之间的变换参数。第二阶段滤除不需要的运动,最后阶段重建稳定的视频。
一个快速和稳健的数字视频稳定算法的实现是基于一个二维运动模型,在这个模型中,应用了一个欧几里得(又名相似度)变换,结合了平移、旋转和缩放
如上图所示,在欧几里得运动模型中,图像中的正方形可以转换为任何位置、大小或旋转不同的正方形。它比仿射和单应变换有更多的限制,但对于运动稳定是足够的,因为在连续的视频帧之间的摄像机运动通常是小的。
利用特征点匹配实现视频稳定
跟踪两个连续帧之间的几个特征点,跟踪特征后估计帧之间的运动并进行补偿。
python流程为:
步骤1:设置“输入”和“输出”视频
首先,让我们完成读取输入视频和写入输出视频的设置。代码中的注释解释了每一行。
# Import numpy and OpenCV
import numpy as np
import cv2
# Read input video
cap = cv2.VideoCapture('video.mp4')
# Get frame count
n_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
# Get width and height of video stream
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
# Define the codec for output video
fourcc = cv2.VideoWriter_fourcc(*'MJPG')
# Set up output video
out = cv2.VideoWriter('video_out.mp4', fourcc, fps, (w, h))
步骤2:读取第一帧并将其转换为灰度
对于视频稳定,我们需要捕捉一个视频的两帧,估计帧之间的运动,并最终纠正运动。
# Read first frame
_, prev = cap.read()
# Convert frame to grayscale
prev_gray = cv2.cvtColor(prev, cv2.COLOR_BGR2GRAY)
步骤3:寻找帧之间的运动
这是算法中最关键的部分。我们将遍历所有帧,并找到当前帧和前一帧之间的运动。没有必要知道每个像素的运动。欧几里得运动模型要求我们只知道两个坐标系中两个点的运动。但在实践中,最好是找到50-100个点的运动,然后用它们来鲁棒估计运动模型。
(1)使用goodFeaturesToTrack
现在的问题是我们应该选择什么点来跟踪。记住,跟踪算法使用一个点周围的小补丁来跟踪它。这种跟踪算法会遇到光圈问题,如下面的视频所述
所以,光滑的区域不利于跟踪,而有很多角落的纹理区域是好的。幸运的是,OpenCV有一个快速的特征检测器,可以检测出理想的跟踪特征。它被称为goodFeaturesToTrack。
(2)calcOpticalFlowPyrLK
一旦我们在前一帧中找到好的特征,我们就可以使用一种叫做卢卡斯-卡纳德光流(Lucas-Kanade Optical Flow)的算法在下一帧中跟踪它们,这种算法是以算法的发明者命名的。
它是通过OpenCV中的calcOpticalFlowPyrLK函数实现的。在calcOpticalFlowPyrLK这个名字中,LK代表Lucas-Kanade, Pyr代表金字塔。计算机视觉中的图像金字塔用于处理不同尺度(分辨率)的图像。
由于各种原因,calcOpticalFlowPyrLK可能无法计算出所有点的运动。例如,当前帧中的特征点可能会被下一帧中的另一个对象遮挡。幸运的是,正如您在下面的代码中看到的,calcOpticalFlowPyrLK中的状态标志可以用来过滤掉这些值。
(3)estimateRigidTransform
Pre-define transformation-store array
transforms = np.zeros((n_frames-1, 3), np.float32)
for i in range(n_frames-2):
Detect feature points in previous frame
prev_pts = cv2.goodFeaturesToTrack(prev_gray,
maxCorners=200,
qualityLevel=0.01,
minDistance=30,
blockSize=3)
Read next frame
success, curr = cap.read()
if not success:
break
Convert to grayscale
curr_gray = cv2.cvtColor(curr, cv2.COLOR_BGR2GRAY)
Calculate optical flow (i.e. track feature points)
curr_pts, status, err = cv2.calcOpticalFlowPyrLK(prev_gray, curr_gray, prev_pts, None)
Sanity check
assert prev_pts.shape == curr_pts.shape
Filter only valid points
idx = np.where(status==1)[0]
prev_pts = prev_pts[idx]
curr_pts = curr_pts[idx]
#Find transformation matrix
m = cv2.estimateRigidTransform(prev_pts, curr_pts, fullAffine=False) #will only work with OpenCV-3 or less
Extract traslation
dx = m[0,2]
dy = m[1,2]在这里插入代码片
Extract rotation angle
da = np.arctan2(m[1,0], m[0,0])
Store transformation
transforms[i] = [dx,dy,da]
Move to next frame
prev_gray = curr_gray
print("Frame: " + str(i) + “/” + str(n_frames) + " - Tracked points : " + str(len(prev_pts)))
在下面的代码中,我们循环这些帧并执行步骤3.1到3.3
第四步:计算帧之间的平滑运动
在之前的步骤中,我们估计了帧之间的移动并将它们存储在一个数组中。我们现在需要通过累积加上一步估计的微分运动来找到运动轨迹。
(1)步骤4.1:计算轨迹
在这一步中,我们将相加两帧之间的运动来计算轨迹。我们的最终目标是使这条轨迹变得平滑。
#cumsum(累积和)
# Compute trajectory using cumulative sum of transformations
trajectory = np.cumsum(transforms, axis=0)
我们还定义了一个函数cumsum,它接受TransformParams的向量,并通过执行微分运动dx、dy和da(角度)的累加和返回轨迹。
步骤4.2:计算平滑轨迹
在前面的步骤中,我们计算了运动轨迹。所以我们有三条曲线来显示运动(x, y和角度)如何随时间变化。
在这一步中,我们将展示如何平滑这三条曲线。
平滑任何曲线最简单的方法是使用移动平均过滤器。顾名思义,移动平均滤波器用窗口定义的函数相邻值的平均值来替换函数在该点上的值。让我们看一个例子。
假设我们在数组c中存储了一条曲线,所以曲线上的点是c[0]…c[n-1]。设f为我们用宽度为5的移动平均滤波器对c进行滤波得到的平滑曲线。
这条曲线的k^{th}元素的计算使用。正如你所看到的,平滑曲线的值是噪声曲线在一个小窗口内的平均值。下图显示了左侧噪声曲线的一个例子,右侧使用尺寸为5的盒形滤波器进行平滑。
我们定义了一个移动平均滤波器,它接受任何曲线(即数字的一维)作为输入,并返回平滑的曲线版本。
def movingAverage(curve, radius):
window_size = 2 * radius + 1
# Define the filter
f = np.ones(window_size)/window_size
# Add padding to the boundaries
curve_pad = np.lib.pad(curve, (radius, radius), 'edge')
# Apply convolution
curve_smoothed = np.convolve(curve_pad, f, mode='same')
# Remove padding
curve_smoothed = curve_smoothed[radius:-radius]
# return smoothed curve
return curve_smoothed
定义了一个函数,它接受轨迹并对三个分量进行平滑处理。
def smooth(trajectory):
smoothed_trajectory = np.copy(trajectory)
# Filter the x, y and angle curves
for i in range(3):
smoothed_trajectory[:,i] = movingAverage(trajectory[:,i], radius=SMOOTHING_RADIUS)
return smoothed_trajectory
这是最后的用法
# Compute trajectory using cumulative sum of transformations
trajectory = np.cumsum(transforms, axis=0)
步骤4.3计算光滑变换
到目前为止,我们获得了平稳的轨迹。在这一步中,我们将使用平滑轨迹得到平滑变换,可以应用到视频的帧上使其稳定。
这是通过找到平滑轨道和原始轨道之间的差值然后把这个差值加回到原始变换中
# Calculate difference in smoothed_trajectory and trajectory
difference = smoothed_trajectory - trajectory
# Calculate newer transformation array
transforms_smooth = transforms + difference
步骤5:应用平滑的相机运动帧
差不多做完了。我们现在需要做的就是循环遍历这些帧并应用我们刚刚计算的变换。
如果有一个运动指定为(x, y, \theta),相应的变换矩阵为
# Reset stream to first frame
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
# Write n_frames-1 transformed frames
for i in range(n_frames-2):
# Read next frame
success, frame = cap.read()
if not success:
break
# Extract transformations from the new transformation array
dx = transforms_smooth[i,0]
dy = transforms_smooth[i,1]
da = transforms_smooth[i,2]
# Reconstruct transformation matrix accordingly to new values
m = np.zeros((2,3), np.float32)
m[0,0] = np.cos(da)
m[0,1] = -np.sin(da)
m[1,0] = np.sin(da)
m[1,1] = np.cos(da)
m[0,2] = dx
m[1,2] = dy
# Apply affine wrapping to the given frame
frame_stabilized = cv2.warpAffine(frame, m, (w,h))
# Fix border artifacts
frame_stabilized = fixBorder(frame_stabilized)
# Write the frame to the file
frame_out = cv2.hconcat([frame, frame_stabilized])
# If the image is too big, resize it.
if(frame_out.shape[1] > 1920):
frame_out = cv2.resize(frame_out, (frame_out.shape[1]/2, frame_out.shape[0]/2));
cv2.imshow("Before and After", frame_out)
cv2.waitKey(10)
out.write(frame_out)
步骤5.1:修复边界
当我们稳定视频时,我们可能会看到一些黑色边界伪影。这是意料之中的,因为为了稳定视频,可能需要缩小帧的大小。
我们可以通过将视频的中心缩放少量(例如4%)来缓解这个问题。
面的函数fixBorder显示了实现。我们使用getRotationMatrix2D,因为它缩放和旋转图像而不移动图像的中心。我们所需要做的就是以0旋转和缩放1.04(即4%的高档)来调用这个函数。
def fixBorder(frame):
s = frame.shape
# Scale the image 4% without moving the center
T = cv2.getRotationMatrix2D((s[1]/2, s[0]/2), 0, 1.04)
frame = cv2.warpAffine(frame, T, (s[1], s[0]))
return frame
显著减少运动,但不是完全消除它。
我们让读者去想如何修改代码来完全消除帧间的移动。如果你试图消除所有的相机运动,会有什么副作用?
目前的方法只适用于固定长度的视频,而不适用于实时提要。我们必须大量修改这个方法以获得实时视频输出,这超出了本文的范围,但它是可以实现的,更多信息可以在这里找到
优点
这种方法对低频运动(较慢的振动)提供了良好的稳定性。
这种方法具有较低的内存消耗,因此非常适合嵌入式设备(如树莓派)。
这种方法对视频中的缩放抖动很好。
缺点
这种方法对高频扰动的抑制效果很差。
如果存在严重的运动模糊,特征跟踪就会失败,结果也不会是最优的。
这种方法对卷帘失真也不好