iris-backend / models.py
trretretret's picture
updated phase 1
8fdf597
Raw
History Blame Contribute Delete
36.9 kB
# # 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