当前位置: 首页 > news >正文

OpenCV防抖实践及代码解析笔记

视频防抖是指用于减少摄像机运动对最终视频的影响的一系列方法。摄像机的运动可以是平移(比如沿着x、y、z方向上的运动)或旋转(偏航、俯仰、翻滚)。
在这里插入图片描述
正如你在上面的图片中看到的,在欧几里得运动模型中,图像中的一个正方形可以转换为任何其他位置、大小或旋转不同的正方形。它比仿射变换和单应变换限制更严格,但对于运动稳定来说足够了,因为摄像机在视频连续帧之间的运动通常很小。

1. 识别抖动

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

特征跟踪首先需要识别出易跟踪的特征,光滑的区域不利于跟踪,而有很多角的纹理区域则比较好。OpenCV有一个快速的特征检测器goodFeaturesToTrack,可以检测最适合跟踪的特性。

我们在前一帧中找到好的特征,就可以使用Lucas-Kanade光流算法在下一帧中跟踪它们。它是利用OpenCV中的calcOpticalFlowPyrLK函数实现的。

接着估计运动,我们已经找到了特征在当前帧中的位置,并且我们已经知道了特征在前一帧中的位置。所以我们可以使用这两组点来找到映射前一个坐标系到当前坐标系的刚性(欧几里德)变换。这是使用函数estimateRigidTransform完成的。

    # 检测前一帧的特征点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)# 检查完整性assert prev_pts.shape == curr_pts.shape# 只过滤有效点idx = np.where(status == 1)[0]prev_pts = prev_pts[idx]curr_pts = curr_pts[idx]# 找到变换矩阵# 只适用于OpenCV-3或更少的版本吗# m = cv2.estimateRigidTransform(prev_pts, curr_pts, fullAffine=False)m, inlier = cv2.estimateAffine2D(prev_pts, curr_pts, )

2. 计算帧之间的平滑运动

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

轨迹计算,在这一步,我们将增加运动之间的帧来计算轨迹。我们的最终目标是平滑这条轨迹,可以很容易地使用numpy中的cumsum(累计和)来实现。

​计算平滑轨迹,我们计算了运动轨迹。所以我们有三条曲线来显示运动(x, y,和角度)如何随时间变化。

平滑任何曲线最简单的方法是使用移动平均滤波器(moving average filter)。顾名思义,移动平均过滤器将函数在某一点上的值替换为由窗口定义的其相邻函数的平均值。

