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

小白学视觉

共 10666字,需浏览 22分钟

 · 2021-03-15

击上方小白学视觉”,选择加"星标"或“置顶

重磅干货,第一时间送达

本文转自 | AI算法与图像处理

一、问题背景

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

原效果

目标效果

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


二、量化指标

抖动可以用相邻帧之间的 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实现,如下所示

import cv2import timeimport numpy as npimport os  class Stable:    # 处理视频文件路径    __video_path = None     # 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,        'frame_count': 9999    }     # 当前处理帧数    __current_frame = 0     # 需要处理帧数    __handle_count = 0     # 处理时间    __handle_timer = {        'init': 0,        'handle': 0,        'read': 0,        'key': 0,        'matrix': 0,        'flann': 0,        'perspective': 0,        'write': 0,        'other': 0,    }     # 帧队列    __frame_queue = None     # 需要写入的帧队列    __write_frame_queue = None     # 特征提取列表    __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.__video_path.replace('.', '_stable.'),                                                  cv2.VideoWriter_fourcc(*"mp4v"),                                                  self.__capture['fps'],                                                  self.__capture['size'])         self.__capture['frame_count'] = int(self.__capture['cap'].get(cv2.CAP_PROP_FRAME_COUNT))         self.__handle_count = min(self.__config['frame_count'], self.__capture['frame_count'])     # 初始化surf    def __init_surf(self):         st = time.time()        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'] - 20)        state, last_frame = self.__capture['cap'].read()         self.__surf['surf'] = cv2.xfeatures2d.SURF_create(self.__config['key_point_count'], 1, 1, 1, 1)         # nfeatures:默认为0,要保留的最佳特征的数量。特征按其分数排名(在SIFT算法中按局部对比度排序)        # nOctaveLayers:默认为3,金字塔每组(Octave)有多少层。3是D. Lowe纸中使用的值。        # contrastThreshold:默认为0.04,对比度阈值,用于滤除半均匀(低对比度)区域中的弱特征。阈值越大,检测器产生的特征越少。        # edgeThreshold:默认为10,用来过滤边缘特征的阈值。注意,它的意思与contrastThreshold不同,edgeThreshold越大,滤出的特征越少(保留更多特征)。        # sigma:默认为1.6,高斯金字塔中的σ。如果使用带有软镜头的弱相机拍摄图像,则可能需要减少数量。         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])         self.__capture['cap'].set(cv2.CAP_PROP_POS_FRAMES, 0)         self.__handle_timer['init'] = int((time.time() - st) * 1000)         print("[INFO] init time:{}ms".format(self.__handle_timer['init']))     # 初始化 队列    def __init_data(self):        pass     # 初始化    def __init(self):        self.__init_capture()        self.__init_surf()        self.__init_data()     # 处理    def __process(self):         self.__current_frame = 1         while True:             if self.__current_frame > self.__handle_count:                break             start_time = time.time()             # 抽帧            success, frame = self.__capture['cap'].read()            self.__handle_timer['read'] = int((time.time() - start_time) * 1000)             if not success: return             # 计算            frame = self.detect_compute(frame)             # 写帧            st = time.time()            self.__capture['video'].write(frame)            self.__handle_timer['write'] = int((time.time() - st) * 1000)             self.__handle_timer['handle'] = int((time.time() - start_time) * 1000)             self.__current_frame += 1             self.print_handle_time()     # 视频稳像    def stable(self, path):        self.__video_path = path        self.__init()        self.__process()     # 打印耗时    def print_handle_time(self):        print(            "[INFO] handle frame:{}/{} time:{}ms(read:{}ms key:{}ms flann:{}ms matrix:{}ms perspective:{}ms write:{}ms)".                format(self.__current_frame,                       self.__handle_count,                       self.__handle_timer['handle'],                       self.__handle_timer['read'],                       self.__handle_timer['key'],                       self.__handle_timer['flann'],                       self.__handle_timer['matrix'],                       self.__handle_timer['perspective'],                       self.__handle_timer['write']))     # 特征点提取    def detect_compute(self, frame):         frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)         # 计算特征点        st = time.time()        kp, des = self.__surf['surf'].detectAndCompute(frame_gray, None)        self.__handle_timer['key'] = int((time.time() - st) * 1000)         # 快速临近匹配        st = time.time()        flann = cv2.FlannBasedMatcher(self.__config['index_params'], self.__config['search_params'])        matches = flann.knnMatch(self.__surf['des'], des, k=2)        self.__handle_timer['flann'] = int((time.time() - st) * 1000)         # 计算单应性矩阵        st = time.time()        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)        self.__handle_timer['matrix'] = int((time.time() - st) * 1000)         # 透视变换        st = time.time()        output_frame = cv2.warpPerspective(frame, H, self.__capture['size'], borderMode=cv2.BORDER_REPLICATE)        self.__handle_timer['perspective'] = int((time.time() - st) * 1000)         return output_frame  s = Stable() s.stable('video/test10.mov')

 五、效果展示

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

消抖前

消抖后

六、效率优化

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

处理效率

 七、存在问题

目前存在的问题 还不能完全消除视频中的所有抖动

(1)尤其是对于原来的抖动比较剧烈的视频,目前只能去除大部分明显抖动;

(2)由于画面旋转造成的边缘画面缺失,目前采取了复制边缘点 (replicate) 的操作,是否会对数据集的使用造成影响还需要进行实验。

改进思路

(1)对于抖动问题,计划通过调整关键点检测器参数、尽可能过滤掉运动物体的特征点、调整特征点匹配参数来解决;

(2)对于边缘画面缺失问题,可以使用基于 CNN 的图像修复算法,尽可能让缺失的边缘表现得更自然 后续进一步增加运动平滑等算法,实现对整体运动的进一步平滑。

下载1:OpenCV-Contrib扩展模块中文版教程
在「小白学视觉」公众号后台回复:扩展模块中文教程即可下载全网第一份OpenCV扩展模块教程中文版,涵盖扩展模块安装、SFM算法、立体视觉、目标跟踪、生物视觉、超分辨率处理等二十多章内容。

下载2:Python视觉实战项目52讲
小白学视觉公众号后台回复:Python视觉实战项目即可下载包括图像分割、口罩检测、车道线检测、车辆计数、添加眼线、车牌识别、字符识别、情绪检测、文本内容提取、面部识别等31个视觉实战项目,助力快速学校计算机视觉。

下载3:OpenCV实战项目20讲
小白学视觉公众号后台回复:OpenCV实战项目20讲即可下载含有20个基于OpenCV实现20个实战项目,实现OpenCV学习进阶。

交流群


欢迎加入公众号读者群一起和同行交流,目前有SLAM、三维视觉、传感器自动驾驶、计算摄影、检测、分割、识别、医学影像、GAN算法竞赛等微信群(以后会逐渐细分),请扫描下面微信号加群,备注:”昵称+学校/公司+研究方向“,例如:”张三 + 上海交大 + 视觉SLAM“。请按照格式备注,否则不予通过。添加成功后会根据研究方向邀请进入相关微信群。请勿在群内发送广告,否则会请出群,谢谢理解~


浏览 35
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报