OpenCV联合C++/Qt 学习笔记(二十四)----差值法检测移动物体、稠密光流法跟踪移动物体及稀疏光流法跟踪移动物体

一、差值法检测移动物体

1、差值法原理

  • 核心思想:

通过比较两帧或多帧图像之间的像素变化,提取发生运动的区域。

  • 基本思想:

当摄像机固定时:

  • 静止背景区域像素变化很小
  • 运动目标区域像素变化较大

因此:将当前帧与参考帧做差,若某区域差值较大,则认为该区域存在运动物体。

  • 两帧差分法:

最常见的方法:

设:当前帧:,前一帧:

则差分图像:

其中:

  • :差值结果
  • 差值越大,说明变化越明显
  • 二值化处理:

为了提取运动区域,需要设置阈值:

其中:

  • T:阈值
  • 1:运动区域
  • 0:背景区域
  • 形态学优化:

由于差分后容易产生噪声,需要进行:

  • 腐蚀(去小噪点)
  • 膨胀(填充目标)
  • 开运算
  • 闭运算

提高目标完整性。

  • 三帧差分法

两帧差分容易出现:

  • 目标空洞
  • 拖影
  • 不完整轮廓

因此引入三帧差分:

分别计算:

  • 当前帧与前一帧
  • 前一帧与前前帧

再对结果进行逻辑与运算:

Result = D1 AND D2

2、相关函数

cpp 复制代码
/* 用途:用于计算两幅图像之间对应像素的绝对差值。
       对于每个像素:dst(x,y)=|src1(x,y)-src2(x,y)| */
void cv::absdiff(InputArray src1, InputArray src2, OutputArray dst);
/*
src1:输入图像1,可以是灰度图像或彩色图像
src2:输入图像2,尺寸、通道数和数据类型必须与src1相同
dst:输出图像,保存两个输入图像对应像素的绝对差值结果
*/

3、示例代码

cpp 复制代码
    QString videoPath = QApplication::applicationDirPath() + "/Videos/box.mp4";
    cv::String s_videoPath = videoPath.toLocal8Bit().data();
    VideoCapture capture(s_videoPath);
    if (!capture.isOpened())
    {
        qDebug() << "视频文件加载失败, 视频文件路径: " << QString::fromStdString(s_videoPath);
        return;
    }
    /*输出视频相关信息*/
    int fps = capture.get(CAP_PROP_FPS);
    int width = capture.get(CAP_PROP_FRAME_WIDTH);
    int height = capture.get(CAP_PROP_FRAME_HEIGHT);
    int num_of_frames = capture.get(CAP_PROP_FRAME_COUNT);
    cout << "width: " << width << " height: " << height << " fps: " << fps << " num_of_frames: " << num_of_frames << endl;

    /*读取视频中第一帧图像作为前一帧图像,并进行灰度化*/
    Mat preFrame, preGray;
    capture.read(preFrame);
    cvtColor(preFrame, preGray, COLOR_BGR2GRAY);
    /*对图像进行高斯滤波,减少噪声干扰*/
    GaussianBlur(preGray, preGray, Size(0, 0), 15);

    Mat binary;
    Mat frame, gray;
    /*形态学操作的矩形模板*/
    Mat k = getStructuringElement(MORPH_RECT, Size(7, 7), Point(-1, -1));

    while (true)
    {
        /*视频中所有图像处理完后退出循环*/
        if (!capture.read(frame))
        {
            break;
        }
        /*对当前帧进行灰度化*/
        cvtColor(frame, gray, COLOR_BGR2GRAY);
        GaussianBlur(gray, gray, Size(0, 0), 15);

        /*计算当前帧与前一帧的差值的绝对值*/
        absdiff(gray, preGray, binary);
        /*对计算结果二值化并进行开运算,减少噪声的干扰*/
        threshold(binary, binary, 10, 255, THRESH_BINARY | THRESH_OTSU);
        morphologyEx(binary, binary, MORPH_OPEN, k);
        /*显示处理结果*/
        imshow("input", frame);
        imshow("result", binary);
        /*将当前帧变成前一帧,准备下一个循环,注释掉这行代码为固定背景*/
        //gray.copyTo(preGray);

        /*5毫秒延时判断是否退出程序,按ESC键退出*/
        char c = waitKey(5);
        if (c == 27)
        {
            break;
        }
    }
    waitKey(0);

