Fixed database typo and removed unnecessary class identifier.

This commit is contained in:
Batuhan Berk Başoğlu 2020-10-14 10:10:37 -04:00
parent 00ad49a143
commit 45fb349a7d
5098 changed files with 952558 additions and 85 deletions

View file

@ -0,0 +1,31 @@
from .draw import (circle, ellipse, set_color, polygon_perimeter,
line, line_aa, polygon, ellipse_perimeter,
circle_perimeter, circle_perimeter_aa,
disk,
bezier_curve, rectangle, rectangle_perimeter)
from .draw3d import ellipsoid, ellipsoid_stats
from ._draw import _bezier_segment
from ._random_shapes import random_shapes
from ._polygon2mask import polygon2mask
from .draw_nd import line_nd
__all__ = ['line',
'line_aa',
'line_nd',
'bezier_curve',
'polygon',
'polygon_perimeter',
'ellipse',
'ellipse_perimeter',
'ellipsoid',
'ellipsoid_stats',
'circle',
'circle_perimeter',
'circle_perimeter_aa',
'disk',
'set_color',
'random_shapes',
'rectangle',
'rectangle_perimeter',
'polygon2mask']

View file

@ -0,0 +1,41 @@
import numpy as np
from . import draw
def polygon2mask(image_shape, polygon):
"""Compute a mask from polygon.
Parameters
----------
image_shape : tuple of size 2.
The shape of the mask.
polygon : array_like.
The polygon coordinates of shape (N, 2) where N is
the number of points.
Returns
-------
mask : 2-D ndarray of type 'bool'.
The mask that corresponds to the input polygon.
Notes
-----
This function does not do any border checking, so that all
the vertices need to be within the given shape.
Examples
--------
>>> image_shape = (128, 128)
>>> polygon = np.array([[60, 100], [100, 40], [40, 40]])
>>> mask = polygon2mask(image_shape, polygon)
>>> mask.shape
(128, 128)
"""
polygon = np.asarray(polygon)
vertex_row_coords, vertex_col_coords = polygon.T
fill_row_coords, fill_col_coords = draw.polygon(
vertex_row_coords, vertex_col_coords, image_shape)
mask = np.zeros(image_shape, dtype=np.bool)
mask[fill_row_coords, fill_col_coords] = True
return mask

View file

