视频处理

读写操作

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:提取视频中的前景物体

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

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

一个解决方案是用滑动平均的方法动态的构建背景图
\mu_t = (1-\alpha)\mu_{t-1} + \alpha p_t

其中 \mu_tt 时刻估算的背景图,p_tt 时刻的新图,\alpha 为权重因子,\alpha=0 表示背景图保持不变,始终为初始背景图;\alpha=1 表示将当前图像作为背景图。在实际应用中一般采用比较小的值,例如 \alpha=0.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<cv::Point2f> points[2]; // 两个关键点坐标向量,分别存储前一时刻和当前时刻关键点的位置
std::vector<cv::Point2f> initial;  // 初始关键点位置,画图用
std::vector<cv::Point2f> features; // 初始化关键点,以及增补关键点
std::vector<uchar> status;  // 是否成功跟踪了关键点
std::vector<float> 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; i<points[1].size(); i++){
        if ((status[i]) && (abs(points[0][i].x - points[1][i].x) + abs(points[0][i].y - points[1][i].y) >2)){
            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 算法,它的基本思想如下:
首先假设前后帧中特征点的强度/灰度值没有变化,因此有
I_t(x,y) = I_{t+1}(x+u, y+v)

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

将上式右边的像素值是位置和时间的函数,本质上等价于 I(x+u, y+v, t+1) 这种函数形式,因此在 (x,y,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}

由于强度值没有变化,所以最后关系式为:
\frac{\partial I}{\partial x}u + \frac{\partial I}{\partial y}v = - \frac{\partial I}{\partial t}

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

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

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

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

tracking.gif

案例3:绘制光流场

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

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

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

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

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

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

cv::Ptr<cv::DualTVL1OpticalFlow> tvl1 = cv::createOptFlow_DualTVL1();

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

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

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

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<oflow.rows; y+=stride){
        for (int x=0; x<oflow.cols; x+=stride){
            cv::Point2f vector = oflow.at<cv::Point2f>(y,x);
            cv::line(flowImage, cv::Point(x,y), cv::Point(static_cast<int>(x+scale*vector.x+0.5), static_cast<int>(y+scale*vector.y+0.5)),  cv::Scalar(0,0,0));
            cv::circle(flowImage, cv::Point(static_cast<int>(x+scale*vector.x+0.5), static_cast<int>(y+scale*vector.y+0.5)), 1, cv::Scalar(0,0, 0),-1);
        }
    }
}

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

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


goose230.jpeg
goose237.jpeg

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


opticalFlow.png
最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,100评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,308评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,718评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,275评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,376评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,454评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,464评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,248评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,686评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,974评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,150评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,817评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,484评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,140评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,374评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,012评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,041评论 2 351

推荐阅读更多精彩内容