二、稠密光流法跟踪移动物体

1、光流法原理介绍

三个前提假设:

  1. 同一个物体在图像中对应的像素亮度不变
  2. 要求两帧图像必须具有较小的运动
  3. 区域运动具有一致性

流光法公式表示:

泰勒展开:

消去

两边同时除以

令:,这里的u,v不再是像素坐标,而是x,y方向的移动速度

得到经典光流约束方程:

2、稠密光流法函数

cpp 复制代码
/* 用途:用于计算两帧图像之间的稠密光流(Dense Optical Flow)。
    该函数能够估计图像中每个像素点的运动方向与位移。
    Farneback光流法属于:稠密光流算法。
    即:"所有像素"都会计算运动向量 */
void cv::calcOpticalFlowFarneback( InputArray prev, InputArray next, 
                                    InputOutputArray flow,
                                    double pyr_scale, int levels, int winsize,
                                    int iterations, int poly_n, double poly_sigma,
                                    int flags );
/*
prev:前一帧图像,必须为CV_8UC1单通道灰度图像
next:当前帧图像,必须与prev具有相同尺寸和数据类型
flow:输出稠密光流结果,数据类型为CV_32FC2,每个像素包含:
        flow(x,y)[0]:X方向位移
        flow(x,y)[1]:Y方向位移
pyr_scale:图像金字塔缩放比例,一般取0.5,表示每层缩小为上一层的一半
levels:金字塔层数,层数越多,能检测更大运动
winsize:均值窗口大小,值越大越平滑,抗噪能力越强,但细节可能减少
iterations:每层金字塔迭代次数,次数越多精度越高,但计算速度变慢
poly_n:在每个像素中找到多项式展开的像素邻域大小,常用5或7
poly_sigma:高斯标准差,用于平滑导数
flags:算法控制标志,常见参数包括:
        OPTFLOW_USE_INITIAL_FLOW:使用输入flow作为初始光流
        OPTFLOW_FARNEBACK_GAUSSIAN:使用高斯窗口替代均值窗口
*/

3、示例代码

cpp 复制代码
    QString videoPath = QApplication::applicationDirPath() + "/Videos/box.mp4";
    cv::String s_videoPath = videoPath.toLocal8Bit().data();
    VideoCapture capture(s_videoPath);
    Mat preFrame, preGray;
    if (!capture.read(preFrame))
    {
        qDebug() << "视频文件加载失败, 视频文件路径: " << QString::fromStdString(s_videoPath);
        return;
    }
    /*将彩色图像转换成灰度图像*/
    cvtColor(preFrame, preGray, COLOR_BGR2GRAY);
    while (true)
    {
        Mat nextFrame, nextGray;
        /*所有图像处理完成后退出程序*/
        if (!capture.read(nextFrame))
        {
            break;
        }
        imshow("nextFrame", nextFrame);

        cvtColor(nextFrame, nextGray, COLOR_BGR2GRAY);
        /*计算稠密光流*/
        Mat_<Point2f> flow;/*两个方向的运动速度*/
        calcOpticalFlowFarneback(preGray, nextGray, flow, 0.5, 3, 15, 3, 5, 1.2, 0);

        Mat xV = Mat::zeros(preFrame.size(), CV_32FC1);/*x方向移动速度*/
        Mat yV = Mat::zeros(preFrame.size(), CV_32FC1);/*y方向移动速度*/
        /*提取两个方向的速度*/
        for (int row = 0; row < flow.rows; row++)
        {
            for (int col = 0; col < flow.cols; col++)
            {
                const Point2f& flow_xy = flow.at<Point2f>(row, col);
                xV.at<float>(row, col) = flow_xy.x;
                yV.at<float>(row, col) = flow_xy.y;
            }
        }
        /*计算向量角度和幅值*/
        Mat magnitude, angle;
        cartToPolar(xV, yV, magnitude, angle);
        /*将角度转换成角度制*/
        angle = angle * 180.0 / CV_PI / 2.0;
        /*把幅值归一化到0-255区间便于显示结果*/
        normalize(magnitude, magnitude, 0, 255, NORM_MINMAX);
        /*计算角度和幅值的绝对值*/
        convertScaleAbs(magnitude, magnitude);
        convertScaleAbs(angle, angle);
        /*将运动的幅值和角度生成HSV颜色空间的图像*/
        Mat HSV = Mat::zeros(preFrame.size(), preFrame.type());
        vector<Mat> result;
        split(HSV, result);
        result[0] = angle;/*决定颜色*/
        result[1] = Scalar(255);
        result[2] = magnitude;/*决定形态*/
        /*将三个多通道图像合并成三通道图像*/
        merge(result, HSV);
        /*将HSV颜色空间图像转换到RGB颜色空间中*/
        Mat rgbImg;
        cvtColor(HSV, rgbImg, COLOR_HSV2BGR);
        ///*显示检测结果*/
        imshow("rgbImg", rgbImg);
        char c = waitKey(5);
        if (c == 27)
        {
            break;
        }
    }
    waitKey(0);
    destroyAllWindows();

