def adjust_image(image, target_width=WORKING_WIDTH): """ Adjusts given image so that it is horizontal and resizes it to given target width. Parameters ---------- image : array_like Image to adjust. target_width : int, optional Width the image is resized to. Returns ------- ndarray Resized image. """ width, height = image.shape[1], image.shape[0] if width < height: image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE) height, width = width, height ratio = width / target_width if np.allclose(ratio, 1): return image else: return cv2.resize(image, (as_int(target_width), as_int(height / ratio)), interpolation=cv2.INTER_CUBIC)
def adjust_image(image, target_width=WORKING_WIDTH): """ Adjusts given image so that it is horizontal and resizes it to given target width. Parameters ---------- image : array_like Image to adjust. target_width : int, optional Width the image is resized to. Returns ------- ndarray Resized image. Examples -------- >>> from colour.algebra import random_triplet_generator >>> prng = np.random.RandomState(4) >>> image = list(random_triplet_generator(8, random_state=prng)) >>> image = np.reshape(image, [2, 4, 3]) >>> adjust_image(image, 5) # doctest: +ELLIPSIS array([[[ 0.9823518..., 0.5380895..., 1.0186476...], [ 0.7563578..., 0.731978 ..., 0.4231120...], [ 0.8726642..., 0.2936055..., 0.1687892...], [ 0.8540266..., 0.1457020..., 0.2416218...], [ 0.4018965..., 0.8263517..., 0.1943257...]], <BLANKLINE> [[ 0.8791320..., 1.0425965..., 0.1503114...], [ 0.7324403..., 0.1763674..., 0.3189642...], [ 0.2110148..., 0.4074382..., 0.3919139...], [ 0.2356165..., 1.0136062..., 0.5616219...], [ 1.0039447..., 0.7759574..., 0.8924203...]]]) """ width, height = image.shape[1], image.shape[0] if width < height: image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE) height, width = width, height ratio = width / target_width if np.allclose(ratio, 1): return image else: return cv2.resize(image, (as_int(target_width), as_int(height / ratio)), interpolation=cv2.INTER_CUBIC)
def adjust_image(image, target_width=WORKING_WIDTH): """ Adjusts given image so that it is horizontal and resizes it to given target width. Parameters ---------- image : array_like Image to adjust. target_width : int, optional Width the image is resized to. Returns ------- ndarray Resized image. Examples -------- >>> from colour.algebra import random_triplet_generator >>> prng = np.random.RandomState(4) >>> image = list(random_triplet_generator(8, random_state=prng)) >>> image = np.reshape(image, [2, 4, 3]) >>> adjust_image(image, 5) # doctest: +ELLIPSIS array([[[ 0.9925326..., 0.2419374..., -0.0139522...], [ 0.6174496..., 0.3460755..., 0.3189758...], [ 0.7447774..., 0.6786660..., 0.1652180...], [ 0.9476451..., 0.6550805..., 0.2609945...], [ 0.6991505..., 0.1623470..., 1.0120867...]], <BLANKLINE> [[ 0.7269885..., 0.8556784..., 0.4049920...], [ 0.2666564..., 1.0401633..., 0.8238320...], [ 0.6419699..., 0.5442698..., 0.9082210...], [ 0.7894426..., 0.1944301..., 0.7906868...], [-0.0526997..., 0.6236684..., 0.8711482...]]]) """ width, height = image.shape[1], image.shape[0] if width < height: image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE) height, width = width, height ratio = width / target_width if np.allclose(ratio, 1): return image else: return cv2.resize(image, (as_int(target_width), as_int(height / ratio)), interpolation=cv2.INTER_CUBIC)
def test_as_int(self): """Test :func:`colour.utilities.array.as_int` definition.""" self.assertEqual(as_int(1), 1) self.assertEqual(as_int(np.array([1])), 1) np.testing.assert_almost_equal(as_int(np.array([1.0, 2.0, 3.0])), np.array([1, 2, 3])) self.assertEqual( as_int(np.array([1.0, 2.0, 3.0])).dtype, DEFAULT_INT_DTYPE) self.assertIsInstance(as_int(1), DEFAULT_INT_DTYPE)
def test_transfer_functions(self): """ Tests transfer functions reciprocity. """ ignored_transfer_functions = ('ACESproxy', 'DICOM GSDF', 'Filmic Pro 6') decimals = {'D-Log': 1, 'F-Log': 4} reciprocal_mappings = [ (LOG_ENCODINGS, LOG_DECODINGS), (OETFS, OETF_INVERSES), (EOTFS, EOTF_INVERSES), (CCTF_ENCODINGS, CCTF_DECODINGS), (OOTFS, OOTF_INVERSES), ] samples = np.hstack([ np.linspace(0, 1, as_int(1e5)), np.linspace(0, 65504, 65504 * 10) ]) for encoding_mapping, _decoding_mapping in reciprocal_mappings: for name in encoding_mapping: if name in ignored_transfer_functions: continue encoded_s = CCTF_ENCODINGS[name](samples) decoded_s = CCTF_DECODINGS[name](encoded_s) np.testing.assert_almost_equal(samples, decoded_s, decimal=decimals.get(name, 7))
def test_cctf(self): """ Tests colour component transfer functions from the :attr:`colour.models.rgb.rgb_colourspace.RGB_COLOURSPACES` attribute colourspace models. """ ignored_colourspaces = ('ACESproxy', ) decimals = {'DJI D-Gamut': 1, 'F-Gamut': 4} samples = np.hstack([ np.linspace(0, 1, as_int(1e5)), np.linspace(0, 65504, 65504 * 10) ]) for colourspace in RGB_COLOURSPACES.values(): if colourspace.name in ignored_colourspaces: continue cctf_encoding_s = colourspace.cctf_encoding(samples) cctf_decoding_s = colourspace.cctf_decoding(cctf_encoding_s) np.testing.assert_almost_equal(samples, cctf_decoding_s, decimal=decimals.get( colourspace.name, 7))
def test_as_int(self): """ Tests :func:`colour.utilities.array.as_int` definition. """ self.assertEqual(as_int(1), 1) self.assertEqual(as_int(np.array([1])), 1) np.testing.assert_almost_equal( as_int(np.array([1.0, 2.0, 3.0])), np.array([1, 2, 3])) self.assertEqual( as_int(np.array([1.0, 2.0, 3.0])).dtype, DEFAULT_INT_DTYPE) self.assertIsInstance(as_int(1), int)
def swatch_masks( width: Integer, height: Integer, swatches_h: Integer, swatches_v: Integer, samples: Integer, ) -> Tuple[NDArray, ...]: """ Return swatch masks for given image width and height and swatches count. Parameters ---------- width Image width. height Image height. swatches_h Horizontal swatches count. swatches_v Vertical swatches count. samples Samples count. Returns ------- :class:`tuple` Tuple of swatch masks. Examples -------- >>> from pprint import pprint >>> pprint(swatch_masks(16, 8, 4, 2, 1)) # doctest: +ELLIPSIS (array([2, 2, 2, 2]...), array([2, 2, 6, 6]...), array([ 2, 2, 10, 10]...), array([ 2, 2, 14, 14]...), array([6, 6, 2, 2]...), array([6, 6, 6, 6]...), array([ 6, 6, 10, 10]...), array([ 6, 6, 14, 14]...)) """ samples_half = as_int(samples / 2) masks = [] offset_h = width / swatches_h / 2 offset_v = height / swatches_v / 2 for j in np.linspace(offset_v, height - offset_v, swatches_v): for i in np.linspace(offset_h, width - offset_h, swatches_h): masks.append( as_int_array([ j - samples_half, j + samples_half, i - samples_half, i + samples_half, ])) return tuple(masks)
def swatch_masks(width, height, swatches_h, swatches_v, samples): """ Returns swatch masks for given image width and height and swatches count. Parameters ---------- width : int Image width. height : height Image height. swatches_h : int Horizontal swatches count. swatches_v : int Vertical swatches count. samples : int Samples count. Returns ------- list List of swatch masks. Examples -------- >>> from pprint import pprint >>> pprint(swatch_masks(16, 8, 4, 2, 1)) [array([2, 2, 2, 2]), array([2, 2, 6, 6]), array([ 2, 2, 10, 10]), array([ 2, 2, 14, 14]), array([6, 6, 2, 2]), array([6, 6, 6, 6]), array([ 6, 6, 10, 10]), array([ 6, 6, 14, 14])] """ samples = as_int(samples / 2) masks = [] offset_h = width / swatches_h / 2 offset_v = height / swatches_v / 2 for j in np.linspace(offset_v, height - offset_v, swatches_v): for i in np.linspace(offset_h, width - offset_h, swatches_h): masks.append( as_int_array( [j - samples, j + samples, i - samples, i + samples])) return masks
def load_TCS_CIE2017(shape): """ Loads the *CIE 2017 Test Colour Samples* dataset appropriate for the given spectral shape. The datasets are cached and won't be loaded again on subsequent calls to this definition. Parameters ---------- shape : SpectralShape Spectral shape of the tested illuminant. Returns ------- MultiSpectralDistributions *CIE 2017 Test Colour Samples* dataset. Examples -------- >>> sds_tcs = load_TCS_CIE2017(SpectralShape(interval=5)) >>> len(sds_tcs.labels) 99 """ global _CACHE_TCS_CIE2017 interval = shape.interval assert interval in (1, 5), ( 'Spectral shape interval must be either 1nm or 5nm!') filename = 'tcs_cfi2017_{0}_nm.csv.gz'.format(as_int(interval)) if filename in _CACHE_TCS_CIE2017: return _CACHE_TCS_CIE2017[filename] data = np.genfromtxt(str( os.path.join(RESOURCES_DIRECTORY_CIE2017, filename)), delimiter=',') labels = ['TCS{0} (CIE 2017)'.format(i) for i in range(99)] return MultiSpectralDistributions(data[:, 1:], data[:, 0], labels)
def load(self): """ Syncs, parses, converts and returns the *Brendel (2020)* *Measured Commercial LED Spectra* dataset content. Returns ------- OrderedDict *Brendel (2020)* *Measured Commercial LED Spectra* dataset content. Examples -------- >>> from colour_datasets.utilities import suppress_stdout >>> dataset = DatasetLoader_Brendel2020() >>> with suppress_stdout(): ... dataset.load() >>> len(dataset.content.keys()) 29 """ super(DatasetLoader_Brendel2020, self).sync() self._content = OrderedDict() wavelengths = SpectralShape(350, 700, 2).range() csv_path = os.path.join(self.record.repository, 'dataset', 'led_spd_350_700.csv') for i, values in enumerate( np.loadtxt(csv_path, delimiter=',', skiprows=1)): peak = as_int(wavelengths[np.argmax(values)]) name = '{0}nm - LED {1} - Brendel (2020)'.format(peak, i) self._content[name] = SpectralDistribution( values, wavelengths, name=name, interpolator=LinearInterpolator) return self._content
def full_to_legal( CV: Union[FloatingOrArrayLike, IntegerOrArrayLike], bit_depth: Integer = 10, in_int: Boolean = False, out_int: Boolean = False, ) -> Union[FloatingOrNDArray, IntegerOrNDArray]: """ Convert given code value :math:`CV` or float equivalent of a code value at a given bit depth from full range (full swing) to legal range (studio swing). Parameters ---------- CV Full range code value :math:`CV` or float equivalent of a code value at a given bit depth. bit_depth Bit depth used for conversion. in_int Whether to treat the input value as integer code value or float equivalent of a code value at a given bit depth. out_int Whether to return value as integer code value or float equivalent of a code value at a given bit depth. Returns ------- :class:`numpy.floating` or :class:`numpy.integer` or :class:`numpy.ndarray` Legal range code value :math:`CV` or float equivalent of a code value at a given bit depth. Examples -------- >>> full_to_legal(0.0) # doctest: +ELLIPSIS 0.0625610... >>> full_to_legal(1.0) # doctest: +ELLIPSIS 0.9188660... >>> full_to_legal(0.0, out_int=True) 64 >>> full_to_legal(1.0, out_int=True) 940 >>> full_to_legal(0, in_int=True) # doctest: +ELLIPSIS 0.0625610... >>> full_to_legal(1023, in_int=True) # doctest: +ELLIPSIS 0.9188660... >>> full_to_legal(0, in_int=True, out_int=True) 64 >>> full_to_legal(1023, in_int=True, out_int=True) 940 """ CV = as_float_array(CV) MV = 2**bit_depth - 1 CV_legal = as_int_array(np.round(CV / MV)) if in_int else CV B, W = CV_range(bit_depth, True, True) CV_legal = (W - B) * CV_legal + B if out_int: return as_int(np.round(CV_legal)) else: return as_float(CV_legal / MV)
def legal_to_full( CV: Union[FloatingOrArrayLike, IntegerOrArrayLike], bit_depth: Integer = 10, in_int: Boolean = False, out_int: Boolean = False, ) -> Union[FloatingOrNDArray, IntegerOrNDArray]: """ Convert given code value :math:`CV` or float equivalent of a code value at a given bit depth from legal range (studio swing) to full range (full swing). Parameters ---------- CV Legal range code value :math:`CV` or float equivalent of a code value at a given bit depth. bit_depth Bit depth used for conversion. in_int Whether to treat the input value as integer code value or float equivalent of a code value at a given bit depth. out_int Whether to return value as integer code value or float equivalent of a code value at a given bit depth. Returns ------- :class:`numpy.floating` or :class:`numpy.integer` or :class:`numpy.ndarray` Full range code value :math:`CV` or float equivalent of a code value at a given bit depth. Examples -------- >>> legal_to_full(64 / 1023) 0.0 >>> legal_to_full(940 / 1023) 1.0 >>> legal_to_full(64 / 1023, out_int=True) 0 >>> legal_to_full(940 / 1023, out_int=True) 1023 >>> legal_to_full(64, in_int=True) 0.0 >>> legal_to_full(940, in_int=True) 1.0 >>> legal_to_full(64, in_int=True, out_int=True) 0 >>> legal_to_full(940, in_int=True, out_int=True) 1023 """ CV = as_float_array(CV) MV = 2**bit_depth - 1 CV_full = as_int_array(np.round(CV)) if in_int else CV * MV B, W = CV_range(bit_depth, True, True) CV_full = (CV_full - B) / (W - B) if out_int: return as_int(np.round(CV_full * MV)) else: return as_float(CV_full)
def adjust_image( image: ArrayLike, target_width: Integer, interpolation_method: Literal[ # type: ignore[misc] cv2.INTER_AREA, cv2.INTER_BITS, cv2.INTER_BITS2, cv2.INTER_CUBIC, cv2.INTER_LANCZOS4, cv2.INTER_LINEAR, ] = cv2.INTER_CUBIC, ) -> NDArray: """ Adjust given image so that it is horizontal and resizes it to given target width. Parameters ---------- image Image to adjust. target_width Width the image is resized to. interpolation_method Interpolation method. Returns ------- :class:`numpy.ndarray` Resized image. Examples -------- >>> from colour.algebra import random_triplet_generator >>> prng = np.random.RandomState(4) >>> image = list(random_triplet_generator(8, random_state=prng)) >>> image = np.reshape(image, [2, 4, 3]) >>> adjust_image(image, 5) # doctest: +ELLIPSIS array([[[ 0.9925325..., 0.2419374..., -0.0139522...], [ 0.6174497..., 0.3460756..., 0.3189758...], [ 0.7447774..., 0.678666 ..., 0.1652180...], [ 0.9476452..., 0.6550805..., 0.2609945...], [ 0.6991505..., 0.1623470..., 1.0120867...]], <BLANKLINE> [[ 0.7269885..., 0.8556784..., 0.4049920...], [ 0.2666565..., 1.0401633..., 0.8238320...], [ 0.6419699..., 0.5442698..., 0.9082211...], [ 0.7894426..., 0.1944301..., 0.7906868...], [-0.0526997..., 0.6236685..., 0.8711483...]]], dtype=float32) """ image = as_float_array(image, FLOAT_DTYPE_DEFAULT)[..., :3] width, height = image.shape[1], image.shape[0] if width < height: image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE) height, width = width, height ratio = width / target_width if np.allclose(ratio, 1): return cast(NDArray, image) else: return cv2.resize( image, (as_int(target_width), as_int(height / ratio)), interpolation=interpolation_method, )
def eotf_inverse_DICOMGSDF(L, out_int=False): """ Defines the *DICOM - Grayscale Standard Display Function* inverse electro-optical transfer function (EOTF / EOCF). Parameters ---------- L : numeric or array_like *Luminance* :math:`L`. out_int : bool, optional Whether to return value as integer code value or float equivalent of a code value at a given bit depth. Returns ------- numeric or ndarray Just-Noticeable Difference (JND) Index, :math:`j`. Notes ----- +------------+-----------------------+---------------+ | **Domain** | **Scale - Reference** | **Scale - 1** | +============+=======================+===============+ | ``L`` | [0, 1] | [0, 1] | +------------+-----------------------+---------------+ +------------+-----------------------+---------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +============+=======================+===============+ | ``J`` | [0, 1] | [0, 1] | +------------+-----------------------+---------------+ References ---------- :cite:`NationalElectricalManufacturersAssociation2004b` Examples -------- >>> eotf_inverse_DICOMGSDF(130.0662) # doctest: +ELLIPSIS 0.5004862... >>> eotf_inverse_DICOMGSDF(130.0662, out_int=True) 512 """ L = to_domain_1(L) L_lg = np.log10(L) A = CONSTANTS_DICOMGSDF.A B = CONSTANTS_DICOMGSDF.B C = CONSTANTS_DICOMGSDF.C D = CONSTANTS_DICOMGSDF.D E = CONSTANTS_DICOMGSDF.E F = CONSTANTS_DICOMGSDF.F G = CONSTANTS_DICOMGSDF.G H = CONSTANTS_DICOMGSDF.H I = CONSTANTS_DICOMGSDF.I # noqa J = (A + B * L_lg + C * L_lg ** 2 + D * L_lg ** 3 + E * L_lg ** 4 + F * L_lg ** 5 + G * L_lg ** 6 + H * L_lg ** 7 + I * L_lg ** 8) if out_int: return as_int(np.round(J)) else: return as_float(from_range_1(J / 1023))
def eotf_inverse_DCDM( XYZ: FloatingOrArrayLike, out_int: Boolean = False ) -> Union[FloatingOrNDArray, IntegerOrNDArray]: """ Define the *DCDM* inverse electro-optical transfer function (EOTF). Parameters ---------- XYZ *CIE XYZ* tristimulus values. out_int Whether to return value as integer code value or float equivalent of a code value at a given bit depth. Returns ------- :class:`numpy.floating` or :class:`numpy.integer` or :class:`numpy.ndarray` Non-linear *CIE XYZ'* tristimulus values. Warnings -------- *DCDM* is an absolute transfer function. Notes ----- - *DCDM* is an absolute transfer function, thus the domain and range values for the *Reference* and *1* scales are only indicative that the data is not affected by scale transformations. +----------------+-----------------------+---------------+ | **Domain \\*** | **Scale - Reference** | **Scale - 1** | +================+=======================+===============+ | ``XYZ`` | ``UN`` | ``UN`` | +----------------+-----------------------+---------------+ +----------------+-----------------------+---------------+ | **Range \\*** | **Scale - Reference** | **Scale - 1** | +================+=======================+===============+ | ``XYZ_p`` | ``UN`` | ``UN`` | +----------------+-----------------------+---------------+ \\* This definition has an output integer switch, thus the domain-range scale information is only given for the floating point mode. References ---------- :cite:`DigitalCinemaInitiatives2007b` Examples -------- >>> eotf_inverse_DCDM(0.18) # doctest: +ELLIPSIS 0.1128186... >>> eotf_inverse_DCDM(0.18, out_int=True) 462 """ XYZ = as_float_array(XYZ) XYZ_p = spow(XYZ / 52.37, 1 / 2.6) if out_int: return as_int(np.round(4095 * XYZ_p)) else: return as_float(XYZ_p)
def cctf_encoding_ROMMRGB(X, bit_depth=8, out_int=False): """ Defines the *ROMM RGB* encoding colour component transfer function (Encoding CCTF). Parameters ---------- X : numeric or array_like Linear data :math:`X_{ROMM}`. bit_depth : int, optional Bit depth used for conversion. out_int : bool, optional Whether to return value as integer code value or float equivalent of a code value at a given bit depth. Returns ------- numeric or ndarray Non-linear data :math:`X'_{ROMM}`. Notes ----- +----------------+-----------------------+---------------+ | **Domain \\*** | **Scale - Reference** | **Scale - 1** | +================+=======================+===============+ | ``X`` | [0, 1] | [0, 1] | +----------------+-----------------------+---------------+ +----------------+-----------------------+---------------+ | **Range \\*** | **Scale - Reference** | **Scale - 1** | +================+=======================+===============+ | ``X_p`` | [0, 1] | [0, 1] | +----------------+-----------------------+---------------+ \\* This definition has an output integer switch, thus the domain-range scale information is only given for the floating point mode. References ---------- :cite:`ANSI2003a`, :cite:`Spaulding2000b` Examples -------- >>> cctf_encoding_ROMMRGB(0.18) # doctest: +ELLIPSIS 0.3857114... >>> cctf_encoding_ROMMRGB(0.18, out_int=True) 98 """ X = to_domain_1(X) I_max = 2**bit_depth - 1 E_t = 16**(1.8 / (1 - 1.8)) X_p = np.where(X < E_t, X * 16 * I_max, spow(X, 1 / 1.8) * I_max) if out_int: return as_int(np.round(X_p)) else: return as_float(from_range_1(X_p / I_max))
def oetf_ROMMRGB(X, bit_depth=8, out_int=False): """ Defines the *ROMM RGB* encoding opto-electronic transfer function (OETF / OECF). Parameters ---------- X : numeric or array_like Linear data :math:`X_{ROMM}`. bit_depth : int, optional Bit depth used for conversion. out_int : bool, optional Whether to return value as integer code value or float equivalent of a code value at a given bit depth. Returns ------- numeric or ndarray Non-linear data :math:`X'_{ROMM}`. Notes ----- +----------------+-----------------------+---------------+ | **Domain \\*** | **Scale - Reference** | **Scale - 1** | +================+=======================+===============+ | ``X`` | [0, 1] | [0, 1] | +----------------+-----------------------+---------------+ +----------------+-----------------------+---------------+ | **Range \\*** | **Scale - Reference** | **Scale - 1** | +================+=======================+===============+ | ``X_p`` | [0, 1] | [0, 1] | +----------------+-----------------------+---------------+ - \\* This definition has an output integer switch, thus the domain-range scale information is only given for the floating point mode. References ---------- :cite:`ANSI2003a`, :cite:`Spaulding2000b` Examples -------- >>> oetf_ROMMRGB(0.18) # doctest: +ELLIPSIS 0.3857114... >>> oetf_ROMMRGB(0.18, out_int=True) 98 """ X = to_domain_1(X) I_max = 2 ** bit_depth - 1 E_t = 16 ** (1.8 / (1 - 1.8)) X_p = np.where(X < E_t, X * 16 * I_max, spow(X, 1 / 1.8) * I_max) if out_int: return as_int(np.round(X_p)) else: return as_float(from_range_1(X_p / I_max))
def log_encoding_ACESproxy( lin_AP1: FloatingOrArrayLike, bit_depth: Literal[10, 12] = 10, out_int: Boolean = False, constants: Dict = CONSTANTS_ACES_PROXY, ) -> Union[FloatingOrNDArray, IntegerOrNDArray]: """ Define the *ACESproxy* colourspace log encoding curve / opto-electronic transfer function. Parameters ---------- lin_AP1 *lin_AP1* value. bit_depth *ACESproxy* bit depth. out_in Whether to return value as integer code value or float equivalent of a code value at a given bit depth. constants *ACESproxy* constants. Returns ------- :class:`numpy.floating` or :class:`numpy.integer` or :class:`numpy.ndarray` *ACESproxy* non-linear value. Notes ----- +----------------+-----------------------+---------------+ | **Domain \\*** | **Scale - Reference** | **Scale - 1** | +================+=======================+===============+ | ``lin_AP1`` | [0, 1] | [0, 1] | +----------------+-----------------------+---------------+ +----------------+-----------------------+---------------+ | **Range \\*** | **Scale - Reference** | **Scale - 1** | +================+=======================+===============+ | ``ACESproxy`` | [0, 1] | [0, 1] | +----------------+-----------------------+---------------+ \\* This definition has an output integer switch, thus the domain-range scale information is only given for the floating point mode. References ---------- :cite:`TheAcademyofMotionPictureArtsandSciences2014q`, :cite:`TheAcademyofMotionPictureArtsandSciences2014r`, :cite:`TheAcademyofMotionPictureArtsandSciences2014s`, :cite:`TheAcademyofMotionPictureArtsandSciencese` Examples -------- >>> log_encoding_ACESproxy(0.18) # doctest: +ELLIPSIS 0.4164222... >>> log_encoding_ACESproxy(0.18, out_int=True) 426 """ lin_AP1 = to_domain_1(lin_AP1) CV_min = constants[bit_depth].CV_min CV_max = constants[bit_depth].CV_max mid_CV_offset = constants[bit_depth].mid_CV_offset mid_log_offset = constants[bit_depth].mid_log_offset steps_per_stop = constants[bit_depth].steps_per_stop def float_2_cv(x: Floating) -> Floating: """Convert given numeric to code value.""" return np.maximum(CV_min, np.minimum(CV_max, np.round(x))) ACESproxy = np.where( lin_AP1 > 2**-9.72, float_2_cv((np.log2(lin_AP1) + mid_log_offset) * steps_per_stop + mid_CV_offset), np.resize(CV_min, lin_AP1.shape), ) if out_int: return as_int(np.round(ACESproxy)) else: return as_float(from_range_1(ACESproxy / (2**bit_depth - 1)))
def colour_checkers_coordinates_segmentation(image, additional_data=False): """ Detects the colour checkers coordinates in given image :math:`image` using segmentation. This is the core detection definition. The process is a follows: - Input image :math:`image` is converted to a grayscale image :math:`image_g`. - Image :math:`image_g` is denoised. - Image :math:`image_g` is thresholded/segmented to image :math:`image_s`. - Image :math:`image_s` is eroded and dilated to cleanup remaining noise. - Contours are detected on image :math:`image_s`. - Contours are filtered to only keep squares/swatches above and below defined surface area. - Squares/swatches are clustered to isolate region-of-interest that are potentially colour checkers: Contours are scaled by a third so that colour checkers swatches are expected to be joined, creating a large rectangular cluster. Rectangles are fitted to the clusters. - Clusters with an aspect ratio different to the expected one are rejected, a side-effect is that the complementary pane of the *X-Rite* *ColorChecker Passport* is omitted. - Clusters with a number of swatches close to :attr:`SWATCHES` are kept. Parameters ---------- image : array_like Image to detect the colour checkers in. additional_data : bool, optional Whether to output additional data. Returns ------- list or ColourCheckersDetectionData List of colour checkers coordinates or :class:`ColourCheckersDetectionData` class instance with additional data. Notes ----- - Multiple colour checkers can be detected if presented in ``image``. Examples -------- >>> import os >>> from colour import read_image >>> from colour_checker_detection import TESTS_RESOURCES_DIRECTORY >>> path = os.path.join(TESTS_RESOURCES_DIRECTORY, ... 'colour_checker_detection', 'detection', ... 'IMG_1967.png') >>> image = read_image(path) >>> colour_checkers_coordinates_segmentation(image) [array([[1065, 707], [ 369, 688], [ 382, 226], [1078, 246]])] """ image = as_8_bit_BGR_image(adjust_image(image, WORKING_WIDTH)) width, height = image.shape[1], image.shape[0] maximum_area = width * height / SWATCHES minimum_area = width * height / SWATCHES / SWATCH_MINIMUM_AREA_FACTOR block_size = as_int(WORKING_WIDTH * 0.015) block_size = block_size - block_size % 2 + 1 # Thresholding/Segmentation. image_g = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) image_g = cv2.fastNlMeansDenoising(image_g, None, 10, 7, 21) image_s = cv2.adaptiveThreshold(image_g, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, block_size, 3) # Cleanup. kernel = np.ones((3, 3), np.uint8) image_c = cv2.erode(image_s, kernel, iterations=1) image_c = cv2.dilate(image_c, kernel, iterations=1) # Detecting contours. _image_c, contours, _hierarchy = cv2.findContours(image_c, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE) # Filtering squares/swatches contours. swatches = [] for contour in contours: curve = cv2.approxPolyDP(contour, 0.01 * cv2.arcLength(contour, True), True) if minimum_area < cv2.contourArea(curve) < maximum_area and is_square( curve): swatches.append(as_int_array(cv2.boxPoints( cv2.minAreaRect(curve)))) # Clustering squares/swatches. clusters = np.zeros(image.shape, dtype=np.uint8) for swatch in [ as_int_array(scale_contour(swatch, 1 + 1 / 3)) for swatch in swatches ]: cv2.drawContours(clusters, [swatch], -1, [255] * 3, -1) clusters = cv2.cvtColor(clusters, cv2.COLOR_RGB2GRAY) _image_c, clusters, _hierarchy = cv2.findContours(clusters, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) clusters = [ as_int_array( scale_contour(cv2.boxPoints(cv2.minAreaRect(cluster)), 0.975)) for cluster in clusters ] # Filtering clusters using their aspect ratio. filtered_clusters = [] for cluster in clusters[:]: rectangle = cv2.minAreaRect(cluster) width = max(rectangle[1][0], rectangle[1][1]) height = min(rectangle[1][0], rectangle[1][1]) ratio = width / height if ASPECT_RATIO * 0.9 < ratio < ASPECT_RATIO * 1.1: filtered_clusters.append(cluster) clusters = filtered_clusters # Filtering swatches within cluster. counts = [] for cluster in clusters: count = 0 for swatch in swatches: if cv2.pointPolygonTest(cluster, contour_centroid(swatch), False) == 1: count += 1 counts.append(count) counts = np.array(counts) indexes = np.where( np.logical_and(counts >= SWATCHES * 0.75, counts <= SWATCHES * 1.25))[0].tolist() colour_checkers = [clusters[i] for i in indexes] if additional_data: return ColourCheckersDetectionData(colour_checkers, clusters, swatches, image_c) else: return colour_checkers
def polynomial_expansion_Finlayson2015(RGB, degree=1, root_polynomial_expansion=True): """ Performs polynomial expansion of given *RGB* colourspace array using *Finlayson et al. (2015)* method. Parameters ---------- RGB : array_like *RGB* colourspace array to expand. degree : int, optional Expanded polynomial degree. root_polynomial_expansion : bool Whether to use the root-polynomials set for the expansion. Returns ------- ndarray Expanded *RGB* colourspace array. References ---------- :cite:`Finlayson2015` Examples -------- >>> RGB = np.array([0.17224810, 0.09170660, 0.06416938]) >>> polynomial_expansion_Finlayson2015(RGB, degree=2) # doctest: +ELLIPSIS array([ 0.1722481..., 0.0917066..., 0.06416938...\ , 0.0078981..., 0.0029423..., 0.0055265...]) """ R, G, B = tsplit(RGB) # TODO: Generalise polynomial expansion. existing_degrees = np.array([1, 2, 3, 4]) closest_degree = as_int(closest(existing_degrees, degree)) if closest_degree != degree: raise ValueError('"Finlayson et al. (2015)" method does not define ' 'a polynomial expansion for {0} degree, ' 'closest polynomial expansion is {1} degree!'.format( degree, closest_degree)) if degree == 1: return RGB elif degree == 2: if root_polynomial_expansion: return tstack([ R, G, B, (R * G) ** 1 / 2, (G * B) ** 1 / 2, (R * B) ** 1 / 2 ]) else: return tstack( [R, G, B, R ** 2, G ** 2, B ** 2, R * G, G * B, R * B]) elif degree == 3: if root_polynomial_expansion: return tstack([ R, G, B, (R * G) ** 1 / 2, (G * B) ** 1 / 2, (R * B) ** 1 / 2, (R * G ** 2) ** 1 / 3, (G * B ** 2) ** 1 / 3, (R * B ** 2) ** 1 / 3, (G * R ** 2) ** 1 / 3, (B * G ** 2) ** 1 / 3, (B * R ** 2) ** 1 / 3, (R * G * B) ** 1 / 3 ]) else: return tstack([ R, G, B, R ** 2, G ** 2, B ** 2, R * G, G * B, R * B, R ** 3, G ** 3, B ** 3, R * G ** 2, G * B ** 2, R * B ** 2, G * R ** 2, B * G ** 2, B * R ** 2, R * G * B ]) elif degree == 4: if root_polynomial_expansion: return tstack([ R, G, B, (R * G) ** 1 / 2, (G * B) ** 1 / 2, (R * B) ** 1 / 2, (R * G ** 2) ** 1 / 3, (G * B ** 2) ** 1 / 3, (R * B ** 2) ** 1 / 3, (G * R ** 2) ** 1 / 3, (B * G ** 2) ** 1 / 3, (B * R ** 2) ** 1 / 3, (R * G * B) ** 1 / 3, (R ** 3 * G) ** 1 / 4, (R ** 3 * B) ** 1 / 4, (G ** 3 * R) ** 1 / 4, (G ** 3 * B) ** 1 / 4, (B ** 3 * R) ** 1 / 4, (B ** 3 * G) ** 1 / 4, (R ** 2 * G * B) ** 1 / 4, (G ** 2 * R * B) ** 1 / 4, (B ** 2 * R * G) ** 1 / 4 ]) else: return tstack([ R, G, B, R ** 2, G ** 2, B ** 2, R * G, G * B, R * B, R ** 3, G ** 3, B ** 3, R * G ** 2, G * B ** 2, R * B ** 2, G * R ** 2, B * G ** 2, B * R ** 2, R * G * B, R ** 4, G ** 4, B ** 4, R ** 3 * G, R ** 3 * B, G ** 3 * R, G ** 3 * B, B ** 3 * R, B ** 3 * G, R ** 2 * G ** 2, G ** 2 * B ** 2, R ** 2 * B ** 2, R ** 2 * G * B, G ** 2 * R * B, B ** 2 * R * G ])
def matrix_augmented_Cheung2004( RGB: ArrayLike, terms: Literal[3, 5, 7, 8, 10, 11, 14, 16, 17, 19, 20, 22] = 3, ) -> NDArray: """ Perform polynomial expansion of given *RGB* colourspace array using *Cheung et al. (2004)* method. Parameters ---------- RGB *RGB* colourspace array to expand. terms Number of terms of the expanded polynomial. Returns ------- :class:`numpy.ndarray` Expanded *RGB* colourspace array. Notes ----- - This definition combines the augmented matrices given in :cite:`Cheung2004` and :cite:`Westland2004`. References ---------- :cite:`Cheung2004`, :cite:`Westland2004` Examples -------- >>> RGB = np.array([0.17224810, 0.09170660, 0.06416938]) >>> matrix_augmented_Cheung2004(RGB, terms=5) # doctest: +ELLIPSIS array([ 0.1722481..., 0.0917066..., 0.0641693..., 0.0010136..., 1...]) """ RGB = as_float_array(RGB) R, G, B = tsplit(RGB) tail = ones(R.shape) existing_terms = np.array([3, 5, 7, 8, 10, 11, 14, 16, 17, 19, 20, 22]) closest_terms = as_int(closest(existing_terms, terms)) if closest_terms != terms: raise ValueError( f'"Cheung et al. (2004)" method does not define an augmented ' f"matrix with {terms} terms, closest augmented matrix has " f"{closest_terms} terms!") if terms == 3: return RGB elif terms == 5: return tstack([ R, G, B, R * G * B, tail, ]) elif terms == 7: return tstack([ R, G, B, R * G, R * B, G * B, tail, ]) elif terms == 8: return tstack([ R, G, B, R * G, R * B, G * B, R * G * B, tail, ]) elif terms == 10: return tstack([ R, G, B, R * G, R * B, G * B, R**2, G**2, B**2, tail, ]) elif terms == 11: return tstack([ R, G, B, R * G, R * B, G * B, R**2, G**2, B**2, R * G * B, tail, ]) elif terms == 14: return tstack([ R, G, B, R * G, R * B, G * B, R**2, G**2, B**2, R * G * B, R**3, G**3, B**3, tail, ]) elif terms == 16: return tstack([ R, G, B, R * G, R * B, G * B, R**2, G**2, B**2, R * G * B, R**2 * G, G**2 * B, B**2 * R, R**3, G**3, B**3, ]) elif terms == 17: return tstack([ R, G, B, R * G, R * B, G * B, R**2, G**2, B**2, R * G * B, R**2 * G, G**2 * B, B**2 * R, R**3, G**3, B**3, tail, ]) elif terms == 19: return tstack([ R, G, B, R * G, R * B, G * B, R**2, G**2, B**2, R * G * B, R**2 * G, G**2 * B, B**2 * R, R**2 * B, G**2 * R, B**2 * G, R**3, G**3, B**3, ]) elif terms == 20: return tstack([ R, G, B, R * G, R * B, G * B, R**2, G**2, B**2, R * G * B, R**2 * G, G**2 * B, B**2 * R, R**2 * B, G**2 * R, B**2 * G, R**3, G**3, B**3, tail, ]) elif terms == 22: return tstack([ R, G, B, R * G, R * B, G * B, R**2, G**2, B**2, R * G * B, R**2 * G, G**2 * B, B**2 * R, R**2 * B, G**2 * R, B**2 * G, R**3, G**3, B**3, R**2 * G * B, R * G**2 * B, R * G * B**2, ])
def sd_reference_illuminant(CCT, shape): """ Computes the reference illuminant for a given correlated colour temperature :math:`T_{cp}` for use in *CIE 2017 Colour Fidelity Index* (CFI) computation. Parameters ---------- CCT : numeric Correlated colour temperature :math:`T_{cp}`. shape : SpectralShape Desired shape of the returned spectral distribution. Returns ------- SpectralDistribution Reference illuminant for *CIE 2017 Colour Fidelity Index* (CFI) computation. Examples -------- >>> from colour.utilities import numpy_print_options >>> with numpy_print_options(suppress=True): ... sd_reference_illuminant( # doctest: +ELLIPSIS ... 4224.469705295263300, SpectralShape(380, 780, 20)) SpectralDistribution([[ 380. , 0.0034089...], [ 400. , 0.0044208...], [ 420. , 0.0053260...], [ 440. , 0.0062857...], [ 460. , 0.0072767...], [ 480. , 0.0080207...], [ 500. , 0.0086590...], [ 520. , 0.0092242...], [ 540. , 0.0097686...], [ 560. , 0.0101444...], [ 580. , 0.0104475...], [ 600. , 0.0107642...], [ 620. , 0.0110439...], [ 640. , 0.0112535...], [ 660. , 0.0113922...], [ 680. , 0.0115185...], [ 700. , 0.0113155...], [ 720. , 0.0108192...], [ 740. , 0.0111582...], [ 760. , 0.0101299...], [ 780. , 0.0105638...]], interpolator=SpragueInterpolator, interpolator_kwargs={}, extrapolator=Extrapolator, extrapolator_kwargs={...}) """ if CCT <= 5000: sd_planckian = sd_blackbody(CCT, shape) if CCT >= 4000: xy = CCT_to_xy_CIE_D(CCT) sd_daylight = sd_CIE_illuminant_D_series(xy).align(shape) if CCT < 4000: sd_reference = sd_planckian elif 4000 <= CCT <= 5000: # Planckian and daylight illuminant must be normalised so that the # mixture isn't biased. sd_planckian /= sd_to_XYZ(sd_planckian)[1] sd_daylight /= sd_to_XYZ(sd_daylight)[1] # Mixture: 4200K should be 80% Planckian, 20% CIE Illuminant D Series. m = (CCT - 4000) / 1000 values = lerp(sd_planckian.values, sd_daylight.values, m) name = ('{0}K Blackbody & CIE Illuminant D Series Mixture - {1:.1f}%'. format(as_int(CCT), 100 * m)) sd_reference = SpectralDistribution(values, shape.range(), name=name) elif CCT > 5000: sd_reference = sd_daylight return sd_reference
def oetf_RIMMRGB(X, bit_depth=8, out_int=False, E_clip=2.0): """ Defines the *RIMM RGB* encoding opto-electronic transfer function (OETF / OECF). *RIMM RGB* encoding non-linearity is based on that specified by *Recommendation ITU-R BT.709-6*. Parameters ---------- X : numeric or array_like Linear data :math:`X_{RIMM}`. bit_depth : int, optional Bit depth used for conversion. out_int : bool, optional Whether to return value as integer code value or float equivalent of a code value at a given bit depth. E_clip : numeric, optional Maximum exposure level. Returns ------- numeric or ndarray Non-linear data :math:`X'_{RIMM}`. Notes ----- +----------------+-----------------------+---------------+ | **Domain \\*** | **Scale - Reference** | **Scale - 1** | +================+=======================+===============+ | ``X`` | [0, 1] | [0, 1] | +----------------+-----------------------+---------------+ +----------------+-----------------------+---------------+ | **Range \\*** | **Scale - Reference** | **Scale - 1** | +================+=======================+===============+ | ``X_p`` | [0, 1] | [0, 1] | +----------------+-----------------------+---------------+ - \\* This definition has an output integer switch, thus the domain-range scale information is only given for the floating point mode. References ---------- :cite:`Spaulding2000b` Examples -------- >>> oetf_RIMMRGB(0.18) # doctest: +ELLIPSIS 0.2916737... >>> oetf_RIMMRGB(0.18, out_int=True) 74 """ X = to_domain_1(X) I_max = 2 ** bit_depth - 1 V_clip = 1.099 * spow(E_clip, 0.45) - 0.099 q = I_max / V_clip X_p = q * np.select([X < 0.0, X < 0.018, X >= 0.018, X > E_clip], [0, 4.5 * X, 1.099 * spow(X, 0.45) - 0.099, I_max]) if out_int: return as_int(np.round(X_p)) else: return as_float(from_range_1(X_p / I_max))
def colour_fidelity_index_ANSIIESTM3018(sd_test, additional_data=False): """ Returns the *ANSI/IES TM-30-18 Colour Fidelity Index* (CFI) :math:`R_f` of given spectral distribution. Parameters ---------- sd_test : SpectralDistribution Test spectral distribution. additional_data : bool, optional Whether to output additional data. Returns ------- numeric or ColourQuality_Specification_ANSIIESTM3018 *ANSI/IES TM-30-18 Colour Fidelity Index* (CFI). References ---------- :cite:`ANSI2018` Examples -------- >>> from colour import SDS_ILLUMINANTS >>> sd = SDS_ILLUMINANTS['FL2'] >>> colour_fidelity_index_ANSIIESTM3018(sd) # doctest: +ELLIPSIS 70.1208254... """ if not additional_data: return colour_fidelity_index_CIE2017(sd_test, False) specification = colour_fidelity_index_CIE2017(sd_test, True) # Setup bins based on where the reference a'b' points are located. bins = [[] for _i in range(16)] for i, sample in enumerate(specification.colorimetry_data[1]): bin_index = as_int(np.floor(sample.CAM.h / 22.5)) bins[bin_index].append(i) # Per-bin a'b' averages. averages_test = np.empty([16, 2]) averages_reference = np.empty([16, 2]) for i in range(16): apbp_s = [ specification.colorimetry_data[0][j].Jpapbp[[1, 2]] for j in bins[i] ] averages_test[i, :] = np.mean(apbp_s, axis=0) apbp_s = [ specification.colorimetry_data[1][j].Jpapbp[[1, 2]] for j in bins[i] ] averages_reference[i, :] = np.mean(apbp_s, axis=0) # Gamut Index. R_g = 100 * (averages_area(averages_test) / averages_area(averages_reference)) # Local colour fidelity indexes, i.e. 16 CFIs for each bin. bin_delta_E_s = [ np.mean([specification.delta_E_s[bins[i]]]) for i in range(16) ] R_fs = delta_E_to_R_f(as_float_array(bin_delta_E_s)) # Angles bisecting the hue bins. angles = (22.5 * np.arange(16) + 11.25) / 180 * np.pi cosines = np.cos(angles) sines = np.sin(angles) average_norms = np.linalg.norm(averages_reference, axis=1) a_deltas = averages_test[:, 0] - averages_reference[:, 0] b_deltas = averages_test[:, 1] - averages_reference[:, 1] # Local chromaticity shifts, multiplied by 100 to obtain percentages. R_cs = 100 * (a_deltas * cosines + b_deltas * sines) / average_norms # Local hue shifts. R_hs = (-a_deltas * sines + b_deltas * cosines) / average_norms return ColourQuality_Specification_ANSIIESTM3018( specification.name, sd_test, specification.sd_reference, specification.R_f, specification.R_s, specification.CCT, specification.D_uv, specification.colorimetry_data, R_g, bins, averages_test, averages_reference, average_norms, R_fs, R_cs, R_hs)
def cctf_encoding_RIMMRGB(X, bit_depth=8, out_int=False, E_clip=2.0): """ Defines the *RIMM RGB* encoding colour component transfer function (Encoding CCTF). *RIMM RGB* encoding non-linearity is based on that specified by *Recommendation ITU-R BT.709-6*. Parameters ---------- X : numeric or array_like Linear data :math:`X_{RIMM}`. bit_depth : int, optional Bit depth used for conversion. out_int : bool, optional Whether to return value as integer code value or float equivalent of a code value at a given bit depth. E_clip : numeric, optional Maximum exposure level. Returns ------- numeric or ndarray Non-linear data :math:`X'_{RIMM}`. Notes ----- +----------------+-----------------------+---------------+ | **Domain \\*** | **Scale - Reference** | **Scale - 1** | +================+=======================+===============+ | ``X`` | [0, 1] | [0, 1] | +----------------+-----------------------+---------------+ +----------------+-----------------------+---------------+ | **Range \\*** | **Scale - Reference** | **Scale - 1** | +================+=======================+===============+ | ``X_p`` | [0, 1] | [0, 1] | +----------------+-----------------------+---------------+ \\* This definition has an output integer switch, thus the domain-range scale information is only given for the floating point mode. References ---------- :cite:`Spaulding2000b` Examples -------- >>> cctf_encoding_RIMMRGB(0.18) # doctest: +ELLIPSIS 0.2916737... >>> cctf_encoding_RIMMRGB(0.18, out_int=True) 74 """ X = to_domain_1(X) I_max = 2**bit_depth - 1 V_clip = 1.099 * spow(E_clip, 0.45) - 0.099 q = I_max / V_clip X_p = q * np.select([X < 0.0, X < 0.018, X >= 0.018, X > E_clip], [0, 4.5 * X, 1.099 * spow(X, 0.45) - 0.099, I_max]) if out_int: return as_int(np.round(X_p)) else: return as_float(from_range_1(X_p / I_max))
def oetf_DICOMGSDF(L, out_int=False): """ Defines the *DICOM - Grayscale Standard Display Function* opto-electronic transfer function (OETF / OECF). Parameters ---------- L : numeric or array_like *Luminance* :math:`L`. out_int : bool, optional Whether to return value as integer code value or float equivalent of a code value at a given bit depth. Returns ------- numeric or ndarray Just-Noticeable Difference (JND) Index, :math:`j`. Notes ----- +------------+-----------------------+---------------+ | **Domain** | **Scale - Reference** | **Scale - 1** | +============+=======================+===============+ | ``L`` | [0, 1] | [0, 1] | +------------+-----------------------+---------------+ +------------+-----------------------+---------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +============+=======================+===============+ | ``J`` | [0, 1] | [0, 1] | +------------+-----------------------+---------------+ References ---------- :cite:`NationalElectricalManufacturersAssociation2004b` Examples -------- >>> oetf_DICOMGSDF(130.0662) # doctest: +ELLIPSIS 0.5004862... >>> oetf_DICOMGSDF(130.0662, out_int=True) 512 """ L = to_domain_1(L) L_lg = np.log10(L) A = DICOMGSDF_CONSTANTS.A B = DICOMGSDF_CONSTANTS.B C = DICOMGSDF_CONSTANTS.C D = DICOMGSDF_CONSTANTS.D E = DICOMGSDF_CONSTANTS.E F = DICOMGSDF_CONSTANTS.F G = DICOMGSDF_CONSTANTS.G H = DICOMGSDF_CONSTANTS.H I = DICOMGSDF_CONSTANTS.I # noqa J = (A + B * L_lg + C * L_lg ** 2 + D * L_lg ** 3 + E * L_lg ** 4 + F * L_lg ** 5 + G * L_lg ** 6 + H * L_lg ** 7 + I * L_lg ** 8) if out_int: return as_int(np.round(J)) else: return as_float(from_range_1(J / 1023))
def log_encoding_ERIMMRGB(X, bit_depth=8, out_int=False, E_min=0.001, E_clip=316.2): """ Defines the *ERIMM RGB* log encoding curve / opto-electronic transfer function (OETF / OECF). Parameters ---------- X : numeric or array_like Linear data :math:`X_{ERIMM}`. bit_depth : int, optional Bit depth used for conversion. out_int : bool, optional Whether to return value as integer code value or float equivalent of a code value at a given bit depth. E_min : numeric, optional Minimum exposure limit. E_clip : numeric, optional Maximum exposure limit. Returns ------- numeric or ndarray Non-linear data :math:`X'_{ERIMM}`. Notes ----- +----------------+-----------------------+---------------+ | **Domain \\*** | **Scale - Reference** | **Scale - 1** | +================+=======================+===============+ | ``X`` | [0, 1] | [0, 1] | +----------------+-----------------------+---------------+ +----------------+-----------------------+---------------+ | **Range \\*** | **Scale - Reference** | **Scale - 1** | +================+=======================+===============+ | ``X_p`` | [0, 1] | [0, 1] | +----------------+-----------------------+---------------+ \\* This definition has an output integer switch, thus the domain-range scale information is only given for the floating point mode. References ---------- :cite:`Spaulding2000b` Examples -------- >>> log_encoding_ERIMMRGB(0.18) # doctest: +ELLIPSIS 0.4100523... >>> log_encoding_ERIMMRGB(0.18, out_int=True) 105 """ X = to_domain_1(X) I_max = 2**bit_depth - 1 E_t = np.exp(1) * E_min X_p = np.select([ X < 0.0, X <= E_t, X > E_t, X > E_clip, ], [ 0, I_max * ((np.log(E_t) - np.log(E_min)) / (np.log(E_clip) - np.log(E_min))) * (X / E_t), I_max * ((np.log(X) - np.log(E_min)) / (np.log(E_clip) - np.log(E_min))), I_max, ]) if out_int: return as_int(np.round(X_p)) else: return as_float(from_range_1(X_p / I_max))
def matrix_augmented_Cheung2004(RGB, terms=3): """ Performs polynomial expansion of given *RGB* colourspace array using *Cheung et al. (2004)* method. Parameters ---------- RGB : array_like *RGB* colourspace array to expand. terms : int, optional Number of terms of the expanded polynomial, must be one of *[3, 5, 7, 8, 10, 11, 14, 16, 17, 19, 20, 22]*. Returns ------- ndarray Expanded *RGB* colourspace array. Notes ----- - This definition combines the augmented matrices given in :cite:`Cheung2004` and :cite:`Westland2004`. References ---------- :cite:`Cheung2004`, :cite:`Westland2004` Examples -------- >>> RGB = np.array([0.17224810, 0.09170660, 0.06416938]) >>> matrix_augmented_Cheung2004(RGB, terms=5) # doctest: +ELLIPSIS array([ 0.1722481..., 0.0917066..., 0.0641693..., 0.0010136..., 1...]) """ R, G, B = tsplit(RGB) tail = ones(R.shape) existing_terms = np.array([3, 5, 7, 8, 10, 11, 14, 16, 17, 19, 20, 22]) closest_terms = as_int(closest(existing_terms, terms)) if closest_terms != terms: raise ValueError('"Cheung et al. (2004)" method does not define ' 'an augmented matrix with {0} terms, ' 'closest augmented matrix has {1} terms!'.format( terms, closest_terms)) if terms == 3: return RGB elif terms == 5: return tstack([ R, G, B, R * G * B, tail, ]) elif terms == 7: return tstack([ R, G, B, R * G, R * B, G * B, tail, ]) elif terms == 8: return tstack([ R, G, B, R * G, R * B, G * B, R * G * B, tail, ]) elif terms == 10: return tstack([ R, G, B, R * G, R * B, G * B, R**2, G**2, B**2, tail, ]) elif terms == 11: return tstack([ R, G, B, R * G, R * B, G * B, R**2, G**2, B**2, R * G * B, tail, ]) elif terms == 14: return tstack([ R, G, B, R * G, R * B, G * B, R**2, G**2, B**2, R * G * B, R**3, G**3, B**3, tail, ]) elif terms == 16: return tstack([ R, G, B, R * G, R * B, G * B, R**2, G**2, B**2, R * G * B, R**2 * G, G**2 * B, B**2 * R, R**3, G**3, B**3, ]) elif terms == 17: return tstack([ R, G, B, R * G, R * B, G * B, R**2, G**2, B**2, R * G * B, R**2 * G, G**2 * B, B**2 * R, R**3, G**3, B**3, tail, ]) elif terms == 19: return tstack([ R, G, B, R * G, R * B, G * B, R**2, G**2, B**2, R * G * B, R**2 * G, G**2 * B, B**2 * R, R**2 * B, G**2 * R, B**2 * G, R**3, G**3, B**3, ]) elif terms == 20: return tstack([ R, G, B, R * G, R * B, G * B, R**2, G**2, B**2, R * G * B, R**2 * G, G**2 * B, B**2 * R, R**2 * B, G**2 * R, B**2 * G, R**3, G**3, B**3, tail, ]) elif terms == 22: return tstack([ R, G, B, R * G, R * B, G * B, R**2, G**2, B**2, R * G * B, R**2 * G, G**2 * B, B**2 * R, R**2 * B, G**2 * R, B**2 * G, R**3, G**3, B**3, R**2 * G * B, R * G**2 * B, R * G * B**2, ])
def log_encoding_ACESproxy(lin_AP1, bit_depth=10, out_int=False, constants=ACES_PROXY_CONSTANTS): """ Defines the *ACESproxy* colourspace log encoding curve / opto-electronic transfer function. Parameters ---------- lin_AP1 : numeric or array_like *lin_AP1* value. bit_depth : int, optional **{10, 12}**, *ACESproxy* bit depth. out_int : bool, optional Whether to return value as integer code value or float equivalent of a code value at a given bit depth. constants : Structure, optional *ACESproxy* constants. Returns ------- numeric or ndarray *ACESproxy* non-linear value. Notes ----- +----------------+-----------------------+---------------+ | **Domain \\*** | **Scale - Reference** | **Scale - 1** | +================+=======================+===============+ | ``lin_AP1`` | [0, 1] | [0, 1] | +----------------+-----------------------+---------------+ +----------------+-----------------------+---------------+ | **Range \\*** | **Scale - Reference** | **Scale - 1** | +================+=======================+===============+ | ``ACESproxy`` | [0, 1] | [0, 1] | +----------------+-----------------------+---------------+ \\* This definition has an output integer switch, thus the domain-range scale information is only given for the floating point mode. References ---------- :cite:`TheAcademyofMotionPictureArtsandSciences2014q`, :cite:`TheAcademyofMotionPictureArtsandSciences2014r`, :cite:`TheAcademyofMotionPictureArtsandSciences2014s`, :cite:`TheAcademyofMotionPictureArtsandSciencese` Examples -------- >>> log_encoding_ACESproxy(0.18) # doctest: +ELLIPSIS 0.4164222... >>> log_encoding_ACESproxy(0.18, out_int=True) 426 """ lin_AP1 = to_domain_1(lin_AP1) constants = constants[bit_depth] CV_min = np.resize(constants.CV_min, lin_AP1.shape) CV_max = np.resize(constants.CV_max, lin_AP1.shape) def float_2_cv(x): """ Converts given numeric to code value. """ return np.maximum(CV_min, np.minimum(CV_max, np.round(x))) ACESproxy = np.where( lin_AP1 > 2**-9.72, float_2_cv((np.log2(lin_AP1) + constants.mid_log_offset) * constants.steps_per_stop + constants.mid_CV_offset), np.resize(CV_min, lin_AP1.shape), ) if out_int: return as_int(np.round(ACESproxy)) else: return as_float(from_range_1(ACESproxy / (2**bit_depth - 1)))
def log_encoding_ERIMMRGB(X, bit_depth=8, out_int=False, E_min=0.001, E_clip=316.2): """ Defines the *ERIMM RGB* log encoding curve / opto-electronic transfer function (OETF / OECF). Parameters ---------- X : numeric or array_like Linear data :math:`X_{ERIMM}`. bit_depth : int, optional Bit depth used for conversion. out_int : bool, optional Whether to return value as integer code value or float equivalent of a code value at a given bit depth. E_min : numeric, optional Minimum exposure limit. E_clip : numeric, optional Maximum exposure limit. Returns ------- numeric or ndarray Non-linear data :math:`X'_{ERIMM}`. Notes ----- +----------------+-----------------------+---------------+ | **Domain \\*** | **Scale - Reference** | **Scale - 1** | +================+=======================+===============+ | ``X`` | [0, 1] | [0, 1] | +----------------+-----------------------+---------------+ +----------------+-----------------------+---------------+ | **Range \\*** | **Scale - Reference** | **Scale - 1** | +================+=======================+===============+ | ``X_p`` | [0, 1] | [0, 1] | +----------------+-----------------------+---------------+ - \\* This definition has an output integer switch, thus the domain-range scale information is only given for the floating point mode. References ---------- :cite:`Spaulding2000b` Examples -------- >>> log_encoding_ERIMMRGB(0.18) # doctest: +ELLIPSIS 0.4100523... >>> log_encoding_ERIMMRGB(0.18, out_int=True) 105 """ X = to_domain_1(X) I_max = 2 ** bit_depth - 1 E_t = np.exp(1) * E_min X_p = np.select([ X < 0.0, X <= E_t, X > E_t, X > E_clip, ], [ 0, I_max * ((np.log(E_t) - np.log(E_min)) / (np.log(E_clip) - np.log(E_min))) * (X / E_t), I_max * ( (np.log(X) - np.log(E_min)) / (np.log(E_clip) - np.log(E_min))), I_max, ]) if out_int: return as_int(np.round(X_p)) else: return as_float(from_range_1(X_p / I_max))
def polynomial_expansion_Finlayson2015( RGB: ArrayLike, degree: Literal[1, 2, 3, 4] = 1, root_polynomial_expansion: Boolean = True, ) -> NDArray: """ Perform polynomial expansion of given *RGB* colourspace array using *Finlayson et al. (2015)* method. Parameters ---------- RGB *RGB* colourspace array to expand. degree Expanded polynomial degree. root_polynomial_expansion Whether to use the root-polynomials set for the expansion. Returns ------- :class:`numpy.ndarray` Expanded *RGB* colourspace array. References ---------- :cite:`Finlayson2015` Examples -------- >>> RGB = np.array([0.17224810, 0.09170660, 0.06416938]) >>> polynomial_expansion_Finlayson2015(RGB, degree=2) # doctest: +ELLIPSIS array([ 0.1722481..., 0.0917066..., 0.0641693..., 0.1256832..., \ 0.0767121..., 0.1051335...]) """ RGB = as_float_array(RGB) R, G, B = tsplit(RGB) # TODO: Generalise polynomial expansion. existing_degrees = np.array([1, 2, 3, 4]) closest_degree = as_int(closest(existing_degrees, degree)) if closest_degree != degree: raise ValueError( f'"Finlayson et al. (2015)" method does not define a polynomial ' f"expansion for {degree} degree, closest polynomial expansion is " f"{closest_degree} degree!") if degree == 1: return RGB elif degree == 2: if root_polynomial_expansion: return tstack([ R, G, B, spow(R * G, 1 / 2), spow(G * B, 1 / 2), spow(R * B, 1 / 2), ]) else: return tstack([ R, G, B, R**2, G**2, B**2, R * G, G * B, R * B, ]) elif degree == 3: if root_polynomial_expansion: return tstack([ R, G, B, spow(R * G, 1 / 2), spow(G * B, 1 / 2), spow(R * B, 1 / 2), spow(R * G**2, 1 / 3), spow(G * B**2, 1 / 3), spow(R * B**2, 1 / 3), spow(G * R**2, 1 / 3), spow(B * G**2, 1 / 3), spow(B * R**2, 1 / 3), spow(R * G * B, 1 / 3), ]) else: return tstack([ R, G, B, R**2, G**2, B**2, R * G, G * B, R * B, R**3, G**3, B**3, R * G**2, G * B**2, R * B**2, G * R**2, B * G**2, B * R**2, R * G * B, ]) elif degree == 4: if root_polynomial_expansion: return tstack([ R, G, B, spow(R * G, 1 / 2), spow(G * B, 1 / 2), spow(R * B, 1 / 2), spow(R * G**2, 1 / 3), spow(G * B**2, 1 / 3), spow(R * B**2, 1 / 3), spow(G * R**2, 1 / 3), spow(B * G**2, 1 / 3), spow(B * R**2, 1 / 3), spow(R * G * B, 1 / 3), spow(R**3 * G, 1 / 4), spow(R**3 * B, 1 / 4), spow(G**3 * R, 1 / 4), spow(G**3 * B, 1 / 4), spow(B**3 * R, 1 / 4), spow(B**3 * G, 1 / 4), spow(R**2 * G * B, 1 / 4), spow(G**2 * R * B, 1 / 4), spow(B**2 * R * G, 1 / 4), ]) else: return tstack([ R, G, B, R**2, G**2, B**2, R * G, G * B, R * B, R**3, G**3, B**3, R * G**2, G * B**2, R * B**2, G * R**2, B * G**2, B * R**2, R * G * B, R**4, G**4, B**4, R**3 * G, R**3 * B, G**3 * R, G**3 * B, B**3 * R, B**3 * G, R**2 * G**2, G**2 * B**2, R**2 * B**2, R**2 * G * B, G**2 * R * B, B**2 * R * G, ])
def eotf_inverse_DICOMGSDF( L: FloatingOrArrayLike, out_int: Boolean = False, constants: Structure = CONSTANTS_DICOMGSDF, ) -> Union[FloatingOrNDArray, IntegerOrNDArray]: """ Define the *DICOM - Grayscale Standard Display Function* inverse electro-optical transfer function (EOTF). Parameters ---------- L *Luminance* :math:`L`. out_int Whether to return value as integer code value or float equivalent of a code value at a given bit depth. constants *DICOM - Grayscale Standard Display Function* constants. Returns ------- :class:`numpy.floating` or :class:`numpy.integer` or :class:`numpy.ndarray` Just-Noticeable Difference (JND) Index, :math:`j`. Notes ----- +------------+-----------------------+---------------+ | **Domain** | **Scale - Reference** | **Scale - 1** | +============+=======================+===============+ | ``L`` | [0, 1] | [0, 1] | +------------+-----------------------+---------------+ +------------+-----------------------+---------------+ | **Range** | **Scale - Reference** | **Scale - 1** | +============+=======================+===============+ | ``J`` | [0, 1] | [0, 1] | +------------+-----------------------+---------------+ References ---------- :cite:`NationalElectricalManufacturersAssociation2004b` Examples -------- >>> eotf_inverse_DICOMGSDF(130.0662) # doctest: +ELLIPSIS 0.5004862... >>> eotf_inverse_DICOMGSDF(130.0662, out_int=True) 512 """ L = to_domain_1(L) L_lg = np.log10(L) A = constants.A B = constants.B C = constants.C D = constants.D E = constants.E F = constants.F G = constants.G H = constants.H I = constants.I # noqa J = (A + B * L_lg + C * L_lg**2 + D * L_lg**3 + E * L_lg**4 + F * L_lg**5 + G * L_lg**6 + H * L_lg**7 + I * L_lg**8) if out_int: return as_int(np.round(J)) else: return as_float(from_range_1(J / 1023))
def augmented_matrix_Cheung2004(RGB, terms=3): """ Performs polynomial expansion of given *RGB* colourspace array using *Cheung et al. (2004)* method. Parameters ---------- RGB : array_like *RGB* colourspace array to expand. terms : int, optional Number of terms of the expanded polynomial, must be one of *[3, 5, 7, 8, 10, 11, 14, 16, 17, 19, 20, 22]*. Returns ------- ndarray Expanded *RGB* colourspace array. Notes ----- - This definition combines the augmented matrices given in :cite:`Cheung2004` and :cite:`Westland2004`. References ---------- :cite:`Cheung2004`, :cite:`Westland2004` Examples -------- >>> RGB = np.array([0.17224810, 0.09170660, 0.06416938]) >>> augmented_matrix_Cheung2004(RGB, terms=5) # doctest: +ELLIPSIS array([ 0.1722481..., 0.0917066..., 0.0641693..., 0.0010136..., 1...]) """ R, G, B = tsplit(RGB) ones = np.ones(R.shape) existing_terms = np.array([3, 5, 7, 8, 10, 11, 14, 16, 17, 19, 20, 22]) closest_terms = as_int(closest(existing_terms, terms)) if closest_terms != terms: raise ValueError('"Cheung et al. (2004)" method does not define ' 'an augmented matrix with {0} terms, ' 'closest augmented matrix has {1} terms!'.format( terms, closest_terms)) if terms == 3: return RGB elif terms == 5: return tstack([R, G, B, R * G * B, ones]) elif terms == 7: return tstack([R, G, B, R * G, R * B, G * B, ones]) elif terms == 8: return tstack([R, G, B, R * G, R * B, G * B, R * G * B, ones]) elif terms == 10: return tstack( [R, G, B, R * G, R * B, G * B, R ** 2, G ** 2, B ** 2, ones]) elif terms == 11: return tstack([ R, G, B, R * G, R * B, G * B, R ** 2, G ** 2, B ** 2, R * G * B, ones ]) elif terms == 14: return tstack([ R, G, B, R * G, R * B, G * B, R ** 2, G ** 2, B ** 2, R * G * B, R ** 3, G ** 3, B ** 3, ones ]) elif terms == 16: return tstack([ R, G, B, R * G, R * B, G * B, R ** 2, G ** 2, B ** 2, R * G * B, R ** 2 * G, G ** 2 * B, B ** 2 * R, R ** 3, G ** 3, B ** 3 ]) elif terms == 17: return tstack([ R, G, B, R * G, R * B, G * B, R ** 2, G ** 2, B ** 2, R * G * B, R ** 2 * G, G ** 2 * B, B ** 2 * R, R ** 3, G ** 3, B ** 3, ones ]) elif terms == 19: return tstack([ R, G, B, R * G, R * B, G * B, R ** 2, G ** 2, B ** 2, R * G * B, R ** 2 * G, G ** 2 * B, B ** 2 * R, R ** 2 * B, G ** 2 * R, B ** 2 * G, R ** 3, G ** 3, B ** 3 ]) elif terms == 20: return tstack([ R, G, B, R * G, R * B, G * B, R ** 2, G ** 2, B ** 2, R * G * B, R ** 2 * G, G ** 2 * B, B ** 2 * R, R ** 2 * B, G ** 2 * R, B ** 2 * G, R ** 3, G ** 3, B ** 3, ones ]) elif terms == 22: return tstack([ R, G, B, R * G, R * B, G * B, R ** 2, G ** 2, B ** 2, R * G * B, R ** 2 * G, G ** 2 * B, B ** 2 * R, R ** 2 * B, G ** 2 * R, B ** 2 * G, R ** 3, G ** 3, B ** 3, R ** 2 * G * B, R * G ** 2 * B, R * G * B ** 2 ])
def polynomial_expansion_Finlayson2015(RGB, degree=1, root_polynomial_expansion=True): """ Performs polynomial expansion of given *RGB* colourspace array using *Finlayson et al. (2015)* method. Parameters ---------- RGB : array_like *RGB* colourspace array to expand. degree : int, optional Expanded polynomial degree. root_polynomial_expansion : bool Whether to use the root-polynomials set for the expansion. Returns ------- ndarray Expanded *RGB* colourspace array. References ---------- :cite:`Finlayson2015` Examples -------- >>> RGB = np.array([0.17224810, 0.09170660, 0.06416938]) >>> polynomial_expansion_Finlayson2015(RGB, degree=2) # doctest: +ELLIPSIS array([ 0.1722481..., 0.0917066..., 0.0641693..., 0.1256832..., \ 0.0767121..., 0.1051335...]) """ R, G, B = tsplit(RGB) # TODO: Generalise polynomial expansion. existing_degrees = np.array([1, 2, 3, 4]) closest_degree = as_int(closest(existing_degrees, degree)) if closest_degree != degree: raise ValueError('"Finlayson et al. (2015)" method does not define ' 'a polynomial expansion for {0} degree, ' 'closest polynomial expansion is {1} degree!'.format( degree, closest_degree)) if degree == 1: return RGB elif degree == 2: if root_polynomial_expansion: return tstack([ R, G, B, spow(R * G, 1 / 2), spow(G * B, 1 / 2), spow(R * B, 1 / 2), ]) else: return tstack([ R, G, B, R**2, G**2, B**2, R * G, G * B, R * B, ]) elif degree == 3: if root_polynomial_expansion: return tstack([ R, G, B, spow(R * G, 1 / 2), spow(G * B, 1 / 2), spow(R * B, 1 / 2), spow(R * G**2, 1 / 3), spow(G * B**2, 1 / 3), spow(R * B**2, 1 / 3), spow(G * R**2, 1 / 3), spow(B * G**2, 1 / 3), spow(B * R**2, 1 / 3), spow(R * G * B, 1 / 3), ]) else: return tstack([ R, G, B, R**2, G**2, B**2, R * G, G * B, R * B, R**3, G**3, B**3, R * G**2, G * B**2, R * B**2, G * R**2, B * G**2, B * R**2, R * G * B, ]) elif degree == 4: if root_polynomial_expansion: return tstack([ R, G, B, spow(R * G, 1 / 2), spow(G * B, 1 / 2), spow(R * B, 1 / 2), spow(R * G**2, 1 / 3), spow(G * B**2, 1 / 3), spow(R * B**2, 1 / 3), spow(G * R**2, 1 / 3), spow(B * G**2, 1 / 3), spow(B * R**2, 1 / 3), spow(R * G * B, 1 / 3), spow(R**3 * G, 1 / 4), spow(R**3 * B, 1 / 4), spow(G**3 * R, 1 / 4), spow(G**3 * B, 1 / 4), spow(B**3 * R, 1 / 4), spow(B**3 * G, 1 / 4), spow(R**2 * G * B, 1 / 4), spow(G**2 * R * B, 1 / 4), spow(B**2 * R * G, 1 / 4), ]) else: return tstack([ R, G, B, R**2, G**2, B**2, R * G, G * B, R * B, R**3, G**3, B**3, R * G**2, G * B**2, R * B**2, G * R**2, B * G**2, B * R**2, R * G * B, R**4, G**4, B**4, R**3 * G, R**3 * B, G**3 * R, G**3 * B, B**3 * R, B**3 * G, R**2 * G**2, G**2 * B**2, R**2 * B**2, R**2 * G * B, G**2 * R * B, B**2 * R * G, ])