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