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
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,
51 Parameter description.
56 Return value description.
59 ramp = oiio.ImageOutput.create(ramp_1d_path)
61 spec = oiio.ImageSpec()
62 spec.set_format(oiio.FLOAT)
63 # spec.format.basetype = oiio.FLOAT
64 spec.width = resolution
68 ramp.open(ramp_1d_path, spec, oiio.Create)
70 data = array.array('f',
71 '\0' * spec.width * spec.height * spec.nchannels * 4)
72 for i in range(resolution):
73 value = float(i) / (resolution - 1) * (
74 max_value - min_value) + min_value
75 data[i * spec.nchannels + 0] = value
76 data[i * spec.nchannels + 1] = value
77 data[i * spec.nchannels + 2] = value
79 ramp.write_image(spec.format, data)
83 def write_SPI_1d(filename,
93 Credit to *Alex Fry* for the original single channel version of the spi1d
99 Parameter description.
104 Return value description.
107 # May want to use fewer components than there are channels in the data
108 # Most commonly used for single channel LUTs
109 components = min(3, components, channels)
111 with open(filename, 'w') as fp:
112 fp.write('Version 1\n')
113 fp.write('From %f %f\n' % (from_min, from_max))
114 fp.write('Length %d\n' % entries)
115 fp.write('Components %d\n' % components)
117 for i in range(0, entries):
119 for j in range(0, components):
120 entry = '%s %s' % (entry, data[i * channels + j])
121 fp.write(' %s\n' % entry)
125 def write_CSP_1d(filename,
138 Parameter description.
143 Return value description.
146 # May want to use fewer components than there are channels in the data
147 # Most commonly used for single channel LUTs
148 components = min(3, components, channels)
150 with open(filename, 'w') as fp:
151 fp.write('CSPLUTV100\n')
154 fp.write('BEGIN METADATA\n')
155 fp.write('END METADATA\n')
160 fp.write('%f %f\n' % (from_min, from_max))
161 fp.write('0.0 1.0\n')
163 fp.write('%f %f\n' % (from_min, from_max))
164 fp.write('0.0 1.0\n')
166 fp.write('%f %f\n' % (from_min, from_max))
167 fp.write('0.0 1.0\n')
171 fp.write('%d\n' % entries)
173 for i in range(0, entries):
176 entry = '%s %s' % (entry, data[i * channels])
177 fp.write('%s\n' % entry)
179 for i in range(entries):
181 for j in range(components):
182 entry = '%s %s' % (entry, data[i * channels + j])
183 fp.write('%s\n' % entry)
187 def write_CTL_1d(filename,
200 Parameter description.
205 Return value description.
208 # May want to use fewer components than there are channels in the data
209 # Most commonly used for single channel LUTs
210 components = min(3, components, channels)
212 with open(filename, 'w') as fp:
213 fp.write('// %d x %d LUT generated by "generate_lut"\n' % (
214 entries, components))
216 fp.write('const float min1d = %3.9f;\n' % from_min)
217 fp.write('const float max1d = %3.9f;\n' % from_max)
222 fp.write('const float lut[] = {\n')
223 for i in range(0, entries):
224 fp.write('%s' % data[i * channels])
225 if i != (entries - 1):
231 for j in range(components):
232 fp.write('const float lut%d[] = {\n' % j)
233 for i in range(0, entries):
234 fp.write('%s' % data[i * channels])
235 if i != (entries - 1):
241 fp.write('void main\n')
243 fp.write(' input varying float rIn,\n')
244 fp.write(' input varying float gIn,\n')
245 fp.write(' input varying float bIn,\n')
246 fp.write(' input varying float aIn,\n')
247 fp.write(' output varying float rOut,\n')
248 fp.write(' output varying float gOut,\n')
249 fp.write(' output varying float bOut,\n')
250 fp.write(' output varying float aOut\n')
253 fp.write(' float r = rIn;\n')
254 fp.write(' float g = gIn;\n')
255 fp.write(' float b = bIn;\n')
257 fp.write(' // Apply LUT\n')
259 fp.write(' r = lookup1D(lut, min1d, max1d, r);\n')
260 fp.write(' g = lookup1D(lut, min1d, max1d, g);\n')
261 fp.write(' b = lookup1D(lut, min1d, max1d, b);\n')
262 elif components == 3:
263 fp.write(' r = lookup1D(lut0, min1d, max1d, r);\n')
264 fp.write(' g = lookup1D(lut1, min1d, max1d, g);\n')
265 fp.write(' b = lookup1D(lut2, min1d, max1d, b);\n')
267 fp.write(' rOut = r;\n')
268 fp.write(' gOut = g;\n')
269 fp.write(' bOut = b;\n')
270 fp.write(' aOut = aIn;\n')
274 def write_1d(filename,
288 Parameter description.
293 Return value description.
296 ocio_formats_to_extensions = {'cinespace': 'csp',
303 if format in ocio_formats_to_extensions:
304 if ocio_formats_to_extensions[format] == 'csp':
305 write_CSP_1d(filename,
312 elif ocio_formats_to_extensions[format] == 'ctl':
313 write_CTL_1d(filename,
321 write_SPI_1d(filename,
330 def generate_1d_LUT_from_image(ramp_1d_path,
342 Parameter description.
347 Return value description.
350 if output_path is None:
351 output_path = '%s.%s' % (ramp_1d_path, 'spi1d')
353 ramp = oiio.ImageInput.open(ramp_1d_path)
355 ramp_spec = ramp.spec()
356 ramp_width = ramp_spec.width
357 ramp_channels = ramp_spec.nchannels
359 # Forcibly read data as float, the Python API doesn't handle half-float
362 ramp_data = ramp.read_image(type)
364 write_1d(output_path, min_value, max_value,
365 ramp_data, ramp_width, ramp_channels, channels, format)
368 def generate_3d_LUT_image(ramp_3d_path, resolution=32):
375 Parameter description.
380 Return value description.
383 args = ['--generate',
387 str(resolution * resolution),
390 lut_extract = Process(description='generate a 3d LUT image',
393 lut_extract.execute()
396 def generate_3d_LUT_from_image(ramp_3d_path,
406 Parameter description.
411 Return value description.
414 if output_path is None:
415 output_path = '%s.%s' % (ramp_3d_path, 'spi3d')
417 ocio_formats_to_extensions = {'cinespace': 'csp',
423 if format == 'spi3d' or not (format in ocio_formats_to_extensions):
424 # Extract a spi3d LUT
429 str(resolution * resolution),
434 lut_extract = Process(description='extract a 3d LUT',
437 lut_extract.execute()
440 output_path_spi3d = '%s.%s' % (output_path, 'spi3d')
442 # Extract a spi3d LUT
447 str(resolution * resolution),
452 lut_extract = Process(description='extract a 3d LUT',
455 lut_extract.execute()
457 # Convert to a different format
463 lut_convert = Process(description='convert a 3d LUT',
466 lut_convert.execute()
469 def apply_CTL_to_image(input_image,
475 aces_ctl_directory=None):
482 Parameter description.
487 Return value description.
490 if ctl_paths is None:
492 if global_params is None:
495 if len(ctl_paths) > 0:
498 if "/usr/local/bin" not in ctlenv['PATH'].split(':'):
499 ctlenv['PATH'] = "%s:/usr/local/bin" % ctlenv['PATH']
501 if aces_ctl_directory is not None:
502 if os.path.split(aces_ctl_directory)[1] != 'utilities':
503 ctl_module_path = os.path.join(aces_ctl_directory, 'utilities')
505 ctl_module_path = aces_ctl_directory
506 ctlenv['CTL_MODULE_PATH'] = ctl_module_path
509 for ctl in ctl_paths:
510 args += ['-ctl', ctl]
512 args += ['-input_scale', str(input_scale)]
513 args += ['-output_scale', str(output_scale)]
514 args += ['-global_param1', 'aIn', '1.0']
515 for key, value in global_params.iteritems():
516 args += ['-global_param1', key, str(value)]
517 args += [input_image]
518 args += [output_image]
520 ctlp = Process(description='a ctlrender process',
522 args=args, env=ctlenv)
527 def convert_bit_depth(input_image, output_image, depth):
534 Parameter description.
539 Return value description.
547 convert = Process(description='convert image bit depth',
553 def generate_1d_LUT_from_CTL(lut_path,
556 identity_lut_bit_depth='half',
561 aces_ctl_directory=None,
572 Parameter description.
577 Return value description.
580 if global_params is None:
583 lut_path_base = os.path.splitext(lut_path)[0]
585 identity_lut_image_float = '%s.%s.%s' % (lut_path_base, 'float', 'tiff')
586 generate_1d_LUT_image(identity_lut_image_float,
591 if identity_lut_bit_depth not in ['half', 'float']:
592 identity_lut_image = '%s.%s.%s' % (lut_path_base, 'uint16', 'tiff')
593 convert_bit_depth(identity_lut_image_float,
595 identity_lut_bit_depth)
597 identity_lut_image = identity_lut_image_float
599 transformed_lut_image = '%s.%s.%s' % (lut_path_base, 'transformed', 'exr')
600 apply_CTL_to_image(identity_lut_image,
601 transformed_lut_image,
608 generate_1d_LUT_from_image(transformed_lut_image,
616 os.remove(identity_lut_image)
617 if identity_lut_image != identity_lut_image_float:
618 os.remove(identity_lut_image_float)
619 os.remove(transformed_lut_image)
622 def correct_LUT_image(transformed_lut_image,
631 Parameter description.
636 Return value description.
639 transformed = oiio.ImageInput.open(transformed_lut_image)
641 transformed_spec = transformed.spec()
642 width = transformed_spec.width
643 height = transformed_spec.height
644 channels = transformed_spec.nchannels
646 if width != lut_resolution * lut_resolution or height != lut_resolution:
647 print(('Correcting image as resolution is off. '
648 'Found %d x %d. Expected %d x %d') % (
651 lut_resolution * lut_resolution,
653 print('Generating %s' % corrected_lut_image)
655 # Forcibly read data as float, the Python API doesn't handle half-float
658 source_data = transformed.read_image(type)
660 correct = oiio.ImageOutput.create(corrected_lut_image)
662 correct_spec = oiio.ImageSpec()
663 correct_spec.set_format(oiio.FLOAT)
664 correct_spec.width = height
665 correct_spec.height = width
666 correct_spec.nchannels = channels
668 correct.open(corrected_lut_image, correct_spec, oiio.Create)
670 dest_data = array.array('f',
671 ('\0' * correct_spec.width *
672 correct_spec.height *
673 correct_spec.nchannels * 4))
674 for j in range(0, correct_spec.height):
675 for i in range(0, correct_spec.width):
676 for c in range(0, correct_spec.nchannels):
677 dest_data[(correct_spec.nchannels *
678 correct_spec.width * j +
679 correct_spec.nchannels * i + c)] = (
680 source_data[correct_spec.nchannels *
681 correct_spec.width * j +
682 correct_spec.nchannels * i + c])
684 correct.write_image(correct_spec.format, dest_data)
687 # shutil.copy(transformedLUTImage, correctedLUTImage)
688 corrected_lut_image = transformed_lut_image
692 return corrected_lut_image
695 def generate_3d_LUT_from_CTL(lut_path,
698 identity_lut_bit_depth='half',
703 aces_ctl_directory=None,
711 Parameter description.
716 Return value description.
719 if global_params is None:
722 lut_path_base = os.path.splitext(lut_path)[0]
724 identity_lut_image_float = '%s.%s.%s' % (lut_path_base, 'float', 'tiff')
725 generate_3d_LUT_image(identity_lut_image_float, lut_resolution)
727 if identity_lut_bit_depth not in ['half', 'float']:
728 identity_lut_image = '%s.%s.%s' % (lut_path_base,
729 identity_lut_bit_depth,
731 convert_bit_depth(identity_lut_image_float,
733 identity_lut_bit_depth)
735 identity_lut_image = identity_lut_image_float
737 transformed_lut_image = '%s.%s.%s' % (lut_path_base, 'transformed', 'exr')
738 apply_CTL_to_image(identity_lut_image,
739 transformed_lut_image,
746 corrected_lut_image = '%s.%s.%s' % (lut_path_base, 'correct', 'exr')
747 corrected_lut_image = correct_LUT_image(transformed_lut_image,
751 generate_3d_LUT_from_image(corrected_lut_image,
757 os.remove(identity_lut_image)
758 if identity_lut_image != identity_lut_image_float:
759 os.remove(identity_lut_image_float)
760 os.remove(transformed_lut_image)
761 if corrected_lut_image != transformed_lut_image:
762 os.remove(corrected_lut_image)
763 if format != 'spi3d':
764 lut_path_spi3d = '%s.%s' % (lut_path, 'spi3d')
765 os.remove(lut_path_spi3d)
775 Parameter description.
780 Return value description.
785 p = optparse.OptionParser(
786 description='A utility to generate LUTs from CTL',
789 usage='%prog [options]')
791 p.add_option('--lut', '-l', type='string', default='')
792 p.add_option('--format', '-f', type='string', default='')
793 p.add_option('--ctl', '-c', type='string', action='append')
794 p.add_option('--lutResolution1d', '', type='int', default=1024)
795 p.add_option('--lutResolution3d', '', type='int', default=33)
796 p.add_option('--ctlReleasePath', '-r', type='string', default='')
797 p.add_option('--bitDepth', '-b', type='string', default='float')
798 p.add_option('--keepTempImages', '', action='store_true')
799 p.add_option('--minValue', '', type='float', default=0)
800 p.add_option('--maxValue', '', type='float', default=1)
801 p.add_option('--inputScale', '', type='float', default=1)
802 p.add_option('--outputScale', '', type='float', default=1)
803 p.add_option('--ctlRenderParam', '-p', type='string', nargs=2,
806 p.add_option('--generate1d', '', action='store_true')
807 p.add_option('--generate3d', '', action='store_true')
809 options, arguments = p.parse_args()
812 format = options.format
814 lut_resolution_1d = options.lutResolution1d
815 lut_resolution_3d = options.lutResolution3d
816 min_value = options.minValue
817 max_value = options.maxValue
818 input_scale = options.inputScale
819 output_scale = options.outputScale
820 ctl_release_path = options.ctlReleasePath
821 generate_1d = options.generate1d is True
822 generate_3d = options.generate3d is True
823 bit_depth = options.bitDepth
824 cleanup = not options.keepTempImages
827 if options.ctlRenderParam is not None:
828 for param in options.ctlRenderParam:
829 params[param[0]] = float(param[1])
832 print('1D LUT generation options')
834 print('3D LUT generation options')
836 print('LUT : %s' % lut)
837 print('Format : %s' % format)
838 print('CTLs : %s' % ctls)
839 print('LUT Res 1d : %s' % lut_resolution_1d)
840 print('LUT Res 3d : %s' % lut_resolution_3d)
841 print('Min Value : %s' % min_value)
842 print('Max Value : %s' % max_value)
843 print('Input Scale : %s' % input_scale)
844 print('Output Scale : %s' % output_scale)
845 print('CTL Render Params : %s' % params)
846 print('CTL Release Path : %s' % ctl_release_path)
847 print('Input Bit Depth : %s' % bit_depth)
848 print('Cleanup Temp Images : %s' % cleanup)
851 generate_1d_LUT_from_CTL(lut,
865 generate_3d_LUT_from_CTL(lut,
876 print(('\n\nNo LUT generated! '
877 'You must choose either 1D or 3D LUT generation\n\n'))
880 if __name__ == '__main__':