deepclip 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. deepclip/__init__.py +31 -0
  2. deepclip/config/deepclip_b0.yaml +65 -0
  3. deepclip/config/deepclip_b5.yaml +65 -0
  4. deepclip/data/__init__.py +9 -0
  5. deepclip/data/collate.py +104 -0
  6. deepclip/data/dataset.py +67 -0
  7. deepclip/data/handle_imbalance.py +44 -0
  8. deepclip/data/split_data.py +83 -0
  9. deepclip/data/text/__init__.py +16 -0
  10. deepclip/data/text/bpe_simple_vocab_16e6.txt.gz +0 -0
  11. deepclip/data/text/template.py +133 -0
  12. deepclip/data/text/tokenizer.py +146 -0
  13. deepclip/data/transforms.py +76 -0
  14. deepclip/layers/__init__.py +13 -0
  15. deepclip/layers/attention.py +153 -0
  16. deepclip/layers/clip_text.py +61 -0
  17. deepclip/layers/drop.py +35 -0
  18. deepclip/layers/featextractor.py +56 -0
  19. deepclip/layers/global_query.py +65 -0
  20. deepclip/layers/mlp.py +41 -0
  21. deepclip/layers/patch.py +58 -0
  22. deepclip/layers/reducesize.py +45 -0
  23. deepclip/layers/se.py +28 -0
  24. deepclip/layers/transformer.py +266 -0
  25. deepclip/layers/weight_init.py +30 -0
  26. deepclip/layers/window.py +22 -0
  27. deepclip/losses.py +16 -0
  28. deepclip/models/__init__.py +8 -0
  29. deepclip/models/deepclip.py +199 -0
  30. deepclip/models/image_encoder.py +198 -0
  31. deepclip/models/text_encoder.py +125 -0
  32. deepclip/optimizer/__init__.py +1 -0
  33. deepclip/optimizer/build_optimizer.py +29 -0
  34. deepclip/scheduler/__init__.py +1 -0
  35. deepclip/scheduler/fetch_scheduler.py +58 -0
  36. deepclip/utils/__init__.py +4 -0
  37. deepclip/utils/build_metrics.py +62 -0
  38. deepclip/utils/early_stopping.py +24 -0
  39. deepclip/utils/meter.py +26 -0
  40. deepclip/utils/seed.py +23 -0
  41. deepclip-0.1.0.dist-info/METADATA +36 -0
  42. deepclip-0.1.0.dist-info/RECORD +45 -0
  43. deepclip-0.1.0.dist-info/WHEEL +5 -0
  44. deepclip-0.1.0.dist-info/licenses/LICENSE +21 -0
  45. deepclip-0.1.0.dist-info/top_level.txt +1 -0
