XcodeでiOS向けOpenCV

わたくし、ゆるやかなアンチマカーで、よってど素人なんですが、かの孫子でも「彼を知り己を知れば百戦殆うからず」と申します。それに携帯端末周りは以前から興味があり、知識を広げるのは悪くないと思いました。

色々あって会社からMacBookを買ってもらってしまったこともあり、始めてみることにしました。その第一歩の備忘録です。

iOS向けフレームワークのビルド

こちらの通りです。次期3.0.0向けですが、今の安定板(2.4.x)でもほぼこのままで問題ないはずです。
http://docs.opencv.org/trunk/doc/tutorials/introduction/ios_install/ios_install.html

"Required Packages" にあるように、XcodeCMakeが必要です。XcodeはAppStoreでインストールします。CMakeはCMakeの公式ページからダウンロードしました(http://www.cmake.org/cmake/resources/software.html)。

Hello world

これもこちらの通り。
http://docs.opencv.org/doc/tutorials/ios/hello/hello.html

Single View Applicationでプロジェクトを作るのが、もっとも簡単そうです。そのあと、ページで説明しているように、前節のビルドによって作れたはずのopencv2.frameworkを追加します。
f:id:Schima:20140519002122p:plain

処理に使う画像については、ドラッグ&ドロップで持って来るのが楽です。おそらく"Copy items into destination group's folder (if needed)"のチェックは入れたほうが良さげです。

ただしこのページ、事前準備については書いてありますが、プログラムはアラートダイアログを出しているだけで、いまひとつまだ世界に挨拶するには声が小さいです。

viewDidLoadで背景画像を設定している箇所を、わかりやすくばらすとこうなります。このUIImageを望むように画像処理できる段階まで行けば、世界に挨拶しても恥ずかしくありません。

UIImage *img = [UIImage imageNamed:@"lenna.png"];
self.view.backgroundColor = [UIColor colorWithPatternImage: img];

cv::Mat に画像を読み込む

C++を使えるようにする

現在編集しているのはViewController.mです。これをViewController.mmに変更します。これによりコードがObjective-C++として扱われるようになり、C++のコードも混ぜて書くことができます。

画像のパスを取得

画像をcv::Matに読み込むだけなら簡単簡単、とこのように書きましたが、動きません。パスが無効のため、空で返ります。

cv::Mat src = cv::imread("lenna.png");

先ほどプロジェクトに追加したつもりの画像は、リソースファイルとして扱われるようで、その場所を指し示すように世話しないといけないようです。

絶対パス (/Users/hoge/piyo/fuga.jpg など) で指定すればこの場は乗り切れますが、実デバイスになるとダメになるに決まっているので、なんとか相対パスでの決着を図りたいところです。

調べた成果が以下です。この2つはどちらも、指定した画像ファイルを読み込みcv::Matとして返す関数です。上の方が直接ファイル名を渡せて気持ちが良いのですが、下の方が望ましいようです。

static cv::Mat loadMatFromFile(NSString *fileName)
{
    NSString *resourcePath = [[NSBundle mainBundle] resourcePath];
    NSString *path = [resourcePath stringByAppendingPathComponent:fileName];
    const char *pathChars = [path UTF8String];
    return cv::imread(pathChars);
}
static cv::Mat loadMatFromFile(NSString *fileBaseName, NSString *type)
{
    NSString *path = [[NSBundle mainBundle] pathForResource:fileBaseName ofType:type];
    const char *pathChars = [path UTF8String];
    return cv::imread(pathChars);
}

使い方はこのようになります。

cv::Mat mat1 = loadMatFromFile(@"lenna.png");
cv::Mat mat2 = loadMatFromFile(@"lenna", @"png");

UIImageでは、この辺を後ろでうまくやってくれているようですね。

または、画像読み込みはUIImageに任せ、次に述べる変換関数でcv::Matにする作戦でも良いと思います。

cv::Mat から UIImage* への変換

cv::Matから画像処理をかけるのは省略します。それが終わったとして、最後に表示のためUIImageに戻すという仕事が発生します。

最近のOpenCVにはその関数がすでに定義されています。opencv2/highgui/ios.hをimportします。この中には、以下の2つの関数があります。

UIImage* MatToUIImage(const cv::Mat& image);
void UIImageToMat(const UIImage* image,
                         cv::Mat& m, bool alphaExist = false);

これにより以下のようにしてUIImageに変換ができます。なお、cv::Matは画素の並びがBGRで、UIImageはRGBを期待します。単純に行うとおそらくレナさんが青くなってしまうので、BとRの入れ替え処理も含めています。

static UIImage *toUIImage(const cv::Mat &m)
{
    cv::Mat mm;
    cv::cvtColor(m, mm, CV_BGR2RGB);
    return MatToUIImage(mm);
}

Mat <-> UIImage 変換の自前定義

最初は変換関数が用意されていると知らず、以下のページをベースに、動かない箇所をいじって自分で用意していました。参考までに載せておきます。
http://code.opencv.org/svn/gsoc2012/ios/trunk/HelloWorld_iOS/HelloWorld_iOS/ViewController.mm

static UIImage *matToUIImage(const cv::Mat &m)
{
    CV_Assert(m.depth() == CV_8U);
    NSData *data = [NSData dataWithBytes:m.data length:m.step*m.rows];
    CGColorSpaceRef colorSpace = m.channels() == 1 ?
        CGColorSpaceCreateDeviceGray() : CGColorSpaceCreateDeviceRGB();
    CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);
    
    // Creating CGImage from cv::Mat
    CGImageRef imageRef = CGImageCreate(m.cols, m.rows, 8, m.elemSize()*8,
                                        m.step[0], colorSpace, kCGImageAlphaNoneSkipLast|kCGBitmapByteOrderDefault, provider, NULL, false, kCGRenderingIntentDefault);
    UIImage *finalImage = [UIImage imageWithCGImage:imageRef];
    CGImageRelease(imageRef);
    CGDataProviderRelease(provider);
    CGColorSpaceRelease(colorSpace);
    return finalImage;
}

