refactor: share backend image serialization helpers

This commit is contained in:
Dariusz L
2026-08-13 05:39:24 +02:00
parent f60b7c0d8d
commit 212d6b7728
5 changed files with 170 additions and 55 deletions
+7 -1
View File
@@ -29,4 +29,10 @@ def file_to_data_url(path: str, *, mime_type: str = "image/png") -> str:
return _bytes_to_data_url(image_file.read(), mime_type)
__all__ = ["file_to_data_url", "pil_to_data_url"]
def data_url_to_pil(data_url: str) -> Image.Image:
"""Decode an image data URL into a lazily opened PIL image."""
image_bytes = base64.b64decode(data_url.split(",")[1])
return Image.open(io.BytesIO(image_bytes))
__all__ = ["data_url_to_pil", "file_to_data_url", "pil_to_data_url"]
+2 -7
View File
@@ -1,22 +1,17 @@
"""Image and tensor conversion helpers shared by LayerForge endpoints."""
import base64
import io
import numpy as np
from PIL import Image
from .image_serialization import pil_to_data_url
from torchvision import transforms
from .image_serialization import data_url_to_pil, pil_to_data_url
from .node import log
def convert_base64_to_tensor(base64_str):
"""Convert a data-URL image into a BCHW tensor and optional alpha tensor."""
try:
img_data = base64.b64decode(base64_str.split(",")[1])
img = Image.open(io.BytesIO(img_data))
img = data_url_to_pil(base64_str)
has_alpha = img.mode == "RGBA"
alpha = None
+42 -46
View File
@@ -1,7 +1,5 @@
"""Core LayerForge ComfyUI node and its in-memory canvas state."""
import base64
import io
import os
import threading
import time
@@ -15,7 +13,9 @@ import torch
import torch.nn.functional as F
from PIL import Image
from .image_serialization import pil_to_data_url
from .image_serialization import data_url_to_pil, pil_to_data_url
_OUTPUT_IMAGE_EXTENSIONS = (".png", ".jpg", ".jpeg", ".bmp", ".gif")
try:
from .log_system import create_module_logger
@@ -172,6 +172,16 @@ class LayerForgeNode:
FUNCTION = "process_canvas_image"
CATEGORY = "azNodes > LayerForge"
@staticmethod
def _serialize_rgb_tensor_sample(image_tensor):
image_array = (image_tensor.cpu().numpy() * 255).astype(np.uint8)
pil_image = Image.fromarray(image_array, "RGB")
return {
"data": pil_to_data_url(pil_image),
"width": pil_image.width,
"height": pil_image.height,
}
def add_image_to_canvas(self, input_image):
try:
if not isinstance(input_image, torch.Tensor):
@@ -252,27 +262,21 @@ class LayerForgeNode:
log.info(f"Processing batch of {batch_size} image(s)")
if batch_size == 1:
img_np = (input_image.squeeze(0).cpu().numpy() * 255).astype(np.uint8)
pil_img = Image.fromarray(img_np, "RGB")
input_data["input_image"] = pil_to_data_url(pil_img)
input_data["input_image_width"] = pil_img.width
input_data["input_image_height"] = pil_img.height
log.debug(f"Stored single input image: {pil_img.width}x{pil_img.height}")
serialized_image = self._serialize_rgb_tensor_sample(input_image.squeeze(0))
input_data["input_image"] = serialized_image["data"]
input_data["input_image_width"] = serialized_image["width"]
input_data["input_image_height"] = serialized_image["height"]
log.debug(
f"Stored single input image: {serialized_image['width']}x{serialized_image['height']}"
)
else:
images_array = []
for index in range(batch_size):
img_np = (input_image[index].cpu().numpy() * 255).astype(np.uint8)
pil_img = Image.fromarray(img_np, "RGB")
images_array.append(
{
"data": pil_to_data_url(pil_img),
"width": pil_img.width,
"height": pil_img.height,
}
)
serialized_image = self._serialize_rgb_tensor_sample(input_image[index])
images_array.append(serialized_image)
log.debug(
f"Stored batch image {index + 1}/{batch_size}: "
f"{pil_img.width}x{pil_img.height}"
f"{serialized_image['width']}x{serialized_image['height']}"
)
input_data["input_images_batch"] = images_array
@@ -300,15 +304,13 @@ class LayerForgeNode:
if canvas_data:
log.info(f"Canvas data found for node {node_id} from WebSocket")
if canvas_data.get("image"):
image_bytes = base64.b64decode(canvas_data["image"].split(",")[1])
pil_image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
pil_image = data_url_to_pil(canvas_data["image"]).convert("RGB")
image_array = np.array(pil_image).astype(np.float32) / 255.0
processed_image = torch.from_numpy(image_array)[None,]
log.debug(f"Image loaded from WebSocket, shape: {processed_image.shape}")
if canvas_data.get("mask"):
mask_bytes = base64.b64decode(canvas_data["mask"].split(",")[1])
pil_mask = Image.open(io.BytesIO(mask_bytes)).convert("L")
pil_mask = data_url_to_pil(canvas_data["mask"]).convert("L")
mask_array = np.array(pil_mask).astype(np.float32) / 255.0
processed_mask = torch.from_numpy(mask_array)[None,]
log.debug(f"Mask loaded from WebSocket, shape: {processed_mask.shape}")
@@ -353,38 +355,33 @@ class LayerForgeNode:
@classmethod
def get_latest_image(cls):
output_dir = folder_paths.get_output_directory()
files = [
os.path.join(output_dir, filename)
for filename in os.listdir(output_dir)
if os.path.isfile(os.path.join(output_dir, filename))
]
image_files = [
path for path in files if path.lower().endswith((".png", ".jpg", ".jpeg", ".bmp", ".gif"))
]
image_files = list(cls._iter_output_image_paths())
if not image_files:
return None
return max(image_files, key=os.path.getctime)
@classmethod
def get_latest_images(cls, since_timestamp=0):
output_dir = folder_paths.get_output_directory()
files = []
for filename in os.listdir(output_dir):
file_path = os.path.join(output_dir, filename)
if os.path.isfile(file_path) and file_path.lower().endswith(
(".png", ".jpg", ".jpeg", ".bmp", ".gif")
):
try:
mtime = os.path.getmtime(file_path)
if mtime > since_timestamp:
files.append((mtime, file_path))
except OSError:
continue
for file_path in cls._iter_output_image_paths():
try:
mtime = os.path.getmtime(file_path)
if mtime > since_timestamp:
files.append((mtime, file_path))
except OSError:
continue
files.sort(key=lambda item: item[0])
return [item[1] for item in files]
@classmethod
def _iter_output_image_paths(cls):
output_dir = folder_paths.get_output_directory()
for filename in os.listdir(output_dir):
file_path = os.path.join(output_dir, filename)
if os.path.isfile(file_path) and file_path.lower().endswith(_OUTPUT_IMAGE_EXTENSIONS):
yield file_path
@classmethod
def get_flow_status(cls, flow_id=None):
if flow_id:
@@ -423,8 +420,7 @@ class LayerForgeNode:
def store_image(self, image_data):
if isinstance(image_data, str) and image_data.startswith("data:image"):
image_bytes = base64.b64decode(image_data.split(",")[1])
self.cached_image = Image.open(io.BytesIO(image_bytes))
self.cached_image = data_url_to_pil(image_data)
else:
self.cached_image = image_data