Top

Module pylut.pylut

#MIT License
#Authored by Greg Cotten


import os
import math
import numpy as np
import kdtree
from progress.bar import Bar

def EmptyLatticeOfSize(cubeSize):
	return np.zeros((cubeSize, cubeSize, cubeSize), object)

def Indices01(cubeSize):
	indices = []
	ratio = 1.0/float(cubeSize-1)
	for i in xrange(cubeSize):
		indices.append(float(i) * ratio)
	return indices

def Indices(cubeSize, bitdepth):
	indices = []
	for i in Indices01(cubeSize):
		indices.append(i * (2**bitdepth - 1))
	return indices


def RemapIntTo01(val, maxVal):
	return (float(val)/float(maxVal))

def Remap01ToInt(val, bitdepth):
	return int(val * (2**bitdepth - 1))

def LerpColor(beginning, end, value01):
	if value01 < 0 or value01 > 1:
		raise NameError("Improper Lerp")
	return Color(Lerp1D(beginning.r, end.r, value01), Lerp1D(beginning.g, end.g, value01), Lerp1D(beginning.b, end.b, value01))

def Lerp3D(beginning, end, value01):
	if value01 < 0 or value01 > 1:
		raise NameError("Improper Lerp")
	return [Lerp1D(beginning[0], end[0], value01), Lerp1D(beginning[1], end[1], value01), Lerp1D(beginning[2], end[2], value01)]

def Lerp1D(beginning, end, value01):
	if value01 < 0 or value01 > 1:
		raise NameError("Improper Lerp")

	range = float(end) - float(beginning)
	return float(beginning) + float(range) * float(value01)

def Distance3D(a, b):
	return math.sqrt((a[0] - b[0])**2 + (a[1] - b[1])**2 + (a[2] - b[2])**2)

def Clamp(value, min, max):
	if min > max:
		raise NameError("Invalid Clamp Values")
	if value < min:
		return float(min)
	if value > max:
		return float(max)
	return value

class Color:
	"""
	RGB floating point representation of a color. 0 is absolute black, 1 is absolute white.
	Access channel data by color.r, color.g, or color.b. 
	"""
	def __init__(self, r, g, b):
		self.r = r
		self.g = g
		self.b = b

	def Clamped01(self):
		return Color(Clamp(float(self.r), 0, 1), Clamp(float(self.g), 0, 1), Clamp(float(self.b), 0, 1))

	@staticmethod
	def FromRGBInteger(r, g, b, bitdepth):
		"""
		Instantiates a floating point color from RGB integers at a bitdepth.
		"""
		maxBits = 2**bitdepth - 1
		return Color(RemapIntTo01(r, maxBits), RemapIntTo01(g, maxBits), RemapIntTo01(b, maxBits))

	@staticmethod
	def FromFloatArray(array):
		"""
		Creates Color from a list or tuple of 3 floats.
		"""
		return Color(array[0], array[1], array[2])

	@staticmethod
	def FromRGBIntegerArray(array, bitdepth):
		"""
		Creates Color from a list or tuple of 3 RGB integers at a specified bitdepth.
		"""
		maxBits = 2**bitdepth - 1
		return Color(RemapIntTo01(array[0], maxBits), RemapIntTo01(array[1], maxBits), RemapIntTo01(array[2], maxBits))

	
	def ToFloatArray(self):
		"""
		Creates a tuple of 3 floating point RGB values from the floating point color.
		"""
		return (self.r, self.g, self.b)

	def ToRGBIntegerArray(self, bitdepth):
		"""
		Creates a list of 3 RGB integer values at specified bitdepth from the floating point color.
		"""
		return (Remap01ToInt(self.r, bitdepth), Remap01ToInt(self.g, bitdepth), Remap01ToInt(self.b, bitdepth))

	def ClampColor(self, min, max):
		"""
		Returns a clamped color.
		"""
		return Color(Clamp(self.r, min.r, max.r), Clamp(self.g, min.g, max.g), Clamp(self.b, min.b, max.b))

	def DistanceToColor(color):
		if isinstance(color, Color):
			return Distance3D(self.ToFloatArray(), color.ToFloatArray())
		return NotImplemented
	
	def __add__(self, color):
		return Color(self.r + color.r, self.g + color.g, self.b + color.b)


	def __sub__(self, color):
		return Color(self.r - color.r, self.g - color.g, self.b - color.b)
	
	def __mul__(self, color):
		if not isinstance(color, Color):
			mult = float(color)
			return Color(self.r * mult, self.g * mult, self.b * mult)
		return Color(self.r * color.r, self.g * color.g, self.b * color.b)

	def __eq__(self, color):
		if isinstance(color, Color):
			return self.r == color.r and self.g == color.g and self.b == color.b
		return NotImplemented

	def __ne__(self, color):
		result = self.__eq__(color)
		if result is NotImplemented:
			return result
		return not result
	
	def __str__(self):
		return "(" + str(self.r) + ", " + str(self.g) + ", " + str(self.b) + ")"
	
	def FormattedAsFloat(self, format = '{:1.6f}'):
		return format.format(self.r) + " " + format.format(self.g) + " " + format.format(self.b)
	
	def FormattedAsInteger(self, bitdepth):
		rjustValue = len(str(2**bitdepth - 1)) + 1
		return str(Remap01ToInt(self.r, bitdepth)).rjust(rjustValue) + " " + str(Remap01ToInt(self.g, bitdepth)).rjust(rjustValue) + " " + str(Remap01ToInt(self.b, bitdepth)).rjust(rjustValue)

