Update app.py
Browse files
app.py
CHANGED
|
@@ -160,105 +160,95 @@ def find_last_dense_layer(model):
|
|
| 160 |
raise ValueError("Aucune couche Dense trouvée dans le modèle.")
|
| 161 |
|
| 162 |
# ---- GRAD-CAM ----
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
current_predictions = None
|
| 166 |
-
|
| 167 |
-
# ---- Fonctions pour l'UI Gradio ----
|
| 168 |
-
def quick_predict_ui(image_pil):
|
| 169 |
-
global current_image, current_predictions
|
| 170 |
-
if image_pil is None:
|
| 171 |
-
return "Veuillez uploader une image.", None, None
|
| 172 |
try:
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
print("Somme après renorm:", ensemble_probs.sum())
|
| 187 |
-
|
| 188 |
-
top_class_idx = int(np.argmax(ensemble_probs))
|
| 189 |
-
top_class_name = CLASS_NAMES[top_class_idx]
|
| 190 |
-
global_diag = diagnosis_map[top_class_name]
|
| 191 |
-
|
| 192 |
-
# Conversion en pourcentages pour le DataFrame
|
| 193 |
-
confidences = {CLASS_NAMES[i]: float(ensemble_probs[i] * 100) for i in range(len(CLASS_NAMES))} # En pourcentages
|
| 194 |
-
df = pd.DataFrame.from_dict(confidences, orient='index', columns=['Probabilité']).reset_index().rename(columns={'index': 'Classe'})
|
| 195 |
-
df = df.sort_values(by='Probabilité', ascending=False)
|
| 196 |
-
|
| 197 |
-
# Ajout d'une colonne pour les étiquettes avec pourcentages
|
| 198 |
-
df['Pourcentage'] = df['Probabilité'].apply(lambda x: f"{x:.1f}%")
|
| 199 |
-
|
| 200 |
-
# Génération du graphique avec texte dynamique
|
| 201 |
-
fig = px.bar(df,
|
| 202 |
-
x="Classe",
|
| 203 |
-
y="Probabilité",
|
| 204 |
-
color="Probabilité",
|
| 205 |
-
color_continuous_scale=px.colors.sequential.Viridis,
|
| 206 |
-
title="Probabilités par classe",
|
| 207 |
-
text="Pourcentage") # Utilise la colonne Pourcentage pour les étiquettes
|
| 208 |
-
|
| 209 |
-
# Définir textposition comme une liste personnalisée
|
| 210 |
-
text_positions = []
|
| 211 |
-
for val in df['Probabilité']:
|
| 212 |
-
if val <= 10: # Si la probabilité est ≤ 10%, texte à l'extérieur
|
| 213 |
-
text_positions.append("outside")
|
| 214 |
-
else: # Sinon, texte à l'intérieur
|
| 215 |
-
text_positions.append("inside")
|
| 216 |
-
fig.update_traces(textposition=text_positions) # Appliquer les positions personnalisées
|
| 217 |
|
| 218 |
-
|
|
|
|
|
|
|
| 219 |
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
|
|
|
| 224 |
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
return None, 0
|
| 229 |
-
try:
|
| 230 |
-
ensemble_probs = current_predictions["ensemble"]
|
| 231 |
-
top_class_idx = int(np.argmax(ensemble_probs))
|
| 232 |
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
if model_resnet50 is not None: candidates.append(("resnet50", model_resnet50, current_predictions["resnet50"][top_class_idx]))
|
| 236 |
-
if model_densenet is not None: candidates.append(("densenet201", model_densenet, current_predictions["densenet201"][top_class_idx]))
|
| 237 |
|
| 238 |
-
|
| 239 |
-
|
|
|
|
|
|
|
|
|
|
| 240 |
|
| 241 |
-
|
| 242 |
-
progress = 0
|
| 243 |
-
yield None, progress # Initialisation
|
| 244 |
-
progress = 10
|
| 245 |
-
yield None, progress # Sélection du modèle
|
| 246 |
-
explainer_model_name, explainer_model, conf = max(candidates, key=lambda t: t[2])
|
| 247 |
-
explainer_layer = LAST_CONV_LAYERS.get(explainer_model_name)
|
| 248 |
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
yield None, progress # Début du calcul
|
| 253 |
-
gradcam_img = make_gradcam(current_image, explainer_model, explainer_layer, class_index=top_class_idx)
|
| 254 |
|
| 255 |
-
|
| 256 |
-
|
|
|
|
| 257 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
except Exception as e:
|
| 259 |
-
print(f"❌ Erreur dans
|
| 260 |
import traceback; traceback.print_exc()
|
| 261 |
-
|
| 262 |
|
| 263 |
# ---- GESTION ASYNCHRONE / ÉTAT ----
|
| 264 |
current_image = None
|
|
@@ -338,7 +328,7 @@ def generate_gradcam_ui():
|
|
| 338 |
if not candidates:
|
| 339 |
return None, 0
|
| 340 |
|
| 341 |
-
#
|
| 342 |
progress = 0
|
| 343 |
yield None, progress # Initialisation
|
| 344 |
progress = 10
|
|
@@ -358,7 +348,7 @@ def generate_gradcam_ui():
|
|
| 358 |
except Exception as e:
|
| 359 |
print(f"❌ Erreur dans generate_gradcam_ui: {e}")
|
| 360 |
import traceback; traceback.print_exc()
|
| 361 |
-
yield None, 0
|
| 362 |
|
| 363 |
# ---- INTERFACE GRADIO ----
|
| 364 |
example_paths = ["ISIC_0024627.jpg", "ISIC_0025539.jpg", "ISIC_0031410.jpg"]
|
|
|
|
| 160 |
raise ValueError("Aucune couche Dense trouvée dans le modèle.")
|
| 161 |
|
| 162 |
# ---- GRAD-CAM ----
|
| 163 |
+
def make_gradcam(image_pil, model, last_conv_layer_name, class_index):
|
| 164 |
+
if model is None: return np.array(image_pil)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
try:
|
| 166 |
+
input_size = model.input_shape[1:3]
|
| 167 |
+
bb_name = _guess_backbone_name(model)
|
| 168 |
+
print(f"Backbone détecté: {bb_name}")
|
| 169 |
+
|
| 170 |
+
if 'xception' in model.name.lower():
|
| 171 |
+
preprocessor = preprocess_xception
|
| 172 |
+
elif 'resnet50' in model.name.lower():
|
| 173 |
+
preprocessor = preprocess_resnet
|
| 174 |
+
elif 'densenet' in model.name.lower():
|
| 175 |
+
preprocessor = preprocess_densenet
|
| 176 |
+
else:
|
| 177 |
+
print("Préprocesseur non reconnu - utilisation de preprocess_densenet par défaut")
|
| 178 |
+
preprocessor = preprocess_densenet
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
|
| 180 |
+
img_np = np.array(image_pil.convert("RGB"))
|
| 181 |
+
img_resized = cv2.resize(img_np, input_size)
|
| 182 |
+
img_array_preprocessed = preprocessor(np.expand_dims(img_resized, axis=0))
|
| 183 |
|
| 184 |
+
try:
|
| 185 |
+
conv_layer = model.get_layer(last_conv_layer_name)
|
| 186 |
+
except ValueError:
|
| 187 |
+
print(f"Couche '{last_conv_layer_name}' non trouvée dans le modèle")
|
| 188 |
+
return img_resized
|
| 189 |
|
| 190 |
+
dense_layer = find_last_dense_layer(model)
|
| 191 |
+
|
| 192 |
+
grad_model = Model(model.inputs, [conv_layer.output, model.output])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
|
| 194 |
+
input_name = get_primary_input_name(model)
|
| 195 |
+
input_for_model = {input_name: img_array_preprocessed}
|
|
|
|
|
|
|
| 196 |
|
| 197 |
+
with tf.GradientTape() as tape:
|
| 198 |
+
last_conv_layer_output, preds = grad_model(input_for_model, training=False)
|
| 199 |
+
if isinstance(preds, list):
|
| 200 |
+
preds = preds[0]
|
| 201 |
+
class_channel = preds[:, int(class_index)]
|
| 202 |
|
| 203 |
+
grads = tape.gradient(class_channel, last_conv_layer_output)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
|
| 205 |
+
if grads is None:
|
| 206 |
+
print("⚠️ Gradients sont None - retour de l'image originale.")
|
| 207 |
+
return img_resized
|
|
|
|
|
|
|
| 208 |
|
| 209 |
+
if tf.reduce_any(tf.math.is_nan(grads)) or tf.reduce_any(tf.math.is_inf(grads)):
|
| 210 |
+
print("⚠️ Gradients contiennent des NaN/inf - retour de l'image originale.")
|
| 211 |
+
return img_resized
|
| 212 |
|
| 213 |
+
pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2))
|
| 214 |
+
last_conv_layer_output = last_conv_layer_output[0]
|
| 215 |
+
|
| 216 |
+
heatmap = last_conv_layer_output @ pooled_grads[..., tf.newaxis]
|
| 217 |
+
heatmap = tf.squeeze(heatmap)
|
| 218 |
+
|
| 219 |
+
heatmap = tf.maximum(heatmap, 0)
|
| 220 |
+
max_val = tf.math.reduce_max(heatmap)
|
| 221 |
+
|
| 222 |
+
if max_val == 0:
|
| 223 |
+
print("Heatmap max est 0 - création d'une heatmap neutre")
|
| 224 |
+
heatmap = tf.ones_like(heatmap) * 0.5
|
| 225 |
+
else:
|
| 226 |
+
heatmap = heatmap / max_val
|
| 227 |
+
|
| 228 |
+
heatmap_np = heatmap.numpy()
|
| 229 |
+
|
| 230 |
+
if heatmap_np.size == 0:
|
| 231 |
+
print("Heatmap vide - retour de l'image originale")
|
| 232 |
+
return img_resized
|
| 233 |
+
|
| 234 |
+
if np.any(np.isnan(heatmap_np)) or np.any(np.isinf(heatmap_np)):
|
| 235 |
+
print("Heatmap contient des NaN/inf après conversion - retour de l'image originale")
|
| 236 |
+
return img_resized
|
| 237 |
+
|
| 238 |
+
heatmap_np = np.clip(heatmap_np.astype(np.float32), 0, 1)
|
| 239 |
+
|
| 240 |
+
heatmap_resized = cv2.resize(heatmap_np, (img_resized.shape[1], img_resized.shape[0]))
|
| 241 |
+
heatmap_uint8 = np.uint8(255 * heatmap_resized)
|
| 242 |
+
heatmap_colored = cv2.applyColorMap(heatmap_uint8, cv2.COLORMAP_JET)
|
| 243 |
+
|
| 244 |
+
img_bgr = cv2.cvtColor(img_resized, cv2.COLOR_RGB2BGR)
|
| 245 |
+
superimposed_img = cv2.addWeighted(img_bgr, 0.6, heatmap_colored, 0.4, 0)
|
| 246 |
+
|
| 247 |
+
return cv2.cvtColor(superimposed_img, cv2.COLOR_BGR2RGB)
|
| 248 |
except Exception as e:
|
| 249 |
+
print(f"❌ Erreur inattendue dans make_gradcam: {e}")
|
| 250 |
import traceback; traceback.print_exc()
|
| 251 |
+
return np.array(image_pil)
|
| 252 |
|
| 253 |
# ---- GESTION ASYNCHRONE / ÉTAT ----
|
| 254 |
current_image = None
|
|
|
|
| 328 |
if not candidates:
|
| 329 |
return None, 0
|
| 330 |
|
| 331 |
+
# Génération asynchrone avec progression
|
| 332 |
progress = 0
|
| 333 |
yield None, progress # Initialisation
|
| 334 |
progress = 10
|
|
|
|
| 348 |
except Exception as e:
|
| 349 |
print(f"❌ Erreur dans generate_gradcam_ui: {e}")
|
| 350 |
import traceback; traceback.print_exc()
|
| 351 |
+
yield None, 0
|
| 352 |
|
| 353 |
# ---- INTERFACE GRADIO ----
|
| 354 |
example_paths = ["ISIC_0024627.jpg", "ISIC_0025539.jpg", "ISIC_0031410.jpg"]
|