SiemonCha commited on
Commit
dda3973
·
1 Parent(s): 53ec840

add generator type detection

Browse files
app.py CHANGED
@@ -1,7 +1,9 @@
1
  import gradio as gr
2
  import torch
 
 
3
  from PIL import Image
4
- from src.models.inference import load_model, predict
5
  from src.services.metadata_checker import get_metadata
6
  from src.services.gradcam import generate_gradcam
7
  import tempfile
@@ -9,8 +11,12 @@ import os
9
 
10
  # Load model once
11
  model = load_model()
 
12
 
13
  def analyze_image(image):
 
 
 
14
  # Save PIL image to temp file
15
  with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
16
  image.save(tmp.name)
@@ -21,17 +27,25 @@ def analyze_image(image):
21
  metadata = get_metadata(tmp_path)
22
  cam_array = generate_gradcam(tmp_path, model=model)
23
  cam_image = Image.fromarray(cam_array)
 
24
  finally:
25
  os.remove(tmp_path)
26
 
27
  label = prediction["label"]
28
  confidence = prediction["confidence"]
29
- raw_score = prediction["raw_score"]
30
 
31
  result_text = f"**{label}** — Confidence: {confidence}%\n\n"
32
- result_text += f"Raw Score: {raw_score}\n\n"
33
  result_text += f"⚠️ This is a model-based estimate, not definitive proof.\n\n"
34
- result_text += f"**Metadata:**\n"
 
 
 
 
 
 
 
 
 
35
  result_text += f"- Format: {metadata['format']}\n"
36
  result_text += f"- Dimensions: {metadata['dimensions']}\n"
37
  result_text += f"- File Size: {metadata['file_size_kb']} KB\n"
 
1
  import gradio as gr
2
  import torch
3
+ import base64
4
+ import io
5
  from PIL import Image
6
+ from src.models.inference import load_model, predict, predict_generator, load_generator_model
7
  from src.services.metadata_checker import get_metadata
8
  from src.services.gradcam import generate_gradcam
9
  import tempfile
 
11
 
12
  # Load model once
13
  model = load_model()
14
+ generator_model = load_generator_model()
15
 
16
  def analyze_image(image):
17
+ if image is None:
18
+ return "Please upload an image.", None
19
+
20
  # Save PIL image to temp file
21
  with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
22
  image.save(tmp.name)
 
27
  metadata = get_metadata(tmp_path)
28
  cam_array = generate_gradcam(tmp_path, model=model)
29
  cam_image = Image.fromarray(cam_array)
30
+ generator_result = predict_generator(tmp_path, model=generator_model)
31
  finally:
32
  os.remove(tmp_path)
33
 
34
  label = prediction["label"]
35
  confidence = prediction["confidence"]
 
36
 
37
  result_text = f"**{label}** — Confidence: {confidence}%\n\n"
 
38
  result_text += f"⚠️ This is a model-based estimate, not definitive proof.\n\n"
39
+
40
+ # Generator type
41
+ gen_type = generator_result["generator_type"]
42
+ gen_conf = generator_result["confidence"]
43
+ result_text += f"**Generator Type:** {gen_type} ({gen_conf}%)\n\n"
44
+ result_text += "**Class Probabilities:**\n"
45
+ for cls, prob in generator_result["class_probabilities"].items():
46
+ result_text += f"- {cls}: {prob}%\n"
47
+
48
+ result_text += f"\n**Metadata:**\n"
49
  result_text += f"- Format: {metadata['format']}\n"
50
  result_text += f"- Dimensions: {metadata['dimensions']}\n"
51
  result_text += f"- File Size: {metadata['file_size_kb']} KB\n"
