読者です 読者をやめる 読者になる 読者になる

あらみそかもすぞ

まじめにふまじめ

アプリ開発をしながら学ぶ,iOSのためのOpenCV 1

iOSのためのOpenCVについて適当なアプリを作りながら勉強したので,不明な点やわかったところについて以下にまとめておく.
付け焼き刃な情報だと思うので,何か間違いがあれば指摘をよろしくお願いします.

まず,OpenCVとは何なのか.
Wikipedia - OpenCV
Open Source Computer Vision Libraryの頭文字をとってOpenCV.C,C++JavaPython用の画像処理を支援してくれるオープンソースなライブラリなので,先人の知恵がつまっているようだ.
ここに,Objective-Cは含まれてはいないのだけれど,Xcodeは様々な言語を扱うことができるので,ライブラリを使うことができる.

OpenCVの概要がわかったような気がしたので,ここからは先人の知恵をお借りし,簡単なiOS向けアプリを作っていくことにする.イメージはこんなものだ.

「ボタンを押したときに,カラー画像をモノクロ画像にするような画像処理を行い,それを表示する」ようなアプリだ.間違っても,もとからカラーとモノクロ画像を用意しておいて差し替える,なんてことはしない.

ちなみに開発環境はXcode 4.6.2を用い,OpenCVライブラリは2.4.5で,動作確認はiOSシミュレータ 6.0とiPhone5iOS6.1.4)で行った.

さて,ひとまず適当なXcodeプロジェクトを作成する.
  1. Xcodeを起動して,Create a new Xcode projectをクリック.
  2. 今回はiOS向けのアプリを作成するので,iOSのApplicationを選択する(図中1).
    次にSingle View Applicationを選択する(図中2).ちなみにここではプロジェクトのテンプレートを決めている.今回選んだのはSingle Viewとあるので,1画面だけのアプリを生成してくれる.
    テンプレートを選択したらNext(図中3).
  3. 次にプロジェクトの設定を行う.設定が終わったらNext.
    Product Name:アプリの名前.
    Organization Name:会社とか組織の名前.
    Company Identifier:"ドメイン.*"で記入.とりあえず触ってみるだけの人は,"com.自分の名前.*"と適当に記入しておく.
    Class Prefix:今回はひとまず空で.
    Devices:今回はiPhoneで.
    Use Storyboards:ストーリーボードなるものを使用する.後で使用するのでひとまずチェック.
    Use Automatic Reference Counting:自動でメモリを解放してくれるのでひとまずチェック.
    Include Unit Tests:テストを走らせることができる.今回は触れないので,以下の画像ではチェックがあるが,チェックしなくてもいい.

    私は上記の画像のように記入を行った.以下,プロジェクト名は「ImageEditor」とする.
  4. プロジェクトの保存場所を決める.
    Source Controlのチェックはとりあえずチェックをいれておくと,後々プロジェクトの管理が便利になる(ローカルリポジトリの有無の設定)が,今回はチェックなしでOK.
    Createを選択するとプロジェクトの作成が始まるので,少し待つ.
  5. 最後に,きちんとプロジェクトが生成できたか確認を行う.
    ウィンドウの左上にあるRun,Stopでアプリを走らせることができるので,Runをクリックする.iOSシミュレータが起動するので,真っ白な画面が起動するまで環境によっては気長に待つ.確認できたらStopで停止させる.
次に,今作成した「ImageEditor」でOpenCVライブラリが使えるように環境を整える.
前の記事にも書いたけれど,秋山ブログ [Xcode4,Objective-C++] OpenCV2.4.1をiOSで使うを参考に行う.
  1. OpenCVコンパイル」を1〜6まで行う.
  2. Xcodeプロジェクトの設定」の1,2を行う.
  3. Xcodeプロジェクトの設定」の3では,今回必要な
    「libopencv_core.a」
    「libopencv_imgproc.a」
    の2つを追加する.OpenCVのライブラリ全てを選択してもいいが,その分アプリの容量が増えてしまうため,必要なライブラリのみをいれるのがよい.
  4. Xcodeプロジェクトの設定」の4,5を行う.
これでOpenCVライブラリを作成したプロジェクトで使用する環境が整った.
次は,グレースケールに変換を行いたい画像を用意する.
  1. 使用する画像を用意する.
    モノクロに変換する予定なので,カラーな画像を適当に用意する.私は自分のアイコン画像を使用した.
  2. 画像をプロジェクトに追加する.Finderからドラッグ&ドロップでOK.

  3. そうするとファイルを追加するときのオプションを決めるウィンドウが出てくる.
    DestinationのCopy items into なんちゃらのチェックを入れることを忘れずに行う.これは元のファイルをコピーして追加するというものだ.プロジェクトで使用したファイルが散らばらなくて済むので,おすすめだ.元のファイルは登録が済んだら好きにしてもらってかまわない.
  4. ファイルがリストに追加されているか確認を行う.
