iOS × Vision フレームワークで物体検出にチャレンジ!

この記事は Goodpatch Advent Calendar 13日目の記事です。

こんにちは、iOS エンジニアの田中です。この記事では、Visionフレームワークを用いた物体検出のおおまかな実装ポイントをまとめてみました。

きっかけ

この秋、社内でとあるアイデアをアプリでプロトタイプ実装してみました。

その名も「パクパクくん」、デザインは筆者によるもの...🙇

身の回りにあるものについて、その関連知識がシナプス的に気持ちよく得られたら良いよね、、というあるUXデザイナーからの思いつきがあり、まずはざっくり、以下のようなMVPから作り始めてみよう(そして出来上がったらオフィスにいる社員に試してもらおう)という試みでした。

  • iPhoneのカメラから取り込んだ映像をもとに
  • 映像に映った物体を検出し
  • 検出した物体に付随した豆知識を表示する

検出したオブジェクトをタップして豆知識を表示する様子

画像からの物体検出は未体験の領域でしたので、Apple の提供するサンプルプロジェクトが大変参考になりました。これは、 Vision フレームワークを用いてカメラ映像から物体検出し、検出結果を映像に対してオーバーレイ表示するというものです。このソースコードから大まかな実装方針を理解することができました。

Recognizing Objects in Live Capture

サンプルプロジェクト「BreakfastFinder」

Visionで物体検出を実装するポイント

Visionで物体検出を実装するにおけるポイントは3点です。

  1. 学習モデルを用意し、これをもとに検出リクエストを生成する
  2. 物体を検出したい画像に対して、検出リクエストを実行する
  3. 検出結果に応じた処理を実行する

学習モデルを用意し、これをもとに検出リクエストを生成する

// Visionの分析リクエストをセットアップ
//(画像処理が必要となる前に実行 e.g. カメラキャプチャのセットアップ時など)
func setupVisionRequest(with modelURL: URL) {
    do {
        let mlModel = MLModel(contentsOf: modelURL) // 学習モデルファイル(.mlmodel)のURL
        let vnModel = try VNCoreMLModel(for: mlModel) 
        let detectionRequest = VNCoreMLRequest(
            model: vnModel,
            completionHandler: { (request, error) in
                DispatchQueue.main.async(execute: {
                    if let results = request.results {
                        // ここに検出結果に応じて処理を実装
                    }
                })
            })
        self.detectionRequest = detectionRequest
    } catch let error as NSError {
        // ここにエラーハンドリングを実装
    }
}

Visionフレームワークでは内部的に Core ML を用いているため、学習モデルファイルには Core ML が用いる MLModel ファイル(.mlmodel)を用意します。 用意の仕方は2通りあります。

  1. 既存の学習モデルを使う
  2. 学習モデルを自作する(後述)

既存の学習モデルは、Apple が集約したものがあるので、まずはここからユースケースに見合うものを探して活用するのが手っ取り早いです。

Models - Machine Learning - Apple Developer

今回は物体検出ということで、YOLOv3 の学習済みモデル を用いました。検出可能な物体については、次の80のクラスに対応しています。

“person”, “bicycle”, “car”, “motorcycle”, “airplane”, “bus”, “train”, “truck”, “boat”, “traffic light”, “fire hydrant”, “stop sign”, “parking meter”, “bench”, “bird”, “cat”, “dog”, “horse”, “sheep”, “cow”, “elephant”, “bear”, “zebra”, “giraffe”, “backpack”, “umbrella”, “handbag”, “tie”, “suitcase”, “frisbee”, “skis”, “snowboard”, “sports ball”, “kite”, “baseball bat”, “baseball glove”, “skateboard”, “surfboard”, “tennis racket”, “bottle”, “wine glass”, “cup”, “fork”, “knife”, “spoon”, “bowl”, “banana”, “apple”, “sandwich”, “orange”, “broccoli”, “carrot”, “hot dog”, “pizza”, “donut”, “cake”, “chair”, “couch”, “potted plant”, “bed”, “dining table”, “toilet”, “tv”, “laptop”, “mouse”, “remote”, “keyboard”, “cell phone”, “microwave”, “oven”, “toaster”, “sink”, “refrigerator”, “book”, “clock”, “vase”, “scissors”, “teddy bear”, “hair drier”, “toothbrush”

