بعد تدريب نموذجك الخاص باستخدام AutoML Vision Edge، يمكنك استخدامه في تطبيقك لرصد العناصر في الصور.
هناك طريقتان لدمج النماذج المدرَّبة من AutoML Vision Edge. يمكنك تجميع النموذج من خلال نسخ ملفات النموذج إلى مشروع Xcode، أو يمكنك تنزيله ديناميكيًا من Firebase.
خيارات تجميع النماذج | |
---|---|
مضمّنة في تطبيقك |
|
مستضافة باستخدام Firebase |
|
قبل البدء
إذا كنت تريد تنزيل نموذج، تأكَّد من إضافة Firebase إلى مشروع Apple، إذا لم يسبق لك إجراء ذلك. ولا يكون ذلك مطلوبًا عند تجميع النموذج.
أدرِج مكتبتَي TensorFlow وFirebase في ملف Podfile:
لتضمين نموذج مع تطبيقك، اتّبِع الخطوات التالية:
pod 'TensorFlowLiteSwift'
pod 'TensorFlowLiteObjC'
لتنزيل نموذج بشكل ديناميكي من Firebase، أضِف التبعية
Firebase/MLModelInterpreter
:pod 'TensorFlowLiteSwift' pod 'Firebase/MLModelInterpreter'
pod 'TensorFlowLiteObjC' pod 'Firebase/MLModelInterpreter'
بعد تثبيت أو تعديل Pods في مشروعك، افتح مشروع Xcode باستخدام
.xcworkspace
.
1. تحميل النموذج
ضبط مصدر نموذج محلي
لتضمين النموذج في تطبيقك، انسخ النموذج وملف التصنيفات إلى مشروع Xcode، مع الحرص على اختيار إنشاء مراجع للمجلدات عند إجراء ذلك. سيتم تضمين ملف النموذج والتصنيفات في حِزمة التطبيق.
اطّلِع أيضًا على الملف tflite_metadata.json
الذي تم إنشاؤه بجانب النموذج. تحتاج إلى قيمتَين:
- أبعاد الإدخال للنموذج يبلغ حجمها 320 × 320 تلقائيًا.
- الحد الأقصى لعمليات الرصد التي يمكن أن يجريها النموذج القيمة التلقائية هي 40.
إعداد مصدر نموذج مستضاف على Firebase
لاستخدام النموذج المستضاف عن بُعد، أنشئ عنصر CustomRemoteModel
، وحدِّد الاسم الذي خصّصته للنموذج عند نشره:
let remoteModel = CustomRemoteModel(
name: "your_remote_model" // The name you assigned in the Google Cloud console.
)
FIRCustomRemoteModel *remoteModel = [[FIRCustomRemoteModel alloc]
initWithName:@"your_remote_model"];
بعد ذلك، ابدأ مهمة تنزيل النموذج، مع تحديد الشروط التي تريد السماح بالتنزيل بموجبها. إذا لم يكن النموذج متوفّرًا على الجهاز، أو إذا كان يتوفّر إصدار أحدث منه، ستنزّل المهمة النموذج بشكل غير متزامن من Firebase:
let downloadProgress = ModelManager.modelManager().download(
remoteModel,
conditions: ModelDownloadConditions(
allowsCellularAccess: true,
allowsBackgroundDownloading: true
)
)
FIRModelDownloadConditions *conditions =
[[FIRModelDownloadConditions alloc] initWithAllowsCellularAccess:YES
allowsBackgroundDownloading:YES];
NSProgress *progress = [[FIRModelManager modelManager] downloadModel:remoteModel
conditions:conditions];
تبدأ العديد من التطبيقات مهمة التنزيل في رمز التهيئة، ولكن يمكنك إجراء ذلك في أي وقت قبل الحاجة إلى استخدام النموذج.
إنشاء أداة رصد كائنات من النموذج
بعد ضبط مصادر النماذج، أنشئ عنصر TensorFlow Lite Interpreter
من أحدها.
إذا كان لديك نموذج مجمّع محليًا فقط، ما عليك سوى إنشاء مترجم من ملف النموذج:
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()
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
: إنشاء مترجم من النموذج المستضاف عن بُعد إذا تم تنزيله، ومن النموذج المحلي في الحالات الأخرى.
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()
__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; }
إذا كان لديك نموذج مستضاف عن بُعد فقط، عليك إيقاف الوظائف ذات الصلة بالنموذج، مثل إخفاء جزء من واجهة المستخدم أو عرضه باللون الرمادي، إلى أن تتأكّد من تنزيل النموذج.
يمكنك الحصول على حالة تنزيل النموذج من خلال ربط مراقِبين بـ Notification Center التلقائي. احرص على استخدام مرجع ضعيف إلى self
في كتلة المراقب، لأنّ عمليات التنزيل قد تستغرق بعض الوقت، ويمكن تحرير العنصر الأصلي قبل انتهاء عملية التنزيل. على سبيل المثال:
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]
// ...
}
__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
):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) } }
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- تشغيل أداة رصد العناصر
بعد ذلك، مرِّر الإدخال المُعدّ إلى المترجم:
try interpreter.copy(inputData, toInputAt: 0)
try interpreter.invoke()
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
).
يتطابق كل عنصر مع كائن محتمل واحد. المصفوفة الأولى هي مصفوفة مربعات محيطة، والثانية هي مصفوفة تصنيفات، والثالثة هي مصفوفة قيم الثقة. للحصول على نتائج النموذج، اتّبِع الخطوات التالية:
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)
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; }
بعد ذلك، يمكنك دمج نواتج التصنيف مع قاموس التصنيفات:
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))")
}
}
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 في تطبيق العرض التوضيحي النموذجي للحصول على مثال.