Phát hiện các đối tượng trong hình ảnh bằng mô hình được đào tạo AutoML trên nền tảng Apple

Sau khi huấn luyện mô hình của riêng mình bằng AutoML Vision Edge , bạn có thể sử dụng mô hình đó trong ứng dụng của mình để phát hiện các đối tượng trong hình ảnh.

Có hai cách để tích hợp các mô hình được đào tạo từ AutoML Vision Edge. Bạn có thể gói mô hình bằng cách sao chép các tệp của mô hình vào dự án Xcode của mình hoặc bạn có thể tải mô hình xuống từ Firebase một cách linh hoạt.

Tùy chọn gói mô hình
Được gói trong ứng dụng của bạn
  • Mô hình này là một phần của gói
  • Model có sẵn ngay lập tức, ngay cả khi thiết bị Apple ngoại tuyến
  • Không cần dự án Firebase
Được lưu trữ với Firebase
  • Lưu trữ mô hình bằng cách tải nó lên Firebase Machine Learning
  • Giảm kích thước gói ứng dụng
  • Mô hình được tải xuống theo yêu cầu
  • Đẩy các bản cập nhật mô hình mà không cần xuất bản lại ứng dụng của bạn
  • Thử nghiệm A/B dễ dàng với Cấu hình từ xa Firebase
  • Yêu cầu dự án Firebase

Trước khi bắt đầu

  1. Nếu bạn muốn tải xuống một mô hình , hãy đảm bảo bạn thêm Firebase vào dự án Apple của mình , nếu bạn chưa làm như vậy. Điều này không bắt buộc khi bạn đóng gói mô hình.

  2. Bao gồm các thư viện TensorFlow và Firebase trong Podfile của bạn:

    Để đóng gói một mô hình với ứng dụng của bạn:

    Nhanh

    pod 'TensorFlowLiteSwift'
    

    Mục tiêu-C

    pod 'TensorFlowLiteObjC'
    

    Để tải xuống động một mô hình từ Firebase, hãy thêm phần phụ thuộc Firebase/MLModelInterpreter :

    Nhanh

    pod 'TensorFlowLiteSwift'
    pod 'Firebase/MLModelInterpreter'
    

    Mục tiêu-C

    pod 'TensorFlowLiteObjC'
    pod 'Firebase/MLModelInterpreter'
    
  3. Sau khi bạn cài đặt hoặc cập nhật Pod của dự án, hãy mở dự án Xcode của bạn bằng cách sử dụng .xcworkspace .

1. Tải mô hình

Định cấu hình nguồn mô hình cục bộ

Để gói mô hình với ứng dụng của bạn, hãy sao chép tệp mô hình và nhãn vào dự án Xcode của bạn, chú ý chọn Tạo tham chiếu thư mục khi bạn làm như vậy. Tệp mô hình và nhãn sẽ được bao gồm trong gói ứng dụng.

Ngoài ra, hãy xem tệp tflite_metadata.json được tạo cùng với mô hình. Bạn cần hai giá trị:

  • Kích thước đầu vào của mô hình Đây là kích thước mặc định là 320x320.
  • Phát hiện tối đa của mô hình. Đây là 40 theo mặc định.

Định cấu hình nguồn mô hình được lưu trữ trên Firebase

Để sử dụng mô hình được lưu trữ từ xa, hãy tạo một đối tượng CustomRemoteModel , chỉ định tên mà bạn đã gán cho mô hình khi xuất bản nó:

Nhanh

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

Mục tiêu-C

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

Sau đó, bắt đầu tác vụ tải xuống mô hình, chỉ định các điều kiện mà bạn muốn cho phép tải xuống. Nếu mô hình không có trên thiết bị hoặc nếu có phiên bản mới hơn của mô hình thì tác vụ sẽ tải mô hình xuống một cách không đồng bộ từ Firebase:

Nhanh

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

Mục tiêu-C

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

Nhiều ứng dụng bắt đầu tác vụ tải xuống trong mã khởi tạo của chúng nhưng bạn có thể làm như vậy bất kỳ lúc nào trước khi cần sử dụng mô hình.

Tạo trình phát hiện đối tượng từ mô hình của bạn

Sau khi bạn định cấu hình các nguồn mô hình của mình, hãy tạo đối tượng Interpreter TensorFlow Lite từ một trong các nguồn đó.

