Added new export node
This commit is contained in:
+10
-8
@@ -16,15 +16,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **Enhanced 16-bit linear output** - Improved scaling algorithm for better 16-bit range utilization
|
||||
- **Changed default algorithm** - Natural Blend is now default instead of Mertens for natural results
|
||||
|
||||
### Added
|
||||
- **🆕 HDR Export Node** - Dedicated EXR export node that preserves full HDR dynamic range
|
||||
- **True HDR file output** - EXR files maintain values above 1.0 without normalization
|
||||
- **ComfyUI-style interface** - Filename prefix and output path inputs like built-in save nodes
|
||||
- **HDR verification** - Automatic verification that HDR data is preserved in exported files
|
||||
|
||||
### Fixed
|
||||
- **🚨 CRITICAL: Fixed HDR data completely broken** - Values were being normalized to 0-1 range, killing HDR!
|
||||
- **🚨 CRITICAL: Now outputs TRUE 16-bit HDR values** - Values go above 1.0 (up to 5-100 range) as required for HDR
|
||||
- **Fixed all algorithms looking the same** - Each algorithm now produces distinctly different outputs
|
||||
- **Algorithm-specific HDR scaling** - Natural Blend (1-8 range), Mertens (1-12 range), Debevec/Robertson (1-100 range)
|
||||
- **Preserved dynamic range data** - No more normalization destroying HDR information
|
||||
- **Enhanced visual differences** - Each algorithm produces visually different and useful results
|
||||
- **Fixed Natural Blend masking** - Better blending logic with 30% strength for natural transitions
|
||||
- **Algorithm-specific processing** - Gamma correction and color handling only where beneficial
|
||||
- **🚨 CRITICAL: Identified HDR export issue** - ComfyUI's built-in save nodes normalize to 0-1 range
|
||||
- **Reverted Debevec color inversion** - Restored working algorithm implementations
|
||||
- **Created proper HDR workflow** - Use HDR Export Node instead of built-in save nodes for EXR
|
||||
- **Algorithm differences working** - Confirmed algorithms produce different results as intended
|
||||
|
||||
## [1.0.1] - 2025-01-20
|
||||
|
||||
|
||||
@@ -12,18 +12,19 @@ Professional HDR (High Dynamic Range) processing nodes for ComfyUI using the **D
|
||||
|
||||
## 🎯 Features
|
||||
|
||||
- **🚨 TRUE 16-bit HDR Output**: Values go above 1.0 (up to 100+) for authentic HDR data
|
||||
- **Four Distinct Algorithms**: Each produces different results with specific HDR ranges
|
||||
- **Natural Blend** (1-8): EV0 appearance with enhanced dynamic range
|
||||
- **Mertens** (1-12): Adobe Lightroom style results
|
||||
- **Debevec** (1-100+): Maximum dynamic range for extreme lighting
|
||||
- **Robertson** (1-80+): Alternative robust HDR processing
|
||||
- **Two Processing Modes**:
|
||||
- **🆕 HDR Export Node**: Dedicated EXR export that preserves full HDR dynamic range
|
||||
- **🚨 TRUE HDR Values Above 1.0**: Proper HDR data preservation in EXR files
|
||||
- **Four Distinct HDR Algorithms**: Each produces different visual results
|
||||
- **Natural Blend**: EV0 appearance with enhanced dynamic range
|
||||
- **Mertens**: Adobe Lightroom style exposure fusion
|
||||
- **Debevec**: Classic HDR with maximum dynamic range
|
||||
- **Robertson**: Alternative robust HDR processing
|
||||
- **Three Custom Nodes**:
|
||||
- **3-Stop Processor**: Merges EV+2, EV+0, EV-2 exposures
|
||||
- **5-Stop Processor**: Merges EV+4, EV+2, EV+0, EV-2, EV-4 exposures
|
||||
- **Linear Colorspace Preservation**: True 16-bit linear data with no normalization
|
||||
- **Visual Algorithm Differences**: Each algorithm produces noticeably different results
|
||||
- **ComfyUI Integration**: Seamless integration with proper HDR tensor handling
|
||||
- **HDR Export**: Saves EXR files with preserved HDR data
|
||||
- **ComfyUI-Style Interface**: Filename and path inputs like built-in save nodes
|
||||
- **HDR Verification**: Automatic checking that HDR data is preserved in exports
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
@@ -71,45 +72,75 @@ Professional HDR (High Dynamic Range) processing nodes for ComfyUI using the **D
|
||||
|
||||
## 🎨 Usage
|
||||
|
||||
### 🚨 **CRITICAL: Proper HDR Workflow**
|
||||
|
||||
1. **HDR Processing**: Use "Luminance Stack Processor" nodes
|
||||
2. **🔥 HDR Export**: **ALWAYS** connect to "HDR Export to EXR" node
|
||||
3. **❌ Never use ComfyUI's built-in save nodes** for HDR - they normalize to 0-1!
|
||||
|
||||
### Luminance Stack Processor (3 Stops)
|
||||
|
||||
Perfect for standard HDR bracketing with 3 exposures:
|
||||
|
||||
**Inputs:**
|
||||
- `ev_plus_2`: Overexposed image (+2 EV)
|
||||
- `ev_0`: Normal exposure image (0 EV)
|
||||
- `ev_0`: Normal exposure image (0 EV)
|
||||
- `ev_minus_2`: Underexposed image (-2 EV)
|
||||
- `exposure_step`: (Optional) EV step size (default: 2.0)
|
||||
- `hdr_algorithm`: (Optional) Algorithm to use: "mertens" (default), "debevec", "robertson"
|
||||
- `hdr_algorithm`: Choose "natural_blend" (default), "mertens", "debevec", "robertson"
|
||||
|
||||
**Output:**
|
||||
- `hdr_image`: Merged 16-bit linear HDR image
|
||||
- `hdr_image`: HDR tensor with values potentially above 1.0
|
||||
|
||||
### HDR Export to EXR
|
||||
|
||||
**REQUIRED for true HDR preservation:**
|
||||
|
||||
**Inputs:**
|
||||
- `hdr_image`: HDR tensor from processing nodes
|
||||
- `filename_prefix`: Base filename (e.g., "My_HDR_Image")
|
||||
- `output_path`: Directory path (empty = ComfyUI output folder)
|
||||
|
||||
**Output:**
|
||||
- `filepath`: Path to saved EXR file with preserved HDR values
|
||||
|
||||
### Luminance Stack Processor (5 Stops)
|
||||
|
||||
For extended dynamic range with 5 exposures:
|
||||
|
||||
**Inputs:**
|
||||
- `ev_plus_4`: Most overexposed image (+4 EV)
|
||||
- `ev_plus_4`: Overexposed image (+4 EV)
|
||||
- `ev_plus_2`: Overexposed image (+2 EV)
|
||||
- `ev_0`: Normal exposure image (0 EV)
|
||||
- `ev_minus_2`: Underexposed image (-2 EV)
|
||||
- `ev_minus_4`: Most underexposed image (-4 EV)
|
||||
- `ev_minus_4`: Underexposed image (-4 EV)
|
||||
- `exposure_step`: (Optional) EV step size (default: 2.0)
|
||||
- `hdr_algorithm`: (Optional) Algorithm to use: "mertens" (default), "debevec", "robertson"
|
||||
- `hdr_algorithm`: Choose "natural_blend" (default), "mertens", "debevec", "robertson"
|
||||
|
||||
**Output:**
|
||||
- `hdr_image`: Merged 16-bit linear HDR image
|
||||
- `hdr_image`: HDR tensor with values potentially above 1.0
|
||||
|
||||
### 📋 **Complete HDR Workflow Example:**
|
||||
|
||||
1. **Load Images**: Load your bracketed exposures (3 or 5 images)
|
||||
2. **Add Processing Node**: "Luminance Stack Processor (3/5 Stops)"
|
||||
3. **Connect Exposures**: Connect each EV image to corresponding input
|
||||
4. **Choose Algorithm**: Select HDR algorithm (Natural Blend recommended)
|
||||
5. **Add Export Node**: "HDR Export to EXR"
|
||||
6. **Connect HDR Output**: From processor to export node
|
||||
7. **Set Filename**: Enter desired filename prefix
|
||||
8. **Set Path**: Output directory (or leave empty for default)
|
||||
9. **Execute**: Get true HDR EXR file with values above 1.0!
|
||||
|
||||
## 🔬 How It Works
|
||||
|
||||
The nodes implement **multiple HDR algorithms** with **Natural Blend** as the default (preserves natural appearance):
|
||||
|
||||
1. **Analyzes Multiple Exposures**: Takes differently exposed 8-bit images of the same scene
|
||||
2. **Estimates Camera Response Function**: Determines how the camera sensor responds to light
|
||||
3. **Recovers Scene Radiance**: Calculates the actual light values in the scene
|
||||
4. **Merges to HDR**: Combines all exposures into a single high dynamic range image
|
||||
5. **Linear 16-bit Output**: Outputs 16-bit linear colorspace data preserving full HDR range
|
||||
1. **Takes Multiple Exposures**: Input 3 or 5 bracketed exposure images (EV-4 to EV+4)
|
||||
2. **Selects HDR Algorithm**: Choose between Natural Blend, Mertens, Debevec, or Robertson
|
||||
3. **Processes HDR Data**: Merges exposures preserving dynamic range above 1.0
|
||||
4. **Outputs HDR Tensor**: 16-bit linear data ready for EXR export
|
||||
5. **🚨 CRITICAL: Use HDR Export Node**: ComfyUI's built-in save nodes normalize to 0-1, use our HDR Export for true EXR
|
||||
|
||||
### 🎯 **HDR Algorithm Options:**
|
||||
|
||||
|
||||
+152
-33
@@ -163,45 +163,29 @@ class DebevecHDRProcessor:
|
||||
logger.info(f"Image formats: {[img.shape for img in processed_images]}")
|
||||
logger.info(f"Image dtypes: {[img.dtype for img in processed_images]}")
|
||||
|
||||
# Process using selected algorithm - each should produce DIFFERENT outputs
|
||||
# Process using selected algorithm
|
||||
if algorithm == "mertens":
|
||||
# Mertens Exposure Fusion - Adobe Lightroom style
|
||||
# Mertens Exposure Fusion - uses original gamma, no correction needed
|
||||
logger.info("Using Mertens Exposure Fusion algorithm...")
|
||||
hdr_radiance = self.merge_mertens.process(processed_images)
|
||||
|
||||
logger.info(f"Mertens raw output range: [{hdr_radiance.min():.6f}, {hdr_radiance.max():.6f}]")
|
||||
|
||||
# Mertens produces natural results, enhance for HDR without breaking appearance
|
||||
# Mertens output is typically in 0-1 range, gently scale for HDR
|
||||
if hdr_radiance.max() <= 1.0:
|
||||
# Boost but keep natural look - this should make it DIFFERENT from other algorithms
|
||||
hdr_radiance = hdr_radiance * 2.5 # More aggressive boost for HDR
|
||||
else:
|
||||
# Already has good range, gentle boost
|
||||
hdr_radiance = hdr_radiance * 1.8
|
||||
hdr_radiance = hdr_radiance * 1.5 # Gentle boost, preserve contrast
|
||||
|
||||
elif algorithm == "natural_blend":
|
||||
# Natural Blend - EV0 appearance but this should be DIFFERENT from Mertens
|
||||
# Natural Blend - maintains EV0 appearance with enhanced dynamic range
|
||||
logger.info("Using Natural Blend exposure blending...")
|
||||
hdr_radiance = self._blend_ev0_based(processed_images, times)
|
||||
|
||||
logger.info(f"Natural Blend raw output range: [{hdr_radiance.min():.6f}, {hdr_radiance.max():.6f}]")
|
||||
|
||||
# Natural blend should maintain EV0 look but add HDR data
|
||||
# This should produce DIFFERENT results from Mertens
|
||||
hdr_radiance = hdr_radiance * 1.2 # Gentle boost to add HDR headroom
|
||||
|
||||
elif algorithm == "robertson":
|
||||
# Robertson algorithm - should be DIFFERENT from Debevec
|
||||
# Robertson algorithm - alternative to Debevec
|
||||
logger.info("Using Robertson algorithm...")
|
||||
response = self.calibrator_robertson.process(processed_images, times)
|
||||
hdr_radiance = self.merge_robertson.process(processed_images, times, response)
|
||||
|
||||
logger.info(f"Robertson raw output range: [{hdr_radiance.min():.6f}, {hdr_radiance.max():.6f}]")
|
||||
|
||||
# Apply minimal processing - this should be DIFFERENT from Debevec
|
||||
# Apply minimal tone mapping to preserve HDR range
|
||||
hdr_radiance = self._gentle_tone_map(hdr_radiance, "Robertson")
|
||||
|
||||
else: # Default to Debevec - should be DIFFERENT from Robertson
|
||||
else: # Default to Debevec
|
||||
# Estimate camera response function using Debevec method
|
||||
logger.info("Using Debevec algorithm...")
|
||||
response = self.calibrator.process(processed_images, times)
|
||||
@@ -209,10 +193,7 @@ class DebevecHDRProcessor:
|
||||
|
||||
# Merge images into HDR using Debevec algorithm
|
||||
hdr_radiance = self.merge_debevec.process(processed_images, times, response)
|
||||
|
||||
logger.info(f"Debevec raw output range: [{hdr_radiance.min():.6f}, {hdr_radiance.max():.6f}]")
|
||||
|
||||
# Apply minimal processing - should produce DIFFERENT results from Robertson
|
||||
# Apply minimal tone mapping to preserve HDR range
|
||||
hdr_radiance = self._gentle_tone_map(hdr_radiance, "Debevec")
|
||||
|
||||
# Validate HDR output
|
||||
@@ -369,10 +350,8 @@ class DebevecHDRProcessor:
|
||||
|
||||
logger.info("Natural Blend completed - appearance preserved with enhanced dynamic range")
|
||||
|
||||
# Return in float32 format - DO NOT CLIP TO 0-1! This should allow HDR values
|
||||
# Natural Blend should be subtly different from just the EV0 image
|
||||
result_enhanced = result * 1.1 # Small boost to differentiate from pure EV0
|
||||
return np.clip(result_enhanced, 0.0, 5.0).astype(np.float32) # Allow values above 1.0
|
||||
# Return in float32 format (0-1 range) for consistency
|
||||
return np.clip(result, 0.0, 1.0).astype(np.float32)
|
||||
|
||||
def _create_highlight_mask(self, gray_image: np.ndarray, threshold: float = 0.8) -> np.ndarray:
|
||||
"""Create a mask for highlight areas that need detail recovery"""
|
||||
@@ -586,14 +565,154 @@ class LuminanceStackProcessor5Stops:
|
||||
return (ev_0,)
|
||||
|
||||
|
||||
class HDRExportNode:
|
||||
"""
|
||||
ComfyUI Custom Node for exporting HDR images to EXR format
|
||||
Preserves full dynamic range data without normalization
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"hdr_image": ("IMAGE", {"tooltip": "HDR image tensor with values potentially above 1.0"}),
|
||||
"filename_prefix": ("STRING", {"default": "HDR_Export", "tooltip": "Filename prefix for the EXR file"}),
|
||||
"output_path": ("STRING", {"default": "", "tooltip": "Output directory (leave empty for ComfyUI output folder)"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("filepath",)
|
||||
FUNCTION = "export_hdr"
|
||||
CATEGORY = "Luminance Stack Processor"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def export_hdr(self, hdr_image: torch.Tensor, filename_prefix: str = "HDR_Export", output_path: str = ""):
|
||||
"""
|
||||
Export HDR image to EXR format preserving full dynamic range
|
||||
|
||||
Args:
|
||||
hdr_image: HDR image tensor (potentially with values > 1.0)
|
||||
filename_prefix: Base filename for the output
|
||||
output_path: Output directory path
|
||||
|
||||
Returns:
|
||||
Tuple containing the filepath of saved EXR
|
||||
"""
|
||||
try:
|
||||
# Convert tensor to numpy array
|
||||
if len(hdr_image.shape) == 4:
|
||||
hdr_image = hdr_image.squeeze(0) # Remove batch dimension
|
||||
|
||||
hdr_array = hdr_image.cpu().numpy()
|
||||
|
||||
logger.info(f"HDR Export: Input range [{hdr_array.min():.6f}, {hdr_array.max():.6f}]")
|
||||
logger.info(f"HDR Export: Shape {hdr_array.shape}, dtype {hdr_array.dtype}")
|
||||
|
||||
# Determine output path
|
||||
if not output_path or output_path.strip() == "":
|
||||
# Use ComfyUI's output directory
|
||||
try:
|
||||
import folder_paths
|
||||
output_dir = folder_paths.get_output_directory()
|
||||
except ImportError:
|
||||
# Fallback if folder_paths module doesn't exist
|
||||
output_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "output")
|
||||
except Exception:
|
||||
# Secondary fallback
|
||||
import os as os_module
|
||||
output_dir = os_module.path.join(os_module.getcwd(), "output")
|
||||
else:
|
||||
output_dir = output_path.strip()
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Generate unique filename with timestamp
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"{filename_prefix}_{timestamp}.exr"
|
||||
filepath = os.path.join(output_dir, filename)
|
||||
|
||||
logger.info(f"HDR Export: Saving to {filepath}")
|
||||
|
||||
# Convert BGR to RGB if needed (OpenCV uses BGR)
|
||||
if len(hdr_array.shape) == 3 and hdr_array.shape[2] == 3:
|
||||
# Assume ComfyUI tensors are RGB, but OpenCV expects BGR for writing
|
||||
hdr_bgr = cv2.cvtColor(hdr_array.astype(np.float32), cv2.COLOR_RGB2BGR)
|
||||
else:
|
||||
hdr_bgr = hdr_array.astype(np.float32)
|
||||
|
||||
# Save as EXR using OpenCV - this preserves HDR data!
|
||||
success = cv2.imwrite(filepath, hdr_bgr)
|
||||
|
||||
if success:
|
||||
# Verify the saved file
|
||||
saved_stats = self._get_file_stats(filepath)
|
||||
logger.info(f"HDR Export: Successfully saved EXR")
|
||||
logger.info(f" File: {filepath}")
|
||||
logger.info(f" Size: {saved_stats['size_mb']:.2f} MB")
|
||||
logger.info(f" Dimensions: {saved_stats['width']}x{saved_stats['height']}")
|
||||
logger.info(f" Channels: {saved_stats['channels']}")
|
||||
|
||||
# Verify HDR values are preserved
|
||||
test_read = cv2.imread(filepath, cv2.IMREAD_ANYCOLOR | cv2.IMREAD_ANYDEPTH)
|
||||
if test_read is not None:
|
||||
logger.info(f" Verification: Saved range [{test_read.min():.6f}, {test_read.max():.6f}]")
|
||||
if test_read.max() > 1.0:
|
||||
logger.info(f" ✅ HDR data preserved (max value: {test_read.max():.2f})")
|
||||
else:
|
||||
logger.warning(f" ⚠️ HDR data may be clipped (max value: {test_read.max():.2f})")
|
||||
|
||||
return (filepath,)
|
||||
else:
|
||||
error_msg = f"Failed to save EXR file to {filepath}"
|
||||
logger.error(f"HDR Export: {error_msg}")
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"HDR Export failed: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
raise RuntimeError(error_msg)
|
||||
|
||||
def _get_file_stats(self, filepath: str) -> dict:
|
||||
"""Get statistics about the saved file"""
|
||||
try:
|
||||
# File size
|
||||
size_bytes = os.path.getsize(filepath)
|
||||
size_mb = size_bytes / (1024 * 1024)
|
||||
|
||||
# Image dimensions using OpenCV
|
||||
img = cv2.imread(filepath, cv2.IMREAD_ANYCOLOR | cv2.IMREAD_ANYDEPTH)
|
||||
if img is not None:
|
||||
height, width = img.shape[:2]
|
||||
channels = img.shape[2] if len(img.shape) > 2 else 1
|
||||
else:
|
||||
width = height = channels = 0
|
||||
|
||||
return {
|
||||
'size_mb': size_mb,
|
||||
'width': width,
|
||||
'height': height,
|
||||
'channels': channels
|
||||
}
|
||||
except Exception:
|
||||
return {
|
||||
'size_mb': 0,
|
||||
'width': 0,
|
||||
'height': 0,
|
||||
'channels': 0
|
||||
}
|
||||
|
||||
|
||||
# Node class mappings for ComfyUI
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"LuminanceStackProcessor3Stops": LuminanceStackProcessor3Stops,
|
||||
"LuminanceStackProcessor5Stops": LuminanceStackProcessor5Stops,
|
||||
"HDRExportNode": HDRExportNode
|
||||
}
|
||||
|
||||
# Display names for nodes in ComfyUI interface
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"LuminanceStackProcessor3Stops": "Luminance Stack Processor (3 Stops)",
|
||||
"LuminanceStackProcessor5Stops": "Luminance Stack Processor (5 Stops)",
|
||||
"HDRExportNode": "HDR Export to EXR"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user