Roles now have some assignments
[OpenColorIO-Configs.git] / aces_1.0.0 / 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, 2d and 3d LUTs in various file
6 formats.
7 """
8
9 from __future__ import division
10
11 import array
12 import os
13 import sys
14
15 import OpenImageIO as oiio
16
17 from aces_ocio.process import Process
18
19 __author__ = 'ACES Developers'
20 __copyright__ = 'Copyright (C) 2014 - 2015 - ACES Developers'
21 __license__ = ''
22 __maintainer__ = 'ACES Developers'
23 __email__ = 'aces@oscars.org'
24 __status__ = 'Production'
25
26 __all__ = ['generate_1d_LUT_image',
27            'write_SPI_1d',
28            'generate_1d_LUT_from_image',
29            'generate_3d_LUT_image',
30            'generate_3d_LUT_from_image',
31            'apply_CTL_to_image',
32            'convert_bit_depth',
33            'generate_1d_LUT_from_CTL',
34            'correct_LUT_image',
35            'generate_3d_LUT_from_CTL',
36            'main']
37
38
39 def generate_1d_LUT_image(ramp_1d_path,
40                           resolution=1024,
41                           min_value=0,
42                           max_value=1):
43     """
44     Object description.
45
46     Parameters
47     ----------
48     parameter : type
49         Parameter description.
50
51     Returns
52     -------
53     type
54          Return value description.
55     """
56
57     ramp = oiio.ImageOutput.create(ramp_1d_path)
58
59     spec = oiio.ImageSpec()
60     spec.set_format(oiio.FLOAT)
61     # spec.format.basetype = oiio.FLOAT
62     spec.width = resolution
63     spec.height = 1
64     spec.nchannels = 3
65
66     ramp.open(ramp_1d_path, spec, oiio.Create)
67
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
76
77     ramp.write_image(spec.format, data)
78     ramp.close()
79
80
81 def write_SPI_1d(filename, 
82                  from_min, 
83                  from_max, 
84                  data, 
85                  entries, 
86                  channels, 
87                  components=3):
88     """
89     Object description.
90
91     Credit to *Alex Fry* for the original single channel version of the spi1d
92     writer.
93
94     Parameters
95     ----------
96     parameter : type
97         Parameter description.
98
99     Returns
100     -------
101     type
102          Return value description.
103     """
104
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)
108
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)
114         fp.write('{\n')
115         for i in range(0, entries):
116             entry = ''
117             for j in range(0, components):
118                 entry = '%s %s' % (entry, data[i * channels + j])
119             fp.write('        %s\n' % entry)
120         fp.write('}\n')
121
122
123 def generate_1d_LUT_from_image(ramp_1d_path,
124                                output_path=None,
125                                min_value=0,
126                                max_value=1,
127                                channels=3):
128     """
129     Object description.
130
131     Parameters
132     ----------
133     parameter : type
134         Parameter description.
135
136     Returns
137     -------
138     type
139          Return value description.
140     """
141
142     if output_path is None:
143         output_path = '%s.%s' % (ramp_1d_path, 'spi1d')
144
145     ramp = oiio.ImageInput.open(ramp_1d_path)
146
147     ramp_spec = ramp.spec()
148     ramp_width = ramp_spec.width
149     ramp_channels = ramp_spec.nchannels
150
151     # Forcibly read data as float, the Python API doesn't handle half-float
152     # well yet.
153     type = oiio.FLOAT
154     ramp_data = ramp.read_image(type)
155
156     write_SPI_1d(output_path, min_value, max_value, 
157       ramp_data, ramp_width, ramp_channels, channels)
158
159
160 def generate_3d_LUT_image(ramp_3d_path, resolution=32):
161     """
162     Object description.
163
164     Parameters
165     ----------
166     parameter : type
167         Parameter description.
168
169     Returns
170     -------
171     type
172          Return value description.
173     """
174
175     args = ['--generate',
176             '--cubesize',
177             str(resolution),
178             '--maxwidth',
179             str(resolution * resolution),
180             '--output',
181             ramp_3d_path]
182     lut_extract = Process(description='generate a 3d LUT image',
183                           cmd='ociolutimage',
184                           args=args)
185     lut_extract.execute()
186
187
188 def generate_3d_LUT_from_image(ramp_3d_path, output_path=None, resolution=32):
189     """
190     Object description.
191
192     Parameters
193     ----------
194     parameter : type
195         Parameter description.
196
197     Returns
198     -------
199     type
200          Return value description.
201     """
202
203     if output_path is None:
204         output_path = '%s.%s' % (ramp_3d_path, 'spi1d')
205
206     args = ['--extract',
207             '--cubesize',
208             str(resolution),
209             '--maxwidth',
210             str(resolution * resolution),
211             '--input',
212             ramp_3d_path,
213             '--output',
214             output_path]
215     lut_extract = Process(description='extract a 3d LUT',
216                           cmd='ociolutimage',
217                           args=args)
218     lut_extract.execute()
219
220
221 def apply_CTL_to_image(input_image,
222                        output_image,
223                        ctl_paths=None,
224                        input_scale=1,
225                        output_scale=1,
226                        global_params=None,
227                        aces_ctl_directory=None):
228     """
229     Object description.
230
231     Parameters
232     ----------
233     parameter : type
234         Parameter description.
235
236     Returns
237     -------
238     type
239          Return value description.
240     """
241
242     if ctl_paths is None:
243         ctl_paths = []
244     if global_params is None:
245         global_params = {}
246
247     if len(ctl_paths) > 0:
248         ctlenv = os.environ
249         if aces_ctl_directory is not None:
250             if os.path.split(aces_ctl_directory)[1] != 'utilities':
251                 ctl_module_path = os.path.join(aces_ctl_directory, 'utilities')
252             else:
253                 ctl_module_path = aces_ctl_directory
254             ctlenv['CTL_MODULE_PATH'] = ctl_module_path
255
256         args = []
257         for ctl in ctl_paths:
258             args += ['-ctl', ctl]
259         args += ['-force']
260         args += ['-input_scale', str(input_scale)]
261         args += ['-output_scale', str(output_scale)]
262         args += ['-global_param1', 'aIn', '1.0']
263         for key, value in global_params.iteritems():
264             args += ['-global_param1', key, str(value)]
265         args += [input_image]
266         args += [output_image]
267
268         ctlp = Process(description='a ctlrender process',
269                        cmd='ctlrender',
270                        args=args, env=ctlenv)
271
272         ctlp.execute()
273
274
275 def convert_bit_depth(input_image, output_image, depth):
276     """
277     Object description.
278
279     Parameters
280     ----------
281     parameter : type
282         Parameter description.
283
284     Returns
285     -------
286     type
287          Return value description.
288     """
289
290     args = [input_image,
291             '-d',
292             depth,
293             '-o',
294             output_image]
295     convert = Process(description='convert image bit depth',
296                       cmd='oiiotool',
297                       args=args)
298     convert.execute()
299
300
301 def generate_1d_LUT_from_CTL(lut_path,
302                              ctl_paths,
303                              lut_resolution=1024,
304                              identity_LUT_bit_depth='half',
305                              input_scale=1,
306                              output_scale=1,
307                              global_params=None,
308                              cleanup=True,
309                              aces_ctl_directory=None,
310                              min_value=0,
311                              max_value=1,
312                              channels=3):
313     """
314     Object description.
315
316     Parameters
317     ----------
318     parameter : type
319         Parameter description.
320
321     Returns
322     -------
323     type
324          Return value description.
325     """
326
327     if global_params is None:
328         global_params = {}
329
330     lut_path_base = os.path.splitext(lut_path)[0]
331
332     identity_LUT_image_float = '%s.%s.%s' % (lut_path_base, 'float', 'tiff')
333     generate_1d_LUT_image(identity_LUT_image_float,
334                           lut_resolution,
335                           min_value,
336                           max_value)
337
338     if identity_LUT_bit_depth not in ['half', 'float']:
339         identity_LUT_image = '%s.%s.%s' % (lut_path_base, 'uint16', 'tiff')
340         convert_bit_depth(identity_LUT_image_float,
341                           identity_LUT_image,
342                           identity_LUT_bit_depth)
343     else:
344         identity_LUT_image = identity_LUT_image_float
345
346     transformed_LUT_image = '%s.%s.%s' % (lut_path_base, 'transformed', 'exr')
347     apply_CTL_to_image(identity_LUT_image,
348                        transformed_LUT_image,
349                        ctl_paths,
350                        input_scale,
351                        output_scale,
352                        global_params,
353                        aces_ctl_directory)
354
355     generate_1d_LUT_from_image(transformed_LUT_image,
356                                lut_path,
357                                min_value,
358                                max_value,
359                                channels)
360
361     if cleanup:
362         os.remove(identity_LUT_image)
363         if identity_LUT_image != identity_LUT_image_float:
364             os.remove(identity_LUT_image_float)
365         os.remove(transformed_LUT_image)
366
367
368 def correct_LUT_image(transformed_LUT_image,
369                       corrected_LUT_image,
370                       lut_resolution):
371     """
372     Object description.
373
374     Parameters
375     ----------
376     parameter : type
377         Parameter description.
378
379     Returns
380     -------
381     type
382          Return value description.
383     """
384
385     transformed = oiio.ImageInput.open(transformed_LUT_image)
386
387     transformed_spec = transformed.spec()
388     width = transformed_spec.width
389     height = transformed_spec.height
390     channels = transformed_spec.nchannels
391
392     if width != lut_resolution * lut_resolution or height != lut_resolution:
393         print(('Correcting image as resolution is off. '
394                'Found %d x %d. Expected %d x %d') % (
395                   width,
396                   height,
397                   lut_resolution * lut_resolution,
398                   lut_resolution))
399         print('Generating %s' % corrected_LUT_image)
400
401         # Forcibly read data as float, the Python API doesn't handle half-float
402         # well yet.
403         type = oiio.FLOAT
404         source_data = transformed.read_image(type)
405
406         correct = oiio.ImageOutput.create(corrected_LUT_image)
407
408         correct_spec = oiio.ImageSpec()
409         correct_spec.set_format(oiio.FLOAT)
410         correct_spec.width = height
411         correct_spec.height = width
412         correct_spec.nchannels = channels
413
414         correct.open(corrected_LUT_image, correct_spec, oiio.Create)
415
416         dest_data = array.array('f',
417                                 ('\0' * correct_spec.width *
418                                  correct_spec.height *
419                                  correct_spec.nchannels * 4))
420         for j in range(0, correct_spec.height):
421             for i in range(0, correct_spec.width):
422                 for c in range(0, correct_spec.nchannels):
423                     dest_data[(correct_spec.nchannels *
424                                correct_spec.width * j +
425                                correct_spec.nchannels * i + c)] = (
426                         source_data[correct_spec.nchannels *
427                                     correct_spec.width * j +
428                                     correct_spec.nchannels * i + c])
429
430         correct.write_image(correct_spec.format, dest_data)
431         correct.close()
432     else:
433         # shutil.copy(transformedLUTImage, correctedLUTImage)
434         corrected_LUT_image = transformed_LUT_image
435
436     transformed.close()
437
438     return corrected_LUT_image
439
440
441 def generate_3d_LUT_from_CTL(lut_path,
442                              ctl_paths,
443                              lut_resolution=64,
444                              identity_LUT_bit_depth='half',
445                              input_scale=1,
446                              output_scale=1,
447                              global_params=None,
448                              cleanup=True,
449                              aces_ctl_directory=None):
450     """
451     Object description.
452
453     Parameters
454     ----------
455     parameter : type
456         Parameter description.
457
458     Returns
459     -------
460     type
461          Return value description.
462     """
463
464     if global_params is None:
465         global_params = {}
466
467     lut_path_base = os.path.splitext(lut_path)[0]
468
469     identity_LUT_image_float = '%s.%s.%s' % (lut_path_base, 'float', 'tiff')
470     generate_3d_LUT_image(identity_LUT_image_float, lut_resolution)
471
472     if identity_LUT_bit_depth not in ['half', 'float']:
473         identity_LUT_image = '%s.%s.%s' % (lut_path_base,
474                                            identity_LUT_bit_depth,
475                                            'tiff')
476         convert_bit_depth(identity_LUT_image_float,
477                           identity_LUT_image,
478                           identity_LUT_bit_depth)
479     else:
480         identity_LUT_image = identity_LUT_image_float
481
482     transformed_LUT_image = '%s.%s.%s' % (lut_path_base, 'transformed', 'exr')
483     apply_CTL_to_image(identity_LUT_image,
484                        transformed_LUT_image,
485                        ctl_paths,
486                        input_scale,
487                        output_scale,
488                        global_params,
489                        aces_ctl_directory)
490
491     corrected_LUT_image = '%s.%s.%s' % (lut_path_base, 'correct', 'exr')
492     corrected_LUT_image = correct_LUT_image(transformed_LUT_image,
493                                             corrected_LUT_image,
494                                             lut_resolution)
495
496     generate_3d_LUT_from_image(corrected_LUT_image, lut_path, lut_resolution)
497
498     if cleanup:
499         os.remove(identity_LUT_image)
500         if identity_LUT_image != identity_LUT_image_float:
501             os.remove(identity_LUT_image_float)
502         os.remove(transformed_LUT_image)
503         if corrected_LUT_image != transformed_LUT_image:
504             os.remove(corrected_LUT_image)
505
506
507 def main():
508     """
509     Object description.
510
511     Parameters
512     ----------
513     parameter : type
514         Parameter description.
515
516     Returns
517     -------
518     type
519          Return value description.
520     """
521
522     import optparse
523
524     p = optparse.OptionParser(
525         description='A utility to generate LUTs from CTL',
526         prog='generateLUT',
527         version='0.01',
528         usage='%prog [options]')
529
530     p.add_option('--lut', '-l', type='string', default='')
531     p.add_option('--ctl', '-c', type='string', action='append')
532     p.add_option('--lutResolution1d', '', type='int', default=1024)
533     p.add_option('--lutResolution3d', '', type='int', default=33)
534     p.add_option('--ctlReleasePath', '-r', type='string', default='')
535     p.add_option('--bitDepth', '-b', type='string', default='float')
536     p.add_option('--keepTempImages', '', action='store_true')
537     p.add_option('--minValue', '', type='float', default=0)
538     p.add_option('--maxValue', '', type='float', default=1)
539     p.add_option('--inputScale', '', type='float', default=1)
540     p.add_option('--outputScale', '', type='float', default=1)
541     p.add_option('--ctlRenderParam', '-p', type='string', nargs=2,
542                  action='append')
543
544     p.add_option('--generate1d', '', action='store_true')
545     p.add_option('--generate3d', '', action='store_true')
546
547     options, arguments = p.parse_args()
548
549     lut = options.lut
550     ctls = options.ctl
551     lut_resolution_1d = options.lutResolution1d
552     lut_resolution_3d = options.lutResolution3d
553     min_value = options.minValue
554     max_value = options.maxValue
555     input_scale = options.inputScale
556     output_scale = options.outputScale
557     ctl_release_path = options.ctlReleasePath
558     generate_1d = options.generate1d is True
559     generate_3d = options.generate3d is True
560     bit_depth = options.bitDepth
561     cleanup = not options.keepTempImages
562
563     params = {}
564     if options.ctlRenderParam is not None:
565         for param in options.ctlRenderParam:
566             params[param[0]] = float(param[1])
567
568     try:
569         args_start = sys.argv.index('--') + 1
570         args = sys.argv[args_start:]
571     except:
572         args_start = len(sys.argv) + 1
573         args = []
574
575     if generate_1d:
576         print('1D LUT generation options')
577     else:
578         print('3D LUT generation options')
579
580     print('lut                 : %s' % lut)
581     print('ctls                : %s' % ctls)
582     print('lut res 1d          : %s' % lut_resolution_1d)
583     print('lut res 3d          : %s' % lut_resolution_3d)
584     print('min value           : %s' % min_value)
585     print('max value           : %s' % max_value)
586     print('input scale         : %s' % input_scale)
587     print('output scale        : %s' % output_scale)
588     print('ctl render params   : %s' % params)
589     print('ctl release path    : %s' % ctl_release_path)
590     print('bit depth of input  : %s' % bit_depth)
591     print('cleanup temp images : %s' % cleanup)
592
593     if generate_1d:
594         generate_1d_LUT_from_CTL(lut,
595                                  ctls,
596                                  lut_resolution_1d,
597                                  bit_depth,
598                                  input_scale,
599                                  output_scale,
600                                  params,
601                                  cleanup,
602                                  ctl_release_path,
603                                  min_value,
604                                  max_value)
605
606     elif generate_3d:
607         generate_3d_LUT_from_CTL(lut,
608                                  ctls,
609                                  lut_resolution_3d,
610                                  bit_depth,
611                                  input_scale,
612                                  output_scale,
613                                  params,
614                                  cleanup,
615                                  ctl_release_path)
616     else:
617         print(('\n\nNo LUT generated. '
618                'You must choose either 1D or 3D LUT generation\n\n'))
619
620
621 if __name__ == '__main__':
622     main()
623