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