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