import cv2 import numpy as np import inspect import re import os os.environ.setdefault("NO_ALBUMENTATIONS_UPDATE", "1") import albumentations as A from albumentations import Downscale, Compose, ImageCompression, GaussNoise, MotionBlur, GaussianBlur from torchvision import transforms, datasets import torchvision.transforms.functional as TF from random import random, choice from io import BytesIO from PIL import Image from PIL import ImageFile from PIL import ImageFilter from PIL import ImageOps from PIL import UnidentifiedImageError from scipy.ndimage import gaussian_filter from torchvision.transforms import InterpolationMode import torch ImageFile.LOAD_TRUNCATED_IMAGES = True CLIP_MEAN = [0.48145466, 0.4578275, 0.40821073] CLIP_STD = [0.26862954, 0.26130258, 0.27577711] IMAGENET_MEAN = [0.485, 0.456, 0.406] IMAGENET_STD = [0.229, 0.224, 0.225] def _supports_albu_arg(cls, arg_name): try: return arg_name in inspect.signature(cls.__init__).parameters except Exception: return False def _build_downscale_aug(): """Build Downscale across the Albumentations 1.x and 2.x APIs.""" if _supports_albu_arg(Downscale, "scale_range"): return Downscale( scale_range=(0.25, 0.75), interpolation_pair={ "downscale": cv2.INTER_LINEAR, "upscale": cv2.INTER_LINEAR, }, p=0.5, ) return Downscale( scale_min=0.25, scale_max=0.75, interpolation=cv2.INTER_LINEAR, p=0.5, ) def _build_testshift_like_aug(p_apply=1.0, max_groups=3): # group 1) blur blur_group = A.OneOf([ A.GaussianBlur(sigma_limit=(0.1, 5.0), p=1.0), A.Defocus(radius=(1, 8), alias_blur=(0.1, 0.5), p=1.0), ], p=1.0) # group 2) color distortion color_group = A.OneOf([ A.ChromaticAberration( primary_distortion_limit=(0.01, 0.08), secondary_distortion_limit=(0.01, 0.12), p=1.0, ), A.ColorJitter(brightness=0.0, contrast=0.0, saturation=1.0, hue=0.0, p=1.0), ], p=1.0) # group 3) jpeg if _supports_albu_arg(A.ImageCompression, "quality_range"): jpeg_group = A.ImageCompression(compression_type="jpeg", quality_range=(4, 43), p=1.0) else: jpeg_group = A.ImageCompression(quality_lower=4, quality_upper=43, p=1.0) # group 4) noise if _supports_albu_arg(A.GaussNoise, "std_range"): gauss_noise = A.GaussNoise(std_range=(0.032, 0.100), mean_range=(0.0, 0.0), per_channel=True, p=1.0) else: gauss_noise = A.GaussNoise(var_limit=(0.032 ** 2, 0.100 ** 2), mean=0.0, per_channel=True, p=1.0) if hasattr(A, "SaltAndPepper"): impulse_noise = A.SaltAndPepper(amount=(0.001, 0.03), salt_vs_pepper=(0.45, 0.55), p=1.0) else: impulse_noise = A.ISONoise(color_shift=(0.01, 0.05), intensity=(0.1, 0.5), p=1.0) noise_group = A.OneOf([ gauss_noise, impulse_noise, ], p=1.0) # group 5) brightness brightness_group = A.RandomBrightnessContrast( brightness_limit=(-0.8, 0.8), contrast_limit=(0.0, 0.0), brightness_by_max=True, p=1.0, ) # group 6) spatial distortion spatial_group = A.OneOf([ A.GridDistortion( num_steps=5, distort_limit=(0.05, 0.5), interpolation=cv2.INTER_LINEAR, border_mode=cv2.BORDER_REFLECT_101, p=1.0, ), A.Sequential([ A.ToGray(num_output_channels=3, p=1.0), A.Posterize(num_bits=(3, 5), p=1.0), ], p=1.0), ], p=1.0) # group 7) sharpness / contrast sharp_contrast_group = A.RandomToneCurve(scale=0.2, p=1.0) groups = [ blur_group, color_group, jpeg_group, noise_group, brightness_group, spatial_group, sharp_contrast_group, ] max_groups = max(1, min(int(max_groups), len(groups))) choices = [A.SomeOf(groups, n=n, replace=False, p=1.0) for n in range(1, max_groups + 1)] return A.Compose([ A.OneOf(choices, p=1.0), ], p=float(p_apply)) def dataset_folder(opt, root): if opt.mode == 'binary': return binary_dataset(opt, root) if opt.mode == 'filename': return FileNameDataset(opt, root) raise ValueError('opt.mode needs to be binary or filename.') def _apply_single_robustness(img, robustness_type, params=None): params = dict(params or {}) robustness_type = str(robustness_type or "").strip().lower() if not robustness_type: return img if robustness_type == "jpeg": quality = int(params.get("jpeg_quality", 75) or 75) out = BytesIO() img.save(out, format="JPEG", quality=quality) out.seek(0) with Image.open(out) as aug_img: return aug_img.convert("RGB") if robustness_type == "gaussian_blur": sigma = float(params.get("gaussian_blur_sigma", 1.0) or 1.0) return img.filter(ImageFilter.GaussianBlur(radius=sigma)) if robustness_type == "gaussian_noise": std = float(params.get("gaussian_noise_std", 0.1) or 0.1) img_np = np.asarray(img).astype(np.float32) / 255.0 noise = np.random.normal(loc=0.0, scale=std, size=img_np.shape).astype(np.float32) img_np = np.clip(img_np + noise, 0.0, 1.0) img_np = (img_np * 255.0).round().astype(np.uint8) return Image.fromarray(img_np) if robustness_type == "downsample": scale = float(params.get("downsample_scale", 0.5) or 0.5) width, height = img.size down_w = max(1, int(round(width * scale))) down_h = max(1, int(round(height * scale))) down = img.resize((down_w, down_h), resample=Image.BILINEAR) return down.resize((width, height), resample=Image.BILINEAR) if robustness_type == "color_jitter": brightness = float(params.get("cj_brightness", 0.0) or 0.0) contrast = float(params.get("cj_contrast", 0.0) or 0.0) saturation = float(params.get("cj_saturation", 0.0) or 0.0) hue = float(params.get("cj_hue", 0.0) or 0.0) out = img if brightness != 0.0: out = TF.adjust_brightness(out, 1.0 + brightness) if contrast != 0.0: out = TF.adjust_contrast(out, 1.0 + contrast) if saturation != 0.0: out = TF.adjust_saturation(out, 1.0 + saturation) if hue != 0.0: hue = max(-0.5, min(0.5, hue)) out = TF.adjust_hue(out, hue) return out raise ValueError(f"Unsupported robustness_type: {robustness_type}") def _apply_explicit_robustness(img, opt): robustness_chain = getattr(opt, "robustness_chain", None) or [] if robustness_chain: out = img for idx, entry in enumerate(robustness_chain): if not isinstance(entry, dict): raise ValueError(f"robustness_chain[{idx}] must be a dict, got {type(entry).__name__}") out = _apply_single_robustness( out, entry.get("type", ""), entry.get("params", {}), ) return out return _apply_single_robustness( img, getattr(opt, "robustness_type", ""), { "jpeg_quality": getattr(opt, "robustness_jpeg_quality", 75), "gaussian_blur_sigma": getattr(opt, "robustness_gaussian_blur_sigma", 1.0), "gaussian_noise_std": getattr(opt, "robustness_gaussian_noise_std", 0.1), "downsample_scale": getattr(opt, "robustness_downsample_scale", 0.5), "cj_brightness": getattr(opt, "robustness_cj_brightness", 0.0), "cj_contrast": getattr(opt, "robustness_cj_contrast", 0.0), "cj_saturation": getattr(opt, "robustness_cj_saturation", 0.0), "cj_hue": getattr(opt, "robustness_cj_hue", 0.0), }, ) import base64 from io import BytesIO from PIL import Image import numpy as np import torchvision.transforms from scipy.ndimage import gaussian_filter class ImageProcessor: def __init__(self, opt): self.opt = opt def data_augment(self, img): noise_type = getattr(self.opt, "noise_type", None) noise_ratio = getattr(self.opt, "noise_ratio", None) if noise_type == 'jpg': img_processed = self.pil_jpg_new(img, noise_ratio) elif noise_type == 'resize': width, height = img.size img_processed = torchvision.transforms.Resize((int(height / noise_ratio), int(width / noise_ratio)))(img) elif noise_type == 'blur': img = np.array(img) self.gaussian_blur(img, noise_ratio) img_processed = Image.fromarray(img) elif noise_type is None: img_processed = img return img_processed def pil_jpg_new(self, img, compress_val): out = BytesIO() img.save(out, format='jpeg', quality=compress_val) img = Image.open(out) # load from memory before ByteIO closes img = np.array(img) img = Image.fromarray(img) out.close() return img def gaussian_blur(self, img, sigma): # Check if the image has a third dimension if img.ndim == 3: # Check the number of channels in the image channels = img.shape[2] # if channels == 0: # # If there are no channels, return the image as is # return img if channels == 1: # If there is one channel, apply the filter to the single channel gaussian_filter(img[:, :, 0], output=img[:, :, 0], sigma=sigma) elif channels == 2: # If there are two channels, apply the filter to both channels gaussian_filter(img[:, :, 0], output=img[:, :, 0], sigma=sigma) gaussian_filter(img[:, :, 1], output=img[:, :, 1], sigma=sigma) elif channels == 3: # If there are three channels, apply the filter to all three channels 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) else: # If the image does not have a third dimension, apply the filter to the 2D image gaussian_filter(img, output=img, sigma=sigma) # return img def encode_image(image_path, processor=None): with open(image_path, "rb") as image_file: img = Image.open(image_file) # Apply data augmentation if processor is provided if processor is not None: # print(processor.opt) img = processor.data_augment(img) # Convert image to Base64 buffered = BytesIO() img.save(buffered, format="PNG") img_base64 = base64.b64encode(buffered.getvalue()).decode('utf-8') return img, img_base64 class Options: def __init__(self, noise_type=None, noise_ratio=None): """ 初始化数据增强的配置参数。 :param noise_type: 噪声类型,可选值为 'jpg', 'resize', 'blur' 或 None :param noise_ratio: 噪声强度或比例,具体含义取决于 noise_type """ self.noise_type = noise_type self.noise_ratio = noise_ratio def __str__(self): """返回对象的字符串表示,便于调试""" return (f"Options(noise_type={self.noise_type}, noise_ratio={self.noise_ratio}") # Robust ImageFolder: skip unreadable/corrupted files instead of crashing workers. class SafeImageFolder(datasets.ImageFolder): def __init__(self, *args, skip_broken=True, skip_log_limit=0, **kwargs): super().__init__(*args, **kwargs) self.skip_broken = bool(skip_broken) self.skip_log_limit = max(0, int(skip_log_limit)) self._skip_log_count = 0 @staticmethod def _is_image_decode_error(exc): return isinstance(exc, (UnidentifiedImageError, OSError, ValueError)) def __getitem__(self, index): if not self.skip_broken: return super().__getitem__(index) total = len(self.samples) if total == 0: raise IndexError("SafeImageFolder is empty") idx = int(index) % total for _ in range(total): try: return super().__getitem__(idx) except Exception as exc: if not self._is_image_decode_error(exc): raise bad_path = self.samples[idx][0] if self._skip_log_count < self.skip_log_limit: print( f"[Data] Skipping unreadable image: {bad_path} " f"({type(exc).__name__}: {exc})" ) self._skip_log_count += 1 idx = (idx + 1) % total raise RuntimeError( "Unable to fetch a readable image from dataset. " "All candidates appear unreadable." ) class FlatLabeledImageDataset(torch.utils.data.Dataset): """Fallback dataset for flat folders: labels parsed from filename suffix (_real/_fake).""" def __init__(self, samples, transform=None, skip_broken=True, skip_log_limit=0): self.samples = list(samples) self.transform = transform self.skip_broken = bool(skip_broken) self.skip_log_limit = max(0, int(skip_log_limit)) self._skip_log_count = 0 self.targets = [int(t) for _, t in self.samples] self.class_to_idx = {'0_real': 0, '1_fake': 1} self.classes = ['0_real', '1_fake'] self.imgs = self.samples @staticmethod def _is_image_decode_error(exc): return isinstance(exc, (UnidentifiedImageError, OSError, ValueError)) def __len__(self): return len(self.samples) def __getitem__(self, index): total = len(self.samples) if total == 0: raise IndexError("FlatLabeledImageDataset is empty") idx = int(index) % total for _ in range(total): path, target = self.samples[idx] try: with Image.open(path) as img: img = img.convert("RGB") if self.transform is not None: img = self.transform(img) return img, int(target) except Exception as exc: if (not self.skip_broken) or (not self._is_image_decode_error(exc)): raise if self._skip_log_count < self.skip_log_limit: print( f"[Data] Skipping unreadable image: {path} " f"({type(exc).__name__}: {exc})" ) self._skip_log_count += 1 idx = (idx + 1) % total raise RuntimeError("Unable to fetch a readable image from flat dataset.") # Example usage: # opt = SomeOptionsClass() # Replace with your actual options class # print(opt_class) def _find_binary_roots(root): """Yield directories whose immediate children contain 0_real and 1_fake.""" binary_roots = [] valid_pairs = ( {'0_real', '1_fake'}, {'0_fake', '1_real'}, {'raw', 'synthesis'}, # deepfake datasets {'real', 'fake'}, ) for current_root, dirs, _ in os.walk(root): present = set(dirs) if any(pair.issubset(present) for pair in valid_pairs): binary_roots.append(current_root) # ensure deterministic ordering so label mapping is stable binary_roots.sort() return binary_roots def infer_single_label_from_root(root): """Infer fixed binary label from directory name for one-class dataset roots.""" # Canonical deepfake aliases used by Celeb-DF/FF++ style trees. alias_to_label = { # real "0_real": 0, "1_real": 0, "real": 0, "raw": 0, "original_sequences": 0, "celeb-real-mtcnn": 0, "youtube-real-mtcnn": 0, # fake "1_fake": 1, "0_fake": 1, "fake": 1, "synthesis": 1, "manipulated_sequences": 1, "celeb-synthesis-mtcnn": 1, } base = os.path.basename(os.path.abspath(root)).strip().lower() return alias_to_label.get(base) def _collect_recursive_image_samples(root, label): exts = {".jpg", ".jpeg", ".png", ".bmp", ".webp", ".tif", ".tiff"} samples = [] for current_root, _, files in os.walk(root): for fname in files: if os.path.splitext(fname)[1].lower() in exts: samples.append((os.path.join(current_root, fname), int(label))) samples.sort(key=lambda x: x[0]) return samples def _resolve_dfdc_labels_path(root, opt): root_abs = os.path.abspath(root) explicit_candidates = [ getattr(opt, "dfdc_labels_npy", None), getattr(opt, "data_dfdc_labels_npy", None), ] candidates = [c for c in explicit_candidates if c] candidates.extend( [ os.path.join(os.path.dirname(root_abs), "labels.npy"), os.path.join(os.path.dirname(os.path.dirname(root_abs)), "labels.npy"), os.path.join(root_abs, "labels.npy"), ] ) seen = set() for candidate in candidates: p = os.path.abspath(os.path.expanduser(str(candidate))) if p in seen: continue seen.add(p) if os.path.isfile(p): return p return None def is_dfdc_train_faces_root(root, opt=None): root_abs = os.path.abspath(root) if os.path.basename(root_abs).strip().lower() != "train_faces": return False opt_obj = opt if opt is not None else object() return _resolve_dfdc_labels_path(root_abs, opt_obj) is not None def _build_dfdc_train_faces_dataset(opt, root, transform): root_abs = os.path.abspath(root) if not is_dfdc_train_faces_root(root_abs, opt): return None labels_path = _resolve_dfdc_labels_path(root_abs, opt) if labels_path is None: return None try: labels_raw = np.load(labels_path, allow_pickle=True).item() except Exception as exc: raise RuntimeError(f"Failed to load DFDC labels file: {labels_path} ({exc})") from exc if not isinstance(labels_raw, dict): raise RuntimeError(f"DFDC labels file is not a dict: {labels_path}") video_to_label = {} for k, v in labels_raw.items(): key = str(k) if key.endswith(".mp4"): key = key[:-4] try: iv = int(v) except (TypeError, ValueError): continue if iv in (0, 1): video_to_label[key] = iv if not video_to_label: raise RuntimeError(f"No valid binary labels parsed from: {labels_path}") exts = {".jpg", ".jpeg", ".png", ".bmp", ".webp", ".tif", ".tiff"} samples = [] total_video_dirs = 0 missing_label_video_dirs = 0 missing_label_examples = [] for part_name in sorted(os.listdir(root_abs)): part_dir = os.path.join(root_abs, part_name) if not os.path.isdir(part_dir): continue for video_name in sorted(os.listdir(part_dir)): video_dir = os.path.join(part_dir, video_name) if not os.path.isdir(video_dir): continue total_video_dirs += 1 label = video_to_label.get(video_name) if label is None: missing_label_video_dirs += 1 if len(missing_label_examples) < 5: missing_label_examples.append(video_dir) continue for fname in sorted(os.listdir(video_dir)): if os.path.splitext(fname)[1].lower() in exts: samples.append((os.path.join(video_dir, fname), int(label))) if not samples: raise RuntimeError( f"Detected DFDC train_faces root but no labeled image files were collected under {root_abs}" ) if missing_label_video_dirs > 0: print( f"[DFDC] Warning: {missing_label_video_dirs}/{total_video_dirs} video folders in {root_abs} " f"missing labels from {labels_path}. Examples: {missing_label_examples}" ) real_count = sum(1 for _, t in samples if int(t) == 0) fake_count = len(samples) - real_count print( f"[DFDC] Loaded train_faces with labels from {labels_path}: " f"samples={len(samples)} real={real_count} fake={fake_count}" ) skip_broken_images = bool(getattr(opt, "skip_broken_images", True)) skip_log_limit = int(getattr(opt, "skip_broken_images_log_limit", 0)) return FlatLabeledImageDataset( samples=samples, transform=transform, skip_broken=skip_broken_images, skip_log_limit=skip_log_limit, ) def _build_binary_datasets(opt, root, transform): """Create ImageFolder datasets for every binary directory discovered under root.""" binary_roots = _find_binary_roots(root) if not binary_roots: dfdc_dataset = _build_dfdc_train_faces_dataset(opt, root, transform) if dfdc_dataset is not None: return dfdc_dataset # Fallback: flat folder with filename labels like xxx_real.png / xxx_fake.png exts = {'.jpg', '.jpeg', '.png', '.bmp', '.webp', '.tif', '.tiff'} suffix_re = re.compile(r'(?:^|[_-])(real|fake)$', flags=re.IGNORECASE) flat_samples = [] root_path = os.path.abspath(root) for p in sorted(os.listdir(root_path)): abs_p = os.path.join(root_path, p) if not os.path.isfile(abs_p): continue ext = os.path.splitext(p)[1].lower() if ext not in exts: continue stem = os.path.splitext(p)[0] m = suffix_re.search(stem) if not m: continue label = 0 if m.group(1).lower() == 'real' else 1 flat_samples.append((abs_p, label)) skip_broken_images = bool(getattr(opt, "skip_broken_images", True)) skip_log_limit = int(getattr(opt, "skip_broken_images_log_limit", 0)) if flat_samples: return FlatLabeledImageDataset( samples=flat_samples, transform=transform, skip_broken=skip_broken_images, skip_log_limit=skip_log_limit, ) forced_label = infer_single_label_from_root(root) if forced_label is not None: recursive_samples = _collect_recursive_image_samples(root, forced_label) if not recursive_samples: raise RuntimeError( f"Single-label root inferred for {root}, but no image files were found recursively." ) return FlatLabeledImageDataset( samples=recursive_samples, transform=transform, skip_broken=skip_broken_images, skip_log_limit=skip_log_limit, ) raise RuntimeError( f"Could not locate binary folders (0_real/1_fake, raw/synthesis, real/fake), " f"flat *_real/*_fake files, or a known single-label root alias under {root}" ) datasets_list = [] skip_broken_images = bool(getattr(opt, "skip_broken_images", True)) skip_log_limit = int(getattr(opt, "skip_broken_images_log_limit", 0)) for binary_root in binary_roots: dset = SafeImageFolder( binary_root, transform=transform, skip_broken=skip_broken_images, skip_log_limit=skip_log_limit, ) idx_to_class = {idx: cls_name for cls_name, idx in dset.class_to_idx.items()} class_names = set(idx_to_class.values()) if class_names == {'0_real', '1_fake'}: desired_order = {'0_real': 0, '1_fake': 1} elif class_names == {'0_fake', '1_real'}: # reverse-labelled datasets: map back to canonical ordering desired_order = {'1_real': 0, '0_fake': 1} elif class_names == {'raw', 'synthesis'}: desired_order = {'raw': 0, 'synthesis': 1} elif class_names == {'real', 'fake'}: desired_order = {'real': 0, 'fake': 1} else: raise RuntimeError( f"Unsupported class names {class_names} under {binary_root}; " "expected 0_real/1_fake, 0_fake/1_real, raw/synthesis, or real/fake" ) remapped = [] for path, target in dset.samples: cls_name = idx_to_class[target] if cls_name not in desired_order: raise RuntimeError( f"Encountered unexpected class '{cls_name}' in {binary_root}; expected one of {list(desired_order.keys())}" ) remapped.append((path, desired_order[cls_name])) if remapped != dset.samples: dset.samples = remapped dset.imgs = remapped dset.targets = [t for _, t in remapped] # Normalise metadata to canonical labels for downstream consumers dset.class_to_idx = {'0_real': 0, '1_fake': 1} dset.classes = ['0_real', '1_fake'] datasets_list.append(dset) if len(datasets_list) == 1: return datasets_list[0] return torch.utils.data.ConcatDataset(datasets_list) def _pad_to_min_size(img, target_h, target_w): """Pad image with zeros so that height/width are at least target sizes.""" width, height = img.size pad_w = max(target_w - width, 0) pad_h = max(target_h - height, 0) if pad_w == 0 and pad_h == 0: return img left = pad_w // 2 right = pad_w - left top = pad_h // 2 bottom = pad_h - top return ImageOps.expand(img, border=(left, top, right, bottom), fill=0) def binary_dataset(opt, root): # Allow switching to a simplified (legacy) preprocessing pipeline to match the NPR script. legacy_preprocess = getattr(opt, "legacy_preprocess", False) if opt.isTrain: crop_func = transforms.RandomCrop(opt.cropSize) elif opt.no_crop: crop_func = transforms.Lambda(lambda img: img) else: crop_func = transforms.CenterCrop(opt.cropSize) if opt.isTrain and not opt.no_flip: flip_func = transforms.RandomHorizontalFlip() else: flip_func = transforms.Lambda(lambda img: img) if opt.no_resize: rz_func = transforms.Lambda(lambda img: img) else: # rz_func = transforms.Lambda(lambda img: custom_resize(img, opt)) rz_func = transforms.Resize((opt.loadSize, opt.loadSize)) # rz_func = transforms.Resize(opt.loadSize) if legacy_preprocess: # Match the older flow: resize -> basic augment -> crop -> flip -> IMAGENET normalize. normalize_transform = transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD) # normalize_transform = transforms.Normalize(mean=CLIP_MEAN, std=CLIP_STD) transform = transforms.Compose([ transforms.Lambda(lambda img: _apply_explicit_robustness(img, opt)), rz_func, transforms.Lambda(lambda img: data_augment(img, opt)), crop_func, flip_func, transforms.ToTensor(), normalize_transform, ]) return _build_binary_datasets(opt, root, transform) NOISE_TYPE = getattr(opt, "noise_type", None) NOISE_RATIO = getattr(opt, "noise_ratio", None) opt_class = Options(noise_type=NOISE_TYPE, noise_ratio=NOISE_RATIO) processor = ImageProcessor(opt_class) # 定义Albumentations增强(兼容不同版本的 ImageCompression 参数) def _build_image_compression(): try: return ImageCompression(quality_range=(60, 100), p=0.5) except TypeError: return ImageCompression(quality_lower=60, quality_upper=100, p=0.5) aug = Compose([ _build_image_compression(), GaussNoise(p=0.2), MotionBlur(p=0.2), GaussianBlur(blur_limit=3, p=0.5), _build_downscale_aug(), ]) # 自定义转换函数:将PIL图像转换为NumPy数组,应用Albumentations增强,再转回PIL def albumentations_aug(img): # 将PIL图像转换为NumPy数组 (H, W, C) RGB格式 img_np = np.array(img) # 应用Albumentations增强 augmented = aug(image=img_np) # 从字典中提取增强后的图像 img_aug = augmented['image'] # 将NumPy数组转回PIL图像 return Image.fromarray(img_aug) DATA_AUG = getattr(opt, "data_aug", False) USE_TESTSHIFT_LIKE_AUG_TRAIN = bool(getattr(opt, "use_testshift_like_aug", False)) and bool(opt.isTrain) USE_TESTSHIFT_LIKE_AUG_EVAL = bool(getattr(opt, "use_testshift_like_aug_eval", False)) and (not bool(opt.isTrain)) USE_TESTSHIFT_LIKE_AUG = USE_TESTSHIFT_LIKE_AUG_TRAIN or USE_TESTSHIFT_LIKE_AUG_EVAL TESTSHIFT_LIKE_APPLY_PROB = float(getattr(opt, "testshift_like_apply_prob", 1.0)) TESTSHIFT_LIKE_MAX_GROUPS = int(getattr(opt, "testshift_like_max_groups", 3)) USE_AUG_UTILS_EVAL = bool(getattr(opt, "use_aug_utils_eval", False)) and (not bool(opt.isTrain)) USE_AUG_UTILS_TRAIN = bool(getattr(opt, "use_aug_utils_train", False)) and ( bool(opt.isTrain) or USE_AUG_UTILS_EVAL ) AUG_UTILS_PROB = float(getattr(opt, "aug_utils_prob", 1.0)) AUG_UTILS_MAX_DISTORTIONS = int(getattr(opt, "aug_utils_max_distortions", 3)) AUG_UTILS_NUM_LEVELS = int(getattr(opt, "aug_utils_num_levels", 5)) # 创建数据增强变换(deepfake专用 TestShift-like 与原有增强互斥,避免叠加偏移) if USE_TESTSHIFT_LIKE_AUG: testshift_aug = _build_testshift_like_aug( p_apply=TESTSHIFT_LIKE_APPLY_PROB, max_groups=TESTSHIFT_LIKE_MAX_GROUPS, ) def _testshift_like_aug(img): img_np = np.array(img) out = testshift_aug(image=img_np)["image"] return Image.fromarray(out) data_aug = transforms.Lambda(_testshift_like_aug) print( f"[Aug] Using testshift-like aug (prob={TESTSHIFT_LIKE_APPLY_PROB}, " f"max_groups={TESTSHIFT_LIKE_MAX_GROUPS})" ) elif DATA_AUG: data_aug = transforms.Lambda(albumentations_aug) else: data_aug = transforms.Lambda(lambda img: img) if isinstance(opt.cropSize, (tuple, list)): target_h = opt.cropSize[0] target_w = opt.cropSize[1] if len(opt.cropSize) > 1 else opt.cropSize[0] else: target_h = target_w = opt.cropSize apply_padding = opt.no_resize and (opt.isTrain or not opt.no_crop) pad_func = transforms.Lambda(lambda img: _pad_to_min_size(img, target_h, target_w)) if apply_padding else transforms.Lambda(lambda img: img) trainmode = str(getattr(opt, 'trainmode', '')).lower() modelname = str(getattr(opt, 'modelname', '')) if trainmode == 'lora' and modelname.startswith('CLIP:'): normalize_transform = transforms.Normalize(mean=CLIP_MEAN, std=CLIP_STD) else: normalize_transform = transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD) # Optional: apply augmentations from aug_utils_train on tensor in [0, 1]. aug_utils_transform = transforms.Lambda(lambda tens: tens) if USE_AUG_UTILS_TRAIN: try: from aug_utils_train.utils_data import distort_images as aug_utils_distort_images def _apply_aug_utils_train(tens): if random() > AUG_UTILS_PROB: return tens distorted, _, _ = aug_utils_distort_images( tens, max_distortions=AUG_UTILS_MAX_DISTORTIONS, num_levels=AUG_UTILS_NUM_LEVELS, ) return distorted aug_utils_transform = transforms.Lambda(_apply_aug_utils_train) stage = "train" if bool(opt.isTrain) else "eval" print( f"[Aug] Using aug_utils_train@{stage} (prob={AUG_UTILS_PROB}, " f"max_distortions={AUG_UTILS_MAX_DISTORTIONS}, num_levels={AUG_UTILS_NUM_LEVELS})" ) except Exception as exc: print(f"[Aug] Failed to enable aug_utils_train, fallback to default aug only: {exc}") transform = transforms.Compose([ transforms.Lambda(lambda img: processor.data_augment(img)), transforms.Lambda(lambda img: _apply_explicit_robustness(img, opt)), # 使用自定义函数替换原processor.data_augment data_aug, rz_func, # 后续PyTorch变换 (如调整大小) pad_func, # 当禁用resize且图像不足裁剪尺寸时补零 crop_func, # 裁剪 flip_func, # 翻转 transforms.ToTensor(), aug_utils_transform, normalize_transform, ]) return _build_binary_datasets(opt, root, 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 data_augment(img, opt): img = np.array(img) if random() < opt.blur_prob: sig = sample_continuous(opt.blur_sig) gaussian_blur(img, sig) if random() < opt.jpg_prob: method = sample_discrete(opt.jpg_method) qual = sample_discrete(opt.jpg_qual) img = jpeg_from_key(img, qual, method) return Image.fromarray(img) def sample_continuous(s): 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): if len(s) == 1: return s[0] return choice(s) def gaussian_blur(img, sigma): 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, compress_val): 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, compress_val): 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, compress_val, key): method = jpeg_dict[key] return method(img, compress_val) # rz_dict = {'bilinear': Image.BILINEAR, # 'bicubic': Image.BICUBIC, # 'lanczos': Image.LANCZOS, # 'nearest': Image.NEAREST} rz_dict = {'bilinear': InterpolationMode.BILINEAR, 'bicubic': InterpolationMode.BICUBIC, 'lanczos': InterpolationMode.LANCZOS, 'nearest': InterpolationMode.NEAREST} def custom_resize(img, opt): interp = sample_discrete(opt.rz_interp) return TF.resize(img, (opt.loadSize,opt.loadSize), interpolation=rz_dict[interp])