Web制作・アプリ開発をコストパフォーマンスで考えるなら

Firebase ML Kitのテキスト認識×翻訳を使ってアプリを作ってみた

こんにちは。graphy事業部の遠藤と申します。

Firebase ML Kitを検証するため、一部の機能を使って簡単にアプリを実装してみました。今回はその詳細について記載していきたいと思います。

Firebase ML Kitとは

Firebase ML Kitを使うことで、機械学習の機能をアプリに実装することができます。主な機能としてはテキストの認識、顔検出、ランドマークの認識、バーコードのスキャン、画像のラベル付け、テキストの言語の識別など機能をライブラリを通して簡単に利用することができます。Firebase ML Kitはまだベータ版となっております。詳しくは公式ドキュメントを参照してください。

ML Kit Image

今回は、カメラで写真を撮って画像上の英語を日本語に翻訳できるiOSアプリを実装してみようと思います。

Firebase ML Kit導入

XcodeでiOSの新規プロジェクトを作成して、アプリを実装していきます。

New Project

はじめに、スタートアップガイドを参考にFirebaseの準備を行ってください。準備が完了したら、CocoaPodsを使ってFirebaseをインストールします。CocoaPodsの導入については公式ドキュメントを参照してください。今回のアプリでは、テキスト認識と翻訳の機能を利用するので、Podfileの中身は以下のようになります。

# Podfile
target 'MLSample' do
  use_frameworks!

  # Pods for MLSample
  pod 'Firebase/MLVision'
  pod 'Firebase/MLVisionTextModel'
  pod 'Firebase/MLNLTranslate'
end

Podfileを作成したら、以下のコマンドでインストールします。

$ pod install

インストールが完了後、Firebaseの初期化を行うソースコードを追加します。

// AppDelegate.swift
...
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    FirebaseApp.configure() // 追加
    return true
}
...

以上で、Firebase ML Kitの導入までは完了です。

カメラで写真を撮影する機能を追加

続いてカメラで写真を撮影する部分を追加します。以下のソースで、カメラからのインプットがプレビューされるようになります。

// ViewController.swift
import AVFoundation
...
let captureSession = AVCaptureSession()
let deviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [AVCaptureDevice.DeviceType.builtInWideAngleCamera], mediaType: AVMediaType.video, position: AVCaptureDevice.Position.unspecified)
var mainCamera: AVCaptureDevice?
var innerCamera: AVCaptureDevice?
var currentDevice: AVCaptureDevice?
var photoOutput : AVCapturePhotoOutput?
var cameraPreviewLayer : AVCaptureVideoPreviewLayer?

func setupCamera() {
    captureSession.sessionPreset = AVCaptureSession.Preset.photo

    let deviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [AVCaptureDevice.DeviceType.builtInWideAngleCamera], mediaType: AVMediaType.video, position: AVCaptureDevice.Position.unspecified)
    let devices = deviceDiscoverySession.devices
    for device in devices {
        if device.position == AVCaptureDevice.Position.back {
            mainCamera = device
        } else if device.position == AVCaptureDevice.Position.front {
            innerCamera = device
        }
    }
    currentDevice = mainCamera

    do {
        let captureDeviceInput = try AVCaptureDeviceInput(device: currentDevice!)
        captureSession.addInput(captureDeviceInput)
        photoOutput = AVCapturePhotoOutput()
        photoOutput!.setPreparedPhotoSettingsArray([AVCapturePhotoSettings(format: [AVVideoCodecKey : AVVideoCodecType.jpeg])], completionHandler: nil)
        captureSession.addOutput(photoOutput!)
    } catch {
        print(error)
    }

    cameraPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
    cameraPreviewLayer?.videoGravity = AVLayerVideoGravity.resizeAspect
    cameraPreviewLayer?.connection?.videoOrientation = AVCaptureVideoOrientation.landscapeRight
    cameraPreviewLayer?.frame = view.frame
    view.layer.insertSublayer(self.cameraPreviewLayer!, at: 0)
}
...

次に、Main.storyboard上で写真を撮影するボタンと戻るボタンを追加します。

Add Buttons

追加できたら、Storyboard上でボタンの項目を右クリックのメニューからReferencing Outletsを使ってソースに紐づけます。

Referencing Outlets

// ViewController.swift
...
@IBOutlet weak var captureButton: UIButton!
@IBOutlet weak var backButton: UIButton!
...

