OpenCV实战(27)——追踪视频中的特征点

news2024/9/21 8:03:21

OpenCV实战(27)——追踪视频中的特征点

    • 0. 前言
    • 1. 追踪视频中的特征点
    • 2. 特征点追踪原理
    • 3. 完整代码
    • 小结
    • 系列链接

0. 前言

我们已经知道可以通过一些最独特的点来分析图像,对于图像序列同样如此,其中一些特征点的运动可用于了解捕获场景的不同元素如何移动。在本节中,我们将学习如何在特征点逐帧移动时通过跟踪特征点来执行序列的时间分析。

1. 追踪视频中的特征点

(1) 要开始追踪运动过程,首先要做的是检测初始帧中的特征点。然后尝试在之后的帧中跟踪这些点。

(2) 由于我们正在处理一个视频序列,因此找到特征点的对象很可能已经移动(这种移动也可能是由于相机移动造成的)。因此,必须围绕一个点的先前位置进行搜索,以便在下一帧中找到它的新位置,可以通过 cv::calcOpticalFlowPyrLK 函数完成此过程。也就是说,输入两个连续帧和一个特征点向量,函数可以返回特征点在新图像中的位置。要跟踪完整序列中的点,需要逐帧重复此过程。需要注意的是,当在整个序列中追踪点时,不可避免地会失去对其中一些点的跟踪,因此跟踪的特征点的数量将逐渐减少。因此,通常需要持续检测新的特征点。

(3) 我们继续利用在视频序列处理一节中定义的视频处理框架,我们将定义一个实现 FrameProcessor 接口的类。此类的数据属性包括执行特征点检测及其跟踪所需的变量:

class FeatureTracker : public FrameProcessor {
    cv::Mat gray;                       // 当前灰度图像
    cv::Mat gray_prev;                  // 前一帧灰度图像
    std::vector<cv::Point2f> points[2]; // 追踪特征从索引 0->1
    std::vector<cv::Point2f> initial;   // 追踪点的初始位置 
    std::vector<cv::Point2f> features;  // 特征探测
    int max_count;                      // 要检测的最大特征数 
    double qlevel;                      // 特征检测的质量水平 
    double minDist;                     // 两个特征点之间的最小距离 
    std::vector<uchar> status;          // 跟踪特征的状态
    std::vector<float> err;             // 追踪误差
    public:
        FeatureTracker() : max_count(500), qlevel(0.01), minDist(10.) {}

(4) 接下来,定义序列的每一帧需要调用的处理方法:

  • 首先,定义检测特征点
  • 接下来,跟踪这些点
  • 放弃无法跟踪或不需要再追踪的点
  • 最后,当前帧及其点成为下一次迭代时的上一帧及其点
        // 处理方法
        void process(cv:: Mat &frame, cv:: Mat &output) {
            // 转换为灰度图像
            cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY);
            frame.copyTo(output);
            // 1. 如果需要添加新特征
            if (addNewPoints()) {
                // 检测特征点
                detectFeaturePoints();
                // 将检测到的特征添加到当前追踪的特征
                points[0].insert(points[0].end(), features.begin(), features.end());
                initial.insert(initial.end(), features.begin(), features.end());
            }
            // 对于第一张图像
            if (gray_prev.empty()) gray.copyTo(gray_prev);
            // 追踪特征
            cv::calcOpticalFlowPyrLK(gray_prev, gray,   // 两张连续图像帧
                        points[0],                      // 第一帧输入点位置
                        points[1],                      // 第二帧输出点位置
                        status,                         // 追踪状态
                        err);                           // 追踪误差
            // 在跟踪的点上循环以丢弃无关对象 
            int k = 0;
            for (int i=0; i<points[1].size(); i++) {
                // 判断是否接受当前点
                if (acceptTrackedPoint(i)) {
                    initial[k] = initial[i];
                    points[1][k++] = points[1][i];
                }
            }
            // 消除丢弃掉的点
            points[1].resize(k);
            initial.resize(k);
            // 处理接受的点
            handleTrackedPoints(frame, output);
            // 将当前点和图像变为下一次处理时的前一帧点和图像
            std::swap(points[1], points[0]);
            cv::swap(gray_prev, gray);
        }

