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()