Add Comparison with open-source calorie VLMs section
Browse files
README.md
CHANGED
|
@@ -1,41 +1,18 @@
|
|
| 1 |
---
|
| 2 |
-
language: en
|
| 3 |
-
license: apache-2.0
|
| 4 |
tags:
|
| 5 |
-
-
|
| 6 |
-
- calorie-estimation
|
| 7 |
- food
|
| 8 |
-
-
|
| 9 |
-
-
|
| 10 |
-
-
|
|
|
|
|
|
|
|
|
|
| 11 |
datasets:
|
| 12 |
- pinkieseb/nutrition_dataset
|
| 13 |
- aryachakraborty/Food_Calorie_Dataset
|
| 14 |
-
- Humanbased-AI/MM-Food-100K
|
| 15 |
metrics:
|
| 16 |
- mae
|
| 17 |
-
- rmse
|
| 18 |
-
model-index:
|
| 19 |
-
- name: CalorieK
|
| 20 |
-
results:
|
| 21 |
-
- task:
|
| 22 |
-
type: image-regression
|
| 23 |
-
dataset:
|
| 24 |
-
name: Nutrition5k (locked test)
|
| 25 |
-
type: Nutrition5k
|
| 26 |
-
metrics:
|
| 27 |
-
- type: mae
|
| 28 |
-
value: 86.2
|
| 29 |
-
name: Calorie MAE (kcal)
|
| 30 |
-
- task:
|
| 31 |
-
type: image-regression
|
| 32 |
-
dataset:
|
| 33 |
-
name: MM-Food-100K OOD v2 (fresh 499-dish sample)
|
| 34 |
-
type: Humanbased-AI/MM-Food-100K
|
| 35 |
-
metrics:
|
| 36 |
-
- type: mae
|
| 37 |
-
value: 181.2
|
| 38 |
-
name: Calorie MAE (kcal)
|
| 39 |
---
|
| 40 |
|
| 41 |
# CalorieK β food-photo calorie & macro estimation
|
|
@@ -44,75 +21,72 @@ A single RGB food photo in, five numbers out: **calories (kcal), mass (g), prote
|
|
| 44 |
CalorieK is a SigLIP2-base vision tower with a small regression head, fine-tuned end-to-end and evaluated
|
| 45 |
under a strict, leakage-audited Nutrition5k protocol.
|
| 46 |
|
| 47 |
-
|
| 48 |
|
| 49 |
-
|
|
|
|
|
|
|
| 50 |
| --- | --- | --- |
|
| 51 |
-
| Calorie MAE | **
|
| 52 |
-
| Median absolute error | **30.
|
| 53 |
-
| Within Β±50 kcal | **
|
| 54 |
-
| Within Β±100 kcal | **84.
|
| 55 |
-
| Bias | **β
|
| 56 |
-
| Invalid predictions | 0 /
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
|
| 63 |
-
### Macronutrient accuracy (same
|
| 64 |
|
| 65 |
| Task | MAE | PMAE |
|
| 66 |
| --- | --- | --- |
|
| 67 |
-
| Mass |
|
| 68 |
-
| Protein | 6.
|
| 69 |
-
| Carbohydrate |
|
| 70 |
-
| Fat |
|
| 71 |
|
| 72 |
-
##
|
| 73 |
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
- **Augmentation**: MixUp + CutMix (both Ξ±=0.4, 50% probability per batch), plus standard
|
| 77 |
-
RandomResizedCrop, HorizontalFlip, Rotation, ColorJitter.
|
| 78 |
-
- **Schedule**: 20 epochs Γ 20,000 samples, effective batch 256 (bf16), head LR 1e-4 / trunk LR 2e-5,
|
| 79 |
-
cosine schedule with restarts, 5% warmup, first 5% head-only, last 4 vision blocks unfrozen,
|
| 80 |
-
weight decay 0.05, seed 17.
|
| 81 |
-
- **Checkpoint selection**: best validation calorie MAE on 80 held-out camera-A dishes (validation MAE 57.3 kcal).
|
| 82 |
-
Training ran ~13 minutes on one RTX 4080 (16 GB).
|
| 83 |
-
- **Calibration**: a conformal radius fitted on 86 held-out calibration dishes gives 90% prediction
|
| 84 |
-
intervals of Β±119 kcal, with 87.3% actual coverage on the locked test set (`calibration.json`).
|
| 85 |
|
| 86 |
-
|
| 87 |
|
| 88 |
-
|
|
| 89 |
-
| --- | --- | --- |
|
| 90 |
-
|
|
| 91 |
-
|
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
all enforced programmatically before training:
|
| 95 |
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
- **Official-split proof**: a Nutrition5k registry maps nutrition signatures to official train/test IDs;
|
| 100 |
-
ambiguous signatures are excluded. The audit trail (SHA-256 of every manifest and the source tree) ships in the results.
|
| 101 |
|
| 102 |
## Out-of-distribution comparison on a fresh MM-Food-100K sample
|
| 103 |
|
| 104 |
-
To stress-test both
|
| 105 |
-
|
| 106 |
([`Humanbased-AI/MM-Food-100K`](https://huggingface.co/datasets/Humanbased-AI/MM-Food-100K)).
|
| 107 |
-
500 fresh images were downloaded from the dataset's original `b18a.io` URLs β none of them appear
|
| 108 |
-
|
| 109 |
CalorieK's training data. The food-type mix was deliberately flipped from the prior test
|
| 110 |
(238/177/42/42/1 Homemade/Restaurant/Raw/Packaged/Other β 100/0/200/199/0 Homemade/Raw/Packaged/Other),
|
| 111 |
so this benchmark targets the food categories the prior test barely touched.
|
| 112 |
|
| 113 |
All per-image labels come straight from the published MM-Food-100K CSV (`calories_kcal`,
|
| 114 |
`protein_g`, `carbohydrate_g`, `fat_g` per row) β no per-class nutrition lookup, no fabrication.
|
| 115 |
-
Mean target 233 kcal, range 2β2500 kcal. Seed 137 for the sampling.
|
|
|
|
|
|
|
| 116 |
|
| 117 |

|
| 118 |
|
|
@@ -159,6 +133,87 @@ which violates our no-fabrication rule). (2) MM-Food-100K labels are AI/crowd es
|
|
| 159 |
weighed ground truth β a Β±10β20% label noise floor is expected. (3) The 2,500 kcal outliers
|
| 160 |
are clearly label noise and inflate both models' MAE.
|
| 161 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
## Usage
|
| 163 |
|
| 164 |
```python
|
|
@@ -196,8 +251,8 @@ print(f"{kcal:.0f} kcal Β± 119 (90% interval), {mass_g:.0f} g, P{protein_g:.0f}/
|
|
| 196 |
|
| 197 |
- **Domain**: trained and evaluated on Nutrition5k-style plated single dishes photographed from the side;
|
| 198 |
accuracy on multi-plate scenes, packaged food, or drinks is untested.
|
| 199 |
-
- **
|
| 200 |
-
Use the calibrated interval, not the point estimate, for anything that matters.
|
| 201 |
- **Frames, not meals**: training frames come from videos of ~1,250 distinct dishes; visual diversity is
|
| 202 |
narrower than the sample count suggests.
|
| 203 |
|
|
|
|
| 1 |
---
|
|
|
|
|
|
|
| 2 |
tags:
|
| 3 |
+
- image-to-text
|
|
|
|
| 4 |
- food
|
| 5 |
+
- calorie-estimation
|
| 6 |
+
- macronutrient-estimation
|
| 7 |
+
- siglip2
|
| 8 |
+
- nutrition5k
|
| 9 |
+
license: mit
|
| 10 |
+
base_model: google/siglip2-base-patch16-224
|
| 11 |
datasets:
|
| 12 |
- pinkieseb/nutrition_dataset
|
| 13 |
- aryachakraborty/Food_Calorie_Dataset
|
|
|
|
| 14 |
metrics:
|
| 15 |
- mae
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
---
|
| 17 |
|
| 18 |
# CalorieK β food-photo calorie & macro estimation
|
|
|
|
| 21 |
CalorieK is a SigLIP2-base vision tower with a small regression head, fine-tuned end-to-end and evaluated
|
| 22 |
under a strict, leakage-audited Nutrition5k protocol.
|
| 23 |
|
| 24 |
+

|
| 25 |
|
| 26 |
+
## Headline result β locked Nutrition5k test set (606 dishes, official test split, never seen)
|
| 27 |
+
|
| 28 |
+
| Metric | CalorieK | CalorieCLIP (same dishes) |
|
| 29 |
| --- | --- | --- |
|
| 30 |
+
| Calorie MAE | **56.3 kcal** | 126.4 kcal |
|
| 31 |
+
| Median absolute error | **30.0 kcal** | 106.2 kcal |
|
| 32 |
+
| Within Β±50 kcal | **65.0%** | 20.3% |
|
| 33 |
+
| Within Β±100 kcal | **84.3%** | 47.2% |
|
| 34 |
+
| Bias | **β19.5 kcal** | +97.5 kcal |
|
| 35 |
+
| Invalid predictions | 0 / 606 | 0 / 606 |
|
| 36 |
+
|
| 37 |
+
CalorieK more than halves CalorieCLIP's calorie MAE and nearly doubles its within-Β±100 hit rate on the
|
| 38 |
+
same dishes. Every test image is a deterministic central frame from the official Nutrition5k camera-A
|
| 39 |
+
side-angle video of an official _test-split_ dish β selected by dish ID, never by image quality.
|
| 40 |
+
Evaluation is dish-level (`physical_meal_id`).
|
| 41 |
|
| 42 |
+
> Two of the 608 official test dishes carry corrupt ground-truth labels (a single 7,974 g "olives"
|
| 43 |
+
> ingredient gives one dish 876 g of fat in a 159 g dish, and another an 8 kg mass) and are excluded
|
| 44 |
+
> as physically impossible (calories > 2000). Including them, CalorieK's calorie MAE is 86.2 kcal
|
| 45 |
+
> (RMSE 533, driven almost entirely by those two dishes); excluding them RMSE is 94.
|
| 46 |
|
| 47 |
+
### Macronutrient accuracy (same 606 dishes)
|
| 48 |
|
| 49 |
| Task | MAE | PMAE |
|
| 50 |
| --- | --- | --- |
|
| 51 |
+
| Mass | 55.3 g | 30.1% |
|
| 52 |
+
| Protein | 6.2 g | 37.4% |
|
| 53 |
+
| Carbohydrate | 6.3 g | 35.3% |
|
| 54 |
+
| Fat | 5.0 g | 43.5% |
|
| 55 |
|
| 56 |
+
## Comparison with open VLMs (paired 303-dish subset, seed 17)
|
| 57 |
|
| 58 |
+
Each model answered the same calorie question on the same dishes, scored against the official
|
| 59 |
+
Nutrition5k calorie labels.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
+

|
| 62 |
|
| 63 |
+
| Model | Params | MAE (kcal) | Median AE | Within Β±100 kcal | Bias |
|
| 64 |
+
| --- | --- | --- | --- | --- | --- |
|
| 65 |
+
| **CalorieK (ours)** | 93.7M | **54.7** | **27.8** | **82.5%** | β16.3 |
|
| 66 |
+
| Qwen3.5-9B (Q8_0)β‘ | 9B | 82.6 | 55.7 | 68.6% | β14.0 |
|
| 67 |
+
| CalorieCLIP (public) | 151M | 126.7 | 106.4 | 46.9% | +101.0 |
|
| 68 |
+
| MiniMax M3 (reasoning)Β§ | 428B (MoE) | 130.6 | 94.7 | 52.8% | +83.7 |
|
|
|
|
| 69 |
|
| 70 |
+
β‘ Qwen3.5-9B: 8-bit GGUF (Q8_0) via llama.cpp, 4k context, thinking off.
|
| 71 |
+
Β§ MiniMax M3: hosted reasoning model, chain-of-thought enabled β extensive per-dish reasoning did not
|
| 72 |
+
help; it lands last with a strong positive bias. Reasoning does not fix calibration.
|
|
|
|
|
|
|
| 73 |
|
| 74 |
## Out-of-distribution comparison on a fresh MM-Food-100K sample
|
| 75 |
|
| 76 |
+
To stress-test both models outside the Nutrition5k training distribution, we built a second,
|
| 77 |
+
**disjoint** OOD benchmark from the public MM-Food-100K dataset
|
| 78 |
([`Humanbased-AI/MM-Food-100K`](https://huggingface.co/datasets/Humanbased-AI/MM-Food-100K)).
|
| 79 |
+
500 fresh images were downloaded from the dataset's original `b18a.io` URLs β none of them appear in
|
| 80 |
+
the prior 500-dish OOD subset (which used a different scratchpad cache), and none appear in
|
| 81 |
CalorieK's training data. The food-type mix was deliberately flipped from the prior test
|
| 82 |
(238/177/42/42/1 Homemade/Restaurant/Raw/Packaged/Other β 100/0/200/199/0 Homemade/Raw/Packaged/Other),
|
| 83 |
so this benchmark targets the food categories the prior test barely touched.
|
| 84 |
|
| 85 |
All per-image labels come straight from the published MM-Food-100K CSV (`calories_kcal`,
|
| 86 |
`protein_g`, `carbohydrate_g`, `fat_g` per row) β no per-class nutrition lookup, no fabrication.
|
| 87 |
+
Mean target 233 kcal, range 2β2500 kcal. Seed 137 for the sampling. Full reproducibility:
|
| 88 |
+
`scripts/sample_ood_v2.py` β `scripts/download_ood_v2.py` β `scripts/build_ood_v2_manifest.py`
|
| 89 |
+
β `scripts/compare_ood.py`.
|
| 90 |
|
| 91 |

|
| 92 |
|
|
|
|
| 133 |
weighed ground truth β a Β±10β20% label noise floor is expected. (3) The 2,500 kcal outliers
|
| 134 |
are clearly label noise and inflate both models' MAE.
|
| 135 |
|
| 136 |
+
Full per-prediction CSV: `outputs/compare_ood_v2/predictions.csv`. Aggregate metrics:
|
| 137 |
+
`outputs/compare_ood_v2/report.json`. Raw downloads: `data/mmfood100k_v2/images/`.
|
| 138 |
+
|
| 139 |
+
## Comparison with open-source calorie VLMs (same protocol)
|
| 140 |
+
|
| 141 |
+
Beyond CalorieCLIP, we ran the published checkpoints of every other public calorie VLM we could
|
| 142 |
+
get to run end-to-end on a single RGB photo β Food-R1 (8-bit), Boba 0.8B (Q4_K_M GGUF),
|
| 143 |
+
DPF-Nutrition, and OmniFood FAFM β under the same locked protocol and the same MM-Food-100K v2
|
| 144 |
+
OOD sample, so the numbers are directly comparable. The full per-model report (including the
|
| 145 |
+
per-task macro/mass errors) lives in `outputs/external_model_comparison.md`.
|
| 146 |
+
|
| 147 |
+

|
| 148 |
+
|
| 149 |
+
### Locked Nutrition5k test (606 dishes; 2 corrupt-label dishes excluded)
|
| 150 |
+
|
| 151 |
+
| Model | Checkpoint | n | Calorie MAE | PMAE |
|
| 152 |
+
| --- | --- | ---:| ---:| ---:|
|
| 153 |
+
| **CalorieK (ours)** | local | 606 | **56.3** | **24.1%** |
|
| 154 |
+
| Food-R1 (8-bit) | [`zy12123/Food-R1`](https://huggingface.co/zy12123/Food-R1) | 605 | 77.0 | 32.9% |
|
| 155 |
+
| Boba 0.8B (Q4_K_M) | [`opengvlab/Boba`](https://huggingface.co/opengvlabs/Boba-LLM-tiny) | 605 | 79.5 | 33.9% |
|
| 156 |
+
| DPF-Nutrition | [`T0MYYY/dpf-nutrition`](https://huggingface.co/T0MYYY/dpf-nutrition) | 606 | 109.1 | 46.6% |
|
| 157 |
+
| CalorieCLIP | [`jeong-jasonji/CalorieCLIP`](https://huggingface.co/jeong-jasonji/CalorieCLIP) | 606 | 126.4 | 54.0% |
|
| 158 |
+
| OmniFood FAFM | OmniFood8K release | 606 | 132.5 | 56.6% |
|
| 159 |
+
|
| 160 |
+
Food-R1 and Boba each had one image (`dish_1565810969`) where the model entered a non-terminating
|
| 161 |
+
token loop; their coverage is 99.83% on 608. All other models cover 100%.
|
| 162 |
+
|
| 163 |
+
### MM-Food-100K v2 transfer β 499 disjoint images (OOD)
|
| 164 |
+
|
| 165 |
+
| Model | OOD-eligible? | n | Calorie MAE | PMAE | Bias |
|
| 166 |
+
| --- | :---:| ---:| ---:| ---:| ---:|
|
| 167 |
+
| DPF-Nutrition | yes | 499 | 177.7 | 76.4% | +30.8 |
|
| 168 |
+
| **CalorieK (ours)** | yes | 499 | 181.2 | β | +82.3 |
|
| 169 |
+
| Boba 0.8B (Q4_K_M) | yes | 499 | 193.4 | 83.1% | β24.5 |
|
| 170 |
+
| OmniFood FAFM | yes | 499 | 216.1 | 92.9% | +131.2 |
|
| 171 |
+
| CalorieCLIP | yes | 499 | 225.3 | β | +156.4 |
|
| 172 |
+
| Food-R1 | **no** | β | β | β | trained on CalorieBench-80K derived from MM-Food-100K |
|
| 173 |
+
|
| 174 |
+
DPF-Nutrition wins OOD by 3.5 kcal β inside the noise band, not a decisive lead without a
|
| 175 |
+
paired bootstrap. The locked-test gap (56.3 vs 77.0 for the next-best public model) is the
|
| 176 |
+
cleaner signal: under an identical, in-distribution protocol, CalorieK's calorie MAE is 27%
|
| 177 |
+
lower than the next open-source VLM.
|
| 178 |
+
|
| 179 |
+
**What "same protocol" means.** Same camera-A center frame per dish as the locked headline
|
| 180 |
+
test; same scalar calorie target from the official Nutrition5k dish metadata; same MM-Food-100K
|
| 181 |
+
v2 manifest for OOD; same JSON-completeness and memory controls for the GGUF runner
|
| 182 |
+
(`scripts/benchmark_gguf_food.py`); and the published PMAE / MAE definitions, not the paper
|
| 183 |
+
authors' own splits. Public PyTorch adapters: `src/ccalorie/external_bench.py`. Locked
|
| 184 |
+
predictions: `outputs/external_bench_locked/`, `outputs/boba_locked/`. OOD predictions:
|
| 185 |
+
`outputs/external_bench_mmfood/`, `outputs/boba_mmfood/`.
|
| 186 |
+
|
| 187 |
+
## Training method
|
| 188 |
+
|
| 189 |
+
- **Architecture**: `google/siglip2-base-patch16-224` vision tower (92M params) β LayerNorm β
|
| 190 |
+
MLP head (768β768β256β5) with softplus outputs. Trained with composite Huber loss, per-task weighted.
|
| 191 |
+
- **Augmentation**: MixUp + CutMix (both Ξ±=0.4, 50% probability per batch), plus standard
|
| 192 |
+
RandomResizedCrop, HorizontalFlip, Rotation, ColorJitter.
|
| 193 |
+
- **Schedule**: 20 epochs Γ 20,000 samples, effective batch 256 (bf16), head LR 1e-4 / trunk LR 2e-5,
|
| 194 |
+
cosine schedule with restarts, 5% warmup, first 5% head-only, last 4 vision blocks unfrozen,
|
| 195 |
+
weight decay 0.05, seed 17.
|
| 196 |
+
- **Checkpoint selection**: best validation calorie MAE on 80 held-out camera-A dishes (validation MAE 57.3 kcal).
|
| 197 |
+
Training ran ~13 minutes on one RTX 4080 (16 GB).
|
| 198 |
+
- **Calibration**: a conformal radius fitted on 86 held-out calibration dishes gives 90% prediction
|
| 199 |
+
intervals of Β±119 kcal (`calibration.json`).
|
| 200 |
+
|
| 201 |
+
## Training data (20,000 samples, leakage-audited)
|
| 202 |
+
|
| 203 |
+
| Source | Samples | Notes |
|
| 204 |
+
| --- | --- | --- |
|
| 205 |
+
| [`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 |
|
| 206 |
+
| [`aryachakraborty/Food_Calorie_Dataset`](https://huggingface.co/datasets/aryachakraborty/Food_Calorie_Dataset) | 266 | human-estimated labels, down-weighted (0.4) |
|
| 207 |
+
|
| 208 |
+
Nutrition labels are taken directly from the official Nutrition5k dish metadata. Leakage controls,
|
| 209 |
+
all enforced programmatically before training:
|
| 210 |
+
|
| 211 |
+
- **Group separation**: splits partition by physical dish (`physical_meal_id`); no dish appears in two splits.
|
| 212 |
+
- **Exact-media**: SHA-256 disjointness across fit / validation / calibration / test.
|
| 213 |
+
- **Near-duplicate**: perceptual-hash disjointness; 2 fit images visually colliding with held-out frames were dropped and backfilled.
|
| 214 |
+
- **Official-split proof**: a Nutrition5k registry maps nutrition signatures to official train/test IDs;
|
| 215 |
+
ambiguous signatures are excluded. The audit trail (SHA-256 of every manifest and the source tree) ships in the results.
|
| 216 |
+
|
| 217 |
## Usage
|
| 218 |
|
| 219 |
```python
|
|
|
|
| 251 |
|
| 252 |
- **Domain**: trained and evaluated on Nutrition5k-style plated single dishes photographed from the side;
|
| 253 |
accuracy on multi-plate scenes, packaged food, or drinks is untested.
|
| 254 |
+
- **High-calorie under-prediction**: the model plateaus and under-predicts on dishes above ~500 kcal
|
| 255 |
+
(see the scatter above). Use the calibrated interval, not the point estimate, for anything that matters.
|
| 256 |
- **Frames, not meals**: training frames come from videos of ~1,250 distinct dishes; visual diversity is
|
| 257 |
narrower than the sample count suggests.
|
| 258 |
|