次に,追加した画像を表示してみる.
  1. リストからMainSroryboard.storyboardを選択する.
    ストーリーボードを使うことで,視覚的に部品を設置することができる.
  2. Object libraryのImage ViewをViewにドラッグ&ドロップ.
    Image Viewとは部品の1つで,画像を表示することができる.
  3. Image Viewに先ほど追加した画像を表示する.Imageで画像の名前を選択する.

    表示できた.

    がサイズがおかしいかもしれない.
  4. そこでImage Viewの調整を行う.
    表示する位置や,大きさなどが修正できる.

    私はアイコンだったのでこのくらいにした.
次は押すと画像がモノクロになるためのボタンを配置しておく.
  1. Object libraryのRound Rect ButtonをViewにドラッグ&ドロップ.
    Round Rect Buttonとは部品の1つだ.これを押したとき,押して離したときなどに処理を行うことができる.
  2. デフォルトだとButtonと表示され,なんだかそっけない.好きな表示名に変更する.

    長過ぎると省略されてしまうので,好みに応じて調整する.
これで下準備が終わった.見た目も整ったので,早速Runしてみるといい.無事に表示されただろうか?
先ほど設置したボタンをクリックしてみるとわかるが,もちろん押しても何もおこらない.次はボタンを押したときに反応するようにしていく.
  1. 左上のEditorの真ん中,Assistant editorをクリックする.
    すると画面が2つに分かれる.クリックで左の画面に表示,option+クリックで右の画面に表示することができるので,左にMainStoryboard.storyboardを,右にViewController.hを表示する.
  2. ViewController.hの@interface内を以下のように書き加える.
    @interface ViewController : UIViewController{

    }

    @end
  3. ImageViewとViewControllerを接続する.
    MainStoryboard.storyboardに表示されているImageViewを選択しておく.
    controlを押しながらImageViewをViewController.hの{}の中へドラッグ&ドロップする.
  4. 吹き出しが表示されるので,NameにImageViewの名前を設定し,Connect.
    ここではimageViewにした.
  5. 次はRound Rect ButtonとViewControllerを接続する.
    MainStoryboard.storyboardに表示されているRound Rect Buttonを選択しておく.
    controlを押しながらRound Rect ButtonをViewController.hの}から@endの間へドラッグ&ドロップする.
  6. 吹き出しが表示されるので,ConnectionをActionに,NameをpushButtonとする.入力が終わったらConnect.
    これでImageViewとRound Rect Buttonの接続が終わった.
  7. 左にViewController.h,右にViewController.mを表示してみると以下のようになっているはずだ.

    ここで,ViewController.mのpushButton内を
    - (IBAction)pushButton:(id)sender {
    NSLog(@"pushButton");
    }
    と記述し,RunしてRound Rect Buttonを押してみてほしい.下記のようにコンソールに文字列が表示されたはずだ.

    pushButtonはRound Rect Buttonを押したときに呼び出されるメソッドだということがわかるだろう.ここで画像処理を行うことにする.いよいよOpenCVライブラリを活用するときがきた.
