CodeZine(コードジン)

特集ページ一覧

AVI動画ファイルからBMPファイルを抽出する

Video for Windows APIによる動画ファイル制御プログラミング

  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加
2005/11/15 15:10

WindowsでAVIファイルから任意のフレームをビットマップイメージで抽出する制御方法を解説します。Video for Windows APIを学習すれば、例えば動画ファイル編集ソフトウェアのようなものを開発できるようになります。

図1 完成図(左:動画を表示するサンプルアプリケーション。右:抽出したビットマップ)
図1 完成図(左:動画を表示するサンプルアプリケーション。右:抽出したビットマップ)

はじめに

 マルチメディアファイルの制御は、たとえ未圧縮のデータ形式でも複雑なものです。音声ファイルやビットマップファイルを純粋な入出力APIだけを使って生成したり、データを解析しようと考えた場合は、音声やイメージに関する物理的な知識が必要になります。さらに、マルチメディアデータはすべてがバイナリで表現されるため、ファイル形式の仕様を熟知した上で、1バイト単位の複雑な入出力コードを書かなければなりません。

 幸い、多くの環境でこうした問題を解決するためのAPIが提供されています。例えばJavaの場合はJava Media Framework APIとしてメディアデータの制御用ライブラリが提供されています。Win32 APIやDirectXでも、同様にマルチメディアデータを制御するための様々な関数が提供されているため、これらを学習することでマルチメディアアプリケーションを開発することができます。

 Win32 APIでマルチメディアを扱う代表的な機能はMCI(Media Control Interface) でしょう。MCIを使えば、Windowsが対応している音声データや動画を自由に再生、停止、位置の移動などを行うことができ、例えばマルチメディアプレイヤーを開発することも可能です。しかし、こうした高水準APIは、再生や操作を専門としているため、マルチメディアデータの特定の部位の抽出や編集を行うには、より複雑な低水準APIを使わなければなりません。

 今回はVideo for Windows APIを使って、AVI動画ファイルから任意のフレームをビットマップとして抽出し、BMPファイルとして保存するアプリケーションを開発します。

対象読者

 本稿は、C言語の基本的な文法を理解し、Windows32 APIでWindowsクライアントアプリケーションの開発経験がある方を対象としています。ハンドルの概念、ウィンドウプロシージャ、メッセージの処理など、基本的なGUIプログラミングの仕組みについての解説は割愛します。

 また、AVI動画ファイルのフレームからBMPファイルを生成します。ビットマップの仕組み、およびデバイス独立ビットマップに関して知識があると、より本稿のプログラムを読み取ることができます。

Video for Windowsライブラリの初期化

 Video for Windows APIを利用するには、まず開発環境に対してインポートライブラリ「vfw32.lib」をインポートしてください。インポートの方法は、利用しているコンパイラ、または総合開発環境のドキュメントを参照してください。代表的な開発環境であるVisual Studioの場合は、プロジェクトのプロパティから設定します。

図2 Visual Studio .NET 2003プロパティページ
図2 Visual Studio .NET 2003プロパティページ
図3 Visual Studio 2005プロパティページ
図3 Visual Studio 2005プロパティページ

 プロジェクトを右クリックして[プロパティ]を選択し、ダイアログの左側のツリーから[構成プロパティ]→[リンカ]→[コマンドライン]を展開します。ここにリンカに設定するオプションを追加できるので[追加のオプション]に「vfw32.lib」を指定します(日本語名はすべてVisual Studio 2003のもの)。これで、「vfw32.lib」をインポートすることができます。

 プログラムでは、AVI関連の関数を使うソースコードに「vfw.h」ヘッダファイルをインクルードさせる必要があります。これで、基本的な準備は終わります。

 Video for Windows APIを利用するには、まずAVIFileInit()関数を呼び出してAVIFileライブラリを初期化しなければなりません。

STDAPI_(VOID) AVIFileInit(VOID);

 他のAVIから始まる関数を呼び出しは、必ずAVIFileInit()関数よりも後で行わなければなりません。この関数はライブラリの参照カウントをインクリメントします。AVIFileライブラリが不要になればAVIFileExit()関数で参照カウントをデクリメントすることができます。

