Edge TF Lite iOS のチュートリアル

作業内容

このチュートリアルでは、エクスポートされたカスタム TensorFlow Lite モデルを AutoML Vision Edge からダウンロードします。それから、モデルを使用して画像内の複数のオブジェクトを(境界ボックスとともに)検出する既製の iOS アプリを実行し、オブジェクト カテゴリのカスタムラベルを作成します。

エンド プロダクトのモバイルのスクリーンショット

目標

この入門用のエンドツーエンドのチュートリアルでは、コードを使用して次のことを行います。

  • TF Lite インタープリタを使用して、iOS アプリで AutoML Vision Object Detection Edge モデルを実行します。

始める前に

Git リポジトリのクローンを作成する

コマンドラインの次のコマンドを使用して、以下の Git リポジトリのクローンを作成します。

git clone https://github.com/tensorflow/examples.git

リポジトリ(examples/lite/examples/object_detection/ios/)のローカル クローンの ios ディレクトリに移動します。以下のすべてのコードサンプルを ios ディレクトリで実行します。

cd examples/lite/examples/object_detection/ios

前提条件

  • Git がインストールされている必要があります。
  • サポートされている iOS のバージョン: iOS 12.0 以降

iOS アプリを設定する

ワークスペース ファイルを生成して開く

オリジナルの iOS アプリの設定を開始するには、まず必要なソフトウェアを使用して、ワークスペース ファイルを生成する必要があります。

  1. まだ ios フォルダに移動していない場合は、移動します。

    cd examples/lite/examples/object_detection/ios
  2. ポッドをインストールして、ワークスペース ファイルを生成します。

    pod install

    このポッドを以前インストールしたことがある場合は、次のコマンドを使用します。

    pod update
  3. ワークスペース ファイルを生成したら、Xcode でプロジェクトを開けるようになります。コマンドラインでプロジェクトを開くには、ios ディレクトリから次のコマンドを実行します。

    open ./ObjectDetection.xcworkspace

固有の ID を作成してアプリをビルドする

Xcode で ObjectDetection.xcworkspace を開くには、まずバンドル識別子(バンドル ID)を一意の値に変更します。

  1. 左側のプロジェクト ナビゲータ上部にある ObjectDetection プロジェクト アイテムを選択します。

    サイド ナビゲーション内の ObjectDetection プロジェクトの画像

  2. [Targets] > [ObjectDetection] が選択されていることを確認します。

    選択した Targets オプションの画像

  3. [General] > [Identity] セクションで、[Bundle Identifier] フィールドを一意の値に変更します。望ましいスタイルは逆ドメイン名表記です。

    Identity セクションのバンドル ID の例の画像

  4. [Identity] の下部の [General] > [Signing] セクションで、プルダウン メニューからチームを指定します。この値は、デベロッパー ID で指定します。

    チームをプルダウン メニューから選択する画像

  5. iOS デバイスをパソコンに接続します。デバイスが検出されたら、デバイスの一覧から選択します。

    デフォルトのデバイスが選択されている

    接続したデバイスを選択

  6. すべての構成変更を指定したら、command+B キーを使用して Xcode でアプリをビルドします。

オリジナル アプリの実行

サンプルアプリは、デバイスの背面カメラで撮影されるフレーム内のオブジェクト(境界ボックスとラベル)を続けて検出するカメラアプリです。アプリは、COCO データセットでトレーニングされた量子化 MobileNet SSD モデルを使用します。

ここでは、iOS デバイスでデモをビルドおよび実行する方法について、段階的に説明します。

モデルファイルは、ビルドおよび実行時に Xcode のスクリプトでダウンロードされます。TF Lite モデルをプロジェクトにわざわざダウンロードする手順は必要ありません。

カスタマイズしたモデルを挿入する前に、トレーニングされたモデルのベース「mobilenet」を使用するアプリのベースライン バージョンをテストします。

  1. Simulator でアプリを起動するには、Xcode ウィンドウの左上隅にある再生ボタン xcode 再生アイコン を選択します。

    カメラアクセスを許可するプロンプトの画像

  2. [Allow] ボタンを選択してアプリにカメラへのアクセスを許可すると、アプリはライブ検出とアノテーションを開始します。オブジェクトが検出され、各カメラフレーム内で境界ボックスとラベルでマークされます。

  3. デバイスを周囲のさまざまなものに向けて動かし、アプリが画像を正しく検出していることを確認します。

    エンド プロダクトのモバイルのスクリーンショット

