cleaning ...

* patch_search.py renamed to synthesis_subroutines.py.
* fixed number of channels when using cv.floodFill with latents in new min cut implementation.
* make_seamless.py and make_seamless2.py use same "min_cut_patch" implementation.
* quilting.py allows to use v0 implementation, but otherwise, also shares the same as seamless nodes.
* check optional lib using importlib
This commit is contained in:
Bruno Madeira
2024-08-19 15:56:04 +01:00
parent 217b71b426
commit 657dd120e1
5 changed files with 115 additions and 61 deletions
+9
View File
@@ -1,3 +1,4 @@
import importlib.util
from .nodes import *
NODE_CLASS_MAPPINGS = {
@@ -19,3 +20,11 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"LatentQuiltingSeamlessSB_Bmad": "Latent Seamless Quilting SP",
"GuessQuiltingBlockSize_Bmad": "Guess Quilting Block Size",
}
optional_package_name = 'pyastar2d'
if importlib.util.find_spec(optional_package_name) is None:
print(f"{optional_package_name} is not installed;"
f" jena2020 implementation will be used instead for min cut.")
else:
print(f"{optional_package_name} is installed;"
f" it will be used for computing the min cut unless specified otherwise.")
+1 -1
View File
@@ -1,4 +1,4 @@
from .patch_search import get_generic_find_patch_method, get_4way_min_cut_patch
from .synthesis_subroutines import get_generic_find_patch_method, get_4way_min_cut_patch
from .types import UiCoordData
from math import ceil
import numpy as np
+3 -4
View File
@@ -1,6 +1,5 @@
# An alternative approach to making the texture seamless
from .patch_search import compute_errors, get_match_template_method
from .jena2020.generate import getMinCutPatchHorizontal
from .synthesis_subroutines import compute_errors, get_match_template_method, get_min_cut_patch_horizontal
from .make_seamless import patch_horizontal_seam
from .types import UiCoordData
import numpy as np
@@ -40,9 +39,9 @@ def seamless_horizontal(image, block_size, overlap, version, lookup_texture, rng
fake_block_sized_patch = np.empty((image.shape[0], image.shape[0], image.shape[2]), dtype=image.dtype)
fake_block_sized_patch[:, :overlap] = lookup_texture[y:y + image.shape[0], x:x + overlap]
fake_block_sized_patch[:, -overlap:] = lookup_texture[y:y + image.shape[0], x + block_size - overlap:x + block_size]
left_side_patch = getMinCutPatchHorizontal(fake_left_block, fake_block_sized_patch, image.shape[0], overlap)
left_side_patch = get_min_cut_patch_horizontal(fake_left_block, fake_block_sized_patch, image.shape[0], overlap)
right_side_patch = np.fliplr(
getMinCutPatchHorizontal(
get_min_cut_patch_horizontal(
np.fliplr(fake_right_block),
np.fliplr(fake_block_sized_patch),
image.shape[0], overlap
+12 -6
View File
@@ -1,5 +1,6 @@
from .patch_search import get_find_patch_to_the_right_method, get_find_patch_below_method, get_find_patch_both_method
from .jena2020.generate import getMinCutPatchHorizontal, getMinCutPatchVertical, getMinCutPatchBoth
from .synthesis_subroutines import (
get_find_patch_to_the_right_method, get_find_patch_below_method, get_find_patch_both_method,
get_min_cut_patch_horizontal_method, get_min_cut_patch_vertical_method, get_min_cut_patch_both_method)
from multiprocessing.shared_memory import SharedMemory
from dataclasses import dataclass
from .types import UiCoordData
@@ -50,6 +51,7 @@ import numpy as np
def fill_column(image, initial_block, overlap, rows: int, tolerance, version, rng: np.random.Generator):
find_patch_below = get_find_patch_below_method(version)
get_min_cut_patch = get_min_cut_patch_vertical_method(version)
block_size = initial_block.shape[0]
texture_map = np.zeros(
((block_size + rows * (block_size - overlap)), block_size, image.shape[2])).astype(image.dtype)
@@ -57,13 +59,14 @@ def fill_column(image, initial_block, overlap, rows: int, tolerance, version, rn
for i, blk_idx in enumerate(range((block_size - overlap), texture_map.shape[0] - overlap, (block_size - overlap))):
ref_block = texture_map[(blk_idx - block_size + overlap):(blk_idx + overlap), :block_size]
patch_block = find_patch_below(ref_block, image, block_size, overlap, tolerance, rng)
min_cut_patch = getMinCutPatchVertical(ref_block, patch_block, block_size, overlap)
min_cut_patch = get_min_cut_patch(ref_block, patch_block, block_size, overlap)
texture_map[blk_idx:(blk_idx + block_size), :block_size] = min_cut_patch
return texture_map
def fill_row(image, initial_block, overlap, columns: int, tolerance, version: int, rng: np.random.Generator):
find_patch_to_the_right = get_find_patch_to_the_right_method(version)
get_min_cut_patch = get_min_cut_patch_horizontal_method(version)
block_size = initial_block.shape[0]
texture_map = np.zeros(
(block_size, (block_size + columns * (block_size - overlap)), image.shape[2])).astype(image.dtype)
@@ -71,7 +74,7 @@ def fill_row(image, initial_block, overlap, columns: int, tolerance, version: in
for i, blk_idx in enumerate(range((block_size - overlap), texture_map.shape[1] - overlap, (block_size - overlap))):
ref_block = texture_map[:block_size, (blk_idx - block_size + overlap):(blk_idx + overlap)]
patch_block = find_patch_to_the_right(ref_block, image, block_size, overlap, tolerance, rng)
min_cut_patch = getMinCutPatchHorizontal(ref_block, patch_block, block_size, overlap)
min_cut_patch = get_min_cut_patch(ref_block, patch_block, block_size, overlap)
texture_map[:block_size, blk_idx:(blk_idx + block_size)] = min_cut_patch
return texture_map
@@ -79,6 +82,7 @@ def fill_row(image, initial_block, overlap, columns: int, tolerance, version: in
def fill_quad(rows: int, columns: int, block_size, overlap, texture_map, image, tolerance, version,
rng: np.random.Generator, uicd: UiCoordData | None):
find_patch_both = get_find_patch_both_method(version)
get_min_cut_patch = get_min_cut_patch_both_method(version)
for i in range(1, rows + 1):
for j in range(1, columns + 1):
@@ -92,7 +96,7 @@ def fill_quad(rows: int, columns: int, block_size, overlap, texture_map, image,
blk_index_j:(blk_index_j + block_size)]
patch_block = find_patch_both(ref_block_left, ref_block_top, image, block_size, overlap, tolerance, rng)
min_cut_patch = getMinCutPatchBoth(ref_block_left, ref_block_top, patch_block, block_size, overlap)
min_cut_patch = get_min_cut_patch(ref_block_left, ref_block_top, patch_block, block_size, overlap)
texture_map[blk_index_i:(blk_index_i + block_size), blk_index_j:(blk_index_j + block_size)] = min_cut_patch
@@ -360,6 +364,8 @@ class ParaRowsJobInfo:
def fill_rows_ps(pid: int, job: ParaRowsJobInfo, jobs_events: list, uicd: UiCoordData | None):
find_patch_both = get_find_patch_both_method(job.version)
get_min_cut_patch = get_min_cut_patch_both_method(job.version)
# unwrap data
block_size, overlap, tolerance, rng = job.block_size, job.overlap, job.tolerance, job.rng
total_procs, image, rows, columns = job.total_procs, job.src, job.rows, job.columns
@@ -396,7 +402,7 @@ def fill_rows_ps(pid: int, job: ParaRowsJobInfo, jobs_events: list, uicd: UiCoor
blk_index_j:(blk_index_j + block_size)]
patch_block = find_patch_both(ref_block_left, ref_block_top, image, block_size, overlap, tolerance, rng)
min_cut_patch = getMinCutPatchBoth(ref_block_left, ref_block_top, patch_block, block_size, overlap)
min_cut_patch = get_min_cut_patch(ref_block_left, ref_block_top, patch_block, block_size, overlap)
texture[blk_index_i:(blk_index_i + block_size), blk_index_j:(blk_index_j + block_size)] = min_cut_patch
+90 -50
View File
@@ -1,12 +1,11 @@
from functools import lru_cache
import numpy as np
import importlib.util
import cv2 as cv
from .jena2020.generate import findPatchVertical, findPatchHorizontal, findPatchBoth
from .jena2020.generate import *
from .misc.bse_type_aliases import num_pixels
epsilon = np.finfo(float).eps
inf = float('inf')
# region get methods by version
@@ -48,6 +47,18 @@ def get_find_patch_both_method(version: int):
return vx_both
def get_min_cut_patch_horizontal_method(version: int):
return get_min_cut_patch_horizontal if version > 0 else getMinCutPatchHorizontal
def get_min_cut_patch_vertical_method(version: int):
return get_min_cut_patch_vertical if version > 0 else getMinCutPatchVertical
def get_min_cut_patch_both_method(version: int):
return get_min_cut_patch_both if version > 0 else getMinCutPatchBoth
def get_generic_find_patch_method(version: int):
def vx_patch_find(ref_block_left, ref_block_right, ref_block_top, ref_block_bottom,
texture, block_size, overlap, tolerance, rng):
@@ -84,6 +95,8 @@ def get_match_template_method(version: int) -> int:
# endregion
# region custom implementation of: patch search & min cut + auxiliary methods
def find_patch_vx(ref_block_left, ref_block_right, ref_block_top, ref_block_bottom,
texture, block_size, overlap, tolerance,
rng: np.random.Generator, version):
@@ -118,12 +131,11 @@ def find_patch_vx(ref_block_left, ref_block_right, ref_block_top, ref_block_bott
return texture[y:y + block_size, x:x + block_size]
# min path cut 4 way should go to here?
# TODO because it is not a class func the cache must be cleared when a node finishes running!
@lru_cache(maxsize=4)
def patch_blending_vignette(block_size: num_pixels, overlap: num_pixels, left: bool, right: bool, top: bool, bottom: bool):
margin = 1 #ceil(overlap / 12) # must be small
def patch_blending_vignette(block_size: num_pixels, overlap: num_pixels,
left: bool, right: bool, top: bool, bottom: bool) -> np.ndarray:
margin = 1 # must be small !
power = 2.5 # controls drop-off
p = 6 # controls the shape
@@ -186,7 +198,8 @@ def patch_blending_vignette(block_size: num_pixels, overlap: num_pixels, left: b
return mask
def blur_patch_mask(src_mask, block_size: num_pixels, overlap: num_pixels, left: bool, right: bool, top: bool, bottom: bool):
def blur_patch_mask(src_mask, block_size: num_pixels, overlap: num_pixels, left: bool, right: bool, top: bool,
bottom: bool):
#print(f"src_mask type > {src_mask.dtype}")
return src_mask # don't use it for now until further testing
@@ -202,7 +215,7 @@ def blur_patch_mask(src_mask, block_size: num_pixels, overlap: num_pixels, left:
result = (vignette * blurred) + ((1 - vignette) * src_mask[:, :, 0])
print(f"mask min max = {(np.min(result), np.max(result))}")
result = np.clip(result, 0, 1) # better safe than sorry
result = np.stack((result, ) * src_mask.shape[2], axis=-1)
result = np.stack((result,) * src_mask.shape[2], axis=-1)
return result
@@ -245,6 +258,48 @@ def get_min_cut_patch_mask_horizontal_jena2020(block1, block2, block_size: num_p
return mask
if importlib.util.find_spec("pyastar2d") is not None:
import pyastar2d
def get_min_cut_patch_mask_horizontal_astar(block1, block2, block_size: num_pixels, overlap: num_pixels):
"""
@param block1: block to the left, with the overlap on its right edge
@param block2: block to the right, with the overlap on its left edge
@return: ONLY the mask (not the patched overlap section)
"""
err = ((block1[:, -overlap:] - block2[:, :overlap]) ** 2).mean(2)
err *= block_size ** 3
err += 1
err *= block_size ** 3 # make the lowest value big enough for 1 to be negligible
err = np.pad(err, ((1, 1), (0, 0)), 'constant', constant_values=(1, 1))
start = (0, err.shape[1] // 2)
end = (err.shape[0] - 1, err.shape[1] // 2)
path = pyastar2d.astar_path(err, start, end, allow_diagonal=True)
mask = np.ones((block_size, block_size), dtype=block1.dtype)
shape_m2 = err.shape[0] - 2
start_index = 0 # find start index to avoid checking 0 < i every iteration
for idx, (i, j) in enumerate(path):
if 0 < i:
start_index = idx
break
for i, j in path[start_index:]: # draw path
mask[i - 1, j + 1] = 0
if i >= shape_m2:
break
cv.floodFill(mask, None, (mask.shape[0] - 1, mask.shape[1] - 1), (0,))
mask = np.stack((mask,) * block1.shape[2], axis=-1)
return mask
get_min_cut_patch_mask_horizontal = get_min_cut_patch_mask_horizontal_astar
else:
get_min_cut_patch_mask_horizontal = get_min_cut_patch_mask_horizontal_jena2020
def get_4way_min_cut_patch(ref_block_left, ref_block_right, ref_block_top, ref_block_bottom,
patch_block, block_size, overlap):
# (optional step) blur masks for a more seamless integration ( sometimes makes transition more noticeable, depends )
@@ -269,7 +324,8 @@ def get_4way_min_cut_patch(ref_block_left, ref_block_right, ref_block_top, ref_b
if has_top:
# V , > counterclockwise rotation
mask_top = get_min_cut_patch_mask_horizontal(np.rot90(ref_block_top), np.rot90(patch_block), block_size, overlap)
mask_top = get_min_cut_patch_mask_horizontal(np.rot90(ref_block_top), np.rot90(patch_block), block_size,
overlap)
mask_top = np.rot90(mask_top, 3)
mask_top = blur_patch_mask(mask_top, block_size, overlap, has_left, has_right, has_top, has_bottom)
masks_list.append(mask_top)
@@ -307,45 +363,29 @@ def get_4way_min_cut_patch(ref_block_left, ref_block_right, ref_block_top, ref_b
return res_block
try:
import pyastar2d
def get_min_cut_patch_mask_horizontal_astar(block1, block2, block_size: num_pixels, overlap: num_pixels):
"""
@param block1: block to the left, with the overlap on its right edge
@param block2: block to the right, with the overlap on its left edge
@return: ONLY the mask (not the patched overlap section)
"""
err = ((block1[:, -overlap:] - block2[:, :overlap]) ** 2).mean(2)
err *= block_size ** 3
err += 1
err *= block_size ** 3 # make the lowest value big enough for 1 to be negligible
err = np.pad(err, ((1, 1), (0, 0)), 'constant', constant_values=(1, 1))
start = (0, err.shape[1] // 2)
end = (err.shape[0] - 1, err.shape[1] // 2)
path = pyastar2d.astar_path(err, start, end, allow_diagonal=True)
mask = np.ones((block_size, block_size, block1.shape[2]), dtype=block1.dtype)
shape_m2 = err.shape[0] - 2
start_index = 0 # find start index to avoid checking 0 < i every iteration
for idx, (i, j) in enumerate(path):
if 0 < i:
start_index = idx
break
for i, j in path[start_index:]: # draw path
mask[i - 1, j + 1, :] = 0
if i >= shape_m2:
break
cv.floodFill(mask, None, (mask.shape[0]-1, mask.shape[1]-1), (0, ) * block1.shape[2])
return mask
# endregion
get_min_cut_patch_mask_horizontal = get_min_cut_patch_mask_horizontal_astar
print("comfyui_quilting: pyastar2d will be used to compute minimum cut.")
except Exception:
get_min_cut_patch_mask_horizontal = get_min_cut_patch_mask_horizontal_jena2020
print("comfyui_quilting: jena2020 based solution will be used to compute minimum cut.")
# region min cut patch aliases
def get_min_cut_patch_horizontal(left_block, patch_block, block_size, overlap):
return get_4way_min_cut_patch(
left_block, None, None, None,
patch_block, block_size, overlap
)
def get_min_cut_patch_vertical(top_block, patch_block, block_size, overlap):
return get_4way_min_cut_patch(
None, None, top_block, None,
patch_block, block_size, overlap
)
def get_min_cut_patch_both(left_block, top_block, patch_block, block_size, overlap):
return get_4way_min_cut_patch(
left_block, None, top_block, None,
patch_block, block_size, overlap
)
# endregion