diff --git a/lutLab/rgb_to_xyz_matrix.py b/lutLab/rgb_to_rgb_matrix.py similarity index 60% rename from lutLab/rgb_to_xyz_matrix.py rename to lutLab/rgb_to_rgb_matrix.py index ab801dc..92f2d4f 100644 --- a/lutLab/rgb_to_xyz_matrix.py +++ b/lutLab/rgb_to_rgb_matrix.py @@ -5,8 +5,8 @@ .. moduleauthor:: `Marie FETIVEAU `_ """ -__version__ = "0.4" -from utils.colors_helper import get_RGB_to_XYZ_matrix, get_primaries_matrix +__version__ = "0.5" +from utils.colors_helper import get_RGB_to_RGB_matrix, get_colorspace_matrix from utils.colorspaces import COLORSPACES from utils.private_colorspaces import PRIVATE_COLORSPACES from utils.matrix_helper import matrix_to_string, matrix_to_spimtx_string @@ -14,6 +14,8 @@ import sys from utils import debug_helper +XYZ_colorspace = 'XYZ' + class RGBToXYZMatrixException(Exception): """Module custom exception @@ -25,7 +27,7 @@ class RGBToXYZMatrixException(Exception): pass -def display_matrix(colorspace, matrix_format, primaries_only=False): +def display_matrix(in_colorspace, out_colorspace, matrix_format, primaries_only=False): """Display RGB to XYZ matrix corresponding to colorspace and formatting as format @@ -35,34 +37,28 @@ def display_matrix(colorspace, matrix_format, primaries_only=False): matrix_format (str): output format. simple, matrix, spimtx. """ - try: - colorspace_obj = COLORSPACES[colorspace] - except KeyError: - colorspace_obj = PRIVATE_COLORSPACES[colorspace] - if primaries_only: - matrix = get_primaries_matrix(colorspace_obj.get_red_primaries(), - colorspace_obj.get_green_primaries(), - colorspace_obj.get_blue_primaries()) - matrix_type = "Primaries" + if in_colorspace == XYZ_colorspace: + if out_colorspace == XYZ_colorspace: + raise AttributeError("In and out colorspaces can't be both XYZ !") + matrix = get_colorspace_matrix(out_colorspace, primaries_only, inv=True) + elif out_colorspace == XYZ_colorspace: + matrix = get_colorspace_matrix(in_colorspace, primaries_only, inv=False) else: - matrix = get_RGB_to_XYZ_matrix(colorspace_obj.get_red_primaries(), - colorspace_obj.get_green_primaries(), - colorspace_obj.get_blue_primaries(), - colorspace_obj.get_white_point()) - matrix_type = "Primaries + white point" + matrix = get_RGB_to_RGB_matrix(in_colorspace, out_colorspace, primaries_only) + if matrix_format == 'simple': matrix_dump = matrix_to_string(matrix) - inv_matrix_dump = matrix_to_string(matrix.I) elif matrix_format == 'spimtx': matrix_dump = matrix_to_spimtx_string(matrix) - inv_matrix_dump = matrix_to_spimtx_string(matrix.I) else: matrix_dump = "{0}".format(matrix) - inv_matrix_dump = "{0}".format(matrix.I) - print "{0} to XYZ matrix ({1}, {2} output):\n".format(colorspace, matrix_type, matrix_format) + + print "{0} to {1} matrix ({2} {3} output):\n".format(in_colorspace, + out_colorspace, + primaries_only and "primaries" or + "primaries + white point", + matrix_format) print matrix_dump - print "XYZ to {0} matrix ({1}, {2} output):\n".format(colorspace, matrix_type, matrix_format) - print inv_matrix_dump def __get_options(): @@ -73,15 +69,20 @@ def __get_options(): """ # Define parser - description = 'Print RGB -> XYZ matrix' + description = 'Print RGB -> RGB matrix' parser = argparse.ArgumentParser(description=description) # RGB colorspace - parser.add_argument("-c", "--colorspace", + colorspaces = sorted(COLORSPACES.keys() + PRIVATE_COLORSPACES.keys() + [XYZ_colorspace]) + parser.add_argument("-in", "--in-colorspace", help=("Input RGB Colorspace."), type=str, - choices=sorted(COLORSPACES.keys() + - PRIVATE_COLORSPACES.keys()), - default='Rec709') + choices=colorspaces, + required=True) + parser.add_argument("-out", "--out-colorspace", + help=("Output RGB Colorspace."), + type=str, + choices=colorspaces, + required=True) # Get primarie matrix only parser.add_argument("-po", "--primaries-only", help="Primaries matrix only, doesn't include white point.", @@ -109,4 +110,4 @@ def __get_options(): if __name__ == '__main__': ARGS = __get_options() - display_matrix(ARGS.colorspace, ARGS.format, ARGS.primaries_only) + display_matrix(ARGS.in_colorspace, ARGS.out_colorspace, ARGS.format, ARGS.primaries_only) diff --git a/test/colorspaces_test.py b/test/colorspaces_test.py index e6af293..414e610 100644 --- a/test/colorspaces_test.py +++ b/test/colorspaces_test.py @@ -2,10 +2,12 @@ """ import unittest +import numpy from utils.colorspaces import (REC709, ALEXALOGCV3, WIDEGAMUT, REC2020_12B, ACESLOG_32f, sRGB, SGAMUTSLOG, SGAMUTSLOG2, - SGAMUTSLOG3, + SGAMUTSLOG3, ACESCC, ACESPROXY_10i ) +from utils.colors_helper import apply_matrix, get_RGB_to_RGB_matrix, get_colorspace_matrix class ColorspaceTest(unittest.TestCase): @@ -16,27 +18,77 @@ def test_gradation(self): """ Test encode + decode """ + colorspace_to_test = [REC709, ALEXALOGCV3, WIDEGAMUT, REC2020_12B, ACESLOG_32f, + ACESCC, sRGB, SGAMUTSLOG, SGAMUTSLOG2, SGAMUTSLOG3, ] - delta = 0.000000000000001 for space in colorspace_to_test: name = space.__class__.__name__ for value in [0.0, 1.0, 0.5]: res = space.decode_gradation(space.encode_gradation(value)) - diff = abs(res - value) message = ("{0} gradations not transparent ! " "in: {1:8f} out: {2:8f}").format(name, value, res) - self.assert_(diff < delta, message) + self.assertTrue(numpy.isclose(res, value, atol=0.00000000000001), message) + + def test_aces_proxy(self): + """Test ACES proxy (matrix + encoding) + + """ + ref_colors = [[0.001184464, 64.0, 0.001185417], + [222.875, 940.0, 222.860944204] + ] + ACES_to_proxy_matrix = get_RGB_to_RGB_matrix('ACES', 'ACESproxy_10') + proxy_to_ACES_matrix = get_RGB_to_RGB_matrix('ACESproxy_10', 'ACES') + for color in ref_colors: + aces_proxy_lin = apply_matrix(ACES_to_proxy_matrix, [color[0]]*3)[2] + aces_proxy = ACESPROXY_10i._encode_gradation(aces_proxy_lin) + self.assertEqual(aces_proxy, color[1]) + aces_proxy_lin = ACESPROXY_10i._decode_gradation(aces_proxy) + aces = apply_matrix(proxy_to_ACES_matrix, [aces_proxy_lin]*3)[0] + message = ("ACESproxy not valid ! " + "in: {0} out: {1}").format(aces, color[2]) + self.assertTrue(numpy.isclose(aces, color[2], atol=0.000001), message) + + def test_colorspace_matrices(self): + """Test matrix conversions + + """ + ACES_to_XYZ = [[0.95255239593818575, 0.0, 9.3678631660468553e-05], + [0.34396644976507507, 0.72816609661348575, -0.072132546378560786], + [0.0, 0.0, 1.0088251843515859]] + + XYZ_to_ACES = [[1.0498110174979742, 0.0, -9.7484540579252874e-05], + [-0.49590302307731976, 1.3733130458157063, 0.098240036057309993], + [0.0, 0.0, 0.99125201820049902]] + self.assertEqual(ACES_to_XYZ, get_colorspace_matrix("ACES").tolist()) + self.assertEqual(XYZ_to_ACES, get_colorspace_matrix("ACES", inv=True).tolist()) + + def test_rgb_to_rgb_matrix(self): + """Test rgb to rgb matrix + + """ + ACES_to_proxy_matrix = get_RGB_to_RGB_matrix('ACES', 'ACEScc') + ref_value = numpy.matrix([[1.4514393161, -0.2365107469, -0.2149285693], + [-0.0765537734, 1.1762296998, -0.0996759264], + [0.0083161484, -0.0060324498, 0.9977163014]]) + self.assertTrue(numpy.allclose(ACES_to_proxy_matrix, ref_value), + "Processed ACES to ACEScc matrix is different from reference ! ") + ACES_to_Rec2020_matrix = get_RGB_to_RGB_matrix("ACES", 'Rec2020_12bits') + ref_value = [[1.5128613853114372, -0.2589874063019148, -0.22978603267468098], + [-0.079036464595355627, 1.1770668323294038, -0.10075565571179679], + [0.0020912324769753847, -0.03114411050570343, 0.95350416068074784]] + self.assertTrue(numpy.allclose(ACES_to_Rec2020_matrix, ref_value), + "Processed ACES to Rec2020 matrix is different from reference ! ") if __name__ == "__main__": diff --git a/test/general_test.py b/test/general_test.py index 62ff85f..c84ae82 100644 --- a/test/general_test.py +++ b/test/general_test.py @@ -6,7 +6,7 @@ import os import tempfile import shutil -from lutLab import rgb_to_xyz_matrix +from utils.colors_helper import get_colorspace_matrix, get_RGB_to_RGB_matrix DISPLAY = False @@ -45,13 +45,6 @@ def test_lut_3d(self): # TODO pass - def test_rgb_to_matrix(self): - """Display rgb matrix - - """ - rgb_to_xyz_matrix.display_matrix('Rec709', 'spimtx') - # TODO test values - def tearDown(self): # Remove test directory shutil.rmtree(self.tmp_dir) diff --git a/utils/colors_helper.py b/utils/colors_helper.py index 1769053..353b154 100644 --- a/utils/colors_helper.py +++ b/utils/colors_helper.py @@ -3,7 +3,7 @@ .. moduleauthor:: `Marie FETIVEAU `_ """ -__version__ = "0.3" +__version__ = "0.4" import math import numpy @@ -121,11 +121,8 @@ def apply_matrix(matrix, triplet): .[float, float, float] """ - a, b, c = triplet - ap = matrix.item(0, 0) * a + matrix.item(0, 1) * b + matrix.item(0, 2) * c - bp = matrix.item(1, 0) * a + matrix.item(1, 1) * b + matrix.item(1, 2) * c - cp = matrix.item(2, 0) * a + matrix.item(2, 1) * b + matrix.item(2, 2) * c - return [ap, bp, cp] + values = numpy.matrix(triplet) + return numpy.dot(matrix, values.T).T.tolist()[0] def clamp_value(value, max_value=1.0, min_value=0.0): @@ -307,3 +304,60 @@ def get_XYZ_to_RGB_matrix(xy_red, xy_green, xy_blue, xy_white): """ return get_RGB_to_XYZ_matrix(xy_red, xy_green, xy_blue, xy_white).I + + +def get_colorspace_matrix(colorspace, primaries_only=False, inv=False): + """Return a colorspace RGB to XYZ matrix. + + Args: + colorspace (str): input colorspace. + + Kwargs: + primaries_only (bool): primaries matrix only, doesn't include white point. + inv (bool): return XYZ to RGB matrix. + + Returns: + .numpy.matrix (3x3) + + """ + from utils.colorspaces import COLORSPACES + from utils.private_colorspaces import PRIVATE_COLORSPACES + colorspace_obj = COLORSPACES.get(colorspace) or PRIVATE_COLORSPACES.get(colorspace) + + if not colorspace_obj: + raise NotImplementedError("Could not find {0} colorspace".format(colorspace)) + + if primaries_only: + matrix = get_primaries_matrix(colorspace_obj.get_red_primaries(), + colorspace_obj.get_green_primaries(), + colorspace_obj.get_blue_primaries()) + else: + matrix = get_RGB_to_XYZ_matrix(colorspace_obj.get_red_primaries(), + colorspace_obj.get_green_primaries(), + colorspace_obj.get_blue_primaries(), + colorspace_obj.get_white_point()) + if inv: + return matrix.I + return matrix + + +def get_RGB_to_RGB_matrix(in_colorspace, out_colorspace, primaries_only=False): + """Return RGB to RGB conversion matrix. + + Args: + in_colorspace (str): input colorspace. + out_colorspace (str): output colorspace. + + Kwargs: + primaries_only (bool): primaries matrix only, doesn't include white point. + + Returns: + .numpy.matrix (3x3) + + """ + # Get colorspace in to XYZ matrix + in_matrix = get_colorspace_matrix(in_colorspace, primaries_only) + # Get XYZ to colorspace out matrix + out_matrix = get_colorspace_matrix(out_colorspace, primaries_only, inv=True) + # Return scalar product of the 2 matrices + return numpy.dot(out_matrix, in_matrix) diff --git a/utils/colorspaces.py b/utils/colorspaces.py index defbdad..3f94c03 100644 --- a/utils/colorspaces.py +++ b/utils/colorspaces.py @@ -255,7 +255,7 @@ def _decode_gradation(self, value): class ACES(AbstractColorspace): - """ACES Colorspace + """ACES Colorspace (P0 primaries and linear encoding) """ def get_red_primaries(self): @@ -277,8 +277,61 @@ def _decode_gradation(self, value): return value +class ACEScg(ACES): + """ACES cg Colorspace (P1 primaries and linear encoding) + + """ + def get_red_primaries(self): + return 0.713, 0.293 + + def get_green_primaries(self): + return 0.165, 0.830 + + def get_blue_primaries(self): + return 0.128, 0.044 + + def get_white_point(self): + return 0.32168, 0.33767 + + +class ACEScc(ACEScg): + """ACEScc Colorspace (P1 primaries and log encoding) + + """ + def __init__(self): + self.enc_threshold = math.pow(2.0, -15) + self.denorm_fake0 = math.pow(2.0, -16) + self.offset = 9.72 + self.factor = 17.52 + + # encode constants + self.enc_threshold = math.pow(2.0, -15) + self.enc_threshold_cst = (math.log(self.enc_threshold * 0.5, 2.0) + self.offset) / self.factor + + # decode constants + self.dec_low_threshold = (self.offset - 15) / self.factor + self.dec_white_threshold = 65504.0 + self.dec_up_threshold = (math.log(self.dec_white_threshold, 2.0) + self.offset) / self.factor + + def _encode_gradation(self, value): + if value <= 0: + return self.enc_threshold_cst + elif value < self.enc_threshold: + return (math.log(self.denorm_fake0 + value * 0.5, 2) + self.offset) / self.factor + else: + return (math.log(value, 2.0) + self.offset) / self.factor + + def _decode_gradation(self, value): + if value < self.dec_low_threshold: + return (math.pow(2.0, value * self.factor - self.offset) - self.denorm_fake0) * 2 + elif value >= self.dec_up_threshold: + return self.dec_white_threshold + else: + return math.pow(2.0, value * self.factor - self.offset) + + class ACESlog(ACES): - """ACES LOG colorspace + """ACES LOG colorspace (deprecated by ACEScc) """ def __init__(self, is_integer=False): @@ -313,7 +366,7 @@ def _decode_gradation(self, value): return math.pow(2, (value - self.unity) / self.xperstop) -class ACESproxy(ACES): +class ACESproxy(ACEScg): """ACESproxy colorspace """ def __init__(self, cv_min, cv_max, steps_per_stop, mid_cv_offset, @@ -345,6 +398,7 @@ def __init__(self, cv_min, cv_max, steps_per_stop, mid_cv_offset, self.steps_per_stop = steps_per_stop self.mid_cv_offset = mid_cv_offset self.mid_log_offset = mid_log_offset + self.threshold = math.pow(2.0, -9.72) def float_to_cv(self, value): """Math function returning MAX(cv_min, MIN(cv_max, ROUND(value))) @@ -352,16 +406,16 @@ def float_to_cv(self, value): return max(self.cv_min, min(self.cv_max, round(value))) def _encode_gradation(self, value): - if value <= 0: + if value <= self.threshold: return self.cv_min else: - return self.float_to_cv((math.log(value, 2) - self.mid_log_offset) + return self.float_to_cv((math.log(value, 2.0) - self.mid_log_offset) * self.steps_per_stop + self.mid_cv_offset ) def _decode_gradation(self, value): - return math.pow(2, (value - self.mid_cv_offset) / self.steps_per_stop + return math.pow(2.0, (value - self.mid_cv_offset) / self.steps_per_stop + self.mid_log_offset) @@ -370,8 +424,8 @@ class ACESproxy10(ACESproxy): """ def __init__(self): - ACESproxy.__init__(self, cv_min=0, cv_max=1023, steps_per_stop=50, - mid_cv_offset=425, mid_log_offset=-2.5) + ACESproxy.__init__(self, cv_min=64.0, cv_max=940.0, steps_per_stop=50.0, + mid_cv_offset=425.0, mid_log_offset=-2.5) class ACESproxy12(ACESproxy): @@ -379,8 +433,8 @@ class ACESproxy12(ACESproxy): """ def __init__(self): - ACESproxy.__init__(self, cv_min=0, cv_max=4095, steps_per_stop=200, - mid_cv_offset=1700, mid_log_offset=-2.5) + ACESproxy.__init__(self, cv_min=256.0, cv_max=3760.0, steps_per_stop=200.0, + mid_cv_offset=1700.0, mid_log_offset=-2.5) class SGamutSLog(AbstractColorspace): @@ -487,6 +541,8 @@ def get_white_point(self): ACESLOG_16i = ACESlog(is_integer=True) ACESPROXY_10i = ACESproxy10() ACESPROXY_12i = ACESproxy12() +ACESCG = ACEScg() +ACESCC = ACEScc() sRGB = sRGB() SGAMUTSLOG = SGamutSLog() SGAMUTSLOG2 = SGamutSLog2() @@ -505,6 +561,8 @@ def get_white_point(self): 'ACESlog_16i': ACESLOG_16i, 'ACESproxy_10': ACESPROXY_10i, 'ACESproxy_12': ACESPROXY_12i, + 'ACEScg': ACESCG, + 'ACEScc': ACESCC, 'SGamutSLog': SGAMUTSLOG, 'SGamutSLog2': SGAMUTSLOG2, 'SGamutSLog3': SGAMUTSLOG3,