"""Predict food segment area fractions for one image using a trained YOLO model. Example: python scripts/predict_segment_areas.py --image path/to/meal.jpg --output data/nutrition/segments.json """ from __future__ import annotations import argparse import json import os import sys from pathlib import Path import cv2 import numpy as np PROJECT_ROOT = Path(__file__).resolve().parents[1] SCRIPT_DIR = Path(__file__).resolve().parent if str(SCRIPT_DIR) not in sys.path: sys.path.insert(0, str(SCRIPT_DIR)) os.environ.setdefault("YOLO_CONFIG_DIR", str(PROJECT_ROOT / ".ultralytics")) from ultralytics import YOLO from segmentation_utils import load_data_yaml_class_names DEFAULT_WEIGHTS = ( PROJECT_ROOT / "runs" / "foodseg103_target" / "yolov8s_target_e10_w02" / "weights" / "best.pt" ) DEFAULT_DATA = PROJECT_ROOT / "data" / "processed" / "foodseg103_target_yolo" / "data.yaml" def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--image", type=Path, required=True) parser.add_argument("--weights", type=Path, default=DEFAULT_WEIGHTS) parser.add_argument("--data", type=Path, default=DEFAULT_DATA) parser.add_argument("--imgsz", type=int, default=512) parser.add_argument("--conf", type=float, default=0.05) parser.add_argument("--output", type=Path) return parser.parse_args() def main() -> None: args = parse_args() class_names = load_data_yaml_class_names(args.data) image_bgr = cv2.imread(str(args.image)) if image_bgr is None: raise RuntimeError(f"Could not read image: {args.image}") image = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB) height, width = image.shape[:2] model = YOLO(str(args.weights)) result = model.predict(image, imgsz=args.imgsz, conf=args.conf, verbose=False)[0] area_by_class = {name: 0 for name in class_names.values()} occupied = np.zeros((height, width), dtype=bool) if result.masks is not None: masks = result.masks.data.cpu().numpy() classes = result.boxes.cls.cpu().numpy().astype(int) confs = result.boxes.conf.cpu().numpy() for idx in np.argsort(confs): item = masks[idx] if item.shape != (height, width): item = cv2.resize(item, (width, height), interpolation=cv2.INTER_NEAREST) binary = item > 0.5 class_name = class_names[int(classes[idx])] area_by_class[class_name] += int(np.logical_and(binary, ~occupied).sum()) occupied |= binary total_food_area = sum(area_by_class.values()) segments = [] for class_name, pixel_area in area_by_class.items(): if pixel_area <= 0 or total_food_area <= 0: continue segments.append( { "class_name": class_name, "pixel_area": pixel_area, "area_fraction": round(pixel_area / total_food_area, 6), } ) payload = { "image": str(args.image), "weights": str(args.weights), "conf": args.conf, "total_food_area": total_food_area, "segments": segments, } 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 segments: {args.output}") else: print(json.dumps(payload, indent=2)) if __name__ == "__main__": main()