カスタマイズしたアプリを実行する

カスタム オブジェクトの画像カテゴリで、再トレーニングされたモデルを使用するようにアプリを変更します。

モデルファイルをプロジェクトに追加する

デモ プロジェクトは、ios/objectDetection/model ディレクトリの 2 つのファイルを検索するように構成されています。

  • detect.tflite
  • labelmap.txt

これら 2 つのファイルをカスタム バージョンに置き換えるには、次のコマンドを実行します。

cp tf_files/optimized_graph.lite ios/objectDetection/model/detect.tflite
cp tf_files/retrained_labels.txt ios/objectDetection/model/labelmap.txt

アプリを実行する

iOS デバイスでアプリを再起動するには、Xcode ウィンドウの左上隅にある再生ボタン xcode 再生アイコン を選択します。

変更をテストするには、デバイスのカメラをさまざまなオブジェクトに向けて動かして、ライブ予測を表示します。

結果は次のようになります。

キッチン用品のカスタムアプリのオブジェクト検出

仕組み

アプリを実行したら、TensorFlow Lite 固有のコードを確認します。

TensorFlowLite ポッド

このアプリはコンパイル済み TFLite Cocoapod を使用しています。Podfile にはプロジェクトの cocoapod が含まれています。

Podfile

# Uncomment the next line to define a global platform for your project
platform :ios, '12.0'

target 'ObjectDetection' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for ObjectDetection
  pod 'TensorFlowLiteSwift'

end

TF Lite に連結されたコードは、すべて、ModelDataHandler.swift ファイルに含まれます。このクラスは、すべてのデータ前処理を行い、Interpreter を呼び出して指定されたフレームでの推論を実行します。次に、取得した推論をフォーマットしてから、成功した推論の上位 N 件の結果を返します。

コードの確認

ModelDataHandler.swift

まず注目すべきブロックは(必要なインポートをしたあと)、プロパティの宣言です。tfLite モデル inputShape パラメータ(batchSizeinputChannelsinputWidthinputHeight)は tflite_metadata.json にあります。このファイルは、tflite モデルのエクスポート時に作成されます。詳しくは、Edge モデルのエクスポートの入門トピックをご覧ください。

tflite_metadata.json の例は次のコードのようになります。

{
    "inferenceType": "QUANTIZED_UINT8",
    "inputShape": [
        1,   // This represents batch size
        512,  // This represents image width
        512,  // This represents image Height
        3  //This represents inputChannels
    ],
    "inputTensor": "normalized_input_image_tensor",
    "maxDetections": 20,  // This represents max number of boxes.
    "outputTensorRepresentation": [
        "bounding_boxes",
        "class_labels",
        "class_confidences",
        "num_of_boxes"
    ],
    "outputTensors": [
        "TFLite_Detection_PostProcess",
        "TFLite_Detection_PostProcess:1",
        "TFLite_Detection_PostProcess:2",
        "TFLite_Detection_PostProcess:3"
    ]
}
...

モデル パラメータ

モデルの tflite_metadata.json ファイルに合わせて、以下の値を置き換えます。

let batchSize = 1 //Number of images to get prediction, the model takes 1 image at a time
let inputChannels = 3 //The pixels of the image input represented in RGB values
let inputWidth = 300 //Width of the image
let inputHeight = 300 //Height of the image
...

init

init メソッドは、Model パスと InterpreterOptionsInterpreter を作成し、モデルの入力にメモリを割り当てます。

init?(modelFileInfo: FileInfo, labelsFileInfo: FileInfo, threadCount: Int = 1) {
    let modelFilename = modelFileInfo.name

    // Construct the path to the model file.
    guard let modelPath = Bundle.main.path(forResource: modelFilename,ofType: modelFileInfo.extension)

    // Specify the options for the `Interpreter`.
   var options = InterpreterOptions()
    options.threadCount = threadCount
    do {
      // Create the `Interpreter`.
      interpreter = try Interpreter(modelPath: modelPath, options: options)
      // Allocate memory for the model's input  `Tensor`s.
      try interpreter.allocateTensors()
    }

    super.init()

    // Load the classes listed in the labels file.
    loadLabels(fileInfo: labelsFileInfo)
  }