STDAPI_(VOID) AVIFileExit(VOID);

 アプリケーション全体でAVI関連関数を使うような場合は、WinMain()関数の最初でAVIFIleInit()を呼び出し、メッセージループを抜け出した後のreturn文の直前で、AVIFIleExit()を呼び出してリソースを解放すればよいでしょう。

int WINAPI WinMain(
    HINSTANCE hInstance, HINSTANCE hPrevInstance,
    PSTR lpCmdLine, int nCmdShow
) {
    WNDCLASS wc;
    MSG msg;

    //AVIライブラリの初期化
    AVIFileInit();

    wc.style            = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc        = WindowProc;
    wc.cbClsExtra        = 0;
    wc.cbWndExtra        = 0;
    wc.hInstance        = hInstance;
    wc.hIcon            = LoadIcon(NULL , IDI_APPLICATION);
    wc.hCursor        = LoadCursor(NULL , IDC_ARROW);
    wc.hbrBackground        = GetStockObject(WHITE_BRUSH);
    wc.lpszMenuName        = IDR_MENU1;
    wc.lpszClassName        = APP_NAME;

    if (!RegisterClass(&wc)) return 0;

    if (CreateWindow(
        APP_NAME , APP_NAME ,
        WS_OVERLAPPEDWINDOW | WS_VISIBLE | WS_HSCROLL,
        CW_USEDEFAULT , CW_USEDEFAULT,
        CW_USEDEFAULT  , CW_USEDEFAULT,
        NULL , NULL , hInstance , NULL
    ) == NULL) return 0;

    while(GetMessage(&msg , NULL , 0 , 0) > 0) {
        DispatchMessage(&msg);
    }

    //AVIライブラリを解放する
    AVIFileExit();
    return msg.wParam;
}

 このコードは、本稿サンプルプログラムのWinMain()関数です。最初にAVIFIleInit()関数でライブラリを初期化し、return文の直前でAVIFileExit()を呼び出しています。

AVIファイルを開く

 AVIファイルを操作するには、何よりもまずAVIファイルを開く必要があります。AVIファイルを開くにはAVIFileOpen()関数を使います。この関数で開くAVIファイルインタフェースはファイル入出力で利用するハンドルではなく、AVIFileライブラリでファイルを操作するために使う専用のハンドルです。

STDAPI AVIFileOpen(PAVIFILE * ppfile, LPCTSTR szFile,
                   UINT mode, CLSID * pclsidHandler);

 AVIファイルのハンドルはPAVIFILE型として扱われます。ppfileには、開いたAVIファイルのハンドルを受けるPAVIFILEへのポインタを指定します。szFileには開くAVIファイルのファイル名が格納されているNULLで終わる文字列を指定します。

 modeにはファイルを開くときに使うアクセスモードを指定します。ここには、次のいずれかの定数を指定します。今回の用途はファイルからビットマップデータを抽出することなのでOF_READで開きます。

アクセスモードの種類
定数意味
OF_CREATE新しいファイルを作成します。
OF_SHARE_DENY_NONEファイルを非排他的に開きます。他のプロセスは、このファイルを読み取り専用または書き込み専用で開くことができます。
OF_SHARE_DENY_READファイルを非排他的に開きます。他のプロセスは、このファイルを書き込み専用で開くことができます。
OF_SHARE_DENY_WRITEファイルを非排他的に開きます。他のプロセスは、そのファイルを読み取り専用で開くことができます。
OF_SHARE_EXCLUSIVEファイルを開き、そのファイルに対する他のプロセスからのアクセスを禁止します。
OF_READファイルを読み取り用に開きます。
OF_READWRITEファイルを読み取りおよび書き込み用に開きます。
OF_WRITEファイルを書き込み用に開きます。

 pclsidHandlerには、利用するハンドラの識別子へのポインタを指定します。CLSIDはコンポーネントを識別するための16バイトからなる数値情報ですが、通常はNULLを指定します。ここにNULLが指定された場合は、システムがファイルの拡張子やRIFFタイプに基づいて、レジストリから適切なハンドラを選択します。ファイル拡張子やRIFFタイプに依存せずに固定のハンドラを使いたい場合には、NULL以外の適切な値を指定します。

 関数が成功すれば0が、失敗すれば0以外のエラー値が返ります。

 開いたAVIファイルを解放するにはAVIFIleRelease()関数を使います。AVIファイルのハンドルが不要になった場合は、この関数を呼び出して解放してください。

