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