class LUT:
	"""
	A class that represents a 3D LUT with a 3D numpy array. The idea is that the modifications are non-volatile, meaning that every modification method returns a new LUT object.
	"""
	def __init__(self, lattice, name = "Untitled LUT"):
		self.lattice = lattice
		"""
		Numpy 3D array representing the 3D LUT.
		"""

		self.cubeSize = self.lattice.shape[0]
		"""
		LUT is of size (cubeSize, cubeSize, cubeSize) and index positions are from 0 to cubeSize-1
		"""
		
		self.name = str(name)
		"""
		Every LUT has a name!
		"""
		
	def Resize(self, newCubeSize):
		"""
		Scales the lattice to a new cube size.
		"""
		if newCubeSize == self.cubeSize:
			return self

		newLattice = EmptyLatticeOfSize(newCubeSize)
		ratio = float(self.cubeSize - 1.0) / float(newCubeSize-1.0)
		for x in xrange(newCubeSize):
			for y in xrange(newCubeSize):
				for z in xrange(newCubeSize):
					newLattice[x, y, z] = self.ColorAtInterpolatedLatticePoint(x*ratio, y*ratio, z*ratio)
		return LUT(newLattice, name = self.name + "_Resized"+str(newCubeSize))

	def _ResizeAndAddToData(self, newCubeSize, data, progress = False):
		"""
		Scales the lattice to a new cube size.
		"""
		newLattice = EmptyLatticeOfSize(newCubeSize)
		ratio = float(self.cubeSize - 1.0) / float(newCubeSize-1.0)
		maxVal = newCubeSize-1

		bar = Bar("Building search tree", max = maxVal, suffix='%(percent)d%% - %(eta)ds remain')
		try:
			for x in xrange(newCubeSize):
				if progress:
					bar.next()
				for y in xrange(newCubeSize):
					for z in xrange(newCubeSize):
						data.add(self.ColorAtInterpolatedLatticePoint(x*ratio, y*ratio, z*ratio).ToFloatArray(), (RemapIntTo01(x,maxVal), RemapIntTo01(y,maxVal), RemapIntTo01(z,maxVal)))
		except KeyboardInterrupt:
			bar.finish()
			raise KeyboardInterrupt
		bar.finish()
		return data

	def Reverse(self, progress = False):
		"""
		Reverses a LUT. Warning: This can take a long time depending on if the input/output is a bijection.
		"""
		tree = self.KDTree(progress)
		newLattice = EmptyLatticeOfSize(self.cubeSize)
		maxVal = self.cubeSize - 1
		bar = Bar("Searching for matches", max = maxVal, suffix='%(percent)d%% - %(eta)ds remain')
		try:
			for x in xrange(self.cubeSize):
				if progress:
					bar.next()
				for y in xrange(self.cubeSize):
					for z in xrange(self.cubeSize):
						newLattice[x, y, z] = Color.FromFloatArray(tree.search_nn((RemapIntTo01(x,maxVal), RemapIntTo01(y,maxVal), RemapIntTo01(z,maxVal))).aux)
		except KeyboardInterrupt:
			bar.finish()
			raise KeyboardInterrupt
		bar.finish()
		return LUT(newLattice, name = self.name +"_Reverse")
	
	def KDTree(self, progress = False):
		tree = kdtree.create(dimensions=3)
		
		tree = self._ResizeAndAddToData(self.cubeSize*3, tree, progress)
	
		return tree

		
	def CombineWithLUT(self, otherLUT):
		"""
		Combines LUT with another LUT.
		"""
		if self.cubeSize is not otherLUT.cubeSize:
			raise NameError("Lattice Sizes not equivalent")
		
		
		cubeSize = self.cubeSize
		newLattice = EmptyLatticeOfSize(cubeSize)
		
		for x in xrange(cubeSize):
			for y in xrange(cubeSize):
				for z in xrange(cubeSize):
					selfColor = self.lattice[x, y, z].Clamped01()
					newLattice[x, y, z] = otherLUT.ColorFromColor(selfColor)
		return LUT(newLattice, name = self.name + "+" + otherLUT.name)

	def ClampColor(self, min, max):
		"""
		Returns a new RGB clamped LUT.
		"""
		cubeSize = self.cubeSize
		newLattice = EmptyLatticeOfSize(cubeSize)
		for x in xrange(cubeSize):
			for y in xrange(cubeSize):
				for z in xrange(cubeSize):
					newLattice[x, y, z] = self.ColorAtLatticePoint(x, y, z).ClampColor(min, max)
		return LUT(newLattice)

	def _LatticeTo3DLString(self, bitdepth):
		"""
		Used for internal creating of 3DL files.
		"""
		string = ""
		cubeSize = self.cubeSize
		for currentCubeIndex in range(0, cubeSize**3):
			redIndex = currentCubeIndex / (cubeSize*cubeSize)
			greenIndex = ( (currentCubeIndex % (cubeSize*cubeSize)) / (cubeSize) )
			blueIndex = currentCubeIndex % cubeSize

			latticePointColor = self.lattice[redIndex, greenIndex, blueIndex].Clamped01()
			
			string += latticePointColor.FormattedAsInteger(bitdepth) + "\n"
		
		return string

	
	def ToLustre3DLFile(self, fileOutPath, bitdepth = 12):
		cubeSize = self.cubeSize
		inputDepth = math.log(cubeSize-1, 2)

		if int(inputDepth) != inputDepth:
			raise NameError("Invalid cube size for 3DL. Cube size must be 2^x + 1")

		lutFile = open(fileOutPath, 'w')

		lutFile.write("3DMESH\n")
		lutFile.write("Mesh " + str(int(inputDepth)) + " " + str(bitdepth) + "\n")
		lutFile.write(' '.join([str(int(x)) for x in Indices(cubeSize, 10)]) + "\n")
		
		lutFile.write(self._LatticeTo3DLString(bitdepth))

		lutFile.write("\n#Tokens required by applications - do not edit\nLUT8\ngamma 1.0")

		lutFile.close()

	def ToNuke3DLFile(self, fileOutPath, bitdepth = 16):
		cubeSize = self.cubeSize

		lutFile = open(fileOutPath, 'w')

		lutFile.write(' '.join([str(int(x)) for x in Indices(cubeSize, bitdepth)]) + "\n")

		lutFile.write(self._LatticeTo3DLString(bitdepth))

		lutFile.close()
	
	def ToCubeFile(self, cubeFileOutPath):
		cubeSize = self.cubeSize
		cubeFile = open(cubeFileOutPath, 'w')
		cubeFile.write("LUT_3D_SIZE " + str(cubeSize) + "\n")
		
		for currentCubeIndex in range(0, cubeSize**3):
			redIndex = currentCubeIndex % cubeSize
			greenIndex = ( (currentCubeIndex % (cubeSize*cubeSize)) / (cubeSize) )
			blueIndex = currentCubeIndex / (cubeSize*cubeSize)

			latticePointColor = self.lattice[redIndex, greenIndex, blueIndex].Clamped01()
			
			cubeFile.write( latticePointColor.FormattedAsFloat() )
			
			if(currentCubeIndex != cubeSize**3 - 1):
				cubeFile.write("\n")

		cubeFile.close()


	def ColorFromColor(self, color):
		"""
		Returns what a color value should be transformed to when piped through the LUT.
		"""
		color = color.Clamped01()
		cubeSize = self.cubeSize
		return self.ColorAtInterpolatedLatticePoint(color.r * (cubeSize-1), color.g * (cubeSize-1), color.b * (cubeSize-1))

	#integer input from 0 to cubeSize-1
	def ColorAtLatticePoint(self, redPoint, greenPoint, bluePoint):
		"""
		Returns a color at a specified lattice point - this value is pulled from the actual LUT file and is not interpolated.
		"""
		cubeSize = self.cubeSize
		if redPoint > cubeSize-1 or greenPoint > cubeSize-1 or bluePoint > cubeSize-1:
			raise NameError("Point Out of Bounds: (" + str(redPoint) + ", " + str(greenPoint) + ", " + str(bluePoint) + ")")

		return self.lattice[redPoint, greenPoint, bluePoint]

	#float input from 0 to cubeSize-1
	def ColorAtInterpolatedLatticePoint(self, redPoint, greenPoint, bluePoint):
		"""
		Gets the interpolated color at an interpolated lattice point.
		"""
		cubeSize = self.cubeSize

		if 0 < redPoint > cubeSize-1 or 0 < greenPoint > cubeSize-1 or 0 < bluePoint > cubeSize-1:
			raise NameError("Point Out of Bounds")

		lowerRedPoint = Clamp(int(math.floor(redPoint)), 0, cubeSize-1)
		upperRedPoint = Clamp(lowerRedPoint + 1, 0, cubeSize-1)

		lowerGreenPoint = Clamp(int(math.floor(greenPoint)), 0, cubeSize-1)
		upperGreenPoint = Clamp(lowerGreenPoint + 1, 0, cubeSize-1)

		lowerBluePoint = Clamp(int(math.floor(bluePoint)), 0, cubeSize-1)
		upperBluePoint = Clamp(lowerBluePoint + 1, 0, cubeSize-1)

		C000 = self.ColorAtLatticePoint(lowerRedPoint, lowerGreenPoint, lowerBluePoint)
		C010 = self.ColorAtLatticePoint(lowerRedPoint, lowerGreenPoint, upperBluePoint)
		C100 = self.ColorAtLatticePoint(upperRedPoint, lowerGreenPoint, lowerBluePoint)
		C001 = self.ColorAtLatticePoint(lowerRedPoint, upperGreenPoint, lowerBluePoint)
		C110 = self.ColorAtLatticePoint(upperRedPoint, lowerGreenPoint, upperBluePoint)
		C111 = self.ColorAtLatticePoint(upperRedPoint, upperGreenPoint, upperBluePoint)
		C101 = self.ColorAtLatticePoint(upperRedPoint, upperGreenPoint, lowerBluePoint)
		C011 = self.ColorAtLatticePoint(lowerRedPoint, upperGreenPoint, upperBluePoint)

		C00  = LerpColor(C000, C100, 1.0 - (upperRedPoint - redPoint))
		C10  = LerpColor(C010, C110, 1.0 - (upperRedPoint - redPoint))
		C01  = LerpColor(C001, C101, 1.0 - (upperRedPoint - redPoint))
		C11  = LerpColor(C011, C111, 1.0 - (upperRedPoint - redPoint))

		C1 = LerpColor(C01, C11, 1.0 - (upperBluePoint - bluePoint))
		C0 = LerpColor(C00, C10, 1.0 - (upperBluePoint - bluePoint))

		return LerpColor(C0, C1, 1.0 - (upperGreenPoint - greenPoint))

	@staticmethod
	def FromIdentity(cubeSize):
		"""
		Creates an indentity LUT of specified size.
		"""
		identityLattice = EmptyLatticeOfSize(cubeSize)
		indices01 = Indices01(cubeSize)
		for r in xrange(cubeSize):
			for g in xrange(cubeSize):
				for b in xrange(cubeSize):
					identityLattice[r, g, b] = Color(indices01[r], indices01[g], indices01[b])
		return LUT(identityLattice, name = "Identity"+str(cubeSize))

	@staticmethod
	def FromLustre3DLFile(lutFilePath):
		lutFile = open(lutFilePath, 'rU')
		lutFileLines = lutFile.readlines()
		lutFile.close()

		meshLineIndex = 0
		cubeSize = -1

		for line in lutFileLines:
			if "Mesh" in line:
				inputDepth = int(line.split()[1])
				outputDepth = int(line.split()[2])
				cubeSize = 2**inputDepth + 1
				break
			meshLineIndex += 1

		if cubeSize == -1:
			raise NameError("Invalid .3dl file.")

		lattice = EmptyLatticeOfSize(cubeSize)
		currentCubeIndex = 0
		
		for line in lutFileLines[meshLineIndex+1:]:
			if len(line) > 0 and len(line.split()) == 3 and "#" not in line:
				#valid cube line
				redValue = line.split()[0]
				greenValue = line.split()[1]
				blueValue = line.split()[2]

				redIndex = currentCubeIndex / (cubeSize*cubeSize)
				greenIndex = ( (currentCubeIndex % (cubeSize*cubeSize)) / (cubeSize) )
				blueIndex = currentCubeIndex % cubeSize

				lattice[redIndex, greenIndex, blueIndex] = Color.FromRGBInteger(redValue, greenValue, blueValue, bitdepth = outputDepth)
				currentCubeIndex += 1

		return LUT(lattice, name = os.path.splitext(os.path.basename(lutFilePath))[0])

	@staticmethod
	def FromNuke3DLFile(lutFilePath):
		lutFile = open(lutFilePath, 'rU')
		lutFileLines = lutFile.readlines()
		lutFile.close()

		meshLineIndex = 0
		cubeSize = -1
		lineSkip = 0

		for line in lutFileLines:
			if "#" in line or line == "\n":
				meshLineIndex += 1
	
		outputDepth = int(math.log(int(lutFileLines[meshLineIndex].split()[-1])+1,2))
		cubeSize = len(lutFileLines[meshLineIndex].split())
		
	
		if cubeSize == -1:
			raise NameError("Invalid .3dl file.")

		lattice = EmptyLatticeOfSize(cubeSize)
		currentCubeIndex = 0

		# for line in lutFileLines[meshLineIndex+1:]:
		for line in lutFileLines[meshLineIndex+1:]:
			# print line
			if len(line) > 0 and len(line.split()) == 3 and "#" not in line:
				#valid cube line
				redValue = line.split()[0]
				greenValue = line.split()[1]
				blueValue = line.split()[2]

				redIndex = currentCubeIndex / (cubeSize*cubeSize)
				greenIndex = ( (currentCubeIndex % (cubeSize*cubeSize)) / (cubeSize) )
				blueIndex = currentCubeIndex % cubeSize

				lattice[redIndex, greenIndex, blueIndex] = Color.FromRGBInteger(redValue, greenValue, blueValue, bitdepth = outputDepth)
				currentCubeIndex += 1
		return LUT(lattice, name = os.path.splitext(os.path.basename(lutFilePath))[0])

	@staticmethod
	def FromCubeFile(cubeFilePath):
		cubeFile = open(cubeFilePath, 'rU')
		cubeFileLines = cubeFile.readlines()
		cubeFile.close()

		cubeSizeLineIndex = 0
		cubeSize = -1

		for line in cubeFileLines:
			if "LUT_3D_SIZE" in line:
				cubeSize = int(line.split()[1])
				break
			cubeSizeLineIndex += 1
		if cubeSize == -1:
			raise NameError("Invalid .cube file.")

		lattice = EmptyLatticeOfSize(cubeSize)
		currentCubeIndex = 0
		for line in cubeFileLines[cubeSizeLineIndex+1:]:
			if len(line) > 0 and len(line.split()) == 3 and "#" not in line:
				#valid cube line
				redValue = float(line.split()[0])
				greenValue = float(line.split()[1])
				blueValue = float(line.split()[2])

				redIndex = currentCubeIndex % cubeSize
				greenIndex = ( (currentCubeIndex % (cubeSize*cubeSize)) / (cubeSize) )
				blueIndex = currentCubeIndex / (cubeSize*cubeSize)

				lattice[redIndex, greenIndex, blueIndex] = Color(redValue, greenValue, blueValue)
				currentCubeIndex += 1

		return LUT(lattice, name = os.path.splitext(os.path.basename(cubeFilePath))[0])

	def AddColorToEachPoint(self, color):
		"""
		Add a Color value to every lattice point on the cube.
		"""
		cubeSize = self.cubeSize
		newLattice = EmptyLatticeOfSize(cubeSize)
		for r in xrange(cubeSize):
			for g in xrange(cubeSize):
				for b in xrange(cubeSize):
					newLattice[r, g, b] = self.lattice[r, g, b] + color
		return LUT(newLattice)

	def SubtractColorFromEachPoint(self, color):
		"""
		Subtract a Color value to every lattice point on the cube.
		"""
		cubeSize = self.cubeSize
		newLattice = EmptyLatticeOfSize(cubeSize)
		for r in xrange(cubeSize):
			for g in xrange(cubeSize):
				for b in xrange(cubeSize):
					newLattice[r, g, b] = self.lattice[r, g, b] - color
		return LUT(newLattice)

	def MultiplyEachPoint(self, color):
		"""
		Multiply by a Color value or float for every lattice point on the cube.
		"""
		cubeSize = self.cubeSize
		newLattice = EmptyLatticeOfSize(cubeSize)
		for r in xrange(cubeSize):
			for g in xrange(cubeSize):
				for b in xrange(cubeSize):
					newLattice[r, g, b] = self.lattice[r, g, b] * color
		return LUT(newLattice)


	def __add__(self, other):
		if self.cubeSize is not other.cubeSize:
			raise NameError("Lattice Sizes not equivalent")

		return LUT(self.lattice + other.lattice)

	def __sub__(self, other):
		if self.cubeSize is not other.cubeSize:
			raise NameError("Lattice Sizes not equivalent")

		return LUT(self.lattice - other.lattice)

	def __mul__(self, other):
		className = other.__class__.__name__
		if "Color" in className or "float" in className or "int" in className:
			return self.MultiplyEachPoint(other)

		if self.cubeSize is not other.cubeSize:
			raise NameError("Lattice Sizes not equivalent")

		return LUT(self.lattice * other.lattice)

	def __rmul__(self, other):
		return self.__mul__(other)

	def __eq__(self, lut):
		if isinstance(lut, LUT):
			return (self.lattice == lut.lattice).all()
		return NotImplemented

	def __ne__(self, lut):
		result = self.__eq__(lut)
		if result is NotImplemented:
			return result
		return not result

	def Plot(self):
		"""
		Plot a LUT as a 3D RGB cube using matplotlib. Stolen from https://github.com/mikrosimage/ColorPipe-tools/tree/master/plotThatLut.
		"""
		
		try:
			import matplotlib
			# matplotlib : general plot
			from matplotlib.pyplot import title, figure
			# matplotlib : for 3D plot
			# mplot3d has to be imported for 3d projection
			import mpl_toolkits.mplot3d
			from matplotlib.colors import rgb2hex
		except ImportError:
			print "matplotlib not installed. Run: pip install matplotlib"
			return

		#for performance reasons lattice size must be 9 or less
		lut = None
		if self.cubeSize > 9:
			lut = self.Resize(9)
		else:
			lut = self


		# init vars
		cubeSize = lut.cubeSize
		input_range = xrange(0, cubeSize)
		max_value = cubeSize - 1.0
		red_values = []
		green_values = []
		blue_values = []
		colors = []
		# process color values
		for r in input_range:
			for g in input_range:
				for b in input_range:
					# get a value between [0..1]
					norm_r = r/max_value
					norm_g = g/max_value
					norm_b = b/max_value
					# apply correction
					res = lut.ColorFromColor(Color(norm_r, norm_g, norm_b))
					# append values
					red_values.append(res.r)
					green_values.append(res.g)
					blue_values.append(res.b)
					# append corresponding color
					colors.append(rgb2hex([norm_r, norm_g, norm_b]))
		# init plot
		fig = figure()
		fig.canvas.set_window_title('pylut Plotter')
		ax = fig.add_subplot(111, projection='3d')
		ax.set_xlabel('Red')
		ax.set_ylabel('Green')
		ax.set_zlabel('Blue')
		ax.set_xlim(min(red_values), max(red_values))
		ax.set_ylim(min(green_values), max(green_values))
		ax.set_zlim(min(blue_values), max(blue_values))
		title(self.name)
		# plot 3D values
		ax.scatter(red_values, green_values, blue_values, c=colors, marker="o")
		matplotlib.pyplot.show()

