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

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

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

相关文章

leetcode:190. 颠倒二进制位

一、题目: 函数原型: uint32_t reverseBits(uint32_t n) 解释:uint32是无符号int或short的别称,传入的参数是一个32位二进制串,返回值是该32位二进制串逆序后的十进制值 二、思路: 实际上并不需要真的去逆…

移动应用-Android-开发指南

Android-UI开发指南 Android Studio调试UI设计UI框架布局Layout文本框 android的活动Activity基本概念Activity的生命周期Activity栈创建Activity管理ActivityActivity间传递数据 FragmentAdapterRecyclerViewRecyclerView Adapter(适配器)事件setOnItem…

Can 通信-协议

概述 CAN 是 Controller Area Network 的缩写(以下称为 CAN),是 ISO国际标准化的串行通信协议。 在当前的汽车产业中,出于对安全性、舒适性、方便性、低公害、低成本的要求,各种各样的电子控制系统 被开发了出来。由于…

uniapp快速入门系列(3)- CSS技巧与布局

章节三:CSS技巧与布局 1. uniapp中的样式编写2. 常见布局技巧与实例解析2.1 水平居中布局2.2 垂直居中布局2.3 等高布局2.4 响应式布局 3. CSS动画与过渡效果 在uniapp中,我们使用CSS来设置页面的样式和布局。本章将介绍一些在uniapp中常用的CSS技巧和布…

IDEA的使用(一)代码模块的导入、快捷使用、自定义 (IntelliJ IDEA 2022.1.3版本)

目录 1. IDEA项目结构 2. 模块的导入操作 2.1 正规操作 2.2 取巧操作 2.3 出现乱码 2.4 模块改名 3. 代码模板的使用 后缀补全(Postfix Completion)、实时模板(Live Templates)菜单里面什么介绍都有,可以自学&a…

4.02 用户中心-上传头像功能开发

详细内容请看下面地址: 地址:http://www.gxcode.top/code

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

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

唐老师讲电赛

dc-dc电源布局要点

存档&改造【04】二维码操作入口设置细节自动刷新设置后的交互式网格内容的隐藏

因为数据库中没有数据无法查看设置效果,于是自己创建了个测试数据表,用来给demo测试 -- 二维码操作入口设置 create table JM_QR_CODE(QR_CODE_ID NUMBER generated as identity primary key,SYSTEM_ID NUMBER(20) not null,IS_ENAB…

iceberg简介004_iceberg和其他数据湖框架的对比---​​数据湖Apache Iceberg工作笔记0004

然后来看一下iceberg和其他数据湖框架的对比这里可以看到hudi支持的多一点对吧,但是 iceberg有自己的优势,并且他们都支持timeline 也就是时间旅行对吧. 然后这个图是显示了,数据湖三剑客的开源时间,以及火热程度,可以对比一下看看.

R语言R包详解——stringr包:字符处理

R语言 R语言R包详解——stringr包:字符处理 一切用法皆以说明书为准,想要了解该包,请多查阅说明书或者查看底层算法。 文章目录 R语言一、安装与加载R包二、函数简介三、函数详解3.1、str_c: 字符串拼接3.2、str_trim: 去掉字符串的空格和TA…

win1011安装MG-SOFT+MIB+Browser+v10b

文章目录 安装MG-SOFTSNMP服务配置安装MG-SOFT启动MIB-Browser以及错误解决MIB Browser使用 安装MG-SOFT win10和win11安装基本一样,所以参照下面的操作即可! SNMP服务配置 打开设置,应用和功能,可选功能,选择添加功…

java项目中git的.ignore文件设置

在Git中,ignore是用来指定Git应该忽略的故意不被追踪的文件。它并不影响已经被Git追踪的文件。我们可以通过.ignore文件在Git中指定要忽略的文件。 当我们执行git add命令时,Git会检查.gitignore文件,并自动忽略这些文件和目录。这样可以避免…

归纳所猜半结论推出完整结论:CF1592F1

https://www.luogu.com.cn/problem/CF1592F1 场上猜了个结论,感觉只会操作1。然后被样例1hack了。然后就猜如果 ( n , m ) (n,m) (n,m) 为1则翻转4操作,被#14hack了。然后就猜4操作只会进行一次,然后就不知道怎么做下去了。 上面猜的结论都…

计算机网络 面试题

PART1 1.TCP和UDP的区别是什么? 2.TCP报文首部格式是什么? 3.TCP三次握手的过程 4.为什么TCP要三次握手? 5.TCP三次握手的数据报可以携带数据吗? 6.半连接队列是什么? 7.SYN 洪泛攻击是什么? 8.TCP…

Http请求响应 Ajax 过滤器

10/10/2023 近期总结: 最近学的后端部署,web服务器运行,各种请求响应,内容很多,学的很乱,还是需要好好整理,前面JavaSE内容还没有完全掌握,再加上一边刷题,感觉压力很大哈…

Postman接口测试学习之常用断言

什么是断言? 断言——就是结果中的特定属性或值与预期做对比,如果一致,则用例通过,如果不一致,断言失败,用例失败。断言,是一个完整测试用例所不可或缺的一部分,没有断言的测试用例…

MAX30102心率血氧传感器

MAX30102心率血氧传感器介绍 背景基本功能基本结构基本原理采集方法直通式采集方法反射式采集方法 血氧采集原理Beer-Lambert 定理皮肤组织模型血氧测量过程AC / DC 的计算 心率采集原理 实验结果代码走读资源链接 背景 目前,基本上所有的可穿戴式设备都集成了心率…

【Java 进阶篇】CSS盒子模型详解

CSS盒子模型是网页布局的基础之一,它定义了HTML元素在页面上的占用空间和相互关系。理解CSS盒子模型对于构建各种类型的网页布局至关重要。在本文中,我们将深入探讨CSS盒子模型的各个方面,包括盒子模型的概念、属性和如何使用它们来控制元素的…