added node + added feature + fixes.

* new node: GuessNiceBlockSize.
* new feature: image quilting nodes get a special block size range that uses guess_nice_block_size; also works with parallel batch but the guess must be done before parallelization in order to setup the total number of steps in pbar.
* fixes: node options using shallow copy fixed; fixed parallelization error when lookup is None; fixed edge case in bse_desc_util.py.
* misc: guess_nice_block_size can be given an alternative upper bound (needed when patching H seam); guess_nice_block_size can run bse_ft only for faster analysis.
This commit is contained in:
Bruno Madeira
2024-08-05 00:01:07 +01:00
parent 01afd3e042
commit 28fa8fea58
5 changed files with 192 additions and 81 deletions
+2 -2
View File
@@ -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"
}
+18 -9
View File
@@ -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__":
+6 -1
View File
@@ -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)
+2 -2
View File
@@ -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
+164 -67
View File
@@ -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