Détecter des objets dans des images avec un modèle entraîné par AutoML sur les plates-formes Apple

Une fois que vous avez entraîné votre propre modèle à l'aide d'AutoML Vision Edge, vous pouvez l'utiliser dans votre application pour détecter des objets dans des images.

Il existe deux façons d'intégrer des modèles entraînés à partir d'AutoML Vision Edge. Vous pouvez empaqueter le modèle en copiant ses fichiers dans votre projet Xcode ou en le téléchargeant de manière dynamique depuis Firebase.

Options de regroupement de modèles
Groupées dans votre application
  • Le modèle fait partie du lot
  • Le modèle est disponible immédiatement, même lorsque l'appareil Apple est hors connexion.
  • Pas besoin d'un projet Firebase
Hébergement avec Firebase
  • Hébergez le modèle en l'important dans Firebase Machine Learning.
  • Réduit la taille de l'app bundle
  • Le modèle est téléchargé à la demande
  • Déployer les mises à jour du modèle sans publier à nouveau votre application
  • Tests A/B faciles avec Firebase Remote Config
  • Nécessite un projet Firebase

Avant de commencer

  1. Si vous souhaitez télécharger un modèle, veillez à ajouter Firebase à votre projet Apple, si ce n'est pas déjà fait. Cette opération n'est pas requise lorsque vous regroupez le modèle.

  2. Incluez les bibliothèques TensorFlow et Firebase dans votre Podfile:

    Pour regrouper un modèle avec votre application:

    Swift

    pod 'TensorFlowLiteSwift'
    

    Objective-C

    pod 'TensorFlowLiteObjC'
    

    Pour télécharger dynamiquement un modèle depuis Firebase, ajoutez la dépendance Firebase/MLModelInterpreter:

    Swift

    pod 'TensorFlowLiteSwift'
    pod 'Firebase/MLModelInterpreter'
    

    Objective-C

    pod 'TensorFlowLiteObjC'
    pod 'Firebase/MLModelInterpreter'
    
  3. Après avoir installé ou mis à jour les pods de votre projet, ouvrez votre projet Xcode à l'aide de son .xcworkspace.

1. Charger le modèle

Configurer une source de modèle locale

Pour associer le modèle à votre application, copiez le modèle et le fichier d'étiquettes dans votre projet Xcode, en prenant soin de sélectionner Créer des références de dossier lorsque vous effectuez cette opération. Le fichier de modèle et les étiquettes seront inclus dans l'app bundle.

Examinez également le fichier tflite_metadata.json créé avec le modèle. Vous avez besoin de deux valeurs:

  • Dimensions d'entrée du modèle. Par défaut, il s'agit de 320 x 320.
  • Nombre maximal de détections du modèle. La valeur par défaut est 40.

Configurer une source de modèle hébergée par Firebase

Pour utiliser le modèle hébergé à distance, créez un objet CustomRemoteModel, en spécifiant le nom que vous avez attribué au modèle lorsque vous l'avez publié:

Swift

let remoteModel = CustomRemoteModel(
    name: "your_remote_model"  // The name you assigned in the Google Cloud console.
)

Objective-C

FIRCustomRemoteModel *remoteModel = [[FIRCustomRemoteModel alloc]
                                     initWithName:@"your_remote_model"];

Lancez ensuite la tâche de téléchargement du modèle, en spécifiant les conditions dans lesquelles vous souhaitez autoriser le téléchargement. Si le modèle ne se trouve pas sur l'appareil ou si une version plus récente du modèle est disponible, la tâche télécharge le modèle de manière asynchrone depuis Firebase:

Swift

let downloadProgress = ModelManager.modelManager().download(
    remoteModel,
    conditions: ModelDownloadConditions(
        allowsCellularAccess: true,
        allowsBackgroundDownloading: true
    )
)

Objective-C

FIRModelDownloadConditions *conditions =
        [[FIRModelDownloadConditions alloc] initWithAllowsCellularAccess:YES
                                             allowsBackgroundDownloading:YES];
NSProgress *progress = [[FIRModelManager modelManager] downloadModel:remoteModel
                                                          conditions:conditions];

De nombreuses applications démarrent la tâche de téléchargement dans leur code d'initialisation, mais vous pouvez le faire à tout moment avant d'avoir besoin d'utiliser le modèle.

Créer un détecteur d'objets à partir de votre modèle

Après avoir configuré vos sources de modèle, créez un objet Interpreter TensorFlow Lite à partir de l'une d'elles.

