Multiple architectural upgrades Enhanced Algorithms
This commit is contained in:
+7
-4
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user