iOS でカスタムモデルを使用して画像にラベルを付ける

ML Kit を使用すると、画像内のエンティティを認識してラベルを付けることができます。 この API は、幅広いカスタム画像分類モデルをサポートしています。モデルの互換性要件、事前トレーニング済みモデルの入手先、独自のモデルをトレーニングする方法については、ML Kit でのカスタムモデルをご覧ください。

カスタムモデルを統合する方法は 2 つあります。モデルをアプリのアセット フォルダに配置してバンドルする方法と、Cloud Storage から動的にダウンロードする方法があります。次の表に、2 つのオプションを比較します。

バンドルモデル ホストされているモデル
モデルはアプリの APK の一部であるため、サイズが大きくなります。 モデルは APK の一部ではありません。Cloud Storage にアップロードすることでホストされます。Cloud Storage for Firebase を使用することをおすすめします。
このモデルは、Android デバイスがオフラインのときでもすぐに利用可能 アプリには、オンデマンドでモデルをダウンロードするコードを含める必要があります。
Firebase プロジェクトは不要 Firebase プロジェクトが必要(Cloud Storage for Firebase を使用する場合)。
モデルを更新するにはアプリを再公開する必要がある アプリを再公開することなくモデルの更新を push できる
組み込みの A/B テストはない Firebase Remote Config を使用した A/B テスト

試してみる

始める前に

  1. Podfile に ML Kit ライブラリを含めます。

    pod 'GoogleMLKit/ImageLabelingCustom', '8.0.0'
    
  2. プロジェクトの Pod をインストールまたは更新した後に、.xcworkspace を使用して Xcode プロジェクトを開きます。ML Kit は Xcode バージョン 13.2.1 以降でサポートされています。

  3. Cloud Storage for Firebase を使用してモデルをダウンロードする場合は、 まだ行っていない場合は Firebase を iOS プロジェクトに追加します。 これは、モデルをバンドルする場合には必要ありません。

1. モデルを読み込む

ローカル モデルソースを構成する

モデルをアプリにバンドルするには:

  1. モデルファイル(拡張子は通常 .tflite または .lite)を Xcode プロジェクトにコピーします。その際、Copy bundle resources を選択してください。モデルファイルは App Bundle に含められ、ML Kit から使用できます。

  2. LocalModel オブジェクトを作成し、モデルファイルのパスを指定します。

    Swift

    let localModel = LocalModel(path: localModelFilePath)

    Objective-C

    MLKLocalModel *localModel =
        [[MLKLocalModel alloc] initWithPath:localModelFilePath];

リモートでホストされるモデルソースを構成する

リモートでホストされるモデルを使用するには、独自のアプリロジックを使用してモデルファイルをデバイスのローカル ストレージにダウンロードし、ローカルモデルとして読み込む必要があります。モデルをホストするには、Cloud Storage for Firebase を使用することをおすすめします。実装の詳細については、 Firebase ML から Cloud Storage への移行ガイドをご覧ください。

画像ラベラーを構成する

モデルソースを構成したら、そのいずれかから ImageLabeler オブジェクトを作成します。

以下のオプションを使用できます。

オプション
confidenceThreshold

検出されたラベルの最小信頼スコア。設定されていない場合は、モデルのメタデータで指定された分類子のしきい値が使用されます。モデルにメタデータが含まれていない場合、またはメタデータで 分類子のしきい値が指定されていない場合は、デフォルトのしきい値 0.0 が 使用されます。

maxResultCount

返されるラベルの最大数。設定されていない場合は、デフォルト値の 10 が使用されます。

ローカル バンドルモデルのみがある場合は、LocalModel オブジェクトからラベラーを作成するだけで済みます。

Swift

let options = CustomImageLabelerOptions(localModel: localModel)
options.confidenceThreshold = NSNumber(value: 0.0)
let imageLabeler = ImageLabeler.imageLabeler(options: options)

Objective-C

MLKCustomImageLabelerOptions *options =
    [[MLKCustomImageLabelerOptions alloc] initWithLocalModel:localModel];
options.confidenceThreshold = @(0.0);
MLKImageLabeler *imageLabeler =
    [MLKImageLabeler imageLabelerWithOptions:options];

リモートでホストされるモデルがある場合は、そのモデルを実行する前にダウンロード済みであることを確認する必要があります。

ダウンロードのステータスはラベラーを実行する前に確認するだけで済みますが、リモートでホストされるモデルとローカル バンドルモデルの両方がある場合は、ImageLabeler をインスタンス化する、つまりラベラーを作成する(リモートモデルをダウンロード済みの場合はリモートモデルから、ダウンロードされていない場合はローカルモデルから作成する)ときに確認しても問題ありません。

Swift

// Path where your download logic saves the model
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let localModelURL = documentDirectory.appendingPathComponent("my_remote_model.tflite")

