Files
Willie G de60953950 Update geekyremb_core.py
Last update broke using single images for the background when using video input for the foreground without adding animation effects. Corrected it and also improved frame composition.
2025-03-15 00:04:30 -04:00

848 lines
44 KiB
Python

import numpy as np
import torch
import cv2
from PIL import Image, ImageOps
from rembg import remove, new_session
from dataclasses import dataclass
from typing import List, Tuple, Optional, Dict, Callable
from threading import Lock
from concurrent.futures import ThreadPoolExecutor
from tqdm import tqdm
import os
import gc
import logging
# Import utilities
from .utils.image_utils import tensor2pil, pil2tensor, debug_tensor_info, parse_aspect_ratio
from .utils.cache import LRUCache
from .utils.mask_processing import EnhancedMaskProcessor, MaskProcessingConfig, remove_background_chroma, ChromaKeyConfig
from .utils.blend_modes import EnhancedBlendMode
from .utils.animation import EnhancedAnimator, AnimationType, EASING_FUNCTIONS
from .utils.light_shadow_util import LightShadowProcessor, LightingEffectConfig, ShadowEffectConfig
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@dataclass
class BackgroundRemovalConfig:
"""Configuration for background removal processing"""
enable_background_removal: bool = True
removal_method: str = "rembg" # "rembg" or "chroma_key"
model: str = "u2net"
alpha_matting: bool = False
alpha_matting_foreground_threshold: int = 240
alpha_matting_background_threshold: int = 10
# For chroma key
chroma_key_color: str = "green"
chroma_key_tolerance: float = 0.1
spill_reduction: float = 0.0
edge_refinement: bool = False
# For mask processing
mask_expansion: int = 0
edge_detection: bool = False
edge_thickness: int = 1
edge_color: Tuple[int, int, int, int] = (0, 0, 0, 255)
mask_blur: int = 5
threshold: float = 0.5
invert_generated_mask: bool = False
remove_small_regions: bool = False
small_region_size: int = 100
class GeekyRemB:
"""Core background removal node with advanced mask processing"""
def __init__(self):
self.session = None
self.session_lock = Lock()
self.use_gpu = torch.cuda.is_available()
self.config = BackgroundRemovalConfig()
self.mask_processor = EnhancedMaskProcessor()
self.light_config = LightingEffectConfig(enable_lighting=False) # Disabled by default
self.shadow_config = ShadowEffectConfig(enable_shadow=False) # Disabled by default
# Thread pool for parallel processing
cpu_cores = os.cpu_count() or 4
self.max_workers = min(cpu_cores, 8)
self.executor = ThreadPoolExecutor(max_workers=self.max_workers)
# Cache for processed frames
self.frame_cache = LRUCache(maxsize=100)
def cleanup_resources(self):
"""Clean up resources"""
try:
if self.session is not None:
self.session = None
if self.executor is not None:
try:
self.executor.shutdown(wait=False)
except Exception as e:
logger.error(f"Error shutting down executor: {str(e)}")
self.executor = None
if self.frame_cache is not None:
try:
self.frame_cache.clear()
except Exception as e:
logger.error(f"Error clearing cache: {str(e)}")
self.frame_cache = None
# Force garbage collection
gc.collect()
if self.use_gpu:
torch.cuda.empty_cache()
except Exception as e:
logger.error(f"Error during cleanup: {str(e)}")
def __del__(self):
"""Ensure proper cleanup on deletion"""
try:
self.cleanup_resources()
except:
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"output_format": (["RGBA", "RGB"],),
"foreground": ("IMAGE",),
"enable_background_removal": ("BOOLEAN", {"default": True}),
"removal_method": (["rembg", "chroma_key"],),
"model": ([
"u2net", "u2netp", "u2net_human_seg", "u2net_cloth_seg",
"silueta", "isnet-general-use", "isnet-anime"
],),
"chroma_key_color": (["green", "blue", "red"],),
"chroma_key_tolerance": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step": 0.01}),
"spill_reduction": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"edge_refinement": ("BOOLEAN", {"default": False}),
"mask_expansion": ("INT", {"default": 0, "min": -100, "max": 100, "step": 1}),
"edge_detection": ("BOOLEAN", {"default": False}),
"edge_thickness": ("INT", {"default": 1, "min": 1, "max": 10, "step": 1}),
"mask_blur": ("INT", {"default": 5, "min": 0, "max": 100, "step": 1}),
"threshold": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
"invert_generated_mask": ("BOOLEAN", {"default": False}),
"remove_small_regions": ("BOOLEAN", {"default": False}),
"small_region_size": ("INT", {"default": 100, "min": 1, "max": 1000, "step": 1}),
"alpha_matting": ("BOOLEAN", {"default": False}),
"alpha_matting_foreground_threshold": ("INT", {"default": 240, "min": 0, "max": 255, "step": 1}),
"alpha_matting_background_threshold": ("INT", {"default": 10, "min": 0, "max": 255, "step": 1}),
"aspect_ratio": ("STRING", {
"default": "",
"placeholder": "e.g., 16:9, 4:3, 1:1, portrait, landscape"
}),
"scale": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 5.0, "step": 0.1}),
"frames": ("INT", {"default": 1, "min": 1, "max": 1000, "step": 1}),
"x_position": ("INT", {"default": 0, "min": -2048, "max": 2048, "step": 1}),
"y_position": ("INT", {"default": 0, "min": -2048, "max": 2048, "step": 1}),
},
"optional": {
"background": ("IMAGE",),
"additional_mask": ("MASK",),
"invert_additional_mask": ("BOOLEAN", {"default": False}),
# Update auxiliary node inputs
"animator": ("ANIMATOR",), # Connection for GeekyRemB_Animator
"lightshadow": ("LIGHTSHADOW",), # Connection for GeekyRemB_LightShadow
"keyframe": ("KEYFRAME",), # Direct connection for individual keyframes - replacing keyframe_animator
}
}
RETURN_TYPES = ("IMAGE", "MASK")
FUNCTION = "process_image"
CATEGORY = "image/processing"
DISPLAY_NAME = "Geeky RemB"
def initialize_model(self, model: str) -> None:
"""Thread-safe model initialization"""
with self.session_lock:
if self.session is None or getattr(self.session, 'model_name', None) != model:
providers = ['CUDAExecutionProvider', 'CPUExecutionProvider'] if self.use_gpu else ['CPUExecutionProvider']
try:
self.session = new_session(model, providers=providers)
logger.info(f"Model '{model}' initialized successfully.")
except Exception as e:
logger.error(f"Failed to initialize model '{model}': {str(e)}")
raise RuntimeError(f"Model initialization failed: {str(e)}")
def remove_background_rembg(self, image: Image.Image) -> Tuple[Image.Image, Image.Image]:
"""Enhanced background removal using rembg with alpha matting"""
try:
if image.mode != 'RGBA':
image = image.convert('RGBA')
result = remove(
image,
session=self.session,
alpha_matting=self.config.alpha_matting,
alpha_matting_foreground_threshold=self.config.alpha_matting_foreground_threshold,
alpha_matting_background_threshold=self.config.alpha_matting_background_threshold
)
return result, result.split()[3]
except Exception as e:
logger.error(f"Background removal failed: {str(e)}")
raise RuntimeError(f"Background removal failed: {str(e)}")
def process_frame(self, frame: Image.Image, background_frame: Optional[Image.Image], x_pos: int = 0, y_pos: int = 0) -> Tuple[Image.Image, Image.Image]:
"""Process a single frame for background removal"""
try:
logger.info(f"Processing frame with background: {background_frame is not None}")
if background_frame:
logger.info(f"Background frame size: {background_frame.size}")
logger.info(f"Foreground frame size: {frame.size}")
if isinstance(frame, np.ndarray):
frame = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
# Ensure RGBA mode
frame = frame.convert('RGBA')
if self.config.enable_background_removal:
if self.config.removal_method == "rembg":
frame_with_alpha, mask = self.remove_background_rembg(frame)
frame = frame_with_alpha
else:
# Create chroma key config
chroma_config = ChromaKeyConfig(
chroma_key_color=self.config.chroma_key_color,
chroma_key_tolerance=self.config.chroma_key_tolerance,
spill_reduction=self.config.spill_reduction,
edge_refinement=self.config.edge_refinement
)
# Get mask using chroma key
mask = remove_background_chroma(frame, chroma_config)
# Create mask processing config
mask_config = MaskProcessingConfig(
mask_expansion=self.config.mask_expansion,
edge_detection=self.config.edge_detection,
edge_thickness=self.config.edge_thickness,
edge_color=self.config.edge_color,
mask_blur=self.config.mask_blur,
threshold=self.config.threshold,
invert_generated_mask=self.config.invert_generated_mask,
remove_small_regions=self.config.remove_small_regions,
small_region_size=self.config.small_region_size,
edge_refinement=self.config.edge_refinement
)
# Refine mask
refined_mask = self.mask_processor.refine_mask(mask, mask_config, frame)
# Create new frame with refined alpha
frame_array = np.array(frame)
mask_array = np.array(refined_mask)
# Ensure mask is in correct shape for alpha channel
if len(mask_array.shape) == 2:
mask_array = mask_array[:, :, None]
# Apply mask to RGB channels
frame_array = frame_array * (mask_array / 255.0)
frame_array[:, :, 3] = mask_array[:, :, 0] # Set alpha channel
frame = Image.fromarray(frame_array.astype(np.uint8), 'RGBA')
mask = refined_mask
else:
mask = Image.new('L', frame.size, 255)
# Apply light and shadow effects if enabled
# Apply lighting effect if enabled
if self.light_config.enable_lighting:
frame = LightShadowProcessor.apply_lighting_effect(frame, self.light_config)
# Create shadow if enabled
if self.shadow_config.enable_shadow:
# Calculate shadow offset
shadow_offset = (
self.shadow_config.shadow_direction_x,
self.shadow_config.shadow_direction_y
)
# Create shadow from the frame
shadow = LightShadowProcessor.create_shadow(frame, self.shadow_config)
if shadow:
# Create new canvas with background first if available
if background_frame is not None:
canvas = background_frame.convert('RGBA')
else:
canvas = Image.new('RGBA', frame.size, (0, 0, 0, 0))
# Paste shadow without offset (shadow already contains the offset)
canvas.paste(shadow, (0, 0), shadow)
# Paste original frame on top with position offset
canvas.paste(frame, (x_pos, y_pos), frame)
frame = canvas
# Handle background composition if provided and shadow is not enabled
elif background_frame is not None:
# Make sure the background frame is ready for composition
bg = background_frame.convert('RGBA')
# Create a new canvas with the appropriate size
if bg.size != frame.size:
# If sizes differ, create a canvas with background size
result = Image.new('RGBA', bg.size, (0, 0, 0, 0))
# Place the background first
result.paste(bg, (0, 0))
# Calculate center position if not provided
if x_pos == 0 and y_pos == 0:
x_pos = (bg.width - frame.width) // 2
y_pos = (bg.height - frame.height) // 2
# Place the foreground on top
result.paste(frame, (x_pos, y_pos), frame)
else:
# If sizes match, create a canvas with that size
result = Image.new('RGBA', frame.size, (0, 0, 0, 0))
result.paste(bg, (0, 0))
result.paste(frame, (x_pos, y_pos), frame)
frame = result
return frame, mask
except Exception as e:
logger.error(f"Frame processing failed: {str(e)}")
return frame, Image.new('L', frame.size, 255)
def _process_foreground_only(self, frame: Image.Image) -> Tuple[Image.Image, Image.Image]:
"""Process a single frame for background removal and lighting only, no background compositing"""
try:
if isinstance(frame, np.ndarray):
frame = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
# Ensure RGBA mode
frame = frame.convert('RGBA')
# Handle background removal
if self.config.enable_background_removal:
if self.config.removal_method == "rembg":
frame_with_alpha, mask = self.remove_background_rembg(frame)
frame = frame_with_alpha
else:
# Create chroma key config
chroma_config = ChromaKeyConfig(
chroma_key_color=self.config.chroma_key_color,
chroma_key_tolerance=self.config.chroma_key_tolerance,
spill_reduction=self.config.spill_reduction,
edge_refinement=self.config.edge_refinement
)
# Get mask using chroma key
mask = remove_background_chroma(frame, chroma_config)
# Create mask processing config
mask_config = MaskProcessingConfig(
mask_expansion=self.config.mask_expansion,
edge_detection=self.config.edge_detection,
edge_thickness=self.config.edge_thickness,
edge_color=self.config.edge_color,
mask_blur=self.config.mask_blur,
threshold=self.config.threshold,
invert_generated_mask=self.config.invert_generated_mask,
remove_small_regions=self.config.remove_small_regions,
small_region_size=self.config.small_region_size,
edge_refinement=self.config.edge_refinement
)
# Refine mask
refined_mask = self.mask_processor.refine_mask(mask, mask_config, frame)
# Create new frame with refined alpha
frame_array = np.array(frame)
mask_array = np.array(refined_mask)
# Ensure mask is in correct shape for alpha channel
if len(mask_array.shape) == 2:
mask_array = mask_array[:, :, None]
# Apply mask to RGB channels
frame_array = frame_array * (mask_array / 255.0)
frame_array[:, :, 3] = mask_array[:, :, 0] # Set alpha channel
frame = Image.fromarray(frame_array.astype(np.uint8), 'RGBA')
mask = refined_mask
else:
mask = Image.new('L', frame.size, 255)
# Apply lighting effect if enabled (but not shadow)
if self.light_config.enable_lighting:
frame = LightShadowProcessor.apply_lighting_effect(frame, self.light_config)
# Return processed foreground only (no shadow or background composition)
return frame, mask
except Exception as e:
logger.error(f"Foreground processing failed: {str(e)}")
return frame, Image.new('L', frame.size, 255)
def process_image(self, output_format, foreground, enable_background_removal, removal_method, model,
chroma_key_color, chroma_key_tolerance, spill_reduction, edge_refinement,
mask_expansion, edge_detection, edge_thickness, mask_blur, threshold,
invert_generated_mask, remove_small_regions, small_region_size, alpha_matting,
alpha_matting_foreground_threshold, alpha_matting_background_threshold,
aspect_ratio, scale, frames, x_position, y_position, background=None, additional_mask=None,
invert_additional_mask=False, animator=None, lightshadow=None, keyframe=None):
try:
# Debug logging for input parameters
logger.info(f"Foreground batch size: {foreground.shape[0]}")
if background is not None:
logger.info(f"Background batch size: {background.shape[0]}")
else:
logger.info("No background provided")
# Update configuration
self.config.enable_background_removal = enable_background_removal
self.config.removal_method = removal_method
self.config.model = model
self.config.chroma_key_color = chroma_key_color
self.config.chroma_key_tolerance = chroma_key_tolerance
self.config.spill_reduction = spill_reduction
self.config.edge_refinement = edge_refinement
self.config.mask_expansion = mask_expansion
self.config.edge_detection = edge_detection
self.config.edge_thickness = edge_thickness
self.config.mask_blur = mask_blur
self.config.threshold = threshold
self.config.invert_generated_mask = invert_generated_mask
self.config.remove_small_regions = remove_small_regions
self.config.small_region_size = small_region_size
self.config.alpha_matting = alpha_matting
self.config.alpha_matting_foreground_threshold = alpha_matting_foreground_threshold
self.config.alpha_matting_background_threshold = alpha_matting_background_threshold
debug_tensor_info(foreground, "Input foreground")
# Initialize AI model if needed
if enable_background_removal and removal_method == "rembg":
self.initialize_model(model)
# Check if lightshadow node is connected and initialize configs
if lightshadow is not None:
light_shadow_params = lightshadow.get("effect_params", {})
# Update light config
if "light_config" in light_shadow_params:
self.light_config = LightingEffectConfig(
enable_lighting=light_shadow_params["light_config"]["enable_lighting"],
light_intensity=light_shadow_params["light_config"]["light_intensity"],
light_direction_x=light_shadow_params["light_config"]["light_direction_x"],
light_direction_y=light_shadow_params["light_config"]["light_direction_y"],
light_radius=light_shadow_params["light_config"]["light_radius"],
light_falloff=light_shadow_params["light_config"]["light_falloff"],
light_color=light_shadow_params["light_config"]["light_color"],
light_from_behind=light_shadow_params["light_config"].get("light_from_behind", False),
# Advanced lighting parameters
enable_normal_mapping=light_shadow_params["light_config"].get("enable_normal_mapping", False),
enable_specular=light_shadow_params["light_config"].get("enable_specular", False),
specular_intensity=light_shadow_params["light_config"].get("specular_intensity", 0.3),
specular_shininess=light_shadow_params["light_config"].get("specular_shininess", 32),
ambient_light=light_shadow_params["light_config"].get("ambient_light", 0.2),
light_source_height=light_shadow_params["light_config"].get("light_source_height", 200),
kelvin_temperature=light_shadow_params["light_config"].get("kelvin_temperature", 0)
)
# Update shadow config
if "shadow_config" in light_shadow_params:
self.shadow_config = ShadowEffectConfig(
enable_shadow=light_shadow_params["shadow_config"]["enable_shadow"],
shadow_opacity=light_shadow_params["shadow_config"]["shadow_opacity"],
shadow_blur=light_shadow_params["shadow_config"]["shadow_blur"],
shadow_direction_x=light_shadow_params["shadow_config"]["shadow_direction_x"],
shadow_direction_y=light_shadow_params["shadow_config"]["shadow_direction_y"],
shadow_color=light_shadow_params["shadow_config"]["shadow_color"],
shadow_expansion=light_shadow_params["shadow_config"]["shadow_expansion"],
# Advanced shadow parameters
perspective_shadow=light_shadow_params["shadow_config"].get("perspective_shadow", False),
light_source_height=light_shadow_params["shadow_config"].get("light_source_height", 200),
distance_fade=light_shadow_params["shadow_config"].get("distance_fade", False),
fade_distance=light_shadow_params["shadow_config"].get("fade_distance", 100),
soft_edges=light_shadow_params["shadow_config"].get("soft_edges", True)
)
else:
# Disable light and shadow effects if node is not connected
self.light_config.enable_lighting = False
self.shadow_config.enable_shadow = False
# Convert inputs to PIL images
fg_frames = [tensor2pil(foreground[i]) for i in range(foreground.shape[0])]
bg_frames = [tensor2pil(background[i]) for i in range(background.shape[0])] if background is not None else None
# After converting to PIL images - debug logging
logger.info(f"Number of foreground frames: {len(fg_frames)}")
if bg_frames:
logger.info(f"Number of background frames: {len(bg_frames)}")
logger.info(f"First background frame size: {bg_frames[0].size}")
logger.info(f"First foreground frame size: {fg_frames[0].size}")
# Parse and apply aspect ratio if specified
aspect_ratio_value = parse_aspect_ratio(aspect_ratio)
if aspect_ratio_value is not None:
for i in range(len(fg_frames)):
new_width = int(fg_frames[i].width * scale)
new_height = int(new_width / aspect_ratio_value)
fg_frames[i] = fg_frames[i].resize((new_width, new_height), Image.LANCZOS)
# Check if animation is enabled through animator
animation_enabled = animator is not None
# If direct keyframe is provided but no animator, log warning
if keyframe is not None and not animation_enabled:
logger.warning("Keyframe provided but no animator is connected. Keyframe will be ignored.")
# If animation is enabled, create multiple frames
if animation_enabled:
# Extract animation parameters
animation_params = animator.get("animation_params", {})
# Check if this is keyframe animation
animation_type_value = animation_params.get("animation_type", "none")
is_keyframe_animation = animation_type_value == "keyframe"
logger.info(f"Animation type: {animation_type_value}, Is keyframe animation: {is_keyframe_animation}")
if is_keyframe_animation:
# Extract keyframe specific parameters
keyframes = animation_params.get("keyframes", [])
fps = animation_params.get("fps", 30)
animation_frames = animation_params.get("total_frames", frames) # Use total_frames directly from keyframe params
easing_function = animation_params.get("default_easing", "linear")
# Log keyframe details
logger.info(f"Using keyframe animation with {len(keyframes)} keyframes for {animation_frames} total frames")
for i, kf in enumerate(keyframes):
kf_frame = kf.get("frame", 0)
kf_pos = kf.get("position", (0, 0))
kf_scale = kf.get("scale", 1.0)
kf_rot = kf.get("rotation", 0.0)
logger.info(f"Keyframe {i}: frame={kf_frame}, position={kf_pos}, scale={kf_scale}, rotation={kf_rot}")
# Get easing function
easing_func = EASING_FUNCTIONS.get(easing_function, lambda t: t)
# Set placeholder values that will be overridden by keyframe interpolation
animation_speed = 1.0
repeats = 1
reverse = False
delay = 0.0
x_pos = animation_params.get("base_position", (x_position, y_position))[0]
y_pos = animation_params.get("base_position", (x_position, y_position))[1]
animation_scale = animation_params.get("base_scale", scale)
rotation = animation_params.get("base_rotation", 0.0)
steps = 1
phase_shift = 0.0
# Setup keyframe parameters
keyframe_parameters = animation_params
else:
# Standard animation parameters
animation_speed = animation_params.get("animation_speed", 1.0)
frame_count = animation_params.get("frame_count", frames) # Get frame_count directly
animation_frames = frame_count # Use frame_count as animation_frames
repeats = animation_params.get("repeats", 1)
reverse = animation_params.get("reverse", False)
easing_function = animation_params.get("easing_function", "linear")
delay = animation_params.get("delay", 0.0)
x_pos = animation_params.get("x_position", x_position)
y_pos = animation_params.get("y_position", y_position)
animation_scale = animation_params.get("scale", 1.0)
rotation = animation_params.get("rotation", 0.0)
steps = animation_params.get("steps", 1)
phase_shift = animation_params.get("phase_shift", 0.0)
# Get easing function
easing_func = EASING_FUNCTIONS.get(easing_function, lambda t: t)
# No keyframe parameters for standard animation
keyframe_parameters = None
# Process frames with animation
processed_frames = []
masks = []
# Handle background frames for animation
if bg_frames:
if len(bg_frames) == 1:
# If only one background frame, replicate it for all frames
bg_frames = [bg_frames[0]] * animation_frames
elif len(bg_frames) < animation_frames:
# If not enough background frames, extend by repeating
bg_frames = bg_frames * (animation_frames // len(bg_frames) + 1)
bg_frames = bg_frames[:animation_frames]
# Process each frame with background removal and lighting effects, but NOT shadow or background compositing
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
futures = []
for i in range(len(fg_frames)):
future = executor.submit(
# Modified process_frame call that skips background compositing
lambda frame: self._process_foreground_only(frame),
fg_frames[i]
)
futures.append(future)
# Process results as they complete
for i, future in enumerate(tqdm(futures, desc="Processing foregrounds")):
try:
result_frame, mask = future.result()
# Handle additional mask if provided
if additional_mask is not None:
additional_mask_pil = tensor2pil(
additional_mask[i % len(additional_mask)]
)
if invert_additional_mask:
additional_mask_pil = ImageOps.invert(additional_mask_pil)
mask = Image.fromarray(
np.minimum(np.array(mask), np.array(additional_mask_pil))
)
# Cache results
frame_key = f"frame_{i}"
self.frame_cache[frame_key] = (result_frame, mask)
processed_frames.append(result_frame)
masks.append(mask.convert('L'))
except Exception as e:
logger.error(f"Error processing frame {i}: {str(e)}")
if processed_frames:
processed_frames.append(processed_frames[-1])
masks.append(masks[-1])
else:
blank_frame = Image.new('RGBA', fg_frames[0].size, (0, 0, 0, 0))
blank_mask = Image.new('L', fg_frames[0].size, 0)
processed_frames.append(blank_frame)
masks.append(blank_mask)
# Create animated sequence
animated_frames = []
animated_masks = []
# Get dimensions for output canvas
example_frame = processed_frames[0]
frame_width, frame_height = example_frame.size
# If background provided, use its dimensions for canvas
if bg_frames:
bg_width, bg_height = bg_frames[0].size
canvas_width, canvas_height = bg_width, bg_height
else:
# Create canvas with enough space for animations
padding = int(100 * animation_speed)
canvas_width = frame_width + padding * 2
canvas_height = frame_height + padding * 2
# Pre-compute shadows if shadow is enabled
shadow_frames = []
if self.shadow_config.enable_shadow:
for frame in processed_frames:
shadow = LightShadowProcessor.create_shadow(frame, self.shadow_config)
shadow_frames.append(shadow)
# Process each frame index
for frame_idx in range(animation_frames):
# Calculate which source frame to use (loop if needed)
source_idx = frame_idx % len(processed_frames)
frame = processed_frames[source_idx]
current_mask = masks[source_idx]
# Get background for this frame if available
bg = bg_frames[frame_idx % len(bg_frames)] if bg_frames else None
# Create initial canvas with background
if bg:
canvas = bg.convert('RGBA')
else:
canvas = Image.new('RGBA', (canvas_width, canvas_height), (0, 0, 0, 0))
# Debug log for keyframe animation
if is_keyframe_animation:
logger.info(f"Processing keyframe animation frame {frame_idx}/{animation_frames}")
# Apply animation to frame
animated_frame, x, y = EnhancedAnimator.animate_element(
frame,
animation_type_value,
animation_speed,
frame_idx,
animation_frames,
x_pos,
y_pos,
canvas_width,
canvas_height,
animation_scale,
rotation,
easing_func,
repeats,
reverse,
delay,
steps,
phase_shift,
keyframe_parameters # Pass keyframe parameters
)
# Log position for debugging
if is_keyframe_animation and frame_idx % 10 == 0:
logger.info(f"Frame {frame_idx}: Position = ({x}, {y})")
# Apply shadow if enabled
if self.shadow_config.enable_shadow and shadow_frames[source_idx]:
# Animate the shadow with same parameters
animated_shadow, shadow_x, shadow_y = EnhancedAnimator.animate_element(
shadow_frames[source_idx],
animation_type_value,
animation_speed,
frame_idx,
animation_frames,
x_pos + self.shadow_config.shadow_direction_x,
y_pos + self.shadow_config.shadow_direction_y,
canvas_width,
canvas_height,
animation_scale,
rotation,
easing_func,
repeats,
reverse,
delay,
steps,
phase_shift,
keyframe_parameters # Pass keyframe parameters
)
# Paste shadow onto canvas
canvas.paste(animated_shadow, (shadow_x, shadow_y), animated_shadow)
# Position the animated frame on canvas
canvas.paste(animated_frame, (x, y), animated_frame)
# Handle mask similarly
mask_canvas = Image.new('L', (canvas_width, canvas_height), 0)
animated_mask, _, _ = EnhancedAnimator.animate_element(
current_mask,
animation_type_value,
animation_speed,
frame_idx,
animation_frames,
x_pos,
y_pos,
canvas_width,
canvas_height,
animation_scale,
rotation,
easing_func,
repeats,
reverse,
delay,
steps,
phase_shift,
keyframe_parameters # Pass keyframe parameters
)
mask_canvas.paste(animated_mask, (x, y))
# Convert to desired output format
if output_format == "RGB":
canvas = canvas.convert('RGB')
# Add to result lists
animated_frames.append(pil2tensor(canvas))
animated_masks.append(pil2tensor(mask_canvas))
# Combine all frames into a single tensor
result = torch.cat(animated_frames, dim=0)
result_masks = torch.cat(animated_masks, dim=0)
else:
# No animation, just process frames normally
processed_frames = []
masks = []
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
futures = []
for i in range(len(fg_frames)):
# Modified background handling logic
if bg_frames:
# If we have only one background frame but multiple foreground frames,
# reuse the same background for all foreground frames
if len(bg_frames) == 1 and len(fg_frames) > 1:
bg_frame = bg_frames[0]
else:
# Otherwise use the corresponding frame if available
bg_frame = bg_frames[i] if i < len(bg_frames) else None
else:
bg_frame = None
future = executor.submit(
self.process_frame,
fg_frames[i],
bg_frame,
x_position, # Use x_position parameter
y_position # Use y_position parameter
)
futures.append(future)
# Process results as they complete
for i, future in enumerate(tqdm(futures, desc="Processing frames")):
try:
result_frame, mask = future.result()
# Handle additional mask if provided
if additional_mask is not None:
additional_mask_pil = tensor2pil(
additional_mask[i % len(additional_mask)]
)
if invert_additional_mask:
additional_mask_pil = ImageOps.invert(additional_mask_pil)
mask = Image.fromarray(
np.minimum(np.array(mask), np.array(additional_mask_pil))
)
# Cache results
frame_key = f"frame_{i}"
self.frame_cache[frame_key] = (result_frame, mask)
processed_frames.append(pil2tensor(result_frame))
masks.append(pil2tensor(mask.convert('L')))
except Exception as e:
logger.error(f"Error processing frame {i}: {str(e)}")
if processed_frames:
processed_frames.append(processed_frames[-1])
masks.append(masks[-1])
else:
blank_frame = Image.new('RGBA', fg_frames[0].size, (0, 0, 0, 0))
blank_mask = Image.new('L', fg_frames[0].size, 0)
processed_frames.append(pil2tensor(blank_frame))
masks.append(pil2tensor(blank_mask))
# Convert output format if needed
if output_format == "RGB":
for i in range(len(processed_frames)):
frame = tensor2pil(processed_frames[i])
frame = frame.convert('RGB')
processed_frames[i] = pil2tensor(frame)
# Combine initial results
result = torch.cat(processed_frames, dim=0)
result_masks = torch.cat(masks, dim=0)
return (result, result_masks)
except Exception as e:
logger.error(f"Error in process_image: {str(e)}")
return (foreground, torch.zeros_like(foreground[:, :1, :, :]))