rkonan commited on
Commit
810c411
·
1 Parent(s): 9fe0ff3

premier deploiemen

Browse files
.dockerignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .DS_Store
5
+ .env
.gitattributes CHANGED
@@ -33,3 +33,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ model/ filter=lfs diff=lfs merge=lfs -text
37
+ model/best_efficientnetv2m_gradcam.keras filter=lfs diff=lfs merge=lfs -text
38
+ model/best_ResNet50V2_04_improved_target_augment.keras filter=lfs diff=lfs merge=lfs -text
39
+ model/best_ResNet50V2_gradcam.keras filter=lfs diff=lfs merge=lfs -text
40
+ model/best_EfficientNetV2M_03_improved_global_augment.keras filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environnements virtuels
2
+ venv/
3
+ .venv/
4
+ env/
5
+ .env/
6
+
7
+ # Fichiers Python compilés
8
+ app/__pycache__/
9
+ *.pyc
10
+
11
+ # Fichiers système
12
+ .DS_Store
13
+
14
+ # Configurations IDE
15
+ .vscode/
16
+ .idea/
17
+
18
+ # Fichiers de logs
19
+ *.log
Dockerfile ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Image TensorFlow GPU (fonctionne aussi en CPU-only)
2
+ FROM tensorflow/tensorflow:2.15.0-gpu
3
+
4
+ # Installation des dépendances système supplémentaires
5
+ RUN apt-get update && apt-get install -y \
6
+ libgl1-mesa-glx \
7
+ libglib2.0-0 \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Création de l'utilisateur user (requis par Hugging Face)
11
+ RUN useradd -m -u 1000 user
12
+ ENV HOME=/home/user
13
+ ENV PATH=/home/user/.local/bin:$PATH
14
+
15
+ # Définir le répertoire de travail
16
+ WORKDIR $HOME/app
17
+
18
+ # Copier les fichiers de requirements avec les bonnes permissions
19
+ COPY --chown=user:user requirements.txt .
20
+
21
+ # Passer à l'utilisateur user
22
+ USER user
23
+
24
+ # Installer les dépendances Python
25
+ RUN pip install --no-cache-dir --upgrade pip && \
26
+ pip install --no-cache-dir -r requirements.txt
27
+
28
+ # Copier le reste de l'application
29
+ COPY --chown=user:user . .
30
+
31
+ # Port requis par Hugging Face Spaces
32
+ EXPOSE 7860
33
+
34
+ # Variables d'environnement pour Hugging Face Spaces
35
+ ENV PORT=7860
36
+ ENV HOST=0.0.0.0
37
+
38
+ # Commande de démarrage compatible HF Spaces
39
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860", "--log-level", "info"]
Dockerfile copy ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Image TensorFlow GPU (fonctionne aussi en CPU-only)
2
+ FROM tensorflow/tensorflow:2.15.0-gpu
3
+
4
+ # Installation des dépendances système supplémentaires
5
+ RUN apt-get update && apt-get install -y \
6
+ libgl1-mesa-glx \
7
+ libglib2.0-0 \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Création de l'utilisateur user (requis par Hugging Face)
11
+ RUN useradd -m -u 1000 user
12
+ ENV HOME=/home/user
13
+ ENV PATH=/home/user/.local/bin:$PATH
14
+
15
+ # Définir le répertoire de travail
16
+ WORKDIR $HOME/app
17
+
18
+ # Copier les fichiers de requirements avec les bonnes permissions
19
+ COPY --chown=user:user requirements.txt .
20
+
21
+ # Passer à l'utilisateur user
22
+ USER user
23
+
24
+ # Installer les dépendances Python
25
+ RUN pip install --no-cache-dir --upgrade pip && \
26
+ pip install --no-cache-dir -r requirements.txt
27
+
28
+ # Copier le reste de l'application
29
+ COPY --chown=user:user . .
30
+
31
+ # Port requis par Hugging Face Spaces
32
+ EXPOSE 7860
33
+
34
+ # Variables d'environnement pour Hugging Face Spaces
35
+ ENV PORT=7860
36
+ ENV HOST=0.0.0.0
37
+
38
+ # Commande de démarrage compatible HF Spaces
39
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860", "--log-level", "info"]
app/main.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File, HTTPException, Request,Query
2
+ import io
3
+ from PIL import Image
4
+ from io import BytesIO
5
+ from pydantic import BaseModel
6
+ from typing import Union
7
+ from io import BytesIO
8
+ import base64
9
+ import logging
10
+ import logging
11
+ from app.model import load_model, predict_with_model
12
+ # Configuration de base du logger
13
+ logging.basicConfig(
14
+ level=logging.DEBUG, # DEBUG pour voir tous les logs (INFO, WARNING, ERROR, etc.)
15
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
16
+ )
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+
22
+ app = FastAPI()
23
+
24
+
25
+ @app.on_event("startup")
26
+ def load_models_once():
27
+ _ = load_model()
28
+
29
+ class ImagePayload(BaseModel):
30
+ image: str # chaîne encodée en base64
31
+ @app.post("/predict")
32
+ async def predict(request: Request,
33
+ file: UploadFile = File(None),
34
+ payload: Union[ImagePayload, None] = None,
35
+ show_heatmap: bool = Query(False, description="Afficher la heatmap"),
36
+ ):
37
+
38
+ logger.info("🔁 Requête reçue")
39
+ logger.info(f"✅ Show heatmap : {show_heatmap}")
40
+
41
+ try:
42
+ # Cas 1 : multipart avec fichier
43
+ if file is not None:
44
+ image_bytes = await file.read()
45
+ logger.debug("✅ Image reçue via multipart :", file.filename, len(image_bytes), "octets")
46
+
47
+ # Cas 2 : JSON base64
48
+ elif await request.json():
49
+ body = await request.json()
50
+ if "image" not in body:
51
+ raise HTTPException(status_code=422, detail="Champ 'image' manquant.")
52
+ image_base64 = body["image"]
53
+ image_bytes = base64.b64decode(image_base64)
54
+ logger.debug("✅ Image décodée depuis base64 :", len(image_bytes), "octets")
55
+
56
+ else:
57
+ logger.info("⚠️ Aucune image reçue")
58
+ raise HTTPException(status_code=400, detail="Format de requête non supporté.")
59
+
60
+ # Appel de ta logique de prédiction
61
+ logger.debug("🔍 Appel du vote multi-modèles...")
62
+ model_config = load_model()[0]
63
+ prediction = predict_with_model(model_config, image_bytes, show_heatmap)
64
+
65
+ # Pour l’instant : réponse simulée
66
+ return prediction
67
+
68
+ except Exception as e:
69
+ logger.error("❌ Une erreur s'est produite", exc_info=True)
70
+ raise HTTPException(status_code=500, detail=str(e))
71
+
72
+
73
+ @app.get("/health")
74
+ def health_check():
75
+ return {"status": "ok"}
app/model.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tensorflow as tf
2
+ import numpy as np
3
+ from PIL import Image
4
+ import tensorflow as tf
5
+ import logging
6
+ import numpy as np
7
+ from PIL import Image
8
+ from keras.applications.efficientnet_v2 import preprocess_input as effnet_preprocess
9
+ from keras.applications.resnet_v2 import preprocess_input as resnet_preprocess
10
+ import io
11
+ from tf_keras_vis.gradcam import Gradcam,GradcamPlusPlus
12
+ from tf_keras_vis.utils import normalize
13
+
14
+ import numpy as np
15
+ import tensorflow as tf
16
+ from tf_keras_vis.saliency import Saliency
17
+ from tf_keras_vis.utils import normalize
18
+ import numpy as np
19
+ import tensorflow as tf
20
+ from tf_keras_vis.saliency import Saliency
21
+ from tf_keras_vis.utils import normalize
22
+ import logging
23
+ import time
24
+
25
+ from typing import TypedDict, Callable, Any
26
+ logging.basicConfig(
27
+ level=logging.INFO, # ou logging.DEBUG
28
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
29
+ )
30
+ logger = logging.getLogger(__name__)
31
+ confidence_threshold=0.5
32
+ entropy_threshold=2
33
+
34
+ class ModelStruct(TypedDict):
35
+ model_name: str
36
+ model: tf.keras.Model
37
+ gradcam_model:tf.keras.Model
38
+ preprocess_input: Callable[[np.ndarray], Any]
39
+ target_size: tuple[int, int]
40
+ last_conv_layer:str
41
+ gradcam_type:str
42
+
43
+
44
+ _model_cache: list[ModelStruct] | None = None
45
+ def load_model() -> list[ModelStruct]:
46
+ global _model_cache
47
+ if _model_cache is None:
48
+ print("📦 Chargement du modèle EfficientNetV2M...")
49
+ model = tf.keras.models.load_model("model/best_efficientnetv2m_gradcam.keras", compile=False)
50
+
51
+ _model_cache = [{
52
+ "model_name": "EfficientNetV2M",
53
+ "model": model,
54
+ "gradcam_model": model,
55
+ "preprocess_input": effnet_preprocess,
56
+ "target_size": (480, 480),
57
+ "last_conv_layer": "block7a_expand_conv",
58
+ "gradcam_type": "gradcam++"
59
+ }]
60
+ return _model_cache
61
+
62
+
63
+
64
+ def compute_gradcam(model, image_array, class_index=None, layer_name=None,gradcam_type="gradcam"):
65
+ """
66
+ Calcule la carte Grad-CAM pour une image et un modèle Keras.
67
+
68
+ Args:
69
+ model: tf.keras.Model.
70
+ image_array: np.array (H, W, 3), float32, pré-traitée.
71
+ class_index: int ou None, index de la classe cible. Si None, classe prédite.
72
+ layer_name: str ou None, nom de la couche convolutionnelle à utiliser. Si None, dernière conv.
73
+
74
+ Returns:
75
+ gradcam_map: np.array (H, W), normalisée entre 0 et 1.
76
+ """
77
+ logging.info(f"Lancement calcul de la gradcam avec le type {gradcam_type}")
78
+
79
+ if image_array.ndim == 3:
80
+ input_tensor = np.expand_dims(image_array, axis=0)
81
+ else:
82
+ input_tensor = image_array
83
+ if gradcam_type=="gradcam++":
84
+ gradcam = GradcamPlusPlus(model, clone=False)
85
+ else:
86
+ gradcam = Gradcam(model, clone=False)
87
+
88
+ def loss(output):
89
+ if class_index is None:
90
+ class_index_local = tf.argmax(output[0])
91
+ else:
92
+ class_index_local = class_index
93
+ return output[:, class_index_local]
94
+
95
+ # Choisir la couche à utiliser pour GradCAM
96
+ if layer_name is None:
97
+ # Si non spécifié, chercher la dernière couche conv 2D
98
+ for layer in reversed(model.layers):
99
+ if 'conv' in layer.name and len(layer.output_shape) == 4:
100
+ layer_name = layer.name
101
+ break
102
+ if layer_name is None:
103
+ raise ValueError("Aucune couche convolutionnelle 2D trouvée dans le modèle.")
104
+
105
+ cam = gradcam(loss, input_tensor, penultimate_layer=layer_name)
106
+ cam = cam[0]
107
+
108
+ # Normaliser entre 0 et 1
109
+ cam = normalize(cam)
110
+
111
+ return cam
112
+
113
+
114
+ def preprocess_image(image_bytes, target_size, preprocess_input):
115
+ try:
116
+ logger.info("📤 Lecture des bytes et conversion en image PIL")
117
+ image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
118
+ except Exception as e:
119
+ logger.exception("❌ Erreur lors de l'ouverture de l'image")
120
+ raise ValueError("Impossible de décoder l'image") from e
121
+
122
+ logger.info(f"📐 Redimensionnement de l'image à la taille {target_size}")
123
+ image = image.resize(target_size)
124
+ image_array = np.array(image).astype(np.float32)
125
+
126
+ logger.debug(f"🔍 Shape de l'image après conversion en tableau : {image_array.shape}")
127
+
128
+ if image_array.ndim != 3 or image_array.shape[-1] != 3:
129
+ logger.error(f"❌ Image invalide : shape={image_array.shape}")
130
+ raise ValueError("Image must have 3 channels (RGB)")
131
+
132
+ logger.info("🎨 Conversion et prétraitement de l'image")
133
+
134
+ # Préparation pour la prédiction
135
+ preprocessed_input = preprocess_input(image_array.copy())
136
+ preprocessed_input = np.expand_dims(preprocessed_input, axis=0)
137
+
138
+ # Préparation pour Grad-CAM (non prétraitée, mais batchifiée et en float32)
139
+ raw_input = np.expand_dims(image_array / 255.0, axis=0) # Mise à l’échelle simple
140
+
141
+ logger.debug(f"🧪 Shape après ajout de la dimension batch : {preprocessed_input.shape}")
142
+ return preprocessed_input, raw_input
143
+
144
+
145
+
146
+ def compute_entropy_safe(probas):
147
+ probas = np.array(probas)
148
+ # On garde uniquement les probabilités strictement positives
149
+ mask = probas > 0
150
+ entropy = -np.sum(probas[mask] * np.log(probas[mask]))
151
+ return entropy
152
+
153
+
154
+ def predict_with_model(config, image_bytes: bytes,show_heatmap=False):
155
+
156
+ input_array,raw_input = preprocess_image(image_bytes,config["target_size"],config["preprocess_input"])
157
+
158
+ logger.info("🤖 Lancement de la prédiction avec le modèle")
159
+ preds = config["model"].predict(input_array)
160
+ logger.debug(f"📈 Prédictions brutes : {preds[0].tolist()}")
161
+
162
+ predicted_class_index = int(np.argmax(preds[0]))
163
+ confidence = float(preds[0][predicted_class_index])
164
+ entropy=float(compute_entropy_safe(preds))
165
+ is_uncertain_model= (confidence<confidence_threshold) or (entropy>entropy_threshold)
166
+ logger.info(f"✅ Prédiction : classe={predicted_class_index}, confiance={confidence:.4f},entropy={entropy:.4f},is_uncertain_model={is_uncertain_model}")
167
+
168
+ result= {
169
+ "preds": preds[0].tolist(),
170
+ "predicted_class": predicted_class_index,
171
+ "confidence": confidence,
172
+ "entropy":entropy,
173
+ "is_uncertain_model":is_uncertain_model
174
+ }
175
+ if show_heatmap and not is_uncertain_model:
176
+ try:
177
+ logger.info("✅ Début de la génération de la heatmap")
178
+ start_time = time.time()
179
+
180
+ # Vérification des entrées
181
+ logger.info(f"🖼️ Image d'entrée shape: {raw_input.shape}")
182
+ logger.info(f"🎯 Index de classe prédite: {predicted_class_index}")
183
+ logger.info(f"🛠️ Dernière couche utilisée: {config['last_conv_layer']}")
184
+
185
+ # Calcul de la heatmap
186
+ heatmap = compute_gradcam(config["gradcam_model"], raw_input, class_index=predicted_class_index, layer_name=config["last_conv_layer"],gradcam_type=config["gradcam_type"])
187
+
188
+ elapsed_time = time.time() - start_time
189
+ logger.info(f"✅ Heatmap générée en {elapsed_time:.2f} secondes")
190
+
191
+ # Conversion en liste pour le JSON
192
+ result["heatmap"] = heatmap.tolist()
193
+
194
+ except Exception as e:
195
+ logger.error(f"❌ Erreur lors de la génération de la heatmap: {e}")
196
+ result["heatmap"] = []
197
+ else:
198
+ logger.info("ℹ️ Heatmap non générée (option désactivée ou modèle incertain)")
199
+ result["heatmap"] = []
200
+
201
+
202
+ return result
model/best_efficientnetv2m_gradcam.keras ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:303b262f1a720ec476e5521f0728843c90650dabbe0adfc7c7a5a33e6fd9e9d2
3
+ size 216600984