视频防抖技术的实现

article/2025/4/21 18:12:52

1. 介绍

视频防抖是指用于减少摄像机运动对最终视频的影响的一系列方法。摄像机的运动可以是平移(比如沿着x、y、z方向上的运动)或旋转(偏航、俯仰、翻滚)。

对视频防抖的需求在许多领域都有。

这在消费者和专业摄像中是极其重要的。因此,存在许多不同的机械、光学和算法解决方案。即使在静态图像拍摄中,防抖技术也可以帮助拍摄长时间曝光的手持照片。

在内窥镜和结肠镜等医疗诊断应用中,需要对视频进行稳定,以确定问题的确切位置和宽度。

同样,在军事应用中,无人机在侦察飞行中捕获的视频也需要进行稳定,以便定位、导航、目标跟踪等。同样的道理也适用于机器人。

2. 防抖策略

视频防抖的方法包括机械稳定方法、光学稳定方法和数字稳定方法。下面将简要讨论这些问题:

  • 机械视频稳定:机械图像稳定系统使用由特殊传感器如陀螺仪和加速度计检测到的运动来移动图像传感器以补偿摄像机的运动。
  • 光学视频稳定:在这种方法中,不是移动整个摄像机,而是通过镜头的移动部分来实现稳定。这种方法使用了一个可移动的镜头组合,当光通过相机的镜头系统时,可以可变地调整光的路径长度。
  • 数字视频稳定:这种方法不需要特殊的传感器来估计摄像机的运动。主要有三个步骤:1)运动估计2)运动平滑,3)图像合成。第一步导出了两个连续坐标系之间的变换参数。第二步过滤不需要的运动,在最后一步重建稳定的视频。

在这篇文章中,我们将学习一个快速和鲁棒性好的数字视频稳定算法的实现。它是基于二维运动模型,其中我们应用欧几里得(即相似性)变换包含平移、旋转和缩放。

在这里插入图片描述
正如你在上面的图片中看到的,在欧几里得运动模型中,图像中的一个正方形可以转换为任何其他位置、大小或旋转不同的正方形。它比仿射变换和单应变换限制更严格,但对于运动稳定来说足够了,因为摄像机在视频连续帧之间的运动通常很小。

3. 基于点特征匹配的视频防抖

该方法涉及跟踪两个连续帧之间的多个特征点。跟踪特征允许我们估计帧之间的运动并对其进行补偿。

下面的流程图显示了基本步骤。
在这里插入图片描述

3.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))

3.2 读取第一帧并将其转换为灰度图

对于视频稳定,我们需要捕捉视频的两帧,估计帧之间的运动,最后校正运动。

# Read first frame
_, prev = cap.read() # Convert frame to grayscale
prev_gray = cv2.cvtColor(prev, cv2.COLOR_BGR2GRAY)

3.3 寻找帧之间的移动

这是算法中最关键的部分。我们将遍历所有的帧,并找到当前帧和前一帧之间的移动。没有必要知道每一个像素的运动。欧几里得运动模型要求我们知道两个坐标系中两个点的运动。但是在实际应用中,找到50-100个点的运动,然后用它们来稳健地估计运动模型是一个好方法。

  1. 可用于跟踪的优质特征
    现在的问题是我们应该选择哪些点进行跟踪。请记住,跟踪算法使用一个小补丁围绕一个点来跟踪它。这样的跟踪算法受到孔径问题的困扰,如下面的视频所述, 因此,光滑的区域不利于跟踪,而有很多角的纹理区域则比较好。幸运的是,OpenCV有一个快速的特征检测器,可以检测最适合跟踪的特性。它被称为goodFeaturesToTrack)
  2. Lucas-Kanade光流
    一旦我们在前一帧中找到好的特征,我们就可以使用Lucas-Kanade光流算法在下一帧中跟踪它们。
    它是利用OpenCV中的calcOpticalFlowPyrLK函数实现的。在calcOpticalFlowPyrLK这个名字中,LK代表Lucas-Kanade,而Pyr代表金字塔。计算机视觉中的图像金字塔是用来处理不同尺度(分辨率)的图像的。
    由于各种原因,calcOpticalFlowPyrLK可能无法计算出所有点的运动。例如,当前帧的特征点可能会被下一帧的另一个对象遮挡。幸运的是,您将在下面的代码中看到,calcOpticalFlowPyrLK中的状态标志可以用来过滤掉这些值。
  3. 估计运动
    在步骤1中,我们在前一帧中找到了一些好的特征。在步骤2中,我们使用光流来跟踪特征。换句话说,我们已经找到了特征在当前帧中的位置,并且我们已经知道了特征在前一帧中的位置。所以我们可以使用这两组点来找到映射前一个坐标系到当前坐标系的刚性(欧几里德)变换。这是使用函数estimateRigidTransform完成的。
    一旦我们估计了运动,我们可以把它分解成x和y的平移和旋转(角度)。我们将这些值存储在一个数组中,这样就可以平稳地更改它们。

