diff --git a/cloud/APIs/__init__.py b/cloud/APIs/__init__.py new file mode 100644 index 0000000..8f53c54 --- /dev/null +++ b/cloud/APIs/__init__.py @@ -0,0 +1,2 @@ +from APIs.aichat import * +from APIs.edamam import * \ No newline at end of file diff --git a/cloud/tools/aichat.py b/cloud/APIs/aichat.py similarity index 100% rename from cloud/tools/aichat.py rename to cloud/APIs/aichat.py diff --git a/cloud/APIs/edamam.py b/cloud/APIs/edamam.py new file mode 100644 index 0000000..f28e84d --- /dev/null +++ b/cloud/APIs/edamam.py @@ -0,0 +1,937 @@ +import os +import requests +from typing import Dict, Any, Optional + +EDAMAM_APP_ID__FOOD = os.getenv("EDAMAM_APP_ID__FOOD", None) +EDAMAM_APP_KEY__FOOD = os.getenv("EDAMAM_APP_KEY__FOOD", None) + +class EdamamAPI: + """Wrapper around Edamam Food Database API 2.0 (Vision & Nutrients)""" + + BASE_URL = "https://api.edamam.com/api/vision/v1/nutrients" + + def __init__(self): + pass + + def analyze_dish_image(self, image_file_path: str): + url = "https://api.edamam.com/api/vision/v1/nutrients" + + # Query parameters as specified by the YAML schema + params = { + "app_id": EDAMAM_APP_ID__FOOD, + "app_key": EDAMAM_APP_KEY__FOOD, + "beta": "true" # Recommended since vision is in Beta + } + print("params:", params) + + # Specify the media type in the header + headers = { + "Content-Type": "image/jpeg" + } + + # Read and send the raw binary bytes directly in data (NOT files= or json=) + with open(image_file_path, "rb") as image_file: + raw_image_bytes = image_file.read() + + response = requests.post( + url, + params=params, + headers=headers, + data=raw_image_bytes + ) + + print(f"Edamam API response : {response.json()}") + response.raise_for_status() + return response.json() + # Returns something like: + """ + { + "combined": { + "problems": [], + "recipe": { + "calories": 306, + "cautions": [ + "GLUTEN", + "WHEAT", + "MILK", + "SULFITES", + "FODMAP" + ], + "dietLabels": [], + "healthLabels": [ + "EGG_FREE", + "PEANUT_FREE", + "TREE_NUT_FREE", + "SOY_FREE", + "FISH_FREE", + "SHELLFISH_FREE", + "CRUSTACEAN_FREE", + "CELERY_FREE", + "MUSTARD_FREE", + "SESAME_FREE", + "LUPINE_FREE", + "MOLLUSK_FREE", + "ALCOHOL_FREE" + ], + "ingredientLines": [ + "90 g (3 oz) Beef chunks", + "100 g (0.75 cup) Potatoes, cubed", + "40 g (0.33 cup) Carrots, sliced", + "30 g (0.25 cup) Green beans", + "120 g (0.5 cup) Brown gravy or stew sauce", + "Salt and pepper to taste" + ], + "label": "Beef Stew with Vegetables (Frozen Meal)", + "totalDaily": { + "CA": { + "label": "Calcium", + "quantity": 9.808827779627787, + "unit": "%" + }, + "CHOCDF": { + "label": "Carbs", + "quantity": 9.07220373540042, + "unit": "%" + }, + "CHOCDF.net": { + "label": "Carbohydrates (net)", + "quantity": 0.0, + "unit": "g" + }, + "CHOLE": { + "label": "Cholesterol", + "quantity": 20.837056709550403, + "unit": "%" + }, + "ENERC_KCAL": { + "label": "Energy", + "quantity": 13.100053025164515, + "unit": "%" + }, + "FASAT": { + "label": "Saturated", + "quantity": 17.148508115376796, + "unit": "%" + }, + "FAT": { + "label": "Fat", + "quantity": 14.902089676480887, + "unit": "%" + }, + "FE": { + "label": "Iron", + "quantity": 21.951689781236098, + "unit": "%" + }, + "FIBTG": { + "label": "Fiber", + "quantity": 13.858671762525935, + "unit": "%" + }, + "FOLDFE": { + "label": "Folate equivalent (total)", + "quantity": 10.817828789332737, + "unit": "%" + }, + "K": { + "label": "Potassium", + "quantity": 28.904445650775536, + "unit": "%" + }, + "MG": { + "label": "Magnesium", + "quantity": 15.863016599088612, + "unit": "%" + }, + "NA": { + "label": "Sodium", + "quantity": 60.46248, + "unit": "%" + }, + "NIA": { + "label": "Niacin (B3)", + "quantity": 66.37252070251446, + "unit": "%" + }, + "P": { + "label": "Phosphorus", + "quantity": 25.034580972170218, + "unit": "%" + }, + "PROCNT": { + "label": "Protein", + "quantity": 21.91938193709794, + "unit": "%" + }, + "RIBF": { + "label": "Riboflavin (B2)", + "quantity": 73.88477475545037, + "unit": "%" + }, + "THIA": { + "label": "Thiamin (B1)", + "quantity": 84.12049444050538, + "unit": "%" + }, + "TOCPHA": { + "label": "Vitamin E", + "quantity": 12.990953399638641, + "unit": "%" + }, + "VITA_RAE": { + "label": "Vitamin A", + "quantity": 45.87938825251855, + "unit": "%" + }, + "VITB12": { + "label": "Vitamin B12", + "quantity": 113.07016020542191, + "unit": "%" + }, + "VITB6A": { + "label": "Vitamin B6", + "quantity": 90.1530888403073, + "unit": "%" + }, + "VITC": { + "label": "Vitamin C", + "quantity": 35.18207820897409, + "unit": "%" + }, + "VITD": { + "label": "Vitamin D", + "quantity": 13.66228503481425, + "unit": "%" + }, + "VITK1": { + "label": "Vitamin K", + "quantity": 27.534449511962567, + "unit": "%" + }, + "ZN": { + "label": "Zinc", + "quantity": 55.091388643252344, + "unit": "%" + } + }, + "totalNutrients": { + "CA": { + "label": "Calcium", + "quantity": 127.51476113516124, + "unit": "mg" + }, + "CHOCDF": { + "label": "Carbs", + "quantity": 26.573051753721295, + "unit": "g" + }, + "CHOCDF.net": { + "label": "Carbohydrates (net)", + "quantity": 21.30675648396144, + "unit": "g" + }, + "CHOLE": { + "label": "Cholesterol", + "quantity": 73.23964970198097, + "unit": "mg" + }, + "ENERC_KCAL": { + "label": "Energy", + "quantity": 306.9669925121675, + "unit": "kcal" + }, + "FAMS": { + "label": "Monounsaturated", + "quantity": 5.622055495052523, + "unit": "g" + }, + "FAPU": { + "label": "Polyunsaturated", + "quantity": 1.5770619917608195, + "unit": "g" + }, + "FASAT": { + "label": "Saturated", + "quantity": 4.0183241641356675, + "unit": "g" + }, + "FAT": { + "label": "Fat", + "quantity": 11.639773878137946, + "unit": "g" + }, + "FATRN": { + "label": "Trans", + "quantity": 0.24391676668519283, + "unit": "g" + }, + "FE": { + "label": "Iron", + "quantity": 3.9513041606224975, + "unit": "mg" + }, + "FIBTG": { + "label": "Fiber", + "quantity": 5.266295269759856, + "unit": "g" + }, + "FOLAC": { + "label": "Folic acid", + "quantity": 18.024043373983595, + "unit": "\u00b5g" + }, + "FOLDFE": { + "label": "Folate equivalent (total)", + "quantity": 43.271315157330946, + "unit": "\u00b5g" + }, + "FOLFD": { + "label": "Folate (food)", + "quantity": 40.54129806334046, + "unit": "\u00b5g" + }, + "K": { + "label": "Potassium", + "quantity": 982.7511521263683, + "unit": "mg" + }, + "MG": { + "label": "Magnesium", + "quantity": 66.62466971617216, + "unit": "mg" + }, + "NA": { + "label": "Sodium", + "quantity": 906.9372000000001, + "unit": "mg" + }, + "NIA": { + "label": "Niacin (B3)", + "quantity": 10.619603312402313, + "unit": "mg" + }, + "P": { + "label": "Phosphorus", + "quantity": 312.93226215212775, + "unit": "mg" + }, + "PROCNT": { + "label": "Protein", + "quantity": 25.68129586205237, + "unit": "g" + }, + "RIBF": { + "label": "Riboflavin (B2)", + "quantity": 0.9605020718208549, + "unit": "mg" + }, + "SUGAR": { + "label": "Sugars", + "quantity": 8.053516658227332, + "unit": "g" + }, + "SUGAR.added": { + "label": "Sugars, added", + "quantity": 2.3940423830596225, + "unit": "g" + }, + "Sugar.alcohol": { + "label": "Sugar alcohol", + "quantity": 0.0, + "unit": "g" + }, + "THIA": { + "label": "Thiamin (B1)", + "quantity": 1.0094459332860646, + "unit": "mg" + }, + "TOCPHA": { + "label": "Vitamin E", + "quantity": 1.9486430099457963, + "unit": "mg" + }, + "VITA_RAE": { + "label": "Vitamin A", + "quantity": 412.914494272667, + "unit": "\u00b5g" + }, + "VITB12": { + "label": "Vitamin B12", + "quantity": 2.7136838449301255, + "unit": "\u00b5g" + }, + "VITB6A": { + "label": "Vitamin B6", + "quantity": 1.5326025102852243, + "unit": "mg" + }, + "VITC": { + "label": "Vitamin C", + "quantity": 31.66387038807668, + "unit": "mg" + }, + "VITD": { + "label": "Vitamin D", + "quantity": 2.73245700696285, + "unit": "\u00b5g" + }, + "VITK1": { + "label": "Vitamin K", + "quantity": 33.04133941435508, + "unit": "\u00b5g" + }, + "WATER": { + "label": "Water", + "quantity": 321.1285604946806, + "unit": "g" + }, + "ZN": { + "label": "Zinc", + "quantity": 6.060052750757758, + "unit": "mg" + } + }, + "totalWeight": 390.7575715150228, + "translations": [ + { + "id": "EGG_FREE", + "label": "Egg-Free", + "type": "health" + }, + { + "id": "PEANUT_FREE", + "label": "Peanut-Free", + "type": "health" + }, + { + "id": "TREE_NUT_FREE", + "label": "Tree-Nut-Free", + "type": "health" + }, + { + "id": "SOY_FREE", + "label": "Soy-Free", + "type": "health" + }, + { + "id": "FISH_FREE", + "label": "Fish-Free", + "type": "health" + }, + { + "id": "SHELLFISH_FREE", + "label": "Shellfish-Free", + "type": "health" + }, + { + "id": "CRUSTACEAN_FREE", + "label": "Crustacean-Free", + "type": "health" + }, + { + "id": "CELERY_FREE", + "label": "Celery-Free", + "type": "health" + }, + { + "id": "MUSTARD_FREE", + "label": "Mustard-Free", + "type": "health" + }, + { + "id": "SESAME_FREE", + "label": "Sesame-Free", + "type": "health" + }, + { + "id": "LUPINE_FREE", + "label": "Lupine-Free", + "type": "health" + }, + { + "id": "MOLLUSK_FREE", + "label": "Mollusk-Free", + "type": "health" + }, + { + "id": "ALCOHOL_FREE", + "label": "Alcohol-Free", + "type": "health" + }, + { + "id": "GLUTEN", + "label": "Gluten", + "type": "caution" + }, + { + "id": "WHEAT", + "label": "Wheat", + "type": "caution" + }, + { + "id": "MILK", + "label": "Milk", + "type": "caution" + }, + { + "id": "SULFITES", + "label": "Sulfites", + "type": "caution" + }, + { + "id": "FODMAP", + "label": "FODMAP", + "type": "caution" + } + ], + "uri": "http://www.edamam.com/ontologies/edamam.owl#recipe_e59b15e71cd34bf4b7c02c2f545e40f3", + "yield": 1.0 + } + }, + "dishes": [ + { + "problems": [], + "recipe": { + "calories": 306, + "cautions": [ + "GLUTEN", + "WHEAT", + "MILK", + "SULFITES", + "FODMAP" + ], + "dietLabels": [], + "healthLabels": [ + "EGG_FREE", + "PEANUT_FREE", + "TREE_NUT_FREE", + "SOY_FREE", + "FISH_FREE", + "SHELLFISH_FREE", + "CRUSTACEAN_FREE", + "CELERY_FREE", + "MUSTARD_FREE", + "SESAME_FREE", + "LUPINE_FREE", + "MOLLUSK_FREE", + "ALCOHOL_FREE" + ], + "ingredientLines": [ + "90 g (3 oz) Beef chunks", + "100 g (0.75 cup) Potatoes, cubed", + "40 g (0.33 cup) Carrots, sliced", + "30 g (0.25 cup) Green beans", + "120 g (0.5 cup) Brown gravy or stew sauce", + "Salt and pepper to taste" + ], + "label": "Beef Stew with Vegetables (Frozen Meal)", + "totalDaily": { + "CA": { + "label": "Calcium", + "quantity": 9.808827779627787, + "unit": "%" + }, + "CHOCDF": { + "label": "Carbs", + "quantity": 9.07220373540042, + "unit": "%" + }, + "CHOCDF.net": { + "label": "Carbohydrates (net)", + "quantity": 0.0, + "unit": "g" + }, + "CHOLE": { + "label": "Cholesterol", + "quantity": 20.837056709550403, + "unit": "%" + }, + "ENERC_KCAL": { + "label": "Energy", + "quantity": 13.100053025164515, + "unit": "%" + }, + "FASAT": { + "label": "Saturated", + "quantity": 17.148508115376796, + "unit": "%" + }, + "FAT": { + "label": "Fat", + "quantity": 14.902089676480887, + "unit": "%" + }, + "FE": { + "label": "Iron", + "quantity": 21.951689781236098, + "unit": "%" + }, + "FIBTG": { + "label": "Fiber", + "quantity": 13.858671762525935, + "unit": "%" + }, + "FOLDFE": { + "label": "Folate equivalent (total)", + "quantity": 10.817828789332737, + "unit": "%" + }, + "K": { + "label": "Potassium", + "quantity": 28.904445650775536, + "unit": "%" + }, + "MG": { + "label": "Magnesium", + "quantity": 15.863016599088612, + "unit": "%" + }, + "NA": { + "label": "Sodium", + "quantity": 60.46248, + "unit": "%" + }, + "NIA": { + "label": "Niacin (B3)", + "quantity": 66.37252070251446, + "unit": "%" + }, + "P": { + "label": "Phosphorus", + "quantity": 25.034580972170218, + "unit": "%" + }, + "PROCNT": { + "label": "Protein", + "quantity": 21.91938193709794, + "unit": "%" + }, + "RIBF": { + "label": "Riboflavin (B2)", + "quantity": 73.88477475545037, + "unit": "%" + }, + "THIA": { + "label": "Thiamin (B1)", + "quantity": 84.12049444050538, + "unit": "%" + }, + "TOCPHA": { + "label": "Vitamin E", + "quantity": 12.990953399638641, + "unit": "%" + }, + "VITA_RAE": { + "label": "Vitamin A", + "quantity": 45.87938825251855, + "unit": "%" + }, + "VITB12": { + "label": "Vitamin B12", + "quantity": 113.07016020542191, + "unit": "%" + }, + "VITB6A": { + "label": "Vitamin B6", + "quantity": 90.1530888403073, + "unit": "%" + }, + "VITC": { + "label": "Vitamin C", + "quantity": 35.18207820897409, + "unit": "%" + }, + "VITD": { + "label": "Vitamin D", + "quantity": 13.66228503481425, + "unit": "%" + }, + "VITK1": { + "label": "Vitamin K", + "quantity": 27.534449511962567, + "unit": "%" + }, + "ZN": { + "label": "Zinc", + "quantity": 55.091388643252344, + "unit": "%" + } + }, + "totalNutrients": { + "CA": { + "label": "Calcium", + "quantity": 127.51476113516124, + "unit": "mg" + }, + "CHOCDF": { + "label": "Carbs", + "quantity": 26.573051753721295, + "unit": "g" + }, + "CHOCDF.net": { + "label": "Carbohydrates (net)", + "quantity": 21.30675648396144, + "unit": "g" + }, + "CHOLE": { + "label": "Cholesterol", + "quantity": 73.23964970198097, + "unit": "mg" + }, + "ENERC_KCAL": { + "label": "Energy", + "quantity": 306.9669925121675, + "unit": "kcal" + }, + "FAMS": { + "label": "Monounsaturated", + "quantity": 5.622055495052523, + "unit": "g" + }, + "FAPU": { + "label": "Polyunsaturated", + "quantity": 1.5770619917608195, + "unit": "g" + }, + "FASAT": { + "label": "Saturated", + "quantity": 4.0183241641356675, + "unit": "g" + }, + "FAT": { + "label": "Fat", + "quantity": 11.639773878137946, + "unit": "g" + }, + "FATRN": { + "label": "Trans", + "quantity": 0.24391676668519283, + "unit": "g" + }, + "FE": { + "label": "Iron", + "quantity": 3.9513041606224975, + "unit": "mg" + }, + "FIBTG": { + "label": "Fiber", + "quantity": 5.266295269759856, + "unit": "g" + }, + "FOLAC": { + "label": "Folic acid", + "quantity": 18.024043373983595, + "unit": "\u00b5g" + }, + "FOLDFE": { + "label": "Folate equivalent (total)", + "quantity": 43.271315157330946, + "unit": "\u00b5g" + }, + "FOLFD": { + "label": "Folate (food)", + "quantity": 40.54129806334046, + "unit": "\u00b5g" + }, + "K": { + "label": "Potassium", + "quantity": 982.7511521263683, + "unit": "mg" + }, + "MG": { + "label": "Magnesium", + "quantity": 66.62466971617216, + "unit": "mg" + }, + "NA": { + "label": "Sodium", + "quantity": 906.9372000000001, + "unit": "mg" + }, + "NIA": { + "label": "Niacin (B3)", + "quantity": 10.619603312402313, + "unit": "mg" + }, + "P": { + "label": "Phosphorus", + "quantity": 312.93226215212775, + "unit": "mg" + }, + "PROCNT": { + "label": "Protein", + "quantity": 25.68129586205237, + "unit": "g" + }, + "RIBF": { + "label": "Riboflavin (B2)", + "quantity": 0.9605020718208549, + "unit": "mg" + }, + "SUGAR": { + "label": "Sugars", + "quantity": 8.053516658227332, + "unit": "g" + }, + "SUGAR.added": { + "label": "Sugars, added", + "quantity": 2.3940423830596225, + "unit": "g" + }, + "Sugar.alcohol": { + "label": "Sugar alcohol", + "quantity": 0.0, + "unit": "g" + }, + "THIA": { + "label": "Thiamin (B1)", + "quantity": 1.0094459332860646, + "unit": "mg" + }, + "TOCPHA": { + "label": "Vitamin E", + "quantity": 1.9486430099457963, + "unit": "mg" + }, + "VITA_RAE": { + "label": "Vitamin A", + "quantity": 412.914494272667, + "unit": "\u00b5g" + }, + "VITB12": { + "label": "Vitamin B12", + "quantity": 2.7136838449301255, + "unit": "\u00b5g" + }, + "VITB6A": { + "label": "Vitamin B6", + "quantity": 1.5326025102852243, + "unit": "mg" + }, + "VITC": { + "label": "Vitamin C", + "quantity": 31.66387038807668, + "unit": "mg" + }, + "VITD": { + "label": "Vitamin D", + "quantity": 2.73245700696285, + "unit": "\u00b5g" + }, + "VITK1": { + "label": "Vitamin K", + "quantity": 33.04133941435508, + "unit": "\u00b5g" + }, + "WATER": { + "label": "Water", + "quantity": 321.1285604946806, + "unit": "g" + }, + "ZN": { + "label": "Zinc", + "quantity": 6.060052750757758, + "unit": "mg" + } + }, + "totalWeight": 390.7575715150228, + "translations": [ + { + "id": "EGG_FREE", + "label": "Egg-Free", + "type": "health" + }, + { + "id": "PEANUT_FREE", + "label": "Peanut-Free", + "type": "health" + }, + { + "id": "TREE_NUT_FREE", + "label": "Tree-Nut-Free", + "type": "health" + }, + { + "id": "SOY_FREE", + "label": "Soy-Free", + "type": "health" + }, + { + "id": "FISH_FREE", + "label": "Fish-Free", + "type": "health" + }, + { + "id": "SHELLFISH_FREE", + "label": "Shellfish-Free", + "type": "health" + }, + { + "id": "CRUSTACEAN_FREE", + "label": "Crustacean-Free", + "type": "health" + }, + { + "id": "CELERY_FREE", + "label": "Celery-Free", + "type": "health" + }, + { + "id": "MUSTARD_FREE", + "label": "Mustard-Free", + "type": "health" + }, + { + "id": "SESAME_FREE", + "label": "Sesame-Free", + "type": "health" + }, + { + "id": "LUPINE_FREE", + "label": "Lupine-Free", + "type": "health" + }, + { + "id": "MOLLUSK_FREE", + "label": "Mollusk-Free", + "type": "health" + }, + { + "id": "ALCOHOL_FREE", + "label": "Alcohol-Free", + "type": "health" + }, + { + "id": "GLUTEN", + "label": "Gluten", + "type": "caution" + }, + { + "id": "WHEAT", + "label": "Wheat", + "type": "caution" + }, + { + "id": "MILK", + "label": "Milk", + "type": "caution" + }, + { + "id": "SULFITES", + "label": "Sulfites", + "type": "caution" + }, + { + "id": "FODMAP", + "label": "FODMAP", + "type": "caution" + } + ], + "uri": "http://www.edamam.com/ontologies/edamam.owl#recipe_e59b15e71cd34bf4b7c02c2f545e40f3", + "yield": 1.0 + } + } + ] + } + + """ diff --git a/cloud/app.py b/cloud/app.py index 67e0dec..dc714ba 100644 --- a/cloud/app.py +++ b/cloud/app.py @@ -3,8 +3,13 @@ import base64 import uuid from flask import Flask, request, jsonify from pymongo import MongoClient -from tools.aichat import generate -from shared import config +from APIs import generate, EdamamAPI +import sys +sys.path.insert(0, '..') +try: + from shared import config +except ImportError: + from ..shared import config app = Flask(__name__) @@ -93,7 +98,12 @@ def device_network(): except Exception as e: return jsonify({"error": f"Database error: {str(e)}"}), 500 - + +@app.route("/debug", methods=["GET"]) +def debug(): + image_path = "microwaveDish.jpg" + edamam = EdamamAPI() + return edamam.analyze_dish_image(image_path) if __name__ == "__main__": app.run(debug=config.DEBUG) \ No newline at end of file diff --git a/cloud/microwaveDish.jpg b/cloud/microwaveDish.jpg new file mode 100644 index 0000000..87531df Binary files /dev/null and b/cloud/microwaveDish.jpg differ diff --git a/cloud/microwaveDishAnalyzer.py b/cloud/microwaveDishAnalyzer.py new file mode 100644 index 0000000..8413443 --- /dev/null +++ b/cloud/microwaveDishAnalyzer.py @@ -0,0 +1,129 @@ +import cv2 +import numpy as np +from typing import Dict, Any, Optional + + +class MicrowaveDishAnalyzer: + """ + Estimates food dish volume from top-down camera images and dish height, + and cross-validates physical volume against Edamam AI mass estimates. + """ + + # Constant scale ratio: Centimeters per Pixel. + # TODO : Replace this value once your camera calibration is complete. + CM_PER_PIXEL: float = 0.05 # Example: 1 pixel = 0.05 cm + + def __init__(self, cm_per_pixel: Optional[float] = None): + if cm_per_pixel is not None: + self.cm_per_pixel = cm_per_pixel + else: + self.cm_per_pixel = self.CM_PER_PIXEL + + def calculate_surface_area_cm2(self, image_path: str) -> float: + """ + Segments the food/dish from the background and returns surface area in cm². + """ + image = cv2.imread(image_path) + if image is None: + raise FileNotFoundError(f"Image could not be loaded from path: {image_path}") + + # 1. Convert to grayscale & blur to reduce noise + gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + blurred = cv2.GaussianBlur(gray, (5, 5), 0) + + # 2. Otsu thresholding to segment foreground (dish) from background (turntable) + _, thresh = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) + + # 3. Find contours + contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + if not contours: + return 0.0 + + # 4. Assume the largest contour corresponds to the dish/food area + largest_contour = max(contours, key=cv2.contourArea) + area_pixels = cv2.contourArea(largest_contour) + + # 5. Convert pixels² to cm² using scale ratio squared + area_cm2 = area_pixels * (self.cm_per_pixel ** 2) + return float(area_cm2) + + @staticmethod + def _get_shape_factor(food_label: str) -> float: + """ + Selects geometric correction factor (k_shape) based on container/food shape: + - Bowls/Soups: ~0.60 (paraboloid) + - Drinks/Mugs: ~0.95 (cylinder) + - Flat plates/solid foods: ~0.85 (truncated cone / disk) + """ + label = food_label.lower() + if any(w in label for w in ["soup", "chili", "stew", "curry", "bowl"]): + return 0.60 + elif any(w in label for w in ["coffee", "tea", "milk", "water", "beverage", "mug"]): + return 0.95 + elif any(w in label for w in ["bread", "cake", "muffin"]): + return 0.80 + return 0.85 # Default factor for plated meals + + def estimate_volume( + self, image_path: str, height_cm: float, food_label: str = "" + ) -> Dict[str, float]: + """ + Computes total physical volume in cm³ (mL). + Volume = Area (cm²) * Height (cm) * Shape Factor + """ + area_cm2 = self.calculate_surface_area_cm2(image_path) + k_shape = self._get_shape_factor(food_label) + volume_cm3 = area_cm2 * height_cm * k_shape + + return { + "surface_area_cm2": round(area_cm2, 2), + "height_cm": round(height_cm, 2), + "shape_factor": k_shape, + "volume_cm3": round(volume_cm3, 2), + } + + def reconcile_mass( + self, edamam_mass_g: float, volume_cm3: float, food_label: str = "" + ) -> Dict[str, Any]: + """ + Cross-validates Edamam's visual mass against physical volume using expected density. + Returns the most physically accurate mass estimate in grams. + """ + if volume_cm3 <= 0: + return { + "final_mass_g": edamam_mass_g, + "status": "unvalidated_zero_volume", + "calculated_density": None, + } + + calculated_density = edamam_mass_g / volume_cm3 + label = food_label.lower() + + # Expected food densities (g/cm³) + if any(w in label for w in ["bread", "popcorn", "cake"]): + expected_density = 0.35 + elif any(w in label for w in ["soup", "beverage", "water", "milk"]): + expected_density = 1.0 + else: + expected_density = 0.92 # Average cooked meal (water + fats + carbs) + + # Plausibility bounds (±35% variance around expected density) + min_density = expected_density * 0.65 + max_density = expected_density * 1.35 + + if min_density <= calculated_density <= max_density: + # Edamam estimate is physically realistic + final_mass = edamam_mass_g + status = "validated_edamam_mass" + else: + # Edamam misjudged scale — fallback to Volume * Expected Density + final_mass = volume_cm3 * expected_density + status = "reconciled_via_volume_density" + + return { + "final_mass_g": round(final_mass, 2), + "raw_edamam_mass_g": edamam_mass_g, + "calculated_density_g_cm3": round(calculated_density, 3), + "expected_density_g_cm3": expected_density, + "status": status, + } \ No newline at end of file diff --git a/cloud/microwaveThermalEngine.py b/cloud/microwaveThermalEngine.py new file mode 100644 index 0000000..f65efa1 --- /dev/null +++ b/cloud/microwaveThermalEngine.py @@ -0,0 +1,52 @@ +from dataclasses import dataclass +from typing import Dict, Any, Optional + +@dataclass +class DishThermalState: + food_name: str + macronutrients: Dict[str, float] # grams of water, fat, protein, carbs + estimated_mass_g: float + initial_temp_c: float + volume_cm3: Optional[float] = None + +class MicrowaveThermalEngine: + """Calculates cook parameters based on physical properties""" + + DEFAULT_EFFICIENCY = 0.70 # ~70% magnetron efficiency + TARGET_TEMP_C = 74.0 # Safe food temperature + + @staticmethod + def estimate_specific_heat(macros: Dict[str, float], total_weight_g: float) -> float: + """Estimates Cp in J/(g*C) based on macro composition""" + if total_weight_g <= 0: + return 3.5 # Fallback average for mixed meals + + w_water = macros.get("water_g", total_weight_g * 0.7) / total_weight_g + w_protein = macros.get("protein_g", 0.0) / total_weight_g + w_fat = macros.get("fat_g", 0.0) / total_weight_g + w_carbs = macros.get("carbs_g", 0.0) / total_weight_g + + return (4.184 * w_water) + (1.71 * w_protein) + (1.67 * w_fat) + (1.42 * w_carbs) + + def calculate_cook_plan(self, state: DishThermalState, microwave_wattage: int) -> Dict[str, Any]: + cp = self.estimate_specific_heat(state.macronutrients, state.estimated_mass_g) + delta_t = max(0.0, self.TARGET_TEMP_C - state.initial_temp_c) + + # Q = m * c_p * delta_t + required_joules = state.estimated_mass_g * cp * delta_t + effective_power_watts = microwave_wattage * self.DEFAULT_EFFICIENCY + + total_seconds = required_joules / effective_power_watts if effective_power_watts > 0 else 0 + + # Determine duty cycle / power level recommendations + power_level = 100 + if state.initial_temp_c < 0: # Frozen food requires defrost cycle to prevent edge-cooking + power_level = 50 + total_seconds *= 1.4 + + return { + "cook_time_seconds": round(total_seconds), + "recommended_power_level_pct": power_level, + "estimated_specific_heat": round(cp, 2), + "energy_joules": round(required_joules) + } \ No newline at end of file diff --git a/cloud/requirements.txt b/cloud/requirements.txt index ab247cb..68993f1 100644 --- a/cloud/requirements.txt +++ b/cloud/requirements.txt @@ -1,3 +1,4 @@ Flask==3.0.2 pymongo==4.6.1 -gunicorn==21.2.0 \ No newline at end of file +gunicorn==21.2.0 +opencv-python-headless \ No newline at end of file