わたくし、ゆるやかなアンチマカーで、よってど素人なんですが、かの孫子でも「彼を知り己を知れば百戦殆うからず」と申します。それに携帯端末周りは以前から興味があり、知識を広げるのは悪くないと思いました。
色々あって会社からMacBookを買ってもらってしまったこともあり、始めてみることにしました。その第一歩の備忘録です。
iOS向けフレームワークのビルド
こちらの通りです。次期3.0.0向けですが、今の安定板(2.4.x)でもほぼこのままで問題ないはずです。
http://docs.opencv.org/trunk/doc/tutorials/introduction/ios_install/ios_install.html
"Required Packages" にあるように、XcodeとCMakeが必要です。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を追加します。
処理に使う画像については、ドラッグ&ドロップで持って来るのが楽です。おそらく"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の画像なので思い切りはみ出ています。