使用OpenCV实现视频去抖

整体步骤:

  1. 设置输入输出视频
  2. 寻找帧之间的移动:使用opencv的特征检测器,检测前一帧的特征,并使用Lucas-Kanade光流算法在下一帧跟踪这些特征,根据两组点,将前一个坐标系映射到当前坐标系完成刚性(欧几里得)变换,最后使用数组纪录帧之间的运动。
  3. 计算帧之间的平滑运动:根据第二步的帧运动,计算轨迹,并使用移动平均滤波器对轨迹进行平滑处理,同时记录平滑轨迹与原始轨迹的差异。
  4. 将平滑运动应用到帧中,修复边界伪影:将第三步得到的差异加到原始轨迹,并将视频中心缩放4%缓解黑色的边界伪影。

1. 设置视频的输入输出

import numpy as np
import cv2
import os def set_int_and_out_video(input_path: str, output_path: str, fps: int = None):
# 读取视频
cap = cv2.VideoCapture(input_path) # 获取视频宽高
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) # 定义视频编码方式
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
# MJPG是一种视频输出格式,MJPG格式输出的是将视频图像采用JPEG格式压缩后得到的视频帧,优点是帧率高(视频开启快,曝光快),缺点是影像有马赛克,并且需要解码器,会占用PC系统资源。MJPG视频帧直接保存成jpg文件即可用常见的图片查看工具打开。 # 获取视频帧率
if fps is None:
fps = int(cap.get(cv2.CAP_PROP_FPS)) # 设置输出视频
basename = os.path.basename(input_path).split('.')[0]
video_compete_name = basename + '_compete.mp4'
video_stabilized_name = basename + '_stabilized.mp4'
output_path1 = os.path.join(output_path, video_compete_name)
output_path2 = os.path.join(output_path, video_stabilized_name) out = cv2.VideoWriter(output_path1, fourcc, fps, (2*w, h)) # 作为原视频的对比视频
out2 = cv2.VideoWriter(output_path2, fourcc, fps, (w, h)) # 输出结果
return cap, out, out2

2. 找出帧之间的运动

def find_motion(cap) -> np.ndarray:
# 设置视频指针到第一帧
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
# 读取第一帧,并转为灰度图类型为np.ndarray
_, prev = cap.read()
prev_gray = cv2.cvtColor(prev, cv2.COLOR_BGR2GRAY) # 获取视频帧数
n_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) # 定义保存动作变化的数组
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) # 使用Lucas-Kanade光流算法在下一帧(当前帧)中跟踪前一帧的特征点
curr_pts, status, err = cv2.calcOpticalFlowPyrLK(prev_gray, curr_gray, prev_pts, None)
# status :输出状态向量(无符号字符); 如果找到相应特征的流,则向量的每个元素设置为1,否则设置为0 # 完整性检查,通常用在对输入数据的检查。匹配特征点大小
assert prev_pts.shape == curr_pts.shape # 根据calcOpticalFlowPyrLK的状态标志,过滤掉当前帧中,可能被下一帧另一对像遮挡的特征点
idx = np.where(status == 1)[0]
prev_pts = prev_pts[idx]
curr_pts = curr_pts[idx] # 找出动作变化矩阵:使用这两组点来找到映射前一个坐标系到当前坐标系的刚性(欧几里德)变换
m, inl = cv2.estimateAffinePartial2D(prev_pts, curr_pts) # 提取动作
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 # Move to next frame print("Frame: " + str(i) + "/" + str(n_frames) + " - Tracked points : " + str(len(prev_pts)))
return transforms

3. 计算帧之间的平滑动作

# 在上一步中,计算了运动轨迹,共有三条曲线来显示运动(x,y,和角度)如何随时间变化。
# 此处将平滑这三条曲线:定义了一个移动平均滤波器,它接受任何曲线作为输入,并返回曲线的平滑版本。
def movingAverage(curve, radius):
window_size = 2 * radius + 1
# 定义过滤器(卷积核)
f = np.ones(window_size) / window_size
# 在边界增加padding
curve_pad = np.lib.pad(curve, (radius, radius), 'edge')
# 进行卷积操作
curve_smoothed = np.convolve(curve_pad, f, mode='same')
# 去掉padding
curve_smoothed = curve_smoothed[radius:-radius]
# 得到平滑曲线
return curve_smoothed # 对轨迹的三部分即x, y和角度进行平滑处理
def smooth(trajectory, smoothing_radius:int = 50):
smoothed_trajectory = np.copy(trajectory) for ii in range(3):
smoothed_trajectory[:, ii] = movingAverage(trajectory[:, ii], radius=smoothing_radius)
return smoothed_trajectory def calculate_smooth_motion(transforms: np.ndarray):
# 通过累加计算轨迹
trajectory = np.cumsum(transforms, axis=0) # 计算平滑变化
smoothed_trajectory = smooth(trajectory)
# 计算平滑轨迹与轨迹的差
difference = smoothed_trajectory - trajectory # 计算新的运动数组
transforms_smooth = transforms + difference
return transforms_smooth