下面的代码将完成步骤1到3。

# 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 frameprev_pts = cv2.goodFeaturesToTrack(prev_gray,maxCorners=200,qualityLevel=0.01,minDistance=30,blockSize=3)# Read next framesuccess, curr = cap.read() if not success: break # Convert to grayscalecurr_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 checkassert prev_pts.shape == curr_pts.shape # Filter only valid pointsidx = np.where(status==1)[0]prev_pts = prev_pts[idx]curr_pts = curr_pts[idx]#Find transformation matrixm = cv2.estimateRigidTransform(prev_pts, curr_pts, fullAffine=False) #will only work with OpenCV-3 or less# Extract traslationdx = m[0,2]dy = m[1,2]# Extract rotation angleda = np.arctan2(m[1,0], m[0,0])# Store transformationtransforms[i] = [dx,dy,da]# Move to next frameprev_gray = curr_grayprint("Frame: " + str(i) +  "/" + str(n_frames) + " -  Tracked points : " + str(len(prev_pts)))

3.4 计算帧之间的平滑运动

在前面的步骤中,我们估计帧之间的运动并将它们存储在一个数组中。我们现在需要通过叠加上一步估计的微分运动来找到运动轨迹。

  1. 轨迹计算
    在这一步,我们将增加运动之间的帧来计算轨迹。我们的最终目标是平滑这条轨迹。
    Python 在Python中,可以很容易地使用numpy中的cumsum(累计和)来实现。
