bimoadiparwa commited on
Commit
9fd8a40
·
verified ·
1 Parent(s): aed733b

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. app.py +754 -0
  2. 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