2値化(Binarization)

2値化というのは文字通り、画像の色情報を黒と白の2値に単純化する操作である。
画像処理、特に各種の認識処理では画像の情報が単純であればあるほど効率良く行えるので2値化は非常に重要である。

一般的には2値化のことを「モノクローム化」と考えるが(私もそうだった)実際にはモノクローム化した画像はいわゆる「グレイスケール」画像であり黒と白以外の色が含まれる。

    • グレイスケール化された画像

2値化はグレイスケール化された画像情報を閾値に則って文字通り黒と白の二色に分類する処理である。OpenCVでの2値化は

元画像のグレースケール化
グレースケール化した画像を閾値で2値化

という手順となる

    • OpenCVUtil.binarization
+ (IplImage*)binarize:(IplImage*)src threshold:(int)threshold
{
    IplImage *tmp = cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 1);
    IplImage *bin = cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 1);

    /*	カラー画像をグレイスケール画像に変換  */
    cvCvtColor(src, tmp, CV_BGR2GRAY);
    
    /*グレイスケールを閾値で単純化*/
    cvThreshold(tmp, bin, threshold, 255, CV_THRESH_BINARY);

    cvReleaseImage(&tmp);
    return bin;
}
    • 処理結果(閾値128で2値化)

なお、実際にはcvThreshold実行前に画像のノイズを取り去る目的でcvSmoothによる平滑化を施すのがイディオムとなっている。(サンプル画像では違いが分からないので結果は省略する)

    • OpenCVUtil.binarization (平滑化を施した)
+ (IplImage*)binarize:(IplImage*)src threshold:(int)threshold
{
    IplImage *tmp = cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 1);
    IplImage *bin = cvCreateImage(cvGetSize(src), IPL_DEPTH_8U, 1);

    /*	カラー画像をグレイスケール画像に変換  */
    cvCvtColor(src, tmp, CV_BGR2GRAY);

    /* 平滑化 */
    cvSmooth(tmp, bin, CV_MEDIAN, 3);
    cvCopy(bin, tmp);

    /* グレイスケールを閾値で単純化 */
    cvThreshold(tmp, bin, threshold, 255, CV_THRESH_BINARY);

    cvReleaseImage(&tmp);
    return bin;
}

IplImageとUIImageの相互変換

既に色々な方が書いており色々なユーティリティとなっているが、私は取りあえずObjective-Cのクラスユーティリティとして使っている。(そのうち手を入れる予定だ)

    • OpenCVUtil
+ (IplImage*)IplImageFromUIImage:(UIImage*)image
{
    CGImageRef imageRef = image.CGImage;
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    IplImage *iplimage = cvCreateImage(cvSize(image.size.width,image.size.height), IPL_DEPTH_8U, 4 );
    
    CGContextRef contextRef = CGBitmapContextCreate(
                                iplimage->imageData,
                                iplimage->width,
                                iplimage->height,
                                iplimage->depth,
                                iplimage->widthStep,
                                colorSpace,
                                kCGImageAlphaPremultipliedLast|kCGBitmapByteOrderDefault);
    CGContextDrawImage(contextRef,
                       CGRectMake(0, 0, image.size.width, image.size.height),
                       imageRef);
    
    CGContextRelease(contextRef);
    CGColorSpaceRelease(colorSpace);
    
    IplImage *ret = cvCreateImage(cvGetSize(iplimage), IPL_DEPTH_8U, 3);
    cvCvtColor(iplimage, ret, CV_RGBA2BGR);
    cvReleaseImage(&iplimage);
    
    return ret;
}
+ (UIImage*)UIImageFromIplImage:(IplImage*)image
{
    CGColorSpaceRef colorSpace;
    if (image->nChannels == 1)
    {
        colorSpace = CGColorSpaceCreateDeviceGray();
    } else {
        colorSpace = CGColorSpaceCreateDeviceRGB();
        //BGRになっているのでRGBに変換
        cvCvtColor(image, image, CV_BGR2RGB);
    }
    NSData *data = [NSData dataWithBytes:image->imageData length:image->imageSize];
    CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);
    CGImageRef imageRef = CGImageCreate(image->width,
                                        image->height,
                                        image->depth,
                                        image->depth * image->nChannels,
                                        image->widthStep,
                                        colorSpace,
                                        kCGImageAlphaNone|kCGBitmapByteOrderDefault,
                                        provider,
                                        NULL,
                                        false,
                                        kCGRenderingIntentDefault
                                        );
    UIImage *ret = [UIImage imageWithCGImage:imageRef];
    
    CGImageRelease(imageRef);
    CGDataProviderRelease(provider);
    CGColorSpaceRelease(colorSpace);
    
    return ret;
}

cvCvtColorは重要だ。CGImageの色空間は"RGB"でありIplImageの"BGR"とは違うため、この色の変換を忘れるとおかしなことになってしまう。(赤と青が反転する)

使い方は以下のようになるだろう。

