Chapter 12 - Automated Color and Viewing Management

Color management in RV can be broken into three separate issues:

  • Determination of the input color space

  • Deciding whether the input color space should be converted to the linear working space and with what transform

  • Displaying the working color space on a particular device possibly in a manner which simulates another device (e.g, film look on an LCD monitor).

Each of the above corresponds to a set of features in RV which can be automated:

  • Examining particular image attributes it’s often possible to determine color space. Some images may use a naming convention or may be located in a particular place on a file system which indicates its color space. It’s even possible that a separate file or program needs to be executed to get the actual color space.

  • Input spaces can be transformed to working spaces using the built in transforms like sRGB, gamma, or Cineon log space to linear space. If RV does not have a built-in transform for the color space, a file LUT (one per input source) may be used to interpolate independent channel functions using a channel LUT or a general function of R G and B channels using a 3D LUT. Values from a CDL can also be used to bring the input into the working color space.

  • Ideally RV will have a fixed set of display transform which map a linear space to a display. This makes it possible to load multiple sets of images with differing color spaces, transform them to a common linear working space, and display them using a global display transform. RV has built-in transform for sRGB and gamma and can also use a channel or 3D LUT if a custom function is needed.

In addition to the color issues there are a few others which might need to be detected and/or corrected:

  • Unrecorded or incorrect pixel aspect ratios (e.g., DPX files with inaccurate headers)

  • Special mattes which should be used with particular images

  • Incorrect frame numbers

  • Incorrect fps

  • Specific production information which is not located in the image (e.g., shot information, tracking information)

RV lets you customize all of the above for your facility and workflow by hooking into the user interface code. The most important method of doing so is using special events generated by RV internally and setting internal state at that time.

12.1 The source-group-complete Event

The source-group-complete event is generated whenever media is added to a session; this includes when a Source is created, or when the set of media held be a Source is modified. By binding a function to this event, it’s possible to configure any color space or other image dependant aspects of RV at the time the file is added. This can save a considerable amount of time and headache when a large number of people are using RV in differing circumstances.See the sections below for information about creating a package which binds source-group-complete to do color management.

12.2 The default source-group-complete behavior

RV binds its own color management function located in the source_setup.py file called sourceSetup(). It’s a good idea to override or augment this package for use in production environments. For example, you may want to have certain default color behavior for technical directors using movie files which differs from how a coordinator might view them (the coordinator may be looking at movies in sRGB space instead of with a film simulation for example).RV’s default color management tries to use good defaults for incoming file formats. Here’s the complete behavior shown as a set of heuristics applied in order:

  1. If the incoming image is a TIFF file and it has no color space attribute assume it’s linear

  2. If the image is JPEG or a quicktime movie file (.mov) and there is no color space attribute assume it’s in sRGB space

  3. If there is an embedded ICC profile and that profile is for sRGB space use RV’s internal sRGB space transform instead (because RV does not yet handle embedded ICC profiles)

  4. If the image is TIFF and it was created by ifftoany, assume the pixel aspect ratio is incorrect and fix it

  5. If the image is JPEG, has no pixel aspect ratio attribute and no density attribute and looks like it comes from Maya, fix the pixel aspect ratio

  6. Use the proper built-in conversion for the color space indicated in the color space attribute of the image

  7. Use the sRGB display transform if any color space was successfully determined for the input image(s)

From the user’s point of view, the following situations will occur:

  • A DPX or Cineon is loaded which is determined to be in Log space — turn on the built in log to linear converter

  • A JPEG or Quicktime movie file is determined to be in sRGB space or if no space is specified assumed to be in sRGB space — apply the built-in sRGB to linear converter

  • An EXR is loaded — assume it’s linear

  • A TIFF file with no color space indication is assumed to be linear, if it does have a color space use that.

  • A PNG file with no color space is assumed linear, otherwise use the color space attribute in the file

  • Any file with a pixel aspect ratio attribute will be assumed to be correct (unless it’s determined to have come from Maya)

  • The monitor’s ``gamma’’ will be accounted for automatically (because RV assumes the monitor is an sRGB device)

