Updated to ACES 1.0.1
[OpenColorIO-Configs.git] / aces_1.0.1 / python / aces_ocio / generate_lut.py
diff --git a/aces_1.0.1/python/aces_ocio/generate_lut.py b/aces_1.0.1/python/aces_ocio/generate_lut.py
new file mode 100755 (executable)
index 0000000..94bd735
--- /dev/null
@@ -0,0 +1,1048 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+Defines objects to generate various kind of 1D and 3D LUTs in various file
+formats.
+"""
+
+from __future__ import division
+
+import array
+import os
+
+import OpenImageIO as oiio
+
+from aces_ocio.process import Process
+
+__author__ = 'ACES Developers'
+__copyright__ = 'Copyright (C) 2014 - 2015 - ACES Developers'
+__license__ = ''
+__maintainer__ = 'ACES Developers'
+__email__ = 'aces@oscars.org'
+__status__ = 'Production'
+
+__all__ = ['generate_1d_LUT_image',
+           'write_SPI_1d',
+           'write_CSP_1d',
+           'write_CTL_1d',
+           'write_1d',
+           'generate_1d_LUT_from_image',
+           'generate_3d_LUT_image',
+           'generate_3d_LUT_from_image',
+           'apply_CTL_to_image',
+           'convert_bit_depth',
+           'generate_1d_LUT_from_CTL',
+           'correct_LUT_image',
+           'generate_3d_LUT_from_CTL',
+           'main']
+
+
+def generate_1d_LUT_image(ramp_1d_path,
+                          resolution=1024,
+                          min_value=0,
+                          max_value=1):
+    """
+    Generates a 1D LUT image, i.e. a simple ramp, going from the min_value to 
+    the max_value.
+
+    Parameters
+    ----------
+    ramp_1d_path : str or unicode
+        The path of the 1D ramp image to be written
+    resolution : int, optional
+        The resolution of the 1D ramp image to be written
+    min_value : float, optional
+        The lowest value in the 1D ramp
+    max_value : float, optional
+        The highest value in the 1D ramp
+
+    Returns
+    -------
+    None
+    """
+
+    ramp = oiio.ImageOutput.create(ramp_1d_path)
+
+    spec = oiio.ImageSpec()
+    spec.set_format(oiio.FLOAT)
+    # spec.format.basetype = oiio.FLOAT
+    spec.width = resolution
+    spec.height = 1
+    spec.nchannels = 3
+
+    ramp.open(ramp_1d_path, spec, oiio.Create)
+
+    data = array.array('f',
+                       '\0' * spec.width * spec.height * spec.nchannels * 4)
+    for i in range(resolution):
+        value = float(i) / (resolution - 1) * (
+            max_value - min_value) + min_value
+        data[i * spec.nchannels + 0] = value
+        data[i * spec.nchannels + 1] = value
+        data[i * spec.nchannels + 2] = value
+
+    ramp.write_image(spec.format, data)
+    ramp.close()
+
+
+def write_SPI_1d(filename,
+                 from_min,
+                 from_max,
+                 data,
+                 entries,
+                 channels,
+                 components=3):
+    """
+    Writes a 1D LUT in the Sony Pictures Imageworks .spi1d format.
+
+    Credit to *Alex Fry* for the original single channel version of the spi1d
+    writer.
+
+    Parameters
+    ----------
+    filename : str or unicode
+        The path of the 1D LUT to be written
+    from_min : float
+        The lowest value in the 1D ramp
+    from_max : float
+        The highest value in the 1D ramp
+    data : array of floats
+        The entries in the LUT
+    entries : int
+        The resolution of the LUT, i.e. number of entries in the data set
+    channels : int
+        The number of channels in the data
+    components : int, optional
+        The number of channels in the data to actually write
+
+    Returns
+    -------
+    None
+    """
+
+    # May want to use fewer components than there are channels in the data
+    # Most commonly used for single channel LUTs
+    components = min(3, components, channels)
+
+    with open(filename, 'w') as fp:
+        fp.write('Version 1\n')
+        fp.write('From %f %f\n' % (from_min, from_max))
+        fp.write('Length %d\n' % entries)
+        fp.write('Components %d\n' % components)
+        fp.write('{\n')
+        for i in range(0, entries):
+            entry = ''
+            for j in range(0, components):
+                entry = '%s %s' % (entry, data[i * channels + j])
+            fp.write('        %s\n' % entry)
+        fp.write('}\n')
+
+
+def write_CSP_1d(filename,
+                 from_min,
+                 from_max,
+                 data,
+                 entries,
+                 channels,
+                 components=3):
+    """
+    Writes a 1D LUT in the Rising Sun Research Cinespace .csp format.
+
+    Parameters
+    ----------
+    filename : str or unicode
+        The path of the 1D LUT to be written
+    from_min : float
+        The lowest value in the 1D ramp
+    from_max : float
+        The highest value in the 1D ramp
+    data : array of floats
+        The entries in the LUT
+    entries : int
+        The resolution of the LUT, i.e. number of entries in the data set
+    channels : int
+        The number of channels in the data
+    components : int, optional
+        The number of channels in the data to actually write
+
+    Returns
+    -------
+    None
+    """
+
+    # May want to use fewer components than there are channels in the data
+    # Most commonly used for single channel LUTs
+    components = min(3, components, channels)
+
+    with open(filename, 'w') as fp:
+        fp.write('CSPLUTV100\n')
+        fp.write('1D\n')
+        fp.write('\n')
+        fp.write('BEGIN METADATA\n')
+        fp.write('END METADATA\n')
+
+        fp.write('\n')
+
+        fp.write('2\n')
+        fp.write('%f %f\n' % (from_min, from_max))
+        fp.write('0.0 1.0\n')
+        fp.write('2\n')
+        fp.write('%f %f\n' % (from_min, from_max))
+        fp.write('0.0 1.0\n')
+        fp.write('2\n')
+        fp.write('%f %f\n' % (from_min, from_max))
+        fp.write('0.0 1.0\n')
+
+        fp.write('\n')
+
+        fp.write('%d\n' % entries)
+        if components == 1:
+            for i in range(0, entries):
+                entry = ''
+                for j in range(3):
+                    entry = '%s %s' % (entry, data[i * channels])
+                fp.write('%s\n' % entry)
+        else:
+            for i in range(entries):
+                entry = ''
+                for j in range(components):
+                    entry = '%s %s' % (entry, data[i * channels + j])
+                fp.write('%s\n' % entry)
+        fp.write('\n')
+
+
+def write_CTL_1d(filename,
+                 from_min,
+                 from_max,
+                 data,
+                 entries,
+                 channels,
+                 components=3):
+    """
+    Writes a 1D LUT in the Academy Color Transformation Language .ctl format.
+
+    Parameters
+    ----------
+    filename : str or unicode
+        The path of the 1D LUT to be written
+    from_min : float
+        The lowest value in the 1D ramp
+    from_max : float
+        The highest value in the 1D ramp
+    data : array of floats
+        The entries in the LUT
+    entries : int
+        The resolution of the LUT, i.e. number of entries in the data set
+    channels : int
+        The number of channels in the data
+    components : int, optional
+        The number of channels in the data to actually write
+
+    Returns
+    -------
+    None
+    """
+
+    # May want to use fewer components than there are channels in the data
+    # Most commonly used for single channel LUTs
+    components = min(3, components, channels)
+
+    with open(filename, 'w') as fp:
+        fp.write('// %d x %d LUT generated by "generate_lut"\n' % (
+            entries, components))
+        fp.write('\n')
+        fp.write('const float min1d = %3.9f;\n' % from_min)
+        fp.write('const float max1d = %3.9f;\n' % from_max)
+        fp.write('\n')
+
+        # Write LUT
+        if components == 1:
+            fp.write('const float lut[] = {\n')
+            for i in range(0, entries):
+                fp.write('%s' % data[i * channels])
+                if i != (entries - 1):
+                    fp.write(',')
+                fp.write('\n')
+            fp.write('};\n')
+            fp.write('\n')
+        else:
+            for j in range(components):
+                fp.write('const float lut%d[] = {\n' % j)
+                for i in range(0, entries):
+                    fp.write('%s' % data[i * channels])
+                    if i != (entries - 1):
+                        fp.write(',')
+                    fp.write('\n')
+                fp.write('};\n')
+                fp.write('\n')
+
+        fp.write('void main\n')
+        fp.write('(\n')
+        fp.write('  input varying float rIn,\n')
+        fp.write('  input varying float gIn,\n')
+        fp.write('  input varying float bIn,\n')
+        fp.write('  input varying float aIn,\n')
+        fp.write('  output varying float rOut,\n')
+        fp.write('  output varying float gOut,\n')
+        fp.write('  output varying float bOut,\n')
+        fp.write('  output varying float aOut\n')
+        fp.write(')\n')
+        fp.write('{\n')
+        fp.write('  float r = rIn;\n')
+        fp.write('  float g = gIn;\n')
+        fp.write('  float b = bIn;\n')
+        fp.write('\n')
+        fp.write('  // Apply LUT\n')
+        if components == 1:
+            fp.write('  r = lookup1D(lut, min1d, max1d, r);\n')
+            fp.write('  g = lookup1D(lut, min1d, max1d, g);\n')
+            fp.write('  b = lookup1D(lut, min1d, max1d, b);\n')
+        elif components == 3:
+            fp.write('  r = lookup1D(lut0, min1d, max1d, r);\n')
+            fp.write('  g = lookup1D(lut1, min1d, max1d, g);\n')
+            fp.write('  b = lookup1D(lut2, min1d, max1d, b);\n')
+        fp.write('\n')
+        fp.write('  rOut = r;\n')
+        fp.write('  gOut = g;\n')
+        fp.write('  bOut = b;\n')
+        fp.write('  aOut = aIn;\n')
+        fp.write('}\n')
+
+
+def write_1d(filename,
+             from_min,
+             from_max,
+             data,
+             data_entries,
+             data_channels,
+             lut_components=3,
+             format='spi1d'):
+    """
+    Writes a 1D LUT in the specified format.
+
+    Parameters
+    ----------
+    filename : str or unicode
+        The path of the 1D LUT to be written
+    from_min : float
+        The lowest value in the 1D ramp
+    from_max : float
+        The highest value in the 1D ramp
+    data : array of floats
+        The entries in the LUT
+    data_entries : int
+        The resolution of the LUT, i.e. number of entries in the data set
+    data_channels : int
+        The number of channels in the data
+    lut_components : int, optional
+        The number of channels in the data to actually use when writing
+    format : str or unicode, optional
+        The format of the the 1D LUT that will be written
+
+    Returns
+    -------
+    None
+    """
+
+    ocio_formats_to_extensions = {'cinespace': 'csp',
+                                  'flame': '3dl',
+                                  'icc': 'icc',
+                                  'houdini': 'lut',
+                                  'lustre': '3dl',
+                                  'ctl': 'ctl'}
+
+    if format in ocio_formats_to_extensions:
+        if ocio_formats_to_extensions[format] == 'csp':
+            write_CSP_1d(filename,
+                         from_min,
+                         from_max,
+                         data,
+                         data_entries,
+                         data_channels,
+                         lut_components)
+        elif ocio_formats_to_extensions[format] == 'ctl':
+            write_CTL_1d(filename,
+                         from_min,
+                         from_max,
+                         data,
+                         data_entries,
+                         data_channels,
+                         lut_components)
+    else:
+        write_SPI_1d(filename,
+                     from_min,
+                     from_max,
+                     data,
+                     data_entries,
+                     data_channels,
+                     lut_components)
+
+
+def generate_1d_LUT_from_image(ramp_1d_path,
+                               output_path=None,
+                               min_value=0,
+                               max_value=1,
+                               channels=3,
+                               format='spi1d'):
+    """
+    Reads a 1D LUT image and writes a 1D LUT in the specified format.
+
+    Parameters
+    ----------
+    ramp_1d_path : str or unicode
+        The path of the 1D ramp image to be read
+    output_path : str or unicode, optional
+        The path of the 1D LUT to be written
+    min_value : float, optional
+        The lowest value in the 1D ramp
+    max_value : float, optional
+        The highest value in the 1D ramp
+    channels : int, optional
+        The number of channels in the data
+    format : str or unicode, optional
+        The format of the the 1D LUT that will be written
+
+    Returns
+    -------
+    None
+    """
+
+    if output_path is None:
+        output_path = '%s.%s' % (ramp_1d_path, 'spi1d')
+
+    ramp = oiio.ImageInput.open(ramp_1d_path)
+
+    ramp_spec = ramp.spec()
+    ramp_width = ramp_spec.width
+    ramp_channels = ramp_spec.nchannels
+
+    # Forcibly read data as float, the Python API doesn't handle half-float
+    # well yet.
+    type = oiio.FLOAT
+    ramp_data = ramp.read_image(type)
+
+    write_1d(output_path, min_value, max_value,
+             ramp_data, ramp_width, ramp_channels, channels, format)
+
+
+def generate_3d_LUT_image(ramp_3d_path, resolution=32):
+    """
+    Generates a 3D LUT image covering the specified resolution
+    Relies on OCIO's ociolutimage command
+
+    Parameters
+    ----------
+    ramp_3d_path : str or unicode
+        The path of the 3D ramp image to be written
+    resolution : int, optional
+        The resolution of the 3D ramp image to be written
+
+    Returns
+    -------
+    None
+    """
+
+    args = ['--generate',
+            '--cubesize',
+            str(resolution),
+            '--maxwidth',
+            str(resolution * resolution),
+            '--output',
+            ramp_3d_path]
+    lut_extract = Process(description='generate a 3d LUT image',
+                          cmd='ociolutimage',
+                          args=args)
+    lut_extract.execute()
+
+
+def generate_3d_LUT_from_image(ramp_3d_path,
+                               output_path=None,
+                               resolution=32,
+                               format='spi3d'):
+    """
+    Reads a 3D LUT image and writes a 3D LUT in the specified format.
+    Relies on OCIO's ociolutimage command
+
+    Parameters
+    ----------
+    ramp_3d_path : str or unicode
+        The path of the 3D ramp image to be read
+    output_path : str or unicode, optional
+        The path of the 1D LUT to be written
+    resolution : int, optional
+        The resolution of the 3D LUT represented in the image
+    format : str or unicode, optional
+        The format of the the 3D LUT that will be written
+
+    Returns
+    -------
+    None
+    """
+
+    if output_path is None:
+        output_path = '%s.%s' % (ramp_3d_path, 'spi3d')
+
+    ocio_formats_to_extensions = {'cinespace': 'csp',
+                                  'flame': '3dl',
+                                  'icc': 'icc',
+                                  'houdini': 'lut',
+                                  'lustre': '3dl'}
+
+    if format == 'spi3d' or not (format in ocio_formats_to_extensions):
+        # Extract a spi3d LUT
+        args = ['--extract',
+                '--cubesize',
+                str(resolution),
+                '--maxwidth',
+                str(resolution * resolution),
+                '--input',
+                ramp_3d_path,
+                '--output',
+                output_path]
+        lut_extract = Process(description='extract a 3d LUT',
+                              cmd='ociolutimage',
+                              args=args)
+        lut_extract.execute()
+
+    else:
+        output_path_spi3d = '%s.%s' % (output_path, 'spi3d')
+
+        # Extract a spi3d LUT
+        args = ['--extract',
+                '--cubesize',
+                str(resolution),
+                '--maxwidth',
+                str(resolution * resolution),
+                '--input',
+                ramp_3d_path,
+                '--output',
+                output_path_spi3d]
+        lut_extract = Process(description='extract a 3d LUT',
+                              cmd='ociolutimage',
+                              args=args)
+        lut_extract.execute()
+
+        # Convert to a different format
+        args = ['--lut',
+                output_path_spi3d,
+                '--format',
+                format,
+                output_path]
+        lut_convert = Process(description='convert a 3d LUT',
+                              cmd='ociobakelut',
+                              args=args)
+        lut_convert.execute()
+
+
+def apply_CTL_to_image(input_image,
+                       output_image,
+                       ctl_paths=None,
+                       input_scale=1,
+                       output_scale=1,
+                       global_params=None,
+                       aces_ctl_directory=None):
+    """
+    Applies a set of Academy Color Transformation Language .ctl files to 
+    an input image and writes a new image.
+    Relies on the ACES ctlrender command
+
+    Parameters
+    ----------
+    input_image : str or unicode
+        The path to the image to transform using the CTL files
+    output_image : str or unicode
+        The path to write the result of the transforms
+    ctl_paths : array of str or unicode, optional
+        The path to write the result of the transforms
+    input_scale : float, optional
+        The argument to the ctlrender -input_scale parameter
+        For images with integer bit depths, this divides image code values 
+        before they are sent to the ctl commands
+        For images with float or half bit depths, this multiplies image code 
+        values before they are sent to the ctl commands
+    output_scale : float, optional
+        The argument to the ctlrender -output_scale parameter
+        For images with integer bit depths, this multiplies image code values 
+        before they are written to a file.
+        For images with float or half bit depths, this divides image code values 
+        before they are sent to the ctl commands
+    global_params : dict of key value pairs, optional
+        The set of parameter names and values to pass to the ctlrender 
+        -global_param1 parameter
+    aces_ctl_directory : str or unicode, optional
+        The path to the aces 'transforms/ctl/utilities'
+
+    Returns
+    -------
+    None
+    """
+
+    if ctl_paths is None:
+        ctl_paths = []
+    if global_params is None:
+        global_params = {}
+
+    if len(ctl_paths) > 0:
+        ctlenv = os.environ
+
+        if "/usr/local/bin" not in ctlenv['PATH'].split(':'):
+            ctlenv['PATH'] = "%s:/usr/local/bin" % ctlenv['PATH']
+
+        if aces_ctl_directory is not None:
+            if os.path.split(aces_ctl_directory)[1] != 'utilities':
+                ctl_module_path = os.path.join(aces_ctl_directory, 'utilities')
+            else:
+                ctl_module_path = aces_ctl_directory
+            ctlenv['CTL_MODULE_PATH'] = ctl_module_path
+
+        args = []
+        for ctl in ctl_paths:
+            args += ['-ctl', ctl]
+        args += ['-force']
+        args += ['-input_scale', str(input_scale)]
+        args += ['-output_scale', str(output_scale)]
+        args += ['-global_param1', 'aIn', '1.0']
+        for key, value in global_params.iteritems():
+            args += ['-global_param1', key, str(value)]
+        args += [input_image]
+        args += [output_image]
+
+        ctlp = Process(description='a ctlrender process',
+                       cmd='ctlrender',
+                       args=args, env=ctlenv)
+
+        ctlp.execute()
+
+
+def convert_bit_depth(input_image, output_image, depth):
+    """
+    Convert the input image to the specified bit depth and write a new image
+    Relies on the OIIO oiiotool command
+
+    Parameters
+    ----------
+    input_image : str or unicode
+        The path to the image to transform using the CTL files
+    output_image : str or unicode
+        The path to write the result of the transforms
+    depth : str or unicode
+        The bit depth of the output image
+        Data types include: uint8, sint8, uint10, uint12, uint16, sint16, half, float, double
+
+    Returns
+    -------
+    None
+    """
+
+    args = [input_image,
+            '-d',
+            depth,
+            '-o',
+            output_image]
+    convert = Process(description='convert image bit depth',
+                      cmd='oiiotool',
+                      args=args)
+    convert.execute()
+
+
+def generate_1d_LUT_from_CTL(lut_path,
+                             ctl_paths,
+                             lut_resolution=1024,
+                             identity_lut_bit_depth='half',
+                             input_scale=1,
+                             output_scale=1,
+                             global_params=None,
+                             cleanup=True,
+                             aces_ctl_directory=None,
+                             min_value=0,
+                             max_value=1,
+                             channels=3,
+                             format='spi1d'):
+    """
+    Creates a 1D LUT from the specified CTL files by creating a 1D LUT image,
+    applying the CTL files and then extracting and writing a LUT based on the
+    resulting image
+
+    Parameters
+    ----------
+    lut_path : str or unicode
+        The path to write the 1D LUT
+    ctl_paths : array of str or unicode
+        The CTL files to apply
+    lut_resolution : int, optional
+        The resolution of the 1D LUT to generate
+    identity_lut_bit_depth : string, optional
+        The bit depth to use for the intermediate 1D LUT image
+    input_scale : float, optional
+        The argument to the ctlrender -input_scale parameter
+        For images with integer bit depths, this divides image code values 
+        before they are sent to the ctl commands
+        For images with float or half bit depths, this multiplies image code 
+        values before they are sent to the ctl commands
+    output_scale : float, optional
+        The argument to the ctlrender -output_scale parameter
+        For images with integer bit depths, this multiplies image code values 
+        before they are written to a file.
+        For images with float or half bit depths, this divides image code values 
+        before they are sent to the ctl commands
+    global_params : dict of key, value pairs, optional
+        The set of parameter names and values to pass to the ctlrender 
+        -global_param1 parameter
+    cleanup : bool, optional
+        Whether or not to clean up the intermediate images 
+    aces_ctl_directory : str or unicode, optional
+        The path to the aces 'transforms/ctl/utilities'
+    min_value : float, optional
+        The minimum value to consider as input to the LUT
+    max_value : float, optional
+        The maximum value to consider as input to the LUT
+    channels : int, optional
+        The number of channels to use for the LUT. 1 or 3 are valid.
+    format : str or unicode, optional
+        The format to use when writing the LUT
+
+    Returns
+    -------
+    None
+    """
+
+    if global_params is None:
+        global_params = {}
+
+    lut_path_base = os.path.splitext(lut_path)[0]
+
+    identity_lut_image_float = '%s.%s.%s' % (lut_path_base, 'float', 'tiff')
+    generate_1d_LUT_image(identity_lut_image_float,
+                          lut_resolution,
+                          min_value,
+                          max_value)
+
+    if identity_lut_bit_depth not in ['half', 'float']:
+        identity_lut_image = '%s.%s.%s' % (lut_path_base, 'uint16', 'tiff')
+        convert_bit_depth(identity_lut_image_float,
+                          identity_lut_image,
+                          identity_lut_bit_depth)
+    else:
+        identity_lut_image = identity_lut_image_float
+
+    transformed_lut_image = '%s.%s.%s' % (lut_path_base, 'transformed', 'exr')
+    apply_CTL_to_image(identity_lut_image,
+                       transformed_lut_image,
+                       ctl_paths,
+                       input_scale,
+                       output_scale,
+                       global_params,
+                       aces_ctl_directory)
+
+    generate_1d_LUT_from_image(transformed_lut_image,
+                               lut_path,
+                               min_value,
+                               max_value,
+                               channels,
+                               format)
+
+    if cleanup:
+        os.remove(identity_lut_image)
+        if identity_lut_image != identity_lut_image_float:
+            os.remove(identity_lut_image_float)
+        os.remove(transformed_lut_image)
+
+
+def correct_LUT_image(transformed_lut_image,
+                      corrected_lut_image,
+                      lut_resolution):
+    """
+    For some combinations of resolution and bit depth, ctlrender would generate
+    images with the right number of pixels but with the values for width and 
+    height transposed. This function generating a new, corrected image based on
+    the original. The function acts as a pass through if the problem is not
+    detected.
+
+    Parameters
+    ----------
+    transformed_lut_image : str or unicode
+        The path to an image generated by cltrender
+    corrected_lut_image : str or unicode
+        The path to an 'corrected' image to be generated
+    lut_resolution : int
+        The resolution of the 3D LUT that should be contained in 
+        transformed_lut_image
+
+    Returns
+    -------
+    str or unicode
+        The path to the corrected image, or the original, if no correction was
+        needed.
+    """
+
+    transformed = oiio.ImageInput.open(transformed_lut_image)
+
+    transformed_spec = transformed.spec()
+    width = transformed_spec.width
+    height = transformed_spec.height
+    channels = transformed_spec.nchannels
+
+    if width != lut_resolution * lut_resolution or height != lut_resolution:
+        print(('Correcting image as resolution is off. '
+               'Found %d x %d. Expected %d x %d') % (
+                  width,
+                  height,
+                  lut_resolution * lut_resolution,
+                  lut_resolution))
+        print('Generating %s' % corrected_lut_image)
+
+        # Forcibly read data as float, the Python API doesn't handle half-float
+        # well yet.
+        type = oiio.FLOAT
+        source_data = transformed.read_image(type)
+
+        correct = oiio.ImageOutput.create(corrected_lut_image)
+
+        correct_spec = oiio.ImageSpec()
+        correct_spec.set_format(oiio.FLOAT)
+        correct_spec.width = height
+        correct_spec.height = width
+        correct_spec.nchannels = channels
+
+        correct.open(corrected_lut_image, correct_spec, oiio.Create)
+
+        dest_data = array.array('f',
+                                ('\0' * correct_spec.width *
+                                 correct_spec.height *
+                                 correct_spec.nchannels * 4))
+        for j in range(0, correct_spec.height):
+            for i in range(0, correct_spec.width):
+                for c in range(0, correct_spec.nchannels):
+                    dest_data[(correct_spec.nchannels *
+                               correct_spec.width * j +
+                               correct_spec.nchannels * i + c)] = (
+                        source_data[correct_spec.nchannels *
+                                    correct_spec.width * j +
+                                    correct_spec.nchannels * i + c])
+
+        correct.write_image(correct_spec.format, dest_data)
+        correct.close()
+    else:
+        # shutil.copy(transformedLUTImage, correctedLUTImage)
+        corrected_lut_image = transformed_lut_image
+
+    transformed.close()
+
+    return corrected_lut_image
+
+
+def generate_3d_LUT_from_CTL(lut_path,
+                             ctl_paths,
+                             lut_resolution=64,
+                             identity_lut_bit_depth='half',
+                             input_scale=1,
+                             output_scale=1,
+                             global_params=None,
+                             cleanup=True,
+                             aces_ctl_directory=None,
+                             format='spi3d'):
+    """
+    Creates a 3D LUT from the specified CTL files by creating a 3D LUT image,
+    applying the CTL files and then extracting and writing a LUT based on the
+    resulting image
+
+    Parameters
+    ----------
+    lut_path : str or unicode
+        The path to write the 1D LUT
+    ctl_paths : array of str or unicode
+        The CTL files to apply
+    lut_resolution : int, optional
+        The resolution of the 1D LUT to generate
+    identity_lut_bit_depth : string, optional
+        The bit depth to use for the intermediate 1D LUT image
+    input_scale : float, optional
+        The argument to the ctlrender -input_scale parameter
+        For images with integer bit depths, this divides image code values 
+        before they are sent to the ctl commands
+        For images with float or half bit depths, this multiplies image code 
+        values before they are sent to the ctl commands
+    output_scale : float, optional
+        The argument to the ctlrender -output_scale parameter
+        For images with integer bit depths, this multiplies image code values 
+        before they are written to a file.
+        For images with float or half bit depths, this divides image code values 
+        before they are sent to the ctl commands
+    global_params : dict of key, value pairs, optional
+        The set of parameter names and values to pass to the ctlrender 
+        -global_param1 parameter
+    cleanup : bool, optional
+        Whether or not to clean up the intermediate images 
+    aces_ctl_directory : str or unicode, optional
+        The path to the aces 'transforms/ctl/utilities'
+    format : str or unicode, optional
+        The format to use when writing the LUT
+
+    Returns
+    -------
+    None
+    """
+
+    if global_params is None:
+        global_params = {}
+
+    lut_path_base = os.path.splitext(lut_path)[0]
+
+    identity_lut_image_float = '%s.%s.%s' % (lut_path_base, 'float', 'tiff')
+    generate_3d_LUT_image(identity_lut_image_float, lut_resolution)
+
+    if identity_lut_bit_depth not in ['half', 'float']:
+        identity_lut_image = '%s.%s.%s' % (lut_path_base,
+                                           identity_lut_bit_depth,
+                                           'tiff')
+        convert_bit_depth(identity_lut_image_float,
+                          identity_lut_image,
+                          identity_lut_bit_depth)
+    else:
+        identity_lut_image = identity_lut_image_float
+
+    transformed_lut_image = '%s.%s.%s' % (lut_path_base, 'transformed', 'exr')
+    apply_CTL_to_image(identity_lut_image,
+                       transformed_lut_image,
+                       ctl_paths,
+                       input_scale,
+                       output_scale,
+                       global_params,
+                       aces_ctl_directory)
+
+    corrected_lut_image = '%s.%s.%s' % (lut_path_base, 'correct', 'exr')
+    corrected_lut_image = correct_LUT_image(transformed_lut_image,
+                                            corrected_lut_image,
+                                            lut_resolution)
+
+    generate_3d_LUT_from_image(corrected_lut_image,
+                               lut_path,
+                               lut_resolution,
+                               format)
+
+    if cleanup:
+        os.remove(identity_lut_image)
+        if identity_lut_image != identity_lut_image_float:
+            os.remove(identity_lut_image_float)
+        os.remove(transformed_lut_image)
+        if corrected_lut_image != transformed_lut_image:
+            os.remove(corrected_lut_image)
+        if format != 'spi3d':
+            lut_path_spi3d = '%s.%s' % (lut_path, 'spi3d')
+            os.remove(lut_path_spi3d)
+
+
+def main():
+    """
+    A simple main that allows the user to exercise the various functions
+    defined in this file
+
+    Parameters
+    ----------
+    None
+
+    Returns
+    -------
+    None
+    """
+
+    import optparse
+
+    p = optparse.OptionParser(
+        description='A utility to generate LUTs from CTL',
+        prog='generateLUT',
+        version='0.01',
+        usage='%prog [options]')
+
+    p.add_option('--lut', '-l', type='string', default='')
+    p.add_option('--format', '-f', type='string', default='')
+    p.add_option('--ctl', '-c', type='string', action='append')
+    p.add_option('--lutResolution1d', '', type='int', default=1024)
+    p.add_option('--lutResolution3d', '', type='int', default=33)
+    p.add_option('--ctlReleasePath', '-r', type='string', default='')
+    p.add_option('--bitDepth', '-b', type='string', default='float')
+    p.add_option('--keepTempImages', '', action='store_true')
+    p.add_option('--minValue', '', type='float', default=0)
+    p.add_option('--maxValue', '', type='float', default=1)
+    p.add_option('--inputScale', '', type='float', default=1)
+    p.add_option('--outputScale', '', type='float', default=1)
+    p.add_option('--ctlRenderParam', '-p', type='string', nargs=2,
+                 action='append')
+
+    p.add_option('--generate1d', '', action='store_true')
+    p.add_option('--generate3d', '', action='store_true')
+
+    options, arguments = p.parse_args()
+
+    lut = options.lut
+    format = options.format
+    ctls = options.ctl
+    lut_resolution_1d = options.lutResolution1d
+    lut_resolution_3d = options.lutResolution3d
+    min_value = options.minValue
+    max_value = options.maxValue
+    input_scale = options.inputScale
+    output_scale = options.outputScale
+    ctl_release_path = options.ctlReleasePath
+    generate_1d = options.generate1d is True
+    generate_3d = options.generate3d is True
+    bit_depth = options.bitDepth
+    cleanup = not options.keepTempImages
+
+    params = {}
+    if options.ctlRenderParam is not None:
+        for param in options.ctlRenderParam:
+            params[param[0]] = float(param[1])
+
+    if generate_1d:
+        print('1D LUT generation options')
+    else:
+        print('3D LUT generation options')
+
+    print('LUT                 : %s' % lut)
+    print('Format              : %s' % format)
+    print('CTLs                : %s' % ctls)
+    print('LUT Res 1d          : %s' % lut_resolution_1d)
+    print('LUT Res 3d          : %s' % lut_resolution_3d)
+    print('Min Value           : %s' % min_value)
+    print('Max Value           : %s' % max_value)
+    print('Input Scale         : %s' % input_scale)
+    print('Output Scale        : %s' % output_scale)
+    print('CTL Render Params   : %s' % params)
+    print('CTL Release Path    : %s' % ctl_release_path)
+    print('Input Bit Depth     : %s' % bit_depth)
+    print('Cleanup Temp Images : %s' % cleanup)
+
+    if generate_1d:
+        generate_1d_LUT_from_CTL(lut,
+                                 ctls,
+                                 lut_resolution_1d,
+                                 bit_depth,
+                                 input_scale,
+                                 output_scale,
+                                 params,
+                                 cleanup,
+                                 ctl_release_path,
+                                 min_value,
+                                 max_value,
+                                 format=format)
+
+    elif generate_3d:
+        generate_3d_LUT_from_CTL(lut,
+                                 ctls,
+                                 lut_resolution_3d,
+                                 bit_depth,
+                                 input_scale,
+                                 output_scale,
+                                 params,
+                                 cleanup,
+                                 ctl_release_path,
+                                 format=format)
+    else:
+        print(('\n\nNo LUT generated! '
+               'You must choose either 1D or 3D LUT generation\n\n'))
+
+
+if __name__ == '__main__':
+    main()