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,52 @@
from ._find_contours import find_contours
from ._marching_cubes_lewiner import marching_cubes_lewiner, marching_cubes
from ._marching_cubes_classic import (marching_cubes_classic,
mesh_surface_area)
from ._regionprops import regionprops, perimeter, regionprops_table
from .simple_metrics import compare_mse, compare_nrmse, compare_psnr
from ._structural_similarity import compare_ssim
from ._polygon import approximate_polygon, subdivide_polygon
from .pnpoly import points_in_poly, grid_points_in_poly
from ._moments import (moments, moments_central, moments_coords,
moments_coords_central, moments_normalized, centroid,
moments_hu, inertia_tensor, inertia_tensor_eigvals)
from .profile import profile_line
from .fit import LineModelND, CircleModel, EllipseModel, ransac
from .block import block_reduce
from ._label import label
from .entropy import shannon_entropy
__all__ = ['find_contours',
'regionprops',
'regionprops_table',
'perimeter',
'approximate_polygon',
'subdivide_polygon',
'LineModelND',
'CircleModel',
'EllipseModel',
'ransac',
'block_reduce',
'moments',
'moments_central',
'moments_coords',
'moments_coords_central',
'moments_normalized',
'moments_hu',
'inertia_tensor',
'inertia_tensor_eigvals',
'marching_cubes',
'marching_cubes_lewiner',
'marching_cubes_classic',
'mesh_surface_area',
'profile_line',
'label',
'points_in_poly',
'grid_points_in_poly',
'compare_ssim',
'compare_mse',
'compare_nrmse',
'compare_psnr',
'shannon_entropy',
]

View file

@ -0,0 +1,206 @@
import numpy as np
from ._find_contours_cy import _get_contour_segments
from collections import deque
_param_options = ('high', 'low')
def find_contours(array, level,
fully_connected='low', positive_orientation='low',
*,
mask=None):
"""Find iso-valued contours in a 2D array for a given level value.
Uses the "marching squares" method to compute a the iso-valued contours of
the input 2D array for a particular level value. Array values are linearly
interpolated to provide better precision for the output contours.
Parameters
----------
array : 2D ndarray of double
Input data in which to find contours.
level : float
Value along which to find contours in the array.
fully_connected : str, {'low', 'high'}
Indicates whether array elements below the given level value are to be
considered fully-connected (and hence elements above the value will
only be face connected), or vice-versa. (See notes below for details.)
positive_orientation : either 'low' or 'high'
Indicates whether the output contours will produce positively-oriented
polygons around islands of low- or high-valued elements. If 'low' then
contours will wind counter- clockwise around elements below the
iso-value. Alternately, this means that low-valued elements are always
on the left of the contour. (See below for details.)
mask : 2D ndarray of bool, or None
A boolean mask, True where we want to draw contours.
Note that NaN values are always excluded from the considered region
(``mask`` is set to ``False`` wherever ``array`` is ``NaN``).
Returns
-------
contours : list of (n,2)-ndarrays
Each contour is an ndarray of shape ``(n, 2)``,
consisting of n ``(row, column)`` coordinates along the contour.
Notes
-----
The marching squares algorithm is a special case of the marching cubes
algorithm [1]_. A simple explanation is available here::
http://users.polytech.unice.fr/~lingrand/MarchingCubes/algo.html
There is a single ambiguous case in the marching squares algorithm: when
a given ``2 x 2``-element square has two high-valued and two low-valued
elements, each pair diagonally adjacent. (Where high- and low-valued is
with respect to the contour value sought.) In this case, either the
high-valued elements can be 'connected together' via a thin isthmus that
separates the low-valued elements, or vice-versa. When elements are
connected together across a diagonal, they are considered 'fully
connected' (also known as 'face+vertex-connected' or '8-connected'). Only
high-valued or low-valued elements can be fully-connected, the other set
will be considered as 'face-connected' or '4-connected'. By default,
low-valued elements are considered fully-connected; this can be altered
with the 'fully_connected' parameter.
Output contours are not guaranteed to be closed: contours which intersect
the array edge or a masked-off region (either where mask is False or where
array is NaN) will be left open. All other contours will be closed. (The
closed-ness of a contours can be tested by checking whether the beginning
point is the same as the end point.)
Contours are oriented. By default, array values lower than the contour
value are to the left of the contour and values greater than the contour
value are to the right. This means that contours will wind
counter-clockwise (i.e. in 'positive orientation') around islands of
low-valued pixels. This behavior can be altered with the
'positive_orientation' parameter.
The order of the contours in the output list is determined by the position
of the smallest ``x,y`` (in lexicographical order) coordinate in the
contour. This is a side-effect of how the input array is traversed, but
can be relied upon.
.. warning::
Array coordinates/values are assumed to refer to the *center* of the
array element. Take a simple example input: ``[0, 1]``. The interpolated
position of 0.5 in this array is midway between the 0-element (at
``x=0``) and the 1-element (at ``x=1``), and thus would fall at
``x=0.5``.
This means that to find reasonable contours, it is best to find contours
midway between the expected "light" and "dark" values. In particular,
given a binarized array, *do not* choose to find contours at the low or
high value of the array. This will often yield degenerate contours,
especially around structures that are a single array element wide. Instead
choose a middle value, as above.
References
----------
.. [1] Lorensen, William and Harvey E. Cline. Marching Cubes: A High
Resolution 3D Surface Construction Algorithm. Computer Graphics
(SIGGRAPH 87 Proceedings) 21(4) July 1987, p. 163-170).
Examples
--------
>>> a = np.zeros((3, 3))
>>> a[0, 0] = 1
>>> a
array([[1., 0., 0.],
[0., 0., 0.],
[0., 0., 0.]])
>>> find_contours(a, 0.5)
[array([[0. , 0.5],
[0.5, 0. ]])]
"""
if fully_connected not in _param_options:
raise ValueError('Parameters "fully_connected" must be either '
'"high" or "low".')
if positive_orientation not in _param_options:
raise ValueError('Parameters "positive_orientation" must be either '
'"high" or "low".')
if array.shape[0] < 2 or array.shape[1] < 2:
raise ValueError("Input array must be at least 2x2.")
if array.ndim != 2:
raise ValueError('Only 2D arrays are supported.')
if mask is not None:
if mask.shape != array.shape:
raise ValueError('Parameters "array" and "mask"'
' must have same shape.')
if not np.can_cast(mask.dtype, bool, casting='safe'):
raise TypeError('Parameter "mask" must be a binary array.')
mask = mask.astype(np.uint8, copy=False)
segments = _get_contour_segments(array.astype(np.double), float(level),
fully_connected == 'high', mask=mask)
contours = _assemble_contours(segments)
if positive_orientation == 'high':
contours = [c[::-1] for c in contours]
return contours
def _assemble_contours(segments):
current_index = 0
contours = {}
starts = {}
ends = {}
for from_point, to_point in segments:
# Ignore degenerate segments.
# This happens when (and only when) one vertex of the square is
# exactly the contour level, and the rest are above or below.
# This degenerate vertex will be picked up later by neighboring
# squares.
if from_point == to_point:
continue
tail, tail_num = starts.pop(to_point, (None, None))
head, head_num = ends.pop(from_point, (None, None))
if tail is not None and head is not None:
# We need to connect these two contours.
if tail is head:
# We need to closed a contour.
# Add the end point
head.append(to_point)
else: # tail is not head
# We need to join two distinct contours.
# We want to keep the first contour segment created, so that
# the final contours are ordered left->right, top->bottom.
if tail_num > head_num:
# tail was created second. Append tail to head.
head.extend(tail)
# remove all traces of tail:
ends.pop(tail[-1])
contours.pop(tail_num, None)
# Update contour starts end ends
starts[head[0]] = (head, head_num)
ends[head[-1]] = (head, head_num)
else: # tail_num <= head_num
# head was created second. Prepend head to tail.
tail.extendleft(reversed(head))
# remove all traces of head:
starts.pop(head[0])
contours.pop(head_num, None)
# Update contour starts end ends
starts[tail[0]] = (tail, tail_num)
ends[tail[-1]] = (tail, tail_num)
elif tail is None and head is None:
# we need to add a new contour
new_contour = deque((from_point, to_point))
contours[current_index] = new_contour
starts[from_point] = (new_contour, current_index)
ends[to_point] = (new_contour, current_index)
current_index += 1
elif head is None: # tail is not None
# We've found a single contour to which the new segment should be
# prepended.
tail.appendleft(from_point)
starts[from_point] = (tail, tail_num)
else: # tail is None and head is not None:
# We've found a single contour to which the new segment should be
# appended
head.append(to_point)
ends[to_point] = (head, head_num)
return [np.array(contour) for _, contour in sorted(contours.items())]

View file

@ -0,0 +1,93 @@
from ._ccomp import label_cython as clabel
def label(input, neighbors=None, background=None, return_num=False,
connectivity=None):
r"""Label connected regions of an integer array.
Two pixels are connected when they are neighbors and have the same value.
In 2D, they can be neighbors either in a 1- or 2-connected sense.
The value refers to the maximum number of orthogonal hops to consider a
pixel/voxel a neighbor::
1-connectivity 2-connectivity diagonal connection close-up
[ ] [ ] [ ] [ ] [ ]
| \ | / | <- hop 2
[ ]--[x]--[ ] [ ]--[x]--[ ] [x]--[ ]
| / | \ hop 1
[ ] [ ] [ ] [ ]
Parameters
----------
input : ndarray of dtype int
Image to label.
neighbors : {4, 8}, int, optional
Whether to use 4- or 8-"connectivity".
In 3D, 4-"connectivity" means connected pixels have to share face,
whereas with 8-"connectivity", they have to share only edge or vertex.
**Deprecated, use** ``connectivity`` **instead.**
background : int, optional
Consider all pixels with this value as background pixels, and label
them as 0. By default, 0-valued pixels are considered as background
pixels.
return_num : bool, optional
Whether to return the number of assigned labels.
connectivity : int, optional
Maximum number of orthogonal hops to consider a pixel/voxel
as a neighbor.
Accepted values are ranging from 1 to input.ndim. If ``None``, a full
connectivity of ``input.ndim`` is used.
Returns
-------
labels : ndarray of dtype int
Labeled array, where all connected regions are assigned the
same integer value.
num : int, optional
Number of labels, which equals the maximum label index and is only
returned if return_num is `True`.
See Also
--------
regionprops
References
----------
.. [1] Christophe Fiorio and Jens Gustedt, "Two linear time Union-Find
strategies for image processing", Theoretical Computer Science
154 (1996), pp. 165-181.
.. [2] Kensheng Wu, Ekow Otoo and Arie Shoshani, "Optimizing connected
component labeling algorithms", Paper LBNL-56864, 2005,
Lawrence Berkeley National Laboratory (University of California),
http://repositories.cdlib.org/lbnl/LBNL-56864
Examples
--------
>>> import numpy as np
>>> x = np.eye(3).astype(int)
>>> print(x)
[[1 0 0]
[0 1 0]
[0 0 1]]
>>> print(label(x, connectivity=1))
[[1 0 0]
[0 2 0]
[0 0 3]]
>>> print(label(x, connectivity=2))
[[1 0 0]
[0 1 0]
[0 0 1]]
>>> print(label(x, background=-1))
[[1 2 2]
[2 1 2]
[2 2 1]]
>>> x = np.array([[1, 0, 0],
... [1, 1, 5],
... [0, 0, 0]])
>>> print(label(x))
[[1 0 0]
[1 1 2]
[0 0 0]]
"""
return clabel(input, neighbors, background, return_num, connectivity)

View file

@ -0,0 +1,301 @@
import warnings
import numpy as np
import scipy.ndimage as ndi
from . import _marching_cubes_classic_cy
def marching_cubes_classic(volume, level=None, spacing=(1., 1., 1.),
gradient_direction='descent'):
"""
Classic marching cubes algorithm to find surfaces in 3d volumetric data.
Note that the ``marching_cubes()`` algorithm is recommended over
this algorithm, because it's faster and produces better results.
Parameters
----------
volume : (M, N, P) array of doubles
Input data volume to find isosurfaces. Will be cast to `np.float64`.
level : float
Contour value to search for isosurfaces in `volume`. If not
given or None, the average of the min and max of vol is used.
spacing : length-3 tuple of floats
Voxel spacing in spatial dimensions corresponding to numpy array
indexing dimensions (M, N, P) as in `volume`.
gradient_direction : string
Controls if the mesh was generated from an isosurface with gradient
descent toward objects of interest (the default), or the opposite.
The two options are:
* descent : Object was greater than exterior
* ascent : Exterior was greater than object
Returns
-------
verts : (V, 3) array
Spatial coordinates for V unique mesh vertices. Coordinate order
matches input `volume` (M, N, P).
faces : (F, 3) array
Define triangular faces via referencing vertex indices from ``verts``.
This algorithm specifically outputs triangles, so each face has
exactly three indices.
Notes
-----
The marching cubes algorithm is implemented as described in [1]_.
A simple explanation is available here::
http://users.polytech.unice.fr/~lingrand/MarchingCubes/algo.html
There are several known ambiguous cases in the marching cubes algorithm.
Using point labeling as in [1]_, Figure 4, as shown::
v8 ------ v7
/ | / | y
/ | / | ^ z
v4 ------ v3 | | /
| v5 ----|- v6 |/ (note: NOT right handed!)
| / | / ----> x
| / | /
v1 ------ v2
Most notably, if v4, v8, v2, and v6 are all >= `level` (or any
generalization of this case) two parallel planes are generated by this
algorithm, separating v4 and v8 from v2 and v6. An equally valid
interpretation would be a single connected thin surface enclosing all
four points. This is the best known ambiguity, though there are others.
This algorithm does not attempt to resolve such ambiguities; it is a naive
implementation of marching cubes as in [1]_, but may be a good beginning
for work with more recent techniques (Dual Marching Cubes, Extended
Marching Cubes, Cubic Marching Squares, etc.).
Because of interactions between neighboring cubes, the isosurface(s)
generated by this algorithm are NOT guaranteed to be closed, particularly
for complicated contours. Furthermore, this algorithm does not guarantee
a single contour will be returned. Indeed, ALL isosurfaces which cross
`level` will be found, regardless of connectivity.
The output is a triangular mesh consisting of a set of unique vertices and
connecting triangles. The order of these vertices and triangles in the
output list is determined by the position of the smallest ``x,y,z`` (in
lexicographical order) coordinate in the contour. This is a side-effect
of how the input array is traversed, but can be relied upon.
The generated mesh guarantees coherent orientation as of version 0.12.
To quantify the area of an isosurface generated by this algorithm, pass
outputs directly into `skimage.measure.mesh_surface_area`.
References
----------
.. [1] Lorensen, William and Harvey E. Cline. Marching Cubes: A High
Resolution 3D Surface Construction Algorithm. Computer Graphics
(SIGGRAPH 87 Proceedings) 21(4) July 1987, p. 163-170).
:DOI:`10.1145/37401.37422`
See Also
--------
skimage.measure.marching_cubes
skimage.measure.mesh_surface_area
"""
# Deprecate the function in favor of marching_cubes
warnings.warn("marching_cubes_classic is deprecated in favor of "
"marching_cubes with `method='_lorensen'` "
"to apply Lorensen et al. algorithm. "
"marching_cubes_classic will be removed in version 0.19",
FutureWarning, stacklevel=2)
return _marching_cubes_classic(volume, level, spacing, gradient_direction)
def _marching_cubes_classic(volume, level, spacing, gradient_direction):
"""Lorensen et al. algorithm for marching cubes. See
marching_cubes_classic for documentation.
"""
# Check inputs and ensure `volume` is C-contiguous for memoryviews
if volume.ndim != 3:
raise ValueError("Input volume must have 3 dimensions.")
if level is None:
level = 0.5 * (volume.min() + volume.max())
else:
level = float(level)
if level < volume.min() or level > volume.max():
raise ValueError("Surface level must be within volume data range.")
if len(spacing) != 3:
raise ValueError("`spacing` must consist of three floats.")
volume = np.array(volume, dtype=np.float64, order="C")
# Extract raw triangles using marching cubes in Cython
# Returns a list of length-3 lists, each sub-list containing three
# tuples. The tuples hold (x, y, z) coordinates for triangle vertices.
# Note: this algorithm is fast, but returns degenerate "triangles" which
# have repeated vertices - and equivalent vertices are redundantly
# placed in every triangle they connect with.
raw_faces = _marching_cubes_classic_cy.iterate_and_store_3d(volume,
float(level))
# Find and collect unique vertices, storing triangle verts as indices.
# Returns a true mesh with no degenerate faces.
verts, faces = _marching_cubes_classic_cy.unpack_unique_verts(raw_faces)
verts = np.asarray(verts)
faces = np.asarray(faces)
# Fancy indexing to define two vector arrays from triangle vertices
faces = _correct_mesh_orientation(volume, verts[faces], faces, spacing,
gradient_direction)
# Adjust for non-isotropic spacing in `verts` at time of return
return verts * np.r_[spacing], faces
def mesh_surface_area(verts, faces):
"""
Compute surface area, given vertices & triangular faces
Parameters
----------
verts : (V, 3) array of floats
Array containing (x, y, z) coordinates for V unique mesh vertices.
faces : (F, 3) array of ints
List of length-3 lists of integers, referencing vertex coordinates as
provided in `verts`
Returns
-------
area : float
Surface area of mesh. Units now [coordinate units] ** 2.
Notes
-----
The arguments expected by this function are the first two outputs from
`skimage.measure.marching_cubes`. For unit correct output, ensure correct
`spacing` was passed to `skimage.measure.marching_cubes`.
This algorithm works properly only if the ``faces`` provided are all
triangles.
See Also
--------
skimage.measure.marching_cubes
skimage.measure.marching_cubes_classic
"""
# Fancy indexing to define two vector arrays from triangle vertices
actual_verts = verts[faces]
a = actual_verts[:, 0, :] - actual_verts[:, 1, :]
b = actual_verts[:, 0, :] - actual_verts[:, 2, :]
del actual_verts
# Area of triangle in 3D = 1/2 * Euclidean norm of cross product
return ((np.cross(a, b) ** 2).sum(axis=1) ** 0.5).sum() / 2.
def _correct_mesh_orientation(volume, actual_verts, faces,
spacing=(1., 1., 1.),
gradient_direction='descent'):
"""
Correct orientations of mesh faces.
Parameters
----------
volume : (M, N, P) array of doubles
Input data volume to find isosurfaces. Will be cast to `np.float64`.
actual_verts : (F, 3, 3) array of floats
Array with (face, vertex, coords) index coordinates.
faces : (F, 3) array of ints
List of length-3 lists of integers, referencing vertex coordinates as
provided in `verts`.
spacing : length-3 tuple of floats
Voxel spacing in spatial dimensions corresponding to numpy array
indexing dimensions (M, N, P) as in `volume`.
gradient_direction : string
Controls if the mesh was generated from an isosurface with gradient
descent toward objects of interest (the default), or the opposite.
The two options are:
* descent : Object was greater than exterior
* ascent : Exterior was greater than object
Returns
-------
faces_corrected (F, 3) array of ints
Corrected list of faces referencing vertex coordinates in `verts`.
Notes
-----
Certain applications and mesh processing algorithms require all faces
to be oriented in a consistent way. Generally, this means a normal vector
points "out" of the meshed shapes. This algorithm corrects the output from
`skimage.measure.marching_cubes_classic` by flipping the orientation of
mis-oriented faces.
Because marching cubes could be used to find isosurfaces either on
gradient descent (where the desired object has greater values than the
exterior) or ascent (where the desired object has lower values than the
exterior), the ``gradient_direction`` kwarg allows the user to inform this
algorithm which is correct. If the resulting mesh appears to be oriented
completely incorrectly, try changing this option.
The arguments expected by this function are the exact outputs from
`skimage.measure.marching_cubes_classic` except `actual_verts`, which is an
uncorrected version of the fancy indexing operation `verts[faces]`.
Only `faces` is corrected and returned as the vertices do not change,
only the order in which they are referenced.
This algorithm assumes ``faces`` provided are exclusively triangles.
See Also
--------
skimage.measure.marching_cubes_classic
skimage.measure.mesh_surface_area
"""
# Calculate gradient of `volume`, then interpolate to vertices in `verts`
grad_x, grad_y, grad_z = np.gradient(volume)
a = actual_verts[:, 0, :] - actual_verts[:, 1, :]
b = actual_verts[:, 0, :] - actual_verts[:, 2, :]
# Find triangle centroids
centroids = (actual_verts.sum(axis=1) / 3.).T
del actual_verts
# Interpolate face centroids into each gradient axis
grad_centroids_x = ndi.map_coordinates(grad_x, centroids)
grad_centroids_y = ndi.map_coordinates(grad_y, centroids)
grad_centroids_z = ndi.map_coordinates(grad_z, centroids)
# Combine and normalize interpolated gradients
grad_centroids = np.c_[grad_centroids_x, grad_centroids_y,
grad_centroids_z]
grad_centroids = (grad_centroids /
(np.sum(grad_centroids ** 2,
axis=1) ** 0.5)[:, np.newaxis])
# Find normal vectors for each face via cross product
crosses = np.cross(a, b)
crosses = crosses / (np.sum(crosses ** 2, axis=1) ** (0.5))[:, np.newaxis]
# Take dot product
dotproducts = (grad_centroids * crosses).sum(axis=1)
# Find mis-oriented faces
if 'descent' in gradient_direction:
# Faces with incorrect orientations have dot product < 0
indices = (dotproducts < 0).nonzero()[0]
elif 'ascent' in gradient_direction:
# Faces with incorrection orientation have dot product > 0
indices = (dotproducts > 0).nonzero()[0]
else:
raise ValueError("Incorrect input %s in `gradient_direction`, see "
"docstring." % (gradient_direction))
# Correct orientation and return, without modifying original data
faces_corrected = faces.copy()
faces_corrected[indices] = faces_corrected[indices, ::-1]
return faces_corrected

View file

@ -0,0 +1,388 @@
import warnings
import base64
import numpy as np
from . import _marching_cubes_lewiner_luts as mcluts
from . import _marching_cubes_lewiner_cy
from ._marching_cubes_classic import _marching_cubes_classic
def marching_cubes(volume, level=None, *, spacing=(1., 1., 1.),
gradient_direction='descent', step_size=1,
allow_degenerate=True, method='lewiner', mask=None):
"""Marching cubes algorithm to find surfaces in 3d volumetric data.
In contrast with Lorensen et al. approach [2], Lewiner et
al. algorithm is faster, resolves ambiguities, and guarantees
topologically correct results. Therefore, this algorithm generally
a better choice.
Parameters
----------
volume : (M, N, P) array
Input data volume to find isosurfaces. Will internally be
converted to float32 if necessary.
level : float
Contour value to search for isosurfaces in `volume`. If not
given or None, the average of the min and max of vol is used.
spacing : length-3 tuple of floats
Voxel spacing in spatial dimensions corresponding to numpy array
indexing dimensions (M, N, P) as in `volume`.
gradient_direction : string
Controls if the mesh was generated from an isosurface with gradient
descent toward objects of interest (the default), or the opposite,
considering the *left-hand* rule.
The two options are:
* descent : Object was greater than exterior
* ascent : Exterior was greater than object
step_size : int
Step size in voxels. Default 1. Larger steps yield faster but
coarser results. The result will always be topologically correct
though.
allow_degenerate : bool
Whether to allow degenerate (i.e. zero-area) triangles in the
end-result. Default True. If False, degenerate triangles are
removed, at the cost of making the algorithm slower.
method: str
One of 'lewiner', 'lorensen' or '_lorensen'. Specify witch of
Lewiner et al. or Lorensen et al. method will be used. The
'_lorensen' flag correspond to an old implementation that will
be deprecated in version 0.19.
mask : (M, N, P) array
Boolean array. The marching cube algorithm will be computed only on
True elements. This will save computational time when interfaces
are located within certain region of the volume M, N, P-e.g. the top
half of the cube-and also allow to compute finite surfaces-i.e. open
surfaces that do not end at the border of the cube.
Returns
-------
verts : (V, 3) array
Spatial coordinates for V unique mesh vertices. Coordinate order
matches input `volume` (M, N, P).
faces : (F, 3) array
Define triangular faces via referencing vertex indices from ``verts``.
This algorithm specifically outputs triangles, so each face has
exactly three indices.
normals : (V, 3) array
The normal direction at each vertex, as calculated from the
data.
values : (V, ) array
Gives a measure for the maximum value of the data in the local region
near each vertex. This can be used by visualization tools to apply
a colormap to the mesh.
Notes
-----
The algorithm [1] is an improved version of Chernyaev's Marching
Cubes 33 algorithm. It is an efficient algorithm that relies on
heavy use of lookup tables to handle the many different cases,
keeping the algorithm relatively easy. This implementation is
written in Cython, ported from Lewiner's C++ implementation.
To quantify the area of an isosurface generated by this algorithm, pass
verts and faces to `skimage.measure.mesh_surface_area`.
Regarding visualization of algorithm output, to contour a volume
named `myvolume` about the level 0.0, using the ``mayavi`` package::
>>>
>> from mayavi import mlab
>> verts, faces, _, _ = marching_cubes(myvolume, 0.0)
>> mlab.triangular_mesh([vert[0] for vert in verts],
[vert[1] for vert in verts],
[vert[2] for vert in verts],
faces)
>> mlab.show()
Similarly using the ``visvis`` package::
>>>
>> import visvis as vv
>> verts, faces, normals, values = marching_cubes(myvolume, 0.0)
>> vv.mesh(np.fliplr(verts), faces, normals, values)
>> vv.use().Run()
References
----------
.. [1] Thomas Lewiner, Helio Lopes, Antonio Wilson Vieira and Geovan
Tavares. Efficient implementation of Marching Cubes' cases with
topological guarantees. Journal of Graphics Tools 8(2)
pp. 1-15 (december 2003).
:DOI:`10.1080/10867651.2003.10487582`
.. [2] Lorensen, William and Harvey E. Cline. Marching Cubes: A High
Resolution 3D Surface Construction Algorithm. Computer Graphics
(SIGGRAPH 87 Proceedings) 21(4) July 1987, p. 163-170).
:DOI:`10.1145/37401.37422`
See Also
--------
skimage.measure.mesh_surface_area
"""
if method == 'lewiner':
return _marching_cubes_lewiner(volume, level, spacing,
gradient_direction, step_size,
allow_degenerate, use_classic=False, mask=mask)
elif method == 'lorensen':
return _marching_cubes_lewiner(volume, level, spacing,
gradient_direction, step_size,
allow_degenerate, use_classic=True, mask=mask)
elif method == '_lorensen':
if mask is not None:
raise NotImplementedError(
'Parameter `mask` is not implemented for method "_lorensen" '
'and will be ignored.'
)
return _marching_cubes_classic(volume, level, spacing,
gradient_direction)
else:
raise ValueError("method should be one of 'lewiner', 'lorensen' or "
"'_lorensen'.")
def marching_cubes_lewiner(volume, level=None, spacing=(1., 1., 1.),
gradient_direction='descent', step_size=1,
allow_degenerate=True, use_classic=False, mask=None):
"""
Lewiner marching cubes algorithm to find surfaces in 3d volumetric data.
In contrast to ``marching_cubes_classic()``, this algorithm is faster,
resolves ambiguities, and guarantees topologically correct results.
Therefore, this algorithm generally a better choice, unless there
is a specific need for the classic algorithm.
Parameters
----------
volume : (M, N, P) array
Input data volume to find isosurfaces. Will internally be
converted to float32 if necessary.
level : float
Contour value to search for isosurfaces in `volume`. If not
given or None, the average of the min and max of vol is used.
spacing : length-3 tuple of floats
Voxel spacing in spatial dimensions corresponding to numpy array
indexing dimensions (M, N, P) as in `volume`.
gradient_direction : string
Controls if the mesh was generated from an isosurface with gradient
descent toward objects of interest (the default), or the opposite,
considering the *left-hand* rule.
The two options are:
* descent : Object was greater than exterior
* ascent : Exterior was greater than object
step_size : int
Step size in voxels. Default 1. Larger steps yield faster but
coarser results. The result will always be topologically correct
though.
allow_degenerate : bool
Whether to allow degenerate (i.e. zero-area) triangles in the
end-result. Default True. If False, degenerate triangles are
removed, at the cost of making the algorithm slower.
use_classic : bool
If given and True, the classic marching cubes by Lorensen (1987)
is used. This option is included for reference purposes. Note
that this algorithm has ambiguities and is not guaranteed to
produce a topologically correct result. The results with using
this option are *not* generally the same as the
``marching_cubes_classic()`` function.
mask : (M, N, P) array
Boolean array. The marching cube algorithm will be computed only on
True elements. This will save computational time when interfaces
are located within certain region of the volume M, N, P-e.g. the top
half of the cube-and also allow to compute finite surfaces-i.e. open
surfaces that do not end at the border of the cube.
Returns
-------
verts : (V, 3) array
Spatial coordinates for V unique mesh vertices. Coordinate order
matches input `volume` (M, N, P).
faces : (F, 3) array
Define triangular faces via referencing vertex indices from ``verts``.
This algorithm specifically outputs triangles, so each face has
exactly three indices.
normals : (V, 3) array
The normal direction at each vertex, as calculated from the
data.
values : (V, ) array
Gives a measure for the maximum value of the data in the local region
near each vertex. This can be used by visualization tools to apply
a colormap to the mesh.
Notes
-----
The algorithm [1] is an improved version of Chernyaev's Marching
Cubes 33 algorithm. It is an efficient algorithm that relies on
heavy use of lookup tables to handle the many different cases,
keeping the algorithm relatively easy. This implementation is
written in Cython, ported from Lewiner's C++ implementation.
To quantify the area of an isosurface generated by this algorithm, pass
verts and faces to `skimage.measure.mesh_surface_area`.
Regarding visualization of algorithm output, to contour a volume
named `myvolume` about the level 0.0, using the ``mayavi`` package::
>>> from mayavi import mlab # doctest: +SKIP
>>> verts, faces, normals, values = marching_cubes_lewiner(myvolume, 0.0) # doctest: +SKIP
>>> mlab.triangular_mesh([vert[0] for vert in verts],
... [vert[1] for vert in verts],
... [vert[2] for vert in verts],
... faces) # doctest: +SKIP
>>> mlab.show() # doctest: +SKIP
Similarly using the ``visvis`` package::
>>> import visvis as vv # doctest: +SKIP
>>> verts, faces, normals, values = marching_cubes_lewiner(myvolume, 0.0) # doctest: +SKIP
>>> vv.mesh(np.fliplr(verts), faces, normals, values) # doctest: +SKIP
>>> vv.use().Run() # doctest: +SKIP
References
----------
.. [1] Thomas Lewiner, Helio Lopes, Antonio Wilson Vieira and Geovan
Tavares. Efficient implementation of Marching Cubes' cases with
topological guarantees. Journal of Graphics Tools 8(2)
pp. 1-15 (december 2003).
:DOI:`10.1080/10867651.2003.10487582`
See Also
--------
skimage.measure.marching_cubes
skimage.measure.mesh_surface_area
"""
# Deprecate the function in favor of marching_cubes
warnings.warn("marching_cubes_lewiner is deprecated in favor of "
"marching_cubes. marching_cubes_lewiner will "
"be removed in version 0.19",
FutureWarning, stacklevel=2)
return _marching_cubes_lewiner(volume, level, spacing, gradient_direction,
step_size, allow_degenerate, use_classic, mask)
def _marching_cubes_lewiner(volume, level, spacing, gradient_direction,
step_size, allow_degenerate, use_classic, mask):
"""Lewiner et al. algorithm for marching cubes. See
marching_cubes_lewiner for documentation.
"""
# Check volume and ensure its in the format that the alg needs
if not isinstance(volume, np.ndarray) or (volume.ndim != 3):
raise ValueError('Input volume should be a 3D numpy array.')
if volume.shape[0] < 2 or volume.shape[1] < 2 or volume.shape[2] < 2:
raise ValueError("Input array must be at least 2x2x2.")
volume = np.ascontiguousarray(volume,
np.float32) # no copy if not necessary
# Check/convert other inputs:
# level
if level is None:
level = 0.5 * (volume.min() + volume.max())
else:
level = float(level)
if level < volume.min() or level > volume.max():
raise ValueError("Surface level must be within volume data range.")
# spacing
if len(spacing) != 3:
raise ValueError("`spacing` must consist of three floats.")
# step_size
step_size = int(step_size)
if step_size < 1:
raise ValueError('step_size must be at least one.')
# use_classic
use_classic = bool(use_classic)
# Get LutProvider class (reuse if possible)
L = _get_mc_luts()
# Check if a mask array is passed
if mask is not None:
if not mask.shape == volume.shape:
raise ValueError('volume and mask must have the same shape.')
# Apply algorithm
func = _marching_cubes_lewiner_cy.marching_cubes
vertices, faces, normals, values = func(volume, level, L,
step_size, use_classic, mask)
if not len(vertices):
raise RuntimeError('No surface found at the given iso value.')
# Output in z-y-x order, as is common in skimage
vertices = np.fliplr(vertices)
normals = np.fliplr(normals)
# Finishing touches to output
faces.shape = -1, 3
if gradient_direction == 'descent':
# MC implementation is right-handed, but gradient_direction is
# left-handed
faces = np.fliplr(faces)
elif not gradient_direction == 'ascent':
raise ValueError("Incorrect input %s in `gradient_direction`, see "
"docstring." % (gradient_direction))
if not np.array_equal(spacing, (1, 1, 1)):
vertices = vertices * np.r_[spacing]
if allow_degenerate:
return vertices, faces, normals, values
else:
fun = _marching_cubes_lewiner_cy.remove_degenerate_faces
return fun(vertices.astype(np.float32), faces, normals, values)
def _to_array(args):
shape, text = args
byts = base64.decodebytes(text.encode('utf-8'))
ar = np.frombuffer(byts, dtype='int8')
ar.shape = shape
return ar
# Map an edge-index to two relative pixel positions. The ege index
# represents a point that lies somewhere in between these pixels.
# Linear interpolation should be used to determine where it is exactly.
# 0
# 3 1 -> 0x
# 2 xx
EDGETORELATIVEPOSX = np.array([ [0,1],[1,1],[1,0],[0,0], [0,1],[1,1],[1,0],[0,0], [0,0],[1,1],[1,1],[0,0] ], 'int8')
EDGETORELATIVEPOSY = np.array([ [0,0],[0,1],[1,1],[1,0], [0,0],[0,1],[1,1],[1,0], [0,0],[0,0],[1,1],[1,1] ], 'int8')
EDGETORELATIVEPOSZ = np.array([ [0,0],[0,0],[0,0],[0,0], [1,1],[1,1],[1,1],[1,1], [0,1],[0,1],[0,1],[0,1] ], 'int8')
def _get_mc_luts():
""" Kind of lazy obtaining of the luts.
"""
if not hasattr(mcluts, 'THE_LUTS'):
mcluts.THE_LUTS = _marching_cubes_lewiner_cy.LutProvider(
EDGETORELATIVEPOSX, EDGETORELATIVEPOSY, EDGETORELATIVEPOSZ,
_to_array(mcluts.CASESCLASSIC), _to_array(mcluts.CASES),
_to_array(mcluts.TILING1), _to_array(mcluts.TILING2), _to_array(mcluts.TILING3_1), _to_array(mcluts.TILING3_2),
_to_array(mcluts.TILING4_1), _to_array(mcluts.TILING4_2), _to_array(mcluts.TILING5), _to_array(mcluts.TILING6_1_1),
_to_array(mcluts.TILING6_1_2), _to_array(mcluts.TILING6_2), _to_array(mcluts.TILING7_1),
_to_array(mcluts.TILING7_2), _to_array(mcluts.TILING7_3), _to_array(mcluts.TILING7_4_1),
_to_array(mcluts.TILING7_4_2), _to_array(mcluts.TILING8), _to_array(mcluts.TILING9),
_to_array(mcluts.TILING10_1_1), _to_array(mcluts.TILING10_1_1_), _to_array(mcluts.TILING10_1_2),
_to_array(mcluts.TILING10_2), _to_array(mcluts.TILING10_2_), _to_array(mcluts.TILING11),
_to_array(mcluts.TILING12_1_1), _to_array(mcluts.TILING12_1_1_), _to_array(mcluts.TILING12_1_2),
_to_array(mcluts.TILING12_2), _to_array(mcluts.TILING12_2_), _to_array(mcluts.TILING13_1),
_to_array(mcluts.TILING13_1_), _to_array(mcluts.TILING13_2), _to_array(mcluts.TILING13_2_),
_to_array(mcluts.TILING13_3), _to_array(mcluts.TILING13_3_), _to_array(mcluts.TILING13_4),
_to_array(mcluts.TILING13_5_1), _to_array(mcluts.TILING13_5_2), _to_array(mcluts.TILING14),
_to_array(mcluts.TEST3), _to_array(mcluts.TEST4), _to_array(mcluts.TEST6),
_to_array(mcluts.TEST7), _to_array(mcluts.TEST10), _to_array(mcluts.TEST12),
_to_array(mcluts.TEST13), _to_array(mcluts.SUBCONFIG13),
)
return mcluts.THE_LUTS