def movingAverage(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_smootheddef smooth(trajectory):smoothed_trajectory = np.copy(trajectory)# 过滤x, y和角度曲线for i in range(3):smoothed_trajectory[:, i] = movingAverage(trajectory[:, i], radius=SMOOTHING_RADIUS)return smoothed_trajectory

计算平滑变换
到目前为止,我们已经得到了一个平滑的轨迹。在这一步,我们将使用平滑的轨迹来获得平滑的变换,可以应用到视频的帧来稳定它。

这是通过找到平滑轨迹和原始轨迹之间的差异,并将这些差异加回到原始的变换中来完成的。

# 使用累积变换和计算轨迹
trajectory = np.cumsum(transforms, axis=0)# 创建变量来存储平滑的轨迹
smoothed_trajectory = smooth(trajectory)# 计算smoothed_trajectory与trajectory的差值
difference = smoothed_trajectory - trajectory# 计算更新的转换数组
transforms_smooth = transforms + difference

3. 将平滑的摄像机运动应用到帧中

现在我们所需要做的就是循环帧并应用我们刚刚计算的变换。如果我们有一个指定为(x, y, θ \theta θ),的运动,对应的变换矩阵是:

T = [ c o s θ − s i n θ x s i n θ c o s θ y ] T=\begin{bmatrix} cos\theta & -sin \theta & x\\ sin \theta & cos \theta & y \end{bmatrix} T=[cosθsinθsinθcosθxy]

    # 从新的转换数组中提取转换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] = dxm[1, 2] = dy# 应用仿射包装到给定的框架frame_stabilized = cv2.warpAffine(frame, m, (w, h))# Fix border artifactsframe_stabilized = fixBorder(frame_stabilized)# 将框架写入文件frame_out = frame_stabilizedout.write(frame_out)

修复边界伪影
当我们稳定一个视频,我们可能会看到一些黑色的边界伪影。这是意料之中的,因为为了稳定视频,帧可能不得不缩小大小。我们可以通过将视频的中心缩小一小部分(例如4%)来缓解这个问题。

下面的fixBorder函数显示了实现。我们使用getRotationMatrix2D,因为它在不移动图像中心的情况下缩放和旋转图像。我们所需要做的就是调用这个函数时,旋转为0,缩放为1.04(也就是提升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

4. 实践代码

代码来自[1]

import numpy as np
import cv2def movingAverage(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_smootheddef smooth(trajectory):smoothed_trajectory = np.copy(trajectory)# 过滤x, y和角度曲线for i in range(3):smoothed_trajectory[:, i] = movingAverage(trajectory[:, i], radius=SMOOTHING_RADIUS)return smoothed_trajectorydef 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# 尺寸越大,视频越稳定,但对突然平移的反应越小
SMOOTHING_RADIUS = 50# 读取输入视频
cap = cv2.VideoCapture('video1.mp4')# 得到帧数
n_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
print(n_frames)
# exit()
# #我们的测试视频可能读错了1300帧之后的帧
# n_frames = 1300# 获取视频流的宽度和高度
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))# 获取每秒帧数(fps)
fps = cap.get(cv2.CAP_PROP_FPS)# 定义输出视频的编解码器
#fourcc = cv2.VideoWriter_fourcc(*'MJPG')
#fourcc = cv2.VideoWriter_fourcc('M','P','4','V')
fourcc = cv2.VideoWriter_fourcc(*'MP4V')
# 设置输出视频
out = cv2.VideoWriter('video1_1.mp4', fourcc, fps, (w, h))
#out = cv2.VideoWriter('video_out.avi', fourcc, fps, (2 * w, h)) # 2*w用于前后对比
# 读第一帧
_, prev = cap.read()# 将帧转换为灰度
prev_gray = cv2.cvtColor(prev, cv2.COLOR_BGR2GRAY)# 预定义转换numpy矩阵
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)# 检查完整性assert prev_pts.shape == curr_pts.shape# 只过滤有效点idx = np.where(status == 1)[0]prev_pts = prev_pts[idx]curr_pts = curr_pts[idx]# 找到变换矩阵# 只适用于OpenCV-3或更少的版本吗# m = cv2.estimateRigidTransform(prev_pts, curr_pts, fullAffine=False)m, inlier = cv2.estimateAffine2D(prev_pts, curr_pts, )# 提取traslationdx = m[0, 2]dy = m[1, 2]# 提取旋转角da = np.arctan2(m[1, 0], m[0, 0])# 存储转换transforms[i] = [dx, dy, da]# 移到下一帧prev_gray = curr_grayprint("Frame: " + str(i) + "/" + str(n_frames) +" -  Tracked points : " + str(len(prev_pts)))# 使用累积变换和计算轨迹
trajectory = np.cumsum(transforms, axis=0)# 创建变量来存储平滑的轨迹
smoothed_trajectory = smooth(trajectory)# 计算smoothed_trajectory与trajectory的差值
difference = smoothed_trajectory - trajectory# 计算更新的转换数组
transforms_smooth = transforms + difference# 将视频流重置为第一帧
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)# 写入n_frames-1转换后的帧
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] = dxm[1, 2] = dy# 应用仿射包装到给定的框架frame_stabilized = cv2.warpAffine(frame, m, (w, h))# Fix border artifactsframe_stabilized = fixBorder(frame_stabilized)# 将框架写入文件#frame_out = cv2.hconcat([frame, frame_stabilized]) # 合并前后对比视频frame_out = frame_stabilized# 如果图像太大,调整它的大小。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)# 发布视频
cap.release()
out.release()
# 关闭窗口
cv2.destroyAllWindows()

