def rgb2hsi(rgb: np.ndarray, *, axis: int = None) -> np.ndarray: """ Convert RGB to Hue Saturation Intensity :param rgb: :param axis: :return: """ if axis is None: axis = get_matching_axis(rgb.shape, 3) big_m, little_m, chroma = _compute_chroma(rgb, axis) inds = construct_component_inds(axis, rgb.ndim, 3) hsi = np.zeros(rgb.shape) hsi[inds[0]] = _compute_rgb_hue(rgb, big_m, little_m, chroma, axis) hsi[inds[2]] = np.mean(rgb, axis=axis, keepdims=True) i_nz = hsi[inds[2]] != 0 # type: np.ndarray if little_m.ndim < i_nz.ndim: # This only happens in the 1D case little_m = little_m[slice(None), np.newaxis] if np.any(i_nz): hsi[inds[1]][i_nz] = 1 - little_m[i_nz] / hsi[inds[2]][i_nz] return hsi
def xyzr2xyz(xyzr: np.ndarray, *, axis: int = None, illuminant: Illuminant = None, observer: Observer = None) -> np.ndarray: """ Convert normalized XYZ to LAB :param xyzr: :param axis: :param illuminant: :param observer: :return: """ if axis is None: axis = get_matching_axis(xyzr.shape, 3) if illuminant is None: illuminant = get_default_illuminant() if observer is None: observer = get_default_observer() new_shape = tuple(-1 if dim == axis else 1 for dim in range(len(xyzr.shape))) white_point = illuminant.get_white_point(observer).reshape(new_shape) return xyzr * white_point
def hsl2rgb(hsl: np.ndarray, *, axis: int = None) -> np.ndarray: """ Convert from Hue Saturation Lightness (HSL) to RGB :type hsl: np.ndarray :param hsl: :param axis: :return: """ if axis is None: axis = get_matching_axis(hsl.shape, 3) inds = construct_component_inds(axis, hsl.ndim, 3) chroma = ((1. - np.abs(2. * hsl[inds[2]] - 1.)) * hsl[inds[1]] ) # type: np.ndarray h_prime = hsl[inds[0]] / 60. x = chroma * (1 - np.abs(np.mod(h_prime, 2) - 1)) rgb1 = _compute_rgb1(hsl.shape, inds, h_prime, x, chroma) little_m = hsl[inds[2]] - chroma / 2. if little_m.ndim > rgb1.ndim: # This only happens if hsl is 1-D return rgb1 + little_m[0] else: return rgb1 + little_m
def set_axis(self, a: int): if a is None: self._axis = get_matching_axis(self.data.shape, self.num_components) elif a != self.axis: new_dims = list(range(self.data.ndim)) new_dims[a] = self.axis new_dims[self.axis] = a self._data = self._data.transpose(new_dims) self._axis = a
def spectrum2xyz( spectrum: np.ndarray, wavelengths: np.ndarray, *, axis: int = None, illuminant: Illuminant = get_default_illuminant(), observer: Observer = get_default_observer() ) -> np.ndarray: """ Convert reflectance spectrum to XYZ :param spectrum: the reflectance spectrum :param wavelengths: the wavelengths corresponding to the spectra :param axis: The axis along which the spectra lie. If this is None, then the axis is the last axis with a size that matches wavelengths. :param illuminant: the illuminant :param observer: the observer :return: """ if axis is None: axis = get_matching_axis(spectrum.shape, wavelengths.size) # Need to ensure that we reshape all the 1-D parts of the integral, so that # they can be broadcast properly new_shape = [1] * len(spectrum.shape) new_shape[axis] = -1 wavelengths = wavelengths.reshape(new_shape) xbar = observer.get_xbar(wavelengths).reshape(new_shape) ybar = observer.get_ybar(wavelengths).reshape(new_shape) zbar = observer.get_zbar(wavelengths).reshape(new_shape) illuminant_psd = illuminant.get_psd(wavelengths).reshape(new_shape) norm_factor = ( 1 / integrate.trapz(illuminant_psd * ybar, x=wavelengths, axis=axis)) phi = spectrum * illuminant_psd # This is the shape that each XYZ component must be component_shape = list(spectrum.shape) component_shape[axis] = 1 def integrate_phi(bar: np.ndarray): area = norm_factor * integrate.trapz(bar * phi, wavelengths, axis=axis) # noinspection PyTypeChecker return np.reshape(area, component_shape) x = integrate_phi(xbar) y = integrate_phi(ybar) z = integrate_phi(zbar) return np.concatenate((x, y, z), axis=axis)
def __init__(self, data: Union[np.ndarray, Iterable[Any], ColorSpaceData], wavelengths: Union[np.ndarray, Iterable[float]], *, axis=None, **kwargs): """ In addition to the usual data arguments, this data also needs the wavelengths of the spectra. :param data: The spectral data [reflectance]. :param wavelengths: The wavelengths that correspond to the spectra. :param axis: The axis along which the spectra lie. :param illuminant: The illuminant :type illuminant: Illuminant :param observer: The observer :type observer: Observer :param rgbs: The RGB specification :type rgbs: RgbSpecification :param caa: The chromatic adaptation algorithm. :type caa: ChromaticAdaptationAlgorithm """ if not isinstance(data, np.ndarray): data = np.array(data) if axis is None: if isinstance(data, ColorSpaceData): if data.axis is None: axis = get_matching_axis(data.data.shape, len(wavelengths)) else: axis = data.axis else: axis = get_matching_axis(data.shape, len(wavelengths)) # noinspection PyArgumentList super().__init__(data, axis=axis, **kwargs) self._wavelengths = np.array(wavelengths, copy=True)
def xyz2xyy(xyz: np.ndarray, *, axis: int = None, illuminant: Illuminant = None, observer: Observer = None) -> np.ndarray: """ Convert XYZ to xyY :param xyz: :param axis: :param illuminant: :param observer: :return: """ if axis is None: axis = get_matching_axis(xyz.shape, 3) if illuminant is None: illuminant = get_default_illuminant() if observer is None: observer = get_default_observer() inds = construct_component_inds(axis, xyz.ndim, 3) denominator = np.sum(xyz, axis, keepdims=True) nzd = denominator != 0 xyy = np.zeros(xyz.shape) if np.any(nzd): xyy[inds[0]][nzd] = xyz[inds[0]][nzd] / denominator[nzd] xyy[inds[1]][nzd] = xyz[inds[1]][nzd] / denominator[nzd] xyy[inds[2]] = xyz[inds[1]] if not np.all(nzd): # For any point that is pure black (X=Y=Z=0), give it the # chromaticity of the white point of the specified illuminant and # observer. white_point = illuminant.get_white_point(observer) # to prevent infinite recursion, ensure that the white point is # non-black if white_point[1] > 0: zd = np.logical_not(nzd) white_point_xyy = xyz2xyy(white_point, illuminant=illuminant, observer=observer) xyy[inds[0]][zd] = white_point_xyy[0] xyy[inds[1]][zd] = white_point_xyy[1] return xyy
def xyz2xyz(source_xyz: np.ndarray, source_white_point: np.ndarray, destination_white_point: np.ndarray, axis: int = None, caa: ChromaticAdaptationAlgorithm = None) -> np.ndarray: """ Convert XYZ values between two white points :param source_xyz: :param source_white_point: :param destination_white_point: :param axis: :param caa: :return: """ if axis is None: axis = get_matching_axis(source_xyz.shape, 3) if caa is None: caa = get_default_chromatic_adaptation_algorithm() # Get the source color into the correct shape for matrix multiplication n_dims = source_xyz.ndim new_dims = list(range(n_dims)) if axis != n_dims - 1: new_dims[-1] = axis new_dims[axis] = n_dims - 1 source_xyz = source_xyz.transpose(new_dims) input_shape = source_xyz.shape xyz_is_not_matrix = n_dims != 2 if xyz_is_not_matrix: source_xyz = source_xyz.reshape((-1, 3)) # Convert between the white points m = caa.get_linear_transformation(source_white_point, destination_white_point) destination_xyz = source_xyz.dot(m) # Now convert the shape back to the original if xyz_is_not_matrix: destination_xyz = destination_xyz.reshape(input_shape) return destination_xyz.transpose(new_dims)
def lch2lab(lch: np.ndarray, *, axis: int = None) -> np.ndarray: """ Converts LCh to L*a*b* :param lch: :param axis: :return: """ if axis is None: axis = get_matching_axis(lch.shape, 3) inds = construct_component_inds(axis, lch.ndim, 3) lab = np.zeros(lch.shape) lab[inds[0]] = lch[inds[0]] h_rad = (np.pi / 180) * lch[inds[2]] lab[inds[1]] = lch[inds[1]] * np.cos(h_rad) lab[inds[2]] = lch[inds[1]] * np.sin(h_rad) return lab
def lab2lch(lab: np.ndarray, *, axis: int = None) -> np.ndarray: """ Convert L*a*b* to LCh :param lab: :param axis: :return: """ if axis is None: axis = get_matching_axis(lab.shape, 3) inds = construct_component_inds(axis, lab.ndim, 3) lch = np.zeros(lab.shape) lch[inds[0]] = lab[inds[0]] lch[inds[1]] = np.sqrt(lab[inds[1]]**2 + lab[inds[2]]**2) lch[inds[2]] = np.mod( (180 / np.pi) * np.arctan2(lab[inds[2]], lab[inds[1]]), 360.) return lch
def __init__( self, data: ArrayLike, *, axis: int = None, illuminant: Illuminant = get_default_illuminant(), observer: Observer = get_default_observer(), rgbs: RgbSpecification = get_default_rgb_specification(), caa: ChromaticAdaptationAlgorithm = get_default_chromatic_adaptation_algorithm( ), is_scaled: bool = False): """ :param data: the color space data to contain :param axis: the axis along which the color data lies. If `axis` is not specified, then it will be determined automatically by finding the last dimension with the required size. :param illuminant: the illuminant :param observer: the observer :param rgbs: the rgb specification :param caa: the chromatic adaptation algorithm :param is_scaled: Whether or not the data is scaled """ if is_scaled: self._data = np.array(data, copy=True) / self.scale_factor else: self._data = np.array(data, copy=True) self._data.flags.writeable = False self._axis = (axis if axis is not None else get_matching_axis( self._data.shape, 3)) self._illuminant = (illuminant if illuminant is not None else get_default_illuminant()) self._observer = (observer if observer is not None else get_default_observer()) self._rgbs = (rgbs if rgbs is not None else get_default_rgb_specification()) self._caa = (caa if caa is not None else get_default_chromatic_adaptation_algorithm()) self._is_scaled = is_scaled
def xyz2xyzr( xyz: np.ndarray, *, axis: int = None, illuminant: Illuminant = get_default_illuminant(), observer: Observer = get_default_observer() ) -> np.ndarray: """ Convert XYZ to normalized XYZ reflectance :param xyz: the raw xyz values :param axis: the axis that the XYZ values lie along :param illuminant: the illuminant :param observer: the observer :return: the xyz normalized Reflectance """ if axis is None: axis = get_matching_axis(xyz.shape, 3) new_shape = [1] * len(xyz.shape) new_shape[axis] = -1 white_point = illuminant.get_white_point(observer).reshape(new_shape) return xyz / white_point
def rgb2hcy(rgb: np.ndarray, *, axis: int = None) -> np.ndarray: """ Convert from RGB to Hue, Chroma, Luma (Y'_601) :param rgb: :param axis: :return: """ if axis is None: axis = get_matching_axis(rgb.shape, 3) big_m, little_m, chroma = _compute_chroma(rgb, axis) inds = construct_component_inds(axis, rgb.ndim, 3) hcy = np.zeros(rgb.shape) hcy[inds[0]] = _compute_rgb_hue(rgb, big_m, little_m, chroma, axis) hcy[inds[1]] = chroma hcy[inds[2]] = 0.299 * rgb[inds[0]] + 0.587 * rgb[inds[1]] + 0.114 * rgb[ inds[2]] return hcy
def rgb2hsv(rgb: np.ndarray, *, axis: int = None) -> np.ndarray: """ Convert from RGB to Hue Saturation Value (HSV) :param rgb: :param axis: :return: """ if axis is None: axis = get_matching_axis(rgb.shape, 3) big_m, little_m, chroma = _compute_chroma(rgb, axis) inds = construct_component_inds(axis, rgb.ndim, 3) hsv = np.zeros(rgb.shape) hsv[inds[0]] = _compute_rgb_hue(rgb, big_m, little_m, chroma, axis) hsv[inds[2]] = big_m big_m_nz = big_m != 0 hsv[inds[1]][big_m_nz] = chroma[big_m_nz] / big_m[big_m_nz] return hsv
def hsv2rgb(hsv: np.ndarray, *, axis: int = None) -> np.ndarray: """ Convert from HSV to RGB :param hsv: :param axis: :return: """ if axis is None: axis = get_matching_axis(hsv.shape, 3) inds = construct_component_inds(axis, hsv.ndim, 3) chroma = hsv[inds[1]] * hsv[inds[2]] h_prime = hsv[inds[0]] / 60. x = chroma * (1. - np.abs(np.mod(h_prime, 2.) - 1)) # type: np.ndarray rgb1 = _compute_rgb1(hsv.shape, inds, h_prime, x, chroma) little_m = hsv[inds[2]] - chroma if little_m.ndim > rgb1.ndim: # This only happens in the 1D case return rgb1 + little_m[0] else: return rgb1 + little_m
def lab2xyzr(lab: np.ndarray, *, axis: int = None) -> np.ndarray: """ Convert LAB to normalized XYZ :param lab: :param axis: :return: """ if axis is None: axis = get_matching_axis(lab.shape, 3) inds = construct_component_inds(axis, len(lab.shape), 3) fxyz = np.zeros(lab.shape) fxyz[inds[1]] = (lab[inds[0]] + 16) / 116 fxyz[inds[0]] = lab[inds[1]] / 500 + fxyz[inds[1]] fxyz[inds[2]] = fxyz[inds[1]] - lab[inds[2]] / 200 is_small = fxyz <= (LAB_EPS**(1.0 / 3.0)) is_big = np.logical_not(is_small) xyzr = np.zeros(lab.shape) xyzr[is_big] = fxyz[is_big]**3.0 xyzr[is_small] = (116 * fxyz[is_small] - 16) / LAB_KAPPA return xyzr
def hcy2rgb(hcy: np.ndarray, *, axis: int = None) -> np.ndarray: """ :param hcy: :param axis: :return: """ if axis is None: axis = get_matching_axis(hcy.shape, 3) inds = construct_component_inds(axis, hcy.ndim, 3) h_prime = hcy[inds[0]] / 60. x: np.ndarray = hcy[inds[1]] * (1 - np.abs(np.mod(h_prime, 2) - 1)) rgb1 = _compute_rgb1(hcy.shape, inds, h_prime, x, hcy[inds[1]]) little_m: np.ndarray = hcy[inds[2]] - ( 0.299 * rgb1[inds[0]] + 0.587 * rgb1[inds[1]] + 0.114 * rgb1[inds[2]]) if little_m.ndim > rgb1.ndim: # This only happens in the 1D case return rgb1 + little_m[0] else: return rgb1 + little_m
def xyy2xyz(xyy, *, axis: int = None) -> np.ndarray: """ converts from xyY to XYZ :param xyy: :param axis: :return: """ if axis is None: axis = get_matching_axis(xyy.shape, 3) inds = construct_component_inds(axis, len(xyy.shape), 3) # Determine where y iz 0 so we don't divide by it nzy = np.nonzero(xyy[inds[1]]) xyz = np.zeros(xyy.shape) xyz[inds[0]][nzy] = xyy[inds[0]][nzy] * xyy[inds[2]][nzy] / xyy[ inds[1]][nzy] xyz[inds[1]][nzy] = xyy[inds[2]][nzy] xyz[inds[2]][nzy] = ((1 - xyy[inds[0]][nzy] - xyy[inds[1]][nzy]) * xyy[inds[2]][nzy] / xyy[inds[1]][nzy]) return xyz
def hsi2rgb(hsi: np.ndarray, *, axis: int = None) -> np.ndarray: """ Convert Hue Saturation Intensity (HSI) to RGB :param hsi: :param axis: :return: """ if axis is None: axis = get_matching_axis(hsi.shape, 3) inds = construct_component_inds(axis, hsi.ndim, 3) h_prime = hsi[inds[0]] / 60. z = 1 - np.abs(np.mod(h_prime, 2.) - 1) chroma = 3 * hsi[inds[2]] * hsi[inds[1]] / (1 + z) # type: np.ndarray x = chroma * z rgb1 = _compute_rgb1(hsi.shape, inds, h_prime, x, chroma) little_m = hsi[inds[2]] * (1 - hsi[inds[1]]) # type: np.ndarray if little_m.ndim > rgb1.ndim: # This only happens in the 1D case return rgb1 + little_m[0] else: return rgb1 + little_m
def rgb2hsl(rgb: np.ndarray, *, axis: int = None) -> np.ndarray: """ Convert RGB to Hue Saturation Lightness :param rgb: :param axis: :return: """ if axis is None: axis = get_matching_axis(rgb.shape, 3) big_m, little_m, chroma = _compute_chroma(rgb, axis) inds = construct_component_inds(axis, rgb.ndim, 3) hsl = np.zeros(rgb.shape) hsl[inds[0]] = _compute_rgb_hue(rgb, big_m, little_m, chroma, axis) l = 0.5 * (big_m + little_m) hsl[inds[2]] = l l_lt_one = l < 1. if np.any(l_lt_one): hsl[inds[1]][l_lt_one] = ((big_m[l_lt_one] - little_m[l_lt_one]) / (1. - np.abs(2 * l[l_lt_one] - 1))) return hsl
def xyzr2lab(xyzr: np.ndarray, *, axis: int = None) -> np.ndarray: """ Convert from normalized XYZ to LAB :param xyzr: normalized XYZ :param axis: the axis along which the values lie :return: LAB """ if axis is None: axis = get_matching_axis(xyzr.shape, 3) fxyz = np.zeros(xyzr.shape) is_big = xyzr > LAB_EPS is_small = xyzr <= LAB_EPS fxyz[is_big] = np.power(xyzr[is_big], 1.0 / 3.0) fxyz[is_small] = (LAB_KAPPA * xyzr[is_small] + 16) / 116 lab = np.zeros(xyzr.shape) # Construct the indices for the 3 components inds = construct_component_inds(axis, len(xyzr.shape), 3) lab[inds[0]] = 116 * fxyz[inds[1]] - 16 lab[inds[1]] = 500 * (fxyz[inds[0]] - fxyz[inds[1]]) lab[inds[2]] = 200 * (fxyz[inds[1]] - fxyz[inds[2]]) return lab
def lrgb2xyz(lrgb: np.ndarray, *, axis: int = None, illuminant: Illuminant = None, observer: Observer = None, rgbs: RgbSpecification = None, caa: ChromaticAdaptationAlgorithm = None) -> np.ndarray: """ Convert from linear RGB to XYZ :param lrgb: :param axis: :param illuminant: :param observer: :param rgbs: :param caa: :return: """ if axis is None: axis = get_matching_axis(lrgb.shape, 3) if illuminant is None: illuminant = get_default_illuminant() if observer is None: observer = get_default_observer() if rgbs is None: rgbs = get_default_rgb_specification() if caa is None: caa = get_default_chromatic_adaptation_algorithm() # Get the XYZ values into the correct shape for matrix multiplication n_dims = lrgb.ndim new_dims = list(range(n_dims)) if axis != n_dims - 1: new_dims[-1] = axis new_dims[axis] = n_dims - 1 lrgb = lrgb.transpose(new_dims) input_shape = lrgb.shape lrgb_is_not_matrix = n_dims != 2 if lrgb_is_not_matrix: lrgb = lrgb.reshape((-1, 3)) # Do the transformation m = rgbs.linear_transformation xyz = lrgb.dot(m) # Transform back to the original shape if lrgb_is_not_matrix: xyz = xyz.reshape(input_shape) if axis != n_dims - 1: xyz = xyz.transpose(new_dims) source_white_point = rgbs.white_point destination_white_point = illuminant.get_white_point(observer) if not np.allclose( source_white_point, destination_white_point, rtol=1e-5, atol=1e-14): return xyz2xyz(xyz, source_white_point, destination_white_point, axis, caa) else: return xyz
def xyz2lrgb(xyz: np.ndarray, *, axis: int = None, illuminant: Illuminant = None, observer: Observer = None, rgbs: RgbSpecification = None, caa: ChromaticAdaptationAlgorithm = None) -> np.ndarray: """ Convert XYZ to linear RGB :param xyz: :param axis: :param illuminant: :param observer: :param rgbs: :param caa: :return: """ if axis is None: axis = get_matching_axis(xyz.shape, 3) if illuminant is None: illuminant = get_default_illuminant() if observer is None: observer = get_default_observer() if rgbs is None: rgbs = get_default_rgb_specification() if caa is None: caa = get_default_chromatic_adaptation_algorithm() # If the white points are not equal, we will need to convert to the # RGB white point source_white_point = illuminant.get_white_point(observer) destination_white_point = rgbs.white_point if not np.allclose( source_white_point, destination_white_point, rtol=1e-5, atol=1e-14): xyz = xyz2xyz(xyz, source_white_point, destination_white_point, axis, caa) # Get the XYZ values into the correct shape for matrix multiplication n_dims = xyz.ndim new_dims = list(range(n_dims)) if axis != n_dims - 1: new_dims[-1] = axis new_dims[axis] = n_dims - 1 xyz = xyz.transpose(new_dims) input_shape = xyz.shape xyz_is_not_matrix = xyz.ndim != 2 if xyz_is_not_matrix: xyz = xyz.reshape((-1, 3)) # Convert to linear RGB m = rgbs.linear_transformation lrgb = np.linalg.solve(m.T, xyz.T).T # Transform the destination data back to the original shape if xyz_is_not_matrix: lrgb = lrgb.reshape(input_shape) return lrgb.transpose(new_dims)