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