PhoenixBomb commited on
Commit
1aa6fe7
·
verified ·
1 Parent(s): 3612559

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +545 -436
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, List, Tuple
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
- # Utility helpers
54
  # ---------------------------------------------------------------------
55
 
56
- def _safe_image(img):
57
  if img is None:
58
  return None
59
  if isinstance(img, Image.Image):
60
  return img.convert("RGB")
61
- return Image.fromarray(np.array(img)).convert("RGB")
62
 
63
 
64
- def _array_to_pil(arr: np.ndarray) -> Image.Image:
65
- arr = np.asarray(arr)
66
  arr = np.nan_to_num(arr)
67
- if arr.max() <= 1.0:
 
 
68
  arr = arr * 255.0
69
  arr = np.clip(arr, 0, 255).astype(np.uint8)
70
  return Image.fromarray(arr)
71
 
72
 
73
- def _normalize01(arr: np.ndarray) -> np.ndarray:
74
  arr = np.asarray(arr, dtype=np.float32)
75
- mn, mx = float(arr.min()), float(arr.max())
 
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 _seed_from_key(key: str) -> int:
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 _resize_gray(img: Image.Image, size: int = DEFAULT_SIZE) -> np.ndarray:
88
- img = _safe_image(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 _pad_or_trim(vec: np.ndarray, length: int) -> np.ndarray:
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 _cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
 
 
 
 
 
 
 
 
 
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 = _unit_vector(a[:n])
122
- b = _unit_vector(b[:n])
123
- score = float(np.dot(a, b))
124
- return max(0.0, min(1.0, (score + 1.0) / 2.0))
125
 
126
 
127
- def _hamming_similarity(a: np.ndarray, b: np.ndarray) -> float:
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 _vector_preview(vec: np.ndarray, limit: int = 16) -> str:
137
  vec = np.asarray(vec).flatten()
138
- shown = vec[:limit]
139
- return np.array2string(shown, precision=4, separator=", ")
140
 
141
 
142
- def _make_feature_dataframe(vec: np.ndarray, limit: int = 32) -> pd.DataFrame:
143
  vec = np.asarray(vec).flatten()
144
- rows = []
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 _fig_feature_bar(vec: np.ndarray, title: str = "Feature vector preview"):
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
- # Preprocessing
164
  # ---------------------------------------------------------------------
165
 
166
- def preprocess_modality(img: Image.Image, modality: str) -> Tuple[np.ndarray, Image.Image, Dict]:
167
- img = _safe_image(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
- center = (DEFAULT_SIZE - 1) / 2
186
- radius_outer = DEFAULT_SIZE * 0.46
187
- radius_inner = DEFAULT_SIZE * 0.12
188
- dist = np.sqrt((xx - center) ** 2 + (yy - center) ** 2)
189
- mask = (dist <= radius_outer) & (dist >= radius_inner)
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 clinical iris segmenter."
197
  }
198
- return masked, _array_to_pil(masked), meta
199
 
200
  if modality == "Fingerprint":
201
- gray = _resize_gray(img)
202
- # Increase ridge visibility.
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
- # Face / generic biometric image.
214
- gray = _resize_gray(img)
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 methods
227
  # ---------------------------------------------------------------------
228
 
229
- def _conv2d_same(img: np.ndarray, kernel: np.ndarray) -> np.ndarray:
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
- gb = np.exp(-(x_theta ** 2 + gamma ** 2 * y_theta ** 2) / (2 * sigma ** 2))
254
- gb *= np.cos(2 * np.pi * frequency * x_theta)
255
- gb -= gb.mean()
256
- return gb.astype(np.float32)
257
 
258
 
259
- def extract_gabor(arr: np.ndarray) -> Tuple[np.ndarray, Image.Image, Dict]:
260
  orientations = [0, np.pi / 4, np.pi / 2, 3 * np.pi / 4]
261
  responses = []
262
- features = []
263
-
264
  for theta in orientations:
265
- kernel = gabor_kernel(theta=theta)
266
- response = _conv2d_same(arr, kernel)
267
- responses.append(response)
268
- abs_resp = np.abs(response)
269
- features.extend([
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": "Handcrafted texture/ridge-frequency features",
282
- "feature_length": len(features),
283
- "advantages": "Good for ridge and iris texture enhancement; interpretable.",
284
- "limitations": "Sensitive to segmentation quality, rotation, scale, and chosen filter parameters."
285
  }
286
- return np.array(features, dtype=np.float32), _array_to_pil(visual), meta
287
 
288
 
289
- def extract_lbp(arr: np.ndarray) -> Tuple[np.ndarray, Image.Image, Dict]:
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": "Handcrafted local texture histogram",
312
  "feature_length": len(hist),
313
- "advantages": "Fast, simple, strong texture descriptor.",
314
- "limitations": "Can be sensitive to noise and does not model global structure well."
315
  }
316
- return hist.astype(np.float32), _array_to_pil(visual), meta
317
 
318
 
319
- def extract_sift_like(arr: np.ndarray) -> Tuple[np.ndarray, Image.Image, Dict]:
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": "Keypoint descriptor",
343
  "feature_length": len(desc),
344
- "advantages": "Robust to scale/rotation changes when keypoints are stable.",
345
- "limitations": "Can fail on low-texture or poor-quality biometric images."
346
  }