@ -0,0 +1,434 @@
import math
import numpy as np
from . import (polygon as draw_polygon, disk as draw_disk,
ellipse as draw_ellipse)
from .._shared.utils import warn
def _generate_rectangle_mask(point, image, shape, random):
"""Generate a mask for a filled rectangle shape.
The height and width of the rectangle are generated randomly.
Parameters
----------
point : tuple
The row and column of the top left corner of the rectangle.
image : tuple
The height, width and depth of the image into which the shape is placed.
shape : tuple
The minimum and maximum size of the shape to fit.
random : np.random.RandomState
The random state to use for random sampling.
Raises
------
ArithmeticError
When a shape cannot be fit into the image with the given starting
coordinates. This usually means the image dimensions are too small or
shape dimensions too large.
Returns
-------
label : tuple
A (category, ((r0, r1), (c0, c1))) tuple specifying the category and
bounding box coordinates of the shape.
indices : 2-D array
A mask of indices that the shape fills.
"""
available_width = min(image[1] - point[1], shape[1])
if available_width < shape[0]:
raise ArithmeticError('cannot fit shape to image')
available_height = min(image[0] - point[0], shape[1])
if available_height < shape[0]:
raise ArithmeticError('cannot fit shape to image')
# Pick random widths and heights.
r = random.randint(shape[0], available_height + 1)
c = random.randint(shape[0], available_width + 1)
rectangle = draw_polygon([
point[0],
point[0] + r,
point[0] + r,
point[0],
], [
point[1],
point[1],
point[1] + c,
point[1] + c,
])
label = ('rectangle', ((point[0], point[0] + r), (point[1], point[1] + c)))
return rectangle, label
def _generate_circle_mask(point, image, shape, random):
"""Generate a mask for a filled circle shape.
The radius of the circle is generated randomly.
Parameters
----------
point : tuple
The row and column of the top left corner of the rectangle.
image : tuple
The height, width and depth of the image into which the shape is placed.
shape : tuple
The minimum and maximum size and color of the shape to fit.
random : np.random.RandomState
The random state to use for random sampling.
Raises
------
ArithmeticError
When a shape cannot be fit into the image with the given starting
coordinates. This usually means the image dimensions are too small or
shape dimensions too large.
Returns
-------
label : tuple
A (category, ((r0, r1), (c0, c1))) tuple specifying the category and
bounding box coordinates of the shape.
indices : 2-D array
A mask of indices that the shape fills.
"""
if shape[0] == 1 or shape[1] == 1:
raise ValueError('size must be > 1 for circles')
min_radius = shape[0] / 2.0
max_radius = shape[1] / 2.0
left = point[1]
right = image[1] - point[1]
top = point[0]
bottom = image[0] - point[0]
available_radius = min(left, right, top, bottom, max_radius)
if available_radius < min_radius:
raise ArithmeticError('cannot fit shape to image')
radius = random.randint(min_radius, available_radius + 1)
# TODO: think about how to deprecate this
# while draw_circle was deprecated in favor of draw_disk
# switching to a label of 'disk' here
# would be a breaking change for downstream libraries
# See discussion on naming convention here
# https://github.com/scikit-image/scikit-image/pull/4428
disk = draw_disk((point[0], point[1]), radius)
# Until a deprecation path is decided, always return `'circle'`
label = ('circle', ((point[0] - radius + 1, point[0] + radius),
(point[1] - radius + 1, point[1] + radius)))
return disk, label
def _generate_triangle_mask(point, image, shape, random):
"""Generate a mask for a filled equilateral triangle shape.
The length of the sides of the triangle is generated randomly.
Parameters
----------
point : tuple
The row and column of the top left corner of a down-pointing triangle.
image : tuple
The height, width and depth of the image into which the shape is placed.
shape : tuple
The minimum and maximum size and color of the shape to fit.
random : np.random.RandomState
The random state to use for random sampling.
Raises
------
ArithmeticError
When a shape cannot be fit into the image with the given starting
coordinates. This usually means the image dimensions are too small or
shape dimensions too large.
Returns
-------
label : tuple
A (category, ((r0, r1), (c0, c1))) tuple specifying the category and
bounding box coordinates of the shape.
indices : 2-D array
A mask of indices that the shape fills.
"""
if shape[0] == 1 or shape[1] == 1:
raise ValueError('dimension must be > 1 for triangles')
available_side = min(image[1] - point[1], point[0] + 1, shape[1])
if available_side < shape[0]:
raise ArithmeticError('cannot fit shape to image')
side = random.randint(shape[0], available_side + 1)
triangle_height = int(np.ceil(np.sqrt(3 / 4.0) * side))
triangle = draw_polygon([
point[0],
point[0] - triangle_height,
point[0],
], [
point[1],
point[1] + side // 2,
point[1] + side,
])
label = ('triangle', ((point[0] - triangle_height, point[0]),
(point[1], point[1] + side)))
return triangle, label
def _generate_ellipse_mask(point, image, shape, random):
"""Generate a mask for a filled ellipse shape.
The rotation, major and minor semi-axes of the ellipse are generated
randomly.
Parameters
----------
point : tuple
The row and column of the top left corner of the rectangle.
image : tuple
The height, width and depth of the image into which the shape is
placed.
shape : tuple
The minimum and maximum size and color of the shape to fit.
random : np.random.RandomState
The random state to use for random sampling.
Raises
------
ArithmeticError
When a shape cannot be fit into the image with the given starting
coordinates. This usually means the image dimensions are too small or
shape dimensions too large.
Returns
-------
label : tuple
A (category, ((r0, r1), (c0, c1))) tuple specifying the category and
bounding box coordinates of the shape.
indices : 2-D array
A mask of indices that the shape fills.
"""
if shape[0] == 1 or shape[1] == 1:
raise ValueError('size must be > 1 for ellipses')
min_radius = shape[0] / 2.0
max_radius = shape[1] / 2.0
left = point[1]
right = image[1] - point[1]
top = point[0]
bottom = image[0] - point[0]
available_radius = min(left, right, top, bottom, max_radius)
if available_radius < min_radius:
raise ArithmeticError('cannot fit shape to image')
# NOTE: very conservative because we could take into account the fact that
# we have 2 different radii, but this is a good first approximation.
# Also, we can afford to have a uniform sampling because the ellipse will
# be rotated.
r_radius = random.uniform(min_radius, available_radius + 1)
c_radius = random.uniform(min_radius, available_radius + 1)
rotation = random.uniform(-np.pi, np.pi)
ellipse = draw_ellipse(
point[0],
point[1],
r_radius,
c_radius,
shape=image[:2],
rotation=rotation,
)
max_radius = math.ceil(max(r_radius, c_radius))
min_x = np.min(ellipse[0])
max_x = np.max(ellipse[0]) + 1
min_y = np.min(ellipse[1])
max_y = np.max(ellipse[1]) + 1
label = ('ellipse', ((min_x, max_x), (min_y, max_y)))
return ellipse, label
# Allows lookup by key as well as random selection.
SHAPE_GENERATORS = dict(
rectangle=_generate_rectangle_mask,
circle=_generate_circle_mask,
triangle=_generate_triangle_mask,
ellipse=_generate_ellipse_mask)
SHAPE_CHOICES = list(SHAPE_GENERATORS.values())
def _generate_random_colors(num_colors, num_channels, intensity_range, random):
"""Generate an array of random colors.
Parameters
----------
num_colors : int
Number of colors to generate.
num_channels : int
Number of elements representing color.
intensity_range : {tuple of tuples of ints, tuple of ints}, optional
The range of values to sample pixel values from. For grayscale images
the format is (min, max). For multichannel - ((min, max),) if the
ranges are equal across the channels, and
((min_0, max_0), ... (min_N, max_N)) if they differ.
random : np.random.RandomState
The random state to use for random sampling.
Raises
------
ValueError
When the `intensity_range` is not in the interval (0, 255).
Returns
-------
colors : array
An array of shape (num_colors, num_channels), where the values for
each channel are drawn from the corresponding `intensity_range`.
"""
if num_channels == 1:
intensity_range = (intensity_range, )
elif len(intensity_range) == 1:
intensity_range = intensity_range * num_channels
colors = [random.randint(r[0], r[1]+1, size=num_colors)
for r in intensity_range]
return np.transpose(colors)
def random_shapes(image_shape,
max_shapes,
min_shapes=1,
min_size=2,
max_size=None,
multichannel=True,
num_channels=3,
shape=None,
intensity_range=None,
allow_overlap=False,
num_trials=100,
random_seed=None):
"""Generate an image with random shapes, labeled with bounding boxes.
The image is populated with random shapes with random sizes, random
locations, and random colors, with or without overlap.
Shapes have random (row, col) starting coordinates and random sizes bounded
by `min_size` and `max_size`. It can occur that a randomly generated shape
will not fit the image at all. In that case, the algorithm will try again
with new starting coordinates a certain number of times. However, it also
means that some shapes may be skipped altogether. In that case, this
function will generate fewer shapes than requested.
Parameters
----------
image_shape : tuple
The number of rows and columns of the image to generate.
max_shapes : int
The maximum number of shapes to (attempt to) fit into the shape.
min_shapes : int, optional
The minimum number of shapes to (attempt to) fit into the shape.
min_size : int, optional
The minimum dimension of each shape to fit into the image.
max_size : int, optional
The maximum dimension of each shape to fit into the image.
multichannel : bool, optional
If True, the generated image has ``num_channels`` color channels,
otherwise generates grayscale image.
num_channels : int, optional
Number of channels in the generated image. If 1, generate monochrome
images, else color images with multiple channels. Ignored if
``multichannel`` is set to False.
shape : {rectangle, circle, triangle, ellipse, None} str, optional
The name of the shape to generate or `None` to pick random ones.
intensity_range : {tuple of tuples of uint8, tuple of uint8}, optional
The range of values to sample pixel values from. For grayscale images
the format is (min, max). For multichannel - ((min, max),) if the
ranges are equal across the channels, and ((min_0, max_0), ... (min_N, max_N))
if they differ. As the function supports generation of uint8 arrays only,
the maximum range is (0, 255). If None, set to (0, 254) for each
channel reserving color of intensity = 255 for background.
allow_overlap : bool, optional
If `True`, allow shapes to overlap.
num_trials : int, optional
How often to attempt to fit a shape into the image before skipping it.
random_seed : int, optional
Seed to initialize the random number generator.
If `None`, a random seed from the operating system is used.
Returns
-------
image : uint8 array
An image with the fitted shapes.
labels : list
A list of labels, one per shape in the image. Each label is a
(category, ((r0, r1), (c0, c1))) tuple specifying the category and
bounding box coordinates of the shape.
Examples
--------
>>> import skimage.draw
>>> image, labels = skimage.draw.random_shapes((32, 32), max_shapes=3)
>>> image # doctest: +SKIP
array([
[[255, 255, 255],
[255, 255, 255],
[255, 255, 255],
...,
[255, 255, 255],
[255, 255, 255],
[255, 255, 255]]], dtype=uint8)
>>> labels # doctest: +SKIP
[('circle', ((22, 18), (25, 21))),
('triangle', ((5, 6), (13, 13)))]
"""
if min_size > image_shape[0] or min_size > image_shape[1]:
raise ValueError('Minimum dimension must be less than ncols and nrows')
max_size = max_size or max(image_shape[0], image_shape[1])
if not multichannel:
num_channels = 1
if intensity_range is None:
intensity_range = (0, 254) if num_channels == 1 else ((0, 254), )
else:
tmp = (intensity_range, ) if num_channels == 1 else intensity_range
for intensity_pair in tmp:
for intensity in intensity_pair:
if not (0 <= intensity <= 255):
msg = 'Intensity range must lie within (0, 255) interval'
raise ValueError(msg)
random = np.random.RandomState(random_seed)
user_shape = shape
image_shape = (image_shape[0], image_shape[1], num_channels)
image = np.full(image_shape, 255, dtype=np.uint8)
filled = np.zeros(image_shape, dtype=bool)
labels = []
num_shapes = random.randint(min_shapes, max_shapes + 1)
colors = _generate_random_colors(num_shapes, num_channels,
intensity_range, random)
for shape_idx in range(num_shapes):
if user_shape is None:
shape_generator = random.choice(SHAPE_CHOICES)
else:
shape_generator = SHAPE_GENERATORS[user_shape]
shape = (min_size, max_size)
for _ in range(num_trials):
# Pick start coordinates.
column = random.randint(image_shape[1])
row = random.randint(image_shape[0])
point = (row, column)
try:
indices, label = shape_generator(point, image_shape, shape,
random)
except ArithmeticError:
# Couldn't fit the shape, skip it.
continue
# Check if there is an overlap where the mask is nonzero.
if allow_overlap or not filled[indices].any():
image[indices] = colors[shape_idx]
filled[indices] = True
labels.append(label)
break
else:
warn('Could not fit any shapes to image, '
'consider reducing the minimum dimension')
if not multichannel:
image = np.squeeze(image, axis=2)
return image, labels

View file

