在 iOS 上使用自訂分類模型來偵測、追蹤及分類物件

您可以使用 ML Kit 偵測及追蹤連續影片影格中的物件。

將圖片傳遞至 ML Kit 時,系統會偵測圖片中最多五個物件,以及每個物件在圖片中的位置。偵測影片串流中的物件時,每個物件都有專屬 ID,可用於追蹤影格中的物件。

您可以使用自訂圖片分類模型,分類偵測到的物件。如需模型相容性規定、預先訓練模型取得位置,以及如何訓練自有模型的相關指引,請參閱「使用 ML Kit 的自訂模型」。

整合自訂模型的方法有兩種。您可以將模型放入應用程式的資產資料夾中,或從 Cloud Storage 動態下載模型。下表比較這兩個選項。

組合模式 代管模型
模型是應用程式 .ipa 檔案的一部分,因此會增加檔案大小。 模型不屬於應用程式的 .ipa 檔案,而是透過上傳至 Cloud Storage 進行代管。建議使用 Cloud Storage for Firebase
即使 Android 裝置未連上網路,也能立即使用模型 應用程式必須包含程式碼,才能視需要下載模型
不需要 Firebase 專案 需要 Firebase 專案 (如果使用 Cloud Storage for Firebase)。
您必須重新發布應用程式,才能更新模型 無須重新發布應用程式,即可推送模型更新
沒有內建的 A/B 測試 使用 Firebase 遠端設定進行 A/B 測試

立即試用

事前準備

  1. 在 Podfile 中加入 ML Kit 程式庫:

    pod 'GoogleMLKit/ObjectDetectionCustom', '8.0.0'
    
  2. 安裝或更新專案的 Pod 後,請使用 .xcworkspace 開啟 Xcode 專案。Xcode 13.2.1 以上版本支援 ML Kit。

  3. 如要使用 Cloud Storage for Firebase 下載模型,請務必將 Firebase 新增至 iOS 專案 (如果尚未新增)。如果將模型與應用程式套件一併發布,則不需要這麼做。

1. 載入模型

設定本機模型來源

如要將模型與應用程式組合,請按照下列步驟操作:

  1. 將模型檔案 (通常以 .tflite.lite 結尾) 複製到 Xcode 專案,複製時請務必選取 Copy bundle resources。模型檔案會納入應用程式套件,並供 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 指南

2. 設定物件偵測器

設定模型來源後,請使用 CustomObjectDetectorOptions 物件,為您的用途設定物件偵測器。您可以變更下列設定:

物件偵測工具設定
偵測模式 STREAM_MODE (預設) | SINGLE_IMAGE_MODE

STREAM_MODE (預設) 模式下,物件偵測器會以低延遲執行,但可能在前幾次呼叫偵測器時產生不完整的結果 (例如不明的定界框或類別標籤)。此外,在 STREAM_MODE 模式下,偵測器會為物件指派追蹤 ID,您可以使用這些 ID 追蹤影格中的物件。如要追蹤物件,或低延遲很重要 (例如即時處理影片串流時),請使用這個模式。

SINGLE_IMAGE_MODE 中,物件偵測器會在判斷物件的邊界框後傳回結果。如果您也啟用分類功能,偵測器會在邊界框和類別標籤都可用時傳回結果。因此,偵測延遲時間可能會較長。此外,在 SINGLE_IMAGE_MODE 中,系統不會指派追蹤 ID。如果延遲時間不重要,且您不想處理部分結果,請使用這個模式。

偵測及追蹤多個物件 false (預設) | true

是否要偵測及追蹤最多五個物件,或只追蹤最顯眼的物件 (預設)。

分類物件 false (預設) | true

是否使用提供的自訂敏感內容類別模型分類偵測到的物件。如要使用自訂分類模型,請將此值設為 true

分類可信度門檻

偵測到的標籤的最低信賴度分數。如未設定,系統會使用模型中繼資料指定的任何分類器門檻。如果模型不含任何中繼資料,或中繼資料未指定分類器門檻,系統會使用 0.0 的預設門檻。

每個物件的標籤數量上限

偵測器傳回的每個物件標籤數量上限。如未設定,系統會使用預設值 10。

如果只有本機套件組合模型,只要從 LocalModel 物件建立物件偵測器即可:

Swift

