CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-openexr

Professional-grade EXR image format library for high-dynamic-range scene-linear image data with multi-part and deep compositing support

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

image-data.mddocs/

Image Data Structures

Channel-based image representation supporting arbitrary pixel types, subsampling patterns, and deep compositing data structures for professional VFX and animation workflows.

Capabilities

Pixel Type System

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)

Channel Architecture

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 array

Array Data Layout

OpenEXR 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 arrays

Geometric Types

Mathematical 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 corner

Deep Image Data

Multi-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 samples

Usage Examples

Working with Pixel Types

import 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.UINT

Channel Configurations

import 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
}

Subsampled Channels

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

Working with Image Bounds

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

Multi-Channel VFX Data

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

Memory Optimization

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

docs

advanced-features.md

cpp-api.md

file-io.md

image-data.md

index.md

metadata.md

tile.json