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