4. 将平滑动作应用到帧中

# 修复边界伪影
def fixBorder(frame):
s = frame.shape
# 在视频中心缩放4%
T = cv2.getRotationMatrix2D((s[1] / 2, s[0] / 2), 0, 1.04)
frame = cv2.warpAffine(frame, T, (s[1], s[0]))
return frame def apply_smooth_motion(cap, transforms_smooth: np.ndarray, out, out2) -> bool:
# 设置视频指针到第一帧
cap.set(cv2.CAP_PROP_POS_FRAMES, 0) # 获取视频帧数
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)) # 处理剩下 n_frames - 2帧
for i in range(n_frames - 2):
# Read next frame
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 = fixBorder(frame_stabilized) # frame为变换前,frame_stabilized为变换后,将两者拼接作为对比
frame_out = cv2.hconcat([frame, frame_stabilized]) # 拼接函数 # 视频过大时进行裁剪
if frame_out.shape[1] > 1920:
frame_out = cv2.resize(frame_out, (int(frame_out.shape[1] / 2), int(frame_out.shape[0] / 2))) # cv2.imshow("Before and After", frame_out)
# cv2.waitKey(10)
out.write(frame_out)
# 把稳定后的视频单独保存一份
out2.write(frame_stabilized)
return True

最后对视频进行集中处理

videos = os.listdir('./Video_Stabilization_data')
output_path = './video_result'
if os.path.exists(output_path) is False:
os.mkdir(output_path) for video in videos:
input_path = os.path.join('./Video_Stabilization_data', video)
print(f'{input_path} is processing')
cap, out, out2 = set_int_and_out_video(input_path, output_path, fps = 30)
transforms = find_motion(cap)
transforms_smooth = calculate_smooth_motion(transforms)
apply_smooth_motion(cap, transforms_smooth, out, out2)
cap.rele
out.release()
out2.release()
print(f'{input_path} completed')
# cv2.destroyAllWindows()

可以将视频转为GIF插入文档。

from moviepy.editor import VideoFileClip
def mp4_to_gif2(input_path, output_path):
clip = VideoFileClip(input_path)
clip.write_gif(output_path, fps=15)

受限于导出的GIF过大,博客不能上传MP4文件,最后的对比就不放了。

运行环境:

moviepy==1.0.3
numpy==1.26.2
opencv_contrib_python==4.6.0.66
opencv_python==4.6.0.66

参考文献

  1. OpenCV实现视频防抖技术
  2. opencv视频去抖
  3. Video Stabilization Using Point Feature Matching in OpenCV
  4. 【深度学习】2021 最新视频防抖论文+开源代码汇总