…

runModel

runModel メソッドは、以下を行います。

  1. モデルをトレーニングしたアスペクト比に合わせて入力画像をスケーリングします。
  2. 画像バッファから alpha コンポーネントを削除して、RGB データを取得します。
  3. RGB データを入力 Tensor にコピーします。
  4. Interpreter を呼び出して推論を実行します。
  5. インタープリタからの出力を取得します。
  6. 出力をフォーマットします。
func runModel(onFrame pixelBuffer: CVPixelBuffer) -> Result? {

中央の正方形が最も大きくなるように画像を切り抜き、モデルのサイズに縮小します。

let scaledSize = CGSize(width: inputWidth, height: inputHeight)
guard let scaledPixelBuffer = pixelBuffer.resized(to: scaledSize) else
{
    return nil
}
...

do {
  let inputTensor = try interpreter.input(at: 0)

画像バッファから alpha コンポーネントを削除して、RGB データを取得します。

guard let rgbData = rgbDataFromBuffer(
  scaledPixelBuffer,
  byteCount: batchSize * inputWidth * inputHeight * inputChannels,
  isModelQuantized: inputTensor.dataType == .uInt8
) else {
  print("Failed to convert the image buffer to RGB data.")
  return nil
}

RGB データを入力 Tensor にコピーします。

try interpreter.copy(rgbData, toInputAt: 0)

Interpreter を呼び出して推論を実行します。

    let startDate = Date()
    try interpreter.invoke()
    interval = Date().timeIntervalSince(startDate) * 1000
    outputBoundingBox = try interpreter.output(at: 0)
    outputClasses = try interpreter.output(at: 1)
    outputScores = try interpreter.output(at: 2)
    outputCount = try interpreter.output(at: 3)
  }

結果をフォーマットします。

    let resultArray = formatResults(
      boundingBox: [Float](unsafeData: outputBoundingBox.data) ?? [],
      outputClasses: [Float](unsafeData: outputClasses.data) ?? [],
      outputScores: [Float](unsafeData: outputScores.data) ?? [],
      outputCount: Int(([Float](unsafeData: outputCount.data) ?? [0])[0]),
      width: CGFloat(imageWidth),
      height: CGFloat(imageHeight)
    )
...
}

すべての結果を [confidence score] < [threshold] でフィルタし、上位 N 件の結果を降順に並べ替えて返します。

  func formatResults(boundingBox: [Float], outputClasses: [Float],
  outputScores: [Float], outputCount: Int, width: CGFloat, height: CGFloat)
  -> [Inference]{
    var resultsArray: [Inference] = []
    for i in 0...outputCount - 1 {

      let score = outputScores[i]

結果を [confidence] < [threshold] でフィルタします。

      guard score >= threshold else {
        continue
      }

ラベルリストから検出されたクラスの出力クラス名を取得します。

      let outputClassIndex = Int(outputClasses[i])
      let outputClass = labels[outputClassIndex + 1]

      var rect: CGRect = CGRect.zero

検出された境界ボックスを CGRect に変換します。

      rect.origin.y = CGFloat(boundingBox[4*i])
      rect.origin.x = CGFloat(boundingBox[4*i+1])
      rect.size.height = CGFloat(boundingBox[4*i+2]) - rect.origin.y
      rect.size.width = CGFloat(boundingBox[4*i+3]) - rect.origin.x

検出されたコーナーはモデルのサイズに使用します。実際の画像サイズに対して rect をスケールします。

let newRect = rect.applying(CGAffineTransform(scaleX: width, y: height))

クラスに割り当てられた色を取得します。

let colorToAssign = colorForClass(withIndex: outputClassIndex + 1)
      let inference = Inference(confidence: score,
                                className: outputClass,
                                rect: newRect,
                                displayColor: colorToAssign)
      resultsArray.append(inference)
    }

    // Sort results in descending order of confidence.
    resultsArray.sort { (first, second) -> Bool in
      return first.confidence  > second.confidence
    }

    return resultsArray
  }

CameraFeedManager

