Skip to content

Commit 5b097a1

Browse files
committed
Merge branch 'magic-converter'
2 parents 0915653 + 235dbf1 commit 5b097a1

19 files changed

Lines changed: 2238 additions & 631 deletions

.travis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
language: python
22
python:
3+
- 2.6
34
- 2.7
45
- 3.3
56
- 3.4
@@ -10,7 +11,7 @@ before_install:
1011
# Escape Travis virtualenv
1112
- deactivate
1213
# See: http://conda.pydata.org/docs/travis.html
13-
- wget http://repo.continuum.io/miniconda/Miniconda3-3.6.0-Linux-x86_64.sh -O miniconda.sh
14+
- wget http://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh
1415
- bash miniconda.sh -b -p $HOME/miniconda
1516
- export PATH="$HOME/miniconda/bin:$PATH"
1617
- hash -r

README.rst

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,21 @@ Code and bug tracker:
3434
https://github.com/njsmith/pycam02ucs
3535

3636
Contact:
37-
Nathaniel J. Smith <nathaniel.smith@ed.ac.uk>
37+
Nathaniel J. Smith <njs@pobox.com>
38+
39+
Dependencies:
40+
* Python 2.6+, or 3.3+
41+
* NumPy
3842

3943
Developer dependencies (only needed for hacking on source):
4044
* nose: needed to run tests
4145

4246
License:
4347
MIT, see LICENSE.txt for details.
48+
49+
Other Python packages with similar functionality that you might also
50+
like to consider:
51+
* ``colour``: http://colour-science.org/
52+
* ``colormath``: http://python-colormath.readthedocs.org/
53+
* ``ciecam02``: https://pypi.python.org/pypi/ciecam02/
54+
* ``ColorPy``: http://markkness.net/colorpy/ColorPy.html

pycam02ucs/__init__.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
# Copyright (C) 2014 Nathaniel Smith <njs@pobox.com>
33
# See file LICENSE.txt for license information.
44

5-
from .ciecam02 import *
5+
from .illuminants import standard_illuminant_XYZ100, as_XYZ100_w
66

7-
from .cam02ucs import deltaEp_sRGB
7+
from .cvd import machado_et_al_2009_matrix
8+
9+
from .ciecam02 import CIECAM02Space, CIECAM02Surround, NegativeAError, JChQMsH
10+
11+
from .luoetal2006 import LuoEtAl2006UniformSpace, CAM02UCS, CAM02SCD, CAM02LCD
12+
13+
from .conversion import cspace_converter, cspace_convert
14+
15+
from .deltaEp import deltaEp

