health-assistant / app /services /integrated_food_analysis_service.py
yuting111222's picture
Update health assistant minimal with new services and improvements
a608ddf
# 檔案路徑: app/services/integrated_food_analysis_service.py
import logging
import numpy as np
from PIL import Image
import io
from typing import Dict, Any, List, Optional, Tuple
from datetime import datetime
# 導入各個服務
from .ai_service import classify_food_image
from .reference_detection_service import detect_reference_objects_from_image
from .weight_calculation_service import calculate_food_weight
from .nutrition_api_service import fetch_nutrition_data
# 設置日誌
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class IntegratedFoodAnalysisService:
def __init__(self):
"""初始化整合食物分析服務"""
logger.info("初始化整合食物分析服務...")
def analyze_food_image(self, image_bytes: bytes, debug: bool = False) -> Dict[str, Any]:
"""
整合食物分析主函數
新架構流程:
1. FOOD101 模型判斷食物
2. YOLO 主要判斷參考物在哪、大小為何
3. 再利用 SAM+DPT 去計算可能的重量
4. 再利用重量去乘上 USDA 每100克的數值
Args:
image_bytes: 圖片二進位數據
debug: 是否啟用調試模式
Returns:
Dict: 完整的分析結果
"""
try:
logger.info("=== 開始整合食物分析 ===")
start_time = datetime.now()
# 將 bytes 轉換為 PIL Image
image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
logger.info(f"圖片載入完成,尺寸: {image.size}")
# === 第一層:FOOD101 模型判斷食物 ===
logger.info("--- 第一層:FOOD101 食物識別 ---")
food_name = classify_food_image(image_bytes)
logger.info(f"FOOD101 識別結果: {food_name}")
if food_name.startswith("Error") or food_name == "Unknown":
return self._create_error_response("食物識別失敗", food_name)
# === 第二層:YOLO 判斷參考物 ===
logger.info("--- 第二層:YOLO 參考物偵測 ---")
reference_objects, pixel_ratio = detect_reference_objects_from_image(image_bytes)
if not reference_objects:
logger.warning("未偵測到參考物,使用預設像素比例")
pixel_ratio = 0.01 # 預設比例
best_reference = reference_objects[0] if reference_objects else None
logger.info(f"參考物偵測結果: {len(reference_objects)} 個參考物")
if best_reference:
logger.info(f"最佳參考物: {best_reference['label']}, 信心度: {best_reference['confidence']:.2f}")
logger.info(f"像素比例: {pixel_ratio:.4f} cm/pixel")
# === 第三層:SAM+DPT 重量計算 ===
logger.info("--- 第三層:SAM+DPT 重量計算 ---")
weight_result = calculate_food_weight(
image_bytes=image_bytes,
food_name=food_name,
pixel_ratio=pixel_ratio,
bbox=None # 使用整個圖片
)
if not weight_result.get("success", False):
logger.error("重量計算失敗")
return self._create_error_response("重量計算失敗", weight_result.get("error", "未知錯誤"))
estimated_weight = weight_result["estimated_weight"]
weight_confidence = weight_result["weight_confidence"]
weight_error_range = weight_result["weight_error_range"]
logger.info(f"重量計算結果: {estimated_weight}g, 信心度: {weight_confidence:.2f}")
# === 第四層:USDA API 營養查詢 ===
logger.info("--- 第四層:USDA API 營養查詢 ---")
nutrition_info = fetch_nutrition_data(food_name)
if nutrition_info is None:
logger.warning("USDA API 查詢失敗,使用預設營養值")
nutrition_info = self._get_default_nutrition(food_name)
# === 第五層:根據重量調整營養素 ===
logger.info("--- 第五層:重量調整營養素 ---")
weight_ratio = estimated_weight / 100 # 每100克的營養值
adjusted_nutrition = {}
for nutrient, value in nutrition_info.items():
if nutrient not in ["food_name", "chinese_name"]:
adjusted_nutrition[nutrient] = round(value * weight_ratio, 1)
logger.info(f"營養調整完成,重量比例: {weight_ratio:.2f}")
# === 生成分析報告 ===
analysis_time = (datetime.now() - start_time).total_seconds()
result = {
"success": True,
"analysis_time": round(analysis_time, 2),
"food_analysis": {
"food_name": food_name,
"recognition_method": "FOOD101",
"confidence": 0.95 # FOOD101 通常有很高的準確度
},
"reference_analysis": {
"detected_objects": reference_objects,
"best_reference": best_reference,
"pixel_ratio": pixel_ratio,
"detection_method": "YOLO"
},
"weight_analysis": {
"estimated_weight": estimated_weight,
"weight_confidence": weight_confidence,
"weight_error_range": weight_error_range,
"calculation_method": "SAM+DPT",
"reference_object": best_reference["label"] if best_reference else None
},
"nutrition_analysis": {
"base_nutrition": nutrition_info, # 每100克的營養值
"adjusted_nutrition": adjusted_nutrition, # 根據重量調整的營養值
"data_source": "USDA API",
"weight_ratio": weight_ratio
},
"summary": {
"total_calories": adjusted_nutrition.get("calories", 0),
"total_protein": adjusted_nutrition.get("protein", 0),
"total_carbs": adjusted_nutrition.get("carbs", 0),
"total_fat": adjusted_nutrition.get("fat", 0),
"health_score": self._calculate_health_score(adjusted_nutrition)
},
"architecture": {
"layer_1": "FOOD101 (食物識別)",
"layer_2": "YOLO (參考物偵測)",
"layer_3": "SAM+DPT (重量計算)",
"layer_4": "USDA API (營養查詢)",
"layer_5": "重量調整 (營養計算)"
}
}
logger.info("=== 整合食物分析完成 ===")
return result
except Exception as e:
logger.error(f"整合食物分析失敗: {str(e)}")
return self._create_error_response("整合分析失敗", str(e))
def _create_error_response(self, error_type: str, error_message: str) -> Dict[str, Any]:
"""創建錯誤回應"""
return {
"success": False,
"error_type": error_type,
"error_message": error_message,
"timestamp": datetime.now().isoformat()
}
def _get_default_nutrition(self, food_name: str) -> Dict[str, Any]:
"""取得預設營養值"""
default_nutrition = {
"food_name": food_name,
"calories": 100,
"protein": 5,
"fat": 2,
"carbs": 15,
"fiber": 2,
"sugar": 1,
"sodium": 200
}
return default_nutrition
def _calculate_health_score(self, nutrition: Dict[str, float]) -> int:
"""計算健康評分"""
score = 100
# 熱量評分
calories = nutrition.get("calories", 0)
if calories > 400:
score -= 20
elif calories > 300:
score -= 10
# 脂肪評分
fat = nutrition.get("fat", 0)
if fat > 20:
score -= 15
elif fat > 15:
score -= 8
# 蛋白質評分
protein = nutrition.get("protein", 0)
if protein > 15:
score += 10
elif protein < 5:
score -= 10
# 鈉含量評分
sodium = nutrition.get("sodium", 0)
if sodium > 800:
score -= 15
elif sodium > 600:
score -= 8
return max(0, min(100, score))
# 全域服務實例
integrated_service = IntegratedFoodAnalysisService()
def analyze_food_image_integrated(image_bytes: bytes, debug: bool = False) -> Dict[str, Any]:
"""
整合食物分析的外部接口
Args:
image_bytes: 圖片二進位數據
debug: 是否啟用調試模式
Returns:
Dict: 完整的分析結果
"""
return integrated_service.analyze_food_image(image_bytes, debug)