使用OpenCV实现视频去抖的更多相关文章

  1. 基于opencv将视频转化为字符串Java版

    基于opencv将视频转化为字符串Java版 opencv java  先上一个效果图吧 首先,弄清一下原理 我们要将视频转化为字符画,那么就需要获取画面的每一帧,也就是每一张图片,然后将图片进行转化 ...

  2. JavaScript 函数节流和函数去抖应用场景辨析

    概述 也是好久没更新 源码解读,看着房价蹭蹭暴涨,心里也是五味杂陈,对未来充满恐惧和迷茫 ...(敢问一句你们上岸了吗) 言归正传,今天要介绍的是 underscore 中两个重要的方法,函数节流和函 ...

  3. (转)JavaScript-性能优化之函数节流(throttle)与函数去抖(debounce)

     JavaScript-性能优化之函数节流(throttle)与函数去抖(debounce)         函数节流,简单地讲,就是让一个函数无法在很短的时间间隔内连续调用,只有当上一次函数执行后过 ...

  4. 使用opencv显示视频的方法

    下面对使用opencv显示视频做一个简单的记录.当然,网上这方面的资料已经数不胜数了,我只是将其简单记录,总结一下. 在opencv中显示视频主要有: (1)从本地读取视频和调用摄像头读取视频 (2) ...

  5. Opencv读取视频一闪而过情况分析

    在参加一个软件比赛需要用opencv对视频的处理,也碰到了一些问题. 最常见的就是视频一闪而过了,在网上查了好久都没解决, 最后重装在配置环境变量时发现的. 现在我来终结一下估计是比较全的了. 先说明 ...

  6. 浅析 JavaScript 的函数节流和去抖

    现代网页的实现上,会有很多交互上的优化,比如常见的 滚动加载 ,输入联想 等等.他们的实现思路很简单,以滚动加载而言,无非就是去是增加一个滚动的事件监听,每次滚动判断当前的元素是否已经滚动到了用户的可 ...

  7. JavaScript函数节流与函数去抖

    介绍 首先解释一下这两个概念: 函数节流(throttle):是让一个函数无法在很短的时间间隔内连续调用,当上一次函数执行后过了规定的时间间隔,才能进行下一次该函数的调用. 函数去抖(debounce ...

  8. JavaScript性能优化之函数节流(throttle)与函数去抖(debounce)

    函数节流,简单地讲,就是让一个函数无法在很短的时间间隔内连续调用,只有当上一次函数执行后过了你规定的时间间隔,才能进行下一次该函数的调用. 函数节流的原理挺简单的,估计大家都想到了,那就是定时器.当我 ...

  9. JS 函数节流和去抖

    1.什么是节流和去抖? 节流.就是拧紧水龙头让水少流一点,但是不是不让水流了.想象一下在现实生活中有时候我们需要接一桶水,接水的同时不想一直站在那等着,可能要离开一会去干一点别的事请,让水差不多流满一 ...

  10. JS的去抖、节流

    去抖(debounce) 在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时. //模拟一段ajax请求 function ajax(content) { console.log('aj ...

随机推荐

  1. vivo 全球商城:架构演进之路

    本文讲述 vivo 官方商城从单体应用到具备综合能力电商平台的演进,系统架构往服务化.中台化的变迁历程. 一.前言 vivo官方商城,是vivo官方的线上电商平台,主营vivo手机及专属配件.经过几年 ...

  2. 详解KubeEdge EdgeMesh v1.15 边缘CNI特性

    本文分享自华为云社区<KubeEdge EdgeMesh v1.15 边缘CNI特性原理及功能详解>,作者:达益鑫 |南开大学,刘家伟.吴锟 |DaoCloud,王杰章 |华为云 特性研发 ...

  3. 最全!即学即会 Serverless Devs 基础入门(下)

    作者 | 刘宇(阿里云 Serverless 产品经理) 在上篇<最全!即学即会 Serverless Devs 基础入门>中,我们阐述了工具链的重要性,并对安装方式 & 密钥配置 ...

  4. 深度揭秘阿里云 Serverless Kubernetes

    作者 | 陈晓宇(阿里云技术专家) 策划 | 褚杏娟 伴随着云原生的发展,从早先的单机版 Docker 到 Kubernetes 的编排领域的一统江湖,再到云上托管 Kubernetes,技术风雨变化 ...

  5. 一次完整的HTTP服务过程

  6. 基于 SpringBoot + magic-api + Vue3 + Element Plus + amis3.0 快速开发管理系统

    Tansci-Boot 基于 SpringBoot2 + magic-api + Vue3 + Element Plus + amis3.0 快速开发管理系统 Tansci-Boot 是一个前后端分离 ...

  7. Element UI Table合并行

    Vue使用Element-ui Table 合并行,官方只是一个非常简单的合并例子,通常业务都是相同的某个字段进行合并. 效果图 代码实现 1.Table <el-table :data=&qu ...

  8. freeswitch隐藏fs标识

    概述 freeswitch是一款好用的开源软交换平台. fs的默认配置环境下,sip消息中有很多freeswitch的标识,比如"user-agent","contact ...

  9. windows无法远程访问liunx的mysql解决方案(8.0.27版本)

    一.安装后mysql后发现windows上的无法正常访问,报错如下: 不管是navicat软件,还是使用python的pymsql进行连接 1.navicat软件如下:"Access den ...

  10. asp.net core之实时应用

    本文将介绍ASP.NET Core SignalR,这是一个强大的实时通信库,用于构建实时.双向通信应用程序.我们将探讨SignalR的基本概念.架构和工作原理,并提供一些示例代码来帮助读者更好地理解 ...