このリストに存在しない物体を検出したい場合は、後述の方法を用いて自前で学習モデルを作成する方法があります。

物体を検出したい画像に対して、検出リクエストを実行する

静止画像やカメラのフレーム更新ごとに得られる画像など、入力画像のピクセルバッファをもとに VNImageRequestHandler を生成し、上で生成した画像分析リクエストを実行します。

let imageRequestHandler = VNImageRequestHandler(
    cvPixelBuffer: pixelBuffer,
    orientation: /*画像の方向*/
)
do {
    try imageRequestHandler.perform([detectionRequest])
} catch {
    // ここにエラーハンドリングを実装
}

ピクセルバッファは CVPixelBuffer 型となっているため UIImage を入力として使いたい場合は変換する必要があります。

上述のサンプルプロジェクトの場合は AVCaptureVideoDataOutputSampleBufferDelegate.captureOutput:didOutputSampleBuffer:fromConnection: から毎フレーム得られる CMSampleBufferCoreMedia の関数を用いて CVPixelBuffer 変換しています。

また RealityKit.ARView から映像を得る場合は、ARSessionDelegate.session(_:, didUpdate frame:) から得られる frame: ARFrame から、frame.capturedImage として取得することができます。

検出結果に応じた処理を実行する

検出結果は、上述手順1. で生成した、分析リクエストのハンドラに返ってきます。

let detectionRequest = VNCoreMLRequest(
            model: vnModel,
            completionHandler: { (request, error) in
                // ここに検出結果が返ってくる
                guard let results = request.results else { return }
                DispatchQueue.main.async {
                    // 検出結果の表示処理など
                }
            })

この resultVNObservation 型の配列となっていますが、物体検出結果においてはその孫クラスにあたる VNRecognizedObjectObservation クラスのオブジェクトが個々の要素になります。(画像内に含まれるすべての検出結果が配列として返ってきます)

この VNRecognizedObjectObservation には、それぞれ次のような情報が含まれており、こうした情報を組み合わせて検出結果を画面に表示します。

  • labels:識別結果の配列
    • VNClassificationObservation の配列で、それぞれに検出物体のクラス名(e.g. person car など学習モデルで定められたもの)や確信度が含まれる。先頭要素が最も確信度が高いため、通常 labels[0] を用いる。
  • boundingBox:物体のバウンディングボックス(位置、サイズ)

ここでの boundingBox はピクセルバッファにおける画像サイズをもとにしているので、そのまま UIView の座標系に用いることはできません。 例えば、カメラ映像を Aspect To Fill でビューに表示する場合、以下のような座標変換が必要になります。

// ピクセルバッファからcameraViewに座標変換する際のスケールを計算
let xScale: CGFloat = cameraView.bounds.size.width / bufferSize.height
let yScale: CGFloat = cameraView.bounds.size.height / bufferSize.width
var scale = fmax(xScale, yScale)
if scale.isInfinite {
    scale = 1.0
}

// Aspect To Fill するにおいて、画像をcameraViewに対して中央揃えする際のオフセットを計算
let offsetX = abs(bounds.size.width - scale * bufferSize.height)
let offsetY = abs(bounds.size.height - scale * bufferSize.width)

// cameraView 上に検出結果のバウンディングボックスを表示する際の frame を計算
detectionFrame = CGRect(
    x: detection.boundingRect.origin.y * bufferSize.height * scale - offsetX * 0.5,
    y: detection.boundingRect.origin.x * bufferSize.width * scale - offsetY * 0.5,
    width: detection.boundingRect.size.height * bufferSize.height - offsetX * 0.5,
    height: detection.boundingRect.size.width * bufferSize.width * scale - offsetY * 0.5
)

