EternalWindows
AVI / フレームの保存

今回は、ビデオストリーム内のフレームをビットマップとして保存する方法を考えてみます。 このためには、まず無圧縮のフレームを取得する必要がありますが、 多くのフレームは圧縮されてビデオストリーム内に保存されているため、 フレームの解凍処理が別途必要になります。 よって、AVIStreamReadで目的のフレームを取得した後、 ICDecompressで明示的に解凍処理を行うという流れになります。 ただし、コーデックがGetFrameオブジェクトを実装している場合は、 ビデオストリーム内におけるフレームのインデックスを指定するだけで、 無圧縮のフレームを取得することができます。 つまり、GetFrameオブジェクトを利用すれば、 アプリケーションが明示的に解凍処理を行う必要がなくなります。 GetFrameオブジェクトは、AVIStreamGetFrameOpenで取得することができます。

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

paviは、ビデオストリームのインターフェースを指定します。 lpbiWantedは、解凍後のフォーマットを指定します。 NULLを指定した場合、多くのコーデックでは無圧縮のDIBに解凍されますが、 コーデックによっては明示的に指定しなければならない場合もあります。 戻り値は、GetFrameオブジェクトへのポインタとなります。

GetFrameオブジェクトを取得すれば、AVIStreamGetFrameで解凍されたフレームを取得することができます。

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

pgfは、GetFrameオブジェクトへのポインタを指定します。 lPosは、取得したいフレームのインデックスを指定します。 戻り値は、解凍されたフレームへのポインタが返ります。 これは、LPBITMAPINFOHEADER型で表すことができ、 BITMAPINFOHEADER構造体の後にはビットイメージが格納されています。 取得したフレームに開放処理などは必要ありません。

不要になったGetFrameオブジェクトは、AVIStreamGetFrameCloseで開放することになります。

STDAPI AVIStreamGetFrameClose(
  PGETFRAME pget  
);

pgetは、GetFrameオブジェクトへのポインタを指定します。

今回のプログラムは、ウインドウにスクロールバーを設定し、 スクロールによって表示する各フレームを調整します。 メニューから「フレームの保存」を選択すると、現在表示されているフレームが ビットマップとして保存されます。

#include <windows.h>
#include <vfw.h>

#pragma comment (lib, "vfw32.lib")

#define ID_SAVE 100