@ -0,0 +1,949 @@
import warnings
import numpy as np
from .._shared._geometry import polygon_clip
from ._draw import (_coords_inside_image, _line, _line_aa,
_polygon, _ellipse_perimeter,
_circle_perimeter, _circle_perimeter_aa,
_bezier_curve)
def _ellipse_in_shape(shape, center, radii, rotation=0.):
"""Generate coordinates of points within ellipse bounded by shape.
Parameters
----------
shape : iterable of ints
Shape of the input image. Must be at least length 2. Only the first
two values are used to determine the extent of the input image.
center : iterable of floats
(row, column) position of center inside the given shape.
radii : iterable of floats
Size of two half axes (for row and column)
rotation : float, optional
Rotation of the ellipse defined by the above, in radians
in range (-PI, PI), in contra clockwise direction,
with respect to the column-axis.
Returns
-------
rows : iterable of ints
Row coordinates representing values within the ellipse.
cols : iterable of ints
Corresponding column coordinates representing values within the ellipse.
"""
r_lim, c_lim = np.ogrid[0:float(shape[0]), 0:float(shape[1])]
r_org, c_org = center
r_rad, c_rad = radii
rotation %= np.pi
sin_alpha, cos_alpha = np.sin(rotation), np.cos(rotation)
r, c = (r_lim - r_org), (c_lim - c_org)
distances = ((r * cos_alpha + c * sin_alpha) / r_rad) ** 2 \
+ ((r * sin_alpha - c * cos_alpha) / c_rad) ** 2
return np.nonzero(distances < 1)
def ellipse(r, c, r_radius, c_radius, shape=None, rotation=0.):
"""Generate coordinates of pixels within ellipse.
Parameters
----------
r, c : double
Centre coordinate of ellipse.
r_radius, c_radius : double
Minor and major semi-axes. ``(r/r_radius)**2 + (c/c_radius)**2 = 1``.
shape : tuple, optional
Image shape which is used to determine the maximum extent of output pixel
coordinates. This is useful for ellipses which exceed the image size.
By default the full extent of the ellipse are used. Must be at least
length 2. Only the first two values are used to determine the extent.
rotation : float, optional (default 0.)
Set the ellipse rotation (rotation) in range (-PI, PI)
in contra clock wise direction, so PI/2 degree means swap ellipse axis
Returns
-------
rr, cc : ndarray of int
Pixel coordinates of ellipse.
May be used to directly index into an array, e.g.
``img[rr, cc] = 1``.
Examples
--------
>>> from skimage.draw import ellipse
>>> img = np.zeros((10, 12), dtype=np.uint8)
>>> rr, cc = ellipse(5, 6, 3, 5, rotation=np.deg2rad(30))
>>> img[rr, cc] = 1
>>> img
array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
Notes
-----
The ellipse equation::
((x * cos(alpha) + y * sin(alpha)) / x_radius) ** 2 +
((x * sin(alpha) - y * cos(alpha)) / y_radius) ** 2 = 1
Note that the positions of `ellipse` without specified `shape` can have
also, negative values, as this is correct on the plane. On the other hand
using these ellipse positions for an image afterwards may lead to appearing
on the other side of image, because ``image[-1, -1] = image[end-1, end-1]``
>>> rr, cc = ellipse(1, 2, 3, 6)
>>> img = np.zeros((6, 12), dtype=np.uint8)
>>> img[rr, cc] = 1
>>> img
array([[1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1]], dtype=uint8)
"""
center = np.array([r, c])
radii = np.array([r_radius, c_radius])
# allow just rotation with in range +/- 180 degree
rotation %= np.pi
# compute rotated radii by given rotation
r_radius_rot = abs(r_radius * np.cos(rotation)) \
+ c_radius * np.sin(rotation)
c_radius_rot = r_radius * np.sin(rotation) \
+ abs(c_radius * np.cos(rotation))
# The upper_left and lower_right corners of the smallest rectangle
# containing the ellipse.
radii_rot = np.array([r_radius_rot, c_radius_rot])
upper_left = np.ceil(center - radii_rot).astype(int)
lower_right = np.floor(center + radii_rot).astype(int)
if shape is not None:
# Constrain upper_left and lower_right by shape boundary.
upper_left = np.maximum(upper_left, np.array([0, 0]))
lower_right = np.minimum(lower_right, np.array(shape[:2]) - 1)
shifted_center = center - upper_left
bounding_shape = lower_right - upper_left + 1
rr, cc = _ellipse_in_shape(bounding_shape, shifted_center, radii, rotation)
rr.flags.writeable = True
cc.flags.writeable = True
rr += upper_left[0]
cc += upper_left[1]
return rr, cc
def circle(r, c, radius, shape=None):
"""Generate coordinates of pixels within circle.
Parameters
----------
r, c : double
Center coordinate of disk.
radius : double
Radius of disk.
shape : tuple, optional
Image shape which is used to determine the maximum extent of output
pixel coordinates. This is useful for disks that exceed the image
size. If None, the full extent of the disk is used. Must be at least
length 2. Only the first two values are used to determine the extent of
the input image.
Returns
-------
rr, cc : ndarray of int
Pixel coordinates of disk.
May be used to directly index into an array, e.g.
``img[rr, cc] = 1``.
Warns
-----
Deprecated:
.. versionadded:: 0.17
This function is deprecated and will be removed in scikit-image 0.19.
Please use the function named ``disk`` instead.
"""
warnings.warn("circle is deprecated in favor of "
"disk."
"circle will be removed in version 0.19",
FutureWarning, stacklevel=2)
return disk((r, c), radius, shape=shape)
def disk(center, radius, *, shape=None):
"""Generate coordinates of pixels within circle.
Parameters
----------
center : tuple
Center coordinate of disk.
radius : double
Radius of disk.
shape : tuple, optional
Image shape which is used to determine the maximum extent of output
pixel coordinates. This is useful for disks that exceed the image
size. If None, the full extent of the disk is used. Must be at least
length 2. Only the first two values are used to determine the extent of
the input image.
Returns
-------
rr, cc : ndarray of int
Pixel coordinates of disk.
May be used to directly index into an array, e.g.
``img[rr, cc] = 1``.
Examples
--------
>>> from skimage.draw import disk
>>> img = np.zeros((10, 10), dtype=np.uint8)
>>> rr, cc = disk((4, 4), 5)
>>> img[rr, cc] = 1
>>> img
array([[0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
"""
r, c = center
return ellipse(r, c, radius, radius, shape)
def polygon_perimeter(r, c, shape=None, clip=False):
"""Generate polygon perimeter coordinates.
Parameters
----------
r : (N,) ndarray
Row coordinates of vertices of polygon.
c : (N,) ndarray
Column coordinates of vertices of polygon.
shape : tuple, optional
Image shape which is used to determine maximum extents of output pixel
coordinates. This is useful for polygons that exceed the image size.
If None, the full extents of the polygon is used. Must be at least
length 2. Only the first two values are used to determine the extent of
the input image.
clip : bool, optional
Whether to clip the polygon to the provided shape. If this is set
to True, the drawn figure will always be a closed polygon with all
edges visible.
Returns
-------
rr, cc : ndarray of int
Pixel coordinates of polygon.
May be used to directly index into an array, e.g.
``img[rr, cc] = 1``.
Examples
--------
>>> from skimage.draw import polygon_perimeter
>>> img = np.zeros((10, 10), dtype=np.uint8)
>>> rr, cc = polygon_perimeter([5, -1, 5, 10],
... [-1, 5, 11, 5],
... shape=img.shape, clip=True)
>>> img[rr, cc] = 1
>>> img
array([[0, 0, 0, 0, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 1, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 1, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[0, 1, 1, 0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 1, 0, 0, 0, 1, 1, 0],
[0, 0, 0, 0, 1, 1, 1, 0, 0, 0]], dtype=uint8)
"""
if clip:
if shape is None:
raise ValueError("Must specify clipping shape")
clip_box = np.array([0, 0, shape[0] - 1, shape[1] - 1])
else:
clip_box = np.array([np.min(r), np.min(c),
np.max(r), np.max(c)])
# Do the clipping irrespective of whether clip is set. This
# ensures that the returned polygon is closed and is an array.
r, c = polygon_clip(r, c, *clip_box)
r = np.round(r).astype(int)
c = np.round(c).astype(int)
# Construct line segments
rr, cc = [], []
for i in range(len(r) - 1):
line_r, line_c = line(r[i], c[i], r[i + 1], c[i + 1])
rr.extend(line_r)
cc.extend(line_c)
rr = np.asarray(rr)
cc = np.asarray(cc)
if shape is None:
return rr, cc
else:
return _coords_inside_image(rr, cc, shape)
def set_color(image, coords, color, alpha=1):
"""Set pixel color in the image at the given coordinates.
Note that this function modifies the color of the image in-place.
Coordinates that exceed the shape of the image will be ignored.
Parameters
----------
image : (M, N, D) ndarray
Image
coords : tuple of ((P,) ndarray, (P,) ndarray)
Row and column coordinates of pixels to be colored.
color : (D,) ndarray
Color to be assigned to coordinates in the image.
alpha : scalar or (N,) ndarray
Alpha values used to blend color with image. 0 is transparent,
1 is opaque.
Examples
--------
>>> from skimage.draw import line, set_color
>>> img = np.zeros((10, 10), dtype=np.uint8)
>>> rr, cc = line(1, 1, 20, 20)
>>> set_color(img, (rr, cc), 1)
>>> img
array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1]], dtype=uint8)
"""
rr, cc = coords
if image.ndim == 2:
image = image[..., np.newaxis]
color = np.array(color, ndmin=1, copy=False)
if image.shape[-1] != color.shape[-1]:
raise ValueError('Color shape ({}) must match last '
'image dimension ({}).'.format(color.shape[0],
image.shape[-1]))
if np.isscalar(alpha):
# Can be replaced by ``full_like`` when numpy 1.8 becomes
# minimum dependency
alpha = np.ones_like(rr) * alpha
rr, cc, alpha = _coords_inside_image(rr, cc, image.shape, val=alpha)
alpha = alpha[..., np.newaxis]
color = color * alpha
vals = image[rr, cc] * (1 - alpha)
image[rr, cc] = vals + color
def line(r0, c0, r1, c1):
"""Generate line pixel coordinates.
Parameters
----------
r0, c0 : int
Starting position (row, column).
r1, c1 : int
End position (row, column).
Returns
-------
rr, cc : (N,) ndarray of int
Indices of pixels that belong to the line.
May be used to directly index into an array, e.g.
``img[rr, cc] = 1``.
Notes
-----
Anti-aliased line generator is available with `line_aa`.
Examples
--------
>>> from skimage.draw import line
>>> img = np.zeros((10, 10), dtype=np.uint8)
>>> rr, cc = line(1, 1, 8, 8)
>>> img[rr, cc] = 1
>>> img
array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
"""
return _line(r0, c0, r1, c1)
def line_aa(r0, c0, r1, c1):
"""Generate anti-aliased line pixel coordinates.
Parameters
----------
r0, c0 : int
Starting position (row, column).
r1, c1 : int
End position (row, column).
Returns
-------
rr, cc, val : (N,) ndarray (int, int, float)
Indices of pixels (`rr`, `cc`) and intensity values (`val`).
``img[rr, cc] = val``.
References
----------
.. [1] A Rasterizing Algorithm for Drawing Curves, A. Zingl, 2012
http://members.chello.at/easyfilter/Bresenham.pdf
Examples
--------
>>> from skimage.draw import line_aa
>>> img = np.zeros((10, 10), dtype=np.uint8)
>>> rr, cc, val = line_aa(1, 1, 8, 8)
>>> img[rr, cc] = val * 255
>>> img
array([[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 255, 74, 0, 0, 0, 0, 0, 0, 0],
[ 0, 74, 255, 74, 0, 0, 0, 0, 0, 0],
[ 0, 0, 74, 255, 74, 0, 0, 0, 0, 0],
[ 0, 0, 0, 74, 255, 74, 0, 0, 0, 0],
[ 0, 0, 0, 0, 74, 255, 74, 0, 0, 0],
[ 0, 0, 0, 0, 0, 74, 255, 74, 0, 0],
[ 0, 0, 0, 0, 0, 0, 74, 255, 74, 0],
[ 0, 0, 0, 0, 0, 0, 0, 74, 255, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
"""
return _line_aa(r0, c0, r1, c1)
def polygon(r, c, shape=None):
"""Generate coordinates of pixels within polygon.
Parameters
----------
r : (N,) ndarray
Row coordinates of vertices of polygon.
c : (N,) ndarray
Column coordinates of vertices of polygon.
shape : tuple, optional
Image shape which is used to determine the maximum extent of output
pixel coordinates. This is useful for polygons that exceed the image
size. If None, the full extent of the polygon is used. Must be at
least length 2. Only the first two values are used to determine the
extent of the input image.
Returns
-------
rr, cc : ndarray of int
Pixel coordinates of polygon.
May be used to directly index into an array, e.g.
``img[rr, cc] = 1``.
Examples
--------
>>> from skimage.draw import polygon
>>> img = np.zeros((10, 10), dtype=np.uint8)
>>> r = np.array([1, 2, 8])
>>> c = np.array([1, 7, 4])
>>> rr, cc = polygon(r, c)
>>> img[rr, cc] = 1
>>> img
array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
"""
return _polygon(r, c, shape)
def circle_perimeter(r, c, radius, method='bresenham', shape=None):
"""Generate circle perimeter coordinates.
Parameters
----------
r, c : int
Centre coordinate of circle.
radius : int
Radius of circle.
method : {'bresenham', 'andres'}, optional
bresenham : Bresenham method (default)
andres : Andres method
shape : tuple, optional
Image shape which is used to determine the maximum extent of output
pixel coordinates. This is useful for circles that exceed the image
size. If None, the full extent of the circle is used. Must be at least
length 2. Only the first two values are used to determine the extent of
the input image.
Returns
-------
rr, cc : (N,) ndarray of int
Bresenham and Andres' method:
Indices of pixels that belong to the circle perimeter.
May be used to directly index into an array, e.g.
``img[rr, cc] = 1``.
Notes
-----
Andres method presents the advantage that concentric
circles create a disc whereas Bresenham can make holes. There
is also less distortions when Andres circles are rotated.
Bresenham method is also known as midpoint circle algorithm.
Anti-aliased circle generator is available with `circle_perimeter_aa`.
References
----------
.. [1] J.E. Bresenham, "Algorithm for computer control of a digital
plotter", IBM Systems journal, 4 (1965) 25-30.
.. [2] E. Andres, "Discrete circles, rings and spheres", Computers &
Graphics, 18 (1994) 695-706.
Examples
--------
>>> from skimage.draw import circle_perimeter
>>> img = np.zeros((10, 10), dtype=np.uint8)
>>> rr, cc = circle_perimeter(4, 4, 3)
>>> img[rr, cc] = 1
>>> img
array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 1, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 1, 0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
"""
return _circle_perimeter(r, c, radius, method, shape)
def circle_perimeter_aa(r, c, radius, shape=None):
"""Generate anti-aliased circle perimeter coordinates.
Parameters
----------
r, c : int
Centre coordinate of circle.
radius : int
Radius of circle.
shape : tuple, optional
Image shape which is used to determine the maximum extent of output
pixel coordinates. This is useful for circles that exceed the image
size. If None, the full extent of the circle is used. Must be at least
length 2. Only the first two values are used to determine the extent of
the input image.
Returns
-------
rr, cc, val : (N,) ndarray (int, int, float)
Indices of pixels (`rr`, `cc`) and intensity values (`val`).
``img[rr, cc] = val``.
Notes
-----
Wu's method draws anti-aliased circle. This implementation doesn't use
lookup table optimization.
Use the function ``draw.set_color`` to apply ``circle_perimeter_aa``
results to color images.
References
----------
.. [1] X. Wu, "An efficient antialiasing technique", In ACM SIGGRAPH
Computer Graphics, 25 (1991) 143-152.
Examples
--------
>>> from skimage.draw import circle_perimeter_aa
>>> img = np.zeros((10, 10), dtype=np.uint8)
>>> rr, cc, val = circle_perimeter_aa(4, 4, 3)
>>> img[rr, cc] = val * 255
>>> img
array([[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 60, 211, 255, 211, 60, 0, 0, 0],
[ 0, 60, 194, 43, 0, 43, 194, 60, 0, 0],
[ 0, 211, 43, 0, 0, 0, 43, 211, 0, 0],
[ 0, 255, 0, 0, 0, 0, 0, 255, 0, 0],
[ 0, 211, 43, 0, 0, 0, 43, 211, 0, 0],
[ 0, 60, 194, 43, 0, 43, 194, 60, 0, 0],
[ 0, 0, 60, 211, 255, 211, 60, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
>>> from skimage import data, draw
>>> image = data.chelsea()
>>> rr, cc, val = draw.circle_perimeter_aa(r=100, c=100, radius=75)
>>> draw.set_color(image, (rr, cc), [1, 0, 0], alpha=val)
"""
return _circle_perimeter_aa(r, c, radius, shape)
def ellipse_perimeter(r, c, r_radius, c_radius, orientation=0, shape=None):
"""Generate ellipse perimeter coordinates.
Parameters
----------
r, c : int
Centre coordinate of ellipse.
r_radius, c_radius : int
Minor and major semi-axes. ``(r/r_radius)**2 + (c/c_radius)**2 = 1``.
orientation : double, optional
Major axis orientation in clockwise direction as radians.
shape : tuple, optional
Image shape which is used to determine the maximum extent of output
pixel coordinates. This is useful for ellipses that exceed the image
size. If None, the full extent of the ellipse is used. Must be at
least length 2. Only the first two values are used to determine the
extent of the input image.
Returns
-------
rr, cc : (N,) ndarray of int
Indices of pixels that belong to the ellipse perimeter.
May be used to directly index into an array, e.g.
``img[rr, cc] = 1``.
References
----------
.. [1] A Rasterizing Algorithm for Drawing Curves, A. Zingl, 2012
http://members.chello.at/easyfilter/Bresenham.pdf
Examples
--------
>>> from skimage.draw import ellipse_perimeter
>>> img = np.zeros((10, 10), dtype=np.uint8)
>>> rr, cc = ellipse_perimeter(5, 5, 3, 4)
>>> img[rr, cc] = 1
>>> img
array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 1, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 1],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 1],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 1],
[0, 0, 1, 0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
Note that the positions of `ellipse` without specified `shape` can have
also, negative values, as this is correct on the plane. On the other hand
using these ellipse positions for an image afterwards may lead to appearing
on the other side of image, because ``image[-1, -1] = image[end-1, end-1]``
>>> rr, cc = ellipse_perimeter(2, 3, 4, 5)
>>> img = np.zeros((9, 12), dtype=np.uint8)
>>> img[rr, cc] = 1
>>> img
array([[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]], dtype=uint8)
"""
return _ellipse_perimeter(r, c, r_radius, c_radius, orientation, shape)
def bezier_curve(r0, c0, r1, c1, r2, c2, weight, shape=None):
"""Generate Bezier curve coordinates.
Parameters
----------
r0, c0 : int
Coordinates of the first control point.
r1, c1 : int
Coordinates of the middle control point.
r2, c2 : int
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 the maximum extent of output
pixel coordinates. This is useful for curves that exceed the image
size. If None, the full extent of the curve is used.
Returns
-------
rr, cc : (N,) ndarray of int
Indices of pixels that belong to the Bezier curve.
May be used to directly index into an array, e.g.
``img[rr, cc] = 1``.
Notes
-----
The algorithm is the rational quadratic algorithm presented in
reference [1]_.
References
----------
.. [1] A Rasterizing Algorithm for Drawing Curves, A. Zingl, 2012
http://members.chello.at/easyfilter/Bresenham.pdf
Examples
--------
>>> import numpy as np
>>> from skimage.draw import bezier_curve
>>> img = np.zeros((10, 10), dtype=np.uint8)
>>> rr, cc = bezier_curve(1, 5, 5, -2, 8, 8, 2)
>>> img[rr, cc] = 1
>>> img
array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 1, 1, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8)
"""
return _bezier_curve(r0, c0, r1, c1, r2, c2, weight, shape)
def rectangle(start, end=None, extent=None, shape=None):
"""Generate coordinates of pixels within a rectangle.
Parameters
----------
start : tuple
Origin point of the rectangle, e.g., ``([plane,] row, column)``.
end : tuple
End point of the rectangle ``([plane,] row, column)``.
For a 2D matrix, the slice defined by the rectangle is
``[start:(end+1)]``.
Either `end` or `extent` must be specified.
extent : tuple
The extent (size) of the drawn rectangle. E.g.,
``([num_planes,] num_rows, num_cols)``.
Either `end` or `extent` must be specified.
A negative extent is valid, and will result in a rectangle
going along the oposite direction. If extent is negative, the
`start` point is not included.
shape : tuple, optional
Image shape used to determine the maximum bounds of the output
coordinates. This is useful for clipping rectangles that exceed
the image size. By default, no clipping is done.
Returns
-------
coords : array of int, shape (Ndim, Npoints)
The coordinates of all pixels in the rectangle.
Notes
-----
This function can be applied to N-dimensional images, by passing `start` and
`end` or `extent` as tuples of length N.
Examples
--------
>>> import numpy as np
>>> from skimage.draw import rectangle
>>> img = np.zeros((5, 5), dtype=np.uint8)
>>> start = (1, 1)
>>> extent = (3, 3)
>>> rr, cc = rectangle(start, extent=extent, shape=img.shape)
>>> img[rr, cc] = 1
>>> img
array([[0, 0, 0, 0, 0],
[0, 1, 1, 1, 0],
[0, 1, 1, 1, 0],
[0, 1, 1, 1, 0],
[0, 0, 0, 0, 0]], dtype=uint8)
>>> img = np.zeros((5, 5), dtype=np.uint8)
>>> start = (0, 1)
>>> end = (3, 3)
>>> rr, cc = rectangle(start, end=end, shape=img.shape)
>>> img[rr, cc] = 1
>>> img
array([[0, 1, 1, 1, 0],
[0, 1, 1, 1, 0],
[0, 1, 1, 1, 0],
[0, 1, 1, 1, 0],
[0, 0, 0, 0, 0]], dtype=uint8)
>>> import numpy as np
>>> from skimage.draw import rectangle
>>> img = np.zeros((6, 6), dtype=np.uint8)
>>> start = (3, 3)
>>>
>>> rr, cc = rectangle(start, extent=(2, 2))
>>> img[rr, cc] = 1
>>> rr, cc = rectangle(start, extent=(-2, 2))
>>> img[rr, cc] = 2
>>> rr, cc = rectangle(start, extent=(-2, -2))
>>> img[rr, cc] = 3
>>> rr, cc = rectangle(start, extent=(2, -2))
>>> img[rr, cc] = 4
>>> print(img)
[[0 0 0 0 0 0]
[0 3 3 2 2 0]
[0 3 3 2 2 0]
[0 4 4 1 1 0]
[0 4 4 1 1 0]
[0 0 0 0 0 0]]
"""
tl, br = _rectangle_slice(start=start, end=end, extent=extent)
if shape is not None:
n_dim = len(start)
br = np.minimum(shape[0:n_dim], br)
tl = np.maximum(np.zeros_like(shape[0:n_dim]), tl)
coords = np.meshgrid(*[np.arange(st, en) for st, en in zip(tuple(tl),
tuple(br))])
return coords
def rectangle_perimeter(start, end=None, extent=None, shape=None, clip=False):
"""Generate coordinates of pixels that are exactly around a rectangle.
Parameters
----------
start : tuple
Origin point of the inner rectangle, e.g., ``(row, column)``.
end : tuple
End point of the inner rectangle ``(row, column)``.
For a 2D matrix, the slice defined by inner the rectangle is
``[start:(end+1)]``.
Either `end` or `extent` must be specified.
extent : tuple
The extent (size) of the inner rectangle. E.g.,
``(num_rows, num_cols)``.
Either `end` or `extent` must be specified.
Negative extents are permitted. See `rectangle` to better
understand how they behave.
shape : tuple, optional
Image shape used to determine the maximum bounds of the output
coordinates. This is useful for clipping perimeters that exceed
the image size. By default, no clipping is done. Must be at least
length 2. Only the first two values are used to determine the extent of
the input image.
clip : bool, optional
Whether to clip the perimeter to the provided shape. If this is set
to True, the drawn figure will always be a closed polygon with all
edges visible.
Returns
-------
coords : array of int, shape (2, Npoints)
The coordinates of all pixels in the rectangle.
Examples
--------
>>> import numpy as np
>>> from skimage.draw import rectangle_perimeter
>>> img = np.zeros((5, 6), dtype=np.uint8)
>>> start = (2, 3)
>>> end = (3, 4)
>>> rr, cc = rectangle_perimeter(start, end=end, shape=img.shape)
>>> img[rr, cc] = 1
>>> img
array([[0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 1],
[0, 0, 1, 0, 0, 1],
[0, 0, 1, 0, 0, 1],
[0, 0, 1, 1, 1, 1]], dtype=uint8)
>>> img = np.zeros((5, 5), dtype=np.uint8)
>>> r, c = rectangle_perimeter(start, (10, 10), shape=img.shape, clip=True)
>>> img[r, c] = 1
>>> img
array([[0, 0, 0, 0, 0],
[0, 0, 1, 1, 1],
[0, 0, 1, 0, 1],
[0, 0, 1, 0, 1],
[0, 0, 1, 1, 1]], dtype=uint8)
"""
top_left, bottom_right = _rectangle_slice(start=start,
end=end,
extent=extent)
top_left -= 1
r = [top_left[0], top_left[0], bottom_right[0], bottom_right[0],
top_left[0]]
c = [top_left[1], bottom_right[1], bottom_right[1], top_left[1],
top_left[1]]
return polygon_perimeter(r, c, shape=shape, clip=clip)
def _rectangle_slice(start, end=None, extent=None):
"""Return the slice ``(top_left, bottom_right)`` of the rectangle.
Returns
=======
(top_left, bottomm_right)
The slice you would need to select the region in the rectangle defined
by the parameters.
Select it like:
``rect[top_left[0]:bottom_right[0], top_left[1]:bottom_right[1]]``
"""
if end is None and extent is None:
raise ValueError("Either `end` or `extent` must be given.")
if end is not None and extent is not None:
raise ValueError("Cannot provide both `end` and `extent`.")
if extent is not None:
end = np.asarray(start) + np.asarray(extent)
top_left = np.minimum(start, end)
bottom_right = np.maximum(start, end)
if extent is None:
bottom_right += 1
return (top_left, bottom_right)