Index

Functions

def Clamp(

value, min, max)

def Clamp(value, min, max):
	if min > max:
		raise NameError("Invalid Clamp Values")
	if value < min:
		return float(min)
	if value > max:
		return float(max)
	return value

def Distance3D(

a, b)

def Distance3D(a, b):
	return math.sqrt((a[0] - b[0])**2 + (a[1] - b[1])**2 + (a[2] - b[2])**2)

def EmptyLatticeOfSize(

cubeSize)

def EmptyLatticeOfSize(cubeSize):
	return np.zeros((cubeSize, cubeSize, cubeSize), object)

def Indices(

cubeSize, bitdepth)

def Indices(cubeSize, bitdepth):
	indices = []
	for i in Indices01(cubeSize):
		indices.append(i * (2**bitdepth - 1))
	return indices

def Indices01(

cubeSize)

def Indices01(cubeSize):
	indices = []
	ratio = 1.0/float(cubeSize-1)
	for i in xrange(cubeSize):
		indices.append(float(i) * ratio)
	return indices

def Lerp1D(

beginning, end, value01)

def Lerp1D(beginning, end, value01):
	if value01 < 0 or value01 > 1:
		raise NameError("Improper Lerp")

	range = float(end) - float(beginning)
	return float(beginning) + float(range) * float(value01)

def Lerp3D(

beginning, end, value01)

def Lerp3D(beginning, end, value01):
	if value01 < 0 or value01 > 1:
		raise NameError("Improper Lerp")
	return [Lerp1D(beginning[0], end[0], value01), Lerp1D(beginning[1], end[1], value01), Lerp1D(beginning[2], end[2], value01)]

def LerpColor(

beginning, end, value01)

def LerpColor(beginning, end, value01):
	if value01 < 0 or value01 > 1:
		raise NameError("Improper Lerp")
	return Color(Lerp1D(beginning.r, end.r, value01), Lerp1D(beginning.g, end.g, value01), Lerp1D(beginning.b, end.b, value01))

def Remap01ToInt(

val, bitdepth)

def Remap01ToInt(val, bitdepth):
	return int(val * (2**bitdepth - 1))

def RemapIntTo01(

val, maxVal)

def RemapIntTo01(val, maxVal):
	return (float(val)/float(maxVal))

Classes

class Color

RGB floating point representation of a color. 0 is absolute black, 1 is absolute white. Access channel data by color.r, color.g, or color.b.

class Color:
	"""
	RGB floating point representation of a color. 0 is absolute black, 1 is absolute white.
	Access channel data by color.r, color.g, or color.b. 
	"""
	def __init__(self, r, g, b):
		self.r = r
		self.g = g
		self.b = b

	def Clamped01(self):
		return Color(Clamp(float(self.r), 0, 1), Clamp(float(self.g), 0, 1), Clamp(float(self.b), 0, 1))

	@staticmethod
	def FromRGBInteger(r, g, b, bitdepth):
		"""
		Instantiates a floating point color from RGB integers at a bitdepth.
		"""
		maxBits = 2**bitdepth - 1
		return Color(RemapIntTo01(r, maxBits), RemapIntTo01(g, maxBits), RemapIntTo01(b, maxBits))

	@staticmethod
	def FromFloatArray(array):
		"""
		Creates Color from a list or tuple of 3 floats.
		"""
		return Color(array[0], array[1], array[2])

	@staticmethod
	def FromRGBIntegerArray(array, bitdepth):
		"""
		Creates Color from a list or tuple of 3 RGB integers at a specified bitdepth.
		"""
		maxBits = 2**bitdepth - 1
		return Color(RemapIntTo01(array[0], maxBits), RemapIntTo01(array[1], maxBits), RemapIntTo01(array[2], maxBits))

	
	def ToFloatArray(self):
		"""
		Creates a tuple of 3 floating point RGB values from the floating point color.
		"""
		return (self.r, self.g, self.b)

	def ToRGBIntegerArray(self, bitdepth):
		"""
		Creates a list of 3 RGB integer values at specified bitdepth from the floating point color.
		"""
		return (Remap01ToInt(self.r, bitdepth), Remap01ToInt(self.g, bitdepth), Remap01ToInt(self.b, bitdepth))

	def ClampColor(self, min, max):
		"""
		Returns a clamped color.
		"""
		return Color(Clamp(self.r, min.r, max.r), Clamp(self.g, min.g, max.g), Clamp(self.b, min.b, max.b))

	def DistanceToColor(color):
		if isinstance(color, Color):
			return Distance3D(self.ToFloatArray(), color.ToFloatArray())
		return NotImplemented
	
	def __add__(self, color):
		return Color(self.r + color.r, self.g + color.g, self.b + color.b)


	def __sub__(self, color):
		return Color(self.r - color.r, self.g - color.g, self.b - color.b)
	
	def __mul__(self, color):
		if not isinstance(color, Color):
			mult = float(color)
			return Color(self.r * mult, self.g * mult, self.b * mult)
		return Color(self.r * color.r, self.g * color.g, self.b * color.b)

	def __eq__(self, color):
		if isinstance(color, Color):
			return self.r == color.r and self.g == color.g and self.b == color.b
		return NotImplemented

	def __ne__(self, color):
		result = self.__eq__(color)
		if result is NotImplemented:
			return result
		return not result
	
	def __str__(self):
		return "(" + str(self.r) + ", " + str(self.g) + ", " + str(self.b) + ")"
	
	def FormattedAsFloat(self, format = '{:1.6f}'):
		return format.format(self.r) + " " + format.format(self.g) + " " + format.format(self.b)
	
	def FormattedAsInteger(self, bitdepth):
		rjustValue = len(str(2**bitdepth - 1)) + 1
		return str(Remap01ToInt(self.r, bitdepth)).rjust(rjustValue) + " " + str(Remap01ToInt(self.g, bitdepth)).rjust(rjustValue) + " " + str(Remap01ToInt(self.b, bitdepth)).rjust(rjustValue)

Ancestors (in MRO)

Static methods

def FromFloatArray(

array)

Creates Color from a list or tuple of 3 floats.

@staticmethod
def FromFloatArray(array):
	"""
	Creates Color from a list or tuple of 3 floats.
	"""
	return Color(array[0], array[1], array[2])

def FromRGBInteger(

r, g, b, bitdepth)

Instantiates a floating point color from RGB integers at a bitdepth.

@staticmethod
def FromRGBInteger(r, g, b, bitdepth):
	"""
	Instantiates a floating point color from RGB integers at a bitdepth.
	"""
	maxBits = 2**bitdepth - 1
	return Color(RemapIntTo01(r, maxBits), RemapIntTo01(g, maxBits), RemapIntTo01(b, maxBits))

def FromRGBIntegerArray(

array, bitdepth)

Creates Color from a list or tuple of 3 RGB integers at a specified bitdepth.

@staticmethod
def FromRGBIntegerArray(array, bitdepth):
	"""
	Creates Color from a list or tuple of 3 RGB integers at a specified bitdepth.
	"""
	maxBits = 2**bitdepth - 1
	return Color(RemapIntTo01(array[0], maxBits), RemapIntTo01(array[1], maxBits), RemapIntTo01(array[2], maxBits))

Instance variables

var b

var g

var r

Methods

def __init__(

self, r, g, b)

def __init__(self, r, g, b):
	self.r = r
	self.g = g
	self.b = b

def ClampColor(

self, min, max)

Returns a clamped color.

def ClampColor(self, min, max):
	"""
	Returns a clamped color.
	"""
	return Color(Clamp(self.r, min.r, max.r), Clamp(self.g, min.g, max.g), Clamp(self.b, min.b, max.b))

def Clamped01(

self)

def Clamped01(self):
	return Color(Clamp(float(self.r), 0, 1), Clamp(float(self.g), 0, 1), Clamp(float(self.b), 0, 1))

def DistanceToColor(

color)

def DistanceToColor(color):
	if isinstance(color, Color):
		return Distance3D(self.ToFloatArray(), color.ToFloatArray())
	return NotImplemented

def FormattedAsFloat(

self, format='{:1.6f}')

def FormattedAsFloat(self, format = '{:1.6f}'):
	return format.format(self.r) + " " + format.format(self.g) + " " + format.format(self.b)

def FormattedAsInteger(

self, bitdepth)

def FormattedAsInteger(self, bitdepth):
	rjustValue = len(str(2**bitdepth - 1)) + 1
	return str(Remap01ToInt(self.r, bitdepth)).rjust(rjustValue) + " " + str(Remap01ToInt(self.g, bitdepth)).rjust(rjustValue) + " " + str(Remap01ToInt(self.b, bitdepth)).rjust(rjustValue)

def ToFloatArray(

self)

Creates a tuple of 3 floating point RGB values from the floating point color.

def ToFloatArray(self):
	"""
	Creates a tuple of 3 floating point RGB values from the floating point color.
	"""
	return (self.r, self.g, self.b)

def ToRGBIntegerArray(

self, bitdepth)

Creates a list of 3 RGB integer values at specified bitdepth from the floating point color.

def ToRGBIntegerArray(self, bitdepth):
	"""
	Creates a list of 3 RGB integer values at specified bitdepth from the floating point color.
	"""
	return (Remap01ToInt(self.r, bitdepth), Remap01ToInt(self.g, bitdepth), Remap01ToInt(self.b, bitdepth))

class LUT

A class that represents a 3D LUT with a 3D numpy array. The idea is that the modifications are non-volatile, meaning that every modification method returns a new LUT object.