Si vous ne disposez que d'un modèle groupé localement, créez simplement un interpréteur à partir du fichier de modèle:

Swift

guard let modelPath = Bundle.main.path(
    forResource: "model",
    ofType: "tflite"
) else {
  print("Failed to load the model file.")
  return true
}
let interpreter = try Interpreter(modelPath: modelPath)
try interpreter.allocateTensors()

Objective-C

NSString *modelPath = [[NSBundle mainBundle] pathForResource:@"model"
                                                      ofType:@"tflite"];

NSError *error;
TFLInterpreter *interpreter = [[TFLInterpreter alloc] initWithModelPath:modelPath
                                                                  error:&error];
if (error != NULL) { return; }

[interpreter allocateTensorsWithError:&error];
if (error != NULL) { return; }

Si vous disposez d'un modèle hébergé à distance, vous devez vérifier qu'il a été téléchargé avant de l'exécuter. Vous pouvez vérifier l'état de la tâche de téléchargement du modèle à l'aide de la méthode isModelDownloaded(remoteModel:) du gestionnaire de modèles.

Bien que vous ne deviez le confirmer qu'avant d'exécuter l'interprète, si vous disposez à la fois d'un modèle hébergé à distance et d'un modèle groupé localement, il peut être judicieux d'effectuer cette vérification lors de l'instanciation de Interpreter: créez un interprète à partir du modèle distant s'il a été téléchargé, et à partir du modèle local dans le cas contraire.

Swift

var modelPath: String?
if ModelManager.modelManager().isModelDownloaded(remoteModel) {
    ModelManager.modelManager().getLatestModelFilePath(remoteModel) { path, error in
        guard error == nil else { return }
        guard let path = path else { return }
        modelPath = path
    }
} else {
    modelPath = Bundle.main.path(
        forResource: "model",
        ofType: "tflite"
    )
}

guard modelPath != nil else { return }
let interpreter = try Interpreter(modelPath: modelPath)
try interpreter.allocateTensors()

Objective-C

__block NSString *modelPath;
if ([[FIRModelManager modelManager] isModelDownloaded:remoteModel]) {
    [[FIRModelManager modelManager] getLatestModelFilePath:remoteModel
                                                completion:^(NSString * _Nullable filePath,
                                                             NSError * _Nullable error) {
        if (error != NULL) { return; }
        if (filePath == NULL) { return; }
        modelPath = filePath;
    }];
} else {
    modelPath = [[NSBundle mainBundle] pathForResource:@"model"
                                                ofType:@"tflite"];
}

NSError *error;
TFLInterpreter *interpreter = [[TFLInterpreter alloc] initWithModelPath:modelPath
                                                                  error:&error];
if (error != NULL) { return; }

[interpreter allocateTensorsWithError:&error];
if (error != NULL) { return; }

Si vous ne disposez que d'un modèle hébergé à distance, vous devez désactiver les fonctionnalités liées au modèle (par exemple, griser ou masquer une partie de votre UI) jusqu'à ce que vous confirmiez que le modèle a été téléchargé.

Vous pouvez obtenir l'état du téléchargement du modèle en associant des observateurs au centre de notifications par défaut. Veillez à utiliser une référence faible à self dans le bloc d'observateur, car les téléchargements peuvent prendre un certain temps et l'objet d'origine peut être libéré au moment où le téléchargement se termine. Exemple :

Swift

NotificationCenter.default.addObserver(
    forName: .firebaseMLModelDownloadDidSucceed,
    object: nil,
    queue: nil
) { [weak self] notification in
    guard let strongSelf = self,
        let userInfo = notification.userInfo,
        let model = userInfo[ModelDownloadUserInfoKey.remoteModel.rawValue]
            as? RemoteModel,
        model.name == "your_remote_model"
        else { return }
    // The model was downloaded and is available on the device
}

NotificationCenter.default.addObserver(
    forName: .firebaseMLModelDownloadDidFail,
    object: nil,
    queue: nil
) { [weak self] notification in
    guard let strongSelf = self,
        let userInfo = notification.userInfo,
        let model = userInfo[ModelDownloadUserInfoKey.remoteModel.rawValue]
            as? RemoteModel
        else { return }
    let error = userInfo[ModelDownloadUserInfoKey.error.rawValue]
    // ...
}

Objective-C

__weak typeof(self) weakSelf = self;

