-
Notifications
You must be signed in to change notification settings - Fork 0
FAQ
Common questions and solutions for DTUTMO users.
DTUTMO (DTU Tone Mapping Operator) is a biologically-inspired HDR tone mapping framework that simulates the complete human visual processing pipeline. Unlike simple compression operators, it models optical phenomena, photoreceptor responses, neural processing, and perceptual appearance to produce physiologically-accurate tone mapping.
- HDR photographers - Professional tone mapping with color accuracy
- VR/AR developers - Accurate rendering of wide luminance ranges
- Medical imaging - Diagnostic display of HDR scans
- Researchers - Validated components for vision science
- Display engineers - Cross-display appearance matching
- Complete physiological model: Simulates the entire visual pipeline
- Hood adaptation: Power-law adaptation with adaptive gain control
- DTUCAM: First CAM with physiologically-consistent dual adaptation
- Explicit inverses: O(1) formulas eliminate iteration
- Hybrid mapping: Gradient-adaptive blending (2.5-4× speedup)
- Extreme range: Handles 0.001 to 100,000 cd/m² (9+ orders)
- Validated: Each stage independently validated against psychophysics
Yes! DTUTMO is designed for production use:
- No parameter tuning required (validated defaults)
- Real-time performance (GPU acceleration)
- Robust across diverse content
- Stable numerical behavior
- Well-documented API
Problem: Package not installed correctly.
Solutions:
# Option 1: Install from source
git clone https://github.com/TSOR666/DTUCAM25.git
cd dtutmo
pip install -e .
# Option 2: Verify installation
python -c "import dtutmo; print(dtutmo.__version__)"Yes! DTUTMO works perfectly on CPU. GPU acceleration is optional:
- CPU: Works out of the box with NumPy/SciPy
- GPU: Install PyTorch for 10-50× speedup
# CPU only
pip install numpy scipy
# With GPU support
pip install "torch>=2.5.1" "torchvision>=0.20.1"Python 3.10 or higher is required. Tested on:
- Python 3.10
- Python 3.11
- Python 3.12
Problem: Incorrect input units or scaling.
Solution: DTUTMO expects physical luminance in cd/m²:
# If your HDR is in relative units [0, 1]
hdr_physical = hdr_relative * peak_luminance # e.g., * 1000.0
# Process
ldr = tmo.process(hdr_physical)Check input range:
print(f"Min: {hdr.min()}, Max: {hdr.max()}, Mean: {hdr.mean()}")
# Expected: Min ~0.01-1, Max ~100-10000, Mean ~10-100Problem: Input is gamma-encoded instead of linear.
Solution: Linearize sRGB gamma before processing:
def linearize_srgb(img_gamma):
"""Convert sRGB gamma to linear RGB"""
linear = img_gamma.copy()
low = img_gamma <= 0.04045
linear[low] = img_gamma[low] / 12.92
linear[~low] = ((img_gamma[~low] + 0.055) / 1.055) ** 2.4
return linear
hdr_linear = linearize_srgb(hdr_gamma)
ldr = tmo.process(hdr_linear)Possible causes and solutions:
- Bilateral filtering artifacts:
# Adjust sigma or disable
config = DTUTMOConfig(use_bilateral=False)- Glare PSF ringing:
# Use smaller field diameter
config = DTUTMOConfig(field_diameter=30)- Gradient map artifacts in hybrid:
# Increase smoothing
config = DTUTMOConfig(
display_mapping=DisplayMapping.PRODUCTION_HYBRID,
gradient_smoothing=3.0
)Output is in [0, 1] range, ready for 8-bit encoding:
from PIL import Image
import numpy as np
# Tone map
ldr = tmo.process(hdr)
# Convert to 8-bit
img_8bit = (np.clip(ldr, 0, 1) * 255).astype(np.uint8)
# Save
Image.fromarray(img_8bit).save('output.png')Solutions in order of impact:
- Use GPU acceleration (10-50× speedup):
import torch
from dtutmo import TorchDTUTMO
tmo = TorchDTUTMO().cuda()- Use faster display mapping:
config = DTUTMOConfig(
display_mapping=DisplayMapping.WHITEBOARD # 3× faster
)- Disable optional stages:
config = DTUTMOConfig(
use_otf=False, # Skip optical blur
use_glare=False, # Skip glare
use_bilateral=False # Skip detail separation
)- Reduce resolution:
# Downscale for processing
scale = 0.5
h, w = int(hdr.shape[0] * scale), int(hdr.shape[1] * scale)
hdr_small = cv2.resize(hdr, (w, h))
# Process
ldr_small = tmo.process(hdr_small)
# Upscale
ldr = cv2.resize(ldr_small, (hdr.shape[1], hdr.shape[0]))Solutions:
- Process tiles:
def process_tiled(tmo, hdr, tile_size=512, overlap=64):
"""Process image in overlapping tiles"""
h, w = hdr.shape[:2]
output = np.zeros((h, w, 3))
for y in range(0, h, tile_size - overlap):
for x in range(0, w, tile_size - overlap):
# Extract tile with overlap
y_end = min(y + tile_size, h)
x_end = min(x + tile_size, w)
tile = hdr[y:y_end, x:x_end]
# Process
tile_out = tmo.process(tile)
# Blend into output (handle overlap)
output[y:y_end, x:x_end] = tile_out
return output- Reduce image size:
# Process at lower resolution
hdr_small = cv2.resize(hdr, None, fx=0.5, fy=0.5)- Disable bilateral:
config = DTUTMOConfig(use_bilateral=False)- Use GPU (larger memory pool):
tmo = TorchDTUTMO().cuda()Solutions:
- Reduce batch size:
# Process one image at a time
for img in images:
output = tmo.process(img.unsqueeze(0))- Use mixed precision:
with torch.cuda.amp.autocast():
output = tmo.process(input)- Clear cache:
import torch
torch.cuda.empty_cache()Decision tree:
Luminance range?
├─ >16,860 cd/m² → CAMType.DTUCAM
├─ 0.01-16,860 → CAMType.XLRCAM
└─ 0.1-1,000 → CAMType.CIECAM16
Need CIE standard? → CAMType.CIECAM16
Need speed? → CAMType.DTUCAM (explicit inverse)
Need physiological accuracy? → CAMType.DTUCAM
Don't need cross-media? → CAMType.NONE (fastest)
Recommendation: Start with CAMType.DTUCAM
| Use Case | Mapping | Why |
|---|---|---|
| Production | PRODUCTION_HYBRID |
Best quality/speed |
| Preview | WHITEBOARD |
Fastest |
| Research | FULL_INVERSE |
Most accurate |
| Legacy | LEGACY |
v1.x compatibility |
Recommendation: PRODUCTION_HYBRID for almost everything
# HDR10 (PQ)
config = DTUTMOConfig(
target_display=DisplayStandard.REC_2100_PQ,
target_luminance=1000.0, # Your display's peak
viewing_condition=ViewingCondition.DIM,
)
# HLG
config = DTUTMOConfig(
target_display=DisplayStandard.REC_2100_HLG,
target_luminance=1000.0,
viewing_condition=ViewingCondition.DIM,
)Based on ambient lighting:
# Dark room (cinema, <0.2 cd/m²)
config = DTUTMOConfig(viewing_condition=ViewingCondition.DARK)
# Dim room (home theater, 0.2-20 cd/m²)
config = DTUTMOConfig(viewing_condition=ViewingCondition.DIM)
# Bright room (office, >20 cd/m²)
config = DTUTMOConfig(viewing_condition=ViewingCondition.AVERAGE)Hood & Finkelstein (1986) showed that the semi-saturation parameter σ is NOT constant but increases with adaptation:
σ(I_a) = k₁ · ((O₁ + I_a) / O₁)^m
Additionally, DTUTMO implements adaptive gain control:
R_max(I_a) = k₂ · [(O₂ + p·I_a) / O₂]^(-1/2)
This dual adaptation (σ + R_max) enables accurate responses across scotopic to photopic vision (10⁻³ to 10⁴ Td).
See Pipeline Stages - Stage 8 for details.
Rods and cones have different spectral sensitivities:
- Cones: Peak at 555nm (yellowish-green)
- Rods: Peak at 507nm (bluish-green)
This 48nm difference explains why blue-green appears brighter in dim conditions. DTUTMO accounts for this by extracting separate rod signals:
L_scotopic = 0.05·R + 0.85·G + 0.10·BCommon error: Treating RGB channels as LMS cone responses.
Correct approach: Two-stage transformation through CIE XYZ ensures physiologically-accurate cone fundamentals:
RGB (device) → XYZ (standard) → LMS (physiological)
This accounts for:
- Device-dependent primaries
- Standard illuminant (D65)
- Physiological cone spectral sensitivities
See Pipeline Stages - Stage 3 for details.
Observation: Full inverse accuracy needed primarily at edges.
Strategy: Compute both methods, blend based on gradient:
L_d = (1 - w) · L_whiteboard + w · L_full_inverse
Where w ∈ [0,1] depends on gradient magnitude.
Result:
- 70-90% of pixels use fast whiteboard
- 10-30% use accurate inverse
- 2.5-4× speedup vs full inverse
- Identical visual quality
See Display Mapping for details.
| Feature | DTUTMO | Reinhard |
|---|---|---|
| Adaptation | Hood (physiological) | Global/local heuristic |
| Photoreceptor | Full model | None |
| Spectral | RGB→XYZ→LMS | RGB direct |
| Validation | Per-stage | Subjective |
| Speed | Fast (hybrid) | Very fast |
When to use Reinhard: Need simplest possible method, don't care about physiological accuracy.
When to use DTUTMO: Need accurate color, cross-display matching, validated components.
| Feature | DTUTMO | Mantiuk |
|---|---|---|
| Basis | Physiology | Contrast perception |
| Metric | Custom stages | HDR-VDP-2 |
| Adaptation | Hood model | Simplified |
| Speed | Fast | Slow (iterative) |
When to use Mantiuk: Optimizing for HDR-VDP-2 metric.
When to use DTUTMO: Need physiological accuracy, faster processing, modular pipeline.
| Feature | DTUTMO | Photographic |
|---|---|---|
| Model | Physiological | Exposure adjustment |
| Parameters | Validated defaults | User tuning required |
| Color | Accurate | Subjective |
| Automation | Fully automatic | Manual adjustment |
When to use Photographic: Want artistic control, film look.
When to use DTUTMO: Want consistent results, color accuracy, no manual tuning.
from pathlib import Path
from dtutmo import CompleteDTUTMO
def process_directory(input_dir, output_dir, config=None):
"""Process all HDR images in directory"""
tmo = CompleteDTUTMO(config)
input_path = Path(input_dir)
output_path = Path(output_dir)
output_path.mkdir(exist_ok=True)
for hdr_file in input_path.glob("*.exr"):
print(f"Processing {hdr_file.name}...")
hdr = load_hdr(hdr_file)
ldr = tmo.process(hdr)
output_file = output_path / f"{hdr_file.stem}.png"
save_image(output_file, ldr)
# Usage
process_directory("hdr_images/", "output/")Yes! Use return_intermediate=True:
result = tmo.process(hdr_image, return_intermediate=True)
# Access stages
adaptation = result['local_adaptation']
cone_L = result['photoreceptor_responses']['cones']['L']
cone_M = result['photoreceptor_responses']['cones']['M']
cone_S = result['photoreceptor_responses']['cones']['S']
rod = result['photoreceptor_responses']['rod']
csf_output = result['csf_output']
display_output = result['output']
# Visualize
import matplotlib.pyplot as plt
plt.imshow(adaptation, cmap='viridis')
plt.colorbar(label='Adaptation (cd/m²)')
plt.show()import time
# Profile total time
start = time.time()
output = tmo.process(hdr_image)
total_time = time.time() - start
print(f"Total: {total_time:.3f}s")
# Profile with intermediate stages
result = tmo.process(hdr_image, return_intermediate=True)
# Each stage timing is in result['timings']For detailed profiling:
import cProfile
import pstats
profiler = cProfile.Profile()
profiler.enable()
output = tmo.process(hdr_image)
profiler.disable()
stats = pstats.Stats(profiler)
stats.sort_stats('cumulative')
stats.print_stats(20) # Top 20 functionsYes! DTUTMO is modular:
from dtutmo.optics import apply_otf
from dtutmo.photoreceptors import CorrectedPhotoreceptorResponse
from dtutmo.display import HybridDisplayMapper
# Custom pipeline
img = apply_otf(hdr_image, age=30, ppd=45)
# ... custom processing ...
photo = CorrectedPhotoreceptorResponse()
responses = photo.compute_response(img, adapt, pupil, 'L_cone')
# ... more stages ...
mapper = HybridDisplayMapper()
output = mapper.map_to_display(responses, adapt, config)import logging
logging.basicConfig(level=logging.DEBUG)
from dtutmo import CompleteDTUTMO
tmo = CompleteDTUTMO()
output = tmo.process(hdr_image) # Will print debug inforesult = tmo.process(hdr_image, return_intermediate=True)
# Check for NaN/Inf
import numpy as np
for key, value in result.items():
if isinstance(value, np.ndarray):
print(f"{key}:")
print(f" Min: {value.min()}")
print(f" Max: {value.max()}")
print(f" Mean: {value.mean()}")
print(f" NaN: {np.isnan(value).sum()}")
print(f" Inf: {np.isinf(value).sum()}")def validate_hdr_input(img):
"""Validate HDR input"""
assert img.ndim == 3, f"Expected 3D array, got {img.ndim}D"
assert img.shape[2] == 3, f"Expected RGB, got {img.shape[2]} channels"
assert img.dtype in [np.float32, np.float64], f"Expected float, got {img.dtype}"
assert img.min() >= 0, f"Negative values found: {img.min()}"
assert img.max() > 1, f"Suspiciously low max: {img.max()} (should be in cd/m²)"
print("✓ Input validation passed")
validate_hdr_input(hdr_image)- 📧 Email: tsor-at-dtu-dk
- 💬 GitHub Discussions: https://github.com/TSOR666/DTUCAM25/discussions
- 🐛 Bug Reports: https://github.com/TSOR666/DTUCAM25/issues
- 📚 Documentation: This wiki
Please include:
-
DTUTMO version:
python -c "import dtutmo; print(dtutmo.__version__)" -
Python version:
python --version - Operating system: Windows/macOS/Linux
- Minimal reproducible example:
import numpy as np
from dtutmo import CompleteDTUTMO
hdr = np.random.rand(100, 100, 3) * 1000
tmo = CompleteDTUTMO()
output = tmo.process(hdr) # Bug occurs here- Error message: Full traceback
- Expected behavior: What should happen
- Actual behavior: What actually happens
Open an issue on GitHub with:
- Use case: Why do you need this feature?
- Proposed API: How would you like to use it?
- Alternatives: What workarounds have you tried?
- Impact: Who else might benefit?
See Development Guide for:
- Setting up development environment
- Running tests
- Code style guidelines
- Pull request process
- Hood, D. C., & Finkelstein, M. A. (1986). Sensitivity to Light. Handbook of Perception and Human Performance.
- Kim, M., Weyrich, T., & Kautz, J. (2009). Modeling Human Color Perception Under Extended Luminance Levels. ACM TOG.
- CIE 180:2010. Disability Glare.
- CIE 248:2022. CIECAM16.
- Ashraf, M., et al. (2024). CastleCSF. Journal of Vision.
- Reinhard, E., et al. (2010). High Dynamic Range Imaging, 2nd ed. Morgan Kaufmann.
- Fairchild, M. D. (2013). Color Appearance Models, 3rd ed. Wiley.
Can't find your question? Ask on GitHub Discussions!