Wykrywanie obiektów w obrazach za pomocą modelu wytrenowanego przez AutoML na platformach Apple

Gdy wytrenujesz własny model przy użyciu AutoML Vision Edge, możesz używać go w swojej aplikacji do wykrywania obiektów na obrazach.

Istnieją 2 sposoby integracji modeli wytrenowanych w AutoML Vision Edge. Dostępne opcje spakować model przez skopiowanie plików modelu do projektu Xcode lub może dynamicznie pobrać je z Firebase.

Opcje grupowania modeli
Pakiet w aplikacji
  • Model jest częścią pakietu
  • Model jest dostępny od razu, nawet gdy urządzenie Apple jest offline
  • Nie potrzeba projektu Firebase
Hostowane w Firebase

Zanim zaczniesz

  1. Jeśli chcesz pobrać model, dodaj Firebase do swojego projektu Apple, jeśli jeszcze nie zostało to zrobione. Nie jest to wymagane, gdy łączysz model.

  2. Umieść w pliku Podfile biblioteki TensorFlow i Firebase:

    Aby dołączyć model do aplikacji:

    Swift

    pod 'TensorFlowLiteSwift'
    

    Objective-C

    pod 'TensorFlowLiteObjC'
    

    Aby dynamicznie pobierać model z Firebase, dodaj parametr Zależność Firebase/MLModelInterpreter:

    Swift

    pod 'TensorFlowLiteSwift'
    pod 'Firebase/MLModelInterpreter'
    

    Objective-C

    pod 'TensorFlowLiteObjC'
    pod 'Firebase/MLModelInterpreter'
    
  3. Po zainstalowaniu lub zaktualizowaniu podów swojego projektu otwórz projekt Xcode za pomocą: .xcworkspace.

1. Wczytaj model

Skonfiguruj źródło modelu lokalnego

Aby połączyć model z aplikacją: Skopiuj model i plik etykiet do projektu Xcode, pamiętając o zaznaczeniu Utwórz odwołania do folderów. Plik modelu i etykiety będą widoczne w pakiecie aplikacji.

Spójrz też na plik tflite_metadata.json, który został utworzony obok model. Potrzebujesz 2 wartości:

  • Wymiary wejściowe modelu. Domyślny rozmiar to 320 x 320.
  • Maksymalna liczba wykrywania przez model. Wartość domyślna to 40.

Skonfiguruj źródło modelu hostowanego w Firebase

Aby używać modelu hostowanego zdalnie, utwórz CustomRemoteModel z nazwą przypisaną do modelu podczas jego publikacji:

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

Następnie rozpocznij zadanie pobierania modelu, określając warunki, które którym chcesz zezwolić na pobieranie. Jeśli nie ma modelu na urządzeniu lub jest on nowszy gdy dostępna będzie wersja modelu, zadanie asynchronicznie pobierze model z 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];

Wiele aplikacji rozpoczyna zadanie pobierania w kodzie inicjowania, możesz to zrobić w dowolnym momencie, zanim trzeba będzie skorzystać z modelu.

Tworzenie wzorca do wykrywania obiektów na podstawie modelu

Po skonfigurowaniu źródeł modelu utwórz Interpreter w TensorFlow Lite lub obiektu z jednego z nich.

Jeśli masz tylko model w pakiecie lokalnym, po prostu utwórz tłumacza plik modelu:

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

Jeśli masz model hostowany zdalnie, musisz sprawdzić, czy został pobrane przed uruchomieniem. Stan pobierania modelu możesz sprawdzić za pomocą metody isModelDownloaded(remoteModel:) menedżera modeli.

Mimo że przed uruchomieniem tłumaczenia rozmowy trzeba to potwierdzić, korzystają zarówno z modelu hostowanego zdalnie, jak i z pakietu lokalnego, może to sprawić, warto przeprowadzić tę kontrolę przy tworzeniu wystąpienia Interpreter: utwórz interpreter z modelu zdalnego, jeśli został pobrany, oraz z modelu lokalnego w przeciwnym razie.

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

Jeśli masz tylko model hostowany zdalnie, wyłącz powiązany z nim model funkcji – na przykład wyszarzenia lub ukrycia części interfejsu – do potwierdzasz, że model został pobrany.

Stan pobierania modelu możesz sprawdzić, dołączając obserwatorów do wartości domyślnej. Centrum powiadomień. Pamiętaj, aby w obserwatorium używać słabego odniesienia do self bo pobieranie może trochę potrwać, a źródłowy obiekt zwolniony do momentu zakończenia pobierania. Przykład:

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. Przygotowywanie obrazu wejściowego

Następnie musisz przygotować obrazy do interpretera TensorFlow Lite.

  1. Przytnij i przeskaluj obraz do wymiarów wejściowych modelu, zgodnie z opisem w sekcji plik tflite_metadata.json (domyślnie 320 x 320 pikseli). Możesz to zrobić z obrazem podstawowym lub biblioteką innej firmy

  2. Skopiuj dane obrazu do obiektu Data (obiektu 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. Uruchom detektor obiektów

Następnie przekaż tłumaczowi przygotowane dane wejściowe:

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. Uzyskiwanie informacji o wykrytych obiektach

Jeśli wykrywanie obiektów się powiedzie, model wygeneruje jako dane wyjściowe 3 tablice wyjściowe po 40 elementów (lub innych elementów określonych w pliku tflite_metadata.json). Każdy element odpowiada jednemu potencjalnemu obiektowi. Pierwsza tablica jest tablicą ramek ograniczających; drugi – tablica etykiet; a trzeci – tablicę wartości ufności. Aby uzyskać dane wyjściowe modelu:

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

Następnie możesz połączyć dane wyjściowe etykiet ze słownikiem etykiet:

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

Wskazówki dotyczące poprawy skuteczności w czasie rzeczywistym

Jeśli chcesz oznaczać obrazy w aplikacji działającej w czasie rzeczywistym, postępuj zgodnie z tymi instrukcjami wytycznych dotyczących uzyskiwania najlepszej liczby klatek na sekundę:

  • Ogranicz wywołania do detektora. Jeśli nowa klatka wideo dostępnych, gdy detektor jest uruchomiony, upuść ramkę.
  • Jeśli używasz danych wyjściowych detektora do nakładania grafiki na obraz wejściowy, najpierw uzyskaj wynik, a następnie wyrenderuj obraz i nakładanie nakładek w jednym kroku. W ten sposób renderowanie na powierzchni tylko raz na każdą ramkę wejściową. Zobacz previewOverlayView. i FIRDetectionOverlayView w aplikacji z funkcją prezentacji.