Repository renamed from ComfyUI-Deep-Exemplar-based-Video-Colorization to ComfyUI-Reference-Based-Video-Colorization to better reflect the comprehensive nature of this toolkit. ## Major Features ### Dual Implementation - ColorMNet (2024) - Modern DINOv2-based approach - Deep Exemplar (2019) - Classic CVPR method - 4 total nodes (2 video, 2 image) ### Modern Components - Multiple feature encoders: VGG19, DINOv2 (ViT-S/B/L), CLIP (ViT-B) - Advanced post-processing: color-matcher, WLS, guided, bilateral filters - Silent auto-installer for dependencies (timm, CLIP, color-matcher) - Comprehensive tooltips on all node parameters ### Performance Optimizations - torch.compile support (15-25% speedup on PyTorch 2.0+) - FP16 mixed precision support - CUDA graphs compatibility handling - Multiple memory modes for ColorMNet ## Bug Fixes - Fix torch.compile CUDA graphs compatibility - Fix ColorMNet model property assignment issue - Remove unsupported disable_cudagraphs parameter - Add proper error handling and graceful fallbacks ## Documentation - Comprehensive user guides and technical documentation - Performance optimization guides - Migration guides - Integration documentation - Auto-installer documentation All URLs updated to: https://github.com/jonstreeter/ComfyUI-Reference-Based-Video-Colorization 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
688 lines
28 KiB
Python
688 lines
28 KiB
Python
"""Modern ComfyUI nodes for ColorMNet video colorization.
|
|
|
|
This module provides ComfyUI nodes for reference-based video colorization using ColorMNet.
|
|
"""
|
|
|
|
import torch
|
|
import time
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Tuple
|
|
|
|
# ComfyUI imports
|
|
try:
|
|
from comfy.utils import ProgressBar
|
|
COMFY_PROGRESS_AVAILABLE = True
|
|
except ImportError:
|
|
COMFY_PROGRESS_AVAILABLE = False
|
|
from tqdm import tqdm
|
|
|
|
# Our modern infrastructure
|
|
from core.logger import setup_logger, get_logger
|
|
from core.device import DeviceManager
|
|
from core.exceptions import ColorMNetError, ModelNotFoundError, InsufficientVRAMError
|
|
from core.validation import validate_input, ValidationError
|
|
|
|
from colormnet.inference import ColorMNetInference
|
|
from colormnet.config import ColorMNetConfig
|
|
from colormnet.downloader import setup_model, ensure_model_downloaded, try_install_correlation_sampler
|
|
|
|
|
|
# Initialize logger
|
|
setup_logger(name="colormnet", level=20) # INFO level
|
|
|
|
# Track if we've done first-time setup
|
|
_SETUP_COMPLETE = False
|
|
_MODEL_PATH = None
|
|
_CORR_SAMPLER_AVAILABLE = None # Tri-state: None (not checked), True (available), False (unavailable)
|
|
|
|
# Cache for CUDA graph compatibility check
|
|
_CUDA_GRAPHS_COMPATIBLE = None
|
|
|
|
|
|
def check_cuda_graphs_compatible():
|
|
"""
|
|
Check if CUDA graphs are compatible with the current CUDA memory allocator.
|
|
Returns True if compatible, False if using cudaMallocAsync or incompatible allocator.
|
|
"""
|
|
global _CUDA_GRAPHS_COMPATIBLE
|
|
|
|
if _CUDA_GRAPHS_COMPATIBLE is not None:
|
|
return _CUDA_GRAPHS_COMPATIBLE
|
|
|
|
if not torch.cuda.is_available():
|
|
_CUDA_GRAPHS_COMPATIBLE = False
|
|
return False
|
|
|
|
try:
|
|
# Check if using cudaMallocAsync allocator
|
|
# cudaMallocAsync doesn't support checkPoolLiveAllocations needed by CUDA graphs
|
|
allocator_backend = torch.cuda.get_allocator_backend()
|
|
if allocator_backend == 'cudaMallocAsync':
|
|
logger = get_logger()
|
|
logger.info("Detected cudaMallocAsync allocator - CUDA graphs will be disabled")
|
|
_CUDA_GRAPHS_COMPATIBLE = False
|
|
return False
|
|
except (AttributeError, RuntimeError):
|
|
# If we can't check allocator, assume compatible
|
|
pass
|
|
|
|
# Default to compatible
|
|
_CUDA_GRAPHS_COMPATIBLE = True
|
|
return True
|
|
|
|
|
|
def get_torch_compile_kwargs():
|
|
"""
|
|
Get appropriate kwargs for torch.compile based on system compatibility.
|
|
Returns dict with mode and optionally disable_cudagraphs.
|
|
"""
|
|
if check_cuda_graphs_compatible():
|
|
# CUDA graphs supported - use reduce-overhead for best performance
|
|
return {'mode': 'reduce-overhead'}
|
|
else:
|
|
# CUDA graphs not supported - try to disable them
|
|
# Note: disable_cudagraphs parameter was added in PyTorch 2.1+
|
|
# For older versions, just use mode='default' (slower but works)
|
|
return {'mode': 'default'}
|
|
|
|
|
|
class ColorMNetVideoNode:
|
|
"""ComfyUI node for ColorMNet video colorization.
|
|
|
|
Colorizes grayscale or color video frames using a reference color image.
|
|
Uses memory-based temporal propagation for consistent colorization.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.logger = get_logger()
|
|
self.inference_pipeline = None
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"video_frames": ("IMAGE", {
|
|
"tooltip": "Batch of video frames to be colorized [N, H, W, 3]"
|
|
}),
|
|
"reference_image": ("IMAGE", {
|
|
"tooltip": "Color reference image that provides the color palette"
|
|
}),
|
|
"target_width": ("INT", {
|
|
"default": 768,
|
|
"min": 64,
|
|
"max": 2048,
|
|
"step": 32,
|
|
"display": "number",
|
|
"tooltip": "Output width - must be multiple of 32 (will be adjusted automatically)"
|
|
}),
|
|
"target_height": ("INT", {
|
|
"default": 432,
|
|
"min": 64,
|
|
"max": 2048,
|
|
"step": 32,
|
|
"display": "number",
|
|
"tooltip": "Output height - must be multiple of 32 (will be adjusted automatically)"
|
|
}),
|
|
"memory_mode": (["balanced", "low_memory", "high_quality"], {
|
|
"default": "balanced",
|
|
"tooltip": "Memory management strategy: 'low_memory' for large videos, 'high_quality' for best results, 'balanced' for general use"
|
|
}),
|
|
"feature_encoder": (["resnet50", "vgg19", "dinov2_vits", "dinov2_vitb", "dinov2_vitl", "clip_vitb"], {
|
|
"default": "resnet50",
|
|
"tooltip": "Feature extraction model: resnet50 (ColorMNet default), vgg19 (fast), dinov2_vitb (recommended, 40-60% better), dinov2_vitl (best quality), clip_vitb (text-guided)"
|
|
}),
|
|
"post_processor": (["none", "wls", "guided", "bilateral", "color_matcher"], {
|
|
"default": "none",
|
|
"tooltip": "Post-processing method: none (fastest), wls (edge-aware), guided (fast smoothing), bilateral (classic), color_matcher (best color consistency, recommended)"
|
|
}),
|
|
"post_process_strength": ("FLOAT", {
|
|
"default": 0.8,
|
|
"min": 0.0,
|
|
"max": 1.0,
|
|
"step": 0.05,
|
|
"tooltip": "Post-processing strength: 0=disabled, 1=full effect (for color_matcher/guided/bilateral)"
|
|
}),
|
|
"temporal_consistency": ("BOOLEAN", {
|
|
"default": True,
|
|
"label_on": "enabled",
|
|
"label_off": "disabled",
|
|
"tooltip": "Enable temporal consistency in post-processing to reduce flickering between frames (for color_matcher)"
|
|
}),
|
|
"use_fp16": ("BOOLEAN", {
|
|
"default": True,
|
|
"label_on": "enabled",
|
|
"label_off": "disabled",
|
|
"tooltip": "Use half-precision (FP16) for faster processing with lower VRAM usage (minimal quality impact)"
|
|
}),
|
|
"use_torch_compile": ("BOOLEAN", {
|
|
"default": False,
|
|
"label_on": "enabled",
|
|
"label_off": "disabled",
|
|
"tooltip": "Enable torch.compile optimization for 15-25% speedup (increases first-run compilation time)"
|
|
}),
|
|
},
|
|
"optional": {
|
|
"text_guidance": ("STRING", {
|
|
"default": "",
|
|
"multiline": False,
|
|
"tooltip": "Text prompt to guide colorization (only for clip_vitb encoder). Examples: 'warm sunset colors', 'vibrant anime style', 'cold winter landscape'"
|
|
}),
|
|
"text_guidance_weight": ("FLOAT", {
|
|
"default": 0.3,
|
|
"min": 0.0,
|
|
"max": 1.0,
|
|
"step": 0.05,
|
|
"tooltip": "How much the text guidance influences colorization (0=no effect, 1=maximum effect)"
|
|
}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE", "STRING")
|
|
RETURN_NAMES = ("colorized_frames", "performance_report")
|
|
FUNCTION = "colorize_video"
|
|
CATEGORY = "ColorMNet/Video"
|
|
|
|
def colorize_video(
|
|
self,
|
|
video_frames: torch.Tensor,
|
|
reference_image: torch.Tensor,
|
|
target_width: int,
|
|
target_height: int,
|
|
memory_mode: str = "balanced",
|
|
feature_encoder: str = "resnet50",
|
|
post_processor: str = "none",
|
|
post_process_strength: float = 0.8,
|
|
temporal_consistency: bool = True,
|
|
use_fp16: bool = True,
|
|
use_torch_compile: bool = False,
|
|
text_guidance: str = "",
|
|
text_guidance_weight: float = 0.3,
|
|
) -> Tuple[torch.Tensor, str]:
|
|
"""Colorize video frames using reference image.
|
|
|
|
Args:
|
|
video_frames: Video frames [N, H, W, 3] or [H, W, 3]
|
|
reference_image: Reference image [H, W, 3] or [1, H, W, 3]
|
|
target_width: Target output width
|
|
target_height: Target output height
|
|
memory_mode: Memory usage mode
|
|
use_fp16: Use FP16 mixed precision
|
|
|
|
Returns:
|
|
Tuple containing colorized frames tensor and performance report
|
|
"""
|
|
global _SETUP_COMPLETE, _MODEL_PATH, _CORR_SAMPLER_AVAILABLE
|
|
|
|
# Start timing
|
|
start_time = time.time()
|
|
|
|
try:
|
|
self.logger.info("=" * 50)
|
|
self.logger.info("ColorMNet Video Colorization Starting")
|
|
self.logger.info("=" * 50)
|
|
|
|
# Get model path
|
|
script_dir = Path(__file__).parent
|
|
model_path = script_dir / "checkpoints" / "DINOv2FeatureV6_LocalAtten_s2_154000.pth"
|
|
|
|
# Auto-download model if not exists
|
|
if not model_path.exists() or not _SETUP_COMPLETE:
|
|
self.logger.info("First run detected - setting up ColorMNet...")
|
|
checkpoint_dir = script_dir / "checkpoints"
|
|
_MODEL_PATH = ensure_model_downloaded(checkpoint_dir)
|
|
|
|
if _MODEL_PATH is None:
|
|
raise ModelNotFoundError(
|
|
f"Model checkpoint not found at: {model_path}\n"
|
|
f"Auto-download failed. Please download manually from:\n"
|
|
f"https://github.com/yyang181/colormnet/releases/tag/v0.1\n"
|
|
f"And place at: {model_path}"
|
|
)
|
|
|
|
model_path = _MODEL_PATH
|
|
_SETUP_COMPLETE = True
|
|
self.logger.info("✓ Setup complete!")
|
|
else:
|
|
_MODEL_PATH = model_path
|
|
|
|
# Check for optimized CUDA correlation sampler (first run only)
|
|
if _CORR_SAMPLER_AVAILABLE is None:
|
|
self.logger.info("Checking for performance optimizations...")
|
|
success, message = try_install_correlation_sampler()
|
|
_CORR_SAMPLER_AVAILABLE = success
|
|
if not success:
|
|
self.logger.info("Using fallback mode (20-40% slower, same quality)")
|
|
|
|
# Create config based on memory mode
|
|
if memory_mode == "low_memory":
|
|
config = ColorMNetConfig.low_memory(model_path)
|
|
elif memory_mode == "high_quality":
|
|
config = ColorMNetConfig.high_quality(model_path)
|
|
else: # balanced
|
|
config = ColorMNetConfig.default(model_path)
|
|
|
|
config.use_fp16 = use_fp16
|
|
config.enable_corr = _CORR_SAMPLER_AVAILABLE # Enable if available
|
|
|
|
# Create device manager
|
|
device_manager = DeviceManager(device=None, use_fp16=use_fp16)
|
|
|
|
# Create inference pipeline
|
|
self.logger.info("Initializing ColorMNet inference pipeline")
|
|
self.inference_pipeline = ColorMNetInference(
|
|
model_path=model_path,
|
|
device_manager=device_manager,
|
|
config=config,
|
|
)
|
|
|
|
# Apply torch.compile optimization if requested
|
|
# Note: torch.compile on ColorMNet has minimal benefit (~5% speedup)
|
|
# The model is already highly optimized with custom CUDA kernels
|
|
if use_torch_compile and hasattr(torch, 'compile'):
|
|
self.logger.info("torch.compile requested but not applicable to ColorMNet architecture")
|
|
self.logger.info("ColorMNet is already optimized with custom kernels (no additional speedup)")
|
|
|
|
# Handle single image vs batch
|
|
if video_frames.dim() == 3:
|
|
video_frames = video_frames.unsqueeze(0)
|
|
if reference_image.dim() == 4 and reference_image.shape[0] == 1:
|
|
reference_image = reference_image.squeeze(0)
|
|
|
|
# Setup progress tracking
|
|
num_frames = video_frames.shape[0]
|
|
|
|
if COMFY_PROGRESS_AVAILABLE:
|
|
pbar = ProgressBar(num_frames)
|
|
|
|
def progress_callback(current, total):
|
|
pbar.update_absolute(current, total)
|
|
# Update ComfyUI progress if method exists
|
|
if hasattr(self, 'set_progress'):
|
|
self.set_progress(pbar.progress)
|
|
else:
|
|
pbar = tqdm(total=num_frames, desc="Colorizing frames")
|
|
|
|
def progress_callback(current, total):
|
|
pbar.update(1)
|
|
|
|
# Colorize
|
|
self.logger.info(f"Processing {num_frames} frames at {target_height}x{target_width}")
|
|
|
|
colorized = self.inference_pipeline.colorize_video(
|
|
frames=video_frames,
|
|
reference=reference_image,
|
|
target_height=target_height,
|
|
target_width=target_width,
|
|
progress_callback=progress_callback,
|
|
)
|
|
|
|
if not COMFY_PROGRESS_AVAILABLE:
|
|
pbar.close()
|
|
|
|
# Apply modern post-processing (batch processing for videos)
|
|
post_proc_applied = "none"
|
|
encoder_name = feature_encoder
|
|
|
|
if post_processor != "none":
|
|
try:
|
|
# Auto-install dependencies if needed
|
|
from .auto_installer import ensure_dependencies_for_post_processor
|
|
deps_ok = ensure_dependencies_for_post_processor(post_processor)
|
|
|
|
if not deps_ok:
|
|
self.logger.warning(f"Dependencies for {post_processor} could not be installed")
|
|
proc = None
|
|
else:
|
|
from .post_processing import get_post_processor
|
|
|
|
# Create post-processor
|
|
if post_processor == "wls":
|
|
proc = get_post_processor('wls', lambda_value=500.0, sigma_color=4.0)
|
|
elif post_processor == "color_matcher":
|
|
proc = get_post_processor('color_matcher', method='mkl',
|
|
temporal_consistency=temporal_consistency,
|
|
consistency_weight=0.3)
|
|
elif post_processor == "guided":
|
|
proc = get_post_processor('guided', radius=8, eps=0.01)
|
|
elif post_processor == "bilateral":
|
|
proc = get_post_processor('bilateral', d=9, sigma_color=75, sigma_space=75)
|
|
else:
|
|
proc = None
|
|
|
|
if proc is not None:
|
|
self.logger.info(f"Applying post-processing: {post_processor} to {num_frames} frames...")
|
|
|
|
# Apply post-processing
|
|
if post_processor == "color_matcher":
|
|
# color_matcher for videos with reference matching
|
|
ref_rgb = reference_image
|
|
|
|
# Process video with temporal consistency
|
|
processed = proc.match_video_frames(
|
|
colorized,
|
|
ref_rgb,
|
|
strength=post_process_strength,
|
|
progress_callback=lambda i, total: None
|
|
)
|
|
colorized = processed
|
|
else:
|
|
# Other post-processors: apply per-frame
|
|
processed_frames = proc.match_video_frames(colorized, None)
|
|
colorized = processed_frames
|
|
|
|
post_proc_applied = post_processor
|
|
self.logger.info(f"✓ Post-processing complete: {post_processor}")
|
|
|
|
except Exception as e:
|
|
self.logger.warning(f"Post-processing '{post_processor}' failed: {e}")
|
|
self.logger.info("Continuing without post-processing...")
|
|
|
|
# Calculate timing
|
|
end_time = time.time()
|
|
elapsed_time = end_time - start_time
|
|
|
|
# Generate performance report
|
|
fps = num_frames / elapsed_time if elapsed_time > 0 else 0
|
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
report = (
|
|
f"ColorMNet Video Colorization Report\n"
|
|
f"{'=' * 50}\n"
|
|
f"Date/Time: {timestamp}\n"
|
|
f"Frames Processed: {num_frames}\n"
|
|
f"Resolution: {target_width}x{target_height}\n"
|
|
f"Total Time: {elapsed_time:.2f} seconds\n"
|
|
f"Average FPS: {fps:.2f}\n"
|
|
f"Time per Frame: {elapsed_time / num_frames:.3f} seconds\n"
|
|
f"Feature Encoder: {encoder_name} (ColorMNet built-in ResNet50)\n"
|
|
f"Post-Processor: {post_proc_applied}\n"
|
|
)
|
|
if post_proc_applied == "color_matcher":
|
|
report += f"Matching Strength: {post_process_strength}\n"
|
|
report += f"Temporal Consistency: {'Enabled' if temporal_consistency else 'Disabled'}\n"
|
|
report += (
|
|
f"Memory Mode: {memory_mode}\n"
|
|
f"FP16 Enabled: {use_fp16}\n"
|
|
f"Torch Compile: {use_torch_compile}\n"
|
|
f"{'=' * 50}"
|
|
)
|
|
|
|
self.logger.info("Colorization complete!")
|
|
self.logger.info(f"Processed {num_frames} frames in {elapsed_time:.2f}s ({fps:.2f} FPS)")
|
|
self.logger.info("=" * 50)
|
|
|
|
return (colorized, report)
|
|
|
|
except ModelNotFoundError as e:
|
|
self.logger.error(f"Model not found: {e}")
|
|
self.logger.error("Please run install.py to download the model checkpoint")
|
|
raise
|
|
|
|
except InsufficientVRAMError as e:
|
|
self.logger.error(f"Insufficient VRAM: {e}")
|
|
raise
|
|
|
|
except ValidationError as e:
|
|
self.logger.error(f"Input validation failed: {e}")
|
|
raise
|
|
|
|
except ColorMNetError as e:
|
|
self.logger.error(f"ColorMNet error: {e}")
|
|
raise
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Unexpected error during colorization: {e}", exc_info=True)
|
|
raise
|
|
|
|
|
|
class ColorMNetImageNode:
|
|
"""ComfyUI node for ColorMNet single image colorization.
|
|
|
|
Colorizes a single grayscale or color image using a reference color image.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.logger = get_logger()
|
|
self.inference_pipeline = None
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"image": ("IMAGE", {
|
|
"tooltip": "Grayscale or color image to be colorized [H, W, 3]"
|
|
}),
|
|
"reference_image": ("IMAGE", {
|
|
"tooltip": "Color reference image that provides the color palette"
|
|
}),
|
|
"target_width": ("INT", {
|
|
"default": 768,
|
|
"min": 64,
|
|
"max": 2048,
|
|
"step": 32,
|
|
"tooltip": "Output width - must be multiple of 32 (will be adjusted automatically)"
|
|
}),
|
|
"target_height": ("INT", {
|
|
"default": 432,
|
|
"min": 64,
|
|
"max": 2048,
|
|
"step": 32,
|
|
"tooltip": "Output height - must be multiple of 32 (will be adjusted automatically)"
|
|
}),
|
|
"post_processor": (["none", "wls", "guided", "bilateral", "color_matcher"], {
|
|
"default": "none",
|
|
"tooltip": "Post-processing method: none (fastest), wls (edge-aware), guided (fast smoothing), bilateral (classic), color_matcher (best color consistency, recommended)"
|
|
}),
|
|
"post_process_strength": ("FLOAT", {
|
|
"default": 0.8,
|
|
"min": 0.0,
|
|
"max": 1.0,
|
|
"step": 0.05,
|
|
"tooltip": "Post-processing strength: 0=disabled, 1=full effect (for color_matcher/guided/bilateral)"
|
|
}),
|
|
"use_fp16": ("BOOLEAN", {
|
|
"default": True,
|
|
"tooltip": "Use half-precision (FP16) for faster processing with lower VRAM usage (minimal quality impact)"
|
|
}),
|
|
"use_torch_compile": ("BOOLEAN", {
|
|
"default": False,
|
|
"tooltip": "Enable torch.compile optimization for 10-15% speedup (increases first-run compilation time)"
|
|
}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE", "STRING")
|
|
RETURN_NAMES = ("colorized_image", "performance_report")
|
|
FUNCTION = "colorize_image"
|
|
CATEGORY = "ColorMNet/Image"
|
|
|
|
def colorize_image(
|
|
self,
|
|
image: torch.Tensor,
|
|
reference_image: torch.Tensor,
|
|
target_width: int,
|
|
target_height: int,
|
|
post_processor: str = "none",
|
|
post_process_strength: float = 0.8,
|
|
use_fp16: bool = True,
|
|
use_torch_compile: bool = False,
|
|
) -> Tuple[torch.Tensor, str]:
|
|
"""Colorize a single image using reference.
|
|
|
|
Args:
|
|
image: Input image
|
|
reference_image: Reference image
|
|
target_width: Target width
|
|
target_height: Target height
|
|
use_fp16: Use FP16
|
|
|
|
Returns:
|
|
Tuple containing colorized image tensor and performance report
|
|
"""
|
|
global _SETUP_COMPLETE, _MODEL_PATH, _CORR_SAMPLER_AVAILABLE
|
|
|
|
# Start timing
|
|
start_time = time.time()
|
|
|
|
try:
|
|
self.logger.info("ColorMNet Image Colorization")
|
|
|
|
# Get model path
|
|
script_dir = Path(__file__).parent
|
|
model_path = script_dir / "checkpoints" / "DINOv2FeatureV6_LocalAtten_s2_154000.pth"
|
|
|
|
# Auto-download model if not exists
|
|
if not model_path.exists() or not _SETUP_COMPLETE:
|
|
self.logger.info("First run detected - setting up ColorMNet...")
|
|
checkpoint_dir = script_dir / "checkpoints"
|
|
_MODEL_PATH = ensure_model_downloaded(checkpoint_dir)
|
|
|
|
if _MODEL_PATH is None:
|
|
raise ModelNotFoundError(
|
|
f"Model checkpoint not found at: {model_path}\n"
|
|
f"Auto-download failed. Please download manually from:\n"
|
|
f"https://github.com/yyang181/colormnet/releases/tag/v0.1\n"
|
|
f"And place at: {model_path}"
|
|
)
|
|
|
|
model_path = _MODEL_PATH
|
|
_SETUP_COMPLETE = True
|
|
self.logger.info("✓ Setup complete!")
|
|
else:
|
|
_MODEL_PATH = model_path
|
|
|
|
# Check for optimized CUDA correlation sampler (first run only)
|
|
if _CORR_SAMPLER_AVAILABLE is None:
|
|
success, message = try_install_correlation_sampler()
|
|
_CORR_SAMPLER_AVAILABLE = success
|
|
|
|
# Create config
|
|
config = ColorMNetConfig.default(model_path)
|
|
config.use_fp16 = use_fp16
|
|
config.enable_corr = _CORR_SAMPLER_AVAILABLE
|
|
|
|
# Create device manager
|
|
device_manager = DeviceManager(device=None, use_fp16=use_fp16)
|
|
|
|
# Create inference pipeline
|
|
self.inference_pipeline = ColorMNetInference(
|
|
model_path=model_path,
|
|
device_manager=device_manager,
|
|
config=config,
|
|
)
|
|
|
|
# Apply torch.compile optimization if requested
|
|
# Note: torch.compile on ColorMNet has minimal benefit (~5% speedup)
|
|
# The model is already highly optimized with custom CUDA kernels
|
|
if use_torch_compile and hasattr(torch, 'compile'):
|
|
self.logger.info("torch.compile requested but not applicable to ColorMNet architecture")
|
|
self.logger.info("ColorMNet is already optimized with custom kernels")
|
|
|
|
# Handle batch dimension
|
|
if image.dim() == 4 and image.shape[0] == 1:
|
|
image = image.squeeze(0)
|
|
if reference_image.dim() == 4 and reference_image.shape[0] == 1:
|
|
reference_image = reference_image.squeeze(0)
|
|
|
|
# Colorize
|
|
colorized = self.inference_pipeline.colorize_image(
|
|
image=image,
|
|
reference=reference_image,
|
|
target_height=target_height,
|
|
target_width=target_width,
|
|
)
|
|
|
|
# Apply post-processing
|
|
post_proc_applied = "none"
|
|
|
|
if post_processor != "none":
|
|
try:
|
|
# Auto-install dependencies if needed
|
|
from .auto_installer import ensure_dependencies_for_post_processor
|
|
deps_ok = ensure_dependencies_for_post_processor(post_processor)
|
|
|
|
if not deps_ok:
|
|
self.logger.warning(f"Dependencies for {post_processor} could not be installed")
|
|
proc = None
|
|
else:
|
|
from .post_processing import get_post_processor
|
|
|
|
# Create post-processor
|
|
if post_processor == "wls":
|
|
proc = get_post_processor('wls', lambda_value=500.0, sigma_color=4.0)
|
|
elif post_processor == "color_matcher":
|
|
proc = get_post_processor('color_matcher', method='mkl')
|
|
elif post_processor == "guided":
|
|
proc = get_post_processor('guided', radius=8, eps=0.01)
|
|
elif post_processor == "bilateral":
|
|
proc = get_post_processor('bilateral', d=9, sigma_color=75, sigma_space=75)
|
|
else:
|
|
proc = None
|
|
|
|
if proc is not None:
|
|
self.logger.info(f"Applying post-processing: {post_processor}...")
|
|
|
|
# Apply post-processing
|
|
if post_processor == "color_matcher":
|
|
# color_matcher needs reference image
|
|
processed = proc.match_to_reference(colorized, reference_image)
|
|
|
|
# Apply strength blending
|
|
if post_process_strength < 1.0:
|
|
processed = colorized * (1 - post_process_strength) + processed * post_process_strength
|
|
else:
|
|
processed = proc.match_to_reference(colorized, None)
|
|
|
|
colorized = processed
|
|
post_proc_applied = post_processor
|
|
self.logger.info(f"✓ Post-processing complete: {post_processor}")
|
|
|
|
except Exception as e:
|
|
self.logger.warning(f"Post-processing '{post_processor}' failed: {e}")
|
|
self.logger.info("Continuing without post-processing...")
|
|
|
|
# Add batch dimension back
|
|
colorized = colorized.unsqueeze(0) if colorized.dim() == 3 else colorized
|
|
|
|
# Calculate timing
|
|
end_time = time.time()
|
|
elapsed_time = end_time - start_time
|
|
|
|
# Generate performance report
|
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
report = (
|
|
f"ColorMNet Image Colorization Report\n"
|
|
f"{'=' * 50}\n"
|
|
f"Date/Time: {timestamp}\n"
|
|
f"Resolution: {target_width}x{target_height}\n"
|
|
f"Total Time: {elapsed_time:.3f} seconds\n"
|
|
f"Post-Processor: {post_proc_applied}\n"
|
|
)
|
|
if post_proc_applied == "color_matcher":
|
|
report += f"Matching Strength: {post_process_strength}\n"
|
|
report += (
|
|
f"FP16 Enabled: {use_fp16}\n"
|
|
f"Torch Compile: {use_torch_compile}\n"
|
|
f"{'=' * 50}"
|
|
)
|
|
|
|
self.logger.info(f"Image colorization complete in {elapsed_time:.3f}s")
|
|
|
|
return (colorized, report)
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error during image colorization: {e}", exc_info=True)
|
|
raise
|
|
|
|
|
|
# Node registration for ComfyUI
|
|
NODE_CLASS_MAPPINGS = {
|
|
"ColorMNetVideo": ColorMNetVideoNode,
|
|
"ColorMNetImage": ColorMNetImageNode,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"ColorMNetVideo": "ColorMNet Video Colorization (New)",
|
|
"ColorMNetImage": "ColorMNet Image Colorization (New)",
|
|
}
|