なすびのブログ

iOS・swiftの情報を気ままに書いていきます。

【swift4.2】iOSのVisionフレームワークで矩形検出をしてみる

はじめに

こんにちは、なすびです。
今回はよくある画像の矩形検出をしてみました。
画像処理といえばOpenCVを使うというイメージでしたがiOS11からVisionフレームワークでを思い出したので使ってみました。

画像から矩形を検出する

・VNImageRequestHandlerに矩形検出する画像を与える
・VNDetectRectanglesRequestのクロージャーで検出後の処理を行う

let image = UIImage(named: "sample")!
let handler = VNImageRequestHandler(cgImage: image.cgImage!, options: [:])
let request = VNDetectRectanglesRequest(completionHandler: {(request, error) in
    // 検出後の処理
})

try! handler.perform([request])

クロージャーの引数requestのresultsで検出結果としてVNRectangleObservationの配列で取得できます。

矩形の頂点座標を取得する

VNRectangleObservationからは矩形の頂点座標を取得することができます。

座標系

Visionで取得できる座標系は以下の図ようにUIKitの座標系とは異なるため、適宜変換してやる必要があります。
Vision座標系では与えた画像に対する割合で座標が表現されるため0.0〜1.0の間の値が渡されます。

f:id:nasubiblog:20181224132933p:plainf:id:nasubiblog:20181224132958p:plain

座標変換

Visionの座標からCGPoint、CGRectをUIKit上の座標に変換する関数です。
それぞれ差を取ってサイズをかけているだけです。
CGRectのy座標について、Vision座標では長方形の左下を原点とするのに対して、UIKit座標では長方形の左上を原点とするためrect.size.heightを引いています。

func convertPoint(_ point: CGPoint, to size: CGSize) -> CGPoint {
    return CGPoint(x: point.x * size.width,
                   y: (1.0 - point.y) * size.height)
}

func convertRect(_ rect: CGRect, to size: CGSize) -> CGRect {
    return CGRect(x: rect.origin.x * size.width,
                  y: (1.0 - rect.origin.y - rect.size.height) * size.height,
                  width: rect.width * size.width,
                  height: rect.height * size.height)
}

矩形を取得する

それでは矩形を検出します。
VNRectangleObservationのtopLeft〜bottomLeftで検出対象の頂点座標をそれぞれ取得取得します。(下記画像の赤線部分)
boundingBoxでは検出した長方形を包括する矩形を取得できます。(下記画像の青線部分)

