from itertools import combinations_with_replacement import itertools import numpy as np from skimage import filters, feature from skimage import img_as_float32 from concurrent.futures import ThreadPoolExecutor 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) 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 neighbourhoods before extracting features. sigma_max : float, optional Largest value of the Gaussian kernel used to average local neighbourhoods 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 ThreadPoolExecutor(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, multichannel=False, intensity=True, edges=True, texture=True, sigma_min=0.5, sigma_max=16, num_sigma=None, num_workers=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. multichannel : bool, default False True if the last dimension corresponds to color channels. 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 neighbourhoods before extracting features. sigma_max : float, optional Largest value of the Gaussian kernel used to average local neighbourhoods 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 : np.ndarray Array of shape ``image.shape + (n_features,)`` """ 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 image.ndim < 3: multichannel = False if not multichannel: image = image[..., np.newaxis] 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)) return np.stack(features, axis=-1)