I often have VFX students ask me about creating an app to easily browse HDRIs. Browsing HDRIs can sometimes be a challenge for artists because the operating system doesn’t display thumbnails for HDR files like it does for LDR image formats.

If you’re writing a tool for browsing HDRIs you could read the files and convert them in memory in order to present a gallery, but it’s usually more practical and efficient to create lower-res LDR copies. You could use the LDR copies in a custom browsing app or in a standard operating system file browser. Let’s take a look at how to convert high-dynamic range images to low-dynamic range using Python. Continuing from my previous post about How To Read An HDR Image Using Python, I’ll show you different approaches using OpenCV, ImageIO, and OpenImageIO. I recommend taking a look at that post if you need help installing any of these libraries.

The Process#

There are a number of steps involved in the conversion that may be automatically handled by the library you’re using, but I will explain them briefly to make sense of the the code for the libraries that don’t handle it. It’s also just good to have a general understanding of what’s going on under the hood.

Tone-mapping#

You might recall from How To Read An HDR Image Using Python that HDR images have an infinitely large range. Now, we want to condense that range to 0-255. There are many ways that you can perform the conversion and each would result in a different look. That conversion process is called tone-mapping. The simplest approach is to clamp the values. Anything smaller that 0 becomes 0 and and anything larger than 1 becomes 1. This will work fine for most captured environments and our purposes, but you could also look into other popular tone-mapping operators like those listed in this paper.

Clamp tone-mapping#

Sunflowers with clamp tone-mapping

Drago tone-mapping#

Sunflowers with Drago tone-mapping

Color Space Conversion#

HDRIs are stored in linear color space, but LDR images are typically in an sRGB or gamma 2.2 color space. We need to apply the gamma transfer function to get the images to display well on our monitors or print.

Linear color space before gamma transfer function#

Sunflowers Linear

Bit-Depth Conversion#

The HDRIs are represented as floating point in memory, but we need to convert that into 8-bit unsigned integers if we want to convert them to PNG or JPEG. The 0-1 range will be converted to 0-255 to be stored in the LDR image file.

With that in mind, let’s look at the code.

OpenCV#

OpenCV comes with a number of easy to use tone-mapping operators. They’ll give you much better results, but require fine-tuning of parameters on a case-by-case basis. I’m not using them because the clamping tone map is sufficient for the thumbnail preview scenario that I set out at the start of this blog post, but I highly recommend checking out this blog post about the tone-mapping operators included with OpenCV if you’re interested in HDR to LDR conversion with more artistic control.


import cv2
hdr = cv2.imread(hdr_path, flags=cv2.IMREAD_ANYDEPTH)
# Simply clamp values to a 0-1 range for tone-mapping
ldr = np.clip(hdr, 0, 1)
# Color space conversion
ldr = ldr**(1/2.2)
# 0-255 remapping for bit-depth conversion
ldr = 255.0 * ldr
cv2.imwrite(ldr_path, ldr)

While we’re here, let’s at least take a look at how easy it is with OpenCVs Drago tone map operator. You’ll want to tweak the scale variable for your given HDRI.


import cv2
hdr = cv2.imread(hdr_path, flags=cv2.IMREAD_ANYDEPTH)
# Tone-mapping and color space conversion
tonemap = cv2.createTonemapDrago(2.2)
scale = 5
ldr = scale * tonemap.process(hdr)
# Remap to 0-255 for the bit-depth conversion
cv2.imwrite(ldr_path, ldr*255)

ImageIO#

Same deal as OpenCV, we’re handling every step ourselves for ImageIO.


import imageio
import numpy as np

hdr = imageio.imread(hdr_path, format='HDR-FI')
# Simply clamp values to a 0-1 range for tone-mapping
ldr = np.clip(hdr, 0, 1)
# Color space conversion
ldr = ldr**(1/2.2)
# 0-255 remapping for bit-depth conversion
ldr = 255.0 * ldr
imageio.imwrite(ldr_path, ldr)

OpenImageIO#

OpenImageIO seems to handle the tone-mapping for you though it’s likely just clamping the values. All we really need to do is the color space conversion.


from OpenImageIO import ImageBuf, ImageBugAlgo
hdr = ImageBuf(hdr_path)
# Color space conversion
ldr = oiio.ImageBufAlgo.colorconvert(hdr, "linear", "sRGB")
ldr.write(ldr_path)