class LUT:
	"""
	A class that represents a 3D LUT with a 3D numpy array. The idea is that the modifications are non-volatile, meaning that every modification method returns a new LUT object.
	"""
	def __init__(self, lattice, name = "Untitled LUT"):
		self.lattice = lattice
		"""
		Numpy 3D array representing the 3D LUT.
		"""

		self.cubeSize = self.lattice.shape[0]
		"""
		LUT is of size (cubeSize, cubeSize, cubeSize) and index positions are from 0 to cubeSize-1
		"""
		
		self.name = str(name)
		"""
		Every LUT has a name!
		"""
		
	def Resize(self, newCubeSize):
		"""
		Scales the lattice to a new cube size.
		"""
		if newCubeSize == self.cubeSize:
			return self

		newLattice = EmptyLatticeOfSize(newCubeSize)
		ratio = float(self.cubeSize - 1.0) / float(newCubeSize-1.0)
		for x in xrange(newCubeSize):
			for y in xrange(newCubeSize):
				for z in xrange(newCubeSize):
					newLattice[x, y, z] = self.ColorAtInterpolatedLatticePoint(x*ratio, y*ratio, z*ratio)
		return LUT(newLattice, name = self.name + "_Resized"+str(newCubeSize))

	def _ResizeAndAddToData(self, newCubeSize, data, progress = False):
		"""
		Scales the lattice to a new cube size.
		"""
		newLattice = EmptyLatticeOfSize(newCubeSize)
		ratio = float(self.cubeSize - 1.0) / float(newCubeSize-1.0)
		maxVal = newCubeSize-1

		bar = Bar("Building search tree", max = maxVal, suffix='%(percent)d%% - %(eta)ds remain')
		try:
			for x in xrange(newCubeSize):
				if progress:
					bar.next()
				for y in xrange(newCubeSize):
					for z in xrange(newCubeSize):
						data.add(self.ColorAtInterpolatedLatticePoint(x*ratio, y*ratio, z*ratio).ToFloatArray(), (RemapIntTo01(x,maxVal), RemapIntTo01(y,maxVal), RemapIntTo01(z,maxVal)))
		except KeyboardInterrupt:
			bar.finish()
			raise KeyboardInterrupt
		bar.finish()
		return data

	def Reverse(self, progress = False):
		"""
		Reverses a LUT. Warning: This can take a long time depending on if the input/output is a bijection.
		"""
		tree = self.KDTree(progress)
		newLattice = EmptyLatticeOfSize(self.cubeSize)
		maxVal = self.cubeSize - 1
		bar = Bar("Searching for matches", max = maxVal, suffix='%(percent)d%% - %(eta)ds remain')
		try:
			for x in xrange(self.cubeSize):
				if progress:
					bar.next()
				for y in xrange(self.cubeSize):
					for z in xrange(self.cubeSize):
						newLattice[x, y, z] = Color.FromFloatArray(tree.search_nn((RemapIntTo01(x,maxVal), RemapIntTo01(y,maxVal), RemapIntTo01(z,maxVal))).aux)
		except KeyboardInterrupt:
			bar.finish()
			raise KeyboardInterrupt
		bar.finish()
		return LUT(newLattice, name = self.name +"_Reverse")
	
	def KDTree(self, progress = False):
		tree = kdtree.create(dimensions=3)
		
		tree = self._ResizeAndAddToData(self.cubeSize*3, tree, progress)
	
		return tree

		
	def CombineWithLUT(self, otherLUT):
		"""
		Combines LUT with another LUT.
		"""
		if self.cubeSize is not otherLUT.cubeSize:
			raise NameError("Lattice Sizes not equivalent")
		
		
		cubeSize = self.cubeSize
		newLattice = EmptyLatticeOfSize(cubeSize)
		
		for x in xrange(cubeSize):
			for y in xrange(cubeSize):
				for z in xrange(cubeSize):
					selfColor = self.lattice[x, y, z].Clamped01()
					newLattice[x, y, z] = otherLUT.ColorFromColor(selfColor)
		return LUT(newLattice, name = self.name + "+" + otherLUT.name)

	def ClampColor(self, min, max):
		"""
		Returns a new RGB clamped LUT.
		"""
		cubeSize = self.cubeSize
		newLattice = EmptyLatticeOfSize(cubeSize)
		for x in xrange(cubeSize):
			for y in xrange(cubeSize):
				for z in xrange(cubeSize):
					newLattice[x, y, z] = self.ColorAtLatticePoint(x, y, z).ClampColor(min, max)
		return LUT(newLattice)

	def _LatticeTo3DLString(self, bitdepth):
		"""
		Used for internal creating of 3DL files.
		"""
		string = ""
		cubeSize = self.cubeSize
		for currentCubeIndex in range(0, cubeSize**3):
			redIndex = currentCubeIndex / (cubeSize*cubeSize)
			greenIndex = ( (currentCubeIndex % (cubeSize*cubeSize)) / (cubeSize) )
			blueIndex = currentCubeIndex % cubeSize

			latticePointColor = self.lattice[redIndex, greenIndex, blueIndex].Clamped01()
			
			string += latticePointColor.FormattedAsInteger(bitdepth) + "\n"
		
		return string

	
	def ToLustre3DLFile(self, fileOutPath, bitdepth = 12):
		cubeSize = self.cubeSize
		inputDepth = math.log(cubeSize-1, 2)

		if int(inputDepth) != inputDepth:
			raise NameError("Invalid cube size for 3DL. Cube size must be 2^x + 1")

		lutFile = open(fileOutPath, 'w')

		lutFile.write("3DMESH\n")
		lutFile.write("Mesh " + str(int(inputDepth)) + " " + str(bitdepth) + "\n")
		lutFile.write(' '.join([str(int(x)) for x in Indices(cubeSize, 10)]) + "\n")
		
		lutFile.write(self._LatticeTo3DLString(bitdepth))

		lutFile.write("\n#Tokens required by applications - do not edit\nLUT8\ngamma 1.0")

		lutFile.close()

	def ToNuke3DLFile(self, fileOutPath, bitdepth = 16):
		cubeSize = self.cubeSize

		lutFile = open(fileOutPath, 'w')

		lutFile.write(' '.join([str(int(x)) for x in Indices(cubeSize, bitdepth)]) + "\n")

		lutFile.write(self._LatticeTo3DLString(bitdepth))

		lutFile.close()
	
	def ToCubeFile(self, cubeFileOutPath):
		cubeSize = self.cubeSize
		cubeFile = open(cubeFileOutPath, 'w')
		cubeFile.write("LUT_3D_SIZE " + str(cubeSize) + "\n")
		
		for currentCubeIndex in range(0, cubeSize**3):
			redIndex = currentCubeIndex % cubeSize
			greenIndex = ( (currentCubeIndex % (cubeSize*cubeSize)) / (cubeSize) )
			blueIndex = currentCubeIndex / (cubeSize*cubeSize)

			latticePointColor = self.lattice[redIndex, greenIndex, blueIndex].Clamped01()
			
			cubeFile.write( latticePointColor.FormattedAsFloat() )
			
			if(currentCubeIndex != cubeSize**3 - 1):
				cubeFile.write("\n")

		cubeFile.close()


	def ColorFromColor(self, color):
		"""
		Returns what a color value should be transformed to when piped through the LUT.
		"""
		color = color.Clamped01()
		cubeSize = self.cubeSize
		return self.ColorAtInterpolatedLatticePoint(color.r * (cubeSize-1), color.g * (cubeSize-1), color.b * (cubeSize-1))

	#integer input from 0 to cubeSize-1
	def ColorAtLatticePoint(self, redPoint, greenPoint, bluePoint):
		"""
		Returns a color at a specified lattice point - this value is pulled from the actual LUT file and is not interpolated.
		"""
		cubeSize = self.cubeSize
		if redPoint > cubeSize-1 or greenPoint > cubeSize-1 or bluePoint > cubeSize-1:
			raise NameError("Point Out of Bounds: (" + str(redPoint) + ", " + str(greenPoint) + ", " + str(bluePoint) + ")")

		return self.lattice[redPoint, greenPoint, bluePoint]

	#float input from 0 to cubeSize-1
	def ColorAtInterpolatedLatticePoint(self, redPoint, greenPoint, bluePoint):
		"""
		Gets the interpolated color at an interpolated lattice point.
		"""
		cubeSize = self.cubeSize

		if 0 < redPoint > cubeSize-1 or 0 < greenPoint > cubeSize-1 or 0 < bluePoint > cubeSize-1:
			raise NameError("Point Out of Bounds")

		lowerRedPoint = Clamp(int(math.floor(redPoint)), 0, cubeSize-1)
		upperRedPoint = Clamp(lowerRedPoint + 1, 0, cubeSize-1)

		lowerGreenPoint = Clamp(int(math.floor(greenPoint)), 0, cubeSize-1)
		upperGreenPoint = Clamp(lowerGreenPoint + 1, 0, cubeSize-1)

		lowerBluePoint = Clamp(int(math.floor(bluePoint)), 0, cubeSize-1)
		upperBluePoint = Clamp(lowerBluePoint + 1, 0, cubeSize-1)

		C000 = self.ColorAtLatticePoint(lowerRedPoint, lowerGreenPoint, lowerBluePoint)
		C010 = self.ColorAtLatticePoint(lowerRedPoint, lowerGreenPoint, upperBluePoint)
		C100 = self.ColorAtLatticePoint(upperRedPoint, lowerGreenPoint, lowerBluePoint)
		C001 = self.ColorAtLatticePoint(lowerRedPoint, upperGreenPoint, lowerBluePoint)
		C110 = self.ColorAtLatticePoint(upperRedPoint, lowerGreenPoint, upperBluePoint)
		C111 = self.ColorAtLatticePoint(upperRedPoint, upperGreenPoint, upperBluePoint)
		C101 = self.ColorAtLatticePoint(upperRedPoint, upperGreenPoint, lowerBluePoint)
		C011 = self.ColorAtLatticePoint(lowerRedPoint, upperGreenPoint, upperBluePoint)

		C00  = LerpColor(C000, C100, 1.0 - (upperRedPoint - redPoint))
		C10  = LerpColor(C010, C110, 1.0 - (upperRedPoint - redPoint))
		C01  = LerpColor(C001, C101, 1.0 - (upperRedPoint - redPoint))
		C11  = LerpColor(C011, C111, 1.0 - (upperRedPoint - redPoint))

		C1 = LerpColor(C01, C11, 1.0 - (upperBluePoint - bluePoint))
		C0 = LerpColor(C00, C10, 1.0 - (upperBluePoint - bluePoint))

		return LerpColor(C0, C1, 1.0 - (upperGreenPoint - greenPoint))

	@staticmethod
	def FromIdentity(cubeSize):
		"""
		Creates an indentity LUT of specified size.
		"""
		identityLattice = EmptyLatticeOfSize(cubeSize)
		indices01 = Indices01(cubeSize)
		for r in xrange(cubeSize):
			for g in xrange(cubeSize):
				for b in xrange(cubeSize):
					identityLattice[r, g, b] = Color(indices01[r], indices01[g], indices01[b])
		return LUT(identityLattice, name = "Identity"+str(cubeSize))

	@staticmethod
	def FromLustre3DLFile(lutFilePath):
		lutFile = open(lutFilePath, 'rU')
		lutFileLines = lutFile.readlines()
		lutFile.close()

		meshLineIndex = 0
		cubeSize = -1

		for line in lutFileLines:
			if "Mesh" in line:
				inputDepth = int(line.split()[1])
				outputDepth = int(line.split()[2])
				cubeSize = 2**inputDepth + 1
				break
			meshLineIndex += 1

		if cubeSize == -1:
			raise NameError("Invalid .3dl file.")

		lattice = EmptyLatticeOfSize(cubeSize)
		currentCubeIndex = 0
		
		for line in lutFileLines[meshLineIndex+1:]:
			if len(line) > 0 and len(line.split()) == 3 and "#" not in line:
				#valid cube line
				redValue = line.split()[0]
				greenValue = line.split()[1]
				blueValue = line.split()[2]

				redIndex = currentCubeIndex / (cubeSize*cubeSize)
				greenIndex = ( (currentCubeIndex % (cubeSize*cubeSize)) / (cubeSize) )
				blueIndex = currentCubeIndex % cubeSize

				lattice[redIndex, greenIndex, blueIndex] = Color.FromRGBInteger(redValue, greenValue, blueValue, bitdepth = outputDepth)
				currentCubeIndex += 1

		return LUT(lattice, name = os.path.splitext(os.path.basename(lutFilePath))[0])

	@staticmethod
	def FromNuke3DLFile(lutFilePath):
		lutFile = open(lutFilePath, 'rU')
		lutFileLines = lutFile.readlines()
		lutFile.close()

		meshLineIndex = 0
		cubeSize = -1
		lineSkip = 0

		for line in lutFileLines:
			if "#" in line or line == "\n":
				meshLineIndex += 1
	
		outputDepth = int(math.log(int(lutFileLines[meshLineIndex].split()[-1])+1,2))
		cubeSize = len(lutFileLines[meshLineIndex].split())
		
	
		if cubeSize == -1:
			raise NameError("Invalid .3dl file.")

		lattice = EmptyLatticeOfSize(cubeSize)
		currentCubeIndex = 0

		# for line in lutFileLines[meshLineIndex+1:]:
		for line in lutFileLines[meshLineIndex+1:]:
			# print line
			if len(line) > 0 and len(line.split()) == 3 and "#" not in line:
				#valid cube line
				redValue = line.split()[0]
				greenValue = line.split()[1]
				blueValue = line.split()[2]

				redIndex = currentCubeIndex / (cubeSize*cubeSize)
				greenIndex = ( (currentCubeIndex % (cubeSize*cubeSize)) / (cubeSize) )
				blueIndex = currentCubeIndex % cubeSize

				lattice[redIndex, greenIndex, blueIndex] = Color.FromRGBInteger(redValue, greenValue, blueValue, bitdepth = outputDepth)
				currentCubeIndex += 1
		return LUT(lattice, name = os.path.splitext(os.path.basename(lutFilePath))[0])

	@staticmethod
	def FromCubeFile(cubeFilePath):
		cubeFile = open(cubeFilePath, 'rU')
		cubeFileLines = cubeFile.readlines()
		cubeFile.close()

		cubeSizeLineIndex = 0
		cubeSize = -1

		for line in cubeFileLines:
			if "LUT_3D_SIZE" in line:
				cubeSize = int(line.split()[1])
				break
			cubeSizeLineIndex += 1
		if cubeSize == -1:
			raise NameError("Invalid .cube file.")

		lattice = EmptyLatticeOfSize(cubeSize)
		currentCubeIndex = 0
		for line in cubeFileLines[cubeSizeLineIndex+1:]:
			if len(line) > 0 and len(line.split()) == 3 and "#" not in line:
				#valid cube line
				redValue = float(line.split()[0])
				greenValue = float(line.split()[1])
				blueValue = float(line.split()[2])

				redIndex = currentCubeIndex % cubeSize
				greenIndex = ( (currentCubeIndex % (cubeSize*cubeSize)) / (cubeSize) )
				blueIndex = currentCubeIndex / (cubeSize*cubeSize)

				lattice[redIndex, greenIndex, blueIndex] = Color(redValue, greenValue, blueValue)
				currentCubeIndex += 1

		return LUT(lattice, name = os.path.splitext(os.path.basename(cubeFilePath))[0])

	def AddColorToEachPoint(self, color):
		"""
		Add a Color value to every lattice point on the cube.
		"""
		cubeSize = self.cubeSize
		newLattice = EmptyLatticeOfSize(cubeSize)
		for r in xrange(cubeSize):
			for g in xrange(cubeSize):
				for b in xrange(cubeSize):
					newLattice[r, g, b] = self.lattice[r, g, b] + color
		return LUT(newLattice)

	def SubtractColorFromEachPoint(self, color):
		"""
		Subtract a Color value to every lattice point on the cube.
		"""
		cubeSize = self.cubeSize
		newLattice = EmptyLatticeOfSize(cubeSize)
		for r in xrange(cubeSize):
			for g in xrange(cubeSize):
				for b in xrange(cubeSize):
					newLattice[r, g, b] = self.lattice[r, g, b] - color
		return LUT(newLattice)

	def MultiplyEachPoint(self, color):
		"""
		Multiply by a Color value or float for every lattice point on the cube.
		"""
		cubeSize = self.cubeSize
		newLattice = EmptyLatticeOfSize(cubeSize)
		for r in xrange(cubeSize):
			for g in xrange(cubeSize):
				for b in xrange(cubeSize):
					newLattice[r, g, b] = self.lattice[r, g, b] * color
		return LUT(newLattice)


	def __add__(self, other):
		if self.cubeSize is not other.cubeSize:
			raise NameError("Lattice Sizes not equivalent")

		return LUT(self.lattice + other.lattice)

	def __sub__(self, other):
		if self.cubeSize is not other.cubeSize:
			raise NameError("Lattice Sizes not equivalent")

		return LUT(self.lattice - other.lattice)

	def __mul__(self, other):
		className = other.__class__.__name__
		if "Color" in className or "float" in className or "int" in className:
			return self.MultiplyEachPoint(other)

		if self.cubeSize is not other.cubeSize:
			raise NameError("Lattice Sizes not equivalent")

		return LUT(self.lattice * other.lattice)

	def __rmul__(self, other):
		return self.__mul__(other)

	def __eq__(self, lut):
		if isinstance(lut, LUT):
			return (self.lattice == lut.lattice).all()
		return NotImplemented

	def __ne__(self, lut):
		result = self.__eq__(lut)
		if result is NotImplemented:
			return result
		return not result

	def Plot(self):
		"""
		Plot a LUT as a 3D RGB cube using matplotlib. Stolen from https://github.com/mikrosimage/ColorPipe-tools/tree/master/plotThatLut.
		"""
		
		try:
			import matplotlib
			# matplotlib : general plot
			from matplotlib.pyplot import title, figure
			# matplotlib : for 3D plot
			# mplot3d has to be imported for 3d projection
			import mpl_toolkits.mplot3d
			from matplotlib.colors import rgb2hex
		except ImportError:
			print "matplotlib not installed. Run: pip install matplotlib"
			return

		#for performance reasons lattice size must be 9 or less
		lut = None
		if self.cubeSize > 9:
			lut = self.Resize(9)
		else:
			lut = self


		# init vars
		cubeSize = lut.cubeSize
		input_range = xrange(0, cubeSize)
		max_value = cubeSize - 1.0
		red_values = []
		green_values = []
		blue_values = []
		colors = []
		# process color values
		for r in input_range:
			for g in input_range:
				for b in input_range:
					# get a value between [0..1]
					norm_r = r/max_value
					norm_g = g/max_value
					norm_b = b/max_value
					# apply correction
					res = lut.ColorFromColor(Color(norm_r, norm_g, norm_b))
					# append values
					red_values.append(res.r)
					green_values.append(res.g)
					blue_values.append(res.b)
					# append corresponding color
					colors.append(rgb2hex([norm_r, norm_g, norm_b]))
		# init plot
		fig = figure()
		fig.canvas.set_window_title('pylut Plotter')
		ax = fig.add_subplot(111, projection='3d')
		ax.set_xlabel('Red')
		ax.set_ylabel('Green')
		ax.set_zlabel('Blue')
		ax.set_xlim(min(red_values), max(red_values))
		ax.set_ylim(min(green_values), max(green_values))
		ax.set_zlim(min(blue_values), max(blue_values))
		title(self.name)
		# plot 3D values
		ax.scatter(red_values, green_values, blue_values, c=colors, marker="o")
		matplotlib.pyplot.show()

