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