Nếu bạn chỉ có một mô hình được đóng gói cục bộ, chỉ cần tạo một trình thông dịch từ tệp mô hình:

Nhanh

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

Mục tiêu-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; }

Nếu bạn có một mô hình được lưu trữ từ xa, bạn sẽ phải kiểm tra xem nó đã được tải xuống chưa trước khi chạy nó. Bạn có thể kiểm tra trạng thái của tác vụ tải xuống mô hình bằng phương thức isModelDownloaded(remoteModel:) của trình quản lý mô hình.

Mặc dù bạn chỉ phải xác nhận điều này trước khi chạy trình thông dịch, nhưng nếu bạn có cả mô hình được lưu trữ từ xa và mô hình được đóng gói cục bộ, thì có thể thực hiện kiểm tra này khi khởi tạo Interpreter : tạo một trình thông dịch từ mô hình từ xa nếu nó đã được tải xuống và từ mô hình cục bộ nếu không.

Nhanh

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

Mục tiêu-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; }

Nếu chỉ có một mô hình được lưu trữ từ xa, bạn nên tắt chức năng liên quan đến mô hình—ví dụ: chuyển sang màu xám hoặc ẩn một phần giao diện người dùng của bạn—cho đến khi bạn xác nhận rằng mô hình đã được tải xuống.

Bạn có thể nhận trạng thái tải xuống mô hình bằng cách đính kèm người quan sát vào Trung tâm thông báo mặc định. Hãy đảm bảo sử dụng tham chiếu yếu tới self trong khối quan sát, vì quá trình tải xuống có thể mất chút thời gian và đối tượng ban đầu có thể được giải phóng khi quá trình tải xuống kết thúc. Ví dụ:

Nhanh

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

Mục tiêu-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. Chuẩn bị hình ảnh đầu vào

Tiếp theo, bạn cần chuẩn bị hình ảnh cho trình thông dịch TensorFlow Lite.

  1. Cắt và chia tỷ lệ hình ảnh theo kích thước đầu vào của mô hình, như được chỉ định trong tệp tflite_metadata.json (320x320 pixel theo mặc định). Bạn có thể thực hiện việc này bằng Core Image hoặc thư viện của bên thứ ba

  2. Copy dữ liệu ảnh vào một đối tượng Data ( NSData ):

    Nhanh

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

    Mục tiêu-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. Chạy trình dò ​​tìm đối tượng

Tiếp theo, chuyển đầu vào đã chuẩn bị cho trình thông dịch:

Nhanh

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

Mục tiêu-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. Nhận thông tin về các đối tượng được phát hiện

Nếu phát hiện đối tượng thành công, mô hình sẽ tạo ra ba mảng gồm 40 phần tử đầu ra (hoặc bất kỳ phần tử nào đã được chỉ định trong tệp tflite_metadata.json ). Mỗi phần tử tương ứng với một đối tượng tiềm năng. Mảng đầu tiên là một mảng các hộp giới hạn; thứ hai, một mảng nhãn; và thứ ba, một loạt các giá trị tin cậy. Để có được kết quả đầu ra của mô hình:

Nhanh

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)

Mục tiêu-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; }

Sau đó, bạn có thể kết hợp đầu ra nhãn với từ điển nhãn của mình:

Nhanh

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

Mục tiêu-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);
    }
}

Mẹo để cải thiện hiệu suất thời gian thực

Nếu bạn muốn gắn nhãn hình ảnh trong ứng dụng thời gian thực, hãy làm theo các nguyên tắc sau để đạt được tốc độ khung hình tốt nhất:

  • Van tiết lưu gọi tới máy dò. Nếu có khung hình video mới trong khi trình phát hiện đang chạy, hãy thả khung hình đó xuống.
  • Nếu bạn đang sử dụng đầu ra của bộ dò để phủ đồ họa lên hình ảnh đầu vào, trước tiên hãy lấy kết quả, sau đó hiển thị hình ảnh và lớp phủ trong một bước duy nhất. Bằng cách đó, bạn chỉ hiển thị trên bề mặt hiển thị một lần cho mỗi khung hình đầu vào. Xem các lớp PreviewOverlayViewFIRDetectionOverlayView trong ứng dụng mẫu giới thiệu để biết ví dụ.