OpenEXR

edit

OpenEXR stores scene-linear, high dynamic range images in 16-bit (half) or 32-bit floating-point samples, as rendering and visual-effects tools produce them. Files load as RgbaHalf or RgbaFloat pixels, values above 1 included, and OpenExrEncoder compresses the stored samples losslessly. Conversion between the library's straight colors and OpenEXR's premultiplied colors can round them; this is not a general bit-exact source-pixel round-trip guarantee.

What is read and written

ReadingWriting
FilesSingle-part scan-line and tiled files (the full-resolution level of mipmaps and ripmaps)Single-part scan-line files
SamplesHALF, FLOAT and UINTHALF or FLOAT
ChannelsR, G, B, A, or Y (luminance) with or without A; or one layer of a file with other channelsR, G, B (and A for pixel formats with alpha), or Y for gray pixel formats
WindowsThe display window; an overscan cropped to it, or the data windowBoth windows are the image
CompressionNONE, RLE, ZIPS, ZIP, PIZ, PXR24, B44, B44ANONE, RLE, ZIPS, ZIP (default) or PIZ, all lossless
MetadataChromaticities, the windowsChromaticities

A file of half samples without an alpha channel loads as RgbaHalf, any other as RgbaFloat, in linear light. The extra precision is used when dividing colors by alpha. Infinities and samples that are not numbers are kept, subject to the alpha conversion.

Load and save an OpenEXR file

public static Image<Rgba32> ConvertOpenExr(string exrPath, string pizPath)
{
    // Half-only files without alpha load as RgbaHalf; other OpenEXR files load as RgbaFloat, in linear light
    using var hdr = Image.Load(exrPath);

    // PIZ compresses the stored samples losslessly, values above 1 included; alpha conversion can round colors
    hdr.Save(pizPath, new OpenExrEncoder { Compression = OpenExrCompression.Piz });

    // Integer pixels cannot hold the highlights: a typed load compresses them with a tone mapping
    return Image.Load<Rgba32>(exrPath, new ImageDecodeOptions
    {
        Conversion = new PixelConversionOptions { ToneMapping = new HdrToneMappingOptions() },
    });
}

Integer pixels cannot hold values above 1: loading an OpenEXR file as Rgba32 or Rgba64 rejects them, unless the conversion clips them or compresses the highlights with a tone mapping, as in the example. The same applies to saving floating-point images to other formats, with ImageEncoder.FloatingPointConversion. See Pixel formats.

Read a layer of a renderer output

Renderers write depth (Z), other outputs in layers (diffuse.R, diffuse.G...) and often an overscan: a data window larger than the display window. The default options reject such a file, since those channels and pixels would be lost. ImageConfiguration.OpenExrDecoding selects what is read:

public static (Image Image, Point Origin) LoadRenderLayer(string exrPath, string layer)
{
    // Renderers store RGBA next to depth (Z) and other layers (diffuse.R, diffuse.G, ...), often with an overscan
    // around the display window: the default options reject such a file, since those channels and pixels would be lost
    var configuration = new ImageConfiguration
    {
        OpenExrDecoding = new OpenExrDecodingOptions
        {
            // "" is the default layer (R, G, B, A or Y); "diffuse" selects diffuse.R, diffuse.G, diffuse.B, diffuse.A
            Layer = layer,

            // DataWindow keeps every stored pixel instead
            WindowMode = OpenExrWindowMode.CropToDisplayWindow,
        },
    };

    // Identify, readers and collections take the same configuration, and report the same image
    var image = Image.Load(exrPath, new ImageDecodeOptions { Configuration = configuration });

    // The windows of the file, in its pixel coordinates: the cropped image starts at the display window
    var windows = image.Metadata.OpenExrWindows!;
    return (image, windows.DisplayWindowMin);
}
OptionWhat it doesDefault
LayerThe layer to decode: "" for the default layer, "diffuse" for diffuse.R, diffuse.G, diffuse.B and diffuse.A; the other channels are ignorednull: every channel must be R, G, B, A or Y
WindowModeDisplayWindow, CropToDisplayWindow (the pixels outside the display window are discarded) or DataWindow (every stored pixel is the image)DisplayWindow: an overscan is rejected

The options apply to identification, eager loads, readers and collections alike. ImageMetadata.OpenExrWindows gives the two windows of the file; it is informational, and the encoder writes both windows from (0, 0). The decoder never falls back to another layer: a layer without color channel, a layer with RGB and luminance channels, a Y channel next to an X channel (a vector such as N.X, N.Y, N.Z, or CIE XYZ colors) and the chroma channels RY and BY are rejected with an UnsupportedImageFeatureException.

Options

OptionWhat it doesDefault
CompressionNone, Rle, Zips, Zip or Piz; PIZ is usually the smallest for photos and noisy imagesZip
PixelTypeHalf or Float; null chooses half for RgbaHalf and 8-bit pixels, float otherwisenull
AllowBitDepthReductionLets RgbaFloat and 16-bit pixels be rounded to half samples when PixelType is Halffalse
MetadataHandlingWhat to do with metadata OpenEXR cannot storeStrict

By default, RgbaHalf and 8-bit pixels are written as half samples, and RgbaFloat and 16-bit pixels as float samples. Integer pixels are written as their value divided by the largest sample (v / max).

Nonzero alpha multiplies the colors on encoding and divides them on decoding. Rounding, underflow or overflow can change the colors even with the default sample type. A zero alpha keeps its colors without multiplication or division. Lossless compression preserves the resulting stored samples, independently of integer normalization, half-sample rounding and alpha conversion.

Pitfalls

  • OpenEXR samples are linear light. An image labeled sRGB, which is the label of integer pixel formats, or with an ICC profile follows MetadataHandling, which rejects it by default: saving an 8-bit PNG as OpenEXR throws. A CICP description with linear transfer characteristics and BT.709, BT.2020, DCI-P3 or Display P3 primaries is written as chromaticities.
  • Compressed output needs a seekable destination (the offset table precedes the chunks); None does not.
  • Half samples are limited to ±65,504: a larger finite value is rejected when it is written as half.
  • EXIF, XMP, an orientation, a resolution and text entries cannot be stored, and the attributes of a file other than its windows and chromaticities are not read.
  • Multi-part files, deep data, the DWAA, DWAB, HTJ2K and ZSTD compressions, the chroma channels RY and BY, and, unless a layer is selected, channels other than R, G, B, A and Y (layers, depth) are rejected with an UnsupportedImageFeatureException.
  • The decoder reads the full-resolution level of mipmaps and ripmaps; the lower levels and the preview image are not read.
  • A file whose writer stopped before patching its offset table (zeros in the table) is rejected by default; the lenient decoding rebuilds the table from the chunks, as OpenEXR does, when they are all there.