MorphGuard / scripts /train_demorpher.py
juanquy's picture
Initial clean commit of modular MorphGuard
2978bba
Raw
History Blame Contribute Delete
10.5 kB
#!/usr/bin/env python3
"""
scripts/train_demorpher.py
Training script for the MorphGuard demorpher models:
1. Transformer-based demorpher (default)
2. GAN-based demorpher (with the --gan flag)
Usage for transformer-based demorpher:
python scripts/train_demorpher.py \
--data-dir data \
--epochs 20 \
--batch-size 16 \
--lr 1e-4 \
--num-pairs 10000 \
--save-path models/demorpher.pth \
[--gpus 1] [--num-workers 4]
Usage for GAN-based demorpher:
python scripts/train_demorpher.py \
--gan \
--data-dir data \
--epochs 30 \
--batch-size 4 \
--lr 1e-4 \
--model-type toonify \
--save-path models/demorpher_gan.pth
"""
import os
import sys
# ensure project root is on PYTHONPATH so imports like morphguard_api work
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
import argparse
import random
from PIL import Image
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms
# Import the demorpher module definition
from morphguard_api import DemorphingModule
class DemorphDataset(Dataset):
"""Dataset of randomly synthesized morph pairs from real face images."""
def __init__(self, data_dir, split='train', transform_input=None, transform_target=None, num_pairs=10000):
self.real_dir = os.path.join(data_dir, split, 'real')
self.files = [f for f in os.listdir(self.real_dir)
if f.lower().endswith(('.png', '.jpg', '.jpeg'))]
if len(self.files) < 2:
raise RuntimeError(f"Not enough real images in {self.real_dir} to generate morphs")
self.transform_input = transform_input
self.transform_target = transform_target
self.num_pairs = num_pairs
def __len__(self):
return self.num_pairs
def __getitem__(self, idx):
# Sample two distinct real images
a, b = random.sample(self.files, 2)
path_a = os.path.join(self.real_dir, a)
path_b = os.path.join(self.real_dir, b)
img_a = Image.open(path_a).convert('RGB').resize((224, 224))
img_b = Image.open(path_b).convert('RGB').resize((224, 224))
# Create morph by averaging pixel values
arr_a = np.array(img_a).astype(np.float32)
arr_b = np.array(img_b).astype(np.float32)
morph_arr = ((arr_a + arr_b) / 2.0).astype(np.uint8)
img_morph = Image.fromarray(morph_arr)
# Apply transforms
if self.transform_input:
morph_tensor = self.transform_input(img_morph)
ref_tensor = self.transform_input(img_a)
else:
morph_tensor = transforms.ToTensor()(img_morph)
ref_tensor = transforms.ToTensor()(img_a)
if self.transform_target:
target_tensor = self.transform_target(img_b)
else:
target_tensor = transforms.ToTensor()(img_b)
return morph_tensor, ref_tensor, target_tensor
def train_demorpher(args):
# Determine device
use_cuda = args.gpus > 0 and torch.cuda.is_available()
device = torch.device('cuda' if use_cuda else 'cpu')
# Define transforms for inputs and targets
transform_input = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
transform_target = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor()
])
# Prepare datasets and loaders
train_ds = DemorphDataset(
args.data_dir, split='train',
transform_input=transform_input,
transform_target=transform_target,
num_pairs=args.num_pairs
)
val_pairs = max(1, int(args.num_pairs * args.val_ratio))
val_ds = DemorphDataset(
args.data_dir, split='val',
transform_input=transform_input,
transform_target=transform_target,
num_pairs=val_pairs
)
train_loader = DataLoader(
train_ds, batch_size=args.batch_size, shuffle=True,
num_workers=args.num_workers, pin_memory=use_cuda
)
val_loader = DataLoader(
val_ds, batch_size=args.batch_size, shuffle=False,
num_workers=args.num_workers, pin_memory=use_cuda
)
# Initialize model, loss, optimizer
model = DemorphingModule().to(device)
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=args.lr)
best_val_loss = float('inf')
# Setup statistics logging
import json # noqa: E402
stats = {'epochs': [], 'train_loss': [], 'val_loss': []}
# Training loop
for epoch in range(1, args.epochs + 1):
model.train()
total_train = 0.0
for morph, ref, target in train_loader:
morph = morph.to(device)
ref = ref.to(device)
target = target.to(device)
optimizer.zero_grad()
output = model(morph, ref)
loss = criterion(output, target)
loss.backward()
optimizer.step()
total_train += loss.item() * morph.size(0)
avg_train = total_train / len(train_loader.dataset)
model.eval()
total_val = 0.0
with torch.no_grad():
for morph, ref, target in val_loader:
morph = morph.to(device)
ref = ref.to(device)
target = target.to(device)
output = model(morph, ref)
loss = criterion(output, target)
total_val += loss.item() * morph.size(0)
avg_val = total_val / len(val_loader.dataset)
print(f"Epoch {epoch}/{args.epochs} | train_loss={avg_train:.4f} | val_loss={avg_val:.4f}")
# Update and write stats
stats['epochs'].append(epoch)
stats['train_loss'].append(avg_train)
stats['val_loss'].append(avg_val)
try:
with open(args.stats_file, 'w') as sf:
json.dump(stats, sf, indent=2)
except Exception as e:
print(f"Warning: could not write stats file: {e}")
# Save best model
if avg_val < best_val_loss:
best_val_loss = avg_val
os.makedirs(os.path.dirname(args.save_path), exist_ok=True)
torch.save(model.state_dict(), args.save_path)
print(f"Saved best demorpher model to {args.save_path} (val_loss={avg_val:.4f})")
def parse_args():
parser = argparse.ArgumentParser(description='Train MorphGuard Demorpher')
# Common parameters
parser.add_argument('--data-dir', type=str, default='data', help='Root data directory')
parser.add_argument('--epochs', type=int, default=20, help='Number of training epochs')
parser.add_argument('--batch-size', type=int, default=16, help='Batch size')
parser.add_argument('--lr', type=float, default=1e-4, help='Learning rate')
parser.add_argument('--gpus', type=int, default=1, help='GPUs to use (0 for CPU)')
parser.add_argument('--num-workers', type=int, default=4, help='DataLoader workers')
parser.add_argument('--save-path', type=str, default='models/demorpher.pth', help='Path to save trained model')
parser.add_argument('--stats-file', type=str, default='training_demorpher_stats.json', help='JSON file to write metrics')
# Parameters for transformer-based model
parser.add_argument('--num-pairs', type=int, default=10000, help='Number of morph pairs per epoch (transformer model)')
parser.add_argument('--val-ratio', type=float, default=0.1, help='Fraction of pairs for validation (transformer model)')
# Parameters for GAN-based model
parser.add_argument('--gan', action='store_true', help='Train GAN-based demorpher instead of transformer-based')
parser.add_argument('--model-type', type=str, default='toonify',
choices=['toonify', 'frontalization', 'superresolution', 'sketch_to_face', 'inversion'],
help='GAN model type to train (when using --gan)')
parser.add_argument('--checkpoint-path', type=str, default=None,
help='Path to specific checkpoint for GAN fine-tuning (when using --gan)')
parser.add_argument('--output-dir', type=str, default='models/demorpher',
help='Directory to save GAN checkpoints and logs (when using --gan)')
return parser.parse_args()
if __name__ == '__main__':
args = parse_args()
# If using GAN-based demorpher, use a different training function
if args.gan:
try:
# Import GAN training modules
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from plugins.gan_demorpher import GanDemorpher, AVAILABLE_MODEL_TYPES
# Before importing pSp Coach, check if pSp is available
try:
# Import pSp modules
psp_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'pSp'))
sys.path.insert(0, psp_path)
from pSp.training.coach import Coach
from pSp.options.train_options import TrainOptions
# Import the GAN training functionality from a separate module
import importlib.util
spec = importlib.util.spec_from_file_location(
"train_gan_demorpher",
os.path.abspath(os.path.join(os.path.dirname(__file__), 'train_gan_demorpher.py'))
)
gan_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(gan_module)
# Use the GAN training function
print(f"Training GAN-based demorpher with model type: {args.model_type}")
gan_module.train_gan_demorpher(args)
except ImportError as e:
print(f"Error: pixel2style2pixel (pSp) not found or not properly configured: {e}")
print("Please ensure the pSp subdirectory is correctly set up for GAN-based training.")
sys.exit(1)
except ImportError as e:
print(f"Error: GAN demorpher module not available: {e}")
print("Falling back to transformer-based demorpher training.")
train_demorpher(args)
else:
# Use the regular transformer-based demorpher training
train_demorpher(args)