また、Sent Events>Touch Up Insideを同じくソースに紐づけます。

// ViewController.swift
...
@IBAction func captureButton_TouchUpInside(_ sender: Any) {
}

@IBAction func backButton_TouchUpInside(_ sender: Any) {
}
...

ボタンの見た目を整えます。

// ViewController.swift
...
func setupCaptureButton() {
    captureButton.layer.borderColor = UIColor.gray.cgColor
    captureButton.layer.borderWidth = 5
    captureButton.clipsToBounds = true
    captureButton.backgroundColor = UIColor.white;
    captureButton.layer.cornerRadius = min(captureButton.frame.width, captureButton.frame.height) / 2
}

func setupBackButton() {
    backButton.layer.borderColor = UIColor.white.cgColor
    backButton.layer.borderWidth = 5
    backButton.clipsToBounds = true
    backButton.layer.cornerRadius = min(captureButton.frame.width, captureButton.frame.height) / 2
}
...

撮影ボタンを押したタイミングで、カメラに写っている画像が出力されるようにします。

// ViewController.swift
...
// AVCapturePhotoCaptureDelegate を追加
class ViewController: UIViewController, AVCapturePhotoCaptureDelegate {
...
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
    if let imageData = photo.fileDataRepresentation(), let uiImage = UIImage(data: imageData) {
        disableCamera()
        // TODO: テキスト認識の処理をここに追加
    }
}

// disableCamera・enableCameraでは、カメラのプレビューの一時停止・再開を行う
func disableCamera() {
    captureSession.stopRunning()
    captureButton.isHidden = true
    backButton.isHidden = false
}

func enableCamera() {
    captureSession.startRunning()
    captureButton.isHidden = false
    backButton.isHidden = true
}

// 撮影ボタンを押された後の処理を追加
@IBAction func captureButton_TouchUpInside(_ sender: Any) {
    let settings = AVCapturePhotoSettings()
    settings.flashMode = .auto
    settings.isAutoStillImageStabilizationEnabled = true
    photoOutput?.capturePhoto(with: settings, delegate: self)
}

// プレビューが一時停止中に、戻るボタンを押したタイミングでプレビューを再開
@IBAction func backButton_TouchUpInside(_ sender: Any) {
    enableCamera()
}
...

以上が、カメラ撮影部分となります。

撮影した画像からテキスト認識する

ここから、Firebase ML Kitの機能を利用します。カメラ撮影後の処理に、テキスト認識の処理を追加していきます。テキスト認識の機能についての詳細は公式ドキュメントを参照してください。

// ViewController.swift
import Firebase
...
// カメラ撮影後の処理に、processTextを追加
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
    if let imageData = photo.fileDataRepresentation(), let uiImage = UIImage(data: imageData) {
        disableCamera()
        processText(image: uiImage)
    }
}
// 画像からテキスト認識して、ログに出力するまで
func processText(image: UIImage) {
    let vision = Vision.vision()
    let textRecognizer = vision.onDeviceTextRecognizer()
    let visionImage = VisionImage(image: image)
    textRecognizer.process(visionImage) { result, error in
        guard error == nil, let result = result else {
            print("[ERROR]: " + error.debugDescription)
            return
        }
        for block in result.blocks {
            let blockText = block.text
            print(blockText)
        }
    }
}
...

以上のソースを追加するだけで、画像から読み取ったテキストがログに出力されるようになります。

認識したテキストを翻訳する

続いて、認識したテキストを翻訳する処理を追加していきます。翻訳の機能についての詳細は公式ドキュメントを参照してください。

// ViewController.swift
...
var englishJapaneseTranslator: Translator?

// Translator オブジェクトの初期化と、翻訳モデルをデバイスにダウンロード
func setupTranslator() {
    englishJapaneseTranslator = NaturalLanguage.naturalLanguage().translator(options: TranslatorOptions(sourceLanguage: .en, targetLanguage: .ja))
    let conditions = ModelDownloadConditions(
        allowsCellularAccess: false,
        allowsBackgroundDownloading: true
    )
    if englishJapaneseTranslator != nil {
        englishJapaneseTranslator!.downloadModelIfNeeded(with: conditions) { error in
            guard error == nil else { return }
            print("[ERROR]: " + error.debugDescription)
        }
    }
}