347
- return desc.astype(np.float32), visual, meta
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 * ch, (cy + 1) * ch
364
- x0, x1 = cx * cw, (cx + 1) * cw
365
- cell_ori = ori[y0:y1, x0:x1].flatten()
366
- cell_mag = mag[y0:y1, x0:x1].flatten()
367
- hist, _ = np.histogram(cell_ori, bins=bins, range=(0, 1), weights=cell_mag)
 
 
 
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
- # Draw top gradient points as pseudo-keypoints.
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), fill=(255, 0, 0))
379
-
380
  meta = {
381
- "method": "SIFT-like fallback",
382
- "feature_type": "Educational gradient keypoint/orientation descriptor",
383
  "feature_length": len(feats),
384
- "advantages": "Demonstrates SIFT/SURF idea without heavy dependencies.",
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: np.ndarray) -> Tuple[np.ndarray, Image.Image, Dict]:
391
- # Educational fingerprint minutiae approximation:
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
- neighbor_count = np.zeros_like(binary, dtype=np.int32)
403
  for dy in [-1, 0, 1]:
404
  for dx in [-1, 0, 1]:
405
  if dy == 0 and dx == 0:
406
  continue
407
- neighbor_count += np.roll(np.roll(binary, dy, axis=0), dx, axis=1).astype(np.int32)
408
-
409
- endpoints = binary & (neighbor_count == 1)
410
- bifurcations = binary & (neighbor_count >= 3)
411
 
412
- # Spatial histograms.
413
- grid = 4
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(neighbor_count[binary].mean()) if binary.any() else 0.0,
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": "Educational ridge endpoint/bifurcation approximation",
442
  "feature_length": len(feats),
443
- "advantages": "Explains classic fingerprint minutiae concepts visually.",
444
- "limitations": "Not a true skeletonization-based forensic minutiae extractor."
445
  }
446
- return np.array(feats, dtype=np.float32), visual, meta
447
 
448
 
449
- def extract_cnn_like(arr: np.ndarray) -> Tuple[np.ndarray, Image.Image, Dict]:
450
- # Lightweight CNN-style embedding simulation:
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 = _conv2d_same(arr, sobel_x)
455
- gy = _conv2d_same(arr, sobel_y)
456
- edge = _normalize01(np.sqrt(gx ** 2 + gy ** 2))
457
-
458
  feats = []
 
459
  for grid in [2, 4, 8]:
460
- h, w = arr.shape
461
- for y in range(grid):
462
- for x in range(grid):
463
- y0, y1 = y * h // grid, (y + 1) * h // grid
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.std()),
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": "Lightweight multiscale pooled edge/texture embedding",
490
  "feature_length": len(feats),
491
- "advantages": "Demonstrates deep-model-style hierarchical feature pooling on CPU.",
492
  "limitations": "Not trained; does not replace a real CNN biometric model."
493
  }
494
- return feats, _array_to_pil(edge), meta
495
 
496
 
