2016-08-23 02:42:50 +02:00
#!/usr/bin/env python3.5
'''
openlut : A package for managing and applying 1 D and 3 D LUTs .
Color Management : openlut deals with the raw RGB values , does its work , then puts out images with correct raw RGB values - a no - op .
Dependencies :
- numpy : Like , everything .
- wand : Saving / loading all images .
- numba : 38 % speedup for matrix math .
- scipy - OPTIONAL : For spline interpolation .
Easily get all deps : sudo pip3 install numpy wand numba scipy
* Make sure you get the Python 3. X version of these packages ! ! !
LICENCE :
The MIT License ( MIT )
Copyright ( c ) 2016 Sofus Rose
Permission is hereby granted , free of charge , to any person obtaining a copy
of this software and associated documentation files ( the " Software " ) , to deal
in the Software without restriction , including without limitation the rights
to use , copy , modify , merge , publish , distribute , sublicense , and / or sell
copies of the Software , and to permit persons to whom the Software is
furnished to do so , subject to the following conditions :
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software .
THE SOFTWARE IS PROVIDED " AS IS " , WITHOUT WARRANTY OF ANY KIND , EXPRESS OR
IMPLIED , INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY ,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT . IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM , DAMAGES OR OTHER
LIABILITY , WHETHER IN AN ACTION OF CONTRACT , TORT OR OTHERWISE , ARISING FROM ,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE .
'''
import sys , os , math , abc , ctypes
import multiprocessing as mp
from functools import reduce
import operator as oper
MOD_SCIPY = False
try :
from scipy . interpolate import splrep , splev
MOD_SCIPY = True
except :
pass
import numpy as np
import numba
#~ import skimage as si
#~ import skimage.io
#~ si.io.use_plugin('freeimage')
#~ from PIL import Image
#~ import tifffile as tff
import wand
import wand . image
import wand . display
from wand . api import library
2016-08-23 19:00:26 +02:00
#~ library.MagickSetCompressionQuality.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
#~ library.MagickSetCompression.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
2016-08-23 02:42:50 +02:00
2016-08-23 19:00:26 +02:00
#~ COMPRESS_TYPES = dict(zip(wand.image.COMPRESSION_TYPES, tuple(map(ctypes.c_int, range(len(wand.image.COMPRESSION_TYPES))))))
2016-08-23 02:42:50 +02:00
#~ from lib.files import Log #For Development
class Transform :
def apply ( self , cMap ) :
"""
Applies this transformation to a ColMap .
"""
return ColMap ( self . sample ( cMap . asarray ( ) ) )
@abc.abstractmethod
def sample ( self , fSeq ) :
"""
Samples the Transformation .
"""
def spSeq ( seq , outLen ) :
"""
Utility function for splitting a sequence into equal parts , for multithreading .
"""
perfSep = ( 1 / outLen ) * len ( seq )
return list ( filter ( len , [ seq [ round ( perfSep * i ) : round ( perfSep * ( i + 1 ) ) ] for i in range ( len ( seq ) ) ] ) ) if len ( seq ) > 1 else seq
class ColMap :
def __init__ ( self , rgbArr ) :
self . rgbArr = rgbArr
def fromIntArray ( imgArr ) :
bitDepth = int ( ' ' . join ( [ i for i in str ( imgArr . dtype ) if i . isdigit ( ) ] ) )
return ColMap ( np . divide ( imgArr . astype ( np . float64 ) , 2 * * bitDepth - 1 ) )
#Operations - returns new ColMaps.
def apply ( self , transform ) :
'''
Applies a Transform object by running its apply method .
'''
return transform . apply ( self )
#IO Functions
def open ( path ) :
'''
Opens 8 and 16 bit images of many formats .
'''
try :
openFunction = {
" exr " : ColMap . openWand ,
" dpx " : ColMap . openWand ,
} [ path [ path . rfind ( ' . ' ) + 1 : ] ]
return openFunction ( path ) #Any fancy formats will go here.
except :
#Fallback to opening using Wand.
return ColMap . openWand ( path )
#Vendor-specific open methods.
#~ def openSci(path) :
#~ return ColMap.fromIntArray(si.io.imread(path)[:,:,:3])
def openWand ( path ) :
'''
Open a file using the Wand ImageMagick binding .
'''
with wand . image . Image ( filename = path ) as img :
#Quick inverse sRGB transform, to undo what Wand did.
img . colorspace = ' srgb '
img . transform_colorspace ( ' rgb ' )
img . colorspace = ' srgb ' if img . format == ' DPX ' else ' rgb ' #Fix for IM's dpx bug.
return ColMap . fromIntArray ( np . fromstring ( img . make_blob ( " RGB " ) , dtype = ' uint {} ' . format ( img . depth ) ) . reshape ( img . height , img . width , 3 ) )
def save ( self , path , compress = None , depth = None ) :
'''
Save the image . The filetype will be inferred from the path , and the appropriate backend will be used .
Compression scheme will be applied based on the backend compatiblity . Wand compression types can be used : Browse then
at http : / / docs . wand - py . org / en / 0.4 .3 / wand / image . html #wand.image.COMPRESSION_TYPES .
'''
if depth is None : depth = 16
try :
saveFunction = {
" exr " : self . saveWand ,
" dpx " : self . saveWand ,
" tif " : self . saveWand ,
" tiff " : self . saveWand
} [ path [ path . rfind ( ' . ' ) + 1 : ] ]
return saveFunction ( path , compress , depth )
except :
#Fallback to saving using Wand.
self . saveWand ( path , compress , depth )
#Vendor-specific save methods
def saveWand ( self , path , compress = None , depth = 16 ) :
data = self . apply ( LUT . lutFunc ( Gamma . sRGB ) ) if path [ path . rfind ( ' . ' ) + 1 : ] == ' dpx ' else self
i = data . asWandImg ( depth )
i . colorspace = ' srgb ' #Make sure it saves without a colorspace transformation.
#~ if compress :
#~ library.MagickSetCompression(i.wand, 'rle')
#~ i.compression = 'lzma'
#~ i.compression_quality = 80
i . save ( filename = path )
#~ def saveSci(self, path, compress = None, depth = 16) :
#~ if compress is not None: raise ValueError('Scipy Backend cannot compress the output image!')
#~ si.io.imsave(path, self.asIntArray())
#~ def savePil(self, path, compress = None, depth = 8) :
#~ if compress is not None: raise ValueError('Scipy Backend cannot compress the output image!')
#~ if depth != 8: raise ValueError('Cannot save non-8 bit image using PIL.')
#~ self.asPilImg().save(path)
def show ( self ) :
#~ ColMap.pilShow(self.apply(LUT.lutFunc(Gamma.sRGB)).asPilImg())
ColMap . wandShow ( self . asWandImg ( ) )
#~ def pilShow(pilImg) :
#~ pilImg.show()
def wandShow ( wandImg ) :
#Do a quick sRGB transform for viewing. Must be in 'rgb' colorspace for this to take effect.
wandImg . transform_colorspace ( ' srgb ' )
wand . display . display ( wandImg )
wandImg . transform_colorspace ( ' rgb ' ) #This transforms it back to linearity.
#Data Form Functions
#~ def asPilImg(self) :
#~ return Image.fromarray(self.asIntArray(8), mode='RGB')
def asWandImg ( self , depth = 16 ) :
i = wand . image . Image ( blob = self . asIntArray ( depth ) . tostring ( ) , width = np . shape ( self . rgbArr ) [ 1 ] , height = np . shape ( self . rgbArr ) [ 0 ] , format = ' RGB ' )
i . colorspace = ' rgb ' #Specify, to Wand, that this image is to be treated as raw, linear, data.
return i
def asarray ( self ) :
"""
Returns the base float array .
"""
return self . rgbArr
def asIntArray ( self , depth = 16 , us = True ) :
u = ' u ' if us else ' '
return np . multiply ( self . rgbArr . clip ( 0 , 1 ) , 2.0 * * depth - 1 ) . astype ( " {0} int {1} " . format ( u , depth ) )
#Overloads
def __repr__ ( self ) :
return ' ColMap( \n \t rgbArr = {0} \n ) ' . format ( ' \n \t \t ' . join ( [ line . strip ( ) for line in repr ( self . rgbArr ) . split ( ' \n ' ) ] ) )
class LUT ( Transform ) :
def __init__ ( self , dims = 1 , size = 16384 , title = " openlut_LUT " , array = None , iRange = ( 0.0 , 1.0 ) ) :
'''
Create an identity LUT with given dimensions ( 1 or 3 ) , size , and title .
'''
if array is not None :
LUT . lutArray ( array , size , dims , title )
else :
if dims != 1 and dims != 3 : raise ValueError ( " Dimensions must be 1 or 3! " )
self . title = title #The title.
self . size = size #The size. 1D LUTs: size numbers. 3D LUTs: size x size x size numbers.
self . range = iRange #The input range - creates data or legal LUTs. Should work fine, but untested.
self . dims = dims #The dimensions. 1 or 3; others aren't accepted.
self . ID = np . linspace ( self . range [ 0 ] , self . range [ 1 ] , self . size ) #Read Only.
if dims == 1 :
self . array = np . linspace ( self . range [ 0 ] , self . range [ 1 ] , self . size ) #Size number of floats.
elif dims == 3 :
print ( " 3D LUT Not Implemented! " )
#~ self.array = np.linspace(self.range[0], self.range[1], self.size**3).reshape(self.size, self.size, self.size) #Should make an identity size x size x size array.
def lutFunc ( func , size = 16384 , dims = 1 , title = " openlut_FuncGen " ) :
'''
Creates a LUT from a simple function .
'''
if dims == 1 :
lut = LUT ( dims = dims , size = size , title = title )
vFunc = np . vectorize ( func , otypes = [ np . float ] )
lut . array = vFunc ( lut . array )
return lut
elif dims == 3 :
print ( " 3D LUT Not Implemented! " )
def lutArray ( array , title = " Array_Generated " ) :
'''
Creates a LUT from a float array . Elements must be in range [ 0 , 1 ] .
'''
if len ( np . shape ( array ) ) == 1 :
lut = LUT ( dims = 1 , size = len ( array ) , title = title )
lut . array = array
return lut
elif len ( np . shape ( array ) ) == 3 :
print ( " 3D LUT Not Implemented! " )
else :
raise ValueError ( " lutArray input must be 1D or 3D! " )
#LUT Functions.
def __interp ( q , cpu , spSeq , ID , array , spl ) :
if spl :
q . put ( ( cpu , splev ( spSeq , splrep ( ID , array ) ) ) ) #Spline Interpolation. Pretty quick, considering.
else :
q . put ( ( cpu , np . interp ( spSeq , ID , array ) ) )
def sample ( self , fSeq , spl = True ) :
'''
Sample the LUT using a flat float sequence ( ideally a numpy array ; ( 0. .1 ) ) .
Each n ( dimensions ) clump of arguments will be used to sample the LUT . So :
1 D LUT : in1 , in2 , in3 - - > out1 , out2 , out3
* Min 1 argument .
3 D LUT : inR , inG , inB - - > outR , outG , outB
* Min 3 arguments , len ( arguments ) % 3 must equal 0.
Returns a numpy array with identical shape to the input array .
'''
fSeq = np . array ( fSeq )
if self . dims == 1 :
#~ return np.interp(spSeq, self.ID, self.array)
#If scipy isn't loaded, we can't use spline interpolation!
if ( not MOD_SCIPY ) or self . size > 1023 : spl = False # Auto-adapts big LUTs to use the faster, more brute-forceish, linear interpolation.
#~ spl = True
out = [ ]
q = mp . Queue ( )
splt = Transform . spSeq ( fSeq , mp . cpu_count ( ) )
for cpu in range ( mp . cpu_count ( ) ) :
p = mp . Process ( target = LUT . __interp , args = ( q , cpu , splt [ cpu ] , self . ID , self . array , spl ) )
p . start ( )
for num in range ( len ( splt ) ) :
out . append ( q . get ( ) )
return np . concatenate ( [ seq [ 1 ] for seq in sorted ( out , key = lambda seq : seq [ 0 ] ) ] , axis = 0 )
elif self . dims == 3 :
print ( " 3D LUT Not Implemented! " )
def resized ( self , newSize ) :
if newSize == self . size : return self
fac = newSize / self . size
useSpl = self . size < newSize #If the new size is lower, we use Linear interpolation. If the new size is higher, we use Spline interpolation.
if self . size < 128 : useSpl = True #If the current size is too low, use spline regardless.
if self . dims == 1 :
newID = np . linspace ( self . range [ 0 ] , self . range [ 1 ] , newSize )
return LUT . lutArray ( self . sample ( newID , spl = useSpl ) , title = " Resized to {0} " . format ( newSize ) )
if self . dims == 3 :
print ( " 3D LUT Not Implemented " )
#IO Functions.
def open ( path ) :
'''
Opens any supported file format , located at path .
'''
openFunction = {
" cube " : LUT . openCube ,
} [ path [ path . rfind ( ' . ' ) + 1 : ] ]
return openFunction ( path )
def openCube ( path ) :
'''
Opens . cube files . They must be saved with whitespaces . Referenced by open ( ) .
'''
lut = LUT ( ) #Mutable luts are not reccommended for users.
with open ( path , ' r ' ) as f :
i = 0
for line in f :
#~ if not line.strip(): continue
sLine = line . strip ( )
if not sLine : continue
if sLine [ 0 ] == ' # ' : continue
index = sLine [ : sLine . find ( ' ' ) ]
data = sLine [ sLine . find ( ' ' ) + 1 : ]
if index == " TITLE " : lut . title = data . strip ( ' " ' ) ; continue
if index == " LUT_1D_SIZE " : lut . dims = 1 ; lut . size = int ( data ) ; continue
if index == " LUT_3D_SIZE " : lut . dims = 3 ; lut . size = int ( data ) ; continue
if index == " LUT_1D_INPUT_RANGE " : lut . range = ( float ( data [ : data . find ( ' ' ) ] ) , float ( data [ data . find ( ' ' ) + 1 : ] ) ) ; continue
if lut . dims == 1 and sLine [ : sLine . find ( ' ' ) ] :
lut . array [ i ] = float ( sLine [ : sLine . find ( ' ' ) ] )
i + = 1
elif lut . dims == 3 :
print ( " 3D LUT Not Implemened! " )
return lut
def save ( self , path ) :
'''
Method that saves the LUT in a supported format , based on the path .
'''
saveFunction = {
" cube " : self . saveCube ,
} [ path [ path . rfind ( ' . ' ) + 1 : ] ]
saveFunction ( path )
def saveCube ( self , path ) :
with open ( path , ' w ' ) as f :
print ( ' TITLE ' , ' " {} " ' . format ( self . title ) , file = f )
if self . dims == 1 :
print ( ' LUT_1D_SIZE ' , ' {} ' . format ( self . size ) , file = f )
print ( ' LUT_1D_INPUT_RANGE ' , ' {0:.6f} {1:.6f} ' . format ( * self . range ) , file = f )
print ( ' # Created by openlut. \n ' , file = f )
for itm in self . array :
entry = ' {0:.6f} ' . format ( itm )
print ( entry , entry , entry , file = f )
elif self . dims == 3 :
print ( " 3D LUT Not Implemented! " )
#Overloaded functions
def __iter__ ( self ) :
if dims == 1 :
return iter ( self . array )
elif dims == 3 :
iArr = self . array . reshape ( self . dims , self . size / self . dims ) #Group into triplets.
return iter ( iArr )
def __getitem__ ( self , key ) :
return self . sample ( key )
def __repr__ ( self ) :
return ' LUT( \t dims = {0} , \n \t size = {1} , \n \t title = " {2} " \n \t array = {3} \n ) ' . format ( self . dims , self . size , self . title , ' \n \t \t ' . join ( [ line . strip ( ) for line in repr ( self . array ) . split ( ' \n ' ) ] ) )
class Gamma ( Transform ) :
def __init__ ( self , func ) :
self . func = func
#Gamma Methods
def __gamma ( q , cpu , f , spSeq ) :
q . put ( ( cpu , f ( spSeq ) ) )
def sample ( self , fSeq ) :
fSeq = np . array ( fSeq )
fVec = np . vectorize ( self . func )
out = [ ]
q = mp . Queue ( )
splt = Transform . spSeq ( fSeq , mp . cpu_count ( ) )
for cpu in range ( mp . cpu_count ( ) ) :
p = mp . Process ( target = Gamma . __gamma , args = ( q , cpu , fVec , splt [ cpu ] ) )
p . start ( )
for num in range ( len ( splt ) ) :
out . append ( q . get ( ) )
return np . concatenate ( [ seq [ 1 ] for seq in sorted ( out , key = lambda seq : seq [ 0 ] ) ] , axis = 0 ) if len ( fSeq ) > 1 else self . func ( fSeq [ 0 ] )
return fVec ( fSeq ) if len ( fSeq ) > 1 else self . func ( fSeq [ 0 ] )
#Static Gamma Functions (partly adapted from MLRawViewer)
def lin ( x ) : return x
def sRGB ( x ) :
'''
sRGB formula . Domain must be within [ 0 , 1 ] .
'''
return ( ( 1.055 ) * ( x * * ( 1.0 / 2.4 ) ) ) - 0.055 if x > 0.0031308 else x * 12.92
def sRGBinv ( x ) :
'''
Inverse sRGB formula . Domain must be within [ 0 , 1 ] .
'''
return ( ( x + 0.055 ) / 1.055 ) * * 2.4 if x > 0.04045 else x / 12.92
def Rec709 ( x ) :
'''
Rec709 formula . Domain must be within [ 0 , 1 ] .
'''
return 1.099 * ( x * * 0.45 ) - 0.099 if x > = 0.018 else 4.5 * x
def ReinhardHDR ( x ) :
'''
Reinhard Tonemapping formula . Domain must be within [ 0 , 1 ] .
'''
return x / ( 1.0 + x )
def sLog ( x ) :
'''
sLog 1 formula . Domain must be within [ 0 , 1 ] . See https : / / pro . sony . com / bbsccms / assets /
files / mkt / cinema / solutions / slog_manual . pdf .
'''
return ( 0.432699 * math . log ( x + 0.037584 , 10.0 ) + 0.616596 ) + 0.03
def sLog2 ( x ) :
'''
sLog2 formula . Domain must be within [ 0 , 1 ] . See https : / / pro . sony . com / bbsccms / assets / files / micro / dmpc / training / S - Log2_Technical_PaperV1_0 . pdf .
'''
return ( 0.432699 * math . log ( ( 155.0 * x ) / 219.0 + 0.037584 , 10.0 ) + 0.616596 ) + 0.03
def sLog3 ( x ) :
'''
Not yet implemented . See http : / / community . sony . com / sony / attachments / sony / large - sensor - camera - F5 - F55 / 12359 / 2 / TechnicalSummary_for_S - Gamut3Cine_S - Gamut3_S - Log3_V1_00 . pdf .
'''
return x
def DanLog ( x ) :
return ( 10.0 * * ( ( x - 0.385537 ) / 0.2471896 ) - 0.071272 ) / 3.555556 if x > 0.1496582 else ( x - 0.092809 ) / 5.367655
def DanLoginv ( x ) :
pass
class TransMat ( Transform ) :
def __init__ ( self , * mats ) :
'''
Initializes a combined 3 x3 Transformation Matrix from any number of input matrices . These may be numpy arrays , matrices ,
other TransMats , or any combination thereof .
'''
if len ( mats ) == 1 :
mat = mats [ 0 ]
if isinstance ( mat , TransMat ) :
self . mat = mat . mat #Support a copy constructor.
else :
self . mat = np . array ( mat ) #Simply set self.mat with the numpy array version of the mat.
elif len ( mats ) > 1 :
self . mat = TransMat . __mats ( * [ TransMat ( mat ) for mat in mats ] ) . mat
elif not mats :
self . mat = np . identity ( 3 )
def __mats ( * inMats ) :
'''
Initialize a combined Transform matrix from several input TransMats .
'''
return TransMat ( reduce ( TransMat . __mul__ , reversed ( inMats ) ) ) #Works because multiply is actually non-commutative dot.
#This is why we reverse inMats.
@numba.jit ( nopython = True )
def __optDot ( img , mat , shp , out ) :
shaped = img . reshape ( ( shp [ 0 ] * shp [ 1 ] , shp [ 2 ] ) ) #Flatten to 2D array for iteration over colors.
i = 0
while i < shp [ 0 ] * shp [ 1 ] :
res = np . dot ( mat , shaped [ i ] )
out [ i ] = res
i + = 1
def __applMat ( q , cpu , shp , mat , img3D ) :
out = np . zeros ( ( shp [ 0 ] * shp [ 1 ] , shp [ 2 ] ) )
TransMat . __optDot ( img3D , mat , shp , out )
q . put ( ( cpu , out . reshape ( shp ) ) )
def sample ( self , fSeq ) :
shp = np . shape ( fSeq )
if len ( shp ) == 1 :
return self . mat . dot ( fSeq )
if len ( shp ) == 3 :
cpus = mp . cpu_count ( )
out = [ ]
q = mp . Queue ( )
splt = Transform . spSeq ( fSeq , cpus )
for cpu in range ( cpus ) :
p = mp . Process ( target = TransMat . __applMat , args = ( q , cpu , np . shape ( splt [ cpu ] ) , self . mat , splt [ cpu ] ) )
p . start ( )
for num in range ( len ( splt ) ) :
out . append ( q . get ( ) )
return np . concatenate ( [ seq [ 1 ] for seq in sorted ( out , key = lambda seq : seq [ 0 ] ) ] , axis = 0 )
#~ out = np.zeros((shp[0] * shp[1], shp[2]))
#~ TransMat.__optDot(fSeq, self.mat, shp, out)
#~ return out.reshape(shp)
#~ return np.array([self.mat.dot(col) for col in fSeq.reshape(shp[0] * shp[1], shp[2])]).reshape(shp)
#~ p = mp.Pool(mp.cpu_count())
#~ return np.array(list(map(self.mat.dot, fSeq.reshape(shp[0] * shp[1], shp[2])))).reshape(shp)
#~ return fSeq.dot(self.mat)
def inv ( obj ) :
if isinstance ( obj , TransMat ) : #Works on any TransMat object - including self.
return TransMat ( np . linalg . inv ( obj . mat ) )
else : #Works on raw numpy arrays as well.
return np . linalg . inv ( obj )
def transpose ( self ) :
return TransMat ( np . transpose ( self . mat ) )
#Overloading
def __mul__ ( self , other ) :
'''
* implements matrix multiplication .
'''
if isinstance ( other , TransMat ) :
return TransMat ( self . mat . dot ( other . mat ) )
elif isinstance ( other , float ) or isinstance ( other , int ) :
return TransMat ( np . multiply ( self . mat , other ) )
elif isinstance ( other , np . ndarray ) or isinstance ( other , np . matrixlib . defmatrix . matrix ) :
return TransMat ( self . mat . dot ( np . array ( other ) ) )
else :
raise ValueError ( ' Invalid multiplication arguments! ' )
__rmul__ = __mul__
def __add__ ( self , other ) :
if isinstance ( other , TransMat ) :
return TransMat ( self . mat + other . mat )
elif isinstance ( other , np . ndarray ) or isinstance ( other , np . matrixlib . defmatrix . matrix ) :
return TransMat ( self . mat + np . array ( other ) )
else :
raise ValueError ( ' Invalid addition arguments! ' )
__radd__ = __add__
def __pow__ ( self , other ) :
'''
* * implements direct multiplication . You usually don ' t want this.
'''
if isinstance ( other , TransMat ) :
return TransMat ( self . mat * other . mat )
elif isinstance ( other , float ) or isinstance ( other , int ) :
return TransMat ( np . multiply ( self . mat , other ) )
elif isinstance ( other , np . ndarray ) or isinstance ( other , np . matrixlib . defmatrix . matrix ) :
return TransMat ( self . mat * np . array ( other ) )
else :
raise ValueError ( ' Invalid multiplication arguments! ' )
def __invert__ ( self ) :
return self . inv ( )
def __len__ ( self ) :
return len ( self . mat )
def __getitem__ ( self , key ) :
return self . mat [ key ]
def __iter__ ( self ) :
return iter ( self . mat )
def __repr__ ( self ) :
return " \n TransMat ( \n {0} ) \n " . format ( str ( self . mat ) )
#Static Transmat Matrices -- all go from ACES to <gamut name>. <gamut name>inv functions go from the gamut to ACES.
#Converted (CIECAT02) D65 Illuminant for all.
XYZ = np . array (
[
0.93863095 , - 0.00574192 , 0.0175669 ,
0.33809359 , 0.7272139 , - 0.0653075 ,
0.00072312 , 0.00081844 , 1.08751619
]
) . reshape ( 3 , 3 )
XYZinv = np . array (
[
1.06236611 , 0.00840695 , - 0.01665579 ,
- 0.49394137 , 1.37110953 , 0.09031659 ,
- 0.00033467 , - 0.00103746 , 0.91946965
]
) . reshape ( 3 , 3 )
sRGB = np . array (
[
2.52193473 , - 1.1370239 , - 0.38491083 ,
- 0.27547943 , 1.36982898 , - 0.09434955 ,
- 0.01598287 , - 0.14778923 , 1.1637721
]
) . reshape ( 3 , 3 )
sRGBinv = np . array (
[
0.43957568 , 0.38391259 , 0.17651173 ,
0.08960038 , 0.81471415 , 0.09568546 ,
0.01741548 , 0.10873435 , 0.87385017
]
) . reshape ( 3 , 3 )
aRGB = np . array (
[
1.72502307 , - 0.4228857 , - 0.30213736 ,
- 0.27547943 , 1.36982898 , - 0.09434955 ,
- 0.02666425 , - 0.08532111 , 1.11198537
]
) . reshape ( 3 , 3 )
aRGBinv = np . array (
[
0.61468318 , 0.20122762 , 0.1840892 ,
0.12529321 , 0.77491365 , 0.09979314 ,
0.02435304 , 0.06428329 , 0.91136367
]
) . reshape ( 3 , 3 )
if __name__ == " __main__ " :
if not sys . argv : print ( ' Use -t to test! ' )
if sys . argv [ 1 ] == ' -t ' :
print ( ' Open openlut.py and scroll down to the end to see the code that \' s working! ' )
#Open any format image. Try it with exr/dpx/anything!
img = ColMap . open ( ' testpath/test.exr ' ) #Opens a test image 'test.exr', creating a ColMap object, automatically using the best image backend available to load the image at the correct bit depth.
'''
Gamma has gamma functions like Gamma . sRGB , called by value like Gamma . sRGB ( val ) . All take one argument , the value ( x ) , and returns the transformed value . Color doesn ' t matter for gamma.
TransMat has matrices , in 3 x3 numpy array form . All are relative to ACES , with direction aptly named . So , TransMat . XYZ is a matrix from ACES - - > XYZ , while TransMat . XYZinv goes from XYZ - - > ACES . All use / are converted to the D65 illuminant , for consistency sake .
'''
#Gamma Functions: sRGB --> Linear.
gFunc = Gamma ( Gamma . sRGBinv ) #A Gamma Transform object using the sRGB-->Linear gamma formula. Apply to ColMaps!
gFuncManualsRGB = Gamma ( lambda val : ( ( val + 0.055 ) / 1.055 ) * * 2.4 if val > 0.04045 else val / 12.92 ) #It's generic - specify any gamma function, even inline with a lambda!
#LUT from Function: sRGB --> Linear
oLut = LUT . lutFunc ( Gamma . sRGBinv ) #A LUT Transform object, created from a gamma function. Size is 16384 by default. LUTs are faster!
oLut . save ( ' testpath/sRGB-->Lin.cube ' ) #Saves the LUT to a format inferred from the extension. cube only for now!
#Opening LUTs from .cube files.
lut = LUT . open ( ' testpath/sRGB-->Lin.cube ' ) #Opens the lut we just made into a different LUT object.
lut . resized ( 17 ) . save ( ' testpath/sRGB-->Lin_tiny.cube ' ) #Resizes the LUT, then saves it again to a much smaller file!
#Matrix Transformations
simpleMat = TransMat ( TransMat . sRGBinv ) #A Matrix Transform (TransMat) object, created from a color transform matrix for gamut transformations! This one is sRGB --> ACES.
mat = TransMat ( TransMat . sRGBinv , TransMat . XYZ , TransMat . XYZinv , TransMat . aRGB ) * TransMat . aRGBinv
#Indeed, specify many matrices which auto-multiply into a single one! You can also combine them after, with simple multiplication.
#Applying and saving.
img . apply ( gFunc ) . save ( ' testpath/openlut_gammafunc.png ' ) #save saves an image using the appropriate image backend, based on the extension.
img . apply ( lut ) . save ( ' testpath/openlut_lut-lin-16384.png ' ) #apply applies any color transformation object that inherits from Transform - LUT, Gamma, TransMat, etc., or make your own! It's easy ;) .
img . apply ( lut . resized ( 17 ) ) . save ( ' testpath/openlut_lut-lin-17.png ' ) #Why so small? Because spline interpolation automatically turns on. It's identical to the larger LUT!
img . apply ( mat ) . save ( ' testpath/openlut_mat.png ' ) #Applies the gamut transformation.
#As a proof of concept, here's a long list of transformations that should, in sum, do nothing :) :
img . apply ( lut ) . apply ( LUT . lutFunc ( Gamma . sRGB ) ) . apply ( mat ) . apply ( ~ mat ) . save ( ' testpath/openlut_noop.png ' ) #~mat is the inverse of mat. Easily undo the gamut operation!
#Format Test: All output images are in Linear ACES.
tImg = img . apply ( mat )
tImg . save ( ' testpath/output.exr ' )
tImg . save ( ' testpath/output.dpx ' )
tImg . save ( ' testpath/output.png ' )
tImg . save ( ' testpath/output.jpg ' )
tImg . save ( ' testpath/output.tif ' ) #All sorts of formats work! Bit depth is 16, unless you say something else.
#Compression is impossible right now - wand is being difficult.
#Keep in mind, values are clipped from 0 to 1 when done. Scary transforms can make this an issue!
#Color management is simple: openlut doesn't touch your data, unless you tell it to with a Transform. So, the data that goes in, goes out, unless a Transform was applied.