static cv::Mat uiImageToMat(const UIImage *image)
{
    CGColorSpaceRef colorSpace = CGImageGetColorSpace(image.CGImage);
    CGFloat cols = image.size.width;
    CGFloat rows = image.size.height;
    
    cv::Mat m = cv::Mat(rows, cols, CV_8UC4);
    CGContextRef contextRef = CGBitmapContextCreate(m.data, m.cols, m.rows, 8, m.step[0], colorSpace, kCGImageAlphaNoneSkipLast | kCGBitmapByteOrderDefault);
    CGContextDrawImage(contextRef, CGRectMake(0, 0, cols, rows), image.CGImage);

    // remove 4th channel
    cv::Mat result;
    cv::cvtColor(m, result, CV_RGBA2RGB);
    return result;
}

コードまとめ

#import "ViewController.h"
#import <opencv2/opencv.hpp>
#import <opencv2/highgui/ios.h>

static cv::Mat loadMatFromFile(NSString *fileName)
{
    NSString *resourcePath = [[NSBundle mainBundle] resourcePath];
    NSString *path = [resourcePath stringByAppendingPathComponent:fileName];
    const char *pathChars = [path UTF8String];
    return cv::imread(pathChars);
}
static cv::Mat loadMatFromFile(NSString *fileBaseName, NSString *type)
{
    NSString *path = [[NSBundle mainBundle] pathForResource:fileBaseName ofType:type];
    const char *pathChars = [path UTF8String];
    return cv::imread(pathChars);
}
// カラー画像の時は使いましょう
static cv::Mat Bgr2Rgb(cv::Mat m)
{
    cv::Mat result;
    cv::cvtColor(m, result, CV_BGR2RGB);
    return result;
}

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    // Do any additional setup after loading the view, typically from a nib.
    
    cv::Mat src = loadMatFromFile(@"lenna.png");
    cv::Mat gray, canny;
    
    cv::cvtColor(src, gray, CV_BGR2GRAY);
    cv::Canny(gray, canny, 50, 200);

    UIImage *img = MatToUIImage(canny);
    
    self.view.backgroundColor = [UIColor colorWithPatternImage: img];
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

またCannyにしてしまいました。

実行してエミュレータにこう表示されれば成功です。幅500pxの画像なので思い切りはみ出ています。