// 認識されたテキストを、翻訳する処理を追加
func processText(image: UIImage) {
    let vision = Vision.vision()
    let textRecognizer = vision.onDeviceTextRecognizer()
    let visionImage = VisionImage(image: image)
    textRecognizer.process(visionImage) { result, error in
        guard error == nil, let result = result else {
            print("[ERROR]: " + error.debugDescription)
            return
        }

        let imageSize = image.size
        let aspectRatio = imageSize.width / imageSize.height
        let previewFrame = CGRect(x: view.frame.origin.x, y: view.frame.origin.y, width: view.frame.width * aspectRatio, height: view.frame.height)
        let xOffSet = (view.frame.width - previewFrame.width) * 0.5
        let widthRate = previewFrame.width / imageSize.height;
        let heightRate = previewFrame.height / imageSize.width;

        for block in result.blocks {
            let blockText = block.text
            let frame = block.frame;
            // 今回は、ある程度の文章になっているテキストを認識させたいので細かいのはスキップ
            if frame.width < 100 || frame.height < 100 {
                continue;
            }
            // 認識したテキストの翻訳処理
            if self.englishJapaneseTranslator != nil {
                self.englishJapaneseTranslator!.translate(blockText) { translatedText, error in
                    guard error == nil, let translatedText = translatedText else {
                        print("[ERROR]: " + error.debugDescription)
                        return
                    }
                    print(translatedText)
                }
            }
        }
    }
}
...

以上で、翻訳されたテキストがログに出力されるようになります。

翻訳したテキストを画面上に表示する

翻訳したテキストを画面上に表示し、カメラで撮影したテキストを翻訳できるアプリに仕上げます。

// ViewController.swift
...
// 翻訳したテキスト配置用のUIViewを追加
@IBOutlet weak var translateLabelView: UIView!

// 翻訳したテキストをUILabelで配置
func processText(image: UIImage) {
    let vision = Vision.vision()
    let textRecognizer = vision.onDeviceTextRecognizer()
    let visionImage = VisionImage(image: image)
    textRecognizer.process(visionImage) { result, error in
        guard error == nil, let result = result else {
            print("[ERROR]: " + error.debugDescription)
            return
        }

        let imageSize = image.size
        let aspectRatio = imageSize.width / imageSize.height
        let previewFrame = CGRect(x: self.view.frame.origin.x, y: self.view.frame.origin.y, width: self.view.frame.width * aspectRatio, height: self.view.frame.height)
        let xOffSet = (self.view.frame.width - previewFrame.width) * 0.5
        let widthRate = previewFrame.width / imageSize.height;
        let heightRate = previewFrame.height / imageSize.width;

        for block in result.blocks {
            let blockText = block.text
            let frame = block.frame;
            if frame.width < 100 || frame.height < 100 {
                continue;
            }
            if self.englishJapaneseTranslator != nil {
                self.englishJapaneseTranslator!.translate(blockText) { translatedText, error in
                    guard error == nil, let translatedText = translatedText else {
                        print("[ERROR]: " + error.debugDescription)
                        return
                    }
                    // 認識したテキストの位置にUILabelを配置
                    DispatchQueue.main.async {
                        let label = UILabel(frame: CGRect(x: xOffSet + frame.origin.x * widthRate, y: frame.origin.y * heightRate, width: frame.width * widthRate, height: frame.height * heightRate))
                        label.text = translatedText
                        label.textColor = UIColor.white
                        label.backgroundColor = UIColor.red
                        label.numberOfLines = 0
                        label.adjustsFontSizeToFitWidth = true
                        self.translateLabelView.addSubview(label)
                    }
                }
            }
        }
    }
}

// プレビューを再開した際に、配置したUILabelを削除
@IBAction func backButton_TouchUpInside(_ sender: Any) {
    enableCamera()
    for view in translateLabelView.subviews {
        view.removeFromSuperview()
    }
}
...

デモ

こちらが完成したアプリのデモになります。Wikipediaから引っ張ってきた英文をテキストエディタに貼り付けて、それをカメラで撮影してみました。

Demo

まとめ

Firebase ML Kitは導入も簡単で、機械学習の機能を簡単に導入することができました。アプリ開発で機械学習の機能を使いたい場合に一度Firebase ML Kitを検討してみてはいかがでしょうか。今回作成したアプリのソースコードはGitHubにアップしておきますので、参考にしていただければ幸いです。 https://github.com/ekazuki/firebase-ml-sample