画像処理の最近のブログ記事



カメラでキャプチャした画像にテキストをオーバレイしようと思ったんだけど、OpenCVのテキスト描画機能が貧弱(どのくらい貧弱かは『opencv.jp - OpenCV: テキスト(Text)サンプルコード -』のページ最下部のサンプルを参照)で、加えてASCIIな文字列しか扱えないそうだったので別の方法を考えることにした。


TrueTypeとかOpenTypeなフォントを扱うライブラリとしてはFreeTypeが有名。ベクタデータで表現されているフォントデータを、扱いやすいビットマップデータにしてくれる。


ビットマップにさえ持ち込んでしまえばあとはこちらで勝手に合成すればいいので、そこまでをいい感じに自動化してくれるラッパーを作りたい。そのためにとりあえず何らかのテキストを適当にビットマップに書き出してみた。


開発環境


高専プロコンに向けての調整の合間に作ったので、学校で使用している環境での作業となった。開発環境は以下のとおり。



  • Windows XP Pro

  • Visual C++ Express Edition

  • Core 2 Quad(2.66GHz)

  • 2GB


ちなみにid:Tnzkの自宅のPCはCeleron D, 516MB\(^0^)/


FreeTypeのインストール


Win用のバイナリはGnuWin32 projectにより提供されている。ダウンロードページのDownload節にある表の、Binariesの項目がWin用のバイナリ。"Zip"のリンクから落とせる。現時点での最新版は2.3.5らしい。


落としたzipは展開して適当な場所へ。ぼくはC:\freetypeに配置しました。


VC++プロジェクト作成


VCにて、空のプロジェクトを新規作成する。コンソールアプリケーション。


作成したらプロジェクトのプロパティを開き、構成プロパティを開き、以下の設定をする。



  • C/C++→全般→追加のインクルードディレクトリに、C:\freetype\include\freetype2;C:\freetype\includeのふたつを追加。

  • リンカ→追加のライブラリディレクトリにC:\freetype\libを追加。

  • リンカ→入力→追加の依存ファイルにfreetype.libを追加。


サンプルのビルド


FreeType Projectのページにはチュートリアルがある(FreeType 2 Tutorial)ので、まずはこれをビルドしてみる。コードを少しずつ書きながら完成に近づいていく形式なのでコピペしづらい。というかマークアップが特殊なのかコピペすると改行が残らない。エディタにコピペしてs/\;/\n\;/gすると幾分か楽になります。


7. Simple Text Rendering(見出しにくらいアンカーしてくれてもいいのに...)という項目があるが、これはレンダリングまで記述してあるわけではなく、my_draw_bitmapとかいう関数を呼んでお茶を濁してある。まあとりあえずは表示せずに、ビットマップにまで持ち込むことができれば良いので、my_draw_bitmapのくだりを省いてビルド→実行。動いたもののとてもむなしい。


ビットマップファイルに保存


ちゃんとラスタライズされてるかわかんないので、ビットマップに保存することに。かといってWin32APIにはなるべく関わりたくない(多くの開発者にはこの気持ちが理解していただけると思う)ので、なるべく出来合いのもので済ませようとしたところ、SaveBitmapFile( HDC, HBITMAP, LPCSTR)な使いやすそうな関数を公開している人がいたのでこれを利用することにした。「かなり使えるんで使いたいかたはどうぞ」とのこと。本当にありがとうございました。


SaveBitmapFileはデバイスコンテキストとビットマップのハンドルを準備する必要があるようなので、この辺りは仕方ないので自分で準備することにした。


一文字ごとに保存

チュートリアルを見ると各文字ごとにビットマップを生成しているようなので、まずは各文字ごとにそれぞれファイルを作成することにした。



#include <string.h>
#include <windows.h>
#include <ft2build.h>
#include FT_FREETYPE_H

#include "SaveBitmap.h"

int _main(){
FT_Library library;
FT_Face face;

FT_GlyphSlot slot;
FT_UInt glyph_index;

BYTE* m_pbits;
HBITMAP hBitmap;
BITMAPINFO *info;
info = (BITMAPINFO*)malloc( sizeof(BITMAPINFOHEADER));
ZeroMemory( info, sizeof(BITMAPINFOHEADER));
info->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);