let request = VNDetectRectanglesRequest(completionHandler: {(request, error) in
    guard let results = request.results as? [VNRectangleObservation] else {
        return
    }
    guard !results.isEmpty else {
        return
    }
    // 頂点座標取得
    let topLeft = self.convertPoint(results[0].topLeft, to: image.size)
    let topRight = self.convertPoint(results[0].topRight, to: image.size)
    let bottomRight = self.convertPoint(results[0].bottomRight, to: image.size)
    let bottomLeft = self.convertPoint(results[0].bottomLeft, to: image.size)
    // 検出対象を包括する矩形
    let bounds = self.convertRect(results[0].boundingBox, to: image.size)
}

f:id:nasubiblog:20181224145526p:plain
あとは取得した座標を使って画像を切り抜いたり、カメラ画像に矩形のラインを描画したりするだけです。
UIImageViewに表示する場合はさらにUIImageViewの中で表示されている画像の座標も合わせて計算してあげる必要がありますがそれはまた別の機会にでも書きます。

最後に

今回はiOS11から追加されたVisionフレームワークを使って矩形の検出を行いました。
以前までは画像処理に関する知識が必要だったりOpenCVを使わないとできないことがswiftで簡単にできるようになっています。
とはいえ複雑なことをやろうと思うとやっぱりOpenCVとか使わないといけなくなるとは思いますが。。。台形補正とかやろうとしたらやっぱりOpenCVの方がいいんかなー

他にもCoreMLなど機械学習フレームワークが出てたりするのでその辺も使っていくと簡単に面白いアプリが作れそうですね!
CoreMLも今度触ってみようかなー

今後もブログではswiftやiOSに関する内容を書くのでよかったら読者登録お願いします!

参考サイト

iOS VisionFramework 矩形検知(VNDetectRectanglesRequest)のTips - Qiita

【swift4.2】swiftでOpenCVを使ってみる

はじめに

どーも、なすびです。
今回はswiftからOpenCVを使って画像をいじれるようにしようかと思います。
調べてみたところswiftから直接は呼び出せないみたいですが、あまり気にしなくても良さげですね。
流れとしてはc++Objective-C++swiftのような形で利用します。
では簡単に説明していきます。
参考サイト:iOS(swift)でOpenCVを使うシンプルなサンプル - Qiita

OpenCVをインストールする

・CocoaPodsを使用してインストール。
CocoaPodsの使い方は別で記事を書いているのでそちらを参照してください。
【Xcode】CocoaPodsのインストール - なすびのブログ
PodfileにOpenCVを追加

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

target '[プロジェクト名]' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for [プロジェクト名]
  # ここを追加
  pod 'OpenCV'

  target '[プロジェクト名]Tests' do
    inherit! :search_paths
    # Pods for testing
  end

  target '[プロジェクト名]UITests' do
    inherit! :search_paths
    # Pods for testing
  end

end


ちなみにもちろんCocoaPodsを使わなくても利用できます。 その場合は公式サイトから OpenCV iOS Packをダウンロードして追加してください。 http://opencv.org/releases.html

OpenCVのラッパークラスを作成する

Cocoa Touch Classを選択
f:id:nasubiblog:20181222002530p:plain

Objective-Cを選択
f:id:nasubiblog:20181222002601p:plain

Bridging-Header.hを追加
swiftからObjective-Cコードを使うために必要になります。
f:id:nasubiblog:20181222003059p:plain

・ファイル拡張子を.mから.mmにリネームする
.mObjective-Cのファイル拡張子なのでObjective-C++とするために.mmに変更します。
f:id:nasubiblog:20181222003436p:plain

Bridging-Header.hObjective-Cクラスのヘッダを追加

#import "OpenCV.h"

OpenCV.hにラッパーメソッドを定義

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

NS_ASSUME_NONNULL_BEGIN

@interface OpenCV : NSObject
+(UIImage *)grayScale:(UIImage *)image;
@end

NS_ASSUME_NONNULL_END

OpenCV.mmに処理を実装
下記の例ではグレースケール変換を行なっています。

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

@implementation OpenCV

+(UIImage *)grayScale:(UIImage *)image {
    // UIImageからMatを作成
    cv::Mat mat;
    UIImageToMat(image, mat);
    
    // グレースケール変換
    cv::Mat gray;
    cv::cvtColor(mat, gray, CV_BGR2GRAY);
    
    // MatからUIImageを作成
    UIImage *grayImg = MatToUIImage(gray);
    
    return grayImg;
}

@end

swiftコードから呼び出す

あとはいつものようにswiftでメソッドを呼び出します。

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var imageView: UIImageView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let image = UIImage(named: "nasubi")!
        imageView.image = OpenCVWrapper.grayScale(image)
    }
}


実行。
f:id:nasubiblog:20181222015439p:plain
おー、簡単にできる。

二値化

グレースケール後に下記処理をすると二値化変換もできます。

cv::Mat binary;
cv::threshold(gray, binary, 128, 255, cv::THRESH_BINARY);

最後に

今回はswiftからOpenCVを使うサンプルについて説明しました。
今回は基本的な実装だけですが、これで色々と画像処理が捗りますね。
次は矩形検出とかしてみよかなー。画像処理の勉強をしたいです。
さて今回はこの辺りで。


今後も画像以外にもswiftに関するブログを書くのでよかったら読者登録お願いします!

【Xcode】CocoaPodsのインストール

はじめに

こんにちは、なすびです!
最近ライブラリ管理ツールには専らCarthageばかり使っています。
そこでいざCocoaPodsを使おうとインストールしようとしたらハマってしまったのでメモに残します。

開発環境

MacOS 10.14.1(Mojave)
Xcode 10.1

導入手順

インストール手順は色々書かれているので簡単に。
参考サイト:【Swift】CocoaPods導入手順 - Qiita

環境構築

ターミナルからコマンドを順番に実行する。
・CocoaPodsをgemでインストール
El Capitanからrootlessという概念が登場してrootユーザーでも/usrの権限がないみたいです。
そのためインストール先を/usr/local/binをしています。
参考サイト:MacOSX El Capitanでcocoapodsインストールが出来ない時の対処法 - Qiita

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

・CocoaPodsをセットアップする
これでレポジトリの設定とか諸々してるっぽい

$ pod setup

ライブラリ管理

・Podfileを作成する
実行するとプロジェクトのディレクトリ直下にPodfileが自動で生成されます。

$ cd [プロジェクトのディレクトリ]
$ pod init

・Podfileにライブラリを追記する

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

