| import os |
| import logging |
| from glob import glob |
|
|
| import matplotlib.pyplot as plt |
| import numpy as np |
| import torch |
|
|
| from models.preprocessing import process_image_pil, process_metadata_pad20 |
| from models.model_loader import load_model, find_last_conv |
| from models.cam import GradCAMPlusPlus |
|
|
| logging.basicConfig(level=logging.INFO) |
|
|
|
|
| def _resolve_project_root() -> str: |
| app_root = os.path.abspath( |
| os.environ.get( |
| "APP_ROOT", |
| os.path.join(os.path.dirname(__file__), "..", ".."), |
| ) |
| ) |
| return app_root |
|
|
|
|
| def _resolve_data_root(project_root: str) -> str: |
| explicit_data_root = os.environ.get("DATA_ROOT") |
| if explicit_data_root: |
| return os.path.abspath(explicit_data_root) |
|
|
| candidates = [ |
| "/data", |
| os.path.join(project_root, "data"), |
| "/app/data", |
| ] |
|
|
| for candidate in candidates: |
| preprocess_dir = os.path.join(candidate, "preprocess_data") |
| weights_dir = os.path.join(candidate, "weights") |
| if os.path.exists(preprocess_dir) and os.path.exists(weights_dir): |
| return candidate |
|
|
| return os.path.join(project_root, "data") |
|
|
|
|
| DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
| PROJECT_ROOT = _resolve_project_root() |
| DATA_ROOT = _resolve_data_root(PROJECT_ROOT) |
|
|
| CLASS_LIST = ["NEV", "BCC", "ACK", "SEK", "SCC", "MEL"] |
|
|
| ENCODER_DIR = os.path.join(DATA_ROOT, "preprocess_data") |
| MODEL_ROOT_PATTERNS = [ |
| os.path.join( |
| DATA_ROOT, |
| "weights", |
| "TO_BE_USED", |
| "*", |
| "PAD-UFES-20", |
| "*_weights", |
| "*", |
| "*", |
| "model_*_with_one-hot-encoder_512_with_best_architecture", |
| ), |
| os.path.join( |
| DATA_ROOT, |
| "weights", |
| "TO_BE_USED", |
| "*", |
| "model_*_with_one-hot-encoder_512_with_best_architecture", |
| ), |
| ] |
|
|
| PREFERRED_FOLD = 3 |
| IMPLEMENTED_ATTENTION_MECHANISMS = { |
| "no-metadata", |
| "no-metadata-without-mlp", |
| "concatenation", |
| "crossattention", |
| "weighted", |
| "gfcam", |
| "cross-weights-after-crossattention", |
| "metablock", |
| "only-with-att-intramodal+residual", |
| "att-intramodal+residual", |
| "att-intramodal+residual+cross-attention-metadados", |
| "att-intramodal+residual+cross-attention-metadados+metablock", |
| "att-intramodal+residual+cross-attention-metadados+att-intramodal+residual", |
| } |
|
|
| MODEL_CONFIGS = {} |
| MODEL_LABELS = {} |
| MODEL_CACHE = {} |
| DEFAULT_MODEL_KEY = None |
| _DISCOVERED = False |
|
|
|
|
| def _debug_paths() -> None: |
| print(f"[inference] DEVICE={DEVICE}") |
| print(f"[inference] PROJECT_ROOT={PROJECT_ROOT}") |
| print(f"[inference] DATA_ROOT={DATA_ROOT}") |
| print(f"[inference] DATA_ROOT exists? {os.path.exists(DATA_ROOT)}") |
| print(f"[inference] ENCODER_DIR={ENCODER_DIR}") |
| print(f"[inference] ENCODER_DIR exists? {os.path.exists(ENCODER_DIR)}") |
|
|
| data_root = DATA_ROOT |
| weights_root = os.path.join(data_root, "weights") |
| to_be_used_root = os.path.join(weights_root, "TO_BE_USED") |
|
|
| print(f"[inference] data root={data_root}") |
| print(f"[inference] data root exists? {os.path.exists(data_root)}") |
| print(f"[inference] weights root={weights_root}") |
| print(f"[inference] weights root exists? {os.path.exists(weights_root)}") |
| print(f"[inference] TO_BE_USED root={to_be_used_root}") |
| print(f"[inference] TO_BE_USED root exists? {os.path.exists(to_be_used_root)}") |
|
|
| for pattern in MODEL_ROOT_PATTERNS: |
| print(f"[inference] MODEL_ROOT_PATTERN={pattern}") |
|
|
|
|
| def _parse_cnn_model_name(model_dir_name: str): |
| prefix = "model_" |
| suffix = "_with_one-hot-encoder_512_with_best_architecture" |
| if not model_dir_name.startswith(prefix) or not model_dir_name.endswith(suffix): |
| return None |
| return model_dir_name[len(prefix):-len(suffix)] |
|
|
|
|
| def _find_fold_dir(model_root: str, cnn_model_name: str): |
| fold_dirs = sorted(glob(os.path.join(model_root, f"{cnn_model_name}_fold_*"))) |
| if not fold_dirs: |
| return None |
|
|
| preferred = os.path.join(model_root, f"{cnn_model_name}_fold_{PREFERRED_FOLD}") |
| if os.path.exists(os.path.join(preferred, "model.pth")): |
| return preferred |
|
|
| for fold_dir in fold_dirs: |
| if os.path.exists(os.path.join(fold_dir, "model.pth")): |
| return fold_dir |
|
|
| return None |
|
|
|
|
| def _extract_fold_number(fold_dir: str) -> str: |
| name = os.path.basename(fold_dir) |
| return name.split("_fold_")[-1] if "_fold_" in name else "?" |
|
|
|
|
| def _extract_path_metadata(model_root: str): |
| parts = os.path.normpath(model_root).split(os.sep) |
| mechanism = os.path.basename(os.path.dirname(model_root)) |
| unfreeze_weights = "unfrozen_weights" |
| num_heads = "8" |
|
|
| if "PAD-UFES-20" in parts: |
| idx = parts.index("PAD-UFES-20") |
| if len(parts) > idx + 1: |
| unfreeze_weights = parts[idx + 1] |
| if len(parts) > idx + 2: |
| num_heads = parts[idx + 2] |
| if len(parts) > idx + 3: |
| mechanism = parts[idx + 3] |
|
|
| return mechanism, unfreeze_weights, num_heads |
|
|
|
|
| def _discover_models() -> None: |
| global DEFAULT_MODEL_KEY |
|
|
| print("[inference] starting model discovery...") |
| MODEL_CONFIGS.clear() |
| MODEL_LABELS.clear() |
| DEFAULT_MODEL_KEY = None |
|
|
| model_roots = sorted({ |
| path |
| for pattern in MODEL_ROOT_PATTERNS |
| for path in glob(pattern) |
| }) |
|
|
| print(f"[inference] candidate model roots found: {len(model_roots)}") |
| for idx, path in enumerate(model_roots[:20], start=1): |
| print(f"[inference] candidate[{idx}] = {path}") |
| if len(model_roots) > 20: |
| print("[inference] ... additional candidates omitted from log ...") |
|
|
| for model_root in model_roots: |
| mechanism, unfreeze_weights, num_heads = _extract_path_metadata(model_root) |
| model_dir_name = os.path.basename(model_root) |
| cnn_model_name = _parse_cnn_model_name(model_dir_name) |
|
|
| if cnn_model_name is None: |
| print(f"[inference] skipping invalid model dir name: {model_dir_name}") |
| continue |
|
|
| fold_dir = _find_fold_dir(model_root, cnn_model_name) |
| if fold_dir is None: |
| print(f"[inference] no valid fold dir found for: {model_root}") |
| continue |
|
|
| model_path = os.path.join(fold_dir, "model.pth") |
| if not os.path.exists(model_path): |
| print(f"[inference] missing model.pth: {model_path}") |
| continue |
|
|
| fold_number = _extract_fold_number(fold_dir) |
| model_key = f"{mechanism}|{cnn_model_name}|{unfreeze_weights}|{num_heads}|{fold_number}" |
| supported = mechanism in IMPLEMENTED_ATTENTION_MECHANISMS |
| support_tag = "" if supported else " | not-implemented" |
| label = f"{mechanism} | {cnn_model_name} | fold {fold_number} | {unfreeze_weights}{support_tag}" |
|
|
| MODEL_CONFIGS[model_key] = { |
| "model_path": model_path, |
| "attention_mecanism": mechanism, |
| "cnn_model_name": cnn_model_name, |
| "unfreeze_weights": unfreeze_weights, |
| "num_heads": int(num_heads) if str(num_heads).isdigit() else 8, |
| "supported": supported, |
| "label": label, |
| } |
| MODEL_LABELS[model_key] = label |
| print(f"[inference] registered model: {label}") |
|
|
| preferred_defaults = [ |
| key for key, cfg in MODEL_CONFIGS.items() |
| if cfg["attention_mecanism"] == "gfcam" |
| and cfg["cnn_model_name"] == "densenet169" |
| and cfg["unfreeze_weights"] == "unfrozen_weights" |
| and cfg["model_path"].endswith(f"_fold_{PREFERRED_FOLD}/model.pth") |
| ] |
|
|
| if preferred_defaults: |
| DEFAULT_MODEL_KEY = preferred_defaults[0] |
| elif MODEL_CONFIGS: |
| DEFAULT_MODEL_KEY = sorted(MODEL_CONFIGS.keys())[0] |
|
|
| print(f"[inference] discovery done. models={len(MODEL_CONFIGS)}") |
| print(f"[inference] DEFAULT_MODEL_KEY={DEFAULT_MODEL_KEY}") |
|
|
|
|
| def ensure_models_discovered() -> None: |
| global _DISCOVERED |
| if _DISCOVERED: |
| return |
|
|
| _debug_paths() |
| _discover_models() |
| _DISCOVERED = True |
|
|
|
|
| def get_available_model_choices(): |
| ensure_models_discovered() |
| return [(MODEL_LABELS[key], key) for key in sorted(MODEL_LABELS.keys())] |
|
|
|
|
| def get_default_model_key(): |
| ensure_models_discovered() |
| return DEFAULT_MODEL_KEY |
|
|
|
|
| def get_model_label(model_key): |
| ensure_models_discovered() |
| return MODEL_LABELS.get(model_key, "Unknown model") |
|
|
|
|
| def _get_model_and_cam(model_key): |
| ensure_models_discovered() |
|
|
| if not MODEL_CONFIGS: |
| raise RuntimeError( |
| f"No compatible model checkpoints were found in " |
| f"{os.path.join(DATA_ROOT, 'weights')}. " |
| f"Please verify that the model assets were uploaded to the Space." |
| ) |
|
|
| if not os.path.exists(ENCODER_DIR): |
| raise RuntimeError( |
| f"Metadata encoder directory not found: {ENCODER_DIR}. " |
| "Please verify that preprocess artifacts were uploaded to the Space." |
| ) |
|
|
| if model_key is None: |
| model_key = DEFAULT_MODEL_KEY |
|
|
| if model_key not in MODEL_CONFIGS: |
| raise RuntimeError( |
| f"Selected model key is invalid: {model_key}. " |
| "Please choose a valid model option in the interface." |
| ) |
|
|
| cfg = MODEL_CONFIGS[model_key] |
| if not cfg["supported"]: |
| raise RuntimeError( |
| f"The selected mechanism '{cfg['attention_mecanism']}' exists in TO_BE_USED, " |
| "but is not implemented in the current inference model forward." |
| ) |
|
|
| if model_key not in MODEL_CACHE: |
| model_path = cfg["model_path"] |
| print(f"[inference] loading model from: {model_path}") |
|
|
| model = load_model( |
| device=DEVICE, |
| model_path=model_path, |
| cnn_model_name=cfg["cnn_model_name"], |
| attention_mecanism=cfg["attention_mecanism"], |
| num_heads=cfg["num_heads"], |
| unfreeze_weights=cfg["unfreeze_weights"], |
| ) |
|
|
| print("[inference] locating final conv layer...") |
| target_layer = find_last_conv(model.image_encoder) |
|
|
| print("[inference] creating GradCAM++ object...") |
| MODEL_CACHE[model_key] = (model, GradCAMPlusPlus(model, target_layer)) |
| print("[inference] model ready.") |
|
|
| return MODEL_CACHE[model_key] |
|
|
|
|
| def run_inference(image_pil, metadata_text, model_key=None): |
| model, cam = _get_model_and_cam(model_key) |
|
|
| print("[inference] processing image...") |
| image_tensor = process_image_pil(image_pil, DEVICE) |
|
|
| print("[inference] processing metadata...") |
| metadata_tensor = process_metadata_pad20( |
| metadata_text, |
| ENCODER_DIR, |
| DEVICE |
| ) |
|
|
| print("[inference] running forward pass...") |
| with torch.no_grad(): |
| logits = model(image_tensor, metadata_tensor) |
| probs = torch.softmax(logits, dim=1) |
|
|
| pred_class = torch.argmax(probs, dim=1).item() |
| confidence = probs[0, pred_class].item() |
|
|
| print("[inference] generating heatmap...") |
| heatmap = cam.generate( |
| image_tensor, |
| metadata_tensor, |
| pred_class |
| ) |
|
|
| image_np = np.array(image_pil.convert("RGB")) |
| heatmap = np.asarray(heatmap, dtype=np.float32).squeeze() |
|
|
| if heatmap.shape != image_np.shape[:2]: |
| heatmap_tensor = torch.from_numpy(heatmap).unsqueeze(0).unsqueeze(0) |
| heatmap = torch.nn.functional.interpolate( |
| heatmap_tensor, |
| size=image_np.shape[:2], |
| mode="bilinear", |
| align_corners=False, |
| ).squeeze().cpu().numpy() |
|
|
| heatmap = np.clip(heatmap, 0.0, 1.0) |
| alpha_map = np.clip(heatmap * 0.6, 0.0, 0.6) |
|
|
| fig, ax = plt.subplots(figsize=(6, 6)) |
| ax.imshow(image_np) |
| ax.imshow(heatmap, cmap="jet", alpha=alpha_map) |
| ax.axis("off") |
|
|
| title = f"{CLASS_LIST[pred_class]} | conf={confidence:.3f}" |
| ax.set_title(title) |
|
|
| fig.canvas.draw() |
| result = np.array(fig.canvas.renderer.buffer_rgba()) |
| plt.close(fig) |
|
|
| print(f"[inference] inference complete: {title}") |
| return result, title |
|
|