视频处理

读写操作

cv::VideoCapture capture("bike.avi");  // 生成 cv::VideoCapture 类的对象,并初始化
//如果在定义上述对象的时候没有初始化,也可以用对象的 open() 函数打开文件
// capture.open("bike.avi");

// 检查是否成功打开
if(!capture.isOpened()){ ... }

// 获取帧速率 FPS
double rate = capture.get(cv::CAP_PROP_FPS);

// 循环读取视频的每一帧
bool stop(false);
int delay = 1000/rate;
while (!stop){
    if (!capture.read(frame))  // 读完了所有的帧
        break;
    if (cv::waitKey(delay)>=0)
        stop = true;
}

// 关闭视频,不是必须的, cv::VideoCapture 类的解构函数中会自动调用它
capture.release();

上述 .get() 函数还可以获得很多信息:

  • cv::CAP_PROP_FRAME_COUNT :视频的总帧数
  • cv::CAP_PROP_POS_FRAMES:得到下一帧的序号
  • cv::CAP_PROP_FOURCC:编码器的四字符代码
  • cv::CAP_PROP_FRAME_WIDTH:图像的宽
  • cv::CAP_PROP_FRAME_HEIGH:图像的高

与之对应的,还有 .set() 函数,对视频进行某些设置。例如令视频跳转到某个位置:

double position = 50.0; 

capture.set(cv::CAP_PROP_POS_FRAMES, position); // 跳转到第 50 帧

capture.set(cv::CAP_PROP_POS_MSEC, time_place); // 以毫秒为单位的时间

capture.set(cv::CAP_PROP_AVI_RATIO, ratio_place); // 按比例指定,0.0 表示开头, 1.0 表示结束

需要注意的是,.get().set() 都是通用函数,为了涵盖尽可能多的参数获取和设置场景,它们将参数的数据类型统一设定为 double 。因此,有时需要进行数据类型转换,例如要获得 int 类型的 frame 序号,就需要将 .get() 的返回结果转成 int 型。

当将视频读取为一帧一帧的图片时,后续的处理操作与普通的静态图片完全相同。
处理之后,一帧一帧的图片如何保存成视频文件呢?
在读视频的时候用的是 cv::VideoCapture 类,在写的时候用 cv::VideoWriter 类。

cv::VideoWriter writer;

writer.open(const String &  filename, // 写入的文件名
            int fourcc,  // 视频编码,可以送入格式类似于 CV_FOURCC('X', 'V', 'I', 'D'),会自动转化为整型格式
            double fps, // 帧率
            cv::Size frameSize, // 画面大小
            bool isColor = true // 是否为彩色
);

// 设置好了写入格式之后,就可以将处理之后的 OutFrame 写入文件
writer.write(OutFrame);

案例1:提取视频中的前景物体

要区分前景与背景,基本思想是将那些改变很少的区域作为背景。
如果我们事先有了静态的背景图,则只需要跟当前图像对比,像素值不同的那些部分就是前景。
但这里有些问题:

  • 静态背景图不可知
  • 即使给定了背景图,由于光照等变化,像素值也会发生变化

一个解决方案是用滑动平均的方法动态的构建背景图

其中 为 时刻估算的背景图, 为 时刻的新图, 为权重因子, 表示背景图保持不变,始终为初始背景图; 表示将当前图像作为背景图。在实际应用中一般采用比较小的值,例如 ,维持背景的相对稳定性。

下边是提取前景物体的核心代码:

cv::VideoCapture capture("bike.avi"); // 生成视频读取对象,并初始化
if (!capture.isOpened())  // 确认成功生成了对象
    return 1;

double rate = capture.get(cv::CAP_PROP_FPS);
int delay = 1000/rate;  // 这两句计算恰当的帧延迟,保证观看效果

cv::Mat frame, gray, background, backImage, foreground, output;

while (capture.read(frame)){
    cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY);  // 转化成灰度图,容易对比
    if (background.empty())
        gray.convertTo(background, CV_32F);  // 初始时将第一帧画面作为背景图
    background.convertTo(backImage, CV_8U);  // 为方便与新的 frame 对比,转化成统一的 CV_8U

    cv::absdiff(backImage, gray, foreground); // 计算灰度差别
    cv::threshold(foreground, output, 25, 255, cv::THRESH_BINARY_INV); // 前景的灰度设置为 0

    cv::accumulateWeighted(gray, background, 0.01, output); // 用滑动平均的方式更新背景,前景部分不参与更新

    cv::imshow("foreground", output);
    cv::waitKey(delay);
}

本文使用的视频例子 ”bike.avi“ 来自 https://github.com/laganiere/OpenCV3Cookbook/blob/master/src/images/bike.avi

前景物体提取效果如下:


原视频.gif
提取前景动态物体.gif

案例2:跟踪目标

核心程序如下:

cv::Mat frame; // 存储当前读取的 frame
cv::Mat output; // frame 的 copy, 画图用
cv::Mat gray, gray_prev; // 当前 frame 和前一 frame 的灰度图

std::vector points[2]; // 两个关键点坐标向量,分别存储前一时刻和当前时刻关键点的位置
std::vector initial;  // 初始关键点位置,画图用
std::vector features; // 初始化关键点,以及增补关键点
std::vector status;  // 是否成功跟踪了关键点
std::vector err;  // 跟踪的误差量

while (capture.read(frame)){
    cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY);
    frame.copyTo(output);

    // 初始时刻添加关键点,如果在整个跟踪过程中,由于关键点跟踪丢失导致数目太少,也会增补
    // 这里用了 goodFeaturesToTrack 算法,这里返回的是关键点位置(Point2f),前边在特征点检测部分我们用过了它的相关算法来检测关键点
    if (points[0].size() <= 10){
        cv::goodFeaturesToTrack(gray, features, 500, 0.01, 10);
        points[0].insert(points[0].end(), features.begin(), features.end());
        initial.insert(initial.end(), features.begin(), features.end());
    }

    // 初始化前一个 frame
    if (gray_prev.empty()){
        gray.copyTo(gray_prev);
    }

    // 用 Lukas-Kanade 算法跟踪特征
    cv::calcOpticalFlowPyrLK(gray_prev, gray, points[0], points[1], status, err);

    // 只保留成功跟踪的、动态的特征
    int k = 0;       
    for (int i=0; i2)){
            initial[k] = initial[i];
            points[1][k++] = points[1][i];
        }
    }
    points[1].resize(k);
    initial.resize(k);

    // 画图,直观显示跟踪的过程
    for (int i=0; i< points[1].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);
    }

    std::swap(points[1], points[0]);
    cv::swap(gray_prev, gray);

    cv::imshow("result", output);
    cv::waitKey(delay);
}

这里在检测关键点时采用了 cv::goodFeatureToTrack方法。前边在特征检测时已经使用了与之相关的检测特征点的函数 cv::GFTTDetector

这里用的跟踪算法是 Lukas-Kanade 算法,它的基本思想如下:
首先假设前后帧中特征点的强度/灰度值没有变化,因此有

其中 为特征点的位移,也就是待求量。

将上式右边的像素值是位置和时间的函数,本质上等价于 这种函数形式,因此在 处使用泰勒展开得:

由于强度值没有变化,所以最后关系式为:

这就是光流约束方程(optical flow constraint eqaution)。

这个方程中只有 是未知数。

我们进一步假定关键点邻域中的像素具有同样的位移量,这样就可以得到若干方程,可以计算出均方误差意义下的最优解。

实例:继续使用上边的 "bike.avi" 例子。物体跟踪效果如下:

tracking.gif

案例3:绘制光流场

所谓光流就是图片中光亮模式的变化。

通过光流场可以粗略的判断运动场,即 3D 运动在 2D 平面上的投影。但也有例外,比如:

  • 在白墙前边移动相机,光流场近似为 0, 但运动场不为 0
  • 静止物体在不同光照下可能会产生非 0 的光流场,但其运动场为 0.

计算光流场主要基于两个假设:

  • 光流约束方程,反映了同一幅图片中像素变化规律
  • 光流向量的拉普拉斯算子,反映了相邻两帧图片中光流场变化的平滑性

OpenCV 提供了几种估算光流场的算法,这里采用 TVL-1 算法:

cv::Ptr tvl1 = cv::createOptFlow_DualTVL1();

cv::Mat oflow;
tvl1 -> calc(frame1, frame2, oflow);

计算得到的 oflowcv::Mat 类型的数据,其中每个元素又是一个二维向量,反映了两帧在 方向上的变化。

为了直观显示光流场,可以编写画图函数如下:

void drawOpticalFlow(const cv::Mat& oflow, cv::Mat& flowImage, int stride, float scale){
    if (flowImage.size() != oflow.size()){
        flowImage.create(oflow.size(), CV_8UC3);
        flowImage = cv::Vec3i(255, 255, 255);
    }
    for (int y=0; y(y,x);
            cv::line(flowImage, cv::Point(x,y), cv::Point(static_cast(x+scale*vector.x+0.5), static_cast(y+scale*vector.y+0.5)),  cv::Scalar(0,0,0));
            cv::circle(flowImage, cv::Point(static_cast(x+scale*vector.x+0.5), static_cast(y+scale*vector.y+0.5)), 1, cv::Scalar(0,0, 0),-1);
        }
    }
}

为了清晰显示,每隔 8 个像素画一次光流线条,长度缩放因子为 2。

我们采用如下的两帧图片:


goose230.jpeg
goose237.jpeg

用上述程序计算出光流场如下:


opticalFlow.png

你可能感兴趣的:(视频处理)