[NSNotificationCenter.defaultCenter
    addObserverForName:FIRModelDownloadDidSucceedNotification
                object:nil
                 queue:nil
            usingBlock:^(NSNotification *_Nonnull note) {
              if (weakSelf == nil | note.userInfo == nil) {
                return;
              }
              __strong typeof(self) strongSelf = weakSelf;

              FIRRemoteModel *model = note.userInfo[FIRModelDownloadUserInfoKeyRemoteModel];
              if ([model.name isEqualToString:@"your_remote_model"]) {
                // The model was downloaded and is available on the device
              }
            }];

[NSNotificationCenter.defaultCenter
    addObserverForName:FIRModelDownloadDidFailNotification
                object:nil
                 queue:nil
            usingBlock:^(NSNotification *_Nonnull note) {
              if (weakSelf == nil | note.userInfo == nil) {
                return;
              }
              __strong typeof(self) strongSelf = weakSelf;

              NSError *error = note.userInfo[FIRModelDownloadUserInfoKeyError];
            }];

2. Préparer l'image d'entrée

Vous devez ensuite préparer vos images pour l'interpréteur TensorFlow Lite.

  1. Recadrez et redimensionnez l'image en fonction des dimensions d'entrée du modèle, comme indiqué dans le fichier tflite_metadata.json (320 x 320 pixels par défaut). Vous pouvez le faire avec Core Image ou une bibliothèque tierce

  2. Copiez les données de l'image dans un Data (objet NSData):

    Swift

    guard let image: CGImage = // Your input image
    guard let context = CGContext(
      data: nil,
      width: image.width, height: image.height,
      bitsPerComponent: 8, bytesPerRow: image.width * 4,
      space: CGColorSpaceCreateDeviceRGB(),
      bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue
    ) else {
      return nil
    }
    
    context.draw(image, in: CGRect(x: 0, y: 0, width: image.width, height: image.height))
    guard let imageData = context.data else { return nil }
    
    var inputData = Data()
    for row in 0 ..< 320 {    // Model takes 320x320 pixel images as input
      for col in 0 ..< 320 {
        let offset = 4 * (col * context.width + row)
        // (Ignore offset 0, the unused alpha channel)
        var red = imageData.load(fromByteOffset: offset+1, as: UInt8.self)
        var green = imageData.load(fromByteOffset: offset+2, as: UInt8.self)
        var blue = imageData.load(fromByteOffset: offset+3, as: UInt8.self)
    
        inputData.append(&red, count: 1)
        inputData.append(&green, count: 1)
        inputData.append(&blue, count: 1)
      }
    }
    

    Objective-C

    CGImageRef image = // Your input image
    long imageWidth = CGImageGetWidth(image);
    long imageHeight = CGImageGetHeight(image);
    CGContextRef context = CGBitmapContextCreate(nil,
                                                 imageWidth, imageHeight,
                                                 8,
                                                 imageWidth * 4,
                                                 CGColorSpaceCreateDeviceRGB(),
                                                 kCGImageAlphaNoneSkipFirst);
    CGContextDrawImage(context, CGRectMake(0, 0, imageWidth, imageHeight), image);
    UInt8 *imageData = CGBitmapContextGetData(context);
    
    NSMutableData *inputData = [[NSMutableData alloc] initWithCapacity:0];
    
    for (int row = 0; row < 300; row++) {
      for (int col = 0; col < 300; col++) {
        long offset = 4 * (row * imageWidth + col);
        // (Ignore offset 0, the unused alpha channel)
        UInt8 red = imageData[offset+1];
        UInt8 green = imageData[offset+2];
        UInt8 blue = imageData[offset+3];
    
        [inputData appendBytes:&red length:1];
        [inputData appendBytes:&green length:1];
        [inputData appendBytes:&blue length:1];
      }
    }
    

3. Exécuter le détecteur d'objets

Transmettez ensuite l'entrée préparée à l'interprète:

Swift

try interpreter.copy(inputData, toInputAt: 0)
try interpreter.invoke()

Objective-C

TFLTensor *input = [interpreter inputTensorAtIndex:0 error:&error];
if (error != nil) { return; }

[input copyData:inputData error:&error];
if (error != nil) { return; }

[interpreter invokeWithError:&error];
if (error != nil) { return; }

4. Obtenir des informations sur les objets détectés

Si la détection d'objets aboutit, le modèle produit en sortie trois tableaux de 40 éléments (ou tout autre élément spécifié dans le fichier tflite_metadata.json) chacun. Chaque élément correspond à un objet potentiel. Le premier tableau est un tableau de cadres de délimitation, le deuxième un tableau d'étiquettes et le troisième un tableau de valeurs de confiance. Pour obtenir les sorties du modèle:

Swift

var output = try interpreter.output(at: 0)
let boundingBoxes =
    UnsafeMutableBufferPointer<Float32>.allocate(capacity: 4 * 40)
output.data.copyBytes(to: boundingBoxes)

output = try interpreter.output(at: 1)
let labels =
    UnsafeMutableBufferPointer<Float32>.allocate(capacity: 40)
output.data.copyBytes(to: labels)

output = try interpreter.output(at: 2)
let probabilities =
    UnsafeMutableBufferPointer<Float32>.allocate(capacity: 40)
output.data.copyBytes(to: probabilities)

Objective-C

TFLTensor *output = [interpreter outputTensorAtIndex:0 error:&error];
if (error != nil) { return; }
NSData *boundingBoxes = [output dataWithError:&error];
if (error != nil) { return; }

output = [interpreter outputTensorAtIndex:1 error:&error];
if (error != nil) { return; }
NSData *labels = [output dataWithError:&error];
if (error != nil) { return; }

output = [interpreter outputTensorAtIndex:2 error:&error];
if (error != nil) { return; }
NSData *probabilities = [output dataWithError:&error];
if (error != nil) { return; }

Vous pouvez ensuite combiner les sorties des libellés avec votre dictionnaire de libellés:

Swift

guard let labelPath = Bundle.main.path(
    forResource: "dict",
    ofType: "txt"
) else { return true }
let fileContents = try? String(contentsOfFile: labelPath)
guard let labelText = fileContents?.components(separatedBy: "\n") else { return true }

for i in 0 ..< 40 {
    let top = boundingBoxes[0 * i]
    let left = boundingBoxes[1 * i]
    let bottom = boundingBoxes[2 * i]
    let right = boundingBoxes[3 * i]

    let labelIdx = Int(labels[i])
    let label = labelText[labelIdx]
    let confidence = probabilities[i]

    if confidence > 0.66 {
        print("Object found: \(label) (confidence: \(confidence))")
        print("  Top-left: (\(left),\(top))")
        print("  Bottom-right: (\(right),\(bottom))")
    }
}

Objective-C

NSString *labelPath = [NSBundle.mainBundle pathForResource:@"dict"
                                                    ofType:@"txt"];
NSString *fileContents = [NSString stringWithContentsOfFile:labelPath
                                                   encoding:NSUTF8StringEncoding
                                                      error:&error];
if (error != nil || fileContents == NULL) { return; }
NSArray<NSString*> *labelText = [fileContents componentsSeparatedByString:@"\n"];

for (int i = 0; i < 40; i++) {
    Float32 top, right, bottom, left;
    Float32 labelIdx;
    Float32 confidence;

    [boundingBoxes getBytes:&top range:NSMakeRange(16 * i + 0, 4)];
    [boundingBoxes getBytes:&left range:NSMakeRange(16 * i + 4, 4)];
    [boundingBoxes getBytes:&bottom range:NSMakeRange(16 * i + 8, 4)];
    [boundingBoxes getBytes:&right range:NSMakeRange(16 * i + 12, 4)];

    [labels getBytes:&labelIdx range:NSMakeRange(4 * i, 4)];
    [probabilities getBytes:&confidence range:NSMakeRange(4 * i, 4)];

    if (confidence > 0.5f) {
        NSString *label = labelText[(int)labelIdx];
        NSLog(@"Object detected: %@", label);
        NSLog(@"  Confidence: %f", confidence);
        NSLog(@"  Top-left: (%f,%f)", left, top);
        NSLog(@"  Bottom-right: (%f,%f)", right, bottom);
    }
}

Conseils pour améliorer les performances en temps réel

Si vous souhaitez ajouter des libellés aux images dans une application en temps réel, suivez ces consignes pour obtenir les meilleurs fréquences d'images:

  • Limitez les appels au détecteur. Si une nouvelle image vidéo devient disponible pendant l'exécution du détecteur, supprimez-la.
  • Si vous utilisez la sortie du détecteur pour superposer des éléments graphiques à l'image d'entrée, obtenez d'abord le résultat, puis affichez l'image et la superposition en une seule étape. Vous ne procédez ainsi qu'une seule fois pour chaque frame d'entrée. Pour en savoir plus, consultez les classes previewOverlayView et FIRDetectionOverlayView dans l'application exemple.