# Compute trajectory using cumulative sum of transformations
trajectory = np.cumsum(transforms, axis=0
  1. 计算平滑轨迹
    在上一步中,我们计算了运动轨迹。所以我们有三条曲线来显示运动(x, y,和角度)如何随时间变化。
    在这一步,我们将展示如何平滑这三条曲线。
    平滑任何曲线最简单的方法是使用移动平均滤波器(moving average filter)。顾名思义,移动平均过滤器将函数在某一点上的值替换为由窗口定义的其相邻函数的平均值。让我们看一个例子。
    假设我们在数组c中存储了一条曲线,那么曲线上的点是c[0]…c[n-1]。设f是我们通过宽度为5的移动平均滤波器过滤c得到的平滑曲线。
    该曲线的 k t h k^{th} kth元素使用
    在这里插入图片描述
    如您所见,平滑曲线的值是噪声曲线在一个小窗口上的平均值。下图显示了左边的噪点曲线的例子,使用右边的尺度为5 滤波器进行平滑。
    在这里插入图片描述
    在Python实现中,我们定义了一个移动平均滤波器,它接受任何曲线(即1-D的数字)作为输入,并返回曲线的平滑版本。
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 curvereturn curve_smoothed

我们还定义了一个函数,它接受轨迹并对这三个部分进行平滑处理。

def smooth(trajectory): smoothed_trajectory = np.copy(trajectory) # Filter the x, y and angle curvesfor 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)
  1. 计算平滑变换
    到目前为止,我们已经得到了一个平滑的轨迹。在这一步,我们将使用平滑的轨迹来获得平滑的变换,可以应用到视频的帧来稳定它。
    这是通过找到平滑轨迹和原始轨迹之间的差异,并将这些差异加回到原始的变换中来完成的。
# 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 framesuccess, frame = cap.read() if not success:break# Extract transformations from the new transformation arraydx = transforms_smooth[i,0]dy = transforms_smooth[i,1]da = transforms_smooth[i,2]# Reconstruct transformation matrix accordingly to new valuesm = 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] = dxm[1,2] = dy# Apply affine wrapping to the given frameframe_stabilized = cv2.warpAffine(frame, m, (w,h))# Fix border artifactsframe_stabilized = fixBorder(frame_stabilized) # Write the frame to the fileframe_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)
  1. 修复边界伪影
    当我们稳定一个视频,我们可能会看到一些黑色的边界伪影。这是意料之中的,因为为了稳定视频,帧可能不得不缩小大小。
    我们可以通过将视频的中心缩小一小部分(例如4%)来缓解这个问题。
    下面的fixBorder函数显示了实现。我们使用getRotationMatrix2D,因为它在不移动图像中心的情况下缩放和旋转图像。我们所需要做的就是调用这个函数时,旋转为0,缩放为1.04(也就是提升4%)。
def fixBorder(frame):s = frame.shape# Scale the image 4% without moving the centerT = cv2.getRotationMatrix2D((s[1]/2, s[0]/2), 0, 1.04)frame = cv2.warpAffine(frame, T, (s[1], s[0]))return frame

5. 结果

本文分享的视频防抖代码的结果如上所示。我们的目标是显著减少运动,但不是完全消除它。

目前的方法只适用于固定长度的视频,而不适用于实时feed。我不得不对这个方法进行大量修改,以获得实时视频输出,这超出了本文的范围,但这是可以实现的,更多的信息可以在这里找到。

https://abhitronix.github.io/2018/11/30/humanoid-AEAM-3/

6. 优缺点

6.1 优点

这种方法对低频运动(较慢的振动)具有良好的稳定性。这种方法内存消耗低,因此非常适合嵌入式设备(如树莓派)。这种方法对视频缩放抖动有很好的效果。

6.2 缺点

这种方法对高频扰动的抵抗效果很差。如果有一个严重的运动模糊,特征跟踪将失败,结果将不是最佳的。这种方法也不适用于滚动快门失真。

7. 参考文献

  • Example video and Code reference from Nghia Ho’s post
  • https://www.learnopencv.com/video-stabilization-using-point-feature-matching-in-opencv/

8. 最后


http://chatgpt.dhexx.cn/article/0bQHgKIA.shtml

相关文章

音视频基础:视频的编码

本文主要参考《图像通信技术与应用》作者陈柘 陈川 记录了数字图像的模型、图像变换的数学原理、图像编码的基本思想、H.26x等视频标准、以及视频的传输模型等概念,只介绍思想,无数学推导,无代码实践,属于音视频入门知识 视频的编…

视频压缩基本概念

视频压缩基本概念 为什么需要压缩视频 首先回答why这个问题。 假设一个2小时未压缩的高清视频,19201080p的电影,我们来计算一下他的存储容量。先介绍一下帧率(frame rate或者是 FPS)概念,也就是每秒传输图片的帧数,也可以理解为…

视频压缩基本介绍与标准

视频压缩基本介绍与标准 视频压缩又称视频编码,所谓视频编码方式就是指通过特定的压缩技术,将某个视频格式的文件转换成另一种视频格式文件的方式。 一般的通用数据压缩方案如下图: 压缩就是一个传播的过程,所以在压缩与解压缩…

vr视频六面体变换

本文会对facebook的开源filter:vf_transform.c 做代码级分析,解释vr视频是如何做六面体转换的。 转换的关键其实就是输入vr视频到六面体的映射(也就是下图中蓝色图像映射到红色图像): 假设每个正方形的像素是512x512个…

学生角度看傅里叶变换,拉普拉斯变换,z变换(一)

学生角度看傅里叶变换,拉普拉斯变换,z变换(一) 学生角度看傅里叶变换,拉普拉斯变换,z变换(一) 离散和连续信号的表示 信号的脉冲表示响应与卷积 傅里叶变换 为什么需要傅里叶变换ejw…

树莓派GPIO引脚介绍

在已经安装好GPIO库的情况下,命令行输出gpio readall即可列出IO引脚的定义和编号,显示如下(替换为图) 编号规则可参考树莓派GPIO控制 Python配置GPIO安装参考: sudo apt-get install python-dev sudo apt-get insta…

树莓派GPIO引脚控制红绿灯与轰鸣器

参考: https://blog.csdn.net/weixin_44415639/article/details/114600919 https://blog.csdn.net/chenbo163/article/details/78716269 https://blog.51cto.com/u_11643026/4291142 这里文章用的编程引脚是物理引擎代号BOARD 1、GPIO控制包安装 参考&#xff1…

树莓派教程 : 树莓派各版本引脚定义

树莓派Zero W/WH(Raspberry Pi Zero W/WH) GPIO针脚定义

树莓派各版本引脚及参数

一、树莓派引脚 图片来源 注:本表格适用于各版本,并且兼容26Pin的树莓派B,树莓派B为26Pin,其引脚对应于上表的前26Pin。 二、树莓派参数 1.树莓派各版本比较: 2.树莓派3B参数 表格来源 名称参数备注SOC博通 BCM28…

树莓派4b 引脚图

树莓派 4B 详细资料

树莓派引脚远程控制

最近和树莓派群里朋友合作想做个开源RPILINK系统满足群里一些朋友的需求,虽然不少地方还不完善至少开了个头,先记录下来功能,以后有空会陆续会完善技术方面记录。 Rpilink系统是一个将树莓派设备连接到一起的数据平台,树莓派之间可以通过用户的配置进行数据交换和命令的执行…

树莓派4B的引脚控制简单demo

淘宝上的店家提供了很多示例程序,这里只拿一种来演示下,借此熟悉下如何用c或者python环境去操作树莓派。前提配置见我的之前的系列博客 树莓派4B基础软硬件环境搭建_jiugeshao的专栏-CSDN博客 树莓派4B上多版本python切换(一)_j…

树莓派的GPIO端口详解

首先上一张端口图 GPIO(GeneralPurposeI/OPorts)意思为通用输入/输出端口,通俗地说,就是一些引脚,可以通过它们输出高低电平或者通过它们读入引脚的状态-是高电平或是低电平。GPIO是个比较重要的概念,用户可…

树莓派管脚编码c语言,树莓派IO引脚定义 | 北岛夜话

我们使用树莓派的很重要一个原因是想通过它来进行一些智能控制,比如控制舵机、LED灯、继电器或者用来进行串口、SPI通信等,这些都需要通过控制树莓派的输入输出引脚(IO引脚)来实现的。前面的文章我们说过树莓派的输入输出引脚有26针和40针的区别,今天这篇文章我们具体看看它…

树莓派GPIO引脚详解

树莓派的GPIO引脚还是小有点复杂,有些是共用的。需要在启动时切换不同的设备树文件来设置不同的模式。详情参考: UART at Raspberry Pi GPIO PinoutRaspberry Pi UART pinshttps://pinout.xyz/pinout/uart

树莓派引脚编号说明

目前,树莓派引脚编号方式有三种: 1,基于引脚的物理位置进行编号; 2,C语言GPIO库wiringPi约定的编号方式; 3,Python语言GPIO库RPi.GPIO约定的编号方式。 如果我们要基于wiringPi库用C语言对树…

树莓派4b引脚图

感觉树莓派4B的引脚分布排序和树莓派3B的引脚排序差不太多,或者我需要的电源和串口应该是一样的。 转载自:树莓派4b引脚图_0欣欣0的博客-CSDN博客_树莓派4b引脚图

树莓派开发笔记(五):GPIO引脚介绍和GPIO的输入输出使用(驱动LED灯、检测按键)

若该文为原创文章,未经允许不得转载 原博主博客地址:https://blog.csdn.net/qq21497936 原博主博客导航:https://blog.csdn.net/qq21497936/article/details/102478062 本文章博客地址:https://blog.csdn.net/qq21497936/article/…

树莓派4B引脚定义及运行实例

一、树莓派引脚定义 学习如何用树莓派驱动LED灯时需要先了解树莓派管脚GPIO的编码方式,树莓派的管脚编码方式与51单片机的管脚编码有些不一样,在树莓派的GPIO上分为两种编码方式,分别为BCM和wiringPi编码,不同的编码方式在编写程…