Spaces:
Running
Running
File size: 11,457 Bytes
a608ddf |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 |
# 檔案路徑: app/services/weight_calculation_service.py
import logging
import numpy as np
from PIL import Image
import io
from typing import Dict, Any, List, Optional, Tuple
import torch
from transformers import SamModel, SamProcessor, pipeline
# 設置日誌
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 食物密度表 (g/cm³) - 常見食物的平均密度
FOOD_DENSITY_TABLE = {
"rice": 0.8, # 米飯
"fried_rice": 0.7, # 炒飯
"noodles": 0.6, # 麵條
"bread": 0.3, # 麵包
"meat": 1.0, # 肉類
"fish": 1.1, # 魚類
"vegetables": 0.4, # 蔬菜
"fruits": 0.8, # 水果
"soup": 1.0, # 湯類
"sushi": 0.9, # 壽司
"pizza": 0.6, # 披薩
"hamburger": 0.7, # 漢堡
"salad": 0.3, # 沙拉
"default": 0.8 # 預設密度
}
class WeightCalculationService:
def __init__(self):
"""初始化重量計算服務"""
self.sam_model = None
self.sam_processor = None
self.dpt_model = None
self._load_models()
def _load_models(self):
"""載入 SAM 和 DPT 模型"""
try:
# 載入 SAM 分割模型
logger.info("正在載入 SAM 分割模型...")
self.sam_model = SamModel.from_pretrained("facebook/sam-vit-base")
self.sam_processor = SamProcessor.from_pretrained("facebook/sam-vit-base")
# 載入 DPT 深度估計模型
logger.info("正在載入 DPT 深度估計模型...")
self.dpt_model = pipeline("depth-estimation", model="Intel/dpt-large")
logger.info("SAM 和 DPT 模型載入完成!")
except Exception as e:
logger.error(f"模型載入失敗: {str(e)}")
raise
def segment_food_area(self, image: Image.Image, bbox: List[float]) -> np.ndarray:
"""
使用 SAM 分割食物區域
Args:
image: PIL Image 物件
bbox: 邊界框 [x1, y1, x2, y2]
Returns:
np.ndarray: 食物區域的遮罩
"""
try:
# 使用 SAM 進行分割
inputs = self.sam_processor(image, input_boxes=[bbox], return_tensors="pt")
with torch.no_grad():
outputs = self.sam_model(**inputs)
# 取得分割遮罩
masks_tensor = self.sam_processor.image_processor.post_process_masks(
outputs.pred_masks.sigmoid(),
inputs["original_sizes"],
inputs["reshaped_input_sizes"]
)[0]
# 選擇最大的遮罩
mask = masks_tensor[0].squeeze().cpu().numpy().astype(bool)
logger.info(f"SAM 分割完成,遮罩大小: {mask.shape}")
return mask
except Exception as e:
logger.error(f"SAM 分割失敗: {str(e)}")
# 回傳預設遮罩
return np.ones((image.height, image.width), dtype=bool)
def estimate_depth(self, image: Image.Image) -> np.ndarray:
"""
使用 DPT 進行深度估計
Args:
image: PIL Image 物件
Returns:
np.ndarray: 深度圖
"""
try:
# 使用 DPT 進行深度估計
depth_result = self.dpt_model(image)
depth_map = depth_result["depth"]
logger.info(f"DPT 深度估計完成,深度圖大小: {depth_map.shape}")
return np.array(depth_map)
except Exception as e:
logger.error(f"DPT 深度估計失敗: {str(e)}")
# 回傳預設深度圖
return np.ones((image.height, image.width))
def calculate_volume_and_weight(self,
mask: np.ndarray,
depth_map: np.ndarray,
food_name: str,
pixel_ratio: float) -> Tuple[float, float, float]:
"""
計算體積和重量
Args:
mask: 食物區域遮罩
depth_map: 深度圖
food_name: 食物名稱
pixel_ratio: 像素比例 (cm/pixel)
Returns:
Tuple[float, float, float]: (重量, 信心度, 誤差範圍)
"""
try:
# 計算食物區域的像素數量
food_pixels = np.sum(mask)
logger.info(f"重量計算開始 - 食物: {food_name}, 像素數量: {food_pixels}")
# 計算食物區域的平均深度
food_depth_values = depth_map[mask]
if len(food_depth_values) > 0:
food_depth = np.mean(food_depth_values)
depth_variance = np.var(food_depth_values)
else:
food_depth = 1.0
depth_variance = 0.0
logger.info(f"深度分析 - 平均深度: {food_depth:.4f}, 深度變異: {depth_variance:.4f}")
# 計算實際面積 (cm²)
area_cm2 = food_pixels * (pixel_ratio ** 2)
logger.info(f"面積計算 - 像素比例: {pixel_ratio:.4f}, 實際面積: {area_cm2:.2f} cm²")
# 動態調整形狀因子 (基於深度資訊)
if depth_variance > 0:
# 深度變異大,表示食物較立體
shape_factor = np.clip(0.6 + (depth_variance * 0.2), 0.3, 0.8)
else:
# 深度變異小,表示食物較扁平
shape_factor = np.clip(0.4 + (food_depth * 0.2), 0.2, 0.7)
logger.info(f"形狀因子 - 動態調整: {shape_factor:.4f}")
# 計算體積 (cm³)
volume_cm3 = shape_factor * (area_cm2 ** 1.5)
logger.info(f"體積計算 - 估算體積: {volume_cm3:.2f} cm³")
# 取得食物密度
density = self._get_food_density(food_name)
logger.info(f"密度查詢 - 食物: {food_name}, 密度: {density} g/cm³")
# 計算重量 (g)
weight = volume_cm3 * density
logger.info(f"重量計算 - 原始重量: {weight:.2f} g")
# 合理性檢查和調整
if weight > 2000: # 超過 2kg
logger.warning(f"重量 {weight:.2f}g 過高,進行調整")
weight = min(weight, 2000)
elif weight < 10: # 少於 10g
logger.warning(f"重量 {weight:.2f}g 過低,進行調整")
weight = max(weight, 10)
# 計算信心度和誤差範圍
confidence = self._calculate_confidence(pixel_ratio, depth_variance, food_pixels)
error_range = self._calculate_error_range(confidence)
logger.info(f"最終結果 - 重量: {weight:.2f}g, 信心度: {confidence:.2f}, 誤差範圍: ±{error_range*100:.1f}%")
return weight, confidence, error_range
except Exception as e:
logger.error(f"重量計算失敗: {str(e)}")
return 150.0, 0.3, 0.5 # 預設值
def _get_food_density(self, food_name: str) -> float:
"""根據食物名稱取得密度"""
food_name_lower = food_name.lower()
# 關鍵字匹配
for keyword, density in FOOD_DENSITY_TABLE.items():
if keyword in food_name_lower:
return density
return FOOD_DENSITY_TABLE["default"]
def _calculate_confidence(self, pixel_ratio: float, depth_variance: float, food_pixels: int) -> float:
"""計算信心度"""
# 基礎信心度
base_confidence = 0.6
# 像素比例影響 (比例越合理,信心度越高)
if 0.005 <= pixel_ratio <= 0.05:
base_confidence += 0.2
elif 0.001 <= pixel_ratio <= 0.1:
base_confidence += 0.1
# 深度變異影響 (適中的變異表示好的深度估計)
if 0.01 <= depth_variance <= 0.1:
base_confidence += 0.1
elif depth_variance > 0.5:
base_confidence -= 0.1
# 像素數量影響 (適中的像素數量表示好的分割)
if 1000 <= food_pixels <= 100000:
base_confidence += 0.1
elif food_pixels < 100:
base_confidence -= 0.2
return np.clip(base_confidence, 0.1, 0.95)
def _calculate_error_range(self, confidence: float) -> float:
"""根據信心度計算誤差範圍"""
# 信心度越高,誤差範圍越小
if confidence >= 0.8:
return 0.1 # ±10%
elif confidence >= 0.6:
return 0.2 # ±20%
elif confidence >= 0.4:
return 0.3 # ±30%
else:
return 0.5 # ±50%
# 全域服務實例
weight_calculation_service = WeightCalculationService()
def calculate_food_weight(image_bytes: bytes,
food_name: str,
pixel_ratio: float,
bbox: Optional[List[float]] = None) -> Dict[str, Any]:
"""
計算食物重量的主函數
Args:
image_bytes: 圖片二進位數據
food_name: 食物名稱
pixel_ratio: 像素比例
bbox: 可選的邊界框,如果沒有提供則使用整個圖片
Returns:
Dict: 包含重量計算結果的字典
"""
try:
image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
# 如果沒有提供邊界框,使用整個圖片
if bbox is None:
bbox = [0, 0, image.width, image.height]
logger.info(f"開始計算 {food_name} 的重量,使用邊界框: {bbox}")
# 1. 使用 SAM 分割食物區域
mask = weight_calculation_service.segment_food_area(image, bbox)
# 2. 使用 DPT 進行深度估計
depth_map = weight_calculation_service.estimate_depth(image)
# 3. 計算體積和重量
weight, confidence, error_range = weight_calculation_service.calculate_volume_and_weight(
mask, depth_map, food_name, pixel_ratio
)
# 4. 計算誤差範圍
weight_min = weight * (1 - error_range)
weight_max = weight * (1 + error_range)
return {
"food_name": food_name,
"estimated_weight": round(weight, 1),
"weight_confidence": confidence,
"weight_error_range": [round(weight_min, 1), round(weight_max, 1)],
"pixel_ratio": pixel_ratio,
"calculation_method": "SAM+DPT",
"success": True
}
except Exception as e:
logger.error(f"重量計算主流程失敗: {str(e)}")
return {
"food_name": food_name,
"estimated_weight": 150.0,
"weight_confidence": 0.3,
"weight_error_range": [100.0, 200.0],
"pixel_ratio": pixel_ratio,
"calculation_method": "SAM+DPT",
"success": False,
"error": str(e)
} |