Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0caf7f7177 | ||
|
|
7e5f7c3c0a | ||
|
|
9ccef62bc6 | ||
|
|
0b8bf65b57 | ||
|
|
e72ea99a6e | ||
|
|
7cfe734e54 | ||
|
|
fecff541e6 | ||
|
|
4d23ae490a | ||
|
|
415513c1a7 | ||
|
|
dbb20b3713 | ||
|
|
e105455bc7 | ||
|
|
fb99aa0a65 | ||
|
|
726cbdba51 |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -7,7 +7,7 @@ def get_e_measure(
|
||||
pred: torch.Tensor,
|
||||
gt: torch.Tensor,
|
||||
num_thresholds: int = F_POINTS,
|
||||
chunk_size: int = 16
|
||||
chunk_size: int = -1
|
||||
) -> Tuple[float, float, float, torch.Tensor, torch.Tensor]:
|
||||
"""
|
||||
Calculates the E-measure scores using a memory-efficient chunking strategy.
|
||||
@@ -38,6 +38,10 @@ def get_e_measure(
|
||||
thlist = torch.linspace(0, 1 - 1e-10, num_thresholds, device=pred.device)
|
||||
all_scores = []
|
||||
|
||||
if chunk_size == -1:
|
||||
chunk_size = round(64 / (pred.shape[0] * pred.shape[1] / (1 << 20)))
|
||||
chunk_size = min(max(1, chunk_size), num_thresholds)
|
||||
|
||||
# Process thresholds in memory-efficient chunks
|
||||
for i in range(0, num_thresholds, chunk_size):
|
||||
# Get the current chunk of thresholds
|
||||
|
||||
+29
-11
@@ -147,13 +147,16 @@ def get_image_preview_info(file_name, where="input"):
|
||||
return {"filename": fname, "subfolder": dname, "type": where}
|
||||
|
||||
|
||||
def load_one_image(file_name, disp_name, embed_transparency):
|
||||
def load_one_image(file_name, disp_name, embed_transparency, known=None):
|
||||
if file_name is None:
|
||||
return (empty_image(b=1, c=3), empty_image(b=1), file_name)
|
||||
if not os.path.isabs(file_name):
|
||||
file_name = os.path.join(get_input_directory(), file_name)
|
||||
if not os.path.exists(file_name):
|
||||
raise ValueError(f"File '{file_name}' not found")
|
||||
if known is not None and file_name in known:
|
||||
logger.debug('Recycling '+file_name)
|
||||
return (known[file_name][0], known[file_name][1], file_name)
|
||||
|
||||
try:
|
||||
if has_load_image:
|
||||
@@ -182,6 +185,8 @@ def load_one_image(file_name, disp_name, embed_transparency):
|
||||
# Concatenate image and mask into (b, h, w, 4)
|
||||
image_with_alpha = torch.cat([image, 1.0 - mask], dim=-1)
|
||||
result = (image_with_alpha, mask, file_name)
|
||||
if known is not None:
|
||||
known[file_name] = (result[0], result[1])
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
@@ -191,13 +196,16 @@ def load_one_image(file_name, disp_name, embed_transparency):
|
||||
"It may be corrupt or in an unsupported format.") from e
|
||||
|
||||
|
||||
def load_one_mask(file_name, disp_name, channel='red'):
|
||||
def load_one_mask(file_name, disp_name, channel='red', known=None):
|
||||
if file_name is None:
|
||||
return (empty_image(b=1), file_name)
|
||||
if not os.path.isabs(file_name):
|
||||
file_name = os.path.join(get_input_directory(), file_name)
|
||||
if not os.path.exists(file_name):
|
||||
raise ValueError(f"File '{file_name}' not found")
|
||||
if known is not None and file_name in known:
|
||||
logger.debug('Recycling '+file_name)
|
||||
return (known[file_name], file_name)
|
||||
|
||||
try:
|
||||
if has_load_image:
|
||||
@@ -210,6 +218,8 @@ def load_one_mask(file_name, disp_name, channel='red'):
|
||||
|
||||
# Call the method and return its result directly
|
||||
result = loader_instance.load_image(file_name, channel)
|
||||
if known is not None:
|
||||
known[file_name] = result[0]
|
||||
return (result[0], file_name)
|
||||
|
||||
except Exception as e:
|
||||
@@ -219,9 +229,12 @@ def load_one_mask(file_name, disp_name, channel='red'):
|
||||
"It may be corrupt or in an unsupported format.") from e
|
||||
|
||||
|
||||
def load_image_wrapper(file_name, embed_transparency, disp_name=None, show_preview=True):
|
||||
def load_image_wrapper(file_name, embed_transparency=False, disp_name=None, show_preview=True, channel=None):
|
||||
disp_name = disp_name or file_name
|
||||
result = load_one_image(file_name, disp_name, embed_transparency)
|
||||
if channel is not None:
|
||||
result = load_one_mask(file_name, disp_name, channel)
|
||||
else:
|
||||
result = load_one_image(file_name, disp_name, embed_transparency)
|
||||
if not show_preview:
|
||||
return result
|
||||
return {"ui": {"images": [get_image_preview_info(file_name)]}, "result": result}
|
||||
@@ -238,6 +251,8 @@ def load_images_wrapper(file_names, embed_transparency=False, disp_names=None, s
|
||||
|
||||
imgs = []
|
||||
masks = []
|
||||
known_imgs = {}
|
||||
known_masks = {}
|
||||
is_mask = channel is not None
|
||||
used_file_names = []
|
||||
all_preview_imgs = []
|
||||
@@ -258,9 +273,9 @@ def load_images_wrapper(file_names, embed_transparency=False, disp_names=None, s
|
||||
|
||||
if is_mask:
|
||||
# A mask
|
||||
mask, file_name = load_one_mask(file_name, disp_name, channel)
|
||||
mask, file_name = load_one_mask(file_name, disp_name, channel, known=known_masks)
|
||||
else:
|
||||
img, mask, file_name = load_one_image(file_name, disp_name, embed_transparency)
|
||||
img, mask, file_name = load_one_image(file_name, disp_name, embed_transparency, known=known_imgs)
|
||||
|
||||
max_w = max(max_w, img.shape[2])
|
||||
max_h = max(max_h, img.shape[1])
|
||||
@@ -276,7 +291,7 @@ def load_images_wrapper(file_names, embed_transparency=False, disp_names=None, s
|
||||
all_preview_imgs.append(get_image_preview_info(file_name))
|
||||
|
||||
for j in range(len(imgs_batch)):
|
||||
if channel is None:
|
||||
if not is_mask:
|
||||
img = imgs_batch[j]
|
||||
H, W = img.shape[1:3]
|
||||
if H != max_h or W != max_w:
|
||||
@@ -289,7 +304,7 @@ def load_images_wrapper(file_names, embed_transparency=False, disp_names=None, s
|
||||
logger.debug(f"Upscaling mask to fit batch: {W}x{H} -> {max_mw}x{max_mh}")
|
||||
masks_batch[j] = upscale_comfy(mask, max_mw, max_mh, "bicubic")
|
||||
|
||||
if channel is None:
|
||||
if not is_mask:
|
||||
imgs.append(torch.cat(imgs_batch))
|
||||
masks.append(torch.cat(masks_batch))
|
||||
else:
|
||||
@@ -298,9 +313,9 @@ def load_images_wrapper(file_names, embed_transparency=False, disp_names=None, s
|
||||
|
||||
if is_mask:
|
||||
# A mask
|
||||
mask, file_name = load_one_mask(file_name, disp_names[i], channel)
|
||||
mask, file_name = load_one_mask(file_name, disp_names[i], channel, known=known_masks)
|
||||
else:
|
||||
img, mask, file_name = load_one_image(file_name, disp_names[i], embed_transparency)
|
||||
img, mask, file_name = load_one_image(file_name, disp_names[i], embed_transparency, known=known_imgs)
|
||||
imgs.append(img)
|
||||
|
||||
masks.append(mask)
|
||||
@@ -310,7 +325,8 @@ def load_images_wrapper(file_names, embed_transparency=False, disp_names=None, s
|
||||
all_preview_imgs.append(get_image_preview_info(file_name))
|
||||
|
||||
logger.debug(f"Loaded {len(masks)} batches:")
|
||||
for n, i in enumerate(masks):
|
||||
items = masks if is_mask else imgs
|
||||
for n, i in enumerate(items):
|
||||
logger.debug(f"{n}) {i.shape}")
|
||||
|
||||
result = (imgs, masks, used_file_names) if channel is None else (masks, used_file_names)
|
||||
@@ -336,6 +352,8 @@ def save_image(images, filenames, prompt=None, extra_pnginfo=None, compress_leve
|
||||
|
||||
all_preview_imgs = []
|
||||
for index, (image, filename) in enumerate(zip(imgs, filenames)):
|
||||
if filename is None:
|
||||
continue
|
||||
i = 255. * image.cpu().numpy()
|
||||
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
||||
metadata = PngInfo()
|
||||
|
||||
+268
-44
@@ -19,10 +19,11 @@ import re
|
||||
from seconohe.apply_mask import apply_mask
|
||||
from seconohe.color import color_to_rgb_float, color_to_rgb_uint8
|
||||
from seconohe.comfy_misc import (get_input_directory, get_output_directory, MAX_RESOLUTION, PromptServer, IO, ComfyNodeABC,
|
||||
ExecutionBlocker, upscale_methods)
|
||||
ExecutionBlocker, upscale_methods, ProgressBar, get_annotated_filepath)
|
||||
from seconohe.downloader import download_file
|
||||
from seconohe.foreground_estimation.affce import affce
|
||||
from seconohe.foreground_estimation.fmlfe import fmlfe, IMPL_PRIORITY
|
||||
from seconohe.misc import format_bytes
|
||||
from seconohe.tensor import batched_min_max_norm
|
||||
from seconohe.torch import get_default_comfy_device, get_canonical_device
|
||||
import torch
|
||||
@@ -546,6 +547,12 @@ class ImageDataset(ComfyNodeABC):
|
||||
"default": True,
|
||||
"tooltip": "Block the execution when all images are processed"
|
||||
}),
|
||||
"invert": (IO.BOOLEAN, {
|
||||
"default": False,
|
||||
"tooltip": "Instead of listing the missing `destination` files\n"
|
||||
"we'll list the existing `destination` files.\n"
|
||||
"This can be used to collect generated information"
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,7 +572,7 @@ class ImageDataset(ComfyNodeABC):
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, source, pattern, destination, dest_ext, reference=None, sort_method="None",
|
||||
image_load_cap=1, skip_first_images=0, select_every_nth=1, random_seed=1,
|
||||
block_when_finished=False):
|
||||
block_when_finished=False, invert=False):
|
||||
# We always indicate an evaluation is needed.
|
||||
# 1) We must check the directory to know it, so we don't have any advantage on doing it now.
|
||||
# 2) ComfyUI makes any check impossible, if source is connected to a node it will be None
|
||||
@@ -573,7 +580,7 @@ class ImageDataset(ComfyNodeABC):
|
||||
|
||||
def execute(self, source, pattern, destination, dest_ext, reference=None, sort_method="None",
|
||||
image_load_cap=1, skip_first_images=0, select_every_nth=1, random_seed=1,
|
||||
block_when_finished=False):
|
||||
block_when_finished=False, invert=False):
|
||||
logger.debug(f"ImageDataset.source = {source}")
|
||||
logger.debug(f"ImageDataset.destination = {destination}")
|
||||
# Here self isn't really needed, our state is the filesystem
|
||||
@@ -643,7 +650,7 @@ class ImageDataset(ComfyNodeABC):
|
||||
dest_path = dest_dir / dest_filename
|
||||
|
||||
# Skip if the result file already exists
|
||||
if dest_path.exists():
|
||||
if dest_path.exists() ^ invert:
|
||||
continue
|
||||
|
||||
# Find the reference file (case-insensitive and extension-agnostic)
|
||||
@@ -794,8 +801,60 @@ class SaliencyEvaluationMetrics(ComfyNodeABC):
|
||||
UNIQUE_NAME = "SET_SaliencyEvaluationMetrics"
|
||||
DISPLAY_NAME = "Saliency Evaluation Metrics"
|
||||
|
||||
def read_single(self, path):
|
||||
if not path.is_file():
|
||||
return None
|
||||
with open(path, "rt") as f:
|
||||
f.readline()
|
||||
return float(f.readline())
|
||||
|
||||
def read_f(self, path):
|
||||
if not path.is_file():
|
||||
return None, None, None
|
||||
f4 = torch.Tensor(size=(4, F_POINTS), device="cpu")
|
||||
f1 = f4.movedim(1, 0) # Easier here, not for computation
|
||||
with open(path, "rt") as f:
|
||||
reader = csv.reader(f)
|
||||
next(reader) # header
|
||||
for c in range(F_POINTS):
|
||||
f1[c] = torch.Tensor(tuple(float(v) for v in next(reader)))
|
||||
next(reader)
|
||||
row = next(reader)
|
||||
assert row[0] == "Max", row
|
||||
f_max = float(row[1])
|
||||
row = next(reader)
|
||||
assert row[0] == "Adaptive", row
|
||||
f_adp = float(row[1])
|
||||
return f_max, f4, f_adp
|
||||
|
||||
def read_e(self, path):
|
||||
if not path.is_file():
|
||||
return None, None, None, None, None
|
||||
with open(path, "rt") as f:
|
||||
reader = csv.reader(f)
|
||||
next(reader) # header
|
||||
all_e = []
|
||||
thres = []
|
||||
for c in range(F_POINTS):
|
||||
row = next(reader)
|
||||
thres.append(float(row[0]))
|
||||
all_e.append(float(row[1]))
|
||||
thres = torch.Tensor(thres)
|
||||
all_e = torch.Tensor(all_e)
|
||||
next(reader)
|
||||
row = next(reader)
|
||||
assert row[0] == "Mean"
|
||||
e_mean = float(row[1])
|
||||
row = next(reader)
|
||||
assert row[0] == "Max"
|
||||
e_max = float(row[1])
|
||||
row = next(reader)
|
||||
assert row[0] == "Adaptive"
|
||||
e_adp = float(row[1])
|
||||
return e_mean, e_max, e_adp, all_e, thres
|
||||
|
||||
def evaluate(self, prediction: torch.Tensor, ground_truth: torch.Tensor, unique_id,
|
||||
img_name, normalize, result_save: bool = False, mae_enable: bool = True, mae_save: bool = False,
|
||||
img_name, normalize: bool = False, result_save: bool = False, mae_enable: bool = True, mae_save: bool = False,
|
||||
max_f_mes_enable: bool = True, max_f_mes_save: bool = False, s_mes_enable: bool = True,
|
||||
s_mes_save: bool = False, e_mes_enable: bool = True, e_mes_save: bool = False,
|
||||
wf_mes_enable: bool = True, wf_mes_save: bool = True):
|
||||
@@ -835,6 +894,7 @@ class SaliencyEvaluationMetrics(ComfyNodeABC):
|
||||
if names_len != imgs_len:
|
||||
raise ValueError(f"Got {imgs_len} images and {names_len} names, they must match")
|
||||
|
||||
prog_bar = ProgressBar(pred_len)
|
||||
for index_img in range(pred_len):
|
||||
# Ensure tensors are on the same device
|
||||
inputs_are_copies = get_canonical_device(prediction[index_img].device) != device
|
||||
@@ -866,75 +926,105 @@ class SaliencyEvaluationMetrics(ComfyNodeABC):
|
||||
|
||||
# 1. Mean Absolute Error (MAE)
|
||||
if mae_enable:
|
||||
mae = torch.mean(torch.abs(pred_i - gt_i)).item()
|
||||
logger.debug(f"MAE: {mae}")
|
||||
mae_path = Path(imgp.parent, imgp.stem+"_mae.csv")
|
||||
mae = self.read_single(mae_path)
|
||||
if mae is None:
|
||||
mae = torch.mean(torch.abs(pred_i - gt_i)).item()
|
||||
logger.debug(f"MAE: {mae}")
|
||||
if mae_save:
|
||||
with open(mae_path, "wt") as f:
|
||||
f.write(f"MAE\n{mae}")
|
||||
else:
|
||||
logger.debug(f"MAE already computed: {mae}")
|
||||
mae_total += mae
|
||||
res['mae'] = mae
|
||||
if mae_save:
|
||||
with open(Path(imgp.parent, imgp.stem+"_mae.csv"), "wt") as f:
|
||||
f.write(f"MAE\n{mae}")
|
||||
|
||||
# --- Metrics requiring binary ground truth ---
|
||||
if max_f_mes_enable or s_mes_enable or e_mes_enable or wf_mes_enable:
|
||||
gt_binary = (gt_i >= 0.5).float()
|
||||
gt_binary = None
|
||||
|
||||
# 2. Max F-measure
|
||||
if max_f_mes_enable:
|
||||
f_max, all_f, f_adp = get_f_measure(pred_i, gt_binary)
|
||||
f_path = Path(imgp.parent, imgp.stem+"_F.csv")
|
||||
f_max, all_f, f_adp = self.read_f(f_path)
|
||||
if f_max is None:
|
||||
gt_binary = (gt_i >= 0.5).float() if gt_binary is None else gt_binary
|
||||
f_max, all_f, f_adp = get_f_measure(pred_i, gt_binary)
|
||||
logger.debug(f"Fβmax: {f_max}")
|
||||
if max_f_mes_save:
|
||||
all_fp = all_f.movedim(1, 0)
|
||||
with open(f_path, "wt") as f:
|
||||
f.write("Threshold, F-measure, Precision, Recall\n")
|
||||
for fn in all_fp:
|
||||
f.write(f"{fn[0]}, {fn[1]}, {fn[2]}, {fn[3]}\n")
|
||||
f.write(f"\nMax, {f_max}\n")
|
||||
f.write(f"Adaptive, {f_adp}\n")
|
||||
else:
|
||||
logger.debug(f"Fβmax already computed: {f_max}")
|
||||
f_measure_max_total += f_max
|
||||
logger.debug(f"Fβmax: {f_max}")
|
||||
res['max_f_mes'] = f_max
|
||||
res['adp_f_mes'] = f_adp
|
||||
res['f'] = all_f[1]
|
||||
res['fp'] = all_f[2]
|
||||
res['fr'] = all_f[3]
|
||||
if max_f_mes_save:
|
||||
with open(Path(imgp.parent, imgp.stem+"_F.csv"), "wt") as f:
|
||||
f.write("Threshold, F-measure\n")
|
||||
for fn in all_f:
|
||||
f.write(f"{fn[0]}, {fn[1]}\n")
|
||||
f.write(f"\nMax, {f_max}\n")
|
||||
|
||||
# 3. S-measure
|
||||
if s_mes_enable:
|
||||
s_measure = get_s_measure(pred_i, gt_binary)
|
||||
s_path = Path(imgp.parent, imgp.stem+"_S.csv")
|
||||
s_measure = self.read_single(s_path)
|
||||
if s_measure is None:
|
||||
gt_binary = (gt_i >= 0.5).float() if gt_binary is None else gt_binary
|
||||
s_measure = get_s_measure(pred_i, gt_binary)
|
||||
logger.debug(f"Sα: {s_measure}")
|
||||
if s_mes_save:
|
||||
with open(s_path, "wt") as f:
|
||||
f.write(f"S-measure\n{s_measure}")
|
||||
else:
|
||||
logger.debug(f"Sα already computed: {s_measure}")
|
||||
s_measure_total += s_measure
|
||||
logger.debug(f"Sα: {s_measure}")
|
||||
res['s_mes'] = s_measure
|
||||
if s_mes_save:
|
||||
with open(Path(imgp.parent, imgp.stem+"_S.csv"), "wt") as f:
|
||||
f.write(f"S-measure\n{s_measure}")
|
||||
|
||||
# 4. E-measure
|
||||
if e_mes_enable:
|
||||
e_mean, e_max, e_adp, all_e, thres = get_e_measure(pred_i, gt_binary)
|
||||
e_path = Path(imgp.parent, imgp.stem+"_E.csv")
|
||||
e_mean, e_max, e_adp, all_e, thres = self.read_e(e_path)
|
||||
if e_mean is None:
|
||||
gt_binary = (gt_i >= 0.5).float() if gt_binary is None else gt_binary
|
||||
e_mean, e_max, e_adp, all_e, thres = get_e_measure(pred_i, gt_binary)
|
||||
logger.debug(f"Eϕ: {e_mean} {e_max} {e_adp}")
|
||||
if e_mes_save:
|
||||
with open(e_path, "wt") as f:
|
||||
f.write("Threshold, E-measure\n")
|
||||
for index, en in enumerate(all_e):
|
||||
f.write(f"{thres[index]}, {en}\n")
|
||||
f.write("\n")
|
||||
f.write(f"Mean, {e_mean}\n")
|
||||
f.write(f"Max, {e_max}\n")
|
||||
f.write(f"Adaptive, {e_adp}\n")
|
||||
else:
|
||||
logger.debug(f"Eϕ already computed: {e_mean} {e_max} {e_adp}")
|
||||
e_measure_total += e_mean
|
||||
e_measure_max_total += e_max
|
||||
e_measure_adp_total += e_adp
|
||||
logger.debug(f"Eϕ: {e_mean} {e_max} {e_adp}")
|
||||
res['e_mes'] = e_mean
|
||||
res['max_e_mes'] = e_max
|
||||
res['adp_e_mes'] = e_adp
|
||||
res['e'] = all_e
|
||||
if e_mes_save:
|
||||
with open(Path(imgp.parent, imgp.stem+"_E.csv"), "wt") as f:
|
||||
f.write("Threshold, E-measure\n")
|
||||
for index, en in enumerate(all_e):
|
||||
f.write(f"{thres[index]}, {en}\n")
|
||||
f.write("\n")
|
||||
f.write(f"Mean, {e_mean}\n")
|
||||
f.write(f"Max, {e_max}\n")
|
||||
f.write(f"Adaptive, {e_adp}\n")
|
||||
|
||||
# 5. Weighted F-measure
|
||||
if wf_mes_enable:
|
||||
wf = get_weighted_f_measure(pred_i, gt_binary)
|
||||
wf_path = Path(imgp.parent, imgp.stem+"_wF.csv")
|
||||
wf = self.read_single(wf_path)
|
||||
if wf is None:
|
||||
gt_binary = (gt_i >= 0.5).float() if gt_binary is None else gt_binary
|
||||
wf = get_weighted_f_measure(pred_i, gt_binary)
|
||||
logger.debug(f"Fβw: {wf}")
|
||||
if wf_mes_save:
|
||||
with open(wf_path, "wt") as f:
|
||||
f.write(f"Weighted F-measure\n{wf}")
|
||||
else:
|
||||
logger.debug(f"Fβw already computed: {wf}")
|
||||
weighted_f_total += wf
|
||||
logger.debug(f"Fβw: {wf}")
|
||||
res['wf_mes'] = wf
|
||||
if wf_mes_save:
|
||||
with open(Path(imgp.parent, imgp.stem+"_wF.csv"), "wt") as f:
|
||||
f.write(f"Weighted F-measure\n{wf}")
|
||||
|
||||
if result_save and res:
|
||||
with open(Path(imgp.parent, imgp.stem+".csv"), "wt") as f:
|
||||
@@ -943,6 +1033,7 @@ class SaliencyEvaluationMetrics(ComfyNodeABC):
|
||||
f.write(','.join(expand_data_row(res, keys))+"\n")
|
||||
|
||||
all.append(res)
|
||||
prog_bar.update(1)
|
||||
|
||||
# Average metrics over the batch/es
|
||||
mae_avg = mae_total / gt_len
|
||||
@@ -1115,8 +1206,8 @@ class ConsolidateMetrics(ComfyNodeABC):
|
||||
raise ValueError(f"Got {n_metrics} metrics and {n_destination} destinations. They must be multiples.")
|
||||
if n_res_destination > 1:
|
||||
raise ValueError(f"Only one resume destination currently supported, got {n_res_destination}")
|
||||
if n_res_destination and n_metric_ids != n_metrics:
|
||||
raise ValueError(f"Got {n_metrics} metrics and {n_metric_ids} metric IDs. They must match.")
|
||||
if n_res_destination and n_metric_ids != n_destination:
|
||||
raise ValueError(f"Got {n_destination} destinations and {n_metric_ids} metric IDs. They must match.")
|
||||
slice_size = n_metrics // n_destination
|
||||
|
||||
res = []
|
||||
@@ -1126,7 +1217,7 @@ class ConsolidateMetrics(ComfyNodeABC):
|
||||
end = start + slice_size
|
||||
data, total = self.consolidate(metrics[start:end], img_name[start:end], d)
|
||||
res.append(data)
|
||||
if n_res_destination:
|
||||
if n_res_destination and total:
|
||||
totals[metric_ids[c]] = total
|
||||
|
||||
if totals:
|
||||
@@ -2521,6 +2612,29 @@ class ImagePad(ComfyNodeABC):
|
||||
return (out_image, out_masks)
|
||||
|
||||
|
||||
class ChooseUpscaleMethod(ComfyNodeABC):
|
||||
"""
|
||||
Used to send the same upscale option to other nodes.
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"upscale_method": UPSCALE_OPT,
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = (IO.ANY,)
|
||||
RETURN_NAMES = ("upscale_method",)
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = BASE_CATEGORY + "/" + MANIPULATION_CATEGORY
|
||||
UNIQUE_NAME = "SET_ChooseUpscaleMethod"
|
||||
DISPLAY_NAME = "Choose Upscale Method"
|
||||
|
||||
def execute(self, upscale_method):
|
||||
return (upscale_method, )
|
||||
|
||||
|
||||
# Adapted from KJNodes, credits to Kijai (ImageResizeKJv2)
|
||||
# Differences:
|
||||
# - The color is an string that support various formats
|
||||
@@ -3027,6 +3141,9 @@ class ImageWithTextLabel(ComfyNodeABC):
|
||||
|
||||
|
||||
class CartesianProduct(ComfyNodeABC):
|
||||
"""
|
||||
Produces a list with all the combinations from the items in list_A and list_B.
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
@@ -3071,3 +3188,110 @@ class CartesianProduct(ComfyNodeABC):
|
||||
res_B.append(b)
|
||||
|
||||
return (res_A, res_B)
|
||||
|
||||
|
||||
class ImageMemoryEstimator:
|
||||
"""
|
||||
Estimates the ammount of RAM needed to load the indicated images.
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"file_name": (IO.STRING, {
|
||||
"forceInput": True,
|
||||
"tooltip": "One or more image names. ComfyUI input dir is the base"}),
|
||||
"is_image": (IO.BOOLEAN, {
|
||||
"default": True,
|
||||
"tooltip": "Compute three channels. Enable it and `is_mask` for RGBA"}),
|
||||
"is_mask": (IO.BOOLEAN, {
|
||||
"default": False,
|
||||
"tooltip": "Compute one channel. Can be combined with `is_image`"}),
|
||||
},
|
||||
"hidden": {
|
||||
"unique_id": "UNIQUE_ID",
|
||||
},
|
||||
}
|
||||
|
||||
INPUT_IS_LIST = True
|
||||
RETURN_TYPES = ("INT", "STRING", "STRING")
|
||||
RETURN_NAMES = ("total_bytes", "human_readable", "file_name")
|
||||
OUTPUT_IS_LIST = (False, False, True)
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = BASE_CATEGORY + "/" + VALIDATION
|
||||
UNIQUE_NAME = "SET_ImageMemoryEstimator"
|
||||
DISPLAY_NAME = "Image Memory Estimator"
|
||||
|
||||
def execute(self, file_name, is_image, is_mask, unique_id):
|
||||
total_pixels = 0
|
||||
min_area = float('inf')
|
||||
max_area = 0
|
||||
min_dim = (0, 0)
|
||||
max_dim = (0, 0)
|
||||
valid_count = 0
|
||||
|
||||
# Bytes per pixel for ComfyUI Tensors (Float32 = 4 bytes, RGB = 3 channels)
|
||||
channels = 3 if is_image[0] else 0
|
||||
channels += 1 if is_mask[0] else 0
|
||||
if channels == 0:
|
||||
logger.warning("Enable `is_image` and/or `is_mask`")
|
||||
BYTES_PER_PIXEL = 4 * channels
|
||||
logger.debug(f"BYTES_PER_PIXEL {BYTES_PER_PIXEL}")
|
||||
|
||||
known = set()
|
||||
for fname in file_name:
|
||||
if fname is None:
|
||||
continue
|
||||
# Try to resolve path (handles both absolute paths and ComfyUI input folder files)
|
||||
file_path = get_annotated_filepath(fname)
|
||||
|
||||
if not file_path or not os.path.exists(file_path):
|
||||
logger.warning(f"Could not find file: {fname}")
|
||||
continue
|
||||
|
||||
if file_path in known:
|
||||
continue
|
||||
|
||||
try:
|
||||
# PIL.Image.open is lazy; it reads metadata without loading pixel data
|
||||
with Image.open(file_path) as img:
|
||||
w, h = img.size
|
||||
area = w * h
|
||||
total_pixels += area
|
||||
|
||||
if area < min_area:
|
||||
min_area = area
|
||||
min_dim = (w, h)
|
||||
|
||||
if area > max_area:
|
||||
max_area = area
|
||||
max_dim = (w, h)
|
||||
|
||||
valid_count += 1
|
||||
known.add(file_path)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to read metadata for {fname}: {e}")
|
||||
|
||||
total_bytes = total_pixels * BYTES_PER_PIXEL
|
||||
human_readable = format_bytes(total_bytes)
|
||||
|
||||
# --- Information ---
|
||||
logger.info("--- Image Memory Estimation ---")
|
||||
msg = f"Number of images: {valid_count}"
|
||||
logger.info(msg)
|
||||
if valid_count > 0:
|
||||
m = f"Minimum Dimensions: {min_dim[0]}x{min_dim[1]}"
|
||||
logger.info(m)
|
||||
msg += "<br>" + m
|
||||
m = f"Maximum Dimensions: {max_dim[0]}x{max_dim[1]}"
|
||||
logger.info(m)
|
||||
msg += "<br>" + m
|
||||
m = f"Total Raw Tensor Size: {human_readable} ({total_bytes} bytes)"
|
||||
logger.info(m)
|
||||
msg += "<br>" + m
|
||||
else:
|
||||
logger.info("No valid images processed.")
|
||||
logger.info("-------------------------------")
|
||||
send_progress_text(unique_id[0], msg)
|
||||
|
||||
return (total_bytes, human_readable, file_name)
|
||||
|
||||
Reference in New Issue
Block a user