(5) 该算法使用了四种实用方法,我们可以改进这些方法中以便为跟踪器定义新的行为。第一种方法用于检测特征点:

        // 特征点检测
        void detectFeaturePoints() {
            // 检测特征
            cv::goodFeaturesToTrack(gray,   // 输入图像
                    features,               // 输出检测特征
                    max_count,              // 特征的最大数量
                    qlevel,                 // 质量级别
                    minDist);               // 两个特征间的最小距离
        }

(6) 第二种方法用于确定是否应该检测新的特征点:

        // 判断新点是否应该被添加
        bool addNewPoints() {
            return points[0].size() <= 10;
        }

(7) 第三种方法基于应用程序定义的标准移除一些跟踪点。我们移除不移动的点(除了那些无法被 cv::calcOpticalFlowPyrLK 函数跟踪的点),不移动的点被认为属于背景场景,因此对此不感兴趣:

        // 确定应接受哪个追踪点
        bool acceptTrackedPoint(int i) {
            return status[i] &&  // 如果无法追踪点 i,则该点状态为 false
                    (abs(points[0][i].x-points[1][i].x)+(abs(points[0][i].y-points[1][i].y))>2); // 如果点已经移动
        }

(8) 第四种方法通过用一条线将所有跟踪点连接到它们在当前帧上的初始位置(即它们第一次被检测到的位置),绘制所有跟踪点处理跟踪的特征点:

        // 处理当前追踪点
        void handleTrackedPoints(cv::Mat &frame, cv::Mat &output) {
            for (int i=0; i<points[i].size(); i++) {
                cv::line(output, initial[i], points[1][i], cv::Scalar(255, 255, 255));
                cv::circle(output, points[1][i], 3, cv::Scalar(255, 255, 255), -1);
            }
        }

(9) 编写 main 函数追踪视频序列中的特征点:

int main() {
    // 创建视频处理器实例
    VideoProcessor processor;
    // 创建特征追踪实例
    FeatureTracker tracker;
    // 打开视频文件
    processor.setInput("r3.mp4");
    processor.setOutput("test000.mp4");
    // 设置帧处理器
    processor.setFrameProcessor(&tracker);
    processor.displayOutput("Tracked Features");
    processor.setDelay(1000./processor.getFrameRate());
    processor.stopAtFrameNo(90);
    // 开始处理
    processor.run();
    cv::waitKey();
}

以上程序可以展示移动追踪特征随时间的演变。在示例视频中,假设相机是固定的:

开始视频帧
经历一段时间后,我们可以得到以下帧结果:

追踪结果

2. 特征点追踪原理