View file

@ -0,0 +1,527 @@
# -*- coding: utf-8 -*-
# This file was auto-generated from `mc_meta/LookUpTable.h` by
# `mc_meta/createluts.py`.
#static const char casesClassic[256][16]
CASESCLASSIC = (256, 16), """
/////////////////////wAIA/////////////////8AAQn/////////////////AQgDCQgB////
/////////wECCv////////////////8ACAMBAgr/////////////CQIKAAIJ/////////////wII
AwIKCAoJCP////////8DCwL/////////////////AAsCCAsA/////////////wEJAAIDC///////
//////8BCwIBCQsJCAv/////////AwoBCwoD/////////////wAKAQAICggLCv////////8DCQAD
CwkLCgn/////////CQgKCggL/////////////wQHCP////////////////8EAwAHAwT/////////
////AAEJCAQH/////////////wQBCQQHAQcDAf////////8BAgoIBAf/////////////AwQHAwAE
AQIK/////////wkCCgkAAggEB/////////8CCgkCCQcCBwMHCQT/////CAQHAwsC////////////
/wsEBwsCBAIABP////////8JAAEIBAcCAwv/////////BAcLCQQLCQsCCQIB/////wMKAQMLCgcI
BP////////8BCwoBBAsBAAQHCwT/////BAcICQALCQsKCwAD/////wQHCwQLCQkLCv////////8J
BQT/////////////////CQUEAAgD/////////////wAFBAEFAP////////////8IBQQIAwUDAQX/
////////AQIKCQUE/////////////wMACAECCgQJBf////////8FAgoFBAIEAAL/////////AgoF
AwIFAwUEAwQI/////wkFBAIDC/////////////8ACwIACAsECQX/////////AAUEAAEFAgML////
/////wIBBQIFCAIICwQIBf////8KAwsKAQMJBQT/////////BAkFAAgBCAoBCAsK/////wUEAAUA
CwULCgsAA/////8FBAgFCAoKCAv/////////CQcIBQcJ/////////////wkDAAkFAwUHA///////
//8ABwgAAQcBBQf/////////AQUDAwUH/////////////wkHCAkFBwoBAv////////8KAQIJBQAF
AwAFBwP/////CAACCAIFCAUHCgUC/////wIKBQIFAwMFB/////////8HCQUHCAkDCwL/////////
CQUHCQcCCQIAAgcL/////wIDCwABCAEHCAEFB/////8LAgELAQcHAQX/////////CQUICAUHCgED
CgML/////wUHAAUACQcLAAEACgsKAP8LCgALAAMKBQAIAAcFBwD/CwoFBwsF/////////////woG
Bf////////////////8ACAMFCgb/////////////CQABBQoG/////////////wEIAwEJCAUKBv//
//////8BBgUCBgH/////////////AQYFAQIGAwAI/////////wkGBQkABgACBv////////8FCQgF
CAIFAgYDAgj/////AgMLCgYF/////////////wsACAsCAAoGBf////////8AAQkCAwsFCgb/////
////BQoGAQkCCQsCCQgL/////wYDCwYFAwUBA/////////8ACAsACwUABQEFCwb/////AwsGAAMG
AAYFAAUJ/////wYFCQYJCwsJCP////////8FCgYEBwj/////////////BAMABAcDBgUK////////
/wEJAAUKBggEB/////////8KBgUBCQcBBwMHCQT/////BgECBgUBBAcI/////////wECBQUCBgMA
BAMEB/////8IBAcJAAUABgUAAgb/////BwMJBwkEAwIJBQkGAgYJ/wMLAgcIBAoGBf////////8F
CgYEBwIEAgACBwv/////AAEJBAcIAgMLBQoG/////wkCAQkLAgkECwcLBAUKBv8IBAcDCwUDBQEF
Cwb/////BQELBQsGAQALBwsEAAQL/wAFCQAGBQADBgsGAwgEB/8GBQkGCQsEBwkHCwn/////CgQJ
BgQK/////////////wQKBgQJCgAIA/////////8KAAEKBgAGBAD/////////CAMBCAEGCAYEBgEK
/////wEECQECBAIGBP////////8DAAgBAgkCBAkCBgT/////AAIEBAIG/////////////wgDAggC
BAQCBv////////8KBAkKBgQLAgP/////////AAgCAggLBAkKBAoG/////wMLAgABBgAGBAYBCv//
//8GBAEGAQoECAECAQsICwH/CQYECQMGCQEDCwYD/////wgLAQgBAAsGAQkBBAYEAf8DCwYDBgAA
BgT/////////BgQICwYI/////////////wcKBgcICggJCv////////8ABwMACgcACQoGBwr/////
CgYHAQoHAQcIAQgA/////woGBwoHAQEHA/////////8BAgYBBggBCAkIBgf/////AgYJAgkBBgcJ
AAkDBwMJ/wcIAAcABgYAAv////////8HAwIGBwL/////////////AgMLCgYICggJCAYH/////wIA
BwIHCwAJBwYHCgkKB/8BCAABBwgBCgcGBwoCAwv/CwIBCwEHCgYBBgcB/////wgJBggGBwkBBgsG
AwEDBv8ACQELBgf/////////////BwgABwAGAwsACwYA/////wcLBv////////////////8HBgv/
////////////////AwAICwcG/////////////wABCQsHBv////////////8IAQkIAwELBwb/////
////CgECBgsH/////////////wECCgMACAYLB/////////8CCQACCgkGCwf/////////BgsHAgoD
CggDCgkI/////wcCAwYCB/////////////8HAAgHBgAGAgD/////////AgcGAgMHAAEJ////////
/wEGAgEIBgEJCAgHBv////8KBwYKAQcBAwf/////////CgcGAQcKAQgHAQAI/////wADBwAHCgAK
CQYKB/////8HBgoHCggICgn/////////BggECwgG/////////////wMGCwMABgAEBv////////8I
BgsIBAYJAAH/////////CQQGCQYDCQMBCwMG/////wYIBAYLCAIKAf////////8BAgoDAAsABgsA
BAb/////BAsIBAYLAAIJAgoJ/////woJAwoDAgkEAwsDBgQGA/8IAgMIBAIEBgL/////////AAQC
BAYC/////////////wEJAAIDBAIEBgQDCP////8BCQQBBAICBAb/////////CAEDCAYBCAQGBgoB
/////woBAAoABgYABP////////8EBgMEAwgGCgMAAwkKCQP/CgkEBgoE/////////////wQJBQcG
C/////////////8ACAMECQULBwb/////////BQABBQQABwYL/////////wsHBggDBAMFBAMBBf//
//8JBQQKAQIHBgv/////////BgsHAQIKAAgDBAkF/////wcGCwUECgQCCgQAAv////8DBAgDBQQD
AgUKBQILBwb/BwIDBwYCBQQJ/////////wkFBAAIBgAGAgYIB/////8DBgIDBwYBBQAFBAD/////
BgIIBggHAgEIBAgFAQUI/wkFBAoBBgEHBgEDB/////8BBgoBBwYBAAcIBwAJBQT/BAAKBAoFAAMK
BgoHAwcK/wcGCgcKCAUECgQICv////8GCQUGCwkLCAn/////////AwYLAAYDAAUGAAkF/////wAL
CAAFCwABBQUGC/////8GCwMGAwUFAwH/////////AQIKCQULCQsICwUG/////wALAwAGCwAJBgUG
CQECCv8LCAULBQYIAAUKBQIAAgX/BgsDBgMFAgoDCgUD/////wUICQUCCAUGAgMIAv////8JBQYJ
BgAABgL/////////AQUIAQgABQYIAwgCBgII/wEFBgIBBv////////////8BAwYBBgoDCAYFBgkI
CQb/CgEACgAGCQUABQYA/////wADCAUGCv////////////8KBQb/////////////////CwUKBwUL
/////////////wsFCgsHBQgDAP////////8FCwcFCgsBCQD/////////CgcFCgsHCQgBCAMB////
/wsBAgsHAQcFAf////////8ACAMBAgcBBwUHAgv/////CQcFCQIHCQACAgsH/////wcFAgcCCwUJ
AgMCCAkIAv8CBQoCAwUDBwX/////////CAIACAUCCAcFCgIF/////wkAAQUKAwUDBwMKAv////8J
CAIJAgEIBwIKAgUHBQL/AQMFAwcF/////////////wAIBwAHAQEHBf////////8JAAMJAwUFAwf/
////////CQgHBQkH/////////////wUIBAUKCAoLCP////////8FAAQFCwAFCgsLAwD/////AAEJ
CAQKCAoLCgQF/////woLBAoEBQsDBAkEAQMBBP8CBQECCAUCCwgEBQj/////AAQLAAsDBAULAgsB
BQEL/wACBQAFCQILBQQFCAsIBf8JBAUCCwP/////////////AgUKAwUCAwQFAwgE/////wUKAgUC
BAQCAP////////8DCgIDBQoDCAUEBQgAAQn/BQoCBQIEAQkCCQQC/////wgEBQgFAwMFAf//////
//8ABAUBAAX/////////////CAQFCAUDCQAFAAMF/////wkEBf////////////////8ECwcECQsJ
Cgv/////////AAgDBAkHCQsHCQoL/////wEKCwELBAEEAAcEC/////8DAQQDBAgBCgQHBAsKCwT/
BAsHCQsECQILCQEC/////wkHBAkLBwkBCwILAQAIA/8LBwQLBAICBAD/////////CwcECwQCCAME
AwIE/////wIJCgIHCQIDBwcECf////8JCgcJBwQKAgcIBwACAAf/AwcKAwoCBwQKAQoABAAK/wEK
AggHBP////////////8ECQEEAQcHAQP/////////BAkBBAEHAAgBCAcB/////wQAAwcEA///////
//////8ECAf/////////////////CQoICgsI/////////////wMACQMJCwsJCv////////8AAQoA
CggICgv/////////AwEKCwMK/////////////wECCwELCQkLCP////////8DAAkDCQsBAgkCCwn/
////AAILCAAL/////////////wMCC/////////////////8CAwgCCAoKCAn/////////CQoCAAkC
/////////////wIDCAIICgABCAEKCP////8BCgL/////////////////AQMICQEI////////////
/wAJAf////////////////8AAwj//////////////////////////////////////w==
"""
#static const char cases[256][2]
CASES = (256, 2), """
AP8BAAEBAgABAgMAAgMFAAEDAgEDAwUBAgUFBAUJCAABBAICAwQFAgQCBgIGCQsAAwgFBQcDCQEG
EA4DDAwFGAEFAwECBAUDAwYHAAUKCQAEAwYEBgsOAQYRDAQLBgUZAggFBwUMCAEGEgwFDgcFHAYV
CwQMDwUeCgUGIAYnAgwBBgQAAwUGAAIGBgMFCw4AAwkGBQcEDAEFDgsDCQQFGgMKBgYHBQwCBhMK
AQwNBhgHBwwJDQEHCQwUBiEHDQMMAgoGBwUNCwIFEAwHCAMFHQYWCgIMEQYbDgkGIgUnAg4FFA4F
CQUFIAsKBiMFKQIQDBcGJQcOAxAGLgQGAxUBCAEHAwIEAQYBAwcHAQYKDAACBwUGBgwLAQUPCQIO
BgUbAgkFCAYNDgIGFAwGCgMGGQUSCAIMEAUfCwkFIgYoAg0DCwcCBg4MAwcGDQAMDgcIBhcMCgoE
BhwMFQcKBikDDQUVCQMLCAUhDBYHCwYqAw4OCwUkBiwCEQYvAxIEBwEJAgsGCAYPCgAFEQwICwcG
GgUTDgQMEgYdCAQFIwUoAg8FFgsFDBMGHg4KBiQGKwQECQcFJQcPAxEFLAITAxYBCgUXDAsOCAYf
CQYHDAUqAw8LCwYmBi0EBQUtAxMCFQELCAUFJgUrAhIFLgMUAhYBDAUvAhQDFwENAhcBDgEPAP8=
"""
#static const char tiling1[16][3]
TILING1 = (16, 3), """
AAgDAAEJAQIKAwsCBAcICQUECgYFBwYLBwsGCgUGCQQFBAgHAwILAQoCAAkBAAMI
"""
#static const char tiling2[24][6]
TILING2 = (24, 6), """
AQgDCQgBAAsCCAsABAMABwMECQIKAAIJAAUEAQUAAwoBCwoDAQYFAgYBBwIDBgIHCQcIBQcJBggE
CwgGCgQJBgQKCwUKBwULCwoFBwsFCgkEBgoEBgQICwYICQgHBQkHBwMCBgcCAQUGAgEGAwEKCwMK
AAQFAQAFCQoCAAkCBAADBwQDAAILCAALAQMICQEI
"""
#static const char tiling3_1[24][6]
TILING3_1 = (24, 6), """
AAgDAQIKCQUEAAgDAwAICwcGAQkAAgMLAAEJCAQHCQABBQoGAQIKCQUECgECBgsHCAQHAwsCAgML
CgYFBQoGBAcIBAkFBwYLBQkECwYHBgoFCAcECwMCBQYKBwQIAgsDAgEKBwsGCgIBBAUJAQAJBgoF
CQEABwQIAAkBCwMCCAADBgcLBAUJAwgAAwgACgIB
"""
#static const char tiling3_2[24][12]
TILING3_2 = (24, 12), """
CgMCCggDCgEACAoAAwQIAwUEAwAJBQMJBggHBgAIBgsDAAYDCwADCwkACwIBCQsBBwkEBwEJBwgA
AQcABgEKBgABCQAGCQYFBAoFBAIKBAkBAgQBBwILBwECBwYKAQcKAgcLAgQHAgMIBAIIBQsGBQML
BQoCAwUCCAYHCAoGCAQFCggFCwUGCwkFCwcECQsEBgULBQkLBAcLBAsJBwYIBgoIBQQIBQgKBgsF
CwMFAgoFAgUDCwcCBwQCCAMCCAIECwIHAgEHCgYHCgcBBQoECgIEAQkEAQQCCgEGAQAGBgAJBQYJ
BAkHCQEHAAgHAAcBAwALAAkLAQILAQsJBwgGCAAGAwsGAwYACAQDBAUDCQADCQMFAgMKAwgKAAEK
AAoI
"""
#static const char tiling4_1[8][6]
TILING4_1 = (8, 6), """
AAgDBQoGAAEJCwcGAQIKCAQHCQUEAgMLBAUJCwMCCgIBBwQICQEABgcLAwgABgoF
"""
#static const char tiling4_2[8][18]
TILING4_2 = (8, 18), """
CAUABQgGAwYIBgMKAAoDCgAFCQYBBgkHAAcJBwALAQsACwEGCgcCBwoEAQQKBAEIAggBCAIHCwQD
BAsFAgULBQIJAwkCCQMEAwQLBQsECwUCCQIFAgkDBAMJAgcKBAoHCgQBCAEEAQgCBwIIAQYJBwkG
CQcACwAHAAsBBgELAAUIBggFCAYDCgMGAwoABQAK
"""
#static const char tiling5[48][9]
TILING5 = (48, 9), """
AggDAgoICgkIAQsCAQkLCQgLBAEJBAcBBwMBCAUECAMFAwEFAAoBAAgKCAsKCwQHCwIEAgAEBwAI
BwYABgIACQMACQUDBQcDAwYLAwAGAAQGAwkAAwsJCwoJBQIKBQQCBAACCQYFCQAGAAIGAAcIAAEH
AQUHCgABCgYABgQABgMLBgUDBQEDCgcGCgEHAQMHAQQJAQIEAgYECwECCwcBBwUBCAIDCAQCBAYC
AgUKAgMFAwcFBwoGBwgKCAkKBgkFBgsJCwgJBQgEBQoICgsIBAsHBAkLCQoLBAcLBAsJCQsKBQQI
BQgKCggLBgUJBgkLCwkIBwYKBwoICAoJAgoFAgUDAwUHCAMCCAIEBAIGCwIBCwEHBwEFAQkEAQQC
AgQGCgYHCgcBAQcDBgsDBgMFBQMBCgEACgAGBgAEAAgHAAcBAQcFCQUGCQYAAAYCBQoCBQIEBAIA
AwAJAwkLCwkKAwsGAwYAAAYECQADCQMFBQMHBwgABwAGBgACCwcECwQCAgQAAAEKAAoICAoLCAQF
CAUDAwUBBAkBBAEHBwEDAQILAQsJCQsIAgMIAggKCggJ
"""
#static const char tiling6_1_1[48][9]
TILING6_1_1 = (48, 9), """
BgUKAwEICQgBCwcGCQMBAwkIAQIKBwAEAAcDAwAIBQIGAgUBBQQJAgALCAsACgYFCAIAAggLCgYF
AAQDBwMEAwAIBgQKCQoECAMACgcFBwoLCAQHCgACAAoJBwYLAAIJCgkCAgMLBAEFAQQAAAEJBgMH
AwYCCQABCwQGBAsICwcGAQUABAAFAAEJBwULCgsFBAcIAQMKCwoDCQUECwEDAQsKCgECCAUHBQgJ
CAQHAgYBBQEGAQIKBAYICwgGAgMLBQcJCAkHCwIDCQYEBgkKCQUEAwcCBgIHBAUJAgcDBwIGAwIL
BAYJCgkGCwMCCQcFBwkICgIBCAYEBggLBwQIAQYCBgEFAgEKBwUICQgFBAUJAwELCgsBCAcECgMB
AwoLCQEACwUHBQsKBgcLAAUBBQAEAQAJBgQLCAsECQEABwMGAgYDCwMCBQEEAAQBCwYHCQIAAgkK
BwQIAgAKCQoAAAMIBQcKCwoHCAADCgQGBAoJBQYKAwQABAMHBQYKAAIICwgCCQQFCwACAAsICAAD
BgIFAQUCCgIBBAAHAwcABgcLAQMJCAkDCgUGCAEDAQgJ
"""
#static const char tiling6_1_2[48][27]
TILING6_1_2 = (48, 27), """
AQwDDAoDBgMKAwYIBQgGCAUMDAkIAQkMDAUKAQwDAQsMCwEGCQYBBgkHDAcJCQgMDAgDCwcMBAwA
BAEMAQQKBwoECgcCDAIHBwMMDAMAAQIMBgwCBgMMAwYIBQgGCAUADAAFBQEMDAECAwAMAAwCDAkC
BQIJAgULBAsFCwQMDAgLAAgMDAQJAAwCAAoMCgAFCAUABQgGDAYICAsMDAsCCgYMBAwADAUACgAF
AAoDBgMKAwYMDAcDBAcMDAYFBAwGDAgGAwYIBgMKAAoDCgAMDAkKBAkMDAAIBQwHBQgMCAUACgAF
AAoDDAMKCgsMDAsHCAMMAgwAAggMCAIHCgcCBwoEDAQKCgkMDAkACAQMAgwADAsABwALAAcJBgkH
CQYMDAoJAgoMDAYLBQwBBQIMAgULBAsFCwQDDAMEBAAMDAABAgMMBwwDBwAMAAcJBgkHCQYBDAEG
BgIMDAIDAAEMBgwEBgkMCQYBCwEGAQsADAALCwgMDAgECQAMBQwBDAYBCwEGAQsABwALAAcMDAQA
BQQMDAcGBQwHDAkHAAcJBwALAQsACwEMDAoLBQoMDAEJAwwBDAgBBAEIAQQKBwoECgcMDAsKAwsM
DAcIAwwBAwkMCQMECwQDBAsFDAULCwoMDAoBCQUMBwwFBwoMCgcCCAIHAggBDAEICAkMDAkFCgEM
BgwCDAcCCAIHAggBBAEIAQQMDAUBBgUMDAQHBgwEDAoEAQQKBAEIAggBCAIMDAsIBgsMDAIKBwwF
DAsFAgULBQIJAwkCCQMMDAgJBwgMDAMLBAwGBAsMCwQDCQMEAwkCDAIJCQoMDAoGCwIMBwwDDAQD
CQMEAwkCBQIJAgUMDAYCBwYMDAUEAwwHAwQMBAMJAgkDCQIFDAUCAgYMDAYHBAUMBgwEDAsEAwQL
BAMJAgkDCQIMDAoJBgoMDAILBQwHBQsMCwUCCQIFAgkDDAMJCQgMDAgHCwMMBAwGBAoMCgQBCAEE
AQgCDAIICAsMDAsGCgIMAgwGAgcMBwIIAQgCCAEEDAQBAQUMDAUGBwQMBQwHDAoHAgcKBwIIAQgC
CAEMDAkIBQkMDAEKAQwDDAkDBAMJAwQLBQsECwUMDAoLAQoMDAUJAQwDAQgMCAEECgQBBAoHDAcK
CgsMDAsDCAcMBwwFBwkMCQcACwAHAAsBDAELCwoMDAoFCQEMAQwFAQYMBgELAAsBCwAHDAcAAAQM
DAQFBgcMBAwGDAkGAQYJBgELAAsBCwAMDAgLBAgMDAAJAwwHDAAHCQcABwkGAQYJBgEMDAIGAwIM
DAEAAQwFDAIFCwUCBQsEAwQLBAMMDAAEAQAMDAMCAAwCAAsMCwAHCQcABwkGDAYJCQoMDAoCCwYM
AAwCDAgCBwIIAgcKBAoHCgQMDAkKAAkMDAQIBwwFDAgFAAUIBQAKAwoACgMMDAsKBwsMDAMIBgwE
BggMCAYDCgMGAwoADAAKCgkMDAkECAAMAAwEAAUMBQAKAwoACgMGDAYDAwcMDAcEBQYMAgwADAoA
BQAKAAUIBggFCAYMDAsIAgsMDAYKAgwAAgkMCQIFCwUCBQsEDAQLCwgMDAgACQQMAgwGDAMGCAYD
BggFAAUIBQAMDAEFAgEMDAADAAwEDAEECgQBBAoHAgcKBwIMDAMHAAMMDAIBAwwBDAsBBgELAQYJ
BwkGCQcMDAgJAwgMDAcLAwwBAwoMCgMGCAYDBggFDAUICAkMDAkBCgUM
"""
#static const char tiling6_2[48][15]
TILING6_2 = (48, 15), """
AQoDBgMKAwYIBQgGCAUJAQsDCwEGCQYBBgkHCAcJBAEAAQQKBwoECgcCAwIHBgMCAwYIBQgGCAUA
AQAFAAkCBQIJAgULBAsFCwQIAAoCCgAFCAUABQgGCwYIBAUACgAFAAoDBgMKAwYHBAgGAwYIBgMK
AAoDCgAJBQgHCAUACgAFAAoDCwMKAggACAIHCgcCBwoECQQKAgsABwALAAcJBgkHCQYKBQIBAgUL
BAsFCwQDAAMEBwADAAcJBgkHCQYBAgEGBgkECQYBCwEGAQsACAALBQYBCwEGAQsABwALAAcEBQkH
AAcJBwALAQsACwEKAwgBBAEIAQQKBwoECgcLAwkBCQMECwQDBAsFCgULBwoFCgcCCAIHAggBCQEI
BgcCCAIHAggBBAEIAQQFBgoEAQQKBAEIAggBCAILBwsFAgULBQIJAwkCCQMIBAsGCwQDCQMEAwkC
CgIJBwQDCQMEAwkCBQIJAgUGAwQHBAMJAgkDCQIFBgUCBgsEAwQLBAMJAgkDCQIKBQsHCwUCCQIF
AgkDCAMJBAoGCgQBCAEEAQgCCwIIAgcGBwIIAQgCCAEEBQQBBQoHAgcKBwIIAQgCCAEJAQkDBAMJ
AwQLBQsECwUKAQgDCAEECgQBBAoHCwcKBwkFCQcACwAHAAsBCgELAQYFBgELAAsBCwAHBAcABAkG
AQYJBgELAAsBCwAIAwAHCQcABwkGAQYJBgECAQIFCwUCBQsEAwQLBAMAAAsCCwAHCQcABwkGCgYJ
AAgCBwIIAgcKBAoHCgQJBwgFAAUIBQAKAwoACgMLBggECAYDCgMGAwoACQAKAAUEBQAKAwoACgMG
BwYDAgoABQAKAAUIBggFCAYLAgkACQIFCwUCBQsECAQLAgMGCAYDBggFAAUIBQABAAEECgQBBAoH
AgcKBwIDAwsBBgELAQYJBwkGCQcIAwoBCgMGCAYDBggFCQUI
"""
#static const char tiling7_1[16][9]
TILING7_1 = (16, 9), """
CQUECgECCAMACwcGCAMACgECAwAIBQQJBwYLCAQHCQABCwIDCgYFCwIDCQABAAEJBgUKBAcIAQIK
BwYLBQQJAgMLBAcIBgUKCwMCCAcECgUGCgIBCwYHCQQFCQEACgUGCAcEBQYKAwILAQAJBwQIAQAJ
AwILCAADCQQFCwYHBgcLAAMIAgEKBAUJAgEKAAMI
"""
#static const char tiling7_2[16][3][15]
TILING7_2 = (16, 3, 15), """
AQIKAwQIBAMFAAUDBQAJAwAICQEEAgQBBAIFCgUCCQUEAAoBCgAICggCAwIIAwAIAQYKBgEHAgcB
BwILAQIKCwMGAAYDBgAHCAcACwcGAggDCAIKCAoAAQAKCQUECwMGAAYDBgAHCAcACwcGAwQIBAMF
AAUDBQAJAwAIBAkHCwcJBQsJCwUGAAEJAgcLBwIEAwQCBAMIAgMLCAAHAQcABwEECQQBCAQHAwkA
CQMLCQsBAgELAgMLAAUJBQAGAQYABgEKAAEJCgIFAwUCBQMGCwYDBgUKAQsCCwEJCwkDAAMJBgUK
CAAHAQcABwEECQQBCAQHAAUJBQAGAQYABgEKAAEJBQoECAQKBggKCAYHCwcGCQEEAgQBBAIFCgUC
CQUEAQYKBgEHAgcBBwILAQIKBgsFCQULBwkLCQcECAQHCgIFAwUCBQMGCwYDBgUKAgcLBwIEAwQC
BAMIAgMLBwgGCgYIBAoICgQFBwQIBQIKAgUDBgMFAwYLCgUGCwcCBAIHAgQDCAMECwMCBggHCAYK
CAoEBQQKBgcLBAEJAQQCBQIEAgUKBAUJCgYBBwEGAQcCCwIHCgIBBQsGCwUJCwkHBAcJCgUGBwAI
AAcBBAEHAQQJBwQICQUABgAFAAYBCgEGCQEABAoFCgQICggGBwYICwMCCQUABgAFAAYBCgEGCQEA
BQIKAgUDBgMFAwYLCgUGAgsBCQELAwkLCQMACQEACwcCBAIHAgQDCAMECwMCBwAIAAcBBAEHAQQJ
BwQIAAkDCwMJAQsJCwECBAUJBgMLAwYABwAGAAcIBgcLCAQDBQMEAwUACQAFCAADBwkECQcLCQsF
BgULCAADCgYBBwEGAQcCCwIHCgIBBgMLAwYABwAGAAcIBgcLAwgCCgIIAAoICgABCgIBCAQDBQME
AwUACQAFCAADBAEJAQQCBQIEAgUKBAUJAQoACAAKAggKCAID
"""
#static const char tiling7_3[16][3][27]
TILING7_3 = (16, 3, 27), """
DAIKDAoFDAUEDAQIDAgDDAMADAAJDAkBDAECDAUEDAQIDAgDDAMCDAIKDAoBDAEADAAJDAkFBQQM
CgUMAgoMAwIMCAMMAAgMAQAMCQEMBAkMDAAIDAgHDAcGDAYKDAoBDAECDAILDAsDDAMADAcGDAYK
DAoBDAEADAAIDAgDDAMCDAILDAsHBwYMCAcMAAgMAQAMCgEMAgoMAwIMCwMMBgsMCQUMAAkMAwAM
CwMMBgsMBwYMCAcMBAgMBQQMAwAMCwMMBgsMBQYMCQUMBAkMBwQMCAcMAAgMDAMADAAJDAkFDAUG
DAYLDAsHDAcEDAQIDAgDDAEJDAkEDAQHDAcLDAsCDAIDDAMIDAgADAABDAQHDAcLDAsCDAIBDAEJ
DAkADAADDAMIDAgEBAcMCQQMAQkMAgEMCwIMAwsMAAMMCAAMBwgMDAMLDAsGDAYFDAUJDAkADAAB
DAEKDAoCDAIDDAYFDAUJDAkADAADDAMLDAsCDAIBDAEKDAoGBgUMCwYMAwsMAAMMCQAMAQkMAgEM
CgIMBQoMCgYMAQoMAAEMCAAMBwgMBAcMCQQMBQkMBgUMAAEMCAAMBwgMBgcMCgYMBQoMBAUMCQQM
AQkMDAABDAEKDAoGDAYHDAcIDAgEDAQFDAUJDAkACwcMAgsMAQIMCQEMBAkMBQQMCgUMBgoMBwYM
AQIMCQEMBAkMBwQMCwcMBgsMBQYMCgUMAgoMDAECDAILDAsHDAcEDAQJDAkFDAUGDAYKDAoBCAQM
AwgMAgMMCgIMBQoMBgUMCwYMBwsMBAcMAgMMCgIMBQoMBAUMCAQMBwgMBgcMCwYMAwsMDAIDDAMI
DAgEDAQFDAUKDAoGDAYHDAcLDAsCDAQIDAgDDAMCDAIKDAoFDAUGDAYLDAsHDAcEDAMCDAIKDAoF
DAUEDAQIDAgHDAcGDAYLDAsDAwIMCAMMBAgMBQQMCgUMBgoMBwYMCwcMAgsMDAcLDAsCDAIBDAEJ
DAkEDAQFDAUKDAoGDAYHDAIBDAEJDAkEDAQHDAcLDAsGDAYFDAUKDAoCAgEMCwIMBwsMBAcMCQQM
BQkMBgUMCgYMAQoMDAYKDAoBDAEADAAIDAgHDAcEDAQJDAkFDAUGDAEADAAIDAgHDAcGDAYKDAoF
DAUEDAQJDAkBAQAMCgEMBgoMBwYMCAcMBAgMBQQMCQUMAAkMCwMMBgsMBQYMCQUMAAkMAQAMCgEM
AgoMAwIMBQYMCQUMAAkMAwAMCwMMAgsMAQIMCgEMBgoMDAUGDAYLDAsDDAMADAAJDAkBDAECDAIK
DAoFCQEMBAkMBwQMCwcMAgsMAwIMCAMMAAgMAQAMBwQMCwcMAgsMAQIMCQEMAAkMAwAMCAMMBAgM
DAcEDAQJDAkBDAECDAILDAsDDAMADAAIDAgHDAUJDAkADAADDAMLDAsGDAYHDAcIDAgEDAQFDAAD
DAMLDAsGDAYFDAUJDAkEDAQHDAcIDAgAAAMMCQAMBQkMBgUMCwYMBwsMBAcMCAQMAwgMCAAMBwgM
BgcMCgYMAQoMAgEMCwIMAwsMAAMMBgcMCgYMAQoMAAEMCAAMAwgMAgMMCwIMBwsMDAYHDAcIDAgA
DAABDAEKDAoCDAIDDAMLDAsGCgIMBQoMBAUMCAQMAwgMAAMMCQAMAQkMAgEMBAUMCAQMAwgMAgMM
CgIMAQoMAAEMCQAMBQkMDAQFDAUKDAoCDAIDDAMIDAgADAABDAEJDAkE
"""
#static const char tiling7_4_1[16][15]
TILING7_4_1 = (16, 15), """
AwQIBAMKAgoDBAoFCQEAAQYKBgEIAAgBBggHCwMCCwMGCQYDBgkFAAkDBwQIAgcLBwIJAQkCBwkE
CAADAAUJBQALAwsABQsGCgIBCAAHCgcABwoGAQoABAUJCQEECwQBBAsHAgsBBQYKCgIFCAUCBQgE
AwgCBgcLBQIKAgUIBAgFAggDCwcGBAEJAQQLBwsEAQsCCgYFBwAIAAcKBgoHAAoBCQUECQUACwAF
AAsDBgsFAQIKCwcCCQIHAgkBBAkHAwAIBgMLAwYJBQkGAwkACAQHCgYBCAEGAQgABwgGAgMLCAQD
CgMEAwoCBQoEAAEJ
"""
#static const char tiling7_4_2[16][27]
TILING7_4_2 = (16, 27), """
CQQIBAkFCgUJAQoJCgECAAIBAgADCAMACQgACwYKBgsHCAcLAwgLCAMAAgADAAIBCgECCwoCCwMI
AAgDCAAJCAkEBQQJBAUHBgcFBwYLBwsICAcLBwgECQQIAAkICQABAwEAAQMCCwIDCAsDCgUJBQoG
CwYKAgsKCwIDAQMCAwEACQABCgkBCAAJAQkACQEKCQoFBgUKBQYEBwQGBAcIBAgJCQEKAgoBCgIL
CgsGBwYLBgcFBAUHBQQJBQkKCgILAwsCCwMICwgHBAcIBwQGBQYEBgUKBgoLCwIKAgsDCAMLBwgL
CAcEBgQHBAYFCgUGCwoGCgEJAQoCCwIKBgsKCwYHBQcGBwUECQQFCgkFCQAIAAkBCgEJBQoJCgUG
BAYFBgQHCAcECQgECQUKBgoFCgYLCgsCAwILAgMBAAEDAQAJAQkKCwcIBAgHCAQJCAkAAQAJAAED
AgMBAwILAwsICAMLAwgACQAIBAkICQQFBwUEBQcGCwYHCAsHCgYLBwsGCwcICwgDAAMIAwACAQIA
AgEKAgoLCAQJBQkECQUKCQoBAgEKAQIAAwACAAMIAAgJ
"""
#static const char tiling8[6][6]
TILING8 = (6, 6), """
CQgKCggLAQUDAwUHAAQCBAYCAAIEBAIGAQMFAwcFCQoICgsI
"""
#static const char tiling9[8][12]
TILING9 = (8, 12), """
AgoFAwIFAwUEAwQIBAcLCQQLCQsCCQIBCgcGAQcKAQgHAQAIAwYLAAYDAAUGAAkFAwsGAAMGAAYF
AAUJCgYHAQoHAQcIAQgABAsHCQsECQILCQECAgUKAwUCAwQFAwgE
"""
#static const char tiling10_1_1[6][12]
TILING10_1_1 = (6, 12), """
BQoHCwcKCAEJAQgDAQIFBgUCBAMAAwQHCwAIAAsCBAkGCgYJCQAKAgoABggECAYLBwIDAgcGAAEE
BQQBBwkFCQcICgELAwsB
"""
#static const char tiling10_1_1_[6][12]
TILING10_1_1_ = (6, 12), """
BQkHCAcJCwEKAQsDAwIHBgcCBAEAAQQFCgAJAAoCBAgGCwYICAALAgsABgkECQYKBQIBAgUGAAME
BwQDBwoFCgcLCQEIAwgB
"""
#static const char tiling10_1_2[6][24]
TILING10_1_2 = (6, 24), """
AwsHAwcICQgHBQkHCQUKCQoBAwEKCwMKBwYFBwUEAAQFAQAFAAECAAIDBwMCBgcCCwIKBgsKCwYE
CwQIAAgECQAEAAkKAAoCCwIKCwoGBAYKCQQKBAkABAAICwgAAgsABwYFBAcFBwQABwADAgMAAQIA
AgEFAgUGBwgDCwcDBwsKBwoFCQUKAQkKCQEDCQMI
"""
#static const char tiling10_2[6][24]
TILING10_2 = (6, 24), """
DAUJDAkIDAgDDAMBDAEKDAoLDAsHDAcFDAEADAAEDAQHDAcDDAMCDAIGDAYFDAUBBAgMBgQMCgYM
CQoMAAkMAgAMCwIMCAsMDAkEDAQGDAYLDAsIDAgADAACDAIKDAoJAAMMBAAMBQQMAQUMAgEMBgIM
BwYMAwcMCgUMCwoMAwsMAQMMCQEMCAkMBwgMBQcM
"""
#static const char tiling10_2_[6][24]
TILING10_2_ = (6, 24), """
CAcMCQgMAQkMAwEMCwMMCgsMBQoMBwUMBAUMAAQMAwAMBwMMBgcMAgYMAQIMBQEMDAsGDAYEDAQJ
DAkKDAoCDAIADAAIDAgLBgoMBAYMCAQMCwgMAgsMAAIMCQAMCgkMDAcEDAQADAABDAEFDAUGDAYC
DAIDDAMHDAcLDAsKDAoBDAEDDAMIDAgJDAkFDAUH
"""
#static const char tiling11[12][12]
TILING11 = (12, 12), """
AgoJAgkHAgcDBwkEAQYCAQgGAQkICAcGCAMBCAEGCAYEBgEKAAgLAAsFAAUBBQsGCQUHCQcCCQIA
AgcLBQAEBQsABQoLCwMABQQABQALBQsKCwADCQcFCQIHCQACAgsHAAsIAAULAAEFBQYLCAEDCAYB
CAQGBgoBAQIGAQYIAQgJCAYHAgkKAgcJAgMHBwQJ
"""
#static const char tiling12_1_1[24][12]
TILING12_1_1 = (24, 12), """
BwYLCgMCAwoICQgKBgUKCQIBAgkLCAsJCgYFBwkECQcBAwEHBwYLBAgFAwUIBQMBBQQJCAEAAQgK
CwoIAQIKAAkDBQMJAwUHCgECAAsDCwAGBAYACAMAAgkBCQIEBgQCAwAIAgsBBwELAQcFBgUKBwsE
AgQLBAIACQUEBggHCAYAAgAGCAMABwQLCQsECwkKBAcICwADAAsJCgkLBAcIBQkGAAYJBgACCwcG
BAoFCgQCAAIECwIDAQgACAEHBQcBAAEJAwgCBAIIAgQGAgMLAQoABgAKAAYECQABAwoCCgMFBwUD
CQABBAUICggFCAoLCAQHBQsGCwUDAQMFBQQJBgoHAQcKBwEDCgECBQYJCwkGCQsICwIDBgcKCAoH
CggJ
"""
#static const char tiling12_1_1_[24][12]
TILING12_1_1_ = (24, 12), """
AwILCgcGBwoICQgKAgEKCQYFBgkLCAsJCQQFBwoGCgcBAwEHBwQIBgsFAwULBQMBAQAJCAUEBQgK
CwoIAQAJAgoDBQMKAwUHCwMCAAoBCgAGBAYACQEAAggDCAIEBgQCAwILAAgBBwEIAQcFBgcLBQoE
AgQKBAIACAcEBgkFCQYAAgAGCAcEAwALCQsACwkKAAMICwQHBAsJCgkLBAUJBwgGAAYIBgACCgUG
BAsHCwQCAAIECAADAQsCCwEHBQcBAAMIAQkCBAIJAgQGAgEKAwsABgALAAYECgIBAwkACQMFBwUD
CQQFAAEICggBCAoLCwYHBQgECAUDAQMFBQYKBAkHAQcJBwEDCgUGAQIJCwkCCQsICwYHAgMKCAoD
CggJ
"""
#static const char tiling12_1_2[24][24]
TILING12_1_2 = (24, 24), """
BwMLAwcICQgHBgkHCQYKAgoGCwIGAgsDBgIKAgYLCAsGBQgGCAUJAQkFCgEFAQoCCgkFCQoBAwEK
BgMKAwYHBAcGBQQGBAUJBwgLAwsICwMBCwEGBQYBBgUEBgQHCAcEBQEJAQUKCwoFBAsFCwQIAAgE
CQAEAAkBAQkKBQoJCgUHCgcCAwIHAgMAAgABCQEACgsCCwoGBAYKAQQKBAEAAwABAgMBAwILCAkA
CQgEBgQIAwYIBgMCAQIDAAEDAQAJAwsIBwgLCAcFCAUAAQAFAAECAAIDCwMCBgsKAgoLCgIACgAF
BAUABQQHBQcGCwYHCQgECAkAAgAJBQIJAgUGBwYFBAcFBwQICAQACQAEAAkKAAoDCwMKAwsHAwcI
BAgHBAAIAAQJCgkEBwoECgcLAwsHCAMHAwgABAkIAAgJCAACCAIHBgcCBwYFBwUECQQFCwoGCgsC
AAILBwALAAcEBQQHBgUHBQYKCwgDCAsHBQcLAgULBQIBAAECAwACAAMIAAgJBAkICQQGCQYBAgEG
AQIDAQMACAADAgoLBgsKCwYECwQDAAMEAwABAwECCgIBCQoBCgkFBwUJAAcJBwADAgMAAQIAAgEK
CQUBCgEFAQoLAQsACAALAAgEAAQJBQkECAsHCwgDAQMIBAEIAQQFBgUEBwYEBgcLBQoJAQkKCQED
CQMEBwQDBAcGBAYFCgUGCgYCCwIGAgsIAggBCQEIAQkFAQUKBgoFCwcDCAMHAwgJAwkCCgIJAgoG
AgYLBwsG
"""
#static const char tiling12_2[24][24]
TILING12_2 = (24, 24), """
CQgMCgkMAgoMAwIMCwMMBgsMBwYMCAcMCAsMCQgMAQkMAgEMCgIMBQoMBgUMCwYMAwEMBwMMBAcM
CQQMBQkMBgUMCgYMAQoMDAMBDAEFDAUGDAYLDAsHDAcEDAQIDAgDCwoMCAsMAAgMAQAMCQEMBAkM
BQQMCgUMDAUHDAcDDAMCDAIKDAoBDAEADAAJDAkFBAYMAAQMAQAMCgEMAgoMAwIMCwMMBgsMBgQM
AgYMAwIMCAMMAAgMAQAMCQEMBAkMDAcFDAUBDAEADAAIDAgDDAMCDAILDAsHDAIADAAEDAQFDAUK
DAoGDAYHDAcLDAsCAgAMBgIMBwYMCAcMBAgMBQQMCQUMAAkMDAkKDAoLDAsHDAcEDAQIDAgDDAMA
DAAJCgkMCwoMBwsMBAcMCAQMAwgMAAMMCQAMDAACDAIGDAYHDAcIDAgEDAQFDAUJDAkAAAIMBAAM
BQQMCgUMBgoMBwYMCwcMAgsMBQcMAQUMAAEMCAAMAwgMAgMMCwIMBwsMDAQGDAYCDAIDDAMIDAgA
DAABDAEJDAkEDAYEDAQADAABDAEKDAoCDAIDDAMLDAsGBwUMAwcMAgMMCgIMAQoMAAEMCQAMBQkM
DAoLDAsIDAgADAABDAEJDAkEDAQFDAUKAQMMBQEMBgUMCwYMBwsMBAcMCAQMAwgMDAEDDAMHDAcE
DAQJDAkFDAUGDAYKDAoBDAsIDAgJDAkBDAECDAIKDAoFDAUGDAYLDAgJDAkKDAoCDAIDDAMLDAsG
DAYHDAcI
"""
#static const char tiling12_2_[24][24]
TILING12_2_ = (24, 24), """
DAILDAsHDAcGDAYKDAoJDAkIDAgDDAMCDAEKDAoGDAYFDAUJDAkIDAgLDAsCDAIBDAQFDAUKDAoG
DAYHDAcDDAMBDAEJDAkEBwYMCAcMBAgMBQQMAQUMAwEMCwMMBgsMDAAJDAkFDAUEDAQIDAgLDAsK
DAoBDAEAAQIMCQEMAAkMAwAMBwMMBQcMCgUMAgoMDAECDAILDAsDDAMADAAEDAQGDAYKDAoBDAMA
DAAJDAkBDAECDAIGDAYEDAQIDAgDAwAMCwMMAgsMAQIMBQEMBwUMCAcMAAgMBgUMCwYMBwsMBAcM
AAQMAgAMCgIMBQoMDAcEDAQJDAkFDAUGDAYCDAIADAAIDAgHCAcMAAgMAwAMCwMMCgsMCQoMBAkM
BwQMDAcIDAgADAADDAMLDAsKDAoJDAkEDAQHBAcMCQQMBQkMBgUMAgYMAAIMCAAMBwgMDAUGDAYL
DAsHDAcEDAQADAACDAIKDAoFDAADDAMLDAsCDAIBDAEFDAUHDAcIDAgAAAMMCQAMAQkMAgEMBgIM
BAYMCAQMAwgMAgEMCwIMAwsMAAMMBAAMBgQMCgYMAQoMDAIBDAEJDAkADAADDAMHDAcFDAUKDAoC
CQAMBQkMBAUMCAQMCwgMCgsMAQoMAAEMDAYHDAcIDAgEDAQFDAUBDAEDDAMLDAsGBQQMCgUMBgoM
BwYMAwcMAQMMCQEMBAkMCgEMBgoMBQYMCQUMCAkMCwgMAgsMAQIMCwIMBwsMBgcMCgYMCQoMCAkM
AwgMAgMM
"""
#static const char tiling13_1[2][12]
TILING13_1 = (2, 12), """
CwcGAQIKCAMACQUECAQHAgMLCQABCgYF
"""
#static const char tiling13_1_[2][12]
TILING13_1_ = (2, 12), """
BwQICwMCAQAJBQYKBgcLCgIBAAMIBAUJ
"""
#static const char tiling13_2[2][6][18]
TILING13_2 = (2, 6, 18), """
AQIKCwcGAwQIBAMFAAUDBQAJCAMACwcGCQEEAgQBBAIFCgUCCQUECAMAAQYKBgEHAgcBBwILCQUE
AQIKCwMGAAYDBgAHCAcACQUECwcGAAoBCgAICggCAwIIAQIKAwAIBAkHCwcJBQsJCwUGAgMLCAQH
AAUJBQAGAQYABgEKCQABCAQHCgIFAwUCBQMGCwYDBgUKCQABAgcLBwIEAwQCBAMIBgUKAgMLCAAH
AQcABwEECQQBBgUKCAQHAQsCCwEJCwkDAAMJAgMLAAEJBQoECAQKBggKCAYH
"""
#static const char tiling13_2_[2][6][18]
TILING13_2_ = (2, 6, 18), """
CgUGCwMCBwAIAAcBBAEHAQQJCwMCBwQICQUABgAFAAYBCgEGAQAJBwQIBQIKAgUDBgMFAwYLCgUG
AQAJCwcCBAIHAgQDCAMECgUGBwQIAgsBCQELAwkLCQMACwMCCQEABAoFCgQICggGBwYIBgcLCAAD
BAEJAQQCBQIEAgUKCAADBAUJCgYBBwEGAQcCCwIHAgEKBAUJBgMLAwYABwAGAAcIBgcLAgEKCAQD
BQMEAwUACQAFBgcLBAUJAwgCCgIIAAoICgABCAADCgIBBQsGCwUJCwkHBAcJ
"""
#static const char tiling13_3[2][12][30]
TILING13_3 = (2, 12, 30), """
CwcGDAIKDAoFDAUEDAQIDAgDDAMADAAJDAkBDAECAQIKCQUMAAkMAwAMCwMMBgsMBwYMCAcMBAgM
BQQMCwcGDAUEDAQIDAgDDAMCDAIKDAoBDAEADAAJDAkFAQIKDAMADAAJDAkFDAUGDAYLDAsHDAcE
DAQIDAgDCAMACwcMAgsMAQIMCQEMBAkMBQQMCgUMBgoMBwYMCwcGBQQMCgUMAgoMAwIMCAMMAAgM
AQAMCQEMBAkMCAMAAQIMCQEMBAkMBwQMCwcMBgsMBQYMCgUMAgoMCQUEDAAIDAgHDAcGDAYKDAoB
DAECDAILDAsDDAMACQUEDAcGDAYKDAoBDAEADAAIDAgDDAMCDAILDAsHCAMADAECDAILDAsHDAcE
DAQJDAkFDAUGDAYKDAoBCQUEBwYMCAcMAAgMAQAMCgEMAgoMAwIMCwMMBgsMAQIKAwAMCwMMBgsM
BQYMCQUMBAkMBwQMCAcMAAgMCAQHDAMLDAsGDAYFDAUJDAkADAABDAEKDAoCDAIDAgMLCgYMAQoM
AAEMCAAMBwgMBAcMCQQMBQkMBgUMCAQHDAYFDAUJDAkADAADDAMLDAsCDAIBDAEKDAoGAgMLDAAB
DAEKDAoGDAYHDAcIDAgEDAQFDAUJDAkAAAEJCAQMAwgMAgMMCgIMBQoMBgUMCwYMBwsMBAcMCAQH
BgUMCwYMAwsMAAMMCQAMAQkMAgEMCgIMBQoMCQABAgMMCgIMBQoMBAUMCAQMBwgMBgcMCwYMAwsM
BgUKDAEJDAkEDAQHDAcLDAsCDAIDDAMIDAgADAABBgUKDAQHDAcLDAsCDAIBDAEJDAkADAADDAMI
DAgECQABDAIDDAMIDAgEDAQFDAUKDAoGDAYHDAcLDAsCBgUKBAcMCQQMAQkMAgEMCwIMAwsMAAMM
CAAMBwgMAgMLAAEMCAAMBwgMBgcMCgYMBQoMBAUMCQQMAQkM
"""
#static const char tiling13_3_[2][12][30]
TILING13_3_ = (2, 12, 30), """
AwILCAcMAAgMAQAMCgEMBgoMBQYMCQUMBAkMBwQMBQYKDAILDAsHDAcEDAQJDAkBDAEADAAIDAgD
DAMCCgUGDAcEDAQJDAkBDAECDAILDAsDDAMADAAIDAgHCwMCDAEADAAIDAgHDAcGDAYKDAoFDAUE
DAQJDAkBBwQICwMMBgsMBQYMCQUMAAkMAQAMCgEMAgoMAwIMBwQIBQYMCQUMAAkMAwAMCwMMAgsM
AQIMCgEMBgoMCwMCAQAMCgEMBgoMBwYMCAcMBAgMBQQMCQUMAAkMAQAJDAQIDAgDDAMCDAIKDAoF
DAUGDAYLDAsHDAcEBwQIDAUGDAYLDAsDDAMADAAJDAkBDAECDAIKDAoFAQAJDAMCDAIKDAoFDAUE
DAQIDAgHDAcGDAYLDAsDCgUGBwQMCwcMAgsMAQIMCQEMAAkMAwAMCAMMBAgMCQEAAwIMCAMMBAgM
BQQMCgUMBgoMBwYMCwcMAgsMAAMICQQMAQkMAgEMCwIMBwsMBgcMCgYMBQoMBAUMCwYHDAMIDAgE
DAQFDAUKDAoCDAIBDAEJDAkADAADBgcLDAQFDAUKDAoCDAIDDAMIDAgADAABDAEJDAkECAADDAIB
DAEJDAkEDAQHDAcLDAsGDAYFDAUKDAoCBAUJCAAMBwgMBgcMCgYMAQoMAgEMCwIMAwsMAAMMBAUJ
BgcMCgYMAQoMAAEMCAAMAwgMAgMMCwIMBwsMCAADAgEMCwIMBwsMBAcMCQQMBQkMBgUMCgYMAQoM
AgEKDAUJDAkADAADDAMLDAsGDAYHDAcIDAgEDAQFBAUJDAYHDAcIDAgADAABDAEKDAoCDAIDDAML
DAsGAgEKDAADDAMLDAsGDAYFDAUJDAkEDAQHDAcIDAgABgcLBAUMCAQMAwgMAgMMCgIMAQoMAAEM
CQAMBQkMCgIBAAMMCQAMBQkMBgUMCwYMBwsMBAcMCAQMAwgM
"""
#static const char tiling13_4[2][4][36]
TILING13_4 = (2, 4, 36), """
DAIKDAoFDAUGDAYLDAsHDAcEDAQIDAgDDAMADAAJDAkBDAECCwMMBgsMBwYMCAcMBAgMBQQMCQUM
AAkMAQAMCgEMAgoMAwIMCQEMBAkMBQQMCgUMBgoMBwYMCwcMAgsMAwIMCAMMAAgMAQAMDAAIDAgH
DAcEDAQJDAkFDAUGDAYKDAoBDAECDAILDAsDDAMADAMLDAsGDAYHDAcIDAgEDAQFDAUJDAkADAAB
DAEKDAoCDAIDCAAMBwgMBAcMCQQMBQkMBgUMCgYMAQoMAgEMCwIMAwsMAAMMCgIMBQoMBgUMCwYM
BwsMBAcMCAQMAwgMAAMMCQAMAQkMAgEMDAEJDAkEDAQFDAUKDAoGDAYHDAcLDAsCDAIDDAMIDAgA
DAAB
"""
#static const char tiling13_5_1[2][4][18]
TILING13_5_1 = (2, 4, 18), """
BwYLAQAJCgMCAwoFAwUIBAgFAQIKBwQIAwALBgsACQYABgkFAwAIBQYKAQIJBAkCCwQCBAsHBQQJ
AwILCAEAAQgHAQcKBgoHBAcIAgEKCwADAAsGAAYJBQkGAgMLBAUJAAEIBwgBCgcBBwoGAAEJBgcL
AgMKBQoDCAUDBQgEBgUKAAMICQIBAgkEAgQLBwsE
"""
#static const char tiling13_5_2[2][4][30]
TILING13_5_2 = (2, 4, 30), """
AQAJBwQIBwgDBwMLAgsDCwIKCwoGBQYKBgUHBAcFBwQICwMCBgsCCgYCBgoFCQUKAQkKCQEAAgAB
AAIDBQYKCQEABAkACAQABAgHCwcIAwsICwMCAAIDAgABAwILBQYKBQoBBQEJAAkBCQAICQgEBAgH
BAcFBgUHAgEKBAUJBAkABAAIAwgACAMLCAsHBgcLBwYEBQQGBAUJCAADBwgDCwcDBwsGCgYLAgoL
CgIBAwECAQMABgcLCgIBBQoBCQUBBQkECAQJAAgJCAADAQMAAwECAAMIBgcLBgsCBgIKAQoCCgEJ
CgkFBQkEBQQGBwYE
"""
#static const char tiling14[12][12]
TILING14 = (12, 12), """
BQkIBQgCBQIGAwIIAgEFAgUIAggLBAgFCQQGCQYDCQMBCwMGAQsKAQQLAQAEBwsECAIACAUCCAcF
CgIFAAcDAAoHAAkKBgcKAAMHAAcKAAoJBgoHCAACCAIFCAUHCgUCAQoLAQsEAQQABwQLCQYECQMG
CQEDCwYDAgUBAggFAgsIBAUIBQgJBQIIBQYCAwgC
"""
#static const char test3[24]
TEST3 = (24,), """
BQEEBQECAgMEAwYG+vr9/P3+/v/7/P/7
"""
#static const char test4[8]
TEST4 = (8,), """
BwcHB/n5+fk=
"""
#static const char test6[48][3]
TEST6 = (48, 3), """
AgcKBAcLBQcBBQcDAQcJAwcKBgcFAQcIBAcIAQcIAwcLBQcCBQcAAQcJBgcGAgcJBAcIAgcJAgcK
BgcHAwcKBAcLAwcLBgcE+vkE/fkL/PkL/fkK+vkH/vkK/vkJ/PkI/vkJ+vkG//kJ+/kA+/kC/fkL
//kI/PkI//kI+vkF/fkK//kJ+/kD+/kB/PkL/vkK
"""
#static const char test7[16][5]
TEST7 = (16, 5), """
AQIFBwEDBAUHAwQBBgcEBAEFBwACAwUHAgECBgcFAgMGBwYDBAYHB/38+vkH/v36+Qb//vr5Bf79
+/kC/P/7+QD8//r5BP38+/kD//77+QE=
"""
#static const char test10[6][3]
TEST10 = (6, 3), """
AgQHBQYHAQMHAQMHBQYHAgQH
"""
#static const char test12[24][4]
TEST12 = (24, 4), """
BAMHCwMCBwoCBgcFBgQHBwIBBwkFAgcBBQMHAgUBBwAFBAcDBgMHBgEGBwQBBAcIBAEHCAYBBwQD
BgcGBAUHAwEFBwADBQcCAgUHAQECBwkEBgcHBgIHBQIDBwoDBAcL
"""
#static const char test13[2][7]
TEST13 = (2, 7), """
AQIDBAUGBwIDBAEFBgc=
"""
#static const char subconfig13[64]
SUBCONFIG13 = (64,), """
AAECBwP/C/8ECP//Dv///wUJDBcP/xUmERT/JBohHiwGCg0TEP8ZJRIY/yMWIB0r////Iv//HCr/
H/8pGygnLQ==
"""

View file

@ -0,0 +1,469 @@
import numpy as np
from .._shared.utils import check_nD
from . import _moments_cy
import itertools
def moments_coords(coords, order=3):
"""Calculate all raw image moments up to a certain order.
The following properties can be calculated from raw image moments:
* Area as: ``M[0, 0]``.
* Centroid as: {``M[1, 0] / M[0, 0]``, ``M[0, 1] / M[0, 0]``}.
Note that raw moments are neither translation, scale nor rotation
invariant.
Parameters
----------
coords : (N, D) double or uint8 array
Array of N points that describe an image of D dimensionality in
Cartesian space.
order : int, optional
Maximum order of moments. Default is 3.
Returns
-------
M : (``order + 1``, ``order + 1``, ...) array
Raw image moments. (D dimensions)
References
----------
.. [1] Johannes Kilian. Simple Image Analysis By Moments. Durham
University, version 0.2, Durham, 2001.
Examples
--------
>>> coords = np.array([[row, col]
... for row in range(13, 17)
... for col in range(14, 18)], dtype=np.double)
>>> M = moments_coords(coords)
>>> centroid = (M[1, 0] / M[0, 0], M[0, 1] / M[0, 0])
>>> centroid
(14.5, 15.5)
"""
return moments_coords_central(coords, 0, order=order)
def moments_coords_central(coords, center=None, order=3):
"""Calculate all central image moments up to a certain order.
The following properties can be calculated from raw image moments:
* Area as: ``M[0, 0]``.
* Centroid as: {``M[1, 0] / M[0, 0]``, ``M[0, 1] / M[0, 0]``}.
Note that raw moments are neither translation, scale nor rotation
invariant.
Parameters
----------
coords : (N, D) double or uint8 array
Array of N points that describe an image of D dimensionality in
Cartesian space. A tuple of coordinates as returned by
``np.nonzero`` is also accepted as input.
center : tuple of float, optional
Coordinates of the image centroid. This will be computed if it
is not provided.
order : int, optional
Maximum order of moments. Default is 3.
Returns
-------
Mc : (``order + 1``, ``order + 1``, ...) array
Central image moments. (D dimensions)
References
----------
.. [1] Johannes Kilian. Simple Image Analysis By Moments. Durham
University, version 0.2, Durham, 2001.
Examples
--------
>>> coords = np.array([[row, col]
... for row in range(13, 17)
... for col in range(14, 18)])
>>> moments_coords_central(coords)
array([[16., 0., 20., 0.],
[ 0., 0., 0., 0.],
[20., 0., 25., 0.],
[ 0., 0., 0., 0.]])
As seen above, for symmetric objects, odd-order moments (columns 1 and 3,
rows 1 and 3) are zero when centered on the centroid, or center of mass,
of the object (the default). If we break the symmetry by adding a new
point, this no longer holds:
>>> coords2 = np.concatenate((coords, [[17, 17]]), axis=0)
>>> np.round(moments_coords_central(coords2),
... decimals=2) # doctest: +NORMALIZE_WHITESPACE
array([[17. , 0. , 22.12, -2.49],
[ 0. , 3.53, 1.73, 7.4 ],
[25.88, 6.02, 36.63, 8.83],
[ 4.15, 19.17, 14.8 , 39.6 ]])
Image moments and central image moments are equivalent (by definition)
when the center is (0, 0):
>>> np.allclose(moments_coords(coords),
... moments_coords_central(coords, (0, 0)))
True
"""
if isinstance(coords, tuple):
# This format corresponds to coordinate tuples as returned by
# e.g. np.nonzero: (row_coords, column_coords).
# We represent them as an npoints x ndim array.
coords = np.transpose(coords)
check_nD(coords, 2)
ndim = coords.shape[1]
if center is None:
center = np.mean(coords, axis=0)
# center the coordinates
coords = coords.astype(float) - center
# generate all possible exponents for each axis in the given set of points
# produces a matrix of shape (N, D, order + 1)
coords = coords[..., np.newaxis] ** np.arange(order + 1)
# add extra dimensions for proper broadcasting
coords = coords.reshape(coords.shape + (1,) * (ndim - 1))
calc = 1
for axis in range(ndim):
# isolate each point's axis
isolated_axis = coords[:, axis]
# rotate orientation of matrix for proper broadcasting
isolated_axis = np.moveaxis(isolated_axis, 1, 1 + axis)
# calculate the moments for each point, one axis at a time
calc = calc * isolated_axis
# sum all individual point moments to get our final answer
Mc = np.sum(calc, axis=0)
return Mc
def moments(image, order=3):
"""Calculate all raw image moments up to a certain order.
The following properties can be calculated from raw image moments:
* Area as: ``M[0, 0]``.
* Centroid as: {``M[1, 0] / M[0, 0]``, ``M[0, 1] / M[0, 0]``}.
Note that raw moments are neither translation, scale nor rotation
invariant.
Parameters
----------
image : nD double or uint8 array
Rasterized shape as image.
order : int, optional
Maximum order of moments. Default is 3.
Returns
-------
m : (``order + 1``, ``order + 1``) array
Raw image moments.
References
----------
.. [1] Wilhelm Burger, Mark Burge. Principles of Digital Image Processing:
Core Algorithms. Springer-Verlag, London, 2009.
.. [2] B. Jähne. Digital Image Processing. Springer-Verlag,
Berlin-Heidelberg, 6. edition, 2005.
.. [3] T. H. Reiss. Recognizing Planar Objects Using Invariant Image
Features, from Lecture notes in computer science, p. 676. Springer,
Berlin, 1993.
.. [4] https://en.wikipedia.org/wiki/Image_moment
Examples
--------
>>> image = np.zeros((20, 20), dtype=np.double)
>>> image[13:17, 13:17] = 1
>>> M = moments(image)
>>> centroid = (M[1, 0] / M[0, 0], M[0, 1] / M[0, 0])
>>> centroid
(14.5, 14.5)
"""
return moments_central(image, (0,) * image.ndim, order=order)
def moments_central(image, center=None, order=3, **kwargs):
"""Calculate all central image moments up to a certain order.
The center coordinates (cr, cc) can be calculated from the raw moments as:
{``M[1, 0] / M[0, 0]``, ``M[0, 1] / M[0, 0]``}.
Note that central moments are translation invariant but not scale and
rotation invariant.
Parameters
----------
image : nD double or uint8 array
Rasterized shape as image.
center : tuple of float, optional
Coordinates of the image centroid. This will be computed if it
is not provided.
order : int, optional
The maximum order of moments computed.
Returns
-------
mu : (``order + 1``, ``order + 1``) array
Central image moments.
References
----------
.. [1] Wilhelm Burger, Mark Burge. Principles of Digital Image Processing:
Core Algorithms. Springer-Verlag, London, 2009.
.. [2] B. Jähne. Digital Image Processing. Springer-Verlag,
Berlin-Heidelberg, 6. edition, 2005.
.. [3] T. H. Reiss. Recognizing Planar Objects Using Invariant Image
Features, from Lecture notes in computer science, p. 676. Springer,
Berlin, 1993.
.. [4] https://en.wikipedia.org/wiki/Image_moment
Examples
--------
>>> image = np.zeros((20, 20), dtype=np.double)
>>> image[13:17, 13:17] = 1
>>> M = moments(image)
>>> centroid = (M[1, 0] / M[0, 0], M[0, 1] / M[0, 0])
>>> moments_central(image, centroid)
array([[16., 0., 20., 0.],
[ 0., 0., 0., 0.],
[20., 0., 25., 0.],
[ 0., 0., 0., 0.]])
"""
if center is None:
center = centroid(image)
calc = image.astype(float)
for dim, dim_length in enumerate(image.shape):
delta = np.arange(dim_length, dtype=float) - center[dim]
powers_of_delta = delta[:, np.newaxis] ** np.arange(order + 1)
calc = np.rollaxis(calc, dim, image.ndim)
calc = np.dot(calc, powers_of_delta)
calc = np.rollaxis(calc, -1, dim)
return calc
def moments_normalized(mu, order=3):
"""Calculate all normalized central image moments up to a certain order.
Note that normalized central moments are translation and scale invariant
but not rotation invariant.
Parameters
----------
mu : (M,[ ...,] M) array
Central image moments, where M must be greater than or equal
to ``order``.
order : int, optional
Maximum order of moments. Default is 3.
Returns
-------
nu : (``order + 1``,[ ...,] ``order + 1``) array
Normalized central image moments.
References
----------
.. [1] Wilhelm Burger, Mark Burge. Principles of Digital Image Processing:
Core Algorithms. Springer-Verlag, London, 2009.
.. [2] B. Jähne. Digital Image Processing. Springer-Verlag,
Berlin-Heidelberg, 6. edition, 2005.
.. [3] T. H. Reiss. Recognizing Planar Objects Using Invariant Image
Features, from Lecture notes in computer science, p. 676. Springer,
Berlin, 1993.
.. [4] https://en.wikipedia.org/wiki/Image_moment
Examples
--------
>>> image = np.zeros((20, 20), dtype=np.double)
>>> image[13:17, 13:17] = 1
>>> m = moments(image)
>>> centroid = (m[0, 1] / m[0, 0], m[1, 0] / m[0, 0])
>>> mu = moments_central(image, centroid)
>>> moments_normalized(mu)
array([[ nan, nan, 0.078125 , 0. ],
[ nan, 0. , 0. , 0. ],
[0.078125 , 0. , 0.00610352, 0. ],
[0. , 0. , 0. , 0. ]])
"""
if np.any(np.array(mu.shape) <= order):
raise ValueError("Shape of image moments must be >= `order`")
nu = np.zeros_like(mu)
mu0 = mu.ravel()[0]
for powers in itertools.product(range(order + 1), repeat=mu.ndim):
if sum(powers) < 2:
nu[powers] = np.nan
else:
nu[powers] = mu[powers] / (mu0 ** (sum(powers) / nu.ndim + 1))
return nu
def moments_hu(nu):
"""Calculate Hu's set of image moments (2D-only).
Note that this set of moments is proofed to be translation, scale and
rotation invariant.
Parameters
----------
nu : (M, M) array
Normalized central image moments, where M must be >= 4.
Returns
-------
nu : (7,) array
Hu's set of image moments.
References
----------
.. [1] M. K. Hu, "Visual Pattern Recognition by Moment Invariants",
IRE Trans. Info. Theory, vol. IT-8, pp. 179-187, 1962
.. [2] Wilhelm Burger, Mark Burge. Principles of Digital Image Processing:
Core Algorithms. Springer-Verlag, London, 2009.
.. [3] B. Jähne. Digital Image Processing. Springer-Verlag,
Berlin-Heidelberg, 6. edition, 2005.
.. [4] T. H. Reiss. Recognizing Planar Objects Using Invariant Image
Features, from Lecture notes in computer science, p. 676. Springer,
Berlin, 1993.
.. [5] https://en.wikipedia.org/wiki/Image_moment
Examples
--------
>>> image = np.zeros((20, 20), dtype=np.double)
>>> image[13:17, 13:17] = 0.5
>>> image[10:12, 10:12] = 1
>>> mu = moments_central(image)
>>> nu = moments_normalized(mu)
>>> moments_hu(nu)
array([7.45370370e-01, 3.51165981e-01, 1.04049179e-01, 4.06442107e-02,
2.64312299e-03, 2.40854582e-02, 4.33680869e-19])
"""
return _moments_cy.moments_hu(nu.astype(np.double))
def centroid(image):
"""Return the (weighted) centroid of an image.
Parameters
----------
image : array
The input image.
Returns
-------
center : tuple of float, length ``image.ndim``
The centroid of the (nonzero) pixels in ``image``.
Examples
--------
>>> image = np.zeros((20, 20), dtype=np.double)
>>> image[13:17, 13:17] = 0.5
>>> image[10:12, 10:12] = 1
>>> centroid(image)
array([13.16666667, 13.16666667])
"""
M = moments_central(image, center=(0,) * image.ndim, order=1)
center = (M[tuple(np.eye(image.ndim, dtype=int))] # array of weighted sums
# for each axis
/ M[(0,) * image.ndim]) # weighted sum of all points
return center
def inertia_tensor(image, mu=None):
"""Compute the inertia tensor of the input image.
Parameters
----------
image : array
The input image.
mu : array, optional
The pre-computed central moments of ``image``. The inertia tensor
computation requires the central moments of the image. If an
application requires both the central moments and the inertia tensor
(for example, `skimage.measure.regionprops`), then it is more
efficient to pre-compute them and pass them to the inertia tensor
call.
Returns
-------
T : array, shape ``(image.ndim, image.ndim)``
The inertia tensor of the input image. :math:`T_{i, j}` contains
the covariance of image intensity along axes :math:`i` and :math:`j`.
References
----------
.. [1] https://en.wikipedia.org/wiki/Moment_of_inertia#Inertia_tensor
.. [2] Bernd Jähne. Spatio-Temporal Image Processing: Theory and
Scientific Applications. (Chapter 8: Tensor Methods) Springer, 1993.
"""
if mu is None:
mu = moments_central(image, order=2) # don't need higher-order moments
mu0 = mu[(0,) * image.ndim]
result = np.zeros((image.ndim, image.ndim))
# nD expression to get coordinates ([2, 0], [0, 2]) (2D),
# ([2, 0, 0], [0, 2, 0], [0, 0, 2]) (3D), etc.
corners2 = tuple(2 * np.eye(image.ndim, dtype=int))
d = np.diag(result)
d.flags.writeable = True
# See https://ocw.mit.edu/courses/aeronautics-and-astronautics/
# 16-07-dynamics-fall-2009/lecture-notes/MIT16_07F09_Lec26.pdf
# Iii is the sum of second-order moments of every axis *except* i, not the
# second order moment of axis i.
# See also https://github.com/scikit-image/scikit-image/issues/3229
d[:] = (np.sum(mu[corners2]) - mu[corners2]) / mu0
for dims in itertools.combinations(range(image.ndim), 2):
mu_index = np.zeros(image.ndim, dtype=int)
mu_index[list(dims)] = 1
result[dims] = -mu[tuple(mu_index)] / mu0
result.T[dims] = -mu[tuple(mu_index)] / mu0
return result
def inertia_tensor_eigvals(image, mu=None, T=None):
"""Compute the eigenvalues of the inertia tensor of the image.
The inertia tensor measures covariance of the image intensity along
the image axes. (See `inertia_tensor`.) The relative magnitude of the
eigenvalues of the tensor is thus a measure of the elongation of a
(bright) object in the image.
Parameters
----------
image : array
The input image.
mu : array, optional
The pre-computed central moments of ``image``.
T : array, shape ``(image.ndim, image.ndim)``
The pre-computed inertia tensor. If ``T`` is given, ``mu`` and
``image`` are ignored.
Returns
-------
eigvals : list of float, length ``image.ndim``
The eigenvalues of the inertia tensor of ``image``, in descending
order.
Notes
-----
Computing the eigenvalues requires the inertia tensor of the input image.
This is much faster if the central moments (``mu``) are provided, or,
alternatively, one can provide the inertia tensor (``T``) directly.
"""
if T is None:
T = inertia_tensor(image, mu)
eigvals = np.linalg.eigvalsh(T)
# Floating point precision problems could make a positive
# semidefinite matrix have an eigenvalue that is very slightly
# negative. This can cause problems down the line, so set values
# very near zero to zero.
eigvals = np.clip(eigvals, 0, None, out=eigvals)
return sorted(eigvals, reverse=True)

