AutoML Vision Edge를 사용하여 자체 모델을 학습시킨 후 앱에서 이 모델을 사용해 이미지에서 객체를 감지할 수 있습니다.
AutoML Vision Edge에서 학습된 모델을 통합하는 방법에는 두 가지가 있습니다. 모델의 파일을 Xcode 프로젝트로 복사하여 모델을 번들로 묶거나 Firebase에서 동적으로 다운로드할 수 있습니다.
모델 번들 옵션 | |
---|---|
앱에 번들로 제공 |
|
Firebase로 호스팅 |
|
시작하기 전에
모델을 다운로드하려면 Apple 프로젝트에 Firebase를 추가해야 합니다(아직 추가하지 않은 경우). 모델을 번들로 묶을 때는 이 작업이 필요하지 않습니다.
Podfile에 TensorFlow 및 Firebase 라이브러리를 포함합니다.
모델을 앱과 함께 번들로 묶는 방법은 다음과 같습니다.
Swift
pod 'TensorFlowLiteSwift'
Objective-C
pod 'TensorFlowLiteObjC'
Firebase에서 모델을 동적으로 다운로드하려면
Firebase/MLModelInterpreter
종속 항목을 추가합니다.Swift
pod 'TensorFlowLiteSwift' pod 'Firebase/MLModelInterpreter'
Objective-C
pod 'TensorFlowLiteObjC' pod 'Firebase/MLModelInterpreter'
프로젝트의 pod를 설치하거나 업데이트한 후
.xcworkspace
를 사용하여 Xcode 프로젝트를 엽니다.
1. 모델 로드
로컬 모델 소스 구성
모델을 앱과 번들로 묶으려면 Xcode 프로젝트에 모델 및 라벨 파일을 복사합니다. 이때 폴더 참조 만들기를 선택해야 합니다. 모델 파일과 라벨이 App Bundle에 포함됩니다.
또한 모델과 함께 생성된 tflite_metadata.json
파일도 확인합니다. 다음 두 가지 값이 필요합니다.
- 모델의 입력 크기. 기본적으로 320x320입니다.
- 모델의 최대 감지 수. 기본적으로 40개입니다.
Firebase 호스팅 모델 소스 구성
원격 호스팅 모델을 사용하려면 모델을 게시할 때 모델에 할당한 이름을 지정하여 CustomRemoteModel
객체를 만듭니다.
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"];
이제 다운로드를 허용할 조건을 지정하여 모델 다운로드 작업을 시작합니다. 모델이 기기에 없거나 최신 버전의 모델을 사용할 수 있으면 모델이 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];
대부분의 앱은 초기화 코드에서 다운로드 작업을 시작하지만 모델 사용이 필요한 시점 이전에 언제든지 다운로드할 수 있습니다.
모델에서 객체 감지기 만들기
모델 소스를 구성한 후 모델 소스 중 하나에서 TensorFlow Lite Interpreter
객체를 만듭니다.
로컬로 번들된 모델만 있다면 모델 파일에서 인터프리터를 만듭니다.
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; }
원격 호스팅 모델이 있다면 실행 전에 모델이 다운로드되었는지 확인해야 합니다. 모델 관리자의 isModelDownloaded(remoteModel:)
메서드로도 모델 다운로드 작업의 상태를 확인할 수 있습니다.
이 상태는 인터프리터를 실행하기 전에만 확인하면 되지만 원격 호스팅 모델과 로컬로 번들된 모델이 모두 있는 경우에는 Interpreter
를 인스턴스화할 때 이 확인 작업을 수행하는 것이 합리적일 수 있습니다. 원격 모델이 다운로드되었으면 원격 모델에서, 그렇지 않으면 로컬 모델에서 인터프리터를 만듭니다.
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; }
원격 호스팅 모델만 있다면 모델 다운로드 여부가 확인될 때까지 모델 관련 기능 사용을 중지해야 합니다(예: UI 비활성화 또는 숨김).
기본 알림 센터에 관찰자를 연결하여 모델 다운로드 상태를 가져올 수 있습니다. 다운로드하는 데 시간이 걸릴 수 있고 다운로드가 완료되면 원래 객체가 해제될 수 있으므로 관찰자 블록의 self
에 약한 참조를 사용하세요. 예를 들면 다음과 같습니다.
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. 입력 이미지 준비
다음으로 TensorFlow Lite 인터프리터용 이미지를 준비해야 합니다.
tflite_metadata.json
파일(기본적으로 320x320픽셀)에 지정된 대로 이미지를 모델의 입력 크기로 자르고 크기를 조정합니다. Core 이미지 또는 타사 라이브러리를 사용하여 이 작업을 수행할 수 있습니다.이미지 데이터를
Data
(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. 객체 감지기 실행
다음으로 준비된 입력을 인터프리터에 전달합니다.
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. 감지된 객체 정보 가져오기
객체 감지가 성공하면 모델은 각각 40개의 요소(또는 tflite_metadata.json
파일에 지정된 모든 요소)로 구성된 배열 3개를 출력으로 생성합니다.
각 요소는 하나의 잠재적 객체에 해당합니다. 첫 번째 배열은 경계 상자의 배열이고, 두 번째는 라벨의 배열이고, 세 번째는 신뢰값의 배열입니다. 모델 출력을 가져오려면 다음 안내를 따르세요.
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; }
그런 다음 라벨 출력을 라벨 사전과 결합할 수 있습니다.
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);
}
}
실시간 성능 향상을 위한 팁
실시간 애플리케이션에서 이미지 라벨을 지정하려는 경우 최상의 프레임 속도를 얻으려면 다음 안내를 따르세요.
- 인식기 호출을 제한합니다. 인식기가 실행 중일 때 새 동영상 프레임이 제공되는 경우 해당 프레임을 드롭합니다.
- 인식기 출력을 사용하여 입력 이미지에 그래픽을 오버레이하는 경우 먼저 인식 결과를 가져온 후 이미지를 렌더링하고 단일 단계로 오버레이합니다. 이렇게 하면 입력 프레임별로 한 번만 디스플레이 표면에 렌더링됩니다. 관련 예시는 쇼케이스 샘플 앱에서 previewOverlayView 및 FIRDetectionOverlayView 클래스를 참조하세요.