import os # 1. Toujours importer `spaces` EN PREMIER avant torch (évite le RuntimeError ZeroGPU) try: import spaces HAS_SPACES = True except ImportError: HAS_SPACES = False import cv2 import numpy as np from PIL import Image import torch import torch.nn as nn from torchvision import transforms import gradio as gr # ===================================================================== # 1. CONFIGURATION & ARCHITECTURE DU MODÈLE # ===================================================================== IMG_SIZE = 256 MODEL_WEIGHTS_PATH = "best_casting_model.pth" DEFAULT_THRESHOLD = 0.40 CLASS_NAMES = { 0: "Conforme (OK)", 1: "Défectueuse (DÉFAUT)" } # Initialisation sur CPU au démarrage pour ne pas déclencher d'erreur NVML/ZeroGPU. # (Un CNN 4-blocs 256x256 s'exécute en ~15ms sur CPU). device = torch.device("cpu") class CustomIndustrialCNN(nn.Module): def __init__(self): super(CustomIndustrialCNN, self).__init__() self.features = nn.Sequential( # Bloc 1 : 256x256 -> 128x128 nn.Conv2d(1, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), # Bloc 2 : 128x128 -> 64x64 nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), # Bloc 3 : 64x64 -> 32x32 nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), # Bloc 4 : 32x32 -> 16x16 nn.Conv2d(128, 256, kernel_size=3, padding=1), nn.BatchNorm2d(256), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), ) self.global_pool = nn.AdaptiveAvgPool2d((1, 1)) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(256, 128), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(128, 1) ) def forward(self, x): x = self.features(x) x = self.global_pool(x) x = self.classifier(x) return x # ===================================================================== # 2. CHARGEMENT DU MODÈLE ET DES POIDS # ===================================================================== model = CustomIndustrialCNN().to(device) if os.path.exists(MODEL_WEIGHTS_PATH): checkpoint = torch.load(MODEL_WEIGHTS_PATH, map_location=device, weights_only=False) if isinstance(checkpoint, dict) and "model_state_dict" in checkpoint: model.load_state_dict(checkpoint["model_state_dict"]) if "optimal_threshold" in checkpoint: DEFAULT_THRESHOLD = float(checkpoint["optimal_threshold"]) else: model.load_state_dict(checkpoint) print(f"Poids chargés avec succès depuis '{MODEL_WEIGHTS_PATH}' sur {device}.") else: print(f"ATTENTION : '{MODEL_WEIGHTS_PATH}' introuvable.") model.eval() # ===================================================================== # 3. PRÉTRAITEMENT # ===================================================================== val_test_transforms = transforms.Compose([ transforms.Grayscale(num_output_channels=1), transforms.Resize((IMG_SIZE, IMG_SIZE)), transforms.ToTensor(), transforms.Normalize(mean=[0.5], std=[0.5]) ]) # ===================================================================== # 4. EXPLICABILITÉ : GRAD-CAM # ===================================================================== def find_last_conv_layer(net): last_conv = None for module in net.modules(): if isinstance(module, nn.Conv2d): last_conv = module return last_conv def generate_gradcam(model, input_tensor, original_pil_img): activations = [] gradients = [] target_layer = find_last_conv_layer(model) if target_layer is None: return np.array(original_pil_img.convert("RGB")), None def forward_hook(module, inp, out): activations.append(out) def backward_hook(module, grad_in, grad_out): gradients.append(grad_out[0]) handle_f = target_layer.register_forward_hook(forward_hook) handle_b = target_layer.register_full_backward_hook(backward_hook) try: model.zero_grad() input_tensor.requires_grad_(True) output = model(input_tensor) logit = output.squeeze() prob_defect = torch.sigmoid(logit).item() logit.backward() grads = gradients[0].detach().cpu().numpy()[0] acts = activations[0].detach().cpu().numpy()[0] weights = np.mean(grads, axis=(1, 2)) cam = np.zeros(acts.shape[1:], dtype=np.float32) for i, w in enumerate(weights): cam += w * acts[i] cam = np.maximum(cam, 0) if np.max(cam) > 0: cam = cam / np.max(cam) orig_w, orig_h = original_pil_img.size cam_resized = cv2.resize(cam, (orig_w, orig_h)) heatmap = np.uint8(255 * cam_resized) heatmap_colored = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET) heatmap_colored = cv2.cvtColor(heatmap_colored, cv2.COLOR_BGR2RGB) orig_rgb = np.array(original_pil_img.convert("L").convert("RGB")) overlay = cv2.addWeighted(orig_rgb, 0.55, heatmap_colored, 0.45, 0) return overlay, prob_defect finally: handle_f.remove() handle_b.remove() # ===================================================================== # 5. FONCTION D'INFÉRENCE GRADIO # ===================================================================== def inspect_casting_part(image: Image.Image, threshold: float): if image is None: return None, None, "
{action_msg}