Phát hiện đối tượng trong hình ảnh bằng mô hình được huấn luyện bằng AutoML trên các nền tảng của 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 để phát hiện đối tượng trong hình ảnh.

Có hai cách để tích hợp các mô hình được huấn luyện 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 hoặc tải mô hình xuống một cách linh động từ Firebase.

Tuỳ chọn gói mô hình
Được đóng gói trong ứng dụng
  • Mô hình là một phần của gói
  • Bạn có thể sử dụng mô hình ngay lập tức, ngay cả khi thiết bị Apple không có kết nối mạng
  • Không cần dự án Firebase
Được lưu trữ bằng Firebase
  • Lưu trữ mô hình bằng cách tải mô hình đó 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 nội dung cập nhật mô hình mà không cần phát hành lại ứng dụng
  • Dễ dàng thử nghiệm A/B bằng Cấu hình từ xa Firebase
  • Cần có dự án Firebase

Trước khi bắt đầu

  1. Nếu bạn muốn tải mô hình xuống, hãy nhớ thêm Firebase vào dự án Apple nếu bạn chưa thực hiện việc này. Bạn không cần thực hiện việc này khi gói mô hình.

  2. Đưa thư viện TensorFlow và Firebase vào Podfile:

    Cách gói một mô hình với ứng dụng:

    Swift

    pod 'TensorFlowLiteSwift'
    

    Objective-C

    pod 'TensorFlowLiteObjC'
    

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

    Swift

    pod 'TensorFlowLiteSwift'
    pod 'Firebase/MLModelInterpreter'
    

    Objective-C

    pod 'TensorFlowLiteObjC'
    pod 'Firebase/MLModelInterpreter'
    
  3. Sau khi bạn cài đặt hoặc cập nhật Pods của dự án, hãy mở dự án Xcode bằ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, hãy sao chép mô hình và tệp nhãn vào dự án Xcode, nhớ chọn Create folder references (Tạo tham chiếu thư mục) khi bạn thực hiện việc này. Tệp mô hình và nhãn sẽ được đưa vào 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ị:

  • Phương diện đầu vào của mô hình. Kích thước này là 320x320 theo mặc định.
  • Số lần phát hiện tối đa của mô hình. Giá trị này là 40 theo mặc định.

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

Để 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 phát hành:

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

Sau đó, hãy bắt đầu tác vụ tải mô hình xuống, 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ô hình mới hơn, thì tác vụ sẽ tải mô hình xuống không đồng bộ từ 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];

Nhiều ứng dụng bắt đầu tác vụ tải xuống trong mã khởi chạy, nhưng bạn có thể làm như vậy bất cứ 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

Sau khi bạn định cấu hình nguồn mô hình, hãy tạo một đố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ộ, bạn chỉ cần tạo một trình thông dịch từ tệp mô hình:

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

Nếu có một mô hình được lưu trữ từ xa, bạn sẽ phải kiểm tra để đảm bảo rằng mô hình đó đã được tải xuống trước khi chạy. Bạn có thể kiểm tra trạng thái của tác vụ tải mô hình xuống 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 diễn giải, 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ì bạn nên thực hiện việc kiểm tra này khi tạo bản sao Interpreter: tạo trình diễn giải từ mô hình từ xa nếu mô hình đó đã được tải xuống và từ mô hình cục bộ nếu không.

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

Nếu chỉ có 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ụ: làm mờ hoặc ẩn một phần giao diện người dùng) cho đến khi xác nhận rằng mô hình đã được tải xuống.

Bạn có thể xem trạng thái tải mô hình xuống bằng cách đính kèm trình quan sát vào Trung tâm thông báo mặc định. Hãy nhớ sử dụng tệp tham chiếu yếu đến self trong khối trình quan sát, vì quá trình tải xuống có thể mất 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ụ:

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. 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 diễn giải TensorFlow Lite.

  1. Cắt và điều chỉnh 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ể làm việc này bằng Core Image hoặc thư viện bên thứ ba

  2. Sao chép dữ liệu hình ảnh vào Data (đối tượng 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. Chạy trình phát hiện đối tượng

Tiếp theo, hãy truyền dữ liệu đầu vào đã chuẩn bị cho trình thông dịch:

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. 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 3 mảng gồm 40 phần tử (hoặc bất kỳ giá trị 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; mảng thứ hai là một mảng các nhãn; và mảng thứ ba là một mảng các giá trị tin cậy. Cách lấy kết quả của mô hình:

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

Sau đó, bạn có thể kết hợp kết quả nhãn với từ điển nhãn:

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

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

Nếu bạn muốn gắn nhãn hình ảnh trong một ứng dụng theo 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:

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