2 # -*- coding: utf-8 -*-
5 Defines objects to generate various kind of 1d, 2d and 3d LUTs in various file
13 import OpenImageIO as oiio
15 from aces_ocio.process import Process
17 __author__ = 'ACES Developers'
18 __copyright__ = 'Copyright (C) 2014 - 2015 - ACES Developers'
20 __maintainer__ = 'ACES Developers'
21 __email__ = 'aces@oscars.org'
22 __status__ = 'Production'
24 __all__ = ['generate1dLUTImage',
26 'generate1dLUTFromImage',
28 'generate3dLUTFromImage',
31 'generate1dLUTFromCTL',
33 'generate3dLUTFromCTL',
37 def generate1dLUTImage(ramp1dPath,
47 Parameter description.
52 Return value description.
55 # print("Generate 1d LUT image - %s" % ramp1dPath)
58 format = os.path.splitext(ramp1dPath)[1]
59 ramp = oiio.ImageOutput.create(ramp1dPath)
62 spec = oiio.ImageSpec()
63 spec.set_format(oiio.FLOAT)
64 # spec.format.basetype = oiio.FLOAT
65 spec.width = resolution
69 ramp.open(ramp1dPath, spec, oiio.Create)
71 data = array.array("f",
72 "\0" * spec.width * spec.height * spec.nchannels * 4)
73 for i in range(resolution):
74 value = float(i) / (resolution - 1) * (maxValue - minValue) + minValue
75 data[i * spec.nchannels + 0] = value
76 data[i * spec.nchannels + 1] = value
77 data[i * spec.nchannels + 2] = value
79 ramp.write_image(spec.format, data)
83 def writeSPI1D(filename, fromMin, fromMax, data, entries, channels):
87 Credit to *Alex Fry* for the original single channel version of the spi1d
93 Parameter description.
98 Return value description.
101 f = file(filename, 'w')
102 f.write("Version 1\n")
103 f.write("From %f %f\n" % (fromMin, fromMax))
104 f.write("Length %d\n" % entries)
105 f.write("Components %d\n" % (min(3, channels)))
107 for i in range(0, entries):
109 for j in range(0, min(3, channels)):
110 entry = "%s %s" % (entry, data[i * channels + j])
111 f.write(" %s\n" % entry)
116 def generate1dLUTFromImage(ramp1dPath,
126 Parameter description.
131 Return value description.
134 if outputPath == None:
135 outputPath = ramp1dPath + ".spi1d"
138 ramp = oiio.ImageInput.open(ramp1dPath)
142 type = spec.format.basetype
145 channels = spec.nchannels
148 # Force data to be read as float. The Python API doesn't handle
149 # half-floats well yet.
151 data = ramp.read_image(type)
153 writeSPI1D(outputPath, minValue, maxValue, data, width, channels)
156 def generate3dLUTImage(ramp3dPath, resolution=32):
163 Parameter description.
168 Return value description.
171 args = ["--generate",
175 str(resolution * resolution),
178 lutExtract = Process(description="generate a 3d LUT image",
184 def generate3dLUTFromImage(ramp3dPath, outputPath=None, resolution=32):
191 Parameter description.
196 Return value description.
199 if outputPath == None:
200 outputPath = ramp3dPath + ".spi3d"
206 str(resolution * resolution),
211 lutExtract = Process(description="extract a 3d LUT",
217 def applyCTLToImage(inputImage,
223 acesCTLReleaseDir=None):
230 Parameter description.
235 Return value description.
238 if len(ctlPaths) > 0:
240 if acesCTLReleaseDir != None:
241 if os.path.split(acesCTLReleaseDir)[1] != "utilities":
242 ctlModulePath = "%s/utilities" % acesCTLReleaseDir
244 ctlModulePath = acesCTLReleaseDir
245 ctlenv['CTL_MODULE_PATH'] = ctlModulePath
249 args += ['-ctl', ctl]
251 # args += ["-verbose"]
252 args += ["-input_scale", str(inputScale)]
253 args += ["-output_scale", str(outputScale)]
254 args += ["-global_param1", "aIn", "1.0"]
255 for key, value in globalParams.iteritems():
256 args += ["-global_param1", key, str(value)]
258 args += [outputImage]
260 # print("args : %s" % args)
262 ctlp = Process(description="a ctlrender process",
264 args=args, env=ctlenv)
269 def convertBitDepth(inputImage, outputImage, depth):
276 Parameter description.
281 Return value description.
289 convert = Process(description="convert image bit depth",
295 def generate1dLUTFromCTL(lutPath,
298 identityLutBitDepth='half',
303 acesCTLReleaseDir=None,
312 Parameter description.
317 Return value description.
323 lutPathBase = os.path.splitext(lutPath)[0]
325 identityLUTImageFloat = lutPathBase + ".float.tiff"
326 generate1dLUTImage(identityLUTImageFloat,
331 if identityLutBitDepth != 'half':
332 identityLUTImage = lutPathBase + ".uint16.tiff"
333 convertBitDepth(identityLUTImageFloat,
337 identityLUTImage = identityLUTImageFloat
339 transformedLUTImage = lutPathBase + ".transformed.exr"
340 applyCTLToImage(identityLUTImage,
348 generate1dLUTFromImage(transformedLUTImage, lutPath, minValue, maxValue)
351 os.remove(identityLUTImage)
352 if identityLUTImage != identityLUTImageFloat:
353 os.remove(identityLUTImageFloat)
354 os.remove(transformedLUTImage)
357 def correctLUTImage(transformedLUTImage, correctedLUTImage, lutResolution):
364 Parameter description.
369 Return value description.
373 transformed = oiio.ImageInput.open(transformedLUTImage)
376 transformedSpec = transformed.spec()
377 type = transformedSpec.format.basetype
378 width = transformedSpec.width
379 height = transformedSpec.height
380 channels = transformedSpec.nchannels
383 if width != lutResolution * lutResolution or height != lutResolution:
384 print(("Correcting image as resolution is off. "
385 "Found %d x %d. Expected %d x %d") % (
386 width, height, lutResolution * lutResolution, lutResolution))
387 print("Generating %s" % correctedLUTImage)
390 # We're going to generate a new correct image
393 # Get the source data
394 # Force data to be read as float. The Python API doesn't handle
395 # half-floats well yet.
397 sourceData = transformed.read_image(type)
399 format = os.path.splitext(correctedLUTImage)[1]
400 correct = oiio.ImageOutput.create(correctedLUTImage)
403 correctSpec = oiio.ImageSpec()
404 correctSpec.set_format(oiio.FLOAT)
405 correctSpec.width = height
406 correctSpec.height = width
407 correctSpec.nchannels = channels
409 correct.open(correctedLUTImage, correctSpec, oiio.Create)
411 destData = array.array("f",
412 ("\0" * correctSpec.width *
414 correctSpec.nchannels * 4))
415 for j in range(0, correctSpec.height):
416 for i in range(0, correctSpec.width):
417 for c in range(0, correctSpec.nchannels):
419 destData[(correctSpec.nchannels *
420 correctSpec.width * j +
421 correctSpec.nchannels * i + c)] = (
422 sourceData[correctSpec.nchannels *
423 correctSpec.width * j +
424 correctSpec.nchannels * i + c])
426 correct.write_image(correctSpec.format, destData)
429 # shutil.copy(transformedLUTImage, correctedLUTImage)
430 correctedLUTImage = transformedLUTImage
434 return correctedLUTImage
437 def generate3dLUTFromCTL(lutPath,
440 identityLutBitDepth='half',
445 acesCTLReleaseDir=None):
452 Parameter description.
457 Return value description.
463 lutPathBase = os.path.splitext(lutPath)[0]
465 identityLUTImageFloat = lutPathBase + ".float.tiff"
466 generate3dLUTImage(identityLUTImageFloat, lutResolution)
468 if identityLutBitDepth != 'half':
469 identityLUTImage = lutPathBase + "." + identityLutBitDepth + ".tiff"
470 convertBitDepth(identityLUTImageFloat,
474 identityLUTImage = identityLUTImageFloat
476 transformedLUTImage = lutPathBase + ".transformed.exr"
477 applyCTLToImage(identityLUTImage,
485 correctedLUTImage = lutPathBase + ".correct.exr"
486 correctedLUTImage = correctLUTImage(transformedLUTImage,
490 generate3dLUTFromImage(correctedLUTImage, lutPath, lutResolution)
493 os.remove(identityLUTImage)
494 if identityLUTImage != identityLUTImageFloat:
495 os.remove(identityLUTImageFloat)
496 os.remove(transformedLUTImage)
497 if correctedLUTImage != transformedLUTImage:
498 os.remove(correctedLUTImage)
499 # os.remove(correctedLUTImage)
509 Parameter description.
514 Return value description.
519 p = optparse.OptionParser(
520 description='A utility to generate LUTs from CTL',
523 usage='%prog [options]')
525 p.add_option('--lut', '-l', type="string", default="")
526 p.add_option('--ctl', '-c', type="string", action="append")
527 p.add_option('--lutResolution1d', '', type="int", default=1024)
528 p.add_option('--lutResolution3d', '', type="int", default=33)
529 p.add_option('--ctlReleasePath', '-r', type="string", default="")
530 p.add_option('--bitDepth', '-b', type="string", default="float")
531 p.add_option('--keepTempImages', '', action="store_true")
532 p.add_option('--minValue', '', type="float", default=0.0)
533 p.add_option('--maxValue', '', type="float", default=1.0)
534 p.add_option('--inputScale', '', type="float", default=1.0)
535 p.add_option('--outputScale', '', type="float", default=1.0)
536 p.add_option('--ctlRenderParam', '-p', type="string", nargs=2,
539 p.add_option('--generate1d', '', action="store_true")
540 p.add_option('--generate3d', '', action="store_true")
542 options, arguments = p.parse_args()
549 lutResolution1d = options.lutResolution1d
550 lutResolution3d = options.lutResolution3d
551 minValue = options.minValue
552 maxValue = options.maxValue
553 inputScale = options.inputScale
554 outputScale = options.outputScale
555 ctlReleasePath = options.ctlReleasePath
556 generate1d = options.generate1d == True
557 generate3d = options.generate3d == True
558 bitDepth = options.bitDepth
559 cleanup = not options.keepTempImages
562 if options.ctlRenderParam != None:
563 for param in options.ctlRenderParam:
564 params[param[0]] = float(param[1])
567 argsStart = sys.argv.index('--') + 1
568 args = sys.argv[argsStart:]
570 argsStart = len(sys.argv) + 1
573 # print("command line : \n%s\n" % " ".join(sys.argv))
579 print("1D LUT generation options")
581 print("3D LUT generation options")
583 print("lut : %s" % lut)
584 print("ctls : %s" % ctls)
585 print("lut res 1d : %s" % lutResolution1d)
586 print("lut res 3d : %s" % lutResolution3d)
587 print("min value : %s" % minValue)
588 print("max value : %s" % maxValue)
589 print("input scale : %s" % inputScale)
590 print("output scale : %s" % outputScale)
591 print("ctl render params : %s" % params)
592 print("ctl release path : %s" % ctlReleasePath)
593 print("bit depth of input : %s" % bitDepth)
594 print("cleanup temp images : %s" % cleanup)
597 generate1dLUTFromCTL(lut,
610 generate3dLUTFromCTL(lut,
620 print(("\n\nNo LUT generated. "
621 "You must choose either 1D or 3D LUT generation\n\n"))
625 if __name__ == '__main__':