Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import random | |
| from PIL import Image | |
| import numpy as np | |
| from datetime import datetime, timedelta | |
| import pandas as pd | |
| import plotly.graph_objects as go | |
| from plotly.subplots import make_subplots | |
| # 食物營養資料庫 | |
| food_nutrition = { | |
| "米飯": { | |
| "calories": 130, "protein": 2.7, "carbs": 28.7, "fat": 0.3, "fiber": 0.4, | |
| "category": "主食", "glycemic_index": "高", "allergens": [], | |
| "vitamins": {"B1": 0.02, "B2": 0.01, "B3": 0.4}, | |
| "minerals": {"鐵": 0.2, "鎂": 12, "鋅": 0.4} | |
| }, | |
| "牛肉": { | |
| "calories": 250, "protein": 26, "carbs": 0, "fat": 17, "fiber": 0, | |
| "category": "蛋白質", "glycemic_index": "低", "allergens": [], | |
| "vitamins": {"B12": 2.1, "B6": 0.3, "D": 0.1}, | |
| "minerals": {"鐵": 2.7, "鋅": 4.8, "硒": 33.2} | |
| }, | |
| "麵包": { | |
| "calories": 265, "protein": 9, "carbs": 49, "fat": 3.2, "fiber": 2.5, | |
| "category": "主食", "glycemic_index": "高", "allergens": ["麩質"], | |
| "vitamins": {"B1": 0.1, "B3": 1.3, "葉酸": 0.04}, | |
| "minerals": {"鎂": 26, "磷": 86, "鐵": 1.4} | |
| }, | |
| "蔬菜": { | |
| "calories": 25, "protein": 1.5, "carbs": 5, "fat": 0.2, "fiber": 2.5, | |
| "category": "蔬菜", "glycemic_index": "低", "allergens": [], | |
| "vitamins": {"A": 0.5, "C": 10, "K": 0.1}, | |
| "minerals": {"鉀": 290, "鈣": 40, "鎂": 15} | |
| }, | |
| "番茄": { | |
| "calories": 18, "protein": 0.9, "carbs": 3.9, "fat": 0.2, "fiber": 1.2, | |
| "category": "蔬果", "glycemic_index": "低", "allergens": [], | |
| "vitamins": {"C": 13.7, "A": 0.8, "K": 0.07}, | |
| "minerals": {"鉀": 237, "鎂": 11, "鈣": 10} | |
| }, | |
| "雞蛋": { | |
| "calories": 155, "protein": 13, "carbs": 0.6, "fat": 11, "fiber": 0, | |
| "category": "蛋白質", "glycemic_index": "低", "allergens": ["蛋"], | |
| "vitamins": {"D": 1.1, "B12": 0.6, "B2": 0.3}, | |
| "minerals": {"硒": 20, "鋅": 1.3, "鐵": 1.8} | |
| }, | |
| "豬肉": { | |
| "calories": 242, "protein": 22.5, "carbs": 0, "fat": 17, "fiber": 0, | |
| "category": "蛋白質", "glycemic_index": "低", "allergens": [], | |
| "vitamins": {"B1": 0.6, "B6": 0.4, "B12": 0.7}, | |
| "minerals": {"鋅": 4.5, "鐵": 1.2, "硒": 35.3} | |
| }, | |
| "三文魚": { | |
| "calories": 208, "protein": 22, "carbs": 0, "fat": 13, "fiber": 0, | |
| "category": "蛋白質", "glycemic_index": "低", "allergens": ["魚"], | |
| "vitamins": {"D": 9.5, "B12": 3.2, "B6": 0.6}, | |
| "minerals": {"硒": 40.2, "鋅": 0.5, "鐵": 0.5} | |
| }, | |
| "青菜": { | |
| "calories": 16, "protein": 1.3, "carbs": 2.9, "fat": 0.2, "fiber": 2.1, | |
| "category": "蔬菜", "glycemic_index": "低", "allergens": [], | |
| "vitamins": {"A": 0.3, "C": 35, "K": 0.2}, | |
| "minerals": {"鈣": 105, "鎂": 22, "鉀": 370} | |
| }, | |
| "水果": { | |
| "calories": 52, "protein": 0.5, "carbs": 14, "fat": 0.2, "fiber": 2.4, | |
| "category": "蔬果", "glycemic_index": "中", "allergens": [], | |
| "vitamins": {"C": 8.5, "A": 0.2, "葉酸": 0.02}, | |
| "minerals": {"鉀": 200, "鎂": 10, "鈣": 20} | |
| }, | |
| "土豆": { | |
| "calories": 77, "protein": 2, "carbs": 17, "fat": 0.1, "fiber": 2.2, | |
| "category": "主食", "glycemic_index": "高", "allergens": [], | |
| "vitamins": {"C": 10, "B6": 0.3, "葉酸": 0.01}, | |
| "minerals": {"鉀": 421, "鎂": 23, "磷": 57} | |
| }, | |
| "豆腐": { | |
| "calories": 84, "protein": 9, "carbs": 2, "fat": 5, "fiber": 0.5, | |
| "category": "蛋白質", "glycemic_index": "低", "allergens": ["大豆"], | |
| "vitamins": {"A": 0.1, "葉酸": 0.02, "K": 0.02}, | |
| "minerals": {"鈣": 350, "鐵": 2.1, "鎂": 37} | |
| }, | |
| "花生": { | |
| "calories": 567, "protein": 25.8, "carbs": 16.1, "fat": 49.2, "fiber": 8.5, | |
| "category": "脂肪", "glycemic_index": "低", "allergens": ["花生"], | |
| "vitamins": {"E": 8.3, "B3": 14.4, "葉酸": 0.24}, | |
| "minerals": {"鎂": 168, "磷": 376, "鋅": 3.3} | |
| }, | |
| "牛奶": { | |
| "calories": 42, "protein": 3.4, "carbs": 4.8, "fat": 1.0, "fiber": 0, | |
| "category": "乳製品", "glycemic_index": "低", "allergens": ["乳製品"], | |
| "vitamins": {"B12": 0.4, "B2": 0.2, "D": 0.1}, | |
| "minerals": {"鈣": 120, "磷": 93, "鉀": 150} | |
| }, | |
| "香蕉": { | |
| "calories": 89, "protein": 1.1, "carbs": 22.8, "fat": 0.3, "fiber": 2.6, | |
| "category": "蔬果", "glycemic_index": "中", "allergens": [], | |
| "vitamins": {"C": 8.7, "B6": 0.4, "A": 0.1}, | |
| "minerals": {"鉀": 358, "鎂": 27, "鐵": 0.3} | |
| }, | |
| "雞肉": { | |
| "calories": 239, "protein": 27, "carbs": 0, "fat": 14, "fiber": 0, | |
| "category": "蛋白質", "glycemic_index": "低", "allergens": [], | |
| "vitamins": {"B6": 0.5, "B12": 0.3, "A": 0.02}, | |
| "minerals": {"鋅": 2.7, "鐵": 0.9, "硒": 24.5} | |
| } | |
| } | |
| class UserProfile: | |
| # 初始化用戶資料 | |
| def __init__(self, age, gender, height, weight, activity_level): | |
| # 身高與體重範圍檢查 | |
| if not (50 <= height <= 250): | |
| raise ValueError("身高應在50到250公分之間") | |
| if not (20 <= weight <= 300): | |
| raise ValueError("體重應在20到300公斤之間") | |
| self.age = age | |
| self.gender = gender | |
| self.height = height | |
| self.weight = weight | |
| self.activity_level = activity_level | |
| def calculate_bmr(self): | |
| # 計算基礎代謢率(BMR) | |
| if self.gender == "男": | |
| return 88.362 + (13.397 * self.weight) + (4.799 * self.height) - (5.677 * self.age) | |
| else: | |
| return 447.593 + (9.247 * self.weight) + (3.098 * self.height) - (4.330 * self.age) | |
| def calculate_tdee(self): | |
| # 計算每日總能量消耗(TDEE) | |
| activity_factors = { | |
| "久坐": 1.2, | |
| "輕度活動": 1.375, | |
| "中度活動": 1.55, | |
| "重度活動": 1.725, | |
| "極重度活動": 1.9 | |
| } | |
| return self.calculate_bmr() * activity_factors[self.activity_level] | |
| def create_nutrition_charts(total_nutrients, recommended_nutrients, total_calories): | |
| """創建優化後的營養圖表""" | |
| # 創建子圖布局 | |
| fig = make_subplots( | |
| rows=2, cols=2, | |
| specs=[ | |
| [{"colspan": 2}, None], | |
| [{"type": "pie"}, {"type": "bar"}] | |
| ], | |
| subplot_titles=( | |
| '每日營養素攝入比較', | |
| '熱量來源分布', | |
| '營養素達標率' | |
| ), | |
| vertical_spacing=0.12, | |
| horizontal_spacing=0.1 | |
| ) | |
| # 1. 營養素對比條形圖(上方大圖) | |
| nutrients = ['蛋白質', '碳水化合物', '脂肪', '膳食纖維'] | |
| fig.add_trace( | |
| go.Bar( | |
| name='實際攝入', | |
| x=nutrients, | |
| y=[total_nutrients[n] for n in nutrients], | |
| marker_color='rgb(55, 83, 109)' | |
| ), | |
| row=1, col=1 | |
| ) | |
| fig.add_trace( | |
| go.Bar( | |
| name='建議攝入', | |
| x=nutrients, | |
| y=[recommended_nutrients[n] for n in nutrients], | |
| marker_color='rgb(26, 118, 255)' | |
| ), | |
| row=1, col=1 | |
| ) | |
| # 2. 熱量來源分布圓餅圖(左下) | |
| calorie_values = [ | |
| total_nutrients['蛋白質'] * 4, | |
| total_nutrients['碳水化合物'] * 4, | |
| total_nutrients['脂肪'] * 9 | |
| ] | |
| fig.add_trace( | |
| go.Pie( | |
| labels=['蛋白質', '碳水化合物', '脂肪'], | |
| values=calorie_values, | |
| hole=0.4, | |
| marker=dict(colors=['rgb(55, 83, 109)', 'rgb(26, 118, 255)', 'rgb(95, 70, 144)']) | |
| ), | |
| row=2, col=1 | |
| ) | |
| # 3. 營養素達標率(右下) | |
| achievement_rates = [ | |
| (total_nutrients[n] / recommended_nutrients[n] * 100) | |
| if recommended_nutrients[n] > 0 else 0 | |
| for n in nutrients | |
| ] | |
| fig.add_trace( | |
| go.Bar( | |
| x=nutrients, | |
| y=achievement_rates, | |
| marker_color='rgb(158, 202, 225)', | |
| text=[f"{rate:.1f}%" for rate in achievement_rates], | |
| textposition='auto', | |
| ), | |
| row=2, col=2 | |
| ) | |
| # 更新布局 | |
| fig.update_layout( | |
| height=800, # 增加圖表高度 | |
| width=900, # 適當的寬度 | |
| showlegend=True, | |
| legend=dict( | |
| orientation="h", | |
| yanchor="bottom", | |
| y=1.02, | |
| xanchor="right", | |
| x=1 | |
| ), | |
| title_text="營養分析總覽", | |
| title_x=0.5, | |
| font=dict(size=12), | |
| ) | |
| # 更新y軸標題 | |
| fig.update_yaxes(title_text="克", row=1, col=1) | |
| fig.update_yaxes(title_text="達標率(%)", row=2, col=2) | |
| return fig | |
| def analyze_meals_advanced( | |
| images, age, gender, height, weight, | |
| activity_level, diet_preference, health_goal, | |
| allergies, medical_conditions | |
| ): | |
| try: | |
| # 新增的輸入驗證 | |
| if not all([age, gender, height, weight, activity_level]): | |
| return "請填寫所有必要的個人信息", "", "", None, "" | |
| # 檢查過敏原 | |
| if "無" in allergies and len(allergies) > 1: | |
| return "過敏原不能同時選擇「無」和其他選項", "", "", None, "" | |
| # 檢查醫療狀況 | |
| if "無" in medical_conditions and len(medical_conditions) > 1: | |
| return "醫療狀況不能同時選擇「無」和其他選項", "", "", None, "" | |
| # 嘗試建立UserProfile並檢查身高體重範圍 | |
| user = UserProfile(age, gender, height, weight, activity_level) | |
| tdee = user.calculate_tdee() | |
| # 模擬食物識別 | |
| recognized_foods = [] | |
| for img in images: | |
| foods = random.sample(list(food_nutrition.keys()), k=random.randint(1, 3)) | |
| recognized_foods.extend(foods) | |
| # 計算營養總量 | |
| total_nutrients = { | |
| "蛋白質": sum(food_nutrition[food]["protein"] for food in recognized_foods), | |
| "碳水化合物": sum(food_nutrition[food]["carbs"] for food in recognized_foods), | |
| "脂肪": sum(food_nutrition[food]["fat"] for food in recognized_foods), | |
| "膳食纖維": sum(food_nutrition[food]["fiber"] for food in recognized_foods) | |
| } | |
| # 計算建議攝入量 | |
| recommended_nutrients = { | |
| "蛋白質": weight * 1.2, | |
| "碳水化合物": tdee * 0.5 / 4, | |
| "脂肪": tdee * 0.3 / 9, | |
| "膳食纖維": 25 | |
| } | |
| total_calories = sum(food_nutrition[food]["calories"] for food in recognized_foods) | |
| # 創建優化後的圖表 | |
| charts = create_nutrition_charts(total_nutrients, recommended_nutrients, total_calories) | |
| # 生成更簡潔的報告 | |
| report = f"""📊 營養攝入概要 | |
| -------------------------------- | |
| ⚡ 每日能量需求:{tdee:.0f} 大卡 | |
| 🍽️ 實際攝入:{total_calories:.0f} 大卡 | |
| 🥩 蛋白質:{total_nutrients['蛋白質']:.1f}g | |
| 🍚 碳水化合物:{total_nutrients['碳水化合物']:.1f}g | |
| 🥑 脂肪:{total_nutrients['脂肪']:.1f}g | |
| 🥬 膳食纖維:{total_nutrients['膳食纖維']:.1f}g""" | |
| # 過敏警告 | |
| allergy_warnings = "⚠️ 過敏原警告:\n" | |
| if allergies and "無" not in allergies: | |
| found_allergens = [] | |
| for food in recognized_foods: | |
| food_allergens = food_nutrition[food].get("allergens", []) | |
| found_allergens.extend([a for a in allergies if a in food_allergens]) | |
| if found_allergens: | |
| allergy_warnings += "\n".join(f"- 發現含有{allergen}的食物" for allergen in set(found_allergens)) | |
| else: | |
| allergy_warnings += "未發現任何過敏原" | |
| else: | |
| allergy_warnings += "未設置過敏原信息" | |
| # 醫療建議 | |
| medical_advice = "👨⚕️ 健康建議:\n" | |
| if medical_conditions: | |
| for condition in medical_conditions: | |
| if condition == "糖尿病": | |
| if total_nutrients['碳水化合物'] > recommended_nutrients['碳水化合物']: | |
| medical_advice += "- 碳水化合物攝入偏高,建議調整\n" | |
| elif condition == "高血壓": | |
| medical_advice += "- 建議選擇低鈉食物,控制鹽分攝入\n" | |
| else: | |
| medical_advice += "無特殊醫療注意事項" | |
| return report, allergy_warnings, recognized_foods, charts, medical_advice | |
| except ValueError as e: | |
| return f"錯誤:{str(e)}", "", "", None, "" # 返回錯誤訊息 | |
| except Exception as e: | |
| return f"分析過程出錯:{str(e)}", "", "", None, "" | |
| def create_interface(): | |
| with gr.Blocks(theme=gr.themes.Soft()) as iface: | |
| gr.Markdown("# 🥗 智能營養分析系統") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| images = gr.Gallery(label="上傳一日三餐的照片", height=200) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| age = gr.Number(label="年齡", minimum=1, maximum=120) | |
| height = gr.Number(label="身高(cm)", minimum=50, maximum=250) | |
| with gr.Column(scale=1): | |
| gender = gr.Radio(["男", "女"], label="性別") | |
| weight = gr.Number(label="體重(kg)", minimum=20, maximum=200) | |
| activity_level = gr.Dropdown( | |
| ["久坐", "輕度活動", "中度活動", "重度活動", "極重度活動"], | |
| label="活動程度" | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| diet_preference = gr.Radio( | |
| ["一般", "素食", "低碳", "高蛋白", "低脂", "無糖"], | |
| label="飲食偏好" | |
| ) | |
| health_goal = gr.Radio( | |
| ["減肥", "增肌", "維持健康", "改善消化"], | |
| label="健康目標" | |
| ) | |
| with gr.Column(scale=1): | |
| allergies = gr.Radio( | |
| ["無", "花生", "海鮮", "乳製品", "麩質", "蛋", "豆類"], | |
| label="過敏原", | |
| type="value" # 新增這個參數 | |
| ) | |
| medical_conditions = gr.Radio( | |
| ["無", "糖尿病", "高血壓", "高血脂", "腎臟病"], | |
| label="醫療狀況", | |
| type="value" # 新增這個參數 | |
| ) | |
| with gr.Column(scale=1): | |
| charts_output = gr.Plot(label="營養分析圖表") | |
| report_output = gr.Textbox(label="營養分析報告", lines=6) | |
| with gr.Row(): | |
| allergy_warning_output = gr.Textbox(label="過敏警告", lines=3) | |
| medical_advice_output = gr.Textbox(label="醫療建議", lines=3) | |
| suggestions_output = gr.Textbox(label="個人化建議", lines=8) | |
| analyze_btn = gr.Button("開始分析", variant="primary") | |
| analyze_btn.click( | |
| fn=analyze_meals_advanced, | |
| inputs=[images, age, gender, height, weight, | |
| activity_level, diet_preference, health_goal, | |
| allergies, medical_conditions], | |
| outputs=[report_output, allergy_warning_output, | |
| medical_advice_output, charts_output, suggestions_output] | |
| ) | |
| return iface | |
| if __name__ == "__main__": | |
| iface = create_interface() | |
| iface.launch() |