Galapagos Tech Blog

株式会社ガラパゴスのメンバーによる技術ブログです。

Swift3.0でCloud APIを使ってOCRをしてみる

御機嫌よう、最近Swiftな世界に入門しているガラパゴスのおとめです。

今回は、SwiftからGoogle Cloud Vision APIとMicrsoft Cognitive Computer Vision APIのテキスト検出(OCR)を使ってみようと思います。

なお、この記事はSwift 3.0とXcode 8.2.1を対象にしています。

プロジェクトの作成

まず適当にプロジェクトを作ります。また、今回はAlamfireSwiftyJSONを使っていますが、これらのライブラリはCocoaPodsでインストール・管理しました(インストール方法などは省略します)。

ではサクッとViewを作ってしまいます。今回はカメラロールから選んだ画像を上に、検出したテキストを下に表示したいと思います。

カメラロールを表示するボタンは何でもいいのですが、ここではNavigation Barにおいてみました。 次に画像とテキストの表示ですが、わかりやすく半々に配置するために、Vertical Stack Viewを使ってみます。ぽいっとドラッグしたら、Viewをいっぱいに使うために、Add New Constraintsで上下左右を0に設定します。

f:id:glpgsinc:20170120161952p:plain

画像とテキストを半々に表示するために、Stack ViewDistributionFill Equallyにします。

f:id:glpgsinc:20170120162041p:plain

Vertical Stack Viewの設定が終わったら、Image ViewLabelを放り込みます。すると以下のように、自動的に半々になりましたね?

f:id:glpgsinc:20170120162141p:plain

Labelは複数行表示できるようにするために、Lines0を設定しておきます。

f:id:glpgsinc:20170120162222p:plain

これらの要素の配置が終わったら、Assistant Editorを表示して、Ctrlキーを押しながらEditorにドラッグし、@IBAction@IBOutletを設定しておきます。

// 選択した画像の表示
@IBOutlet weak var image: UIImageView!
// 検出したテキストの表示
@IBOutlet weak var text: UILabel!
// カメラロールを表示するボタンを押した時の処理
@IBAction func selectImage(_ sender: Any) {
}

カメラロールを使えるようにする

iOSシミュレータではカメラが使えないので、今回はカメラロールから選ぶことにします。iOSシミュレータを起動すると数枚の風景写真が入っていますが、適当な画像をシミュレータのウインドウにドロップするとカメラロールに登録されて選択できるようになります。

カメラロールにアクセスするには、info.plistPrivacy - Photo Library Usage Descriptionというキーを設定する必要があります(キーは自分で書かずにドロップダウンから選択します)。値にはアプリから初めてカメラロールを使うときに表示されるアクセス許可のダイアログに表示するメッセージを入れます。

f:id:glpgsinc:20170120162317p:plain

では実際にカメラロールを表示する処理を書いていきましょう。

@IBAction func selectImage(_ sender: Any) {
    if UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.photoLibrary) {
        // 前回検出したテキストをクリアする
        text.text = ""
        // 写真を選ぶ
        let picker = UIImagePickerController()
        picker.sourceType = .photoLibrary
        // 編集はしない
        picker.allowsEditing = false
        // カメラロールを表示
        picker.delegate = self
        present(picker, animated: true, completion: nil)
    }
}

次に、画像を選択した時の処理を書きます。まずはカメラロールからのメッセージを受け取れるようにするために、UIImagePickerControllerDelegateUINavigationControllerDelegateを継承します。

class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {

画像が選択された時にはimagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any])が呼ばれます。ここでは、選択された画像を表示して、 APIを呼び出します。

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
    let selected = info[UIImagePickerControllerOriginalImage] as! UIImage
    // 選択した画像をUIImageViewに表示する際にアスペクト比を維持する
    image.contentMode = .scaleAspectFit
    image.image = selected
    // 処理中表示にする
    text.text = "(文字を検出しています...)"
    // カメラロール非表示
    dismiss(animated: true, completion: nil)
    // Vision APIを使う
    detectText()
}