为了逐帧跟踪特征点,我们必须在后续帧中定位特征点的新位置。如果我们假设特征点的强度从一帧到下一帧都没有变化,那么寻找 ( u , v ) (u,v) (u,v) 的位移如下:
I t ( x , y ) = I t + 1 ( x + u , y + v ) I_t(x,y)=I_{t+1}(x+u,y+v) It(x,y)=It+1(x+u,y+v)
其中, I t I_t It I t + 1 I_{t+1} It+1 分别是当前帧和下一帧,适用于在两个相应时刻拍摄的图像中的较小位移。我们可以使用泰勒展开基于图像导数的方程来近似以上方程:
I t + 1 ( x + u , y + v ) ≈ I t ( x , y ) + ∂ I ∂ x u + ∂ I ∂ y v + ∂ I ∂ t I_{t+1}(x+u,y+v)\approx I_t(x,y)+\frac {\partial I}{\partial x}u+\frac {\partial I}{\partial y}v+\frac {\partial I}{\partial t} It+1(x+u,y+v)It(x,y)+xIu+yIv+tI
假设不考虑两个强度项的恒定强度假设,根据方程右侧的等式,可以得到以下结果:
∂ I ∂ x u + ∂ I ∂ y v = − ∂ I ∂ t \frac {\partial I}{\partial x}u+\frac {\partial I}{\partial y}v=-\frac {\partial I}{\partial t} xIu+yIv=tI
该约束是基本的光流 (optical flow) 约束方程,也称为亮度恒定方程 (brightness constancy equation)。
Lukas-Kanade 特征追踪算法利用了这种约束。除了使用这个约束,Lukas-Kanade 算法还假设特征点邻域内所有点的位移是相同的。因此,我们可以使用唯一的 ( u , v ) (u, v) (u,v) 未知位移对所有这些点施加光流约束。由此可以得到比未知数(两个)更多的方程,因此,我们可以求解该方程组。在实践中,可以通过迭代解决以上方程,OpenCV 还提供了以不同分辨率执行此估计的可能性,以便使搜索更有效并更能容忍更大的位移。默认情况下,图像级别数为 3,窗口大小为 15。当然,这些参数是可以改变的。我们还可以指定终止条件,该条件定义停止迭代搜索的条件。cv::calcOpticalFlowPyrLK 的第六个参数包含可用于评估追踪质量的残差均方误差。第五个参数包含二进制标志,用于指示跟踪的相应点是否是成功的。
以上描述给出了 Lukas-Kanade 追踪器背后的基本原理,实现中包含了其他优化和改进,使算法在计算大量特征点的位移时更有效。

3. 完整代码

头文件 (videoprocessor.h) 完整代码参考视频序列处理一节,头文件 (featuretracker.h) 完整代码如下所示:

#if !defined FTRACKER
#define FTRACKER

#include <string>
#include <vector>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/features2d.hpp>
#include <opencv2/video/tracking.hpp>

#include "videoprocessor.h"

class FeatureTracker : public FrameProcessor {
    cv::Mat gray;                       // 当前灰度图像
    cv::Mat gray_prev;                  // 前一帧灰度图像
    std::vector<cv::Point2f> points[2]; // 追踪特征从索引 0->1
    std::vector<cv::Point2f> initial;   // 追踪点的初始位置 
    std::vector<cv::Point2f> features;  // 特征探测
    int max_count;                      // 要检测的最大特征数 
    double qlevel;                      // 特征检测的质量水平 
    double minDist;                     // 两个特征点之间的最小距离 
    std::vector<uchar> status;          // 跟踪特征的状态
    std::vector<float> err;             // 追踪误差
    public:
        FeatureTracker() : max_count(500), qlevel(0.01), minDist(10.) {}
        // 处理方法
        void process(cv:: Mat &frame, cv:: Mat &output) {
            // 转换为灰度图像
            cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY);
            frame.copyTo(output);
            // 1. 如果需要添加新特征
            if (addNewPoints()) {
                // 检测特征点
                detectFeaturePoints();
                // 将检测到的特征添加到当前追踪的特征
                points[0].insert(points[0].end(), features.begin(), features.end());
                initial.insert(initial.end(), features.begin(), features.end());
            }
            // 对于第一张图像
            if (gray_prev.empty()) gray.copyTo(gray_prev);
            // 追踪特征
            cv::calcOpticalFlowPyrLK(gray_prev, gray,   // 两张连续图像帧
                        points[0],                      // 第一帧输入点位置
                        points[1],                      // 第二帧输出点位置
                        status,                         // 追踪状态
                        err);                           // 追踪误差
            // 在跟踪的点上循环以丢弃无关对象 
            int k = 0;
            for (int i=0; i<points[1].size(); i++) {
                // 判断是否接受当前点
                if (acceptTrackedPoint(i)) {
                    initial[k] = initial[i];
                    points[1][k++] = points[1][i];
                }
            }
            // 消除丢弃掉的点
            points[1].resize(k);
            initial.resize(k);
            // 处理接受的点
            handleTrackedPoints(frame, output);
            // 将当前点和图像变为下一次处理时的前一帧点和图像
            std::swap(points[1], points[0]);
            cv::swap(gray_prev, gray);
        }
        // 特征点检测
        void detectFeaturePoints() {
            // 检测特征
            cv::goodFeaturesToTrack(gray,   // 输入图像
                    features,               // 输出检测特征
                    max_count,              // 特征的最大数量
                    qlevel,                 // 质量级别
                    minDist);               // 两个特征间的最小距离
        }
        // 判断新点是否应该被添加
        bool addNewPoints() {
            return points[0].size() <= 10;
        }
        // 确定应接受哪个追踪点
        bool acceptTrackedPoint(int i) {
            return status[i] &&  // 如果无法追踪点 i,则该点状态为 false
                    (abs(points[0][i].x-points[1][i].x)+(abs(points[0][i].y-points[1][i].y))>2); // 如果点已经移动
        }
        // 处理当前追踪点
        void handleTrackedPoints(cv::Mat &frame, cv::Mat &output) {
            for (int i=0; i<points[i].size(); i++) {
                cv::line(output, initial[i], points[1][i], cv::Scalar(255, 255, 255));
                cv::circle(output, points[1][i], 3, cv::Scalar(255, 255, 255), -1);
            }
        }
};

