基于opencv对高空拍摄视频消抖处理

news2024/11/24 19:29:38

一、问题背景

无人机在拍摄视频时,由于风向等影响因素,不可避免会出现位移和旋转,导致拍摄出的画面存在平移和旋转的帧间变换, 即“抖动” 抖动会改变目标物体 (车辆、行人) 的坐标,给后续的检测、跟踪任务引入额外误差,造成数据集不可用。

原效果
目标效果

理想的无抖动视频中,对应于真实世界同一位置的背景点在不同帧中的坐标应保持一致,从而使车辆、行人等目标物体的坐标变化只由物体本身的运动导致,而不包含相机的运动 抖动可以由不同帧中对应背景点的坐标变换来描述

二、量化指标

抖动可以用相邻帧之间的 x 方向平移像素 dx,y 方向平移像素 dy,旋转角度 da,缩放比例 s 来描述,分别绘制出 4 个折线图,根据折线图的走势可以判断抖动的程度 理想的无抖动视频中,dx、dy、da 几乎始终为 0,s 几乎始终为 1。

三、技术思路

我们最终实现,将视频的所有帧都对齐到第一帧,以达到视频消抖问题,实现逻辑如下图所示。

 (1)首先对视频进行抽第一帧与最后一帧,为什么抽取两帧?这样做的主要目的是,我们在做帧对齐时,使用帧中静态物的关键点做对齐,如果特征点来源于动态物上,那么对齐后就会产生形变,我们选取第一帧与最后一帧,提取特征点,留下交集部分,则可以得到静态特征点我们这里称为特征模板,然后将特征模板应用到每一帧上,这样可以做有效对齐。

(2)常用特征点检测器:

SIFT: 04 年提出,广泛应用于各种跟踪和识别算法,表现能力强,但计算复杂度高。

SURF: 06 年提出,是 SIFT 的演进版本,保持强表现能力的同时大大减少了计算量。

BRISK: BRIEF 的演进版本,压缩了特征的表示,提高了匹配速度。 ORB: 以速度著称,是 SURF 的演进版本,多用于实时应用。

GFTT: 最早提出的 Harris 角点的改进版本,经常合称为 Harris-Shi-Tomasi 角点。

SimpleBlob: 使用 blob 的概念来抽取图像中的特征点,相对于角点的一种创新。 FAST: 相比其他方法特征点数量最多,但也容易得到距离过近的点,需要经过 NMS。

Star: 最初用于视觉测距,后来也成为一种通用的特征点检测方法。

我们这里使用的是SURF特征点检测器

第一帧特特征点提取​​​​​​

最后一帧特征点提取

(3)在上图中,我们发现所提取的特征点中部分来自于车身,由于车是运动的,所以我们不能使用,我们用第一帧与最后一帧做静态特帧点匹配,生成静态特征模板,在下图中,我们发现只有所有的特征点只选取在静态物上

静态特征点模板

(4)静态特征模板匹配 ,我们这里使用Flann算法,匹配结果如下

特征匹配

(5)使用匹配成功的两组特征点,估计两帧之间的透视变换 (Perspective Transformation)。估计矩阵 H,其中 (x_i, y_i) 和 (x_i^′, y_i^′) 分别是两帧的特征点。

第一帧

最后一帧对齐到第一帧

四、实现代码

运行环境以及版本,安装命令如下:
python版本:3.X
opencv-python:3.4.2.16
opencv-contrib-python:3.4.2.16

需要卸载之前的opencv-python版本
pip uninstall opencv-python
pip uninstall opencv-contrib-python

安装新的版本
pip install opencv_python==3.4.2.16 
pip install opencv-contrib-python==3.4.2.16

代码基于python实现,如下所示:

import cv2
import numpy as np
from tqdm import tqdm
import argparse
import os

# get param
parser = argparse.ArgumentParser(description='')
parser.add_argument('-v', type=str, default='')  # 指定输入视频路径位置(参数必选)
parser.add_argument('-o', type=str, default='')  # 指定输出视频路径位置(参数必选)
parser.add_argument('-n', type=int, default=-1)  # 指定处理的帧数(参数可选), 不设置使用视频实际帧

# eg: python3 stable.py -v=video/01.mp4 -o=video/01_stable.mp4 -n=100 -p=6

args = parser.parse_args()

input_path = args.v
output_path = args.o
number = args.n

class Stable:
    # 处理视频文件路径
    __input_path = None

    __output_path = None

    __number = number

    # surf 特征提取
    __surf = {
        # surf算法
        'surf': None,
        # 提取的特征点
        'kp': None,
        # 描述符
        'des': None,
        # 过滤后的特征模板
        'template_kp': None
    }

    # capture
    __capture = {
        # 捕捉器
        'cap': None,
        # 视频大小
        'size': None,
        # 视频总帧
        'frame_count': None,
        # 视频帧率
        'fps': None,
        # 视频
        'video': None,
    }

    # 配置
    __config = {
        # 要保留的最佳特征的数量
        'key_point_count': 5000,
        # Flann特征匹配
        'index_params': dict(algorithm=0, trees=5),
        'search_params': dict(checks=50),
        'ratio': 0.5,
    }

    # 特征提取列表
    __surf_list = []

    def __init__(self):
        pass

    # 初始化capture
    def __init_capture(self):
        self.__capture['cap'] = cv2.VideoCapture(self.__video_path)
        self.__capture['size'] = (int(self.__capture['cap'].get(cv2.CAP_PROP_FRAME_WIDTH)),
                                  int(self.__capture['cap'].get(cv2.CAP_PROP_FRAME_HEIGHT)))

        self.__capture['fps'] = self.__capture['cap'].get(cv2.CAP_PROP_FPS)

        self.__capture['video'] = cv2.VideoWriter(self.__output_path, cv2.VideoWriter_fourcc(*"mp4v"),
                                                  self.__capture['fps'], self.__capture['size'])

        self.__capture['frame_count'] = int(self.__capture['cap'].get(cv2.CAP_PROP_FRAME_COUNT))

        if number == -1:
            self.__number = self.__capture['frame_count']
        else:
            self.__number = min(self.__number, self.__capture['frame_count'])

    # 初始化surf
    def __init_surf(self):

        self.__capture['cap'].set(cv2.CAP_PROP_POS_FRAMES, 0)
        state, first_frame = self.__capture['cap'].read()

        self.__capture['cap'].set(cv2.CAP_PROP_POS_FRAMES, self.__capture['frame_count'] - 1)
        state, last_frame = self.__capture['cap'].read()

        self.__surf['surf'] = cv2.xfeatures2d.SURF_create(self.__config['key_point_count'])

        self.__surf['kp'], self.__surf['des'] = self.__surf['surf'].detectAndCompute(first_frame, None)
        kp, des = self.__surf['surf'].detectAndCompute(last_frame, None)

        # 快速临近匹配
        flann = cv2.FlannBasedMatcher(self.__config['index_params'], self.__config['search_params'])
        matches = flann.knnMatch(self.__surf['des'], des, k=2)

        good_match = []
        for m, n in matches:
            if m.distance < self.__config['ratio'] * n.distance:
                good_match.append(m)

        self.__surf['template_kp'] = []
        for f in good_match:
            self.__surf['template_kp'].append(self.__surf['kp'][f.queryIdx])

    # 释放
    def __release(self):
        self.__capture['video'].release()
        self.__capture['cap'].release()

    # 处理
    def __process(self):

        current_frame = 1

        self.__capture['cap'].set(cv2.CAP_PROP_POS_FRAMES, 0)

        process_bar = tqdm(self.__number, position=current_frame)

        while current_frame <= self.__number:
            # 抽帧
            success, frame = self.__capture['cap'].read()

            if not success: return

            # 计算
            frame = self.detect_compute(frame)

            # 写帧
            self.__capture['video'].write(frame)

            current_frame += 1

            process_bar.update(1)

    # 视频稳像
    def stable(self, input_path, output_path, number):
        self.__video_path = input_path
        self.__output_path = output_path
        self.__number = number

        self.__init_capture()
        self.__init_surf()
        self.__process()
        self.__release()

    # 特征点提取
    def detect_compute(self, frame):

        frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

        # 计算特征点
        kp, des = self.__surf['surf'].detectAndCompute(frame_gray, None)

        # 快速临近匹配
        flann = cv2.FlannBasedMatcher(self.__config['index_params'], self.__config['search_params'])
        matches = flann.knnMatch(self.__surf['des'], des, k=2)

        # 计算单应性矩阵
        good_match = []
        for m, n in matches:
            if m.distance < self.__config['ratio'] * n.distance:
                good_match.append(m)

        # 特征模版过滤
        p1, p2 = [], []
        for f in good_match:
            if self.__surf['kp'][f.queryIdx] in self.__surf['template_kp']:
                p1.append(self.__surf['kp'][f.queryIdx].pt)
                p2.append(kp[f.trainIdx].pt)

        # 单应性矩阵
        H, _ = cv2.findHomography(np.float32(p2), np.float32(p1), cv2.RHO)

        # 透视变换
        output_frame = cv2.warpPerspective(frame, H, self.__capture['size'], borderMode=cv2.BORDER_REPLICATE)

        return output_frame


if __name__ == '__main__':

    if not os.path.exists(input_path):
        print(f'[ERROR] File "{input_path}" not found')
        exit(0)
    else:
        print(f'[INFO] Video "{input_path}" stable begin')

    s = Stable()
    s.stable(input_path, output_path, number)

    print('[INFO] Done.')
    exit(0)

参数说明:

-v    指定输入视频路径位置(参数必选)

-o    指定输出视频路径位置(参数必选)

-n    指定处理的帧数(参数可选), 不设置使用视频实际帧

调用示例:

python3 stable.py -v=test.mp4 -o=test_stable.mp4

五、效果展示

我们消抖后的视频道路完全没有晃动,但是在边界有马赛克一样的东西,那是因为图片对齐后后出现黑边,我们采用边缘点重复来弥补黑边。

消抖前

消抖后

 六、效率优化

目前的处理效率(原视频尺寸3840*2160),我们可以看出主要时间是花费在特征点(key)提取上。
可以采用异步处理+GPU提高计算效率

处理效率

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

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

相关文章

Html基础知识学习——圣杯布局、margin负值、等高布局(十七)

文章目录 圣杯布局margin负值等高布局 圣杯布局 两边页面固定中间页面宽度随着浏览器大小自适应 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-widt…

macOS - 安装 node、npm

文章目录 关于 node安装安装 node、npm 关于 node node.js 官网 : https://nodejs.org/engithub : https://github.com/nodejs Node.js is a free, open-sourced, cross-platform JavaScript run-time environment— that lets developers write command line tools and serv…

使用QTableWidget实现录像排程操作

一、介绍 该录像排程&#xff0c;是采用继承于QTableWidget的ScheduleTableWidget类进行实现。在ScheduleTableWidget实现类中&#xff0c;去除了Table原本的横向表头和纵向表头&#xff0c;分别采用第一行和第一列构成新的表头。新的横向表头代表一天24小时&#xff0c;新的纵…

CMake基础入门

文章目录 一、一个目录下有一个源文件&#xff08;入门&#xff09;1.1 预定义的变量1.2 语法介绍cmake_minimum_requiredprojectsetmessageadd_executable 1.3 最基础的实例1.4 第一步优化&#xff1a;build目录实操流程 1.5 第二步优化&#xff1a;src目录1.5.1 实操流程1.5.…

用心做好一款堡垒机,升级版《新一代堡垒机建设指南》白皮书现已开放下载!

编者注&#xff1a;本文刊登在《新一代堡垒机建设指南》&#xff08;JumpServer v3.0发布纪念版&#xff09;序言部分&#xff0c;文章作者为JumpServer开源堡垒机项目创始人广宏伟。点击文章底部“阅读原文”链接&#xff0c;即可下载新版《新一代堡垒机建设指南》白皮书。 “…

Vue3 – 实现过渡动画

1 认识Vue的动画原理 2 动画中常见类的作用 3 animation动画实现 4 动画的常见属性设置 5 列表元素动画组实现 6 列表元素的移动动画 认识Vue的动画原理 利用transition标签和一系列的动画类来控制标签的动画效果。 transition标签的本质是帮你添加和删除动画类的。 trans…

SpringBoot整合EasyExcel实现读操作

&#x1f60a; 作者&#xff1a; 一恍过去 &#x1f496; 主页&#xff1a; https://blog.csdn.net/zhuocailing3390 &#x1f38a; 社区&#xff1a; Java技术栈交流 &#x1f389; 主题&#xff1a; SpringBoot整合EasyExcel实现读操作 ⏱️ 创作时间&#xff1a; 2023年…

linux中的sendmail发送邮件

Linux/UNIX 下的老牌邮件服务器。 Sendmail 作为一种免费的邮件服务器软件&#xff0c;已被广泛的应用于各种服务器中&#xff0c;它在稳定性、可移植性、及确保没有 bug 等方面具有一定的特色&#xff0c;且可以在网络中搜索到大量的使用资料。 一、邮件发送原理图 MUA&#x…

通过弹性算力支持与托管式机器学习服务,亚马逊云科技为客户提升技术竞争力

时下数字化浪潮中&#xff0c;生成式人工智能&#xff08;AIGC&#xff09;已成为企业实现业务增长和数字化转型的重要技术&#xff0c;为企业提供了重新定义和调优业务模式的机遇。越来越多的企业希望利用人工智能技术提升竞争力&#xff0c;应对复杂的商业环境和市场挑战&…