In addition, the default color management implements two varieties of user-level control, as examples of what you can do from the scripting level.First, environment variables with a standard format can be used to control the linearization process for a given file type. An environment variable of the form “RV_OVERRIDE_TRANSFER_” will set the linearization transform for the specified file type (and this will override the default rules described above). For example, if the environment variable “RV_OVERRIDE_TRANSFER_TIF” is set to “sRGB” then all files with extension “tif” or “TIF” will be linearized with the sRGB transform. If you want, you can also specify the bit depth. So you could set RV_OVERRIDE_TRANSFER_TIF_8 to sRGB and RV_OVERRIDE_TRANSFER_TIF_32 to Linear. The transform function name must be one of the following standard transforms. (The number following “Gamma” is arbitrary.)

Linear

sRGB

Cineon Log

Viper Log

Rec709

Gamma f

Table 12.1:Standard Linearization Transforms.

Second, any of the above-described environment variable names and standard transform names can appear on the command line following the “-flags” option.

12.3 Breakdown of sourceSetup() in the source_setup Package

The source_setup system package defines the default sourceSetup() function. This is where RV’s default color management comes from. The function starts by parsing the event contents (which contains the name of the file, the type of source node, and the source node name) as well as setting up the regular expressions used later in the function:

Note: RV Python to implement the source_setup package. The actual sourceSetup() function in source_setup.py may differ from what is described here.

        args         = event.contents().split(";;")
        group        = args[0]
        fileSource   = groupMemberOfType(group, "RVFileSource")
        imageSource  = groupMemberOfType(group, "RVImageSource")
        source       = fileSource if imageSource == None else imageSource
        linPipeNode  = groupMemberOfType(group, "RVLinearizePipelineGroup")
        linNode      = groupMemberOfType(linPipeNode, "RVLinearize")
        lensNode     = groupMemberOfType(linPipeNode, "RVLensWarp")
        fmtNode      = groupMemberOfType(group, "RVFormat")
        tformNode    = groupMemberOfType(group, "RVTransform2D")
        lookPipeNode = groupMemberOfType(group, "RVLookPipelineGroup")
        lookNode     = groupMemberOfType(lookPipeNode, "RVLookLUT")
        typeName     = commands.nodeType(source)
        fileNames    = commands.getStringProperty("%s.media.movie" % source, 0, 1000)
        fileName     = fileNames[0]
        ext          = fileName.split('.')[-1].upper()
        igPrim       = self.checkIgnorePrimaries(ext)
        mInfo        = commands.sourceMediaInfo(source, None) 

The event.contents() function returns a string which might look something like this:

 sourceGroup000000;;new 

The split() function is used to create a dynamic array of strings to extract the source group’s name. The nodes associated with the source group are then located and the media names are taken from the source node. The source node is either an RVImageSource which stores its image data directly in the session or an RVFileSource which references media on the filesystem. Both of these node types have a media component which contains the actual media names (usually a single file in the case of an RVFileSource node).

There are three pipeline group nodes in each source group node and one pipeline group in the display group. For the default source_setup the linearize pipeline group is need to get the default RVLinearize node it contains.The next section of the function iterates over the image attributes and caches the ones we’re interested in. The most important of these is the Colorspace attribute which is set by the file readers when the image color space is known.

        srcAttrs = commands.sourceAttributes(source, fileName)
        attrDict = dict(zip([i[0] for i in srcAttrs],[j[1] for j in srcAttrs]))
        attrMap = {
            "ColorSpace/ICC/Description" : "ICCProfileDesc",
            "ColorSpace" : "ColorSpace",
            "ColorSpace/Transfer" : "TransferFunction",
            "ColorSpace/Primaries" : "ColorSpacePrimaries",
            "DPX-0/Transfer" : "DPX0Transfer",
            "ColorSpace/Conversion" : "ConversionMatrix",
            "JPEG/PixelAspect" : "JPEGPixelAspect",
            "PixelAspectRatio" : "PixelAspectRatio",
            "JPEG/Density" : "JPEGDensity",
            "TIFF/ImageDescription" : "TIFFImageDescription",
            "DPX/Creator" : "DPXCreator",
            "EXIF/Orientation" : "EXIFOrientation",
            "EXIF/Gamma" : "EXIFGamma"}
        for key in attrMap.keys():
            try:
                exec('%s = "%s"' % (attrMap[key],attrDict[key]))
            except KeyError:
                pass 

