Professional-grade EXR image format library for high-dynamic-range scene-linear image data with multi-part and deep compositing support
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Channel-based image representation supporting arbitrary pixel types, subsampling patterns, and deep compositing data structures for professional VFX and animation workflows.
OpenEXR supports multiple pixel data types optimized for different content and precision requirements.
# Pixel type constants
OpenEXR.UINT: int # 32-bit unsigned integer (0 to 4,294,967,295)
OpenEXR.HALF: int # 16-bit IEEE 754 floating point (half precision)
OpenEXR.FLOAT: int # 32-bit IEEE 754 floating point (single precision)Individual image channels with flexible configuration for subsampling, pixel types, and linear encoding.
class Channel:
def __init__(self, name: str = None, pixels = None, xSampling: int = 1, ySampling: int = 1, pLinear: bool = False):
"""
Create image channel with pixel data.
Args:
name: Channel identifier (e.g., "R", "G", "B", "A", "Z", "motion.X")
pixels: Pixel data as numpy array
xSampling: Horizontal subsampling factor (1 = full resolution)
ySampling: Vertical subsampling factor (1 = full resolution)
pLinear: True if pixels are perceptually linear encoded
"""
def pixelType(self):
"""
Get pixel data type based on numpy array dtype.
Returns:
OpenEXR pixel type constant (UINT/HALF/FLOAT)
"""
name: str # Channel name/identifier
xSampling: int # Horizontal subsampling (1, 2, 4, ...)
ySampling: int # Vertical subsampling (1, 2, 4, ...)
pLinear: bool # Perceptual linearity flag
pixels: numpy.ndarray # Pixel data arrayOpenEXR uses specific array layouts and data type mappings for efficient processing.
# Numpy dtype mapping to OpenEXR pixel types
numpy.uint32 -> OpenEXR.UINT # 32-bit unsigned integer
numpy.float16 -> OpenEXR.HALF # 16-bit floating point (half)
numpy.float32 -> OpenEXR.FLOAT # 32-bit floating point (float)
# Array shapes for different channel configurations
single_channel: (height, width) # Grayscale, alpha, depth
rgb_packed: (height, width, 3) # RGB as single array
rgba_packed: (height, width, 4) # RGBA as single array
separate_channels: dict[str, (height, width)] # Individual channel arraysMathematical types for image bounds, vectors, and coordinate systems from the Imath module.
# 2D Vector types
class V2i:
def __init__(self, x: int = 0, y: int = 0): ...
x: int
y: int
class V2f:
def __init__(self, x: float = 0.0, y: float = 0.0): ...
x: float
y: float
# 2D Box types for image bounds
class Box2i:
def __init__(self, min: V2i = None, max: V2i = None): ...
def __init__(self, minX: int, minY: int, maxX: int, maxY: int): ...
min: V2i # Minimum corner
max: V2i # Maximum corner
def width(self) -> int: ...
def height(self) -> int: ...
def isEmpty(self) -> bool: ...
def hasVolume(self) -> bool: ...
class Box2f:
def __init__(self, min: V2f = None, max: V2f = None): ...
def __init__(self, minX: float, minY: float, maxX: float, maxY: float): ...
min: V2f # Minimum corner
max: V2f # Maximum cornerMulti-sample per pixel data structures for advanced compositing workflows.
# Deep image support (conceptual - implementation varies)
class DeepChannel:
"""
Channel supporting variable samples per pixel.
Used for deep compositing workflows.
"""
def __init__(self, name: str, sampleCounts, sampleData):
"""
Create deep channel.
Args:
name: Channel identifier
sampleCounts: Array of sample counts per pixel
sampleData: Flattened array of all sample values
"""
name: str # Channel name
sampleCounts: numpy.ndarray # Samples per pixel (height, width)
sampleData: numpy.ndarray # All sample values (flattened)
totalSamples: int # Total number of samplesimport OpenEXR
import numpy as np
# Create data with different pixel types
height, width = 1080, 1920
# HALF precision (16-bit float) - common for HDR images
half_data = np.random.rand(height, width, 3).astype(np.float16)
half_channels = {"RGB": half_data}
# FLOAT precision (32-bit float) - maximum precision
float_data = np.random.rand(height, width, 3).astype(np.float32)
float_channels = {"RGB": float_data}
# UINT (32-bit unsigned int) - for ID mattes, masks
uint_data = np.random.randint(0, 255, (height, width), dtype=np.uint32)
uint_channels = {"ID": uint_data}
# Check pixel types
print(f"Half pixel type: {OpenEXR.Channel(pixels=half_data).pixelType()}") # OpenEXR.HALF
print(f"Float pixel type: {OpenEXR.Channel(pixels=float_data).pixelType()}") # OpenEXR.FLOAT
print(f"UINT pixel type: {OpenEXR.Channel(pixels=uint_data).pixelType()}") # OpenEXR.UINTimport OpenEXR
import numpy as np
height, width = 1080, 1920
# RGB channels as single packed array
rgb_packed = np.random.rand(height, width, 3).astype('f')
channels_packed = {"RGB": rgb_packed}
# RGB channels as separate arrays
r_data = np.random.rand(height, width).astype('f')
g_data = np.random.rand(height, width).astype('f')
b_data = np.random.rand(height, width).astype('f')
channels_separate = {"R": r_data, "G": g_data, "B": b_data}
# RGBA with alpha channel
rgba_data = np.random.rand(height, width, 4).astype('f')
rgba_data[:, :, 3] = 1.0 # Set alpha to 1.0
channels_rgba = {"RGBA": rgba_data}
# Mixed precision channels
beauty_rgb = np.random.rand(height, width, 3).astype(np.float16) # HALF for beauty
depth_z = np.random.rand(height, width).astype(np.float32) # FLOAT for depth
mask_id = np.random.randint(0, 10, (height, width), dtype=np.uint32) # UINT for ID
mixed_channels = {
"RGB": beauty_rgb,
"Z": depth_z,
"ID": mask_id
}import OpenEXR
import numpy as np
height, width = 1080, 1920
# Full resolution luminance
y_data = np.random.rand(height, width).astype('f')
# Subsampled chrominance (half resolution)
# Create data at full resolution then subsample
chroma_full = np.random.rand(height, width, 2).astype('f')
chroma_subsampled = chroma_full[::2, ::2, :] # Every other pixel
# Create channels with subsampling
channels = {
"Y": OpenEXR.Channel("Y", y_data, xSampling=1, ySampling=1),
"RY": OpenEXR.Channel("RY", chroma_subsampled[:,:,0], xSampling=2, ySampling=2),
"BY": OpenEXR.Channel("BY", chroma_subsampled[:,:,1], xSampling=2, ySampling=2)
}
# Write YC format image
header = {
"compression": OpenEXR.ZIP_COMPRESSION,
"type": OpenEXR.scanlineimage
}
with OpenEXR.File(header, channels) as outfile:
outfile.write("yc_subsampled.exr")from OpenEXR import Imath
import OpenEXR
import numpy as np
# Define image windows using Box2i
display_window = Imath.Box2i(
Imath.V2i(0, 0), # min corner
Imath.V2i(1919, 1079) # max corner (1920x1080)
)
# Data window can be subset of display window
data_window = Imath.Box2i(
Imath.V2i(100, 100), # Cropped region
Imath.V2i(1819, 979) # 1720x880 actual data
)
# Calculate dimensions
data_width = data_window.width() # 1720
data_height = data_window.height() # 880
display_width = display_window.width() # 1920
display_height = display_window.height() # 1080
print(f"Display: {display_width}x{display_height}")
print(f"Data: {data_width}x{data_height}")
# Create image data for actual data window size
rgb_data = np.random.rand(data_height, data_width, 3).astype('f')
# Header with window information
header = {
"compression": OpenEXR.ZIP_COMPRESSION,
"type": OpenEXR.scanlineimage,
"displayWindow": (display_window.min.x, display_window.min.y,
display_window.max.x, display_window.max.y),
"dataWindow": (data_window.min.x, data_window.min.y,
data_window.max.x, data_window.max.y)
}
channels = {"RGB": rgb_data}
with OpenEXR.File(header, channels) as outfile:
outfile.write("windowed_image.exr")import OpenEXR
import numpy as np
height, width = 1080, 1920
# Beauty pass (RGB)
beauty = np.random.rand(height, width, 3).astype(np.float16)
# Depth pass (Z)
depth = np.random.exponential(10.0, (height, width)).astype(np.float32)
# Motion vectors (2D)
motion_x = np.random.normal(0, 2, (height, width)).astype(np.float32)
motion_y = np.random.normal(0, 2, (height, width)).astype(np.float32)
# Normal vectors (3D)
normals = np.random.normal(0, 1, (height, width, 3)).astype(np.float32)
# Normalize to unit vectors
norm_length = np.sqrt(np.sum(normals**2, axis=2, keepdims=True))
normals = normals / norm_length
# Object ID (integer)
object_ids = np.random.randint(0, 100, (height, width)).astype(np.uint32)
# Material ID (integer)
material_ids = np.random.randint(0, 50, (height, width)).astype(np.uint32)
# Coverage/alpha (float)
coverage = np.random.rand(height, width).astype(np.float32)
# Create comprehensive channel set
vfx_channels = {
# Beauty
"RGB": beauty,
# Geometry
"Z": depth,
"N": normals, # Normals as packed RGB
"N.X": normals[:,:,0], # Or separate components
"N.Y": normals[:,:,1],
"N.Z": normals[:,:,2],
# Motion
"motion": np.stack([motion_x, motion_y], axis=2), # Packed 2D
"motion.X": motion_x, # Or separate components
"motion.Y": motion_y,
# IDs
"objectID": object_ids,
"materialID": material_ids,
# Coverage
"A": coverage
}
# VFX-optimized header
vfx_header = {
"compression": OpenEXR.DWAA_COMPRESSION, # Good for mixed content
"type": OpenEXR.scanlineimage,
"pixelAspectRatio": 1.0,
# Custom attributes
"software": "VFX Pipeline v1.0",
"comments": "Multi-pass render with motion vectors"
}
with OpenEXR.File(vfx_header, vfx_channels) as outfile:
outfile.write("vfx_multipass.exr")
# Read back and verify channel types
with OpenEXR.File("vfx_multipass.exr") as infile:
channels = infile.channels()
for name, channel in channels.items():
pixel_type = channel.pixelType()
shape = channel.pixels.shape
dtype = channel.pixels.dtype
print(f"{name}: {shape} {dtype} -> OpenEXR.{pixel_type}")import OpenEXR
import numpy as np
def create_memory_efficient_channels(height, width):
"""Create channels with memory-conscious data types."""
# Use HALF precision for beauty (saves 50% memory vs FLOAT)
beauty_data = np.random.rand(height, width, 3).astype(np.float16)
# Use FLOAT only when precision is critical (depth, motion)
depth_data = np.random.rand(height, width).astype(np.float32)
motion_data = np.random.rand(height, width, 2).astype(np.float32)
# Use UINT for discrete data (IDs, masks)
id_data = np.random.randint(0, 1000, (height, width), dtype=np.uint32)
# Calculate memory usage
beauty_mb = beauty_data.nbytes / (1024 * 1024)
depth_mb = depth_data.nbytes / (1024 * 1024)
motion_mb = motion_data.nbytes / (1024 * 1024)
id_mb = id_data.nbytes / (1024 * 1024)
total_mb = beauty_mb + depth_mb + motion_mb + id_mb
print(f"Memory usage:")
print(f" Beauty (HALF): {beauty_mb:.1f} MB")
print(f" Depth (FLOAT): {depth_mb:.1f} MB")
print(f" Motion (FLOAT): {motion_mb:.1f} MB")
print(f" ID (UINT): {id_mb:.1f} MB")
print(f" Total: {total_mb:.1f} MB")
return {
"RGB": beauty_data,
"Z": depth_data,
"motion": motion_data,
"ID": id_data
}
# Create 4K image with memory-efficient types
channels = create_memory_efficient_channels(2160, 3840) # 4K resolution
header = {
"compression": OpenEXR.DWAA_COMPRESSION,
"type": OpenEXR.scanlineimage
}
with OpenEXR.File(header, channels) as outfile:
outfile.write("4k_memory_efficient.exr")Install with Tessl CLI
npx tessl i tessl/pypi-openexr