御機嫌よう、最近Swiftな世界に入門しているガラパゴスのおとめです。
今回は、SwiftからGoogle Cloud Vision APIとMicrsoft Cognitive Computer Vision APIのテキスト検出(OCR)を使ってみようと思います。
なお、この記事はSwift 3.0とXcode 8.2.1を対象にしています。
プロジェクトの作成
まず適当にプロジェクトを作ります。また、今回はAlamfireとSwiftyJSONを使っていますが、これらのライブラリはCocoaPodsでインストール・管理しました(インストール方法などは省略します)。
ではサクッとViewを作ってしまいます。今回はカメラロールから選んだ画像を上に、検出したテキストを下に表示したいと思います。
カメラロールを表示するボタンは何でもいいのですが、ここではNavigation Bar
においてみました。
次に画像とテキストの表示ですが、わかりやすく半々に配置するために、Vertical Stack View
を使ってみます。ぽいっとドラッグしたら、Viewをいっぱいに使うために、Add New Constraints
で上下左右を0に設定します。
画像とテキストを半々に表示するために、Stack View
のDistribution
をFill Equally
にします。
Vertical Stack View
の設定が終わったら、Image View
とLabel
を放り込みます。すると以下のように、自動的に半々になりましたね?
Label
は複数行表示できるようにするために、Lines
に0
を設定しておきます。
これらの要素の配置が終わったら、Assistant Editor
を表示して、Ctrlキーを押しながらEditorにドラッグし、@IBAction
と@IBOutlet
を設定しておきます。
// 選択した画像の表示 @IBOutlet weak var image: UIImageView! // 検出したテキストの表示 @IBOutlet weak var text: UILabel! // カメラロールを表示するボタンを押した時の処理 @IBAction func selectImage(_ sender: Any) { }
カメラロールを使えるようにする
iOSシミュレータではカメラが使えないので、今回はカメラロールから選ぶことにします。iOSシミュレータを起動すると数枚の風景写真が入っていますが、適当な画像をシミュレータのウインドウにドロップするとカメラロールに登録されて選択できるようになります。
カメラロールにアクセスするには、info.plist
にPrivacy - Photo Library Usage Description
というキーを設定する必要があります(キーは自分で書かずにドロップダウンから選択します)。値にはアプリから初めてカメラロールを使うときに表示されるアクセス許可のダイアログに表示するメッセージを入れます。
では実際にカメラロールを表示する処理を書いていきましょう。
@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) } }
次に、画像を選択した時の処理を書きます。まずはカメラロールからのメッセージを受け取れるようにするために、UIImagePickerControllerDelegate
とUINavigationControllerDelegate
を継承します。
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呼び出しに取り掛かりましょう。
テキスト検出する
はじめに書いたように、GoogleとMicrosoftの両方を試してみます。本来なら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呼び出しの実装
今回はPNGのみを対象にします。他のフォーマットを使いたい場合、例えばJpegならUIImageJPEGRepresentation
などを使います。
では仕様に沿ってリクエストを組み立てていきましょう。画像をbase64エンコードして、テキスト検出にはTEXT_DETECTION
を指定します。
また、リクエストヘッダに、キーの制限
で指定したBundle Identifier
を、URLにAPIキーを指定します。
ところで、Vision APIで指定できる画像にはサイズの制限があるのですが、ここではとりあえずリサイズなどはしないことにします(カメラで撮った写真をそのまま送るとサイズ制限をオーバーしますのでご注意ください)。
リクエストの処理には、はじめに書いたようにAlamofireを使います。リクエスト形式はJSONですので、encoding
にJSONEncoding.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の実装
(2017/01/17からエンドポイントが変わっています。旧エンドポイントは4/15で廃止になります。この記事では新エンドポイントを使用しています)。
こちらもPNGのみを対象にします。MicrosoftのComputer 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のテキスト検出結果はこちら。
(結果が二重に表示されているのはご愛嬌ですね)。
そして、Microsoftの結果はこちら。
うまくテキストが検出できましたね。
最後に
ごく簡単なテキスト検出アプリを作ってみました。テキスト以外にも顔検出やラベル付けなどもできますのでお試しあれ。
また、iOSアプリは、もうずっと以前にObjective-Cを書いたことはあったのですが、Swiftの方が書きやすいなという印象がありました。
さて、ガラパゴスでは、アプリもサーバもインフラもできちゃうエンジニアや、そのうちの一つだけでもできちゃうエンジニアを大絶賛超募集しています。皆様の応募をお待ちしています。
では、ご機嫌よう。
この記事は業務の一環として業務時間を使って書きました。