View file

@ -0,0 +1,168 @@
import numpy as np
from scipy import signal
def approximate_polygon(coords, tolerance):
"""Approximate a polygonal chain with the specified tolerance.
It is based on the Douglas-Peucker algorithm.
Note that the approximated polygon is always within the convex hull of the
original polygon.
Parameters
----------
coords : (N, 2) array
Coordinate array.
tolerance : float
Maximum distance from original points of polygon to approximated
polygonal chain. If tolerance is 0, the original coordinate array
is returned.
Returns
-------
coords : (M, 2) array
Approximated polygonal chain where M <= N.
References
----------
.. [1] https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm
"""
if tolerance <= 0:
return coords
chain = np.zeros(coords.shape[0], 'bool')
# pre-allocate distance array for all points
dists = np.zeros(coords.shape[0])
chain[0] = True
chain[-1] = True
pos_stack = [(0, chain.shape[0] - 1)]
end_of_chain = False
while not end_of_chain:
start, end = pos_stack.pop()
# determine properties of current line segment
r0, c0 = coords[start, :]
r1, c1 = coords[end, :]
dr = r1 - r0
dc = c1 - c0
segment_angle = - np.arctan2(dr, dc)
segment_dist = c0 * np.sin(segment_angle) + r0 * np.cos(segment_angle)
# select points in-between line segment
segment_coords = coords[start + 1:end, :]
segment_dists = dists[start + 1:end]
# check whether to take perpendicular or euclidean distance with
# inner product of vectors
# vectors from points -> start and end
dr0 = segment_coords[:, 0] - r0
dc0 = segment_coords[:, 1] - c0
dr1 = segment_coords[:, 0] - r1
dc1 = segment_coords[:, 1] - c1
# vectors points -> start and end projected on start -> end vector
projected_lengths0 = dr0 * dr + dc0 * dc
projected_lengths1 = - dr1 * dr - dc1 * dc
perp = np.logical_and(projected_lengths0 > 0,
projected_lengths1 > 0)
eucl = np.logical_not(perp)
segment_dists[perp] = np.abs(
segment_coords[perp, 0] * np.cos(segment_angle)
+ segment_coords[perp, 1] * np.sin(segment_angle)
- segment_dist
)
segment_dists[eucl] = np.minimum(
# distance to start point
np.sqrt(dc0[eucl] ** 2 + dr0[eucl] ** 2),
# distance to end point
np.sqrt(dc1[eucl] ** 2 + dr1[eucl] ** 2)
)
if np.any(segment_dists > tolerance):
# select point with maximum distance to line
new_end = start + np.argmax(segment_dists) + 1
pos_stack.append((new_end, end))
pos_stack.append((start, new_end))
chain[new_end] = True
if len(pos_stack) == 0:
end_of_chain = True
return coords[chain, :]
# B-Spline subdivision
_SUBDIVISION_MASKS = {
# degree: (mask_even, mask_odd)
# extracted from (degree + 2)th row of Pascal's triangle
1: ([1, 1], [1, 1]),
2: ([3, 1], [1, 3]),
3: ([1, 6, 1], [0, 4, 4]),
4: ([5, 10, 1], [1, 10, 5]),
5: ([1, 15, 15, 1], [0, 6, 20, 6]),
6: ([7, 35, 21, 1], [1, 21, 35, 7]),
7: ([1, 28, 70, 28, 1], [0, 8, 56, 56, 8]),
}
def subdivide_polygon(coords, degree=2, preserve_ends=False):
"""Subdivision of polygonal curves using B-Splines.
Note that the resulting curve is always within the convex hull of the
original polygon. Circular polygons stay closed after subdivision.
Parameters
----------
coords : (N, 2) array
Coordinate array.
degree : {1, 2, 3, 4, 5, 6, 7}, optional
Degree of B-Spline. Default is 2.
preserve_ends : bool, optional
Preserve first and last coordinate of non-circular polygon. Default is
False.
Returns
-------
coords : (M, 2) array
Subdivided coordinate array.
References
----------
.. [1] http://mrl.nyu.edu/publications/subdiv-course2000/coursenotes00.pdf
"""
if degree not in _SUBDIVISION_MASKS:
raise ValueError("Invalid B-Spline degree. Only degree 1 - 7 is "
"supported.")
circular = np.all(coords[0, :] == coords[-1, :])
method = 'valid'
if circular:
# remove last coordinate because of wrapping
coords = coords[:-1, :]
# circular convolution by wrapping boundaries
method = 'same'
mask_even, mask_odd = _SUBDIVISION_MASKS[degree]
# divide by total weight
mask_even = np.array(mask_even, np.float) / (2 ** degree)
mask_odd = np.array(mask_odd, np.float) / (2 ** degree)
even = signal.convolve2d(coords.T, np.atleast_2d(mask_even), mode=method,
boundary='wrap')
odd = signal.convolve2d(coords.T, np.atleast_2d(mask_odd), mode=method,
boundary='wrap')
out = np.zeros((even.shape[1] + odd.shape[1], 2))
out[1::2] = even.T
out[::2] = odd.T
if circular:
# close polygon
out = np.vstack([out, out[0, :]])
if preserve_ends and not circular:
out = np.vstack([coords[0, :], out, coords[-1, :]])
return out

View file

@ -0,0 +1,987 @@
from warnings import warn
from math import sqrt, atan2, pi as PI
import numpy as np
from scipy import ndimage as ndi
from ._label import label
from . import _moments
from functools import wraps
__all__ = ['regionprops', 'perimeter']
STREL_4 = np.array([[0, 1, 0],
[1, 1, 1],
[0, 1, 0]], dtype=np.uint8)
STREL_8 = np.ones((3, 3), dtype=np.uint8)
STREL_26_3D = np.ones((3, 3, 3), dtype=np.uint8)
PROPS = {
'Area': 'area',
'BoundingBox': 'bbox',
'BoundingBoxArea': 'bbox_area',
'CentralMoments': 'moments_central',
'Centroid': 'centroid',
'ConvexArea': 'convex_area',
# 'ConvexHull',
'ConvexImage': 'convex_image',
'Coordinates': 'coords',
'Eccentricity': 'eccentricity',
'EquivDiameter': 'equivalent_diameter',
'EulerNumber': 'euler_number',
'Extent': 'extent',
# 'Extrema',
'FilledArea': 'filled_area',
'FilledImage': 'filled_image',
'HuMoments': 'moments_hu',
'Image': 'image',
'InertiaTensor': 'inertia_tensor',
'InertiaTensorEigvals': 'inertia_tensor_eigvals',
'IntensityImage': 'intensity_image',
'Label': 'label',
'LocalCentroid': 'local_centroid',
'MajorAxisLength': 'major_axis_length',
'MaxIntensity': 'max_intensity',
'MeanIntensity': 'mean_intensity',
'MinIntensity': 'min_intensity',
'MinorAxisLength': 'minor_axis_length',
'Moments': 'moments',
'NormalizedMoments': 'moments_normalized',
'Orientation': 'orientation',
'Perimeter': 'perimeter',
# 'PixelIdxList',
# 'PixelList',
'Slice': 'slice',
'Solidity': 'solidity',
# 'SubarrayIdx'
'WeightedCentralMoments': 'weighted_moments_central',
'WeightedCentroid': 'weighted_centroid',
'WeightedHuMoments': 'weighted_moments_hu',
'WeightedLocalCentroid': 'weighted_local_centroid',
'WeightedMoments': 'weighted_moments',
'WeightedNormalizedMoments': 'weighted_moments_normalized'
}
OBJECT_COLUMNS = {
'image', 'coords', 'convex_image', 'slice',
'filled_image', 'intensity_image'
}
COL_DTYPES = {
'area': int,
'bbox': int,
'bbox_area': int,
'moments_central': float,
'centroid': float,
'convex_area': int,
'convex_image': object,
'coords': object,
'eccentricity': float,
'equivalent_diameter': float,
'euler_number': int,
'extent': float,
'filled_area': int,
'filled_image': object,
'moments_hu': float,
'image': object,
'inertia_tensor': float,
'inertia_tensor_eigvals': float,
'intensity_image': object,
'label': int,
'local_centroid': float,
'major_axis_length': float,
'max_intensity': int,
'mean_intensity': float,
'min_intensity': int,
'minor_axis_length': float,
'moments': float,
'moments_normalized': float,
'orientation': float,
'perimeter': float,
'slice': object,
'solidity': float,
'weighted_moments_central': float,
'weighted_centroid': float,
'weighted_moments_hu': float,
'weighted_local_centroid': float,
'weighted_moments': float,
'weighted_moments_normalized': float
}
PROP_VALS = set(PROPS.values())
def _cached(f):
@wraps(f)
def wrapper(obj):
cache = obj._cache
prop = f.__name__
if not ((prop in cache) and obj._cache_active):
cache[prop] = f(obj)
return cache[prop]
return wrapper
def only2d(method):
@wraps(method)
def func2d(self, *args, **kwargs):
if self._ndim > 2:
raise NotImplementedError('Property %s is not implemented for '
'3D images' % method.__name__)
return method(self, *args, **kwargs)
return func2d
class RegionProperties:
"""Please refer to `skimage.measure.regionprops` for more information
on the available region properties.
"""
def __init__(self, slice, label, label_image, intensity_image,
cache_active):
if intensity_image is not None:
if not intensity_image.shape == label_image.shape:
raise ValueError('Label and intensity image must have the'
' same shape.')
self.label = label
self._slice = slice
self.slice = slice
self._label_image = label_image
self._intensity_image = intensity_image
self._cache_active = cache_active
self._cache = {}
self._ndim = label_image.ndim
@property
@_cached
def area(self):
return np.sum(self.image)
@property
def bbox(self):
"""
Returns
-------
A tuple of the bounding box's start coordinates for each dimension,
followed by the end coordinates for each dimension
"""
return tuple([self.slice[i].start for i in range(self._ndim)] +
[self.slice[i].stop for i in range(self._ndim)])
@property
def bbox_area(self):
return self.image.size
@property
def centroid(self):
return tuple(self.coords.mean(axis=0))
@property
@_cached
def convex_area(self):
return np.sum(self.convex_image)
@property
@_cached
def convex_image(self):
from ..morphology.convex_hull import convex_hull_image
return convex_hull_image(self.image)
@property
def coords(self):
indices = np.nonzero(self.image)
return np.vstack([indices[i] + self.slice[i].start
for i in range(self._ndim)]).T
@property
@only2d
def eccentricity(self):
l1, l2 = self.inertia_tensor_eigvals
if l1 == 0:
return 0
return sqrt(1 - l2 / l1)
@property
def equivalent_diameter(self):
if self._ndim == 2:
return sqrt(4 * self.area / PI)
elif self._ndim == 3:
return (6 * self.area / PI) ** (1. / 3)
@property
def euler_number(self):
euler_array = self.filled_image != self.image
_, num = label(euler_array, connectivity=self._ndim, return_num=True,
background=0)
return -num + 1
@property
def extent(self):
return self.area / self.image.size
@property
def filled_area(self):
return np.sum(self.filled_image)
@property
@_cached
def filled_image(self):
structure = np.ones((3,) * self._ndim)
return ndi.binary_fill_holes(self.image, structure)
@property
@_cached
def image(self):
return self._label_image[self.slice] == self.label
@property
@_cached
def inertia_tensor(self):
mu = self.moments_central
return _moments.inertia_tensor(self.image, mu)
@property
@_cached
def inertia_tensor_eigvals(self):
return _moments.inertia_tensor_eigvals(self.image,
T=self.inertia_tensor)
@property
@_cached
def intensity_image(self):
if self._intensity_image is None:
raise AttributeError('No intensity image specified.')
return self._intensity_image[self.slice] * self.image
def _intensity_image_double(self):
return self.intensity_image.astype(np.double)
@property
def local_centroid(self):
M = self.moments
return tuple(M[tuple(np.eye(self._ndim, dtype=int))] /
M[(0,) * self._ndim])
@property
def max_intensity(self):
return np.max(self.intensity_image[self.image])
@property
def mean_intensity(self):
return np.mean(self.intensity_image[self.image])
@property
def min_intensity(self):
return np.min(self.intensity_image[self.image])
@property
def major_axis_length(self):
l1 = self.inertia_tensor_eigvals[0]
return 4 * sqrt(l1)
@property
def minor_axis_length(self):
l2 = self.inertia_tensor_eigvals[-1]
return 4 * sqrt(l2)
@property
@_cached
def moments(self):
M = _moments.moments(self.image.astype(np.uint8), 3)
return M
@property
@_cached
def moments_central(self):
mu = _moments.moments_central(self.image.astype(np.uint8),
self.local_centroid, order=3)
return mu
@property
@only2d
def moments_hu(self):
return _moments.moments_hu(self.moments_normalized)
@property
@_cached
def moments_normalized(self):
return _moments.moments_normalized(self.moments_central, 3)
@property
@only2d
def orientation(self):
a, b, b, c = self.inertia_tensor.flat
if a - c == 0:
if b < 0:
return -PI / 4.
else:
return PI / 4.
else:
return 0.5 * atan2(-2 * b, c - a)
@property
@only2d
def perimeter(self):
return perimeter(self.image, 4)
@property
def solidity(self):
return self.area / self.convex_area
@property
def weighted_centroid(self):
ctr = self.weighted_local_centroid
return tuple(idx + slc.start
for idx, slc in zip(ctr, self.slice))
@property
def weighted_local_centroid(self):
M = self.weighted_moments
return (M[tuple(np.eye(self._ndim, dtype=int))] /
M[(0,) * self._ndim])
@property
@_cached
def weighted_moments(self):
return _moments.moments(self._intensity_image_double(), 3)
@property
@_cached
def weighted_moments_central(self):
ctr = self.weighted_local_centroid
return _moments.moments_central(self._intensity_image_double(),
center=ctr, order=3)
@property
@only2d
def weighted_moments_hu(self):
return _moments.moments_hu(self.weighted_moments_normalized)
@property
@_cached
def weighted_moments_normalized(self):
return _moments.moments_normalized(self.weighted_moments_central, 3)
def __iter__(self):
props = PROP_VALS
if self._intensity_image is None:
unavailable_props = ('intensity_image',
'max_intensity',
'mean_intensity',
'min_intensity',
'weighted_moments',
'weighted_moments_central',
'weighted_centroid',
'weighted_local_centroid',
'weighted_moments_hu',
'weighted_moments_normalized')
props = props.difference(unavailable_props)
return iter(sorted(props))
def __getitem__(self, key):
value = getattr(self, key, None)
if value is not None:
return value
else: # backwards compatibility
return getattr(self, PROPS[key])
def __eq__(self, other):
if not isinstance(other, RegionProperties):
return False
for key in PROP_VALS:
try:
# so that NaNs are equal
np.testing.assert_equal(getattr(self, key, None),
getattr(other, key, None))
except AssertionError:
return False
return True
# For compatibility with code written prior to 0.16
_RegionProperties = RegionProperties
def _props_to_dict(regions, properties=('label', 'bbox'), separator='-'):
"""Convert image region properties list into a column dictionary.
Parameters
----------
regions : (N,) list
List of RegionProperties objects as returned by :func:`regionprops`.
properties : tuple or list of str, optional
Properties that will be included in the resulting dictionary
For a list of available properties, please see :func:`regionprops`.
Users should remember to add "label" to keep track of region
identities.
separator : str, optional
For non-scalar properties not listed in OBJECT_COLUMNS, each element
will appear in its own column, with the index of that element separated
from the property name by this separator. For example, the inertia
tensor of a 2D region will appear in four columns:
``inertia_tensor-0-0``, ``inertia_tensor-0-1``, ``inertia_tensor-1-0``,
and ``inertia_tensor-1-1`` (where the separator is ``-``).
Object columns are those that cannot be split in this way because the
number of columns would change depending on the object. For example,
``image`` and ``coords``.
Returns
-------
out_dict : dict
Dictionary mapping property names to an array of values of that
property, one value per region. This dictionary can be used as input to
pandas ``DataFrame`` to map property names to columns in the frame and
regions to rows.
Notes
-----
Each column contains either a scalar property, an object property, or an
element in a multidimensional array.
Properties with scalar values for each region, such as "eccentricity", will
appear as a float or int array with that property name as key.
Multidimensional properties *of fixed size* for a given image dimension,
such as "centroid" (every centroid will have three elements in a 3D image,
no matter the region size), will be split into that many columns, with the
name {property_name}{separator}{element_num} (for 1D properties),
{property_name}{separator}{elem_num0}{separator}{elem_num1} (for 2D
properties), and so on.
For multidimensional properties that don't have a fixed size, such as
"image" (the image of a region varies in size depending on the region
size), an object array will be used, with the corresponding property name
as the key.
Examples
--------
>>> from skimage import data, util, measure
>>> image = data.coins()
>>> label_image = measure.label(image > 110, connectivity=image.ndim)
>>> proplist = regionprops(label_image, image)
>>> props = _props_to_dict(proplist, properties=['label', 'inertia_tensor',
... 'inertia_tensor_eigvals'])
>>> props # doctest: +ELLIPSIS +SKIP
{'label': array([ 1, 2, ...]), ...
'inertia_tensor-0-0': array([ 4.012...e+03, 8.51..., ...]), ...
...,
'inertia_tensor_eigvals-1': array([ 2.67...e+02, 2.83..., ...])}
The resulting dictionary can be directly passed to pandas, if installed, to
obtain a clean DataFrame:
>>> import pandas as pd # doctest: +SKIP
>>> data = pd.DataFrame(props) # doctest: +SKIP
>>> data.head() # doctest: +SKIP
label inertia_tensor-0-0 ... inertia_tensor_eigvals-1
0 1 4012.909888 ... 267.065503
1 2 8.514739 ... 2.834806
2 3 0.666667 ... 0.000000
3 4 0.000000 ... 0.000000
4 5 0.222222 ... 0.111111
"""
out = {}
n = len(regions)
for prop in properties:
dtype = COL_DTYPES[prop]
column_buffer = np.zeros(n, dtype=dtype)
r = regions[0][prop]
# scalars and objects are dedicated one column per prop
# array properties are raveled into multiple columns
# for more info, refer to notes 1
if np.isscalar(r) or prop in OBJECT_COLUMNS:
for i in range(n):
column_buffer[i] = regions[i][prop]
out[prop] = np.copy(column_buffer)
else:
if isinstance(r, np.ndarray):
shape = r.shape
else:
shape = (len(r),)
for ind in np.ndindex(shape):
for k in range(n):
loc = ind if len(ind) > 1 else ind[0]
column_buffer[k] = regions[k][prop][loc]
modified_prop = separator.join(map(str, (prop,) + ind))
out[modified_prop] = np.copy(column_buffer)
return out
def regionprops_table(label_image, intensity_image=None,
properties=('label', 'bbox'),
*,
cache=True, separator='-'):
"""Compute image properties and return them as a pandas-compatible table.
The table is a dictionary mapping column names to value arrays. See Notes
section below for details.
Parameters
----------
label_image : (N, M) ndarray
Labeled input image. Labels with value 0 are ignored.
intensity_image : (N, M) ndarray, optional
Intensity (i.e., input) image with same size as labeled image.
Default is None.
properties : tuple or list of str, optional
Properties that will be included in the resulting dictionary
For a list of available properties, please see :func:`regionprops`.
Users should remember to add "label" to keep track of region
identities.
cache : bool, optional
Determine whether to cache calculated properties. The computation is
much faster for cached properties, whereas the memory consumption
increases.
separator : str, optional
For non-scalar properties not listed in OBJECT_COLUMNS, each element
will appear in its own column, with the index of that element separated
from the property name by this separator. For example, the inertia
tensor of a 2D region will appear in four columns:
``inertia_tensor-0-0``, ``inertia_tensor-0-1``, ``inertia_tensor-1-0``,
and ``inertia_tensor-1-1`` (where the separator is ``-``).
Object columns are those that cannot be split in this way because the
number of columns would change depending on the object. For example,
``image`` and ``coords``.
Returns
-------
out_dict : dict
Dictionary mapping property names to an array of values of that
property, one value per region. This dictionary can be used as input to
pandas ``DataFrame`` to map property names to columns in the frame and
regions to rows. If the image has no regions,
the arrays will have length 0, but the correct type.
Notes
-----
Each column contains either a scalar property, an object property, or an
element in a multidimensional array.
Properties with scalar values for each region, such as "eccentricity", will
appear as a float or int array with that property name as key.
Multidimensional properties *of fixed size* for a given image dimension,
such as "centroid" (every centroid will have three elements in a 3D image,
no matter the region size), will be split into that many columns, with the
name {property_name}{separator}{element_num} (for 1D properties),
{property_name}{separator}{elem_num0}{separator}{elem_num1} (for 2D
properties), and so on.
For multidimensional properties that don't have a fixed size, such as
"image" (the image of a region varies in size depending on the region
size), an object array will be used, with the corresponding property name
as the key.
Examples
--------
>>> from skimage import data, util, measure
>>> image = data.coins()
>>> label_image = measure.label(image > 110, connectivity=image.ndim)
>>> props = regionprops_table(label_image, image,
... properties=['label', 'inertia_tensor',
... 'inertia_tensor_eigvals'])
>>> props # doctest: +ELLIPSIS +SKIP
{'label': array([ 1, 2, ...]), ...
'inertia_tensor-0-0': array([ 4.012...e+03, 8.51..., ...]), ...
...,
'inertia_tensor_eigvals-1': array([ 2.67...e+02, 2.83..., ...])}
The resulting dictionary can be directly passed to pandas, if installed, to
obtain a clean DataFrame:
>>> import pandas as pd # doctest: +SKIP
>>> data = pd.DataFrame(props) # doctest: +SKIP
>>> data.head() # doctest: +SKIP
label inertia_tensor-0-0 ... inertia_tensor_eigvals-1
0 1 4012.909888 ... 267.065503
1 2 8.514739 ... 2.834806
2 3 0.666667 ... 0.000000
3 4 0.000000 ... 0.000000
4 5 0.222222 ... 0.111111
[5 rows x 7 columns]
"""
regions = regionprops(label_image, intensity_image=intensity_image,
cache=cache)
if len(regions) == 0:
label_image = np.zeros((3,) * label_image.ndim, dtype=int)
label_image[(1,) * label_image.ndim] = 1
if intensity_image is not None:
intensity_image = np.zeros(label_image.shape,
dtype=intensity_image.dtype)
regions = regionprops(label_image, intensity_image=intensity_image,
cache=cache)
out_d = _props_to_dict(regions, properties=properties,
separator=separator)
return {k: v[:0] for k, v in out_d.items()}
return _props_to_dict(regions, properties=properties, separator=separator)
def regionprops(label_image, intensity_image=None, cache=True,
coordinates=None):
r"""Measure properties of labeled image regions.
Parameters
----------
label_image : (N, M) ndarray
Labeled input image. Labels with value 0 are ignored.
.. versionchanged:: 0.14.1
Previously, ``label_image`` was processed by ``numpy.squeeze`` and
so any number of singleton dimensions was allowed. This resulted in
inconsistent handling of images with singleton dimensions. To
recover the old behaviour, use
``regionprops(np.squeeze(label_image), ...)``.
intensity_image : (N, M) ndarray, optional
Intensity (i.e., input) image with same size as labeled image.
Default is None.
cache : bool, optional
Determine whether to cache calculated properties. The computation is
much faster for cached properties, whereas the memory consumption
increases.
coordinates : DEPRECATED
This argument is deprecated and will be removed in a future version
of scikit-image.
See :ref:`Coordinate conventions <numpy-images-coordinate-conventions>`
for more details.
.. deprecated:: 0.16.0
Use "rc" coordinates everywhere. It may be sufficient to call
``numpy.transpose`` on your label image to get the same values as
0.15 and earlier. However, for some properties, the transformation
will be less trivial. For example, the new orientation is
:math:`\frac{\pi}{2}` plus the old orientation.
Returns
-------
properties : list of RegionProperties
Each item describes one labeled region, and can be accessed using the
attributes listed below.
Notes
-----
The following properties can be accessed as attributes or keys:
**area** : int
Number of pixels of the region.
**bbox** : tuple
Bounding box ``(min_row, min_col, max_row, max_col)``.
Pixels belonging to the bounding box are in the half-open interval
``[min_row; max_row)`` and ``[min_col; max_col)``.
**bbox_area** : int
Number of pixels of bounding box.
**centroid** : array
Centroid coordinate tuple ``(row, col)``.
**convex_area** : int
Number of pixels of convex hull image, which is the smallest convex
polygon that encloses the region.
**convex_image** : (H, J) ndarray
Binary convex hull image which has the same size as bounding box.
**coords** : (N, 2) ndarray
Coordinate list ``(row, col)`` of the region.
**eccentricity** : float
Eccentricity of the ellipse that has the same second-moments as the
region. The eccentricity is the ratio of the focal distance
(distance between focal points) over the major axis length.
The value is in the interval [0, 1).
When it is 0, the ellipse becomes a circle.
**equivalent_diameter** : float
The diameter of a circle with the same area as the region.
**euler_number** : int
Euler characteristic of region. Computed as number of objects (= 1)
subtracted by number of holes (8-connectivity).
**extent** : float
Ratio of pixels in the region to pixels in the total bounding box.
Computed as ``area / (rows * cols)``
**filled_area** : int
Number of pixels of the region will all the holes filled in. Describes
the area of the filled_image.
**filled_image** : (H, J) ndarray
Binary region image with filled holes which has the same size as
bounding box.
**image** : (H, J) ndarray
Sliced binary region image which has the same size as bounding box.
**inertia_tensor** : ndarray
Inertia tensor of the region for the rotation around its mass.
**inertia_tensor_eigvals** : tuple
The eigenvalues of the inertia tensor in decreasing order.
**intensity_image** : ndarray
Image inside region bounding box.
**label** : int
The label in the labeled input image.
**local_centroid** : array
Centroid coordinate tuple ``(row, col)``, relative to region bounding
box.
**major_axis_length** : float
The length of the major axis of the ellipse that has the same
normalized second central moments as the region.
**max_intensity** : float
Value with the greatest intensity in the region.
**mean_intensity** : float
Value with the mean intensity in the region.
**min_intensity** : float
Value with the least intensity in the region.
**minor_axis_length** : float
The length of the minor axis of the ellipse that has the same
normalized second central moments as the region.
**moments** : (3, 3) ndarray
Spatial moments up to 3rd order::
m_ij = sum{ array(row, col) * row^i * col^j }
where the sum is over the `row`, `col` coordinates of the region.
**moments_central** : (3, 3) ndarray
Central moments (translation invariant) up to 3rd order::
mu_ij = sum{ array(row, col) * (row - row_c)^i * (col - col_c)^j }
where the sum is over the `row`, `col` coordinates of the region,
and `row_c` and `col_c` are the coordinates of the region's centroid.
**moments_hu** : tuple
Hu moments (translation, scale and rotation invariant).
**moments_normalized** : (3, 3) ndarray
Normalized moments (translation and scale invariant) up to 3rd order::
nu_ij = mu_ij / m_00^[(i+j)/2 + 1]
where `m_00` is the zeroth spatial moment.
**orientation** : float
Angle between the 0th axis (rows) and the major
axis of the ellipse that has the same second moments as the region,
ranging from `-pi/2` to `pi/2` counter-clockwise.
**perimeter** : float
Perimeter of object which approximates the contour as a line
through the centers of border pixels using a 4-connectivity.
**slice** : tuple of slices
A slice to extract the object from the source image.
**solidity** : float
Ratio of pixels in the region to pixels of the convex hull image.
**weighted_centroid** : array
Centroid coordinate tuple ``(row, col)`` weighted with intensity
image.
**weighted_local_centroid** : array
Centroid coordinate tuple ``(row, col)``, relative to region bounding
box, weighted with intensity image.
**weighted_moments** : (3, 3) ndarray
Spatial moments of intensity image up to 3rd order::
wm_ij = sum{ array(row, col) * row^i * col^j }
where the sum is over the `row`, `col` coordinates of the region.
**weighted_moments_central** : (3, 3) ndarray
Central moments (translation invariant) of intensity image up to
3rd order::
wmu_ij = sum{ array(row, col) * (row - row_c)^i * (col - col_c)^j }
where the sum is over the `row`, `col` coordinates of the region,
and `row_c` and `col_c` are the coordinates of the region's weighted
centroid.
**weighted_moments_hu** : tuple
Hu moments (translation, scale and rotation invariant) of intensity
image.
**weighted_moments_normalized** : (3, 3) ndarray
Normalized moments (translation and scale invariant) of intensity
image up to 3rd order::
wnu_ij = wmu_ij / wm_00^[(i+j)/2 + 1]
where ``wm_00`` is the zeroth spatial moment (intensity-weighted area).
Each region also supports iteration, so that you can do::
for prop in region:
print(prop, region[prop])
See Also
--------
label
References
----------
.. [1] Wilhelm Burger, Mark Burge. Principles of Digital Image Processing:
Core Algorithms. Springer-Verlag, London, 2009.
.. [2] B. Jähne. Digital Image Processing. Springer-Verlag,
Berlin-Heidelberg, 6. edition, 2005.
.. [3] T. H. Reiss. Recognizing Planar Objects Using Invariant Image
Features, from Lecture notes in computer science, p. 676. Springer,
Berlin, 1993.
.. [4] https://en.wikipedia.org/wiki/Image_moment
Examples
--------
>>> from skimage import data, util
>>> from skimage.measure import label
>>> img = util.img_as_ubyte(data.coins()) > 110
>>> label_img = label(img, connectivity=img.ndim)
>>> props = regionprops(label_img)
>>> # centroid of first labeled object
>>> props[0].centroid
(22.72987986048314, 81.91228523446583)
>>> # centroid of first labeled object
>>> props[0]['centroid']
(22.72987986048314, 81.91228523446583)
"""
if label_image.ndim not in (2, 3):
raise TypeError('Only 2-D and 3-D images supported.')
if not np.issubdtype(label_image.dtype, np.integer):
if np.issubdtype(label_image.dtype, np.bool_):
raise TypeError(
'Non-integer image types are ambiguous: '
'use skimage.measure.label to label the connected'
'components of label_image,'
'or label_image.astype(np.uint8) to interpret'
'the True values as a single label.')
else:
raise TypeError(
'Non-integer label_image types are ambiguous')
if coordinates is not None:
if coordinates == 'rc':
msg = ('The coordinates keyword argument to skimage.measure.'
'regionprops is deprecated. All features are now computed '
'in rc (row-column) coordinates. Please remove '
'`coordinates="rc"` from all calls to regionprops before '
'updating scikit-image.')
warn(msg, stacklevel=2, category=FutureWarning)
else:
msg = ('Values other than "rc" for the "coordinates" argument '
'to skimage.measure.regionprops are no longer supported. '
'You should update your code to use "rc" coordinates and '
'stop using the "coordinates" argument, or use skimage '
'version 0.15.x or earlier.')
raise ValueError(msg)
regions = []
objects = ndi.find_objects(label_image)
for i, sl in enumerate(objects):
if sl is None:
continue
label = i + 1
props = RegionProperties(sl, label, label_image, intensity_image,
cache)
regions.append(props)
return regions
def perimeter(image, neighbourhood=4):
"""Calculate total perimeter of all objects in binary image.
Parameters
----------
image : (N, M) ndarray
2D binary image.
neighbourhood : 4 or 8, optional
Neighborhood connectivity for border pixel determination. It is used to
compute the contour. A higher neighbourhood widens the border on which
the perimeter is computed.
Returns
-------
perimeter : float
Total perimeter of all objects in binary image.
References
----------
.. [1] K. Benkrid, D. Crookes. Design and FPGA Implementation of
a Perimeter Estimator. The Queen's University of Belfast.
http://www.cs.qub.ac.uk/~d.crookes/webpubs/papers/perimeter.doc
Examples
--------
>>> from skimage import data, util
>>> from skimage.measure import label
>>> # coins image (binary)
>>> img_coins = data.coins() > 110
>>> # total perimeter of all objects in the image
>>> perimeter(img_coins, neighbourhood=4) # doctest: +ELLIPSIS
7796.867...
>>> perimeter(img_coins, neighbourhood=8) # doctest: +ELLIPSIS
8806.268...
"""
if image.ndim != 2:
raise NotImplementedError('`perimeter` supports 2D images only')
if neighbourhood == 4:
strel = STREL_4
else:
strel = STREL_8
image = image.astype(np.uint8)
eroded_image = ndi.binary_erosion(image, strel, border_value=0)
border_image = image - eroded_image
perimeter_weights = np.zeros(50, dtype=np.double)
perimeter_weights[[5, 7, 15, 17, 25, 27]] = 1
perimeter_weights[[21, 33]] = sqrt(2)
perimeter_weights[[13, 23]] = (1 + sqrt(2)) / 2
perimeter_image = ndi.convolve(border_image, np.array([[10, 2, 10],
[ 2, 1, 2],
[10, 2, 10]]),
mode='constant', cval=0)
# You can also write
# return perimeter_weights[perimeter_image].sum()
# but that was measured as taking much longer than bincount + np.dot (5x
# as much time)
perimeter_histogram = np.bincount(perimeter_image.ravel(), minlength=50)
total_perimeter = perimeter_histogram @ perimeter_weights
return total_perimeter
def _parse_docs():
import re
import textwrap
doc = regionprops.__doc__ or ''
matches = re.finditer(r'\*\*(\w+)\*\* \:.*?\n(.*?)(?=\n [\*\S]+)',
doc, flags=re.DOTALL)
prop_doc = {m.group(1): textwrap.dedent(m.group(2)) for m in matches}
return prop_doc
def _install_properties_docs():
prop_doc = _parse_docs()
for p in [member for member in dir(RegionProperties)
if not member.startswith('_')]:
getattr(RegionProperties, p).__doc__ = prop_doc[p]
if __debug__:
# don't install docstrings when in optimized/non-debug mode
_install_properties_docs()

