はじめに
Xcodeを用いて,OpenCVを使ったiOSアプリケーションを作成する方法をまとめます.
#1. 新規プロジェクトを作成する
File→New→ProjectからSingle View Appを作成します.
アプリ名を設定します.
ProductNameが作成したアプリケーションの名前になります.
LanguageにはSwiftを選択しましょう.
そのままアプリの保存先を設定して,Nextを選択しプロジェクトを立ち上げます.
#2. OpenCVライブラリのインポート
次に作成したプロジェクトにOpenCVのライブラリを追加していきます.
ライブラリはCocoaPodsを通してインストールするため,先にCocoaPodsをインストールします.
Terminalを開いて,以下を入力してください.
sudo gem install cocoapods
もし上が動かないなら,こちらを入力してください.
sudo gem install -n /usr/local/bin cocoapods
これでCocoapodsがインストールできたので,作成したプロジェクトのディレクトリに移動してください.
.xcodeprojが存在するディレクトリです.
移動したら,以下のコマンドを実行してください.
実行後,Podfileというファイルが追加されています.
pod init
作成した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
これでOpenCVライブラリをインストールすることができました.
ディレクトリに拡張子.xcworkspaceというファイルができていることを確認してください.
今後はこのファイルを用いてアプリケーションを作成していきます.
ちなみに,ライブラリを追加したり,アップデートしたい時には以下のコマンドを入力してください.
pod update
#3. OpenCVのラップ
.xcworkspaceとなっているファイルを開いてください.
ラッパーを作成していきます.
まず,左側の領域から,最初につけたプロジェクト名と同じ名前のディレクトリを選択して,Command + Nを押してください.
新規ファイル作成画面になるので,Cocoa Touch Classを選択して,Nextを選択してください.
Class名をOpenCV(なんでも良いです)とし,LanguageをObjective-Cに変更します.
Nextを押すと,Bridging Headerを作りますかと聞かれるので,Create Bridging Headerを選択します.
下図のように,3つのファイルが作成できたことを確認してください.
まず,Bridging Headerファイルを編集していきます.
作成した,headerファイルをimportしてください.
これを記述するだけで,Bridging Headerを触ることはもうありません.
OpenCVをSwiftで用いる方法は,C++のファイルをObjective C++にラップして,Swiftで用いるという方法となります.
しかし,作成した.mファイルはObjective Cのファイルとなるため,拡張子を.mmに変更します.
これで,Objective C++を使うことが可能です.
.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内にドラッグアンドドロップすることでレイアウトしていくことが可能です.
今回は,画像を表示したいのでimage Viewを配置します.
右下のFilterに「image」と入力すると,image Viewが出てくると思うので画面内にドラッグします.
次に,配置したオブジェクトについて,処理していきたいのでコードエディタを表示します.
Assistant Editorを開きましょう.
Assistant Editorには,現在編集中のViewControllerに対応したコードが表示されます.
配置したImageViewを右クリックしながらoverride func viewDidLoadの上あたりにドラッグします.
とりあえず名前を「cameraImageView」にしましたが,なんでもいいです.
名前をつけたらConnectを押しましょう.
カメラを使うので, 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」でカメラ使用を許可する必要があります.
一番上の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