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

news2024/11/26 23:37:03

视频防抖是指用于减少摄像机运动对最终视频的影响的一系列方法。摄像机的运动可以是平移(比如沿着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_smoothed

def 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] = dx
    m[1, 2] = dy

    # 应用仿射包装到给定的框架
    frame_stabilized = cv2.warpAffine(frame, m, (w, h))

    # Fix border artifacts
    frame_stabilized = fixBorder(frame_stabilized)

    # 将框架写入文件
    frame_out = frame_stabilized
    out.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 cv2


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_smoothed


def 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


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


# 尺寸越大,视频越稳定,但对突然平移的反应越小
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, )

    # 提取traslation
    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("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] = dx
    m[1, 2] = dy

    # 应用仿射包装到给定的框架
    frame_stabilized = cv2.warpAffine(frame, m, (w, h))

    # Fix border artifacts
    frame_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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1070533.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

分布式文件系统HDFS(林子雨慕课课程)

文章目录 3. 分布式文件系统HDFS3.1 分布式文件系统HDFS简介3.2 HDFS相关概念3.3 HDFS的体系结构3.4 HDFS的存储原理3.5 HDFS数据读写3.5.1 HDFS的读数据过程3.5.2 HDFS的写数据过程 3.6 HDFS编程实战 3. 分布式文件系统HDFS 3.1 分布式文件系统HDFS简介 HDFS就是解决海量数据…

4.方法操作实例变量 对象的行为

4.1 操作对象状态的方法 同一类型的每个对象能够有不同的方法行为,任一类的每个实例都带有相同的方法,但是方法可以根据实例变量的值来表现不同的行为。 play()会播放title值表示的歌曲,调用某个实例的play()可能会播放“Politik”而另一个会…

第三章 Android 开发从入门到实战--简单控件

文章目录 1.文本显示1.1设置文本的内容1.2设置文本字体大小1.3设置文本的颜色 2.视图基础2.1设置视图的宽高2.2设置视图的间距2.3设置视图的对齐方式 3.常用布局3.1线性布局LinearLayout3.2相对布局RelativeLayout3.3网格布局GridLayout3.4滚动视图ScrollView 4.按钮触控4.1But…

集线器、交换机、路由器是如何转发包的

集线器、交换机、路由器是如何转发包的 集线器交换机MAC地址表的维护 路由器路由表中的信息路由器的包接收操作查询路由表确定输出端口找不到匹配路由时选择默认路由包的有效期通过分片功能拆分大网络包路由器发送操作中的一些特点 参考文档 集线器 集线器是一层(物…

异常:找不到匹配的key exchange算法

目录 问题描述原因分析解决方案 问题描述 PC 操作系统:Windows 10 企业版 LTSC PC 异常软件:XshellPortable 4(Build 0127) PC 正常软件:PuTTY Release 0.74、MobaXterm_Personal_23.1 服务器操作系统:OpenEuler 22.03 (LTS-SP2)…

【数据结构-二叉树 九】【树的子结构】:树的子结构

废话不多说,喊一句号子鼓励自己:程序员永不失业,程序员走向架构!本篇Blog的主题是【子结构】,使用【二叉树】这个基本的数据结构来实现,这个高频题的站点是:CodeTop,筛选条件为&…

Qt单一应用实例判断

原本项目中使用QSharedMemory的方法来判断当前是否已存在运行的实例,但在MacOS上,当程序异常崩溃后,QSharedMemory没有被正常销毁,导致应用程序无法再次被打开。 对此,Qt assistant中有相关说明: 摘抄 qt-s…

Linux防火墙之firewalld

iptables与firewalld的联系 netfilter 位于Linux内核中的包过滤功能体系 称为Linux防火墙的“内核态” Firewalld/iptables CentOS7默认的管理防火墙规则的工具(Firewalld) 称为Linux防火墙的“用户态” iptables与firewalld的区别 iptables主要是基…

Spring的beanName生成器AnnotationBeanNameGenerator

博主介绍:✌全网粉丝4W,全栈开发工程师,从事多年软件开发,在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战,博主也曾写过优秀论文,查重率极低,在这方面有丰富的经验…

软件设计原则 1小时系列 (C++版)

文章目录 前言基本概念 Design Principles⭐单一职责原则(SRP) Single Responsibility PrincipleCode ⭐里氏替换原则(LSP) Liskov Substitution PrincipleCode ⭐开闭原则(OCP) Open Closed PrincipleCode ⭐依赖倒置原则(DIP) Dependency Inversion PrincipleCode ⭐接口隔离…

tailscale自建headscale和derp中继

tailscale自建headscale和derp中继 Tailscale 官方的 DERP 中继服务器全部在境外,在国内的网络环境中不一定能稳定连接,所以有必要建立自己的 DERP 服务器的。 准备工作: 需要有自己的云服务器,本示例为阿里云轻量服务器需要有…

Tasmota系统之外设配置

Tasmota系统之外设配置 🎈相关篇《ESP32/ESP8266在线刷写Sonoff Tasmota固件以及配置简要》🔖这里以ESP32配置DS18B20温度传感器和dht11温湿度传感器为例。 ✨如果想接特定型号的显示屏幕,需要下载指定的固件,目前官方所提供的固件…

剑指offer——JZ36 二叉搜索树与双向链表 解题思路与具体代码【C++】

一、题目描述与要求 二叉搜索树与双向链表_牛客题霸_牛客网 (nowcoder.com) 题目描述 输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。如下图所示 数据范围:输入二叉树的节点数 0≤n≤1000,二叉树中每个节点的值 0≤val≤10…

“首站告捷 完美收官” | 风丘-EVM ASIA 2023精彩锦集

2023年9月19-21日,风丘携手德国IPETRONIK首次亮相马来西亚-EVM ASIA 2023——该地区第一大电动汽车、移动、制造和汽车零部件展览会,为大家呈现了在汽车测试、车辆诊断领域里专业的研发测试工具及创新解决方案,吸引了众多客户驻足洽谈。 无法…

SpringBoot-黑马程序员-学习笔记(一)

8.pom文件中的parent 我们使用普通maven项目导入依赖时,通常需要在导入依赖的时候指定版本号,而springboot项目不需要指定版本号,会根据当前springboot的版本来下载对应的最稳定的依赖版本。 点开pom文件会看到这个: 继承了一个…

WebGoat 靶场 JWT tokens 四 五 七关通关教程

文章目录 webGoat靶场第 四 关 修改投票数第五关第七关 你购买书,让Tom用户付钱 webGoat靶场 越权漏洞 将webgoat-server-8.1.0.jar复制到kali虚拟机中 sudo java -jar webgoat-server-8.1.0.jar --server.port8888解释: java:这是用于执行…

WebKit Inside: CSS 样式表的解析

CSS 全称为层叠样式表(Cascading Style Sheet),用来定义 HTML 文件最终显示的外观。 为了理解 CSS 的加载与解析,需要对 CSS 样式表的组成,尤其是 CSS Selector 有所了解,相关部分可以参看这里。 HTML 文件里面引入 CSS 样式表有 …

开启AI大模型时代|「Transformer论文精读」

论文地址: https://arxiv.org/pdf/1706.03762v5.pdf 代码地址: https://github.com/tensorflow/tensor2tensor.git 首发:微信公众号「魔方AI空间」,欢迎关注~ 大家好,我是魔方君~~ 近年来,人工智能技术发展迅猛&#…

解锁C语言结构体的力量(进阶)

引言:结构体是C语言中的重要部分,也是通向数据结构的一把“钥匙”,之前我们在这篇文章:http://t.csdnimg.cn/fBkBI已经简单的介绍了结构体的基础知识,本篇我们来更进一步的学习结构体。 目录 结构体的内存对齐 结构体…

二维码是啥?

大家好,我是tony4geek。 今天说下二维码。二维码我们每天都在使用。本文将深入探讨二维码的识别原理,了解其背后的技术和算法,以及它是如何将编码的信息解析成可读的文本或链接的。 一、二维码的基本结构 在探讨二维码的识别原理之前&…