CameraFeedManager.swift はカメラ関連のすべての機能を管理します。

  1. AVCaptureSession を初期化して構成します。

    private func configureSession() {
    session.beginConfiguration()
  2. 次に、AVCaptureDeviceInput の追加を試みて、セッション向けのデバイス入力として builtInWideAngleCamera を追加します。

    addVideoDeviceInput()

    続いて AVCaptureVideoDataOutput の追加を試みます。

    addVideoDataOutput()

       session.commitConfiguration()
        self.cameraConfiguration = .success
      }
  3. セッションを開始します。

  4. セッションを停止します。

  5. AVCaptureSession 通知を追加および削除します。

エラー処理

  • NSNotification.Name.AVCaptureSessionRuntimeError: AVCaptureSession インスタンスの実行中に予期しないエラーが発生した場合に送信されます。userInfo 辞書には、鍵 NSErrorAVCaptureSessionErrorKey オブジェクトが含まれます。
  • NSNotification.Name.AVCaptureSessionWasInterrupted: 割り込み(例: 通話、アラームなど)が発生したときに送信されます。必要に応じて、AVCaptureSession インスタンスは割り込みへの自動的なレスポンスの実行を停止します。userInfo には、割り込みの理由を表す AVCaptureSessionInterruptionReasonKey が含まれます。
  • NSNotification.Name.AVCaptureSessionInterruptionEnded: AVCaptureSession割り込みを中止したときに送信されます。セッション インスタンスは、通話などの割り込みが終了すると再開できます。

InferenceViewController.swift クラスは、以下の画面を担当しています。ここではハイライト表示された部分を中心に取り上げます。

  • Resolution: 現在のフレーム(動画セッションの画像)の解像度を表示します。
  • Crop: 現在のフレームの切り抜きサイズを表示します。
  • InferenceTime: モデルがオブジェクトを検出するまでの時間を表示します。
  • Threads: 実行中のスレッドの数を表示します。ステッパーの + または - サインをタップすると、この数を増減できます。TensorFlow Lite インタープリタで使用されている現在のスレッド数。

スレッド イメージを調整する

ViewController.swift クラスは、カメラ関連の機能と ModelDataHandler を管理する CameraFeedManager のインスタンスを保持します。ModelDataHandlerModel(トレーニングされたモデル)を担当し、動画セッションの画像フレームの出力を取得します。

private lazy var cameraFeedManager = CameraFeedManager(previewView: previewView)

private var modelDataHandler: ModelDataHandler? =
    ModelDataHandler(modelFileInfo: MobileNetSSD.modelInfo, labelsFileInfo: MobileNetSSD.labelsInfo)

次のように呼び出すと、カメラ セッションが開始されます。

cameraFeedManager.checkCameraConfigurationAndStartSession()

スレッド カウントを変更すると、このクラスは didChangeThreadCount 関数の新しいスレッド数でモデルを再び初期化します。

CameraFeedManager クラスは CVPixelBuffer として ImageFrameViewController に送信します。これは予測のためにモデルに送信されます。

このメソッドは、TensorFlow を通じてライブカメラ pixelBuffer を実行して、結果を取得します。

@objc
func runModel(onPixelBuffer pixelBuffer: CVPixelBuffer) {

TensorFlow を通じてライブカメラ pixelBuffer を実行して、結果を取得します。

result = self.modelDataHandler?.runModel(onFrame: pixelBuffer)
...
let displayResult = result

let width = CVPixelBufferGetWidth(pixelBuffer)
    let height = CVPixelBufferGetHeight(pixelBuffer)

    DispatchQueue.main.async {

InferenceViewController に結果を引き渡して、表示します。

self.inferenceViewController?.resolution = CGSize(width: width, height: height)

self.inferenceViewController?.inferenceTime = inferenceTime

境界ボックスを描画し、クラス名と信頼スコアを表示します。

self.drawAfterPerformingCalculations(onInferences: displayResult.inferences, withImageSize: CGSize(width: CGFloat(width), height: CGFloat(height)))
    }
  }

次のステップ

Edge モデルを使用した iOS オブジェクトの検出とアノテーションのアプリのチュートリアルは、これで完了です。オブジェクト検出アプリに変更を加えてサンプル アノテーションを取得する前に、トレーニングした Edge Tensorflow Lite モデルを使用して、オブジェクト検出アプリをテストしました。次に、TensorFlow Lite 固有のコードを調べて基本的な機能を理解しました。

次のリソースは、TensorFlow モデルと AutoML Vision Edge の使い方を説明しています。