14
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

iOSでOpenCVを触る

Last updated at Posted at 2018-09-16

はじめに

Xcodeを用いて,OpenCVを使ったiOSアプリケーションを作成する方法をまとめます.

#1. 新規プロジェクトを作成する
File→New→ProjectからSingle View Appを作成します.
スクリーンショット 2018-09-16 10.44.17.png

アプリ名を設定します.
ProductNameが作成したアプリケーションの名前になります.
LanguageにはSwiftを選択しましょう.
アートボード 2.png

そのままアプリの保存先を設定して,Nextを選択しプロジェクトを立ち上げます.

#2. OpenCVライブラリのインポート
次に作成したプロジェクトにOpenCVのライブラリを追加していきます.
ライブラリはCocoaPodsを通してインストールするため,先にCocoaPodsをインストールします.
Terminalを開いて,以下を入力してください.

sudo gem install cocoapods

もし上が動かないなら,こちらを入力してください.

sudo gem install -n /usr/local/bin cocoapods

これでCocoapodsがインストールできたので,作成したプロジェクトのディレクトリに移動してください.
.xcodeprojが存在するディレクトリです.
スクリーンショット 2018-09-16 10.58.31.png

移動したら,以下のコマンドを実行してください.
実行後,Podfileというファイルが追加されています.

pod init
スクリーンショット 2018-09-16 11.01.04.png

作成したPodfileを以下のように編集します.

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'OpenCVSample' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for OpenCVSample

  ### 追記 ###

  pod 'OpenCV'
  
  ###########

  target 'OpenCVSampleTests' do
    inherit! :search_paths
    # Pods for testing
  end

  target 'OpenCVSampleUITests' do
    inherit! :search_paths
    # Pods for testing
  end

end

編集し,保存したあとターミナルに戻り,以下を入力して実行してください.

pod install
スクリーンショット 2018-09-16 11.07.53.png

これでOpenCVライブラリをインストールすることができました.
スクリーンショット 2018-09-16 11.12.41.png
ディレクトリに拡張子.xcworkspaceというファイルができていることを確認してください.
今後はこのファイルを用いてアプリケーションを作成していきます.

ちなみに,ライブラリを追加したり,アップデートしたい時には以下のコマンドを入力してください.

pod update

#3. OpenCVのラップ
.xcworkspaceとなっているファイルを開いてください.
アートボード 2.png

ラッパーを作成していきます.
まず,左側の領域から,最初につけたプロジェクト名と同じ名前のディレクトリを選択して,Command + Nを押してください.
新規ファイル作成画面になるので,Cocoa Touch Classを選択して,Nextを選択してください.
スクリーンショット 2018-09-16 11.24.43.png

Class名をOpenCV(なんでも良いです)とし,LanguageをObjective-Cに変更します.
スクリーンショット 2018-09-16 11.25.40.png

Nextを押すと,Bridging Headerを作りますかと聞かれるので,Create Bridging Headerを選択します.
スクリーンショット 2018-09-16 11.27.40.png

下図のように,3つのファイルが作成できたことを確認してください.
スクリーンショット 2018-09-16 11.32.03.png

まず,Bridging Headerファイルを編集していきます.
作成した,headerファイルをimportしてください.
これを記述するだけで,Bridging Headerを触ることはもうありません.
スクリーンショット 2018-09-16 11.50.44.png

OpenCVをSwiftで用いる方法は,C++のファイルをObjective C++にラップして,Swiftで用いるという方法となります.
しかし,作成した.mファイルはObjective Cのファイルとなるため,拡張子を.mmに変更します.
これで,Objective C++を使うことが可能です.
スクリーンショット 2018-09-16 11.57.10.png

.hファイルに使用する関数を宣言して,.mmファイルにその関数の処理を記述していきます.
とりあえず.mmファイルにOpenCVのヘッダーファイルをインストールしておきましょう.
これでOpenCVを使う準備はOKです.

#import <opencv2/opencv.hpp> //これを追加
#import "OpenCV.h" //ライブラリによってはNOマクロがバッティングするので,これは最後にimport

@implementation OpenCV

@end

#4. サンプル実装(グレースケール変換)
今回は,サンプルとしてリアルタイム グレースケール変換を行いましょう.
まずは,関数を宣言したいので,.hのファイルを変更します.
もともと,以下のようになっているので,

//OpenCV.h

#import <Foundation/Foundation.h>

@interface OpenCV : NSObject

@end

iOSの基本的なライブラリである,UIKitをインポートしておきます.
今回は,グレースケールに変換するので,関数toGrayImgを定義します.
引数,返り値は,画像であるUIImageを選択します.