#endif

主函数文件 (tracker.cpp) 完整代码如下所示:

#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/features2d.hpp>
#include <opencv2/video/tracking.hpp>

#include "featuretracker.h"

int main() {
    // 创建视频处理器实例
    VideoProcessor processor;
    // 创建特征追踪实例
    FeatureTracker tracker;
    // 打开视频文件
    processor.setInput("r3.mp4");
    processor.setOutput("test000.mp4");
    // 设置帧处理器
    processor.setFrameProcessor(&tracker);
    processor.displayOutput("Tracked Features");
    processor.setDelay(1000./processor.getFrameRate());
    processor.stopAtFrameNo(90);
    // 开始处理
    processor.run();
    cv::waitKey();
}

小结

视频特征点追踪,对于分析视频中不同重要元素的移动而言十分重要,在本节中,我们学习了如何在特征点逐帧移动时通过跟踪特征点来执行序列的时间分析。

系列链接

OpenCV实战(1)——OpenCV与图像处理基础
OpenCV实战(2)——OpenCV核心数据结构
OpenCV实战(3)——图像感兴趣区域
OpenCV实战(4)——像素操作
OpenCV实战(5)——图像运算详解
OpenCV实战(6)——OpenCV策略设计模式
OpenCV实战(7)——OpenCV色彩空间转换
OpenCV实战(8)——直方图详解
OpenCV实战(9)——基于反向投影直方图检测图像内容
OpenCV实战(10)——积分图像详解
OpenCV实战(11)——形态学变换详解
OpenCV实战(12)——图像滤波详解
OpenCV实战(13)——高通滤波器及其应用
OpenCV实战(14)——图像线条提取
OpenCV实战(15)——轮廓检测详解
OpenCV实战(16)——角点检测详解
OpenCV实战(17)——FAST特征点检测
OpenCV实战(18)——特征匹配
OpenCV实战(19)——特征描述符
OpenCV实战(20)——图像投影关系
OpenCV实战(21)——基于随机样本一致匹配图像
OpenCV实战(22)——单应性及其应用
OpenCV实战(23)——相机标定
OpenCV实战(24)——相机姿态估计
OpenCV实战(25)——3D场景重建
OpenCV实战(26)——视频序列处理

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

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

相关文章

机器视觉之表面缺陷检测

曾有一个科研项目&#xff0c;青岛啤酒厂委托&#xff0c;研究啤酒瓶的质量检测。 一般这种玻璃瓶会有一定次品、不良品&#xff0c;特征是&#xff0c;内部细细的裂缝纹路长度长于一定限制&#xff0c;那么这种啤酒瓶在运输或者开瓶盖的过程中就有炸裂的风险。 所以&#xf…

【实证分析】地区竞争、推广数字普惠金融与绿色经济效率

研究内容 基于考虑非期望产出的 Super-SBM 模型测算了 2011—2018 年中国 265 个地级市的绿色经济效率,综合运用面板模型、空间杜宾模型,系统分析了地区竞争下推广数字普惠金融对绿色经济效率的影响效应。研究发现:(1)绿色经济效率存在显著正向空间溢出效应,地理距离与经济差异…

mysql数据(添加,修改,删除,单表查询)操作

插入&#xff1a; 主键自增&#xff08;第一列&#xff09;&#xff0c;可以写null&#xff0c;自动生成 最后一列是空值null&#xff0c;即不给分配 如果设置了默认值&#xff0c;如性别设置了默认值男&#xff0c;就可以写default 1、 查询起别名&#xff0c;如果别名有特…

操作数据库DDL、DML

DDL 1、创建表 create table tb_user(id int,username varchar(20),password varchar(10) ) 2、数据类型 3、删除表 drop table 表名 4、修改表 修改表名 alter table 表名 rename to 新表名 添加一列 alter table 表名 add 列名 数据类型修改数据类型 …

Vue中watch与computed区别

<body><div id"root">姓&#xff1a;<input type"text" v-model"firstName"><br/><br/>名&#xff1a;<input type"text" v-model"lastName"><br/><br/>全名&#xff1a;&…

virtio-net前端-virtqueue