View file

@ -0,0 +1,114 @@
import numpy as np
from scipy.special import (ellipkinc as ellip_F, ellipeinc as ellip_E)
def ellipsoid(a, b, c, spacing=(1., 1., 1.), levelset=False):
"""
Generates ellipsoid with semimajor axes aligned with grid dimensions
on grid with specified `spacing`.
Parameters
----------
a : float
Length of semimajor axis aligned with x-axis.
b : float
Length of semimajor axis aligned with y-axis.
c : float
Length of semimajor axis aligned with z-axis.
spacing : tuple of floats, length 3
Spacing in (x, y, z) spatial dimensions.
levelset : bool
If True, returns the level set for this ellipsoid (signed level
set about zero, with positive denoting interior) as np.float64.
False returns a binarized version of said level set.
Returns
-------
ellip : (N, M, P) array
Ellipsoid centered in a correctly sized array for given `spacing`.
Boolean dtype unless `levelset=True`, in which case a float array is
returned with the level set above 0.0 representing the ellipsoid.
"""
if (a <= 0) or (b <= 0) or (c <= 0):
raise ValueError('Parameters a, b, and c must all be > 0')
offset = np.r_[1, 1, 1] * np.r_[spacing]
# Calculate limits, and ensure output volume is odd & symmetric
low = np.ceil((- np.r_[a, b, c] - offset))
high = np.floor((np.r_[a, b, c] + offset + 1))
for dim in range(3):
if (high[dim] - low[dim]) % 2 == 0:
low[dim] -= 1
num = np.arange(low[dim], high[dim], spacing[dim])
if 0 not in num:
low[dim] -= np.max(num[num < 0])
# Generate (anisotropic) spatial grid
x, y, z = np.mgrid[low[0]:high[0]:spacing[0],
low[1]:high[1]:spacing[1],
low[2]:high[2]:spacing[2]]
if not levelset:
arr = ((x / float(a)) ** 2 +
(y / float(b)) ** 2 +
(z / float(c)) ** 2) <= 1
else:
arr = ((x / float(a)) ** 2 +
(y / float(b)) ** 2 +
(z / float(c)) ** 2) - 1
return arr
def ellipsoid_stats(a, b, c):
"""
Calculates analytical surface area and volume for ellipsoid with
semimajor axes aligned with grid dimensions of specified `spacing`.
Parameters
----------
a : float
Length of semimajor axis aligned with x-axis.
b : float
Length of semimajor axis aligned with y-axis.
c : float
Length of semimajor axis aligned with z-axis.
Returns
-------
vol : float
Calculated volume of ellipsoid.
surf : float
Calculated surface area of ellipsoid.
"""
if (a <= 0) or (b <= 0) or (c <= 0):
raise ValueError('Parameters a, b, and c must all be > 0')
# Calculate volume & surface area
# Surface calculation requires a >= b >= c and a != c.
abc = [a, b, c]
abc.sort(reverse=True)
a = abc[0]
b = abc[1]
c = abc[2]
# Volume
vol = 4 / 3. * np.pi * a * b * c
# Analytical ellipsoid surface area
phi = np.arcsin((1. - (c ** 2 / (a ** 2.))) ** 0.5)
d = float((a ** 2 - c ** 2) ** 0.5)
m = (a ** 2 * (b ** 2 - c ** 2) /
float(b ** 2 * (a ** 2 - c ** 2)))
F = ellip_F(phi, m)
E = ellip_E(phi, m)
surf = 2 * np.pi * (c ** 2 +
b * c ** 2 / d * F +
b * d * E)
return vol, surf

