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 __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 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 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 get_white_point(self, observer: Observer = None) -> np.ndarray: """ Calculate the white point of the illuminant with a specified observer :param observer: The observer :return: the white point """ if observer is None: from chromathicity.defaults import get_default_observer observer = get_default_observer() wls = observer.wavelengths p = self.get_psd(wls) x_power = observer.xbar * p is_valid_x = np.logical_not(np.isnan(x_power)) x_point = integrate.trapz(x_power[is_valid_x], wls[is_valid_x]) y_power = observer.ybar * p is_valid_y = np.logical_not(np.isnan(y_power)) y_point = integrate.trapz(y_power[is_valid_y], wls[is_valid_y]) z_power = observer.zbar * p is_valid_z = np.logical_not(np.isnan(z_power)) z_point = integrate.trapz(z_power[is_valid_z], wls[is_valid_z]) return np.array([x_point, y_point, z_point]) / y_point
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 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)
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