انتقل إلى المحتوى الرئيسي

الوحدة 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:

المعيارTFLiteCore 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، واحصل على الاثنين معًا داخل التطبيق.

تحويل النموذج إلى Core ML (اختياريّ)

# على المكتب
import coremltools as ct

# تحويل من Keras (أنظف من TFLite→CoreML)
model = tf.keras.models.load_model("plantvillage_mobilenetv2.keras")

mlmodel = ct.convert(
model,
inputs=[ct.ImageType(shape=(1, 224, 224, 3), scale=1/127.5, bias=[-1, -1, -1])],
classifier_config=ct.ClassifierConfig("labels.txt"),
convert_to="mlprogram", # الصيغة الحديثة، أفضل من neuralnetwork
compute_units=ct.ComputeUnit.CPU_AND_NE, # يشتغل على Neural Engine
)

mlmodel.save("PlantLeaf.mlpackage")

على iPhone 13 مثلًا، الاستدلال يقفز من 18 مِلّي ثانية (TFLite + Core ML delegate) إلى 8 مِلّي ثانية (Core ML أصليّ). مكسب مضاعف تقريبًا.

أذون الكاميرا وسياسة الخصوصيّة

Info.plist يحتاج مفتاحًا صريحًا يشرح للمستخدم سبب طلب الكاميرا:

<key>NSCameraUsageDescription</key>
<string>يستعمل التطبيق الكاميرا لتحليل صور أوراق النباتات محلّيًّا على جهازك. لا تُرسَل أيّ صورة إلى خادم خارجيّ.</string>

بلا هذا المفتاح، iOS يُلغي التطبيق فورًا عند أوّل نداء للكاميرا (SIGABRT). الرسالة الشفّافة تُطمئن المستخدم وتُساعد على قبول متجر App Store.

App Store: تصنيف « ذكاء اصطناعيّ »

منذ 2024، App Store يطلب صراحة الإفصاح إن كان التطبيق يستعمل التعلّم الآليّ على الجهاز، ونوع البيانات المُعالَجة. عبِّئ حقل « App Privacy » بدقّة: نوع البيانات (صور)، الاستعمال (تحليل محلّيّ)، لا مشاركة مع طرف ثالث، لا تتبّع. أيّ تباين بين الإفصاح والسلوك يُطيل المراجعة.

الخلاصة

  • TFLite على iOS مسار قصير: CocoaPods مع TensorFlowLiteSwift، ونموذج .tflite نفسه من Android. المفسّر الخام هو الواجهة الرئيسيّة (لا مقابل مباشر لـTask Library).
  • المعالجة المسبقة تحتاج انتباهًا: UIImage → CGImage → Data مع طرح 128 لتحويل uint8 إلى int8. عمليّة &- بدل - تجنّبًا لتدفّق حسابيّ.
  • Core ML يفوز على iOS: إن كان التطبيق حصريًّا لـiPhone، حوِّل النموذج إلى .mlpackage واستعمل Core ML مباشرة. Neural Engine يعمل عليه أفضل من مفوَّض TFLite.
  • الأذون والخصوصيّة صريحة: NSCameraUsageDescription بلغة واضحة، وحقل « App Privacy » في متجر App Store يعكس الاستعمال الفعليّ. الشفافيّة تُسرِّع المراجعة وتبني ثقة المستخدم.

الوحدة التالية تنتقل إلى قياس زمن الاستجابة واستهلاك الطاقة على ثلاث فئات من الهواتف، لنُبرهن — بأرقام لا بادّعاءات — أنّ الميزانيّة المُعلَنة في الوحدة 1 قد احتُرمَت.