Enterwar99 commited on
Commit
fe7534d
·
verified ·
1 Parent(s): 0a1e45a

Update api_app.py

Browse files
Files changed (1) hide show
  1. api_app.py +188 -68
api_app.py CHANGED
@@ -8,7 +8,7 @@ import torch.nn as nn
8
  import io
9
  import numpy as np
10
  import os
11
- from typing import List, Dict, Any
12
  import logging # Dodajemy import modułu logging
13
  import cv2 # Dodajemy OpenCV
14
  import base64 # Dodajemy base64
@@ -17,6 +17,7 @@ import base64 # Dodajemy base64
17
  from pytorch_grad_cam import GradCAMPlusPlus
18
  from pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget
19
  from huggingface_hub import hf_hub_download # Do pobierania modelu z Huba
 
20
 
21
  # --- Konfiguracja Logowania ---
22
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
@@ -31,6 +32,14 @@ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
31
  IMAGENET_MEAN = [0.485, 0.456, 0.406]
32
  IMAGENET_STD = [0.229, 0.224, 0.225]
33
 
 
 
 
 
 
 
 
 
34
  # Globalne zmienne dla modelu i transformacji
35
  model_instance = None
36
  transform_pipeline = None
@@ -84,10 +93,57 @@ def initialize_model():
84
  transform_pipeline = transforms.Compose([
85
  transforms.Resize((224, 224)),
86
  transforms.ToTensor(),
87
- transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD)
88
  ])
89
  logger.info(f"Model BI-RADS classifier initialized successfully on device: {DEVICE}")
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  # --- Funkcja do tworzenia obrazu z nało��oną mapą Grad-CAM (zaadaptowana z app.py) ---
92
  def create_grad_cam_overlay_image(original_pil_image: Image.Image, grayscale_cam: np.ndarray, birads_category: int, transparency: float = 0.5) -> Image.Image:
93
  """Tworzy obraz PIL z nałożoną mapą Grad-CAM."""
