Edamam API and dish volume estimation
Build, push image, and notify Watchtower / build-image (push) Successful in 2m33s
Build, push image, and notify Watchtower / notify (push) Successful in 9s

This commit is contained in:
2026-07-25 16:31:50 +02:00
parent a0af426c78
commit 6a42e4a772
8 changed files with 1135 additions and 4 deletions
+2
View File
@@ -0,0 +1,2 @@
from APIs.aichat import *
from APIs.edamam import *
+937
View File
@@ -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
}
}
]
}
"""
+11 -1
View File
@@ -3,8 +3,13 @@ import base64
import uuid import uuid
from flask import Flask, request, jsonify from flask import Flask, request, jsonify
from pymongo import MongoClient from pymongo import MongoClient
from tools.aichat import generate from APIs import generate, EdamamAPI
import sys
sys.path.insert(0, '..')
try:
from shared import config from shared import config
except ImportError:
from ..shared import config
app = Flask(__name__) app = Flask(__name__)
@@ -94,6 +99,11 @@ def device_network():
except Exception as e: except Exception as e:
return jsonify({"error": f"Database error: {str(e)}"}), 500 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__": if __name__ == "__main__":
app.run(debug=config.DEBUG) app.run(debug=config.DEBUG)
Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

+129
View File
@@ -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,
}
+52
View File
@@ -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)
}
+1
View File
@@ -1,3 +1,4 @@
Flask==3.0.2 Flask==3.0.2
pymongo==4.6.1 pymongo==4.6.1
gunicorn==21.2.0 gunicorn==21.2.0
opencv-python-headless