2016年6月25日土曜日

iOS swift で OpenCV を使う

[This report is also available in English.]

最近は Apple に Developer 登録しなくても自作 iOS アプリを実機で動かせるようになっています。

しかも、どうやら Xcode で OpenCV が使えるようです。



という訳で、ふと思い立ったので OpenCV を使った顔検出アプリを作ってみます。

・・・と思ったけど、顔認識の説明が長くなるのでとりあえずソーベルエッジ。

今回のサンプルコードはGitHubから取得できます。

使う機材


  • Mac

    ご家庭で余っているMacを使います。
  • iPhone または iPad

    どこのご家庭にも2,3台あると思います。
  • Xcode

    iOS アプリの開発環境です。

    開発だけなら無料です。凄いです。

    AppStore からインストール。
  • OpenCV

    画像処理界のグローバルスタンダード。
    今回の方法だといまのところ 3.0.0 が入るようです。




Cocoapods をインストール


OpenCV 単独であればライブラリを直接放り込んでも使えるようですが、

せっかく Cocoapods というパッケージ管理ツールがあるので使ってみます。


まずはターミナルを起動。

これ。




インストールしようとすると依存関係で何やら大量に入りますが

エラーが出ていないなら気にしないことにします。

iMac:~ $ sudo gem install cocoapods
Fetching: i18n-0.7.0gem (100%)
Successfully installed i18n-0.7.0
   : (中略)
Parsing documentation for cocoapods-1.0.1
Installing ri documentation for cocoapods-1.0.1
23 gems installed
iMac:~ $ pod setup



Project を作成


Xcode 上でアプリを開発する際、各アプリのソースコード等が入ったデータ一式を Project と言います。

OpenCV は Project 毎にインストールするので先に Project を作成します。



このアイコンから Xcode を起動して

Create a new Xcode Project を選択します。





とりあえず Single View Application で。





アプリ名などは適当に。





Projectが出来ました。

アプリを公開するときは細かく設定する必要がありますが今回は試しに動かすだけなのでデフォルトのままで。





OpenCV のインストール


一旦 Xcode の Project を閉じます。

再びターミナルに戻って Project の xcodeproj が入っているディレクトリ内で
Podfile を作り、OpenCV をインストールします。

対応する iOS のバージョンは 7.0 以上にしました。

6.x 以下に対応させようとすると泥沼化します。

iMac: ~ $ cd FaceRecogApp
iMac:FaceRecogApp $ vi Podfile
iMac:FaceRecogApp $ cat Podfile
target "FaceRecogApp" do
echo 'platform :ios, "7.0"
pod 'OpenCV' > Podfile
end
iMac:FaceRecogApp $ pod install



結構時間が掛かりますがエラーなく終了すればインストール完了。

xcworkspace というファイルをダブルクリックして Xcode で開きます。



アプリでビデオキャプチャする


よくある iOS アプリの Hello World では「Storyboard で~」という感じで始まりますが、

iOS アプリの経験がない人間が Storyboard をいじり始めると余計に訳わからなくなるので

いきなりコードを書いていきます。



Xcode 上でアプリの Project 内に ViewController.swift というファイルがあるので開きます。

ここにこんな感じでガシガシ書いていくと、とりあえずカメラプレビューできるアプリが出来上がります。

コードの意味はコメントを参照。

import UIKit
import AVFoundation // カメラ映像用ライブラリ

class ViewController: UIViewController {

var input:AVCaptureDeviceInput! // 映像入力

var cameraView:UIView! // プレビュー表示用View
var session:AVCaptureSession! // セッション
var camera:AVCaptureDevice! // カメラデバイス

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}

// 画面の初期化
override func viewWillAppear(animated: Bool) {

// プレビューを画面全体に引き延ばす
let screenWidth = UIScreen.mainScreen().bounds.size.width;
let screenHeight = UIScreen.mainScreen().bounds.size.height;
cameraView = UIView(frame: CGRectMake(0.0, 0.0, screenWidth, screenHeight))

// 背面カメラを探す
// 自撮りにしたければ AVCaptureDevicePosition.Front
session = AVCaptureSession()
for captureDevice: AnyObject in AVCaptureDevice.devices() {
if captureDevice.position == AVCaptureDevicePosition.Back {
camera = captureDevice as? AVCaptureDevice
break
}
}

// カメラ映像を取得
do {
input = try AVCaptureDeviceInput(device: camera) as AVCaptureDeviceInput
} catch let error as NSError {
print(error)
}

if( session.canAddInput(input)) {
session.addInput(input)
}

// 画面に表示
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer.frame = cameraView.frame
previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill

self.view.layer.addSublayer(previewLayer)

session.startRunning()
}
}



