changed exr sequence to work with start and end frames, made node mappings more consistent across project
This commit is contained in:
+20
-20
@@ -1,11 +1,11 @@
|
||||
|
||||
# Import all available modules
|
||||
from .modules.image_loader import coco_loader
|
||||
from .modules.load_exr import load_exr
|
||||
from .modules.saver import saver
|
||||
from .modules.load_exr_layer_by_name import load_exr_layer_by_name, shamble_cryptomatte
|
||||
from .modules.colorspace import colorspace
|
||||
from .modules.znormalize import znormalize
|
||||
from .modules.image_loader import ImageLoader
|
||||
from .modules.load_exr import LoadExr
|
||||
from .modules.saver import SaverNode
|
||||
from .modules.load_exr_layer_by_name import LoadExrLayerByName, CryptomatteLayer
|
||||
from .modules.colorspace import ColorspaceNode
|
||||
from .modules.znormalize import ZNormalizeNode
|
||||
|
||||
# Initialize node mappings
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
@@ -18,24 +18,24 @@ WEB_DIRECTORY = os.path.join(NODE_DIR, "js")
|
||||
|
||||
# Add all available node classes
|
||||
NODE_CLASS_MAPPINGS.update({
|
||||
"ImageLoader": coco_loader,
|
||||
"LoadExr": load_exr,
|
||||
"SaverNode": saver,
|
||||
"LoadExrLayerByName": load_exr_layer_by_name,
|
||||
"CryptomatteLayer": shamble_cryptomatte,
|
||||
"ColorspaceNode": colorspace,
|
||||
"ZNormalizeNode": znormalize
|
||||
"ImageLoader": ImageLoader,
|
||||
"LoadExr": LoadExr,
|
||||
"SaverNode": SaverNode,
|
||||
"LoadExrLayerByName": LoadExrLayerByName,
|
||||
"CryptomatteLayer": CryptomatteLayer,
|
||||
"ColorspaceNode": ColorspaceNode,
|
||||
"ZNormalizeNode": ZNormalizeNode
|
||||
})
|
||||
|
||||
# Add display names for better UI presentation
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update({
|
||||
"ImageLoader": "coco Loader",
|
||||
"LoadExr": "coco Load EXR",
|
||||
"SaverNode": "coco Saver",
|
||||
"LoadExrLayerByName": "Load EXR Layer by Name",
|
||||
"CryptomatteLayer": "Cryptomatte Layer",
|
||||
"ColorspaceNode": "Colorspace",
|
||||
"ZNormalizeNode": "Z Normalize"
|
||||
"ImageLoader": "CoCo Loader",
|
||||
"LoadExr": "CoCo Load EXR",
|
||||
"SaverNode": "CoCo Saver",
|
||||
"LoadExrLayerByName": "CoCo Load EXR Layer by Name",
|
||||
"CryptomatteLayer": "CoCo Cryptomatte Layer",
|
||||
"ColorspaceNode": "CoCo Colorspace",
|
||||
"ZNormalizeNode": "CoCo Z Normalize"
|
||||
})
|
||||
|
||||
# Expose what ComfyUI needs
|
||||
|
||||
@@ -37,10 +37,13 @@ function addSequenceWidgets(nodeType, nodeData) {
|
||||
}
|
||||
let sequenceWidgetsCount = 0;
|
||||
chainCallback(typeWidget, "callback", (value) => {
|
||||
console.log("Load EXR widget callback triggered, value:", value);
|
||||
const formats = (LiteGraph.registered_node_types[this.type]
|
||||
?.nodeData?.input?.required?.image_type?.[1]?.formats);
|
||||
console.log("Available formats:", formats);
|
||||
let newWidgets = [];
|
||||
if (formats?.[value]) {
|
||||
console.log("Creating widgets for format:", value, formats[value]);
|
||||
let sequenceWidgets = formats[value];
|
||||
for (let wDef of sequenceWidgets) {
|
||||
let type = wDef[1];
|
||||
@@ -55,9 +58,11 @@ function addSequenceWidgets(nodeType, nodeData) {
|
||||
}
|
||||
}
|
||||
w.config = wDef.slice(1);
|
||||
console.log("Created widget:", w.name, "value:", w.value, "config:", w.config);
|
||||
newWidgets.push(w);
|
||||
}
|
||||
}
|
||||
console.log("Total new widgets created:", newWidgets.length);
|
||||
let removed = this.widgets.splice(typeWidgetIndex,
|
||||
sequenceWidgetsCount, ...newWidgets);
|
||||
for (let w of removed) {
|
||||
|
||||
@@ -4,11 +4,11 @@ app.registerExtension({
|
||||
name: "LoadExrLayerByName",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
// Only handle the Load EXR Layer by Name nodes
|
||||
if (nodeData.name !== "load_exr_layer_by_name" && nodeData.name !== "shamble_cryptomatte") {
|
||||
if (nodeData.name !== "LoadExrLayerByName" && nodeData.name !== "CryptomatteLayer") {
|
||||
return;
|
||||
}
|
||||
|
||||
const isCryptomatte = nodeData.name === "shamble_cryptomatte";
|
||||
const isCryptomatte = nodeData.name === "CryptomatteLayer";
|
||||
console.log(`Registering ${isCryptomatte ? "Cryptomatte " : ""}Load EXR Layer by Name node`);
|
||||
|
||||
// Store original methods to call them later
|
||||
@@ -113,8 +113,8 @@ app.registerExtension({
|
||||
const sourceNode = connectionInfo.node;
|
||||
console.log(`Finding available layers from ${sourceNode.title || sourceNode.type}`);
|
||||
|
||||
// Check if the source is a load_exr node
|
||||
const isLoadExr = sourceNode.type.includes("load_exr");
|
||||
// Check if the source is a LoadExr node
|
||||
const isLoadExr = sourceNode.type.includes("LoadExr");
|
||||
|
||||
// Try multiple approaches to get layer names
|
||||
let layerNames = [];
|
||||
@@ -242,7 +242,7 @@ app.registerExtension({
|
||||
app.addEventListener("graphExecuted", (e) => {
|
||||
try {
|
||||
// Get all matched nodes
|
||||
const matchedNodes = findNodes(isCryptomatte ? "shamble_cryptomatte" : "load_exr_layer_by_name");
|
||||
const matchedNodes = findNodes(isCryptomatte ? "CryptomatteLayer" : "LoadExrLayerByName");
|
||||
|
||||
if (matchedNodes.length === 0) {
|
||||
return; // No nodes to update
|
||||
|
||||
@@ -27,7 +27,7 @@ except ImportError:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class colorspace:
|
||||
class ColorspaceNode:
|
||||
"""Simplified colorspace converter using colour-science library."""
|
||||
|
||||
def __init__(self):
|
||||
@@ -357,13 +357,13 @@ def test_colour_science_setup():
|
||||
return False
|
||||
|
||||
# Register the node
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"ColorspaceNode": colorspace,
|
||||
}
|
||||
# NODE_CLASS_MAPPINGS = {
|
||||
# "ColorspaceNode": colorspace,
|
||||
# }
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"ColorspaceNode": "Colorspace",
|
||||
}
|
||||
# NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
# "ColorspaceNode": "Colorspace",
|
||||
# }
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_colour_science_setup()
|
||||
|
||||
@@ -9,7 +9,7 @@ logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class coco_loader:
|
||||
class ImageLoader:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -128,10 +128,10 @@ class coco_loader:
|
||||
return image_tensor
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"coco_loader": coco_loader
|
||||
}
|
||||
# NODE_CLASS_MAPPINGS = {
|
||||
# "coco_loader": coco_loader
|
||||
# }
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"coco_loader": "Load Image (supports jpg, png, tif, avif, webp)"
|
||||
}
|
||||
# NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
# "coco_loader": "Load Image (supports jpg, png, tif, avif, webp)"
|
||||
# }
|
||||
|
||||
+134
-265
@@ -38,10 +38,17 @@ except ImportError:
|
||||
def detect_sequence_pattern(path):
|
||||
return '####' in path if path else False
|
||||
@staticmethod
|
||||
def validate_sequence_parameters(start, count, step):
|
||||
return (start if start is not None else 1,
|
||||
count if count is not None else 10,
|
||||
step if step is not None else 1)
|
||||
def validate_sequence_parameters(start, end, step):
|
||||
if start is None:
|
||||
raise ValueError("start_frame parameter is required and cannot be None. " +
|
||||
"Make sure the node is in 'sequence' mode and the sequence widgets are visible.")
|
||||
if end is None:
|
||||
raise ValueError("end_frame parameter is required and cannot be None. " +
|
||||
"Make sure the node is in 'sequence' mode and the sequence widgets are visible.")
|
||||
if step is None:
|
||||
raise ValueError("frame_step parameter is required and cannot be None. " +
|
||||
"Make sure the node is in 'sequence' mode and the sequence widgets are visible.")
|
||||
return (start, end, step)
|
||||
@staticmethod
|
||||
def find_sequence_files(pattern_path):
|
||||
raise NotImplementedError("SequenceHandler.find_sequence_files not available - utils not imported")
|
||||
@@ -49,18 +56,18 @@ except ImportError:
|
||||
def extract_frame_numbers(files):
|
||||
raise NotImplementedError("SequenceHandler.extract_frame_numbers not available - utils not imported")
|
||||
@staticmethod
|
||||
def select_sequence_frames(frames, start, count, step, strict_count=True):
|
||||
def select_sequence_frames(frames, start, end, step):
|
||||
raise NotImplementedError("SequenceHandler.select_sequence_frames not available - utils not imported")
|
||||
@staticmethod
|
||||
def generate_frame_paths(pattern, start, count, step):
|
||||
def generate_frame_paths(pattern, start, end, step):
|
||||
raise NotImplementedError("SequenceHandler.generate_frame_paths not available - utils not imported")
|
||||
|
||||
class DynamicUIHelper:
|
||||
@staticmethod
|
||||
def create_sequence_widgets():
|
||||
return {"sequence": [["start_frame", "INT", {"default": 1}], ["frame_count", "INT", {"default": 10}], ["frame_step", "INT", {"default": 1}]]}
|
||||
return {"sequence": [["start_frame", "INT", {"default": 1}], ["end_frame", "INT", {"default": 100}], ["frame_step", "INT", {"default": 1}]]}
|
||||
|
||||
class load_exr:
|
||||
class LoadExr:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
# Use shared sequence widgets
|
||||
@@ -101,7 +108,7 @@ class load_exr:
|
||||
return float("NaN") # Always execute the node
|
||||
|
||||
def load_image(self, image_path: str, image_type: str = "single_frame", normalize: bool = False,
|
||||
start_frame: int = None, frame_count: int = None, frame_step: int = None,
|
||||
start_frame: int = None, end_frame: int = None, frame_step: int = None,
|
||||
node_id: str = None, layer_data: Dict = None, **kwargs) -> List:
|
||||
"""
|
||||
Load an EXR image with support for multiple layers/channel groups.
|
||||
@@ -120,11 +127,18 @@ class load_exr:
|
||||
raise ImportError("OpenImageIO is required for EXR loading but not available")
|
||||
|
||||
try:
|
||||
# Validate and provide defaults for sequence parameters
|
||||
start_frame, frame_count, frame_step = SequenceHandler.validate_sequence_parameters(
|
||||
start_frame, frame_count, frame_step
|
||||
)
|
||||
|
||||
# Debug: Log the actual parameter values received
|
||||
debug_log(logger, "debug", f"Parameter values received",
|
||||
f"start_frame={start_frame}, end_frame={end_frame}, frame_step={frame_step}, image_type={image_type}")
|
||||
|
||||
# Provide defensive defaults for sequence parameters (like saver pattern)
|
||||
if start_frame is None:
|
||||
start_frame = 1
|
||||
if end_frame is None:
|
||||
end_frame = 100
|
||||
if frame_step is None:
|
||||
frame_step = 1
|
||||
|
||||
# Determine loading mode based on image_type and pattern detection
|
||||
has_sequence_pattern = SequenceHandler.detect_sequence_pattern(image_path)
|
||||
|
||||
@@ -132,8 +146,12 @@ class load_exr:
|
||||
is_sequence = (image_type == "sequence") or has_sequence_pattern
|
||||
|
||||
if is_sequence:
|
||||
# Validate sequence parameters with defensive defaults
|
||||
start_frame, end_frame, frame_step = SequenceHandler.validate_sequence_parameters(
|
||||
start_frame, end_frame, frame_step
|
||||
)
|
||||
# Load as sequence
|
||||
return self._load_sequence(image_path, normalize, start_frame, frame_count, frame_step, node_id, layer_data)
|
||||
return self._load_sequence(image_path, normalize, start_frame, end_frame, frame_step, node_id, layer_data)
|
||||
else:
|
||||
# Validate single image path
|
||||
if not os.path.isfile(image_path):
|
||||
@@ -145,219 +163,75 @@ class load_exr:
|
||||
debug_log(logger, "error", "Error loading EXR", f"Error loading EXR file {image_path}: {str(e)}")
|
||||
raise
|
||||
|
||||
def _load_sequence(self, pattern_path: str, normalize: bool, start_frame: int, frame_count: int, frame_step: int, node_id: str = None, layer_data: Dict = None) -> List:
|
||||
def _load_sequence(self, pattern_path: str, normalize: bool, start_frame: int, end_frame: int, frame_step: int, node_id: str = None, layer_data: Dict = None) -> List:
|
||||
"""Load a sequence of EXR files and return batched tensors"""
|
||||
frame_count = len(range(start_frame, end_frame + 1, frame_step))
|
||||
debug_log(logger, "info", f"Loading EXR sequence: {frame_count} frames",
|
||||
f"Loading EXR sequence: {pattern_path} (start={start_frame}, count={frame_count}, step={frame_step})")
|
||||
f"Loading EXR sequence: {pattern_path} (start={start_frame}, end={end_frame}, step={frame_step})")
|
||||
|
||||
# Use shared sequence utilities to find and select frames
|
||||
# Try to use SequenceHandler - if it fails, log error and raise exception
|
||||
try:
|
||||
existing_sequence_files = SequenceHandler.find_sequence_files(pattern_path)
|
||||
debug_log(logger, "info", f"Using SequenceHandler: found {len(existing_sequence_files)} files",
|
||||
debug_log(logger, "info", f"Found {len(existing_sequence_files)} sequence files",
|
||||
f"SequenceHandler found {len(existing_sequence_files)} files for pattern: {pattern_path}")
|
||||
except AttributeError:
|
||||
# Fallback if shared utilities not available
|
||||
debug_log(logger, "warning", "Using fallback sequence detection",
|
||||
f"SequenceHandler not available, using fallback for pattern: {pattern_path}")
|
||||
import glob
|
||||
glob_pattern = pattern_path.replace('####', '*')
|
||||
matching_files = glob.glob(glob_pattern)
|
||||
debug_log(logger, "info", f"Glob found {len(matching_files)} potential matches",
|
||||
f"Glob pattern '{glob_pattern}' found {len(matching_files)} files: {matching_files[:5]}")
|
||||
|
||||
# Create regex pattern with better cross-platform support
|
||||
escaped_pattern = re.escape(pattern_path)
|
||||
# Replace escaped #### with regex for 4 digits - handle both Windows and Unix escaping
|
||||
if '\\\\#\\\\#\\\\#\\\\#' in escaped_pattern:
|
||||
pattern_for_regex = escaped_pattern.replace('\\\\#\\\\#\\\\#\\\\#', r'\d{4}')
|
||||
elif '\\#\\#\\#\\#' in escaped_pattern:
|
||||
pattern_for_regex = escaped_pattern.replace('\\#\\#\\#\\#', r'\d{4}')
|
||||
else:
|
||||
# Direct replacement if no escaping occurred
|
||||
pattern_for_regex = escaped_pattern.replace('####', r'\d{4}')
|
||||
|
||||
debug_log(logger, "info", f"Using regex pattern: {pattern_for_regex}",
|
||||
f"Original: {pattern_path}, Escaped: {escaped_pattern}, Regex: {pattern_for_regex}")
|
||||
|
||||
regex_pattern = re.compile(pattern_for_regex)
|
||||
existing_sequence_files = []
|
||||
for file_path in matching_files:
|
||||
if regex_pattern.match(file_path):
|
||||
existing_sequence_files.append(file_path)
|
||||
else:
|
||||
# Log first few mismatches for debugging
|
||||
if len(existing_sequence_files) < 3:
|
||||
debug_log(logger, "debug", f"Regex mismatch: {file_path}",
|
||||
f"File '{file_path}' doesn't match regex '{pattern_for_regex}'")
|
||||
existing_sequence_files.sort()
|
||||
except (AttributeError, NotImplementedError) as e:
|
||||
logger.error(f"SequenceHandler.find_sequence_files not available: {str(e)}")
|
||||
logger.error("Sequence utilities are required but not properly installed or imported")
|
||||
raise ImportError("Sequence utilities not available. Please ensure utils are properly installed.")
|
||||
|
||||
if existing_sequence_files:
|
||||
debug_log(logger, "info", f"Found {len(existing_sequence_files)} files in sequence",
|
||||
f"Found sequence files: {len(existing_sequence_files)} files matching pattern")
|
||||
|
||||
# Extract frame numbers and sort
|
||||
try:
|
||||
frame_info = SequenceHandler.extract_frame_numbers(existing_sequence_files)
|
||||
except AttributeError:
|
||||
# Fallback implementation using improved frame extraction
|
||||
frame_info = []
|
||||
for file_path in existing_sequence_files:
|
||||
# Try improved extraction first
|
||||
try:
|
||||
frame_num = SequenceHandler.extract_frame_number_from_path(file_path)
|
||||
if frame_num is not None:
|
||||
frame_info.append((frame_num, file_path))
|
||||
except (AttributeError, NameError):
|
||||
# Ultimate fallback - original regex approach
|
||||
import re
|
||||
basename = os.path.basename(file_path)
|
||||
match = re.search(r'(\d{3,4})', basename) # Look for 3-4 digit numbers
|
||||
if match:
|
||||
frame_num = int(match.group(1))
|
||||
frame_info.append((frame_num, file_path))
|
||||
frame_info.sort()
|
||||
|
||||
# Apply frame selection logic - STRICT count enforcement
|
||||
try:
|
||||
# Debug: Check which SequenceHandler we're using
|
||||
debug_log(logger, "debug", "Using SequenceHandler.select_sequence_frames",
|
||||
f"SequenceHandler class: {SequenceHandler.__module__}.{SequenceHandler.__name__}")
|
||||
|
||||
selected_frames = SequenceHandler.select_sequence_frames(
|
||||
frame_info, start_frame, frame_count, frame_step, strict_count=False
|
||||
)
|
||||
except AttributeError:
|
||||
# Fallback implementation - use improved frame selection logic
|
||||
selected_frames = []
|
||||
|
||||
# Method 1: Try to match exact frame numbers first
|
||||
for i in range(0, frame_count * frame_step, frame_step):
|
||||
target_frame = start_frame + i
|
||||
# Find the exact matching frame
|
||||
for frame_num, file_path in frame_info:
|
||||
if frame_num == target_frame:
|
||||
selected_frames.append(file_path)
|
||||
debug_log(logger, "debug", f"Exact match found",
|
||||
f"Selected frame {frame_num} (target: {target_frame})")
|
||||
break
|
||||
|
||||
# Method 2: If we don't have enough exact matches, use fallback selection
|
||||
if len(selected_frames) < frame_count:
|
||||
debug_log(logger, "info", f"Only found {len(selected_frames)} exact matches",
|
||||
f"Need {frame_count} frames, found {len(selected_frames)} exact matches. Using fallback selection.")
|
||||
|
||||
# Fill in with available frames, respecting start_frame and step as much as possible
|
||||
available_paths = [fp for fn, fp in frame_info if fn >= start_frame]
|
||||
|
||||
# Take every nth frame from available, where n approximates the desired step
|
||||
if available_paths and frame_step > 1:
|
||||
step_adjusted = max(1, frame_step)
|
||||
additional_frames = available_paths[::step_adjusted]
|
||||
|
||||
# Add frames we don't already have
|
||||
for frame_path in additional_frames:
|
||||
if frame_path not in selected_frames and len(selected_frames) < frame_count:
|
||||
selected_frames.append(frame_path)
|
||||
|
||||
# If still not enough, just take sequential frames
|
||||
if len(selected_frames) < frame_count:
|
||||
for frame_path in available_paths:
|
||||
if frame_path not in selected_frames and len(selected_frames) < frame_count:
|
||||
selected_frames.append(frame_path)
|
||||
|
||||
existing_frames = selected_frames
|
||||
|
||||
# Debug logging to show selected frames
|
||||
if existing_frames:
|
||||
selected_frame_numbers = []
|
||||
for frame_path in existing_frames:
|
||||
# Extract frame number for logging
|
||||
basename = os.path.basename(frame_path)
|
||||
match = re.search(r'(\d{3,4})', basename)
|
||||
if match:
|
||||
selected_frame_numbers.append(int(match.group(1)))
|
||||
|
||||
debug_log(logger, "info", f"Final selection: {len(existing_frames)} frames",
|
||||
f"Selected {len(existing_frames)} frames with numbers: {sorted(selected_frame_numbers)}")
|
||||
else:
|
||||
debug_log(logger, "warning", "No frames selected from sequence",
|
||||
"Frame selection resulted in empty list")
|
||||
else:
|
||||
# Fallback to generating frame paths using improved system
|
||||
try:
|
||||
frame_paths = SequenceHandler.generate_frame_paths(pattern_path, start_frame, frame_count, frame_step)
|
||||
except AttributeError:
|
||||
# Ultimate fallback if SequenceHandler not available
|
||||
frame_paths = []
|
||||
for i in range(0, frame_count * frame_step, frame_step):
|
||||
frame_number = start_frame + i
|
||||
frame_path = pattern_path.replace('####', f'{frame_number:04d}')
|
||||
frame_paths.append(frame_path)
|
||||
|
||||
# Check if files exist
|
||||
existing_frames = []
|
||||
for frame_path in frame_paths:
|
||||
if os.path.exists(frame_path):
|
||||
existing_frames.append(frame_path)
|
||||
else:
|
||||
debug_log(logger, "warning", "Frame not found", f"Frame not found: {frame_path}")
|
||||
|
||||
# Debug logging to show generated and found frames
|
||||
if existing_frames:
|
||||
generated_frame_numbers = []
|
||||
for frame_path in frame_paths:
|
||||
basename = os.path.basename(frame_path)
|
||||
match = re.search(r'(\d{3,4})', basename)
|
||||
if match:
|
||||
generated_frame_numbers.append(int(match.group(1)))
|
||||
|
||||
found_frame_numbers = []
|
||||
for frame_path in existing_frames:
|
||||
basename = os.path.basename(frame_path)
|
||||
match = re.search(r'(\d{3,4})', basename)
|
||||
if match:
|
||||
found_frame_numbers.append(int(match.group(1)))
|
||||
|
||||
debug_log(logger, "info", f"Generated {len(frame_paths)} paths, found {len(existing_frames)} files",
|
||||
f"Generated frames: {sorted(generated_frame_numbers)}, Found frames: {sorted(found_frame_numbers)}")
|
||||
else:
|
||||
debug_log(logger, "warning", f"Generated {len(frame_paths)} paths but no files exist",
|
||||
f"Generated frame paths but none of the files exist on disk")
|
||||
|
||||
if not existing_frames:
|
||||
# Try to provide helpful information about what files were found
|
||||
directory = os.path.dirname(pattern_path) if pattern_path else "."
|
||||
pattern_filename = os.path.basename(pattern_path) if pattern_path else ""
|
||||
|
||||
debug_log(logger, "error", "No sequence frames found",
|
||||
f"No sequence frames found for pattern: {pattern_path}")
|
||||
|
||||
if os.path.exists(directory):
|
||||
available_files = [f for f in os.listdir(directory) if f.endswith('.exr')]
|
||||
debug_log(logger, "info", f"Directory scan: {len(available_files)} EXR files found",
|
||||
f"Available EXR files in directory '{directory}': {available_files[:10]}")
|
||||
|
||||
# If there are EXR files, show pattern matching hints
|
||||
if available_files:
|
||||
debug_log(logger, "info", "Pattern matching hints",
|
||||
f"Pattern '{pattern_filename}' expected in directory. " +
|
||||
f"Consider checking: 1) Path separators (/ vs \\), " +
|
||||
f"2) Frame numbering format (should be 4 digits like 0001), " +
|
||||
f"3) File naming convention matches exactly")
|
||||
else:
|
||||
debug_log(logger, "error", f"Directory does not exist: {directory}",
|
||||
f"The directory '{directory}' from pattern '{pattern_path}' does not exist")
|
||||
|
||||
if not existing_sequence_files:
|
||||
logger.error(f"No sequence files found for pattern: {pattern_path}")
|
||||
logger.error(f"Pattern path: {pattern_path}")
|
||||
logger.error(f"Expected frame range: {start_frame} to {end_frame} step {frame_step}")
|
||||
raise FileNotFoundError(f"No sequence frames found for pattern: {pattern_path}")
|
||||
|
||||
# Enforce frame count limit - ensure we don't load more than requested
|
||||
if len(existing_frames) > frame_count:
|
||||
existing_frames = existing_frames[:frame_count]
|
||||
debug_log(logger, "info", f"Limited to requested frame count: {frame_count}",
|
||||
f"Limited sequence to {frame_count} frames as requested by user")
|
||||
# Extract frame numbers
|
||||
try:
|
||||
frame_info = SequenceHandler.extract_frame_numbers(existing_sequence_files)
|
||||
except (AttributeError, NotImplementedError) as e:
|
||||
logger.error(f"SequenceHandler.extract_frame_numbers not available: {str(e)}")
|
||||
raise ImportError("Frame number extraction utilities not available")
|
||||
|
||||
# Load first frame to establish structure
|
||||
first_frame_result = self._load_single_image(existing_frames[0], normalize, node_id, layer_data)
|
||||
# Select frames based on parameters
|
||||
try:
|
||||
selected_frames = SequenceHandler.select_sequence_frames(
|
||||
frame_info, start_frame, end_frame, frame_step
|
||||
)
|
||||
except (AttributeError, NotImplementedError) as e:
|
||||
logger.error(f"SequenceHandler.select_sequence_frames not available: {str(e)}")
|
||||
raise ImportError("Frame selection utilities not available")
|
||||
|
||||
if not selected_frames:
|
||||
logger.error(f"No frames selected from sequence")
|
||||
logger.error(f"Available frames: {len(existing_sequence_files)}")
|
||||
logger.error(f"Frame range requested: {start_frame} to {end_frame} step {frame_step}")
|
||||
if frame_info:
|
||||
available_frame_numbers = [frame_num for frame_num, _ in frame_info]
|
||||
logger.error(f"Available frame numbers: {sorted(available_frame_numbers)}")
|
||||
raise ValueError(f"No frames selected from sequence for range {start_frame}-{end_frame} step {frame_step}")
|
||||
|
||||
debug_log(logger, "info", f"Selected {len(selected_frames)} frames for loading",
|
||||
f"Loading {len(selected_frames)} frames from sequence")
|
||||
|
||||
# Find first valid frame to establish structure
|
||||
first_valid_frame = None
|
||||
first_frame_index = 0
|
||||
for i, frame_path in enumerate(selected_frames):
|
||||
if frame_path is not None:
|
||||
first_valid_frame = frame_path
|
||||
first_frame_index = i
|
||||
break
|
||||
|
||||
if first_valid_frame is None:
|
||||
raise ValueError(f"No valid frames found in sequence range {start_frame}-{end_frame} step {frame_step}")
|
||||
|
||||
# Load first valid frame to establish structure
|
||||
try:
|
||||
first_frame_result = self._load_single_image(first_valid_frame, normalize, node_id, layer_data)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load first frame: {first_valid_frame}")
|
||||
logger.error(f"Error: {str(e)}")
|
||||
raise
|
||||
|
||||
# Handle result format (dict if preview generated, list if not)
|
||||
if isinstance(first_frame_result, dict):
|
||||
@@ -366,26 +240,26 @@ class load_exr:
|
||||
first_frame_data = first_frame_result
|
||||
|
||||
# Initialize batch tensors with first frame
|
||||
batch_rgb = first_frame_data[0] # IMAGE
|
||||
batch_alpha = first_frame_data[1] # MASK
|
||||
batch_layers = first_frame_data[3] # LAYERS
|
||||
batch_cryptomatte = first_frame_data[4] # CRYPTOMATTE
|
||||
|
||||
# Convert single frame tensors to batch tensors
|
||||
batch_rgb_list = [batch_rgb]
|
||||
batch_alpha_list = [batch_alpha]
|
||||
batch_rgb_list = [first_frame_data[0]]
|
||||
batch_alpha_list = [first_frame_data[1]]
|
||||
batch_layers_dict = {}
|
||||
batch_cryptomatte_dict = {}
|
||||
|
||||
# Initialize layer dictionaries
|
||||
for layer_name, layer_tensor in batch_layers.items():
|
||||
for layer_name, layer_tensor in first_frame_data[3].items():
|
||||
batch_layers_dict[layer_name] = [layer_tensor]
|
||||
|
||||
for crypto_name, crypto_tensor in batch_cryptomatte.items():
|
||||
for crypto_name, crypto_tensor in first_frame_data[4].items():
|
||||
batch_cryptomatte_dict[crypto_name] = [crypto_tensor]
|
||||
|
||||
# Load remaining frames
|
||||
for frame_path in existing_frames[1:]:
|
||||
# Load remaining frames (skip the first valid frame we already loaded)
|
||||
for i, frame_path in enumerate(selected_frames):
|
||||
# Skip the first valid frame (already loaded) and None paths
|
||||
if i == first_frame_index or frame_path is None:
|
||||
if frame_path is None:
|
||||
logger.warning(f"Skipping missing frame {i+1}/{len(selected_frames)}: frame not found in sequence")
|
||||
continue
|
||||
|
||||
try:
|
||||
frame_result = self._load_single_image(frame_path, normalize, node_id, layer_data)
|
||||
|
||||
@@ -400,66 +274,57 @@ class load_exr:
|
||||
batch_alpha_list.append(frame_data[1])
|
||||
|
||||
# Add layers to batch
|
||||
frame_layers = frame_data[3]
|
||||
for layer_name, layer_tensor in frame_layers.items():
|
||||
for layer_name, layer_tensor in frame_data[3].items():
|
||||
if layer_name in batch_layers_dict:
|
||||
batch_layers_dict[layer_name].append(layer_tensor)
|
||||
else:
|
||||
debug_log(logger, "warning", "Layer not found in first frame",
|
||||
f"Layer '{layer_name}' not found in first frame, skipping")
|
||||
logger.error(f"Layer '{layer_name}' not found in first frame, cannot batch")
|
||||
|
||||
# Add cryptomatte to batch
|
||||
frame_cryptomatte = frame_data[4]
|
||||
for crypto_name, crypto_tensor in frame_cryptomatte.items():
|
||||
for crypto_name, crypto_tensor in frame_data[4].items():
|
||||
if crypto_name in batch_cryptomatte_dict:
|
||||
batch_cryptomatte_dict[crypto_name].append(crypto_tensor)
|
||||
else:
|
||||
debug_log(logger, "warning", "Cryptomatte not found in first frame",
|
||||
f"Cryptomatte '{crypto_name}' not found in first frame, skipping")
|
||||
logger.error(f"Cryptomatte '{crypto_name}' not found in first frame, cannot batch")
|
||||
|
||||
except Exception as e:
|
||||
debug_log(logger, "error", "Error loading frame", f"Error loading frame {frame_path}: {str(e)}")
|
||||
# Continue with other frames
|
||||
continue
|
||||
logger.error(f"Failed to load frame {i+1}/{len(selected_frames)}: {frame_path}")
|
||||
logger.error(f"Error: {str(e)}")
|
||||
raise
|
||||
|
||||
# Stack tensors into batches - ensure proper [B, H, W, C] format for ComfyUI
|
||||
final_rgb = torch.cat(batch_rgb_list, dim=0) # Shape: [B, H, W, 3]
|
||||
final_alpha = torch.cat(batch_alpha_list, dim=0) # Shape: [B, H, W]
|
||||
# Stack tensors into batches
|
||||
final_rgb = torch.cat(batch_rgb_list, dim=0)
|
||||
final_alpha = torch.cat(batch_alpha_list, dim=0)
|
||||
|
||||
# Stack layer tensors - maintain proper batch dimensions
|
||||
# Stack layer tensors
|
||||
final_layers = {}
|
||||
for layer_name, tensor_list in batch_layers_dict.items():
|
||||
if tensor_list:
|
||||
stacked_tensor = torch.cat(tensor_list, dim=0)
|
||||
final_layers[layer_name] = stacked_tensor
|
||||
debug_log(logger, "info", f"Layer {layer_name}: {format_tensor_info(stacked_tensor.shape, stacked_tensor.dtype)}",
|
||||
f"Batched layer '{layer_name}': shape={stacked_tensor.shape}, dtype={stacked_tensor.dtype}")
|
||||
final_layers[layer_name] = torch.cat(tensor_list, dim=0)
|
||||
|
||||
# Stack cryptomatte tensors - maintain proper batch dimensions
|
||||
# Stack cryptomatte tensors
|
||||
final_cryptomatte = {}
|
||||
for crypto_name, tensor_list in batch_cryptomatte_dict.items():
|
||||
if tensor_list:
|
||||
stacked_tensor = torch.cat(tensor_list, dim=0)
|
||||
final_cryptomatte[crypto_name] = stacked_tensor
|
||||
debug_log(logger, "info", f"Cryptomatte {crypto_name}: {format_tensor_info(stacked_tensor.shape, stacked_tensor.dtype)}",
|
||||
f"Batched cryptomatte '{crypto_name}': shape={stacked_tensor.shape}, dtype={stacked_tensor.dtype}")
|
||||
final_cryptomatte[crypto_name] = torch.cat(tensor_list, dim=0)
|
||||
|
||||
# Update metadata to include sequence info
|
||||
# Update metadata
|
||||
metadata_str = first_frame_data[2]
|
||||
metadata = json.loads(metadata_str) if metadata_str else {}
|
||||
metadata["sequence_info"] = {
|
||||
"is_sequence": True,
|
||||
"pattern": pattern_path,
|
||||
"start_frame": start_frame,
|
||||
"frame_count": len(existing_frames),
|
||||
"end_frame": end_frame,
|
||||
"frame_step": frame_step,
|
||||
"loaded_frames": existing_frames
|
||||
"total_frames": len(selected_frames),
|
||||
"loaded_frames": selected_frames
|
||||
}
|
||||
metadata_json = json.dumps(metadata)
|
||||
|
||||
# Log final batch information
|
||||
debug_log(logger, "info", f"Sequence loaded: {len(existing_frames)} frames, RGB shape: {format_tensor_info(final_rgb.shape, final_rgb.dtype)}",
|
||||
f"Successfully loaded sequence: {len(existing_frames)} frames, RGB batch shape: {final_rgb.shape}, "
|
||||
debug_log(logger, "info", f"Sequence loaded: {len(selected_frames)} frames, RGB shape: {format_tensor_info(final_rgb.shape, final_rgb.dtype)}",
|
||||
f"Successfully loaded sequence: {len(selected_frames)} frames, RGB batch shape: {final_rgb.shape}, "
|
||||
f"Alpha batch shape: {final_alpha.shape}, {len(final_layers)} layer types, {len(final_cryptomatte)} cryptomatte types")
|
||||
|
||||
# Generate preview for sequence (first frame) at full resolution
|
||||
@@ -484,6 +349,10 @@ class load_exr:
|
||||
|
||||
def _load_single_image(self, image_path: str, normalize: bool, node_id: str = None, layer_data: Dict = None) -> List:
|
||||
"""Load a single EXR image (original functionality)"""
|
||||
# Validate image_path is not None
|
||||
if image_path is None:
|
||||
raise ValueError("image_path cannot be None. This may indicate a missing frame in the sequence.")
|
||||
|
||||
try:
|
||||
# Scan the EXR metadata if not already provided
|
||||
metadata = layer_data if layer_data else self.scan_exr_metadata(image_path)
|
||||
@@ -1309,10 +1178,10 @@ class load_exr:
|
||||
|
||||
# Removed old preview generation method - now using modular system
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"load_exr": load_exr
|
||||
}
|
||||
# NODE_CLASS_MAPPINGS = {
|
||||
# "load_exr": load_exr
|
||||
# }
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"load_exr": "Load EXR"
|
||||
}
|
||||
# NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
# "load_exr": "Load EXR"
|
||||
# }
|
||||
|
||||
@@ -18,7 +18,7 @@ except ImportError:
|
||||
return f"{name} shape={tensor_shape}" if name else f"shape={tensor_shape}"
|
||||
DEBUG_MODE = "simple"
|
||||
|
||||
class load_exr_layer_by_name:
|
||||
class LoadExrLayerByName:
|
||||
"""
|
||||
The Load EXR Layer by Name node allows selecting a specific layer from an EXR layer dictionary.
|
||||
It works like Nuke's Shuffle node, allowing users to pick a specific layer to output.
|
||||
@@ -232,7 +232,7 @@ class load_exr_layer_by_name:
|
||||
return [image_output, mask_output]
|
||||
|
||||
# Define a copy of the main class for cryptomatte layers
|
||||
class shamble_cryptomatte(load_exr_layer_by_name):
|
||||
class CryptomatteLayer(LoadExrLayerByName):
|
||||
"""
|
||||
The Cryptomatte Shamble node allows selecting a specific cryptomatte layer from an EXR dictionary.
|
||||
It is identical to the Load EXR Layer by Name node but filters for cryptomatte layers only.
|
||||
@@ -325,12 +325,12 @@ class shamble_cryptomatte(load_exr_layer_by_name):
|
||||
# Return the requested cryptomatte layer
|
||||
return [cryptomatte[layer_name]]
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"load_exr_layer_by_name": load_exr_layer_by_name,
|
||||
"shamble_cryptomatte": shamble_cryptomatte
|
||||
}
|
||||
# NODE_CLASS_MAPPINGS = {
|
||||
# "load_exr_layer_by_name": load_exr_layer_by_name,
|
||||
# "shamble_cryptomatte": shamble_cryptomatte
|
||||
# }
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"load_exr_layer_by_name": "Load EXR Layer by Name",
|
||||
"shamble_cryptomatte": "Cryptomatte Layer"
|
||||
}
|
||||
# NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
# "load_exr_layer_by_name": "Load EXR Layer by Name",
|
||||
# "shamble_cryptomatte": "Cryptomatte Layer"
|
||||
# }
|
||||
|
||||
+7
-7
@@ -40,7 +40,7 @@ except ImportError:
|
||||
@staticmethod
|
||||
def create_versioning_widgets(): return {"versioning": [["version", "INT", {"default": 1}]]}
|
||||
|
||||
class saver:
|
||||
class SaverNode:
|
||||
"""Optimized image saver node with consistent bit depth handling"""
|
||||
|
||||
# Format specifications
|
||||
@@ -390,10 +390,10 @@ class saver:
|
||||
debug_log(logger, "error", "Save operation failed", f"Saver error: {str(e)}")
|
||||
raise RuntimeError(f"Saver error: {str(e)}") from e
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"saver": saver,
|
||||
}
|
||||
# NODE_CLASS_MAPPINGS = {
|
||||
# "saver": saver,
|
||||
# }
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"saver": "Image Saver"
|
||||
}
|
||||
# NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
# "saver": "Image Saver"
|
||||
# }
|
||||
|
||||
@@ -20,7 +20,7 @@ except ImportError:
|
||||
def log_batch_processing(tensor, operation, name="tensor"):
|
||||
pass
|
||||
|
||||
class znormalize:
|
||||
class ZNormalizeNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -109,10 +109,10 @@ class znormalize:
|
||||
raise
|
||||
|
||||
# Node registration
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"znormalize": znormalize
|
||||
}
|
||||
# NODE_CLASS_MAPPINGS = {
|
||||
# "znormalize": znormalize
|
||||
# }
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"znormalize": "Z Normalize"
|
||||
}
|
||||
# NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
# "znormalize": "Z Normalize"
|
||||
# }
|
||||
+40
-43
@@ -138,81 +138,78 @@ class SequenceHandler:
|
||||
return frame_info
|
||||
|
||||
@staticmethod
|
||||
def generate_frame_paths(pattern_path: str, start_frame: int, frame_count: int, frame_step: int) -> List[str]:
|
||||
def generate_frame_paths(pattern_path: str, start_frame: int, end_frame: int, frame_step: int) -> List[str]:
|
||||
"""Generate list of frame paths based on pattern and parameters using improved regex approach"""
|
||||
frame_paths = []
|
||||
|
||||
for i in range(0, frame_count * frame_step, frame_step):
|
||||
frame_number = start_frame + i
|
||||
frame_path = SequenceHandler.replace_frame_number(pattern_path, frame_number)
|
||||
current_frame = start_frame
|
||||
while current_frame <= end_frame:
|
||||
frame_path = SequenceHandler.replace_frame_number(pattern_path, current_frame)
|
||||
frame_paths.append(frame_path)
|
||||
current_frame += frame_step
|
||||
|
||||
return frame_paths
|
||||
|
||||
@staticmethod
|
||||
def select_sequence_frames(available_frames: List[Tuple[int, str]], start_frame: int,
|
||||
frame_count: int, frame_step: int, strict_count: bool = True) -> List[str]:
|
||||
end_frame: int, frame_step: int) -> List[str]:
|
||||
"""
|
||||
Select specific frames from available sequence based on parameters
|
||||
|
||||
Args:
|
||||
available_frames: List of (frame_number, file_path) tuples
|
||||
start_frame: Starting frame number
|
||||
frame_count: Number of frames to select
|
||||
end_frame: Ending frame number
|
||||
frame_step: Step between frames
|
||||
strict_count: If True, return exactly frame_count frames (pad if needed)
|
||||
If False, return what's available up to frame_count
|
||||
|
||||
Returns:
|
||||
List of selected file paths
|
||||
List of selected file paths (strict selection - no fallbacks)
|
||||
"""
|
||||
selected_frames = []
|
||||
|
||||
# Method 1: Try to match exact frame numbers
|
||||
for i in range(0, frame_count * frame_step, frame_step):
|
||||
target_frame = start_frame + i
|
||||
# Generate expected frame numbers and find exact matches
|
||||
current_frame = start_frame
|
||||
while current_frame <= end_frame:
|
||||
# Find the exact matching frame
|
||||
found = False
|
||||
for frame_num, file_path in available_frames:
|
||||
if frame_num == target_frame:
|
||||
if frame_num == current_frame:
|
||||
selected_frames.append(file_path)
|
||||
found = True
|
||||
break
|
||||
|
||||
# Method 2: If we don't have enough exact matches and strict_count is False
|
||||
if len(selected_frames) < frame_count and not strict_count:
|
||||
# Fill in with available frames, respecting start_frame and step as much as possible
|
||||
available_paths = [fp for fn, fp in available_frames if fn >= start_frame]
|
||||
|
||||
# Take every nth frame from available, where n approximates the desired step
|
||||
if available_paths and frame_step > 1:
|
||||
step_adjusted = max(1, frame_step)
|
||||
additional_frames = available_paths[::step_adjusted]
|
||||
|
||||
# Add frames we don't already have
|
||||
for frame_path in additional_frames:
|
||||
if frame_path not in selected_frames and len(selected_frames) < frame_count:
|
||||
selected_frames.append(frame_path)
|
||||
# If frame not found, append None to maintain sequence positions
|
||||
if not found:
|
||||
selected_frames.append(None)
|
||||
|
||||
# If still not enough, just take sequential frames
|
||||
if len(selected_frames) < frame_count:
|
||||
for frame_path in available_paths:
|
||||
if frame_path not in selected_frames and len(selected_frames) < frame_count:
|
||||
selected_frames.append(frame_path)
|
||||
current_frame += frame_step
|
||||
|
||||
debug_log(logger, "info", f"Selected {len(selected_frames)} frames",
|
||||
f"Selected {len(selected_frames)} frames from {len(available_frames)} available " +
|
||||
f"(start={start_frame}, count={frame_count}, step={frame_step})")
|
||||
expected_count = len(range(start_frame, end_frame + 1, frame_step))
|
||||
debug_log(logger, "info", f"Selected {len([f for f in selected_frames if f is not None])} of {expected_count} frames",
|
||||
f"Selected {len([f for f in selected_frames if f is not None])} frames from {len(available_frames)} available " +
|
||||
f"(start={start_frame}, end={end_frame}, step={frame_step})")
|
||||
|
||||
return selected_frames
|
||||
|
||||
@staticmethod
|
||||
def validate_sequence_parameters(start_frame: int, frame_count: int, frame_step: int) -> Tuple[int, int, int]:
|
||||
def validate_sequence_parameters(start_frame: int, end_frame: int, frame_step: int) -> Tuple[int, int, int]:
|
||||
"""Validate and sanitize sequence parameters"""
|
||||
# Ensure positive values
|
||||
start_frame = max(0, start_frame if start_frame is not None else 1)
|
||||
frame_count = max(1, frame_count if frame_count is not None else 10)
|
||||
frame_step = max(1, frame_step if frame_step is not None else 1)
|
||||
# Ensure valid values - no fallback defaults
|
||||
if start_frame is None:
|
||||
raise ValueError("start_frame parameter is required and cannot be None. " +
|
||||
"Make sure the node is in 'sequence' mode and the sequence widgets are visible.")
|
||||
if end_frame is None:
|
||||
raise ValueError("end_frame parameter is required and cannot be None. " +
|
||||
"Make sure the node is in 'sequence' mode and the sequence widgets are visible.")
|
||||
if frame_step is None:
|
||||
raise ValueError("frame_step parameter is required and cannot be None. " +
|
||||
"Make sure the node is in 'sequence' mode and the sequence widgets are visible.")
|
||||
|
||||
start_frame = max(0, start_frame)
|
||||
end_frame = max(start_frame, end_frame)
|
||||
frame_step = max(1, frame_step)
|
||||
|
||||
return start_frame, frame_count, frame_step
|
||||
return start_frame, end_frame, frame_step
|
||||
|
||||
@staticmethod
|
||||
def get_sequence_info(pattern_path: str) -> Dict:
|
||||
@@ -240,12 +237,12 @@ class DynamicUIHelper:
|
||||
"""Helper for creating dynamic UI widgets in ComfyUI nodes"""
|
||||
|
||||
@staticmethod
|
||||
def create_sequence_widgets(default_start: int = 1, default_count: int = 10, default_step: int = 1) -> Dict:
|
||||
def create_sequence_widgets(default_start: int = 1, default_end: int = 100, default_step: int = 1) -> Dict:
|
||||
"""Create standard sequence control widgets"""
|
||||
return {
|
||||
"sequence": [
|
||||
["start_frame", "INT", {"default": default_start, "min": 0, "max": 999999}],
|
||||
["frame_count", "INT", {"default": default_count, "min": 1, "max": 1000}],
|
||||
["end_frame", "INT", {"default": default_end, "min": 1, "max": 999999}],
|
||||
["frame_step", "INT", {"default": default_step, "min": 1, "max": 100}]
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user