Team Ai
Apppublic

JBMoute/Object_classification_JB

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
app.py391 linesDownload Raw Back to root
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)