let options = CustomObjectDetectorOptions(localModel: localModel)
options.detectorMode = .singleImage
options.shouldEnableClassification = true
options.shouldEnableMultipleObjects = true
options.classificationConfidenceThreshold = NSNumber(value: 0.5)
options.maxPerObjectLabelCount = 3

Objective-C

MLKCustomObjectDetectorOptions *options =
    [[MLKCustomObjectDetectorOptions alloc] initWithLocalModel:localModel];
options.detectorMode = MLKObjectDetectorModeSingleImage;
options.shouldEnableClassification = YES;
options.shouldEnableMultipleObjects = YES;
options.classificationConfidenceThreshold = @(0.5);
options.maxPerObjectLabelCount = 3;

如果您有遠端代管模型,必須先檢查模型是否已下載,才能運作執行。

雖然您只需要在執行物件偵測器前確認這項設定,但如果您同時有遠端代管模型和本機模型,建議在例項化 ObjectDetector 時執行這項檢查:如果已下載遠端模型,請從遠端模型建立偵測器,否則請從本機模型建立。

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 = CustomObjectDetectorOptions(localModel: model)
options.detectorMode = .singleImage
options.shouldEnableClassification = true
options.shouldEnableMultipleObjects = true
options.classificationConfidenceThreshold = NSNumber(value: 0.5)
options.maxPerObjectLabelCount = 3
let objectDetector = ObjectDetector.objectDetector(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];
}

MLKCustomObjectDetectorOptions *options = [[MLKCustomObjectDetectorOptions alloc] initWithLocalModel:model];
options.detectorMode = MLKObjectDetectorModeSingleImage;
options.shouldEnableClassification = YES;
options.shouldEnableMultipleObjects = YES;
options.classificationConfidenceThreshold = @(0.5);
options.maxPerObjectLabelCount = 3;
MLKObjectDetector *objectDetector = [MLKObjectDetector objectDetectorWithOptions: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.initializeDetector(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 detector
      self.initializeDetector(with: modelURL)
    }
  }
}

func initializeDetector(with modelURL: URL) {
  let localModel = LocalModel(path: modelURL.path)
  let options = CustomObjectDetectorOptions(localModel: localModel)
  options.detectorMode = .singleImage
  options.shouldEnableClassification = true
  options.shouldEnableMultipleObjects = true
  self.objectDetector = ObjectDetector.objectDetector(options: options)
  // Enable ML features in UI
  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 initializeDetectorWithURL: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 detector
                 [self initializeDetectorWithURL:URL];
               }
             }];
}

- (void)initializeDetectorWithURL:(NSURL *)modelURL {
  MLKLocalModel *localModel = [[MLKLocalModel alloc] initWithPath:modelURL.path];
  MLKCustomObjectDetectorOptions *options = [[MLKCustomObjectDetectorOptions alloc] initWithLocalModel:localModel];
  options.detectorMode = MLKObjectDetectorModeSingleImage;
  options.shouldEnableClassification = YES;
  options.shouldEnableMultipleObjects = YES;
  self.objectDetector = [MLKObjectDetector objectDetectorWithOptions:options];

  // Enable ML features in UI
  [self enableMLFeatures];
}

物件偵測和追蹤 API 適用於下列兩項核心用途:

  • 即時偵測及追蹤相機觀景窗中最顯眼的物件。
  • 從靜態圖片偵測多個物件。

如要為這些用途設定 API,請按照下列步驟操作:

Swift

// Live detection and tracking
let options = CustomObjectDetectorOptions(localModel: localModel)
options.shouldEnableClassification = true
options.maxPerObjectLabelCount = 3

// Multiple object detection in static images
let options = CustomObjectDetectorOptions(localModel: localModel)
options.detectorMode = .singleImage
options.shouldEnableMultipleObjects = true
options.shouldEnableClassification = true
options.maxPerObjectLabelCount = 3

Objective-C

// Live detection and tracking
MLKCustomObjectDetectorOptions *options =
    [[MLKCustomObjectDetectorOptions alloc] initWithLocalModel:localModel];
options.shouldEnableClassification = YES;
options.maxPerObjectLabelCount = 3;

// Multiple object detection in static images
MLKCustomObjectDetectorOptions *options =
    [[MLKCustomObjectDetectorOptions alloc] initWithLocalModel:localModel];
