Spaces:
Runtime error
Runtime error
| # app.py | |
| # Biometric Authentication Literature Survey + Interactive Demonstration | |
| # Single-file Gradio app for Hugging Face Spaces free CPU tier. | |
| import base64 | |
| import hashlib | |
| import warnings | |
| from typing import Dict, Tuple | |
| import gradio as gr | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| import pandas as pd | |
| from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageOps | |
| warnings.filterwarnings("ignore") | |
| try: | |
| from cryptography.fernet import Fernet | |
| HAS_CRYPTO = True | |
| except Exception: | |
| HAS_CRYPTO = False | |
| try: | |
| import cv2 | |
| HAS_CV2 = True | |
| except Exception: | |
| HAS_CV2 = False | |
| APP_TITLE = "Biometric Authentication Literature Survey & Interactive Demo" | |
| DEFAULT_SIZE = 128 | |
| # --------------------------------------------------------------------- | |
| # General helpers | |
| # --------------------------------------------------------------------- | |
| def safe_image(img): | |
| if img is None: | |
| return None | |
| if isinstance(img, Image.Image): | |
| return img.convert("RGB") | |
| return Image.fromarray(np.asarray(img)).convert("RGB") | |
| def array_to_pil(arr): | |
| arr = np.asarray(arr, dtype=np.float32) | |
| arr = np.nan_to_num(arr) | |
| if arr.size == 0: | |
| arr = np.zeros((DEFAULT_SIZE, DEFAULT_SIZE), dtype=np.float32) | |
| if float(arr.max()) <= 1.0: | |
| arr = arr * 255.0 | |
| arr = np.clip(arr, 0, 255).astype(np.uint8) | |
| return Image.fromarray(arr) | |
| def normalize01(arr): | |
| arr = np.asarray(arr, dtype=np.float32) | |
| mn = float(arr.min()) | |
| mx = float(arr.max()) | |
| if mx - mn < 1e-8: | |
| return np.zeros_like(arr, dtype=np.float32) | |
| return (arr - mn) / (mx - mn) | |
| def seed_from_key(key): | |
| key = str(key or "student-demo-key") | |
| digest = hashlib.sha256(key.encode("utf-8")).digest() | |
| return int.from_bytes(digest[:8], "little") % (2**32 - 1) | |
| def resize_gray(img, size=DEFAULT_SIZE): | |
| img = safe_image(img) | |
| gray = ImageOps.grayscale(img) | |
| gray = ImageOps.autocontrast(gray) | |
| gray = gray.resize((size, size)) | |
| return np.asarray(gray, dtype=np.float32) / 255.0 | |
| def unit_vector(vec): | |
| vec = np.asarray(vec, dtype=np.float32).flatten() | |
| vec = np.nan_to_num(vec) | |
| norm = float(np.linalg.norm(vec)) | |
| if norm < 1e-8: | |
| return vec | |
| return vec / norm | |
| def pad_or_trim(vec, length): | |
| vec = np.asarray(vec, dtype=np.float32).flatten() | |
| if len(vec) >= length: | |
| return vec[:length] | |
| out = np.zeros(length, dtype=np.float32) | |
| out[: len(vec)] = vec | |
| return out | |
| def cosine_similarity(a, b): | |
| a = np.asarray(a, dtype=np.float32).flatten() | |
| b = np.asarray(b, dtype=np.float32).flatten() | |
| n = min(len(a), len(b)) | |
| if n == 0: | |
| return 0.0 | |
| a = unit_vector(a[:n]) | |
| b = unit_vector(b[:n]) | |
| raw = float(np.dot(a, b)) | |
| return max(0.0, min(1.0, (raw + 1.0) / 2.0)) | |
| def hamming_similarity(a, b): | |
| a = np.asarray(a).flatten() > 0.5 | |
| b = np.asarray(b).flatten() > 0.5 | |
| n = min(len(a), len(b)) | |
| if n == 0: | |
| return 0.0 | |
| return float(1.0 - np.mean(a[:n] != b[:n])) | |
| def vector_preview(vec, limit=24): | |
| vec = np.asarray(vec).flatten() | |
| return np.array2string(vec[:limit], precision=4, separator=", ") | |
| def feature_df(vec, limit=40): | |
| vec = np.asarray(vec).flatten() | |
| return pd.DataFrame({"index": list(range(min(limit, len(vec)))), "value": [float(v) for v in vec[:limit]]}) | |
| def feature_plot(vec, title): | |
| vec = np.asarray(vec).flatten() | |
| n = min(64, len(vec)) | |
| fig = plt.figure(figsize=(7, 3)) | |
| plt.bar(np.arange(n), vec[:n]) | |
| plt.title(title) | |
| plt.xlabel("Feature index") | |
| plt.ylabel("Value") | |
| plt.tight_layout() | |
| return fig | |
| # --------------------------------------------------------------------- | |
| # Image preprocessing | |
| # --------------------------------------------------------------------- | |
| def preprocess_modality(img, modality): | |
| img = safe_image(img) | |
| if img is None: | |
| raise ValueError("Please upload an image.") | |
| if modality == "Iris": | |
| w, h = img.size | |
| side = min(w, h) | |
| left = (w - side) // 2 | |
| top = (h - side) // 2 | |
| crop = img.crop((left, top, left + side, top + side)) | |
| gray = ImageOps.grayscale(crop) | |
| gray = ImageOps.autocontrast(gray) | |
| gray = gray.resize((DEFAULT_SIZE, DEFAULT_SIZE)) | |
| arr = np.asarray(gray, dtype=np.float32) / 255.0 | |
| yy, xx = np.ogrid[:DEFAULT_SIZE, :DEFAULT_SIZE] | |
| c = (DEFAULT_SIZE - 1) / 2 | |
| dist = np.sqrt((xx - c) ** 2 + (yy - c) ** 2) | |
| mask = (dist <= DEFAULT_SIZE * 0.46) & (dist >= DEFAULT_SIZE * 0.12) | |
| arr2 = arr.copy() | |
| arr2[~mask] = 0.0 | |
| meta = { | |
| "modality": modality, | |
| "preprocessing": "central crop, grayscale, autocontrast, circular iris-style mask", | |
| "note": "Educational approximation; not a true iris segmentation algorithm." | |
| } | |
| return arr2, array_to_pil(arr2), meta | |
| if modality == "Fingerprint": | |
| arr = resize_gray(img) | |
| pil = array_to_pil(arr) | |
| pil = ImageEnhance.Contrast(pil).enhance(1.8) | |
| pil = pil.filter(ImageFilter.SHARPEN) | |
| arr = np.asarray(pil, dtype=np.float32) / 255.0 | |
| meta = { | |
| "modality": modality, | |
| "preprocessing": "grayscale, resize, autocontrast, contrast enhancement, sharpening" | |
| } | |
| return arr, pil, meta | |
| arr = resize_gray(img) | |
| pil = array_to_pil(arr) | |
| pil = ImageEnhance.Contrast(pil).enhance(1.25) | |
| arr = np.asarray(pil, dtype=np.float32) / 255.0 | |
| meta = { | |
| "modality": modality, | |
| "preprocessing": "grayscale, resize, autocontrast, light contrast enhancement" | |
| } | |
| return arr, pil, meta | |
| # --------------------------------------------------------------------- | |
| # Feature extraction | |
| # --------------------------------------------------------------------- | |
| def conv2d_same(img, kernel): | |
| img = np.asarray(img, dtype=np.float32) | |
| kernel = np.asarray(kernel, dtype=np.float32) | |
| kh, kw = kernel.shape | |
| ph, pw = kh // 2, kw // 2 | |
| padded = np.pad(img, ((ph, ph), (pw, pw)), mode="reflect") | |
| try: | |
| windows = np.lib.stride_tricks.sliding_window_view(padded, (kh, kw)) | |
| return np.einsum("ijkl,kl->ij", windows, kernel) | |
| except Exception: | |
| out = np.zeros_like(img) | |
| for y in range(img.shape[0]): | |
| for x in range(img.shape[1]): | |
| out[y, x] = np.sum(padded[y:y + kh, x:x + kw] * kernel) | |
| return out | |
| def gabor_kernel(size=21, sigma=4.0, theta=0.0, frequency=0.12, gamma=0.5): | |
| radius = size // 2 | |
| y, x = np.mgrid[-radius:radius + 1, -radius:radius + 1] | |
| x_theta = x * np.cos(theta) + y * np.sin(theta) | |
| y_theta = -x * np.sin(theta) + y * np.cos(theta) | |
| kernel = np.exp(-(x_theta ** 2 + gamma ** 2 * y_theta ** 2) / (2 * sigma ** 2)) | |
| kernel *= np.cos(2 * np.pi * frequency * x_theta) | |
| kernel -= kernel.mean() | |
| return kernel.astype(np.float32) | |
| def extract_gabor(arr): | |
| orientations = [0, np.pi / 4, np.pi / 2, 3 * np.pi / 4] | |
| responses = [] | |
| feats = [] | |
| for theta in orientations: | |
| resp = conv2d_same(arr, gabor_kernel(theta=theta)) | |
| responses.append(resp) | |
| a = np.abs(resp) | |
| feats.extend([float(a.mean()), float(a.std()), float(a.max()), float(np.percentile(a, 75))]) | |
| visual = normalize01(np.stack([np.abs(r) for r in responses], axis=0).max(axis=0)) | |
| meta = { | |
| "method": "Gabor filters", | |
| "feature_type": "handcrafted texture and ridge-frequency descriptor", | |
| "feature_length": len(feats), | |
| "advantages": "Interpretable and useful for fingerprint ridges and iris texture.", | |
| "limitations": "Sensitive to segmentation, rotation, scale, and manually chosen parameters." | |
| } | |
| return np.asarray(feats, dtype=np.float32), array_to_pil(visual), meta | |
| def extract_lbp(arr): | |
| center = arr | |
| neighbors = [ | |
| np.roll(np.roll(arr, -1, axis=0), -1, axis=1), | |
| np.roll(arr, -1, axis=0), | |
| np.roll(np.roll(arr, -1, axis=0), 1, axis=1), | |
| np.roll(arr, 1, axis=1), | |
| np.roll(np.roll(arr, 1, axis=0), 1, axis=1), | |
| np.roll(arr, 1, axis=0), | |
| np.roll(np.roll(arr, 1, axis=0), -1, axis=1), | |
| np.roll(arr, -1, axis=1), | |
| ] | |
| code = np.zeros_like(arr, dtype=np.uint8) | |
| for i, n in enumerate(neighbors): | |
| code += ((n >= center).astype(np.uint8) << i) | |
| hist, _ = np.histogram(code.flatten(), bins=256, range=(0, 256), density=True) | |
| meta = { | |
| "method": "Local Binary Pattern", | |
| "feature_type": "handcrafted local texture histogram", | |
| "feature_length": len(hist), | |
| "advantages": "Fast, simple, and useful for texture-based biometric patterns.", | |
| "limitations": "Sensitive to noise and weaker for global structure." | |
| } | |
| return hist.astype(np.float32), array_to_pil(code.astype(np.float32) / 255.0), meta | |
| def extract_sift_like(arr): | |
| if HAS_CV2: | |
| img8 = np.clip(arr * 255, 0, 255).astype(np.uint8) | |
| sift = None | |
| try: | |
| sift = cv2.SIFT_create() | |
| except Exception: | |
| sift = None | |
| if sift is not None: | |
| keypoints, descriptors = sift.detectAndCompute(img8, None) | |
| if descriptors is None or len(descriptors) == 0: | |
| desc = np.zeros(128, dtype=np.float32) | |
| else: | |
| desc = unit_vector(descriptors.mean(axis=0).astype(np.float32)) | |
| color = cv2.cvtColor(img8, cv2.COLOR_GRAY2RGB) | |
| drawn = cv2.drawKeypoints(color, keypoints[:80], None, flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS) | |
| meta = { | |
| "method": "SIFT", | |
| "feature_type": "keypoint descriptor", | |
| "feature_length": len(desc), | |
| "advantages": "Robust to scale and rotation when stable keypoints exist.", | |
| "limitations": "Can be sparse on low-texture or poor-quality biometric images." | |
| } | |
| return desc.astype(np.float32), Image.fromarray(drawn), meta | |
| gy, gx = np.gradient(arr) | |
| mag = np.sqrt(gx ** 2 + gy ** 2) | |
| ori = (np.arctan2(gy, gx) + np.pi) / (2 * np.pi) | |
| cells = 4 | |
| bins = 8 | |
| h, w = arr.shape | |
| feats = [] | |
| for cy in range(cells): | |
| for cx in range(cells): | |
| y0, y1 = cy * h // cells, (cy + 1) * h // cells | |
| x0, x1 = cx * w // cells, (cx + 1) * w // cells | |
| hist, _ = np.histogram( | |
| ori[y0:y1, x0:x1].flatten(), | |
| bins=bins, | |
| range=(0, 1), | |
| weights=mag[y0:y1, x0:x1].flatten() | |
| ) | |
| feats.extend(hist.tolist()) | |
| feats = unit_vector(np.asarray(feats, dtype=np.float32)) | |
| visual = Image.fromarray(np.uint8(np.stack([arr, arr, arr], axis=-1) * 255)) | |
| draw = ImageDraw.Draw(visual) | |
| flat_idx = np.argsort(mag.flatten())[-70:] | |
| for idx in flat_idx: | |
| y, x = divmod(int(idx), w) | |
| draw.ellipse((x - 1, y - 1, x + 1, y + 1), outline=(255, 0, 0)) | |
| meta = { | |
| "method": "SIFT/SURF-like fallback", | |
| "feature_type": "educational gradient orientation descriptor", | |
| "feature_length": len(feats), | |
| "advantages": "Demonstrates local keypoint/gradient-descriptor ideas without heavy models.", | |
| "limitations": "Not a full SIFT/SURF implementation unless OpenCV SIFT is available." | |
| } | |
| return feats.astype(np.float32), visual, meta | |
| def extract_minutiae_like(arr): | |
| smooth = conv2d_same(arr, np.ones((3, 3), dtype=np.float32) / 9.0) | |
| binary = smooth < np.percentile(smooth, 45) | |
| binary[:2, :] = False | |
| binary[-2:, :] = False | |
| binary[:, :2] = False | |
| binary[:, -2:] = False | |
| ncount = np.zeros_like(binary, dtype=np.int32) | |
| for dy in [-1, 0, 1]: | |
| for dx in [-1, 0, 1]: | |
| if dy == 0 and dx == 0: | |
| continue | |
| ncount += np.roll(np.roll(binary, dy, axis=0), dx, axis=1).astype(np.int32) | |
| endpoints = binary & (ncount == 1) | |
| bifurcations = binary & (ncount >= 3) | |
| feats = [ | |
| float(endpoints.sum()) / 1000.0, | |
| float(bifurcations.sum()) / 1000.0, | |
| float(binary.mean()), | |
| float(ncount[binary].mean()) if binary.any() else 0.0, | |
| ] | |
| grid = 4 | |
| h, w = arr.shape | |
| for mask in [endpoints, bifurcations]: | |
| for gy in range(grid): | |
| for gx in range(grid): | |
| y0, y1 = gy * h // grid, (gy + 1) * h // grid | |
| x0, x1 = gx * w // grid, (gx + 1) * w // grid | |
| feats.append(float(mask[y0:y1, x0:x1].sum()) / 100.0) | |
| visual = Image.fromarray(np.uint8(np.stack([arr, arr, arr], axis=-1) * 255)) | |
| draw = ImageDraw.Draw(visual) | |
| ey, ex = np.where(endpoints) | |
| by, bx = np.where(bifurcations) | |
| for y, x in list(zip(ey, ex))[:120]: | |
| draw.ellipse((x - 2, y - 2, x + 2, y + 2), outline=(0, 255, 0), width=1) | |
| for y, x in list(zip(by, bx))[:120]: | |
| draw.rectangle((x - 2, y - 2, x + 2, y + 2), outline=(255, 0, 0), width=1) | |
| meta = { | |
| "method": "Minutiae-like extraction", | |
| "feature_type": "educational endpoint and bifurcation approximation", | |
| "feature_length": len(feats), | |
| "advantages": "Visually explains classic fingerprint minutiae concepts.", | |
| "limitations": "Not a true forensic minutiae extractor; segmentation and thinning are simplified." | |
| } | |
| return np.asarray(feats, dtype=np.float32), visual, meta | |
| def extract_cnn_like(arr): | |
| sobel_x = np.asarray([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=np.float32) | |
| sobel_y = sobel_x.T | |
| gx = conv2d_same(arr, sobel_x) | |
| gy = conv2d_same(arr, sobel_y) | |
| edge = normalize01(np.sqrt(gx ** 2 + gy ** 2)) | |
| feats = [] | |
| h, w = arr.shape | |
| for grid in [2, 4, 8]: | |
| for yy in range(grid): | |
| for xx in range(grid): | |
| y0, y1 = yy * h // grid, (yy + 1) * h // grid | |
| x0, x1 = xx * w // grid, (xx + 1) * w // grid | |
| patch = arr[y0:y1, x0:x1] | |
| epatch = edge[y0:y1, x0:x1] | |
| feats.extend([float(patch.mean()), float(patch.std()), float(epatch.mean()), float(epatch.std())]) | |
| feats.extend([ | |
| float(arr.mean()), float(arr.std()), float(edge.mean()), float(edge.std()), | |
| float(np.percentile(arr, 25)), float(np.percentile(arr, 50)), float(np.percentile(arr, 75)) | |
| ]) | |
| feats = unit_vector(np.asarray(feats, dtype=np.float32)) | |
| meta = { | |
| "method": "CNN-like embedding", | |
| "feature_type": "lightweight multiscale pooled edge and texture embedding", | |
| "feature_length": len(feats), | |
| "advantages": "Demonstrates hierarchical feature pooling on CPU.", | |
| "limitations": "Not trained; does not replace a real CNN biometric model." | |
| } | |
| return feats.astype(np.float32), array_to_pil(edge), meta | |
| def extract_deep_embedding(arr, modality): | |
| gabor_vec, _, _ = extract_gabor(arr) | |
| lbp_vec, _, _ = extract_lbp(arr) | |
| sift_vec, _, _ = extract_sift_like(arr) | |
| cnn_vec, cnn_vis, _ = extract_cnn_like(arr) | |
| base = np.concatenate([ | |
| pad_or_trim(gabor_vec, 32), | |
| pad_or_trim(lbp_vec, 128), | |
| pad_or_trim(sift_vec, 128), | |
| pad_or_trim(cnn_vec, 128), | |
| ]) | |
| base = unit_vector(base) | |
| rng = np.random.default_rng(seed_from_key("deep-" + str(modality))) | |
| projection = rng.normal(0, 1, size=(len(base), 128)).astype(np.float32) | |
| emb = unit_vector(base @ projection) | |
| meta = { | |
| "method": "Deep embedding simulation", | |
| "feature_type": "deterministic projected multimethod embedding", | |
| "feature_length": len(emb), | |
| "advantages": "Shows the idea of compact embeddings used by FaceNet, ArcFace, and CNN systems.", | |
| "limitations": "Educational simulation; not trained on biometric identity labels." | |
| } | |
| return emb.astype(np.float32), cnn_vis, meta | |
| def extract_features(img, modality, method): | |
| arr, preprocessed, pre_meta = preprocess_modality(img, modality) | |
| if method == "Minutiae-like": | |
| vec, vis, meta = extract_minutiae_like(arr) | |
| elif method == "LBP": | |
| vec, vis, meta = extract_lbp(arr) | |
| elif method == "Gabor": | |
| vec, vis, meta = extract_gabor(arr) | |
| elif method == "SIFT/SURF-like": | |
| vec, vis, meta = extract_sift_like(arr) | |
| elif method == "CNN-like": | |
| vec, vis, meta = extract_cnn_like(arr) | |
| elif method == "Deep embedding": | |
| vec, vis, meta = extract_deep_embedding(arr, modality) | |
| else: | |
| vec, vis, meta = extract_gabor(arr) | |
| return vec.astype(np.float32), preprocessed, vis, {**pre_meta, **meta} | |
| # --------------------------------------------------------------------- | |
| # Template protection | |
| # --------------------------------------------------------------------- | |
| def fernet_key(secret): | |
| digest = hashlib.sha256(str(secret or "demo-secret").encode("utf-8")).digest() | |
| return base64.urlsafe_b64encode(digest) | |
| def encrypted_storage_preview(vec, secret): | |
| raw = np.asarray(vec[:64], dtype=np.float32).tobytes() | |
| if HAS_CRYPTO: | |
| token = Fernet(fernet_key(secret)).encrypt(raw) | |
| return token[:180].decode("utf-8") + "..." | |
| digest = hashlib.sha256(raw + str(secret).encode("utf-8")).hexdigest() | |
| return "cryptography package missing; SHA-256 preview only: " + digest | |
| def random_projection(vec, secret, out_dim=128): | |
| vec = unit_vector(vec) | |
| rng = np.random.default_rng(seed_from_key(secret)) | |
| projection = rng.normal(0, 1, size=(len(vec), out_dim)).astype(np.float32) | |
| return unit_vector(vec @ projection) | |
| def biohash(vec, secret, out_dim=128): | |
| projected = random_projection(vec, secret, out_dim) | |
| return (projected > np.median(projected)).astype(np.float32) | |
| def chaotic_mapping(vec, secret): | |
| vec = np.asarray(vec, dtype=np.float32).flatten() | |
| seed = seed_from_key(secret) | |
| x = ((seed % 100000) + 1) / 100001.0 | |
| r = 3.99 | |
| seq = [] | |
| for _ in range(len(vec)): | |
| x = r * x * (1.0 - x) | |
| seq.append(x) | |
| perm = np.argsort(seq) | |
| return unit_vector(vec[perm]) | |
| def fuzzy_bits(vec, secret, out_dim=128): | |
| projected = random_projection(vec, secret, out_dim) | |
| return (projected > 0).astype(np.float32) | |
| def protect_for_matching(vec, method, secret): | |
| vec = np.asarray(vec, dtype=np.float32).flatten() | |
| if method == "Plain template": | |
| return unit_vector(vec), "cosine", "Raw normalized template. Fast but unsafe if stolen." | |
| if method == "Encrypted storage": | |
| return unit_vector(vec), "cosine", "Encrypted at rest. Matching uses decrypted vector in this demo." | |
| if method == "Cancelable biometric": | |
| return random_projection(vec, secret), "cosine", "Secret-key random projection. Change key to revoke/reissue template." | |
| if method == "BioHashing": | |
| return biohash(vec, secret), "hamming", "Random projection plus binarization. Comparison uses Hamming similarity." | |
| if method == "Chaotic mapping": | |
| return chaotic_mapping(vec, secret), "cosine", "Logistic-map sequence permutes the template using a key." | |
| if method == "Fuzzy extractor simulation": | |
| return fuzzy_bits(vec, secret), "hamming", "Simulated stable binary helper-data-style output." | |
| if method == "Toy homomorphic encryption": | |
| return unit_vector(vec), "cosine", "Conceptual placeholder. Real homomorphic matching is much more expensive." | |
| return unit_vector(vec), "cosine", "Default normalized template." | |
| def template_preview(vec, method, secret): | |
| protected, metric, explanation = protect_for_matching(vec, method, secret) | |
| if method == "Encrypted storage": | |
| preview = encrypted_storage_preview(vec, secret) | |
| elif method == "Toy homomorphic encryption": | |
| q = np.round(np.asarray(vec[:16]) * 1000).astype(int) | |
| preview = "Toy encrypted-integer preview: " + np.array2string(q, separator=", ") | |
| else: | |
| preview = vector_preview(protected, 24) | |
| info = pd.DataFrame({ | |
| "property": ["protected length", "matching metric", "revocation capability", "explanation"], | |
| "value": [ | |
| len(protected), | |
| metric, | |
| "Yes" if method in ["Cancelable biometric", "BioHashing", "Chaotic mapping", "Fuzzy extractor simulation"] else "Limited", | |
| explanation, | |
| ], | |
| }) | |
| return preview, info | |
| # --------------------------------------------------------------------- | |
| # Liveness and attacks | |
| # --------------------------------------------------------------------- | |
| def liveness_metrics(img): | |
| arr, _, _ = preprocess_modality(img, "Face") | |
| lap = conv2d_same(arr, np.asarray([[0, 1, 0], [1, -4, 1], [0, 1, 0]], dtype=np.float32)) | |
| blur_var = float(lap.var()) | |
| fft = np.fft.fftshift(np.fft.fft2(arr)) | |
| mag = np.abs(fft) | |
| h, w = mag.shape | |
| cy, cx = h // 2, w // 2 | |
| yy, xx = np.ogrid[:h, :w] | |
| dist = np.sqrt((yy - cy) ** 2 + (xx - cx) ** 2) | |
| high_mask = dist > min(h, w) * 0.18 | |
| high_freq_ratio = float(mag[high_mask].sum() / (mag.sum() + 1e-8)) | |
| lbp_vec, _, _ = extract_lbp(arr) | |
| entropy = float(-np.sum(lbp_vec * np.log2(lbp_vec + 1e-8))) | |
| entropy_score = min(1.0, entropy / 8.0) | |
| contrast = float(arr.std()) | |
| brightness = float(arr.mean()) | |
| blur_score = min(1.0, blur_var * 120.0) | |
| freq_score = min(1.0, high_freq_ratio * 4.0) | |
| contrast_score = min(1.0, contrast * 4.0) | |
| overall = 0.30 * blur_score + 0.30 * freq_score + 0.25 * entropy_score + 0.15 * contrast_score | |
| reasons = [] | |
| if blur_score < 0.18: | |
| reasons.append("low sharpness") | |
| if freq_score < 0.18: | |
| reasons.append("low high-frequency detail") | |
| if contrast < 0.05: | |
| reasons.append("very low contrast") | |
| if brightness < 0.08 or brightness > 0.92: | |
| reasons.append("extreme brightness") | |
| return { | |
| "blur_score": round(float(blur_score), 4), | |
| "frequency_score": round(float(freq_score), 4), | |
| "texture_entropy_score": round(float(entropy_score), 4), | |
| "contrast_score": round(float(contrast_score), 4), | |
| "brightness": round(float(brightness), 4), | |
| "overall_liveness_score": round(float(overall), 4), | |
| "suspicious_reasons": ", ".join(reasons) if reasons else "none", | |
| } | |
| def simulate_attack(img, attack, intensity): | |
| img = safe_image(img) | |
| if img is None: | |
| raise ValueError("Please upload an image.") | |
| intensity = float(intensity) | |
| if attack == "None": | |
| return img | |
| if attack == "Blur / out-of-focus": | |
| return img.filter(ImageFilter.GaussianBlur(radius=0.5 + intensity * 5.0)) | |
| if attack == "Gaussian noise": | |
| arr = np.asarray(img).astype(np.float32) | |
| rng = np.random.default_rng(123) | |
| noise = rng.normal(0, 8 + intensity * 45, size=arr.shape) | |
| return Image.fromarray(np.clip(arr + noise, 0, 255).astype(np.uint8)) | |
| if attack == "Low-contrast print": | |
| out = ImageOps.grayscale(img).convert("RGB") | |
| out = ImageEnhance.Contrast(out).enhance(max(0.2, 1.0 - intensity * 0.8)) | |
| out = ImageEnhance.Brightness(out).enhance(0.85 + intensity * 0.15) | |
| return out | |
| if attack == "Replay-screen scanlines": | |
| arr = np.asarray(img).astype(np.float32) | |
| step = max(2, int(8 - intensity * 5)) | |
| arr[::step, :, :] *= 0.55 | |
| arr[:, ::max(3, step + 1), :] *= 0.85 | |
| return Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8)) | |
| if attack == "Deepfake-like smoothing": | |
| out = img.filter(ImageFilter.MedianFilter(size=3)) | |
| out = out.filter(ImageFilter.GaussianBlur(radius=0.5 + intensity * 2.5)) | |
| out = ImageEnhance.Sharpness(out).enhance(0.5) | |
| return out | |
| if attack == "Adversarial-style tiny noise": | |
| arr = np.asarray(img).astype(np.float32) | |
| rng = np.random.default_rng(999) | |
| pattern = rng.choice([-1, 1], size=arr.shape) * (2 + intensity * 12) | |
| return Image.fromarray(np.clip(arr + pattern, 0, 255).astype(np.uint8)) | |
| return img | |
| # --------------------------------------------------------------------- | |
| # Gradio callbacks | |
| # --------------------------------------------------------------------- | |
| def run_feature_lab(img, modality, method): | |
| if img is None: | |
| return None, None, None, pd.DataFrame(), {}, "Upload an image first." | |
| try: | |
| vec, pre, vis, meta = extract_features(img, modality, method) | |
| explanation = ( | |
| "### Feature extraction result\n\n" | |
| f"**Modality:** {modality}\n\n" | |
| f"**Method:** {meta.get('method')}\n\n" | |
| f"**Feature type:** {meta.get('feature_type')}\n\n" | |
| f"**Feature length:** {meta.get('feature_length')}\n\n" | |
| f"**Advantages:** {meta.get('advantages')}\n\n" | |
| f"**Limitations:** {meta.get('limitations')}\n\n" | |
| "This is an educational demonstration. The final report should cite actual paper metrics." | |
| ) | |
| return pre, vis, feature_plot(vec, f"{method} feature preview"), feature_df(vec), meta, explanation | |
| except Exception as e: | |
| return None, None, None, pd.DataFrame(), {}, f"Error: {e}" | |
| def run_verification(enroll_img, verify_img, modality, method, protection_method, secret_key, threshold): | |
| if enroll_img is None or verify_img is None: | |
| return "## REJECTED\n\nUpload both enrollment and verification images.", pd.DataFrame(), None, None | |
| try: | |
| e_vec, _, e_vis, _ = extract_features(enroll_img, modality, method) | |
| v_vec, _, v_vis, _ = extract_features(verify_img, modality, method) | |
| e_prot, metric, explanation = protect_for_matching(e_vec, protection_method, secret_key) | |
| v_prot, _, _ = protect_for_matching(v_vec, protection_method, secret_key) | |
| if metric == "hamming": | |
| similarity = hamming_similarity(e_prot, v_prot) | |
| else: | |
| similarity = cosine_similarity(e_prot, v_prot) | |
| live = liveness_metrics(verify_img) | |
| live_score = float(live["overall_liveness_score"]) | |
| is_live = live_score >= 0.35 | |
| accepted = similarity >= float(threshold) and is_live | |
| decision = "ACCEPTED" if accepted else "REJECTED" | |
| reasons = [] | |
| if similarity < float(threshold): | |
| reasons.append("similarity below threshold") | |
| if not is_live: | |
| reasons.append("liveness score suspicious") | |
| if not reasons: | |
| reasons.append("similarity and liveness passed") | |
| result = ( | |
| f"## {decision}\n\n" | |
| "| Check | Value |\n" | |
| "|---|---:|\n" | |
| f"| Similarity score | **{similarity:.4f}** |\n" | |
| f"| Threshold | **{float(threshold):.4f}** |\n" | |
| f"| Matching metric | **{metric}** |\n" | |
| f"| Liveness score | **{live_score:.4f}** |\n" | |
| f"| Liveness verdict | **{'Live / acceptable' if is_live else 'Suspicious'}** |\n" | |
| f"| Reason | **{', '.join(reasons)}** |\n\n" | |
| f"**Template protection note:** {explanation}\n\n" | |
| "The demo fails closed: no image or processing failure means no authentication success." | |
| ) | |
| metrics = pd.DataFrame([ | |
| {"metric": "similarity", "value": round(float(similarity), 4)}, | |
| {"metric": "threshold", "value": round(float(threshold), 4)}, | |
| {"metric": "liveness_score", "value": live_score}, | |
| {"metric": "blur_score", "value": live["blur_score"]}, | |
| {"metric": "frequency_score", "value": live["frequency_score"]}, | |
| {"metric": "texture_entropy_score", "value": live["texture_entropy_score"]}, | |
| {"metric": "contrast_score", "value": live["contrast_score"]}, | |
| {"metric": "suspicious_reasons", "value": live["suspicious_reasons"]}, | |
| ]) | |
| return result, metrics, e_vis, v_vis | |
| except Exception as e: | |
| return f"## REJECTED\n\nProcessing error: {e}", pd.DataFrame(), None, None | |
| def run_template_lab(img, modality, feature_method, protection_method, secret_key): | |
| if img is None: | |
| return "Upload an image first.", pd.DataFrame(), pd.DataFrame(), None | |
| try: | |
| vec, _, vis, _ = extract_features(img, modality, feature_method) | |
| preview, info = template_preview(vec, protection_method, secret_key) | |
| raw = pd.DataFrame({"index": list(range(min(32, len(vec)))), "raw_feature_value": [float(x) for x in vec[:32]]}) | |
| md = ( | |
| "## Template protection preview\n\n" | |
| f"**Feature method:** {feature_method}\n\n" | |
| f"**Protection method:** {protection_method}\n\n" | |
| f"**Raw feature length:** {len(vec)}\n\n" | |
| "### Protected / stored preview\n\n" | |
| f"`{preview}`\n\n" | |
| "### Key concept\n\n" | |
| "Encryption protects storage. Cancelable biometrics and BioHashing make templates revocable by changing the secret key. " | |
| "Fuzzy extractors aim to generate stable keys from noisy biometric samples. Homomorphic encryption is conceptually powerful but computationally expensive." | |
| ) | |
| return md, raw, info, vis | |
| except Exception as e: | |
| return f"Error: {e}", pd.DataFrame(), pd.DataFrame(), None | |
| def run_attack_lab(img, attack, intensity): | |
| if img is None: | |
| return None, pd.DataFrame(), "Upload an image first." | |
| try: | |
| attacked = simulate_attack(img, attack, intensity) | |
| metrics = liveness_metrics(attacked) | |
| verdict = "Live / acceptable" if metrics["overall_liveness_score"] >= 0.35 else "Suspicious / possible spoof" | |
| df = pd.DataFrame([{"metric": k, "value": v} for k, v in metrics.items()]) | |
| md = ( | |
| f"## {verdict}\n\n" | |
| f"**Attack simulation:** {attack}\n\n" | |
| f"**Intensity:** {float(intensity):.2f}\n\n" | |
| "This demonstrates basic liveness/PAD ideas using blur, texture, contrast, and frequency cues. " | |
| "It is not a production anti-spoofing detector." | |
| ) | |
| return attacked, df, md | |
| except Exception as e: | |
| return None, pd.DataFrame(), f"Error: {e}" | |
| # --------------------------------------------------------------------- | |
| # Tables and static content | |
| # --------------------------------------------------------------------- | |
| def model_comparison_table(): | |
| rows = [ | |
| ["Shallow CNN", "Small convolution + pooling stack", "0.1M-2M", "Low", "Medium", "Fast on CPU", "Good", "May underfit complex variations"], | |
| ["ResNet", "Residual CNN blocks", "11M+ for ResNet-18", "Medium/high", "High with data", "Medium", "Moderate", "Heavier than MobileNet"], | |
| ["MobileNet", "Depthwise separable CNN", "3M-5M", "Low", "Good", "Fast", "Excellent", "May lose accuracy on difficult data"], | |
| ["Vision Transformer", "Patch tokens + self-attention", "High", "High", "High with large data", "Slow on CPU", "Weak/moderate", "Data hungry and heavy"], | |
| ["Autoencoder", "Encoder learns compressed representation", "Variable", "Medium", "Task-dependent", "Medium", "Moderate", "Embedding may not be discriminative"], | |
| ["FaceNet / ArcFace-style", "Metric-learning embedding", "Medium/high", "Medium/high", "Very strong for face", "Medium", "Depends on backbone", "Needs threshold and liveness checks"], | |
| ] | |
| cols = ["Model", "Architecture idea", "Approx. params", "Approx. FLOPs", "Accuracy tendency", "Inference time", "Edge suitability", "Limitation"] | |
| return pd.DataFrame(rows, columns=cols) | |
| def model_notes(selected): | |
| notes = { | |
| "Shallow CNN": "Useful for a student demo. Low complexity but limited robustness.", | |
| "ResNet": "Good baseline for fingerprint or face feature learning. Residual connections help deeper CNN training.", | |
| "MobileNet": "Best example for edge deployment because it is designed for efficient inference.", | |
| "Vision Transformer": "Useful for modern attention-based model discussion, but heavy for free CPU deployment.", | |
| "Autoencoder": "Useful for representation learning or anomaly detection, but not automatically strong for identity verification.", | |
| "FaceNet / ArcFace-style": "Best conceptual model for verification: extract embedding, compare with cosine similarity, tune threshold." | |
| } | |
| return f"### {selected}\n\n{notes.get(selected, 'Select a model.')}" | |
| def survey_table(topic): | |
| if topic == "Student 1 - Feature Extraction": | |
| rows = [ | |
| ["Hong, Wan & Jain, 1998", "Fingerprint", "Gabor/ridge enhancement", "Fingerprint images", "Enhancement/matching improvement", "Improves ridge clarity", "Parameter-sensitive"], | |
| ["Jain, Prabhakar & Hong, 1999", "Fingerprint", "Filterbank features", "Fingerprint databases", "Recognition/matching rate", "Strong handcrafted baseline", "Needs alignment"], | |
| ["Maio & Maltoni, 1997", "Fingerprint", "Minutiae extraction", "Fingerprint images", "Minutiae accuracy", "Classic approach", "False minutiae in poor images"], | |
| ["Ratha et al., 1996", "Fingerprint", "Ridge flow + minutiae", "Fingerprint images", "Verification metrics", "End-to-end pipeline", "Segmentation-sensitive"], | |
| ["Ojala et al., 2002", "Texture", "LBP", "Texture datasets", "Classification rate", "Fast descriptor", "Weak global structure"], | |
| ["Ahonen et al., 2006", "Face", "LBP face descriptor", "Face datasets", "Recognition rate", "Simple/interpretable", "Pose and illumination issues"], | |
| ["Lowe, 2004", "General vision", "SIFT", "Image datasets", "Keypoint matching", "Scale/rotation robust", "Sparse on some biometrics"], | |
| ["Bay et al., 2008", "General vision", "SURF", "Image datasets", "Speed/matching", "Faster than SIFT", "Less common in modern biometrics"], | |
| ["Daugman, 1993", "Iris", "Gabor iris code", "Iris images", "False match rates", "Foundational iris method", "Needs accurate segmentation"], | |
| ["Wildes, 1997", "Iris", "Iris texture matching", "Iris images", "Recognition performance", "Strong iris pipeline", "Controlled imaging needed"], | |
| ["Masek & Kovesi, 2003", "Iris", "Segmentation + encoding", "CASIA-style iris data", "Recognition metrics", "Useful baseline", "Older pipeline"], | |
| ["Schroff et al., 2015", "Face", "FaceNet embedding", "Large face data", "Verification accuracy", "Strong deep embedding", "Needs large training data"], | |
| ["Deng et al., 2019", "Face", "ArcFace embedding", "Face datasets", "Verification accuracy", "Discriminative loss", "Heavy training"], | |
| ["CNN iris studies", "Iris", "CNN features", "Iris datasets", "Accuracy/EER", "Learns features", "Dataset bias risk"], | |
| ["DeepPrint-style work", "Fingerprint", "Deep embedding", "Fingerprint datasets", "Verification accuracy", "Robust representation", "Needs careful evaluation"], | |
| ] | |
| cols = ["Paper", "Modality", "Method", "Dataset", "Accuracy / metric", "Advantages", "Limitations"] | |
| return pd.DataFrame(rows, columns=cols) | |
| if topic == "Student 2 - Template Protection": | |
| rows = [ | |
| ["Ratha et al., 2001", "Cancelable biometrics", "Non-invertible transform", "Medium", "Medium", "Low/medium", "Yes"], | |
| ["Teoh et al., 2004", "BioHashing", "Random projection + binarization", "Medium/high", "Medium", "Low", "Yes"], | |
| ["Juels & Wattenberg, 1999", "Fuzzy commitment", "Bind key with noisy biometric", "High", "Medium", "Medium", "Possible"], | |
| ["Juels & Sudan, 2002", "Fuzzy vault", "Hide secret among chaff points", "High", "Medium", "Medium/high", "Possible"], | |
| ["Dodis et al., 2004", "Fuzzy extractor", "Stable key from noisy input", "High", "Medium", "Medium", "Yes"], | |
| ["Clancy et al., 2003", "Fingerprint vault", "Minutiae cryptosystem", "High", "Medium", "Medium/high", "Possible"], | |
| ["Uludag et al., 2004", "Biometric cryptosystem", "Key binding/generation", "High", "Medium", "Medium", "Depends"], | |
| ["Nandakumar et al., 2007", "Fingerprint fuzzy vault", "Vault for minutiae", "High", "Medium", "Medium/high", "Yes"], | |
| ["Jain, Nandakumar & Nagar, 2008", "Survey", "Template security comparison", "N/A", "N/A", "N/A", "N/A"], | |
| ["Nagar et al., 2010", "Multibiometric cryptosystem", "Fusion + protection", "High", "High", "High", "Possible"], | |
| ["Rathgeb & Uhl, 2011", "Survey", "Protection taxonomy", "N/A", "N/A", "N/A", "N/A"], | |
| ["Gomez-Barrero et al., 2017", "Evaluation", "Unlinkability/reversibility", "High", "Medium", "Medium", "Yes"], | |
| ["Chaotic map approaches", "Chaotic mapping", "Permutation/substitution", "Medium", "Medium", "Low/medium", "Yes"], | |
| ["ECC-based approaches", "Error correction", "Correct biometric noise", "High", "Medium", "Medium", "Possible"], | |
| ["Homomorphic matching", "Homomorphic encryption", "Compute on encrypted template", "Very high", "High", "High", "Yes"], | |
| ] | |
| cols = ["Paper", "Technique", "Core idea", "Security", "Complexity", "Computational cost", "Template revocation"] | |
| return pd.DataFrame(rows, columns=cols) | |
| if topic == "Student 3 - Deep Learning": | |
| rows = [ | |
| ["DeepFace, 2014", "Deep CNN", "Face", "High", "High", "High", "Medium/slow"], | |
| ["DeepID, 2014", "CNN embedding", "Face", "Medium/high", "High", "High", "Medium"], | |
| ["VGGFace, 2015", "VGG-style CNN", "Face", "High", "High", "High", "Slow"], | |
| ["FaceNet, 2015", "Triplet-loss embedding", "Face", "High", "Very high", "Very high", "Medium"], | |
| ["SphereFace, 2017", "Angular-margin loss", "Face", "High", "Very high", "High", "Medium"], | |
| ["CosFace, 2018", "Cosine-margin loss", "Face", "High", "Very high", "High", "Medium"], | |
| ["ArcFace, 2019", "Additive angular margin", "Face", "High", "Very high", "High", "Medium"], | |
| ["MobileFaceNets, 2018", "Mobile CNN", "Face", "Low/medium", "High", "Low", "Fast"], | |
| ["FingerNet-style work", "CNN", "Fingerprint", "Medium", "Good", "Medium", "Medium"], | |
| ["DeepPrint-style work", "Deep embedding", "Fingerprint", "Medium/high", "High", "Medium/high", "Medium"], | |
| ["Iris CNN studies", "CNN", "Iris", "Medium", "Good/high", "Medium", "Medium"], | |
| ["Autoencoder biometric work", "Autoencoder", "Multiple", "Variable", "Task-dependent", "Medium", "Medium"], | |
| ["Vision Transformer, 2020", "ViT", "Adapted biometrics", "High", "High with data", "High", "Slow on CPU"], | |
| ["Swin Transformer", "Hierarchical ViT", "Face/iris", "High", "High", "High", "Medium/slow"], | |
| ["MobileNet biometric work", "Efficient CNN", "Face/fingerprint", "Low", "Good", "Low", "Fast"], | |
| ] | |
| cols = ["Paper/model", "Architecture", "Modality", "Parameters", "Accuracy tendency", "FLOPs", "Inference time"] | |
| return pd.DataFrame(rows, columns=cols) | |
| rows = [ | |
| ["Printed photo attack", "Presentation attack", "Face/fingerprint", "False acceptance", "Texture/liveness/challenge-response"], | |
| ["Replay-screen attack", "Presentation attack", "Face", "Bypass camera login", "Screen artifact detection/challenge-response"], | |
| ["Silicone fingerprint", "Presentation attack", "Fingerprint", "Fake finger accepted", "Perspiration/pulse/texture PAD"], | |
| ["Deepfake face", "Synthetic attack", "Face", "Video impersonation", "Deepfake detection + active challenge"], | |
| ["Adversarial perturbation", "Model attack", "Any deep model", "Model misclassification", "Adversarial training"], | |
| ["Template inversion", "Template attack", "Stored embeddings", "Recover biometric information", "Cancelable templates/encryption"], | |
| ["Hill-climbing attack", "Matcher attack", "Score-based systems", "Score optimization", "Limit score leakage/rate limiting"], | |
| ["Replay of stored template", "Database attack", "Template storage", "Identity compromise", "Template protection/key binding"], | |
| ["Texture PAD studies", "Anti-spoofing", "Face", "Photo attack detection", "LBP/texture features"], | |
| ["Replay-Attack dataset studies", "Dataset/PAD", "Face", "Replay/photo detection", "Standardized PAD evaluation"], | |
| ["CASIA-FASD studies", "Dataset/PAD", "Face", "Video/photo attack detection", "Motion/texture cues"], | |
| ["LivDet studies", "Fingerprint PAD", "Fingerprint", "Fake fingerprint detection", "Benchmark anti-spoofing"], | |
| ["Depth-based PAD", "Anti-spoofing", "Face", "Flat photo rejection", "Depth camera / 3D cues"], | |
| ["rPPG liveness", "Anti-spoofing", "Face", "Detect pulse signal", "Needs video and lighting quality"], | |
| ["Multimodal PAD", "Defense", "Multiple", "Improved robustness", "Higher cost and complexity"], | |
| ] | |
| cols = ["Paper / attack", "Category", "Modality", "Risk", "Defense"] | |
| return pd.DataFrame(rows, columns=cols) | |
| def survey_notes(topic): | |
| return ( | |
| f"### {topic}\n\n" | |
| "This is a starter comparison matrix for the literature survey. " | |
| "Before final submission, replace qualitative entries with exact metrics from your selected papers: " | |
| "dataset, accuracy/EER/FAR/FRR/APCER/BPCER, computational cost, advantages, and limitations." | |
| ) | |
| def update_survey(topic): | |
| return survey_notes(topic), survey_table(topic) | |
| # --------------------------------------------------------------------- | |
| # Gradio UI | |
| # --------------------------------------------------------------------- | |
| CSS = """ | |
| .gradio-container { max-width: 1200px !important; } | |
| """ | |
| with gr.Blocks(title=APP_TITLE, css=CSS) as demo: | |
| gr.Markdown( | |
| "# " + APP_TITLE + "\n\n" | |
| "This is a professor-facing educational demo for a biometric authentication literature-survey project.\n\n" | |
| "It demonstrates feature extraction, template protection, deep-learning trade-offs, verification, attacks, liveness, and survey tables.\n\n" | |
| "**Security note:** This is not a production biometric login system. It stores no permanent biometric database." | |
| ) | |
| with gr.Tab("1. Project Overview"): | |
| gr.Markdown( | |
| "## Biometric authentication pipeline\n\n" | |
| "Biometric input -> preprocessing -> feature extraction -> template generation -> template protection -> matching -> liveness check -> accept/reject\n\n" | |
| "## Student-wise mapping\n\n" | |
| "| Student | Assignment area | App tabs |\n" | |
| "|---|---|---|\n" | |
| "| Student 1 | Feature extraction | Feature Extraction Lab |\n" | |
| "| Student 2 | Template protection | Template Protection Lab |\n" | |
| "| Student 3 | Deep learning | Deep Model Comparison |\n" | |
| "| Student 4 | Attacks and liveness | Attacks & Liveness |\n\n" | |
| "The app is designed to fail closed. It does not return fake authentication success if real processing fails." | |
| ) | |
| with gr.Tab("2. Feature Extraction Lab"): | |
| with gr.Row(): | |
| with gr.Column(): | |
| feat_img = gr.Image(label="Upload biometric image", type="pil") | |
| feat_modality = gr.Dropdown(["Fingerprint", "Iris", "Face"], value="Fingerprint", label="Biometric modality") | |
| feat_method = gr.Dropdown(["Minutiae-like", "LBP", "Gabor", "SIFT/SURF-like", "CNN-like", "Deep embedding"], value="Gabor", label="Feature extraction method") | |
| feat_btn = gr.Button("Extract features") | |
| with gr.Column(): | |
| feat_pre = gr.Image(label="Preprocessed image") | |
| feat_vis = gr.Image(label="Feature visualization") | |
| feat_plot_out = gr.Plot(label="Feature vector plot") | |
| feat_df_out = gr.Dataframe(label="Feature vector preview") | |
| feat_json_out = gr.JSON(label="Method metadata") | |
| feat_md_out = gr.Markdown() | |
| feat_btn.click(run_feature_lab, [feat_img, feat_modality, feat_method], [feat_pre, feat_vis, feat_plot_out, feat_df_out, feat_json_out, feat_md_out]) | |
| with gr.Tab("3. Enrollment & Verification Demo"): | |
| gr.Markdown( | |
| "Upload one image as the enrolled template and another image as the verification attempt. " | |
| "The app extracts features from both, applies the selected template protection transform, then compares similarity." | |
| ) | |
| with gr.Row(): | |
| enroll_img = gr.Image(label="Enrollment image", type="pil") | |
| verify_img = gr.Image(label="Verification image", type="pil") | |
| with gr.Row(): | |
| verify_modality = gr.Dropdown(["Fingerprint", "Iris", "Face"], value="Fingerprint", label="Modality") | |
| verify_method = gr.Dropdown(["Minutiae-like", "LBP", "Gabor", "SIFT/SURF-like", "CNN-like", "Deep embedding"], value="Gabor", label="Feature method") | |
| with gr.Row(): | |
| verify_protection = gr.Dropdown(["Plain template", "Encrypted storage", "Cancelable biometric", "BioHashing", "Chaotic mapping", "Fuzzy extractor simulation", "Toy homomorphic encryption"], value="Cancelable biometric", label="Template protection") | |
| secret_key = gr.Textbox(value="student-demo-key", label="Secret key / transform key") | |
| threshold = gr.Slider(0.0, 1.0, value=0.75, step=0.01, label="Decision threshold") | |
| verify_btn = gr.Button("Run verification") | |
| verify_result = gr.Markdown() | |
| verify_metrics = gr.Dataframe(label="Decision metrics") | |
| with gr.Row(): | |
| enroll_feat_vis = gr.Image(label="Enrollment feature visualization") | |
| verify_feat_vis = gr.Image(label="Verification feature visualization") | |
| verify_btn.click(run_verification, [enroll_img, verify_img, verify_modality, verify_method, verify_protection, secret_key, threshold], [verify_result, verify_metrics, enroll_feat_vis, verify_feat_vis]) | |
| with gr.Tab("4. Template Protection Lab"): | |
| with gr.Row(): | |
| with gr.Column(): | |
| tpl_img = gr.Image(label="Upload biometric image", type="pil") | |
| tpl_modality = gr.Dropdown(["Fingerprint", "Iris", "Face"], value="Fingerprint", label="Modality") | |
| tpl_feature = gr.Dropdown(["Minutiae-like", "LBP", "Gabor", "SIFT/SURF-like", "CNN-like", "Deep embedding"], value="Deep embedding", label="Feature method") | |
| tpl_protection = gr.Dropdown(["Plain template", "Encrypted storage", "Cancelable biometric", "BioHashing", "Chaotic mapping", "Fuzzy extractor simulation", "Toy homomorphic encryption"], value="BioHashing", label="Protection method") | |
| tpl_secret = gr.Textbox(value="student-demo-key", label="Secret key") | |
| tpl_btn = gr.Button("Generate protected template") | |
| with gr.Column(): | |
| tpl_vis = gr.Image(label="Feature visualization") | |
| tpl_md = gr.Markdown() | |
| tpl_raw_df = gr.Dataframe(label="Raw feature preview") | |
| tpl_info_df = gr.Dataframe(label="Protection properties") | |
| tpl_btn.click(run_template_lab, [tpl_img, tpl_modality, tpl_feature, tpl_protection, tpl_secret], [tpl_md, tpl_raw_df, tpl_info_df, tpl_vis]) | |
| with gr.Tab("5. Deep Model Comparison"): | |
| gr.Markdown( | |
| "This tab supports Student 3's literature review. It compares CNN, ResNet, MobileNet, Vision Transformer, Autoencoder, and FaceNet/ArcFace-style embeddings." | |
| ) | |
| gr.Dataframe(value=model_comparison_table(), label="Deep learning model comparison") | |
| selected_model = gr.Dropdown(["Shallow CNN", "ResNet", "MobileNet", "Vision Transformer", "Autoencoder", "FaceNet / ArcFace-style"], value="MobileNet", label="Select model") | |
| model_md = gr.Markdown(value=model_notes("MobileNet")) | |
| selected_model.change(model_notes, [selected_model], [model_md]) | |
| with gr.Tab("6. Attacks & Liveness"): | |
| gr.Markdown( | |
| "This tab supports Student 4's survey on attacks and anti-spoofing. " | |
| "It simulates common input attacks and estimates a basic liveness score." | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| attack_img = gr.Image(label="Upload image", type="pil") | |
| attack_type = gr.Dropdown(["None", "Blur / out-of-focus", "Gaussian noise", "Low-contrast print", "Replay-screen scanlines", "Deepfake-like smoothing", "Adversarial-style tiny noise"], value="Low-contrast print", label="Attack simulation") | |
| attack_intensity = gr.Slider(0.0, 1.0, value=0.5, step=0.05, label="Attack intensity") | |
| attack_btn = gr.Button("Simulate attack + check liveness") | |
| with gr.Column(): | |
| attacked_img = gr.Image(label="Attacked / modified image") | |
| attack_md = gr.Markdown() | |
| attack_df = gr.Dataframe(label="Liveness metrics") | |
| attack_btn.click(run_attack_lab, [attack_img, attack_type, attack_intensity], [attacked_img, attack_df, attack_md]) | |
| gr.Markdown( | |
| "## Attack-defense taxonomy\n\n" | |
| "| Attack | Description | Typical defense |\n" | |
| "|---|---|---|\n" | |
| "| Presentation attack | Fake biometric shown to sensor | Liveness / PAD |\n" | |
| "| Replay attack | Photo or video on screen | Challenge-response |\n" | |
| "| Deepfake attack | Synthetic face/video | Deepfake detector + temporal cues |\n" | |
| "| Adversarial attack | Small perturbation fools model | Robust training |\n" | |
| "| Template attack | Stored template stolen | Cancelable biometrics + encryption |" | |
| ) | |
| with gr.Tab("7. Literature Survey Tables"): | |
| survey_topic = gr.Dropdown(["Student 1 - Feature Extraction", "Student 2 - Template Protection", "Student 3 - Deep Learning", "Student 4 - Attacks & Liveness"], value="Student 1 - Feature Extraction", label="Select student topic") | |
| survey_md = gr.Markdown(value=survey_notes("Student 1 - Feature Extraction")) | |
| survey_df = gr.Dataframe(value=survey_table("Student 1 - Feature Extraction"), label="Survey comparison table") | |
| survey_topic.change(update_survey, [survey_topic], [survey_md, survey_df]) | |
| with gr.Tab("8. Viva / Explanation Script"): | |
| gr.Markdown( | |
| "## 2-minute explanation for professor\n\n" | |
| "Our project is a literature-survey-based biometric authentication demo. The biometric pipeline starts with image acquisition. " | |
| "Preprocessing improves the image quality. Then features are extracted using handcrafted methods such as minutiae, LBP, Gabor filters, and SIFT/SURF-like descriptors, or deep-feature ideas such as CNN-style embeddings.\n\n" | |
| "The extracted vector is called a biometric template. A raw template is risky because if it is stolen, the user cannot change their fingerprint or iris. Therefore, the template protection tab demonstrates encryption, cancelable biometrics, BioHashing, chaotic mapping, fuzzy-extractor simulation, and homomorphic-encryption concepts.\n\n" | |
| "The verification tab compares an enrolled image with a verification image using similarity scores. The system accepts only when the score is above a threshold and the liveness score is acceptable.\n\n" | |
| "The attack tab demonstrates spoofing and presentation attack ideas. It shows how blur, print-like low contrast, replay-screen scanlines, deepfake-like smoothing, and adversarial noise can affect the biometric input.\n\n" | |
| "The deep-learning tab compares CNN, ResNet, MobileNet, Vision Transformers, Autoencoders, and FaceNet/ArcFace-style embeddings in terms of parameters, FLOPs, accuracy tendency, inference time, and edge deployment.\n\n" | |
| "This is an educational demonstration, not a production security system. Its purpose is to connect the literature survey with visible working examples." | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() |