Edited at

OpenCV for iOSの使い方

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アカウントになってました