Ancestors (in MRO)

Static methods

def FromCubeFile(

cubeFilePath)

@staticmethod
def FromCubeFile(cubeFilePath):
	cubeFile = open(cubeFilePath, 'rU')
	cubeFileLines = cubeFile.readlines()
	cubeFile.close()
	cubeSizeLineIndex = 0
	cubeSize = -1
	for line in cubeFileLines:
		if "LUT_3D_SIZE" in line:
			cubeSize = int(line.split()[1])
			break
		cubeSizeLineIndex += 1
	if cubeSize == -1:
		raise NameError("Invalid .cube file.")
	lattice = EmptyLatticeOfSize(cubeSize)
	currentCubeIndex = 0
	for line in cubeFileLines[cubeSizeLineIndex+1:]:
		if len(line) > 0 and len(line.split()) == 3 and "#" not in line:
			#valid cube line
			redValue = float(line.split()[0])
			greenValue = float(line.split()[1])
			blueValue = float(line.split()[2])
			redIndex = currentCubeIndex % cubeSize
			greenIndex = ( (currentCubeIndex % (cubeSize*cubeSize)) / (cubeSize) )
			blueIndex = currentCubeIndex / (cubeSize*cubeSize)
			lattice[redIndex, greenIndex, blueIndex] = Color(redValue, greenValue, blueValue)
			currentCubeIndex += 1
	return LUT(lattice, name = os.path.splitext(os.path.basename(cubeFilePath))[0])

