الوحدة 8 — التكامل مع iOS
النموذج نفسه الذي شغَّلناه على Android في الوحدة 7 سيعمل الآن على iPhone. الملفّ plantvillage_int8.tflite لا يتغيَّر، لكنّ محيطه يتغيَّر كلّيًّا: نظام آخر، ولغة أخرى (Swift بدل Kotlin)، وأدوات مختلفة لالتقاط صور الكاميرا (AVFoundation بدل CameraX)، وبديل تنافسيّ عتيد اسمه Core ML يشتغل على Neural Engine مباشرة على أجهزة Apple. سنغطّي في هذه الوحدة كيف نُدمج TFLite بأنظف طريقة على iOS، ومتى يُفضَّل استعمال Core ML بدل TFLite أو معه.
أوّلًا: إضافة TFLite إلى المشروع
طريقتان للتثبيت، كلاهما مدعوم رسميًّا:
عبر CocoaPods (الأكثر شيوعًا، يعمل بلا مفاجآت):
# Podfile
platform :ios, '13.0'
use_frameworks!
target 'PlantLeafApp' do
pod 'TensorFlowLiteSwift', '~> 2.16.0'
pod 'TensorFlowLiteSwift/CoreML', '~> 2.16.0' # مفوَّض Core ML (اختياريّ)
pod 'TensorFlowLiteSwift/Metal', '~> 2.16.0' # مفوَّض GPU عبر Metal
end
ثمّ pod install وفتح ملفّ .xcworkspace (لا .xcodeproj).
عبر Swift Package Manager (المستقبل، مندمج مع Xcode):
في Xcode: File → Add Packages، والرابط https://github.com/tensorflow/tensorflow.git، ثمّ اختيار حزمة TensorFlowLiteSwift. الطريقة أنظف مع مشاريع Swift الحديثة، لكنّها لا تدعم كلّ المفوَّضين بالطريقة نفسها التي تدعمها CocoaPods.
نقطة مهمّة: iOS 13.0 هو الحدّ الأدنى المُوصى به لتفعيل مفوَّض Core ML. للأجهزة الأقدم، سيرجع الاستدلال إلى CPU تلقائيًّا، وهو ما يريده TFLite.
إضافة النموذج إلى الحزمة
ملفّ plantvillage_int8.tflite يُسحب داخل Xcode إلى مجموعة « Resources »، مع تفعيل Copy items if needed واختيار الهدف الصحيح. بلا هذه الخطوة، الملفّ لن يوجد في الحزمة النهائيّة، والنموذج لن يُحمَّل. نفس الشيء لملفّ labels.txt.
المسار الأوّل: المفسّر الخام في Swift
خلاف Android التي تقدِّم Task Library ذات مستوى عالٍ، TFLite على iOS يعمل أساسًا مع المفسّر الخام. الشيفرة الأدنى:
import TensorFlowLite
import UIKit
class PlantLeafClassifier {
private var interpreter: Interpreter
private let labels: [String]
private let inputSize = 224
init() throws {
guard let modelPath = Bundle.main.path(
forResource: "plantvillage_int8", ofType: "tflite"
) else {
throw NSError(domain: "PlantLeaf", code: 1)
}
// خيارات المفسّر مع مفوَّض Core ML
var options = Interpreter.Options()
options.threadCount = 4
let coreMLDelegate = CoreMLDelegate() // يعمل على Neural Engine
interpreter = try Interpreter(
modelPath: modelPath,
options: options,
delegates: coreMLDelegate.map { [$0] } ?? []
)
try interpreter.allocateTensors()
// تحميل الأصناف
let labelsPath = Bundle.main.path(forResource: "labels", ofType: "txt")!
labels = try String(contentsOfFile: labelsPath, encoding: .utf8)
.split(separator: "\n").map(String.init)
}
func classify(image: UIImage) throws -> [(label: String, score: Float)] {
// 1) تحويل UIImage إلى Data بـint8 (224x224x3)
let data = try preprocess(image: image)
// 2) تعبئة تنسور الإدخال
try interpreter.copy(data, toInputAt: 0)
// 3) الاستدلال
try interpreter.invoke()
// 4) قراءة تنسور المخرَج
let output = try interpreter.output(at: 0)
let scores = output.data.withUnsafeBytes {
Array(UnsafeBufferPointer(
start: $0.baseAddress?.assumingMemoryBound(to: Int8.self),
count: labels.count
))
}
// 5) ترتيب أعلى ثلاثة أصناف
return scores.enumerated()
.map { (labels[$0.offset], (Float($0.element) + 128) / 255) }
.sorted { $0.1 > $1.1 }
.prefix(3)
.map { $0 }
}
}
المعالجة المسبقة: من UIImage إلى تنسور int8
خطوة حرجة تُشبه ما فعلناه على Android، مع تفاصيل Swift الخاصّة:
extension PlantLeafClassifier {
private func preprocess(image: UIImage) throws -> Data {
// 1) تصغير إلى 224x224
UIGraphicsBeginImageContextWithOptions(
CGSize(width: inputSize, height: inputSize), true, 1.0
)
image.draw(in: CGRect(x: 0, y: 0, width: inputSize, height: inputSize))
let resized = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
// 2) استخراج البيكسلات RGBA
guard let cgImage = resized.cgImage,
let rgbaData = cgImage.dataProvider?.data,
let bytes = CFDataGetBytePtr(rgbaData) else {
throw NSError(domain: "PlantLeaf", code: 2)
}
// 3) تحويل من uint8 RGBA إلى int8 RGB
var input = Data(count: inputSize * inputSize * 3)
input.withUnsafeMutableBytes { dst in
let d = dst.bindMemory(to: Int8.self)
for i in 0 ..< inputSize * inputSize {
d[3 * i + 0] = Int8(bitPattern: bytes[4 * i + 0]) &- 128
d[3 * i + 1] = Int8(bitPattern: bytes[4 * i + 1]) &- 128
d[3 * i + 2] = Int8(bitPattern: bytes[4 * i + 2]) &- 128
}
}
return input
}
}
نقطة الطرح 128 في Swift: عمليّة &- (طرح مع الالتفاف) ضروريّة لأنّ العمليّة العاديّة - تُلقي استثناء تدفّق حسابيّ. هذه دقّة صغيرة تكلّف ساعات لمن لا يعرفها.
المسار الثاني: التقاط الكاميرا مع AVFoundation
نظير CameraX على Android هو AVCaptureSession مع AVCaptureVideoDataOutput. الشيفرة أطول لكنّها مباشرة:
import AVFoundation
class CameraFeed: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
let session = AVCaptureSession()
let classifier: PlantLeafClassifier
let onResult: ([(String, Float)]) -> Void
let queue = DispatchQueue(label: "leaf.inference", qos: .userInitiated)
init(classifier: PlantLeafClassifier,
onResult: @escaping ([(String, Float)]) -> Void) throws {
self.classifier = classifier
self.onResult = onResult
super.init()
try configureSession()
}
private func configureSession() throws {
session.sessionPreset = .vga640x480
guard let device = AVCaptureDevice.default(for: .video) else {
throw NSError(domain: "Camera", code: 1)
}
let input = try AVCaptureDeviceInput(device: device)
session.addInput(input)
let output = AVCaptureVideoDataOutput()
output.setSampleBufferDelegate(self, queue: queue)
output.videoSettings = [
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA
]
session.addOutput(output)
}
func captureOutput(_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection) {
// تحويل CMSampleBuffer إلى UIImage ثمّ استدلال
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer),
let uiImage = imageFromPixelBuffer(pixelBuffer) else { return }
do {
let results = try classifier.classify(image: uiImage)
DispatchQueue.main.async { self.onResult(results) }
} catch {
print("خطأ استدلال : \(error)")
}
}
}
المفتاح: طابور .userInitiated يُشغِّل الاستدلال خارج الخيط الرئيسيّ. الرجوع إلى DispatchQueue.main لتحديث الواجهة إلزاميّ، وإلاّ فـUIKit يُلقي تحذيرات وقد يُعطِّل الشاشة.
Core ML مقابل TFLite: متى نختار أيّهما؟
Core ML خيار Apple الأصليّ للتعلّم الآليّ على iOS. فوارقه مع TFLite:
| المعيار | TFLite | Core ML |
|---|---|---|
| تعدّد المنصّات | Android وiOS ومتصفَّح | iOS وmacOS فقط |
| صيغة الملفّ | .tflite | .mlmodel أو .mlpackage |
| المسرِّع العتاديّ | Neural Engine عبر مفوَّض | Neural Engine مباشرة |
| النموذج المصدر | Keras وPyTorch (عبر ONNX) | يقبل TFLite وPyTorch (عبر أدوات تحويل) |
| النشر الحرّ | فتح المصدر بالكامل | مغلق المصدر |
القاعدة العمليّة:
- إن كنت تكتب تطبيقًا iOS فقط: Core ML خيار أفضل. Neural Engine يعمل عليه أفضل، والحجم الإضافيّ في الحزمة أصغر.
- إن كنت تكتب تطبيقًا موحَّدًا لـiOS وAndroid: TFLite. النموذج نفسه يعمل على المنصّتَين، فتحافظ على مصدر واحد. هذا خيارنا في الخيط الأحمر.
- إن كنت تريد أسرع أداء على iPhone: حوِّل نموذج TFLite إلى Core ML عبر
coremltools، واحصل على الاثنين معًا داخل التطبيق.