or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

advanced-features.mdcpp-api.mdfile-io.mdimage-data.mdindex.mdmetadata.md

advanced-features.mddocs/

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

```