Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d84d71541c | ||
|
|
e8895028a2 | ||
|
|
b5c00450de | ||
|
|
59083391c7 | ||
|
|
8f2adacc20 |
@@ -1,110 +0,0 @@
|
|||||||
import os
|
|
||||||
import io
|
|
||||||
import cv2 as cv
|
|
||||||
import numpy as np
|
|
||||||
import torch
|
|
||||||
import requests
|
|
||||||
from folder_paths import get_annotated_filepath
|
|
||||||
|
|
||||||
class ComfyUIDeployExternalEXR:
|
|
||||||
RETURN_TYPES = ("IMAGE", "MASK")
|
|
||||||
RETURN_NAMES = ("image", "mask")
|
|
||||||
FUNCTION = "load_exr"
|
|
||||||
CATEGORY = "🔗ComfyDeploy"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def INPUT_TYPES(cls):
|
|
||||||
return {
|
|
||||||
"required": {
|
|
||||||
"input_id": (
|
|
||||||
"STRING",
|
|
||||||
{"multiline": False, "default": "input_exr"},
|
|
||||||
),
|
|
||||||
"exr_file": ("STRING", {"default": ""}),
|
|
||||||
"tonemap": (["linear", "sRGB", "Reinhard"], {"default": "sRGB"}),
|
|
||||||
},
|
|
||||||
"optional": {
|
|
||||||
"default_image": ("IMAGE",),
|
|
||||||
"default_mask": ("MASK",),
|
|
||||||
"display_name": (
|
|
||||||
"STRING",
|
|
||||||
{"multiline": False, "default": ""},
|
|
||||||
),
|
|
||||||
"description": (
|
|
||||||
"STRING",
|
|
||||||
{"multiline": False, "default": ""},
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def VALIDATE_INPUTS(s, exr_file, **kwargs):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def sRGBtoLinear(self, npArray):
|
|
||||||
less = npArray <= 0.0404482362771082
|
|
||||||
npArray[less] = npArray[less] / 12.92
|
|
||||||
npArray[~less] = np.power((npArray[~less] + 0.055) / 1.055, 2.4)
|
|
||||||
|
|
||||||
def linearToSRGB(self, npArray):
|
|
||||||
less = npArray <= 0.0031308
|
|
||||||
npArray[less] = npArray[less] * 12.92
|
|
||||||
npArray[~less] = np.power(npArray[~less], 1/2.4) * 1.055 - 0.055
|
|
||||||
|
|
||||||
def load_exr(self, input_id, exr_file, tonemap="sRGB",
|
|
||||||
default_image=None, default_mask=None,
|
|
||||||
display_name=None, description=None):
|
|
||||||
try:
|
|
||||||
if exr_file and exr_file != "":
|
|
||||||
if exr_file.startswith(('http://', 'https://')):
|
|
||||||
# Handle URL input
|
|
||||||
response = requests.get(exr_file)
|
|
||||||
# Write to temp buffer
|
|
||||||
buffer = io.BytesIO(response.content)
|
|
||||||
nparr = np.frombuffer(buffer.getvalue(), np.uint8)
|
|
||||||
image = cv.imdecode(nparr, cv.IMREAD_UNCHANGED).astype(np.float32)
|
|
||||||
else:
|
|
||||||
# Handle local file
|
|
||||||
exr_path = get_annotated_filepath(exr_file)
|
|
||||||
image = cv.imread(exr_path, cv.IMREAD_UNCHANGED).astype(np.float32)
|
|
||||||
|
|
||||||
if len(image.shape) == 2:
|
|
||||||
image = np.repeat(image[..., np.newaxis], 3, axis=2)
|
|
||||||
|
|
||||||
# Extract RGB and flip channels
|
|
||||||
rgb = np.flip(image[:,:,:3], 2).copy()
|
|
||||||
|
|
||||||
# Apply tonemapping
|
|
||||||
if tonemap == "sRGB":
|
|
||||||
self.linearToSRGB(rgb)
|
|
||||||
rgb = np.clip(rgb, 0, 1)
|
|
||||||
elif tonemap == "Reinhard":
|
|
||||||
rgb = np.clip(rgb, 0, None)
|
|
||||||
rgb = rgb / (rgb + 1)
|
|
||||||
self.linearToSRGB(rgb)
|
|
||||||
rgb = np.clip(rgb, 0, 1)
|
|
||||||
|
|
||||||
rgb = torch.unsqueeze(torch.from_numpy(rgb), 0)
|
|
||||||
|
|
||||||
# Handle alpha/mask
|
|
||||||
mask = torch.zeros((1, image.shape[0], image.shape[1]), dtype=torch.float32)
|
|
||||||
if image.shape[2] > 3:
|
|
||||||
mask[0] = torch.from_numpy(np.clip(image[:,:,3], 0, 1))
|
|
||||||
|
|
||||||
return (rgb, mask)
|
|
||||||
else:
|
|
||||||
# Return defaults if no file provided
|
|
||||||
return (default_image, default_mask)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading EXR: {str(e)}")
|
|
||||||
# Return defaults on error
|
|
||||||
return (default_image, default_mask)
|
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
|
||||||
"ComfyUIDeployExternalEXR": ComfyUIDeployExternalEXR
|
|
||||||
}
|
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
||||||
"ComfyUIDeployExternalEXR": "External EXR (ComfyUI Deploy)"
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import os
|
||||||
|
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||||
|
import cv2 as cv
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
from folder_paths import get_annotated_filepath
|
||||||
|
|
||||||
|
def linear_to_srgb(np_array):
|
||||||
|
"""Converts a linear RGB numpy array to sRGB."""
|
||||||
|
less = np_array <= 0.0031308
|
||||||
|
np_array[less] = np_array[less] * 12.92
|
||||||
|
np_array[~less] = np.power(np_array[~less], 1/2.4) * 1.055 - 0.055
|
||||||
|
return np_array
|
||||||
|
|
||||||
|
class ExternalExrInput:
|
||||||
|
"""
|
||||||
|
Node to load a single EXR image from a local file path.
|
||||||
|
"""
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"exr_file": ("STRING", {"default": "path/to/image.exr"}),
|
||||||
|
"tonemap": (["linear", "sRGB", "Reinhard"], {"default": "sRGB"}),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"default_image": ("IMAGE",),
|
||||||
|
"default_mask": ("MASK",),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE", "MASK")
|
||||||
|
RETURN_NAMES = ("image", "mask",)
|
||||||
|
FUNCTION = "run"
|
||||||
|
CATEGORY = "🔗ComfyDeploy/EXR"
|
||||||
|
|
||||||
|
def run(self, exr_file, tonemap, default_image=None, default_mask=None):
|
||||||
|
image = None
|
||||||
|
try:
|
||||||
|
if exr_file and exr_file.strip() != "":
|
||||||
|
exr_path = get_annotated_filepath(exr_file)
|
||||||
|
if os.path.exists(exr_path):
|
||||||
|
image = cv.imread(exr_path, cv.IMREAD_UNCHANGED).astype(np.float32)
|
||||||
|
else:
|
||||||
|
print(f"Warning: File not found at {exr_path}")
|
||||||
|
|
||||||
|
if image is None:
|
||||||
|
raise ValueError("Image could not be loaded.")
|
||||||
|
|
||||||
|
if len(image.shape) == 2: # Grayscale
|
||||||
|
image = np.repeat(image[..., np.newaxis], 3, axis=2)
|
||||||
|
|
||||||
|
rgb = np.flip(image[:, :, :3], 2).copy() # BGR to RGB
|
||||||
|
|
||||||
|
# Apply tonemapping
|
||||||
|
if tonemap == "sRGB":
|
||||||
|
rgb = linear_to_srgb(rgb)
|
||||||
|
rgb = np.clip(rgb, 0, 1)
|
||||||
|
elif tonemap == "Reinhard":
|
||||||
|
rgb = np.clip(rgb, 0, None)
|
||||||
|
rgb = rgb / (rgb + 1)
|
||||||
|
rgb = linear_to_srgb(rgb)
|
||||||
|
rgb = np.clip(rgb, 0, 1)
|
||||||
|
|
||||||
|
rgb_tensor = torch.from_numpy(rgb).unsqueeze(0)
|
||||||
|
|
||||||
|
# Handle alpha/mask
|
||||||
|
if image.shape[2] > 3:
|
||||||
|
mask = np.clip(image[:, :, 3], 0, 1)
|
||||||
|
else:
|
||||||
|
mask = np.ones_like(rgb[:, :, 0])
|
||||||
|
mask_tensor = torch.from_numpy(mask).unsqueeze(0)
|
||||||
|
|
||||||
|
return (rgb_tensor, mask_tensor)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading EXR file '{exr_file}': {e}")
|
||||||
|
if default_image is not None and default_mask is not None:
|
||||||
|
print("Returning default image.")
|
||||||
|
return (default_image, default_mask)
|
||||||
|
|
||||||
|
print("Warning: Error loading EXR and no default image. Returning a black image.")
|
||||||
|
blank_image = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
|
||||||
|
blank_mask = torch.zeros((1, 64, 64), dtype=torch.float32)
|
||||||
|
return (blank_image, blank_mask)
|
||||||
|
|
||||||
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
"ExternalExrInput": ExternalExrInput
|
||||||
|
}
|
||||||
|
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
|
"ExternalExrInput": "External EXR Input (ComfyDeploy)"
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import os
|
||||||
|
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||||
|
import cv2 as cv
|
||||||
|
import torch
|
||||||
|
import numpy as np
|
||||||
|
import folder_paths
|
||||||
|
|
||||||
|
def srgb_to_linear(np_array):
|
||||||
|
"""Converts an sRGB numpy array to linear RGB."""
|
||||||
|
less = np_array <= 0.0404482362771082
|
||||||
|
np_array[less] = np_array[less] / 12.92
|
||||||
|
np_array[~less] = np.power((np_array[~less] + 0.055) / 1.055, 2.4)
|
||||||
|
return np_array
|
||||||
|
|
||||||
|
class ExternalExrOutput:
|
||||||
|
"""
|
||||||
|
Node to save a single image as an EXR file to a local path.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self.output_dir = folder_paths.get_output_directory()
|
||||||
|
self.type = "output"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"images": ("IMAGE",),
|
||||||
|
"filepath": ("STRING", {"default": "/tmp/output.exr"}),
|
||||||
|
"tonemap": (["linear", "sRGB"], {"default": "linear"}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ()
|
||||||
|
FUNCTION = "run"
|
||||||
|
OUTPUT_NODE = True
|
||||||
|
CATEGORY = "🔗ComfyDeploy/EXR"
|
||||||
|
|
||||||
|
def run(self, images, filepath, tonemap):
|
||||||
|
if not filepath.endswith(".exr"):
|
||||||
|
raise ValueError("Filepath must end with '.exr'")
|
||||||
|
|
||||||
|
output_dir = os.path.dirname(filepath)
|
||||||
|
if not os.path.isabs(output_dir):
|
||||||
|
raise ValueError("Filepath must be an absolute path.")
|
||||||
|
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# We only process the first image in the batch
|
||||||
|
image_tensor = images[0]
|
||||||
|
|
||||||
|
linear = image_tensor.cpu().numpy().astype(np.float32)
|
||||||
|
|
||||||
|
# If the source is sRGB, convert to linear
|
||||||
|
if tonemap == "sRGB":
|
||||||
|
linear[...,:3] = srgb_to_linear(linear[...,:3])
|
||||||
|
|
||||||
|
# Convert RGB to BGR for OpenCV
|
||||||
|
bgr = np.flip(linear, 2).copy()
|
||||||
|
|
||||||
|
# Save the image
|
||||||
|
cv.imwrite(filepath, bgr)
|
||||||
|
|
||||||
|
print(f"Saved EXR file to: {filepath}")
|
||||||
|
|
||||||
|
return {"ui": {"images": [{"filename": os.path.basename(filepath), "subfolder": os.path.dirname(filepath), "type": self.type}]}}
|
||||||
|
|
||||||
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
"ExternalExrOutput": ExternalExrOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
|
"ExternalExrOutput": "External EXR Output (ComfyDeploy)"
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import os
|
||||||
|
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||||
|
import cv2 as cv
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
import re
|
||||||
|
from folder_paths import get_annotated_filepath
|
||||||
|
|
||||||
|
def linear_to_srgb(np_array):
|
||||||
|
"""Converts a linear RGB numpy array to sRGB."""
|
||||||
|
less = np_array <= 0.0031308
|
||||||
|
np_array[less] = np_array[less] * 12.92
|
||||||
|
np_array[~less] = np.power(np_array[~less], 1/2.4) * 1.055 - 0.055
|
||||||
|
return np_array
|
||||||
|
|
||||||
|
class ExternalExrSequenceInput:
|
||||||
|
"""
|
||||||
|
Node to load a sequence of EXR images from a local filepath pattern, a directory,
|
||||||
|
or a single file within a sequence.
|
||||||
|
"""
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"path_or_pattern": ("STRING", {"default": "path/to/frames_or_pattern"}),
|
||||||
|
"tonemap": (["linear", "sRGB", "Reinhard"], {"default": "sRGB"}),
|
||||||
|
"start_frame": ("INT", {"default": 1, "min": 1}),
|
||||||
|
"end_frame": ("INT", {"default": 50, "min": 1}),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"default_image": ("IMAGE",),
|
||||||
|
"default_mask": ("MASK",),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE", "MASK")
|
||||||
|
RETURN_NAMES = ("image", "mask",)
|
||||||
|
FUNCTION = "run"
|
||||||
|
CATEGORY = "🔗ComfyDeploy/EXR"
|
||||||
|
|
||||||
|
def get_image_paths(self, path_input, start_frame, end_frame):
|
||||||
|
image_paths = []
|
||||||
|
|
||||||
|
# Case 1: Input is a C-style pattern
|
||||||
|
if '%' in path_input:
|
||||||
|
print(f"Pattern detected: {path_input}")
|
||||||
|
for i in range(start_frame, end_frame + 1):
|
||||||
|
fpath = get_annotated_filepath(path_input % i)
|
||||||
|
if os.path.exists(fpath):
|
||||||
|
image_paths.append(fpath)
|
||||||
|
return image_paths
|
||||||
|
|
||||||
|
annotated_path = get_annotated_filepath(path_input)
|
||||||
|
|
||||||
|
# Case 2: Input is a directory
|
||||||
|
if os.path.isdir(annotated_path):
|
||||||
|
print(f"Directory detected: {annotated_path}")
|
||||||
|
files_in_dir = sorted(os.listdir(annotated_path))
|
||||||
|
for filename in files_in_dir:
|
||||||
|
if not filename.lower().endswith('.exr'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
matches = re.findall(r'\d+', filename)
|
||||||
|
if not matches:
|
||||||
|
continue
|
||||||
|
|
||||||
|
frame_number = int(matches[-1])
|
||||||
|
if start_frame <= frame_number <= end_frame:
|
||||||
|
image_paths.append(os.path.join(annotated_path, filename))
|
||||||
|
return image_paths
|
||||||
|
|
||||||
|
# Case 3: Input is a single file from a sequence
|
||||||
|
if os.path.isfile(annotated_path):
|
||||||
|
print(f"Single file detected: {annotated_path}. Attempting to find sequence.")
|
||||||
|
base_dir = os.path.dirname(annotated_path)
|
||||||
|
filename = os.path.basename(annotated_path)
|
||||||
|
|
||||||
|
matches = list(re.finditer(r'(\d+)', filename))
|
||||||
|
if not matches: # It's a single file with no frame number
|
||||||
|
return [annotated_path]
|
||||||
|
|
||||||
|
last_match = matches[-1]
|
||||||
|
num_start_pos, num_end_pos = last_match.span()
|
||||||
|
prefix = filename[:num_start_pos]
|
||||||
|
suffix = filename[num_end_pos:]
|
||||||
|
padding = len(last_match.group(0))
|
||||||
|
|
||||||
|
for i in range(start_frame, end_frame + 1):
|
||||||
|
potential_filename = f"{prefix}{str(i).zfill(padding)}{suffix}"
|
||||||
|
potential_path = os.path.join(base_dir, potential_filename)
|
||||||
|
if os.path.exists(potential_path):
|
||||||
|
image_paths.append(potential_path)
|
||||||
|
return image_paths
|
||||||
|
|
||||||
|
return [] # Return empty if no cases match
|
||||||
|
|
||||||
|
def run(self, path_or_pattern, tonemap, start_frame, end_frame, default_image=None, default_mask=None):
|
||||||
|
try:
|
||||||
|
image_paths = self.get_image_paths(path_or_pattern, start_frame, end_frame)
|
||||||
|
if not image_paths:
|
||||||
|
raise ValueError(f"No EXR files found for '{path_or_pattern}' between frames {start_frame}-{end_frame}.")
|
||||||
|
|
||||||
|
print(f"Found {len(image_paths)} EXR files to load.")
|
||||||
|
rgb_frames = []
|
||||||
|
mask_frames = []
|
||||||
|
|
||||||
|
for path in image_paths:
|
||||||
|
image = cv.imread(path, cv.IMREAD_UNCHANGED)
|
||||||
|
if image is None:
|
||||||
|
print(f"Warning: Could not read file {path}, skipping.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
image = image.astype(np.float32)
|
||||||
|
if len(image.shape) == 2:
|
||||||
|
image = np.repeat(image[..., np.newaxis], 3, axis=2)
|
||||||
|
|
||||||
|
rgb = np.flip(image[:, :, :3], 2).copy()
|
||||||
|
|
||||||
|
if tonemap == "sRGB":
|
||||||
|
rgb = linear_to_srgb(rgb)
|
||||||
|
rgb = np.clip(rgb, 0, 1)
|
||||||
|
elif tonemap == "Reinhard":
|
||||||
|
rgb = np.clip(rgb, 0, None)
|
||||||
|
rgb = rgb / (rgb + 1)
|
||||||
|
rgb = linear_to_srgb(rgb)
|
||||||
|
rgb = np.clip(rgb, 0, 1)
|
||||||
|
|
||||||
|
rgb_frames.append(torch.from_numpy(rgb))
|
||||||
|
|
||||||
|
if image.shape[2] > 3:
|
||||||
|
mask = np.clip(image[:, :, 3], 0, 1)
|
||||||
|
else:
|
||||||
|
mask = np.ones_like(rgb[:, :, 0])
|
||||||
|
mask_frames.append(torch.from_numpy(mask))
|
||||||
|
|
||||||
|
if not rgb_frames:
|
||||||
|
raise ValueError("No frames were loaded successfully.")
|
||||||
|
|
||||||
|
print(f"Successfully loaded {len(rgb_frames)} frames into a batch.")
|
||||||
|
return (torch.stack(rgb_frames, 0), torch.stack(mask_frames, 0))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading EXR sequence: {e}")
|
||||||
|
if default_image is not None and default_mask is not None:
|
||||||
|
print("Returning default image.")
|
||||||
|
return (default_image, default_mask)
|
||||||
|
|
||||||
|
print("Warning: Error loading sequence and no default image. Returning a black image.")
|
||||||
|
blank_image = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
|
||||||
|
blank_mask = torch.zeros((1, 64, 64), dtype=torch.float32)
|
||||||
|
return (blank_image, blank_mask)
|
||||||
|
|
||||||
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
"ExternalExrSequenceInput": ExternalExrSequenceInput
|
||||||
|
}
|
||||||
|
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
|
"ExternalExrSequenceInput": "External EXR Sequence Input (ComfyDeploy)"
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import os
|
||||||
|
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||||
|
import cv2 as cv
|
||||||
|
import torch
|
||||||
|
import numpy as np
|
||||||
|
import re
|
||||||
|
|
||||||
|
def srgb_to_linear(np_array):
|
||||||
|
"""Converts an sRGB numpy array to linear RGB."""
|
||||||
|
less = np_array <= 0.0404482362771082
|
||||||
|
np_array[less] = np_array[less] / 12.92
|
||||||
|
np_array[~less] = np.power((np_array[~less] + 0.055) / 1.055, 2.4)
|
||||||
|
return np_array
|
||||||
|
|
||||||
|
class ExternalExrSequenceOutput:
|
||||||
|
"""
|
||||||
|
Node to save a sequence of images as EXR files to a local directory.
|
||||||
|
It uses a filepath pattern like 'path/to/frame_%04d.exr' to save each frame.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self.type = "output"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"images": ("IMAGE",),
|
||||||
|
"filepath_pattern": ("STRING", {"default": "/tmp/exr_sequence/frame_%04d.exr"}),
|
||||||
|
"tonemap": (["linear", "sRGB"], {"default": "linear"}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ()
|
||||||
|
FUNCTION = "run"
|
||||||
|
OUTPUT_NODE = True
|
||||||
|
CATEGORY = "🔗ComfyDeploy/EXR"
|
||||||
|
|
||||||
|
def run(self, images, filepath_pattern, tonemap):
|
||||||
|
# Basic validation for the filepath pattern
|
||||||
|
if not re.search(r'%0?\d+d', filepath_pattern):
|
||||||
|
raise ValueError("Filepath pattern must contain a C-style format specifier like '%04d'.")
|
||||||
|
|
||||||
|
if not filepath_pattern.endswith(".exr"):
|
||||||
|
raise ValueError("Filepath pattern must end with '.exr'.")
|
||||||
|
|
||||||
|
output_dir = os.path.dirname(filepath_pattern)
|
||||||
|
if not os.path.isabs(output_dir):
|
||||||
|
raise ValueError("Filepath must be an absolute path.")
|
||||||
|
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Convert tensor to numpy array
|
||||||
|
linear_images = images.cpu().numpy().astype(np.float32)
|
||||||
|
|
||||||
|
# If the source is sRGB, convert to linear
|
||||||
|
if tonemap == "sRGB":
|
||||||
|
srgb_to_linear(linear_images[...,:3])
|
||||||
|
|
||||||
|
# Convert RGB to BGR for OpenCV
|
||||||
|
bgr_images = np.flip(linear_images, 3).copy()
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for i, bgr_image in enumerate(bgr_images):
|
||||||
|
frame_num = i + 1
|
||||||
|
try:
|
||||||
|
# Use the pattern to format the full file path
|
||||||
|
file_path = filepath_pattern % frame_num
|
||||||
|
except TypeError:
|
||||||
|
raise ValueError("Invalid format specifier in filepath_pattern. Use '%d', '%04d', etc.")
|
||||||
|
|
||||||
|
# Save the image
|
||||||
|
cv.imwrite(file_path, bgr_image)
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
"filename": os.path.basename(file_path),
|
||||||
|
"subfolder": os.path.dirname(file_path),
|
||||||
|
"type": self.type,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {"ui": {"images": results}}
|
||||||
|
|
||||||
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
"ExternalExrSequenceOutput": ExternalExrSequenceOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
|
"ExternalExrSequenceOutput": "External EXR Sequence Output (ComfyDeploy)"
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import os
|
||||||
|
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||||
|
import cv2 as cv
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
import requests
|
||||||
|
|
||||||
|
def linear_to_srgb(np_array):
|
||||||
|
"""Converts a linear RGB numpy array to sRGB."""
|
||||||
|
less = np_array <= 0.0031308
|
||||||
|
np_array[less] = np_array[less] * 12.92
|
||||||
|
np_array[~less] = np.power(np_array[~less], 1/2.4) * 1.055 - 0.055
|
||||||
|
return np_array
|
||||||
|
|
||||||
|
class HttpExrInput:
|
||||||
|
"""
|
||||||
|
Node to load a single EXR image from a URL, with optional tonemapping.
|
||||||
|
This node is designed to be used in a ComfyDeploy environment where input files are provided via signed URLs.
|
||||||
|
"""
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"get_signed_url": ("STRING", {"multiline": True, "default": ""}),
|
||||||
|
"tonemap": (["linear", "sRGB", "Reinhard"], {"default": "sRGB"}),
|
||||||
|
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"default_image": ("IMAGE",),
|
||||||
|
"default_mask": ("MASK",),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE", "MASK")
|
||||||
|
RETURN_NAMES = ("image", "mask",)
|
||||||
|
FUNCTION = "run"
|
||||||
|
CATEGORY = "🔗ComfyDeploy/EXR"
|
||||||
|
|
||||||
|
def load_exr_from_data(self, exr_data):
|
||||||
|
try:
|
||||||
|
nparr = np.frombuffer(exr_data, np.uint8)
|
||||||
|
# Use cv.IMREAD_UNCHANGED to keep all channels (e.g., alpha)
|
||||||
|
image = cv.imdecode(nparr, cv.IMREAD_UNCHANGED)
|
||||||
|
if image is None:
|
||||||
|
raise ValueError("Failed to decode EXR data.")
|
||||||
|
return image.astype(np.float32)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error decoding EXR data: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def run(self, get_signed_url, tonemap, seed, default_image=None, default_mask=None):
|
||||||
|
if not get_signed_url or get_signed_url.strip() == "":
|
||||||
|
print("Warning: No input URL provided. Returning default image if available.")
|
||||||
|
if default_image is not None and default_mask is not None:
|
||||||
|
return (default_image, default_mask)
|
||||||
|
|
||||||
|
print("Warning: No input URL and no default image. Returning a black image.")
|
||||||
|
blank_image = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
|
||||||
|
blank_mask = torch.zeros((1, 64, 64), dtype=torch.float32)
|
||||||
|
return (blank_image, blank_mask)
|
||||||
|
|
||||||
|
image = None
|
||||||
|
try:
|
||||||
|
print(f"Fetching EXR from URL: {get_signed_url}")
|
||||||
|
response = requests.get(get_signed_url)
|
||||||
|
response.raise_for_status()
|
||||||
|
image = self.load_exr_from_data(response.content)
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"Error fetching EXR from URL {get_signed_url}: {e}")
|
||||||
|
|
||||||
|
if image is None:
|
||||||
|
print("Warning: Could not load or decode EXR image. Returning default image if available.")
|
||||||
|
if default_image is not None and default_mask is not None:
|
||||||
|
return (default_image, default_mask)
|
||||||
|
|
||||||
|
print("Warning: Failed to load EXR and no default image. Returning a black image.")
|
||||||
|
blank_image = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
|
||||||
|
blank_mask = torch.zeros((1, 64, 64), dtype=torch.float32)
|
||||||
|
return (blank_image, blank_mask)
|
||||||
|
|
||||||
|
# BGR to RGB conversion and channel handling
|
||||||
|
if len(image.shape) == 2: # Grayscale
|
||||||
|
image = np.repeat(image[..., np.newaxis], 3, axis=2)
|
||||||
|
|
||||||
|
rgb = np.flip(image[:, :, :3], 2).copy() # OpenCV loads as BGR, convert to RGB
|
||||||
|
|
||||||
|
# Tonemapping
|
||||||
|
if tonemap == "sRGB":
|
||||||
|
rgb = linear_to_srgb(rgb)
|
||||||
|
rgb = np.clip(rgb, 0, 1)
|
||||||
|
elif tonemap == "Reinhard":
|
||||||
|
rgb = np.clip(rgb, 0, None) # Ensure no negative values
|
||||||
|
rgb = rgb / (rgb + 1)
|
||||||
|
rgb = linear_to_srgb(rgb)
|
||||||
|
rgb = np.clip(rgb, 0, 1)
|
||||||
|
|
||||||
|
# Handle alpha channel if it exists
|
||||||
|
if image.shape[2] > 3:
|
||||||
|
mask = np.clip(image[:, :, 3], 0, 1)
|
||||||
|
else:
|
||||||
|
mask = np.ones_like(rgb[:, :, 0]) # Create a full white mask if no alpha
|
||||||
|
|
||||||
|
return (torch.from_numpy(rgb).unsqueeze(0), torch.from_numpy(mask).unsqueeze(0),)
|
||||||
|
|
||||||
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
"HttpExrInput": HttpExrInput
|
||||||
|
}
|
||||||
|
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
|
"HttpExrInput": "HTTP EXR Input (ComfyDeploy)"
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import os
|
||||||
|
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||||
|
import cv2 as cv
|
||||||
|
import torch
|
||||||
|
import numpy as np
|
||||||
|
import requests
|
||||||
|
|
||||||
|
def srgb_to_linear(np_array):
|
||||||
|
"""Converts an sRGB numpy array to linear RGB."""
|
||||||
|
less = np_array <= 0.0404482362771082
|
||||||
|
np_array[less] = np_array[less] / 12.92
|
||||||
|
np_array[~less] = np.power((np_array[~less] + 0.055) / 1.055, 2.4)
|
||||||
|
return np_array
|
||||||
|
|
||||||
|
class HttpExrOutput:
|
||||||
|
"""
|
||||||
|
Node to save a single EXR image to a pre-signed URL.
|
||||||
|
This node is designed for ComfyDeploy to upload the generated EXR file.
|
||||||
|
"""
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"images": ("IMAGE",),
|
||||||
|
"put_signed_url": ("STRING", {"multiline": True, "default": ""}),
|
||||||
|
"tonemap": (["linear", "sRGB"], {"default": "linear"}),
|
||||||
|
},
|
||||||
|
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ()
|
||||||
|
FUNCTION = "run"
|
||||||
|
OUTPUT_NODE = True
|
||||||
|
CATEGORY = "ComfyDeploy/EXR"
|
||||||
|
|
||||||
|
def run(self, images, put_signed_url, tonemap, prompt=None, extra_pnginfo=None):
|
||||||
|
if not put_signed_url or put_signed_url.strip() == "":
|
||||||
|
print("Warning: No put_signed_url provided. Nothing will be uploaded.")
|
||||||
|
return {"ui": {"images": []}}
|
||||||
|
|
||||||
|
# We process only the first image of the batch
|
||||||
|
image_tensor = images[0]
|
||||||
|
|
||||||
|
# Convert tensor to numpy array, assuming it's in range [0, 1]
|
||||||
|
linear = image_tensor.cpu().numpy().astype(np.float32)
|
||||||
|
|
||||||
|
# If the source is sRGB, convert to linear
|
||||||
|
if tonemap == "sRGB":
|
||||||
|
linear[...,:3] = srgb_to_linear(linear[...,:3])
|
||||||
|
|
||||||
|
# Convert RGB to BGR for OpenCV
|
||||||
|
bgr = np.flip(linear, 2).copy()
|
||||||
|
|
||||||
|
results = []
|
||||||
|
try:
|
||||||
|
# Encode the image to the EXR format in memory
|
||||||
|
is_success, buffer = cv.imencode(".exr", bgr)
|
||||||
|
if not is_success:
|
||||||
|
raise Exception("Failed to encode image to EXR format.")
|
||||||
|
|
||||||
|
# Upload the image data to the pre-signed URL
|
||||||
|
response = requests.put(put_signed_url, data=buffer.tobytes(), headers={'Content-Type': 'image/x-exr'})
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
print(f"Successfully uploaded EXR to: {put_signed_url}")
|
||||||
|
# The UI can optionally display a link or confirmation
|
||||||
|
results.append({"url": put_signed_url, "output_id": "output_http_exr"})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error uploading EXR to signed URL: {e}")
|
||||||
|
|
||||||
|
return {"ui": {"images": results}}
|
||||||
|
|
||||||
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
"HttpExrOutput": HttpExrOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
|
"HttpExrOutput": "HTTP EXR Output (ComfyDeploy)"
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import os
|
||||||
|
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||||
|
import cv2 as cv
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
def linear_to_srgb(np_array):
|
||||||
|
"""Converts a linear RGB numpy array to sRGB."""
|
||||||
|
less = np_array <= 0.0031308
|
||||||
|
np_array[less] = np_array[less] * 12.92
|
||||||
|
np_array[~less] = np.power(np_array[~less], 1/2.4) * 1.055 - 0.055
|
||||||
|
return np_array
|
||||||
|
|
||||||
|
class HttpExrSequenceInput:
|
||||||
|
"""
|
||||||
|
Node to load a sequence of EXR images from a list of URLs provided as a JSON string.
|
||||||
|
"""
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"urls_json": ("STRING", {"multiline": True, "default": "[]"}),
|
||||||
|
"tonemap": (["linear", "sRGB", "Reinhard"], {"default": "sRGB"}),
|
||||||
|
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"default_image": ("IMAGE",),
|
||||||
|
"default_mask": ("MASK",),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE", "MASK")
|
||||||
|
RETURN_NAMES = ("image", "mask",)
|
||||||
|
FUNCTION = "run"
|
||||||
|
CATEGORY = "🔗ComfyDeploy/EXR"
|
||||||
|
|
||||||
|
def load_exr_from_data(self, exr_data):
|
||||||
|
try:
|
||||||
|
nparr = np.frombuffer(exr_data, np.uint8)
|
||||||
|
image = cv.imdecode(nparr, cv.IMREAD_UNCHANGED)
|
||||||
|
if image is None:
|
||||||
|
raise ValueError("Failed to decode EXR data.")
|
||||||
|
return image.astype(np.float32)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error decoding EXR data: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def run(self, urls_json, tonemap, seed, default_image=None, default_mask=None):
|
||||||
|
try:
|
||||||
|
urls = json.loads(urls_json)
|
||||||
|
if not isinstance(urls, list) or not all(isinstance(u, str) for u in urls):
|
||||||
|
raise ValueError("urls_json must be a JSON array of URL strings.")
|
||||||
|
except (json.JSONDecodeError, ValueError) as e:
|
||||||
|
print(f"Error parsing urls_json: {e}. Using default image if available.")
|
||||||
|
urls = []
|
||||||
|
|
||||||
|
if not urls:
|
||||||
|
if default_image is not None and default_mask is not None:
|
||||||
|
return (default_image, default_mask)
|
||||||
|
|
||||||
|
print("Warning: No valid URLs and no default image. Returning a black image.")
|
||||||
|
blank_image = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
|
||||||
|
blank_mask = torch.zeros((1, 64, 64), dtype=torch.float32)
|
||||||
|
return (blank_image, blank_mask)
|
||||||
|
|
||||||
|
rgb_frames = []
|
||||||
|
mask_frames = []
|
||||||
|
|
||||||
|
for url in urls:
|
||||||
|
image = None
|
||||||
|
try:
|
||||||
|
print(f"Fetching EXR from URL: {url}")
|
||||||
|
response = requests.get(url)
|
||||||
|
response.raise_for_status()
|
||||||
|
image = self.load_exr_from_data(response.content)
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"Error fetching EXR from URL {url}: {e}")
|
||||||
|
|
||||||
|
if image is None:
|
||||||
|
print(f"Warning: Could not decode EXR from {url}. Skipping frame.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(image.shape) == 2: # Grayscale
|
||||||
|
image = np.repeat(image[..., np.newaxis], 3, axis=2)
|
||||||
|
|
||||||
|
rgb = np.flip(image[:, :, :3], 2).copy() # BGR to RGB
|
||||||
|
|
||||||
|
if tonemap == "sRGB":
|
||||||
|
rgb = linear_to_srgb(rgb)
|
||||||
|
rgb = np.clip(rgb, 0, 1)
|
||||||
|
elif tonemap == "Reinhard":
|
||||||
|
rgb = np.clip(rgb, 0, None)
|
||||||
|
rgb = rgb / (rgb + 1)
|
||||||
|
rgb = linear_to_srgb(rgb)
|
||||||
|
rgb = np.clip(rgb, 0, 1)
|
||||||
|
|
||||||
|
rgb_frames.append(torch.from_numpy(rgb))
|
||||||
|
|
||||||
|
if image.shape[2] > 3:
|
||||||
|
mask = np.clip(image[:, :, 3], 0, 1)
|
||||||
|
else:
|
||||||
|
mask = np.ones_like(rgb[:, :, 0])
|
||||||
|
mask_frames.append(torch.from_numpy(mask))
|
||||||
|
|
||||||
|
if not rgb_frames:
|
||||||
|
print("Could not load any frames. Returning default image if available.")
|
||||||
|
if default_image is not None and default_mask is not None:
|
||||||
|
return (default_image, default_mask)
|
||||||
|
|
||||||
|
print("Warning: Failed to load any frames and no default image. Returning a black image.")
|
||||||
|
blank_image = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
|
||||||
|
blank_mask = torch.zeros((1, 64, 64), dtype=torch.float32)
|
||||||
|
return (blank_image, blank_mask)
|
||||||
|
|
||||||
|
print(f"Loaded {len(rgb_frames)} frames successfully.")
|
||||||
|
return (torch.stack(rgb_frames, 0), torch.stack(mask_frames, 0))
|
||||||
|
|
||||||
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
"HttpExrSequenceInput": HttpExrSequenceInput
|
||||||
|
}
|
||||||
|
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
|
"HttpExrSequenceInput": "HTTP EXR Sequence Input (ComfyDeploy)"
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import os
|
||||||
|
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||||
|
import cv2 as cv
|
||||||
|
import torch
|
||||||
|
import numpy as np
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
def srgb_to_linear(np_array):
|
||||||
|
"""Converts an sRGB numpy array to linear RGB."""
|
||||||
|
less = np_array <= 0.0404482362771082
|
||||||
|
np_array[less] = np_array[less] / 12.92
|
||||||
|
np_array[~less] = np.power((np_array[~less] + 0.055) / 1.055, 2.4)
|
||||||
|
return np_array
|
||||||
|
|
||||||
|
class HttpExrSequenceOutput:
|
||||||
|
"""
|
||||||
|
Node to save a sequence of images as EXR files to a list of pre-signed URLs.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self.type = "output"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"images": ("IMAGE",),
|
||||||
|
"upload_urls_json": ("STRING", {"multiline": True, "default": "[]"}),
|
||||||
|
"tonemap": (["linear", "sRGB"], {"default": "linear"}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ()
|
||||||
|
FUNCTION = "run"
|
||||||
|
OUTPUT_NODE = True
|
||||||
|
CATEGORY = "🔗ComfyDeploy/EXR"
|
||||||
|
|
||||||
|
def run(self, images, upload_urls_json, tonemap):
|
||||||
|
try:
|
||||||
|
upload_urls = json.loads(upload_urls_json)
|
||||||
|
if not isinstance(upload_urls, list) or not all(isinstance(u, str) for u in upload_urls):
|
||||||
|
raise ValueError("upload_urls_json must be a JSON array of URL strings.")
|
||||||
|
except (json.JSONDecodeError, ValueError) as e:
|
||||||
|
print(f"Error parsing upload_urls_json: {e}. Aborting upload.")
|
||||||
|
return {"ui": {"images": []}}
|
||||||
|
|
||||||
|
if not upload_urls:
|
||||||
|
print("Warning: No upload URLs provided. Nothing will be uploaded.")
|
||||||
|
return {"ui": {"images": []}}
|
||||||
|
|
||||||
|
if len(images) != len(upload_urls):
|
||||||
|
print(f"Warning: Mismatch between number of images ({len(images)}) and upload URLs ({len(upload_urls)}). Aborting upload.")
|
||||||
|
return {"ui": {"images": []}}
|
||||||
|
|
||||||
|
# Convert tensor to numpy array
|
||||||
|
linear_images = images.cpu().numpy().astype(np.float32)
|
||||||
|
|
||||||
|
# If the source is sRGB, convert all images to linear
|
||||||
|
if tonemap == "sRGB":
|
||||||
|
srgb_to_linear(linear_images[...,:3])
|
||||||
|
|
||||||
|
# Convert RGB to BGR for OpenCV
|
||||||
|
bgr_images = np.flip(linear_images, 3).copy()
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for i, (bgr_image, url) in enumerate(zip(bgr_images, upload_urls)):
|
||||||
|
try:
|
||||||
|
# Encode the image to the EXR format in memory
|
||||||
|
is_success, buffer = cv.imencode(".exr", bgr_image)
|
||||||
|
if not is_success:
|
||||||
|
raise Exception("Failed to encode image to EXR format.")
|
||||||
|
|
||||||
|
# Upload the image data to the pre-signed URL
|
||||||
|
response = requests.put(url, data=buffer.tobytes(), headers={'Content-Type': 'image/x-exr'})
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
print(f"Successfully uploaded frame {i+1} to: {url}")
|
||||||
|
results.append({"url": url})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error uploading frame {i+1} to {url}: {e}")
|
||||||
|
|
||||||
|
return {"ui": {"images": results}}
|
||||||
|
|
||||||
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
"HttpExrSequenceOutput": HttpExrSequenceOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
|
"HttpExrSequenceOutput": "HTTP EXR Sequence Output (ComfyDeploy)"
|
||||||
|
}
|
||||||
+16
@@ -56,11 +56,27 @@ streaming_prompt_metadata: dict[str, StreamingPrompt] = {}
|
|||||||
class BinaryEventTypes:
|
class BinaryEventTypes:
|
||||||
PREVIEW_IMAGE = 1
|
PREVIEW_IMAGE = 1
|
||||||
UNENCODED_PREVIEW_IMAGE = 2
|
UNENCODED_PREVIEW_IMAGE = 2
|
||||||
|
EXR_IMAGE = 4
|
||||||
|
|
||||||
|
|
||||||
max_output_id_length = 24
|
max_output_id_length = 24
|
||||||
|
|
||||||
|
|
||||||
|
async def send_exr(image_data, sid=None, output_id: str = None):
|
||||||
|
max_length = max_output_id_length
|
||||||
|
output_id = output_id[:max_length]
|
||||||
|
padded_output_id = output_id.ljust(max_length, "\x00")
|
||||||
|
encoded_output_id = padded_output_id.encode("ascii", "replace")
|
||||||
|
|
||||||
|
bytesIO = BytesIO()
|
||||||
|
# 10 bytes for the output_id
|
||||||
|
bytesIO.write(encoded_output_id)
|
||||||
|
bytesIO.write(image_data)
|
||||||
|
|
||||||
|
preview_bytes = bytesIO.getvalue()
|
||||||
|
await send_bytes(BinaryEventTypes.EXR_IMAGE, preview_bytes, sid=sid)
|
||||||
|
|
||||||
|
|
||||||
async def send_image(image_data, sid=None, output_id: str = None):
|
async def send_image(image_data, sid=None, output_id: str = None):
|
||||||
max_length = max_output_id_length
|
max_length = max_output_id_length
|
||||||
output_id = output_id[:max_length]
|
output_id = output_id[:max_length]
|
||||||
|
|||||||
Reference in New Issue
Block a user