char text[] = "Hello, world!";
TCHAR filename[] = L"testA.bmp";
int pen_x, pen_y, n;
int num_chars = (int)strlen( text);

// FreeTypeの初期化とTrueTypeフォントの読み込み
FT_Init_FreeType( &library );
FT_New_Face( library, "C:\\WINDOWS\\Fonts\\trebuc.TTF", 0, &face );
slot = face->glyph;

FT_Set_Char_Size( face, 0, 16 * 64, 300, 300);

pen_x = 300;
pen_y = 200;
for ( n = 0;n < num_chars; n++ ){
int i;
DWORD writeSize;
FT_Bitmap bitmap;
HDC memDC;
HDC hBuffer;
HANDLE fh;
BITMAPFILEHEADER head={0};
RECT rect;

// n文字目の文字をビットマップ化
FT_Load_Char( face, text[n], FT_LOAD_RENDER);
bitmap = slot->bitmap;

// ビットマップヘッダの設定
head.bfType = 'MB';
head.bfSize = sizeof(BITMAPINFOHEADER) + sizeof(BITMAPFILEHEADER) + bitmap.rows * bitmap.width * 4;
head.bfOffBits = sizeof(BITMAPINFOHEADER) + sizeof(BITMAPFILEHEADER);
info->bmiHeader.biWidth = bitmap.width;
info->bmiHeader.biHeight = bitmap.rows;
info->bmiHeader.biPlanes = 1;
info->bmiHeader.biBitCount = 32;
info->bmiHeader.biCompression = BI_RGB;
info->bmiHeader.biSizeImage = bitmap.rows * bitmap.width * 4;
info->bmiHeader.biXPelsPerMeter = 0;
info->bmiHeader.biYPelsPerMeter = 0;

// DIBを作成
hBitmap = CreateDIBSection( NULL, info, DIB_RGB_COLORS, (VOID**)&m_pbits, NULL, 0);

// デバイスコンテキストを作成
memDC = CreateCompatibleDC( NULL);
hBuffer = CreateCompatibleDC( memDC);
SelectObject( memDC, hBitmap);
rect.left = 0;
rect.top = 0;
rect.bottom = bitmap.rows;
rect.right = bitmap.width;

for( i = 0; i < bitmap.rows * bitmap.width; i++){
SetPixel( memDC, i % bitmap.width, i / bitmap.width, RGB( bitmap.buffer[i], bitmap.buffer[i] >> 1, bitmap.buffer[i] >> 2));
}
// ビットマップを保存
SaveBitmapFile( memDC, hBitmap, filename);
filename[4]++;
pen_x += slot->advance.x >> 6;

}

return 0;

}

実行すると、文字列ごとにtestA.bmp, testB.bmpといったように連番で保存する。以下のような感じ。


f:id:Tnzk:20081005205452p:image


チュートリアルどおりに適当に動かしたところ8bitのグレースケール画像が作成されたので、SetPixelでオレンジ色になるように描画してみた。あくまでFreeTypeはフォントの形状を扱うだけなのでグレースケールを主に使うっぽい。まあそれさえあればグラデーションでも何でもできるので問題ないはず。


文字列を1枚の画像として保存


各文字ごとに保存できれば簡単な話だろなー、と思って次のように書いた。



#include <string.h>
#include <windows.h>
#include <ft2build.h>
#include FT_FREETYPE_H

#include "SaveBitmap.h"

