Spaces:
Sleeping
Sleeping
# 檔案路徑: 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) |