Skip to content

Commit 031eefc

Browse files
authored
Merge pull request #8 from jorenham/development
Version 0.2
2 parents fd5d7a0 + 412be51 commit 031eefc

File tree

8 files changed

+85
-30
lines changed

8 files changed

+85
-30
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ All notable changes to this project will be documented in this file.
33

44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
55

6+
## [0.2] - 2019-09-29
7+
### Added
8+
- The `Turtle` class now has color support.
9+
- The examples work with RGBA images now.
10+
611
## [0.1] - 2018-06-26
712
### Added
813
- The `Turtle` class as wrapper for a two-dimensional (grayscale) NumPy array.

numpy_turtle/examples/fractal_plant.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import numpy as np
2-
from scipy.misc import toimage
2+
from matplotlib import pyplot as plt
3+
from skimage.io import imsave
34

45
from numpy_turtle import Turtle, l_system
56

@@ -17,12 +18,13 @@ def main():
1718
padding = 32
1819
n = 6
1920

20-
a = np.zeros((rows, cols))
21+
a = np.zeros((rows, cols, 4))
2122
s = l_system.grow(axiom, rules, n)
2223

23-
t = Turtle(a)
24+
t = Turtle(a, aa=True)
2425
t.position = rows - padding, padding
2526
t.rotate(np.pi - angle)
27+
t.color = (0, 1, 0, 1)
2628

2729
for s_n in s:
2830
if s_n == 'F':
@@ -36,7 +38,7 @@ def main():
3638
elif s_n == ']':
3739
t.pop()
3840

39-
toimage(a).show()
41+
imsave('images/fractal_plant.png', a)
4042

4143

4244
if __name__ == '__main__':
11.9 KB
Loading
Loading

numpy_turtle/examples/sierpinski_triangle.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import numpy as np
2-
from scipy.misc import toimage
2+
from skimage.io import imsave
33

44
from numpy_turtle import Turtle, l_system
55

@@ -20,12 +20,13 @@ def main():
2020
rows = int(np.ceil(cols * np.sin(angle / 2)))
2121
n = 8
2222

23-
a = np.zeros((rows, cols))
23+
a = np.zeros((rows, cols, 4))
2424
s = l_system.grow(axiom, rules, n)
2525

2626
t = Turtle(a)
2727
t.position = rows, 0
2828
t.rotate(np.pi / 2)
29+
t.color = (0, 0, 0, 1)
2930

3031
for s_n in s:
3132
if s_n == 'F' or s_n == 'G':
@@ -35,7 +36,7 @@ def main():
3536
elif s_n == '+':
3637
t.rotate(-angle)
3738

38-
toimage(a).show()
39+
imsave('images/sierpinski_triangle.png', a)
3940

4041

4142
if __name__ == '__main__':

numpy_turtle/numpy_turtle.py

+62-19
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
from typing import Tuple
1+
from typing import Tuple, Union
22

33
import numpy as np
44
from skimage.draw import line, line_aa
55

66
_TAU = np.pi * 2
77

8+
Color = Union[int, float, Tuple[int, ...], Tuple[float, ...]]
9+
810

911
class Turtle:
10-
def __init__(self, array, deg: bool=False, aa: bool=False):
12+
def __init__(self, array: np.ndarray, deg: bool = False, aa: bool = False):
1113
"""Draw on a NumPy array using turtle graphics.
1214
1315
Starts at (0, 0) (top-left corner) with a direction of 0 (pointing
@@ -16,18 +18,23 @@ def __init__(self, array, deg: bool=False, aa: bool=False):
1618
Parameters
1719
----------
1820
array: np.ndarray
19-
The 2D array to write to
21+
The 2D array to write to. Can be either of shape h x w (grayscale),
22+
h x w x c (e.g. rgb for c=3 channels).
23+
The dtype is used to determine the color depth of each channel:
24+
25+
* `bool` for 2 colors.
26+
* All `np.integer` subtypes for discrete depth, ranging from 0 to
27+
its max value (e.g. `np.uint8` for values in 0 - 255).
28+
* All `np.floating` subtypes for continuous depth, ranging from 0
29+
to 1.
30+
2031
deg : :obj:`bool`, optional
2132
Use degrees instead of radians.
2233
aa : :obj:`bool`, optional
2334
Enable anti-aliasing.
2435
"""
25-
assert type(array) is np.ndarray, 'Array should be a NumPy ndarray'
26-
assert array.ndim == 2, 'Only 2D arrays are supported'
27-
assert (
28-
np.issubdtype(array.dtype, np.integer) or
29-
np.issubdtype(array.dtype, np.floating)
30-
), '{} is unsupported'.format(array.dtype)
36+
if type(array) is not np.ndarray:
37+
raise TypeError('Array should be a NumPy ndarray')
3138