//OpenCV.h

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

@interface OpenCV : NSObject
//+ or - (返り値 *)関数名:(引数の型 *)引数名;
//+ : クラスメソッド
//- : インスタンスメソッド

- (UIImage *)toGrayImg:(UIImage *)img;
@end 

関数を宣言したので,処理を記述していきます.
.mmファイルを選択してください.
現在は以下のようになっているので,ここに宣言した関数の処理を記述していきます.

// OpenCV.mm

#import <opencv2/opencv.hpp>
#import "OpenCV.h" //ライブラリによってはNOマクロがバッティングするので,これは最後にimport

@implementation OpenCV

@end

まず必要なヘッダーファイルをimportします.

// OpenCV.mm

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

#import "OpenCV.h" //ライブラリによってはNOマクロがバッティングするので,これは最後にimport

次に宣言した関数の処理を記述します.

// OpenCV.mm

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

#import "OpenCV.h" //ライブラリによってはNOマクロがバッティングするので,これは最後にimport

@implementation OpenCV
- (UIImage *) toGrayImg:(UIImage *)img{
    
    // *************** UIImage -> cv::Mat変換 ***************
    CGColorSpaceRef colorSpace = CGImageGetColorSpace(img.CGImage);
    CGFloat cols = img.size.width;
    CGFloat rows = img.size.height;
    
    cv::Mat mat(rows, cols, CV_8UC4);
    
    CGContextRef contextRef = CGBitmapContextCreate(mat.data,
                                                    cols,
                                                    rows,
                                                    8,
                                                    mat.step[0],
                                                    colorSpace,
                                                    kCGImageAlphaNoneSkipLast |
                                                    kCGBitmapByteOrderDefault);
    
    CGContextDrawImage(contextRef, CGRectMake(0, 0, cols, rows), img.CGImage);
    CGContextRelease(contextRef);
    
    // *************** 処理 ***************
    cv::Mat grayImg;
    cv::cvtColor(mat, grayImg, CV_BGR2GRAY); //グレースケール変換
    
    // *************** cv::Mat → UIImage ***************
    UIImage *resultImg = MatToUIImage(grayImg);
    return resultImg;
}


@end

次に,レイアウトを作成していきましょう.
Main.storyboardを選択してください.
右側のObjectライブラリから,Storyboard内にドラッグアンドドロップすることでレイアウトしていくことが可能です.

スクリーンショット 2018-09-16 13.45.33.png

 今回は,画像を表示したいのでimage Viewを配置します.
右下のFilterに「image」と入力すると,image Viewが出てくると思うので画面内にドラッグします.
スクリーンショット 2018-09-16 13.48.21.png

次に,配置したオブジェクトについて,処理していきたいのでコードエディタを表示します.
Assistant Editorを開きましょう.
スクリーンショット 2018-09-16 13.42.06.png

スクリーンショット 2018-09-16 13.40.17.png

Assistant Editorには,現在編集中のViewControllerに対応したコードが表示されます.
配置したImageViewを右クリックしながらoverride func viewDidLoadの上あたりにドラッグします.
とりあえず名前を「cameraImageView」にしましたが,なんでもいいです.
名前をつけたらConnectを押しましょう.
スクリーンショット 2018-09-16 13.59.28.png

カメラを使うので, AVFoundationをimportします.

//
//  ViewController.swift


import UIKit
import AVFoundation

class ViewController: UIViewController {

    @IBOutlet weak var cameraImageView: UIImageView!
    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.
    }

    
}

自作のOpenCVクラスをインスタンス化します.

class ViewController: UIViewController {

    @IBOutlet weak var cameraImageView: UIImageView!
    
    var openCV = OpenCV()

デリゲートプロトコル宣言をしておきましょう.
ViewController: UIViewControllerと書いてある部分を以下のようにします.

class ViewController: UIViewController,AVCaptureVideoDataOutputSampleBufferDelegate 

使用する変数を先に宣言しておきます.
3変数を定義してください.

class ViewController: UIViewController {

    @IBOutlet weak var cameraImageView: UIImageView!
    
    var openCV = OpenCV()
    
