Реализация AI-распознавания еды и подсчёта калорий по фотографии в мобильном приложении
Фудтрекинг через фото — одна из самых технически насыщенных «простых» задач в мобильной AI. Пользователь ожидает: сфотографировал борщ — получил КБЖУ. На деле между этими двумя точками стоит целая цепочка: распознавание блюда, идентификация ингредиентов, оценка порции, поиск в нутриционной базе. Каждое звено добавляет погрешность.
Техническая цепочка распознавания
Правильная архитектура — не «одна модель на всё», а pipeline из нескольких специализированных шагов.
Шаг 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: полный pipeline распознавания
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)
// + carbs, fat
]
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 месяца.