int main(){
FT_Library library;
FT_Face face;
FT_GlyphSlot slot;
FT_UInt glyph_index;
FT_Error error;

char text[] = "Hello, world!";
TCHAR filename[] = L"test.bmp";
int pen_x, pen_y, n;

FT_Init_FreeType( &library );
FT_New_Face( library, "C:\\WINDOWS\\Fonts\\trebuc.TTF", 0, &face );
slot = face->glyph;
FT_Set_Char_Size( face, 0, 16 * 64, 300, 300);

BYTE* m_pbits;
HBITMAP hBitmap;
BITMAPINFO *info;
BITMAPFILEHEADER head={0};
HDC memDC;
HDC hBuffer;

head.bfType = 'MB';
head.bfSize = sizeof(BITMAPINFOHEADER) + sizeof(BITMAPFILEHEADER) + 640 * 480 * 4;
head.bfOffBits = sizeof(BITMAPINFOHEADER) + sizeof(BITMAPFILEHEADER);

info = (BITMAPINFO*)malloc( sizeof(BITMAPINFOHEADER));
ZeroMemory( info, sizeof(BITMAPINFOHEADER));
info->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
int num_chars = (int)strlen( text);

info->bmiHeader.biWidth = 640;
info->bmiHeader.biHeight = 480;
info->bmiHeader.biPlanes = 1;
info->bmiHeader.biBitCount = 32;
info->bmiHeader.biCompression = BI_RGB;
info->bmiHeader.biSizeImage = 640 * 480 * 4;
info->bmiHeader.biXPelsPerMeter = 0;
info->bmiHeader.biYPelsPerMeter = 0;

hBitmap = CreateDIBSection( NULL, info, DIB_RGB_COLORS, (VOID**)&m_pbits, NULL, 0);

memDC = CreateCompatibleDC( NULL);
hBuffer = CreateCompatibleDC( memDC);

SelectObject( memDC, hBitmap);

int cpos_x = 50;

pen_x = 300;
pen_y = 200;
for ( n = 0;n < num_chars; n++ ){
int i;
DWORD writeSize;
FT_Bitmap bitmap;
RECT rect;

FT_Load_Char( face, text[n], FT_LOAD_RENDER);
bitmap = slot->bitmap;

rect.left = 0;
rect.top = 0;
rect.right = 640;
rect.bottom = 480;
for( i = 0; i < bitmap.rows * bitmap.width; i++){
SetPixel( memDC, ( i % bitmap.width) + cpos_x, ( i / bitmap.width), RGB( bitmap.buffer[i], bitmap.buffer[i] >> 1, bitmap.buffer[i] >> 2));
}
cpos_x += bitmap.width + 5;
pen_x += slot->advance.x >> 6;
}

SaveBitmapFile( memDC, hBitmap, filename);
}

書いたところ、次のようなことになった。繰り返しになるがFreeTypeはフォントの形状だけを扱い、レイアウトやカラーリングなどには関与しない。


f:id:Tnzk:20081005205453p:image


というわけで、まず描画領域(左上の座標と幅、高さ)を設定し、描画を開始するy座標と高さの和から文字の高さを引いたものをオフセットとし、このオフセットを加えて描画するようにした。次のようなコード。



#include <string.h>
#include <windows.h>
#include <ft2build.h>
#include FT_FREETYPE_H

#include "SaveBitmap.h"

