Source code for colourlab.space
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
space: Colour spaces, part of the colourlab package
Copyright (C) 2013-2016 Ivar Farup
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import numpy as np
from . import misc
# =============================================================================
# Colour space classes
#
# Throughout the code, the name ndata is used for numerical data (numpy
# arrays), and data is used for objects of the type Points.
# =============================================================================
[docs]class Space(object):
"""
Base class for the colour space classes.
"""
# White points in XYZ
white_A = np.array([1.0985, 1., 0.35585])
white_B = np.array([.990720, 1., .852230])
white_C = np.array([.980740, 1., .82320])
white_D50 = np.array([.964220, 1., .825210])
white_D55 = np.array([.956820, 1., .921490])
white_D65 = np.array([.950470, 1., 1.088830])
white_D75 = np.array([.949720, 1., 1.226380])
white_E = np.array([1., 1., 1.])
white_F2 = np.array([.991860, 1., .673930])
white_F7 = np.array([.950410, 1., 1.087470])
white_F11 = np.array([1.009620, 1., .643500])
[docs] def empty_matrix(self, ndata):
"""
Return list of emtpy (zero) matrixes suitable for jacobians etc.
Parameters
----------
ndata : ndarray
List of colour data.
Returns
-------
empty_matrix : ndarray
List of empty matrices of dimensions corresponding to ndata.
"""
return np.zeros((np.shape(ndata)[0], 3, 3))
[docs] def jacobian_XYZ(self, data):
"""
Return the Jacobian to XYZ, dx^i/dXYZ^j.
The Jacobian is calculated at the given data points (of the
Points class) by inverting the inverse Jacobian.
Parameters
----------
data : Points
Colour data points for the jacobians to be computed.
Returns
-------
jacobian : ndarray
The list of Jacobians to XYZ.
"""
return np.linalg.inv(self.inv_jacobian_XYZ(data))
[docs] def inv_jacobian_XYZ(self, data):
"""
Return the inverse Jacobian to XYZ, dXYZ^i/dx^j.
The inverse Jacobian is calculated at the given data points
(of the Points class) by inverting the Jacobian.
Parameters
----------
data : Points
Colour data points for the jacobians to be computed.
Returns
-------
jacobian : ndarray
The list of Jacobians from XYZ.
"""
return np.linalg.inv(self.jacobian_XYZ(data))
[docs] def vectors_to_XYZ(self, points_data, vectors_ndata):
"""
Convert metric data to the XYZ colour space.
Parameters
----------
points_data : Points
The colour data points.
vectors_ndata : ndarray
Array of colour metric tensors in current colour space.
Returns
-------
xyz_vectors : ndarray
Array of colour vectors in XYZ.
"""
jacobian = self.inv_jacobian_XYZ(points_data)
return np.einsum('...ij,...j->...i', jacobian, vectors_ndata)
[docs] def vectors_from_XYZ(self, points_data, vectors_ndata):
"""
Convert metric data from the XYZ colour space.
Parameters
----------
points_data : Points
The colour data points.
vectors_ndata : ndarray
Array of colour metric tensors in XYZ.
Returns
-------
vectors : ndarray
Array of colour vectors in the current colour space.
"""
jacobian = self.jacobian_XYZ(points_data)
return np.einsum('...ij,...j->...i', jacobian, vectors_ndata)
[docs] def metrics_to_XYZ(self, points_data, metrics_ndata):
"""
Convert metric data to the XYZ colour space.
Parameters
----------
points_data : Points
The colour data points.
metrics_ndata : ndarray
Array of colour metric tensors in current colour space.
Returns
-------
xyz_metrics : ndarray
Array of colour metric tensors in XYZ.
"""
jacobian = self.jacobian_XYZ(points_data)
return np.einsum('...ij,...ik,...kl->...jl', jacobian,
metrics_ndata, jacobian)
[docs] def metrics_from_XYZ(self, points_data, metrics_ndata):
"""
Convert metric data from the XYZ colour space.
Parameters
----------
points_data : Points
The colour data points.
metrics_ndata : ndarray
Array of colour metric tensors in XYZ.
Returns
-------
metrics : ndarray
Array of colour metric tensors in the current colour space.
"""
jacobian = self.inv_jacobian_XYZ(points_data)
return np.einsum('...ij,...ik,...kl->...jl', jacobian,
metrics_ndata, jacobian)
[docs]class XYZ(Space):
"""
The XYZ colour space.
Assumes that the CIE 1931 XYZ colour matching functions are
used. The white point is D65. Serves a special role in the code in that
it serves as a common reference point.
"""
[docs] def to_XYZ(self, ndata):
"""
Convert from current colour space to XYZ.
Parameters
----------
ndata : ndarray
Colour data in the current colour space.
Returns
-------
xyz : ndarray
Colour data in the XYZ colour space.
"""
return ndata.copy() # identity transform
[docs] def from_XYZ(self, ndata):
"""
Convert from XYZ to current colour space.
Parameters
----------
ndata : ndarray
Colour data in the XYZ colour space.
Returns
-------
xyz : ndarray
Colour data in the current colour space.
"""
return ndata.copy() # identity transform
[docs] def jacobian_XYZ(self, data):
"""
Return the Jacobian to XYZ, dx^i/dXYZ^j.
The Jacobian is calculated at the given data points (of the
Points class).
Parameters
----------
data : Points
Colour data points for the jacobians to be computed.
Returns
-------
jacobian : ndarray
The list of Jacobians to XYZ.
"""
jac = self.empty_matrix(data.flattened_XYZ)
jac[:] = np.eye(3)
return jac
[docs] def inv_jacobian_XYZ(self, data):
"""
Return the inverse Jacobian to XYZ, dXYZ^i/dx^j.
The inverse Jacobian is calculated at the given data points
(of the Points class).
Parameters
----------
data : Points
Colour data points for the jacobians to be computed.
Returns
-------
jacobian : ndarray
The list of Jacobians from XYZ.
"""
ijac = self.empty_matrix(data.flattened_XYZ)
ijac[:] = np.eye(3)
return ijac
[docs]class Transform(Space):
"""
Base class for colour space transforms.
Real transforms (children) must implement to_base, from_base and either
jacobian_base or inv_jacobian_base.
"""
def __init__(self, base):
"""
Construct instance and set base space for transformation.
Parameters
----------
base : Space
The base for the colour space transform.
"""
self.base = base
[docs] def to_XYZ(self, ndata):
"""
Transform data to XYZ by using the transformation to the base.
Parameters
----------
ndata : ndarray
Colour data in the current colour space
Returns
-------
xyz : ndarray
Colour data in the XYZ colour space
"""
return self.base.to_XYZ(self.to_base(ndata))
[docs] def from_XYZ(self, ndata):
"""
Transform data from XYZ using the transformation to the base.
Parameters
----------
ndata : ndarray
Colour data in the XYZ colour space.
Returns
-------
xyz : ndarray
Colour data in the current colour space.
"""
return self.from_base(self.base.from_XYZ(ndata))
[docs] def jacobian_base(self, data):
"""
Return the Jacobian to base, dx^i/dbase^j.
The Jacobian is calculated at the given data points (of the
Points class) by inverting the inverse Jacobian.
Parameters
----------
data : Points
Colour data points for the jacobians to be computed.
Returns
-------
jacobian : ndarray
The list of Jacobians to the base colour space.
"""
return np.linalg.inv(self.inv_jacobian_base(data))
[docs] def inv_jacobian_base(self, data):
"""
Return the inverse Jacobian to base, dbase^i/dx^j.
The inverse Jacobian is calculated at the given data points
(of the Points class) by inverting the Jacobian.
Parameters
----------
data : Points
Colour data points for the jacobians to be computed.
Returns
-------
jacobian : ndarray
The list of Jacobians from the base colour space.
"""
return np.linalg.inv(self.jacobian_base(data))
[docs] def jacobian_XYZ(self, data):
"""
Return the Jacobian to XYZ, dx^i/dXYZ^j.
The Jacobian is calculated at the given data points (of the
Points class) using the jacobian to the base and the Jacobian
of the base space.
Parameters
----------
data : Points
Colour data points for the jacobians to be computed.
Returns
-------
jacobian : ndarray
The list of Jacobians to XYZ.
"""
dxdbase = self.jacobian_base(data)
dbasedXYZ = self.base.jacobian_XYZ(data)
return np.einsum('...ij,...jk->...ik', dxdbase, dbasedXYZ)
[docs] def inv_jacobian_XYZ(self, data):
"""
Return the inverse Jacobian to XYZ, dXYZ^i/dx^j.
The Jacobian is calculated at the given data points (of the
Points class) using the inverse jacobian to the base and the
inverse Jacobian of the base space.
Parameters
----------
data : Points
Colour data points for the jacobians to be computed.
Returns
-------
jacobian : ndarray
The list of Jacobians from XYZ.
"""
dXYZdbase = self.base.inv_jacobian_XYZ(data)
dbasedx = self.inv_jacobian_base(data)
return np.einsum('...ij,...jk->...ik', dXYZdbase, dbasedx)
[docs]class TransformxyY(Transform):
"""
The XYZ to xyY projective transform.
"""
def __init__(self, base):
"""
Construct instance.
Parameters
----------
base : Space
Base colour space.
"""
super(TransformxyY, self).__init__(base)
[docs] def to_base(self, ndata):
"""
Convert from xyY to XYZ.
Parameters
----------
ndata : ndarray
Colour data in the current colour space
Returns
-------
col : ndarray
Colour data in the base colour space
"""
xyz = np.zeros(np.shape(ndata))
xyz[:, 0] = ndata[:, 0]*ndata[:, 2] / ndata[:, 1]
xyz[:, 1] = ndata[:, 2]
xyz[:, 2] = (1 - ndata[:, 0] - ndata[:, 1]) * ndata[:, 2] / ndata[:, 1]
return xyz
[docs] def from_base(self, ndata):
"""
Convert from XYZ to xyY.
Parameters
----------
ndata : ndarray
Colour data in the base colour space.
Returns
-------
col : ndarray
Colour data in the current colour space.
"""
xyz = ndata
xyY = np.zeros(np.shape(xyz))
xyz_sum = np.sum(xyz, axis=1)
xyY[:, 0] = xyz[:, 0] / xyz_sum # x
xyY[:, 1] = xyz[:, 1] / xyz_sum # y
xyY[:, 2] = xyz[:, 1] # Y
return xyY
[docs] def jacobian_base(self, data):
"""
Return the Jacobian to XYZ, dxyY^i/dXYZ^j.
The Jacobian is calculated at the given data points (of the
Points class).
Parameters
----------
data : Points
Colour data points for the jacobians to be computed.
Returns
-------
jacobian : ndarray
The list of Jacobians to the base colour space.
"""
xyzdata = data.get_flattened(self.base)
jac = self.empty_matrix(xyzdata)
for i in range(np.shape(jac)[0]):
jac[i, 0, 0] = (xyzdata[i, 1] + xyzdata[i, 2]) / \
(xyzdata[i, 0] + xyzdata[i, 1] + xyzdata[i, 2]) ** 2
jac[i, 0, 1] = -xyzdata[i, 0] / \
(xyzdata[i, 0] + xyzdata[i, 1] + xyzdata[i, 2]) ** 2
jac[i, 0, 2] = -xyzdata[i, 0] / \
(xyzdata[i, 0] + xyzdata[i, 1] + xyzdata[i, 2]) ** 2
jac[i, 1, 0] = -xyzdata[i, 1] / \
(xyzdata[i, 0] + xyzdata[i, 1] + xyzdata[i, 2]) ** 2
jac[i, 1, 1] = (xyzdata[i, 0] + xyzdata[i, 2]) / \
(xyzdata[i, 0] + xyzdata[i, 1] + xyzdata[i, 2]) ** 2
jac[i, 1, 2] = -xyzdata[i, 1] / \
(xyzdata[i, 0] + xyzdata[i, 1] + xyzdata[i, 2]) ** 2
jac[i, 2, 1] = 1
return jac
[docs] def inv_jacobian_base(self, data):
"""
Return the Jacobian from XYZ, dXYZ^i/dxyY^j.
The Jacobian is calculated at the given data points (of the
Points class).
Parameters
----------
data : Points
Colour data points for the jacobians to be computed.
Returns
-------
jacobian : ndarray
The list of Jacobians to the base colour space.
"""
xyYdata = data.get_flattened(self)
jac = self.empty_matrix(xyYdata)
for i in range(np.shape(jac)[0]):
jac[i, 0, 0] = xyYdata[i, 2] / xyYdata[i, 1]
jac[i, 0, 1] = - xyYdata[i, 0] * xyYdata[i, 2] / xyYdata[i, 1] ** 2
jac[i, 0, 2] = xyYdata[i, 0] / xyYdata[i, 1]
jac[i, 1, 2] = 1
jac[i, 2, 0] = - xyYdata[i, 2] / xyYdata[i, 1]
jac[i, 2, 1] = xyYdata[i, 2] * (xyYdata[i, 0] - 1) / \
xyYdata[i, 1] ** 2
jac[i, 2, 2] = (1 - xyYdata[i, 0] - xyYdata[i, 1]) / xyYdata[i, 1]
return jac
[docs]class TransformCIELAB(Transform):
"""
The XYZ to CIELAB colour space transform.
The white point is a parameter in the transform.
"""
kappa = 24389. / 27. # standard: 903.3
epsilon = 216. / 24389. # standard: 0.008856
def __init__(self, base, white_point=Space.white_D65):
"""
Construct instance by setting base space and white point.
Parameters
----------
base : Space
The base colour space.
white_point : ndarray or Points
The white point
"""
super(TransformCIELAB, self).__init__(base)
if not isinstance(white_point, np.ndarray):
self.white_point = white_point.get(xyz)
else:
self.white_point = white_point
[docs] def f(self, ndata):
"""
Auxiliary function for the conversion.
"""
fx = (self.kappa * ndata + 16.) / 116.
fx[ndata > self.epsilon] = ndata[ndata > self.epsilon] ** (1. / 3)
return fx
[docs] def dfdx(self, ndata):
"""
Auxiliary function for the Jacobian.
Returns the derivative of the function f above. Works for arrays.
"""
df = self.kappa / 116. * np.ones(np.shape(ndata))
df[ndata > self.epsilon] = \
(ndata[ndata > self.epsilon] ** (-2. / 3)) / 3
return df
[docs] def to_base(self, ndata):
"""
Convert from CIELAB to XYZ (base).
Parameters
----------
ndata : ndarray
Colour data in the current colour space
Returns
-------
col : ndarray
Colour data in the base colour space
"""
ndata
fy = (ndata[:, 0] + 16.) / 116.
fx = ndata[:, 1] / 500. + fy
fz = fy - ndata[:, 2] / 200.
xr = fx ** 3
xr[xr <= self.epsilon] = ((116 * fx[xr <= self.epsilon] - 16) /
self.kappa)
yr = fy ** 3
yr[ndata[:, 0] <= self.kappa * self.epsilon] = \
ndata[ndata[:, 0] <= self.kappa * self.epsilon, 0] / self.kappa
zr = fz ** 3
zr[zr <= self.epsilon] = ((116 * fz[zr <= self.epsilon] - 16) /
self.kappa)
xyz = np.zeros(np.shape(ndata))
xyz[:, 0] = xr * self.white_point[0]
xyz[:, 1] = yr * self.white_point[1]
xyz[:, 2] = zr * self.white_point[2]
return xyz
[docs] def from_base(self, ndata):
"""
Convert from XYZ (base) to CIELAB.
Parameters
----------
ndata : ndarray
Colour data in the base colour space.
Returns
-------
col : ndarray
Colour data in the current colour space.
"""
lab = np.zeros(np.shape(ndata))
fx = self.f(ndata[:, 0] / self.white_point[0])
fy = self.f(ndata[:, 1] / self.white_point[1])
fz = self.f(ndata[:, 2] / self.white_point[2])
lab[:, 0] = 116. * fy - 16.
lab[:, 1] = 500. * (fx - fy)
lab[:, 2] = 200. * (fy - fz)
return lab
[docs] def jacobian_base(self, data):
"""
Return the Jacobian to XYZ (base), dCIELAB^i/dXYZ^j.
The Jacobian is calculated at the given data points (of the
Points class).
Parameters
----------
data : Points
Colour data points for the jacobians to be computed.
Returns
-------
jacobian : ndarray
The list of Jacobians to the base colour space.
"""
d = data.get_flattened(self.base)
dr = d.copy()
for i in range(3):
dr[:, i] = dr[:, i] / self.white_point[i]
df = self.dfdx(dr)
jac = self.empty_matrix(d)
jac[:, 0, 1] = 116 * df[:, 1] / self.white_point[1] # dL/dY
jac[:, 1, 0] = 500 * df[:, 0] / self.white_point[0] # da/dX
jac[:, 1, 1] = -500 * df[:, 1] / self.white_point[1] # da/dY
jac[:, 2, 1] = 200 * df[:, 1] / self.white_point[1] # db/dY
jac[:, 2, 2] = -200 * df[:, 2] / self.white_point[2] # db/dZ
return jac
[docs]class TransformCIELUV(Transform):
"""
The XYZ to CIELUV colour space transform.
The white point is a parameter in the transform.
"""
kappa = 24389. / 27. # standard: 903.3
epsilon = 216. / 24389. # standard: 0.008856
def __init__(self, base, white_point=Space.white_D65):
"""
Construct instance by setting base space and white point.
Parameters
----------
base : Space
The base colour space.
white_point : ndarray or Points
The white point
"""
super(TransformCIELUV, self).__init__(base)
if not isinstance(white_point, np.ndarray):
self.white_point = white_point.get(xyz)
else:
self.white_point = white_point
[docs] def f(self, ndata):
"""
Auxiliary function for the conversion.
"""
fx = (self.kappa * ndata + 16.) / 116.
fx[ndata > self.epsilon] = ndata[ndata > self.epsilon] ** (1. / 3)
return fx
[docs] def dfdx(self, ndata):
"""
Auxiliary function for the Jacobian.
Returns the derivative of the function f above. Works for arrays.
"""
df = self.kappa / 116. * np.ones(np.shape(ndata))
df[ndata > self.epsilon] = \
(ndata[ndata > self.epsilon] ** (-2. / 3)) / 3
return df
[docs] def to_base(self, ndata):
"""
Convert from CIELUV to XYZ (base).
Parameters
----------
ndata : ndarray
Colour data in the current colour space
Returns
-------
col : ndarray
Colour data in the base colour space
"""
luv = ndata
fy = (luv[:, 0] + 16.) / 116.
y = fy ** 3
y[luv[:, 0] <= self.kappa * self.epsilon] = \
luv[luv[:, 0] <= self.kappa * self.epsilon, 0] / self.kappa
upr = 4 * self.white_point[0] / (self.white_point[0] +
15*self.white_point[1] +
3*self.white_point[2])
vpr = 9 * self.white_point[1] / (self.white_point[0] +
15*self.white_point[1] +
3*self.white_point[2])
a = (52*luv[:, 0] / (luv[:, 1] + 13*luv[:, 0]*upr) - 1) / 3
b = -5 * y
c = -1/3.
d = y * (39*luv[:, 0] / (luv[:, 2] + 13*luv[:, 0]*vpr) - 5)
x = (d - b) / (a - c)
z = x * a + b
# Combine into matrix
xyz = np.zeros(np.shape(luv))
xyz[:, 0] = x
xyz[:, 1] = y
xyz[:, 2] = z
return xyz
[docs] def from_base(self, ndata):
"""
Convert from XYZ (base) to CIELUV.
Parameters
----------
ndata : ndarray
Colour data in the base colour space.
Returns
-------
col : ndarray
Colour data in the current colour space.
"""
d = ndata
luv = np.zeros(np.shape(d))
fy = self.f(d[:, 1] / self.white_point[1])
up = 4 * d[:, 0] / (d[:, 0] + 15*d[:, 1] + 3*d[:, 2])
upr = 4 * self.white_point[0] / (self.white_point[0] +
15*self.white_point[1] +
3*self.white_point[2])
vp = 9 * d[:, 1] / (d[:, 0] + 15*d[:, 1] + 3*d[:, 2])
vpr = 9 * self.white_point[1] / (self.white_point[0] +
15*self.white_point[1] +
3*self.white_point[2])
luv[:, 0] = 116. * fy - 16.
luv[:, 1] = 13 * luv[:, 0] * (up - upr)
luv[:, 2] = 13 * luv[:, 0] * (vp - vpr)
return luv
[docs] def jacobian_base(self, data):
"""
Return the Jacobian to XYZ (base), dCIELUV^i/dXYZ^j.
The Jacobian is calculated at the given data points (of the
Points class).
Parameters
----------
data : Points
Colour data points for the jacobians to be computed.
Returns
-------
jacobian : ndarray
The list of Jacobians to the base colour space.
"""
xyz_ = data.get_flattened(xyz)
luv = data.get_flattened(cieluv)
df = self.dfdx(xyz_)
jac = self.empty_matrix(xyz_)
# dL/dY:
jac[:, 0, 1] = 116 * df[:, 1] / self.white_point[1]
# du/dX:
jac[:, 1, 0] = 13 * luv[:, 0] * \
(60 * xyz_[:, 1] + 12 * xyz_[:, 2]) / \
(xyz_[:, 0] + 15 * xyz_[:, 1] + 3 * xyz_[:, 2]) ** 2
# du/dY:
jac[:, 1, 1] = 13 * luv[:, 0] * \
-60 * xyz_[:, 0] / \
(xyz_[:, 0] + 15 * xyz_[:, 1] + 3 * xyz_[:, 2]) ** 2 + \
13 * jac[:, 0, 1] * (
4 * xyz_[:, 0] / (xyz_[:, 0] + 15 * xyz_[:, 1] +
3 * xyz_[:, 2]) -
4 * self.white_point[0] /
(self.white_point[0] + 15 * self.white_point[1] +
3 * self.white_point[2]))
# du/dZ:
jac[:, 1, 2] = 13 * luv[:, 0] * \
-12 * xyz_[:, 0] / \
(xyz_[:, 0] + 15 * xyz_[:, 1] + 3 * xyz_[:, 2]) ** 2
# dv/dX:
jac[:, 2, 0] = 13 * luv[:, 0] * \
-9 * xyz_[:, 1] / \
(xyz_[:, 0] + 15 * xyz_[:, 1] + 3 * xyz_[:, 2]) ** 2
# dv/dY:
jac[:, 2, 1] = 13 * luv[:, 0] * \
(9 * xyz_[:, 0] + 27 * xyz_[:, 2]) / \
(xyz_[:, 0] + 15 * xyz_[:, 1] + 3 * xyz_[:, 2]) ** 2 + \
13 * jac[:, 0, 1] * (
9 * xyz_[:, 1] / (xyz_[:, 0] + 15 * xyz_[:, 1] +
3 * xyz_[:, 2]) - 9 * self.white_point[1] /
(self.white_point[0] + 15 * self.white_point[1] +
3 * self.white_point[2]))
# dv/dZ:
jac[:, 2, 2] = 13 * luv[:, 0] * \
-27 * xyz_[:, 1] / \
(xyz_[:, 0] + 15 * xyz_[:, 1] + 3 * xyz_[:, 2]) ** 2
return jac
[docs]class TransformCIEDE00(Transform):
"""
The CIELAB to CIEDE00 L'a'b' colour space transform.
"""
def __init__(self, base):
"""
Construct instance by setting base space.
Parameters
----------
base : Space
The base colour space.
"""
super(TransformCIEDE00, self).__init__(base)
[docs] def to_base(self, ndata):
"""
Convert from CIEDE00 to CIELAB (base).
Parameters
----------
ndata : ndarray
Colour data in the current colour space
Returns
-------
col : ndarray
Colour data in the base colour space
"""
raise RuntimeError('No conversion of CIEDE00 Lab to CIELAB implemented (yet).')
[docs] def from_base(self, ndata):
"""
Convert from CIELAB (base) to CIEDE00.
Parameters
----------
ndata : ndarray
Colour data in the base colour space.
Returns
-------
labp : ndarray
Colour data in the CIEDE00 L'a'b' colour space.
"""
lab = ndata
labp = lab.copy()
Cab = np.sqrt(lab[:, 1]**2 + lab[:, 2]**2)
G = .5 * (1 - np.sqrt(Cab**7 / (Cab**7 + 25**7)))
labp[:, 1] = lab[:, 1] * (1 + G)
return labp
[docs] def jacobian_base(self, data):
"""
Return the Jacobian to CIELAB (base), dCIEDE00^i/dCIELAB^j.
The Jacobian is calculated at the given data points (of the
Points class).
Parameters
----------
data : Points
Colour data points for the jacobians to be computed.
Returns
-------
jacobian : ndarray
The list of Jacobians to the base colour space.
"""
lab = data.get_flattened(cielab)
lch = data.get_flattened(cielch)
a = lab[:, 1]
b = lab[:, 2]
C = lch[:, 1]
G = .5 * (1 - np.sqrt(C**7 / (C**7 + 25**7)))
jac = self.empty_matrix(lab)
jac[:, 0, 0] = 1 # dLp/dL
jac[:, 2, 2] = 1 # dbp/db
# jac[:, 1, 1] = 1 + G - misc.safe_div(a**2, C) * \
# (7 * 25**7 * C**(5/2.) /
# (4 * (C**7 + 25**7)**(3/2.))) # dap/da
jac[:, 1, 1] = 1 + G - misc.safe_div(a**2, C) * \
(7 * 25**7 * C**(5/2.) /
(8 * (C**7 + 25**7)**(3/2.))) # dap/da
jac[C == 0, 1, 1] = 1
# jac[:, 1, 2] = - a * misc.safe_div(b, C) * \
# (7 * 25**7 * C**(5/2.) / (4 * (C**7 + 25**7)**(3/2.)))
jac[:, 1, 2] = - a * misc.safe_div(b, C) * \
(7 * 25**7 * C**(5/2.) / (8 * (C**7 + 25**7)**(3/2.)))
jac[C == 0, 1, 2] = 0
return jac
[docs]class TransformSRGB(Transform):
"""
Transform linear RGB with sRGB primaries to sRGB.
"""
def __init__(self, base):
"""
Construct sRGB space instance, setting the base (linear RGB).
Parameters
----------
base : Space
The base colour space.
"""
super(TransformSRGB, self).__init__(base)
[docs] def to_base(self, ndata):
"""
Convert from sRGB to linear RGB. Performs gamut clipping if necessary.
Parameters
----------
ndata : ndarray
Colour data in the sRGB colour space
Returns
-------
col : ndarray
Colour data in the linear RGB colour space
"""
nd = ndata.copy()
nd[nd < 0] = 0
nd[nd > 1] = 1
rgb = ((nd + 0.055) / 1.055)**2.4
rgb[nd <= 0.04045] = nd[nd <= 0.04045] / 12.92
return rgb
[docs] def jacobian_base(self, data):
"""
Return the Jacobian to linear RGB (base), dsRGB^i/dRGB^j.
The Jacobian is calculated at the given data points (of the
Points class).
Parameters
----------
data : Points
Colour data points for the jacobians to be computed.
Returns
-------
jacobian : ndarray
The list of Jacobians to the base colour space.
"""
rgb = data.get_flattened(self.base)
r = rgb[:, 0]
g = rgb[:, 1]
b = rgb[:, 2]
jac = self.empty_matrix(rgb)
jac[:, 0, 0] = 1.055 / 2.4 * r**(1 / 2.4 - 1)
jac[r < 0.0031308, 0, 0] = 12.92
jac[:, 1, 1] = 1.055 / 2.4 * g**(1 / 2.4 - 1)
jac[g < 0.0031308, 1, 1] = 12.92
jac[:, 2, 2] = 1.055 / 2.4 * b**(1 / 2.4 - 1)
jac[b < 0.0031308, 2, 2] = 12.92
return jac
[docs] def from_base(self, ndata):
"""
Convert from linear RGB to sRGB. Performs gamut clipping if necessary.
Parameters
----------
ndata : ndarray
Colour data in the linear colour space
Returns
-------
col : ndarray
Colour data in the sRGB colour space
"""
nd = ndata.copy()
nd[nd < 0] = 0
nd[nd > 1] = 1
srgb = 1.055 * nd**(1 / 2.4) - 0.055
srgb[nd <= 0.0031308] = 12.92 * nd[nd <= 0.0031308]
return srgb
[docs]class TransformLinear(Transform):
"""
General linear transform, transformed = M * base
"""
def __init__(self, base, M=np.eye(3)):
"""
Construct instance, setting the matrix of the linear transfrom.
Parameters
----------
base : Space
The base colour space.
"""
super(TransformLinear, self).__init__(base)
self.M = M.copy()
self.M_inv = np.linalg.inv(M)
[docs] def to_base(self, ndata):
"""
Convert from linear to the base.
Parameters
----------
ndata : ndarray
Colour data in the current colour space
Returns
-------
col : ndarray
Colour data in the base colour space
"""
xyz = np.zeros(np.shape(ndata))
for i in range(np.shape(ndata)[0]):
xyz[i] = np.dot(self.M_inv, ndata[i])
return xyz
[docs] def from_base(self, ndata):
"""
Convert from the base to linear.
Parameters
----------
ndata : ndarray
Colour data in the base colour space.
Returns
-------
col : ndarray
Colour data in the current colour space.
"""
xyz = ndata
lins = np.zeros(np.shape(xyz))
for i in range(np.shape(xyz)[0]):
lins[i] = np.dot(self.M, xyz[i])
return lins
[docs] def jacobian_base(self, data):
"""
Return the Jacobian to XYZ (base), dlinear^i/dXYZ^j.
The Jacobian is calculated at the given data points (of the
Points class).
Parameters
----------
data : Points
Colour data points for the jacobians to be computed.
Returns
-------
jacobian : ndarray
The list of Jacobians to the base colour space.
"""
xyzdata = data.get_flattened(xyz)
jac = self.empty_matrix(xyzdata)
jac[:] = self.M
return jac
[docs] def inv_jacobian_base(self, data):
"""
Return the Jacobian from XYZ (base), dXYZ^i/dlinear^j.
The Jacobian is calculated at the given data points (of the
Points class).
Parameters
----------
data : Points
Colour data points for the jacobians to be computed.
Returns
-------
jacobian : ndarray
The list of Jacobians to the base colour space.
"""
xyzdata = data.get_flattened(xyz)
jac = self.empty_matrix(xyzdata)
jac[:] = self.M_inv
return jac
[docs]class TransformGamma(Transform):
"""
General gamma transform, transformed = base**gamma
Uses absolute value and sign for negative base values:
transformed = sign(base) * abs(base)**gamma
"""
def __init__(self, base, gamma=1):
"""
Construct instance, setting the gamma of the transfrom.
Parameters
----------
base : Space
The base colour space.
gamma : float
The exponent for the gamma transformation from the base.
"""
super(TransformGamma, self).__init__(base)
self.gamma = float(gamma)
self.gamma_inv = 1. / gamma
[docs] def to_base(self, ndata):
"""
Convert from gamma corrected to XYZ (base).
Parameters
----------
ndata : ndarray
Colour data in the current colour space
Returns
-------
col : ndarray
Colour data in the base colour space
"""
return np.sign(ndata) * np.abs(ndata)**self.gamma_inv
[docs] def from_base(self, ndata):
"""
Convert from XYZ to gamma corrected.
Parameters
----------
ndata : ndarray
Colour data in the base colour space.
Returns
-------
col : ndarray
Colour data in the current colour space.
"""
return np.sign(ndata) * np.abs(ndata)**self.gamma
[docs] def jacobian_base(self, data):
"""
Return the Jacobian to XYZ (base), dgamma^i/dXYZ^j.
The Jacobian is calculated at the given data points (of the
Points class).
Parameters
----------
data : Points
Colour data points for the jacobians to be computed.
Returns
-------
jacobian : ndarray
The list of Jacobians to the base colour space.
"""
basedata = data.get_flattened(self.base)
jac = self.empty_matrix(basedata)
for i in range(np.shape(basedata)[0]):
jac[i, 0, 0] = self.gamma * \
np.abs(basedata[i, 0])**(self.gamma - 1)
jac[i, 1, 1] = self.gamma * \
np.abs(basedata[i, 1])**(self.gamma - 1)
jac[i, 2, 2] = self.gamma * \
np.abs(basedata[i, 2])**(self.gamma - 1)
return jac
[docs]class TransformPolar(Transform):
"""
Transform form Cartesian to polar coordinates in the two last variables.
For example CIELAB to CIELCH.
"""
def __init__(self, base):
"""
Construct instance, setting base space.
Parameters
----------
base : Space
The base colour space.
"""
super(TransformPolar, self).__init__(base)
[docs] def to_base(self, ndata):
"""
Convert from polar to Cartesian.
Parameters
----------
ndata : ndarray
Colour data in the current colour space
Returns
-------
col : ndarray
Colour data in the base colour space
"""
Lab = np.zeros(np.shape(ndata))
Lab[:, 0] = ndata[:, 0]
C = ndata[:, 1]
h = ndata[:, 2]
Lab[:, 1] = C * np.cos(h)
Lab[:, 2] = C * np.sin(h)
return Lab
[docs] def from_base(self, ndata):
"""
Convert from Cartesian (base) to polar.
Parameters
----------
ndata : ndarray
Colour data in the base colour space.
Returns
-------
col : ndarray
Colour data in the current colour space.
"""
LCh = np.zeros(np.shape(ndata))
LCh[:, 0] = ndata[:, 0]
x = ndata[:, 1]
y = ndata[:, 2]
LCh[:, 1] = np.sqrt(x**2 + y**2)
LCh[:, 2] = np.arctan2(y, x)
return LCh
[docs] def inv_jacobian_base(self, data):
"""
Return the Jacobian from CIELAB (base), dCIELAB^i/dCIELCH^j.
The Jacobian is calculated at the given data points (of the
Points class).
Parameters
----------
data : Points
Colour data points for the jacobians to be computed.
Returns
-------
jacobian : ndarray
The list of Jacobians to the base colour space.
"""
LCh = data.get_flattened(self)
C = LCh[:, 1]
h = LCh[:, 2]
jac = self.empty_matrix(LCh)
for i in range(np.shape(jac)[0]):
jac[i, 0, 0] = 1 # dL/dL
jac[i, 1, 1] = np.cos(h[i]) # da/dC
jac[i, 1, 2] = -C[i] * np.sin(h[i]) # da/dh
jac[i, 2, 1] = np.sin(h[i]) # db/dC
jac[i, 2, 2] = C[i] * np.cos(h[i]) # db/dh
if C[i] == 0:
jac[i, 2, 2] = 1
jac[i, 1, 1] = 1
return jac
[docs]class TransformCartesian(Transform):
"""
Transform form polar to Cartesian coordinates in the two last variables.
For example CIELCH to CIELAB.
"""
def __init__(self, base):
"""
Construct instance, setting base space.
Parameters
----------
base : Space
The base colour space.
"""
super(TransformCartesian, self).__init__(base)
[docs] def from_base(self, ndata):
"""
Convert from polar to Cartesian.
Parameters
----------
ndata : ndarray
Colour data in the base colour space.
Returns
-------
col : ndarray
Colour data in the current colour space.
"""
Lab = np.zeros(np.shape(ndata))
Lab[:, 0] = ndata[:, 0]
C = ndata[:, 1]
h = ndata[:, 2]
Lab[:, 1] = C * np.cos(h)
Lab[:, 2] = C * np.sin(h)
return Lab
[docs] def to_base(self, ndata):
"""
Convert from Cartesian (base) to polar.
Parameters
----------
ndata : ndarray
Colour data in the current colour space
Returns
-------
col : ndarray
Colour data in the base colour space
"""
LCh = np.zeros(np.shape(ndata))
LCh[:, 0] = ndata[:, 0]
x = ndata[:, 1]
y = ndata[:, 2]
LCh[:, 1] = np.sqrt(x**2 + y**2)
LCh[:, 2] = np.arctan2(y, x)
return LCh
[docs] def jacobian_base(self, data):
"""
Return the Jacobian from CIELCh (base), dCIELAB^i/dCIELCH^j.
The Jacobian is calculated at the given data points (of the
Points class).
Parameters
----------
data : Points
Colour data points for the jacobians to be computed.
Returns
-------
jacobian : ndarray
The list of Jacobians to the base colour space.
"""
LCh = data.get_flattened(self.base)
C = LCh[:, 1]
h = LCh[:, 2]
jac = self.empty_matrix(LCh)
for i in range(np.shape(jac)[0]):
jac[i, 0, 0] = 1 # dL/dL
jac[i, 1, 1] = np.cos(h[i]) # da/dC
jac[i, 1, 2] = -C[i] * np.sin(h[i]) # da/dh
jac[i, 2, 1] = np.sin(h[i]) # db/dC
jac[i, 2, 2] = C[i] * np.cos(h[i]) # db/dh
return jac
[docs]class TransformLGJOSA(Transform):
"""
Transform from XYZ type coordinates to L_osa G J.
"""
def __init__(self, base):
"""
Construct instance, setting base space.
Parameters
----------
base : Space
The base colour space.
"""
super(TransformLGJOSA, self).__init__(base)
self.space_ABC = TransformLinear(self.base,
np.array([[0.6597, 0.4492, -0.1089],
[-0.3053, 1.2126, 0.0927],
[-0.0374, 0.4795, 0.5579]]))
self.space_xyY = TransformxyY(self.base)
[docs] def err_func(self, xyz, lgj):
clgj = self.from_base(np.reshape(xyz, (1, 3)))
diff = clgj - np.reshape(lgj, (1, 3))
n = np.linalg.norm(diff)
return n
[docs] def to_base(self, ndata):
"""
Convert from LGJOSA to XYZ (base).
Implemented as numerical inversion of the from_base method,
since the functions unfortunately are not analytically
invertible.
Parameters
----------
ndata : ndarray
Colour data in the current colour space
Returns
-------
col : ndarray
Colour data in the base colour space
"""
import scipy.optimize
xyz = .5 * np.ones(np.shape(ndata))
for i in range(np.shape(xyz)[0]):
xyz_guess = xyz[i].copy()
lgj = ndata[i].copy()
xyz[i] = scipy.optimize.fmin(self.err_func, xyz_guess, (lgj,))
return xyz
[docs] def from_base(self, ndata):
"""
Transform from base to LGJ OSA.
Parameters
----------
ndata : ndarray
Colour data in the base colour space (XYZ).
Returns
-------
col : ndarray
Colour data in the LGJOSA colour space.
"""
abc = self.space_ABC.from_base(ndata)
A = abc[:, 0]
B = abc[:, 1]
C = abc[:, 2]
xyY = self.space_xyY.from_base(ndata)
x = xyY[:, 0]
y = xyY[:, 1]
Y = xyY[:, 2]
Y_0 = 100 * Y * (4.4934 * x**2 + 4.3034 * y**2 - 4.2760 * x * y -
1.3744 * x - 2.5643 * y + 1.8103)
L_osa = (5.9 * ((Y_0**(1/3.) - (2/3.)) +
0.0042 * np.sign(Y_0 - 30) *
np.abs(Y_0 - 30)**(1/3.)) - 14.4) / np.sqrt(2)
G = -2 * (0.764 * L_osa + 9.2521) * (
0.9482 * (np.log(A) - np.log(0.9366 * B)) -
0.3175 * (np.log(B) - np.log(0.9807 * C)))
J = 2 * (0.5735 * L_osa + 7.0892) * (
0.1792 * (np.log(A) - np.log(0.9366 * B)) +
0.9237 * (np.log(B) - np.log(0.9807 * C)))
col = np.zeros(np.shape(ndata))
col[:, 0] = L_osa
col[:, 1] = G
col[:, 2] = J
return col
[docs] def jacobian_base(self, data):
"""
Return the Jacobian from XYZ (base), dLGJOSA^i/dXYZ^j.
The Jacobian is calculated at the given data points (of the
Points class). Like the colour space, a terrible mess...
Parameters
----------
data : Points
Colour data points for the jacobians to be computed.
Returns
-------
jacobian : ndarray
The list of Jacobians to the base colour space.
"""
ABC = data.get_flattened(self.space_ABC)
xyY = data.get_flattened(self.space_xyY)
x = xyY[:, 0]
y = xyY[:, 1]
Y = xyY[:, 2]
A = ABC[:, 0]
B = ABC[:, 1]
C = ABC[:, 2]
dxyY_dXYZ = self.space_xyY.jacobian_base(data)
dx_dX = dxyY_dXYZ[:, 0, 0]
dx_dY = dxyY_dXYZ[:, 0, 1]
dx_dZ = dxyY_dXYZ[:, 0, 2]
dy_dX = dxyY_dXYZ[:, 1, 0]
dy_dY = dxyY_dXYZ[:, 1, 1]
dy_dZ = dxyY_dXYZ[:, 1, 2]
dY_dX = dxyY_dXYZ[:, 2, 0]
dY_dY = dxyY_dXYZ[:, 2, 1]
dY_dZ = dxyY_dXYZ[:, 2, 2]
dABC_dXYZ = self.space_ABC.jacobian_base(data)
dA_dX = dABC_dXYZ[:, 0, 0]
dA_dY = dABC_dXYZ[:, 0, 1]
dA_dZ = dABC_dXYZ[:, 0, 2]
dB_dX = dABC_dXYZ[:, 1, 0]
dB_dY = dABC_dXYZ[:, 1, 1]
dB_dZ = dABC_dXYZ[:, 1, 2]
dC_dX = dABC_dXYZ[:, 2, 0]
dC_dY = dABC_dXYZ[:, 2, 1]
dC_dZ = dABC_dXYZ[:, 2, 2]
Y_0 = 100 * Y * (4.4934 * x**2 + 4.3034 * y**2 - 4.2760 * x * y -
1.3744 * x - 2.5643 * y + 1.8103)
L = (5.9 * ((Y_0**(1/3.) - (2/3.)) +
0.0042 * np.sign(Y_0 - 30) *
np.abs(Y_0 - 30)**(1/3.)) - 14.4) / np.sqrt(2)
dL_dY0 = 5.9 * (Y_0**(-2./3) + 0.042 * np.sign(Y_0 - 30) *
np.abs(Y_0 - 30)**(-2./3) / 3) / np.sqrt(2)
dY0_dx = 100 * Y * (4.4934 * 2 * x - 4.2760 * y - 1.3744)
dY0_dy = 100 * Y * (4.3034 * 2 * y - 4.2760 * x - 2.5643)
dY0_dY = 100 * (4.4934 * x**2 + 4.3034 * y**2 - 4.2760 * x * y -
1.3744 * x - 2.5643 * y + 1.8103)
dL_dX = dL_dY0 * (dY0_dx * dx_dX + dY0_dy * dy_dX + dY0_dY * dY_dX)
dL_dY = dL_dY0 * (dY0_dx * dx_dY + dY0_dy * dy_dY + dY0_dY * dY_dY)
dL_dZ = dL_dY0 * (dY0_dx * dx_dZ + dY0_dy * dy_dZ + dY0_dY * dY_dZ)
TG = 0.9482 * (np.log(A) - np.log(0.9366 * B)) - \
0.3175 * (np.log(B) - np.log(0.9807 * C))
TJ = 0.1792 * (np.log(A) - np.log(0.9366 * B)) + \
0.9237 * (np.log(B) - np.log(0.9807 * C))
SG = - 2 * (0.764 * L + 9.2521)
SJ = 2 * (0.5735 * L + 7.0892)
dG_dL = - 2 * 0.764 * TG
dJ_dL = 2 * 0.57354 * TJ
dG_dA = misc.safe_div(SG * 0.9482, A)
dG_dB = misc.safe_div(SG * (-0.9482 - 0.3175), B)
dG_dC = misc.safe_div(SG * 0.3175, C)
dJ_dA = misc.safe_div(SJ * 0.1792, A)
dJ_dB = misc.safe_div(SJ * (-0.1792 + 0.9837), B)
dJ_dC = misc.safe_div(SJ * (-0.9837), C)
dG_dX = dG_dL * dL_dX + dG_dA * dA_dX + dG_dB * dB_dX + dG_dC * dC_dX
dG_dY = dG_dL * dL_dY + dG_dA * dA_dY + dG_dB * dB_dY + dG_dC * dC_dY
dG_dZ = dG_dL * dL_dZ + dG_dA * dA_dZ + dG_dB * dB_dZ + dG_dC * dC_dZ
dJ_dX = dJ_dL * dL_dX + dJ_dA * dA_dX + dJ_dB * dB_dX + dJ_dC * dC_dX
dJ_dY = dJ_dL * dL_dY + dJ_dA * dA_dY + dJ_dB * dB_dY + dJ_dC * dC_dY
dJ_dZ = dJ_dL * dL_dZ + dJ_dA * dA_dZ + dJ_dB * dB_dZ + dJ_dC * dC_dZ
jac = self.empty_matrix(ABC)
jac[:, 0, 0] = dL_dX
jac[:, 0, 1] = dL_dY
jac[:, 0, 2] = dL_dZ
jac[:, 1, 0] = dG_dX
jac[:, 1, 1] = dG_dY
jac[:, 1, 2] = dG_dZ
jac[:, 2, 0] = dJ_dX
jac[:, 2, 1] = dJ_dY
jac[:, 2, 2] = dJ_dZ
return jac
[docs]class TransformLGJE(Transform):
"""
Transform from LGJOSA type coordinates to L_E, G_E, J_E.
"""
def __init__(self, base):
"""
Construct instance, setting base space.
Parameters
----------
base : Space
The base colour space.
"""
super(TransformLGJE, self).__init__(base)
self.aL = 2.890
self.bL = 0.015
self.ac = 1.256
self.bc = 0.050
[docs] def to_base(self, ndata):
"""
Convert from LGJE to LGJOSA (base).
Parameters
----------
ndata : ndarray
Colour data in the current colour space
Returns
-------
col : ndarray
Colour data in the base colour space
"""
LE = ndata[:, 0]
GE = ndata[:, 1]
JE = ndata[:, 2]
CE = np.sqrt(GE**2 + JE**2)
L = self.aL * (np.exp(self.bL * LE) - 1) / (10 * self.bL)
C = self.ac * (np.exp(self.bc * CE) - 1) / (10 * self.bc)
scale = misc.safe_div(C, CE)
G = - scale * GE
J = - scale * JE
col = ndata.copy()
col[:, 0] = L
col[:, 1] = G
col[:, 2] = J
return col
[docs] def from_base(self, ndata):
"""
Transform from LGJOSA (base) to LGJE.
Parameters
----------
ndata : ndarray
Colour data in the base colour space (LGJOSA).
Returns
-------
col : ndarray
Colour data in the LGJOSA colour space.
"""
L = ndata[:, 0]
G = ndata[:, 1]
J = ndata[:, 2]
C = np.sqrt(G**2 + J**2)
L_E = np.log(1 + 10 * L * self.bL / self.aL) / self.bL
C_E = np.log(1 + 10 * C * self.bc / self.ac) / self.bc
scale = misc.safe_div(C_E, C)
G_E = - scale * G
J_E = - scale * J
col = ndata.copy()
col[:, 0] = L_E
col[:, 1] = G_E
col[:, 2] = J_E
return col
[docs] def jacobian_base(self, data):
"""
Return the Jacobian from LGJOSA (base), dLGJE^i/dLGJOSA^j.
The Jacobian is calculated at the given data points (of the
Points class).
Parameters
----------
data : Points
Colour data points for the jacobians to be computed.
Returns
-------
jacobian : ndarray
The list of Jacobians to the base colour space.
"""
lgj = data.get_flattened(self.base)
L = lgj[:, 0]
G = lgj[:, 1]
J = lgj[:, 2]
C = np.sqrt(G**2 + J**2)
lgj_e = data.get_flattened(self)
C_E = np.sqrt(lgj_e[:, 1]**2 + lgj_e[:, 2]**2)
dLE_dL = 10 / (self.aL + 10 * self.bL * L)
dCE_dC = 10 / (self.ac + 10 * self.bc * C)
dCEC_dC = misc.safe_div(dCE_dC * C - C_E, C**2)
dC_dG = misc.safe_div(G, C)
dC_dJ = misc.safe_div(J, C)
dCEC_dG = dCEC_dC * dC_dG
dCEC_dJ = dCEC_dC * dC_dJ
dGE_dG = - misc.safe_div(C_E, C) - G * dCEC_dG
dGE_dJ = - G * dCEC_dJ
dJE_dG = - J * dCEC_dG
dJE_dJ = - misc.safe_div(C_E, C) - J * dCEC_dJ
jac = self.empty_matrix(lgj)
jac[:, 0, 0] = dLE_dL
jac[:, 1, 1] = dGE_dG
jac[:, 1, 2] = dGE_dJ
jac[:, 2, 1] = dJE_dG
jac[:, 2, 2] = dJE_dJ
return jac
[docs]class TransformLogCompressL(Transform):
"""
Perform parametric logarithmic compression of lightness.
As in the DIN99x formulae.
"""
def __init__(self, base, aL, bL):
"""
Construct instance, setting base space.
Parameters
----------
base : Space
The base colour space.
"""
super(TransformLogCompressL, self).__init__(base)
self.aL = aL
self.bL = bL
[docs] def from_base(self, ndata):
"""
Transform from Lab (base) to L'ab.
Parameters
----------
ndata : ndarray
Colour data in the base colour space (Lab).
Returns
-------
col : ndarray
Colour data in the La'b' colour space.
"""
Lpab = ndata.copy()
Lpab[:, 0] = self.aL * np.log(1 + self.bL * ndata[:, 0])
return Lpab
[docs] def to_base(self, ndata):
"""
Transform from L'ab to Lab (base).
Parameters
----------
ndata : ndarray
Colour data in L'ab colour space.
Returns
-------
col : ndarray
Colour data in the Lab colour space.
"""
Lab = ndata.copy()
Lab[:, 0] = (np.exp(ndata[:, 0] / self.aL) - 1) / self.bL
return Lab
[docs] def jacobian_base(self, data):
"""
Return the Jacobian from Lab (base), dL'ab^i/dLab^j.
The Jacobian is calculated at the given data points (of the
Points class).
Parameters
----------
data : Points
Colour data points for the jacobians to be computed.
Returns
-------
jacobian : ndarray
The list of Jacobians to the base colour space.
"""
lab = data.get_flattened(self.base)
L = lab[:, 0]
dLp_dL = self.aL * self.bL / (1 + self.bL * L)
jac = self.empty_matrix(lab)
jac[:, 0, 0] = dLp_dL
jac[:, 1, 1] = 1
jac[:, 2, 2] = 1
return jac
[docs]class TransformLogCompressC(Transform):
"""
Perform parametric logarithmic compression of chroma.
As in the DIN99x formulae.
"""
def __init__(self, base, aC, bC):
"""
Construct instance, setting base space.
Parameters
----------
base : Space
The base colour space.
"""
super(TransformLogCompressC, self).__init__(base)
self.aC = aC
self.bC = bC
[docs] def from_base(self, ndata):
"""
Transform from Lab (base) to La'b'.
Parameters
----------
ndata : ndarray
Colour data in the base colour space (Lab).
Returns
-------
col : ndarray
Colour data in the La'b' colour space.
"""
Lapbp = ndata.copy()
C = np.sqrt(ndata[:, 1]**2 + ndata[:, 2]**2)
Cp = self.aC * np.log(1 + self.bC * C)
scale = misc.safe_div(Cp, C)
ap = scale * ndata[:, 1]
bp = scale * ndata[:, 2]
Lapbp[:, 1] = ap
Lapbp[:, 2] = bp
return Lapbp
[docs] def to_base(self, ndata):
"""
Transform from La'b' to Lab (base).
Parameters
----------
ndata : ndarray
Colour data in L'ab colour space.
Returns
-------
col : ndarray
Colour data in the Lab colour space.
"""
Lab = ndata.copy()
ap = ndata[:, 1]
bp = ndata[:, 2]
Cp = np.sqrt(ap**2 + bp**2)
C = (np.exp(Cp / self.aC) - 1) / self.bC
scale = misc.safe_div(Cp, C)
a = scale * ap
b = scale * bp
Lab[:, 1] = a
Lab[:, 2] = b
return Lab
[docs] def jacobian_base(self, data):
"""
Return the Jacobian from Lab (base), dLa'b'^i/dLab^j.
The Jacobian is calculated at the given data points (of the
Points class).
Parameters
----------
data : Points
Colour data points for the jacobians to be computed.
Returns
-------
jacobian : ndarray
The list of Jacobians to the base colour space.
"""
lab = data.get_flattened(self.base)
lapbp = data.get_flattened(self)
a = lab[:, 1]
b = lab[:, 2]
C = np.sqrt(a**2 + b**2)
Cp = np.sqrt(lapbp[:, 1]**2 + lapbp[:, 2]**2)
dC_da = misc.safe_div(a, C)
dC_db = misc.safe_div(b, C)
dCp_dC = self.aC * self.bC / (1 + self.bC * C)
dCpC_dC = misc.safe_div(dCp_dC * C - Cp, C**2)
dap_da = misc.safe_div(Cp, C) + a * (dCpC_dC * dC_da)
dbp_db = misc.safe_div(Cp, C) + b * (dCpC_dC * dC_db)
dap_db = a * dCpC_dC * dC_db
dbp_da = b * dCpC_dC * dC_da
jac = self.empty_matrix(lab)
jac[:, 0, 0] = 1
jac[:, 1, 1] = dap_da
jac[:, 1, 2] = dap_db
jac[:, 2, 1] = dbp_da
jac[:, 2, 2] = dbp_db
return jac
[docs]class TransformPoincareDisk(Transform):
"""
Transform from Cartesian coordinates to Poincare disk coordinates.
The coordinate transform only changes the radius (chroma, typically),
and does so in a way that preserves the radial distance with respect to
the Euclidean metric and the Poincare disk metric in the source and
target spaces, respectively.
"""
def __init__(self, base, R=1.):
"""
Construct instance, setting base space and radius of curvature.
Parameters
----------
base : Space
The base colour space.
R : float
The radius of curvature.
"""
super(TransformPoincareDisk, self).__init__(base)
self.R = R
[docs] def to_base(self, ndata):
"""
Transform from Poincare disk to base.
Parameters
----------
ndata : ndarray
Colour data in the current colour space
Returns
-------
col : ndarray
Colour data in the base colour space
"""
Lab = ndata.copy()
Lab[:, 1:] = 0
x = ndata[:, 1]
y = ndata[:, 2]
r = np.sqrt(x**2 + y**2)
for i in range(np.shape(Lab)[0]):
if r[i] > 0:
Lab[i, 1:] = ndata[i, 1:] * 2 * \
self.R * np.arctanh(r[i]) / r[i]
return Lab
[docs] def from_base(self, ndata):
"""
Transform from base to Poincare disk
Parameters
----------
ndata : ndarray
Colour data in the base colour space.
Returns
-------
col : ndarray
Colour data in the current colour space.
"""
Lxy = ndata.copy()
Lxy[:, 1:] = 0
a = ndata[:, 1]
b = ndata[:, 2]
C = np.sqrt(a**2 + b**2)
for i in range(np.shape(Lxy)[0]):
if C[i] > 0:
Lxy[i, 1:] = ndata[i, 1:] * np.tanh(C[i] / (2 * self.R)) / C[i]
return Lxy
[docs] def jacobian_base(self, data):
"""
Return the Jacobian from CIELAB (base), dLxy^i/dCIELAB^j.
The Jacobian is calculated at the given data points (of the
Points class).
Parameters
----------
data : Points
Colour data points for the jacobians to be computed.
Returns
-------
jacobian : ndarray
The list of Jacobians to the base colour space.
"""
# TODO: bugfix!!!
Lab = data.get_flattened(self.base)
a = Lab[:, 1]
b = Lab[:, 2]
C = np.sqrt(a**2 + b**2)
tanhC2R = np.tanh(C / (2. * self.R))
tanhC2C = misc.safe_div(tanhC2R, C)
dCda = misc.safe_div(a, C)
dCdb = misc.safe_div(b, C)
dtanhdC = misc.safe_div(C / (2. * self.R) *
(1 - tanhC2R**2) - tanhC2R, C**2)
jac = self.empty_matrix(Lab)
for i in range(np.shape(jac)[0]):
jac[i, 0, 0] = 1 # dL/dL
if C[i] == 0:
jac[i, 1, 1] = .5 # dx/da
jac[i, 2, 2] = .5 # dy/db
else:
jac[i, 1, 1] = tanhC2C[i] + \
a[i] * dtanhdC[i] * dCda[i] # dx/da
jac[i, 1, 2] = a[i] * dtanhdC[i] * dCdb[i] # dx/db
jac[i, 2, 1] = b[i] * dtanhdC[i] * dCda[i] # dy/da
jac[i, 2, 2] = tanhC2C[i] + \
b[i] * dtanhdC[i] * dCdb[i] # dy/db
return jac
# =============================================================================
# Colour space instances
# =============================================================================
# CIE based
xyz = XYZ()
xyY = TransformxyY(xyz)
cielab = TransformCIELAB(xyz)
cielch = TransformPolar(cielab)
cieluv = TransformCIELUV(xyz)
ciede00lab = TransformCIEDE00(cielab)
ciede00lch = TransformPolar(ciede00lab)
ciecat02 = TransformLinear(xyz,
np.array([[.7328, .4296, -.1624],
[-.7036, 1.675, .0061],
[.0030, .0136, .9834]]))
ciecat16 = TransformLinear(xyz,
np.array([[.401288, .650173, -.051461],
[-.250268, 1.204414, .045854],
[-.002079, .048952, .953127]]))
# sRGB
_srgb_linear = TransformLinear(
xyz,
np.array([[3.2404542, -1.5371385, -0.4985314],
[-0.9692660, 1.8760108, 0.0415560],
[0.0556434, -0.2040259, 1.0572252]]))
srgb = TransformSRGB(_srgb_linear)
# Adobe RGB
_rgb_adobe_linear = TransformLinear(
xyz,
np.array([[2.0413690, -0.5649464, -0.3446944],
[-0.9692660, 1.8760108, 0.0415560],
[0.0134474, -0.1183897, 1.0154096]]))
rgb_adobe = TransformGamma(_rgb_adobe_linear, 1 / 2.2)
# IPT
_ipt_lms = TransformLinear(
xyz,
np.array([[.4002, .7075, -.0807],
[-.228, 1.15, .0612],
[0, 0, .9184]]))
_ipt_lmsp = TransformGamma(_ipt_lms, .43)
ipt = TransformLinear(
_ipt_lmsp,
np.array([[.4, .4, .2],
[4.455, -4.850, .3960],
[.8056, .3572, -1.1628]]))
# OSA-UCS
lgj_osa = TransformLGJOSA(xyz)
lgj_e = TransformLGJE(lgj_osa)
# DIN99
_din99_lpab = TransformLogCompressL(cielab, 105.51, 0.0158)
_din99_lef = TransformLinear(
_din99_lpab,
np.array([[1, 0, 0],
[0, np.cos(np.deg2rad(16.)),
np.sin(np.deg2rad(16.))],
[0, - 0.7 * np.sin(np.deg2rad(16.)),
0.7 * np.cos(np.deg2rad(16.))]]))
din99 = TransformLogCompressC(_din99_lef, 1 / 0.045, 0.045)
# DIN99b
_din99b_lpab = TransformLogCompressL(cielab, 303.67, 0.0039)
_din99b_lef = TransformLinear(
_din99b_lpab,
np.array([[1, 0, 0],
[0, np.cos(np.deg2rad(26.)), np.sin(np.deg2rad(26.))],
[0, - 0.83 * np.sin(np.deg2rad(26.)),
0.83 * np.cos(np.deg2rad(26.))]]))
_din99b_rot = TransformLogCompressC(_din99b_lef, 23.0, 0.075)
din99b = TransformLinear(
_din99b_rot,
np.array([[1, 0, 0],
[0, np.cos(np.deg2rad(-26.)), np.sin(np.deg2rad(-26.))],
[0, - np.sin(np.deg2rad(-26.)), np.cos(np.deg2rad(-26.))]]))
# DIN99c
_din99c_xyz = TransformLinear(xyz,
np.array([[1.1, 0, -0.1],
[0, 1, 0],
[0, 0, 1]]))
_din99c_white = np.dot(_din99c_xyz.M, _din99c_xyz.white_D65)
_din99c_lab = TransformCIELAB(_din99c_xyz, _din99c_white)
_din99c_lpab = TransformLogCompressL(_din99c_lab, 317.65, 0.0037)
_din99c_lef = TransformLinear(_din99c_lpab,
np.array([[1, 0, 0],
[0, 1, 0],
[0, 0, .94]]))
din99c = TransformLogCompressC(_din99c_lef, 23., 0.066)
# DIN99d
_din99d_xyz = TransformLinear(xyz,
np.array([[1.12, 0, -0.12],
[0, 1, 0],
[0, 0, 1]]))
_din99d_white = np.dot(_din99d_xyz.M, _din99d_xyz.white_D65)
_din99d_lab = TransformCIELAB(_din99d_xyz, _din99d_white)
_din99d_lpab = TransformLogCompressL(_din99c_lab, 325.22, 0.0036)
_din99d_lef = TransformLinear(
_din99d_lpab,
np.array([[1, 0, 0],
[0, np.cos(np.deg2rad(50.)), np.sin(np.deg2rad(50.))],
[0, - 1.14 * np.sin(np.deg2rad(50.)),
1.14 * np.cos(np.deg2rad(50.))]]))
_din99d_rot = TransformLogCompressC(_din99d_lef, 23., 0.066)
din99d = TransformLinear(
_din99d_rot,
np.array([[1, 0, 0],
[0, np.cos(np.deg2rad(-50.)), np.sin(np.deg2rad(-50.))],
[0, - np.sin(np.deg2rad(-50.)), np.cos(np.deg2rad(-50.))]]))