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:
+2
-2
@@ -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
@@ -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__":
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user