    var session: AVCaptureSession! //セッション
    var device: AVCaptureDevice! //カメラ
    var output: AVCaptureVideoDataOutput! //出力先

カメラをセットアップする関数(initCamera)を作成し,Viewをロードした後に呼ばれる関数(ViewDidLoad)内に以下を記述します.

// カメラの準備処理
    func initCamera() -> Bool {
        let preset = AVCaptureSession.Preset.medium //解像度
        //解像度
        //        AVCaptureSession.Preset.Photo : 852x640
        //        AVCaptureSession.Preset.High : 1280x720
        //        AVCaptureSession.Preset.Medium : 480x360
        //        AVCaptureSession.Preset.Low : 192x144
        //        AVCaptureSession.Preset.640x480 : 640x480
        //        AVCaptureSession.Preset.1280x720 : 1280x720
        
        let frame = CMTimeMake(1, 20) //フレームレート
        let position = AVCaptureDevice.Position.back //フロントカメラかバックカメラか
        
        
        // セッションの作成.
        session = AVCaptureSession()
        
        // 解像度の指定.
        session.sessionPreset = preset
        
        // デバイス取得.
        device = AVCaptureDevice.default(AVCaptureDevice.DeviceType.builtInWideAngleCamera,
                                           for: AVMediaType.video,
                                           position: position)
        
        // VideoInputを取得.
        var input: AVCaptureDeviceInput! = nil
        do {
            input = try
                AVCaptureDeviceInput(device: device) as AVCaptureDeviceInput
        } catch let error {
            print(error)
            return false
        }
        
        // セッションに追加.
        if session.canAddInput(input) {
            session.addInput(input)
        } else {
            return false
        }
        
        // 出力先を設定
        output = AVCaptureVideoDataOutput()
        
        //ピクセルフォーマットを設定
        output.videoSettings =
            [ kCVPixelBufferPixelFormatTypeKey as AnyHashable as! String : Int(kCVPixelFormatType_32BGRA) ]
        
        //サブスレッド用のシリアルキューを用意
        output.setSampleBufferDelegate(self, queue: DispatchQueue.main)
        
        // 遅れてきたフレームは無視する
        output.alwaysDiscardsLateVideoFrames = true
        
        // FPSを設定
        do {
            try device.lockForConfiguration()
            
            device.activeVideoMinFrameDuration = frame //フレームレート
            device.unlockForConfiguration()
        } catch {
            return false
        }
        
        // セッションに追加.
        if session.canAddOutput(output) {
            session.addOutput(output)
        } else {
            return false
        }
        
        // カメラの向きを合わせる
        for connection in output.connections {
            if connection.isVideoOrientationSupported {
                connection.videoOrientation = AVCaptureVideoOrientation.portrait
            }
        }
        
        return true
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        
        if initCamera() {
            session.startRunning()
        }else{
            assert(false) //カメラが使えない
        }
    }

カメラから取得した画像をバッファーに入れていく処理ができたので,バッファーから画像を生成する処理を書いていきます.
まず,BufferからUIImageに変換するための関数を作成します.

    // sampleBufferからUIImageを作成
    func captureImage(_ sampleBuffer:CMSampleBuffer) -> UIImage{
        let imageBuffer: CVImageBuffer! = CMSampleBufferGetImageBuffer(sampleBuffer)
        
        // ベースアドレスをロック
        CVPixelBufferLockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))
        
        // 画像データの情報を取得
        let baseAddress: UnsafeMutableRawPointer = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0)!
        
        let bytesPerRow: Int = CVPixelBufferGetBytesPerRow(imageBuffer)
        let width: Int = CVPixelBufferGetWidth(imageBuffer)
        let height: Int = CVPixelBufferGetHeight(imageBuffer)
        
        // RGB色空間を作成
        let colorSpace: CGColorSpace! = CGColorSpaceCreateDeviceRGB()
        
        // Bitmap graphic contextを作成
        let bitsPerCompornent: Int = 8
        let bitmapInfo = CGBitmapInfo(rawValue: (CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue) as UInt32)
        let newContext: CGContext! = CGContext(data: baseAddress, width: width, height: height, bitsPerComponent: bitsPerCompornent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo.rawValue) as CGContext?
        
        // Quartz imageを作成
        let imageRef: CGImage! = newContext!.makeImage()
        
        // ベースアドレスをアンロック
        CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))
        
        // UIImageを作成
        let resultImage: UIImage = UIImage(cgImage: imageRef)
        
        return resultImage
    }

次にフレームごとに処理を行うための関数を書きます.
この関数は,先ほど追加した,AVCaptureVideoDataOutputSampleBufferDelegateによってフレームごとに実行されるようになっています.

    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        DispatchQueue.main.async{ //非同期処理として実行
            let img = self.captureImage(sampleBuffer) //UIImageへ変換
            var resultImg: UIImage //結果を格納する

            // *************** 画像処理 ***************
            resultImg = self.openCV.toGrayImg(img) //変換
            // *****************************************
            
            // 表示
            self.cameraImageView.image = resultImg
        }
    }

