EduTechTeam's picture
Create app.py
0d6e1d4 verified
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()