"""Dataset core: image bookkeeping, bucketing and the BaseDataset/DatasetGroup hierarchy. This module owns the data-loading side of training: * ``IMAGE_EXTENSIONS`` / ``TEXT_ENCODER_OUTPUTS_CACHE_SUFFIX*`` constants * ``ImageInfo`` (per-image metadata), ``BucketManager`` (aspect-ratio buckets), ``BucketBatchIndex``, ``AugHelper`` (color augmentation) * ``BaseDataset`` (abstract dataset shared by DreamBooth / FineTuning / ControlNet) * ``DatasetGroup`` (concat of multiple datasets) * ``MinimalDataset`` and ``load_arbitrary_dataset`` (user-supplied dataset hook) * ``debug_dataset`` viewer plus ``glob_images`` / ``glob_images_pathlib`` helpers * ``split_train_val`` shared between training/validation dataset halves Leaf image helpers (``IMAGE_TRANSFORMS`` / ``load_image`` / ``get_crop_ltrb`` / ``trim_and_resize_if_required``) live in ``library.utils`` and are re-exported here for backward compatibility. The DreamBooth / FineTuning / ControlNet specializations of ``BaseDataset`` live in their dedicated modules: ``library.dreambooth_dataset``, ``library.finetuning_dataset`` and ``library.controlnet_dataset``. ``HIGH_VRAM`` (a mutable module-level flag toggled by ``enable_high_vram``) lives in ``library.accelerator_setup``; that module has no cycle with this one so it is imported directly. """ import glob import importlib import logging import math import os import pathlib import random import re from concurrent.futures import Future, ThreadPoolExecutor from typing import Any, Callable, Dict, List, NamedTuple, Optional, Sequence, Tuple, Union import cv2 import imagesize import numpy as np import torch from PIL import Image from accelerate import Accelerator from diffusers import AutoencoderKL from torchvision import transforms from tqdm import tqdm from transformers import CLIPTokenizer import library.model_util as model_util from library import accelerator_setup from library.device_utils import clean_memory_on_device from library.strategy_base import ( LatentsCachingStrategy, TextEncoderOutputsCachingStrategy, TextEncodingStrategy, TokenizeStrategy, ) from library.subset import ( BaseSubset, ControlNetSubset, DreamBoothSubset, FineTuningSubset, ) from library.utils import ( IMAGE_TRANSFORMS, get_crop_ltrb, load_image, resize_image, setup_logging, trim_and_resize_if_required, validate_interpolation_fn, ) setup_logging() logger = logging.getLogger(__name__) IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".webp", ".bmp", ".PNG", ".JPG", ".JPEG", ".WEBP", ".BMP"] try: import pillow_avif IMAGE_EXTENSIONS.extend([".avif", ".AVIF"]) except: pass # JPEG-XL on Linux try: from jxlpy import JXLImagePlugin from library.jpeg_xl_util import get_jxl_size IMAGE_EXTENSIONS.extend([".jxl", ".JXL"]) except: pass # JPEG-XL on Linux and Windows try: import pillow_jxl from library.jpeg_xl_util import get_jxl_size IMAGE_EXTENSIONS.extend([".jxl", ".JXL"]) except: pass TEXT_ENCODER_OUTPUTS_CACHE_SUFFIX = "_te_outputs.npz" TEXT_ENCODER_OUTPUTS_CACHE_SUFFIX_SD3 = "_sd3_te.npz" def split_train_val( paths: List[str], sizes: List[Optional[Tuple[int, int]]], is_training_dataset: bool, validation_split: float, validation_seed: int | None, ) -> Tuple[List[str], List[Optional[Tuple[int, int]]]]: """ Split the dataset into train and validation Shuffle the dataset based on the validation_seed or the current random seed. For example if the split of 0.2 of 100 images. [0:80] = 80 training images [80:] = 20 validation images """ dataset = list(zip(paths, sizes)) if validation_seed is not None: logging.info(f"Using validation seed: {validation_seed}") prevstate = random.getstate() random.seed(validation_seed) random.shuffle(dataset) random.setstate(prevstate) else: random.shuffle(dataset) paths, sizes = zip(*dataset) paths = list(paths) sizes = list(sizes) # Split the dataset between training and validation if is_training_dataset: # Training dataset we split to the first part split = math.ceil(len(paths) * (1 - validation_split)) return paths[0:split], sizes[0:split] else: # Validation dataset we split to the second part split = len(paths) - round(len(paths) * validation_split) return paths[split:], sizes[split:] class ImageInfo: def __init__( self, image_key: str, num_repeats: int, caption: str, is_reg: bool, absolute_path: str, caption_dropout_rate: float = 0.0 ) -> None: self.image_key: str = image_key self.num_repeats: int = num_repeats self.caption: str = caption self.is_reg: bool = is_reg self.absolute_path: str = absolute_path self.caption_dropout_rate: float = caption_dropout_rate self.image_size: Tuple[int, int] = None self.resized_size: Tuple[int, int] = None self.bucket_reso: Tuple[int, int] = None self.latents: Optional[torch.Tensor] = None self.latents_flipped: Optional[torch.Tensor] = None self.latents_npz: Optional[str] = None # set in cache_latents self.latents_original_size: Optional[Tuple[int, int]] = None # original image size, not latents size self.latents_crop_ltrb: Optional[Tuple[int, int]] = ( None # crop left top right bottom in original pixel size, not latents size ) self.cond_img_path: Optional[str] = None self.image: Optional[Image.Image] = None # optional, original PIL Image self.text_encoder_outputs_npz: Optional[str] = None # filename. set in cache_text_encoder_outputs # new self.text_encoder_outputs: Optional[List[torch.Tensor]] = None # old self.text_encoder_outputs1: Optional[torch.Tensor] = None self.text_encoder_outputs2: Optional[torch.Tensor] = None self.text_encoder_pool2: Optional[torch.Tensor] = None self.alpha_mask: Optional[torch.Tensor] = None # alpha mask can be flipped in runtime self.resize_interpolation: Optional[str] = None class BucketManager: def __init__(self, no_upscale, max_reso, min_size, max_size, reso_steps) -> None: if max_size is not None: if max_reso is not None: assert max_size >= max_reso[0], "the max_size should be larger than the width of max_reso" assert max_size >= max_reso[1], "the max_size should be larger than the height of max_reso" if min_size is not None: assert max_size >= min_size, "the max_size should be larger than the min_size" self.no_upscale = no_upscale if max_reso is None: self.max_reso = None self.max_area = None else: self.max_reso = max_reso self.max_area = max_reso[0] * max_reso[1] self.min_size = min_size self.max_size = max_size self.reso_steps = reso_steps self.resos = [] self.reso_to_id = {} self.buckets = [] # 前処理時は (image_key, image, original size, crop left/top)、学習時は image_key def add_image(self, reso, image_or_info): bucket_id = self.reso_to_id[reso] self.buckets[bucket_id].append(image_or_info) def shuffle(self): for bucket in self.buckets: random.shuffle(bucket) def sort(self): # 解像度順にソートする(表示時、メタデータ格納時の見栄えをよくするためだけ)。bucketsも入れ替えてreso_to_idも振り直す sorted_resos = self.resos.copy() sorted_resos.sort() sorted_buckets = [] sorted_reso_to_id = {} for i, reso in enumerate(sorted_resos): bucket_id = self.reso_to_id[reso] sorted_buckets.append(self.buckets[bucket_id]) sorted_reso_to_id[reso] = i self.resos = sorted_resos self.buckets = sorted_buckets self.reso_to_id = sorted_reso_to_id def make_buckets(self): resos = model_util.make_bucket_resolutions(self.max_reso, self.min_size, self.max_size, self.reso_steps) self.set_predefined_resos(resos) def set_predefined_resos(self, resos): # 規定サイズから選ぶ場合の解像度、aspect ratioの情報を格納しておく self.predefined_resos = resos.copy() self.predefined_resos_set = set(resos) self.predefined_aspect_ratios = np.array([w / h for w, h in resos]) def add_if_new_reso(self, reso): if reso not in self.reso_to_id: bucket_id = len(self.resos) self.reso_to_id[reso] = bucket_id self.resos.append(reso) self.buckets.append([]) # logger.info(reso, bucket_id, len(self.buckets)) def round_to_steps(self, x): x = int(x + 0.5) return x - x % self.reso_steps def select_bucket(self, image_width, image_height): aspect_ratio = image_width / image_height if not self.no_upscale: # 拡大および縮小を行う # 同じaspect ratioがあるかもしれないので(fine tuningで、no_upscale=Trueで前処理した場合)、解像度が同じものを優先する reso = (image_width, image_height) if reso in self.predefined_resos_set: pass else: ar_errors = self.predefined_aspect_ratios - aspect_ratio predefined_bucket_id = np.abs(ar_errors).argmin() # 当該解像度以外でaspect ratio errorが最も少ないもの reso = self.predefined_resos[predefined_bucket_id] ar_reso = reso[0] / reso[1] if aspect_ratio > ar_reso: # 横が長い→縦を合わせる scale = reso[1] / image_height else: scale = reso[0] / image_width resized_size = (int(image_width * scale + 0.5), int(image_height * scale + 0.5)) # logger.info(f"use predef, {image_width}, {image_height}, {reso}, {resized_size}") else: # 縮小のみを行う if image_width * image_height > self.max_area: # 画像が大きすぎるのでアスペクト比を保ったまま縮小することを前提にbucketを決める resized_width = math.sqrt(self.max_area * aspect_ratio) resized_height = self.max_area / resized_width assert abs(resized_width / resized_height - aspect_ratio) < 1e-2, "aspect is illegal" # リサイズ後の短辺または長辺をreso_steps単位にする:aspect ratioの差が少ないほうを選ぶ # 元のbucketingと同じロジック b_width_rounded = self.round_to_steps(resized_width) b_height_in_wr = self.round_to_steps(b_width_rounded / aspect_ratio) ar_width_rounded = b_width_rounded / b_height_in_wr b_height_rounded = self.round_to_steps(resized_height) b_width_in_hr = self.round_to_steps(b_height_rounded * aspect_ratio) ar_height_rounded = b_width_in_hr / b_height_rounded # logger.info(b_width_rounded, b_height_in_wr, ar_width_rounded) # logger.info(b_width_in_hr, b_height_rounded, ar_height_rounded) if abs(ar_width_rounded - aspect_ratio) < abs(ar_height_rounded - aspect_ratio): resized_size = (b_width_rounded, int(b_width_rounded / aspect_ratio + 0.5)) else: resized_size = (int(b_height_rounded * aspect_ratio + 0.5), b_height_rounded) # logger.info(resized_size) else: resized_size = (image_width, image_height) # リサイズは不要 # 画像のサイズ未満をbucketのサイズとする(paddingせずにcroppingする) bucket_width = resized_size[0] - resized_size[0] % self.reso_steps bucket_height = resized_size[1] - resized_size[1] % self.reso_steps # logger.info(f"use arbitrary {image_width}, {image_height}, {resized_size}, {bucket_width}, {bucket_height}") reso = (bucket_width, bucket_height) self.add_if_new_reso(reso) ar_error = (reso[0] / reso[1]) - aspect_ratio return reso, resized_size, ar_error @staticmethod def get_crop_ltrb(bucket_reso: Tuple[int, int], image_size: Tuple[int, int]): # implementation moved to library.utils.get_crop_ltrb; kept as a staticmethod for backward compatibility return get_crop_ltrb(bucket_reso, image_size) class BucketBatchIndex(NamedTuple): bucket_index: int bucket_batch_size: int batch_index: int class AugHelper: # albumentationsへの依存をなくしたがとりあえず同じinterfaceを持たせる def __init__(self): pass def color_aug(self, image: np.ndarray): # self.color_aug_method = albu.OneOf( # [ # albu.HueSaturationValue(8, 0, 0, p=0.5), # albu.RandomGamma((95, 105), p=0.5), # ], # p=0.33, # ) hue_shift_limit = 8 # remove dependency to albumentations if random.random() <= 0.33: if random.random() > 0.5: # hue shift hsv_img = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) hue_shift = random.uniform(-hue_shift_limit, hue_shift_limit) if hue_shift < 0: hue_shift = 180 + hue_shift hsv_img[:, :, 0] = (hsv_img[:, :, 0] + hue_shift) % 180 image = cv2.cvtColor(hsv_img, cv2.COLOR_HSV2BGR) else: # random gamma gamma = random.uniform(0.95, 1.05) image = np.clip(image**gamma, 0, 255).astype(np.uint8) return {"image": image} def get_augmentor(self, use_color_aug: bool): # -> Optional[Callable[[np.ndarray], Dict[str, np.ndarray]]]: return self.color_aug if use_color_aug else None class BaseDataset(torch.utils.data.Dataset): def __init__( self, resolution: Optional[Tuple[int, int]], network_multiplier: float, train_inpainting: bool, debug_dataset: bool, resize_interpolation: Optional[str] = None, skip_image_resolution: Optional[Tuple[int, int]] = None, ) -> None: super().__init__() # width/height is used when enable_bucket==False self.width, self.height = (None, None) if resolution is None else resolution self.network_multiplier = network_multiplier self.debug_dataset = debug_dataset self.subsets: List[Union[DreamBoothSubset, FineTuningSubset]] = [] self.token_padding_disabled = False self.tag_frequency = {} self.XTI_layers = None self.token_strings = None self.enable_bucket = False self.bucket_manager: BucketManager = None # not initialized self.min_bucket_reso = None self.max_bucket_reso = None self.bucket_reso_steps = None self.bucket_no_upscale = None self.bucket_info = None # for metadata self.current_epoch: int = 0 # インスタンスがepochごとに新しく作られるようなので外側から渡さないとダメ self.current_step: int = 0 self.max_train_steps: int = 0 self.seed: int = 0 # inpainting self.train_inpainting = train_inpainting # augmentation self.aug_helper = AugHelper() self.image_transforms = IMAGE_TRANSFORMS if resize_interpolation is not None: assert validate_interpolation_fn( resize_interpolation ), f'Resize interpolation "{resize_interpolation}" is not a valid interpolation' self.resize_interpolation = resize_interpolation self.skip_image_resolution = skip_image_resolution self.image_data: Dict[str, ImageInfo] = {} self.image_to_subset: Dict[str, Union[DreamBoothSubset, FineTuningSubset]] = {} self.replacements = {} self.tokenize_strategy = None self.text_encoder_output_caching_strategy = None self.latents_caching_strategy = None def set_current_strategies(self): self.tokenize_strategy = TokenizeStrategy.get_strategy() self.text_encoder_output_caching_strategy = TextEncoderOutputsCachingStrategy.get_strategy() self.latents_caching_strategy = LatentsCachingStrategy.get_strategy() def adjust_min_max_bucket_reso_by_steps( self, resolution: Tuple[int, int], min_bucket_reso: int, max_bucket_reso: int, bucket_reso_steps: int ) -> Tuple[int, int]: # make min/max bucket reso to be multiple of bucket_reso_steps if min_bucket_reso % bucket_reso_steps != 0: adjusted_min_bucket_reso = min_bucket_reso - min_bucket_reso % bucket_reso_steps logger.warning( f"min_bucket_reso is adjusted to be multiple of bucket_reso_steps" f" / min_bucket_resoがbucket_reso_stepsの倍数になるように調整されました: {min_bucket_reso} -> {adjusted_min_bucket_reso}" ) min_bucket_reso = adjusted_min_bucket_reso if max_bucket_reso % bucket_reso_steps != 0: adjusted_max_bucket_reso = max_bucket_reso + bucket_reso_steps - max_bucket_reso % bucket_reso_steps logger.warning( f"max_bucket_reso is adjusted to be multiple of bucket_reso_steps" f" / max_bucket_resoがbucket_reso_stepsの倍数になるように調整されました: {max_bucket_reso} -> {adjusted_max_bucket_reso}" ) max_bucket_reso = adjusted_max_bucket_reso assert ( min(resolution) >= min_bucket_reso ), f"min_bucket_reso must be equal or less than resolution / min_bucket_resoは最小解像度より大きくできません。解像度を大きくするかmin_bucket_resoを小さくしてください" assert ( max(resolution) <= max_bucket_reso ), f"max_bucket_reso must be equal or greater than resolution / max_bucket_resoは最大解像度より小さくできません。解像度を小さくするかmin_bucket_resoを大きくしてください" return min_bucket_reso, max_bucket_reso def set_seed(self, seed): self.seed = seed def set_current_epoch(self, epoch): if not self.current_epoch == epoch: # epochが切り替わったらバケツをシャッフルする if epoch > self.current_epoch: logger.info("epoch is incremented. current_epoch: {}, epoch: {}".format(self.current_epoch, epoch)) num_epochs = epoch - self.current_epoch for _ in range(num_epochs): self.current_epoch += 1 self.shuffle_buckets() # self.current_epoch seem to be set to 0 again in the next epoch. it may be caused by skipped_dataloader? else: logger.warning("epoch is not incremented. current_epoch: {}, epoch: {}".format(self.current_epoch, epoch)) self.current_epoch = epoch def set_current_step(self, step): self.current_step = step def set_max_train_steps(self, max_train_steps): self.max_train_steps = max_train_steps def set_tag_frequency(self, dir_name, captions): frequency_for_dir = self.tag_frequency.get(dir_name, {}) self.tag_frequency[dir_name] = frequency_for_dir for caption in captions: for tag in caption.split(","): tag = tag.strip() if tag: tag = tag.lower() frequency = frequency_for_dir.get(tag, 0) frequency_for_dir[tag] = frequency + 1 def disable_token_padding(self): self.token_padding_disabled = True def enable_XTI(self, layers=None, token_strings=None): self.XTI_layers = layers self.token_strings = token_strings def add_replacement(self, str_from, str_to): self.replacements[str_from] = str_to def process_caption(self, subset: BaseSubset, caption): # caption に prefix/suffix を付ける if subset.caption_prefix: caption = subset.caption_prefix + " " + caption if subset.caption_suffix: caption = caption + " " + subset.caption_suffix # dropoutの決定:tag dropがこのメソッド内にあるのでここで行うのが良い is_drop_out = subset.caption_dropout_rate > 0 and random.random() < subset.caption_dropout_rate is_drop_out = ( is_drop_out or subset.caption_dropout_every_n_epochs > 0 and self.current_epoch % subset.caption_dropout_every_n_epochs == 0 ) if is_drop_out: caption = "" else: # process wildcards if subset.enable_wildcard: # if caption is multiline, random choice one line if "\n" in caption: caption = random.choice(caption.split("\n")) # wildcard is like '{aaa|bbb|ccc...}' # escape the curly braces like {{ or }} replacer1 = "⦅" replacer2 = "⦆" while replacer1 in caption or replacer2 in caption: replacer1 += "⦅" replacer2 += "⦆" caption = caption.replace("{{", replacer1).replace("}}", replacer2) # replace the wildcard def replace_wildcard(match): return random.choice(match.group(1).split("|")) caption = re.sub(r"\{([^}]+)\}", replace_wildcard, caption) # unescape the curly braces caption = caption.replace(replacer1, "{").replace(replacer2, "}") else: # if caption is multiline, use the first line caption = caption.split("\n")[0] if subset.shuffle_caption or subset.token_warmup_step > 0 or subset.caption_tag_dropout_rate > 0: fixed_tokens = [] flex_tokens = [] fixed_suffix_tokens = [] if ( hasattr(subset, "keep_tokens_separator") and subset.keep_tokens_separator and subset.keep_tokens_separator in caption ): fixed_part, flex_part = caption.split(subset.keep_tokens_separator, 1) if subset.keep_tokens_separator in flex_part: flex_part, fixed_suffix_part = flex_part.split(subset.keep_tokens_separator, 1) fixed_suffix_tokens = [t.strip() for t in fixed_suffix_part.split(subset.caption_separator) if t.strip()] fixed_tokens = [t.strip() for t in fixed_part.split(subset.caption_separator) if t.strip()] flex_tokens = [t.strip() for t in flex_part.split(subset.caption_separator) if t.strip()] else: tokens = [t.strip() for t in caption.strip().split(subset.caption_separator)] flex_tokens = tokens[:] if subset.keep_tokens > 0: fixed_tokens = flex_tokens[: subset.keep_tokens] flex_tokens = tokens[subset.keep_tokens :] if subset.token_warmup_step < 1: # 初回に上書きする subset.token_warmup_step = math.floor(subset.token_warmup_step * self.max_train_steps) if subset.token_warmup_step and self.current_step < subset.token_warmup_step: tokens_len = ( math.floor( (self.current_step) * ((len(flex_tokens) - subset.token_warmup_min) / (subset.token_warmup_step)) ) + subset.token_warmup_min ) flex_tokens = flex_tokens[:tokens_len] def dropout_tags(tokens): if subset.caption_tag_dropout_rate <= 0: return tokens l = [] for token in tokens: if random.random() >= subset.caption_tag_dropout_rate: l.append(token) return l if subset.shuffle_caption: random.shuffle(flex_tokens) flex_tokens = dropout_tags(flex_tokens) caption = f"{subset.caption_separator} ".join(fixed_tokens + flex_tokens + fixed_suffix_tokens) # process secondary separator if subset.secondary_separator: caption = caption.replace(subset.secondary_separator, subset.caption_separator) # textual inversion対応 for str_from, str_to in self.replacements.items(): if str_from == "": # replace all if type(str_to) == list: caption = random.choice(str_to) else: caption = str_to else: caption = caption.replace(str_from, str_to) return caption def register_image(self, info: ImageInfo, subset: BaseSubset): self.image_data[info.image_key] = info self.image_to_subset[info.image_key] = subset def make_buckets(self): """ bucketingを行わない場合も呼び出し必須(ひとつだけbucketを作る) min_size and max_size are ignored when enable_bucket is False """ logger.info("loading image sizes.") for info in tqdm(self.image_data.values()): if info.image_size is None: info.image_size = self.get_image_size(info.absolute_path) # # run in parallel # max_workers = min(os.cpu_count(), len(self.image_data)) # TODO consider multi-gpu (processes) # with ThreadPoolExecutor(max_workers) as executor: # futures = [] # for info in tqdm(self.image_data.values(), desc="loading image sizes"): # if info.image_size is None: # def get_and_set_image_size(info): # info.image_size = self.get_image_size(info.absolute_path) # futures.append(executor.submit(get_and_set_image_size, info)) # # consume futures to reduce memory usage and prevent Ctrl-C hang # if len(futures) >= max_workers: # for future in futures: # future.result() # futures = [] # for future in futures: # future.result() if self.enable_bucket: logger.info("make buckets") else: logger.info("prepare dataset") # bucketを作成し、画像をbucketに振り分ける if self.enable_bucket: if self.bucket_manager is None: # fine tuningの場合でmetadataに定義がある場合は、すでに初期化済み self.bucket_manager = BucketManager( self.bucket_no_upscale, (self.width, self.height), self.min_bucket_reso, self.max_bucket_reso, self.bucket_reso_steps, ) if not self.bucket_no_upscale: self.bucket_manager.make_buckets() else: logger.warning( "min_bucket_reso and max_bucket_reso are ignored if bucket_no_upscale is set, because bucket reso is defined by image size automatically / bucket_no_upscaleが指定された場合は、bucketの解像度は画像サイズから自動計算されるため、min_bucket_resoとmax_bucket_resoは無視されます" ) img_ar_errors = [] for image_info in self.image_data.values(): image_width, image_height = image_info.image_size image_info.bucket_reso, image_info.resized_size, ar_error = self.bucket_manager.select_bucket( image_width, image_height ) # logger.info(image_info.image_key, image_info.bucket_reso) img_ar_errors.append(abs(ar_error)) self.bucket_manager.sort() else: self.bucket_manager = BucketManager(False, (self.width, self.height), None, None, None) self.bucket_manager.set_predefined_resos([(self.width, self.height)]) # ひとつの固定サイズbucketのみ for image_info in self.image_data.values(): image_width, image_height = image_info.image_size image_info.bucket_reso, image_info.resized_size, _ = self.bucket_manager.select_bucket(image_width, image_height) for image_info in self.image_data.values(): for _ in range(image_info.num_repeats): self.bucket_manager.add_image(image_info.bucket_reso, image_info.image_key) # bucket情報を表示、格納する if self.enable_bucket: self.bucket_info = {"buckets": {}} logger.info("number of images (including repeats) / 各bucketの画像枚数(繰り返し回数を含む)") for i, (reso, bucket) in enumerate(zip(self.bucket_manager.resos, self.bucket_manager.buckets)): count = len(bucket) if count > 0: self.bucket_info["buckets"][i] = {"resolution": reso, "count": len(bucket)} logger.info(f"bucket {i}: resolution {reso}, count: {len(bucket)}") if len(img_ar_errors) == 0: mean_img_ar_error = 0 # avoid NaN else: img_ar_errors = np.array(img_ar_errors) mean_img_ar_error = np.mean(np.abs(img_ar_errors)) self.bucket_info["mean_img_ar_error"] = mean_img_ar_error logger.info(f"mean ar error (without repeats): {mean_img_ar_error}") # データ参照用indexを作る。このindexはdatasetのshuffleに用いられる self.buckets_indices: List[BucketBatchIndex] = [] for bucket_index, bucket in enumerate(self.bucket_manager.buckets): batch_count = int(math.ceil(len(bucket) / self.batch_size)) for batch_index in range(batch_count): self.buckets_indices.append(BucketBatchIndex(bucket_index, self.batch_size, batch_index)) self.shuffle_buckets() self._length = len(self.buckets_indices) def shuffle_buckets(self): # set random seed for this epoch random.seed(self.seed + self.current_epoch) random.shuffle(self.buckets_indices) self.bucket_manager.shuffle() def verify_bucket_reso_steps(self, min_steps: int): assert self.bucket_reso_steps is None or self.bucket_reso_steps % min_steps == 0, ( f"bucket_reso_steps is {self.bucket_reso_steps}. it must be divisible by {min_steps}.\n" + f"bucket_reso_stepsが{self.bucket_reso_steps}です。{min_steps}で割り切れる必要があります" ) def is_latent_cacheable(self): return all([not subset.color_aug and not subset.random_crop for subset in self.subsets]) def is_text_encoder_output_cacheable(self, cache_supports_dropout: bool = False): return all( [ not ( subset.caption_dropout_rate > 0 and not cache_supports_dropout or subset.shuffle_caption or subset.token_warmup_step > 0 or subset.caption_tag_dropout_rate > 0 ) for subset in self.subsets ] ) def new_cache_latents(self, model: Any, accelerator: Accelerator): r""" a brand new method to cache latents. This method caches latents with caching strategy. normal cache_latents method is used by default, but this method is used when caching strategy is specified. """ logger.info("caching latents with caching strategy.") caching_strategy = LatentsCachingStrategy.get_strategy() image_infos = list(self.image_data.values()) # sort by resolution image_infos.sort(key=lambda info: info.bucket_reso[0] * info.bucket_reso[1]) # split by resolution and some conditions class Condition: def __init__(self, reso, flip_aug, alpha_mask, random_crop): self.reso = reso self.flip_aug = flip_aug self.alpha_mask = alpha_mask self.random_crop = random_crop def __eq__(self, other): return ( other is not None and self.reso == other.reso and self.flip_aug == other.flip_aug and self.alpha_mask == other.alpha_mask and self.random_crop == other.random_crop ) batch: List[ImageInfo] = [] current_condition = None # support multiple-gpus num_processes = accelerator.num_processes process_index = accelerator.process_index # define a function to submit a batch to cache def submit_batch(batch, cond): for info in batch: if info.image is not None and isinstance(info.image, Future): info.image = info.image.result() # future to image caching_strategy.cache_batch_latents(model, batch, cond.flip_aug, cond.alpha_mask, cond.random_crop) # remove image from memory for info in batch: info.image = None # define ThreadPoolExecutor to load images in parallel max_workers = min(os.cpu_count(), len(image_infos)) max_workers = max(1, max_workers // num_processes) # consider multi-gpu max_workers = min(max_workers, caching_strategy.batch_size) # max_workers should be less than batch_size executor = ThreadPoolExecutor(max_workers) try: # iterate images logger.info("caching latents...") for i, info in enumerate(tqdm(image_infos)): subset = self.image_to_subset[info.image_key] if info.latents_npz is not None: # fine tuning dataset continue # check disk cache exists and size of latents if caching_strategy.cache_to_disk: # info.latents_npz = os.path.splitext(info.absolute_path)[0] + file_suffix info.latents_npz = caching_strategy.get_latents_npz_path(info.absolute_path, info.image_size) # if the modulo of num_processes is not equal to process_index, skip caching # this makes each process cache different latents if i % num_processes != process_index: continue # print(f"{process_index}/{num_processes} {i}/{len(image_infos)} {info.latents_npz}") cache_available = caching_strategy.is_disk_cached_latents_expected( info.bucket_reso, info.latents_npz, subset.flip_aug, subset.alpha_mask ) if cache_available: # do not add to batch continue # if batch is not empty and condition is changed, flush the batch. Note that current_condition is not None if batch is not empty condition = Condition(info.bucket_reso, subset.flip_aug, subset.alpha_mask, subset.random_crop) if len(batch) > 0 and current_condition != condition: submit_batch(batch, current_condition) batch = [] if condition != current_condition and accelerator_setup.HIGH_VRAM: # even with high VRAM, if shape is changed clean_memory_on_device(accelerator.device) if info.image is None: # load image in parallel info.image = executor.submit(load_image, info.absolute_path, condition.alpha_mask) batch.append(info) current_condition = condition # if number of data in batch is enough, flush the batch if len(batch) >= caching_strategy.batch_size: submit_batch(batch, current_condition) batch = [] # current_condition = None # keep current_condition to avoid next `clean_memory_on_device` call if len(batch) > 0: submit_batch(batch, current_condition) finally: executor.shutdown() def new_cache_text_encoder_outputs(self, models: List[Any], accelerator: Accelerator): r""" a brand new method to cache text encoder outputs. This method caches text encoder outputs with caching strategy. """ tokenize_strategy = TokenizeStrategy.get_strategy() text_encoding_strategy = TextEncodingStrategy.get_strategy() caching_strategy = TextEncoderOutputsCachingStrategy.get_strategy() batch_size = caching_strategy.batch_size or self.batch_size logger.info("caching Text Encoder outputs with caching strategy.") image_infos = list(self.image_data.values()) # split by resolution batches = [] batch = [] # support multiple-gpus num_processes = accelerator.num_processes process_index = accelerator.process_index logger.info("checking cache validity...") for i, info in enumerate(tqdm(image_infos)): # check disk cache exists and size of text encoder outputs if caching_strategy.cache_to_disk: te_out_npz = caching_strategy.get_outputs_npz_path(info.absolute_path) info.text_encoder_outputs_npz = te_out_npz # set npz filename regardless of cache availability # if the modulo of num_processes is not equal to process_index, skip caching # this makes each process cache different text encoder outputs if i % num_processes != process_index: continue cache_available = caching_strategy.is_disk_cached_outputs_expected(te_out_npz) if cache_available: # do not add to batch continue batch.append(info) # if number of data in batch is enough, flush the batch if len(batch) >= batch_size: batches.append(batch) batch = [] if len(batch) > 0: batches.append(batch) if len(batches) == 0: logger.info("no Text Encoder outputs to cache") return # iterate batches logger.info("caching Text Encoder outputs...") for batch in tqdm(batches, smoothing=1, total=len(batches)): caching_strategy.cache_batch_outputs(tokenize_strategy, models, text_encoding_strategy, batch) def get_image_size(self, image_path): if image_path.endswith(".jxl") or image_path.endswith(".JXL"): return get_jxl_size(image_path) # return imagesize.get(image_path) image_size = imagesize.get(image_path) if image_size[0] <= 0: # imagesize doesn't work for some images, so use PIL as a fallback try: with Image.open(image_path) as img: image_size = img.size except Exception as e: logger.warning(f"failed to get image size: {image_path}, error: {e}") image_size = (0, 0) return image_size def load_image_with_face_info(self, subset: BaseSubset, image_path: str, alpha_mask=False): img = load_image(image_path, alpha_mask) face_cx = face_cy = face_w = face_h = 0 if subset.face_crop_aug_range is not None: tokens = os.path.splitext(os.path.basename(image_path))[0].split("_") if len(tokens) >= 5: face_cx = int(tokens[-4]) face_cy = int(tokens[-3]) face_w = int(tokens[-2]) face_h = int(tokens[-1]) return img, face_cx, face_cy, face_w, face_h # いい感じに切り出す def crop_target(self, subset: BaseSubset, image, face_cx, face_cy, face_w, face_h): height, width = image.shape[0:2] if height == self.height and width == self.width: return image # 画像サイズはsizeより大きいのでリサイズする face_size = max(face_w, face_h) size = min(self.height, self.width) # 短いほう min_scale = max(self.height / height, self.width / width) # 画像がモデル入力サイズぴったりになる倍率(最小の倍率) min_scale = min(1.0, max(min_scale, size / (face_size * subset.face_crop_aug_range[1]))) # 指定した顔最小サイズ max_scale = min(1.0, max(min_scale, size / (face_size * subset.face_crop_aug_range[0]))) # 指定した顔最大サイズ if min_scale >= max_scale: # range指定がmin==max scale = min_scale else: scale = random.uniform(min_scale, max_scale) nh = int(height * scale + 0.5) nw = int(width * scale + 0.5) assert nh >= self.height and nw >= self.width, f"internal error. small scale {scale}, {width}*{height}" image = resize_image(image, width, height, nw, nh, subset.resize_interpolation) face_cx = int(face_cx * scale + 0.5) face_cy = int(face_cy * scale + 0.5) height, width = nh, nw # 顔を中心として448*640とかへ切り出す for axis, (target_size, length, face_p) in enumerate(zip((self.height, self.width), (height, width), (face_cy, face_cx))): p1 = face_p - target_size // 2 # 顔を中心に持ってくるための切り出し位置 if subset.random_crop: # 背景も含めるために顔を中心に置く確率を高めつつずらす range = max(length - face_p, face_p) # 画像の端から顔中心までの距離の長いほう p1 = p1 + (random.randint(0, range) + random.randint(0, range)) - range # -range ~ +range までのいい感じの乱数 else: # range指定があるときのみ、すこしだけランダムに(わりと適当) if subset.face_crop_aug_range[0] != subset.face_crop_aug_range[1]: if face_size > size // 10 and face_size >= 40: p1 = p1 + random.randint(-face_size // 20, +face_size // 20) p1 = max(0, min(p1, length - target_size)) if axis == 0: image = image[p1 : p1 + target_size, :] else: image = image[:, p1 : p1 + target_size] return image def __len__(self): return self._length def __getitem__(self, index): bucket = self.bucket_manager.buckets[self.buckets_indices[index].bucket_index] bucket_batch_size = self.buckets_indices[index].bucket_batch_size image_index = self.buckets_indices[index].batch_index * bucket_batch_size loss_weights = [] captions = [] input_ids_list = [] latents_list = [] alpha_mask_list = [] images = [] original_sizes_hw = [] crop_top_lefts = [] target_sizes_hw = [] flippeds = [] # 変数名が微妙 text_encoder_outputs_list = [] custom_attributes = [] masks = [] masked_images = [] for image_key in bucket[image_index : image_index + bucket_batch_size]: image_info = self.image_data[image_key] subset = self.image_to_subset[image_key] custom_attributes.append(subset.custom_attributes) # in case of fine tuning, is_reg is always False loss_weights.append(self.prior_loss_weight if image_info.is_reg else 1.0) flipped = subset.flip_aug and random.random() < 0.5 # not flipped or flipped with 50% chance # image/latentsを処理する if image_info.latents is not None: # cache_latents=Trueの場合 original_size = image_info.latents_original_size crop_ltrb = image_info.latents_crop_ltrb # calc values later if flipped if not flipped: latents = image_info.latents alpha_mask = image_info.alpha_mask else: latents = image_info.latents_flipped alpha_mask = None if image_info.alpha_mask is None else torch.flip(image_info.alpha_mask, [1]) image = None elif image_info.latents_npz is not None: # FineTuningDatasetまたはcache_latents_to_disk=Trueの場合 latents, original_size, crop_ltrb, flipped_latents, alpha_mask = ( self.latents_caching_strategy.load_latents_from_disk(image_info.latents_npz, image_info.bucket_reso) ) if flipped: latents = flipped_latents alpha_mask = None if alpha_mask is None else alpha_mask[:, ::-1].copy() # copy to avoid negative stride problem del flipped_latents latents = torch.FloatTensor(latents) if alpha_mask is not None: alpha_mask = torch.FloatTensor(alpha_mask) image = None else: # 画像を読み込み、必要ならcropする img, face_cx, face_cy, face_w, face_h = self.load_image_with_face_info( subset, image_info.absolute_path, subset.alpha_mask ) im_h, im_w = img.shape[0:2] if self.enable_bucket: img, original_size, crop_ltrb = trim_and_resize_if_required( subset.random_crop, img, image_info.bucket_reso, image_info.resized_size, resize_interpolation=image_info.resize_interpolation, ) else: if face_cx > 0: # 顔位置情報あり img = self.crop_target(subset, img, face_cx, face_cy, face_w, face_h) elif im_h > self.height or im_w > self.width: assert ( subset.random_crop ), f"image too large, but cropping and bucketing are disabled / 画像サイズが大きいのでface_crop_aug_rangeかrandom_crop、またはbucketを有効にしてください: {image_info.absolute_path}" if im_h > self.height: p = random.randint(0, im_h - self.height) img = img[p : p + self.height] if im_w > self.width: p = random.randint(0, im_w - self.width) img = img[:, p : p + self.width] im_h, im_w = img.shape[0:2] assert ( im_h == self.height and im_w == self.width ), f"image size is small / 画像サイズが小さいようです: {image_info.absolute_path}" original_size = [im_w, im_h] crop_ltrb = (0, 0, 0, 0) # augmentation aug = self.aug_helper.get_augmentor(subset.color_aug) if aug is not None: # augment RGB channels only img_rgb = img[:, :, :3] img_rgb = aug(image=img_rgb)["image"] img[:, :, :3] = img_rgb if flipped: img = img[:, ::-1, :].copy() # copy to avoid negative stride problem if subset.alpha_mask: if img.shape[2] == 4: alpha_mask = img[:, :, 3] # [H,W] alpha_mask = alpha_mask.astype(np.float32) / 255.0 # 0.0~1.0 alpha_mask = torch.FloatTensor(alpha_mask) else: alpha_mask = torch.ones((img.shape[0], img.shape[1]), dtype=torch.float32) else: alpha_mask = None img = img[:, :, :3] # remove alpha channel if self.train_inpainting: pil_image = transforms.functional.to_pil_image(img) mask = self.random_mask(pil_image.size) mask, masked_image = self.prepare_mask_and_masked_image(pil_image, mask) masks.append(mask) masked_images.append(masked_image) latents = None image = self.image_transforms(img) # -1.0~1.0のtorch.Tensorになる del img images.append(image) latents_list.append(latents) alpha_mask_list.append(alpha_mask) target_size = (image.shape[2], image.shape[1]) if image is not None else (latents.shape[2] * 8, latents.shape[1] * 8) if not flipped: crop_left_top = (crop_ltrb[0], crop_ltrb[1]) else: # crop_ltrb[2] is right, so target_size[0] - crop_ltrb[2] is left in flipped image crop_left_top = (target_size[0] - crop_ltrb[2], crop_ltrb[1]) original_sizes_hw.append((int(original_size[1]), int(original_size[0]))) crop_top_lefts.append((int(crop_left_top[1]), int(crop_left_top[0]))) target_sizes_hw.append((int(target_size[1]), int(target_size[0]))) flippeds.append(flipped) # captionとtext encoder outputを処理する caption = image_info.caption # default tokenization_required = ( self.text_encoder_output_caching_strategy is None or self.text_encoder_output_caching_strategy.is_partial ) text_encoder_outputs = None input_ids = None if image_info.text_encoder_outputs is not None: # cached text_encoder_outputs = image_info.text_encoder_outputs elif image_info.text_encoder_outputs_npz is not None: # on disk text_encoder_outputs = self.text_encoder_output_caching_strategy.load_outputs_npz( image_info.text_encoder_outputs_npz ) else: tokenization_required = True text_encoder_outputs_list.append(text_encoder_outputs) if tokenization_required: caption = self.process_caption(subset, image_info.caption) input_ids = [ids[0] for ids in self.tokenize_strategy.tokenize(caption)] # remove batch dimension input_ids_list.append(input_ids) captions.append(caption) def none_or_stack_elements(tensors_list, converter): # [[clip_l, clip_g, t5xxl], [clip_l, clip_g, t5xxl], ...] -> [torch.stack(clip_l), torch.stack(clip_g), torch.stack(t5xxl)] if len(tensors_list) == 0 or tensors_list[0] == None or len(tensors_list[0]) == 0 or tensors_list[0][0] is None: return None # old implementation without padding: all elements must have same length # return [torch.stack([converter(x[i]) for x in tensors_list]) for i in range(len(tensors_list[0]))] # new implementation with padding support result = [] for i in range(len(tensors_list[0])): tensors = [x[i] for x in tensors_list] if tensors[0].ndim == 0: # scalar value: e.g. ocr mask result.append(torch.stack([converter(x[i]) for x in tensors_list])) continue min_len = min([len(x) for x in tensors]) max_len = max([len(x) for x in tensors]) if min_len == max_len: # no padding result.append(torch.stack([converter(x) for x in tensors])) else: # padding tensors = [converter(x) for x in tensors] if tensors[0].ndim == 1: # input_ids or mask result.append(torch.stack([(torch.nn.functional.pad(x, (0, max_len - x.shape[0]))) for x in tensors])) else: # text encoder outputs result.append(torch.stack([(torch.nn.functional.pad(x, (0, 0, 0, max_len - x.shape[0]))) for x in tensors])) return result # set example example = {} example["custom_attributes"] = custom_attributes # may be list of empty dict example["loss_weights"] = torch.FloatTensor(loss_weights) example["text_encoder_outputs_list"] = none_or_stack_elements(text_encoder_outputs_list, torch.FloatTensor) example["input_ids_list"] = none_or_stack_elements(input_ids_list, lambda x: x) # if one of alpha_masks is not None, we need to replace None with ones none_or_not = [x is None for x in alpha_mask_list] if all(none_or_not): example["alpha_masks"] = None elif any(none_or_not): for i in range(len(alpha_mask_list)): if alpha_mask_list[i] is None: if images[i] is not None: alpha_mask_list[i] = torch.ones((images[i].shape[1], images[i].shape[2]), dtype=torch.float32) else: alpha_mask_list[i] = torch.ones( (latents_list[i].shape[1] * 8, latents_list[i].shape[2] * 8), dtype=torch.float32 ) example["alpha_masks"] = torch.stack(alpha_mask_list) else: example["alpha_masks"] = torch.stack(alpha_mask_list) if images[0] is not None: images = torch.stack(images) images = images.to(memory_format=torch.contiguous_format).float() else: images = None example["images"] = images example["masks"] = torch.stack(masks) if masks else None example["masked_images"] = torch.stack(masked_images) if masked_images else None example["latents"] = torch.stack(latents_list) if latents_list[0] is not None else None example["captions"] = captions example["original_sizes_hw"] = torch.stack([torch.LongTensor(x) for x in original_sizes_hw]) example["crop_top_lefts"] = torch.stack([torch.LongTensor(x) for x in crop_top_lefts]) example["target_sizes_hw"] = torch.stack([torch.LongTensor(x) for x in target_sizes_hw]) example["flippeds"] = flippeds example["network_multipliers"] = torch.FloatTensor([self.network_multiplier] * len(captions)) if self.debug_dataset: example["image_keys"] = bucket[image_index : image_index + self.batch_size] return example @staticmethod def prepare_mask_and_masked_image(image, mask): image = np.array(image.convert("RGB")) image = image.transpose(2, 0, 1) # HWC -> CHW image = torch.from_numpy(image).to(dtype=torch.float32) / 127.5 - 1.0 mask = np.array(mask.convert("L")) mask = mask.astype(np.float32) / 255.0 mask = mask[None] # 1,H,W mask[mask < 0.5] = 0 mask[mask >= 0.5] = 1 mask = torch.from_numpy(mask) masked_image = image * (mask < 0.5) return mask, masked_image # generate random masks @staticmethod def random_mask(im_shape): from library.mask_generator import random_mask as _random_mask w, h = im_shape return _random_mask(w, h) class DatasetGroup(torch.utils.data.ConcatDataset): # DreamBoothDataset / FineTuningDataset still live in library.train_util (PR-1d will move them). def __init__(self, datasets: 'Sequence[Union[DreamBoothDataset, FineTuningDataset]]'): self.datasets: 'List[Union[DreamBoothDataset, FineTuningDataset]]' super().__init__(datasets) self.image_data = {} self.num_train_images = 0 self.num_reg_images = 0 # simply concat together # TODO: handling image_data key duplication among dataset # In practical, this is not the big issue because image_data is accessed from outside of dataset only for debug_dataset. for dataset in datasets: self.image_data.update(dataset.image_data) self.num_train_images += dataset.num_train_images self.num_reg_images += dataset.num_reg_images def add_replacement(self, str_from, str_to): for dataset in self.datasets: dataset.add_replacement(str_from, str_to) # def make_buckets(self): # for dataset in self.datasets: # dataset.make_buckets() def set_text_encoder_output_caching_strategy(self, strategy: TextEncoderOutputsCachingStrategy): """ DataLoader is run in multiple processes, so we need to set the strategy manually. """ for dataset in self.datasets: dataset.set_text_encoder_output_caching_strategy(strategy) def enable_XTI(self, *args, **kwargs): for dataset in self.datasets: dataset.enable_XTI(*args, **kwargs) def new_cache_latents(self, model: Any, accelerator: Accelerator): for i, dataset in enumerate(self.datasets): logger.info(f"[Dataset {i}]") dataset.new_cache_latents(model, accelerator) accelerator.wait_for_everyone() def new_cache_text_encoder_outputs(self, models: List[Any], accelerator: Accelerator): for i, dataset in enumerate(self.datasets): logger.info(f"[Dataset {i}]") dataset.new_cache_text_encoder_outputs(models, accelerator) accelerator.wait_for_everyone() def verify_bucket_reso_steps(self, min_steps: int): for dataset in self.datasets: dataset.verify_bucket_reso_steps(min_steps) def get_resolutions(self) -> List[Tuple[int, int]]: return [(dataset.width, dataset.height) for dataset in self.datasets] def is_latent_cacheable(self) -> bool: return all([dataset.is_latent_cacheable() for dataset in self.datasets]) def is_text_encoder_output_cacheable(self, cache_supports_dropout: bool = False) -> bool: return all([dataset.is_text_encoder_output_cacheable(cache_supports_dropout) for dataset in self.datasets]) def set_current_strategies(self): for dataset in self.datasets: dataset.set_current_strategies() def set_current_epoch(self, epoch): for dataset in self.datasets: dataset.set_current_epoch(epoch) def set_current_step(self, step): for dataset in self.datasets: dataset.set_current_step(step) def set_max_train_steps(self, max_train_steps): for dataset in self.datasets: dataset.set_max_train_steps(max_train_steps) def disable_token_padding(self): for dataset in self.datasets: dataset.disable_token_padding() def debug_dataset(train_dataset, show_input_ids=False): logger.info(f"Total dataset length (steps) / データセットの長さ(ステップ数): {len(train_dataset)}") logger.info( "`S` for next step, `E` for next epoch no. , Escape for exit. / Sキーで次のステップ、Eキーで次のエポック、Escキーで中断、終了します" ) epoch = 1 while True: logger.info(f"") logger.info(f"epoch: {epoch}") steps = (epoch - 1) * len(train_dataset) + 1 indices = list(range(len(train_dataset))) random.shuffle(indices) k = 0 for i, idx in enumerate(indices): train_dataset.set_current_epoch(epoch) train_dataset.set_current_step(steps) logger.info(f"steps: {steps} ({i + 1}/{len(train_dataset)})") example = train_dataset[idx] if example["latents"] is not None: logger.info(f"sample has latents from npz file: {example['latents'].size()}") for j, (ik, cap, lw, orgsz, crptl, trgsz, flpdz) in enumerate( zip( example["image_keys"], example["captions"], example["loss_weights"], # example["input_ids"], example["original_sizes_hw"], example["crop_top_lefts"], example["target_sizes_hw"], example["flippeds"], ) ): logger.info( f'{ik}, size: {train_dataset.image_data[ik].image_size}, loss weight: {lw}, caption: "{cap}", original size: {orgsz}, crop top left: {crptl}, target size: {trgsz}, flipped: {flpdz}' ) if "network_multipliers" in example: logger.info(f"network multiplier: {example['network_multipliers'][j]}") if "custom_attributes" in example: logger.info(f"custom attributes: {example['custom_attributes'][j]}") # if show_input_ids: # logger.info(f"input ids: {iid}") # if "input_ids2" in example: # logger.info(f"input ids2: {example['input_ids2'][j]}") if example["images"] is not None: im = example["images"][j] logger.info(f"image size: {im.size()}") im = ((im.numpy() + 1.0) * 127.5).astype(np.uint8) im = np.transpose(im, (1, 2, 0)) # c,H,W -> H,W,c im = im[:, :, ::-1] # RGB -> BGR (OpenCV) if "conditioning_images" in example or "masked_images" in example: cond_img = example["conditioning_images"][j] if "conditioning_images" in example else example["masked_images"][j] logger.info(f"conditioning image size: {cond_img.size()}") cond_img = ((cond_img.numpy() + 1.0) * 127.5).astype(np.uint8) cond_img = np.transpose(cond_img, (1, 2, 0)) cond_img = cond_img[:, :, ::-1] if os.name == "nt": cv2.imshow("cond_img", cond_img) if "alpha_masks" in example and example["alpha_masks"] is not None: alpha_mask = example["alpha_masks"][j] logger.info(f"alpha mask size: {alpha_mask.size()}") alpha_mask = (alpha_mask.numpy() * 255.0).astype(np.uint8) if os.name == "nt": cv2.imshow("alpha_mask", alpha_mask) if os.name == "nt": # only windows cv2.imshow("img", im) k = cv2.waitKey() cv2.destroyAllWindows() if k == 27 or k == ord("s") or k == ord("e"): break steps += 1 if k == ord("e"): break if k == 27 or (example["images"] is None and i >= 8): k = 27 break if k == 27: break epoch += 1 def glob_images(directory, base="*"): img_paths = [] for ext in IMAGE_EXTENSIONS: if base == "*": img_paths.extend(glob.glob(os.path.join(glob.escape(directory), base + ext))) else: img_paths.extend(glob.glob(glob.escape(os.path.join(directory, base + ext)))) img_paths = list(set(img_paths)) # 重複を排除 img_paths.sort() return img_paths def glob_images_pathlib(dir_path, recursive): image_paths = [] if recursive: for ext in IMAGE_EXTENSIONS: image_paths += list(dir_path.rglob("*" + ext)) else: for ext in IMAGE_EXTENSIONS: image_paths += list(dir_path.glob("*" + ext)) image_paths = list(set(image_paths)) # 重複を排除 image_paths.sort() return image_paths class MinimalDataset(BaseDataset): def __init__(self, resolution, network_multiplier, train_inpainting=False, debug_dataset=False): super().__init__(resolution, network_multiplier, train_inpainting, debug_dataset) self.num_train_images = 0 # update in subclass self.num_reg_images = 0 # update in subclass self.datasets = [self] self.batch_size = 1 # update in subclass self.subsets = [self] self.num_repeats = 1 # update in subclass if needed self.img_count = 1 # update in subclass if needed self.bucket_info = {} self.is_reg = False self.image_dir = "dummy" # for metadata def verify_bucket_reso_steps(self, min_steps: int): pass def is_latent_cacheable(self) -> bool: return False def __len__(self): raise NotImplementedError # override to avoid shuffling buckets def set_current_epoch(self, epoch): self.current_epoch = epoch def __getitem__(self, idx): r""" The subclass may have image_data for debug_dataset, which is a dict of ImageInfo objects. Returns: example like this: for i in range(batch_size): image_key = ... # whatever hashable image_keys.append(image_key) image = ... # PIL Image img_tensor = self.image_transforms(img) images.append(img_tensor) caption = ... # str input_ids = [ids[0] for ids in self.tokenize_strategy.tokenize(caption)] input_ids_list.append(input_ids) captions.append(caption) images = torch.stack(images, dim=0) input_ids_list = torch.stack(input_ids_list, dim=0) example = { "images": images, "input_ids": input_ids_list, "captions": captions, # for debug_dataset "latents": None, "image_keys": image_keys, # for debug_dataset "loss_weights": torch.ones(batch_size, dtype=torch.float32), } return example """ raise NotImplementedError def get_resolutions(self) -> List[Tuple[int, int]]: return [] def load_arbitrary_dataset(args, tokenizer=None) -> MinimalDataset: module = ".".join(args.dataset_class.split(".")[:-1]) dataset_class = args.dataset_class.split(".")[-1] module = importlib.import_module(module) dataset_class = getattr(module, dataset_class) train_dataset_group: MinimalDataset = dataset_class(tokenizer, args.max_token_length, args.resolution, args.debug_dataset) return train_dataset_group # collate_fn 用 epoch, step は multiprocessing.Value class collator_class: def __init__(self, epoch, step, dataset): self.current_epoch = epoch self.current_step = step self.dataset = dataset # not used if worker_info is not None, in case of multiprocessing def __call__(self, examples): worker_info = torch.utils.data.get_worker_info() # worker_info is None in the main process if worker_info is not None: dataset = worker_info.dataset else: dataset = self.dataset # set epoch and step dataset.set_current_epoch(self.current_epoch.value) dataset.set_current_step(self.current_step.value) return examples[0]