JBMoute/Object_classification_JB
0
1import os2import io3import base644import numpy as np5from PIL import Image, ImageDraw, ImageFont6from flask import Flask, request, jsonify7from werkzeug.utils import secure_filename8 9# ── TensorFlow ────────────────────────────────────────────────────────────────10try:11 import tensorflow as tf12 from tensorflow import keras13 import h5py, json as _json14 TENSORFLOW_AVAILABLE = True15except ImportError:16 tf = keras = h5py = None17 TENSORFLOW_AVAILABLE = False18 print("⚠️ TensorFlow non installé.")19 20# ── PyTorch ───────────────────────────────────────────────────────────────────21try:22 import torch23 import torch.nn as nn24 import torchvision.transforms as transforms25 import torchvision.models as tv_models26 PYTORCH_AVAILABLE = True27except ImportError:28 torch = nn = transforms = tv_models = None29 PYTORCH_AVAILABLE = False30 print("⚠️ PyTorch non installé.")31 32# ─────────────────────────────────────────────────────────────────────────────33app = Flask(__name__)34app.config["UPLOAD_FOLDER"] = os.path.join(app.root_path, "static", "uploads")35os.makedirs(app.config["UPLOAD_FOLDER"], exist_ok=True)36 37CNN_PATH = os.path.join(app.root_path, "cnn_model.keras")38ALEXNET_PATH = os.path.join(app.root_path, "stage-1.pth")39 40CNN_IMG_SIZE = 15041ALEXNET_IMG_SIZE = 22442ALLOWED_EXTENSIONS = {".png", ".jpg", ".jpeg"}43 44CLASSES = ["buildings", "forest", "glacier", "mountain", "sea", "street"]45CLASS_COLORS = {46 "buildings": (99, 179, 237),47 "forest": (72, 187, 120),48 "glacier": (154, 230, 255),49 "mountain": (160, 140, 210),50 "sea": (56, 161, 222),51 "street": (246, 173, 85),52}53 54# ─────────────────────────────────────────────────────────────────────────────55# Chargement des modèles56# ─────────────────────────────────────────────────────────────────────────────57def load_cnn_model(model_path):58 try:59 if not TENSORFLOW_AVAILABLE:60 return None61 with h5py.File(model_path, "r") as f:62 config_json = f.attrs["model_config"]63 if isinstance(config_json, bytes):64 config_json = config_json.decode("utf-8")65 config_dict = _json.loads(config_json)66 model = keras.Sequential.from_config(config_dict["config"])67 model.compile(optimizer="adam", loss="categorical_crossentropy", metrics=["accuracy"])68 for layer in model.layers:69 lname = layer.name70 if lname in f["model_weights"]:71 grp = f["model_weights"][lname]72 if lname in grp:73 nested = grp[lname]74 weights = [nested[k][()] for k in sorted(nested.keys())]75 if weights:76 try:77 layer.set_weights(weights)78 except ValueError:79 pass80 print(f"✓ CNN chargé : {model_path}")81 return model82 except FileNotFoundError:83 print(f"⚠️ CNN introuvable : {model_path}")84 return None85 except Exception as e:86 print(f"⚠️ Erreur CNN : {e}")87 return None88 89 90def load_alexnet_model(model_path):91 try:92 if not PYTORCH_AVAILABLE:93 return None94 checkpoint = torch.load(model_path, map_location="cpu", weights_only=False)95 state_dict = checkpoint["model"] if isinstance(checkpoint, dict) and "model" in checkpoint else checkpoint96 model = tv_models.alexnet(weights=None)97 model.classifier[-1] = nn.Linear(4096, len(CLASSES))98 model.load_state_dict(state_dict, strict=False)99 model.eval()100 print(f"✓ AlexNet chargé : {model_path}")101 return model102 except FileNotFoundError:103 print(f"⚠️ AlexNet introuvable : {model_path}")104 return None105 except Exception as e:106 print(f"⚠️ Erreur AlexNet : {e}")107 return None108 109 110cnn_model = load_cnn_model(CNN_PATH) if TENSORFLOW_AVAILABLE else None111alexnet = load_alexnet_model(ALEXNET_PATH) if PYTORCH_AVAILABLE else None112 113# ─────────────────────────────────────────────────────────────────────────────114# Prédictions115# ─────────────────────────────────────────────────────────────────────────────116def predict_cnn(image):117 if cnn_model is None:118 return None119 img = image.resize((CNN_IMG_SIZE, CNN_IMG_SIZE))120 arr = np.expand_dims(np.array(img) / 255.0, 0)121 preds = cnn_model.predict(arr, verbose=0)[0]122 return dict(zip(CLASSES, [round(float(p) * 100, 2) for p in preds]))123 124 125ALEXNET_TRANSFORM = transforms.Compose([126 transforms.Resize((ALEXNET_IMG_SIZE, ALEXNET_IMG_SIZE)),127 transforms.ToTensor(),128 transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),129]) if PYTORCH_AVAILABLE else None130 131 132def predict_alexnet(image):133 if alexnet is None or ALEXNET_TRANSFORM is None:134 return None135 tensor = ALEXNET_TRANSFORM(image).unsqueeze(0)136 with torch.no_grad():137 probs = torch.softmax(alexnet(tensor), dim=1)[0].numpy()138 return dict(zip(CLASSES, [round(float(p) * 100, 2) for p in probs]))139 140# ─────────────────────────────────────────────────────────────────────────────141# Annotation image142# ─────────────────────────────────────────────────────────────────────────────143def annotate_image(image_path, scores):144 img = Image.open(image_path).convert("RGB")145 w, h = img.size146 bar_h = 60147 out = Image.new("RGB", (w, h + bar_h), (18, 22, 36))148 out.paste(img, (0, 0))149 draw = ImageDraw.Draw(out)150 151 top_class = max(scores, key=scores.get)152 top_conf = scores[top_class]153 color = CLASS_COLORS.get(top_class, (200, 200, 200))154 155 draw.rectangle([0, 0, w - 1, h - 1], outline=color, width=4)156 draw.rectangle([0, h, w, h + bar_h], fill=(18, 22, 36))157 158 try:159 font_b = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 20)160 font_s = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 12)161 except Exception:162 font_b = font_s = ImageFont.load_default()163 164 draw.text((10, h + 6), f"{top_class.upper()} {top_conf:.1f}%", fill=color, font=font_b)165 sub = " | ".join([f"{k[:3]}: {v:.0f}%" for k, v in sorted(scores.items(), key=lambda x: -x[1])])166 draw.text((10, h + 38), sub, fill=(170, 170, 170), font=font_s)167 168 buf = io.BytesIO()169 out.save(buf, format="PNG")170 return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()171 172# ─────────────────────────────────────────────────────────────────────────────173# HTML intégré174# ─────────────────────────────────────────────────────────────────────────────175INDEX_HTML = """<!DOCTYPE html>176<html lang="fr">177<head>178<meta charset="UTF-8">179<meta name="viewport" content="width=device-width, initial-scale=1.0">180<title>Classificateur d'images</title>181<style>182 :root{--bg:#f3f7fb;--card:#fff;--primary:#2c3e50;--secondary:#3498db;--accent:#e67e22;--border:#dfe6ed}183 *{box-sizing:border-box;margin:0;padding:0}184 body{font-family:Inter,Arial,sans-serif;background:linear-gradient(180deg,#eef6fc,#f9fbfe);color:var(--primary);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}185 .card{width:100%;max-width:960px;background:var(--card);border-radius:28px;box-shadow:0 16px 40px rgba(44,62,80,.08);padding:36px}186 h1{font-size:clamp(1.6rem,2.5vw,2.4rem);text-align:center;margin-bottom:6px}187 .subtitle{text-align:center;color:#4a6d86;margin-bottom:32px;font-size:.95rem}188 .grid{display:grid;grid-template-columns:1fr 300px;gap:24px}189 .panel{border:1px solid var(--border);border-radius:18px;padding:24px;background:#fbfcff}190 .drop-zone{border:2px dashed var(--secondary);border-radius:14px;padding:40px 20px;text-align:center;cursor:pointer;transition:.2s;background:#f6fbff}191 .drop-zone:hover,.drop-zone.over{border-color:var(--accent);background:#eef6ff}192 .drop-zone input{display:none}193 .drop-zone p{color:#4a6d86;font-size:.9rem;margin-top:8px}194 .model-btns{display:flex;gap:12px;margin-top:20px}195 .model-btn{flex:1;padding:12px;border-radius:10px;border:1.5px solid var(--border);background:transparent;color:#3b4f67;cursor:pointer;font-size:.9rem;font-weight:600;transition:.2s}196 .model-btn.active{border-color:var(--secondary);background:#ebf5fb;color:var(--secondary)}197 .submit-btn{width:100%;margin-top:16px;padding:14px;border-radius:12px;border:none;background:var(--secondary);color:#fff;font-size:1rem;font-weight:700;cursor:pointer;transition:.2s;display:flex;align-items:center;justify-content:center;gap:10px}198 .submit-btn:hover{background:#297fb8}199 .submit-btn:disabled{opacity:.5;cursor:not-allowed}200 .spinner{width:18px;height:18px;border:3px solid #ffffff55;border-top-color:#fff;border-radius:50%;animation:spin .7s linear infinite;display:none}201 @keyframes spin{to{transform:rotate(360deg)}}202 .preview-panel{display:flex;flex-direction:column;align-items:center;gap:16px}203 .preview-panel strong{align-self:flex-start}204 #previewImg{width:100%;border-radius:14px;border:1px solid var(--border);object-fit:cover;min-height:180px;background:#f5f9ff}205 .result{margin-top:28px;padding:24px;border-radius:18px;background:#f8fbff;border:1px solid var(--border);display:none}206 .result h2{font-size:1.1rem;margin-bottom:16px}207 .annotated{width:100%;border-radius:12px;margin-bottom:20px}208 .bar-row{display:flex;align-items:center;gap:10px;margin-bottom:10px;font-size:.85rem}209 .bar-label{width:80px;color:#3b4f67;font-weight:600}210 .bar-bg{flex:1;height:10px;background:#e8edf2;border-radius:5px;overflow:hidden}211 .bar-fill{height:100%;border-radius:5px;transition:width .6s ease}212 .bar-val{width:50px;text-align:right;color:#4a6d86}213 .error-box{margin-top:16px;padding:14px 18px;background:#fef3f2;border:1px solid #fca5a5;border-radius:12px;color:#c0392b;font-weight:600;display:none}214 @media(max-width:720px){.grid{grid-template-columns:1fr}}215</style>216</head>217<body>218<div class="card">219 <h1>🌄 Classificateur d'images</h1>220 <p class="subtitle">Choisissez une image, sélectionnez un modèle et obtenez les résultats.</p>221 <div class="grid">222 <div class="panel">223 <div class="drop-zone" id="dropZone">224 <input type="file" id="fileInput" accept=".png,.jpg,.jpeg">225 <div style="font-size:2.2rem">🖼️</div>226 <p>Glissez une image ici ou <span style="color:var(--secondary);cursor:pointer" onclick="document.getElementById('fileInput').click()">parcourir</span></p>227 <p><small>PNG, JPG, JPEG</small></p>228 </div>229 <div class="model-btns">230 <button class="model-btn active" data-model="cnn" onclick="selectModel(this)">🧠 CNN (Keras)</button>231 <button class="model-btn" data-model="alexnet" onclick="selectModel(this)">🔥 AlexNet</button>232 </div>233 <button class="submit-btn" id="submitBtn" onclick="runPrediction()">234 <span id="btnText">Lancer la prédiction</span>235 <div class="spinner" id="spinner"></div>236 </button>237 <div class="error-box" id="errorBox"></div>238 </div>239 <div class="panel preview-panel">240 <strong>Aperçu</strong>241 <img id="previewImg" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='200' viewBox='0 0 300 200'%3E%3Crect fill='%23f5f9ff' width='300' height='200'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%2394a8bf' font-family='Arial' font-size='14'%3EAucune image%3C/text%3E%3C/svg%3E" alt="aperçu">242 </div>243 </div>244 <div class="result" id="result">245 <h2>Résultats de prédiction</h2>246 <img class="annotated" id="annotatedImg">247 <div id="barsDiv"></div>248 </div>249</div>250 251<script>252const COLORS = {buildings:'#63b3ed',forest:'#48bb78',glacier:'#9ae6ff',mountain:'#a08cd2',sea:'#38a1de',street:'#f6ad55'};253let selectedModel = 'cnn';254let selectedFile = null;255 256function selectModel(btn) {257 document.querySelectorAll('.model-btn').forEach(b => b.classList.remove('active'));258 btn.classList.add('active');259 selectedModel = btn.dataset.model;260}261 262const dropZone = document.getElementById('dropZone');263const fileInput = document.getElementById('fileInput');264dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('over'); });265dropZone.addEventListener('dragleave', () => dropZone.classList.remove('over'));266dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('over'); handleFile(e.dataTransfer.files[0]); });267fileInput.addEventListener('change', () => handleFile(fileInput.files[0]));268 269function handleFile(file) {270 if (!file) return;271 selectedFile = file;272 const reader = new FileReader();273 reader.onload = e => { document.getElementById('previewImg').src = e.target.result; };274 reader.readAsDataURL(file);275 document.getElementById('result').style.display = 'none';276 document.getElementById('errorBox').style.display = 'none';277}278 279async function runPrediction() {280 if (!selectedFile) { showError('Veuillez sélectionner une image.'); return; }281 setLoading(true);282 const fd = new FormData();283 fd.append('image', selectedFile);284 fd.append('model', selectedModel);285 try {286 const res = await fetch('/predict', { method: 'POST', body: fd });287 const data = await res.json();288 if (data.error) { showError(data.error); return; }289 showResult(data);290 } catch(e) { showError('Erreur de connexion au serveur.'); }291 finally { setLoading(false); }292}293 294function showResult(data) {295 document.getElementById('errorBox').style.display = 'none';296 document.getElementById('annotatedImg').src = data.annotated_image;297 const sorted = Object.entries(data.scores).sort((a,b) => b[1]-a[1]);298 document.getElementById('barsDiv').innerHTML = sorted.map(([k,v]) =>299 `<div class="bar-row">300 <span class="bar-label">${k}</span>301 <div class="bar-bg"><div class="bar-fill" style="width:${v}%;background:${COLORS[k]||'#3498db'}"></div></div>302 <span class="bar-val">${v.toFixed(1)}%</span>303 </div>`304 ).join('');305 document.getElementById('result').style.display = 'block';306}307 308function showError(msg) {309 const el = document.getElementById('errorBox');310 el.textContent = msg; el.style.display = 'block';311}312 313function setLoading(on) {314 document.getElementById('submitBtn').disabled = on;315 document.getElementById('btnText').style.display = on ? 'none' : 'inline';316 document.getElementById('spinner').style.display = on ? 'block' : 'none';317}318</script>319</body>320</html>"""321 322# ─────────────────────────────────────────────────────────────────────────────323# Routes324# ─────────────────────────────────────────────────────────────────────────────325@app.route("/")326def index():327 return INDEX_HTML, 200, {"Content-Type": "text/html; charset=utf-8"}328 329 330@app.route("/predict", methods=["POST"])331def predict():332 file = request.files.get("image")333 if not file or file.filename == "":334 return jsonify({"error": "Aucune image reçue."}), 400335 336 ext = os.path.splitext(file.filename)[1].lower()337 if ext not in ALLOWED_EXTENSIONS:338 return jsonify({"error": "Format non supporté. Utilisez PNG, JPG ou JPEG."}), 400339 340 model_choice = request.form.get("model", "cnn").lower()341 if model_choice not in ("cnn", "alexnet"):342 return jsonify({"error": "Modèle invalide."}), 400343 344 # Vérifications de disponibilité345 if model_choice == "cnn":346 if not TENSORFLOW_AVAILABLE:347 return jsonify({"error": "TensorFlow non installé."}), 503348 if cnn_model is None:349 return jsonify({"error": "Modèle CNN introuvable (Models/cnn_model.keras)."}), 503350 else:351 if not PYTORCH_AVAILABLE:352 return jsonify({"error": "PyTorch non installé."}), 503353 if alexnet is None:354 return jsonify({"error": "Modèle AlexNet introuvable (Models/stage-1.pth)."}), 503355 356 filename = secure_filename(file.filename)357 filepath = os.path.join(app.config["UPLOAD_FOLDER"], filename)358 file.save(filepath)359 360 image = Image.open(filepath).convert("RGB")361 scores = predict_cnn(image) if model_choice == "cnn" else predict_alexnet(image)362 363 if scores is None:364 return jsonify({"error": "Échec de la prédiction."}), 500365 366 annotated = annotate_image(filepath, scores)367 top_class = max(scores, key=scores.get)368 369 return jsonify({370 "predicted_class": top_class,371 "confidence": scores[top_class],372 "scores": scores,373 "model_used": model_choice,374 "annotated_image": annotated,375 })376 377 378@app.route("/health")379def health():380 return jsonify({381 "status": "ok",382 "cnn_loaded": cnn_model is not None,383 "alexnet_loaded": alexnet is not None,384 "tensorflow": TENSORFLOW_AVAILABLE,385 "pytorch": PYTORCH_AVAILABLE,386 })387 388 389if __name__ == "__main__":390 port = int(os.environ.get("PORT", 7860))391 app.run(host="0.0.0.0", port=port, debug=False)