では、API呼び出しに取り掛かりましょう。

テキスト検出する

はじめに書いたように、GoogleMicrosoftの両方を試してみます。本来ならUIで切り替えられるようにしたりするべきなのですが、今回はちょっと横着して、コードで切り替えます。まずはこんな感じで使用するAPIを定義します。

enum DetectionMethod: String {
    case GOOGLE
    case MS
}
var method = DetectionMethod.GOOGLE

設定に応じてメソッドを切り替えるようにします。

func detectText() {
    switch method {
    case DetectionMethod.GOOGLE:
        detectTextGoogle()
    case DetectionMethod.MS:
        detectTextMs()
    }
}    

では、それぞれのAPIを準備、実装していきましょう。

Google Cloud Vision APIの準備

Google Cloud PlatformのコンソールからAPI Managerを開き、ライブラリからVision APIを有効にします(まだサインアップしていない方は無料トライアルを開始します)。 次に認証情報からAPIキーを作成し、キーの制限でiOSアプリを選択し、先に作成したiOSプロジェクトのBundle Identifierを登録します。 作成したAPIキーはSwiftコードで使いますが、直接書いてGithubなどにアップしてしまうと請求が大変なことになる場合がありますので取り扱いに注意してください。

Google Cloud Vision API呼び出しの実装

APIの公式な仕様はこちら

今回はPNGのみを対象にします。他のフォーマットを使いたい場合、例えばJpegならUIImageJPEGRepresentationなどを使います。 では仕様に沿ってリクエストを組み立てていきましょう。画像をbase64エンコードして、テキスト検出にはTEXT_DETECTIONを指定します。 また、リクエストヘッダに、キーの制限で指定したBundle Identifierを、URLにAPIキーを指定します。 ところで、Vision APIで指定できる画像にはサイズの制限があるのですが、ここではとりあえずリサイズなどはしないことにします(カメラで撮った写真をそのまま送るとサイズ制限をオーバーしますのでご注意ください)。

リクエストの処理には、はじめに書いたようにAlamofireを使います。リクエスト形式はJSONですので、encodingJSONEncoding.defaultを、またリクエストヘッダを送りますので、headersを指定します。そして、正常なレスポンスだけを処理したいので、.validate(statusCode: 200..<300)のように検証し、レスポンスもJSONですので.responseJSONとします。

func detectTextGoogle() {
    // 画像はbase64する
    // 仮にPNGのみ対象
    if let base64image = UIImagePNGRepresentation(image.image!)?.base64EncodedString() {
        // リクエストの作成
        // 文字検出をしたいのでtypeにはTEXT_DETECTIONを指定する
        // 画像サイズの制限があるので本当は大きすぎたらリサイズしたりする必要がある
        let request: Parameters = [
            "requests": [
                "image": [
                    "content": base64image
                ],
                "features": [
                    [
                        "type": "TEXT_DETECTION",
                        "maxResults": 1
                    ]
                ]
            ]
        ]
        // Google Cloud PlatformのAPI Managerでキーを制限している場合、リクエストヘッダのX-Ios-Bundle-Identifierに指定した値を入れる
        let httpHeader: HTTPHeaders = [
            "Content-Type": "application/json",
            "X-Ios-Bundle-Identifier": Bundle.main.bundleIdentifier ?? ""
        ]
        // googleApiKeyにGoogle Cloud PlatformのAPI Managerで取得したAPIキーを入れる
        Alamofire.request("https://vision.googleapis.com/v1/images:annotate?key=\(googleApiKey)", method: .post, parameters: request, encoding: JSONEncoding.default, headers: httpHeader).validate(statusCode: 200..<300).responseJSON { response in
            // レスポンスの処理
            self.googleResult(response: response)
        }
    }
}

