Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,26 +1,7 @@
|
|
| 1 |
-
# app.py
|
| 2 |
-
# Biometric Authentication Literature Survey + Interactive Demonstration
|
| 3 |
-
# Designed for Hugging Face Spaces free CPU tier.
|
| 4 |
-
#
|
| 5 |
-
# Educational scope:
|
| 6 |
-
# - Fingerprint, iris, and optional face feature extraction
|
| 7 |
-
# - Handcrafted features: minutiae-like, LBP, Gabor, SIFT-like
|
| 8 |
-
# - Deep-feature simulation: CNN-like and deep embedding
|
| 9 |
-
# - Enrollment vs verification matching
|
| 10 |
-
# - Template protection demonstrations
|
| 11 |
-
# - Attack/liveness simulation
|
| 12 |
-
# - Survey comparison tables for all 4 assigned students
|
| 13 |
-
#
|
| 14 |
-
# Important:
|
| 15 |
-
# This is NOT a production biometric authentication system.
|
| 16 |
-
# It stores no biometric database and performs session-only comparisons.
|
| 17 |
-
|
| 18 |
import base64
|
| 19 |
import hashlib
|
| 20 |
-
import io
|
| 21 |
-
import math
|
| 22 |
import warnings
|
| 23 |
-
from typing import Dict,
|
| 24 |
|
| 25 |
import gradio as gr
|
| 26 |
import matplotlib.pyplot as plt
|
|
@@ -32,99 +13,97 @@ warnings.filterwarnings("ignore")
|
|
| 32 |
|
| 33 |
try:
|
| 34 |
from cryptography.fernet import Fernet
|
| 35 |
-
|
| 36 |
HAS_CRYPTO = True
|
| 37 |
except Exception:
|
| 38 |
HAS_CRYPTO = False
|
| 39 |
|
| 40 |
try:
|
| 41 |
import cv2
|
| 42 |
-
|
| 43 |
HAS_CV2 = True
|
| 44 |
except Exception:
|
| 45 |
HAS_CV2 = False
|
| 46 |
|
| 47 |
-
|
| 48 |
APP_TITLE = "Biometric Authentication Literature Survey & Interactive Demo"
|
| 49 |
DEFAULT_SIZE = 128
|
| 50 |
|
| 51 |
|
| 52 |
# ---------------------------------------------------------------------
|
| 53 |
-
#
|
| 54 |
# ---------------------------------------------------------------------
|
| 55 |
|
| 56 |
-
def
|
| 57 |
if img is None:
|
| 58 |
return None
|
| 59 |
if isinstance(img, Image.Image):
|
| 60 |
return img.convert("RGB")
|
| 61 |
-
return Image.fromarray(np.
|
| 62 |
|
| 63 |
|
| 64 |
-
def
|
| 65 |
-
arr = np.asarray(arr)
|
| 66 |
arr = np.nan_to_num(arr)
|
| 67 |
-
if arr.
|
|
|
|
|
|
|
| 68 |
arr = arr * 255.0
|
| 69 |
arr = np.clip(arr, 0, 255).astype(np.uint8)
|
| 70 |
return Image.fromarray(arr)
|
| 71 |
|
| 72 |
|
| 73 |
-
def
|
| 74 |
arr = np.asarray(arr, dtype=np.float32)
|
| 75 |
-
mn
|
|
|
|
| 76 |
if mx - mn < 1e-8:
|
| 77 |
return np.zeros_like(arr, dtype=np.float32)
|
| 78 |
return (arr - mn) / (mx - mn)
|
| 79 |
|
| 80 |
|
| 81 |
-
def
|
| 82 |
-
key = key or "student-demo-key"
|
| 83 |
digest = hashlib.sha256(key.encode("utf-8")).digest()
|
| 84 |
return int.from_bytes(digest[:8], "little") % (2**32 - 1)
|
| 85 |
|
| 86 |
|
| 87 |
-
def
|
| 88 |
-
img =
|
| 89 |
gray = ImageOps.grayscale(img)
|
| 90 |
gray = ImageOps.autocontrast(gray)
|
| 91 |
gray = gray.resize((size, size))
|
| 92 |
return np.asarray(gray, dtype=np.float32) / 255.0
|
| 93 |
|
| 94 |
|
| 95 |
-
def
|
| 96 |
-
vec = np.asarray(vec, dtype=np.float32).flatten()
|
| 97 |
-
if len(vec) == length:
|
| 98 |
-
return vec
|
| 99 |
-
if len(vec) > length:
|
| 100 |
-
return vec[:length]
|
| 101 |
-
out = np.zeros(length, dtype=np.float32)
|
| 102 |
-
out[:len(vec)] = vec
|
| 103 |
-
return out
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
def _unit_vector(vec: np.ndarray) -> np.ndarray:
|
| 107 |
vec = np.asarray(vec, dtype=np.float32).flatten()
|
| 108 |
vec = np.nan_to_num(vec)
|
| 109 |
-
norm = np.linalg.norm(vec)
|
| 110 |
if norm < 1e-8:
|
| 111 |
return vec
|
| 112 |
return vec / norm
|
| 113 |
|
| 114 |
|
| 115 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
a = np.asarray(a, dtype=np.float32).flatten()
|
| 117 |
b = np.asarray(b, dtype=np.float32).flatten()
|
| 118 |
n = min(len(a), len(b))
|
| 119 |
if n == 0:
|
| 120 |
return 0.0
|
| 121 |
-
a =
|
| 122 |
-
b =
|
| 123 |
-
|
| 124 |
-
return max(0.0, min(1.0, (
|
| 125 |
|
| 126 |
|
| 127 |
-
def
|
| 128 |
a = np.asarray(a).flatten() > 0.5
|
| 129 |
b = np.asarray(b).flatten() > 0.5
|
| 130 |
n = min(len(a), len(b))
|
|
@@ -133,24 +112,20 @@ def _hamming_similarity(a: np.ndarray, b: np.ndarray) -> float:
|
|
| 133 |
return float(1.0 - np.mean(a[:n] != b[:n]))
|
| 134 |
|
| 135 |
|
| 136 |
-
def
|
| 137 |
vec = np.asarray(vec).flatten()
|
| 138 |
-
|
| 139 |
-
return np.array2string(shown, precision=4, separator=", ")
|
| 140 |
|
| 141 |
|
| 142 |
-
def
|
| 143 |
vec = np.asarray(vec).flatten()
|
| 144 |
-
|
| 145 |
-
for i, v in enumerate(vec[:limit]):
|
| 146 |
-
rows.append({"index": i, "value": float(v)})
|
| 147 |
-
return pd.DataFrame(rows)
|
| 148 |
|
| 149 |
|
| 150 |
-
def
|
| 151 |
vec = np.asarray(vec).flatten()
|
| 152 |
-
fig = plt.figure(figsize=(7, 3))
|
| 153 |
n = min(64, len(vec))
|
|
|
|
| 154 |
plt.bar(np.arange(n), vec[:n])
|
| 155 |
plt.title(title)
|
| 156 |
plt.xlabel("Feature index")
|
|
@@ -160,17 +135,15 @@ def _fig_feature_bar(vec: np.ndarray, title: str = "Feature vector preview"):
|
|
| 160 |
|
| 161 |
|
| 162 |
# ---------------------------------------------------------------------
|
| 163 |
-
#
|
| 164 |
# ---------------------------------------------------------------------
|
| 165 |
|
| 166 |
-
def preprocess_modality(img
|
| 167 |
-
img =
|
| 168 |
if img is None:
|
| 169 |
raise ValueError("Please upload an image.")
|
| 170 |
|
| 171 |
if modality == "Iris":
|
| 172 |
-
# Educational iris approximation:
|
| 173 |
-
# central crop + circular mask. This is not true iris segmentation.
|
| 174 |
w, h = img.size
|
| 175 |
side = min(w, h)
|
| 176 |
left = (w - side) // 2
|
|
@@ -182,25 +155,21 @@ def preprocess_modality(img: Image.Image, modality: str) -> Tuple[np.ndarray, Im
|
|
| 182 |
arr = np.asarray(gray, dtype=np.float32) / 255.0
|
| 183 |
|
| 184 |
yy, xx = np.ogrid[:DEFAULT_SIZE, :DEFAULT_SIZE]
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
mask =
|
| 190 |
-
masked = arr.copy()
|
| 191 |
-
masked[~mask] = 0.0
|
| 192 |
-
|
| 193 |
meta = {
|
| 194 |
"modality": modality,
|
| 195 |
"preprocessing": "central crop, grayscale, autocontrast, circular iris-style mask",
|
| 196 |
-
"note": "Educational approximation; not a
|
| 197 |
}
|
| 198 |
-
return
|
| 199 |
|
| 200 |
if modality == "Fingerprint":
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
pil = _array_to_pil(gray)
|
| 204 |
pil = ImageEnhance.Contrast(pil).enhance(1.8)
|
| 205 |
pil = pil.filter(ImageFilter.SHARPEN)
|
| 206 |
arr = np.asarray(pil, dtype=np.float32) / 255.0
|
|
@@ -210,9 +179,8 @@ def preprocess_modality(img: Image.Image, modality: str) -> Tuple[np.ndarray, Im
|
|
| 210 |
}
|
| 211 |
return arr, pil, meta
|
| 212 |
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
pil = _array_to_pil(gray)
|
| 216 |
pil = ImageEnhance.Contrast(pil).enhance(1.25)
|
| 217 |
arr = np.asarray(pil, dtype=np.float32) / 255.0
|
| 218 |
meta = {
|
|
@@ -223,16 +191,15 @@ def preprocess_modality(img: Image.Image, modality: str) -> Tuple[np.ndarray, Im
|
|
| 223 |
|
| 224 |
|
| 225 |
# ---------------------------------------------------------------------
|
| 226 |
-
# Feature extraction
|
| 227 |
# ---------------------------------------------------------------------
|
| 228 |
|
| 229 |
-
def
|
| 230 |
img = np.asarray(img, dtype=np.float32)
|
| 231 |
kernel = np.asarray(kernel, dtype=np.float32)
|
| 232 |
kh, kw = kernel.shape
|
| 233 |
ph, pw = kh // 2, kw // 2
|
| 234 |
padded = np.pad(img, ((ph, ph), (pw, pw)), mode="reflect")
|
| 235 |
-
|
| 236 |
try:
|
| 237 |
windows = np.lib.stride_tricks.sliding_window_view(padded, (kh, kw))
|
| 238 |
return np.einsum("ijkl,kl->ij", windows, kernel)
|
|
@@ -249,44 +216,33 @@ def gabor_kernel(size=21, sigma=4.0, theta=0.0, frequency=0.12, gamma=0.5):
|
|
| 249 |
y, x = np.mgrid[-radius:radius + 1, -radius:radius + 1]
|
| 250 |
x_theta = x * np.cos(theta) + y * np.sin(theta)
|
| 251 |
y_theta = -x * np.sin(theta) + y * np.cos(theta)
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
return gb.astype(np.float32)
|
| 257 |
|
| 258 |
|
| 259 |
-
def extract_gabor(arr
|
| 260 |
orientations = [0, np.pi / 4, np.pi / 2, 3 * np.pi / 4]
|
| 261 |
responses = []
|
| 262 |
-
|
| 263 |
-
|
| 264 |
for theta in orientations:
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
float(abs_resp.mean()),
|
| 271 |
-
float(abs_resp.std()),
|
| 272 |
-
float(abs_resp.max()),
|
| 273 |
-
float(np.percentile(abs_resp, 75)),
|
| 274 |
-
])
|
| 275 |
-
|
| 276 |
-
stacked = np.stack([np.abs(r) for r in responses], axis=0)
|
| 277 |
-
visual = _normalize01(stacked.max(axis=0))
|
| 278 |
-
|
| 279 |
meta = {
|
| 280 |
"method": "Gabor filters",
|
| 281 |
-
"feature_type": "
|
| 282 |
-
"feature_length": len(
|
| 283 |
-
"advantages": "
|
| 284 |
-
"limitations": "Sensitive to segmentation
|
| 285 |
}
|
| 286 |
-
return np.
|
| 287 |
|
| 288 |
|
| 289 |
-
def extract_lbp(arr
|
| 290 |
center = arr
|
| 291 |
neighbors = [
|
| 292 |
np.roll(np.roll(arr, -1, axis=0), -1, axis=1),
|
|
@@ -298,25 +254,21 @@ def extract_lbp(arr: np.ndarray) -> Tuple[np.ndarray, Image.Image, Dict]:
|
|
| 298 |
np.roll(np.roll(arr, 1, axis=0), -1, axis=1),
|
| 299 |
np.roll(arr, -1, axis=1),
|
| 300 |
]
|
| 301 |
-
|
| 302 |
code = np.zeros_like(arr, dtype=np.uint8)
|
| 303 |
for i, n in enumerate(neighbors):
|
| 304 |
code += ((n >= center).astype(np.uint8) << i)
|
| 305 |
-
|
| 306 |
hist, _ = np.histogram(code.flatten(), bins=256, range=(0, 256), density=True)
|
| 307 |
-
visual = code.astype(np.float32) / 255.0
|
| 308 |
-
|
| 309 |
meta = {
|
| 310 |
"method": "Local Binary Pattern",
|
| 311 |
-
"feature_type": "
|
| 312 |
"feature_length": len(hist),
|
| 313 |
-
"advantages": "Fast, simple,
|
| 314 |
-
"limitations": "
|
| 315 |
}
|
| 316 |
-
return hist.astype(np.float32),
|
| 317 |
|
| 318 |
|
| 319 |
-
def extract_sift_like(arr
|
| 320 |
if HAS_CV2:
|
| 321 |
img8 = np.clip(arr * 255, 0, 255).astype(np.uint8)
|
| 322 |
sift = None
|
|
@@ -324,101 +276,83 @@ def extract_sift_like(arr: np.ndarray) -> Tuple[np.ndarray, Image.Image, Dict]:
|
|
| 324 |
sift = cv2.SIFT_create()
|
| 325 |
except Exception:
|
| 326 |
sift = None
|
| 327 |
-
|
| 328 |
if sift is not None:
|
| 329 |
keypoints, descriptors = sift.detectAndCompute(img8, None)
|
| 330 |
if descriptors is None or len(descriptors) == 0:
|
| 331 |
desc = np.zeros(128, dtype=np.float32)
|
| 332 |
else:
|
| 333 |
-
desc = descriptors.mean(axis=0).astype(np.float32)
|
| 334 |
-
desc = _unit_vector(desc)
|
| 335 |
-
|
| 336 |
color = cv2.cvtColor(img8, cv2.COLOR_GRAY2RGB)
|
| 337 |
drawn = cv2.drawKeypoints(color, keypoints[:80], None, flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
|
| 338 |
-
visual = Image.fromarray(drawn)
|
| 339 |
-
|
| 340 |
meta = {
|
| 341 |
"method": "SIFT",
|
| 342 |
-
"feature_type": "
|
| 343 |
"feature_length": len(desc),
|
| 344 |
-
"advantages": "Robust to scale
|
| 345 |
-
"limitations": "Can
|
| 346 |
}
|
| 347 |
-
return desc.astype(np.float32),
|
| 348 |
|
| 349 |
-
# Fallback SIFT-like descriptor:
|
| 350 |
-
# 4x4 grid, 8-bin orientation histogram = 128 dims.
|
| 351 |
gy, gx = np.gradient(arr)
|
| 352 |
mag = np.sqrt(gx ** 2 + gy ** 2)
|
| 353 |
ori = (np.arctan2(gy, gx) + np.pi) / (2 * np.pi)
|
| 354 |
-
|
| 355 |
cells = 4
|
| 356 |
bins = 8
|
| 357 |
h, w = arr.shape
|
| 358 |
-
ch, cw = h // cells, w // cells
|
| 359 |
feats = []
|
| 360 |
-
|
| 361 |
for cy in range(cells):
|
| 362 |
for cx in range(cells):
|
| 363 |
-
y0, y1 = cy *
|
| 364 |
-
x0, x1 = cx *
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
|
|
|
|
|
|
|
|
|
| 368 |
feats.extend(hist.tolist())
|
| 369 |
-
|
| 370 |
-
feats = _unit_vector(np.array(feats, dtype=np.float32))
|
| 371 |
-
|
| 372 |
visual = Image.fromarray(np.uint8(np.stack([arr, arr, arr], axis=-1) * 255))
|
| 373 |
draw = ImageDraw.Draw(visual)
|
| 374 |
-
|
| 375 |
-
flat_idx = np.argsort(mag.flatten())[-60:]
|
| 376 |
for idx in flat_idx:
|
| 377 |
y, x = divmod(int(idx), w)
|
| 378 |
-
draw.ellipse((x - 1, y - 1, x + 1, y + 1),
|
| 379 |
-
|
| 380 |
meta = {
|
| 381 |
-
"method": "SIFT-like fallback",
|
| 382 |
-
"feature_type": "
|
| 383 |
"feature_length": len(feats),
|
| 384 |
-
"advantages": "Demonstrates
|
| 385 |
"limitations": "Not a full SIFT/SURF implementation unless OpenCV SIFT is available."
|
| 386 |
}
|
| 387 |
return feats.astype(np.float32), visual, meta
|
| 388 |
|
| 389 |
|
| 390 |
-
def extract_minutiae_like(arr
|
| 391 |
-
|
| 392 |
-
# threshold ridges + estimate endpoints/bifurcations through neighbor counts.
|
| 393 |
-
smooth = _conv2d_same(arr, np.ones((3, 3), dtype=np.float32) / 9.0)
|
| 394 |
binary = smooth < np.percentile(smooth, 45)
|
| 395 |
-
|
| 396 |
-
# Remove border.
|
| 397 |
binary[:2, :] = False
|
| 398 |
binary[-2:, :] = False
|
| 399 |
binary[:, :2] = False
|
| 400 |
binary[:, -2:] = False
|
| 401 |
|
| 402 |
-
|
| 403 |
for dy in [-1, 0, 1]:
|
| 404 |
for dx in [-1, 0, 1]:
|
| 405 |
if dy == 0 and dx == 0:
|
| 406 |
continue
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
endpoints = binary & (neighbor_count == 1)
|
| 410 |
-
bifurcations = binary & (neighbor_count >= 3)
|
| 411 |
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
h, w = arr.shape
|
| 415 |
feats = [
|
| 416 |
float(endpoints.sum()) / 1000.0,
|
| 417 |
float(bifurcations.sum()) / 1000.0,
|
| 418 |
float(binary.mean()),
|
| 419 |
-
float(
|
| 420 |
]
|
| 421 |
-
|
|
|
|
| 422 |
for mask in [endpoints, bifurcations]:
|
| 423 |
for gy in range(grid):
|
| 424 |
for gx in range(grid):
|
|
@@ -430,7 +364,6 @@ def extract_minutiae_like(arr: np.ndarray) -> Tuple[np.ndarray, Image.Image, Dic
|
|
| 430 |
draw = ImageDraw.Draw(visual)
|
| 431 |
ey, ex = np.where(endpoints)
|
| 432 |
by, bx = np.where(bifurcations)
|
| 433 |
-
|
| 434 |
for y, x in list(zip(ey, ex))[:120]:
|
| 435 |
draw.ellipse((x - 2, y - 2, x + 2, y + 2), outline=(0, 255, 0), width=1)
|
| 436 |
for y, x in list(zip(by, bx))[:120]:
|
|
@@ -438,98 +371,72 @@ def extract_minutiae_like(arr: np.ndarray) -> Tuple[np.ndarray, Image.Image, Dic
|
|
| 438 |
|
| 439 |
meta = {
|
| 440 |
"method": "Minutiae-like extraction",
|
| 441 |
-
"feature_type": "
|
| 442 |
"feature_length": len(feats),
|
| 443 |
-
"advantages": "
|
| 444 |
-
"limitations": "Not a true
|
| 445 |
}
|
| 446 |
-
return np.
|
| 447 |
|
| 448 |
|
| 449 |
-
def extract_cnn_like(arr
|
| 450 |
-
|
| 451 |
-
# edge responses + pooled statistics across multiple grid sizes.
|
| 452 |
-
sobel_x = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=np.float32)
|
| 453 |
sobel_y = sobel_x.T
|
| 454 |
-
gx =
|
| 455 |
-
gy =
|
| 456 |
-
edge =
|
| 457 |
-
|
| 458 |
feats = []
|
|
|
|
| 459 |
for grid in [2, 4, 8]:
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
x0, x1 = x * w // grid, (x + 1) * w // grid
|
| 465 |
patch = arr[y0:y1, x0:x1]
|
| 466 |
epatch = edge[y0:y1, x0:x1]
|
| 467 |
-
feats.extend([
|
| 468 |
-
float(patch.mean()),
|
| 469 |
-
float(patch.std()),
|
| 470 |
-
float(epatch.mean()),
|
| 471 |
-
float(epatch.std()),
|
| 472 |
-
])
|
| 473 |
-
|
| 474 |
-
# Add global moments.
|
| 475 |
feats.extend([
|
| 476 |
-
float(arr.mean()),
|
| 477 |
-
float(arr.
|
| 478 |
-
float(edge.mean()),
|
| 479 |
-
float(edge.std()),
|
| 480 |
-
float(np.percentile(arr, 25)),
|
| 481 |
-
float(np.percentile(arr, 50)),
|
| 482 |
-
float(np.percentile(arr, 75)),
|
| 483 |
])
|
| 484 |
-
|
| 485 |
-
feats = _unit_vector(np.array(feats, dtype=np.float32))
|
| 486 |
-
|
| 487 |
meta = {
|
| 488 |
"method": "CNN-like embedding",
|
| 489 |
-
"feature_type": "
|
| 490 |
"feature_length": len(feats),
|
| 491 |
-
"advantages": "Demonstrates
|
| 492 |
"limitations": "Not trained; does not replace a real CNN biometric model."
|
| 493 |
}
|
| 494 |
-
return feats,
|
| 495 |
|
| 496 |
|
| 497 |
-
def extract_deep_embedding(arr
|
| 498 |
-
|
| 499 |
-
# This mimics a compact deep embedding for demonstration.
|
| 500 |
-
gabor_vec, gabor_vis, _ = extract_gabor(arr)
|
| 501 |
lbp_vec, _, _ = extract_lbp(arr)
|
| 502 |
sift_vec, _, _ = extract_sift_like(arr)
|
| 503 |
cnn_vec, cnn_vis, _ = extract_cnn_like(arr)
|
| 504 |
-
|
| 505 |
base = np.concatenate([
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
])
|
| 511 |
-
base =
|
| 512 |
-
|
| 513 |
-
rng = np.random.default_rng(_seed_from_key("deep-" + modality))
|
| 514 |
projection = rng.normal(0, 1, size=(len(base), 128)).astype(np.float32)
|
| 515 |
-
emb = base @ projection
|
| 516 |
-
emb = _unit_vector(emb)
|
| 517 |
-
|
| 518 |
-
visual = cnn_vis
|
| 519 |
-
|
| 520 |
meta = {
|
| 521 |
"method": "Deep embedding simulation",
|
| 522 |
-
"feature_type": "
|
| 523 |
"feature_length": len(emb),
|
| 524 |
-
"advantages": "Shows the idea of compact embeddings used by FaceNet
|
| 525 |
"limitations": "Educational simulation; not trained on biometric identity labels."
|
| 526 |
}
|
| 527 |
-
return emb.astype(np.float32),
|
| 528 |
|
| 529 |
|
| 530 |
-
def extract_features(img
|
| 531 |
arr, preprocessed, pre_meta = preprocess_modality(img, modality)
|
| 532 |
-
|
| 533 |
if method == "Minutiae-like":
|
| 534 |
vec, vis, meta = extract_minutiae_like(arr)
|
| 535 |
elif method == "LBP":
|
|
@@ -544,161 +451,113 @@ def extract_features(img: Image.Image, modality: str, method: str):
|
|
| 544 |
vec, vis, meta = extract_deep_embedding(arr, modality)
|
| 545 |
else:
|
| 546 |
vec, vis, meta = extract_gabor(arr)
|
| 547 |
-
|
| 548 |
-
full_meta = {**pre_meta, **meta}
|
| 549 |
-
return vec.astype(np.float32), preprocessed, vis, full_meta
|
| 550 |
|
| 551 |
|
| 552 |
# ---------------------------------------------------------------------
|
| 553 |
# Template protection
|
| 554 |
# ---------------------------------------------------------------------
|
| 555 |
|
| 556 |
-
def
|
| 557 |
-
digest = hashlib.sha256((secret or "demo-secret").encode()).digest()
|
| 558 |
return base64.urlsafe_b64encode(digest)
|
| 559 |
|
| 560 |
|
| 561 |
-
def
|
| 562 |
raw = np.asarray(vec[:64], dtype=np.float32).tobytes()
|
| 563 |
if HAS_CRYPTO:
|
| 564 |
-
|
| 565 |
-
token = f.encrypt(raw)
|
| 566 |
return token[:180].decode("utf-8") + "..."
|
| 567 |
-
|
| 568 |
-
return "cryptography package missing; SHA-256 preview only: " +
|
| 569 |
|
| 570 |
|
| 571 |
-
def random_projection(vec
|
| 572 |
-
vec =
|
| 573 |
-
rng = np.random.default_rng(
|
| 574 |
projection = rng.normal(0, 1, size=(len(vec), out_dim)).astype(np.float32)
|
| 575 |
-
|
| 576 |
-
return _unit_vector(out)
|
| 577 |
|
| 578 |
|
| 579 |
-
def biohash(vec
|
| 580 |
projected = random_projection(vec, secret, out_dim)
|
| 581 |
return (projected > np.median(projected)).astype(np.float32)
|
| 582 |
|
| 583 |
|
| 584 |
-
def
|
| 585 |
vec = np.asarray(vec, dtype=np.float32).flatten()
|
| 586 |
-
seed =
|
| 587 |
x = ((seed % 100000) + 1) / 100001.0
|
| 588 |
r = 3.99
|
| 589 |
-
|
| 590 |
for _ in range(len(vec)):
|
| 591 |
-
x = r * x * (1 - x)
|
| 592 |
-
|
| 593 |
-
perm = np.argsort(
|
| 594 |
-
return
|
| 595 |
|
| 596 |
|
| 597 |
-
def fuzzy_bits(vec
|
| 598 |
projected = random_projection(vec, secret, out_dim)
|
| 599 |
return (projected > 0).astype(np.float32)
|
| 600 |
|
| 601 |
|
| 602 |
-
def protect_for_matching(vec
|
| 603 |
vec = np.asarray(vec, dtype=np.float32).flatten()
|
| 604 |
-
|
| 605 |
if method == "Plain template":
|
| 606 |
-
return
|
| 607 |
-
|
| 608 |
if method == "Encrypted storage":
|
| 609 |
-
|
| 610 |
-
# unless using special cryptographic protocols.
|
| 611 |
-
return _unit_vector(vec), "cosine", (
|
| 612 |
-
"Template is encrypted at rest. For this demo, matching uses the decrypted vector. "
|
| 613 |
-
"Encryption protects storage but does not provide cancelability by itself."
|
| 614 |
-
)
|
| 615 |
-
|
| 616 |
if method == "Cancelable biometric":
|
| 617 |
-
return random_projection(vec, secret), "cosine",
|
| 618 |
-
"Feature vector is transformed using a secret-key random projection. "
|
| 619 |
-
"Changing the key revokes and reissues a new template."
|
| 620 |
-
)
|
| 621 |
-
|
| 622 |
if method == "BioHashing":
|
| 623 |
-
return biohash(vec, secret), "hamming",
|
| 624 |
-
"Projected features are binarized into a BioHash. "
|
| 625 |
-
"Comparison uses Hamming similarity."
|
| 626 |
-
)
|
| 627 |
-
|
| 628 |
if method == "Chaotic mapping":
|
| 629 |
-
return
|
| 630 |
-
"A logistic-map sequence permutes the feature vector. "
|
| 631 |
-
"Changing the key changes the permutation."
|
| 632 |
-
)
|
| 633 |
-
|
| 634 |
if method == "Fuzzy extractor simulation":
|
| 635 |
-
return fuzzy_bits(vec, secret), "hamming",
|
| 636 |
-
"Features are converted into stable binary helper-data-style bits. "
|
| 637 |
-
"This demonstrates the concept; it is not a full fuzzy extractor implementation."
|
| 638 |
-
)
|
| 639 |
-
|
| 640 |
if method == "Toy homomorphic encryption":
|
| 641 |
-
return
|
| 642 |
-
|
| 643 |
-
"with much higher cost."
|
| 644 |
-
)
|
| 645 |
-
|
| 646 |
-
return _unit_vector(vec), "cosine", "Default normalized template."
|
| 647 |
|
| 648 |
|
| 649 |
-
def template_preview(vec
|
| 650 |
protected, metric, explanation = protect_for_matching(vec, method, secret)
|
| 651 |
-
|
| 652 |
if method == "Encrypted storage":
|
| 653 |
-
preview =
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
"ciphertext preview",
|
| 658 |
-
metric,
|
| 659 |
-
"possible by changing encryption key, but biometric itself is unchanged",
|
| 660 |
-
explanation
|
| 661 |
-
]
|
| 662 |
-
})
|
| 663 |
-
return preview, df
|
| 664 |
-
|
| 665 |
-
if method == "Toy homomorphic encryption":
|
| 666 |
-
quantized = np.round(np.asarray(vec[:16]) * 1000).astype(int)
|
| 667 |
-
preview = "Encrypted-integer toy preview: " + np.array2string(quantized, separator=", ")
|
| 668 |
else:
|
| 669 |
-
preview =
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
"field": ["protected length", "matching metric", "revocability", "explanation"],
|
| 673 |
"value": [
|
| 674 |
len(protected),
|
| 675 |
metric,
|
| 676 |
"Yes" if method in ["Cancelable biometric", "BioHashing", "Chaotic mapping", "Fuzzy extractor simulation"] else "Limited",
|
| 677 |
-
explanation
|
| 678 |
-
]
|
| 679 |
})
|
| 680 |
-
return preview,
|
| 681 |
|
| 682 |
|
| 683 |
# ---------------------------------------------------------------------
|
| 684 |
# Liveness and attacks
|
| 685 |
# ---------------------------------------------------------------------
|
| 686 |
|
| 687 |
-
def liveness_metrics(img
|
| 688 |
arr, _, _ = preprocess_modality(img, "Face")
|
| 689 |
-
|
| 690 |
-
lap_kernel = np.array([[0, 1, 0], [1, -4, 1], [0, 1, 0]], dtype=np.float32)
|
| 691 |
-
lap = _conv2d_same(arr, lap_kernel)
|
| 692 |
blur_var = float(lap.var())
|
| 693 |
|
| 694 |
-
# Frequency energy.
|
| 695 |
fft = np.fft.fftshift(np.fft.fft2(arr))
|
| 696 |
mag = np.abs(fft)
|
| 697 |
h, w = mag.shape
|
| 698 |
cy, cx = h // 2, w // 2
|
| 699 |
yy, xx = np.ogrid[:h, :w]
|
| 700 |
dist = np.sqrt((yy - cy) ** 2 + (xx - cx) ** 2)
|
| 701 |
-
high_mask = dist >
|
| 702 |
high_freq_ratio = float(mag[high_mask].sum() / (mag.sum() + 1e-8))
|
| 703 |
|
| 704 |
lbp_vec, _, _ = extract_lbp(arr)
|
|
@@ -707,183 +566,146 @@ def liveness_metrics(img: Image.Image) -> Dict:
|
|
| 707 |
|
| 708 |
contrast = float(arr.std())
|
| 709 |
brightness = float(arr.mean())
|
| 710 |
-
|
| 711 |
blur_score = min(1.0, blur_var * 120.0)
|
| 712 |
freq_score = min(1.0, high_freq_ratio * 4.0)
|
| 713 |
contrast_score = min(1.0, contrast * 4.0)
|
| 714 |
-
|
| 715 |
overall = 0.30 * blur_score + 0.30 * freq_score + 0.25 * entropy_score + 0.15 * contrast_score
|
| 716 |
|
| 717 |
-
|
| 718 |
if blur_score < 0.18:
|
| 719 |
-
|
| 720 |
if freq_score < 0.18:
|
| 721 |
-
|
| 722 |
if contrast < 0.05:
|
| 723 |
-
|
| 724 |
if brightness < 0.08 or brightness > 0.92:
|
| 725 |
-
|
| 726 |
|
| 727 |
return {
|
| 728 |
-
"blur_score": round(blur_score, 4),
|
| 729 |
-
"frequency_score": round(freq_score, 4),
|
| 730 |
-
"texture_entropy_score": round(entropy_score, 4),
|
| 731 |
-
"contrast_score": round(contrast_score, 4),
|
| 732 |
-
"brightness": round(brightness, 4),
|
| 733 |
"overall_liveness_score": round(float(overall), 4),
|
| 734 |
-
"suspicious_reasons": ", ".join(
|
| 735 |
}
|
| 736 |
|
| 737 |
|
| 738 |
-
def simulate_attack(img
|
| 739 |
-
img =
|
| 740 |
if img is None:
|
| 741 |
raise ValueError("Please upload an image.")
|
| 742 |
intensity = float(intensity)
|
| 743 |
|
| 744 |
if attack == "None":
|
| 745 |
return img
|
| 746 |
-
|
| 747 |
if attack == "Blur / out-of-focus":
|
| 748 |
-
return img.filter(ImageFilter.GaussianBlur(radius=0.5 + intensity * 5))
|
| 749 |
-
|
| 750 |
if attack == "Gaussian noise":
|
| 751 |
arr = np.asarray(img).astype(np.float32)
|
| 752 |
rng = np.random.default_rng(123)
|
| 753 |
noise = rng.normal(0, 8 + intensity * 45, size=arr.shape)
|
| 754 |
-
|
| 755 |
-
return Image.fromarray(out)
|
| 756 |
-
|
| 757 |
if attack == "Low-contrast print":
|
| 758 |
out = ImageOps.grayscale(img).convert("RGB")
|
| 759 |
out = ImageEnhance.Contrast(out).enhance(max(0.2, 1.0 - intensity * 0.8))
|
| 760 |
out = ImageEnhance.Brightness(out).enhance(0.85 + intensity * 0.15)
|
| 761 |
return out
|
| 762 |
-
|
| 763 |
if attack == "Replay-screen scanlines":
|
| 764 |
arr = np.asarray(img).astype(np.float32)
|
| 765 |
step = max(2, int(8 - intensity * 5))
|
| 766 |
arr[::step, :, :] *= 0.55
|
| 767 |
arr[:, ::max(3, step + 1), :] *= 0.85
|
| 768 |
return Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8))
|
| 769 |
-
|
| 770 |
if attack == "Deepfake-like smoothing":
|
| 771 |
out = img.filter(ImageFilter.MedianFilter(size=3))
|
| 772 |
out = out.filter(ImageFilter.GaussianBlur(radius=0.5 + intensity * 2.5))
|
| 773 |
out = ImageEnhance.Sharpness(out).enhance(0.5)
|
| 774 |
return out
|
| 775 |
-
|
| 776 |
if attack == "Adversarial-style tiny noise":
|
| 777 |
arr = np.asarray(img).astype(np.float32)
|
| 778 |
rng = np.random.default_rng(999)
|
| 779 |
pattern = rng.choice([-1, 1], size=arr.shape) * (2 + intensity * 12)
|
| 780 |
-
|
| 781 |
-
return Image.fromarray(out)
|
| 782 |
-
|
| 783 |
return img
|
| 784 |
|
| 785 |
|
| 786 |
# ---------------------------------------------------------------------
|
| 787 |
-
# Gradio
|
| 788 |
# ---------------------------------------------------------------------
|
| 789 |
|
| 790 |
def run_feature_lab(img, modality, method):
|
| 791 |
if img is None:
|
| 792 |
return None, None, None, pd.DataFrame(), {}, "Upload an image first."
|
| 793 |
-
|
| 794 |
try:
|
| 795 |
vec, pre, vis, meta = extract_features(img, modality, method)
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
**
|
| 802 |
-
**
|
| 803 |
-
**
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
**Limitations:** {meta.get("limitations")}
|
| 809 |
-
|
| 810 |
-
**Note:** The app is educational. For a final report, use exact metrics from the papers you review.
|
| 811 |
-
"""
|
| 812 |
-
return pre, vis, fig, df, meta, explanation
|
| 813 |
except Exception as e:
|
| 814 |
return None, None, None, pd.DataFrame(), {}, f"Error: {e}"
|
| 815 |
|
| 816 |
|
| 817 |
def run_verification(enroll_img, verify_img, modality, method, protection_method, secret_key, threshold):
|
| 818 |
if enroll_img is None or verify_img is None:
|
| 819 |
-
return "
|
| 820 |
-
|
| 821 |
try:
|
| 822 |
-
e_vec,
|
| 823 |
-
v_vec,
|
| 824 |
-
|
| 825 |
-
e_prot, metric, prot_explanation = protect_for_matching(e_vec, protection_method, secret_key)
|
| 826 |
v_prot, _, _ = protect_for_matching(v_vec, protection_method, secret_key)
|
| 827 |
-
|
| 828 |
if metric == "hamming":
|
| 829 |
-
similarity =
|
| 830 |
else:
|
| 831 |
-
similarity =
|
| 832 |
|
| 833 |
live = liveness_metrics(verify_img)
|
| 834 |
-
|
| 835 |
-
is_live =
|
| 836 |
-
accepted = similarity >= threshold and is_live
|
| 837 |
-
|
| 838 |
decision = "ACCEPTED" if accepted else "REJECTED"
|
| 839 |
-
color = "green" if accepted else "red"
|
| 840 |
|
| 841 |
-
|
| 842 |
-
if similarity < threshold:
|
| 843 |
-
|
| 844 |
if not is_live:
|
| 845 |
-
|
| 846 |
-
if not
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
##
|
| 851 |
-
|
| 852 |
-
|
|
| 853 |
-
|
|
| 854 |
-
|
|
| 855 |
-
|
|
| 856 |
-
|
|
| 857 |
-
| Liveness
|
| 858 |
-
|
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
**Important:** This demo fails closed. If the image cannot be processed, it does not return fake success.
|
| 865 |
-
"""
|
| 866 |
-
|
| 867 |
-
metrics_df = pd.DataFrame([
|
| 868 |
-
{"metric": "similarity", "value": round(similarity, 4)},
|
| 869 |
{"metric": "threshold", "value": round(float(threshold), 4)},
|
| 870 |
-
{"metric": "liveness_score", "value":
|
| 871 |
{"metric": "blur_score", "value": live["blur_score"]},
|
| 872 |
{"metric": "frequency_score", "value": live["frequency_score"]},
|
| 873 |
{"metric": "texture_entropy_score", "value": live["texture_entropy_score"]},
|
| 874 |
{"metric": "contrast_score", "value": live["contrast_score"]},
|
|
|
|
| 875 |
])
|
| 876 |
-
|
| 877 |
-
fig = plt.figure(figsize=(6, 3))
|
| 878 |
-
labels = ["similarity", "threshold", "liveness"]
|
| 879 |
-
values = [similarity, threshold, liveness_score]
|
| 880 |
-
plt.bar(labels, values)
|
| 881 |
-
plt.ylim(0, 1)
|
| 882 |
-
plt.title("Verification decision signals")
|
| 883 |
-
plt.tight_layout()
|
| 884 |
-
|
| 885 |
-
return result_md, metrics_df, e_vis, v_vis
|
| 886 |
-
|
| 887 |
except Exception as e:
|
| 888 |
return f"## REJECTED\n\nProcessing error: {e}", pd.DataFrame(), None, None
|
| 889 |
|
|
@@ -891,24 +713,311 @@ def run_verification(enroll_img, verify_img, modality, method, protection_method
|
|
| 891 |
def run_template_lab(img, modality, feature_method, protection_method, secret_key):
|
| 892 |
if img is None:
|
| 893 |
return "Upload an image first.", pd.DataFrame(), pd.DataFrame(), None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 894 |
|
|
|
|
|
|
|
|
|
|
| 895 |
try:
|
| 896 |
-
|
| 897 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 898 |
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
"raw_feature_value": [float(x) for x in vec[:24]]
|
| 902 |
-
})
|
| 903 |
|
| 904 |
-
md = f"""
|
| 905 |
-
## Template protection preview
|
| 906 |
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 910 |
|
| 911 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 912 |
|
| 913 |
-
|
| 914 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import base64
|
| 2 |
import hashlib
|
|
|
|
|
|
|
| 3 |
import warnings
|
| 4 |
+
from typing import Dict, Tuple
|
| 5 |
|
| 6 |
import gradio as gr
|
| 7 |
import matplotlib.pyplot as plt
|
|
|
|
| 13 |
|
| 14 |
try:
|
| 15 |
from cryptography.fernet import Fernet
|
|
|
|
| 16 |
HAS_CRYPTO = True
|
| 17 |
except Exception:
|
| 18 |
HAS_CRYPTO = False
|
| 19 |
|
| 20 |
try:
|
| 21 |
import cv2
|
|
|
|
| 22 |
HAS_CV2 = True
|
| 23 |
except Exception:
|
| 24 |
HAS_CV2 = False
|
| 25 |
|
|
|
|
| 26 |
APP_TITLE = "Biometric Authentication Literature Survey & Interactive Demo"
|
| 27 |
DEFAULT_SIZE = 128
|
| 28 |
|
| 29 |
|
| 30 |
# ---------------------------------------------------------------------
|
| 31 |
+
# General helpers
|
| 32 |
# ---------------------------------------------------------------------
|
| 33 |
|
| 34 |
+
def safe_image(img):
|
| 35 |
if img is None:
|
| 36 |
return None
|
| 37 |
if isinstance(img, Image.Image):
|
| 38 |
return img.convert("RGB")
|
| 39 |
+
return Image.fromarray(np.asarray(img)).convert("RGB")
|
| 40 |
|
| 41 |
|
| 42 |
+
def array_to_pil(arr):
|
| 43 |
+
arr = np.asarray(arr, dtype=np.float32)
|
| 44 |
arr = np.nan_to_num(arr)
|
| 45 |
+
if arr.size == 0:
|
| 46 |
+
arr = np.zeros((DEFAULT_SIZE, DEFAULT_SIZE), dtype=np.float32)
|
| 47 |
+
if float(arr.max()) <= 1.0:
|
| 48 |
arr = arr * 255.0
|
| 49 |
arr = np.clip(arr, 0, 255).astype(np.uint8)
|
| 50 |
return Image.fromarray(arr)
|
| 51 |
|
| 52 |
|
| 53 |
+
def normalize01(arr):
|
| 54 |
arr = np.asarray(arr, dtype=np.float32)
|
| 55 |
+
mn = float(arr.min())
|
| 56 |
+
mx = float(arr.max())
|
| 57 |
if mx - mn < 1e-8:
|
| 58 |
return np.zeros_like(arr, dtype=np.float32)
|
| 59 |
return (arr - mn) / (mx - mn)
|
| 60 |
|
| 61 |
|
| 62 |
+
def seed_from_key(key):
|
| 63 |
+
key = str(key or "student-demo-key")
|
| 64 |
digest = hashlib.sha256(key.encode("utf-8")).digest()
|
| 65 |
return int.from_bytes(digest[:8], "little") % (2**32 - 1)
|
| 66 |
|
| 67 |
|
| 68 |
+
def resize_gray(img, size=DEFAULT_SIZE):
|
| 69 |
+
img = safe_image(img)
|
| 70 |
gray = ImageOps.grayscale(img)
|
| 71 |
gray = ImageOps.autocontrast(gray)
|
| 72 |
gray = gray.resize((size, size))
|
| 73 |
return np.asarray(gray, dtype=np.float32) / 255.0
|
| 74 |
|
| 75 |
|
| 76 |
+
def unit_vector(vec):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
vec = np.asarray(vec, dtype=np.float32).flatten()
|
| 78 |
vec = np.nan_to_num(vec)
|
| 79 |
+
norm = float(np.linalg.norm(vec))
|
| 80 |
if norm < 1e-8:
|
| 81 |
return vec
|
| 82 |
return vec / norm
|
| 83 |
|
| 84 |
|
| 85 |
+
def pad_or_trim(vec, length):
|
| 86 |
+
vec = np.asarray(vec, dtype=np.float32).flatten()
|
| 87 |
+
if len(vec) >= length:
|
| 88 |
+
return vec[:length]
|
| 89 |
+
out = np.zeros(length, dtype=np.float32)
|
| 90 |
+
out[: len(vec)] = vec
|
| 91 |
+
return out
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def cosine_similarity(a, b):
|
| 95 |
a = np.asarray(a, dtype=np.float32).flatten()
|
| 96 |
b = np.asarray(b, dtype=np.float32).flatten()
|
| 97 |
n = min(len(a), len(b))
|
| 98 |
if n == 0:
|
| 99 |
return 0.0
|
| 100 |
+
a = unit_vector(a[:n])
|
| 101 |
+
b = unit_vector(b[:n])
|
| 102 |
+
raw = float(np.dot(a, b))
|
| 103 |
+
return max(0.0, min(1.0, (raw + 1.0) / 2.0))
|
| 104 |
|
| 105 |
|
| 106 |
+
def hamming_similarity(a, b):
|
| 107 |
a = np.asarray(a).flatten() > 0.5
|
| 108 |
b = np.asarray(b).flatten() > 0.5
|
| 109 |
n = min(len(a), len(b))
|
|
|
|
| 112 |
return float(1.0 - np.mean(a[:n] != b[:n]))
|
| 113 |
|
| 114 |
|
| 115 |
+
def vector_preview(vec, limit=24):
|
| 116 |
vec = np.asarray(vec).flatten()
|
| 117 |
+
return np.array2string(vec[:limit], precision=4, separator=", ")
|
|
|
|
| 118 |
|
| 119 |
|
| 120 |
+
def feature_df(vec, limit=40):
|
| 121 |
vec = np.asarray(vec).flatten()
|
| 122 |
+
return pd.DataFrame({"index": list(range(min(limit, len(vec)))), "value": [float(v) for v in vec[:limit]]})
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
|
| 125 |
+
def feature_plot(vec, title):
|
| 126 |
vec = np.asarray(vec).flatten()
|
|
|
|
| 127 |
n = min(64, len(vec))
|
| 128 |
+
fig = plt.figure(figsize=(7, 3))
|
| 129 |
plt.bar(np.arange(n), vec[:n])
|
| 130 |
plt.title(title)
|
| 131 |
plt.xlabel("Feature index")
|
|
|
|
| 135 |
|
| 136 |
|
| 137 |
# ---------------------------------------------------------------------
|
| 138 |
+
# Image preprocessing
|
| 139 |
# ---------------------------------------------------------------------
|
| 140 |
|
| 141 |
+
def preprocess_modality(img, modality):
|
| 142 |
+
img = safe_image(img)
|
| 143 |
if img is None:
|
| 144 |
raise ValueError("Please upload an image.")
|
| 145 |
|
| 146 |
if modality == "Iris":
|
|
|
|
|
|
|
| 147 |
w, h = img.size
|
| 148 |
side = min(w, h)
|
| 149 |
left = (w - side) // 2
|
|
|
|
| 155 |
arr = np.asarray(gray, dtype=np.float32) / 255.0
|
| 156 |
|
| 157 |
yy, xx = np.ogrid[:DEFAULT_SIZE, :DEFAULT_SIZE]
|
| 158 |
+
c = (DEFAULT_SIZE - 1) / 2
|
| 159 |
+
dist = np.sqrt((xx - c) ** 2 + (yy - c) ** 2)
|
| 160 |
+
mask = (dist <= DEFAULT_SIZE * 0.46) & (dist >= DEFAULT_SIZE * 0.12)
|
| 161 |
+
arr2 = arr.copy()
|
| 162 |
+
arr2[~mask] = 0.0
|
|
|
|
|
|
|
|
|
|
| 163 |
meta = {
|
| 164 |
"modality": modality,
|
| 165 |
"preprocessing": "central crop, grayscale, autocontrast, circular iris-style mask",
|
| 166 |
+
"note": "Educational approximation; not a true iris segmentation algorithm."
|
| 167 |
}
|
| 168 |
+
return arr2, array_to_pil(arr2), meta
|
| 169 |
|
| 170 |
if modality == "Fingerprint":
|
| 171 |
+
arr = resize_gray(img)
|
| 172 |
+
pil = array_to_pil(arr)
|
|
|
|
| 173 |
pil = ImageEnhance.Contrast(pil).enhance(1.8)
|
| 174 |
pil = pil.filter(ImageFilter.SHARPEN)
|
| 175 |
arr = np.asarray(pil, dtype=np.float32) / 255.0
|
|
|
|
| 179 |
}
|
| 180 |
return arr, pil, meta
|
| 181 |
|
| 182 |
+
arr = resize_gray(img)
|
| 183 |
+
pil = array_to_pil(arr)
|
|
|
|
| 184 |
pil = ImageEnhance.Contrast(pil).enhance(1.25)
|
| 185 |
arr = np.asarray(pil, dtype=np.float32) / 255.0
|
| 186 |
meta = {
|
|
|
|
| 191 |
|
| 192 |
|
| 193 |
# ---------------------------------------------------------------------
|
| 194 |
+
# Feature extraction
|
| 195 |
# ---------------------------------------------------------------------
|
| 196 |
|
| 197 |
+
def conv2d_same(img, kernel):
|
| 198 |
img = np.asarray(img, dtype=np.float32)
|
| 199 |
kernel = np.asarray(kernel, dtype=np.float32)
|
| 200 |
kh, kw = kernel.shape
|
| 201 |
ph, pw = kh // 2, kw // 2
|
| 202 |
padded = np.pad(img, ((ph, ph), (pw, pw)), mode="reflect")
|
|
|
|
| 203 |
try:
|
| 204 |
windows = np.lib.stride_tricks.sliding_window_view(padded, (kh, kw))
|
| 205 |
return np.einsum("ijkl,kl->ij", windows, kernel)
|
|
|
|
| 216 |
y, x = np.mgrid[-radius:radius + 1, -radius:radius + 1]
|
| 217 |
x_theta = x * np.cos(theta) + y * np.sin(theta)
|
| 218 |
y_theta = -x * np.sin(theta) + y * np.cos(theta)
|
| 219 |
+
kernel = np.exp(-(x_theta ** 2 + gamma ** 2 * y_theta ** 2) / (2 * sigma ** 2))
|
| 220 |
+
kernel *= np.cos(2 * np.pi * frequency * x_theta)
|
| 221 |
+
kernel -= kernel.mean()
|
| 222 |
+
return kernel.astype(np.float32)
|
|
|
|
| 223 |
|
| 224 |
|
| 225 |
+
def extract_gabor(arr):
|
| 226 |
orientations = [0, np.pi / 4, np.pi / 2, 3 * np.pi / 4]
|
| 227 |
responses = []
|
| 228 |
+
feats = []
|
|
|
|
| 229 |
for theta in orientations:
|
| 230 |
+
resp = conv2d_same(arr, gabor_kernel(theta=theta))
|
| 231 |
+
responses.append(resp)
|
| 232 |
+
a = np.abs(resp)
|
| 233 |
+
feats.extend([float(a.mean()), float(a.std()), float(a.max()), float(np.percentile(a, 75))])
|
| 234 |
+
visual = normalize01(np.stack([np.abs(r) for r in responses], axis=0).max(axis=0))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
meta = {
|
| 236 |
"method": "Gabor filters",
|
| 237 |
+
"feature_type": "handcrafted texture and ridge-frequency descriptor",
|
| 238 |
+
"feature_length": len(feats),
|
| 239 |
+
"advantages": "Interpretable and useful for fingerprint ridges and iris texture.",
|
| 240 |
+
"limitations": "Sensitive to segmentation, rotation, scale, and manually chosen parameters."
|
| 241 |
}
|
| 242 |
+
return np.asarray(feats, dtype=np.float32), array_to_pil(visual), meta
|
| 243 |
|
| 244 |
|
| 245 |
+
def extract_lbp(arr):
|
| 246 |
center = arr
|
| 247 |
neighbors = [
|
| 248 |
np.roll(np.roll(arr, -1, axis=0), -1, axis=1),
|
|
|
|
| 254 |
np.roll(np.roll(arr, 1, axis=0), -1, axis=1),
|
| 255 |
np.roll(arr, -1, axis=1),
|
| 256 |
]
|
|
|
|
| 257 |
code = np.zeros_like(arr, dtype=np.uint8)
|
| 258 |
for i, n in enumerate(neighbors):
|
| 259 |
code += ((n >= center).astype(np.uint8) << i)
|
|
|
|
| 260 |
hist, _ = np.histogram(code.flatten(), bins=256, range=(0, 256), density=True)
|
|
|
|
|
|
|
| 261 |
meta = {
|
| 262 |
"method": "Local Binary Pattern",
|
| 263 |
+
"feature_type": "handcrafted local texture histogram",
|
| 264 |
"feature_length": len(hist),
|
| 265 |
+
"advantages": "Fast, simple, and useful for texture-based biometric patterns.",
|
| 266 |
+
"limitations": "Sensitive to noise and weaker for global structure."
|
| 267 |
}
|
| 268 |
+
return hist.astype(np.float32), array_to_pil(code.astype(np.float32) / 255.0), meta
|
| 269 |
|
| 270 |
|
| 271 |
+
def extract_sift_like(arr):
|
| 272 |
if HAS_CV2:
|
| 273 |
img8 = np.clip(arr * 255, 0, 255).astype(np.uint8)
|
| 274 |
sift = None
|
|
|
|
| 276 |
sift = cv2.SIFT_create()
|
| 277 |
except Exception:
|
| 278 |
sift = None
|
|
|
|
| 279 |
if sift is not None:
|
| 280 |
keypoints, descriptors = sift.detectAndCompute(img8, None)
|
| 281 |
if descriptors is None or len(descriptors) == 0:
|
| 282 |
desc = np.zeros(128, dtype=np.float32)
|
| 283 |
else:
|
| 284 |
+
desc = unit_vector(descriptors.mean(axis=0).astype(np.float32))
|
|
|
|
|
|
|
| 285 |
color = cv2.cvtColor(img8, cv2.COLOR_GRAY2RGB)
|
| 286 |
drawn = cv2.drawKeypoints(color, keypoints[:80], None, flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
|
|
|
|
|
|
|
| 287 |
meta = {
|
| 288 |
"method": "SIFT",
|
| 289 |
+
"feature_type": "keypoint descriptor",
|
| 290 |
"feature_length": len(desc),
|
| 291 |
+
"advantages": "Robust to scale and rotation when stable keypoints exist.",
|
| 292 |
+
"limitations": "Can be sparse on low-texture or poor-quality biometric images."
|
| 293 |
}
|
| 294 |
+
return desc.astype(np.float32), Image.fromarray(drawn), meta
|
| 295 |
|
|
|
|
|
|
|
| 296 |
gy, gx = np.gradient(arr)
|
| 297 |
mag = np.sqrt(gx ** 2 + gy ** 2)
|
| 298 |
ori = (np.arctan2(gy, gx) + np.pi) / (2 * np.pi)
|
|
|
|
| 299 |
cells = 4
|
| 300 |
bins = 8
|
| 301 |
h, w = arr.shape
|
|
|
|
| 302 |
feats = []
|
|
|
|
| 303 |
for cy in range(cells):
|
| 304 |
for cx in range(cells):
|
| 305 |
+
y0, y1 = cy * h // cells, (cy + 1) * h // cells
|
| 306 |
+
x0, x1 = cx * w // cells, (cx + 1) * w // cells
|
| 307 |
+
hist, _ = np.histogram(
|
| 308 |
+
ori[y0:y1, x0:x1].flatten(),
|
| 309 |
+
bins=bins,
|
| 310 |
+
range=(0, 1),
|
| 311 |
+
weights=mag[y0:y1, x0:x1].flatten()
|
| 312 |
+
)
|
| 313 |
feats.extend(hist.tolist())
|
| 314 |
+
feats = unit_vector(np.asarray(feats, dtype=np.float32))
|
|
|
|
|
|
|
| 315 |
visual = Image.fromarray(np.uint8(np.stack([arr, arr, arr], axis=-1) * 255))
|
| 316 |
draw = ImageDraw.Draw(visual)
|
| 317 |
+
flat_idx = np.argsort(mag.flatten())[-70:]
|
|
|
|
| 318 |
for idx in flat_idx:
|
| 319 |
y, x = divmod(int(idx), w)
|
| 320 |
+
draw.ellipse((x - 1, y - 1, x + 1, y + 1), outline=(255, 0, 0))
|
|
|
|
| 321 |
meta = {
|
| 322 |
+
"method": "SIFT/SURF-like fallback",
|
| 323 |
+
"feature_type": "educational gradient orientation descriptor",
|
| 324 |
"feature_length": len(feats),
|
| 325 |
+
"advantages": "Demonstrates local keypoint/gradient-descriptor ideas without heavy models.",
|
| 326 |
"limitations": "Not a full SIFT/SURF implementation unless OpenCV SIFT is available."
|
| 327 |
}
|
| 328 |
return feats.astype(np.float32), visual, meta
|
| 329 |
|
| 330 |
|
| 331 |
+
def extract_minutiae_like(arr):
|
| 332 |
+
smooth = conv2d_same(arr, np.ones((3, 3), dtype=np.float32) / 9.0)
|
|
|
|
|
|
|
| 333 |
binary = smooth < np.percentile(smooth, 45)
|
|
|
|
|
|
|
| 334 |
binary[:2, :] = False
|
| 335 |
binary[-2:, :] = False
|
| 336 |
binary[:, :2] = False
|
| 337 |
binary[:, -2:] = False
|
| 338 |
|
| 339 |
+
ncount = np.zeros_like(binary, dtype=np.int32)
|
| 340 |
for dy in [-1, 0, 1]:
|
| 341 |
for dx in [-1, 0, 1]:
|
| 342 |
if dy == 0 and dx == 0:
|
| 343 |
continue
|
| 344 |
+
ncount += np.roll(np.roll(binary, dy, axis=0), dx, axis=1).astype(np.int32)
|
|
|
|
|
|
|
|
|
|
| 345 |
|
| 346 |
+
endpoints = binary & (ncount == 1)
|
| 347 |
+
bifurcations = binary & (ncount >= 3)
|
|
|
|
| 348 |
feats = [
|
| 349 |
float(endpoints.sum()) / 1000.0,
|
| 350 |
float(bifurcations.sum()) / 1000.0,
|
| 351 |
float(binary.mean()),
|
| 352 |
+
float(ncount[binary].mean()) if binary.any() else 0.0,
|
| 353 |
]
|
| 354 |
+
grid = 4
|
| 355 |
+
h, w = arr.shape
|
| 356 |
for mask in [endpoints, bifurcations]:
|
| 357 |
for gy in range(grid):
|
| 358 |
for gx in range(grid):
|
|
|
|
| 364 |
draw = ImageDraw.Draw(visual)
|
| 365 |
ey, ex = np.where(endpoints)
|
| 366 |
by, bx = np.where(bifurcations)
|
|
|
|
| 367 |
for y, x in list(zip(ey, ex))[:120]:
|
| 368 |
draw.ellipse((x - 2, y - 2, x + 2, y + 2), outline=(0, 255, 0), width=1)
|
| 369 |
for y, x in list(zip(by, bx))[:120]:
|
|
|
|
| 371 |
|
| 372 |
meta = {
|
| 373 |
"method": "Minutiae-like extraction",
|
| 374 |
+
"feature_type": "educational endpoint and bifurcation approximation",
|
| 375 |
"feature_length": len(feats),
|
| 376 |
+
"advantages": "Visually explains classic fingerprint minutiae concepts.",
|
| 377 |
+
"limitations": "Not a true forensic minutiae extractor; segmentation and thinning are simplified."
|
| 378 |
}
|
| 379 |
+
return np.asarray(feats, dtype=np.float32), visual, meta
|
| 380 |
|
| 381 |
|
| 382 |
+
def extract_cnn_like(arr):
|
| 383 |
+
sobel_x = np.asarray([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=np.float32)
|
|
|
|
|
|
|
| 384 |
sobel_y = sobel_x.T
|
| 385 |
+
gx = conv2d_same(arr, sobel_x)
|
| 386 |
+
gy = conv2d_same(arr, sobel_y)
|
| 387 |
+
edge = normalize01(np.sqrt(gx ** 2 + gy ** 2))
|
|
|
|
| 388 |
feats = []
|
| 389 |
+
h, w = arr.shape
|
| 390 |
for grid in [2, 4, 8]:
|
| 391 |
+
for yy in range(grid):
|
| 392 |
+
for xx in range(grid):
|
| 393 |
+
y0, y1 = yy * h // grid, (yy + 1) * h // grid
|
| 394 |
+
x0, x1 = xx * w // grid, (xx + 1) * w // grid
|
|
|
|
| 395 |
patch = arr[y0:y1, x0:x1]
|
| 396 |
epatch = edge[y0:y1, x0:x1]
|
| 397 |
+
feats.extend([float(patch.mean()), float(patch.std()), float(epatch.mean()), float(epatch.std())])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
feats.extend([
|
| 399 |
+
float(arr.mean()), float(arr.std()), float(edge.mean()), float(edge.std()),
|
| 400 |
+
float(np.percentile(arr, 25)), float(np.percentile(arr, 50)), float(np.percentile(arr, 75))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
])
|
| 402 |
+
feats = unit_vector(np.asarray(feats, dtype=np.float32))
|
|
|
|
|
|
|
| 403 |
meta = {
|
| 404 |
"method": "CNN-like embedding",
|
| 405 |
+
"feature_type": "lightweight multiscale pooled edge and texture embedding",
|
| 406 |
"feature_length": len(feats),
|
| 407 |
+
"advantages": "Demonstrates hierarchical feature pooling on CPU.",
|
| 408 |
"limitations": "Not trained; does not replace a real CNN biometric model."
|
| 409 |
}
|
| 410 |
+
return feats.astype(np.float32), array_to_pil(edge), meta
|
| 411 |
|
| 412 |
|
| 413 |
+
def extract_deep_embedding(arr, modality):
|
| 414 |
+
gabor_vec, _, _ = extract_gabor(arr)
|
|
|
|
|
|
|
| 415 |
lbp_vec, _, _ = extract_lbp(arr)
|
| 416 |
sift_vec, _, _ = extract_sift_like(arr)
|
| 417 |
cnn_vec, cnn_vis, _ = extract_cnn_like(arr)
|
|
|
|
| 418 |
base = np.concatenate([
|
| 419 |
+
pad_or_trim(gabor_vec, 32),
|
| 420 |
+
pad_or_trim(lbp_vec, 128),
|
| 421 |
+
pad_or_trim(sift_vec, 128),
|
| 422 |
+
pad_or_trim(cnn_vec, 128),
|
| 423 |
])
|
| 424 |
+
base = unit_vector(base)
|
| 425 |
+
rng = np.random.default_rng(seed_from_key("deep-" + str(modality)))
|
|
|
|
| 426 |
projection = rng.normal(0, 1, size=(len(base), 128)).astype(np.float32)
|
| 427 |
+
emb = unit_vector(base @ projection)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
meta = {
|
| 429 |
"method": "Deep embedding simulation",
|
| 430 |
+
"feature_type": "deterministic projected multimethod embedding",
|
| 431 |
"feature_length": len(emb),
|
| 432 |
+
"advantages": "Shows the idea of compact embeddings used by FaceNet, ArcFace, and CNN systems.",
|
| 433 |
"limitations": "Educational simulation; not trained on biometric identity labels."
|
| 434 |
}
|
| 435 |
+
return emb.astype(np.float32), cnn_vis, meta
|
| 436 |
|
| 437 |
|
| 438 |
+
def extract_features(img, modality, method):
|
| 439 |
arr, preprocessed, pre_meta = preprocess_modality(img, modality)
|
|
|
|
| 440 |
if method == "Minutiae-like":
|
| 441 |
vec, vis, meta = extract_minutiae_like(arr)
|
| 442 |
elif method == "LBP":
|
|
|
|
| 451 |
vec, vis, meta = extract_deep_embedding(arr, modality)
|
| 452 |
else:
|
| 453 |
vec, vis, meta = extract_gabor(arr)
|
| 454 |
+
return vec.astype(np.float32), preprocessed, vis, {**pre_meta, **meta}
|
|
|
|
|
|
|
| 455 |
|
| 456 |
|
| 457 |
# ---------------------------------------------------------------------
|
| 458 |
# Template protection
|
| 459 |
# ---------------------------------------------------------------------
|
| 460 |
|
| 461 |
+
def fernet_key(secret):
|
| 462 |
+
digest = hashlib.sha256(str(secret or "demo-secret").encode("utf-8")).digest()
|
| 463 |
return base64.urlsafe_b64encode(digest)
|
| 464 |
|
| 465 |
|
| 466 |
+
def encrypted_storage_preview(vec, secret):
|
| 467 |
raw = np.asarray(vec[:64], dtype=np.float32).tobytes()
|
| 468 |
if HAS_CRYPTO:
|
| 469 |
+
token = Fernet(fernet_key(secret)).encrypt(raw)
|
|
|
|
| 470 |
return token[:180].decode("utf-8") + "..."
|
| 471 |
+
digest = hashlib.sha256(raw + str(secret).encode("utf-8")).hexdigest()
|
| 472 |
+
return "cryptography package missing; SHA-256 preview only: " + digest
|
| 473 |
|
| 474 |
|
| 475 |
+
def random_projection(vec, secret, out_dim=128):
|
| 476 |
+
vec = unit_vector(vec)
|
| 477 |
+
rng = np.random.default_rng(seed_from_key(secret))
|
| 478 |
projection = rng.normal(0, 1, size=(len(vec), out_dim)).astype(np.float32)
|
| 479 |
+
return unit_vector(vec @ projection)
|
|
|
|
| 480 |
|
| 481 |
|
| 482 |
+
def biohash(vec, secret, out_dim=128):
|
| 483 |
projected = random_projection(vec, secret, out_dim)
|
| 484 |
return (projected > np.median(projected)).astype(np.float32)
|
| 485 |
|
| 486 |
|
| 487 |
+
def chaotic_mapping(vec, secret):
|
| 488 |
vec = np.asarray(vec, dtype=np.float32).flatten()
|
| 489 |
+
seed = seed_from_key(secret)
|
| 490 |
x = ((seed % 100000) + 1) / 100001.0
|
| 491 |
r = 3.99
|
| 492 |
+
seq = []
|
| 493 |
for _ in range(len(vec)):
|
| 494 |
+
x = r * x * (1.0 - x)
|
| 495 |
+
seq.append(x)
|
| 496 |
+
perm = np.argsort(seq)
|
| 497 |
+
return unit_vector(vec[perm])
|
| 498 |
|
| 499 |
|
| 500 |
+
def fuzzy_bits(vec, secret, out_dim=128):
|
| 501 |
projected = random_projection(vec, secret, out_dim)
|
| 502 |
return (projected > 0).astype(np.float32)
|
| 503 |
|
| 504 |
|
| 505 |
+
def protect_for_matching(vec, method, secret):
|
| 506 |
vec = np.asarray(vec, dtype=np.float32).flatten()
|
|
|
|
| 507 |
if method == "Plain template":
|
| 508 |
+
return unit_vector(vec), "cosine", "Raw normalized template. Fast but unsafe if stolen."
|
|
|
|
| 509 |
if method == "Encrypted storage":
|
| 510 |
+
return unit_vector(vec), "cosine", "Encrypted at rest. Matching uses decrypted vector in this demo."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 511 |
if method == "Cancelable biometric":
|
| 512 |
+
return random_projection(vec, secret), "cosine", "Secret-key random projection. Change key to revoke/reissue template."
|
|
|
|
|
|
|
|
|
|
|
|
|
| 513 |
if method == "BioHashing":
|
| 514 |
+
return biohash(vec, secret), "hamming", "Random projection plus binarization. Comparison uses Hamming similarity."
|
|
|
|
|
|
|
|
|
|
|
|
|
| 515 |
if method == "Chaotic mapping":
|
| 516 |
+
return chaotic_mapping(vec, secret), "cosine", "Logistic-map sequence permutes the template using a key."
|
|
|
|
|
|
|
|
|
|
|
|
|
| 517 |
if method == "Fuzzy extractor simulation":
|
| 518 |
+
return fuzzy_bits(vec, secret), "hamming", "Simulated stable binary helper-data-style output."
|
|
|
|
|
|
|
|
|
|
|
|
|
| 519 |
if method == "Toy homomorphic encryption":
|
| 520 |
+
return unit_vector(vec), "cosine", "Conceptual placeholder. Real homomorphic matching is much more expensive."
|
| 521 |
+
return unit_vector(vec), "cosine", "Default normalized template."
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
|
| 523 |
|
| 524 |
+
def template_preview(vec, method, secret):
|
| 525 |
protected, metric, explanation = protect_for_matching(vec, method, secret)
|
|
|
|
| 526 |
if method == "Encrypted storage":
|
| 527 |
+
preview = encrypted_storage_preview(vec, secret)
|
| 528 |
+
elif method == "Toy homomorphic encryption":
|
| 529 |
+
q = np.round(np.asarray(vec[:16]) * 1000).astype(int)
|
| 530 |
+
preview = "Toy encrypted-integer preview: " + np.array2string(q, separator=", ")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 531 |
else:
|
| 532 |
+
preview = vector_preview(protected, 24)
|
| 533 |
+
info = pd.DataFrame({
|
| 534 |
+
"property": ["protected length", "matching metric", "revocation capability", "explanation"],
|
|
|
|
| 535 |
"value": [
|
| 536 |
len(protected),
|
| 537 |
metric,
|
| 538 |
"Yes" if method in ["Cancelable biometric", "BioHashing", "Chaotic mapping", "Fuzzy extractor simulation"] else "Limited",
|
| 539 |
+
explanation,
|
| 540 |
+
],
|
| 541 |
})
|
| 542 |
+
return preview, info
|
| 543 |
|
| 544 |
|
| 545 |
# ---------------------------------------------------------------------
|
| 546 |
# Liveness and attacks
|
| 547 |
# ---------------------------------------------------------------------
|
| 548 |
|
| 549 |
+
def liveness_metrics(img):
|
| 550 |
arr, _, _ = preprocess_modality(img, "Face")
|
| 551 |
+
lap = conv2d_same(arr, np.asarray([[0, 1, 0], [1, -4, 1], [0, 1, 0]], dtype=np.float32))
|
|
|
|
|
|
|
| 552 |
blur_var = float(lap.var())
|
| 553 |
|
|
|
|
| 554 |
fft = np.fft.fftshift(np.fft.fft2(arr))
|
| 555 |
mag = np.abs(fft)
|
| 556 |
h, w = mag.shape
|
| 557 |
cy, cx = h // 2, w // 2
|
| 558 |
yy, xx = np.ogrid[:h, :w]
|
| 559 |
dist = np.sqrt((yy - cy) ** 2 + (xx - cx) ** 2)
|
| 560 |
+
high_mask = dist > min(h, w) * 0.18
|
| 561 |
high_freq_ratio = float(mag[high_mask].sum() / (mag.sum() + 1e-8))
|
| 562 |
|
| 563 |
lbp_vec, _, _ = extract_lbp(arr)
|
|
|
|
| 566 |
|
| 567 |
contrast = float(arr.std())
|
| 568 |
brightness = float(arr.mean())
|
|
|
|
| 569 |
blur_score = min(1.0, blur_var * 120.0)
|
| 570 |
freq_score = min(1.0, high_freq_ratio * 4.0)
|
| 571 |
contrast_score = min(1.0, contrast * 4.0)
|
|
|
|
| 572 |
overall = 0.30 * blur_score + 0.30 * freq_score + 0.25 * entropy_score + 0.15 * contrast_score
|
| 573 |
|
| 574 |
+
reasons = []
|
| 575 |
if blur_score < 0.18:
|
| 576 |
+
reasons.append("low sharpness")
|
| 577 |
if freq_score < 0.18:
|
| 578 |
+
reasons.append("low high-frequency detail")
|
| 579 |
if contrast < 0.05:
|
| 580 |
+
reasons.append("very low contrast")
|
| 581 |
if brightness < 0.08 or brightness > 0.92:
|
| 582 |
+
reasons.append("extreme brightness")
|
| 583 |
|
| 584 |
return {
|
| 585 |
+
"blur_score": round(float(blur_score), 4),
|
| 586 |
+
"frequency_score": round(float(freq_score), 4),
|
| 587 |
+
"texture_entropy_score": round(float(entropy_score), 4),
|
| 588 |
+
"contrast_score": round(float(contrast_score), 4),
|
| 589 |
+
"brightness": round(float(brightness), 4),
|
| 590 |
"overall_liveness_score": round(float(overall), 4),
|
| 591 |
+
"suspicious_reasons": ", ".join(reasons) if reasons else "none",
|
| 592 |
}
|
| 593 |
|
| 594 |
|
| 595 |
+
def simulate_attack(img, attack, intensity):
|
| 596 |
+
img = safe_image(img)
|
| 597 |
if img is None:
|
| 598 |
raise ValueError("Please upload an image.")
|
| 599 |
intensity = float(intensity)
|
| 600 |
|
| 601 |
if attack == "None":
|
| 602 |
return img
|
|
|
|
| 603 |
if attack == "Blur / out-of-focus":
|
| 604 |
+
return img.filter(ImageFilter.GaussianBlur(radius=0.5 + intensity * 5.0))
|
|
|
|
| 605 |
if attack == "Gaussian noise":
|
| 606 |
arr = np.asarray(img).astype(np.float32)
|
| 607 |
rng = np.random.default_rng(123)
|
| 608 |
noise = rng.normal(0, 8 + intensity * 45, size=arr.shape)
|
| 609 |
+
return Image.fromarray(np.clip(arr + noise, 0, 255).astype(np.uint8))
|
|
|
|
|
|
|
| 610 |
if attack == "Low-contrast print":
|
| 611 |
out = ImageOps.grayscale(img).convert("RGB")
|
| 612 |
out = ImageEnhance.Contrast(out).enhance(max(0.2, 1.0 - intensity * 0.8))
|
| 613 |
out = ImageEnhance.Brightness(out).enhance(0.85 + intensity * 0.15)
|
| 614 |
return out
|
|
|
|
| 615 |
if attack == "Replay-screen scanlines":
|
| 616 |
arr = np.asarray(img).astype(np.float32)
|
| 617 |
step = max(2, int(8 - intensity * 5))
|
| 618 |
arr[::step, :, :] *= 0.55
|
| 619 |
arr[:, ::max(3, step + 1), :] *= 0.85
|
| 620 |
return Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8))
|
|
|
|
| 621 |
if attack == "Deepfake-like smoothing":
|
| 622 |
out = img.filter(ImageFilter.MedianFilter(size=3))
|
| 623 |
out = out.filter(ImageFilter.GaussianBlur(radius=0.5 + intensity * 2.5))
|
| 624 |
out = ImageEnhance.Sharpness(out).enhance(0.5)
|
| 625 |
return out
|
|
|
|
| 626 |
if attack == "Adversarial-style tiny noise":
|
| 627 |
arr = np.asarray(img).astype(np.float32)
|
| 628 |
rng = np.random.default_rng(999)
|
| 629 |
pattern = rng.choice([-1, 1], size=arr.shape) * (2 + intensity * 12)
|
| 630 |
+
return Image.fromarray(np.clip(arr + pattern, 0, 255).astype(np.uint8))
|
|
|
|
|
|
|
| 631 |
return img
|
| 632 |
|
| 633 |
|
| 634 |
# ---------------------------------------------------------------------
|
| 635 |
+
# Gradio callbacks
|
| 636 |
# ---------------------------------------------------------------------
|
| 637 |
|
| 638 |
def run_feature_lab(img, modality, method):
|
| 639 |
if img is None:
|
| 640 |
return None, None, None, pd.DataFrame(), {}, "Upload an image first."
|
|
|
|
| 641 |
try:
|
| 642 |
vec, pre, vis, meta = extract_features(img, modality, method)
|
| 643 |
+
explanation = (
|
| 644 |
+
"### Feature extraction result\n\n"
|
| 645 |
+
f"**Modality:** {modality}\n\n"
|
| 646 |
+
f"**Method:** {meta.get('method')}\n\n"
|
| 647 |
+
f"**Feature type:** {meta.get('feature_type')}\n\n"
|
| 648 |
+
f"**Feature length:** {meta.get('feature_length')}\n\n"
|
| 649 |
+
f"**Advantages:** {meta.get('advantages')}\n\n"
|
| 650 |
+
f"**Limitations:** {meta.get('limitations')}\n\n"
|
| 651 |
+
"This is an educational demonstration. The final report should cite actual paper metrics."
|
| 652 |
+
)
|
| 653 |
+
return pre, vis, feature_plot(vec, f"{method} feature preview"), feature_df(vec), meta, explanation
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
except Exception as e:
|
| 655 |
return None, None, None, pd.DataFrame(), {}, f"Error: {e}"
|
| 656 |
|
| 657 |
|
| 658 |
def run_verification(enroll_img, verify_img, modality, method, protection_method, secret_key, threshold):
|
| 659 |
if enroll_img is None or verify_img is None:
|
| 660 |
+
return "## REJECTED\n\nUpload both enrollment and verification images.", pd.DataFrame(), None, None
|
|
|
|
| 661 |
try:
|
| 662 |
+
e_vec, _, e_vis, _ = extract_features(enroll_img, modality, method)
|
| 663 |
+
v_vec, _, v_vis, _ = extract_features(verify_img, modality, method)
|
| 664 |
+
e_prot, metric, explanation = protect_for_matching(e_vec, protection_method, secret_key)
|
|
|
|
| 665 |
v_prot, _, _ = protect_for_matching(v_vec, protection_method, secret_key)
|
|
|
|
| 666 |
if metric == "hamming":
|
| 667 |
+
similarity = hamming_similarity(e_prot, v_prot)
|
| 668 |
else:
|
| 669 |
+
similarity = cosine_similarity(e_prot, v_prot)
|
| 670 |
|
| 671 |
live = liveness_metrics(verify_img)
|
| 672 |
+
live_score = float(live["overall_liveness_score"])
|
| 673 |
+
is_live = live_score >= 0.35
|
| 674 |
+
accepted = similarity >= float(threshold) and is_live
|
|
|
|
| 675 |
decision = "ACCEPTED" if accepted else "REJECTED"
|
|
|
|
| 676 |
|
| 677 |
+
reasons = []
|
| 678 |
+
if similarity < float(threshold):
|
| 679 |
+
reasons.append("similarity below threshold")
|
| 680 |
if not is_live:
|
| 681 |
+
reasons.append("liveness score suspicious")
|
| 682 |
+
if not reasons:
|
| 683 |
+
reasons.append("similarity and liveness passed")
|
| 684 |
+
|
| 685 |
+
result = (
|
| 686 |
+
f"## {decision}\n\n"
|
| 687 |
+
"| Check | Value |\n"
|
| 688 |
+
"|---|---:|\n"
|
| 689 |
+
f"| Similarity score | **{similarity:.4f}** |\n"
|
| 690 |
+
f"| Threshold | **{float(threshold):.4f}** |\n"
|
| 691 |
+
f"| Matching metric | **{metric}** |\n"
|
| 692 |
+
f"| Liveness score | **{live_score:.4f}** |\n"
|
| 693 |
+
f"| Liveness verdict | **{'Live / acceptable' if is_live else 'Suspicious'}** |\n"
|
| 694 |
+
f"| Reason | **{', '.join(reasons)}** |\n\n"
|
| 695 |
+
f"**Template protection note:** {explanation}\n\n"
|
| 696 |
+
"The demo fails closed: no image or processing failure means no authentication success."
|
| 697 |
+
)
|
| 698 |
+
metrics = pd.DataFrame([
|
| 699 |
+
{"metric": "similarity", "value": round(float(similarity), 4)},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 700 |
{"metric": "threshold", "value": round(float(threshold), 4)},
|
| 701 |
+
{"metric": "liveness_score", "value": live_score},
|
| 702 |
{"metric": "blur_score", "value": live["blur_score"]},
|
| 703 |
{"metric": "frequency_score", "value": live["frequency_score"]},
|
| 704 |
{"metric": "texture_entropy_score", "value": live["texture_entropy_score"]},
|
| 705 |
{"metric": "contrast_score", "value": live["contrast_score"]},
|
| 706 |
+
{"metric": "suspicious_reasons", "value": live["suspicious_reasons"]},
|
| 707 |
])
|
| 708 |
+
return result, metrics, e_vis, v_vis
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 709 |
except Exception as e:
|
| 710 |
return f"## REJECTED\n\nProcessing error: {e}", pd.DataFrame(), None, None
|
| 711 |
|
|
|
|
| 713 |
def run_template_lab(img, modality, feature_method, protection_method, secret_key):
|
| 714 |
if img is None:
|
| 715 |
return "Upload an image first.", pd.DataFrame(), pd.DataFrame(), None
|
| 716 |
+
try:
|
| 717 |
+
vec, _, vis, _ = extract_features(img, modality, feature_method)
|
| 718 |
+
preview, info = template_preview(vec, protection_method, secret_key)
|
| 719 |
+
raw = pd.DataFrame({"index": list(range(min(32, len(vec)))), "raw_feature_value": [float(x) for x in vec[:32]]})
|
| 720 |
+
md = (
|
| 721 |
+
"## Template protection preview\n\n"
|
| 722 |
+
f"**Feature method:** {feature_method}\n\n"
|
| 723 |
+
f"**Protection method:** {protection_method}\n\n"
|
| 724 |
+
f"**Raw feature length:** {len(vec)}\n\n"
|
| 725 |
+
"### Protected / stored preview\n\n"
|
| 726 |
+
f"`{preview}`\n\n"
|
| 727 |
+
"### Key concept\n\n"
|
| 728 |
+
"Encryption protects storage. Cancelable biometrics and BioHashing make templates revocable by changing the secret key. "
|
| 729 |
+
"Fuzzy extractors aim to generate stable keys from noisy biometric samples. Homomorphic encryption is conceptually powerful but computationally expensive."
|
| 730 |
+
)
|
| 731 |
+
return md, raw, info, vis
|
| 732 |
+
except Exception as e:
|
| 733 |
+
return f"Error: {e}", pd.DataFrame(), pd.DataFrame(), None
|
| 734 |
+
|
| 735 |
|
| 736 |
+
def run_attack_lab(img, attack, intensity):
|
| 737 |
+
if img is None:
|
| 738 |
+
return None, pd.DataFrame(), "Upload an image first."
|
| 739 |
try:
|
| 740 |
+
attacked = simulate_attack(img, attack, intensity)
|
| 741 |
+
metrics = liveness_metrics(attacked)
|
| 742 |
+
verdict = "Live / acceptable" if metrics["overall_liveness_score"] >= 0.35 else "Suspicious / possible spoof"
|
| 743 |
+
df = pd.DataFrame([{"metric": k, "value": v} for k, v in metrics.items()])
|
| 744 |
+
md = (
|
| 745 |
+
f"## {verdict}\n\n"
|
| 746 |
+
f"**Attack simulation:** {attack}\n\n"
|
| 747 |
+
f"**Intensity:** {float(intensity):.2f}\n\n"
|
| 748 |
+
"This demonstrates basic liveness/PAD ideas using blur, texture, contrast, and frequency cues. "
|
| 749 |
+
"It is not a production anti-spoofing detector."
|
| 750 |
+
)
|
| 751 |
+
return attacked, df, md
|
| 752 |
+
except Exception as e:
|
| 753 |
+
return None, pd.DataFrame(), f"Error: {e}"
|
| 754 |
+
|
| 755 |
+
|
| 756 |
+
# ---------------------------------------------------------------------
|
| 757 |
+
# Tables and static content
|
| 758 |
+
# ---------------------------------------------------------------------
|
| 759 |
+
|
| 760 |
+
def model_comparison_table():
|
| 761 |
+
rows = [
|
| 762 |
+
["Shallow CNN", "Small convolution + pooling stack", "0.1M-2M", "Low", "Medium", "Fast on CPU", "Good", "May underfit complex variations"],
|
| 763 |
+
["ResNet", "Residual CNN blocks", "11M+ for ResNet-18", "Medium/high", "High with data", "Medium", "Moderate", "Heavier than MobileNet"],
|
| 764 |
+
["MobileNet", "Depthwise separable CNN", "3M-5M", "Low", "Good", "Fast", "Excellent", "May lose accuracy on difficult data"],
|
| 765 |
+
["Vision Transformer", "Patch tokens + self-attention", "High", "High", "High with large data", "Slow on CPU", "Weak/moderate", "Data hungry and heavy"],
|
| 766 |
+
["Autoencoder", "Encoder learns compressed representation", "Variable", "Medium", "Task-dependent", "Medium", "Moderate", "Embedding may not be discriminative"],
|
| 767 |
+
["FaceNet / ArcFace-style", "Metric-learning embedding", "Medium/high", "Medium/high", "Very strong for face", "Medium", "Depends on backbone", "Needs threshold and liveness checks"],
|
| 768 |
+
]
|
| 769 |
+
cols = ["Model", "Architecture idea", "Approx. params", "Approx. FLOPs", "Accuracy tendency", "Inference time", "Edge suitability", "Limitation"]
|
| 770 |
+
return pd.DataFrame(rows, columns=cols)
|
| 771 |
+
|
| 772 |
+
|
| 773 |
+
def model_notes(selected):
|
| 774 |
+
notes = {
|
| 775 |
+
"Shallow CNN": "Useful for a student demo. Low complexity but limited robustness.",
|
| 776 |
+
"ResNet": "Good baseline for fingerprint or face feature learning. Residual connections help deeper CNN training.",
|
| 777 |
+
"MobileNet": "Best example for edge deployment because it is designed for efficient inference.",
|
| 778 |
+
"Vision Transformer": "Useful for modern attention-based model discussion, but heavy for free CPU deployment.",
|
| 779 |
+
"Autoencoder": "Useful for representation learning or anomaly detection, but not automatically strong for identity verification.",
|
| 780 |
+
"FaceNet / ArcFace-style": "Best conceptual model for verification: extract embedding, compare with cosine similarity, tune threshold."
|
| 781 |
+
}
|
| 782 |
+
return f"### {selected}\n\n{notes.get(selected, 'Select a model.')}"
|
| 783 |
+
|
| 784 |
+
|
| 785 |
+
def survey_table(topic):
|
| 786 |
+
if topic == "Student 1 - Feature Extraction":
|
| 787 |
+
rows = [
|
| 788 |
+
["Hong, Wan & Jain, 1998", "Fingerprint", "Gabor/ridge enhancement", "Fingerprint images", "Enhancement/matching improvement", "Improves ridge clarity", "Parameter-sensitive"],
|
| 789 |
+
["Jain, Prabhakar & Hong, 1999", "Fingerprint", "Filterbank features", "Fingerprint databases", "Recognition/matching rate", "Strong handcrafted baseline", "Needs alignment"],
|
| 790 |
+
["Maio & Maltoni, 1997", "Fingerprint", "Minutiae extraction", "Fingerprint images", "Minutiae accuracy", "Classic approach", "False minutiae in poor images"],
|
| 791 |
+
["Ratha et al., 1996", "Fingerprint", "Ridge flow + minutiae", "Fingerprint images", "Verification metrics", "End-to-end pipeline", "Segmentation-sensitive"],
|
| 792 |
+
["Ojala et al., 2002", "Texture", "LBP", "Texture datasets", "Classification rate", "Fast descriptor", "Weak global structure"],
|
| 793 |
+
["Ahonen et al., 2006", "Face", "LBP face descriptor", "Face datasets", "Recognition rate", "Simple/interpretable", "Pose and illumination issues"],
|
| 794 |
+
["Lowe, 2004", "General vision", "SIFT", "Image datasets", "Keypoint matching", "Scale/rotation robust", "Sparse on some biometrics"],
|
| 795 |
+
["Bay et al., 2008", "General vision", "SURF", "Image datasets", "Speed/matching", "Faster than SIFT", "Less common in modern biometrics"],
|
| 796 |
+
["Daugman, 1993", "Iris", "Gabor iris code", "Iris images", "False match rates", "Foundational iris method", "Needs accurate segmentation"],
|
| 797 |
+
["Wildes, 1997", "Iris", "Iris texture matching", "Iris images", "Recognition performance", "Strong iris pipeline", "Controlled imaging needed"],
|
| 798 |
+
["Masek & Kovesi, 2003", "Iris", "Segmentation + encoding", "CASIA-style iris data", "Recognition metrics", "Useful baseline", "Older pipeline"],
|
| 799 |
+
["Schroff et al., 2015", "Face", "FaceNet embedding", "Large face data", "Verification accuracy", "Strong deep embedding", "Needs large training data"],
|
| 800 |
+
["Deng et al., 2019", "Face", "ArcFace embedding", "Face datasets", "Verification accuracy", "Discriminative loss", "Heavy training"],
|
| 801 |
+
["CNN iris studies", "Iris", "CNN features", "Iris datasets", "Accuracy/EER", "Learns features", "Dataset bias risk"],
|
| 802 |
+
["DeepPrint-style work", "Fingerprint", "Deep embedding", "Fingerprint datasets", "Verification accuracy", "Robust representation", "Needs careful evaluation"],
|
| 803 |
+
]
|
| 804 |
+
cols = ["Paper", "Modality", "Method", "Dataset", "Accuracy / metric", "Advantages", "Limitations"]
|
| 805 |
+
return pd.DataFrame(rows, columns=cols)
|
| 806 |
+
|
| 807 |
+
if topic == "Student 2 - Template Protection":
|
| 808 |
+
rows = [
|
| 809 |
+
["Ratha et al., 2001", "Cancelable biometrics", "Non-invertible transform", "Medium", "Medium", "Low/medium", "Yes"],
|
| 810 |
+
["Teoh et al., 2004", "BioHashing", "Random projection + binarization", "Medium/high", "Medium", "Low", "Yes"],
|
| 811 |
+
["Juels & Wattenberg, 1999", "Fuzzy commitment", "Bind key with noisy biometric", "High", "Medium", "Medium", "Possible"],
|
| 812 |
+
["Juels & Sudan, 2002", "Fuzzy vault", "Hide secret among chaff points", "High", "Medium", "Medium/high", "Possible"],
|
| 813 |
+
["Dodis et al., 2004", "Fuzzy extractor", "Stable key from noisy input", "High", "Medium", "Medium", "Yes"],
|
| 814 |
+
["Clancy et al., 2003", "Fingerprint vault", "Minutiae cryptosystem", "High", "Medium", "Medium/high", "Possible"],
|
| 815 |
+
["Uludag et al., 2004", "Biometric cryptosystem", "Key binding/generation", "High", "Medium", "Medium", "Depends"],
|
| 816 |
+
["Nandakumar et al., 2007", "Fingerprint fuzzy vault", "Vault for minutiae", "High", "Medium", "Medium/high", "Yes"],
|
| 817 |
+
["Jain, Nandakumar & Nagar, 2008", "Survey", "Template security comparison", "N/A", "N/A", "N/A", "N/A"],
|
| 818 |
+
["Nagar et al., 2010", "Multibiometric cryptosystem", "Fusion + protection", "High", "High", "High", "Possible"],
|
| 819 |
+
["Rathgeb & Uhl, 2011", "Survey", "Protection taxonomy", "N/A", "N/A", "N/A", "N/A"],
|
| 820 |
+
["Gomez-Barrero et al., 2017", "Evaluation", "Unlinkability/reversibility", "High", "Medium", "Medium", "Yes"],
|
| 821 |
+
["Chaotic map approaches", "Chaotic mapping", "Permutation/substitution", "Medium", "Medium", "Low/medium", "Yes"],
|
| 822 |
+
["ECC-based approaches", "Error correction", "Correct biometric noise", "High", "Medium", "Medium", "Possible"],
|
| 823 |
+
["Homomorphic matching", "Homomorphic encryption", "Compute on encrypted template", "Very high", "High", "High", "Yes"],
|
| 824 |
+
]
|
| 825 |
+
cols = ["Paper", "Technique", "Core idea", "Security", "Complexity", "Computational cost", "Template revocation"]
|
| 826 |
+
return pd.DataFrame(rows, columns=cols)
|
| 827 |
+
|
| 828 |
+
if topic == "Student 3 - Deep Learning":
|
| 829 |
+
rows = [
|
| 830 |
+
["DeepFace, 2014", "Deep CNN", "Face", "High", "High", "High", "Medium/slow"],
|
| 831 |
+
["DeepID, 2014", "CNN embedding", "Face", "Medium/high", "High", "High", "Medium"],
|
| 832 |
+
["VGGFace, 2015", "VGG-style CNN", "Face", "High", "High", "High", "Slow"],
|
| 833 |
+
["FaceNet, 2015", "Triplet-loss embedding", "Face", "High", "Very high", "Very high", "Medium"],
|
| 834 |
+
["SphereFace, 2017", "Angular-margin loss", "Face", "High", "Very high", "High", "Medium"],
|
| 835 |
+
["CosFace, 2018", "Cosine-margin loss", "Face", "High", "Very high", "High", "Medium"],
|
| 836 |
+
["ArcFace, 2019", "Additive angular margin", "Face", "High", "Very high", "High", "Medium"],
|
| 837 |
+
["MobileFaceNets, 2018", "Mobile CNN", "Face", "Low/medium", "High", "Low", "Fast"],
|
| 838 |
+
["FingerNet-style work", "CNN", "Fingerprint", "Medium", "Good", "Medium", "Medium"],
|
| 839 |
+
["DeepPrint-style work", "Deep embedding", "Fingerprint", "Medium/high", "High", "Medium/high", "Medium"],
|
| 840 |
+
["Iris CNN studies", "CNN", "Iris", "Medium", "Good/high", "Medium", "Medium"],
|
| 841 |
+
["Autoencoder biometric work", "Autoencoder", "Multiple", "Variable", "Task-dependent", "Medium", "Medium"],
|
| 842 |
+
["Vision Transformer, 2020", "ViT", "Adapted biometrics", "High", "High with data", "High", "Slow on CPU"],
|
| 843 |
+
["Swin Transformer", "Hierarchical ViT", "Face/iris", "High", "High", "High", "Medium/slow"],
|
| 844 |
+
["MobileNet biometric work", "Efficient CNN", "Face/fingerprint", "Low", "Good", "Low", "Fast"],
|
| 845 |
+
]
|
| 846 |
+
cols = ["Paper/model", "Architecture", "Modality", "Parameters", "Accuracy tendency", "FLOPs", "Inference time"]
|
| 847 |
+
return pd.DataFrame(rows, columns=cols)
|
| 848 |
+
|
| 849 |
+
rows = [
|
| 850 |
+
["Printed photo attack", "Presentation attack", "Face/fingerprint", "False acceptance", "Texture/liveness/challenge-response"],
|
| 851 |
+
["Replay-screen attack", "Presentation attack", "Face", "Bypass camera login", "Screen artifact detection/challenge-response"],
|
| 852 |
+
["Silicone fingerprint", "Presentation attack", "Fingerprint", "Fake finger accepted", "Perspiration/pulse/texture PAD"],
|
| 853 |
+
["Deepfake face", "Synthetic attack", "Face", "Video impersonation", "Deepfake detection + active challenge"],
|
| 854 |
+
["Adversarial perturbation", "Model attack", "Any deep model", "Model misclassification", "Adversarial training"],
|
| 855 |
+
["Template inversion", "Template attack", "Stored embeddings", "Recover biometric information", "Cancelable templates/encryption"],
|
| 856 |
+
["Hill-climbing attack", "Matcher attack", "Score-based systems", "Score optimization", "Limit score leakage/rate limiting"],
|
| 857 |
+
["Replay of stored template", "Database attack", "Template storage", "Identity compromise", "Template protection/key binding"],
|
| 858 |
+
["Texture PAD studies", "Anti-spoofing", "Face", "Photo attack detection", "LBP/texture features"],
|
| 859 |
+
["Replay-Attack dataset studies", "Dataset/PAD", "Face", "Replay/photo detection", "Standardized PAD evaluation"],
|
| 860 |
+
["CASIA-FASD studies", "Dataset/PAD", "Face", "Video/photo attack detection", "Motion/texture cues"],
|
| 861 |
+
["LivDet studies", "Fingerprint PAD", "Fingerprint", "Fake fingerprint detection", "Benchmark anti-spoofing"],
|
| 862 |
+
["Depth-based PAD", "Anti-spoofing", "Face", "Flat photo rejection", "Depth camera / 3D cues"],
|
| 863 |
+
["rPPG liveness", "Anti-spoofing", "Face", "Detect pulse signal", "Needs video and lighting quality"],
|
| 864 |
+
["Multimodal PAD", "Defense", "Multiple", "Improved robustness", "Higher cost and complexity"],
|
| 865 |
+
]
|
| 866 |
+
cols = ["Paper / attack", "Category", "Modality", "Risk", "Defense"]
|
| 867 |
+
return pd.DataFrame(rows, columns=cols)
|
| 868 |
+
|
| 869 |
+
|
| 870 |
+
def survey_notes(topic):
|
| 871 |
+
return (
|
| 872 |
+
f"### {topic}\n\n"
|
| 873 |
+
"This is a starter comparison matrix for the literature survey. "
|
| 874 |
+
"Before final submission, replace qualitative entries with exact metrics from your selected papers: "
|
| 875 |
+
"dataset, accuracy/EER/FAR/FRR/APCER/BPCER, computational cost, advantages, and limitations."
|
| 876 |
+
)
|
| 877 |
+
|
| 878 |
|
| 879 |
+
def update_survey(topic):
|
| 880 |
+
return survey_notes(topic), survey_table(topic)
|
|
|
|
|
|
|
| 881 |
|
|
|
|
|
|
|
| 882 |
|
| 883 |
+
# ---------------------------------------------------------------------
|
| 884 |
+
# Gradio UI
|
| 885 |
+
# ---------------------------------------------------------------------
|
| 886 |
+
|
| 887 |
+
CSS = """
|
| 888 |
+
.gradio-container { max-width: 1200px !important; }
|
| 889 |
+
"""
|
| 890 |
|
| 891 |
+
with gr.Blocks(title=APP_TITLE, css=CSS) as demo:
|
| 892 |
+
gr.Markdown(
|
| 893 |
+
"# " + APP_TITLE + "\n\n"
|
| 894 |
+
"This is a professor-facing educational demo for a biometric authentication literature-survey project.\n\n"
|
| 895 |
+
"It demonstrates feature extraction, template protection, deep-learning trade-offs, verification, attacks, liveness, and survey tables.\n\n"
|
| 896 |
+
"**Security note:** This is not a production biometric login system. It stores no permanent biometric database."
|
| 897 |
+
)
|
| 898 |
+
|
| 899 |
+
with gr.Tab("1. Project Overview"):
|
| 900 |
+
gr.Markdown(
|
| 901 |
+
"## Biometric authentication pipeline\n\n"
|
| 902 |
+
"Biometric input -> preprocessing -> feature extraction -> template generation -> template protection -> matching -> liveness check -> accept/reject\n\n"
|
| 903 |
+
"## Student-wise mapping\n\n"
|
| 904 |
+
"| Student | Assignment area | App tabs |\n"
|
| 905 |
+
"|---|---|---|\n"
|
| 906 |
+
"| Student 1 | Feature extraction | Feature Extraction Lab |\n"
|
| 907 |
+
"| Student 2 | Template protection | Template Protection Lab |\n"
|
| 908 |
+
"| Student 3 | Deep learning | Deep Model Comparison |\n"
|
| 909 |
+
"| Student 4 | Attacks and liveness | Attacks & Liveness |\n\n"
|
| 910 |
+
"The app is designed to fail closed. It does not return fake authentication success if real processing fails."
|
| 911 |
+
)
|
| 912 |
+
|
| 913 |
+
with gr.Tab("2. Feature Extraction Lab"):
|
| 914 |
+
with gr.Row():
|
| 915 |
+
with gr.Column():
|
| 916 |
+
feat_img = gr.Image(label="Upload biometric image", type="pil")
|
| 917 |
+
feat_modality = gr.Dropdown(["Fingerprint", "Iris", "Face"], value="Fingerprint", label="Biometric modality")
|
| 918 |
+
feat_method = gr.Dropdown(["Minutiae-like", "LBP", "Gabor", "SIFT/SURF-like", "CNN-like", "Deep embedding"], value="Gabor", label="Feature extraction method")
|
| 919 |
+
feat_btn = gr.Button("Extract features")
|
| 920 |
+
with gr.Column():
|
| 921 |
+
feat_pre = gr.Image(label="Preprocessed image")
|
| 922 |
+
feat_vis = gr.Image(label="Feature visualization")
|
| 923 |
+
feat_plot_out = gr.Plot(label="Feature vector plot")
|
| 924 |
+
feat_df_out = gr.Dataframe(label="Feature vector preview")
|
| 925 |
+
feat_json_out = gr.JSON(label="Method metadata")
|
| 926 |
+
feat_md_out = gr.Markdown()
|
| 927 |
+
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])
|
| 928 |
+
|
| 929 |
+
with gr.Tab("3. Enrollment & Verification Demo"):
|
| 930 |
+
gr.Markdown(
|
| 931 |
+
"Upload one image as the enrolled template and another image as the verification attempt. "
|
| 932 |
+
"The app extracts features from both, applies the selected template protection transform, then compares similarity."
|
| 933 |
+
)
|
| 934 |
+
with gr.Row():
|
| 935 |
+
enroll_img = gr.Image(label="Enrollment image", type="pil")
|
| 936 |
+
verify_img = gr.Image(label="Verification image", type="pil")
|
| 937 |
+
with gr.Row():
|
| 938 |
+
verify_modality = gr.Dropdown(["Fingerprint", "Iris", "Face"], value="Fingerprint", label="Modality")
|
| 939 |
+
verify_method = gr.Dropdown(["Minutiae-like", "LBP", "Gabor", "SIFT/SURF-like", "CNN-like", "Deep embedding"], value="Gabor", label="Feature method")
|
| 940 |
+
with gr.Row():
|
| 941 |
+
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")
|
| 942 |
+
secret_key = gr.Textbox(value="student-demo-key", label="Secret key / transform key")
|
| 943 |
+
threshold = gr.Slider(0.0, 1.0, value=0.75, step=0.01, label="Decision threshold")
|
| 944 |
+
verify_btn = gr.Button("Run verification")
|
| 945 |
+
verify_result = gr.Markdown()
|
| 946 |
+
verify_metrics = gr.Dataframe(label="Decision metrics")
|
| 947 |
+
with gr.Row():
|
| 948 |
+
enroll_feat_vis = gr.Image(label="Enrollment feature visualization")
|
| 949 |
+
verify_feat_vis = gr.Image(label="Verification feature visualization")
|
| 950 |
+
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])
|
| 951 |
+
|
| 952 |
+
with gr.Tab("4. Template Protection Lab"):
|
| 953 |
+
with gr.Row():
|
| 954 |
+
with gr.Column():
|
| 955 |
+
tpl_img = gr.Image(label="Upload biometric image", type="pil")
|
| 956 |
+
tpl_modality = gr.Dropdown(["Fingerprint", "Iris", "Face"], value="Fingerprint", label="Modality")
|
| 957 |
+
tpl_feature = gr.Dropdown(["Minutiae-like", "LBP", "Gabor", "SIFT/SURF-like", "CNN-like", "Deep embedding"], value="Deep embedding", label="Feature method")
|
| 958 |
+
tpl_protection = gr.Dropdown(["Plain template", "Encrypted storage", "Cancelable biometric", "BioHashing", "Chaotic mapping", "Fuzzy extractor simulation", "Toy homomorphic encryption"], value="BioHashing", label="Protection method")
|
| 959 |
+
tpl_secret = gr.Textbox(value="student-demo-key", label="Secret key")
|
| 960 |
+
tpl_btn = gr.Button("Generate protected template")
|
| 961 |
+
with gr.Column():
|
| 962 |
+
tpl_vis = gr.Image(label="Feature visualization")
|
| 963 |
+
tpl_md = gr.Markdown()
|
| 964 |
+
tpl_raw_df = gr.Dataframe(label="Raw feature preview")
|
| 965 |
+
tpl_info_df = gr.Dataframe(label="Protection properties")
|
| 966 |
+
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])
|
| 967 |
+
|
| 968 |
+
with gr.Tab("5. Deep Model Comparison"):
|
| 969 |
+
gr.Markdown(
|
| 970 |
+
"This tab supports Student 3's literature review. It compares CNN, ResNet, MobileNet, Vision Transformer, Autoencoder, and FaceNet/ArcFace-style embeddings."
|
| 971 |
+
)
|
| 972 |
+
gr.Dataframe(value=model_comparison_table(), label="Deep learning model comparison")
|
| 973 |
+
selected_model = gr.Dropdown(["Shallow CNN", "ResNet", "MobileNet", "Vision Transformer", "Autoencoder", "FaceNet / ArcFace-style"], value="MobileNet", label="Select model")
|
| 974 |
+
model_md = gr.Markdown(value=model_notes("MobileNet"))
|
| 975 |
+
selected_model.change(model_notes, [selected_model], [model_md])
|
| 976 |
+
|
| 977 |
+
with gr.Tab("6. Attacks & Liveness"):
|
| 978 |
+
gr.Markdown(
|
| 979 |
+
"This tab supports Student 4's survey on attacks and anti-spoofing. "
|
| 980 |
+
"It simulates common input attacks and estimates a basic liveness score."
|
| 981 |
+
)
|
| 982 |
+
with gr.Row():
|
| 983 |
+
with gr.Column():
|
| 984 |
+
attack_img = gr.Image(label="Upload image", type="pil")
|
| 985 |
+
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")
|
| 986 |
+
attack_intensity = gr.Slider(0.0, 1.0, value=0.5, step=0.05, label="Attack intensity")
|
| 987 |
+
attack_btn = gr.Button("Simulate attack + check liveness")
|
| 988 |
+
with gr.Column():
|
| 989 |
+
attacked_img = gr.Image(label="Attacked / modified image")
|
| 990 |
+
attack_md = gr.Markdown()
|
| 991 |
+
attack_df = gr.Dataframe(label="Liveness metrics")
|
| 992 |
+
attack_btn.click(run_attack_lab, [attack_img, attack_type, attack_intensity], [attacked_img, attack_df, attack_md])
|
| 993 |
+
gr.Markdown(
|
| 994 |
+
"## Attack-defense taxonomy\n\n"
|
| 995 |
+
"| Attack | Description | Typical defense |\n"
|
| 996 |
+
"|---|---|---|\n"
|
| 997 |
+
"| Presentation attack | Fake biometric shown to sensor | Liveness / PAD |\n"
|
| 998 |
+
"| Replay attack | Photo or video on screen | Challenge-response |\n"
|
| 999 |
+
"| Deepfake attack | Synthetic face/video | Deepfake detector + temporal cues |\n"
|
| 1000 |
+
"| Adversarial attack | Small perturbation fools model | Robust training |\n"
|
| 1001 |
+
"| Template attack | Stored template stolen | Cancelable biometrics + encryption |"
|
| 1002 |
+
)
|
| 1003 |
+
|
| 1004 |
+
with gr.Tab("7. Literature Survey Tables"):
|
| 1005 |
+
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")
|
| 1006 |
+
survey_md = gr.Markdown(value=survey_notes("Student 1 - Feature Extraction"))
|
| 1007 |
+
survey_df = gr.Dataframe(value=survey_table("Student 1 - Feature Extraction"), label="Survey comparison table")
|
| 1008 |
+
survey_topic.change(update_survey, [survey_topic], [survey_md, survey_df])
|
| 1009 |
+
|
| 1010 |
+
with gr.Tab("8. Viva / Explanation Script"):
|
| 1011 |
+
gr.Markdown(
|
| 1012 |
+
"## 2-minute explanation for professor\n\n"
|
| 1013 |
+
"Our project is a literature-survey-based biometric authentication demo. The biometric pipeline starts with image acquisition. "
|
| 1014 |
+
"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"
|
| 1015 |
+
"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"
|
| 1016 |
+
"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"
|
| 1017 |
+
"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"
|
| 1018 |
+
"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"
|
| 1019 |
+
"This is an educational demonstration, not a production security system. Its purpose is to connect the literature survey with visible working examples."
|
| 1020 |
+
)
|
| 1021 |
|
| 1022 |
+
if __name__ == "__main__":
|
| 1023 |
+
demo.launch()
|