3239
self.array = array
3340
self.aa = aa
@@ -37,10 +44,32 @@ def __init__(self, array, deg: bool=False, aa: bool=False):
3744
self.__r, self.__c = 0, 0
3845
self.__stack = []
3946

40-
if np.issubdtype(array.dtype, np.integer):
41-
self.__color = np.iinfo(array.dtype).max
47+
if array.ndim == 2:
48+
self.__channels = 1
49+
elif array.ndim == 3:
50+
self.__channels = array.shape[2]
51+
else:
52+
raise TypeError('Array does not have 2 or 3 dimensions')
53+
54+
if array.dtype == np.dtype(bool):
55+
self.__depth = 1
56+
self.__dtype = bool
57+
elif np.issubdtype(array.dtype, np.integer):
58+
self.__depth = np.iinfo(array.dtype).max
59+
self.__dtype = int
4260
elif np.issubdtype(array.dtype, np.floating):
43-
self.__color = np.finfo(array.dtype).max
61+
self.__depth = 1.0
62+
self.__dtype = float
63+
else:
64+
raise TypeError(
65+
'Array should have a bool, int-like, or float-like dtype'
66+
)
67+
68+
# color initially the max depth (white).
69+
if self.__channels == 1:
70+
self.__color = self.__depth
71+
else:
72+
self.__color = np.full(self.__channels, self.__depth, self.__dtype)
4473

4574
def __in_array(self, r=None, c=None):
4675
r = self.__r if r is None else r
@@ -58,10 +87,15 @@ def __draw_line(self, new_c, new_r):
5887

5988
if self.aa:
6089
rr, cc, val = line_aa(r0, c0, r1, c1)
61-
self.array[rr, cc] = (val / 255 * self.__color).astype(self.array.dtype)
6290
else:
6391
rr, cc = line(r0, c0, r1, c1)
64-
self.array[rr, cc] = self.__color
92+
val = 1
93+
94+
if self.__channels == 1:
95+
self.array[rr, cc] = val * self.__color
96+
else:
97+
for c in range(self.__channels):
98+
self.array[rr, cc, c] = val * self.__color[c]
6599

66100
def forward(self, distance: float):
67101
"""Move in the current direction and draw a line with Euclidian
@@ -123,10 +157,19 @@ def position(self, rc: Tuple[int, int]):
123157
self.__r, self.__c = rc
124158

125159
@property
126-
def color(self) -> float:
127-
"""float: Grayscale color"""
128-
return self.__color
160+
def color(self) -> Color:
161+
"""int, float, tuple of int or tuple of float: Grayscale color"""
162+
if self.__channels == 1:
163+
return self.__color
164+
else:
165+
return tuple(self.__color)
129166

130167
@color.setter
131-
def color(self, c: float):
132-
self.__color = c
168+
def color(self, c: Color):
169+
if not np.isscalar(c) and len(c) != self.__channels:
170+
raise TypeError('Invalid amount of color values')
171+
for _c in [c] if np.isscalar(c) else c:
172+
if _c < 0 or _c > self.__depth:
173+
raise ValueError('Color value out of range')
174+
175+
self.__color = np.array(c, dtype=self.__dtype)

numpy_turtle/tests/test_turtle.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
class TestTurtleUint8(TestCase):
1010
def setUp(self):
1111
self.size = 10
12-
self.array = np.zeros((self.size, self.size), dtype=np.uint8)
12+
self.array = np.zeros((self.size, self.size, 3), dtype=np.uint8)
1313
self.turtle = Turtle(self.array)
1414

1515
def test_pos_forward(self):
@@ -39,5 +39,9 @@ def test_eye(self):
3939
self.turtle.rotate(0.25 * np.pi)
4040
self.turtle.forward(np.sqrt(2 * 10**2))
4141

42-
eye = np.eye(self.size, dtype=self.array.dtype) * self.turtle.color
43-
npt.assert_array_equal(self.array, eye)
42+
eye = np.eye(self.size, dtype=self.array.dtype)
43+
eye3 = np.zeros_like(self.array)
44+
for i, c in enumerate(self.turtle.color):
45+
eye3[:, :, i] = eye * c
46+
47+
npt.assert_array_equal(self.array, eye3)

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
setup(
44
name='numpy-turtle',
5-
version='0.1',
5+
version='0.2',
66
packages=['numpy_turtle'],
77
python_requires='>=3.5',
88
install_requires=['numpy>=1.13.1', 'scikit_image>=0.13.1'],

0 commit comments

Comments
 (0)