Spaces:
Running on Zero
Running on Zero
| from utils.lmdb import get_array_shape_from_lmdb, retrieve_row_from_lmdb | |
| from torch.utils.data import Dataset | |
| import numpy as np | |
| import torch | |
| import lmdb | |
| import json | |
| from pathlib import Path | |
| from PIL import Image | |
| import os | |
| import decord | |
| class TextDataset(Dataset): | |
| def __init__(self, prompt_path, extended_prompt_path=None): | |
| with open(prompt_path, encoding="utf-8") as f: | |
| self.prompt_list = [line.rstrip() for line in f] | |
| if extended_prompt_path is not None: | |
| with open(extended_prompt_path, encoding="utf-8") as f: | |
| self.extended_prompt_list = [line.rstrip() for line in f] | |
| assert len(self.extended_prompt_list) == len(self.prompt_list) | |
| else: | |
| self.extended_prompt_list = None | |
| def __len__(self): | |
| return len(self.prompt_list) | |
| def __getitem__(self, idx): | |
| batch = { | |
| "prompts": self.prompt_list[idx], | |
| "idx": idx, | |
| } | |
| if self.extended_prompt_list is not None: | |
| batch["extended_prompts"] = self.extended_prompt_list[idx] | |
| return batch | |
| class ODERegressionLMDBDataset(Dataset): | |
| def __init__(self, data_path: str, max_pair: int = int(1e8)): | |
| self.env = lmdb.open(data_path, readonly=True, | |
| lock=False, readahead=False, meminit=False) | |
| self.latents_shape = get_array_shape_from_lmdb(self.env, 'latents') | |
| self.max_pair = max_pair | |
| def __len__(self): | |
| return min(self.latents_shape[0], self.max_pair) | |
| def __getitem__(self, idx): | |
| """ | |
| Outputs: | |
| - prompts: List of Strings | |
| - latents: Tensor of shape (num_denoising_steps, num_frames, num_channels, height, width). It is ordered from pure noise to clean image. | |
| """ | |
| latents = retrieve_row_from_lmdb( | |
| self.env, | |
| "latents", np.float16, idx, shape=self.latents_shape[1:] | |
| ) | |
| if len(latents.shape) == 4: | |
| latents = latents[None, ...] | |
| prompts = retrieve_row_from_lmdb( | |
| self.env, | |
| "prompts", str, idx | |
| ) | |
| return { | |
| "prompts": prompts, | |
| "ode_latent": torch.tensor(latents, dtype=torch.float32) | |
| } | |
| class ShardingLMDBDataset(Dataset): | |
| def __init__(self, data_path: str, max_pair: int = int(1e8)): | |
| self.envs = [] | |
| self.index = [] | |
| for fname in sorted(os.listdir(data_path)): | |
| path = os.path.join(data_path, fname) | |
| env = lmdb.open(path, | |
| readonly=True, | |
| lock=False, | |
| readahead=False, | |
| meminit=False) | |
| self.envs.append(env) | |
| self.latents_shape = [None] * len(self.envs) | |
| for shard_id, env in enumerate(self.envs): | |
| self.latents_shape[shard_id] = get_array_shape_from_lmdb(env, 'latents') | |
| for local_i in range(self.latents_shape[shard_id][0]): | |
| self.index.append((shard_id, local_i)) | |
| # print("shard_id ", shard_id, " local_i ", local_i) | |
| self.max_pair = max_pair | |
| def __len__(self): | |
| return len(self.index) | |
| def __getitem__(self, idx): | |
| """ | |
| Outputs: | |
| - prompts: List of Strings | |
| - latents: Tensor of shape (num_denoising_steps, num_frames, num_channels, height, width). It is ordered from pure noise to clean image. | |
| """ | |
| shard_id, local_idx = self.index[idx] | |
| latents = retrieve_row_from_lmdb( | |
| self.envs[shard_id], | |
| "latents", np.float16, local_idx, | |
| shape=self.latents_shape[shard_id][1:] | |
| ) | |
| if len(latents.shape) == 4: | |
| latents = latents[None, ...] | |
| prompts = retrieve_row_from_lmdb( | |
| self.envs[shard_id], | |
| "prompts", str, local_idx | |
| ) | |
| return { | |
| "prompts": prompts, | |
| "ode_latent": torch.tensor(latents, dtype=torch.float32) | |
| } | |
| class TextImagePairDataset(Dataset): | |
| def __init__( | |
| self, | |
| data_dir, | |
| transform=None, | |
| eval_first_n=-1, | |
| pad_to_multiple_of=None | |
| ): | |
| """ | |
| Args: | |
| data_dir (str): Path to the directory containing: | |
| - target_crop_info_*.json (metadata file) | |
| - */ (subdirectory containing images with matching aspect ratio) | |
| transform (callable, optional): Optional transform to be applied on the image | |
| """ | |
| self.transform = transform | |
| data_dir = Path(data_dir) | |
| # Find the metadata JSON file | |
| metadata_files = list(data_dir.glob('target_crop_info_*.json')) | |
| if not metadata_files: | |
| raise FileNotFoundError(f"No metadata file found in {data_dir}") | |
| if len(metadata_files) > 1: | |
| raise ValueError(f"Multiple metadata files found in {data_dir}") | |
| metadata_path = metadata_files[0] | |
| # Extract aspect ratio from metadata filename (e.g. target_crop_info_26-15.json -> 26-15) | |
| aspect_ratio = metadata_path.stem.split('_')[-1] | |
| # Use aspect ratio subfolder for images | |
| self.image_dir = data_dir / aspect_ratio | |
| if not self.image_dir.exists(): | |
| raise FileNotFoundError(f"Image directory not found: {self.image_dir}") | |
| # Load metadata | |
| with open(metadata_path, 'r') as f: | |
| self.metadata = json.load(f) | |
| eval_first_n = eval_first_n if eval_first_n != -1 else len(self.metadata) | |
| self.metadata = self.metadata[:eval_first_n] | |
| # Verify all images exist | |
| for item in self.metadata: | |
| image_path = self.image_dir / item['file_name'] | |
| if not image_path.exists(): | |
| raise FileNotFoundError(f"Image not found: {image_path}") | |
| self.dummy_prompt = "DUMMY PROMPT" | |
| self.pre_pad_len = len(self.metadata) | |
| if pad_to_multiple_of is not None and len(self.metadata) % pad_to_multiple_of != 0: | |
| # Duplicate the last entry | |
| self.metadata += [self.metadata[-1]] * ( | |
| pad_to_multiple_of - len(self.metadata) % pad_to_multiple_of | |
| ) | |
| def __len__(self): | |
| return len(self.metadata) | |
| def __getitem__(self, idx): | |
| """ | |
| Returns: | |
| dict: A dictionary containing: | |
| - image: PIL Image | |
| - caption: str | |
| - target_bbox: list of int [x1, y1, x2, y2] | |
| - target_ratio: str | |
| - type: str | |
| - origin_size: tuple of int (width, height) | |
| """ | |
| item = self.metadata[idx] | |
| # Load image | |
| image_path = self.image_dir / item['file_name'] | |
| image = Image.open(image_path).convert('RGB') | |
| # Apply transform if specified | |
| if self.transform: | |
| image = self.transform(image) | |
| return { | |
| 'image': image, | |
| 'prompts': item['caption'], | |
| 'target_bbox': item['target_crop']['target_bbox'], | |
| 'target_ratio': item['target_crop']['target_ratio'], | |
| 'type': item['type'], | |
| 'origin_size': (item['origin_width'], item['origin_height']), | |
| 'idx': idx | |
| } | |
| class TextVideoPairDataset(Dataset): | |
| def __init__( | |
| self, | |
| json_path, | |
| root_dir="", | |
| transform=None, | |
| num_frames=16, | |
| sample_rate=1, | |
| eval_first_n=-1 | |
| ): | |
| """ | |
| Args: | |
| json_path (str): 包含 source_path, edited_path, instruction 的 JSON 文件路径 | |
| root_dir (str): 视频文件的根目录(如果 JSON 里的路径是相对路径) | |
| transform (callable, optional): 同时应用于 source 和 edited 视频的变换 | |
| num_frames (int): 抽取的总帧数 | |
| sample_rate (int): 抽帧间隔 | |
| """ | |
| self.root_dir = Path(root_dir) | |
| self.transform = transform | |
| self.num_frames = num_frames | |
| self.sample_rate = sample_rate | |
| with open(json_path, 'r', encoding='utf-8') as f: | |
| self.metadata = json.load(f) | |
| if eval_first_n != -1: | |
| self.metadata = self.metadata[:eval_first_n] | |
| def __len__(self): | |
| return len(self.metadata) | |
| def _get_frames(self, video_path): | |
| """使用 decord 载入视频并均匀/步进采样帧""" | |
| try: | |
| full_path = self.root_dir / video_path | |
| vr = decord.VideoReader(str(full_path), ctx=decord.cpu(0)) | |
| except Exception as e: | |
| print(f"Error loading {full_path}: {e}") | |
| # 返回全黑帧作为 fallback(或者抛出错误) | |
| return [Image.new('RGB', (224, 224), (0, 0, 0))] * self.num_frames | |
| total_frames = len(vr) | |
| # 采样逻辑:根据 sample_rate 计算索引 | |
| # 如果视频长度不足,则进行简单处理 | |
| required_len = self.num_frames * self.sample_rate | |
| if total_frames >= required_len: | |
| # 正常采样 | |
| indices = np.arange(0, required_len, self.sample_rate) | |
| else: | |
| # 视频太短时的策略:重复最后一帧或等间距取帧 | |
| indices = np.linspace(0, total_frames - 1, self.num_frames, dtype=int) | |
| frames = vr.get_batch(indices).asnumpy() # (T, H, W, C) | |
| # 为了适应vae的输入要求 (B, C, T, H, W) | |
| return [Image.fromarray(f) for f in frames] | |
| def __getitem__(self, idx): | |
| item = self.metadata[idx] | |
| # 1. 加载视频帧。推理时可以只提供 source_path;成对评估/训练仍可提供 edited_path。 | |
| source_frames = self._get_frames(item['source_path']) | |
| edited_frames = self._get_frames(item['edited_path']) if 'edited_path' in item else None | |
| # 2. 应用变换 | |
| if self.transform: | |
| source_video = torch.stack([self.transform(f) for f in source_frames]).permute(0, 2, 3, 1) # [T, C, H, W] -> [T, H, W, C] | |
| if edited_frames is not None: | |
| edited_video = torch.stack([self.transform(f) for f in edited_frames]).permute(0, 2, 3, 1) # [T, C, H, W] -> [T, H, W, C] | |
| else: | |
| # [W, H] * T | |
| source_video = torch.stack([torch.tensor(np.array(f)) for f in source_frames]) # [T, H, W, C] | |
| if edited_frames is not None: | |
| edited_video = torch.stack([torch.tensor(np.array(f)) for f in edited_frames]) # [T, H, W, C] | |
| # 3. 整理输出 | |
| batch = { | |
| "source_video": source_video, # (T, H, W, C) | |
| "prompts": item['instruction'], # 这里的指令对应之前的 prompts | |
| "idx": idx | |
| } | |
| if edited_frames is not None: | |
| batch["edited_video"] = edited_video # (T, H, W, C) | |
| return batch | |
| def cycle(dl): | |
| while True: | |
| for data in dl: | |
| yield data | |