Multiple architectural upgrades Enhanced Algorithms

This commit is contained in:
sumitchatterjee13
2025-09-21 01:45:03 +10:00
parent 16e772fd1d
commit ec9c8244bf
3 changed files with 146 additions and 24 deletions
+7 -4
View File
@@ -7,19 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Nothing yet
### Added
- **NEW: EV0-Based Blending Algorithm** - Now default! Preserves exact EV0 appearance with enhanced dynamic range
- **Smart luminance masking** - Seamless highlight and shadow detail recovery
### Changed
- **Optimized for 8-bit input workflow** - Removed unnecessary 8-bit conversion since inputs are already 8-bit
- **Enhanced 16-bit linear output** - Improved scaling algorithm for better 16-bit range utilization
- **Changed default algorithm** - EV0-Based is now default instead of Mertens for natural results
### Fixed
- **CRITICAL: Fixed contrast issues** - EV0-Based algorithm maintains original image contrast
- **CRITICAL: Fixed color inversion issues** - Added proper BGR↔RGB conversion for correct color handling
- **Fixed brightness problems** - Improved scaling and algorithm selection
- **Added Mertens Exposure Fusion algorithm** - Now default, produces better results like Adobe Lightroom
- **Added Mertens Exposure Fusion algorithm** - Alternative for Lightroom-style results
- **Added Robertson algorithm** - Alternative HDR method for different use cases
- **Enhanced algorithm selection** - Users can choose between Mertens (default), Debevec, and Robertson
- **Enhanced algorithm selection** - Users can choose between EV0-Based (default), Mertens, Debevec, and Robertson
- **CRITICAL: Fixed Debevec algorithm issues** - Added Reinhard tone mapping to fix brightness/color inversion
- **Improved Debevec output** - Now produces natural results similar to Mertens
- **Enhanced per-algorithm scaling** - Different scaling strategies for different algorithms
+23 -14
View File
@@ -99,7 +99,7 @@ For extended dynamic range with 5 exposures:
## 🔬 How It Works
The nodes implement **multiple HDR algorithms** with **Mertens Exposure Fusion** as the default (often produces better results):
The nodes implement **multiple HDR algorithms** with **EV0-Based Blending** 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
@@ -109,11 +109,17 @@ The nodes implement **multiple HDR algorithms** with **Mertens Exposure Fusion**
### 🎯 **HDR Algorithm Options:**
#### **Mertens Exposure Fusion (Default - Recommended)**
- **Best for most use cases**: Produces natural-looking results similar to Adobe Lightroom
- **No tone mapping needed**: Output looks like enhanced EV0 with extended dynamic range
#### **EV0-Based Blending (Default - Recommended)**
- **Perfect for natural look**: Uses EV0 as base appearance, adds dynamic range from other exposures
- **Zero contrast change**: Output looks exactly like EV0 with enhanced highlight/shadow detail
- **Seamless blending**: Uses luminance masks for smooth transitions
- **Best of both worlds**: Natural EV0 appearance + extended dynamic range data
#### **Mertens Exposure Fusion**
- **Good alternative**: Produces natural-looking results similar to Adobe Lightroom
- **Automatic blending**: No tone mapping needed, enhanced EV0-like appearance
- **Fastest processing**: No camera response function estimation required
- **Color accuracy**: Superior color handling, avoids common HDR artifacts
- **May add contrast**: Can make images slightly more contrasty than original
#### **Debevec Algorithm (Classic HDR)**
- **Industry standard**: Original HDR reconstruction method from 1997
@@ -140,13 +146,14 @@ The nodes implement **multiple HDR algorithms** with **Mertens Exposure Fusion**
- Adjust `exposure_step` parameter if using different increments
### Algorithm Selection Guide:
- **Use Mertens (Default)**: For most photography - produces natural results like Lightroom
- **Use Debevec**: For scientific/research work requiring precise scene radiance values
- **Use EV0-Based (Default)**: When you want to keep the exact look of your EV0 image with enhanced dynamic range
- **Use Mertens**: For natural HDR look similar to Adobe Lightroom (may add some contrast)
- **Use Debevec**: For scientific/research work requiring precise scene radiance values
- **Use Robertson**: Alternative to Debevec with different mathematical approach
## ⚙️ Technical Details
- **Algorithms**: Mertens Exposure Fusion (default), Debevec, Robertson
- **Algorithms**: EV0-Based Blending (default), Mertens Exposure Fusion, Debevec, Robertson
- **Input Format**: 8-bit ComfyUI IMAGE tensors (0-1 float range from 8-bit sources)
- **Output Format**: 16-bit linear colorspace images for extended dynamic range
- **Processing**: OpenCV's `createCalibrateDebevec()` and `createMergeDebevec()`
@@ -162,21 +169,23 @@ The nodes implement **multiple HDR algorithms** with **Mertens Exposure Fusion**
- Restart ComfyUI completely
2. **Color inversion or wrong colors**:
- **Mertens algorithm** (default) - produces natural colors, no issues expected
- **EV0-Based algorithm** (default) - uses original EV0 colors, no issues expected
- **Mertens algorithm** - produces natural colors, no issues expected
- **Debevec algorithm** - now includes automatic Reinhard tone mapping for proper colors
- **Robertson algorithm** - also includes tone mapping for natural appearance
3. **Image too bright or too dark**:
- **All algorithms** now produce proper brightness levels automatically
- **Mertens** (default) - natural brightness similar to Adobe Lightroom
- **EV0-Based** (default) - keeps exact brightness of your EV0 image
- **All other algorithms** produce proper brightness levels automatically
- **Mertens** - natural brightness similar to Adobe Lightroom
- **Debevec/Robertson** - include automatic Reinhard tone mapping for proper brightness
- Output should look like enhanced EV0 with extended dynamic range
4. **Poor HDR results**:
- Ensure input images are properly exposed (not all over/under)
- Check that images are aligned (use tripod)
- Check that images are aligned (use tripod)
- Verify EV differences match your capture method
- Try different algorithms: Mertens usually works best
- **For EV0-Based**: Make sure the EV0 image is your desired base appearance
- Try different algorithms: EV0-Based preserves natural look, Mertens for more artistic results
5. **Memory issues**:
- Process smaller images first
+116 -6
View File
@@ -49,7 +49,11 @@ def cv2_to_tensor(hdr_image: np.ndarray, output_16bit_linear: bool = True, algor
# Scale to 16-bit range while preserving linear characteristics
# Different scaling strategies based on algorithm
if algorithm_hint == "mertens":
if algorithm_hint == "ev0_base":
# EV0-based should have similar range to original images, use 85th percentile
scale_reference = np.percentile(hdr_image, 85.0)
target_scale = 0.85 # 85% of 16-bit range
elif algorithm_hint == "mertens":
# Mertens typically produces 0-2 range, use 90th percentile
scale_reference = np.percentile(hdr_image, 90.0)
target_scale = 0.8 # 80% of 16-bit range
@@ -73,7 +77,16 @@ def cv2_to_tensor(hdr_image: np.ndarray, output_16bit_linear: bool = True, algor
logger.info(f"HDR 16-bit linear output ({algorithm_hint}):")
logger.info(f" Original range: [{hdr_image.min():.6f}, {hdr_image.max():.6f}]")
logger.info(f" Scale reference ({90 if algorithm_hint == 'mertens' else 95}th percentile): {scale_reference:.6f}")
# Choose percentile based on algorithm
if algorithm_hint == "ev0_base":
percentile_used = 85
elif algorithm_hint == "mertens":
percentile_used = 90
else:
percentile_used = 95
logger.info(f" Scale reference ({percentile_used}th percentile): {scale_reference:.6f}")
logger.info(f" 16-bit range: [{hdr_16bit_linear.min():.1f}, {hdr_16bit_linear.max():.1f}]")
logger.info(f" Final range: [{normalized_16bit.min():.6f}, {normalized_16bit.max():.6f}]")
logger.info(f" Scale factor: {scale_factor:.2f}")
@@ -148,6 +161,11 @@ class DebevecHDRProcessor:
# Mertens output is typically in 0-1 range, scale appropriately
if hdr_radiance.max() <= 1.0:
hdr_radiance = hdr_radiance * 2.0 # Boost for better dynamic range
elif algorithm == "ev0_base":
# EV0-based exposure blending - maintains EV0 appearance with enhanced dynamic range
logger.info("Using EV0-based exposure blending...")
hdr_radiance = self._blend_ev0_based(processed_images, times)
elif algorithm == "robertson":
# Robertson algorithm - alternative to Debevec
@@ -251,6 +269,98 @@ class DebevecHDRProcessor:
normalized = hdr_image / np.percentile(hdr_image, 95)
return np.clip(normalized, 0.0, 5.0).astype(np.float32)
return hdr_image.astype(np.float32)
def _blend_ev0_based(self, images: List[np.ndarray], times: List[float]) -> np.ndarray:
"""
Blend exposures using EV0 as base appearance with enhanced dynamic range
This method preserves the exact look of the EV0 image while adding
highlight and shadow detail from other exposures.
Args:
images: List of exposure images (should be ordered from overexposed to underexposed)
times: Exposure times
Returns:
Enhanced image that looks like EV0 but with extended dynamic range
"""
logger.info("EV0-based blending: Preserving EV0 appearance with enhanced dynamic range")
# Find the EV0 image (middle exposure - should be the normal exposure)
ev0_idx = len(images) // 2 # Middle image is typically EV0
ev0_base = images[ev0_idx].astype(np.float32) / 255.0
logger.info(f"Using image {ev0_idx} as EV0 base (out of {len(images)} images)")
# Convert all images to float for processing
float_images = [img.astype(np.float32) / 255.0 for img in images]
# Create luminance masks for blending
ev0_gray = cv2.cvtColor(ev0_base, cv2.COLOR_RGB2GRAY)
# Start with EV0 as the base result
result = ev0_base.copy()
# Blend highlights from underexposed images (better highlight detail)
for i in range(ev0_idx + 1, len(float_images)):
img = float_images[i]
# Create highlight mask - where EV0 is bright but this image has detail
highlight_mask = self._create_highlight_mask(ev0_gray)
# Blend highlight areas
for c in range(3): # RGB channels
result[:, :, c] = (1 - highlight_mask) * result[:, :, c] + highlight_mask * img[:, :, c]
logger.info(f"Blended highlights from underexposed image {i}")
# Blend shadows from overexposed images (better shadow detail)
for i in range(ev0_idx):
img = float_images[i]
# Create shadow mask - where EV0 is dark but this image has detail
shadow_mask = self._create_shadow_mask(ev0_gray)
# Blend shadow areas
for c in range(3): # RGB channels
result[:, :, c] = (1 - shadow_mask) * result[:, :, c] + shadow_mask * img[:, :, c]
logger.info(f"Blended shadows from overexposed image {i}")
# Convert back to 0-255 range for consistency with other algorithms
result_8bit = np.clip(result * 255.0, 0, 255).astype(np.uint8)
logger.info("EV0-based blending completed - appearance preserved with enhanced dynamic range")
return result_8bit.astype(np.float32) / 255.0 # Return as float32 in 0-1 range
def _create_highlight_mask(self, gray_image: np.ndarray, threshold: float = 0.7) -> np.ndarray:
"""Create a mask for highlight areas that need detail recovery"""
# Smooth transition for highlights
mask = np.zeros_like(gray_image, dtype=np.float32)
# Areas above threshold get progressively more blending
bright_areas = gray_image > threshold
mask[bright_areas] = (gray_image[bright_areas] - threshold) / (1.0 - threshold)
# Smooth the mask to avoid harsh transitions
mask = cv2.GaussianBlur(mask, (21, 21), 0)
return np.clip(mask, 0, 1)
def _create_shadow_mask(self, gray_image: np.ndarray, threshold: float = 0.3) -> np.ndarray:
"""Create a mask for shadow areas that need detail recovery"""
# Smooth transition for shadows
mask = np.zeros_like(gray_image, dtype=np.float32)
# Areas below threshold get progressively more blending
dark_areas = gray_image < threshold
mask[dark_areas] = (threshold - gray_image[dark_areas]) / threshold
# Smooth the mask to avoid harsh transitions
mask = cv2.GaussianBlur(mask, (21, 21), 0)
return np.clip(mask, 0, 1)
class LuminanceStackProcessor3Stops:
@@ -275,8 +385,8 @@ class LuminanceStackProcessor3Stops:
"step": 0.1,
"display": "number"
}),
"hdr_algorithm": (["mertens", "debevec", "robertson"], {
"default": "mertens"
"hdr_algorithm": (["ev0_base", "mertens", "debevec", "robertson"], {
"default": "ev0_base"
}),
}
}
@@ -289,7 +399,7 @@ class LuminanceStackProcessor3Stops:
def __init__(self):
self.processor = DebevecHDRProcessor()
def process_3_stop_hdr(self, ev_plus_2, ev_0, ev_minus_2, exposure_step=2.0, hdr_algorithm="mertens"):
def process_3_stop_hdr(self, ev_plus_2, ev_0, ev_minus_2, exposure_step=2.0, hdr_algorithm="ev0_base"):
"""
Process 3-stop HDR merge
@@ -373,7 +483,7 @@ class LuminanceStackProcessor5Stops:
def __init__(self):
self.processor = DebevecHDRProcessor()
def process_5_stop_hdr(self, ev_plus_4, ev_plus_2, ev_0, ev_minus_2, ev_minus_4, exposure_step=2.0, hdr_algorithm="mertens"):
def process_5_stop_hdr(self, ev_plus_4, ev_plus_2, ev_0, ev_minus_2, ev_minus_4, exposure_step=2.0, hdr_algorithm="ev0_base"):
"""
Process 5-stop HDR merge