学習モデルを自作してみる

既存の学習モデルでは検出できない物体を検出したい場合は、その物体を学習した機械学習モデル(MLModel)を生成する必要があります。そのために必要なのは以下のステップです。

  1. 学習データ(画像)を収集する
  2. 個々の学習データに対するアノテーションを作成する
  3. Create ML.app により機械学習を実行し、MLModelを生成する

学習データ(画像)の収集

学習画像の収集は、自身で必要データを撮影収集するのが基本ですが、これはなかなか骨の折れる作業となります。たとえば商用でなく個人的利用に留めるのであれば、Google 画像検索などから一括して画像を収集することも選択肢としてあります。

qiita.com

また、iPhone で撮影する画像は解像度が非常に高く(たとえば iPhone 14 Pro の場合 4032x3024)、学習時のコストもそれに伴い激増するため、必要十分なサイズに縮小することをお勧めします(筆者は短辺500pxくらいにしました)。

学習データに対するアノテーション

次に学習データに対するアノテーションですが、アノテーションとは画像中のどの領域に何が映っているのかの情報、機械学習をする際の「正解」を作り上げる工程です。

これは、画像ひとつひとつに対して行う必要があり、これも骨の折れる作業ですが、例えば RectLabel.appRoboflow がが提供するアノテーションツール使うことで、比較的楽に行うことができます。

Roboflow上でのアノテーション作業の様子

アノテーション結果はJSONやXMLといったさまざまな形式で表現されますが、Create ML.app を用いて機械学習する場合は、JSON形式かつCreate ML用のデータ構造である必要があります。(データセットの出力時に、上述のツールでこのフォーマットを選択することになります)

// Roboflow を用いて出力したアノテーションデータの例
[
  {
    "image": "photo1.jpg",
    "annotations": [
      {
        "label": "pen",
        "coordinates": { "x": 235, "y": 173, "width": 171.5, "height": 216 }
      },
      {
        "label": "pen",
        "coordinates": { "x": 361, "y": 135, "width": 60.5, "height": 250 }
      }
    ]
  },
  {
    "image": "photo2.jpg",
    "annotations": [
      {
        "label": "pen",
        "coordinates": { "x": 170.5, "y": 324, "width": 206, "height": 307 }
      },
      {
        "label": "pen",
        "coordinates": { "x": 300, "y": 228, "width": 82.5, "height": 378.5 }
      },
      {
        "label": "pen",
        "coordinates": { "x": 382, "y": 531, "width": 214, "height": 126.5 }
      }
    ]
  }
]

このアノテーションファイルと画像とを Create ML.app に取り込み学習を実行することで、学習モデルファイル(.mlmodel)を得ることができます。(本記事ではこの工程は割愛)

Create ML.app は Xcode のメニューから起動が可能

おわりに

普段のプロダクトでは触れる機会のないこうした技術も、気軽にプロトタイプしてみると新たな学びとなって良いと思います。

アノテーションや機械学習の作業ステップについてもここで記そうかと思ったのですが、非常に長くなりそうだったので、次回に分けたいと思います。

おまけ:アイデアスケッチ

ちなみに完全な余談ですが、このプロトタイプを作っていた頃に甥っ子と教育テレビを見ていたところ、このアイデアにそっくりな番組を発見して、とても印象的でした。


グッドパッチにはこのように、デザイナーとエンジニアとが距離を近くに、アイデアと技術を互いに持ち寄ってトライアンドエラーできる、楽しい環境があります。

デザインも開発も好きだという方、もしご興味をお持ちいただけましたら、気軽にカジュアルに面談からいかがでしょうか。 お待ちしています!😀

【Design Div】Androidデベロッパー | 株式会社グッドパッチ