let model: LocalModel
if FileManager.default.fileExists(atPath: localModelURL.path) {
  // Use the downloaded model
  model = LocalModel(path: localModelURL.path)
} else {
  // Fall back to bundled model
  guard let bundledModelPath = Bundle.main.path(forResource: "model", ofType: "tflite") else { return }
  model = LocalModel(path: bundledModelPath)
}

let options = CustomImageLabelerOptions(localModel: model)
let imageLabeler = ImageLabeler.imageLabeler(options: options)

Objective-C

NSString *documentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString *localModelPath = [documentsDirectory stringByAppendingPathComponent:@"my_remote_model.tflite"];

MLKLocalModel *model;
if ([NSFileManager.defaultManager fileExistsAtPath:localModelPath]) {
  // Use the downloaded model
  model = [[MLKLocalModel alloc] initWithPath:localModelPath];
} else {
  // Fall back to bundled model
  NSString *bundledModelPath = [NSBundle.mainBundle pathForResource:@"model" ofType:@"tflite"];
  model = [[MLKLocalModel alloc] initWithPath:bundledModelPath];
}

MLKCustomImageLabelerOptions *options = [[MLKCustomImageLabelerOptions alloc] initWithLocalModel:model];
MLKImageLabeler *imageLabeler = [MLKImageLabeler imageLabelerWithOptions:options];

リモートでホストされるモデルのみがある場合は、モデルがダウンロード済みであることを確認するまで、モデルに関連する機能(UI の一部をグレー表示または非表示にするなど)を無効にする必要があります。

Swift

let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let localModelURL = documentDirectory.appendingPathComponent("my_remote_model.tflite")
if FileManager.default.fileExists(atPath: localModelURL.path) {
  // Model is already cached, initialize immediately
  self.initializeLabeler(with: localModelURL)
} else {
  // Model is not yet available, show loading UI and start download
  self.showLoadingUI()
  let storage = Storage.storage()
  let modelRef = storage.reference(forURL: "gs://YOUR_BUCKET/path/to/model.tflite")
  modelRef.write(toFile: localModelURL) { url, error in
    self.hideLoadingUI()
    if let error = error {
      // Handle download error
      self.showErrorUI()
    } else if let modelURL = url {
      // Download success, initialize labeler
      self.initializeLabeler(with: modelURL)
    }
  }
}

func initializeLabeler(with modelURL: URL) {
  let localModel = LocalModel(path: modelURL.path)
  let options = CustomImageLabelerOptions(localModel: localModel)
  self.imageLabeler = ImageLabeler.imageLabeler(options: options)
  // Enable ML-related UI features here
  self.enableMLFeatures()
}

Objective-C

NSString *documentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString *localModelPath = [documentsDirectory stringByAppendingPathComponent:@"my_remote_model.tflite"];
NSURL *localModelURL = [NSURL fileURLWithPath:localModelPath];

if ([NSFileManager.defaultManager fileExistsAtPath:localModelPath]) {
  // Model is already cached, initialize immediately
  [self initializeLabelerWithURL:localModelURL];
} else {
  // Model is not yet available, show loading UI and start download
  [self showLoadingUI];

  FIRStorage *storage = [FIRStorage storage];
  FIRStorageReference *modelRef = [storage referenceForURL:@"gs://YOUR_BUCKET/path/to/model.tflite"];

  [modelRef writeToFile:localModelURL
             completion:^(NSURL * _Nullable URL, NSError * _Nullable error) {
               [self hideLoadingUI];
               if (error != nil) {
                 // Handle download error
                 [self showErrorUI];
               } else {
                 // Download success, initialize labeler
                 [self initializeLabelerWithURL:URL];
               }
             }];
}

- (void)initializeLabelerWithURL:(NSURL *)modelURL {
  MLKLocalModel *localModel = [[MLKLocalModel alloc] initWithPath:modelURL.path];
  MLKCustomImageLabelerOptions *options = [[MLKCustomImageLabelerOptions alloc] initWithLocalModel:localModel];
  self.imageLabeler = [MLKImageLabeler imageLabelerWithOptions:options];

  // Enable ML-related UI features here
  [self enableMLFeatures];
}

2. 入力画像を準備する

UIImage または CMSampleBuffer を使用して VisionImage オブジェクトを作成します。

UIImage を使用する場合は、次の手順を行います。

  • VisionImage オブジェクトを UIImage で作成します。正しい .orientation を指定してください。

    Swift

    let image = VisionImage(image: UIImage)
    visionImage.orientation = image.imageOrientation

    Objective-C

    MLKVisionImage *visionImage = [[MLKVisionImage alloc] initWithImage:image];
    visionImage.orientation = image.imageOrientation;

