What Is OpenEXR?
OpenEXR is a high dynamic range (HDR) image file format developed by Industrial Light & Magic (ILM) in 1999 and released as open source in 2003. It has become the industry standard for visual effects, 3D rendering, and film production — virtually every major VFX studio (ILM, Weta Digital, DNEG, Framestore), renderer (Arnold, V-Ray, RenderMan, Cycles), and compositing application (Nuke, Fusion, After Effects) uses OpenEXR as its primary interchange format.
The defining feature of OpenEXR is its support for floating-point pixel values — where standard 8-bit images store pixel values from 0–255, OpenEXR stores values as 16-bit half-precision (float16) or 32-bit full-precision (float32) floating-point numbers, with no clipping at "pure white." A sun in an EXR file can have a value of 50,000 — 50,000× brighter than a value of 1.0 — while keeping deep shadow detail at values like 0.0003. This enormous dynamic range (the ratio of the brightest to darkest value representable) is essential for physically-based rendering and compositing.
Dynamic Range: Why It Matters for VFX
In film production, the term linear light is fundamental. Physical light in the real world is linear: double the number of photons means double the brightness. But digital cameras and monitors apply non-linear tone curves (gamma) to images to match human vision and display characteristics.
OpenEXR stores pixel data in scene-linear light — unmodified, unclamped, untone-mapped floating-point values as they came from the renderer or camera. This means:
- No banding in shadows or highlights: floating-point has more precision where it matters
- No clipping: highlights remain recoverable no matter how bright
- Mathematically correct compositing: adding, multiplying, and filtering operations work correctly in linear light — they produce wrong results on gamma-encoded images
- Deferred tone mapping: the LUT (Look-Up Table) or tone mapping curve is applied at the very end of the pipeline, not baked into the image data
This is why a CG sunset rendered in a 3D application looks "washed out" when viewed in a standard image viewer — the viewer is displaying raw linear values on a gamma-corrected monitor without applying tone mapping. The data is correct; the display transform is missing.
OpenEXR File Structure
An OpenEXR file is a binary file with:
- Magic number:
76 2F 31 01(little-endian0x0131_2F76) - Version field: flags indicating scan-line vs tiled, single vs multi-part, deep data
- Header: key-value attribute pairs including:
channels: the channel list (R, G, B, A, Z, normal.X, normal.Y, etc.)dataWindow: the pixel data rectangle (often larger thandisplayWindowwith overscan)displayWindow: the intended display regionpixelAspectRatio: for anamorphic formatscompression: compression algorithmlineOrder:INCREASING_Y,DECREASING_Y, orRANDOM_Y- Arbitrary custom attributes (camera metadata, render settings, etc.)
- Offset table: byte offsets to each scan-line block or tile for random access
- Pixel data: compressed channel data
Data Types and Bit Depth
OpenEXR supports three pixel data types:
| Type | Bits | Range | Use case |
|---|---|---|---|
HALF |
16-bit float | ~6.1×10⁻⁵ to 65504 | Primary VFX and compositing (best balance of size and precision) |
FLOAT |
32-bit float | ~1.2×10⁻³⁸ to 3.4×10³⁸ | Deep shadow maps, Z-depth, scientific applications |
UINT |
32-bit unsigned int | 0 to 4,294,967,295 | Object ID mattes, cryptomatte, integer masks |
Why HALF for most VFX? The 16-bit half-float provides sufficient precision for HDR imaging (about 3.3 decimal digits in each decade) while using half the memory of float32. A 4K OpenEXR in HALF (RGBA) uses 4096 × 2160 × 4 channels × 2 bytes = 67 MB per frame. The same in FLOAT would be 134 MB.
Channels: Beyond RGBA
One of OpenEXR's most powerful features is arbitrary channel support. A single EXR file can store dozens of render passes:
R, G, B, A # beauty (final composite) RGBA
diffuse.R, diffuse.G, diffuse.B # diffuse lighting component
specular.R, specular.G, specular.B # specular highlights
emission.R, emission.G, emission.B # self-illumination
Z # depth (distance from camera, in scene units)
normal.X, normal.Y, normal.Z # world-space surface normals
P.X, P.Y, P.Z # world-space surface positions
cryptomatte00.R, cryptomatte00.G, cryptomatte00.B, cryptomatte00.A # object mattes
shadow # shadow pass
ao # ambient occlusion
velocity.X, velocity.Y # motion vectors for motion blur
This multi-channel / multi-pass EXR workflow is the foundation of modern VFX compositing: the renderer produces a single EXR file containing all render passes, and the compositor (Nuke, Fusion) can selectively combine, adjust, and relight them without re-rendering.
Compression Algorithms
OpenEXR supports multiple lossless compression algorithms:
| Compression | Description | Best for |
|---|---|---|
NONE |
No compression | Fastest read/write, largest files |
RLE |
Run-Length Encoding | Flat-color CGI, sharp edges |
ZIPS |
DEFLATE, one scan line | Good balance (default in many tools) |
ZIP |
DEFLATE, 16-scan-line blocks | Slightly better ratio than ZIPS |
PIZ |
Wavelet-based | Best for natural imagery and photorealistic renders |
PXR24 |
Lossy (float32→24-bit) | Float depth data — preserves most precision |
B44 |
Lossy half-float compression | Fixed 2.28:1 ratio, very fast decode |
B44A |
B44 with sparse block optimization | Alpha channels and flat areas |
DWAA |
DreamWorks Animation, scan-line | Best balance of ratio and speed for VFX |
DWAB |
DreamWorks Animation, 256-line block | Slightly better than DWAA |
PIZ is lossless and provides excellent ratios for CG renders. DWAA/DWAB are lossy but perceptually lossless at their default quality settings — they offer 5-10× compression while maintaining full visual quality, making them increasingly popular for online delivery and archival.
Working with OpenEXR
Python with OpenEXR and Imath
# pip install OpenEXR Imath
import OpenEXR
import Imath
import numpy as np
# --- Reading an EXR file ---
exr = OpenEXR.InputFile('render.exr')
header = exr.header()
# Get data window (pixel dimensions)
dw = header['dataWindow']
width = dw.max.x - dw.min.x + 1
height = dw.max.y - dw.min.y + 1
print(f"Dimensions: {width} x {height}")
print(f"Channels: {list(header['channels'].keys())}")
print(f"Compression: {header['compression']}")
# Read RGB channels as 16-bit half-float (HALF)
HALF = Imath.PixelType(Imath.PixelType.HALF)
r_bytes = exr.channel('R', HALF)
g_bytes = exr.channel('G', HALF)
b_bytes = exr.channel('B', HALF)
# Convert to numpy arrays (float16)
r = np.frombuffer(r_bytes, dtype=np.float16).reshape(height, width)
g = np.frombuffer(g_bytes, dtype=np.float16).reshape(height, width)
b = np.frombuffer(b_bytes, dtype=np.float16).reshape(height, width)
# Stack to (H, W, 3) array
rgb = np.stack([r, g, b], axis=-1)
print(f"Max pixel value: {rgb.max():.2f}") # may be >> 1.0 for HDR content
# --- Writing an EXR file ---
output_header = OpenEXR.Header(width, height)
output_header['compression'] = Imath.Compression(Imath.Compression.PIZ_COMPRESSION)
channels = {
'R': r.astype(np.float16).tobytes(),
'G': g.astype(np.float16).tobytes(),
'B': b.astype(np.float16).tobytes(),
}
out = OpenEXR.OutputFile('output.exr', output_header)
out.writePixels(channels)
out.close()
Python with imageio and OpenImageIO
# pip install imageio
import imageio.v3 as iio
import numpy as np
# Read EXR (returns float32 array)
img = iio.imread('render.exr', plugin='EXR')
print(f"Shape: {img.shape}, Dtype: {img.dtype}") # (H, W, 3+) float32
# Simple tone mapping for preview (Reinhard)
def reinhard(img):
return img / (1.0 + img)
preview = (reinhard(img[:,:,:3]) * 255).clip(0, 255).astype(np.uint8)
iio.imwrite('preview.jpg', preview)
# Write EXR
iio.imwrite('output.exr', img.astype(np.float32))
Command-Line with oiiotool (OpenImageIO)
# oiiotool — the Swiss Army knife of image processing for VFX
# Part of OpenImageIO (OIIO), available via conda, brew, or pre-built binaries
# Convert EXR to PNG (with tone mapping)
oiiotool render.exr --ociodisplay "ACES" "sRGB" -o preview.png
# Extract a specific channel
oiiotool render.exr --ch Z -o depth.exr
# Combine multiple EXR passes into a multi-channel EXR
oiiotool beauty.exr diffuse.exr --chnames "diffuse.R,diffuse.G,diffuse.B" --siappend -o combined.exr
# Convert to TIFF (16-bit)
oiiotool render.exr -d uint16 -o output.tiff
# Apply ACES tone mapping and export to JPEG
oiiotool hdr.exr --iscolorspace "lin_srgb" --tocolorspace "sRGB" -o srgb.jpg
# Inspect an EXR file
oiiotool --info -v render.exr
Tiled and Deep OpenEXR
Tiled OpenEXR
Tiled EXR files divide the image into rectangular tiles (e.g., 64×64 or 128×128 pixels) stored with multiple resolution levels (like a mipmap pyramid). This enables:
- Random tile access: read only the tiles needed for a given zoom level in a large image viewer
- MIP mapping: pre-computed downsampled versions for texture lookup
Deep OpenEXR
Deep EXR stores multiple samples per pixel — each pixel can contain a list of depth samples with their own RGBA values. This enables:
- Deep compositing: combining volumetric elements (smoke, fog, hair) correctly with solid geometry at different depths
- Deep shadows: accurate shadows cast through semi-transparent geometry
Deep EXR is part of the OpenEXR 2.0 specification and is supported by Nuke, VRay, Arnold, and other professional tools.
OpenEXR vs Other HDR Formats
| Format | Bit depth | Channels | Compression | Primary use |
|---|---|---|---|---|
| OpenEXR | 16/32-bit float | Arbitrary | Multiple lossless+lossy | VFX, film, rendering |
| HDRI (.hdr, Radiance) | 8-bit mantissa + exp | RGB | RLE | Environment maps, IBL |
| TIFF | 8/16/32-bit | 4 (RGBA) | LZW, DEFLATE | Print, medical, scanning |
| DNG | 12-16-bit | BAYER pattern | Lossless | Camera RAW |
| PNG (16-bit) | 16-bit integer | RGBA | DEFLATE | Web, lossless general |
OpenEXR's combination of floating-point precision, arbitrary channels, lossless compression, and standardized color management makes it uniquely suited for professional image production workflows where quality and correctness are non-negotiable.
Related conversions
Frequent conversions across the catalogue: