Реализация AI-классификации фотографий в галерее мобильного приложения
Классификация фотографий в галерее — это одна из немногих AI-задач, где on-device обработка является стандартом, а не исключением. Apple Photos, Google Photos — оба используют on-device ML. Загружать личные фото пользователей на сервер ради классификации — это технически избыточно и неправильно с точки зрения приватности.
On-device классификация: что доступно из коробки
На iOS работает Vision framework с VNClassifyImageRequest. Не нужны сторонние модели — встроенная классификация покрывает 1000+ категорий:
import Vision
func classifyPhoto(cgImage: CGImage, completion: @escaping ([String]) -> Void) {
let request = VNClassifyImageRequest { request, error in
guard let results = request.results as? [VNClassificationObservation] else { return }
// Берём категории с confidence > 0.5
let labels = results
.filter { $0.confidence > 0.5 }
.map { $0.identifier }
completion(labels)
}
try? VNImageRequestHandler(cgImage: cgImage, options: [:]).perform([request])
}
Время инференса — 5–15 ms на фото в зависимости от устройства. На iPhone 13+ Neural Engine обрабатывает всю галерею в 1000 фото за ~15–20 секунд фоновой задачи.
На Android: ML Kit ImageLabeler с ImageLabelerOptions:
val labeler = ImageLabeling.getClient(
ImageLabelerOptions.Builder()
.setConfidenceThreshold(0.5f)
.build()
)
labeler.process(InputImage.fromBitmap(bitmap, 0))
.addOnSuccessListener { labels ->
val categories = labels.map { it.text }
// "Dog", "Outdoor", "Sky", "Food", etc.
}
ML Kit поддерживает 400+ категорий на устройстве без сети.
Обработка всей галереи: PHFetchResult и батчинг
Проблема: галерея может содержать 50 000+ фотографий. Обходить их все сразу — заблокируем main thread и убьём батарею.
Правильный подход: PHFetchResult + инкрементальная обработка через DispatchQueue.global(qos: .background):
func classifyGallery() {
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
let allPhotos = PHAsset.fetchAssets(with: .image, options: fetchOptions)
let batchSize = 50
let processingQueue = DispatchQueue(label: "photo.classification", qos: .background)
processingQueue.async {
var offset = 0
while offset < allPhotos.count {
let batch = (offset..<min(offset + batchSize, allPhotos.count))
.map { allPhotos.object(at: $0) }
self.processBatch(assets: batch)
offset += batchSize
Thread.sleep(forTimeInterval: 0.1) // Даём системе отдышаться
}
}
}
Thread.sleep(0.1) между батчами критичен — без него CPU throttle через 2–3 минуты и скорость падает в 3–5 раз.
Хранение результатов классификации
Результаты сохраняем локально — не на сервер. Core Data с NSPersistentContainer:
// Entity: PhotoClassification
// Attributes: assetLocalIdentifier (String), labels (Transformable: [String]), classifiedAt (Date)
Индекс на assetLocalIdentifier + индекс на labels для быстрого поиска. При 50k фото таблица весит ~5–10 MB.
При открытии галереи — подтягиваем классификации из локальной БД, отображаем. Новые фото (добавленные после последней сессии) — классифицируем инкрементально через PHPhotoLibraryChangeObserver.
Кастомные категории через CoreML
Встроенный Vision не всегда покрывает нужные категории. Для кастомных (например, «рецепт», «скриншот документа», «чек», «виза») — обучаем CreateML модель:
// CreateML: 5–10 примеров на категорию достаточно для базовой точности
let dataSource = MLImageClassifier.DataSource.labeledDirectories(at: trainingDir)
let model = try MLImageClassifier(trainingData: dataSource)
try model.write(to: modelURL)
Точность на кастомных категориях при 20+ примерах — 85–95%. Модель в CoreML формате — 5–15 MB. Можно доставлять через Core ML Model Deployment без обновления приложения.
Типичные ошибки
Classifying на main thread — самое частое. VNImageRequestHandler.perform выполняется синхронно. Всегда в background queue.
Запрашивать full-resolution для классификации — лишнее. PHAsset requestImage с targetSize: CGSize(width: 224, height: 224) достаточно — это стандартный input для большинства моделей классификации.
Не обновлять классификации при изменении галереи — PHPhotoLibraryChangeObserver должен запускать инкрементальную обработку только для новых/изменённых фото.
Сроки
Базовая классификация с Vision + хранение в Core Data — 4–6 дней. Полная реализация с кастомными категориями, инкрементальными обновлениями и быстрым поиском по тегам — 2–3 недели. Стоимость рассчитывается индивидуально.