STDAPI_(ULONG) AVIFileRelease(PAVIFILE pfile);

 pfileには開いている有効なAVIファイルのハンドルを指定します。

AVIファイルストリーム

 AVIファイルを無事に開くことができれば、PAVIFILEハンドルを取得することができます。AVIFileから始まる関数を使ってAVIファイルの情報を取得したり、AVIファイルを操作するにはPAVIFILE型のハンドルが必要になります。

 AVIファイルにデータを書き込んだり、AVIファイルからデータを取得するにはストリームを開く必要があります。AVIファイルのストリームを開くにはAVIFileGetStream()関数を呼び出します。

STDAPI AVIFileGetStream(PAVIFILE pfile, PAVISTREAM * ppavi,
                        DWORD fccType, LONG lParam);

 pfileには有効なAVIファイルのハンドルを、ppaviにはAVIファイルから取得するストリームハンドルへのポインタを指定します。関数が成功すればppaviが参照している変数に、適切なストリームハンドルが格納されます。fccTypeには開くストリームの種類を指定します。通常のAVIファイルには音声と映像ストリームがそれぞれ格納されています。fccTypeには以下のいずれかの定数を指定します。

定数ストリームの種類
StreamtypeAUDIOオーディオストリーム
StreamtypeMIDIMIDIストリーム
StreamtypeTEXTテキストストリーム
StreamtypeVIDEOビデオストリーム

 ビデオストリームからフレームを取得してビットマップファイルに保存することが今回の目的なので、この場はstreamtypeVIDEOを指定します。開いているストリームハンドルが不要になればAVIStreamRelease()関数で解放してください。

STDAPI_(LONG) AVIStreamRelease(PAVISTREAM pavi);

 paviには有効なAVIストリームハンドルを指定します。

フレーム解凍オブジェクト

 AVIファイルを開き、ビデオストリームのハンドルを取得することができれば、いよいよフレームの解凍処理に入ります。AVIフレームは単純なビットマップの配列ではなく、圧縮されているため、まずは解凍オブジェクトを取得しなければなりません。解凍オブジェクトを取得してフレームの解凍を準備するにはAVIStreamGetFrameOpen()関数を使います。

STDAPI_(PGETFRAME) AVIStreamGetFrameOpen(PAVISTREAM pavi,
    LPBITMAPINFOHEADER lpbiWanted);

 paviには有効なAVIビデオストリームのハンドルを指定します。lpbiWantedには回答結果となるビデオフォーマットを表すBITMAPINFOHEADER構造体へのポインタを指定します。通常はデフォルトを表すNULLか、システムで最適な表示フォーマットにデコードすることを表すAVIGETFRAMEF_BESTDISPLAYFMTを指定します。色数や圧縮形式など、取得するビットマップのフォーマットを指定したい場合は、適切なBITMAPINFOHEADER構造体へのポインタを指定します。

 関数が成功すればPGETFRAME型の解凍オブジェクトを取得することができます。指定されたフォーマットに解凍できる解凍ツールが発見されなかった場合は、関数が失敗してNULLが返ります。

 AVIフレームの解凍オブジェクトを取得することができれば、AVIStreamGetFrame()関数からデバイス独立ビットマップ(DIB)を取得することができます。