deepclip/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+ import warnings
2
+ import logging
3
+ from importlib.metadata import version, PackageNotFoundError
4
+
5
+ try:
6
+ __version__ = version("deepclip") # pyproject.toml
7
+ except PackageNotFoundError: # Not installed (e.g. run from the repository)
8
+ __version__ = "0.0.0"
9
+
10
+ # ------- 로그 및 경고 제어 ---------
11
+ warnings.filterwarnings("ignore") # 일반 경고 숨기기
12
+ logging.getLogger("timm").setLevel(logging.ERROR) # timm 내부 로그 숨기기
13
+ logging.getLogger("torch").setLevel(logging.ERROR) # torch 관련 로그 숨기기
14
+
15
+ from .models import image_encoder, text_encoder, deepclip
16
+ from .models.image_encoder import DeepImageEncoder
17
+ from .models.text_encoder import DeepTextEncoder
18
+ from .models.deepclip import (
19
+ DeepClip,
20
+ deepclip_b0,
21
+ deepclip_b5,
22
+ )
23
+
24
+ __all__ = [
25
+ "__version__",
26
+ "DeepImageEncoder",
27
+ "DeepTextEncoder",
28
+ "DeepClip",
29
+ "deepclip_b0",
30
+ "deepclip_b5",
31
+ ]
@@ -0,0 +1,65 @@
1
+ dataset: "FaceForensics++"
2
+
3
+ model:
4
+ model_name: "tf_efficientnet_b0.ns_jft_in1k"
5
+ img_size: [224,224]
6
+
7
+ # ===== Feature Extraction Blocks =====
8
+ l_block_idx: 1 # Low-level feature stage index
9
+ h_block_idx: 6 # High-level feature stage index
10
+
11
+ # ===== Transformer Dimensions =====
12
+ l_dim: 24 # Dimension for low-block transformer
13
+ h_dim: 256 # Dimension for high-block transformer
14
+ l_depths: [2,2,4,2] # Number of transformer layers (Low)
15
+ h_depths: [4] # Number of transformer layers (High)
16
+
17
+ # ===== Attention Settings =====
18
+ l_windows: [7,7,14,7] # Window Size for Attention (Low)
19
+ h_windows: [7] # Window Size for Attention (High)
20
+ l_heads: [1,2,4,8] # Multi Head Attention (Low)
21
+ h_heads: [4] # Multi Head Attention (High)
22
+ l_ratio: [4,4,4,4] # MLP expansion ratio (Low)
23
+ h_ratio: [4] # MLP expansion ratio (High)
24
+
25
+ # ===== Regularization (Dropout) =====
26
+ l_drop: 0 # Pos dropout, Mlp dropout, Proj dropout (Low)
27
+ h_drop: 0.05 # Pos dropout, Mlp dropout, Proj dropout (High)
28
+ l_attn_drop: 0.05 # Attention Dropout (Low)
29
+ h_attn_drop: 0 # Attention Dropout (High)
30
+ l_drop_path: 0.1 # Stochastic depth rate (Low)
31
+ h_drop_path: 0.05 # Stochastic depth rate (High)
32
+
33
+ # ===== Norm (GCViT + image projection) =====
34
+ image_norm: "rmsnorm" # rmsnorm or layernorm
35
+
36
+ # ===== Text Encoder (frozen) =====
37
+ clip_name: "ViT-B/16"
38
+ embed_dim: 512 # Text embedding dimension of clip_name (checked)
39
+
40
+ text:
41
+ type_sampling_alpha: 1.0 # Type weight = freq^-alpha
42
+ region_sampling_beta: 1.0 # Region weight = region_freq^-beta
43
+
44
+ loss:
45
+ real_text_weight: 1.0 # Weight of the REAL_TEXT column of the loss
46
+ fake_text_weight: 1.0 # Weight of the FAKE caption columns of the loss
47
+
48
+ train:
49
+ epochs: 10
50
+ use_amp: True # Mixed Precision Training
51
+ n_accumulate: 1 # Gradient Accumulation steps
52
+ optimizer: "AdamW"
53
+ scheduler: "CosineAnnealingLR"
54
+
55
+ # DataLoader
56
+ train_bs: 16
57
+ valid_bs: 32
58
+
59
+ # Optimizer (decay / no_decay groups, same lr)
60
+ lr: 2e-4
61
+ weight_decay: 1e-3
62
+
63
+ # Early Stopping (valid_loss)
64
+ patience: 3
65
+ min_delta: 0.001
@@ -0,0 +1,65 @@
1
+ dataset: "FaceForensics++"
2
+
3
+ model:
4
+ model_name: "tf_efficientnet_b5.ns_jft_in1k"
5
+ img_size: [384,384]
6
+
7
+ # ===== Feature Extraction Blocks =====
8
+ l_block_idx: 1 # Low-level feature stage index
9
+ h_block_idx: 6 # High-level feature stage index
10
+
11
+ # ===== Transformer Dimensions =====
12
+ l_dim: 48 # Dimension for low-block transformer
13
+ h_dim: 512 # Dimension for high-block transformer
14
+ l_depths: [2,2,6,2] # Number of transformer layers (Low)
15
+ h_depths: [6] # Number of transformer layers (High)
16
+
17
+ # ===== Attention Settings =====
18
+ l_windows: [12,12,24,12] # Window Size for Attention (Low)
19
+ h_windows: [12] # Window Size for Attention (High)
20
+ l_heads: [2,4,8,16] # Multi Head Attention (Low)
21
+ h_heads: [16] # Multi Head Attention (High)
22
+ l_ratio: [3,3,3,3] # MLP expansion ratio (Low)
23
+ h_ratio: [3] # MLP expansion ratio (High)
24
+
25
+ # ===== Regularization (Dropout) =====
26
+ l_drop: 0.0 # Pos dropout, Mlp dropout, Proj dropout (Low)
27
+ h_drop: 0.1 # Pos dropout, Mlp dropout, Proj dropout (High)
28
+ l_attn_drop: 0.1 # Attention Dropout (Low)
29
+ h_attn_drop: 0.0 # Attention Dropout (High)
30
+ l_drop_path: 0.15 # Stochastic depth rate (Low)
31
+ h_drop_path: 0.1 # Stochastic depth rate (High)
32
+
33
+ # ===== Norm (GCViT + image projection) =====
34
+ image_norm: "rmsnorm" # rmsnorm or layernorm
35
+
36
+ # ===== Text Encoder (frozen) =====
37
+ clip_name: "ViT-L/14"
38
+ embed_dim: 768 # Text embedding dimension of clip_name (checked)
39
+
40
+ text:
41
+ type_sampling_alpha: 1.0 # Type weight = freq^-alpha
42
+ region_sampling_beta: 1.0 # Region weight = region_freq^-beta
43
+
44
+ loss:
45
+ real_text_weight: 1.0 # Weight of the REAL_TEXT column of the loss
46
+ fake_text_weight: 1.0 # Weight of the FAKE caption columns of the loss
47
+
48
+ train:
49
+ epochs: 10
50
+ use_amp: True # Mixed Precision Training
51
+ n_accumulate: 1 # Gradient Accumulation steps
52
+ optimizer: "AdamW"
53
+ scheduler: "CosineAnnealingLR"
54
+
55
+ # DataLoader
56
+ train_bs: 16
57
+ valid_bs: 32
58
+
59
+ # Optimizer (decay / no_decay groups, same lr)
60
+ lr: 5e-5
61
+ weight_decay: 2e-4
62
+
63
+ # Early Stopping (valid_loss)
64
+ patience: 3
65
+ min_delta: 0.001
@@ -0,0 +1,9 @@
1
+ from .dataset import DeepClipDataset
2
+ from .collate import DeepClipCollate
3
+ from .transforms import (
4
+ get_train_transforms,
5
+ get_valid_transforms,
6
+ get_test_transforms,
7
+ )
8
+ from .handle_imbalance import class_imbalance_handle
9
+ from .split_data import split_data
@@ -0,0 +1,104 @@
1
+ import numpy as np
2
+ import torch
3
+ from torch.utils.data import get_worker_info
4
+ from deepclip.data.text import REAL_TEXT, parse_pair_label, sample_selection, render_caption
5
+
6
+ class DeepClipCollate:
7
+ """
8
+ Makes the captions of a batch and the targets of the loss.
9
+
10
+ For every image: REAL -> REAL_TEXT, FAKE -> the caption of the pair chosen by sample_selection
11
+ (a new choice at every step). The B captions are reduced to the U unique captions (in order).
12
+
13
+ Returns a dict:
14
+ images (B,3,H,W)
15
+ labels (B,) 0: REAL, 1: FAKE
16
+ text_idx (U,) rows of text_table of the unique captions
17
+ Y (B,U) 1 if caption j is true for image i
18
+ (REAL_TEXT column: image i is REAL, FAKE column: the pair of caption j is on in image i)
19
+ W (B,U) real_text_weight for the REAL_TEXT column, fake_text_weight for the FAKE columns
20
+ pair_targets (B,20) pair_label of every image (all 0 for REAL), target of map and the AP
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ caption_to_idx: dict,
26
+ type_weights: np.ndarray,
27
+ region_weights: np.ndarray,
28
+ real_text_weight: float = 1.0,
29
+ fake_text_weight: float = 1.0,
30
+ seed: int | None = None,
31
+ ):
32
+ """
33
+ Args:
34
+ caption_to_idx: caption -> row of text_table (DeepClip.caption_to_idx).
35
+ type_weights: (4,5) weights of the types (type_weights of the train FAKE frames).
36
+ region_weights: (4,) weights of the regions (region_weights of the train FAKE frames).
37
+ real_text_weight, fake_text_weight: Weights of the REAL_TEXT column and the FAKE columns of the loss.
38
+ seed: Fixed seed for the same captions at every epoch (valid). None for new captions at every epoch (train).
39
+ """
40
+
41
+ self.caption_to_idx = caption_to_idx
42
+ self.type_weights = type_weights
43
+ self.region_weights = region_weights
44
+ self.real_text_weight = real_text_weight
45
+ self.fake_text_weight = fake_text_weight
46
+ self.seed = seed
47
+ self.reset()
48
+
49
+ def reset(self):
50
+ """Restarts the random captions (with seed: the same captions as the previous epoch when num_workers=0)"""
51
+ self.rng = np.random.default_rng(self.seed if self.seed is not None else np.random.randint(2**31))
52
+ self._worker_id = None
53
+
54
+ def _update_rng(self):
55
+ # Every DataLoader worker gets a copy of this object, so the rng is set again in each worker
56
+ info = get_worker_info()
57
+
58
+ if info is not None and self._worker_id != info.id:
59
+ self._worker_id = info.id
60
+ self.rng = np.random.default_rng([self.seed, info.id] if self.seed is not None else info.seed)
61
+
62
+ def __call__(self, batch):
63
+ self._update_rng()
64
+
65
+ imgs, labels, pair_labels = zip(*batch)
66
+ images = torch.stack(imgs) # (B,3,H,W)
67
+ labels = torch.stack(labels).float() # (B,)
68
+
69
+ lab = np.stack([parse_pair_label(p) for p in pair_labels]) # (B,4,5)
70
+ is_fake = labels.numpy() == 1
71
+
72
+ # ====== Caption of every image ======
73
+ captions, selections = [], []
74
+ for i in range(len(batch)):
75
+ if is_fake[i]:
76
+ sel = sample_selection(lab[i], self.type_weights, self.region_weights, self.rng)
77
+ captions.append(render_caption(sel))
78
+ else:
79
+ sel = np.zeros_like(lab[i])
80
+ captions.append(REAL_TEXT)
81
+ selections.append(sel)
82
+
83
+ # ====== Unique captions (in order) ======
84
+ uniq = list(dict.fromkeys(captions))
85
+ first = [captions.index(c) for c in uniq]
86
+ text_idx = torch.tensor([self.caption_to_idx[c] for c in uniq], dtype=torch.long) # (U,)
87
+
88
+ # ====== Targets Y and weights W ======
89
+ is_real_col = np.array([c == REAL_TEXT for c in uniq]) # (U,)
90
+ sel_cols = np.stack([selections[i] for i in first]) # (U,4,5)
91
+
92
+ # the pair of caption j is on in image i
93
+ subset = ~(sel_cols[None] & ~lab[:, None]).any(axis=(2, 3)) # (B,U)
94
+ Y = np.where(is_real_col[None], ~is_fake[:, None], is_fake[:, None] & subset)
95
+ W = np.where(is_real_col, self.real_text_weight, self.fake_text_weight)
96
+
97
+ return {
98
+ "images": images,
99
+ "labels": labels,
100
+ "text_idx": text_idx,
101
+ "Y": torch.from_numpy(Y).float(),
102
+ "W": torch.from_numpy(np.broadcast_to(W, Y.shape).copy()).float(),
103
+ "pair_targets": torch.from_numpy(lab.reshape(len(batch), -1)).long(),
104
+ }
@@ -0,0 +1,67 @@
1
+ import pandas as pd
2
+ from typing import List
3
+ import cv2
4
+ import torch
5
+ from torch.utils.data import Dataset
6
+ import albumentations as A
7
+
8
+ class DeepClipDataset(Dataset):
9
+ """
10
+ Dataset class for DeepClip, based on DeepFakeDataset of DeepGuard.
11
+ Reads image paths, labels and pair labels from a pandas DataFrame, loads images,
12
+ and applies specified transformations. The caption is made by DeepClipCollate.
13
+
14
+ Attributes:
15
+ meta_df (pd.DataFrame): Dataframe containing image paths, labels and pair labels.
16
+ transforms (A.Compose): Albumentations transformation pipeline.
17
+ img_size (List[int]): [Height, Width] of the image.
18
+ img_col (str): Name of the column containing image file paths.
19
+ label_col (str): Name of the column containing labels (0: REAL, 1: FAKE).
20
+ pair_col (str): Name of the column containing pair labels (20 characters of 0 and 1).
21
+ return_label (bool): Whether to return (image, label, pair_label) or just image.
22
+ """
23
+ def __init__(
24
+ self,
25
+ meta_df: pd.DataFrame,
26
+ img_size: List[int],
27
+ transforms: A.Compose,
28
+ img_col: str = "img_path",
29
+ label_col: str = "label",
30
+ pair_col: str = "pair_label",
31
+ return_label: bool = True,
32
+ ):
33
+ self.meta_df = meta_df
34
+ self.img_size = img_size
35
+ self.transforms = transforms(img_size)
36
+ self.img_col = img_col
37
+ self.label_col = label_col
38
+ self.pair_col = pair_col
39
+ self.return_label = return_label
40
+
41
+ def __len__(self):
42
+ return len(self.meta_df)
43
+
44
+ def _load_image(self, path: str):
45
+ img_bgr = cv2.imread(path)
46
+ if img_bgr is None:
47
+ raise FileNotFoundError(f"Cannot read image: {path}")
48
+
49
+ img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
50
+
51
+ return img_rgb
52
+
53
+ def __getitem__(self, idx):
54
+ row = self.meta_df.iloc[idx]
55
+ img_path = str(row[self.img_col])
56
+
57
+ img = self._load_image(img_path)
58
+ img = self.transforms(image=img)['image']
59
+
60
+ if not self.return_label:
61
+ return img
62
+ else:
63
+ label = row[self.label_col]
64
+ return img, torch.tensor(label, dtype=torch.float32), str(row[self.pair_col])
65
+
66
+ def __str__(self):
67
+ return f"DeepClipDataset with {len(self.meta_df)} samples"
@@ -0,0 +1,44 @@
1
+ import pandas as pd
2
+ from typing import Optional
3
+
4
+ def class_imbalance_handle(
5
+ real_df: pd.DataFrame,
6
+ fake_df: pd.DataFrame,
7
+ k: int,
8
+ seed: int = 2025):
9
+ """
10
+ Constructs a balanced training dataset by combining real data with a specific slice of fake data.
11
+
12
+ This function implements a sliding-window approach to select synthetic (fake) samples
13
+ to match the count of real samples. It ensures a 1:1 class distribution for
14
+ iterative training or cross-validation scenarios.
15
+
16
+ Args:
17
+ real_df (pd.DataFrame): The original/minority class dataframe.
18
+ fake_df (pd.DataFrame): The synthetic/majority class dataframe to sample from.
19
+ k (int): The fold index used to determine the starting position of the fake data slice.
20
+ seed (int, optional): Random seed for reproducibility during sampling and shuffling. Defaults to 2025.
21
+
22
+ """
23
+
24
+ n_real = len(real_df)
25
+ start_idx = k * n_real
26
+ end_idx = start_idx + n_real
27
+
28
+ # Handle wrap-around if the slice exceeds the number of fake samples
29
+ if end_idx > len(fake_df):
30
+ remaining = fake_df[start_idx:]
31
+ n_remaining = len(remaining)
32
+
33
+ # Sample additional fake data from the start to match the required slice
34
+ extra = fake_df[:start_idx].sample(n=n_real - n_remaining, replace=False, random_state=seed)
35
+ fake_slice = pd.concat([remaining, extra], axis=0).reset_index(drop=True)
36
+
37
+ else:
38
+ fake_slice = fake_df[start_idx:end_idx].reset_index(drop=True)
39
+
40
+ # Combine real and fake samples, then shuffle
41
+ train_df = pd.concat([real_df, fake_slice], ignore_index=True)
42
+ train_df = train_df.sample(frac=1, random_state=seed).reset_index(drop=True)
43
+
44
+ return train_df
@@ -0,0 +1,83 @@
1
+ import pandas as pd
2
+ from sklearn.model_selection import train_test_split, GroupKFold
3
+
4
+ def split_data(
5
+ meta_df: pd.DataFrame,
6
+ seed: int = 2025,
7
+ label_col: str = 'label',
8
+ ori_vid: str = 'ori_vid',
9
+ dataset: str = 'kodf',
10
+ group_col: str = "pid",
11
+ test_size: float = 0.2,
12
+ balance_val: bool = True,
13
+ debug: bool = False,
14
+ debug_ratio: float = 0.1,
15
+ ) :
16
+ """Splits the dataset into training and validation sets based on original video IDs.
17
+
18
+ This function ensures that all fake videos derived from the same original video
19
+ stay within the same split (Train or Val) to prevent data leakage. It first
20
+ splits the real videos and then maps the corresponding fake videos.
21
+
22
+ Args:
23
+ meta_df: The complete metadata DataFrame containing labels and video IDs.
24
+ seed: Random seed for reproducibility. Defaults to 2025.
25
+ label_col: Name of the column containing labels (0 for REAL, 1 for FAKE).
26
+ ori_vid: Name of the column identifying the original video source.
27
+ test_size: Proportion of the dataset to include in the validation split.
28
+ balance_val: If True, samples the fake validation set to match the
29
+ length of the real validation set.
30
+ debug: If True, downsamples the final datasets for quick testing.
31
+ debug_ratio: The ratio of data to keep when debug mode is enabled.
32
+
33
+ Returns:
34
+ tuple[pd.DataFrame, pd.DataFrame]: A tuple containing (train_df, valid_df).
35
+ """
36
+
37
+ # Split real and fake(0: REAL, 1: FAKE)
38
+
39
+ assert meta_df[label_col].dtype in [int, float], "You need to label encoding {'FAKE': 1, 'REAL': 0}"
40
+
41
+ df_real = meta_df[meta_df[label_col] == 0].reset_index(drop=True)
42
+ df_fake = meta_df[meta_df[label_col] == 1].reset_index(drop=True)
43
+
44
+ # Split real Images
45
+ if dataset.lower() != "kodf":
46
+ real_train, real_val = train_test_split(df_real, test_size=test_size, random_state=seed, shuffle=True)
47
+ else:
48
+ df_real_shuffled = df_real.sample(frac=1, random_state=seed).reset_index(drop=True)
49
+
50
+ n_splits = int(1 / test_size)
51
+ gkf = GroupKFold(n_splits=n_splits)
52
+
53
+ splits = list(gkf.split(df_real_shuffled, groups=df_real_shuffled[group_col]))
54
+ train_idx, val_idx = splits[0]
55
+
56
+ real_train = df_real_shuffled.iloc[train_idx]
57
+ real_val = df_real_shuffled.iloc[val_idx]
58
+
59
+ # Match fake images to corresponding real train/val
60
+ fake_train = df_fake[df_fake[ori_vid].isin(real_train[ori_vid].unique())].reset_index(drop=True)
61
+ fake_val = df_fake[df_fake[ori_vid].isin(real_val[ori_vid].unique())].reset_index(drop=True)
62
+
63
+ # Sample fake_val to match real_val length
64
+ if balance_val:
65
+ fake_val = fake_val.sample(len(real_val), random_state=seed)
66
+
67
+ # Concatenate real + fake
68
+ train_df = pd.concat([real_train, fake_train], axis=0)
69
+ valid_df = pd.concat([real_val, fake_val], axis=0)
70
+
71
+ # Debug mode: downsample dataset
72
+ if debug:
73
+ n_train = max(1, int(len(train_df) * debug_ratio))
74
+ n_valid = max(1, int(len(valid_df) * debug_ratio))
75
+ train_df = train_df.sample(n=n_train, random_state=seed).reset_index(drop=True)
76
+ valid_df = valid_df.sample(n=n_valid, random_state=seed).reset_index(drop=True)
77
+
78
+
79
+ # Shuffle final datasets
80
+ train_df = train_df.sample(frac=1, random_state=seed).reset_index(drop=True)
81
+ valid_df = valid_df.sample(frac=1, random_state=seed).reset_index(drop=True)
82
+
83
+ return train_df, valid_df
@@ -0,0 +1,16 @@
1
+ from .template import (
2
+ REGIONS,
3
+ TYPES,
4
+ PAIR_ORDER,
5
+ REAL_TEXT,
6
+ parse_pair_label,
7
+ pair_frequency,
8
+ region_frequency,
9
+ type_weights,
10
+ region_weights,
11
+ sample_selection,
12
+ render_caption,
13
+ all_captions,
14
+ single_pair_caption,
15
+ )
16
+ from .tokenizer import tokenize
@@ -0,0 +1,133 @@
1
+ """
2
+ Face forgery text template.
3
+
4
+ Every rule about captions is kept in this file: the order of the 20 (region, type) pairs of `pair_label`
5
+ (also used by preprocess/raw_annotation.py), the caption phrases, the sampling of the pairs described by a
6
+ caption and the list of every caption a model can see (the rows of its text table).
7
+ """
8
+
9
+ import numpy as np
10
+
11
+ REGIONS = ("face", "eyes", "nose", "mouth")
12
+ TYPES = ("blur", "color", "texture", "structure", "blend")
13
+ # Order of the 20 characters of pair_label: index = 5 * region + type
14
+ PAIR_ORDER = [(region, type_) for region in REGIONS for type_ in TYPES]
15
+
16
+ REGION_PHRASES = {
17
+ "face": "the whole face",
18
+ "eyes": "the eyes",
19
+ "nose": "the nose",
20
+ "mouth": "the mouth",
21
+ }
22
+ TYPE_PHRASES = {
23
+ "blur": "unnatural blur",
24
+ "color": "unnatural color variations",
25
+ "texture": "unusual texture patterns",
26
+ "structure": "structural distortion",
27
+ "blend": "blending artifacts",
28
+ }
29
+
30
+ # REAL caption of the loss, also the caption of the real/fake score (p_fake = 1 - p_real)
31
+ REAL_TEXT = "a photo of a real face."
32
+
33
+ def parse_pair_label(pair_label: str) -> np.ndarray:
34
+ """'0101...' (20 characters) -> (4, 5) bool array"""
35
+
36
+ if len(pair_label) != len(PAIR_ORDER):
37
+ raise ValueError(f"pair_label must have {len(PAIR_ORDER)} characters: {pair_label!r}")
38
+
39
+ return (np.frombuffer(pair_label.encode(), dtype=np.uint8) == ord("1")).reshape(len(REGIONS), len(TYPES))
40
+
41
+ def _stack(pair_labels) -> np.ndarray:
42
+ return np.stack([parse_pair_label(s) for s in pair_labels])
43
+
44
+ def pair_frequency(pair_labels) -> np.ndarray:
45
+ """
46
+ pair_label of the FAKE frames -> (4, 5) frequency.
47
+ freq(region, type) = frames where the type flag of the region is on / frames where the region is forged
48
+ """
49
+
50
+ labels = _stack(pair_labels)
51
+ forged = labels.any(axis=2).sum(axis=0)
52
+
53
+ return labels.sum(axis=0) / np.maximum(forged, 1)[:, None]
54
+
55
+ def region_frequency(pair_labels) -> np.ndarray:
56
+ """pair_label of the FAKE frames -> (4,) ratio of the frames where each region is forged"""
57
+
58
+ return _stack(pair_labels).any(axis=2).mean(axis=0)
59
+
60
+ def type_weights(freq: np.ndarray, alpha: float) -> np.ndarray:
61
+ """(4, 5) w = freq^-alpha (0 for a pair that never appears)"""
62
+
63
+ return np.where(freq > 0, np.power(np.maximum(freq, 1e-12), -alpha), 0.0)
64
+
65
+ def region_weights(freq: np.ndarray, beta: float) -> np.ndarray:
66
+ """(4,) rw = freq^-beta (0 for a region that is never forged)"""
67
+
68
+ return np.where(freq > 0, np.power(np.maximum(freq, 1e-12), -beta), 0.0)
69
+
70
+ def _choice(rng: np.random.Generator, items: np.ndarray, size: int, weights: np.ndarray) -> np.ndarray:
71
+ """
72
+ Weighted choice without replacement (uniform when every weight is 0).
73
+ An item with weight 0 (a pair or region never seen in the train FAKE frames) gets the smallest nonzero
74
+ weight, otherwise there can be fewer nonzero weights than size and rng.choice fails.
75
+ """
76
+
77
+ nonzero = weights > 0
78
+ if not nonzero.all():
79
+ weights = np.where(nonzero, weights, weights[nonzero].min() if nonzero.any() else 1.0)
80
+
81
+ return rng.choice(items, size=size, replace=False, p=weights / weights.sum())
82
+
83
+ def sample_selection(label: np.ndarray, weights: np.ndarray, region_weights: np.ndarray,
84
+ rng: np.random.Generator) -> np.ndarray:
85
+ """
86
+ Chooses the (region, type) pair described by the caption of one FAKE image in this step.
87
+
88
+ 1. One region is chosen among the forged regions with the probability region_weights[region]
89
+ (normalized among them).
90
+ 2. One type is chosen among the types which are on in that region with the probability
91
+ weights[region, type] (normalized among them).
92
+
93
+ label: (4, 5) bool, the pairs which are on in the image
94
+ return: (4, 5) bool with one True, always a subset of label
95
+ """
96
+
97
+ active_regions = np.flatnonzero(label.any(axis=1))
98
+
99
+ if len(active_regions) == 0:
100
+ raise ValueError("A FAKE image needs at least one pair which is on")
101
+
102
+ selection = np.zeros_like(label, dtype=bool)
103
+
104
+ region = _choice(rng, active_regions, 1, region_weights[active_regions])[0]
105
+ active_types = np.flatnonzero(label[region])
106
+ type_ = _choice(rng, active_types, 1, weights[region, active_types])[0]
107
+ selection[region, type_] = True
108
+
109
+ return selection
110
+
111
+ def render_caption(selection: np.ndarray) -> str:
112
+ """Chosen pair ((4, 5) bool with one True) -> caption. No pair gives REAL_TEXT."""
113
+
114
+ pairs = np.argwhere(selection)
115
+
116
+ if len(pairs) == 0:
117
+ return REAL_TEXT
118
+ if len(pairs) != 1:
119
+ raise ValueError(f"A caption describes one (region, type) pair, not {len(pairs)}")
120
+
121
+ region, type_ = pairs[0]
122
+ return f"a photo of a fake face with {TYPE_PHRASES[TYPES[type_]]} in {REGION_PHRASES[REGIONS[region]]}."
123
+
124
+ def all_captions() -> list:
125
+ """Every caption the collate can make, in a fixed order: REAL_TEXT, then the 20 captions of PAIR_ORDER"""
126
+
127
+ return [REAL_TEXT] + [single_pair_caption(region, type_) for region, type_ in PAIR_ORDER]
128
+
129
+ def single_pair_caption(region: str, type_: str) -> str:
130
+ selection = np.zeros((len(REGIONS), len(TYPES)), dtype=bool)
131
+ selection[REGIONS.index(region), TYPES.index(type_)] = True
132
+
133
+ return render_caption(selection)