オーディオとビデオを支える技術(6)- FFMPEGを使用して、ビデオを画像として保存する。

前回、FFMPEGを使用する環境を構築しました。
今回もその続きから始めていきます。

前回の記事
nakadasanda.hatenablog.jp

まず簡単な例として、FFMPEGを使用して、ビデオファイルを開き、デコードして、ビデオを画像として保存します。

*デコードとは:
元の容量よりも少ない容量で表現された圧縮データを、圧縮前のデータに復元する処理をデコードと呼びます。

今回の目標 f:id:nakadasanda1:20200630115158p:plain

プログラム開始

1.まず最初にエンコーダとデコーダを初期化する必要があります。次の関数を使います。

av_registter_all(); //FFMPEGを初期化します。

この関数を使用して、エンコーダとデコーダの初期化を完了です。
最初に呼び出しておきましょう。
ffmpeg4.0からなぜか、warningが出ますが、現在調査中です。
知っている方がいたら教えてください。

2.次にAVFormatContextを初期化します。FFMPEGのすべての操作は、このAVFormatContextを介して実行されます。

AVFormat* pFormatCtx =avformat_alloc_context();

3.ビデオを開きます。 ここでは、日本語のファイルを使わないでください。

char* file_path = "D:\\Amthem.mp4";
avformat_open_input(&pFormatCtx,file_path,NULL,NULL);

cout << "file open" << endl;

4.ファイルが開かれた後ファイル内のビデオストリームを検索します。

ストリームとは、ビデオや、オーディオなどを管理する構造の一つです。

    /// video タイプのストリームを見つけるまで、動画に含まれるストリーム情報をループします。
    /// それを記録しvideoStreamに保存します。
    ///ここでは、video ストリームだけを扱います。

    for(i=0;i < pFormatCtx->nb_streams;i++)
    {
        if(pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
        {
            videoStream = i;
        }
    }

    ///見つかりませんでした。
    if(videoStream == -1)
    {
        cout << "Don't find stream " << endl;
        return -1;
    }

5.次にデコーダーを開いて、ビデオストリームに従ってデコードします。

    //デコーダーを見つける。
    pCodecCtx = pFormatCtx->streams[videoStream]->codec;
    pCodec = avcodec_find_decoder(pCodecCtx->codec_id);

    if(pCodec == NULL)
    {
        cout << "not find codec " << endl;
    }

    cout << "codec find " << endl;

    //コーデックを開く
    if(avcodec_open2(pCodecCtx,pCodec,NULL)<0)
    {
        cout << "not open codec " << endl;
    }
    cout << "codec open " << endl;

見つかったビデオストリームに基づいて、デコーダーを直接取得できることがわかります。
実際に使用しているエンコーダーは、わかりません。(実際には、pCodecCtxの中にcodec_nameという価があるので、参照したらわかります。)
ビデオコーデックについて、毎回気にする必要がないためffmpegを使用しています。

6.それでは、ビデオを読み込みます。

    int y_size = pCodecCtx->width * pCodecCtx->height;

    packt = (AVPacket *) malloc(sizeof(AVPacket)); //パケットを割り当てる
    av_new_packet(packt,y_size); 。
   
    int index = 0;

    while (1) {
        if(av_read_frame(pFormatCtx,packt) < 0)
        {
            break;  //読み込み終わったらwhile()終了
        }

7.ビデオのデータは、圧縮されているので、デコードする必要があります。

pFrameは、AVFrame構造体でこの構造体は、デコードされた、オーディオまたは、ビデオのデータを保存します。 got_pictureには、decodeできない場合には、0が入り、それ以外には、使用されたバイト数が入ります。

if(packt -> stream_index == videoStream)
        {
            ret = avcodec_decode_video2(pCodecCtx,pFrame,&got_picture,packt);
            if(ret < 0)
            {
                cout << "decode error " << endl;
                return -1;
            }
        }

8.基本的にデコード後に取得されたすべての画像は、YUV420形式です。ここでは、画像ファイルとして保存するので、取得したYUV420データをRGB形式に変換する必要があります。

 if(got_picture){
                sws_scale(img_convert_ctx,
                          (uint8_t const *const *)pFrame->data,
                          pFrame->linesize,0,pCodecCtx->height,pFrameRGB->data,
                          pFrameRGB->linesize);
                SaveFrame(pFrameRGB,pCodecCtx->width,pCodecCtx->height,index++);
                if(index > 1000) return 0;   //1000回したら終了。
            }

sws_scaleは、連続する画像をカットし、結果として、得られた画像をdst画像に配置します。

9.RGBデータを取得した後、それを直接画像に保存します。

void SaveFrame(AVFrame *pFrame, int width, int height,int index)
{
    FILE *pFile;
    char Filename[32];
    int y;

    //Openfile
    sprintf(Filename,"frame%d.ppm",index);
    pFile = fopen(szFilename,"wb");

    if(pFile==NULL) return;

    //Write header
    fprintf(pFile,"P6\n%d %d\n255\n", width, height);

    //Write pixel data
    for(y=0;y<height;y++)
    {
        fwrite(pFrame->data[0]+y*pFrame->linesize[0],1,width*3,pFile);
    }

    //close file
    fclose(pFile);

}

fopenするところに画像が保存されるので、自分で正しい場所に、Filenameを書き換えるといいと思います。

実行すると画像が、1000枚保存されます。
手元に偶然あったデレステのMVをかけてみました。
1分の動画をかけてみると21GBのファイルサイズになり画像は、 60秒*60fps=3600枚できます。

f:id:nakadasanda1:20200630115158p:plain

ソースコードは、以下にあります。 github.com

シリーズ一覧 nakadasanda.hatenablog.jp