環境
- iOS16
- Xcode14.1
- シミュレーターにて動作を確認
やりたいこと
- UIImageViewの画像の文字認識を行う
- 取得した文字列を下部のUILabelに表示
- UIImageView上に赤い矩形で認識した文字列の位置を表示
Vision Framework
iOSで画像認識を行うことができるフレームワーク。今回は文字列の認識機能を利用した。
下記のチュートリアルの通りに進めることで文字列認識が行えた。
https://developer.apple.com/documentation/vision/recognizing_text_in_images
文字列認識処理は下記で実行できた。UIはStoryboardで配置している。
import UIKit
import Vision
import AVFoundation
class ViewController: UIViewController {
let image = UIImage(named: "sample_image")!
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var label: UILabel!
@IBAction func didPushButton(_ sender: Any) {
run()
}
override func viewDidLoad() {
super.viewDidLoad()
imageView.image = image
}
// 文字列認識実行
private func run() {
guard let cgImage = image.cgImage else { return }
let requestHandler = VNImageRequestHandler(cgImage: cgImage)
let request = VNRecognizeTextRequest(completionHandler: recognizeTextHandler)
// 日本語に設定
request.recognitionLanguages = ["ja-JP"]
do {
try requestHandler.perform([request])
} catch {
print("Unable to perform the requests: \(error).")
}
}
// 文字列認識結果
func recognizeTextHandler(request: VNRequest, error: Error?) {
guard let observations =
request.results as? [VNRecognizedTextObservation] else {
return
}
let recognizedStrings = observations.compactMap { observation in
return observation.topCandidates(1).first?.string
}
// 取得できた文字列配列を表示
label.text = recognizedStrings.debugDescription
// ** 矩形を表示(後述する) ** //
showBoundingRect(observations)
}
文字列認識した位置を画像上に表示する
ここがハマりポイントだった。
上記のチュートリアルでも矩形の位置を取得する方法が紹介されている。VNImageRectForNormalizedRectを用いて、画像上のどの位置かが取得できた。
しかし、今回はUIImageViewのContentModeをAspectFitに設定しているのでアスペクト比を維持したまま、画像が縮小されて表示されている。またx,y座標もUIImageViewの中央に移動している。
-
Visionで取れる矩形の位置はy座標は反対向きになっているので反転させる必要があった。
- https://qiita.com/john-rocky/items/86ddab067fb1179976d0 こちらの記事で紹介されているようにy軸方向に反転する計算を行うことで対応できた。
-
画像は元画像よりサイズを端末サイズに合わせて縮小されているので、その縮小率に合わせて矩形の位置を調整する必要があった。
- UIImageView内の縮小されたimageのCGRectを取得したい。AVFoundationのメソッドを利用することで取得できた。(https://qiita.com/fr0g_fr0g/items/35339e0b9d977a404a22)。 取得できたCGRect.sizeと元画像のCGSizeから縮小率を計算できた。
-
画像はUIImageViewの真ん中に表示されるのでその分平行移動させる必要があった。
- 2で取得できたx,yの座標を足して、並行移動することで対応できた。
// 文字列認識できた位置に赤い矩形を表示
private func showBoundingRect(_ observations: [VNRecognizedTextObservation]) {
// ① 画像のRectを取得。メソッド内にてy座標の反転を行う。
let boundingRects = getBoundingRects(observations: observations)
// ② 画像のUIImageView内でのRectを取得。
let imageRectInView = imageRect()
// ② 画像の縮小率を取得。
let ratio = imageRectInView.width / image.size.width
boundingRects.forEach { rect in
let view = UIView()
view.layer.borderColor = UIColor.red.cgColor
view.layer.borderWidth = 1
let resizedRect = CGRect(x: rect.minX * ratio + imageRectInView.minX, // ③ 画像を並行移動
y: rect.minY * ratio + imageRectInView.minY, // ③ 画像を並行移動
width: rect.width * ratio,
height: rect.height * ratio)
view.frame = resizedRect
imageView.addSubview(view)
}
}
// ② UIImageView内のimageのCGRectを取得
func imageRect() -> CGRect {
return AVMakeRect(aspectRatio: image.size, insideRect: imageView.bounds)
}
func getBoundingRects(observations: [VNRecognizedTextObservation]) -> [CGRect] {
let boundingRects: [CGRect] = observations.compactMap { observation in
guard let candidate = observation.topCandidates(1).first else { return .zero }
let stringRange = candidate.string.startIndex..<candidate.string.endIndex
let boxObservation = try? candidate.boundingBox(for: stringRange)
let boundingBox = boxObservation?.boundingBox ?? .zero
let normalizedRect = VNImageRectForNormalizedRect(boundingBox,
Int(image.size.width),
Int(image.size.height))
// ① Visionのy座標はUIKitとは逆向きなので反転する必要がある
let yInImage = image.size.height - normalizedRect.minY - normalizedRect.height
return CGRect(x: normalizedRect.minX,
y: yInImage,
width: normalizedRect.width,
height: normalizedRect.height)
}
return boundingRects
}