int main(){
FT_Library library;
FT_Face face;
FT_GlyphSlot slot;
FT_UInt glyph_index;
FT_Error error;

char text[] = "Hello, world!";
TCHAR filename[] = L"test.bmp";
int pen_x, pen_y, n;

FT_Init_FreeType( &library );
FT_New_Face( library, "C:\\WINDOWS\\Fonts\\trebuc.TTF", 0, &face );
slot = face->glyph;
FT_Set_Char_Size( face, 0, 16 * 64, 300, 300);

BYTE* m_pbits;
HBITMAP hBitmap;
BITMAPINFO *info;
BITMAPFILEHEADER head={0};
HDC memDC;
HDC hBuffer;

head.bfType = 'MB';
head.bfSize = sizeof(BITMAPINFOHEADER) + sizeof(BITMAPFILEHEADER) + 640 * 480 * 4;
head.bfOffBits = sizeof(BITMAPINFOHEADER) + sizeof(BITMAPFILEHEADER);

info = (BITMAPINFO*)malloc( sizeof(BITMAPINFOHEADER));
ZeroMemory( info, sizeof(BITMAPINFOHEADER));
info->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
int num_chars = (int)strlen( text);

info->bmiHeader.biWidth = 640;
info->bmiHeader.biHeight = 480;
info->bmiHeader.biPlanes = 1;
info->bmiHeader.biBitCount = 32;
info->bmiHeader.biCompression = BI_RGB;
info->bmiHeader.biSizeImage = 640 * 480 * 4;
info->bmiHeader.biXPelsPerMeter = 0;
info->bmiHeader.biYPelsPerMeter = 0;

hBitmap = CreateDIBSection( NULL, info, DIB_RGB_COLORS, (VOID**)&m_pbits, NULL, 0);

memDC = CreateCompatibleDC( NULL);
hBuffer = CreateCompatibleDC( memDC);

SelectObject( memDC, hBitmap);

int cpos_x = 50;
int cpos_y = 60;
int cpos_w = 640;
int cpos_h = 50;

pen_x = 300;
pen_y = 200;
for ( n = 0;n < num_chars; n++ ){
int i;
DWORD writeSize;
FT_Bitmap bitmap;
RECT rect;

FT_Load_Char( face, text[n], FT_LOAD_RENDER);
bitmap = slot->bitmap;

rect.left = 0;
rect.top = 0;
rect.right = 640;
rect.bottom = 480;
int offset_y = cpos_h - bitmap.rows + cpos_y;
for( i = 0; i < bitmap.rows * bitmap.width; i++){
SetPixel( memDC, ( i % bitmap.width) + cpos_x, offset_y + ( i / bitmap.width), RGB( bitmap.buffer[i], bitmap.buffer[i] >> 1, bitmap.buffer[i] >> 2));
}
cpos_x += bitmap.width + 5;
pen_x += slot->advance.x >> 6;
}

SaveBitmapFile( memDC, hBitmap, filename);
}

結果は次のとおり。いい感じになった気がする。


f:id:Tnzk:20081005205454p:image


課題点


神経質な人にはわかると思うのだけど、コンマの位置がやけに高い(ぼくは言われるまでまったく気がつかなかった)。これはビットマップデータの高さを基準に座標を決定しているから起きる問題で、文字にはそれとは別に「ベースライン」という考え方があるらしい。これもやはりFreeTypeによって取得できるようなので、次はベースラインでの配置を行おうと思う。




ひょんなことからモーションキャプチャシステムみたいなものを作ることになって、その実験として小規模なものを開発しているところ。キャプチャシステム自体は未完成です。


準備するもの


以下のページに従って開発環境を整える。


インストール - OpenCV@Chihara-Lab.


具体的にインストールしたものは次のとおり。



  • Visual C++ 2005 Express Edition

    • SP1 for Windows Vista

    • Platform SDK



  • OpenCV

  • IPL


経験のある人は知ってると思うけど、Platform SDKのインストールには異常に時間がかかるので、本の一冊や二冊は準備しておくといいと思う。


開発環境


使用したマシンは工人舎のSH8WP12A。USBカメラが手元にないと思っていたところ、こいつが内臓していたのでこれを使うことにした。


ところが内臓しているため、PlatformSDKのインストール中にカメラを別のマシンで使うことができず、もう一台のマシン(以前にPlatform SDKインストール済み)で先んじてテストしておくことができなかった。


あとOpenCVのインストーラをメディアにコピーしておくのを忘れたために、これをインストールすることもできなかったので、ManyCamsを使ってRubyからキャプチャして時間を潰してた。これは本当に時間を無駄にしたと思うので、事前に準備しておくといいと思う。


サンプルをチェキ!


OpenCVをインストール後、パスを通せばサンプルが動くということなのでチェックしてみた。


OpenCV/samples/を開くと、ソースファイルやプロジェクトファイルなどにまぎれて実行ファイルがあったので、ビルドなどをする前にこっちを見てみた。エッジの抽出やフラクタルの描画みたいなことをやっていて面白かったので見てみるといいと思う。


で、肝心のビルドはというと、これは失敗した。読み取り専用がどうとか、見慣れないエラーが出てたので、そろそろ時間も遅いし帰るかーという流れに。


部室を出て電車に乗った頃に気づいたんだけど、サンプルが動くというのは「サンプルプロジェクトがビルドできる」という意味ではなく「サンプルの実行ファイルが動作する」という意味だったのかも、と思った。


ここまでが昨日の話。


テスト用プロジェクトの作成


