Skip to content

Commit

Permalink
Merge pull request #190 from alcarney/color
Browse files Browse the repository at this point in the history
- Add back the ability to pass in parameter values directly to definitions
- Add `lerp` function to do linear interpolation
- Add `clamp` function that can be used to limit values in an array
- Add `colorramp` function that can map an array of values into a color range
- Update tests and docs
  • Loading branch information
alcarney authored Feb 16, 2020
2 parents e2852bd + c1e9eae commit 0357a7d
Show file tree
Hide file tree
Showing 14 changed files with 207 additions and 32 deletions.
4 changes: 2 additions & 2 deletions arlunio/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from ._color import RGB8 # noqa: F401
from ._core import Collection, Definition, definition # noqa: F401
from ._expressions import all, any, invert # noqa: F401
from ._image import Image, Resolutions, fill # noqa: F401
from ._expressions import all, any, clamp, invert, lerp # noqa: F401
from ._images import Image, Resolutions, colorramp, fill # noqa: F401
from ._version import __version__ # noqa: F401
11 changes: 10 additions & 1 deletion arlunio/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,15 @@ class Definition:

ATTR_ID: ClassVar[str] = "arlunio.attribute"

def __call__(self, width: int, height: int):
def __call__(self, width: int = None, height: int = None, **kwargs):
args = dict(self.definitions)
attributes = self.attributes

try:
width, height = width
except TypeError:
pass

for name in args:

if name == "width" and args[name] == inspect.Parameter.empty:
Expand All @@ -174,6 +179,10 @@ def __call__(self, width: int, height: int):
args[name] = height
continue

if name in kwargs:
args[name] = kwargs[name]
continue

# Else it must be a definition so let's evaluate it
defn = _prepare_definition(args[name], attributes)
args[name] = defn(width, height)
Expand Down
71 changes: 64 additions & 7 deletions arlunio/_expressions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import functools

from typing import Union
from typing import Callable, Union

import numpy as np

Expand Down Expand Up @@ -33,19 +33,16 @@ def any(*args: Union[bool, np.ndarray]) -> Union[bool, np.ndarray]:
>>> import arlunio as ar
>>> ar.any(True, False, False)
True
>>> ar.any(False, False, False, False)
False
If the arguments are boolean numpy arrays, then the any condition is applied
element-wise
>>> import numpy as np
>>> x1 = np.array([True, False, True])
>>> x2 = np.array([False, False, True])
>>> x3 = np.array([False, True, False])
>>> ar.any(x1, x2, x3)
array([ True, True, True])
Expand Down Expand Up @@ -87,19 +84,16 @@ def all(*args: Union[bool, np.ndarray]) -> Union[bool, np.ndarray]:
>>> import arlunio as ar
>>> ar.all(True, True, True)
True
>>> ar.all(True, False, True, True)
False
If the arguments are boolean numpy arrays, then the any condition is applied
element-wise
>>> import numpy as np
>>> x1 = np.array([True, False, True])
>>> x2 = np.array([False, False, True])
>>> x3 = np.array([False, True, True])
>>> ar.all(x1, x2, x3)
array([False, False, True])
Expand All @@ -113,5 +107,68 @@ def all(*args: Union[bool, np.ndarray]) -> Union[bool, np.ndarray]:
return functools.reduce(np.logical_and, args)


def clamp(values, min_value=0, max_value=1):
"""Force an array of values to stay within a range of values.
:param values: The array of values to clamp
:param min_value: The minimum value the result should contain (Default :code:`0`)
:param max_value: The maximum value the resul should contain (Default :code:`1`)
:Examples:
By default values will be limited to between :code:`0` and :code:`1`
>>> import arlunio as ar
>>> import numpy as np
>>> vs = np.linspace(-1, 2, 6)
>>> ar.clamp(vs)
array([0. , 0. , 0.2, 0.8, 1. , 1. ])
But this can be changed with extra arguments to the :code:`clamp` function
>>> ar.clamp(vs, min_value=-1, max_value=0.5)
array([-1. , -0.4, 0.2, 0.5, 0.5, 0.5])
"""
vs = np.array(values)
vs[vs > max_value] = max_value
vs[vs < min_value] = min_value

return vs


def invert(x):
return np.logical_not(x)


def lerp(start: float = 0, stop: float = 1) -> Callable[[float], float]:
"""Return a function that will linerarly interpolate between a and b.
:param start: The value the interpolation should start from. (Default :code:`0`)
:param stop: The value the interpolation should stop at. (Default :code:`1`)
:Examples:
By default this function will interpolate between :code:`0` and :code:`1`
>>> import arlunio as ar
>>> f = ar.lerp()
>>> f(0)
0
>>> f(1)
1
However by passing arguments to the :code:`lerp` function we can change the bounds
of the interpolation.
>>> import numpy as np
>>> ts = np.linspace(0, 1, 4)
>>> f = lerp(start=3, stop=-1)
>>> f(ts)
array([ 3. , 1.66666667, 0.33333333, -1. ])
"""

def f(t: float) -> float:
return (1 - t) * start + t * stop

return f
16 changes: 15 additions & 1 deletion arlunio/_image.py → arlunio/_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import PIL.Image

from ._color import RGB8
from ._expressions import lerp

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -153,7 +154,6 @@ def save(self, filename: str, mkdirs: bool = False) -> None:

def encode(self) -> bytes:
"""Return the image encoded as a base64 string."""
logger.debug("Encoding image as base64")
image = self._as_pillow_image()