saved_models/generator_model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5bda82f8d7eb4b25248e53aae79dedf7b344440fea165845cf04fded0d692770
3
+ size 45314775
src/data/generator_loader.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pandas as pd
3
+ import random
4
+ from PIL import Image
5
+ from torch.utils.data import Dataset, DataLoader, random_split
6
+ from src.data.transforms import train_transforms, val_transforms
7
+
8
+ DATASET_ROOT = "/Users/siemoncha/Desktop/CS/datasets/artifact-dataset"
9
+
10
+ # Manual class mapping based on architecture knowledge
11
+ SOURCE_CLASS_MAP = {
12
+ # Class 0 - Real
13
+ "coco": 0,
14
+ "ffhq": 0,
15
+ "lsun": 0,
16
+ "imagenet": 0,
17
+ "landscape": 0,
18
+ "afhq": 0,
19
+ "celebahq": 0,
20
+ "metfaces": 0,
21
+
22
+ # Class 1 - GAN
23
+ "stylegan1": 1,
24
+ "stylegan2": 1,
25
+ "stylegan3": 1,
26
+ "pro_gan": 1,
27
+ "big_gan": 1,
28
+ "star_gan": 1,
29
+ "cycle_gan": 1,
30
+ "gansformer": 1,
31
+ "generative_inpainting": 1,
32
+ "lama": 1,
33
+ "mat": 1,
34
+ "sfhq": 1,
35
+ "cips": 1,
36
+ "projected_gan": 1,
37
+ "gau_gan": 1,
38
+
39
+ # Class 2 - Diffusion
40
+ "stable_diffusion": 2,
41
+ "ddpm": 2,
42
+ "glide": 2,
43
+ "latent_diffusion": 2,
44
+ "vq_diffusion": 2,
45
+ "denoising_diffusion_gan": 2,
46
+ "diffusion_gan": 2,
47
+ "palette": 2,
48
+
49
+ # Class 3 - Other
50
+ "taming_transformer": 3,
51
+ "face_synthetics": 3,
52
+ }
53
+
54
+ CLASS_NAMES = {0: "Real", 1: "GAN", 2: "Diffusion", 3: "Other"}
55
+ MAX_PER_CLASS = 10000
56
+
57
+
58
+ class GeneratorDataset(Dataset):
59
+ def __init__(self, samples, transform=None):
60
+ self.samples = samples
61
+ self.transform = transform
62
+
63
+ def __len__(self):
64
+ return len(self.samples)
65
+
66
+ def __getitem__(self, idx):
67
+ img_path, label = self.samples[idx]
68
+ image = Image.open(img_path).convert("RGB")
69
+ if self.transform:
70
+ image = self.transform(image)
71
+ return image, label
72
+
73
+
74
+ def load_generator_samples():
75
+ class_samples = {0: [], 1: [], 2: [], 3: []}
76
+
77
+ for source, cls in SOURCE_CLASS_MAP.items():
78
+ csv_path = os.path.join(DATASET_ROOT, source, "metadata.csv")
79
+ if not os.path.exists(csv_path):
80
+ print(f"Skipping {source} - no metadata.csv")
81
+ continue
82
+ df = pd.read_csv(csv_path)
83
+ for _, row in df.iterrows():
84
+ img_path = os.path.join(DATASET_ROOT, source, row["image_path"])
85
+ class_samples[cls].append((img_path, cls))
86
+
87
+ # Balance classes
88
+ for cls in class_samples:
89
+ class_samples[cls] = class_samples[cls][:MAX_PER_CLASS]
90
+ print(f"Class {cls} ({CLASS_NAMES[cls]}): {len(class_samples[cls])} samples")
91
+
92
+ all_samples = []
93
+ for cls in class_samples:
94
+ all_samples.extend(class_samples[cls])
95
+
96
+ print(f"Total: {len(all_samples)}")
97
+ return all_samples
98
+
99
+
100
+ def get_generator_dataloaders(batch_size=32):
101
+ all_samples = load_generator_samples()
102
+
103
+ # Shuffle before splitting
104
+ random.shuffle(all_samples)
105
+
106
+ train_size = int(0.75 * len(all_samples))
107
+ val_size = int(0.125 * len(all_samples))
108
+ test_size = len(all_samples) - train_size - val_size
109
+
110
+ train_samples = all_samples[:train_size]
111
+ val_samples = all_samples[train_size:train_size + val_size]
112
+ test_samples = all_samples[train_size + val_size:]
113
+
114
+ train_set = GeneratorDataset(train_samples, transform=train_transforms)
115
+ val_set = GeneratorDataset(val_samples, transform=val_transforms)
116
+ test_set = GeneratorDataset(test_samples, transform=val_transforms)
117
+
118
+ train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True, num_workers=2)
119
+ val_loader = DataLoader(val_set, batch_size=batch_size, shuffle=False, num_workers=2)
120
+ test_loader = DataLoader(test_set, batch_size=batch_size, shuffle=False, num_workers=2)
121
+
122
+ return train_loader, val_loader, test_loader
src/models/inference.py CHANGED
@@ -2,6 +2,7 @@ import torch
2
  from PIL import Image