@@ -127,10 +183,60 @@ def create_grad_cam_overlay_image(original_pil_image: Image.Image, grayscale_cam
127
  logger.info("Obraz Grad-CAM overlay pomyślnie utworzony.")
128
  return Image.fromarray(final_image_np)
129
  except Exception as e:
130
- logger.error(f"Błąd podczas tworzenia obrazu Grad-CAM overlay: {e}", exc_info=True)
131
  return None
132
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  # --- Aplikacja FastAPI ---
 
 
 
 
 
 
 
 
 
 
 
134
  app = FastAPI(title="BI-RADS Mammography Classification API")
135
 
136
  @app.on_event("startup")
@@ -139,7 +245,7 @@ async def startup_event():
139
  logger.info("Rozpoczynanie eventu startup aplikacji FastAPI.")
140
  initialize_model()
141
 
142
- @app.post("/predict/", response_model=List[Dict[str, Any]])
143
  async def predict_image(file: UploadFile = File(...)):
144
  """
145
  Endpoint do klasyfikacji obrazu mammograficznego.
@@ -156,79 +262,93 @@ async def predict_image(file: UploadFile = File(...)):
156
  try:
157
  logger.info(f"[RequestID: {request_id}] Odczytywanie i przetwarzanie wgranego pliku...")
158
  contents = await file.read()
159
- image = Image.open(io.BytesIO(contents)).convert("RGB")
160
- logger.info(f"[RequestID: {request_id}] Plik obrazu pomyślnie odczytany i przekonwertowany do RGB.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  except Exception as e:
162
  logger.error(f"[RequestID: {request_id}] Błąd podczas odczytu pliku obrazu: {e}", exc_info=True)
163
  raise HTTPException(status_code=400, detail=f"Nie można odczytać pliku obrazu: {e}")
164
-
165
- # Preprocessing
166
  logger.info(f"[RequestID: {request_id}] Rozpoczynanie preprocessingu obrazu...")
167
  input_tensor = transform_pipeline(image).unsqueeze(0).to(DEVICE)
168
  logger.info(f"[RequestID: {request_id}] Preprocessing zakończony. Kształt tensora wejściowego: {input_tensor.shape}")
169
 
170
- # Inference
171
- logger.info(f"[RequestID: {request_id}] Rozpoczynanie inferencji modelu...")
172
- with torch.no_grad():
173
- model_outputs = model_instance(input_tensor)
174
- logger.info(f"[RequestID: {request_id}] Inferencja zakończona.")
175
- # Postprocessing
176
- probs = torch.nn.functional.softmax(model_outputs, dim=1)
177
- confidences, predicted_indices = torch.max(probs, 1)
178
-
179
  results = []
180
- for i in range(len(predicted_indices)): # Pętla na wypadek przyszłego batch processingu
181
- birads_category = predicted_indices[i].item() + 1
182
- logger.info(f"[RequestID: {request_id}] Przewidziana kategoria BI-RADS: {birads_category}, Pewność: {confidences[i].item():.4f}")
183
- confidence = confidences[i].item()
184
- interpretation = interpretations_dict.get(birads_category, "Nieznana klasyfikacja")
185
-
186
- all_class_probs_tensor = probs[i].cpu().numpy()
187
- class_probabilities = {str(j+1): float(all_class_probs_tensor[j]) for j in range(len(all_class_probs_tensor))}
188
-
189
- # Generowanie Grad-CAM
190
- grad_cam_map_serialized = None
191
- grad_cam_image_base64 = None
192
- logger.info(f"[RequestID: {request_id}] Rozpoczynanie generowania Grad-CAM dla kategorii {birads_category}...")
193
- try:
194
- # model_instance is already in eval mode from initialize_model()
195
- # pytorch-grad-cam typically handles necessary gradient contexts.
196
- target_layers = [model_instance.layer4[-1]] # Dla ResNet-18
197
- cam_algorithm = GradCAMPlusPlus(model=model_instance, target_layers=target_layers)
198
-
199
- current_input_tensor_for_cam = input_tensor[i].unsqueeze(0).clone().detach().requires_grad_(True)
200
- targets_for_cam = [ClassifierOutputTarget(predicted_indices[i].item())]
201
-
202
- grayscale_cam = cam_algorithm(input_tensor=current_input_tensor_for_cam, targets=targets_for_cam)
203
-
204
- if grayscale_cam is not None:
205
- grad_cam_map_np = grayscale_cam[0, :]
206
- logger.info(f"[RequestID: {request_id}] Grad-CAM wygenerowany pomyślnie.")
207
-
208
- # Tworzenie obrazu z nałożoną mapą Grad-CAM
209
- overlay_image_pil = create_grad_cam_overlay_image(original_pil_image=image, # oryginalny obraz PIL
210
- grayscale_cam=grad_cam_map_np,
211
- birads_category=birads_category)
212
- if overlay_image_pil:
213
- buffered = io.BytesIO()
214
- overlay_image_pil.save(buffered, format="PNG") # Zapisz jako PNG
215
- grad_cam_image_base64 = base64.b64encode(buffered.getvalue()).decode('utf-8')
216
- logger.info(f"[RequestID: {request_id}] Obraz Grad-CAM overlay zakodowany do base64.")
217
- else:
218
- logger.warning(f"[RequestID: {request_id}] Nie udało się utworzyć obrazu Grad-CAM overlay.")
 
 
 
219
  else:
220
- logger.warning(f"[RequestID: {request_id}] Wygenerowany Grad-CAM jest None.")
221
- except Exception as e:
222
- logger.error(f"[RequestID: {request_id}] Błąd podczas generowania Grad-CAM w API: {e}", exc_info=True)
223
-
224
- results.append({
225
- "birads": birads_category,
226
- "confidence": confidence,
227
- "interpretation": interpretation,
228
- "class_probabilities": class_probabilities,
229
- "grad_cam_image_base64": grad_cam_image_base64 # Dodajemy obraz base64
230
- })
231
-
232
  logger.info(f"[RequestID: {request_id}] Przetwarzanie żądania /predict/ zakończone. Zwracam wyniki.")
233
  return JSONResponse(content=results)
234
 
 
8
  import io
9
  import numpy as np
10
  import os
11
+ from typing import List, Dict, Any, Optional # Dodano Optional
12
  import logging # Dodajemy import modułu logging
13
  import cv2 # Dodajemy OpenCV
14
  import base64 # Dodajemy base64
 
17
  from pytorch_grad_cam import GradCAMPlusPlus
18
  from pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget
19
  from huggingface_hub import hf_hub_download # Do pobierania modelu z Huba
20
+ from pydantic import BaseModel # Dodano Pydantic
21
 
22
  # --- Konfiguracja Logowania ---
23
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
32
  IMAGENET_MEAN = [0.485, 0.456, 0.406]
33
  IMAGENET_STD = [0.229, 0.224, 0.225]
34
 
35
+ # --- Konfigurowalne progi (ze zmiennych środowiskowych z wartościami domyślnymi) ---
36
+ COLORFULNESS_THRESHOLD = float(os.environ.get("COLORFULNESS_THRESHOLD", 15))
37
+ UNIFORMITY_THRESHOLD = float(os.environ.get("UNIFORMITY_THRESHOLD", 10))
38
+ ASPECT_RATIO_MIN = float(os.environ.get("ASPECT_RATIO_MIN", 0.4))
39
+ ASPECT_RATIO_MAX = float(os.environ.get("ASPECT_RATIO_MAX", 2.5))
40
+ MC_DROPOUT_SAMPLES = int(os.environ.get("MC_DROPOUT_SAMPLES", 25))
41
+ UNCERTAINTY_THRESHOLD_STD = float(os.environ.get("UNCERTAINTY_THRESHOLD_STD", 0.08))
42
+
43
  # Globalne zmienne dla modelu i transformacji
44
  model_instance = None
45
  transform_pipeline = None
 
93
  transform_pipeline = transforms.Compose([
94
  transforms.Resize((224, 224)),
95
  transforms.ToTensor(),
96
+ transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD) # Upewnij się, że IMAGENET_MEAN i IMAGENET_STD są zdefiniowane
97
  ])
98
  logger.info(f"Model BI-RADS classifier initialized successfully on device: {DEVICE}")
99
 
100
+ # --- Funkcja do predykcji z kwantyfikacją niepewności (MC Dropout) ---
101
+ def predict_with_mc_dropout(current_model_instance, input_tensor_on_device): # Usunięto domyślne wartości, będą brane z globalnych zmiennych
102
+ """
103
+ Wykonuje predykcję z użyciem Monte Carlo Dropout do oszacowania niepewności.
104
+ """
105
+ logger.info(f"Performing MC Dropout with {MC_DROPOUT_SAMPLES} samples. Uncertainty threshold (std): {UNCERTAINTY_THRESHOLD_STD}")
106
+
107
+ original_mode_is_training = current_model_instance.training
108
+ current_model_instance.train() # Włącz warstwy dropout
109
+
110
+ all_probs_list = []
111
+ with torch.no_grad(): # Gradienty nie są potrzebne do samego przejścia w przód
112
+ for _ in range(n_samples):
113
+ output = current_model_instance(input_tensor_on_device) # Użyj MC_DROPOUT_SAMPLES
114
+ probs_tensor = torch.nn.functional.softmax(output, dim=1)
115
+ all_probs_list.append(probs_tensor.cpu().numpy())
116
+
117
+ # Przywróć oryginalny tryb modelu
118
+ if not original_mode_is_training:
119
+ current_model_instance.eval()
120
+
121
+ predictions_stack = np.vstack(all_probs_list) # Kształt: (n_samples, num_classes)
122
+ mean_probabilities = np.mean(predictions_stack, axis=0) # Kształt: (num_classes,)
123
+ std_dev_probabilities = np.std(predictions_stack, axis=0) # Kształt: (num_classes,)
124
+
125
+ predicted_class_index = np.argmax(mean_probabilities)
126
+ confidence_in_predicted_class = mean_probabilities[predicted_class_index]
127
+
128
+ # Użyj średniej odchyleń standardowych prawdopodobieństw wszystkich klas jako metryki niepewności
129
+ uncertainty_metric = np.mean(std_dev_probabilities)
130
+
131
+ is_uncertain = uncertainty_metric > UNCERTAINTY_THRESHOLD_STD # Użyj UNCERTAINTY_THRESHOLD_STD
132
+ logger.info(f"MC Dropout Results: Predicted Index: {predicted_class_index}, Confidence: {confidence_in_predicted_class:.4f}, Uncertainty (avg_std): {uncertainty_metric:.4f}, Is Uncertain: {is_uncertain}")
133
+
134
+ birads_category_if_confident = predicted_class_index + 1
135
+ base_result = {
136
+ "birads": birads_category_if_confident if not is_uncertain else None,
137
+ "confidence": float(confidence_in_predicted_class) if not is_uncertain else None,
138
+ "interpretation": interpretations_dict.get(birads_category_if_confident, "Nieznana klasyfikacja") if not is_uncertain \
139
+ else f"Model jest niepewny co do tego obrazu (niepewność: {uncertainty_metric:.4f}). Sprawdź jakość i typ obrazu. Może to być obraz spoza domeny medycznej.",
140
+ "class_probabilities": {str(j + 1): float(mean_probabilities[j]) for j in range(len(mean_probabilities))},
141
+ "grad_cam_image_base64": None, # Zostanie wypełnione później, jeśli pewne
142
+ "error": "High prediction uncertainty" if is_uncertain else None,
143
+ "details": f"Uncertainty metric ({uncertainty_metric:.4f}) {'przekroczyła' if is_uncertain else 'jest w granicach'} progu ({UNCERTAINTY_THRESHOLD_STD})."
144
+ }
145
+ return base_result, predicted_class_index # Zwróć również indeks dla Grad-CAM
146
+
147
  # --- Funkcja do tworzenia obrazu z nało��oną mapą Grad-CAM (zaadaptowana z app.py) ---
148
  def create_grad_cam_overlay_image(original_pil_image: Image.Image, grayscale_cam: np.ndarray, birads_category: int, transparency: float = 0.5) -> Image.Image:
149
  """Tworzy obraz PIL z nałożoną mapą Grad-CAM."""
 
183
  logger.info("Obraz Grad-CAM overlay pomyślnie utworzony.")
184
  return Image.fromarray(final_image_np)
185
  except Exception as e:
186
+ logger.error(f"Błąd podczas tworzenia obrazu Grad-CAM overlay: {e}", exc_info=True)
187
  return None
188
 
189
+ # --- Funkcja do heurystycznych testów OOD ---
190
+ def run_heuristic_ood_checks(pil_image: Image.Image, request_id: str) -> bool:
191
+ """
192
+ Wykonuje zestaw prostych heurystyk do wykrywania obrazów spoza dystrybucji.
193
+ Zwraca True, jeśli obraz przeszedł testy (prawdopodobnie jest OK), False jeśli nie.
194
+ """
195
+ logger.info(f"[RequestID: {request_id}] Uruchamianie heurystycznych testów OOD...")
196
+ width, height = pil_image.size
197
+
198
+ # Heurystyka 1: Sprawdzenie proporcji obrazu
199
+ aspect_ratio = width / height
200
+ if not (ASPECT_RATIO_MIN < aspect_ratio < ASPECT_RATIO_MAX): # Użyj zmiennych konfiguracyjnych
201
+ logger.warning(f"[RequestID: {request_id}] Heurystyka OOD: Nietypowe proporcje obrazu: {aspect_ratio:.2f}. Odrzucam.")
202
+ return False
203
+
204
+ # Heurystyka 2: Analiza "kolorowości" (dla obrazów RGB)
205
+ # Mammografie są w skali szarości; jeśli obraz jest kolorowy, ta metryka będzie wysoka.
206
+ # Zakładamy, że pil_image może być już w trybie RGB lub zostanie do niego skonwertowany.
207
+ # Jeśli pil_image jest w trybie 'L', można by tę heurystykę pominąć lub dostosować.
208
+ # Dla obrazu w skali szarości skonwertowanego do RGB, R=G=B, więc std_per_pixel_across_channels będzie bliskie 0.
209
+ img_rgb_for_color_check = pil_image.convert('RGB') # Upewnijmy się, że pracujemy na RGB dla tej heurystyki
210
+ img_np_rgb = np.array(img_rgb_for_color_check)
211
+ std_per_pixel_across_channels = np.std(img_np_rgb, axis=2) # Odch. std. dla każdego piksela po kanałach R,G,B
212
+ mean_std_across_channels = np.mean(std_per_pixel_across_channels)
213
+ if mean_std_across_channels > COLORFULNESS_THRESHOLD: # Użyj zmiennej konfiguracyjnej
214
+ logger.warning(f"[RequestID: {request_id}] Heurystyka OOD: Obraz wydaje się zbyt kolorowy. Średnie odch. std. między kanałami: {mean_std_across_channels:.2f}. Odrzucam.")
215
+ return False
216
+
217
+ # Heurystyka 3: Sprawdzenie, czy obraz nie jest prawie jednolity (dominująca jasność/ciemność)
218
+ # Konwertujemy do skali szarości dla tej analizy
219
+ gray_image = pil_image.convert('L')
220
+ std_dev_intensity = np.std(np.array(gray_image))
221
+ if std_dev_intensity < UNIFORMITY_THRESHOLD: # Użyj zmiennej konfiguracyjnej
222
+ logger.warning(f"[RequestID: {request_id}] Heurystyka OOD: Obraz wydaje się zbyt jednolity (mało zróżnicowania jasności). Odch. std. intensywności: {std_dev_intensity:.2f}. Odrzucam.")
223
+ return False
224
+
225
+ logger.info(f"[RequestID: {request_id}] Heurystyczne testy OOD zakończone pomyślnie. Obraz wygląda na potencjalnie poprawny.")
226
+ return True
227
+
228
  # --- Aplikacja FastAPI ---
229
+ # --- Definicja modelu odpowiedzi Pydantic ---
230
+ class PredictionResult(BaseModel):
231
+ birads: Optional[int] = None
232
+ confidence: Optional[float] = None
233
+ interpretation: str
234
+ class_probabilities: Dict[str, float]
235
+ grad_cam_image_base64: Optional[str] = None
236
+ error: Optional[str] = None
237
+ details: Optional[str] = None
238
+
239
+
240
  app = FastAPI(title="BI-RADS Mammography Classification API")
241
 
242
  @app.on_event("startup")
 
245
  logger.info("Rozpoczynanie eventu startup aplikacji FastAPI.")
246
  initialize_model()
247
 
248
+ @app.post("/predict/", response_model=List[PredictionResult]) # Użycie modelu Pydantic
249
  async def predict_image(file: UploadFile = File(...)):
250
  """
251
  Endpoint do klasyfikacji obrazu mammograficznego.
 
262
  try:
263
  logger.info(f"[RequestID: {request_id}] Odczytywanie i przetwarzanie wgranego pliku...")
264
  contents = await file.read()
265
+ # Wczytaj obraz, ale jeszcze nie konwertuj do RGB, aby heurystyki mogły działać na bardziej "surowych" danych
266
+ image_pil_original = Image.open(io.BytesIO(contents))
267
+ logger.info(f"[RequestID: {request_id}] Plik obrazu pomyślnie odczytany. Oryginalny tryb: {image_pil_original.mode}, Rozmiar: {image_pil_original.size}")
268
+
269
+ # --- Etap: Heurystyczne testy OOD (PRE-FILTR) ---
270
+ # Używamy .copy(), aby uniknąć potencjalnych modyfikacji oryginalnego obiektu image_pil_original
271
+ # przez funkcję run_heuristic_ood_checks, jeśli by takie wykonywała (np. konwersje inplace).
272
+ if not run_heuristic_ood_checks(image_pil_original.copy(), request_id):
273
+ # Heurystyki wykryły problem
274
+ return JSONResponse(
275
+ status_code=400, # Bad Request
276
+ content=[{ # API oczekuje listy wyników
277
+ "error": "Image does not appear to be a valid medical mammogram based on initial checks.",
278
+ "details": "Heurystyczne testy OOD nie powiodły się. Obraz odrzucony przed analizą przez model AI."
279
+ }]
280
+ )
281
+ image = image_pil_original.convert("RGB") # Teraz konwertuj do RGB dla modelu głównego
282
  except Exception as e:
283
  logger.error(f"[RequestID: {request_id}] Błąd podczas odczytu pliku obrazu: {e}", exc_info=True)
284
  raise HTTPException(status_code=400, detail=f"Nie można odczytać pliku obrazu: {e}")
285
+
286
+ # --- Preprocessing ---
287
  logger.info(f"[RequestID: {request_id}] Rozpoczynanie preprocessingu obrazu...")
288
  input_tensor = transform_pipeline(image).unsqueeze(0).to(DEVICE)
289
  logger.info(f"[RequestID: {request_id}] Preprocessing zakończony. Kształt tensora wejściowego: {input_tensor.shape}")
290
 
291
+ # --- Etap: Predykcja z kwantyfikacją niepewności (MC Dropout) ---
292
+ # Upewnij się, że model_instance, DEVICE, interpretations_dict są dostępne
293
+ mc_output_dict, predicted_idx_from_mc = predict_with_mc_dropout(
294
+ model_instance,
295
+ input_tensor,
296
+ # n_samples i uncertainty_threshold_std są teraz brane z globalnych zmiennych
297
+ )
298
+
 
299
  results = []
300
+ if mc_output_dict.get("error") == "High prediction uncertainty":
301
+ logger.warning(f"[RequestID: {request_id}] Wysoka niepewność predykcji: {mc_output_dict.get('details')}")
302
+ results.append(mc_output_dict) # Dodaj wynik z informacją o niepewności
303
+ return JSONResponse(content=results) # API oczekuje listy
304
+
305
+ # --- Jeśli predykcja jest pewna, kontynuuj z Grad-CAM ---
306
+ # mc_output_dict zawiera już 'birads', 'confidence', 'interpretation', 'class_probabilities'
307
+ birads_category_for_cam = mc_output_dict["birads"] # To jest już BI-RADS 1-5
308
+ # predicted_idx_from_mc to indeks 0-4
309
+
310
+ # Upewnij się, że model jest w trybie ewaluacji dla Grad-CAM
311
+ # Funkcja MC Dropout powinna go przywrócić, ale dla pewności:
312
+ model_instance.eval()
313
+
314
+ # Generowanie Grad-CAM
315
+ grad_cam_map_serialized = None # Zmienna zdefiniowana przed blokiem try
316
+ grad_cam_image_base64 = None # Zmienna zdefiniowana przed blokiem try
317
+ # Poprawka: Użyj birads_category_for_cam zamiast niezdefiniowanej birads_category
318
+ logger.info(f"[RequestID: {request_id}] Rozpoczynanie generowania Grad-CAM dla kategorii {birads_category_for_cam}...")
319
+ try:
320
+ # model_instance is already in eval mode from initialize_model()
321
+ target_layers = [model_instance.layer4[-1]]
322
+ cam_algorithm = GradCAMPlusPlus(model=model_instance, target_layers=target_layers)
323
+
324
+ # Tensor wejściowy dla CAM musi mieć requires_grad=True
325
+ current_input_tensor_for_cam = input_tensor.clone().detach().requires_grad_(True)
326
+ targets_for_cam = [ClassifierOutputTarget(predicted_idx_from_mc)] # Użyj indeksu z MC Dropout
327
+
328
+ grayscale_cam = cam_algorithm(input_tensor=current_input_tensor_for_cam, targets=targets_for_cam)
329
+ if grayscale_cam is not None:
330
+ grad_cam_map_np = grayscale_cam[0, :]
331
+ logger.info(f"[RequestID: {request_id}] Grad-CAM wygenerowany pomyślnie.")
332
+
333
+ # Tworzenie obrazu z nałożoną mapą Grad-CAM
334
+ overlay_image_pil = create_grad_cam_overlay_image(original_pil_image=image, # oryginalny obraz PIL
335
+ grayscale_cam=grad_cam_map_np,
336
+ birads_category=birads_category_for_cam)
337
+ if overlay_image_pil:
338
+ buffered = io.BytesIO()
339
+ overlay_image_pil.save(buffered, format="PNG") # Zapisz jako PNG
340
+ grad_cam_image_base64 = base64.b64encode(buffered.getvalue()).decode('utf-8')
341
+ logger.info(f"[RequestID: {request_id}] Obraz Grad-CAM overlay zakodowany do base64.")
342
  else:
343
+ logger.warning(f"[RequestID: {request_id}] Nie udało się utworzyć obrazu Grad-CAM overlay.")
344
+ else:
345
+ logger.warning(f"[RequestID: {request_id}] Wygenerowany Grad-CAM jest None.")
346
+ except Exception as e:
347
+ logger.error(f"[RequestID: {request_id}] Błąd podczas generowania Grad-CAM w API: {e}", exc_info=True)
348
+ # grad_cam_image_base64 pozostanie None
349
+
350
+ mc_output_dict["grad_cam_image_base64"] = grad_cam_image_base64 # Zaktualizuj słownik wynikowy
351
+ results.append(mc_output_dict) # Dodaj finalny wynik (pewny, z Grad-CAM lub bez)
 
 
 
352
  logger.info(f"[RequestID: {request_id}] Przetwarzanie żądania /predict/ zakończone. Zwracam wyniki.")
353
  return JSONResponse(content=results)
354