The function sourceAttributes() returns the image attributes for a given file in a source. In this case we’re passing in the source and file which caused the event. The return value of the function is a dynamic array of tuples of type (string,string) where the first element is the name of the attribute and the second is a string representation of the value. Each iteration through the loop, the next tuple is used to assign the attribute value to the a variable with name of the attribute.The variables ICCProfileName, Colorspace, JPEGPixelAspect, etc, are all variable of type string which are defined earlier in the function.Before getting to the meat of the function, there are two helper functions declared: setPixelAspect() and setFileColorSpace().The next major section of the function matches the file name against the regular expressions that were declared at the beginning and against the values of some of the attributes that were cached.

        #
        #  Rules based on the extension
        #

        if (ext == 'DPX'):
            if (DPXCreator == "AppleComputers.libcineon" or
                DPXCreator == "AUTODESK"):
                #
                #  Final Cut's "Color" and Maya write out bogus DPX
                #  header info with the aspect ratio fields set
                #  improperly (to 0s usually). Properly undefined DPX
                #  headers do not have the value 0.
                #

                if (int(PixelAspectRatio) == 0):
                    self.setPixelAspect(lensNode, 1.0)
            elif (DPXCreator == "Nuke" and
                    (ColorSpace == ""   or ColorSpace == "Other (0)") and
                    (DPX0Transfer == "" or DPX0Transfer == "Other (0)")):
                #
                #  Nuke produces identical (uninformative) dpx headers for
                #  both Linear and Cineon files.  But we expect Cineon to be
                #  much more common, so go with that.
                #

                TransferFunction = "Cineon Log"
        elif (ext == 'TIF' and TransferFunction == ""):
            #
            #  Assume 8bit tif files are sRGB if there's no other indication;
            #  fall back to linear.
            #

            if (mInfo['bitsPerChannel'] == 8):
                TransferFunction = "sRGB"
            else:
                TransferFunction = "Linear"
         
        elif (ext in ['JPEG','JPG','MOV','AVI','MP4'] and TransferFunction == ""):
            #
            #  Assume jpeg/mov is in sRGB space if none is specified
            #

            TransferFunction = "sRGB"
        elif (ext in ['J2C','J2K','JPT','JP2'] and ColorSpacePrimaries == "UNSPECIFIED"):
            #
            #  If we're assuming XYZ primaries, but ignoring primaries just set
            #  transfer to sRGB.
            #

            if (igPrim):
                TransferFunction = "sRGB";

        if (igPrim):
            commands.setIntProperty(linNode + ".color.ignoreChromaticities", [1], True)

        if (ICCProfileDesc != ""):
            #
            #  Hack -- if you see sRGB in a color profile name just use the
            #  built-in sRGB conversion.
            #

            if ("sRGB" in ICCProfileDesc):
                TransferFunction = "sRGB"
            else:
                TransferFunction = ""

        if (TIFFImageDescription == "Image converted using ifftoany"):
            #
            #  Get around maya bugs
            #

            print("WARNING: Assuming %s was created by Maya with a bad pixel aspect ratio\n" % fileName)
            self.setPixelAspect(lensNode, 1.0)

        if (JPEGPixelAspect != "" and JPEGDensity != ""):
            info     = commands.sourceMediaInfo(source, fileName)
            attrPA   = float(JPEGPixelAspect)
            imagePA  = float(info['width']) / float(info['height'])
            testDiff = attrPA - 1.0 / imagePA

            if ((testDiff < 0.0001) and (testDiff > -0.0001)):
                #
                #  Maya JPEG -- fix pixel aspect
                #

                print("WARNING: Assuming %s was created by Maya with a bad pixel aspect ratio\n" % fileName)
                self.setPixelAspect(lensNode, 1.0)

        if (EXIFOrientation != ""):
            #
            #  Some of these tags are beyond the internal image
            #  orientation choices so we need to possibly rotate, etc
            #

            if not self.definedInSessionFile(tformNode):
                rprop = tformNode + ".transform.rotate"
                if (EXIFOrientation == "right - top"):
                    commands.setFloatProperty(rprop, [90.0], True)
                elif (EXIFOrientation == "right - bottom"):
                    commands.setFloatProperty(rprop, [-90.0], True)
                elif (EXIFOrientation == "left - top"):
                    commands.setFloatProperty(rprop, [90.0], True)
                elif (EXIFOrientation == "left - bottom"):
                    commands.setFloatProperty(rprop, [-90.0], True) 

