Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e8ed400
ColorConvert: the basic stuff
l1ebl Jun 2, 2016
3d806b8
ColorConvert: GrayLevel and more
l1ebl Jun 2, 2016
3aabe9b
work in progress, cleanup: LUVColors, ref white
l1ebl Jun 4, 2016
39a9a78
color inversion tests working now, docs
l1ebl Jun 4, 2016
3c9262f
ColorDistance works now, docs
l1ebl Jun 4, 2016
4d00bb9
smaller tolerance for to_hsba
l1ebl Jun 4, 2016
11ec099
fixes test cases
l1ebl Jun 4, 2016
04bfbdc
adapted to rgba without alpha
l1ebl Jun 4, 2016
9c6557e
fixes to test cases
l1ebl Jun 4, 2016
8e9ced7
one more xyz test case
l1ebl Jun 4, 2016
c6bd775
rgb-xyz transforms are exact now
l1ebl Jun 4, 2016
7ed2f75
bug fixes and better accurary in the LAB conversions
l1ebl Jun 5, 2016
5a12ef6
work in progress: color conversion with and without numpy
l1ebl Jun 14, 2016
5b2821b
should fix pre-Python 3.5
l1ebl Jun 14, 2016
00bb6f3
simpler calculation of conversion flows
l1ebl Jun 14, 2016
bd5de4c
more Python 2 fixes
l1ebl Jun 14, 2016
f550a9c
fixes PyPy
l1ebl Jun 14, 2016
910f912
bug fixes and test cases for image color space conversions
l1ebl Jun 15, 2016
ca4b4e7
new numpy utils cleanup
l1ebl Jun 15, 2016
6f54950
test cases for (new) numpy utils
l1ebl Jun 15, 2016
5f380ab
moved numpy_utils into a new "numpy" package
l1ebl Jun 16, 2016
38b93ee
renamed package "numpy" back to "numpy_utils"
l1ebl Jun 16, 2016
dbf13ec
added "numpy_utils" to setup.py
l1ebl Jun 16, 2016
eb916b6
work in progress: bug fixes, test cases
l1ebl Jun 16, 2016
cd04571
bug fixes
l1ebl Jun 16, 2016
b24e628
major cleanup
l1ebl Jun 23, 2016
bb6c45a
fixes identation
l1ebl Jun 23, 2016
eb9ddab
rebase, central color conversion, more robust test cases
l1ebl Aug 24, 2016
33ec0c5
reduced color convert test case accuracy, make ColorConvert[] usable …
l1ebl Aug 25, 2016
d3ef32a
cleaner color conversion paths
l1ebl Sep 1, 2016
9f41f10
Merge branch 'colorconvert' of https://github.com/poke1024/Mathics in…
l1ebl Sep 1, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
416 changes: 416 additions & 0 deletions mathics/builtin/colors.py

Large diffs are not rendered by default.

245 changes: 204 additions & 41 deletions mathics/builtin/graphics.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from __future__ import division

from math import floor, ceil, log10
import math
import json
import base64
from six.moves import map
Expand All @@ -22,7 +23,8 @@
from mathics.builtin.options import options_to_rules
from mathics.core.expression import (
Expression, Integer, Real, String, Symbol, strip_context,
system_symbols, system_symbols_dict)
system_symbols, system_symbols_dict, from_python)
from mathics.builtin.colors import convert as convert_color


class CoordinatesError(BoxConstructError):
Expand Down Expand Up @@ -163,6 +165,14 @@ def _data_and_options(leaves, defined_options):
return data, options


def _euclidean_distance(a, b):
return math.sqrt(sum((x1 - x2) * (x1 - x2) for x1, x2 in zip(a, b)))


def _component_distance(a, b, i):
return abs(a[i] - b[i])