View file

@ -0,0 +1,108 @@
import numpy as np
def _round_safe(coords):
"""Round coords while ensuring successive values are less than 1 apart.
When rounding coordinates for `line_nd`, we want coordinates that are less
than 1 apart (always the case, by design) to remain less than one apart.
However, NumPy rounds values to the nearest *even* integer, so:
>>> np.round([0.5, 1.5, 2.5, 3.5, 4.5])
array([0., 2., 2., 4., 4.])
So, for our application, we detect whether the above case occurs, and use
``np.floor`` if so. It is sufficient to detect that the first coordinate
falls on 0.5 and that the second coordinate is 1.0 apart, since we assume
by construction that the inter-point distance is less than or equal to 1
and that all successive points are equidistant.
Parameters
----------
coords : 1D array of float
The coordinates array. We assume that all successive values are
equidistant (``np.all(np.diff(coords) = coords[1] - coords[0])``)
and that this distance is no more than 1
(``np.abs(coords[1] - coords[0]) <= 1``).
Returns
-------
rounded : 1D array of int
The array correctly rounded for an indexing operation, such that no
successive indices will be more than 1 apart.
Examples
--------
>>> coords0 = np.array([0.5, 1.25, 2., 2.75, 3.5])
>>> _round_safe(coords0)
array([0, 1, 2, 3, 4])
>>> coords1 = np.arange(0.5, 8, 1)
>>> coords1
array([0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5])
>>> _round_safe(coords1)
array([0, 1, 2, 3, 4, 5, 6, 7])
"""
if (len(coords) > 1
and coords[0] % 1 == 0.5
and coords[1] - coords[0] == 1):
_round_function = np.floor
else:
_round_function = np.round
return _round_function(coords).astype(int)
def line_nd(start, stop, *, endpoint=False, integer=True):
"""Draw a single-pixel thick line in n dimensions.
The line produced will be ndim-connected. That is, two subsequent
pixels in the line will be either direct or diagonal neighbours in
n dimensions.
Parameters
----------
start : array-like, shape (N,)
The start coordinates of the line.
stop : array-like, shape (N,)
The end coordinates of the line.
endpoint : bool, optional
Whether to include the endpoint in the returned line. Defaults
to False, which allows for easy drawing of multi-point paths.
integer : bool, optional
Whether to round the coordinates to integer. If True (default),
the returned coordinates can be used to directly index into an
array. `False` could be used for e.g. vector drawing.
Returns
-------
coords : tuple of arrays
The coordinates of points on the line.
Examples
--------
>>> lin = line_nd((1, 1), (5, 2.5), endpoint=False)
>>> lin
(array([1, 2, 3, 4]), array([1, 1, 2, 2]))
>>> im = np.zeros((6, 5), dtype=int)
>>> im[lin] = 1
>>> im
array([[0, 0, 0, 0, 0],
[0, 1, 0, 0, 0],
[0, 1, 0, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 0, 0]])
>>> line_nd([2, 1, 1], [5, 5, 2.5], endpoint=True)
(array([2, 3, 4, 4, 5]), array([1, 2, 3, 4, 5]), array([1, 1, 2, 2, 2]))
"""
start = np.asarray(start)
stop = np.asarray(stop)
npoints = int(np.ceil(np.max(np.abs(stop - start))))
if endpoint:
npoints += 1
coords = []
for dim in range(len(start)):
dimcoords = np.linspace(start[dim], stop[dim], npoints, endpoint)
if integer:
dimcoords = _round_safe(dimcoords).astype(int)
coords.append(dimcoords)
return tuple(coords)