さて、Vision APIの戻りを文字列に戻します。結果はJSONですので、SwiftyJSONでデコードし、["responses"][0]["textAnnotations"]["descriptsion"]に検出した文字が入っていますので、取り出してLabelに貼り付けます。

func googleResult(response: DataResponse<Any>) {
    guard let result = response.result.value else {
        // レスポンスが空っぽだったりしたら終了
        return
    }
    let json = JSON(result)
    let annotations: JSON = json["responses"][0]["textAnnotations"]
    var detectedText: String = ""
    // 結果からdescriptionを取り出して一つの文字列にする
    annotations.forEach { (_, annotation) in
        detectedText += annotation["description"].string!
    }
    // 結果を表示する
    text.text = detectedText
}

Microsoft Cognitive Computer Vision APIの準備

Microsoft Cognitive Services Computer Vision APIのコンソールからComputer Vision - Previewをサブスクライブします(まだサインアップしていない方はGet stated for freeします)。 するとキーが表示されます。このキーをSwiftコードで使います。Google Cloudと違い、こちらは請求情報を登録しなければ有料プランになる前に自動で止まりますが、やはりキーが流出すると大変なことになる場合がありますので取り扱いに注意しましょう。

Microsoft Cognitive Computer Vision APIの実装

APIの公式な仕様はこちら

(2017/01/17からエンドポイントが変わっています。旧エンドポイントは4/15で廃止になります。この記事では新エンドポイントを使用しています)

こちらもPNGのみを対象にします。MicrosoftComputer Vision APIは画像をoctet-streamで直接送り、リクエストヘッダでAPIキーを指定します。Alamofireバイナリを送信するには、upload(...)を使います。Cognitive Vision APIにも画像サイズの制限がありますが、ここでもリサイズなどは省略しています。

func detectTextMs() {
    // 仮にPNGのみ対象
    // 画像サイズの制限があるので本当は大きすぎたらリサイズしたりする必要がある
    let imageData = UIImagePNGRepresentation(image.image!)!
    // Ocp-Apim-Subscription-KeyにCognitive Serviceで取得したAPIキーを入れる
    let httpHeader: HTTPHeaders = [
        "Content-Type": "application/octet-stream",
        "Ocp-Apim-Subscription-Key": msApiKey
    ]
    // 言語に日本語を指定して向きの検出も行うようにする
    Alamofire.upload(imageData, to: "https://westus.api.cognitive.microsoft.com/vision/v1.0/ocr?language=ja&detectOrientation=true", method: .post, headers: httpHeader).validate(statusCode: 200..<300).responseJSON { response in
        self.msResult(response: response)
    }
}

さて、こちらもAPIの戻りを文字列に戻しましょう。やはり結果はJSONで、["regions"]["lines"]["words"]["text"]に検出した文字が入っています。

func msResult(response: DataResponse<Any>) {
    guard let result = response.result.value else {
        // レスポンスが空っぽだったりしたら終了
        return
    }
    let json = JSON(result)
    let regions: JSON = json["regions"]
    var detectedText: String = ""
    // 結果からテキストを取り出して一つの文字列にする
    regions.forEach { (_, region) in
        let lines = region["lines"]
        lines.forEach { (_, line) in
            let words = line["words"]
            words.forEach { (_, word) in
                detectedText += word["text"].string!
            }
        }
    }
    text.text = detectedText
}

実行してみる

では実行してみましょう。今回は適当な画像として、ガラパゴスWebサイトの企業理念のページスクリーンショットを使ってみます。シミュレータに画像をドロップしたらカメラロールから画像を選択してみましょう。

Googleのテキスト検出結果はこちら。

f:id:glpgsinc:20170120163536p:plain

(結果が二重に表示されているのはご愛嬌ですね)

そして、Microsoftの結果はこちら。

f:id:glpgsinc:20170120163609p:plain

うまくテキストが検出できましたね。

最後に

