Overview

Introduction

Warning

This section is from OCIO v1 and has not been updated yet.

OpenColorIO (OCIO) is a complete color management solution geared towards motion picture production with an emphasis on visual effects and computer animation. As such, OCIO helps enforce a color management methodology that is required for the high fidelity color imaging in modern computer graphics. This section introduces those concepts and general workflow practices. Additional information can be found in Jeremy Selan’s Cinematic Color document.

While OCIO is a color management library, it’s only knowledge of color science comes from it’s execution of the transforms defined in the OCIO configuration file. These transforms are either defined by the end user in a custom OCIO config or inherited from the publicly available configs.

By specifying your desired config.ocio Config file in the local environment all OCIO compatible applications and software libraries will be able to see your defined color transform “universe”, and direct the transformation of image data from one defined OCIO.ColorSpace to another, in addition to the other transforms documented elsewhere.

Sony Pictures Imageworks Color Pipeline

This document describes a high-level overview on how to emulate the current color management practice at Sony Imageworks. It applies equally to all profiles used at Imageworks, including both the VFX and Animation profiles. It’s by no means a requirement to follow this workflow at your own facility, this is merely a guideline on how we choose to work.

General Pipeline Observations

  • All images, on disk, contain colorspace information as a substring within the filename. This is obeyed by all applications that load image, write images, or view images. File extensions and metadata are ignored with regards to color processing.

Example:

colorimage_lnf.exr  : lnf
dataimage_ncf.exr : ncf
plate_lg10.dpx : lg10
texture_dt8.tif : dt8

Note

File format extension does NOT imply a color space. Not all .dpx files are lg10. Not all .tif images are dt8.

  • The common file formats we use are exr, tif, dpx.

  • render outputs: exr

  • render inputs (mipmapped-textures): exr, tif (txtif)

  • photographic plates (scans): dpx

  • composite outputs: dpx, exr

  • on-set reference: (camera raw) NEF, CR2, etc.

  • painted textures: psd, tif

  • output proxies: jpg

  • All pipelines that need to be colorspace aware rely on Config.parseColorSpaceFromString.

  • Color configurations are show specific. The $OCIO environment variable is set as part of a ‘setshot’ process, before other applications are launched. Artists are not allowed to work across different shows without using a fresh shell + setshot.

  • While the list of colorspaces can be show specific, care is taken to maintain similar naming to the greatest extent feasible. This reduces artist confusion. Even if two color spaces are not identical across shows, if they serve a similar purpose they are named the same.

  • Example: We label 10-bit scanned film negatives as lg10. Even if two different shows use different acquisition film stocks, and rely on different linearization curves, they are both labeled lg10.

  • There is no explicit guarantee that image assets copied across shows will be transferable in a color-correct manner. For example, in the above film scan example, one would not expect that the linearized versions of scans processed on different shows to match. In practice, this is not a problematic issue as the colorspaces which are convenient to copy (such as texture assets) happen to be similarly defined across show profiles.

Rendering

  • Rendering and shading occurs in a scene-linear floating point space, typically named “ln”. Half-float (16-bit) images are labeled lnh, full float images (32-bit) are labeled lnf.

  • All image inputs should be converted to ln prior to render-time. Typically, this is done when textures are published. (See below)

  • Renderer outputs are always floating-point. Color outputs are typically stored as lnh (16-bit half float).

  • Data outputs (normals, depth data, etc) are stored as ncf (“not color” data, 32-bit full float). Lossy compression is never utilized.

  • Render outputs are always viewed with an OCIO compatible image viewer. Thus, for typical color imagery the lnf display transform will be applied. In Nuke, this can be emulated using the OCIODisplay node. A standalone image viewer, ociodisplay, is also included with OpenColorIO src/example.

