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