ビルド、実行、そして失敗


カメラのキャプチャをするのでシミュレータでは動きません。

Mac に USB ケーブルで iPhone を接続してから Xcode の実行ボタンを押します。



・・・が、ビルドに失敗します。

Xcode7 では bitcode という中間データを使用しますが、

OpenCV は bitcode を含んでいないのでリンクエラーになるようです。



Xcode 上でプロジェクト名を選択し、Build Settings から bitcode を disable にします。





で、再度試すと今度は iPhone 上でアプリが起動します。



処理対象の画像を取り出す


このままだと撮影した映像はそのまま画面に出てしまうので、
UIImage という画像オブジェクトの形でデータを取り出します。
// バッファ処理用 delegate を作れるように AVCaptureVideoDataOutputSampleBufferDelegate を継承
class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate {

var input:AVCaptureDeviceInput! // 映像入力
var output:AVCaptureVideoDataOutput! // 映像出力



撮影した映像を queue に放り込んで delegate で処理するように変更。

処理が重くなって追いつかなくなるので間に合わなかったフレームは捨てます。

本当はフレームレートを落とした方が良いのですがやってません。

本体が発熱するかも。

// 画面の初期化
override func viewWillAppear(animated: Bool) {

// プレビューを画面全体に引き延ばす
let screenWidth = UIScreen.mainScreen().bounds.size.width;
let screenHeight = UIScreen.mainScreen().bounds.size.height;
cameraView = UIImageView(frame: CGRectMake(0.0, 0.0, screenWidth, screenHeight))

// 背面カメラを探す
// 自撮りにしたければ AVCaptureDevicePosition.Front
session = AVCaptureSession()
for captureDevice: AnyObject in AVCaptureDevice.devices() {
if captureDevice.position == AVCaptureDevicePosition.Back {
camera = captureDevice as? AVCaptureDevice
break
}
}

// カメラ映像を取得
do {
input = try AVCaptureDeviceInput(device: camera) as AVCaptureDeviceInput
} catch let error as NSError {
print(error)
}

if( session.canAddInput(input)) {
session.addInput(input)
}

// カメラ映像を画像処理に回す.
output = AVCaptureVideoDataOutput()
output.videoSettings = [kCVPixelBufferPixelFormatTypeKey : Int(kCVPixelFormatType_32BGRA)]

// デリゲート
let queue: dispatch_queue_t = dispatch_queue_create("videoqueue" , nil)
output.setSampleBufferDelegate(self, queue: queue)

// 処理が遅れたフレームを捨てる
output.alwaysDiscardsLateVideoFrames = true

// セッションに出力を追加
if session.canAddOutput(output) {
session.addOutput(output)
}

// カメラ向きを固定
for connection in output.connections {
if let conn = connection as? AVCaptureConnection {
if conn.supportsVideoOrientation {
conn.videoOrientation = AVCaptureVideoOrientation.Portrait
}
}
}

// 画面に表示 → captureOutputで出力
self.view.addSubview(cameraView)
//let previewLayer = AVCaptureVideoPreviewLayer(session: session)
//previewLayer.frame = cameraView.frame
//previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
//
//self.view.layer.addSublayer(previewLayer)

session.startRunning()
}



delegate で画像バッファを拾って UIImage を作ります。

正直言って、このコードは何をやっているのか全く分かりません。

訳も分からずサンプルコードを swift2 に対応させただけ。



作った UIImage は最後の ImageProcessing.SobelFilter という行で OpenCV に渡して処理します。

// 表示更新
func captureOutput(captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, fromConnection connection: AVCaptureConnection!) {

let image:UIImage = self.captureImage(sampleBuffer)

dispatch_async(dispatch_get_main_queue()) {
// 描画
self.cameraView.image = image
}
}

// sampleBuffer から UIImage を作成
func captureImage(sampleBuffer:CMSampleBufferRef) -> UIImage {

// 画像取得
let imageBuffer: CVImageBufferRef = CMSampleBufferGetImageBuffer(sampleBuffer)!

// ベースアドレスをロック
CVPixelBufferLockBaseAddress(imageBuffer, 0 )

// 画像データ情報
let baseAddress: UnsafeMutablePointer<Void> = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0)

let bytesPerRow: Int = CVPixelBufferGetBytesPerRow(imageBuffer)
let width: Int = CVPixelBufferGetWidth(imageBuffer)
let height: Int = CVPixelBufferGetHeight(imageBuffer)
let bitmapInfo = CGImageAlphaInfo.PremultipliedFirst.rawValue|CGBitmapInfo.ByteOrder32Little.rawValue as UInt32

//RGB色空間
let colorSpace: CGColorSpaceRef = CGColorSpaceCreateDeviceRGB()!
let newContext: CGContextRef = CGBitmapContextCreate(baseAddress,                                      width, height, 8, bytesPerRow, colorSpace, bitmapInfo)!
// Quartz Image
let imageRef: CGImageRef = CGBitmapContextCreateImage(newContext)!

// UIImage
let cameraImage: UIImage = UIImage(CGImage: imageRef)

// OpenCV で SobelFilter を掛ける
let resultImage: UIImage = ImageProcessing.SobelFilter(cameraImage)

return resultImage

}