STDAPI_(LPVOID) AVIStreamGetFrame(PGETFRAME pgf, LONG lPos);

 pgfには、AVIStreamGetFrameOpen()関数が返した適切なフレーム解凍オブジェクトを指定します。lPosには、取得するストリーム内のフレーム番号を指定します。この番号は0からストリームの長さまで指定することができます。関数が成功すれば、フレームデータへのポインタを返します。

 フレームデータは、パックDIBの先頭へのポインタとして返されるので、BITMAPINFOHEADER構造体へのポインタとして扱うことができます。ピクセルデータがBITMAPINFOHEADER構造体に続いて保存されているため、この関数の戻り値から画像をウィンドウに表示させたり、あるいはビットマップファイルとして保存することができます。ただし、AVIStreamGetFrame()が返したフレームは解凍オブジェクトが解放されるか、またはAVIStreamGetFrame()関数が次に呼び出されるまでの間のみ有効です。

 フレームが何番まで指定できるかどうかは、AVIファイルのビデオストリームの長さに依存します。ストリームの長さを取得するにはAVIStreamLength()関数を使います。

STDAPI_(LONG) AVIStreamLength(PAVISTREAM pavi);

 paviにはAVIストリームハンドルを指定します。開いたビデオストリームを指定すれば、AVIStreamGetFrame()関数のlPosで指定できる最大値を知ることができます。

 AVIStreamGetFrameOpen()関数で開いた解凍オブジェクトが不要になればAVIStreamGetFrameClose()関数で解放してください。

STDAPI AVIStreamGetFrameClose(PGETFRAME pget);

 pgetには、有効なPGETFRAMEを指定します。

アプリケーション解説

 今回開発したサンプルアプリケーションでは、メニューの[AVIを開く]を選択して、表示されたファイル選択ダイアログからAVIファイルを選択すると、ウィンドウ上に選択したAVIファイルの任意のフレームを表示します。フレームは、ウィンドウ下部の水平スクロールバーで移動することができます。表示しているフレームをビットマップイメージとしてディスクに保存するには、「ビットマップで保存」メニューを選択してくださ。

 このアプリケーションでは、[AVIを開く]でAVIファイルを選択した際に、AVIファイルを開いてハンドルを初期化するInitAVIResource()関数と、現在開いているAVI関連リソースを解放するRemoveAVIResource()関数に注目してください。

//AVI ファイルオブジェクト
PAVIFILE aviFile = NULL;

//AVI ファイルのストリームオブジェクト
PAVISTREAM aviStream = NULL;

//AVI ファイルからフレームを解凍するオブジェクト
PGETFRAME aviFrame = NULL;

//現在のカレントフレーム
BITMAPINFOHEADER * bmpInfoHeader = NULL;

//AVI ファイルを開いて必要な初期化処理を行う
BOOL InitAVIResource(PCTSTR fileName) {
    //AVIFile を開く
    if (AVIFileOpen(&aviFile , fileName , OF_READ , NULL)) {
        return FALSE;
    }
    //AVI ストリームを取得する
    if (AVIFileGetStream(aviFile , &aviStream , streamtypeVIDEO , 0)) {
        AVIFileRelease(aviFile);
        aviFile = NULL;
        return FALSE;
    }
    //フレームを解凍するためのオブジェクトを取得
    aviFrame = AVIStreamGetFrameOpen(aviStream ,
        AVIGETFRAMEF_BESTDISPLAYFMT);
    if (aviFrame == NULL) {
        AVIStreamRelease(aviStream);
        AVIFileRelease(aviFile);
        aviStream = NULL;
        aviFile = NULL;
        return FALSE;
    }

    bmpInfoHeader = (BITMAPINFOHEADER *)AVIStreamGetFrame(aviFrame , 0);
    return TRUE;
}

//AVI ファイルに関連するすべてのリソースを解放する
void RemoveAVIResource(){
    //カレントフレームを NULL に設定して描画されないようにする
    bmpInfoHeader = NULL;

    //各リソースを解放
    if (aviFrame) AVIStreamGetFrameClose(aviFrame);
    if (aviStream) AVIStreamRelease(aviStream);
    if (aviFile) AVIFileRelease(aviFile);

    aviFrame = NULL;
    aviStream = NULL;
    aviFile = NULL;
}

 InitAVIResource()関数では、引数から受け取ったファイル名でAVIファイルを開きます。同時にストリームを開き、解凍オブジェクトを取得します。これらの一連の流れで失敗が無ければ、グローバル変数にAVIファイルのハンドル、AVIストリームのハンドル、フレーム解凍オブジェクトを保存し、フレーム0番のイメージをDIBで取得します。

 現在選択されているカレントフレームのイメージは、常にグローバル変数のbmpInfoHeaderに保存されるものとし、選択されていない場合はNULLとします。ウィンドウプロシージャでは、AVIファイル制御にはほとんど関与せず、単純にbmpInfoHeaderをイメージとして扱っているだけです。WM_PAINTメッセージではbmpInfoHeaderを使って、SetDIBitsToDevice()関数からウィンドウにイメージを描画しています。

 以下はウィンドウプロシージャのWM_PAINTメッセージの処理です。

