Mati Codes
# _  _ ____ ___ _    ____ ____ ___  ____ ____ #
# |\/| |__|  |  |    |    |  | |  \ |___ [__  #
# |  | |  |  |  |    |___ |__| |__/ |___ ___] #
#                                             #
# _  _ ____ ___ _    ____ ____ ___  ____ ____ #
# |\/| |__|  |  |    |    |  | |  \ |___ [__  #
# |  | |  |  |  |    |___ |__| |__/ |___ ___] #
#                                             #

Dec. 28, 2020

HOW TO CONVERT AN HDR IMAGE TO LDR USING PYTHON


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.

No tone-mapping

Sunflowers with no tone-mapping

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_no_gamma.png

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)
>>>

Tags

python, HDR, imageio, HDRI, opencv, LDR, image manipulation, tone-mapping, bit-depth, color space