View file

@ -0,0 +1,31 @@
#!/usr/bin/env python
import os
from skimage._build import cython
base_path = os.path.abspath(os.path.dirname(__file__))
def configuration(parent_package='', top_path=None):
from numpy.distutils.misc_util import Configuration, get_numpy_include_dirs
config = Configuration('draw', parent_package, top_path)
cython(['_draw.pyx'], working_path=base_path)
config.add_extension('_draw', sources=['_draw.c'],
include_dirs=[get_numpy_include_dirs(), '../_shared'])
return config
if __name__ == '__main__':
from numpy.distutils.core import setup
setup(maintainer='scikit-image developers',
author='scikit-image developers',
maintainer_email='scikit-image@python.org',
description='Drawing',
url='https://github.com/scikit-image/scikit-image',
license='SciPy License (BSD Style)',
**(configuration(top_path='').todict())
)

View file

@ -0,0 +1,9 @@
from ..._shared.testing import setup_test, teardown_test
def setup():
setup_test()
def teardown():
teardown_test()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,174 @@
import numpy as np
from skimage._shared.testing import assert_array_equal, assert_allclose
from skimage.draw import ellipsoid, ellipsoid_stats, rectangle
from skimage._shared import testing
def test_ellipsoid_sign_parameters1():
with testing.raises(ValueError):
ellipsoid(-1, 2, 2)
def test_ellipsoid_sign_parameters2():
with testing.raises(ValueError):
ellipsoid(0, 2, 2)
def test_ellipsoid_sign_parameters3():
with testing.raises(ValueError):
ellipsoid(-3, -2, 2)
def test_ellipsoid_bool():
test = ellipsoid(2, 2, 2)[1:-1, 1:-1, 1:-1]
test_anisotropic = ellipsoid(2, 2, 4, spacing=(1., 1., 2.))
test_anisotropic = test_anisotropic[1:-1, 1:-1, 1:-1]
expected = np.array([[[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0],
[0, 1, 1, 1, 0],
[0, 1, 1, 1, 0],
[0, 1, 1, 1, 0],
[0, 0, 0, 0, 0]],
[[0, 0, 1, 0, 0],
[0, 1, 1, 1, 0],
[1, 1, 1, 1, 1],
[0, 1, 1, 1, 0],
[0, 0, 1, 0, 0]],
[[0, 0, 0, 0, 0],
[0, 1, 1, 1, 0],
[0, 1, 1, 1, 0],
[0, 1, 1, 1, 0],
[0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]]])
assert_array_equal(test, expected.astype(bool))
assert_array_equal(test_anisotropic, expected.astype(bool))
def test_ellipsoid_levelset():
test = ellipsoid(2, 2, 2, levelset=True)[1:-1, 1:-1, 1:-1]
test_anisotropic = ellipsoid(2, 2, 4, spacing=(1., 1., 2.),
levelset=True)
test_anisotropic = test_anisotropic[1:-1, 1:-1, 1:-1]
expected = np.array([[[ 2. , 1.25, 1. , 1.25, 2. ],
[ 1.25, 0.5 , 0.25, 0.5 , 1.25],
[ 1. , 0.25, 0. , 0.25, 1. ],
[ 1.25, 0.5 , 0.25, 0.5 , 1.25],
[ 2. , 1.25, 1. , 1.25, 2. ]],
[[ 1.25, 0.5 , 0.25, 0.5 , 1.25],
[ 0.5 , -0.25, -0.5 , -0.25, 0.5 ],
[ 0.25, -0.5 , -0.75, -0.5 , 0.25],
[ 0.5 , -0.25, -0.5 , -0.25, 0.5 ],
[ 1.25, 0.5 , 0.25, 0.5 , 1.25]],
[[ 1. , 0.25, 0. , 0.25, 1. ],
[ 0.25, -0.5 , -0.75, -0.5 , 0.25],
[ 0. , -0.75, -1. , -0.75, 0. ],
[ 0.25, -0.5 , -0.75, -0.5 , 0.25],
[ 1. , 0.25, 0. , 0.25, 1. ]],
[[ 1.25, 0.5 , 0.25, 0.5 , 1.25],
[ 0.5 , -0.25, -0.5 , -0.25, 0.5 ],
[ 0.25, -0.5 , -0.75, -0.5 , 0.25],
[ 0.5 , -0.25, -0.5 , -0.25, 0.5 ],
[ 1.25, 0.5 , 0.25, 0.5 , 1.25]],
[[ 2. , 1.25, 1. , 1.25, 2. ],
[ 1.25, 0.5 , 0.25, 0.5 , 1.25],
[ 1. , 0.25, 0. , 0.25, 1. ],
[ 1.25, 0.5 , 0.25, 0.5 , 1.25],
[ 2. , 1.25, 1. , 1.25, 2. ]]])
assert_allclose(test, expected)
assert_allclose(test_anisotropic, expected)
def test_ellipsoid_stats():
# Test comparison values generated by Wolfram Alpha
vol, surf = ellipsoid_stats(6, 10, 16)
assert_allclose(1280 * np.pi, vol, atol=1e-4)
assert_allclose(1383.28, surf, atol=1e-2)
# Test when a <= b <= c does not hold
vol, surf = ellipsoid_stats(16, 6, 10)
assert_allclose(1280 * np.pi, vol, atol=1e-4)
assert_allclose(1383.28, surf, atol=1e-2)
# Larger test to ensure reliability over broad range
vol, surf = ellipsoid_stats(17, 27, 169)
assert_allclose(103428 * np.pi, vol, atol=1e-4)
assert_allclose(37426.3, surf, atol=1e-1)
def test_rect_3d_extent():
expected = np.array([[[0, 0, 1, 1, 1],
[0, 0, 1, 1, 1],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]],
[[0, 0, 1, 1, 1],
[0, 0, 1, 1, 1],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]],
[[0, 0, 1, 1, 1],
[0, 0, 1, 1, 1],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]],
[[0, 0, 1, 1, 1],
[0, 0, 1, 1, 1],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]]], dtype=np.uint8)
img = np.zeros((4, 5, 5), dtype=np.uint8)
start = (0, 0, 2)
extent = (5, 2, 3)
pp, rr, cc = rectangle(start, extent=extent, shape=img.shape)
img[pp, rr, cc] = 1
assert_array_equal(img, expected)
def test_rect_3d_end():
expected = np.array([[[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]],
[[0, 0, 1, 1, 0],
[0, 0, 1, 1, 0],
[0, 0, 1, 1, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]],
[[0, 0, 1, 1, 0],
[0, 0, 1, 1, 0],
[0, 0, 1, 1, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]],
[[0, 0, 1, 1, 0],
[0, 0, 1, 1, 0],
[0, 0, 1, 1, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]]], dtype=np.uint8)
img = np.zeros((4, 5, 5), dtype=np.uint8)
start = (1, 0, 2)
end = (3, 2, 3)
pp, rr, cc = rectangle(start, end=end, shape=img.shape)
img[pp, rr, cc] = 1
assert_array_equal(img, expected)

