Allow LLS Save Image to output mask-only
This commit is contained in:
@@ -191,7 +191,7 @@ node/
|
||||
|
||||
### `LLS Save Image`
|
||||
|
||||
`LLS Save Image` 现在除了保存或预览 `image`,也支持接收一个可选的 `mask` 输入。连上 `mask` 之后,节点会额外输出一份独立的黑白遮罩图,不会覆盖原图结果,也不会做彩色叠加。
|
||||
`LLS Save Image` 现在支持独立保存或预览 `image` 和 `mask`。`image`、`mask` 两个端口都不是必填,但至少要连接其中一个。连上哪个端口,就输出哪个结果;两个都连时,就同时输出两份结果。
|
||||
|
||||
**推荐工作流:**
|
||||
|
||||
@@ -202,8 +202,9 @@ node/
|
||||
|
||||
**行为说明:**
|
||||
|
||||
- `output_mode = save` 时:保存原图,同时额外保存一份 `<filename_prefix>_mask`
|
||||
- `output_mode = preview_only` 时:预览原图,同时额外预览一份黑白 mask
|
||||
- 只连接 `image`:只保存或预览原图
|
||||
- 只连接 `mask`:只保存或预览黑白 mask
|
||||
- 同时连接 `image + mask`:两者都输出,mask 在保存模式下使用 `<filename_prefix>_mask`
|
||||
- `image` 与 `mask` 各自独立输出,互不影响
|
||||
|
||||
### `LLS Simple Mask Draw`
|
||||
|
||||
+48
-37
@@ -356,12 +356,12 @@ class LLSSaveImage:
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"filename_prefix": ("STRING", {"default": "LLS"}),
|
||||
"output_mode": (_SAVE_OUTPUT_MODES, {"default": "save"}),
|
||||
"save_metadata": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
"optional": {
|
||||
"image": ("IMAGE",),
|
||||
"mask": ("MASK",),
|
||||
"prompt_info": ("STRING", {"forceInput": True}),
|
||||
"latent_info": ("STRING", {"forceInput": True}),
|
||||
@@ -376,6 +376,8 @@ class LLSSaveImage:
|
||||
}
|
||||
|
||||
def _merge_ui_results(self, primary: dict, secondary: dict | None = None) -> dict:
|
||||
if not primary:
|
||||
return secondary or {}
|
||||
if not secondary:
|
||||
return primary
|
||||
|
||||
@@ -454,7 +456,7 @@ class LLSSaveImage:
|
||||
|
||||
def save(
|
||||
self,
|
||||
image,
|
||||
image=None,
|
||||
filename_prefix: str = "LLS",
|
||||
output_mode: str = "save",
|
||||
save_metadata: bool = True,
|
||||
@@ -467,53 +469,62 @@ class LLSSaveImage:
|
||||
prompt=None,
|
||||
extra_pnginfo=None,
|
||||
):
|
||||
if image is None and mask is None:
|
||||
raise RuntimeError("[LLS] At least one of IMAGE or MASK must be connected for LLS Save Image.")
|
||||
|
||||
if output_mode == "preview_only":
|
||||
if comfy_core_nodes is None or not hasattr(comfy_core_nodes, "PreviewImage"):
|
||||
raise RuntimeError("[LLS] ComfyUI core PreviewImage node is not available.")
|
||||
previewer = comfy_core_nodes.PreviewImage()
|
||||
image_result = previewer.save_images(
|
||||
image,
|
||||
prompt=prompt,
|
||||
extra_pnginfo=None,
|
||||
)
|
||||
if mask is None:
|
||||
return image_result
|
||||
mask_result = previewer.save_images(
|
||||
mask_to_image(mask),
|
||||
prompt=prompt,
|
||||
extra_pnginfo=None,
|
||||
)
|
||||
image_result = None
|
||||
if image is not None:
|
||||
image_result = previewer.save_images(
|
||||
image,
|
||||
prompt=prompt,
|
||||
extra_pnginfo=None,
|
||||
)
|
||||
|
||||
mask_result = None
|
||||
if mask is not None:
|
||||
mask_result = previewer.save_images(
|
||||
mask_to_image(mask),
|
||||
prompt=prompt,
|
||||
extra_pnginfo=None,
|
||||
)
|
||||
return self._merge_ui_results(image_result, mask_result)
|
||||
|
||||
if comfy_core_nodes is None or not hasattr(comfy_core_nodes, "SaveImage"):
|
||||
raise RuntimeError("[LLS] ComfyUI core SaveImage node is not available.")
|
||||
|
||||
saver = comfy_core_nodes.SaveImage()
|
||||
merged_extra_pnginfo = dict(extra_pnginfo or {})
|
||||
if save_metadata:
|
||||
merged_extra_pnginfo["lls_metadata"] = self._build_metadata(
|
||||
image=image,
|
||||
prompt_info=prompt_info,
|
||||
latent_info=latent_info,
|
||||
sample_info=sample_info,
|
||||
decode_info=decode_info,
|
||||
upscale_info=upscale_info,
|
||||
image_result = None
|
||||
if image is not None:
|
||||
merged_extra_pnginfo = dict(extra_pnginfo or {})
|
||||
if save_metadata:
|
||||
merged_extra_pnginfo["lls_metadata"] = self._build_metadata(
|
||||
image=image,
|
||||
prompt_info=prompt_info,
|
||||
latent_info=latent_info,
|
||||
sample_info=sample_info,
|
||||
decode_info=decode_info,
|
||||
upscale_info=upscale_info,
|
||||
)
|
||||
image_result = saver.save_images(
|
||||
image,
|
||||
filename_prefix=filename_prefix,
|
||||
prompt=prompt,
|
||||
extra_pnginfo=merged_extra_pnginfo,
|
||||
)
|
||||
image_result = saver.save_images(
|
||||
image,
|
||||
filename_prefix=filename_prefix,
|
||||
prompt=prompt,
|
||||
extra_pnginfo=merged_extra_pnginfo,
|
||||
)
|
||||
if mask is None:
|
||||
return image_result
|
||||
|
||||
mask_result = saver.save_images(
|
||||
mask_to_image(mask),
|
||||
filename_prefix=f"{filename_prefix}_mask",
|
||||
prompt=prompt,
|
||||
extra_pnginfo={},
|
||||
)
|
||||
mask_result = None
|
||||
if mask is not None:
|
||||
mask_filename_prefix = filename_prefix if image is None else f"{filename_prefix}_mask"
|
||||
mask_result = saver.save_images(
|
||||
mask_to_image(mask),
|
||||
filename_prefix=mask_filename_prefix,
|
||||
prompt=prompt,
|
||||
extra_pnginfo={},
|
||||
)
|
||||
return self._merge_ui_results(image_result, mask_result)
|
||||
|
||||
|
||||
|
||||
@@ -977,13 +977,14 @@ class TestLoaderPromptRefactor(unittest.TestCase):
|
||||
required = node_cls.INPUT_TYPES()["required"]
|
||||
optional = node_cls.INPUT_TYPES()["optional"]
|
||||
|
||||
self.assertEqual(required["image"][0], "IMAGE")
|
||||
self.assertEqual(required["filename_prefix"][0], "STRING")
|
||||
self.assertEqual(required["output_mode"][0], ["save", "preview_only"])
|
||||
self.assertEqual(required["save_metadata"][0], "BOOLEAN")
|
||||
self.assertNotIn("image", required)
|
||||
self.assertNotIn("model", optional)
|
||||
self.assertNotIn("clip", optional)
|
||||
self.assertNotIn("vae", optional)
|
||||
self.assertEqual(optional["image"], ("IMAGE",))
|
||||
self.assertEqual(optional["mask"], ("MASK",))
|
||||
self.assertEqual(optional["prompt_info"], ("STRING", {"forceInput": True}))
|
||||
self.assertEqual(optional["latent_info"], ("STRING", {"forceInput": True}))
|
||||
@@ -1092,6 +1093,35 @@ class TestLoaderPromptRefactor(unittest.TestCase):
|
||||
self.assertEqual(len(result["ui"]["images"]), 2)
|
||||
self.assertIsNone(result["ui"]["images"][1]["extra_pnginfo"])
|
||||
|
||||
def test_save_image_preview_only_supports_mask_without_image(self):
|
||||
load_plugin_package()
|
||||
from lls_node_test_refactor.image import nodes as image_nodes
|
||||
|
||||
CoreNodesStub.save_calls = []
|
||||
CoreNodesStub.preview_calls = []
|
||||
|
||||
with mock.patch.object(image_nodes, "comfy_core_nodes", CoreNodesStub()):
|
||||
with mock.patch.object(
|
||||
image_nodes,
|
||||
"mask_to_image",
|
||||
side_effect=lambda mask: FakeTensor((mask.shape[0], mask.shape[1], mask.shape[2], 3)),
|
||||
create=True,
|
||||
):
|
||||
node = image_nodes.LLSSaveImage()
|
||||
result = node.save(
|
||||
image=None,
|
||||
mask=FakeTensor((1, 4, 4)),
|
||||
filename_prefix="LLS",
|
||||
save_metadata=True,
|
||||
output_mode="preview_only",
|
||||
)
|
||||
|
||||
self.assertEqual(CoreNodesStub.save_calls, [])
|
||||
self.assertEqual(len(CoreNodesStub.preview_calls), 1)
|
||||
self.assertEqual(tuple(CoreNodesStub.preview_calls[0]["images"].shape), (1, 4, 4, 3))
|
||||
self.assertEqual(len(result["ui"]["images"]), 1)
|
||||
self.assertEqual(result["ui"]["images"][0]["type"], "temp")
|
||||
|
||||
def test_save_image_save_mode_emits_separate_mask_file_without_lls_metadata(self):
|
||||
load_plugin_package()
|
||||
from lls_node_test_refactor.image import nodes as image_nodes
|
||||
@@ -1124,6 +1154,51 @@ class TestLoaderPromptRefactor(unittest.TestCase):
|
||||
self.assertEqual(tuple(CoreNodesStub.save_calls[1]["images"].shape), (1, 4, 4, 3))
|
||||
self.assertEqual(len(result["ui"]["images"]), 2)
|
||||
|
||||
def test_save_image_save_mode_supports_mask_without_image(self):
|
||||
load_plugin_package()
|
||||
from lls_node_test_refactor.image import nodes as image_nodes
|
||||
|
||||
CoreNodesStub.save_calls = []
|
||||
CoreNodesStub.preview_calls = []
|
||||
|
||||
with mock.patch.object(image_nodes, "comfy_core_nodes", CoreNodesStub()):
|
||||
with mock.patch.object(
|
||||
image_nodes,
|
||||
"mask_to_image",
|
||||
side_effect=lambda mask: FakeTensor((mask.shape[0], mask.shape[1], mask.shape[2], 3)),
|
||||
create=True,
|
||||
):
|
||||
node = image_nodes.LLSSaveImage()
|
||||
result = node.save(
|
||||
image=None,
|
||||
mask=FakeTensor((1, 4, 4)),
|
||||
filename_prefix="LLS",
|
||||
save_metadata=True,
|
||||
)
|
||||
|
||||
self.assertEqual(len(CoreNodesStub.preview_calls), 0)
|
||||
self.assertEqual(len(CoreNodesStub.save_calls), 1)
|
||||
self.assertEqual(CoreNodesStub.save_calls[0]["filename_prefix"], "LLS")
|
||||
self.assertEqual(CoreNodesStub.save_calls[0]["extra_pnginfo"], {})
|
||||
self.assertEqual(tuple(CoreNodesStub.save_calls[0]["images"].shape), (1, 4, 4, 3))
|
||||
self.assertEqual(len(result["ui"]["images"]), 1)
|
||||
|
||||
def test_save_image_rejects_missing_image_and_mask(self):
|
||||
load_plugin_package()
|
||||
from lls_node_test_refactor.image import nodes as image_nodes
|
||||
|
||||
with mock.patch.object(image_nodes, "comfy_core_nodes", CoreNodesStub()):
|
||||
node = image_nodes.LLSSaveImage()
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, "At least one of IMAGE or MASK"):
|
||||
node.save(
|
||||
image=None,
|
||||
mask=None,
|
||||
filename_prefix="LLS",
|
||||
save_metadata=True,
|
||||
output_mode="preview_only",
|
||||
)
|
||||
|
||||
def test_generation_config_uses_model_inference_without_context(self):
|
||||
plugin = load_plugin_package()
|
||||
node_cls = plugin.NODE_CLASS_MAPPINGS["LLSGenerationConfig"]
|
||||
|
||||
Reference in New Issue
Block a user