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的《设计模式:可复用面向对象软件的基础》一书中对观察者模式是这样定义的:定义对象的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。当一个对象发生了变化࿰…...
数据结构与算法之堆: 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…...
Prompt Tuning、P-Tuning、Prefix Tuning的区别
一、Prompt Tuning、P-Tuning、Prefix Tuning的区别 1. Prompt Tuning(提示调优) 核心思想:固定预训练模型参数,仅学习额外的连续提示向量(通常是嵌入层的一部分)。实现方式:在输入文本前添加可训练的连续向量(软提示),模型只更新这些提示参数。优势:参数量少(仅提…...
前端倒计时误差!
提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...
DAY 47
三、通道注意力 3.1 通道注意力的定义 # 新增:通道注意力模块(SE模块) class ChannelAttention(nn.Module):"""通道注意力模块(Squeeze-and-Excitation)"""def __init__(self, in_channels, reduction_rat…...
linux 错误码总结
1,错误码的概念与作用 在Linux系统中,错误码是系统调用或库函数在执行失败时返回的特定数值,用于指示具体的错误类型。这些错误码通过全局变量errno来存储和传递,errno由操作系统维护,保存最近一次发生的错误信息。值得注意的是,errno的值在每次系统调用或函数调用失败时…...
JVM暂停(Stop-The-World,STW)的原因分类及对应排查方案
JVM暂停(Stop-The-World,STW)的完整原因分类及对应排查方案,结合JVM运行机制和常见故障场景整理而成: 一、GC相关暂停 1. 安全点(Safepoint)阻塞 现象:JVM暂停但无GC日志,日志显示No GCs detected。原因:JVM等待所有线程进入安全点(如…...
Python Ovito统计金刚石结构数量
大家好,我是小马老师。 本文介绍python ovito方法统计金刚石结构的方法。 Ovito Identify diamond structure命令可以识别和统计金刚石结构,但是无法直接输出结构的变化情况。 本文使用python调用ovito包的方法,可以持续统计各步的金刚石结构,具体代码如下: from ovito…...
scikit-learn机器学习
# 同时添加如下代码, 这样每次环境(kernel)启动的时候只要运行下方代码即可: # Also add the following code, # so that every time the environment (kernel) starts, # just run the following code: import sys sys.path.append(/home/aistudio/external-libraries)机…...
ZYNQ学习记录FPGA(一)ZYNQ简介
一、知识准备 1.一些术语,缩写和概念: 1)ZYNQ全称:ZYNQ7000 All Pgrammable SoC 2)SoC:system on chips(片上系统),对比集成电路的SoB(system on board) 3)ARM:处理器…...
【Linux】Linux安装并配置RabbitMQ
目录 1. 安装 Erlang 2. 安装 RabbitMQ 2.1.添加 RabbitMQ 仓库 2.2.安装 RabbitMQ 3.配置 3.1.启动和管理服务 4. 访问管理界面 5.安装问题 6.修改密码 7.修改端口 7.1.找到文件 7.2.修改文件 1. 安装 Erlang 由于 RabbitMQ 是用 Erlang 编写的,需要先安…...
企业大模型服务合规指南:深度解析备案与登记制度
伴随AI技术的爆炸式发展,尤其是大模型(LLM)在各行各业的深度应用和整合,企业利用AI技术提升效率、创新服务的步伐不断加快。无论是像DeepSeek这样的前沿技术提供者,还是积极拥抱AI转型的传统企业,在面向公众…...
