C++
Objective-C
iOS
画像処理
OpenCV

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

下記の公式ドキュメントを参考に、自分でやってみて補足を加えました。
公式ドキュメント1 / 公式ドキュメント2 / 公式ドキュメント3

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

1. Githubからソースコードを取得

githubからcloneします。1

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

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"

実践編:簡単な画像処理を実行してみる

1. UIImage 型の画像を cv::Mat 型に変換するメソッドを定義

UIImage 型の画像を cv::Mat 型に変換するメソッドを定義します。(cv::Mat は、画像のビットマップデータへのポインタと、幅,高さ,ビット深度などの様々なプロパティを保持するクラスです。)
実装不要になりました
- (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に、

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

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

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

cv::Mat を UIImage に変換するメソッドも定義しておきます。
実装不要になりました
- (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に、

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

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

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

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

cv::Mat srcMat;
UIImageToMat(srcImage, srcMat);
cv::Mat grayMat;
cv::cvtColor(srcMat, grayMat, CV_BGR2GRAY);

return MatToUIImage(grayMat);

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

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

たったの6ステップ!『漫画カメラ』風に写真を加工するiPhoneアプリの作り方

manga

トラブルシューティング

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

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

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

- (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;
}
cv::Mat srcMat;

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

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

return MatToUIImage(srcMat);

参考


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