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

Après avoir 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 les images.

Il existe deux manières d'intégrer des modèles formés à partir d'AutoML Vision Edge. Vous pouvez regrouper le modèle en copiant les fichiers du modèle dans votre projet Xcode, ou vous pouvez le télécharger dynamiquement depuis Firebase.

Options de regroupement de modèles
Intégré dans votre application
  • Le modèle fait partie du bundle
  • Le modèle est disponible immédiatement, même lorsque l'appareil Apple est hors ligne
  • Pas besoin de projet Firebase
Hébergé avec Firebase
  • Hébergez le modèle en le téléchargeant sur Firebase Machine Learning
  • Réduit la taille du bundle d’applications
  • Le modèle est téléchargé sur demande
  • Envoyez les mises à jour du modèle sans republier votre application
  • Tests A/B faciles avec Firebase Remote Config
  • Nécessite un projet Firebase

Avant que tu commences

  1. Si vous souhaitez télécharger un modèle , assurez-vous d'ajouter Firebase à votre projet Apple , si ce n'est pas déjà fait. Cela n’est pas obligatoire 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 :

    Rapide

    pod 'TensorFlowLiteSwift'
    

    Objectif c

    pod 'TensorFlowLiteObjC'
    

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

    Rapide

    pod 'TensorFlowLiteSwift'
    pod 'Firebase/MLModelInterpreter'
    

    Objectif 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. Chargez le modèle

Configurer une source de modèle locale

Pour regrouper le modèle avec votre application, copiez le fichier de modèle et d'étiquettes dans votre projet Xcode, en prenant soin de sélectionner Créer des références de dossier lorsque vous le faites. Le fichier modèle et les étiquettes seront inclus dans l’ensemble d’applications.

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

  • Dimensions d'entrée du modèle. C'est 320x320 par défaut.
  • Détections maximales du modèle. C'est 40 par défaut.

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 lors de sa publication :

Rapide

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

Objectif c

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

Ensuite, démarrez 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 n'est pas sur l'appareil ou si une version plus récente du modèle est disponible, la tâche téléchargera le modèle de manière asynchrone depuis Firebase :

Rapide

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

Objectif 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 de devoir utiliser le modèle.

Créez un détecteur d'objet à partir de votre modèle

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

Si vous disposez uniquement d'un modèle fourni localement, créez simplement un interpréteur à partir du fichier modèle :

Rapide

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()

Objectif 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 devrez 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 de modèle à l'aide de la méthode isModelDownloaded(remoteModel:) du gestionnaire de modèles.

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

Rapide

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()

Objectif 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 disposez uniquement 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 interface utilisateur) jusqu'à ce que vous confirmiez que le modèle a été téléchargé.

Vous pouvez obtenir l'état de téléchargement du modèle en attachant des observateurs au centre de notifications par défaut. Assurez-vous d'utiliser une référence faible à self dans le bloc observateur, car les téléchargements peuvent prendre un certain temps et l'objet d'origine peut être libéré à la fin du téléchargement. Par exemple:

Rapide

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]
    // ...
}

Objectif 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éparez l'image d'entrée

Ensuite, vous devez préparer vos images pour l'interpréteur TensorFlow Lite.

  1. Recadrez et redimensionnez l'image aux dimensions d'entrée du modèle, comme spécifié 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 objet Data ( NSData ) :

    Rapide

    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)
      }
    }
    

    Objectif 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écutez le détecteur d'objet

Ensuite, transmettez l'entrée préparée à l'interprète :

Rapide

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

Objectif 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. Obtenez des informations sur les objets détectés

Si la détection d'objet réussit, le modèle produit en sortie trois tableaux de 40 éléments (ou tout ce qui a été 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 second, un tableau d’étiquettes ; et le troisième, un ensemble de valeurs de confiance. Pour obtenir les sorties du modèle :

Rapide

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)

Objectif 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; }

Ensuite, vous pouvez combiner les sorties d'étiquettes avec votre dictionnaire d'étiquettes :

Rapide

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))")
    }
}

Objectif 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 étiqueter des images dans une application en temps réel, suivez ces directives pour obtenir les meilleures fréquences d'images :

  • Accélérez les appels au détecteur. Si une nouvelle image vidéo devient disponible pendant le fonctionnement du détecteur, supprimez l'image.
  • Si vous utilisez la sortie du détecteur pour superposer des graphiques sur l'image d'entrée, obtenez d'abord le résultat, puis effectuez le rendu de l'image et la superposition en une seule étape. Ce faisant, vous effectuez le rendu sur la surface d'affichage une seule fois pour chaque image d'entrée. Consultez les classes previewOverlayView et FIRDetectionOverlayView dans l’exemple d’application de présentation pour un exemple.