ごく簡単なテキスト検出アプリを作ってみました。テキスト以外にも顔検出やラベル付けなどもできますのでお試しあれ。

また、iOSアプリは、もうずっと以前にObjective-Cを書いたことはあったのですが、Swiftの方が書きやすいなという印象がありました。

さて、ガラパゴスでは、アプリもサーバもインフラもできちゃうエンジニアや、そのうちの一つだけでもできちゃうエンジニアを大絶賛超募集しています。皆様の応募をお待ちしています。

www.glpgs.com

では、ご機嫌よう。

この記事は業務の一環として業務時間を使って書きました

Swiftで直感的に書ける範囲比較演算を定義する

こんにちは、iOS開発チームの本柳です。

Pythonなどでは値の範囲チェック(m < x and n > xのような評価)をする時、数学の評価式のようなm < x < nと記述することが出来、大変分かりやすいですよね。

swiftでは、動的に変化する画面の座標が範囲内にあるか?という評価を行うケースがそれなりに多く、このような評価式をつくることが出来ると大変に便利。

便利ならば作ってしまおう!ということで~を使って似たような評価式を書けるようにしてみました。

オペレーターの定義

まずは、<,<=,>,>=に該当するオペレーターを定義します。

Pythonだとm > x > nのようにも書けますが、これはn < x < mと等価であるので、<,<=だけを書ければ良いはずです。

なので、<~!<=~に置き換えて定義出来るようにします。

// `<=`に該当するオペレーター
infix operator ~ { associativity left }
// `<`に該当するオペレーター
infix operator ~! { associativity left }

こうして定義することで、Swiftではm ~ x ~ nのように記述することが可能になります。 これはm <= x <= nと等価になります。

Swiftでは常に左辺の結果を利用したいので、オペレーターにはassociativity leftを指定して左結合するようにしておきます。

オペレーターが利用する処理の実装

実装方針は下記の通りです。

  • 最初の~オペレーターは中間比較オブジェクトを生成する
  • 次の~オペレーターは左辺に比較オブジェクトを取り、右辺にComparableな値をとって真偽値を返す

比較オブジェクト(_Rangeとして定義することにします)は中間状態、完全状態を持ちますが、この状態はファントムタイプで実装することにします。

まずはファントムタイプの定義です。

class _RangeState {}
class _RangeIntermediate: _RangeState {}
class _RangePerfection: _RangeState {}

次に_Rangeオブジェクトの定義を行います。

デフォルトの_Rangeオブジェクトはコンストラクタをプライベートにして、さらに、中間状態をもつインスタンスのファクトリメソッドを定義しておくことで、初回生成は常に中間状態を持つように定義します。

enum _Range_MinOp {
    case less
    case lessOrEq
}

enum _Range_MaxOp {
    case gt
    case gtOrEq
}

struct _Range<T: Comparable, U: _RangeState> {
    typealias MinOp = _Range_MinOp
    typealias MaxOp = _Range_MaxOp
    
    private let value: T
    private let min: T
    private let minOp: MinOp
    private let max: T?
    private let maxOp: MaxOp?

    // コンストラクタはプライベートにして、状態を持たないインスタンスを生成出来ないようにします
    private init(value: T, min: T, minOp: MinOp, max: T?, maxOp: MaxOp?) {
        self.value = value
        self.min = min
        self.minOp = minOp
        self.max = max
        self.maxOp = maxOp
    }

    // 初期状態では中間オブジェクトのみ生成することが出来ます
    static func buildIntermediate(value: T, min: T, minOp: MinOp) -> _Range<T, _RangeIntermediate> {
        return _Range<T, _RangeIntermediate>(value: value,
                                             min:   min,
                                             minOp: minOp,
                                             max:   nil,
                                             maxOp: nil)
    }
}

デフォルトの_Rangeオブジェクトの定義が出来たら、今度は中間状態の_Rangeオブジェクトを拡張して、完全状態の_Rangeオブジェクトを生成出来るようにします。