def FromIdentity(

cubeSize)

Creates an indentity LUT of specified size.

@staticmethod
def FromIdentity(cubeSize):
	"""
	Creates an indentity LUT of specified size.
	"""
	identityLattice = EmptyLatticeOfSize(cubeSize)
	indices01 = Indices01(cubeSize)
	for r in xrange(cubeSize):
		for g in xrange(cubeSize):
			for b in xrange(cubeSize):
				identityLattice[r, g, b] = Color(indices01[r], indices01[g], indices01[b])
	return LUT(identityLattice, name = "Identity"+str(cubeSize))

def FromLustre3DLFile(

lutFilePath)

@staticmethod
def FromLustre3DLFile(lutFilePath):
	lutFile = open(lutFilePath, 'rU')
	lutFileLines = lutFile.readlines()
	lutFile.close()
	meshLineIndex = 0
	cubeSize = -1
	for line in lutFileLines:
		if "Mesh" in line:
			inputDepth = int(line.split()[1])
			outputDepth = int(line.split()[2])
			cubeSize = 2**inputDepth + 1
			break
		meshLineIndex += 1
	if cubeSize == -1:
		raise NameError("Invalid .3dl file.")
	lattice = EmptyLatticeOfSize(cubeSize)
	currentCubeIndex = 0
	
	for line in lutFileLines[meshLineIndex+1:]:
		if len(line) > 0 and len(line.split()) == 3 and "#" not in line:
			#valid cube line
			redValue = line.split()[0]
			greenValue = line.split()[1]
			blueValue = line.split()[2]
			redIndex = currentCubeIndex / (cubeSize*cubeSize)
			greenIndex = ( (currentCubeIndex % (cubeSize*cubeSize)) / (cubeSize) )
			blueIndex = currentCubeIndex % cubeSize
			lattice[redIndex, greenIndex, blueIndex] = Color.FromRGBInteger(redValue, greenValue, blueValue, bitdepth = outputDepth)
			currentCubeIndex += 1
	return LUT(lattice, name = os.path.splitext(os.path.basename(lutFilePath))[0])

def FromNuke3DLFile(

lutFilePath)

@staticmethod
def FromNuke3DLFile(lutFilePath):
	lutFile = open(lutFilePath, 'rU')
	lutFileLines = lutFile.readlines()
	lutFile.close()
	meshLineIndex = 0
	cubeSize = -1
	lineSkip = 0
	for line in lutFileLines:
		if "#" in line or line == "\n":
			meshLineIndex += 1

	outputDepth = int(math.log(int(lutFileLines[meshLineIndex].split()[-1])+1,2))
	cubeSize = len(lutFileLines[meshLineIndex].split())
	

	if cubeSize == -1:
		raise NameError("Invalid .3dl file.")
	lattice = EmptyLatticeOfSize(cubeSize)
	currentCubeIndex = 0
	# for line in lutFileLines[meshLineIndex+1:]:
	for line in lutFileLines[meshLineIndex+1:]:
		# print line
		if len(line) > 0 and len(line.split()) == 3 and "#" not in line:
			#valid cube line
			redValue = line.split()[0]
			greenValue = line.split()[1]
			blueValue = line.split()[2]
			redIndex = currentCubeIndex / (cubeSize*cubeSize)
			greenIndex = ( (currentCubeIndex % (cubeSize*cubeSize)) / (cubeSize) )
			blueIndex = currentCubeIndex % cubeSize
			lattice[redIndex, greenIndex, blueIndex] = Color.FromRGBInteger(redValue, greenValue, blueValue, bitdepth = outputDepth)
			currentCubeIndex += 1
	return LUT(lattice, name = os.path.splitext(os.path.basename(lutFilePath))[0])

Instance variables

var cubeSize

LUT is of size (cubeSize, cubeSize, cubeSize) and index positions are from 0 to cubeSize-1

var lattice

Numpy 3D array representing the 3D LUT.

var name

Every LUT has a name!

Methods

def __init__(

self, lattice, name='Untitled LUT')

def __init__(self, lattice, name = "Untitled LUT"):
	self.lattice = lattice
	"""
	Numpy 3D array representing the 3D LUT.
	"""
	self.cubeSize = self.lattice.shape[0]
	"""
	LUT is of size (cubeSize, cubeSize, cubeSize) and index positions are from 0 to cubeSize-1
	"""
	
	self.name = str(name)
	"""
	Every LUT has a name!
	"""

def AddColorToEachPoint(

self, color)

Add a Color value to every lattice point on the cube.

def AddColorToEachPoint(self, color):
	"""
	Add a Color value to every lattice point on the cube.
	"""
	cubeSize = self.cubeSize
	newLattice = EmptyLatticeOfSize(cubeSize)
	for r in xrange(cubeSize):
		for g in xrange(cubeSize):
			for b in xrange(cubeSize):
				newLattice[r, g, b] = self.lattice[r, g, b] + color
	return LUT(newLattice)

def ClampColor(

self, min, max)

Returns a new RGB clamped LUT.

def ClampColor(self, min, max):
	"""
	Returns a new RGB clamped LUT.
	"""
	cubeSize = self.cubeSize
	newLattice = EmptyLatticeOfSize(cubeSize)
	for x in xrange(cubeSize):
		for y in xrange(cubeSize):
			for z in xrange(cubeSize):
				newLattice[x, y, z] = self.ColorAtLatticePoint(x, y, z).ClampColor(min, max)
	return LUT(newLattice)

def ColorAtInterpolatedLatticePoint(

self, redPoint, greenPoint, bluePoint)

Gets the interpolated color at an interpolated lattice point.

def ColorAtInterpolatedLatticePoint(self, redPoint, greenPoint, bluePoint):
	"""
	Gets the interpolated color at an interpolated lattice point.
	"""
	cubeSize = self.cubeSize
	if 0 < redPoint > cubeSize-1 or 0 < greenPoint > cubeSize-1 or 0 < bluePoint > cubeSize-1:
		raise NameError("Point Out of Bounds")
	lowerRedPoint = Clamp(int(math.floor(redPoint)), 0, cubeSize-1)
	upperRedPoint = Clamp(lowerRedPoint + 1, 0, cubeSize-1)
	lowerGreenPoint = Clamp(int(math.floor(greenPoint)), 0, cubeSize-1)
	upperGreenPoint = Clamp(lowerGreenPoint + 1, 0, cubeSize-1)
	lowerBluePoint = Clamp(int(math.floor(bluePoint)), 0, cubeSize-1)
	upperBluePoint = Clamp(lowerBluePoint + 1, 0, cubeSize-1)
	C000 = self.ColorAtLatticePoint(lowerRedPoint, lowerGreenPoint, lowerBluePoint)
	C010 = self.ColorAtLatticePoint(lowerRedPoint, lowerGreenPoint, upperBluePoint)
	C100 = self.ColorAtLatticePoint(upperRedPoint, lowerGreenPoint, lowerBluePoint)
	C001 = self.ColorAtLatticePoint(lowerRedPoint, upperGreenPoint, lowerBluePoint)
	C110 = self.ColorAtLatticePoint(upperRedPoint, lowerGreenPoint, upperBluePoint)
	C111 = self.ColorAtLatticePoint(upperRedPoint, upperGreenPoint, upperBluePoint)
	C101 = self.ColorAtLatticePoint(upperRedPoint, upperGreenPoint, lowerBluePoint)
	C011 = self.ColorAtLatticePoint(lowerRedPoint, upperGreenPoint, upperBluePoint)
	C00  = LerpColor(C000, C100, 1.0 - (upperRedPoint - redPoint))
	C10  = LerpColor(C010, C110, 1.0 - (upperRedPoint - redPoint))
	C01  = LerpColor(C001, C101, 1.0 - (upperRedPoint - redPoint))
	C11  = LerpColor(C011, C111, 1.0 - (upperRedPoint - redPoint))
	C1 = LerpColor(C01, C11, 1.0 - (upperBluePoint - bluePoint))
	C0 = LerpColor(C00, C10, 1.0 - (upperBluePoint - bluePoint))
	return LerpColor(C0, C1, 1.0 - (upperGreenPoint - greenPoint))

def ColorAtLatticePoint(

self, redPoint, greenPoint, bluePoint)

Returns a color at a specified lattice point - this value is pulled from the actual LUT file and is not interpolated.

