見出し画像

iOSで英語手書き入力をやってみる

これは mikan Advent Calendar 2023 19日目の記事です。

皆様こんにちは。
mikanでiOSエンジニアをしているSabと申します。
mikanのAdvent Calender2023はお楽しみいただけておりますでしょうか?
ぜひコタツでみかんでも食べながらゆる〜くお楽しみいただければと思います。

昨日は@uk_oasisさんの「めくるめく自作キーボードの世界〜光るキーボードは男のロマンやで〜」でした。
自作キーボードは奥が深いですね…僕は全然その辺りに手を出していないのですが、これから自作を検討している方は是非参考にしてみてください。
そういえば、Pittaというサービスでご本人から無料で直接話を聞けるチャンスもあるみたいなので、気になった方はいますぐクリック!!


それでは、本日Day19ではiOSの実装よりのお話しを書かせていただこうと思います。
といっても、mikanとは関係なく最近プライベートでPencilKitを使った実装に触れる機会があったので、この機会に手書きで英語を入力する方法を書いてみます。
(いやそれ、スクリブルでできるで?はナシの方向で…)


何を作るか

今回は単純に手書きで書いた英語を認識してテキスト表示するアプリを作ろうと思います。


こんな感じで、一番下の白い入力スペースに手書きで文字を入力し、オレンジのEnterボタンを押すとその上に認識したテキスト情報と実際に書いた文字が表示されるというだけのカンタンなアプリです。
それでは早速やっていきましょう!

UIを作る

プロジェクトを作成し、まずは画面から作っていこうと思います。

import SwiftUI

struct ContentView: View {
  
  var body: some View {
    ZStack {
      Color(uiColor: .systemGray6)
      VStack {

        Spacer()

        Text("mikan")
          .font(.system(size: 24, weight: .bold))
          .background(.white)

        Spacer()

        Button {
          // TODO: Button Action
        } label: {
          Text("Enter")
            .font(.system(size: 20, weight: .medium))
            .foregroundStyle(.white)
            .padding(.vertical, 4)
            .frame(maxWidth: .infinity)
        }
        .background(.orange)
        
				TextField("後から手書き入力Viewに差し替え", text: $text)
          .frame(height: 100)
      }
      .padding()
    }
  }
}

ひとまずこんな感じですかね。
これでこんな画面になると思います。

もういきなりそれっぽいですね!
アプリ開発はこういう時が一番面白いですからね〜。
では次に手書き入力部分を作っていきたいと思います。
手書き入力にはPencilKitを使います。

import SwiftUI
import PencilKit

struct CanvasView: UIViewRepresentable {
  let canvas: PKCanvasView
  
  func makeUIView(context: Context) -> some UIView {
    canvas.backgroundColor = .white
    canvas.drawingPolicy = .anyInput
    let toolPicker = PKToolPicker()
    toolPicker.addObserver(canvas)
    toolPicker.setVisible(true, forFirstResponder: canvas)
    canvas.becomeFirstResponder()
    return canvas
  }
  
  func updateUIView(_ uiView: UIViewType, context: Context) {}
  
  func reset() {
    canvas.drawing = PKDrawing()
  }
}

こんな感じでしょうか。
これを先ほど作った画面に組み込んでいきます。
すると…

なんとこれだけでもう手書きができるアプリの完成です!すご🙄
でもこれだけではただ画面に線がかけるだけのアプリなので、書かれたものを英語として認識できるようにしていきます。

手書き入力を取得する

まずは入力された手書き情報を取得する必要があるため、Enterボタンが押されたタイミングでキャンバスの情報を取得します。
このまま書き進めても良いのですが、色々処理を足していくとViewの可読性が下がりそうなのでついでにViewModelとして切り出しましょう。

import Foundation
import PencilKit

final class ContentViewModel: ObservableObject {
  @Published var image: UIImage?

  private let canvasView = PKCanvasView()
  
  func didTapEnterButton() {
		let inputImage = canvasView.drawing.image(from: canvasView.bounds, scale: 1)
    self.image = inputImage
  }
}

一旦こんな感じですね。
PKCanvasView.drawing.image(from:)で入力情報をUIImageとして取得できます。
なんて便利なんだ…
これでView側にViewModelのimageプロパティを表示させるようにしてみます。

struct ContentView: View {
  @StateObject var viewModel = ContentViewModel()
  @State var text: String = "mikan"
  
