Esempio n. 1
0
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
Esempio n. 2
0
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
Esempio n. 3
0
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
Esempio n. 4
0
 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
Esempio n. 5
0
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)
Esempio n. 6
0
 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)
Esempio n. 7
0
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
Esempio n. 8
0
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)
Esempio n. 9
0
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
Esempio n. 10
0
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
Esempio n. 11
0
 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
Esempio n. 12
0
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
Esempio n. 13
0
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
Esempio n. 14
0
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
Esempio n. 15
0
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
Esempio n. 16
0
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
Esempio n. 17
0
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
Esempio n. 18
0
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
Esempio n. 19
0
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
Esempio n. 20
0
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
Esempio n. 21
0
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
Esempio n. 22
0
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
Esempio n. 23
0
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)