View file

@ -0,0 +1,36 @@
from warnings import warn
from ..metrics._structural_similarity import structural_similarity
__all__ = ['compare_ssim']
def compare_ssim(X, Y, win_size=None, gradient=False,
data_range=None, multichannel=False, gaussian_weights=False,
full=False, **kwargs):
warn('DEPRECATED: skimage.measure.compare_ssim has been moved to '
'skimage.metrics.structural_similarity. It will be removed from '
'skimage.measure in version 0.18.', stacklevel=2)
return structural_similarity(X, Y, win_size=win_size, gradient=gradient,
data_range=data_range,
multichannel=multichannel,
gaussian_weights=gaussian_weights, full=full,
**kwargs)
if structural_similarity.__doc__ is not None:
compare_ssim.__doc__ = structural_similarity.__doc__ + """
Warns
-----
Deprecated:
.. versionadded:: 0.16
This function is deprecated and will be
removed in scikit-image 0.18. Please use the function named
``structural_similarity`` from the ``metrics`` module instead.
See also
--------
skimage.metrics.structural_similarity
"""

View file

@ -0,0 +1,87 @@
import numpy as np
from ..util import view_as_blocks
def block_reduce(image, block_size, func=np.sum, cval=0, func_kwargs=None):
"""Downsample image by applying function `func` to local blocks.
This function is useful for max and mean pooling, for example.
Parameters
----------
image : ndarray
N-dimensional input image.
block_size : array_like
Array containing down-sampling integer factor along each axis.
func : callable
Function object which is used to calculate the return value for each
local block. This function must implement an ``axis`` parameter.
Primary functions are ``numpy.sum``, ``numpy.min``, ``numpy.max``,
``numpy.mean`` and ``numpy.median``. See also `func_kwargs`.
cval : float
Constant padding value if image is not perfectly divisible by the
block size.
func_kwargs : dict
Keyword arguments passed to `func`. Notably useful for passing dtype
argument to ``np.mean``. Takes dictionary of inputs, e.g.:
``func_kwargs={'dtype': np.float16})``.
Returns
-------
image : ndarray
Down-sampled image with same number of dimensions as input image.
Examples
--------
>>> from skimage.measure import block_reduce
>>> image = np.arange(3*3*4).reshape(3, 3, 4)
>>> image # doctest: +NORMALIZE_WHITESPACE
array([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]],
[[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 23]],
[[24, 25, 26, 27],
[28, 29, 30, 31],
[32, 33, 34, 35]]])
>>> block_reduce(image, block_size=(3, 3, 1), func=np.mean)
array([[[16., 17., 18., 19.]]])
>>> image_max1 = block_reduce(image, block_size=(1, 3, 4), func=np.max)
>>> image_max1 # doctest: +NORMALIZE_WHITESPACE
array([[[11]],
[[23]],
[[35]]])
>>> image_max2 = block_reduce(image, block_size=(3, 1, 4), func=np.max)
>>> image_max2 # doctest: +NORMALIZE_WHITESPACE
array([[[27],
[31],
[35]]])
"""
if len(block_size) != image.ndim:
raise ValueError("`block_size` must have the same length "
"as `image.shape`.")
if func_kwargs is None:
func_kwargs = {}
pad_width = []
for i in range(len(block_size)):
if block_size[i] < 1:
raise ValueError("Down-sampling factors must be >= 1. Use "
"`skimage.transform.resize` to up-sample an "
"image.")
if image.shape[i] % block_size[i] != 0:
after_width = block_size[i] - (image.shape[i] % block_size[i])
else:
after_width = 0
pad_width.append((0, after_width))
image = np.pad(image, pad_width=pad_width, mode='constant',
constant_values=cval)
blocked = view_as_blocks(image, block_size)
return func(blocked, axis=tuple(range(image.ndim, blocked.ndim)),
**func_kwargs)

View file

@ -0,0 +1,40 @@
from numpy import unique
from scipy.stats import entropy as scipy_entropy
def shannon_entropy(image, base=2):
"""Calculate the Shannon entropy of an image.
The Shannon entropy is defined as S = -sum(pk * log(pk)),
where pk are frequency/probability of pixels of value k.
Parameters
----------
image : (N, M) ndarray
Grayscale input image.
base : float, optional
The logarithmic base to use.
Returns
-------
entropy : float
Notes
-----
The returned value is measured in bits or shannon (Sh) for base=2, natural
unit (nat) for base=np.e and hartley (Hart) for base=10.
References
----------
.. [1] `https://en.wikipedia.org/wiki/Entropy_(information_theory) <https://en.wikipedia.org/wiki/Entropy_(information_theory)>`_
.. [2] https://en.wiktionary.org/wiki/Shannon_entropy
Examples
--------
>>> from skimage import data
>>> shannon_entropy(data.camera())
7.047955232423086
"""
_, counts = unique(image, return_counts=True)
return scipy_entropy(counts, base=base)

View file

@ -0,0 +1,876 @@
import math
import numpy as np
from numpy.linalg import inv, pinv
from scipy import optimize
from .._shared.utils import check_random_state
def _check_data_dim(data, dim):
if data.ndim != 2 or data.shape[1] != dim:
raise ValueError('Input data must have shape (N, %d).' % dim)
def _check_data_atleast_2D(data):
if data.ndim < 2 or data.shape[1] < 2:
raise ValueError('Input data must be at least 2D.')
def _norm_along_axis(x, axis):
"""NumPy < 1.8 does not support the `axis` argument for `np.linalg.norm`."""
return np.sqrt(np.einsum('ij,ij->i', x, x))
class BaseModel(object):
def __init__(self):
self.params = None
class LineModelND(BaseModel):
"""Total least squares estimator for N-dimensional lines.
In contrast to ordinary least squares line estimation, this estimator
minimizes the orthogonal distances of points to the estimated line.
Lines are defined by a point (origin) and a unit vector (direction)
according to the following vector equation::
X = origin + lambda * direction
Attributes
----------
params : tuple
Line model parameters in the following order `origin`, `direction`.
Examples
--------
>>> x = np.linspace(1, 2, 25)
>>> y = 1.5 * x + 3
>>> lm = LineModelND()
>>> lm.estimate(np.array([x, y]).T)
True
>>> tuple(np.round(lm.params, 5))
(array([1.5 , 5.25]), array([0.5547 , 0.83205]))
>>> res = lm.residuals(np.array([x, y]).T)
>>> np.abs(np.round(res, 9))
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.])
>>> np.round(lm.predict_y(x[:5]), 3)
array([4.5 , 4.562, 4.625, 4.688, 4.75 ])
>>> np.round(lm.predict_x(y[:5]), 3)
array([1. , 1.042, 1.083, 1.125, 1.167])
"""
def estimate(self, data):
"""Estimate line model from data.
This minimizes the sum of shortest (orthogonal) distances
from the given data points to the estimated line.
Parameters
----------
data : (N, dim) array
N points in a space of dimensionality dim >= 2.
Returns
-------
success : bool
True, if model estimation succeeds.
"""
_check_data_atleast_2D(data)
origin = data.mean(axis=0)
data = data - origin
if data.shape[0] == 2: # well determined
direction = data[1] - data[0]
norm = np.linalg.norm(direction)
if norm != 0: # this should not happen to be norm 0
direction /= norm
elif data.shape[0] > 2: # over-determined
# Note: with full_matrices=1 Python dies with joblib parallel_for.
_, _, v = np.linalg.svd(data, full_matrices=False)
direction = v[0]
else: # under-determined
raise ValueError('At least 2 input points needed.')
self.params = (origin, direction)
return True
def residuals(self, data, params=None):
"""Determine residuals of data to model.
For each point, the shortest (orthogonal) distance to the line is
returned. It is obtained by projecting the data onto the line.
Parameters
----------
data : (N, dim) array
N points in a space of dimension dim.
params : (2, ) array, optional
Optional custom parameter set in the form (`origin`, `direction`).
Returns
-------
residuals : (N, ) array
Residual for each data point.
"""
_check_data_atleast_2D(data)
if params is None:
if self.params is None:
raise ValueError('Parameters cannot be None')
params = self.params
if len(params) != 2:
raise ValueError('Parameters are defined by 2 sets.')
origin, direction = params
res = (data - origin) - \
((data - origin) @ direction)[..., np.newaxis] * direction
return _norm_along_axis(res, axis=1)
def predict(self, x, axis=0, params=None):
"""Predict intersection of the estimated line model with a hyperplane
orthogonal to a given axis.
Parameters
----------
x : (n, 1) array
Coordinates along an axis.
axis : int
Axis orthogonal to the hyperplane intersecting the line.
params : (2, ) array, optional
Optional custom parameter set in the form (`origin`, `direction`).
Returns
-------
data : (n, m) array
Predicted coordinates.
Raises
------
ValueError
If the line is parallel to the given axis.
"""
if params is None:
if self.params is None:
raise ValueError('Parameters cannot be None')
params = self.params
if len(params) != 2:
raise ValueError('Parameters are defined by 2 sets.')
origin, direction = params
if direction[axis] == 0:
# line parallel to axis
raise ValueError('Line parallel to axis %s' % axis)
l = (x - origin[axis]) / direction[axis]
data = origin + l[..., np.newaxis] * direction
return data
def predict_x(self, y, params=None):
"""Predict x-coordinates for 2D lines using the estimated model.
Alias for::
predict(y, axis=1)[:, 0]
Parameters
----------
y : array
y-coordinates.
params : (2, ) array, optional
Optional custom parameter set in the form (`origin`, `direction`).
Returns
-------
x : array
Predicted x-coordinates.
"""
x = self.predict(y, axis=1, params=params)[:, 0]
return x
def predict_y(self, x, params=None):
"""Predict y-coordinates for 2D lines using the estimated model.
Alias for::
predict(x, axis=0)[:, 1]
Parameters
----------
x : array
x-coordinates.
params : (2, ) array, optional
Optional custom parameter set in the form (`origin`, `direction`).
Returns
-------
y : array
Predicted y-coordinates.
"""
y = self.predict(x, axis=0, params=params)[:, 1]
return y
class CircleModel(BaseModel):
"""Total least squares estimator for 2D circles.
The functional model of the circle is::
r**2 = (x - xc)**2 + (y - yc)**2
This estimator minimizes the squared distances from all points to the
circle::
min{ sum((r - sqrt((x_i - xc)**2 + (y_i - yc)**2))**2) }
A minimum number of 3 points is required to solve for the parameters.
Attributes
----------
params : tuple
Circle model parameters in the following order `xc`, `yc`, `r`.
Examples
--------
>>> t = np.linspace(0, 2 * np.pi, 25)
>>> xy = CircleModel().predict_xy(t, params=(2, 3, 4))
>>> model = CircleModel()
>>> model.estimate(xy)
True
>>> tuple(np.round(model.params, 5))
(2.0, 3.0, 4.0)
>>> res = model.residuals(xy)
>>> np.abs(np.round(res, 9))
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.])
"""
def estimate(self, data):
"""Estimate circle model from data using total least squares.
Parameters
----------
data : (N, 2) array
N points with ``(x, y)`` coordinates, respectively.
Returns
-------
success : bool
True, if model estimation succeeds.
"""
_check_data_dim(data, dim=2)
x = data[:, 0]
y = data[:, 1]
# http://www.had2know.com/academics/best-fit-circle-least-squares.html
x2y2 = (x ** 2 + y ** 2)
sum_x = np.sum(x)
sum_y = np.sum(y)
sum_xy = np.sum(x * y)
m1 = np.array([[np.sum(x ** 2), sum_xy, sum_x],
[sum_xy, np.sum(y ** 2), sum_y],
[sum_x, sum_y, float(len(x))]])
m2 = np.array([[np.sum(x * x2y2),
np.sum(y * x2y2),
np.sum(x2y2)]]).T
a, b, c = pinv(m1) @ m2
a, b, c = a[0], b[0], c[0]
xc = a / 2
yc = b / 2
r = np.sqrt(4 * c + a ** 2 + b ** 2) / 2
self.params = (xc, yc, r)
return True
def residuals(self, data):
"""Determine residuals of data to model.
For each point the shortest distance to the circle is returned.
Parameters
----------
data : (N, 2) array
N points with ``(x, y)`` coordinates, respectively.
Returns
-------
residuals : (N, ) array
Residual for each data point.
"""
_check_data_dim(data, dim=2)
xc, yc, r = self.params
x = data[:, 0]
y = data[:, 1]
return r - np.sqrt((x - xc)**2 + (y - yc)**2)
def predict_xy(self, t, params=None):
"""Predict x- and y-coordinates using the estimated model.
Parameters
----------
t : array
Angles in circle in radians. Angles start to count from positive
x-axis to positive y-axis in a right-handed system.
params : (3, ) array, optional
Optional custom parameter set.
Returns
-------
xy : (..., 2) array
Predicted x- and y-coordinates.
"""
if params is None:
params = self.params
xc, yc, r = params
x = xc + r * np.cos(t)
y = yc + r * np.sin(t)
return np.concatenate((x[..., None], y[..., None]), axis=t.ndim)
class EllipseModel(BaseModel):
"""Total least squares estimator for 2D ellipses.
The functional model of the ellipse is::
xt = xc + a*cos(theta)*cos(t) - b*sin(theta)*sin(t)
yt = yc + a*sin(theta)*cos(t) + b*cos(theta)*sin(t)
d = sqrt((x - xt)**2 + (y - yt)**2)
where ``(xt, yt)`` is the closest point on the ellipse to ``(x, y)``. Thus
d is the shortest distance from the point to the ellipse.
The estimator is based on a least squares minimization. The optimal
solution is computed directly, no iterations are required. This leads
to a simple, stable and robust fitting method.
The ``params`` attribute contains the parameters in the following order::
xc, yc, a, b, theta
Attributes
----------
params : tuple
Ellipse model parameters in the following order `xc`, `yc`, `a`, `b`,
`theta`.
Examples
--------
>>> xy = EllipseModel().predict_xy(np.linspace(0, 2 * np.pi, 25),
... params=(10, 15, 4, 8, np.deg2rad(30)))
>>> ellipse = EllipseModel()
>>> ellipse.estimate(xy)
True
>>> np.round(ellipse.params, 2)
array([10. , 15. , 4. , 8. , 0.52])
>>> np.round(abs(ellipse.residuals(xy)), 5)
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.])
"""
def estimate(self, data):
"""Estimate circle model from data using total least squares.
Parameters
----------
data : (N, 2) array
N points with ``(x, y)`` coordinates, respectively.
Returns
-------
success : bool
True, if model estimation succeeds.
References
----------
.. [1] Halir, R.; Flusser, J. "Numerically stable direct least squares
fitting of ellipses". In Proc. 6th International Conference in
Central Europe on Computer Graphics and Visualization.
WSCG (Vol. 98, pp. 125-132).
"""
# Original Implementation: Ben Hammel, Nick Sullivan-Molina
# another REFERENCE: [2] http://mathworld.wolfram.com/Ellipse.html
_check_data_dim(data, dim=2)
x = data[:, 0]
y = data[:, 1]
# Quadratic part of design matrix [eqn. 15] from [1]
D1 = np.vstack([x ** 2, x * y, y ** 2]).T
# Linear part of design matrix [eqn. 16] from [1]
D2 = np.vstack([x, y, np.ones(len(x))]).T
# forming scatter matrix [eqn. 17] from [1]
S1 = D1.T @ D1
S2 = D1.T @ D2
S3 = D2.T @ D2
# Constraint matrix [eqn. 18]
C1 = np.array([[0., 0., 2.], [0., -1., 0.], [2., 0., 0.]])
try:
# Reduced scatter matrix [eqn. 29]
M = inv(C1) @ (S1 - S2 @ inv(S3) @ S2.T)
except np.linalg.LinAlgError: # LinAlgError: Singular matrix
return False
# M*|a b c >=l|a b c >. Find eigenvalues and eigenvectors
# from this equation [eqn. 28]
eig_vals, eig_vecs = np.linalg.eig(M)
# eigenvector must meet constraint 4ac - b^2 to be valid.
cond = 4 * np.multiply(eig_vecs[0, :], eig_vecs[2, :]) \
- np.power(eig_vecs[1, :], 2)
a1 = eig_vecs[:, (cond > 0)]
# seeks for empty matrix
if 0 in a1.shape or len(a1.ravel()) != 3:
return False
a, b, c = a1.ravel()
# |d f g> = -S3^(-1)*S2^(T)*|a b c> [eqn. 24]
a2 = -inv(S3) @ S2.T @ a1
d, f, g = a2.ravel()
# eigenvectors are the coefficients of an ellipse in general form
# a*x^2 + 2*b*x*y + c*y^2 + 2*d*x + 2*f*y + g = 0 (eqn. 15) from [2]
b /= 2.
d /= 2.
f /= 2.
# finding center of ellipse [eqn.19 and 20] from [2]
x0 = (c * d - b * f) / (b ** 2. - a * c)
y0 = (a * f - b * d) / (b ** 2. - a * c)
# Find the semi-axes lengths [eqn. 21 and 22] from [2]
numerator = a * f ** 2 + c * d ** 2 + g * b ** 2 \
- 2 * b * d * f - a * c * g
term = np.sqrt((a - c) ** 2 + 4 * b ** 2)
denominator1 = (b ** 2 - a * c) * (term - (a + c))
denominator2 = (b ** 2 - a * c) * (- term - (a + c))
width = np.sqrt(2 * numerator / denominator1)
height = np.sqrt(2 * numerator / denominator2)
# angle of counterclockwise rotation of major-axis of ellipse
# to x-axis [eqn. 23] from [2].
phi = 0.5 * np.arctan((2. * b) / (a - c))
if a > c:
phi += 0.5 * np.pi
self.params = np.nan_to_num([x0, y0, width, height, phi]).tolist()
self.params = [float(np.real(x)) for x in self.params]
return True
def residuals(self, data):
"""Determine residuals of data to model.
For each point the shortest distance to the ellipse is returned.
Parameters
----------
data : (N, 2) array
N points with ``(x, y)`` coordinates, respectively.
Returns
-------
residuals : (N, ) array
Residual for each data point.
"""
_check_data_dim(data, dim=2)
xc, yc, a, b, theta = self.params
ctheta = math.cos(theta)
stheta = math.sin(theta)
x = data[:, 0]
y = data[:, 1]
N = data.shape[0]
def fun(t, xi, yi):
ct = math.cos(t)
st = math.sin(t)
xt = xc + a * ctheta * ct - b * stheta * st
yt = yc + a * stheta * ct + b * ctheta * st
return (xi - xt) ** 2 + (yi - yt) ** 2
# def Dfun(t, xi, yi):
# ct = math.cos(t)
# st = math.sin(t)
# xt = xc + a * ctheta * ct - b * stheta * st
# yt = yc + a * stheta * ct + b * ctheta * st
# dfx_t = - 2 * (xi - xt) * (- a * ctheta * st
# - b * stheta * ct)
# dfy_t = - 2 * (yi - yt) * (- a * stheta * st
# + b * ctheta * ct)
# return [dfx_t + dfy_t]
residuals = np.empty((N, ), dtype=np.double)
# initial guess for parameter t of closest point on ellipse
t0 = np.arctan2(y - yc, x - xc) - theta
# determine shortest distance to ellipse for each point
for i in range(N):
xi = x[i]
yi = y[i]
# faster without Dfun, because of the python overhead
t, _ = optimize.leastsq(fun, t0[i], args=(xi, yi))
residuals[i] = np.sqrt(fun(t, xi, yi))
return residuals
def predict_xy(self, t, params=None):
"""Predict x- and y-coordinates using the estimated model.
Parameters
----------
t : array
Angles in circle in radians. Angles start to count from positive
x-axis to positive y-axis in a right-handed system.
params : (5, ) array, optional
Optional custom parameter set.
Returns
-------
xy : (..., 2) array
Predicted x- and y-coordinates.
"""
if params is None:
params = self.params
xc, yc, a, b, theta = params
ct = np.cos(t)
st = np.sin(t)
ctheta = math.cos(theta)
stheta = math.sin(theta)
x = xc + a * ctheta * ct - b * stheta * st
y = yc + a * stheta * ct + b * ctheta * st
return np.concatenate((x[..., None], y[..., None]), axis=t.ndim)
def _dynamic_max_trials(n_inliers, n_samples, min_samples, probability):
"""Determine number trials such that at least one outlier-free subset is
sampled for the given inlier/outlier ratio.
Parameters
----------
n_inliers : int
Number of inliers in the data.
n_samples : int
Total number of samples in the data.
min_samples : int
Minimum number of samples chosen randomly from original data.
probability : float
Probability (confidence) that one outlier-free sample is generated.
Returns
-------
trials : int
Number of trials.
"""
if n_inliers == 0:
return np.inf
nom = 1 - probability
if nom == 0:
return np.inf
inlier_ratio = n_inliers / float(n_samples)
denom = 1 - inlier_ratio ** min_samples
if denom == 0:
return 1
elif denom == 1:
return np.inf
nom = np.log(nom)
denom = np.log(denom)
if denom == 0:
return 0
return int(np.ceil(nom / denom))
def ransac(data, model_class, min_samples, residual_threshold,
is_data_valid=None, is_model_valid=None,
max_trials=100, stop_sample_num=np.inf, stop_residuals_sum=0,
stop_probability=1, random_state=None, initial_inliers=None):
"""Fit a model to data with the RANSAC (random sample consensus) algorithm.
RANSAC is an iterative algorithm for the robust estimation of parameters
from a subset of inliers from the complete data set. Each iteration
performs the following tasks:
1. Select `min_samples` random samples from the original data and check
whether the set of data is valid (see `is_data_valid`).
2. Estimate a model to the random subset
(`model_cls.estimate(*data[random_subset]`) and check whether the
estimated model is valid (see `is_model_valid`).
3. Classify all data as inliers or outliers by calculating the residuals
to the estimated model (`model_cls.residuals(*data)`) - all data samples
with residuals smaller than the `residual_threshold` are considered as
inliers.
4. Save estimated model as best model if number of inlier samples is
maximal. In case the current estimated model has the same number of
inliers, it is only considered as the best model if it has less sum of
residuals.
These steps are performed either a maximum number of times or until one of
the special stop criteria are met. The final model is estimated using all
inlier samples of the previously determined best model.
Parameters
----------
data : [list, tuple of] (N, ...) array
Data set to which the model is fitted, where N is the number of data
points and the remaining dimension are depending on model requirements.
If the model class requires multiple input data arrays (e.g. source and
destination coordinates of ``skimage.transform.AffineTransform``),
they can be optionally passed as tuple or list. Note, that in this case
the functions ``estimate(*data)``, ``residuals(*data)``,
``is_model_valid(model, *random_data)`` and
``is_data_valid(*random_data)`` must all take each data array as
separate arguments.
model_class : object
Object with the following object methods:
* ``success = estimate(*data)``
* ``residuals(*data)``
where `success` indicates whether the model estimation succeeded
(`True` or `None` for success, `False` for failure).
min_samples : int in range (0, N)
The minimum number of data points to fit a model to.
residual_threshold : float larger than 0
Maximum distance for a data point to be classified as an inlier.
is_data_valid : function, optional
This function is called with the randomly selected data before the
model is fitted to it: `is_data_valid(*random_data)`.
is_model_valid : function, optional
This function is called with the estimated model and the randomly
selected data: `is_model_valid(model, *random_data)`, .
max_trials : int, optional
Maximum number of iterations for random sample selection.
stop_sample_num : int, optional
Stop iteration if at least this number of inliers are found.
stop_residuals_sum : float, optional
Stop iteration if sum of residuals is less than or equal to this
threshold.
stop_probability : float in range [0, 1], optional
RANSAC iteration stops if at least one outlier-free set of the
training data is sampled with ``probability >= stop_probability``,
depending on the current best model's inlier ratio and the number
of trials. This requires to generate at least N samples (trials):
N >= log(1 - probability) / log(1 - e**m)
where the probability (confidence) is typically set to a high value
such as 0.99, e is the current fraction of inliers w.r.t. the
total number of samples, and m is the min_samples value.
random_state : int, RandomState instance or None, optional
If int, random_state is the seed used by the random number generator;
If RandomState instance, random_state is the random number generator;
If None, the random number generator is the RandomState instance used
by `np.random`.
initial_inliers : array-like of bool, shape (N,), optional
Initial samples selection for model estimation
Returns
-------
model : object
Best model with largest consensus set.
inliers : (N, ) array
Boolean mask of inliers classified as ``True``.
References
----------
.. [1] "RANSAC", Wikipedia, https://en.wikipedia.org/wiki/RANSAC
Examples
--------
Generate ellipse data without tilt and add noise:
>>> t = np.linspace(0, 2 * np.pi, 50)
>>> xc, yc = 20, 30
>>> a, b = 5, 10
>>> x = xc + a * np.cos(t)
>>> y = yc + b * np.sin(t)
>>> data = np.column_stack([x, y])
>>> np.random.seed(seed=1234)
>>> data += np.random.normal(size=data.shape)
Add some faulty data:
>>> data[0] = (100, 100)
>>> data[1] = (110, 120)
>>> data[2] = (120, 130)
>>> data[3] = (140, 130)
Estimate ellipse model using all available data:
>>> model = EllipseModel()
>>> model.estimate(data)
True
>>> np.round(model.params) # doctest: +SKIP
array([ 72., 75., 77., 14., 1.])
Estimate ellipse model using RANSAC:
>>> ransac_model, inliers = ransac(data, EllipseModel, 20, 3, max_trials=50)
>>> abs(np.round(ransac_model.params))
array([20., 30., 5., 10., 0.])
>>> inliers # doctest: +SKIP
array([False, False, False, False, True, True, True, True, True,
True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True,
True, True, True, True, True], dtype=bool)
>>> sum(inliers) > 40
True
RANSAC can be used to robustly estimate a geometric transformation. In this section,
we also show how to use a proportion of the total samples, rather than an absolute number.
>>> from skimage.transform import SimilarityTransform
>>> np.random.seed(0)
>>> src = 100 * np.random.rand(50, 2)
>>> model0 = SimilarityTransform(scale=0.5, rotation=1, translation=(10, 20))
>>> dst = model0(src)
>>> dst[0] = (10000, 10000)
>>> dst[1] = (-100, 100)
>>> dst[2] = (50, 50)
>>> ratio = 0.5 # use half of the samples
>>> min_samples = int(ratio * len(src))
>>> model, inliers = ransac((src, dst), SimilarityTransform, min_samples, 10,
... initial_inliers=np.ones(len(src), dtype=bool))
>>> inliers
array([False, False, False, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True,
True, True, True, True, True, True, True, True, True,
True, True, True, True, True])
"""
best_model = None
best_inlier_num = 0
best_inlier_residuals_sum = np.inf
best_inliers = None
random_state = check_random_state(random_state)
# in case data is not pair of input and output, male it like it
if not isinstance(data, (tuple, list)):
data = (data, )
num_samples = len(data[0])
if not (0 < min_samples < num_samples):
raise ValueError("`min_samples` must be in range (0, <number-of-samples>)")
if residual_threshold < 0:
raise ValueError("`residual_threshold` must be greater than zero")
if max_trials < 0:
raise ValueError("`max_trials` must be greater than zero")
if not (0 <= stop_probability <= 1):
raise ValueError("`stop_probability` must be in range [0, 1]")
if initial_inliers is not None and len(initial_inliers) != num_samples:
raise ValueError("RANSAC received a vector of initial inliers (length %i)"
" that didn't match the number of samples (%i)."
" The vector of initial inliers should have the same length"
" as the number of samples and contain only True (this sample"
" is an initial inlier) and False (this one isn't) values."
% (len(initial_inliers), num_samples))
# for the first run use initial guess of inliers
spl_idxs = (initial_inliers if initial_inliers is not None
else random_state.choice(num_samples, min_samples, replace=False))
for num_trials in range(max_trials):
# do sample selection according data pairs
samples = [d[spl_idxs] for d in data]
# for next iteration choose random sample set and be sure that no samples repeat
spl_idxs = random_state.choice(num_samples, min_samples, replace=False)
# optional check if random sample set is valid
if is_data_valid is not None and not is_data_valid(*samples):
continue
# estimate model for current random sample set
sample_model = model_class()
success = sample_model.estimate(*samples)
# backwards compatibility
if success is not None and not success:
continue
# optional check if estimated model is valid
if is_model_valid is not None and not is_model_valid(sample_model, *samples):
continue
sample_model_residuals = np.abs(sample_model.residuals(*data))
# consensus set / inliers
sample_model_inliers = sample_model_residuals < residual_threshold
sample_model_residuals_sum = np.sum(sample_model_residuals ** 2)
# choose as new best model if number of inliers is maximal
sample_inlier_num = np.sum(sample_model_inliers)
if (
# more inliers
sample_inlier_num > best_inlier_num
# same number of inliers but less "error" in terms of residuals
or (sample_inlier_num == best_inlier_num
and sample_model_residuals_sum < best_inlier_residuals_sum)
):
best_model = sample_model
best_inlier_num = sample_inlier_num
best_inlier_residuals_sum = sample_model_residuals_sum
best_inliers = sample_model_inliers
dynamic_max_trials = _dynamic_max_trials(best_inlier_num,
num_samples,
min_samples,
stop_probability)
if (best_inlier_num >= stop_sample_num
or best_inlier_residuals_sum <= stop_residuals_sum
or num_trials >= dynamic_max_trials):
break
# estimate final model using all inliers
if best_inliers is not None:
# select inliers for each data array
data_inliers = [d[best_inliers] for d in data]
best_model.estimate(*data_inliers)
return best_model, best_inliers

