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