├── requirements.txt ├── .gitignore ├── README.md ├── NOTES.md ├── hald_to_3dl.py └── hald_to_cube.py /requirements.txt: -------------------------------------------------------------------------------- 1 | PIL 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /venv 2 | *.pyc 3 | *.png 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | LUT Converter 2 | ============= 3 | 4 | This project contains a Python script which can convert Hald LUTs to the `.cube` format for use in Adobe Photoshop (>= CS 6). 5 | 6 | Create a HALD image: 7 | 8 | ~~~ 9 | convert hald:17 identity.png 10 | ~~~ 11 | 12 | Convert it to `cube` format: 13 | 14 | ~~~ 15 | python hald_to_cube.py identity.png identity.cube 16 | ~~~ 17 | 18 | [Read the notes.](NOTES.md) 19 | -------------------------------------------------------------------------------- /NOTES.md: -------------------------------------------------------------------------------- 1 | General 2 | ======= 3 | 4 | - There is a LOT more info in a Hald image than we pass into the *.cube: 5 | - `convert hald:5 ident_5.png` results in a 25x25 image; 625 values 6 | 7 | - Do we want to target 3dl at all? It seems that the 10-bit nature will lead to rounding errors. 8 | 9 | 10 | Formats 11 | ======= 12 | 13 | 3DL 14 | --- 15 | 16 | # Optional(?) header: 17 | 3DMESH 18 | Mesh 4 12 19 | 20 | # Line of the input values: 17 of them in this case. 21 | 0 64 128 192 256 320 384 448 512 575 639 703 767 831 895 959 1023 22 | 23 | # Integer output values. 24 | # for r in xrange(18): 25 | # for g in xrange(18): 26 | # for b in xrange(18): 27 | $R_out $G_out $B_out 28 | 29 | 30 | CUBE 31 | ---- 32 | 33 | #Created by: Adobe Photoshop CS6 34 | #Copyright: Copyright 2012 Adobe Systems Inc. 35 | TITLE "Night from Day" 36 | 37 | #LUT size 38 | LUT_3D_SIZE 17 39 | 40 | #data domain 41 | DOMAIN_MIN 0.0 0.0 0.0 42 | DOMAIN_MAX 1.0 1.0 1.0 43 | 44 | # Float output values. 45 | # for b in xrange(18): 46 | # for g in xrange(18): 47 | # for r in xrange(18): 48 | $R_out $G_out $B_out 49 | -------------------------------------------------------------------------------- /hald_to_3dl.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, division 2 | 3 | import math 4 | import sys 5 | from optparse import OptionParser 6 | 7 | from PIL import Image 8 | 9 | 10 | def main(): 11 | 12 | print("Warning: This script does not return accurate results.", file=sys.stderr) 13 | 14 | opt_parser = OptionParser(usage='%prog [options] input.png output.3dl') 15 | opts, args = opt_parser.parse_args() 16 | 17 | if len(args) != 2: 18 | opt_parser.print_usage() 19 | exit(1) 20 | 21 | in_ = Image.open(args[0]) 22 | w, h = in_.size 23 | if w != h: 24 | print('HALD input is not square.', file=sys.stderr) 25 | exit(2) 26 | steps = int(round(math.pow(w, 1/3))) 27 | if steps ** 3 != w: 28 | print('HALD input size is invalid: %d is not a cube.' % w, file=sys.stderr) 29 | print('%d steps' % steps, file=sys.stderr) 30 | # Assume that we are going from 8 bits to 10. 31 | 32 | out = open(args[1], 'w') 33 | header = [1023 * i // (steps - 1) for i in xrange(steps)] 34 | out.write(' '.join(str(x) for x in header)) 35 | out.write('\n') 36 | 37 | steps1 = steps + 1 38 | steps3 = steps ** 2 * (steps + 1) 39 | steps5 = steps ** 4 * (steps + 1) 40 | data = list(in_.getdata()) 41 | def lookup(ri, gi, bi): 42 | return data[ 43 | ri * steps1 + gi * steps3 + bi * steps5 44 | ] 45 | for ri in xrange(steps): 46 | for gi in xrange(steps): 47 | for bi in xrange(steps): 48 | r, g, b = lookup(ri, gi, bi) 49 | out.write('%d %d %d\n' % (r * 4, g * 4, b * 4)) 50 | 51 | 52 | 53 | if __name__ == '__main__': 54 | main() 55 | -------------------------------------------------------------------------------- /hald_to_cube.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, division 2 | 3 | import math 4 | import sys 5 | from optparse import OptionParser 6 | 7 | from PIL import Image 8 | 9 | 10 | def main(): 11 | 12 | opt_parser = OptionParser(usage='%prog [options] input.png output.cube') 13 | opts, args = opt_parser.parse_args() 14 | 15 | if len(args) != 2: 16 | opt_parser.print_usage() 17 | exit(1) 18 | 19 | in_ = Image.open(args[0]) 20 | w, h = in_.size 21 | if w != h: 22 | print('HALD input is not square.', file=sys.stderr) 23 | exit(2) 24 | steps = int(round(math.pow(w, 1/3))) 25 | if steps ** 3 != w: 26 | print('HALD input size is invalid: %d is not a cube.' % w, file=sys.stderr) 27 | 28 | print('%d steps -> %d values' % (steps, steps**6), file=sys.stderr) 29 | # Assume that we are going from 8 bits to 10. 30 | 31 | out = open(args[1], 'w') 32 | #out.write('#Created by: Adobe Photoshop CS6\n') 33 | #out.write('#Copyright: Copyright 2012 Adobe Systems Inc.\n') 34 | #out.write('TITLE "Testing"\n') 35 | #out.write('\n') 36 | #out.write('#LUT size\n') 37 | out.write('LUT_3D_SIZE %d\n' % (steps ** 2)) 38 | #out.write('\n') 39 | #out.write('#data domain\n') 40 | out.write('DOMAIN_MIN 0.0 0.0 0.0\n') 41 | out.write('DOMAIN_MAX 1.0 1.0 1.0\n') 42 | #out.write('\n') 43 | #out.write('#LUT data points\n') 44 | 45 | if False: 46 | steps1 = steps + 1 47 | steps3 = steps ** 2 * (steps + 1) 48 | steps5 = steps ** 4 * (steps + 1) 49 | data = list(in_.getdata()) 50 | def lookup(ri, gi, bi): 51 | return data[ 52 | ri * steps1 + gi * steps3 + bi * steps5 53 | ] 54 | for bi in xrange(steps): 55 | for gi in xrange(steps): 56 | for ri in xrange(steps): 57 | r, g, b = lookup(ri, gi, bi)[:3] 58 | out.write('%f %f %f\n' % (r / 255.0, g / 255.0, b / 255.0)) 59 | else: 60 | for pixel in in_.getdata(): 61 | r, g, b = pixel[:3] 62 | out.write('%f %f %f\n' % (r / 255.0, g / 255.0, b / 255.0)) 63 | 64 | 65 | 66 | 67 | if __name__ == '__main__': 68 | main() 69 | --------------------------------------------------------------------------------