Upload README.md with huggingface_hub
Browse files
README.md
CHANGED
|
@@ -1,53 +1,44 @@
|
|
| 1 |
-
---
|
| 2 |
-
license: apache-2.0
|
| 3 |
-
library_name: pytorch
|
| 4 |
-
base_model: google/siglip2-base-patch16-224
|
| 5 |
-
tags:
|
| 6 |
-
- nutrition-estimation
|
| 7 |
-
- calorie-estimation
|
| 8 |
-
- food
|
| 9 |
-
- nutrition5k
|
| 10 |
-
- image-regression
|
| 11 |
-
datasets:
|
| 12 |
-
- Nutrition5k
|
| 13 |
-
language:
|
| 14 |
-
- en
|
| 15 |
-
metrics:
|
| 16 |
-
- mae
|
| 17 |
-
---
|
| 18 |
-
|
| 19 |
# CalorieK — food-photo calorie & macro estimation
|
| 20 |
|
| 21 |
A single RGB food photo in, five numbers out: **calories (kcal), mass (g), protein (g), carbohydrate (g), fat (g)**.
|
| 22 |
CalorieK is a SigLIP2-base vision tower with a small regression head, fine-tuned end-to-end and evaluated
|
| 23 |
under a strict, leakage-audited Nutrition5k protocol.
|
| 24 |
|
| 25 |
-

|
| 26 |
|
| 27 |
## Headline result — locked Nutrition5k test set (608 dishes, official test split, never seen)
|
| 28 |
|
| 29 |
| Metric | CalorieK | CalorieCLIP (same 608 dishes) |
|
| 30 |
-
|---|---|---|
|
| 31 |
-
| Calorie MAE | **
|
| 32 |
-
| Median absolute error | **
|
| 33 |
-
| Within ±50 kcal | **
|
| 34 |
-
| Within ±100 kcal | **
|
| 35 |
-
| Bias | **−
|
| 36 |
| Calibrated 90% interval | **±105.9 kcal** (actual coverage 93.6%) | — |
|
| 37 |
| Invalid predictions | 0 / 608 | 0 / 608 |
|
| 38 |
|
| 39 |
Every test image is a deterministic central frame from the official Nutrition5k camera-A side-angle
|
| 40 |
-
video of an official
|
| 41 |
dish-level (`physical_meal_id`).
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
## Comparison with open VLMs (paired 304-dish subset, seed 17)
|
| 44 |
|
| 45 |
Each model answered the same calorie question on the same dishes
|
| 46 |
-
(prompt:
|
| 47 |
|
| 48 |
| Model | n | MAE (kcal) | Median AE | Within ±100 kcal | Bias |
|
| 49 |
-
|---|---|---|---|---|---|
|
| 50 |
-
| **CalorieK (ours)** | 304 | **
|
| 51 |
| Qwen3.5-2B | 304 | 116.4 | 65.0 | 69.4% | −48.1 |
|
| 52 |
| Qwen3.5-9B (Q8_0, thinking off) ‡ | 304 | 117.2 | 64.5 | 65.8% | +6.1 |
|
| 53 |
| MiniCPM-V 4.6 (1.3B) | 304 | 163.3 | 97.0 | 52.6% | +75.5 |
|
|
@@ -58,8 +49,6 @@ Each model answered the same calorie question on the same dishes
|
|
| 58 |
†Gemma-4-12B was scored on a seeded 100-dish paired subset of the same 304 dishes (compute budget);
|
| 59 |
all parse rates were 100%.
|
| 60 |
‡ Qwen3.5-9B ran as an 8-bit GGUF (Q8_0) via llama.cpp with 4k context and thinking disabled.
|
| 61 |
-
Notably it matches the 2B (117.2 vs 116.4 MAE) — 4.5× more parameters bought no accuracy on this task,
|
| 62 |
-
suggesting the zero-shot VLM error floor is portion-mass ambiguity, not model capacity.
|
| 63 |
MiniMax M3 (a hosted reasoning model, chain-of-thought enabled) reinforces the point from the other
|
| 64 |
direction: extensive per-dish reasoning did not help — it lands second-to-last with a strong +104 kcal
|
| 65 |
systematic overestimate. Reasoning does not fix calibration.
|
|
@@ -67,53 +56,58 @@ systematic overestimate. Reasoning does not fix calibration.
|
|
| 67 |
## Out-of-distribution evaluation (MM-Food-100K, 500 wild food photos)
|
| 68 |
|
| 69 |
To measure generalization beyond Nutrition5k plating, both regression models were scored on a seeded
|
| 70 |
-
500-image sample of [MM-Food-100K](https://huggingface.co/datasets/
|
| 71 |
-
(restaurant/home photos; `food_prob ≥ 0.9`). **Caveat: these labels are crowd/AI estimates, not weighed
|
| 72 |
-
measurements** — absolute errors include label noise no model can beat, so the relative comparison is
|
| 73 |
the meaningful signal.
|
| 74 |
|
| 75 |
-
|
|
| 76 |
-
|---|---|---|
|
| 77 |
-
| MAE | **
|
| 78 |
-
| Median error | **
|
| 79 |
-
| Within ±100 kcal | **
|
| 80 |
-
| Bias | **−
|
| 81 |
-
| R² | **+0.24** | −0.08 |
|
| 82 |
|
| 83 |
Findings, stated honestly:
|
|
|
|
| 84 |
- CalorieK beats CalorieCLIP out-of-distribution on every metric, keeps **near-zero bias** on food it
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
- But CalorieK degrades ~3× versus in-distribution (
|
| 88 |
-
|
| 89 |
-
|
| 90 |
|
| 91 |
-
## Training method
|
| 92 |
|
| 93 |
- **Architecture**: `google/siglip2-base-patch16-224` vision tower (92M params) → LayerNorm →
|
| 94 |
-
|
| 95 |
-
- **
|
| 96 |
-
|
| 97 |
-
- **
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
| 99 |
- **Calibration**: a conformal radius fitted on 86 held-out calibration dishes gives 90% prediction
|
| 100 |
-
|
| 101 |
|
| 102 |
-

|
| 103 |
|
| 104 |
## Training data (20,000 samples, leakage-audited)
|
| 105 |
|
| 106 |
| Source | Samples | Notes |
|
| 107 |
-
|---|---|---|
|
| 108 |
| [`pinkieseb/nutrition_dataset`](https://huggingface.co/datasets/pinkieseb/nutrition_dataset) (Nutrition5k video frames) | 19,734 | only dishes provably in the **official Nutrition5k train split** (matched by exact nutrition signature), ≤16 frames per dish |
|
| 109 |
| [`aryachakraborty/Food_Calorie_Dataset`](https://huggingface.co/datasets/aryachakraborty/Food_Calorie_Dataset) | 266 | human-estimated labels, down-weighted (0.4) |
|
| 110 |
|
| 111 |
Leakage controls, all enforced programmatically before training:
|
|
|
|
| 112 |
- **Group separation**: splits partition by physical dish (`physical_meal_id`); no dish appears in two splits.
|
| 113 |
- **Exact-media**: SHA-256 disjointness across fit / validation / calibration / test.
|
| 114 |
- **Near-duplicate**: perceptual-hash disjointness; 2 fit images visually colliding with held-out frames were dropped and backfilled.
|
| 115 |
- **Official-split proof**: a Nutrition5k registry maps nutrition signatures to official train/test IDs;
|
| 116 |
-
|
| 117 |
|
| 118 |
## Usage
|
| 119 |
|
|
@@ -151,9 +145,20 @@ print(f"{kcal:.0f} kcal ± 106 (90% interval), {mass_g:.0f} g, P{protein_g:.0f}/
|
|
| 151 |
## Limitations
|
| 152 |
|
| 153 |
- **Domain**: trained and evaluated on Nutrition5k-style plated single dishes photographed from the side;
|
| 154 |
-
|
| 155 |
-
- **Heavy tail**: RMSE (
|
| 156 |
-
|
| 157 |
- **Frames, not meals**: training frames come from videos of ~1,250 distinct dishes; visual diversity is
|
| 158 |
-
|
| 159 |
- The VLM baselines answered a zero-shot prompt; instruction-tuned prompting or few-shot could improve them.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# CalorieK — food-photo calorie & macro estimation
|
| 2 |
|
| 3 |
A single RGB food photo in, five numbers out: **calories (kcal), mass (g), protein (g), carbohydrate (g), fat (g)**.
|
| 4 |
CalorieK is a SigLIP2-base vision tower with a small regression head, fine-tuned end-to-end and evaluated
|
| 5 |
under a strict, leakage-audited Nutrition5k protocol.
|
| 6 |
|
| 7 |
+
[](https://huggingface.co/Dralkh/CalorieK/blob/main/comparison.png)
|
| 8 |
|
| 9 |
## Headline result — locked Nutrition5k test set (608 dishes, official test split, never seen)
|
| 10 |
|
| 11 |
| Metric | CalorieK | CalorieCLIP (same 608 dishes) |
|
| 12 |
+
| --- | --- | --- |
|
| 13 |
+
| Calorie MAE | **49.8 kcal** (95% CI 34.4–78.0) | 181.2 kcal |
|
| 14 |
+
| Median absolute error | **24.5 kcal** | 137.0 kcal |
|
| 15 |
+
| Within ±50 kcal | **78.5%** | 21.4% |
|
| 16 |
+
| Within ±100 kcal | **94.2%** | 40.6% |
|
| 17 |
+
| Bias | **−6.6 kcal** | +134.8 kcal |
|
| 18 |
| Calibrated 90% interval | **±105.9 kcal** (actual coverage 93.6%) | — |
|
| 19 |
| Invalid predictions | 0 / 608 | 0 / 608 |
|
| 20 |
|
| 21 |
Every test image is a deterministic central frame from the official Nutrition5k camera-A side-angle
|
| 22 |
+
video of an official _test-split_ dish — selected by dish ID, never by image quality. Evaluation is
|
| 23 |
dish-level (`physical_meal_id`).
|
| 24 |
|
| 25 |
+
### Macronutrient accuracy (same 608 dishes)
|
| 26 |
+
|
| 27 |
+
| Task | MAE | Median AE | Bias |
|
| 28 |
+
| --- | --- | --- | --- |
|
| 29 |
+
| Mass | **90.4 g** | 34.5 g | −47.3 g |
|
| 30 |
+
| Protein | **7.8 g** | 3.9 g | −1.9 g |
|
| 31 |
+
| Carbohydrate | **5.8 g** | 2.8 g | −1.2 g |
|
| 32 |
+
| Fat | **7.2 g** | 2.2 g | −3.1 g |
|
| 33 |
+
|
| 34 |
## Comparison with open VLMs (paired 304-dish subset, seed 17)
|
| 35 |
|
| 36 |
Each model answered the same calorie question on the same dishes
|
| 37 |
+
(prompt: _"Estimate the total calories in this food image. Return exactly one integer number in kcal…"_).
|
| 38 |
|
| 39 |
| Model | n | MAE (kcal) | Median AE | Within ±100 kcal | Bias |
|
| 40 |
+
| --- | --- | --- | --- | --- | --- |
|
| 41 |
+
| **CalorieK (ours)** | 304 | **62.7** | **24.0** | **94.1%** | −11.0 |
|
| 42 |
| Qwen3.5-2B | 304 | 116.4 | 65.0 | 69.4% | −48.1 |
|
| 43 |
| Qwen3.5-9B (Q8_0, thinking off) ‡ | 304 | 117.2 | 64.5 | 65.8% | +6.1 |
|
| 44 |
| MiniCPM-V 4.6 (1.3B) | 304 | 163.3 | 97.0 | 52.6% | +75.5 |
|
|
|
|
| 49 |
†Gemma-4-12B was scored on a seeded 100-dish paired subset of the same 304 dishes (compute budget);
|
| 50 |
all parse rates were 100%.
|
| 51 |
‡ Qwen3.5-9B ran as an 8-bit GGUF (Q8_0) via llama.cpp with 4k context and thinking disabled.
|
|
|
|
|
|
|
| 52 |
MiniMax M3 (a hosted reasoning model, chain-of-thought enabled) reinforces the point from the other
|
| 53 |
direction: extensive per-dish reasoning did not help — it lands second-to-last with a strong +104 kcal
|
| 54 |
systematic overestimate. Reasoning does not fix calibration.
|
|
|
|
| 56 |
## Out-of-distribution evaluation (MM-Food-100K, 500 wild food photos)
|
| 57 |
|
| 58 |
To measure generalization beyond Nutrition5k plating, both regression models were scored on a seeded
|
| 59 |
+
500-image sample of [MM-Food-100K](https://huggingface.co/datasets/Codatta/MM-Food-100K)
|
| 60 |
+
(restaurant/home photos; `food_prob ≥ 0.9`). **Caveat: these labels are crowd/AI estimates, not weighed**
|
| 61 |
+
**measurements** — absolute errors include label noise no model can beat, so the relative comparison is
|
| 62 |
the meaningful signal.
|
| 63 |
|
| 64 |
+
| | CalorieK | CalorieCLIP |
|
| 65 |
+
| --- | --- | --- |
|
| 66 |
+
| MAE | **162.5 kcal** | 206.4 kcal |
|
| 67 |
+
| Median error | **135.0 kcal** | 179.9 kcal |
|
| 68 |
+
| Within ±100 kcal | **38.8%** | 25.6% |
|
| 69 |
+
| Bias | **−8.1 kcal** | +98.3 kcal |
|
| 70 |
+
| R² | **+0.24** | −0.08 |
|
| 71 |
|
| 72 |
Findings, stated honestly:
|
| 73 |
+
|
| 74 |
- CalorieK beats CalorieCLIP out-of-distribution on every metric, keeps **near-zero bias** on food it
|
| 75 |
+
has never seen, and retains real signal (R² +0.24) where CalorieCLIP is worse than predicting the
|
| 76 |
+
mean (R² −0.08).
|
| 77 |
+
- But CalorieK degrades ~3× versus in-distribution (49.8 → 162.5 kcal MAE): it is substantially a
|
| 78 |
+
specialist in Nutrition5k-style plated dishes. For wild-photo deployment, mixing diverse weakly
|
| 79 |
+
labeled data into training is the indicated next step.
|
| 80 |
|
| 81 |
+
## Training method (improved)
|
| 82 |
|
| 83 |
- **Architecture**: `google/siglip2-base-patch16-224` vision tower (92M params) → LayerNorm →
|
| 84 |
+
MLP head (768→768→256→5) with softplus outputs. Trained with composite Huber loss, per-task weighted.
|
| 85 |
+
- **Augmentation**: MixUp + CutMix (both α=0.4, 50% probability per batch), plus standard
|
| 86 |
+
RandomResizedCrop, HorizontalFlip, Rotation, ColorJitter.
|
| 87 |
+
- **Schedule**: 20 epochs × 20,000 samples, effective batch 256 (bf16), head LR 1e-4 / trunk LR 2e-5,
|
| 88 |
+
cosine schedule with restarts, 5% warmup, first 5% head-only, last 4 vision blocks unfrozen,
|
| 89 |
+
weight decay 0.05, seed 17.
|
| 90 |
+
- **Checkpoint selection**: best validation MAE on 80 held-out camera-A dishes (validation MAE 39.3 kcal).
|
| 91 |
+
Training ran ~12 minutes on one RTX 4080 (16 GB).
|
| 92 |
- **Calibration**: a conformal radius fitted on 86 held-out calibration dishes gives 90% prediction
|
| 93 |
+
intervals of ±105.9 kcal, with 93.6% actual coverage on the locked test set (`calibration.json`).
|
| 94 |
|
| 95 |
+
[](https://huggingface.co/Dralkh/CalorieK/blob/main/loss_curve.png)
|
| 96 |
|
| 97 |
## Training data (20,000 samples, leakage-audited)
|
| 98 |
|
| 99 |
| Source | Samples | Notes |
|
| 100 |
+
| --- | --- | --- |
|
| 101 |
| [`pinkieseb/nutrition_dataset`](https://huggingface.co/datasets/pinkieseb/nutrition_dataset) (Nutrition5k video frames) | 19,734 | only dishes provably in the **official Nutrition5k train split** (matched by exact nutrition signature), ≤16 frames per dish |
|
| 102 |
| [`aryachakraborty/Food_Calorie_Dataset`](https://huggingface.co/datasets/aryachakraborty/Food_Calorie_Dataset) | 266 | human-estimated labels, down-weighted (0.4) |
|
| 103 |
|
| 104 |
Leakage controls, all enforced programmatically before training:
|
| 105 |
+
|
| 106 |
- **Group separation**: splits partition by physical dish (`physical_meal_id`); no dish appears in two splits.
|
| 107 |
- **Exact-media**: SHA-256 disjointness across fit / validation / calibration / test.
|
| 108 |
- **Near-duplicate**: perceptual-hash disjointness; 2 fit images visually colliding with held-out frames were dropped and backfilled.
|
| 109 |
- **Official-split proof**: a Nutrition5k registry maps nutrition signatures to official train/test IDs;
|
| 110 |
+
ambiguous signatures are excluded. The audit trail (SHA-256 of every manifest and the source tree) ships in the results.
|
| 111 |
|
| 112 |
## Usage
|
| 113 |
|
|
|
|
| 145 |
## Limitations
|
| 146 |
|
| 147 |
- **Domain**: trained and evaluated on Nutrition5k-style plated single dishes photographed from the side;
|
| 148 |
+
accuracy on multi-plate scenes, packaged food, or drinks is untested.
|
| 149 |
+
- **Heavy tail**: RMSE (326) is far above MAE (50) — very high-calorie dishes are occasionally missed badly.
|
| 150 |
+
Use the calibrated interval, not the point estimate, for anything that matters.
|
| 151 |
- **Frames, not meals**: training frames come from videos of ~1,250 distinct dishes; visual diversity is
|
| 152 |
+
narrower than the sample count suggests.
|
| 153 |
- The VLM baselines answered a zero-shot prompt; instruction-tuned prompting or few-shot could improve them.
|
| 154 |
+
|
| 155 |
+
## Citation
|
| 156 |
+
|
| 157 |
+
```bibtex
|
| 158 |
+
@misc{caloriek2025,
|
| 159 |
+
title={CalorieK: Accurate Food Calorie Estimation from a Single RGB Image},
|
| 160 |
+
author={Dralkh},
|
| 161 |
+
year={2025},
|
| 162 |
+
url={https://huggingface.co/Dralkh/CalorieK}
|
| 163 |
+
}
|
| 164 |
+
```
|