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)
185 def write_CTL_1d(filename,
198 Parameter description.
203 Return value description.
206 # May want to use fewer components than there are channels in the data
207 # Most commonly used for single channel LUTs
208 components = min(3, components, channels)
210 with open(filename, 'w') as fp:
211 fp.write('// %d x %d LUT generated by "generate_lut"\n' % (
212 entries, components))
214 fp.write('const float min1d = %3.9f;\n' % from_min)
215 fp.write('const float max1d = %3.9f;\n' % from_max)
220 fp.write('const float lut[] = {\n')
221 for i in range(0, entries):
222 fp.write('%s' % data[i * channels])
223 if i != (entries - 1):
229 for j in range(components):
230 fp.write('const float lut%d[] = {\n' % j)
231 for i in range(0, entries):
232 fp.write('%s' % data[i * channels])
233 if i != (entries - 1):
239 fp.write('void main\n')
241 fp.write(' input varying float rIn,\n')
242 fp.write(' input varying float gIn,\n')
243 fp.write(' input varying float bIn,\n')
244 fp.write(' input varying float aIn,\n')
245 fp.write(' output varying float rOut,\n')
246 fp.write(' output varying float gOut,\n')
247 fp.write(' output varying float bOut,\n')
248 fp.write(' output varying float aOut\n')
251 fp.write(' float r = rIn;\n')
252 fp.write(' float g = gIn;\n')
253 fp.write(' float b = bIn;\n')
255 fp.write(' // Apply LUT\n')
257 fp.write(' r = lookup1D(lut, min1d, max1d, r);\n')
258 fp.write(' g = lookup1D(lut, min1d, max1d, g);\n')
259 fp.write(' b = lookup1D(lut, min1d, max1d, b);\n')
260 elif components == 3:
261 fp.write(' r = lookup1D(lut0, min1d, max1d, r);\n')
262 fp.write(' g = lookup1D(lut1, min1d, max1d, g);\n')
263 fp.write(' b = lookup1D(lut2, min1d, max1d, b);\n')
265 fp.write(' rOut = r;\n')
266 fp.write(' gOut = g;\n')
267 fp.write(' bOut = b;\n')
268 fp.write(' aOut = aIn;\n')
272 def write_1d(filename,
286 Parameter description.
291 Return value description.
294 ocioFormatsToExtensions = {'cinespace': 'csp',
301 if format in ocioFormatsToExtensions:
302 if ocioFormatsToExtensions[format] == 'csp':
303 write_CSP_1d(filename,
310 elif ocioFormatsToExtensions[format] == 'ctl':
311 write_CTL_1d(filename,
319 write_SPI_1d(filename,
328 def generate_1d_LUT_from_image(ramp_1d_path,
340 Parameter description.
345 Return value description.
348 if output_path is None:
349 output_path = '%s.%s' % (ramp_1d_path, 'spi1d')
351 ramp = oiio.ImageInput.open(ramp_1d_path)
353 ramp_spec = ramp.spec()
354 ramp_width = ramp_spec.width
355 ramp_channels = ramp_spec.nchannels
357 # Forcibly read data as float, the Python API doesn't handle half-float
360 ramp_data = ramp.read_image(type)
362 write_1d(output_path, min_value, max_value,
363 ramp_data, ramp_width, ramp_channels, channels, format)
366 def generate_3d_LUT_image(ramp_3d_path, resolution=32):
373 Parameter description.
378 Return value description.
381 args = ['--generate',
385 str(resolution * resolution),
388 lut_extract = Process(description='generate a 3d LUT image',
391 lut_extract.execute()
394 def generate_3d_LUT_from_image(ramp_3d_path,
404 Parameter description.
409 Return value description.
412 if output_path is None:
413 output_path = '%s.%s' % (ramp_3d_path, 'spi3d')
415 ocioFormatsToExtensions = {'cinespace': 'csp',
421 if format == 'spi3d' or not (format in ocioFormatsToExtensions):
422 # Extract a spi3d LUT
427 str(resolution * resolution),
432 lut_extract = Process(description='extract a 3d LUT',
435 lut_extract.execute()
438 output_path_spi3d = '%s.%s' % (output_path, 'spi3d')
440 # Extract a spi3d LUT
445 str(resolution * resolution),
450 lut_extract = Process(description='extract a 3d LUT',
453 lut_extract.execute()
455 # Convert to a different format
461 lut_convert = Process(description='convert a 3d LUT',
464 lut_convert.execute()
467 def apply_CTL_to_image(input_image,
473 aces_ctl_directory=None):
480 Parameter description.
485 Return value description.
488 if ctl_paths is None:
490 if global_params is None:
493 if len(ctl_paths) > 0:
495 if aces_ctl_directory is not None:
496 if os.path.split(aces_ctl_directory)[1] != 'utilities':
497 ctl_module_path = os.path.join(aces_ctl_directory, 'utilities')
499 ctl_module_path = aces_ctl_directory
500 ctlenv['CTL_MODULE_PATH'] = ctl_module_path
503 for ctl in ctl_paths:
504 args += ['-ctl', ctl]
506 args += ['-input_scale', str(input_scale)]
507 args += ['-output_scale', str(output_scale)]
508 args += ['-global_param1', 'aIn', '1.0']
509 for key, value in global_params.iteritems():
510 args += ['-global_param1', key, str(value)]
511 args += [input_image]
512 args += [output_image]
514 ctlp = Process(description='a ctlrender process',
516 args=args, env=ctlenv)
521 def convert_bit_depth(input_image, output_image, depth):
528 Parameter description.
533 Return value description.
541 convert = Process(description='convert image bit depth',
547 def generate_1d_LUT_from_CTL(lut_path,
550 identity_LUT_bit_depth='half',
555 aces_ctl_directory=None,
566 Parameter description.
571 Return value description.
574 if global_params is None:
577 lut_path_base = os.path.splitext(lut_path)[0]
579 identity_LUT_image_float = '%s.%s.%s' % (lut_path_base, 'float', 'tiff')
580 generate_1d_LUT_image(identity_LUT_image_float,
585 if identity_LUT_bit_depth not in ['half', 'float']:
586 identity_LUT_image = '%s.%s.%s' % (lut_path_base, 'uint16', 'tiff')
587 convert_bit_depth(identity_LUT_image_float,
589 identity_LUT_bit_depth)
591 identity_LUT_image = identity_LUT_image_float
593 transformed_LUT_image = '%s.%s.%s' % (lut_path_base, 'transformed', 'exr')
594 apply_CTL_to_image(identity_LUT_image,
595 transformed_LUT_image,
602 generate_1d_LUT_from_image(transformed_LUT_image,
610 os.remove(identity_LUT_image)
611 if identity_LUT_image != identity_LUT_image_float:
612 os.remove(identity_LUT_image_float)
613 os.remove(transformed_LUT_image)
616 def correct_LUT_image(transformed_LUT_image,
625 Parameter description.
630 Return value description.
633 transformed = oiio.ImageInput.open(transformed_LUT_image)
635 transformed_spec = transformed.spec()
636 width = transformed_spec.width
637 height = transformed_spec.height
638 channels = transformed_spec.nchannels
640 if width != lut_resolution * lut_resolution or height != lut_resolution:
641 print(('Correcting image as resolution is off. '
642 'Found %d x %d. Expected %d x %d') % (
645 lut_resolution * lut_resolution,
647 print('Generating %s' % corrected_LUT_image)
649 # Forcibly read data as float, the Python API doesn't handle half-float
652 source_data = transformed.read_image(type)
654 correct = oiio.ImageOutput.create(corrected_LUT_image)
656 correct_spec = oiio.ImageSpec()
657 correct_spec.set_format(oiio.FLOAT)
658 correct_spec.width = height
659 correct_spec.height = width
660 correct_spec.nchannels = channels
662 correct.open(corrected_LUT_image, correct_spec, oiio.Create)
664 dest_data = array.array('f',
665 ('\0' * correct_spec.width *
666 correct_spec.height *
667 correct_spec.nchannels * 4))
668 for j in range(0, correct_spec.height):
669 for i in range(0, correct_spec.width):
670 for c in range(0, correct_spec.nchannels):
671 dest_data[(correct_spec.nchannels *
672 correct_spec.width * j +
673 correct_spec.nchannels * i + c)] = (
674 source_data[correct_spec.nchannels *
675 correct_spec.width * j +
676 correct_spec.nchannels * i + c])
678 correct.write_image(correct_spec.format, dest_data)
681 # shutil.copy(transformedLUTImage, correctedLUTImage)
682 corrected_LUT_image = transformed_LUT_image
686 return corrected_LUT_image
689 def generate_3d_LUT_from_CTL(lut_path,
692 identity_LUT_bit_depth='half',
697 aces_ctl_directory=None,
705 Parameter description.
710 Return value description.
713 if global_params is None:
716 lut_path_base = os.path.splitext(lut_path)[0]
718 identity_LUT_image_float = '%s.%s.%s' % (lut_path_base, 'float', 'tiff')
719 generate_3d_LUT_image(identity_LUT_image_float, lut_resolution)
721 if identity_LUT_bit_depth not in ['half', 'float']:
722 identity_LUT_image = '%s.%s.%s' % (lut_path_base,
723 identity_LUT_bit_depth,
725 convert_bit_depth(identity_LUT_image_float,
727 identity_LUT_bit_depth)
729 identity_LUT_image = identity_LUT_image_float
731 transformed_LUT_image = '%s.%s.%s' % (lut_path_base, 'transformed', 'exr')
732 apply_CTL_to_image(identity_LUT_image,
733 transformed_LUT_image,
740 corrected_LUT_image = '%s.%s.%s' % (lut_path_base, 'correct', 'exr')
741 corrected_LUT_image = correct_LUT_image(transformed_LUT_image,
745 generate_3d_LUT_from_image(corrected_LUT_image,
751 os.remove(identity_LUT_image)
752 if identity_LUT_image != identity_LUT_image_float:
753 os.remove(identity_LUT_image_float)
754 os.remove(transformed_LUT_image)
755 if corrected_LUT_image != transformed_LUT_image:
756 os.remove(corrected_LUT_image)
757 if format != 'spi3d':
758 lut_path_spi3d = '%s.%s' % (lut_path, 'spi3d')
759 os.remove(lut_path_spi3d)
769 Parameter description.
774 Return value description.
779 p = optparse.OptionParser(
780 description='A utility to generate LUTs from CTL',
783 usage='%prog [options]')
785 p.add_option('--lut', '-l', type='string', default='')
786 p.add_option('--format', '-f', type='string', default='')
787 p.add_option('--ctl', '-c', type='string', action='append')
788 p.add_option('--lutResolution1d', '', type='int', default=1024)
789 p.add_option('--lutResolution3d', '', type='int', default=33)
790 p.add_option('--ctlReleasePath', '-r', type='string', default='')
791 p.add_option('--bitDepth', '-b', type='string', default='float')
792 p.add_option('--keepTempImages', '', action='store_true')
793 p.add_option('--minValue', '', type='float', default=0)
794 p.add_option('--maxValue', '', type='float', default=1)
795 p.add_option('--inputScale', '', type='float', default=1)
796 p.add_option('--outputScale', '', type='float', default=1)
797 p.add_option('--ctlRenderParam', '-p', type='string', nargs=2,
800 p.add_option('--generate1d', '', action='store_true')
801 p.add_option('--generate3d', '', action='store_true')
803 options, arguments = p.parse_args()
806 format = options.format
808 lut_resolution_1d = options.lutResolution1d
809 lut_resolution_3d = options.lutResolution3d
810 min_value = options.minValue
811 max_value = options.maxValue
812 input_scale = options.inputScale
813 output_scale = options.outputScale
814 ctl_release_path = options.ctlReleasePath
815 generate_1d = options.generate1d is True
816 generate_3d = options.generate3d is True
817 bit_depth = options.bitDepth
818 cleanup = not options.keepTempImages
821 if options.ctlRenderParam is not None:
822 for param in options.ctlRenderParam:
823 params[param[0]] = float(param[1])
826 args_start = sys.argv.index('--') + 1
827 args = sys.argv[args_start:]
829 args_start = len(sys.argv) + 1
833 print('1D LUT generation options')
835 print('3D LUT generation options')
837 print('lut : %s' % lut)
838 print('format : %s' % format)
839 print('ctls : %s' % ctls)
840 print('lut res 1d : %s' % lut_resolution_1d)
841 print('lut res 3d : %s' % lut_resolution_3d)
842 print('min value : %s' % min_value)
843 print('max value : %s' % max_value)
844 print('input scale : %s' % input_scale)
845 print('output scale : %s' % output_scale)
846 print('ctl render params : %s' % params)
847 print('ctl release path : %s' % ctl_release_path)
848 print('bit depth of input : %s' % bit_depth)
849 print('cleanup temp images : %s' % cleanup)
852 generate_1d_LUT_from_CTL(lut,
866 generate_3d_LUT_from_CTL(lut,
877 print(('\n\nNo LUT generated. '
878 'You must choose either 1D or 3D LUT generation\n\n'))
881 if __name__ == '__main__':