三、稀疏光流法跟踪移动物体

1、稀疏光流法函数

cpp 复制代码
/* 用途:用于计算两帧图像中特征点的稀疏光流(Sparse Optical Flow)。
   该函数基于:Lucas-Kanade(LK)光流法。
   与稠密光流不同:它只跟踪指定特征点的运动。 */
void cv::calcOpticalFlowPyrLK( InputArray prevImg, InputArray nextImg,  
        InputArray prevPts, InputOutputArray nextPts,
        OutputArray status, OutputArray err,
        Size winSize = Size(21,21), int maxLevel = 3,
        TermCriteria criteria = 
            TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 30, 0.01),
        int flags = 0, double minEigThreshold = 1e-4 );
/*
prevImg:前一帧图像,必须为单通道灰度图像
nextImg:当前帧图像,必须与prevImg具有相同尺寸和数据类型
prevPts:前一帧图像的稀疏光流点坐标,必须是单精度浮点数
nextPts:当前帧中与前一帧图像稀疏光流点匹配成功的稀疏光流点坐标,同样必须是单精度浮点数
status:输出状态标志,status[i]=1表示跟踪成功,status[i]=0表示跟踪失败
err:输出误差向量,向量每个元素都设置为对应点的误差,度量误差的标准可以在flags参数中设置
winSize:搜索窗口大小,窗口越大,抗噪能力越强,但计算速度降低
maxLevel:金字塔最大层数,层数越高,能处理更大位移运动
criteria:迭代终止条件,包括:最大迭代次数与误差精度
flags:算法控制标志,常见参数包括:
        OPTFLOW_USE_INITIAL_FLOW:使用nextPts作为初始值
        OPTFLOW_LK_GET_MIN_EIGENVALS:返回最小特征值误差
minEigThreshold:最小特征值阈值,用于过滤不稳定特征点
*/

2、示例代码