View file

@ -0,0 +1,18 @@
from skimage.draw import line_nd
from skimage._shared.testing import assert_equal
def test_empty_line():
coords = line_nd((1, 1, 1), (1, 1, 1))
assert len(coords) == 3
assert all(len(c) == 0 for c in coords)
def test_zero_line():
coords = line_nd((-1, -1), (2, 2))
assert_equal(coords, [[-1, 0, 1], [-1, 0, 1]])
def test_no_round():
coords = line_nd((0.5, 0), (2.5, 0), integer=False, endpoint=True)
assert_equal(coords, [[0.5, 1.5, 2.5], [0, 0, 0]])

View file

@ -0,0 +1,14 @@
import numpy as np
from skimage import draw
image_shape = (512, 512)
polygon = np.array([[80, 111, 146, 234, 407, 300, 187, 45],
[465, 438, 499, 380, 450, 287, 210, 167]]).T
def test_polygon2mask():
mask = draw.polygon2mask(image_shape, polygon)
assert mask.shape == image_shape
assert mask.sum() == 57647

View file

@ -0,0 +1,171 @@
import numpy as np
from skimage.draw import random_shapes
from skimage._shared import testing
from skimage._shared._warnings import expected_warnings
def test_generates_color_images_with_correct_shape():
image, _ = random_shapes((128, 128), max_shapes=10)
assert image.shape == (128, 128, 3)
def test_generates_gray_images_with_correct_shape():
image, _ = random_shapes(
(4567, 123), min_shapes=3, max_shapes=20, multichannel=False)
assert image.shape == (4567, 123)
def test_generates_correct_bounding_boxes_for_rectangles():
image, labels = random_shapes(
(128, 128),
max_shapes=1,
shape='rectangle',
random_seed=42)
assert len(labels) == 1
label, bbox = labels[0]
assert label == 'rectangle', label
crop = image[bbox[0][0]:bbox[0][1], bbox[1][0]:bbox[1][1]]
# The crop is filled.
assert (crop >= 0).all() and (crop < 255).all()
# The crop is complete.
image[bbox[0][0]:bbox[0][1], bbox[1][0]:bbox[1][1]] = 255
assert (image == 255).all()
def test_generates_correct_bounding_boxes_for_triangles():
image, labels = random_shapes(
(128, 128),
max_shapes=1,
shape='triangle',
random_seed=42)
assert len(labels) == 1
label, bbox = labels[0]
assert label == 'triangle', label
crop = image[bbox[0][0]:bbox[0][1], bbox[1][0]:bbox[1][1]]
# The crop is filled.
assert (crop >= 0).any() and (crop < 255).any()
# The crop is complete.
image[bbox[0][0]:bbox[0][1], bbox[1][0]:bbox[1][1]] = 255
assert (image == 255).all()
def test_generates_correct_bounding_boxes_for_circles():
image, labels = random_shapes(
(43, 44),
max_shapes=1,
min_size=20,
max_size=20,
shape='circle',
random_seed=42)
assert len(labels) == 1
label, bbox = labels[0]
assert label == 'circle', label
crop = image[bbox[0][0]:bbox[0][1], bbox[1][0]:bbox[1][1]]
# The crop is filled.
assert (crop >= 0).any() and (crop < 255).any()
# The crop is complete.
image[bbox[0][0]:bbox[0][1], bbox[1][0]:bbox[1][1]] = 255
assert (image == 255).all()
def test_generates_correct_bounding_boxes_for_ellipses():
image, labels = random_shapes(
(43, 44),
max_shapes=1,
min_size=20,
max_size=20,
shape='ellipse',
random_seed=42)
assert len(labels) == 1
label, bbox = labels[0]
assert label == 'ellipse', label
crop = image[bbox[0][0]:bbox[0][1], bbox[1][0]:bbox[1][1]]
# The crop is filled.
assert (crop >= 0).any() and (crop < 255).any()
# The crop is complete.
image[bbox[0][0]:bbox[0][1], bbox[1][0]:bbox[1][1]] = 255
assert (image == 255).all()
def test_generate_circle_throws_when_size_too_small():
with testing.raises(ValueError):
random_shapes(
(64, 128), max_shapes=1, min_size=1, max_size=1, shape='circle')
def test_generate_ellipse_throws_when_size_too_small():
with testing.raises(ValueError):
random_shapes(
(64, 128), max_shapes=1, min_size=1, max_size=1, shape='ellipse')
def test_generate_triangle_throws_when_size_too_small():
with testing.raises(ValueError):
random_shapes(
(128, 64), max_shapes=1, min_size=1, max_size=1, shape='triangle')
def test_can_generate_one_by_one_rectangle():
image, labels = random_shapes(
(50, 128),
max_shapes=1,
min_size=1,
max_size=1,
shape='rectangle')
assert len(labels) == 1
_, bbox = labels[0]
crop = image[bbox[0][0]:bbox[0][1], bbox[1][0]:bbox[1][1]]
# rgb
assert (np.shape(crop) == (1, 1, 3) and np.any(crop >= 1)
and np.any(crop < 255))
def test_throws_when_intensity_range_out_of_range():
with testing.raises(ValueError):
random_shapes((1000, 1234), max_shapes=1, multichannel=False,
intensity_range=(0, 256))
with testing.raises(ValueError):
random_shapes((2, 2), max_shapes=1,
intensity_range=((-1, 255),))
def test_returns_empty_labels_and_white_image_when_cannot_fit_shape():
# The circle will never fit this.
with expected_warnings(['Could not fit']):
image, labels = random_shapes(
(10000, 10000), max_shapes=1, min_size=10000, shape='circle')
assert len(labels) == 0
assert (image == 255).all()
def test_random_shapes_is_reproducible_with_seed():
random_seed = 42
labels = []
for _ in range(5):
_, label = random_shapes((128, 128), max_shapes=5,
random_seed=random_seed)
labels.append(label)
assert all(other == labels[0] for other in labels[1:])
def test_generates_white_image_when_intensity_range_255():
image, labels = random_shapes((128, 128), max_shapes=3,
intensity_range=((255, 255),),
random_seed=42)
assert len(labels) > 0
assert (image == 255).all()