文章目录 1.概述2.数据结构3. 流程分析3.1 virtio总线创建3.3virtio-net3.3.1virtio-net初始化![在这里插入图片描述](https://img-blog.csdnimg.cn/7246c1705ac24f88b75fad63f8941ca5.png)3.3.2 virtio-net驱动发送3.3.3 Qemu virtio-net设备接收 4.virtqueue4.1数据结构4.2发…

模糊测试Fuzzing基础知识学习笔记

概念 模糊测试&#xff08;Fuzzing&#xff09;&#xff0c;是一种通过向目标系统提供非预期的输入并监视异常结果来发现软件漏洞的方法。在模糊测试中&#xff0c;用随机坏数据&#xff08;也称做 fuzz&#xff09;攻击一个程序&#xff0c;然后观察哪里遭到了破坏。 模糊测试…

6.Mysql自连接

针对相同的表进行的连接被称为“自连接”&#xff08;self join&#xff09; 那么为什么要把相同的一张表连接起来呢&#xff1f;一开始还是挺难理解的。把它想象成连接两张不同的表&#xff0c;这样容易理解一些。事实上&#xff0c;自连接还是有很多用处的 1. 在同一张表内进…

模板初识与STL简介

初识模板 引言函数模板定义实例化隐式实例化显式实例化 类模板定义实例化 STL简介STL六大组件 总结 引言 模板是泛型编程的基础 在之前我们介绍过函数重载&#xff0c;可以定义许多函数名相同参数列表不同的重载函数&#xff0c;来实现不同类型的相似操作。调用重载函数时会根…

chatgpt赋能python:归一化在PyTorch中的运用

归一化在PyTorch中的运用 PyTorch是一种广泛使用的深度学习框架&#xff0c;它可以用于训练和预测各种类型的神经网络。在深度学习中&#xff0c;归一化是提高模型性能的一种常用技术。归一化是指将输入数据进行标准化或规范化处理&#xff0c;以确保数据的统一性和一致性。在…

golang硬核技术(二)go程序从启动到运行到底经历了啥

前言 go相对其他语言&#xff0c;对并发的支持更友好。这使得他的设计和其他程序迥然不同。让我们来看看它都是如何初始化程序的&#xff0c;从程序加载到运行到底经历的什么。 我们继续之前的版本1.18.4 汇编入口 首先我们编译一个hello world 程序。 package main impor…

python--连接oracle数据库

python--连接oracle数据库 前言一、安装cx_Oracle二、导入库三、数据库操作实例3.1 连接3.2数据库查询3.3数据库插入3.4 实例从某网站上面爬取彩票号码 四、异常4.1、运行时&#xff0c;出现连接数据库失败:DatabaseError:DPI-1047解决连接失败问题1、查看Oracle的版本号2、进入…

辅助驾驶功能开发-功能规范篇(21)-1-XP行泊一体方案功能规范

XPilot Safety 主动安全系统 一、前向碰撞预防(Forward Collision Prevention) - 产品定义 基于车辆前向视觉模块和前毫米波雷达的感知能力,对车辆前方扇形区域内的车辆和VRU(弱势道路使用者) 有可能与本车的运行路线发生碰撞,进行预警、碰撞伤害降低或碰撞避免的一系列…

QT 简易视频播放器版本1.1

设计Qt界面实现播放、暂停、停止、下一集、上一集、快进、后退、倍速播放、进度调节&#xff0c;音量调节、视频播放列表等功能 先上演示效果&#xff1a; ui界面设计 videoplayer.h #ifndef VIDEOPLAYER_H #define VIDEOPLAYER_H#pragma execution_character_set("utf-…

深入了解Promise机制并使用JS实现一个Promise(一)

前言 关于为什么会有Promise以及Promise的一些用法和基本机制可以参考之前的文章JS中的异步与Promise使用整体来说&#xff0c;Promise可以帮助我们很好的解决异步的问题&#xff0c;号称是异步的终极解决方案。在浏览器中Promise是使用C实现的&#xff0c;今天就使用js来实现…

JSP原理以及基本语法

1、JSP原理 什么是JSP&#xff1f; Java Server Pages&#xff1a;Java服务器页面&#xff0c;和Servlet一样是动态Web技术&#xff01; 和HTML的区别&#xff1f; HTML是静态页面。在JSP页面中可以嵌入Java代码&#xff0c;为用户提供动态的数据。 JSP 和 Servlet 的关系…

枚举一个进程中的所有线程

在 Win32 开发中&#xff0c;如果需要获取程序运行过程中的一些较为底层的信息&#xff0c;你可能需要使用到 Tool Helper 库。但我愿意称之它为 Win32 中的 “害群之马”。何解&#xff1f; Tool Helper 库在 16 位 Windows 时代就已经存在了&#xff0c;这个库主要用来提供一…

今天给我的Ubuntu服务器挂在了一个4T的硬盘却只能识别到2T,原来是因为这!涨知识了

前言 今天买的4T机械硬盘到了&#xff0c;准备给我的服务器加装上&#xff0c;用来作为Nextcloud的存储硬盘。把硬盘安装好后就迫不及待的进行挂载&#xff0c;挂载的操作倒是挺顺利的&#xff0c;但是无论怎么操作Ubuntu系统识别到的大小居然都是2T&#xff0c;最后没办法&am…

chatgpt赋能python:开方在Python中的用法

开方在Python中的用法 开方是数学中常见的一种运算&#xff0c;用于求一个数的平方根。在Python中&#xff0c;开方运算可以通过使用math模块中的sqrt函数来实现。本文将介绍开方运算的概念、Python中的应用以及一些常见问题的解决方法。 开方的概念 开方是指&#xff0c;对…

chatgpt赋能python:在Python中运行程序的方法介绍

在Python中运行程序的方法介绍 Python是一种广泛使用的编程语言&#xff0c;也是人工智能和数据科学领域的首选。在这篇SEO文章中&#xff0c;我们将介绍Python中运行程序的几种方法。 1. 在Python环境中运行程序 Python环境是一个Python解释器及其标准库的集合。为了在Pyth…