Texture Painting / Matte Painting

  • Textures are painted either in a non-OCIO color-managed environment (Photoshop, etc), or a color managed one like Mari.

  • At texture publish time, before mipmaps are generated, all color processing is applied. Internally at SPI we use OpenImageIO’s maketx that also links to OpenColorIO. This code is available on the public OIIO repository. Color processing (linearization) is applied before mipmap generation in order to assure energy preservation in the render. If the opposite processing order were used, (mipmap in the original space, color convert in the shader), the apparent intensity of texture values would change as the object approached or receded from the camera.

  • The original texture filenames contain the colorspace information as a substring, to signify processing intent.

  • Textures that contain data (bump maps, opacity maps, blend maps, etc) are labeled with the nc colorspaces according to their bitdepth.

  • Example: an 8-bit opacity map -> skin_opacity_nc8.tif

  • Painted textures that are intended to modulate diffuse color components are labeled dt (standing for “diffuse texture”). The dt8 colorspace is designed such that, when linearized, values will not extend above 1.0. At texture publishing time these are converted to lnh mipmapped tiffs/exr. Note that as linear textures have greater allocation requirements, a bit depth promotion is required in this case. I.e., even if the original texture as painted was only 8-bits, the mipmapped texture will be stored as a 16-bit float image.

  • Painted environment maps, which may be emissive as labeled vd (standing for ‘video’). These values, when linearized, have the potential to generate specular information well above 1.0. Note that in the current vd linearization curves, the top code values may be very “sensitive”. I.e., very small changes in the initial code value (such as 254->255) may actually result in very large differences in the estimated scene-linear intensity. All environment maps are store as lnh mipmapped tiffs/exr. The same bit-depth promotion as in the dt8 case is required here.

Compositing

  • The majority of compositing operations happen in scene-linear, lnf, colorspace.

  • All image inputs are linearized to lnf as they are loaded. Customized input nodes make this processing convenient. Rendered elements, which are stored in linear already, do not require processing. Photographic plates will typically be linearized according to their source type, (lg10 for film scans, gn10 for genesis sources, etc).

  • All output images are de-linearized from lnf when they are written. A customized output node makes this convenient.

  • On occasion log data is required for certain processing operations. (Plate resizing, pulling keys, degrain, etc). For each show, a colorspace is specified as appropriate for this operation. The artist does not have to keep track of which colorspace is appropriate to use; the OCIOLogConvert node is always intended for this purpose. (Within the OCIO profile, this is specified using the ‘compositing_log’ role).

Further Information

Specific information with regard to the public OCIO configs can be found in the Configurations section.

Internal Architecture Overview

Warning

This section is from OCIO v1 and has not been updated yet.

External API

Configs

At the highest level, we have OCIO::Configs. This represents the entirety of the current color “universe”. Configs are serialized as .ocio files, read at runtime, and are often used in a ‘read-only’ context.

Config are loaded at runtime to allow for customized color handling in a show- dependent manner.

Example Configs:

  • ACES (Academy’s standard color workflow)

  • spi-vfx (Used on some Imageworks VFX shows such as spiderman, etc).

  • and others

ColorSpaces

The meat of an OCIO::Config is a list of named ColorSpaces. ColorSpace often correspond to input image states, output image states, or image states used for internal processing.

Example ColorSpaces (from ACES configuration):

  • aces (HDR, scene-linear)

  • adx10 (log-like density encoding space)

  • slogf35 (sony F35 slog camera encoding)

  • rrt_srgb (baked in display transform, suitable for srgb display)

  • rrt_p3dci (baked in display transform, suitable for dcip3 display)

Transforms

ColorSpaces contain an ordered list of transforms, which define the conversion to and from the Config’s “reference” space.

Transforms are the atomic units available to the designer in order to specify a color conversion.

Examples of OCIO::Transforms are:

  • File-based transforms (1d lut, 3d lut, mtx… anything, really.)

  • Math functions (gamma, log, mtx)

  • The ‘meta’ GroupTransform, which contains itself an ordered lists of transforms

  • The ‘meta’ LookTransform, which contains an ordered lists of transforms

