def test_sequence(self): """Test :class:`colour.io.luts.sequence.LUTSequence.sequence` property.""" sequence = [self._LUT_1, self._LUT_2, self._LUT_3] LUT_sequence = LUTSequence() LUT_sequence.sequence = sequence self.assertListEqual(self._LUT_sequence.sequence, sequence)
def setUp(self): """Initialise the common tests attributes.""" self._LUT_1 = LUT1D(LUT1D.linear_table(16) + 0.125, "Nemo 1D") self._LUT_2 = LUT3D(LUT3D.linear_table(16)**(1 / 2.2), "Nemo 3D") self._LUT_3 = LUT3x1D(LUT3x1D.linear_table(16) * 0.750, "Nemo 3x1D") self._LUT_sequence = LUTSequence(self._LUT_1, self._LUT_2, self._LUT_3) samples = np.linspace(0, 1, 5) self._RGB = tstack([samples, samples, samples])
def test__eq__(self): """Test :class:`colour.io.luts.sequence.LUTSequence.__eq__` method.""" LUT_sequence_1 = LUTSequence(self._LUT_1, self._LUT_2, self._LUT_3) LUT_sequence_2 = LUTSequence(self._LUT_1, self._LUT_2) self.assertEqual(self._LUT_sequence, LUT_sequence_1) self.assertNotEqual(self._LUT_sequence, self._LUT_sequence[0]) self.assertNotEqual(LUT_sequence_1, LUT_sequence_2)
def test__init__(self): """Test :class:`colour.io.luts.sequence.LUTSequence.__init__` method.""" self.assertEqual( LUTSequence(self._LUT_1, self._LUT_2, self._LUT_3), self._LUT_sequence, )
def test__neq__(self): """Test :class:`colour.io.luts.sequence.LUTSequence.__neq__` method.""" self.assertNotEqual( self._LUT_sequence, LUTSequence(self._LUT_1, self._LUT_2.copy() * 0.75, self._LUT_3), )
def test_insert(self): """Test :class:`colour.io.luts.sequence.LUTSequence.insert` method.""" LUT_sequence = self._LUT_sequence.copy() LUT_sequence.insert(1, self._LUT_2.copy()) self.assertEqual( LUT_sequence, LUTSequence( self._LUT_1, self._LUT_2, self._LUT_2, self._LUT_3, ), )
def read_LUT_ResolveCube(path): """ Reads given *Resolve* *.cube* *LUT* file. Parameters ---------- path : unicode *LUT* path. Returns ------- LUT3x1D or LUT3D or LUTSequence :class:`LUT3x1D` or :class:`LUT3D` or :class:`LUTSequence` class instance. References ---------- :cite:`Chamberlain2015` Examples -------- Reading a 3x1D *Resolve* *.cube* *LUT*: >>> import os >>> path = os.path.join( ... os.path.dirname(__file__), 'tests', 'resources', 'resolve_cube', ... 'ACES_Proxy_10_to_ACES.cube') >>> print(read_LUT_ResolveCube(path)) LUT3x1D - ACES Proxy 10 to ACES ------------------------------- <BLANKLINE> Dimensions : 2 Domain : [[ 0. 0. 0.] [ 1. 1. 1.]] Size : (32, 3) Reading a 3D *Resolve* *.cube* *LUT*: >>> path = os.path.join( ... os.path.dirname(__file__), 'tests', 'resources', 'resolve_cube', ... 'Colour_Correct.cube') >>> print(read_LUT_ResolveCube(path)) LUT3D - Generated by Foundry::LUT --------------------------------- <BLANKLINE> Dimensions : 3 Domain : [[ 0. 0. 0.] [ 1. 1. 1.]] Size : (4, 4, 4, 3) Reading a 3D *Resolve* *.cube* *LUT* with comments: >>> path = os.path.join( ... os.path.dirname(__file__), 'tests', 'resources', 'resolve_cube', ... 'Demo.cube') >>> print(read_LUT_ResolveCube(path)) LUT3x1D - Demo -------------- <BLANKLINE> Dimensions : 2 Domain : [[ 0. 0. 0.] [ 3. 3. 3.]] Size : (3, 3) Comment 01 : Comments can't go anywhere Reading a 3x1D + 3D *Resolve* *.cube* *LUT*: >>> path = os.path.join( ... os.path.dirname(__file__), 'tests', 'resources', 'resolve_cube', ... 'Three_Dimensional_Table_With_Shaper.cube') >>> print(read_LUT_ResolveCube(path)) LUT Sequence ------------ <BLANKLINE> Overview <BLANKLINE> LUT3x1D ---> LUT3D <BLANKLINE> Operations <BLANKLINE> LUT3x1D - LUT3D with My Shaper - Shaper --------------------------------------- <BLANKLINE> Dimensions : 2 Domain : [[-0.1 -0.1 -0.1] [ 3. 3. 3. ]] Size : (10, 3) <BLANKLINE> LUT3D - LUT3D with My Shaper - Cube ----------------------------------- <BLANKLINE> Dimensions : 3 Domain : [[-0.1 -0.1 -0.1] [ 3. 3. 3. ]] Size : (3, 3, 3, 3) Comment 01 : A first "Shaper" comment. Comment 02 : A second "Shaper" comment. Comment 03 : A first "LUT3D" comment. Comment 04 : A second "LUT3D" comment. """ title = path_to_title(path) size_3x1D = size_3D = 2 table = [] comments = [] has_3x1D, has_3D = False, False with open(path) as cube_file: lines = cube_file.readlines() LUT = LUTSequence(LUT3x1D(), LUT3D()) for line in lines: line = line.strip() if len(line) == 0: continue if line.startswith('#'): comments.append(line[1:].strip()) continue tokens = line.split() if tokens[0] == 'TITLE': title = ' '.join(tokens[1:])[1:-1] elif tokens[0] == 'LUT_1D_INPUT_RANGE': domain = parse_array(tokens[1:]) LUT[0].domain = tstack([domain, domain, domain]) elif tokens[0] == 'LUT_3D_INPUT_RANGE': domain = parse_array(tokens[1:]) LUT[1].domain = tstack([domain, domain, domain]) elif tokens[0] == 'LUT_1D_SIZE': has_3x1D = True size_3x1D = np.int_(tokens[1]) elif tokens[0] == 'LUT_3D_SIZE': has_3D = True size_3D = np.int_(tokens[1]) else: table.append(parse_array(tokens)) table = as_float_array(table) if has_3x1D and has_3D: LUT[0].name = '{0} - Shaper'.format(title) LUT[1].name = '{0} - Cube'.format(title) LUT[1].comments = comments LUT[0].table = table[:size_3x1D] # The lines of table data shall be in ascending index order, # with the first component index (Red) changing most rapidly, # and the last component index (Blue) changing least rapidly. LUT[1].table = table[size_3x1D:].reshape( (size_3D, size_3D, size_3D, 3), order='F') return LUT elif has_3x1D: LUT[0].name = title LUT[0].comments = comments LUT[0].table = table return LUT[0] elif has_3D: LUT[1].name = title LUT[1].comments = comments # The lines of table data shall be in ascending index order, # with the first component index (Red) changing most rapidly, # and the last component index (Blue) changing least rapidly. table = table.reshape([size_3D, size_3D, size_3D, 3], order='F') LUT[1].table = table return LUT[1]
def write_LUT_ResolveCube(LUT, path, decimals=7): """ Writes given *LUT* to given *Resolve* *.cube* *LUT* file. Parameters ---------- LUT : LUT1D or LUT3x1D or LUT3D or LUTSequence :class:`LUT1D`, :class:`LUT3x1D` or :class:`LUT3D` or :class:`LUTSequence` class instance to write at given path. path : unicode *LUT* path. decimals : int, optional Formatting decimals. Returns ------- bool Definition success. References ---------- :cite:`Chamberlain2015` Examples -------- Writing a 3x1D *Resolve* *.cube* *LUT*: >>> from colour.algebra import spow >>> domain = np.array([[-0.1, -0.1, -0.1], [3.0, 3.0, 3.0]]) >>> LUT = LUT3x1D( ... spow(LUT3x1D.linear_table(16, domain), 1 / 2.2), ... 'My LUT', ... domain, ... comments=['A first comment.', 'A second comment.']) >>> write_LUT_ResolveCube(LUT, 'My_LUT.cube') # doctest: +SKIP Writing a 3D *Resolve* *.cube* *LUT*: >>> domain = np.array([[-0.1, -0.1, -0.1], [3.0, 3.0, 3.0]]) >>> LUT = LUT3D( ... spow(LUT3D.linear_table(16, domain), 1 / 2.2), ... 'My LUT', ... domain, ... comments=['A first comment.', 'A second comment.']) >>> write_LUT_ResolveCube(LUT, 'My_LUT.cube') # doctest: +SKIP Writing a 3x1D + 3D *Resolve* *.cube* *LUT*: >>> from colour.models import RGB_to_HSV, HSV_to_RGB >>> from colour.utilities import tstack >>> def rotate_hue(a, angle): ... H, S, V = RGB_to_HSV(a) ... H += angle / 360 ... H[H > 1] -= 1 ... H[H < 0] += 1 ... return HSV_to_RGB([H, S, V]) >>> domain = np.array([[-0.1, -0.1, -0.1], [3.0, 3.0, 3.0]]) >>> shaper = LUT3x1D( ... spow(LUT3x1D.linear_table(10, domain), 1 / 2.2), ... 'My Shaper', ... domain, ... comments=[ ... 'A first "Shaper" comment.', 'A second "Shaper" comment.']) >>> LUT = LUT3D( ... rotate_hue(LUT3D.linear_table(3, domain), 10), ... 'LUT3D with My Shaper', ... domain, ... comments=['A first "LUT3D" comment.', 'A second "LUT3D" comment.']) >>> LUT_sequence = LUTSequence(shaper, LUT) >>> write_LUT_ResolveCube(LUT_sequence, 'My_LUT.cube') # doctest: +SKIP """ has_3D, has_3x1D = False, False if isinstance(LUT, LUTSequence): assert (len(LUT) == 2 and isinstance(LUT[0], (LUT1D, LUT3x1D)) and isinstance(LUT[1], LUT3D)), ( 'LUTSequence must be 1D + 3D or 3x1D + 3D!') if isinstance(LUT[0], LUT1D): LUT[0] = LUT[0].as_LUT(LUT3x1D) has_3x1D = True has_3D = True name = LUT[1].name elif isinstance(LUT, LUT1D): name = LUT.name LUT = LUTSequence(LUT.as_LUT(LUT3x1D), LUT3D()) has_3x1D = True elif isinstance(LUT, LUT3x1D): name = LUT.name LUT = LUTSequence(LUT, LUT3D()) has_3x1D = True elif isinstance(LUT, LUT3D): name = LUT.name LUT = LUTSequence(LUT3x1D(), LUT) has_3D = True else: raise ValueError('LUT must be 1D, 3x1D, 3D, 1D + 3D or 3x1D + 3D!') for i in range(2): assert not LUT[i].is_domain_explicit(), ( '"LUT" domain must be implicit!') assert (len(np.unique(LUT[0].domain)) == 2 and len(np.unique(LUT[1].domain)) == 2), 'LUT domain must be 1D!' if has_3x1D: assert 2 <= LUT[0].size <= 65536, ( 'Shaper size must be in domain [2, 65536]!') if has_3D: assert 2 <= LUT[1].size <= 256, 'Cube size must be in domain [2, 256]!' def _format_array(array): """ Formats given array as a *Resolve* *.cube* data row. """ return '{1:0.{0}f} {2:0.{0}f} {3:0.{0}f}'.format(decimals, *array) def _format_tuple(array): """ Formats given array as 2 space separated values to *decimals* precision. """ return '{1:0.{0}f} {2:0.{0}f}'.format(decimals, *array) with open(path, 'w') as cube_file: cube_file.write('TITLE "{0}"\n'.format(name)) if LUT[0].comments: for comment in LUT[0].comments: cube_file.write('# {0}\n'.format(comment)) if LUT[1].comments: for comment in LUT[1].comments: cube_file.write('# {0}\n'.format(comment)) default_domain = np.array([[0, 0, 0], [1, 1, 1]]) if has_3x1D: cube_file.write('{0} {1}\n'.format('LUT_1D_SIZE', LUT[0].table.shape[0])) if not np.array_equal(LUT[0].domain, default_domain): cube_file.write('LUT_1D_INPUT_RANGE {0}\n'.format( _format_tuple([LUT[0].domain[0][0], LUT[0].domain[1][0]]))) if has_3D: cube_file.write('{0} {1}\n'.format('LUT_3D_SIZE', LUT[1].table.shape[0])) if not np.array_equal(LUT[1].domain, default_domain): cube_file.write('LUT_3D_INPUT_RANGE {0}\n'.format( _format_tuple([LUT[1].domain[0][0], LUT[1].domain[1][0]]))) if has_3x1D: table = LUT[0].table for row in table: cube_file.write('{0}\n'.format(_format_array(row))) cube_file.write('\n') if has_3D: table = LUT[1].table.reshape([-1, 3], order='F') for row in table: cube_file.write('{0}\n'.format(_format_array(row))) return True
def read_LUT_Cinespace(path): """ Reads given *Cinespace* *.csp* *LUT* file. Parameters ---------- path : unicode *LUT* path. Returns ------- LUT2D or LUT3D or LUTSequence :class:`LUT2D` or :class:`LUT3D` or :class:`LUTSequence` class instance. References ---------- :cite:`RisingSunResearch` Examples -------- Reading a 2D *Cinespace* *.csp* *LUT*: >>> import os >>> path = os.path.join( ... os.path.dirname(__file__), 'tests', 'resources', 'cinespace', ... 'ACES_Proxy_10_to_ACES.csp') >>> print(read_LUT_Cinespace(path)) LUT2D - ACES Proxy 10 to ACES ----------------------------- <BLANKLINE> Dimensions : 2 Domain : [[ 0. 0. 0.] [ 1. 1. 1.]] Size : (32, 3) Reading a 3D *Cinespace* *.csp* *LUT*: >>> path = os.path.join( ... os.path.dirname(__file__), 'tests', 'resources', 'cinespace', ... 'ColourCorrect.csp') >>> print(read_LUT_Cinespace(path)) LUT3D - Generated by Foundry::LUT --------------------------------- <BLANKLINE> Dimensions : 3 Domain : [[ 0. 0. 0.] [ 1. 1. 1.]] Size : (4, 4, 4, 3) """ unity_range = np.array([[0., 0., 0.], [1., 1., 1.]]) def _parse_metadata_section(lines): """ Parses the metadata at given lines. """ if len(metadata) > 0: title = metadata[0] comments = metadata[1:] else: title = '' comments = [] return title, comments def _parse_domain_section(lines): """ Parses the domain at given lines. """ pre_LUT_size = max([int(lines[i]) for i in [0, 3, 6]]) pre_LUT = [parse_array(lines[i]) for i in [1, 2, 4, 5, 7, 8]] pre_LUT_padded = [] for row in pre_LUT: if len(row) != pre_LUT_size: pre_LUT_padded.append( np.pad( row, (0, pre_LUT_size - row.shape[0]), mode='constant', constant_values=np.nan)) else: pre_LUT_padded.append(row) pre_LUT = np.asarray(pre_LUT_padded) return pre_LUT def _parse_table_section(lines): """ Parses the table at given lines. """ size = parse_array(lines[0]).astype(int) table = np.array([parse_array(line) for line in lines[1:]]) return size, table with open(path) as csp_file: lines = csp_file.readlines() assert len(lines) > 0, 'LUT file empty!' lines = [line.strip() for line in lines if line.strip()] header = lines[0] assert header == 'CSPLUTV100', 'Invalid header!' kind = lines[1] assert kind in ('1D', '3D'), 'Invalid kind!' is_3D = kind == '3D' seek = 2 metadata = [] is_metadata = False for i, line in enumerate(lines[2:]): line = line.strip() if line == 'BEGIN METADATA': is_metadata = True continue elif line == 'END METADATA': seek += i break if is_metadata: metadata.append(line) title, comments = _parse_metadata_section(metadata) seek += 1 pre_LUT = _parse_domain_section(lines[seek:seek + 9]) seek += 9 size, table = _parse_table_section(lines[seek:]) assert np.product(size) == len(table), 'Invalid table size!' if (is_3D and pre_LUT.shape == (6, 2) and np.array_equal( pre_LUT.reshape(3, 4).transpose()[2:4], unity_range)): table = table.reshape((size[0], size[1], size[2], 3), order='F') LUT = LUT3D( domain=pre_LUT.reshape(3, 4).transpose()[0:2], name=title, comments=comments, table=table) return LUT if (not is_3D and pre_LUT.shape == (6, 2) and np.array_equal( pre_LUT.reshape(3, 4).transpose()[2:4], unity_range)): LUT = LUT2D( domain=pre_LUT.reshape(3, 4).transpose()[0:2], name=title, comments=comments, table=table) return LUT if is_3D: pre_domain = tstack((pre_LUT[0], pre_LUT[2], pre_LUT[4])) pre_table = tstack((pre_LUT[1], pre_LUT[3], pre_LUT[5])) shaper_name = '{0} - Shaper'.format(title) cube_name = '{0} - Cube'.format(title) table = table.reshape((size[0], size[1], size[2], 3), order='F') LUT_A = LUT2D(pre_table, shaper_name, pre_domain) LUT_B = LUT3D(table, cube_name, comments=comments) return LUTSequence(LUT_A, LUT_B) if not is_3D: pre_domain = tstack((pre_LUT[0], pre_LUT[2], pre_LUT[4])) pre_table = tstack((pre_LUT[1], pre_LUT[3], pre_LUT[5])) if np.array_equal(table, unity_range): return LUT2D(pre_table, title, pre_domain, comments=comments) elif table.shape == (2, 3): table_max = table[1] table_min = table[0] pre_table *= (table_max - table_min) pre_table += table_min return LUT2D(pre_table, title, pre_domain, comments=comments) else: pre_name = '{0} - preLUT'.format(title) table_name = '{0} - table'.format(title) LUT_A = LUT2D(pre_table, pre_name, pre_domain) LUT_B = LUT2D(table, table_name, comments=comments) return LUTSequence(LUT_A, LUT_B)
def write_LUT_Cinespace(LUT, path, decimals=7): """ Writes given *LUT* to given *Cinespace* *.csp* *LUT* file. Parameters ---------- LUT : LUT1D or LUT2D or LUT3D or LUTSequence :class:`LUT1D`, :class:`LUT2D` or :class:`LUT3D` or :class:`LUTSequence` class instance to write at given path. path : unicode *LUT* path. decimals : int, optional Formatting decimals. Returns ------- bool Definition success. References ---------- :cite:`RisingSunResearch` Examples -------- Writing a 2D *Cinespace* *.csp* *LUT*: >>> from colour.algebra import spow >>> domain = np.array([[-0.1, -0.2, -0.4], [1.5, 3.0, 6.0]]) >>> LUT = LUT2D( ... spow(LUT2D.linear_table(16, domain), 1 / 2.2), ... 'My LUT', ... domain, ... comments=['A first comment.', 'A second comment.']) >>> write_LUT_Cinespace(LUT, 'My_LUT.cube') # doctest: +SKIP Writing a 3D *Cinespace* *.csp* *LUT*: >>> domain = np.array([[-0.1, -0.2, -0.4], [1.5, 3.0, 6.0]]) >>> LUT = LUT3D( ... spow(LUT3D.linear_table(16, domain), 1 / 2.2), ... 'My LUT', ... domain, ... comments=['A first comment.', 'A second comment.']) >>> write_LUT_Cinespace(LUT, 'My_LUT.cube') # doctest: +SKIP """ has_3D, has_2D, non_uniform = False, False, False if isinstance(LUT, LUTSequence): assert (len(LUT) == 2 and (isinstance(LUT[0], LUT1D) or isinstance(LUT[0], LUT2D)) and isinstance(LUT[1], LUT3D)), 'LUTSequence must be 1D+3D or 2D+3D!' has_2D = True has_3D = True name = LUT[1].name if isinstance(LUT[0], LUT1D): LUT[0] = LUT[0].as_LUT(LUT2D) elif isinstance(LUT, LUT1D): if LUT.is_domain_explicit(): non_uniform = True name = LUT.name LUT = LUTSequence(LUT.as_LUT(LUT2D), LUT3D()) has_2D = True elif isinstance(LUT, LUT2D): if LUT.is_domain_explicit(): non_uniform = True name = LUT.name LUT = LUTSequence(LUT, LUT3D()) has_2D = True elif isinstance(LUT, LUT3D): name = LUT.name LUT = LUTSequence(LUT2D(), LUT) has_3D = True else: assert False, 'LUT must be 1D, 2D, 3D, 1D+3D or 2D+3D!' if has_2D: assert 2 <= LUT[0].size <= 65536, ( 'Shaper size must be in domain [2, 65536]!') if has_3D: assert 2 <= LUT[1].size <= 256, 'Cube size must be in domain [2, 256]!' def _ragged_size(table): """ Return the ragged size of given table. """ r, g, b = tsplit(table) r_len = r.shape[-1] - np.sum(np.isnan(r)) g_len = g.shape[-1] - np.sum(np.isnan(g)) b_len = b.shape[-1] - np.sum(np.isnan(b)) return [r_len, g_len, b_len] def _format_array(array): """ Formats given array as a *Cinespace* *.cube* data row. """ return '{1:0.{0}f} {2:0.{0}f} {3:0.{0}f}'.format(decimals, *array) def _format_tuple(array): """ Formats given array as 2 space separated values to *decimals* precision. """ return '{1:0.{0}f} {2:0.{0}f}'.format(decimals, *array) with open(path, 'w') as csp_file: csp_file.write('CSPLUTV100\n') if has_3D: csp_file.write('3D\n\n') else: csp_file.write('1D\n\n') csp_file.write('BEGIN METADATA\n') csp_file.write('{0}\n'.format(name)) if LUT[0].comments: for comment in LUT[0].comments: csp_file.write('{0}\n'.format(comment)) if LUT[1].comments: for comment in LUT[1].comments: csp_file.write('{0}\n'.format(comment)) csp_file.write('END METADATA\n\n') if has_3D or non_uniform: if has_2D: for i in range(3): if LUT[0].is_domain_explicit(): size = _ragged_size(LUT[0].domain)[i] table_min = np.nanmin(LUT[0].table) table_max = np.nanmax(LUT[0].table) else: size = LUT[0].size csp_file.write('{0}\n'.format(size)) for j in range(size): if LUT[0].is_domain_explicit(): entry = LUT[0].domain[j][i] else: entry = ( LUT[0].domain[0][i] + j * (LUT[0].domain[1][i] - LUT[0].domain[0][i]) / (LUT[0].size - 1)) csp_file.write('{0:.{1}f} '.format(entry, decimals)) csp_file.write('\n') for j in range(size): entry = LUT[0].table[j][i] if non_uniform: entry -= table_min entry /= (table_max - table_min) csp_file.write('{0:.{1}f} '.format(entry, decimals)) csp_file.write('\n') else: for i in range(3): csp_file.write('2\n') csp_file.write('{0}\n'.format( _format_tuple( [LUT[1].domain[0][i], LUT[1].domain[1][i]]))) csp_file.write('{0:.{2}f} {1:.{2}f}\n'.format( 0, 1, decimals)) if non_uniform: csp_file.write('\n{0}\n'.format(2)) row = [table_min, table_min, table_min] csp_file.write('{0}\n'.format(_format_array(row))) row = [table_max, table_max, table_max] csp_file.write('{0}\n'.format(_format_array(row))) else: csp_file.write('\n{0} {1} {2}\n'.format( LUT[1].table.shape[0], LUT[1].table.shape[1], LUT[1].table.shape[2])) table = LUT[1].table.reshape((-1, 3), order='F') for row in table: csp_file.write('{0}\n'.format(_format_array(row))) else: for i in range(3): csp_file.write('2\n') csp_file.write('{0}\n'.format( _format_tuple([LUT[0].domain[0][i], LUT[0].domain[1][i]]))) csp_file.write('0.0 1.0\n') csp_file.write('\n{0}\n'.format(LUT[0].size)) table = LUT[0].table for row in table: csp_file.write('{0}\n'.format(_format_array(row))) return True
def write_LUT_ResolveCube(LUT, path, decimals=7): """ Writes given *LUT* to given *Resolve* *.cube* *LUT* file. Parameters ---------- LUT : LUT1D or LUT3x1D or LUT3D or LUTSequence :class:`LUT1D`, :class:`LUT3x1D` or :class:`LUT3D` or :class:`LUTSequence` class instance to write at given path. path : unicode *LUT* path. decimals : int, optional Formatting decimals. Returns ------- bool Definition success. References ---------- :cite:`Chamberlain2015` Examples -------- Writing a 3x1D *Resolve* *.cube* *LUT*: >>> from colour.algebra import spow >>> domain = np.array([[-0.1, -0.1, -0.1], [3.0, 3.0, 3.0]]) >>> LUT = LUT3x1D( ... spow(LUT3x1D.linear_table(16, domain), 1 / 2.2), ... 'My LUT', ... domain, ... comments=['A first comment.', 'A second comment.']) >>> write_LUT_ResolveCube(LUT, 'My_LUT.cube') # doctest: +SKIP Writing a 3D *Iridas* *.cube* *LUT*: >>> domain = np.array([[-0.1, -0.1, -0.1], [3.0, 3.0, 3.0]]) >>> LUT = LUT3D( ... spow(LUT3D.linear_table(16, domain), 1 / 2.2), ... 'My LUT', ... domain, ... comments=['A first comment.', 'A second comment.']) >>> write_LUT_ResolveCube(LUT, 'My_LUT.cube') # doctest: +SKIP """ has_3D, has_3x1D = False, False if isinstance(LUT, LUTSequence): assert (len(LUT) == 2 and isinstance(LUT[0], (LUT1D, LUT3x1D)) and isinstance(LUT[1], LUT3D)), ( 'LUTSequence must be 1D + 3D or 3x1D + 3D!') if isinstance(LUT[0], LUT1D): LUT[0] = LUT[0].as_LUT(LUT3x1D) has_3x1D = True has_3D = True name = LUT[1].name elif isinstance(LUT, LUT1D): name = LUT.name LUT = LUTSequence(LUT.as_LUT(LUT3x1D), LUT3D()) has_3x1D = True elif isinstance(LUT, LUT3x1D): name = LUT.name LUT = LUTSequence(LUT, LUT3D()) has_3x1D = True elif isinstance(LUT, LUT3D): name = LUT.name LUT = LUTSequence(LUT3x1D(), LUT) has_3D = True else: raise ValueError('LUT must be 1D, 3x1D, 3D, 1D + 3D or 3x1D + 3D!') for i in range(2): assert not LUT[i].is_domain_explicit(), ( '"LUT" domain must be implicit!') assert (len(np.unique(LUT[0].domain)) == 2 and len(np.unique(LUT[1].domain)) == 2), 'LUT domain must be 1D!' if has_3x1D: assert 2 <= LUT[0].size <= 65536, ( 'Shaper size must be in domain [2, 65536]!') if has_3D: assert 2 <= LUT[1].size <= 256, 'Cube size must be in domain [2, 256]!' def _format_array(array): """ Formats given array as a *Resolve* *.cube* data row. """ return '{1:0.{0}f} {2:0.{0}f} {3:0.{0}f}'.format(decimals, *array) def _format_tuple(array): """ Formats given array as 2 space separated values to *decimals* precision. """ return '{1:0.{0}f} {2:0.{0}f}'.format(decimals, *array) with open(path, 'w') as cube_file: cube_file.write('TITLE "{0}"\n'.format(name)) if LUT[0].comments: for comment in LUT[0].comments: cube_file.write('# {0}\n'.format(comment)) if LUT[1].comments: for comment in LUT[1].comments: cube_file.write('# {0}\n'.format(comment)) default_domain = np.array([[0, 0, 0], [1, 1, 1]]) if has_3x1D: cube_file.write('{0} {1}\n'.format('LUT_1D_SIZE', LUT[0].table.shape[0])) if not np.array_equal(LUT[0].domain, default_domain): cube_file.write('LUT_1D_INPUT_RANGE {0}\n'.format( _format_tuple([LUT[0].domain[0][0], LUT[0].domain[1][0]]))) if has_3D: cube_file.write('{0} {1}\n'.format('LUT_3D_SIZE', LUT[1].table.shape[0])) if not np.array_equal(LUT[1].domain, default_domain): cube_file.write('LUT_3D_INPUT_RANGE {0}\n'.format( _format_tuple([LUT[1].domain[0][0], LUT[1].domain[1][0]]))) if has_3x1D: table = LUT[0].table for row in table: cube_file.write('{0}\n'.format(_format_array(row))) cube_file.write('\n') if has_3D: table = LUT[1].table.reshape([-1, 3], order='F') for row in table: cube_file.write('{0}\n'.format(_format_array(row))) return True
class TestLUTSequence(unittest.TestCase): """ Define :class:`colour.io.luts.sequence.LUTSequence` class unit tests methods. """ def setUp(self): """Initialise the common tests attributes.""" self._LUT_1 = LUT1D(LUT1D.linear_table(16) + 0.125, "Nemo 1D") self._LUT_2 = LUT3D(LUT3D.linear_table(16)**(1 / 2.2), "Nemo 3D") self._LUT_3 = LUT3x1D(LUT3x1D.linear_table(16) * 0.750, "Nemo 3x1D") self._LUT_sequence = LUTSequence(self._LUT_1, self._LUT_2, self._LUT_3) samples = np.linspace(0, 1, 5) self._RGB = tstack([samples, samples, samples]) def test_required_attributes(self): """Test the presence of required attributes.""" required_attributes = ("sequence", ) for attribute in required_attributes: self.assertIn(attribute, dir(LUTSequence)) def test_required_methods(self): """Test the presence of required methods.""" required_methods = ( "__init__", "__getitem__", "__setitem__", "__delitem__", "__len__", "__str__", "__repr__", "__eq__", "__ne__", "insert", "apply", "copy", ) for method in required_methods: self.assertIn(method, dir(LUTSequence)) def test_sequence(self): """Test :class:`colour.io.luts.sequence.LUTSequence.sequence` property.""" sequence = [self._LUT_1, self._LUT_2, self._LUT_3] LUT_sequence = LUTSequence() LUT_sequence.sequence = sequence self.assertListEqual(self._LUT_sequence.sequence, sequence) def test__init__(self): """Test :class:`colour.io.luts.sequence.LUTSequence.__init__` method.""" self.assertEqual( LUTSequence(self._LUT_1, self._LUT_2, self._LUT_3), self._LUT_sequence, ) def test__getitem__(self): """Test :class:`colour.io.luts.sequence.LUTSequence.__getitem__` method.""" self.assertEqual(self._LUT_sequence[0], self._LUT_1) self.assertEqual(self._LUT_sequence[1], self._LUT_2) self.assertEqual(self._LUT_sequence[2], self._LUT_3) def test__setitem__(self): """Test :class:`colour.io.luts.sequence.LUTSequence.__setitem__` method.""" LUT_sequence = self._LUT_sequence.copy() LUT_sequence[0] = self._LUT_3 LUT_sequence[1] = self._LUT_1 LUT_sequence[2] = self._LUT_2 self.assertEqual(LUT_sequence[1], self._LUT_1) self.assertEqual(LUT_sequence[2], self._LUT_2) self.assertEqual(LUT_sequence[0], self._LUT_3) def test__delitem__(self): """Test :class:`colour.io.luts.sequence.LUTSequence.__delitem__` method.""" LUT_sequence = self._LUT_sequence.copy() del LUT_sequence[0] del LUT_sequence[0] self.assertEqual(LUT_sequence[0], self._LUT_3) def test__len__(self): """Test :class:`colour.io.luts.sequence.LUTSequence.__len__` method.""" self.assertEqual(len(self._LUT_sequence), 3) def test__str__(self): """Test :class:`colour.io.luts.sequence.LUTSequence.__str__` method.""" self.assertEqual( str(self._LUT_sequence), textwrap.dedent(""" LUT Sequence ------------ Overview LUT1D --> LUT3D --> LUT3x1D Operations LUT1D - Nemo 1D --------------- Dimensions : 1 Domain : [ 0. 1.] Size : (16,) LUT3D - Nemo 3D --------------- Dimensions : 3 Domain : [[ 0. 0. 0.] [ 1. 1. 1.]] Size : (16, 16, 16, 3) LUT3x1D - Nemo 3x1D ------------------- Dimensions : 2 Domain : [[ 0. 0. 0.] [ 1. 1. 1.]] Size : (16, 3)""")[1:], ) def test__repr__(self): """Test :class:`colour.io.luts.sequence.LUTSequence.__repr__` method.""" LUT_sequence = self._LUT_sequence.copy() LUT_sequence[1].table = LUT3D.linear_table(5) self.assertEqual( repr(LUT_sequence), textwrap.dedent(""" LUTSequence( LUT1D([ 0.125 , 0.19166667, 0.25833333, 0.325 , \ 0.39166667, 0.45833333, 0.525 , 0.59166667, 0.65833333, \ 0.725 , 0.79166667, 0.85833333, 0.925 , 0.99166667, \ 1.05833333, 1.125 ], name='Nemo 1D', domain=[ 0., 1.]), LUT3D([[[[ 0. , 0. , 0. ], [ 0. , 0. , 0.25], [ 0. , 0. , 0.5 ], [ 0. , 0. , 0.75], [ 0. , 0. , 1. ]], [[ 0. , 0.25, 0. ], [ 0. , 0.25, 0.25], [ 0. , 0.25, 0.5 ], [ 0. , 0.25, 0.75], [ 0. , 0.25, 1. ]], [[ 0. , 0.5 , 0. ], [ 0. , 0.5 , 0.25], [ 0. , 0.5 , 0.5 ], [ 0. , 0.5 , 0.75], [ 0. , 0.5 , 1. ]], [[ 0. , 0.75, 0. ], [ 0. , 0.75, 0.25], [ 0. , 0.75, 0.5 ], [ 0. , 0.75, 0.75], [ 0. , 0.75, 1. ]], [[ 0. , 1. , 0. ], [ 0. , 1. , 0.25], [ 0. , 1. , 0.5 ], [ 0. , 1. , 0.75], [ 0. , 1. , 1. ]]], [[[ 0.25, 0. , 0. ], [ 0.25, 0. , 0.25], [ 0.25, 0. , 0.5 ], [ 0.25, 0. , 0.75], [ 0.25, 0. , 1. ]], [[ 0.25, 0.25, 0. ], [ 0.25, 0.25, 0.25], [ 0.25, 0.25, 0.5 ], [ 0.25, 0.25, 0.75], [ 0.25, 0.25, 1. ]], [[ 0.25, 0.5 , 0. ], [ 0.25, 0.5 , 0.25], [ 0.25, 0.5 , 0.5 ], [ 0.25, 0.5 , 0.75], [ 0.25, 0.5 , 1. ]], [[ 0.25, 0.75, 0. ], [ 0.25, 0.75, 0.25], [ 0.25, 0.75, 0.5 ], [ 0.25, 0.75, 0.75], [ 0.25, 0.75, 1. ]], [[ 0.25, 1. , 0. ], [ 0.25, 1. , 0.25], [ 0.25, 1. , 0.5 ], [ 0.25, 1. , 0.75], [ 0.25, 1. , 1. ]]], [[[ 0.5 , 0. , 0. ], [ 0.5 , 0. , 0.25], [ 0.5 , 0. , 0.5 ], [ 0.5 , 0. , 0.75], [ 0.5 , 0. , 1. ]], [[ 0.5 , 0.25, 0. ], [ 0.5 , 0.25, 0.25], [ 0.5 , 0.25, 0.5 ], [ 0.5 , 0.25, 0.75], [ 0.5 , 0.25, 1. ]], [[ 0.5 , 0.5 , 0. ], [ 0.5 , 0.5 , 0.25], [ 0.5 , 0.5 , 0.5 ], [ 0.5 , 0.5 , 0.75], [ 0.5 , 0.5 , 1. ]], [[ 0.5 , 0.75, 0. ], [ 0.5 , 0.75, 0.25], [ 0.5 , 0.75, 0.5 ], [ 0.5 , 0.75, 0.75], [ 0.5 , 0.75, 1. ]], [[ 0.5 , 1. , 0. ], [ 0.5 , 1. , 0.25], [ 0.5 , 1. , 0.5 ], [ 0.5 , 1. , 0.75], [ 0.5 , 1. , 1. ]]], [[[ 0.75, 0. , 0. ], [ 0.75, 0. , 0.25], [ 0.75, 0. , 0.5 ], [ 0.75, 0. , 0.75], [ 0.75, 0. , 1. ]], [[ 0.75, 0.25, 0. ], [ 0.75, 0.25, 0.25], [ 0.75, 0.25, 0.5 ], [ 0.75, 0.25, 0.75], [ 0.75, 0.25, 1. ]], [[ 0.75, 0.5 , 0. ], [ 0.75, 0.5 , 0.25], [ 0.75, 0.5 , 0.5 ], [ 0.75, 0.5 , 0.75], [ 0.75, 0.5 , 1. ]], [[ 0.75, 0.75, 0. ], [ 0.75, 0.75, 0.25], [ 0.75, 0.75, 0.5 ], [ 0.75, 0.75, 0.75], [ 0.75, 0.75, 1. ]], [[ 0.75, 1. , 0. ], [ 0.75, 1. , 0.25], [ 0.75, 1. , 0.5 ], [ 0.75, 1. , 0.75], [ 0.75, 1. , 1. ]]], [[[ 1. , 0. , 0. ], [ 1. , 0. , 0.25], [ 1. , 0. , 0.5 ], [ 1. , 0. , 0.75], [ 1. , 0. , 1. ]], [[ 1. , 0.25, 0. ], [ 1. , 0.25, 0.25], [ 1. , 0.25, 0.5 ], [ 1. , 0.25, 0.75], [ 1. , 0.25, 1. ]], [[ 1. , 0.5 , 0. ], [ 1. , 0.5 , 0.25], [ 1. , 0.5 , 0.5 ], [ 1. , 0.5 , 0.75], [ 1. , 0.5 , 1. ]], [[ 1. , 0.75, 0. ], [ 1. , 0.75, 0.25], [ 1. , 0.75, 0.5 ], [ 1. , 0.75, 0.75], [ 1. , 0.75, 1. ]], [[ 1. , 1. , 0. ], [ 1. , 1. , 0.25], [ 1. , 1. , 0.5 ], [ 1. , 1. , 0.75], [ 1. , 1. , 1. ]]]], name='Nemo 3D', domain=[[ 0., 0., 0.], [ 1., 1., 1.]]), LUT3x1D([[ 0. , 0. , 0. ], [ 0.05, 0.05, 0.05], [ 0.1 , 0.1 , 0.1 ], [ 0.15, 0.15, 0.15], [ 0.2 , 0.2 , 0.2 ], [ 0.25, 0.25, 0.25], [ 0.3 , 0.3 , 0.3 ], [ 0.35, 0.35, 0.35], [ 0.4 , 0.4 , 0.4 ], [ 0.45, 0.45, 0.45], [ 0.5 , 0.5 , 0.5 ], [ 0.55, 0.55, 0.55], [ 0.6 , 0.6 , 0.6 ], [ 0.65, 0.65, 0.65], [ 0.7 , 0.7 , 0.7 ], [ 0.75, 0.75, 0.75]], name='Nemo 3x1D', domain=[[ 0., 0., 0.], [ 1., 1., 1.]]) )"""[1:]), ) def test__eq__(self): """Test :class:`colour.io.luts.sequence.LUTSequence.__eq__` method.""" LUT_sequence_1 = LUTSequence(self._LUT_1, self._LUT_2, self._LUT_3) LUT_sequence_2 = LUTSequence(self._LUT_1, self._LUT_2) self.assertEqual(self._LUT_sequence, LUT_sequence_1) self.assertNotEqual(self._LUT_sequence, self._LUT_sequence[0]) self.assertNotEqual(LUT_sequence_1, LUT_sequence_2) def test__neq__(self): """Test :class:`colour.io.luts.sequence.LUTSequence.__neq__` method.""" self.assertNotEqual( self._LUT_sequence, LUTSequence(self._LUT_1, self._LUT_2.copy() * 0.75, self._LUT_3), ) def test_insert(self): """Test :class:`colour.io.luts.sequence.LUTSequence.insert` method.""" LUT_sequence = self._LUT_sequence.copy() LUT_sequence.insert(1, self._LUT_2.copy()) self.assertEqual( LUT_sequence, LUTSequence( self._LUT_1, self._LUT_2, self._LUT_2, self._LUT_3, ), ) def test_apply(self): """Test :class:`colour.io.luts.sequence.LUTSequence.apply` method.""" class GammaOperator(AbstractLUTSequenceOperator): """ Gamma operator for unit tests. Parameters ---------- gamma Gamma value. """ def __init__(self, gamma: FloatingOrNDArray = 1.0): self._gamma = gamma def apply(self, RGB: ArrayLike, *args: Any, **kwargs: Any) -> NDArray: """ Apply the *LUT* sequence operator to given *RGB* colourspace array. Parameters ---------- RGB *RGB* colourspace array to apply the *LUT* sequence operator onto. Returns ------- :class:`numpy.ndarray` Processed *RGB* colourspace array. """ direction = kwargs.get("direction", "Forward") gamma = (self._gamma if direction == "Forward" else 1 / self._gamma) return as_float_array(gamma_function(RGB, gamma)) LUT_sequence = self._LUT_sequence.copy() LUT_sequence.insert(1, GammaOperator(1 / 2.2)) samples = np.linspace(0, 1, 5) RGB = tstack([samples, samples, samples]) np.testing.assert_almost_equal( LUT_sequence.apply(RGB, GammaOperator={"direction": "Inverse"}), np.array([ [0.03386629, 0.03386629, 0.03386629], [0.27852298, 0.27852298, 0.27852298], [0.46830881, 0.46830881, 0.46830881], [0.65615595, 0.65615595, 0.65615595], [0.75000000, 0.75000000, 0.75000000], ]), )
def write_LUT_Cinespace(LUT, path, decimals=7): """ Writes given *LUT* to given *Cinespace* *.csp* *LUT* file. Parameters ---------- LUT : LUT1D or LUT3x1D or LUT3D or LUTSequence :class:`LUT1D`, :class:`LUT3x1D` or :class:`LUT3D` or :class:`LUTSequence` class instance to write at given path. path : unicode *LUT* path. decimals : int, optional Formatting decimals. Returns ------- bool Definition success. References ---------- :cite:`RisingSunResearch` Examples -------- Writing a 3x1D *Cinespace* *.csp* *LUT*: >>> from colour.algebra import spow >>> domain = np.array([[-0.1, -0.2, -0.4], [1.5, 3.0, 6.0]]) >>> LUT = LUT3x1D( ... spow(LUT3x1D.linear_table(16, domain), 1 / 2.2), ... 'My LUT', ... domain, ... comments=['A first comment.', 'A second comment.']) >>> write_LUT_Cinespace(LUT, 'My_LUT.cube') # doctest: +SKIP Writing a 3D *Cinespace* *.csp* *LUT*: >>> domain = np.array([[-0.1, -0.2, -0.4], [1.5, 3.0, 6.0]]) >>> LUT = LUT3D( ... spow(LUT3D.linear_table(16, domain), 1 / 2.2), ... 'My LUT', ... domain, ... comments=['A first comment.', 'A second comment.']) >>> write_LUT_Cinespace(LUT, 'My_LUT.cube') # doctest: +SKIP """ has_3D, has_3x1D, non_uniform = False, False, False if isinstance(LUT, LUTSequence): assert (len(LUT) == 2 and (isinstance(LUT[0], LUT1D) or isinstance(LUT[0], LUT3x1D)) and isinstance(LUT[1], LUT3D)), 'LUTSequence must be 1D+3D or 3x1D+3D!' has_3x1D = True has_3D = True name = LUT[1].name if isinstance(LUT[0], LUT1D): LUT[0] = LUT[0].as_LUT(LUT3x1D) elif isinstance(LUT, LUT1D): if LUT.is_domain_explicit(): non_uniform = True name = LUT.name LUT = LUTSequence(LUT.as_LUT(LUT3x1D), LUT3D()) has_3x1D = True elif isinstance(LUT, LUT3x1D): if LUT.is_domain_explicit(): non_uniform = True name = LUT.name LUT = LUTSequence(LUT, LUT3D()) has_3x1D = True elif isinstance(LUT, LUT3D): name = LUT.name LUT = LUTSequence(LUT3x1D(), LUT) has_3D = True else: assert False, 'LUT must be 1D, 3x1D, 3D, 1D+3D or 3x1D+3D!' if has_3x1D: assert 2 <= LUT[0].size <= 65536, ( 'Shaper size must be in domain [2, 65536]!') if has_3D: assert 2 <= LUT[1].size <= 256, 'Cube size must be in domain [2, 256]!' def _ragged_size(table): """ Return the ragged size of given table. """ r, g, b = tsplit(table) r_len = r.shape[-1] - np.sum(np.isnan(r)) g_len = g.shape[-1] - np.sum(np.isnan(g)) b_len = b.shape[-1] - np.sum(np.isnan(b)) return [r_len, g_len, b_len] def _format_array(array): """ Formats given array as a *Cinespace* *.cube* data row. """ return '{1:0.{0}f} {2:0.{0}f} {3:0.{0}f}'.format(decimals, *array) def _format_tuple(array): """ Formats given array as 2 space separated values to *decimals* precision. """ return '{1:0.{0}f} {2:0.{0}f}'.format(decimals, *array) with open(path, 'w') as csp_file: csp_file.write('CSPLUTV100\n') if has_3D: csp_file.write('3D\n\n') else: csp_file.write('1D\n\n') csp_file.write('BEGIN METADATA\n') csp_file.write('{0}\n'.format(name)) if LUT[0].comments: for comment in LUT[0].comments: csp_file.write('{0}\n'.format(comment)) if LUT[1].comments: for comment in LUT[1].comments: csp_file.write('{0}\n'.format(comment)) csp_file.write('END METADATA\n\n') if has_3D or non_uniform: if has_3x1D: for i in range(3): if LUT[0].is_domain_explicit(): size = _ragged_size(LUT[0].domain)[i] table_min = np.nanmin(LUT[0].table) table_max = np.nanmax(LUT[0].table) else: size = LUT[0].size csp_file.write('{0}\n'.format(size)) for j in range(size): if LUT[0].is_domain_explicit(): entry = LUT[0].domain[j][i] else: entry = ( LUT[0].domain[0][i] + j * (LUT[0].domain[1][i] - LUT[0].domain[0][i]) / (LUT[0].size - 1)) csp_file.write('{0:.{1}f} '.format(entry, decimals)) csp_file.write('\n') for j in range(size): entry = LUT[0].table[j][i] if non_uniform: entry -= table_min entry /= (table_max - table_min) csp_file.write('{0:.{1}f} '.format(entry, decimals)) csp_file.write('\n') else: for i in range(3): csp_file.write('2\n') csp_file.write('{0}\n'.format( _format_tuple( [LUT[1].domain[0][i], LUT[1].domain[1][i]]))) csp_file.write('{0:.{2}f} {1:.{2}f}\n'.format( 0, 1, decimals)) if non_uniform: csp_file.write('\n{0}\n'.format(2)) row = [table_min, table_min, table_min] csp_file.write('{0}\n'.format(_format_array(row))) row = [table_max, table_max, table_max] csp_file.write('{0}\n'.format(_format_array(row))) else: csp_file.write('\n{0} {1} {2}\n'.format( LUT[1].table.shape[0], LUT[1].table.shape[1], LUT[1].table.shape[2])) table = LUT[1].table.reshape([-1, 3], order='F') for row in table: csp_file.write('{0}\n'.format(_format_array(row))) else: for i in range(3): csp_file.write('2\n') csp_file.write('{0}\n'.format( _format_tuple([LUT[0].domain[0][i], LUT[0].domain[1][i]]))) csp_file.write('0.0 1.0\n') csp_file.write('\n{0}\n'.format(LUT[0].size)) table = LUT[0].table for row in table: csp_file.write('{0}\n'.format(_format_array(row))) return True
def read_LUT_Cinespace(path: str) -> Union[LUT3x1D, LUT3D, LUTSequence]: """ Read given *Cinespace* *.csp* *LUT* file. Parameters ---------- path *LUT* path. Returns ------- :class:`colour.LUT3x1D` or :class:`colour.LUT3D` or \ :class:`colour.LUTSequence` :class:`LUT3x1D` or :class:`LUT3D` or :class:`LUTSequence` class instance. References ---------- :cite:`RisingSunResearch` Examples -------- Reading a 3x1D *Cinespace* *.csp* *LUT*: >>> import os >>> path = os.path.join( ... os.path.dirname(__file__), 'tests', 'resources', 'cinespace', ... 'ACES_Proxy_10_to_ACES.csp') >>> print(read_LUT_Cinespace(path)) LUT3x1D - ACES Proxy 10 to ACES ------------------------------- <BLANKLINE> Dimensions : 2 Domain : [[ 0. 0. 0.] [ 1. 1. 1.]] Size : (32, 3) Reading a 3D *Cinespace* *.csp* *LUT*: >>> path = os.path.join( ... os.path.dirname(__file__), 'tests', 'resources', 'cinespace', ... 'Colour_Correct.csp') >>> print(read_LUT_Cinespace(path)) LUT3D - Generated by Foundry::LUT --------------------------------- <BLANKLINE> Dimensions : 3 Domain : [[ 0. 0. 0.] [ 1. 1. 1.]] Size : (4, 4, 4, 3) """ unity_range = np.array([[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]]) def _parse_metadata_section(metadata: List) -> Tuple: """Parse the metadata at given lines.""" return (metadata[0], metadata[1:]) if len(metadata) > 0 else ("", []) def _parse_domain_section(lines: List[str]) -> NDArray: """Parse the domain at given lines.""" pre_LUT_size = max(int(lines[i]) for i in [0, 3, 6]) pre_LUT = [ as_float_array(lines[i].split()) for i in [1, 2, 4, 5, 7, 8] ] pre_LUT_padded = [] for row in pre_LUT: if len(row) != pre_LUT_size: pre_LUT_padded.append( np.pad( row, (0, pre_LUT_size - row.shape[0]), mode="constant", constant_values=np.nan, )) else: pre_LUT_padded.append(row) return np.asarray(pre_LUT_padded) def _parse_table_section(lines): """Parse the table at given lines.""" size = as_int_array(lines[0].split()) table = as_float_array([line.split() for line in lines[1:]]) return size, table with open(path) as csp_file: lines = csp_file.readlines() attest(len(lines) > 0, '"LUT" is empty!') lines = [line.strip() for line in lines if line.strip()] header = lines[0] attest(header == "CSPLUTV100", '"LUT" header is invalid!') kind = lines[1] attest(kind in ("1D", "3D"), '"LUT" type must be "1D" or "3D"!') is_3D = kind == "3D" seek = 2 metadata = [] is_metadata = False for i, line in enumerate(lines[2:]): line = line.strip() if line == "BEGIN METADATA": is_metadata = True continue elif line == "END METADATA": seek += i break if is_metadata: metadata.append(line) title, comments = _parse_metadata_section(metadata) seek += 1 pre_LUT = _parse_domain_section(lines[seek:seek + 9]) seek += 9 size, table = _parse_table_section(lines[seek:]) attest(np.product(size) == len(table), '"LUT" table size is invalid!') LUT: Union[LUT3x1D, LUT3D, LUTSequence] if (is_3D and pre_LUT.shape == (6, 2) and np.array_equal( np.transpose(np.reshape(pre_LUT, (3, 4)))[2:4], unity_range)): table = table.reshape([size[0], size[1], size[2], 3], order="F") LUT = LUT3D( domain=np.transpose(np.reshape(pre_LUT, (3, 4)))[0:2], name=title, comments=comments, table=table, ) elif (not is_3D and pre_LUT.shape == (6, 2) and np.array_equal( np.transpose(np.reshape(pre_LUT, (3, 4)))[2:4], unity_range)): LUT = LUT3x1D( domain=pre_LUT.reshape(3, 4).transpose()[0:2], name=title, comments=comments, table=table, ) elif is_3D: pre_domain = tstack((pre_LUT[0], pre_LUT[2], pre_LUT[4])) pre_table = tstack((pre_LUT[1], pre_LUT[3], pre_LUT[5])) shaper_name = f"{title} - Shaper" cube_name = f"{title} - Cube" table = table.reshape([size[0], size[1], size[2], 3], order="F") LUT = LUTSequence( LUT3x1D(pre_table, shaper_name, pre_domain), LUT3D(table, cube_name, comments=comments), ) elif not is_3D: pre_domain = tstack((pre_LUT[0], pre_LUT[2], pre_LUT[4])) pre_table = tstack((pre_LUT[1], pre_LUT[3], pre_LUT[5])) if table.shape == (2, 3): table_max = table[1] table_min = table[0] pre_table *= table_max - table_min pre_table += table_min LUT = LUT3x1D(pre_table, title, pre_domain, comments=comments) else: pre_name = f"{title} - PreLUT" table_name = f"{title} - Table" LUT = LUTSequence( LUT3x1D(pre_table, pre_name, pre_domain), LUT3x1D(table, table_name, comments=comments), ) return LUT
def write_LUT_Cinespace(LUT: Union[LUT3x1D, LUT3D, LUTSequence], path: str, decimals: Integer = 7) -> Boolean: """ Write given *LUT* to given *Cinespace* *.csp* *LUT* file. Parameters ---------- LUT :class:`LUT1D`, :class:`LUT3x1D` or :class:`LUT3D` or :class:`LUTSequence` class instance to write at given path. path *LUT* path. decimals Formatting decimals. Returns ------- :class:`bool` Definition success. References ---------- :cite:`RisingSunResearch` Examples -------- Writing a 3x1D *Cinespace* *.csp* *LUT*: >>> from colour.algebra import spow >>> domain = np.array([[-0.1, -0.2, -0.4], [1.5, 3.0, 6.0]]) >>> LUT = LUT3x1D( ... spow(LUT3x1D.linear_table(16, domain), 1 / 2.2), ... 'My LUT', ... domain, ... comments=['A first comment.', 'A second comment.']) >>> write_LUT_Cinespace(LUT, 'My_LUT.cube') # doctest: +SKIP Writing a 3D *Cinespace* *.csp* *LUT*: >>> domain = np.array([[-0.1, -0.2, -0.4], [1.5, 3.0, 6.0]]) >>> LUT = LUT3D( ... spow(LUT3D.linear_table(16, domain), 1 / 2.2), ... 'My LUT', ... domain, ... comments=['A first comment.', 'A second comment.']) >>> write_LUT_Cinespace(LUT, 'My_LUT.cube') # doctest: +SKIP """ has_3D, has_3x1D = False, False if isinstance(LUT, LUTSequence): attest( len(LUT) == 2 and isinstance(LUT[0], (LUT1D, LUT3x1D)) and isinstance(LUT[1], LUT3D), '"LUTSequence" must be "1D + 3D" or "3x1D + 3D"!', ) LUT[0] = (LUT[0].as_LUT(LUT3x1D) if isinstance(LUT[0], LUT1D) else LUT[0]) name = f"{LUT[0].name} - {LUT[1].name}" has_3x1D = True has_3D = True elif isinstance(LUT, LUT1D): name = LUT.name has_3x1D = True LUT = LUTSequence(LUT.as_LUT(LUT3x1D), LUT3D()) elif isinstance(LUT, LUT3x1D): name = LUT.name has_3x1D = True LUT = LUTSequence(LUT, LUT3D()) elif isinstance(LUT, LUT3D): name = LUT.name has_3D = True LUT = LUTSequence(LUT3x1D(), LUT) else: raise ValueError("LUT must be 1D, 3x1D, 3D, 1D + 3D or 3x1D + 3D!") if has_3x1D: attest( 2 <= LUT[0].size <= 65536, "Shaper size must be in domain [2, 65536]!", ) if has_3D: attest(2 <= LUT[1].size <= 256, "Cube size must be in domain [2, 256]!") def _ragged_size(table: ArrayLike) -> List: """Return the ragged size of given table.""" R, G, B = tsplit(table) R_len = R.shape[-1] - np.sum(np.isnan(R)) G_len = G.shape[-1] - np.sum(np.isnan(G)) B_len = B.shape[-1] - np.sum(np.isnan(B)) return [R_len, G_len, B_len] def _format_array(array: Union[List, Tuple]) -> str: """Format given array as a *Cinespace* *.cube* data row.""" return "{1:0.{0}f} {2:0.{0}f} {3:0.{0}f}".format(decimals, *array) def _format_tuple(array: Union[List, Tuple]) -> str: """ Format given array as 2 space separated values to *decimals* precision. """ return "{1:0.{0}f} {2:0.{0}f}".format(decimals, *array) with open(path, "w") as csp_file: csp_file.write("CSPLUTV100\n") if has_3D: csp_file.write("3D\n\n") else: csp_file.write("1D\n\n") csp_file.write("BEGIN METADATA\n") csp_file.write(f"{name}\n") if LUT[0].comments: for comment in LUT[0].comments: csp_file.write(f"{comment}\n") if LUT[1].comments: for comment in LUT[1].comments: csp_file.write(f"{comment}\n") csp_file.write("END METADATA\n\n") if has_3D: if has_3x1D: for i in range(3): size = (_ragged_size(LUT[0].domain)[i] if LUT[0].is_domain_explicit() else LUT[0].size) csp_file.write(f"{size}\n") for j in range(size): entry = (LUT[0].domain[j][i] if LUT[0].is_domain_explicit() else (LUT[0].domain[0][i] + j * (LUT[0].domain[1][i] - LUT[0].domain[0][i]) / (LUT[0].size - 1))) csp_file.write("{0:.{1}f} ".format(entry, decimals)) csp_file.write("\n") for j in range(size): entry = LUT[0].table[j][i] csp_file.write("{0:.{1}f} ".format(entry, decimals)) csp_file.write("\n") else: for i in range(3): csp_file.write("2\n") domain = _format_tuple( [LUT[1].domain[0][i], LUT[1].domain[1][i]]) csp_file.write(f"{domain}\n") csp_file.write("{0:.{2}f} {1:.{2}f}\n".format( 0, 1, decimals)) csp_file.write(f"\n{LUT[1].table.shape[0]} " f"{LUT[1].table.shape[1]} " f"{LUT[1].table.shape[2]}\n") table = LUT[1].table.reshape([-1, 3], order="F") for row in table: csp_file.write(f"{_format_array(row)}\n") else: for i in range(3): csp_file.write("2\n") domain = _format_tuple( [LUT[0].domain[0][i], LUT[0].domain[1][i]]) csp_file.write(f"{domain}\n") csp_file.write("0.0 1.0\n") csp_file.write(f"\n{LUT[0].size}\n") table = LUT[0].table for row in table: csp_file.write(f"{_format_array(row)}\n") return True