OpenCV for iOSの使い方

Core Image や vImage 等、iOS SDK の標準フレームワークの機能が充実してきたとはいえ、コンピュータビジョンの分野においてできることの幅広さではやはり OpenCV に軍配が上がります。そんな OpenCV を iOS アプリに導入する手順です。

下記の公式ドキュメントを参考に、自分でやってみて補足を加えました。
[公式ドキュメント1](http://docs.opencv.org/doc/tutorials/introduction/ios_install/ios_install.html#ios-installation) / [公式ドキュメント2](http://docs.opencv.org/doc/tutorials/ios/hello/hello.html#opencvioshelloworld) / [公式ドキュメント3](http://docs.opencv.org/doc/tutorials/ios/image_manipulation/image_manipulation.html#opencviosimagemanipulation)

##準備編：フレームワークをビルドする


###1. Githubからソースコードを取得
githubからcloneします。[^1]

    cd ~/<my_working _directory>
    git clone https://github.com/opencv/opencv.git

[^1]: 以前はItseezアカウントにあったのですが、いつの間にかOpenCVアカウントになってました

###2. ビルドの準備
ルートディレクトリに、Xcodeのバンドル内にあるDeveloperフォルダへのシンボリックリンクを作成します。（ビルドスクリプトが参照するため）

    cd /
    sudo ln -s /Applications/Xcode.app/Contents/Developer Developer


###3. ビルド
ビルドスクリプトを実行します。

    cd ~/<my_working_directory>
    python opencv/platforms/ios/build_framework.py ios

ビルドが成功すると、最後に `** INSTALL SUCCEEDED **` と出て、 `~/<my_working_directory>/ios/opencv2.framework` にフレームワークができています。

- ※1: もしスクリプト実行時に `sh: cmake: command not found` というエラーが出る場合は、cmake が入ってないのでインストールが必要です。homebrew の場合は

    $ brew install cmake

でインストールが始まります。

- ※2: pythonのエラーが出た場合はスクリプトを実行する**Pythonのバージョンが合ってない可能性**があります。私はOpenCV 3.1のビルドスクリプトをPython v3.5.1で実行しようとして以下のようなエラーが出ました。

> TypeError: cannot use a string pattern on a bytes-like object

pyenvでPython 2.7に切り替えてこのエラーは解消されました。

- ※3: OpenCV 3.1のビルドスクリプトがxcodebuildでコケました。マスターの最新コミット（2016.12.2現在）でも同様。


###バージョン確認方法

`opencv2.framework/Versions/A/Resources/Info.plist` に書かれているフレームワークバンドルのバージョン＝OpenCVのバージョンとなります。

たとえば 3.0.0 の場合は Info.plist はこうなっています。

```
<key>CFBundleVersion</key>
<string>3.0.0</string>
<key>CFBundleShortVersionString</key>
<string>3.0.0</string>
```

##導入編：Xcode プロジェクトにフレームワークを追加する
###1. フレームワークを追加
プロジェクトに opencv2.framework を追加します。

###2. 実装ファイルの拡張子を.mmにする

C++ のコードを書くため、実装ファイルの拡張子を .m から .mm に修正します。

### 3. ヘッダファイルをインポート

    #ifdef __cplusplus
    #import <opencv2/opencv.hpp>
    #endif

#### （2018.10追記）ビルド時に"Expected identifier"エラーが発生する場合

OpenCV 3.2で、次のようなビルドエラーが発生しました。

> Expected identifier

`# warning`で次のようなメッセージが仕込まれていました。

> Detected Apple 'NO' macro definition, it can cause build conflicts. Please, include this header before any Apple headers.

解決方法として、**`<opencv2/opencv.hpp>`のインポートを他のAppleヘッダのインポートよりも先に書く**、とのこと。

次のようにすると解決しました。

```
#ifdef __cplusplus
#import <opencv2/opencv.hpp>
#endif
#import "xxxx.h"
```


##実践編：簡単な画像処理を実行してみる
### <del>1. UIImage 型の画像を cv::Mat 型に変換するメソッドを定義</del>

<del>UIImage 型の画像を cv::Mat 型に変換するメソッドを定義します。（cv::Mat は、画像のビットマップデータへのポインタと、幅，高さ，ビット深度などの様々なプロパティを保持するクラスです。）</del>

```cpp:実装不要になりました
- (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;
}
```

現行バージョンでは`opencv2/imgcodecs/ios.h`に、

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

が既に用意されています。変換関数を自分で実装する必要はなくなりました。


### <del>2. cv::Mat 型の画像を UIImage 型に変換するメソッドを定義</del>

<del>cv::Mat を UIImage に変換するメソッドも定義しておきます。</del>

```cpp:実装不要になりました
- (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;
}
```


現行バージョンでは`opencv2/imgcodecs/ios.h`に、

```cpp
UIImage* MatToUIImage(const cv::Mat& image);
```

が既に用意されています。変換関数を自分で実装する必要はなくなりました。


### 3. グレースケール画像に変換する

色空間変換を行う cvtColor() というメソッドと、上記で実装した UIImage と cv::Mat を相互変換するメソッドを用いて、下記のように UIImage をグレースケールに変換して UIImage として出力する処理を実装できます。

```cpp
cv::Mat srcMat;
UIImageToMat(srcImage, srcMat);
cv::Mat grayMat;
cv::cvtColor(srcMat, grayMat, CV_BGR2GRAY);
    
return MatToUIImage(grayMat);
```

## 応用編：漫画カメラ風に写真を加工する

下記記事に、OpenCVを用いて**漫画カメラ風に加工する手順**を書いたので、よろしければご参照ください。

[たったの6ステップ！『漫画カメラ』風に写真を加工するiPhoneアプリの作り方](http://d.hatena.ne.jp/shu223/20121129/1354145960)

![manga](http://img.f.hatena.ne.jp/images/fotolife/s/shu223/20121129/20121129210530.png)

## トラブルシューティング

### 画像が90度回転してしまう

`UIImage` -> `cv::Mat`の変換の際、`UImage`の`imageOrientation`が`UIImageOrientationUp`の場合は問題ないが、`UIImageOrientationLeft`だったり`UIImageOrientationRight`だったりすると、90°（あるいは270°）回転してしまう。

解決方法としては、あらかじめ回転を画像に反映してから変換する。

```objc
- (UIImage *)normalizedImage {
    if (self.imageOrientation == UIImageOrientationUp) return self; 

    UIGraphicsBeginImageContextWithOptions(self.size, NO, self.scale);
    [self drawInRect:(CGRect){0, 0, self.size}];
    UIImage *normalizedImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return normalizedImage;
}
```

```objc
cv::Mat srcMat;

// これだと結果が90°回転してしまう場合がある
// UIImageToMat(self, srcMat);

// こうするとOK
UIImageToMat([self normalizedImage], srcMat);

return MatToUIImage(srcMat);
```

参考

- https://stackoverflow.com/questions/5427656/ios-uiimagepickercontroller-result-image-orientation-after-upload/10611036#10611036
- https://stackoverflow.com/questions/14332687/converting-uiimage-to-cvmat