For example, the adx10 ColorSpace (in one particular ACES configuration) -Transform FROM adx, to our reference ColorSpace:

  1. Apply FileTransform adx_adx10_to_cdd.spimtx

  2. Apply FileTransform adx_cdd_to_cid.spimtx

  3. Apply FileTransform adx_cid_to_rle.spi1d

  4. Apply LogTransform base 10 (inverse)

  5. Apply FileTransform adx_exp_to_aces.spimtx

If we have an image in the reference ColorSpace (unnamed), we can convert TO adx by applying each in the inverse direction:

  1. Apply FileTransform adx_exp_to_aces.spimtx (inverse)

  2. Apply LogTransform base 10 (forward)

  3. Apply FileTransform adx_cid_to_rle.spi1d (inverse)

  4. Apply FileTransform adx_cdd_to_cid.spimtx (inverse)

  5. Apply FileTransform adx_adx10_to_cdd.spimtx (inverse)

Note that this isn’t possible in all cases (what if a lut or matrix is not invertible?), but conceptually it’s a simple way to think about the design.

Summary

Configs and ColorSpaces are just a bookkeeping device used to get and ordered lists of Transforms corresponding to image color transformation.

Transforms are visible to the person AUTHORING the OCIO config, but are NOT visible to the client applications. Client apps need only concern themselves with Configs and Processors.

OCIO::Processors

A processor corresponds to a ‘baked’ color transformation. You specify two arguments when querying a processor: the colorspace_section you are coming from, and the colorspace_section you are going to.

Once you have the processor, you can apply the color transformation using the “apply” function. For the CPU veseion, first wrap your image in an ImageDesc class, and then call apply to process in place.

Example:

#include <OpenColorIO/OpenColorIO.h>
namespace OCIO = OCIO_NAMESPACE;

try
{
   // Get the global OpenColorIO config
   // This will auto-initialize (using $OCIO) on first use
   OCIO::ConstConfigRcPtr config = OCIO::GetCurrentConfig();

   // Get the processor corresponding to this transform.
   // These strings, in this example, are specific to the above
   // example. ColorSpace names should NEVER be hard-coded into client
   // software, but should be dynamically queried at runtime from the library
   OCIO::ConstProcessorRcPtr processor = config->getProcessor("adx10", "aces");
   OCIO::ConstCPUProcessorRcPtr cpu = processor->getDefaultCPUProcessor();

   // Wrap the image in a light-weight ImageDescription
   OCIO::PackedImageDesc img(imageData, w, h, 4);

   // Apply the color transformation (in place)
   cpu->apply(img);
}
catch(OCIO::Exception & exception)
{
   std::cerr << "OpenColorIO Error: " << exception.what() << std::endl;
}

The GPU code path is similar. You get the processor from the config, and then query the shaderText and the lut3d. The client loads these to the GPU themselves, and then makes the appropriate calls to the newly defined function.

See src/apps/ociodisplay for an example.

Internal API

The Op Abstraction

It is a useful abstraction, both for code-reuse and optimization, to not relying on the transforms to do pixel processing themselves.

Consider that the FileTransform represents a wide-range of image processing operations (basically all of em), many of which are really complex. For example, the houdini lut format in a single file may contain a log convert, a 1d lut, and then a 3d lut; all of which need to be applied in a row! If we don’t want the FileTransform to know how to process all possible pixel operations, it’s much simpler to make light-weight processing operations, which the transforms can create to do the dirty work as needed.

All image processing operations (ops) are a class that present the same interface, and it’s rather simple:

virtual void apply(float* rgbaBuffer, long numPixels)

Basically, given a packed float array with the specified number of pixels, process em.

Examples of ops include Lut1DOp, Lut3DOp, MtxOffsetOp, LogOp, etc.

Thus, the job of a transform becomes much simpler and they’re only responsible for converting themselves to a list of ops. A simple FileTransform that only has a single 1D lut internally may just generate a single Lut1DOp, but a FileTransform that references a more complex format (such as the houdini lut case referenced above) may generate a few ops:

void FileFormatHDL::BuildFileOps(OpRcPtrVec & ops,
                         const Config& /*config*/,
                         const ConstContextRcPtr & /*context*/,
                         CachedFileRcPtr untypedCachedFile,
                         const FileTransform& fileTransform,
                         TransformDirection dir) const {

// Code omitted which loads the lut file into the file cache...

CreateLut1DOp(ops, cachedFile->lut1D,
                   fileTransform.getInterpolation(), dir);
CreateLut3DOp(ops, cachedFile->lut3D,
                   fileTransform.getInterpolation(), dir);

See (src/core/*Ops.h) for the available ops.

Note that while compositors often have complex, branching trees of image processing operations, we just have a linear list of ops, lending itself very well to optimization.

Before the ops are run, they are optimized. (Collapsed with appropriate neighbors, etc).

An Example

Let us consider the internal steps when getProcessor() is called to convert from ColorSpace ‘adx10’ to ColorSpace ‘aces’:

  • The first step is to turn this ColorSpace conversion into an ordered list of transforms.

We do this by creating a single list of the conversions from ‘adx10’ to reference, and then adding the transforms required to go from reference to ‘aces’. * The Transform list is then converted into a list of ops. It is during this stage luts, are loaded, etc.

CPU CODE PATH

The main list of ops is then optimized, and stored internally in the processor.

FinalizeOpVec(m_cpuOps);

During Processor::apply(…), a subunit of pixels in the image are formatted into a sequential rgba block. (Block size is optimized for computational (SSE) simplicity and performance, and is typically similar in size to an image scanline)

float * rgbaBuffer = 0;
long numPixels = 0;
while(true) {
   scanlineHelper.prepRGBAScanline(&rgbaBuffer, &numPixels);
   ...

Then for each op, op->apply is called in-place.

for(OpRcPtrVec::size_type i=0, size = m_cpuOps.size(); i<size; ++i)
{
   m_cpuOps[i]->apply(rgbaBuffer, numPixels);
}

After all ops have been applied, the results are copied back to the source

scanlineHelper.finishRGBAScanline();

GPU CODE PATH

  1. The main list of ops is partitioned into 3 ordered lists:

  • As many ops as possible from the BEGINNING of the op-list that can be done analytically in shader text. (called gpu-preops)

  • As many ops as possible from the END of the op-list that can be done analytically in shader text. (called gpu-postops)

  • The left-over ops in the middle that cannot support shader text, and thus will be baked into a 3dlut. (called gpu-lattice)

#. Between the first an the second lists (gpu-preops, and gpu-latticeops), we analyze the op-stream metadata and determine the appropriate allocation to use. (to minimize clamping, quantization, etc). This is accounted for here by interserting a forward allocation to the end of the pre-ops, and the inverse allocation to the start of the lattice ops.

See https://github.com/AcademySoftwareFoundation/OpenColorIO/blob/main/src/core/NoOps.cpp#L183

#. The 3 lists of ops are then optimized individually, and stored on the processor. The Lut3d is computed by applying the gpu-lattice ops, on the CPU, to a lut3d image.

The shader text is computed by calculating the shader for the gpu-preops, adding a sampling function of the 3d lut, and then calculating the shader for the gpu post ops.

Glossary

  • Transform - a function that alters RGB(A) data (e.g transform an image from scene linear to sRGB)

  • Reference space - a space that connects colorspaces

  • Colorspace - a meaningful space that can be transferred to and from the reference space

  • Display - a virtual or physical display device (e.g an sRGB display device)

  • View - a meaningful view of the reference space on a Display (e.g a film emulation view on an sRGB display device)

  • Role - abstract colorspace naming (e.g specify the “lnh” colorspace as the scene_linear role, or the color-picker UI uses color_picking role)

  • Look - a color transform which applies a creative look (for example a per-shot neutral grade to remove color-casts from a sequence of film scans, or a DI look)