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',
27 'generate_1d_LUT_from_image',
28 'generate_3d_LUT_image',
29 'generate_3d_LUT_from_image',
32 'generate_1d_LUT_from_CTL',
34 'generate_3d_LUT_from_CTL',
38 def generate_1d_LUT_image(ramp_1d_path,
48 Parameter description.
53 Return value description.
56 ramp = oiio.ImageOutput.create(ramp_1d_path)
58 spec = oiio.ImageSpec()
59 spec.set_format(oiio.FLOAT)
60 # spec.format.basetype = oiio.FLOAT
61 spec.width = resolution
65 ramp.open(ramp_1d_path, spec, oiio.Create)
67 data = array.array('f',
68 '\0' * spec.width * spec.height * spec.nchannels * 4)
69 for i in range(resolution):
70 value = float(i) / (resolution - 1) * (
71 max_value - min_value) + min_value
72 data[i * spec.nchannels + 0] = value
73 data[i * spec.nchannels + 1] = value
74 data[i * spec.nchannels + 2] = value
76 ramp.write_image(spec.format, data)
80 def write_SPI_1d(filename,
90 Credit to *Alex Fry* for the original single channel version of the spi1d
96 Parameter description.
101 Return value description.
104 # May want to use fewer components than there are channels in the data
105 # Most commonly used for single channel LUTs
106 components = min(3, components, channels)
108 with open(filename, 'w') as fp:
109 fp.write('Version 1\n')
110 fp.write('From %f %f\n' % (from_min, from_max))
111 fp.write('Length %d\n' % entries)
112 fp.write('Components %d\n' % components)
114 for i in range(0, entries):
116 for j in range(0, components):
117 entry = '%s %s' % (entry, data[i * channels + j])
118 fp.write(' %s\n' % entry)
122 def write_CSP_1d(filename,
135 Parameter description.
140 Return value description.
143 # May want to use fewer components than there are channels in the data
144 # Most commonly used for single channel LUTs
145 components = min(3, components, channels)
147 with open(filename, 'w') as fp:
148 fp.write('CSPLUTV100\n')
151 fp.write('BEGIN METADATA\n')
152 fp.write('END METADATA\n')
157 fp.write('%f %f\n' % (from_min, from_max))
158 fp.write('0.0 1.0\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')
168 fp.write('%d\n' % entries)
170 for i in range(0, entries):
173 entry = '%s %s' % (entry, data[i * channels])
174 fp.write('%s\n' % entry)
176 for i in range(entries):
178 for j in range(components):
179 entry = '%s %s' % (entry, data[i * channels + j])
180 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])
222 if i != (entries - 1):
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])
232 if i != (entries - 1):
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')
271 def write_1d(filename,
285 Parameter description.
290 Return value description.
293 ocio_formats_to_extensions = {'cinespace': 'csp',
300 if format in ocio_formats_to_extensions:
301 if ocio_formats_to_extensions[format] == 'csp':
302 write_CSP_1d(filename,
309 elif ocio_formats_to_extensions[format] == 'ctl':
310 write_CTL_1d(filename,
318 write_SPI_1d(filename,
327 def generate_1d_LUT_from_image(ramp_1d_path,
339 Parameter description.
344 Return value description.
347 if output_path is None:
348 output_path = '%s.%s' % (ramp_1d_path, 'spi1d')
350 ramp = oiio.ImageInput.open(ramp_1d_path)
352 ramp_spec = ramp.spec()
353 ramp_width = ramp_spec.width
354 ramp_channels = ramp_spec.nchannels
356 # Forcibly read data as float, the Python API doesn't handle half-float
359 ramp_data = ramp.read_image(type)
361 write_1d(output_path, min_value, max_value,
362 ramp_data, ramp_width, ramp_channels, channels, format)
365 def generate_3d_LUT_image(ramp_3d_path, resolution=32):
372 Parameter description.
377 Return value description.
380 args = ['--generate',
384 str(resolution * resolution),
387 lut_extract = Process(description='generate a 3d LUT image',
390 lut_extract.execute()
393 def generate_3d_LUT_from_image(ramp_3d_path,
403 Parameter description.
408 Return value description.
411 if output_path is None:
412 output_path = '%s.%s' % (ramp_3d_path, 'spi3d')
414 ocio_formats_to_extensions = {'cinespace': 'csp',
420 if format == 'spi3d' or not (format in ocio_formats_to_extensions):
421 # Extract a spi3d LUT
426 str(resolution * resolution),
431 lut_extract = Process(description='extract a 3d LUT',
434 lut_extract.execute()
437 output_path_spi3d = '%s.%s' % (output_path, 'spi3d')
439 # Extract a spi3d LUT
444 str(resolution * resolution),
449 lut_extract = Process(description='extract a 3d LUT',
452 lut_extract.execute()
454 # Convert to a different format
460 lut_convert = Process(description='convert a 3d LUT',
463 lut_convert.execute()
466 def apply_CTL_to_image(input_image,
472 aces_ctl_directory=None):
479 Parameter description.
484 Return value description.
487 if ctl_paths is None:
489 if global_params is None:
492 if len(ctl_paths) > 0:
494 if aces_ctl_directory is not None:
495 if os.path.split(aces_ctl_directory)[1] != 'utilities':
496 ctl_module_path = os.path.join(aces_ctl_directory, 'utilities')
498 ctl_module_path = aces_ctl_directory
499 ctlenv['CTL_MODULE_PATH'] = ctl_module_path
502 for ctl in ctl_paths:
503 args += ['-ctl', ctl]
505 args += ['-input_scale', str(input_scale)]
506 args += ['-output_scale', str(output_scale)]
507 args += ['-global_param1', 'aIn', '1.0']
508 for key, value in global_params.iteritems():
509 args += ['-global_param1', key, str(value)]
510 args += [input_image]
511 args += [output_image]
513 ctlp = Process(description='a ctlrender process',
515 args=args, env=ctlenv)
520 def convert_bit_depth(input_image, output_image, depth):
527 Parameter description.
532 Return value description.
540 convert = Process(description='convert image bit depth',
546 def generate_1d_LUT_from_CTL(lut_path,
549 identity_lut_bit_depth='half',
554 aces_ctl_directory=None,
565 Parameter description.
570 Return value description.
573 if global_params is None:
576 lut_path_base = os.path.splitext(lut_path)[0]
578 identity_lut_image_float = '%s.%s.%s' % (lut_path_base, 'float', 'tiff')
579 generate_1d_LUT_image(identity_lut_image_float,
584 if identity_lut_bit_depth not in ['half', 'float']:
585 identity_lut_image = '%s.%s.%s' % (lut_path_base, 'uint16', 'tiff')
586 convert_bit_depth(identity_lut_image_float,
588 identity_lut_bit_depth)
590 identity_lut_image = identity_lut_image_float
592 transformed_lut_image = '%s.%s.%s' % (lut_path_base, 'transformed', 'exr')
593 apply_CTL_to_image(identity_lut_image,
594 transformed_lut_image,
601 generate_1d_LUT_from_image(transformed_lut_image,
609 os.remove(identity_lut_image)
610 if identity_lut_image != identity_lut_image_float:
611 os.remove(identity_lut_image_float)
612 os.remove(transformed_lut_image)
615 def correct_LUT_image(transformed_lut_image,
624 Parameter description.
629 Return value description.
632 transformed = oiio.ImageInput.open(transformed_lut_image)
634 transformed_spec = transformed.spec()
635 width = transformed_spec.width
636 height = transformed_spec.height
637 channels = transformed_spec.nchannels
639 if width != lut_resolution * lut_resolution or height != lut_resolution:
640 print(('Correcting image as resolution is off. '
641 'Found %d x %d. Expected %d x %d') % (
644 lut_resolution * lut_resolution,
646 print('Generating %s' % corrected_lut_image)
648 # Forcibly read data as float, the Python API doesn't handle half-float
651 source_data = transformed.read_image(type)
653 correct = oiio.ImageOutput.create(corrected_lut_image)
655 correct_spec = oiio.ImageSpec()
656 correct_spec.set_format(oiio.FLOAT)
657 correct_spec.width = height
658 correct_spec.height = width
659 correct_spec.nchannels = channels
661 correct.open(corrected_lut_image, correct_spec, oiio.Create)
663 dest_data = array.array('f',
664 ('\0' * correct_spec.width *
665 correct_spec.height *
666 correct_spec.nchannels * 4))
667 for j in range(0, correct_spec.height):
668 for i in range(0, correct_spec.width):
669 for c in range(0, correct_spec.nchannels):
670 dest_data[(correct_spec.nchannels *
671 correct_spec.width * j +
672 correct_spec.nchannels * i + c)] = (
673 source_data[correct_spec.nchannels *
674 correct_spec.width * j +
675 correct_spec.nchannels * i + c])
677 correct.write_image(correct_spec.format, dest_data)
680 # shutil.copy(transformedLUTImage, correctedLUTImage)
681 corrected_lut_image = transformed_lut_image
685 return corrected_lut_image
688 def generate_3d_LUT_from_CTL(lut_path,
691 identity_lut_bit_depth='half',
696 aces_ctl_directory=None,
704 Parameter description.
709 Return value description.
712 if global_params is None:
715 lut_path_base = os.path.splitext(lut_path)[0]
717 identity_lut_image_float = '%s.%s.%s' % (lut_path_base, 'float', 'tiff')
718 generate_3d_LUT_image(identity_lut_image_float, lut_resolution)
720 if identity_lut_bit_depth not in ['half', 'float']:
721 identity_lut_image = '%s.%s.%s' % (lut_path_base,
722 identity_lut_bit_depth,
724 convert_bit_depth(identity_lut_image_float,
726 identity_lut_bit_depth)
728 identity_lut_image = identity_lut_image_float
730 transformed_lut_image = '%s.%s.%s' % (lut_path_base, 'transformed', 'exr')
731 apply_CTL_to_image(identity_lut_image,
732 transformed_lut_image,
739 corrected_lut_image = '%s.%s.%s' % (lut_path_base, 'correct', 'exr')
740 corrected_lut_image = correct_LUT_image(transformed_lut_image,
744 generate_3d_LUT_from_image(corrected_lut_image,
750 os.remove(identity_lut_image)
751 if identity_lut_image != identity_lut_image_float:
752 os.remove(identity_lut_image_float)
753 os.remove(transformed_lut_image)
754 if corrected_lut_image != transformed_lut_image:
755 os.remove(corrected_lut_image)
756 if format != 'spi3d':
757 lut_path_spi3d = '%s.%s' % (lut_path, 'spi3d')
758 os.remove(lut_path_spi3d)
768 Parameter description.
773 Return value description.
778 p = optparse.OptionParser(
779 description='A utility to generate LUTs from CTL',
782 usage='%prog [options]')
784 p.add_option('--lut', '-l', type='string', default='')
785 p.add_option('--format', '-f', type='string', default='')
786 p.add_option('--ctl', '-c', type='string', action='append')
787 p.add_option('--lutResolution1d', '', type='int', default=1024)
788 p.add_option('--lutResolution3d', '', type='int', default=33)
789 p.add_option('--ctlReleasePath', '-r', type='string', default='')
790 p.add_option('--bitDepth', '-b', type='string', default='float')
791 p.add_option('--keepTempImages', '', action='store_true')
792 p.add_option('--minValue', '', type='float', default=0)
793 p.add_option('--maxValue', '', type='float', default=1)
794 p.add_option('--inputScale', '', type='float', default=1)
795 p.add_option('--outputScale', '', type='float', default=1)
796 p.add_option('--ctlRenderParam', '-p', type='string', nargs=2,
799 p.add_option('--generate1d', '', action='store_true')
800 p.add_option('--generate3d', '', action='store_true')
802 options, arguments = p.parse_args()
805 format = options.format
807 lut_resolution_1d = options.lutResolution1d
808 lut_resolution_3d = options.lutResolution3d
809 min_value = options.minValue
810 max_value = options.maxValue
811 input_scale = options.inputScale
812 output_scale = options.outputScale
813 ctl_release_path = options.ctlReleasePath
814 generate_1d = options.generate1d is True
815 generate_3d = options.generate3d is True
816 bit_depth = options.bitDepth
817 cleanup = not options.keepTempImages
820 if options.ctlRenderParam is not None:
821 for param in options.ctlRenderParam:
822 params[param[0]] = float(param[1])
825 print('1D LUT generation options')
827 print('3D LUT generation options')
829 print('lut : %s' % lut)
830 print('format : %s' % format)
831 print('ctls : %s' % ctls)
832 print('lut res 1d : %s' % lut_resolution_1d)
833 print('lut res 3d : %s' % lut_resolution_3d)
834 print('min value : %s' % min_value)
835 print('max value : %s' % max_value)
836 print('input scale : %s' % input_scale)
837 print('output scale : %s' % output_scale)
838 print('ctl render params : %s' % params)
839 print('ctl release path : %s' % ctl_release_path)
840 print('bit depth of input : %s' % bit_depth)
841 print('cleanup temp images : %s' % cleanup)
844 generate_1d_LUT_from_CTL(lut,
858 generate_3d_LUT_from_CTL(lut,
869 print(('\n\nNo LUT generated. '
870 'You must choose either 1D or 3D LUT generation\n\n'))
873 if __name__ == '__main__':