机器学习实战:Python基于Ridge岭回归进行正则化(十三)

文章目录 1.前言1.1 岭回归的介绍1.2 岭回归的应用 2.自定义数据集实战演示2.1 导入函数2.2 创建数据集2.3 alpha0、1、10、100的分别情况 3.Dushanbe_house数据集实战演示3.1 导入函数和数据3.2 剔除空值及可视化3.3 整理数据3.4 训练和测试数据集3.5 评估数据集 4.讨论 1.前言…

mysql更新关联字段问题

现象 ### 表结构 CREATE TABLE wjf_test_update_num (id bigint(20) NOT NULL AUTO_INCREMENT,num1 int(11) DEFAULT NULL,num2 int(11) DEFAULT NULL,PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT3 DEFAULT CHARSETutf8 |## 插入两行数据 insert into wjf_test_update_nu…

从源码理解Scala中函数reduceRight的计算过程

水善利万物而不争&#xff0c;处众人之所恶&#xff0c;故几于道&#x1f4a6; 以List集合为例&#xff0c;进行reduceRight()的计算过程分析&#xff0c;总体分为两部分&#xff0c;一部分是看最顶层特质的那个通用的reduceRight方法&#xff0c;另一部分是讲直接混入的特质的…

【Linux】分布式存储系统 Ceph

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 分布式存储系统 Ceph Ceph 概述1、Ceph 简介2、存储基础3、Ceph 优势4、Ceph 架构4、Ceph 核心组件5、OSD 存储后端6、Ceph 数据的存储过程7、Ceph 版本发行生命周期 Ceph 集…

Day13 02-Linux常用命令汇总

文章目录 第三章 Linux的常用命令【重要】3.1 命令格式的说明3.2 帮助命令3.2.1 man3.2.2 help 3.3 文件处理命令3.3.1 cd3.3.2 ls3.3.3 pwd3.3.4 mkdir3.3.5 touch3.3.6 echo3.3.7 cp3.3.8 mv3.3.9 rm3.3.10 vi编辑器3.3.11 ln 3.4 查看命令3.4.1 cat3.4.2 more3.4.3 head3.4.…

代码量、代码行数计算工具

cloc 极客命令行工具 下载地址&#xff1a;https://github.com/AlDanial/cloc 常用命令&#xff1a; // 文件夹的名称 cloc ./StaticAnalyzer/计算结果&#xff1a; 速度快&#xff0c;结果精准&#xff0c;十分实用&#xff01;

51单片机学习--数码管显示

首先实现静态数码管显示 SMG表示共阴极 &#xff0c;要让第三位显示数字6&#xff0c;需要LED6端口接0&#xff0c;即Y50&#xff0c;5的二进制为101&#xff0c;按P2_4 到 P2_2 的顺序接&#xff0c;则CBA分别接101 然后在数码管另一端&#xff0c;需要点亮的接上1&#xff…

亚信科技荣任「DBL电信行业工作组」副组长单位,AntDB数据库连年入选《中国数据库产品图谱》

日前&#xff0c;“2023可信数据库发展大会”在京圆满召开。亚信科技凭借自研的电信级核心交易数据库AntDB在通信行业15年的技术积累和行业贡献&#xff0c;成功当选为数据库应用创新实验室&#xff08;DBL&#xff09;电信行业工作组副组长单位。AntDB数据库连续两年入选《全球…

客户案例 | 数字化加速,金融企业实现3D打印式应用程序开发

关键发现&#xff1a; 客户痛点&#xff1a;传统开发周期长&#xff0c;流程复杂&#xff0c;难以满足杭银消金在企业快速发展过程中的应用开发需求&#xff1b;内部业务因为优先级不高&#xff0c;导致开发资源分配有限&#xff0c;更加迟滞了管理部门数字化转型的进度。 解决…

大规模新能源并网下火电机组深度调峰优化调度

1主要内容 程序主要参考《大规模新能源并网下火电机组深度调峰优化调度》&#xff0c;建立了深度调峰基础模型&#xff0c;采用IEEE30节点系统&#xff0c;通过直流潮流建模&#xff0c;以火电机组运行成本、开机成本等综合运行成本最低为目标函数&#xff0c;包括潮流约束、旋…

Spring 能解决所有循环依赖吗?

以下内容基于 Spring6.0.4。 看了上篇文章的小伙伴&#xff0c;对于 Spring 解决循环依赖的思路应该有一个大致了解了&#xff0c;今天我们再来看一看&#xff0c;按照上篇文章介绍的思路&#xff0c;有哪些循环依赖 Spring 处理不了。 严格来说&#xff0c;其实也不是解决不了…