2 # -*- coding: utf-8 -*-
5 Implements support for *ACES* colorspaces conversions and transfer functions.
14 import PyOpenColorIO as ocio
16 import aces_ocio.generate_lut as genlut
17 from aces_ocio.generate_lut import (
18 generate_1d_LUT_from_CTL,
19 generate_3d_LUT_from_CTL,
21 from aces_ocio.utilities import ColorSpace, mat44_from_mat33, sanitize_path, compact
24 __author__ = 'ACES Developers'
25 __copyright__ = 'Copyright (C) 2014 - 2015 - ACES Developers'
27 __maintainer__ = 'ACES Developers'
28 __email__ = 'aces@oscars.org'
29 __status__ = 'Production'
31 __all__ = ['create_ACEScc',
38 'create_ACES_RRT_plus_ODT',
43 # -------------------------------------------------------------------------
45 # -------------------------------------------------------------------------
47 # Matrix converting *ACES AP1* primaries to *AP0*.
48 ACES_AP1_to_AP0 = [0.6954522414, 0.1406786965, 0.1638690622,
49 0.0447945634, 0.8596711185, 0.0955343182,
50 -0.0055258826, 0.0040252103, 1.0015006723]
52 # Matrix converting *ACES AP0* primaries to *XYZ*.
53 ACES_AP0_to_XYZ = [0.9525523959, 0.0000000000, 0.0000936786,
54 0.3439664498, 0.7281660966, -0.0721325464,
55 0.0000000000, 0.0000000000, 1.0088251844]
57 # -------------------------------------------------------------------------
59 # -------------------------------------------------------------------------
60 def create_ACEScc(aces_CTL_directory,
69 cs.description = 'The %s color space' % name
70 cs.aliases = ["acescc_ap1"]
71 cs.equality_group = ''
75 ctls = [os.path.join(aces_CTL_directory,
77 'ACEScsc.ACEScc_to_ACES.a1.0.0.ctl'),
78 # This transform gets back to the *AP1* primaries.
79 # Useful as the 1d LUT is only covering the transfer function.
80 # The primaries switch is covered by the matrix below:
81 os.path.join(aces_CTL_directory,
83 'ACEScsc.ACES_to_ACEScg.a1.0.0.ctl')]
84 lut = '%s_to_ACES.spi1d' % name
86 lut = sanitize_path(lut)
88 generate_1d_LUT_from_CTL(
89 os.path.join(lut_directory, lut),
101 cs.to_reference_transforms = []
102 cs.to_reference_transforms.append({
105 'interpolation': 'linear',
106 'direction': 'forward'})
108 # *AP1* primaries to *AP0* primaries.
109 cs.to_reference_transforms.append({
111 'matrix': mat44_from_mat33(ACES_AP1_to_AP0),
112 'direction': 'forward'})
114 cs.from_reference_transforms = []
118 # -------------------------------------------------------------------------
120 # -------------------------------------------------------------------------
121 def create_ACESproxy(aces_CTL_directory,
126 cs = ColorSpace(name)
127 cs.description = 'The %s color space' % name
128 cs.aliases = ["acesproxy_ap1"]
129 cs.equality_group = ''
133 ctls = [os.path.join(aces_CTL_directory,
135 'ACEScsc.ACESproxy10i_to_ACES.a1.0.0.ctl'),
136 # This transform gets back to the *AP1* primaries.
137 # Useful as the 1d LUT is only covering the transfer function.
138 # The primaries switch is covered by the matrix below:
139 os.path.join(aces_CTL_directory,
141 'ACEScsc.ACES_to_ACEScg.a1.0.0.ctl')]
142 lut = '%s_to_aces.spi1d' % name
144 lut = sanitize_path(lut)
146 generate_1d_LUT_from_CTL(
147 os.path.join(lut_directory, lut),
157 cs.to_reference_transforms = []
158 cs.to_reference_transforms.append({
161 'interpolation': 'linear',
162 'direction': 'forward'
165 # *AP1* primaries to *AP0* primaries.
166 cs.to_reference_transforms.append({
168 'matrix': mat44_from_mat33(ACES_AP1_to_AP0),
169 'direction': 'forward'
172 cs.from_reference_transforms = []
175 # -------------------------------------------------------------------------
177 # -------------------------------------------------------------------------
178 def create_ACEScg(aces_CTL_directory,
183 cs = ColorSpace(name)
184 cs.description = 'The %s color space' % name
185 cs.aliases = ["lin_ap1"]
186 cs.equality_group = ''
190 cs.to_reference_transforms = []
192 # *AP1* primaries to *AP0* primaries.
193 cs.to_reference_transforms.append({
195 'matrix': mat44_from_mat33(ACES_AP1_to_AP0),
196 'direction': 'forward'
199 cs.from_reference_transforms = []
202 # -------------------------------------------------------------------------
204 # -------------------------------------------------------------------------
205 def create_ADX(lut_directory,
209 name = '%s%s' % (name, bit_depth)
210 cs = ColorSpace(name)
211 cs.description = '%s color space - used for film scans' % name
212 cs.aliases = ["adx%s" % str(bit_depth)]
213 cs.equality_group = ''
218 cs.bit_depth = ocio.Constants.BIT_DEPTH_UINT10
219 adx_to_cdd = [1023.0 / 500.0, 0.0, 0.0, 0.0,
220 0.0, 1023.0 / 500.0, 0.0, 0.0,
221 0.0, 0.0, 1023.0 / 500.0, 0.0,
223 offset = [-95.0 / 500.0, -95.0 / 500.0, -95.0 / 500.0, 0.0]
224 elif bit_depth == 16:
225 cs.bit_depth = ocio.Constants.BIT_DEPTH_UINT16
226 adx_to_cdd = [65535.0 / 8000.0, 0.0, 0.0, 0.0,
227 0.0, 65535.0 / 8000.0, 0.0, 0.0,
228 0.0, 0.0, 65535.0 / 8000.0, 0.0,
230 offset = [-1520.0 / 8000.0, -1520.0 / 8000.0, -1520.0 / 8000.0,
233 cs.to_reference_transforms = []
235 # Converting from *ADX* to *Channel-Dependent Density*.
236 cs.to_reference_transforms.append({
238 'matrix': adx_to_cdd,
240 'direction': 'forward'})
242 # Convert from Channel-Dependent Density to Channel-Independent Density
243 cs.to_reference_transforms.append({
245 'matrix': [0.75573, 0.22197, 0.02230, 0,
246 0.05901, 0.96928, -0.02829, 0,
247 0.16134, 0.07406, 0.76460, 0,
249 'direction': 'forward'})
251 # Copied from *Alex Fry*'s *adx_cid_to_rle.py*
252 def create_CID_to_RLE_LUT():
254 def interpolate_1D(x, xp, fp):
255 return numpy.interp(x, xp, fp)
257 LUT_1D_xp = [-0.190000000000000,
269 LUT_1D_fp = [-6.000000000000000,
281 REF_PT = ((7120.0 - 1520.0) / 8000.0 * (100.0 / 55.0) -
282 math.log(0.18, 10.0))
286 return interpolate_1D(x, LUT_1D_xp, LUT_1D_fp)
287 return (100.0 / 55.0) * x - REF_PT
289 def fit(value, from_min, from_max, to_min, to_max):
290 if from_min == from_max:
291 raise ValueError('from_min == from_max')
292 return (value - from_min) / (from_max - from_min) * (
293 to_max - to_min) + to_min
295 NUM_SAMPLES = 2 ** 12
298 for i in xrange(NUM_SAMPLES):
299 x = i / (NUM_SAMPLES - 1.0)
300 x = fit(x, 0.0, 1.0, RANGE[0], RANGE[1])
301 data.append(cid_to_rle(x))
303 lut = 'ADX_CID_to_RLE.spi1d'
304 write_SPI_1d(os.path.join(lut_directory, lut),
312 # Converting *Channel Independent Density* values to
313 # *Relative Log Exposure* values.
314 lut = create_CID_to_RLE_LUT()
315 cs.to_reference_transforms.append({
318 'interpolation': 'linear',
319 'direction': 'forward'})
321 # Converting *Relative Log Exposure* values to
322 # *Relative Exposure* values.
323 cs.to_reference_transforms.append({
326 'direction': 'inverse'})
328 # Convert *Relative Exposure* values to *ACES* values.
329 cs.to_reference_transforms.append({
331 'matrix': [0.72286, 0.12630, 0.15084, 0,
332 0.11923, 0.76418, 0.11659, 0,
333 0.01427, 0.08213, 0.90359, 0,
335 'direction': 'forward'})
337 cs.from_reference_transforms = []
340 # -------------------------------------------------------------------------
341 # *Generic Log Transform*
342 # -------------------------------------------------------------------------
343 def create_generic_log(aces_CTL_directory,
355 cs = ColorSpace(name)
356 cs.description = 'The %s color space' % name
358 cs.equality_group = name
359 cs.family = 'Utility'
362 ctls = [os.path.join(
365 'ACESlib.OCIO_shaper_log2_to_lin_param.a1.0.0.ctl')]
366 lut = '%s_to_aces.spi1d' % name
368 lut = sanitize_path(lut)
370 generate_1d_LUT_from_CTL(
371 os.path.join(lut_directory, lut),
377 {'middleGrey': middle_grey,
378 'minExposure': min_exposure,
379 'maxExposure': max_exposure},
385 cs.to_reference_transforms = []
386 cs.to_reference_transforms.append({
389 'interpolation': 'linear',
390 'direction': 'forward'})
392 cs.from_reference_transforms = []
396 # -------------------------------------------------------------------------
398 # -------------------------------------------------------------------------
399 def create_ACES_LMT(lmt_name,
404 lut_resolution_1d=1024,
405 lut_resolution_3d=64,
408 cs = ColorSpace('%s' % lmt_name)
409 cs.description = 'The ACES Look Transform: %s' % lmt_name
411 cs.equality_group = ''
415 pprint.pprint(lmt_values)
417 # Generating the *shaper* transform.
420 shaper_from_ACES_CTL,
422 shaper_params) = shaper_info
424 shaper_lut = '%s_to_aces.spi1d' % shaper_name
425 if not os.path.exists(os.path.join(lut_directory, shaper_lut)):
426 ctls = [shaper_to_ACES_CTL % aces_CTL_directory]
428 shaper_lut = sanitize_path(shaper_lut)
430 generate_1d_LUT_from_CTL(
431 os.path.join(lut_directory, shaper_lut),
435 1.0 / shaper_input_scale,
441 shaper_OCIO_transform = {
444 'interpolation': 'linear',
445 'direction': 'inverse'}
447 # Generating the forward transform.
448 cs.from_reference_transforms = []
450 if 'transformCTL' in lmt_values:
451 ctls = [shaper_to_ACES_CTL % aces_CTL_directory,
452 os.path.join(aces_CTL_directory,
453 lmt_values['transformCTL'])]
454 lut = '%s.%s.spi3d' % (shaper_name, lmt_name)
456 lut = sanitize_path(lut)
458 generate_3d_LUT_from_CTL(
459 os.path.join(lut_directory, lut),
463 1.0 / shaper_input_scale,
469 cs.from_reference_transforms.append(shaper_OCIO_transform)
470 cs.from_reference_transforms.append({
473 'interpolation': 'tetrahedral',
474 'direction': 'forward'
477 # Generating the inverse transform.
478 cs.to_reference_transforms = []
480 if 'transformCTLInverse' in lmt_values:
481 ctls = [os.path.join(aces_CTL_directory,
482 odt_values['transformCTLInverse']),
483 shaper_from_ACES_CTL % aces_CTL_directory]
484 lut = 'Inverse.%s.%s.spi3d' % (odt_name, shaper_name)
486 lut = sanitize_path(lut)
488 generate_3d_LUT_from_CTL(
489 os.path.join(lut_directory, lut),
499 cs.to_reference_transforms.append({
502 'interpolation': 'tetrahedral',
503 'direction': 'forward'})
505 shaper_inverse = shaper_OCIO_transform.copy()
506 shaper_inverse['direction'] = 'forward'
507 cs.to_reference_transforms.append(shaper_inverse)
511 # -------------------------------------------------------------------------
513 # -------------------------------------------------------------------------
514 def create_lmts(aces_CTL_directory,
524 # -------------------------------------------------------------------------
526 # -------------------------------------------------------------------------
527 lmt_lut_resolution_1d = max(4096, lut_resolution_1d)
528 lmt_lut_resolution_3d = max(65, lut_resolution_3d)
530 # Defining the *Log 2* shaper.
531 lmt_shaper_name = 'LMT Shaper'
532 lmt_shaper_name_aliases = ['crv_lmtshaper']
535 'minExposure': -10.0,
538 lmt_shaper = create_generic_log(aces_CTL_directory,
540 lmt_lut_resolution_1d,
542 name=lmt_shaper_name,
543 middle_grey=lmt_params['middleGrey'],
544 min_exposure=lmt_params['minExposure'],
545 max_exposure=lmt_params['maxExposure'],
546 aliases=lmt_shaper_name_aliases)
547 colorspaces.append(lmt_shaper)
549 shaper_input_scale_generic_log2 = 1.0
551 # *Log 2* shaper name and *CTL* transforms bundled up.
556 'ACESlib.OCIO_shaper_log2_to_lin_param.a1.0.0.ctl'),
559 'ACESlib.OCIO_shaper_lin_to_log2_param.a1.0.0.ctl'),
560 shaper_input_scale_generic_log2,
563 sorted_LMTs = sorted(lmt_info.iteritems(), key=lambda x: x[1])
565 for lmt in sorted_LMTs:
566 (lmt_name, lmt_values) = lmt
567 lmt_aliases = ["look_%s" % compact(lmt_values['transformUserName'])]
568 cs = create_ACES_LMT(
569 lmt_values['transformUserName'],
574 lmt_lut_resolution_1d,
575 lmt_lut_resolution_3d,
578 colorspaces.append(cs)
582 # -------------------------------------------------------------------------
583 # *ACES RRT* with supplied *ODT*.
584 # -------------------------------------------------------------------------
585 def create_ACES_RRT_plus_ODT(odt_name,
590 lut_resolution_1d=1024,
591 lut_resolution_3d=64,
594 cs = ColorSpace('%s' % odt_name)
595 cs.description = '%s - %s Output Transform' % (
596 odt_values['transformUserNamePrefix'], odt_name)
598 cs.equality_group = ''
602 pprint.pprint(odt_values)
604 # Generating the *shaper* transform.
607 shaper_from_ACES_CTL,
609 shaper_params) = shaper_info
611 if 'legalRange' in odt_values:
612 shaper_params['legalRange'] = odt_values['legalRange']
614 shaper_params['legalRange'] = 0
616 shaper_lut = '%s_to_aces.spi1d' % shaper_name
617 if not os.path.exists(os.path.join(lut_directory, shaper_lut)):
618 ctls = [shaper_to_ACES_CTL % aces_CTL_directory]
620 shaper_lut = sanitize_path(shaper_lut)
622 generate_1d_LUT_from_CTL(
623 os.path.join(lut_directory, shaper_lut),
627 1.0 / shaper_input_scale,
633 shaper_OCIO_transform = {
636 'interpolation': 'linear',
637 'direction': 'inverse'}
639 # Generating the *forward* transform.
640 cs.from_reference_transforms = []
642 if 'transformLUT' in odt_values:
643 transform_LUT_file_name = os.path.basename(
644 odt_values['transformLUT'])
645 lut = os.path.join(lut_directory, transform_LUT_file_name)
646 shutil.copy(odt_values['transformLUT'], lut)
648 cs.from_reference_transforms.append(shaper_OCIO_transform)
649 cs.from_reference_transforms.append({
651 'path': transform_LUT_file_name,
652 'interpolation': 'tetrahedral',
653 'direction': 'forward'})
654 elif 'transformCTL' in odt_values:
656 shaper_to_ACES_CTL % aces_CTL_directory,
657 os.path.join(aces_CTL_directory,
660 os.path.join(aces_CTL_directory,
662 odt_values['transformCTL'])]
663 lut = '%s.RRT.a1.0.0.%s.spi3d' % (shaper_name, odt_name)
665 lut = sanitize_path(lut)
667 generate_3d_LUT_from_CTL(
668 os.path.join(lut_directory, lut),
673 1.0 / shaper_input_scale,
679 cs.from_reference_transforms.append(shaper_OCIO_transform)
680 cs.from_reference_transforms.append({
683 'interpolation': 'tetrahedral',
684 'direction': 'forward'})
686 # Generating the *inverse* transform.
687 cs.to_reference_transforms = []
689 if 'transformLUTInverse' in odt_values:
690 transform_LUT_inverse_file_name = os.path.basename(
691 odt_values['transformLUTInverse'])
692 lut = os.path.join(lut_directory, transform_LUT_inverse_file_name)
693 shutil.copy(odt_values['transformLUTInverse'], lut)
695 cs.to_reference_transforms.append({
697 'path': transform_LUT_inverse_file_name,
698 'interpolation': 'tetrahedral',
699 'direction': 'forward'})
701 shaper_inverse = shaper_OCIO_transform.copy()
702 shaper_inverse['direction'] = 'forward'
703 cs.to_reference_transforms.append(shaper_inverse)
704 elif 'transformCTLInverse' in odt_values:
705 ctls = [os.path.join(aces_CTL_directory,
707 odt_values['transformCTLInverse']),
708 os.path.join(aces_CTL_directory,
710 'InvRRT.a1.0.0.ctl'),
711 shaper_from_ACES_CTL % aces_CTL_directory]
712 lut = 'InvRRT.a1.0.0.%s.%s.spi3d' % (odt_name, shaper_name)
714 lut = sanitize_path(lut)
716 generate_3d_LUT_from_CTL(
717 os.path.join(lut_directory, lut),
728 cs.to_reference_transforms.append({
731 'interpolation': 'tetrahedral',
732 'direction': 'forward'})
734 shaper_inverse = shaper_OCIO_transform.copy()
735 shaper_inverse['direction'] = 'forward'
736 cs.to_reference_transforms.append(shaper_inverse)
740 # -------------------------------------------------------------------------
742 # -------------------------------------------------------------------------
743 def create_odts(aces_CTL_directory,
750 linear_display_space,
756 # -------------------------------------------------------------------------
757 # *RRT / ODT* Shaper Options
758 # -------------------------------------------------------------------------
761 # Defining the *Log 2* shaper.
762 log2_shaper_name = shaper_name
763 log2_shaper_name_aliases = ["crv_%s" % compact(shaper_name)]
769 log2_shaper = create_generic_log(
774 name=log2_shaper_name,
775 middle_grey=log2_params['middleGrey'],
776 min_exposure=log2_params['minExposure'],
777 max_exposure=log2_params['maxExposure'],
778 aliases=log2_shaper_name_aliases)
779 colorspaces.append(log2_shaper)
781 shaper_input_scale_generic_log2 = 1.0
783 # *Log 2* shaper name and *CTL* transforms bundled up.
788 'ACESlib.OCIO_shaper_log2_to_lin_param.a1.0.0.ctl'),
791 'ACESlib.OCIO_shaper_lin_to_log2_param.a1.0.0.ctl'),
792 shaper_input_scale_generic_log2,
795 shaper_data[log2_shaper_name] = log2_shaper_data
797 # Shaper that also includes the AP1 primaries.
798 # Needed for some LUT baking steps.
799 log2_shaper_api1_name_aliases = ["%s_ap1" % compact(shaper_name)]
800 log2_shaper_AP1 = create_generic_log(
805 name=log2_shaper_name,
806 middle_grey=log2_params['middleGrey'],
807 min_exposure=log2_params['minExposure'],
808 max_exposure=log2_params['maxExposure'],
809 aliases=log2_shaper_api1_name_aliases)
810 log2_shaper_AP1.name = '%s - AP1' % log2_shaper_AP1.name
812 # *AP1* primaries to *AP0* primaries.
813 log2_shaper_AP1.to_reference_transforms.append({
815 'matrix': mat44_from_mat33(ACES_AP1_to_AP0),
816 'direction': 'forward'
818 colorspaces.append(log2_shaper_AP1)
820 rrt_shaper = log2_shaper_data
822 # *RRT + ODT* combinations.
823 sorted_odts = sorted(odt_info.iteritems(), key=lambda x: x[1])
825 for odt in sorted_odts:
826 (odt_name, odt_values) = odt
828 # Handling *ODTs* that can generate either *legal* or *full* output.
829 if odt_name in ['Academy.Rec2020_100nits_dim.a1.0.0',
830 'Academy.Rec709_100nits_dim.a1.0.0',
831 'Academy.Rec709_D60sim_100nits_dim.a1.0.0']:
832 odt_name_legal = '%s - Legal' % odt_values['transformUserName']
834 odt_name_legal = odt_values['transformUserName']
836 odt_legal = odt_values.copy()
837 odt_legal['legalRange'] = 1
839 odt_aliases = ["out_%s" % compact(odt_name_legal)]
841 cs = create_ACES_RRT_plus_ODT(
851 colorspaces.append(cs)
853 displays[odt_name_legal] = {
854 'Linear': linear_display_space,
855 'Log': log_display_space,
856 'Output Transform': cs}
858 if odt_name in ['Academy.Rec2020_100nits_dim.a1.0.0',
859 'Academy.Rec709_100nits_dim.a1.0.0',
860 'Academy.Rec709_D60sim_100nits_dim.a1.0.0']:
861 print('Generating full range ODT for %s' % odt_name)
863 odt_name_full = '%s - Full' % odt_values['transformUserName']
864 odt_full = odt_values.copy()
865 odt_full['legalRange'] = 0
867 odt_full_aliases = ["out_%s" % compact(odt_name_full)]
869 cs_full = create_ACES_RRT_plus_ODT(
879 colorspaces.append(cs_full)
881 displays[odt_name_full] = {
882 'Linear': linear_display_space,
883 'Log': log_display_space,
884 'Output Transform': cs_full}
886 return (colorspaces, displays)
889 # Defining the reference colorspace.
890 ACES = ColorSpace('ACES2065-1')
892 'The Academy Color Encoding System reference color space')
893 ACES.equality_group = ''
894 ACES.aliases = ["lin_ap0", "aces"]
897 ACES.allocation_type = ocio.Constants.ALLOCATION_LG2
898 ACES.allocation_vars = [-15, 6]
902 def create_colorspaces(aces_CTL_directory,
911 Generates the colorspace conversions.
916 Parameter description.
921 Return value description.
928 ACEScc = create_ACEScc(aces_CTL_directory, lut_directory, lut_resolution_1d, cleanup)
929 colorspaces.append(ACEScc)
931 ACESproxy = create_ACESproxy(aces_CTL_directory, lut_directory, lut_resolution_1d, cleanup)
932 colorspaces.append(ACESproxy)
934 ACEScg = create_ACEScg(aces_CTL_directory, lut_directory, lut_resolution_1d, cleanup)
935 colorspaces.append(ACEScg)
937 ADX10 = create_ADX(lut_directory, lut_resolution_1d, bit_depth=10)
938 colorspaces.append(ADX10)
940 ADX16 = create_ADX(lut_directory, lut_resolution_1d, bit_depth=16)
941 colorspaces.append(ADX16)
943 lmts = create_lmts(aces_CTL_directory,
950 colorspaces.extend(lmts)
952 (odts, displays) = create_odts(aces_CTL_directory,
961 colorspaces.extend(odts)
963 return (ACES, colorspaces, displays, ACEScc)