Spaces:
Runtime error
Runtime error
Upload folder using huggingface_hub
Browse files- app.py +754 -0
- requirements.txt +4 -0
app.py
ADDED
|
@@ -0,0 +1,754 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Face Anti-Spoofing Dataset Generator
|
| 3 |
+
|
| 4 |
+
This application generates synthetic spoof images for face anti-spoofing dataset creation.
|
| 5 |
+
Attack types align with iBeta Level 1 and Level 2 standards:
|
| 6 |
+
|
| 7 |
+
iBeta Level 1 (Basic Attacks):
|
| 8 |
+
- Print Attack: Printed photos of the target face
|
| 9 |
+
- Display Attack: Screen replay of face images/photos
|
| 10 |
+
- Cut Photo Attack: Partially occluded printed photos
|
| 11 |
+
|
| 12 |
+
iBeta Level 2 (Advanced Attacks):
|
| 13 |
+
- Mask Attack: Paper/plastic masks
|
| 14 |
+
- Warped Photo Attack: Deformed/repositioned photos
|
| 15 |
+
- Eye Frame Attack: Photos with eye cutouts
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import gradio as gr
|
| 19 |
+
import numpy as np
|
| 20 |
+
from PIL import Image, ImageDraw, ImageFilter, ImageEnhance
|
| 21 |
+
import io
|
| 22 |
+
import base64
|
| 23 |
+
import json
|
| 24 |
+
from dataclasses import dataclass
|
| 25 |
+
from typing import List, Tuple, Optional
|
| 26 |
+
import random
|
| 27 |
+
|
| 28 |
+
# Attack types aligned with iBeta standards
|
| 29 |
+
@dataclass
|
| 30 |
+
class AttackType:
|
| 31 |
+
"""Represents a spoof attack type"""
|
| 32 |
+
name: str
|
| 33 |
+
level: int # 1 or 2 (iBeta level)
|
| 34 |
+
description: str
|
| 35 |
+
severity: str # low, medium, high
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# Define attack types
|
| 39 |
+
ATTACK_TYPES = [
|
| 40 |
+
AttackType("Print Attack", 1, "Printed photograph of the face", "low"),
|
| 41 |
+
AttackType("Display Attack", 1, "Face shown on screen/display", "low"),
|
| 42 |
+
AttackType("Cut Photo Attack", 1, "Partially cut photograph with eye holes", "medium"),
|
| 43 |
+
AttackType("Paper Mask Attack", 2, "Paper-based face mask", "medium"),
|
| 44 |
+
AttackType("Warped Photo Attack", 2, "Warped/deformed photograph", "high"),
|
| 45 |
+
AttackType("Eye Frame Attack", 2, "Photo with eye cutouts and frame", "high"),
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def generate_print_attack(image: Image.Image, quality: str = "high") -> Image.Image:
|
| 50 |
+
"""
|
| 51 |
+
Simulate a printed photograph attack.
|
| 52 |
+
Adds print artifacts like grain, slight blur, color shift.
|
| 53 |
+
"""
|
| 54 |
+
img = image.copy()
|
| 55 |
+
|
| 56 |
+
if quality == "high":
|
| 57 |
+
# Slight blur simulating high-quality print
|
| 58 |
+
img = img.filter(ImageFilter.GaussianBlur(radius=0.5))
|
| 59 |
+
# Add slight noise
|
| 60 |
+
np_img = np.array(img)
|
| 61 |
+
noise = np.random.normal(0, 3, np_img.shape).astype(np.int16)
|
| 62 |
+
np_img = np.clip(np_img.astype(np.int16) + noise, 0, 255).astype(np.uint8)
|
| 63 |
+
img = Image.fromarray(np_img)
|
| 64 |
+
else:
|
| 65 |
+
# Lower quality print
|
| 66 |
+
img = img.filter(ImageFilter.GaussianBlur(radius=1.5))
|
| 67 |
+
np_img = np.array(img)
|
| 68 |
+
noise = np.random.normal(0, 10, np_img.shape).astype(np.int16)
|
| 69 |
+
np_img = np.clip(np_img.astype(np.int16) + noise, 0, 255).astype(np.uint8)
|
| 70 |
+
img = Image.fromarray(np_img)
|
| 71 |
+
|
| 72 |
+
# Slight color shift (printing ink effect)
|
| 73 |
+
enhancer = ImageEnhance.Color(img)
|
| 74 |
+
img = enhancer.enhance(0.9)
|
| 75 |
+
|
| 76 |
+
return img
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def generate_display_attack(image: Image.Image, screen_type: str = "phone") -> Image.Image:
|
| 80 |
+
"""
|
| 81 |
+
Simulate a display replay attack.
|
| 82 |
+
Adds screen artifacts like moiré patterns, reflections.
|
| 83 |
+
"""
|
| 84 |
+
img = image.copy()
|
| 85 |
+
|
| 86 |
+
# Resize to simulate different screen sizes
|
| 87 |
+
if screen_type == "phone":
|
| 88 |
+
img = img.resize((224, 224), Image.LANCZOS)
|
| 89 |
+
img = img.resize((300, 300), Image.LANCZOS)
|
| 90 |
+
else:
|
| 91 |
+
img = img.resize((256, 256), Image.LANCZOS)
|
| 92 |
+
img = img.resize((400, 300), Image.LANCZOS)
|
| 93 |
+
|
| 94 |
+
# Add screen moiré effect
|
| 95 |
+
np_img = np.array(img)
|
| 96 |
+
moiré = np.zeros_like(np_img)
|
| 97 |
+
for i in range(moiré.shape[0]):
|
| 98 |
+
for j in range(moiré.shape[1]):
|
| 99 |
+
moiré[i, j] = int(15 * np.sin(i * 0.1) * np.sin(j * 0.1))
|
| 100 |
+
|
| 101 |
+
np_img = np.clip(np_img.astype(np.int16) + moiré, 0, 255).astype(np.uint8)
|
| 102 |
+
img = Image.fromarray(np_img)
|
| 103 |
+
|
| 104 |
+
# Add slight glow effect
|
| 105 |
+
img = img.filter(ImageFilter.GaussianBlur(radius=0.5))
|
| 106 |
+
|
| 107 |
+
return img
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def generate_cut_photo_attack(image: Image.Image, cut_type: str = "eyes") -> Image.Image:
|
| 111 |
+
"""
|
| 112 |
+
Simulate a cut photo attack with eye holes cut out.
|
| 113 |
+
Used to simulate attempts to bypass eye-based liveness detection.
|
| 114 |
+
"""
|
| 115 |
+
img = image.copy()
|
| 116 |
+
width, height = img.size
|
| 117 |
+
|
| 118 |
+
# Create a black background
|
| 119 |
+
background = Image.new('RGB', (width, height), (0, 0, 0))
|
| 120 |
+
|
| 121 |
+
# Calculate eye positions (approximate)
|
| 122 |
+
left_eye_x = int(width * 0.35)
|
| 123 |
+
left_eye_y = int(height * 0.35)
|
| 124 |
+
right_eye_x = int(width * 0.65)
|
| 125 |
+
right_eye_y = int(height * 0.35)
|
| 126 |
+
eye_radius = int(min(width, height) * 0.08)
|
| 127 |
+
|
| 128 |
+
draw = ImageDraw.Draw(background)
|
| 129 |
+
|
| 130 |
+
if cut_type == "eyes":
|
| 131 |
+
# Cut out eye regions
|
| 132 |
+
mask = Image.new('L', (width, height), 0)
|
| 133 |
+
mask_draw = ImageDraw.Draw(mask)
|
| 134 |
+
mask_draw.ellipse(
|
| 135 |
+
(left_eye_x - eye_radius, left_eye_y - eye_radius,
|
| 136 |
+
left_eye_x + eye_radius, left_eye_y + eye_radius),
|
| 137 |
+
fill=255
|
| 138 |
+
)
|
| 139 |
+
mask_draw.ellipse(
|
| 140 |
+
(right_eye_x - eye_radius, right_eye_y - eye_radius,
|
| 141 |
+
right_eye_x + eye_radius, right_eye_y + eye_radius),
|
| 142 |
+
fill=255
|
| 143 |
+
)
|
| 144 |
+
else:
|
| 145 |
+
# Cut out larger region around eyes
|
| 146 |
+
mask = Image.new('L', (width, height), 0)
|
| 147 |
+
mask_draw = ImageDraw.Draw(mask)
|
| 148 |
+
mask_draw.ellipse(
|
| 149 |
+
(left_eye_x - eye_radius*2, left_eye_y - eye_radius*1.5,
|
| 150 |
+
left_eye_x + eye_radius*2, left_eye_y + eye_radius*1.5),
|
| 151 |
+
fill=255
|
| 152 |
+
)
|
| 153 |
+
mask_draw.ellся:
|
| 154 |
+
mask_draw.ellipse(
|
| 155 |
+
(right_eye_x - eye_radius*2, right_eye_y - eye_radius*1.5,
|
| 156 |
+
right_eye_x + eye_radius*2, right_eye_y + eye_radius*1.5),
|
| 157 |
+
fill=255
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
# Apply the cut
|
| 161 |
+
img.paste(background, mask=mask)
|
| 162 |
+
|
| 163 |
+
# Add slight paper texture
|
| 164 |
+
np_img = np.array(img)
|
| 165 |
+
noise = np.random.normal(0, 5, np_img.shape).astype(np.int16)
|
| 166 |
+
np_img = np.clip(np_img.astype(np.int16) + noise, 0, 255).astype(np.uint8)
|
| 167 |
+
img = Image.fromarray(np_img)
|
| 168 |
+
|
| 169 |
+
return img
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def generate_paper_mask_attack(image: Image.Image, mask_style: str = "flat") -> Image.Image:
|
| 173 |
+
"""
|
| 174 |
+
Simulate a paper-based mask attack.
|
| 175 |
+
Creates a simplified face shape on paper.
|
| 176 |
+
"""
|
| 177 |
+
img = image.copy()
|
| 178 |
+
width, height = img.size
|
| 179 |
+
|
| 180 |
+
# Create a face-shaped mask
|
| 181 |
+
mask = Image.new('L', (width, height), 0)
|
| 182 |
+
draw = ImageDraw.Draw(mask)
|
| 183 |
+
|
| 184 |
+
# Draw oval face shape
|
| 185 |
+
face_center_x = width // 2
|
| 186 |
+
face_center_y = height // 2
|
| 187 |
+
face_width = int(width * 0.7)
|
| 188 |
+
face_height = int(height * 0.8)
|
| 189 |
+
|
| 190 |
+
draw.ellipse(
|
| 191 |
+
(face_center_x - face_width//2, face_center_y - face_height//2,
|
| 192 |
+
face_center_x + face_width//2, face_center_y + face_height//2),
|
| 193 |
+
fill=255
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
# Create RGB mask for pasting
|
| 197 |
+
mask_rgb = Image.merge('RGB', [mask, mask, mask])
|
| 198 |
+
|
| 199 |
+
# Apply the mask
|
| 200 |
+
img = Image.composite(img, Image.new('RGB', img.size, (128, 128, 128)), mask)
|
| 201 |
+
|
| 202 |
+
if mask_style == "curled":
|
| 203 |
+
# Add curling effect at edges
|
| 204 |
+
img = img.filter(ImageFilter.GaussianBlur(radius=2))
|
| 205 |
+
else:
|
| 206 |
+
# Flat paper effect
|
| 207 |
+
img = img.filter(ImageFilter.GaussianBlur(radius=0.5))
|
| 208 |
+
|
| 209 |
+
# Add paper texture
|
| 210 |
+
np_img = np.array(img)
|
| 211 |
+
paper_noise = np.random.normal(0, 8, np_img.shape).astype(np.int16)
|
| 212 |
+
np_img = np.clip(np_img.astype(np.int16) + paper_noise, 0, 255).astype(np.uint8)
|
| 213 |
+
img = Image.fromarray(np_img)
|
| 214 |
+
|
| 215 |
+
return img
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def generate_warped_photo_attack(image: Image.Image, warp_type: str = "moderate") -> Image.Image:
|
| 219 |
+
"""
|
| 220 |
+
Simulate a warped/deformed photo attack.
|
| 221 |
+
Creates non-rigid deformations in the face image.
|
| 222 |
+
"""
|
| 223 |
+
img = image.copy()
|
| 224 |
+
width, height = img.size
|
| 225 |
+
|
| 226 |
+
# Apply different warping based on type
|
| 227 |
+
if warp_type == "slight":
|
| 228 |
+
# Very subtle warping
|
| 229 |
+
coeffs = [(1.02, 0.01, -0.01), (0.01, 1.01, -0.02), (0, 0, 1)]
|
| 230 |
+
elif warp_type == "moderate":
|
| 231 |
+
# Moderate warping
|
| 232 |
+
coeffs = [(1.05, 0.02, -0.03), (0.02, 1.03, -0.02), (0, 0, 1)]
|
| 233 |
+
else: # severe
|
| 234 |
+
# More pronounced warping
|
| 235 |
+
coeffs = [(1.08, 0.03, -0.05), (0.03, 1.06, -0.03), (0, 0, 1)]
|
| 236 |
+
|
| 237 |
+
# Apply affine transformation
|
| 238 |
+
img = img.transform(
|
| 239 |
+
(width, height),
|
| 240 |
+
Image.AFFINE,
|
| 241 |
+
coeffs[:2],
|
| 242 |
+
Image.BICUBIC
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
# Add slight blur
|
| 246 |
+
img = img.filter(ImageFilter.GaussianBlur(radius=0.5))
|
| 247 |
+
|
| 248 |
+
return img
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def generate_eye_frame_attack(image: Image.Image, frame_type: str = "plastic") -> Image.Image:
|
| 252 |
+
"""
|
| 253 |
+
Simulate an eye frame attack with cutouts for eyes.
|
| 254 |
+
Includes a physical frame around the photo.
|
| 255 |
+
"""
|
| 256 |
+
img = image.copy()
|
| 257 |
+
width, height = img.size
|
| 258 |
+
|
| 259 |
+
# Create image with frame border
|
| 260 |
+
border_size = int(min(width, height) * 0.1)
|
| 261 |
+
new_width = width + 2 * border_size
|
| 262 |
+
new_height = height + 2 * border_size
|
| 263 |
+
|
| 264 |
+
# Create new canvas with frame
|
| 265 |
+
if frame_type == "plastic":
|
| 266 |
+
frame_color = (30, 30, 30) # Dark plastic frame
|
| 267 |
+
else:
|
| 268 |
+
frame_color = (200, 180, 140) # Wood frame
|
| 269 |
+
|
| 270 |
+
canvas = Image.new('RGB', (new_width, new_height), frame_color)
|
| 271 |
+
|
| 272 |
+
# Paste original image in center
|
| 273 |
+
canvas.paste(img, (border_size, border_size))
|
| 274 |
+
|
| 275 |
+
# Add frame border details
|
| 276 |
+
draw = ImageDraw.Draw(canvas)
|
| 277 |
+
|
| 278 |
+
# Draw inner border
|
| 279 |
+
inner_border = int(border_size * 0.2)
|
| 280 |
+
draw.rectangle(
|
| 281 |
+
[border_size - inner_border, border_size - inner_border,
|
| 282 |
+
new_width - border_size + inner_border, new_height - border_size + inner_border],
|
| 283 |
+
outline=(200, 200, 200) if frame_type == "plastic" else (150, 130, 90),
|
| 284 |
+
width=3
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
# Calculate eye positions in the pasted image
|
| 288 |
+
left_eye_x = border_size + int(width * 0.35)
|
| 289 |
+
left_eye_y = border_size + int(height * 0.35)
|
| 290 |
+
right_eye_x = border_size + int(width * 0.65)
|
| 291 |
+
right_eye_y = border_size + int(height * 0.35)
|
| 292 |
+
eye_radius = int(min(width, height) * 0.06)
|
| 293 |
+
|
| 294 |
+
# Cut out eye holes
|
| 295 |
+
mask = Image.new('L', canvas.size, 0)
|
| 296 |
+
mask_draw = ImageDraw.Draw(mask)
|
| 297 |
+
mask_draw.ellipse(
|
| 298 |
+
(left_eye_x - eye_radius, left_eye_y - eye_radius,
|
| 299 |
+
left_eye_x + eye_radius, left_eye_y + eye_radius),
|
| 300 |
+
fill=255
|
| 301 |
+
)
|
| 302 |
+
mask_draw.ellipse(
|
| 303 |
+
(right_eye_x - eye_radius, right_eye_y - eye_radius,
|
| 304 |
+
right_eye_x + eye_radius, right_eye_y + eye_radius),
|
| 305 |
+
fill=255
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
# Apply the cutouts
|
| 309 |
+
np_canvas = np.array(canvas)
|
| 310 |
+
np_mask = np.array(mask)
|
| 311 |
+
|
| 312 |
+
# Darken the cutout regions (simulating background behind frame)
|
| 313 |
+
np_canvas = np.where(np_mask[:, :, np.newaxis] == 255, np_canvas * 0.3, np_canvas)
|
| 314 |
+
|
| 315 |
+
result = Image.fromarray(np_canvas.astype(np.uint8))
|
| 316 |
+
|
| 317 |
+
# Add frame texture
|
| 318 |
+
np_result = np.array(result)
|
| 319 |
+
frame_texture = np.random.normal(0, 3, np_result.shape).astype(np.int16)
|
| 320 |
+
np_result = np.clip(np_result.astype(np.int16) + frame_texture, 0, 255).astype(np.uint8)
|
| 321 |
+
result = Image.fromarray(np_result)
|
| 322 |
+
|
| 323 |
+
return result
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
def generate_spoof_image(
|
| 327 |
+
reference_image: Image.Image,
|
| 328 |
+
attack_type: str,
|
| 329 |
+
quality_variant: str = "standard"
|
| 330 |
+
) -> Tuple[Image.Image, dict]:
|
| 331 |
+
"""
|
| 332 |
+
Generate a spoof image based on the selected attack type.
|
| 333 |
+
|
| 334 |
+
Args:
|
| 335 |
+
reference_image: The input face image to generate spoof from
|
| 336 |
+
attack_type: Type of spoof attack
|
| 337 |
+
quality_variant: Quality variation of the attack
|
| 338 |
+
|
| 339 |
+
Returns:
|
| 340 |
+
Tuple of (spoof_image, metadata_dict)
|
| 341 |
+
"""
|
| 342 |
+
img = reference_image.copy()
|
| 343 |
+
|
| 344 |
+
# Convert to RGB if needed
|
| 345 |
+
if img.mode != 'RGB':
|
| 346 |
+
img = img.convert('RGB')
|
| 347 |
+
|
| 348 |
+
metadata = {
|
| 349 |
+
"attack_type": attack_type,
|
| 350 |
+
"quality_variant": quality_variant,
|
| 351 |
+
"ibeta_level": None,
|
| 352 |
+
"spoof_indicators": []
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
if attack_type == "Print Attack":
|
| 356 |
+
quality = "high" if quality_variant == "high_quality" else "low"
|
| 357 |
+
result = generate_print_attack(img, quality)
|
| 358 |
+
metadata["ibeta_level"] = 1
|
| 359 |
+
metadata["spoof_indicators"] = [
|
| 360 |
+
"print_texture_artifact",
|
| 361 |
+
"moiré_pattern_possible",
|
| 362 |
+
"flat_surface_indicator"
|
| 363 |
+
]
|
| 364 |
+
|
| 365 |
+
elif attack_type == "Display Attack":
|
| 366 |
+
screen = "phone" if quality_variant == "mobile" else "monitor"
|
| 367 |
+
result = generate_display_attack(img, screen)
|
| 368 |
+
metadata["ibeta_level"] = 1
|
| 369 |
+
metadata["spoof_indicators"] = [
|
| 370 |
+
"screen_reflection",
|
| 371 |
+
"moiré_pattern",
|
| 372 |
+
"backlight_artifact"
|
| 373 |
+
]
|
| 374 |
+
|
| 375 |
+
elif attack_type == "Cut Photo Attack":
|
| 376 |
+
cut_type = "eyes" if quality_variant == "standard" else "large"
|
| 377 |
+
result = generate_cut_photo_attack(img, cut_type)
|
| 378 |
+
metadata["ibeta_level"] = 1
|
| 379 |
+
metadata["spoof_indicators"] = [
|
| 380 |
+
"photo_cut_marks",
|
| 381 |
+
"inconsistent_occlusion",
|
| 382 |
+
"background_discontinuity"
|
| 383 |
+
]
|
| 384 |
+
|
| 385 |
+
elif attack_type == "Paper Mask Attack":
|
| 386 |
+
mask_style = "curled" if quality_variant == "worn" else "flat"
|
| 387 |
+
result = generate_paper_mask_attack(img, mask_style)
|
| 388 |
+
metadata["ibeta_level"] = 2
|
| 389 |
+
metadata["spoof_indicators"] = [
|
| 390 |
+
"mask_edge_artifact",
|
| 391 |
+
"flat_surface_texture",
|
| 392 |
+
"inconsistent_skin_texture"
|
| 393 |
+
]
|
| 394 |
+
|
| 395 |
+
elif attack_type == "Warped Photo Attack":
|
| 396 |
+
warp_type = "slight" if quality_variant == "minimal" else "moderate"
|
| 397 |
+
result = generate_warped_photo_attack(img, warp_type)
|
| 398 |
+
metadata["ibeta_level"] = 2
|
| 399 |
+
metadata["spoof_indicators"] = [
|
| 400 |
+
"geometric_distortion",
|
| 401 |
+
"inconsistent_perspective",
|
| 402 |
+
"non_rigid_deformation"
|
| 403 |
+
]
|
| 404 |
+
|
| 405 |
+
elif attack_type == "Eye Frame Attack":
|
| 406 |
+
frame_type = "plastic" if quality_variant == "standard" else "wooden"
|
| 407 |
+
result = generate_eye_frame_attack(img, frame_type)
|
| 408 |
+
metadata["ibeta_level"] = 2
|
| 409 |
+
metadata["spoof_indicators"] = [
|
| 410 |
+
"frame_artifact",
|
| 411 |
+
"eye_cutout_marks",
|
| 412 |
+
"inconsistent_depth"
|
| 413 |
+
]
|
| 414 |
+
|
| 415 |
+
else:
|
| 416 |
+
result = img.copy()
|
| 417 |
+
metadata["error"] = "Unknown attack type"
|
| 418 |
+
|
| 419 |
+
return result, metadata
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
def create_dataset_preview(
|
| 423 |
+
reference_image: Image.Image,
|
| 424 |
+
selected_attacks: List[str],
|
| 425 |
+
generate_all: bool = False
|
| 426 |
+
) -> Tuple[Image.Image, str, dict]:
|
| 427 |
+
"""
|
| 428 |
+
Create a preview of generated spoof images.
|
| 429 |
+
|
| 430 |
+
Returns:
|
| 431 |
+
Preview image, attack info summary, and dataset metadata
|
| 432 |
+
"""
|
| 433 |
+
if reference_image is None:
|
| 434 |
+
return None, "Please upload a reference image first.", {}
|
| 435 |
+
|
| 436 |
+
if not generate_all and not selected_attacks:
|
| 437 |
+
return None, "Please select at least one attack type.", {}
|
| 438 |
+
|
| 439 |
+
attacks_to_generate = ATTACK_TYPES if generate_all else [
|
| 440 |
+
at for at in ATTACK_TYPES if at.name in selected_attacks
|
| 441 |
+
]
|
| 442 |
+
|
| 443 |
+
# Create a grid preview
|
| 444 |
+
cols = min(len(attacks_to_generate), 3)
|
| 445 |
+
rows = (len(attacks_to_generate) + cols - 1) // cols
|
| 446 |
+
|
| 447 |
+
img = reference_image.copy()
|
| 448 |
+
if img.mode != 'RGB':
|
| 449 |
+
img = img.convert('RGB')
|
| 450 |
+
|
| 451 |
+
# Resize for consistent preview
|
| 452 |
+
preview_size = (200, 200)
|
| 453 |
+
img = img.resize(preview_size, Image.LANCZOS)
|
| 454 |
+
|
| 455 |
+
# Calculate grid dimensions
|
| 456 |
+
cell_width = preview_size[0] + 20
|
| 457 |
+
cell_height = preview_size[1] + 40
|
| 458 |
+
|
| 459 |
+
grid_width = cols * cell_width
|
| 460 |
+
grid_height = rows * cell_height + 60
|
| 461 |
+
|
| 462 |
+
# Create preview canvas
|
| 463 |
+
preview = Image.new('RGB', (grid_width, grid_height), (245, 245, 245))
|
| 464 |
+
draw = ImageDraw.Draw(preview)
|
| 465 |
+
|
| 466 |
+
# Title
|
| 467 |
+
from PIL import ImageFont
|
| 468 |
+
try:
|
| 469 |
+
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 14)
|
| 470 |
+
except:
|
| 471 |
+
font = ImageFont.load_default()
|
| 472 |
+
|
| 473 |
+
draw.text((10, 10), "Generated Spoof Samples Preview", fill=(50, 50, 50), font=font)
|
| 474 |
+
|
| 475 |
+
# Generate and place each attack
|
| 476 |
+
dataset_metadata = {
|
| 477 |
+
"total_samples": len(attacks_to_generate),
|
| 478 |
+
"ibeta_level_1_count": sum(1 for a in attacks_to_generate if a.level == 1),
|
| 479 |
+
"ibeta_level_2_count": sum(1 for a in attacks_to_generate if a.level == 2),
|
| 480 |
+
"attacks": []
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
for idx, attack in enumerate(attacks_to_generate):
|
| 484 |
+
col = idx % cols
|
| 485 |
+
row = idx // cols
|
| 486 |
+
|
| 487 |
+
x = col * cell_width + 10
|
| 488 |
+
y = row * cell_height + 40
|
| 489 |
+
|
| 490 |
+
# Generate spoof image
|
| 491 |
+
spoof_img, metadata = generate_spoof_image(
|
| 492 |
+
reference_image,
|
| 493 |
+
attack.name,
|
| 494 |
+
"standard"
|
| 495 |
+
)
|
| 496 |
+
|
| 497 |
+
spoof_img = spoof_img.resize(preview_size, Image.LANCZOS)
|
| 498 |
+
|
| 499 |
+
# Paste into preview
|
| 500 |
+
preview.paste(spoof_img, (x, y))
|
| 501 |
+
|
| 502 |
+
# Draw attack name
|
| 503 |
+
name_y = y + preview_size[1] + 5
|
| 504 |
+
level_text = f"[L{attack.level}]"
|
| 505 |
+
draw.text((x, name_y), level_text, fill=(100, 100, 100), font=font)
|
| 506 |
+
draw.text((x + 30, name_y), attack.name[:15], fill=(50, 50, 50), font=font)
|
| 507 |
+
|
| 508 |
+
dataset_metadata["attacks"].append({
|
| 509 |
+
"attack_type": attack.name,
|
| 510 |
+
"ibeta_level": attack.level,
|
| 511 |
+
"severity": attack.severity,
|
| 512 |
+
"description": attack.description,
|
| 513 |
+
"metadata": metadata
|
| 514 |
+
})
|
| 515 |
+
|
| 516 |
+
# Create summary
|
| 517 |
+
summary = (
|
| 518 |
+
f"Dataset Preview Generated:\n"
|
| 519 |
+
f"• Total Samples: {dataset_metadata['total_samples']}\n"
|
| 520 |
+
f"• iBeta Level 1: {dataset_metadata['ibeta_level_1_count']} attacks\n"
|
| 521 |
+
f"• iBeta Level 2: {dataset_metadata['ibeta_level_2_count']} attacks\n"
|
| 522 |
+
f"• Attacks: {', '.join(a.name for a in attacks_to_generate)}"
|
| 523 |
+
)
|
| 524 |
+
|
| 525 |
+
return preview, summary, dataset_metadata
|
| 526 |
+
|
| 527 |
+
|
| 528 |
+
def export_dataset_metadata(metadata: dict) -> str:
|
| 529 |
+
"""Export dataset metadata as JSON string."""
|
| 530 |
+
return json.dumps(metadata, indent=2)
|
| 531 |
+
|
| 532 |
+
|
| 533 |
+
# Custom theme for the app
|
| 534 |
+
def create_custom_theme():
|
| 535 |
+
"""Create a custom theme for the anti-spoofing dataset generator."""
|
| 536 |
+
return gr.themes.Soft(
|
| 537 |
+
primary_hue="red",
|
| 538 |
+
secondary_hue="orange",
|
| 539 |
+
neutral_hue="slate",
|
| 540 |
+
font=gr.themes.GoogleFont("Inter"),
|
| 541 |
+
text_size="lg",
|
| 542 |
+
spacing_size="lg",
|
| 543 |
+
radius_size="md"
|
| 544 |
+
).set(
|
| 545 |
+
button_primary_background_fill="*primary_600",
|
| 546 |
+
button_primary_background_fill_hover="*primary_700",
|
| 547 |
+
block_title_text_weight="600",
|
| 548 |
+
body_text_weight="500",
|
| 549 |
+
)
|
| 550 |
+
|
| 551 |
+
|
| 552 |
+
# Gradio 6 App
|
| 553 |
+
with gr.Blocks() as demo:
|
| 554 |
+
# Custom CSS for styling
|
| 555 |
+
custom_css = """
|
| 556 |
+
.spoof-header {
|
| 557 |
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
| 558 |
+
padding: 20px;
|
| 559 |
+
border-radius: 12px;
|
| 560 |
+
margin-bottom: 20px;
|
| 561 |
+
}
|
| 562 |
+
.attack-card {
|
| 563 |
+
background: white;
|
| 564 |
+
border-radius: 10px;
|
| 565 |
+
padding: 15px;
|
| 566 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
| 567 |
+
margin: 10px 0;
|
| 568 |
+
}
|
| 569 |
+
.level-badge {
|
| 570 |
+
background: linear-gradient(135deg, #f97316 0%, #ef4444 100%);
|
| 571 |
+
color: white;
|
| 572 |
+
padding: 4px 12px;
|
| 573 |
+
border-radius: 20px;
|
| 574 |
+
font-size: 12px;
|
| 575 |
+
font-weight: bold;
|
| 576 |
+
}
|
| 577 |
+
.level-badge-1 {
|
| 578 |
+
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
| 579 |
+
}
|
| 580 |
+
.level-badge-2 {
|
| 581 |
+
background: linear-gradient(135deg, #f97316 0%, #ef4444 100%);
|
| 582 |
+
}
|
| 583 |
+
.info-box {
|
| 584 |
+
background: #fef3c7;
|
| 585 |
+
border-left: 4px solid #f59e0b;
|
| 586 |
+
padding: 12px;
|
| 587 |
+
border-radius: 4px;
|
| 588 |
+
margin: 10px 0;
|
| 589 |
+
}
|
| 590 |
+
.metadata-box {
|
| 591 |
+
background: #f1f5f9;
|
| 592 |
+
border: 1px solid #e2e8f0;
|
| 593 |
+
padding: 15px;
|
| 594 |
+
border-radius: 8px;
|
| 595 |
+
font-family: monospace;
|
| 596 |
+
font-size: 12px;
|
| 597 |
+
max-height: 300px;
|
| 598 |
+
overflow-y: auto;
|
| 599 |
+
}
|
| 600 |
+
"""
|
| 601 |
+
|
| 602 |
+
# Header with branding
|
| 603 |
+
with gr.Group(elem_classes=["spoof-header"]):
|
| 604 |
+
gr.Markdown(
|
| 605 |
+
"""
|
| 606 |
+
# 🔒 Face Anti-Spoofing Dataset Generator
|
| 607 |
+
|
| 608 |
+
Generate synthetic spoof images for face anti-spoofing dataset creation.
|
| 609 |
+
Aligned with **iBeta Level 1 & 2** standards.
|
| 610 |
+
|
| 611 |
+
*Built with [anycoder](https://huggingface.co/spaces/akhaliq/anycoder)*
|
| 612 |
+
"""
|
| 613 |
+
)
|
| 614 |
+
|
| 615 |
+
# Info boxes about iBeta levels
|
| 616 |
+
with gr.Row():
|
| 617 |
+
with gr.Column(scale=1):
|
| 618 |
+
gr.Markdown(
|
| 619 |
+
"""
|
| 620 |
+
### 📋 iBeta Level 1 (Basic)
|
| 621 |
+
- **Print Attack**: Printed photos
|
| 622 |
+
- **Display Attack**: Screen replays
|
| 623 |
+
- **Cut Photo Attack**: Partially occluded
|
| 624 |
+
""",
|
| 625 |
+
elem_classes=["attack-card"]
|
| 626 |
+
)
|
| 627 |
+
with gr.Column(scale=1):
|
| 628 |
+
gr.Markdown(
|
| 629 |
+
"""
|
| 630 |
+
### ⚠️ iBeta Level 2 (Advanced)
|
| 631 |
+
- **Paper Mask Attack**: Paper masks
|
| 632 |
+
- **Warped Photo Attack**: Deformed photos
|
| 633 |
+
- **Eye Frame Attack**: Eye cutout frames
|
| 634 |
+
""",
|
| 635 |
+
elem_classes=["attack-card"]
|
| 636 |
+
)
|
| 637 |
+
|
| 638 |
+
# Main input section
|
| 639 |
+
with gr.Row():
|
| 640 |
+
with gr.Column(scale=1):
|
| 641 |
+
gr.Markdown("### 📤 Reference Image")
|
| 642 |
+
reference_image = gr.Image(
|
| 643 |
+
label="Upload Live Face Image",
|
| 644 |
+
type="pil",
|
| 645 |
+
sources=["upload"],
|
| 646 |
+
height=300
|
| 647 |
+
)
|
| 648 |
+
|
| 649 |
+
gr.Markdown("### 🎯 Attack Selection")
|
| 650 |
+
attack_checkbox = gr.CheckboxGroup(
|
| 651 |
+
choices=[at.name for at in ATTACK_TYPES],
|
| 652 |
+
value=[ATTACK_TYPES[0].name], # Default to first attack
|
| 653 |
+
label="Select Attack Types",
|
| 654 |
+
info="Choose which spoof attacks to generate"
|
| 655 |
+
)
|
| 656 |
+
|
| 657 |
+
generate_all_checkbox = gr.Checkbox(
|
| 658 |
+
value=False,
|
| 659 |
+
label="Generate All Attack Types",
|
| 660 |
+
info="Generate samples for all available attacks"
|
| 661 |
+
)
|
| 662 |
+
|
| 663 |
+
generate_btn = gr.Button(
|
| 664 |
+
"Generate Spoof Samples",
|
| 665 |
+
variant="primary",
|
| 666 |
+
size="lg"
|
| 667 |
+
)
|
| 668 |
+
|
| 669 |
+
with gr.Column(scale=1):
|
| 670 |
+
gr.Markdown("### 📊 Preview & Output")
|
| 671 |
+
preview_output = gr.Image(
|
| 672 |
+
label="Generated Spoof Samples",
|
| 673 |
+
type="pil",
|
| 674 |
+
height=400
|
| 675 |
+
)
|
| 676 |
+
|
| 677 |
+
summary_output = gr.Textbox(
|
| 678 |
+
label="Generation Summary",
|
| 679 |
+
lines=6,
|
| 680 |
+
interactive=False
|
| 681 |
+
)
|
| 682 |
+
|
| 683 |
+
# Metadata section
|
| 684 |
+
with gr.Accordion("📄 Dataset Metadata (JSON)", open=True):
|
| 685 |
+
metadata_output = gr.Code(
|
| 686 |
+
label="Metadata",
|
| 687 |
+
language="json",
|
| 688 |
+
elem_classes=["metadata-box"]
|
| 689 |
+
)
|
| 690 |
+
|
| 691 |
+
# Attack details section
|
| 692 |
+
with gr.Accordion("ℹ️ Attack Type Details", open=False):
|
| 693 |
+
gr.Markdown(
|
| 694 |
+
"""
|
| 695 |
+
| Attack Type | iBeta Level | Severity | Description |
|
| 696 |
+
|-------------|-------------|----------|-------------|
|
| 697 |
+
| Print Attack | 1 | Low | Printed photograph with typical print artifacts |
|
| 698 |
+
| Display Attack | 1 | Low | Face displayed on screen with moiré patterns |
|
| 699 |
+
| Cut Photo Attack | 1 | Medium | Printed photo with eye cutouts |
|
| 700 |
+
| Paper Mask Attack | 2 | Medium | Flat paper-based face mask |
|
| 701 |
+
| Warped Photo Attack | 2 | High | Deformed photograph with geometric distortion |
|
| 702 |
+
| Eye Frame Attack | 2 | High | Photo with eye cutouts and physical frame |
|
| 703 |
+
|
| 704 |
+
### Spoof Indicators
|
| 705 |
+
Each generated sample includes metadata with expected spoof indicators for training:
|
| 706 |
+
- Texture artifacts (print, paper)
|
| 707 |
+
- Moiré patterns (display)
|
| 708 |
+
- Geometric distortions (warped)
|
| 709 |
+
- Occlusion patterns (cut, frame)
|
| 710 |
+
- Surface inconsistencies (mask)
|
| 711 |
+
"""
|
| 712 |
+
)
|
| 713 |
+
|
| 714 |
+
# Event handlers
|
| 715 |
+
def handle_generate(ref_img, attacks, generate_all):
|
| 716 |
+
preview, summary, metadata = create_dataset_preview(
|
| 717 |
+
ref_img,
|
| 718 |
+
attacks,
|
| 719 |
+
generate_all
|
| 720 |
+
)
|
| 721 |
+
metadata_json = export_dataset_metadata(metadata)
|
| 722 |
+
return preview, summary, metadata_json
|
| 723 |
+
|
| 724 |
+
generate_btn.click(
|
| 725 |
+
fn=handle_generate,
|
| 726 |
+
inputs=[reference_image, attack_checkbox, generate_all_checkbox],
|
| 727 |
+
outputs=[preview_output, summary_output, metadata_output]
|
| 728 |
+
)
|
| 729 |
+
|
| 730 |
+
# Update when "Generate All" changes
|
| 731 |
+
generate_all_checkbox.change(
|
| 732 |
+
fn=lambda x: gr.CheckboxGroup(interactive=not x),
|
| 733 |
+
inputs=generate_all_checkbox,
|
| 734 |
+
outputs=attack_checkbox
|
| 735 |
+
)
|
| 736 |
+
|
| 737 |
+
# Live preview on image change
|
| 738 |
+
reference_image.change(
|
| 739 |
+
fn=handle_generate,
|
| 740 |
+
inputs=[reference_image, attack_checkbox, generate_all_checkbox],
|
| 741 |
+
outputs=[preview_output, summary_output, metadata_output]
|
| 742 |
+
)
|
| 743 |
+
|
| 744 |
+
# Launch with Gradio 6 theme and configuration
|
| 745 |
+
demo.launch(
|
| 746 |
+
theme=create_custom_theme(),
|
| 747 |
+
css=custom_css if 'custom_css' in dir() else None,
|
| 748 |
+
footer_links=[
|
| 749 |
+
{"label": "Built with anycoder", "url": "https://huggingface.co/spaces/akhaliq/anycoder"},
|
| 750 |
+
{"label": "Documentation", "url": "https://gradio.app"},
|
| 751 |
+
],
|
| 752 |
+
title="Face Anti-Spoofing Dataset Generator",
|
| 753 |
+
description="Generate synthetic spoof images aligned with iBeta Level 1 & 2 standards",
|
| 754 |
+
)
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Pillow
|
| 2 |
+
dataclasses
|
| 3 |
+
gradio
|
| 4 |
+
numpy
|