At this point in the function the color space of the input image will be known or assumed to be linear. Finally, we try to set the color space (which will result in the image pixels being converted to the linear working space). If this succeeds, use sRGB display as the default.

        if (not noColorChanges):
        #
        #  Assume (in the absence of info to the contrary) any 8bit file will be in sRGB space.
        #
            if (TransferFunction == "" and mInfo['bitsPerChannel'] == 8):
                TransferFunction = "sRGB"

        #
        #  Allow user to override with environment variables
        #
        TransferFunction = self.checkEnvVar(ext, mInfo['bitsPerChannel'], TransferFunction)

        if (self.setFileColorSpace(linNode, TransferFunction, ColorSpace)):

            #
            #  The default display correction is sRGB if the
            #  pixels can be converted to (or are already in)
            #  linear space
            #
            #  For gamma instead do this:
            #
            #      setFloatProperty("#RVDisplayColor.color.gamma", float[] {2.2}, true);
            #
            #  For a linear -> screen LUT do this:
            #
            #      readLUT(lutfile, "#RVDisplayColor", true);
            #
            #  If this is not the first source, assume that user or source_seetup
            #  has already set the desired display transform

            if len(commands.sources()) == 1:
                self.setDisplayFromProfile() 

12.4 Setting up 3D and Channel LUTs

The default source-group-complete event function does not set up any non-built-in transforms. When you need to automatically apply a LUT, as a file, look, or a display LUT, you need to do the following:

 readLUT(file, nodeName, True) 

The nodeName will be ``#RVDisplayColor’’ (to refer to it by type) for the display LUT. For a file or look LUT, you use the associated node name for the color node — in the default sourceSetup() function this would be the linNode variable. The file parameter to readLUT() will be the name of the LUT file on disk and can be any of the LUT types that RV reads.

12.5 Setting CDL Values From File

As with using LUT files to fill in where built-in transforms do not cover your needs, you can read in CDL property values from a file. Use the following to read values from a CDL file on disk:

 readCDL(file, nodeName, True) 

When using readCDL the “nodeName” should be that of the targeted RVColor or RVLookLUT node to which you are applying the CDL values read from “file”. In the default RV graph you will find CDL properties to set in the RVColor and RVLookLUT nodes for each source, but there are none out-of-the-box in the display pipeline. However, you can add RVColor or RVLookLUT nodes to any pipeline you need CDL control that does not have them by default.You can also add RVCDL nodes where you want CDL control, but these nodes do not require the use of readCDL. With RVCDL nodes you only need to set the node’s node.file property and it will automatically load and parse the file from the path provided. Errors will be thrown if the file provided is invalid.

12.6 Building a Package For Color Management

The recommend way to handle all event bindings is via a python package. To customize color management you can either create a new package from scratch as described here, or copy, rename, and hack the existing source_setup package.The use of source-group-complete is no different from any other event. By creating a package you can override the existing behavior or modify it. It also makes it possible to have layers of color management packages which (assuming they don’t contradict each other) can collectively create a desired behavior.

from rv import rvtypes, commands, extra_commands
import os, re

class CustomColorManagementMode(rvtypes.MinorMode):

    def sourceSetup (self, event, noColorChanges=False):

        // do work on the new source here
        event.reject()

    def __init__(self):
        rvtypes.MinorMode.__init__(self)
        self.init("Source Setup",
                  None,
                  None,
                  [ ("source-group-complete", self.sourceSetup, "Color and Geometry Management") ],
                  "source_setup",
                  20)

def createMode():
    return CustomColorManagementMode() 

Note that we use the sortKey “source_setup” and the sortOrder “20”. This will ensure that our additional sourceSetup runs after the default color management.The included optional package “ocio_source_setup” is a good example of a package that does additional source setup.