Merge pull request #13 from selfshadow/patches-2
[OpenColorIO-Configs.git] / aces_1.0.1 / python / aces_ocio / generate_lut.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 """
5 Defines objects to generate various kind of 1D and 3D LUTs in various file
6 formats.
7 """
8
9 from __future__ import division
10
11 import array
12 import os
13
14 import OpenImageIO as oiio
15
16 from aces_ocio.process import Process
17
18 __author__ = 'ACES Developers'
19 __copyright__ = 'Copyright (C) 2014 - 2015 - ACES Developers'
20 __license__ = ''
21 __maintainer__ = 'ACES Developers'
22 __email__ = 'aces@oscars.org'
23 __status__ = 'Production'
24
25 __all__ = ['generate_1d_LUT_image',
26            'write_SPI_1d',
27            'write_CSP_1d',
28            'write_CTL_1d',
29            'write_1d',
30            'generate_1d_LUT_from_image',
31            'generate_3d_LUT_image',
32            'generate_3d_LUT_from_image',
33            'apply_CTL_to_image',
34            'convert_bit_depth',
35            'generate_1d_LUT_from_CTL',
36            'correct_LUT_image',
37            'generate_3d_LUT_from_CTL',
38            'main']
39
40
41 def generate_1d_LUT_image(ramp_1d_path,
42                           resolution=1024,
43                           min_value=0,
44                           max_value=1):
45     """
46     Generates a 1D LUT image, i.e. a simple ramp, going from the min_value to 
47     the max_value.
48
49     Parameters
50     ----------
51     ramp_1d_path : str or unicode
52         The path of the 1D ramp image to be written
53     resolution : int, optional
54         The resolution of the 1D ramp image to be written
55     min_value : float, optional
56         The lowest value in the 1D ramp
57     max_value : float, optional
58         The highest value in the 1D ramp
59
60     Returns
61     -------
62     None
63     """
64
65     ramp = oiio.ImageOutput.create(ramp_1d_path)
66
67     spec = oiio.ImageSpec()
68     spec.set_format(oiio.FLOAT)
69     # spec.format.basetype = oiio.FLOAT
70     spec.width = resolution
71     spec.height = 1
72     spec.nchannels = 3
73
74     ramp.open(ramp_1d_path, spec, oiio.Create)
75
76     data = array.array('f',
77                        '\0' * spec.width * spec.height * spec.nchannels * 4)
78     for i in range(resolution):
79         value = float(i) / (resolution - 1) * (
80             max_value - min_value) + min_value
81         data[i * spec.nchannels + 0] = value
82         data[i * spec.nchannels + 1] = value
83         data[i * spec.nchannels + 2] = value
84
85     ramp.write_image(spec.format, data)
86     ramp.close()
87
88
89 def write_SPI_1d(filename,
90                  from_min,
91                  from_max,
92                  data,
93                  entries,
94                  channels,
95                  components=3):
96     """
97     Writes a 1D LUT in the Sony Pictures Imageworks .spi1d format.
98
99     Credit to *Alex Fry* for the original single channel version of the spi1d
100     writer.
101
102     Parameters
103     ----------
104     filename : str or unicode
105         The path of the 1D LUT to be written
106     from_min : float
107         The lowest value in the 1D ramp
108     from_max : float
109         The highest value in the 1D ramp
110     data : array of floats
111         The entries in the LUT
112     entries : int
113         The resolution of the LUT, i.e. number of entries in the data set
114     channels : int
115         The number of channels in the data
116     components : int, optional
117         The number of channels in the data to actually write
118
119     Returns
120     -------
121     None
122     """
123
124     # May want to use fewer components than there are channels in the data
125     # Most commonly used for single channel LUTs
126     components = min(3, components, channels)
127
128     with open(filename, 'w') as fp:
129         fp.write('Version 1\n')
130         fp.write('From %f %f\n' % (from_min, from_max))
131         fp.write('Length %d\n' % entries)
132         fp.write('Components %d\n' % components)
133         fp.write('{\n')
134         for i in range(0, entries):
135             entry = ''
136             for j in range(0, components):
137                 entry = '%s %s' % (entry, data[i * channels + j])
138             fp.write('        %s\n' % entry)
139         fp.write('}\n')
140
141
142 def write_CSP_1d(filename,
143                  from_min,
144                  from_max,
145                  data,
146                  entries,
147                  channels,
148                  components=3):
149     """
150     Writes a 1D LUT in the Rising Sun Research Cinespace .csp format.
151
152     Parameters
153     ----------
154     filename : str or unicode
155         The path of the 1D LUT to be written
156     from_min : float
157         The lowest value in the 1D ramp
158     from_max : float
159         The highest value in the 1D ramp
160     data : array of floats
161         The entries in the LUT
162     entries : int
163         The resolution of the LUT, i.e. number of entries in the data set
164     channels : int
165         The number of channels in the data
166     components : int, optional
167         The number of channels in the data to actually write
168
169     Returns
170     -------
171     None
172     """
173
174     # May want to use fewer components than there are channels in the data
175     # Most commonly used for single channel LUTs
176     components = min(3, components, channels)
177
178     with open(filename, 'w') as fp:
179         fp.write('CSPLUTV100\n')
180         fp.write('1D\n')
181         fp.write('\n')
182         fp.write('BEGIN METADATA\n')
183         fp.write('END METADATA\n')
184
185         fp.write('\n')
186
187         fp.write('2\n')
188         fp.write('%f %f\n' % (from_min, from_max))
189         fp.write('0.0 1.0\n')
190         fp.write('2\n')
191         fp.write('%f %f\n' % (from_min, from_max))
192         fp.write('0.0 1.0\n')
193         fp.write('2\n')
194         fp.write('%f %f\n' % (from_min, from_max))
195         fp.write('0.0 1.0\n')
196
197         fp.write('\n')
198
199         fp.write('%d\n' % entries)
200         if components == 1:
201             for i in range(0, entries):
202                 entry = ''
203                 for j in range(3):
204                     entry = '%s %s' % (entry, data[i * channels])
205                 fp.write('%s\n' % entry)
206         else:
207             for i in range(entries):
208                 entry = ''
209                 for j in range(components):
210                     entry = '%s %s' % (entry, data[i * channels + j])
211                 fp.write('%s\n' % entry)
212         fp.write('\n')
213
214
215 def write_CTL_1d(filename,
216                  from_min,
217                  from_max,
218                  data,
219                  entries,
220                  channels,
221                  components=3):
222     """
223     Writes a 1D LUT in the Academy Color Transformation Language .ctl format.
224
225     Parameters
226     ----------
227     filename : str or unicode
228         The path of the 1D LUT to be written
229     from_min : float
230         The lowest value in the 1D ramp
231     from_max : float
232         The highest value in the 1D ramp
233     data : array of floats
234         The entries in the LUT
235     entries : int
236         The resolution of the LUT, i.e. number of entries in the data set
237     channels : int
238         The number of channels in the data
239     components : int, optional
240         The number of channels in the data to actually write
241
242     Returns
243     -------
244     None
245     """
246
247     # May want to use fewer components than there are channels in the data
248     # Most commonly used for single channel LUTs
249     components = min(3, components, channels)
250
251     with open(filename, 'w') as fp:
252         fp.write('// %d x %d LUT generated by "generate_lut"\n' % (
253             entries, components))
254         fp.write('\n')
255         fp.write('const float min1d = %3.9f;\n' % from_min)
256         fp.write('const float max1d = %3.9f;\n' % from_max)
257         fp.write('\n')
258
259         # Write LUT
260         if components == 1:
261             fp.write('const float lut[] = {\n')
262             for i in range(0, entries):
263                 fp.write('%s' % data[i * channels])
264                 if i != (entries - 1):
265                     fp.write(',')
266                 fp.write('\n')
267             fp.write('};\n')
268             fp.write('\n')
269         else:
270             for j in range(components):
271                 fp.write('const float lut%d[] = {\n' % j)
272                 for i in range(0, entries):
273                     fp.write('%s' % data[i * channels])
274                     if i != (entries - 1):
275                         fp.write(',')
276                     fp.write('\n')
277                 fp.write('};\n')
278                 fp.write('\n')
279
280         fp.write('void main\n')
281         fp.write('(\n')
282         fp.write('  input varying float rIn,\n')
283         fp.write('  input varying float gIn,\n')
284         fp.write('  input varying float bIn,\n')
285         fp.write('  input varying float aIn,\n')
286         fp.write('  output varying float rOut,\n')
287         fp.write('  output varying float gOut,\n')
288         fp.write('  output varying float bOut,\n')
289         fp.write('  output varying float aOut\n')
290         fp.write(')\n')
291         fp.write('{\n')
292         fp.write('  float r = rIn;\n')
293         fp.write('  float g = gIn;\n')
294         fp.write('  float b = bIn;\n')
295         fp.write('\n')
296         fp.write('  // Apply LUT\n')
297         if components == 1:
298             fp.write('  r = lookup1D(lut, min1d, max1d, r);\n')
299             fp.write('  g = lookup1D(lut, min1d, max1d, g);\n')
300             fp.write('  b = lookup1D(lut, min1d, max1d, b);\n')
301         elif components == 3:
302             fp.write('  r = lookup1D(lut0, min1d, max1d, r);\n')
303             fp.write('  g = lookup1D(lut1, min1d, max1d, g);\n')
304             fp.write('  b = lookup1D(lut2, min1d, max1d, b);\n')
305         fp.write('\n')
306         fp.write('  rOut = r;\n')
307         fp.write('  gOut = g;\n')
308         fp.write('  bOut = b;\n')
309         fp.write('  aOut = aIn;\n')
310         fp.write('}\n')
311
312
313 def write_1d(filename,
314              from_min,
315              from_max,
316              data,
317              data_entries,
318              data_channels,
319              lut_components=3,
320              format='spi1d'):
321     """
322     Writes a 1D LUT in the specified format.
323
324     Parameters
325     ----------
326     filename : str or unicode
327         The path of the 1D LUT to be written
328     from_min : float
329         The lowest value in the 1D ramp
330     from_max : float
331         The highest value in the 1D ramp
332     data : array of floats
333         The entries in the LUT
334     data_entries : int
335         The resolution of the LUT, i.e. number of entries in the data set
336     data_channels : int
337         The number of channels in the data
338     lut_components : int, optional
339         The number of channels in the data to actually use when writing
340     format : str or unicode, optional
341         The format of the the 1D LUT that will be written
342
343     Returns
344     -------
345     None
346     """
347
348     ocio_formats_to_extensions = {'cinespace': 'csp',
349                                   'flame': '3dl',
350                                   'icc': 'icc',
351                                   'houdini': 'lut',
352                                   'lustre': '3dl',
353                                   'ctl': 'ctl'}
354
355     if format in ocio_formats_to_extensions:
356         if ocio_formats_to_extensions[format] == 'csp':
357             write_CSP_1d(filename,
358                          from_min,
359                          from_max,
360                          data,
361                          data_entries,
362                          data_channels,
363                          lut_components)
364         elif ocio_formats_to_extensions[format] == 'ctl':
365             write_CTL_1d(filename,
366                          from_min,
367                          from_max,
368                          data,
369                          data_entries,
370                          data_channels,
371                          lut_components)
372     else:
373         write_SPI_1d(filename,
374                      from_min,
375                      from_max,
376                      data,
377                      data_entries,
378                      data_channels,
379                      lut_components)
380
381
382 def generate_1d_LUT_from_image(ramp_1d_path,
383                                output_path=None,
384                                min_value=0,
385                                max_value=1,
386                                channels=3,
387                                format='spi1d'):
388     """
389     Reads a 1D LUT image and writes a 1D LUT in the specified format.
390
391     Parameters
392     ----------
393     ramp_1d_path : str or unicode
394         The path of the 1D ramp image to be read
395     output_path : str or unicode, optional
396         The path of the 1D LUT to be written
397     min_value : float, optional
398         The lowest value in the 1D ramp
399     max_value : float, optional
400         The highest value in the 1D ramp
401     channels : int, optional
402         The number of channels in the data
403     format : str or unicode, optional
404         The format of the the 1D LUT that will be written
405
406     Returns
407     -------
408     None
409     """
410
411     if output_path is None:
412         output_path = '%s.%s' % (ramp_1d_path, 'spi1d')
413
414     ramp = oiio.ImageInput.open(ramp_1d_path)
415
416     ramp_spec = ramp.spec()
417     ramp_width = ramp_spec.width
418     ramp_channels = ramp_spec.nchannels
419
420     # Forcibly read data as float, the Python API doesn't handle half-float
421     # well yet.
422     type = oiio.FLOAT
423     ramp_data = ramp.read_image(type)
424
425     write_1d(output_path, min_value, max_value,
426              ramp_data, ramp_width, ramp_channels, channels, format)
427
428
429 def generate_3d_LUT_image(ramp_3d_path, resolution=32):
430     """
431     Generates a 3D LUT image covering the specified resolution
432     Relies on OCIO's ociolutimage command
433
434     Parameters
435     ----------
436     ramp_3d_path : str or unicode
437         The path of the 3D ramp image to be written
438     resolution : int, optional
439         The resolution of the 3D ramp image to be written
440
441     Returns
442     -------
443     None
444     """
445
446     args = ['--generate',
447             '--cubesize',
448             str(resolution),
449             '--maxwidth',
450             str(resolution * resolution),
451             '--output',
452             ramp_3d_path]
453     lut_extract = Process(description='generate a 3d LUT image',
454                           cmd='ociolutimage',
455                           args=args)
456     lut_extract.execute()
457
458
459 def generate_3d_LUT_from_image(ramp_3d_path,
460                                output_path=None,
461                                resolution=32,
462                                format='spi3d'):
463     """
464     Reads a 3D LUT image and writes a 3D LUT in the specified format.
465     Relies on OCIO's ociolutimage command
466
467     Parameters
468     ----------
469     ramp_3d_path : str or unicode
470         The path of the 3D ramp image to be read
471     output_path : str or unicode, optional
472         The path of the 1D LUT to be written
473     resolution : int, optional
474         The resolution of the 3D LUT represented in the image
475     format : str or unicode, optional
476         The format of the the 3D LUT that will be written
477
478     Returns
479     -------
480     None
481     """
482
483     if output_path is None:
484         output_path = '%s.%s' % (ramp_3d_path, 'spi3d')
485
486     ocio_formats_to_extensions = {'cinespace': 'csp',
487                                   'flame': '3dl',
488                                   'icc': 'icc',
489                                   'houdini': 'lut',
490                                   'lustre': '3dl'}
491
492     if format == 'spi3d' or not (format in ocio_formats_to_extensions):
493         # Extract a spi3d LUT
494         args = ['--extract',
495                 '--cubesize',
496                 str(resolution),
497                 '--maxwidth',
498                 str(resolution * resolution),
499                 '--input',
500                 ramp_3d_path,
501                 '--output',
502                 output_path]
503         lut_extract = Process(description='extract a 3d LUT',
504                               cmd='ociolutimage',
505                               args=args)
506         lut_extract.execute()
507
508     else:
509         output_path_spi3d = '%s.%s' % (output_path, 'spi3d')
510
511         # Extract a spi3d LUT
512         args = ['--extract',
513                 '--cubesize',
514                 str(resolution),
515                 '--maxwidth',
516                 str(resolution * resolution),
517                 '--input',
518                 ramp_3d_path,
519                 '--output',
520                 output_path_spi3d]
521         lut_extract = Process(description='extract a 3d LUT',
522                               cmd='ociolutimage',
523                               args=args)
524         lut_extract.execute()
525
526         # Convert to a different format
527         args = ['--lut',
528                 output_path_spi3d,
529                 '--format',
530                 format,
531                 output_path]
532         lut_convert = Process(description='convert a 3d LUT',
533                               cmd='ociobakelut',
534                               args=args)
535         lut_convert.execute()
536
537
538 def apply_CTL_to_image(input_image,
539                        output_image,
540                        ctl_paths=None,
541                        input_scale=1,
542                        output_scale=1,
543                        global_params=None,
544                        aces_ctl_directory=None):
545     """
546     Applies a set of Academy Color Transformation Language .ctl files to 
547     an input image and writes a new image.
548     Relies on the ACES ctlrender command
549
550     Parameters
551     ----------
552     input_image : str or unicode
553         The path to the image to transform using the CTL files
554     output_image : str or unicode
555         The path to write the result of the transforms
556     ctl_paths : array of str or unicode, optional
557         The path to write the result of the transforms
558     input_scale : float, optional
559         The argument to the ctlrender -input_scale parameter
560         For images with integer bit depths, this divides image code values 
561         before they are sent to the ctl commands
562         For images with float or half bit depths, this multiplies image code 
563         values before they are sent to the ctl commands
564     output_scale : float, optional
565         The argument to the ctlrender -output_scale parameter
566         For images with integer bit depths, this multiplies image code values 
567         before they are written to a file.
568         For images with float or half bit depths, this divides image code values 
569         before they are sent to the ctl commands
570     global_params : dict of key value pairs, optional
571         The set of parameter names and values to pass to the ctlrender 
572         -global_param1 parameter
573     aces_ctl_directory : str or unicode, optional
574         The path to the aces 'transforms/ctl/utilities'
575
576     Returns
577     -------
578     None
579     """
580
581     if ctl_paths is None:
582         ctl_paths = []
583     if global_params is None:
584         global_params = {}
585
586     if len(ctl_paths) > 0:
587         ctlenv = os.environ
588
589         if "/usr/local/bin" not in ctlenv['PATH'].split(':'):
590             ctlenv['PATH'] = "%s:/usr/local/bin" % ctlenv['PATH']
591
592         if aces_ctl_directory is not None:
593             if os.path.split(aces_ctl_directory)[1] != 'utilities':
594                 ctl_module_path = os.path.join(aces_ctl_directory, 'utilities')
595             else:
596                 ctl_module_path = aces_ctl_directory
597             ctlenv['CTL_MODULE_PATH'] = ctl_module_path
598
599         args = []
600         for ctl in ctl_paths:
601             args += ['-ctl', ctl]
602         args += ['-force']
603         args += ['-input_scale', str(input_scale)]
604         args += ['-output_scale', str(output_scale)]
605         args += ['-global_param1', 'aIn', '1.0']
606         for key, value in global_params.iteritems():
607             args += ['-global_param1', key, str(value)]
608         args += [input_image]
609         args += [output_image]
610
611         ctlp = Process(description='a ctlrender process',
612                        cmd='ctlrender',
613                        args=args, env=ctlenv)
614
615         ctlp.execute()
616
617
618 def convert_bit_depth(input_image, output_image, depth):
619     """
620     Convert the input image to the specified bit depth and write a new image
621     Relies on the OIIO oiiotool command
622
623     Parameters
624     ----------
625     input_image : str or unicode
626         The path to the image to transform using the CTL files
627     output_image : str or unicode
628         The path to write the result of the transforms
629     depth : str or unicode
630         The bit depth of the output image
631         Data types include: uint8, sint8, uint10, uint12, uint16, sint16, half, float, double
632
633     Returns
634     -------
635     None
636     """
637
638     args = [input_image,
639             '-d',
640             depth,
641             '-o',
642             output_image]
643     convert = Process(description='convert image bit depth',
644                       cmd='oiiotool',
645                       args=args)
646     convert.execute()
647
648
649 def generate_1d_LUT_from_CTL(lut_path,
650                              ctl_paths,
651                              lut_resolution=1024,
652                              identity_lut_bit_depth='half',
653                              input_scale=1,
654                              output_scale=1,
655                              global_params=None,
656                              cleanup=True,
657                              aces_ctl_directory=None,
658                              min_value=0,
659                              max_value=1,
660                              channels=3,
661                              format='spi1d'):
662     """
663     Creates a 1D LUT from the specified CTL files by creating a 1D LUT image,
664     applying the CTL files and then extracting and writing a LUT based on the
665     resulting image
666
667     Parameters
668     ----------
669     lut_path : str or unicode
670         The path to write the 1D LUT
671     ctl_paths : array of str or unicode
672         The CTL files to apply
673     lut_resolution : int, optional
674         The resolution of the 1D LUT to generate
675     identity_lut_bit_depth : string, optional
676         The bit depth to use for the intermediate 1D LUT image
677     input_scale : float, optional
678         The argument to the ctlrender -input_scale parameter
679         For images with integer bit depths, this divides image code values 
680         before they are sent to the ctl commands
681         For images with float or half bit depths, this multiplies image code 
682         values before they are sent to the ctl commands
683     output_scale : float, optional
684         The argument to the ctlrender -output_scale parameter
685         For images with integer bit depths, this multiplies image code values 
686         before they are written to a file.
687         For images with float or half bit depths, this divides image code values 
688         before they are sent to the ctl commands
689     global_params : dict of key, value pairs, optional
690         The set of parameter names and values to pass to the ctlrender 
691         -global_param1 parameter
692     cleanup : bool, optional
693         Whether or not to clean up the intermediate images 
694     aces_ctl_directory : str or unicode, optional
695         The path to the aces 'transforms/ctl/utilities'
696     min_value : float, optional
697         The minimum value to consider as input to the LUT
698     max_value : float, optional
699         The maximum value to consider as input to the LUT
700     channels : int, optional
701         The number of channels to use for the LUT. 1 or 3 are valid.
702     format : str or unicode, optional
703         The format to use when writing the LUT
704
705     Returns
706     -------
707     None
708     """
709
710     if global_params is None:
711         global_params = {}
712
713     lut_path_base = os.path.splitext(lut_path)[0]
714
715     identity_lut_image_float = '%s.%s.%s' % (lut_path_base, 'float', 'tiff')
716     generate_1d_LUT_image(identity_lut_image_float,
717                           lut_resolution,
718                           min_value,
719                           max_value)
720
721     if identity_lut_bit_depth not in ['half', 'float']:
722         identity_lut_image = '%s.%s.%s' % (lut_path_base, 'uint16', 'tiff')
723         convert_bit_depth(identity_lut_image_float,
724                           identity_lut_image,
725                           identity_lut_bit_depth)
726     else:
727         identity_lut_image = identity_lut_image_float
728
729     transformed_lut_image = '%s.%s.%s' % (lut_path_base, 'transformed', 'exr')
730     apply_CTL_to_image(identity_lut_image,
731                        transformed_lut_image,
732                        ctl_paths,
733                        input_scale,
734                        output_scale,
735                        global_params,
736                        aces_ctl_directory)
737
738     generate_1d_LUT_from_image(transformed_lut_image,
739                                lut_path,
740                                min_value,
741                                max_value,
742                                channels,
743                                format)
744
745     if cleanup:
746         os.remove(identity_lut_image)
747         if identity_lut_image != identity_lut_image_float:
748             os.remove(identity_lut_image_float)
749         os.remove(transformed_lut_image)
750
751
752 def correct_LUT_image(transformed_lut_image,
753                       corrected_lut_image,
754                       lut_resolution):
755     """
756     For some combinations of resolution and bit depth, ctlrender would generate
757     images with the right number of pixels but with the values for width and 
758     height transposed. This function generating a new, corrected image based on
759     the original. The function acts as a pass through if the problem is not
760     detected.
761
762     Parameters
763     ----------
764     transformed_lut_image : str or unicode
765         The path to an image generated by cltrender
766     corrected_lut_image : str or unicode
767         The path to an 'corrected' image to be generated
768     lut_resolution : int
769         The resolution of the 3D LUT that should be contained in 
770         transformed_lut_image
771
772     Returns
773     -------
774     str or unicode
775         The path to the corrected image, or the original, if no correction was
776         needed.
777     """
778
779     transformed = oiio.ImageInput.open(transformed_lut_image)
780
781     transformed_spec = transformed.spec()
782     width = transformed_spec.width
783     height = transformed_spec.height
784     channels = transformed_spec.nchannels
785
786     if width != lut_resolution * lut_resolution or height != lut_resolution:
787         print(('Correcting image as resolution is off. '
788                'Found %d x %d. Expected %d x %d') % (
789                   width,
790                   height,
791                   lut_resolution * lut_resolution,
792                   lut_resolution))
793         print('Generating %s' % corrected_lut_image)
794
795         # Forcibly read data as float, the Python API doesn't handle half-float
796         # well yet.
797         type = oiio.FLOAT
798         source_data = transformed.read_image(type)
799
800         correct = oiio.ImageOutput.create(corrected_lut_image)
801
802         correct_spec = oiio.ImageSpec()
803         correct_spec.set_format(oiio.FLOAT)
804         correct_spec.width = height
805         correct_spec.height = width
806         correct_spec.nchannels = channels
807
808         correct.open(corrected_lut_image, correct_spec, oiio.Create)
809
810         dest_data = array.array('f',
811                                 ('\0' * correct_spec.width *
812                                  correct_spec.height *
813                                  correct_spec.nchannels * 4))
814         for j in range(0, correct_spec.height):
815             for i in range(0, correct_spec.width):
816                 for c in range(0, correct_spec.nchannels):
817                     dest_data[(correct_spec.nchannels *
818                                correct_spec.width * j +
819                                correct_spec.nchannels * i + c)] = (
820                         source_data[correct_spec.nchannels *
821                                     correct_spec.width * j +
822                                     correct_spec.nchannels * i + c])
823
824         correct.write_image(correct_spec.format, dest_data)
825         correct.close()
826     else:
827         # shutil.copy(transformedLUTImage, correctedLUTImage)
828         corrected_lut_image = transformed_lut_image
829
830     transformed.close()
831
832     return corrected_lut_image
833
834
835 def generate_3d_LUT_from_CTL(lut_path,
836                              ctl_paths,
837                              lut_resolution=64,
838                              identity_lut_bit_depth='half',
839                              input_scale=1,
840                              output_scale=1,
841                              global_params=None,
842                              cleanup=True,
843                              aces_ctl_directory=None,
844                              format='spi3d'):
845     """
846     Creates a 3D LUT from the specified CTL files by creating a 3D LUT image,
847     applying the CTL files and then extracting and writing a LUT based on the
848     resulting image
849
850     Parameters
851     ----------
852     lut_path : str or unicode
853         The path to write the 1D LUT
854     ctl_paths : array of str or unicode
855         The CTL files to apply
856     lut_resolution : int, optional
857         The resolution of the 1D LUT to generate
858     identity_lut_bit_depth : string, optional
859         The bit depth to use for the intermediate 1D LUT image
860     input_scale : float, optional
861         The argument to the ctlrender -input_scale parameter
862         For images with integer bit depths, this divides image code values 
863         before they are sent to the ctl commands
864         For images with float or half bit depths, this multiplies image code 
865         values before they are sent to the ctl commands
866     output_scale : float, optional
867         The argument to the ctlrender -output_scale parameter
868         For images with integer bit depths, this multiplies image code values 
869         before they are written to a file.
870         For images with float or half bit depths, this divides image code values 
871         before they are sent to the ctl commands
872     global_params : dict of key, value pairs, optional
873         The set of parameter names and values to pass to the ctlrender 
874         -global_param1 parameter
875     cleanup : bool, optional
876         Whether or not to clean up the intermediate images 
877     aces_ctl_directory : str or unicode, optional
878         The path to the aces 'transforms/ctl/utilities'
879     format : str or unicode, optional
880         The format to use when writing the LUT
881
882     Returns
883     -------
884     None
885     """
886
887     if global_params is None:
888         global_params = {}
889
890     lut_path_base = os.path.splitext(lut_path)[0]
891
892     identity_lut_image_float = '%s.%s.%s' % (lut_path_base, 'float', 'tiff')
893     generate_3d_LUT_image(identity_lut_image_float, lut_resolution)
894
895     if identity_lut_bit_depth not in ['half', 'float']:
896         identity_lut_image = '%s.%s.%s' % (lut_path_base,
897                                            identity_lut_bit_depth,
898                                            'tiff')
899         convert_bit_depth(identity_lut_image_float,
900                           identity_lut_image,
901                           identity_lut_bit_depth)
902     else:
903         identity_lut_image = identity_lut_image_float
904
905     transformed_lut_image = '%s.%s.%s' % (lut_path_base, 'transformed', 'exr')
906     apply_CTL_to_image(identity_lut_image,
907                        transformed_lut_image,
908                        ctl_paths,
909                        input_scale,
910                        output_scale,
911                        global_params,
912                        aces_ctl_directory)
913
914     corrected_lut_image = '%s.%s.%s' % (lut_path_base, 'correct', 'exr')
915     corrected_lut_image = correct_LUT_image(transformed_lut_image,
916                                             corrected_lut_image,
917                                             lut_resolution)
918
919     generate_3d_LUT_from_image(corrected_lut_image,
920                                lut_path,
921                                lut_resolution,
922                                format)
923
924     if cleanup:
925         os.remove(identity_lut_image)
926         if identity_lut_image != identity_lut_image_float:
927             os.remove(identity_lut_image_float)
928         os.remove(transformed_lut_image)
929         if corrected_lut_image != transformed_lut_image:
930             os.remove(corrected_lut_image)
931         if format != 'spi3d':
932             lut_path_spi3d = '%s.%s' % (lut_path, 'spi3d')
933             os.remove(lut_path_spi3d)
934
935
936 def main():
937     """
938     A simple main that allows the user to exercise the various functions
939     defined in this file
940
941     Parameters
942     ----------
943     None
944
945     Returns
946     -------
947     None
948     """
949
950     import optparse
951
952     p = optparse.OptionParser(
953         description='A utility to generate LUTs from CTL',
954         prog='generateLUT',
955         version='0.01',
956         usage='%prog [options]')
957
958     p.add_option('--lut', '-l', type='string', default='')
959     p.add_option('--format', '-f', type='string', default='')
960     p.add_option('--ctl', '-c', type='string', action='append')
961     p.add_option('--lutResolution1d', '', type='int', default=4096)
962     p.add_option('--lutResolution3d', '', type='int', default=65)
963     p.add_option('--ctlReleasePath', '-r', type='string', default='')
964     p.add_option('--bitDepth', '-b', type='string', default='float')
965     p.add_option('--keepTempImages', '', action='store_true')
966     p.add_option('--minValue', '', type='float', default=0)
967     p.add_option('--maxValue', '', type='float', default=1)
968     p.add_option('--inputScale', '', type='float', default=1)
969     p.add_option('--outputScale', '', type='float', default=1)
970     p.add_option('--ctlRenderParam', '-p', type='string', nargs=2,
971                  action='append')
972
973     p.add_option('--generate1d', '', action='store_true')
974     p.add_option('--generate3d', '', action='store_true')
975
976     options, arguments = p.parse_args()
977
978     lut = options.lut
979     format = options.format
980     ctls = options.ctl
981     lut_resolution_1d = options.lutResolution1d
982     lut_resolution_3d = options.lutResolution3d
983     min_value = options.minValue
984     max_value = options.maxValue
985     input_scale = options.inputScale
986     output_scale = options.outputScale
987     ctl_release_path = options.ctlReleasePath
988     generate_1d = options.generate1d is True
989     generate_3d = options.generate3d is True
990     bit_depth = options.bitDepth
991     cleanup = not options.keepTempImages
992
993     params = {}
994     if options.ctlRenderParam is not None:
995         for param in options.ctlRenderParam:
996             params[param[0]] = float(param[1])
997
998     if generate_1d:
999         print('1D LUT generation options')
1000     else:
1001         print('3D LUT generation options')
1002
1003     print('LUT                 : %s' % lut)
1004     print('Format              : %s' % format)
1005     print('CTLs                : %s' % ctls)
1006     print('LUT Res 1d          : %s' % lut_resolution_1d)
1007     print('LUT Res 3d          : %s' % lut_resolution_3d)
1008     print('Min Value           : %s' % min_value)
1009     print('Max Value           : %s' % max_value)
1010     print('Input Scale         : %s' % input_scale)
1011     print('Output Scale        : %s' % output_scale)
1012     print('CTL Render Params   : %s' % params)
1013     print('CTL Release Path    : %s' % ctl_release_path)
1014     print('Input Bit Depth     : %s' % bit_depth)
1015     print('Cleanup Temp Images : %s' % cleanup)
1016
1017     if generate_1d:
1018         generate_1d_LUT_from_CTL(lut,
1019                                  ctls,
1020                                  lut_resolution_1d,
1021                                  bit_depth,
1022                                  input_scale,
1023                                  output_scale,
1024                                  params,
1025                                  cleanup,
1026                                  ctl_release_path,
1027                                  min_value,
1028                                  max_value,
1029                                  format=format)
1030
1031     elif generate_3d:
1032         generate_3d_LUT_from_CTL(lut,
1033                                  ctls,
1034                                  lut_resolution_3d,
1035                                  bit_depth,
1036                                  input_scale,
1037                                  output_scale,
1038                                  params,
1039                                  cleanup,
1040                                  ctl_release_path,
1041                                  format=format)
1042     else:
1043         print(('\n\nNo LUT generated! '
1044                'You must choose either 1D or 3D LUT generation\n\n'))
1045
1046
1047 if __name__ == '__main__':
1048     main()