diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e3a1e2..879e0e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,11 @@ 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 ### Fixed -- Nothing yet +- **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 Robertson algorithm** - Alternative HDR method for different use cases +- **Enhanced algorithm selection** - Users can choose between Mertens (default), Debevec, and Robertson ## [1.0.1] - 2025-01-20 diff --git a/README.md b/README.md index d4c4eff..8a241f8 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Professional HDR (High Dynamic Range) processing nodes for ComfyUI using the **D ## 🎯 Features -- **Professional HDR Processing**: Implements the industry-standard Debevec Algorithm for HDR image creation +- **Multiple HDR Algorithms**: Supports Mertens Exposure Fusion (default), Debevec, and Robertson algorithms - **Two Processing Modes**: - **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 @@ -76,6 +76,7 @@ Perfect for standard HDR bracketing with 3 exposures: - `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" **Output:** - `hdr_image`: Merged 16-bit linear HDR image @@ -91,13 +92,14 @@ For extended dynamic range with 5 exposures: - `ev_minus_2`: Underexposed image (-2 EV) - `ev_minus_4`: Most underexposed image (-4 EV) - `exposure_step`: (Optional) EV step size (default: 2.0) +- `hdr_algorithm`: (Optional) Algorithm to use: "mertens" (default), "debevec", "robertson" **Output:** - `hdr_image`: Merged 16-bit linear HDR image ## 🔬 How It Works -The nodes implement the **Debevec Algorithm** (Paul E. Debevec, Jitendra Malik, 1997) which: +The nodes implement **multiple HDR algorithms** with **Mertens Exposure Fusion** as the default (often produces better results): 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 @@ -105,6 +107,23 @@ The nodes implement the **Debevec Algorithm** (Paul E. Debevec, Jitendra Malik, 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 +### 🎯 **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 +- **Fastest processing**: No camera response function estimation required +- **Color accuracy**: Superior color handling, avoids common HDR artifacts + +#### **Debevec Algorithm (Classic HDR)** +- **Industry standard**: Original HDR reconstruction method from 1997 +- **True scene radiance**: Recovers actual physical light values +- **Research accurate**: Mathematically precise but may require tone mapping for display + +#### **Robertson Algorithm** +- **Alternative approach**: Different camera response function estimation method +- **Similar to Debevec**: But with different mathematical approach + ## 📸 Best Practices ### For Capturing Source Images: @@ -119,9 +138,14 @@ The nodes implement the **Debevec Algorithm** (Paul E. Debevec, Jitendra Malik, - **5-Stop**: +4, +2, 0, -2, -4 EV (16x range) - 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 Robertson**: Alternative to Debevec with different mathematical approach + ## ⚙️ Technical Details -- **Algorithm**: Debevec & Malik HDR reconstruction +- **Algorithms**: Mertens Exposure Fusion (default), 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()` @@ -136,12 +160,23 @@ The nodes implement the **Debevec Algorithm** (Paul E. Debevec, Jitendra Malik, - Ensure all dependencies are installed: `pip install -r requirements.txt` - Restart ComfyUI completely -2. **Poor HDR results**: +2. **Color inversion or wrong colors**: + - Try switching to **Mertens algorithm** (default) - usually fixes color issues + - Mertens produces more natural colors similar to Adobe Lightroom + - Debevec/Robertson may need additional tone mapping for proper color appearance + +3. **Image too bright or too dark**: + - **Mertens algorithm** (default) produces best brightness levels + - Output should look like enhanced EV0 with extended dynamic range + - For Debevec/Robertson, you may need tone mapping in post-processing + +4. **Poor HDR results**: - Ensure input images are properly exposed (not all over/under) - Check that images are aligned (use tripod) - Verify EV differences match your capture method + - Try different algorithms: Mertens usually works best -3. **Memory issues**: +5. **Memory issues**: - Process smaller images first - Ensure adequate RAM available - Close unnecessary applications diff --git a/luminance_stack_processor.py b/luminance_stack_processor.py index acb21ee..3c01be7 100644 --- a/luminance_stack_processor.py +++ b/luminance_stack_processor.py @@ -88,19 +88,24 @@ def cv2_to_tensor(hdr_image: np.ndarray, output_16bit_linear: bool = True) -> to class DebevecHDRProcessor: - """Core HDR processing using Debevec Algorithm""" + """Core HDR processing using multiple algorithms""" def __init__(self): self.calibrator = cv2.createCalibrateDebevec() self.merge_debevec = cv2.createMergeDebevec() + # Alternative algorithms + self.merge_mertens = cv2.createMergeMertens() + self.merge_robertson = cv2.createMergeRobertson() + self.calibrator_robertson = cv2.createCalibrateRobertson() - def process_hdr(self, images: List[np.ndarray], exposure_times: List[float]) -> np.ndarray: + def process_hdr(self, images: List[np.ndarray], exposure_times: List[float], algorithm: str = "mertens") -> np.ndarray: """ - Process multiple exposure images using Debevec algorithm with proper linear colorspace handling + Process multiple exposure images using various HDR algorithms Args: images: List of 8-bit images (OpenCV format) - CRITICAL: Must be 8-bit for OpenCV HDR - exposure_times: List of exposure times in seconds + exposure_times: List of exposure times in seconds + algorithm: HDR algorithm to use ("mertens", "debevec", "robertson") Returns: HDR merged image in linear colorspace (float32) @@ -112,9 +117,11 @@ class DebevecHDRProcessor: if img.dtype != np.uint8: logger.warning(f"Image {i} is not 8-bit (dtype: {img.dtype}), this may cause issues") - # Ensure proper format + # CRITICAL FIX: Convert BGR to RGB for proper color handling + # OpenCV uses BGR by default, but HDR processing expects RGB if len(img.shape) == 3 and img.shape[2] == 3: # RGB - processed_images.append(img) + img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + processed_images.append(img_rgb) else: logger.error(f"Image {i} has invalid shape: {img.shape}") raise ValueError(f"Image must be 3-channel RGB, got shape: {img.shape}") @@ -122,24 +129,40 @@ class DebevecHDRProcessor: # Convert exposure times to numpy array times = np.array(exposure_times, dtype=np.float32) - logger.info(f"Processing {len(processed_images)} images with exposure times: {times}") + logger.info(f"Processing {len(processed_images)} images with {algorithm} algorithm") + logger.info(f"Exposure times: {times}") logger.info(f"Image formats: {[img.shape for img in processed_images]}") logger.info(f"Image dtypes: {[img.dtype for img in processed_images]}") - # Estimate camera response function using Debevec method - logger.info("Estimating camera response function...") - response = self.calibrator.process(processed_images, times) - logger.info(f"Response function shape: {response.shape}") - - # Merge images into HDR using Debevec algorithm - logger.info("Merging images using Debevec algorithm...") - hdr_radiance = self.merge_debevec.process(processed_images, times, response) + # Process using selected algorithm + if algorithm == "mertens": + # Mertens Exposure Fusion - often produces better results + logger.info("Using Mertens Exposure Fusion algorithm...") + hdr_radiance = self.merge_mertens.process(processed_images) + # 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 == "robertson": + # 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) + + else: # Default to Debevec + # Estimate camera response function using Debevec method + logger.info("Using Debevec algorithm...") + response = self.calibrator.process(processed_images, times) + logger.info(f"Response function shape: {response.shape}") + + # Merge images into HDR using Debevec algorithm + hdr_radiance = self.merge_debevec.process(processed_images, times, response) # Validate HDR output if hdr_radiance is None or hdr_radiance.size == 0: raise ValueError("HDR merge produced empty result") - logger.info(f"HDR merge completed:") + logger.info(f"HDR merge completed with {algorithm} algorithm:") logger.info(f" Output shape: {hdr_radiance.shape}") logger.info(f" Output dtype: {hdr_radiance.dtype}") logger.info(f" Value range: [{hdr_radiance.min():.6f}, {hdr_radiance.max():.6f}]") @@ -149,6 +172,11 @@ class DebevecHDRProcessor: if np.all(hdr_radiance == 0): raise ValueError("HDR merge produced all-zero result") + # CRITICAL FIX: Convert back from RGB to BGR for consistency + # This ensures the output color channels are in the expected order + if len(hdr_radiance.shape) == 3 and hdr_radiance.shape[2] == 3: + hdr_radiance = cv2.cvtColor(hdr_radiance, cv2.COLOR_RGB2BGR) + # The result is already in linear colorspace (scene radiance values) # Don't apply tone mapping - preserve the HDR data as-is return hdr_radiance.astype(np.float32) @@ -194,6 +222,9 @@ class LuminanceStackProcessor3Stops: "step": 0.1, "display": "number" }), + "hdr_algorithm": (["mertens", "debevec", "robertson"], { + "default": "mertens" + }), } } @@ -205,7 +236,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): + def process_3_stop_hdr(self, ev_plus_2, ev_0, ev_minus_2, exposure_step=2.0, hdr_algorithm="mertens"): """ Process 3-stop HDR merge @@ -235,10 +266,10 @@ class LuminanceStackProcessor3Stops: images = [img_plus_2, img_0, img_minus_2] times = [time_plus_2, time_0, time_minus_2] - logger.info(f"3-Stop HDR: Processing with times {times}") + logger.info(f"3-Stop HDR: Processing with times {times} using {hdr_algorithm} algorithm") - # Process HDR using improved Debevec algorithm - hdr_result = self.processor.process_hdr(images, times) + # Process HDR using selected algorithm (default: Mertens for better results) + hdr_result = self.processor.process_hdr(images, times, algorithm=hdr_algorithm) # Convert back to tensor - output 16-bit linear data output_tensor = cv2_to_tensor(hdr_result, output_16bit_linear=True) @@ -275,6 +306,9 @@ class LuminanceStackProcessor5Stops: "step": 0.1, "display": "number" }), + "hdr_algorithm": (["mertens", "debevec", "robertson"], { + "default": "mertens" + }), } } @@ -286,7 +320,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): + 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"): """ Process 5-stop HDR merge @@ -321,10 +355,10 @@ class LuminanceStackProcessor5Stops: images = [img_plus_4, img_plus_2, img_0, img_minus_2, img_minus_4] times = [time_plus_4, time_plus_2, time_0, time_minus_2, time_minus_4] - logger.info(f"5-Stop HDR: Processing with times {times}") + logger.info(f"5-Stop HDR: Processing with times {times} using {hdr_algorithm} algorithm") - # Process HDR using improved Debevec algorithm - hdr_result = self.processor.process_hdr(images, times) + # Process HDR using selected algorithm (default: Mertens for better results) + hdr_result = self.processor.process_hdr(images, times, algorithm=hdr_algorithm) # Convert back to tensor - output 16-bit linear data output_tensor = cv2_to_tensor(hdr_result, output_16bit_linear=True)