2 # -*- coding: utf-8 -*-
5 Defines objects to generate various kind of 1D and 3D LUTs in various file
9 from __future__ import division
14 import OpenImageIO as oiio
16 from aces_ocio.process import Process
18 __author__ = 'ACES Developers'
19 __copyright__ = 'Copyright (C) 2014 - 2015 - ACES Developers'
21 __maintainer__ = 'ACES Developers'
22 __email__ = 'aces@oscars.org'
23 __status__ = 'Production'
25 __all__ = ['generate_1d_LUT_image',
30 'generate_1d_LUT_from_image',
31 'generate_3d_LUT_image',
32 'generate_3d_LUT_from_image',
35 'generate_1d_LUT_from_CTL',
37 'generate_3d_LUT_from_CTL',
41 def generate_1d_LUT_image(ramp_1d_path,
46 Generates a 1D LUT image, i.e. a simple ramp, going from the min_value to
51 ramp_1d_path : str or unicode
52 The path of the 1D ramp image to be written
53 resolution : int, optional
54 The resolution of the 1D ramp image to be written
55 min_value : float, optional
56 The lowest value in the 1D ramp
57 max_value : float, optional
58 The highest value in the 1D ramp
65 ramp = oiio.ImageOutput.create(ramp_1d_path)
67 spec = oiio.ImageSpec()
68 spec.set_format(oiio.FLOAT)
69 # spec.format.basetype = oiio.FLOAT
70 spec.width = resolution
74 ramp.open(ramp_1d_path, spec, oiio.Create)
76 data = array.array('f',
77 '\0' * spec.width * spec.height * spec.nchannels * 4)
78 for i in range(resolution):
79 value = float(i) / (resolution - 1) * (
80 max_value - min_value) + min_value
81 data[i * spec.nchannels + 0] = value
82 data[i * spec.nchannels + 1] = value
83 data[i * spec.nchannels + 2] = value
85 ramp.write_image(spec.format, data)
89 def write_SPI_1d(filename,
97 Writes a 1D LUT in the Sony Pictures Imageworks .spi1d format.
99 Credit to *Alex Fry* for the original single channel version of the spi1d
104 filename : str or unicode
105 The path of the 1D LUT to be written
107 The lowest value in the 1D ramp
109 The highest value in the 1D ramp
110 data : array of floats
111 The entries in the LUT
113 The resolution of the LUT, i.e. number of entries in the data set
115 The number of channels in the data
116 components : int, optional
117 The number of channels in the data to actually write
124 # May want to use fewer components than there are channels in the data
125 # Most commonly used for single channel LUTs
126 components = min(3, components, channels)
128 with open(filename, 'w') as fp:
129 fp.write('Version 1\n')
130 fp.write('From %f %f\n' % (from_min, from_max))
131 fp.write('Length %d\n' % entries)
132 fp.write('Components %d\n' % components)
134 for i in range(0, entries):
136 for j in range(0, components):
137 entry = '%s %s' % (entry, data[i * channels + j])
138 fp.write(' %s\n' % entry)
142 def write_CSP_1d(filename,
150 Writes a 1D LUT in the Rising Sun Research Cinespace .csp format.
154 filename : str or unicode
155 The path of the 1D LUT to be written
157 The lowest value in the 1D ramp
159 The highest value in the 1D ramp
160 data : array of floats
161 The entries in the LUT
163 The resolution of the LUT, i.e. number of entries in the data set
165 The number of channels in the data
166 components : int, optional
167 The number of channels in the data to actually write
174 # May want to use fewer components than there are channels in the data
175 # Most commonly used for single channel LUTs
176 components = min(3, components, channels)
178 with open(filename, 'w') as fp:
179 fp.write('CSPLUTV100\n')
182 fp.write('BEGIN METADATA\n')
183 fp.write('END METADATA\n')
188 fp.write('%f %f\n' % (from_min, from_max))
189 fp.write('0.0 1.0\n')
191 fp.write('%f %f\n' % (from_min, from_max))
192 fp.write('0.0 1.0\n')
194 fp.write('%f %f\n' % (from_min, from_max))
195 fp.write('0.0 1.0\n')
199 fp.write('%d\n' % entries)
201 for i in range(0, entries):
204 entry = '%s %s' % (entry, data[i * channels])
205 fp.write('%s\n' % entry)
207 for i in range(entries):
209 for j in range(components):
210 entry = '%s %s' % (entry, data[i * channels + j])
211 fp.write('%s\n' % entry)
215 def write_CTL_1d(filename,
223 Writes a 1D LUT in the Academy Color Transformation Language .ctl format.
227 filename : str or unicode
228 The path of the 1D LUT to be written
230 The lowest value in the 1D ramp
232 The highest value in the 1D ramp
233 data : array of floats
234 The entries in the LUT
236 The resolution of the LUT, i.e. number of entries in the data set
238 The number of channels in the data
239 components : int, optional
240 The number of channels in the data to actually write
247 # May want to use fewer components than there are channels in the data
248 # Most commonly used for single channel LUTs
249 components = min(3, components, channels)
251 with open(filename, 'w') as fp:
252 fp.write('// %d x %d LUT generated by "generate_lut"\n' % (
253 entries, components))
255 fp.write('const float min1d = %3.9f;\n' % from_min)
256 fp.write('const float max1d = %3.9f;\n' % from_max)
261 fp.write('const float lut[] = {\n')
262 for i in range(0, entries):
263 fp.write('%s' % data[i * channels])
264 if i != (entries - 1):
270 for j in range(components):
271 fp.write('const float lut%d[] = {\n' % j)
272 for i in range(0, entries):
273 fp.write('%s' % data[i * channels])
274 if i != (entries - 1):
280 fp.write('void main\n')
282 fp.write(' input varying float rIn,\n')
283 fp.write(' input varying float gIn,\n')
284 fp.write(' input varying float bIn,\n')
285 fp.write(' input varying float aIn,\n')
286 fp.write(' output varying float rOut,\n')
287 fp.write(' output varying float gOut,\n')
288 fp.write(' output varying float bOut,\n')
289 fp.write(' output varying float aOut\n')
292 fp.write(' float r = rIn;\n')
293 fp.write(' float g = gIn;\n')
294 fp.write(' float b = bIn;\n')
296 fp.write(' // Apply LUT\n')
298 fp.write(' r = lookup1D(lut, min1d, max1d, r);\n')
299 fp.write(' g = lookup1D(lut, min1d, max1d, g);\n')
300 fp.write(' b = lookup1D(lut, min1d, max1d, b);\n')
301 elif components == 3:
302 fp.write(' r = lookup1D(lut0, min1d, max1d, r);\n')
303 fp.write(' g = lookup1D(lut1, min1d, max1d, g);\n')
304 fp.write(' b = lookup1D(lut2, min1d, max1d, b);\n')
306 fp.write(' rOut = r;\n')
307 fp.write(' gOut = g;\n')
308 fp.write(' bOut = b;\n')
309 fp.write(' aOut = aIn;\n')
313 def write_1d(filename,
322 Writes a 1D LUT in the specified format.
326 filename : str or unicode
327 The path of the 1D LUT to be written
329 The lowest value in the 1D ramp
331 The highest value in the 1D ramp
332 data : array of floats
333 The entries in the LUT
335 The resolution of the LUT, i.e. number of entries in the data set
337 The number of channels in the data
338 lut_components : int, optional
339 The number of channels in the data to actually use when writing
340 format : str or unicode, optional
341 The format of the the 1D LUT that will be written
348 ocio_formats_to_extensions = {'cinespace': 'csp',
355 if format in ocio_formats_to_extensions:
356 if ocio_formats_to_extensions[format] == 'csp':
357 write_CSP_1d(filename,
364 elif ocio_formats_to_extensions[format] == 'ctl':
365 write_CTL_1d(filename,
373 write_SPI_1d(filename,
382 def generate_1d_LUT_from_image(ramp_1d_path,
389 Reads a 1D LUT image and writes a 1D LUT in the specified format.
393 ramp_1d_path : str or unicode
394 The path of the 1D ramp image to be read
395 output_path : str or unicode, optional
396 The path of the 1D LUT to be written
397 min_value : float, optional
398 The lowest value in the 1D ramp
399 max_value : float, optional
400 The highest value in the 1D ramp
401 channels : int, optional
402 The number of channels in the data
403 format : str or unicode, optional
404 The format of the the 1D LUT that will be written
411 if output_path is None:
412 output_path = '%s.%s' % (ramp_1d_path, 'spi1d')
414 ramp = oiio.ImageInput.open(ramp_1d_path)
416 ramp_spec = ramp.spec()
417 ramp_width = ramp_spec.width
418 ramp_channels = ramp_spec.nchannels
420 # Forcibly read data as float, the Python API doesn't handle half-float
423 ramp_data = ramp.read_image(type)
425 write_1d(output_path, min_value, max_value,
426 ramp_data, ramp_width, ramp_channels, channels, format)
429 def generate_3d_LUT_image(ramp_3d_path, resolution=32):
431 Generates a 3D LUT image covering the specified resolution
432 Relies on OCIO's ociolutimage command
436 ramp_3d_path : str or unicode
437 The path of the 3D ramp image to be written
438 resolution : int, optional
439 The resolution of the 3D ramp image to be written
446 args = ['--generate',
450 str(resolution * resolution),
453 lut_extract = Process(description='generate a 3d LUT image',
456 lut_extract.execute()
459 def generate_3d_LUT_from_image(ramp_3d_path,
464 Reads a 3D LUT image and writes a 3D LUT in the specified format.
465 Relies on OCIO's ociolutimage command
469 ramp_3d_path : str or unicode
470 The path of the 3D ramp image to be read
471 output_path : str or unicode, optional
472 The path of the 1D LUT to be written
473 resolution : int, optional
474 The resolution of the 3D LUT represented in the image
475 format : str or unicode, optional
476 The format of the the 3D LUT that will be written
483 if output_path is None:
484 output_path = '%s.%s' % (ramp_3d_path, 'spi3d')
486 ocio_formats_to_extensions = {'cinespace': 'csp',
492 if format == 'spi3d' or not (format in ocio_formats_to_extensions):
493 # Extract a spi3d LUT
498 str(resolution * resolution),
503 lut_extract = Process(description='extract a 3d LUT',
506 lut_extract.execute()
509 output_path_spi3d = '%s.%s' % (output_path, 'spi3d')
511 # Extract a spi3d LUT
516 str(resolution * resolution),
521 lut_extract = Process(description='extract a 3d LUT',
524 lut_extract.execute()
526 # Convert to a different format
532 lut_convert = Process(description='convert a 3d LUT',
535 lut_convert.execute()
538 def apply_CTL_to_image(input_image,
544 aces_ctl_directory=None):
546 Applies a set of Academy Color Transformation Language .ctl files to
547 an input image and writes a new image.
548 Relies on the ACES ctlrender command
552 input_image : str or unicode
553 The path to the image to transform using the CTL files
554 output_image : str or unicode
555 The path to write the result of the transforms
556 ctl_paths : array of str or unicode, optional
557 The path to write the result of the transforms
558 input_scale : float, optional
559 The argument to the ctlrender -input_scale parameter
560 For images with integer bit depths, this divides image code values
561 before they are sent to the ctl commands
562 For images with float or half bit depths, this multiplies image code
563 values before they are sent to the ctl commands
564 output_scale : float, optional
565 The argument to the ctlrender -output_scale parameter
566 For images with integer bit depths, this multiplies image code values
567 before they are written to a file.
568 For images with float or half bit depths, this divides image code values
569 before they are sent to the ctl commands
570 global_params : dict of key value pairs, optional
571 The set of parameter names and values to pass to the ctlrender
572 -global_param1 parameter
573 aces_ctl_directory : str or unicode, optional
574 The path to the aces 'transforms/ctl/utilities'
581 if ctl_paths is None:
583 if global_params is None:
586 if len(ctl_paths) > 0:
589 if "/usr/local/bin" not in ctlenv['PATH'].split(':'):
590 ctlenv['PATH'] = "%s:/usr/local/bin" % ctlenv['PATH']
592 if aces_ctl_directory is not None:
593 if os.path.split(aces_ctl_directory)[1] != 'utilities':
594 ctl_module_path = os.path.join(aces_ctl_directory, 'utilities')
596 ctl_module_path = aces_ctl_directory
597 ctlenv['CTL_MODULE_PATH'] = ctl_module_path
600 for ctl in ctl_paths:
601 args += ['-ctl', ctl]
603 args += ['-input_scale', str(input_scale)]
604 args += ['-output_scale', str(output_scale)]
605 args += ['-global_param1', 'aIn', '1.0']
606 for key, value in global_params.iteritems():
607 args += ['-global_param1', key, str(value)]
608 args += [input_image]
609 args += [output_image]
611 ctlp = Process(description='a ctlrender process',
613 args=args, env=ctlenv)
618 def convert_bit_depth(input_image, output_image, depth):
620 Convert the input image to the specified bit depth and write a new image
621 Relies on the OIIO oiiotool command
625 input_image : str or unicode
626 The path to the image to transform using the CTL files
627 output_image : str or unicode
628 The path to write the result of the transforms
629 depth : str or unicode
630 The bit depth of the output image
631 Data types include: uint8, sint8, uint10, uint12, uint16, sint16, half, float, double
643 convert = Process(description='convert image bit depth',
649 def generate_1d_LUT_from_CTL(lut_path,
652 identity_lut_bit_depth='half',
657 aces_ctl_directory=None,
663 Creates a 1D LUT from the specified CTL files by creating a 1D LUT image,
664 applying the CTL files and then extracting and writing a LUT based on the
669 lut_path : str or unicode
670 The path to write the 1D LUT
671 ctl_paths : array of str or unicode
672 The CTL files to apply
673 lut_resolution : int, optional
674 The resolution of the 1D LUT to generate
675 identity_lut_bit_depth : string, optional
676 The bit depth to use for the intermediate 1D LUT image
677 input_scale : float, optional
678 The argument to the ctlrender -input_scale parameter
679 For images with integer bit depths, this divides image code values
680 before they are sent to the ctl commands
681 For images with float or half bit depths, this multiplies image code
682 values before they are sent to the ctl commands
683 output_scale : float, optional
684 The argument to the ctlrender -output_scale parameter
685 For images with integer bit depths, this multiplies image code values
686 before they are written to a file.
687 For images with float or half bit depths, this divides image code values
688 before they are sent to the ctl commands
689 global_params : dict of key, value pairs, optional
690 The set of parameter names and values to pass to the ctlrender
691 -global_param1 parameter
692 cleanup : bool, optional
693 Whether or not to clean up the intermediate images
694 aces_ctl_directory : str or unicode, optional
695 The path to the aces 'transforms/ctl/utilities'
696 min_value : float, optional
697 The minimum value to consider as input to the LUT
698 max_value : float, optional
699 The maximum value to consider as input to the LUT
700 channels : int, optional
701 The number of channels to use for the LUT. 1 or 3 are valid.
702 format : str or unicode, optional
703 The format to use when writing the LUT
710 if global_params is None:
713 lut_path_base = os.path.splitext(lut_path)[0]
715 identity_lut_image_float = '%s.%s.%s' % (lut_path_base, 'float', 'tiff')
716 generate_1d_LUT_image(identity_lut_image_float,
721 if identity_lut_bit_depth not in ['half', 'float']:
722 identity_lut_image = '%s.%s.%s' % (lut_path_base, 'uint16', 'tiff')
723 convert_bit_depth(identity_lut_image_float,
725 identity_lut_bit_depth)
727 identity_lut_image = identity_lut_image_float
729 transformed_lut_image = '%s.%s.%s' % (lut_path_base, 'transformed', 'exr')
730 apply_CTL_to_image(identity_lut_image,
731 transformed_lut_image,
738 generate_1d_LUT_from_image(transformed_lut_image,
746 os.remove(identity_lut_image)
747 if identity_lut_image != identity_lut_image_float:
748 os.remove(identity_lut_image_float)
749 os.remove(transformed_lut_image)
752 def correct_LUT_image(transformed_lut_image,
756 For some combinations of resolution and bit depth, ctlrender would generate
757 images with the right number of pixels but with the values for width and
758 height transposed. This function generating a new, corrected image based on
759 the original. The function acts as a pass through if the problem is not
764 transformed_lut_image : str or unicode
765 The path to an image generated by cltrender
766 corrected_lut_image : str or unicode
767 The path to an 'corrected' image to be generated
769 The resolution of the 3D LUT that should be contained in
770 transformed_lut_image
775 The path to the corrected image, or the original, if no correction was
779 transformed = oiio.ImageInput.open(transformed_lut_image)
781 transformed_spec = transformed.spec()
782 width = transformed_spec.width
783 height = transformed_spec.height
784 channels = transformed_spec.nchannels
786 if width != lut_resolution * lut_resolution or height != lut_resolution:
787 print(('Correcting image as resolution is off. '
788 'Found %d x %d. Expected %d x %d') % (
791 lut_resolution * lut_resolution,
793 print('Generating %s' % corrected_lut_image)
795 # Forcibly read data as float, the Python API doesn't handle half-float
798 source_data = transformed.read_image(type)
800 correct = oiio.ImageOutput.create(corrected_lut_image)
802 correct_spec = oiio.ImageSpec()
803 correct_spec.set_format(oiio.FLOAT)
804 correct_spec.width = height
805 correct_spec.height = width
806 correct_spec.nchannels = channels
808 correct.open(corrected_lut_image, correct_spec, oiio.Create)
810 dest_data = array.array('f',
811 ('\0' * correct_spec.width *
812 correct_spec.height *
813 correct_spec.nchannels * 4))
814 for j in range(0, correct_spec.height):
815 for i in range(0, correct_spec.width):
816 for c in range(0, correct_spec.nchannels):
817 dest_data[(correct_spec.nchannels *
818 correct_spec.width * j +
819 correct_spec.nchannels * i + c)] = (
820 source_data[correct_spec.nchannels *
821 correct_spec.width * j +
822 correct_spec.nchannels * i + c])
824 correct.write_image(correct_spec.format, dest_data)
827 # shutil.copy(transformedLUTImage, correctedLUTImage)
828 corrected_lut_image = transformed_lut_image
832 return corrected_lut_image
835 def generate_3d_LUT_from_CTL(lut_path,
838 identity_lut_bit_depth='half',
843 aces_ctl_directory=None,
846 Creates a 3D LUT from the specified CTL files by creating a 3D LUT image,
847 applying the CTL files and then extracting and writing a LUT based on the
852 lut_path : str or unicode
853 The path to write the 1D LUT
854 ctl_paths : array of str or unicode
855 The CTL files to apply
856 lut_resolution : int, optional
857 The resolution of the 1D LUT to generate
858 identity_lut_bit_depth : string, optional
859 The bit depth to use for the intermediate 1D LUT image
860 input_scale : float, optional
861 The argument to the ctlrender -input_scale parameter
862 For images with integer bit depths, this divides image code values
863 before they are sent to the ctl commands
864 For images with float or half bit depths, this multiplies image code
865 values before they are sent to the ctl commands
866 output_scale : float, optional
867 The argument to the ctlrender -output_scale parameter
868 For images with integer bit depths, this multiplies image code values
869 before they are written to a file.
870 For images with float or half bit depths, this divides image code values
871 before they are sent to the ctl commands
872 global_params : dict of key, value pairs, optional
873 The set of parameter names and values to pass to the ctlrender
874 -global_param1 parameter
875 cleanup : bool, optional
876 Whether or not to clean up the intermediate images
877 aces_ctl_directory : str or unicode, optional
878 The path to the aces 'transforms/ctl/utilities'
879 format : str or unicode, optional
880 The format to use when writing the LUT
887 if global_params is None:
890 lut_path_base = os.path.splitext(lut_path)[0]
892 identity_lut_image_float = '%s.%s.%s' % (lut_path_base, 'float', 'tiff')
893 generate_3d_LUT_image(identity_lut_image_float, lut_resolution)
895 if identity_lut_bit_depth not in ['half', 'float']:
896 identity_lut_image = '%s.%s.%s' % (lut_path_base,
897 identity_lut_bit_depth,
899 convert_bit_depth(identity_lut_image_float,
901 identity_lut_bit_depth)
903 identity_lut_image = identity_lut_image_float
905 transformed_lut_image = '%s.%s.%s' % (lut_path_base, 'transformed', 'exr')
906 apply_CTL_to_image(identity_lut_image,
907 transformed_lut_image,
914 corrected_lut_image = '%s.%s.%s' % (lut_path_base, 'correct', 'exr')
915 corrected_lut_image = correct_LUT_image(transformed_lut_image,
919 generate_3d_LUT_from_image(corrected_lut_image,
925 os.remove(identity_lut_image)
926 if identity_lut_image != identity_lut_image_float:
927 os.remove(identity_lut_image_float)
928 os.remove(transformed_lut_image)
929 if corrected_lut_image != transformed_lut_image:
930 os.remove(corrected_lut_image)
931 if format != 'spi3d':
932 lut_path_spi3d = '%s.%s' % (lut_path, 'spi3d')
933 os.remove(lut_path_spi3d)
938 A simple main that allows the user to exercise the various functions
952 p = optparse.OptionParser(
953 description='A utility to generate LUTs from CTL',
956 usage='%prog [options]')
958 p.add_option('--lut', '-l', type='string', default='')
959 p.add_option('--format', '-f', type='string', default='')
960 p.add_option('--ctl', '-c', type='string', action='append')
961 p.add_option('--lutResolution1d', '', type='int', default=1024)
962 p.add_option('--lutResolution3d', '', type='int', default=33)
963 p.add_option('--ctlReleasePath', '-r', type='string', default='')
964 p.add_option('--bitDepth', '-b', type='string', default='float')
965 p.add_option('--keepTempImages', '', action='store_true')
966 p.add_option('--minValue', '', type='float', default=0)
967 p.add_option('--maxValue', '', type='float', default=1)
968 p.add_option('--inputScale', '', type='float', default=1)
969 p.add_option('--outputScale', '', type='float', default=1)
970 p.add_option('--ctlRenderParam', '-p', type='string', nargs=2,
973 p.add_option('--generate1d', '', action='store_true')
974 p.add_option('--generate3d', '', action='store_true')
976 options, arguments = p.parse_args()
979 format = options.format
981 lut_resolution_1d = options.lutResolution1d
982 lut_resolution_3d = options.lutResolution3d
983 min_value = options.minValue
984 max_value = options.maxValue
985 input_scale = options.inputScale
986 output_scale = options.outputScale
987 ctl_release_path = options.ctlReleasePath
988 generate_1d = options.generate1d is True
989 generate_3d = options.generate3d is True
990 bit_depth = options.bitDepth
991 cleanup = not options.keepTempImages
994 if options.ctlRenderParam is not None:
995 for param in options.ctlRenderParam:
996 params[param[0]] = float(param[1])
999 print('1D LUT generation options')
1001 print('3D LUT generation options')
1003 print('LUT : %s' % lut)
1004 print('Format : %s' % format)
1005 print('CTLs : %s' % ctls)
1006 print('LUT Res 1d : %s' % lut_resolution_1d)
1007 print('LUT Res 3d : %s' % lut_resolution_3d)
1008 print('Min Value : %s' % min_value)
1009 print('Max Value : %s' % max_value)
1010 print('Input Scale : %s' % input_scale)
1011 print('Output Scale : %s' % output_scale)
1012 print('CTL Render Params : %s' % params)
1013 print('CTL Release Path : %s' % ctl_release_path)
1014 print('Input Bit Depth : %s' % bit_depth)
1015 print('Cleanup Temp Images : %s' % cleanup)
1018 generate_1d_LUT_from_CTL(lut,
1032 generate_3d_LUT_from_CTL(lut,
1043 print(('\n\nNo LUT generated! '
1044 'You must choose either 1D or 3D LUT generation\n\n'))
1047 if __name__ == '__main__':