そういう流れで、もう面倒くさいのでプロジェクト作成して依存ファイルの追加設定なども済ませてやろうと思った(今冷静に考えると、これはプロジェクト単位での設定なのでサンプルとは関係ないのよね)。


そのままここに則ってカメラキャプチャをしようとしたのだけど、動作はするしGUIも生成される一方でカメラの映像が取得できない。


色々調べてみると、映像の取得ではなくカメラの取得(デバイスハンドルの取得みたいな感じ)でコケてることが判明。cvCaptureFromCAMはその引数で取得するカメラの種類を指定するのだけど、サンプルではこれが -1 となっていた。引数に -1 を与えると、cvCaptureFromCAMはイイカンジにはからって適当にカメラを取得してくれる。


これが不審だったので、試しに定義されている CV_CAP_ANY, CV_CAP_MIL, CV_CAP_VFW, CV_CAP_IEEE1394をそれぞれ与えて実行してみたところ、CV_CAP_VFW以外は取得に失敗。自動選択されているのはCV_CAP_CFWらしい。


VFWはVideo For Windowsの略で、正攻法ではこいつとDirectShowを組み合わせて実装するらしい。今回はGW中に完成させる必要があるので、手っ取り早く使えるものが欲しいので、別の方法を考えた。


まず、サンプルでカメラが取得できるかどうか調べてみた。camcapture.c(みたいな感じの(うろ覚え))をビルドしてみると、先ほどテスト用プロジェクトで発生したのと同じ状態になった。


ところが、OpenCV/otherlibs/cvcams/samples/にあるサンプルを実行してみると、普通にキャプチャできた。


CvCamを使うことに


どうやらcvcamは廃れる運命にある(具体的にはOpenCVのchangeLogにおいてその撤廃が告知されている)ようなのだけど、別にそんな末永く使うつもりじゃないし、それまでにはDirectShow+VFWに移行できると思うので、今回はcvcamを使うことにした。


cvcamを使うと、普通にキャプチャできていい感じですね。


赤い部分の抽出


カメラベースでマーカーを利用したキャプチャを行おうと考えているので、特定の色を抽出する処理が必要。


(R,G,B) = (255,0,0)な画素だけ残して、あとは(0,0,0)で埋めてしまえばいいんじゃね!? とか思うほどバカではないですが、程度問題で結局のところバカでした。


(R > 100,G,B)な画素ryということで実装してみたところ、当然ながら白も黄色も拾ってしまって話にならず、まずはさらに安直に考えを進めて「G, Bが小さければいいんじゃね?」という発想に至る。


というわけで(R > 100, G < 70, B < 70)の画素を残すようにしてみる。そこそこきれいにはなったものの、影になっている部分(暗い赤)などを拾えず、形状が欠損してしまう。


というわけで今度は比にして考えてみた。100:70ということで( R/G > 1.42 && R/B > 1.42)の画素のみを残してみると、意外とイイカンジになった。比率を調整して、今日は最終的に以下のような感じに。


f:id:Tnzk:20080504221429j:image:w200


赤い領域の抽出(1)


f:id:Tnzk:20080504221428j:image:w200


赤い領域の抽出(2)


画面左下のウィンドウが原画像、左上のウィンドウが抽出した画像を表示している。


あとはこいつらの重心を求めたりして、なんとかして座標値にする。もちろんペンとかだとどこの座標にするかという話にもなるので、ちゃんとマーカーを作ってやる。この辺は明日。


で、問題点は同様に実装できるのがR,G,Bを抽出する場合のみということ。紫とかそういう色が欲しいときにどうすればいいかというのも問題なので、そこも明日の課題。比率からもう少し考えを進めれば実現できると思うけど、どうだろう。


今回のことですごく思ったんだけど、画像処理(というほどのことはやっていないけど)は楽しいよ!3Dレンダリングが楽しいのと同じで、処理結果が目に見えるのはうれしい。


このアーカイブについて

このページには、過去に書かれたブログ記事のうち画像処理カテゴリに属しているものが含まれています。

前のカテゴリは映画です。

次のカテゴリはです。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。

ウェブページ

Powered by Movable Type 4.32-ja