with io.BytesIO() as byte_stream:
Expand All @@ -178,3 +178,17 @@ def fill(mask, color=None, background=None) -> Image:
image[mask] = color

return image


def colorramp(values, start=None, stop=None) -> Image:
"""Given a range of values, produce an image mapping those values onto colors."""

(r, g, b) = RGB8.parse("000") if start is None else RGB8.parse(start)
(R, G, B) = RGB8.parse("fff") if stop is None else RGB8.parse(stop)

reds = np.floor(lerp(r, R)(values))
greens = np.floor(lerp(g, G)(values))
blues = np.floor(lerp(b, B)(values))

pixels = np.array(np.dstack([reds, greens, blues]), dtype=np.uint8)
return Image(pixels)
2 changes: 1 addition & 1 deletion arlunio/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.0.4"
__version__ = "0.0.5"
4 changes: 2 additions & 2 deletions arlunio/doc/directives.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ def load_definition(object_spec: str) -> (ar.Definition, str):
>>> from arlunio.doc.directives import load_definition
>>> load_definition("arlunio.lib.basic.Circle")
(<class 'arlunio._core.Circle'>, 'arlunio.lib.basic')
>>> load_definition("arlunio.lib.shapes.Circle")
(<class 'arlunio._core.Circle'>, 'arlunio.lib.shapes')
If the module is not found then a :code:`MoudleNotFoundError` will be raised::
Expand Down
File renamed without changes.
6 changes: 6 additions & 0 deletions docs/stdlib/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,12 @@ Expressions

.. autofunction:: arlunio.all

.. autofunction:: arlunio.clamp

.. autofunction:: arlunio.invert

.. autofunction:: arlunio.lerp

Images
------

Expand All @@ -40,4 +44,6 @@ Images
.. autoclass:: arlunio.Resolutions
:members:

.. autofunction:: arlunio.colorramp

.. autofunction:: arlunio.fill
16 changes: 0 additions & 16 deletions docs/stdlib/basic-shapes.rst

This file was deleted.

2 changes: 1 addition & 1 deletion docs/stdlib/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ everyone will need along with some useful utilities for various tasks.
:maxdepth: 1

api
basic-shapes
parameters
patterns
shapes
16 changes: 16 additions & 0 deletions docs/stdlib/shapes.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.. _stdlib_shapes:

Shapes
======

Built in definitions of basic shapes.

.. autodefn:: arlunio.lib.shapes.Circle

.. autodefn:: arlunio.lib.shapes.Ellipse

.. autodefn:: arlunio.lib.shapes.Rectangle

.. autodefn:: arlunio.lib.shapes.Square

.. autodefn:: arlunio.lib.shapes.SuperEllipse
2 changes: 1 addition & 1 deletion tests/doc/test_directives.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def test_render_image_image_provided():

src = """\
import arlunio as ar
from arlunio.lib.basic import Circle
from arlunio.lib.shapes import Circle
circle = Circle()
disk = ar.fill(circle(4,4))
Expand Down
34 changes: 34 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,37 @@ def Derived(base: Base, *, b=3, d=4):
return 5

assert Derived().attribs == {"b": 3, "d": 4}


def test_derived_definition_eval_width_height():
"""Ensure that a definition can be evaluated with width and height as positional
arguments."""

@ar.definition()
def Base(width, height):
return width + height

assert Base()(4, 4) == 8

@ar.definition()
def Derived(height, base: Base):
return height - base

assert Derived()(2, 4) == -2


def test_derived_definition_eval_kwargs():
"""Ensure that a definition can be evaluted with inputs provided as keyword
arguments"""

@ar.definition()
def Base(width, height):
return width + height

assert Base()(width=4, height=4) == 8

@ar.definition()
def Derived(height, base: Base):
return height - base

assert Derived()(height=4, base=4) == 0
55 changes: 55 additions & 0 deletions tests/test_images.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import arlunio as ar
import numpy as np


def test_colorramp_defaults():
"""Ensure that the colorramp method chooses sensible defaults."""

values = np.array([[0.0, 0.5], [0.75, 1.0]])
img = ar.colorramp(values)

pix = np.array(
[[[0, 0, 0], [127, 127, 127]], [[191, 191, 191], [255, 255, 255]]],
dtype=np.uint8,
)

assert (img.pixels == pix).all()


def test_colorramp():
"""Ensure that the colorramp parses the colors it is given."""

values = np.array([[0.0, 0.5], [0.75, 1.0]])
img = ar.colorramp(values, start="f00", stop="0f0")

pix = np.array(
[[[255, 0, 0], [127, 127, 0]], [[63, 191, 0], [0, 255, 0]]], dtype=np.uint8
)

assert (img.pixels == pix).all()


def test_fill_defaults():
"""Ensure that the fill method chooses sensible defaults."""

mask = np.array([[False, True], [True, False]])
img = ar.fill(mask)

pix = np.array(
[[[255, 255, 255], [0, 0, 0]], [[0, 0, 0], [255, 255, 255]]], dtype=np.uint8
)

assert (img.pixels == pix).all()


def test_fill():
"""Ensure that the fill method parses colors that it is given."""

mask = np.array([[False, True], [True, False]])
img = ar.fill(mask, color="f00", background="0f0")

pix = np.array(
[[[0, 255, 0], [255, 0, 0]], [[255, 0, 0], [0, 255, 0]]], dtype=np.uint8
)

assert (img.pixels == pix).all()

0 comments on commit 0357a7d

Please sign in to comment.