497
- def extract_deep_embedding(arr: np.ndarray, modality: str) -> Tuple[np.ndarray, Image.Image, Dict]:
498
- # Deterministic random projection of multiple handcrafted features.
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
- _pad_or_trim(gabor_vec, 32),
507
- _pad_or_trim(lbp_vec, 128),
508
- _pad_or_trim(sift_vec, 128),
509
- _pad_or_trim(cnn_vec, 128),
510
  ])
511
- base = _unit_vector(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": "Deterministic projected multimethod embedding",
523
  "feature_length": len(emb),
524
- "advantages": "Shows the idea of compact embeddings used by FaceNet/ArcFace/CNN systems.",
525
  "limitations": "Educational simulation; not trained on biometric identity labels."
526
  }
527
- return emb.astype(np.float32), visual, meta
528
 
529
 
530
- def extract_features(img: Image.Image, modality: str, method: str):
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 _fernet_key(secret: str) -> bytes:
557
- digest = hashlib.sha256((secret or "demo-secret").encode()).digest()
558
  return base64.urlsafe_b64encode(digest)
559
 
560
 
561
- def encrypted_preview(vec: np.ndarray, secret: str) -> str:
562
  raw = np.asarray(vec[:64], dtype=np.float32).tobytes()
563
  if HAS_CRYPTO:
564
- f = Fernet(_fernet_key(secret))
565
- token = f.encrypt(raw)
566
  return token[:180].decode("utf-8") + "..."
567
- fallback = hashlib.sha256(raw + secret.encode()).hexdigest()
568
- return "cryptography package missing; SHA-256 preview only: " + fallback
569
 
570
 
571
- def random_projection(vec: np.ndarray, secret: str, out_dim: int = 128) -> np.ndarray:
572
- vec = _unit_vector(vec)
573
- rng = np.random.default_rng(_seed_from_key(secret))
574
  projection = rng.normal(0, 1, size=(len(vec), out_dim)).astype(np.float32)
575
- out = vec @ projection
576
- return _unit_vector(out)
577
 
578
 
579
- def biohash(vec: np.ndarray, secret: str, out_dim: int = 128) -> np.ndarray:
580
  projected = random_projection(vec, secret, out_dim)
581
  return (projected > np.median(projected)).astype(np.float32)
582
 
583
 
584
- def chaotic_permutation(vec: np.ndarray, secret: str) -> np.ndarray:
585
  vec = np.asarray(vec, dtype=np.float32).flatten()
586
- seed = _seed_from_key(secret)
587
  x = ((seed % 100000) + 1) / 100001.0
588
  r = 3.99
589
- chaotic = []
590
  for _ in range(len(vec)):
591
- x = r * x * (1 - x)
592
- chaotic.append(x)
593
- perm = np.argsort(chaotic)
594
- return _unit_vector(vec[perm])
595
 
596
 
597
- def fuzzy_bits(vec: np.ndarray, secret: str, out_dim: int = 128) -> np.ndarray:
598
  projected = random_projection(vec, secret, out_dim)
599
  return (projected > 0).astype(np.float32)
600
 
601
 
602
- def protect_for_matching(vec: np.ndarray, method: str, secret: str) -> Tuple[np.ndarray, str, str]:
603
  vec = np.asarray(vec, dtype=np.float32).flatten()
604
-
605
  if method == "Plain template":
606
- return _unit_vector(vec), "cosine", "Raw normalized template used for comparison."
607
-
608
  if method == "Encrypted storage":
