SDK-Streamlit / scripts /estimate_macros_from_segments.py
Gilgarmesh's picture
Upload 22 files
e0e2b27 verified
Raw
History Blame Contribute Delete
4.72 kB
"""Estimate meal macros from segmented area fractions and a macro lookup CSV.
This is the nutrition-side glue for the project:
1. The segmentation model predicts visible area by class.
2. Portion assumptions convert relative area to approximate grams.
3. A USDA-derived macro table converts grams to calories/macros.
Example:
python scripts/estimate_macros_from_segments.py --segments data/nutrition/sample_segments.json
"""
from __future__ import annotations
import argparse
import csv
import json
from pathlib import Path
from typing import Any
import yaml
PROJECT_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_SEGMENTS = PROJECT_ROOT / "data" / "nutrition" / "sample_segments.json"
DEFAULT_MACROS = PROJECT_ROOT / "data" / "nutrition" / "macros_per_100g.csv"
DEFAULT_PORTIONS = PROJECT_ROOT / "configs" / "portion_assumptions.yaml"
MACRO_FIELDS = ("kcal", "protein", "fat", "carbs")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--segments", type=Path, default=DEFAULT_SEGMENTS)
parser.add_argument("--macros", type=Path, default=DEFAULT_MACROS)
parser.add_argument("--portions", type=Path, default=DEFAULT_PORTIONS)
parser.add_argument("--output", type=Path)
return parser.parse_args()
def load_segments(path: Path) -> list[dict[str, Any]]:
payload = json.loads(path.read_text(encoding="utf-8"))
if isinstance(payload, list):
return payload
if isinstance(payload, dict) and "segments" in payload:
return payload["segments"]
if isinstance(payload, dict):
return [
{"class_name": class_name, "area_fraction": area_fraction}
for class_name, area_fraction in payload.items()
]
raise ValueError(f"Unsupported segment JSON shape: {path}")
def load_macro_table(path: Path) -> dict[str, dict[str, float]]:
with path.open(newline="", encoding="utf-8") as f:
rows = list(csv.DictReader(f))
table = {}
for row in rows:
class_name = row["class_name"]
table[class_name] = {
field: float(row[field]) if row.get(field) not in (None, "") else 0.0
for field in MACRO_FIELDS
}
return table
def load_portions(path: Path) -> dict[str, Any]:
return yaml.safe_load(path.read_text(encoding="utf-8"))
def estimate_grams(
segments: list[dict[str, Any]],
portions: dict[str, Any],
) -> dict[str, float]:
total_plate_grams = float(portions["total_plate_grams"])
class_config = portions.get("classes", {})
weighted_areas = {}
for segment in segments:
class_name = str(segment["class_name"])
area_fraction = float(segment["area_fraction"])
density_weight = float(class_config.get(class_name, {}).get("density_weight", 1.0))
weighted_areas[class_name] = weighted_areas.get(class_name, 0.0) + area_fraction * density_weight
total_weighted_area = sum(weighted_areas.values())
if total_weighted_area <= 0:
raise ValueError("Segment area fractions must sum to a positive value.")
return {
class_name: total_plate_grams * weighted_area / total_weighted_area
for class_name, weighted_area in weighted_areas.items()
}
def estimate_macros(
grams_by_class: dict[str, float],
macro_table: dict[str, dict[str, float]],
) -> tuple[list[dict[str, Any]], dict[str, float]]:
rows = []
totals = {field: 0.0 for field in ("grams", *MACRO_FIELDS)}
for class_name, grams in grams_by_class.items():
if class_name not in macro_table:
raise KeyError(f"Missing macro row for class '{class_name}'")
row = {"class_name": class_name, "grams": round(grams, 1)}
for field in MACRO_FIELDS:
value = grams / 100.0 * macro_table[class_name][field]
row[field] = round(value, 1)
totals[field] += value
totals["grams"] += grams
rows.append(row)
totals = {key: round(value, 1) for key, value in totals.items()}
return rows, totals
def main() -> None:
args = parse_args()
segments = load_segments(args.segments)
macro_table = load_macro_table(args.macros)
portions = load_portions(args.portions)
grams_by_class = estimate_grams(segments, portions)
rows, totals = estimate_macros(grams_by_class, macro_table)
payload = {"items": rows, "totals": totals}
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(payload, indent=2), encoding="utf-8")
print(f"Wrote estimate: {args.output}")
else:
print(json.dumps(payload, indent=2))
if __name__ == "__main__":
main()