pycam02ucs/basics.py

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
# This file is part of pycam02ucs
2+
# Copyright (C) 2014-2015 Nathaniel Smith <njs@pobox.com>
3+
# See file LICENSE.txt for license information.
4+
5+
# Basic colorspaces: conversions between sRGB, XYZ, xyY, CIELab
6+
7+
import numpy as np
8+
9+
from .util import stacklast, color_cart2polar, color_polar2cart
10+
from .illuminants import as_XYZ100_w
11+
from .testing import check_conversion
12+
13+
################################################################
14+
# sRGB <-> sRGB-linear <-> XYZ100
15+
################################################################
16+
17+
# https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation
18+
def C_linear(C_srgb):
19+
out = np.empty(C_srgb.shape, dtype=float)
20+
linear_portion = (C_srgb < 0.04045)
21+
a = 0.055
22+
out[linear_portion] = C_srgb[linear_portion] / 12.92
23+
out[~linear_portion] = ((C_srgb[~linear_portion] + a) / (a + 1)) ** 2.4
24+
return out
25+
26+
def C_srgb(C_linear):
27+
out = np.empty(C_linear.shape, dtype=float)
28+
linear_portion = (C_linear <= 0.0031308)
29+
a = 0.055
30+
out[linear_portion] = C_linear[linear_portion] * 12.92
31+
out[~linear_portion] = (1+a) * C_linear[~linear_portion] ** (1/2.4) - a
32+
return out
33+
34+
XYZ100_to_sRGB1_matrix = np.array([
35+
# This is the exact matrix specified in IEC 61966-2-1:1999
36+
[ 3.2406, -1.5372, -0.4986],
37+
[-0.9689, 1.8758, 0.0415],
38+
[ 0.0557, -0.2040, 1.0570],
39+
])
40+
41+
# Condition number is 4.3, inversion is safe:
42+
sRGB1_to_XYZ100_matrix = np.linalg.inv(XYZ100_to_sRGB1_matrix)
43+
44+
def XYZ100_to_sRGB1_linear(XYZ100):
45+
"""Convert XYZ to linear sRGB, where XYZ is normalized so that reference
46+
white D65 is X=95.05, Y=100, Z=108.90 and sRGB is on the 0-1 scale. Linear
47+
sRGB has a linear relationship to actual light, so it is an appropriate
48+
space for simulating light (e.g. for alpha blending).
49+
50+
"""
51+
XYZ100 = np.asarray(XYZ100, dtype=float)
52+
# this is broadcasting matrix * array-of-vectors, where the vector is the
53+
# last dim
54+
RGB_linear = np.einsum("...ij,...j->...i", XYZ100_to_sRGB1_matrix, XYZ100 / 100)
55+
return RGB_linear
56+
57+
def sRGB1_linear_to_sRGB1(sRGB1_linear):
58+
return C_srgb(np.asarray(sRGB1_linear, dtype=float))
59+
60+
def sRGB1_to_sRGB1_linear(sRGB1):
61+
"""Convert sRGB (as floats in the 0-to-1 range) to linear sRGB."""
62+
sRGB1 = np.asarray(sRGB1, dtype=float)
63+
sRGB1_linear = C_linear(sRGB1)
64+
return sRGB1_linear
65+
66+
def sRGB1_linear_to_XYZ100(sRGB1_linear):
67+
sRGB1_linear = np.asarray(sRGB1_linear, dtype=float)
68+
# this is broadcasting matrix * array-of-vectors, where the vector is the
69+
# last dim
70+
XYZ100 = np.einsum("...ij,...j->...i", sRGB1_to_XYZ100_matrix, sRGB1_linear)
71+
XYZ100 *= 100
72+
return XYZ100
73+
74+
def test_sRGB1_to_sRGB1_linear():
75+
from .gold_values import sRGB1_sRGB1_linear_gold
76+
check_conversion(sRGB1_to_sRGB1_linear, sRGB1_linear_to_sRGB1,
77+
sRGB1_sRGB1_linear_gold,
78+
a_max=1, b_max=1)
79+
80+
def test_sRGB1_linear_to_XYZ100():
81+
from .gold_values import sRGB1_linear_XYZ100_gold
82+
check_conversion(sRGB1_linear_to_XYZ100, XYZ100_to_sRGB1_linear,
83+
sRGB1_linear_XYZ100_gold,
84+
a_max=1, b_max=100)
85+
86+
################################################################
87+
# XYZ <-> xyY
88+
################################################################
89+
90+
# These functions work identically for both the 0-100 and 0-1 versions of
91+
# XYZ/xyY.
92+
def XYZ_to_xyY(XYZ):
93+
XYZ = np.asarray(XYZ, dtype=float)
94+
norm = np.sum(XYZ, axis=-1, keepdims=True)
95+
xy = XYZ[..., :2] / norm
96+
return np.concatenate((xy, XYZ[..., 1:2]), axis=-1)
97+
98+
def xyY_to_XYZ(xyY):
99+
xyY = np.asarray(xyY, dtype=float)
100+
x = xyY[..., 0]
101+
y = xyY[..., 1]
102+
Y = xyY[..., 2]
103+
X = Y / y * x
104+
Z = Y / y * (1 - x - y)
105+
return stacklast(X, Y, Z)
106+
107+
_XYZ100_to_xyY100_test_vectors = [
108+
([10, 20, 30], [ 10. / 60, 20. / 60, 20]),
109+
([99, 98, 3], [99. / 200, 98. / 200, 98]),
110+
]
111+
112+
_XYZ1_to_xyY1_test_vectors = [
113+
([0.10, 0.20, 0.30], [ 0.10 / 0.60, 0.20 / 0.60, 0.20]),
114+
([0.99, 0.98, 0.03], [0.99 / 2.00, 0.98 / 2.00, 0.98]),
115+
]
116+
117+
def test_XYZ_to_xyY():
118+
check_conversion(XYZ_to_xyY, xyY_to_XYZ,
119+
_XYZ100_to_xyY100_test_vectors, b_max=[1, 1, 100])
120+
121+
check_conversion(XYZ_to_xyY, xyY_to_XYZ,
122+
_XYZ1_to_xyY1_test_vectors, b_max=[1, 1, 1])
123+
124+
################################################################
125+
# XYZ100 <-> CIEL*a*b*
126+
################################################################
127+
128+
# https://en.wikipedia.org/wiki/Lab_color_space#CIELAB-CIEXYZ_conversions
129+
def _f(t):
130+
out = np.empty(t.shape, dtype=float)
131+
linear_portion = (t < (6. / 29) ** 3)
132+
out[linear_portion] = ((1. / 3) * (29. / 6) ** 2 * t[linear_portion]
133+
+ 4. / 29)
134+
out[~linear_portion] = t[~linear_portion] ** (1. / 3)
135+
return out
136+
137+
def XYZ100_to_CIELab(XYZ100, XYZ100_w):
138+
XYZ100 = np.asarray(XYZ100, dtype=float)
139+
XYZ100_w = as_XYZ100_w(XYZ100_w)
140+
141+
fXYZ100_norm = _f(XYZ100 / XYZ100_w)
142+
L = 116 * fXYZ100_norm[..., 1:2] - 16
143+
a = 500 * (fXYZ100_norm[..., 0:1] - fXYZ100_norm[..., 1:2])
144+
b = 200 * (fXYZ100_norm[..., 1:2] - fXYZ100_norm[..., 2:3])
145+
return np.concatenate((L, a, b), axis=-1)
146+
147+
def _finv(t):
148+
linear_portion = (t <= 6. / 29)
149+
out = np.select([linear_portion, ~linear_portion],
150+
[3 * (6. / 29) ** 2 * (t - 4. / 29),
151+
t ** 3])
152+
return out
153+
154+
def CIELab_to_XYZ100(CIELab, XYZ100_w):
155+
CIELab = np.asarray(CIELab, dtype=float)
156+
XYZ100_w = as_XYZ100_w(XYZ100_w)
157+
158+
L = CIELab[..., 0]
159+
a = CIELab[..., 1]
160+
b = CIELab[..., 2]
161+
X_w = XYZ100_w[..., 0]
162+
Y_w = XYZ100_w[..., 1]
163+
Z_w = XYZ100_w[..., 2]
164+
165+
l_piece = 1. / 116 * (L + 16)
166+
X = X_w * _finv(l_piece + 1. / 500 * a)
167+
Y = Y_w * _finv(l_piece)
168+
Z = Z_w * _finv(l_piece - 1. / 200 * b)
169+
170+
return stacklast(X, Y, Z)
171+
172+
def test_XYZ100_to_CIELab():
173+
from .gold_values import XYZ100_CIELab_gold_D65, XYZ100_CIELab_gold_D50
174+
175+
check_conversion(XYZ100_to_CIELab, CIELab_to_XYZ100,
176+
XYZ100_CIELab_gold_D65,
177+
# Stick to randomized values in the mid-range to avoid
178+
# hitting negative luminances
179+
b_min=[10, -30, -30], b_max=[90, 30, 30],
180+
XYZ100_w="D65")
181+
182+
check_conversion(XYZ100_to_CIELab, CIELab_to_XYZ100,
183+
XYZ100_CIELab_gold_D50,
184+
# Stick to randomized values in the mid-range to avoid
185+
# hitting negative luminances
186+
b_min=[10, -30, -30], b_max=[90, 30, 30],
187+
XYZ100_w="D50")
188+
189+
XYZ100_1 = np.asarray(XYZ100_CIELab_gold_D65[0][0])
190+
CIELab_1 = np.asarray(XYZ100_CIELab_gold_D65[0][1])
191+
192+
XYZ100_2 = np.asarray(XYZ100_CIELab_gold_D50[1][0])
193+
CIELab_2 = np.asarray(XYZ100_CIELab_gold_D50[1][1])
194+
195+
XYZ100_mixed = np.concatenate((XYZ100_1[np.newaxis, :],
196+
XYZ100_2[np.newaxis, :]))
197+
CIELab_mixed = np.concatenate((CIELab_1[np.newaxis, :],
198+
CIELab_2[np.newaxis, :]))
199+
200+
XYZ100_w_mixed = np.row_stack((as_XYZ100_w("D65"), as_XYZ100_w("D50")))
201+
202+
assert np.allclose(XYZ100_to_CIELab(XYZ100_mixed, XYZ100_w=XYZ100_w_mixed),
203+
CIELab_mixed, rtol=0.001)
204+
assert np.allclose(CIELab_to_XYZ100(CIELab_mixed, XYZ100_w=XYZ100_w_mixed),
205+
XYZ100_mixed, rtol=0.001)
206+
207+
################################################################
208+
# CIELab <-> CIELCh
209+
################################################################
210+
211+
def CIELab_to_CIELCh(CIELab):
212+
CIELab = np.asarray(CIELab)
213+
L = CIELab[..., 0]
214+
a = CIELab[..., 1]
215+
b = CIELab[..., 2]
216+
C, h = color_cart2polar(a, b)
217+
return stacklast(L, C, h)
218+
219+
def CIELCh_to_CIELab(CIELCh):
220+
CIELCh = np.asarray(CIELCh)
221+
L = CIELCh[..., 0]
222+
C = CIELCh[..., 1]
223+
h = CIELCh[..., 2]
224+
a, b = color_polar2cart(C, h)
225+
return stacklast(L, a, b)

0 commit comments

Comments
 (0)