# # import torch # # import torch.nn as nn # # import torch.nn.functional as F # # from torchvision import models, transforms # # import os # # import numpy as np # # import cv2 # # from PIL import Image # # from huggingface_hub import hf_hub_download # # import os # # MODEL_REPO = "Omamaa12/iris-models" # # device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # # # ========================= # # # PHASE 1 MODEL (ResNet18) # # # ========================= # # def load_phase1_model(model_path): # # """Load the iris/non-iris classifier""" # # model = models.resnet18(weights=None) # # num_ftrs = model.fc.in_features # # model.fc = nn.Linear(num_ftrs, 2) # # state_dict = torch.load(model_path, map_location=device, weights_only=False) # # model.load_state_dict(state_dict) # # model.to(device) # # model.eval() # # return model # # phase1_transform = transforms.Compose([ # # transforms.Resize((128, 128)), # # transforms.ToTensor(), # # ]) # # def get_phase1_model(): # # path = hf_hub_download( # # repo_id=MODEL_REPO, # # filename="phase1_iris_classifier.pth" # # ) # # return load_phase1_model(path) # # class DNetPAD(nn.Module): # # def __init__(self, num_classes=2): # # super(DNetPAD, self).__init__() # # self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1) # # self.bn1 = nn.BatchNorm2d(32) # # self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1) # # self.bn2 = nn.BatchNorm2d(64) # # self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1) # # self.bn3 = nn.BatchNorm2d(128) # # self.conv4 = nn.Conv2d(128, 256, kernel_size=3, padding=1) # # self.bn4 = nn.BatchNorm2d(256) # # self.pool = nn.MaxPool2d(2, 2) # # self.dropout = nn.Dropout(0.5) # # self.gap = nn.AdaptiveAvgPool2d(1) # # self.fc1 = nn.Linear(256, 128) # # self.fc2 = nn.Linear(128, 64) # # self.fc3 = nn.Linear(64, num_classes) # # # ✅ REQUIRED FOR GRAD-CAM # # self.last_conv_output = None # # def forward(self, x): # # x = self.pool(F.relu(self.bn1(self.conv1(x)))) # # x = self.pool(F.relu(self.bn2(self.conv2(x)))) # # x = self.pool(F.relu(self.bn3(self.conv3(x)))) # # # last conv block (IMPORTANT for explainability) # # # x = F.relu(self.bn4(self.conv4(x))) # # x = F.relu(self.bn4(self.conv4(x))) # # # REQUIRED FOR GRAD-CAM # # self.last_conv_output = x # # self.last_conv_output.retain_grad() # # # ✅ REQUIRED FOR GRAD-CAM # # x.retain_grad() # # self.last_conv_output = x # # x = self.pool(x) # # x = self.gap(x) # # x = x.view(x.size(0), -1) # # x = F.relu(self.fc1(x)) # # x = self.dropout(x) # # x = F.relu(self.fc2(x)) # # x = self.dropout(x) # # x = self.fc3(x) # # return x # # def load_phase2_model(model_path): # # """Load the PAD (Presentation Attack Detection) model""" # # model = DNetPAD(num_classes=2) # # checkpoint = torch.load(model_path, map_location=device, weights_only=False) # # # Handle different save formats # # if isinstance(checkpoint, dict) and "model_state_dict" in checkpoint: # # model.load_state_dict(checkpoint["model_state_dict"]) # # elif isinstance(checkpoint, dict) and "state_dict" in checkpoint: # # model.load_state_dict(checkpoint["state_dict"]) # # elif isinstance(checkpoint, dict): # # model.load_state_dict(checkpoint) # # else: # # model = checkpoint # # model.to(device) # # model.eval() # # return model # # phase2_transform = transforms.Compose([ # # transforms.Resize((224, 224)), # # transforms.ToTensor(), # # transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) # # ]) # # def get_region_explanation(heatmap): # # """ # # Converts Grad-CAM heatmap → human-readable region focus # # """ # # h, w = heatmap.shape # # # split into regions # # center = heatmap[h//3:2*h//3, w//3:2*w//3] # # top = heatmap[0:h//3, :] # # bottom = heatmap[2*h//3:h, :] # # left = heatmap[:, 0:w//3] # # right = heatmap[:, 2*w//3:w] # # scores = { # # "center iris region": np.mean(center), # # "upper iris region": np.mean(top), # # "lower iris region": np.mean(bottom), # # "left iris region": np.mean(left), # # "right iris region": np.mean(right), # # } # # # sort by importance # # sorted_regions = sorted(scores.items(), key=lambda x: x[1], reverse=True) # # top_region = sorted_regions[0][0] # # top_score = sorted_regions[0][1] # # if top_score < 0.1: # # return "Model focused on low-intensity features across full iris" # # return f"Model focused mainly on {top_region}" # # def generate_explanation(pred_class, confidence): # # """ # # Converts model output → human explanation # # (THIS is what you're missing) # # """ # # if pred_class == 1: # FAKE # # if confidence > 0.9: # # return "Strong spoof detected. Likely printed or displayed iris pattern." # # elif confidence > 0.75: # # return "Possible presentation attack (screen replay or printed image)." # # else: # # return "Weak spoof indicators detected. Uncertain attack type." # # else: # REAL # # if confidence > 0.9: # # return "High-quality genuine iris texture detected." # # else: # # return "Likely real iris but image quality is moderate." # # def pad_explainable(image_path, model): # # """ # # FULL FIXED VERSION: # # - prediction # # - confidence # # - Grad-CAM # # - region explanation # # - human explanation # # """ # # img = Image.open(image_path).convert("RGB") # # input_tensor = phase2_transform(img).unsqueeze(0).to(device) # # model.zero_grad() # # # forward # # outputs = model(input_tensor) # # probs = torch.softmax(outputs, dim=1) # # pred_class = torch.argmax(probs, dim=1).item() # # confidence = probs[0, pred_class].item() # # # 🔥 IMPORTANT: force gradient tracking # # # input_tensor.requires_grad = True # # # backward # # score = outputs[0, pred_class] # # score.backward(retain_graph=True) # # # ========================= # # # GRAD-CAM (FIXED) # # # ========================= # # activations = model.last_conv_output # (1, C, H, W) # # # gradients = model.last_conv_output.grad # # gradients = torch.autograd.grad( # # outputs=outputs[:, pred_class], # # inputs=model.last_conv_output, # # retain_graph=True # # )[0] # # if gradients is None: # # raise ValueError( # # "Gradients are None. You MUST add retain_grad() in forward for last_conv_output." # # ) # # pooled_gradients = torch.mean(gradients, dim=[0, 2, 3]) # # for i in range(activations.shape[1]): # # activations[:, i, :, :] *= pooled_gradients[i] # # heatmap = torch.mean(activations, dim=1).squeeze().detach().cpu().numpy() # # heatmap = np.maximum(heatmap, 0) # # if np.max(heatmap) > 0: # # heatmap /= np.max(heatmap) # # heatmap_resized = cv2.resize(heatmap, (224, 224)) # # # ========================= # # # REGION EXPLANATION # # # ========================= # # region_text = get_region_explanation(heatmap_resized) # # # ========================= # # # HUMAN EXPLANATION # # # ========================= # # explanation_text = generate_explanation(pred_class, confidence) # # # ========================= # # # HEATMAP VISUAL # # # ========================= # # heatmap_color = np.uint8(255 * heatmap_resized) # # heatmap_color = cv2.applyColorMap(heatmap_color, cv2.COLORMAP_JET) # # heatmap_color = cv2.cvtColor(heatmap_color, cv2.COLOR_BGR2RGB) # # overlay = cv2.addWeighted( # # np.array(img.resize((224, 224))), 0.6, # # heatmap_color, 0.4, 0 # # ) # # return { # # "prediction": "FAKE" if pred_class == 1 else "REAL", # # "confidence": float(confidence), # # # 🔥 NEW THINGS YOU WANTED # # "human_explanation": explanation_text, # # "region_explanation": region_text, # # # visualization # # "heatmap_image": overlay # # } # # # ========================= # # # GAN MODEL (Generator) # # # ========================= # # class Generator(nn.Module): # # """GAN Generator for synthetic iris image generation""" # # def __init__(self, z_dim=128, channels=3, feature_maps=128): # # super(Generator, self).__init__() # # self.init_layer = nn.Sequential( # # nn.ConvTranspose2d(z_dim, feature_maps * 8, 4, 1, 0, bias=False), # # nn.BatchNorm2d(feature_maps * 8), # # nn.ReLU(True) # # ) # # self.middle_layers = nn.Sequential( # # nn.ConvTranspose2d(feature_maps * 8, feature_maps * 4, 4, 2, 1, bias=False), # # nn.BatchNorm2d(feature_maps * 4), # # nn.ReLU(True), # # nn.ConvTranspose2d(feature_maps * 4, feature_maps * 2, 4, 2, 1, bias=False), # # nn.BatchNorm2d(feature_maps * 2), # # nn.ReLU(True), # # nn.ConvTranspose2d(feature_maps * 2, feature_maps, 4, 2, 1, bias=False), # # nn.BatchNorm2d(feature_maps), # # nn.ReLU(True), # # nn.ConvTranspose2d(feature_maps, feature_maps, 4, 2, 1, bias=False), # # nn.BatchNorm2d(feature_maps), # # nn.ReLU(True), # # nn.ConvTranspose2d(feature_maps, feature_maps // 2, 4, 2, 1, bias=False), # # nn.BatchNorm2d(feature_maps // 2), # # nn.ReLU(True), # # nn.ConvTranspose2d(feature_maps // 2, channels, 4, 2, 1, bias=False), # # nn.Tanh() # # ) # # def forward(self, noise): # # x = self.init_layer(noise) # # x = self.middle_layers(x) # # return x # # def load_gan_model(model_path, z_dim=128): # # """Load the GAN generator model""" # # model = Generator(z_dim=z_dim) # # checkpoint = torch.load(model_path, map_location=device, weights_only=False) # # # Handle different save formats # # if isinstance(checkpoint, dict) and "G_state_dict" in checkpoint: # # model.load_state_dict(checkpoint["G_state_dict"]) # # elif isinstance(checkpoint, dict) and "model_state_dict" in checkpoint: # # model.load_state_dict(checkpoint["model_state_dict"]) # # else: # # model.load_state_dict(checkpoint) # # model.to(device) # # model.eval() # # return model # import torch # import torch.nn as nn # import torch.nn.functional as F # from torchvision import models, transforms # import os # import numpy as np # import cv2 # from PIL import Image # from huggingface_hub import hf_hub_download # import os # MODEL_REPO = "Omamaa12/iris-models" # device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # # ========================= # # PHASE 1 MODEL (ResNet18) # # ========================= # def load_phase1_model(model_path): # """Load the iris/non-iris classifier""" # model = models.resnet18(weights=None) # num_ftrs = model.fc.in_features # model.fc = nn.Linear(num_ftrs, 2) # state_dict = torch.load(model_path, map_location=device, weights_only=False) # model.load_state_dict(state_dict) # model.to(device) # model.eval() # return model # phase1_transform = transforms.Compose([ # transforms.Resize((128, 128)), # transforms.ToTensor(), # ]) # def get_phase1_model(): # path = hf_hub_download( # repo_id=MODEL_REPO, # filename="phase1_iris_classifier.pth" # ) # return load_phase1_model(path) # # ========================= # # FIXED: PHASE 2 LOADER FROM HF (ADDED) # # ========================= # def get_phase2_model(): # path = hf_hub_download( # repo_id=MODEL_REPO, # filename="dnet_pad_model.pth" # ) # return load_phase2_model(path) # class DNetPAD(nn.Module): # def __init__(self, num_classes=2): # super(DNetPAD, self).__init__() # self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1) # self.bn1 = nn.BatchNorm2d(32) # self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1) # self.bn2 = nn.BatchNorm2d(64) # self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1) # self.bn3 = nn.BatchNorm2d(128) # self.conv4 = nn.Conv2d(128, 256, kernel_size=3, padding=1) # self.bn4 = nn.BatchNorm2d(256) # self.pool = nn.MaxPool2d(2, 2) # self.dropout = nn.Dropout(0.5) # self.gap = nn.AdaptiveAvgPool2d(1) # self.fc1 = nn.Linear(256, 128) # self.fc2 = nn.Linear(128, 64) # self.fc3 = nn.Linear(64, num_classes) # # FIXED (ONLY ONCE) # self.last_conv_output = None # def forward(self, x): # x = self.pool(F.relu(self.bn1(self.conv1(x)))) # x = self.pool(F.relu(self.bn2(self.conv2(x)))) # x = self.pool(F.relu(self.bn3(self.conv3(x)))) # x = F.relu(self.bn4(self.conv4(x))) # # FIXED: correct grad-cam hook (NO DUPLICATE) # self.last_conv_output = x # self.last_conv_output.retain_grad() # x = self.pool(x) # x = self.gap(x) # x = x.view(x.size(0), -1) # x = F.relu(self.fc1(x)) # x = self.dropout(x) # x = F.relu(self.fc2(x)) # x = self.dropout(x) # x = self.fc3(x) # return x # def load_phase2_model(model_path): # """Load the PAD (Presentation Attack Detection) model""" # model = DNetPAD(num_classes=2) # checkpoint = torch.load(model_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"]) # elif isinstance(checkpoint, dict) and "state_dict" in checkpoint: # model.load_state_dict(checkpoint["state_dict"]) # elif isinstance(checkpoint, dict): # model.load_state_dict(checkpoint) # else: # model = checkpoint # model.to(device) # model.eval() # return model # phase2_transform = transforms.Compose([ # transforms.Resize((224, 224)), # transforms.ToTensor(), # transforms.Normalize(mean=[0.485, 0.456, 0.406], # std=[0.229, 0.224, 0.225]) # ]) # def get_region_explanation(heatmap): # h, w = heatmap.shape # center = heatmap[h//3:2*h//3, w//3:2*w//3] # top = heatmap[0:h//3, :] # bottom = heatmap[2*h//3:h, :] # left = heatmap[:, 0:w//3] # right = heatmap[:, 2*w//3:w] # scores = { # "center iris region": np.mean(center), # "upper iris region": np.mean(top), # "lower iris region": np.mean(bottom), # "left iris region": np.mean(left), # "right iris region": np.mean(right), # } # sorted_regions = sorted(scores.items(), key=lambda x: x[1], reverse=True) # top_region = sorted_regions[0][0] # top_score = sorted_regions[0][1] # if top_score < 0.1: # return "Model focused on low-intensity features across full iris" # return f"Model focused mainly on {top_region}" # def generate_explanation(pred_class, confidence): # if pred_class == 1: # if confidence > 0.9: # return "Strong spoof detected. Likely printed or displayed iris pattern." # elif confidence > 0.75: # return "Possible presentation attack (screen replay or printed image)." # else: # return "Weak spoof indicators detected. Uncertain attack type." # else: # if confidence > 0.9: # return "High-quality genuine iris texture detected." # else: # return "Likely real iris but image quality is moderate." # def pad_explainable(image_path, model): # img = Image.open(image_path).convert("RGB") # input_tensor = phase2_transform(img).unsqueeze(0).to(device) # model.zero_grad() # outputs = model(input_tensor) # probs = torch.softmax(outputs, dim=1) # pred_class = torch.argmax(probs, dim=1).item() # confidence = probs[0, pred_class].item() # score = outputs[0, pred_class] # score.backward(retain_graph=True) # activations = model.last_conv_output # gradients = torch.autograd.grad( # outputs=outputs[:, pred_class], # inputs=model.last_conv_output, # retain_graph=True # )[0] # if gradients is None: # raise ValueError("Gradients missing") # pooled_gradients = torch.mean(gradients, dim=[0, 2, 3]) # for i in range(activations.shape[1]): # activations[:, i, :, :] *= pooled_gradients[i] # heatmap = torch.mean(activations, dim=1).squeeze().detach().cpu().numpy() # heatmap = np.maximum(heatmap, 0) # if np.max(heatmap) > 0: # heatmap /= np.max(heatmap) # heatmap_resized = cv2.resize(heatmap, (224, 224)) # region_text = get_region_explanation(heatmap_resized) # explanation_text = generate_explanation(pred_class, confidence) # heatmap_color = np.uint8(255 * heatmap_resized) # heatmap_color = cv2.applyColorMap(heatmap_color, cv2.COLORMAP_JET) # heatmap_color = cv2.cvtColor(heatmap_color, cv2.COLOR_BGR2RGB) # overlay = cv2.addWeighted( # np.array(img.resize((224, 224))), 0.6, # heatmap_color, 0.4, 0 # ) # return { # "prediction": "FAKE" if pred_class == 1 else "REAL", # "confidence": float(confidence), # "human_explanation": explanation_text, # "region_explanation": region_text, # "heatmap_image": overlay # } # # ========================= # # GAN MODEL # # ========================= # class Generator(nn.Module): # def __init__(self, z_dim=128, channels=3, feature_maps=128): # super(Generator, self).__init__() # self.init_layer = nn.Sequential( # nn.ConvTranspose2d(z_dim, feature_maps * 8, 4, 1, 0, bias=False), # nn.BatchNorm2d(feature_maps * 8), # nn.ReLU(True) # ) # self.middle_layers = nn.Sequential( # nn.ConvTranspose2d(feature_maps * 8, feature_maps * 4, 4, 2, 1, bias=False), # nn.BatchNorm2d(feature_maps * 4), # nn.ReLU(True), # nn.ConvTranspose2d(feature_maps * 4, feature_maps * 2, 4, 2, 1, bias=False), # nn.BatchNorm2d(feature_maps * 2), # nn.ReLU(True), # nn.ConvTranspose2d(feature_maps * 2, feature_maps, 4, 2, 1, bias=False), # nn.BatchNorm2d(feature_maps), # nn.ReLU(True), # nn.ConvTranspose2d(feature_maps, feature_maps, 4, 2, 1, bias=False), # nn.BatchNorm2d(feature_maps), # nn.ReLU(True), # nn.ConvTranspose2d(feature_maps, feature_maps // 2, 4, 2, 1, bias=False), # nn.BatchNorm2d(feature_maps // 2), # nn.ReLU(True), # nn.ConvTranspose2d(feature_maps // 2, channels, 4, 2, 1, bias=False), # nn.Tanh() # ) # def forward(self, noise): # x = self.init_layer(noise) # return self.middle_layers(x) # def load_gan_model(model_path, z_dim=128): # model = Generator(z_dim=z_dim) # checkpoint = torch.load(model_path, map_location=device, weights_only=False) # if isinstance(checkpoint, dict) and "G_state_dict" in checkpoint: # model.load_state_dict(checkpoint["G_state_dict"]) # elif isinstance(checkpoint, dict) and "model_state_dict" in checkpoint: # model.load_state_dict(checkpoint["model_state_dict"]) # else: # model.load_state_dict(checkpoint) # model.to(device) # model.eval() # return model import os import numpy as np import cv2 from PIL import Image import torch import torch.nn as nn import torch.nn.functional as F from torchvision import models, transforms from huggingface_hub import hf_hub_download import timm # ───────────────────────────────────────── # HuggingFace repo (set HF_TOKEN env var if repo is private) # ───────────────────────────────────────── MODEL_REPO = "Omamaa12/iris-models" HF_TOKEN = os.getenv("HF_TOKEN") # None is fine if repo is public device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # ───────────────────────────────────────── # Helper: download a file from HuggingFace once and cache it # ───────────────────────────────────────── def _hf_download(filename): return hf_hub_download( repo_id=MODEL_REPO, filename=filename, token=HF_TOKEN, ) # ========================= # PHASE 1 — Iris / Non-Iris classifier (ResNet18) # ========================= # phase1_transform = transforms.Compose([ # transforms.Resize((128, 128)), # transforms.ToTensor(), # ]) # def load_phase1_model(): # """Download phase1_iris_classifier.pth from HF and load it.""" # path = _hf_download("phase1_iris_classifier.pth") # model = models.resnet18(weights=None) # model.fc = nn.Linear(model.fc.in_features, 2) # state_dict = torch.load(path, map_location=device, weights_only=False) # model.load_state_dict(state_dict) # model.to(device).eval() # return model # ========================= # PHASE 1 — Mobile Biometric Classifier (EfficientNet-B0, 4-class) # Classes (alphabetical ImageFolder order): # 0: animal_eye 1: animals 2: human_iris 3: other # ========================= class BiometricClassifier(nn.Module): def __init__(self, num_classes=4): super().__init__() self.backbone = timm.create_model( 'efficientnet_b2', pretrained=False, num_classes=0, global_pool='avg' ) feat = self.backbone.num_features # 1408 self.head_main = nn.Sequential( nn.Dropout(0.3), nn.Linear(feat, 512), nn.BatchNorm1d(512), nn.GELU(), nn.Dropout(0.2), nn.Linear(512, num_classes), ) self.head_eye = nn.Sequential( nn.Dropout(0.3), nn.Linear(feat, 256), nn.BatchNorm1d(256), nn.GELU(), nn.Dropout(0.2), nn.Linear(256, 2), ) self.head_disc = nn.Sequential( nn.Dropout(0.3), nn.Linear(feat, 512), nn.BatchNorm1d(512), nn.GELU(), nn.Dropout(0.2), nn.Linear(512, 256), nn.BatchNorm1d(256), nn.GELU(), nn.Dropout(0.1), nn.Linear(256, 2), ) def forward(self, x): f = self.backbone(x) return self.head_main(f), self.head_eye(f), self.head_disc(f), f def load_biometric_model(): """Download biometric_flask_model.pth from HuggingFace and load it.""" path = _hf_download("biometric_flask_model.pth") model = BiometricClassifier(num_classes=4) try: ckpt = torch.load(path, map_location=device, weights_only=False) except Exception as e: print(f" Error loading checkpoint: {e}") model.to(device) model.eval() return ( model, {0: 'human_iris', 1: 'animal_eye', 2: 'animals', 3: 'other'}, {'human_iris': 0.5, 'animal_eye': 0.5, 'animals': 0.5, 'other': 0.5}, {} ) # Handle different save formats if isinstance(ckpt, dict): if 'state_dict' in ckpt: model.load_state_dict(ckpt['state_dict'], strict=False) elif 'model_state_dict' in ckpt: model.load_state_dict(ckpt['model_state_dict'], strict=False) else: model.load_state_dict(ckpt, strict=False) else: model.load_state_dict(ckpt, strict=False) model.to(device) model.eval() classes = ( ckpt.get('classes', {0: 'human_iris', 1: 'animal_eye', 2: 'animals', 3: 'other'}) if isinstance(ckpt, dict) else {0: 'human_iris', 1: 'animal_eye', 2: 'animals', 3: 'other'} ) thresholds = ( ckpt.get('thresholds', {'human_iris': 0.5, 'animal_eye': 0.5, 'animals': 0.5, 'other': 0.5}) if isinstance(ckpt, dict) else {'human_iris': 0.5, 'animal_eye': 0.5, 'animals': 0.5, 'other': 0.5} ) return model, classes, thresholds, ckpt if isinstance(ckpt, dict) else {} phase1_transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize( mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ) ]) # ========================= # PHASE 2 — Presentation Attack Detection (DNetPAD) # ========================= phase2_transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) # class DNetPAD(nn.Module): # def __init__(self, num_classes=2): # super().__init__() # self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1) # self.bn1 = nn.BatchNorm2d(32) # self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1) # self.bn2 = nn.BatchNorm2d(64) # self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1) # self.bn3 = nn.BatchNorm2d(128) # self.conv4 = nn.Conv2d(128, 256, kernel_size=3, padding=1) # self.bn4 = nn.BatchNorm2d(256) # self.pool = nn.MaxPool2d(2, 2) # self.dropout = nn.Dropout(0.5) # self.gap = nn.AdaptiveAvgPool2d(1) # self.fc1 = nn.Linear(256, 128) # self.fc2 = nn.Linear(128, 64) # self.fc3 = nn.Linear(64, num_classes) # # Used by Grad-CAM # self.last_conv_output = None # def forward(self, x): # x = self.pool(F.relu(self.bn1(self.conv1(x)))) # x = self.pool(F.relu(self.bn2(self.conv2(x)))) # x = self.pool(F.relu(self.bn3(self.conv3(x)))) # x = F.relu(self.bn4(self.conv4(x))) # self.last_conv_output = x # self.last_conv_output.retain_grad() # x = self.pool(x) # x = self.gap(x) # x = x.view(x.size(0), -1) # x = F.relu(self.fc1(x)); x = self.dropout(x) # x = F.relu(self.fc2(x)); x = self.dropout(x) # x = self.fc3(x) # return x class DNetPAD(nn.Module): def __init__(self, num_classes=2): super().__init__() self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1) self.bn1 = nn.BatchNorm2d(32) self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1) self.bn2 = nn.BatchNorm2d(64) self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1) self.bn3 = nn.BatchNorm2d(128) self.conv4 = nn.Conv2d(128, 256, kernel_size=3, padding=1) self.bn4 = nn.BatchNorm2d(256) self.pool = nn.MaxPool2d(2, 2) self.dropout = nn.Dropout(0.5) self.gap = nn.AdaptiveAvgPool2d(1) self.fc1 = nn.Linear(256, 128) self.fc2 = nn.Linear(128, 64) self.fc3 = nn.Linear(64, num_classes) def forward(self, x): # ✅ 100% identical to Version 1 — nothing added x = self.pool(F.relu(self.bn1(self.conv1(x)))) x = self.pool(F.relu(self.bn2(self.conv2(x)))) x = self.pool(F.relu(self.bn3(self.conv3(x)))) x = self.pool(F.relu(self.bn4(self.conv4(x)))) x = self.gap(x) x = x.view(x.size(0), -1) x = F.relu(self.fc1(x)); x = self.dropout(x) x = F.relu(self.fc2(x)); x = self.dropout(x) x = self.fc3(x) return x def load_phase2_model(): """Download dnet_pad_model.pth from HF and load it.""" path = _hf_download("best_dnet_pad.pth") model = DNetPAD(num_classes=2) checkpoint = torch.load(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"]) elif isinstance(checkpoint, dict) and "state_dict" in checkpoint: model.load_state_dict(checkpoint["state_dict"]) elif isinstance(checkpoint, dict): model.load_state_dict(checkpoint) else: model = checkpoint # entire model object was saved model.to(device).eval() return model # ========================= # Grad-CAM helpers # ========================= def _get_region_explanation(heatmap): h, w = heatmap.shape scores = { "center iris region": np.mean(heatmap[h//3:2*h//3, w//3:2*w//3]), "upper iris region": np.mean(heatmap[0:h//3, :]), "lower iris region": np.mean(heatmap[2*h//3:, :]), "left iris region": np.mean(heatmap[:, 0:w//3]), "right iris region": np.mean(heatmap[:, 2*w//3:]), } top_region, top_score = max(scores.items(), key=lambda kv: kv[1]) if top_score < 0.1: return "Model focused on low-intensity features across full iris" return f"Model focused mainly on {top_region}" def _generate_explanation(pred_class, confidence): if pred_class == 1: # FAKE if confidence > 0.90: return "Strong spoof detected. Likely printed or displayed iris pattern." elif confidence > 0.75: return "Possible presentation attack (screen replay or printed image)." else: return "Weak spoof indicators detected. Uncertain attack type." else: # REAL if confidence > 0.90: return "High-quality genuine iris texture detected." else: return "Likely real iris but image quality is moderate." # def pad_explainable(image_path, model): # """Run PAD model + Grad-CAM and return prediction + visual explanation.""" # img = Image.open(image_path).convert("RGB") # input_tensor = phase2_transform(img).unsqueeze(0).to(device) # model.zero_grad() # outputs = model(input_tensor) # probs = torch.softmax(outputs, dim=1) # pred_class = torch.argmax(probs, dim=1).item() # confidence = probs[0, pred_class].item() # # Backward pass for Grad-CAM # outputs[0, pred_class].backward(retain_graph=True) # activations = model.last_conv_output # gradients = torch.autograd.grad( # outputs=outputs[:, pred_class], # inputs=model.last_conv_output, # retain_graph=True, # )[0] # pooled_gradients = torch.mean(gradients, dim=[0, 2, 3]) # for i in range(activations.shape[1]): # activations[:, i, :, :] *= pooled_gradients[i] # heatmap = torch.mean(activations, dim=1).squeeze().detach().cpu().numpy() # heatmap = np.maximum(heatmap, 0) # if np.max(heatmap) > 0: # heatmap /= np.max(heatmap) # heatmap_resized = cv2.resize(heatmap, (224, 224)) # region_text = _get_region_explanation(heatmap_resized) # explanation = _generate_explanation(pred_class, confidence) # heatmap_color = cv2.applyColorMap(np.uint8(255 * heatmap_resized), cv2.COLORMAP_JET) # heatmap_color = cv2.cvtColor(heatmap_color, cv2.COLOR_BGR2RGB) # overlay = cv2.addWeighted( # np.array(img.resize((224, 224))), 0.6, # heatmap_color, 0.4, 0, # ) # return { # "prediction": "FAKE" if pred_class == 1 else "REAL", # "confidence": float(confidence), # "human_explanation": explanation, # "region_explanation": region_text, # "heatmap_image": overlay, # } def pad_explainable(image_path, model): """Run PAD model + Grad-CAM using hooks — architecture untouched.""" img = Image.open(image_path).convert("RGB") input_tensor = phase2_transform(img).unsqueeze(0).to(device) # ✅ Hook storage activation_store = {} gradient_store = {} # ✅ Register hooks on conv4 (last conv layer) def forward_hook(module, input, output): activation_store["conv4"] = output def backward_hook(module, grad_input, grad_output): gradient_store["conv4"] = grad_output[0] # Attach hooks handle_f = model.conv4.register_forward_hook(forward_hook) handle_b = model.conv4.register_full_backward_hook(backward_hook) try: model.eval() model.zero_grad() # ✅ No torch.no_grad() — gradients must flow outputs = model(input_tensor) probs = torch.softmax(outputs, dim=1) pred_class = torch.argmax(probs, dim=1).item() confidence = probs[0, pred_class].item() # ✅ Single backward pass outputs[0, pred_class].backward() # ✅ Get activations and gradients from hooks activations = activation_store["conv4"].detach() # [1, 256, H, W] gradients = gradient_store["conv4"].detach() # [1, 256, H, W] # ✅ Grad-CAM pooled_gradients = torch.mean(gradients, dim=[0, 2, 3]) # [256] for i in range(activations.shape[1]): activations[:, i, :, :] *= pooled_gradients[i] heatmap = torch.mean(activations, dim=1).squeeze().cpu().numpy() heatmap = np.maximum(heatmap, 0) if np.max(heatmap) > 0: heatmap /= np.max(heatmap) heatmap_resized = cv2.resize(heatmap, (224, 224)) region_text = _get_region_explanation(heatmap_resized) explanation = _generate_explanation(pred_class, confidence) heatmap_color = cv2.applyColorMap(np.uint8(255 * heatmap_resized), cv2.COLORMAP_JET) heatmap_color = cv2.cvtColor(heatmap_color, cv2.COLOR_BGR2RGB) overlay = cv2.addWeighted( np.array(img.resize((224, 224))), 0.6, heatmap_color, 0.4, 0, ) return { "prediction": "FAKE" if pred_class == 1 else "REAL", "confidence": float(confidence), "human_explanation": explanation, "region_explanation": region_text, "heatmap_image": overlay, } finally: # ✅ Always remove hooks to avoid memory leaks handle_f.remove() handle_b.remove() # ========================= # GAN — Synthetic iris generator # ========================= class Generator(nn.Module): def __init__(self, z_dim=128, channels=3, feature_maps=128): super().__init__() self.init_layer = nn.Sequential( nn.ConvTranspose2d(z_dim, feature_maps * 8, 4, 1, 0, bias=False), nn.BatchNorm2d(feature_maps * 8), nn.ReLU(True), ) self.middle_layers = nn.Sequential( nn.ConvTranspose2d(feature_maps * 8, feature_maps * 4, 4, 2, 1, bias=False), nn.BatchNorm2d(feature_maps * 4), nn.ReLU(True), nn.ConvTranspose2d(feature_maps * 4, feature_maps * 2, 4, 2, 1, bias=False), nn.BatchNorm2d(feature_maps * 2), nn.ReLU(True), nn.ConvTranspose2d(feature_maps * 2, feature_maps, 4, 2, 1, bias=False), nn.BatchNorm2d(feature_maps), nn.ReLU(True), nn.ConvTranspose2d(feature_maps, feature_maps, 4, 2, 1, bias=False), nn.BatchNorm2d(feature_maps), nn.ReLU(True), nn.ConvTranspose2d(feature_maps, feature_maps // 2, 4, 2, 1, bias=False), nn.BatchNorm2d(feature_maps // 2), nn.ReLU(True), nn.ConvTranspose2d(feature_maps // 2, channels, 4, 2, 1, bias=False), nn.Tanh(), ) def forward(self, noise): return self.middle_layers(self.init_layer(noise)) def load_gan_model(z_dim=128): """Download gan_model.pth from HF and load the Generator.""" path = _hf_download("gan_model.pth") model = Generator(z_dim=z_dim) checkpoint = torch.load(path, map_location=device, weights_only=False) if isinstance(checkpoint, dict) and "G_state_dict" in checkpoint: model.load_state_dict(checkpoint["G_state_dict"]) elif isinstance(checkpoint, dict) and "model_state_dict" in checkpoint: model.load_state_dict(checkpoint["model_state_dict"]) elif isinstance(checkpoint, dict): model.load_state_dict(checkpoint) else: model = checkpoint model.to(device).eval() return model