CMSampleBuffer を使用する場合は、次の手順を行います。

  • に含まれる画像データの向きを指定します。CMSampleBuffer

    画像の向きは次のように取得します。

    Swift

    func imageOrientation(
      deviceOrientation: UIDeviceOrientation,
      cameraPosition: AVCaptureDevice.Position
    ) -> UIImage.Orientation {
      switch deviceOrientation {
      case .portrait:
        return cameraPosition == .front ? .leftMirrored : .right
      case .landscapeLeft:
        return cameraPosition == .front ? .downMirrored : .up
      case .portraitUpsideDown:
        return cameraPosition == .front ? .rightMirrored : .left
      case .landscapeRight:
        return cameraPosition == .front ? .upMirrored : .down
      case .faceDown, .faceUp, .unknown:
        return .up
      }
    }
          

    Objective-C

    - (UIImageOrientation)
      imageOrientationFromDeviceOrientation:(UIDeviceOrientation)deviceOrientation
                             cameraPosition:(AVCaptureDevicePosition)cameraPosition {
      switch (deviceOrientation) {
        case UIDeviceOrientationPortrait:
          return cameraPosition == AVCaptureDevicePositionFront ? UIImageOrientationLeftMirrored
                                                                : UIImageOrientationRight;
    
        case UIDeviceOrientationLandscapeLeft:
          return cameraPosition == AVCaptureDevicePositionFront ? UIImageOrientationDownMirrored
                                                                : UIImageOrientationUp;
        case UIDeviceOrientationPortraitUpsideDown:
          return cameraPosition == AVCaptureDevicePositionFront ? UIImageOrientationRightMirrored
                                                                : UIImageOrientationLeft;
        case UIDeviceOrientationLandscapeRight:
          return cameraPosition == AVCaptureDevicePositionFront ? UIImageOrientationUpMirrored
                                                                : UIImageOrientationDown;
        case UIDeviceOrientationUnknown:
        case UIDeviceOrientationFaceUp:
        case UIDeviceOrientationFaceDown:
          return UIImageOrientationUp;
      }
    }
          
  • VisionImage オブジェクトと向きを使用して CMSampleBuffer オブジェクトを作成します:

    Swift

    let image = VisionImage(buffer: sampleBuffer)
    image.orientation = imageOrientation(
      deviceOrientation: UIDevice.current.orientation,
      cameraPosition: cameraPosition)

    Objective-C

     MLKVisionImage *image = [[MLKVisionImage alloc] initWithBuffer:sampleBuffer];
     image.orientation =
       [self imageOrientationFromDeviceOrientation:UIDevice.currentDevice.orientation
                                    cameraPosition:cameraPosition];

3. 画像ラベラーを実行する

画像内のオブジェクトにラベルを付けるには、image オブジェクトを ImageLabelerprocess() メソッドに渡します。

非同期:

Swift

imageLabeler.process(image) { labels, error in
    guard error == nil, let labels = labels, !labels.isEmpty else {
        // Handle the error.
        return
    }
    // Show results.
}

Objective-C

[imageLabeler
    processImage:image
      completion:^(NSArray *_Nullable labels,
                   NSError *_Nullable error) {
        if (label.count == 0) {
            // Handle the error.
            return;
        }
        // Show results.
     }];

同期:

Swift

var labels: [ImageLabel]
do {
    labels = try imageLabeler.results(in: image)
} catch let error {
    // Handle the error.
    return
}
// Show results.

Objective-C

NSError *error;
NSArray *labels =
    [imageLabeler resultsInImage:image error:&error];
// Show results or handle the error.

4. ラベル付きエンティティに関する情報を取得する

画像のラベル付けオペレーションが成功すると、 ImageLabel の配列が返されます。各 ImageLabel は画像内でラベル付けされたものを表します。各ラベルのテキストの説明(LiteRT モデルファイルのメタデータで使用できる場合)、信頼スコア、インデックスを取得できます。次に例を示します。

Swift

for label in labels {
  let labelText = label.text
  let confidence = label.confidence
  let index = label.index
}

Objective-C

for (MLKImageLabel *label in labels) {
  NSString *labelText = label.text;
  float confidence = label.confidence;
  NSInteger index = label.index;
}

リアルタイムのパフォーマンスを改善するためのヒント

リアルタイムのアプリケーションで画像にラベルを付ける場合は、次の ガイドラインに従って適切なフレームレートを得てください。

  • 動画フレームを処理するには、検出機能の results(in:) 同期 API を使用します。このメソッドを AVCaptureVideoDataOutputSampleBufferDelegate captureOutput(_, didOutput:from:) 関数から呼び出して、指定された動画 フレームから結果を同期的に取得します。検出器の呼び出しをスロットルするために、 AVCaptureVideoDataOutput alwaysDiscardsLateVideoFramestrue のままにします。検出器の実行中に新しい 動画フレームが使用可能になった場合は、そのフレームはドロップされます。
  • 検出器の出力を使用して入力画像の上にグラフィックスをオーバーレイする場合は、まず ML Kit から検出結果を取得し、画像とオーバーレイを 1 つのステップでレンダリングします。これにより、ディスプレイ サーフェスへのレンダリングは 処理済みの入力フレームごとに 1 回で済みます。例については、ML Kit クイックスタート サンプルの updatePreviewOverlayViewWithLastFrame をご覧ください。