OpenCV视频生成

计算机视觉课的作业,挺有意思的,记录一下。

一 问题描述

对输入的一个彩色视频与3张以上照片,用OpenCV实现以下功能要求:
1. 将输入视频vi与多张图片pics处理成相同长宽后,合在一起生成一个视频vo;
2. 图片pics合成到视频中时需要编程实现图片切换效果,如幻灯片中的渐入、飞入等;
3. 在新视频中vo中需要完全编程实现一段片头,如编程绘制一个动图;
4. 最后以输入视频vi的两倍播放输出视频vo,并在视频底部打上含自己姓名的字幕。

二 实现过程

第1步:构造输出视频

首先我们需要确定最终输出视频的参数信息,包括视频的长宽、帧率、编码等信息,这里我们使用输入视频的参数来确定输出视频vo的参数信息。代码如下:

VideoCapture cap = VideoCapture("in.avi");
double fps = cap.get(CV_CAP_PROP_FPS);
Size size = Size(cap.get(CV_CAP_PROP_FRAME_WIDTH), cap.get(CV_CAP_PROP_FRAME_HEIGHT));
VideoWriter writer;
writer.open("out.avi", CV_FOURCC('M', 'J', 'P', 'G'), fps*2, size);

初始化VideoCapture实例读入输入视频”in.avi”,这里的输入视频是截取的蓝色星球2的前600帧制作的,然后使用VideoCapture的get方法分别获取视频流的帧率fps和图像大小size,有了这些参数信息我们就可以使用VideoWriter的open方法来构造输出视频了,“out.avi”指定了视频的文件名,CV_FOURCC(‘M’, ‘J’, ‘P’, ‘G’)指定了视频的编码格式,fps*2指定了视频的帧率(乘以2是因为输出视频需要以两倍原视频的速度播放,因此我们设置输出视频的帧率为输入视频帧率的两倍即可),size指定了视频的长宽。构造出一个空的视频之后,接下来的工作就只需要按要求将一个个的帧插入到视频中就可以了。输入视频:

第2步:制作片头

因为刚开始接触OpenCV,啥高大上的技术也不会,片头这部分我就是参考OpenCV的例子程序画个旋转的原子图了。在图中有一个不动的原子核和四个围绕原子核旋转的电子,简单起见四个电子旋转时投影在二维平面上的大小没有做变化,虽然这样做看上去也就没有什么立体的效果了(后期可以再优化一下)。有了想法之后实现起来就比较容易了,主要就是在电子的旋转路径上依次把电子画出来就好了,而电子的旋转路径在二维平面看上去也就是围绕在原子核周边的一个个椭圆了。具体代码如下所示:

void rotateAtom2(Size size) {
    cout << "rotateAtom2..." << endl;
    Point center;       // 原子的中心位置
    center.x = size.width / 2;
    center.y = size.height / 2;
    Size axes;          // 椭圆的半长轴和半短轴大小
    axes.width = min(size.width, size.height) / 4;
    axes.height = min(size.width, size.height) / 16;
    int radius = min(size.width, size.height) / 32;             // 原子核的半径大小
    int lineType = 8;
    for (int i = 0; i < 720; i++) {
        Mat img = Mat(size.height, size.width, CV_8UC3, Scalar(0, 0, 0));           // 初始化一个背景为黑色的帧
        circle(img, center, radius, Scalar(0, 0, 255), -1, lineType);               // 画原子核:在center位置画一个半径为radius的圆
        ellipse(img, center, axes, 0, 0, 360, Scalar(255, 0, 0), 1, lineType);      // 画四个电子的旋转路径:一个椭圆
        ellipse(img, center, axes, 45, 0, 360, Scalar(255, 0, 0), 1, lineType);
        ellipse(img, center, axes, 90, 0, 360, Scalar(255, 0, 0), 1, lineType);
        ellipse(img, center, axes, 135, 0, 360, Scalar(255, 0, 0), 1, lineType);
        Point p0(center.x + axes.width * cos(i*PI / 180), center.y + axes.height * sin(i*PI / 180));            // 电子0的位置
        //cout << "p0:x " << p0.x << ",y " << p0.y << endl;
        Point p = Point(center.x + axes.width * cos((i+30)*PI / 180), center.y + axes.height * sin((i+30)*PI / 180));       // 用来计算电子123位置的点(不同点的起始位置不同)
        Point p1((p.x - center.x)*cos(45 * PI / 180) - (p.y - center.y)*sin(45 * PI / 180) + center.x, (p.y - center.y)*cos(45 * PI / 180) + (p.x - center.x)*sin(45 * PI / 180) + center.y);       // 电子1的位置(p按center顺时针旋转45度后的位置)
        p = Point(center.x + axes.width * cos((i + 60)*PI / 180), center.y + axes.height * sin((i + 60)*PI / 180));
        Point p2((p.x - center.x)*cos(90 * PI / 180) - (p.y - center.y)*sin(90 * PI / 180) + center.x, (p.y - center.y)*cos(90 * PI / 180) + (p.x - center.x)*sin(90 * PI / 180) + center.y);
        p = Point(center.x + axes.width * cos((i + 90)*PI / 180), center.y + axes.height * sin((i + 90)*PI / 180));
        Point p3((p.x - center.x)*cos(135 * PI / 180) - (p.y - center.y)*sin(135 * PI / 180) + center.x, (p.y - center.y)*cos(135 * PI / 180) + (p.x - center.x)*sin(135 * PI / 180) + center.y);
        circle(img, p0, radius / 2, Scalar(0, 255, 0), -1, lineType);           // 按照电子的位置分别将电子画出来
        circle(img, p1, radius / 2, Scalar(0, 255, 0), -1, lineType);
        circle(img, p2, radius / 2, Scalar(0, 255, 0), -1, lineType);
        circle(img, p3, radius / 2, Scalar(0, 255, 0), -1, lineType);
        //waitKey(25);
        //imshow("in", img);
        drawImg(img);           // 将该帧写入到输出视频中
    }
}

稍微麻烦一点的是计算电子的位置,因为好久没碰数学了,还是在网上找的公式自己推了一遍。。。需要注意的是计算机视觉里的坐标与常用的坐标位置不太一样。最后做出来的效果还算凑活,不足之处是电子旋转的不是很流畅,一方面是因为帧数低,另一方面是因为轨迹的坐标有一点误差。
这里的drawImg()是把VideoWriter的write()封装了一下,在这个方法中多了一步将字幕写入到帧中的过程,后文会简单介绍。效果预览:
OpenCV视频生成_第1张图片

第3步:插入图片并实现切换效果

在做好片头之后,接下来我们来做图片切换。因为一开始对如何实现毫无头绪,所以看了一下别人的代码[3],总结出来切换效果其实就是按照一定规则将下张图片中的像素点画在上张图片上。这里我就简单实现了一种百叶窗载入效果和随机方块载入效果,代码如下:

void randTransition(Mat pic0, Mat pic1) {           // 随机方块载入
    cout << "randTransition..." << endl;
    int x_max = pic0.cols / 48, y_max = pic0.rows / 27;// 将图片分为x_max*y_max个方块,每个方块大小为27*48
    int cnt = x_max * y_max;                        
    int* a = new int[cnt];                          // a[i]用来存放第i次载入的方块位置
    for (int i = 0; i < cnt; i++) {
        a[i] = i;
    }
    shuffle(a, a + cnt, default_random_engine());   // 打乱载入顺序
    for (int r = 0; r < cnt; r++) {
        //cout << r << endl;
        int y = a[r] / x_max, x = a[r] % x_max;     // (x, y)为第r次载入方块的位置
        for (int i = y*27; i < (y+1)*27; i++) {     // 将pic1(x, y)处的方块写入到pic0中的相同位置
            uchar* data = pic0.ptr<uchar>(i);
            uchar* p1 = pic1.ptr<uchar>(i);
            for (int j = x*48; j < (x+1)*48; j++) {
                data[3 * j] = p1[3 * j];
                data[3 * j + 1] = p1[3 * j + 1];
                data[3 * j + 2] = p1[3 * j + 2];
            }
        }
        drawImg(pic0);                              // 将写入一个pic1方块的pic0写入到视频中
    }
}

