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()封裝了一下,在這個方法中多了一步將字幕寫入到幀中的過程,後文會簡單介紹。效果預覽:
這裏寫圖片描述

第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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章