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
Multi-part files, deep compositing images, tiled storage formats, and advanced compression methods for high-performance professional workflows.
Container format supporting multiple independent images in a single file, essential for VFX workflows with beauty passes, depth maps, motion vectors, and auxiliary data.
class Part:
def __init__(self, header: dict, channels: dict, name: str):
"""
Create image part for multi-part EXR files.
Args:
header: Part-specific metadata and format settings
channels: Channel data dictionary for this part
name: Human-readable part identifier
"""
def name(self) -> str:
"""Get part name identifier."""
def shape(self) -> tuple:
"""Get image dimensions as (height, width)."""
def width(self) -> int:
"""Get image width in pixels."""
def height(self) -> int:
"""Get image height in pixels."""
def compression(self):
"""Get compression method for this part."""
def type(self):
"""Get image type constant (scanline/tiled/deep)."""
def typeString(self) -> str:
"""Get human-readable image type."""
header: dict # Part header attributes
channels: dict # Channel name to data mapping
part_index: int # Zero-based part index
# Multi-part file creation
def create_multipart_file(parts: list, filename: str):
"""
Create multi-part EXR file.
Args:
parts: List of Part objects
filename: Output file path
"""Multi-sample per pixel data structures for advanced compositing, volumetric rendering, and motion blur effects.
# Deep image type constants
OpenEXR.deepscanline: int # Deep scanline format
OpenEXR.deeptile: int # Deep tiled format
# Deep image structure (conceptual representation)
class DeepImageData:
"""
Deep image data with variable samples per pixel.
Used for volume rendering, motion blur, and compositing.
"""
def __init__(self, width: int, height: int):
"""Initialize deep image structure."""
sampleCounts: numpy.ndarray # Per-pixel sample counts (height, width)
sampleData: dict # Channel name to sample data arrays
totalSamples: int # Total samples across all pixels
def setSampleCount(self, x: int, y: int, count: int):
"""Set sample count for specific pixel."""
def getSampleCount(self, x: int, y: int) -> int:
"""Get sample count for specific pixel."""
def setSampleData(self, channel: str, x: int, y: int, samples: list):
"""Set sample data for channel at pixel."""
def getSampleData(self, channel: str, x: int, y: int) -> list:
"""Get sample data for channel at pixel."""Block-based storage enabling efficient random access, streaming, and multi-resolution representations.
# Tiled image configuration
class TileDescription:
def __init__(self, xSize: int, ySize: int, mode: int, roundingMode: int = None):
"""
Configure tiled image parameters.
Args:
xSize: Tile width in pixels
ySize: Tile height in pixels
mode: Level mode (ONE_LEVEL/MIPMAP_LEVELS/RIPMAP_LEVELS)
roundingMode: Level rounding (ROUND_DOWN/ROUND_UP)
"""
xSize: int # Tile width
ySize: int # Tile height
mode: int # Level generation mode
roundingMode: int # Dimension rounding mode
# Level modes for tiled images
from OpenEXR import Imath
Imath.LevelMode.ONE_LEVEL: int # Single resolution level
Imath.LevelMode.MIPMAP_LEVELS: int # Square mipmaps (power of 2)
Imath.LevelMode.RIPMAP_LEVELS: int # Rectangular ripmaps (independent X/Y)
Imath.LevelRoundingMode.ROUND_DOWN: int # Round dimensions down
Imath.LevelRoundingMode.ROUND_UP: int # Round dimensions up
# Tiled image header
tiled_header = {
"type": OpenEXR.tiledimage,
"tiles": {
"xSize": 64, # Tile width
"ySize": 64, # Tile height
"mode": Imath.LevelMode.MIPMAP_LEVELS, # Generate mipmaps
"roundingMode": Imath.LevelRoundingMode.ROUND_DOWN
}
}Advanced compression algorithms optimized for different content types and quality requirements.
# Lossless compression methods
OpenEXR.NO_COMPRESSION: int # No compression (fastest)
OpenEXR.RLE_COMPRESSION: int # Run-length encoding (simple)
OpenEXR.ZIPS_COMPRESSION: int # ZIP scanline compression
OpenEXR.ZIP_COMPRESSION: int # ZIP block compression
OpenEXR.PIZ_COMPRESSION: int # PIZ wavelet compression (best ratio)
# Lossy compression methods
OpenEXR.PXR24_COMPRESSION: int # PXR24 (24-bit RGB lossy)
OpenEXR.B44_COMPRESSION: int # B44 (lossy for some channels)
OpenEXR.B44A_COMPRESSION: int # B44A (adaptive lossy)
OpenEXR.DWAA_COMPRESSION: int # DWAA (lossy, good for VFX)
OpenEXR.DWAB_COMPRESSION: int # DWAB (lossy, better compression)
# Future compression methods
OpenEXR.HTJ2K256_COMPRESSION: int # JPEG 2000 HTJ2K (256x256 blocks)
OpenEXR.HTJ2K32_COMPRESSION: int # JPEG 2000 HTJ2K (32x32 blocks)
# Compression configuration
def configure_compression(compression_type: int, **kwargs):
"""
Configure compression parameters.
Args:
compression_type: Compression method constant
**kwargs: Compression-specific parameters
"""
# DWA compression quality (0.0-100.0)
if compression_type in (OpenEXR.DWAA_COMPRESSION, OpenEXR.DWAB_COMPRESSION):
quality = kwargs.get("quality", 45.0) # Default quality
# ZIP compression level (1-9)
if compression_type in (OpenEXR.ZIP_COMPRESSION, OpenEXR.ZIPS_COMPRESSION):
level = kwargs.get("level", 6) # Default levelMulti-view image storage for stereoscopic content and camera arrays.
# Multi-view attributes
multiview_header = {
"type": OpenEXR.scanlineimage,
"multiView": [ # List of view names
"left", "right" # Stereo views
# or ["cam1", "cam2", "cam3", "cam4"] # Camera array
],
"view": str, # Current view name for single-view parts
}
# Multi-view channel naming convention
stereo_channels = {
"RGB.left": left_rgb_data, # Left eye RGB
"RGB.right": right_rgb_data, # Right eye RGB
"Z.left": left_depth_data, # Left eye depth
"Z.right": right_depth_data, # Right eye depth
}
# Alternative: separate parts per view
left_part = OpenEXR.Part(left_header, left_channels, "left")
right_part = OpenEXR.Part(right_header, right_channels, "right")Embedded thumbnail images for fast preview without decoding full resolution data.
from OpenEXR import Imath
class PreviewImage:
def __init__(self, width: int, height: int, pixels: bytes):
"""
Create preview image.
Args:
width: Preview width in pixels
height: Preview height in pixels
pixels: RGBA pixel data (4 bytes per pixel)
"""
width: int # Preview width
height: int # Preview height
pixels: bytes # RGBA8 pixel data
# Add preview to header
preview_header = {
"compression": OpenEXR.ZIP_COMPRESSION,
"type": OpenEXR.scanlineimage,
"preview": PreviewImage(160, 120, rgba_thumbnail_bytes)
}Specialized support for environment mapping and spherical images.
# Environment map attributes
envmap_header = {
"compression": OpenEXR.ZIP_COMPRESSION,
"type": OpenEXR.scanlineimage,
# Environment map metadata
"envmap": str, # Environment map type
# Values: "latlong", "cube", "sphere"
# Spherical coordinates
"wrapmodes": str, # Wrap modes ("clamp", "repeat", "mirror")
# Cube face mapping (for cube maps)
"cubeFace": str, # Face identifier ("+X", "-X", "+Y", "-Y", "+Z", "-Z")
}import OpenEXR
import numpy as np
def create_vfx_multipart_file(output_path, image_size):
"""Create comprehensive VFX multi-part EXR file."""
height, width = image_size
# Beauty pass (primary image)
beauty_rgb = np.random.rand(height, width, 3).astype(np.float16)
beauty_alpha = np.ones((height, width), dtype=np.float16)
beauty_header = {
"compression": OpenEXR.DWAA_COMPRESSION,
"type": OpenEXR.scanlineimage,
"renderPass": "beauty",
"samples": 512
}
beauty_channels = {
"RGB": beauty_rgb,
"A": beauty_alpha
}
# Depth pass (Z-buffer)
depth_data = np.random.exponential(10.0, (height, width)).astype(np.float32)
depth_header = {
"compression": OpenEXR.ZIP_COMPRESSION,
"type": OpenEXR.scanlineimage,
"renderPass": "depth"
}
depth_channels = {"Z": depth_data}
# Normal pass (surface normals)
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
normal_header = {
"compression": OpenEXR.ZIP_COMPRESSION,
"type": OpenEXR.scanlineimage,
"renderPass": "normal"
}
normal_channels = {"N": normals}
# Motion vector pass
motion_x = np.random.normal(0, 2, (height, width)).astype(np.float32)
motion_y = np.random.normal(0, 2, (height, width)).astype(np.float32)
motion_header = {
"compression": OpenEXR.ZIP_COMPRESSION,
"type": OpenEXR.scanlineimage,
"renderPass": "motion"
}
motion_channels = {
"motion.X": motion_x,
"motion.Y": motion_y
}
# ID mattes (object and material IDs)
object_ids = np.random.randint(0, 100, (height, width), dtype=np.uint32)
material_ids = np.random.randint(0, 50, (height, width), dtype=np.uint32)
id_header = {
"compression": OpenEXR.ZIP_COMPRESSION,
"type": OpenEXR.scanlineimage,
"renderPass": "id"
}
id_channels = {
"objectID": object_ids,
"materialID": material_ids
}
# Cryptomatte pass (for advanced compositing)
crypto_data = np.random.rand(height, width, 4).astype(np.float32)
crypto_header = {
"compression": OpenEXR.ZIP_COMPRESSION,
"type": OpenEXR.scanlineimage,
"renderPass": "cryptomatte",
"cryptomatte/id": "cryptomatte_object",
"cryptomatte/hash": "MurmurHash3_32",
"cryptomatte/conversion": "uint32_to_float32"
}
crypto_channels = {"crypto": crypto_data}
# Create multi-part file
parts = [
OpenEXR.Part(beauty_header, beauty_channels, "beauty"),
OpenEXR.Part(depth_header, depth_channels, "depth"),
OpenEXR.Part(normal_header, normal_channels, "normal"),
OpenEXR.Part(motion_header, motion_channels, "motion"),
OpenEXR.Part(id_header, id_channels, "id"),
OpenEXR.Part(crypto_header, crypto_channels, "cryptomatte")
]
with OpenEXR.File(parts) as outfile:
outfile.write(output_path)
print(f"Created VFX multi-part file: {output_path}")
return len(parts)
# Create 2K VFX file
num_parts = create_vfx_multipart_file("vfx_shot.exr", (1556, 2048))
print(f"Created {num_parts} parts in VFX file")
# Read and examine multi-part file
with OpenEXR.File("vfx_shot.exr") as infile:
for i in range(6): # We know we have 6 parts
header = infile.header(i)
channels = infile.channels(i)
pass_name = header.get("renderPass", f"part_{i}")
channel_names = list(channels.keys())
print(f"Part {i} ({pass_name}): {channel_names}")
# Access specific pass data
if pass_name == "beauty":
beauty_rgb = channels["RGB"].pixels
beauty_alpha = channels["A"].pixels
print(f" Beauty RGB: {beauty_rgb.shape} {beauty_rgb.dtype}")
elif pass_name == "depth":
depth_z = channels["Z"].pixels
print(f" Depth range: {depth_z.min():.3f} - {depth_z.max():.3f}")
elif pass_name == "motion":
motion_x = channels["motion.X"].pixels
motion_y = channels["motion.Y"].pixels
motion_magnitude = np.sqrt(motion_x**2 + motion_y**2)
print(f" Motion magnitude: {motion_magnitude.mean():.3f} ± {motion_magnitude.std():.3f}")import OpenEXR
from OpenEXR import Imath
import numpy as np
def create_tiled_image_with_mipmaps(output_path, base_size):
"""Create tiled EXR with mipmap levels for efficient access."""
height, width = base_size
# Generate base level image data
base_image = np.random.rand(height, width, 3).astype('f')
# Configure tiled format with mipmaps
tile_desc = {
"xSize": 64, # 64x64 tiles
"ySize": 64,
"mode": Imath.LevelMode.MIPMAP_LEVELS, # Generate mipmaps
"roundingMode": Imath.LevelRoundingMode.ROUND_DOWN
}
tiled_header = {
"compression": OpenEXR.ZIP_COMPRESSION,
"type": OpenEXR.tiledimage,
"tiles": tile_desc,
# Tiled image metadata
"software": "Tiled Image Generator v1.0",
"comments": "Tiled format with mipmap levels for efficient random access"
}
channels = {"RGB": base_image}
with OpenEXR.File(tiled_header, channels) as outfile:
outfile.write(output_path)
print(f"Created tiled image: {output_path}")
print(f"Base level: {width}x{height}")
# Calculate mipmap levels
level = 0
while width > 1 or height > 1:
level += 1
width = max(1, width // 2)
height = max(1, height // 2)
print(f"Level {level}: {width}x{height}")
# Create 4K tiled image
create_tiled_image_with_mipmaps("tiled_4k.exr", (2160, 3840))
# Read tiled image (conceptual - actual tile access requires C++ API)
with OpenEXR.File("tiled_4k.exr") as infile:
header = infile.header()
# Check if tiled
if header.get("type") == OpenEXR.tiledimage:
tile_info = header.get("tiles", {})
print(f"Tile size: {tile_info.get('xSize')}x{tile_info.get('ySize')}")
print(f"Level mode: {tile_info.get('mode')}")
# Access full resolution data
channels = infile.channels()
rgb_data = channels["RGB"].pixels
print(f"Full resolution data: {rgb_data.shape}")import OpenEXR
import numpy as np
def create_deep_image_example(output_path, image_size):
"""Create deep EXR file with multiple samples per pixel."""
height, width = image_size
# Simulate deep compositing data
# In practice, this would come from a volume renderer or deep compositing system
# Create sample count array (number of samples per pixel)
# Most pixels have 1-3 samples, some have more for complex areas
base_samples = np.ones((height, width), dtype=np.uint32)
complex_area = np.random.random((height, width)) > 0.8 # 20% complex pixels
base_samples[complex_area] = np.random.randint(2, 8, np.sum(complex_area))
total_samples = np.sum(base_samples)
print(f"Total samples: {total_samples} across {height*width} pixels")
print(f"Average samples per pixel: {total_samples / (height*width):.2f}")
# Create flattened sample data
# Each sample has RGBA + Z + ID data
sample_rgba = np.random.rand(total_samples, 4).astype('f')
sample_z = np.random.exponential(5.0, total_samples).astype('f')
sample_id = np.random.randint(0, 100, total_samples, dtype=np.uint32)
# Deep image header
deep_header = {
"compression": OpenEXR.ZIP_COMPRESSION,
"type": OpenEXR.deepscanline, # Deep scanline format
# Deep image metadata
"software": "Deep Compositor v2.0",
"comments": "Deep compositing data with multiple samples per pixel",
"renderPass": "deep_beauty",
"deepData": True
}
# Note: Deep image channel creation is conceptual
# Actual implementation requires specialized deep image classes
deep_channels = {
"R": sample_rgba[:, 0],
"G": sample_rgba[:, 1],
"B": sample_rgba[:, 2],
"A": sample_rgba[:, 3],
"Z": sample_z,
"ID": sample_id,
# Special metadata for deep format
"_sampleCounts": base_samples, # Samples per pixel
"_totalSamples": total_samples # Total sample count
}
# This is conceptual - actual deep image writing requires C++ API
print("Deep image creation (conceptual):")
print(f" Sample counts shape: {base_samples.shape}")
print(f" Sample data length: {len(sample_rgba)}")
print(f" Channel types: RGBA=float, Z=float, ID=uint")
return deep_header, deep_channels
# Create deep compositing data
deep_header, deep_channels = create_deep_image_example("deep_comp.exr", (540, 960))import OpenEXR
import numpy as np
import os
import time
def compression_benchmark(image_data, output_dir="compression_test"):
"""Compare different compression methods for image data."""
os.makedirs(output_dir, exist_ok=True)
# Test different compression methods
compressions = [
("no_compression", OpenEXR.NO_COMPRESSION),
("rle", OpenEXR.RLE_COMPRESSION),
("zip", OpenEXR.ZIP_COMPRESSION),
("zips", OpenEXR.ZIPS_COMPRESSION),
("piz", OpenEXR.PIZ_COMPRESSION),
("pxr24", OpenEXR.PXR24_COMPRESSION),
("b44", OpenEXR.B44_COMPRESSION),
("b44a", OpenEXR.B44A_COMPRESSION),
("dwaa", OpenEXR.DWAA_COMPRESSION),
("dwab", OpenEXR.DWAB_COMPRESSION)
]
results = []
for name, compression in compressions:
print(f"Testing {name}...")
header = {
"compression": compression,
"type": OpenEXR.scanlineimage
}
channels = {"RGB": image_data}
filename = os.path.join(output_dir, f"test_{name}.exr")
# Time compression
start_time = time.time()
try:
with OpenEXR.File(header, channels) as outfile:
outfile.write(filename)
write_time = time.time() - start_time
# Get file size
file_size = os.path.getsize(filename)
# Time decompression
start_time = time.time()
with OpenEXR.File(filename) as infile:
_ = infile.channels()["RGB"].pixels
read_time = time.time() - start_time
results.append({
"name": name,
"compression": compression,
"file_size_mb": file_size / (1024 * 1024),
"write_time": write_time,
"read_time": read_time,
"compression_ratio": (image_data.nbytes / file_size),
"success": True
})
except Exception as e:
print(f" Failed: {e}")
results.append({
"name": name,
"success": False,
"error": str(e)
})
# Print results
print("\nCompression Benchmark Results:")
print("=" * 80)
print(f"{'Method':<12} {'Size (MB)':<10} {'Ratio':<8} {'Write (s)':<10} {'Read (s)':<10}")
print("-" * 80)
for result in results:
if result["success"]:
print(f"{result['name']:<12} "
f"{result['file_size_mb']:<10.2f} "
f"{result['compression_ratio']:<8.1f} "
f"{result['write_time']:<10.3f} "
f"{result['read_time']:<10.3f}")
else:
print(f"{result['name']:<12} FAILED: {result['error']}")
return results
# Create test image (4K resolution)
height, width = 2160, 3840
test_image = np.random.rand(height, width, 3).astype('f')
print(f"Testing compression on {width}x{height} RGB image")
print(f"Uncompressed size: {test_image.nbytes / (1024*1024):.1f} MB")
# Run benchmark
results = compression_benchmark(test_image)
# Find best compression for different criteria
successful = [r for r in results if r["success"]]
if successful:
best_ratio = max(successful, key=lambda x: x["compression_ratio"])
fastest_write = min(successful, key=lambda x: x["write_time"])
fastest_read = min(successful, key=lambda x: x["read_time"])
smallest_file = min(successful, key=lambda x: x["file_size_mb"])
print(f"\nBest Results:")
print(f" Best ratio: {best_ratio['name']} ({best_ratio['compression_ratio']:.1f}x)")
print(f" Fastest write: {fastest_write['name']} ({fastest_write['write_time']:.3f}s)")
print(f" Fastest read: {fastest_read['name']} ({fastest_read['read_time']:.3f}s)")
print(f" Smallest file: {smallest_file['name']} ({smallest_file['file_size_mb']:.2f} MB)")import OpenEXR
import numpy as np
def create_stereo_exr(output_path, image_size, eye_separation=0.065):
"""Create stereoscopic EXR file with left and right eye views."""
height, width = image_size
# Simulate stereo image pair
# In practice, these would be renders from offset camera positions
# Base scene
base_scene = np.random.rand(height, width, 3).astype('f')
# Simulate parallax shift for stereo effect
shift_pixels = int(eye_separation * width / 0.1) # Approximate parallax shift
# Left eye view (base)
left_rgb = base_scene
left_depth = np.random.exponential(10.0, (height, width)).astype('f')
# Right eye view (shifted for parallax)
right_rgb = np.roll(base_scene, shift_pixels, axis=1) # Horizontal shift
right_depth = np.roll(left_depth, shift_pixels, axis=1)
# Method 1: Multi-part stereo file
left_header = {
"compression": OpenEXR.DWAA_COMPRESSION,
"type": OpenEXR.scanlineimage,
"view": "left",
"stereoscopic": True,
"eyeSeparation": eye_separation
}
left_channels = {
"RGB": left_rgb,
"Z": left_depth
}
right_header = {
"compression": OpenEXR.DWAA_COMPRESSION,
"type": OpenEXR.scanlineimage,
"view": "right",
"stereoscopic": True,
"eyeSeparation": eye_separation
}
right_channels = {
"RGB": right_rgb,
"Z": right_depth
}
# Create stereo multi-part file
stereo_parts = [
OpenEXR.Part(left_header, left_channels, "left"),
OpenEXR.Part(right_header, right_channels, "right")
]
multipart_path = output_path.replace(".exr", "_multipart.exr")
with OpenEXR.File(stereo_parts) as outfile:
outfile.write(multipart_path)
# Method 2: Single-part with view-named channels
combined_header = {
"compression": OpenEXR.DWAA_COMPRESSION,
"type": OpenEXR.scanlineimage,
"multiView": ["left", "right"],
"stereoscopic": True,
"eyeSeparation": eye_separation,
"software": "Stereo Renderer v1.0"
}
# View-specific channel naming
combined_channels = {
"RGB.left": left_rgb,
"RGB.right": right_rgb,
"Z.left": left_depth,
"Z.right": right_depth
}
combined_path = output_path.replace(".exr", "_combined.exr")
with OpenEXR.File(combined_header, combined_channels) as outfile:
outfile.write(combined_path)
print(f"Created stereo files:")
print(f" Multi-part: {multipart_path}")
print(f" Combined: {combined_path}")
print(f" Eye separation: {eye_separation}m")
print(f" Parallax shift: {shift_pixels} pixels")
return multipart_path, combined_path
# Create stereo image pair
left_path, combined_path = create_stereo_exr("stereo_image.exr", (1080, 1920))
# Read stereo multi-part file
print("\nReading multi-part stereo file:")
with OpenEXR.File(left_path) as infile:
# Left eye (part 0)
left_header = infile.header(0)
left_channels = infile.channels(0)
left_view = left_header.get("view", "unknown")
# Right eye (part 1)
right_header = infile.header(1)
right_channels = infile.channels(1)
right_view = right_header.get("view", "unknown")
print(f" Part 0: {left_view} eye - {list(left_channels.keys())}")
print(f" Part 1: {right_view} eye - {list(right_channels.keys())}")
# Read combined stereo file
print("\nReading combined stereo file:")
with OpenEXR.File(combined_path) as infile:
header = infile.header()
channels = infile.channels()
views = header.get("multiView", [])
view_channels = [ch for ch in channels.keys() if "." in ch]
print(f" Views: {views}")
print(f" View channels: {view_channels}")
# Extract per-view data
for view in views:
view_rgb = channels.get(f"RGB.{view}")
view_depth = channels.get(f"Z.{view}")
if view_rgb:
print(f" {view.capitalize()} eye RGB: {view_rgb.pixels.shape}")Install with Tessl CLI
npx tessl i tessl/pypi-openexr