From 8e2f3977005334029e51d39bdb975365fee35425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Boulogne?= Date: Sun, 9 Nov 2014 22:05:59 +0100 Subject: [PATCH] add shape option to draw functions --- skimage/draw/_draw.pyx | 66 +++++++++++++++++++++++++++++++-- skimage/draw/tests/test_draw.py | 50 +++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 4 deletions(-) diff --git a/skimage/draw/_draw.pyx b/skimage/draw/_draw.pyx index 7300a2a1..69dd1075 100644 --- a/skimage/draw/_draw.pyx +++ b/skimage/draw/_draw.pyx @@ -10,6 +10,31 @@ from libc.math cimport sqrt, sin, cos, floor, ceil from skimage._shared.geometry cimport point_in_polygon +def _coords_inside_image(rr, cc, shape, val=None): + """ + Return the coordinates inside an image of a given shape. + + Parameters + ---------- + rr, cc : (N,) ndarray of int + Indices of pixels. + shape : tuple, optional + Image shape which is used to determine maximum extents of output pixel + coordinates. + val : ndarray of float + Values of pixels. + + Returns + ------- + rr, cc, val : (N,) ndarray (int, int, float) + Indices of pixels (and values if specified) inside the shape. + """ + mask = (rr >= 0) & (rr < shape[0]) & (cc >= 0) & (cc < shape[1]) + if val is not None: + return rr[mask], cc[mask], val[mask] + return rr[mask], cc[mask] + + def line(Py_ssize_t y, Py_ssize_t x, Py_ssize_t y2, Py_ssize_t x2): """Generate line pixel coordinates. @@ -263,7 +288,7 @@ def polygon(y, x, shape=None): def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, - method='bresenham'): + method='bresenham', shape=None): """Generate circle perimeter coordinates. Parameters @@ -275,6 +300,10 @@ def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, method : {'bresenham', 'andres'}, optional bresenham : Bresenham method (default) andres : Andres method + shape : tuple, optional + Image shape which is used to determine maximum extents of output pixel + coordinates. This is useful for circles which exceed the image size. + By default the full extents of the polygon are used. Returns ------- @@ -361,11 +390,16 @@ def circle_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, d = d + 2 * (y - x - 1) y = y - 1 x = x + 1 + if shape is not None: + return _coords_inside_image(np.array(rr, dtype=np.intp) + cy, + np.array(cc, dtype=np.intp) + cx, + shape) return (np.array(rr, dtype=np.intp) + cy, np.array(cc, dtype=np.intp) + cx) -def circle_perimeter_aa(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius): +def circle_perimeter_aa(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius, + shape=None): """Generate anti-aliased circle perimeter coordinates. Parameters @@ -374,6 +408,10 @@ def circle_perimeter_aa(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius): Centre coordinate of circle. radius: int Radius of circle. + shape : tuple, optional + Image shape which is used to determine maximum extents of output pixel + coordinates. This is useful for circles which exceed the image size. + By default the full extents of the polygon are used. Returns ------- @@ -436,13 +474,18 @@ def circle_perimeter_aa(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t radius): val.extend([1 - dceil, dceil] * 8) dceil_prev = dceil + if shape is not None: + return _coords_inside_image(np.array(rr, dtype=np.intp) + cy, + np.array(cc, dtype=np.intp) + cx, + shape, + val=np.array(val, dtype=np.float)) return (np.array(rr, dtype=np.intp) + cy, np.array(cc, dtype=np.intp) + cx, np.array(val, dtype=np.float)) def ellipse_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t yradius, - Py_ssize_t xradius, double orientation=0): + Py_ssize_t xradius, double orientation=0, shape=None): """Generate ellipse perimeter coordinates. Parameters @@ -453,6 +496,10 @@ def ellipse_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t yradius, Minor and major semi-axes. ``(x/xradius)**2 + (y/yradius)**2 = 1``. orientation : double, optional (default 0) Major axis orientation in clockwise direction as radians. + shape : tuple, optional + Image shape which is used to determine maximum extents of output pixel + coordinates. This is useful for ellipses which exceed the image size. + By default the full extents of the polygon are used. Returns ------- @@ -574,6 +621,9 @@ def ellipse_perimeter(Py_ssize_t cy, Py_ssize_t cx, Py_ssize_t yradius, py.extend(rr) px.extend(cc) + if shape is not None: + return _coords_inside_image(np.array(py, dtype=np.intp), + np.array(px, dtype=np.intp), shape) return np.array(py, dtype=np.intp), np.array(px, dtype=np.intp) @@ -708,7 +758,7 @@ def _bezier_segment(Py_ssize_t y0, Py_ssize_t x0, def bezier_curve(Py_ssize_t y0, Py_ssize_t x0, Py_ssize_t y1, Py_ssize_t x1, Py_ssize_t y2, Py_ssize_t x2, - double weight): + double weight, shape=None): """Generate Bezier curve coordinates. Parameters @@ -721,6 +771,10 @@ def bezier_curve(Py_ssize_t y0, Py_ssize_t x0, Coordinates of the last control point. weight : double Middle control point weight, it describes the line tension. + shape : tuple, optional + Image shape which is used to determine maximum extents of output pixel + coordinates. This is useful for curves which exceed the image size. + By default the full extents of the polygon are used. Returns ------- @@ -833,4 +887,8 @@ def bezier_curve(Py_ssize_t y0, Py_ssize_t x0, rr, cc = _bezier_segment(y0, x0, y1, x1, y2, x2, weight * weight) px.extend(rr) py.extend(cc) + + if shape is not None: + return _coords_inside_image(np.array(px, dtype=np.intp), + np.array(py, dtype=np.intp), shape) return np.array(px, dtype=np.intp), np.array(py, dtype=np.intp) diff --git a/skimage/draw/tests/test_draw.py b/skimage/draw/tests/test_draw.py index dce29f6a..fd759b4e 100644 --- a/skimage/draw/tests/test_draw.py +++ b/skimage/draw/tests/test_draw.py @@ -234,6 +234,17 @@ def test_circle_perimeter_bresenham(): assert_array_equal(img, img_) +def test_circle_perimeter_bresenham_shape(): + img = np.zeros((15, 20), 'uint8') + rr, cc = circle_perimeter(7, 10, 9, method='bresenham', shape=(15, 20)) + img[rr, cc] = 1 + shift = 5 + img_ = np.zeros((15 + 2 * shift, 20), 'uint8') + rr, cc = circle_perimeter(7 + shift, 10, 9, method='bresenham', shape=None) + img_[rr, cc] = 1 + assert_array_equal(img, img_[shift:-shift, :]) + + def test_circle_perimeter_andres(): img = np.zeros((15, 15), 'uint8') rr, cc = circle_perimeter(7, 7, 0, method='andres') @@ -298,6 +309,16 @@ def test_circle_perimeter_aa(): assert_array_equal(img, img_) +def test_circle_perimeter_aa_shape(): + img = np.zeros((15, 20), 'uint8') + rr, cc, val = circle_perimeter_aa(7, 10, 9, shape=(15, 20)) + img[rr, cc] = val * 255 + shift = 5 + img_ = np.zeros((15 + 2 * shift, 20), 'uint8') + rr, cc, val = circle_perimeter_aa(7 + shift, 10, 9, shape=None) + img_[rr, cc] = val * 255 + assert_array_equal(img, img_[shift:-shift, :]) + def test_ellipse_trivial(): img = np.zeros((2, 2), 'uint8') rr, cc = ellipse(0.5, 0.5, 0.5, 0.5) @@ -580,6 +601,17 @@ def test_ellipse_perimeter_nzeroangle(): assert_array_equal(img, img_) +def test_ellipse_perimeter_shape(): + img = np.zeros((15, 20), 'uint8') + rr, cc = ellipse_perimeter(7, 10, 9, 9, 0, shape=(15, 20)) + img[rr, cc] = 1 + shift = 5 + img_ = np.zeros((15 + 2 * shift, 20), 'uint8') + rr, cc = ellipse_perimeter(7 + shift, 10, 9, 9, 0, shape=None) + img_[rr, cc] = 1 + assert_array_equal(img, img_[shift:-shift, :]) + + def test_bezier_segment_straight(): image = np.zeros((200, 200), dtype=int) x0 = 50 @@ -726,6 +758,24 @@ def test_bezier_curved_weight_neq_1(): ) assert_equal(img, img_) + +def test_bezier_curve_shape(): + img = np.zeros((15, 20), 'uint8') + x1, y1 = (1, 5) + x2, y2 = (6, 11) + x3, y3 = (1, 14) + rr, cc = bezier_curve(x1, y1, x2, y2, x3, y3, 2, shape=(15, 20)) + img[rr, cc] = 1 + shift = 5 + img_ = np.zeros((15 + 2 * shift, 20), 'uint8') + x1, y1 = (1 + shift, 5) + x2, y2 = (6 + shift, 11) + x3, y3 = (1 + shift, 14) + rr, cc = bezier_curve(x1, y1, x2, y2, x3, y3, 2, shape=None) + img_[rr, cc] = 1 + assert_array_equal(img, img_[shift:-shift, :]) + + if __name__ == "__main__": from numpy.testing import run_module_suite run_module_suite()