5. OepnCV相关知识点

常见视频编码参数

VideoWriter_fourcc()常见的编码参数
参数列表
cv2.VideoWriter_fourcc(‘M’, ‘P’, ‘4’, ‘V’)
MPEG-4编码 .mp4 可指定结果视频的大小
cv2.VideoWriter_fourcc(‘X’,‘2’,‘6’,‘4’)
MPEG-4编码 .mp4 可指定结果视频的大小
cv2.VideoWriter_fourcc(‘I’, ‘4’, ‘2’, ‘0’)
该参数是YUV编码类型,文件名后缀为.avi 广泛兼容,但会产生大文件
cv2.VideoWriter_fourcc(‘P’, ‘I’, ‘M’, ‘I’)
该参数是MPEG-1编码类型,文件名后缀为.avi
cv2.VideoWriter_fourcc(‘X’, ‘V’, ‘I’, ‘D’)
该参数是MPEG-4编码类型,文件名后缀为.avi,可指定结果视频的大小
cv2.VideoWriter_fourcc(‘T’, ‘H’, ‘E’, ‘O’)
该参数是Ogg Vorbis,文件名后缀为.ogv
cv2.VideoWriter_fourcc(‘F’, ‘L’, ‘V’, ‘1’)
该参数是Flash视频,文件名后缀为.flv

cv2.VideoWriter写入为空或打不开的解决方案

一定要用video.release()方法关闭文件。

参考:

[1]. 默凉. opencv-python 视频流光去抖、实时去抖. CSDN博客. 2022.10
[2]. AI算法与图像处理. OpenCV实现视频防抖技术. 知乎. 2020.09

相关文章:

OpenCV防抖实践及代码解析笔记

视频防抖是指用于减少摄像机运动对最终视频的影响的一系列方法。摄像机的运动可以是平移(比如沿着x、y、z方向上的运动)或旋转(偏航、俯仰、翻滚)。 正如你在上面的图片中看到的,在欧几里得运动模型中,图像…...

函数栈帧的创建与销毁剖析

目录 一、前言 二、基础知识介绍 2.1 寄存器介绍 2.2、汇编指令介绍 三、函数栈帧的创建销毁过程 3.1 调用main函数的函数 3.2 main函数开辟栈帧 3.3 在main函数中创建变量 3.4 调用Add函数前的准备 3.5 为Add函数开辟栈帧 3.6 在Add函数中创建变量并运算 3.7 Add函…...

性能测试-如何进行监控设计

监控设计步骤 首先,你要分析系统的架构。在知道架构中使用的组件之后,再针对每个组件进行监控。 其次,监控要有层次,要有步骤。先全局,后定向定量分析。 最后,通过分析全局、定向、分层的监控数据做分析…...

大数据List去重

概述 两个超大List集合去重,时间最短的方式去实现。 详细 MaxList模块主要是对Java集合大数据去重的相关介绍。 背景: 最近在项目中遇到了List集合中的数据要去重,大概一个2500万的数据,开始存储在List中,需要跟一个2万的List去…...

CentOS8.2重启网络

查看网络配置命令 # ip addr  # nmcli ens160: 已连接 到 ens160"VMware VMXNET3"ethernet (vmxnet3), 00:50:56:B6:34:84, 硬件, mtu 1500ip4 默认inet4 10.3.10.111/24route4 10.3.10.0/24route4 0.0.0.0/0inet6 fe80::250:56ff:feb6:3484/64route6 ff00::/8rou…...

2023年【G1工业锅炉司炉】考试题及G1工业锅炉司炉模拟考试

题库来源:安全生产模拟考试一点通公众号小程序 2023年G1工业锅炉司炉考试题为正在备考G1工业锅炉司炉操作证的学员准备的理论考试专题,每个月更新的G1工业锅炉司炉模拟考试祝您顺利通过G1工业锅炉司炉考试。 1、【多选题】TSGG0001-2012《锅炉安全技术监…...

观察者模式 行为型设计模式之七