case WM_PAINT:
    hdc = BeginPaint(hWnd , &ps);

    //カレントフレームが NULL でなければ描画する
    if (bmpInfoHeader != NULL) {
        SetDIBitsToDevice(
            hdc , 0 , 0 , bmpInfoHeader->biWidth ,
            bmpInfoHeader->biHeight ,
            0 , 0 , 0 , bmpInfoHeader->biHeight , bmpInfoHeader + 1 ,
            (BITMAPINFO *)bmpInfoHeader , DIB_RGB_COLORS
        );
    }
    wsprintf(text , TEXT("フレーム=%d") , scrollInfo.nPos);
    TextOut(hdc , 0 , 0 , text , lstrlen(text));

    EndPaint(hWnd , &ps);
    return 0;

 表示するフレームの選択は、ウィンドウ下部の水平スクロールバーによって行われています。AVIファイルを開くと、最初にInitAVIResource()関数を呼び出し、関数が成功すればストリームからフレームの長さを取得して、これをスクロールバーの最大値に設定することで、スクロールバーをフレーム選択に利用しています。

 スクロールバーを操作するとWM_HSCROLLメッセージが発行されるので、このタイミングで移動されたスクロールバーの位置を取得します。スクロールバーの位置はAVIストリームのフレームに関連付けているため、スクロールバーの位置に対応したフレームを解凍してbmpInfoHeader変数を更新します。

 以下は、メニューで[AVIを開く]と[ビットマップで保存]を選択したときの処理と、水平スクロールバーの処理です。

case WM_COMMAND:
    switch (LOWORD(wParam)) {
    case IDM_FILE_OPEN:    //AVIを開く
        if (GetOpenFileName(&openFileName)) {
            RemoveAVIResource();
            if (!InitAVIResource(openFileName.lpstrFile)) {
                //失敗した
                break;
            }

            //スクロール情報を初期化
            scrollInfo.fMask = SIF_RANGE | SIF_POS;
            scrollInfo.nPos = 0;
            scrollInfo.nMin = 0;
            scrollInfo.nMax = AVIStreamLength(aviStream) - 1;
            //スクロールバーを初期位置へ移動
            SetScrollInfo(hWnd, SB_HORZ, &scrollInfo, TRUE);

            //ウィンドウを再描画
            InvalidateRect(hWnd, NULL, TRUE);
        }
        break;
    case IDM_FILE_SAVE:    //ビットマップで保存
        //カレントフレームが NULL ならば何もしない
        if (bmpInfoHeader == NULL) break;

        if (GetSaveFileName(&saveFileName)) {
            HANDLE handle;
            BITMAPFILEHEADER bitmapFileHeader = { 0x4D42, 0, 0, 0, 0};
            DWORD dwResult;

            //ビットマップファイルヘッダーを初期化
            bitmapFileHeader.bfOffBits =
                sizeof(BITMAPFILEHEADER) +  sizeof(BITMAPINFOHEADER);
            bitmapFileHeader.bfSize =
                bitmapFileHeader.bfOffBits + bmpInfoHeader->biSizeImage;

            //ディスクファイルに書き込み処理
            handle = CreateFile(
                saveFileName.lpstrFile, GENERIC_WRITE, FILE_SHARE_READ,
                NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL
            );
            WriteFile(handle, &bitmapFileHeader,
                sizeof(BITMAPFILEHEADER), &dwResult, NULL);
            WriteFile(handle, bmpInfoHeader,
                sizeof(BITMAPINFOHEADER), &dwResult, NULL);
            WriteFile(handle, bmpInfoHeader + 1,
                bmpInfoHeader->biSizeImage, &dwResult, NULL);
            CloseHandle(handle);
        }
        break;
    }