def ColorAtLatticePoint(self, redPoint, greenPoint, bluePoint):
	"""
	Returns a color at a specified lattice point - this value is pulled from the actual LUT file and is not interpolated.
	"""
	cubeSize = self.cubeSize
	if redPoint > cubeSize-1 or greenPoint > cubeSize-1 or bluePoint > cubeSize-1:
		raise NameError("Point Out of Bounds: (" + str(redPoint) + ", " + str(greenPoint) + ", " + str(bluePoint) + ")")
	return self.lattice[redPoint, greenPoint, bluePoint]

def ColorFromColor(

self, color)

Returns what a color value should be transformed to when piped through the LUT.

def ColorFromColor(self, color):
	"""
	Returns what a color value should be transformed to when piped through the LUT.
	"""
	color = color.Clamped01()
	cubeSize = self.cubeSize
	return self.ColorAtInterpolatedLatticePoint(color.r * (cubeSize-1), color.g * (cubeSize-1), color.b * (cubeSize-1))

def CombineWithLUT(

self, otherLUT)

Combines LUT with another LUT.

def CombineWithLUT(self, otherLUT):
	"""
	Combines LUT with another LUT.
	"""
	if self.cubeSize is not otherLUT.cubeSize:
		raise NameError("Lattice Sizes not equivalent")
	
	
	cubeSize = self.cubeSize
	newLattice = EmptyLatticeOfSize(cubeSize)
	
	for x in xrange(cubeSize):
		for y in xrange(cubeSize):
			for z in xrange(cubeSize):
				selfColor = self.lattice[x, y, z].Clamped01()
				newLattice[x, y, z] = otherLUT.ColorFromColor(selfColor)
	return LUT(newLattice, name = self.name + "+" + otherLUT.name)

def KDTree(

self, progress=False)

def KDTree(self, progress = False):
	tree = kdtree.create(dimensions=3)
	
	tree = self._ResizeAndAddToData(self.cubeSize*3, tree, progress)

	return tree

def MultiplyEachPoint(

self, color)

Multiply by a Color value or float for every lattice point on the cube.

def MultiplyEachPoint(self, color):
	"""
	Multiply by a Color value or float for every lattice point on the cube.
	"""
	cubeSize = self.cubeSize
	newLattice = EmptyLatticeOfSize(cubeSize)
	for r in xrange(cubeSize):
		for g in xrange(cubeSize):
			for b in xrange(cubeSize):
				newLattice[r, g, b] = self.lattice[r, g, b] * color
	return LUT(newLattice)

def Plot(

self)

Plot a LUT as a 3D RGB cube using matplotlib. Stolen from https://github.com/mikrosimage/ColorPipe-tools/tree/master/plotThatLut.

def Plot(self):
	"""
	Plot a LUT as a 3D RGB cube using matplotlib. Stolen from https://github.com/mikrosimage/ColorPipe-tools/tree/master/plotThatLut.
	"""
	
	try:
		import matplotlib
		# matplotlib : general plot
		from matplotlib.pyplot import title, figure
		# matplotlib : for 3D plot
		# mplot3d has to be imported for 3d projection
		import mpl_toolkits.mplot3d
		from matplotlib.colors import rgb2hex
	except ImportError:
		print "matplotlib not installed. Run: pip install matplotlib"
		return
	#for performance reasons lattice size must be 9 or less
	lut = None
	if self.cubeSize > 9:
		lut = self.Resize(9)
	else:
		lut = self
	# init vars
	cubeSize = lut.cubeSize
	input_range = xrange(0, cubeSize)
	max_value = cubeSize - 1.0
	red_values = []
	green_values = []
	blue_values = []
	colors = []
	# process color values
	for r in input_range:
		for g in input_range:
			for b in input_range:
				# get a value between [0..1]
				norm_r = r/max_value
				norm_g = g/max_value
				norm_b = b/max_value
				# apply correction
				res = lut.ColorFromColor(Color(norm_r, norm_g, norm_b))
				# append values
				red_values.append(res.r)
				green_values.append(res.g)
				blue_values.append(res.b)
				# append corresponding color
				colors.append(rgb2hex([norm_r, norm_g, norm_b]))
	# init plot
	fig = figure()
	fig.canvas.set_window_title('pylut Plotter')
	ax = fig.add_subplot(111, projection='3d')
	ax.set_xlabel('Red')
	ax.set_ylabel('Green')
	ax.set_zlabel('Blue')
	ax.set_xlim(min(red_values), max(red_values))
	ax.set_ylim(min(green_values), max(green_values))
	ax.set_zlim(min(blue_values), max(blue_values))
	title(self.name)
	# plot 3D values
	ax.scatter(red_values, green_values, blue_values, c=colors, marker="o")
	matplotlib.pyplot.show()

def Resize(

self, newCubeSize)

Scales the lattice to a new cube size.

def Resize(self, newCubeSize):
	"""
	Scales the lattice to a new cube size.
	"""
	if newCubeSize == self.cubeSize:
		return self
	newLattice = EmptyLatticeOfSize(newCubeSize)
	ratio = float(self.cubeSize - 1.0) / float(newCubeSize-1.0)
	for x in xrange(newCubeSize):
		for y in xrange(newCubeSize):
			for z in xrange(newCubeSize):
				newLattice[x, y, z] = self.ColorAtInterpolatedLatticePoint(x*ratio, y*ratio, z*ratio)
	return LUT(newLattice, name = self.name + "_Resized"+str(newCubeSize))

def Reverse(

self, progress=False)

Reverses a LUT. Warning: This can take a long time depending on if the input/output is a bijection.

def Reverse(self, progress = False):
	"""
	Reverses a LUT. Warning: This can take a long time depending on if the input/output is a bijection.
	"""
	tree = self.KDTree(progress)
	newLattice = EmptyLatticeOfSize(self.cubeSize)
	maxVal = self.cubeSize - 1
	bar = Bar("Searching for matches", max = maxVal, suffix='%(percent)d%% - %(eta)ds remain')
	try:
		for x in xrange(self.cubeSize):
			if progress:
				bar.next()
			for y in xrange(self.cubeSize):
				for z in xrange(self.cubeSize):
					newLattice[x, y, z] = Color.FromFloatArray(tree.search_nn((RemapIntTo01(x,maxVal), RemapIntTo01(y,maxVal), RemapIntTo01(z,maxVal))).aux)
	except KeyboardInterrupt:
		bar.finish()
		raise KeyboardInterrupt
	bar.finish()
	return LUT(newLattice, name = self.name +"_Reverse")

def SubtractColorFromEachPoint(

self, color)

Subtract a Color value to every lattice point on the cube.

def SubtractColorFromEachPoint(self, color):
	"""
	Subtract a Color value to every lattice point on the cube.
	"""
	cubeSize = self.cubeSize
	newLattice = EmptyLatticeOfSize(cubeSize)
	for r in xrange(cubeSize):
		for g in xrange(cubeSize):
			for b in xrange(cubeSize):
				newLattice[r, g, b] = self.lattice[r, g, b] - color
	return LUT(newLattice)

def ToCubeFile(

self, cubeFileOutPath)

def ToCubeFile(self, cubeFileOutPath):
	cubeSize = self.cubeSize
	cubeFile = open(cubeFileOutPath, 'w')
	cubeFile.write("LUT_3D_SIZE " + str(cubeSize) + "\n")
	
	for currentCubeIndex in range(0, cubeSize**3):
		redIndex = currentCubeIndex % cubeSize
		greenIndex = ( (currentCubeIndex % (cubeSize*cubeSize)) / (cubeSize) )
		blueIndex = currentCubeIndex / (cubeSize*cubeSize)
		latticePointColor = self.lattice[redIndex, greenIndex, blueIndex].Clamped01()
		
		cubeFile.write( latticePointColor.FormattedAsFloat() )
		
		if(currentCubeIndex != cubeSize**3 - 1):
			cubeFile.write("\n")
	cubeFile.close()

def ToLustre3DLFile(

self, fileOutPath, bitdepth=12)

def ToLustre3DLFile(self, fileOutPath, bitdepth = 12):
	cubeSize = self.cubeSize
	inputDepth = math.log(cubeSize-1, 2)
	if int(inputDepth) != inputDepth:
		raise NameError("Invalid cube size for 3DL. Cube size must be 2^x + 1")
	lutFile = open(fileOutPath, 'w')
	lutFile.write("3DMESH\n")
	lutFile.write("Mesh " + str(int(inputDepth)) + " " + str(bitdepth) + "\n")
	lutFile.write(' '.join([str(int(x)) for x in Indices(cubeSize, 10)]) + "\n")
	
	lutFile.write(self._LatticeTo3DLString(bitdepth))
	lutFile.write("\n#Tokens required by applications - do not edit\nLUT8\ngamma 1.0")
	lutFile.close()

def ToNuke3DLFile(

self, fileOutPath, bitdepth=16)

def ToNuke3DLFile(self, fileOutPath, bitdepth = 16):
	cubeSize = self.cubeSize
	lutFile = open(fileOutPath, 'w')
	lutFile.write(' '.join([str(int(x)) for x in Indices(cubeSize, bitdepth)]) + "\n")
	lutFile.write(self._LatticeTo3DLString(bitdepth))
	lutFile.close()

Documentation generated by pdoc 0.1.8. pdoc is in the public domain with the UNLICENSE.