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