  var body: some View {
    ZStack {
      Color(uiColor: .systemGray6)
      VStack {
        Spacer()

        Text(text)
          .font(.system(size: 24, weight: .bold))
          .background(.white)
        
        Spacer()
        
        if let image = viewModel.image {
          Image(uiImage: image)
        }

        Button {
          viewModel.didTapEnterButton()
        } label: {
          Text("Enter")
            .font(.system(size: 20, weight: .medium))
            .foregroundStyle(.white)
            .padding(.vertical, 4)
            .frame(maxWidth: .infinity)
        }
        .background(.orange)
        
        CanvasView(canvas: viewModel.canvasView)
          .frame(height: 100)
      }
      .padding()
    }
  }
}

これでボタンを押すと手書き入力した内容がボタンの上に画像として表示されるようになりました🙆‍♂️

文字認識する

では仕上げに取得した画像から文字認識で英語を取得していきましょう。
文字認識にはVision frameworkを使います。
では早速先ほどのViewModelに処理を足していきます。

final class ContentViewModel: ObservableObject {
  @Published var image: UIImage?
  @Published var displayText: String = ""

  private let canvasView = PKCanvasView()
  
  func didTapEnterButton() {
     
    guard let inputImage = canvasView.drawing.image(from: canvasView.bounds, scale: 1).cgImage else { return }
    self.image = UIImage(cgImage: inputImage)
    
    let request = VNRecognizeTextRequest { request, error in
      guard let results = request.results as? [VNRecognizedTextObservation],
            let resultText = results.flatMap({ $0.topCandidates(1) }).sorted(by: { $0.confidence > $1.confidence }).first else { return }

      self.displayText = resultText.string
      self.canvasView.drawing = PKDrawing()
    }
    
    request.recognitionLevel = .accurate
    request.recognitionLanguages = ["en_US"]
    request.usesLanguageCorrection = false
    let imageRequestHandler = VNImageRequestHandler(cgImage: inputImage)
    do {
      try imageRequestHandler.perform([request])
    } catch {
      self.displayText = error.localizedDescription
    }
  }
}

こんな感じでしょうか。
Vision frameworkを使ったOCRは他にも多くの記事が既に公開されているため細かい説明は省きますが、VNRecognizeTextRequestのコールバックに文字認識後の結果が返ってくるので、そこで認識結果をハンドリングする処理を行います。

今回は返ってきたresultsから1件ずつ認識結果を取得し、精度の高い順で並べ替えた最初のもの、つまり一番精度が高いものを結果として表示するようにしています。

あとはrecognitionLevelで認識精度を、recognitionLanguagesで認識言語を、usesLanguageCorrectionで補正の有無を設定してリクエストを実行します。

後は連続した入力を受け付けられるように文字認識したら手書きキャンバスをリセットする処理も入れておきましょう。
これでView側のテキストもViewModelから受け取るように変更して実行すれば…

お、おぉ、おや??
うまく認識されていない…ですね…

デバッグしてみるとどうやらresultsが0件になっているようです。
つまり文字が認識できていないということですね。

なんでや…?



……

おやおや、よく見てみると手書き入力を取得した画像の背景は透過になっていますね。
画像も表示させといてよかったー!
これだとコントラスト比が環境依存しそうなので、背景を白く塗りつぶすことにしましょう。

こんなextensionを追加して…

extension UIImage {
  func fillTransparentPixels(with color: UIColor) -> UIImage? {
      UIGraphicsBeginImageContextWithOptions(size, false, scale)
      let context = UIGraphicsGetCurrentContext()
      let rect = CGRect(origin: .zero, size: size)

      color.setFill()
      context?.fill(rect)

      draw(in: rect, blendMode: .normal, alpha: 1.0)

      let resultImage = UIGraphicsGetImageFromCurrentImageContext()
      UIGraphicsEndImageContext()

      return resultImage
  }
}


こんな感じで使います。

guard let inputImage = canvasView
      .drawing
      .image(from: canvasView.bounds, scale: 1)
      .fillTransparentPixels(with: .white)?
      .cgImage else { return }


これで…

なんということでしょう!
手書きが、デジタルテキストにしっかり変換されて表示されています!!最高!!!

あれ、そういえばこれってmikanユーザーさんから稀によく届く「単語を書いて覚えたい」っていう要望に…

💡 この記事の内容は個人の見解であり、所属する企業、団体とは一切関係ありません

おしまい

最後までお読みくださりありがとうございます!
mikanではiOSエンジニアをはじめ様々な職種を積極採用中です!!
iOSアプリを筆頭に、プロダクトも組織もまだまだ試行錯誤して磨いているフェーズですので、もし一緒に最高の英語学習アプリを作りたいと思ったら迷わずコチラからご連絡ください🙏
それでは、明日の担当はいつもお客様に寄り添ってくれているmikanのCS担当@yoshimi_ishidaさんです!
残りのAdvent Calendarも是非お楽しみください🎄


この記事が気に入ったらサポートをしてみませんか?