target '[プロジェクト名]' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for [プロジェクト名]
  # ここを追加
  pod 'OpenCV'

  target '[プロジェクト名]Tests' do
    inherit! :search_paths
    # Pods for testing
  end

  target '[プロジェクト名]UITests' do
    inherit! :search_paths
    # Pods for testing
  end

end

・ライブラリをインストールする

$ pod install

※2回目以降の場合

$ pod update


「pod install」実行後xcworkspaceが作成されます。以降のプロジェクトはxcworkspaceから開きます。
あとはそれぞれライブラリを使うコードにimportして使います。

podのコマンドでエラーが発生する

本当なら上記の手順だけでいいはずだったんですが、私の場合エラーが発生してうまくいきませんでした。
podコマンドを入力すると以下のようなエラーが。。。

/System/Library/Frameworks/Ruby.framework/Versions/2.3/usr/lib/ruby/2.3.0/rubygems/core_ext/kernel_require.rb:55:in `require': cannot load such file -- rubygems/core_ext/kernel_warn (LoadError)

kernel_warnが読み込めないと。なるほどよくわからん。
とりあえず再度インストールし直しても変わらず。

しょうがないのでログを見ているとgemをアップデートしているときに一部エラーが発生していることに気づく。

$ sudo gem update
...
ERROR:  Error installing did_you_mean:
    did_you_mean requires Ruby version >= 2.5.0.
...

んーこれが原因かわからんけど少なくともエラーが起きてるからRubyバージョンを上げてみる。
参考サイト:【2018年版】macにrbenvを入れてrubyを管理できるようにしちゃう - Qiita

再びCocoaPodsのインストールをするとできた!

最後に

とりあえずCocoaPodsのインストールは無事に終わりました。
いつもこの辺りは思考停止してコマンドを入力しているだけだったのでエラーの原因がわからないことが多いです。
まあ今回はRubyのバージョン違い程度でしたが、やっぱりきちんと理解した上でやらないとですね。
反省反省。

今後もブログではswiftやiOSに関する内容を書くのでよかったら読者登録お願いします!

【swift4.2】UIImageから二値化画像を作成する

はじめに

こんにちは、なすびです。
この前カメラロールの画像を二値化するアプリをストアに公開しました。

その時にswiftでの二値化処理の情報があまり見つからなかったのでメモ書き程度に二値化処理を共有しようかと思います。
今回の例は単純二値化の処理になります。

ちなみにこんなアプリです。よかったらインストールお願いします!

ニチカメラ

ニチカメラ

  • Naoya Ishida
  • 写真/ビデオ
  • 無料

UIImageからピクセルデータを取得

・UIImage → CGImage → ピクセルデータとどんどん下位のデータにアクセスしバッファに詰め込む。

// 画像データを取得
let image = UIImage(named: "SampleImage")!
let imageRef = image.cgImage!
let data = imageRef.dataProvider!.data
let buffer = CFDataGetBytePtr(data)!


二値化処理

・今回のデータは8bitのRGBA画像を想定。
そのため二値化後のデータを格納するimageBytesをUInt8で高さ×幅×4(RGBA)で準備する。
・計算するピクセルの先頭インデックスを計算する。
・先頭インデックスから8bitずつずらして各色情報にアクセスする。
・RGBの平均値が閾値を超えるかを判定し、0と255を割り当てる。

let threshold = 128
let imageBytes = UnsafeMutablePointer<UInt8>.allocate(capacity: imageRef.height * imageRef.width * 4)
// 各ピクセルに対し色を割り当てる
for y in 0..<imageRef.height {
    for x in 0..<imageRef.width {
        // ピクセルの先頭の配列
        let frontPixelIndex = (y * imageRef.width + x) * 4
        let red     = Float(buffer[frontPixelIndex + 0]) / 255.0
        let green   = Float(buffer[frontPixelIndex + 1]) / 255.0
        let blue    = Float(buffer[frontPixelIndex + 2]) / 255.0
        let alpha   = buffer[frontPixelIndex + 3]
        let average = UInt8((red + green + blue) * 255 / 3)
        let binaryValue = average < threshold * 255 ? 0 : 255
        imageBytes[frontPixelIndex + 0] = binaryValue
        imageBytes[frontPixelIndex + 1] = binaryValue
        imageBytes[frontPixelIndex + 2] = binaryValue
        imageBytes[frontPixelIndex + 3] = alpha
    }
}


二値化後のピクセルデータからUIImageを作成

ピクセルデータからCFDataRefを作成
・忘れずにimageBytesを解放する
・元画像の情報を元にCGImageを作成
・UIImageを作成

// 画像処理後のデータから画像を作成
let resultData = CFDataCreate(nil, imageBytes, imageRef.height * imageRef.width * 4)!
imageBytes.deallocate()
let  resultDataProvider = CGDataProvider(data: resultData)!
let resultImageRef = CGImage(width: imageRef.width,
                             height: imageRef.height,
                             bitsPerComponent: imageRef.bitsPerComponent,
                             bitsPerPixel: imageRef.bitsPerPixel,
                             bytesPerRow: imageRef.bytesPerRow,
                             space: imageRef.colorSpace!,
                             bitmapInfo: imageRef.bitmapInfo,
                             provider: resultDataProvider,
                             decode: nil,
                             shouldInterpolate: imageRef.shouldInterpolate,
                             intent: imageRef.renderingIntent)!
let resultImage = UIImage(cgImage: resultImageRef, scale: 1.0, orientation:image.imageOrientation)


最後に

今回はswiftで単純二値化処理を行うサンプルを説明しました。
カラースペースは8bitのRGBA固定でやっているのでそれ以外の場合にも対応できるように修正する必要はあるかと思います。

やっぱり画像の扱いはどんどん型を変換していく必要があるのでわかりにくい印象がありますね。
精進せねば。。。

今後も画像以外にもswiftに関するブログを書くのでよかったら読者登録お願いします!

【Swift4.2】UITableViewをシンプルに管理する

はじめに

UITableViewで表示パターンが増えてセルの表示設定がカオスになった経験はありませんか? 僕は以前複数パターンのセル表示をしたときに、メソッドの中がぐちゃぐちゃになり絶望していました(笑) 今回はその時に考えたEnumを使ってシンプルに管理する方法を紹介します。

この方法は下図にある入力フォームのような表示項目が動的に変わらないかつ、セル内の要素がある程度似ている場合のUITableView向けです。

f:id:nasubiblog:20181217223837p:plain

ストーリーボードでセルを作成する

今回は2パターンのセルを作成しています。 セルのidentifierはそれぞれ"StandardCell"、"PeriodCell"としています。

f:id:nasubiblog:20181217223939p:plain

Enumで表示するセルを定義する

表示するセル毎にEnumを定義します。 例ではユーザー名、パスワード、何かの期間を入力するセルを定義しています。 表示するセルのidentifierとタイトルをEnumから取得できるように定義します。

enum DisplayCellType: Int, CaseIterable {
    case userID
    case password
    case period
    
    var identifier: String {
        switch self {
        case .userID, .password:
            return "StandardCell"
        case .period:
            return "PeriodCell"
        }
    }
    
    var title: String {
        switch self {
        case .userID:
            return "ユーザーID"
        case .password:
            return "パスワード"
        case .period:
            return "何かの期間"
        }
    }
}

セルのパターン毎にクラスを作成する

ViewControllerから使う時にどのクラスのセルか判定したくないのでインターフェースを定義しておきます。

protocol CustomCell {
    func setItem(_ title: String)
}

final class StandardCell: UITableViewCell, CustomCell {
    
    @IBOutlet weak var titleLabel: UILabel!
    
    func setItem(_ title: String) {
        self.titleLabel.text = title
    }
}

final class PeriodCell: UITableViewCell, CustomCell {
    
    @IBOutlet weak var titleLabel: UILabel!
    
    func setItem(_ title: String) {
        self.titleLabel.text = title
    }
}

ViewControllerからセルの設定を行う

あとはViewControllerからセルを利用します。 DisplayCellTypeはCaseIterableを継承しているのでallCasesでEnumの個数を返してやります。 セルのidentifierとタイトルはEnumから引っ張ってきて設定します。

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return DisplayCellType.allCases.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cellType = DisplayCellType(rawValue: indexPath.row)!
        let cell = tableView.dequeueReusableCell(withIdentifier: cellType.identifier) as! CustomCell & UITableViewCell
        cell.setItem(cellType.title)
        return cell
    }
}

最後に

今回のようにEnumを使いセルの情報をEnumで定義してあげればViewControllerがシンプルになります。 またセルに表示する項目が増えた場合もViewContollerを触る必要なくEnumに項目を追加するだけでいいので改修もしやすくなるのかなーと思います。

ただこのやり方だと動的にセルを追加する場合には対応できないので、それについては別途考えたいと思います。

たった2日でマスターできるiPhoneアプリ開発集中講座 Xcode 10 Swift 4.2対応

たった2日でマスターできるiPhoneアプリ開発集中講座 Xcode 10 Swift 4.2対応