最後に,今回アプリでは,カメラを使うため「info.plist」でカメラ使用を許可する必要があります.
スクリーンショット 2018-09-16 15.00.02.png

一番上のInformation Property Listのプラスを押して,Privacy - Cameraと入力してください.
Privacy - Camera Usage Descriptionが選択候補に出ると思うので選択してEnterを押してください.
これで準備はOKです.

ところで,ここまでの過程では,ViewControllerに配置してあるImageViewの大きさも場所も適当なので,調整しましょう.
適当に解像度に合わせてサイズを調整するようにしてみました.

    // カメラの準備処理
    func initCamera() -> Bool {
        let preset = AVCaptureSession.Preset.medium //解像度
        //解像度
        //        AVCaptureSession.Preset.Photo : 852x640
        //        AVCaptureSession.Preset.High : 1280x720
        //        AVCaptureSession.Preset.Medium : 480x360
        //        AVCaptureSession.Preset.Low : 192x144
 
        
        let frame = CMTimeMake(1, 20) //フレームレート
        let position = AVCaptureDevice.Position.back //フロントカメラかバックカメラか
        
                // *************** 追加 ***************
        setImageViewLayout(preset: preset)//UIImageViewの大きさを調整

    //imageViewの大きさを調整
    func setImageViewLayout(preset: AVCaptureSession.Preset){
        let width = self.view.frame.width
        var height:CGFloat
        switch preset {
        case .photo:
            height = width * 852 / 640
        case .high:
            height = width * 1280 / 720
        case .medium:
            height = width * 480 / 360
        case .low:
            height = width * 192 / 144
        case .cif352x288:
            height = width * 352 / 288
        case .hd1280x720:
            height = width * 1280 / 720
        default:
            height = self.view.frame.height
        }
        cameraImageView.frame = CGRect(x: 0, y: 0, width: width, height: height)
    }

では,実行してみましょう.

ちゃんと動いていました.

以下最終的なコードです.

//
//  ViewController.swift


import UIKit
import AVFoundation

class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate {

    @IBOutlet weak var cameraImageView: UIImageView!
    
    var openCV = OpenCV()
    
    var session: AVCaptureSession! //セッション
    var device: AVCaptureDevice! //カメラ
    var output: AVCaptureVideoDataOutput! //出力先
    
    override func viewDidLoad() {
        super.viewDidLoad()
        super.viewDidLoad()
        
        if initCamera() {
            session.startRunning()
        }else{
            assert(false) //カメラが使えない
        }
    }

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

    
    // カメラの準備処理
    func initCamera() -> Bool {
        let preset = AVCaptureSession.Preset.medium //解像度
        //解像度
        //        AVCaptureSession.Preset.Photo : 852x640
        //        AVCaptureSession.Preset.High : 1280x720
        //        AVCaptureSession.Preset.Medium : 480x360
        //        AVCaptureSession.Preset.Low : 192x144
 
        
        let frame = CMTimeMake(1, 20) //フレームレート
        let position = AVCaptureDevice.Position.back //フロントカメラかバックカメラか
        
        setImageViewLayout(preset: preset)//UIImageViewの大きさを調整
        
        // セッションの作成.
        session = AVCaptureSession()
        
        // 解像度の指定.
        session.sessionPreset = preset
        
        // デバイス取得.
        device = AVCaptureDevice.default(AVCaptureDevice.DeviceType.builtInWideAngleCamera,
                                           for: AVMediaType.video,
                                           position: position)
        
        // VideoInputを取得.
        var input: AVCaptureDeviceInput! = nil
        do {
            input = try
                AVCaptureDeviceInput(device: device) as AVCaptureDeviceInput
        } catch let error {
            print(error)
            return false
        }
        
        // セッションに追加.
        if session.canAddInput(input) {
            session.addInput(input)
        } else {
            return false
        }
        
        // 出力先を設定
        output = AVCaptureVideoDataOutput()
        
        //ピクセルフォーマットを設定
        output.videoSettings =
            [ kCVPixelBufferPixelFormatTypeKey as AnyHashable as! String : Int(kCVPixelFormatType_32BGRA) ]
        
        //サブスレッド用のシリアルキューを用意
        output.setSampleBufferDelegate(self, queue: DispatchQueue.main)
        
        // 遅れてきたフレームは無視する
        output.alwaysDiscardsLateVideoFrames = true
        
        // FPSを設定
        do {
            try device.lockForConfiguration()
            
            device.activeVideoMinFrameDuration = frame //フレームレート
            device.unlockForConfiguration()
        } catch {
            return false
        }
        
        // セッションに追加.
        if session.canAddOutput(output) {
            session.addOutput(output)
        } else {
            return false
        }
        
        // カメラの向きを合わせる
        for connection in output.connections {
            if connection.isVideoOrientationSupported {
                connection.videoOrientation = AVCaptureVideoOrientation.portrait
            }
        }
        
        return true
    }
    
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        DispatchQueue.main.async{ //非同期処理として実行
            let img = self.captureImage(sampleBuffer) //UIImageへ変換
            var resultImg: UIImage //結果を格納する

            // *************** 画像処理 ***************
            resultImg = self.openCV.toGrayImg(img) //変換
            // *****************************************
            
            // 表示
            self.cameraImageView.image = resultImg
        }
    }
    
    // sampleBufferからUIImageを作成
    func captureImage(_ sampleBuffer:CMSampleBuffer) -> UIImage{
        let imageBuffer: CVImageBuffer! = CMSampleBufferGetImageBuffer(sampleBuffer)
        
        // ベースアドレスをロック
        CVPixelBufferLockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))
        
        // 画像データの情報を取得
        let baseAddress: UnsafeMutableRawPointer = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0)!
        
        let bytesPerRow: Int = CVPixelBufferGetBytesPerRow(imageBuffer)
        let width: Int = CVPixelBufferGetWidth(imageBuffer)
        let height: Int = CVPixelBufferGetHeight(imageBuffer)
        
        // RGB色空間を作成
        let colorSpace: CGColorSpace! = CGColorSpaceCreateDeviceRGB()
        
        // Bitmap graphic contextを作成
        let bitsPerCompornent: Int = 8
        let bitmapInfo = CGBitmapInfo(rawValue: (CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue) as UInt32)
        let newContext: CGContext! = CGContext(data: baseAddress, width: width, height: height, bitsPerComponent: bitsPerCompornent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo.rawValue) as CGContext?
        
        // Quartz imageを作成
        let imageRef: CGImage! = newContext!.makeImage()
        
        // ベースアドレスをアンロック
        CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))
        
        // UIImageを作成
        let resultImage: UIImage = UIImage(cgImage: imageRef)
        
        return resultImage
    }
    
    //imageViewの大きさを調整
    func setImageViewLayout(preset: AVCaptureSession.Preset){
        let width = self.view.frame.width
        var height:CGFloat
        switch preset {
        case .photo:
            height = width * 852 / 640
        case .high:
            height = width * 1280 / 720
        case .medium:
            height = width * 480 / 360
        case .low:
            height = width * 192 / 144
        case .cif352x288:
            height = width * 352 / 288
        case .hd1280x720:
            height = width * 1280 / 720
        default:
            height = self.view.frame.height
        }
        cameraImageView.frame = CGRect(x: 0, y: 0, width: width, height: height)
    }

}
// OpenCV.h

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

