AI-Розпізнавання їжі та підрахунок калорій за фотографією у мобільних застосунках
Фудтрекинг через фото—одна з найтехнічно насичених «простих» задач у мобільній AI. Користувач очікує: сфотографував борщ—отримав КБЖУ. На деле між цими двома точками стоїть ланцюжок: розпізнавання блюда, ідентифікація інгредієнтів, оцінка порції, пошук у нутриційній базі. Кожне звено додає похибку.
Технічна ланцюжок розпізнавання
Правильна архітектура—не «одна модель на всі», а конвеєр із кількох спеціалізованих кроків.
Крок 1: детекція й класифікація блюда. CoreML на iOS (модель EfficientDet або YOLOv8 класифікації), TFLite на Android. Для MVP—облачне API: Clarifai Food Model, Google Cloud Vision із food-тегами або спеціалізований Logmeal API.
Крок 2: оцінка порції. Складніше. Без reference object у кадрі (монета, рука, стандартна тарілка) оцінити граммовку майже неможливо. Два практичні рішення: попросити користувача вказати тип тари (тарілка 20см, склянка 200мл) або використовувати ARKit/ARCore для depth estimation. Depth estimation через ARKit дає прийнятні результати для об'ємних блюд—похибка 15–25%, краще, ніж ручний ввід у користувачів (вони зазвичай занижають порцію).
Крок 3: пошук нутриційних даних. USDA FoodData Central—безплатне API з 700 000+ продуктів. Open Food Facts—open-source база, хороша для упакованих продуктів. Для російського ринку критично мати вітчизняні блюда: борщ, пельмені, олів'є—їх немає в USDA в звичному форматі.
// iOS: повний конвеєр розпізнавання
struct FoodRecognitionPipeline {
func analyze(image: UIImage, portionContext: PortionContext?) async throws -> MealAnalysis {
// 1. Розпізнавання блюда через Logmeal API
let foodItems = try await logmealClient.recognizeFood(image: image)
// 2. Оцінка порції
let portionEstimates: [PortionEstimate]
if let context = portionContext {
portionEstimates = estimatePortionFromContext(foodItems, context: context)
} else {
portionEstimates = try await estimatePortionWithAR(image: image)
}
// 3. Нутриційні дані
let nutritionData = try await withThrowingTaskGroup(of: NutritionResult.self) { group in
for (item, portion) in zip(foodItems, portionEstimates) {
group.addTask {
try await self.fetchNutrition(food: item, grams: portion.estimatedGrams)
}
}
return try await group.reduce(into: []) { $0.append($1) }
}
return MealAnalysis(
items: foodItems,
portions: portionEstimates,
nutrition: nutritionData.aggregate(),
confidence: foodItems.map(\.confidence).min() ?? 0
)
}
}
Паралельні запити нутриційних даних через TaskGroup важливі: при 3 блюдах послідовні запити дають 3× затримку.
Складні блюда—головна складність
Борщ на фото—свіжа капуста, морква, картофель, м'ясо, сметана в невідомих пропорціях. Два варіанти рішення:
Рецептна база. LLM або кастомна модель розбиває блюдо на інгредієнти за рецептом. Працює для стандартних блюд, погано—для домашної кухні з варіаціями.
Користувацька корректировка. Після автоматичного розпізнавання користувач бачить передбачуваний склад і може убрати або додати інгредієнти. Swipe-to-remove на інгредієнті, slider для граммовки. Це принципово кращий UX, ніж «точність 98%» без можливості редагування.
// Android: UI складу блюда з редагуванням
@Composable
fun MealCompositionEditor(
items: List<FoodItem>,
onItemRemoved: (FoodItem) -> Unit,
onPortionChanged: (FoodItem, Float) -> Unit
) {
LazyColumn {
items(items, key = { it.id }) { item ->
SwipeToDismiss(
state = rememberDismissState { if (it == DismissValue.DismissedToStart) {
onItemRemoved(item); true } else false
},
background = { DeleteBackground() },
dismissContent = {
FoodItemRow(
item = item,
onPortionChange = { grams -> onPortionChanged(item, grams) }
)
}
)
}
}
}
Інтеграція HealthKit і Health Connect
Записаний приймання їжі повинно попадати в екосистему здоров'я.
// iOS: запис у HealthKit
func logMealToHealthKit(_ meal: MealAnalysis) async throws {
let store = HKHealthStore()
let caloriesType = HKQuantityType(.dietaryEnergyConsumed)
let proteinType = HKQuantityType(.dietaryProtein)
let carbsType = HKQuantityType(.dietaryCarbohydrates)
let fatType = HKQuantityType(.dietaryFatTotal)
let metadata: [String: Any] = [
HKMetadataKeyFoodType: meal.primaryItem?.name ?? "Mixed Meal"
]
let samples = [
HKQuantitySample(type: caloriesType,
quantity: .init(unit: .kilocalorie(), doubleValue: meal.nutrition.calories),
start: .now, end: .now, metadata: metadata),
HKQuantitySample(type: proteinType,
quantity: .init(unit: .gram(), doubleValue: meal.nutrition.protein),
start: .now, end: .now)
// + вуглеводи, жири
]
try await store.save(samples)
}
Запрошуйте дозволи завчасно через HKHealthStore.requestAuthorization. Розповсюджена помилка—запрошувати дозволи при першому відкритті застосунку, до того як користувач побачив цінність. Apple не заборонює, але конверсія значно вища, якщо запит з'являється в момент першої записи їжі.
Бар'єри точності й чесне показування
Навіть хороша модель помиляється на нестандартних блюдах, поганому освітленні й незвичних ракурсах. Ховати невпевненість—помилка. Показувати confidence score поруч із результатом—правильно:
struct NutritionDisplayView: View {
let analysis: MealAnalysis
var body: some View {
VStack(alignment: .leading, spacing: 12) {
if analysis.confidence < 0.6 {
ConfidenceWarningBanner(
message: "Низька впевненість у розпізнаванні. Перевірте склад блюда."
)
}
CalorieSummaryCard(nutrition: analysis.nutrition)
MacroBreakdownChart(nutrition: analysis.nutrition)
IngredientList(items: analysis.items, editable: true)
}
}
}
Орієнтири за часом
Базова інтеграція (один API розпізнавання + USDA нутриційна база + простий UI)—1–2 тижні. Повна реалізація з оцінкою порції через AR, складними блюдами, користувацькою корректировкою, HealthKit/Health Connect, історією харчування й денними нормами КБЖУ—1–2 місяці.







