"""DreamBooth dataset: directory-based image+caption pairs with optional regularization images. Extracted from ``library.train_util`` as part of the dataset-split refactor; imports the abstract :class:`~library.dataset.BaseDataset` and its :class:`~library.subset.DreamBoothSubset` configuration class. """ import glob import json import logging import os from typing import List, Optional, Sequence, Tuple from tqdm import tqdm from library.dataset import ( BaseDataset, ImageInfo, glob_images, split_train_val, ) from library.strategy_base import LatentsCachingStrategy from library.subset import DreamBoothSubset from library.utils import setup_logging setup_logging() logger = logging.getLogger(__name__) def read_caption( img_path: str, caption_extension: str, enable_wildcard: bool, enable_multiline_caption: bool = False ) -> Optional[str]: """Read the caption file for an image. Returns None if no caption file is found. - enable_multiline_caption: the whole file is returned as is (line breaks are kept, separator lines are not processed here; see ``library.dataset.split_caption_candidates``). - enable_wildcard: non-empty lines are stripped and joined with ``\\n`` (one line is chosen randomly later). - otherwise: only the first line is used. """ # captionの候補ファイル名を作る base_name = os.path.splitext(img_path)[0] base_name_face_det = base_name tokens = base_name.split("_") if len(tokens) >= 5: base_name_face_det = "_".join(tokens[:-4]) cap_paths = [base_name + caption_extension, base_name_face_det + caption_extension] caption = None for cap_path in cap_paths: if os.path.isfile(cap_path): with open(cap_path, "rt", encoding="utf-8") as f: try: lines = f.readlines() except UnicodeDecodeError as e: logger.error(f"illegal char in file (not UTF-8) / ファイルにUTF-8以外の文字があります: {cap_path}") raise e assert len(lines) > 0, f"caption file is empty / キャプションファイルが空です: {cap_path}" if enable_multiline_caption: caption = "".join(lines) # 改行を含めてそのまま(正規化は候補分割時に行う) elif enable_wildcard: caption = "\n".join([line.strip() for line in lines if line.strip() != ""]) # 空行を除く、改行で連結 else: caption = lines[0].strip() break return caption class DreamBoothDataset(BaseDataset): IMAGE_INFO_CACHE_FILE = "metadata_cache.json" # The is_training_dataset defines the type of dataset, training or validation # if is_training_dataset is True -> training dataset # if is_training_dataset is False -> validation dataset def __init__( self, subsets: Sequence[DreamBoothSubset], is_training_dataset: bool, batch_size: int, resolution, network_multiplier: float, enable_bucket: bool, min_bucket_reso: int, max_bucket_reso: int, bucket_reso_steps: int, bucket_no_upscale: bool, prior_loss_weight: float, train_inpainting: bool, debug_dataset: bool, validation_split: float, validation_seed: Optional[int], resize_interpolation: Optional[str], skip_image_resolution: Optional[Tuple[int, int]] = None, ) -> None: super().__init__( resolution, network_multiplier, train_inpainting, debug_dataset, resize_interpolation, skip_image_resolution, ) assert resolution is not None, f"resolution is required / resolution(解像度)指定は必須です" self.batch_size = batch_size self.size = min(self.width, self.height) # 短いほう self.prior_loss_weight = prior_loss_weight self.latents_cache = None self.is_training_dataset = is_training_dataset self.validation_seed = validation_seed self.validation_split = validation_split self.enable_bucket = enable_bucket if self.enable_bucket: min_bucket_reso, max_bucket_reso = self.adjust_min_max_bucket_reso_by_steps( resolution, min_bucket_reso, max_bucket_reso, bucket_reso_steps ) self.min_bucket_reso = min_bucket_reso self.max_bucket_reso = max_bucket_reso self.bucket_reso_steps = bucket_reso_steps self.bucket_no_upscale = bucket_no_upscale else: self.min_bucket_reso = None self.max_bucket_reso = None self.bucket_reso_steps = None # この情報は使われない self.bucket_no_upscale = False def load_dreambooth_dir(subset: DreamBoothSubset): if not os.path.isdir(subset.image_dir): logger.warning(f"not directory: {subset.image_dir}") return [], [], [] info_cache_file = os.path.join(subset.image_dir, self.IMAGE_INFO_CACHE_FILE) use_cached_info_for_subset = subset.cache_info if use_cached_info_for_subset: logger.info( f"using cached image info for this subset / このサブセットで、キャッシュされた画像情報を使います: {info_cache_file}" ) if not os.path.isfile(info_cache_file): logger.warning( f"image info file not found. You can ignore this warning if this is the first time to use this subset" + " / キャッシュファイルが見つかりませんでした。初回実行時はこの警告を無視してください: {metadata_file}" ) use_cached_info_for_subset = False if use_cached_info_for_subset: # json: {`img_path`:{"caption": "caption...", "resolution": [width, height]}, ...} with open(info_cache_file, "r", encoding="utf-8") as f: metas = json.load(f) img_paths = list(metas.keys()) sizes: List[Optional[Tuple[int, int]]] = [meta["resolution"] for meta in metas.values()] # we may need to check image size and existence of image files, but it takes time, so user should check it before training else: img_paths = glob_images(subset.image_dir, "*") sizes: List[Optional[Tuple[int, int]]] = [None] * len(img_paths) # new caching: get image size from cache files strategy = LatentsCachingStrategy.get_strategy() if strategy is not None: logger.info("get image size from name of cache files") # make image path to npz path mapping npz_paths = glob.glob(os.path.join(subset.image_dir, "*" + strategy.cache_suffix)) npz_paths.sort( key=lambda item: item.rsplit("_", maxsplit=2)[0] ) # sort by name excluding resolution and cache_suffix npz_path_index = 0 size_set_count = 0 for i, img_path in enumerate(tqdm(img_paths)): l = len(os.path.splitext(img_path)[0]) # remove extension found = False while npz_path_index < len(npz_paths): # until found or end of npz_paths # npz_paths are sorted, so if npz_path > img_path, img_path is not found if npz_paths[npz_path_index][:l] > img_path[:l]: break if npz_paths[npz_path_index][:l] == img_path[:l]: # found found = True break npz_path_index += 1 # next npz_path if found: w, h = strategy.get_image_size_from_disk_cache_path(img_path, npz_paths[npz_path_index]) else: w, h = None, None if w is not None and h is not None: sizes[i] = (w, h) size_set_count += 1 logger.info(f"set image size from cache files: {size_set_count}/{len(img_paths)}") if self.skip_image_resolution is not None: filtered_img_paths = [] filtered_sizes = [] skip_image_area = self.skip_image_resolution[0] * self.skip_image_resolution[1] for img_path, size in zip(img_paths, sizes): if size is None: # no latents cache file, get image size by reading image file (slow) size = self.get_image_size(img_path) if size[0] * size[1] <= skip_image_area: continue filtered_img_paths.append(img_path) filtered_sizes.append(size) if len(filtered_img_paths) < len(img_paths): logger.info( f"filtered {len(img_paths) - len(filtered_img_paths)} images by original resolution from {subset.image_dir}" ) img_paths = filtered_img_paths sizes = filtered_sizes # We want to create a training and validation split. This should be improved in the future # to allow a clearer distinction between training and validation. This can be seen as a # short-term solution to limit what is necessary to implement validation datasets # # We split the dataset for the subset based on if we are doing a validation split # The self.is_training_dataset defines the type of dataset, training or validation # if self.is_training_dataset is True -> training dataset # if self.is_training_dataset is False -> validation dataset if self.validation_split > 0.0: # For regularization images we do not want to split this dataset. if subset.is_reg is True: # Skip any validation dataset for regularization images if self.is_training_dataset is False: img_paths = [] sizes = [] # Otherwise the img_paths remain as original img_paths and no split # required for training images dataset of regularization images else: img_paths, sizes = split_train_val( img_paths, sizes, self.is_training_dataset, self.validation_split, self.validation_seed ) logger.info(f"found directory {subset.image_dir} contains {len(img_paths)} image files") if use_cached_info_for_subset: captions = [metas[img_path]["caption"] for img_path in img_paths] missing_captions = [img_path for img_path, caption in zip(img_paths, captions) if caption is None or caption == ""] else: # 画像ファイルごとにプロンプトを読み込み、もしあればそちらを使う captions = [] missing_captions = [] for img_path in tqdm(img_paths, desc="read caption"): cap_for_img = read_caption( img_path, subset.caption_extension, subset.enable_wildcard, subset.enable_multiline_caption ) if cap_for_img is None and subset.class_tokens is None: logger.warning( f"neither caption file nor class tokens are found. use empty caption for {img_path} / キャプションファイルもclass tokenも見つかりませんでした。空のキャプションを使用します: {img_path}" ) captions.append("") missing_captions.append(img_path) else: if cap_for_img is None: captions.append(subset.class_tokens) missing_captions.append(img_path) else: captions.append(cap_for_img) # validate separator lines of multi-line captions (raises ValueError if invalid) self.validate_caption_candidates(subset, captions) self.set_tag_frequency(os.path.basename(subset.image_dir), captions) # タグ頻度を記録 if missing_captions: number_of_missing_captions = len(missing_captions) number_of_missing_captions_to_show = 5 remaining_missing_captions = number_of_missing_captions - number_of_missing_captions_to_show logger.warning( f"No caption file found for {number_of_missing_captions} images. Training will continue without captions for these images. If class token exists, it will be used. / {number_of_missing_captions}枚の画像にキャプションファイルが見つかりませんでした。これらの画像についてはキャプションなしで学習を続行します。class tokenが存在する場合はそれを使います。" ) for i, missing_caption in enumerate(missing_captions): if i >= number_of_missing_captions_to_show: logger.warning(missing_caption + f"... and {remaining_missing_captions} more") break logger.warning(missing_caption) if not use_cached_info_for_subset and subset.cache_info: logger.info(f"cache image info for / 画像情報をキャッシュします : {info_cache_file}") sizes = [self.get_image_size(img_path) for img_path in tqdm(img_paths, desc="get image size")] matas = {} for img_path, caption, size in zip(img_paths, captions, sizes): matas[img_path] = {"caption": caption, "resolution": list(size)} with open(info_cache_file, "w", encoding="utf-8") as f: json.dump(matas, f, ensure_ascii=False, indent=2) logger.info(f"cache image info done for / 画像情報を出力しました : {info_cache_file}") # if sizes are not set, image size will be read in make_buckets return img_paths, captions, sizes logger.info("prepare images.") num_train_images = 0 num_reg_images = 0 reg_infos: List[Tuple[ImageInfo, DreamBoothSubset]] = [] for subset in subsets: num_repeats = subset.num_repeats if self.is_training_dataset else 1 if num_repeats < 1: logger.warning( f"ignore subset with image_dir='{subset.image_dir}': num_repeats is less than 1 / num_repeatsが1を下回っているためサブセットを無視します: {num_repeats}" ) continue if subset in self.subsets: logger.warning( f"ignore duplicated subset with image_dir='{subset.image_dir}': use the first one / 既にサブセットが登録されているため、重複した後発のサブセットを無視します" ) continue img_paths, captions, sizes = load_dreambooth_dir(subset) if len(img_paths) < 1: logger.warning( f"ignore subset with image_dir='{subset.image_dir}': no images found / 画像が見つからないためサブセットを無視します" ) continue if subset.is_reg: num_reg_images += num_repeats * len(img_paths) else: num_train_images += num_repeats * len(img_paths) for img_path, caption, size in zip(img_paths, captions, sizes): info = ImageInfo(img_path, num_repeats, caption, subset.is_reg, img_path, subset.caption_dropout_rate) info.resize_interpolation = ( subset.resize_interpolation if subset.resize_interpolation is not None else self.resize_interpolation ) if size is not None: info.image_size = size if subset.is_reg: reg_infos.append((info, subset)) else: self.register_image(info, subset) subset.img_count = len(img_paths) self.subsets.append(subset) images_split_name = "train" if self.is_training_dataset else "validation" logger.info(f"{num_train_images} {images_split_name} images with repeats.") self.num_train_images = num_train_images logger.info(f"{num_reg_images} reg images with repeats.") if num_train_images < num_reg_images: logger.warning("some of reg images are not used / 正則化画像の数が多いので、一部使用されない正則化画像があります") if num_reg_images == 0: logger.warning("no regularization images / 正則化画像が見つかりませんでした") else: # num_repeatsを計算する:どうせ大した数ではないのでループで処理する n = 0 first_loop = True while n < num_train_images: for info, subset in reg_infos: if first_loop: self.register_image(info, subset) n += info.num_repeats else: info.num_repeats += 1 # rewrite registered info n += 1 if n >= num_train_images: break first_loop = False self.num_reg_images = num_reg_images