ところで,Objective-CではUIImageクラスで管理を行う.一方,OpenCVではIplImage,CvMatなどなど使用する言語に応じて色々あるようだが,UIImageクラスはOpenCVには対応していないため,そのまま渡して画像処理を〜というわけにはいかない.今回はOpenCVのcv::Matな型を利用する.実現方法は以下のようなイメージだ.
UIImageな画像をcv::Matな画像に変換して,
cv::Matな画像に画像処理を行い,
cv::Matな画像をUIImageな画像に変換して,表示する.
他のライブラリを使えばモノクロ画像にするくらいの画像処理ならUIImageのままいけるが,OpenCVを使いこなせるようになると後々複雑な画像処理も行うことができる(らしい)ので,ここは1つ我慢だ.
  1. OpenCVライブラリでcv::Matを使うためには,メソッドファイルの拡張子を変更する必要がある.リストからViewController.mを選択し,returnを押すと,名前を変更することができる.ここで「ViewController.m」から「ViewController.mm」に変更する.
  2. UIImageクラスの画像をcv::Matに変換するメソッド「cvMatFromUIImage」を「ViewController.mm」のpushButtonから@endの間に追加する.
    参考元:OpenCV for iOSの使い方 #Objective-C #iOS #OpenCV #AdventCalendar #iPhone #画像処理 - Qiita [キータ]
    - (cv::Mat)cvMatFromUIImage:(UIImage *)image
    {
    CGColorSpaceRef colorSpace = CGImageGetColorSpace(image.CGImage);
    CGFloat cols = image.size.width;
    CGFloat rows = image.size.height;

    cv::Mat cvMat(rows, cols, CV_8UC4); // 8 bits per component, 4 channels

    CGContextRef contextRef = CGBitmapContextCreate(cvMat.data, // Pointer to data
    cols, // Width of bitmap
    rows, // Height of bitmap
    8, // Bits per component
    cvMat.step[0], // Bytes per row
    colorSpace, // Colorspace
    kCGImageAlphaNoneSkipLast |
    kCGBitmapByteOrderDefault); // Bitmap info flags

    CGContextDrawImage(contextRef, CGRectMake(0, 0, cols, rows), image.CGImage);
    CGContextRelease(contextRef);
    CGColorSpaceRelease(colorSpace);

    return cvMat;
    }
  3. cv::Matの画像をUIImageクラスに変換するメソッド「UIImageFromCVMat」を「ViewController.mm」のcvMatFromUIImageから@endの間に追加する.
    参考元:OpenCV for iOSの使い方 #Objective-C #iOS #OpenCV #AdventCalendar #iPhone #画像処理 - Qiita [キータ]
    - (UIImage *)UIImageFromCVMat:(cv::Mat)cvMat
    {
    NSData *data = [NSData dataWithBytes:cvMat.data length:cvMat.elemSize()*cvMat.total()];
    CGColorSpaceRef colorSpace;

    if (cvMat.elemSize() == 1) {
    colorSpace = CGColorSpaceCreateDeviceGray();
    } else {
    colorSpace = CGColorSpaceCreateDeviceRGB();
    }

    CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);

    // Creating CGImage from cv::Mat
    CGImageRef imageRef = CGImageCreate(cvMat.cols, //width
    cvMat.rows, //height
    8, //bits per component
    8 * cvMat.elemSize(), //bits per pixel
    cvMat.step[0], //bytesPerRow
    colorSpace, //colorspace
    kCGImageAlphaNone|kCGBitmapByteOrderDefault,// bitmap info
    provider, //CGDataProviderRef
    NULL, //decode
    false, //should interpolate
    kCGRenderingIntentDefault //intent
    );


    // Getting UIImage from CGImage
    UIImage *finalImage = [UIImage imageWithCGImage:imageRef];
    CGImageRelease(imageRef);
    CGDataProviderRelease(provider);
    CGColorSpaceRelease(colorSpace);

    return finalImage;
    }
  4. UIImage型の画像をモノクロに画像処理し,UIImage型で返すメソッド「greyMatFromUIImage」を「ViewController.mm」のUIImageFromCVMatから@endの間に追加する.
    参考元:OpenCV for iOSの使い方 #Objective-C #iOS #OpenCV #AdventCalendar #iPhone #画像処理 - Qiita [キータ]
    - (UIImage *)greyMatFromUIImage:(UIImage *)srcImage
    {
    cv::Mat srcMat = [self cvMatFromUIImage:srcImage];
    cv::Mat greyMat;
    cv::cvtColor(srcMat, greyMat, CV_BGR2GRAY);

    return [self UIImageFromCVMat:greyMat];
    }
  5. ViewController.mmのpushButton内を
    - (IBAction)pushButton:(id)sender {
    imageView.image = [self greyMatFromUIImage:imageView.image];
    }
    と記述する.
これでRunしてみると以下のようになった.
Buttonをタップして1回目はいいのだが,2回目以降はエラーをはいている.
コンソールには,
ImageEditor[8970] : CGBitmapContextCreate: unsupported parameter combination: 8 integer bits/component; 16 bits/pixel; 1-component color space; kCGImageAlphaNoneSkipLast; 2048 bytes/row.
ImageEditor[8970]
: CGContextDrawImage: invalid context 0x0
とでている.CGBitmapContextCreate,CGContextDrawImageといっているから,これらを使用している「cvMatFromUIImage」に問題があるのだろう.この問題については,「cvMatFromUIImage」,「UIImageFromCVMat」,「greyMatFromUIImage」がそれぞれどのように動作しているかについて考えることで解決の糸口をつかもうと思う.
それにしても,記事がものすごく長くなった気がするので,続きはまた.