diff --git a/__init__.py b/__init__.py index e8fc2b1..32bfecc 100644 --- a/__init__.py +++ b/__init__.py @@ -5,7 +5,7 @@ NODE_CLASS_MAPPINGS = { "LatentQuilting_Bmad": LatentQuilting, "ImageQuiltingSeamlessMB_Bmad": ImageMakeSeamlessMB, "ImageQuiltingSeamlessSB_Bmad": ImageMakeSeamlessSB, - #"GuessQuiltingBlockSize": GuessBlockSize + "GuessQuiltingBlockSize": GuessNiceBlockSize } NODE_DISPLAY_NAME_MAPPINGS = { @@ -13,5 +13,5 @@ NODE_DISPLAY_NAME_MAPPINGS = { "LatentQuilting_Bmad": "Latent Quilting", "ImageQuiltingSeamlessMB_Bmad": "Image Seamless Quilting MP", "ImageQuiltingSeamlessSB_Bmad": "Image Seamless Quilting SP", - # "GuessQuiltingBlockSize": "Guess BlockSize (Quilting)" + "GuessQuiltingBlockSize": "Guess Quilting Block Size" } \ No newline at end of file diff --git a/guess_block_size.py b/guess_block_size.py index 24aaed2..cd4e5e4 100644 --- a/guess_block_size.py +++ b/guess_block_size.py @@ -27,14 +27,15 @@ def find_sync_wavelen(pairs: size_weight_pairs, lower, upper) -> num_pixels: return best_number -def make_guess(pairs: size_weight_pairs, min_dim: num_pixels) -> num_pixels: +def make_guess(pairs: size_weight_pairs, min_dim: num_pixels, max_block_size: num_pixels|None = None) -> num_pixels: default_value = round(min_dim / 2.5) # returned in edge cases if len(pairs) == 0: # an edge case; maybe a blank image is sent... return default_value block_size_lower_bound = pairs[0][0] - block_size_upper_bound = round(min_dim / 1.2) + block_size_upper_bound = round(min_dim / 1.2) if max_block_size is None \ + else min(round(min_dim / 1.2), max_block_size) print(f"initial upper bound = {block_size_upper_bound}") # the lookup should have at least one single freq sized block of addressable space @@ -59,7 +60,11 @@ def filter_pairs_by_weight(pairs: size_weight_pairs, weight_percentage_threshold return filtered_pairs -def guess_nice_block_size(src: np.ndarray) -> num_pixels: +def guess_nice_block_size(src: np.ndarray, freq_analysis_only: bool = False, + max_block_size: num_pixels|None = None ) -> num_pixels: + """ + @param src: numpy image with normalized float32 values + """ def normalize_weights(pairs: size_weight_pairs): if not pairs: return [] @@ -71,17 +76,21 @@ def guess_nice_block_size(src: np.ndarray) -> num_pixels: normalized_pairs = [(index, 0) for index, weight in pairs] return normalized_pairs - freq_analysis_pairs = analyze_freq_spectrum(src) - desc_analysis_pairs = analyze_keypoint_scales(src) - # all should come already sorted in descending order w/ respect to weight + # src should come with normalized float values already + freq_analysis_pairs = analyze_freq_spectrum(src) # here the image needs to go with float normalized values + src = (src*255).astype(np.uint8) + # here the image needs to go with integer, 0 to 255, values + desc_analysis_pairs = [] if freq_analysis_only else analyze_keypoint_scales(src) + + # all pairs should come already sorted in descending order w/ respect to weight print(freq_analysis_pairs) print(desc_analysis_pairs) # filter very small distances, with respect to the src size - min_dim = min(image.shape[:2]) + min_dim = min(src.shape[:2]) thresh_distance = ceil(min_dim ** (1 / 4)) - block_size_upper_bound = round(min_dim / 1.2) + block_size_upper_bound = round(min_dim / 1.2) if max_block_size is None else max_block_size freq_analysis_pairs = [(dst, w) for dst, w in freq_analysis_pairs if thresh_distance <= dst < block_size_upper_bound] desc_analysis_pairs = [(dst, w) for dst, w in desc_analysis_pairs if @@ -97,7 +106,7 @@ def guess_nice_block_size(src: np.ndarray) -> num_pixels: ] # may contain duplicates or multiples, that is expected print(f"final pairs: {final_pairs}") - return make_guess(final_pairs, min_dim) + return make_guess(final_pairs, min_dim, max_block_size) if __name__ == "__main__": diff --git a/misc/bse_desc_util.py b/misc/bse_desc_util.py index 699b00e..54c5a3c 100644 --- a/misc/bse_desc_util.py +++ b/misc/bse_desc_util.py @@ -12,6 +12,9 @@ label: TypeAlias = int def find_optimal_clusters(data: list, max_k: int = 6) -> tuple[list[label], list[...], int, float]: + if len(np.unique(data)) == 1: # edge case + return [0] * len(data), [data[0]], 1, 0 + iters = range(2, max_k + 1) best_k = 2 best_score = -1.0 @@ -81,6 +84,8 @@ def inner_square_area(circle_diameter: float) -> float: def analyze_keypoint_scales(image: np.ndarray) -> size_weight_pairs: sift = cv2.SIFT_create() keypoints = sift.detect(image, None) + if len(keypoints) == 0: # edge case + return [] kp_sizes = [kp.size for kp in keypoints] # keypoints' diameters, in pixels kp_pts = [kp.pt for kp in keypoints] # keypoints' (y, x) positions @@ -104,7 +109,7 @@ def analyze_keypoint_scales(image: np.ndarray) -> size_weight_pairs: if __name__ == "__main__": - image_path = '../t18.png' + image_path = '../t9.png' image = cv2.imread(image_path) data = analyze_keypoint_scales(image) print(data) diff --git a/misc/bse_ft_util.py b/misc/bse_ft_util.py index f98bebc..d581166 100644 --- a/misc/bse_ft_util.py +++ b/misc/bse_ft_util.py @@ -4,8 +4,8 @@ import cv2 def compute_fft(image): - image_float = np.float32(image) - dft = cv2.dft(image_float, flags=cv2.DFT_COMPLEX_OUTPUT) + image = np.float32(image) + dft = cv2.dft(image, flags=cv2.DFT_COMPLEX_OUTPUT) dft_shift = np.fft.fftshift(dft) magnitude_spectrum = cv2.magnitude(dft_shift[:, :, 0], dft_shift[:, :, 1]) return magnitude_spectrum diff --git a/nodes.py b/nodes.py index 7a17186..e6a41bc 100644 --- a/nodes.py +++ b/nodes.py @@ -16,38 +16,42 @@ import cv2 NODES_CATEGORY = "Bmad/CV/Quilting" SEAMLESS_DIRS = ["H", "V", "H & V"] # options for seamless nodes -QUILTING_SHARED_INPUT_TYPES = { - # block size is given in pixels - "block_size": ("INT", {"default": 20, "min": 3, "max": 256, "step": 1}), - # the percentage of pixels that overlap between each block_sized block - "overlap": ("FLOAT", {"default": 1 / 6.0, "min": .1, "max": .9, "step": .01}), +def get_quilting_shared_input_types(): + return { + # block size is given in pixels. + # if inferior to 3, guess_nice_block_size is used instead. + # if negative, guess_nice_block_size only does freq. analysis (should be considerably faster) + "block_size": ("INT", {"default": -1, "min": -1, "max": 512, "step": 1}), - # this is a percentage relative to min error when searching for a patch. - # the ones that within tolerance are potential candidates to be selected. - # tolerance equal to 1 means a tolerance of 2 times the min error. - # my interpretation regarding its application is that - # tolerance can help prevent too much sameness in the texture - # due to some subset of patches being better at minimizing the error. - "tolerance": ("FLOAT", {"default": .1, "min": 0, "max": 2, "step": .01}), + # the percentage of pixels that overlap between each block_sized block + "overlap": ("FLOAT", {"default": 1 / 5.0, "min": .1, "max": .9, "step": .01}), - # 0 -> no parallelization; uses the reference implementation - # 1 -> 4 jobs used ( divides generation into 4 sections ) - # 2 and above -> the number of jobs per section - # parallelization lvl also affects output; even if seed is fixed, changing this will change the output - "parallelization_lvl": ("INT", {"default": 1, "min": 0, "max": 6, "step": 1}), + # this is a percentage relative to min error when searching for a patch. + # the ones that within tolerance are potential candidates to be selected. + # tolerance equal to 1 means a tolerance of 2 times the min error. + # my interpretation regarding its application is that + # tolerance can help prevent too much sameness in the texture + # due to some subset of patches being better at minimizing the error. + "tolerance": ("FLOAT", {"default": .1, "min": 0, "max": 2, "step": .01}), - "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + # 0 -> no parallelization; uses the reference implementation + # 1 -> 4 jobs used ( divides generation into 4 sections ) + # 2 and above -> the number of jobs per section + # parallelization lvl also affects output; even if seed is fixed, changing this will change the output + "parallelization_lvl": ("INT", {"default": 1, "min": 0, "max": 6, "step": 1}), - "version": ("INT", {"default": 1, "min": 0, "max": 3}), -} + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + + "version": ("INT", {"default": 1, "min": 0, "max": 3}), + } @dataclass class QuiltingFuncWrapper: """Wraps node functionality for easy re-use when using jobs.""" - block_size: int - overlap: int + block_sizes: list[int] + overlap: float out_h: int out_w: int tolerance: float @@ -94,29 +98,43 @@ def terminate_generation(finished_event, jobs_shared_memory, pbt: Thread): throw_exception_if_processing_interrupted() -def setup_pbar_quilting(block_size, overlap, out_height, out_width, par_lvl, batch_len): - n_rows = int(np.ceil((out_height - block_size) * 1.0 / (block_size - overlap))) - n_columns = int(np.ceil((out_width - block_size) * 1.0 / (block_size - overlap))) - total_steps: int = ((n_rows + 1) * (n_columns + 1) - 1) * batch_len # ignores first corner/center +def setup_pbar_quilting(block_sizes: list[int], overlap_percentage: float, out_height, out_width, par_lvl): + """ + @param block_sizes: block size for each item in the batch. len(block_sizes) = batch length + """ + total_steps: int = 0 + batch_len: int = len(block_sizes) + + for block_size in block_sizes: + overlap = overlap_percentage_to_pixels(block_size, overlap_percentage) + n_rows = int(np.ceil((out_height - block_size) * 1.0 / (block_size - overlap))) + n_columns = int(np.ceil((out_width - block_size) * 1.0 / (block_size - overlap))) + total_steps += ((n_rows + 1) * (n_columns + 1) - 1) # ignores first corner/center # might be less than the generated when using parallel solution, but not by far, so will leave it like this for now return setup_pbar(total_steps, par_lvl, batch_len) -def setup_pbar_seamless(ori, block_size, overlap, height, width, batch_len): +def setup_pbar_seamless(ori, block_sizes: list[int], overlap_percentage: float, height, width, batch_len): + """ + here batch_len may differ from block_sizes due to the number of lookup textures + """ from .make_seamless import get_numb_of_blocks_to_fill_stripe - match ori: - case "H": - total_steps = get_numb_of_blocks_to_fill_stripe(block_size, overlap, width) - case "V": - total_steps = get_numb_of_blocks_to_fill_stripe(block_size, overlap, height) - case _: - total_steps = ( - 2 + - get_numb_of_blocks_to_fill_stripe(block_size, overlap, height) + - get_numb_of_blocks_to_fill_stripe(block_size, overlap, width) - ) - total_steps = total_steps * batch_len + total_steps: int = 0 + for i in range(batch_len): + block_size = block_sizes[min(i, len(block_sizes) - 1)] + overlap = overlap_percentage_to_pixels(block_size, overlap_percentage) + match ori: + case "H": + total_steps += get_numb_of_blocks_to_fill_stripe(block_size, overlap, width) + case "V": + total_steps += get_numb_of_blocks_to_fill_stripe(block_size, overlap, height) + case _: + total_steps += ( + 2 + + get_numb_of_blocks_to_fill_stripe(block_size, overlap, height) + + get_numb_of_blocks_to_fill_stripe(block_size, overlap, width) + ) return setup_pbar(total_steps, 0, batch_len) @@ -124,7 +142,7 @@ def setup_pbar_seamless_v2(ori, batch_len): # 3 increments per big block + 2 for the "H & V" H Seam patch match ori: case "H & V": - total_steps = 2 + 3*2 + total_steps = 2 + 3 * 2 case _: total_steps = 3 total_steps = total_steps * batch_len @@ -154,6 +172,14 @@ def setup_pbar(total_steps, par_lvl, batch_len): return finished_event, t, shm_jobs.name, shm_jobs +def unwrap_to_grey(image, is_latent: bool = False) -> np.ndarray: + squeeze = len(image.shape) > 3 + image = image.cpu().numpy() + image = image.squeeze() if squeeze else image + image = np.moveaxis(image, 0, -1) if is_latent else image + return cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) + + def unwrap_and_quilt(wrapped_func, image, job_id, is_latent: bool = False): """quilting job when using batches""" squeeze = len(image.shape) > 3 @@ -181,9 +207,40 @@ def unwrap_and_quilt_seamless(wrapped_func, image, lookup, job_id): return result -def overlap_percentage_to_pixels(block_size, overlap): +def overlap_percentage_to_pixels(block_size: int, overlap: float): return int(block_size * overlap) if overlap > 0 else int( - block_size * QUILTING_SHARED_INPUT_TYPES["overlap"][1]["default"]) + block_size * get_quilting_shared_input_types()["overlap"][1]["default"]) + + +def get_block_sizes(src, block_size_input: int, block_size_upper_bound: int|None = None): + if block_size_input >= 3: + return [block_size_input] * src.shape[0] + + print(f"block size set to {block_size_input} less than 3!\nguessing nice block size...") + only_do_freq_analysis = block_size_input < 0 + sizes = [guess_nice_block_size(unwrap_to_grey(src[i]), only_do_freq_analysis, block_size_upper_bound) + for i in range(src.shape[0])] + print(f"guessed block sizes: {sizes}") + + return sizes + + +def block_size_upper_bound_for_seamless(ori, tex_h, tex_w, overlap_percentage): + """ + if using guess_block_size in seamless nodes, H seam + """ + match ori: + case "H & V": + # mind H seam w/ the following related assert -> image.shape[1] >= block_size + overlap * 2 + # -> H >= S + S * OP * 2 <-> H / (1 + 2 * OP ) >= S + return min( + # similar value to the one computed in guess nice block + round(min(tex_w, tex_h) / 1.2), + # space required to patch h seam plus some extra margin + round(tex_h/(1+2*overlap_percentage)/1.1) + ) + case _______: + return None # texture default dims are fine as bounds # endregion AUX FUNCTIONS & CLASSES @@ -198,13 +255,16 @@ class ImageQuilting: if self.version == 2: image = cv2.cvtColor(image, cv2.COLOR_RGB2Lab) + block_size = self.block_sizes[job_id] + overlap = overlap_percentage_to_pixels(block_size, self.overlap) + if self.parallelization_lvl == 0: result = generate_texture( - image, self.block_size, self.overlap, self.out_h, self.out_w, self.tolerance, + image, block_size, overlap, self.out_h, self.out_w, self.tolerance, self.version, self.rng, UiCoordData(self.jobs_shm_name, job_id)) else: result = generate_texture_parallel( - image, self.block_size, self.overlap, self.out_h, self.out_w, self.tolerance, + image, block_size, overlap, self.out_h, self.out_w, self.tolerance, self.version, self.parallelization_lvl, self.rng, UiCoordData(self.jobs_shm_name, job_id)) if self.version == 2: @@ -223,7 +283,7 @@ class ImageQuilting: # self explanatory "scale": ("FLOAT", {"default": 4, "min": 2, "max": 10, "step": .1}), - **QUILTING_SHARED_INPUT_TYPES + **get_quilting_shared_input_types() } } @@ -234,17 +294,17 @@ class ImageQuilting: def compute(self, src, block_size, scale, overlap, tolerance, parallelization_lvl, seed, version): h, w = src.shape[1:3] out_h, out_w = int(scale * h), int(scale * w) - overlap = overlap_percentage_to_pixels(block_size, overlap) + block_sizes = get_block_sizes(src, block_size) # note: the input src should have normalized values, not 0 to 255 rng: numpy.random.Generator = np.random.default_rng(seed=seed) finish_event, t, shm_name, shm_jobs = \ - setup_pbar_quilting(block_size, overlap, out_h, out_w, parallelization_lvl, src.shape[0]) + setup_pbar_quilting(block_sizes, overlap, out_h, out_w, parallelization_lvl) func = ImageQuilting.ImageQuiltingFuncWrapper( - block_size, overlap, out_h, out_w, tolerance, parallelization_lvl, rng, version, shm_name) + block_sizes, overlap, out_h, out_w, tolerance, parallelization_lvl, rng, version, shm_name) if src.shape[0] > 1: # if image batch texture_batch = self.batch_using_jobs(func, src) @@ -269,17 +329,25 @@ class LatentQuilting: """Wraps node functionality for easy re-use when using jobs.""" def __call__(self, latent_image, job_id): + block_size = self.block_sizes[job_id] + overlap = overlap_percentage_to_pixels(block_size, self.overlap) if self.parallelization_lvl == 0: return generate_texture( - latent_image, self.block_size, self.overlap, self.out_h, self.out_w, self.tolerance, + latent_image, block_size, overlap, self.out_h, self.out_w, self.tolerance, self.version, self.rng, UiCoordData(self.jobs_shm_name, job_id)) else: return generate_texture_parallel( - latent_image, self.block_size, self.overlap, self.out_h, self.out_w, self.tolerance, + latent_image, block_size, overlap, self.out_h, self.out_w, self.tolerance, self.version, self.parallelization_lvl, self.rng, UiCoordData(self.jobs_shm_name, job_id)) @classmethod def INPUT_TYPES(cls): + shared_inputs = get_quilting_shared_input_types() + # don't allow for auto block size + shared_inputs["block_size"][1]["default"] = 18 + shared_inputs["block_size"][1]["min"] = 3 + # note: could try running over individual channels; not sure if worth it thought. + return { "required": { "src": ("LATENT",), @@ -287,7 +355,7 @@ class LatentQuilting: # self explanatory "scale": ("FLOAT", {"default": 4, "min": 2, "max": 32, "step": .1}), - **QUILTING_SHARED_INPUT_TYPES + **shared_inputs } } @@ -299,15 +367,15 @@ class LatentQuilting: src = src["samples"] h, w = src.shape[2:4] out_h, out_w = int(scale * h), int(scale * w) - overlap = overlap_percentage_to_pixels(block_size, overlap) + block_sizes = [block_size] * src.shape[0] rng: numpy.random.Generator = np.random.default_rng(seed=seed) finish_event, t, shm_name, shm_jobs = \ - setup_pbar_quilting(block_size, overlap, out_h, out_w, parallelization_lvl, src.shape[0]) + setup_pbar_quilting(block_sizes, overlap, out_h, out_w, parallelization_lvl) func = LatentQuilting.LatentQuiltingFuncWrapper( - block_size, overlap, out_h, out_w, tolerance, parallelization_lvl, rng, version, shm_name) + block_sizes, overlap, out_h, out_w, tolerance, parallelization_lvl, rng, version, shm_name) if src.shape[0] > 1: # if multiple latent_batch = self.batch_using_jobs(func, src) @@ -335,7 +403,7 @@ class ImageMakeSeamlessMB: """Wraps node functionality for easy re-use when using jobs.""" ori: str - block_size: int + block_sizes: list[int] overlap: int tolerance: float rng: numpy.random.Generator @@ -345,6 +413,9 @@ class ImageMakeSeamlessMB: def __call__(self, image, lookup, job_id): from .make_seamless import make_seamless_horizontally, make_seamless_vertically, make_seamless_both + block_size = self.block_sizes[min(job_id, len(self.block_sizes) - 1)] + overlap = overlap_percentage_to_pixels(block_size, self.overlap) + if self.version == 2: image = cv2.cvtColor(image, cv2.COLOR_RGB2Lab) if lookup is not None: @@ -358,7 +429,7 @@ class ImageMakeSeamlessMB: case ___: func = make_seamless_both - result = func(image, self.block_size, self.overlap, self.tolerance, + result = func(image, block_size, overlap, self.tolerance, self.rng, self.version, lookup, UiCoordData(self.jobs_shm_name, job_id)) if self.version == 2: @@ -368,7 +439,7 @@ class ImageMakeSeamlessMB: @classmethod def INPUT_TYPES(cls): - inputs = QUILTING_SHARED_INPUT_TYPES.copy() + inputs = get_quilting_shared_input_types() inputs.pop("parallelization_lvl") inputs["version"][1]["min"] = 1 inputs["overlap"][1]["max"] = .5 @@ -391,15 +462,16 @@ class ImageMakeSeamlessMB: # note that src = lookup is the current algorithm policy when lookup is not provided. # this policy could change in the future, so do not apply it here too despite being idempotent. h, w = src.shape[1:3] - overlap = overlap_percentage_to_pixels(block_size, overlap) + blk_size_upper_bound = block_size_upper_bound_for_seamless(ori, h, w, overlap) + block_sizes = get_block_sizes(src, block_size, blk_size_upper_bound) rng: numpy.random.Generator = np.random.default_rng(seed=seed) lookup_batch_size = lookup.shape[0] if lookup is not None else 0 finish_event, t, shm_name, shm_jobs = setup_pbar_seamless( - ori, block_size, overlap, h, w, max(src.shape[0], lookup_batch_size)) + ori, block_sizes, overlap, h, w, max(src.shape[0], lookup_batch_size)) func = ImageMakeSeamlessMB.SeamlessFuncWrapper( - ori, block_size, overlap, tolerance, rng, version, shm_name) + ori, block_sizes, overlap, tolerance, rng, version, shm_name) if src.shape[0] > 1 or lookup_batch_size > 1: # if image batch texture_batch = self.batch_using_jobs(func, src, lookup) @@ -415,13 +487,14 @@ class ImageMakeSeamlessMB: def batch_using_jobs(wrapped_func, src, lookup): from joblib import Parallel, delayed # process in the same fashion as lists + number_of_processes = src.shape[0] if lookup is None else max(src.shape[0], lookup.shape[0]) results = Parallel(n_jobs=-1, backend="loky", timeout=None)( delayed(unwrap_and_quilt_seamless)( wrapped_func, src[min(i, src.shape[0] - 1)], lookup[min(i, lookup.shape[0] - 1)] if lookup is not None else None, - i) for i in range(max(src.shape[0], lookup.shape[0]))) + i) for i in range(number_of_processes)) return torch.stack(results) @@ -433,7 +506,7 @@ class ImageMakeSeamlessSB: """Wraps node functionality for easy re-use when using jobs.""" ori: str - block_size: int + block_sizes: list[int] overlap: int rng: numpy.random.Generator version: int @@ -442,6 +515,9 @@ class ImageMakeSeamlessSB: def __call__(self, image, lookup, job_id): from .make_seamless2 import seamless_horizontal, seamless_vertical, seamless_both + block_size = self.block_sizes[min(job_id, len(self.block_sizes) - 1)] + overlap = overlap_percentage_to_pixels(block_size, self.overlap) + if self.version == 2: image = cv2.cvtColor(image, cv2.COLOR_RGB2Lab) if lookup is not None: @@ -455,7 +531,7 @@ class ImageMakeSeamlessSB: case ___: func = seamless_both - result = func(image, self.block_size, self.overlap, + result = func(image, block_size, overlap, self.version, lookup, self.rng, UiCoordData(self.jobs_shm_name, job_id)) if self.version == 2: @@ -465,7 +541,7 @@ class ImageMakeSeamlessSB: @classmethod def INPUT_TYPES(cls): - inputs = QUILTING_SHARED_INPUT_TYPES.copy() + inputs = get_quilting_shared_input_types() inputs.pop("parallelization_lvl") inputs.pop("tolerance") inputs["version"][1]["min"] = 1 @@ -488,14 +564,16 @@ class ImageMakeSeamlessSB: def compute(self, src, ori, block_size, overlap, seed, version, lookup=None): # note that src = lookup is the current algorithm policy when lookup is not provided. # this policy could change in the future, so do not apply it here too despite being idempotent. - overlap = overlap_percentage_to_pixels(block_size, overlap) + h, w = src.shape[1:3] + blk_size_upper_bound = block_size_upper_bound_for_seamless(ori, h, w, overlap) + block_sizes = get_block_sizes(src, block_size, blk_size_upper_bound) rng: numpy.random.Generator = np.random.default_rng(seed=seed) lookup_batch_size = lookup.shape[0] if lookup is not None else 0 finish_event, t, shm_name, shm_jobs = setup_pbar_seamless_v2(ori, max(src.shape[0], lookup_batch_size)) func = ImageMakeSeamlessSB.SeamlessFuncWrapper( - ori, block_size, overlap, rng, version, shm_name) + ori, block_sizes, overlap, rng, version, shm_name) if src.shape[0] > 1 or lookup is not None and lookup.shape[0] > 1: # if image batch texture_batch = self.batch_using_jobs(func, src, lookup) @@ -511,13 +589,32 @@ class ImageMakeSeamlessSB: def batch_using_jobs(wrapped_func, src, lookup): from joblib import Parallel, delayed # process in the same fashion as lists + number_of_processes = src.shape[0] if lookup is None else max(src.shape[0], lookup.shape[0]) results = Parallel(n_jobs=-1, backend="loky", timeout=None)( delayed(unwrap_and_quilt_seamless)( wrapped_func, src[min(i, src.shape[0] - 1)], lookup[min(i, lookup.shape[0] - 1)] if lookup is not None else None, - i) for i in range(max(src.shape[0], lookup.shape[0]))) + i) for i in range(number_of_processes)) return torch.stack(results) + +class GuessNiceBlockSize: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "src": ("IMAGE",), + "simple_and_fast": ("BOOLEAN", {"default": False}) + } + } + + RETURN_TYPES = ("INT",) + FUNCTION = "compute" + CATEGORY = NODES_CATEGORY + + def compute(self, src, simple_and_fast): + return (guess_nice_block_size(unwrap_to_grey(src[0]), freq_analysis_only=simple_and_fast),) + # endregion NODES