3
  from src.models.model import build_model
4
  from src.data.transforms import val_transforms
 
5
 
6
  MODEL_PATH = "saved_models/best_model.pth"
7
 
@@ -31,6 +32,38 @@ def predict(image_path: str, model=None):
31
  "raw_score": round(prob, 4)
32
  }
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  if __name__ == "__main__":
35
  import sys
36
  if len(sys.argv) < 2:
 
2
  from PIL import Image
3
  from src.models.model import build_model
4
  from src.data.transforms import val_transforms
5
+ from src.data.generator_loader import CLASS_NAMES
6
 
7
  MODEL_PATH = "saved_models/best_model.pth"
8
 
 
32
  "raw_score": round(prob, 4)
33
  }
34
 
35
+ GENERATOR_MODEL_PATH = "saved_models/generator_model.pth"
36
+
37
+ def load_generator_model(model_path=GENERATOR_MODEL_PATH):
38
+ from src.models.train_generator import build_multiclass_model
39
+ model = build_multiclass_model(num_classes=4, pretrained=False)
40
+ model.load_state_dict(torch.load(model_path, map_location="cpu"))
41
+ model.eval()
42
+ return model
43
+
44
+ def predict_generator(image_path: str, model=None):
45
+ if model is None:
46
+ model = load_generator_model()
47
+
48
+ image = Image.open(image_path).convert("RGB")
49
+ tensor = val_transforms(image).unsqueeze(0)
50
+
51
+ with torch.no_grad():
52
+ output = model(tensor)
53
+ probs = torch.softmax(output, dim=1)[0]
54
+ pred_class = probs.argmax().item()
55
+ confidence = probs[pred_class].item()
56
+
57
+ return {
58
+ "generator_type": CLASS_NAMES[pred_class],
59
+ "confidence": round(confidence * 100, 2),
60
+ "class_probabilities": {
61
+ CLASS_NAMES[i]: round(probs[i].item() * 100, 2)
62
+ for i in range(4)
63
+ }
64
+ }
65
+
66
+
67
  if __name__ == "__main__":
68
  import sys
69
  if len(sys.argv) < 2:
src/models/train_generator.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ from torch.optim import Adam
4
+ from torch.optim.lr_scheduler import ReduceLROnPlateau
5
+ from torchvision import models
6
+ from sklearn.metrics import classification_report, confusion_matrix
7
+ from src.data.generator_loader import get_generator_dataloaders, CLASS_NAMES
8
+
9
+
10
+ def build_multiclass_model(num_classes=4, pretrained=True):
11
+ model = models.resnet18(weights="IMAGENET1K_V1" if pretrained else None)
12
+
13
+ for param in model.parameters():
14
+ param.requires_grad = False
15
+
16
+ # Unfreeze layer4
17
+ for name, param in model.named_parameters():
18
+ if "layer4" in name or "fc" in name:
19
+ param.requires_grad = True
20
+
21
+ in_features = model.fc.in_features
22
+ model.fc = nn.Sequential(
23
+ nn.Dropout(0.5),
24
+ nn.Linear(in_features, 256),
25
+ nn.ReLU(),
26
+ nn.Dropout(0.3),
27
+ nn.Linear(256, num_classes)
28
+ )
29
+ return model
30
+
31
+
32
+ def evaluate(model, loader, device):
33
+ model.eval()
34
+ all_preds, all_labels = [], []
35
+
36
+ with torch.no_grad():
37
+ for images, labels in loader:
38
+ images = images.to(device)
39
+ labels = labels.to(device)
40
+ outputs = model(images)
41
+ preds = outputs.argmax(dim=1)
42
+ all_preds.extend(preds.cpu().numpy())
43
+ all_labels.extend(labels.cpu().numpy())
44
+
45
+ return all_preds, all_labels
46
+
47
+
48
+ def train(epochs=15, batch_size=32, lr=1e-4):
49
+ device = torch.device("mps" if torch.backends.mps.is_available() else "cpu")
50
+ print(f"Using device: {device}")
51
+
52
+ train_loader, val_loader, test_loader = get_generator_dataloaders(batch_size=batch_size)
53
+ model = build_multiclass_model().to(device)
54
+
55
+ criterion = nn.CrossEntropyLoss()
56
+ optimizer = Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=lr)
57
+ scheduler = ReduceLROnPlateau(optimizer, patience=2)
58
+
59
+ best_val_acc = 0
60
+ early_stop_patience = 3
61
+ no_improve_count = 0
62
+
63
+ for epoch in range(epochs):
64
+ model.train()
65
+ train_loss, correct, total = 0, 0, 0
66
+
67
+ for images, labels in train_loader:
68
+ images = images.to(device)
69
+ labels = labels.to(device)
70
+
71
+ optimizer.zero_grad()
72
+ outputs = model(images)
73
+ loss = criterion(outputs, labels)
74
+ loss.backward()
75
+ optimizer.step()
76
+
77
+ train_loss += loss.item()
78
+ preds = outputs.argmax(dim=1)
79
+ correct += (preds == labels).sum().item()
80
+ total += labels.size(0)
81
+
82
+ train_acc = correct / total
83
+ avg_train_loss = train_loss / len(train_loader)
84
+
85
+ val_preds, val_labels = evaluate(model, val_loader, device)
86
+ val_acc = sum(p == l for p, l in zip(val_preds, val_labels)) / len(val_labels)
87
+ scheduler.step(1 - val_acc)
88
+
89
+ print(f"Epoch {epoch+1}/{epochs} | "
90
+ f"Train Loss: {avg_train_loss:.4f} | Train Acc: {train_acc:.4f} | "
91
+ f"Val Acc: {val_acc:.4f}")
92
+
93
+ if val_acc > best_val_acc:
94
+ best_val_acc = val_acc
95
+ no_improve_count = 0
96
+ torch.save(model.state_dict(), "saved_models/generator_model.pth")
97
+ print(f" -> Best model saved")
98
+ else:
99
+ no_improve_count += 1
100
+ if no_improve_count >= early_stop_patience:
101
+ print(f"Early stopping at epoch {epoch+1}")
102
+ break
103
+
104
+ # Final evaluation
105
+ print("\n--- Final Evaluation ---")
106
+ test_preds, test_labels = evaluate(model, test_loader, device)
107
+ test_acc = sum(p == l for p, l in zip(test_preds, test_labels)) / len(test_labels)
108
+ print(f"Test Accuracy: {test_acc:.4f}")
109
+ print("\nClassification Report:")
110
+ print(classification_report(test_labels, test_preds,
111
+ target_names=list(CLASS_NAMES.values())))
112
+ print("Confusion Matrix:")
113
+ print(confusion_matrix(test_labels, test_preds))
114
+
115
+
116
+ if __name__ == "__main__":
117
+ train()