一,简介:
视频防抖技术是一种通过算法补偿摄像过程中产生的抖动,以提升视频画面稳定性的方法。其主要目的是消除由于手持拍摄、移动平台震动或摄像设备机械不稳定等因素引起的画面波动,从而提高视频的整体质量和观看体验。
防抖技术的核心包括以下几个关键步骤:
- 特征点检测与跟踪:通过在视频序列中检测并跟踪特征点,捕捉帧与帧之间的相对运动。
- 运动估计:基于特征点的跟踪结果,估计摄像机的运动向量,包括平移、旋转和缩放等参数。
- 运动平滑:对估计出的运动向量进行滤波处理,以减少突兀的运动变化,实现平滑的运动轨迹。
- 画面矫正:利用运动估计得到的数据,通过几何变换(如仿射变换或透视变换)对视频帧进行矫正,以抵消摄像机的抖动。
视频防抖技术的应用范围广泛,包括但不限于电影制作、电视新闻采集、监控视频分析和无人机航拍等领域。随着技术的不断进步,视频防抖算法正朝着更加智能化、实时化和高效化的方向发展。
二,opencv视频防抖方案
要实现视频防抖首先加载视频文件,并提取视频的帧数、分辨率和帧率等关键信息。接着,分析视频帧之间的差异来计算每一帧的位移和旋转变化,这些变化被累积起来形成一个表示视频整体运动的轨迹。并通过一个滤波器对轨迹进行平滑,以消除小的波动。然后,这个平滑后的轨迹被用来调整每一帧,以补偿原始视频中的抖动,从而创建出一个更加稳定的视频流。最终,稳定化处理后的视频帧与原始视频帧并排组合,输出到一个新的视频文件中。整个流程的本质就是对每一帧抖动的图片进行仿射变换以此达到消抖的作用所以核心就是如何估计一个优秀的仿射变换矩阵。
三、算法实现步骤
3.1 帧间仿射变换矩阵获取
要想获取帧间的仿射变换矩阵。需要经历以下几部操作,首先,初始化一个变换参数数组,用于存储视频帧间的平移和旋转信息。随后,通过循环遍历视频帧序列(除去首尾帧),在每一帧的灰度图像上检测关键特征点。接着,读取下一帧视频,并将其转换为灰度图像以进行光流计算。利用Lucas-Kanade算法计算当前帧到下一帧的特征点光流,筛选出跟踪成功的特征点。基于这些特征点,估计两帧之间的仿射变换矩阵;若跟踪到的特征点不足,则默认使用单位矩阵表示无变换。之后从仿射变换矩阵中提取出平移和旋转参数,并将这些参数存储于先前初始化的变换数组中。最后,更新当前帧的灰度图像,为下一轮的运动估计做准备。
代码如下(示例):
prev_gray = cv2.cvtColor(prev, cv2.COLOR_BGR2GRAY)
# 初始化变换数组
transforms = np.zeros((n_frames - 1, 3), np.float32)
# 遍历视频的每一帧,计算帧间变换
for i in range(n_frames - 2):
prev_pts = cv2.goodFeaturesToTrack(prev_gray, maxCorners=200, qualityLevel=0.01, minDistance=30, blockSize=3)
success, curr = cap.read()
if not success:
break
curr_gray = cv2.cvtColor(curr, cv2.COLOR_BGR2GRAY)
curr_pts, status, err = cv2.calcOpticalFlowPyrLK(prev_gray, curr_gray, prev_pts, None)
# 筛选出成功跟踪的点
idx = np.where(status == 1)[0]
prev_pts = prev_pts[idx]
curr_pts = curr_pts[idx]
# 如果跟踪的点太少,则使用单位矩阵
if prev_pts.shape[0] < 4:
m = np.eye(2, 3, dtype=np.float32)
else:
m, _ = cv2.estimateAffinePartial2D(prev_pts, curr_pts) # 估计仿射变换矩阵
#m是一个2x3的仿射变换矩阵,其一般形式如下
#[ m00 m01 m02 ]
#[ m10 m11 m12 ]
# m00和m01代表旋转和缩放的部分,而m10和m11同样也代表旋转和缩放的部分。m02和m12代表平移的部分。
if m is None:
m = np.eye(2, 3, dtype=np.float32)
# 提取变换参数
dx = m[0, 2]
dy = m[1, 2]
da = np.arctan2(m[1, 0], m[0, 0])
# 保存变换
transforms[i] = [dx, dy, da]
prev_gray = curr_gray
print(f"Frame: {i}/{n_frames - 2} - Tracked points: {len(prev_pts)}")
3.2 平滑仿射变换矩阵的变化序列
将视频所有的帧的变化轨迹融合为一个新的数组,新的数组一般称为轨迹,我们在进行图像消抖的时候需要进行平滑轨迹,目的是为了减少噪声、提高数据准确性和稳定性,改善运动估计,保持物理连贯性从而提升图像消抖的效果。
代码如下(示例):
def moving_average(curve, radius):
window_size = 2 * radius + 1 # 窗口大小
f = np.ones(window_size) / window_size # 创建一个平均滤波器
curve_pad = np.lib.pad(curve, (radius, radius), 'edge') # 对曲线进行边缘填充
curve_smoothed = np.convolve(curve_pad, f, mode='same') # 应用卷积操作进行滤波
curve_smoothed = curve_smoothed[radius:-radius] # 去除填充的边缘
return curve_smoothed
# 定义一个函数,用于平滑整个轨迹
def smooth_trajectory(trajectory):
smoothed_trajectory = np.copy(trajectory) # 复制轨迹数组
for i in range(3): # 对轨迹的每个维度进行平滑处理
smoothed_trajectory[:, i] = moving_average(trajectory[:, i], radius=SMOOTHING_RADIUS)
return smoothed_trajectory
**注意:**平滑一般来说不平滑变换,而是平滑所有变换的和,这样可以保留关键点在原始视频帧中的物理位置,避免直接平滑变换可能导致的物理意义的丢失。这是因为平滑轨迹是基于关键点的整体运动,而不是单独的变换参数。
trajectory = np.cumsum(transforms, axis=0)
# 平滑变换轨迹
smoothed_trajectory = smooth_trajectory(trajectory)
# 计算平滑轨迹与原始轨迹的差异
difference = smoothed_trajectory - trajectory
# 更新变换数组
# 将原始变换与差异相结合,以获得平滑的变换
transforms_smooth = transforms + difference
3.3 对视频每帧图像进行仿射变换
在计算出平滑的变换矩阵后,只需要将变换矩阵作用于视频中的每一帧即可形成一个新的去抖后的视频,实现方法如下:
# 重置视频读取位置到第一帧
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
# 遍历视频帧,应用平滑变换
for i in range(n_frames - 2):
success, frame = cap.read()
if not success:
break
# 获取平滑变换参数
dx = transforms_smooth[i, 0]
dy = transforms_smooth[i, 1]
da = transforms_smooth[i, 2]
# 构造仿射变换矩阵
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
# 应用变换到当前帧
frame_stabilized = cv2.warpAffine(frame, m, (w, h))
四:整体代码实现
# 导入必要的库
import numpy as np
import cv2
# 定义一个函数,用于对曲线进行移动平均滤波,以平滑曲线
def moving_average(curve, radius):
window_size = 2 * radius + 1 # 窗口大小
f = np.ones(window_size) / window_size # 创建一个平均滤波器
curve_pad = np.lib.pad(curve, (radius, radius), 'edge') # 对曲线进行边缘填充
curve_smoothed = np.convolve(curve_pad, f, mode='same') # 应用卷积操作进行滤波
curve_smoothed = curve_smoothed[radius:-radius] # 去除填充的边缘
return curve_smoothed
# 定义一个函数,用于平滑整个轨迹
def smooth_trajectory(trajectory):
smoothed_trajectory = np.copy(trajectory) # 复制轨迹数组
for i in range(3): # 对轨迹的每个维度进行平滑处理
smoothed_trajectory[:, i] = moving_average(trajectory[:, i], radius=SMOOTHING_RADIUS)
return smoothed_trajectory
# 定义一个函数,用于修复由于变换导致的边界问题
def fix_border(frame):
s = frame.shape # 获取帧的尺寸
T = cv2.getRotationMatrix2D((s[1] / 2, s[0] / 2), 0, 1.04) # 创建一个旋转矩阵
frame = cv2.warpAffine(frame, T, (s[1], s[0])) # 应用旋转和缩放
return frame
# 设置平滑半径
SMOOTHING_RADIUS = 50
# 打开视频文件
cap = cv2.VideoCapture(r'D:\input_video.mp4')
# 检查视频是否成功打开
if not cap.isOpened():
print("Error opening video file")
exit()
# 获取视频的总帧数、宽、高和帧率
n_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS)
# 设置视频输出格式
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
# 创建视频写入对象
out = cv2.VideoWriter('stabilized_video.mp4', fourcc, fps, (2 * w, h))
# 读取视频的第一帧
_, prev = cap.read()
if prev is None:
print("Error reading video file")
cap.release()
exit()
# 将第一帧转换为灰度图
prev_gray = cv2.cvtColor(prev, cv2.COLOR_BGR2GRAY)
# 初始化变换数组
transforms = np.zeros((n_frames - 1, 3), np.float32)
# 遍历视频的每一帧,计算帧间变换
for i in range(n_frames - 2):
prev_pts = cv2.goodFeaturesToTrack(prev_gray, maxCorners=200, qualityLevel=0.01, minDistance=30, blockSize=3)
success, curr = cap.read()
if not success:
break
curr_gray = cv2.cvtColor(curr, cv2.COLOR_BGR2GRAY)
curr_pts, status, err = cv2.calcOpticalFlowPyrLK(prev_gray, curr_gray, prev_pts, None)
# 筛选出成功跟踪的点
idx = np.where(status == 1)[0]
prev_pts = prev_pts[idx]
curr_pts = curr_pts[idx]
# 如果跟踪的点太少,则使用单位矩阵
if prev_pts.shape[0] < 4:
m = np.eye(2, 3, dtype=np.float32)
else:
m, _ = cv2.estimateAffinePartial2D(prev_pts, curr_pts) # 估计仿射变换矩阵
if m is None:
m = np.eye(2, 3, dtype=np.float32)
# 提取变换参数
dx = m[0, 2]
dy = m[1, 2]
da = np.arctan2(m[1, 0], m[0, 0])
# 保存变换
transforms[i] = [dx, dy, da]
prev_gray = curr_gray
print(f"Frame: {i}/{n_frames - 2} - Tracked points: {len(prev_pts)}")
print(transforms)
# 计算累积变换轨迹
trajectory = np.cumsum(transforms, axis=0)
# 平滑变换轨迹
smoothed_trajectory = smooth_trajectory(trajectory)
# 计算平滑轨迹与原始轨迹的差异
difference = smoothed_trajectory - trajectory
# 更新变换数组
# 将原始变换与差异相结合,以获得平滑的变换
transforms_smooth = transforms + difference
# 重置视频读取位置到第一帧
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
# 遍历视频帧,应用平滑变换
for i in range(n_frames - 2):
success, frame = cap.read()
if not success:
break
# 获取平滑变换参数
dx = transforms_smooth[i, 0]
dy = transforms_smooth[i, 1]
da = transforms_smooth[i, 2]
# 构造仿射变换矩阵
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
# 应用变换到当前帧
frame_stabilized = cv2.warpAffine(frame, m, (w, h))
# 修复变换后的边界问题
frame_stabilized = fix_border(frame_stabilized)
# 将原始帧和平滑帧并排放置
frame_out = cv2.hconcat([frame, frame_stabilized])
# 如果输出帧的宽度超过1920,则进行缩放
if frame_out.shape[1] > 1920:
frame_out = cv2.resize(frame_out, (frame_out.shape[1] // 2, frame_out.shape[0] // 2))
# 将处理后的帧写入输出视频
out.write(frame_out)
# 释放视频读取和写入对象
cap.release()
out.release()
# 关闭所有OpenCV窗口
cv2.destroyAllWindows()