@interface OpenCV : NSObject
//+ or - (返り値 *)関数名:(引数の型 *)引数名;
//+ : クラスメソッド
//- : インスタンスメソッド
- (UIImage *)toGrayImg:(UIImage *)img;
@end
// OpenCV.mm

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

#import "OpenCV.h" //ライブラリによってはNOマクロがバッティングするので,これは最後にimport

@implementation OpenCV
- (UIImage *) toGrayImg:(UIImage *)img{
    
    // *************** UIImage -> cv::Mat変換 ***************
    CGColorSpaceRef colorSpace = CGImageGetColorSpace(img.CGImage);
    CGFloat cols = img.size.width;
    CGFloat rows = img.size.height;
    
    cv::Mat mat(rows, cols, CV_8UC4);
    
    CGContextRef contextRef = CGBitmapContextCreate(mat.data,
                                                    cols,
                                                    rows,
                                                    8,
                                                    mat.step[0],
                                                    colorSpace,
                                                    kCGImageAlphaNoneSkipLast |
                                                    kCGBitmapByteOrderDefault);
    
    CGContextDrawImage(contextRef, CGRectMake(0, 0, cols, rows), img.CGImage);
    CGContextRelease(contextRef);
    
    // *************** 処理 ***************
    cv::Mat grayImg;
    cv::cvtColor(mat, grayImg, CV_BGR2GRAY); //グレースケール変換
    
    // *************** cv::Mat → UIImage ***************
    UIImage *resultImg = MatToUIImage(grayImg);
    return resultImg;
}


@end
14
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?