Spaces:
Sleeping
Sleeping
Download Interface/app.py from RaxoM21/MIR_Group07: direct link, hf CLI and curl.
- Browser
- Download file 22.3 kB
-
https://huggingface.co/spaces/RaxoM21/MIR_Group07/resolve/main/Interface/app.py
- Command line
-
hf download hf://spaces/RaxoM21/MIR_Group07/Interface/app.py
-
curl -L -o app.py https://huggingface.co/spaces/RaxoM21/MIR_Group07/resolve/main/Interface/app.py
22.3 kB
| import sys | |
| import os | |
| os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE' | |
| from dotenv import load_dotenv | |
| load_dotenv(os.path.join(os.path.dirname(__file__), '..', '.env')) | |
| # make project root importable — must come before any local imports | |
| PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) | |
| sys.path.insert(0, PROJECT_ROOT) | |
| os.environ.setdefault("MIR_H5_DIR", os.path.join(PROJECT_ROOT, "descriptors", "CLIP_Flickr8k")) | |
| import torch | |
| import math | |
| import time | |
| import numpy as np | |
| import cv2 | |
| from flask import Flask, render_template, request, jsonify, send_from_directory, url_for | |
| from werkzeug.utils import secure_filename | |
| import faiss | |
| from pathlib import Path | |
| from PIL import Image | |
| from backbone_manager import manager, BACKBONES, DEFAULT_BACKBONE | |
| from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user | |
| from werkzeug.security import check_password_hash | |
| from flask import redirect | |
| import functions as f | |
| from werkzeug.middleware.proxy_fix import ProxyFix | |
| from flask_wtf.csrf import CSRFProtect | |
| app = Flask(__name__) | |
| app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1) | |
| app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 | |
| app.config['SECRET_KEY'] = os.environ.get("MIR_SECRET_KEY", "mir-static-fallback-2026") | |
| app.config['SESSION_COOKIE_SAMESITE'] = 'None' | |
| app.config['SESSION_COOKIE_SECURE'] = True | |
| csrf = CSRFProtect(app) | |
| ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'bmp', 'webp'} | |
| def allowed_file(fn): | |
| return '.' in fn and fn.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS | |
| login_manager = LoginManager(app) | |
| login_manager.login_view = "login" | |
| login_manager.login_message = "" | |
| class _User(UserMixin): | |
| def __init__(self, uid: str): | |
| self.id = uid | |
| def _load_user(uid: str) -> _User | None: | |
| return _User(uid) if uid == os.environ.get("MIR_USERNAME", "admin") else None | |
| DATASET_PATH = os.path.join(PROJECT_ROOT, "dataset_voitures", "dataset") | |
| DESCRIPTORS_PATH = os.path.join(PROJECT_ROOT, "descriptors") | |
| UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), "uploads") | |
| os.makedirs(UPLOAD_FOLDER, exist_ok=True) | |
| # Flickr8k — images live inside the Flicker8k_Dataset subdirectory (original Project 1 layout) | |
| def _detect_flickr_dir(): | |
| env = os.environ.get("MIR_FLICKR_DIR") | |
| candidates = ([env] if env else []) + [ | |
| os.path.join(PROJECT_ROOT, "Flickr8k_Dataset", "Flicker8k_Dataset"), | |
| os.path.join(PROJECT_ROOT, "Flickr8k_Dataset", "Flickr8k_Dataset"), | |
| os.path.join(PROJECT_ROOT, "Flickr8k_Dataset", "images"), | |
| os.path.join(PROJECT_ROOT, "Flickr8k_Dataset"), | |
| ] | |
| for c in candidates: | |
| try: | |
| if c and os.path.isdir(c) and any( | |
| fn.lower().endswith((".jpg", ".jpeg", ".png")) for fn in os.listdir(c) | |
| ): | |
| print(f"[Flickr8k] Images servies depuis : {c}") | |
| return c | |
| except Exception: | |
| pass | |
| fb = os.path.join(PROJECT_ROOT, "Flickr8k_Dataset", "Flicker8k_Dataset") | |
| print(f"[Flickr8k] ATTENTION : aucune image trouvee, fallback {fb}") | |
| return fb | |
| FLICKR8K_DATASET_PATH = _detect_flickr_dir() | |
| DESCRIPTOR_MAP = { | |
| "Color Histogram": ("Hist_Col", f.generateHistogramme_Color, 1), | |
| "HSV Histogram": ("HSV", f.generateHistogramme_HSV, 2), | |
| "GLCM (Texture)": ("GLCM", f.generateGLCM, 5), | |
| "LBP (Texture)": ("LBP", f.generateLBP, 6), | |
| "HOG (Shape)": ("HOG", f.generateHOG, 7), | |
| "SIFT (Keypoints)": ("SIFT", f.generateSIFT, 3), | |
| "ORB (Keypoints)": ("ORB", f.generateORB, 4), | |
| "ViT (Deep Learning)": ("ViT", f.generateViT, 8), | |
| "ResNet50 (Deep Learning)": ("ResNet", f.generateResNet, 9), | |
| "CLIP (OpenAI)": ("CLIP", f.generateCLIP, 10), | |
| } | |
| VECTOR_DESCRIPTORS = ["Color Histogram", "HSV Histogram", "GLCM (Texture)", "LBP (Texture)", "HOG (Shape)"] | |
| KEYPOINT_DESCRIPTORS = ["SIFT (Keypoints)", "ORB (Keypoints)"] | |
| DL_DESCRIPTORS = ["ViT (Deep Learning)", "ResNet50 (Deep Learning)", "CLIP (OpenAI)"] | |
| VECTOR_DISTANCES = ["Euclidienne", "Chi carre", "Correlation", "Intersection", "Bhattacharyya"] | |
| KEYPOINT_DISTANCES = ["Brute force", "Flann"] | |
| DL_DISTANCES = ["Cosine Similarity", "Euclidienne"] | |
| HIGHER_IS_BETTER = {"Correlation", "Intersection", "Brute force", "Flann", "Cosine Similarity"} | |
| def get_image_class(filename, manual_class=None): | |
| """Extract class from filename like '0_1_BMW_X3_207.jpg' -> '0_1'.""" | |
| if manual_class: | |
| return manual_class.strip() | |
| parts = os.path.basename(filename).split("_") | |
| return parts[0] # classe = 1er chiffre = marque (def. de l'enonce) | |
| def run_search(query_path, descriptor_keys, distance_name, k, manual_class=None): | |
| """Main search function — returns (result_dict, error_string).""" | |
| global_start = time.time() | |
| query_class = get_image_class(query_path, manual_class) | |
| img = cv2.imread(query_path) | |
| if img is None: | |
| return None, "Could not read the query image." | |
| num_descriptors = len(descriptor_keys) | |
| all_distances_per_descriptor = [] | |
| higher_is_better_per_desc = [] | |
| total_relevant = 0 | |
| for d_idx, desc_key in enumerate(descriptor_keys): | |
| desc_folder, desc_func, algo_id = DESCRIPTOR_MAP[desc_key] | |
| desc_dir = os.path.join(DESCRIPTORS_PATH, desc_folder) | |
| query_features = desc_func(img) | |
| if query_features is None: | |
| return None, f"Could not extract {desc_key} features." | |
| all_files = [ff for ff in os.listdir(desc_dir) if ff.endswith(".txt")] | |
| desc_files = [ff for ff in all_files if ff[0].isdigit() and int(ff[0]) % 2 == 0] | |
| if not desc_files: | |
| return None, f"No descriptors found in {desc_dir}" | |
| if d_idx == 0: | |
| total_relevant = sum( | |
| 1 for ff in desc_files | |
| if get_image_class(ff.replace(f'_{desc_folder}.txt', '.jpg')) == query_class | |
| ) | |
| is_keypoint_desc = desc_key in KEYPOINT_DESCRIPTORS | |
| is_binary = desc_key == "ORB (Keypoints)" | |
| if is_keypoint_desc: | |
| effective_distance = distance_name if distance_name in ["Brute force", "Flann"] else "Brute force" | |
| higher_is_better_per_desc.append(True) | |
| else: | |
| effective_distance = distance_name | |
| higher_is_better_per_desc.append(distance_name in HIGHER_IS_BETTER) | |
| results = [] | |
| for fname in desc_files: | |
| fpath = os.path.join(desc_dir, fname) | |
| db_features = np.loadtxt(fpath) | |
| if is_keypoint_desc: | |
| query_f = np.float32(query_features) | |
| db_f = np.float32(db_features) | |
| if query_f.ndim == 1: query_f = query_f.reshape(1, -1) | |
| if db_f.ndim == 1: db_f = db_f.reshape(1, -1) | |
| try: | |
| dist = f.distance_f(query_f, db_f, effective_distance, is_binary=is_binary) | |
| except Exception: | |
| dist = 0 | |
| else: | |
| try: | |
| dist = f.distance_f(query_features, db_features, distance_name) | |
| except Exception: | |
| dist = 0 | |
| base = fname.replace(f"_{desc_folder}.txt", "") | |
| results.append((base + ".jpg", float(dist))) | |
| all_distances_per_descriptor.append(results) | |
| if num_descriptors == 1: | |
| results = all_distances_per_descriptor[0] | |
| results.sort(key=lambda x: x[1], reverse=higher_is_better_per_desc[0]) | |
| elapsed = time.time() - global_start | |
| return build_response(results, query_class, total_relevant, elapsed, k, descriptor_keys), None | |
| combined = {} | |
| for d_idx, results in enumerate(all_distances_per_descriptor): | |
| for fname, dist in results: | |
| if fname not in combined: | |
| combined[fname] = [None] * num_descriptors | |
| combined[fname][d_idx] = dist | |
| combined = {img_key: v for img_key, v in combined.items() if all(x is not None for x in v)} | |
| if not combined: | |
| return None, "No common images found across selected descriptors." | |
| for d_idx in range(num_descriptors): | |
| vals = [combined[img_key][d_idx] for img_key in combined] | |
| min_v, max_v = min(vals), max(vals) | |
| rng = max_v - min_v + 1e-10 | |
| for img_key in combined: | |
| normalized = (combined[img_key][d_idx] - min_v) / rng | |
| if higher_is_better_per_desc[d_idx]: | |
| normalized = 1.0 - normalized | |
| combined[img_key][d_idx] = normalized | |
| final_results = [(fname, sum(dists) / len(dists)) for fname, dists in combined.items()] | |
| final_results.sort(key=lambda x: x[1]) | |
| elapsed = time.time() - global_start | |
| return build_response(final_results, query_class, total_relevant, elapsed, k, descriptor_keys), None | |
| def build_response(all_results, query_class, total_relevant, elapsed, k, descriptor_keys): | |
| """Format results + compute metrics + build P-R curve for the frontend.""" | |
| def compute_metrics(results_slice, total_rel): | |
| if not results_slice: | |
| return 0.0, 0.0, 0.0, 0.0 | |
| relevant_found = sum(1 for fname, _ in results_slice if get_image_class(fname) == query_class) | |
| prec = relevant_found / len(results_slice) | |
| rec = relevant_found / total_rel if total_rel > 0 else 0.0 | |
| hits, s = 0, 0.0 | |
| for i, (fname, _) in enumerate(results_slice, 1): | |
| if get_image_class(fname) == query_class: | |
| hits += 1 | |
| s += hits / i | |
| ap = s / total_rel if total_rel > 0 else 0.0 | |
| r = min(total_rel, len(results_slice)) | |
| r_prec_hits = sum(1 for fname, _ in results_slice[:r] if get_image_class(fname) == query_class) | |
| r_precision = r_prec_hits / r if r > 0 else 0.0 | |
| return prec, rec, ap, r_precision | |
| p50, r50, ap50, rprec50 = compute_metrics(all_results[:50], total_relevant) | |
| p100, r100, ap100, rprec100 = compute_metrics(all_results[:100], total_relevant) | |
| # mAP = AP on the full ranked list (single-query scenario) | |
| map_score = 0.0 | |
| if total_relevant > 0 and all_results: | |
| hits, s = 0, 0.0 | |
| for i, (fname, _) in enumerate(all_results, 1): | |
| if get_image_class(fname) == query_class: | |
| hits += 1 | |
| s += hits / i | |
| map_score = s / total_relevant | |
| # Precision-Recall curve (sampled at every rank up to 300) | |
| pr_curve = [] | |
| max_k_curve = min(len(all_results), 300) | |
| cumulative_relevant = 0 | |
| for rank_i in range(1, max_k_curve + 1): | |
| fname, _ = all_results[rank_i - 1] | |
| if get_image_class(fname) == query_class: | |
| cumulative_relevant += 1 | |
| if rank_i <= 50 or (rank_i <= 200 and rank_i % 5 == 0) or rank_i % 10 == 0: | |
| p = cumulative_relevant / rank_i | |
| r = cumulative_relevant / total_relevant if total_relevant > 0 else 0 | |
| pr_curve.append({"k": rank_i, "precision": round(p * 100, 1), "recall": round(r * 100, 1)}) | |
| top_k = all_results[:k] | |
| results_list = [] | |
| for i, (fname, dist) in enumerate(top_k): | |
| results_list.append({ | |
| "filename": fname, | |
| "distance": round(dist, 6), | |
| "rank": i + 1, | |
| "image_url": url_for('serve_dataset_image', filename=fname), | |
| "is_relevant": get_image_class(fname) == query_class, | |
| }) | |
| return { | |
| "results": results_list, | |
| "elapsed": round(elapsed, 2), | |
| "query_class": query_class, | |
| "total_relevant": total_relevant, | |
| "total_results": len(all_results), | |
| "k": k, | |
| "descriptors_used": ", ".join(descriptor_keys), | |
| "metrics": { | |
| "p50": round(p50 * 100, 1), | |
| "r50": round(r50 * 100, 1), | |
| "ap50": round(ap50 * 100, 1), | |
| "rprec50": round(rprec50 * 100, 1), | |
| "p100": round(p100 * 100, 1), | |
| "r100": round(r100 * 100, 1), | |
| "ap100": round(ap100 * 100, 1), | |
| "rprec100": round(rprec100 * 100, 1), | |
| "map": round(map_score * 100, 1), | |
| }, | |
| "pr_curve": pr_curve, | |
| } | |
| # -- Routes -- | |
| def index(): | |
| return render_template('index.html') | |
| def serve_dataset_image(filename): | |
| safe = os.path.abspath(os.path.join(DATASET_PATH, filename)) | |
| if not safe.startswith(os.path.abspath(DATASET_PATH) + os.sep): | |
| return "Forbidden", 403 | |
| return send_from_directory(DATASET_PATH, filename) | |
| def get_dataset_images(): | |
| page = max(1, request.args.get('page', 1, type=int)) | |
| per_page = 40 | |
| try: | |
| files = [] | |
| for root, _, fs in os.walk(DATASET_PATH): | |
| for f_ in fs: | |
| if allowed_file(f_): | |
| rel = os.path.relpath(os.path.join(root, f_), DATASET_PATH) | |
| files.append(rel.replace(os.sep, '/')) | |
| files.sort() | |
| total = len(files) | |
| pages = max(1, math.ceil(total / per_page)) | |
| start = (page - 1) * per_page | |
| return jsonify({"files": files[start:start+per_page], "page": page, | |
| "total_pages": pages, "total_files": total}) | |
| except Exception as e: | |
| app.logger.error("dataset_images: %s", e) | |
| return jsonify({"error": "Could not read dataset."}), 500 | |
| def serve_flickr8k_image(filename): | |
| return send_from_directory(FLICKR8K_DATASET_PATH, filename) | |
| def get_flickr8k_images(): | |
| page = max(1, request.args.get('page', 1, type=int)) | |
| per_page = 40 | |
| try: | |
| files = sorted([f_ for f_ in os.listdir(FLICKR8K_DATASET_PATH) | |
| if allowed_file(f_) and os.path.isfile(os.path.join(FLICKR8K_DATASET_PATH, f_))]) | |
| total = len(files) | |
| pages = max(1, math.ceil(total / per_page)) | |
| start = (page - 1) * per_page | |
| return jsonify({"files": files[start:start+per_page], "page": page, | |
| "total_pages": pages, "total_files": total}) | |
| except Exception as e: | |
| app.logger.error("flickr8k_images: %s", e) | |
| return jsonify({"error": "Could not read Flickr8k."}), 500 | |
| def serve_upload(filename): | |
| return send_from_directory(UPLOAD_FOLDER, filename) | |
| def get_distances(): | |
| """Return the compatible distance list for the selected descriptor group.""" | |
| data = request.get_json() | |
| descriptors = data.get('descriptors', []) | |
| if any(d in DL_DESCRIPTORS for d in descriptors): | |
| return jsonify(DL_DISTANCES) | |
| elif any(d in KEYPOINT_DESCRIPTORS for d in descriptors): | |
| return jsonify(KEYPOINT_DISTANCES) | |
| else: | |
| return jsonify(VECTOR_DISTANCES) | |
| def search(): | |
| # get the uploaded file | |
| dataset_filename = request.form.get('dataset_filename') | |
| if 'query_image' in request.files and request.files['query_image'].filename != '': | |
| file = request.files['query_image'] | |
| if not allowed_file(file.filename): | |
| return jsonify({"error": "File type not allowed."}), 400 | |
| filename = secure_filename(file.filename) | |
| filepath = os.path.join(UPLOAD_FOLDER, filename) | |
| file.save(filepath) | |
| query_url = url_for('serve_upload', filename=filename) | |
| elif dataset_filename: | |
| clean = dataset_filename.replace('\\', '/').lstrip('/') | |
| if '..' in clean.split('/'): | |
| return jsonify({"error": "Invalid file path."}), 400 | |
| filepath = os.path.abspath(os.path.join(DATASET_PATH, clean)) | |
| if not filepath.startswith(os.path.abspath(DATASET_PATH) + os.sep) or not os.path.isfile(filepath): | |
| return jsonify({"error": "Dataset image not found."}), 404 | |
| query_url = url_for('serve_dataset_image', filename=clean) | |
| else: | |
| return jsonify({"error": "No image selected"}), 400 | |
| # get search params | |
| descriptors = request.form.getlist('descriptors') | |
| distance = request.form.get('distance', 'Euclidienne') | |
| k = int(request.form.get('k', 10)) | |
| manual_class = request.form.get('manual_class', '').strip() or None | |
| if not descriptors: | |
| return jsonify({"error": "Select at least one descriptor"}), 400 | |
| result, error = run_search(filepath, descriptors, distance, k, manual_class=manual_class) | |
| if error: | |
| return jsonify({"error": error}), 400 | |
| # add the query image url so we can show it | |
| result["query_image_url"] = query_url | |
| return jsonify(result) | |
| def search_text(): | |
| t0 = time.perf_counter() | |
| try: | |
| backbone_key = _resolve_backbone(request) | |
| except ValueError as e: | |
| return jsonify({"error": str(e)}), 400 | |
| body = request.get_json(silent=True) or {} | |
| query = (body.get("query") or "").strip() | |
| k = int(body.get("k") or 20) | |
| if not query: | |
| return jsonify({"error": "Empty query"}), 400 | |
| embedder = manager.get_embedder(backbone_key) | |
| image_index = manager.get_image_index(backbone_key) | |
| image_files = manager.get_image_files(backbone_key) | |
| qvec = embedder.encode_text(query).astype("float32").reshape(1, -1) | |
| faiss.normalize_L2(qvec) | |
| sims, idxs = image_index.search(qvec, min(k, len(image_files))) | |
| results = [ | |
| {"rank": r + 1, | |
| "filename": image_files[int(i)], | |
| "similarity": float(s), | |
| "image_url": url_for("serve_flickr8k_image", filename=image_files[int(i)])} | |
| for r, (i, s) in enumerate(zip(idxs[0], sims[0])) | |
| ] | |
| return jsonify({ | |
| "results": results, | |
| "total_results": len(results), | |
| "elapsed": round(time.perf_counter() - t0, 3), | |
| "backbone": backbone_key, | |
| "backbone_display": BACKBONES[backbone_key].display_name, | |
| }) | |
| def search_image_to_text(): | |
| t0 = time.perf_counter() | |
| try: | |
| backbone_key = _resolve_backbone(request) | |
| except ValueError as e: | |
| return jsonify({"error": str(e)}), 400 | |
| k = int(request.form.get("k") or 20) | |
| dataset_filename = request.form.get('dataset_filename') | |
| file = request.files.get("query_image") | |
| if file is not None and file.filename != "": | |
| if not allowed_file(file.filename): | |
| return jsonify({"error": "File type not allowed."}), 400 | |
| filepath = os.path.join(UPLOAD_FOLDER, secure_filename(file.filename)) | |
| file.save(filepath) | |
| elif dataset_filename: | |
| clean = secure_filename(dataset_filename) | |
| filepath = os.path.abspath(os.path.join(FLICKR8K_DATASET_PATH, clean)) | |
| if not filepath.startswith(os.path.abspath(FLICKR8K_DATASET_PATH) + os.sep) or not os.path.isfile(filepath): | |
| return jsonify({"error": "Dataset image not found."}), 404 | |
| else: | |
| return jsonify({"error": "No image uploaded"}), 400 | |
| embedder = manager.get_embedder(backbone_key) | |
| caption_index = manager.get_caption_index(backbone_key) | |
| captions = manager.get_captions(backbone_key) | |
| pil = Image.open(filepath).convert("RGB") | |
| qvec = embedder.encode_image(pil).astype("float32").reshape(1, -1) | |
| faiss.normalize_L2(qvec) | |
| sims, idxs = caption_index.search(qvec, min(k, len(captions))) | |
| caption_filenames = manager.get_caption_filenames(backbone_key) | |
| results = [ | |
| {"rank": r + 1, | |
| "caption": captions[int(i)], | |
| "similarity": float(s), | |
| "source_image": caption_filenames[int(i)], | |
| "image_url": url_for("serve_flickr8k_image", filename=caption_filenames[int(i)])} | |
| for r, (i, s) in enumerate(zip(idxs[0], sims[0])) | |
| ] | |
| return jsonify({ | |
| "results": results, | |
| "elapsed": round(time.perf_counter() - t0, 3), | |
| "backbone": backbone_key, | |
| "backbone_display": BACKBONES[backbone_key].display_name, | |
| }) | |
| def list_backbones(): | |
| return jsonify({ | |
| "backbones": manager.list_backbones(), | |
| "default": DEFAULT_BACKBONE, | |
| "max_resident": int(os.environ.get("MIR_MAX_BACKBONES", "1")), | |
| }) | |
| def _resolve_backbone(req) -> str: | |
| key = None | |
| if req.is_json: | |
| key = (req.get_json(silent=True) or {}).get("backbone") | |
| key = key or req.form.get("backbone") or req.args.get("backbone") or DEFAULT_BACKBONE | |
| if not manager.is_known(key): | |
| raise ValueError(f"Unknown backbone '{key}'") | |
| return key | |
| try: | |
| manager.preload(DEFAULT_BACKBONE) | |
| app.logger.info("Default backbone '%s' preloaded.", DEFAULT_BACKBONE) | |
| except Exception as e: | |
| app.logger.warning("Default backbone preload failed: %s", e) | |
| def login(): | |
| if current_user.is_authenticated: | |
| return redirect("/") | |
| error = None | |
| if request.method == "POST": | |
| username = request.form.get("username", "").strip() | |
| password = request.form.get("password", "") | |
| expected_user = os.environ.get("MIR_USERNAME", "admin") | |
| expected_hash = os.environ.get("MIR_PASSWORD_HASH", "") | |
| if username == expected_user and check_password_hash(expected_hash, password): | |
| login_user(_User(username), remember=True) | |
| return redirect(request.args.get("next") or "/") | |
| error = "Invalid credentials — access denied." | |
| return render_template("login.html", error=error) | |
| def logout(): | |
| logout_user() | |
| return redirect("/login") | |
| if __name__ == '__main__': | |
| app.run(port=5000, debug=os.environ.get('FLASK_DEBUG', 'false').lower() == 'true') | |