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:
497 if aces_ctl_directory is not None:
498 if os.path.split(aces_ctl_directory)[1] != 'utilities':
499 ctl_module_path = os.path.join(aces_ctl_directory, 'utilities')
501 ctl_module_path = aces_ctl_directory
502 ctlenv['CTL_MODULE_PATH'] = ctl_module_path
505 for ctl in ctl_paths:
506 args += ['-ctl', ctl]
508 args += ['-input_scale', str(input_scale)]
509 args += ['-output_scale', str(output_scale)]
510 args += ['-global_param1', 'aIn', '1.0']
511 for key, value in global_params.iteritems():
512 args += ['-global_param1', key, str(value)]
513 args += [input_image]
514 args += [output_image]
516 ctlp = Process(description='a ctlrender process',
518 args=args, env=ctlenv)
523 def convert_bit_depth(input_image, output_image, depth):
530 Parameter description.
535 Return value description.
543 convert = Process(description='convert image bit depth',
549 def generate_1d_LUT_from_CTL(lut_path,
552 identity_lut_bit_depth='half',
557 aces_ctl_directory=None,
568 Parameter description.
573 Return value description.
576 if global_params is None:
579 lut_path_base = os.path.splitext(lut_path)[0]
581 identity_lut_image_float = '%s.%s.%s' % (lut_path_base, 'float', 'tiff')
582 generate_1d_LUT_image(identity_lut_image_float,
587 if identity_lut_bit_depth not in ['half', 'float']:
588 identity_lut_image = '%s.%s.%s' % (lut_path_base, 'uint16', 'tiff')
589 convert_bit_depth(identity_lut_image_float,
591 identity_lut_bit_depth)
593 identity_lut_image = identity_lut_image_float
595 transformed_lut_image = '%s.%s.%s' % (lut_path_base, 'transformed', 'exr')
596 apply_CTL_to_image(identity_lut_image,
597 transformed_lut_image,
604 generate_1d_LUT_from_image(transformed_lut_image,
612 os.remove(identity_lut_image)
613 if identity_lut_image != identity_lut_image_float:
614 os.remove(identity_lut_image_float)
615 os.remove(transformed_lut_image)
618 def correct_LUT_image(transformed_lut_image,
627 Parameter description.
632 Return value description.
635 transformed = oiio.ImageInput.open(transformed_lut_image)
637 transformed_spec = transformed.spec()
638 width = transformed_spec.width
639 height = transformed_spec.height
640 channels = transformed_spec.nchannels
642 if width != lut_resolution * lut_resolution or height != lut_resolution:
643 print(('Correcting image as resolution is off. '
644 'Found %d x %d. Expected %d x %d') % (
647 lut_resolution * lut_resolution,
649 print('Generating %s' % corrected_lut_image)
651 # Forcibly read data as float, the Python API doesn't handle half-float
654 source_data = transformed.read_image(type)
656 correct = oiio.ImageOutput.create(corrected_lut_image)
658 correct_spec = oiio.ImageSpec()
659 correct_spec.set_format(oiio.FLOAT)
660 correct_spec.width = height
661 correct_spec.height = width
662 correct_spec.nchannels = channels
664 correct.open(corrected_lut_image, correct_spec, oiio.Create)
666 dest_data = array.array('f',
667 ('\0' * correct_spec.width *
668 correct_spec.height *
669 correct_spec.nchannels * 4))
670 for j in range(0, correct_spec.height):
671 for i in range(0, correct_spec.width):
672 for c in range(0, correct_spec.nchannels):
673 dest_data[(correct_spec.nchannels *
674 correct_spec.width * j +
675 correct_spec.nchannels * i + c)] = (
676 source_data[correct_spec.nchannels *
677 correct_spec.width * j +
678 correct_spec.nchannels * i + c])
680 correct.write_image(correct_spec.format, dest_data)
683 # shutil.copy(transformedLUTImage, correctedLUTImage)
684 corrected_lut_image = transformed_lut_image
688 return corrected_lut_image
691 def generate_3d_LUT_from_CTL(lut_path,
694 identity_lut_bit_depth='half',
699 aces_ctl_directory=None,
707 Parameter description.
712 Return value description.
715 if global_params is None:
718 lut_path_base = os.path.splitext(lut_path)[0]
720 identity_lut_image_float = '%s.%s.%s' % (lut_path_base, 'float', 'tiff')
721 generate_3d_LUT_image(identity_lut_image_float, lut_resolution)
723 if identity_lut_bit_depth not in ['half', 'float']:
724 identity_lut_image = '%s.%s.%s' % (lut_path_base,
725 identity_lut_bit_depth,
727 convert_bit_depth(identity_lut_image_float,
729 identity_lut_bit_depth)
731 identity_lut_image = identity_lut_image_float
733 transformed_lut_image = '%s.%s.%s' % (lut_path_base, 'transformed', 'exr')
734 apply_CTL_to_image(identity_lut_image,
735 transformed_lut_image,
742 corrected_lut_image = '%s.%s.%s' % (lut_path_base, 'correct', 'exr')
743 corrected_lut_image = correct_LUT_image(transformed_lut_image,
747 generate_3d_LUT_from_image(corrected_lut_image,
753 os.remove(identity_lut_image)
754 if identity_lut_image != identity_lut_image_float:
755 os.remove(identity_lut_image_float)
756 os.remove(transformed_lut_image)
757 if corrected_lut_image != transformed_lut_image:
758 os.remove(corrected_lut_image)
759 if format != 'spi3d':
760 lut_path_spi3d = '%s.%s' % (lut_path, 'spi3d')
761 os.remove(lut_path_spi3d)
771 Parameter description.
776 Return value description.
781 p = optparse.OptionParser(
782 description='A utility to generate LUTs from CTL',
785 usage='%prog [options]')
787 p.add_option('--lut', '-l', type='string', default='')
788 p.add_option('--format', '-f', type='string', default='')
789 p.add_option('--ctl', '-c', type='string', action='append')
790 p.add_option('--lutResolution1d', '', type='int', default=1024)
791 p.add_option('--lutResolution3d', '', type='int', default=33)
792 p.add_option('--ctlReleasePath', '-r', type='string', default='')
793 p.add_option('--bitDepth', '-b', type='string', default='float')
794 p.add_option('--keepTempImages', '', action='store_true')
795 p.add_option('--minValue', '', type='float', default=0)
796 p.add_option('--maxValue', '', type='float', default=1)
797 p.add_option('--inputScale', '', type='float', default=1)
798 p.add_option('--outputScale', '', type='float', default=1)
799 p.add_option('--ctlRenderParam', '-p', type='string', nargs=2,
802 p.add_option('--generate1d', '', action='store_true')
803 p.add_option('--generate3d', '', action='store_true')
805 options, arguments = p.parse_args()
808 format = options.format
810 lut_resolution_1d = options.lutResolution1d
811 lut_resolution_3d = options.lutResolution3d
812 min_value = options.minValue
813 max_value = options.maxValue
814 input_scale = options.inputScale
815 output_scale = options.outputScale
816 ctl_release_path = options.ctlReleasePath
817 generate_1d = options.generate1d is True
818 generate_3d = options.generate3d is True
819 bit_depth = options.bitDepth
820 cleanup = not options.keepTempImages
823 if options.ctlRenderParam is not None:
824 for param in options.ctlRenderParam:
825 params[param[0]] = float(param[1])
828 print('1D LUT generation options')
830 print('3D LUT generation options')
832 print('Lut : %s' % lut)
833 print('Format : %s' % format)
834 print('CTLs : %s' % ctls)
835 print('Lut Res 1d : %s' % lut_resolution_1d)
836 print('Lut Res 3d : %s' % lut_resolution_3d)
837 print('Min Value : %s' % min_value)
838 print('Max Value : %s' % max_value)
839 print('Input Scale : %s' % input_scale)
840 print('Output Scale : %s' % output_scale)
841 print('CTL Render Params : %s' % params)
842 print('CTL Release Path : %s' % ctl_release_path)
843 print('Input Bit Depth : %s' % bit_depth)
844 print('Cleanup Temp Images : %s' % cleanup)
847 generate_1d_LUT_from_CTL(lut,
861 generate_3d_LUT_from_CTL(lut,
872 print(('\n\nNo LUT generated! '
873 'You must choose either 1D or 3D LUT generation\n\n'))
876 if __name__ == '__main__':