【iOS13】Vision.frameworkの文字認識(OCR)で遊んでみる

# はじめに
## Vision.framework とは
iOS11で登場した、画像解析のApple標準フレームワークです。
画像処理に関する研究分野のことを「コンピュータビジョン（computer vision）」と言いますが、`Vision.framework`という名称はそれに由来しています。
同じくiOS11から追加された機械学習フレームワークの`Core ML`が内部で使われています。

[Vision - Apple Developer Documentation](https://developer.apple.com/documentation/vision)

## iOS13でのアップデートについて
従来の`Vision.framework`では、文字と認識されていた部分は矩形の画像領域と判定されていましたが、iOS13ではその部分をテキストデータとして取得できるようになったようです。

自分はiOS11の頃の`Vision.framework`は触ったことがありませんが、当時の記事を拝見する限り、ハマリポイントも減り、簡単に実装できるようになったのではないかと思います（実際、100行にも満たないコードで実装が完了しました）。

WWDC 2019の動画が分かりやすいです。
[Text Recognition in Vision Framework - WWDC 2019 - Videos - Apple Developer](https://developer.apple.com/videos/play/wwdc2019/234/)

# 実装
公式サンプルコードをもとにテストアプリを作ってみます。
[サンプルコード / Locating and Displaying Recognized Text on a Document](https://developer.apple.com/documentation/vision/locating_and_displaying_recognized_text_on_a_document)

ボタンを押したらスキャン用カメラが起動し、画像から検出したテキストデータを画面に表示する簡単なアプリです。
※カメラ機能はシュミレーターでは使えないので、実機ビルドできる環境でお試しください

## 画面を作る
`UIButton`と`UITextView`だけのシンプルな画面です。
適当なAutoLayoutをつけて、`UIButton`はIBOutlet、`UITextView`はIBActionで繋げておきます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112818/28edd990-ec5c-457b-1010-392bd69479d3.png" width="320"/>

## コードを書く
まずインポートを記述します。

```swift:ViewController
import Vision
import VisionKit
```

`VNRecognizeTextRequest`をセットアップするメソッドを実装し、`viewDidLoad()`で呼び出します。

* `VNRecognizeTextRequest`で画像から検出したテキスト情報を受け取り、文字列として連結
* `recognitionLevel`で「文字認識のレベル」を`.accurate`に設定
    * `.fast`と`.accurate`の2つの選択肢がある
          * `.fast`は動画などのリアルタイム読み込みに向いており、速い代わりに文字認識の精度は低め
          * `.accurate`は非同期での読み込みに向いており、若干時間はかかるが、筆記体なども正しく認識できるほど精度が高い

```swift:ViewController
    override func viewDidLoad() {
        super.viewDidLoad()
        setupVision()
    }
    
    // Setup Vision request as the request can be reused
    func setupVision() {
        let textRecognitionRequest = VNRecognizeTextRequest { request, _ in
            guard let observations = request.results as? [VNRecognizedTextObservation] else {
                print("The observations are of an unexpected type.")
                return
            }
            // 解析結果の文字列を連結する
            let maximumCandidates = 1
            for observation in observations {
                guard let candidate = observation.topCandidates(maximumCandidates).first else { continue }
                self.resultingText += candidate.string + "\n"
            }
        }
        // 文字認識のレベルを設定
        textRecognitionRequest.recognitionLevel = .accurate
        self.requests = [textRecognitionRequest]
    }
```

ボタンが押されたときにスキャン用のカメラが起動するように実装します。

```swift:ViewController
    @IBAction func cameraButtonTapped(_ sender: UIButton) {
        let documentCameraViewController = VNDocumentCameraViewController()
        documentCameraViewController.delegate = self
        present(documentCameraViewController, animated: true)
    }
```

`VNDocumentCameraViewController`のデリゲートメソッド`documentCameraViewController(_:didFinishWith:)`を実装します。
これはカメラでスキャン用画像の保存に成功したときに呼ばれます。

* 非同期でリクエストを実行
* メインスレッドで`textView`に検出したテキスト情報を表示する


```swift:ViewController
extension ViewController: VNDocumentCameraViewControllerDelegate {

    // DocumentCamera で画像の保存に成功したときに呼ばれる
    func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) {
        controller.dismiss(animated: true)

        // Dispatch queue to perform Vision requests.
        let textRecognitionWorkQueue = DispatchQueue(label: "TextRecognitionQueue",
                                                             qos: .userInitiated, attributes: [], autoreleaseFrequency: .workItem)
        textRecognitionWorkQueue.async {
            self.resultingText = ""
            for pageIndex in 0 ..< scan.pageCount {
                let image = scan.imageOfPage(at: pageIndex)
                if let cgImage = image.cgImage {
                    let requestHandler = VNImageRequestHandler(cgImage: cgImage, options: [:])

                    do {
                        try requestHandler.perform(self.requests)
                    } catch {
                        print(error)
                    }
                }
            }
            DispatchQueue.main.async(execute: {
                // textViewに表示する
                self.textView.text = self.resultingText
            })
        }
    }
}
```

最後に、`info.plist`に`Privacy - Camera Usage Description`をKeyとして追加します。
Valueには`文字認識のためにカメラを使用します`など適当な文言を入れます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112818/28446b02-e300-2d38-7da6-37d2a55d7fb7.png" width="620"/>

実機ビルドに成功したら、文字認識で遊ぶ準備完了です:tada:

## おまけ
下記メソッドを`viewDidLoad()`などで呼び出すことで、文字認識がサポートされている言語が配列で取得できます。
現状日本語はサポートされておらず、英数字だけが対応しているようです。

```swift:ViewController
    // 文字認識できる言語の取得
    private func getSupportedRecognitionLanguages() {
        let accurate = try! VNRecognizeTextRequest.supportedRecognitionLanguages(for: .accurate, revision: VNRecognizeTextRequestRevision1)
        print(accurate) // ["en-US"]
    }
```

## GitHub
ここで書いたコードはGitHubに上げています。
https://github.com/orimomo/VisionFrameworkTest

# 遊んでみる
## ティッシュ
まずは目についたティッシュをパシャリ :camera_with_flash: （勝手に文字領域を認識してくれて賢い）
まずはお手並み拝見です。
<img width="320" alt="IMG_0886.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112818/bc9cf1e1-139e-118d-974e-7b84e18b091d.png">

↓↓解析結果
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112818/4cabe0a5-835c-b041-7921-0aeff8cb422b.png" width="320"/>
:100: :white_flower:

## ANKERケーブルの箱の裏
続いてANKERケーブルの箱の裏をパシャリ :camera_with_flash:
文量が多めですが、どうでしょうか？
<img width="320" alt="IMG_0888.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112818/6cb5bdaf-2278-af40-30be-687ec6503e9b.png">

↓↓解析結果
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112818/4be217c5-df7f-190c-2d5d-ff03f34d6c61.png" width="320"/>
`TextView`が小さくて全部お見せできないのが残念ですが、ちゃんと最後まで認識していました！
箱に書いてある文字が小さいので、半角スペースを読み取れていない箇所が稀にあるものの、ほぼ :100: と言える精度です。 

## Googleサイト
最後はGoogleサイトをパシャリ :camera_with_flash: 
これまでのような印刷物ではなく、また日本語も含まれていますね。結果はいかに！
<img width="320" alt="IMG_0892.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112818/e7639c8a-068f-bbcd-7c69-d63489d03da2.png">

↓↓解析結果
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112818/9b5b2c07-f9a6-4ad0-1281-6d5c0a063af6.png" width="320"/>
日本語部分はサポートされていないので当然だめでしたが、英語部分は問題なく認識できました！

# おわりに
簡単なコードだけで文字認識が実現できたのは驚きでした。
日本語がサポートされれば活用の幅がぐっと広がると思うので、今後の拡張に期待したいと思います:grin:

## 参考記事
* [iOS13から標準サポートされる文字認識 - Qiita](https://qiita.com/KenNagami/items/a75b2bc282ad05a6dcde)
* [Vision.frameworkでカメラ画像のテキスト認識を行う - Qiita](https://qiita.com/beckyJPN/items/449d0dc97f14e64c0ad8#github)