J-kY*12A!d48n$e8YD+L_kU(=?TL^0R
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
diff --git a/core/utils1/utils1/__pycache__/datasets.cpython-39.pyc b/core/utils1/utils1/__pycache__/datasets.cpython-39.pyc
deleted file mode 100644
index 47acb95ae8ed84ae14ff5313d42c002b97abd6bd..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 6022
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
diff --git a/core/utils1/utils1/__pycache__/eval.cpython-310.pyc b/core/utils1/utils1/__pycache__/eval.cpython-310.pyc
deleted file mode 100644
index ffcbe358dfb7dc8d180ee7c4a4d7b36342b88965..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 1851
zcmZ8h&1)Pt6qhvLvpZ|A<0KAE+^_Vor33;cg%X+?hnPT`mLw!}a2e#0yq>joW}?x$
zF=$TCz4YG0Zg0Kz+`mMpLQnn|LTURnyAGie^FE~C+trhP?^Q_>BN%rd{c(6FMCdQ;
zygOWQzJ{-P1%e@l1xlA1r?~UU)Ja{uT=P=DTMN>l)8RBqBiJL%5%IK__Bz`Y{b`aW
zU?(i#y+gu67QOb;0gHJwS?ktZ)?@wGZo1C!Q#2Z!!@r})=NRM&o8IoXj~_hT+kLe2
z^eMQ3ssBuoHcxIgZaGX0fU^N2JHjiESn%_ZaA`^xg5on5A2P5rhbcS2EBpS7@
zA`@rsYIovkydc`mJdGwk=zuvO!%jvF5LwK;F$UdJeZVJYnEA&<-m`Y=X^-J+2xM>#
zT}8})4|Uk2!wvu&96Ry_U`iv^taFxlbQYKk>O{RFGOnBj%plF^+
z3iy!D#@V4LYBOlKEU3s1sLGFcW8z6Y&Zb-)Rm?bo1N1=5WY!d86WXnpGb)Tzlxh@6
z-yX$lYEdZJv#0IO7Qky|@K&jJV8c>>(gpp75A@o6ZGq;=@O3-b(^UWT_(
zQ~n4@JHLld4d)v=T;NVe3%oif(V-3*J|@86=)z**u?y!}*UMe!?QUnSc8A?=wDLBf
zwn802)Fr*4FX?qXWYI5fyQw#U)$xkejv3HApGlgZW{rJpbCsTONyj{^CC`hdsLItw
zBC9;3vwZ1xyoVMr%q6FaXO_XDqKr$Zg03ax_3Yd>gYoj6|GYPHjkB}6Ysk)bsrMuU
zGxZ)=HM*3pHOGQeS>6I-%OY>&2M{Gcw8FAW4z2hYgz;1*^CROkep2LI-n47|qRHq9
zEd)If+yrHnjU{FBiVd>;#{@w0X;m=e^H++O%mjymDzJ>NDp5cQlCm})m6D#0F3b0A
zpsk(%AEP@#&*qvfla-$WD)mn@C1>1fxHV>6i+~!Oj*SOr;=;I)7fpkrgn};{6IfW0
ztJISj+<)pFT4fw72(3*Zc{3BL=?ZcwZrD6RD7ylFoPPpd^Err3+$SN~g4&Cr;%?#?
z<^XTtO-$Zg|L01)jkm#%fn5aK>HqEe@-769i1a``8x7%b(5GY{JTnBd7L^jk0hGG6
zjK9)OEtK)f*%VsbZNSQr4G6pExK);LgK(R9RUQ^&Y2V#=UEk;)U|IYAHx-)+-u?YM
jnc1czm+9Sw?f{6dY{zXjL4+u@2|{Xp(0#n_Y;FAq%>~*s
diff --git a/core/utils1/utils1/__pycache__/eval.cpython-38.pyc b/core/utils1/utils1/__pycache__/eval.cpython-38.pyc
deleted file mode 100644
index 5fede421ef691570d37b5ab60f1ea81f86a1b502..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 1847
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
diff --git a/core/utils1/utils1/__pycache__/trainer.cpython-38.pyc b/core/utils1/utils1/__pycache__/trainer.cpython-38.pyc
deleted file mode 100644
index 015428d5b713e55ae0c7ab413a4cd1e823afee47..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 5862
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
F{{Umvj#&Tz
diff --git a/core/utils1/utils1/__pycache__/utils.cpython-310.pyc b/core/utils1/utils1/__pycache__/utils.cpython-310.pyc
deleted file mode 100644
index e17d0762fb3a145c6ad8e8c1f7bd9cafd2c34aec..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 3658
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
diff --git a/core/utils1/utils1/__pycache__/warmup.cpython-310.pyc b/core/utils1/utils1/__pycache__/warmup.cpython-310.pyc
deleted file mode 100644
index ab707b7764aa20d3cb4e393148d4c860d8fb6f17..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 3040
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
diff --git a/core/utils1/utils1/config.py b/core/utils1/utils1/config.py
deleted file mode 100644
index e16101d..0000000
--- a/core/utils1/utils1/config.py
+++ /dev/null
@@ -1,157 +0,0 @@
-import argparse
-import os
-import sys
-from abc import ABC
-from typing import Type
-
-
-class DefaultConfigs(ABC):
- ####### base setting ######
- gpus = [0]
- seed = 3407
- arch = "resnet50"
- datasets = ["zhaolian_train"]
- datasets_test = ["adm_res_abs_ddim20s"]
- mode = "binary"
- class_bal = False
- batch_size = 64
- loadSize = 256
- cropSize = 224
- epoch = "latest"
- num_workers = 20
- serial_batches = False
- isTrain = True
-
- # data augmentation
- rz_interp = ["bilinear"]
- # blur_prob = 0.0
- blur_prob = 0.1
- blur_sig = [0.5]
- # jpg_prob = 0.0
- jpg_prob = 0.1
- jpg_method = ["cv2"]
- jpg_qual = [75]
- gray_prob = 0.0
- aug_resize = True
- aug_crop = True
- aug_flip = True
- aug_norm = True
-
- ####### train setting ######
- warmup = False
- # warmup = True
- warmup_epoch = 3
- earlystop = True
- earlystop_epoch = 5
- optim = "adam"
- new_optim = False
- loss_freq = 400
- save_latest_freq = 2000
- save_epoch_freq = 20
- continue_train = False
- epoch_count = 1
- last_epoch = -1
- nepoch = 400
- beta1 = 0.9
- lr = 0.0001
- init_type = "normal"
- init_gain = 0.02
- pretrained = True
-
- # paths information
- root_dir1 = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
- root_dir = os.path.dirname(root_dir1)
- dataset_root = os.path.join(root_dir, "data")
- exp_root = os.path.join(root_dir, "data", "exp")
- _exp_name = ""
- exp_dir = ""
- ckpt_dir = ""
- logs_path = ""
- ckpt_path = ""
-
- @property
- def exp_name(self):
- return self._exp_name
-
- @exp_name.setter
- def exp_name(self, value: str):
- self._exp_name = value
- self.exp_dir: str = os.path.join(self.exp_root, self.exp_name)
- self.ckpt_dir: str = os.path.join(self.exp_dir, "ckpt")
- self.logs_path: str = os.path.join(self.exp_dir, "logs.txt")
-
- os.makedirs(self.exp_dir, exist_ok=True)
- os.makedirs(self.ckpt_dir, exist_ok=True)
-
- def to_dict(self):
- dic = {}
- for fieldkey in dir(self):
- fieldvalue = getattr(self, fieldkey)
- if not fieldkey.startswith("__") and not callable(fieldvalue) and not fieldkey.startswith("_"):
- dic[fieldkey] = fieldvalue
- return dic
-
-
-def args_list2dict(arg_list: list):
- assert len(arg_list) % 2 == 0, f"Override list has odd length: {arg_list}; it must be a list of pairs"
- return dict(zip(arg_list[::2], arg_list[1::2]))
-
-
-def str2bool(v: str) -> bool:
- if isinstance(v, bool):
- return v
- elif v.lower() in ("true", "yes", "on", "y", "t", "1"):
- return True
- elif v.lower() in ("false", "no", "off", "n", "f", "0"):
- return False
- else:
- return bool(v)
-
-
-def str2list(v: str, element_type=None) -> list:
- if not isinstance(v, (list, tuple, set)):
- v = v.lstrip("[").rstrip("]")
- v = v.split(",")
- v = list(map(str.strip, v))
- if element_type is not None:
- v = list(map(element_type, v))
- return v
-
-
-CONFIGCLASS = Type[DefaultConfigs]
-
-parser = argparse.ArgumentParser()
-parser.add_argument("--gpus", default=[0], type=int, nargs="+")
-parser.add_argument("--exp_name", default="", type=str)
-parser.add_argument("--ckpt", default="model_epoch_latest.pth", type=str)
-parser.add_argument("opts", default=[], nargs=argparse.REMAINDER)
-args = parser.parse_args()
-
-if os.path.exists(os.path.join(DefaultConfigs.exp_root, args.exp_name, "config.py")):
- sys.path.insert(0, os.path.join(DefaultConfigs.exp_root, args.exp_name))
- from config import cfg
-
- cfg: CONFIGCLASS
-else:
- cfg = DefaultConfigs()
-
-if args.opts:
- opts = args_list2dict(args.opts)
- for k, v in opts.items():
- if not hasattr(cfg, k):
- raise ValueError(f"Unrecognized option: {k}")
- original_type = type(getattr(cfg, k))
- if original_type == bool:
- setattr(cfg, k, str2bool(v))
- elif original_type in (list, tuple, set):
- setattr(cfg, k, str2list(v, type(getattr(cfg, k)[0])))
- else:
- setattr(cfg, k, original_type(v))
-
-cfg.gpus: list = args.gpus
-os.environ["CUDA_VISIBLE_DEVICES"] = ", ".join([str(gpu) for gpu in cfg.gpus])
-cfg.exp_name = args.exp_name
-cfg.ckpt_path: str = os.path.join(cfg.ckpt_dir, args.ckpt)
-
-if isinstance(cfg.datasets, str):
- cfg.datasets = cfg.datasets.split(",")
diff --git a/core/utils1/utils1/datasets.py b/core/utils1/utils1/datasets.py
deleted file mode 100644
index a35863a..0000000
--- a/core/utils1/utils1/datasets.py
+++ /dev/null
@@ -1,178 +0,0 @@
-import os
-from io import BytesIO
-from random import choice, random
-
-import cv2
-import numpy as np
-import torch
-import torch.utils.data
-import torchvision.datasets as datasets
-import torchvision.transforms as transforms
-import torchvision.transforms.functional as TF
-from PIL import Image, ImageFile
-from scipy.ndimage import gaussian_filter
-from torch.utils.data.sampler import WeightedRandomSampler
-
-from .config import CONFIGCLASS
-
-ImageFile.LOAD_TRUNCATED_IMAGES = True
-
-
-def dataset_folder(root: str, cfg: CONFIGCLASS):
- if cfg.mode == "binary":
- return binary_dataset(root, cfg)
- if cfg.mode == "filename":
- return FileNameDataset(root, cfg)
- raise ValueError("cfg.mode needs to be binary or filename.")
-
-
-def binary_dataset(root: str, cfg: CONFIGCLASS):
- identity_transform = transforms.Lambda(lambda img: img)
-
- rz_func = identity_transform
-
- if cfg.isTrain:
- crop_func = transforms.RandomCrop((448,448))
- else:
- crop_func = transforms.CenterCrop((448,448)) if cfg.aug_crop else identity_transform
-
- if cfg.isTrain and cfg.aug_flip:
- flip_func = transforms.RandomHorizontalFlip()
- else:
- flip_func = identity_transform
-
-
- return datasets.ImageFolder(
- root,
- transforms.Compose(
- [
- rz_func,
- #change
- transforms.Lambda(lambda img: blur_jpg_augment(img, cfg)),
- crop_func,
- flip_func,
- transforms.ToTensor(),
- transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
- if cfg.aug_norm
- else identity_transform,
- ]
- )
- )
-
-
-class FileNameDataset(datasets.ImageFolder):
- def name(self):
- return 'FileNameDataset'
-
- def __init__(self, opt, root):
- self.opt = opt
- super().__init__(root)
-
- def __getitem__(self, index):
- # Loading sample
- path, target = self.samples[index]
- return path
-
-
-def blur_jpg_augment(img: Image.Image, cfg: CONFIGCLASS):
- img: np.ndarray = np.array(img)
- if cfg.isTrain:
- if random() < cfg.blur_prob:
- sig = sample_continuous(cfg.blur_sig)
- gaussian_blur(img, sig)
-
- if random() < cfg.jpg_prob:
- method = sample_discrete(cfg.jpg_method)
- qual = sample_discrete(cfg.jpg_qual)
- img = jpeg_from_key(img, qual, method)
-
- return Image.fromarray(img)
-
-
-def sample_continuous(s: list):
- if len(s) == 1:
- return s[0]
- if len(s) == 2:
- rg = s[1] - s[0]
- return random() * rg + s[0]
- raise ValueError("Length of iterable s should be 1 or 2.")
-
-
-def sample_discrete(s: list):
- return s[0] if len(s) == 1 else choice(s)
-
-
-def gaussian_blur(img: np.ndarray, sigma: float):
- gaussian_filter(img[:, :, 0], output=img[:, :, 0], sigma=sigma)
- gaussian_filter(img[:, :, 1], output=img[:, :, 1], sigma=sigma)
- gaussian_filter(img[:, :, 2], output=img[:, :, 2], sigma=sigma)
-
-
-def cv2_jpg(img: np.ndarray, compress_val: int) -> np.ndarray:
- img_cv2 = img[:, :, ::-1]
- encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), compress_val]
- result, encimg = cv2.imencode(".jpg", img_cv2, encode_param)
- decimg = cv2.imdecode(encimg, 1)
- return decimg[:, :, ::-1]
-
-
-def pil_jpg(img: np.ndarray, compress_val: int):
- out = BytesIO()
- img = Image.fromarray(img)
- img.save(out, format="jpeg", quality=compress_val)
- img = Image.open(out)
- # load from memory before ByteIO closes
- img = np.array(img)
- out.close()
- return img
-
-
-jpeg_dict = {"cv2": cv2_jpg, "pil": pil_jpg}
-
-
-def jpeg_from_key(img: np.ndarray, compress_val: int, key: str) -> np.ndarray:
- method = jpeg_dict[key]
- return method(img, compress_val)
-
-
-rz_dict = {'bilinear': Image.BILINEAR,
- 'bicubic': Image.BICUBIC,
- 'lanczos': Image.LANCZOS,
- 'nearest': Image.NEAREST}
-def custom_resize(img: Image.Image, cfg: CONFIGCLASS) -> Image.Image:
- interp = sample_discrete(cfg.rz_interp)
- return TF.resize(img, cfg.loadSize, interpolation=rz_dict[interp])
-
-
-def get_dataset(cfg: CONFIGCLASS):
- dset_lst = []
- for dataset in cfg.datasets:
- root = os.path.join(cfg.dataset_root, dataset)
- dset = dataset_folder(root, cfg)
- dset_lst.append(dset)
- return torch.utils.data.ConcatDataset(dset_lst)
-
-
-def get_bal_sampler(dataset: torch.utils.data.ConcatDataset):
- targets = []
- for d in dataset.datasets:
- targets.extend(d.targets)
-
- ratio = np.bincount(targets)
- w = 1.0 / torch.tensor(ratio, dtype=torch.float)
- sample_weights = w[targets]
- return WeightedRandomSampler(weights=sample_weights, num_samples=len(sample_weights))
-
-
-def create_dataloader(cfg: CONFIGCLASS):
- shuffle = not cfg.serial_batches if (cfg.isTrain and not cfg.class_bal) else False
- dataset = get_dataset(cfg)
- sampler = get_bal_sampler(dataset) if cfg.class_bal else None
-
- return torch.utils.data.DataLoader(
- dataset,
- batch_size=cfg.batch_size,
- shuffle=shuffle,
- sampler=sampler,
- num_workers=int(cfg.num_workers),
- )
diff --git a/core/utils1/utils1/earlystop.py b/core/utils1/utils1/earlystop.py
deleted file mode 100644
index 25aef71..0000000
--- a/core/utils1/utils1/earlystop.py
+++ /dev/null
@@ -1,46 +0,0 @@
-import numpy as np
-
-from .trainer import Trainer
-
-
-class EarlyStopping:
- """Early stops the training if validation loss doesn't improve after a given patience."""
-
- def __init__(self, patience=1, verbose=False, delta=0):
- """
- Args:
- patience (int): How long to wait after last time validation loss improved.
- Default: 7
- verbose (bool): If True, prints a message for each validation loss improvement.
- Default: False
- delta (float): Minimum change in the monitored quantity to qualify as an improvement.
- Default: 0
- """
- self.patience = patience
- self.verbose = verbose
- self.counter = 0
- self.best_score = None
- self.early_stop = False
- self.score_max = -np.Inf
- self.delta = delta
-
- def __call__(self, score: float, trainer: Trainer):
- if self.best_score is None:
- self.best_score = score
- self.save_checkpoint(score, trainer)
- elif score < self.best_score - self.delta:
- self.counter += 1
- print(f"EarlyStopping counter: {self.counter} out of {self.patience}")
- if self.counter >= self.patience:
- self.early_stop = True
- else:
- self.best_score = score
- self.save_checkpoint(score, trainer)
- self.counter = 0
-
- def save_checkpoint(self, score: float, trainer: Trainer):
- """Saves model when validation loss decrease."""
- if self.verbose:
- print(f"Validation accuracy increased ({self.score_max:.6f} --> {score:.6f}). Saving model ...")
- trainer.save_networks("best")
- self.score_max = score
diff --git a/core/utils1/utils1/eval.py b/core/utils1/utils1/eval.py
deleted file mode 100644
index 5784de3..0000000
--- a/core/utils1/utils1/eval.py
+++ /dev/null
@@ -1,66 +0,0 @@
-import math
-import os
-
-import matplotlib.pyplot as plt
-import numpy as np
-import torch
-import torch.nn as nn
-
-from .config import CONFIGCLASS
-from .utils import to_cuda
-
-
-def get_val_cfg(cfg: CONFIGCLASS, split="val", copy=True):
- if copy:
- from copy import deepcopy
-
- val_cfg = deepcopy(cfg)
- else:
- val_cfg = cfg
- val_cfg.dataset_root = os.path.join(val_cfg.dataset_root, split)
- val_cfg.datasets = cfg.datasets_test
- val_cfg.isTrain = False
- # val_cfg.aug_resize = False
- # val_cfg.aug_crop = False
- val_cfg.aug_flip = False
- val_cfg.serial_batches = True
- val_cfg.jpg_method = ["pil"]
- # Currently assumes jpg_prob, blur_prob 0 or 1
- if len(val_cfg.blur_sig) == 2:
- b_sig = val_cfg.blur_sig
- val_cfg.blur_sig = [(b_sig[0] + b_sig[1]) / 2]
- if len(val_cfg.jpg_qual) != 1:
- j_qual = val_cfg.jpg_qual
- val_cfg.jpg_qual = [int((j_qual[0] + j_qual[-1]) / 2)]
- return val_cfg
-
-def validate(model: nn.Module, cfg: CONFIGCLASS):
- from sklearn.metrics import accuracy_score, average_precision_score, roc_auc_score
-
- from .datasets import create_dataloader
-
- data_loader = create_dataloader(cfg)
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
-
- with torch.no_grad():
- y_true, y_pred = [], []
- for data in data_loader:
- img, label, meta = data if len(data) == 3 else (*data, None)
- in_tens = to_cuda(img, device)
- meta = to_cuda(meta, device)
- predict = model(in_tens, meta).sigmoid()
- y_pred.extend(predict.flatten().tolist())
- y_true.extend(label.flatten().tolist())
-
- y_true, y_pred = np.array(y_true), np.array(y_pred)
- r_acc = accuracy_score(y_true[y_true == 0], y_pred[y_true == 0] > 0.5)
- f_acc = accuracy_score(y_true[y_true == 1], y_pred[y_true == 1] > 0.5)
- acc = accuracy_score(y_true, y_pred > 0.5)
- ap = average_precision_score(y_true, y_pred)
- results = {
- "ACC": acc,
- "AP": ap,
- "R_ACC": r_acc,
- "F_ACC": f_acc,
- }
- return results
diff --git a/core/utils1/utils1/trainer.py b/core/utils1/utils1/trainer.py
deleted file mode 100644
index 7a4ce8b..0000000
--- a/core/utils1/utils1/trainer.py
+++ /dev/null
@@ -1,169 +0,0 @@
-import os
-
-import torch
-import torch.nn as nn
-from torch.nn import init
-
-from .config import CONFIGCLASS
-from .utils import get_network
-from .warmup import GradualWarmupScheduler
-
-
-class BaseModel(nn.Module):
- def __init__(self, cfg: CONFIGCLASS):
- super().__init__()
- self.cfg = cfg
- self.total_steps = 0
- self.isTrain = cfg.isTrain
- self.save_dir = cfg.ckpt_dir
- self.device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
- self.model:nn.Module
- self.model=nn.Module.to(self.device)
- # self.model.to(self.device)
- self.model.load_state_dict(torch.load('./checkpoints/optical.pth'))
- self.optimizer: torch.optim.Optimizer
-
- def save_networks(self, epoch: int):
- save_filename = f"model_epoch_{epoch}.pth"
- save_path = os.path.join(self.save_dir, save_filename)
-
- # serialize model and optimizer to dict
- state_dict = {
- "model": self.model.state_dict(),
- "optimizer": self.optimizer.state_dict(),
- "total_steps": self.total_steps,
- }
-
- torch.save(state_dict, save_path)
-
- # load models from the disk
- def load_networks(self, epoch: int):
- load_filename = f"model_epoch_{epoch}.pth"
- load_path = os.path.join(self.save_dir, load_filename)
-
- if epoch==0:
- # load_filename = f"lsun_adm.pth"
- load_path="checkpoints/optical.pth"
- print("loading optical path")
- else :
- print(f"loading the model from {load_path}")
-
- # print(f"loading the model from {load_path}")
-
- # if you are using PyTorch newer than 0.4 (e.g., built from
- # GitHub source), you can remove str() on self.device
- state_dict = torch.load(load_path, map_location=self.device)
- if hasattr(state_dict, "_metadata"):
- del state_dict._metadata
-
- self.model.load_state_dict(state_dict["model"])
- self.total_steps = state_dict["total_steps"]
-
- if self.isTrain and not self.cfg.new_optim:
- self.optimizer.load_state_dict(state_dict["optimizer"])
- # move optimizer state to GPU
- for state in self.optimizer.state.values():
- for k, v in state.items():
- if torch.is_tensor(v):
- state[k] = v.to(self.device)
-
- for g in self.optimizer.param_groups:
- g["lr"] = self.cfg.lr
-
- def eval(self):
- self.model.eval()
-
- def test(self):
- with torch.no_grad():
- self.forward()
-
-
-def init_weights(net: nn.Module, init_type="normal", gain=0.02):
- def init_func(m: nn.Module):
- classname = m.__class__.__name__
- if hasattr(m, "weight") and (classname.find("Conv") != -1 or classname.find("Linear") != -1):
- if init_type == "normal":
- init.normal_(m.weight.data, 0.0, gain)
- elif init_type == "xavier":
- init.xavier_normal_(m.weight.data, gain=gain)
- elif init_type == "kaiming":
- init.kaiming_normal_(m.weight.data, a=0, mode="fan_in")
- elif init_type == "orthogonal":
- init.orthogonal_(m.weight.data, gain=gain)
- else:
- raise NotImplementedError(f"initialization method [{init_type}] is not implemented")
- if hasattr(m, "bias") and m.bias is not None:
- init.constant_(m.bias.data, 0.0)
- elif classname.find("BatchNorm2d") != -1:
- init.normal_(m.weight.data, 1.0, gain)
- init.constant_(m.bias.data, 0.0)
-
- print(f"initialize network with {init_type}")
- net.apply(init_func)
-
-
-class Trainer(BaseModel):
- def name(self):
- return "Trainer"
-
- def __init__(self, cfg: CONFIGCLASS):
- super().__init__(cfg)
- self.arch = cfg.arch
- self.model = get_network(self.arch, cfg.isTrain, cfg.continue_train, cfg.init_gain, cfg.pretrained)
-
- self.loss_fn = nn.BCEWithLogitsLoss()
- # initialize optimizers
- if cfg.optim == "adam":
- self.optimizer = torch.optim.Adam(self.model.parameters(), lr=cfg.lr, betas=(cfg.beta1, 0.999))
- elif cfg.optim == "sgd":
- self.optimizer = torch.optim.SGD(self.model.parameters(), lr=cfg.lr, momentum=0.9, weight_decay=5e-4)
- else:
- raise ValueError("optim should be [adam, sgd]")
- if cfg.warmup:
- scheduler_cosine = torch.optim.lr_scheduler.CosineAnnealingLR(
- self.optimizer, cfg.nepoch - cfg.warmup_epoch, eta_min=1e-6
- )
- self.scheduler = GradualWarmupScheduler(
- self.optimizer, multiplier=1, total_epoch=cfg.warmup_epoch, after_scheduler=scheduler_cosine
- )
- self.scheduler.step()
- if cfg.continue_train:
- self.load_networks(cfg.epoch)
- self.model.to(self.device)
-
- # self.model.load_state_dict(torch.load('checkpoints/optical.pth'))
- load_path='checkpoints/optical.pth'
- state_dict = torch.load(load_path, map_location=self.device)
-
-
- self.model.load_state_dict(state_dict["model"])
-
-
- def adjust_learning_rate(self, min_lr=1e-6):
- for param_group in self.optimizer.param_groups:
- param_group["lr"] /= 10.0
- if param_group["lr"] < min_lr:
- return False
- return True
-
- def set_input(self, input):
- img, label, meta = input if len(input) == 3 else (input[0], input[1], {})
- self.input = img.to(self.device)
- self.label = label.to(self.device).float()
- for k in meta.keys():
- if isinstance(meta[k], torch.Tensor):
- meta[k] = meta[k].to(self.device)
- self.meta = meta
-
- def forward(self):
- self.output = self.model(self.input, self.meta)
-
- def get_loss(self):
- return self.loss_fn(self.output.squeeze(1), self.label)
-
- def optimize_parameters(self):
- self.forward()
- self.loss = self.loss_fn(self.output.squeeze(1), self.label)
- self.optimizer.zero_grad()
- self.loss.backward()
- self.optimizer.step()
diff --git a/core/utils1/utils1/utils.py b/core/utils1/utils1/utils.py
deleted file mode 100644
index d52ebbd..0000000
--- a/core/utils1/utils1/utils.py
+++ /dev/null
@@ -1,109 +0,0 @@
-import argparse
-import os
-import sys
-import time
-import warnings
-from importlib import import_module
-
-import numpy as np
-import torch
-import torch.nn as nn
-from PIL import Image
-
-warnings.filterwarnings("ignore", category=UserWarning, module="torch.nn.functional")
-
-
-def str2bool(v: str, strict=True) -> bool:
- if isinstance(v, bool):
- return v
- elif isinstance(v, str):
- if v.lower() in ("true", "yes", "on" "t", "y", "1"):
- return True
- elif v.lower() in ("false", "no", "off", "f", "n", "0"):
- return False
- if strict:
- raise argparse.ArgumentTypeError("Unsupported value encountered.")
- else:
- return True
-
-
-def to_cuda(data, device="cuda", exclude_keys: "list[str]" = None):
- if isinstance(data, torch.Tensor):
- data = data.to(device)
- elif isinstance(data, (tuple, list, set)):
- data = [to_cuda(b, device) for b in data]
- elif isinstance(data, dict):
- if exclude_keys is None:
- exclude_keys = []
- for k in data.keys():
- if k not in exclude_keys:
- data[k] = to_cuda(data[k], device)
- else:
- # raise TypeError(f"Unsupported type: {type(data)}")
- data = data
- return data
-
-
-class HiddenPrints:
- def __enter__(self):
- self._original_stdout = sys.stdout
- sys.stdout = open(os.devnull, "w")
-
- def __exit__(self, exc_type, exc_val, exc_tb):
- sys.stdout.close()
- sys.stdout = self._original_stdout
-
-
-class Logger(object):
- def __init__(self):
- self.terminal = sys.stdout
- self.file = None
-
- def open(self, file, mode=None):
- if mode is None:
- mode = "w"
- self.file = open(file, mode)
-
- def write(self, message, is_terminal=1, is_file=1):
- if "\r" in message:
- is_file = 0
- if is_terminal == 1:
- self.terminal.write(message)
- self.terminal.flush()
- if is_file == 1:
- self.file.write(message)
- self.file.flush()
-
- def flush(self):
- # this flush method is needed for python 3 compatibility.
- # this handles the flush command by doing nothing.
- # you might want to specify some extra behavior here.
- pass
-
-
-def get_network(arch: str, isTrain=False, continue_train=False, init_gain=0.02, pretrained=True):
- if "resnet" in arch:
- from networks.resnet import ResNet
-
- resnet = getattr(import_module("networks.resnet"), arch)
- if isTrain:
- if continue_train:
- model: ResNet = resnet(num_classes=1)
- else:
- model: ResNet = resnet(pretrained=pretrained)
- model.fc = nn.Linear(2048, 1)
- nn.init.normal_(model.fc.weight.data, 0.0, init_gain)
- else:
- model: ResNet = resnet(num_classes=1)
- return model
- else:
- raise ValueError(f"Unsupported arch: {arch}")
-
-
-def pad_img_to_square(img: np.ndarray):
- H, W = img.shape[:2]
- if H != W:
- new_size = max(H, W)
- img = np.pad(img, ((0, new_size - H), (0, new_size - W), (0, 0)), mode="constant")
- assert img.shape[0] == img.shape[1] == new_size
- return img
diff --git a/core/utils1/utils1/warmup.py b/core/utils1/utils1/warmup.py
deleted file mode 100644
index c193a6c..0000000
--- a/core/utils1/utils1/warmup.py
+++ /dev/null
@@ -1,70 +0,0 @@
-from torch.optim.lr_scheduler import ReduceLROnPlateau, _LRScheduler
-
-
-class GradualWarmupScheduler(_LRScheduler):
- """Gradually warm-up(increasing) learning rate in optimizer.
- Proposed in 'Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour'.
-
- Args:
- optimizer (Optimizer): Wrapped optimizer.
- multiplier: target learning rate = base lr * multiplier if multiplier > 1.0. if multiplier = 1.0, lr starts from 0 and ends up with the base_lr.
- total_epoch: target learning rate is reached at total_epoch, gradually
- after_scheduler: after target_epoch, use this scheduler(eg. ReduceLROnPlateau)
- """
-
- def __init__(self, optimizer, multiplier, total_epoch, after_scheduler=None):
- self.multiplier = multiplier
- if self.multiplier < 1.0:
- raise ValueError("multiplier should be greater thant or equal to 1.")
- self.total_epoch = total_epoch
- self.after_scheduler = after_scheduler
- self.finished = False
- super().__init__(optimizer)
-
- def get_lr(self):
- if self.last_epoch > self.total_epoch:
- if self.after_scheduler:
- if not self.finished:
- self.after_scheduler.base_lrs = [base_lr * self.multiplier for base_lr in self.base_lrs]
- self.finished = True
- return self.after_scheduler.get_last_lr()
- return [base_lr * self.multiplier for base_lr in self.base_lrs]
-
- if self.multiplier == 1.0:
- return [base_lr * (float(self.last_epoch) / self.total_epoch) for base_lr in self.base_lrs]
- else:
- return [
- base_lr * ((self.multiplier - 1.0) * self.last_epoch / self.total_epoch + 1.0)
- for base_lr in self.base_lrs
- ]
-
- def step_ReduceLROnPlateau(self, metrics, epoch=None):
- if epoch is None:
- epoch = self.last_epoch + 1
- self.last_epoch = (
- epoch if epoch != 0 else 1
- ) # ReduceLROnPlateau is called at the end of epoch, whereas others are called at beginning
- if self.last_epoch <= self.total_epoch:
- warmup_lr = [
- base_lr * ((self.multiplier - 1.0) * self.last_epoch / self.total_epoch + 1.0)
- for base_lr in self.base_lrs
- ]
- for param_group, lr in zip(self.optimizer.param_groups, warmup_lr):
- param_group["lr"] = lr
- else:
- if epoch is None:
- self.after_scheduler.step(metrics, None)
- else:
- self.after_scheduler.step(metrics, epoch - self.total_epoch)
-
- def step(self, epoch=None, metrics=None):
- if type(self.after_scheduler) != ReduceLROnPlateau:
- if self.finished and self.after_scheduler:
- if epoch is None:
- self.after_scheduler.step(None)
- else:
- self.after_scheduler.step(epoch - self.total_epoch)
- else:
- return super().step(epoch)
- else:
- self.step_ReduceLROnPlateau(metrics, epoch)
From a7ea9c02e72804a5ca7bf7a5af7e70c98d8b7e60 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Sat, 22 Nov 2025 06:57:27 +0800
Subject: [PATCH 12/55] Update dataset names in DefaultConfigs class for
consistency
---
core/utils1/config.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/core/utils1/config.py b/core/utils1/config.py
index ef05fa7..d399e41 100644
--- a/core/utils1/config.py
+++ b/core/utils1/config.py
@@ -10,8 +10,8 @@ class DefaultConfigs(ABC):
gpus = [0]
seed = 3407
arch = "resnet50"
- datasets = ["train"]
- datasets_test = ["val"]
+ datasets = ["trainset_1"]
+ datasets_test = ["val_set_1"]
mode = "binary"
class_bal = False
batch_size = 64
From b4806f1330ff2d184a8349da9629a134935e4cb2 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Sat, 22 Nov 2025 09:48:49 +0800
Subject: [PATCH 13/55] Update root directory path in DefaultConfigs class for
correct data access
---
core/utils1/config.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/core/utils1/config.py b/core/utils1/config.py
index d399e41..c76ea41 100644
--- a/core/utils1/config.py
+++ b/core/utils1/config.py
@@ -59,7 +59,7 @@ class DefaultConfigs(ABC):
pretrained = True
# paths information
- root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
+ root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
dataset_root = os.path.join(root_dir, "data")
exp_root = os.path.join(root_dir, "data", "exp")
_exp_name = ""
From 9fd55529737453f94ca76147b5e8a70d17087729 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Sat, 22 Nov 2025 10:03:09 +0800
Subject: [PATCH 14/55] Add AUC, TPR, and TNR metrics to validation results;
fix model initialization in Trainer class
---
core/utils1/eval.py | 9 +++++++++
core/utils1/trainer.py | 1 -
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/core/utils1/eval.py b/core/utils1/eval.py
index 731ebf1..b93d2f8 100644
--- a/core/utils1/eval.py
+++ b/core/utils1/eval.py
@@ -57,10 +57,19 @@ def validate(model: nn.Module, cfg: CONFIGCLASS):
f_acc = accuracy_score(y_true[y_true == 1], y_pred[y_true == 1] > 0.5)
acc = accuracy_score(y_true, y_pred > 0.5)
ap = average_precision_score(y_true, y_pred)
+ auc = roc_auc_score(y_true, y_pred)
+
+ # Calculate TPR (True Positive Rate / Recall) and TNR (True Negative Rate / Specificity)
+ tpr = f_acc # TPR is the accuracy on fake samples (class 1)
+ tnr = r_acc # TNR is the accuracy on real samples (class 0)
+
results = {
"ACC": acc,
"AP": ap,
+ "AUC": auc,
"R_ACC": r_acc,
"F_ACC": f_acc,
+ "TPR": tpr,
+ "TNR": tnr,
}
return results
diff --git a/core/utils1/trainer.py b/core/utils1/trainer.py
index 3a5abc6..6739243 100644
--- a/core/utils1/trainer.py
+++ b/core/utils1/trainer.py
@@ -18,7 +18,6 @@ def __init__(self, cfg: CONFIGCLASS):
self.save_dir = cfg.ckpt_dir
self.device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
self.model:nn.Module
- self.model=nn.Module.to(self.device)
# self.model.to(self.device)
#self.model.load_state_dict(torch.load('./checkpoints/optical.pth'))
self.optimizer: torch.optim.Optimizer
From 3f55265645b513414eab80fade90b8e337abe1fe Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Sat, 22 Nov 2025 10:35:18 +0800
Subject: [PATCH 15/55] Reduce batch size to 16 and num_workers to 4 for
improved compatibility with 4GB GPU
---
core/utils1/config.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/core/utils1/config.py b/core/utils1/config.py
index c76ea41..d1901d7 100644
--- a/core/utils1/config.py
+++ b/core/utils1/config.py
@@ -14,11 +14,11 @@ class DefaultConfigs(ABC):
datasets_test = ["val_set_1"]
mode = "binary"
class_bal = False
- batch_size = 64
+ batch_size = 16 # Reduced from 64 to 16 for 4GB GPU
loadSize = 256
cropSize = 224
epoch = "latest"
- num_workers = 20
+ num_workers = 4 # Reduced from 20 to 4 to avoid shared memory issues
serial_batches = False
isTrain = True
From 71db91869bcea1c0a0a18f8eec4be4d1b9c629bb Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Sat, 22 Nov 2025 23:52:43 +0800
Subject: [PATCH 16/55] Integrate Weights & Biases (W&B) for enhanced training
tracking; reduce training epochs from 400 to 100; update dependencies in
requirements and project files.
---
WANDB_SETUP.md | 216 +++++++++++++++++++++++++++++++++++++++++++++++
pyproject.toml | 1 +
requirements.txt | 1 +
train.py | 37 ++++++++
4 files changed, 255 insertions(+)
create mode 100644 WANDB_SETUP.md
diff --git a/WANDB_SETUP.md b/WANDB_SETUP.md
new file mode 100644
index 0000000..07dc9ad
--- /dev/null
+++ b/WANDB_SETUP.md
@@ -0,0 +1,216 @@
+# Weights & Biases (W&B) Integration Guide
+
+## What is W&B?
+Weights & Biases tracks your training runs, logs metrics, and **stores model checkpoints in the cloud** so you can access them from any device.
+
+## Setup Instructions
+
+### 1. Install wandb
+```bash
+# Local installation
+pip install wandb
+
+# Or with Docker (already included in requirements.txt)
+# Just rebuild the Docker image
+docker build -f Dockerfile.gpu-alt -t sacdalance/thesis-aigvdet:gpu .
+```
+
+### 2. Create W&B Account
+1. Go to https://wandb.ai/signup
+2. Sign up (free for personal use)
+3. Get your API key from https://wandb.ai/authorize
+
+### 3. Login to W&B
+
+**On local machine:**
+```bash
+wandb login
+# Paste your API key when prompted
+```
+
+**On Vast.ai or remote server:**
+```bash
+# Option 1: Interactive login
+wandb login
+
+# Option 2: Use API key directly
+export WANDB_API_KEY="your-api-key-here"
+# Or add to Docker run command:
+docker run --gpus all -e WANDB_API_KEY="your-key" ...
+```
+
+### 4. Train with W&B Tracking
+
+```bash
+# Train normally - W&B will automatically track
+python train.py --gpus 0 --exp_name TRAIN_RGB datasets trainset_1_RGB datasets_test val_set_1_RGB
+
+# With Docker on Vast.ai
+docker run --gpus all --shm-size=8g -it --rm \
+ -e WANDB_API_KEY="your-api-key" \
+ -v /workspace/data:/app/data \
+ -v /workspace/checkpoints:/app/checkpoints \
+ sacdalance/thesis-aigvdet:gpu \
+ python3.11 train.py --gpus 0 --exp_name TRAIN_RGB datasets trainset_1_RGB datasets_test val_set_1_RGB
+```
+
+## What W&B Tracks
+
+### Metrics Logged:
+- **Training loss** - Every batch
+- **Validation metrics** - Every epoch
+ - Accuracy (ACC)
+ - Average Precision (AP)
+ - AUC (Area Under Curve)
+ - TPR (True Positive Rate)
+ - TNR (True Negative Rate)
+- **System metrics** - GPU usage, CPU, memory
+
+### Model Checkpoints:
+- **Automatically uploads** `model_epoch_best.pth` to W&B cloud
+- **Access from anywhere** - Download checkpoints from any device
+- **Version control** - All checkpoint versions saved
+
+## Accessing Your Results
+
+### View Training Dashboard:
+1. Go to https://wandb.ai/
+2. Navigate to your project: `aigvdet-training`
+3. Click on your run (e.g., `TRAIN_RGB`)
+4. View real-time metrics, charts, and system stats
+
+### Download Checkpoints:
+```python
+# From any device with Python
+import wandb
+
+# Login
+wandb.login()
+
+# Download checkpoint
+api = wandb.Api()
+run = api.run("your-username/aigvdet-training/run-id")
+artifact = run.use_artifact('TRAIN_RGB_best_model:latest')
+artifact_dir = artifact.download()
+
+# The checkpoint will be in: artifact_dir/model_epoch_best.pth
+```
+
+**Or via Web UI:**
+1. Go to your run page
+2. Click "Artifacts" tab
+3. Click on `TRAIN_RGB_best_model`
+4. Click "Download" button
+
+## Benefits for Vast.ai Training
+
+### Problem W&B Solves:
+- ❌ **Without W&B**: Must manually download checkpoints before stopping Vast.ai instance
+- ✅ **With W&B**: Checkpoints automatically saved to cloud, accessible from anywhere
+
+### Workflow:
+```bash
+# 1. Start training on Vast.ai
+docker run --gpus all -e WANDB_API_KEY="your-key" ... python train.py ...
+
+# 2. Training runs (checkpoints auto-upload to W&B cloud)
+
+# 3. Stop Vast.ai instance (no need to manually download!)
+
+# 4. Later, on your local machine:
+wandb artifact get your-username/aigvdet-training/TRAIN_RGB_best_model:latest
+# Checkpoint downloaded to local machine!
+```
+
+## Configuration Options
+
+### Disable W&B (if needed):
+```bash
+# Set environment variable
+export WANDB_MODE=disabled
+
+# Or in code (add to train.py):
+os.environ["WANDB_MODE"] = "disabled"
+```
+
+### Change Project Name:
+Edit `train.py` line 43:
+```python
+wandb.init(
+ project="your-custom-project-name", # Change this
+ name=cfg.exp_name,
+ ...
+)
+```
+
+### Resume Training:
+W&B automatically handles resume if you use `continue_train True`:
+```bash
+python train.py --gpus 0 --exp_name TRAIN_RGB continue_train True epoch latest
+```
+
+## Cost
+- **Free tier**: Unlimited personal projects, 100GB storage
+- **Teams tier**: $50/user/month for collaboration
+- For academic research, you can apply for free team accounts
+
+## Troubleshooting
+
+### "wandb: ERROR api_key not configured"
+```bash
+# Set API key
+export WANDB_API_KEY="your-key"
+# Or login
+wandb login
+```
+
+### Checkpoint upload fails
+```bash
+# Check internet connection
+# Check W&B storage quota (free tier = 100GB)
+# Verify checkpoint file exists
+ls -lh data/exp/TRAIN_RGB/ckpt/model_epoch_best.pth
+```
+
+### Disable W&B temporarily
+```bash
+export WANDB_MODE=offline # Run offline, sync later
+# Or
+export WANDB_MODE=disabled # Completely disable
+```
+
+## Example Output
+
+When training starts:
+```
+Setting up TensorBoard...
+✓ Logs will be saved to: data/exp/TRAIN_RGB
+
+Initializing Weights & Biases...
+wandb: Currently logged in as: your-username (use `wandb login --relogin` to force relogin)
+wandb: Tracking run with wandb version 0.16.0
+wandb: Run data is saved locally in data/exp/TRAIN_RGB/wandb
+wandb: Run `wandb offline` to turn off syncing.
+wandb: Syncing run TRAIN_RGB
+wandb: ⭐️ View project at https://wandb.ai/your-username/aigvdet-training
+wandb: 🚀 View run at https://wandb.ai/your-username/aigvdet-training/runs/abc123
+✓ W&B tracking enabled: https://wandb.ai/your-username/aigvdet-training/runs/abc123
+```
+
+Click the URL to view your training in real-time!
+
+## Summary
+
+**With W&B integration:**
+1. ✅ Track training metrics in real-time from any device
+2. ✅ Automatically save checkpoints to cloud storage
+3. ✅ No need to manually download before stopping Vast.ai
+4. ✅ Compare multiple training runs easily
+5. ✅ Share results with collaborators
+
+**Setup is simple:**
+```bash
+pip install wandb
+wandb login
+python train.py # W&B automatically tracks!
+```
diff --git a/pyproject.toml b/pyproject.toml
index 738690b..552ef44 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -26,6 +26,7 @@ dependencies = [
"tensorboardX",
"tqdm",
"blobfile>=1.0.5",
+ "wandb",
"pip",
]
diff --git a/requirements.txt b/requirements.txt
index 57dc112..d8dbfc4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -13,3 +13,4 @@ tensorboard
tensorboardX
tqdm
blobfile>=1.0.5
+wandb
diff --git a/train.py b/train.py
index 87004e6..1020670 100644
--- a/train.py
+++ b/train.py
@@ -5,6 +5,7 @@
from tensorboardX import SummaryWriter
from tqdm import tqdm
+import wandb
from core.utils1.datasets import create_dataloader
from core.utils1.earlystop import EarlyStopping
@@ -40,6 +41,17 @@
val_writer = SummaryWriter(os.path.join(cfg.exp_dir, "val"))
print(f"✓ Logs will be saved to: {cfg.exp_dir}")
+ # Initialize wandb
+ print("\nInitializing wandb...")
+ wandb.init(
+ project="aigvdet-training",
+ name=cfg.exp_name,
+ config=cfg.to_dict(),
+ dir=cfg.exp_dir,
+ resume="allow" if cfg.continue_train else None
+ )
+ print(f"✓ wandb tracking enabled: {wandb.run.url}")
+
print("\nInitializing model...")
trainer = Trainer(cfg)
early_stopping = EarlyStopping(patience=cfg.earlystop_epoch, delta=-0.001, verbose=True)
@@ -67,6 +79,7 @@
# if trainer.total_steps % cfg.loss_freq == 0:
# log.write(f"Train loss: {trainer.loss} at step: {trainer.total_steps}\n")
train_writer.add_scalar("loss", trainer.loss, trainer.total_steps)
+ wandb.log({"train/loss": trainer.loss, "train/step": trainer.total_steps})
if trainer.total_steps % cfg.save_latest_freq == 0:
print(f"💾 Saving checkpoint (epoch {epoch+1}, step {trainer.total_steps})")
@@ -93,6 +106,26 @@
val_writer.add_scalar("TPR", val_results["TPR"], trainer.total_steps)
val_writer.add_scalar("TNR", val_results["TNR"], trainer.total_steps)
+ # Log validation metrics to wandb
+ wandb.log({
+ "val/AP": val_results["AP"],
+ "val/ACC": val_results["ACC"],
+ "val/AUC": val_results["AUC"],
+ "val/TPR": val_results["TPR"],
+ "val/TNR": val_results["TNR"],
+ "epoch": epoch
+ })
+
+ # Save best checkpoint as wandb artifact
+ if os.path.exists(os.path.join(cfg.ckpt_dir, "model_epoch_best.pth")):
+ artifact = wandb.Artifact(
+ name=f"{cfg.exp_name}_best_model",
+ type="model",
+ description=f"Best model checkpoint at epoch {epoch}"
+ )
+ artifact.add_file(os.path.join(cfg.ckpt_dir, "model_epoch_best.pth"))
+ wandb.log_artifact(artifact)
+
print(f"✓ Validation Results - AP: {val_results['AP']:.4f} | ACC: {val_results['ACC']:.4f} | AUC: {val_results['AUC']:.4f}")
log.write(f"(Val @ epoch {epoch}) AP: {val_results['AP']}; ACC: {val_results['ACC']}\n")
@@ -111,3 +144,7 @@
# print(trainer.scheduler.get_lr()[0])
trainer.scheduler.step()
trainer.train()
+
+ # Finish wandb run
+ print("\n✓ Training complete! Finalizing W&B...")
+ wandb.finish()
From 93ebc595e56ca80222e37aecf4957cd3ff358ad2 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Sun, 23 Nov 2025 00:15:42 +0800
Subject: [PATCH 17/55] Add environment variable files to .gitignore for
security
---
.gitignore | 4 ++++
train.py | 2 +-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/.gitignore b/.gitignore
index 2fab0b4..0d394e4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,10 @@ data/
checkpoints/
raft_model/
+# Environment variables (contains secrets)
+.env
+.env.example
+
# Generated output folders
frame/
optical_result/
diff --git a/train.py b/train.py
index 1020670..f740416 100644
--- a/train.py
+++ b/train.py
@@ -46,7 +46,7 @@
wandb.init(
project="aigvdet-training",
name=cfg.exp_name,
- config=cfg.to_dict(),
+ config=cfg.to_dict(),
dir=cfg.exp_dir,
resume="allow" if cfg.continue_train else None
)
From c59f3abf5d0d5706342f3a0e5adeb8d535182122 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Sun, 23 Nov 2025 00:58:02 +0800
Subject: [PATCH 18/55] Add Weights & Biases (W&B) to Dockerfiles for enhanced
tracking
---
Dockerfile.cpu | 3 ++-
Dockerfile.gpu-alt | 3 ++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/Dockerfile.cpu b/Dockerfile.cpu
index 9bd6b9c..2ef81b9 100644
--- a/Dockerfile.cpu
+++ b/Dockerfile.cpu
@@ -50,7 +50,8 @@ RUN uv pip install --system \
tensorboard \
tensorboardX \
tqdm \
- "blobfile>=1.0.5"
+ "blobfile>=1.0.5" \
+ wandb
# Install PyTorch CPU version using pip directly
RUN pip3 install torch==2.0.0+cpu torchvision==0.15.1+cpu \
diff --git a/Dockerfile.gpu-alt b/Dockerfile.gpu-alt
index 94ba401..a59b1c0 100644
--- a/Dockerfile.gpu-alt
+++ b/Dockerfile.gpu-alt
@@ -51,7 +51,8 @@ RUN uv pip install --system \
tensorboard \
tensorboardX \
tqdm \
- "blobfile>=1.0.5"
+ "blobfile>=1.0.5" \
+ wandb
# Install torchvision separately (base image has torch but may need torchvision update)
RUN pip3 install torchvision==0.15.1+cu117 \
From e07001237448cb60cea5bb13e14a3a6ddbc9e56f Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Sun, 23 Nov 2025 18:13:42 +0800
Subject: [PATCH 19/55] Reduce training epochs from 400 to 100 for improved
efficiency
---
core/utils1/config.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/core/utils1/config.py b/core/utils1/config.py
index d1901d7..7aae627 100644
--- a/core/utils1/config.py
+++ b/core/utils1/config.py
@@ -51,7 +51,7 @@ class DefaultConfigs(ABC):
continue_train = False
epoch_count = 1
last_epoch = -1
- nepoch = 400
+ nepoch = 100
beta1 = 0.9
lr = 0.0001
init_type = "normal"
From 4658a745f41d1821a457ffdbbca9399d91b424f6 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Sun, 23 Nov 2025 23:48:46 +0800
Subject: [PATCH 20/55] Add python-dotenv for environment variable management;
update batch size and num_workers for GPU compatibility; enhance early
stopping with W&B logging
---
Dockerfile.cpu | 3 ++-
Dockerfile.gpu-alt | 3 ++-
core/utils1/config.py | 4 ++--
core/utils1/earlystop.py | 8 ++++++++
pyproject.toml | 1 +
requirements.txt | 1 +
train.py | 14 ++++++++++++++
7 files changed, 30 insertions(+), 4 deletions(-)
diff --git a/Dockerfile.cpu b/Dockerfile.cpu
index 2ef81b9..e5444d5 100644
--- a/Dockerfile.cpu
+++ b/Dockerfile.cpu
@@ -51,7 +51,8 @@ RUN uv pip install --system \
tensorboardX \
tqdm \
"blobfile>=1.0.5" \
- wandb
+ wandb \
+ python-dotenv
# Install PyTorch CPU version using pip directly
RUN pip3 install torch==2.0.0+cpu torchvision==0.15.1+cpu \
diff --git a/Dockerfile.gpu-alt b/Dockerfile.gpu-alt
index a59b1c0..50e4fa4 100644
--- a/Dockerfile.gpu-alt
+++ b/Dockerfile.gpu-alt
@@ -52,7 +52,8 @@ RUN uv pip install --system \
tensorboardX \
tqdm \
"blobfile>=1.0.5" \
- wandb
+ wandb \
+ python-dotenv
# Install torchvision separately (base image has torch but may need torchvision update)
RUN pip3 install torchvision==0.15.1+cu117 \
diff --git a/core/utils1/config.py b/core/utils1/config.py
index 7aae627..81fac4f 100644
--- a/core/utils1/config.py
+++ b/core/utils1/config.py
@@ -14,11 +14,11 @@ class DefaultConfigs(ABC):
datasets_test = ["val_set_1"]
mode = "binary"
class_bal = False
- batch_size = 16 # Reduced from 64 to 16 for 4GB GPU
+ batch_size = 64 # RTX 3090 24GB can handle original batch size
loadSize = 256
cropSize = 224
epoch = "latest"
- num_workers = 4 # Reduced from 20 to 4 to avoid shared memory issues
+ num_workers = 16 # Increased for faster data loading on 24GB GPU
serial_batches = False
isTrain = True
diff --git a/core/utils1/earlystop.py b/core/utils1/earlystop.py
index 257e391..3112d65 100644
--- a/core/utils1/earlystop.py
+++ b/core/utils1/earlystop.py
@@ -1,4 +1,5 @@
import numpy as np
+import wandb
from core.utils1.trainer import Trainer
@@ -44,3 +45,10 @@ def save_checkpoint(self, score: float, trainer: Trainer):
print(f"Validation accuracy increased ({self.score_max:.6f} --> {score:.6f}). Saving model ...")
trainer.save_networks("best")
self.score_max = score
+
+ # Log best model save to wandb
+ if wandb.run is not None:
+ wandb.log({
+ "best_model/score": score,
+ "best_model/improvement": score - self.score_max if self.score_max != -np.Inf else 0
+ })
diff --git a/pyproject.toml b/pyproject.toml
index 552ef44..93e2090 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -27,6 +27,7 @@ dependencies = [
"tqdm",
"blobfile>=1.0.5",
"wandb",
+ "python-dotenv",
"pip",
]
diff --git a/requirements.txt b/requirements.txt
index d8dbfc4..8b5b0b5 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -14,3 +14,4 @@ tensorboardX
tqdm
blobfile>=1.0.5
wandb
+python-dotenv
diff --git a/train.py b/train.py
index f740416..bb68523 100644
--- a/train.py
+++ b/train.py
@@ -3,6 +3,7 @@
import os
import time
+from dotenv import load_dotenv
from tensorboardX import SummaryWriter
from tqdm import tqdm
import wandb
@@ -16,6 +17,17 @@
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
+# Load environment variables from .env file
+load_dotenv()
+
+# Set wandb API key from environment
+wandb_api_key = os.getenv("WANDB")
+if wandb_api_key:
+ os.environ["WANDB_API_KEY"] = wandb_api_key
+ print(f"✓ Loaded wandb API key from .env")
+else:
+ print("⚠️ Warning: WANDB API key not found in .env file")
+
if __name__ == "__main__":
print("=" * 60)
@@ -135,10 +147,12 @@
if trainer.adjust_learning_rate():
print("📉 Learning rate dropped by 10, continuing training...")
log.write("Learning rate dropped by 10, continue training...\n")
+ wandb.log({"early_stopping/lr_dropped": True, "epoch": epoch})
early_stopping = EarlyStopping(patience=cfg.earlystop_epoch, delta=-0.002, verbose=True)
else:
print("\n⏹️ Early stopping triggered")
log.write("Early stopping.\n")
+ wandb.log({"early_stopping/triggered": True, "epoch": epoch})
break
if cfg.warmup:
# print(trainer.scheduler.get_lr()[0])
From df351bba4cb9553dfb39763d56652fc98be655cf Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Mon, 24 Nov 2025 00:15:56 +0800
Subject: [PATCH 21/55] Update docker-compose.yml to include logs volume and
ensure .env file is referenced; add Docker VM setup guide for environment
configuration and troubleshooting
---
DOCKER_VM_SETUP.md | 171 +++++++++++++++++++++++++++++++++++++++++++++
docker-compose.yml | 6 ++
2 files changed, 177 insertions(+)
create mode 100644 DOCKER_VM_SETUP.md
diff --git a/DOCKER_VM_SETUP.md b/DOCKER_VM_SETUP.md
new file mode 100644
index 0000000..7014925
--- /dev/null
+++ b/DOCKER_VM_SETUP.md
@@ -0,0 +1,171 @@
+# Docker Setup Guide for VM
+
+## Quick Start
+
+### 1. Create Data Structure
+```bash
+# Run the setup script
+python setup_data_structure.py
+
+# Or manually create:
+mkdir -p data/train/trainset_1/0_real/video_00000
+mkdir -p data/train/trainset_1/1_fake/video_00000
+mkdir -p data/val/val_set_1/0_real/video_00000
+mkdir -p data/val/val_set_1/1_fake/video_00000
+```
+
+### 2. Add Your Training Data
+Place your extracted frames in:
+- `data/train/trainset_1/0_real/` - Real video frames
+- `data/train/trainset_1/1_fake/` - Fake video frames
+
+Each video should be in its own directory with frames named sequentially:
+```
+data/train/trainset_1/0_real/
+├── video_00000/
+│ ├── 00000.png
+│ ├── 00001.png
+│ └── ...
+├── video_00001/
+│ └── ...
+```
+
+### 3. Ensure .env File Exists
+Your `.env` file should contain your wandb API key:
+```bash
+WANDB="your_api_key_here"
+```
+
+### 4. Run with Docker
+
+#### Using docker-compose (Recommended):
+```bash
+# GPU version
+docker-compose up aigvdet-gpu
+
+# CPU version
+docker-compose up aigvdet-cpu
+```
+
+#### Using docker run:
+```bash
+# GPU version
+docker run --gpus all \
+ -v $(pwd)/data:/app/data \
+ -v $(pwd)/checkpoints:/app/checkpoints \
+ -v $(pwd)/.env:/app/.env:ro \
+ --env-file .env \
+ sacdalance/thesis-aigvdet:gpu \
+ python train.py --exp_name my_experiment
+
+# CPU version
+docker run \
+ -v $(pwd)/data:/app/data \
+ -v $(pwd)/checkpoints:/app/checkpoints \
+ -v $(pwd)/.env:/app/.env:ro \
+ --env-file .env \
+ sacdalance/thesis-aigvdet:cpu \
+ python train.py --exp_name my_experiment
+```
+
+#### On Windows PowerShell:
+```powershell
+# GPU version
+docker run --gpus all `
+ -v ${PWD}/data:/app/data `
+ -v ${PWD}/checkpoints:/app/checkpoints `
+ -v ${PWD}/.env:/app/.env:ro `
+ --env-file .env `
+ sacdalance/thesis-aigvdet:gpu `
+ python train.py --exp_name my_experiment
+
+# CPU version
+docker run `
+ -v ${PWD}/data:/app/data `
+ -v ${PWD}/checkpoints:/app/checkpoints `
+ -v ${PWD}/.env:/app/.env:ro `
+ --env-file .env `
+ sacdalance/thesis-aigvdet:cpu `
+ python train.py --exp_name my_experiment
+```
+
+### 5. Verify Setup Before Training
+
+Run this to check if data is properly mounted:
+```bash
+docker run -v $(pwd)/data:/app/data sacdalance/thesis-aigvdet:gpu ls -la /app/data/train/trainset_1/
+```
+
+## Troubleshooting
+
+### Error: "No such file or directory: '/app/data/train/trainset_1'"
+
+**Solutions:**
+1. **Create the directory structure** (see step 1 above)
+2. **Verify volume mount** - ensure data directory exists locally
+3. **Check permissions** - ensure Docker can read the data directory
+
+### Error: "WANDB API key not found"
+
+**Solution:**
+- Create `.env` file with: `WANDB="your_api_key_here"`
+- Make sure it's mounted: `-v $(pwd)/.env:/app/.env:ro`
+- Or pass directly: `-e WANDB_API_KEY=your_api_key`
+
+### No GPU detected
+
+**Solutions:**
+1. Install nvidia-docker2:
+ ```bash
+ sudo apt-get install -y nvidia-docker2
+ sudo systemctl restart docker
+ ```
+2. Use `--gpus all` flag in docker run
+3. Or use CPU version instead
+
+### Permission Denied
+
+**Solution:**
+```bash
+# Fix data directory permissions
+chmod -R 755 data/
+chmod -R 755 checkpoints/
+```
+
+## Data Download Instructions
+
+See `DATA_SETUP.md` for complete instructions on downloading the training dataset from Baiduyun.
+
+Quick summary:
+1. Download from: https://pan.baidu.com/s/17xmDyFjtcmNsoxmUeImMTQ?pwd=ra95
+2. Extract to `data/train/trainset_1/`
+3. Ensure structure matches above format
+
+## Testing Without Full Dataset
+
+For quick testing, you can create a minimal dataset:
+1. Create a few sample frames (any images will do)
+2. Place in `data/train/trainset_1/0_real/video_00000/`
+3. Name them `00000.png`, `00001.png`, etc.
+4. Copy to `1_fake/` directory as well
+5. Run training to verify setup works
+
+## Training Options
+
+```bash
+# Basic training
+python train.py --exp_name my_experiment
+
+# With custom settings
+python train.py \
+ --exp_name my_experiment \
+ --batch_size 32 \
+ --nepoch 50 \
+ --lr 0.0001
+
+# Continue from checkpoint
+python train.py \
+ --exp_name my_experiment \
+ --continue_train \
+ --epoch 10
+```
diff --git a/docker-compose.yml b/docker-compose.yml
index 14febbf..7f880ad 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -12,11 +12,14 @@ services:
environment:
- NVIDIA_VISIBLE_DEVICES=all
- CUDA_VISIBLE_DEVICES=0
+ env_file:
+ - .env
volumes:
- ./data:/app/data
- ./checkpoints:/app/checkpoints
- ./raft_model:/app/raft_model
- ./logs:/app/logs
+ - ./.env:/app/.env:ro
ports:
- "6006:6006"
shm_size: '8gb'
@@ -36,11 +39,14 @@ services:
dockerfile: Dockerfile.cpu
image: sacdalance/thesis-aigvdet:cpu
container_name: aigvdet-cpu
+ env_file:
+ - .env
volumes:
- ./data:/app/data
- ./checkpoints:/app/checkpoints
- ./raft_model:/app/raft_model
- ./logs:/app/logs
+ - ./.env:/app/.env:ro
ports:
- "6007:6006"
shm_size: '4gb'
From 815d63584f25b4215f16f64e3847afd3376ca644 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Mon, 24 Nov 2025 01:19:12 +0800
Subject: [PATCH 22/55] Update docker-compose.yml to use Dockerfile.gpu-alt for
GPU service; add environment variables for WANDB and PYTHONPATH; fix command
to use python instead of python3.11
---
docker-compose.yml | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/docker-compose.yml b/docker-compose.yml
index 7f880ad..e38e3b3 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -5,13 +5,15 @@ services:
aigvdet-gpu:
build:
context: .
- dockerfile: Dockerfile.gpu
+ dockerfile: Dockerfile.gpu-alt
image: sacdalance/thesis-aigvdet:gpu
container_name: aigvdet-gpu
runtime: nvidia
environment:
- NVIDIA_VISIBLE_DEVICES=all
- CUDA_VISIBLE_DEVICES=0
+ - WANDB=${WANDB}
+ - PYTHONPATH=/app
env_file:
- .env
volumes:
@@ -30,7 +32,7 @@ services:
- driver: nvidia
count: all
capabilities: [gpu]
- command: python3.11 train.py --gpus 0 --exp_name default_exp
+ command: python train.py --gpus 0 --exp_name default_exp
# CPU service
aigvdet-cpu:
@@ -39,6 +41,9 @@ services:
dockerfile: Dockerfile.cpu
image: sacdalance/thesis-aigvdet:cpu
container_name: aigvdet-cpu
+ environment:
+ - WANDB=${WANDB}
+ - PYTHONPATH=/app
env_file:
- .env
volumes:
From 2bc6c1a4a865e87cbed655a83d126e13dbef1068 Mon Sep 17 00:00:00 2001
From: Kyle Pagunsan
Date: Mon, 24 Nov 2025 01:32:10 +0800
Subject: [PATCH 23/55] added unzip
---
DOCKER_VM_SETUP.md | 17 ++++++++++++++++-
Dockerfile.cpu | 1 +
Dockerfile.gpu-alt | 4 +++-
3 files changed, 20 insertions(+), 2 deletions(-)
diff --git a/DOCKER_VM_SETUP.md b/DOCKER_VM_SETUP.md
index 7014925..f28e112 100644
--- a/DOCKER_VM_SETUP.md
+++ b/DOCKER_VM_SETUP.md
@@ -2,7 +2,22 @@
## Quick Start
-### 1. Create Data Structure
+### 1. Automated Data Deployment (Recommended for Vast.ai)
+Since the dataset is large (27GB+), we use a startup script to download it directly to the VM.
+
+1. **Start your instance** (Vast.ai, AWS, etc.).
+2. **Open a terminal** in the container.
+3. **Run the download script**:
+ ```bash
+ # This will download the dataset from Google Drive and unzip it
+ chmod +x download_data.sh
+ ./download_data.sh
+ ```
+ *Note: The script defaults to the project's Google Drive ID. You can pass a different ID if needed: `./download_data.sh `*
+
+### 2. Manual Data Setup (Local Development)
+If you are running locally or want to set up manually:
+
```bash
# Run the setup script
python setup_data_structure.py
diff --git a/Dockerfile.cpu b/Dockerfile.cpu
index e5444d5..4cb6bcb 100644
--- a/Dockerfile.cpu
+++ b/Dockerfile.cpu
@@ -17,6 +17,7 @@ RUN apt-get update && apt-get install -y \
libxrender-dev \
libgomp1 \
libgl1-mesa-glx \
+ unzip \
&& rm -rf /var/lib/apt/lists/*
# Install uv
diff --git a/Dockerfile.gpu-alt b/Dockerfile.gpu-alt
index 50e4fa4..15879fc 100644
--- a/Dockerfile.gpu-alt
+++ b/Dockerfile.gpu-alt
@@ -18,6 +18,7 @@ RUN apt-get update && apt-get install -y \
libxrender-dev \
libgomp1 \
libgl1-mesa-glx \
+ unzip \
&& rm -rf /var/lib/apt/lists/*
# Install uv
@@ -53,7 +54,8 @@ RUN uv pip install --system \
tqdm \
"blobfile>=1.0.5" \
wandb \
- python-dotenv
+ python-dotenv \
+ gdown
# Install torchvision separately (base image has torch but may need torchvision update)
RUN pip3 install torchvision==0.15.1+cu117 \
From 94bed71c40a69134cbf8eea27dff2c3cdcdac664 Mon Sep 17 00:00:00 2001
From: Kyle Pagunsan
Date: Mon, 24 Nov 2025 01:40:44 +0800
Subject: [PATCH 24/55] feat: Add script to download and extract dataset from
Google Drive.
---
download_data.sh | 50 ++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 50 insertions(+)
create mode 100644 download_data.sh
diff --git a/download_data.sh b/download_data.sh
new file mode 100644
index 0000000..f9a4879
--- /dev/null
+++ b/download_data.sh
@@ -0,0 +1,50 @@
+#!/bin/bash
+set -e
+
+# Configuration
+# Default File ID provided by user, can be overridden by argument
+FILE_ID="${1:-1YO3qRKbWxOYEm86Vy9QlGMjyi5Q_A6m0}"
+DATA_DIR="/app/data"
+ZIP_FILE="${DATA_DIR}/data.zip"
+
+echo "Starting data download setup..."
+
+# 1. Install dependencies if missing
+if ! command -v gdown &> /dev/null; then
+ echo "Installing gdown..."
+ pip install gdown
+fi
+
+if ! command -v unzip &> /dev/null; then
+ echo "Installing unzip..."
+ apt-get update && apt-get install -y unzip
+fi
+
+# 2. Check if data already exists
+if [ -d "${DATA_DIR}/train" ]; then
+ echo "Data directory ${DATA_DIR}/train already exists."
+ read -p "Do you want to re-download? (y/N) " -n 1 -r
+ echo
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+ echo "Skipping download."
+ exit 0
+ fi
+fi
+
+# 3. Create data directory
+mkdir -p "$DATA_DIR"
+
+# 4. Download file
+echo "Downloading data from Google Drive (ID: $FILE_ID)..."
+gdown "$FILE_ID" -O "$ZIP_FILE"
+
+# 5. Extract
+echo "Extracting data..."
+unzip -o "$ZIP_FILE" -d "$DATA_DIR"
+
+# 6. Cleanup
+echo "Cleaning up zip file..."
+rm "$ZIP_FILE"
+
+echo "Data setup complete!"
+ls -F "$DATA_DIR"
From 2646387e96dd80ae080dc9e2170e82d071468666 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Mon, 24 Nov 2025 02:50:10 +0800
Subject: [PATCH 25/55] fix: Specify .env file for loading environment
variables and correct WANDB API key assignment
---
Dockerfile.cpu | 2 +-
Dockerfile.gpu-alt | 3 ++-
train.py | 4 ++--
3 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/Dockerfile.cpu b/Dockerfile.cpu
index 4cb6bcb..5c4afe4 100644
--- a/Dockerfile.cpu
+++ b/Dockerfile.cpu
@@ -32,7 +32,7 @@ COPY README.md ./
COPY core/ ./core/
COPY networks/ ./networks/
COPY train.py test.py demo.py ./
-COPY train.sh test.sh demo.sh ./
+COPY train.sh test.sh demo.sh download_data.sh ./
# Create directories for data and checkpoints
RUN mkdir -p /app/data /app/checkpoints /app/raft_model
diff --git a/Dockerfile.gpu-alt b/Dockerfile.gpu-alt
index 15879fc..43e5b29 100644
--- a/Dockerfile.gpu-alt
+++ b/Dockerfile.gpu-alt
@@ -33,7 +33,8 @@ COPY README.md ./
COPY core/ ./core/
COPY networks/ ./networks/
COPY train.py test.py demo.py ./
-COPY train.sh test.sh demo.sh ./
+COPY train.sh test.sh demo.sh download_data.sh ./
+
# Create directories for data and checkpoints
RUN mkdir -p /app/data /app/checkpoints /app/raft_model
diff --git a/train.py b/train.py
index bb68523..a4e0444 100644
--- a/train.py
+++ b/train.py
@@ -18,12 +18,12 @@
ssl._create_default_https_context = ssl._create_unverified_context
# Load environment variables from .env file
-load_dotenv()
+load_dotenv('thesis.env')
# Set wandb API key from environment
wandb_api_key = os.getenv("WANDB")
if wandb_api_key:
- os.environ["WANDB_API_KEY"] = wandb_api_key
+ os.environ["WANDB"] = wandb_api_key
print(f"✓ Loaded wandb API key from .env")
else:
print("⚠️ Warning: WANDB API key not found in .env file")
From 1ee6796b0427e785dcb7da8b2126ecc4097d95b3 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Mon, 24 Nov 2025 13:07:57 +0800
Subject: [PATCH 26/55] fix: Update configuration parameters for training;
adjust loadSize and cropSize, and modify num_workers for optimal performance
---
core/utils1/config.py | 6 +++---
train.py | 34 ++++++++++++++++++++++++----------
2 files changed, 27 insertions(+), 13 deletions(-)
diff --git a/core/utils1/config.py b/core/utils1/config.py
index 81fac4f..48a5f71 100644
--- a/core/utils1/config.py
+++ b/core/utils1/config.py
@@ -15,10 +15,10 @@ class DefaultConfigs(ABC):
mode = "binary"
class_bal = False
batch_size = 64 # RTX 3090 24GB can handle original batch size
- loadSize = 256
- cropSize = 224
+ loadSize = 448
+ cropSize = 448
epoch = "latest"
- num_workers = 16 # Increased for faster data loading on 24GB GPU
+ num_workers = 8 # Increased for faster data loading on 24GB GPU
serial_batches = False
isTrain = True
diff --git a/train.py b/train.py
index a4e0444..cb0e3a6 100644
--- a/train.py
+++ b/train.py
@@ -69,6 +69,9 @@
early_stopping = EarlyStopping(patience=cfg.earlystop_epoch, delta=-0.001, verbose=True)
print(f"✓ Model ready (Architecture: {cfg.arch})")
+ # Track best model accuracy
+ best_acc = 0.0
+
print("\n" + "=" * 60)
print(f"Starting training for {cfg.nepoch} epochs")
print("=" * 60 + "\n")
@@ -128,19 +131,30 @@
"epoch": epoch
})
- # Save best checkpoint as wandb artifact
- if os.path.exists(os.path.join(cfg.ckpt_dir, "model_epoch_best.pth")):
- artifact = wandb.Artifact(
- name=f"{cfg.exp_name}_best_model",
- type="model",
- description=f"Best model checkpoint at epoch {epoch}"
- )
- artifact.add_file(os.path.join(cfg.ckpt_dir, "model_epoch_best.pth"))
- wandb.log_artifact(artifact)
-
print(f"✓ Validation Results - AP: {val_results['AP']:.4f} | ACC: {val_results['ACC']:.4f} | AUC: {val_results['AUC']:.4f}")
log.write(f"(Val @ epoch {epoch}) AP: {val_results['AP']}; ACC: {val_results['ACC']}\n")
+ # Save best model if accuracy improves
+ if val_results['ACC'] > best_acc:
+ print(f"⭐ New best model! (ACC: {best_acc:.4f} -> {val_results['ACC']:.4f})")
+ best_acc = val_results['ACC']
+ trainer.save_networks("best")
+
+ best_model_path = os.path.join(cfg.ckpt_dir, "model_epoch_best.pth")
+ if os.path.exists(best_model_path):
+ # 1. Log as Artifact
+ artifact = wandb.Artifact(
+ name=f"{cfg.exp_name}_best_model",
+ type="model",
+ description=f"Best model checkpoint at epoch {epoch} (ACC: {best_acc:.4f})"
+ )
+ artifact.add_file(best_model_path)
+ wandb.log_artifact(artifact)
+
+ # 2. Force upload to Files tab immediately
+ wandb.save(best_model_path, base_path=cfg.root_dir, policy="now")
+ print(f"✓ Uploaded best model to WandB Files: {best_model_path}")
+
if cfg.earlystop:
early_stopping(val_results["ACC"], trainer)
if early_stopping.early_stop:
From d30ac612c78c5935f447f604be074a59a8f15642 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Mon, 24 Nov 2025 23:34:27 +0800
Subject: [PATCH 27/55] fix: Update configuration parameters for image
processing; adjust loadSize, blur_prob, and nepoch for improved performance
and alignment with research paper
---
core/utils1/config.py | 21 ++++++++++++---------
core/utils1/datasets.py | 26 ++++++++++++++++++--------
2 files changed, 30 insertions(+), 17 deletions(-)
diff --git a/core/utils1/config.py b/core/utils1/config.py
index 48a5f71..6db7e0f 100644
--- a/core/utils1/config.py
+++ b/core/utils1/config.py
@@ -15,20 +15,21 @@ class DefaultConfigs(ABC):
mode = "binary"
class_bal = False
batch_size = 64 # RTX 3090 24GB can handle original batch size
- loadSize = 448
+ loadSize = 512 # Resizes the image, this is used as it is standard for HIGH RES CNNs and must have margin before cropping
cropSize = 448
epoch = "latest"
- num_workers = 8 # Increased for faster data loading on 24GB GPU
+ num_workers = 8 # Increased for faster data loading on 24GB GPU (choose 8 or 16) - doesnt affect accuracy but only throughput
serial_batches = False
isTrain = True
- # data augmentation
+ # data augmentation - to match the paper's augmentation rate (10%), set blur and jpg to 0.5
rz_interp = ["bilinear"]
- # blur_prob = 0.0
- blur_prob = 0.1
+ # blur_prob = 0.1 - resnet50 template
+ blur_prob = 0.05
blur_sig = [0.5]
- # jpg_prob = 0.0
- jpg_prob = 0.1
+ # jpg_prob = 0.1 - resnet50 template
+ jpg_prob = 0.05
+ # P(augmented) = 1-(1-0.5)(1-0.05) = 0.975
jpg_method = ["cv2"]
jpg_qual = [75]
gray_prob = 0.0
@@ -41,7 +42,8 @@ class DefaultConfigs(ABC):
warmup = False
# warmup = True
warmup_epoch = 3
- earlystop = True
+ # earlystop = True - resnet50 template, training ends only when lr reaches 1e-6 which trainer already supports (Trainer.adjust_learning_rate(min_lr=1e-6))
+ earlystop = False
earlystop_epoch = 5
optim = "adam"
new_optim = False
@@ -51,7 +53,8 @@ class DefaultConfigs(ABC):
continue_train = False
epoch_count = 1
last_epoch = -1
- nepoch = 100
+ # nepoch = 100, try
+ nepoch = 400
beta1 = 0.9
lr = 0.0001
init_type = "normal"
diff --git a/core/utils1/datasets.py b/core/utils1/datasets.py
index 72d4654..ec6bd4e 100644
--- a/core/utils1/datasets.py
+++ b/core/utils1/datasets.py
@@ -27,20 +27,30 @@ def dataset_folder(root: str, cfg: CONFIGCLASS):
def binary_dataset(root: str, cfg: CONFIGCLASS):
+ # identity_transform = transforms.Lambda(lambda img: img)
+ # rz_func = identity_transform
+ # issue here, destroys performance as no resizing happens at all that go straight to cropping even if they are different resolutions
+
identity_transform = transforms.Lambda(lambda img: img)
-
- rz_func = identity_transform
-
+
+ # Enable resize (paper implies resize > crop for random cropping)
+ if cfg.aug_resize:
+ rz_func = transforms.Lambda(lambda img: custom_resize(img, cfg))
+ else:
+ rz_func = identity_transform
+
+ # Crop to cfg.cropSize (paper uses 448)
if cfg.isTrain:
- crop_func = transforms.RandomCrop((448,448))
+ crop_func = transforms.RandomCrop((cfg.cropSize, cfg.cropSize))
else:
- crop_func = transforms.CenterCrop((448,448)) if cfg.aug_crop else identity_transform
+ crop_func = transforms.CenterCrop((cfg.cropSize, cfg.cropSize)) if cfg.aug_crop else identity_transform
+ # Flip only in training if enabled
if cfg.isTrain and cfg.aug_flip:
flip_func = transforms.RandomHorizontalFlip()
else:
- flip_func = identity_transform
-
+ flip_func = identity_transform # fallback
+
return datasets.ImageFolder(
root,
@@ -48,8 +58,8 @@ def binary_dataset(root: str, cfg: CONFIGCLASS):
[
rz_func,
#change
- transforms.Lambda(lambda img: blur_jpg_augment(img, cfg)),
crop_func,
+ transforms.Lambda(lambda img: blur_jpg_augment(img, cfg)),
flip_func,
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
From 89656a8aa58ecab1b22af6bee99df7061f08fe07 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Tue, 25 Nov 2025 00:43:49 +0800
Subject: [PATCH 28/55] feat: Add data preparation and evaluation script for
dataset processing; includes frame extraction and optical flow generation
---
prepare_data.py | 141 ++++++++++++++++++++++++++++++++++++++++++++
recreate_table_2.py | 63 ++++++++++++++++++++
test.py | 64 +++++++++++++++-----
3 files changed, 253 insertions(+), 15 deletions(-)
create mode 100644 prepare_data.py
create mode 100644 recreate_table_2.py
diff --git a/prepare_data.py b/prepare_data.py
new file mode 100644
index 0000000..d96f827
--- /dev/null
+++ b/prepare_data.py
@@ -0,0 +1,141 @@
+import sys
+import argparse
+import os
+import cv2
+import glob
+import numpy as np
+import torch
+from PIL import Image
+from tqdm import tqdm
+from natsort import natsorted
+
+# Add core to path for imports
+sys.path.append('core')
+from raft import RAFT
+from utils import flow_viz
+from utils.utils import InputPadder
+
+DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
+
+def load_image(imfile):
+ img = np.array(Image.open(imfile)).astype(np.uint8)
+ img = torch.from_numpy(img).permute(2, 0, 1).float()
+ return img[None].to(DEVICE)
+
+def save_flow(img, flo, output_path):
+ img = img[0].permute(1,2,0).cpu().numpy()
+ flo = flo[0].permute(1,2,0).cpu().numpy()
+
+ # map flow to rgb image
+ flo = flow_viz.flow_to_image(flo)
+ cv2.imwrite(output_path, flo)
+
+def video_to_frames(video_path, output_folder):
+ if not os.path.exists(output_folder):
+ os.makedirs(output_folder)
+
+ # Check if frames already exist to skip
+ existing_frames = glob.glob(os.path.join(output_folder, "*.png"))
+ if len(existing_frames) > 0:
+ return sorted(existing_frames)
+
+ cap = cv2.VideoCapture(video_path)
+ frame_count = 0
+
+ while cap.isOpened():
+ ret, frame = cap.read()
+ if not ret:
+ break
+
+ frame_filename = os.path.join(output_folder, f"frame_{frame_count:05d}.png")
+ cv2.imwrite(frame_filename, frame)
+ frame_count += 1
+
+ cap.release()
+
+ images = glob.glob(os.path.join(output_folder, '*.png')) + \
+ glob.glob(os.path.join(output_folder, '*.jpg'))
+ return sorted(images)
+
+def process_dataset(args):
+ # Load RAFT model once
+ print(f"Loading RAFT model from {args.model}...")
+ model = torch.nn.DataParallel(RAFT(args))
+ model.load_state_dict(torch.load(args.model, map_location=torch.device(DEVICE)))
+ model = model.module
+ model.to(DEVICE)
+ model.eval()
+ print("✓ RAFT model loaded")
+
+ # Structure: source_dir / [0_real, 1_fake] / video.mp4
+ for label in ["0_real", "1_fake"]:
+ source_label_dir = os.path.join(args.source_dir, label)
+ if not os.path.exists(source_label_dir):
+ print(f"Skipping {label}, directory not found: {source_label_dir}")
+ continue
+
+ videos = glob.glob(os.path.join(source_label_dir, "*.mp4")) + \
+ glob.glob(os.path.join(source_label_dir, "*.avi")) + \
+ glob.glob(os.path.join(source_label_dir, "*.mov"))
+
+ print(f"Found {len(videos)} videos in {label}")
+
+ for video_path in tqdm(videos, desc=f"Processing {label}"):
+ video_name = os.path.splitext(os.path.basename(video_path))[0]
+
+ # Define output paths
+ # Original frames: args.output_rgb_dir / label / video_name / frames
+ rgb_out_dir = os.path.join(args.output_rgb_dir, label, video_name)
+ # Optical flow: args.output_flow_dir / label / video_name / frames
+ flow_out_dir = os.path.join(args.output_flow_dir, label, video_name)
+
+ if not os.path.exists(flow_out_dir):
+ os.makedirs(flow_out_dir)
+
+ # 1. Extract Frames
+ images = video_to_frames(video_path, rgb_out_dir)
+ images = natsorted(images)
+
+ if len(images) < 2:
+ continue
+
+ # 2. Generate Optical Flow
+ # Check if flow already exists
+ existing_flow = glob.glob(os.path.join(flow_out_dir, "*.png"))
+ if len(existing_flow) >= len(images) - 1:
+ continue
+
+ with torch.no_grad():
+ for i, (imfile1, imfile2) in enumerate(zip(images[:-1], images[1:])):
+ # Output filename matches input filename
+ flow_filename = os.path.basename(imfile1)
+ flow_output_path = os.path.join(flow_out_dir, flow_filename)
+
+ if os.path.exists(flow_output_path):
+ continue
+
+ image1 = load_image(imfile1)
+ image2 = load_image(imfile2)
+
+ padder = InputPadder(image1.shape)
+ image1, image2 = padder.pad(image1, image2)
+
+ flow_low, flow_up = model(image1, image2, iters=20, test_mode=True)
+
+ save_flow(image1, flow_up, flow_output_path)
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--source_dir', required=True, help="Path to folder containing 0_real/1_fake video folders")
+ parser.add_argument('--output_rgb_dir', required=True, help="Output path for RGB frames")
+ parser.add_argument('--output_flow_dir', required=True, help="Output path for Optical Flow frames")
+ parser.add_argument('--model', default="raft_model/raft-things.pth", help="Path to RAFT model checkpoint")
+
+ # RAFT args
+ parser.add_argument('--small', action='store_true', help='use small model')
+ parser.add_argument('--mixed_precision', action='store_true', help='use mixed precision')
+ parser.add_argument('--alternate_corr', action='store_true', help='use efficent correlation implementation')
+
+ args = parser.parse_args()
+
+ process_dataset(args)
diff --git a/recreate_table_2.py b/recreate_table_2.py
new file mode 100644
index 0000000..61f26aa
--- /dev/null
+++ b/recreate_table_2.py
@@ -0,0 +1,63 @@
+import argparse
+import subprocess
+import os
+import sys
+
+def run_command(command):
+ print(f"Running: {command}")
+ try:
+ subprocess.check_call(command, shell=True)
+ except subprocess.CalledProcessError as e:
+ print(f"Error running command: {command}")
+ sys.exit(1)
+
+def main():
+ parser = argparse.ArgumentParser(description="Recreate Table 2 results for a specific dataset.")
+ parser.add_argument("dataset", help="Name of the dataset (e.g., moonvalley, videocraft, pika, neverends)")
+ args = parser.parse_args()
+
+ dataset_name = args.dataset
+
+ # Define paths
+ # Using /app/data for Docker/Jupyter environment compatibility
+ source_video_dir = f"/app/data/test/T2V/{dataset_name}"
+ output_rgb_dir = f"/app/data/test/original/T2V/{dataset_name}"
+ output_flow_dir = f"/app/data/test/T2V/{dataset_name}_flow"
+
+ result_csv = f"/app/data/results/{dataset_name}.csv"
+ result_no_cp_csv = f"/app/data/results/{dataset_name}_no_cp.csv"
+
+ raft_model = "/app/raft_model/raft-things.pth"
+ optical_model = "/app/checkpoints/optical.pth"
+ original_model = "/app/checkpoints/original.pth"
+
+ print(f"--- Processing Dataset: {dataset_name} ---")
+
+ # Check if source directory exists
+ if not os.path.exists(source_video_dir):
+ print(f"Error: Source video directory not found: {source_video_dir}")
+ print("Please download the test videos and place them in the correct folder.")
+ sys.exit(1)
+
+ # Step 1: Prepare Data
+ print("\n[Step 1] Preparing Data (Extracting Frames & Generating Optical Flow)...")
+ # Note: We use sys.executable to ensure we use the same python interpreter
+ cmd_prepare = f'"{sys.executable}" prepare_data.py --source_dir "{source_video_dir}" --output_rgb_dir "{output_rgb_dir}" --output_flow_dir "{output_flow_dir}" --model "{raft_model}"'
+ run_command(cmd_prepare)
+
+ # Step 2: Run Standard Evaluation
+ print("\n[Step 2] Running Standard Evaluation (AIGVDet, Sspatial, Soptical)...")
+ cmd_test = f'"{sys.executable}" test.py -fop "{output_flow_dir}" -for "{output_rgb_dir}" -mop "{optical_model}" -mor "{original_model}" -e "{result_csv}"'
+ run_command(cmd_test)
+
+ # Step 3: Run No-Crop Evaluation
+ print("\n[Step 3] Running No-Crop Evaluation (Soptical no cp)...")
+ cmd_test_no_cp = f'"{sys.executable}" test.py --no_crop -fop "{output_flow_dir}" -for "{output_rgb_dir}" -mop "{optical_model}" -mor "{original_model}" -e "{result_no_cp_csv}"'
+ run_command(cmd_test_no_cp)
+
+ print("\n--- Done! ---")
+ print(f"Standard Results saved to: {result_csv}")
+ print(f"No-Crop Results saved to: {result_no_cp_csv}")
+
+if __name__ == "__main__":
+ main()
diff --git a/test.py b/test.py
index e25e90b..18a8e61 100644
--- a/test.py
+++ b/test.py
@@ -64,6 +64,7 @@
parser.add_argument("--use_cpu", action="store_true", help="uses gpu by default, turn on to use cpu")
parser.add_argument("--arch", type=str, default="resnet50")
parser.add_argument("--aug_norm", type=str2bool, default=True)
+ parser.add_argument("--no_crop", action="store_true", help="disable center crop")
args = parser.parse_args()
subfolder_count = 0
@@ -88,12 +89,19 @@
model_or.cuda()
- trans = transforms.Compose(
- (
- transforms.CenterCrop((448,448)),
- transforms.ToTensor(),
+ if args.no_crop:
+ trans = transforms.Compose(
+ (
+ transforms.ToTensor(),
+ )
+ )
+ else:
+ trans = transforms.Compose(
+ (
+ transforms.CenterCrop((448,448)),
+ transforms.ToTensor(),
+ )
)
- )
print("*" * 50)
@@ -104,6 +112,8 @@
tn=0
y_true=[]
y_pred=[]
+ y_pred_original=[]
+ y_pred_optical=[]
# create an empty DataFrame
df = pd.DataFrame(columns=['name', 'pro','flag','optical_pro','original_pro'])
@@ -130,7 +140,8 @@
print("test subfolder:", subfolder_name)
# Traverse through sub-subfolders within a subfolder.
- for subsubfolder_name in os.listdir(original_subfolder_path):
+ video_list = os.listdir(original_subfolder_path)
+ for subsubfolder_name in tqdm(video_list, desc=f"Testing {subfolder_name}"):
original_subsubfolder_path = os.path.join(original_subfolder_path, subsubfolder_name)
optical_subsubfolder_path = os.path.join(optical_subfolder_path, subsubfolder_name)
if os.path.isdir(optical_subsubfolder_path):
@@ -145,7 +156,8 @@
original_file_list = sorted(glob.glob(os.path.join(original_subsubfolder_path, "*.jpg")) + glob.glob(os.path.join(original_subsubfolder_path, "*.png"))+glob.glob(os.path.join(original_subsubfolder_path, "*.JPEG")))
original_prob_sum=0
- for img_path in tqdm(original_file_list, dynamic_ncols=True, disable=len(original_file_list) <= 1):
+ # Inner loop for frames - disable tqdm to avoid nested clutter
+ for img_path in original_file_list:
img = Image.open(img_path).convert("RGB")
img = trans(img)
@@ -163,12 +175,13 @@
original_predict=original_prob_sum/len(original_file_list)
- print("original prob",original_predict)
+ # print("original prob",original_predict)
#Detect optical flow
optical_file_list = sorted(glob.glob(os.path.join(optical_subsubfolder_path, "*.jpg")) + glob.glob(os.path.join(optical_subsubfolder_path, "*.png"))+glob.glob(os.path.join(optical_subsubfolder_path, "*.JPEG")))
optical_prob_sum=0
- for img_path in tqdm(optical_file_list, dynamic_ncols=True, disable=len(original_file_list) <= 1):
+ # Inner loop for frames - disable tqdm
+ for img_path in optical_file_list:
img = Image.open(img_path).convert("RGB")
img = trans(img)
@@ -188,13 +201,15 @@
index1=index1+1
optical_predict=optical_prob_sum/len(optical_file_list)
- print("optical prob",optical_predict)
+ # print("optical prob",optical_predict)
predict=original_predict*0.5+optical_predict*0.5
- print(f"flag:{flag} predict:{predict}")
+ # print(f"flag:{flag} predict:{predict}")
# y_true.append((float)(flag))
y_true.append((flag))
y_pred.append(predict)
+ y_pred_original.append(original_predict)
+ y_pred_optical.append(optical_predict)
if flag==0:
n+=1
if predict= args.threshold else 0 for p in y_pred])
+
+ # Calculate metrics for individual streams
+ acc_original = accuracy_score(y_true, [1 if p >= args.threshold else 0 for p in y_pred_original])
+ auc_original = roc_auc_score(y_true, y_pred_original)
+
+ acc_optical = accuracy_score(y_true, [1 if p >= args.threshold else 0 for p in y_pred_optical])
+ auc_optical = roc_auc_score(y_true, y_pred_optical)
+
+ print("-" * 30)
+ print("AIGVDet (Fused) Results:")
print(f"tnr:{tn/n}")
- # print(f"f_acc:{f_acc}")
print(f"tpr:{tp/p}")
- print(f"acc:{(tp+tn)/(p+n)}")
- # print(f"acc:{acc}")
+ print(f"acc:{acc}")
print(f"ap:{ap}")
print(f"auc:{auc}")
+
+ print("-" * 30)
+ print("Sspatial (Original RGB) Results:")
+ print(f"acc:{acc_original}")
+ print(f"auc:{auc_original}")
+
+ print("-" * 30)
+ print("Soptical (Optical Flow) Results:")
+ print(f"acc:{acc_optical}")
+ print(f"auc:{auc_optical}")
+ print("-" * 30)
print(f"p:{p}")
print(f"n:{n}")
print(f"tp:{tp}")
From 51a45062e32c53d1ed4e10479730dc4834faaf54 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Tue, 25 Nov 2025 00:47:53 +0800
Subject: [PATCH 29/55] fix: Update configuration parameters for data
augmentation and training; adjust jpg_prob, earlystop, save_epoch_freq, and
nepoch for improved training dynamics
---
core/utils1/config.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/core/utils1/config.py b/core/utils1/config.py
index 6db7e0f..07af9b5 100644
--- a/core/utils1/config.py
+++ b/core/utils1/config.py
@@ -29,7 +29,7 @@ class DefaultConfigs(ABC):
blur_sig = [0.5]
# jpg_prob = 0.1 - resnet50 template
jpg_prob = 0.05
- # P(augmented) = 1-(1-0.5)(1-0.05) = 0.975
+ # P(augmented) = 1-(1-0.5)(1-0.05) = 0.9
jpg_method = ["cv2"]
jpg_qual = [75]
gray_prob = 0.0
@@ -43,18 +43,18 @@ class DefaultConfigs(ABC):
# warmup = True
warmup_epoch = 3
# earlystop = True - resnet50 template, training ends only when lr reaches 1e-6 which trainer already supports (Trainer.adjust_learning_rate(min_lr=1e-6))
- earlystop = False
+ earlystop = True
earlystop_epoch = 5
optim = "adam"
new_optim = False
loss_freq = 400
save_latest_freq = 2000
- save_epoch_freq = 20
+ save_epoch_freq = 5
continue_train = False
epoch_count = 1
last_epoch = -1
# nepoch = 100, try
- nepoch = 400
+ nepoch = 20
beta1 = 0.9
lr = 0.0001
init_type = "normal"
From 821da35ba5fa1958de7973b77c9916fcba1319a9 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Tue, 25 Nov 2025 02:52:49 +0800
Subject: [PATCH 30/55] fix: Update configuration parameters for training;
increase num_workers to 16 and nepoch to 50 for improved performance, adjust
jpg_qual to a range for better augmentation
---
core/utils1/config.py | 8 ++++----
train.py | 4 ++--
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/core/utils1/config.py b/core/utils1/config.py
index 07af9b5..a66d379 100644
--- a/core/utils1/config.py
+++ b/core/utils1/config.py
@@ -14,11 +14,11 @@ class DefaultConfigs(ABC):
datasets_test = ["val_set_1"]
mode = "binary"
class_bal = False
- batch_size = 64 # RTX 3090 24GB can handle original batch size
+ batch_size = 64 # RTX 3090 24GB can handle original batch size
loadSize = 512 # Resizes the image, this is used as it is standard for HIGH RES CNNs and must have margin before cropping
cropSize = 448
epoch = "latest"
- num_workers = 8 # Increased for faster data loading on 24GB GPU (choose 8 or 16) - doesnt affect accuracy but only throughput
+ num_workers = 16 # Increased for faster data loading on 24GB GPU (choose 8 or 16) - doesnt affect accuracy but only throughput
serial_batches = False
isTrain = True
@@ -31,7 +31,7 @@ class DefaultConfigs(ABC):
jpg_prob = 0.05
# P(augmented) = 1-(1-0.5)(1-0.05) = 0.9
jpg_method = ["cv2"]
- jpg_qual = [75]
+ jpg_qual = list(range(70, 91))
gray_prob = 0.0
aug_resize = True
aug_crop = True
@@ -54,7 +54,7 @@ class DefaultConfigs(ABC):
epoch_count = 1
last_epoch = -1
# nepoch = 100, try
- nepoch = 20
+ nepoch = 50
beta1 = 0.9
lr = 0.0001
init_type = "normal"
diff --git a/train.py b/train.py
index cb0e3a6..6b9c862 100644
--- a/train.py
+++ b/train.py
@@ -26,7 +26,7 @@
os.environ["WANDB"] = wandb_api_key
print(f"✓ Loaded wandb API key from .env")
else:
- print("⚠️ Warning: WANDB API key not found in .env file")
+ print("⚠️ Warning: WANDB API key not found in .env file")
if __name__ == "__main__":
@@ -164,7 +164,7 @@
wandb.log({"early_stopping/lr_dropped": True, "epoch": epoch})
early_stopping = EarlyStopping(patience=cfg.earlystop_epoch, delta=-0.002, verbose=True)
else:
- print("\n⏹️ Early stopping triggered")
+ print("\n⏹️ Early stopping triggered")
log.write("Early stopping.\n")
wandb.log({"early_stopping/triggered": True, "epoch": epoch})
break
From 748248cc34914b3e5a5faad2ee9de91faea21deb Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Tue, 25 Nov 2025 08:36:11 +0800
Subject: [PATCH 31/55] fix: Update early stopping and training metrics; remove
local model saving from early stopping, define metrics for tracking best
values in wandb summary
---
core/utils1/earlystop.py | 11 +++--------
train.py | 21 +++++++++++++++++++++
2 files changed, 24 insertions(+), 8 deletions(-)
diff --git a/core/utils1/earlystop.py b/core/utils1/earlystop.py
index 3112d65..2f0164c 100644
--- a/core/utils1/earlystop.py
+++ b/core/utils1/earlystop.py
@@ -42,13 +42,8 @@ def __call__(self, score: float, trainer: Trainer):
def save_checkpoint(self, score: float, trainer: Trainer):
"""Saves model when validation loss decrease."""
if self.verbose:
- print(f"Validation accuracy increased ({self.score_max:.6f} --> {score:.6f}). Saving model ...")
- trainer.save_networks("best")
+ print(f"Validation accuracy increased ({self.score_max:.6f} --> {score:.6f}).")
+ # trainer.save_networks("best") - Handled globally in train.py to avoid overwriting with local bests
self.score_max = score
- # Log best model save to wandb
- if wandb.run is not None:
- wandb.log({
- "best_model/score": score,
- "best_model/improvement": score - self.score_max if self.score_max != -np.Inf else 0
- })
+ self.score_max = score
diff --git a/train.py b/train.py
index 6b9c862..a98a33e 100644
--- a/train.py
+++ b/train.py
@@ -64,6 +64,11 @@
)
print(f"✓ wandb tracking enabled: {wandb.run.url}")
+ # Define metrics to track max value in summary table
+ wandb.define_metric("val/ACC", summary="max")
+ wandb.define_metric("val/AUC", summary="max")
+ wandb.define_metric("val/AP", summary="max")
+
print("\nInitializing model...")
trainer = Trainer(cfg)
early_stopping = EarlyStopping(patience=cfg.earlystop_epoch, delta=-0.001, verbose=True)
@@ -128,6 +133,7 @@
"val/AUC": val_results["AUC"],
"val/TPR": val_results["TPR"],
"val/TNR": val_results["TNR"],
+ "val/best_ACC": max(best_acc, val_results["ACC"]),
"epoch": epoch
})
@@ -137,7 +143,22 @@
# Save best model if accuracy improves
if val_results['ACC'] > best_acc:
print(f"⭐ New best model! (ACC: {best_acc:.4f} -> {val_results['ACC']:.4f})")
+ # Calculate improvement
+ improvement = val_results['ACC'] - best_acc
best_acc = val_results['ACC']
+
+ # Explicitly update summary for the table with ALL metrics from this best epoch
+ wandb.run.summary["best_acc_score"] = best_acc
+ wandb.run.summary["best_epoch"] = epoch
+ wandb.run.summary["best_AUC"] = val_results["AUC"]
+ wandb.run.summary["best_AP"] = val_results["AP"]
+ wandb.run.summary["best_TPR"] = val_results["TPR"]
+ wandb.run.summary["best_TNR"] = val_results["TNR"]
+
+ # Force update the specific columns you want in the table
+ wandb.run.summary["best_model/score"] = best_acc
+ wandb.run.summary["best_model/improvement"] = improvement
+
trainer.save_networks("best")
best_model_path = os.path.join(cfg.ckpt_dir, "model_epoch_best.pth")
From 9117dc38d9f66f10bb2bee95155f564953f5c357 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Tue, 25 Nov 2025 08:37:14 +0800
Subject: [PATCH 32/55] fix: Remove redundant assignment of score_max in
EarlyStopping class
---
core/utils1/earlystop.py | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/core/utils1/earlystop.py b/core/utils1/earlystop.py
index 2f0164c..3fe8b50 100644
--- a/core/utils1/earlystop.py
+++ b/core/utils1/earlystop.py
@@ -44,6 +44,4 @@ def save_checkpoint(self, score: float, trainer: Trainer):
if self.verbose:
print(f"Validation accuracy increased ({self.score_max:.6f} --> {score:.6f}).")
# trainer.save_networks("best") - Handled globally in train.py to avoid overwriting with local bests
- self.score_max = score
-
- self.score_max = score
+ self.score_max = score
From b70220ccc31925acc33a80cfb5ce316313160524 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Tue, 25 Nov 2025 08:40:15 +0800
Subject: [PATCH 33/55] fix: Remove redundant metric definitions and summary
updates for wandb tracking
---
train.py | 6 ------
1 file changed, 6 deletions(-)
diff --git a/train.py b/train.py
index a98a33e..03c7839 100644
--- a/train.py
+++ b/train.py
@@ -64,11 +64,6 @@
)
print(f"✓ wandb tracking enabled: {wandb.run.url}")
- # Define metrics to track max value in summary table
- wandb.define_metric("val/ACC", summary="max")
- wandb.define_metric("val/AUC", summary="max")
- wandb.define_metric("val/AP", summary="max")
-
print("\nInitializing model...")
trainer = Trainer(cfg)
early_stopping = EarlyStopping(patience=cfg.earlystop_epoch, delta=-0.001, verbose=True)
@@ -148,7 +143,6 @@
best_acc = val_results['ACC']
# Explicitly update summary for the table with ALL metrics from this best epoch
- wandb.run.summary["best_acc_score"] = best_acc
wandb.run.summary["best_epoch"] = epoch
wandb.run.summary["best_AUC"] = val_results["AUC"]
wandb.run.summary["best_AP"] = val_results["AP"]
From 2e595391e1f714b86e8c3ae1b23ee4deff453e81 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Tue, 25 Nov 2025 09:45:07 +0800
Subject: [PATCH 34/55] fix: Update import statement for Trainer class to
clarify usage context
---
core/utils1/trainer_optical.py | 169 +++++++++++++++++++++++++++++++++
train.py | 2 +-
2 files changed, 170 insertions(+), 1 deletion(-)
create mode 100644 core/utils1/trainer_optical.py
diff --git a/core/utils1/trainer_optical.py b/core/utils1/trainer_optical.py
new file mode 100644
index 0000000..7a37fb3
--- /dev/null
+++ b/core/utils1/trainer_optical.py
@@ -0,0 +1,169 @@
+import os
+
+import torch
+import torch.nn as nn
+from torch.nn import init
+
+from core.utils1.config import CONFIGCLASS
+from core.utils1.utils import get_network
+from core.utils1.warmup import GradualWarmupScheduler
+
+
+class BaseModel(nn.Module):
+ def __init__(self, cfg: CONFIGCLASS):
+ super().__init__()
+ self.cfg = cfg
+ self.total_steps = 0
+ self.isTrain = cfg.isTrain
+ self.save_dir = cfg.ckpt_dir
+ self.device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
+ self.model:nn.Module
+ # self.model.to(self.device)
+ #self.model.load_state_dict(torch.load('./checkpoints/optical.pth'))
+ self.optimizer: torch.optim.Optimizer
+
+ def save_networks(self, epoch: int):
+ save_filename = f"model_epoch_{epoch}.pth"
+ save_path = os.path.join(self.save_dir, save_filename)
+
+ # serialize model and optimizer to dict
+ state_dict = {
+ "model": self.model.state_dict(),
+ "optimizer": self.optimizer.state_dict(),
+ "total_steps": self.total_steps,
+ }
+
+ torch.save(state_dict, save_path)
+
+ # load models from the disk
+ def load_networks(self, epoch: int):
+ load_filename = f"model_epoch_{epoch}.pth"
+ load_path = os.path.join(self.save_dir, load_filename)
+
+ if epoch==0:
+ # load_filename = f"lsun_adm.pth"
+ load_path="checkpoints/optical.pth"
+ print("loading optical path")
+ else :
+ print(f"loading the model from {load_path}")
+
+ # print(f"loading the model from {load_path}")
+
+ # if you are using PyTorch newer than 0.4 (e.g., built from
+ # GitHub source), you can remove str() on self.device
+ state_dict = torch.load(load_path, map_location=self.device)
+ if hasattr(state_dict, "_metadata"):
+ del state_dict._metadata
+
+ self.model.load_state_dict(state_dict["model"])
+ self.total_steps = state_dict["total_steps"]
+
+ if self.isTrain and not self.cfg.new_optim:
+ self.optimizer.load_state_dict(state_dict["optimizer"])
+ # move optimizer state to GPU
+ for state in self.optimizer.state.values():
+ for k, v in state.items():
+ if torch.is_tensor(v):
+ state[k] = v.to(self.device)
+
+ for g in self.optimizer.param_groups:
+ g["lr"] = self.cfg.lr
+
+ def eval(self):
+ self.model.eval()
+
+ def test(self):
+ with torch.no_grad():
+ self.forward()
+
+
+def init_weights(net: nn.Module, init_type="normal", gain=0.02):
+ def init_func(m: nn.Module):
+ classname = m.__class__.__name__
+ if hasattr(m, "weight") and (classname.find("Conv") != -1 or classname.find("Linear") != -1):
+ if init_type == "normal":
+ init.normal_(m.weight.data, 0.0, gain)
+ elif init_type == "xavier":
+ init.xavier_normal_(m.weight.data, gain=gain)
+ elif init_type == "kaiming":
+ init.kaiming_normal_(m.weight.data, a=0, mode="fan_in")
+ elif init_type == "orthogonal":
+ init.orthogonal_(m.weight.data, gain=gain)
+ else:
+ raise NotImplementedError(f"initialization method [{init_type}] is not implemented")
+ if hasattr(m, "bias") and m.bias is not None:
+ init.constant_(m.bias.data, 0.0)
+ elif classname.find("BatchNorm2d") != -1:
+ init.normal_(m.weight.data, 1.0, gain)
+ init.constant_(m.bias.data, 0.0)
+
+ print(f"initialize network with {init_type}")
+ net.apply(init_func)
+
+
+class Trainer(BaseModel):
+ def name(self):
+ return "Trainer"
+
+ def __init__(self, cfg: CONFIGCLASS):
+ super().__init__(cfg)
+ self.arch = cfg.arch
+ self.model = get_network(self.arch, cfg.isTrain, cfg.continue_train, cfg.init_gain, cfg.pretrained)
+
+ self.loss_fn = nn.BCEWithLogitsLoss()
+ # initialize optimizers
+ if cfg.optim == "adam":
+ self.optimizer = torch.optim.Adam(self.model.parameters(), lr=cfg.lr, betas=(cfg.beta1, 0.999))
+ elif cfg.optim == "sgd":
+ self.optimizer = torch.optim.SGD(self.model.parameters(), lr=cfg.lr, momentum=0.9, weight_decay=5e-4)
+ else:
+ raise ValueError("optim should be [adam, sgd]")
+ if cfg.warmup:
+ scheduler_cosine = torch.optim.lr_scheduler.CosineAnnealingLR(
+ self.optimizer, cfg.nepoch - cfg.warmup_epoch, eta_min=1e-6
+ )
+ self.scheduler = GradualWarmupScheduler(
+ self.optimizer, multiplier=1, total_epoch=cfg.warmup_epoch, after_scheduler=scheduler_cosine
+ )
+ self.scheduler.step()
+ if cfg.continue_train:
+ self.load_networks(cfg.epoch)
+ self.model.to(self.device)
+
+ # OPTICAL FLOW: Force load pretrained optical.pth checkpoint
+ print("Loading pretrained optical flow checkpoint...")
+ load_path = 'checkpoints/optical.pth'
+ state_dict = torch.load(load_path, map_location=self.device)
+ self.model.load_state_dict(state_dict["model"])
+ print(f"✓ Loaded optical flow checkpoint from {load_path}")
+
+
+
+ def adjust_learning_rate(self, min_lr=1e-6):
+ for param_group in self.optimizer.param_groups:
+ param_group["lr"] /= 10.0
+ if param_group["lr"] < min_lr:
+ return False
+ return True
+
+ def set_input(self, input):
+ img, label, meta = input if len(input) == 3 else (input[0], input[1], {})
+ self.input = img.to(self.device)
+ self.label = label.to(self.device).float()
+ for k in meta.keys():
+ if isinstance(meta[k], torch.Tensor):
+ meta[k] = meta[k].to(self.device)
+ self.meta = meta
+
+ def forward(self):
+ self.output = self.model(self.input, self.meta)
+
+ def get_loss(self):
+ return self.loss_fn(self.output.squeeze(1), self.label)
+
+ def optimize_parameters(self):
+ self.forward()
+ self.loss = self.loss_fn(self.output.squeeze(1), self.label)
+ self.optimizer.zero_grad()
+ self.loss.backward()
+ self.optimizer.step()
diff --git a/train.py b/train.py
index 03c7839..055d5f3 100644
--- a/train.py
+++ b/train.py
@@ -11,7 +11,7 @@
from core.utils1.datasets import create_dataloader
from core.utils1.earlystop import EarlyStopping
from core.utils1.eval import get_val_cfg, validate
-from core.utils1.trainer import Trainer
+from core.utils1.trainer import Trainer # change this if optical or rgb
from core.utils1.utils import Logger
import ssl
From 77a465e94352d3e744f7042d45e9aeef316945c2 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Tue, 25 Nov 2025 12:09:34 +0800
Subject: [PATCH 35/55] fix: Update Dockerfile and add download_data.py script
for improved data management
---
DOWNLOAD_GUIDE.md | 162 +++++++++++++++++++++++++
Dockerfile.cpu | 5 +-
download_data.py | 303 ++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 467 insertions(+), 3 deletions(-)
create mode 100644 DOWNLOAD_GUIDE.md
create mode 100644 download_data.py
diff --git a/DOWNLOAD_GUIDE.md b/DOWNLOAD_GUIDE.md
new file mode 100644
index 0000000..9d07aa2
--- /dev/null
+++ b/DOWNLOAD_GUIDE.md
@@ -0,0 +1,162 @@
+# Download Data Script - Usage Guide
+
+This Python script downloads all necessary files for AIGVDet:
+- **Dataset** (training/validation data)
+- **Checkpoints** (original.pth and optical.pth)
+- **RAFT Model** (for optical flow computation)
+
+## Quick Start
+
+### Download Everything
+```bash
+cd AIGVDet
+python download_data.py
+```
+
+This will download:
+- Dataset → `data/` directory
+- Checkpoints → `checkpoints/` directory (contains original.pth, optical.pth)
+- RAFT model → `raft_model/` directory
+
+## Advanced Usage
+
+### Download Specific Components
+
+**Only dataset:**
+```bash
+python download_data.py --skip-checkpoints --skip-raft
+```
+
+**Only checkpoints:**
+```bash
+python download_data.py --skip-data --skip-raft
+```
+
+**Only RAFT model:**
+```bash
+python download_data.py --skip-data --skip-checkpoints
+```
+
+### Custom Directories
+
+```bash
+python download_data.py \
+ --data-dir /path/to/data \
+ --checkpoint-dir /path/to/checkpoints \
+ --raft-dir /path/to/raft_model
+```
+
+### Custom Google Drive IDs
+
+```bash
+python download_data.py \
+ --data-id YOUR_FILE_ID \
+ --checkpoint-folder https://drive.google.com/drive/folders/YOUR_FOLDER_ID \
+ --raft-file https://drive.google.com/file/d/YOUR_FILE_ID/view
+```
+
+## What Gets Downloaded
+
+### 1. Dataset (data/)
+- Training and validation data
+- Extracted from a zip file
+- Default ID: `1YO3qRKbWxOYEm86Vy9QlGMjyi5Q_A6m0`
+
+### 2. Checkpoints (checkpoints/)
+- `original.pth` - Pre-trained RGB model
+- `optical.pth` - Pre-trained optical flow model
+- Folder URL: `https://drive.google.com/drive/folders/18JO_YxOEqwJYfbVvy308XjoV-N6fE4yP`
+
+### 3. RAFT Model (raft_model/)
+- `raft_things.pth` - Pre-trained RAFT model for optical flow computation
+- File URL: `https://drive.google.com/file/d/1MqDajR89k-xLV0HIrmJ0k-n8ZpG6_suM/view`
+
+## Dependencies
+
+The script will automatically install `gdown` if not present:
+```bash
+pip install gdown
+```
+
+Or install manually:
+```bash
+pip install gdown
+```
+
+## Features
+
+✅ **Auto-install dependencies** - Installs gdown if missing
+✅ **Check existing files** - Asks before re-downloading
+✅ **Progress indicators** - Shows download progress
+✅ **Auto-extract** - Extracts zip files automatically
+✅ **Cleanup** - Removes zip files after extraction
+✅ **Detailed output** - Shows file sizes and directory contents
+✅ **Error handling** - Gracefully handles download failures
+
+## Troubleshooting
+
+### gdown installation fails
+```bash
+python -m pip install --upgrade pip
+pip install gdown
+```
+
+### Authentication errors
+Some Google Drive files may require authentication. If you get quota errors:
+1. Wait a few hours and try again
+2. Download manually from the links above
+3. Place files in the correct directories
+
+### Download fails mid-way
+Re-run the script - it will ask if you want to re-download existing files.
+
+## File Structure After Download
+
+```
+AIGVDet/
+├── data/
+│ ├── train/
+│ └── val/
+├── checkpoints/
+│ ├── original.pth
+│ └── optical.pth
+└── raft_model/
+ └── raft_things.pth
+```
+
+## Command Line Options
+
+```
+usage: download_data.py [-h] [--data-id DATA_ID] [--data-dir DATA_DIR]
+ [--checkpoint-dir CHECKPOINT_DIR]
+ [--checkpoint-folder CHECKPOINT_FOLDER]
+ [--raft-dir RAFT_DIR] [--raft-file RAFT_FILE]
+ [--skip-data] [--skip-checkpoints] [--skip-raft]
+
+optional arguments:
+ -h, --help show this help message and exit
+ --data-id DATA_ID Google Drive file ID for dataset
+ --data-dir DATA_DIR Directory to save dataset (default: data)
+ --checkpoint-dir CHECKPOINT_DIR
+ Directory to save checkpoints (default: checkpoints)
+ --checkpoint-folder CHECKPOINT_FOLDER
+ Google Drive folder URL for checkpoints
+ --raft-dir RAFT_DIR Directory to save RAFT model (default: raft_model)
+ --raft-file RAFT_FILE
+ Google Drive file URL for RAFT model
+ --skip-data Skip dataset download
+ --skip-checkpoints Skip checkpoint download
+ --skip-raft Skip RAFT model download
+```
+
+## Original Shell Script
+
+The original `download_data.sh` is still available if you prefer to use bash:
+```bash
+bash download_data.sh [FILE_ID]
+```
+
+However, the Python version is recommended as it:
+- Works on Windows, Linux, and macOS
+- Downloads checkpoints and RAFT model automatically
+- Has better error handling and progress indicators
diff --git a/Dockerfile.cpu b/Dockerfile.cpu
index 5c4afe4..090da6a 100644
--- a/Dockerfile.cpu
+++ b/Dockerfile.cpu
@@ -31,11 +31,10 @@ COPY pyproject.toml ./
COPY README.md ./
COPY core/ ./core/
COPY networks/ ./networks/
-COPY train.py test.py demo.py ./
-COPY train.sh test.sh demo.sh download_data.sh ./
+COPY train.py test.py demo.py download_data.py ./
# Create directories for data and checkpoints
-RUN mkdir -p /app/data /app/checkpoints /app/raft_model
+RUN mkdir -p /app/data /app/checkpoints /app/raft_model
# Install other dependencies first
RUN uv pip install --system \
diff --git a/download_data.py b/download_data.py
new file mode 100644
index 0000000..f3369c9
--- /dev/null
+++ b/download_data.py
@@ -0,0 +1,303 @@
+#!/usr/bin/env python3
+"""
+Download script for AIGVDet data, checkpoints, and RAFT model.
+Converts the original download_data.sh to Python with additional downloads.
+
+Usage:
+ python download_data.py [--data-id FILE_ID] [--skip-data] [--skip-checkpoints] [--skip-raft]
+"""
+
+import os
+import sys
+import argparse
+import subprocess
+import zipfile
+from pathlib import Path
+
+
+def check_and_install_gdown():
+ """Check if gdown is installed, install if missing."""
+ try:
+ import gdown
+ print("✓ gdown is already installed")
+ return True
+ except ImportError:
+ print("Installing gdown...")
+ try:
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "gdown"])
+ print("✓ gdown installed successfully")
+ return True
+ except subprocess.CalledProcessError as e:
+ print(f"✗ Failed to install gdown: {e}")
+ return False
+
+
+def ask_redownload(path_name):
+ """Ask user if they want to re-download existing data."""
+ response = input(f"Do you want to re-download? (y/N): ").strip().lower()
+ return response in ['y', 'yes']
+
+
+def download_from_drive(url_or_id, output_path, is_folder=False):
+ """
+ Download file or folder from Google Drive using gdown.
+
+ Args:
+ url_or_id: Google Drive URL or file ID
+ output_path: Path where to save the downloaded file/folder
+ is_folder: True if downloading a folder, False for single file
+ """
+ import gdown
+
+ try:
+ if is_folder:
+ # Extract folder ID from URL if needed
+ if 'folders/' in url_or_id:
+ folder_id = url_or_id.split('folders/')[-1].split('?')[0]
+ else:
+ folder_id = url_or_id
+
+ print(f"Downloading folder (ID: {folder_id})...")
+ gdown.download_folder(id=folder_id, output=str(output_path), quiet=False)
+ else:
+ # Extract file ID from URL if needed
+ if 'drive.google.com' in url_or_id:
+ if '/file/d/' in url_or_id:
+ file_id = url_or_id.split('/file/d/')[-1].split('/')[0]
+ elif 'id=' in url_or_id:
+ file_id = url_or_id.split('id=')[-1].split('&')[0]
+ else:
+ file_id = url_or_id
+ else:
+ file_id = url_or_id
+
+ print(f"Downloading file (ID: {file_id})...")
+ gdown.download(id=file_id, output=str(output_path), quiet=False)
+
+ return True
+ except Exception as e:
+ print(f"✗ Download failed: {e}")
+ return False
+
+
+def extract_zip(zip_path, extract_to):
+ """Extract a zip file."""
+ print(f"Extracting {zip_path.name}...")
+ try:
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
+ zip_ref.extractall(extract_to)
+ print(f"✓ Extracted to {extract_to}")
+ return True
+ except Exception as e:
+ print(f"✗ Extraction failed: {e}")
+ return False
+
+
+def download_dataset(data_dir, file_id):
+ """Download and extract the main dataset."""
+ print("\n" + "="*60)
+ print("DOWNLOADING DATASET")
+ print("="*60)
+
+ data_dir = Path(data_dir)
+ zip_file = data_dir / "data.zip"
+
+ # Check if data already exists
+ train_dir = data_dir / "train"
+ if train_dir.exists():
+ print(f"Data directory {train_dir} already exists.")
+ if not ask_redownload("dataset"):
+ print("Skipping dataset download.")
+ return True
+
+ # Create data directory
+ data_dir.mkdir(parents=True, exist_ok=True)
+
+ # Download
+ if not download_from_drive(file_id, zip_file, is_folder=False):
+ return False
+
+ # Extract
+ if not extract_zip(zip_file, data_dir):
+ return False
+
+ # Cleanup
+ print("Cleaning up zip file...")
+ zip_file.unlink()
+
+ print(f"✓ Dataset setup complete!")
+ print(f"Contents of {data_dir}:")
+ for item in sorted(data_dir.iterdir()):
+ print(f" {'📁' if item.is_dir() else '📄'} {item.name}")
+
+ return True
+
+
+def download_checkpoints(checkpoint_dir, folder_url):
+ """Download checkpoint files (original.pth and optical.pth)."""
+ print("\n" + "="*60)
+ print("DOWNLOADING CHECKPOINTS")
+ print("="*60)
+
+ checkpoint_dir = Path(checkpoint_dir)
+
+ # Check if checkpoints already exist
+ if checkpoint_dir.exists() and any(checkpoint_dir.glob('*.pth')):
+ print(f"Checkpoint directory {checkpoint_dir} already has .pth files:")
+ for pth in checkpoint_dir.glob('*.pth'):
+ print(f" 📄 {pth.name}")
+ if not ask_redownload("checkpoints"):
+ print("Skipping checkpoint download.")
+ return True
+
+ # Create checkpoint directory
+ checkpoint_dir.mkdir(parents=True, exist_ok=True)
+
+ # Download folder from Google Drive
+ # gdown will create files directly in the checkpoint_dir
+ if not download_from_drive(folder_url, checkpoint_dir, is_folder=True):
+ return False
+
+ print(f"✓ Checkpoints download complete!")
+ print(f"Contents of {checkpoint_dir}:")
+ for item in sorted(checkpoint_dir.iterdir()):
+ size_mb = item.stat().st_size / (1024 * 1024) if item.is_file() else 0
+ size_str = f"({size_mb:.1f} MB)" if item.is_file() else ""
+ print(f" {'📁' if item.is_dir() else '📄'} {item.name} {size_str}")
+
+ return True
+
+
+def download_raft_model(raft_dir, file_url):
+ """Download RAFT model file."""
+ print("\n" + "="*60)
+ print("DOWNLOADING RAFT MODEL")
+ print("="*60)
+
+ raft_dir = Path(raft_dir)
+ model_file = raft_dir / "raft_things.pth"
+
+ # Check if RAFT model already exists
+ if model_file.exists():
+ size_mb = model_file.stat().st_size / (1024 * 1024)
+ print(f"RAFT model already exists: {model_file.name} ({size_mb:.1f} MB)")
+ if not ask_redownload("RAFT model"):
+ print("Skipping RAFT model download.")
+ return True
+
+ # Create RAFT directory
+ raft_dir.mkdir(parents=True, exist_ok=True)
+
+ # Download
+ if not download_from_drive(file_url, model_file, is_folder=False):
+ return False
+
+ print(f"✓ RAFT model download complete!")
+ if model_file.exists():
+ size_mb = model_file.stat().st_size / (1024 * 1024)
+ print(f" 📄 {model_file.name} ({size_mb:.1f} MB)")
+
+ return True
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description='Download AIGVDet data, checkpoints, and RAFT model'
+ )
+ parser.add_argument(
+ '--data-id',
+ default='1YO3qRKbWxOYEm86Vy9QlGMjyi5Q_A6m0',
+ help='Google Drive file ID for dataset (default: from original script)'
+ )
+ parser.add_argument(
+ '--data-dir',
+ default='data',
+ help='Directory to save dataset (default: data)'
+ )
+ parser.add_argument(
+ '--checkpoint-dir',
+ default='checkpoints',
+ help='Directory to save checkpoints (default: checkpoints)'
+ )
+ parser.add_argument(
+ '--checkpoint-folder',
+ default='https://drive.google.com/drive/folders/18JO_YxOEqwJYfbVvy308XjoV-N6fE4yP',
+ help='Google Drive folder URL for checkpoints'
+ )
+ parser.add_argument(
+ '--raft-dir',
+ default='raft_model',
+ help='Directory to save RAFT model (default: raft_model)'
+ )
+ parser.add_argument(
+ '--raft-file',
+ default='https://drive.google.com/file/d/1MqDajR89k-xLV0HIrmJ0k-n8ZpG6_suM/view',
+ help='Google Drive file URL for RAFT model'
+ )
+ parser.add_argument(
+ '--skip-data',
+ action='store_true',
+ help='Skip dataset download'
+ )
+ parser.add_argument(
+ '--skip-checkpoints',
+ action='store_true',
+ help='Skip checkpoint download'
+ )
+ parser.add_argument(
+ '--skip-raft',
+ action='store_true',
+ help='Skip RAFT model download'
+ )
+
+ args = parser.parse_args()
+
+ print("="*60)
+ print("AIGVDet Download Script")
+ print("="*60)
+
+ # Check and install gdown
+ if not check_and_install_gdown():
+ print("\n✗ Cannot proceed without gdown. Please install it manually:")
+ print(" pip install gdown")
+ return 1
+
+ success = True
+
+ # Download dataset
+ if not args.skip_data:
+ if not download_dataset(args.data_dir, args.data_id):
+ print("\n✗ Dataset download failed!")
+ success = False
+ else:
+ print("\nSkipping dataset download (--skip-data)")
+
+ # Download checkpoints
+ if not args.skip_checkpoints:
+ if not download_checkpoints(args.checkpoint_dir, args.checkpoint_folder):
+ print("\n✗ Checkpoint download failed!")
+ success = False
+ else:
+ print("\nSkipping checkpoint download (--skip-checkpoints)")
+
+ # Download RAFT model
+ if not args.skip_raft:
+ if not download_raft_model(args.raft_dir, args.raft_file):
+ print("\n✗ RAFT model download failed!")
+ success = False
+ else:
+ print("\nSkipping RAFT model download (--skip-raft)")
+
+ # Summary
+ print("\n" + "="*60)
+ if success:
+ print("✓ ALL DOWNLOADS COMPLETE!")
+ else:
+ print("⚠ SOME DOWNLOADS FAILED - Check errors above")
+ print("="*60)
+
+ return 0 if success else 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())
From 6387e820223db9fa175fa97887ca9c0e458d778c Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Tue, 25 Nov 2025 16:09:47 +0800
Subject: [PATCH 36/55] fix: Clean up Dockerfile by removing unnecessary script
copies
---
Dockerfile.gpu-alt | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/Dockerfile.gpu-alt b/Dockerfile.gpu-alt
index 43e5b29..2b23046 100644
--- a/Dockerfile.gpu-alt
+++ b/Dockerfile.gpu-alt
@@ -32,12 +32,10 @@ COPY pyproject.toml ./
COPY README.md ./
COPY core/ ./core/
COPY networks/ ./networks/
-COPY train.py test.py demo.py ./
-COPY train.sh test.sh demo.sh download_data.sh ./
-
+COPY train.py test.py demo.py download_data.py ./
# Create directories for data and checkpoints
-RUN mkdir -p /app/data /app/checkpoints /app/raft_model
+RUN mkdir -p /app/data /app/checkpoints /app/raft_model
# Install dependencies (PyTorch already included in base image)
RUN uv pip install --system \
From ead7f974710510963f0e9f074f0634312e3e9421 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Tue, 25 Nov 2025 17:13:34 +0800
Subject: [PATCH 37/55] fix: Update configuration and dataset handling for
optical flow training
---
DOWNLOAD_GUIDE.md | 162 ---------------------------------
core/utils1/config.py | 11 ++-
core/utils1/datasets.py | 28 +++---
core/utils1/trainer_optical.py | 2 +-
train.py | 2 +-
5 files changed, 22 insertions(+), 183 deletions(-)
delete mode 100644 DOWNLOAD_GUIDE.md
diff --git a/DOWNLOAD_GUIDE.md b/DOWNLOAD_GUIDE.md
deleted file mode 100644
index 9d07aa2..0000000
--- a/DOWNLOAD_GUIDE.md
+++ /dev/null
@@ -1,162 +0,0 @@
-# Download Data Script - Usage Guide
-
-This Python script downloads all necessary files for AIGVDet:
-- **Dataset** (training/validation data)
-- **Checkpoints** (original.pth and optical.pth)
-- **RAFT Model** (for optical flow computation)
-
-## Quick Start
-
-### Download Everything
-```bash
-cd AIGVDet
-python download_data.py
-```
-
-This will download:
-- Dataset → `data/` directory
-- Checkpoints → `checkpoints/` directory (contains original.pth, optical.pth)
-- RAFT model → `raft_model/` directory
-
-## Advanced Usage
-
-### Download Specific Components
-
-**Only dataset:**
-```bash
-python download_data.py --skip-checkpoints --skip-raft
-```
-
-**Only checkpoints:**
-```bash
-python download_data.py --skip-data --skip-raft
-```
-
-**Only RAFT model:**
-```bash
-python download_data.py --skip-data --skip-checkpoints
-```
-
-### Custom Directories
-
-```bash
-python download_data.py \
- --data-dir /path/to/data \
- --checkpoint-dir /path/to/checkpoints \
- --raft-dir /path/to/raft_model
-```
-
-### Custom Google Drive IDs
-
-```bash
-python download_data.py \
- --data-id YOUR_FILE_ID \
- --checkpoint-folder https://drive.google.com/drive/folders/YOUR_FOLDER_ID \
- --raft-file https://drive.google.com/file/d/YOUR_FILE_ID/view
-```
-
-## What Gets Downloaded
-
-### 1. Dataset (data/)
-- Training and validation data
-- Extracted from a zip file
-- Default ID: `1YO3qRKbWxOYEm86Vy9QlGMjyi5Q_A6m0`
-
-### 2. Checkpoints (checkpoints/)
-- `original.pth` - Pre-trained RGB model
-- `optical.pth` - Pre-trained optical flow model
-- Folder URL: `https://drive.google.com/drive/folders/18JO_YxOEqwJYfbVvy308XjoV-N6fE4yP`
-
-### 3. RAFT Model (raft_model/)
-- `raft_things.pth` - Pre-trained RAFT model for optical flow computation
-- File URL: `https://drive.google.com/file/d/1MqDajR89k-xLV0HIrmJ0k-n8ZpG6_suM/view`
-
-## Dependencies
-
-The script will automatically install `gdown` if not present:
-```bash
-pip install gdown
-```
-
-Or install manually:
-```bash
-pip install gdown
-```
-
-## Features
-
-✅ **Auto-install dependencies** - Installs gdown if missing
-✅ **Check existing files** - Asks before re-downloading
-✅ **Progress indicators** - Shows download progress
-✅ **Auto-extract** - Extracts zip files automatically
-✅ **Cleanup** - Removes zip files after extraction
-✅ **Detailed output** - Shows file sizes and directory contents
-✅ **Error handling** - Gracefully handles download failures
-
-## Troubleshooting
-
-### gdown installation fails
-```bash
-python -m pip install --upgrade pip
-pip install gdown
-```
-
-### Authentication errors
-Some Google Drive files may require authentication. If you get quota errors:
-1. Wait a few hours and try again
-2. Download manually from the links above
-3. Place files in the correct directories
-
-### Download fails mid-way
-Re-run the script - it will ask if you want to re-download existing files.
-
-## File Structure After Download
-
-```
-AIGVDet/
-├── data/
-│ ├── train/
-│ └── val/
-├── checkpoints/
-│ ├── original.pth
-│ └── optical.pth
-└── raft_model/
- └── raft_things.pth
-```
-
-## Command Line Options
-
-```
-usage: download_data.py [-h] [--data-id DATA_ID] [--data-dir DATA_DIR]
- [--checkpoint-dir CHECKPOINT_DIR]
- [--checkpoint-folder CHECKPOINT_FOLDER]
- [--raft-dir RAFT_DIR] [--raft-file RAFT_FILE]
- [--skip-data] [--skip-checkpoints] [--skip-raft]
-
-optional arguments:
- -h, --help show this help message and exit
- --data-id DATA_ID Google Drive file ID for dataset
- --data-dir DATA_DIR Directory to save dataset (default: data)
- --checkpoint-dir CHECKPOINT_DIR
- Directory to save checkpoints (default: checkpoints)
- --checkpoint-folder CHECKPOINT_FOLDER
- Google Drive folder URL for checkpoints
- --raft-dir RAFT_DIR Directory to save RAFT model (default: raft_model)
- --raft-file RAFT_FILE
- Google Drive file URL for RAFT model
- --skip-data Skip dataset download
- --skip-checkpoints Skip checkpoint download
- --skip-raft Skip RAFT model download
-```
-
-## Original Shell Script
-
-The original `download_data.sh` is still available if you prefer to use bash:
-```bash
-bash download_data.sh [FILE_ID]
-```
-
-However, the Python version is recommended as it:
-- Works on Windows, Linux, and macOS
-- Downloads checkpoints and RAFT model automatically
-- Has better error handling and progress indicators
diff --git a/core/utils1/config.py b/core/utils1/config.py
index a66d379..f0c6702 100644
--- a/core/utils1/config.py
+++ b/core/utils1/config.py
@@ -25,18 +25,19 @@ class DefaultConfigs(ABC):
# data augmentation - to match the paper's augmentation rate (10%), set blur and jpg to 0.5
rz_interp = ["bilinear"]
# blur_prob = 0.1 - resnet50 template
- blur_prob = 0.05
+ blur_prob = 0 # optical flow
blur_sig = [0.5]
# jpg_prob = 0.1 - resnet50 template
- jpg_prob = 0.05
+ jpg_prob = 0 # optical flow
# P(augmented) = 1-(1-0.5)(1-0.05) = 0.9
jpg_method = ["cv2"]
jpg_qual = list(range(70, 91))
gray_prob = 0.0
aug_resize = True
- aug_crop = True
- aug_flip = True
- aug_norm = True
+ optical_crop = True # comment out to disable cropping
+ aug_crop = True
+ aug_flip = False # optical flow
+ aug_norm = False # optical flow
####### train setting ######
warmup = False
diff --git a/core/utils1/datasets.py b/core/utils1/datasets.py
index ec6bd4e..4e4f589 100644
--- a/core/utils1/datasets.py
+++ b/core/utils1/datasets.py
@@ -40,34 +40,34 @@ def binary_dataset(root: str, cfg: CONFIGCLASS):
rz_func = identity_transform
# Crop to cfg.cropSize (paper uses 448)
- if cfg.isTrain:
+ if cfg.isTrain and not cfg.optical_crop: # standard RGB training
crop_func = transforms.RandomCrop((cfg.cropSize, cfg.cropSize))
else:
crop_func = transforms.CenterCrop((cfg.cropSize, cfg.cropSize)) if cfg.aug_crop else identity_transform
- # Flip only in training if enabled
+ # Flip only in training if enabled (disabled for optical flow)
if cfg.isTrain and cfg.aug_flip:
flip_func = transforms.RandomHorizontalFlip()
else:
- flip_func = identity_transform # fallback
+ flip_func = identity_transform # fallback
return datasets.ImageFolder(
root,
transforms.Compose(
[
- rz_func,
- #change
- crop_func,
- transforms.Lambda(lambda img: blur_jpg_augment(img, cfg)),
- flip_func,
- transforms.ToTensor(),
- transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
- if cfg.aug_norm
- else identity_transform,
- ]
+ rz_func,
+ #change
+ crop_func,
+ transforms.Lambda(lambda img: blur_jpg_augment(img, cfg)),
+ flip_func,
+ transforms.ToTensor(),
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ if cfg.aug_norm
+ else identity_transform,
+ ]
+ )
)
- )
class FileNameDataset(datasets.ImageFolder):
diff --git a/core/utils1/trainer_optical.py b/core/utils1/trainer_optical.py
index 7a37fb3..5f500ff 100644
--- a/core/utils1/trainer_optical.py
+++ b/core/utils1/trainer_optical.py
@@ -19,7 +19,7 @@ def __init__(self, cfg: CONFIGCLASS):
self.device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
self.model:nn.Module
# self.model.to(self.device)
- #self.model.load_state_dict(torch.load('./checkpoints/optical.pth'))
+ self.model.load_state_dict(torch.load('./checkpoints/optical.pth'))
self.optimizer: torch.optim.Optimizer
def save_networks(self, epoch: int):
diff --git a/train.py b/train.py
index 055d5f3..15ef4fe 100644
--- a/train.py
+++ b/train.py
@@ -11,7 +11,7 @@
from core.utils1.datasets import create_dataloader
from core.utils1.earlystop import EarlyStopping
from core.utils1.eval import get_val_cfg, validate
-from core.utils1.trainer import Trainer # change this if optical or rgb
+from core.utils1.trainer_optical import Trainer # change this if optical or rgb
from core.utils1.utils import Logger
import ssl
From 96399fdcb348fe217bb954ae23ff5a58d891df28 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Tue, 25 Nov 2025 19:12:23 +0800
Subject: [PATCH 38/55] fix: Update model initialization in BaseModel to
prevent premature loading
---
core/utils1/trainer_optical.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/core/utils1/trainer_optical.py b/core/utils1/trainer_optical.py
index 5f500ff..7a37fb3 100644
--- a/core/utils1/trainer_optical.py
+++ b/core/utils1/trainer_optical.py
@@ -19,7 +19,7 @@ def __init__(self, cfg: CONFIGCLASS):
self.device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
self.model:nn.Module
# self.model.to(self.device)
- self.model.load_state_dict(torch.load('./checkpoints/optical.pth'))
+ #self.model.load_state_dict(torch.load('./checkpoints/optical.pth'))
self.optimizer: torch.optim.Optimizer
def save_networks(self, epoch: int):
From 59796bb2fc23f5a78b09d03e090cce87dfd6b031 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Tue, 25 Nov 2025 22:15:35 +0800
Subject: [PATCH 39/55] fix: Update optical flow augmentation settings and
improve pretrained checkpoint loading logic
---
core/utils1/config.py | 5 ++---
download_data.py | 2 +-
2 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/core/utils1/config.py b/core/utils1/config.py
index f0c6702..94efb7b 100644
--- a/core/utils1/config.py
+++ b/core/utils1/config.py
@@ -34,10 +34,9 @@ class DefaultConfigs(ABC):
jpg_qual = list(range(70, 91))
gray_prob = 0.0
aug_resize = True
- optical_crop = True # comment out to disable cropping
aug_crop = True
- aug_flip = False # optical flow
- aug_norm = False # optical flow
+ aug_flip = True # optical flow
+ aug_norm = True # optical flow
####### train setting ######
warmup = False
diff --git a/download_data.py b/download_data.py
index f3369c9..c74e34b 100644
--- a/download_data.py
+++ b/download_data.py
@@ -206,7 +206,7 @@ def main():
)
parser.add_argument(
'--data-id',
- default='1YO3qRKbWxOYEm86Vy9QlGMjyi5Q_A6m0',
+ default='1BGsUw_A7YT3fCuaCMXcaSMQBBvDyE_59 ',
help='Google Drive file ID for dataset (default: from original script)'
)
parser.add_argument(
From 1d76a53a9bfdb8dae1890d6f84d4db920742f48c Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Tue, 25 Nov 2025 22:16:43 +0800
Subject: [PATCH 40/55] fix: Update trainer import in train.py for consistency
---
train.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/train.py b/train.py
index 15ef4fe..a87f639 100644
--- a/train.py
+++ b/train.py
@@ -11,7 +11,7 @@
from core.utils1.datasets import create_dataloader
from core.utils1.earlystop import EarlyStopping
from core.utils1.eval import get_val_cfg, validate
-from core.utils1.trainer_optical import Trainer # change this if optical or rgb
+from core.utils1.trainer import Trainer # change
from core.utils1.utils import Logger
import ssl
From 7e6a1241b4bd223dc53dcf0fca4e1fbb8da5c88e Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Tue, 25 Nov 2025 23:04:23 +0800
Subject: [PATCH 41/55] fix: Update data augmentation parameters for optical
flow training
---
core/utils1/config.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/core/utils1/config.py b/core/utils1/config.py
index 94efb7b..6231905 100644
--- a/core/utils1/config.py
+++ b/core/utils1/config.py
@@ -25,10 +25,10 @@ class DefaultConfigs(ABC):
# data augmentation - to match the paper's augmentation rate (10%), set blur and jpg to 0.5
rz_interp = ["bilinear"]
# blur_prob = 0.1 - resnet50 template
- blur_prob = 0 # optical flow
+ blur_prob = 0.1 # optical flow
blur_sig = [0.5]
# jpg_prob = 0.1 - resnet50 template
- jpg_prob = 0 # optical flow
+ jpg_prob = 0.1 # optical flow
# P(augmented) = 1-(1-0.5)(1-0.05) = 0.9
jpg_method = ["cv2"]
jpg_qual = list(range(70, 91))
From ee09cf989827e2860f833812320d0b65e64daed4 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Tue, 25 Nov 2025 23:06:00 +0800
Subject: [PATCH 42/55] fix: Remove trailing whitespace from default data ID in
download_data.py
---
download_data.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/download_data.py b/download_data.py
index c74e34b..2855fde 100644
--- a/download_data.py
+++ b/download_data.py
@@ -206,7 +206,7 @@ def main():
)
parser.add_argument(
'--data-id',
- default='1BGsUw_A7YT3fCuaCMXcaSMQBBvDyE_59 ',
+ default='1BGsUw_A7YT3fCuaCMXcaSMQBBvDyE_59',
help='Google Drive file ID for dataset (default: from original script)'
)
parser.add_argument(
From a988c7f911fb6c97332c745520b150deeeb7bbd9 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Tue, 25 Nov 2025 23:26:34 +0800
Subject: [PATCH 43/55] fix: Update default data ID in download_data.py and
enhance docstrings in prepare_data.py
---
Dockerfile.gpu-alt | 2 +-
download_data.py | 2 +-
prepare_data.py | 43 ++++++++++++++++++++++++++++++-------------
3 files changed, 32 insertions(+), 15 deletions(-)
diff --git a/Dockerfile.gpu-alt b/Dockerfile.gpu-alt
index 2b23046..d63a70d 100644
--- a/Dockerfile.gpu-alt
+++ b/Dockerfile.gpu-alt
@@ -32,7 +32,7 @@ COPY pyproject.toml ./
COPY README.md ./
COPY core/ ./core/
COPY networks/ ./networks/
-COPY train.py test.py demo.py download_data.py ./
+COPY train.py test.py demo.py download_data.py recreate_table_2.py prepare_data.py ./
# Create directories for data and checkpoints
RUN mkdir -p /app/data /app/checkpoints /app/raft_model
diff --git a/download_data.py b/download_data.py
index 2855fde..999405b 100644
--- a/download_data.py
+++ b/download_data.py
@@ -206,7 +206,7 @@ def main():
)
parser.add_argument(
'--data-id',
- default='1BGsUw_A7YT3fCuaCMXcaSMQBBvDyE_59',
+ default='1Bgv_TA18CIXwxQ5EQoSybsSC3EcivVv1',
help='Google Drive file ID for dataset (default: from original script)'
)
parser.add_argument(
diff --git a/prepare_data.py b/prepare_data.py
index d96f827..65791b8 100644
--- a/prepare_data.py
+++ b/prepare_data.py
@@ -18,19 +18,28 @@
DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
def load_image(imfile):
+ """
+ Load an image, convert to tensor, and move to the appropriate device.
+ """
img = np.array(Image.open(imfile)).astype(np.uint8)
- img = torch.from_numpy(img).permute(2, 0, 1).float()
+
return img[None].to(DEVICE)
def save_flow(img, flo, output_path):
- img = img[0].permute(1,2,0).cpu().numpy()
- flo = flo[0].permute(1,2,0).cpu().numpy()
-
- # map flow to rgb image
+ """
+ Save optical flow as a color image.
+ """
+ img = img[0].permute(1, 2, 0).cpu().numpy()
+ flo = flo[0].permute(1, 2, 0).cpu().numpy()
+
+ # Convert flow to RGB
flo = flow_viz.flow_to_image(flo)
cv2.imwrite(output_path, flo)
def video_to_frames(video_path, output_folder):
+ """
+ Extract frames from a video and save them as images in the output folder.
+ """
if not os.path.exists(output_folder):
os.makedirs(output_folder)
@@ -53,11 +62,15 @@ def video_to_frames(video_path, output_folder):
cap.release()
+ # Return sorted list of extracted frame files
images = glob.glob(os.path.join(output_folder, '*.png')) + \
glob.glob(os.path.join(output_folder, '*.jpg'))
return sorted(images)
def process_dataset(args):
+ """
+ Process each video in the dataset, extracting frames and computing optical flow.
+ """
# Load RAFT model once
print(f"Loading RAFT model from {args.model}...")
model = torch.nn.DataParallel(RAFT(args))
@@ -83,10 +96,8 @@ def process_dataset(args):
for video_path in tqdm(videos, desc=f"Processing {label}"):
video_name = os.path.splitext(os.path.basename(video_path))[0]
- # Define output paths
- # Original frames: args.output_rgb_dir / label / video_name / frames
+ # Define output paths for RGB frames and optical flow
rgb_out_dir = os.path.join(args.output_rgb_dir, label, video_name)
- # Optical flow: args.output_flow_dir / label / video_name / frames
flow_out_dir = os.path.join(args.output_flow_dir, label, video_name)
if not os.path.exists(flow_out_dir):
@@ -100,7 +111,7 @@ def process_dataset(args):
continue
# 2. Generate Optical Flow
- # Check if flow already exists
+ # Check if flow already exists for the extracted frames
existing_flow = glob.glob(os.path.join(flow_out_dir, "*.png"))
if len(existing_flow) >= len(images) - 1:
continue
@@ -114,28 +125,34 @@ def process_dataset(args):
if os.path.exists(flow_output_path):
continue
+ # Load the consecutive frames
image1 = load_image(imfile1)
image2 = load_image(imfile2)
+ # Pad images if necessary
padder = InputPadder(image1.shape)
image1, image2 = padder.pad(image1, image2)
+ # Compute flow with the RAFT model
flow_low, flow_up = model(image1, image2, iters=20, test_mode=True)
+ # Save the optical flow
save_flow(image1, flow_up, flow_output_path)
if __name__ == '__main__':
+ # Parse command-line arguments
parser = argparse.ArgumentParser()
parser.add_argument('--source_dir', required=True, help="Path to folder containing 0_real/1_fake video folders")
parser.add_argument('--output_rgb_dir', required=True, help="Output path for RGB frames")
parser.add_argument('--output_flow_dir', required=True, help="Output path for Optical Flow frames")
parser.add_argument('--model', default="raft_model/raft-things.pth", help="Path to RAFT model checkpoint")
- # RAFT args
- parser.add_argument('--small', action='store_true', help='use small model')
- parser.add_argument('--mixed_precision', action='store_true', help='use mixed precision')
- parser.add_argument('--alternate_corr', action='store_true', help='use efficent correlation implementation')
+ # RAFT arguments
+ parser.add_argument('--small', action='store_true', help='Use small model')
+ parser.add_argument('--mixed_precision', action='store_true', help='Use mixed precision')
+ parser.add_argument('--alternate_corr', action='store_true', help='Use efficient correlation implementation')
args = parser.parse_args()
+ # Process the dataset with the provided arguments
process_dataset(args)
From ca2f67fdc64f1a978f193a98c96b0966aad326b1 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Wed, 26 Nov 2025 02:10:36 +0800
Subject: [PATCH 44/55] feat: Add video downloading and processing script with
optical flow computation
---
download_t2v.py | 31 +++++++
process_custom_videos.py | 184 +++++++++++++++++++++++++++++++++++++++
2 files changed, 215 insertions(+)
create mode 100644 download_t2v.py
create mode 100644 process_custom_videos.py
diff --git a/download_t2v.py b/download_t2v.py
new file mode 100644
index 0000000..19a20f9
--- /dev/null
+++ b/download_t2v.py
@@ -0,0 +1,31 @@
+import gdown
+import zipfile
+import os
+
+# Google Drive file ID
+file_id = "1FT06IRiy1oB1jHWBEarUI99DFCk6VHxf"
+
+# Output path for the downloaded file
+output_path = "downloaded_file.zip"
+
+# Download the file from Google Drive
+gdown.download(f"https://drive.google.com/uc?id={file_id}", output_path, quiet=False)
+
+# Check if the file was downloaded
+if os.path.exists(output_path):
+ print(f"File downloaded successfully: {output_path}")
+
+ # Unzip the file
+ unzip_dir = "unzipped_files"
+ if not os.path.exists(unzip_dir):
+ os.makedirs(unzip_dir)
+
+ with zipfile.ZipFile(output_path, 'r') as zip_ref:
+ zip_ref.extractall(unzip_dir)
+ print(f"Files extracted to: {unzip_dir}")
+
+ # Optionally, you can delete the zip file after extraction
+ os.remove(output_path)
+ print("Zip file removed after extraction.")
+else:
+ print("Download failed.")
diff --git a/process_custom_videos.py b/process_custom_videos.py
new file mode 100644
index 0000000..c759f24
--- /dev/null
+++ b/process_custom_videos.py
@@ -0,0 +1,184 @@
+import sys
+import argparse
+import os
+import cv2
+import glob
+import numpy as np
+import torch
+from PIL import Image
+from tqdm import tqdm
+from natsort import natsorted
+import warnings
+warnings.filterwarnings("ignore", category=UserWarning, message="torch.meshgrid")
+
+# Add core to path for imports
+sys.path.append('core')
+from raft import RAFT
+from utils import flow_viz
+from utils.utils import InputPadder
+
+if not torch.cuda.is_available():
+ raise RuntimeError("CUDA is not available. This script requires a GPU.")
+DEVICE = 'cuda'
+
+def load_image(imfile):
+ img = np.array(Image.open(imfile)).astype(np.uint8)
+ img = torch.from_numpy(img).permute(2, 0, 1).float()
+ return img[None].to(DEVICE)
+
+def save_flow(img, flo, output_path):
+ img = img[0].permute(1, 2, 0).cpu().numpy()
+ flo = flo[0].permute(1, 2, 0).cpu().numpy()
+
+ # map flow to rgb image
+ flo = flow_viz.flow_to_image(flo)
+ # Save only the flow image as per typical requirements, or concatenated if requested.
+ # The user's code had: img_flo = np.concatenate([img, flo], axis=0)
+ # But usually for training we just want the flow.
+ # The user's demo code saved 'flo'.
+ cv2.imwrite(output_path, flo)
+
+def video_to_frames(video_path, output_folder, max_frames=95, resize=None):
+ if not os.path.exists(output_folder):
+ os.makedirs(output_folder)
+
+ cap = cv2.VideoCapture(video_path)
+ width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
+ height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
+
+ # Print resolution to help user understand speed
+ print(f" -> Video: {os.path.basename(video_path)} | Resolution: {width}x{height}")
+
+ frame_count = 0
+ saved_frames = []
+
+ while cap.isOpened() and frame_count < max_frames:
+ ret, frame = cap.read()
+ if not ret:
+ break
+
+ # Resize if requested
+ if resize is not None:
+ h, w = frame.shape[:2]
+ if min(h, w) > resize:
+ scale = resize / min(h, w)
+ new_h, new_w = int(h * scale), int(w * scale)
+ frame = cv2.resize(frame, (new_w, new_h))
+
+ video_name = os.path.splitext(os.path.basename(video_path))[0]
+ frame_filename = os.path.join(output_folder, f"{video_name}_{frame_count+1:05d}.jpg")
+ cv2.imwrite(frame_filename, frame)
+ saved_frames.append(frame_filename)
+ frame_count += 1
+
+ cap.release()
+ return sorted(saved_frames)
+
+def process_videos(args):
+ # Verify model path
+ if not os.path.exists(args.model):
+ # Try fallback for common issue (hyphen vs underscore)
+ if "raft-model" in args.model and os.path.exists(args.model.replace("raft-model", "raft_model")):
+ args.model = args.model.replace("raft-model", "raft_model")
+ print(f"Found model at alternate path: {args.model}")
+ elif "raft_model" in args.model and os.path.exists(args.model.replace("raft_model", "raft-model")):
+ args.model = args.model.replace("raft_model", "raft-model")
+ print(f"Found model at alternate path: {args.model}")
+ else:
+ print(f"Error: Model file not found at {args.model}")
+ print(f"Current working directory: {os.getcwd()}")
+ print(f"Available directories: {[d for d in os.listdir('.') if os.path.isdir(d)]}")
+ raise FileNotFoundError(f"Model file not found: {args.model}")
+
+ # Load model
+ model = torch.nn.DataParallel(RAFT(args))
+ model.load_state_dict(torch.load(args.model, map_location=torch.device(DEVICE)))
+ model = model.module
+ model.to(DEVICE)
+ model.eval()
+
+ print(f"Processing on device: {DEVICE}")
+ if args.resize:
+ print(f"Resizing frames to short edge: {args.resize}px")
+
+ # Get list of videos
+ videos = glob.glob(os.path.join(args.input_path, '*.mp4')) + \
+ glob.glob(os.path.join(args.input_path, '*.avi')) + \
+ glob.glob(os.path.join(args.input_path, '*.mov'))
+
+ print(f"Found {len(videos)} videos in {args.input_path}")
+
+ for video_path in tqdm(videos):
+ video_name = os.path.splitext(os.path.basename(video_path))[0]
+
+ # Use main output directories directly (flat structure)
+ video_rgb_dir = args.output_rgb
+ video_flow_dir = args.output_flow
+
+ if not os.path.exists(video_rgb_dir):
+ os.makedirs(video_rgb_dir)
+ if not os.path.exists(video_flow_dir):
+ os.makedirs(video_flow_dir)
+
+ # 1. Extract RGB frames (max 95)
+ # Pass resize argument here
+ images = video_to_frames(video_path, video_rgb_dir, max_frames=95, resize=args.resize)
+
+ # 2. Compute Optical Flow (max 94)
+ if len(images) < 2:
+ continue
+
+ with torch.no_grad():
+ images = natsorted(images)
+
+ # Load first image
+ image1 = load_image(images[0])
+
+ for i in range(len(images) - 1):
+ if i >= 94:
+ break
+
+ imfile1 = images[i]
+ imfile2 = images[i+1]
+
+ # Check if flow already exists
+ flow_filename = os.path.basename(imfile1)
+ flow_output_path = os.path.join(video_flow_dir, flow_filename)
+
+ if os.path.exists(flow_output_path):
+ # If skipping, we still need to update image1 for the next iteration if we weren't reloading
+ # But since we are skipping, we might not have loaded image2 yet.
+ # To be safe and simple with skipping logic:
+ image1 = load_image(imfile2) # Prepare for next iter
+ continue
+
+ image2 = load_image(imfile2)
+
+ # Debug: Print shape once to confirm resize
+ if i == 0 and video_path == videos[0]:
+ print(f" -> Model input shape: {image1.shape}")
+
+ padder = InputPadder(image1.shape)
+ image1_padded, image2_padded = padder.pad(image1, image2)
+
+ flow_low, flow_up = model(image1_padded, image2_padded, iters=20, test_mode=True)
+
+ save_flow(image1, flow_up, flow_output_path)
+
+ # Move image2 to image1 for next iteration
+ image1 = image2
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--model', help="restore checkpoint", default="raft-model/raft-things.pth")
+ parser.add_argument('--input_path', help="path to videos", default="data/T2V/moonvalley_mp4")
+ parser.add_argument('--output_rgb', help="output path for RGB frames", default="data/T2V/moonvalley_rgb")
+ parser.add_argument('--output_flow', help="output path for Optical Flow frames", default="data/T2V/moonvalley_flow")
+ parser.add_argument('--small', action='store_true', help='use small model')
+ parser.add_argument('--mixed_precision', action='store_true', help='use mixed precision')
+ parser.add_argument('--alternate_corr', action='store_true', help='use efficent correlation implementation')
+ parser.add_argument('--resize', type=int, default=None, help='Resize smaller edge of frames to this value (e.g. 512) for faster processing')
+
+ args = parser.parse_args()
+
+ process_videos(args)
From 39aa5d589f63d892cc6856f306198172a319800b Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Wed, 26 Nov 2025 02:27:31 +0800
Subject: [PATCH 45/55] fix: Update import path for CONFIGCLASS and adjust
dataset transformations for training
---
core/utils1/datasets.py | 57 +++++++++++++++++++----------------------
1 file changed, 26 insertions(+), 31 deletions(-)
diff --git a/core/utils1/datasets.py b/core/utils1/datasets.py
index 4e4f589..6236369 100644
--- a/core/utils1/datasets.py
+++ b/core/utils1/datasets.py
@@ -13,7 +13,7 @@
from scipy.ndimage import gaussian_filter
from torch.utils.data.sampler import WeightedRandomSampler
-from core.utils1.config import CONFIGCLASS
+from utils1.config import CONFIGCLASS
ImageFile.LOAD_TRUNCATED_IMAGES = True
@@ -27,47 +27,37 @@ def dataset_folder(root: str, cfg: CONFIGCLASS):
def binary_dataset(root: str, cfg: CONFIGCLASS):
- # identity_transform = transforms.Lambda(lambda img: img)
- # rz_func = identity_transform
- # issue here, destroys performance as no resizing happens at all that go straight to cropping even if they are different resolutions
-
identity_transform = transforms.Lambda(lambda img: img)
-
- # Enable resize (paper implies resize > crop for random cropping)
- if cfg.aug_resize:
- rz_func = transforms.Lambda(lambda img: custom_resize(img, cfg))
- else:
- rz_func = identity_transform
-
- # Crop to cfg.cropSize (paper uses 448)
- if cfg.isTrain and not cfg.optical_crop: # standard RGB training
- crop_func = transforms.RandomCrop((cfg.cropSize, cfg.cropSize))
+
+ rz_func = identity_transform
+
+ if cfg.isTrain:
+ crop_func = transforms.RandomCrop((448,448))
else:
- crop_func = transforms.CenterCrop((cfg.cropSize, cfg.cropSize)) if cfg.aug_crop else identity_transform
+ crop_func = transforms.CenterCrop((448,448)) if cfg.aug_crop else identity_transform
- # Flip only in training if enabled (disabled for optical flow)
- if cfg.isTrain and cfg.aug_flip:
+ if cfg.isTrain and cfg.aug_flip and not (hasattr(cfg, 'isOptical') and cfg.isOptical):
flip_func = transforms.RandomHorizontalFlip()
else:
- flip_func = identity_transform # fallback
-
+ flip_func = identity_transform
+
return datasets.ImageFolder(
root,
transforms.Compose(
[
- rz_func,
- #change
- crop_func,
- transforms.Lambda(lambda img: blur_jpg_augment(img, cfg)),
- flip_func,
- transforms.ToTensor(),
- transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
- if cfg.aug_norm
- else identity_transform,
- ]
- )
+ rz_func,
+ #change
+ transforms.Lambda(lambda img: blur_jpg_augment(img, cfg)),
+ crop_func,
+ flip_func,
+ transforms.ToTensor(),
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ if cfg.aug_norm
+ else identity_transform,
+ ]
)
+ )
class FileNameDataset(datasets.ImageFolder):
@@ -86,6 +76,11 @@ def __getitem__(self, index):
def blur_jpg_augment(img: Image.Image, cfg: CONFIGCLASS):
img: np.ndarray = np.array(img)
+
+ # Skip augmentations for optical flow to preserve motion vector integrity
+ if hasattr(cfg, 'isOptical') and cfg.isOptical:
+ return Image.fromarray(img)
+
if cfg.isTrain:
if random() < cfg.blur_prob:
sig = sample_continuous(cfg.blur_sig)
From 6d74fd55f19989d1aa1ca23977566a6de3d57502 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Wed, 26 Nov 2025 02:28:58 +0800
Subject: [PATCH 46/55] fix: Update import path for CONFIGCLASS and enhance
dataset transformations for training
---
core/utils1/datasets.py | 57 ++++++++++++++++++++++-------------------
1 file changed, 31 insertions(+), 26 deletions(-)
diff --git a/core/utils1/datasets.py b/core/utils1/datasets.py
index 6236369..4e4f589 100644
--- a/core/utils1/datasets.py
+++ b/core/utils1/datasets.py
@@ -13,7 +13,7 @@
from scipy.ndimage import gaussian_filter
from torch.utils.data.sampler import WeightedRandomSampler
-from utils1.config import CONFIGCLASS
+from core.utils1.config import CONFIGCLASS
ImageFile.LOAD_TRUNCATED_IMAGES = True
@@ -27,37 +27,47 @@ def dataset_folder(root: str, cfg: CONFIGCLASS):
def binary_dataset(root: str, cfg: CONFIGCLASS):
+ # identity_transform = transforms.Lambda(lambda img: img)
+ # rz_func = identity_transform
+ # issue here, destroys performance as no resizing happens at all that go straight to cropping even if they are different resolutions
+
identity_transform = transforms.Lambda(lambda img: img)
-
- rz_func = identity_transform
-
- if cfg.isTrain:
- crop_func = transforms.RandomCrop((448,448))
+
+ # Enable resize (paper implies resize > crop for random cropping)
+ if cfg.aug_resize:
+ rz_func = transforms.Lambda(lambda img: custom_resize(img, cfg))
else:
- crop_func = transforms.CenterCrop((448,448)) if cfg.aug_crop else identity_transform
+ rz_func = identity_transform
- if cfg.isTrain and cfg.aug_flip and not (hasattr(cfg, 'isOptical') and cfg.isOptical):
+ # Crop to cfg.cropSize (paper uses 448)
+ if cfg.isTrain and not cfg.optical_crop: # standard RGB training
+ crop_func = transforms.RandomCrop((cfg.cropSize, cfg.cropSize))
+ else:
+ crop_func = transforms.CenterCrop((cfg.cropSize, cfg.cropSize)) if cfg.aug_crop else identity_transform
+
+ # Flip only in training if enabled (disabled for optical flow)
+ if cfg.isTrain and cfg.aug_flip:
flip_func = transforms.RandomHorizontalFlip()
else:
- flip_func = identity_transform
-
+ flip_func = identity_transform # fallback
+
return datasets.ImageFolder(
root,
transforms.Compose(
[
- rz_func,
- #change
- transforms.Lambda(lambda img: blur_jpg_augment(img, cfg)),
- crop_func,
- flip_func,
- transforms.ToTensor(),
- transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
- if cfg.aug_norm
- else identity_transform,
- ]
+ rz_func,
+ #change
+ crop_func,
+ transforms.Lambda(lambda img: blur_jpg_augment(img, cfg)),
+ flip_func,
+ transforms.ToTensor(),
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ if cfg.aug_norm
+ else identity_transform,
+ ]
+ )
)
- )
class FileNameDataset(datasets.ImageFolder):
@@ -76,11 +86,6 @@ def __getitem__(self, index):
def blur_jpg_augment(img: Image.Image, cfg: CONFIGCLASS):
img: np.ndarray = np.array(img)
-
- # Skip augmentations for optical flow to preserve motion vector integrity
- if hasattr(cfg, 'isOptical') and cfg.isOptical:
- return Image.fromarray(img)
-
if cfg.isTrain:
if random() < cfg.blur_prob:
sig = sample_continuous(cfg.blur_sig)
From 89411320435860b186a55b7477f454a321014b4f Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Wed, 26 Nov 2025 15:54:22 +0800
Subject: [PATCH 47/55] Add W&B integration guide, data download scripts, and
processing utilities
- Created WANDB_SETUP.md for detailed W&B integration instructions.
- Implemented download_data.py to handle dataset, checkpoint, and RAFT model downloads.
- Added download_t2v.py for downloading a specific file from Google Drive.
- Developed prepare_data.py to extract frames from videos and compute optical flow using RAFT.
- Introduced process_custom_videos.py for processing custom videos with optical flow extraction.
- Added recreate_table_2.py to facilitate the recreation of evaluation results for specific datasets.
- Implemented test_flat.py for evaluating model performance on RGB and optical flow frames.
---
build-docker.ps1 | 65 -------
build-docker.sh | 65 -------
core/utils1/config.py | 2 +-
core/utils1/datasets.py | 2 +-
demo.sh | 1 -
download_data.sh | 50 ------
DATA_SETUP.md => md-files/DATA_SETUP.md | 0
.../DOCKER_COMPARISON.md | 0
DOCKER_INDEX.md => md-files/DOCKER_INDEX.md | 0
.../DOCKER_QUICKREF.md | 0
DOCKER_USAGE.md => md-files/DOCKER_USAGE.md | 0
.../DOCKER_VM_SETUP.md | 0
WANDB_SETUP.md => md-files/WANDB_SETUP.md | 0
push-docker.ps1 | 144 ----------------
push-docker.sh | 150 -----------------
push-simple.ps1 | 49 ------
.../download_data.py | 0
.../download_t2v.py | 0
.../prepare_data.py | 0
.../process_custom_videos.py | 0
python-utils/recreate_table_2.py | 82 +++++++++
python-utils/test_flat.py | 159 ++++++++++++++++++
recreate_table_2.py | 63 -------
test.sh | 1 -
train.sh | 4 -
25 files changed, 243 insertions(+), 594 deletions(-)
delete mode 100644 build-docker.ps1
delete mode 100644 build-docker.sh
delete mode 100644 demo.sh
delete mode 100644 download_data.sh
rename DATA_SETUP.md => md-files/DATA_SETUP.md (100%)
rename DOCKER_COMPARISON.md => md-files/DOCKER_COMPARISON.md (100%)
rename DOCKER_INDEX.md => md-files/DOCKER_INDEX.md (100%)
rename DOCKER_QUICKREF.md => md-files/DOCKER_QUICKREF.md (100%)
rename DOCKER_USAGE.md => md-files/DOCKER_USAGE.md (100%)
rename DOCKER_VM_SETUP.md => md-files/DOCKER_VM_SETUP.md (100%)
rename WANDB_SETUP.md => md-files/WANDB_SETUP.md (100%)
delete mode 100644 push-docker.ps1
delete mode 100644 push-docker.sh
delete mode 100644 push-simple.ps1
rename download_data.py => python-utils/download_data.py (100%)
rename download_t2v.py => python-utils/download_t2v.py (100%)
rename prepare_data.py => python-utils/prepare_data.py (100%)
rename process_custom_videos.py => python-utils/process_custom_videos.py (100%)
create mode 100644 python-utils/recreate_table_2.py
create mode 100644 python-utils/test_flat.py
delete mode 100644 recreate_table_2.py
delete mode 100644 test.sh
delete mode 100644 train.sh
diff --git a/build-docker.ps1 b/build-docker.ps1
deleted file mode 100644
index a545b26..0000000
--- a/build-docker.ps1
+++ /dev/null
@@ -1,65 +0,0 @@
-# PowerShell build script for AIGVDet Docker images
-
-Write-Host "======================================" -ForegroundColor Cyan
-Write-Host "AIGVDet Docker Build Script" -ForegroundColor Cyan
-Write-Host "======================================" -ForegroundColor Cyan
-Write-Host ""
-
-function Build-Image {
- param(
- [string]$Dockerfile,
- [string]$Tag
- )
-
- Write-Host "Building $Tag image..." -ForegroundColor Yellow
- docker build -f $Dockerfile -t aigvdet:$Tag .
-
- if ($LASTEXITCODE -eq 0) {
- Write-Host "✅ Successfully built aigvdet:$Tag" -ForegroundColor Green
- } else {
- Write-Host "❌ Failed to build aigvdet:$Tag" -ForegroundColor Red
- exit 1
- }
- Write-Host ""
-}
-
-# Parse arguments
-$BuildType = if ($args.Count -gt 0) { $args[0] } else { "all" }
-
-switch ($BuildType) {
- "gpu" {
- Write-Host "Building GPU image only..." -ForegroundColor Yellow
- Build-Image -Dockerfile "Dockerfile.gpu" -Tag "gpu"
- }
- "cpu" {
- Write-Host "Building CPU image only..." -ForegroundColor Yellow
- Build-Image -Dockerfile "Dockerfile.cpu" -Tag "cpu"
- }
- "all" {
- Write-Host "Building both CPU and GPU images..." -ForegroundColor Yellow
- Build-Image -Dockerfile "Dockerfile.cpu" -Tag "cpu"
- Build-Image -Dockerfile "Dockerfile.gpu" -Tag "gpu"
- }
- default {
- Write-Host "Usage: .\build-docker.ps1 {gpu|cpu|all}" -ForegroundColor Red
- Write-Host " gpu - Build GPU image only"
- Write-Host " cpu - Build CPU image only"
- Write-Host " all - Build both images (default)"
- exit 1
- }
-}
-
-Write-Host "======================================" -ForegroundColor Cyan
-Write-Host "Build completed!" -ForegroundColor Green
-Write-Host "======================================" -ForegroundColor Cyan
-Write-Host ""
-Write-Host "Available images:" -ForegroundColor Yellow
-docker images | Select-String aigvdet
-Write-Host ""
-Write-Host "To run:" -ForegroundColor Yellow
-Write-Host " GPU: docker run --gpus all -it --rm aigvdet:gpu"
-Write-Host " CPU: docker run -it --rm aigvdet:cpu"
-Write-Host ""
-Write-Host "Or use docker-compose:" -ForegroundColor Yellow
-Write-Host " docker-compose up aigvdet-gpu"
-Write-Host " docker-compose up aigvdet-cpu"
diff --git a/build-docker.sh b/build-docker.sh
deleted file mode 100644
index 5320cca..0000000
--- a/build-docker.sh
+++ /dev/null
@@ -1,65 +0,0 @@
-#!/bin/bash
-# Build script for AIGVDet Docker images
-
-set -e
-
-echo "======================================"
-echo "AIGVDet Docker Build Script"
-echo "======================================"
-echo ""
-
-# Function to build images
-build_image() {
- local dockerfile=$1
- local tag=$2
-
- echo "Building $tag image..."
- docker build -f $dockerfile -t aigvdet:$tag .
-
- if [ $? -eq 0 ]; then
- echo "✅ Successfully built aigvdet:$tag"
- else
- echo "❌ Failed to build aigvdet:$tag"
- exit 1
- fi
- echo ""
-}
-
-# Parse arguments
-case "$1" in
- gpu)
- echo "Building GPU image only..."
- build_image Dockerfile.gpu gpu
- ;;
- cpu)
- echo "Building CPU image only..."
- build_image Dockerfile.cpu cpu
- ;;
- all|"")
- echo "Building both CPU and GPU images..."
- build_image Dockerfile.cpu cpu
- build_image Dockerfile.gpu gpu
- ;;
- *)
- echo "Usage: $0 {gpu|cpu|all}"
- echo " gpu - Build GPU image only"
- echo " cpu - Build CPU image only"
- echo " all - Build both images (default)"
- exit 1
- ;;
-esac
-
-echo "======================================"
-echo "Build completed!"
-echo "======================================"
-echo ""
-echo "Available images:"
-docker images | grep aigvdet
-echo ""
-echo "To run:"
-echo " GPU: docker run --gpus all -it --rm aigvdet:gpu"
-echo " CPU: docker run -it --rm aigvdet:cpu"
-echo ""
-echo "Or use docker-compose:"
-echo " docker-compose up aigvdet-gpu"
-echo " docker-compose up aigvdet-cpu"
diff --git a/core/utils1/config.py b/core/utils1/config.py
index 6231905..c8c9f3e 100644
--- a/core/utils1/config.py
+++ b/core/utils1/config.py
@@ -29,7 +29,7 @@ class DefaultConfigs(ABC):
blur_sig = [0.5]
# jpg_prob = 0.1 - resnet50 template
jpg_prob = 0.1 # optical flow
- # P(augmented) = 1-(1-0.5)(1-0.05) = 0.9
+ # P(augmented) = 1-(1-0.5)(1-0.05) = 0.975
jpg_method = ["cv2"]
jpg_qual = list(range(70, 91))
gray_prob = 0.0
diff --git a/core/utils1/datasets.py b/core/utils1/datasets.py
index 4e4f589..aa20f29 100644
--- a/core/utils1/datasets.py
+++ b/core/utils1/datasets.py
@@ -40,7 +40,7 @@ def binary_dataset(root: str, cfg: CONFIGCLASS):
rz_func = identity_transform
# Crop to cfg.cropSize (paper uses 448)
- if cfg.isTrain and not cfg.optical_crop: # standard RGB training
+ if cfg.isTrain:
crop_func = transforms.RandomCrop((cfg.cropSize, cfg.cropSize))
else:
crop_func = transforms.CenterCrop((cfg.cropSize, cfg.cropSize)) if cfg.aug_crop else identity_transform
diff --git a/demo.sh b/demo.sh
deleted file mode 100644
index c81762a..0000000
--- a/demo.sh
+++ /dev/null
@@ -1 +0,0 @@
-python demo.py --use_cpu --path "video/000000.mp4" --folder_original_path "frame/000000" --folder_optical_flow_path "optical_result/000000" -mop "checkpoints/optical.pth" -mor "checkpoints/original.pth"
\ No newline at end of file
diff --git a/download_data.sh b/download_data.sh
deleted file mode 100644
index f9a4879..0000000
--- a/download_data.sh
+++ /dev/null
@@ -1,50 +0,0 @@
-#!/bin/bash
-set -e
-
-# Configuration
-# Default File ID provided by user, can be overridden by argument
-FILE_ID="${1:-1YO3qRKbWxOYEm86Vy9QlGMjyi5Q_A6m0}"
-DATA_DIR="/app/data"
-ZIP_FILE="${DATA_DIR}/data.zip"
-
-echo "Starting data download setup..."
-
-# 1. Install dependencies if missing
-if ! command -v gdown &> /dev/null; then
- echo "Installing gdown..."
- pip install gdown
-fi
-
-if ! command -v unzip &> /dev/null; then
- echo "Installing unzip..."
- apt-get update && apt-get install -y unzip
-fi
-
-# 2. Check if data already exists
-if [ -d "${DATA_DIR}/train" ]; then
- echo "Data directory ${DATA_DIR}/train already exists."
- read -p "Do you want to re-download? (y/N) " -n 1 -r
- echo
- if [[ ! $REPLY =~ ^[Yy]$ ]]; then
- echo "Skipping download."
- exit 0
- fi
-fi
-
-# 3. Create data directory
-mkdir -p "$DATA_DIR"
-
-# 4. Download file
-echo "Downloading data from Google Drive (ID: $FILE_ID)..."
-gdown "$FILE_ID" -O "$ZIP_FILE"
-
-# 5. Extract
-echo "Extracting data..."
-unzip -o "$ZIP_FILE" -d "$DATA_DIR"
-
-# 6. Cleanup
-echo "Cleaning up zip file..."
-rm "$ZIP_FILE"
-
-echo "Data setup complete!"
-ls -F "$DATA_DIR"
diff --git a/DATA_SETUP.md b/md-files/DATA_SETUP.md
similarity index 100%
rename from DATA_SETUP.md
rename to md-files/DATA_SETUP.md
diff --git a/DOCKER_COMPARISON.md b/md-files/DOCKER_COMPARISON.md
similarity index 100%
rename from DOCKER_COMPARISON.md
rename to md-files/DOCKER_COMPARISON.md
diff --git a/DOCKER_INDEX.md b/md-files/DOCKER_INDEX.md
similarity index 100%
rename from DOCKER_INDEX.md
rename to md-files/DOCKER_INDEX.md
diff --git a/DOCKER_QUICKREF.md b/md-files/DOCKER_QUICKREF.md
similarity index 100%
rename from DOCKER_QUICKREF.md
rename to md-files/DOCKER_QUICKREF.md
diff --git a/DOCKER_USAGE.md b/md-files/DOCKER_USAGE.md
similarity index 100%
rename from DOCKER_USAGE.md
rename to md-files/DOCKER_USAGE.md
diff --git a/DOCKER_VM_SETUP.md b/md-files/DOCKER_VM_SETUP.md
similarity index 100%
rename from DOCKER_VM_SETUP.md
rename to md-files/DOCKER_VM_SETUP.md
diff --git a/WANDB_SETUP.md b/md-files/WANDB_SETUP.md
similarity index 100%
rename from WANDB_SETUP.md
rename to md-files/WANDB_SETUP.md
diff --git a/push-docker.ps1 b/push-docker.ps1
deleted file mode 100644
index d5ecafb..0000000
--- a/push-docker.ps1
+++ /dev/null
@@ -1,144 +0,0 @@
-# Docker Push Script for AIGVDet
-# Pushes images to Docker Hub: sacdalance/thesis-aigvdet
-
-param(
- [Parameter(Mandatory=$false)]
- [ValidateSet("gpu", "cpu", "all")]
- [string]$Version = "all",
-
- [Parameter(Mandatory=$false)]
- [string]$Tag = "latest"
-)
-
-$ErrorActionPreference = "Stop"
-$Repository = "sacdalance/thesis-aigvdet"
-
-Write-Host "`n╔═══════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
-Write-Host "║ Docker Push Script - AIGVDet ║" -ForegroundColor Cyan
-Write-Host "║ Repository: $Repository ║" -ForegroundColor Cyan
-Write-Host "╚═══════════════════════════════════════════════════════════╝`n" -ForegroundColor Cyan
-
-function Build-And-Push-Image {
- param(
- [string]$Dockerfile,
- [string]$ImageTag,
- [string]$VersionTag
- )
-
- $LocalTag = "aigvdet:$ImageTag"
- $RemoteTag = "${Repository}:${VersionTag}"
- $RemoteLatestTag = "${Repository}:${ImageTag}"
-
- Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor DarkGray
- Write-Host "🔨 Building $ImageTag image..." -ForegroundColor Yellow
- Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor DarkGray
-
- docker build -f $Dockerfile -t $LocalTag -t $RemoteTag -t $RemoteLatestTag .
-
- if ($LASTEXITCODE -ne 0) {
- Write-Host "❌ Failed to build $ImageTag image" -ForegroundColor Red
- exit 1
- }
-
- Write-Host "✅ Successfully built $ImageTag image" -ForegroundColor Green
- Write-Host ""
-
- Write-Host "📤 Pushing to Docker Hub..." -ForegroundColor Yellow
-
- # Push with version tag
- Write-Host " → Pushing ${RemoteTag}..." -ForegroundColor Cyan
- docker push $RemoteTag
-
- if ($LASTEXITCODE -ne 0) {
- Write-Host "❌ Failed to push ${RemoteTag}" -ForegroundColor Red
- exit 1
- }
-
- # Push with latest tag
- Write-Host " → Pushing ${RemoteLatestTag}..." -ForegroundColor Cyan
- docker push $RemoteLatestTag
-
- if ($LASTEXITCODE -ne 0) {
- Write-Host "❌ Failed to push ${RemoteLatestTag}" -ForegroundColor Red
- exit 1
- }
-
- Write-Host "✅ Successfully pushed $ImageTag image to Docker Hub" -ForegroundColor Green
- Write-Host ""
-}
-
-# Check if logged in to Docker Hub
-Write-Host "🔐 Checking Docker Hub authentication..." -ForegroundColor Yellow
-docker info | Out-Null
-if ($LASTEXITCODE -ne 0) {
- Write-Host "❌ Docker is not running or not accessible" -ForegroundColor Red
- exit 1
-}
-
-# Try to verify login (will fail if not logged in)
-$loginCheck = docker info 2>&1 | Select-String "Username"
-if (-not $loginCheck) {
- Write-Host "⚠️ You may not be logged in to Docker Hub" -ForegroundColor Yellow
- Write-Host "Please run: docker login" -ForegroundColor Yellow
- $continue = Read-Host "`nContinue anyway? (y/N)"
- if ($continue -ne "y" -and $continue -ne "Y") {
- exit 0
- }
-}
-Write-Host "✅ Docker is ready`n" -ForegroundColor Green
-
-# Build and push based on version
-switch ($Version) {
- "gpu" {
- Write-Host "Building and pushing GPU version only...`n" -ForegroundColor Yellow
- Build-And-Push-Image -Dockerfile "Dockerfile.gpu" -ImageTag "gpu" -VersionTag "$Tag-gpu"
- }
- "cpu" {
- Write-Host "Building and pushing CPU version only...`n" -ForegroundColor Yellow
- Build-And-Push-Image -Dockerfile "Dockerfile.cpu" -ImageTag "cpu" -VersionTag "$Tag-cpu"
- }
- "all" {
- Write-Host "Building and pushing both CPU and GPU versions...`n" -ForegroundColor Yellow
- Build-And-Push-Image -Dockerfile "Dockerfile.cpu" -ImageTag "cpu" -VersionTag "$Tag-cpu"
- Build-And-Push-Image -Dockerfile "Dockerfile.gpu" -ImageTag "gpu" -VersionTag "$Tag-gpu"
- }
-}
-
-Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor DarkGray
-Write-Host "🎉 All images pushed successfully!" -ForegroundColor Green
-Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor DarkGray
-Write-Host ""
-Write-Host "📦 Available images on Docker Hub:" -ForegroundColor Yellow
-
-if ($Version -eq "cpu" -or $Version -eq "all") {
- Write-Host " • $Repository" -NoNewline -ForegroundColor Cyan
- Write-Host ":cpu" -ForegroundColor Cyan
- Write-Host " • $Repository" -NoNewline -ForegroundColor Cyan
- Write-Host ":$Tag-cpu" -ForegroundColor Cyan
-}
-
-if ($Version -eq "gpu" -or $Version -eq "all") {
- Write-Host " • $Repository" -NoNewline -ForegroundColor Cyan
- Write-Host ":gpu" -ForegroundColor Cyan
- Write-Host " • $Repository" -NoNewline -ForegroundColor Cyan
- Write-Host ":$Tag-gpu" -ForegroundColor Cyan
-}
-
-Write-Host ""
-Write-Host "🚀 Pull commands:" -ForegroundColor Yellow
-
-if ($Version -eq "cpu" -or $Version -eq "all") {
- Write-Host " CPU: docker pull $Repository" -NoNewline -ForegroundColor White
- Write-Host ":cpu" -ForegroundColor White
-}
-
-if ($Version -eq "gpu" -or $Version -eq "all") {
- Write-Host " GPU: docker pull $Repository" -NoNewline -ForegroundColor White
- Write-Host ":gpu" -ForegroundColor White
-}
-
-Write-Host ""
-Write-Host "💡 View on Docker Hub:" -ForegroundColor Yellow
-$DockerHubUrl = "https://hub.docker.com/r/$Repository"
-Write-Host " $DockerHubUrl" -ForegroundColor Cyan
-Write-Host ""
diff --git a/push-docker.sh b/push-docker.sh
deleted file mode 100644
index c59a862..0000000
--- a/push-docker.sh
+++ /dev/null
@@ -1,150 +0,0 @@
-#!/bin/bash
-# Docker Push Script for AIGVDet
-# Pushes images to Docker Hub: sacdalance/thesis-aigvdet
-
-set -e
-
-VERSION="${1:-all}"
-TAG="${2:-latest}"
-REPOSITORY="sacdalance/thesis-aigvdet"
-
-# Colors
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-CYAN='\033[0;36m'
-NC='\033[0m' # No Color
-
-echo ""
-echo "╔═══════════════════════════════════════════════════════════╗"
-echo "║ Docker Push Script - AIGVDet ║"
-echo "║ Repository: ${REPOSITORY}"
-echo "╚═══════════════════════════════════════════════════════════╝"
-echo ""
-
-build_and_push_image() {
- local dockerfile=$1
- local image_tag=$2
- local version_tag=$3
-
- local local_tag="aigvdet:${image_tag}"
- local remote_tag="${REPOSITORY}:${version_tag}"
- local remote_latest_tag="${REPOSITORY}:${image_tag}"
-
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
- echo -e "${YELLOW}🔨 Building ${image_tag} image...${NC}"
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
-
- docker build -f "${dockerfile}" -t "${local_tag}" -t "${remote_tag}" -t "${remote_latest_tag}" .
-
- if [ $? -ne 0 ]; then
- echo -e "${RED}❌ Failed to build ${image_tag} image${NC}"
- exit 1
- fi
-
- echo -e "${GREEN}✅ Successfully built ${image_tag} image${NC}"
- echo ""
-
- echo -e "${YELLOW}📤 Pushing to Docker Hub...${NC}"
-
- # Push with version tag
- echo -e "${CYAN} → Pushing ${remote_tag}...${NC}"
- docker push "${remote_tag}"
-
- if [ $? -ne 0 ]; then
- echo -e "${RED}❌ Failed to push ${remote_tag}${NC}"
- exit 1
- fi
-
- # Push with latest tag
- echo -e "${CYAN} → Pushing ${remote_latest_tag}...${NC}"
- docker push "${remote_latest_tag}"
-
- if [ $? -ne 0 ]; then
- echo -e "${RED}❌ Failed to push ${remote_latest_tag}${NC}"
- exit 1
- fi
-
- echo -e "${GREEN}✅ Successfully pushed ${image_tag} image to Docker Hub${NC}"
- echo ""
-}
-
-# Check if Docker is running
-echo -e "${YELLOW}🔐 Checking Docker authentication...${NC}"
-if ! docker info > /dev/null 2>&1; then
- echo -e "${RED}❌ Docker is not running or not accessible${NC}"
- exit 1
-fi
-
-# Check if logged in
-if ! docker info 2>&1 | grep -q "Username"; then
- echo -e "${YELLOW}⚠️ You may not be logged in to Docker Hub${NC}"
- echo -e "${YELLOW}Please run: docker login${NC}"
- read -p "Continue anyway? (y/N) " -n 1 -r
- echo
- if [[ ! $REPLY =~ ^[Yy]$ ]]; then
- exit 0
- fi
-fi
-echo -e "${GREEN}✅ Docker is ready${NC}"
-echo ""
-
-# Build and push based on version
-case "$VERSION" in
- gpu)
- echo -e "${YELLOW}Building and pushing GPU version only...${NC}"
- echo ""
- build_and_push_image "Dockerfile.gpu" "gpu" "${TAG}-gpu"
- ;;
- cpu)
- echo -e "${YELLOW}Building and pushing CPU version only...${NC}"
- echo ""
- build_and_push_image "Dockerfile.cpu" "cpu" "${TAG}-cpu"
- ;;
- all)
- echo -e "${YELLOW}Building and pushing both CPU and GPU versions...${NC}"
- echo ""
- build_and_push_image "Dockerfile.cpu" "cpu" "${TAG}-cpu"
- build_and_push_image "Dockerfile.gpu" "gpu" "${TAG}-gpu"
- ;;
- *)
- echo -e "${RED}Usage: $0 {gpu|cpu|all} [tag]${NC}"
- echo " gpu - Push GPU image only"
- echo " cpu - Push CPU image only"
- echo " all - Push both images (default)"
- echo " tag - Version tag (default: latest)"
- exit 1
- ;;
-esac
-
-echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
-echo -e "${GREEN}🎉 All images pushed successfully!${NC}"
-echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
-echo ""
-echo -e "${YELLOW}📦 Available images on Docker Hub:${NC}"
-
-if [ "$VERSION" = "cpu" ] || [ "$VERSION" = "all" ]; then
- echo -e "${CYAN} • ${REPOSITORY}:cpu${NC}"
- echo -e "${CYAN} • ${REPOSITORY}:${TAG}-cpu${NC}"
-fi
-
-if [ "$VERSION" = "gpu" ] || [ "$VERSION" = "all" ]; then
- echo -e "${CYAN} • ${REPOSITORY}:gpu${NC}"
- echo -e "${CYAN} • ${REPOSITORY}:${TAG}-gpu${NC}"
-fi
-
-echo ""
-echo -e "${YELLOW}🚀 Pull commands:${NC}"
-
-if [ "$VERSION" = "cpu" ] || [ "$VERSION" = "all" ]; then
- echo " CPU: docker pull ${REPOSITORY}:cpu"
-fi
-
-if [ "$VERSION" = "gpu" ] || [ "$VERSION" = "all" ]; then
- echo " GPU: docker pull ${REPOSITORY}:gpu"
-fi
-
-echo ""
-echo -e "${YELLOW}💡 View on Docker Hub:${NC}"
-echo -e "${CYAN} https://hub.docker.com/r/${REPOSITORY}${NC}"
-echo ""
diff --git a/push-simple.ps1 b/push-simple.ps1
deleted file mode 100644
index b95146f..0000000
--- a/push-simple.ps1
+++ /dev/null
@@ -1,49 +0,0 @@
-# Simple Docker Push - Complete Workflow
-# Run this after images are built
-
-Write-Host "`n========================================" -ForegroundColor Cyan
-Write-Host "Docker Push to sacdalance/thesis-aigvdet" -ForegroundColor Green
-Write-Host "========================================`n" -ForegroundColor Cyan
-
-$repo = "sacdalance/thesis-aigvdet"
-
-# Push CPU image
-Write-Host "📤 Pushing CPU image..." -ForegroundColor Yellow
-docker push "${repo}:cpu"
-docker push "${repo}:latest-cpu"
-
-if ($LASTEXITCODE -eq 0) {
- Write-Host "✅ CPU image pushed successfully!`n" -ForegroundColor Green
-} else {
- Write-Host "❌ Failed to push CPU image`n" -ForegroundColor Red
-}
-
-# Build and push GPU image
-Write-Host "🔨 Building GPU image..." -ForegroundColor Yellow
-docker build -f Dockerfile.gpu -t "${repo}:gpu" -t "${repo}:latest-gpu" .
-
-if ($LASTEXITCODE -eq 0) {
- Write-Host "✅ GPU image built successfully!`n" -ForegroundColor Green
-
- Write-Host "📤 Pushing GPU image..." -ForegroundColor Yellow
- docker push "${repo}:gpu"
- docker push "${repo}:latest-gpu"
-
- if ($LASTEXITCODE -eq 0) {
- Write-Host "✅ GPU image pushed successfully!`n" -ForegroundColor Green
- } else {
- Write-Host "❌ Failed to push GPU image`n" -ForegroundColor Red
- }
-} else {
- Write-Host "❌ Failed to build GPU image`n" -ForegroundColor Red
-}
-
-Write-Host "`n========================================" -ForegroundColor Cyan
-Write-Host "✨ Push Complete!" -ForegroundColor Green
-Write-Host "========================================`n" -ForegroundColor Cyan
-
-Write-Host "📦 Your images are now available at:" -ForegroundColor Yellow
-Write-Host " • docker pull ${repo}:cpu" -ForegroundColor Cyan
-Write-Host " • docker pull ${repo}:gpu" -ForegroundColor Cyan
-Write-Host "`n🌐 View on Docker Hub:" -ForegroundColor Yellow
-Write-Host " https://hub.docker.com/r/${repo}`n" -ForegroundColor Cyan
diff --git a/download_data.py b/python-utils/download_data.py
similarity index 100%
rename from download_data.py
rename to python-utils/download_data.py
diff --git a/download_t2v.py b/python-utils/download_t2v.py
similarity index 100%
rename from download_t2v.py
rename to python-utils/download_t2v.py
diff --git a/prepare_data.py b/python-utils/prepare_data.py
similarity index 100%
rename from prepare_data.py
rename to python-utils/prepare_data.py
diff --git a/process_custom_videos.py b/python-utils/process_custom_videos.py
similarity index 100%
rename from process_custom_videos.py
rename to python-utils/process_custom_videos.py
diff --git a/python-utils/recreate_table_2.py b/python-utils/recreate_table_2.py
new file mode 100644
index 0000000..2674edb
--- /dev/null
+++ b/python-utils/recreate_table_2.py
@@ -0,0 +1,82 @@
+import argparse
+import subprocess
+import os
+import sys
+
+def run_command(command):
+ print(f"Running: {command}")
+ try:
+ subprocess.check_call(command, shell=True)
+ except subprocess.CalledProcessError as e:
+ print(f"Error running command: {command}")
+ sys.exit(1)
+
+def main():
+ parser = argparse.ArgumentParser(description="Recreate Table 2 results for a specific dataset.")
+ parser.add_argument("dataset", help="Name of the dataset (e.g., moonvalley, videocraft, pika, neverends)")
+ parser.add_argument("--rgb_dir", help="Path to RGB frames directory", default=None)
+ parser.add_argument("--flow_dir", help="Path to Optical Flow frames directory", default=None)
+ args = parser.parse_args()
+
+ dataset_name = args.dataset
+
+ # Define paths
+ # Default paths (relative to current directory)
+ base_data_dir = "data"
+
+ # If arguments are provided, use them. Otherwise, construct default paths.
+ if args.rgb_dir:
+ output_rgb_dir = args.rgb_dir
+ else:
+ # Matches structure: data/test/videocraft_rgb
+ output_rgb_dir = os.path.join(base_data_dir, "test", f"{dataset_name}_rgb")
+
+ if args.flow_dir:
+ output_flow_dir = args.flow_dir
+ else:
+ # Matches structure: data/test/videocraft_flow
+ output_flow_dir = os.path.join(base_data_dir, "test", f"{dataset_name}_flow")
+
+ # Source video dir (only needed for preparation step, which is skipped)
+ source_video_dir = os.path.join(base_data_dir, "test", "T2V", dataset_name)
+
+ result_csv = os.path.join(base_data_dir, "results", f"{dataset_name}.csv")
+ result_no_cp_csv = os.path.join(base_data_dir, "results", f"{dataset_name}_no_cp.csv")
+
+ # Model paths (relative)
+ raft_model = "raft_model/raft-things.pth"
+ optical_model = "checkpoints/optical.pth"
+ original_model = "checkpoints/original.pth"
+
+ print(f"--- Processing Dataset: {dataset_name} ---")
+ print(f"RGB Directory: {output_rgb_dir}")
+ print(f"Flow Directory: {output_flow_dir}")
+
+ # Check if directories exist
+ if not os.path.exists(output_rgb_dir):
+ print(f"Warning: RGB directory not found: {output_rgb_dir}")
+ if not os.path.exists(output_flow_dir):
+ print(f"Warning: Flow directory not found: {output_flow_dir}")
+
+ # Step 1: Prepare Data
+ # print("\n[Step 1] Preparing Data (Extracting Frames & Generating Optical Flow)...")
+ # # Note: We use sys.executable to ensure we use the same python interpreter
+ # cmd_prepare = f'"{sys.executable}" prepare_data.py --source_dir "{source_video_dir}" --output_rgb_dir "{output_rgb_dir}" --output_flow_dir "{output_flow_dir}" --model "{raft_model}"'
+ # run_command(cmd_prepare)
+
+ # Step 2: Run Standard Evaluation
+ print("\n[Step 2] Running Standard Evaluation (AIGVDet, Sspatial, Soptical)...")
+ cmd_test = f'"{sys.executable}" test_flat.py -fop "{output_flow_dir}" -for "{output_rgb_dir}" -mop "{optical_model}" -mor "{original_model}" -e "{result_csv}"'
+ run_command(cmd_test)
+
+ # Step 3: Run No-Crop Evaluation
+ print("\n[Step 3] Running No-Crop Evaluation (Soptical no cp)...")
+ cmd_test_no_cp = f'"{sys.executable}" test_flat.py --no_crop -fop "{output_flow_dir}" -for "{output_rgb_dir}" -mop "{optical_model}" -mor "{original_model}" -e "{result_no_cp_csv}"'
+ run_command(cmd_test_no_cp)
+
+ print("\n--- Done! ---")
+ print(f"Standard Results saved to: {result_csv}")
+ print(f"No-Crop Results saved to: {result_no_cp_csv}")
+
+if __name__ == "__main__":
+ main()
diff --git a/python-utils/test_flat.py b/python-utils/test_flat.py
new file mode 100644
index 0000000..cc2668b
--- /dev/null
+++ b/python-utils/test_flat.py
@@ -0,0 +1,159 @@
+import argparse
+import glob
+import os
+import pandas as pd
+import torch
+import torch.nn
+import torchvision.transforms as transforms
+import torchvision.transforms.functional as TF
+from PIL import Image
+from tqdm import tqdm
+from sklearn.metrics import accuracy_score, roc_auc_score
+from core.utils1.utils import get_network, str2bool
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-fop", "--folder_optical_flow_path", type=str, required=True)
+ parser.add_argument("-for", "--folder_original_path", type=str, required=True)
+ parser.add_argument("-mop", "--model_optical_flow_path", type=str, default="checkpoints/optical.pth")
+ parser.add_argument("-mor", "--model_original_path", type=str, default="checkpoints/original.pth")
+ parser.add_argument("-t", "--threshold", type=float, default=0.5)
+ parser.add_argument("-e", "--excel_path", type=str, default="results.csv")
+ parser.add_argument("--use_cpu", action="store_true")
+ parser.add_argument("--arch", type=str, default="resnet50")
+ parser.add_argument("--aug_norm", type=str2bool, default=True)
+ parser.add_argument("--no_crop", action="store_true")
+
+ args = parser.parse_args()
+
+ # Load Models
+ device = torch.device("cpu" if args.use_cpu else "cuda")
+
+ print("Loading models...")
+ model_op = get_network(args.arch).to(device)
+ model_op.load_state_dict(torch.load(args.model_optical_flow_path, map_location=device)["model"])
+ model_op.eval()
+
+ model_or = get_network(args.arch).to(device)
+ model_or.load_state_dict(torch.load(args.model_original_path, map_location=device)["model"])
+ model_or.eval()
+
+ # Transforms
+ if args.no_crop:
+ trans = transforms.Compose([transforms.ToTensor()])
+ else:
+ trans = transforms.Compose([transforms.CenterCrop((448, 448)), transforms.ToTensor()])
+
+ print(f"Processing flat directories...")
+ print(f"RGB: {args.folder_original_path}")
+ print(f"Flow: {args.folder_optical_flow_path}")
+
+ # Get list of images
+ # We assume filenames match between RGB and Flow (except maybe extension)
+ # Actually, process_custom_videos outputs:
+ # RGB: video_name_frame_00001.jpg
+ # Flow: video_name_frame_00001.jpg (or .png)
+
+ rgb_files = sorted(glob.glob(os.path.join(args.folder_original_path, "*")))
+ rgb_files = [f for f in rgb_files if f.lower().endswith(('.png', '.jpg', '.jpeg'))]
+
+ if len(rgb_files) == 0:
+ print("No RGB images found!")
+ return
+
+ print(f"Found {len(rgb_files)} RGB frames.")
+
+ y_true = []
+ y_pred = []
+ y_pred_rgb = []
+ y_pred_flow = []
+
+ results = []
+
+ # Iterate
+ for rgb_path in tqdm(rgb_files):
+ filename = os.path.basename(rgb_path)
+ # Try to find corresponding flow file
+ # Flow might be .png even if RGB is .jpg
+ flow_path = os.path.join(args.folder_optical_flow_path, filename)
+ if not os.path.exists(flow_path):
+ # Try replacing extension
+ name, ext = os.path.splitext(filename)
+ flow_path_png = os.path.join(args.folder_optical_flow_path, name + ".png")
+ flow_path_jpg = os.path.join(args.folder_optical_flow_path, name + ".jpg")
+ if os.path.exists(flow_path_png):
+ flow_path = flow_path_png
+ elif os.path.exists(flow_path_jpg):
+ flow_path = flow_path_jpg
+ else:
+ # print(f"Warning: Flow file not found for {filename}")
+ continue
+
+ # Load and Preprocess
+ try:
+ img_rgb = Image.open(rgb_path).convert("RGB")
+ img_flow = Image.open(flow_path).convert("RGB")
+ except Exception as e:
+ print(f"Error loading {filename}: {e}")
+ continue
+
+ # Transform
+ t_rgb = trans(img_rgb)
+ t_flow = trans(img_flow)
+
+ if args.aug_norm:
+ t_rgb = TF.normalize(t_rgb, mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ t_flow = TF.normalize(t_flow, mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+
+ t_rgb = t_rgb.unsqueeze(0).to(device)
+ t_flow = t_flow.unsqueeze(0).to(device)
+
+ # Inference
+ with torch.no_grad():
+ prob_rgb = model_or(t_rgb).sigmoid().item()
+ prob_flow = model_op(t_flow).sigmoid().item()
+
+ prob_fused = 0.5 * prob_rgb + 0.5 * prob_flow
+
+ # Assume Fake (1) since we are testing generators
+ label = 1
+
+ y_true.append(label)
+ y_pred.append(prob_fused)
+ y_pred_rgb.append(prob_rgb)
+ y_pred_flow.append(prob_flow)
+
+ results.append({
+ "filename": filename,
+ "prob_fused": prob_fused,
+ "prob_rgb": prob_rgb,
+ "prob_flow": prob_flow,
+ "label": label
+ })
+
+ # Metrics
+ if len(y_true) == 0:
+ print("No valid pairs processed.")
+ return
+
+ acc_fused = accuracy_score(y_true, [1 if p >= args.threshold else 0 for p in y_pred])
+ acc_rgb = accuracy_score(y_true, [1 if p >= args.threshold else 0 for p in y_pred_rgb])
+ acc_flow = accuracy_score(y_true, [1 if p >= args.threshold else 0 for p in y_pred_flow])
+
+ print("-" * 30)
+ print(f"Results (Assuming all inputs are FAKE/Generated)")
+ print(f"Total Frames: {len(y_true)}")
+ print("-" * 30)
+ print(f"Fused Accuracy (Recall): {acc_fused:.4f}")
+ print(f"RGB Accuracy (Recall): {acc_rgb:.4f}")
+ print(f"Flow Accuracy (Recall): {acc_flow:.4f}")
+ print("-" * 30)
+
+ # Save CSV
+ df = pd.DataFrame(results)
+ os.makedirs(os.path.dirname(args.excel_path), exist_ok=True)
+ df.to_csv(args.excel_path, index=False)
+ print(f"Saved results to {args.excel_path}")
+
+if __name__ == "__main__":
+ main()
diff --git a/recreate_table_2.py b/recreate_table_2.py
deleted file mode 100644
index 61f26aa..0000000
--- a/recreate_table_2.py
+++ /dev/null
@@ -1,63 +0,0 @@
-import argparse
-import subprocess
-import os
-import sys
-
-def run_command(command):
- print(f"Running: {command}")
- try:
- subprocess.check_call(command, shell=True)
- except subprocess.CalledProcessError as e:
- print(f"Error running command: {command}")
- sys.exit(1)
-
-def main():
- parser = argparse.ArgumentParser(description="Recreate Table 2 results for a specific dataset.")
- parser.add_argument("dataset", help="Name of the dataset (e.g., moonvalley, videocraft, pika, neverends)")
- args = parser.parse_args()
-
- dataset_name = args.dataset
-
- # Define paths
- # Using /app/data for Docker/Jupyter environment compatibility
- source_video_dir = f"/app/data/test/T2V/{dataset_name}"
- output_rgb_dir = f"/app/data/test/original/T2V/{dataset_name}"
- output_flow_dir = f"/app/data/test/T2V/{dataset_name}_flow"
-
- result_csv = f"/app/data/results/{dataset_name}.csv"
- result_no_cp_csv = f"/app/data/results/{dataset_name}_no_cp.csv"
-
- raft_model = "/app/raft_model/raft-things.pth"
- optical_model = "/app/checkpoints/optical.pth"
- original_model = "/app/checkpoints/original.pth"
-
- print(f"--- Processing Dataset: {dataset_name} ---")
-
- # Check if source directory exists
- if not os.path.exists(source_video_dir):
- print(f"Error: Source video directory not found: {source_video_dir}")
- print("Please download the test videos and place them in the correct folder.")
- sys.exit(1)
-
- # Step 1: Prepare Data
- print("\n[Step 1] Preparing Data (Extracting Frames & Generating Optical Flow)...")
- # Note: We use sys.executable to ensure we use the same python interpreter
- cmd_prepare = f'"{sys.executable}" prepare_data.py --source_dir "{source_video_dir}" --output_rgb_dir "{output_rgb_dir}" --output_flow_dir "{output_flow_dir}" --model "{raft_model}"'
- run_command(cmd_prepare)
-
- # Step 2: Run Standard Evaluation
- print("\n[Step 2] Running Standard Evaluation (AIGVDet, Sspatial, Soptical)...")
- cmd_test = f'"{sys.executable}" test.py -fop "{output_flow_dir}" -for "{output_rgb_dir}" -mop "{optical_model}" -mor "{original_model}" -e "{result_csv}"'
- run_command(cmd_test)
-
- # Step 3: Run No-Crop Evaluation
- print("\n[Step 3] Running No-Crop Evaluation (Soptical no cp)...")
- cmd_test_no_cp = f'"{sys.executable}" test.py --no_crop -fop "{output_flow_dir}" -for "{output_rgb_dir}" -mop "{optical_model}" -mor "{original_model}" -e "{result_no_cp_csv}"'
- run_command(cmd_test_no_cp)
-
- print("\n--- Done! ---")
- print(f"Standard Results saved to: {result_csv}")
- print(f"No-Crop Results saved to: {result_no_cp_csv}")
-
-if __name__ == "__main__":
- main()
diff --git a/test.sh b/test.sh
deleted file mode 100644
index c870a97..0000000
--- a/test.sh
+++ /dev/null
@@ -1 +0,0 @@
-python test.py -fop "data/test/T2V/hotshot" -mop "checkpoints/optical_aug.pth" -for "data/test/original/T2V/hotshot" -mor "checkpoints/original_aug.pth" -e "data/results/T2V/hotshot.csv" -ef "data/results/frame/T2V/hotshot.csv" -t 0.5
\ No newline at end of file
diff --git a/train.sh b/train.sh
deleted file mode 100644
index d8dd0b3..0000000
--- a/train.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-EXP_NAME="moonvalley_vos2_crop"
-DATASETS="moonvalley_vos2_crop"
-DATASETS_TEST="moonvalley_vos2_crop"
-python train.py --gpus 0 --exp_name $EXP_NAME datasets $DATASETS datasets_test $DATASETS_TEST
\ No newline at end of file
From 4567fa80d8eba6dd31695bbd606f68a56b250699 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Wed, 26 Nov 2025 16:15:52 +0800
Subject: [PATCH 48/55] fix: Update custom_resize function and enhance
validation results for W&B integration
---
core/utils1/datasets.py | 4 ++--
core/utils1/eval.py | 8 +++++---
core/utils1/trainer.py | 1 +
3 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/core/utils1/datasets.py b/core/utils1/datasets.py
index aa20f29..273e495 100644
--- a/core/utils1/datasets.py
+++ b/core/utils1/datasets.py
@@ -149,11 +149,11 @@ def jpeg_from_key(img: np.ndarray, compress_val: int, key: str) -> np.ndarray:
'bicubic': Image.BICUBIC,
'lanczos': Image.LANCZOS,
'nearest': Image.NEAREST}
-def custom_resize(img: Image.Image, cfg: CONFIGCLASS) -> Image.Image:
+
+def custom_resize(img: Image.Image, cfg: CONFIGCLASS) -> Image.Image: # added to implement 70 to 90
interp = sample_discrete(cfg.rz_interp)
return TF.resize(img, cfg.loadSize, interpolation=rz_dict[interp])
-
def get_dataset(cfg: CONFIGCLASS):
dset_lst = []
for dataset in cfg.datasets:
diff --git a/core/utils1/eval.py b/core/utils1/eval.py
index b93d2f8..d896977 100644
--- a/core/utils1/eval.py
+++ b/core/utils1/eval.py
@@ -63,13 +63,15 @@ def validate(model: nn.Module, cfg: CONFIGCLASS):
tpr = f_acc # TPR is the accuracy on fake samples (class 1)
tnr = r_acc # TNR is the accuracy on real samples (class 0)
+
+ # added values for results - wandb integration
results = {
"ACC": acc,
"AP": ap,
"AUC": auc,
- "R_ACC": r_acc,
+ "R_ACC": r_acc,
"F_ACC": f_acc,
- "TPR": tpr,
- "TNR": tnr,
+ "TPR": tpr, # True Positive Rate / Recall
+ "TNR": tnr, # True Negative Rate / Specificity
}
return results
diff --git a/core/utils1/trainer.py b/core/utils1/trainer.py
index 6739243..99534d0 100644
--- a/core/utils1/trainer.py
+++ b/core/utils1/trainer.py
@@ -20,6 +20,7 @@ def __init__(self, cfg: CONFIGCLASS):
self.model:nn.Module
# self.model.to(self.device)
#self.model.load_state_dict(torch.load('./checkpoints/optical.pth'))
+ # removed self.model=nnModule.to(self.device) breaks the model assignment
self.optimizer: torch.optim.Optimizer
def save_networks(self, epoch: int):
From 26be75ba19ec658fc25ca8eada6533cc88981d99 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Fri, 28 Nov 2025 16:41:50 +0800
Subject: [PATCH 49/55] fix: Update Dockerfile to remove unnecessary scripts
and enhance dataset transformation
---
Dockerfile.gpu-alt | 2 +-
core/utils1/datasets.py | 3 +--
python-utils/download_data.py => download_data.py | 0
3 files changed, 2 insertions(+), 3 deletions(-)
rename python-utils/download_data.py => download_data.py (100%)
diff --git a/Dockerfile.gpu-alt b/Dockerfile.gpu-alt
index d63a70d..2b23046 100644
--- a/Dockerfile.gpu-alt
+++ b/Dockerfile.gpu-alt
@@ -32,7 +32,7 @@ COPY pyproject.toml ./
COPY README.md ./
COPY core/ ./core/
COPY networks/ ./networks/
-COPY train.py test.py demo.py download_data.py recreate_table_2.py prepare_data.py ./
+COPY train.py test.py demo.py download_data.py ./
# Create directories for data and checkpoints
RUN mkdir -p /app/data /app/checkpoints /app/raft_model
diff --git a/core/utils1/datasets.py b/core/utils1/datasets.py
index 273e495..cac43e8 100644
--- a/core/utils1/datasets.py
+++ b/core/utils1/datasets.py
@@ -57,9 +57,8 @@ def binary_dataset(root: str, cfg: CONFIGCLASS):
transforms.Compose(
[
rz_func,
- #change
+ transforms.Lambda(lambda img: blur_jpg_augment(img, cfg)), #change
crop_func,
- transforms.Lambda(lambda img: blur_jpg_augment(img, cfg)),
flip_func,
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
diff --git a/python-utils/download_data.py b/download_data.py
similarity index 100%
rename from python-utils/download_data.py
rename to download_data.py
From 5dbfa7928c4870856ddfeed8af93ba295e30db43 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Sat, 29 Nov 2025 02:02:00 +0800
Subject: [PATCH 50/55] fix: Update dataset names in DefaultConfigs to align
with test/train structure and remove trainer_optical.py
---
core/utils1/config.py | 4 +-
core/utils1/trainer_optical.py | 169 ---------------------------------
2 files changed, 2 insertions(+), 171 deletions(-)
delete mode 100644 core/utils1/trainer_optical.py
diff --git a/core/utils1/config.py b/core/utils1/config.py
index c8c9f3e..96cf963 100644
--- a/core/utils1/config.py
+++ b/core/utils1/config.py
@@ -10,8 +10,8 @@ class DefaultConfigs(ABC):
gpus = [0]
seed = 3407
arch = "resnet50"
- datasets = ["trainset_1"]
- datasets_test = ["val_set_1"]
+ datasets = ["trainset_1"] # Changed from trainset_1 to match test/train structure
+ datasets_test = ["val_set_1"] # Changed from val_set_1 to match test/val structure
mode = "binary"
class_bal = False
batch_size = 64 # RTX 3090 24GB can handle original batch size
diff --git a/core/utils1/trainer_optical.py b/core/utils1/trainer_optical.py
deleted file mode 100644
index 7a37fb3..0000000
--- a/core/utils1/trainer_optical.py
+++ /dev/null
@@ -1,169 +0,0 @@
-import os
-
-import torch
-import torch.nn as nn
-from torch.nn import init
-
-from core.utils1.config import CONFIGCLASS
-from core.utils1.utils import get_network
-from core.utils1.warmup import GradualWarmupScheduler
-
-
-class BaseModel(nn.Module):
- def __init__(self, cfg: CONFIGCLASS):
- super().__init__()
- self.cfg = cfg
- self.total_steps = 0
- self.isTrain = cfg.isTrain
- self.save_dir = cfg.ckpt_dir
- self.device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
- self.model:nn.Module
- # self.model.to(self.device)
- #self.model.load_state_dict(torch.load('./checkpoints/optical.pth'))
- self.optimizer: torch.optim.Optimizer
-
- def save_networks(self, epoch: int):
- save_filename = f"model_epoch_{epoch}.pth"
- save_path = os.path.join(self.save_dir, save_filename)
-
- # serialize model and optimizer to dict
- state_dict = {
- "model": self.model.state_dict(),
- "optimizer": self.optimizer.state_dict(),
- "total_steps": self.total_steps,
- }
-
- torch.save(state_dict, save_path)
-
- # load models from the disk
- def load_networks(self, epoch: int):
- load_filename = f"model_epoch_{epoch}.pth"
- load_path = os.path.join(self.save_dir, load_filename)
-
- if epoch==0:
- # load_filename = f"lsun_adm.pth"
- load_path="checkpoints/optical.pth"
- print("loading optical path")
- else :
- print(f"loading the model from {load_path}")
-
- # print(f"loading the model from {load_path}")
-
- # if you are using PyTorch newer than 0.4 (e.g., built from
- # GitHub source), you can remove str() on self.device
- state_dict = torch.load(load_path, map_location=self.device)
- if hasattr(state_dict, "_metadata"):
- del state_dict._metadata
-
- self.model.load_state_dict(state_dict["model"])
- self.total_steps = state_dict["total_steps"]
-
- if self.isTrain and not self.cfg.new_optim:
- self.optimizer.load_state_dict(state_dict["optimizer"])
- # move optimizer state to GPU
- for state in self.optimizer.state.values():
- for k, v in state.items():
- if torch.is_tensor(v):
- state[k] = v.to(self.device)
-
- for g in self.optimizer.param_groups:
- g["lr"] = self.cfg.lr
-
- def eval(self):
- self.model.eval()
-
- def test(self):
- with torch.no_grad():
- self.forward()
-
-
-def init_weights(net: nn.Module, init_type="normal", gain=0.02):
- def init_func(m: nn.Module):
- classname = m.__class__.__name__
- if hasattr(m, "weight") and (classname.find("Conv") != -1 or classname.find("Linear") != -1):
- if init_type == "normal":
- init.normal_(m.weight.data, 0.0, gain)
- elif init_type == "xavier":
- init.xavier_normal_(m.weight.data, gain=gain)
- elif init_type == "kaiming":
- init.kaiming_normal_(m.weight.data, a=0, mode="fan_in")
- elif init_type == "orthogonal":
- init.orthogonal_(m.weight.data, gain=gain)
- else:
- raise NotImplementedError(f"initialization method [{init_type}] is not implemented")
- if hasattr(m, "bias") and m.bias is not None:
- init.constant_(m.bias.data, 0.0)
- elif classname.find("BatchNorm2d") != -1:
- init.normal_(m.weight.data, 1.0, gain)
- init.constant_(m.bias.data, 0.0)
-
- print(f"initialize network with {init_type}")
- net.apply(init_func)
-
-
-class Trainer(BaseModel):
- def name(self):
- return "Trainer"
-
- def __init__(self, cfg: CONFIGCLASS):
- super().__init__(cfg)
- self.arch = cfg.arch
- self.model = get_network(self.arch, cfg.isTrain, cfg.continue_train, cfg.init_gain, cfg.pretrained)
-
- self.loss_fn = nn.BCEWithLogitsLoss()
- # initialize optimizers
- if cfg.optim == "adam":
- self.optimizer = torch.optim.Adam(self.model.parameters(), lr=cfg.lr, betas=(cfg.beta1, 0.999))
- elif cfg.optim == "sgd":
- self.optimizer = torch.optim.SGD(self.model.parameters(), lr=cfg.lr, momentum=0.9, weight_decay=5e-4)
- else:
- raise ValueError("optim should be [adam, sgd]")
- if cfg.warmup:
- scheduler_cosine = torch.optim.lr_scheduler.CosineAnnealingLR(
- self.optimizer, cfg.nepoch - cfg.warmup_epoch, eta_min=1e-6
- )
- self.scheduler = GradualWarmupScheduler(
- self.optimizer, multiplier=1, total_epoch=cfg.warmup_epoch, after_scheduler=scheduler_cosine
- )
- self.scheduler.step()
- if cfg.continue_train:
- self.load_networks(cfg.epoch)
- self.model.to(self.device)
-
- # OPTICAL FLOW: Force load pretrained optical.pth checkpoint
- print("Loading pretrained optical flow checkpoint...")
- load_path = 'checkpoints/optical.pth'
- state_dict = torch.load(load_path, map_location=self.device)
- self.model.load_state_dict(state_dict["model"])
- print(f"✓ Loaded optical flow checkpoint from {load_path}")
-
-
-
- def adjust_learning_rate(self, min_lr=1e-6):
- for param_group in self.optimizer.param_groups:
- param_group["lr"] /= 10.0
- if param_group["lr"] < min_lr:
- return False
- return True
-
- def set_input(self, input):
- img, label, meta = input if len(input) == 3 else (input[0], input[1], {})
- self.input = img.to(self.device)
- self.label = label.to(self.device).float()
- for k in meta.keys():
- if isinstance(meta[k], torch.Tensor):
- meta[k] = meta[k].to(self.device)
- self.meta = meta
-
- def forward(self):
- self.output = self.model(self.input, self.meta)
-
- def get_loss(self):
- return self.loss_fn(self.output.squeeze(1), self.label)
-
- def optimize_parameters(self):
- self.forward()
- self.loss = self.loss_fn(self.output.squeeze(1), self.label)
- self.optimizer.zero_grad()
- self.loss.backward()
- self.optimizer.step()
From 14677f5db8f47905735b38d49aa4ee1bfe779262 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Fri, 5 Dec 2025 10:42:38 +0800
Subject: [PATCH 51/55] feat: Add comprehensive Table 2 recreation and
evaluation framework, data preparation and utility scripts, and a GUI, while
updating training and testing modules.
---
batch_prepare.py | 48 +++++
batch_prepare_all.py | 200 ++++++++++++++++++
batch_prepare_i2v.py | 199 ++++++++++++++++++
clean_extra_frames.py | 81 ++++++++
compile_table2_data2.py | 70 +++++++
compile_table2_data3.py | 70 +++++++
extract_test_data.py | 160 +++++++++++++++
gui_app.py | 364 +++++++++++++++++++++++++++++++++
prepare_real_data.py | 175 ++++++++++++++++
python-utils/prepare_data.py | 9 +-
recreate_table2_data2.py | 309 ++++++++++++++++++++++++++++
recreate_table2_data3.py | 309 ++++++++++++++++++++++++++++
recreate_table2_final.py | 382 +++++++++++++++++++++++++++++++++++
run_gui.bat | 5 +
setup_test_data.py | 254 +++++++++++++++++++++++
test.py | 4 +-
test_single_stream.py | 333 ++++++++++++++++++++++++++++++
train.py | 23 +++
18 files changed, 2989 insertions(+), 6 deletions(-)
create mode 100644 batch_prepare.py
create mode 100644 batch_prepare_all.py
create mode 100644 batch_prepare_i2v.py
create mode 100644 clean_extra_frames.py
create mode 100644 compile_table2_data2.py
create mode 100644 compile_table2_data3.py
create mode 100644 extract_test_data.py
create mode 100644 gui_app.py
create mode 100644 prepare_real_data.py
create mode 100644 recreate_table2_data2.py
create mode 100644 recreate_table2_data3.py
create mode 100644 recreate_table2_final.py
create mode 100644 run_gui.bat
create mode 100644 setup_test_data.py
create mode 100644 test_single_stream.py
diff --git a/batch_prepare.py b/batch_prepare.py
new file mode 100644
index 0000000..11485f3
--- /dev/null
+++ b/batch_prepare.py
@@ -0,0 +1,48 @@
+"""
+Batch Data Preparation Script
+Automatically finds all *_mp4 folders in data/test/T2V and processes them.
+"""
+import os
+import glob
+import subprocess
+from pathlib import Path
+
+# Configuration
+SOURCE_ROOT = "data/test/T2V"
+RGB_OUTPUT_ROOT = "data/test/original/T2V"
+FLOW_OUTPUT_ROOT = "data/test/T2V"
+RAFT_MODEL = "raft-model/raft-things.pth"
+MAX_VIDEOS = 100 # Limit to 100 videos per dataset for faster processing
+
+def main():
+ # Find all folders ending in _mp4
+ source_folders = glob.glob(os.path.join(SOURCE_ROOT, "*_mp4"))
+
+ print(f"Found {len(source_folders)} datasets to process: {[os.path.basename(f) for f in source_folders]}")
+
+ for source_dir in source_folders:
+ dataset_name = os.path.basename(source_dir).replace("_mp4", "")
+
+ print(f"\n{'='*60}")
+ print(f"Processing: {dataset_name}")
+ print(f"{'='*60}")
+
+ # Define output paths
+ rgb_out = os.path.join(RGB_OUTPUT_ROOT, dataset_name)
+ flow_out = os.path.join(FLOW_OUTPUT_ROOT, dataset_name)
+
+ cmd = [
+ "python", "prepare_data.py",
+ "--source_dir", source_dir,
+ "--output_rgb_dir", rgb_out,
+ "--output_flow_dir", flow_out,
+ "--model", RAFT_MODEL,
+ "--label", "1_fake", # Assuming these are all generated video folders
+ "--max_videos", str(MAX_VIDEOS)
+ ]
+
+ print(f"Command: {' '.join(cmd)}")
+ subprocess.run(cmd)
+
+if __name__ == "__main__":
+ main()
diff --git a/batch_prepare_all.py b/batch_prepare_all.py
new file mode 100644
index 0000000..fa212fb
--- /dev/null
+++ b/batch_prepare_all.py
@@ -0,0 +1,200 @@
+"""
+Batch Data Preparation Script for All T2V Models
+Automatically finds all *_mp4 folders in data/test/T2V and processes them.
+"""
+import os
+import glob
+import cv2
+import numpy as np
+import torch
+import sys
+from PIL import Image
+from tqdm import tqdm
+from pathlib import Path
+
+# Add core to path for RAFT
+sys.path.append('core')
+from raft import RAFT
+from utils import flow_viz
+from utils.utils import InputPadder
+
+DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
+
+# Configuration
+SOURCE_ROOT = "data/test/T2V"
+RGB_OUTPUT_ROOT = "data/test/original/T2V"
+FLOW_OUTPUT_ROOT = "data/test/T2V"
+RAFT_MODEL = "raft_model/raft-things.pth"
+MAX_VIDEOS = 500 # Limit to 500 videos per dataset
+
+def load_image(imfile):
+ """Load image and convert to tensor"""
+ img = Image.open(imfile)
+
+ # Resize if too large to prevent OOM
+ max_dim = 1024
+ if max(img.size) > max_dim:
+ scale = max_dim / max(img.size)
+ new_size = (int(img.size[0] * scale), int(img.size[1] * scale))
+ img = img.resize(new_size, Image.BILINEAR)
+
+ img = np.array(img).astype(np.uint8)
+ img = torch.from_numpy(img).permute(2, 0, 1).float()
+ return img[None].to(DEVICE)
+
+def save_flow(img, flo, output_path):
+ """Save optical flow as image"""
+ img = img[0].permute(1, 2, 0).cpu().numpy()
+ flo = flo[0].permute(1, 2, 0).cpu().numpy()
+ flo = flow_viz.flow_to_image(flo)
+ cv2.imwrite(output_path, flo)
+
+def extract_frames(video_path, output_folder, max_frames=95):
+ """Extract frames from video (limit to max_frames)"""
+ os.makedirs(output_folder, exist_ok=True)
+
+ # Check if already extracted
+ existing = glob.glob(os.path.join(output_folder, "*.png"))
+ if len(existing) > 0:
+ return sorted(existing)[:max_frames]
+
+ cap = cv2.VideoCapture(video_path)
+ frame_count = 0
+ frames = []
+
+ while cap.isOpened() and frame_count < max_frames:
+ ret, frame = cap.read()
+ if not ret:
+ break
+
+ frame_path = os.path.join(output_folder, f"{frame_count:08d}.png")
+ cv2.imwrite(frame_path, frame)
+ frames.append(frame_path)
+ frame_count += 1
+
+ cap.release()
+ return frames
+
+def generate_optical_flow(model, frames, output_dir):
+ """Generate optical flow for frame sequence"""
+ os.makedirs(output_dir, exist_ok=True)
+
+ with torch.no_grad():
+ for i, (imfile1, imfile2) in enumerate(tqdm(zip(frames[:-1], frames[1:]),
+ total=len(frames)-1,
+ desc=" Generating flow")):
+ flow_path = os.path.join(output_dir, f"{i:08d}.png")
+
+ if os.path.exists(flow_path):
+ continue
+
+ image1 = load_image(imfile1)
+ image2 = load_image(imfile2)
+
+ padder = InputPadder(image1.shape)
+ image1, image2 = padder.pad(image1, image2)
+
+ flow_low, flow_up = model(image1, image2, iters=20, test_mode=True)
+ save_flow(image1, flow_up, flow_path)
+
+def process_dataset(dataset_name, source_dir, model):
+ """Process a single T2V dataset"""
+ print(f"\n{'='*60}")
+ print(f"Processing: {dataset_name}")
+ print(f"{'='*60}")
+
+ # Get all videos
+ videos = glob.glob(os.path.join(source_dir, "*.mp4")) + \
+ glob.glob(os.path.join(source_dir, "*.avi")) + \
+ glob.glob(os.path.join(source_dir, "*.mov"))
+
+ # Apply limit
+ if len(videos) > MAX_VIDEOS:
+ videos = videos[:MAX_VIDEOS]
+ print(f"Limited to {MAX_VIDEOS} videos")
+
+ print(f"Found {len(videos)} videos to process")
+
+ # Output directories
+ rgb_base = os.path.join(RGB_OUTPUT_ROOT, dataset_name, "1_fake")
+ flow_base = os.path.join(FLOW_OUTPUT_ROOT, dataset_name, "1_fake")
+
+ # Process each video
+ for idx, video_path in enumerate(videos, 1):
+ video_name = Path(video_path).stem
+ print(f"\n[{idx}/{len(videos)}] {video_name}")
+
+ rgb_out = os.path.join(rgb_base, video_name)
+ flow_out = os.path.join(flow_base, video_name)
+
+ # Extract frames
+ print(" Extracting frames...")
+ frames = extract_frames(video_path, rgb_out)
+
+ if len(frames) < 2:
+ print(" ⚠ Skipping (too few frames)")
+ continue
+
+ # Generate optical flow
+ generate_optical_flow(model, frames, flow_out)
+
+ print(f"\n✅ Completed {dataset_name}")
+
+def main():
+ # Find all T2V dataset folders
+ source_folders = glob.glob(os.path.join(SOURCE_ROOT, "*_mp4"))
+
+ if not source_folders:
+ print(f"❌ No *_mp4 folders found in {SOURCE_ROOT}")
+ return
+
+ print("="*60)
+ print("BATCH T2V DATA PREPARATION")
+ print("="*60)
+ print(f"\nFound {len(source_folders)} dataset(s):")
+ for folder in source_folders:
+ dataset_name = os.path.basename(folder).replace("_mp4", "")
+ print(f" 📁 {dataset_name}")
+
+ print(f"\nConfiguration:")
+ print(f" • Max videos per dataset: {MAX_VIDEOS}")
+ print(f" • Max frames per video: 95 (RGB) / 94 (Flow)")
+ print(f" • RAFT model: {RAFT_MODEL}")
+ print("="*60)
+
+ # Load RAFT model
+ print("\nLoading RAFT model...")
+ import argparse
+ raft_args = argparse.Namespace(
+ model=RAFT_MODEL,
+ small=False,
+ mixed_precision=False,
+ alternate_corr=False,
+ dropout=0
+ )
+
+ model = torch.nn.DataParallel(RAFT(raft_args))
+ model.load_state_dict(torch.load(RAFT_MODEL, map_location=torch.device(DEVICE)))
+ model = model.module
+ model.to(DEVICE)
+ model.eval()
+ print("✓ RAFT model loaded")
+
+ # Process each dataset
+ for idx, source_dir in enumerate(source_folders, 1):
+ dataset_name = os.path.basename(source_dir).replace("_mp4", "")
+
+ try:
+ process_dataset(dataset_name, source_dir, model)
+ except Exception as e:
+ print(f"\n❌ Error processing {dataset_name}: {e}")
+ response = input("Continue? (Y/n): ").strip().lower()
+ if response == 'n':
+ break
+
+ print("\n" + "="*60)
+ print("✅ BATCH PROCESSING COMPLETE!")
+ print("="*60)
+
+if __name__ == "__main__":
+ main()
diff --git a/batch_prepare_i2v.py b/batch_prepare_i2v.py
new file mode 100644
index 0000000..d97ba8a
--- /dev/null
+++ b/batch_prepare_i2v.py
@@ -0,0 +1,199 @@
+"""
+Batch Data Preparation Script for I2V Models
+Processes all *_mp4 folders in data/I2V
+"""
+import os
+import glob
+import cv2
+import numpy as np
+import torch
+import sys
+from PIL import Image
+from tqdm import tqdm
+from pathlib import Path
+
+# Add core to path for RAFT
+sys.path.append('core')
+from raft import RAFT
+from utils import flow_viz
+from utils.utils import InputPadder
+
+DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
+
+# Configuration
+SOURCE_ROOT = "data/I2V"
+RGB_OUTPUT_ROOT = "data/test/original/I2V"
+FLOW_OUTPUT_ROOT = "data/test/I2V"
+RAFT_MODEL = "raft_model/raft-things.pth"
+MAX_VIDEOS = 200 # Limit to 200 videos per dataset
+
+def load_image(imfile):
+ """Load image and convert to tensor"""
+ img = Image.open(imfile)
+
+ # Resize if too large to prevent OOM
+ max_dim = 1024
+ if max(img.size) > max_dim:
+ scale = max_dim / max(img.size)
+ new_size = (int(img.size[0] * scale), int(img.size[1] * scale))
+ img = img.resize(new_size, Image.BILINEAR)
+
+ img = np.array(img).astype(np.uint8)
+ img = torch.from_numpy(img).permute(2, 0, 1).float()
+ return img[None].to(DEVICE)
+
+def save_flow(img, flo, output_path):
+ """Save optical flow as image"""
+ img = img[0].permute(1, 2, 0).cpu().numpy()
+ flo = flo[0].permute(1, 2, 0).cpu().numpy()
+ flo = flow_viz.flow_to_image(flo)
+ cv2.imwrite(output_path, flo)
+
+def extract_frames(video_path, output_folder, max_frames=95):
+ """Extract frames from video (limit to max_frames)"""
+ os.makedirs(output_folder, exist_ok=True)
+
+ # Check if already extracted
+ existing = glob.glob(os.path.join(output_folder, "*.png"))
+ if len(existing) > 0:
+ return sorted(existing)[:max_frames] # Return only up to max_frames
+
+ cap = cv2.VideoCapture(video_path)
+ frame_count = 0
+ frames = []
+
+ while cap.isOpened() and frame_count < max_frames:
+ ret, frame = cap.read()
+ if not ret:
+ break
+
+ frame_path = os.path.join(output_folder, f"{frame_count:08d}.png")
+ cv2.imwrite(frame_path, frame)
+ frames.append(frame_path)
+ frame_count += 1
+
+ cap.release()
+ return frames
+
+def generate_optical_flow(model, frames, output_dir):
+ """Generate optical flow for frame sequence"""
+ os.makedirs(output_dir, exist_ok=True)
+
+ with torch.no_grad():
+ for i, (imfile1, imfile2) in enumerate(tqdm(zip(frames[:-1], frames[1:]),
+ total=len(frames)-1,
+ desc=" Generating flow")):
+ flow_path = os.path.join(output_dir, f"{i:08d}.png")
+
+ if os.path.exists(flow_path):
+ continue
+
+ image1 = load_image(imfile1)
+ image2 = load_image(imfile2)
+
+ padder = InputPadder(image1.shape)
+ image1, image2 = padder.pad(image1, image2)
+
+ flow_low, flow_up = model(image1, image2, iters=20, test_mode=True)
+ save_flow(image1, flow_up, flow_path)
+
+def process_dataset(dataset_name, source_dir, model):
+ """Process a single I2V dataset"""
+ print(f"\n{'='*60}")
+ print(f"Processing: {dataset_name}")
+ print(f"{'='*60}")
+
+ # Get all videos
+ videos = glob.glob(os.path.join(source_dir, "*.mp4")) + \
+ glob.glob(os.path.join(source_dir, "*.avi")) + \
+ glob.glob(os.path.join(source_dir, "*.mov"))
+
+ # Apply limit
+ if len(videos) > MAX_VIDEOS:
+ videos = videos[:MAX_VIDEOS]
+ print(f"Limited to {MAX_VIDEOS} videos")
+
+ print(f"Found {len(videos)} videos to process")
+
+ # Output directories
+ rgb_base = os.path.join(RGB_OUTPUT_ROOT, dataset_name, "1_fake")
+ flow_base = os.path.join(FLOW_OUTPUT_ROOT, dataset_name, "1_fake")
+
+ # Process each video
+ for idx, video_path in enumerate(videos, 1):
+ video_name = Path(video_path).stem
+ print(f"\n[{idx}/{len(videos)}] {video_name}")
+
+ rgb_out = os.path.join(rgb_base, video_name)
+ flow_out = os.path.join(flow_base, video_name)
+
+ # Extract frames
+ print(" Extracting frames...")
+ frames = extract_frames(video_path, rgb_out)
+
+ if len(frames) < 2:
+ print(" ⚠ Skipping (too few frames)")
+ continue
+
+ # Generate optical flow
+ generate_optical_flow(model, frames, flow_out)
+
+ print(f"\n✅ Completed {dataset_name}")
+
+def main():
+ # Find all I2V dataset folders
+ source_folders = glob.glob(os.path.join(SOURCE_ROOT, "*_mp4"))
+
+ if not source_folders:
+ print(f"❌ No *_mp4 folders found in {SOURCE_ROOT}")
+ return
+
+ print("="*60)
+ print("BATCH I2V DATA PREPARATION")
+ print("="*60)
+ print(f"\nFound {len(source_folders)} dataset(s):")
+ for folder in source_folders:
+ dataset_name = os.path.basename(folder).replace("_mp4", "")
+ print(f" 📁 {dataset_name}")
+
+ print(f"\nConfiguration:")
+ print(f" • Max videos per dataset: {MAX_VIDEOS}")
+ print(f" • RAFT model: {RAFT_MODEL}")
+ print("="*60)
+
+ # Load RAFT model
+ print("\nLoading RAFT model...")
+ import argparse
+ raft_args = argparse.Namespace(
+ model=RAFT_MODEL,
+ small=False,
+ mixed_precision=False,
+ alternate_corr=False,
+ dropout=0
+ )
+
+ model = torch.nn.DataParallel(RAFT(raft_args))
+ model.load_state_dict(torch.load(RAFT_MODEL, map_location=torch.device(DEVICE)))
+ model = model.module
+ model.to(DEVICE)
+ model.eval()
+ print("✓ RAFT model loaded")
+
+ # Process each dataset
+ for idx, source_dir in enumerate(source_folders, 1):
+ dataset_name = os.path.basename(source_dir).replace("_mp4", "")
+
+ try:
+ process_dataset(dataset_name, source_dir, model)
+ except Exception as e:
+ print(f"\n❌ Error processing {dataset_name}: {e}")
+ response = input("Continue? (Y/n): ").strip().lower()
+ if response == 'n':
+ break
+
+ print("\n" + "="*60)
+ print("✅ BATCH PROCESSING COMPLETE!")
+ print("="*60)
+
+if __name__ == "__main__":
+ main()
diff --git a/clean_extra_frames.py b/clean_extra_frames.py
new file mode 100644
index 0000000..8109291
--- /dev/null
+++ b/clean_extra_frames.py
@@ -0,0 +1,81 @@
+import os
+import glob
+import argparse
+
+def clean_extra_frames(base_dir, max_frame_index=94):
+ print(f"Cleaning frames with index > {max_frame_index} in {base_dir}")
+
+ # Datasets to check
+ datasets = ["moonvalley", "videocraft", "pika", "neverends"]
+
+ total_deleted = 0
+
+ for dataset in datasets:
+ target_dir = os.path.join(base_dir, dataset, "0_real")
+
+ if not os.path.exists(target_dir):
+ print(f"Skipping {target_dir} (not found)")
+ continue
+
+ print(f"Scanning {target_dir}...")
+
+ # Check if it's flat files or folders
+ # Based on previous ls, it seems to be flat files for 0_real in some contexts,
+ # but prepare_real_data.py copies folders: shutil.copytree(temp_flow_dir, flow_dest)
+ # Let's handle both cases.
+
+ items = os.listdir(target_dir)
+ for item in items:
+ item_path = os.path.join(target_dir, item)
+
+ if os.path.isdir(item_path):
+ # It's a video folder
+ video_name = item
+ frames = glob.glob(os.path.join(item_path, "*"))
+ for frame in frames:
+ if should_delete(frame, max_frame_index):
+ os.remove(frame)
+ total_deleted += 1
+ else:
+ # It's a flat file
+ if should_delete(item_path, max_frame_index):
+ os.remove(item_path)
+ total_deleted += 1
+
+ print(f"\n✓ Cleanup complete. Deleted {total_deleted} extra frames.")
+
+def should_delete(filepath, max_index):
+ filename = os.path.basename(filepath)
+ name_part = os.path.splitext(filename)[0]
+
+ # Case 1: Filename is just a number (e.g. 00000094.jpg)
+ if name_part.isdigit():
+ idx = int(name_part)
+ if idx > 90:
+ print(f" Checking {filename}: Index {idx} > {max_index}? {idx > max_index}")
+ if idx > max_index:
+ return True
+
+ # Case 2: Filename has underscores (e.g. video_00000094.jpg)
+ parts = name_part.split('_')
+ if len(parts) > 1:
+ last_part = parts[-1]
+ if last_part.isdigit():
+ idx = int(last_part)
+ if idx > 90:
+ print(f" Checking {filename}: Index {idx} > {max_index}? {idx > max_index}")
+ if idx > max_index:
+ return True
+
+ return False
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--dry-run", action="store_true", help="Print what would be deleted without deleting")
+ args = parser.parse_args()
+
+ # Optical Flow path (Limit to 94 frames -> max index 93)
+ clean_extra_frames("data/test/T2V", max_frame_index=93)
+
+ # RGB path (Limit to 95 frames -> max index 94)
+ clean_extra_frames("data/test/original/T2V", max_frame_index=94)
diff --git a/compile_table2_data2.py b/compile_table2_data2.py
new file mode 100644
index 0000000..b0b8b19
--- /dev/null
+++ b/compile_table2_data2.py
@@ -0,0 +1,70 @@
+ """
+ Quick script to compile Table 2 from existing CSV files in data2/results/table2/
+ """
+
+ import pandas as pd
+ from pathlib import Path
+ from sklearn.metrics import accuracy_score, roc_auc_score, average_precision_score
+
+ RESULTS_DIR = Path("data2/results/table2")
+
+ DATASETS = ["emu", "hotshot", "sora"]
+ VARIANTS = ["S_spatial", "S_optical", "AIGVDet"]
+
+ def get_metrics_from_csv(csv_path):
+ """Calculate metrics from CSV file"""
+ try:
+ df = pd.read_csv(csv_path)
+
+ if len(df) == 0:
+ return None
+
+ y_true = df['flag'].values
+ y_pred = df['pro'].values
+
+ acc = accuracy_score(y_true, (y_pred >= 0.5).astype(int))
+ auc = roc_auc_score(y_true, y_pred) if len(set(y_true)) > 1 else 0.0
+ ap = average_precision_score(y_true, y_pred) if len(set(y_true)) > 1 else 0.0
+
+ return {'ACC': acc, 'AUC': auc, 'AP': ap}
+ except Exception as e:
+ print(f"Error reading {csv_path}: {e}")
+ return None
+
+ # Compile results
+ rows = []
+ for dataset in DATASETS:
+ row = {"Dataset": dataset}
+
+ for variant in VARIANTS:
+ csv_file = RESULTS_DIR / f"{dataset}_{variant}_video.csv"
+
+ if csv_file.exists():
+ metrics = get_metrics_from_csv(csv_file)
+ if metrics:
+ acc = metrics['ACC'] * 100
+ auc = metrics['AUC'] * 100
+ row[variant] = f"{acc:.1f}/{auc:.1f}"
+ print(f"✓ {dataset} - {variant}: ACC={acc:.1f}%, AUC={auc:.1f}%")
+ else:
+ row[variant] = "N/A"
+ print(f"✗ {dataset} - {variant}: No data")
+ else:
+ row[variant] = "N/A"
+ print(f"✗ {dataset} - {variant}: File not found")
+
+ rows.append(row)
+
+ # Create and save table
+ df = pd.DataFrame(rows)
+
+ output_file = RESULTS_DIR / "table2_data2_compiled.csv"
+ df.to_csv(output_file, index=False)
+
+ print("\n" + "="*60)
+ print("TABLE 2 (DATA2)")
+ print("="*60)
+ print(df.to_string(index=False))
+ print("\n" + "="*60)
+ print(f"✓ Saved to: {output_file}")
+ print("="*60)
diff --git a/compile_table2_data3.py b/compile_table2_data3.py
new file mode 100644
index 0000000..b3656c2
--- /dev/null
+++ b/compile_table2_data3.py
@@ -0,0 +1,70 @@
+"""
+Quick script to compile Table 2 from existing CSV files in data3/results/table2/
+"""
+
+import pandas as pd
+from pathlib import Path
+from sklearn.metrics import accuracy_score, roc_auc_score, average_precision_score
+
+RESULTS_DIR = Path("data3/results/table2")
+
+DATASETS = ["moonvalley", "pika", "neverends"]
+VARIANTS = ["S_spatial", "S_optical", "AIGVDet"]
+
+def get_metrics_from_csv(csv_path):
+ """Calculate metrics from CSV file"""
+ try:
+ df = pd.read_csv(csv_path)
+
+ if len(df) == 0:
+ return None
+
+ y_true = df['flag'].values
+ y_pred = df['pro'].values
+
+ acc = accuracy_score(y_true, (y_pred >= 0.5).astype(int))
+ auc = roc_auc_score(y_true, y_pred) if len(set(y_true)) > 1 else 0.0
+ ap = average_precision_score(y_true, y_pred) if len(set(y_true)) > 1 else 0.0
+
+ return {'ACC': acc, 'AUC': auc, 'AP': ap}
+ except Exception as e:
+ print(f"Error reading {csv_path}: {e}")
+ return None
+
+# Compile results
+rows = []
+for dataset in DATASETS:
+ row = {"Dataset": dataset}
+
+ for variant in VARIANTS:
+ csv_file = RESULTS_DIR / f"{dataset}_{variant}_video.csv"
+
+ if csv_file.exists():
+ metrics = get_metrics_from_csv(csv_file)
+ if metrics:
+ acc = metrics['ACC'] * 100
+ auc = metrics['AUC'] * 100
+ row[variant] = f"{acc:.1f}/{auc:.1f}"
+ print(f"✓ {dataset} - {variant}: ACC={acc:.1f}%, AUC={auc:.1f}%")
+ else:
+ row[variant] = "N/A"
+ print(f"✗ {dataset} - {variant}: No data")
+ else:
+ row[variant] = "N/A"
+ print(f"✗ {dataset} - {variant}: File not found")
+
+ rows.append(row)
+
+# Create and save table
+df = pd.DataFrame(rows)
+
+output_file = RESULTS_DIR / "table2_data3_i2v_compiled.csv"
+df.to_csv(output_file, index=False)
+
+print("\n" + "="*60)
+print("TABLE 2 (DATA3 - I2V)")
+print("="*60)
+print(df.to_string(index=False))
+print("\n" + "="*60)
+print(f"✓ Saved to: {output_file}")
+print("="*60)
diff --git a/extract_test_data.py b/extract_test_data.py
new file mode 100644
index 0000000..508d146
--- /dev/null
+++ b/extract_test_data.py
@@ -0,0 +1,160 @@
+"""
+Manual extraction script for test data
+Use this if automatic download fails due to Google Drive rate limits
+
+INSTRUCTIONS:
+1. Manually download the test folder from Google Drive
+2. Place all zip files in: data/temp_download/
+3. Run this script to extract everything
+
+Expected zip files:
+- moonvalley-optical.zip
+- videocraft-optical.zip
+- pika-optical.zip
+- neverends-optical.zip
+- moonvalley-rgb.zip
+- videocraft-rgb.zip
+- pika-rgb.zip
+- neverends-rgb.zip
+"""
+
+import zipfile
+from pathlib import Path
+import shutil
+
+# Paths
+DATA_DIR = Path("data")
+TEST_DIR = DATA_DIR / "test"
+TEMP_DIR = DATA_DIR / "temp_download"
+
+def extract_zip(zip_path, extract_to):
+ """Extract a single zip file"""
+ print(f" Extracting: {zip_path.name}")
+ print(f" → {extract_to}")
+ try:
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
+ zip_ref.extractall(extract_to)
+ print(f" ✓ Done")
+ return True
+ except Exception as e:
+ print(f" ❌ Error: {e}")
+ return False
+
+def main():
+ print("="*80)
+ print("MANUAL TEST DATA EXTRACTION")
+ print("="*80)
+
+ # Check if temp directory exists
+ if not TEMP_DIR.exists():
+ print(f"\n❌ Directory not found: {TEMP_DIR}")
+ print(f"\nPlease create it and place your zip files there:")
+ print(f" mkdir {TEMP_DIR}")
+ return
+
+ # Find all zip files
+ all_zips = list(TEMP_DIR.rglob("*.zip"))
+
+ if not all_zips:
+ print(f"\n❌ No zip files found in: {TEMP_DIR}")
+ print("\nPlease download the following files from Google Drive:")
+ print(" https://drive.google.com/drive/folders/1D1jm1_HCu0Nv21NVjuyL1CB5gF5sy0hx")
+ print(f"\nAnd place them in: {TEMP_DIR.absolute()}")
+ return
+
+ print(f"\n✓ Found {len(all_zips)} zip files")
+ for z in all_zips:
+ print(f" - {z.name}")
+
+ # Create target directories
+ optical_base = TEST_DIR / "T2V"
+ rgb_base = TEST_DIR / "original" / "T2V"
+ optical_base.mkdir(parents=True, exist_ok=True)
+ rgb_base.mkdir(parents=True, exist_ok=True)
+
+ # Extract optical flow data
+ print(f"\n{'='*80}")
+ print("EXTRACTING OPTICAL FLOW DATA")
+ print("="*80)
+
+ optical_zips = [z for z in all_zips if "-optical" in z.name.lower()]
+ for zip_file in optical_zips:
+ dataset_name = zip_file.stem.replace("-optical", "").replace("-Optical", "")
+ target_dir = optical_base / dataset_name
+ target_dir.mkdir(parents=True, exist_ok=True)
+ extract_zip(zip_file, target_dir)
+
+ # Extract RGB data
+ print(f"\n{'='*80}")
+ print("EXTRACTING RGB DATA")
+ print("="*80)
+
+ rgb_zips = [z for z in all_zips if "-rgb" in z.name.lower()]
+ for zip_file in rgb_zips:
+ dataset_name = zip_file.stem.replace("-rgb", "").replace("-RGB", "")
+ target_dir = rgb_base / dataset_name
+ target_dir.mkdir(parents=True, exist_ok=True)
+ extract_zip(zip_file, target_dir)
+
+ # Verify structure
+ print(f"\n{'='*80}")
+ print("VERIFYING STRUCTURE")
+ print("="*80)
+
+ datasets = ["moonvalley", "videocraft", "pika", "neverends"]
+ all_good = True
+
+ print("\nOptical flow:")
+ for dataset in datasets:
+ path = TEST_DIR / "T2V" / dataset
+ if path.exists():
+ real = (path / "0_real").exists()
+ fake = (path / "1_fake").exists()
+ status = f"[Real: {'✓' if real else '✗'}, Fake: {'✓' if fake else '✗'}]"
+ print(f" {'✓' if (real and fake) else '⚠️ '} {dataset}: {status}")
+ if not (real and fake):
+ all_good = False
+ else:
+ print(f" ❌ {dataset}: NOT FOUND")
+ all_good = False
+
+ print("\nRGB:")
+ for dataset in datasets:
+ path = TEST_DIR / "original" / "T2V" / dataset
+ if path.exists():
+ real = (path / "0_real").exists()
+ fake = (path / "1_fake").exists()
+ status = f"[Real: {'✓' if real else '✗'}, Fake: {'✓' if fake else '✗'}]"
+ print(f" {'✓' if (real and fake) else '⚠️ '} {dataset}: {status}")
+ if not (real and fake):
+ all_good = False
+ else:
+ print(f" ❌ {dataset}: NOT FOUND")
+ all_good = False
+
+ # Cleanup
+ print(f"\n{'='*80}")
+ cleanup = input("\nRemove temporary zip files? (y/n): ").strip().lower()
+ if cleanup == 'y':
+ try:
+ shutil.rmtree(TEMP_DIR)
+ print(f"✓ Removed: {TEMP_DIR}")
+ except Exception as e:
+ print(f"⚠️ Could not remove: {e}")
+
+ # Final message
+ print(f"\n{'='*80}")
+ if all_good:
+ print("✓ EXTRACTION COMPLETE!")
+ print("="*80)
+ print("\nAll test data is ready!")
+ print("\nNext step:")
+ print(" python recreate_table2_final.py")
+ else:
+ print("⚠️ EXTRACTION COMPLETED WITH WARNINGS")
+ print("="*80)
+ print("\nSome data may be missing. Check the warnings above.")
+ print("="*80)
+
+if __name__ == "__main__":
+ main()
diff --git a/gui_app.py b/gui_app.py
new file mode 100644
index 0000000..373b032
--- /dev/null
+++ b/gui_app.py
@@ -0,0 +1,364 @@
+import streamlit as st
+import sys
+import os
+import time
+import torch
+import numpy as np
+import cv2
+import glob
+import argparse
+import tempfile
+import shutil
+from PIL import Image
+import torchvision.transforms as transforms
+import torchvision.transforms.functional as TF
+from natsort import natsorted
+
+# Add core to path
+current_dir = os.path.dirname(os.path.abspath(__file__))
+sys.path.append(os.path.join(current_dir, 'core'))
+
+try:
+ from core.raft import RAFT
+ from core.utils import flow_viz
+ from core.utils.utils import InputPadder
+ from core.utils1.utils import get_network
+except ImportError as e:
+ st.error(f"Error importing core modules: {e}. Please ensure you are in the AIGVDet root directory.")
+
+st.set_page_config(page_title="AIGVDet GUI", layout="wide")
+
+st.title("AIGVDet: AI-Generated Video Detection")
+
+# Tabs
+tab1, tab2 = st.tabs(["1. Extract (RGB & Optical Flow)", "2. Detection (Architecture)"])
+
+# Global Device
+DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
+
+def load_image(imfile):
+ img = np.array(Image.open(imfile)).astype(np.uint8)
+ img = torch.from_numpy(img).permute(2, 0, 1).float()
+ return img[None].to(DEVICE)
+
+def save_vis(img, flo, folder_optical_flow_path, imfile1):
+ img = img[0].permute(1,2,0).cpu().numpy()
+ flo = flo[0].permute(1,2,0).cpu().numpy()
+
+ # map flow to rgb image
+ flo = flow_viz.flow_to_image(flo)
+
+ # We only save the flow image as per demo.py logic (it saves 'flo')
+ # demo.py: cv2.imwrite(folder_optical_flow_path, flo)
+
+ content = os.path.basename(imfile1)
+ save_path = os.path.join(folder_optical_flow_path, content)
+
+ # cv2 expects BGR, flow_viz returns RGB likely?
+ # flow_viz.flow_to_image returns RGB. cv2.imwrite expects BGR.
+ # demo.py uses cv2.imwrite(..., flo).
+ # Let's check flow_viz.flow_to_image implementation if possible, but assuming demo.py works, we follow it.
+ # Actually demo.py does: `flo = flow_viz.flow_to_image(flo)` then `cv2.imwrite(..., flo)`.
+ # If flow_viz returns RGB, cv2 saves it as BGR (swapping channels), so colors might be inverted if not handled.
+ # But we will stick to demo.py logic.
+
+ # Convert RGB to BGR for cv2
+ flo_bgr = cv2.cvtColor(flo, cv2.COLOR_RGB2BGR)
+ cv2.imwrite(save_path, flo_bgr)
+
+def video_to_frames(video_path, output_folder, progress_bar=None):
+ if not os.path.exists(output_folder):
+ os.makedirs(output_folder)
+
+ cap = cv2.VideoCapture(video_path)
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
+ frame_count = 0
+
+ while cap.isOpened():
+ ret, frame = cap.read()
+ if not ret:
+ break
+
+ frame_filename = os.path.join(output_folder, f"frame_{frame_count:05d}.png")
+ cv2.imwrite(frame_filename, frame)
+ frame_count += 1
+
+ if progress_bar:
+ progress_bar.progress(min(frame_count / total_frames, 1.0), text=f"Extracting frame {frame_count}/{total_frames}")
+
+ cap.release()
+ return sorted(glob.glob(os.path.join(output_folder, '*.png')))
+
+# --- TAB 1: EXTRACTION ---
+with tab1:
+ st.header("Extract RGB Frames and Optical Flow")
+
+ # Model Upload
+ raft_model_file = st.file_uploader("Upload RAFT Model (raft.pth)", type=['pth'], key="raft_uploader")
+
+ # Video Upload (Batch or Solo)
+ uploaded_videos = st.file_uploader("Upload Video(s)", type=['mp4', 'avi', 'mov', 'mkv'], accept_multiple_files=True, key="video_uploader")
+
+ # Output Directory
+ output_root = st.text_input("Output Directory Root", value="output_data")
+
+ if st.button("Start Extraction", key="extract_btn"):
+ if not raft_model_file:
+ st.error("Please upload the RAFT model.")
+ elif not uploaded_videos:
+ st.error("Please upload at least one video.")
+ else:
+ # Save RAFT model to temp file
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".pth") as tmp_raft:
+ tmp_raft.write(raft_model_file.read())
+ raft_model_path = tmp_raft.name
+
+ st.info(f"Loaded RAFT model. Using device: {DEVICE}")
+
+ # Load RAFT
+ try:
+ # Mock args for RAFT
+ class Args:
+ model = raft_model_path
+ small = False
+ mixed_precision = False
+ alternate_corr = False
+
+ args = Args()
+ model = torch.nn.DataParallel(RAFT(args))
+ model.load_state_dict(torch.load(args.model, map_location=torch.device(DEVICE)))
+ model = model.module
+ model.to(DEVICE)
+ model.eval()
+
+ st.success("RAFT Model Loaded Successfully!")
+
+ # Process Videos
+ total_videos = len(uploaded_videos)
+
+ for i, video_file in enumerate(uploaded_videos):
+ video_name = video_file.name
+ st.subheader(f"Processing: {video_name} ({i+1}/{total_videos})")
+
+ # Save video to temp
+ with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(video_name)[1]) as tmp_vid:
+ tmp_vid.write(video_file.read())
+ video_path = tmp_vid.name
+
+ # Define output paths
+ base_name = os.path.splitext(video_name)[0]
+ frame_output_dir = os.path.join(output_root, "frames", base_name)
+ flow_output_dir = os.path.join(output_root, "optical_flow", base_name)
+
+ # 1. Extract Frames
+ st.write("Extracting frames...")
+ p_bar = st.progress(0)
+ images = video_to_frames(video_path, frame_output_dir, p_bar)
+ st.write(f"Extracted {len(images)} frames to `{frame_output_dir}`")
+
+ # 2. Generate Optical Flow
+ if not os.path.exists(flow_output_dir):
+ os.makedirs(flow_output_dir)
+
+ st.write("Generating Optical Flow...")
+ images = natsorted(images)
+ flow_p_bar = st.progress(0)
+
+ with torch.no_grad():
+ for idx, (imfile1, imfile2) in enumerate(zip(images[:-1], images[1:])):
+ image1 = load_image(imfile1)
+ image2 = load_image(imfile2)
+
+ padder = InputPadder(image1.shape)
+ image1, image2 = padder.pad(image1, image2)
+
+ flow_low, flow_up = model(image1, image2, iters=20, test_mode=True)
+
+ save_vis(image1, flow_up, flow_output_dir, imfile1)
+
+ flow_p_bar.progress((idx + 1) / (len(images) - 1))
+
+ st.write(f"Optical Flow saved to `{flow_output_dir}`")
+
+ # Cleanup temp video
+ os.remove(video_path)
+
+ st.success("All videos processed!")
+ # Cleanup temp model
+ os.remove(raft_model_path)
+
+ except Exception as e:
+ st.error(f"An error occurred: {e}")
+ import traceback
+ st.code(traceback.format_exc())
+
+# --- TAB 2: DETECTION ---
+with tab2:
+ st.header("Run Detection (AIGVDet)")
+
+ col1, col2 = st.columns(2)
+ with col1:
+ optical_model_file = st.file_uploader("Upload Optical Flow Model (optical.pth)", type=['pth'], key="opt_uploader")
+ with col2:
+ original_model_file = st.file_uploader("Upload RGB Model (original.pth)", type=['pth'], key="orig_uploader")
+
+ # Input for processed data path
+ # Default to the output of Tab 1 if available
+ target_dir = st.text_input("Path to Processed Data (Root folder containing 'frames' and 'optical_flow')", value="output_data")
+
+ threshold = st.slider("Threshold", 0.0, 1.0, 0.5)
+
+ if st.button("Run Detection", key="detect_btn"):
+ if not optical_model_file or not original_model_file:
+ st.error("Please upload both Optical Flow and RGB models.")
+ elif not os.path.exists(target_dir):
+ st.error(f"Directory `{target_dir}` does not exist.")
+ else:
+ start_time = time.time()
+
+ # Save models to temp
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".pth") as tmp_opt:
+ tmp_opt.write(optical_model_file.read())
+ opt_model_path = tmp_opt.name
+
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".pth") as tmp_orig:
+ tmp_orig.write(original_model_file.read())
+ orig_model_path = tmp_orig.name
+
+ try:
+ st.info("Loading models...")
+
+ # Load Models
+ # Assuming ResNet50 as per demo.py default
+ model_op = get_network("resnet50")
+ state_dict_op = torch.load(opt_model_path, map_location="cpu")
+ if "model" in state_dict_op:
+ state_dict_op = state_dict_op["model"]
+ model_op.load_state_dict(state_dict_op)
+ model_op.eval()
+ if DEVICE == 'cuda':
+ model_op.cuda()
+
+ model_or = get_network("resnet50")
+ state_dict_or = torch.load(orig_model_path, map_location="cpu")
+ if "model" in state_dict_or:
+ state_dict_or = state_dict_or["model"]
+ model_or.load_state_dict(state_dict_or)
+ model_or.eval()
+ if DEVICE == 'cuda':
+ model_or.cuda()
+
+ load_duration = time.time() - start_time
+ st.write(f"Models loaded in {load_duration:.2f} seconds.")
+
+ # Find subfolders in frames
+ frames_root = os.path.join(target_dir, "frames")
+ flow_root = os.path.join(target_dir, "optical_flow")
+
+ if not os.path.exists(frames_root):
+ st.error(f"Could not find `frames` folder in {target_dir}")
+ st.stop()
+
+ # Get list of video folders
+ video_folders = [f for f in os.listdir(frames_root) if os.path.isdir(os.path.join(frames_root, f))]
+
+ if not video_folders:
+ st.warning("No subfolders found in `frames` directory.")
+
+ # Transforms
+ trans = transforms.Compose((
+ transforms.CenterCrop((448,448)),
+ transforms.ToTensor(),
+ ))
+
+ results = []
+
+ for vid_folder in video_folders:
+ st.subheader(f"Analyzing: {vid_folder}")
+
+ rgb_path = os.path.join(frames_root, vid_folder)
+ opt_path = os.path.join(flow_root, vid_folder)
+
+ if not os.path.exists(opt_path):
+ st.warning(f"No optical flow found for {vid_folder}, skipping.")
+ continue
+
+ # RGB Detection
+ rgb_files = sorted(glob.glob(os.path.join(rgb_path, "*.jpg")) +
+ glob.glob(os.path.join(rgb_path, "*.png")) +
+ glob.glob(os.path.join(rgb_path, "*.JPEG")))
+
+ rgb_prob_sum = 0
+ rgb_bar = st.progress(0, text="RGB Detection")
+
+ for i, img_path in enumerate(rgb_files):
+ img = Image.open(img_path).convert("RGB")
+ img = trans(img)
+ img = TF.normalize(img, mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ in_tens = img.unsqueeze(0).to(DEVICE)
+
+ with torch.no_grad():
+ prob = model_or(in_tens).sigmoid().item()
+ rgb_prob_sum += prob
+
+ rgb_bar.progress((i + 1) / len(rgb_files))
+
+ rgb_score = rgb_prob_sum / len(rgb_files) if rgb_files else 0
+ st.write(f"RGB Score: {rgb_score:.4f}")
+
+ # Optical Flow Detection
+ opt_files = sorted(glob.glob(os.path.join(opt_path, "*.jpg")) +
+ glob.glob(os.path.join(opt_path, "*.png")) +
+ glob.glob(os.path.join(opt_path, "*.JPEG")))
+
+ opt_prob_sum = 0
+ opt_bar = st.progress(0, text="Optical Flow Detection")
+
+ for i, img_path in enumerate(opt_files):
+ img = Image.open(img_path).convert("RGB")
+ img = trans(img)
+ img = TF.normalize(img, mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ in_tens = img.unsqueeze(0).to(DEVICE)
+
+ with torch.no_grad():
+ prob = model_op(in_tens).sigmoid().item()
+ opt_prob_sum += prob
+
+ opt_bar.progress((i + 1) / len(opt_files))
+
+ opt_score = opt_prob_sum / len(opt_files) if opt_files else 0
+ st.write(f"Optical Flow Score: {opt_score:.4f}")
+
+ # Final Decision
+ final_score = (rgb_score * 0.5) + (opt_score * 0.5)
+ decision = "FAKE VIDEO (AI-Generated)" if final_score >= threshold else "REAL VIDEO"
+ color = "red" if final_score >= threshold else "green"
+
+ st.markdown(f"### Result: :{color}[{decision}]")
+ st.write(f"**Combined Probability:** {final_score:.4f}")
+ st.divider()
+
+ results.append({
+ "Video": vid_folder,
+ "RGB Score": rgb_score,
+ "Optical Score": opt_score,
+ "Final Score": final_score,
+ "Decision": decision
+ })
+
+ # Summary Table
+ if results:
+ st.subheader("Batch Summary")
+ st.dataframe(results)
+
+ total_duration = time.time() - start_time
+ st.success(f"Total Processing Time: {total_duration:.2f} seconds")
+
+ # Cleanup
+ os.remove(opt_model_path)
+ os.remove(orig_model_path)
+
+ except Exception as e:
+ st.error(f"An error occurred during detection: {e}")
+ import traceback
+ st.code(traceback.format_exc())
diff --git a/prepare_real_data.py b/prepare_real_data.py
new file mode 100644
index 0000000..51f89b7
--- /dev/null
+++ b/prepare_real_data.py
@@ -0,0 +1,175 @@
+import argparse
+import os
+import glob
+import shutil
+import cv2
+import numpy as np
+import torch
+import torch.nn
+from PIL import Image
+from tqdm import tqdm
+import sys
+
+# Add core to path to import RAFT
+sys.path.append('core')
+from raft import RAFT
+from utils import flow_viz
+from utils.utils import InputPadder
+from natsort import natsorted
+
+DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
+
+def load_image(imfile):
+ img = Image.open(imfile)
+
+ # Resize if too large (e.g. > 1024px) to prevent OOM
+ max_dim = 1024
+ if max(img.size) > max_dim:
+ scale = max_dim / max(img.size)
+ new_size = (int(img.size[0] * scale), int(img.size[1] * scale))
+ img = img.resize(new_size, Image.BILINEAR)
+
+ img = np.array(img).astype(np.uint8)
+ img = torch.from_numpy(img).permute(2, 0, 1).float()
+ return img[None].to(DEVICE)
+
+def viz(img, flo, output_dir, filename):
+ img = img[0].permute(1,2,0).cpu().numpy()
+ flo = flo[0].permute(1,2,0).cpu().numpy()
+
+ # map flow to rgb image
+ flo = flow_viz.flow_to_image(flo)
+
+ # Save flow image
+ # The filename should match the input frame filename
+ save_path = os.path.join(output_dir, os.path.basename(filename))
+ cv2.imwrite(save_path, flo)
+
+def generate_optical_flow(model, frames_dir, output_dir):
+ if not os.path.exists(output_dir):
+ os.makedirs(output_dir)
+
+ # Get all frames
+ images = sorted(glob.glob(os.path.join(frames_dir, '*.jpg')) +
+ glob.glob(os.path.join(frames_dir, '*.png')))
+ images = natsorted(images)
+
+ print(f"Generating optical flow for {len(images)} frames...")
+
+ with torch.no_grad():
+ for imfile1, imfile2 in tqdm(zip(images[:-1], images[1:]), total=len(images)-1, desc="Flow Generation"):
+ image1 = load_image(imfile1)
+ image2 = load_image(imfile2)
+
+ padder = InputPadder(image1.shape)
+ image1, image2 = padder.pad(image1, image2)
+
+ flow_low, flow_up = model(image1, image2, iters=20, test_mode=True)
+
+ viz(image1, flow_up, output_dir, imfile1)
+
+def main():
+ parser = argparse.ArgumentParser(description="Prepare Real Data for all datasets")
+ parser.add_argument("--source", type=str, required=True, help="Path to source folder containing multiple video folders (e.g. 1_real)")
+ parser.add_argument("--raft_model", type=str, default="raft_model/raft-things.pth", help="Path to RAFT model checkpoint")
+ parser.add_argument("--start_index", type=int, default=1, help="Index to start processing from (1-based)")
+ args = parser.parse_args()
+
+ if not os.path.exists(args.source):
+ print(f"❌ Source folder not found: {args.source}")
+ return
+
+ # Find all subdirectories (video folders)
+ video_folders = [f.path for f in os.scandir(args.source) if f.is_dir()]
+ video_folders = sorted(video_folders)
+
+ if not video_folders:
+ print(f"❌ No video folders found in {args.source}")
+ return
+
+ print(f"Found {len(video_folders)} video folders to process")
+
+ # Load RAFT model
+ print("Loading RAFT model...")
+
+ raft_args = argparse.Namespace(
+ model=args.raft_model,
+ small=False,
+ mixed_precision=False,
+ alternate_corr=False,
+ dropout=0
+ )
+
+ model = torch.nn.DataParallel(RAFT(raft_args))
+
+ try:
+ model.load_state_dict(torch.load(args.raft_model, map_location=torch.device(DEVICE)))
+ except FileNotFoundError:
+ print(f"❌ RAFT model not found at {args.raft_model}")
+ print("Please download it first using: python download_data.py --skip-data --skip-checkpoints")
+ return
+
+ model = model.module
+ model.to(DEVICE)
+ model.eval()
+ print("✓ RAFT model loaded")
+
+ # Target datasets
+ datasets = ["moonvalley", "videocraft", "pika", "neverends"]
+
+ # Process each video folder
+ for idx, video_path in enumerate(video_folders, 1):
+ if idx < args.start_index:
+ continue
+
+ video_name = os.path.basename(video_path)
+ print(f"\n{'='*60}")
+ print(f"Processing Video {idx}/{len(video_folders)}: {video_name}")
+ print(f"{'='*60}")
+
+ # 1. Generate Optical Flow ONCE in a temp location
+ temp_flow_dir = os.path.join("data", "temp_flow", video_name)
+ # Skip if already generated in temp
+ if os.path.exists(temp_flow_dir) and len(os.listdir(temp_flow_dir)) > 0:
+ print(f" Using existing flow in temp: {temp_flow_dir}")
+ else:
+ print(f" Generating optical flow to temp location: {temp_flow_dir}")
+ generate_optical_flow(model, video_path, temp_flow_dir)
+
+ # 2. Distribute to all datasets
+ print(f" Distributing to {len(datasets)} datasets...")
+
+ for dataset in datasets:
+ # Paths
+ rgb_dest = os.path.join("data", "test", "original", "T2V", dataset, "0_real", video_name)
+ flow_dest = os.path.join("data", "test", "T2V", dataset, "0_real", video_name)
+
+ # Create directories
+ os.makedirs(rgb_dest, exist_ok=True)
+ os.makedirs(flow_dest, exist_ok=True)
+
+ # Copy RGB frames
+ # print(f" -> RGB: {dataset}")
+ if os.path.exists(rgb_dest):
+ shutil.rmtree(rgb_dest)
+ shutil.copytree(video_path, rgb_dest)
+
+ # Copy Flow frames
+ # print(f" -> Flow: {dataset}")
+ if os.path.exists(flow_dest):
+ shutil.rmtree(flow_dest)
+ shutil.copytree(temp_flow_dir, flow_dest)
+
+ print("\n" + "="*80)
+ print("✓ REAL DATA PREPARATION COMPLETE")
+ print("="*80)
+ print(f"Source: {args.source}")
+ print(f"Processed {len(video_folders)} videos")
+ print(f"Distributed to: {', '.join(datasets)}")
+
+ # Clean up temp
+ if os.path.exists(os.path.join("data", "temp_flow")):
+ shutil.rmtree(os.path.join("data", "temp_flow"))
+
+if __name__ == "__main__":
+ main()
diff --git a/python-utils/prepare_data.py b/python-utils/prepare_data.py
index 65791b8..b72bb1a 100644
--- a/python-utils/prepare_data.py
+++ b/python-utils/prepare_data.py
@@ -36,9 +36,10 @@ def save_flow(img, flo, output_path):
flo = flow_viz.flow_to_image(flo)
cv2.imwrite(output_path, flo)
-def video_to_frames(video_path, output_folder):
+def video_to_frames(video_path, output_folder, max_frames=95):
"""
Extract frames from a video and save them as images in the output folder.
+ Limits to max_frames (default 95) to match paper methodology.
"""
if not os.path.exists(output_folder):
os.makedirs(output_folder)
@@ -46,12 +47,12 @@ def video_to_frames(video_path, output_folder):
# Check if frames already exist to skip
existing_frames = glob.glob(os.path.join(output_folder, "*.png"))
if len(existing_frames) > 0:
- return sorted(existing_frames)
+ return sorted(existing_frames)[:max_frames]
cap = cv2.VideoCapture(video_path)
frame_count = 0
- while cap.isOpened():
+ while cap.isOpened() and frame_count < max_frames:
ret, frame = cap.read()
if not ret:
break
@@ -65,7 +66,7 @@ def video_to_frames(video_path, output_folder):
# Return sorted list of extracted frame files
images = glob.glob(os.path.join(output_folder, '*.png')) + \
glob.glob(os.path.join(output_folder, '*.jpg'))
- return sorted(images)
+ return sorted(images)[:max_frames]
def process_dataset(args):
"""
diff --git a/recreate_table2_data2.py b/recreate_table2_data2.py
new file mode 100644
index 0000000..819c80e
--- /dev/null
+++ b/recreate_table2_data2.py
@@ -0,0 +1,309 @@
+"""
+Script to recreate Table 2 for data2 (Emu, Hotshot, Sora)
+Evaluates only the 3 main variants: AIGVDet (fused), Spatial, Optical
+"""
+
+import os
+import subprocess
+import pandas as pd
+from pathlib import Path
+import re
+import argparse
+
+# Configuration for data2 datasets
+DATASETS = {
+ "emu": {
+ "optical": "data2/test/T2V/emu",
+ "rgb": "data2/test/original/T2V/emu"
+ },
+ "hotshot": {
+ "optical": "data2/test/T2V/hotshot",
+ "rgb": "data2/test/original/T2V/hotshot"
+ },
+ "sora": {
+ "optical": "data2/test/T2V/sora",
+ "rgb": "data2/test/original/T2V/sora"
+ }
+}
+
+# Model configurations - Only 3 variants for Table 2
+VARIANTS = {
+ "S_spatial": {
+ "eval_mode": "rgb_only",
+ "optical_model": "checkpoints/optical.pth",
+ "rgb_model": "checkpoints/original.pth",
+ "no_crop": False
+ },
+ "S_optical": {
+ "eval_mode": "optical_only",
+ "optical_model": "checkpoints/optical.pth",
+ "rgb_model": "checkpoints/original.pth",
+ "no_crop": False
+ },
+ "AIGVDet": {
+ "eval_mode": "fused",
+ "optical_model": "checkpoints/optical.pth",
+ "rgb_model": "checkpoints/original.pth",
+ "no_crop": False
+ }
+}
+
+RESULTS_DIR = Path("data2/results/table2")
+RESULTS_DIR.mkdir(parents=True, exist_ok=True)
+
+def check_prerequisites():
+ """Check if all required datasets and models exist"""
+ print("="*80)
+ print("CHECKING PREREQUISITES FOR DATA2")
+ print("="*80)
+
+ # Check datasets
+ print("\n1. Checking datasets...")
+ missing_data = []
+ for dataset_name, paths in DATASETS.items():
+ for stream_type, path in paths.items():
+ if not os.path.exists(path):
+ missing_data.append(f" ❌ {dataset_name} ({stream_type}): {path}")
+ else:
+ real_path = os.path.join(path, "0_real")
+ fake_path = os.path.join(path, "1_fake")
+
+ status = []
+ if os.path.exists(real_path):
+ status.append("Real ✓")
+ else:
+ status.append("Real ✗")
+
+ if os.path.exists(fake_path):
+ status.append("Fake ✓")
+ else:
+ status.append("Fake ✗")
+
+ print(f" ✓ {dataset_name} ({stream_type}): {path} [{', '.join(status)}]")
+
+ if not os.path.exists(real_path) and not os.path.exists(fake_path):
+ missing_data.append(f" ⚠️ {dataset_name} ({stream_type}): Missing BOTH 0_real and 1_fake folders")
+
+ if missing_data:
+ print("\n⚠️ Some data is missing:")
+ for item in missing_data:
+ print(item)
+
+ # Check models
+ print("\n2. Checking model checkpoints...")
+ models = ["checkpoints/optical.pth", "checkpoints/original.pth"]
+ missing_models = []
+ for model in models:
+ if os.path.exists(model):
+ print(f" ✓ {model}")
+ else:
+ print(f" ❌ {model}")
+ missing_models.append(model)
+
+ if missing_models:
+ print("\n❌ Missing models. Please ensure checkpoints are available.")
+ return False
+
+ if missing_data:
+ print("\n⚠️ Some datasets are incomplete but will continue...")
+
+ return True
+
+def run_evaluation(dataset_name, variant_name, variant_config, limit=None):
+ """Run evaluation for a specific dataset and variant"""
+ dataset_paths = DATASETS[dataset_name]
+
+ # Build command
+ cmd = [
+ "python", "test_single_stream.py",
+ "-fop", dataset_paths["optical"],
+ "-for", dataset_paths["rgb"],
+ "-mop", variant_config["optical_model"],
+ "-mor", variant_config["rgb_model"],
+ "--eval_mode", variant_config["eval_mode"],
+ "-e", str(RESULTS_DIR / f"{dataset_name}_{variant_name}_video.csv"),
+ "-ef", str(RESULTS_DIR / f"{dataset_name}_{variant_name}_frame.csv"),
+ "-t", "0.5"
+ ]
+
+ if variant_config["no_crop"]:
+ cmd.append("--no_crop")
+
+ if limit:
+ cmd.extend(["--limit", str(limit)])
+
+ print(f"\n{'='*80}")
+ print(f"Running: {variant_name} on {dataset_name}")
+ print(f"{'='*80}")
+ print("Command:", " ".join(cmd))
+ print(f"{'='*80}\n")
+
+ # Run the command with real-time output
+ try:
+ # Use Popen to stream output in real-time
+ process = subprocess.Popen(
+ cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ bufsize=1,
+ universal_newlines=True
+ )
+
+ # Collect output while displaying it
+ output_lines = []
+ for line in process.stdout:
+ print(line, end='', flush=True) # Print in real-time
+ output_lines.append(line)
+
+ # Wait for process to complete
+ return_code = process.wait(timeout=3600)
+ output = ''.join(output_lines)
+
+ if return_code != 0:
+ print(f"\n⚠️ Command exited with code {return_code}")
+
+ # Parse metrics from CSV file
+ csv_path = RESULTS_DIR / f"{dataset_name}_{variant_name}_video.csv"
+ metrics = parse_metrics(output, csv_path)
+ return metrics
+ except subprocess.TimeoutExpired:
+ print(f"\n⚠️ Timeout while running {variant_name} on {dataset_name}")
+ process.kill()
+ return None
+ except Exception as e:
+ print(f"\n❌ Error running {variant_name} on {dataset_name}: {e}")
+ return None
+
+def parse_metrics(output, csv_path):
+ """Parse metrics from CSV file (test_single_stream.py doesn't print to stdout)"""
+ try:
+ # Read the CSV file
+ if not os.path.exists(csv_path):
+ print(f" ⚠️ CSV file not found: {csv_path}")
+ return None
+
+ df = pd.read_csv(csv_path)
+
+ if len(df) == 0:
+ print(f" ⚠️ CSV file is empty: {csv_path}")
+ return None
+
+ # Calculate metrics from the CSV data
+ from sklearn.metrics import accuracy_score, roc_auc_score, average_precision_score
+
+ y_true = df['flag'].values # Ground truth (0=real, 1=fake)
+ y_pred = df['pro'].values # Predicted probability
+
+ # Calculate metrics
+ acc = accuracy_score(y_true, (y_pred >= 0.5).astype(int))
+ auc = roc_auc_score(y_true, y_pred) if len(set(y_true)) > 1 else 0.0
+ ap = average_precision_score(y_true, y_pred) if len(set(y_true)) > 1 else 0.0
+
+ metrics = {
+ 'ACC': acc,
+ 'AUC': auc,
+ 'AP': ap
+ }
+
+ print(f"\n ✓ Metrics: ACC={acc:.4f}, AUC={auc:.4f}, AP={ap:.4f}")
+ return metrics
+
+ except Exception as e:
+ print(f" ⚠️ Error reading metrics from CSV: {e}")
+ return None
+
+def run_all_evaluations(limit=None):
+ """Run evaluations for all variants and datasets"""
+ print("\n" + "="*80)
+ print("RUNNING EVALUATIONS FOR DATA2")
+ print("="*80)
+
+ all_results = {}
+
+ total_evals = len(VARIANTS) * len(DATASETS)
+ current_eval = 0
+
+ for variant_idx, (variant_name, variant_config) in enumerate(VARIANTS.items(), 1):
+ all_results[variant_name] = {}
+
+ print(f"\n{'='*80}")
+ print(f"VARIANT {variant_idx}/{len(VARIANTS)}: {variant_name}")
+ print(f"{'='*80}")
+
+ for dataset_idx, dataset_name in enumerate(DATASETS.keys(), 1):
+ current_eval += 1
+ overall_progress = (current_eval / total_evals) * 100
+
+ print(f"\n[Overall: {current_eval}/{total_evals} - {overall_progress:.1f}%]")
+ print(f"[Variant: {variant_idx}/{len(VARIANTS)}] [{variant_name}]")
+ print(f"[Dataset: {dataset_idx}/{len(DATASETS)}] [{dataset_name}]")
+
+ metrics = run_evaluation(dataset_name, variant_name, variant_config, limit)
+ all_results[variant_name][dataset_name] = metrics
+
+ return all_results
+
+def compile_table2(results):
+ """Compile results into Table 2 format"""
+ print("\n" + "="*80)
+ print("COMPILING TABLE 2 (DATA2)")
+ print("="*80)
+
+ # Create DataFrame
+ rows = []
+ for dataset in DATASETS.keys():
+ row = {"Dataset": dataset}
+ for variant in VARIANTS.keys():
+ if results[variant][dataset]:
+ auc = results[variant][dataset].get('AUC', 0) * 100
+ ap = results[variant][dataset].get('AP', 0) * 100
+ row[variant] = f"{auc:.1f}/{ap:.1f}"
+ else:
+ row[variant] = "N/A"
+ rows.append(row)
+
+ df = pd.DataFrame(rows)
+
+ # Save to CSV
+ output_file = RESULTS_DIR / "table2_data2.csv"
+ df.to_csv(output_file, index=False)
+
+ print(f"\n✓ Table 2 saved to: {output_file}")
+ print("\nTable 2 Preview:")
+ print(df.to_string(index=False))
+
+ return df
+
+def main():
+ """Main execution function"""
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--limit", type=int, default=None, help="Limit number of videos per class")
+ args = parser.parse_args()
+
+ print("="*80)
+ print("RECREATING TABLE 2 FOR DATA2 (EMU, HOTSHOT, SORA)")
+ print("="*80)
+ print("\nThis script will:")
+ print("1. Check prerequisites (data and models)")
+ print("2. Run evaluations for 3 variants (AIGVDet, Spatial, Optical)")
+ print("3. Compile results into Table 2 format")
+ print("="*80)
+
+ # Check prerequisites
+ if not check_prerequisites():
+ print("\n❌ Prerequisites not satisfied. Please fix the issues above.")
+ return
+
+ # Run all evaluations
+ all_results = run_all_evaluations(limit=args.limit)
+
+ # Compile and display Table 2
+ table = compile_table2(all_results)
+
+ print("\n" + "="*80)
+ print("✅ TABLE 2 RECREATION COMPLETE!")
+ print("="*80)
+
+if __name__ == "__main__":
+ main()
diff --git a/recreate_table2_data3.py b/recreate_table2_data3.py
new file mode 100644
index 0000000..96fc079
--- /dev/null
+++ b/recreate_table2_data3.py
@@ -0,0 +1,309 @@
+"""
+Script to recreate Table 2 for data3 (I2V: Moonvalley, Pika, NeverEnds)
+Evaluates only the 3 main variants: AIGVDet (fused), Spatial, Optical
+"""
+
+import os
+import subprocess
+import pandas as pd
+from pathlib import Path
+import re
+import argparse
+
+# Configuration for data3 I2V datasets
+DATASETS = {
+ "moonvalley": {
+ "optical": "data3/test/I2V/moonvalley",
+ "rgb": "data3/test/original/I2V/moonvalley"
+ },
+ "pika": {
+ "optical": "data3/test/I2V/pika",
+ "rgb": "data3/test/original/I2V/pika"
+ },
+ "neverends": {
+ "optical": "data3/test/I2V/neverends",
+ "rgb": "data3/test/original/I2V/neverends"
+ }
+}
+
+# Model configurations - Only 3 variants for Table 2
+VARIANTS = {
+ "S_spatial": {
+ "eval_mode": "rgb_only",
+ "optical_model": "checkpoints/optical.pth",
+ "rgb_model": "checkpoints/original.pth",
+ "no_crop": False
+ },
+ "S_optical": {
+ "eval_mode": "optical_only",
+ "optical_model": "checkpoints/optical.pth",
+ "rgb_model": "checkpoints/original.pth",
+ "no_crop": False
+ },
+ "AIGVDet": {
+ "eval_mode": "fused",
+ "optical_model": "checkpoints/optical.pth",
+ "rgb_model": "checkpoints/original.pth",
+ "no_crop": False
+ }
+}
+
+RESULTS_DIR = Path("data3/results/table2")
+RESULTS_DIR.mkdir(parents=True, exist_ok=True)
+
+def check_prerequisites():
+ """Check if all required datasets and models exist"""
+ print("="*80)
+ print("CHECKING PREREQUISITES FOR DATA3 (I2V)")
+ print("="*80)
+
+ # Check datasets
+ print("\n1. Checking I2V datasets...")
+ missing_data = []
+ for dataset_name, paths in DATASETS.items():
+ for stream_type, path in paths.items():
+ if not os.path.exists(path):
+ missing_data.append(f" ❌ {dataset_name} ({stream_type}): {path}")
+ else:
+ real_path = os.path.join(path, "0_real")
+ fake_path = os.path.join(path, "1_fake")
+
+ status = []
+ if os.path.exists(real_path):
+ status.append("Real ✓")
+ else:
+ status.append("Real ✗")
+
+ if os.path.exists(fake_path):
+ status.append("Fake ✓")
+ else:
+ status.append("Fake ✗")
+
+ print(f" ✓ {dataset_name} ({stream_type}): {path} [{', '.join(status)}]")
+
+ if not os.path.exists(real_path) and not os.path.exists(fake_path):
+ missing_data.append(f" ⚠️ {dataset_name} ({stream_type}): Missing BOTH 0_real and 1_fake folders")
+
+ if missing_data:
+ print("\n⚠️ Some data is missing:")
+ for item in missing_data:
+ print(item)
+
+ # Check models
+ print("\n2. Checking model checkpoints...")
+ models = ["checkpoints/optical.pth", "checkpoints/original.pth"]
+ missing_models = []
+ for model in models:
+ if os.path.exists(model):
+ print(f" ✓ {model}")
+ else:
+ print(f" ❌ {model}")
+ missing_models.append(model)
+
+ if missing_models:
+ print("\n❌ Missing models. Please ensure checkpoints are available.")
+ return False
+
+ if missing_data:
+ print("\n⚠️ Some datasets are incomplete but will continue...")
+
+ return True
+
+def run_evaluation(dataset_name, variant_name, variant_config, limit=None):
+ """Run evaluation for a specific dataset and variant"""
+ dataset_paths = DATASETS[dataset_name]
+
+ # Build command
+ cmd = [
+ "python", "test_single_stream.py",
+ "-fop", dataset_paths["optical"],
+ "-for", dataset_paths["rgb"],
+ "-mop", variant_config["optical_model"],
+ "-mor", variant_config["rgb_model"],
+ "--eval_mode", variant_config["eval_mode"],
+ "-e", str(RESULTS_DIR / f"{dataset_name}_{variant_name}_video.csv"),
+ "-ef", str(RESULTS_DIR / f"{dataset_name}_{variant_name}_frame.csv"),
+ "-t", "0.5"
+ ]
+
+ if variant_config["no_crop"]:
+ cmd.append("--no_crop")
+
+ if limit:
+ cmd.extend(["--limit", str(limit)])
+
+ print(f"\n{'='*80}")
+ print(f"Running: {variant_name} on {dataset_name}")
+ print(f"{'='*80}")
+ print("Command:", " ".join(cmd))
+ print(f"{'='*80}\n")
+
+ # Run the command with real-time output
+ try:
+ # Use Popen to stream output in real-time
+ process = subprocess.Popen(
+ cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ bufsize=1,
+ universal_newlines=True
+ )
+
+ # Collect output while displaying it
+ output_lines = []
+ for line in process.stdout:
+ print(line, end='', flush=True) # Print in real-time
+ output_lines.append(line)
+
+ # Wait for process to complete
+ return_code = process.wait(timeout=3600)
+ output = ''.join(output_lines)
+
+ if return_code != 0:
+ print(f"\n⚠️ Command exited with code {return_code}")
+
+ # Parse metrics from CSV file
+ csv_path = RESULTS_DIR / f"{dataset_name}_{variant_name}_video.csv"
+ metrics = parse_metrics(output, csv_path)
+ return metrics
+ except subprocess.TimeoutExpired:
+ print(f"\n⚠️ Timeout while running {variant_name} on {dataset_name}")
+ process.kill()
+ return None
+ except Exception as e:
+ print(f"\n❌ Error running {variant_name} on {dataset_name}: {e}")
+ return None
+
+def parse_metrics(output, csv_path):
+ """Parse metrics from CSV file (test_single_stream.py doesn't print to stdout)"""
+ try:
+ # Read the CSV file
+ if not os.path.exists(csv_path):
+ print(f" ⚠️ CSV file not found: {csv_path}")
+ return None
+
+ df = pd.read_csv(csv_path)
+
+ if len(df) == 0:
+ print(f" ⚠️ CSV file is empty: {csv_path}")
+ return None
+
+ # Calculate metrics from the CSV data
+ from sklearn.metrics import accuracy_score, roc_auc_score, average_precision_score
+
+ y_true = df['flag'].values # Ground truth (0=real, 1=fake)
+ y_pred = df['pro'].values # Predicted probability
+
+ # Calculate metrics
+ acc = accuracy_score(y_true, (y_pred >= 0.5).astype(int))
+ auc = roc_auc_score(y_true, y_pred) if len(set(y_true)) > 1 else 0.0
+ ap = average_precision_score(y_true, y_pred) if len(set(y_true)) > 1 else 0.0
+
+ metrics = {
+ 'acc': acc,
+ 'auc': auc,
+ 'ap': ap
+ }
+
+ print(f"\n ✓ Metrics: ACC={acc:.4f}, AUC={auc:.4f}, AP={ap:.4f}")
+ return metrics
+
+ except Exception as e:
+ print(f" ⚠️ Error reading metrics from CSV: {e}")
+ return None
+
+def run_all_evaluations(limit=None):
+ """Run evaluations for all variants and datasets"""
+ print("\n" + "="*80)
+ print("RUNNING EVALUATIONS FOR DATA3 (I2V)")
+ print("="*80)
+
+ all_results = {}
+
+ total_evals = len(VARIANTS) * len(DATASETS)
+ current_eval = 0
+
+ for variant_idx, (variant_name, variant_config) in enumerate(VARIANTS.items(), 1):
+ all_results[variant_name] = {}
+
+ print(f"\n{'='*80}")
+ print(f"VARIANT {variant_idx}/{len(VARIANTS)}: {variant_name}")
+ print(f"{'='*80}")
+
+ for dataset_idx, dataset_name in enumerate(DATASETS.keys(), 1):
+ current_eval += 1
+ overall_progress = (current_eval / total_evals) * 100
+
+ print(f"\n[Overall: {current_eval}/{total_evals} - {overall_progress:.1f}%]")
+ print(f"[Variant: {variant_idx}/{len(VARIANTS)}] [{variant_name}]")
+ print(f"[Dataset: {dataset_idx}/{len(DATASETS)}] [{dataset_name}]")
+
+ metrics = run_evaluation(dataset_name, variant_name, variant_config, limit)
+ all_results[variant_name][dataset_name] = metrics
+
+ return all_results
+
+def compile_table2(results):
+ """Compile results into Table 2 format"""
+ print("\n" + "="*80)
+ print("COMPILING TABLE 2 (DATA3 - I2V)")
+ print("="*80)
+
+ # Create DataFrame
+ rows = []
+ for dataset in DATASETS.keys():
+ row = {"Dataset": dataset}
+ for variant in VARIANTS.keys():
+ if results[variant][dataset]:
+ auc = results[variant][dataset].get('auc', 0) * 100
+ acc = results[variant][dataset].get('acc', 0) * 100
+ row[variant] = f"{auc:.1f}/{acc:.1f}"
+ else:
+ row[variant] = "N/A"
+ rows.append(row)
+
+ df = pd.DataFrame(rows)
+
+ # Save to CSV
+ output_file = RESULTS_DIR / "table2_data3_i2v.csv"
+ df.to_csv(output_file, index=False)
+
+ print(f"\n✓ Table 2 (I2V) saved to: {output_file}")
+ print("\nTable 2 Preview:")
+ print(df.to_string(index=False))
+
+ return df
+
+def main():
+ """Main execution function"""
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--limit", type=int, default=None, help="Limit number of videos per class")
+ args = parser.parse_args()
+
+ print("="*80)
+ print("RECREATING TABLE 2 FOR DATA3 (I2V: MOONVALLEY, PIKA, NEVERENDS)")
+ print("="*80)
+ print("\nThis script will:")
+ print("1. Check prerequisites (data and models)")
+ print("2. Run evaluations for 3 variants (AIGVDet, Spatial, Optical)")
+ print("3. Compile results into Table 2 format")
+ print("="*80)
+
+ # Check prerequisites
+ if not check_prerequisites():
+ print("\n❌ Prerequisites not satisfied. Please fix the issues above.")
+ return
+
+ # Run all evaluations
+ all_results = run_all_evaluations(limit=args.limit)
+
+ # Compile and display Table 2
+ table = compile_table2(all_results)
+
+ print("\n" + "="*80)
+ print("✅ TABLE 2 (I2V) RECREATION COMPLETE!")
+ print("="*80)
+
+if __name__ == "__main__":
+ main()
diff --git a/recreate_table2_final.py b/recreate_table2_final.py
new file mode 100644
index 0000000..b68754d
--- /dev/null
+++ b/recreate_table2_final.py
@@ -0,0 +1,382 @@
+"""
+Comprehensive script to recreate Table 2 from the AIGVDet paper
+This script:
+1. Checks for required data and models
+2. Runs evaluations for all variants (Sspatial, Soptical, Soptical_no_cp, AIGVDet)
+3. Compiles results into Table 2 format
+"""
+
+import os
+import subprocess
+import pandas as pd
+from pathlib import Path
+import re
+import argparse
+
+# Configuration for datasets
+DATASETS = {
+ "moonvalley": {
+ "optical": "data/test/T2V/moonvalley",
+ "rgb": "data/test/original/T2V/moonvalley"
+ },
+ "videocraft": {
+ "optical": "data/test/T2V/videocraft",
+ "rgb": "data/test/original/T2V/videocraft"
+ },
+ "pika": {
+ "optical": "data/test/T2V/pika",
+ "rgb": "data/test/original/T2V/pika"
+ },
+ "neverends": {
+ "optical": "data/test/T2V/neverends",
+ "rgb": "data/test/original/T2V/neverends"
+ }
+}
+
+# Model configurations for each variant
+VARIANTS = {
+ "S_spatial": {
+ "eval_mode": "rgb_only",
+ "optical_model": "checkpoints/optical.pth",
+ "rgb_model": "checkpoints/original.pth",
+ "no_crop": False
+ },
+ "S_optical": {
+ "eval_mode": "optical_only",
+ "optical_model": "checkpoints/optical.pth",
+ "rgb_model": "checkpoints/original.pth",
+ "no_crop": False
+ },
+ "S_optical_no_cp": {
+ "eval_mode": "optical_only",
+ "optical_model": "checkpoints/optical.pth",
+ "rgb_model": "checkpoints/original.pth",
+ "no_crop": True
+ },
+ "AIGVDet": {
+ "eval_mode": "fused",
+ "optical_model": "checkpoints/optical.pth",
+ "rgb_model": "checkpoints/original.pth",
+ "no_crop": False
+ }
+}
+
+RESULTS_DIR = Path("data/results/table2")
+RESULTS_DIR.mkdir(parents=True, exist_ok=True)
+
+def check_prerequisites():
+ """Check if all required datasets and models exist"""
+ print("="*80)
+ print("CHECKING PREREQUISITES")
+ print("="*80)
+
+ # Check datasets
+ print("\n1. Checking datasets...")
+ missing_data = []
+ for dataset_name, paths in DATASETS.items():
+ for stream_type, path in paths.items():
+ if not os.path.exists(path):
+ missing_data.append(f" ❌ {dataset_name} ({stream_type}): {path}")
+ else:
+ # Check if has 0_real and 1_fake subfolders
+ real_path = os.path.join(path, "0_real")
+ fake_path = os.path.join(path, "1_fake")
+
+ status = []
+ if os.path.exists(real_path):
+ status.append("Real ✓")
+ else:
+ status.append("Real ✗")
+
+ if os.path.exists(fake_path):
+ status.append("Fake ✓")
+ else:
+ status.append("Fake ✗")
+
+ print(f" ✓ {dataset_name} ({stream_type}): {path} [{', '.join(status)}]")
+
+ if not os.path.exists(real_path) and not os.path.exists(fake_path):
+ missing_data.append(f" ⚠️ {dataset_name} ({stream_type}): Missing BOTH 0_real and 1_fake folders")
+
+ if missing_data:
+ print("\n⚠️ Critical issues found:")
+ for issue in missing_data:
+ print(issue)
+ print("\nPlease run prepare_data.py to extract frames from videos first.")
+ return False
+
+ # Check models
+ print("\n2. Checking model checkpoints...")
+ required_models = set()
+ for variant_config in VARIANTS.values():
+ required_models.add(variant_config["optical_model"])
+ required_models.add(variant_config["rgb_model"])
+
+ missing_models = []
+ for model_path in required_models:
+ if not os.path.exists(model_path):
+ missing_models.append(f" ❌ {model_path}")
+ else:
+ print(f" ✓ {model_path}")
+
+ if missing_models:
+ print("\n⚠️ Missing model checkpoints:")
+ for missing in missing_models:
+ print(missing)
+ print("\nPlease ensure you have trained models or download pre-trained checkpoints.")
+ return False
+
+ print("\n✓ All prerequisites satisfied!")
+ return True
+
+def run_evaluation(dataset_name, variant_name, variant_config, limit=None):
+ """
+ Run evaluation for a specific dataset and variant
+ """
+ dataset_paths = DATASETS[dataset_name]
+
+ # Build command
+ cmd = [
+ "python", "test_single_stream.py",
+ "-fop", dataset_paths["optical"],
+ "-for", dataset_paths["rgb"],
+ "-mop", variant_config["optical_model"],
+ "-mor", variant_config["rgb_model"],
+ "--eval_mode", variant_config["eval_mode"],
+ "-e", str(RESULTS_DIR / f"{dataset_name}_{variant_name}_video.csv"),
+ "-ef", str(RESULTS_DIR / f"{dataset_name}_{variant_name}_frame.csv"),
+ "-t", "0.5"
+ ]
+
+ if variant_config["no_crop"]:
+ cmd.append("--no_crop")
+
+ if limit:
+ cmd.extend(["--limit", str(limit)])
+
+ print(f"\n{'='*80}")
+ print(f"Running: {variant_name} on {dataset_name}")
+ print(f"{'='*80}")
+ print("Command:", " ".join(cmd))
+ print(f"{'='*80}\n")
+
+ # Run the command with real-time output
+ try:
+ # Use Popen to stream output in real-time
+ process = subprocess.Popen(
+ cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ bufsize=1,
+ universal_newlines=True
+ )
+
+ # Collect output while displaying it
+ output_lines = []
+ for line in process.stdout:
+ print(line, end='', flush=True) # Print in real-time
+ output_lines.append(line)
+
+ # Wait for process to complete
+ return_code = process.wait(timeout=3600)
+ output = ''.join(output_lines)
+
+ if return_code != 0:
+ print(f"\n⚠️ Command exited with code {return_code}")
+
+ # Parse metrics from output
+ metrics = parse_metrics(output)
+ return metrics
+ except subprocess.TimeoutExpired:
+ print(f"\n⚠️ Timeout while running {variant_name} on {dataset_name}")
+ process.kill()
+ return None
+ except Exception as e:
+ print(f"\n❌ Error running {variant_name} on {dataset_name}: {e}")
+ return None
+
+def parse_metrics(output):
+ """Parse accuracy and AUC from test output"""
+ metrics = {'acc': None, 'auc': None}
+
+ lines = output.split('\n')
+ for line in lines:
+ # Look for "acc: 0.XXXX (XX.X%)" or "acc: 0.XXXX" or "Accuracy: XX.X%"
+ if 'acc' in line.lower():
+ # Try pattern 1: "acc: 0.XXXX"
+ match = re.search(r'acc[:\s]+([0-9.]+)', line, re.IGNORECASE)
+ if match:
+ metrics['acc'] = float(match.group(1))
+
+ # Look for "auc: 0.XXXX (XX.X%)" or "auc: 0.XXXX" or "AUC: XX.X%"
+ if 'auc' in line.lower():
+ # Try pattern 1: "auc: 0.XXXX"
+ match = re.search(r'auc[:\s]+([0-9.]+)', line, re.IGNORECASE)
+ if match:
+ metrics['auc'] = float(match.group(1))
+
+ # Debug output if metrics not found
+ if metrics['acc'] is None or metrics['auc'] is None:
+ print("\n ⚠️ Warning: Could not parse all metrics from output")
+ print(f" Found ACC: {metrics['acc']}, AUC: {metrics['auc']}")
+ print(" Last 10 lines of output:")
+ for line in lines[-10:]:
+ if line.strip():
+ print(f" {line}")
+
+ return metrics
+
+def run_all_evaluations(limit=None):
+ """Run evaluations for all variants and datasets"""
+ print("\n" + "="*80)
+ print("RUNNING EVALUATIONS")
+ print("="*80)
+
+ all_results = {}
+
+ # Calculate total number of evaluations
+ total_evals = len(VARIANTS) * len(DATASETS)
+ current_eval = 0
+
+ for variant_idx, (variant_name, variant_config) in enumerate(VARIANTS.items(), 1):
+ all_results[variant_name] = {}
+
+ print(f"\n{'='*80}")
+ print(f"VARIANT {variant_idx}/{len(VARIANTS)}: {variant_name}")
+ print(f"{'='*80}")
+
+ for dataset_idx, dataset_name in enumerate(DATASETS.keys(), 1):
+ current_eval += 1
+ overall_progress = (current_eval / total_evals) * 100
+
+ print(f"\n[Overall: {current_eval}/{total_evals} - {overall_progress:.1f}%]")
+ print(f"[Variant: {variant_idx}/{len(VARIANTS)}] [{variant_name}]")
+ print(f"[Dataset: {dataset_idx}/{len(DATASETS)}] [{dataset_name}]")
+
+ metrics = run_evaluation(dataset_name, variant_name, variant_config, limit=limit)
+ all_results[variant_name][dataset_name] = metrics
+
+ if metrics:
+ acc_str = f"{metrics['acc']*100:.1f}%" if metrics['acc'] is not None else "N/A"
+ auc_str = f"{metrics['auc']*100:.1f}%" if metrics['auc'] is not None else "N/A"
+ print(f" ✓ ACC: {acc_str}, AUC: {auc_str}")
+ else:
+ print(f" ✗ Failed to get results")
+
+ print(f"\n{'='*80}")
+ print(f"✓ ALL EVALUATIONS COMPLETE ({total_evals}/{total_evals})")
+ print(f"{'='*80}")
+
+ return all_results
+
+def compile_table2(all_results):
+ """
+ Compile all results into Table 2 format
+ """
+ print("\n" + "="*80)
+ print("TABLE 2: Ablation test results")
+ print("Format: ACC(%)/AUC(%)")
+ print("="*80)
+
+ # Create table data
+ table_data = []
+
+ for variant_name in ["S_spatial", "S_optical", "S_optical_no_cp", "AIGVDet"]:
+ row = {"Variants": variant_name}
+
+ acc_values = []
+ auc_values = []
+
+ for dataset_name in ["moonvalley", "videocraft", "pika", "neverends"]:
+ metrics = all_results.get(variant_name, {}).get(dataset_name)
+
+ if metrics:
+ acc_pct = metrics['acc'] * 100 if metrics['acc'] is not None else None
+ auc_pct = metrics['auc'] * 100 if metrics['auc'] is not None else None
+
+ acc_str = f"{acc_pct:.1f}" if acc_pct is not None else "N/A"
+ auc_str = f"{auc_pct:.1f}" if auc_pct is not None else "N/A"
+
+ result_str = f"{acc_str}/{auc_str}"
+
+ if acc_pct is not None: acc_values.append(acc_pct)
+ if auc_pct is not None: auc_values.append(auc_pct)
+ else:
+ result_str = "N/A"
+
+ # Map dataset name to column name
+ column_name = {
+ "moonvalley": "Moonvalley",
+ "videocraft": "VideoCraft",
+ "pika": "Pika",
+ "neverends": "NeverEnds"
+ }[dataset_name]
+
+ row[column_name] = result_str
+
+ # Calculate average
+ avg_acc_str = f"{sum(acc_values) / len(acc_values):.1f}" if acc_values else "N/A"
+ avg_auc_str = f"{sum(auc_values) / len(auc_values):.1f}" if auc_values else "N/A"
+ row["Average"] = f"{avg_acc_str}/{avg_auc_str}"
+
+ table_data.append(row)
+
+ # Create DataFrame
+ df = pd.DataFrame(table_data)
+ df = df[["Variants", "Moonvalley", "VideoCraft", "Pika", "NeverEnds", "Average"]]
+
+ # Display table
+ print("\n" + df.to_string(index=False))
+ print("\n" + "="*80)
+
+ # Save to CSV
+ output_path = RESULTS_DIR / "table2_recreation.csv"
+ df.to_csv(output_path, index=False)
+ print(f"\n✓ Table saved to: {output_path}")
+
+ return df
+
+def main():
+ parser = argparse.ArgumentParser(description="Recreate Table 2 from AIGVDet paper")
+ parser.add_argument("--skip-checks", action="store_true", help="Skip prerequisite checks")
+ parser.add_argument("--limit", type=int, default=None, help="Limit number of videos per class for quick testing")
+ args = parser.parse_args()
+
+ print("="*80)
+ print("RECREATING TABLE 2 FROM AI-GENERATED VIDEO DETECTION PAPER")
+ print("="*80)
+ print("\nThis script will:")
+ print("1. Check prerequisites (data and models)")
+ print("2. Run evaluations for all variants on all datasets")
+ print("3. Compile results into Table 2 format")
+
+ if args.limit:
+ print(f"⚠️ QUICK MODE: Limiting to {args.limit} videos per class")
+ else:
+ print("\nEstimated time: 30-60 minutes depending on dataset sizes")
+ print("="*80)
+
+ # Check prerequisites
+ if not args.skip_checks:
+ if not check_prerequisites():
+ print("\n❌ Prerequisites not satisfied. Please fix the issues above.")
+ return
+
+ # Run all evaluations
+ all_results = run_all_evaluations(limit=args.limit)
+
+ # Compile and display Table 2
+ table = compile_table2(all_results)
+
+ print("\n" + "="*80)
+ print("✓ TABLE 2 RECREATION COMPLETE!")
+ print("="*80)
+ print(f"\nResults saved to: {RESULTS_DIR}")
+ print("\nFiles generated:")
+ print(f" - table2_recreation.csv (summary table)")
+ print(f" - [dataset]_[variant]_video.csv (per-video results)")
+ print(f" - [dataset]_[variant]_frame.csv (per-frame results)")
+
+if __name__ == "__main__":
+ main()
diff --git a/run_gui.bat b/run_gui.bat
new file mode 100644
index 0000000..6157fa2
--- /dev/null
+++ b/run_gui.bat
@@ -0,0 +1,5 @@
+@echo off
+echo Starting AIGVDet GUI...
+echo Ensure you have installed requirements: pip install streamlit
+streamlit run gui_app.py
+pause
diff --git a/setup_test_data.py b/setup_test_data.py
new file mode 100644
index 0000000..701a101
--- /dev/null
+++ b/setup_test_data.py
@@ -0,0 +1,254 @@
+"""
+Simple script to download and extract test data for AIGVDet evaluation
+Google Drive folder: https://drive.google.com/drive/u/3/folders/1gSAUUqYK33262aukdTjZIxUuGrgU8REU
+
+This script:
+1. Downloads the test folder from Google Drive using gdown
+2. Extracts all zip files to the correct locations
+3. Organizes data structure for recreate_table2_final.py
+"""
+
+import os
+import subprocess
+import zipfile
+from pathlib import Path
+import sys
+import shutil
+
+# Google Drive folder link
+GDRIVE_FOLDER = "https://drive.google.com/drive/folders/1D1jm1_HCu0Nv21NVjuyL1CB5gF5sy0hx"
+
+# Paths
+DATA_DIR = Path("data")
+TEST_DIR = DATA_DIR / "test"
+TEMP_DIR = DATA_DIR / "temp_download"
+
+def install_gdown():
+ """Install gdown if not already installed"""
+ print("Checking for gdown...")
+ try:
+ import gdown
+ print("✓ gdown is already installed")
+ return True
+ except ImportError:
+ print("Installing gdown...")
+ try:
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "gdown"])
+ print("✓ gdown installed successfully")
+ return True
+ except Exception as e:
+ print(f"❌ Failed to install gdown: {e}")
+ return False
+
+def download_data():
+ """Download test data from Google Drive"""
+ print(f"\n{'='*80}")
+ print("DOWNLOADING TEST DATA")
+ print(f"{'='*80}")
+ print(f"Source: {GDRIVE_FOLDER}")
+ print(f"Destination: {TEMP_DIR}")
+
+ # Create temp directory
+ TEMP_DIR.mkdir(parents=True, exist_ok=True)
+
+ # Check if files already exist
+ existing_zips = list(TEMP_DIR.rglob("*.zip"))
+ if existing_zips:
+ print(f"\n⚠️ Found {len(existing_zips)} existing zip files in {TEMP_DIR}")
+ print("Skipping download. If you want to re-download, delete the temp_download folder first.")
+ return True
+
+ # Try method 1: gdown with cookies
+ try:
+ import gdown
+ print("\nMethod 1: Trying download with cookies authentication...")
+ gdown.download_folder(GDRIVE_FOLDER, output=str(TEMP_DIR), quiet=False, use_cookies=True)
+ print("\n✓ Download complete")
+ return True
+ except Exception as e:
+ print(f"\n⚠️ Method 1 failed: {e}")
+
+ # Try method 2: gdown without cookies
+ try:
+ import gdown
+ print("\nMethod 2: Trying download without cookies...")
+ gdown.download_folder(GDRIVE_FOLDER, output=str(TEMP_DIR), quiet=False, use_cookies=False)
+ print("\n✓ Download complete")
+ return True
+ except Exception as e:
+ print(f"\n⚠️ Method 2 failed: {e}")
+
+ # Try method 3: Command line with cookies
+ try:
+ print("\nMethod 3: Trying command line with cookies...")
+ cmd = ["gdown", "--folder", GDRIVE_FOLDER, "-O", str(TEMP_DIR), "--use-cookies"]
+ subprocess.run(cmd, check=True)
+ print("\n✓ Download complete")
+ return True
+ except Exception as e:
+ print(f"\n⚠️ Method 3 failed: {e}")
+
+ # All methods failed
+ print("\n" + "="*80)
+ print("❌ AUTOMATIC DOWNLOAD FAILED - MANUAL DOWNLOAD REQUIRED")
+ print("="*80)
+ print("\nGoogle Drive has rate-limited downloads. Please download manually:")
+ print(f"\n1. Open in browser: {GDRIVE_FOLDER}")
+ print("2. Download all files/folders")
+ print(f"3. Extract to: {TEMP_DIR.absolute()}")
+ print("4. Run this script again")
+ print("\nAlternatively, wait 24 hours and try again.")
+ print("="*80)
+ return False
+
+def extract_zip(zip_path, extract_to):
+ """Extract a single zip file"""
+ print(f" Extracting: {zip_path.name} → {extract_to.name}")
+ try:
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
+ zip_ref.extractall(extract_to)
+ return True
+ except Exception as e:
+ print(f" ❌ Error: {e}")
+ return False
+
+def organize_data():
+ """Extract and organize all zip files"""
+ print(f"\n{'='*80}")
+ print("EXTRACTING AND ORGANIZING DATA")
+ print(f"{'='*80}")
+
+ # Create target directories
+ optical_base = TEST_DIR / "T2V"
+ rgb_base = TEST_DIR / "original" / "T2V"
+ optical_base.mkdir(parents=True, exist_ok=True)
+ rgb_base.mkdir(parents=True, exist_ok=True)
+
+ # Find all zip files in temp directory
+ all_zips = list(TEMP_DIR.rglob("*.zip"))
+
+ if not all_zips:
+ print("❌ No zip files found in downloaded data")
+ return False
+
+ print(f"\nFound {len(all_zips)} zip files")
+
+ # Process optical flow zips
+ print("\n1. Processing optical flow data...")
+ optical_zips = [z for z in all_zips if "-optical" in z.name.lower()]
+ for zip_file in optical_zips:
+ # Extract dataset name (e.g., "videocraft" from "videocraft-optical.zip")
+ dataset_name = zip_file.stem.replace("-optical", "").replace("-Optical", "")
+ target_dir = optical_base / dataset_name
+ target_dir.mkdir(parents=True, exist_ok=True)
+ extract_zip(zip_file, target_dir)
+
+ # Process RGB zips
+ print("\n2. Processing RGB data...")
+ rgb_zips = [z for z in all_zips if "-rgb" in z.name.lower()]
+ for zip_file in rgb_zips:
+ # Extract dataset name (e.g., "videocraft" from "videocraft-rgb.zip")
+ dataset_name = zip_file.stem.replace("-rgb", "").replace("-RGB", "")
+ target_dir = rgb_base / dataset_name
+ target_dir.mkdir(parents=True, exist_ok=True)
+ extract_zip(zip_file, target_dir)
+
+ print("\n✓ Extraction complete")
+ return True
+
+def verify_structure():
+ """Verify the extracted data structure"""
+ print(f"\n{'='*80}")
+ print("VERIFYING DATA STRUCTURE")
+ print(f"{'='*80}")
+
+ datasets = ["moonvalley", "videocraft", "pika", "neverends"]
+ all_good = True
+
+ print("\nOptical flow data:")
+ for dataset in datasets:
+ path = TEST_DIR / "T2V" / dataset
+ if path.exists():
+ real = (path / "0_real").exists()
+ fake = (path / "1_fake").exists()
+ status = f"[Real: {'✓' if real else '✗'}, Fake: {'✓' if fake else '✗'}]"
+ print(f" {'✓' if (real and fake) else '⚠️ '} {dataset}: {status}")
+ if not (real and fake):
+ all_good = False
+ else:
+ print(f" ❌ {dataset}: NOT FOUND")
+ all_good = False
+
+ print("\nRGB data:")
+ for dataset in datasets:
+ path = TEST_DIR / "original" / "T2V" / dataset
+ if path.exists():
+ real = (path / "0_real").exists()
+ fake = (path / "1_fake").exists()
+ status = f"[Real: {'✓' if real else '✗'}, Fake: {'✓' if fake else '✗'}]"
+ print(f" {'✓' if (real and fake) else '⚠️ '} {dataset}: {status}")
+ if not (real and fake):
+ all_good = False
+ else:
+ print(f" ❌ {dataset}: NOT FOUND")
+ all_good = False
+
+ return all_good
+
+def cleanup():
+ """Remove temporary files"""
+ print(f"\n{'='*80}")
+ print("CLEANUP")
+ print(f"{'='*80}")
+
+ if TEMP_DIR.exists():
+ try:
+ shutil.rmtree(TEMP_DIR)
+ print(f"✓ Removed temporary directory: {TEMP_DIR}")
+ except Exception as e:
+ print(f"⚠️ Could not remove temp directory: {e}")
+ print(f"You can manually delete: {TEMP_DIR}")
+
+def main():
+ print("="*80)
+ print("AIGVDET TEST DATA SETUP")
+ print("="*80)
+
+ # Step 1: Install gdown
+ if not install_gdown():
+ print("\n❌ Setup failed: Could not install gdown")
+ return
+
+ # Step 2: Download data
+ if not download_data():
+ print("\n❌ Setup failed: Could not download data")
+ return
+
+ # Step 3: Extract and organize
+ if not organize_data():
+ print("\n❌ Setup failed: Could not extract data")
+ return
+
+ # Step 4: Verify structure
+ success = verify_structure()
+
+ # Step 5: Cleanup
+ cleanup()
+
+ # Final message
+ print(f"\n{'='*80}")
+ if success:
+ print("✓ SETUP COMPLETE!")
+ print("="*80)
+ print("\nAll test data is ready!")
+ print("\nNext step:")
+ print(" python recreate_table2_final.py")
+ else:
+ print("⚠️ SETUP COMPLETED WITH WARNINGS")
+ print("="*80)
+ print("\nSome data may be missing. Please check the warnings above.")
+ print("You may need to manually extract some files.")
+ print("="*80)
+
+if __name__ == "__main__":
+ main()
diff --git a/test.py b/test.py
index 18a8e61..804e1cc 100644
--- a/test.py
+++ b/test.py
@@ -171,7 +171,7 @@
prob = model_or(in_tens).sigmoid().item()
original_prob_sum+=prob
- df1 = df1.append({'original_path': img_path, 'original_pro': prob , 'flag':flag}, ignore_index=True)
+ df1 = pd.concat([df1, pd.DataFrame([{'original_path': img_path, 'original_pro': prob , 'flag':flag}])], ignore_index=True)
original_predict=original_prob_sum/len(original_file_list)
@@ -218,7 +218,7 @@
p+=1
if predict>=args.threshold:
tp+=1
- df = df.append({'name': subsubfolder_name, 'pro': predict , 'flag':flag ,'optical_pro':optical_predict,'original_pro':original_predict}, ignore_index=True)
+ df = pd.concat([df, pd.DataFrame([{'name': subsubfolder_name, 'pro': predict , 'flag':flag ,'optical_pro':optical_predict,'original_pro':original_predict}])], ignore_index=True)
else:
print("Subfolder does not exist:", original_subfolder_path)
# r_acc = accuracy_score(y_true[y_true == 0], y_pred[y_true == 0] > args.threshold)
diff --git a/test_single_stream.py b/test_single_stream.py
new file mode 100644
index 0000000..d401104
--- /dev/null
+++ b/test_single_stream.py
@@ -0,0 +1,333 @@
+"""
+Modified test.py that supports single-stream evaluation AND flat directory structures.
+Supports: RGB-only (Sspatial), Optical-only (Soptical), or Fused (AIGVDet)
+"""
+import argparse
+import glob
+import os
+import pandas as pd
+import re
+
+import torch
+import torch.nn
+import torchvision.transforms as transforms
+import torchvision.transforms.functional as TF
+from PIL import Image
+from tqdm import tqdm
+
+from core.utils1.utils import get_network, str2bool, to_cuda
+from sklearn.metrics import accuracy_score, average_precision_score, roc_auc_score
+
+def get_video_name_from_filename(filename):
+ # Assumes format: video_name_XXXXX.png
+ # We split by underscore and take everything except the last part (frame number)
+ parts = os.path.basename(filename).rsplit('_', 1)
+ if len(parts) > 1:
+ return parts[0]
+ return "unknown_video"
+
+if __name__=="__main__":
+
+ parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+ parser.add_argument("-fop", "--folder_optical_flow_path", default="data/test/T2V/videocraft", type=str)
+ parser.add_argument("-for", "--folder_original_path", default="data/test/original/T2V/videocraft", type=str)
+ parser.add_argument("-mop", "--model_optical_flow_path", type=str, default="checkpoints/optical.pth")
+ parser.add_argument("-mor", "--model_original_path", type=str, default="checkpoints/original.pth")
+ parser.add_argument("--eval_mode", type=str, choices=["fused", "rgb_only", "optical_only"], default="fused")
+ parser.add_argument("-t", "--threshold", type=float, default=0.5)
+ parser.add_argument("-e", "--excel_path", type=str, default="data/results/result.csv")
+ parser.add_argument("-ef", "--excel_frame_path", type=str, default="data/results/frame_result.csv")
+ parser.add_argument("--use_cpu", action="store_true")
+ parser.add_argument("--arch", type=str, default="resnet50")
+ parser.add_argument("--aug_norm", type=str2bool, default=True)
+ parser.add_argument("--no_crop", action="store_true")
+ parser.add_argument("--limit", type=int, default=None, help="Limit number of videos per class (optional)")
+
+ args = parser.parse_args()
+
+ # Load models
+ if args.eval_mode in ["fused", "optical_only"]:
+ print(f"Loading optical flow model: {args.model_optical_flow_path}")
+ model_op = get_network(args.arch)
+ state_dict = torch.load(args.model_optical_flow_path, map_location="cpu")
+ if "model" in state_dict: state_dict = state_dict["model"]
+ model_op.load_state_dict(state_dict)
+ model_op.eval()
+ if not args.use_cpu: model_op.cuda()
+ else: model_op = None
+
+ if args.eval_mode in ["fused", "rgb_only"]:
+ print(f"Loading RGB model: {args.model_original_path}")
+ model_or = get_network(args.arch)
+ state_dict = torch.load(args.model_original_path, map_location="cpu")
+ if "model" in state_dict: state_dict = state_dict["model"]
+ model_or.load_state_dict(state_dict)
+ model_or.eval()
+ if not args.use_cpu: model_or.cuda()
+ else: model_or = None
+
+ if args.no_crop:
+ trans = transforms.Compose((transforms.ToTensor(),))
+ else:
+ trans = transforms.Compose((transforms.CenterCrop((448,448)), transforms.ToTensor(),))
+
+ print("*" * 50)
+ print(f"Evaluation Mode: {args.eval_mode}")
+ print("*" * 50)
+
+ flag=0
+ p=0; n=0; tp=0; tn=0
+ y_true=[]; y_pred=[]
+ y_pred_original=[]; y_pred_optical=[]
+
+ df = pd.DataFrame(columns=['name', 'pro','flag','optical_pro','original_pro'])
+ df1 = pd.DataFrame(columns=['original_path', 'original_pro','optical_path','optical_pro','flag'])
+ index1=0
+
+ # Check if standard structure (0_real/1_fake) exists
+ has_standard_structure = os.path.exists(os.path.join(args.folder_original_path, "1_fake")) or \
+ os.path.exists(os.path.join(args.folder_optical_flow_path, "1_fake"))
+
+ if has_standard_structure:
+ print("Detected standard folder structure (0_real/1_fake)")
+ subfolders = ["0_real", "1_fake"]
+ else:
+ print("Detected FLAT folder structure (treating all as 1_fake)")
+ subfolders = ["flat_fake"]
+
+ for subfolder_name in subfolders:
+ if subfolder_name == "0_real":
+ flag = 0
+ current_label_path = "0_real"
+ elif subfolder_name == "1_fake":
+ flag = 1
+ current_label_path = "1_fake"
+ else:
+ flag = 1 # Flat structure assumed to be fake/generated videos
+ current_label_path = "" # Root dir
+
+ optical_subfolder_path = os.path.join(args.folder_optical_flow_path, current_label_path)
+ original_subfolder_path = os.path.join(args.folder_original_path, current_label_path)
+
+ # Get list of videos
+ # In flat structure, we need to group images by video prefix
+ video_groups = {}
+
+ if args.eval_mode != "optical_only":
+ # Scan RGB folder
+ if os.path.exists(original_subfolder_path):
+ files = sorted(glob.glob(os.path.join(original_subfolder_path, "*.png")) +
+ glob.glob(os.path.join(original_subfolder_path, "*.jpg")) +
+ glob.glob(os.path.join(original_subfolder_path, "*.JPEG")))
+
+ # If files found directly, it's flat structure
+ if len(files) > 0 and not os.path.isdir(files[0]):
+ for f in files:
+ vname = get_video_name_from_filename(f)
+ if vname not in video_groups: video_groups[vname] = []
+ video_groups[vname].append(f)
+ else:
+ # Standard structure: folders are videos
+ try:
+ video_list = os.listdir(original_subfolder_path)
+ for v in video_list:
+ v_path = os.path.join(original_subfolder_path, v)
+ if os.path.isdir(v_path):
+ frames = sorted(glob.glob(os.path.join(v_path, "*")))
+ if frames:
+ video_groups[v] = frames
+ except Exception as e:
+ print(f"Error listing directory: {e}")
+
+ # If optical_only, we MUST scan the optical folder
+ if args.eval_mode == "optical_only":
+ if os.path.exists(optical_subfolder_path):
+ files = sorted(glob.glob(os.path.join(optical_subfolder_path, "*.png")) +
+ glob.glob(os.path.join(optical_subfolder_path, "*.jpg")) +
+ glob.glob(os.path.join(optical_subfolder_path, "*.JPEG")))
+
+ if len(files) > 0 and not os.path.isdir(files[0]):
+ for f in files:
+ vname = get_video_name_from_filename(f)
+ if vname not in video_groups: video_groups[vname] = []
+ video_groups[vname].append(f)
+ else:
+ try:
+ video_list = os.listdir(optical_subfolder_path)
+ for v in video_list:
+ v_path = os.path.join(optical_subfolder_path, v)
+ if os.path.isdir(v_path):
+ frames = sorted(glob.glob(os.path.join(v_path, "*")))
+ if frames:
+ video_groups[v] = frames
+ except Exception as e:
+ print(f"Error listing directory: {e}")
+
+ # If optical only or fused, we might need to check optical folder too
+ # But usually RGB folder structure defines the videos.
+
+ print(f"Found {len(video_groups)} videos in {subfolder_name}")
+
+ video_list = list(video_groups.items())
+
+ # Apply limit if specified
+ if args.limit is not None and len(video_list) > args.limit:
+ print(f"⚠️ Limiting to first {args.limit} videos (out of {len(video_list)})")
+ video_list = video_list[:args.limit]
+
+ for idx, (video_name, frames) in enumerate(video_list, 1):
+ progress_pct = (idx / len(video_list)) * 100
+ print(f"\r[{idx}/{len(video_list)} - {progress_pct:.1f}%] Processing: {video_name[:50]}...", end='', flush=True)
+
+ # Detect RGB stream
+ original_predict = 0
+ if args.eval_mode in ["fused", "rgb_only"] and model_or is not None:
+ original_prob_sum=0
+ count = 0
+ for img_path in frames:
+ try:
+ img = Image.open(img_path).convert("RGB")
+ img = trans(img)
+ if args.aug_norm:
+ img = TF.normalize(img, mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ in_tens = img.unsqueeze(0)
+ if not args.use_cpu: in_tens = in_tens.cuda()
+
+ with torch.no_grad():
+ prob = model_or(in_tens).sigmoid().item()
+ original_prob_sum+=prob
+
+ df1 = pd.concat([df1, pd.DataFrame([{'original_path': img_path, 'original_pro': prob , 'flag':flag}])], ignore_index=True)
+ count += 1
+ except Exception as e:
+ print(f"Error processing RGB frame {img_path}: {e}")
+
+ if count > 0:
+ original_predict = original_prob_sum/count
+
+ # Detect Optical Flow stream
+ optical_predict = 0
+ if args.eval_mode in ["fused", "optical_only"] and model_op is not None:
+ # Construct optical flow paths
+ # In flat structure: optical_path/video_name_XXXX.png
+ # In standard: optical_path/video_name/frame_XXXX.png
+
+ optical_prob_sum=0
+ count = 0
+
+ # We iterate through the SAME frames as RGB to ensure alignment
+ # But we need to find the corresponding optical flow file
+ for img_path in frames:
+ basename = os.path.basename(img_path)
+
+ # Construct optical flow path
+ # Try standard path: .../1_fake/video_name/frame.png
+ opt_path_standard = os.path.join(optical_subfolder_path, video_name, basename)
+ # Try flat path: .../1_fake/frame.png
+ opt_path_flat = os.path.join(optical_subfolder_path, basename)
+
+ if os.path.exists(opt_path_standard):
+ opt_path = opt_path_standard
+ elif os.path.exists(opt_path_flat):
+ opt_path = opt_path_flat
+ else:
+ # Fallback to standard for error reporting, or try root if flat structure
+ if current_label_path == "":
+ opt_path = os.path.join(optical_subfolder_path, basename)
+ else:
+ opt_path = opt_path_standard
+
+ if os.path.exists(opt_path):
+ try:
+ img = Image.open(opt_path).convert("RGB")
+ img = trans(img)
+ if args.aug_norm:
+ img = TF.normalize(img, mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
+ in_tens = img.unsqueeze(0)
+ if not args.use_cpu: in_tens = in_tens.cuda()
+
+ with torch.no_grad():
+ prob = model_op(in_tens).sigmoid().item()
+ optical_prob_sum+=prob
+
+ df1.loc[index1, 'optical_path'] = opt_path
+ df1.loc[index1, 'optical_pro'] = prob
+ index1+=1
+ count+=1
+ except Exception as e:
+ print(f"Error processing Flow frame {opt_path}: {e}")
+ index1+=1
+ else:
+ # Flow frame missing
+ index1+=1
+
+ if count > 0:
+ optical_predict = optical_prob_sum/count
+
+ # Final Prediction
+ if args.eval_mode == "fused":
+ predict = original_predict * 0.5 + optical_predict * 0.5
+ elif args.eval_mode == "rgb_only":
+ predict = original_predict
+ else:
+ predict = optical_predict
+
+ y_true.append(flag)
+ y_pred.append(predict)
+ y_pred_original.append(original_predict)
+ y_pred_optical.append(optical_predict)
+
+ if flag==0:
+ n+=1
+ if predict=args.threshold: tp+=1
+
+ df = pd.concat([df, pd.DataFrame([{'name': video_name, 'pro': predict , 'flag':flag ,
+ 'optical_pro':optical_predict,'original_pro':original_predict}])], ignore_index=True)
+
+ # Print newline after completing subfolder
+ print(f"\n✓ Completed {subfolder_name}: {len(video_list)} videos processed")
+
+ # Metrics
+ # Metrics
+ try:
+ if len(y_true) == 0:
+ print("Error: No videos were processed. Cannot calculate metrics.")
+ ap = 0.0; auc = 0.0; acc = 0.0
+ elif len(set(y_true)) > 1:
+ ap = average_precision_score(y_true, y_pred)
+ auc = roc_auc_score(y_true,y_pred)
+ acc = accuracy_score(y_true, [1 if p >= args.threshold else 0 for p in y_pred])
+ else:
+ ap = 0.0; auc = 0.0
+ # Calculate accuracy even if only one class
+ acc = accuracy_score(y_true, [1 if p >= args.threshold else 0 for p in y_pred])
+ print(f"Warning: Only one class present (Class {y_true[0]}). AUC/AP cannot be calculated.")
+ except Exception as e:
+ print(f"Error calculating metrics: {e}")
+ ap=0.0; auc=0.0; acc=0.0
+
+ print("-" * 30)
+ print(f"Evaluation Mode: {args.eval_mode}")
+ print(f"tnr: {tn/n if n > 0 else 0:.4f}")
+ print(f"tpr: {tp/p if p > 0 else 0:.4f}")
+ print(f"acc: {acc:.4f}")
+ print(f"auc: {auc:.4f}")
+ print("-" * 30)
+
+ # Save results
+ csv_folder = os.path.dirname(args.excel_path)
+ if not os.path.exists(csv_folder): os.makedirs(csv_folder)
+
+ if not os.path.exists(args.excel_path): df.to_csv(args.excel_path, index=False)
+ else: df.to_csv(args.excel_path, mode='a', header=False, index=False)
+
+ csv_folder1 = os.path.dirname(args.excel_frame_path)
+ if not os.path.exists(csv_folder1): os.makedirs(csv_folder1)
+
+ if not os.path.exists(args.excel_frame_path): df1.to_csv(args.excel_frame_path, index=False)
+ else: df1.to_csv(args.excel_frame_path, mode='a', header=False, index=False)
+
+ print(f"Results saved to {args.excel_path}")
diff --git a/train.py b/train.py
index a87f639..956099f 100644
--- a/train.py
+++ b/train.py
@@ -121,6 +121,29 @@
val_writer.add_scalar("TPR", val_results["TPR"], trainer.total_steps)
val_writer.add_scalar("TNR", val_results["TNR"], trainer.total_steps)
+ # Log validation metrics to wandb
+ "saving the latest model %s (epoch %d, model.total_steps %d)\n"
+ % (cfg.exp_name, epoch, trainer.total_steps)
+ )
+ trainer.save_networks("latest")
+
+ if epoch % cfg.save_epoch_freq == 0:
+ print(f"💾 Saving epoch checkpoint {epoch+1}")
+ log.write("saving the model at the end of epoch %d, iters %d\n" % (epoch, trainer.total_steps))
+ trainer.save_networks("latest")
+ trainer.save_networks(epoch)
+
+ # Validation
+ print("\n🔍 Running validation...")
+ trainer.eval()
+ val_results = validate(trainer.model, val_cfg)
+ val_writer.add_scalar("AP", val_results["AP"], trainer.total_steps)
+ val_writer.add_scalar("ACC", val_results["ACC"], trainer.total_steps)
+ # add
+ val_writer.add_scalar("AUC", val_results["AUC"], trainer.total_steps)
+ val_writer.add_scalar("TPR", val_results["TPR"], trainer.total_steps)
+ val_writer.add_scalar("TNR", val_results["TNR"], trainer.total_steps)
+
# Log validation metrics to wandb
wandb.log({
"val/AP": val_results["AP"],
From 0593026ee0a31ee41204f9463b4e26c56d82a3ff Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Sat, 6 Dec 2025 01:11:19 +0800
Subject: [PATCH 52/55] feat: Add scripts for local and Docker GUI execution on
Windows and Linux, and update Dockerfile with GUI app and dependencies.
---
Dockerfile.gpu-alt | 7 +++++--
run_gui.bat | 13 ++++++++++++-
run_gui.sh | 18 ++++++++++++++++++
run_gui_docker.bat | 14 ++++++++++++++
run_gui_docker.sh | 14 ++++++++++++++
5 files changed, 63 insertions(+), 3 deletions(-)
create mode 100644 run_gui.sh
create mode 100644 run_gui_docker.bat
create mode 100644 run_gui_docker.sh
diff --git a/Dockerfile.gpu-alt b/Dockerfile.gpu-alt
index 2b23046..52031c7 100644
--- a/Dockerfile.gpu-alt
+++ b/Dockerfile.gpu-alt
@@ -32,7 +32,7 @@ COPY pyproject.toml ./
COPY README.md ./
COPY core/ ./core/
COPY networks/ ./networks/
-COPY train.py test.py demo.py download_data.py ./
+COPY train.py test.py demo.py download_data.py gui_app.py ./
# Create directories for data and checkpoints
RUN mkdir -p /app/data /app/checkpoints /app/raft_model
@@ -52,10 +52,13 @@ RUN uv pip install --system \
tensorboardX \
tqdm \
"blobfile>=1.0.5" \
- wandb \
+ "wandb==0.16.6" \
python-dotenv \
gdown
+# Install pyarrow with a version that has pre-built wheels, then streamlit
+RUN uv pip install --system "pyarrow>=14.0.0,<15.0.0" streamlit
+
# Install torchvision separately (base image has torch but may need torchvision update)
RUN pip3 install torchvision==0.15.1+cu117 \
--index-url https://download.pytorch.org/whl/cu117 || \
diff --git a/run_gui.bat b/run_gui.bat
index 6157fa2..c2011a1 100644
--- a/run_gui.bat
+++ b/run_gui.bat
@@ -1,5 +1,16 @@
@echo off
echo Starting AIGVDet GUI...
-echo Ensure you have installed requirements: pip install streamlit
+echo.
+echo Checking if streamlit is installed...
+python -c "import streamlit" 2>nul
+if errorlevel 1 (
+ echo Streamlit not found. Installing required packages...
+ pip install streamlit torch torchvision opencv-python numpy pillow natsort tqdm
+)
+
+echo.
+echo Starting GUI on http://localhost:8501
+echo Press Ctrl+C to stop
+echo.
streamlit run gui_app.py
pause
diff --git a/run_gui.sh b/run_gui.sh
new file mode 100644
index 0000000..9581029
--- /dev/null
+++ b/run_gui.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+# Run AIGVDet GUI locally (without Docker)
+
+echo "Starting AIGVDet GUI..."
+echo ""
+
+# Check if streamlit is installed
+if ! python -c "import streamlit" 2>/dev/null; then
+ echo "Streamlit not found. Installing required packages..."
+ pip install streamlit torch torchvision opencv-python numpy pillow natsort tqdm
+fi
+
+echo ""
+echo "Starting GUI on http://localhost:8501"
+echo "Press Ctrl+C to stop"
+echo ""
+
+streamlit run gui_app.py
diff --git a/run_gui_docker.bat b/run_gui_docker.bat
new file mode 100644
index 0000000..95f59ae
--- /dev/null
+++ b/run_gui_docker.bat
@@ -0,0 +1,14 @@
+@echo off
+REM Run AIGVDet GUI in Docker (Windows)
+
+echo Starting AIGVDet GUI...
+echo Installing Streamlit and starting server...
+echo Access the GUI at: http://localhost:8501
+echo.
+
+docker run --gpus all -p 8501:8501 -it ^
+ -v %cd%/output_data:/app/output_data ^
+ -v %cd%/checkpoints:/app/checkpoints ^
+ -v %cd%/raft-model:/app/raft-model ^
+ sacdalance/thesis-aigvdet:latest-gpu ^
+ streamlit run gui_app.py --server.address=0.0.0.0
diff --git a/run_gui_docker.sh b/run_gui_docker.sh
new file mode 100644
index 0000000..7e0223b
--- /dev/null
+++ b/run_gui_docker.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+# Run AIGVDet GUI in Docker
+
+echo "Starting AIGVDet GUI..."
+echo "Installing Streamlit and starting server..."
+echo "Access the GUI at: http://localhost:8501"
+echo ""
+
+docker run --gpus all -p 8501:8501 -it \
+ -v $(pwd)/output_data:/app/output_data \
+ -v $(pwd)/checkpoints:/app/checkpoints \
+ -v $(pwd)/raft-model:/app/raft-model \
+ sacdalance/thesis-aigvdet:latest-gpu \
+ streamlit run gui_app.py --server.address=0.0.0.0
From 35e1c5a0475b8bf37ba63b215822ed0d9adda2f3 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Sat, 6 Dec 2025 01:22:48 +0800
Subject: [PATCH 53/55] feat: Add `__contains__` method to RAFT Args object for
compatibility with `in` operator.
---
gui_app.py | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
diff --git a/gui_app.py b/gui_app.py
index 373b032..08f92de 100644
--- a/gui_app.py
+++ b/gui_app.py
@@ -117,12 +117,16 @@ def video_to_frames(video_path, output_folder, progress_bar=None):
# Load RAFT
try:
- # Mock args for RAFT
+ # Args object for RAFT - needs to support 'in' operator
class Args:
- model = raft_model_path
- small = False
- mixed_precision = False
- alternate_corr = False
+ def __init__(self):
+ self.model = raft_model_path
+ self.small = False
+ self.mixed_precision = False
+ self.alternate_corr = False
+
+ def __contains__(self, key):
+ return hasattr(self, key)
args = Args()
model = torch.nn.DataParallel(RAFT(args))
From e702155e87b2b0ed15b2cf5eb15c967852a0d964 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Sat, 6 Dec 2025 02:15:31 +0800
Subject: [PATCH 54/55] feat: Implement model file size validation and display
detailed processing times for video extraction and detection steps.
---
gui_app.py | 55 ++++++++++++++++++++++++++++++++++++++++++++++++------
1 file changed, 49 insertions(+), 6 deletions(-)
diff --git a/gui_app.py b/gui_app.py
index 08f92de..4f7b84e 100644
--- a/gui_app.py
+++ b/gui_app.py
@@ -96,6 +96,15 @@ def video_to_frames(video_path, output_folder, progress_bar=None):
# Model Upload
raft_model_file = st.file_uploader("Upload RAFT Model (raft.pth)", type=['pth'], key="raft_uploader")
+ # Check file size (500MB limit)
+ if raft_model_file:
+ file_size_mb = raft_model_file.size / (1024 * 1024)
+ if file_size_mb > 500:
+ st.error(f"File size ({file_size_mb:.1f} MB) exceeds 500MB limit. Please upload a smaller model.")
+ raft_model_file = None
+ else:
+ st.success(f"Model size: {file_size_mb:.1f} MB")
+
# Video Upload (Batch or Solo)
uploaded_videos = st.file_uploader("Upload Video(s)", type=['mp4', 'avi', 'mov', 'mkv'], accept_multiple_files=True, key="video_uploader")
@@ -103,6 +112,8 @@ def video_to_frames(video_path, output_folder, progress_bar=None):
output_root = st.text_input("Output Directory Root", value="output_data")
if st.button("Start Extraction", key="extract_btn"):
+ extraction_start_time = time.time()
+
if not raft_model_file:
st.error("Please upload the RAFT model.")
elif not uploaded_videos:
@@ -141,6 +152,7 @@ def __contains__(self, key):
total_videos = len(uploaded_videos)
for i, video_file in enumerate(uploaded_videos):
+ video_start_time = time.time()
video_name = video_file.name
st.subheader(f"Processing: {video_name} ({i+1}/{total_videos})")
@@ -156,15 +168,18 @@ def __contains__(self, key):
# 1. Extract Frames
st.write("Extracting frames...")
+ frame_start = time.time()
p_bar = st.progress(0)
images = video_to_frames(video_path, frame_output_dir, p_bar)
- st.write(f"Extracted {len(images)} frames to `{frame_output_dir}`")
+ frame_duration = time.time() - frame_start
+ st.write(f"✓ Extracted {len(images)} frames to `{frame_output_dir}` in {frame_duration:.2f}s")
# 2. Generate Optical Flow
if not os.path.exists(flow_output_dir):
os.makedirs(flow_output_dir)
st.write("Generating Optical Flow...")
+ flow_start = time.time()
images = natsorted(images)
flow_p_bar = st.progress(0)
@@ -182,12 +197,17 @@ def __contains__(self, key):
flow_p_bar.progress((idx + 1) / (len(images) - 1))
- st.write(f"Optical Flow saved to `{flow_output_dir}`")
+ flow_duration = time.time() - flow_start
+ st.write(f"✓ Optical Flow saved to `{flow_output_dir}` in {flow_duration:.2f}s")
+
+ video_duration = time.time() - video_start_time
+ st.info(f"**Video processed in {video_duration:.2f} seconds**")
# Cleanup temp video
os.remove(video_path)
- st.success("All videos processed!")
+ total_extraction_time = time.time() - extraction_start_time
+ st.success(f"✓ All videos processed! Total time: {total_extraction_time:.2f} seconds")
# Cleanup temp model
os.remove(raft_model_path)
@@ -203,8 +223,23 @@ def __contains__(self, key):
col1, col2 = st.columns(2)
with col1:
optical_model_file = st.file_uploader("Upload Optical Flow Model (optical.pth)", type=['pth'], key="opt_uploader")
+ if optical_model_file:
+ opt_size_mb = optical_model_file.size / (1024 * 1024)
+ if opt_size_mb > 500:
+ st.error(f"File size ({opt_size_mb:.1f} MB) exceeds 500MB limit.")
+ optical_model_file = None
+ else:
+ st.success(f"Optical model: {opt_size_mb:.1f} MB")
+
with col2:
original_model_file = st.file_uploader("Upload RGB Model (original.pth)", type=['pth'], key="orig_uploader")
+ if original_model_file:
+ orig_size_mb = original_model_file.size / (1024 * 1024)
+ if orig_size_mb > 500:
+ st.error(f"File size ({orig_size_mb:.1f} MB) exceeds 500MB limit.")
+ original_model_file = None
+ else:
+ st.success(f"RGB model: {orig_size_mb:.1f} MB")
# Input for processed data path
# Default to the output of Tab 1 if available
@@ -278,6 +313,7 @@ def __contains__(self, key):
results = []
for vid_folder in video_folders:
+ video_detect_start = time.time()
st.subheader(f"Analyzing: {vid_folder}")
rgb_path = os.path.join(frames_root, vid_folder)
@@ -294,6 +330,7 @@ def __contains__(self, key):
rgb_prob_sum = 0
rgb_bar = st.progress(0, text="RGB Detection")
+ rgb_start = time.time()
for i, img_path in enumerate(rgb_files):
img = Image.open(img_path).convert("RGB")
@@ -307,8 +344,9 @@ def __contains__(self, key):
rgb_bar.progress((i + 1) / len(rgb_files))
+ rgb_duration = time.time() - rgb_start
rgb_score = rgb_prob_sum / len(rgb_files) if rgb_files else 0
- st.write(f"RGB Score: {rgb_score:.4f}")
+ st.write(f"✓ RGB Score: {rgb_score:.4f} ({len(rgb_files)} frames in {rgb_duration:.2f}s)")
# Optical Flow Detection
opt_files = sorted(glob.glob(os.path.join(opt_path, "*.jpg")) +
@@ -317,6 +355,7 @@ def __contains__(self, key):
opt_prob_sum = 0
opt_bar = st.progress(0, text="Optical Flow Detection")
+ opt_start = time.time()
for i, img_path in enumerate(opt_files):
img = Image.open(img_path).convert("RGB")
@@ -329,17 +368,21 @@ def __contains__(self, key):
opt_prob_sum += prob
opt_bar.progress((i + 1) / len(opt_files))
-
+
+ opt_duration = time.time() - opt_start
opt_score = opt_prob_sum / len(opt_files) if opt_files else 0
- st.write(f"Optical Flow Score: {opt_score:.4f}")
+ st.write(f"✓ Optical Flow Score: {opt_score:.4f} ({len(opt_files)} frames in {opt_duration:.2f}s)")
# Final Decision
final_score = (rgb_score * 0.5) + (opt_score * 0.5)
decision = "FAKE VIDEO (AI-Generated)" if final_score >= threshold else "REAL VIDEO"
color = "red" if final_score >= threshold else "green"
+ video_detect_duration = time.time() - video_detect_start
+
st.markdown(f"### Result: :{color}[{decision}]")
st.write(f"**Combined Probability:** {final_score:.4f}")
+ st.write(f"**Detection Time:** {video_detect_duration:.2f}s")
st.divider()
results.append({
From 960109347fd06b5bf3bc3f538eacd056b2488020 Mon Sep 17 00:00:00 2001
From: Lance Sacdalan <63360390+sacdalance@users.noreply.github.com>
Date: Sat, 6 Dec 2025 02:23:54 +0800
Subject: [PATCH 55/55] feat: Add Streamlit application configuration and
include it in the Docker image.
---
.streamlit/config.toml | 5 +++++
Dockerfile.gpu-alt | 1 +
2 files changed, 6 insertions(+)
create mode 100644 .streamlit/config.toml
diff --git a/.streamlit/config.toml b/.streamlit/config.toml
new file mode 100644
index 0000000..3a5bd47
--- /dev/null
+++ b/.streamlit/config.toml
@@ -0,0 +1,5 @@
+[server]
+maxUploadSize = 500
+
+[browser]
+gatherUsageStats = false
diff --git a/Dockerfile.gpu-alt b/Dockerfile.gpu-alt
index 52031c7..daf0570 100644
--- a/Dockerfile.gpu-alt
+++ b/Dockerfile.gpu-alt
@@ -32,6 +32,7 @@ COPY pyproject.toml ./
COPY README.md ./
COPY core/ ./core/
COPY networks/ ./networks/
+COPY .streamlit/ ./.streamlit/
COPY train.py test.py demo.py download_data.py gui_app.py ./
# Create directories for data and checkpoints