extension _Range where U: _RangeIntermediate {
    func toPerfection(max max: T, maxOp: MaxOp) -> _Range<T, _RangePerfection> {
        return _Range<T, _RangePerfection>(value: value,
                                           min:   min,
                                           minOp: minOp,
                                           max:   max,
                                           maxOp: maxOp)
    }
}

最後に完全状態の_Rangeインスタンスに値の評価する処理を実装すれば_Rangeオブジェクトは完成です。

extension _Range where U: _RangePerfection {
    var valueInRange: Bool {
        return valueLessThanMinimum && valueGtThanMaximum
    }

    private var valueLessThanMinimum: Bool {
        switch minOp {
        case .less:     return value > min
        case .lessOrEq: return value >= min
        }
    }

    private var valueGtThanMaximum: Bool {
        switch maxOp! {
        case .gt:     return value < max
        case .gtOrEq: return value <= max
        }
    }
}

オペレーターの関数を定義

ここまで出来たら最後に、演算子に適合させたい型とオペレーターを紐付ける処理を書いて完了です。

func ~<T: Comparable>(lhs: T, rhs: T) -> _Range<T, _RangeIntermediate> {
    return _Range<T, _RangeState>.buildIntermediate(rhs, min: lhs, minOp: .lessOrEq)
}

func ~!<T: Comparable>(lhs: T, rhs: T) -> _Range<T, _RangeIntermediate> {
    return _Range<T, _RangeState>.buildIntermediate(rhs, min: lhs, minOp: .less)
}

func ~<T: Comparable>(lhs: _Range<T, _RangeIntermediate>, rhs: T) -> Bool {
    let range = lhs.toPerfection(max: rhs, maxOp: .gtOrEq)
    return range.valueInRange
}

func ~!<T: Comparable>(lhs: _Range<T, _RangeIntermediate>, rhs: T) -> Bool {
    let range = lhs.toPerfection(max: rhs, maxOp: .gt)
    return range.valueInRange
}

これで下記のようにシンプルに範囲を比較する処理が書けるようになりました。

let value1 = 10
print(1 ~ value1 ~ 10)  // -> true
print(1 ~ value1 ~! 10) // -> false

let value2 = 1
print(1 ~ value2 ~ 10)   // -> true
print(1 ~! value2 ~ 10)  // -> false

株式会社ガラパゴスでは、Swiftをつかってプログラミングを楽しみたいエンジニアを絶賛大募集中です!

RECRUIT | 株式会社ガラパゴス iPhone/iPad/Androidのスマートフォンアプリ開発

たくさんのご応募、お待ちしております。

とのの「#QAアーキ 行ってきた〜!」

こんにちは!ガラパゴステストチーム とののです。
わたしは先日、QA勉強会「ここは苦しいところですが、どうか一つ、QAアーキテクチャを。」に参加してきました!
たいへん勉強になりましたので、ここでも振り返って自分に納めていきたいと思います。

参加前のとのの

わたしがこの勉強会に参加しようと思ったのは、単純に
「QAアーキテクチャってなんだ???知りたい!」と思ったからです。
内容が難しいかどうか、話についていけるかが参加前一番気になるところです。今回はイベントの詳細ページをみて、”QAに関わる全ての方”とあったので「とりあえずいける!!!」という勢いでした笑

QAアーキに参加したよ

connpass.com

QA勉強会「ここは苦しいところですが、どうか一つ、QAアーキテクチャを。」
2016/12/20 19:00〜 @電気通信大学
講演1「QAアーキテクチャの話」:勉強会主催者 藤沢 耕助さん
講演2「ここは苦しいところですが、どうか一つ、QAアーキテクチャを。」:電気通信大学 にしさん

大学って落ち着きますね…

講演1「QAアーキテクチャの話」

QAアーキテクチャについて、藤沢さんがどう考えているか、というお話でした。

