2 # -*- coding: utf-8 -*-
5 Defines objects to generate various kind of 1d, 2d and 3d LUTs in various file
9 from __future__ import division
15 import OpenImageIO as oiio
17 from aces_ocio.process import Process
19 __author__ = 'ACES Developers'
20 __copyright__ = 'Copyright (C) 2014 - 2015 - ACES Developers'
22 __maintainer__ = 'ACES Developers'
23 __email__ = 'aces@oscars.org'
24 __status__ = 'Production'
26 __all__ = ['generate_1d_LUT_image',
28 'generate_1d_LUT_from_image',
29 'generate_3d_LUT_image',
30 'generate_3d_LUT_from_image',
33 'generate_1d_LUT_from_CTL',
35 'generate_3d_LUT_from_CTL',
39 def generate_1d_LUT_image(ramp_1d_path,
49 Parameter description.
54 Return value description.
57 ramp = oiio.ImageOutput.create(ramp_1d_path)
59 spec = oiio.ImageSpec()
60 spec.set_format(oiio.FLOAT)
61 # spec.format.basetype = oiio.FLOAT
62 spec.width = resolution
66 ramp.open(ramp_1d_path, spec, oiio.Create)
68 data = array.array('f',
69 '\0' * spec.width * spec.height * spec.nchannels * 4)
70 for i in range(resolution):
71 value = float(i) / (resolution - 1) * (
72 max_value - min_value) + min_value
73 data[i * spec.nchannels + 0] = value
74 data[i * spec.nchannels + 1] = value
75 data[i * spec.nchannels + 2] = value
77 ramp.write_image(spec.format, data)
81 def write_SPI_1d(filename,
91 Credit to *Alex Fry* for the original single channel version of the spi1d
97 Parameter description.
102 Return value description.
105 # May want to use fewer components than there are channels in the data
106 # Most commonly used for single channel LUTs
107 components = min(3, components, channels)
109 with open(filename, 'w') as fp:
110 fp.write('Version 1\n')
111 fp.write('From %f %f\n' % (from_min, from_max))
112 fp.write('Length %d\n' % entries)
113 fp.write('Components %d\n' % components)
115 for i in range(0, entries):
117 for j in range(0, components):
118 entry = '%s %s' % (entry, data[i * channels + j])
119 fp.write(' %s\n' % entry)
123 def write_CSP_1d(filename,
136 Parameter description.
141 Return value description.
144 # May want to use fewer components than there are channels in the data
145 # Most commonly used for single channel LUTs
146 components = min(3, components, channels)
148 with open(filename, 'w') as fp:
149 fp.write('CSPLUTV100\n')
152 fp.write('BEGIN METADATA\n')
153 fp.write('END METADATA\n')
158 fp.write('%f %f\n' % (from_min, from_max))
159 fp.write('0.0 1.0\n')
161 fp.write('%f %f\n' % (from_min, from_max))
162 fp.write('0.0 1.0\n')
164 fp.write('%f %f\n' % (from_min, from_max))
165 fp.write('0.0 1.0\n')
169 fp.write('%d\n' % entries)
171 for i in range(0, entries):
174 entry = '%s %s' % (entry, data[i * channels])
175 fp.write('%s\n' % entry)
177 for i in range(entries):
179 for j in range(components):
180 entry = '%s %s' % (entry, data[i * channels + j])
181 fp.write('%s\n' % entry)
184 def write_CTL_1d(filename,
197 Parameter description.
202 Return value description.
205 # May want to use fewer components than there are channels in the data
206 # Most commonly used for single channel LUTs
207 components = min(3, components, channels)
209 with open(filename, 'w') as fp:
210 fp.write('// %d x %d LUT generated by "generate_lut"\n' % (
211 entries, components))
213 fp.write('const float min1d = %3.9f;\n' % from_min)
214 fp.write('const float max1d = %3.9f;\n' % from_max)
219 fp.write('const float lut[] = {\n')
220 for i in range(0, entries):
221 fp.write('%s' % data[i * channels])
228 for j in range(components):
229 fp.write('const float lut%d[] = {\n' % j)
230 for i in range(0, entries):
231 fp.write('%s' % data[i * channels])
238 fp.write('void main\n')
240 fp.write(' input varying float rIn,\n')
241 fp.write(' input varying float gIn,\n')
242 fp.write(' input varying float bIn,\n')
243 fp.write(' input varying float aIn,\n')
244 fp.write(' output varying float rOut,\n')
245 fp.write(' output varying float gOut,\n')
246 fp.write(' output varying float bOut,\n')
247 fp.write(' output varying float aOut\n')
250 fp.write(' float r = rIn;\n')
251 fp.write(' float g = gIn;\n')
252 fp.write(' float b = bIn;\n')
254 fp.write(' // Apply LUT\n')
256 fp.write(' r = lookup1D(lut, min1d, max1d, r);\n')
257 fp.write(' g = lookup1D(lut, min1d, max1d, g);\n')
258 fp.write(' b = lookup1D(lut, min1d, max1d, b);\n')
259 elif components == 3:
260 fp.write(' r = lookup1D(lut0, min1d, max1d, r);\n')
261 fp.write(' g = lookup1D(lut1, min1d, max1d, g);\n')
262 fp.write(' b = lookup1D(lut2, min1d, max1d, b);\n')
264 fp.write(' rOut = r;\n')
265 fp.write(' gOut = g;\n')
266 fp.write(' bOut = b;\n')
267 fp.write(' aOut = aIn;\n')
270 def write_1d(filename,
284 Parameter description.
289 Return value description.
292 ocioFormatsToExtensions = {'cinespace' : 'csp',
299 if format in ocioFormatsToExtensions:
300 if ocioFormatsToExtensions[format] == 'csp':
301 write_CSP_1d(filename,
308 elif ocioFormatsToExtensions[format] == 'ctl':
309 write_CTL_1d(filename,
317 write_SPI_1d(filename,
325 def generate_1d_LUT_from_image(ramp_1d_path,
337 Parameter description.
342 Return value description.
345 if output_path is None:
346 output_path = '%s.%s' % (ramp_1d_path, 'spi1d')
348 ramp = oiio.ImageInput.open(ramp_1d_path)
350 ramp_spec = ramp.spec()
351 ramp_width = ramp_spec.width
352 ramp_channels = ramp_spec.nchannels
354 # Forcibly read data as float, the Python API doesn't handle half-float
357 ramp_data = ramp.read_image(type)
359 write_1d(output_path, min_value, max_value,
360 ramp_data, ramp_width, ramp_channels, channels, format)
363 def generate_3d_LUT_image(ramp_3d_path, resolution=32):
370 Parameter description.
375 Return value description.
378 args = ['--generate',
382 str(resolution * resolution),
385 lut_extract = Process(description='generate a 3d LUT image',
388 lut_extract.execute()
391 def generate_3d_LUT_from_image(ramp_3d_path,
401 Parameter description.
406 Return value description.
409 if output_path is None:
410 output_path = '%s.%s' % (ramp_3d_path, 'spi3d')
412 ocioFormatsToExtensions = {'cinespace' : 'csp',
418 if format == 'spi3d' or not (format in ocioFormatsToExtensions):
419 # Extract a spi3d LUT
424 str(resolution * resolution),
429 lut_extract = Process(description='extract a 3d LUT',
432 lut_extract.execute()
435 output_path_spi3d = '%s.%s' % (output_path, 'spi3d')
437 # Extract a spi3d LUT
442 str(resolution * resolution),
447 lut_extract = Process(description='extract a 3d LUT',
450 lut_extract.execute()
452 # Convert to a different format
458 lut_convert = Process(description='convert a 3d LUT',
461 lut_convert.execute()
464 def apply_CTL_to_image(input_image,
470 aces_ctl_directory=None):
477 Parameter description.
482 Return value description.
485 if ctl_paths is None:
487 if global_params is None:
490 if len(ctl_paths) > 0:
492 if aces_ctl_directory is not None:
493 if os.path.split(aces_ctl_directory)[1] != 'utilities':
494 ctl_module_path = os.path.join(aces_ctl_directory, 'utilities')
496 ctl_module_path = aces_ctl_directory
497 ctlenv['CTL_MODULE_PATH'] = ctl_module_path
500 for ctl in ctl_paths:
501 args += ['-ctl', ctl]
503 args += ['-input_scale', str(input_scale)]
504 args += ['-output_scale', str(output_scale)]
505 args += ['-global_param1', 'aIn', '1.0']
506 for key, value in global_params.iteritems():
507 args += ['-global_param1', key, str(value)]
508 args += [input_image]
509 args += [output_image]
511 ctlp = Process(description='a ctlrender process',
513 args=args, env=ctlenv)
518 def convert_bit_depth(input_image, output_image, depth):
525 Parameter description.
530 Return value description.
538 convert = Process(description='convert image bit depth',
544 def generate_1d_LUT_from_CTL(lut_path,
547 identity_LUT_bit_depth='half',
552 aces_ctl_directory=None,
563 Parameter description.
568 Return value description.
571 if global_params is None:
574 lut_path_base = os.path.splitext(lut_path)[0]
576 identity_LUT_image_float = '%s.%s.%s' % (lut_path_base, 'float', 'tiff')
577 generate_1d_LUT_image(identity_LUT_image_float,
582 if identity_LUT_bit_depth not in ['half', 'float']:
583 identity_LUT_image = '%s.%s.%s' % (lut_path_base, 'uint16', 'tiff')
584 convert_bit_depth(identity_LUT_image_float,
586 identity_LUT_bit_depth)
588 identity_LUT_image = identity_LUT_image_float
590 transformed_LUT_image = '%s.%s.%s' % (lut_path_base, 'transformed', 'exr')
591 apply_CTL_to_image(identity_LUT_image,
592 transformed_LUT_image,
599 generate_1d_LUT_from_image(transformed_LUT_image,
607 os.remove(identity_LUT_image)
608 if identity_LUT_image != identity_LUT_image_float:
609 os.remove(identity_LUT_image_float)
610 os.remove(transformed_LUT_image)
613 def correct_LUT_image(transformed_LUT_image,
622 Parameter description.
627 Return value description.
630 transformed = oiio.ImageInput.open(transformed_LUT_image)
632 transformed_spec = transformed.spec()
633 width = transformed_spec.width
634 height = transformed_spec.height
635 channels = transformed_spec.nchannels
637 if width != lut_resolution * lut_resolution or height != lut_resolution:
638 print(('Correcting image as resolution is off. '
639 'Found %d x %d. Expected %d x %d') % (
642 lut_resolution * lut_resolution,
644 print('Generating %s' % corrected_LUT_image)
646 # Forcibly read data as float, the Python API doesn't handle half-float
649 source_data = transformed.read_image(type)
651 correct = oiio.ImageOutput.create(corrected_LUT_image)
653 correct_spec = oiio.ImageSpec()
654 correct_spec.set_format(oiio.FLOAT)
655 correct_spec.width = height
656 correct_spec.height = width
657 correct_spec.nchannels = channels
659 correct.open(corrected_LUT_image, correct_spec, oiio.Create)
661 dest_data = array.array('f',
662 ('\0' * correct_spec.width *
663 correct_spec.height *
664 correct_spec.nchannels * 4))
665 for j in range(0, correct_spec.height):
666 for i in range(0, correct_spec.width):
667 for c in range(0, correct_spec.nchannels):
668 dest_data[(correct_spec.nchannels *
669 correct_spec.width * j +
670 correct_spec.nchannels * i + c)] = (
671 source_data[correct_spec.nchannels *
672 correct_spec.width * j +
673 correct_spec.nchannels * i + c])
675 correct.write_image(correct_spec.format, dest_data)
678 # shutil.copy(transformedLUTImage, correctedLUTImage)
679 corrected_LUT_image = transformed_LUT_image
683 return corrected_LUT_image
686 def generate_3d_LUT_from_CTL(lut_path,
689 identity_LUT_bit_depth='half',
694 aces_ctl_directory=None,
702 Parameter description.
707 Return value description.
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_3d_LUT_image(identity_LUT_image_float, lut_resolution)
718 if identity_LUT_bit_depth not in ['half', 'float']:
719 identity_LUT_image = '%s.%s.%s' % (lut_path_base,
720 identity_LUT_bit_depth,
722 convert_bit_depth(identity_LUT_image_float,
724 identity_LUT_bit_depth)
726 identity_LUT_image = identity_LUT_image_float
728 transformed_LUT_image = '%s.%s.%s' % (lut_path_base, 'transformed', 'exr')
729 apply_CTL_to_image(identity_LUT_image,
730 transformed_LUT_image,
737 corrected_LUT_image = '%s.%s.%s' % (lut_path_base, 'correct', 'exr')
738 corrected_LUT_image = correct_LUT_image(transformed_LUT_image,
742 generate_3d_LUT_from_image(corrected_LUT_image,
748 os.remove(identity_LUT_image)
749 if identity_LUT_image != identity_LUT_image_float:
750 os.remove(identity_LUT_image_float)
751 os.remove(transformed_LUT_image)
752 if corrected_LUT_image != transformed_LUT_image:
753 os.remove(corrected_LUT_image)
754 if format != 'spi3d':
755 lut_path_spi3d = '%s.%s' % (lut_path, 'spi3d')
756 os.remove(lut_path_spi3d)
765 Parameter description.
770 Return value description.
775 p = optparse.OptionParser(
776 description='A utility to generate LUTs from CTL',
779 usage='%prog [options]')
781 p.add_option('--lut', '-l', type='string', default='')
782 p.add_option('--format', '-f', type='string', default='')
783 p.add_option('--ctl', '-c', type='string', action='append')
784 p.add_option('--lutResolution1d', '', type='int', default=1024)
785 p.add_option('--lutResolution3d', '', type='int', default=33)
786 p.add_option('--ctlReleasePath', '-r', type='string', default='')
787 p.add_option('--bitDepth', '-b', type='string', default='float')
788 p.add_option('--keepTempImages', '', action='store_true')
789 p.add_option('--minValue', '', type='float', default=0)
790 p.add_option('--maxValue', '', type='float', default=1)
791 p.add_option('--inputScale', '', type='float', default=1)
792 p.add_option('--outputScale', '', type='float', default=1)
793 p.add_option('--ctlRenderParam', '-p', type='string', nargs=2,
796 p.add_option('--generate1d', '', action='store_true')
797 p.add_option('--generate3d', '', action='store_true')
799 options, arguments = p.parse_args()
802 format = options.format
804 lut_resolution_1d = options.lutResolution1d
805 lut_resolution_3d = options.lutResolution3d
806 min_value = options.minValue
807 max_value = options.maxValue
808 input_scale = options.inputScale
809 output_scale = options.outputScale
810 ctl_release_path = options.ctlReleasePath
811 generate_1d = options.generate1d is True
812 generate_3d = options.generate3d is True
813 bit_depth = options.bitDepth
814 cleanup = not options.keepTempImages
817 if options.ctlRenderParam is not None:
818 for param in options.ctlRenderParam:
819 params[param[0]] = float(param[1])
822 args_start = sys.argv.index('--') + 1
823 args = sys.argv[args_start:]
825 args_start = len(sys.argv) + 1
829 print('1D LUT generation options')
831 print('3D LUT generation options')
833 print('lut : %s' % lut)
834 print('format : %s' % format)
835 print('ctls : %s' % ctls)
836 print('lut res 1d : %s' % lut_resolution_1d)
837 print('lut res 3d : %s' % lut_resolution_3d)
838 print('min value : %s' % min_value)
839 print('max value : %s' % max_value)
840 print('input scale : %s' % input_scale)
841 print('output scale : %s' % output_scale)
842 print('ctl render params : %s' % params)
843 print('ctl release path : %s' % ctl_release_path)
844 print('bit depth of input : %s' % bit_depth)
845 print('cleanup temp images : %s' % cleanup)
848 generate_1d_LUT_from_CTL(lut,
862 generate_3d_LUT_from_CTL(lut,
873 print(('\n\nNo LUT generated. '
874 'You must choose either 1D or 3D LUT generation\n\n'))
877 if __name__ == '__main__':