View file

@ -0,0 +1,53 @@
from ._pnpoly import _grid_points_in_poly, _points_in_poly
def grid_points_in_poly(shape, verts):
"""Test whether points on a specified grid are inside a polygon.
For each ``(r, c)`` coordinate on a grid, i.e. ``(0, 0)``, ``(0, 1)`` etc.,
test whether that point lies inside a polygon.
Parameters
----------
shape : tuple (M, N)
Shape of the grid.
verts : (V, 2) array
Specify the V vertices of the polygon, sorted either clockwise
or anti-clockwise. The first point may (but does not need to be)
duplicated.
See Also
--------
points_in_poly
Returns
-------
mask : (M, N) ndarray of bool
True where the grid falls inside the polygon.
"""
return _grid_points_in_poly(shape, verts)
def points_in_poly(points, verts):
"""Test whether points lie inside a polygon.
Parameters
----------
points : (N, 2) array
Input points, ``(x, y)``.
verts : (M, 2) array
Vertices of the polygon, sorted either clockwise or anti-clockwise.
The first point may (but does not need to be) duplicated.
See Also
--------
grid_points_in_poly
Returns
-------
mask : (N,) array of bool
True if corresponding point is inside the polygon.
"""
return _points_in_poly(points, verts)

View file

@ -0,0 +1,174 @@
from warnings import warn
import numpy as np
from scipy import ndimage as ndi
from .._shared.utils import _validate_interpolation_order
def profile_line(image, src, dst, linewidth=1,
order=None, mode=None, cval=0.0,
*, reduce_func=np.mean):
"""Return the intensity profile of an image measured along a scan line.
Parameters
----------
image : ndarray, shape (M, N[, C])
The image, either grayscale (2D array) or multichannel
(3D array, where the final axis contains the channel
information).
src : array_like, shape (2, )
The coordinates of the start point of the scan line.
dst : array_like, shape (2, )
The coordinates of the end point of the scan
line. The destination point is *included* in the profile, in
contrast to standard numpy indexing.
linewidth : int, optional
Width of the scan, perpendicular to the line
order : int in {0, 1, 2, 3, 4, 5}, optional
The order of the spline interpolation, default is 0 if
image.dtype is bool and 1 otherwise. The order has to be in
the range 0-5. See `skimage.transform.warp` for detail.
mode : {'constant', 'nearest', 'reflect', 'mirror', 'wrap'}, optional
How to compute any values falling outside of the image.
cval : float, optional
If `mode` is 'constant', what constant value to use outside the image.
reduce_func : callable, optional
Function used to calculate the aggregation of pixel values
perpendicular to the profile_line direction when `linewidth` > 1.
If set to None the unreduced array will be returned.
Returns
-------
return_value : array
The intensity profile along the scan line. The length of the profile
is the ceil of the computed length of the scan line.
Examples
--------
>>> x = np.array([[1, 1, 1, 2, 2, 2]])
>>> img = np.vstack([np.zeros_like(x), x, x, x, np.zeros_like(x)])
>>> img
array([[0, 0, 0, 0, 0, 0],
[1, 1, 1, 2, 2, 2],
[1, 1, 1, 2, 2, 2],
[1, 1, 1, 2, 2, 2],
[0, 0, 0, 0, 0, 0]])
>>> profile_line(img, (2, 1), (2, 4))
array([1., 1., 2., 2.])
>>> profile_line(img, (1, 0), (1, 6), cval=4)
array([1., 1., 1., 2., 2., 2., 4.])
The destination point is included in the profile, in contrast to
standard numpy indexing.
For example:
>>> profile_line(img, (1, 0), (1, 6)) # The final point is out of bounds
array([1., 1., 1., 2., 2., 2., 0.])
>>> profile_line(img, (1, 0), (1, 5)) # This accesses the full first row
array([1., 1., 1., 2., 2., 2.])
For different reduce_func inputs:
>>> profile_line(img, (1, 0), (1, 3), linewidth=3, reduce_func=np.mean)
array([0.66666667, 0.66666667, 0.66666667, 1.33333333])
>>> profile_line(img, (1, 0), (1, 3), linewidth=3, reduce_func=np.max)
array([1, 1, 1, 2])
>>> profile_line(img, (1, 0), (1, 3), linewidth=3, reduce_func=np.sum)
array([2, 2, 2, 4])
The unreduced array will be returned when `reduce_func` is None or when
`reduce_func` acts on each pixel value individually.
>>> profile_line(img, (1, 2), (4, 2), linewidth=3, order=0,
... reduce_func=None)
array([[1, 1, 2],
[1, 1, 2],
[1, 1, 2],
[0, 0, 0]])
>>> profile_line(img, (1, 0), (1, 3), linewidth=3, reduce_func=np.sqrt)
array([[1. , 1. , 0. ],
[1. , 1. , 0. ],
[1. , 1. , 0. ],
[1.41421356, 1.41421356, 0. ]])
"""
order = _validate_interpolation_order(image.dtype, order)
if mode is None:
warn("Default out of bounds interpolation mode 'constant' is "
"deprecated. In version 0.19 it will be set to 'reflect'. "
"To avoid this warning, set `mode=` explicitly.",
FutureWarning, stacklevel=2)
mode = 'constant'
perp_lines = _line_profile_coordinates(src, dst, linewidth=linewidth)
if image.ndim == 3:
pixels = [ndi.map_coordinates(image[..., i], perp_lines,
prefilter=order > 1,
order=order, mode=mode,
cval=cval) for i in
range(image.shape[2])]
pixels = np.transpose(np.asarray(pixels), (1, 2, 0))
else:
pixels = ndi.map_coordinates(image, perp_lines, prefilter=order > 1,
order=order, mode=mode, cval=cval)
# The outputted array with reduce_func=None gives an array where the
# row values (axis=1) are flipped. Here, we make this consistent.
pixels = np.flip(pixels, axis=1)
if reduce_func is None:
intensities = pixels
else:
try:
intensities = reduce_func(pixels, axis=1)
except TypeError: # function doesn't allow axis kwarg
intensities = np.apply_along_axis(reduce_func, arr=pixels, axis=1)
return intensities
def _line_profile_coordinates(src, dst, linewidth=1):
"""Return the coordinates of the profile of an image along a scan line.
Parameters
----------
src : 2-tuple of numeric scalar (float or int)
The start point of the scan line.
dst : 2-tuple of numeric scalar (float or int)
The end point of the scan line.
linewidth : int, optional
Width of the scan, perpendicular to the line
Returns
-------
coords : array, shape (2, N, C), float
The coordinates of the profile along the scan line. The length of the
profile is the ceil of the computed length of the scan line.
Notes
-----
This is a utility method meant to be used internally by skimage functions.
The destination point is included in the profile, in contrast to
standard numpy indexing.
"""
src_row, src_col = src = np.asarray(src, dtype=float)
dst_row, dst_col = dst = np.asarray(dst, dtype=float)
d_row, d_col = dst - src
theta = np.arctan2(d_row, d_col)
length = int(np.ceil(np.hypot(d_row, d_col) + 1))
# we add one above because we include the last point in the profile
# (in contrast to standard numpy indexing)
line_col = np.linspace(src_col, dst_col, length)
line_row = np.linspace(src_row, dst_row, length)
# we subtract 1 from linewidth to change from pixel-counting
# (make this line 3 pixels wide) to point distances (the
# distance between pixel centers)
col_width = (linewidth - 1) * np.sin(-theta) / 2
row_width = (linewidth - 1) * np.cos(theta) / 2
perp_rows = np.array([np.linspace(row_i - row_width, row_i + row_width,
linewidth) for row_i in line_row])
perp_cols = np.array([np.linspace(col_i - col_width, col_i + col_width,
linewidth) for col_i in line_col])
return np.array([perp_rows, perp_cols])

View file

@ -0,0 +1,46 @@
#!/usr/bin/env python
from skimage._build import cython
import os
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('measure', parent_package, top_path)
cython(['_ccomp.pyx',
'_find_contours_cy.pyx',
'_moments_cy.pyx',
'_marching_cubes_classic_cy.pyx',
'_marching_cubes_lewiner_cy.pyx',
'_pnpoly.pyx'], working_path=base_path)
config.add_extension('_ccomp', sources=['_ccomp.c'],
include_dirs=[get_numpy_include_dirs()])
config.add_extension('_find_contours_cy', sources=['_find_contours_cy.c'],
include_dirs=[get_numpy_include_dirs()])
config.add_extension('_moments_cy', sources=['_moments_cy.c'],
include_dirs=[get_numpy_include_dirs()])
config.add_extension('_marching_cubes_classic_cy',
sources=['_marching_cubes_classic_cy.c'],
include_dirs=[get_numpy_include_dirs()])
config.add_extension('_marching_cubes_lewiner_cy',
sources=['_marching_cubes_lewiner_cy.c'],
include_dirs=[get_numpy_include_dirs()])
config.add_extension('_pnpoly', sources=['_pnpoly.c'],
include_dirs=[get_numpy_include_dirs(), '../_shared'])
return config
if __name__ == '__main__':
from numpy.distutils.core import setup
setup(maintainer='scikit-image Developers',
maintainer_email='scikit-image@python.org',
description='Graph-based Image-processing Algorithms',
url='https://github.com/scikit-image/scikit-image',
license='Modified BSD',
**(configuration(top_path='').todict())
)

View file

@ -0,0 +1,81 @@
from warnings import warn
from ..metrics.simple_metrics import (mean_squared_error,
peak_signal_noise_ratio,
normalized_root_mse)
__all__ = ['compare_mse',
'compare_nrmse',
'compare_psnr',
]
def compare_mse(im1, im2):
warn('DEPRECATED: skimage.measure.compare_mse has been moved to '
'skimage.metrics.mean_squared_error. It will be removed from '
'skimage.measure in version 0.18.', stacklevel=2)
return mean_squared_error(im1, im2)
if mean_squared_error.__doc__ is not None:
compare_mse.__doc__ = mean_squared_error.__doc__ + """
Warns
-----
Deprecated:
.. versionadded:: 0.16
This function is deprecated and will be removed in scikit-image 0.18.
Please use the function named ``mean_squared_error`` from the
``metrics`` module instead.
See also
--------
skimage.metrics.mean_squared_error
"""
def compare_nrmse(im_true, im_test, norm_type='euclidean'):
warn('DEPRECATED: skimage.measure.compare_nrmse has been moved to '
'skimage.metrics.normalized_root_mse. It will be removed from '
'skimage.measure in version 0.18.', stacklevel=2)
return normalized_root_mse(im_true, im_test, normalization=norm_type)
if normalized_root_mse.__doc__ is not None:
compare_nrmse.__doc__ = normalized_root_mse.__doc__ + """
Warns
-----
Deprecated:
.. versionadded:: 0.16
This function is deprecated and will be removed in scikit-image 0.18.
Please use the function named ``normalized_root_mse`` from the
``metrics`` module instead.
See also
--------
skimage.metrics.normalized_root_mse
"""
def compare_psnr(im_true, im_test, data_range=None):
warn('DEPRECATED: skimage.measure.compare_psnr has been moved to '
'skimage.metrics.peak_signal_noise_ratio. It will be removed from '
'skimage.measure in version 0.18.', stacklevel=2)
return peak_signal_noise_ratio(im_true, im_test, data_range=data_range)
if peak_signal_noise_ratio.__doc__ is not None:
compare_psnr.__doc__ = peak_signal_noise_ratio.__doc__ + """
Warns
-----
Deprecated:
.. versionadded:: 0.16
This function is deprecated and will be removed in scikit-image 0.18.
Please use the function named ``peak_signal_noise_ratio`` from the
``metrics`` module instead.
See also
--------
skimage.metrics.peak_signal_noise_ratio
"""

View file

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

View file

@ -0,0 +1,117 @@
import numpy as np
from skimage.measure import block_reduce
from skimage._shared import testing
from skimage._shared.testing import assert_equal
def test_block_reduce_sum():
image1 = np.arange(4 * 6).reshape(4, 6)
out1 = block_reduce(image1, (2, 3))
expected1 = np.array([[ 24, 42],
[ 96, 114]])
assert_equal(expected1, out1)
image2 = np.arange(5 * 8).reshape(5, 8)
out2 = block_reduce(image2, (3, 3))
expected2 = np.array([[ 81, 108, 87],
[174, 192, 138]])
assert_equal(expected2, out2)
def test_block_reduce_mean():
image1 = np.arange(4 * 6).reshape(4, 6)
out1 = block_reduce(image1, (2, 3), func=np.mean)
expected1 = np.array([[ 4., 7.],
[ 16., 19.]])
assert_equal(expected1, out1)
image2 = np.arange(5 * 8).reshape(5, 8)
out2 = block_reduce(image2, (4, 5), func=np.mean)
expected2 = np.array([[14. , 10.8],
[ 8.5, 5.7]])
assert_equal(expected2, out2)
def test_block_reduce_median():
image1 = np.arange(4 * 6).reshape(4, 6)
out1 = block_reduce(image1, (2, 3), func=np.median)
expected1 = np.array([[ 4., 7.],
[ 16., 19.]])
assert_equal(expected1, out1)
image2 = np.arange(5 * 8).reshape(5, 8)
out2 = block_reduce(image2, (4, 5), func=np.median)
expected2 = np.array([[ 14., 6.5],
[ 0., 0. ]])
assert_equal(expected2, out2)
image3 = np.array([[1, 5, 5, 5], [5, 5, 5, 1000]])
out3 = block_reduce(image3, (2, 4), func=np.median)
assert_equal(5, out3)
def test_block_reduce_min():
image1 = np.arange(4 * 6).reshape(4, 6)
out1 = block_reduce(image1, (2, 3), func=np.min)
expected1 = np.array([[ 0, 3],
[12, 15]])
assert_equal(expected1, out1)
image2 = np.arange(5 * 8).reshape(5, 8)
out2 = block_reduce(image2, (4, 5), func=np.min)
expected2 = np.array([[0, 0],
[0, 0]])
assert_equal(expected2, out2)
def test_block_reduce_max():
image1 = np.arange(4 * 6).reshape(4, 6)
out1 = block_reduce(image1, (2, 3), func=np.max)
expected1 = np.array([[ 8, 11],
[20, 23]])
assert_equal(expected1, out1)
image2 = np.arange(5 * 8).reshape(5, 8)
out2 = block_reduce(image2, (4, 5), func=np.max)
expected2 = np.array([[28, 31],
[36, 39]])
assert_equal(expected2, out2)
def test_invalid_block_size():
image = np.arange(4 * 6).reshape(4, 6)
with testing.raises(ValueError):
block_reduce(image, [1, 2, 3])
with testing.raises(ValueError):
block_reduce(image, [1, 0.5])
def test_func_kwargs_same_dtype():
image = np.array([[97, 123, 173, 227],
[217, 241, 221, 214],
[211, 11, 170, 53],
[214, 205, 101, 57]], dtype=np.uint8)
out = block_reduce(image, (2, 2), func=np.mean,
func_kwargs={'dtype': np.uint8})
expected = np.array([[41, 16], [32, 31]], dtype=np.uint8)
assert_equal(out, expected)
assert out.dtype == expected.dtype
def test_func_kwargs_different_dtype():
image = np.array([[0.45745366, 0.67479345, 0.20949775, 0.3147348],
[0.7209286, 0.88915504, 0.66153409, 0.07919526],
[0.04640037, 0.54008495, 0.34664343, 0.56152301],
[0.58085003, 0.80144708, 0.87844473, 0.29811511]],
dtype=np.float64)
out = block_reduce(image, (2, 2), func=np.mean,
func_kwargs={'dtype': np.float16})
expected = np.array([[0.6855, 0.3164], [0.4922, 0.521]], dtype=np.float16)
assert_equal(out, expected)
assert out.dtype == expected.dtype

View file

@ -0,0 +1,16 @@
import numpy as np
from skimage.measure import shannon_entropy
from skimage._shared.testing import assert_almost_equal
def test_shannon_ones():
img = np.ones((10, 10))
res = shannon_entropy(img, base=np.e)
assert_almost_equal(res, 0.0)
def test_shannon_all_unique():
img = np.arange(64)
res = shannon_entropy(img, base=2)
assert_almost_equal(res, np.log(64) / np.log(2))

View file

@ -0,0 +1,132 @@
import numpy as np
from skimage.measure import find_contours
from skimage._shared import testing
from skimage._shared.testing import assert_array_equal
from pytest import raises
a = np.ones((8, 8), dtype=np.float32)
a[1:-1, 1] = 0
a[1, 1:-1] = 0
x, y = np.mgrid[-1:1:5j, -1:1:5j]
r = np.sqrt(x**2 + y**2)
def test_binary():
ref = [[6. , 1.5],
[5. , 1.5],
[4. , 1.5],
[3. , 1.5],
[2. , 1.5],
[1.5, 2. ],
[1.5, 3. ],
[1.5, 4. ],
[1.5, 5. ],
[1.5, 6. ],
[1. , 6.5],
[0.5, 6. ],
[0.5, 5. ],
[0.5, 4. ],
[0.5, 3. ],
[0.5, 2. ],
[0.5, 1. ],
[1. , 0.5],
[2. , 0.5],
[3. , 0.5],
[4. , 0.5],
[5. , 0.5],
[6. , 0.5],
[6.5, 1. ],
[6. , 1.5]]
contours = find_contours(a, 0.5, positive_orientation='high')
assert len(contours) == 1
assert_array_equal(contours[0][::-1], ref)
# target contour for mask tests
mask_contour = [
[6. , 0.5],
[5. , 0.5],
[4. , 0.5],
[3. , 0.5],
[2. , 0.5],
[1. , 0.5],
[0.5, 1. ],
[0.5, 2. ],
[0.5, 3. ],
[0.5, 4. ],
[0.5, 5. ],
[0.5, 6. ],
[1. , 6.5],
[1.5, 6. ],
[1.5, 5. ],
[1.5, 4. ],
[1.5, 3. ],
[1.5, 2. ],
[2. , 1.5],
[3. , 1.5],
[4. , 1.5],
[5. , 1.5],
[6. , 1.5],
]
mask = np.ones((8, 8), dtype=bool)
# Some missing data that should result in a hole in the contour:
mask[7, 0:3] = False
def test_nodata():
# Test missing data via NaNs in input array
b = np.copy(a)
b[~mask] = np.nan
contours = find_contours(b, 0.5, positive_orientation='high')
assert len(contours) == 1
assert_array_equal(contours[0], mask_contour)
def test_mask():
# Test missing data via explicit masking
contours = find_contours(a, 0.5, positive_orientation='high', mask=mask)
assert len(contours) == 1
assert_array_equal(contours[0], mask_contour)
def test_mask_shape():
bad_mask = np.ones((8, 7), dtype=bool)
with raises(ValueError, match='shape'):
find_contours(a, 0, mask=bad_mask)
def test_mask_dtype():
bad_mask = np.ones((8,8), dtype=np.uint8)
with raises(TypeError, match='binary'):
find_contours(a, 0, mask=bad_mask)
def test_float():
contours = find_contours(r, 0.5)
assert len(contours) == 1
assert_array_equal(contours[0],
[[ 2., 3.],
[ 1., 2.],
[ 2., 1.],
[ 3., 2.],
[ 2., 3.]])
def test_memory_order():
contours = find_contours(np.ascontiguousarray(r), 0.5)
assert len(contours) == 1
contours = find_contours(np.asfortranarray(r), 0.5)
assert len(contours) == 1
def test_invalid_input():
with testing.raises(ValueError):
find_contours(r, 0.5, 'foo', 'bar')
with testing.raises(ValueError):
find_contours(r[..., None], 0.5)

View file

