Spaces:
Running
Running
| # # 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 | |