UIImage* src = [UIImage imageNamed:@"icebraker.png"];
IplImage* img = [OpenCVUtil IplImageFromUIImage:src];
/** OpenCVによる画像処理 **/
imgProcessed.image = [OpenCVUtil UIImageFromIplImage:img];

IplImageを作成する

OpenCVがXcode上、iPhoneシミュレータで動くようになったのでいろいろと試していこう。
OpenCV上で画像データを格納するための型は現時点(ver2.4.2)で大きく二つある。

  • IplImage構造体

 OpenCV開始当初から用意されているCの構造体。殆どの画像操作関数はこの構造体を受取る。

  • cv::Matクラス

 OpenCV2.0以降追加されたC++のクラス。IplImageと相互変換ができるようになっている。cvは名前空間

書籍やネット上の情報を見ているとIplImageとcv::Matの情報が混在しており、どちらを使ってよいか分からない。C++コンストラクタ/デストラクタに対応したcv::Matクラスがお勧めらしいのだが、呼び出すのはObjective-Cからであり、そうなのではあればC++よりもCの方がいいかなぁと。
あと、IplImageは先達の方々に充分に使い込まれお手本が多いのも魅力だ。

なので私はC++に拘らず、まずはIplImageを使うことを覚えてその上でcv::MatクラスではないとできないことがあればIplImageから変換して使うというスタンスで行こうと思う。

IplImageを生成する

IplImageは構造体なので自体は作るのは簡単だがイメージを作るために最低限必要な情報(画像の大きさ、1pxのビット深度、レイヤ数)を与える関数が用意されているのでそれを使う。

    //IplImageを生成
    IplImage* iplimage = cvCreateImage(cvSize(image.size.width,image.size.height), IPL_DEPTH_8U, 4 );

    //不要になったら必ずリリース
    cvReleaseImage(&iplimage);

    //IplImageを画像ファイルから生成
    iplimage = cvLoadImage("icebraker.png", CV_LOAD_IMAGE_ANYCOLOR);

    //不要になったら必ずリリース
    cvReleaseImage(&iplimage);

C++のようにデストラクタもないしARCも効かないので使い終わったIplImageは必ずリリースする必要がある。

IplImageをUIImageに変換して表示した結果


iOSの世界で画像を表示するのにはUIKit(UIImage)を使うのが一般的だがIplImageはOpenCVの型でありiOSのUIKitでは直接扱えない。なのでIplImageをUIKitの世界に持ち込むには相互変換が必要だ。この辺は次回にでも。

OpenCVをビルドしてプロジェクトに組み込む

少し空いてしまったが、少しずつOpenCVも勉強していこうと思う。
それには実際にサンプルコードを動かしていくのが一番なので、まずはOpenCVを使ったコードが動くように、それもiOS環境上で動くようにしなくてはならない。

導入はHomeBrewかMacPortsを導入してOpenCVをビルドする所からなのだが、私にはMake関係の知識がまるでないので、xcodebuild(とCMake)を使う方法でいくことにする。

以下の作業に行うにこちらのサイトの方法とコードを参考にさせて頂いた。
Computer vision with iOS Part 1: Building an OpenCV framework — Aptogo
[Xcode4,Objective-C++] OpenCV2.4.1をiOSで使う | 秋山ブログ


1. Xcode Command Line Toolsをインストールする

2.CMakeをインストールする
CMake - Cross Platform Make

3.OpenCVのソースコードをダウンロードする
OpenCV - Browse Files at SourceForge.net
アーカイブの展開はどこでも良いが対象プロジェクトの直下に"opencv"という名前のディレクトリにしてコピーしておく

4. ビルド、インストール
2.でCMakeをインストールしたのはここで要求されるからだ。

$ ../opencv/ios/configure-device_xcode.sh
$ xcodebuild -sdk iphoneos -configuration Release -target ALL_BUILD
$ xcodebuild -sdk iphoneos -configuration Release -target install install

対象のディレクトリ直下に"OpenCV_iPhoneOS"が生成できていればOK

5. プロジェクトに組み込む
opencvディレクトリ以下にある"OpenCV.xcodeprj"を(フォルダリファレンス)でプロジェクトに追加する

そして、Header Searchパスに"./OpenCV_iPhoneOS/include/"を追加する。

更にビルドした結果できたライブラリィを"Link Binary With Library"から追加する

これでサンプルコードのコンパイルができるはずなのだが、私の環境ではエラーが出る。

BuildSettingsを見ると使わないはずの"i386 architecture"がOnになっており、対象のプラットホームもMac OS Xになっていたので、これをiOS用に変更して再ビルドした。

これで漸くビルドが通り、サンプルコードも動いた。

オープンソースはとてもとても有り難いんだが馴染みの無いツールを使ったりインストール方法が複数あったりと面倒なことが多い(一度経験してしまえばどうってことはないんだが)。

このようなOSSビルドネタの場合インストールと各種設定だけでブログのエントリが埋まってしまうことが多いのは仕方が無いことだよね。