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

advanced-features.mddocs/

Advanced Features

Multi-part files, deep compositing images, tiled storage formats, and advanced compression methods for high-performance professional workflows.

Capabilities

Multi-Part Files

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

Deep Image Support

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

Tiled Image Format

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

Compression Methods

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 level

Stereo and Multi-View Support

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

Preview Images

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

Environment Maps

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

Usage Examples

Multi-Part VFX Pipeline

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

Tiled Image with Mipmaps

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

Deep Compositing Image

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

Advanced Compression Comparison

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

Stereo Image Creation

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

docs

advanced-features.md

cpp-api.md

file-io.md

image-data.md

index.md

metadata.md

tile.json