OpenCV を組み込む


OpenCV は swift 非対応で、swift には C++ を直接書けないのでそのままでは繋がりません。

まず、Xcode のファイルリストを右クリックして「New File」で C++ 用のファイルを作ります。

その際に Xcode が自動的にヘッダファイルを作ってくれます。

今回、ImageProcessing.m というファイルを作成したら

ヘッダファイル名は{プロジェクト名}-Bridging-Header.h でした。なんでやねん。



ヘッダファイルにクラスを定義します。

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface ImageProcessing: NSObject
+(UIImage *)SobelFilter:(UIImage *)image;
@end



C++ のファイルに実装したいところですが、

拡張子が自動的に「.m」になっており C++ が書けません。

画面右上の「Identity and Type」で拡張子を「.mm」に変更します。



で、UIImage をOpenCVの画像バッファに変換して

画像処理してUIImageに戻して返します。

素晴らしきかなOpenCV。超簡単。

#import <Foundation/Foundation.h>
#import >UIKit/UIKit.h>

#import "FaceRecogApp-Bridging-Header.h"

#import <opencv2/opencv.hpp>
#import <opencv2/imgcodecs/ios.h>

@implementation ImageProcessing: NSObject

+(UIImage *)SobelFilter:(UIImage *)image{

//UIImageをcv::Matに変換
cv::Mat mat;
UIImageToMat(image, mat);

//グレースケールに変換
cv::Mat gray;
cv::cvtColor(mat, gray, cv::COLOR_BGR2GRAY);

// エッジ抽出
cv::Mat edge;
cv::Canny(gray, edge, 100, 200);

//cv::Mat を UIImage に変換
UIImage *result = MatToUIImage(edge);
return result;
}
@end



ところで、ググると「#import <opencv2/highgui/ios.h>」と

なっている説明が大半ですが、OpenCV 3.0.0 では

imgcodecs に移っています。



また、「cv::cvColor」を使っているサンプルコードが多いですが

これも 3.0.0 では「cv::cvtColor」です。



実行


iPad で動かすとこんな感じになります。


2 件のコメント:

  1. SECRET: 0
    PASS: 32c23f5b93892da410e0b455d2481291
    始めまして、よしなりと申します。
    たけやす様の記事を読み、アプリ開発に興味を持ちました。
    まずは記事のカメラアプリを動かして見ようとしたのですが、添付画像の様なエラーが出てしまいます。
    色々と調べてみたのですが、解決には至りませんでした。
    解決方法についてご助言頂けないでしょうか。
    宜しくお願い致します。
    ---動作環境---
    OS:El Capitan 10.11.6
    Xcode:7.3.1
    OpenCV:3.1.0.1

    返信削除
  2. SECRET: 0
    PASS: 74be16979710d4c4e7c6647856088456
    >よしなり様
    せっかく試してくださったのにすみません。
    どうやらFC2ブログが「<Void>」の部分をHTMLだと誤解したようで
    自動的に余計な閉じタグ「</Void>」を付け加えてくださりやがっていました。
    FC2ブログに誤解されないように修正したので再度コピペしてみていただけますか。

    返信削除