class Graphics(Builtin):
r"""
<dl>
Expand Down Expand Up @@ -284,12 +294,23 @@ def init(self, item=None, components=None):
if item is not None:
leaves = item.leaves
if len(leaves) in self.components_sizes:
# we must not clip here; we copy the components, without clipping,
# e.g. RGBColor[-1, 0, 0] stays RGBColor[-1, 0, 0]. this is especially
# important for color spaces like LAB that have negative components.

components = [value.round_to_float() for value in leaves]
if None in components or not all(0 <= c <= 1 for c in components):
if None in components:
raise ColorError
if len(components) < len(self.default_components):
components.extend(self.default_components[
len(components):])

# the following lines always extend to the maximum available
# default_components, so RGBColor[0, 0, 0] will _always_
# become RGBColor[0, 0, 0, 1]. does not seem the right thing
# to do in this general context. poke1024

# if len(components) < len(self.default_components):
# components.extend(self.default_components[
# len(components):])

self.components = components
else:
raise ColorError
Expand All @@ -310,21 +331,32 @@ def create_as_style(klass, graphics, item):

def to_css(self):
rgba = self.to_rgba()
alpha = rgba[3] if len(rgba) > 3 else 1.
return (r'rgb(%f%%, %f%%, %f%%)' % (
rgba[0] * 100, rgba[1] * 100, rgba[2] * 100), rgba[3])
rgba[0] * 100, rgba[1] * 100, rgba[2] * 100), alpha)

def to_asy(self):
rgba = self.to_rgba()
alpha = rgba[3] if len(rgba) > 3 else 1.
return (r'rgb(%s, %s, %s)' % (
asy_number(rgba[0]), asy_number(rgba[1]), asy_number(rgba[2])),
rgba[3])
alpha)

def to_js(self):
return self.to_rgba()

def to_expr(self):
return Expression(self.get_name(), *self.components)

def to_rgba(self):
return self.to_color_space("RGB")

def to_color_space(self, color_space):
components = convert_color(self.components, self.color_space, color_space)
if components is None:
raise ValueError('cannot convert from color space %s to %s.' % (self.color_space, color_space))
return components


class RGBColor(_Color):
"""
Expand All @@ -344,13 +376,68 @@ class RGBColor(_Color):
= StyleBox[GraphicsBox[...], ...]
"""

color_space = 'RGB'
components_sizes = [3, 4]
default_components = [0, 0, 0, 1]

def to_rgba(self):
return self.components


class LABColor(_Color):
"""
<dl>
<dt>'LABColor[$l$, $a$, $b$]'
<dd>represents a color with the specified lightness, red/green and yellow/blue
components in the CIE 1976 L*a*b* (CIELAB) color space.
</dl>
"""

color_space = 'LAB'
components_sizes = [3, 4]
default_components = [0, 0, 0, 1]


class LCHColor(_Color):
"""
<dl>
<dt>'LCHColor[$l$, $c$, $h$]'
<dd>represents a color with the specified lightness, chroma and hue
components in the CIELCh CIELab cube color space.
</dl>
"""

color_space = 'LCH'
components_sizes = [3, 4]
default_components = [0, 0, 0, 1]


class LUVColor(_Color):
"""
<dl>
<dt>'LCHColor[$l$, $u$, $v$]'
<dd>represents a color with the specified components in the CIE 1976 L*u*v* (CIELUV) color space.
</dl>
"""

color_space = 'LUV'
components_sizes = [3, 4]
default_components = [0, 0, 0, 1]


class XYZColor(_Color):
"""
<dl>
<dt>'XYZColor[$x$, $y$, $z$]'
<dd>represents a color with the specified components in the CIE 1931 XYZ color space.
</dl>
"""

color_space = 'XYZ'
components_sizes = [3, 4]
default_components = [0, 0, 0, 1]


class CMYKColor(_Color):
"""
<dl>
Expand All @@ -363,17 +450,10 @@ class CMYKColor(_Color):
= -Graphics-
"""

color_space = 'CMYK'
components_sizes = [3, 4, 5]
default_components = [0, 0, 0, 0, 1]

def to_rgba(self):
k = self.components[3]
k_ = 1 - k
c = self.components
cmy = [c[0] * k_ + k, c[1] * k_ + k, c[2] * k_ + k]
rgb = (1 - cmy[0], 1 - cmy[1], 1 - cmy[2])
return rgb + (c[4],)


class Hue(_Color):
"""
Expand All @@ -395,28 +475,10 @@ class Hue(_Color):
= -Graphics-
"""

color_space = 'HSB'
components_sizes = [1, 2, 3, 4]
default_components = [0, 1, 1, 1]

def to_rgba(self):
h, s, v = self.components[:3]
i = floor(6 * h)
f = 6 * h - i
i = i % 6
p = v * (1 - s)
q = v * (1 - f * s)
t = v * (1 - (1 - f) * s)

rgb = {
0: (v, t, p),
1: (q, v, p),
2: (p, v, t),
3: (p, q, v),
4: (t, p, v),
5: (v, p, q),
}[i]
return rgb + (self.components[3],)

def hsl_to_rgba(self):
h, s, l = self.components[:3]
if l < 0.5:
Expand Down Expand Up @@ -458,12 +520,109 @@ class GrayLevel(_Color):
<dd>represents a shade of gray specified by $g$ with opacity $a$.
</dl>
"""

color_space = 'Grayscale'
components_sizes = [1, 2]
default_components = [0, 1]

def to_rgba(self):
g = self.components[0]
return (g, g, g, self.components[1])

def expression_to_color(color):
try:
return _Color.create(color)
except ColorError:
return None


def color_to_expression(components, colorspace):
if colorspace == 'Grayscale':
converted_color_name = 'GrayLevel'
elif colorspace == 'HSB':
converted_color_name = 'Hue'
else:
converted_color_name = colorspace + 'Color'