cpp 复制代码
vector<Scalar> color_lut;/*颜色查找表*/
void draw_lines(Mat& image, vector<Point2f> pt1, vector<Point2f> pt2)
{
    RNG rng(5000);
    if (color_lut.size() < pt1.size())
    {
        for (size_t t = 0; t < pt1.size(); t++)
        {
            color_lut.push_back(Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)));
        }
    }
    for (size_t t = 0; t < pt1.size(); t++)
    {
        line(image, pt1[t], pt2[t], color_lut[t], 2, 8, 0);
    }
}
/************************************************************************************/
    QString videoPath = QApplication::applicationDirPath() + "/Videos/box.mp4";
    cv::String s_videoPath = videoPath.toLocal8Bit().data();
    VideoCapture capture(s_videoPath);
    Mat preFrame, preImg;
    if (!capture.read(preFrame))
    {
        qDebug() << "视频文件加载失败, 视频文件路径: " << QString::fromStdString(s_videoPath);
        return;
    }
    /*将彩色图像转换成灰度图像*/
    cvtColor(preFrame, preImg, COLOR_BGR2GRAY);

    /*角点检测相关参数设置*/
    vector<Point2f> Points;
    double qualityLevel = 0.01;
    int minDistance = 10;
    int blockSize = 3;
    bool useHarrisDetector = false;
    double k = 0.04;
    int Corners = 5000;
    /*角点检测*/
    goodFeaturesToTrack(preImg, Points, Corners, qualityLevel, minDistance, Mat(),
        blockSize, useHarrisDetector, k);
    /*稀疏光流检测相关参数设置*/
    vector<Point2f> prePts;/*前一帧图像角点坐标*/
    vector<Point2f> nextPts;/*当前帧图像角点坐标*/
    vector<uchar> status;/*角点检测到的状态*/
    vector<float> err;
    TermCriteria criteria = TermCriteria(TermCriteria::COUNT + TermCriteria::EPS, 30, 0.01);
    double derivlambda = 0.5;
    int flags = 0;
    /*初始状态的角点*/
    vector<Point2f> initPoints;
    initPoints.insert(initPoints.end(), Points.begin(), Points.end());
    /*前一帧图像中的角点坐标*/
    prePts.insert(prePts.end(), Points.begin(), Points.end());
    while (true)
    {
        Mat nextFrame, nextImg;
        if (!capture.read(nextFrame))
        {
            break;
        }
        imshow("nextFrame", nextFrame);

        /*光流跟踪*/
        cvtColor(nextFrame, nextImg, COLOR_BGR2GRAY);
        calcOpticalFlowPyrLK(preImg, nextImg, prePts, nextPts, status, err,
            Size(31, 31), 3, criteria, derivlambda, flags);
        /*判断角点是否移动,如果不移动就删除*/
        size_t i, k;
        for (i = k = 0; i < nextPts.size(); i++)
        {
            /*距离与状态测量*/
            double dist = abs(prePts[i].x - nextPts[i].x) + abs(prePts[i].y - nextPts[i].y);
            if (status[i] && dist > 2)
            {
                prePts[k] = prePts[i];
                initPoints[k] = initPoints[i];
                nextPts[k++] = nextPts[i];
                circle(nextFrame, nextPts[i], 3, Scalar(0, 255, 0), -1, 8);
            }
        }
        /*更新移动角点数目*/
        nextPts.resize(k);
        prePts.resize(k);
        initPoints.resize(k);
        /*绘制跟踪轨迹*/
        draw_lines(nextFrame, initPoints, nextPts);
        imshow("result", nextFrame);
        
        char c = waitKey(5);
        if (c == 27)
        {
            break;
        }
        /*更新角点坐标和前一帧图像*/
        std::swap(nextPts, prePts);
        nextImg.copyTo(preImg);
        /*如果角点数目少于30,就重新检测角点*/
        if (initPoints.size() < 30)
        {
            goodFeaturesToTrack(preImg, Points, Corners, qualityLevel, minDistance, Mat(),
                blockSize, useHarrisDetector, k);
            initPoints.insert(initPoints.end(), Points.begin(), Points.end());
            prePts.insert(prePts.end(), Points.begin(), Points.end());
            printf("total feature points : %d\n", prePts.size());
        }
    }
    waitKey(0);
    destroyAllWindows();
相关推荐
清平乐的技术专栏3 小时前
【FlinkSQL笔记】(一)什么是Flink SQL
笔记·sql·flink
坤坤藤椒牛肉面3 小时前
stm32学习1--新建工程
stm32·单片机·学习
半夜修仙3 小时前
Redis中Set数据类型的常见命令
java·数据库·redis·笔记·学习
郭老二3 小时前
【C++】RPC:远程程序调用
c++·rpc
承渊政道3 小时前
【贪心算法】(经典实战应用解析(六):整数替换、俄罗斯套娃信封问题、可被三整除的最⼤和、距离相等的条形码、重构字符串)
c++·算法·leetcode·贪心算法·排序算法·动态规划·哈希算法
宠..3 小时前
VS Code SSH 远程连接 Ubuntu 并实现快速运行(C/C++示例)
java·运维·c语言·开发语言·c++·ubuntu·ssh
Harm灬小海3 小时前
【云计算学习之路】学习Centos7系统:Linux进程管理
linux·运维·服务器·学习·云计算
持梦远方3 小时前
Nginx 静态资源挂载与前端部署实战笔记
linux·前端·笔记·nginx
闻缺陷则喜何志丹3 小时前
【图论 树 启发式合并】P7165 [COCI2020-2021#1] Papričice|普及+
c++·算法·启发式算法·图论··洛谷