void blindTransition(Mat pic0, Mat pic1) {          // 百叶窗载入
    cout << "blindTransition" << endl;
    int x_max = pic0.cols, y_max = pic0.rows;
    int y_step = y_max / 5;                         // 每次载入5行,每行相距y_step行
    for (int r = 0; r < y_step; r++) {              // 共需载入y_step次
        for (int y = r; y < y_max; y += y_step) {   // 载入5行pic1
            uchar* data = pic0.ptr<uchar>(y);       // 取pic0第y行像素点的首地址
            uchar* p1 = pic1.ptr<uchar>(y);         // 取pic1第y行像素点的首地址
            for (int x = 0; x < x_max; x++) {       // 将pic1第y行像素点依次写入到pic0的第y行中
                data[3 * x] = p1[3 * x];            // 因为是三通道
                data[x * 3 + 1] = p1[x * 3 + 1];
                data[x * 3 + 2] = p1[x * 3 + 2];
            }
        }
        for (int i = 0; i < 3; i++) {
            drawImg(pic0);                          // 将新写入5行pic1的pic0写入到视频中
        }
    }
}

需要注意的是,在将图片插入到视频之前,我们需要先将图片处理成与视频相同的长宽,这里可以使用OpenCV的resize()来完成这个工作,代码如下:

Mat toSize(Size size, Mat out) {
    Mat tmp(size, CV_8UC3);    // 构造一个空的输出图像
    resize(out, tmp, size);    // 将out调整到size大小输出到tmp中
    return tmp;
}

将图片处理成相同大小后,我们就可以把图片放到上述的blindTransition()中,完成插入图片和切换效果了。效果预览:

第4步:插入原视频vi

这一步应该是最简单的了,我们只需要把之前载入的输入视频cap的每一帧依次插入到输出视频中就可以了,代码如下:

while (1) {
    cap >> frame;
    if (frame.empty())
        break;
    //imshow("in", frame);
    drawImg(frame);
    //cvWaitKey(fps);
}

第5步:添加字幕

添加字幕也比较简单,直接用OpenCV的putText()就可以了,如果要添加汉字的话还需要一些额外的操作,我这里用的是freetype完成的,代码如下:

void drawImg(Mat img) {
    Cv310Text text("simkai.ttf");           // 读入字体文件
    Size textsize = getTextSize("姓名:jyakaranda(网易云)  学号:12345678  手机号:12345678", FONT_HERSHEY_PLAIN, 1, 1, 0);
    Point org((img.cols - textsize.width) / 2, (img.rows - textsize.height) / 8 * 7);   // 字幕位置
    text.putText(img, "姓名:jyakaranda(网易云)  学号:12345678  手机号:12345678", org, Scalar(255, 255, 255)); // 将字幕写入到帧中
    //waitKey(25);
    //imshow("out", img);
    writer << img;                          // 将写入字幕的帧写入到视频中
}

总结

虽然有些粗糙,但至此我们也算是完成了最开始的各种要求了:编程画了一个旋转的原子图作为视频片头,做了一个图片的随机载入和百叶窗载入效果,将输入视频插入到输出视频并在视频中添加中文字幕。学习了使用OpenCV对视频的基本io操作以及对图像的简单处理,过程还是比较简单有趣的。

参考资料:

  1. OpenCV Installation in Windows
  2. OpenCV入门教程之一
  3. OpenCV实现图片的时钟和中心圆形扩大效果
  4. OpenCV drawing1例子程序
  5. VS2010中OpenCV 显示汉字
  6. 学习OpenCV

源码及输入输出:http://download.csdn.net/download/u013794793/10156392

你可能感兴趣的:(学习,项目中的问题,opencv,视频,图片,编程)