1.定义 在GOF的《设计模式:可复用面向对象软件的基础》一书中对观察者模式是这样定义的:定义对象的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。当一个对象发生了变化&#xff0…...

数据结构与算法之堆: Leetcode 451. 根据字符出现频率排序 (Typescript版)

根据字符出现频率排序 https://leetcode.cn/problems/sort-characters-by-frequency/ 描述 给定一个字符串 s ,根据字符出现的 频率 对其进行 降序排序 。一个字符出现的 频率 是它出现在字符串中的次数。返回 已排序的字符串 。如果有多个答案,返回其…...

吃透底层:从路由到前缀树

前言 今天学到关于路由相关文章,发现动态路由中有一个很常见的实现方式是前缀树,很感兴趣这个算法,故进行记录。 前缀树 Trie(又被叫做字典树)可以看作是一个确定有限状态自动机,尽管边上的符号一般是隐含…...

SparkSQL外部数据源

1.简介 1.1 多数据源支持 Spark 支持以下六个核心数据源,同时 Spark 社区还提供了多达上百种数据源的读取方式,能够满足绝大部分使用场景。 - CSV - JSON - Parquet - ORC - JDBC/ODBC connections - Plain-text files 1.2 读数据格式 所有读取 API 遵循以下调用格式: // …...

林沛满-TCP 是如何避免被发送方分片的?

TCP 可以避免被发送方分片,是因为它主动把数据分成小段再交给网络层。最大的分段大小称为 MSS(Maximum Segment Size),它相当于把 MTU 刨去 IP头和 TCP 头之后的大小,所以一个 MSS 恰好能装进一个 MTU 中。 图4 图 4 …...

Java中的枚举是什么?

Java枚举详解 枚举(Enum)是Java编程语言中的一种特殊数据类型,它用于表示一组具名的常量。枚举提供了一种更加类型安全和易于理解的方式来表示常量值,使代码更加清晰和可维护。 为什么需要枚举? 在介绍Java枚举的具…...

java学习--day24(单例模式序列化Lambda表达式)

文章目录 回顾今天的内容1.单例模式2.序列化3.Lambda表达式3.1入门案例3.2lambda表达式语法格式3.2.1无参无返回值的形式3.2.2有参无返返回值的方法3.2.3无参有返回值3.2.4有参有返回值的 回顾 1.三种创建Class对象的形式Class.forName("")类.class对象.getCalss()字…...

从0开始学go第六天

方法一:gin获取querystring参数 package main//querystring import ("net/http""github.com/gin-gonic/gin" )func main() {r : gin.Default()r.GET("/web", func(c *gin.Context) {//获取浏览器那边发请求携带的query String参数//…...

unity设计模式——代理模式

Subject类,定义了Real Subject和Proxy的共用接口,这样就在任何使用Real Subject的地方都可以使用Proxy。 abstract class Subject : MonoBehaviour {public abstract void Request(); } RealSubject类,定义Proxy所代表的真实实体。 class R…...

SpringBoot 如何使用 Grafana 进行可视化监控

使用Spring Boot Sleuth进行分布式跟踪 在现代分布式应用程序中,跟踪请求和了解应用程序的性能是至关重要的。Spring Boot Sleuth是一个分布式跟踪解决方案,它可以帮助您在分布式系统中跟踪请求并分析性能问题。本文将介绍如何在Spring Boot应用程序中使…...

【Codeforces】 CF1762E Tree Sum

题目链接 CF方向 Luogu方向 题目解法 首先考虑 n n n 为奇数的情况无解,这个可以通过乘积矛盾简单证明 接下来考虑一个结论是:偶数个点的树的形态确定之后,只有恰好 1 1 1 种染色方案,即从叶子一层一层往上面染,…...

用《斗破苍穹》的视角打开C#委托2 委托链 / 泛型委托 / GetInvocationList

委托链 经过不懈地努力,我终于成为了斗师,并成功掌握了两种斗技——八极崩和焰分噬浪尺。于是,我琢磨着,能不能搞一套连招,直接把对方带走。 using System; using System.Collections.Generic; using System.Linq; u…...