@ -0,0 +1,388 @@
import numpy as np
from skimage.measure import LineModelND, CircleModel, EllipseModel, ransac
from skimage.transform import AffineTransform
from skimage.measure.fit import _dynamic_max_trials
from skimage._shared import testing
from skimage._shared.testing import (assert_equal, assert_almost_equal,
assert_array_less, xfail, arch32)
def test_line_model_invalid_input():
with testing.raises(ValueError):
LineModelND().estimate(np.empty((1, 3)))
def test_line_model_predict():
model = LineModelND()
model.params = ((0, 0), (1, 1))
x = np.arange(-10, 10)
y = model.predict_y(x)
assert_almost_equal(x, model.predict_x(y))
def test_line_model_nd_invalid_input():
with testing.raises(ValueError):
LineModelND().predict_x(np.zeros(1))
with testing.raises(ValueError):
LineModelND().predict_y(np.zeros(1))
with testing.raises(ValueError):
LineModelND().predict_x(np.zeros(1), np.zeros(1))
with testing.raises(ValueError):
LineModelND().predict_y(np.zeros(1))
with testing.raises(ValueError):
LineModelND().predict_y(np.zeros(1), np.zeros(1))
with testing.raises(ValueError):
LineModelND().estimate(np.empty((1, 3)))
with testing.raises(ValueError):
LineModelND().residuals(np.empty((1, 3)))
data = np.empty((1, 2))
with testing.raises(ValueError):
LineModelND().estimate(data)
def test_line_model_nd_predict():
model = LineModelND()
model.params = (np.array([0, 0]), np.array([0.2, 0.8]))
x = np.arange(-10, 10)
y = model.predict_y(x)
assert_almost_equal(x, model.predict_x(y))
def test_line_model_nd_estimate():
# generate original data without noise
model0 = LineModelND()
model0.params = (np.array([0, 0, 0], dtype='float'),
np.array([1, 1, 1], dtype='float')/np.sqrt(3))
# we scale the unit vector with a factor 10 when generating points on the
# line in order to compensate for the scale of the random noise
data0 = (model0.params[0] +
10 * np.arange(-100, 100)[..., np.newaxis] * model0.params[1])
# add gaussian noise to data
random_state = np.random.RandomState(1234)
data = data0 + random_state.normal(size=data0.shape)
# estimate parameters of noisy data
model_est = LineModelND()
model_est.estimate(data)
# assert_almost_equal(model_est.residuals(data0), np.zeros(len(data)), 1)
# test whether estimated parameters are correct
# we use the following geometric property: two aligned vectors have
# a cross-product equal to zero
# test if direction vectors are aligned
assert_almost_equal(np.linalg.norm(np.cross(model0.params[1],
model_est.params[1])), 0, 1)
# test if origins are aligned with the direction
a = model_est.params[0] - model0.params[0]
if np.linalg.norm(a) > 0:
a /= np.linalg.norm(a)
assert_almost_equal(np.linalg.norm(np.cross(model0.params[1], a)), 0, 1)
def test_line_model_nd_residuals():
model = LineModelND()
model.params = (np.array([0, 0, 0]), np.array([0, 0, 1]))
assert_equal(abs(model.residuals(np.array([[0, 0, 0]]))), 0)
assert_equal(abs(model.residuals(np.array([[0, 0, 1]]))), 0)
assert_equal(abs(model.residuals(np.array([[10, 0, 0]]))), 10)
# test params argument in model.rediduals
data = np.array([[10, 0, 0]])
params = (np.array([0, 0, 0]), np.array([2, 0, 0]))
assert_equal(abs(model.residuals(data, params=params)), 30)
def test_line_modelND_under_determined():
data = np.empty((1, 3))
with testing.raises(ValueError):
LineModelND().estimate(data)
def test_circle_model_invalid_input():
with testing.raises(ValueError):
CircleModel().estimate(np.empty((5, 3)))
def test_circle_model_predict():
model = CircleModel()
r = 5
model.params = (0, 0, r)
t = np.arange(0, 2 * np.pi, np.pi / 2)
xy = np.array(((5, 0), (0, 5), (-5, 0), (0, -5)))
assert_almost_equal(xy, model.predict_xy(t))
def test_circle_model_estimate():
# generate original data without noise
model0 = CircleModel()
model0.params = (10, 12, 3)
t = np.linspace(0, 2 * np.pi, 1000)
data0 = model0.predict_xy(t)
# add gaussian noise to data
random_state = np.random.RandomState(1234)
data = data0 + random_state.normal(size=data0.shape)
# estimate parameters of noisy data
model_est = CircleModel()
model_est.estimate(data)
# test whether estimated parameters almost equal original parameters
assert_almost_equal(model0.params, model_est.params, 0)
def test_circle_model_residuals():
model = CircleModel()
model.params = (0, 0, 5)
assert_almost_equal(abs(model.residuals(np.array([[5, 0]]))), 0)
assert_almost_equal(abs(model.residuals(np.array([[6, 6]]))),
np.sqrt(2 * 6**2) - 5)
assert_almost_equal(abs(model.residuals(np.array([[10, 0]]))), 5)
def test_ellipse_model_invalid_input():
with testing.raises(ValueError):
EllipseModel().estimate(np.empty((5, 3)))
def test_ellipse_model_predict():
model = EllipseModel()
model.params = (0, 0, 5, 10, 0)
t = np.arange(0, 2 * np.pi, np.pi / 2)
xy = np.array(((5, 0), (0, 10), (-5, 0), (0, -10)))
assert_almost_equal(xy, model.predict_xy(t))
def test_ellipse_model_estimate():
for angle in range(0, 180, 15):
rad = np.deg2rad(angle)
# generate original data without noise
model0 = EllipseModel()
model0.params = (10, 20, 15, 25, rad)
t = np.linspace(0, 2 * np.pi, 100)
data0 = model0.predict_xy(t)
# add gaussian noise to data
random_state = np.random.RandomState(1234)
data = data0 + random_state.normal(size=data0.shape)
# estimate parameters of noisy data
model_est = EllipseModel()
model_est.estimate(data)
# test whether estimated parameters almost equal original parameters
assert_almost_equal(model0.params[:2], model_est.params[:2], 0)
res = model_est.residuals(data0)
assert_array_less(res, np.ones(res.shape))
def test_ellipse_model_estimate_from_data():
data = np.array([
[264, 854], [265, 875], [268, 863], [270, 857], [275, 905], [285, 915],
[305, 925], [324, 934], [335, 764], [336, 915], [345, 925], [345, 945],
[354, 933], [355, 745], [364, 936], [365, 754], [375, 745], [375, 735],
[385, 736], [395, 735], [394, 935], [405, 727], [415, 736], [415, 727],
[425, 727], [426, 929], [435, 735], [444, 933], [445, 735], [455, 724],
[465, 934], [465, 735], [475, 908], [475, 726], [485, 753], [485, 728],
[492, 762], [495, 745], [491, 910], [493, 909], [499, 904], [505, 905],
[504, 747], [515, 743], [516, 752], [524, 855], [525, 844], [525, 885],
[533, 845], [533, 873], [535, 883], [545, 874], [543, 864], [553, 865],
[553, 845], [554, 825], [554, 835], [563, 845], [565, 826], [563, 855],
[563, 795], [565, 735], [573, 778], [572, 815], [574, 804], [575, 665],
[575, 685], [574, 705], [574, 745], [575, 875], [572, 732], [582, 795],
[579, 709], [583, 805], [583, 854], [586, 755], [584, 824], [585, 655],
[581, 718], [586, 844], [585, 915], [587, 905], [594, 824], [593, 855],
[590, 891], [594, 776], [596, 767], [593, 763], [603, 785], [604, 775],
[603, 885], [605, 753], [605, 655], [606, 935], [603, 761], [613, 802],
[613, 945], [613, 965], [615, 693], [617, 665], [623, 962], [624, 972],
[625, 995], [633, 673], [633, 965], [633, 683], [633, 692], [633, 954],
[634, 1016], [635, 664], [641, 804], [637, 999], [641, 956], [643, 946],
[643, 926], [644, 975], [643, 655], [646, 705], [651, 664], [651, 984],
[647, 665], [651, 715], [651, 725], [651, 734], [647, 809], [651, 825],
[651, 873], [647, 900], [652, 917], [651, 944], [652, 742], [648, 811],
[651, 994], [652, 783], [650, 911], [654, 879]])
# estimate parameters of real data
model = EllipseModel()
model.estimate(data)
# test whether estimated parameters are smaller then 1000, so means stable
assert_array_less(np.abs(model.params[:4]), np.array([2e3] * 4))
@xfail(condition=arch32,
reason=('Known test failure on 32-bit platforms. See links for '
'details: '
'https://github.com/scikit-image/scikit-image/issues/3091 '
'https://github.com/scikit-image/scikit-image/issues/2670'))
def test_ellipse_model_estimate_failers():
# estimate parameters of real data
model = EllipseModel()
assert not model.estimate(np.ones((5, 2)))
assert not model.estimate(np.array([[50, 80], [51, 81], [52, 80]]))
def test_ellipse_model_residuals():
model = EllipseModel()
# vertical line through origin
model.params = (0, 0, 10, 5, 0)
assert_almost_equal(abs(model.residuals(np.array([[10, 0]]))), 0)
assert_almost_equal(abs(model.residuals(np.array([[0, 5]]))), 0)
assert_almost_equal(abs(model.residuals(np.array([[0, 10]]))), 5)
def test_ransac_shape():
# generate original data without noise
model0 = CircleModel()
model0.params = (10, 12, 3)
t = np.linspace(0, 2 * np.pi, 1000)
data0 = model0.predict_xy(t)
# add some faulty data
outliers = (10, 30, 200)
data0[outliers[0], :] = (1000, 1000)
data0[outliers[1], :] = (-50, 50)
data0[outliers[2], :] = (-100, -10)
# estimate parameters of corrupted data
model_est, inliers = ransac(data0, CircleModel, 3, 5, random_state=1)
# test whether estimated parameters equal original parameters
assert_almost_equal(model0.params, model_est.params)
for outlier in outliers:
assert outlier not in inliers
def test_ransac_geometric():
random_state = np.random.RandomState(1)
# generate original data without noise
src = 100 * random_state.random_sample((50, 2))
model0 = AffineTransform(scale=(0.5, 0.3), rotation=1,
translation=(10, 20))
dst = model0(src)
# add some faulty data
outliers = (0, 5, 20)
dst[outliers[0]] = (10000, 10000)
dst[outliers[1]] = (-100, 100)
dst[outliers[2]] = (50, 50)
# estimate parameters of corrupted data
model_est, inliers = ransac((src, dst), AffineTransform, 2, 20,
random_state=random_state)
# test whether estimated parameters equal original parameters
assert_almost_equal(model0.params, model_est.params)
assert np.all(np.nonzero(inliers == False)[0] == outliers)
def test_ransac_is_data_valid():
def is_data_valid(data):
return data.shape[0] > 2
model, inliers = ransac(np.empty((10, 2)), LineModelND, 2, np.inf,
is_data_valid=is_data_valid, random_state=1)
assert_equal(model, None)
assert_equal(inliers, None)
def test_ransac_is_model_valid():
def is_model_valid(model, data):
return False
model, inliers = ransac(np.empty((10, 2)), LineModelND, 2, np.inf,
is_model_valid=is_model_valid, random_state=1)
assert_equal(model, None)
assert_equal(inliers, None)
def test_ransac_dynamic_max_trials():
# Numbers hand-calculated and confirmed on page 119 (Table 4.3) in
# Hartley, R.~I. and Zisserman, A., 2004,
# Multiple View Geometry in Computer Vision, Second Edition,
# Cambridge University Press, ISBN: 0521540518
# e = 0%, min_samples = X
assert_equal(_dynamic_max_trials(100, 100, 2, 0.99), 1)
# e = 5%, min_samples = 2
assert_equal(_dynamic_max_trials(95, 100, 2, 0.99), 2)
# e = 10%, min_samples = 2
assert_equal(_dynamic_max_trials(90, 100, 2, 0.99), 3)
# e = 30%, min_samples = 2
assert_equal(_dynamic_max_trials(70, 100, 2, 0.99), 7)
# e = 50%, min_samples = 2
assert_equal(_dynamic_max_trials(50, 100, 2, 0.99), 17)
# e = 5%, min_samples = 8
assert_equal(_dynamic_max_trials(95, 100, 8, 0.99), 5)
# e = 10%, min_samples = 8
assert_equal(_dynamic_max_trials(90, 100, 8, 0.99), 9)
# e = 30%, min_samples = 8
assert_equal(_dynamic_max_trials(70, 100, 8, 0.99), 78)
# e = 50%, min_samples = 8
assert_equal(_dynamic_max_trials(50, 100, 8, 0.99), 1177)
# e = 0%, min_samples = 5
assert_equal(_dynamic_max_trials(1, 100, 5, 0), 0)
assert_equal(_dynamic_max_trials(1, 100, 5, 1), np.inf)
def test_ransac_invalid_input():
# `residual_threshold` must be greater than zero
with testing.raises(ValueError):
ransac(np.zeros((10, 2)), None, min_samples=2,
residual_threshold=-0.5)
# "`max_trials` must be greater than zero"
with testing.raises(ValueError):
ransac(np.zeros((10, 2)), None, min_samples=2,
residual_threshold=0, max_trials=-1)
# `stop_probability` must be in range (0, 1)
with testing.raises(ValueError):
ransac(np.zeros((10, 2)), None, min_samples=2,
residual_threshold=0, stop_probability=-1)
# `stop_probability` must be in range (0, 1)
with testing.raises(ValueError):
ransac(np.zeros((10, 2)), None, min_samples=2,
residual_threshold=0, stop_probability=1.01)
# `min_samples` as ratio must be in range (0, nb)
with testing.raises(ValueError):
ransac(np.zeros((10, 2)), None, min_samples=0,
residual_threshold=0)
# `min_samples` as ratio must be in range (0, nb)
with testing.raises(ValueError):
ransac(np.zeros((10, 2)), None, min_samples=10,
residual_threshold=0)
# `min_samples` must be greater than zero
with testing.raises(ValueError):
ransac(np.zeros((10, 2)), None, min_samples=-1,
residual_threshold=0)
def test_ransac_sample_duplicates():
class DummyModel(object):
"""Dummy model to check for duplicates."""
def estimate(self, data):
# Assert that all data points are unique.
assert_equal(np.unique(data).size, data.size)
return True
def residuals(self, data):
return np.ones(len(data), dtype=np.double)
# Create dataset with four unique points. Force 10 iterations
# and check that there are no duplicated data points.
data = np.arange(4)
ransac(data, DummyModel, min_samples=3, residual_threshold=0.0,
max_trials=10)

View file

@ -0,0 +1,180 @@
import numpy as np
from skimage.draw import ellipsoid, ellipsoid_stats
from skimage.measure import marching_cubes, mesh_surface_area
from skimage._shared import testing
from skimage._shared.testing import assert_array_equal
import pytest
def test_marching_cubes_isotropic():
ellipsoid_isotropic = ellipsoid(6, 10, 16, levelset=True)
_, surf = ellipsoid_stats(6, 10, 16)
# Classic
verts, faces = marching_cubes(ellipsoid_isotropic, 0., method='_lorensen')
surf_calc = mesh_surface_area(verts, faces)
# Test within 1% tolerance for isotropic. Will always underestimate.
assert surf > surf_calc and surf_calc > surf * 0.99
# Lewiner
verts, faces = marching_cubes(ellipsoid_isotropic, 0.)[:2]
surf_calc = mesh_surface_area(verts, faces)
# Test within 1% tolerance for isotropic. Will always underestimate.
assert surf > surf_calc and surf_calc > surf * 0.99
def test_marching_cubes_anisotropic():
# test spacing as numpy array (and not just tuple)
spacing = np.array([1., 10 / 6., 16 / 6.])
ellipsoid_anisotropic = ellipsoid(6, 10, 16, spacing=spacing,
levelset=True)
_, surf = ellipsoid_stats(6, 10, 16)
# Classic
verts, faces = marching_cubes(ellipsoid_anisotropic, 0.,
spacing=spacing, method='_lorensen')
surf_calc = mesh_surface_area(verts, faces)
# Test within 1.5% tolerance for anisotropic. Will always underestimate.
assert surf > surf_calc and surf_calc > surf * 0.985
# Lewiner
verts, faces = marching_cubes(ellipsoid_anisotropic, 0.,
spacing=spacing)[:2]
surf_calc = mesh_surface_area(verts, faces)
# Test within 1.5% tolerance for anisotropic. Will always underestimate.
assert surf > surf_calc and surf_calc > surf * 0.985
# Test marching cube with mask
with pytest.raises(ValueError):
verts, faces = marching_cubes(
ellipsoid_anisotropic, 0., spacing=spacing,
mask=np.array([]))[:2]
# Test spacing together with allow_degenerate=False
marching_cubes(ellipsoid_anisotropic, 0, spacing=spacing,
allow_degenerate=False)
def test_invalid_input():
# Classic
with testing.raises(ValueError):
marching_cubes(np.zeros((2, 2, 1)), 0, method='_lorensen')
with testing.raises(ValueError):
marching_cubes(np.zeros((2, 2, 1)), 1, method='_lorensen')
with testing.raises(ValueError):
marching_cubes(np.ones((3, 3, 3)), 1, spacing=(1, 2), method='_lorensen')
with testing.raises(ValueError):
marching_cubes(np.zeros((20, 20)), 0, method='_lorensen')
# Lewiner
with testing.raises(ValueError):
marching_cubes(np.zeros((2, 2, 1)), 0)
with testing.raises(ValueError):
marching_cubes(np.zeros((2, 2, 1)), 1)
with testing.raises(ValueError):
marching_cubes(np.ones((3, 3, 3)), 1, spacing=(1, 2))
with testing.raises(ValueError):
marching_cubes(np.zeros((20, 20)), 0)
def test_both_algs_same_result_ellipse():
# Performing this test on data that does not have ambiguities
sphere_small = ellipsoid(1, 1, 1, levelset=True)
vertices1, faces1 = marching_cubes(sphere_small, 0, method='_lorensen')[:2]
vertices2, faces2 = marching_cubes(sphere_small, 0,
allow_degenerate=False)[:2]
vertices3, faces3 = marching_cubes(sphere_small, 0,
allow_degenerate=False,
method='lorensen')[:2]
# Order is different, best we can do is test equal shape and same
# vertices present
assert _same_mesh(vertices1, faces1, vertices2, faces2)
assert _same_mesh(vertices1, faces1, vertices3, faces3)
def _same_mesh(vertices1, faces1, vertices2, faces2, tol=1e-10):
""" Compare two meshes, using a certain tolerance and invariant to
the order of the faces.
"""
# Unwind vertices
triangles1 = vertices1[np.array(faces1)]
triangles2 = vertices2[np.array(faces2)]
# Sort vertices within each triangle
triang1 = [np.concatenate(sorted(t, key=lambda x:tuple(x)))
for t in triangles1]
triang2 = [np.concatenate(sorted(t, key=lambda x:tuple(x)))
for t in triangles2]
# Sort the resulting 9-element "tuples"
triang1 = np.array(sorted([tuple(x) for x in triang1]))
triang2 = np.array(sorted([tuple(x) for x in triang2]))
return (triang1.shape == triang2.shape and
np.allclose(triang1, triang2, 0, tol))
def test_both_algs_same_result_donut():
# Performing this test on data that does not have ambiguities
n = 48
a, b = 2.5/n, -1.25
vol = np.empty((n, n, n), 'float32')
for iz in range(vol.shape[0]):
for iy in range(vol.shape[1]):
for ix in range(vol.shape[2]):
# Double-torii formula by Thomas Lewiner
z, y, x = float(iz)*a+b, float(iy)*a+b, float(ix)*a+b
vol[iz,iy,ix] = ( (
(8*x)**2 + (8*y-2)**2 + (8*z)**2 + 16 - 1.85*1.85 ) * ( (8*x)**2 +
(8*y-2)**2 + (8*z)**2 + 16 - 1.85*1.85 ) - 64 * ( (8*x)**2 + (8*y-2)**2 )
) * ( ( (8*x)**2 + ((8*y-2)+4)*((8*y-2)+4) + (8*z)**2 + 16 - 1.85*1.85 )
* ( (8*x)**2 + ((8*y-2)+4)*((8*y-2)+4) + (8*z)**2 + 16 - 1.85*1.85 ) -
64 * ( ((8*y-2)+4)*((8*y-2)+4) + (8*z)**2
) ) + 1025
vertices1, faces1 = marching_cubes(vol, 0, method='_lorensen')[:2]
vertices2, faces2 = marching_cubes(vol, 0)[:2]
vertices3, faces3 = marching_cubes(vol, 0, method='lorensen')[:2]
# Old and new alg are different
assert not _same_mesh(vertices1, faces1, vertices2, faces2)
# New classic and new Lewiner are different
assert not _same_mesh(vertices2, faces2, vertices3, faces3)
# Would have been nice if old and new classic would have been the same
# assert _same_mesh(vertices1, faces1, vertices3, faces3, 5)
def test_masked_marching_cubes():
ellipsoid_scalar = ellipsoid(6, 10, 16, levelset=True)
mask = np.ones_like(ellipsoid_scalar, dtype=bool)
mask[:10, :, :] = False
mask[:, :, 20:] = False
ver, faces, _, _ = marching_cubes(ellipsoid_scalar, 0, mask=mask)
area = mesh_surface_area(ver, faces)
np.testing.assert_allclose(area, 299.56878662109375, rtol=.01)
def test_masked_marching_cubes_empty():
ellipsoid_scalar = ellipsoid(6, 10, 16, levelset=True)
mask = np.array([])
with pytest.raises(ValueError):
_ = marching_cubes(ellipsoid_scalar, 0, mask=mask)
def test_masked_marching_cubes_old_lewiner():
ellipsoid_scalar = ellipsoid(6, 10, 16, levelset=True)
mask = np.array([])
with pytest.raises(NotImplementedError):
_ = marching_cubes(ellipsoid_scalar, 0, mask=mask, method='_lorensen')
def test_masked_marching_cubes_all_true():
ellipsoid_scalar = ellipsoid(6, 10, 16, levelset=True)
mask = np.ones_like(ellipsoid_scalar, dtype=bool)
ver_m, faces_m, _, _ = marching_cubes(ellipsoid_scalar, 0, mask=mask)
ver, faces, _, _ = marching_cubes(ellipsoid_scalar, 0, mask=mask)
np.testing.assert_allclose(ver_m, ver, rtol=.00001)
np.testing.assert_allclose(faces_m, faces, rtol=.00001)

View file

@ -0,0 +1,187 @@
import numpy as np
from scipy import ndimage as ndi
from skimage import draw
from skimage.measure import (moments, moments_central, moments_coords,
moments_coords_central, moments_normalized,
moments_hu, centroid, inertia_tensor,
inertia_tensor_eigvals)
from skimage._shared import testing
from skimage._shared.testing import (assert_equal, assert_almost_equal,
assert_allclose)
from skimage._shared._warnings import expected_warnings
def test_moments():
image = np.zeros((20, 20), dtype=np.double)
image[14, 14] = 1
image[15, 15] = 1
image[14, 15] = 0.5
image[15, 14] = 0.5
m = moments(image)
assert_equal(m[0, 0], 3)
assert_almost_equal(m[1, 0] / m[0, 0], 14.5)
assert_almost_equal(m[0, 1] / m[0, 0], 14.5)
def test_moments_central():
image = np.zeros((20, 20), dtype=np.double)
image[14, 14] = 1
image[15, 15] = 1
image[14, 15] = 0.5
image[15, 14] = 0.5
mu = moments_central(image, (14.5, 14.5))
# check for proper centroid computation
mu_calc_centroid = moments_central(image)
assert_equal(mu, mu_calc_centroid)
# shift image by dx=2, dy=2
image2 = np.zeros((20, 20), dtype=np.double)
image2[16, 16] = 1
image2[17, 17] = 1
image2[16, 17] = 0.5
image2[17, 16] = 0.5
mu2 = moments_central(image2, (14.5 + 2, 14.5 + 2))
# central moments must be translation invariant
assert_equal(mu, mu2)
def test_moments_coords():
image = np.zeros((20, 20), dtype=np.double)
image[13:17, 13:17] = 1
mu_image = moments(image)
coords = np.array([[r, c] for r in range(13, 17)
for c in range(13, 17)], dtype=np.double)
mu_coords = moments_coords(coords)
assert_almost_equal(mu_coords, mu_image)
def test_moments_central_coords():
image = np.zeros((20, 20), dtype=np.double)
image[13:17, 13:17] = 1
mu_image = moments_central(image, (14.5, 14.5))
coords = np.array([[r, c] for r in range(13, 17)
for c in range(13, 17)], dtype=np.double)
mu_coords = moments_coords_central(coords, (14.5, 14.5))
assert_almost_equal(mu_coords, mu_image)
# ensure that center is being calculated normally
mu_coords_calc_centroid = moments_coords_central(coords)
assert_almost_equal(mu_coords_calc_centroid, mu_coords)
# shift image by dx=3 dy=3
image = np.zeros((20, 20), dtype=np.double)
image[16:20, 16:20] = 1
mu_image = moments_central(image, (14.5, 14.5))
coords = np.array([[r, c] for r in range(16, 20)
for c in range(16, 20)], dtype=np.double)
mu_coords = moments_coords_central(coords, (14.5, 14.5))
assert_almost_equal(mu_coords, mu_image)
def test_moments_normalized():
image = np.zeros((20, 20), dtype=np.double)
image[13:17, 13:17] = 1
mu = moments_central(image, (14.5, 14.5))
nu = moments_normalized(mu)
# shift image by dx=-3, dy=-3 and scale by 0.5
image2 = np.zeros((20, 20), dtype=np.double)
image2[11:13, 11:13] = 1
mu2 = moments_central(image2, (11.5, 11.5))
nu2 = moments_normalized(mu2)
# central moments must be translation and scale invariant
assert_almost_equal(nu, nu2, decimal=1)
def test_moments_normalized_3d():
image = draw.ellipsoid(1, 1, 10)
mu_image = moments_central(image)
nu = moments_normalized(mu_image)
assert nu[0, 0, 2] > nu[0, 2, 0]
assert_almost_equal(nu[0, 2, 0], nu[2, 0, 0])
coords = np.where(image)
mu_coords = moments_coords_central(coords)
assert_almost_equal(mu_coords, mu_image)
def test_moments_normalized_invalid():
with testing.raises(ValueError):
moments_normalized(np.zeros((3, 3)), 3)
with testing.raises(ValueError):
moments_normalized(np.zeros((3, 3)), 4)
def test_moments_hu():
image = np.zeros((20, 20), dtype=np.double)
image[13:15, 13:17] = 1
mu = moments_central(image, (13.5, 14.5))
nu = moments_normalized(mu)
hu = moments_hu(nu)
# shift image by dx=2, dy=3, scale by 0.5 and rotate by 90deg
image2 = np.zeros((20, 20), dtype=np.double)
image2[11, 11:13] = 1
image2 = image2.T
mu2 = moments_central(image2, (11.5, 11))
nu2 = moments_normalized(mu2)
hu2 = moments_hu(nu2)
# central moments must be translation and scale invariant
assert_almost_equal(hu, hu2, decimal=1)
def test_centroid():
image = np.zeros((20, 20), dtype=np.double)
image[14, 14:16] = 1
image[15, 14:16] = 1/3
image_centroid = centroid(image)
assert_allclose(image_centroid, (14.25, 14.5))
def test_inertia_tensor_2d():
image = np.zeros((40, 40))
image[15:25, 5:35] = 1 # big horizontal rectangle (aligned with axis 1)
T = inertia_tensor(image)
assert T[0, 0] > T[1, 1]
np.testing.assert_allclose(T[0, 1], 0)
v0, v1 = inertia_tensor_eigvals(image, T=T)
np.testing.assert_allclose(np.sqrt(v0/v1), 3, rtol=0.01, atol=0.05)
def test_inertia_tensor_3d():
image = draw.ellipsoid(10, 5, 3)
T0 = inertia_tensor(image)
eig0, V0 = np.linalg.eig(T0)
# principal axis of ellipse = eigenvector of smallest eigenvalue
v0 = V0[:, np.argmin(eig0)]
assert np.allclose(v0, [1, 0, 0]) or np.allclose(-v0, [1, 0, 0])
imrot = ndi.rotate(image.astype(float), 30, axes=(0, 1), order=1)
Tr = inertia_tensor(imrot)
eigr, Vr = np.linalg.eig(Tr)
vr = Vr[:, np.argmin(eigr)]
# Check that axis has rotated by expected amount
pi, cos, sin = np.pi, np.cos, np.sin
R = np.array([[ cos(pi/6), -sin(pi/6), 0],
[ sin(pi/6), cos(pi/6), 0],
[ 0, 0, 1]])
expected_vr = R @ v0
assert (np.allclose(vr, expected_vr, atol=1e-3, rtol=0.01) or
np.allclose(-vr, expected_vr, atol=1e-3, rtol=0.01))
def test_inertia_tensor_eigvals():
# Floating point precision problems could make a positive
# semidefinite matrix have an eigenvalue that is very slightly
# negative. Check that we have caught and fixed this problem.
image = np.array([[1, 0, 0, 0, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]])
# mu = np.array([[3, 0, 98], [0, 14, 0], [2, 0, 98]])
eigvals = inertia_tensor_eigvals(image=image)
assert (min(eigvals) >= 0)

View file

@ -0,0 +1,35 @@
import numpy as np
from skimage.measure import points_in_poly, grid_points_in_poly
from skimage._shared.testing import assert_array_equal
class TestNpnpoly():
def test_square(self):
v = np.array([[0, 0],
[0, 1],
[1, 1],
[1, 0]])
assert(points_in_poly([[0.5, 0.5]], v)[0])
assert(not points_in_poly([[-0.1, 0.1]], v)[0])
def test_triangle(self):
v = np.array([[0, 0],
[1, 0],
[0.5, 0.75]])
assert(points_in_poly([[0.5, 0.7]], v)[0])
assert(not points_in_poly([[0.5, 0.76]], v)[0])
assert(not points_in_poly([[0.7, 0.5]], v)[0])
def test_type(self):
assert(points_in_poly([[0, 0]], [[0, 0]]).dtype == np.bool)
def test_grid_points_in_poly():
v = np.array([[0, 0],
[5, 0],
[5, 5]])
expected = np.tril(np.ones((5, 5), dtype=bool))
assert_array_equal(grid_points_in_poly((5, 5), v), expected)

View file

@ -0,0 +1,64 @@
import numpy as np
from skimage.measure import approximate_polygon, subdivide_polygon
from skimage.measure._polygon import _SUBDIVISION_MASKS
from skimage._shared import testing
from skimage._shared.testing import assert_array_equal, assert_equal
square = np.array([
[0, 0], [0, 1], [0, 2], [0, 3],
[1, 3], [2, 3], [3, 3],
[3, 2], [3, 1], [3, 0],
[2, 0], [1, 0], [0, 0]
])
def test_approximate_polygon():
out = approximate_polygon(square, 0.1)
assert_array_equal(out, square[(0, 3, 6, 9, 12), :])
out = approximate_polygon(square, 2.2)
assert_array_equal(out, square[(0, 6, 12), :])
out = approximate_polygon(square[(0, 1, 3, 4, 5, 6, 7, 9, 11, 12), :], 0.1)
assert_array_equal(out, square[(0, 3, 6, 9, 12), :])
out = approximate_polygon(square, -1)
assert_array_equal(out, square)
out = approximate_polygon(square, 0)
assert_array_equal(out, square)
def test_subdivide_polygon():
new_square1 = square
new_square2 = square[:-1]
new_square3 = square[:-1]
# test iterative subdvision
for _ in range(10):
square1, square2, square3 = new_square1, new_square2, new_square3
# test different B-Spline degrees
for degree in range(1, 7):
mask_len = len(_SUBDIVISION_MASKS[degree][0])
# test circular
new_square1 = subdivide_polygon(square1, degree)
assert_array_equal(new_square1[-1], new_square1[0])
assert_equal(new_square1.shape[0],
2 * square1.shape[0] - 1)
# test non-circular
new_square2 = subdivide_polygon(square2, degree)
assert_equal(new_square2.shape[0],
2 * (square2.shape[0] - mask_len + 1))
# test non-circular, preserve_ends
new_square3 = subdivide_polygon(square3, degree, True)
assert_equal(new_square3[0], square3[0])
assert_equal(new_square3[-1], square3[-1])
assert_equal(new_square3.shape[0],
2 * (square3.shape[0] - mask_len + 2))
# not supported B-Spline degree
with testing.raises(ValueError):
subdivide_polygon(square, 0)
with testing.raises(ValueError):
subdivide_polygon(square, 8)

View file

@ -0,0 +1,214 @@
import numpy as np
from ..._shared.testing import assert_equal, assert_almost_equal
from ..._shared._warnings import expected_warnings
from ..profile import profile_line
image = np.arange(100).reshape((10, 10)).astype(np.float)
def test_horizontal_rightward():
prof = profile_line(image, (0, 2), (0, 8), order=0, mode='constant')
expected_prof = np.arange(2, 9)
assert_equal(prof, expected_prof)
def test_horizontal_leftward():
prof = profile_line(image, (0, 8), (0, 2), order=0, mode='constant')
expected_prof = np.arange(8, 1, -1)
assert_equal(prof, expected_prof)
def test_vertical_downward():
prof = profile_line(image, (2, 5), (8, 5), order=0, mode='constant')
expected_prof = np.arange(25, 95, 10)
assert_equal(prof, expected_prof)
def test_vertical_upward():
prof = profile_line(image, (8, 5), (2, 5), order=0, mode='constant')
expected_prof = np.arange(85, 15, -10)
assert_equal(prof, expected_prof)
def test_45deg_right_downward():
prof = profile_line(image, (2, 2), (8, 8), order=0, mode='constant')
expected_prof = np.array([22, 33, 33, 44, 55, 55, 66, 77, 77, 88])
# repeats are due to aliasing using nearest neighbor interpolation.
# to see this, imagine a diagonal line with markers every unit of
# length traversing a checkerboard pattern of squares also of unit
# length. Because the line is diagonal, sometimes more than one
# marker will fall on the same checkerboard box.
assert_almost_equal(prof, expected_prof)
def test_45deg_right_downward_interpolated():
prof = profile_line(image, (2, 2), (8, 8), order=1, mode='constant')
expected_prof = np.linspace(22, 88, 10)
assert_almost_equal(prof, expected_prof)
def test_45deg_right_upward():
prof = profile_line(image, (8, 2), (2, 8), order=1, mode='constant')
expected_prof = np.arange(82, 27, -6)
assert_almost_equal(prof, expected_prof)
def test_45deg_left_upward():
prof = profile_line(image, (8, 8), (2, 2), order=1, mode='constant')
expected_prof = np.arange(88, 21, -22. / 3)
assert_almost_equal(prof, expected_prof)
def test_45deg_left_downward():
prof = profile_line(image, (2, 8), (8, 2), order=1, mode='constant')
expected_prof = np.arange(28, 83, 6)
assert_almost_equal(prof, expected_prof)
def test_pythagorean_triangle_right_downward():
prof = profile_line(image, (1, 1), (7, 9), order=0, mode='constant')
expected_prof = np.array([11, 22, 23, 33, 34, 45, 56, 57, 67, 68, 79])
assert_equal(prof, expected_prof)
def test_pythagorean_triangle_right_downward_interpolated():
prof = profile_line(image, (1, 1), (7, 9), order=1, mode='constant')
expected_prof = np.linspace(11, 79, 11)
assert_almost_equal(prof, expected_prof)
pyth_image = np.zeros((6, 7), np.float)
line = ((1, 2, 2, 3, 3, 4), (1, 2, 3, 3, 4, 5))
below = ((2, 2, 3, 4, 4, 5), (0, 1, 2, 3, 4, 4))
above = ((0, 1, 1, 2, 3, 3), (2, 2, 3, 4, 5, 6))
pyth_image[line] = 1.8
pyth_image[below] = 0.6
pyth_image[above] = 0.6
def test_pythagorean_triangle_right_downward_linewidth():
prof = profile_line(pyth_image, (1, 1), (4, 5), linewidth=3, order=0,
mode='constant')
expected_prof = np.ones(6)
assert_almost_equal(prof, expected_prof)
def test_pythagorean_triangle_right_upward_linewidth():
prof = profile_line(pyth_image[::-1, :], (4, 1), (1, 5),
linewidth=3, order=0, mode='constant')
expected_prof = np.ones(6)
assert_almost_equal(prof, expected_prof)
def test_pythagorean_triangle_transpose_left_down_linewidth():
prof = profile_line(pyth_image.T[:, ::-1], (1, 4), (5, 1),
linewidth=3, order=0, mode='constant')
expected_prof = np.ones(6)
assert_almost_equal(prof, expected_prof)
def test_reduce_func_mean():
prof = profile_line(pyth_image, (0, 1), (3, 1), linewidth=3, order=0,
reduce_func=np.mean, mode='reflect')
expected_prof = pyth_image[:4, :3].mean(1)
assert_almost_equal(prof, expected_prof)
def test_reduce_func_max():
prof = profile_line(pyth_image, (0, 1), (3, 1), linewidth=3, order=0,
reduce_func=np.max, mode='reflect')
expected_prof = pyth_image[:4, :3].max(1)
assert_almost_equal(prof, expected_prof)
def test_reduce_func_sum():
prof = profile_line(pyth_image, (0, 1), (3, 1), linewidth=3, order=0,
reduce_func=np.sum, mode='reflect')
expected_prof = pyth_image[:4, :3].sum(1)
assert_almost_equal(prof, expected_prof)
def test_reduce_func_mean_linewidth_1():
prof = profile_line(pyth_image, (0, 1), (3, 1), linewidth=1, order=0,
reduce_func=np.mean, mode='constant')
expected_prof = pyth_image[:4, 1]
assert_almost_equal(prof, expected_prof)
def test_reduce_func_None_linewidth_1():
prof = profile_line(pyth_image, (1, 2), (4, 2), linewidth=1,
order=0, reduce_func=None, mode='constant')
expected_prof = pyth_image[1:5, 2, np.newaxis]
assert_almost_equal(prof, expected_prof)
def test_reduce_func_None_linewidth_3():
prof = profile_line(pyth_image, (1, 2), (4, 2), linewidth=3,
order=0, reduce_func=None, mode='constant')
expected_prof = pyth_image[1:5, 1:4]
assert_almost_equal(prof, expected_prof)
def test_reduce_func_lambda_linewidth_3():
def reduce_func(x):
return x + x ** 2
prof = profile_line(pyth_image, (1, 2), (4, 2), linewidth=3, order=0,
reduce_func=reduce_func, mode='constant')
expected_prof = np.apply_along_axis(reduce_func,
arr=pyth_image[1:5, 1:4], axis=1)
assert_almost_equal(prof, expected_prof)
def test_reduce_func_sqrt_linewidth_3():
def reduce_func(x):
return x ** 0.5
prof = profile_line(pyth_image, (1, 2), (4, 2), linewidth=3,
order=0, reduce_func=reduce_func,
mode='constant')
expected_prof = np.apply_along_axis(reduce_func,
arr=pyth_image[1:5, 1:4], axis=1)
assert_almost_equal(prof, expected_prof)
def test_reduce_func_sumofsqrt_linewidth_3():
def reduce_func(x):
return np.sum(x ** 0.5)
prof = profile_line(pyth_image, (1, 2), (4, 2), linewidth=3, order=0,
reduce_func=reduce_func, mode='constant')
expected_prof = np.apply_along_axis(reduce_func,
arr=pyth_image[1:5, 1:4], axis=1)
assert_almost_equal(prof, expected_prof)
def test_oob_coodinates():
offset = 2
idx = pyth_image.shape[0] + offset
prof = profile_line(pyth_image, (-offset, 2), (idx, 2), linewidth=1,
order=0, reduce_func=None, mode='constant')
expected_prof = np.vstack([np.zeros((offset, 1)),
pyth_image[:, 2, np.newaxis],
np.zeros((offset + 1, 1))])
assert_almost_equal(prof, expected_prof)
def test_bool_array_input():
shape = (200, 200)
center_x, center_y = (140, 150)
radius = 20
x, y = np.meshgrid(range(shape[1]), range(shape[0]))
mask = (y - center_y) ** 2 + (x - center_x) ** 2 < radius ** 2
src = (center_y, center_x)
phi = 4 * np.pi / 9.
dy = 31 * np.cos(phi)
dx = 31 * np.sin(phi)
dst = (center_y + dy, center_x + dx)
profile_u8 = profile_line(mask.astype(np.uint8), src, dst)
assert all(profile_u8[:radius] == 1)
profile_b = profile_line(mask, src, dst)
assert all(profile_b[:radius] == 1)
assert all(profile_b == profile_u8)

