Spaces:
Runtime error
Runtime error
| """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() | |