唐老师讲电赛

dc-dc电源布局要点...

[ICCV-23] DeformToon3D: Deformable Neural Radiance Fields for 3D Toonification

pdf | code 将3D人脸风格化问题拆分为几何风格化与纹理风格化。提出StyleField,学习以风格/ID为控制信号的几何形变残差,实现几何风格化。通过对超分网络引入AdaIN,实现纹理风格化。由于没有修改3D GAN空间,因此可以便捷实现Edit…...

Zustand 状态管理库:极简而强大的解决方案

Zustand 是一个轻量级、快速和可扩展的状态管理库,特别适合 React 应用。它以简洁的 API 和高效的性能解决了 Redux 等状态管理方案中的繁琐问题。 核心优势对比 基本使用指南 1. 创建 Store // store.js import create from zustandconst useStore create((set)…...

【JVM】- 内存结构

引言 JVM:Java Virtual Machine 定义:Java虚拟机,Java二进制字节码的运行环境好处: 一次编写,到处运行自动内存管理,垃圾回收的功能数组下标越界检查(会抛异常,不会覆盖到其他代码…...

让回归模型不再被异常值“带跑偏“,MSE和Cauchy损失函数在噪声数据环境下的实战对比

在机器学习的回归分析中,损失函数的选择对模型性能具有决定性影响。均方误差(MSE)作为经典的损失函数,在处理干净数据时表现优异,但在面对包含异常值的噪声数据时,其对大误差的二次惩罚机制往往导致模型参数…...

算法:模拟

1.替换所有的问号 1576. 替换所有的问号 - 力扣(LeetCode) ​遍历字符串​:通过外层循环逐一检查每个字符。​遇到 ? 时处理​: 内层循环遍历小写字母(a 到 z)。对每个字母检查是否满足: ​与…...

Golang——7、包与接口详解

包与接口详解 1、Golang包详解1.1、Golang中包的定义和介绍1.2、Golang包管理工具go mod1.3、Golang中自定义包1.4、Golang中使用第三包1.5、init函数 2、接口详解2.1、接口的定义2.2、空接口2.3、类型断言2.4、结构体值接收者和指针接收者实现接口的区别2.5、一个结构体实现多…...

comfyui 工作流中 图生视频 如何增加视频的长度到5秒

comfyUI 工作流怎么可以生成更长的视频。除了硬件显存要求之外还有别的方法吗? 在ComfyUI中实现图生视频并延长到5秒,需要结合多个扩展和技巧。以下是完整解决方案: 核心工作流配置(24fps下5秒120帧) #mermaid-svg-yP…...

Windows电脑能装鸿蒙吗_Windows电脑体验鸿蒙电脑操作系统教程

鸿蒙电脑版操作系统来了,很多小伙伴想体验鸿蒙电脑版操作系统,可惜,鸿蒙系统并不支持你正在使用的传统的电脑来安装。不过可以通过可以使用华为官方提供的虚拟机,来体验大家心心念念的鸿蒙系统啦!注意:虚拟…...

大数据治理的常见方式

大数据治理的常见方式 大数据治理是确保数据质量、安全性和可用性的系统性方法,以下是几种常见的治理方式: 1. 数据质量管理 核心方法: 数据校验:建立数据校验规则(格式、范围、一致性等)数据清洗&…...

Java后端检查空条件查询

通过抛出运行异常&#xff1a;throw new RuntimeException("请输入查询条件&#xff01;");BranchWarehouseServiceImpl.java // 查询试剂交易&#xff08;入库/出库&#xff09;记录Overridepublic List<BranchWarehouseTransactions> queryForReagent(Branch…...

SQL进阶之旅 Day 22:批处理与游标优化

【SQL进阶之旅 Day 22】批处理与游标优化 文章简述&#xff08;300字左右&#xff09; 在数据库开发中&#xff0c;面对大量数据的处理任务时&#xff0c;单条SQL语句往往无法满足性能需求。本篇文章聚焦“批处理与游标优化”&#xff0c;深入探讨如何通过批量操作和游标技术提…...