void InitializeMenuItem(HMENU hmenu, LPTSTR lpszItemName, int nId);
BOOL SaveFrame(LPTSTR lpszFileName, LPBITMAPINFOHEADER lpbmi);
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW | WS_HSCROLL, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static PAVISTREAM pavi = NULL;
	static PGETFRAME  pgf = NULL;
	static int        nIndex = 0;

	switch (uMsg) {

	case WM_CREATE: {
		HMENU            hmenu;
		SCROLLINFO       si;
		BITMAPINFOHEADER biOut;
	
		AVIFileInit();

		if (AVIStreamOpenFromFile(&pavi, TEXT("cap.avi"), streamtypeVIDEO, 0, OF_READ, NULL) != 0) {
			MessageBox(NULL, TEXT("ファイルまたはビデオストリームが存在しません。"), NULL, MB_ICONWARNING);
			return -1;
		}
		
		ZeroMemory(&biOut, sizeof(BITMAPINFOHEADER));
		biOut.biSize        = sizeof(BITMAPINFOHEADER);
		biOut.biPlanes      = 1;
		biOut.biBitCount    = 24;
		biOut.biCompression = BI_RGB;

		pgf = AVIStreamGetFrameOpen(pavi, &biOut);
		if (pgf == NULL) {
			MessageBox(NULL, TEXT("GetFrameオブジェクトの取得に失敗しました。"), NULL, MB_ICONWARNING);
			return -1;
		}
		
		si.cbSize = sizeof(SCROLLINFO);
		si.fMask  = SIF_POS | SIF_RANGE | SIF_PAGE | SIF_DISABLENOSCROLL;
		si.nMin   = 0;
		si.nMax   = AVIStreamLength(pavi) - 1;
		si.nPage  = AVIStreamLength(pavi) / 10;
		si.nPos   = 0;
		SetScrollInfo(hwnd, SB_HORZ, &si, TRUE);
		
		hmenu = CreateMenu();
		InitializeMenuItem(hmenu, TEXT("フレームの保存(&F)"), ID_SAVE);
		SetMenu(hwnd, hmenu);
		
		return 0;
	}
	
	case WM_PAINT: {
		HDC                hdc;
		PAINTSTRUCT        ps;
		LPBITMAPINFOHEADER lpbmi;

		hdc = BeginPaint(hwnd, &ps);
		lpbmi = (LPBITMAPINFOHEADER)AVIStreamGetFrame(pgf, nIndex);
		if (lpbmi != NULL) {
			StretchDIBits(hdc, 0, 0, lpbmi->biWidth, lpbmi->biHeight, 0, 0, lpbmi->biWidth, lpbmi->biHeight, lpbmi + 1, (LPBITMAPINFO)lpbmi, DIB_RGB_COLORS, SRCCOPY);
		}
		EndPaint(hwnd, &ps);

		return 0;
	}
	
	case WM_COMMAND:
		if (LOWORD(wParam) == ID_SAVE) {
			TCHAR szFileName[256];
			wsprintf(szFileName, TEXT("frame %d.bmp"), nIndex);
			SaveFrame(szFileName, (LPBITMAPINFOHEADER)AVIStreamGetFrame(pgf, nIndex));
		}
		return 0;

	case WM_HSCROLL: {
		SCROLLINFO si;
		int        d = 0;
		
		si.cbSize = sizeof(SCROLLINFO);
		si.fMask  = SIF_PAGE | SIF_POS | SIF_RANGE;
		GetScrollInfo(hwnd, SB_HORZ, &si);
		
		switch (LOWORD(wParam)) {
		case SB_TOP:
			d = -si.nPos;
			break;
		case SB_BOTTOM:
			d = si.nMax - si.nPos;
			break;
		case SB_LINEUP:
			d = -1;
			break;
		case SB_LINEDOWN:
			d = 1;
			break;
		case SB_PAGEUP:
			d = -1 * si.nPage;
			break;
		case SB_PAGEDOWN:
			d = si.nPage;
			break;
		case SB_THUMBTRACK:
			d = HIWORD(wParam) - si.nPos;
			break;
		default:
			return 0;
		}
		
		si.nPos += d;
		if (si.nPos > si.nMax)
			si.nPos = si.nMax;
		else if (si.nPos < si.nMin)
			si.nPos = si.nMin;
		else
			;
		SetScrollInfo(hwnd, SB_HORZ, &si, TRUE);

		nIndex = si.nPos;
		InvalidateRect(hwnd, NULL, FALSE);
		return 0;
	}

	case WM_DESTROY:
		if (pgf != NULL)
			AVIStreamGetFrameClose(pgf);
		if (pavi != NULL)
			AVIStreamRelease(pavi);

		AVIFileExit();

		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

BOOL SaveFrame(LPTSTR lpszFileName, LPBITMAPINFOHEADER lpbmi)
{
	HANDLE           hFile;
	DWORD            dwResult;
	BITMAPFILEHEADER bmfHeader;

	hFile = CreateFile(lpszFileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE) {
		MessageBox(NULL, TEXT("ファイルの作成に失敗しました。"), NULL, MB_ICONEXCLAMATION | MB_OK);
		return FALSE;
	}

	ZeroMemory(&bmfHeader, sizeof(BITMAPFILEHEADER));
	bmfHeader.bfType    = *(LPWORD)"BM";
	bmfHeader.bfSize    = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + lpbmi->biSizeImage;
	bmfHeader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);

	WriteFile(hFile, &bmfHeader, sizeof(BITMAPFILEHEADER), &dwResult, NULL);
	WriteFile(hFile, lpbmi, sizeof(BITMAPINFOHEADER), &dwResult, NULL);
	WriteFile(hFile, lpbmi + 1, lpbmi->biSizeImage, &dwResult, NULL);

	CloseHandle(hFile);
	
	return TRUE;
}

void InitializeMenuItem(HMENU hmenu, LPTSTR lpszItemName, int nId)
{
	MENUITEMINFO mii;
	
	mii.cbSize     = sizeof(MENUITEMINFO);
	mii.fMask      = MIIM_ID | MIIM_TYPE;
	mii.fType      = MFT_STRING;
	mii.wID        = nId;
	mii.dwTypeData = lpszItemName;
	
	InsertMenuItem(hmenu, nId, FALSE, &mii);
}

まず、AVIStreamOpenFromFileでビデオストリームを取得し、 次にAVIStreamGetFrameOpenを呼び出してGetFrameオブジェクトを取得します。 第2引数にはNULLを指定することもできますが、 中には明示的に指定しなければならないコーデックも存在します。 確実とは言えませんが、biPlanesとbiBitCountを初期化しておけば問題ないと思われます。

CreateWindowExのウインドウスタイルにWS_HSCROLLを指定していることから、 WindowProcにWM_HSCROLLが送られることになります。 ここでは、現在のスクロール位置を算出し、それをフレームのインデックスとして静的変数に保存します。 そして、InvalidateRectの呼び出しでWM_PAINTを生成し、 保存された静的変数のインデックスをAVIStreamGetFrameに指定します。

lpbmi = (LPBITMAPINFOHEADER)AVIStreamGetFrame(pgf, nIndex);
StretchDIBits(hdc, 0, 0, lpbmi->biWidth, lpbmi->biHeight, 0, 0, lpbmi->biWidth, lpbmi->biHeight, lpbmi + 1, (LPBITMAPINFO)lpbmi, DIB_RGB_COLORS, SRCCOPY);		

StretchDIBitsの第10引数は、ビットイメージのアドレスとなっています。 ビットイメージは、AVIStreamGetFrameによって返されたBITMAPINFOHEADER構造体の後に存在するため、 lpbmi + 1とすることでビットイメージの先頭を参照できるようになります。


戻る