This commit is contained in:
ton
2024-10-07 10:13:40 +07:00
parent aa1631742f
commit 3a7d696db6
9729 changed files with 1832837 additions and 161742 deletions

View File

@@ -0,0 +1,3 @@
import lazy_loader as lazy
__getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__)

View File

@@ -0,0 +1,88 @@
# Explicitly setting `__all__` is necessary for type inference engines
# to know which symbols are exported. See
# https://peps.python.org/pep-0484/#stub-files
__all__ = [
'canny',
'Cascade',
'daisy',
'hog',
'graycomatrix',
'graycoprops',
'local_binary_pattern',
'multiblock_lbp',
'draw_multiblock_lbp',
'peak_local_max',
'structure_tensor',
'structure_tensor_eigenvalues',
'hessian_matrix',
'hessian_matrix_det',
'hessian_matrix_eigvals',
'shape_index',
'corner_kitchen_rosenfeld',
'corner_harris',
'corner_shi_tomasi',
'corner_foerstner',
'corner_subpix',
'corner_peaks',
'corner_moravec',
'corner_fast',
'corner_orientations',
'match_template',
'BRIEF',
'CENSURE',
'ORB',
'SIFT',
'match_descriptors',
'plot_matches',
'blob_dog',
'blob_doh',
'blob_log',
'haar_like_feature',
'haar_like_feature_coord',
'draw_haar_like_feature',
'multiscale_basic_features',
'learn_gmm',
'fisher_vector',
]
from ._canny import canny
from ._cascade import Cascade
from ._daisy import daisy
from ._hog import hog
from .texture import (
graycomatrix,
graycoprops,
local_binary_pattern,
multiblock_lbp,
draw_multiblock_lbp,
)
from .peak import peak_local_max
from .corner import (
corner_kitchen_rosenfeld,
corner_harris,
corner_shi_tomasi,
corner_foerstner,
corner_subpix,
corner_peaks,
corner_fast,
structure_tensor,
structure_tensor_eigenvalues,
hessian_matrix,
hessian_matrix_eigvals,
hessian_matrix_det,
corner_moravec,
corner_orientations,
shape_index,
)
from .template import match_template
from .brief import BRIEF
from .censure import CENSURE
from .orb import ORB
from .sift import SIFT
from .match import match_descriptors
from .util import plot_matches
from .blob import blob_dog, blob_log, blob_doh
from .haar import haar_like_feature, haar_like_feature_coord, draw_haar_like_feature
from ._basic_features import multiscale_basic_features
from ._fisher_vector import learn_gmm, fisher_vector

View File

@@ -0,0 +1,198 @@
from itertools import combinations_with_replacement
import itertools
import numpy as np
from skimage import filters, feature
from skimage.util.dtype import img_as_float32
from .._shared._dependency_checks import is_wasm
if not is_wasm:
from concurrent.futures import ThreadPoolExecutor as PoolExecutor
else:
from contextlib import AbstractContextManager
# Threading isn't supported on WASM, mock ThreadPoolExecutor as a fallback
class PoolExecutor(AbstractContextManager):
def __init__(self, *_, **__):
pass
def __exit__(self, exc_type, exc_val, exc_tb):
pass
def map(self, fn, iterables):
return map(fn, iterables)
def _texture_filter(gaussian_filtered):
H_elems = [
np.gradient(np.gradient(gaussian_filtered)[ax0], axis=ax1)
for ax0, ax1 in combinations_with_replacement(range(gaussian_filtered.ndim), 2)
]
eigvals = feature.hessian_matrix_eigvals(H_elems)
return eigvals
def _singlescale_basic_features_singlechannel(
img, sigma, intensity=True, edges=True, texture=True
):
results = ()
gaussian_filtered = filters.gaussian(img, sigma=sigma, preserve_range=False)
if intensity:
results += (gaussian_filtered,)
if edges:
results += (filters.sobel(gaussian_filtered),)
if texture:
results += (*_texture_filter(gaussian_filtered),)
return results
def _mutiscale_basic_features_singlechannel(
img,
intensity=True,
edges=True,
texture=True,
sigma_min=0.5,
sigma_max=16,
num_sigma=None,
num_workers=None,
):
"""Features for a single channel nd image.
Parameters
----------
img : ndarray
Input image, which can be grayscale or multichannel.
intensity : bool, default True
If True, pixel intensities averaged over the different scales
are added to the feature set.
edges : bool, default True
If True, intensities of local gradients averaged over the different
scales are added to the feature set.
texture : bool, default True
If True, eigenvalues of the Hessian matrix after Gaussian blurring
at different scales are added to the feature set.
sigma_min : float, optional
Smallest value of the Gaussian kernel used to average local
neighborhoods before extracting features.
sigma_max : float, optional
Largest value of the Gaussian kernel used to average local
neighborhoods before extracting features.
num_sigma : int, optional
Number of values of the Gaussian kernel between sigma_min and sigma_max.
If None, sigma_min multiplied by powers of 2 are used.
num_workers : int or None, optional
The number of parallel threads to use. If set to ``None``, the full
set of available cores are used.
Returns
-------
features : list
List of features, each element of the list is an array of shape as img.
"""
# computations are faster as float32
img = np.ascontiguousarray(img_as_float32(img))
if num_sigma is None:
num_sigma = int(np.log2(sigma_max) - np.log2(sigma_min) + 1)
sigmas = np.logspace(
np.log2(sigma_min),
np.log2(sigma_max),
num=num_sigma,
base=2,
endpoint=True,
)
with PoolExecutor(max_workers=num_workers) as ex:
out_sigmas = list(
ex.map(
lambda s: _singlescale_basic_features_singlechannel(
img, s, intensity=intensity, edges=edges, texture=texture
),
sigmas,
)
)
features = itertools.chain.from_iterable(out_sigmas)
return features
def multiscale_basic_features(
image,
intensity=True,
edges=True,
texture=True,
sigma_min=0.5,
sigma_max=16,
num_sigma=None,
num_workers=None,
*,
channel_axis=None,
):
"""Local features for a single- or multi-channel nd image.
Intensity, gradient intensity and local structure are computed at
different scales thanks to Gaussian blurring.
Parameters
----------
image : ndarray
Input image, which can be grayscale or multichannel.
intensity : bool, default True
If True, pixel intensities averaged over the different scales
are added to the feature set.
edges : bool, default True
If True, intensities of local gradients averaged over the different
scales are added to the feature set.
texture : bool, default True
If True, eigenvalues of the Hessian matrix after Gaussian blurring
at different scales are added to the feature set.
sigma_min : float, optional
Smallest value of the Gaussian kernel used to average local
neighborhoods before extracting features.
sigma_max : float, optional
Largest value of the Gaussian kernel used to average local
neighborhoods before extracting features.
num_sigma : int, optional
Number of values of the Gaussian kernel between sigma_min and sigma_max.
If None, sigma_min multiplied by powers of 2 are used.
num_workers : int or None, optional
The number of parallel threads to use. If set to ``None``, the full
set of available cores are used.
channel_axis : int or None, optional
If None, the image is assumed to be a grayscale (single channel) image.
Otherwise, this parameter indicates which axis of the array corresponds
to channels.
.. versionadded:: 0.19
``channel_axis`` was added in 0.19.
Returns
-------
features : np.ndarray
Array of shape ``image.shape + (n_features,)``. When `channel_axis` is
not None, all channels are concatenated along the features dimension.
(i.e. ``n_features == n_features_singlechannel * n_channels``)
"""
if not any([intensity, edges, texture]):
raise ValueError(
"At least one of `intensity`, `edges` or `textures`"
"must be True for features to be computed."
)
if channel_axis is None:
image = image[..., np.newaxis]
channel_axis = -1
elif channel_axis != -1:
image = np.moveaxis(image, channel_axis, -1)
all_results = (
_mutiscale_basic_features_singlechannel(
image[..., dim],
intensity=intensity,
edges=edges,
texture=texture,
sigma_min=sigma_min,
sigma_max=sigma_max,
num_sigma=num_sigma,
num_workers=num_workers,
)
for dim in range(image.shape[-1])
)
features = list(itertools.chain.from_iterable(all_results))
out = np.stack(features, axis=-1)
return out

View File

@@ -0,0 +1,262 @@
"""
canny.py - Canny Edge detector
Reference: Canny, J., A Computational Approach To Edge Detection, IEEE Trans.
Pattern Analysis and Machine Intelligence, 8:679-714, 1986
"""
import numpy as np
import scipy.ndimage as ndi
from ..util.dtype import dtype_limits
from .._shared.filters import gaussian
from .._shared.utils import _supported_float_type, check_nD
from ._canny_cy import _nonmaximum_suppression_bilinear
def _preprocess(image, mask, sigma, mode, cval):
"""Generate a smoothed image and an eroded mask.
The image is smoothed using a gaussian filter ignoring masked
pixels and the mask is eroded.
Parameters
----------
image : array
Image to be smoothed.
mask : array
Mask with 1's for significant pixels, 0's for masked pixels.
sigma : scalar or sequence of scalars
Standard deviation for Gaussian kernel. The standard
deviations of the Gaussian filter are given for each axis as a
sequence, or as a single number, in which case it is equal for
all axes.
mode : str, {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}
The ``mode`` parameter determines how the array borders are
handled, where ``cval`` is the value when mode is equal to
'constant'.
cval : float, optional
Value to fill past edges of input if `mode` is 'constant'.
Returns
-------
smoothed_image : ndarray
The smoothed array
eroded_mask : ndarray
The eroded mask.
Notes
-----
This function calculates the fractional contribution of masked pixels
by applying the function to the mask (which gets you the fraction of
the pixel data that's due to significant points). We then mask the image
and apply the function. The resulting values will be lower by the
bleed-over fraction, so you can recalibrate by dividing by the function
on the mask to recover the effect of smoothing from just the significant
pixels.
"""
gaussian_kwargs = dict(sigma=sigma, mode=mode, cval=cval, preserve_range=False)
compute_bleedover = mode == 'constant' or mask is not None
float_type = _supported_float_type(image.dtype)
if mask is None:
if compute_bleedover:
mask = np.ones(image.shape, dtype=float_type)
masked_image = image
eroded_mask = np.ones(image.shape, dtype=bool)
eroded_mask[:1, :] = 0
eroded_mask[-1:, :] = 0
eroded_mask[:, :1] = 0
eroded_mask[:, -1:] = 0
else:
mask = mask.astype(bool, copy=False)
masked_image = np.zeros_like(image)
masked_image[mask] = image[mask]
# Make the eroded mask. Setting the border value to zero will wipe
# out the image edges for us.
s = ndi.generate_binary_structure(2, 2)
eroded_mask = ndi.binary_erosion(mask, s, border_value=0)
if compute_bleedover:
# Compute the fractional contribution of masked pixels by applying
# the function to the mask (which gets you the fraction of the
# pixel data that's due to significant points)
bleed_over = (
gaussian(mask.astype(float_type, copy=False), **gaussian_kwargs)
+ np.finfo(float_type).eps
)
# Smooth the masked image
smoothed_image = gaussian(masked_image, **gaussian_kwargs)
# Lower the result by the bleed-over fraction, so you can
# recalibrate by dividing by the function on the mask to recover
# the effect of smoothing from just the significant pixels.
if compute_bleedover:
smoothed_image /= bleed_over
return smoothed_image, eroded_mask
def canny(
image,
sigma=1.0,
low_threshold=None,
high_threshold=None,
mask=None,
use_quantiles=False,
*,
mode='constant',
cval=0.0,
):
"""Edge filter an image using the Canny algorithm.
Parameters
----------
image : 2D array
Grayscale input image to detect edges on; can be of any dtype.
sigma : float, optional
Standard deviation of the Gaussian filter.
low_threshold : float, optional
Lower bound for hysteresis thresholding (linking edges).
If None, low_threshold is set to 10% of dtype's max.
high_threshold : float, optional
Upper bound for hysteresis thresholding (linking edges).
If None, high_threshold is set to 20% of dtype's max.
mask : array, dtype=bool, optional
Mask to limit the application of Canny to a certain area.
use_quantiles : bool, optional
If ``True`` then treat low_threshold and high_threshold as
quantiles of the edge magnitude image, rather than absolute
edge magnitude values. If ``True`` then the thresholds must be
in the range [0, 1].
mode : str, {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}
The ``mode`` parameter determines how the array borders are
handled during Gaussian filtering, where ``cval`` is the value when
mode is equal to 'constant'.
cval : float, optional
Value to fill past edges of input if `mode` is 'constant'.
Returns
-------
output : 2D array (image)
The binary edge map.
See also
--------
skimage.filters.sobel
Notes
-----
The steps of the algorithm are as follows:
* Smooth the image using a Gaussian with ``sigma`` width.
* Apply the horizontal and vertical Sobel operators to get the gradients
within the image. The edge strength is the norm of the gradient.
* Thin potential edges to 1-pixel wide curves. First, find the normal
to the edge at each point. This is done by looking at the
signs and the relative magnitude of the X-Sobel and Y-Sobel
to sort the points into 4 categories: horizontal, vertical,
diagonal and antidiagonal. Then look in the normal and reverse
directions to see if the values in either of those directions are
greater than the point in question. Use interpolation to get a mix of
points instead of picking the one that's the closest to the normal.
* Perform a hysteresis thresholding: first label all points above the
high threshold as edges. Then recursively label any point above the
low threshold that is 8-connected to a labeled point as an edge.
References
----------
.. [1] Canny, J., A Computational Approach To Edge Detection, IEEE Trans.
Pattern Analysis and Machine Intelligence, 8:679-714, 1986
:DOI:`10.1109/TPAMI.1986.4767851`
.. [2] William Green's Canny tutorial
https://en.wikipedia.org/wiki/Canny_edge_detector
Examples
--------
>>> from skimage import feature
>>> rng = np.random.default_rng()
>>> # Generate noisy image of a square
>>> im = np.zeros((256, 256))
>>> im[64:-64, 64:-64] = 1
>>> im += 0.2 * rng.random(im.shape)
>>> # First trial with the Canny filter, with the default smoothing
>>> edges1 = feature.canny(im)
>>> # Increase the smoothing for better results
>>> edges2 = feature.canny(im, sigma=3)
"""
# Regarding masks, any point touching a masked point will have a gradient
# that is "infected" by the masked point, so it's enough to erode the
# mask by one and then mask the output. We also mask out the border points
# because who knows what lies beyond the edge of the image?
if np.issubdtype(image.dtype, np.int64) or np.issubdtype(image.dtype, np.uint64):
raise ValueError("64-bit integer images are not supported")
check_nD(image, 2)
dtype_max = dtype_limits(image, clip_negative=False)[1]
if low_threshold is None:
low_threshold = 0.1
elif use_quantiles:
if not (0.0 <= low_threshold <= 1.0):
raise ValueError("Quantile thresholds must be between 0 and 1.")
else:
low_threshold /= dtype_max
if high_threshold is None:
high_threshold = 0.2
elif use_quantiles:
if not (0.0 <= high_threshold <= 1.0):
raise ValueError("Quantile thresholds must be between 0 and 1.")
else:
high_threshold /= dtype_max
if high_threshold < low_threshold:
raise ValueError("low_threshold should be lower then high_threshold")
# Image filtering
smoothed, eroded_mask = _preprocess(image, mask, sigma, mode, cval)
# Gradient magnitude estimation
jsobel = ndi.sobel(smoothed, axis=1)
isobel = ndi.sobel(smoothed, axis=0)
magnitude = isobel * isobel
magnitude += jsobel * jsobel
np.sqrt(magnitude, out=magnitude)
if use_quantiles:
low_threshold, high_threshold = np.percentile(
magnitude, [100.0 * low_threshold, 100.0 * high_threshold]
)
# Non-maximum suppression
low_masked = _nonmaximum_suppression_bilinear(
isobel, jsobel, magnitude, eroded_mask, low_threshold
)
# Double thresholding and edge tracking
#
# Segment the low-mask, then only keep low-segments that have
# some high_mask component in them
#
low_mask = low_masked > 0
strel = np.ones((3, 3), bool)
labels, count = ndi.label(low_mask, strel)
if count == 0:
return low_mask
high_mask = low_mask & (low_masked >= high_threshold)
nonzero_sums = np.unique(labels[high_mask])
good_label = np.zeros((count + 1,), bool)
good_label[nonzero_sums] = True
output_mask = good_label[labels]
return output_mask

View File

@@ -0,0 +1,249 @@
import math
import numpy as np
from numpy import arctan2, exp, pi, sqrt
from .. import draw
from ..util.dtype import img_as_float
from .._shared.filters import gaussian
from .._shared.utils import check_nD
from ..color import gray2rgb
def daisy(
image,
step=4,
radius=15,
rings=3,
histograms=8,
orientations=8,
normalization='l1',
sigmas=None,
ring_radii=None,
visualize=False,
):
'''Extract DAISY feature descriptors densely for the given image.
DAISY is a feature descriptor similar to SIFT formulated in a way that
allows for fast dense extraction. Typically, this is practical for
bag-of-features image representations.
The implementation follows Tola et al. [1]_ but deviate on the following
points:
* Histogram bin contribution are smoothed with a circular Gaussian
window over the tonal range (the angular range).
* The sigma values of the spatial Gaussian smoothing in this code do not
match the sigma values in the original code by Tola et al. [2]_. In
their code, spatial smoothing is applied to both the input image and
the center histogram. However, this smoothing is not documented in [1]_
and, therefore, it is omitted.
Parameters
----------
image : (M, N) array
Input image (grayscale).
step : int, optional
Distance between descriptor sampling points.
radius : int, optional
Radius (in pixels) of the outermost ring.
rings : int, optional
Number of rings.
histograms : int, optional
Number of histograms sampled per ring.
orientations : int, optional
Number of orientations (bins) per histogram.
normalization : [ 'l1' | 'l2' | 'daisy' | 'off' ], optional
How to normalize the descriptors
* 'l1': L1-normalization of each descriptor.
* 'l2': L2-normalization of each descriptor.
* 'daisy': L2-normalization of individual histograms.
* 'off': Disable normalization.
sigmas : 1D array of float, optional
Standard deviation of spatial Gaussian smoothing for the center
histogram and for each ring of histograms. The array of sigmas should
be sorted from the center and out. I.e. the first sigma value defines
the spatial smoothing of the center histogram and the last sigma value
defines the spatial smoothing of the outermost ring. Specifying sigmas
overrides the following parameter.
``rings = len(sigmas) - 1``
ring_radii : 1D array of int, optional
Radius (in pixels) for each ring. Specifying ring_radii overrides the
following two parameters.
``rings = len(ring_radii)``
``radius = ring_radii[-1]``
If both sigmas and ring_radii are given, they must satisfy the
following predicate since no radius is needed for the center
histogram.
``len(ring_radii) == len(sigmas) + 1``
visualize : bool, optional
Generate a visualization of the DAISY descriptors
Returns
-------
descs : array
Grid of DAISY descriptors for the given image as an array
dimensionality (P, Q, R) where
``P = ceil((M - radius*2) / step)``
``Q = ceil((N - radius*2) / step)``
``R = (rings * histograms + 1) * orientations``
descs_img : (M, N, 3) array (only if visualize==True)
Visualization of the DAISY descriptors.
References
----------
.. [1] Tola et al. "Daisy: An efficient dense descriptor applied to wide-
baseline stereo." Pattern Analysis and Machine Intelligence, IEEE
Transactions on 32.5 (2010): 815-830.
.. [2] http://cvlab.epfl.ch/software/daisy
'''
check_nD(image, 2, 'img')
image = img_as_float(image)
float_dtype = image.dtype
# Validate parameters.
if (
sigmas is not None
and ring_radii is not None
and len(sigmas) - 1 != len(ring_radii)
):
raise ValueError('`len(sigmas)-1 != len(ring_radii)`')
if ring_radii is not None:
rings = len(ring_radii)
radius = ring_radii[-1]
if sigmas is not None:
rings = len(sigmas) - 1
if sigmas is None:
sigmas = [radius * (i + 1) / float(2 * rings) for i in range(rings)]
if ring_radii is None:
ring_radii = [radius * (i + 1) / float(rings) for i in range(rings)]
if normalization not in ['l1', 'l2', 'daisy', 'off']:
raise ValueError('Invalid normalization method.')
# Compute image derivatives.
dx = np.zeros(image.shape, dtype=float_dtype)
dy = np.zeros(image.shape, dtype=float_dtype)
dx[:, :-1] = np.diff(image, n=1, axis=1)
dy[:-1, :] = np.diff(image, n=1, axis=0)
# Compute gradient orientation and magnitude and their contribution
# to the histograms.
grad_mag = sqrt(dx**2 + dy**2)
grad_ori = arctan2(dy, dx)
orientation_kappa = orientations / pi
orientation_angles = [2 * o * pi / orientations - pi for o in range(orientations)]
hist = np.empty((orientations,) + image.shape, dtype=float_dtype)
for i, o in enumerate(orientation_angles):
# Weigh bin contribution by the circular normal distribution
hist[i, :, :] = exp(orientation_kappa * np.cos(grad_ori - o))
# Weigh bin contribution by the gradient magnitude
hist[i, :, :] = np.multiply(hist[i, :, :], grad_mag)
# Smooth orientation histograms for the center and all rings.
sigmas = [sigmas[0]] + sigmas
hist_smooth = np.empty((rings + 1,) + hist.shape, dtype=float_dtype)
for i in range(rings + 1):
for j in range(orientations):
hist_smooth[i, j, :, :] = gaussian(
hist[j, :, :], sigma=sigmas[i], mode='reflect'
)
# Assemble descriptor grid.
theta = [2 * pi * j / histograms for j in range(histograms)]
desc_dims = (rings * histograms + 1) * orientations
descs = np.empty(
(desc_dims, image.shape[0] - 2 * radius, image.shape[1] - 2 * radius),
dtype=float_dtype,
)
descs[:orientations, :, :] = hist_smooth[0, :, radius:-radius, radius:-radius]
idx = orientations
for i in range(rings):
for j in range(histograms):
y_min = radius + int(round(ring_radii[i] * math.sin(theta[j])))
y_max = descs.shape[1] + y_min
x_min = radius + int(round(ring_radii[i] * math.cos(theta[j])))
x_max = descs.shape[2] + x_min
descs[idx : idx + orientations, :, :] = hist_smooth[
i + 1, :, y_min:y_max, x_min:x_max
]
idx += orientations
descs = descs[:, ::step, ::step]
descs = descs.swapaxes(0, 1).swapaxes(1, 2)
# Normalize descriptors.
if normalization != 'off':
descs += 1e-10
if normalization == 'l1':
descs /= np.sum(descs, axis=2)[:, :, np.newaxis]
elif normalization == 'l2':
descs /= sqrt(np.sum(descs**2, axis=2))[:, :, np.newaxis]
elif normalization == 'daisy':
for i in range(0, desc_dims, orientations):
norms = sqrt(np.sum(descs[:, :, i : i + orientations] ** 2, axis=2))
descs[:, :, i : i + orientations] /= norms[:, :, np.newaxis]
if visualize:
descs_img = gray2rgb(image)
for i in range(descs.shape[0]):
for j in range(descs.shape[1]):
# Draw center histogram sigma
color = [1, 0, 0]
desc_y = i * step + radius
desc_x = j * step + radius
rows, cols, val = draw.circle_perimeter_aa(
desc_y, desc_x, int(sigmas[0])
)
draw.set_color(descs_img, (rows, cols), color, alpha=val)
max_bin = np.max(descs[i, j, :])
for o_num, o in enumerate(orientation_angles):
# Draw center histogram bins
bin_size = descs[i, j, o_num] / max_bin
dy = sigmas[0] * bin_size * math.sin(o)
dx = sigmas[0] * bin_size * math.cos(o)
rows, cols, val = draw.line_aa(
desc_y, desc_x, int(desc_y + dy), int(desc_x + dx)
)
draw.set_color(descs_img, (rows, cols), color, alpha=val)
for r_num, r in enumerate(ring_radii):
color_offset = float(1 + r_num) / rings
color = (1 - color_offset, 1, color_offset)
for t_num, t in enumerate(theta):
# Draw ring histogram sigmas
hist_y = desc_y + int(round(r * math.sin(t)))
hist_x = desc_x + int(round(r * math.cos(t)))
rows, cols, val = draw.circle_perimeter_aa(
hist_y, hist_x, int(sigmas[r_num + 1])
)
draw.set_color(descs_img, (rows, cols), color, alpha=val)
for o_num, o in enumerate(orientation_angles):
# Draw histogram bins
bin_size = descs[
i,
j,
orientations
+ r_num * histograms * orientations
+ t_num * orientations
+ o_num,
]
bin_size /= max_bin
dy = sigmas[r_num + 1] * bin_size * math.sin(o)
dx = sigmas[r_num + 1] * bin_size * math.cos(o)
rows, cols, val = draw.line_aa(
hist_y, hist_x, int(hist_y + dy), int(hist_x + dx)
)
draw.set_color(descs_img, (rows, cols), color, alpha=val)
return descs, descs_img
else:
return descs

View File

@@ -0,0 +1,265 @@
"""
fisher_vector.py - Implementation of the Fisher vector encoding algorithm
This module contains the source code for Fisher vector computation. The
computation is separated into two distinct steps, which are called separately
by the user, namely:
learn_gmm: Used to estimate the GMM for all vectors/descriptors computed for
all examples in the dataset (e.g. estimated using all the SIFT
vectors computed for all images in the dataset, or at least a subset
of this).
fisher_vector: Used to compute the Fisher vector representation for a
single set of descriptors/vector (e.g. the SIFT
descriptors for a single image in your dataset, or
perhaps a test image).
Reference: Perronnin, F. and Dance, C. Fisher kernels on Visual Vocabularies
for Image Categorization, IEEE Conference on Computer Vision and
Pattern Recognition, 2007
Origin Author: Dan Oneata (Author of the original implementation for the Fisher
vector computation using scikit-learn and NumPy. Subsequently ported to
scikit-image (here) by other authors.)
"""
import numpy as np
class FisherVectorException(Exception):
pass
class DescriptorException(FisherVectorException):
pass
def learn_gmm(descriptors, *, n_modes=32, gm_args=None):
"""Estimate a Gaussian mixture model (GMM) given a set of descriptors and
number of modes (i.e. Gaussians). This function is essentially a wrapper
around the scikit-learn implementation of GMM, namely the
:class:`sklearn.mixture.GaussianMixture` class.
Due to the nature of the Fisher vector, the only enforced parameter of the
underlying scikit-learn class is the covariance_type, which must be 'diag'.
There is no simple way to know what value to use for `n_modes` a-priori.
Typically, the value is usually one of ``{16, 32, 64, 128}``. One may train
a few GMMs and choose the one that maximises the log probability of the
GMM, or choose `n_modes` such that the downstream classifier trained on
the resultant Fisher vectors has maximal performance.
Parameters
----------
descriptors : np.ndarray (N, M) or list [(N1, M), (N2, M), ...]
List of NumPy arrays, or a single NumPy array, of the descriptors
used to estimate the GMM. The reason a list of NumPy arrays is
permissible is because often when using a Fisher vector encoding,
descriptors/vectors are computed separately for each sample/image in
the dataset, such as SIFT vectors for each image. If a list if passed
in, then each element must be a NumPy array in which the number of
rows may differ (e.g. different number of SIFT vector for each image),
but the number of columns for each must be the same (i.e. the
dimensionality must be the same).
n_modes : int
The number of modes/Gaussians to estimate during the GMM estimate.
gm_args : dict
Keyword arguments that can be passed into the underlying scikit-learn
:class:`sklearn.mixture.GaussianMixture` class.
Returns
-------
gmm : :class:`sklearn.mixture.GaussianMixture`
The estimated GMM object, which contains the necessary parameters
needed to compute the Fisher vector.
References
----------
.. [1] https://scikit-learn.org/stable/modules/generated/sklearn.mixture.GaussianMixture.html
Examples
--------
.. testsetup::
>>> import pytest; _ = pytest.importorskip('sklearn')
>>> from skimage.feature import fisher_vector
>>> rng = np.random.Generator(np.random.PCG64())
>>> sift_for_images = [rng.standard_normal((10, 128)) for _ in range(10)]
>>> num_modes = 16
>>> # Estimate 16-mode GMM with these synthetic SIFT vectors
>>> gmm = learn_gmm(sift_for_images, n_modes=num_modes)
"""
try:
from sklearn.mixture import GaussianMixture
except ImportError:
raise ImportError(
'scikit-learn is not installed. Please ensure it is installed in '
'order to use the Fisher vector functionality.'
)
if not isinstance(descriptors, (list, np.ndarray)):
raise DescriptorException(
'Please ensure descriptors are either a NumPy array, '
'or a list of NumPy arrays.'
)
d_mat_1 = descriptors[0]
if isinstance(descriptors, list) and not isinstance(d_mat_1, np.ndarray):
raise DescriptorException(
'Please ensure descriptors are a list of NumPy arrays.'
)
if isinstance(descriptors, list):
expected_shape = descriptors[0].shape
ranks = [len(e.shape) == len(expected_shape) for e in descriptors]
if not all(ranks):
raise DescriptorException(
'Please ensure all elements of your descriptor list ' 'are of rank 2.'
)
dims = [e.shape[1] == descriptors[0].shape[1] for e in descriptors]
if not all(dims):
raise DescriptorException(
'Please ensure all descriptors are of the same dimensionality.'
)
if not isinstance(n_modes, int) or n_modes <= 0:
raise FisherVectorException('Please ensure n_modes is a positive integer.')
if gm_args:
has_cov_type = 'covariance_type' in gm_args
cov_type_not_diag = gm_args['covariance_type'] != 'diag'
if has_cov_type and cov_type_not_diag:
raise FisherVectorException('Covariance type must be "diag".')
if isinstance(descriptors, list):
descriptors = np.vstack(descriptors)
if gm_args:
has_cov_type = 'covariance_type' in gm_args
if has_cov_type:
gmm = GaussianMixture(n_components=n_modes, **gm_args)
else:
gmm = GaussianMixture(
n_components=n_modes, covariance_type='diag', **gm_args
)
else:
gmm = GaussianMixture(n_components=n_modes, covariance_type='diag')
gmm.fit(descriptors)
return gmm
def fisher_vector(descriptors, gmm, *, improved=False, alpha=0.5):
"""Compute the Fisher vector given some descriptors/vectors,
and an associated estimated GMM.
Parameters
----------
descriptors : np.ndarray, shape=(n_descriptors, descriptor_length)
NumPy array of the descriptors for which the Fisher vector
representation is to be computed.
gmm : :class:`sklearn.mixture.GaussianMixture`
An estimated GMM object, which contains the necessary parameters needed
to compute the Fisher vector.
improved : bool, default=False
Flag denoting whether to compute improved Fisher vectors or not.
Improved Fisher vectors are L2 and power normalized. Power
normalization is simply f(z) = sign(z) pow(abs(z), alpha) for some
0 <= alpha <= 1.
alpha : float, default=0.5
The parameter for the power normalization step. Ignored if
improved=False.
Returns
-------
fisher_vector : np.ndarray
The computation Fisher vector, which is given by a concatenation of the
gradients of a GMM with respect to its parameters (mixture weights,
means, and covariance matrices). For D-dimensional input descriptors or
vectors, and a K-mode GMM, the Fisher vector dimensionality will be
2KD + K. Thus, its dimensionality is invariant to the number of
descriptors/vectors.
References
----------
.. [1] Perronnin, F. and Dance, C. Fisher kernels on Visual Vocabularies
for Image Categorization, IEEE Conference on Computer Vision and
Pattern Recognition, 2007
.. [2] Perronnin, F. and Sanchez, J. and Mensink T. Improving the Fisher
Kernel for Large-Scale Image Classification, ECCV, 2010
Examples
--------
.. testsetup::
>>> import pytest; _ = pytest.importorskip('sklearn')
>>> from skimage.feature import fisher_vector, learn_gmm
>>> sift_for_images = [np.random.random((10, 128)) for _ in range(10)]
>>> num_modes = 16
>>> # Estimate 16-mode GMM with these synthetic SIFT vectors
>>> gmm = learn_gmm(sift_for_images, n_modes=num_modes)
>>> test_image_descriptors = np.random.random((25, 128))
>>> # Compute the Fisher vector
>>> fv = fisher_vector(test_image_descriptors, gmm)
"""
try:
from sklearn.mixture import GaussianMixture
except ImportError:
raise ImportError(
'scikit-learn is not installed. Please ensure it is installed in '
'order to use the Fisher vector functionality.'
)
if not isinstance(descriptors, np.ndarray):
raise DescriptorException('Please ensure descriptors is a NumPy array.')
if not isinstance(gmm, GaussianMixture):
raise FisherVectorException(
'Please ensure gmm is a sklearn.mixture.GaussianMixture object.'
)
if improved and not isinstance(alpha, float):
raise FisherVectorException(
'Please ensure that the alpha parameter is a float.'
)
num_descriptors = len(descriptors)
mixture_weights = gmm.weights_
means = gmm.means_
covariances = gmm.covariances_
posterior_probabilities = gmm.predict_proba(descriptors)
# Statistics necessary to compute GMM gradients wrt its parameters
pp_sum = posterior_probabilities.mean(axis=0, keepdims=True).T
pp_x = posterior_probabilities.T.dot(descriptors) / num_descriptors
pp_x_2 = posterior_probabilities.T.dot(np.power(descriptors, 2)) / num_descriptors
# Compute GMM gradients wrt its parameters
d_pi = pp_sum.squeeze() - mixture_weights
d_mu = pp_x - pp_sum * means
d_sigma_t1 = pp_sum * np.power(means, 2)
d_sigma_t2 = pp_sum * covariances
d_sigma_t3 = 2 * pp_x * means
d_sigma = -pp_x_2 - d_sigma_t1 + d_sigma_t2 + d_sigma_t3
# Apply analytical diagonal normalization
sqrt_mixture_weights = np.sqrt(mixture_weights)
d_pi /= sqrt_mixture_weights
d_mu /= sqrt_mixture_weights[:, np.newaxis] * np.sqrt(covariances)
d_sigma /= np.sqrt(2) * sqrt_mixture_weights[:, np.newaxis] * covariances
# Concatenate GMM gradients to form Fisher vector representation
fisher_vector = np.hstack((d_pi, d_mu.ravel(), d_sigma.ravel()))
if improved:
fisher_vector = np.sign(fisher_vector) * np.power(np.abs(fisher_vector), alpha)
fisher_vector = fisher_vector / np.linalg.norm(fisher_vector)
return fisher_vector

View File

@@ -0,0 +1,341 @@
import numpy as np
from . import _hoghistogram
from .._shared import utils
def _hog_normalize_block(block, method, eps=1e-5):
if method == 'L1':
out = block / (np.sum(np.abs(block)) + eps)
elif method == 'L1-sqrt':
out = np.sqrt(block / (np.sum(np.abs(block)) + eps))
elif method == 'L2':
out = block / np.sqrt(np.sum(block**2) + eps**2)
elif method == 'L2-Hys':
out = block / np.sqrt(np.sum(block**2) + eps**2)
out = np.minimum(out, 0.2)
out = out / np.sqrt(np.sum(out**2) + eps**2)
else:
raise ValueError('Selected block normalization method is invalid.')
return out
def _hog_channel_gradient(channel):
"""Compute unnormalized gradient image along `row` and `col` axes.
Parameters
----------
channel : (M, N) ndarray
Grayscale image or one of image channel.
Returns
-------
g_row, g_col : channel gradient along `row` and `col` axes correspondingly.
"""
g_row = np.empty(channel.shape, dtype=channel.dtype)
g_row[0, :] = 0
g_row[-1, :] = 0
g_row[1:-1, :] = channel[2:, :] - channel[:-2, :]
g_col = np.empty(channel.shape, dtype=channel.dtype)
g_col[:, 0] = 0
g_col[:, -1] = 0
g_col[:, 1:-1] = channel[:, 2:] - channel[:, :-2]
return g_row, g_col
@utils.channel_as_last_axis(multichannel_output=False)
def hog(
image,
orientations=9,
pixels_per_cell=(8, 8),
cells_per_block=(3, 3),
block_norm='L2-Hys',
visualize=False,
transform_sqrt=False,
feature_vector=True,
*,
channel_axis=None,
):
"""Extract Histogram of Oriented Gradients (HOG) for a given image.
Compute a Histogram of Oriented Gradients (HOG) by
1. (optional) global image normalization
2. computing the gradient image in `row` and `col`
3. computing gradient histograms
4. normalizing across blocks
5. flattening into a feature vector
Parameters
----------
image : (M, N[, C]) ndarray
Input image.
orientations : int, optional
Number of orientation bins.
pixels_per_cell : 2-tuple (int, int), optional
Size (in pixels) of a cell.
cells_per_block : 2-tuple (int, int), optional
Number of cells in each block.
block_norm : str {'L1', 'L1-sqrt', 'L2', 'L2-Hys'}, optional
Block normalization method:
``L1``
Normalization using L1-norm.
``L1-sqrt``
Normalization using L1-norm, followed by square root.
``L2``
Normalization using L2-norm.
``L2-Hys``
Normalization using L2-norm, followed by limiting the
maximum values to 0.2 (`Hys` stands for `hysteresis`) and
renormalization using L2-norm. (default)
For details, see [3]_, [4]_.
visualize : bool, optional
Also return an image of the HOG. For each cell and orientation bin,
the image contains a line segment that is centered at the cell center,
is perpendicular to the midpoint of the range of angles spanned by the
orientation bin, and has intensity proportional to the corresponding
histogram value.
transform_sqrt : bool, optional
Apply power law compression to normalize the image before
processing. DO NOT use this if the image contains negative
values. Also see `notes` section below.
feature_vector : bool, optional
Return the data as a feature vector by calling .ravel() on the result
just before returning.
channel_axis : int or None, optional
If None, the image is assumed to be a grayscale (single channel) image.
Otherwise, this parameter indicates which axis of the array corresponds
to channels.
.. versionadded:: 0.19
`channel_axis` was added in 0.19.
Returns
-------
out : (n_blocks_row, n_blocks_col, n_cells_row, n_cells_col, n_orient) ndarray
HOG descriptor for the image. If `feature_vector` is True, a 1D
(flattened) array is returned.
hog_image : (M, N) ndarray, optional
A visualisation of the HOG image. Only provided if `visualize` is True.
Raises
------
ValueError
If the image is too small given the values of pixels_per_cell and
cells_per_block.
References
----------
.. [1] https://en.wikipedia.org/wiki/Histogram_of_oriented_gradients
.. [2] Dalal, N and Triggs, B, Histograms of Oriented Gradients for
Human Detection, IEEE Computer Society Conference on Computer
Vision and Pattern Recognition 2005 San Diego, CA, USA,
https://lear.inrialpes.fr/people/triggs/pubs/Dalal-cvpr05.pdf,
:DOI:`10.1109/CVPR.2005.177`
.. [3] Lowe, D.G., Distinctive image features from scale-invatiant
keypoints, International Journal of Computer Vision (2004) 60: 91,
http://www.cs.ubc.ca/~lowe/papers/ijcv04.pdf,
:DOI:`10.1023/B:VISI.0000029664.99615.94`
.. [4] Dalal, N, Finding People in Images and Videos,
Human-Computer Interaction [cs.HC], Institut National Polytechnique
de Grenoble - INPG, 2006,
https://tel.archives-ouvertes.fr/tel-00390303/file/NavneetDalalThesis.pdf
Notes
-----
The presented code implements the HOG extraction method from [2]_ with
the following changes: (I) blocks of (3, 3) cells are used ((2, 2) in the
paper); (II) no smoothing within cells (Gaussian spatial window with sigma=8pix
in the paper); (III) L1 block normalization is used (L2-Hys in the paper).
Power law compression, also known as Gamma correction, is used to reduce
the effects of shadowing and illumination variations. The compression makes
the dark regions lighter. When the kwarg `transform_sqrt` is set to
``True``, the function computes the square root of each color channel
and then applies the hog algorithm to the image.
"""
image = np.atleast_2d(image)
float_dtype = utils._supported_float_type(image.dtype)
image = image.astype(float_dtype, copy=False)
multichannel = channel_axis is not None
ndim_spatial = image.ndim - 1 if multichannel else image.ndim
if ndim_spatial != 2:
raise ValueError(
'Only images with two spatial dimensions are '
'supported. If using with color/multichannel '
'images, specify `channel_axis`.'
)
"""
The first stage applies an optional global image normalization
equalisation that is designed to reduce the influence of illumination
effects. In practice we use gamma (power law) compression, either
computing the square root or the log of each color channel.
Image texture strength is typically proportional to the local surface
illumination so this compression helps to reduce the effects of local
shadowing and illumination variations.
"""
if transform_sqrt:
image = np.sqrt(image)
"""
The second stage computes first order image gradients. These capture
contour, silhouette and some texture information, while providing
further resistance to illumination variations. The locally dominant
color channel is used, which provides color invariance to a large
extent. Variant methods may also include second order image derivatives,
which act as primitive bar detectors - a useful feature for capturing,
e.g. bar like structures in bicycles and limbs in humans.
"""
if multichannel:
g_row_by_ch = np.empty_like(image, dtype=float_dtype)
g_col_by_ch = np.empty_like(image, dtype=float_dtype)
g_magn = np.empty_like(image, dtype=float_dtype)
for idx_ch in range(image.shape[2]):
(
g_row_by_ch[:, :, idx_ch],
g_col_by_ch[:, :, idx_ch],
) = _hog_channel_gradient(image[:, :, idx_ch])
g_magn[:, :, idx_ch] = np.hypot(
g_row_by_ch[:, :, idx_ch], g_col_by_ch[:, :, idx_ch]
)
# For each pixel select the channel with the highest gradient magnitude
idcs_max = g_magn.argmax(axis=2)
rr, cc = np.meshgrid(
np.arange(image.shape[0]),
np.arange(image.shape[1]),
indexing='ij',
sparse=True,
)
g_row = g_row_by_ch[rr, cc, idcs_max]
g_col = g_col_by_ch[rr, cc, idcs_max]
else:
g_row, g_col = _hog_channel_gradient(image)
"""
The third stage aims to produce an encoding that is sensitive to
local image content while remaining resistant to small changes in
pose or appearance. The adopted method pools gradient orientation
information locally in the same way as the SIFT [Lowe 2004]
feature. The image window is divided into small spatial regions,
called "cells". For each cell we accumulate a local 1-D histogram
of gradient or edge orientations over all the pixels in the
cell. This combined cell-level 1-D histogram forms the basic
"orientation histogram" representation. Each orientation histogram
divides the gradient angle range into a fixed number of
predetermined bins. The gradient magnitudes of the pixels in the
cell are used to vote into the orientation histogram.
"""
s_row, s_col = image.shape[:2]
c_row, c_col = pixels_per_cell
b_row, b_col = cells_per_block
n_cells_row = int(s_row // c_row) # number of cells along row-axis
n_cells_col = int(s_col // c_col) # number of cells along col-axis
# compute orientations integral images
orientation_histogram = np.zeros(
(n_cells_row, n_cells_col, orientations), dtype=float
)
g_row = g_row.astype(float, copy=False)
g_col = g_col.astype(float, copy=False)
_hoghistogram.hog_histograms(
g_col,
g_row,
c_col,
c_row,
s_col,
s_row,
n_cells_col,
n_cells_row,
orientations,
orientation_histogram,
)
# now compute the histogram for each cell
hog_image = None
if visualize:
from .. import draw
radius = min(c_row, c_col) // 2 - 1
orientations_arr = np.arange(orientations)
# set dr_arr, dc_arr to correspond to midpoints of orientation bins
orientation_bin_midpoints = np.pi * (orientations_arr + 0.5) / orientations
dr_arr = radius * np.sin(orientation_bin_midpoints)
dc_arr = radius * np.cos(orientation_bin_midpoints)
hog_image = np.zeros((s_row, s_col), dtype=float_dtype)
for r in range(n_cells_row):
for c in range(n_cells_col):
for o, dr, dc in zip(orientations_arr, dr_arr, dc_arr):
centre = tuple([r * c_row + c_row // 2, c * c_col + c_col // 2])
rr, cc = draw.line(
int(centre[0] - dc),
int(centre[1] + dr),
int(centre[0] + dc),
int(centre[1] - dr),
)
hog_image[rr, cc] += orientation_histogram[r, c, o]
"""
The fourth stage computes normalization, which takes local groups of
cells and contrast normalizes their overall responses before passing
to next stage. Normalization introduces better invariance to illumination,
shadowing, and edge contrast. It is performed by accumulating a measure
of local histogram "energy" over local groups of cells that we call
"blocks". The result is used to normalize each cell in the block.
Typically each individual cell is shared between several blocks, but
its normalizations are block dependent and thus different. The cell
thus appears several times in the final output vector with different
normalizations. This may seem redundant but it improves the performance.
We refer to the normalized block descriptors as Histogram of Oriented
Gradient (HOG) descriptors.
"""
n_blocks_row = (n_cells_row - b_row) + 1
n_blocks_col = (n_cells_col - b_col) + 1
if n_blocks_col <= 0 or n_blocks_row <= 0:
min_row = b_row * c_row
min_col = b_col * c_col
raise ValueError(
'The input image is too small given the values of '
'pixels_per_cell and cells_per_block. '
'It should have at least: '
f'{min_row} rows and {min_col} cols.'
)
normalized_blocks = np.zeros(
(n_blocks_row, n_blocks_col, b_row, b_col, orientations), dtype=float_dtype
)
for r in range(n_blocks_row):
for c in range(n_blocks_col):
block = orientation_histogram[r : r + b_row, c : c + b_col, :]
normalized_blocks[r, c, :] = _hog_normalize_block(block, method=block_norm)
"""
The final step collects the HOG descriptors from all blocks of a dense
overlapping grid of blocks covering the detection window into a combined
feature vector for use in the window classifier.
"""
if feature_vector:
normalized_blocks = normalized_blocks.ravel()
if visualize:
return normalized_blocks, hog_image
else:
return normalized_blocks

View File

@@ -0,0 +1,10 @@
import os
import numpy as np
# Putting this in cython was giving strange bugs for different versions
# of cython which seemed to indicate troubles with the __file__ variable
# not being defined. Keeping it in pure python makes it more reliable
this_dir = os.path.dirname(__file__)
POS = np.loadtxt(os.path.join(this_dir, "orb_descriptor_positions.txt"), dtype=np.int8)
POS0 = np.ascontiguousarray(POS[:, :2])
POS1 = np.ascontiguousarray(POS[:, 2:])

View File

@@ -0,0 +1,715 @@
import math
import numpy as np
import scipy.ndimage as ndi
from scipy import spatial
from .._shared.filters import gaussian
from .._shared.utils import _supported_float_type, check_nD
from ..transform import integral_image
from ..util import img_as_float
from ._hessian_det_appx import _hessian_matrix_det
from .peak import peak_local_max
# This basic blob detection algorithm is based on:
# http://www.cs.utah.edu/~jfishbau/advimproc/project1/ (04.04.2013)
# Theory behind: https://en.wikipedia.org/wiki/Blob_detection (04.04.2013)
def _compute_disk_overlap(d, r1, r2):
"""
Compute fraction of surface overlap between two disks of radii
``r1`` and ``r2``, with centers separated by a distance ``d``.
Parameters
----------
d : float
Distance between centers.
r1 : float
Radius of the first disk.
r2 : float
Radius of the second disk.
Returns
-------
fraction: float
Fraction of area of the overlap between the two disks.
"""
ratio1 = (d**2 + r1**2 - r2**2) / (2 * d * r1)
ratio1 = np.clip(ratio1, -1, 1)
acos1 = math.acos(ratio1)
ratio2 = (d**2 + r2**2 - r1**2) / (2 * d * r2)
ratio2 = np.clip(ratio2, -1, 1)
acos2 = math.acos(ratio2)
a = -d + r2 + r1
b = d - r2 + r1
c = d + r2 - r1
d = d + r2 + r1
area = r1**2 * acos1 + r2**2 * acos2 - 0.5 * math.sqrt(abs(a * b * c * d))
return area / (math.pi * (min(r1, r2) ** 2))
def _compute_sphere_overlap(d, r1, r2):
"""
Compute volume overlap fraction between two spheres of radii
``r1`` and ``r2``, with centers separated by a distance ``d``.
Parameters
----------
d : float
Distance between centers.
r1 : float
Radius of the first sphere.
r2 : float
Radius of the second sphere.
Returns
-------
fraction: float
Fraction of volume of the overlap between the two spheres.
Notes
-----
See for example http://mathworld.wolfram.com/Sphere-SphereIntersection.html
for more details.
"""
vol = (
math.pi
/ (12 * d)
* (r1 + r2 - d) ** 2
* (d**2 + 2 * d * (r1 + r2) - 3 * (r1**2 + r2**2) + 6 * r1 * r2)
)
return vol / (4.0 / 3 * math.pi * min(r1, r2) ** 3)
def _blob_overlap(blob1, blob2, *, sigma_dim=1):
"""Finds the overlapping area fraction between two blobs.
Returns a float representing fraction of overlapped area. Note that 0.0
is *always* returned for dimension greater than 3.
Parameters
----------
blob1 : sequence of arrays
A sequence of ``(row, col, sigma)`` or ``(pln, row, col, sigma)``,
where ``row, col`` (or ``(pln, row, col)``) are coordinates
of blob and ``sigma`` is the standard deviation of the Gaussian kernel
which detected the blob.
blob2 : sequence of arrays
A sequence of ``(row, col, sigma)`` or ``(pln, row, col, sigma)``,
where ``row, col`` (or ``(pln, row, col)``) are coordinates
of blob and ``sigma`` is the standard deviation of the Gaussian kernel
which detected the blob.
sigma_dim : int, optional
The dimensionality of the sigma value. Can be 1 or the same as the
dimensionality of the blob space (2 or 3).
Returns
-------
f : float
Fraction of overlapped area (or volume in 3D).
"""
ndim = len(blob1) - sigma_dim
if ndim > 3:
return 0.0
root_ndim = math.sqrt(ndim)
# we divide coordinates by sigma * sqrt(ndim) to rescale space to isotropy,
# giving spheres of radius = 1 or < 1.
if blob1[-1] == blob2[-1] == 0:
return 0.0
elif blob1[-1] > blob2[-1]:
max_sigma = blob1[-sigma_dim:]
r1 = 1
r2 = blob2[-1] / blob1[-1]
else:
max_sigma = blob2[-sigma_dim:]
r2 = 1
r1 = blob1[-1] / blob2[-1]
pos1 = blob1[:ndim] / (max_sigma * root_ndim)
pos2 = blob2[:ndim] / (max_sigma * root_ndim)
d = np.sqrt(np.sum((pos2 - pos1) ** 2))
if d > r1 + r2: # centers farther than sum of radii, so no overlap
return 0.0
# one blob is inside the other
if d <= abs(r1 - r2):
return 1.0
if ndim == 2:
return _compute_disk_overlap(d, r1, r2)
else: # ndim=3 http://mathworld.wolfram.com/Sphere-SphereIntersection.html
return _compute_sphere_overlap(d, r1, r2)
def _prune_blobs(blobs_array, overlap, *, sigma_dim=1):
"""Eliminated blobs with area overlap.
Parameters
----------
blobs_array : ndarray
A 2d array with each row representing 3 (or 4) values,
``(row, col, sigma)`` or ``(pln, row, col, sigma)`` in 3D,
where ``(row, col)`` (``(pln, row, col)``) are coordinates of the blob
and ``sigma`` is the standard deviation of the Gaussian kernel which
detected the blob.
This array must not have a dimension of size 0.
overlap : float
A value between 0 and 1. If the fraction of area overlapping for 2
blobs is greater than `overlap` the smaller blob is eliminated.
sigma_dim : int, optional
The number of columns in ``blobs_array`` corresponding to sigmas rather
than positions.
Returns
-------
A : ndarray
`array` with overlapping blobs removed.
"""
sigma = blobs_array[:, -sigma_dim:].max()
distance = 2 * sigma * math.sqrt(blobs_array.shape[1] - sigma_dim)
tree = spatial.cKDTree(blobs_array[:, :-sigma_dim])
pairs = np.array(list(tree.query_pairs(distance)))
if len(pairs) == 0:
return blobs_array
else:
for i, j in pairs:
blob1, blob2 = blobs_array[i], blobs_array[j]
if _blob_overlap(blob1, blob2, sigma_dim=sigma_dim) > overlap:
# note: this test works even in the anisotropic case because
# all sigmas increase together.
if blob1[-1] > blob2[-1]:
blob2[-1] = 0
else:
blob1[-1] = 0
return np.stack([b for b in blobs_array if b[-1] > 0])
def _format_exclude_border(img_ndim, exclude_border):
"""Format an ``exclude_border`` argument as a tuple of ints for calling
``peak_local_max``.
"""
if isinstance(exclude_border, tuple):
if len(exclude_border) != img_ndim:
raise ValueError(
"`exclude_border` should have the same length as the "
"dimensionality of the image."
)
for exclude in exclude_border:
if not isinstance(exclude, int):
raise ValueError(
"exclude border, when expressed as a tuple, must only "
"contain ints."
)
return exclude_border + (0,)
elif isinstance(exclude_border, int):
return (exclude_border,) * img_ndim + (0,)
elif exclude_border is True:
raise ValueError("exclude_border cannot be True")
elif exclude_border is False:
return (0,) * (img_ndim + 1)
else:
raise ValueError(f'Unsupported value ({exclude_border}) for exclude_border')
def blob_dog(
image,
min_sigma=1,
max_sigma=50,
sigma_ratio=1.6,
threshold=0.5,
overlap=0.5,
*,
threshold_rel=None,
exclude_border=False,
):
r"""Finds blobs in the given grayscale image.
Blobs are found using the Difference of Gaussian (DoG) method [1]_, [2]_.
For each blob found, the method returns its coordinates and the standard
deviation of the Gaussian kernel that detected the blob.
Parameters
----------
image : ndarray
Input grayscale image, blobs are assumed to be light on dark
background (white on black).
min_sigma : scalar or sequence of scalars, optional
The minimum standard deviation for Gaussian kernel. Keep this low to
detect smaller blobs. The standard deviations of the Gaussian filter
are given for each axis as a sequence, or as a single number, in
which case it is equal for all axes.
max_sigma : scalar or sequence of scalars, optional
The maximum standard deviation for Gaussian kernel. Keep this high to
detect larger blobs. The standard deviations of the Gaussian filter
are given for each axis as a sequence, or as a single number, in
which case it is equal for all axes.
sigma_ratio : float, optional
The ratio between the standard deviation of Gaussian Kernels used for
computing the Difference of Gaussians
threshold : float or None, optional
The absolute lower bound for scale space maxima. Local maxima smaller
than `threshold` are ignored. Reduce this to detect blobs with lower
intensities. If `threshold_rel` is also specified, whichever threshold
is larger will be used. If None, `threshold_rel` is used instead.
overlap : float, optional
A value between 0 and 1. If the area of two blobs overlaps by a
fraction greater than `threshold`, the smaller blob is eliminated.
threshold_rel : float or None, optional
Minimum intensity of peaks, calculated as
``max(dog_space) * threshold_rel``, where ``dog_space`` refers to the
stack of Difference-of-Gaussian (DoG) images computed internally. This
should have a value between 0 and 1. If None, `threshold` is used
instead.
exclude_border : tuple of ints, int, or False, optional
If tuple of ints, the length of the tuple must match the input array's
dimensionality. Each element of the tuple will exclude peaks from
within `exclude_border`-pixels of the border of the image along that
dimension.
If nonzero int, `exclude_border` excludes peaks from within
`exclude_border`-pixels of the border of the image.
If zero or False, peaks are identified regardless of their
distance from the border.
Returns
-------
A : (n, image.ndim + sigma) ndarray
A 2d array with each row representing 2 coordinate values for a 2D
image, or 3 coordinate values for a 3D image, plus the sigma(s) used.
When a single sigma is passed, outputs are:
``(r, c, sigma)`` or ``(p, r, c, sigma)`` where ``(r, c)`` or
``(p, r, c)`` are coordinates of the blob and ``sigma`` is the standard
deviation of the Gaussian kernel which detected the blob. When an
anisotropic gaussian is used (sigmas per dimension), the detected sigma
is returned for each dimension.
See also
--------
skimage.filters.difference_of_gaussians
References
----------
.. [1] https://en.wikipedia.org/wiki/Blob_detection#The_difference_of_Gaussians_approach
.. [2] Lowe, D. G. "Distinctive Image Features from Scale-Invariant
Keypoints." International Journal of Computer Vision 60, 91110 (2004).
https://www.cs.ubc.ca/~lowe/papers/ijcv04.pdf
:DOI:`10.1023/B:VISI.0000029664.99615.94`
Examples
--------
>>> from skimage import data, feature
>>> coins = data.coins()
>>> feature.blob_dog(coins, threshold=.05, min_sigma=10, max_sigma=40)
array([[128., 155., 10.],
[198., 155., 10.],
[124., 338., 10.],
[127., 102., 10.],
[193., 281., 10.],
[126., 208., 10.],
[267., 115., 10.],
[197., 102., 10.],
[198., 215., 10.],
[123., 279., 10.],
[126., 46., 10.],
[259., 247., 10.],
[196., 43., 10.],
[ 54., 276., 10.],
[267., 358., 10.],
[ 58., 100., 10.],
[259., 305., 10.],
[185., 347., 16.],
[261., 174., 16.],
[ 46., 336., 16.],
[ 54., 217., 10.],
[ 55., 157., 10.],
[ 57., 41., 10.],
[260., 47., 16.]])
Notes
-----
The radius of each blob is approximately :math:`\sqrt{2}\sigma` for
a 2-D image and :math:`\sqrt{3}\sigma` for a 3-D image.
""" # noqa: E501
image = img_as_float(image)
float_dtype = _supported_float_type(image.dtype)
image = image.astype(float_dtype, copy=False)
# if both min and max sigma are scalar, function returns only one sigma
scalar_sigma = np.isscalar(max_sigma) and np.isscalar(min_sigma)
# Gaussian filter requires that sequence-type sigmas have same
# dimensionality as image. This broadcasts scalar kernels
if np.isscalar(max_sigma):
max_sigma = np.full(image.ndim, max_sigma, dtype=float_dtype)
if np.isscalar(min_sigma):
min_sigma = np.full(image.ndim, min_sigma, dtype=float_dtype)
# Convert sequence types to array
min_sigma = np.asarray(min_sigma, dtype=float_dtype)
max_sigma = np.asarray(max_sigma, dtype=float_dtype)
if sigma_ratio <= 1.0:
raise ValueError('sigma_ratio must be > 1.0')
# k such that min_sigma*(sigma_ratio**k) > max_sigma
k = int(np.mean(np.log(max_sigma / min_sigma) / np.log(sigma_ratio) + 1))
# a geometric progression of standard deviations for gaussian kernels
sigma_list = np.array([min_sigma * (sigma_ratio**i) for i in range(k + 1)])
# computing difference between two successive Gaussian blurred images
# to obtain an approximation of the scale invariant Laplacian of the
# Gaussian operator
dog_image_cube = np.empty(image.shape + (k,), dtype=float_dtype)
gaussian_previous = gaussian(image, sigma=sigma_list[0], mode='reflect')
for i, s in enumerate(sigma_list[1:]):
gaussian_current = gaussian(image, sigma=s, mode='reflect')
dog_image_cube[..., i] = gaussian_previous - gaussian_current
gaussian_previous = gaussian_current
# normalization factor for consistency in DoG magnitude
sf = 1 / (sigma_ratio - 1)
dog_image_cube *= sf
exclude_border = _format_exclude_border(image.ndim, exclude_border)
local_maxima = peak_local_max(
dog_image_cube,
threshold_abs=threshold,
threshold_rel=threshold_rel,
exclude_border=exclude_border,
footprint=np.ones((3,) * (image.ndim + 1)),
)
# Catch no peaks
if local_maxima.size == 0:
return np.empty((0, image.ndim + (1 if scalar_sigma else image.ndim)))
# Convert local_maxima to float64
lm = local_maxima.astype(float_dtype)
# translate final column of lm, which contains the index of the
# sigma that produced the maximum intensity value, into the sigma
sigmas_of_peaks = sigma_list[local_maxima[:, -1]]
if scalar_sigma:
# select one sigma column, keeping dimension
sigmas_of_peaks = sigmas_of_peaks[:, 0:1]
# Remove sigma index and replace with sigmas
lm = np.hstack([lm[:, :-1], sigmas_of_peaks])
sigma_dim = sigmas_of_peaks.shape[1]
return _prune_blobs(lm, overlap, sigma_dim=sigma_dim)
def blob_log(
image,
min_sigma=1,
max_sigma=50,
num_sigma=10,
threshold=0.2,
overlap=0.5,
log_scale=False,
*,
threshold_rel=None,
exclude_border=False,
):
r"""Finds blobs in the given grayscale image.
Blobs are found using the Laplacian of Gaussian (LoG) method [1]_.
For each blob found, the method returns its coordinates and the standard
deviation of the Gaussian kernel that detected the blob.
Parameters
----------
image : ndarray
Input grayscale image, blobs are assumed to be light on dark
background (white on black).
min_sigma : scalar or sequence of scalars, optional
the minimum standard deviation for Gaussian kernel. Keep this low to
detect smaller blobs. The standard deviations of the Gaussian filter
are given for each axis as a sequence, or as a single number, in
which case it is equal for all axes.
max_sigma : scalar or sequence of scalars, optional
The maximum standard deviation for Gaussian kernel. Keep this high to
detect larger blobs. The standard deviations of the Gaussian filter
are given for each axis as a sequence, or as a single number, in
which case it is equal for all axes.
num_sigma : int, optional
The number of intermediate values of standard deviations to consider
between `min_sigma` and `max_sigma`.
threshold : float or None, optional
The absolute lower bound for scale space maxima. Local maxima smaller
than `threshold` are ignored. Reduce this to detect blobs with lower
intensities. If `threshold_rel` is also specified, whichever threshold
is larger will be used. If None, `threshold_rel` is used instead.
overlap : float, optional
A value between 0 and 1. If the area of two blobs overlaps by a
fraction greater than `threshold`, the smaller blob is eliminated.
log_scale : bool, optional
If set intermediate values of standard deviations are interpolated
using a logarithmic scale to the base `10`. If not, linear
interpolation is used.
threshold_rel : float or None, optional
Minimum intensity of peaks, calculated as
``max(log_space) * threshold_rel``, where ``log_space`` refers to the
stack of Laplacian-of-Gaussian (LoG) images computed internally. This
should have a value between 0 and 1. If None, `threshold` is used
instead.
exclude_border : tuple of ints, int, or False, optional
If tuple of ints, the length of the tuple must match the input array's
dimensionality. Each element of the tuple will exclude peaks from
within `exclude_border`-pixels of the border of the image along that
dimension.
If nonzero int, `exclude_border` excludes peaks from within
`exclude_border`-pixels of the border of the image.
If zero or False, peaks are identified regardless of their
distance from the border.
Returns
-------
A : (n, image.ndim + sigma) ndarray
A 2d array with each row representing 2 coordinate values for a 2D
image, or 3 coordinate values for a 3D image, plus the sigma(s) used.
When a single sigma is passed, outputs are:
``(r, c, sigma)`` or ``(p, r, c, sigma)`` where ``(r, c)`` or
``(p, r, c)`` are coordinates of the blob and ``sigma`` is the standard
deviation of the Gaussian kernel which detected the blob. When an
anisotropic gaussian is used (sigmas per dimension), the detected sigma
is returned for each dimension.
References
----------
.. [1] https://en.wikipedia.org/wiki/Blob_detection#The_Laplacian_of_Gaussian
Examples
--------
>>> from skimage import data, feature, exposure
>>> img = data.coins()
>>> img = exposure.equalize_hist(img) # improves detection
>>> feature.blob_log(img, threshold = .3)
array([[124. , 336. , 11.88888889],
[198. , 155. , 11.88888889],
[194. , 213. , 17.33333333],
[121. , 272. , 17.33333333],
[263. , 244. , 17.33333333],
[194. , 276. , 17.33333333],
[266. , 115. , 11.88888889],
[128. , 154. , 11.88888889],
[260. , 174. , 17.33333333],
[198. , 103. , 11.88888889],
[126. , 208. , 11.88888889],
[127. , 102. , 11.88888889],
[263. , 302. , 17.33333333],
[197. , 44. , 11.88888889],
[185. , 344. , 17.33333333],
[126. , 46. , 11.88888889],
[113. , 323. , 1. ]])
Notes
-----
The radius of each blob is approximately :math:`\sqrt{2}\sigma` for
a 2-D image and :math:`\sqrt{3}\sigma` for a 3-D image.
""" # noqa: E501
image = img_as_float(image)
float_dtype = _supported_float_type(image.dtype)
image = image.astype(float_dtype, copy=False)
# if both min and max sigma are scalar, function returns only one sigma
scalar_sigma = True if np.isscalar(max_sigma) and np.isscalar(min_sigma) else False
# Gaussian filter requires that sequence-type sigmas have same
# dimensionality as image. This broadcasts scalar kernels
if np.isscalar(max_sigma):
max_sigma = np.full(image.ndim, max_sigma, dtype=float_dtype)
if np.isscalar(min_sigma):
min_sigma = np.full(image.ndim, min_sigma, dtype=float_dtype)
# Convert sequence types to array
min_sigma = np.asarray(min_sigma, dtype=float_dtype)
max_sigma = np.asarray(max_sigma, dtype=float_dtype)
if log_scale:
start = np.log10(min_sigma)
stop = np.log10(max_sigma)
sigma_list = np.logspace(start, stop, num_sigma)
else:
sigma_list = np.linspace(min_sigma, max_sigma, num_sigma)
# computing gaussian laplace
image_cube = np.empty(image.shape + (len(sigma_list),), dtype=float_dtype)
for i, s in enumerate(sigma_list):
# average s**2 provides scale invariance
image_cube[..., i] = -ndi.gaussian_laplace(image, s) * np.mean(s) ** 2
exclude_border = _format_exclude_border(image.ndim, exclude_border)
local_maxima = peak_local_max(
image_cube,
threshold_abs=threshold,
threshold_rel=threshold_rel,
exclude_border=exclude_border,
footprint=np.ones((3,) * (image.ndim + 1)),
)
# Catch no peaks
if local_maxima.size == 0:
return np.empty((0, image.ndim + (1 if scalar_sigma else image.ndim)))
# Convert local_maxima to float64
lm = local_maxima.astype(float_dtype)
# translate final column of lm, which contains the index of the
# sigma that produced the maximum intensity value, into the sigma
sigmas_of_peaks = sigma_list[local_maxima[:, -1]]
if scalar_sigma:
# select one sigma column, keeping dimension
sigmas_of_peaks = sigmas_of_peaks[:, 0:1]
# Remove sigma index and replace with sigmas
lm = np.hstack([lm[:, :-1], sigmas_of_peaks])
sigma_dim = sigmas_of_peaks.shape[1]
return _prune_blobs(lm, overlap, sigma_dim=sigma_dim)
def blob_doh(
image,
min_sigma=1,
max_sigma=30,
num_sigma=10,
threshold=0.01,
overlap=0.5,
log_scale=False,
*,
threshold_rel=None,
):
"""Finds blobs in the given grayscale image.
Blobs are found using the Determinant of Hessian method [1]_. For each blob
found, the method returns its coordinates and the standard deviation
of the Gaussian Kernel used for the Hessian matrix whose determinant
detected the blob. Determinant of Hessians is approximated using [2]_.
Parameters
----------
image : 2D ndarray
Input grayscale image.Blobs can either be light on dark or vice versa.
min_sigma : float, optional
The minimum standard deviation for Gaussian Kernel used to compute
Hessian matrix. Keep this low to detect smaller blobs.
max_sigma : float, optional
The maximum standard deviation for Gaussian Kernel used to compute
Hessian matrix. Keep this high to detect larger blobs.
num_sigma : int, optional
The number of intermediate values of standard deviations to consider
between `min_sigma` and `max_sigma`.
threshold : float or None, optional
The absolute lower bound for scale space maxima. Local maxima smaller
than `threshold` are ignored. Reduce this to detect blobs with lower
intensities. If `threshold_rel` is also specified, whichever threshold
is larger will be used. If None, `threshold_rel` is used instead.
overlap : float, optional
A value between 0 and 1. If the area of two blobs overlaps by a
fraction greater than `threshold`, the smaller blob is eliminated.
log_scale : bool, optional
If set intermediate values of standard deviations are interpolated
using a logarithmic scale to the base `10`. If not, linear
interpolation is used.
threshold_rel : float or None, optional
Minimum intensity of peaks, calculated as
``max(doh_space) * threshold_rel``, where ``doh_space`` refers to the
stack of Determinant-of-Hessian (DoH) images computed internally. This
should have a value between 0 and 1. If None, `threshold` is used
instead.
Returns
-------
A : (n, 3) ndarray
A 2d array with each row representing 3 values, ``(y,x,sigma)``
where ``(y,x)`` are coordinates of the blob and ``sigma`` is the
standard deviation of the Gaussian kernel of the Hessian Matrix whose
determinant detected the blob.
References
----------
.. [1] https://en.wikipedia.org/wiki/Blob_detection#The_determinant_of_the_Hessian
.. [2] Herbert Bay, Andreas Ess, Tinne Tuytelaars, Luc Van Gool,
"SURF: Speeded Up Robust Features"
ftp://ftp.vision.ee.ethz.ch/publications/articles/eth_biwi_00517.pdf
Examples
--------
>>> from skimage import data, feature
>>> img = data.coins()
>>> feature.blob_doh(img)
array([[197. , 153. , 20.33333333],
[124. , 336. , 20.33333333],
[126. , 153. , 20.33333333],
[195. , 100. , 23.55555556],
[192. , 212. , 23.55555556],
[121. , 271. , 30. ],
[126. , 101. , 20.33333333],
[193. , 275. , 23.55555556],
[123. , 205. , 20.33333333],
[270. , 363. , 30. ],
[265. , 113. , 23.55555556],
[262. , 243. , 23.55555556],
[185. , 348. , 30. ],
[156. , 302. , 30. ],
[123. , 44. , 23.55555556],
[260. , 173. , 30. ],
[197. , 44. , 20.33333333]])
Notes
-----
The radius of each blob is approximately `sigma`.
Computation of Determinant of Hessians is independent of the standard
deviation. Therefore detecting larger blobs won't take more time. In
methods line :py:meth:`blob_dog` and :py:meth:`blob_log` the computation
of Gaussians for larger `sigma` takes more time. The downside is that
this method can't be used for detecting blobs of radius less than `3px`
due to the box filters used in the approximation of Hessian Determinant.
""" # noqa: E501
check_nD(image, 2)
image = img_as_float(image)
float_dtype = _supported_float_type(image.dtype)
image = image.astype(float_dtype, copy=False)
image = integral_image(image)
if log_scale:
start, stop = math.log(min_sigma, 10), math.log(max_sigma, 10)
sigma_list = np.logspace(start, stop, num_sigma)
else:
sigma_list = np.linspace(min_sigma, max_sigma, num_sigma)
image_cube = np.empty(shape=image.shape + (len(sigma_list),), dtype=float_dtype)
for j, s in enumerate(sigma_list):
image_cube[..., j] = _hessian_matrix_det(image, s)
local_maxima = peak_local_max(
image_cube,
threshold_abs=threshold,
threshold_rel=threshold_rel,
exclude_border=False,
footprint=np.ones((3,) * image_cube.ndim),
)
# Catch no peaks
if local_maxima.size == 0:
return np.empty((0, 3))
# Convert local_maxima to float64
lm = local_maxima.astype(np.float64)
# Convert the last index to its corresponding scale value
lm[:, -1] = sigma_list[local_maxima[:, -1]]
return _prune_blobs(lm, overlap)

View File

@@ -0,0 +1,209 @@
import copy
import numpy as np
from .._shared.filters import gaussian
from .._shared.utils import check_nD
from .brief_cy import _brief_loop
from .util import (
DescriptorExtractor,
_mask_border_keypoints,
_prepare_grayscale_input_2D,
)
class BRIEF(DescriptorExtractor):
"""BRIEF binary descriptor extractor.
BRIEF (Binary Robust Independent Elementary Features) is an efficient
feature point descriptor. It is highly discriminative even when using
relatively few bits and is computed using simple intensity difference
tests.
For each keypoint, intensity comparisons are carried out for a specifically
distributed number N of pixel-pairs resulting in a binary descriptor of
length N. For binary descriptors the Hamming distance can be used for
feature matching, which leads to lower computational cost in comparison to
the L2 norm.
Parameters
----------
descriptor_size : int, optional
Size of BRIEF descriptor for each keypoint. Sizes 128, 256 and 512
recommended by the authors. Default is 256.
patch_size : int, optional
Length of the two dimensional square patch sampling region around
the keypoints. Default is 49.
mode : {'normal', 'uniform'}, optional
Probability distribution for sampling location of decision pixel-pairs
around keypoints.
rng : {`numpy.random.Generator`, int}, optional
Pseudo-random number generator (RNG).
By default, a PCG64 generator is used (see :func:`numpy.random.default_rng`).
If `rng` is an int, it is used to seed the generator.
The PRNG is used for the random sampling of the decision
pixel-pairs. From a square window with length `patch_size`,
pixel pairs are sampled using the `mode` parameter to build
the descriptors using intensity comparison.
For matching across images, the same `rng` should be used to construct
descriptors. To facilitate this:
(a) `rng` defaults to 1
(b) Subsequent calls of the ``extract`` method will use the same rng/seed.
sigma : float, optional
Standard deviation of the Gaussian low-pass filter applied to the image
to alleviate noise sensitivity, which is strongly recommended to obtain
discriminative and good descriptors.
Attributes
----------
descriptors : (Q, `descriptor_size`) array of dtype bool
2D ndarray of binary descriptors of size `descriptor_size` for Q
keypoints after filtering out border keypoints with value at an
index ``(i, j)`` either being ``True`` or ``False`` representing
the outcome of the intensity comparison for i-th keypoint on j-th
decision pixel-pair. It is ``Q == np.sum(mask)``.
mask : (N,) array of dtype bool
Mask indicating whether a keypoint has been filtered out
(``False``) or is described in the `descriptors` array (``True``).
Examples
--------
>>> from skimage.feature import (corner_harris, corner_peaks, BRIEF,
... match_descriptors)
>>> import numpy as np
>>> square1 = np.zeros((8, 8), dtype=np.int32)
>>> square1[2:6, 2:6] = 1
>>> square1
array([[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0]], dtype=int32)
>>> square2 = np.zeros((9, 9), dtype=np.int32)
>>> square2[2:7, 2:7] = 1
>>> square2
array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=int32)
>>> keypoints1 = corner_peaks(corner_harris(square1), min_distance=1)
>>> keypoints2 = corner_peaks(corner_harris(square2), min_distance=1)
>>> extractor = BRIEF(patch_size=5)
>>> extractor.extract(square1, keypoints1)
>>> descriptors1 = extractor.descriptors
>>> extractor.extract(square2, keypoints2)
>>> descriptors2 = extractor.descriptors
>>> matches = match_descriptors(descriptors1, descriptors2)
>>> matches
array([[0, 0],
[1, 1],
[2, 2],
[3, 3]])
>>> keypoints1[matches[:, 0]]
array([[2, 2],
[2, 5],
[5, 2],
[5, 5]])
>>> keypoints2[matches[:, 1]]
array([[2, 2],
[2, 6],
[6, 2],
[6, 6]])
"""
def __init__(
self, descriptor_size=256, patch_size=49, mode='normal', sigma=1, rng=1
):
mode = mode.lower()
if mode not in ('normal', 'uniform'):
raise ValueError("`mode` must be 'normal' or 'uniform'.")
self.descriptor_size = descriptor_size
self.patch_size = patch_size
self.mode = mode
self.sigma = sigma
if isinstance(rng, np.random.Generator):
# Spawn an independent RNG from parent RNG provided by the user.
# This is necessary so that we can safely deepcopy the RNG.
# See https://github.com/scikit-learn/scikit-learn/issues/16988#issuecomment-1518037853
bg = rng._bit_generator
ss = bg._seed_seq
(child_ss,) = ss.spawn(1)
self.rng = np.random.Generator(type(bg)(child_ss))
elif rng is None:
self.rng = np.random.default_rng(np.random.SeedSequence())
else:
self.rng = np.random.default_rng(rng)
self.descriptors = None
self.mask = None
def extract(self, image, keypoints):
"""Extract BRIEF binary descriptors for given keypoints in image.
Parameters
----------
image : 2D array
Input image.
keypoints : (N, 2) array
Keypoint coordinates as ``(row, col)``.
"""
check_nD(image, 2)
# Copy RNG so we can repeatedly call extract with the same random values
rng = copy.deepcopy(self.rng)
image = _prepare_grayscale_input_2D(image)
# Gaussian low-pass filtering to alleviate noise sensitivity
image = np.ascontiguousarray(gaussian(image, sigma=self.sigma, mode='reflect'))
# Sampling pairs of decision pixels in patch_size x patch_size window
desc_size = self.descriptor_size
patch_size = self.patch_size
if self.mode == 'normal':
samples = (patch_size / 5.0) * rng.standard_normal(desc_size * 8)
samples = np.array(samples, dtype=np.int32)
samples = samples[
(samples < (patch_size // 2)) & (samples > -(patch_size - 2) // 2)
]
pos1 = samples[: desc_size * 2].reshape(desc_size, 2)
pos2 = samples[desc_size * 2 : desc_size * 4].reshape(desc_size, 2)
elif self.mode == 'uniform':
samples = rng.integers(
-(patch_size - 2) // 2, (patch_size // 2) + 1, (desc_size * 2, 2)
)
samples = np.array(samples, dtype=np.int32)
pos1, pos2 = np.split(samples, 2)
pos1 = np.ascontiguousarray(pos1)
pos2 = np.ascontiguousarray(pos2)
# Removing keypoints that are within (patch_size / 2) distance from the
# image border
self.mask = _mask_border_keypoints(image.shape, keypoints, patch_size // 2)
keypoints = np.array(
keypoints[self.mask, :], dtype=np.int64, order='C', copy=False
)
self.descriptors = np.zeros(
(keypoints.shape[0], desc_size), dtype=bool, order='C'
)
_brief_loop(image, self.descriptors.view(np.uint8), keypoints, pos1, pos2)

View File

@@ -0,0 +1,343 @@
import numpy as np
from scipy.ndimage import maximum_filter, minimum_filter, convolve
from ..transform import integral_image
from .corner import structure_tensor
from ..morphology import octagon, star
from .censure_cy import _censure_dob_loop
from ..feature.util import (
FeatureDetector,
_prepare_grayscale_input_2D,
_mask_border_keypoints,
)
from .._shared.utils import check_nD
# The paper(Reference [1]) mentions the sizes of the Octagon shaped filter
# kernel for the first seven scales only. The sizes of the later scales
# have been extrapolated based on the following statement in the paper.
# "These octagons scale linearly and were experimentally chosen to correspond
# to the seven DOBs described in the previous section."
OCTAGON_OUTER_SHAPE = [
(5, 2),
(5, 3),
(7, 3),
(9, 4),
(9, 7),
(13, 7),
(15, 10),
(15, 11),
(15, 12),
(17, 13),
(17, 14),
]
OCTAGON_INNER_SHAPE = [
(3, 0),
(3, 1),
(3, 2),
(5, 2),
(5, 3),
(5, 4),
(5, 5),
(7, 5),
(7, 6),
(9, 6),
(9, 7),
]
# The sizes for the STAR shaped filter kernel for different scales have been
# taken from the OpenCV implementation.
STAR_SHAPE = [1, 2, 3, 4, 6, 8, 11, 12, 16, 22, 23, 32, 45, 46, 64, 90, 128]
STAR_FILTER_SHAPE = [
(1, 0),
(3, 1),
(4, 2),
(5, 3),
(7, 4),
(8, 5),
(9, 6),
(11, 8),
(13, 10),
(14, 11),
(15, 12),
(16, 14),
]
def _filter_image(image, min_scale, max_scale, mode):
response = np.zeros(
(image.shape[0], image.shape[1], max_scale - min_scale + 1), dtype=np.float64
)
if mode == 'dob':
# make response[:, :, i] contiguous memory block
item_size = response.itemsize
response.strides = (
item_size * response.shape[1],
item_size,
item_size * response.shape[0] * response.shape[1],
)
integral_img = integral_image(image)
for i in range(max_scale - min_scale + 1):
n = min_scale + i
# Constant multipliers for the outer region and the inner region
# of the bi-level filters with the constraint of keeping the
# DC bias 0.
inner_weight = 1.0 / (2 * n + 1) ** 2
outer_weight = 1.0 / (12 * n**2 + 4 * n)
_censure_dob_loop(
n, integral_img, response[:, :, i], inner_weight, outer_weight
)
# NOTE : For the Octagon shaped filter, we implemented and evaluated the
# slanted integral image based image filtering but the performance was
# more or less equal to image filtering using
# scipy.ndimage.filters.convolve(). Hence we have decided to use the
# later for a much cleaner implementation.
elif mode == 'octagon':
# TODO : Decide the shapes of Octagon filters for scales > 7
for i in range(max_scale - min_scale + 1):
mo, no = OCTAGON_OUTER_SHAPE[min_scale + i - 1]
mi, ni = OCTAGON_INNER_SHAPE[min_scale + i - 1]
response[:, :, i] = convolve(image, _octagon_kernel(mo, no, mi, ni))
elif mode == 'star':
for i in range(max_scale - min_scale + 1):
m = STAR_SHAPE[STAR_FILTER_SHAPE[min_scale + i - 1][0]]
n = STAR_SHAPE[STAR_FILTER_SHAPE[min_scale + i - 1][1]]
response[:, :, i] = convolve(image, _star_kernel(m, n))
return response
def _octagon_kernel(mo, no, mi, ni):
outer = (mo + 2 * no) ** 2 - 2 * no * (no + 1)
inner = (mi + 2 * ni) ** 2 - 2 * ni * (ni + 1)
outer_weight = 1.0 / (outer - inner)
inner_weight = 1.0 / inner
c = ((mo + 2 * no) - (mi + 2 * ni)) // 2
outer_oct = octagon(mo, no)
inner_oct = np.zeros((mo + 2 * no, mo + 2 * no))
inner_oct[c:-c, c:-c] = octagon(mi, ni)
bfilter = outer_weight * outer_oct - (outer_weight + inner_weight) * inner_oct
return bfilter
def _star_kernel(m, n):
c = m + m // 2 - n - n // 2
outer_star = star(m)
inner_star = np.zeros_like(outer_star)
inner_star[c:-c, c:-c] = star(n)
outer_weight = 1.0 / (np.sum(outer_star - inner_star))
inner_weight = 1.0 / np.sum(inner_star)
bfilter = outer_weight * outer_star - (outer_weight + inner_weight) * inner_star
return bfilter
def _suppress_lines(feature_mask, image, sigma, line_threshold):
Arr, Arc, Acc = structure_tensor(image, sigma, order='rc')
feature_mask[(Arr + Acc) ** 2 > line_threshold * (Arr * Acc - Arc**2)] = False
class CENSURE(FeatureDetector):
"""CENSURE keypoint detector.
min_scale : int, optional
Minimum scale to extract keypoints from.
max_scale : int, optional
Maximum scale to extract keypoints from. The keypoints will be
extracted from all the scales except the first and the last i.e.
from the scales in the range [min_scale + 1, max_scale - 1]. The filter
sizes for different scales is such that the two adjacent scales
comprise of an octave.
mode : {'DoB', 'Octagon', 'STAR'}, optional
Type of bi-level filter used to get the scales of the input image.
Possible values are 'DoB', 'Octagon' and 'STAR'. The three modes
represent the shape of the bi-level filters i.e. box(square), octagon
and star respectively. For instance, a bi-level octagon filter consists
of a smaller inner octagon and a larger outer octagon with the filter
weights being uniformly negative in both the inner octagon while
uniformly positive in the difference region. Use STAR and Octagon for
better features and DoB for better performance.
non_max_threshold : float, optional
Threshold value used to suppress maximas and minimas with a weak
magnitude response obtained after Non-Maximal Suppression.
line_threshold : float, optional
Threshold for rejecting interest points which have ratio of principal
curvatures greater than this value.
Attributes
----------
keypoints : (N, 2) array
Keypoint coordinates as ``(row, col)``.
scales : (N,) array
Corresponding scales.
References
----------
.. [1] Motilal Agrawal, Kurt Konolige and Morten Rufus Blas
"CENSURE: Center Surround Extremas for Realtime Feature
Detection and Matching",
https://link.springer.com/chapter/10.1007/978-3-540-88693-8_8
:DOI:`10.1007/978-3-540-88693-8_8`
.. [2] Adam Schmidt, Marek Kraft, Michal Fularz and Zuzanna Domagala
"Comparative Assessment of Point Feature Detectors and
Descriptors in the Context of Robot Navigation"
http://yadda.icm.edu.pl/yadda/element/bwmeta1.element.baztech-268aaf28-0faf-4872-a4df-7e2e61cb364c/c/Schmidt_comparative.pdf
:DOI:`10.1.1.465.1117`
Examples
--------
>>> from skimage.data import astronaut
>>> from skimage.color import rgb2gray
>>> from skimage.feature import CENSURE
>>> img = rgb2gray(astronaut()[100:300, 100:300])
>>> censure = CENSURE()
>>> censure.detect(img)
>>> censure.keypoints
array([[ 4, 148],
[ 12, 73],
[ 21, 176],
[ 91, 22],
[ 93, 56],
[ 94, 22],
[ 95, 54],
[100, 51],
[103, 51],
[106, 67],
[108, 15],
[117, 20],
[122, 60],
[125, 37],
[129, 37],
[133, 76],
[145, 44],
[146, 94],
[150, 114],
[153, 33],
[154, 156],
[155, 151],
[184, 63]])
>>> censure.scales
array([2, 6, 6, 2, 4, 3, 2, 3, 2, 6, 3, 2, 2, 3, 2, 2, 2, 3, 2, 2, 4, 2,
2])
"""
def __init__(
self,
min_scale=1,
max_scale=7,
mode='DoB',
non_max_threshold=0.15,
line_threshold=10,
):
mode = mode.lower()
if mode not in ('dob', 'octagon', 'star'):
raise ValueError("`mode` must be one of 'DoB', 'Octagon', 'STAR'.")
if min_scale < 1 or max_scale < 1 or max_scale - min_scale < 2:
raise ValueError(
'The scales must be >= 1 and the number of ' 'scales should be >= 3.'
)
self.min_scale = min_scale
self.max_scale = max_scale
self.mode = mode
self.non_max_threshold = non_max_threshold
self.line_threshold = line_threshold
self.keypoints = None
self.scales = None
def detect(self, image):
"""Detect CENSURE keypoints along with the corresponding scale.
Parameters
----------
image : 2D ndarray
Input image.
"""
# (1) First we generate the required scales on the input grayscale
# image using a bi-level filter and stack them up in `filter_response`.
# (2) We then perform Non-Maximal suppression in 3 x 3 x 3 window on
# the filter_response to suppress points that are neither minima or
# maxima in 3 x 3 x 3 neighborhood. We obtain a boolean ndarray
# `feature_mask` containing all the minimas and maximas in
# `filter_response` as True.
# (3) Then we suppress all the points in the `feature_mask` for which
# the corresponding point in the image at a particular scale has the
# ratio of principal curvatures greater than `line_threshold`.
# (4) Finally, we remove the border keypoints and return the keypoints
# along with its corresponding scale.
check_nD(image, 2)
num_scales = self.max_scale - self.min_scale
image = np.ascontiguousarray(_prepare_grayscale_input_2D(image))
# Generating all the scales
filter_response = _filter_image(
image, self.min_scale, self.max_scale, self.mode
)
# Suppressing points that are neither minima or maxima in their
# 3 x 3 x 3 neighborhood to zero
minimas = minimum_filter(filter_response, (3, 3, 3)) == filter_response
maximas = maximum_filter(filter_response, (3, 3, 3)) == filter_response
feature_mask = minimas | maximas
feature_mask[filter_response < self.non_max_threshold] = False
for i in range(1, num_scales):
# sigma = (window_size - 1) / 6.0, so the window covers > 99% of
# the kernel's distribution
# window_size = 7 + 2 * (min_scale - 1 + i)
# Hence sigma = 1 + (min_scale - 1 + i)/ 3.0
_suppress_lines(
feature_mask[:, :, i],
image,
(1 + (self.min_scale + i - 1) / 3.0),
self.line_threshold,
)
rows, cols, scales = np.nonzero(feature_mask[..., 1:num_scales])
keypoints = np.column_stack([rows, cols])
scales = scales + self.min_scale + 1
if self.mode == 'dob':
self.keypoints = keypoints
self.scales = scales
return
cumulative_mask = np.zeros(keypoints.shape[0], dtype=bool)
if self.mode == 'octagon':
for i in range(self.min_scale + 1, self.max_scale):
c = (OCTAGON_OUTER_SHAPE[i - 1][0] - 1) // 2 + OCTAGON_OUTER_SHAPE[
i - 1
][1]
cumulative_mask |= _mask_border_keypoints(image.shape, keypoints, c) & (
scales == i
)
elif self.mode == 'star':
for i in range(self.min_scale + 1, self.max_scale):
c = (
STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]]
+ STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]] // 2
)
cumulative_mask |= _mask_border_keypoints(image.shape, keypoints, c) & (
scales == i
)
self.keypoints = keypoints[cumulative_mask]
self.scales = scales[cumulative_mask]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,339 @@
from itertools import chain
from operator import add
import numpy as np
from ._haar import haar_like_feature_coord_wrapper
from ._haar import haar_like_feature_wrapper
from ..color import gray2rgb
from ..draw import rectangle
from ..util import img_as_float
FEATURE_TYPE = ('type-2-x', 'type-2-y', 'type-3-x', 'type-3-y', 'type-4')
def _validate_feature_type(feature_type):
"""Transform feature type to an iterable and check that it exists."""
if feature_type is None:
feature_type_ = FEATURE_TYPE
else:
if isinstance(feature_type, str):
feature_type_ = [feature_type]
else:
feature_type_ = feature_type
for feat_t in feature_type_:
if feat_t not in FEATURE_TYPE:
raise ValueError(
f'The given feature type is unknown. Got {feat_t} instead of one '
f'of {FEATURE_TYPE}.'
)
return feature_type_
def haar_like_feature_coord(width, height, feature_type=None):
"""Compute the coordinates of Haar-like features.
Parameters
----------
width : int
Width of the detection window.
height : int
Height of the detection window.
feature_type : str or list of str or None, optional
The type of feature to consider:
- 'type-2-x': 2 rectangles varying along the x axis;
- 'type-2-y': 2 rectangles varying along the y axis;
- 'type-3-x': 3 rectangles varying along the x axis;
- 'type-3-y': 3 rectangles varying along the y axis;
- 'type-4': 4 rectangles varying along x and y axis.
By default all features are extracted.
Returns
-------
feature_coord : (n_features, n_rectangles, 2, 2), ndarray of list of \
tuple coord
Coordinates of the rectangles for each feature.
feature_type : (n_features,), ndarray of str
The corresponding type for each feature.
Examples
--------
>>> import numpy as np
>>> from skimage.transform import integral_image
>>> from skimage.feature import haar_like_feature_coord
>>> feat_coord, feat_type = haar_like_feature_coord(2, 2, 'type-4')
>>> feat_coord # doctest: +SKIP
array([ list([[(0, 0), (0, 0)], [(0, 1), (0, 1)],
[(1, 1), (1, 1)], [(1, 0), (1, 0)]])], dtype=object)
>>> feat_type
array(['type-4'], dtype=object)
"""
feature_type_ = _validate_feature_type(feature_type)
feat_coord, feat_type = zip(
*[
haar_like_feature_coord_wrapper(width, height, feat_t)
for feat_t in feature_type_
]
)
return np.concatenate(feat_coord), np.hstack(feat_type)
def haar_like_feature(
int_image, r, c, width, height, feature_type=None, feature_coord=None
):
"""Compute the Haar-like features for a region of interest (ROI) of an
integral image.
Haar-like features have been successfully used for image classification and
object detection [1]_. It has been used for real-time face detection
algorithm proposed in [2]_.
Parameters
----------
int_image : (M, N) ndarray
Integral image for which the features need to be computed.
r : int
Row-coordinate of top left corner of the detection window.
c : int
Column-coordinate of top left corner of the detection window.
width : int
Width of the detection window.
height : int
Height of the detection window.
feature_type : str or list of str or None, optional
The type of feature to consider:
- 'type-2-x': 2 rectangles varying along the x axis;
- 'type-2-y': 2 rectangles varying along the y axis;
- 'type-3-x': 3 rectangles varying along the x axis;
- 'type-3-y': 3 rectangles varying along the y axis;
- 'type-4': 4 rectangles varying along x and y axis.
By default all features are extracted.
If using with `feature_coord`, it should correspond to the feature
type of each associated coordinate feature.
feature_coord : ndarray of list of tuples or None, optional
The array of coordinates to be extracted. This is useful when you want
to recompute only a subset of features. In this case `feature_type`
needs to be an array containing the type of each feature, as returned
by :func:`haar_like_feature_coord`. By default, all coordinates are
computed.
Returns
-------
haar_features : (n_features,) ndarray of int or float
Resulting Haar-like features. Each value is equal to the subtraction of
sums of the positive and negative rectangles. The data type depends of
the data type of `int_image`: `int` when the data type of `int_image`
is `uint` or `int` and `float` when the data type of `int_image` is
`float`.
Notes
-----
When extracting those features in parallel, be aware that the choice of the
backend (i.e. multiprocessing vs threading) will have an impact on the
performance. The rule of thumb is as follows: use multiprocessing when
extracting features for all possible ROI in an image; use threading when
extracting the feature at specific location for a limited number of ROIs.
Refer to the example
:ref:`sphx_glr_auto_examples_applications_plot_haar_extraction_selection_classification.py`
for more insights.
Examples
--------
>>> import numpy as np
>>> from skimage.transform import integral_image
>>> from skimage.feature import haar_like_feature
>>> img = np.ones((5, 5), dtype=np.uint8)
>>> img_ii = integral_image(img)
>>> feature = haar_like_feature(img_ii, 0, 0, 5, 5, 'type-3-x')
>>> feature
array([-1, -2, -3, -4, -5, -1, -2, -3, -4, -5, -1, -2, -3, -4, -5, -1, -2,
-3, -4, -1, -2, -3, -4, -1, -2, -3, -4, -1, -2, -3, -1, -2, -3, -1,
-2, -3, -1, -2, -1, -2, -1, -2, -1, -1, -1])
You can compute the feature for some pre-computed coordinates.
>>> from skimage.feature import haar_like_feature_coord
>>> feature_coord, feature_type = zip(
... *[haar_like_feature_coord(5, 5, feat_t)
... for feat_t in ('type-2-x', 'type-3-x')])
>>> # only select one feature over two
>>> feature_coord = np.concatenate([x[::2] for x in feature_coord])
>>> feature_type = np.concatenate([x[::2] for x in feature_type])
>>> feature = haar_like_feature(img_ii, 0, 0, 5, 5,
... feature_type=feature_type,
... feature_coord=feature_coord)
>>> feature
array([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -3, -5, -2, -4, -1,
-3, -5, -2, -4, -2, -4, -2, -4, -2, -1, -3, -2, -1, -1, -1, -1, -1])
References
----------
.. [1] https://en.wikipedia.org/wiki/Haar-like_feature
.. [2] Oren, M., Papageorgiou, C., Sinha, P., Osuna, E., & Poggio, T.
(1997, June). Pedestrian detection using wavelet templates.
In Computer Vision and Pattern Recognition, 1997. Proceedings.,
1997 IEEE Computer Society Conference on (pp. 193-199). IEEE.
http://tinyurl.com/y6ulxfta
:DOI:`10.1109/CVPR.1997.609319`
.. [3] Viola, Paul, and Michael J. Jones. "Robust real-time face
detection." International journal of computer vision 57.2
(2004): 137-154.
https://www.merl.com/publications/docs/TR2004-043.pdf
:DOI:`10.1109/CVPR.2001.990517`
"""
if feature_coord is None:
feature_type_ = _validate_feature_type(feature_type)
return np.hstack(
list(
chain.from_iterable(
haar_like_feature_wrapper(
int_image, r, c, width, height, feat_t, feature_coord
)
for feat_t in feature_type_
)
)
)
else:
if feature_coord.shape[0] != feature_type.shape[0]:
raise ValueError(
"Inconsistent size between feature coordinates" "and feature types."
)
mask_feature = [feature_type == feat_t for feat_t in FEATURE_TYPE]
haar_feature_idx, haar_feature = zip(
*[
(
np.flatnonzero(mask),
haar_like_feature_wrapper(
int_image, r, c, width, height, feat_t, feature_coord[mask]
),
)
for mask, feat_t in zip(mask_feature, FEATURE_TYPE)
if np.count_nonzero(mask)
]
)
haar_feature_idx = np.concatenate(haar_feature_idx)
haar_feature = np.concatenate(haar_feature)
haar_feature[haar_feature_idx] = haar_feature.copy()
return haar_feature
def draw_haar_like_feature(
image,
r,
c,
width,
height,
feature_coord,
color_positive_block=(1.0, 0.0, 0.0),
color_negative_block=(0.0, 1.0, 0.0),
alpha=0.5,
max_n_features=None,
rng=None,
):
"""Visualization of Haar-like features.
Parameters
----------
image : (M, N) ndarray
The region of an integral image for which the features need to be
computed.
r : int
Row-coordinate of top left corner of the detection window.
c : int
Column-coordinate of top left corner of the detection window.
width : int
Width of the detection window.
height : int
Height of the detection window.
feature_coord : ndarray of list of tuples or None, optional
The array of coordinates to be extracted. This is useful when you want
to recompute only a subset of features. In this case `feature_type`
needs to be an array containing the type of each feature, as returned
by :func:`haar_like_feature_coord`. By default, all coordinates are
computed.
color_positive_block : tuple of 3 floats
Floats specifying the color for the positive block. Corresponding
values define (R, G, B) values. Default value is red (1, 0, 0).
color_negative_block : tuple of 3 floats
Floats specifying the color for the negative block Corresponding values
define (R, G, B) values. Default value is blue (0, 1, 0).
alpha : float
Value in the range [0, 1] that specifies opacity of visualization. 1 -
fully transparent, 0 - opaque.
max_n_features : int, default=None
The maximum number of features to be returned.
By default, all features are returned.
rng : {`numpy.random.Generator`, int}, optional
Pseudo-random number generator.
By default, a PCG64 generator is used (see :func:`numpy.random.default_rng`).
If `rng` is an int, it is used to seed the generator.
The rng is used when generating a set of features smaller than
the total number of available features.
Returns
-------
features : (M, N), ndarray
An image in which the different features will be added.
Examples
--------
>>> import numpy as np
>>> from skimage.feature import haar_like_feature_coord
>>> from skimage.feature import draw_haar_like_feature
>>> feature_coord, _ = haar_like_feature_coord(2, 2, 'type-4')
>>> image = draw_haar_like_feature(np.zeros((2, 2)),
... 0, 0, 2, 2,
... feature_coord,
... max_n_features=1)
>>> image
array([[[0. , 0.5, 0. ],
[0.5, 0. , 0. ]],
<BLANKLINE>
[[0.5, 0. , 0. ],
[0. , 0.5, 0. ]]])
"""
rng = np.random.default_rng(rng)
color_positive_block = np.asarray(color_positive_block, dtype=np.float64)
color_negative_block = np.asarray(color_negative_block, dtype=np.float64)
if max_n_features is None:
feature_coord_ = feature_coord
else:
feature_coord_ = rng.choice(feature_coord, size=max_n_features, replace=False)
output = np.copy(image)
if len(image.shape) < 3:
output = gray2rgb(image)
output = img_as_float(output)
for coord in feature_coord_:
for idx_rect, rect in enumerate(coord):
coord_start, coord_end = rect
coord_start = tuple(map(add, coord_start, [r, c]))
coord_end = tuple(map(add, coord_end, [r, c]))
rr, cc = rectangle(coord_start, coord_end)
if ((idx_rect + 1) % 2) == 0:
new_value = (1 - alpha) * output[rr, cc] + alpha * color_positive_block
else:
new_value = (1 - alpha) * output[rr, cc] + alpha * color_negative_block
output[rr, cc] = new_value
return output

View File

@@ -0,0 +1,103 @@
import numpy as np
from scipy.spatial.distance import cdist
def match_descriptors(
descriptors1,
descriptors2,
metric=None,
p=2,
max_distance=np.inf,
cross_check=True,
max_ratio=1.0,
):
"""Brute-force matching of descriptors.
For each descriptor in the first set this matcher finds the closest
descriptor in the second set (and vice-versa in the case of enabled
cross-checking).
Parameters
----------
descriptors1 : (M, P) array
Descriptors of size P about M keypoints in the first image.
descriptors2 : (N, P) array
Descriptors of size P about N keypoints in the second image.
metric : {'euclidean', 'cityblock', 'minkowski', 'hamming', ...} , optional
The metric to compute the distance between two descriptors. See
`scipy.spatial.distance.cdist` for all possible types. The hamming
distance should be used for binary descriptors. By default the L2-norm
is used for all descriptors of dtype float or double and the Hamming
distance is used for binary descriptors automatically.
p : int, optional
The p-norm to apply for ``metric='minkowski'``.
max_distance : float, optional
Maximum allowed distance between descriptors of two keypoints
in separate images to be regarded as a match.
cross_check : bool, optional
If True, the matched keypoints are returned after cross checking i.e. a
matched pair (keypoint1, keypoint2) is returned if keypoint2 is the
best match for keypoint1 in second image and keypoint1 is the best
match for keypoint2 in first image.
max_ratio : float, optional
Maximum ratio of distances between first and second closest descriptor
in the second set of descriptors. This threshold is useful to filter
ambiguous matches between the two descriptor sets. The choice of this
value depends on the statistics of the chosen descriptor, e.g.,
for SIFT descriptors a value of 0.8 is usually chosen, see
D.G. Lowe, "Distinctive Image Features from Scale-Invariant Keypoints",
International Journal of Computer Vision, 2004.
Returns
-------
matches : (Q, 2) array
Indices of corresponding matches in first and second set of
descriptors, where ``matches[:, 0]`` denote the indices in the first
and ``matches[:, 1]`` the indices in the second set of descriptors.
"""
if descriptors1.shape[1] != descriptors2.shape[1]:
raise ValueError("Descriptor length must equal.")
if metric is None:
if np.issubdtype(descriptors1.dtype, bool):
metric = 'hamming'
else:
metric = 'euclidean'
kwargs = {}
# Scipy raises an error if p is passed as an extra argument when it isn't
# necessary for the chosen metric.
if metric == 'minkowski':
kwargs['p'] = p
distances = cdist(descriptors1, descriptors2, metric=metric, **kwargs)
indices1 = np.arange(descriptors1.shape[0])
indices2 = np.argmin(distances, axis=1)
if cross_check:
matches1 = np.argmin(distances, axis=0)
mask = indices1 == matches1[indices2]
indices1 = indices1[mask]
indices2 = indices2[mask]
if max_distance < np.inf:
mask = distances[indices1, indices2] < max_distance
indices1 = indices1[mask]
indices2 = indices2[mask]
if max_ratio < 1.0:
best_distances = distances[indices1, indices2]
distances[indices1, indices2] = np.inf
second_best_indices2 = np.argmin(distances[indices1], axis=1)
second_best_distances = distances[indices1, second_best_indices2]
second_best_distances[second_best_distances == 0] = np.finfo(np.float64).eps
ratio = best_distances / second_best_distances
mask = ratio < max_ratio
indices1 = indices1[mask]
indices2 = indices2[mask]
matches = np.column_stack((indices1, indices2))
return matches

View File

@@ -0,0 +1,366 @@
import numpy as np
from ..feature.util import (
FeatureDetector,
DescriptorExtractor,
_mask_border_keypoints,
_prepare_grayscale_input_2D,
)
from .corner import corner_fast, corner_orientations, corner_peaks, corner_harris
from ..transform import pyramid_gaussian
from .._shared.utils import check_nD
from .._shared.compat import NP_COPY_IF_NEEDED
from .orb_cy import _orb_loop
OFAST_MASK = np.zeros((31, 31))
OFAST_UMAX = [15, 15, 15, 15, 14, 14, 14, 13, 13, 12, 11, 10, 9, 8, 6, 3]
for i in range(-15, 16):
for j in range(-OFAST_UMAX[abs(i)], OFAST_UMAX[abs(i)] + 1):
OFAST_MASK[15 + j, 15 + i] = 1
class ORB(FeatureDetector, DescriptorExtractor):
"""Oriented FAST and rotated BRIEF feature detector and binary descriptor
extractor.
Parameters
----------
n_keypoints : int, optional
Number of keypoints to be returned. The function will return the best
`n_keypoints` according to the Harris corner response if more than
`n_keypoints` are detected. If not, then all the detected keypoints
are returned.
fast_n : int, optional
The `n` parameter in `skimage.feature.corner_fast`. Minimum number of
consecutive pixels out of 16 pixels on the circle that should all be
either brighter or darker w.r.t test-pixel. A point c on the circle is
darker w.r.t test pixel p if ``Ic < Ip - threshold`` and brighter if
``Ic > Ip + threshold``. Also stands for the n in ``FAST-n`` corner
detector.
fast_threshold : float, optional
The ``threshold`` parameter in ``feature.corner_fast``. Threshold used
to decide whether the pixels on the circle are brighter, darker or
similar w.r.t. the test pixel. Decrease the threshold when more
corners are desired and vice-versa.
harris_k : float, optional
The `k` parameter in `skimage.feature.corner_harris`. Sensitivity
factor to separate corners from edges, typically in range ``[0, 0.2]``.
Small values of `k` result in detection of sharp corners.
downscale : float, optional
Downscale factor for the image pyramid. Default value 1.2 is chosen so
that there are more dense scales which enable robust scale invariance
for a subsequent feature description.
n_scales : int, optional
Maximum number of scales from the bottom of the image pyramid to
extract the features from.
Attributes
----------
keypoints : (N, 2) array
Keypoint coordinates as ``(row, col)``.
scales : (N,) array
Corresponding scales.
orientations : (N,) array
Corresponding orientations in radians.
responses : (N,) array
Corresponding Harris corner responses.
descriptors : (Q, `descriptor_size`) array of dtype bool
2D array of binary descriptors of size `descriptor_size` for Q
keypoints after filtering out border keypoints with value at an
index ``(i, j)`` either being ``True`` or ``False`` representing
the outcome of the intensity comparison for i-th keypoint on j-th
decision pixel-pair. It is ``Q == np.sum(mask)``.
References
----------
.. [1] Ethan Rublee, Vincent Rabaud, Kurt Konolige and Gary Bradski
"ORB: An efficient alternative to SIFT and SURF"
http://www.vision.cs.chubu.ac.jp/CV-R/pdf/Rublee_iccv2011.pdf
Examples
--------
>>> from skimage.feature import ORB, match_descriptors
>>> img1 = np.zeros((100, 100))
>>> img2 = np.zeros_like(img1)
>>> rng = np.random.default_rng(19481137) # do not copy this value
>>> square = rng.random((20, 20))
>>> img1[40:60, 40:60] = square
>>> img2[53:73, 53:73] = square
>>> detector_extractor1 = ORB(n_keypoints=5)
>>> detector_extractor2 = ORB(n_keypoints=5)
>>> detector_extractor1.detect_and_extract(img1)
>>> detector_extractor2.detect_and_extract(img2)
>>> matches = match_descriptors(detector_extractor1.descriptors,
... detector_extractor2.descriptors)
>>> matches
array([[0, 0],
[1, 1],
[2, 2],
[3, 4],
[4, 3]])
>>> detector_extractor1.keypoints[matches[:, 0]]
array([[59. , 59. ],
[40. , 40. ],
[57. , 40. ],
[46. , 58. ],
[58.8, 58.8]])
>>> detector_extractor2.keypoints[matches[:, 1]]
array([[72., 72.],
[53., 53.],
[70., 53.],
[59., 71.],
[72., 72.]])
"""
def __init__(
self,
downscale=1.2,
n_scales=8,
n_keypoints=500,
fast_n=9,
fast_threshold=0.08,
harris_k=0.04,
):
self.downscale = downscale
self.n_scales = n_scales
self.n_keypoints = n_keypoints
self.fast_n = fast_n
self.fast_threshold = fast_threshold
self.harris_k = harris_k
self.keypoints = None
self.scales = None
self.responses = None
self.orientations = None
self.descriptors = None
def _build_pyramid(self, image):
image = _prepare_grayscale_input_2D(image)
return list(
pyramid_gaussian(
image, self.n_scales - 1, self.downscale, channel_axis=None
)
)
def _detect_octave(self, octave_image):
dtype = octave_image.dtype
# Extract keypoints for current octave
fast_response = corner_fast(octave_image, self.fast_n, self.fast_threshold)
keypoints = corner_peaks(fast_response, min_distance=1)
if len(keypoints) == 0:
return (
np.zeros((0, 2), dtype=dtype),
np.zeros((0,), dtype=dtype),
np.zeros((0,), dtype=dtype),
)
mask = _mask_border_keypoints(octave_image.shape, keypoints, distance=16)
keypoints = keypoints[mask]
orientations = corner_orientations(octave_image, keypoints, OFAST_MASK)
harris_response = corner_harris(octave_image, method='k', k=self.harris_k)
responses = harris_response[keypoints[:, 0], keypoints[:, 1]]
return keypoints, orientations, responses
def detect(self, image):
"""Detect oriented FAST keypoints along with the corresponding scale.
Parameters
----------
image : 2D array
Input image.
"""
check_nD(image, 2)
pyramid = self._build_pyramid(image)
keypoints_list = []
orientations_list = []
scales_list = []
responses_list = []
for octave in range(len(pyramid)):
octave_image = np.ascontiguousarray(pyramid[octave])
if np.squeeze(octave_image).ndim < 2:
# No further keypoints can be detected if the image is not really 2d
break
keypoints, orientations, responses = self._detect_octave(octave_image)
keypoints_list.append(keypoints * self.downscale**octave)
orientations_list.append(orientations)
scales_list.append(
np.full(
keypoints.shape[0],
self.downscale**octave,
dtype=octave_image.dtype,
)
)
responses_list.append(responses)
keypoints = np.vstack(keypoints_list)
orientations = np.hstack(orientations_list)
scales = np.hstack(scales_list)
responses = np.hstack(responses_list)
if keypoints.shape[0] < self.n_keypoints:
self.keypoints = keypoints
self.scales = scales
self.orientations = orientations
self.responses = responses
else:
# Choose best n_keypoints according to Harris corner response
best_indices = responses.argsort()[::-1][: self.n_keypoints]
self.keypoints = keypoints[best_indices]
self.scales = scales[best_indices]
self.orientations = orientations[best_indices]
self.responses = responses[best_indices]
def _extract_octave(self, octave_image, keypoints, orientations):
mask = _mask_border_keypoints(octave_image.shape, keypoints, distance=20)
keypoints = np.array(
keypoints[mask], dtype=np.intp, order='C', copy=NP_COPY_IF_NEEDED
)
orientations = np.array(orientations[mask], order='C', copy=False)
descriptors = _orb_loop(octave_image, keypoints, orientations)
return descriptors, mask
def extract(self, image, keypoints, scales, orientations):
"""Extract rBRIEF binary descriptors for given keypoints in image.
Note that the keypoints must be extracted using the same `downscale`
and `n_scales` parameters. Additionally, if you want to extract both
keypoints and descriptors you should use the faster
`detect_and_extract`.
Parameters
----------
image : 2D array
Input image.
keypoints : (N, 2) array
Keypoint coordinates as ``(row, col)``.
scales : (N,) array
Corresponding scales.
orientations : (N,) array
Corresponding orientations in radians.
"""
check_nD(image, 2)
pyramid = self._build_pyramid(image)
descriptors_list = []
mask_list = []
# Determine octaves from scales
octaves = (np.log(scales) / np.log(self.downscale)).astype(np.intp)
for octave in range(len(pyramid)):
# Mask for all keypoints in current octave
octave_mask = octaves == octave
if np.sum(octave_mask) > 0:
octave_image = np.ascontiguousarray(pyramid[octave])
octave_keypoints = keypoints[octave_mask]
octave_keypoints /= self.downscale**octave
octave_orientations = orientations[octave_mask]
descriptors, mask = self._extract_octave(
octave_image, octave_keypoints, octave_orientations
)
descriptors_list.append(descriptors)
mask_list.append(mask)
self.descriptors = np.vstack(descriptors_list).view(bool)
self.mask_ = np.hstack(mask_list)
def detect_and_extract(self, image):
"""Detect oriented FAST keypoints and extract rBRIEF descriptors.
Note that this is faster than first calling `detect` and then
`extract`.
Parameters
----------
image : 2D array
Input image.
"""
check_nD(image, 2)
pyramid = self._build_pyramid(image)
keypoints_list = []
responses_list = []
scales_list = []
orientations_list = []
descriptors_list = []
for octave in range(len(pyramid)):
octave_image = np.ascontiguousarray(pyramid[octave])
if np.squeeze(octave_image).ndim < 2:
# No further keypoints can be detected if the image is not really 2d
break
keypoints, orientations, responses = self._detect_octave(octave_image)
if len(keypoints) == 0:
keypoints_list.append(keypoints)
responses_list.append(responses)
descriptors_list.append(np.zeros((0, 256), dtype=bool))
continue
descriptors, mask = self._extract_octave(
octave_image, keypoints, orientations
)
scaled_keypoints = keypoints[mask] * self.downscale**octave
keypoints_list.append(scaled_keypoints)
responses_list.append(responses[mask])
orientations_list.append(orientations[mask])
scales_list.append(
self.downscale**octave
* np.ones(scaled_keypoints.shape[0], dtype=np.intp)
)
descriptors_list.append(descriptors)
if len(scales_list) == 0:
raise RuntimeError(
"ORB found no features. Try passing in an image containing "
"greater intensity contrasts between adjacent pixels."
)
keypoints = np.vstack(keypoints_list)
responses = np.hstack(responses_list)
scales = np.hstack(scales_list)
orientations = np.hstack(orientations_list)
descriptors = np.vstack(descriptors_list).view(bool)
if keypoints.shape[0] < self.n_keypoints:
self.keypoints = keypoints
self.scales = scales
self.orientations = orientations
self.responses = responses
self.descriptors = descriptors
else:
# Choose best n_keypoints according to Harris corner response
best_indices = responses.argsort()[::-1][: self.n_keypoints]
self.keypoints = keypoints[best_indices]
self.scales = scales[best_indices]
self.orientations = orientations[best_indices]
self.responses = responses[best_indices]
self.descriptors = descriptors[best_indices]

View File

@@ -0,0 +1,256 @@
8 -3 9 5
4 2 7 -12
-11 9 -8 2
7 -12 12 -13
2 -13 2 12
1 -7 1 6
-2 -10 -2 -4
-13 -13 -11 -8
-13 -3 -12 -9
10 4 11 9
-13 -8 -8 -9
-11 7 -9 12
7 7 12 6
-4 -5 -3 0
-13 2 -12 -3
-9 0 -7 5
12 -6 12 -1
-3 6 -2 12
-6 -13 -4 -8
11 -13 12 -8
4 7 5 1
5 -3 10 -3
3 -7 6 12
-8 -7 -6 -2
-2 11 -1 -10
-13 12 -8 10
-7 3 -5 -3
-4 2 -3 7
-10 -12 -6 11
5 -12 6 -7
5 -6 7 -1
1 0 4 -5
9 11 11 -13
4 7 4 12
2 -1 4 4
-4 -12 -2 7
-8 -5 -7 -10
4 11 9 12
0 -8 1 -13
-13 -2 -8 2
-3 -2 -2 3
-6 9 -4 -9
8 12 10 7
0 9 1 3
7 -5 11 -10
-13 -6 -11 0
10 7 12 1
-6 -3 -6 12
10 -9 12 -4
-13 8 -8 -12
-13 0 -8 -4
3 3 7 8
5 7 10 -7
-1 7 1 -12
3 -10 5 6
2 -4 3 -10
-13 0 -13 5
-13 -7 -12 12
-13 3 -11 8
-7 12 -4 7
6 -10 12 8
-9 -1 -7 -6
-2 -5 0 12
-12 5 -7 5
3 -10 8 -13
-7 -7 -4 5
-3 -2 -1 -7
2 9 5 -11
-11 -13 -5 -13
-1 6 0 -1
5 -3 5 2
-4 -13 -4 12
-9 -6 -9 6
-12 -10 -8 -4
10 2 12 -3
7 12 12 12
-7 -13 -6 5
-4 9 -3 4
7 -1 12 2
-7 6 -5 1
-13 11 -12 5
-3 7 -2 -6
7 -8 12 -7
-13 -7 -11 -12
1 -3 12 12
2 -6 3 0
-4 3 -2 -13
-1 -13 1 9
7 1 8 -6
1 -1 3 12
9 1 12 6
-1 -9 -1 3
-13 -13 -10 5
7 7 10 12
12 -5 12 9
6 3 7 11
5 -13 6 10
2 -12 2 3
3 8 4 -6
2 6 12 -13
9 -12 10 3
-8 4 -7 9
-11 12 -4 -6
1 12 2 -8
6 -9 7 -4
2 3 3 -2
6 3 11 0
3 -3 8 -8
7 8 9 3
-11 -5 -6 -4
-10 11 -5 10
-5 -8 -3 12
-10 5 -9 0
8 -1 12 -6
4 -6 6 -11
-10 12 -8 7
4 -2 6 7
-2 0 -2 12
-5 -8 -5 2
7 -6 10 12
-9 -13 -8 -8
-5 -13 -5 -2
8 -8 9 -13
-9 -11 -9 0
1 -8 1 -2
7 -4 9 1
-2 1 -1 -4
11 -6 12 -11
-12 -9 -6 4
3 7 7 12
5 5 10 8
0 -4 2 8
-9 12 -5 -13
0 7 2 12
-1 2 1 7
5 11 7 -9
3 5 6 -8
-13 -4 -8 9
-5 9 -3 -3
-4 -7 -3 -12
6 5 8 0
-7 6 -6 12
-13 6 -5 -2
1 -10 3 10
4 1 8 -4
-2 -2 2 -13
2 -12 12 12
-2 -13 0 -6
4 1 9 3
-6 -10 -3 -5
-3 -13 -1 1
7 5 12 -11
4 -2 5 -7
-13 9 -9 -5
7 1 8 6
7 -8 7 6
-7 -4 -7 1
-8 11 -7 -8
-13 6 -12 -8
2 4 3 9
10 -5 12 3
-6 -5 -6 7
8 -3 9 -8
2 -12 2 8
-11 -2 -10 3
-12 -13 -7 -9
-11 0 -10 -5
5 -3 11 8
-2 -13 -1 12
-1 -8 0 9
-13 -11 -12 -5
-10 -2 -10 11
-3 9 -2 -13
2 -3 3 2
-9 -13 -4 0
-4 6 -3 -10
-4 12 -2 -7
-6 -11 -4 9
6 -3 6 11
-13 11 -5 5
11 11 12 6
7 -5 12 -2
-1 12 0 7
-4 -8 -3 -2
-7 1 -6 7
-13 -12 -8 -13
-7 -2 -6 -8
-8 5 -6 -9
-5 -1 -4 5
-13 7 -8 10
1 5 5 -13
1 0 10 -13
9 12 10 -1
5 -8 10 -9
-1 11 1 -13
-9 -3 -6 2
-1 -10 1 12
-13 1 -8 -10
8 -11 10 -6
2 -13 3 -6
7 -13 12 -9
-10 -10 -5 -7
-10 -8 -8 -13
4 -6 8 5
3 12 8 -13
-4 2 -3 -3
5 -13 10 -12
4 -13 5 -1
-9 9 -4 3
0 3 3 -9
-12 1 -6 1
3 2 4 -8
-10 -10 -10 9
8 -13 12 12
-8 -12 -6 -5
2 2 3 7
10 6 11 -8
6 8 8 -12
-7 10 -6 5
-3 -9 -3 9
-1 -13 -1 5
-3 -7 -3 4
-8 -2 -8 3
4 2 12 12
2 -5 3 11
6 -9 11 -13
3 -1 7 12
11 -1 12 4
-3 0 -3 6
4 -11 4 12
2 -4 2 1
-10 -6 -8 1
-13 7 -11 1
-13 12 -11 -13
6 0 11 -13
0 -1 1 4
-13 3 -9 -2
-9 8 -6 -3
-13 -6 -8 -2
5 -9 8 10
2 7 3 -9
-1 -6 -1 -1
9 5 11 -2
11 -3 12 -8
3 0 3 5
-1 4 0 10
3 -6 4 5
-13 0 -10 5
5 8 12 11
8 9 9 -6
7 -4 8 -12
-10 4 -10 9
7 3 12 4
9 -7 10 -2
7 0 12 -2
-1 -6 0 -11

View File

@@ -0,0 +1,417 @@
from warnings import warn
import numpy as np
import scipy.ndimage as ndi
from .. import measure
from .._shared.coord import ensure_spacing
def _get_high_intensity_peaks(image, mask, num_peaks, min_distance, p_norm):
"""
Return the highest intensity peak coordinates.
"""
# get coordinates of peaks
coord = np.nonzero(mask)
intensities = image[coord]
# Highest peak first
idx_maxsort = np.argsort(-intensities, kind="stable")
coord = np.transpose(coord)[idx_maxsort]
if np.isfinite(num_peaks):
max_out = int(num_peaks)
else:
max_out = None
coord = ensure_spacing(coord, spacing=min_distance, p_norm=p_norm, max_out=max_out)
if len(coord) > num_peaks:
coord = coord[:num_peaks]
return coord
def _get_peak_mask(image, footprint, threshold, mask=None):
"""
Return the mask containing all peak candidates above thresholds.
"""
if footprint.size == 1 or image.size == 1:
return image > threshold
image_max = ndi.maximum_filter(image, footprint=footprint, mode='nearest')
out = image == image_max
# no peak for a trivial image
image_is_trivial = np.all(out) if mask is None else np.all(out[mask])
if image_is_trivial:
out[:] = False
if mask is not None:
# isolated pixels in masked area are returned as peaks
isolated_px = np.logical_xor(mask, ndi.binary_opening(mask))
out[isolated_px] = True
out &= image > threshold
return out
def _exclude_border(label, border_width):
"""Set label border values to 0."""
# zero out label borders
for i, width in enumerate(border_width):
if width == 0:
continue
label[(slice(None),) * i + (slice(None, width),)] = 0
label[(slice(None),) * i + (slice(-width, None),)] = 0
return label
def _get_threshold(image, threshold_abs, threshold_rel):
"""Return the threshold value according to an absolute and a relative
value.
"""
threshold = threshold_abs if threshold_abs is not None else image.min()
if threshold_rel is not None:
threshold = max(threshold, threshold_rel * image.max())
return threshold
def _get_excluded_border_width(image, min_distance, exclude_border):
"""Return border_width values relative to a min_distance if requested."""
if isinstance(exclude_border, bool):
border_width = (min_distance if exclude_border else 0,) * image.ndim
elif isinstance(exclude_border, int):
if exclude_border < 0:
raise ValueError("`exclude_border` cannot be a negative value")
border_width = (exclude_border,) * image.ndim
elif isinstance(exclude_border, tuple):
if len(exclude_border) != image.ndim:
raise ValueError(
"`exclude_border` should have the same length as the "
"dimensionality of the image."
)
for exclude in exclude_border:
if not isinstance(exclude, int):
raise ValueError(
"`exclude_border`, when expressed as a tuple, must only "
"contain ints."
)
if exclude < 0:
raise ValueError("`exclude_border` can not be a negative value")
border_width = exclude_border
else:
raise TypeError(
"`exclude_border` must be bool, int, or tuple with the same "
"length as the dimensionality of the image."
)
return border_width
def peak_local_max(
image,
min_distance=1,
threshold_abs=None,
threshold_rel=None,
exclude_border=True,
num_peaks=np.inf,
footprint=None,
labels=None,
num_peaks_per_label=np.inf,
p_norm=np.inf,
):
"""Find peaks in an image as coordinate list.
Peaks are the local maxima in a region of `2 * min_distance + 1`
(i.e. peaks are separated by at least `min_distance`).
If both `threshold_abs` and `threshold_rel` are provided, the maximum
of the two is chosen as the minimum intensity threshold of peaks.
.. versionchanged:: 0.18
Prior to version 0.18, peaks of the same height within a radius of
`min_distance` were all returned, but this could cause unexpected
behaviour. From 0.18 onwards, an arbitrary peak within the region is
returned. See issue gh-2592.
Parameters
----------
image : ndarray
Input image.
min_distance : int, optional
The minimal allowed distance separating peaks. To find the
maximum number of peaks, use `min_distance=1`.
threshold_abs : float or None, optional
Minimum intensity of peaks. By default, the absolute threshold is
the minimum intensity of the image.
threshold_rel : float or None, optional
Minimum intensity of peaks, calculated as
``max(image) * threshold_rel``.
exclude_border : int, tuple of ints, or bool, optional
If positive integer, `exclude_border` excludes peaks from within
`exclude_border`-pixels of the border of the image.
If tuple of non-negative ints, the length of the tuple must match the
input array's dimensionality. Each element of the tuple will exclude
peaks from within `exclude_border`-pixels of the border of the image
along that dimension.
If True, takes the `min_distance` parameter as value.
If zero or False, peaks are identified regardless of their distance
from the border.
num_peaks : int, optional
Maximum number of peaks. When the number of peaks exceeds `num_peaks`,
return `num_peaks` peaks based on highest peak intensity.
footprint : ndarray of bools, optional
If provided, `footprint == 1` represents the local region within which
to search for peaks at every point in `image`.
labels : ndarray of ints, optional
If provided, each unique region `labels == value` represents a unique
region to search for peaks. Zero is reserved for background.
num_peaks_per_label : int, optional
Maximum number of peaks for each label.
p_norm : float
Which Minkowski p-norm to use. Should be in the range [1, inf].
A finite large p may cause a ValueError if overflow can occur.
``inf`` corresponds to the Chebyshev distance and 2 to the
Euclidean distance.
Returns
-------
output : ndarray
The coordinates of the peaks.
Notes
-----
The peak local maximum function returns the coordinates of local peaks
(maxima) in an image. Internally, a maximum filter is used for finding
local maxima. This operation dilates the original image. After comparison
of the dilated and original images, this function returns the coordinates
of the peaks where the dilated image equals the original image.
See also
--------
skimage.feature.corner_peaks
Examples
--------
>>> img1 = np.zeros((7, 7))
>>> img1[3, 4] = 1
>>> img1[3, 2] = 1.5
>>> img1
array([[0. , 0. , 0. , 0. , 0. , 0. , 0. ],
[0. , 0. , 0. , 0. , 0. , 0. , 0. ],
[0. , 0. , 0. , 0. , 0. , 0. , 0. ],
[0. , 0. , 1.5, 0. , 1. , 0. , 0. ],
[0. , 0. , 0. , 0. , 0. , 0. , 0. ],
[0. , 0. , 0. , 0. , 0. , 0. , 0. ],
[0. , 0. , 0. , 0. , 0. , 0. , 0. ]])
>>> peak_local_max(img1, min_distance=1)
array([[3, 2],
[3, 4]])
>>> peak_local_max(img1, min_distance=2)
array([[3, 2]])
>>> img2 = np.zeros((20, 20, 20))
>>> img2[10, 10, 10] = 1
>>> img2[15, 15, 15] = 1
>>> peak_idx = peak_local_max(img2, exclude_border=0)
>>> peak_idx
array([[10, 10, 10],
[15, 15, 15]])
>>> peak_mask = np.zeros_like(img2, dtype=bool)
>>> peak_mask[tuple(peak_idx.T)] = True
>>> np.argwhere(peak_mask)
array([[10, 10, 10],
[15, 15, 15]])
"""
if (footprint is None or footprint.size == 1) and min_distance < 1:
warn(
"When min_distance < 1, peak_local_max acts as finding "
"image > max(threshold_abs, threshold_rel * max(image)).",
RuntimeWarning,
stacklevel=2,
)
border_width = _get_excluded_border_width(image, min_distance, exclude_border)
threshold = _get_threshold(image, threshold_abs, threshold_rel)
if footprint is None:
size = 2 * min_distance + 1
footprint = np.ones((size,) * image.ndim, dtype=bool)
else:
footprint = np.asarray(footprint)
if labels is None:
# Non maximum filter
mask = _get_peak_mask(image, footprint, threshold)
mask = _exclude_border(mask, border_width)
# Select highest intensities (num_peaks)
coordinates = _get_high_intensity_peaks(
image, mask, num_peaks, min_distance, p_norm
)
else:
_labels = _exclude_border(labels.astype(int, casting="safe"), border_width)
if np.issubdtype(image.dtype, np.floating):
bg_val = np.finfo(image.dtype).min
else:
bg_val = np.iinfo(image.dtype).min
# For each label, extract a smaller image enclosing the object of
# interest, identify num_peaks_per_label peaks
labels_peak_coord = []
for label_idx, roi in enumerate(ndi.find_objects(_labels)):
if roi is None:
continue
# Get roi mask
label_mask = labels[roi] == label_idx + 1
# Extract image roi
img_object = image[roi].copy()
# Ensure masked values don't affect roi's local peaks
img_object[np.logical_not(label_mask)] = bg_val
mask = _get_peak_mask(img_object, footprint, threshold, label_mask)
coordinates = _get_high_intensity_peaks(
img_object, mask, num_peaks_per_label, min_distance, p_norm
)
# transform coordinates in global image indices space
for idx, s in enumerate(roi):
coordinates[:, idx] += s.start
labels_peak_coord.append(coordinates)
if labels_peak_coord:
coordinates = np.vstack(labels_peak_coord)
else:
coordinates = np.empty((0, 2), dtype=int)
if len(coordinates) > num_peaks:
out = np.zeros_like(image, dtype=bool)
out[tuple(coordinates.T)] = True
coordinates = _get_high_intensity_peaks(
image, out, num_peaks, min_distance, p_norm
)
return coordinates
def _prominent_peaks(
image, min_xdistance=1, min_ydistance=1, threshold=None, num_peaks=np.inf
):
"""Return peaks with non-maximum suppression.
Identifies most prominent features separated by certain distances.
Non-maximum suppression with different sizes is applied separately
in the first and second dimension of the image to identify peaks.
Parameters
----------
image : (M, N) ndarray
Input image.
min_xdistance : int
Minimum distance separating features in the x dimension.
min_ydistance : int
Minimum distance separating features in the y dimension.
threshold : float
Minimum intensity of peaks. Default is `0.5 * max(image)`.
num_peaks : int
Maximum number of peaks. When the number of peaks exceeds `num_peaks`,
return `num_peaks` coordinates based on peak intensity.
Returns
-------
intensity, xcoords, ycoords : tuple of array
Peak intensity values, x and y indices.
"""
img = image.copy()
rows, cols = img.shape
if threshold is None:
threshold = 0.5 * np.max(img)
ycoords_size = 2 * min_ydistance + 1
xcoords_size = 2 * min_xdistance + 1
img_max = ndi.maximum_filter1d(
img, size=ycoords_size, axis=0, mode='constant', cval=0
)
img_max = ndi.maximum_filter1d(
img_max, size=xcoords_size, axis=1, mode='constant', cval=0
)
mask = img == img_max
img *= mask
img_t = img > threshold
label_img = measure.label(img_t)
props = measure.regionprops(label_img, img_max)
# Sort the list of peaks by intensity, not left-right, so larger peaks
# in Hough space cannot be arbitrarily suppressed by smaller neighbors
props = sorted(props, key=lambda x: x.intensity_max)[::-1]
coords = np.array([np.round(p.centroid) for p in props], dtype=int)
img_peaks = []
ycoords_peaks = []
xcoords_peaks = []
# relative coordinate grid for local neighborhood suppression
ycoords_ext, xcoords_ext = np.mgrid[
-min_ydistance : min_ydistance + 1, -min_xdistance : min_xdistance + 1
]
for ycoords_idx, xcoords_idx in coords:
accum = img_max[ycoords_idx, xcoords_idx]
if accum > threshold:
# absolute coordinate grid for local neighborhood suppression
ycoords_nh = ycoords_idx + ycoords_ext
xcoords_nh = xcoords_idx + xcoords_ext
# no reflection for distance neighborhood
ycoords_in = np.logical_and(ycoords_nh > 0, ycoords_nh < rows)
ycoords_nh = ycoords_nh[ycoords_in]
xcoords_nh = xcoords_nh[ycoords_in]
# reflect xcoords and assume xcoords are continuous,
# e.g. for angles:
# (..., 88, 89, -90, -89, ..., 89, -90, -89, ...)
xcoords_low = xcoords_nh < 0
ycoords_nh[xcoords_low] = rows - ycoords_nh[xcoords_low]
xcoords_nh[xcoords_low] += cols
xcoords_high = xcoords_nh >= cols
ycoords_nh[xcoords_high] = rows - ycoords_nh[xcoords_high]
xcoords_nh[xcoords_high] -= cols
# suppress neighborhood
img_max[ycoords_nh, xcoords_nh] = 0
# add current feature to peaks
img_peaks.append(accum)
ycoords_peaks.append(ycoords_idx)
xcoords_peaks.append(xcoords_idx)
img_peaks = np.array(img_peaks)
ycoords_peaks = np.array(ycoords_peaks)
xcoords_peaks = np.array(xcoords_peaks)
if num_peaks < len(img_peaks):
idx_maxsort = np.argsort(img_peaks)[::-1][:num_peaks]
img_peaks = img_peaks[idx_maxsort]
ycoords_peaks = ycoords_peaks[idx_maxsort]
xcoords_peaks = xcoords_peaks[idx_maxsort]
return img_peaks, xcoords_peaks, ycoords_peaks

View File

@@ -0,0 +1,771 @@
import math
import numpy as np
import scipy.ndimage as ndi
from .._shared.utils import check_nD, _supported_float_type
from ..feature.util import DescriptorExtractor, FeatureDetector
from .._shared.filters import gaussian
from ..transform import rescale
from ..util import img_as_float
from ._sift import _local_max, _ori_distances, _update_histogram
def _edgeness(hxx, hyy, hxy):
"""Compute edgeness (eq. 18 of Otero et. al. IPOL paper)"""
trace = hxx + hyy
determinant = hxx * hyy - hxy * hxy
return (trace * trace) / determinant
def _sparse_gradient(vol, positions):
"""Gradient of a 3D volume at the provided `positions`.
For SIFT we only need the gradient at specific positions and do not need
the gradient at the edge positions, so can just use this simple
implementation instead of numpy.gradient.
"""
p0 = positions[..., 0]
p1 = positions[..., 1]
p2 = positions[..., 2]
g0 = vol[p0 + 1, p1, p2] - vol[p0 - 1, p1, p2]
g0 *= 0.5
g1 = vol[p0, p1 + 1, p2] - vol[p0, p1 - 1, p2]
g1 *= 0.5
g2 = vol[p0, p1, p2 + 1] - vol[p0, p1, p2 - 1]
g2 *= 0.5
return g0, g1, g2
def _hessian(d, positions):
"""Compute the non-redundant 3D Hessian terms at the requested positions.
Source: "Anatomy of the SIFT Method" p.380 (13)
"""
p0 = positions[..., 0]
p1 = positions[..., 1]
p2 = positions[..., 2]
two_d0 = 2 * d[p0, p1, p2]
# 0 = row, 1 = col, 2 = octave
h00 = d[p0 - 1, p1, p2] + d[p0 + 1, p1, p2] - two_d0
h11 = d[p0, p1 - 1, p2] + d[p0, p1 + 1, p2] - two_d0
h22 = d[p0, p1, p2 - 1] + d[p0, p1, p2 + 1] - two_d0
h01 = 0.25 * (
d[p0 + 1, p1 + 1, p2]
- d[p0 - 1, p1 + 1, p2]
- d[p0 + 1, p1 - 1, p2]
+ d[p0 - 1, p1 - 1, p2]
)
h02 = 0.25 * (
d[p0 + 1, p1, p2 + 1]
- d[p0 + 1, p1, p2 - 1]
+ d[p0 - 1, p1, p2 - 1]
- d[p0 - 1, p1, p2 + 1]
)
h12 = 0.25 * (
d[p0, p1 + 1, p2 + 1]
- d[p0, p1 + 1, p2 - 1]
+ d[p0, p1 - 1, p2 - 1]
- d[p0, p1 - 1, p2 + 1]
)
return (h00, h11, h22, h01, h02, h12)
def _offsets(grad, hess):
"""Compute position refinement offsets from gradient and Hessian.
This is equivalent to np.linalg.solve(-H, J) where H is the Hessian
matrix and J is the gradient (Jacobian).
This analytical solution is adapted from (BSD-licensed) C code by
Otero et. al (see SIFT docstring References).
"""
h00, h11, h22, h01, h02, h12 = hess
g0, g1, g2 = grad
det = h00 * h11 * h22
det -= h00 * h12 * h12
det -= h01 * h01 * h22
det += 2 * h01 * h02 * h12
det -= h02 * h02 * h11
aa = (h11 * h22 - h12 * h12) / det
ab = (h02 * h12 - h01 * h22) / det
ac = (h01 * h12 - h02 * h11) / det
bb = (h00 * h22 - h02 * h02) / det
bc = (h01 * h02 - h00 * h12) / det
cc = (h00 * h11 - h01 * h01) / det
offset0 = -aa * g0 - ab * g1 - ac * g2
offset1 = -ab * g0 - bb * g1 - bc * g2
offset2 = -ac * g0 - bc * g1 - cc * g2
return np.stack((offset0, offset1, offset2), axis=-1)
class SIFT(FeatureDetector, DescriptorExtractor):
"""SIFT feature detection and descriptor extraction.
Parameters
----------
upsampling : int, optional
Prior to the feature detection the image is upscaled by a factor
of 1 (no upscaling), 2 or 4. Method: Bi-cubic interpolation.
n_octaves : int, optional
Maximum number of octaves. With every octave the image size is
halved and the sigma doubled. The number of octaves will be
reduced as needed to keep at least 12 pixels along each dimension
at the smallest scale.
n_scales : int, optional
Maximum number of scales in every octave.
sigma_min : float, optional
The blur level of the seed image. If upsampling is enabled
sigma_min is scaled by factor 1/upsampling
sigma_in : float, optional
The assumed blur level of the input image.
c_dog : float, optional
Threshold to discard low contrast extrema in the DoG. It's final
value is dependent on n_scales by the relation:
final_c_dog = (2^(1/n_scales)-1) / (2^(1/3)-1) * c_dog
c_edge : float, optional
Threshold to discard extrema that lie in edges. If H is the
Hessian of an extremum, its "edgeness" is described by
tr(H)²/det(H). If the edgeness is higher than
(c_edge + 1)²/c_edge, the extremum is discarded.
n_bins : int, optional
Number of bins in the histogram that describes the gradient
orientations around keypoint.
lambda_ori : float, optional
The window used to find the reference orientation of a keypoint
has a width of 6 * lambda_ori * sigma and is weighted by a
standard deviation of 2 * lambda_ori * sigma.
c_max : float, optional
The threshold at which a secondary peak in the orientation
histogram is accepted as orientation
lambda_descr : float, optional
The window used to define the descriptor of a keypoint has a width
of 2 * lambda_descr * sigma * (n_hist+1)/n_hist and is weighted by
a standard deviation of lambda_descr * sigma.
n_hist : int, optional
The window used to define the descriptor of a keypoint consists of
n_hist * n_hist histograms.
n_ori : int, optional
The number of bins in the histograms of the descriptor patch.
Attributes
----------
delta_min : float
The sampling distance of the first octave. It's final value is
1/upsampling.
float_dtype : type
The datatype of the image.
scalespace_sigmas : (n_octaves, n_scales + 3) array
The sigma value of all scales in all octaves.
keypoints : (N, 2) array
Keypoint coordinates as ``(row, col)``.
positions : (N, 2) array
Subpixel-precision keypoint coordinates as ``(row, col)``.
sigmas : (N,) array
The corresponding sigma (blur) value of a keypoint.
scales : (N,) array
The corresponding scale of a keypoint.
orientations : (N,) array
The orientations of the gradient around every keypoint.
octaves : (N,) array
The corresponding octave of a keypoint.
descriptors : (N, n_hist*n_hist*n_ori) array
The descriptors of a keypoint.
Notes
-----
The SIFT algorithm was developed by David Lowe [1]_, [2]_ and later
patented by the University of British Columbia. Since the patent expired in
2020 it's free to use. The implementation here closely follows the
detailed description in [3]_, including use of the same default parameters.
References
----------
.. [1] D.G. Lowe. "Object recognition from local scale-invariant
features", Proceedings of the Seventh IEEE International
Conference on Computer Vision, 1999, vol.2, pp. 1150-1157.
:DOI:`10.1109/ICCV.1999.790410`
.. [2] D.G. Lowe. "Distinctive Image Features from Scale-Invariant
Keypoints", International Journal of Computer Vision, 2004,
vol. 60, pp. 91110.
:DOI:`10.1023/B:VISI.0000029664.99615.94`
.. [3] I. R. Otero and M. Delbracio. "Anatomy of the SIFT Method",
Image Processing On Line, 4 (2014), pp. 370396.
:DOI:`10.5201/ipol.2014.82`
Examples
--------
>>> from skimage.feature import SIFT, match_descriptors
>>> from skimage.data import camera
>>> from skimage.transform import rotate
>>> img1 = camera()
>>> img2 = rotate(camera(), 90)
>>> detector_extractor1 = SIFT()
>>> detector_extractor2 = SIFT()
>>> detector_extractor1.detect_and_extract(img1)
>>> detector_extractor2.detect_and_extract(img2)
>>> matches = match_descriptors(detector_extractor1.descriptors,
... detector_extractor2.descriptors,
... max_ratio=0.6)
>>> matches[10:15]
array([[ 10, 412],
[ 11, 417],
[ 12, 407],
[ 13, 411],
[ 14, 406]])
>>> detector_extractor1.keypoints[matches[10:15, 0]]
array([[ 95, 214],
[ 97, 211],
[ 97, 218],
[102, 215],
[104, 218]])
>>> detector_extractor2.keypoints[matches[10:15, 1]]
array([[297, 95],
[301, 97],
[294, 97],
[297, 102],
[293, 104]])
"""
def __init__(
self,
upsampling=2,
n_octaves=8,
n_scales=3,
sigma_min=1.6,
sigma_in=0.5,
c_dog=0.04 / 3,
c_edge=10,
n_bins=36,
lambda_ori=1.5,
c_max=0.8,
lambda_descr=6,
n_hist=4,
n_ori=8,
):
if upsampling in [1, 2, 4]:
self.upsampling = upsampling
else:
raise ValueError("upsampling must be 1, 2 or 4")
self.n_octaves = n_octaves
self.n_scales = n_scales
self.sigma_min = sigma_min / upsampling
self.sigma_in = sigma_in
self.c_dog = (2 ** (1 / n_scales) - 1) / (2 ** (1 / 3) - 1) * c_dog
self.c_edge = c_edge
self.n_bins = n_bins
self.lambda_ori = lambda_ori
self.c_max = c_max
self.lambda_descr = lambda_descr
self.n_hist = n_hist
self.n_ori = n_ori
self.delta_min = 1 / upsampling
self.float_dtype = None
self.scalespace_sigmas = None
self.keypoints = None
self.positions = None
self.sigmas = None
self.scales = None
self.orientations = None
self.octaves = None
self.descriptors = None
@property
def deltas(self):
"""The sampling distances of all octaves"""
deltas = self.delta_min * np.power(
2, np.arange(self.n_octaves), dtype=self.float_dtype
)
return deltas
def _set_number_of_octaves(self, image_shape):
size_min = 12 # minimum size of last octave
s0 = min(image_shape) * self.upsampling
max_octaves = int(math.log2(s0 / size_min) + 1)
if max_octaves < self.n_octaves:
self.n_octaves = max_octaves
def _create_scalespace(self, image):
"""Source: "Anatomy of the SIFT Method" Alg. 1
Construction of the scalespace by gradually blurring (scales) and
downscaling (octaves) the image.
"""
scalespace = []
if self.upsampling > 1:
image = rescale(image, self.upsampling, order=1)
# smooth to sigma_min, assuming sigma_in
image = gaussian(
image,
sigma=self.upsampling * math.sqrt(self.sigma_min**2 - self.sigma_in**2),
mode='reflect',
)
# Eq. 10: sigmas.shape = (n_octaves, n_scales + 3).
# The three extra scales are:
# One for the differences needed for DoG and two auxiliary
# images (one at either end) for peak_local_max with exclude
# border = True (see Fig. 5)
# The smoothing doubles after n_scales steps.
tmp = np.power(2, np.arange(self.n_scales + 3) / self.n_scales)
tmp *= self.sigma_min
# all sigmas for the gaussian scalespace
sigmas = self.deltas[:, np.newaxis] / self.deltas[0] * tmp[np.newaxis, :]
self.scalespace_sigmas = sigmas
# Eq. 7: Gaussian smoothing depends on difference with previous sigma
# gaussian_sigmas.shape = (n_octaves, n_scales + 2)
var_diff = np.diff(sigmas * sigmas, axis=1)
gaussian_sigmas = np.sqrt(var_diff) / self.deltas[:, np.newaxis]
# one octave is represented by a 3D image with depth (n_scales+x)
for o in range(self.n_octaves):
# Temporarily put scales axis first so octave[i] is C-contiguous
# (this makes Gaussian filtering faster).
octave = np.empty(
(self.n_scales + 3,) + image.shape, dtype=self.float_dtype, order='C'
)
octave[0] = image
for s in range(1, self.n_scales + 3):
# blur new scale assuming sigma of the last one
gaussian(
octave[s - 1],
sigma=gaussian_sigmas[o, s - 1],
mode='reflect',
out=octave[s],
)
# move scales to last axis as expected by other methods
scalespace.append(np.moveaxis(octave, 0, -1))
if o < self.n_octaves - 1:
# downscale the image by taking every second pixel
image = octave[self.n_scales][::2, ::2]
return scalespace
def _inrange(self, a, dim):
return (
(a[:, 0] > 0)
& (a[:, 0] < dim[0] - 1)
& (a[:, 1] > 0)
& (a[:, 1] < dim[1] - 1)
)
def _find_localize_evaluate(self, dogspace, img_shape):
"""Source: "Anatomy of the SIFT Method" Alg. 4-9
1) first find all extrema of a (3, 3, 3) neighborhood
2) use second order Taylor development to refine the positions to
sub-pixel precision
3) filter out extrema that have low contrast and lie on edges or close
to the image borders
"""
extrema_pos = []
extrema_scales = []
extrema_sigmas = []
threshold = self.c_dog * 0.8
for o, (octave, delta) in enumerate(zip(dogspace, self.deltas)):
# find extrema
keys = _local_max(np.ascontiguousarray(octave), threshold)
if keys.size == 0:
extrema_pos.append(np.empty((0, 2)))
continue
# localize extrema
oshape = octave.shape
refinement_iterations = 5
offset_max = 0.6
for i in range(refinement_iterations):
if i > 0:
# exclude any keys that have moved out of bounds
keys = keys[self._inrange(keys, oshape), :]
# Jacobian and Hessian of all extrema
grad = _sparse_gradient(octave, keys)
hess = _hessian(octave, keys)
# solve for offset of the extremum
off = _offsets(grad, hess)
if i == refinement_iterations - 1:
break
# offset is too big and an increase would not bring us out of
# bounds
wrong_position_pos = np.logical_and(
off > offset_max, keys + 1 < tuple([a - 1 for a in oshape])
)
wrong_position_neg = np.logical_and(off < -offset_max, keys - 1 > 0)
if not np.any(np.logical_or(wrong_position_neg, wrong_position_pos)):
break
keys[wrong_position_pos] += 1
keys[wrong_position_neg] -= 1
# mask for all extrema that have been localized successfully
finished = np.all(np.abs(off) < offset_max, axis=1)
keys = keys[finished]
off = off[finished]
grad = [g[finished] for g in grad]
# value of extremum in octave
vals = octave[keys[:, 0], keys[:, 1], keys[:, 2]]
# values at interpolated point
w = vals
for i in range(3):
w += 0.5 * grad[i] * off[:, i]
h00, h11, h01 = hess[0][finished], hess[1][finished], hess[3][finished]
sigmaratio = self.scalespace_sigmas[0, 1] / self.scalespace_sigmas[0, 0]
# filter for contrast, edgeness and borders
contrast_threshold = self.c_dog
contrast_filter = np.abs(w) > contrast_threshold
edge_threshold = np.square(self.c_edge + 1) / self.c_edge
edge_response = _edgeness(
h00[contrast_filter], h11[contrast_filter], h01[contrast_filter]
)
edge_filter = np.abs(edge_response) <= edge_threshold
keys = keys[contrast_filter][edge_filter]
off = off[contrast_filter][edge_filter]
yx = ((keys[:, :2] + off[:, :2]) * delta).astype(self.float_dtype)
sigmas = self.scalespace_sigmas[o, keys[:, 2]] * np.power(
sigmaratio, off[:, 2]
)
border_filter = np.all(
np.logical_and(
(yx - sigmas[:, np.newaxis]) > 0.0,
(yx + sigmas[:, np.newaxis]) < img_shape,
),
axis=1,
)
extrema_pos.append(yx[border_filter])
extrema_scales.append(keys[border_filter, 2])
extrema_sigmas.append(sigmas[border_filter])
octave_indices = np.concatenate(
[np.full(len(p), i) for i, p in enumerate(extrema_pos)]
)
if len(octave_indices) == 0:
raise RuntimeError(
"SIFT found no features. Try passing in an image containing "
"greater intensity contrasts between adjacent pixels."
)
extrema_pos = np.concatenate(extrema_pos)
extrema_scales = np.concatenate(extrema_scales)
extrema_sigmas = np.concatenate(extrema_sigmas)
return extrema_pos, extrema_scales, extrema_sigmas, octave_indices
def _fit(self, h):
"""Refine the position of the peak by fitting it to a parabola"""
return (h[0] - h[2]) / (2 * (h[0] + h[2] - 2 * h[1]))
def _compute_orientation(
self, positions_oct, scales_oct, sigmas_oct, octaves, gaussian_scalespace
):
"""Source: "Anatomy of the SIFT Method" Alg. 11
Calculates the orientation of the gradient around every keypoint
"""
gradient_space = []
# list for keypoints that have more than one reference orientation
keypoint_indices = []
keypoint_angles = []
keypoint_octave = []
orientations = np.zeros_like(sigmas_oct, dtype=self.float_dtype)
key_count = 0
for o, (octave, delta) in enumerate(zip(gaussian_scalespace, self.deltas)):
gradient_space.append(np.gradient(octave))
in_oct = octaves == o
if not np.any(in_oct):
continue
positions = positions_oct[in_oct]
scales = scales_oct[in_oct]
sigmas = sigmas_oct[in_oct]
oshape = octave.shape[:2]
# convert to octave's dimensions
yx = positions / delta
sigma = sigmas / delta
# dimensions of the patch
radius = 3 * self.lambda_ori * sigma
p_min = np.maximum(0, yx - radius[:, np.newaxis] + 0.5).astype(int)
p_max = np.minimum(
yx + radius[:, np.newaxis] + 0.5, (oshape[0] - 1, oshape[1] - 1)
).astype(int)
# orientation histogram
hist = np.empty(self.n_bins, dtype=self.float_dtype)
avg_kernel = np.full((3,), 1 / 3, dtype=self.float_dtype)
for k in range(len(yx)):
hist[:] = 0
# use the patch coordinates to get the gradient and then
# normalize them
r, c = np.meshgrid(
np.arange(p_min[k, 0], p_max[k, 0] + 1),
np.arange(p_min[k, 1], p_max[k, 1] + 1),
indexing='ij',
sparse=True,
)
gradient_row = gradient_space[o][0][r, c, scales[k]]
gradient_col = gradient_space[o][1][r, c, scales[k]]
r = r.astype(self.float_dtype, copy=False)
c = c.astype(self.float_dtype, copy=False)
r -= yx[k, 0]
c -= yx[k, 1]
# gradient magnitude and angles
magnitude = np.sqrt(np.square(gradient_row) + np.square(gradient_col))
theta = np.mod(np.arctan2(gradient_col, gradient_row), 2 * np.pi)
# more weight to center values
kernel = np.exp(
np.divide(r * r + c * c, -2 * (self.lambda_ori * sigma[k]) ** 2)
)
# fill the histogram
bins = np.floor(
(theta / (2 * np.pi) * self.n_bins + 0.5) % self.n_bins
).astype(int)
np.add.at(hist, bins, kernel * magnitude)
# smooth the histogram and find the maximum
hist = np.concatenate((hist[-6:], hist, hist[:6]))
for _ in range(6): # number of smoothings
hist = np.convolve(hist, avg_kernel, mode='same')
hist = hist[6:-6]
max_filter = ndi.maximum_filter(hist, [3], mode='wrap')
# if an angle is in 80% percent range of the maximum, a
# new keypoint is created for it
maxima = np.nonzero(
np.logical_and(
hist >= (self.c_max * np.max(hist)), max_filter == hist
)
)
# save the angles
for c, m in enumerate(maxima[0]):
neigh = np.arange(m - 1, m + 2) % len(hist)
# use neighbors to fit a parabola, to get more accurate
# result
ori = (m + self._fit(hist[neigh]) + 0.5) * 2 * np.pi / self.n_bins
if ori > np.pi:
ori -= 2 * np.pi
if c == 0:
orientations[key_count] = ori
else:
keypoint_indices.append(key_count)
keypoint_angles.append(ori)
keypoint_octave.append(o)
key_count += 1
self.positions = np.concatenate(
(positions_oct, positions_oct[keypoint_indices])
)
self.scales = np.concatenate((scales_oct, scales_oct[keypoint_indices]))
self.sigmas = np.concatenate((sigmas_oct, sigmas_oct[keypoint_indices]))
self.orientations = np.concatenate((orientations, keypoint_angles))
self.octaves = np.concatenate((octaves, keypoint_octave))
# return the gradient_space to reuse it to find the descriptor
return gradient_space
def _rotate(self, row, col, angle):
c = math.cos(angle)
s = math.sin(angle)
rot_row = c * row + s * col
rot_col = -s * row + c * col
return rot_row, rot_col
def _compute_descriptor(self, gradient_space):
"""Source: "Anatomy of the SIFT Method" Alg. 12
Calculates the descriptor for every keypoint
"""
n_key = len(self.scales)
self.descriptors = np.empty(
(n_key, self.n_hist**2 * self.n_ori), dtype=np.uint8
)
# indices of the histograms
hists = np.arange(1, self.n_hist + 1, dtype=self.float_dtype)
# indices of the bins
bins = np.arange(1, self.n_ori + 1, dtype=self.float_dtype)
key_numbers = np.arange(n_key)
for o, (gradient, delta) in enumerate(zip(gradient_space, self.deltas)):
in_oct = self.octaves == o
if not np.any(in_oct):
continue
positions = self.positions[in_oct]
scales = self.scales[in_oct]
sigmas = self.sigmas[in_oct]
orientations = self.orientations[in_oct]
numbers = key_numbers[in_oct]
dim = gradient[0].shape[:2]
center_pos = positions / delta
sigma = sigmas / delta
# dimensions of the patch
radius = self.lambda_descr * (1 + 1 / self.n_hist) * sigma
radius_patch = math.sqrt(2) * radius
p_min = np.asarray(
np.maximum(0, center_pos - radius_patch[:, np.newaxis] + 0.5), dtype=int
)
p_max = np.asarray(
np.minimum(
center_pos + radius_patch[:, np.newaxis] + 0.5,
(dim[0] - 1, dim[1] - 1),
),
dtype=int,
)
for k in range(len(p_max)):
rad_k = float(radius[k])
ori = float(orientations[k])
histograms = np.zeros(
(self.n_hist, self.n_hist, self.n_ori), dtype=self.float_dtype
)
# the patch
r, c = np.meshgrid(
np.arange(p_min[k, 0], p_max[k, 0]),
np.arange(p_min[k, 1], p_max[k, 1]),
indexing='ij',
sparse=True,
)
# normalized coordinates
r_norm = np.subtract(r, center_pos[k, 0], dtype=self.float_dtype)
c_norm = np.subtract(c, center_pos[k, 1], dtype=self.float_dtype)
r_norm, c_norm = self._rotate(r_norm, c_norm, ori)
# select coordinates and gradient values within the patch
inside = np.maximum(np.abs(r_norm), np.abs(c_norm)) < rad_k
r_norm, c_norm = r_norm[inside], c_norm[inside]
r_idx, c_idx = np.nonzero(inside)
r = r[r_idx, 0]
c = c[0, c_idx]
gradient_row = gradient[0][r, c, scales[k]]
gradient_col = gradient[1][r, c, scales[k]]
# compute the (relative) gradient orientation
theta = np.arctan2(gradient_col, gradient_row) - ori
lam_sig = self.lambda_descr * float(sigma[k])
# Gaussian weighted kernel magnitude
kernel = np.exp((r_norm * r_norm + c_norm * c_norm) / (-2 * lam_sig**2))
magnitude = (
np.sqrt(gradient_row * gradient_row + gradient_col * gradient_col)
* kernel
)
lam_sig_ratio = 2 * lam_sig / self.n_hist
rc_bins = (hists - (1 + self.n_hist) / 2) * lam_sig_ratio
rc_bin_spacing = lam_sig_ratio
ori_bins = (2 * np.pi * bins) / self.n_ori
# distances to the histograms and bins
dist_r = np.abs(np.subtract.outer(rc_bins, r_norm))
dist_c = np.abs(np.subtract.outer(rc_bins, c_norm))
# the orientation histograms/bins that get the contribution
near_t, near_t_val = _ori_distances(ori_bins, theta)
# create the histogram
_update_histogram(
histograms,
near_t,
near_t_val,
magnitude,
dist_r,
dist_c,
rc_bin_spacing,
)
# convert the histograms to a 1d descriptor
histograms = histograms.reshape(-1)
# saturate the descriptor
histograms = np.minimum(histograms, 0.2 * np.linalg.norm(histograms))
# normalize the descriptor
descriptor = (512 * histograms) / np.linalg.norm(histograms)
# quantize the descriptor
descriptor = np.minimum(np.floor(descriptor), 255)
self.descriptors[numbers[k], :] = descriptor
def _preprocess(self, image):
check_nD(image, 2)
image = img_as_float(image)
self.float_dtype = _supported_float_type(image.dtype)
image = image.astype(self.float_dtype, copy=False)
self._set_number_of_octaves(image.shape)
return image
def detect(self, image):
"""Detect the keypoints.
Parameters
----------
image : 2D array
Input image.
"""
image = self._preprocess(image)
gaussian_scalespace = self._create_scalespace(image)
dog_scalespace = [np.diff(layer, axis=2) for layer in gaussian_scalespace]
positions, scales, sigmas, octaves = self._find_localize_evaluate(
dog_scalespace, image.shape
)
self._compute_orientation(
positions, scales, sigmas, octaves, gaussian_scalespace
)
self.keypoints = self.positions.round().astype(int)
def extract(self, image):
"""Extract the descriptors for all keypoints in the image.
Parameters
----------
image : 2D array
Input image.
"""
image = self._preprocess(image)
gaussian_scalespace = self._create_scalespace(image)
gradient_space = [np.gradient(octave) for octave in gaussian_scalespace]
self._compute_descriptor(gradient_space)
def detect_and_extract(self, image):
"""Detect the keypoints and extract their descriptors.
Parameters
----------
image : 2D array
Input image.
"""
image = self._preprocess(image)
gaussian_scalespace = self._create_scalespace(image)
dog_scalespace = [np.diff(layer, axis=2) for layer in gaussian_scalespace]
positions, scales, sigmas, octaves = self._find_localize_evaluate(
dog_scalespace, image.shape
)
gradient_space = self._compute_orientation(
positions, scales, sigmas, octaves, gaussian_scalespace
)
self._compute_descriptor(gradient_space)
self.keypoints = self.positions.round().astype(int)

View File

@@ -0,0 +1,186 @@
import math
import numpy as np
from scipy.signal import fftconvolve
from .._shared.utils import check_nD, _supported_float_type
def _window_sum_2d(image, window_shape):
window_sum = np.cumsum(image, axis=0)
window_sum = window_sum[window_shape[0] : -1] - window_sum[: -window_shape[0] - 1]
window_sum = np.cumsum(window_sum, axis=1)
window_sum = (
window_sum[:, window_shape[1] : -1] - window_sum[:, : -window_shape[1] - 1]
)
return window_sum
def _window_sum_3d(image, window_shape):
window_sum = _window_sum_2d(image, window_shape)
window_sum = np.cumsum(window_sum, axis=2)
window_sum = (
window_sum[:, :, window_shape[2] : -1]
- window_sum[:, :, : -window_shape[2] - 1]
)
return window_sum
def match_template(
image, template, pad_input=False, mode='constant', constant_values=0
):
"""Match a template to a 2-D or 3-D image using normalized correlation.
The output is an array with values between -1.0 and 1.0. The value at a
given position corresponds to the correlation coefficient between the image
and the template.
For `pad_input=True` matches correspond to the center and otherwise to the
top-left corner of the template. To find the best match you must search for
peaks in the response (output) image.
Parameters
----------
image : (M, N[, P]) array
2-D or 3-D input image.
template : (m, n[, p]) array
Template to locate. It must be `(m <= M, n <= N[, p <= P])`.
pad_input : bool
If True, pad `image` so that output is the same size as the image, and
output values correspond to the template center. Otherwise, the output
is an array with shape `(M - m + 1, N - n + 1)` for an `(M, N)` image
and an `(m, n)` template, and matches correspond to origin
(top-left corner) of the template.
mode : see `numpy.pad`, optional
Padding mode.
constant_values : see `numpy.pad`, optional
Constant values used in conjunction with ``mode='constant'``.
Returns
-------
output : array
Response image with correlation coefficients.
Notes
-----
Details on the cross-correlation are presented in [1]_. This implementation
uses FFT convolutions of the image and the template. Reference [2]_
presents similar derivations but the approximation presented in this
reference is not used in our implementation.
References
----------
.. [1] J. P. Lewis, "Fast Normalized Cross-Correlation", Industrial Light
and Magic.
.. [2] Briechle and Hanebeck, "Template Matching using Fast Normalized
Cross Correlation", Proceedings of the SPIE (2001).
:DOI:`10.1117/12.421129`
Examples
--------
>>> template = np.zeros((3, 3))
>>> template[1, 1] = 1
>>> template
array([[0., 0., 0.],
[0., 1., 0.],
[0., 0., 0.]])
>>> image = np.zeros((6, 6))
>>> image[1, 1] = 1
>>> image[4, 4] = -1
>>> image
array([[ 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., -1., 0.],
[ 0., 0., 0., 0., 0., 0.]])
>>> result = match_template(image, template)
>>> np.round(result, 3)
array([[ 1. , -0.125, 0. , 0. ],
[-0.125, -0.125, 0. , 0. ],
[ 0. , 0. , 0.125, 0.125],
[ 0. , 0. , 0.125, -1. ]])
>>> result = match_template(image, template, pad_input=True)
>>> np.round(result, 3)
array([[-0.125, -0.125, -0.125, 0. , 0. , 0. ],
[-0.125, 1. , -0.125, 0. , 0. , 0. ],
[-0.125, -0.125, -0.125, 0. , 0. , 0. ],
[ 0. , 0. , 0. , 0.125, 0.125, 0.125],
[ 0. , 0. , 0. , 0.125, -1. , 0.125],
[ 0. , 0. , 0. , 0.125, 0.125, 0.125]])
"""
check_nD(image, (2, 3))
if image.ndim < template.ndim:
raise ValueError(
"Dimensionality of template must be less than or "
"equal to the dimensionality of image."
)
if np.any(np.less(image.shape, template.shape)):
raise ValueError("Image must be larger than template.")
image_shape = image.shape
float_dtype = _supported_float_type(image.dtype)
image = image.astype(float_dtype, copy=False)
pad_width = tuple((width, width) for width in template.shape)
if mode == 'constant':
image = np.pad(
image, pad_width=pad_width, mode=mode, constant_values=constant_values
)
else:
image = np.pad(image, pad_width=pad_width, mode=mode)
# Use special case for 2-D images for much better performance in
# computation of integral images
if image.ndim == 2:
image_window_sum = _window_sum_2d(image, template.shape)
image_window_sum2 = _window_sum_2d(image**2, template.shape)
elif image.ndim == 3:
image_window_sum = _window_sum_3d(image, template.shape)
image_window_sum2 = _window_sum_3d(image**2, template.shape)
template_mean = template.mean()
template_volume = math.prod(template.shape)
template_ssd = np.sum((template - template_mean) ** 2)
if image.ndim == 2:
xcorr = fftconvolve(image, template[::-1, ::-1], mode="valid")[1:-1, 1:-1]
elif image.ndim == 3:
xcorr = fftconvolve(image, template[::-1, ::-1, ::-1], mode="valid")[
1:-1, 1:-1, 1:-1
]
numerator = xcorr - image_window_sum * template_mean
denominator = image_window_sum2
np.multiply(image_window_sum, image_window_sum, out=image_window_sum)
np.divide(image_window_sum, template_volume, out=image_window_sum)
denominator -= image_window_sum
denominator *= template_ssd
np.maximum(denominator, 0, out=denominator) # sqrt of negative number not allowed
np.sqrt(denominator, out=denominator)
response = np.zeros_like(xcorr, dtype=float_dtype)
# avoid zero-division
mask = denominator > np.finfo(float_dtype).eps
response[mask] = numerator[mask] / denominator[mask]
slices = []
for i in range(template.ndim):
if pad_input:
d0 = (template.shape[i] - 1) // 2
d1 = d0 + image_shape[i]
else:
d0 = template.shape[i] - 1
d1 = d0 + image_shape[i] - template.shape[i] + 1
slices.append(slice(d0, d1))
return response[tuple(slices)]

View File

@@ -0,0 +1,62 @@
import pytest
import numpy as np
from skimage.feature import multiscale_basic_features
@pytest.mark.parametrize('edges', (False, True))
@pytest.mark.parametrize('texture', (False, True))
def test_multiscale_basic_features_gray(edges, texture):
img = np.zeros((20, 20))
img[:10] = 1
img += 0.05 * np.random.randn(*img.shape)
features = multiscale_basic_features(img, edges=edges, texture=texture)
n_sigmas = 6
intensity = True
assert features.shape[-1] == (
n_sigmas * (int(intensity) + int(edges) + 2 * int(texture))
)
assert features.shape[:-1] == img.shape[:]
@pytest.mark.parametrize('edges', (False, True))
@pytest.mark.parametrize('texture', (False, True))
def test_multiscale_basic_features_rgb(edges, texture):
img = np.zeros((20, 20, 3))
img[:10] = 1
img += 0.05 * np.random.randn(*img.shape)
features = multiscale_basic_features(
img, edges=edges, texture=texture, channel_axis=-1
)
n_sigmas = 6
intensity = True
assert features.shape[-1] == (
3 * n_sigmas * (int(intensity) + int(edges) + 2 * int(texture))
)
assert features.shape[:-1] == img.shape[:-1]
@pytest.mark.parametrize('channel_axis', [0, 1, 2, -1, -2])
def test_multiscale_basic_features_channel_axis(channel_axis):
num_channels = 5
shape_spatial = (10, 10)
ndim = len(shape_spatial)
shape = tuple(np.insert(shape_spatial, channel_axis % (ndim + 1), num_channels))
img = np.zeros(shape)
img[:10] = 1
img += 0.05 * np.random.randn(*img.shape)
n_sigmas = 2
# features for all channels are concatenated along the last axis
features = multiscale_basic_features(
img, sigma_min=1, sigma_max=2, channel_axis=channel_axis
)
assert features.shape[-1] == 5 * n_sigmas * 4
assert features.shape[:-1] == np.moveaxis(img, channel_axis, -1).shape[:-1]
# Consider channel_axis as spatial dimension
features = multiscale_basic_features(img, sigma_min=1, sigma_max=2)
assert features.shape[-1] == n_sigmas * 5
assert features.shape[:-1] == img.shape

View File

@@ -0,0 +1,588 @@
import math
import numpy as np
import pytest
from numpy.testing import assert_almost_equal
from skimage import feature
from skimage.draw import disk
from skimage.draw.draw3d import ellipsoid
from skimage.feature import blob_dog, blob_doh, blob_log
from skimage.feature.blob import _blob_overlap
@pytest.mark.parametrize('dtype', [np.uint8, np.float16, np.float32, np.float64])
@pytest.mark.parametrize('threshold_type', ['absolute', 'relative'])
def test_blob_dog(dtype, threshold_type):
r2 = math.sqrt(2)
img = np.ones((512, 512), dtype=dtype)
xs, ys = disk((400, 130), 5)
img[xs, ys] = 255
xs, ys = disk((100, 300), 25)
img[xs, ys] = 255
xs, ys = disk((200, 350), 45)
img[xs, ys] = 255
if threshold_type == 'absolute':
threshold = 2.0
if img.dtype.kind != 'f':
# account for internal scaling to [0, 1] by img_as_float
threshold /= np.ptp(img)
threshold_rel = None
elif threshold_type == 'relative':
threshold = None
threshold_rel = 0.5
blobs = blob_dog(
img,
min_sigma=4,
max_sigma=50,
threshold=threshold,
threshold_rel=threshold_rel,
)
def radius(x):
return r2 * x[2]
s = sorted(blobs, key=radius)
thresh = 5
ratio_thresh = 0.25
b = s[0]
assert abs(b[0] - 400) <= thresh
assert abs(b[1] - 130) <= thresh
assert abs(radius(b) - 5) <= ratio_thresh * 5
b = s[1]
assert abs(b[0] - 100) <= thresh
assert abs(b[1] - 300) <= thresh
assert abs(radius(b) - 25) <= ratio_thresh * 25
b = s[2]
assert abs(b[0] - 200) <= thresh
assert abs(b[1] - 350) <= thresh
assert abs(radius(b) - 45) <= ratio_thresh * 45
# Testing no peaks
img_empty = np.zeros((100, 100), dtype=dtype)
assert blob_dog(img_empty).size == 0
@pytest.mark.parametrize('dtype', [np.uint8, np.float16, np.float32, np.float64])
@pytest.mark.parametrize('threshold_type', ['absolute', 'relative'])
def test_blob_dog_3d(dtype, threshold_type):
# Testing 3D
r = 10
pad = 10
im3 = ellipsoid(r, r, r)
im3 = np.pad(im3, pad, mode='constant')
if threshold_type == 'absolute':
threshold = 0.001
threshold_rel = 0
elif threshold_type == 'relative':
threshold = 0
threshold_rel = 0.5
blobs = blob_dog(
im3,
min_sigma=3,
max_sigma=10,
sigma_ratio=1.2,
threshold=threshold,
threshold_rel=threshold_rel,
)
b = blobs[0]
assert b.shape == (4,)
assert b[0] == r + pad + 1
assert b[1] == r + pad + 1
assert b[2] == r + pad + 1
assert abs(math.sqrt(3) * b[3] - r) < 1.1
@pytest.mark.parametrize('dtype', [np.uint8, np.float16, np.float32, np.float64])
@pytest.mark.parametrize('threshold_type', ['absolute', 'relative'])
def test_blob_dog_3d_anisotropic(dtype, threshold_type):
# Testing 3D anisotropic
r = 10
pad = 10
im3 = ellipsoid(r / 2, r, r)
im3 = np.pad(im3, pad, mode='constant')
if threshold_type == 'absolute':
threshold = 0.001
threshold_rel = None
elif threshold_type == 'relative':
threshold = None
threshold_rel = 0.5
blobs = blob_dog(
im3.astype(dtype, copy=False),
min_sigma=[1.5, 3, 3],
max_sigma=[5, 10, 10],
sigma_ratio=1.2,
threshold=threshold,
threshold_rel=threshold_rel,
)
b = blobs[0]
assert b.shape == (6,)
assert b[0] == r / 2 + pad + 1
assert b[1] == r + pad + 1
assert b[2] == r + pad + 1
assert abs(math.sqrt(3) * b[3] - r / 2) < 1.1
assert abs(math.sqrt(3) * b[4] - r) < 1.1
assert abs(math.sqrt(3) * b[5] - r) < 1.1
@pytest.mark.parametrize("disc_center", [(5, 5), (5, 20)])
@pytest.mark.parametrize("exclude_border", [6, (6, 6), (4, 15)])
def test_blob_dog_exclude_border(disc_center, exclude_border):
# Testing exclude border
# image where blob is disc_center px from borders, radius 5
img = np.ones((512, 512))
xs, ys = disk(disc_center, 5)
img[xs, ys] = 255
blobs = blob_dog(
img,
min_sigma=1.5,
max_sigma=5,
sigma_ratio=1.2,
)
assert blobs.shape[0] == 1, "one blob should have been detected"
b = blobs[0]
assert b[0] == disc_center[0], f"blob should be {disc_center[0]} px from x border"
assert b[1] == disc_center[1], f"blob should be {disc_center[1]} px from y border"
blobs = blob_dog(
img,
min_sigma=1.5,
max_sigma=5,
sigma_ratio=1.2,
exclude_border=exclude_border,
)
if disc_center == (5, 20) and exclude_border == (4, 15):
assert blobs.shape[0] == 1, "one blob should have been detected"
b = blobs[0]
assert (
b[0] == disc_center[0]
), f"blob should be {disc_center[0]} px from x border"
assert (
b[1] == disc_center[1]
), f"blob should be {disc_center[1]} px from y border"
else:
msg = "zero blobs should be detected, as only blob is 5 px from border"
assert blobs.shape[0] == 0, msg
@pytest.mark.parametrize('anisotropic', [False, True])
@pytest.mark.parametrize('ndim', [1, 2, 3, 4])
@pytest.mark.parametrize('function_name', ['blob_dog', 'blob_log'])
def test_nd_blob_no_peaks_shape(function_name, ndim, anisotropic):
# uniform image so no blobs will be found
z = np.zeros((16,) * ndim, dtype=np.float32)
if anisotropic:
max_sigma = 8 + np.arange(ndim)
else:
max_sigma = 8
blob_func = getattr(feature, function_name)
blobs = blob_func(z, max_sigma=max_sigma)
# z.ndim + (z.ndim sigmas if anisotropic, only one sigma otherwise)
expected_shape = 2 * z.ndim if anisotropic else z.ndim + 1
assert blobs.shape == (0, expected_shape)
@pytest.mark.parametrize('dtype', [np.uint8, np.float16, np.float32, np.float64])
@pytest.mark.parametrize('threshold_type', ['absolute', 'relative'])
def test_blob_log(dtype, threshold_type):
r2 = math.sqrt(2)
img = np.ones((256, 256), dtype=dtype)
xs, ys = disk((200, 65), 5)
img[xs, ys] = 255
xs, ys = disk((80, 25), 15)
img[xs, ys] = 255
xs, ys = disk((50, 150), 25)
img[xs, ys] = 255
xs, ys = disk((100, 175), 30)
img[xs, ys] = 255
if threshold_type == 'absolute':
threshold = 1
if img.dtype.kind != 'f':
# account for internal scaling to [0, 1] by img_as_float
threshold /= np.ptp(img)
threshold_rel = None
elif threshold_type == 'relative':
threshold = None
threshold_rel = 0.5
blobs = blob_log(
img, min_sigma=5, max_sigma=20, threshold=threshold, threshold_rel=threshold_rel
)
def radius(x):
return r2 * x[2]
s = sorted(blobs, key=radius)
thresh = 3
b = s[0]
assert abs(b[0] - 200) <= thresh
assert abs(b[1] - 65) <= thresh
assert abs(radius(b) - 5) <= thresh
b = s[1]
assert abs(b[0] - 80) <= thresh
assert abs(b[1] - 25) <= thresh
assert abs(radius(b) - 15) <= thresh
b = s[2]
assert abs(b[0] - 50) <= thresh
assert abs(b[1] - 150) <= thresh
assert abs(radius(b) - 25) <= thresh
b = s[3]
assert abs(b[0] - 100) <= thresh
assert abs(b[1] - 175) <= thresh
assert abs(radius(b) - 30) <= thresh
# Testing log scale
blobs = blob_log(
img,
min_sigma=5,
max_sigma=20,
threshold=threshold,
threshold_rel=threshold_rel,
log_scale=True,
)
b = s[0]
assert abs(b[0] - 200) <= thresh
assert abs(b[1] - 65) <= thresh
assert abs(radius(b) - 5) <= thresh
b = s[1]
assert abs(b[0] - 80) <= thresh
assert abs(b[1] - 25) <= thresh
assert abs(radius(b) - 15) <= thresh
b = s[2]
assert abs(b[0] - 50) <= thresh
assert abs(b[1] - 150) <= thresh
assert abs(radius(b) - 25) <= thresh
b = s[3]
assert abs(b[0] - 100) <= thresh
assert abs(b[1] - 175) <= thresh
assert abs(radius(b) - 30) <= thresh
# Testing no peaks
img_empty = np.zeros((100, 100))
assert blob_log(img_empty).size == 0
def test_blob_log_no_warnings():
img = np.ones((11, 11))
xs, ys = disk((5, 5), 2)
img[xs, ys] = 255
xs, ys = disk((7, 6), 2)
img[xs, ys] = 255
blob_log(img, max_sigma=20, num_sigma=10, threshold=0.1)
def test_blob_log_3d():
# Testing 3D
r = 6
pad = 10
im3 = ellipsoid(r, r, r)
im3 = np.pad(im3, pad, mode='constant')
blobs = blob_log(im3, min_sigma=3, max_sigma=10)
b = blobs[0]
assert b.shape == (4,)
assert b[0] == r + pad + 1
assert b[1] == r + pad + 1
assert b[2] == r + pad + 1
assert abs(math.sqrt(3) * b[3] - r) < 1
def test_blob_log_3d_anisotropic():
# Testing 3D anisotropic
r = 6
pad = 10
im3 = ellipsoid(r / 2, r, r)
im3 = np.pad(im3, pad, mode='constant')
blobs = blob_log(
im3,
min_sigma=[1, 2, 2],
max_sigma=[5, 10, 10],
)
b = blobs[0]
assert b.shape == (6,)
assert b[0] == r / 2 + pad + 1
assert b[1] == r + pad + 1
assert b[2] == r + pad + 1
assert abs(math.sqrt(3) * b[3] - r / 2) < 1
assert abs(math.sqrt(3) * b[4] - r) < 1
assert abs(math.sqrt(3) * b[5] - r) < 1
@pytest.mark.parametrize("disc_center", [(5, 5), (5, 20)])
@pytest.mark.parametrize("exclude_border", [6, (6, 6), (4, 15)])
def test_blob_log_exclude_border(disc_center, exclude_border):
# image where blob is disc_center px from borders, radius 5
img = np.ones((512, 512))
xs, ys = disk(disc_center, 5)
img[xs, ys] = 255
blobs = blob_log(
img,
min_sigma=1.5,
max_sigma=5,
)
assert blobs.shape[0] == 1
b = blobs[0]
assert b[0] == disc_center[0], f"blob should be {disc_center[0]} px from x border"
assert b[1] == disc_center[1], f"blob should be {disc_center[1]} px from y border"
blobs = blob_log(
img,
min_sigma=1.5,
max_sigma=5,
exclude_border=exclude_border,
)
if disc_center == (5, 20) and exclude_border == (4, 15):
assert blobs.shape[0] == 1, "one blob should have been detected"
b = blobs[0]
assert (
b[0] == disc_center[0]
), f"blob should be {disc_center[0]} px from x border"
assert (
b[1] == disc_center[1]
), f"blob should be {disc_center[1]} px from y border"
else:
msg = "zero blobs should be detected, as only blob is 5 px from border"
assert blobs.shape[0] == 0, msg
@pytest.mark.parametrize("dtype", [np.uint8, np.float16, np.float32])
@pytest.mark.parametrize('threshold_type', ['absolute', 'relative'])
def test_blob_doh(dtype, threshold_type):
img = np.ones((512, 512), dtype=dtype)
xs, ys = disk((400, 130), 20)
img[xs, ys] = 255
xs, ys = disk((460, 50), 30)
img[xs, ys] = 255
xs, ys = disk((100, 300), 40)
img[xs, ys] = 255
xs, ys = disk((200, 350), 50)
img[xs, ys] = 255
if threshold_type == 'absolute':
# Note: have to either scale up threshold or rescale the image to the
# range [0, 1] internally.
threshold = 0.05
if img.dtype.kind == 'f':
# account for lack of internal scaling to [0, 1] by img_as_float
ptp = np.ptp(img)
threshold *= ptp**2
threshold_rel = None
elif threshold_type == 'relative':
threshold = None
threshold_rel = 0.5
blobs = blob_doh(
img,
min_sigma=1,
max_sigma=60,
num_sigma=10,
threshold=threshold,
threshold_rel=threshold_rel,
)
def radius(x):
return x[2]
s = sorted(blobs, key=radius)
thresh = 4
b = s[0]
assert abs(b[0] - 400) <= thresh
assert abs(b[1] - 130) <= thresh
assert abs(radius(b) - 20) <= thresh
b = s[1]
assert abs(b[0] - 460) <= thresh
assert abs(b[1] - 50) <= thresh
assert abs(radius(b) - 30) <= thresh
b = s[2]
assert abs(b[0] - 100) <= thresh
assert abs(b[1] - 300) <= thresh
assert abs(radius(b) - 40) <= thresh
b = s[3]
assert abs(b[0] - 200) <= thresh
assert abs(b[1] - 350) <= thresh
assert abs(radius(b) - 50) <= thresh
def test_blob_doh_log_scale():
img = np.ones((512, 512), dtype=np.uint8)
xs, ys = disk((400, 130), 20)
img[xs, ys] = 255
xs, ys = disk((460, 50), 30)
img[xs, ys] = 255
xs, ys = disk((100, 300), 40)
img[xs, ys] = 255
xs, ys = disk((200, 350), 50)
img[xs, ys] = 255
blobs = blob_doh(
img, min_sigma=1, max_sigma=60, num_sigma=10, log_scale=True, threshold=0.05
)
def radius(x):
return x[2]
s = sorted(blobs, key=radius)
thresh = 10
b = s[0]
assert abs(b[0] - 400) <= thresh
assert abs(b[1] - 130) <= thresh
assert abs(radius(b) - 20) <= thresh
b = s[2]
assert abs(b[0] - 460) <= thresh
assert abs(b[1] - 50) <= thresh
assert abs(radius(b) - 30) <= thresh
b = s[1]
assert abs(b[0] - 100) <= thresh
assert abs(b[1] - 300) <= thresh
assert abs(radius(b) - 40) <= thresh
b = s[3]
assert abs(b[0] - 200) <= thresh
assert abs(b[1] - 350) <= thresh
assert abs(radius(b) - 50) <= thresh
def test_blob_doh_no_peaks():
# Testing no peaks
img_empty = np.zeros((100, 100))
assert blob_doh(img_empty).size == 0
def test_blob_doh_overlap():
img = np.ones((256, 256), dtype=np.uint8)
xs, ys = disk((100, 100), 20)
img[xs, ys] = 255
xs, ys = disk((120, 100), 30)
img[xs, ys] = 255
blobs = blob_doh(img, min_sigma=1, max_sigma=60, num_sigma=10, threshold=0.05)
assert len(blobs) == 1
def test_blob_log_overlap_3d():
r1, r2 = 7, 6
pad1, pad2 = 11, 12
blob1 = ellipsoid(r1, r1, r1)
blob1 = np.pad(blob1, pad1, mode='constant')
blob2 = ellipsoid(r2, r2, r2)
blob2 = np.pad(
blob2, [(pad2, pad2), (pad2 - 9, pad2 + 9), (pad2, pad2)], mode='constant'
)
im3 = np.logical_or(blob1, blob2)
blobs = blob_log(im3, min_sigma=2, max_sigma=10, overlap=0.1)
assert len(blobs) == 1
def test_blob_overlap_3d_anisotropic():
# Two spheres with distance between centers equal to radius
# One sphere is much smaller than the other so about half of it is within
# the bigger sphere.
s3 = math.sqrt(3)
overlap = _blob_overlap(
np.array([0, 0, 0, 2 / s3, 10 / s3, 10 / s3]),
np.array([0, 0, 10, 0.2 / s3, 1 / s3, 1 / s3]),
sigma_dim=3,
)
assert_almost_equal(overlap, 0.48125)
overlap = _blob_overlap(
np.array([0, 0, 0, 2 / s3, 10 / s3, 10 / s3]),
np.array([2, 0, 0, 0.2 / s3, 1 / s3, 1 / s3]),
sigma_dim=3,
)
assert_almost_equal(overlap, 0.48125)
def test_blob_log_anisotropic():
image = np.zeros((50, 50))
image[20, 10:20] = 1
isotropic_blobs = blob_log(image, min_sigma=0.5, max_sigma=2, num_sigma=3)
assert len(isotropic_blobs) > 1 # many small blobs found in line
ani_blobs = blob_log(
image, min_sigma=[0.5, 5], max_sigma=[2, 20], num_sigma=3
) # 10x anisotropy, line is 1x10
assert len(ani_blobs) == 1 # single anisotropic blob found
def test_blob_log_overlap_3d_anisotropic():
r1, r2 = 7, 6
pad1, pad2 = 11, 12
blob1 = ellipsoid(r1, r1, r1)
blob1 = np.pad(blob1, pad1, mode='constant')
blob2 = ellipsoid(r2, r2, r2)
blob2 = np.pad(
blob2, [(pad2, pad2), (pad2 - 9, pad2 + 9), (pad2, pad2)], mode='constant'
)
im3 = np.logical_or(blob1, blob2)
blobs = blob_log(im3, min_sigma=[2, 2.01, 2.005], max_sigma=10, overlap=0.1)
assert len(blobs) == 1
# Two circles with distance between centers equal to radius
overlap = _blob_overlap(
np.array([0, 0, 10 / math.sqrt(2)]), np.array([0, 10, 10 / math.sqrt(2)])
)
assert_almost_equal(
overlap, 1.0 / math.pi * (2 * math.acos(1.0 / 2) - math.sqrt(3) / 2.0)
)
def test_no_blob():
im = np.zeros((10, 10))
blobs = blob_log(im, min_sigma=2, max_sigma=5, num_sigma=4)
assert len(blobs) == 0

View File

@@ -0,0 +1,110 @@
import pytest
import copy
import numpy as np
from skimage._shared.testing import assert_array_equal
from skimage import data
from skimage.feature import BRIEF, corner_peaks, corner_harris
from skimage._shared import testing
def test_color_image_unsupported_error():
"""Brief descriptors can be evaluated on gray-scale images only."""
img = np.zeros((20, 20, 3))
keypoints = np.asarray([[7, 5], [11, 13]])
with testing.raises(ValueError):
BRIEF().extract(img, keypoints)
@pytest.mark.parametrize('dtype', ['float32', 'float64', 'uint8', 'int'])
def test_normal_mode(dtype):
"""Verify the computed BRIEF descriptors with expected for normal mode."""
img = data.coins().astype(dtype)
keypoints = corner_peaks(
corner_harris(img), min_distance=5, threshold_abs=0, threshold_rel=0.1
)
extractor = BRIEF(descriptor_size=8, sigma=2)
extractor.extract(img, keypoints[:8])
expected = np.array(
[
[1, 1, 1, 0, 1, 1, 0, 1],
[0, 1, 1, 0, 1, 1, 0, 0],
[1, 1, 1, 0, 1, 1, 0, 1],
[0, 0, 0, 1, 0, 0, 1, 0],
[0, 1, 1, 0, 1, 1, 0, 0],
[0, 1, 1, 0, 1, 1, 1, 0],
[1, 1, 1, 0, 1, 1, 0, 1],
[1, 0, 1, 0, 0, 1, 1, 0],
],
dtype=bool,
)
assert_array_equal(extractor.descriptors, expected)
@pytest.mark.parametrize('dtype', ['float32', 'float64', 'uint8', 'int'])
def test_uniform_mode(dtype):
"""Verify the computed BRIEF descriptors with expected for uniform mode."""
img = data.coins().astype(dtype)
keypoints = corner_peaks(
corner_harris(img), min_distance=5, threshold_abs=0, threshold_rel=0.1
)
extractor = BRIEF(descriptor_size=8, sigma=2, mode='uniform', rng=1)
BRIEF(descriptor_size=8, sigma=2, mode='uniform', rng=1)
extractor.extract(img, keypoints[:8])
expected = np.array(
[
[0, 1, 0, 1, 0, 1, 1, 0],
[0, 1, 0, 0, 0, 1, 0, 1],
[0, 1, 0, 0, 0, 1, 1, 1],
[1, 0, 1, 0, 1, 0, 1, 1],
[0, 0, 1, 0, 0, 1, 0, 1],
[0, 1, 0, 1, 0, 1, 0, 1],
[0, 1, 0, 0, 0, 1, 1, 1],
[1, 0, 1, 1, 1, 0, 0, 1],
],
dtype=bool,
)
assert_array_equal(extractor.descriptors, expected)
def test_unsupported_mode():
with testing.raises(ValueError):
BRIEF(mode='foobar')
@pytest.mark.parametrize('dtype', ['float32', 'float64', 'uint8', 'int'])
def test_border(dtype):
img = np.zeros((100, 100), dtype=dtype)
keypoints = np.array([[1, 1], [20, 20], [50, 50], [80, 80]])
extractor = BRIEF(patch_size=41, rng=1)
extractor.extract(img, keypoints)
assert extractor.descriptors.shape[0] == 3
assert_array_equal(extractor.mask, (False, True, True, True))
def test_independent_rng():
img = np.zeros((100, 100), dtype=int)
keypoints = np.array([[1, 1], [20, 20], [50, 50], [80, 80]])
rng = np.random.default_rng()
extractor = BRIEF(patch_size=41, rng=rng)
x = copy.deepcopy(extractor.rng).random()
rng.random()
extractor.extract(img, keypoints)
z = copy.deepcopy(extractor.rng).random()
assert x == z

View File

@@ -0,0 +1,185 @@
import unittest
import numpy as np
import pytest
from skimage._shared.testing import assert_equal
from scipy.ndimage import binary_dilation, binary_erosion
from skimage import data, feature
from skimage.util import img_as_float
class TestCanny(unittest.TestCase):
def test_00_00_zeros(self):
'''Test that the Canny filter finds no points for a blank field'''
result = feature.canny(np.zeros((20, 20)), 4, 0, 0, np.ones((20, 20), bool))
self.assertFalse(np.any(result))
def test_00_01_zeros_mask(self):
'''Test that the Canny filter finds no points in a masked image'''
result = feature.canny(
np.random.uniform(size=(20, 20)), 4, 0, 0, np.zeros((20, 20), bool)
)
self.assertFalse(np.any(result))
def test_01_01_circle(self):
'''Test that the Canny filter finds the outlines of a circle'''
i, j = np.mgrid[-200:200, -200:200].astype(float) / 200
c = np.abs(np.sqrt(i * i + j * j) - 0.5) < 0.02
result = feature.canny(c.astype(float), 4, 0, 0, np.ones(c.shape, bool))
#
# erode and dilate the circle to get rings that should contain the
# outlines
#
cd = binary_dilation(c, iterations=3)
ce = binary_erosion(c, iterations=3)
cde = np.logical_and(cd, np.logical_not(ce))
self.assertTrue(np.all(cde[result]))
#
# The circle has a radius of 100. There are two rings here, one
# for the inside edge and one for the outside. So that's
# 100 * 2 * 2 * 3 for those places where pi is still 3.
# The edge contains both pixels if there's a tie, so we
# bump the count a little.
point_count = np.sum(result)
self.assertTrue(point_count > 1200)
self.assertTrue(point_count < 1600)
def test_01_02_circle_with_noise(self):
'''Test that the Canny filter finds the circle outlines
in a noisy image'''
np.random.seed(0)
i, j = np.mgrid[-200:200, -200:200].astype(float) / 200
c = np.abs(np.sqrt(i * i + j * j) - 0.5) < 0.02
cf = c.astype(float) * 0.5 + np.random.uniform(size=c.shape) * 0.5
result = feature.canny(cf, 4, 0.1, 0.2, np.ones(c.shape, bool))
#
# erode and dilate the circle to get rings that should contain the
# outlines
#
cd = binary_dilation(c, iterations=4)
ce = binary_erosion(c, iterations=4)
cde = np.logical_and(cd, np.logical_not(ce))
self.assertTrue(np.all(cde[result]))
point_count = np.sum(result)
self.assertTrue(point_count > 1200)
self.assertTrue(point_count < 1600)
def test_image_shape(self):
self.assertRaises(ValueError, feature.canny, np.zeros((20, 20, 20)), 4, 0, 0)
def test_mask_none(self):
result1 = feature.canny(np.zeros((20, 20)), 4, 0, 0, np.ones((20, 20), bool))
result2 = feature.canny(np.zeros((20, 20)), 4, 0, 0)
self.assertTrue(np.all(result1 == result2))
def test_use_quantiles(self):
image = img_as_float(data.camera()[::100, ::100])
# Correct output produced manually with quantiles
# of 0.8 and 0.6 for high and low respectively
correct_output = np.array(
[
[False, False, False, False, False, False],
[False, True, True, True, False, False],
[False, False, False, True, False, False],
[False, False, False, True, False, False],
[False, False, True, True, False, False],
[False, False, False, False, False, False],
]
)
result = feature.canny(
image, low_threshold=0.6, high_threshold=0.8, use_quantiles=True
)
assert_equal(result, correct_output)
def test_img_all_ones(self):
image = np.ones((10, 10))
assert np.all(feature.canny(image) == 0)
def test_invalid_use_quantiles(self):
image = img_as_float(data.camera()[::50, ::50])
self.assertRaises(
ValueError,
feature.canny,
image,
use_quantiles=True,
low_threshold=0.5,
high_threshold=3.6,
)
self.assertRaises(
ValueError,
feature.canny,
image,
use_quantiles=True,
low_threshold=-5,
high_threshold=0.5,
)
self.assertRaises(
ValueError,
feature.canny,
image,
use_quantiles=True,
low_threshold=99,
high_threshold=0.9,
)
self.assertRaises(
ValueError,
feature.canny,
image,
use_quantiles=True,
low_threshold=0.5,
high_threshold=-100,
)
# Example from issue #4282
image = data.camera()
self.assertRaises(
ValueError,
feature.canny,
image,
use_quantiles=True,
low_threshold=50,
high_threshold=150,
)
def test_dtype(self):
"""Check that the same output is produced regardless of image dtype."""
image_uint8 = data.camera()
image_float = img_as_float(image_uint8)
result_uint8 = feature.canny(image_uint8)
result_float = feature.canny(image_float)
assert_equal(result_uint8, result_float)
low = 0.1
high = 0.2
assert_equal(
feature.canny(image_float, 1.0, low, high),
feature.canny(image_uint8, 1.0, 255 * low, 255 * high),
)
def test_full_mask_matches_no_mask(self):
"""The masked and unmasked algorithms should return the same result."""
image = data.camera()
for mode in ('constant', 'nearest', 'reflect'):
assert_equal(
feature.canny(image, mode=mode),
feature.canny(image, mode=mode, mask=np.ones_like(image, dtype=bool)),
)
def test_unsupported_int64(self):
for dtype in (np.int64, np.uint64):
image = np.zeros((10, 10), dtype=dtype)
image[3, 3] = np.iinfo(dtype).max
with pytest.raises(
ValueError, match="64-bit integer images are not supported"
):
feature.canny(image)

View File

@@ -0,0 +1,18 @@
import skimage.data as data
from skimage.feature import Cascade
def test_detector_astronaut():
# Load the trained file from the module root.
trained_file = data.lbp_frontal_face_cascade_filename()
# Initialize the detector cascade.
detector = Cascade(trained_file)
img = data.astronaut()
detected = detector.detect_multi_scale(
img=img, scale_factor=1.2, step_ratio=1, min_size=(60, 60), max_size=(123, 123)
)
assert len(detected) == 1, 'One face should be detected.'

View File

@@ -0,0 +1,95 @@
import numpy as np
from skimage._shared.testing import assert_array_equal
from skimage.data import moon
from skimage.feature import CENSURE
from skimage._shared.testing import run_in_parallel
from skimage._shared import testing
from skimage.transform import rescale
img = moon()
np.random.seed(0)
def test_censure_on_rectangular_images():
"""Censure feature detector should work on 2D image of any shape."""
rect_image = np.random.rand(300, 200)
square_image = np.random.rand(200, 200)
CENSURE().detect(square_image)
CENSURE().detect(rect_image)
def test_keypoints_censure_color_image_unsupported_error():
"""Censure keypoints can be extracted from gray-scale images only."""
with testing.raises(ValueError):
CENSURE().detect(np.zeros((20, 20, 3)))
def test_keypoints_censure_mode_validity_error():
"""Mode argument in keypoints_censure can be either DoB, Octagon or
STAR."""
with testing.raises(ValueError):
CENSURE(mode='dummy')
def test_keypoints_censure_scale_range_error():
"""Difference between the the max_scale and min_scale parameters in
keypoints_censure should be greater than or equal to two."""
with testing.raises(ValueError):
CENSURE(min_scale=1, max_scale=2)
def test_keypoints_censure_moon_image_dob():
"""Verify the actual Censure keypoints and their corresponding scale with
the expected values for DoB filter."""
detector = CENSURE()
detector.detect(img)
expected_keypoints = np.array(
[
[21, 497],
[36, 46],
[119, 350],
[185, 177],
[287, 250],
[357, 239],
[463, 116],
[464, 132],
[467, 260],
]
)
expected_scales = np.array([3, 4, 4, 2, 2, 3, 2, 2, 2])
assert_array_equal(expected_keypoints, detector.keypoints)
assert_array_equal(expected_scales, detector.scales)
@run_in_parallel()
def test_keypoints_censure_moon_image_octagon():
"""Verify the actual Censure keypoints and their corresponding scale with
the expected values for Octagon filter."""
detector = CENSURE(mode='octagon')
# quarter scale image for speed
detector.detect(rescale(img, 0.25, anti_aliasing=False, mode='constant'))
expected_keypoints = np.array([[23, 27], [29, 89], [31, 87], [106, 59], [111, 67]])
expected_scales = np.array([3, 2, 5, 2, 4])
assert_array_equal(expected_keypoints, detector.keypoints)
assert_array_equal(expected_scales, detector.scales)
def test_keypoints_censure_moon_image_star():
"""Verify the actual Censure keypoints and their corresponding scale with
the expected values for STAR filter."""
detector = CENSURE(mode='star')
# quarter scale image for speed
detector.detect(rescale(img, 0.25, anti_aliasing=False, mode='constant'))
expected_keypoints = np.array(
[[23, 27], [29, 89], [30, 86], [107, 59], [109, 64], [111, 67], [113, 70]]
)
expected_scales = np.array([3, 2, 4, 2, 5, 3, 2])
assert_array_equal(expected_keypoints, detector.keypoints)
assert_array_equal(expected_scales, detector.scales)

View File

@@ -0,0 +1,818 @@
import numpy as np
import pytest
from numpy.testing import assert_almost_equal, assert_array_equal, assert_equal
from skimage import data, draw, img_as_float
from skimage._shared._warnings import expected_warnings
from skimage._shared.testing import run_in_parallel
from skimage._shared.utils import _supported_float_type
from skimage.color import rgb2gray
from skimage.feature import (
corner_fast,
corner_foerstner,
corner_harris,
corner_kitchen_rosenfeld,
corner_moravec,
corner_orientations,
corner_peaks,
corner_shi_tomasi,
corner_subpix,
hessian_matrix,
hessian_matrix_det,
hessian_matrix_eigvals,
peak_local_max,
shape_index,
structure_tensor,
structure_tensor_eigenvalues,
)
from skimage.morphology import cube, octagon
@pytest.fixture
def im3d():
r = 10
pad = 10
im3 = draw.ellipsoid(r, r, r)
im3 = np.pad(im3, pad, mode='constant').astype(np.uint8)
return im3
@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64])
def test_structure_tensor(dtype):
square = np.zeros((5, 5), dtype=dtype)
square[2, 2] = 1
Arr, Arc, Acc = structure_tensor(square, sigma=0.1, order='rc')
out_dtype = _supported_float_type(dtype)
assert all(a.dtype == out_dtype for a in (Arr, Arc, Acc))
assert_array_equal(
Acc,
np.array(
[
[0, 0, 0, 0, 0],
[0, 1, 0, 1, 0],
[0, 4, 0, 4, 0],
[0, 1, 0, 1, 0],
[0, 0, 0, 0, 0],
]
),
)
assert_array_equal(
Arc,
np.array(
[
[0, 0, 0, 0, 0],
[0, 1, 0, -1, 0],
[0, 0, 0, -0, 0],
[0, -1, -0, 1, 0],
[0, 0, 0, 0, 0],
]
),
)
assert_array_equal(
Arr,
np.array(
[
[0, 0, 0, 0, 0],
[0, 1, 4, 1, 0],
[0, 0, 0, 0, 0],
[0, 1, 4, 1, 0],
[0, 0, 0, 0, 0],
]
),
)
@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64])
def test_structure_tensor_3d(dtype):
cube = np.zeros((5, 5, 5), dtype=dtype)
cube[2, 2, 2] = 1
A_elems = structure_tensor(cube, sigma=0.1)
assert all(a.dtype == _supported_float_type(dtype) for a in A_elems)
assert_equal(len(A_elems), 6)
assert_array_equal(
A_elems[0][:, 1, :],
np.array(
[
[0, 0, 0, 0, 0],
[0, 1, 4, 1, 0],
[0, 0, 0, 0, 0],
[0, 1, 4, 1, 0],
[0, 0, 0, 0, 0],
]
),
)
assert_array_equal(
A_elems[0][1],
np.array(
[
[0, 0, 0, 0, 0],
[0, 1, 4, 1, 0],
[0, 4, 16, 4, 0],
[0, 1, 4, 1, 0],
[0, 0, 0, 0, 0],
]
),
)
assert_array_equal(
A_elems[3][2],
np.array(
[
[0, 0, 0, 0, 0],
[0, 4, 16, 4, 0],
[0, 0, 0, 0, 0],
[0, 4, 16, 4, 0],
[0, 0, 0, 0, 0],
]
),
)
def test_structure_tensor_3d_rc_only():
cube = np.zeros((5, 5, 5))
with pytest.raises(ValueError):
structure_tensor(cube, sigma=0.1, order='xy')
A_elems_rc = structure_tensor(cube, sigma=0.1, order='rc')
A_elems_none = structure_tensor(cube, sigma=0.1)
assert_array_equal(A_elems_rc, A_elems_none)
def test_structure_tensor_orders():
square = np.zeros((5, 5))
square[2, 2] = 1
A_elems_default = structure_tensor(square, sigma=0.1)
A_elems_xy = structure_tensor(square, sigma=0.1, order='xy')
A_elems_rc = structure_tensor(square, sigma=0.1, order='rc')
assert_array_equal(A_elems_rc, A_elems_default)
assert_array_equal(A_elems_xy, A_elems_default[::-1])
@pytest.mark.parametrize('ndim', [2, 3])
def test_structure_tensor_sigma(ndim):
img = np.zeros((5,) * ndim)
img[[2] * ndim] = 1
A_default = structure_tensor(img, sigma=0.1, order='rc')
A_tuple = structure_tensor(img, sigma=(0.1,) * ndim, order='rc')
A_list = structure_tensor(img, sigma=[0.1] * ndim, order='rc')
assert_array_equal(A_tuple, A_default)
assert_array_equal(A_list, A_default)
with pytest.raises(ValueError):
structure_tensor(img, sigma=(0.1,) * (ndim - 1), order='rc')
with pytest.raises(ValueError):
structure_tensor(img, sigma=[0.1] * (ndim + 1), order='rc')
@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64])
def test_hessian_matrix(dtype):
square = np.zeros((5, 5), dtype=dtype)
square[2, 2] = 4
Hrr, Hrc, Hcc = hessian_matrix(
square, sigma=0.1, order='rc', use_gaussian_derivatives=False
)
out_dtype = _supported_float_type(dtype)
assert all(a.dtype == out_dtype for a in (Hrr, Hrc, Hcc))
assert_almost_equal(
Hrr,
np.array(
[
[0, 0, 2, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, -2, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 2, 0, 0],
]
),
)
assert_almost_equal(
Hrc,
np.array(
[
[0, 0, 0, 0, 0],
[0, 1, 0, -1, 0],
[0, 0, 0, 0, 0],
[0, -1, 0, 1, 0],
[0, 0, 0, 0, 0],
]
),
)
assert_almost_equal(
Hcc,
np.array(
[
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[2, 0, -2, 0, 2],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
]
),
)
with expected_warnings(["use_gaussian_derivatives currently defaults"]):
# FutureWarning warning when use_gaussian_derivatives is not
# specified.
hessian_matrix(square, sigma=0.1, order='rc')
@pytest.mark.parametrize('use_gaussian_derivatives', [False, True])
def test_hessian_matrix_order(use_gaussian_derivatives):
square = np.zeros((5, 5), dtype=float)
square[2, 2] = 4
Hxx, Hxy, Hyy = hessian_matrix(
square, sigma=0.1, order="xy", use_gaussian_derivatives=use_gaussian_derivatives
)
Hrr, Hrc, Hcc = hessian_matrix(
square, sigma=0.1, order="rc", use_gaussian_derivatives=use_gaussian_derivatives
)
# verify results are equivalent, just reversed in order
assert_array_equal(Hxx, Hcc)
assert_array_equal(Hxy, Hrc)
assert_array_equal(Hyy, Hrr)
def test_hessian_matrix_3d():
cube = np.zeros((5, 5, 5))
cube[2, 2, 2] = 4
Hs = hessian_matrix(cube, sigma=0.1, order='rc', use_gaussian_derivatives=False)
assert len(Hs) == 6, f"incorrect number of Hessian images ({len(Hs)}) for 3D"
# This test didn't catch the fix in gh-6624 (passes with and without) ...
assert_almost_equal(
Hs[2][:, 2, :],
np.array(
[
[0, 0, 0, 0, 0],
[0, 1, 0, -1, 0],
[0, 0, 0, 0, 0],
[0, -1, 0, 1, 0],
[0, 0, 0, 0, 0],
]
),
)
# ... so we add another test that fails for the not-fixed hessian_matrix
assert_almost_equal(
Hs[0][:, 2, :],
np.array(
[
[0, 0, 2, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, -2, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 2, 0, 0],
]
),
)
@pytest.mark.parametrize('use_gaussian_derivatives', [False, True])
def test_hessian_matrix_3d_xy(use_gaussian_derivatives):
img = np.ones((5, 5, 5))
# order="xy" is only permitted for 2D
with pytest.raises(ValueError):
hessian_matrix(
img,
sigma=0.1,
order="xy",
use_gaussian_derivatives=use_gaussian_derivatives,
)
with pytest.raises(ValueError):
hessian_matrix(
img,
sigma=0.1,
order='nonexistant',
use_gaussian_derivatives=use_gaussian_derivatives,
)
@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64])
def test_structure_tensor_eigenvalues(dtype):
square = np.zeros((5, 5), dtype=dtype)
square[2, 2] = 1
A_elems = structure_tensor(square, sigma=0.1, order='rc')
l1, l2 = structure_tensor_eigenvalues(A_elems)
out_dtype = _supported_float_type(dtype)
assert all(a.dtype == out_dtype for a in (l1, l2))
assert_array_equal(
l1,
np.array(
[
[0, 0, 0, 0, 0],
[0, 2, 4, 2, 0],
[0, 4, 0, 4, 0],
[0, 2, 4, 2, 0],
[0, 0, 0, 0, 0],
]
),
)
assert_array_equal(
l2,
np.array(
[
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
]
),
)
def test_structure_tensor_eigenvalues_3d():
image = np.pad(cube(9, dtype=np.int64), 5, mode='constant') * 1000
boundary = (
np.pad(cube(9), 5, mode='constant') - np.pad(cube(7), 6, mode='constant')
).astype(bool)
A_elems = structure_tensor(image, sigma=0.1)
e0, e1, e2 = structure_tensor_eigenvalues(A_elems)
# e0 should detect facets
assert np.all(e0[boundary] != 0)
@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64])
def test_hessian_matrix_eigvals(dtype):
square = np.zeros((5, 5), dtype=dtype)
square[2, 2] = 4
H = hessian_matrix(square, sigma=0.1, order='rc', use_gaussian_derivatives=False)
l1, l2 = hessian_matrix_eigvals(H)
out_dtype = _supported_float_type(dtype)
assert all(a.dtype == out_dtype for a in (l1, l2))
assert_almost_equal(
l1,
np.array(
[
[0, 0, 2, 0, 0],
[0, 1, 0, 1, 0],
[2, 0, -2, 0, 2],
[0, 1, 0, 1, 0],
[0, 0, 2, 0, 0],
]
),
)
assert_almost_equal(
l2,
np.array(
[
[0, 0, 0, 0, 0],
[0, -1, 0, -1, 0],
[0, 0, -2, 0, 0],
[0, -1, 0, -1, 0],
[0, 0, 0, 0, 0],
]
),
)
@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64])
def test_hessian_matrix_eigvals_3d(im3d, dtype):
im3d = im3d.astype(dtype, copy=False)
H = hessian_matrix(im3d, use_gaussian_derivatives=False)
E = hessian_matrix_eigvals(H)
out_dtype = _supported_float_type(dtype)
assert all(a.dtype == out_dtype for a in E)
# test descending order:
e0, e1, e2 = E
assert np.all(e0 >= e1) and np.all(e1 >= e2)
E0, E1, E2 = E[:, E.shape[1] // 2] # cross section
row_center, col_center = np.array(E0.shape) // 2
circles = [
draw.circle_perimeter(row_center, col_center, radius, shape=E0.shape)
for radius in range(1, E0.shape[1] // 2 - 1)
]
response0 = np.array([np.mean(E0[c]) for c in circles])
response2 = np.array([np.mean(E2[c]) for c in circles])
# eigenvalues are negative just inside the sphere, positive just outside
assert np.argmin(response2) < np.argmax(response0)
assert np.min(response2) < 0
assert np.max(response0) > 0
@run_in_parallel()
def test_hessian_matrix_det():
image = np.zeros((5, 5))
image[2, 2] = 1
det = hessian_matrix_det(image, 5)
assert_almost_equal(det, 0, decimal=3)
@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64])
def test_hessian_matrix_det_3d(im3d, dtype):
im3d = im3d.astype(dtype, copy=False)
D = hessian_matrix_det(im3d)
assert D.dtype == _supported_float_type(dtype)
D0 = D[D.shape[0] // 2]
row_center, col_center = np.array(D0.shape) // 2
# testing in 3D is hard. We test this by showing that you get the
# expected flat-then-low-then-high 2nd derivative response in a circle
# around the midplane of the sphere.
circles = [
draw.circle_perimeter(row_center, col_center, r, shape=D0.shape)
for r in range(1, D0.shape[1] // 2 - 1)
]
response = np.array([np.mean(D0[c]) for c in circles])
lowest = np.argmin(response)
highest = np.argmax(response)
assert lowest < highest
assert response[lowest] < 0
assert response[highest] > 0
def test_shape_index():
# software floating point arm doesn't raise a warning on divide by zero
# https://github.com/scikit-image/scikit-image/issues/3335
square = np.zeros((5, 5))
square[2, 2] = 4
with expected_warnings([r'divide by zero|\A\Z', r'invalid value|\A\Z']):
s = shape_index(square, sigma=0.1)
assert_almost_equal(
s,
np.array(
[
[np.nan, np.nan, -0.5, np.nan, np.nan],
[np.nan, 0, np.nan, 0, np.nan],
[-0.5, np.nan, -1, np.nan, -0.5],
[np.nan, 0, np.nan, 0, np.nan],
[np.nan, np.nan, -0.5, np.nan, np.nan],
]
),
)
@run_in_parallel()
def test_square_image():
im = np.zeros((50, 50)).astype(float)
im[:25, :25] = 1.0
# Moravec
results = corner_moravec(im) > 0
# interest points along edge
assert np.count_nonzero(results) == 92
# Harris
results = peak_local_max(
corner_harris(im, method='k'), min_distance=10, threshold_rel=0
)
# interest at corner
assert len(results) == 1
results = peak_local_max(
corner_harris(im, method='eps'), min_distance=10, threshold_rel=0
)
# interest at corner
assert len(results) == 1
# Shi-Tomasi
results = peak_local_max(corner_shi_tomasi(im), min_distance=10, threshold_rel=0)
# interest at corner
assert len(results) == 1
@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64])
@pytest.mark.parametrize(
'func',
[
corner_moravec,
corner_harris,
corner_shi_tomasi,
corner_kitchen_rosenfeld,
],
)
def test_corner_dtype(dtype, func):
im = np.zeros((50, 50), dtype=dtype)
im[:25, :25] = 1.0
out_dtype = _supported_float_type(dtype)
corners = func(im)
assert corners.dtype == out_dtype
@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64])
def test_corner_foerstner_dtype(dtype):
im = np.zeros((50, 50), dtype=dtype)
im[:25, :25] = 1.0
out_dtype = _supported_float_type(dtype)
assert all(arr.dtype == out_dtype for arr in corner_foerstner(im))
def test_noisy_square_image():
im = np.zeros((50, 50)).astype(float)
im[:25, :25] = 1.0
rng = np.random.default_rng(1234)
im = im + rng.uniform(size=im.shape) * 0.2
# Moravec
results = peak_local_max(corner_moravec(im), min_distance=10, threshold_rel=0)
# undefined number of interest points
assert results.any()
# Harris
results = peak_local_max(
corner_harris(im, method='k'), min_distance=10, threshold_rel=0
)
assert len(results) == 1
results = peak_local_max(
corner_harris(im, method='eps'), min_distance=10, threshold_rel=0
)
assert len(results) == 1
# Shi-Tomasi
results = peak_local_max(
corner_shi_tomasi(im, sigma=1.5), min_distance=10, threshold_rel=0
)
assert len(results) == 1
def test_squared_dot():
im = np.zeros((50, 50))
im[4:8, 4:8] = 1
im = img_as_float(im)
# Moravec fails
# Harris
results = peak_local_max(corner_harris(im), min_distance=10, threshold_rel=0)
assert (results == np.array([[6, 6]])).all()
# Shi-Tomasi
results = peak_local_max(corner_shi_tomasi(im), min_distance=10, threshold_rel=0)
assert (results == np.array([[6, 6]])).all()
def test_rotated_img():
"""
The harris filter should yield the same results with an image and it's
rotation.
"""
im = img_as_float(data.astronaut().mean(axis=2))
im_rotated = im.T
# Moravec
results = np.nonzero(corner_moravec(im))
results_rotated = np.nonzero(corner_moravec(im_rotated))
assert (np.sort(results[0]) == np.sort(results_rotated[1])).all()
assert (np.sort(results[1]) == np.sort(results_rotated[0])).all()
# Harris
results = np.nonzero(corner_harris(im))
results_rotated = np.nonzero(corner_harris(im_rotated))
assert (np.sort(results[0]) == np.sort(results_rotated[1])).all()
assert (np.sort(results[1]) == np.sort(results_rotated[0])).all()
# Shi-Tomasi
results = np.nonzero(corner_shi_tomasi(im))
results_rotated = np.nonzero(corner_shi_tomasi(im_rotated))
assert (np.sort(results[0]) == np.sort(results_rotated[1])).all()
assert (np.sort(results[1]) == np.sort(results_rotated[0])).all()
@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64])
def test_subpix_edge(dtype):
img = np.zeros((50, 50), dtype=dtype)
img[:25, :25] = 255
img[25:, 25:] = 255
corner = peak_local_max(
corner_harris(img), min_distance=10, threshold_rel=0, num_peaks=1
)
subpix = corner_subpix(img, corner)
assert subpix.dtype == _supported_float_type(dtype)
assert_array_equal(subpix[0], (24.5, 24.5))
def test_subpix_dot():
img = np.zeros((50, 50))
img[25, 25] = 255
corner = peak_local_max(
corner_harris(img), min_distance=10, threshold_rel=0, num_peaks=1
)
subpix = corner_subpix(img, corner)
assert_array_equal(subpix[0], (25, 25))
def test_subpix_no_class():
img = np.zeros((50, 50))
subpix = corner_subpix(img, np.array([[25, 25]]))
assert_array_equal(subpix[0], (np.nan, np.nan))
img[25, 25] = 1e-10
corner = peak_local_max(
corner_harris(img), min_distance=10, threshold_rel=0, num_peaks=1
)
subpix = corner_subpix(img, corner)
assert_array_equal(subpix[0], (np.nan, np.nan))
def test_subpix_border():
img = np.zeros((50, 50))
img[1:25, 1:25] = 255
img[25:-1, 25:-1] = 255
corner = corner_peaks(corner_harris(img), threshold_rel=0)
subpix = corner_subpix(img, corner, window_size=11)
ref = np.array(
[
[24.5, 24.5],
[0.52040816, 0.52040816],
[0.52040816, 24.47959184],
[24.47959184, 0.52040816],
[24.52040816, 48.47959184],
[48.47959184, 24.52040816],
[48.47959184, 48.47959184],
]
)
assert_almost_equal(subpix, ref)
def test_num_peaks():
"""For a bunch of different values of num_peaks, check that
peak_local_max returns exactly the right amount of peaks. Test
is run on the astronaut image in order to produce a sufficient number of
corners.
"""
img_corners = corner_harris(rgb2gray(data.astronaut()))
for i in range(20):
n = np.random.randint(1, 21)
results = peak_local_max(
img_corners, min_distance=10, threshold_rel=0, num_peaks=n
)
assert results.shape[0] == n
def test_corner_peaks():
response = np.zeros((10, 10))
response[2:5, 2:5] = 1
response[8:10, 0:2] = 1
corners = corner_peaks(
response, exclude_border=False, min_distance=10, threshold_rel=0
)
assert corners.shape == (1, 2)
corners = corner_peaks(
response, exclude_border=False, min_distance=5, threshold_rel=0
)
assert corners.shape == (2, 2)
corners = corner_peaks(response, exclude_border=False, min_distance=1)
assert corners.shape == (5, 2)
corners = corner_peaks(
response, exclude_border=False, min_distance=1, indices=False
)
assert np.sum(corners) == 5
def test_blank_image_nans():
"""Some of the corner detectors had a weakness in terms of returning
NaN when presented with regions of constant intensity. This should
be fixed by now. We test whether each detector returns something
finite in the case of constant input"""
detectors = [
corner_moravec,
corner_harris,
corner_shi_tomasi,
corner_kitchen_rosenfeld,
corner_foerstner,
]
constant_image = np.zeros((20, 20))
for det in detectors:
response = det(constant_image)
assert np.all(np.isfinite(response))
def test_corner_fast_image_unsupported_error():
img = np.zeros((20, 20, 3))
with pytest.raises(ValueError):
corner_fast(img)
@run_in_parallel()
def test_corner_fast_astronaut():
img = rgb2gray(data.astronaut())
expected = np.array(
[
[444, 310],
[374, 171],
[249, 171],
[492, 139],
[403, 162],
[496, 266],
[362, 328],
[476, 250],
[353, 172],
[346, 279],
[494, 169],
[177, 156],
[413, 181],
[213, 117],
[390, 149],
[140, 205],
[232, 266],
[489, 155],
[387, 195],
[101, 198],
[363, 192],
[364, 147],
[300, 244],
[325, 245],
[141, 242],
[401, 197],
[197, 148],
[339, 242],
[188, 113],
[362, 252],
[379, 183],
[358, 307],
[245, 137],
[369, 159],
[464, 251],
[305, 57],
[223, 375],
]
)
actual = corner_peaks(corner_fast(img, 12, 0.3), min_distance=10, threshold_rel=0)
assert_array_equal(actual, expected)
def test_corner_orientations_image_unsupported_error():
img = np.zeros((20, 20, 3))
with pytest.raises(ValueError):
corner_orientations(img, np.asarray([[7, 7]]), np.ones((3, 3)))
def test_corner_orientations_even_shape_error():
img = np.zeros((20, 20))
with pytest.raises(ValueError):
corner_orientations(img, np.asarray([[7, 7]]), np.ones((4, 4)))
@run_in_parallel()
def test_corner_orientations_astronaut():
img = rgb2gray(data.astronaut())
corners = corner_peaks(
corner_fast(img, 11, 0.35), min_distance=10, threshold_abs=0, threshold_rel=0.1
)
expected = np.array(
[
-4.40598471e-01,
-1.46554357e00,
2.39291733e00,
-1.63869275e00,
1.45931342e00,
-1.64397304e00,
-1.76069982e00,
1.09650167e00,
-1.65449964e00,
1.19134149e00,
5.46905279e-02,
2.17103132e00,
8.12701702e-01,
-1.22091334e-01,
-2.01162417e00,
1.25854853e00,
3.05330950e00,
2.01197383e00,
1.07812134e00,
3.09780364e00,
-3.49561988e-01,
2.43573659e00,
3.14918803e-01,
-9.88548213e-01,
-1.88247204e-01,
2.47305654e00,
-2.99143370e00,
1.47154532e00,
-6.61151410e-01,
-1.68885773e00,
-3.09279990e-01,
-2.81524886e00,
-1.75220190e00,
-1.69230287e00,
-7.52950306e-04,
]
)
actual = corner_orientations(img, corners, octagon(3, 2))
assert_almost_equal(actual, expected)
@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64])
def test_corner_orientations_square(dtype):
square = np.zeros((12, 12), dtype=dtype)
square[3:9, 3:9] = 1
corners = corner_peaks(corner_fast(square, 9), min_distance=1, threshold_rel=0)
actual_orientations = corner_orientations(square, corners, octagon(3, 2))
assert actual_orientations.dtype == _supported_float_type(dtype)
actual_orientations_degrees = np.rad2deg(actual_orientations)
expected_orientations_degree = np.array([45, 135, -45, -135])
assert_array_equal(actual_orientations_degrees, expected_orientations_degree)

View File

@@ -0,0 +1,103 @@
import numpy as np
import pytest
from numpy import sqrt, ceil
from numpy.testing import assert_almost_equal
from skimage import data
from skimage import img_as_float
from skimage.feature import daisy
def test_daisy_color_image_unsupported_error():
img = np.zeros((20, 20, 3))
with pytest.raises(ValueError):
daisy(img)
def test_daisy_desc_dims():
img = img_as_float(data.astronaut()[:128, :128].mean(axis=2))
rings = 2
histograms = 4
orientations = 3
descs = daisy(img, rings=rings, histograms=histograms, orientations=orientations)
assert descs.shape[2] == (rings * histograms + 1) * orientations
rings = 4
histograms = 5
orientations = 13
descs = daisy(img, rings=rings, histograms=histograms, orientations=orientations)
assert descs.shape[2] == (rings * histograms + 1) * orientations
def test_descs_shape():
img = img_as_float(data.astronaut()[:256, :256].mean(axis=2))
radius = 20
step = 8
descs = daisy(img, radius=radius, step=step)
assert descs.shape[0] == ceil((img.shape[0] - radius * 2) / float(step))
assert descs.shape[1] == ceil((img.shape[1] - radius * 2) / float(step))
img = img[:-1, :-2]
radius = 5
step = 3
descs = daisy(img, radius=radius, step=step)
assert descs.shape[0] == ceil((img.shape[0] - radius * 2) / float(step))
assert descs.shape[1] == ceil((img.shape[1] - radius * 2) / float(step))
@pytest.mark.parametrize('dtype', [np.float32, np.float64])
def test_daisy_sigmas_and_radii(dtype):
img = data.astronaut()[:64, :64].mean(axis=2).astype(dtype, copy=False)
sigmas = [1, 2, 3]
radii = [1, 2]
descs = daisy(img, sigmas=sigmas, ring_radii=radii)
assert descs.dtype == img.dtype
def test_daisy_incompatible_sigmas_and_radii():
img = img_as_float(data.astronaut()[:64, :64].mean(axis=2))
sigmas = [1, 2]
radii = [1, 2]
with pytest.raises(ValueError):
daisy(img, sigmas=sigmas, ring_radii=radii)
def test_daisy_normalization():
img = img_as_float(data.astronaut()[:64, :64].mean(axis=2))
descs = daisy(img, normalization='l1')
for i in range(descs.shape[0]):
for j in range(descs.shape[1]):
assert_almost_equal(np.sum(descs[i, j, :]), 1)
descs_ = daisy(img)
assert_almost_equal(descs, descs_)
descs = daisy(img, normalization='l2')
for i in range(descs.shape[0]):
for j in range(descs.shape[1]):
assert_almost_equal(sqrt(np.sum(descs[i, j, :] ** 2)), 1)
orientations = 8
descs = daisy(img, orientations=orientations, normalization='daisy')
desc_dims = descs.shape[2]
for i in range(descs.shape[0]):
for j in range(descs.shape[1]):
for k in range(0, desc_dims, orientations):
assert_almost_equal(
sqrt(np.sum(descs[i, j, k : k + orientations] ** 2)), 1
)
img = np.zeros((50, 50))
descs = daisy(img, normalization='off')
for i in range(descs.shape[0]):
for j in range(descs.shape[1]):
assert_almost_equal(np.sum(descs[i, j, :]), 0)
with pytest.raises(ValueError):
daisy(img, normalization='does_not_exist')
def test_daisy_visualization():
img = img_as_float(data.astronaut()[:32, :32].mean(axis=2))
descs, descs_img = daisy(img, visualize=True)
assert descs_img.shape == (32, 32, 3)

View File

@@ -0,0 +1,191 @@
import pytest
import numpy as np
pytest.importorskip('sklearn')
from skimage.feature._fisher_vector import ( # noqa: E402
learn_gmm,
fisher_vector,
FisherVectorException,
DescriptorException,
)
def test_gmm_wrong_descriptor_format_1():
"""Test that DescriptorException is raised when wrong type for descriptions
is passed.
"""
with pytest.raises(DescriptorException):
learn_gmm('completely wrong test', n_modes=1)
def test_gmm_wrong_descriptor_format_2():
"""Test that DescriptorException is raised when descriptors are of
different dimensionality.
"""
with pytest.raises(DescriptorException):
learn_gmm([np.zeros((5, 11)), np.zeros((4, 10))], n_modes=1)
def test_gmm_wrong_descriptor_format_3():
"""Test that DescriptorException is raised when not all descriptors are of
rank 2.
"""
with pytest.raises(DescriptorException):
learn_gmm([np.zeros((5, 10)), np.zeros((4, 10, 1))], n_modes=1)
def test_gmm_wrong_descriptor_format_4():
"""Test that DescriptorException is raised when elements of descriptor list
are of the incorrect type (i.e. not a NumPy ndarray).
"""
with pytest.raises(DescriptorException):
learn_gmm([[1, 2, 3], [1, 2, 3]], n_modes=1)
def test_gmm_wrong_num_modes_format_1():
"""Test that FisherVectorException is raised when incorrect type for
n_modes is passed into the learn_gmm function.
"""
with pytest.raises(FisherVectorException):
learn_gmm([np.zeros((5, 10)), np.zeros((4, 10))], n_modes='not_valid')
def test_gmm_wrong_num_modes_format_2():
"""Test that FisherVectorException is raised when a number that is not a
positive integer is passed into the n_modes argument of learn_gmm.
"""
with pytest.raises(FisherVectorException):
learn_gmm([np.zeros((5, 10)), np.zeros((4, 10))], n_modes=-1)
def test_gmm_wrong_covariance_type():
"""Test that FisherVectorException is raised when wrong covariance type is
passed in as a keyword argument.
"""
with pytest.raises(FisherVectorException):
learn_gmm(
np.random.random((10, 10)), n_modes=2, gm_args={'covariance_type': 'full'}
)
def test_gmm_correct_covariance_type():
"""Test that GMM estimation is successful when the correct covariance type
is passed in as a keyword argument.
"""
gmm = learn_gmm(
np.random.random((10, 10)), n_modes=2, gm_args={'covariance_type': 'diag'}
)
assert gmm.means_ is not None
assert gmm.covariances_ is not None
assert gmm.weights_ is not None
def test_gmm_e2e():
"""
Test the GMM estimation. Since this is essentially a wrapper for the
scikit-learn GaussianMixture class, the testing of the actual inner
workings of the GMM estimation is left to scikit-learn and its
dependencies.
We instead simply assert that the estimation was successful based on the
fact that the GMM object will have associated mixture weights, means, and
variances after estimation is successful/complete.
"""
gmm = learn_gmm(np.random.random((100, 64)), n_modes=5)
assert gmm.means_ is not None
assert gmm.covariances_ is not None
assert gmm.weights_ is not None
def test_fv_wrong_descriptor_types():
"""
Test that DescriptorException is raised when the incorrect type for the
descriptors is passed into the fisher_vector function.
"""
try:
from sklearn.mixture import GaussianMixture
except ImportError:
print(
'scikit-learn is not installed. Please ensure it is installed in '
'order to use the Fisher vector functionality.'
)
with pytest.raises(DescriptorException):
fisher_vector([[1, 2, 3, 4]], GaussianMixture())
def test_fv_wrong_gmm_type():
"""
Test that FisherVectorException is raised when a GMM not of type
sklearn.mixture.GaussianMixture is passed into the fisher_vector
function.
"""
class MyDifferentGaussianMixture:
pass
with pytest.raises(FisherVectorException):
fisher_vector(np.zeros((10, 10)), MyDifferentGaussianMixture())
def test_fv_e2e():
"""
Test the Fisher vector computation given a GMM returned from the learn_gmm
function. We simply assert that the dimensionality of the resulting Fisher
vector is correct.
The dimensionality of a Fisher vector is given by 2KD + K, where K is the
number of Gaussians specified in the associated GMM, and D is the
dimensionality of the descriptors using to estimate the GMM.
"""
dim = 128
num_modes = 8
expected_dim = 2 * num_modes * dim + num_modes
descriptors = [np.random.random((np.random.randint(5, 30), dim)) for _ in range(10)]
gmm = learn_gmm(descriptors, n_modes=num_modes)
fisher_vec = fisher_vector(descriptors[0], gmm)
assert len(fisher_vec) == expected_dim
def test_fv_e2e_improved():
"""
Test the improved Fisher vector computation given a GMM returned from the
learn_gmm function. We simply assert that the dimensionality of the
resulting Fisher vector is correct.
The dimensionality of a Fisher vector is given by 2KD + K, where K is the
number of Gaussians specified in the associated GMM, and D is the
dimensionality of the descriptors using to estimate the GMM.
"""
dim = 128
num_modes = 8
expected_dim = 2 * num_modes * dim + num_modes
descriptors = [np.random.random((np.random.randint(5, 30), dim)) for _ in range(10)]
gmm = learn_gmm(descriptors, n_modes=num_modes)
fisher_vec = fisher_vector(descriptors[0], gmm, improved=True)
assert len(fisher_vec) == expected_dim

View File

@@ -0,0 +1,182 @@
from random import shuffle
import pytest
import numpy as np
from numpy.testing import assert_allclose
from numpy.testing import assert_array_equal
from skimage.transform import integral_image
from skimage.feature import haar_like_feature
from skimage.feature import haar_like_feature_coord
from skimage.feature import draw_haar_like_feature
def test_haar_like_feature_error():
img = np.ones((5, 5), dtype=np.float32)
img_ii = integral_image(img)
feature_type = 'unknown_type'
with pytest.raises(ValueError):
haar_like_feature(img_ii, 0, 0, 5, 5, feature_type=feature_type)
haar_like_feature_coord(5, 5, feature_type=feature_type)
draw_haar_like_feature(img, 0, 0, 5, 5, feature_type=feature_type)
feat_coord, feat_type = haar_like_feature_coord(5, 5, 'type-2-x')
with pytest.raises(ValueError):
haar_like_feature(
img_ii, 0, 0, 5, 5, feature_type=feat_type[:3], feature_coord=feat_coord
)
@pytest.mark.parametrize("dtype", [np.uint8, np.int8, np.float32, np.float64])
@pytest.mark.parametrize(
"feature_type,shape_feature,expected_feature_value",
[
('type-2-x', (84,), [0.0]),
('type-2-y', (84,), [0.0]),
('type-3-x', (42,), [-5, -4.0, -3.0, -2.0, -1.0]),
('type-3-y', (42,), [-5, -4.0, -3.0, -2.0, -1.0]),
('type-4', (36,), [0.0]),
],
)
def test_haar_like_feature(feature_type, shape_feature, expected_feature_value, dtype):
# test Haar-like feature on a basic one image
img = np.ones((5, 5), dtype=dtype)
img_ii = integral_image(img)
haar_feature = haar_like_feature(img_ii, 0, 0, 5, 5, feature_type=feature_type)
assert_allclose(np.sort(np.unique(haar_feature)), expected_feature_value)
@pytest.mark.parametrize("dtype", [np.uint8, np.int8, np.float32, np.float64])
@pytest.mark.parametrize(
"feature_type", ['type-2-x', 'type-2-y', 'type-3-x', 'type-3-y', 'type-4']
)
def test_haar_like_feature_fused_type(dtype, feature_type):
# check that the input type is kept
img = np.ones((5, 5), dtype=dtype)
img_ii = integral_image(img)
expected_dtype = img_ii.dtype
# to avoid overflow, unsigned type are converted to signed
if 'uint' in expected_dtype.name:
expected_dtype = np.dtype(expected_dtype.name.replace('u', ''))
haar_feature = haar_like_feature(img_ii, 0, 0, 5, 5, feature_type=feature_type)
assert haar_feature.dtype == expected_dtype
def test_haar_like_feature_list():
img = np.ones((5, 5), dtype=np.int8)
img_ii = integral_image(img)
feature_type = ['type-2-x', 'type-2-y', 'type-3-x', 'type-3-y', 'type-4']
haar_list = haar_like_feature(img_ii, 0, 0, 5, 5, feature_type=feature_type)
haar_all = haar_like_feature(img_ii, 0, 0, 5, 5)
assert_array_equal(haar_list, haar_all)
@pytest.mark.parametrize(
"feature_type",
[
'type-2-x',
'type-2-y',
'type-3-x',
'type-3-y',
'type-4',
['type-2-y', 'type-3-x', 'type-4'],
],
)
def test_haar_like_feature_precomputed(feature_type):
img = np.ones((5, 5), dtype=np.int8)
img_ii = integral_image(img)
if isinstance(feature_type, list):
# shuffle the index of the feature to be sure that we are output
# the features in the same order
shuffle(feature_type)
feat_coord, feat_type = zip(
*[haar_like_feature_coord(5, 5, feat_t) for feat_t in feature_type]
)
feat_coord = np.concatenate(feat_coord)
feat_type = np.concatenate(feat_type)
else:
feat_coord, feat_type = haar_like_feature_coord(5, 5, feature_type)
haar_feature_precomputed = haar_like_feature(
img_ii, 0, 0, 5, 5, feature_type=feat_type, feature_coord=feat_coord
)
haar_feature = haar_like_feature(img_ii, 0, 0, 5, 5, feature_type)
assert_array_equal(haar_feature_precomputed, haar_feature)
@pytest.mark.parametrize(
"feature_type,height,width,expected_coord",
[
(
'type-2-x',
2,
2,
[
[[(0, 0), (0, 0)], [(0, 1), (0, 1)]],
[[(0, 0), (1, 0)], [(0, 1), (1, 1)]],
[[(1, 0), (1, 0)], [(1, 1), (1, 1)]],
],
),
(
'type-2-y',
2,
2,
[
[[(0, 0), (0, 0)], [(1, 0), (1, 0)]],
[[(0, 0), (0, 1)], [(1, 0), (1, 1)]],
[[(0, 1), (0, 1)], [(1, 1), (1, 1)]],
],
),
(
'type-3-x',
3,
3,
[
[[(0, 0), (0, 0)], [(0, 1), (0, 1)], [(0, 2), (0, 2)]],
[[(0, 0), (1, 0)], [(0, 1), (1, 1)], [(0, 2), (1, 2)]],
[[(0, 0), (2, 0)], [(0, 1), (2, 1)], [(0, 2), (2, 2)]],
[[(1, 0), (1, 0)], [(1, 1), (1, 1)], [(1, 2), (1, 2)]],
[[(1, 0), (2, 0)], [(1, 1), (2, 1)], [(1, 2), (2, 2)]],
[[(2, 0), (2, 0)], [(2, 1), (2, 1)], [(2, 2), (2, 2)]],
],
),
(
'type-3-y',
3,
3,
[
[[(0, 0), (0, 0)], [(1, 0), (1, 0)], [(2, 0), (2, 0)]],
[[(0, 0), (0, 1)], [(1, 0), (1, 1)], [(2, 0), (2, 1)]],
[[(0, 0), (0, 2)], [(1, 0), (1, 2)], [(2, 0), (2, 2)]],
[[(0, 1), (0, 1)], [(1, 1), (1, 1)], [(2, 1), (2, 1)]],
[[(0, 1), (0, 2)], [(1, 1), (1, 2)], [(2, 1), (2, 2)]],
[[(0, 2), (0, 2)], [(1, 2), (1, 2)], [(2, 2), (2, 2)]],
],
),
(
'type-4',
2,
2,
[[[(0, 0), (0, 0)], [(0, 1), (0, 1)], [(1, 1), (1, 1)], [(1, 0), (1, 0)]]],
),
],
)
def test_haar_like_feature_coord(feature_type, height, width, expected_coord):
feat_coord, feat_type = haar_like_feature_coord(width, height, feature_type)
# convert the output to a full numpy array just for comparison
feat_coord = np.array([hf for hf in feat_coord])
assert_array_equal(feat_coord, expected_coord)
assert np.all(feat_type == feature_type)
@pytest.mark.parametrize("max_n_features,nnz_values", [(None, 46), (1, 4)])
def test_draw_haar_like_feature(max_n_features, nnz_values):
img = np.zeros((5, 5), dtype=np.float32)
coord, _ = haar_like_feature_coord(5, 5, 'type-4')
image = draw_haar_like_feature(
img, 0, 0, 5, 5, coord, max_n_features=max_n_features, rng=0
)
draw_haar_like_feature(img, 0, 0, 5, 5, coord, max_n_features=max_n_features, rng=0)
assert image.shape == (5, 5, 3)
assert np.count_nonzero(image) == nnz_values

View File

@@ -0,0 +1,360 @@
import numpy as np
import pytest
from numpy.testing import assert_almost_equal
from skimage import color, data, draw, feature, img_as_float
from skimage._shared import filters
from skimage._shared.testing import fetch
from skimage._shared.utils import _supported_float_type
def test_hog_output_size():
img = img_as_float(data.astronaut()[:256, :].mean(axis=2))
fd = feature.hog(
img,
orientations=9,
pixels_per_cell=(8, 8),
cells_per_block=(1, 1),
block_norm='L1',
)
assert len(fd) == 9 * (256 // 8) * (512 // 8)
@pytest.mark.parametrize('dtype', [np.float32, np.float64])
def test_hog_output_correctness_l1_norm(dtype):
img = color.rgb2gray(data.astronaut()).astype(dtype=dtype, copy=False)
correct_output = np.load(fetch('data/astronaut_GRAY_hog_L1.npy'))
output = feature.hog(
img,
orientations=9,
pixels_per_cell=(8, 8),
cells_per_block=(3, 3),
block_norm='L1',
feature_vector=True,
transform_sqrt=False,
visualize=False,
)
float_dtype = _supported_float_type(dtype)
assert output.dtype == float_dtype
decimal = 7 if float_dtype == np.float64 else 5
assert_almost_equal(output, correct_output, decimal=decimal)
@pytest.mark.parametrize('dtype', [np.float32, np.float64])
def test_hog_output_correctness_l2hys_norm(dtype):
img = color.rgb2gray(data.astronaut()).astype(dtype=dtype, copy=False)
correct_output = np.load(fetch('data/astronaut_GRAY_hog_L2-Hys.npy'))
output = feature.hog(
img,
orientations=9,
pixels_per_cell=(8, 8),
cells_per_block=(3, 3),
block_norm='L2-Hys',
feature_vector=True,
transform_sqrt=False,
visualize=False,
)
float_dtype = _supported_float_type(dtype)
assert output.dtype == float_dtype
decimal = 7 if float_dtype == np.float64 else 5
assert_almost_equal(output, correct_output, decimal=decimal)
def test_hog_image_size_cell_size_mismatch():
image = data.camera()[:150, :200]
fd = feature.hog(
image,
orientations=9,
pixels_per_cell=(8, 8),
cells_per_block=(1, 1),
block_norm='L1',
)
assert len(fd) == 9 * (150 // 8) * (200 // 8)
def test_hog_odd_cell_size():
img = np.zeros((3, 3))
img[2, 2] = 1
correct_output = np.zeros((9,))
correct_output[0] = 0.5
correct_output[4] = 0.5
output = feature.hog(
img, pixels_per_cell=(3, 3), cells_per_block=(1, 1), block_norm='L1'
)
assert_almost_equal(output, correct_output, decimal=1)
def test_hog_basic_orientations_and_data_types():
# scenario:
# 1) create image (with float values) where upper half is filled by
# zeros, bottom half by 100
# 2) create unsigned integer version of this image
# 3) calculate feature.hog() for both images, both with 'transform_sqrt'
# option enabled and disabled
# 4) verify that all results are equal where expected
# 5) verify that computed feature vector is as expected
# 6) repeat the scenario for 90, 180 and 270 degrees rotated images
# size of testing image
width = height = 35
image0 = np.zeros((height, width), dtype='float')
image0[height // 2 :] = 100
for rot in range(4):
# rotate by 0, 90, 180 and 270 degrees
image_float = np.rot90(image0, rot)
# create uint8 image from image_float
image_uint8 = image_float.astype('uint8')
(hog_float, hog_img_float) = feature.hog(
image_float,
orientations=4,
pixels_per_cell=(8, 8),
cells_per_block=(1, 1),
visualize=True,
transform_sqrt=False,
block_norm='L1',
)
(hog_uint8, hog_img_uint8) = feature.hog(
image_uint8,
orientations=4,
pixels_per_cell=(8, 8),
cells_per_block=(1, 1),
visualize=True,
transform_sqrt=False,
block_norm='L1',
)
(hog_float_norm, hog_img_float_norm) = feature.hog(
image_float,
orientations=4,
pixels_per_cell=(8, 8),
cells_per_block=(1, 1),
visualize=True,
transform_sqrt=True,
block_norm='L1',
)
(hog_uint8_norm, hog_img_uint8_norm) = feature.hog(
image_uint8,
orientations=4,
pixels_per_cell=(8, 8),
cells_per_block=(1, 1),
visualize=True,
transform_sqrt=True,
block_norm='L1',
)
# set to True to enable manual debugging with graphical output,
# must be False for automatic testing
if False:
import matplotlib.pyplot as plt
plt.figure()
plt.subplot(2, 3, 1)
plt.imshow(image_float)
plt.colorbar()
plt.title('image')
plt.subplot(2, 3, 2)
plt.imshow(hog_img_float)
plt.colorbar()
plt.title('HOG result visualisation (float img)')
plt.subplot(2, 3, 5)
plt.imshow(hog_img_uint8)
plt.colorbar()
plt.title('HOG result visualisation (uint8 img)')
plt.subplot(2, 3, 3)
plt.imshow(hog_img_float_norm)
plt.colorbar()
plt.title('HOG result (transform_sqrt) visualisation (float img)')
plt.subplot(2, 3, 6)
plt.imshow(hog_img_uint8_norm)
plt.colorbar()
plt.title('HOG result (transform_sqrt) visualisation (uint8 img)')
plt.show()
# results (features and visualisation) for float and uint8 images must
# be almost equal
assert_almost_equal(hog_float, hog_uint8)
assert_almost_equal(hog_img_float, hog_img_uint8)
# resulting features should be almost equal
# when 'transform_sqrt' is enabled
# or disabled (for current simple testing image)
assert_almost_equal(hog_float, hog_float_norm, decimal=4)
assert_almost_equal(hog_float, hog_uint8_norm, decimal=4)
# reshape resulting feature vector to matrix with 4 columns (each
# corresponding to one of 4 directions); only one direction should
# contain nonzero values (this is manually determined for testing
# image)
actual = np.max(hog_float.reshape(-1, 4), axis=0)
if rot in [0, 2]:
# image is rotated by 0 and 180 degrees
desired = [0, 0, 1, 0]
elif rot in [1, 3]:
# image is rotated by 90 and 270 degrees
desired = [1, 0, 0, 0]
else:
raise Exception('Result is not determined for this rotation.')
assert_almost_equal(actual, desired, decimal=2)
def test_hog_orientations_circle():
# scenario:
# 1) create image with blurred circle in the middle
# 2) calculate feature.hog()
# 3) verify that the resulting feature vector contains uniformly
# distributed values for all orientations, i.e. no orientation is
# lost or emphasized
# 4) repeat the scenario for other 'orientations' option
# size of testing image
width = height = 100
image = np.zeros((height, width))
rr, cc = draw.disk((int(height / 2), int(width / 2)), int(width / 3))
image[rr, cc] = 100
image = filters.gaussian(image, sigma=2, mode='reflect')
for orientations in range(2, 15):
(hog, hog_img) = feature.hog(
image,
orientations=orientations,
pixels_per_cell=(8, 8),
cells_per_block=(1, 1),
visualize=True,
transform_sqrt=False,
block_norm='L1',
)
# set to True to enable manual debugging with graphical output,
# must be False for automatic testing
if False:
import matplotlib.pyplot as plt
plt.figure()
plt.subplot(1, 2, 1)
plt.imshow(image)
plt.colorbar()
plt.title('image_float')
plt.subplot(1, 2, 2)
plt.imshow(hog_img)
plt.colorbar()
plt.title('HOG result visualisation, ' f'orientations={orientations}')
plt.show()
# reshape resulting feature vector to matrix with N columns (each
# column corresponds to one direction),
hog_matrix = hog.reshape(-1, orientations)
# compute mean values in the resulting feature vector for each
# direction, these values should be almost equal to the global mean
# value (since the image contains a circle), i.e., all directions have
# same contribution to the result
actual = np.mean(hog_matrix, axis=0)
desired = np.mean(hog_matrix)
assert_almost_equal(actual, desired, decimal=1)
def test_hog_visualization_orientation():
"""Test that the visualization produces a line with correct orientation
The hog visualization is expected to draw line segments perpendicular to
the midpoints of orientation bins. This example verifies that when
orientations=3 and the gradient is entirely in the middle bin (bisected
by the y-axis), the line segment drawn by the visualization is horizontal.
"""
width = height = 11
image = np.zeros((height, width), dtype='float')
image[height // 2 :] = 1
_, hog_image = feature.hog(
image,
orientations=3,
pixels_per_cell=(width, height),
cells_per_block=(1, 1),
visualize=True,
block_norm='L1',
)
middle_index = height // 2
indices_excluding_middle = [x for x in range(height) if x != middle_index]
assert (hog_image[indices_excluding_middle, :] == 0).all()
assert (hog_image[middle_index, 1:-1] > 0).all()
def test_hog_block_normalization_incorrect_error():
img = np.eye(4)
with pytest.raises(ValueError):
feature.hog(img, block_norm='Linf')
@pytest.mark.parametrize(
"shape,channel_axis",
[
((3, 3, 3), None),
((3, 3), -1),
((3, 3, 3, 3), -1),
],
)
def test_hog_incorrect_dimensions(shape, channel_axis):
img = np.zeros(shape)
with pytest.raises(ValueError):
feature.hog(img, channel_axis=channel_axis, block_norm='L1')
def test_hog_output_equivariance_deprecated_multichannel():
img = data.astronaut()
img[:, :, (1, 2)] = 0
hog_ref = feature.hog(img, channel_axis=-1, block_norm='L1')
for n in (1, 2):
hog_fact = feature.hog(
np.roll(img, n, axis=2), channel_axis=-1, block_norm='L1'
)
assert_almost_equal(hog_ref, hog_fact)
@pytest.mark.parametrize('channel_axis', [0, 1, -1, -2])
def test_hog_output_equivariance_channel_axis(channel_axis):
img = data.astronaut()[:64, :32]
img[:, :, (1, 2)] = 0
img = np.moveaxis(img, -1, channel_axis)
hog_ref = feature.hog(img, channel_axis=channel_axis, block_norm='L1')
for n in (1, 2):
hog_fact = feature.hog(
np.roll(img, n, axis=channel_axis),
channel_axis=channel_axis,
block_norm='L1',
)
assert_almost_equal(hog_ref, hog_fact)
def test_hog_small_image():
"""Test that an exception is thrown whenever the input image is
too small for the given parameters.
"""
img = np.zeros((24, 24))
feature.hog(img, pixels_per_cell=(8, 8), cells_per_block=(3, 3))
img = np.zeros((23, 23))
with pytest.raises(ValueError, match=".*image is too small given"):
feature.hog(
img,
pixels_per_cell=(8, 8),
cells_per_block=(3, 3),
)

View File

@@ -0,0 +1,321 @@
import numpy as np
from skimage._shared.testing import assert_equal
from skimage import data
from skimage import transform
from skimage.color import rgb2gray
from skimage.feature import BRIEF, match_descriptors, corner_peaks, corner_harris
from skimage._shared import testing
def test_binary_descriptors_unequal_descriptor_sizes_error():
"""Sizes of descriptors of keypoints to be matched should be equal."""
descs1 = np.array([[True, True, False, True], [False, True, False, True]])
descs2 = np.array(
[[True, False, False, True, False], [False, True, True, True, False]]
)
with testing.raises(ValueError):
match_descriptors(descs1, descs2)
def test_binary_descriptors():
descs1 = np.array(
[[True, True, False, True, True], [False, True, False, True, True]]
)
descs2 = np.array(
[[True, False, False, True, False], [False, False, True, True, True]]
)
matches = match_descriptors(descs1, descs2)
assert_equal(matches, [[0, 0], [1, 1]])
def test_binary_descriptors_rotation_crosscheck_false():
"""Verify matched keypoints and their corresponding masks results between
image and its rotated version with the expected keypoint pairs with
cross_check disabled."""
img = data.astronaut()
img = rgb2gray(img)
tform = transform.SimilarityTransform(scale=1, rotation=0.15, translation=(0, 0))
rotated_img = transform.warp(img, tform, clip=False)
extractor = BRIEF(descriptor_size=512)
keypoints1 = corner_peaks(
corner_harris(img), min_distance=5, threshold_abs=0, threshold_rel=0.1
)
extractor.extract(img, keypoints1)
descriptors1 = extractor.descriptors
keypoints2 = corner_peaks(
corner_harris(rotated_img), min_distance=5, threshold_abs=0, threshold_rel=0.1
)
extractor.extract(rotated_img, keypoints2)
descriptors2 = extractor.descriptors
matches = match_descriptors(descriptors1, descriptors2, cross_check=False)
exp_matches1 = np.arange(47)
exp_matches2 = np.array(
[
0,
2,
1,
3,
4,
5,
7,
8,
14,
9,
11,
13,
23,
15,
16,
22,
17,
19,
37,
18,
24,
27,
30,
25,
26,
32,
28,
35,
37,
42,
29,
38,
33,
40,
36,
39,
10,
36,
43,
15,
35,
41,
6,
37,
32,
24,
8,
]
)
assert_equal(matches[:, 0], exp_matches1)
assert_equal(matches[:, 1], exp_matches2)
# minkowski takes a different code path, therefore we test it explicitly
matches = match_descriptors(
descriptors1, descriptors2, metric='minkowski', cross_check=False
)
assert_equal(matches[:, 0], exp_matches1)
assert_equal(matches[:, 1], exp_matches2)
# it also has an extra parameter
matches = match_descriptors(
descriptors1, descriptors2, metric='minkowski', p=4, cross_check=False
)
assert_equal(matches[:, 0], exp_matches1)
assert_equal(matches[:, 1], exp_matches2)
def test_binary_descriptors_rotation_crosscheck_true():
"""Verify matched keypoints and their corresponding masks results between
image and its rotated version with the expected keypoint pairs with
cross_check enabled."""
img = data.astronaut()
img = rgb2gray(img)
tform = transform.SimilarityTransform(scale=1, rotation=0.15, translation=(0, 0))
rotated_img = transform.warp(img, tform, clip=False)
extractor = BRIEF(descriptor_size=512)
keypoints1 = corner_peaks(
corner_harris(img), min_distance=5, threshold_abs=0, threshold_rel=0.1
)
extractor.extract(img, keypoints1)
descriptors1 = extractor.descriptors
keypoints2 = corner_peaks(
corner_harris(rotated_img), min_distance=5, threshold_abs=0, threshold_rel=0.1
)
extractor.extract(rotated_img, keypoints2)
descriptors2 = extractor.descriptors
matches = match_descriptors(descriptors1, descriptors2, cross_check=True)
exp_matches1 = np.array(
[
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
19,
20,
21,
22,
23,
24,
26,
27,
28,
29,
30,
31,
32,
33,
34,
38,
41,
42,
]
)
exp_matches2 = np.array(
[
0,
2,
1,
3,
4,
5,
7,
8,
14,
9,
11,
13,
23,
15,
16,
22,
17,
19,
18,
24,
27,
30,
25,
26,
28,
35,
37,
42,
29,
38,
33,
40,
36,
43,
41,
6,
]
)
assert_equal(matches[:, 0], exp_matches1)
assert_equal(matches[:, 1], exp_matches2)
def test_max_distance():
descs1 = np.zeros((10, 128))
descs2 = np.zeros((15, 128))
descs1[0, :] = 1
matches = match_descriptors(
descs1, descs2, metric='euclidean', max_distance=0.1, cross_check=False
)
assert len(matches) == 9
matches = match_descriptors(
descs1,
descs2,
metric='euclidean',
max_distance=np.sqrt(128.1),
cross_check=False,
)
assert len(matches) == 10
matches = match_descriptors(
descs1, descs2, metric='euclidean', max_distance=0.1, cross_check=True
)
assert_equal(matches, [[1, 0]])
matches = match_descriptors(
descs1,
descs2,
metric='euclidean',
max_distance=np.sqrt(128.1),
cross_check=True,
)
assert_equal(matches, [[1, 0]])
def test_max_ratio():
descs1 = 10 * np.arange(10)[:, None].astype(np.float32)
descs2 = 10 * np.arange(15)[:, None].astype(np.float32)
descs2[0] = 5.0
matches = match_descriptors(
descs1, descs2, metric='euclidean', max_ratio=1.0, cross_check=False
)
assert_equal(len(matches), 10)
matches = match_descriptors(
descs1, descs2, metric='euclidean', max_ratio=0.6, cross_check=False
)
assert_equal(len(matches), 10)
matches = match_descriptors(
descs1, descs2, metric='euclidean', max_ratio=0.5, cross_check=False
)
assert_equal(len(matches), 9)
descs1[0] = 7.5
matches = match_descriptors(
descs1, descs2, metric='euclidean', max_ratio=0.5, cross_check=False
)
assert_equal(len(matches), 9)
descs2 = 10 * np.arange(1)[:, None].astype(np.float32)
matches = match_descriptors(
descs1, descs2, metric='euclidean', max_ratio=1.0, cross_check=False
)
assert_equal(len(matches), 10)
matches = match_descriptors(
descs1, descs2, metric='euclidean', max_ratio=0.5, cross_check=False
)
assert_equal(len(matches), 10)
descs1 = 10 * np.arange(1)[:, None].astype(np.float32)
matches = match_descriptors(
descs1, descs2, metric='euclidean', max_ratio=1.0, cross_check=False
)
assert_equal(len(matches), 1)
matches = match_descriptors(
descs1, descs2, metric='euclidean', max_ratio=0.5, cross_check=False
)
assert_equal(len(matches), 1)

View File

@@ -0,0 +1,180 @@
import numpy as np
import pytest
from numpy.testing import assert_almost_equal, assert_equal
from skimage import data
from skimage._shared.testing import run_in_parallel, xfail, arch32
from skimage.feature import ORB
from skimage.util.dtype import _convert
img = data.coins()
@run_in_parallel()
@pytest.mark.parametrize('dtype', ['float32', 'float64', 'uint8', 'uint16', 'int64'])
def test_keypoints_orb_desired_no_of_keypoints(dtype):
_img = _convert(img, dtype)
detector_extractor = ORB(n_keypoints=10, fast_n=12, fast_threshold=0.20)
detector_extractor.detect(_img)
exp_rows = np.array(
[141.0, 108.0, 214.56, 131.0, 214.272, 67.0, 206.0, 177.0, 108.0, 141.0]
)
exp_cols = np.array(
[323.0, 328.0, 282.24, 292.0, 281.664, 85.0, 260.0, 284.0, 328.8, 267.0]
)
exp_scales = np.array([1, 1, 1.44, 1, 1.728, 1, 1, 1, 1.2, 1])
exp_orientations = np.array(
[
-53.97446153,
59.5055285,
-96.01885186,
-149.70789506,
-94.70171899,
-45.76429535,
-51.49752849,
113.57081195,
63.30428063,
-79.56091118,
]
)
exp_response = np.array(
[
1.01168357,
0.82934145,
0.67784179,
0.57176438,
0.56637459,
0.52248355,
0.43696175,
0.42992376,
0.37700486,
0.36126832,
]
)
if np.dtype(dtype) == np.float32:
assert detector_extractor.scales.dtype == np.float32
assert detector_extractor.responses.dtype == np.float32
assert detector_extractor.orientations.dtype == np.float32
else:
assert detector_extractor.scales.dtype == np.float64
assert detector_extractor.responses.dtype == np.float64
assert detector_extractor.orientations.dtype == np.float64
assert_almost_equal(exp_rows, detector_extractor.keypoints[:, 0])
assert_almost_equal(exp_cols, detector_extractor.keypoints[:, 1])
assert_almost_equal(exp_scales, detector_extractor.scales)
assert_almost_equal(exp_response, detector_extractor.responses, 5)
assert_almost_equal(
exp_orientations, np.rad2deg(detector_extractor.orientations), 4
)
detector_extractor.detect_and_extract(img)
assert_almost_equal(exp_rows, detector_extractor.keypoints[:, 0])
assert_almost_equal(exp_cols, detector_extractor.keypoints[:, 1])
@pytest.mark.parametrize('dtype', ['float32', 'float64', 'uint8', 'uint16', 'int64'])
def test_keypoints_orb_less_than_desired_no_of_keypoints(dtype):
_img = _convert(img, dtype)
detector_extractor = ORB(
n_keypoints=15, fast_n=12, fast_threshold=0.33, downscale=2, n_scales=2
)
detector_extractor.detect(_img)
exp_rows = np.array([108.0, 203.0, 140.0, 65.0, 58.0])
exp_cols = np.array([293.0, 267.0, 202.0, 130.0, 291.0])
exp_scales = np.array([1.0, 1.0, 1.0, 1.0, 1.0])
exp_orientations = np.array(
[151.93906, -56.90052, -79.46341, -59.42996, -158.26941]
)
exp_response = np.array([-0.1764169, 0.2652126, -0.0324343, 0.0400902, 0.2667641])
assert_almost_equal(exp_rows, detector_extractor.keypoints[:, 0])
assert_almost_equal(exp_cols, detector_extractor.keypoints[:, 1])
assert_almost_equal(exp_scales, detector_extractor.scales)
assert_almost_equal(exp_response, detector_extractor.responses)
assert_almost_equal(
exp_orientations, np.rad2deg(detector_extractor.orientations), 3
)
detector_extractor.detect_and_extract(img)
assert_almost_equal(exp_rows, detector_extractor.keypoints[:, 0])
assert_almost_equal(exp_cols, detector_extractor.keypoints[:, 1])
@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/2529'
),
)
def test_descriptor_orb():
detector_extractor = ORB(fast_n=12, fast_threshold=0.20)
exp_descriptors = np.array(
[
[0, 0, 0, 1, 0, 0, 0, 1, 0, 1],
[1, 1, 0, 1, 0, 0, 0, 1, 0, 1],
[1, 1, 0, 0, 1, 0, 0, 0, 1, 1],
[1, 1, 1, 0, 0, 0, 1, 1, 1, 0],
[0, 0, 0, 1, 0, 1, 1, 1, 1, 1],
[1, 0, 0, 1, 1, 0, 0, 0, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[1, 1, 1, 0, 1, 1, 1, 1, 0, 0],
[1, 1, 1, 1, 0, 0, 0, 1, 1, 1],
[0, 1, 1, 0, 0, 1, 1, 0, 1, 1],
[1, 1, 0, 0, 0, 0, 0, 0, 1, 1],
[1, 0, 0, 0, 0, 1, 0, 1, 1, 1],
[1, 0, 1, 1, 1, 0, 1, 0, 1, 0],
[0, 0, 1, 1, 0, 0, 0, 0, 1, 1],
[0, 1, 1, 0, 0, 0, 1, 0, 0, 1],
[0, 1, 1, 0, 0, 0, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[0, 0, 1, 1, 1, 1, 0, 1, 1, 0],
[0, 0, 1, 1, 1, 0, 1, 0, 0, 1],
[0, 1, 0, 0, 0, 0, 0, 0, 1, 0],
],
dtype=bool,
)
detector_extractor.detect(img)
detector_extractor.extract(
img,
detector_extractor.keypoints,
detector_extractor.scales,
detector_extractor.orientations,
)
assert_equal(exp_descriptors, detector_extractor.descriptors[100:120, 10:20])
detector_extractor.detect_and_extract(img)
assert_equal(exp_descriptors, detector_extractor.descriptors[100:120, 10:20])
keypoints_count = detector_extractor.keypoints.shape[0]
assert keypoints_count == detector_extractor.descriptors.shape[0]
assert keypoints_count == detector_extractor.orientations.shape[0]
assert keypoints_count == detector_extractor.responses.shape[0]
assert keypoints_count == detector_extractor.scales.shape[0]
def test_no_descriptors_extracted_orb():
img = np.ones((128, 128))
detector_extractor = ORB()
with pytest.raises(RuntimeError):
detector_extractor.detect_and_extract(img)
def test_img_too_small_orb():
img = data.brick()[:64, :64]
detector_extractor = ORB(downscale=2, n_scales=8)
detector_extractor.detect(img)
detector_extractor.detect_and_extract(img)

View File

@@ -0,0 +1,646 @@
import itertools
import numpy as np
import pytest
from numpy.testing import assert_array_almost_equal, assert_array_equal, assert_equal
from scipy import ndimage as ndi
from skimage._shared._warnings import expected_warnings
from skimage.feature import peak
np.random.seed(21)
class TestPeakLocalMax:
def test_trivial_case(self):
trivial = np.zeros((25, 25))
peak_indices = peak.peak_local_max(trivial, min_distance=1)
assert type(peak_indices) is np.ndarray
assert peak_indices.size == 0
def test_noisy_peaks(self):
peak_locations = [(7, 7), (7, 13), (13, 7), (13, 13)]
# image with noise of amplitude 0.8 and peaks of amplitude 1
image = 0.8 * np.random.rand(20, 20)
for r, c in peak_locations:
image[r, c] = 1
peaks_detected = peak.peak_local_max(image, min_distance=5)
assert len(peaks_detected) == len(peak_locations)
for loc in peaks_detected:
assert tuple(loc) in peak_locations
def test_relative_threshold(self):
image = np.zeros((5, 5), dtype=np.uint8)
image[1, 1] = 10
image[3, 3] = 20
peaks = peak.peak_local_max(image, min_distance=1, threshold_rel=0.5)
assert len(peaks) == 1
assert_array_almost_equal(peaks, [(3, 3)])
def test_absolute_threshold(self):
image = np.zeros((5, 5), dtype=np.uint8)
image[1, 1] = 10
image[3, 3] = 20
peaks = peak.peak_local_max(image, min_distance=1, threshold_abs=10)
assert len(peaks) == 1
assert_array_almost_equal(peaks, [(3, 3)])
def test_constant_image(self):
image = np.full((20, 20), 128, dtype=np.uint8)
peaks = peak.peak_local_max(image, min_distance=1)
assert len(peaks) == 0
def test_flat_peak(self):
image = np.zeros((5, 5), dtype=np.uint8)
image[1:3, 1:3] = 10
peaks = peak.peak_local_max(image, min_distance=1)
assert len(peaks) == 4
def test_sorted_peaks(self):
image = np.zeros((5, 5), dtype=np.uint8)
image[1, 1] = 20
image[3, 3] = 10
peaks = peak.peak_local_max(image, min_distance=1)
assert peaks.tolist() == [[1, 1], [3, 3]]
image = np.zeros((3, 10))
image[1, (1, 3, 5, 7)] = (1, 2, 3, 4)
peaks = peak.peak_local_max(image, min_distance=1)
assert peaks.tolist() == [[1, 7], [1, 5], [1, 3], [1, 1]]
def test_num_peaks(self):
image = np.zeros((7, 7), dtype=np.uint8)
image[1, 1] = 10
image[1, 3] = 11
image[1, 5] = 12
image[3, 5] = 8
image[5, 3] = 7
assert len(peak.peak_local_max(image, min_distance=1, threshold_abs=0)) == 5
peaks_limited = peak.peak_local_max(
image, min_distance=1, threshold_abs=0, num_peaks=2
)
assert len(peaks_limited) == 2
assert (1, 3) in peaks_limited
assert (1, 5) in peaks_limited
peaks_limited = peak.peak_local_max(
image, min_distance=1, threshold_abs=0, num_peaks=4
)
assert len(peaks_limited) == 4
assert (1, 3) in peaks_limited
assert (1, 5) in peaks_limited
assert (1, 1) in peaks_limited
assert (3, 5) in peaks_limited
def test_num_peaks_and_labels(self):
image = np.zeros((7, 7), dtype=np.uint8)
labels = np.zeros((7, 7), dtype=np.uint8) + 20
image[1, 1] = 10
image[1, 3] = 11
image[1, 5] = 12
image[3, 5] = 8
image[5, 3] = 7
peaks_limited = peak.peak_local_max(
image, min_distance=1, threshold_abs=0, labels=labels
)
assert len(peaks_limited) == 5
peaks_limited = peak.peak_local_max(
image, min_distance=1, threshold_abs=0, labels=labels, num_peaks=2
)
assert len(peaks_limited) == 2
def test_num_peaks_tot_vs_labels_4quadrants(self):
np.random.seed(21)
image = np.random.uniform(size=(20, 30))
i, j = np.mgrid[0:20, 0:30]
labels = 1 + (i >= 10) + (j >= 15) * 2
result = peak.peak_local_max(
image,
labels=labels,
min_distance=1,
threshold_rel=0,
num_peaks=np.inf,
num_peaks_per_label=2,
)
assert len(result) == 8
result = peak.peak_local_max(
image,
labels=labels,
min_distance=1,
threshold_rel=0,
num_peaks=np.inf,
num_peaks_per_label=1,
)
assert len(result) == 4
result = peak.peak_local_max(
image,
labels=labels,
min_distance=1,
threshold_rel=0,
num_peaks=2,
num_peaks_per_label=2,
)
assert len(result) == 2
def test_num_peaks3D(self):
# Issue 1354: the old code only hold for 2D arrays
# and this code would die with IndexError
image = np.zeros((10, 10, 100))
image[5, 5, ::5] = np.arange(20)
peaks_limited = peak.peak_local_max(image, min_distance=1, num_peaks=2)
assert len(peaks_limited) == 2
def test_reorder_labels(self):
image = np.random.uniform(size=(40, 60))
i, j = np.mgrid[0:40, 0:60]
labels = 1 + (i >= 20) + (j >= 30) * 2
labels[labels == 4] = 5
i, j = np.mgrid[-3:4, -3:4]
footprint = i * i + j * j <= 9
expected = np.zeros(image.shape, float)
for imin, imax in ((0, 20), (20, 40)):
for jmin, jmax in ((0, 30), (30, 60)):
expected[imin:imax, jmin:jmax] = ndi.maximum_filter(
image[imin:imax, jmin:jmax], footprint=footprint
)
expected = expected == image
peak_idx = peak.peak_local_max(
image,
labels=labels,
min_distance=1,
threshold_rel=0,
footprint=footprint,
exclude_border=False,
)
result = np.zeros_like(expected, dtype=bool)
result[tuple(peak_idx.T)] = True
assert (result == expected).all()
def test_indices_with_labels(self):
image = np.random.uniform(size=(40, 60))
i, j = np.mgrid[0:40, 0:60]
labels = 1 + (i >= 20) + (j >= 30) * 2
i, j = np.mgrid[-3:4, -3:4]
footprint = i * i + j * j <= 9
expected = np.zeros(image.shape, float)
for imin, imax in ((0, 20), (20, 40)):
for jmin, jmax in ((0, 30), (30, 60)):
expected[imin:imax, jmin:jmax] = ndi.maximum_filter(
image[imin:imax, jmin:jmax], footprint=footprint
)
expected = np.stack(np.nonzero(expected == image), axis=-1)
expected = expected[np.argsort(image[tuple(expected.T)])[::-1]]
result = peak.peak_local_max(
image,
labels=labels,
min_distance=1,
threshold_rel=0,
footprint=footprint,
exclude_border=False,
)
result = result[np.argsort(image[tuple(result.T)])[::-1]]
assert (result == expected).all()
def test_ndarray_exclude_border(self):
nd_image = np.zeros((5, 5, 5))
nd_image[[1, 0, 0], [0, 1, 0], [0, 0, 1]] = 1
nd_image[3, 0, 0] = 1
nd_image[2, 2, 2] = 1
expected = np.array([[2, 2, 2]], dtype=int)
expectedNoBorder = np.array([[0, 0, 1], [2, 2, 2], [3, 0, 0]], dtype=int)
result = peak.peak_local_max(nd_image, min_distance=2, exclude_border=2)
assert_array_equal(result, expected)
# Check that bools work as expected
assert_array_equal(
peak.peak_local_max(nd_image, min_distance=2, exclude_border=2),
peak.peak_local_max(nd_image, min_distance=2, exclude_border=True),
)
assert_array_equal(
peak.peak_local_max(nd_image, min_distance=2, exclude_border=0),
peak.peak_local_max(nd_image, min_distance=2, exclude_border=False),
)
# Check both versions with no border
result = peak.peak_local_max(nd_image, min_distance=2, exclude_border=0)
assert_array_equal(result, expectedNoBorder)
peak_idx = peak.peak_local_max(nd_image, exclude_border=False)
result = np.zeros_like(nd_image, dtype=bool)
result[tuple(peak_idx.T)] = True
assert_array_equal(result, nd_image.astype(bool))
def test_empty(self):
image = np.zeros((10, 20))
labels = np.zeros((10, 20), int)
result = peak.peak_local_max(
image,
labels=labels,
footprint=np.ones((3, 3), bool),
min_distance=1,
threshold_rel=0,
exclude_border=False,
)
assert result.shape == (0, image.ndim)
def test_empty_non2d_indices(self):
image = np.zeros((10, 10, 10))
result = peak.peak_local_max(
image,
footprint=np.ones((3, 3, 3), bool),
min_distance=1,
threshold_rel=0,
exclude_border=False,
)
assert result.shape == (0, image.ndim)
def test_one_point(self):
image = np.zeros((10, 20))
labels = np.zeros((10, 20), int)
image[5, 5] = 1
labels[5, 5] = 1
peak_idx = peak.peak_local_max(
image,
labels=labels,
footprint=np.ones((3, 3), bool),
min_distance=1,
threshold_rel=0,
exclude_border=False,
)
result = np.zeros_like(image, dtype=bool)
result[tuple(peak_idx.T)] = True
assert np.all(result == (labels == 1))
def test_adjacent_and_same(self):
image = np.zeros((10, 20))
labels = np.zeros((10, 20), int)
image[5, 5:6] = 1
labels[5, 5:6] = 1
expected = np.stack(np.where(labels == 1), axis=-1)
result = peak.peak_local_max(
image,
labels=labels,
footprint=np.ones((3, 3), bool),
min_distance=1,
threshold_rel=0,
exclude_border=False,
)
assert_array_equal(result, expected)
def test_adjacent_and_different(self):
image = np.zeros((10, 20))
labels = np.zeros((10, 20), int)
image[5, 5] = 1
image[5, 6] = 0.5
labels[5, 5:6] = 1
expected = np.stack(np.where(image == 1), axis=-1)
result = peak.peak_local_max(
image,
labels=labels,
footprint=np.ones((3, 3), bool),
min_distance=1,
threshold_rel=0,
exclude_border=False,
)
assert_array_equal(result, expected)
result = peak.peak_local_max(
image, labels=labels, min_distance=1, threshold_rel=0, exclude_border=False
)
assert_array_equal(result, expected)
def test_not_adjacent_and_different(self):
image = np.zeros((10, 20))
labels = np.zeros((10, 20), int)
image[5, 5] = 1
image[5, 8] = 0.5
labels[image > 0] = 1
expected = np.stack(np.where(labels == 1), axis=-1)
result = peak.peak_local_max(
image,
labels=labels,
footprint=np.ones((3, 3), bool),
min_distance=1,
threshold_rel=0,
exclude_border=False,
)
assert_array_equal(result, expected)
def test_two_objects(self):
image = np.zeros((10, 20))
labels = np.zeros((10, 20), int)
image[5, 5] = 1
image[5, 15] = 0.5
labels[5, 5] = 1
labels[5, 15] = 2
expected = np.stack(np.where(labels > 0), axis=-1)
result = peak.peak_local_max(
image,
labels=labels,
footprint=np.ones((3, 3), bool),
min_distance=1,
threshold_rel=0,
exclude_border=False,
)
assert_array_equal(result, expected)
def test_adjacent_different_objects(self):
image = np.zeros((10, 20))
labels = np.zeros((10, 20), int)
image[5, 5] = 1
image[5, 6] = 0.5
labels[5, 5] = 1
labels[5, 6] = 2
expected = np.stack(np.where(labels > 0), axis=-1)
result = peak.peak_local_max(
image,
labels=labels,
footprint=np.ones((3, 3), bool),
min_distance=1,
threshold_rel=0,
exclude_border=False,
)
assert_array_equal(result, expected)
def test_four_quadrants(self):
image = np.random.uniform(size=(20, 30))
i, j = np.mgrid[0:20, 0:30]
labels = 1 + (i >= 10) + (j >= 15) * 2
i, j = np.mgrid[-3:4, -3:4]
footprint = i * i + j * j <= 9
expected = np.zeros(image.shape, float)
for imin, imax in ((0, 10), (10, 20)):
for jmin, jmax in ((0, 15), (15, 30)):
expected[imin:imax, jmin:jmax] = ndi.maximum_filter(
image[imin:imax, jmin:jmax], footprint=footprint
)
expected = expected == image
peak_idx = peak.peak_local_max(
image,
labels=labels,
footprint=footprint,
min_distance=1,
threshold_rel=0,
exclude_border=False,
)
result = np.zeros_like(image, dtype=bool)
result[tuple(peak_idx.T)] = True
assert np.all(result == expected)
def test_disk(self):
'''regression test of img-1194, footprint = [1]
Test peak.peak_local_max when every point is a local maximum
'''
image = np.random.uniform(size=(10, 20))
footprint = np.array([[1]])
peak_idx = peak.peak_local_max(
image,
labels=np.ones((10, 20), int),
footprint=footprint,
min_distance=1,
threshold_rel=0,
threshold_abs=-1,
exclude_border=False,
)
result = np.zeros_like(image, dtype=bool)
result[tuple(peak_idx.T)] = True
assert np.all(result)
peak_idx = peak.peak_local_max(
image, footprint=footprint, threshold_abs=-1, exclude_border=False
)
result = np.zeros_like(image, dtype=bool)
result[tuple(peak_idx.T)] = True
assert np.all(result)
def test_3D(self):
image = np.zeros((30, 30, 30))
image[15, 15, 15] = 1
image[5, 5, 5] = 1
assert_array_equal(
peak.peak_local_max(image, min_distance=10, threshold_rel=0), [[15, 15, 15]]
)
assert_array_equal(
peak.peak_local_max(image, min_distance=6, threshold_rel=0), [[15, 15, 15]]
)
assert sorted(
peak.peak_local_max(
image, min_distance=10, threshold_rel=0, exclude_border=False
).tolist()
) == [[5, 5, 5], [15, 15, 15]]
assert sorted(
peak.peak_local_max(image, min_distance=5, threshold_rel=0).tolist()
) == [[5, 5, 5], [15, 15, 15]]
def test_4D(self):
image = np.zeros((30, 30, 30, 30))
image[15, 15, 15, 15] = 1
image[5, 5, 5, 5] = 1
assert_array_equal(
peak.peak_local_max(image, min_distance=10, threshold_rel=0),
[[15, 15, 15, 15]],
)
assert_array_equal(
peak.peak_local_max(image, min_distance=6, threshold_rel=0),
[[15, 15, 15, 15]],
)
assert sorted(
peak.peak_local_max(
image, min_distance=10, threshold_rel=0, exclude_border=False
).tolist()
) == [[5, 5, 5, 5], [15, 15, 15, 15]]
assert sorted(
peak.peak_local_max(image, min_distance=5, threshold_rel=0).tolist()
) == [[5, 5, 5, 5], [15, 15, 15, 15]]
def test_threshold_rel_default(self):
image = np.ones((5, 5))
image[2, 2] = 1
assert len(peak.peak_local_max(image)) == 0
image[2, 2] = 2
assert_array_equal(peak.peak_local_max(image), [[2, 2]])
image[2, 2] = 0
with expected_warnings(["When min_distance < 1"]):
assert len(peak.peak_local_max(image, min_distance=0)) == image.size - 1
def test_peak_at_border(self):
image = np.full((10, 10), -2)
image[2, 4] = -1
image[3, 0] = -1
peaks = peak.peak_local_max(image, min_distance=3)
assert peaks.size == 0
peaks = peak.peak_local_max(image, min_distance=3, exclude_border=0)
assert len(peaks) == 2
assert [2, 4] in peaks
assert [3, 0] in peaks
@pytest.mark.parametrize(
["indices"],
[[indices] for indices in itertools.product(range(5), range(5))],
)
def test_exclude_border(indices):
image = np.zeros((5, 5))
image[indices] = 1
# exclude_border = False, means it will always be found.
assert len(peak.peak_local_max(image, exclude_border=False)) == 1
# exclude_border = 0, means it will always be found.
assert len(peak.peak_local_max(image, exclude_border=0)) == 1
# exclude_border = True, min_distance=1 means it will be found unless it's
# on the edge.
if indices[0] in (0, 4) or indices[1] in (0, 4):
expected_peaks = 0
else:
expected_peaks = 1
assert (
len(peak.peak_local_max(image, min_distance=1, exclude_border=True))
== expected_peaks
)
# exclude_border = (1, 0) means it will be found unless it's on the edge of
# the first dimension.
if indices[0] in (0, 4):
expected_peaks = 0
else:
expected_peaks = 1
assert len(peak.peak_local_max(image, exclude_border=(1, 0))) == expected_peaks
# exclude_border = (0, 1) means it will be found unless it's on the edge of
# the second dimension.
if indices[1] in (0, 4):
expected_peaks = 0
else:
expected_peaks = 1
assert len(peak.peak_local_max(image, exclude_border=(0, 1))) == expected_peaks
def test_exclude_border_errors():
image = np.zeros((5, 5))
# exclude_border doesn't have the right cardinality.
with pytest.raises(ValueError):
assert peak.peak_local_max(image, exclude_border=(1,))
# exclude_border doesn't have the right type
with pytest.raises(TypeError):
assert peak.peak_local_max(image, exclude_border=1.0)
# exclude_border is a tuple of the right cardinality but contains
# non-integer values.
with pytest.raises(ValueError):
assert peak.peak_local_max(image, exclude_border=(1, 'a'))
# exclude_border is a tuple of the right cardinality but contains a
# negative value.
with pytest.raises(ValueError):
assert peak.peak_local_max(image, exclude_border=(1, -1))
# exclude_border is a negative value.
with pytest.raises(ValueError):
assert peak.peak_local_max(image, exclude_border=-1)
def test_input_values_with_labels():
# Issue #5235: input values may be modified when labels are used
img = np.random.rand(128, 128)
labels = np.zeros((128, 128), int)
labels[10:20, 10:20] = 1
labels[12:16, 12:16] = 0
img_before = img.copy()
_ = peak.peak_local_max(img, labels=labels)
assert_array_equal(img, img_before)
class TestProminentPeaks:
def test_isolated_peaks(self):
image = np.zeros((15, 15))
x0, y0, i0 = (12, 8, 1)
x1, y1, i1 = (2, 2, 1)
x2, y2, i2 = (5, 13, 1)
image[y0, x0] = i0
image[y1, x1] = i1
image[y2, x2] = i2
out = peak._prominent_peaks(image)
assert len(out[0]) == 3
for i, x, y in zip(out[0], out[1], out[2]):
assert i in (i0, i1, i2)
assert x in (x0, x1, x2)
assert y in (y0, y1, y2)
def test_threshold(self):
image = np.zeros((15, 15))
x0, y0, i0 = (12, 8, 10)
x1, y1, i1 = (2, 2, 8)
x2, y2, i2 = (5, 13, 10)
image[y0, x0] = i0
image[y1, x1] = i1
image[y2, x2] = i2
out = peak._prominent_peaks(image, threshold=None)
assert len(out[0]) == 3
for i, x, y in zip(out[0], out[1], out[2]):
assert i in (i0, i1, i2)
assert x in (x0, x1, x2)
out = peak._prominent_peaks(image, threshold=9)
assert len(out[0]) == 2
for i, x, y in zip(out[0], out[1], out[2]):
assert i in (i0, i2)
assert x in (x0, x2)
assert y in (y0, y2)
def test_peaks_in_contact(self):
image = np.zeros((15, 15))
x0, y0, i0 = (8, 8, 1)
x1, y1, i1 = (7, 7, 1) # prominent peak
x2, y2, i2 = (6, 6, 1)
image[y0, x0] = i0
image[y1, x1] = i1
image[y2, x2] = i2
out = peak._prominent_peaks(
image,
min_xdistance=3,
min_ydistance=3,
)
assert_equal(out[0], np.array((i1,)))
assert_equal(out[1], np.array((x1,)))
assert_equal(out[2], np.array((y1,)))
def test_input_labels_unmodified(self):
image = np.zeros((10, 20))
labels = np.zeros((10, 20), int)
image[5, 5] = 1
labels[5, 5] = 3
labelsin = labels.copy()
peak.peak_local_max(
image,
labels=labels,
footprint=np.ones((3, 3), bool),
min_distance=1,
threshold_rel=0,
exclude_border=False,
)
assert np.all(labels == labelsin)
def test_many_objects(self):
mask = np.zeros([500, 500], dtype=bool)
x, y = np.indices((500, 500))
x_c = x // 20 * 20 + 10
y_c = y // 20 * 20 + 10
mask[(x - x_c) ** 2 + (y - y_c) ** 2 < 8**2] = True
labels, num_objs = ndi.label(mask)
dist = ndi.distance_transform_edt(mask)
local_max = peak.peak_local_max(
dist, min_distance=20, exclude_border=False, labels=labels
)
assert len(local_max) == 625

View File

@@ -0,0 +1,179 @@
import numpy as np
import pytest
from numpy.testing import assert_almost_equal, assert_equal
from skimage import data
from skimage._shared.testing import run_in_parallel
from skimage.feature import SIFT
from skimage.util.dtype import _convert
img = data.coins()
@run_in_parallel()
@pytest.mark.parametrize('dtype', ['float32', 'float64', 'uint8', 'uint16', 'int64'])
def test_keypoints_sift(dtype):
_img = _convert(img, dtype)
detector_extractor = SIFT()
detector_extractor.detect_and_extract(_img)
exp_keypoint_rows = np.array([18, 18, 19, 22, 26, 26, 30, 31, 31, 32])
exp_keypoint_cols = np.array([331, 331, 325, 330, 310, 330, 205, 323, 149, 338])
exp_octaves = np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
exp_position_rows = np.array(
[
17.81909936,
17.81909936,
19.05454661,
21.85933727,
25.54800708,
26.25710504,
29.90826307,
30.78713806,
30.87953572,
31.72557969,
]
)
exp_position_cols = np.array(
[
331.49693187,
331.49693187,
325.24476016,
330.44616424,
310.33932904,
330.46155224,
204.74535177,
322.84100812,
149.43192282,
337.89643013,
]
)
exp_orientations = np.array(
[
0.26391655,
0.26391655,
0.39134262,
1.77063053,
0.98637565,
1.37997279,
0.4919992,
1.48615988,
0.33753212,
1.64859617,
]
)
exp_scales = np.array([2, 2, 1, 3, 3, 1, 2, 1, 1, 1])
exp_sigmas = np.array(
[
1.35160379,
1.35160379,
0.94551567,
1.52377498,
1.55173233,
0.93973722,
1.37594124,
1.06663786,
1.04827034,
1.0378916,
]
)
exp_scalespace_sigmas = np.array(
[
[0.8, 1.00793684, 1.26992084, 1.6, 2.01587368, 2.53984168],
[1.6, 2.01587368, 2.53984168, 3.2, 4.03174736, 5.07968337],
[3.2, 4.03174736, 5.07968337, 6.4, 8.06349472, 10.15936673],
[6.4, 8.06349472, 10.15936673, 12.8, 16.12698944, 20.31873347],
[12.8, 16.12698944, 20.31873347, 25.6, 32.25397888, 40.63746693],
[25.6, 32.25397888, 40.63746693, 51.2, 64.50795775, 81.27493386],
]
)
assert_almost_equal(exp_keypoint_rows, detector_extractor.keypoints[:10, 0])
assert_almost_equal(exp_keypoint_cols, detector_extractor.keypoints[:10, 1])
assert_almost_equal(exp_octaves, detector_extractor.octaves[:10])
assert_almost_equal(
exp_position_rows, detector_extractor.positions[:10, 0], decimal=4
)
assert_almost_equal(
exp_position_cols, detector_extractor.positions[:10, 1], decimal=4
)
assert_almost_equal(
exp_orientations, detector_extractor.orientations[:10], decimal=4
)
assert_almost_equal(exp_scales, detector_extractor.scales[:10])
assert_almost_equal(exp_sigmas, detector_extractor.sigmas[:10], decimal=4)
assert_almost_equal(
exp_scalespace_sigmas, detector_extractor.scalespace_sigmas, decimal=4
)
detector_extractor2 = SIFT()
detector_extractor2.detect(img)
detector_extractor2.extract(img)
assert_almost_equal(
detector_extractor.keypoints[:10, 0], detector_extractor2.keypoints[:10, 0]
)
assert_almost_equal(
detector_extractor.keypoints[:10, 0], detector_extractor2.keypoints[:10, 0]
)
def test_descriptor_sift():
detector_extractor = SIFT(n_hist=2, n_ori=4)
exp_descriptors = np.array(
[
[173, 30, 55, 32, 173, 16, 45, 82, 173, 154, 170, 173, 173, 169, 65, 110],
[173, 30, 55, 32, 173, 16, 45, 82, 173, 154, 170, 173, 173, 169, 65, 110],
[189, 52, 18, 18, 189, 11, 21, 55, 189, 75, 173, 91, 189, 65, 189, 162],
[172, 156, 185, 66, 92, 76, 78, 185, 185, 87, 88, 82, 98, 56, 96, 185],
[216, 19, 40, 9, 196, 7, 57, 36, 216, 56, 158, 29, 216, 42, 144, 154],
[169, 120, 169, 91, 129, 108, 169, 67, 169, 142, 111, 95, 169, 120, 69, 41],
[199, 10, 138, 44, 178, 11, 161, 34, 199, 113, 73, 64, 199, 82, 31, 178],
[154, 56, 154, 49, 144, 154, 154, 78, 154, 51, 154, 83, 154, 154, 154, 72],
[230, 46, 47, 21, 230, 15, 65, 95, 230, 52, 72, 51, 230, 19, 59, 130],
[
155,
117,
154,
102,
155,
155,
90,
110,
145,
127,
155,
50,
57,
155,
155,
70,
],
],
dtype=np.uint8,
)
detector_extractor.detect_and_extract(img)
assert_equal(exp_descriptors, detector_extractor.descriptors[:10])
keypoints_count = detector_extractor.keypoints.shape[0]
assert keypoints_count == detector_extractor.descriptors.shape[0]
assert keypoints_count == detector_extractor.orientations.shape[0]
assert keypoints_count == detector_extractor.octaves.shape[0]
assert keypoints_count == detector_extractor.positions.shape[0]
assert keypoints_count == detector_extractor.scales.shape[0]
assert keypoints_count == detector_extractor.scales.shape[0]
def test_no_descriptors_extracted_sift():
img = np.ones((128, 128))
detector_extractor = SIFT()
with pytest.raises(RuntimeError):
detector_extractor.detect_and_extract(img)

View File

@@ -0,0 +1,189 @@
import numpy as np
from skimage._shared.testing import assert_almost_equal, assert_equal
from skimage import data, img_as_float
from skimage.morphology import diamond
from skimage.feature import match_template, peak_local_max
from skimage._shared import testing
@testing.parametrize('dtype', [np.float32, np.float64])
def test_template(dtype):
size = 100
# Float prefactors ensure that image range is between 0 and 1
image = np.full((400, 400), 0.5, dtype=dtype)
target = 0.1 * (np.tri(size) + np.tri(size)[::-1])
target = target.astype(dtype, copy=False)
target_positions = [(50, 50), (200, 200)]
for x, y in target_positions:
image[x : x + size, y : y + size] = target
np.random.seed(1)
image += 0.1 * np.random.uniform(size=(400, 400)).astype(dtype, copy=False)
result = match_template(image, target)
assert result.dtype == dtype
delta = 5
positions = peak_local_max(result, min_distance=delta)
if len(positions) > 2:
# Keep the two maximum peaks.
intensities = result[tuple(positions.T)]
i_maxsort = np.argsort(intensities)[::-1]
positions = positions[i_maxsort][:2]
# Sort so that order matches `target_positions`.
positions = positions[np.argsort(positions[:, 0])]
for xy_target, xy in zip(target_positions, positions):
assert_almost_equal(xy, xy_target)
def test_normalization():
"""Test that `match_template` gives the correct normalization.
Normalization gives 1 for a perfect match and -1 for an inverted-match.
This test adds positive and negative squares to a zero-array and matches
the array with a positive template.
"""
n = 5
N = 20
ipos, jpos = (2, 3)
ineg, jneg = (12, 11)
image = np.full((N, N), 0.5)
image[ipos : ipos + n, jpos : jpos + n] = 1
image[ineg : ineg + n, jneg : jneg + n] = 0
# white square with a black border
template = np.zeros((n + 2, n + 2))
template[1 : 1 + n, 1 : 1 + n] = 1
result = match_template(image, template)
# get the max and min results.
sorted_result = np.argsort(result.flat)
iflat_min = sorted_result[0]
iflat_max = sorted_result[-1]
min_result = np.unravel_index(iflat_min, result.shape)
max_result = np.unravel_index(iflat_max, result.shape)
# shift result by 1 because of template border
assert np.all((np.array(min_result) + 1) == (ineg, jneg))
assert np.all((np.array(max_result) + 1) == (ipos, jpos))
assert np.allclose(result.flat[iflat_min], -1)
assert np.allclose(result.flat[iflat_max], 1)
def test_no_nans():
"""Test that `match_template` doesn't return NaN values.
When image values are only slightly different, floating-point errors can
cause a subtraction inside of a square root to go negative (without an
explicit check that was added to `match_template`).
"""
np.random.seed(1)
image = 0.5 + 1e-9 * np.random.normal(size=(20, 20))
template = np.ones((6, 6))
template[:3, :] = 0
result = match_template(image, template)
assert not np.any(np.isnan(result))
def test_switched_arguments():
image = np.ones((5, 5))
template = np.ones((3, 3))
with testing.raises(ValueError):
match_template(template, image)
def test_pad_input():
"""Test `match_template` when `pad_input=True`.
This test places two full templates (one with values lower than the image
mean, the other higher) and two half templates, which are on the edges of
the image. The two full templates should score the top (positive and
negative) matches and the centers of the half templates should score 2nd.
"""
# Float prefactors ensure that image range is between 0 and 1
template = 0.5 * diamond(2)
image = 0.5 * np.ones((9, 19))
mid = slice(2, 7)
image[mid, :3] -= template[:, -3:] # half min template centered at 0
image[mid, 4:9] += template # full max template centered at 6
image[mid, -9:-4] -= template # full min template centered at 12
image[mid, -3:] += template[:, :3] # half max template centered at 18
result = match_template(
image, template, pad_input=True, constant_values=image.mean()
)
# get the max and min results.
sorted_result = np.argsort(result.flat)
i, j = np.unravel_index(sorted_result[:2], result.shape)
assert_equal(j, (12, 0))
i, j = np.unravel_index(sorted_result[-2:], result.shape)
assert_equal(j, (18, 6))
def test_3d():
np.random.seed(1)
template = np.random.rand(3, 3, 3)
image = np.zeros((12, 12, 12))
image[3:6, 5:8, 4:7] = template
result = match_template(image, template)
assert_equal(result.shape, (10, 10, 10))
assert_equal(np.unravel_index(result.argmax(), result.shape), (3, 5, 4))
def test_3d_pad_input():
np.random.seed(1)
template = np.random.rand(3, 3, 3)
image = np.zeros((12, 12, 12))
image[3:6, 5:8, 4:7] = template
result = match_template(image, template, pad_input=True)
assert_equal(result.shape, (12, 12, 12))
assert_equal(np.unravel_index(result.argmax(), result.shape), (4, 6, 5))
def test_padding_reflect():
template = diamond(2)
image = np.zeros((10, 10))
image[2:7, :3] = template[:, -3:]
result = match_template(image, template, pad_input=True, mode='reflect')
assert_equal(np.unravel_index(result.argmax(), result.shape), (4, 0))
def test_wrong_input():
image = np.ones((5, 5, 1))
template = np.ones((3, 3))
with testing.raises(ValueError):
match_template(template, image)
image = np.ones((5, 5))
template = np.ones((3, 3, 2))
with testing.raises(ValueError):
match_template(template, image)
image = np.ones((5, 5, 3, 3))
template = np.ones((3, 3, 2))
with testing.raises(ValueError):
match_template(template, image)
def test_bounding_values():
image = img_as_float(data.page())
template = np.zeros((3, 3))
template[1, 1] = 1
result = match_template(image, template)
print(result.max())
assert result.max() < 1 + 1e-7
assert result.min() > -1 - 1e-7

View File

@@ -0,0 +1,328 @@
import numpy as np
import pytest
from skimage._shared.testing import expected_warnings, run_in_parallel
from skimage.feature import (
graycomatrix,
graycoprops,
local_binary_pattern,
multiblock_lbp,
)
from skimage.transform import integral_image
class TestGLCM:
def setup_method(self):
self.image = np.array(
[[0, 0, 1, 1], [0, 0, 1, 1], [0, 2, 2, 2], [2, 2, 3, 3]], dtype=np.uint8
)
@run_in_parallel()
def test_output_angles(self):
result = graycomatrix(
self.image, [1], [0, np.pi / 4, np.pi / 2, 3 * np.pi / 4], 4
)
assert result.shape == (4, 4, 1, 4)
expected1 = np.array(
[[2, 2, 1, 0], [0, 2, 0, 0], [0, 0, 3, 1], [0, 0, 0, 1]], dtype=np.uint32
)
np.testing.assert_array_equal(result[:, :, 0, 0], expected1)
expected2 = np.array(
[[1, 1, 3, 0], [0, 1, 1, 0], [0, 0, 0, 2], [0, 0, 0, 0]], dtype=np.uint32
)
np.testing.assert_array_equal(result[:, :, 0, 1], expected2)
expected3 = np.array(
[[3, 0, 2, 0], [0, 2, 2, 0], [0, 0, 1, 2], [0, 0, 0, 0]], dtype=np.uint32
)
np.testing.assert_array_equal(result[:, :, 0, 2], expected3)
expected4 = np.array(
[[2, 0, 0, 0], [1, 1, 2, 0], [0, 0, 2, 1], [0, 0, 0, 0]], dtype=np.uint32
)
np.testing.assert_array_equal(result[:, :, 0, 3], expected4)
def test_output_symmetric_1(self):
result = graycomatrix(self.image, [1], [np.pi / 2], 4, symmetric=True)
assert result.shape == (4, 4, 1, 1)
expected = np.array(
[[6, 0, 2, 0], [0, 4, 2, 0], [2, 2, 2, 2], [0, 0, 2, 0]], dtype=np.uint32
)
np.testing.assert_array_equal(result[:, :, 0, 0], expected)
def test_error_raise_float(self):
for dtype in [float, np.double, np.float16, np.float32, np.float64]:
with pytest.raises(ValueError):
graycomatrix(self.image.astype(dtype), [1], [np.pi], 4)
def test_error_raise_int_types(self):
for dtype in [np.int16, np.int32, np.int64, np.uint16, np.uint32, np.uint64]:
with pytest.raises(ValueError):
graycomatrix(self.image.astype(dtype), [1], [np.pi])
def test_error_raise_negative(self):
with pytest.raises(ValueError):
graycomatrix(self.image.astype(np.int16) - 1, [1], [np.pi], 4)
def test_error_raise_levels_smaller_max(self):
with pytest.raises(ValueError):
graycomatrix(self.image - 1, [1], [np.pi], 3)
def test_image_data_types(self):
for dtype in [np.uint16, np.uint32, np.uint64, np.int16, np.int32, np.int64]:
img = self.image.astype(dtype)
result = graycomatrix(img, [1], [np.pi / 2], 4, symmetric=True)
assert result.shape == (4, 4, 1, 1)
expected = np.array(
[[6, 0, 2, 0], [0, 4, 2, 0], [2, 2, 2, 2], [0, 0, 2, 0]],
dtype=np.uint32,
)
np.testing.assert_array_equal(result[:, :, 0, 0], expected)
return
def test_output_distance(self):
im = np.array(
[[0, 0, 0, 0], [1, 0, 0, 1], [2, 0, 0, 2], [3, 0, 0, 3]], dtype=np.uint8
)
result = graycomatrix(im, [3], [0], 4, symmetric=False)
expected = np.array(
[[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]], dtype=np.uint32
)
np.testing.assert_array_equal(result[:, :, 0, 0], expected)
def test_output_combo(self):
im = np.array([[0], [1], [2], [3]], dtype=np.uint8)
result = graycomatrix(im, [1, 2], [0, np.pi / 2], 4)
assert result.shape == (4, 4, 2, 2)
z = np.zeros((4, 4), dtype=np.uint32)
e1 = np.array(
[[0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1], [0, 0, 0, 0]], dtype=np.uint32
)
e2 = np.array(
[[0, 0, 1, 0], [0, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0]], dtype=np.uint32
)
np.testing.assert_array_equal(result[:, :, 0, 0], z)
np.testing.assert_array_equal(result[:, :, 1, 0], z)
np.testing.assert_array_equal(result[:, :, 0, 1], e1)
np.testing.assert_array_equal(result[:, :, 1, 1], e2)
def test_output_empty(self):
result = graycomatrix(self.image, [10], [0], 4)
np.testing.assert_array_equal(
result[:, :, 0, 0], np.zeros((4, 4), dtype=np.uint32)
)
result = graycomatrix(self.image, [10], [0], 4, normed=True)
np.testing.assert_array_equal(
result[:, :, 0, 0], np.zeros((4, 4), dtype=np.uint32)
)
def test_normed_symmetric(self):
result = graycomatrix(
self.image, [1, 2, 3], [0, np.pi / 2, np.pi], 4, normed=True, symmetric=True
)
for d in range(result.shape[2]):
for a in range(result.shape[3]):
np.testing.assert_almost_equal(result[:, :, d, a].sum(), 1.0)
np.testing.assert_array_equal(
result[:, :, d, a], result[:, :, d, a].transpose()
)
def test_contrast(self):
result = graycomatrix(self.image, [1, 2], [0], 4, normed=True, symmetric=True)
result = np.round(result, 3)
contrast = graycoprops(result, 'contrast')
np.testing.assert_almost_equal(contrast[0, 0], 0.585, decimal=3)
def test_dissimilarity(self):
result = graycomatrix(
self.image, [1], [0, np.pi / 2], 4, normed=True, symmetric=True
)
result = np.round(result, 3)
dissimilarity = graycoprops(result, 'dissimilarity')
np.testing.assert_almost_equal(dissimilarity[0, 0], 0.418, decimal=3)
def test_dissimilarity_2(self):
result = graycomatrix(
self.image, [1, 3], [np.pi / 2], 4, normed=True, symmetric=True
)
result = np.round(result, 3)
dissimilarity = graycoprops(result, 'dissimilarity')[0, 0]
np.testing.assert_almost_equal(dissimilarity, 0.665, decimal=3)
def test_non_normalized_glcm(self):
img = (np.random.random((100, 100)) * 8).astype(np.uint8)
p = graycomatrix(img, [1, 2, 4, 5], [0, 0.25, 1, 1.5], levels=8)
np.testing.assert_(np.max(graycoprops(p, 'correlation')) < 1.0)
def test_invalid_property(self):
result = graycomatrix(self.image, [1], [0], 4)
with pytest.raises(ValueError):
graycoprops(result, 'ABC')
def test_homogeneity(self):
result = graycomatrix(self.image, [1], [0, 6], 4, normed=True, symmetric=True)
homogeneity = graycoprops(result, 'homogeneity')[0, 0]
np.testing.assert_almost_equal(homogeneity, 0.80833333)
def test_energy(self):
result = graycomatrix(self.image, [1], [0, 4], 4, normed=True, symmetric=True)
energy = graycoprops(result, 'energy')[0, 0]
np.testing.assert_almost_equal(energy, 0.38188131)
def test_correlation(self):
result = graycomatrix(self.image, [1, 2], [0], 4, normed=True, symmetric=True)
energy = graycoprops(result, 'correlation')
np.testing.assert_almost_equal(energy[0, 0], 0.71953255)
np.testing.assert_almost_equal(energy[1, 0], 0.41176470)
def test_uniform_properties(self):
im = np.ones((4, 4), dtype=np.uint8)
result = graycomatrix(
im, [1, 2, 8], [0, np.pi / 2], 4, normed=True, symmetric=True
)
for prop in [
'contrast',
'dissimilarity',
'homogeneity',
'energy',
'correlation',
'ASM',
]:
graycoprops(result, prop)
class TestLBP:
def setup_method(self):
self.image = np.array(
[
[255, 6, 255, 0, 141, 0],
[48, 250, 204, 166, 223, 63],
[8, 0, 159, 50, 255, 30],
[167, 255, 63, 40, 128, 255],
[0, 255, 30, 34, 255, 24],
[146, 241, 255, 0, 189, 126],
],
dtype=np.uint8,
)
@run_in_parallel()
def test_default(self):
lbp = local_binary_pattern(self.image, 8, 1, 'default')
ref = np.array(
[
[0, 251, 0, 255, 96, 255],
[143, 0, 20, 153, 64, 56],
[238, 255, 12, 191, 0, 252],
[129, 64.0, 62, 159, 199, 0],
[255, 4, 255, 175, 0, 254],
[3, 5, 0, 255, 4, 24],
]
)
np.testing.assert_array_equal(lbp, ref)
def test_ror(self):
lbp = local_binary_pattern(self.image, 8, 1, 'ror')
ref = np.array(
[
[0, 127, 0, 255, 3, 255],
[31, 0, 5, 51, 1, 7],
[119, 255, 3, 127, 0, 63],
[3, 1, 31, 63, 31, 0],
[255, 1, 255, 95, 0, 127],
[3, 5, 0, 255, 1, 3],
]
)
np.testing.assert_array_equal(lbp, ref)
@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64])
def test_float_warning(self, dtype):
image = self.image.astype(dtype)
msg = "Applying `local_binary_pattern` to floating-point images"
with expected_warnings([msg]):
lbp = local_binary_pattern(image, 8, 1, 'ror')
ref = np.array(
[
[0, 127, 0, 255, 3, 255],
[31, 0, 5, 51, 1, 7],
[119, 255, 3, 127, 0, 63],
[3, 1, 31, 63, 31, 0],
[255, 1, 255, 95, 0, 127],
[3, 5, 0, 255, 1, 3],
]
)
np.testing.assert_array_equal(lbp, ref)
def test_uniform(self):
lbp = local_binary_pattern(self.image, 8, 1, 'uniform')
ref = np.array(
[
[0, 7, 0, 8, 2, 8],
[5, 0, 9, 9, 1, 3],
[9, 8, 2, 7, 0, 6],
[2, 1, 5, 6, 5, 0],
[8, 1, 8, 9, 0, 7],
[2, 9, 0, 8, 1, 2],
]
)
np.testing.assert_array_equal(lbp, ref)
def test_var(self):
# Test idea: mean of variance is estimate of overall variance.
# Fix random seed for test stability.
np.random.seed(13141516)
# Create random image with known variance.
image = np.random.rand(500, 500)
target_std = 0.3
image = image / image.std() * target_std
# Use P=4 to avoid interpolation effects
P, R = 4, 1
msg = "Applying `local_binary_pattern` to floating-point images"
with expected_warnings([msg]):
lbp = local_binary_pattern(image, P, R, 'var')
# Take central part to avoid border effect.
lbp = lbp[5:-5, 5:-5]
# The LBP variance is biased (ddof=0), correct for that.
expected = target_std**2 * (P - 1) / P
np.testing.assert_almost_equal(lbp.mean(), expected, 4)
def test_nri_uniform(self):
lbp = local_binary_pattern(self.image, 8, 1, 'nri_uniform')
ref = np.array(
[
[0, 54, 0, 57, 12, 57],
[34, 0, 58, 58, 3, 22],
[58, 57, 15, 50, 0, 47],
[10, 3, 40, 42, 35, 0],
[57, 7, 57, 58, 0, 56],
[9, 58, 0, 57, 7, 14],
]
)
np.testing.assert_array_almost_equal(lbp, ref)
class TestMBLBP:
def test_single_mblbp(self):
# Create dummy matrix where first and fifth rectangles have greater
# value than the central one. Therefore, the following bits
# should be 1.
test_img = np.zeros((9, 9), dtype='uint8')
test_img[3:6, 3:6] = 1
test_img[:3, :3] = 255
test_img[6:, 6:] = 255
# MB-LBP is filled in reverse order. So the first and fifth bits from
# the end should be filled.
correct_answer = 0b10001000
int_img = integral_image(test_img)
lbp_code = multiblock_lbp(int_img, 0, 0, 3, 3)
np.testing.assert_equal(lbp_code, correct_answer)

View File

@@ -0,0 +1,177 @@
import numpy as np
import pytest
from skimage._shared._dependency_checks import has_mpl
from skimage.feature.util import (
FeatureDetector,
DescriptorExtractor,
_prepare_grayscale_input_2D,
_mask_border_keypoints,
plot_matches,
plot_matched_features,
)
def test_feature_detector():
with pytest.raises(NotImplementedError):
FeatureDetector().detect(None)
def test_descriptor_extractor():
with pytest.raises(NotImplementedError):
DescriptorExtractor().extract(None, None)
def test_prepare_grayscale_input_2D():
with pytest.raises(ValueError):
_prepare_grayscale_input_2D(np.zeros((3, 3, 3)))
with pytest.raises(ValueError):
_prepare_grayscale_input_2D(np.zeros((3, 1)))
with pytest.raises(ValueError):
_prepare_grayscale_input_2D(np.zeros((3, 1, 1)))
_prepare_grayscale_input_2D(np.zeros((3, 3)))
_prepare_grayscale_input_2D(np.zeros((3, 3, 1)))
_prepare_grayscale_input_2D(np.zeros((1, 3, 3)))
def test_mask_border_keypoints():
keypoints = np.array([[0, 0], [1, 1], [2, 2], [3, 3], [4, 4]])
np.testing.assert_equal(
_mask_border_keypoints((10, 10), keypoints, 0), [1, 1, 1, 1, 1]
)
np.testing.assert_equal(
_mask_border_keypoints((10, 10), keypoints, 2), [0, 0, 1, 1, 1]
)
np.testing.assert_equal(
_mask_border_keypoints((4, 4), keypoints, 2), [0, 0, 1, 0, 0]
)
np.testing.assert_equal(
_mask_border_keypoints((10, 10), keypoints, 5), [0, 0, 0, 0, 0]
)
np.testing.assert_equal(
_mask_border_keypoints((10, 10), keypoints, 4), [0, 0, 0, 0, 1]
)
@pytest.mark.skipif(not has_mpl, reason="Matplotlib not installed")
@pytest.mark.parametrize(
"shapes",
[
((10, 10), (10, 10)),
((10, 10), (12, 10)),
((10, 10), (10, 12)),
((10, 10), (12, 12)),
((12, 10), (10, 10)),
((10, 12), (10, 10)),
((12, 12), (10, 10)),
],
)
def test_plot_matches(shapes):
from matplotlib import pyplot as plt
from matplotlib import use
use('Agg')
fig, ax = plt.subplots(nrows=1, ncols=1)
keypoints1 = 10 * np.random.rand(10, 2)
keypoints2 = 10 * np.random.rand(10, 2)
idxs1 = np.random.randint(10, size=10)
idxs2 = np.random.randint(10, size=10)
matches = np.column_stack((idxs1, idxs2))
shape1, shape2 = shapes
img1 = np.zeros(shape1)
img2 = np.zeros(shape2)
with pytest.warns(FutureWarning):
plot_matches(ax, img1, img2, keypoints1, keypoints2, matches)
with pytest.warns(FutureWarning):
plot_matches(ax, img1, img2, keypoints1, keypoints2, matches, only_matches=True)
with pytest.warns(FutureWarning):
plot_matches(
ax, img1, img2, keypoints1, keypoints2, matches, keypoints_color='r'
)
with pytest.warns(FutureWarning):
plot_matches(ax, img1, img2, keypoints1, keypoints2, matches, matches_color='r')
with pytest.warns(FutureWarning):
plot_matches(
ax, img1, img2, keypoints1, keypoints2, matches, alignment='vertical'
)
plt.close()
@pytest.mark.skipif(not has_mpl, reason="Matplotlib not installed")
@pytest.mark.parametrize(
"shapes",
[
((10, 10), (10, 10)),
((10, 10), (12, 10)),
((10, 10), (10, 12)),
((10, 10), (12, 12)),
((12, 10), (10, 10)),
((10, 12), (10, 10)),
((12, 12), (10, 10)),
],
)
def test_plot_matched_features(shapes):
from matplotlib import pyplot as plt
from matplotlib import use
use('Agg')
fig, ax = plt.subplots()
keypoints0 = 10 * np.random.rand(10, 2)
keypoints1 = 10 * np.random.rand(10, 2)
idxs0 = np.random.randint(10, size=10)
idxs1 = np.random.randint(10, size=10)
matches = np.column_stack((idxs0, idxs1))
shape0, shape1 = shapes
img0 = np.zeros(shape0)
img1 = np.zeros(shape1)
plot_matched_features(
img0,
img1,
keypoints0=keypoints0,
keypoints1=keypoints1,
matches=matches,
ax=ax,
)
plot_matched_features(
img0,
img1,
ax=ax,
keypoints0=keypoints0,
keypoints1=keypoints1,
matches=matches,
only_matches=True,
)
plot_matched_features(
img0,
img1,
ax=ax,
keypoints0=keypoints0,
keypoints1=keypoints1,
matches=matches,
keypoints_color='r',
)
plot_matched_features(
img0,
img1,
ax=ax,
keypoints0=keypoints0,
keypoints1=keypoints1,
matches=matches,
matches_color='r',
)
plot_matched_features(
img0,
img1,
ax=ax,
keypoints0=keypoints0,
keypoints1=keypoints1,
matches=matches,
alignment='vertical',
)
plt.close()

View File

@@ -0,0 +1,537 @@
"""
Methods to characterize image textures.
"""
import warnings
import numpy as np
from .._shared.utils import check_nD
from ..color import gray2rgb
from ..util import img_as_float
from ._texture import _glcm_loop, _local_binary_pattern, _multiblock_lbp
def graycomatrix(image, distances, angles, levels=None, symmetric=False, normed=False):
"""Calculate the gray-level co-occurrence matrix.
A gray level co-occurrence matrix is a histogram of co-occurring
grayscale values at a given offset over an image.
.. versionchanged:: 0.19
`greymatrix` was renamed to `graymatrix` in 0.19.
Parameters
----------
image : array_like
Integer typed input image. Only positive valued images are supported.
If type is other than uint8, the argument `levels` needs to be set.
distances : array_like
List of pixel pair distance offsets.
angles : array_like
List of pixel pair angles in radians.
levels : int, optional
The input image should contain integers in [0, `levels`-1],
where levels indicate the number of gray-levels counted
(typically 256 for an 8-bit image). This argument is required for
16-bit images or higher and is typically the maximum of the image.
As the output matrix is at least `levels` x `levels`, it might
be preferable to use binning of the input image rather than
large values for `levels`.
symmetric : bool, optional
If True, the output matrix `P[:, :, d, theta]` is symmetric. This
is accomplished by ignoring the order of value pairs, so both
(i, j) and (j, i) are accumulated when (i, j) is encountered
for a given offset. The default is False.
normed : bool, optional
If True, normalize each matrix `P[:, :, d, theta]` by dividing
by the total number of accumulated co-occurrences for the given
offset. The elements of the resulting matrix sum to 1. The
default is False.
Returns
-------
P : 4-D ndarray
The gray-level co-occurrence histogram. The value
`P[i,j,d,theta]` is the number of times that gray-level `j`
occurs at a distance `d` and at an angle `theta` from
gray-level `i`. If `normed` is `False`, the output is of
type uint32, otherwise it is float64. The dimensions are:
levels x levels x number of distances x number of angles.
References
----------
.. [1] M. Hall-Beyer, 2007. GLCM Texture: A Tutorial
https://prism.ucalgary.ca/handle/1880/51900
DOI:`10.11575/PRISM/33280`
.. [2] R.M. Haralick, K. Shanmugam, and I. Dinstein, "Textural features for
image classification", IEEE Transactions on Systems, Man, and
Cybernetics, vol. SMC-3, no. 6, pp. 610-621, Nov. 1973.
:DOI:`10.1109/TSMC.1973.4309314`
.. [3] M. Nadler and E.P. Smith, Pattern Recognition Engineering,
Wiley-Interscience, 1993.
.. [4] Wikipedia, https://en.wikipedia.org/wiki/Co-occurrence_matrix
Examples
--------
Compute 2 GLCMs: One for a 1-pixel offset to the right, and one
for a 1-pixel offset upwards.
>>> image = np.array([[0, 0, 1, 1],
... [0, 0, 1, 1],
... [0, 2, 2, 2],
... [2, 2, 3, 3]], dtype=np.uint8)
>>> result = graycomatrix(image, [1], [0, np.pi/4, np.pi/2, 3*np.pi/4],
... levels=4)
>>> result[:, :, 0, 0]
array([[2, 2, 1, 0],
[0, 2, 0, 0],
[0, 0, 3, 1],
[0, 0, 0, 1]], dtype=uint32)
>>> result[:, :, 0, 1]
array([[1, 1, 3, 0],
[0, 1, 1, 0],
[0, 0, 0, 2],
[0, 0, 0, 0]], dtype=uint32)
>>> result[:, :, 0, 2]
array([[3, 0, 2, 0],
[0, 2, 2, 0],
[0, 0, 1, 2],
[0, 0, 0, 0]], dtype=uint32)
>>> result[:, :, 0, 3]
array([[2, 0, 0, 0],
[1, 1, 2, 0],
[0, 0, 2, 1],
[0, 0, 0, 0]], dtype=uint32)
"""
check_nD(image, 2)
check_nD(distances, 1, 'distances')
check_nD(angles, 1, 'angles')
image = np.ascontiguousarray(image)
image_max = image.max()
if np.issubdtype(image.dtype, np.floating):
raise ValueError(
"Float images are not supported by graycomatrix. "
"Convert the image to an unsigned integer type."
)
# for image type > 8bit, levels must be set.
if image.dtype not in (np.uint8, np.int8) and levels is None:
raise ValueError(
"The levels argument is required for data types "
"other than uint8. The resulting matrix will be at "
"least levels ** 2 in size."
)
if np.issubdtype(image.dtype, np.signedinteger) and np.any(image < 0):
raise ValueError("Negative-valued images are not supported.")
if levels is None:
levels = 256
if image_max >= levels:
raise ValueError(
"The maximum grayscale value in the image should be "
"smaller than the number of levels."
)
distances = np.ascontiguousarray(distances, dtype=np.float64)
angles = np.ascontiguousarray(angles, dtype=np.float64)
P = np.zeros(
(levels, levels, len(distances), len(angles)), dtype=np.uint32, order='C'
)
# count co-occurences
_glcm_loop(image, distances, angles, levels, P)
# make each GLMC symmetric
if symmetric:
Pt = np.transpose(P, (1, 0, 2, 3))
P = P + Pt
# normalize each GLCM
if normed:
P = P.astype(np.float64)
glcm_sums = np.sum(P, axis=(0, 1), keepdims=True)
glcm_sums[glcm_sums == 0] = 1
P /= glcm_sums
return P
def graycoprops(P, prop='contrast'):
"""Calculate texture properties of a GLCM.
Compute a feature of a gray level co-occurrence matrix to serve as
a compact summary of the matrix. The properties are computed as
follows:
- 'contrast': :math:`\\sum_{i,j=0}^{levels-1} P_{i,j}(i-j)^2`
- 'dissimilarity': :math:`\\sum_{i,j=0}^{levels-1}P_{i,j}|i-j|`
- 'homogeneity': :math:`\\sum_{i,j=0}^{levels-1}\\frac{P_{i,j}}{1+(i-j)^2}`
- 'ASM': :math:`\\sum_{i,j=0}^{levels-1} P_{i,j}^2`
- 'energy': :math:`\\sqrt{ASM}`
- 'correlation':
.. math:: \\sum_{i,j=0}^{levels-1} P_{i,j}\\left[\\frac{(i-\\mu_i) \\
(j-\\mu_j)}{\\sqrt{(\\sigma_i^2)(\\sigma_j^2)}}\\right]
Each GLCM is normalized to have a sum of 1 before the computation of
texture properties.
.. versionchanged:: 0.19
`greycoprops` was renamed to `graycoprops` in 0.19.
Parameters
----------
P : ndarray
Input array. `P` is the gray-level co-occurrence histogram
for which to compute the specified property. The value
`P[i,j,d,theta]` is the number of times that gray-level j
occurs at a distance d and at an angle theta from
gray-level i.
prop : {'contrast', 'dissimilarity', 'homogeneity', 'energy', \
'correlation', 'ASM'}, optional
The property of the GLCM to compute. The default is 'contrast'.
Returns
-------
results : 2-D ndarray
2-dimensional array. `results[d, a]` is the property 'prop' for
the d'th distance and the a'th angle.
References
----------
.. [1] M. Hall-Beyer, 2007. GLCM Texture: A Tutorial v. 1.0 through 3.0.
The GLCM Tutorial Home Page,
https://prism.ucalgary.ca/handle/1880/51900
DOI:`10.11575/PRISM/33280`
Examples
--------
Compute the contrast for GLCMs with distances [1, 2] and angles
[0 degrees, 90 degrees]
>>> image = np.array([[0, 0, 1, 1],
... [0, 0, 1, 1],
... [0, 2, 2, 2],
... [2, 2, 3, 3]], dtype=np.uint8)
>>> g = graycomatrix(image, [1, 2], [0, np.pi/2], levels=4,
... normed=True, symmetric=True)
>>> contrast = graycoprops(g, 'contrast')
>>> contrast
array([[0.58333333, 1. ],
[1.25 , 2.75 ]])
"""
check_nD(P, 4, 'P')
(num_level, num_level2, num_dist, num_angle) = P.shape
if num_level != num_level2:
raise ValueError('num_level and num_level2 must be equal.')
if num_dist <= 0:
raise ValueError('num_dist must be positive.')
if num_angle <= 0:
raise ValueError('num_angle must be positive.')
# normalize each GLCM
P = P.astype(np.float64)
glcm_sums = np.sum(P, axis=(0, 1), keepdims=True)
glcm_sums[glcm_sums == 0] = 1
P /= glcm_sums
# create weights for specified property
I, J = np.ogrid[0:num_level, 0:num_level]
if prop == 'contrast':
weights = (I - J) ** 2
elif prop == 'dissimilarity':
weights = np.abs(I - J)
elif prop == 'homogeneity':
weights = 1.0 / (1.0 + (I - J) ** 2)
elif prop in ['ASM', 'energy', 'correlation']:
pass
else:
raise ValueError(f'{prop} is an invalid property')
# compute property for each GLCM
if prop == 'energy':
asm = np.sum(P**2, axis=(0, 1))
results = np.sqrt(asm)
elif prop == 'ASM':
results = np.sum(P**2, axis=(0, 1))
elif prop == 'correlation':
results = np.zeros((num_dist, num_angle), dtype=np.float64)
I = np.array(range(num_level)).reshape((num_level, 1, 1, 1))
J = np.array(range(num_level)).reshape((1, num_level, 1, 1))
diff_i = I - np.sum(I * P, axis=(0, 1))
diff_j = J - np.sum(J * P, axis=(0, 1))
std_i = np.sqrt(np.sum(P * (diff_i) ** 2, axis=(0, 1)))
std_j = np.sqrt(np.sum(P * (diff_j) ** 2, axis=(0, 1)))
cov = np.sum(P * (diff_i * diff_j), axis=(0, 1))
# handle the special case of standard deviations near zero
mask_0 = std_i < 1e-15
mask_0[std_j < 1e-15] = True
results[mask_0] = 1
# handle the standard case
mask_1 = ~mask_0
results[mask_1] = cov[mask_1] / (std_i[mask_1] * std_j[mask_1])
elif prop in ['contrast', 'dissimilarity', 'homogeneity']:
weights = weights.reshape((num_level, num_level, 1, 1))
results = np.sum(P * weights, axis=(0, 1))
return results
def local_binary_pattern(image, P, R, method='default'):
"""Compute the local binary patterns (LBP) of an image.
LBP is a visual descriptor often used in texture classification.
Parameters
----------
image : (M, N) array
2D grayscale image.
P : int
Number of circularly symmetric neighbor set points (quantization of
the angular space).
R : float
Radius of circle (spatial resolution of the operator).
method : str {'default', 'ror', 'uniform', 'nri_uniform', 'var'}, optional
Method to determine the pattern:
``default``
Original local binary pattern which is grayscale invariant but not
rotation invariant.
``ror``
Extension of default pattern which is grayscale invariant and
rotation invariant.
``uniform``
Uniform pattern which is grayscale invariant and rotation
invariant, offering finer quantization of the angular space.
For details, see [1]_.
``nri_uniform``
Variant of uniform pattern which is grayscale invariant but not
rotation invariant. For details, see [2]_ and [3]_.
``var``
Variance of local image texture (related to contrast)
which is rotation invariant but not grayscale invariant.
Returns
-------
output : (M, N) array
LBP image.
References
----------
.. [1] T. Ojala, M. Pietikainen, T. Maenpaa, "Multiresolution gray-scale
and rotation invariant texture classification with local binary
patterns", IEEE Transactions on Pattern Analysis and Machine
Intelligence, vol. 24, no. 7, pp. 971-987, July 2002
:DOI:`10.1109/TPAMI.2002.1017623`
.. [2] T. Ahonen, A. Hadid and M. Pietikainen. "Face recognition with
local binary patterns", in Proc. Eighth European Conf. Computer
Vision, Prague, Czech Republic, May 11-14, 2004, pp. 469-481, 2004.
http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.214.6851
:DOI:`10.1007/978-3-540-24670-1_36`
.. [3] T. Ahonen, A. Hadid and M. Pietikainen, "Face Description with
Local Binary Patterns: Application to Face Recognition",
IEEE Transactions on Pattern Analysis and Machine Intelligence,
vol. 28, no. 12, pp. 2037-2041, Dec. 2006
:DOI:`10.1109/TPAMI.2006.244`
"""
check_nD(image, 2)
methods = {
'default': ord('D'),
'ror': ord('R'),
'uniform': ord('U'),
'nri_uniform': ord('N'),
'var': ord('V'),
}
if np.issubdtype(image.dtype, np.floating):
warnings.warn(
"Applying `local_binary_pattern` to floating-point images may "
"give unexpected results when small numerical differences between "
"adjacent pixels are present. It is recommended to use this "
"function with images of integer dtype."
)
image = np.ascontiguousarray(image, dtype=np.float64)
output = _local_binary_pattern(image, P, R, methods[method.lower()])
return output
def multiblock_lbp(int_image, r, c, width, height):
"""Multi-block local binary pattern (MB-LBP).
The features are calculated similarly to local binary patterns (LBPs),
(See :py:meth:`local_binary_pattern`) except that summed blocks are
used instead of individual pixel values.
MB-LBP is an extension of LBP that can be computed on multiple scales
in constant time using the integral image. Nine equally-sized rectangles
are used to compute a feature. For each rectangle, the sum of the pixel
intensities is computed. Comparisons of these sums to that of the central
rectangle determine the feature, similarly to LBP.
Parameters
----------
int_image : (N, M) array
Integral image.
r : int
Row-coordinate of top left corner of a rectangle containing feature.
c : int
Column-coordinate of top left corner of a rectangle containing feature.
width : int
Width of one of the 9 equal rectangles that will be used to compute
a feature.
height : int
Height of one of the 9 equal rectangles that will be used to compute
a feature.
Returns
-------
output : int
8-bit MB-LBP feature descriptor.
References
----------
.. [1] L. Zhang, R. Chu, S. Xiang, S. Liao, S.Z. Li. "Face Detection Based
on Multi-Block LBP Representation", In Proceedings: Advances in
Biometrics, International Conference, ICB 2007, Seoul, Korea.
http://www.cbsr.ia.ac.cn/users/scliao/papers/Zhang-ICB07-MBLBP.pdf
:DOI:`10.1007/978-3-540-74549-5_2`
"""
int_image = np.ascontiguousarray(int_image, dtype=np.float32)
lbp_code = _multiblock_lbp(int_image, r, c, width, height)
return lbp_code
def draw_multiblock_lbp(
image,
r,
c,
width,
height,
lbp_code=0,
color_greater_block=(1, 1, 1),
color_less_block=(0, 0.69, 0.96),
alpha=0.5,
):
"""Multi-block local binary pattern visualization.
Blocks with higher sums are colored with alpha-blended white rectangles,
whereas blocks with lower sums are colored alpha-blended cyan. Colors
and the `alpha` parameter can be changed.
Parameters
----------
image : ndarray of float or uint
Image on which to visualize the pattern.
r : int
Row-coordinate of top left corner of a rectangle containing feature.
c : int
Column-coordinate of top left corner of a rectangle containing feature.
width : int
Width of one of 9 equal rectangles that will be used to compute
a feature.
height : int
Height of one of 9 equal rectangles that will be used to compute
a feature.
lbp_code : int
The descriptor of feature to visualize. If not provided, the
descriptor with 0 value will be used.
color_greater_block : tuple of 3 floats
Floats specifying the color for the block that has greater
intensity value. They should be in the range [0, 1].
Corresponding values define (R, G, B) values. Default value
is white (1, 1, 1).
color_greater_block : tuple of 3 floats
Floats specifying the color for the block that has greater intensity
value. They should be in the range [0, 1]. Corresponding values define
(R, G, B) values. Default value is cyan (0, 0.69, 0.96).
alpha : float
Value in the range [0, 1] that specifies opacity of visualization.
1 - fully transparent, 0 - opaque.
Returns
-------
output : ndarray of float
Image with MB-LBP visualization.
References
----------
.. [1] L. Zhang, R. Chu, S. Xiang, S. Liao, S.Z. Li. "Face Detection Based
on Multi-Block LBP Representation", In Proceedings: Advances in
Biometrics, International Conference, ICB 2007, Seoul, Korea.
http://www.cbsr.ia.ac.cn/users/scliao/papers/Zhang-ICB07-MBLBP.pdf
:DOI:`10.1007/978-3-540-74549-5_2`
"""
# Default colors for regions.
# White is for the blocks that are brighter.
# Cyan is for the blocks that has less intensity.
color_greater_block = np.asarray(color_greater_block, dtype=np.float64)
color_less_block = np.asarray(color_less_block, dtype=np.float64)
# Copy array to avoid the changes to the original one.
output = np.copy(image)
# As the visualization uses RGB color we need 3 bands.
if len(image.shape) < 3:
output = gray2rgb(image)
# Colors are specified in floats.
output = img_as_float(output)
# Offsets of neighbor rectangles relative to central one.
# It has order starting from top left and going clockwise.
neighbor_rect_offsets = (
(-1, -1),
(-1, 0),
(-1, 1),
(0, 1),
(1, 1),
(1, 0),
(1, -1),
(0, -1),
)
# Pre-multiply the offsets with width and height.
neighbor_rect_offsets = np.array(neighbor_rect_offsets)
neighbor_rect_offsets[:, 0] *= height
neighbor_rect_offsets[:, 1] *= width
# Top-left coordinates of central rectangle.
central_rect_r = r + height
central_rect_c = c + width
for element_num, offset in enumerate(neighbor_rect_offsets):
offset_r, offset_c = offset
curr_r = central_rect_r + offset_r
curr_c = central_rect_c + offset_c
has_greater_value = lbp_code & (1 << (7 - element_num))
# Mix-in the visualization colors.
if has_greater_value:
new_value = (1 - alpha) * output[
curr_r : curr_r + height, curr_c : curr_c + width
] + alpha * color_greater_block
output[curr_r : curr_r + height, curr_c : curr_c + width] = new_value
else:
new_value = (1 - alpha) * output[
curr_r : curr_r + height, curr_c : curr_c + width
] + alpha * color_less_block
output[curr_r : curr_r + height, curr_c : curr_c + width] = new_value
return output

View File

@@ -0,0 +1,341 @@
import numpy as np
from ..util import img_as_float
from .._shared.utils import (
_supported_float_type,
check_nD,
deprecate_func,
)
class FeatureDetector:
def __init__(self):
self.keypoints_ = np.array([])
def detect(self, image):
"""Detect keypoints in image.
Parameters
----------
image : 2D array
Input image.
"""
raise NotImplementedError()
class DescriptorExtractor:
def __init__(self):
self.descriptors_ = np.array([])
def extract(self, image, keypoints):
"""Extract feature descriptors in image for given keypoints.
Parameters
----------
image : 2D array
Input image.
keypoints : (N, 2) array
Keypoint locations as ``(row, col)``.
"""
raise NotImplementedError()
@deprecate_func(
deprecated_version="0.23",
removed_version="0.25",
hint="Use `skimage.feature.plot_matched_features` instead.",
)
def plot_matches(
ax,
image1,
image2,
keypoints1,
keypoints2,
matches,
keypoints_color='k',
matches_color=None,
only_matches=False,
alignment='horizontal',
):
"""Plot matched features.
.. deprecated:: 0.23
Parameters
----------
ax : matplotlib.axes.Axes
Matches and image are drawn in this ax.
image1 : (N, M [, 3]) array
First grayscale or color image.
image2 : (N, M [, 3]) array
Second grayscale or color image.
keypoints1 : (K1, 2) array
First keypoint coordinates as ``(row, col)``.
keypoints2 : (K2, 2) array
Second keypoint coordinates as ``(row, col)``.
matches : (Q, 2) array
Indices of corresponding matches in first and second set of
descriptors, where ``matches[:, 0]`` denote the indices in the first
and ``matches[:, 1]`` the indices in the second set of descriptors.
keypoints_color : matplotlib color, optional
Color for keypoint locations.
matches_color : matplotlib color, optional
Color for lines which connect keypoint matches. By default the
color is chosen randomly.
only_matches : bool, optional
Whether to only plot matches and not plot the keypoint locations.
alignment : {'horizontal', 'vertical'}, optional
Whether to show images side by side, ``'horizontal'``, or one above
the other, ``'vertical'``.
"""
image1 = img_as_float(image1)
image2 = img_as_float(image2)
new_shape1 = list(image1.shape)
new_shape2 = list(image2.shape)
if image1.shape[0] < image2.shape[0]:
new_shape1[0] = image2.shape[0]
elif image1.shape[0] > image2.shape[0]:
new_shape2[0] = image1.shape[0]
if image1.shape[1] < image2.shape[1]:
new_shape1[1] = image2.shape[1]
elif image1.shape[1] > image2.shape[1]:
new_shape2[1] = image1.shape[1]
if new_shape1 != image1.shape:
new_image1 = np.zeros(new_shape1, dtype=image1.dtype)
new_image1[: image1.shape[0], : image1.shape[1]] = image1
image1 = new_image1
if new_shape2 != image2.shape:
new_image2 = np.zeros(new_shape2, dtype=image2.dtype)
new_image2[: image2.shape[0], : image2.shape[1]] = image2
image2 = new_image2
offset = np.array(image1.shape)
if alignment == 'horizontal':
image = np.concatenate([image1, image2], axis=1)
offset[0] = 0
elif alignment == 'vertical':
image = np.concatenate([image1, image2], axis=0)
offset[1] = 0
else:
mesg = (
f"plot_matches accepts either 'horizontal' or 'vertical' for "
f"alignment, but '{alignment}' was given. See "
f"https://scikit-image.org/docs/dev/api/skimage.feature.html#skimage.feature.plot_matches " # noqa
f"for details."
)
raise ValueError(mesg)
if not only_matches:
ax.scatter(
keypoints1[:, 1],
keypoints1[:, 0],
facecolors='none',
edgecolors=keypoints_color,
)
ax.scatter(
keypoints2[:, 1] + offset[1],
keypoints2[:, 0] + offset[0],
facecolors='none',
edgecolors=keypoints_color,
)
ax.imshow(image, cmap='gray')
ax.axis((0, image1.shape[1] + offset[1], image1.shape[0] + offset[0], 0))
rng = np.random.default_rng()
for i in range(matches.shape[0]):
idx1 = matches[i, 0]
idx2 = matches[i, 1]
if matches_color is None:
color = rng.random(3)
else:
color = matches_color
ax.plot(
(keypoints1[idx1, 1], keypoints2[idx2, 1] + offset[1]),
(keypoints1[idx1, 0], keypoints2[idx2, 0] + offset[0]),
'-',
color=color,
)
def plot_matched_features(
image0,
image1,
*,
keypoints0,
keypoints1,
matches,
ax,
keypoints_color='k',
matches_color=None,
only_matches=False,
alignment='horizontal',
):
"""Plot matched features between two images.
.. versionadded:: 0.23
Parameters
----------
image0 : (N, M [, 3]) array
First image.
image1 : (N, M [, 3]) array
Second image.
keypoints0 : (K1, 2) array
First keypoint coordinates as ``(row, col)``.
keypoints1 : (K2, 2) array
Second keypoint coordinates as ``(row, col)``.
matches : (Q, 2) array
Indices of corresponding matches in first and second sets of
descriptors, where `matches[:, 0]` (resp. `matches[:, 1]`) contains
the indices in the first (resp. second) set of descriptors.
ax : matplotlib.axes.Axes
The Axes object where the images and their matched features are drawn.
keypoints_color : matplotlib color, optional
Color for keypoint locations.
matches_color : matplotlib color, optional
Color for lines which connect keypoint matches. By default the
color is chosen randomly.
only_matches : bool, optional
Set to True to plot matches only and not the keypoint locations.
alignment : {'horizontal', 'vertical'}, optional
Whether to show the two images side by side (`'horizontal'`), or one above
the other (`'vertical'`).
"""
image0 = img_as_float(image0)
image1 = img_as_float(image1)
new_shape0 = list(image0.shape)
new_shape1 = list(image1.shape)
if image0.shape[0] < image1.shape[0]:
new_shape0[0] = image1.shape[0]
elif image0.shape[0] > image1.shape[0]:
new_shape1[0] = image0.shape[0]
if image0.shape[1] < image1.shape[1]:
new_shape0[1] = image1.shape[1]
elif image0.shape[1] > image1.shape[1]:
new_shape1[1] = image0.shape[1]
if new_shape0 != image0.shape:
new_image0 = np.zeros(new_shape0, dtype=image0.dtype)
new_image0[: image0.shape[0], : image0.shape[1]] = image0
image0 = new_image0
if new_shape1 != image1.shape:
new_image1 = np.zeros(new_shape1, dtype=image1.dtype)
new_image1[: image1.shape[0], : image1.shape[1]] = image1
image1 = new_image1
offset = np.array(image0.shape)
if alignment == 'horizontal':
image = np.concatenate([image0, image1], axis=1)
offset[0] = 0
elif alignment == 'vertical':
image = np.concatenate([image0, image1], axis=0)
offset[1] = 0
else:
mesg = (
f"`plot_matched_features` accepts either 'horizontal' or 'vertical' for "
f"alignment, but '{alignment}' was given. See "
f"https://scikit-image.org/docs/dev/api/skimage.feature.html#skimage.feature.plot_matched_features " # noqa
f"for details."
)
raise ValueError(mesg)
if not only_matches:
ax.scatter(
keypoints0[:, 1],
keypoints0[:, 0],
facecolors='none',
edgecolors=keypoints_color,
)
ax.scatter(
keypoints1[:, 1] + offset[1],
keypoints1[:, 0] + offset[0],
facecolors='none',
edgecolors=keypoints_color,
)
ax.imshow(image, cmap='gray')
ax.axis((0, image0.shape[1] + offset[1], image0.shape[0] + offset[0], 0))
rng = np.random.default_rng()
for i in range(matches.shape[0]):
idx0 = matches[i, 0]
idx1 = matches[i, 1]
if matches_color is None:
color = rng.random(3)
else:
color = matches_color
ax.plot(
(keypoints0[idx0, 1], keypoints1[idx1, 1] + offset[1]),
(keypoints0[idx0, 0], keypoints1[idx1, 0] + offset[0]),
'-',
color=color,
)
def _prepare_grayscale_input_2D(image):
image = np.squeeze(image)
check_nD(image, 2)
image = img_as_float(image)
float_dtype = _supported_float_type(image.dtype)
return image.astype(float_dtype, copy=False)
def _prepare_grayscale_input_nD(image):
image = np.squeeze(image)
check_nD(image, range(2, 6))
image = img_as_float(image)
float_dtype = _supported_float_type(image.dtype)
return image.astype(float_dtype, copy=False)
def _mask_border_keypoints(image_shape, keypoints, distance):
"""Mask coordinates that are within certain distance from the image border.
Parameters
----------
image_shape : (2,) array_like
Shape of the image as ``(rows, cols)``.
keypoints : (N, 2) array
Keypoint coordinates as ``(rows, cols)``.
distance : int
Image border distance.
Returns
-------
mask : (N,) bool array
Mask indicating if pixels are within the image (``True``) or in the
border region of the image (``False``).
"""
rows = image_shape[0]
cols = image_shape[1]
mask = (
((distance - 1) < keypoints[:, 0])
& (keypoints[:, 0] < (rows - distance + 1))
& ((distance - 1) < keypoints[:, 1])
& (keypoints[:, 1] < (cols - distance + 1))
)
return mask