return Expression(converted_color_name, *components)


class ColorDistance(Builtin):
"""
<dl>
<dt>'ColorDistance[$c1$, $c2$]'
<dd>returns a measure of color distance between the colors $c1$ and $c2$.
<dt>'ColorDistance[$list$, $c2$]'
<dd>returns a list of color distances between the colors in $list$ and $c2$.
</dl>

The option DistanceFunction specifies the method used to measure the color
distance. Available options are:

CIE76: euclidean distance in the LABColor space
CIE94: euclidean distance in the LCHColor space
DeltaL: difference in the L component of LCHColor
DeltaC: difference in the C component of LCHColor
DeltaH: difference in the H component of LCHColor

>> N[ColorDistance[Magenta, Green], 5]
= 2.2507
"""

options = {
'DistanceFunction': '"CIE76"',
}

messages = {
'invdist': '`` is not a valid color distance function.',
'invarg': '`1` and `2` should be two colors or a color and a lists of colors or ' +
'two lists of colors of the same length.'
}

_distances = {
"CIE76": lambda c1, c2: _euclidean_distance(c1.to_color_space('LAB')[:3], c2.to_color_space('LAB')[:3]),
"CIE94": lambda c1, c2: _euclidean_distance(c1.to_color_space('LCH')[:3], c2.to_color_space('LCH')[:3]),
"DeltaL": lambda c1, c2: _component_distance(c1.to_color_space('LCH'), c2.to_color_space('LCH'), 0),
"DeltaC": lambda c1, c2: _component_distance(c1.to_color_space('LCH'), c2.to_color_space('LCH'), 1),
"DeltaH": lambda c1, c2: _component_distance(c1.to_color_space('LCH'), c2.to_color_space('LCH'), 2),
}

def apply(self, c1, c2, evaluation, options):
'ColorDistance[c1_, c2_, OptionsPattern[ColorDistance]]'
distance_function = options.get('System`DistanceFunction')
if isinstance(distance_function, String):
compute = ColorDistance._distances.get(distance_function.get_string_value())
if not compute:
evaluation.message('ColorDistance', 'invdist', distance_function)
return
else:
def compute(a, b):
Expression(distance_function,
a.to_color_space('LAB'),
b.to_color_space('LAB'))

def distance(a, b):
try:
py_a = _Color.create(a)
py_b = _Color.create(b)
except ColorError:
evaluation.message('ColorDistance', 'invarg', a, b)
raise
return from_python(compute(py_a, py_b))

try:
if c1.get_head_name() == 'System`List':
if c2.get_head_name() == 'System`List':
if len(c1.leaves) != len(c2.leaves):
evaluation.message('ColorDistance', 'invarg', c1, c2)
return
else:
return Expression('List', *[distance(a, b) for a, b in zip(c1.leaves, c2.leaves)])
else:
return Expression('List', *[distance(c, c2) for c in c1.leaves])
elif c2.get_head_name() == 'System`List':
return Expression('List', *[distance(c1, c) for c in c2.leaves])
else:
return distance(c1, c2)
except ColorError:
return


class _Size(_GraphicsElement):
Expand Down Expand Up @@ -2022,11 +2181,11 @@ class Blend(Builtin):
</dl>

>> Blend[{Red, Blue}]
= RGBColor[0.5, 0., 0.5, 1.]
= RGBColor[0.5, 0., 0.5]
>> Blend[{Red, Blue}, 0.3]
= RGBColor[0.7, 0., 0.3, 1.]
= RGBColor[0.7, 0., 0.3]
>> Blend[{Red, Blue, Green}, 0.75]
= RGBColor[0., 0.5, 0.5, 1.]
= RGBColor[0., 0.5, 0.5]

>> Graphics[Table[{Blend[{Red, Green, Blue}, x], Rectangle[{10 x, 0}]}, {x, 0, 1, 1/10}]]
= -Graphics-
Expand Down Expand Up @@ -2129,7 +2288,7 @@ class Lighter(Builtin):
</dl>

>> Lighter[Orange, 1/4]
= RGBColor[1., 0.625, 0.25, 1.]
= RGBColor[1., 0.625, 0.25]
>> Graphics[{Lighter[Orange, 1/4], Disk[]}]
= -Graphics-
>> Graphics[Table[{Lighter[Orange, x], Disk[{12x, 0}]}, {x, 0, 1, 1/6}]]
Expand Down Expand Up @@ -2364,6 +2523,10 @@ class Large(Builtin):

styles = system_symbols_dict({
'RGBColor': RGBColor,
'XYZColor': XYZColor,
'LABColor': LABColor,
'LCHColor': LCHColor,
'LUVColor': LUVColor,
'CMYKColor': CMYKColor,
'Hue': Hue,
'GrayLevel': GrayLevel,
Expand Down
Loading