資料 Qaアーキテクチャの話

■ 「QA」とはなんだろう?
■「アーキテクチャ」とはなんだろう?
■では「QAアーキテクチャ」とは?

(以下ざっくりわたしが理解した内容)

  • 顧客の要求との齟齬が少ないほど「高品質」と言えるのではないか。
  • では品質を保証するためにできることはなんだろう?
    →お客さんに納得してもらうこと?それには何かしらの基準や観点が必要ですね。
    →バグがないこと、は示せないけど、特定の範囲でならバグゼロは示せる→納得してもらえる、のでは?

  • QAとは:何の品質を何の目的で保証しているのかを理解、納得してもらうことなんじゃないだろうか

  • テストにおけるアーキテクチャ設計とは
     何をテストするのか(テスト要求分析)
     そのテストの目的は何か(テスト対象分析)
     どのようにテストするのか(テスト条件分析)
    →テストを分析して、設計していくこと?

  • テストアーキテクチャはQAアーキテクチャの一部で、お客さんに理解、納得してもらうためのいろんなアーキテクチャのまとめがQAアーキテクチャなんじゃないだろうか
    →レビューアーキテクチャ、テストアーキテクチャ、プロセス保証アーキテクチャ、インフラアーキテクチャなど

とのの。。(ふむふむ)
「テスト」がQAの一部、というところにはっとしました。
いままでひたすらアプリを動かして検証して、テストのことばかり考えていましたが、
品質保証にはテスト以外のアプローチもある、ということを教えてもらった気がします。
これからもテストをしてテストを良くして品質保証することに変わりはないですが、講演を聞いて視野が広がりました。

講演2「ここは苦しいところですが、どうか一つ、QAアーキテクチャを。」

QAが機能しないときっていうのは、上手くいっていなくて、なにをすればいいかはわかっているけど、制約とかしがらみで動けない状態が多い。
そんなとき!「QAアーキテクチャを」何卒… という意味だそうです。タイトルにしっくりきました。

資料 Yasuharu Nishi, 電気通信大学 | SlideShare

■テストや品質保証における問題はこういうところです
■QAアーキテクチャに慣れていないので、テストアーキテクチャを設計してみましょう
■レビュー段階で同じように設計することで、テストを楽にすることができる
■では、QAアーキテクチャを設計しましょう

  • まずテスト観点図をかいてみよう
     このテストの意図は何か、というところを考えて書くことがポイント
     テストの意図が明確になり、何をどうするとバグが起きるのか、が図からわかる
     どこで何を保証したのかが図からわかる
  • 開発者が気をつけることもわかってくるので、自然に「レビュー観点」が明らかになってくる
  • なのでまず「レビュー設計」をしましょう
  • レビューには副次的役割もあり、その狙いも含めて考えましょう
  • ではQAアーキテクチャを設計していきましょう
  • ふわふわはっきりしない「品質」をモデリングして、はっきりさせましょう: QA観点モデル
  • 開発工程ごとに、さっき決めたどの観点が保証されているのかを整理しましょう: アシュアランスパイプライン
  • そのパイプラインを支える技術力を整理して持っておきましょう: テックシェルフ

とのの。。(ふむふむ)
質疑応答でにしさんがおっしゃっていた、「初めから全部わかってやるのなんて無理。はじめは40点でいいから試してみよう」
という回答がわたしにとってすごくヒントになりました。
直接QAアーキテクチャを設計する、ということはまだまだできそうにないのですが、
わたしの仕事に置き換えて、力をつけるためにはどんなところに注目して仕事をすればいいかが少しわかった気がします。

さいごに

今回もたいへん勉強になりました!
Twitterでも参加したかったのですが、正直いっぱいいっぱいでした!!!
もっとサクッ!すぱッッ!と文章がかけるようになりたいです。

スピードも含めてスキルアップしていきたいと思います!!
ではまたお会いいたしましょう、とののでした。

www.glpgs.com