אחרי שמאמנים מודל משלכם באמצעות AutoML Vision Edge, אפשר להשתמש בו באפליקציה כדי לזהות אובייקטים בתמונות.
יש שתי דרכים לשלב מודלים שעברו אימון מ-AutoML Vision Edge. אפשר לארוז את המודל על ידי העתקת הקבצים שלו לפרויקט Xcode, או להוריד אותו באופן דינמי מ-Firebase.
אפשרויות של חבילות מודלים | |
---|---|
חבילה באפליקציה |
|
אירוח ב-Firebase |
|
לפני שמתחילים
אם אתם רוצים להוריד מודל, חשוב לוודא שהוספתם את Firebase לפרויקט שלכם ב-Apple, אם עדיין לא עשיתם זאת. לא צריך לעשות זאת כשמקבצים את המודל.
כוללים את ספריות TensorFlow ו-Firebase ב-Podfile:
כדי לצרף מודל לאפליקציה:
Swift
pod 'TensorFlowLiteSwift'
Objective-C
pod 'TensorFlowLiteObjC'
כדי להוריד מודל באופן דינמי מ-Firebase, מוסיפים את התלות
Firebase/MLModelInterpreter
:Swift
pod 'TensorFlowLiteSwift' pod 'Firebase/MLModelInterpreter'
Objective-C
pod 'TensorFlowLiteObjC' pod 'Firebase/MLModelInterpreter'
אחרי שמתקינים או מעדכנים את ה-Pods של הפרויקט, פותחים את פרויקט Xcode באמצעות
.xcworkspace
שלו.
1. טעינת המודל
הגדרת מקור מודל מקומי
כדי לארוז את המודל עם האפליקציה, מעתיקים את קובץ המודל והתוויות לפרויקט Xcode, תוך הקפדה על בחירה באפשרות Create folder references. קובץ המודל והתוויות ייכללו בחבילת האפליקציה.
בנוסף, בדקו את הקובץ 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];
אפליקציות רבות מתחילות את משימת ההורדה בקוד האתחול שלהם, אבל אפשר לעשות זאת בכל שלב לפני שתצטרכו להשתמש במודל.
יצירת גלאי אובייקטים מהמודל
אחרי שמגדירים את מקורות המודלים, יוצרים אובייקט Interpreter
של TensorFlow Lite מאחד מהם.
אם יש לכם רק מודל בחבילה מקומית, פשוט יוצרים מתורגמן מקובץ המודל:
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; }
אם יש לכם רק מודל שמתארח מרחוק, עליכם להשבית את הפונקציונליות שקשורה למודל – לדוגמה, להפוך חלק מממשק המשתמש לאפור או להסתיר אותו – עד שתאשרו שהמודל הוריד.
כדי לקבל את סטטוס ההורדה של המודל, צריך לצרף משגיחים למרכז ההתראות שמוגדר כברירת מחדל. חשוב להשתמש בהפניה חלשה ל-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 Image או ספרייה של צד שלישימעתיקים את נתוני התמונה ל-
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
).
כל רכיב תואם לאובייקט פוטנציאלי אחד. המערך הראשון הוא מערך של תיבות מלבניות, המערך השני הוא מערך של תוויות והמערך השלישי הוא מערך של ערכי ביטחון. כדי לקבל את הפלט של המודל:
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);
}
}
טיפים לשיפור הביצועים בזמן אמת
כדי להוסיף תוויות לתמונות באפליקציה בזמן אמת, צריך לפעול לפי ההנחיות הבאות כדי להשיג את קצב הפריימים הטוב ביותר:
- צמצום מספר הקריאות למזהה. אם פריים חדש לסרטון הופך לזמינה בזמן שהגלאי פועל, משחררים את הפריים.
- אם אתם משתמשים בפלט של הגלאי כדי להוסיף שכבת-על של גרפיקה לתמונה הקלט, תחילה צריך לקבל את התוצאה, ואז לבצע עיבוד תמונה של התמונה ולהוסיף את שכבת-העל בשלב אחד. כך תוכלו לבצע עיבוד (render) למשטח התצוגה רק פעם אחת לכל מסגרת קלט. דוגמה לכך מופיעה במחלקות previewOverlayView ו-FIRDetectionOverlayView באפליקציית הדוגמה.