options.detectorMode = MLKObjectDetectorModeSingleImage;
options.shouldEnableMultipleObjects = YES;
options.shouldEnableClassification = YES;
options.maxPerObjectLabelCount = 3;

3. 準備輸入圖片

使用 UIImageCMSampleBuffer 建立 VisionImage 物件。

如果你使用 UIImage,請按照下列步驟操作:

  • 使用 UIImage 建立 VisionImage 物件。請務必指定正確的 .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;
      }
    }
          
  • 使用 CMSampleBuffer 物件和方向建立 VisionImage 物件:

    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];

4. 建立並執行物件偵測器

  1. 建立新的物件偵測器:

    Swift

    let objectDetector = ObjectDetector.objectDetector(options: options)

    Objective-C

    MLKObjectDetector *objectDetector = [MLKObjectDetector objectDetectorWithOptions:options];
  2. 接著使用偵測器:

    非同步:

    Swift

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

    Objective-C

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

    同步:

    Swift

    var objects: [Object]
    do {
        objects = try objectDetector.results(in: image)
    } catch let error {
        // Handle the error.
        return
    }
    // Show results.

    Objective-C

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

5. 取得標示物件的相關資訊

如果對影像處理器的呼叫成功,系統會將 Object 清單傳遞至完成處理常式,或傳回該清單,具體取決於您呼叫的是非同步或同步方法。

每個 Object 都包含下列屬性:

frame CGRect:表示物件在圖片中的位置。
trackingID 一個整數,用於識別圖片中的物件,或 SINGLE_IMAGE_MODE 中的 `nil`。
labels
label.text 標籤的文字說明。只有在 LiteRT 模型的 中繼資料包含標籤說明時,才會傳回這項資訊。
label.index 分類器支援的所有標籤中,這個標籤的索引。
label.confidence 物件分類的信心值。

Swift

// objects contains one item if multiple object detection wasn't enabled.
for object in objects {
  let frame = object.frame
  let trackingID = object.trackingID
  let description = object.labels.enumerated().map { (index, label) in
    "Label \(index): \(label.text), \(label.confidence), \(label.index)"
  }.joined(separator: "\n")
}

Objective-C

// The list of detected objects contains one item if multiple object detection
// wasn't enabled.
for (MLKObject *object in objects) {
  CGRect frame = object.frame;
  NSNumber *trackingID = object.trackingID;
  for (MLKObjectLabel *label in object.labels) {
    NSString *labelString =
        [NSString stringWithFormat:@"%@, %f, %lu",
                                   label.text,
                                   label.confidence,
                                   (unsigned long)label.index];
  }
}

確保提供優質的使用者體驗

為提供最佳使用者體驗,請確保應用程式遵循下列規範:

  • 物件偵測是否成功取決於物件的視覺複雜度。如要偵測視覺特徵較少的物件,可能需要讓物件在圖片中占較大比例。您應提供使用者指引,說明如何擷取適合偵測目標物件的輸入內容。
  • 使用分類功能時,如要偵測不屬於支援類別的物件,請針對不明物件實作特殊處理方式。

此外,也請參閱 ML Kit Material Design 展示應用程式,以及機器學習輔助功能適用的 Material Design 模式集合。

提升效能

如要在即時應用程式中使用物件偵測功能,請按照下列指南操作,以達到最佳影格速率:

  • 在即時應用程式中使用串流模式時,請勿使用多個物件偵測功能,因為大多數裝置無法產生足夠的影格速率。

  • 如要處理影片影格,請使用偵測器的 results(in:) 同步 API。從 AVCaptureVideoDataOutputSampleBufferDelegate captureOutput(_, didOutput:from:) 函式呼叫這個方法,即可同步取得指定影片影格的結果。請將 AVCaptureVideoDataOutput alwaysDiscardsLateVideoFrames 設為 true,以節流對偵測器的呼叫。如果偵測器正在執行時有新的影片影格可用,系統會捨棄該影格。
  • 如果使用偵測器的輸出內容,在輸入圖片上疊加圖像,請先從 ML Kit 取得結果,然後在單一步驟中算繪圖片並疊加圖像。這樣一來,您只需要為每個處理過的輸入影格,向顯示表面轉譯一次。如需範例,請參閱 ML Kit 快速入門範例中的 updatePreviewOverlayViewWithLastFrame