0
# Advanced Features
1
2
Multi-part files, deep compositing images, tiled storage formats, and advanced compression methods for high-performance professional workflows.
3
4
## Capabilities
5
6
### Multi-Part Files
7
8
Container format supporting multiple independent images in a single file, essential for VFX workflows with beauty passes, depth maps, motion vectors, and auxiliary data.
9
10
```python { .api }
11
class Part:
12
def __init__(self, header: dict, channels: dict, name: str):
13
"""
14
Create image part for multi-part EXR files.
15
16
Args:
17
header: Part-specific metadata and format settings
18
channels: Channel data dictionary for this part
19
name: Human-readable part identifier
20
"""
21
22
def name(self) -> str:
23
"""Get part name identifier."""
24
25
def shape(self) -> tuple:
26
"""Get image dimensions as (height, width)."""
27
28
def width(self) -> int:
29
"""Get image width in pixels."""
30
31
def height(self) -> int:
32
"""Get image height in pixels."""
33
34
def compression(self):
35
"""Get compression method for this part."""
36
37
def type(self):
38
"""Get image type constant (scanline/tiled/deep)."""
39
40
def typeString(self) -> str:
41
"""Get human-readable image type."""
42
43
header: dict # Part header attributes
44
channels: dict # Channel name to data mapping
45
part_index: int # Zero-based part index
46
47
# Multi-part file creation
48
def create_multipart_file(parts: list, filename: str):
49
"""
50
Create multi-part EXR file.
51
52
Args:
53
parts: List of Part objects
54
filename: Output file path
55
"""
56
```
57
58
### Deep Image Support
59
60
Multi-sample per pixel data structures for advanced compositing, volumetric rendering, and motion blur effects.
61
62
```python { .api }
63
# Deep image type constants
64
OpenEXR.deepscanline: int # Deep scanline format
65
OpenEXR.deeptile: int # Deep tiled format
66
67
# Deep image structure (conceptual representation)
68
class DeepImageData:
69
"""
70
Deep image data with variable samples per pixel.
71
Used for volume rendering, motion blur, and compositing.
72
"""
73
74
def __init__(self, width: int, height: int):
75
"""Initialize deep image structure."""
76
77
sampleCounts: numpy.ndarray # Per-pixel sample counts (height, width)
78
sampleData: dict # Channel name to sample data arrays
79
totalSamples: int # Total samples across all pixels
80
81
def setSampleCount(self, x: int, y: int, count: int):
82
"""Set sample count for specific pixel."""
83
84
def getSampleCount(self, x: int, y: int) -> int:
85
"""Get sample count for specific pixel."""
86
87
def setSampleData(self, channel: str, x: int, y: int, samples: list):
88
"""Set sample data for channel at pixel."""
89
90
def getSampleData(self, channel: str, x: int, y: int) -> list:
91
"""Get sample data for channel at pixel."""
92
```
93
94
### Tiled Image Format
95
96
Block-based storage enabling efficient random access, streaming, and multi-resolution representations.
97
98
```python { .api }
99
# Tiled image configuration
100
class TileDescription:
101
def __init__(self, xSize: int, ySize: int, mode: int, roundingMode: int = None):
102
"""
103
Configure tiled image parameters.
104
105
Args:
106
xSize: Tile width in pixels
107
ySize: Tile height in pixels
108
mode: Level mode (ONE_LEVEL/MIPMAP_LEVELS/RIPMAP_LEVELS)
109
roundingMode: Level rounding (ROUND_DOWN/ROUND_UP)
110
"""
111
112
xSize: int # Tile width
113
ySize: int # Tile height
114
mode: int # Level generation mode
115
roundingMode: int # Dimension rounding mode
116
117
# Level modes for tiled images
118
from OpenEXR import Imath
119
120
Imath.LevelMode.ONE_LEVEL: int # Single resolution level
121
Imath.LevelMode.MIPMAP_LEVELS: int # Square mipmaps (power of 2)
122
Imath.LevelMode.RIPMAP_LEVELS: int # Rectangular ripmaps (independent X/Y)
123
124
Imath.LevelRoundingMode.ROUND_DOWN: int # Round dimensions down
125
Imath.LevelRoundingMode.ROUND_UP: int # Round dimensions up
126
127
# Tiled image header
128
tiled_header = {
129
"type": OpenEXR.tiledimage,
130
"tiles": {
131
"xSize": 64, # Tile width
132
"ySize": 64, # Tile height
133
"mode": Imath.LevelMode.MIPMAP_LEVELS, # Generate mipmaps
134
"roundingMode": Imath.LevelRoundingMode.ROUND_DOWN
135
}
136
}
137
```
138
139
### Compression Methods
140
141
Advanced compression algorithms optimized for different content types and quality requirements.
142
143
```python { .api }
144
# Lossless compression methods
145
OpenEXR.NO_COMPRESSION: int # No compression (fastest)
146
OpenEXR.RLE_COMPRESSION: int # Run-length encoding (simple)
147
OpenEXR.ZIPS_COMPRESSION: int # ZIP scanline compression
148
OpenEXR.ZIP_COMPRESSION: int # ZIP block compression
149
OpenEXR.PIZ_COMPRESSION: int # PIZ wavelet compression (best ratio)
150
151
# Lossy compression methods
152
OpenEXR.PXR24_COMPRESSION: int # PXR24 (24-bit RGB lossy)
153
OpenEXR.B44_COMPRESSION: int # B44 (lossy for some channels)
154
OpenEXR.B44A_COMPRESSION: int # B44A (adaptive lossy)
155
OpenEXR.DWAA_COMPRESSION: int # DWAA (lossy, good for VFX)
156
OpenEXR.DWAB_COMPRESSION: int # DWAB (lossy, better compression)
157
158
# Future compression methods
159
OpenEXR.HTJ2K256_COMPRESSION: int # JPEG 2000 HTJ2K (256x256 blocks)
160
OpenEXR.HTJ2K32_COMPRESSION: int # JPEG 2000 HTJ2K (32x32 blocks)
161
162
# Compression configuration
163
def configure_compression(compression_type: int, **kwargs):
164
"""
165
Configure compression parameters.
166
167
Args:
168
compression_type: Compression method constant
169
**kwargs: Compression-specific parameters
170
"""
171
# DWA compression quality (0.0-100.0)
172
if compression_type in (OpenEXR.DWAA_COMPRESSION, OpenEXR.DWAB_COMPRESSION):
173
quality = kwargs.get("quality", 45.0) # Default quality
174
175
# ZIP compression level (1-9)
176
if compression_type in (OpenEXR.ZIP_COMPRESSION, OpenEXR.ZIPS_COMPRESSION):
177
level = kwargs.get("level", 6) # Default level
178
```
179
180
### Stereo and Multi-View Support
181
182
Multi-view image storage for stereoscopic content and camera arrays.
183
184
```python { .api }
185
# Multi-view attributes
186
multiview_header = {
187
"type": OpenEXR.scanlineimage,
188
"multiView": [ # List of view names
189
"left", "right" # Stereo views
190
# or ["cam1", "cam2", "cam3", "cam4"] # Camera array
191
],
192
"view": str, # Current view name for single-view parts
193
}
194
195
# Multi-view channel naming convention
196
stereo_channels = {
197
"RGB.left": left_rgb_data, # Left eye RGB
198
"RGB.right": right_rgb_data, # Right eye RGB
199
"Z.left": left_depth_data, # Left eye depth
200
"Z.right": right_depth_data, # Right eye depth
201
}
202
203
# Alternative: separate parts per view
204
left_part = OpenEXR.Part(left_header, left_channels, "left")
205
right_part = OpenEXR.Part(right_header, right_channels, "right")
206
```
207
208
### Preview Images
209
210
Embedded thumbnail images for fast preview without decoding full resolution data.
211
212
```python { .api }
213
from OpenEXR import Imath
214
215
class PreviewImage:
216
def __init__(self, width: int, height: int, pixels: bytes):
217
"""
218
Create preview image.
219
220
Args:
221
width: Preview width in pixels
222
height: Preview height in pixels
223
pixels: RGBA pixel data (4 bytes per pixel)
224
"""
225
226
width: int # Preview width
227
height: int # Preview height
228
pixels: bytes # RGBA8 pixel data
229
230
# Add preview to header
231
preview_header = {
232
"compression": OpenEXR.ZIP_COMPRESSION,
233
"type": OpenEXR.scanlineimage,
234
"preview": PreviewImage(160, 120, rgba_thumbnail_bytes)
235
}
236
```
237
238
### Environment Maps
239
240
Specialized support for environment mapping and spherical images.
241
242
```python { .api }
243
# Environment map attributes
244
envmap_header = {
245
"compression": OpenEXR.ZIP_COMPRESSION,
246
"type": OpenEXR.scanlineimage,
247
248
# Environment map metadata
249
"envmap": str, # Environment map type
250
# Values: "latlong", "cube", "sphere"
251
252
# Spherical coordinates
253
"wrapmodes": str, # Wrap modes ("clamp", "repeat", "mirror")
254
255
# Cube face mapping (for cube maps)
256
"cubeFace": str, # Face identifier ("+X", "-X", "+Y", "-Y", "+Z", "-Z")
257
}
258
```
259
260
## Usage Examples
261
262
### Multi-Part VFX Pipeline
263
264
```python
265
import OpenEXR
266
import numpy as np
267
268
def create_vfx_multipart_file(output_path, image_size):
269
"""Create comprehensive VFX multi-part EXR file."""
270
271
height, width = image_size
272
273
# Beauty pass (primary image)
274
beauty_rgb = np.random.rand(height, width, 3).astype(np.float16)
275
beauty_alpha = np.ones((height, width), dtype=np.float16)
276
beauty_header = {
277
"compression": OpenEXR.DWAA_COMPRESSION,
278
"type": OpenEXR.scanlineimage,
279
"renderPass": "beauty",
280
"samples": 512
281
}
282
beauty_channels = {
283
"RGB": beauty_rgb,
284
"A": beauty_alpha
285
}
286
287
# Depth pass (Z-buffer)
288
depth_data = np.random.exponential(10.0, (height, width)).astype(np.float32)
289
depth_header = {
290
"compression": OpenEXR.ZIP_COMPRESSION,
291
"type": OpenEXR.scanlineimage,
292
"renderPass": "depth"
293
}
294
depth_channels = {"Z": depth_data}
295
296
# Normal pass (surface normals)
297
normals = np.random.normal(0, 1, (height, width, 3)).astype(np.float32)
298
# Normalize to unit vectors
299
norm_length = np.sqrt(np.sum(normals**2, axis=2, keepdims=True))
300
normals = normals / norm_length
301
normal_header = {
302
"compression": OpenEXR.ZIP_COMPRESSION,
303
"type": OpenEXR.scanlineimage,
304
"renderPass": "normal"
305
}
306
normal_channels = {"N": normals}
307
308
# Motion vector pass
309
motion_x = np.random.normal(0, 2, (height, width)).astype(np.float32)
310
motion_y = np.random.normal(0, 2, (height, width)).astype(np.float32)
311
motion_header = {
312
"compression": OpenEXR.ZIP_COMPRESSION,
313
"type": OpenEXR.scanlineimage,
314
"renderPass": "motion"
315
}
316
motion_channels = {
317
"motion.X": motion_x,
318
"motion.Y": motion_y
319
}
320
321
# ID mattes (object and material IDs)
322
object_ids = np.random.randint(0, 100, (height, width), dtype=np.uint32)
323
material_ids = np.random.randint(0, 50, (height, width), dtype=np.uint32)
324
id_header = {
325
"compression": OpenEXR.ZIP_COMPRESSION,
326
"type": OpenEXR.scanlineimage,
327
"renderPass": "id"
328
}
329
id_channels = {
330
"objectID": object_ids,
331
"materialID": material_ids
332
}
333
334
# Cryptomatte pass (for advanced compositing)
335
crypto_data = np.random.rand(height, width, 4).astype(np.float32)
336
crypto_header = {
337
"compression": OpenEXR.ZIP_COMPRESSION,
338
"type": OpenEXR.scanlineimage,
339
"renderPass": "cryptomatte",
340
"cryptomatte/id": "cryptomatte_object",
341
"cryptomatte/hash": "MurmurHash3_32",
342
"cryptomatte/conversion": "uint32_to_float32"
343
}
344
crypto_channels = {"crypto": crypto_data}
345
346
# Create multi-part file
347
parts = [
348
OpenEXR.Part(beauty_header, beauty_channels, "beauty"),
349
OpenEXR.Part(depth_header, depth_channels, "depth"),
350
OpenEXR.Part(normal_header, normal_channels, "normal"),
351
OpenEXR.Part(motion_header, motion_channels, "motion"),
352
OpenEXR.Part(id_header, id_channels, "id"),
353
OpenEXR.Part(crypto_header, crypto_channels, "cryptomatte")
354
]
355
356
with OpenEXR.File(parts) as outfile:
357
outfile.write(output_path)
358
359
print(f"Created VFX multi-part file: {output_path}")
360
return len(parts)
361
362
# Create 2K VFX file
363
num_parts = create_vfx_multipart_file("vfx_shot.exr", (1556, 2048))
364
print(f"Created {num_parts} parts in VFX file")
365
366
# Read and examine multi-part file
367
with OpenEXR.File("vfx_shot.exr") as infile:
368
for i in range(6): # We know we have 6 parts
369
header = infile.header(i)
370
channels = infile.channels(i)
371
372
pass_name = header.get("renderPass", f"part_{i}")
373
channel_names = list(channels.keys())
374
375
print(f"Part {i} ({pass_name}): {channel_names}")
376
377
# Access specific pass data
378
if pass_name == "beauty":
379
beauty_rgb = channels["RGB"].pixels
380
beauty_alpha = channels["A"].pixels
381
print(f" Beauty RGB: {beauty_rgb.shape} {beauty_rgb.dtype}")
382
383
elif pass_name == "depth":
384
depth_z = channels["Z"].pixels
385
print(f" Depth range: {depth_z.min():.3f} - {depth_z.max():.3f}")
386
387
elif pass_name == "motion":
388
motion_x = channels["motion.X"].pixels
389
motion_y = channels["motion.Y"].pixels
390
motion_magnitude = np.sqrt(motion_x**2 + motion_y**2)
391
print(f" Motion magnitude: {motion_magnitude.mean():.3f} ± {motion_magnitude.std():.3f}")
392
```
393
394
### Tiled Image with Mipmaps
395
396
```python
397
import OpenEXR
398
from OpenEXR import Imath
399
import numpy as np
400
401
def create_tiled_image_with_mipmaps(output_path, base_size):
402
"""Create tiled EXR with mipmap levels for efficient access."""
403
404
height, width = base_size
405
406
# Generate base level image data
407
base_image = np.random.rand(height, width, 3).astype('f')
408
409
# Configure tiled format with mipmaps
410
tile_desc = {
411
"xSize": 64, # 64x64 tiles
412
"ySize": 64,
413
"mode": Imath.LevelMode.MIPMAP_LEVELS, # Generate mipmaps
414
"roundingMode": Imath.LevelRoundingMode.ROUND_DOWN
415
}
416
417
tiled_header = {
418
"compression": OpenEXR.ZIP_COMPRESSION,
419
"type": OpenEXR.tiledimage,
420
"tiles": tile_desc,
421
422
# Tiled image metadata
423
"software": "Tiled Image Generator v1.0",
424
"comments": "Tiled format with mipmap levels for efficient random access"
425
}
426
427
channels = {"RGB": base_image}
428
429
with OpenEXR.File(tiled_header, channels) as outfile:
430
outfile.write(output_path)
431
432
print(f"Created tiled image: {output_path}")
433
print(f"Base level: {width}x{height}")
434
435
# Calculate mipmap levels
436
level = 0
437
while width > 1 or height > 1:
438
level += 1
439
width = max(1, width // 2)
440
height = max(1, height // 2)
441
print(f"Level {level}: {width}x{height}")
442
443
# Create 4K tiled image
444
create_tiled_image_with_mipmaps("tiled_4k.exr", (2160, 3840))
445
446
# Read tiled image (conceptual - actual tile access requires C++ API)
447
with OpenEXR.File("tiled_4k.exr") as infile:
448
header = infile.header()
449
450
# Check if tiled
451
if header.get("type") == OpenEXR.tiledimage:
452
tile_info = header.get("tiles", {})
453
print(f"Tile size: {tile_info.get('xSize')}x{tile_info.get('ySize')}")
454
print(f"Level mode: {tile_info.get('mode')}")
455
456
# Access full resolution data
457
channels = infile.channels()
458
rgb_data = channels["RGB"].pixels
459
print(f"Full resolution data: {rgb_data.shape}")
460
```
461
462
### Deep Compositing Image
463
464
```python
465
import OpenEXR
466
import numpy as np
467
468
def create_deep_image_example(output_path, image_size):
469
"""Create deep EXR file with multiple samples per pixel."""
470
471
height, width = image_size
472
473
# Simulate deep compositing data
474
# In practice, this would come from a volume renderer or deep compositing system
475
476
# Create sample count array (number of samples per pixel)
477
# Most pixels have 1-3 samples, some have more for complex areas
478
base_samples = np.ones((height, width), dtype=np.uint32)
479
complex_area = np.random.random((height, width)) > 0.8 # 20% complex pixels
480
base_samples[complex_area] = np.random.randint(2, 8, np.sum(complex_area))
481
482
total_samples = np.sum(base_samples)
483
print(f"Total samples: {total_samples} across {height*width} pixels")
484
print(f"Average samples per pixel: {total_samples / (height*width):.2f}")
485
486
# Create flattened sample data
487
# Each sample has RGBA + Z + ID data
488
sample_rgba = np.random.rand(total_samples, 4).astype('f')
489
sample_z = np.random.exponential(5.0, total_samples).astype('f')
490
sample_id = np.random.randint(0, 100, total_samples, dtype=np.uint32)
491
492
# Deep image header
493
deep_header = {
494
"compression": OpenEXR.ZIP_COMPRESSION,
495
"type": OpenEXR.deepscanline, # Deep scanline format
496
497
# Deep image metadata
498
"software": "Deep Compositor v2.0",
499
"comments": "Deep compositing data with multiple samples per pixel",
500
"renderPass": "deep_beauty",
501
"deepData": True
502
}
503
504
# Note: Deep image channel creation is conceptual
505
# Actual implementation requires specialized deep image classes
506
deep_channels = {
507
"R": sample_rgba[:, 0],
508
"G": sample_rgba[:, 1],
509
"B": sample_rgba[:, 2],
510
"A": sample_rgba[:, 3],
511
"Z": sample_z,
512
"ID": sample_id,
513
# Special metadata for deep format
514
"_sampleCounts": base_samples, # Samples per pixel
515
"_totalSamples": total_samples # Total sample count
516
}
517
518
# This is conceptual - actual deep image writing requires C++ API
519
print("Deep image creation (conceptual):")
520
print(f" Sample counts shape: {base_samples.shape}")
521
print(f" Sample data length: {len(sample_rgba)}")
522
print(f" Channel types: RGBA=float, Z=float, ID=uint")
523
524
return deep_header, deep_channels
525
526
# Create deep compositing data
527
deep_header, deep_channels = create_deep_image_example("deep_comp.exr", (540, 960))
528
```
529
530
### Advanced Compression Comparison
531
532
```python
533
import OpenEXR
534
import numpy as np
535
import os
536
import time
537
538
def compression_benchmark(image_data, output_dir="compression_test"):
539
"""Compare different compression methods for image data."""
540
541
os.makedirs(output_dir, exist_ok=True)
542
543
# Test different compression methods
544
compressions = [
545
("no_compression", OpenEXR.NO_COMPRESSION),
546
("rle", OpenEXR.RLE_COMPRESSION),
547
("zip", OpenEXR.ZIP_COMPRESSION),
548
("zips", OpenEXR.ZIPS_COMPRESSION),
549
("piz", OpenEXR.PIZ_COMPRESSION),
550
("pxr24", OpenEXR.PXR24_COMPRESSION),
551
("b44", OpenEXR.B44_COMPRESSION),
552
("b44a", OpenEXR.B44A_COMPRESSION),
553
("dwaa", OpenEXR.DWAA_COMPRESSION),
554
("dwab", OpenEXR.DWAB_COMPRESSION)
555
]
556
557
results = []
558
559
for name, compression in compressions:
560
print(f"Testing {name}...")
561
562
header = {
563
"compression": compression,
564
"type": OpenEXR.scanlineimage
565
}
566
channels = {"RGB": image_data}
567
568
filename = os.path.join(output_dir, f"test_{name}.exr")
569
570
# Time compression
571
start_time = time.time()
572
try:
573
with OpenEXR.File(header, channels) as outfile:
574
outfile.write(filename)
575
write_time = time.time() - start_time
576
577
# Get file size
578
file_size = os.path.getsize(filename)
579
580
# Time decompression
581
start_time = time.time()
582
with OpenEXR.File(filename) as infile:
583
_ = infile.channels()["RGB"].pixels
584
read_time = time.time() - start_time
585
586
results.append({
587
"name": name,
588
"compression": compression,
589
"file_size_mb": file_size / (1024 * 1024),
590
"write_time": write_time,
591
"read_time": read_time,
592
"compression_ratio": (image_data.nbytes / file_size),
593
"success": True
594
})
595
596
except Exception as e:
597
print(f" Failed: {e}")
598
results.append({
599
"name": name,
600
"success": False,
601
"error": str(e)
602
})
603
604
# Print results
605
print("\nCompression Benchmark Results:")
606
print("=" * 80)
607
print(f"{'Method':<12} {'Size (MB)':<10} {'Ratio':<8} {'Write (s)':<10} {'Read (s)':<10}")
608
print("-" * 80)
609
610
for result in results:
611
if result["success"]:
612
print(f"{result['name']:<12} "
613
f"{result['file_size_mb']:<10.2f} "
614
f"{result['compression_ratio']:<8.1f} "
615
f"{result['write_time']:<10.3f} "
616
f"{result['read_time']:<10.3f}")
617
else:
618
print(f"{result['name']:<12} FAILED: {result['error']}")
619
620
return results
621
622
# Create test image (4K resolution)
623
height, width = 2160, 3840
624
test_image = np.random.rand(height, width, 3).astype('f')
625
626
print(f"Testing compression on {width}x{height} RGB image")
627
print(f"Uncompressed size: {test_image.nbytes / (1024*1024):.1f} MB")
628
629
# Run benchmark
630
results = compression_benchmark(test_image)
631
632
# Find best compression for different criteria
633
successful = [r for r in results if r["success"]]
634
635
if successful:
636
best_ratio = max(successful, key=lambda x: x["compression_ratio"])
637
fastest_write = min(successful, key=lambda x: x["write_time"])
638
fastest_read = min(successful, key=lambda x: x["read_time"])
639
smallest_file = min(successful, key=lambda x: x["file_size_mb"])
640
641
print(f"\nBest Results:")
642
print(f" Best ratio: {best_ratio['name']} ({best_ratio['compression_ratio']:.1f}x)")
643
print(f" Fastest write: {fastest_write['name']} ({fastest_write['write_time']:.3f}s)")
644
print(f" Fastest read: {fastest_read['name']} ({fastest_read['read_time']:.3f}s)")
645
print(f" Smallest file: {smallest_file['name']} ({smallest_file['file_size_mb']:.2f} MB)")
646
```
647
648
### Stereo Image Creation
649
650
```python
651
import OpenEXR
652
import numpy as np
653
654
def create_stereo_exr(output_path, image_size, eye_separation=0.065):
655
"""Create stereoscopic EXR file with left and right eye views."""
656
657
height, width = image_size
658
659
# Simulate stereo image pair
660
# In practice, these would be renders from offset camera positions
661
662
# Base scene
663
base_scene = np.random.rand(height, width, 3).astype('f')
664
665
# Simulate parallax shift for stereo effect
666
shift_pixels = int(eye_separation * width / 0.1) # Approximate parallax shift
667
668
# Left eye view (base)
669
left_rgb = base_scene
670
left_depth = np.random.exponential(10.0, (height, width)).astype('f')
671
672
# Right eye view (shifted for parallax)
673
right_rgb = np.roll(base_scene, shift_pixels, axis=1) # Horizontal shift
674
right_depth = np.roll(left_depth, shift_pixels, axis=1)
675
676
# Method 1: Multi-part stereo file
677
left_header = {
678
"compression": OpenEXR.DWAA_COMPRESSION,
679
"type": OpenEXR.scanlineimage,
680
"view": "left",
681
"stereoscopic": True,
682
"eyeSeparation": eye_separation
683
}
684
left_channels = {
685
"RGB": left_rgb,
686
"Z": left_depth
687
}
688
689
right_header = {
690
"compression": OpenEXR.DWAA_COMPRESSION,
691
"type": OpenEXR.scanlineimage,
692
"view": "right",
693
"stereoscopic": True,
694
"eyeSeparation": eye_separation
695
}
696
right_channels = {
697
"RGB": right_rgb,
698
"Z": right_depth
699
}
700
701
# Create stereo multi-part file
702
stereo_parts = [
703
OpenEXR.Part(left_header, left_channels, "left"),
704
OpenEXR.Part(right_header, right_channels, "right")
705
]
706
707
multipart_path = output_path.replace(".exr", "_multipart.exr")
708
with OpenEXR.File(stereo_parts) as outfile:
709
outfile.write(multipart_path)
710
711
# Method 2: Single-part with view-named channels
712
combined_header = {
713
"compression": OpenEXR.DWAA_COMPRESSION,
714
"type": OpenEXR.scanlineimage,
715
"multiView": ["left", "right"],
716
"stereoscopic": True,
717
"eyeSeparation": eye_separation,
718
"software": "Stereo Renderer v1.0"
719
}
720
721
# View-specific channel naming
722
combined_channels = {
723
"RGB.left": left_rgb,
724
"RGB.right": right_rgb,
725
"Z.left": left_depth,
726
"Z.right": right_depth
727
}
728
729
combined_path = output_path.replace(".exr", "_combined.exr")
730
with OpenEXR.File(combined_header, combined_channels) as outfile:
731
outfile.write(combined_path)
732
733
print(f"Created stereo files:")
734
print(f" Multi-part: {multipart_path}")
735
print(f" Combined: {combined_path}")
736
print(f" Eye separation: {eye_separation}m")
737
print(f" Parallax shift: {shift_pixels} pixels")
738
739
return multipart_path, combined_path
740
741
# Create stereo image pair
742
left_path, combined_path = create_stereo_exr("stereo_image.exr", (1080, 1920))
743
744
# Read stereo multi-part file
745
print("\nReading multi-part stereo file:")
746
with OpenEXR.File(left_path) as infile:
747
# Left eye (part 0)
748
left_header = infile.header(0)
749
left_channels = infile.channels(0)
750
left_view = left_header.get("view", "unknown")
751
752
# Right eye (part 1)
753
right_header = infile.header(1)
754
right_channels = infile.channels(1)
755
right_view = right_header.get("view", "unknown")
756
757
print(f" Part 0: {left_view} eye - {list(left_channels.keys())}")
758
print(f" Part 1: {right_view} eye - {list(right_channels.keys())}")
759
760
# Read combined stereo file
761
print("\nReading combined stereo file:")
762
with OpenEXR.File(combined_path) as infile:
763
header = infile.header()
764
channels = infile.channels()
765
766
views = header.get("multiView", [])
767
view_channels = [ch for ch in channels.keys() if "." in ch]
768
769
print(f" Views: {views}")
770
print(f" View channels: {view_channels}")
771
772
# Extract per-view data
773
for view in views:
774
view_rgb = channels.get(f"RGB.{view}")
775
view_depth = channels.get(f"Z.{view}")
776
if view_rgb:
777
print(f" {view.capitalize()} eye RGB: {view_rgb.pixels.shape}")
778
```