View file

@ -0,0 +1,575 @@
import math
import numpy as np
from numpy import array
from skimage._shared._warnings import expected_warnings
from skimage.measure._regionprops import (regionprops, PROPS, perimeter,
_parse_docs, _props_to_dict,
regionprops_table, OBJECT_COLUMNS,
COL_DTYPES)
from skimage._shared import testing
from skimage._shared.testing import (assert_array_equal, assert_almost_equal,
assert_array_almost_equal, assert_equal)
SAMPLE = np.array(
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1],
[0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1]]
)
INTENSITY_SAMPLE = SAMPLE.copy()
INTENSITY_SAMPLE[1, 9:11] = 2
SAMPLE_3D = np.zeros((6, 6, 6), dtype=np.uint8)
SAMPLE_3D[1:3, 1:3, 1:3] = 1
SAMPLE_3D[3, 2, 2] = 1
INTENSITY_SAMPLE_3D = SAMPLE_3D.copy()
def test_all_props():
region = regionprops(SAMPLE, INTENSITY_SAMPLE)[0]
for prop in PROPS:
try:
assert_almost_equal(region[prop], getattr(region, PROPS[prop]))
except TypeError: # the `slice` property causes this
pass
def test_all_props_3d():
region = regionprops(SAMPLE_3D, INTENSITY_SAMPLE_3D)[0]
for prop in PROPS:
try:
assert_almost_equal(region[prop], getattr(region, PROPS[prop]))
except (NotImplementedError, TypeError):
pass
def test_dtype():
regionprops(np.zeros((10, 10), dtype=np.int))
regionprops(np.zeros((10, 10), dtype=np.uint))
with testing.raises(TypeError):
regionprops(np.zeros((10, 10), dtype=np.float))
with testing.raises(TypeError):
regionprops(np.zeros((10, 10), dtype=np.double))
with testing.raises(TypeError):
regionprops(np.zeros((10, 10), dtype=np.bool))
def test_ndim():
regionprops(np.zeros((10, 10), dtype=np.int))
regionprops(np.zeros((10, 10, 1), dtype=np.int))
regionprops(np.zeros((10, 10, 10), dtype=np.int))
regionprops(np.zeros((1, 1), dtype=np.int))
regionprops(np.zeros((1, 1, 1), dtype=np.int))
with testing.raises(TypeError):
regionprops(np.zeros((10, 10, 10, 2), dtype=np.int))
def test_area():
area = regionprops(SAMPLE)[0].area
assert area == np.sum(SAMPLE)
area = regionprops(SAMPLE_3D)[0].area
assert area == np.sum(SAMPLE_3D)
def test_bbox():
bbox = regionprops(SAMPLE)[0].bbox
assert_array_almost_equal(bbox, (0, 0, SAMPLE.shape[0], SAMPLE.shape[1]))
SAMPLE_mod = SAMPLE.copy()
SAMPLE_mod[:, -1] = 0
bbox = regionprops(SAMPLE_mod)[0].bbox
assert_array_almost_equal(bbox, (0, 0, SAMPLE.shape[0], SAMPLE.shape[1]-1))
bbox = regionprops(SAMPLE_3D)[0].bbox
assert_array_almost_equal(bbox, (1, 1, 1, 4, 3, 3))
def test_bbox_area():
padded = np.pad(SAMPLE, 5, mode='constant')
bbox_area = regionprops(padded)[0].bbox_area
assert_array_almost_equal(bbox_area, SAMPLE.size)
def test_moments_central():
mu = regionprops(SAMPLE)[0].moments_central
# determined with OpenCV
assert_almost_equal(mu[2, 0], 436.00000000000045)
# different from OpenCV results, bug in OpenCV
assert_almost_equal(mu[3, 0], -737.333333333333)
assert_almost_equal(mu[1, 1], -87.33333333333303)
assert_almost_equal(mu[2, 1], -127.5555555555593)
assert_almost_equal(mu[0, 2], 1259.7777777777774)
assert_almost_equal(mu[1, 2], 2000.296296296291)
assert_almost_equal(mu[0, 3], -760.0246913580195)
def test_centroid():
centroid = regionprops(SAMPLE)[0].centroid
# determined with MATLAB
assert_array_almost_equal(centroid, (5.66666666666666, 9.444444444444444))
def test_centroid_3d():
centroid = regionprops(SAMPLE_3D)[0].centroid
# determined by mean along axis 1 of SAMPLE_3D.nonzero()
assert_array_almost_equal(centroid, (1.66666667, 1.55555556, 1.55555556))
def test_convex_area():
area = regionprops(SAMPLE)[0].convex_area
# determined with MATLAB
assert area == 124
def test_convex_image():
img = regionprops(SAMPLE)[0].convex_image
# determined with MATLAB
ref = np.array(
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]
)
assert_array_equal(img, ref)
def test_coordinates():
sample = np.zeros((10, 10), dtype=np.int8)
coords = np.array([[3, 2], [3, 3], [3, 4]])
sample[coords[:, 0], coords[:, 1]] = 1
prop_coords = regionprops(sample)[0].coords
assert_array_equal(prop_coords, coords)
sample = np.zeros((6, 6, 6), dtype=np.int8)
coords = np.array([[1, 1, 1], [1, 2, 1], [1, 3, 1]])
sample[coords[:, 0], coords[:, 1], coords[:, 2]] = 1
prop_coords = regionprops(sample)[0].coords
assert_array_equal(prop_coords, coords)
def test_slice():
padded = np.pad(SAMPLE, ((2, 4), (5, 2)), mode='constant')
nrow, ncol = SAMPLE.shape
result = regionprops(padded)[0].slice
expected = (slice(2, 2+nrow), slice(5, 5+ncol))
assert_equal(result, expected)
def test_eccentricity():
eps = regionprops(SAMPLE)[0].eccentricity
assert_almost_equal(eps, 0.814629313427)
img = np.zeros((5, 5), dtype=np.int)
img[2, 2] = 1
eps = regionprops(img)[0].eccentricity
assert_almost_equal(eps, 0)
def test_equiv_diameter():
diameter = regionprops(SAMPLE)[0].equivalent_diameter
# determined with MATLAB
assert_almost_equal(diameter, 9.57461472963)
def test_euler_number():
en = regionprops(SAMPLE)[0].euler_number
assert en == 1
SAMPLE_mod = SAMPLE.copy()
SAMPLE_mod[7, -3] = 0
en = regionprops(SAMPLE_mod)[0].euler_number
assert en == 0
def test_extent():
extent = regionprops(SAMPLE)[0].extent
assert_almost_equal(extent, 0.4)
def test_moments_hu():
hu = regionprops(SAMPLE)[0].moments_hu
ref = np.array([
3.27117627e-01,
2.63869194e-02,
2.35390060e-02,
1.23151193e-03,
1.38882330e-06,
-2.72586158e-05,
-6.48350653e-06
])
# bug in OpenCV caused in Central Moments calculation?
assert_array_almost_equal(hu, ref)
def test_image():
img = regionprops(SAMPLE)[0].image
assert_array_equal(img, SAMPLE)
img = regionprops(SAMPLE_3D)[0].image
assert_array_equal(img, SAMPLE_3D[1:4, 1:3, 1:3])
def test_label():
label = regionprops(SAMPLE)[0].label
assert_array_equal(label, 1)
label = regionprops(SAMPLE_3D)[0].label
assert_array_equal(label, 1)
def test_filled_area():
area = regionprops(SAMPLE)[0].filled_area
assert area == np.sum(SAMPLE)
SAMPLE_mod = SAMPLE.copy()
SAMPLE_mod[7, -3] = 0
area = regionprops(SAMPLE_mod)[0].filled_area
assert area == np.sum(SAMPLE)
def test_filled_image():
img = regionprops(SAMPLE)[0].filled_image
assert_array_equal(img, SAMPLE)
def test_major_axis_length():
length = regionprops(SAMPLE)[0].major_axis_length
# MATLAB has different interpretation of ellipse than found in literature,
# here implemented as found in literature
assert_almost_equal(length, 16.7924234999)
def test_max_intensity():
intensity = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE
)[0].max_intensity
assert_almost_equal(intensity, 2)
def test_mean_intensity():
intensity = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE
)[0].mean_intensity
assert_almost_equal(intensity, 1.02777777777777)
def test_min_intensity():
intensity = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE
)[0].min_intensity
assert_almost_equal(intensity, 1)
def test_minor_axis_length():
length = regionprops(SAMPLE)[0].minor_axis_length
# MATLAB has different interpretation of ellipse than found in literature,
# here implemented as found in literature
assert_almost_equal(length, 9.739302807263)
def test_moments():
m = regionprops(SAMPLE)[0].moments
# determined with OpenCV
assert_almost_equal(m[0, 0], 72.0)
assert_almost_equal(m[0, 1], 680.0)
assert_almost_equal(m[0, 2], 7682.0)
assert_almost_equal(m[0, 3], 95588.0)
assert_almost_equal(m[1, 0], 408.0)
assert_almost_equal(m[1, 1], 3766.0)
assert_almost_equal(m[1, 2], 43882.0)
assert_almost_equal(m[2, 0], 2748.0)
assert_almost_equal(m[2, 1], 24836.0)
assert_almost_equal(m[3, 0], 19776.0)
def test_moments_normalized():
nu = regionprops(SAMPLE)[0].moments_normalized
# determined with OpenCV
assert_almost_equal(nu[0, 2], 0.24301268861454037)
assert_almost_equal(nu[0, 3], -0.017278118992041805)
assert_almost_equal(nu[1, 1], -0.016846707818929982)
assert_almost_equal(nu[1, 2], 0.045473992910668816)
assert_almost_equal(nu[2, 0], 0.08410493827160502)
assert_almost_equal(nu[2, 1], -0.002899800614433943)
def test_orientation():
orient = regionprops(SAMPLE)[0].orientation
# determined with MATLAB
assert_almost_equal(orient, -1.4663278802756865)
# test diagonal regions
diag = np.eye(10, dtype=int)
orient_diag = regionprops(diag)[0].orientation
assert_almost_equal(orient_diag, -math.pi / 4)
orient_diag = regionprops(np.flipud(diag))[0].orientation
assert_almost_equal(orient_diag, math.pi / 4)
orient_diag = regionprops(np.fliplr(diag))[0].orientation
assert_almost_equal(orient_diag, math.pi / 4)
orient_diag = regionprops(np.fliplr(np.flipud(diag)))[0].orientation
assert_almost_equal(orient_diag, -math.pi / 4)
def test_perimeter():
per = regionprops(SAMPLE)[0].perimeter
assert_almost_equal(per, 55.2487373415)
per = perimeter(SAMPLE.astype('double'), neighbourhood=8)
assert_almost_equal(per, 46.8284271247)
def test_solidity():
solidity = regionprops(SAMPLE)[0].solidity
# determined with MATLAB
assert_almost_equal(solidity, 0.580645161290323)
def test_weighted_moments_central():
wmu = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE
)[0].weighted_moments_central
ref = np.array(
[[7.4000000000e+01, 3.7303493627e-14, 1.2602837838e+03,
-7.6561796932e+02],
[-2.1316282073e-13, -8.7837837838e+01, 2.1571526662e+03,
-4.2385971907e+03],
[4.7837837838e+02, -1.4801314828e+02, 6.6989799420e+03,
-9.9501164076e+03],
[-7.5943608473e+02, -1.2714707125e+03, 1.5304076361e+04,
-3.3156729271e+04]])
np.set_printoptions(precision=10)
assert_array_almost_equal(wmu, ref)
def test_weighted_centroid():
centroid = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE
)[0].weighted_centroid
assert_array_almost_equal(centroid, (5.540540540540, 9.445945945945))
def test_weighted_moments_hu():
whu = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE
)[0].weighted_moments_hu
ref = np.array([
3.1750587329e-01,
2.1417517159e-02,
2.3609322038e-02,
1.2565683360e-03,
8.3014209421e-07,
-3.5073773473e-05,
-6.7936409056e-06
])
assert_array_almost_equal(whu, ref)
def test_weighted_moments():
wm = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE
)[0].weighted_moments
ref = np.array(
[[7.4000000e+01, 6.9900000e+02, 7.8630000e+03, 9.7317000e+04],
[4.1000000e+02, 3.7850000e+03, 4.4063000e+04, 5.7256700e+05],
[2.7500000e+03, 2.4855000e+04, 2.9347700e+05, 3.9007170e+06],
[1.9778000e+04, 1.7500100e+05, 2.0810510e+06, 2.8078871e+07]]
)
assert_array_almost_equal(wm, ref)
def test_weighted_moments_normalized():
wnu = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE
)[0].weighted_moments_normalized
ref = np.array(
[[ np.nan, np.nan, 0.2301467830, -0.0162529732],
[ np.nan, -0.0160405109, 0.0457932622, -0.0104598869],
[ 0.0873590903, -0.0031421072, 0.0165315478, -0.0028544152],
[-0.0161217406, -0.0031376984, 0.0043903193, -0.0011057191]]
)
assert_array_almost_equal(wnu, ref)
def test_label_sequence():
a = np.empty((2, 2), dtype=np.int)
a[:, :] = 2
ps = regionprops(a)
assert len(ps) == 1
assert ps[0].label == 2
def test_pure_background():
a = np.zeros((2, 2), dtype=np.int)
ps = regionprops(a)
assert len(ps) == 0
def test_invalid():
ps = regionprops(SAMPLE)
def get_intensity_image():
ps[0].intensity_image
with testing.raises(AttributeError):
get_intensity_image()
def test_invalid_size():
wrong_intensity_sample = np.array([[1], [1]])
with testing.raises(ValueError):
regionprops(SAMPLE, wrong_intensity_sample)
def test_equals():
arr = np.zeros((100, 100), dtype=np.int)
arr[0:25, 0:25] = 1
arr[50:99, 50:99] = 2
regions = regionprops(arr)
r1 = regions[0]
regions = regionprops(arr)
r2 = regions[0]
r3 = regions[1]
assert_equal(r1 == r2, True, "Same regionprops are not equal")
assert_equal(r1 != r3, True, "Different regionprops are equal")
def test_iterate_all_props():
region = regionprops(SAMPLE)[0]
p0 = {p: region[p] for p in region}
region = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE)[0]
p1 = {p: region[p] for p in region}
assert len(p0) < len(p1)
def test_cache():
SAMPLE_mod = SAMPLE.copy()
region = regionprops(SAMPLE_mod)[0]
f0 = region.filled_image
region._label_image[:10] = 1
f1 = region.filled_image
# Changed underlying image, but cache keeps result the same
assert_array_equal(f0, f1)
# Now invalidate cache
region._cache_active = False
f1 = region.filled_image
assert np.any(f0 != f1)
def test_docstrings_and_props():
def foo():
"""foo"""
has_docstrings = bool(foo.__doc__)
region = regionprops(SAMPLE)[0]
docs = _parse_docs()
props = [m for m in dir(region) if not m.startswith('_')]
nr_docs_parsed = len(docs)
nr_props = len(props)
if has_docstrings:
assert_equal(nr_docs_parsed, nr_props)
ds = docs['weighted_moments_normalized']
assert 'iteration' not in ds
assert len(ds.split('\n')) > 3
else:
assert_equal(nr_docs_parsed, 0)
def test_props_to_dict():
regions = regionprops(SAMPLE)
out = _props_to_dict(regions)
assert out == {'label': array([1]),
'bbox-0': array([0]), 'bbox-1': array([0]),
'bbox-2': array([10]), 'bbox-3': array([18])}
regions = regionprops(SAMPLE)
out = _props_to_dict(regions, properties=('label', 'area', 'bbox'),
separator='+')
assert out == {'label': array([1]), 'area': array([72]),
'bbox+0': array([0]), 'bbox+1': array([0]),
'bbox+2': array([10]), 'bbox+3': array([18])}
def test_regionprops_table():
out = regionprops_table(SAMPLE)
assert out == {'label': array([1]),
'bbox-0': array([0]), 'bbox-1': array([0]),
'bbox-2': array([10]), 'bbox-3': array([18])}
out = regionprops_table(SAMPLE, properties=('label', 'area', 'bbox'),
separator='+')
assert out == {'label': array([1]), 'area': array([72]),
'bbox+0': array([0]), 'bbox+1': array([0]),
'bbox+2': array([10]), 'bbox+3': array([18])}
out = regionprops_table(np.zeros((2, 2), dtype=int),
properties=('label', 'area', 'bbox'),
separator='+')
assert len(out) == 6
assert len(out['label']) == 0
assert len(out['area']) == 0
assert len(out['bbox+0']) == 0
assert len(out['bbox+1']) == 0
assert len(out['bbox+2']) == 0
assert len(out['bbox+3']) == 0
def test_props_dict_complete():
region = regionprops(SAMPLE)[0]
properties = [s for s in dir(region) if not s.startswith('_')]
assert set(properties) == set(PROPS.values())
def test_column_dtypes_complete():
assert set(COL_DTYPES.keys()).union(OBJECT_COLUMNS) == set(PROPS.values())
def test_column_dtypes_correct():
msg = 'mismatch with expected type,'
region = regionprops(SAMPLE, intensity_image=INTENSITY_SAMPLE)[0]
for col in COL_DTYPES:
r = region[col]
if col in OBJECT_COLUMNS:
assert COL_DTYPES[col] == object
continue
t = type(np.ravel(r)[0])
if np.issubdtype(t, np.floating):
assert COL_DTYPES[col] == float, (
f'{col} dtype {t} {msg} {COL_DTYPES[col]}'
)
elif np.issubdtype(t, np.integer):
assert COL_DTYPES[col] == int, (
f'{col} dtype {t} {msg} {COL_DTYPES[col]}'
)
else:
assert False, (
f'{col} dtype {t} {msg} {COL_DTYPES[col]}'
)
def test_deprecated_coords_argument():
with expected_warnings(['coordinates keyword argument']):
region = regionprops(SAMPLE, coordinates='rc')
with testing.raises(ValueError):
region = regionprops(SAMPLE, coordinates='xy')

View file

@ -0,0 +1,84 @@
import numpy as np
import skimage.data
from skimage.measure import compare_nrmse, compare_psnr, compare_mse
from skimage._shared import testing
from skimage._shared.testing import assert_equal, assert_almost_equal
from skimage._shared._warnings import expected_warnings
np.random.seed(5)
cam = skimage.data.camera()
sigma = 20.0
cam_noisy = np.clip(cam + sigma * np.random.randn(*cam.shape), 0, 255)
cam_noisy = cam_noisy.astype(cam.dtype)
def test_PSNR_vs_IPOL():
# Tests vs. imdiff result from the following IPOL article and code:
# https://www.ipol.im/pub/art/2011/g_lmii/
p_IPOL = 22.4497
with expected_warnings(['DEPRECATED']):
p = compare_psnr(cam, cam_noisy)
assert_almost_equal(p, p_IPOL, decimal=4)
def test_PSNR_float():
with expected_warnings(['DEPRECATED']):
p_uint8 = compare_psnr(cam, cam_noisy)
p_float64 = compare_psnr(cam / 255., cam_noisy / 255.,
data_range=1)
assert_almost_equal(p_uint8, p_float64, decimal=5)
# mixed precision inputs
with expected_warnings(['DEPRECATED']):
p_mixed = compare_psnr(cam / 255., np.float32(cam_noisy / 255.),
data_range=1)
assert_almost_equal(p_mixed, p_float64, decimal=5)
# mismatched dtype results in a warning if data_range is unspecified
with expected_warnings(['Inputs have mismatched dtype', 'DEPRECATED']):
p_mixed = compare_psnr(cam / 255., np.float32(cam_noisy / 255.))
assert_almost_equal(p_mixed, p_float64, decimal=5)
def test_PSNR_errors():
with expected_warnings(['DEPRECATED']):
# shape mismatch
with testing.raises(ValueError):
compare_psnr(cam, cam[:-1, :])
def test_NRMSE():
x = np.ones(4)
y = np.asarray([0., 2., 2., 2.])
with expected_warnings(['DEPRECATED']):
assert_equal(compare_nrmse(y, x, 'mean'), 1 / np.mean(y))
assert_equal(compare_nrmse(y, x, 'Euclidean'), 1 / np.sqrt(3))
assert_equal(compare_nrmse(y, x, 'min-max'), 1 / (y.max() - y.min()))
# mixed precision inputs are allowed
assert_almost_equal(compare_nrmse(y, np.float32(x), 'min-max'),
1 / (y.max() - y.min()))
def test_NRMSE_no_int_overflow():
camf = cam.astype(np.float32)
cam_noisyf = cam_noisy.astype(np.float32)
with expected_warnings(['DEPRECATED']):
assert_almost_equal(compare_mse(cam, cam_noisy),
compare_mse(camf, cam_noisyf))
assert_almost_equal(compare_nrmse(cam, cam_noisy),
compare_nrmse(camf, cam_noisyf))
def test_NRMSE_errors():
x = np.ones(4)
with expected_warnings(['DEPRECATED']):
# shape mismatch
with testing.raises(ValueError):
compare_nrmse(x[:-1], x)
# invalid normalization name
with testing.raises(ValueError):
compare_nrmse(x, x, norm_type='foo')

View file

@ -0,0 +1,240 @@
import os
import numpy as np
from skimage import data, data_dir
from skimage.metrics import structural_similarity
from skimage._shared import testing
from skimage._shared._warnings import expected_warnings
from skimage._shared.testing import (assert_equal, assert_almost_equal,
assert_array_almost_equal, fetch)
np.random.seed(5)
cam = data.camera()
sigma = 20.0
cam_noisy = np.clip(cam + sigma * np.random.randn(*cam.shape), 0, 255)
cam_noisy = cam_noisy.astype(cam.dtype)
np.random.seed(1234)
def test_structural_similarity_patch_range():
N = 51
X = (np.random.rand(N, N) * 255).astype(np.uint8)
Y = (np.random.rand(N, N) * 255).astype(np.uint8)
assert(structural_similarity(X, Y, win_size=N) < 0.1)
assert_equal(structural_similarity(X, X, win_size=N), 1)
def test_structural_similarity_image():
N = 100
X = (np.random.rand(N, N) * 255).astype(np.uint8)
Y = (np.random.rand(N, N) * 255).astype(np.uint8)
S0 = structural_similarity(X, X, win_size=3)
assert_equal(S0, 1)
S1 = structural_similarity(X, Y, win_size=3)
assert(S1 < 0.3)
S2 = structural_similarity(X, Y, win_size=11, gaussian_weights=True)
assert(S2 < 0.3)
mssim0, S3 = structural_similarity(X, Y, full=True)
assert_equal(S3.shape, X.shape)
mssim = structural_similarity(X, Y)
assert_equal(mssim0, mssim)
# ssim of image with itself should be 1.0
assert_equal(structural_similarity(X, X), 1.0)
# Because we are forcing a random seed state, it is probably good to test
# against a few seeds in case on seed gives a particularly bad example
@testing.parametrize('seed', [1, 2, 3, 5, 8, 13])
def test_structural_similarity_grad(seed):
N = 30
# NOTE: This test is known to randomly fail on some systems (Mac OS X 10.6)
# And when testing tests in parallel. Therefore, we choose a few
# seeds that are known to work.
# The likely cause of this failure is that we are setting a hard
# threshold on the value of the gradient. Often the computed gradient
# is only slightly larger than what was measured.
# X = np.random.rand(N, N) * 255
# Y = np.random.rand(N, N) * 255
rnd = np.random.RandomState(seed)
X = rnd.rand(N, N) * 255
Y = rnd.rand(N, N) * 255
f = structural_similarity(X, Y, data_range=255)
g = structural_similarity(X, Y, data_range=255, gradient=True)
assert f < 0.05
assert g[0] < 0.05
assert np.all(g[1] < 0.05)
mssim, grad, s = structural_similarity(X, Y, data_range=255,
gradient=True, full=True)
assert np.all(grad < 0.05)
def test_structural_similarity_dtype():
N = 30
X = np.random.rand(N, N)
Y = np.random.rand(N, N)
S1 = structural_similarity(X, Y)
X = (X * 255).astype(np.uint8)
Y = (X * 255).astype(np.uint8)
S2 = structural_similarity(X, Y)
assert S1 < 0.1
assert S2 < 0.1
def test_structural_similarity_multichannel():
N = 100
X = (np.random.rand(N, N) * 255).astype(np.uint8)
Y = (np.random.rand(N, N) * 255).astype(np.uint8)
S1 = structural_similarity(X, Y, win_size=3)
# replicate across three channels. should get identical value
Xc = np.tile(X[..., np.newaxis], (1, 1, 3))
Yc = np.tile(Y[..., np.newaxis], (1, 1, 3))
S2 = structural_similarity(Xc, Yc, multichannel=True, win_size=3)
assert_almost_equal(S1, S2)
# full case should return an image as well
m, S3 = structural_similarity(Xc, Yc, multichannel=True, full=True)
assert_equal(S3.shape, Xc.shape)
# gradient case
m, grad = structural_similarity(Xc, Yc, multichannel=True, gradient=True)
assert_equal(grad.shape, Xc.shape)
# full and gradient case
m, grad, S3 = structural_similarity(Xc, Yc,
multichannel=True,
full=True,
gradient=True)
assert_equal(grad.shape, Xc.shape)
assert_equal(S3.shape, Xc.shape)
# fail if win_size exceeds any non-channel dimension
with testing.raises(ValueError):
structural_similarity(Xc, Yc, win_size=7, multichannel=False)
def test_structural_similarity_nD():
# test 1D through 4D on small random arrays
N = 10
for ndim in range(1, 5):
xsize = [N, ] * 5
X = (np.random.rand(*xsize) * 255).astype(np.uint8)
Y = (np.random.rand(*xsize) * 255).astype(np.uint8)
mssim = structural_similarity(X, Y, win_size=3)
assert mssim < 0.05
def test_structural_similarity_multichannel_chelsea():
# color image example
Xc = data.chelsea()
sigma = 15.0
Yc = np.clip(Xc + sigma * np.random.randn(*Xc.shape), 0, 255)
Yc = Yc.astype(Xc.dtype)
# multichannel result should be mean of the individual channel results
mssim = structural_similarity(Xc, Yc, multichannel=True)
mssim_sep = [structural_similarity(Yc[..., c], Xc[..., c])
for c in range(Xc.shape[-1])]
assert_almost_equal(mssim, np.mean(mssim_sep))
# ssim of image with itself should be 1.0
assert_equal(structural_similarity(Xc, Xc, multichannel=True), 1.0)
def test_gaussian_mssim_vs_IPOL():
# Tests vs. imdiff result from the following IPOL article and code:
# https://www.ipol.im/pub/art/2011/g_lmii/
mssim_IPOL = 0.327309966087341
mssim = structural_similarity(cam, cam_noisy, gaussian_weights=True,
use_sample_covariance=False)
assert_almost_equal(mssim, mssim_IPOL, decimal=3)
def test_gaussian_mssim_vs_author_ref():
"""
test vs. result from original author's Matlab implementation available at
https://ece.uwaterloo.ca/~z70wang/research/ssim/
Matlab test code:
img1 = imread('camera.png')
img2 = imread('camera_noisy.png')
mssim = ssim_index(img1, img2)
"""
mssim_matlab = 0.327314295673357
mssim = structural_similarity(cam, cam_noisy, gaussian_weights=True,
use_sample_covariance=False)
assert_almost_equal(mssim, mssim_matlab, decimal=10)
def test_gaussian_mssim_and_gradient_vs_Matlab():
# comparison to Matlab implementation of N. Avanaki:
# https://ece.uwaterloo.ca/~nnikvand/Coderep/SHINE%20TOOLBOX/SHINEtoolbox/
# Note: final line of ssim_sens.m was modified to discard image borders
ref = np.load(fetch('data/mssim_matlab_output.npz'))
grad_matlab = ref['grad_matlab']
mssim_matlab = float(ref['mssim_matlab'])
mssim, grad = structural_similarity(cam, cam_noisy, gaussian_weights=True,
gradient=True,
use_sample_covariance=False)
assert_almost_equal(mssim, mssim_matlab, decimal=3)
# check almost equal aside from object borders
assert_array_almost_equal(grad_matlab[5:-5], grad[5:-5])
def test_mssim_vs_legacy():
# check that ssim with default options matches skimage 0.11 result
mssim_skimage_0pt11 = 0.34192589699605191
mssim = structural_similarity(cam, cam_noisy)
assert_almost_equal(mssim, mssim_skimage_0pt11)
def test_mssim_mixed_dtype():
mssim = structural_similarity(cam, cam_noisy)
with expected_warnings(['Inputs have mismatched dtype']):
mssim_mixed = structural_similarity(cam, cam_noisy.astype(np.float32))
assert_almost_equal(mssim, mssim_mixed)
# no warning when user supplies data_range
mssim_mixed = structural_similarity(cam, cam_noisy.astype(np.float32),
data_range=255)
assert_almost_equal(mssim, mssim_mixed)
def test_invalid_input():
# size mismatch
X = np.zeros((9, 9), dtype=np.double)
Y = np.zeros((8, 8), dtype=np.double)
with testing.raises(ValueError):
structural_similarity(X, Y)
# win_size exceeds image extent
with testing.raises(ValueError):
structural_similarity(X, X, win_size=X.shape[0] + 1)
# some kwarg inputs must be non-negative
with testing.raises(ValueError):
structural_similarity(X, X, K1=-0.1)
with testing.raises(ValueError):
structural_similarity(X, X, K2=-0.1)
with testing.raises(ValueError):
structural_similarity(X, X, sigma=-1.0)