case WM_HSCROLL: //スクロールバーからのメッセージ
    if (LOWORD(wParam) == SB_THUMBTRACK) { //スクロールバーが移動した
        DWORD dwCount = HIWORD(wParam);
        bmpInfoHeader =
            (BITMAPINFOHEADER *)AVIStreamGetFrame(aviFrame , dwCount);

        //スクロールバーを設定する
        scrollInfo.fMask = SIF_POS;
        scrollInfo.nPos = dwCount;
        SetScrollInfo(hWnd , SB_HORZ , &scrollInfo , TRUE);

        //フレームを描画するためにウィンドウを再描画
        InvalidateRect(hWnd, NULL, FALSE);
    }
    break;

 AVIファイルを選択すると、最初に既存のAVIリソースを解放するためにRemoveAVIResource()を呼び出して、グローバル変数に保存されている有効なハンドルをすべて解放します。次にInitAVIResource()関数を呼び出して、ユーザーが選択したAVIファイルを開き、必要な初期化処理を行います。そして、スクロールバーを0の位置に初期化してウィンドウを再描画しています。

 ビットマップファイルを保存する場合、まずbmpInfoHeaderが有効かどうかを調べ、カレントフレームがNULLの状態であれば無効と判断して処理を中断させます。カレントフレームが選択されていればユーザーに保存するファイル名を選択してもらい、通常のファイル入出力APIを使ってDIBをビットマップファイルに保存しています。

 水平スクロールバーの処理では、スクロールバーからのメッセージがドラッグしたことを表すSB_THUMBTRACKの場合に、スクロールバーの位置情報を取得して、位置に対応するフレームをAVIStreamGetFrame()関数から取得しています。この戻り値をグローバル変数のbmpInfoHeaderに保存してカレントフレームを上書きすることで、ウィンドウに表示されるフレームを更新することができます。

 今回は複雑なコードを避けるために、スライダーの代用にウィンドウの水平スクロールバーを使っています。しかし、スクロールバーの位置情報は16ビットしかないので、65535以上のフレームを持つAVIファイルを開いた場合、65535以上のフレームを選択すると不具合が発生します。ご了承ください。

最後に

 コーデックがインストールされている環境であれば、DivXなど圧縮されたAVIファイルもVideo for Windows APIを通して安全に解凍することができます。今回は既存のAVIファイルからビットマップを抽出するだけでしたが、他にも音声ストリームからAVIファイルの音楽データを解凍してWAVEファイルとして保存するなど様々な機能があります。こうした機能を追及していけば、動画編集などのツールアプリケーションを開発することができるようになるでしょう。

 マルチメディア制御に更なる興味があるのであれば、マルチメディアファイル入出力API(MMIO)やリソース交換ファイル形式 (RIFF:Resource Interchange File Format) などの世界に足を踏み入れる必要があります。他にも、MCIやオーディオミキ、MIDI、WAVEなど、マルチメディアデータを制御するAPIが多く提供されているので、動画や音声、MIDIなどの制御ソフトウェア開発に興味がある方は、ぜひMSDNなどを使ってこれらの関数を調べてみてください。

参考資料

  1. MSDNライブラリ 2005年4月
  2. Windows95 APIバイブル3 ODBC・マルチメディア編』 Richard J Simon・Tony Davis・John Eaton・R. Murray Goertz 著、スリーエーシステムズ 訳、江藤ソフトオフィス 監修、翔泳社、1997年5月
  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

  • 赤坂 玲音(アカサカ レオン)

    平成13年度「全国高校生・専門学校生プログラミングコンテスト 高校生プログラミングの部」にて最優秀賞を受賞。 2005 年度~ Microsoft Most Variable Professional Visual Developer - Visual C++。 プログラミング入門サイト Wis...

All contents copyright © 2005-2020 Shoeisha Co., Ltd. All rights reserved. ver.1.5