609
- # Real encrypted-template systems usually decrypt before matching
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 chaotic_permutation(vec, secret), "cosine", (
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 _unit_vector(vec), "cosine", (
642
- "Conceptual demo only. Real homomorphic matching would compute on encrypted values "
643
- "with much higher cost."
644
- )
645
-
646
- return _unit_vector(vec), "cosine", "Default normalized template."
647
 
648
 
649
- def template_preview(vec: np.ndarray, method: str, secret: str) -> Tuple[str, pd.DataFrame]:
650
  protected, metric, explanation = protect_for_matching(vec, method, secret)
651
-
652
  if method == "Encrypted storage":
653
- preview = encrypted_preview(vec, secret)
654
- df = pd.DataFrame({
655
- "field": ["storage form", "matching metric", "revocation", "note"],
656
- "value": [
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 = _vector_preview(protected, 24)
670
-
671
- df = pd.DataFrame({
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, df
681
 
682
 
683
  # ---------------------------------------------------------------------
684
  # Liveness and attacks
685
  # ---------------------------------------------------------------------
686
 
687
- def liveness_metrics(img: Image.Image) -> Dict:
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 > (min(h, w) * 0.18)
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
- suspicious_reasons = []
718
  if blur_score < 0.18:
719
- suspicious_reasons.append("low sharpness")
720
  if freq_score < 0.18:
721
- suspicious_reasons.append("low high-frequency detail")
722
  if contrast < 0.05:
723
- suspicious_reasons.append("very low contrast")
724
  if brightness < 0.08 or brightness > 0.92:
725
- suspicious_reasons.append("extreme brightness")
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(suspicious_reasons) if suspicious_reasons else "none"
735
  }
736
 
737
 
738
- def simulate_attack(img: Image.Image, attack: str, intensity: float) -> Image.Image:
739
- img = _safe_image(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
- out = np.clip(arr + noise, 0, 255).astype(np.uint8)
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
- out = np.clip(arr + pattern, 0, 255).astype(np.uint8)
781
- return Image.fromarray(out)
782
-
783
  return img
784
 
785
 
786
  # ---------------------------------------------------------------------
787
- # Gradio callback functions
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
- fig = _fig_feature_bar(vec, f"{method} feature preview")
797
- df = _make_feature_dataframe(vec)
798
- explanation = f"""
799
- ### Feature extraction result
800
-
801
- **Modality:** {modality}
802
- **Method:** {meta.get("method")}
803
- **Feature type:** {meta.get("feature_type")}
804
- **Feature length:** {meta.get("feature_length")}
805
-
806
- **Advantages:** {meta.get("advantages")}
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 "Upload both enrollment and verification images.", pd.DataFrame(), None, None
820
-
821
  try:
822
- e_vec, e_pre, e_vis, e_meta = extract_features(enroll_img, modality, method)
823
- v_vec, v_pre, v_vis, v_meta = extract_features(verify_img, modality, method)
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 = _hamming_similarity(e_prot, v_prot)
830
  else:
831
- similarity = _cosine_similarity(e_prot, v_prot)
832
 
833
  live = liveness_metrics(verify_img)
834
- liveness_score = live["overall_liveness_score"]
835
- is_live = liveness_score >= 0.35
836
- accepted = similarity >= threshold and is_live
837
-
838
  decision = "ACCEPTED" if accepted else "REJECTED"
839
- color = "green" if accepted else "red"
840
 
841
- reason = []
842
- if similarity < threshold:
843
- reason.append("similarity below threshold")
844
  if not is_live:
845
- reason.append("liveness score suspicious")
846
- if not reason:
847
- reason.append("similarity and liveness passed")
848
-
849
- result_md = f"""
850
- ## <span style='color:{color}'>{decision}</span>
851
-
852
- | Check | Value |
853
- |---|---:|
854
- | Similarity score | **{similarity:.4f}** |
855
- | Threshold | **{threshold:.4f}** |
856
- | Matching metric | **{metric}** |
857
- | Liveness score | **{liveness_score:.4f}** |
858
- | Liveness verdict | **{"Live / acceptable" if is_live else "Suspicious"}** |
859
- | Reason | **{", ".join(reason)}** |
860
-
861
- **Template protection explanation:**
862
- {prot_explanation}
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": liveness_score},
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
- vec, pre, vis, meta = extract_features(img, modality, feature_method)
897
- preview, info_df = template_preview(vec, protection_method, secret_key)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
898
 
899
- raw_df = pd.DataFrame({
900
- "index": list(range(min(24, len(vec)))),
901
- "raw_feature_value": [float(x) for x in vec[:24]]
902
- })
903
 
904
- md = f"""
905
- ## Template protection preview
906
 
907
- **Feature method:** {feature_method}
908
- **Protection method:** {protection_method}
909
- **Raw feature length:** {len(vec)}
 
 
 
 
910
 
911
- ### Protected / stored preview
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
912
 
913
- ```text
914
- {preview}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()