Source code for menpo.feature.features

import itertools
import warnings

import numpy as np

from .base import imgfeature, ndfeature


[docs]@ndfeature def gradient(pixels): r""" Calculates the gradient of an input image. The image is assumed to have channel information on the first axis. In the case of multiple channels, it returns the gradient over each axis over each channel as the first axis. The gradient is computed using second order accurate central differences in the interior and first order accurate one-side (forward or backwards) differences at the boundaries. Parameters ---------- pixels : :map:`Image` or subclass or ``(C, X, Y, ..., Z)`` `ndarray` Either the image object itself or an array where the first dimension is interpreted as channels. This means an N-dimensional image is represented by an N+1 dimensional array. If the image is 2-dimensional the pixels should be of type float/double (int is not supported). Returns ------- gradient : `ndarray` The gradient over each axis over each channel. Therefore, the first axis of the gradient of a 2D, single channel image, will have length `2`. The first axis of the gradient of a 2D, 3-channel image, will have length `6`, the ordering being ``I[:, 0, 0] = [R0_y, G0_y, B0_y, R0_x, G0_x, B0_x]``. To be clear, all the ``y``-gradients are returned over each channel, then all the ``x``-gradients. """ if pixels.dtype == np.uint8: raise TypeError("Attempting to take the gradient on a uint8 image.") n_dims = pixels.ndim - 1 grad_per_dim_per_channel = [np.gradient(g, edge_order=1) for g in pixels] # Flatten out the separate dims grad_per_channel = list(itertools.chain.from_iterable(grad_per_dim_per_channel)) # Add a channel axis for broadcasting grad_per_channel = [g[None, ...] for g in grad_per_channel] # Permute the list so it is first axis, second axis, etc grad_per_channel = [grad_per_channel[i::n_dims] for i in range(n_dims)] grad_per_channel = list(itertools.chain.from_iterable(grad_per_channel)) # Concatenate gradient list into an array (the new_image) return np.concatenate(grad_per_channel, axis=0)
[docs]@ndfeature def gaussian_filter(pixels, sigma): r""" Calculates the convolution of the input image with a multidimensional Gaussian filter. Parameters ---------- pixels : :map:`Image` or subclass or ``(C, X, Y, ..., Z)`` `ndarray` Either the image object itself or an array with the pixels. The first dimension is interpreted as channels. This means an N-dimensional image is represented by an N+1 dimensional array. sigma : `float` or `list` of `float` The standard deviation for Gaussian kernel. The standard deviations of the Gaussian filter are given for each axis as a `list`, or as a single `float`, in which case it is equal for all axes. Returns ------- output_image : :map:`Image` or subclass or ``(X, Y, ..., Z, C)`` `ndarray` The filtered image has the same type and size as the input ``pixels``. """ from scipy.ndimage import gaussian_filter as scipy_gaussian_filter # expensive output = np.empty(pixels.shape, dtype=pixels.dtype) for dim in range(pixels.shape[0]): scipy_gaussian_filter(pixels[dim], sigma, output=output[dim]) return output
[docs]@ndfeature def igo(pixels, double_angles=False, verbose=False): r""" Extracts Image Gradient Orientation (IGO) features from the input image. The output image has ``N * C`` number of channels, where ``N`` is the number of channels of the original image and ``C = 2`` or ``C = 4`` depending on whether double angles are used. Parameters ---------- pixels : :map:`Image` or subclass or ``(C, X, Y, ..., Z)`` `ndarray` Either the image object itself or an array with the pixels. The first dimension is interpreted as channels. This means an N-dimensional image is represented by an N+1 dimensional array. double_angles : `bool`, optional Assume that ``phi`` represents the gradient orientations. If this flag is ``False``, the features image is the concatenation of ``cos(phi)`` and ``sin(phi)``, thus 2 channels. If ``True``, the features image is the concatenation of ``cos(phi)``, ``sin(phi)``, ``cos(2 * phi)``, ``sin(2 * phi)``, thus 4 channels. verbose : `bool`, optional Flag to print IGO related information. Returns ------- igo : :map:`Image` or subclass or ``(X, Y, ..., Z, C)`` `ndarray` The IGO features image. It has the same type and shape as the input ``pixels``. The output number of channels depends on the ``double_angles`` flag. Raises ------ ValueError Image has to be 2D in order to extract IGOs. References ---------- .. [1] G. Tzimiropoulos, S. Zafeiriou and M. Pantic, "Subspace learning from image gradient orientations", IEEE Transactions on Pattern Analysis and Machine Intelligence, vol. 34, num. 12, p. 2454--2466, 2012. """ # check number of dimensions if len(pixels.shape) != 3: raise ValueError( "IGOs only work on 2D images. Expects image data " "to be 3D, channels + shape." ) n_img_chnls = pixels.shape[0] # feature channels per image channel feat_chnls = 2 if double_angles: feat_chnls = 4 # compute gradients grad = gradient(pixels) # compute angles grad_orient = np.angle(grad[:n_img_chnls] + 1j * grad[n_img_chnls:]) # compute igo image igo_pixels = np.empty( (n_img_chnls * feat_chnls, pixels.shape[1], pixels.shape[2]), dtype=pixels.dtype ) if double_angles: dbl_grad_orient = 2 * grad_orient # y angles igo_pixels[:n_img_chnls] = np.sin(grad_orient) igo_pixels[n_img_chnls : n_img_chnls * 2] = np.sin(dbl_grad_orient) # x angles igo_pixels[n_img_chnls * 2 : n_img_chnls * 3] = np.cos(grad_orient) igo_pixels[n_img_chnls * 3 :] = np.cos(dbl_grad_orient) else: igo_pixels[:n_img_chnls] = np.sin(grad_orient) # y igo_pixels[n_img_chnls:] = np.cos(grad_orient) # x # print information if verbose: info_str = "IGO Features:\n" info_str = "{} - Input image is {}W x {}H with {} channels.\n".format( info_str, pixels.shape[2], pixels.shape[1], n_img_chnls ) info_str = "{} - Double angles are {}.\n".format( info_str, "enabled" if double_angles else "disabled" ) info_str = "{}Output image size {}W x {}H with {} channels.".format( info_str, igo_pixels.shape[2], igo_pixels.shape[1], n_img_chnls ) print(info_str) return igo_pixels
[docs]@ndfeature def es(pixels, verbose=False): r""" Extracts Edge Structure (ES) features from the input image. The output image has ``N * C`` number of channels, where ``N`` is the number of channels of the original image and ``C = 2``. Parameters ---------- pixels : :map:`Image` or subclass or ``(C, X, Y, ..., Z)`` `ndarray` Either an image object itself or an array where the first axis represents the number of channels. This means an N-dimensional image is represented by an N+1 dimensional array. verbose : `bool`, optional Flag to print ES related information. Returns ------- es : :map:`Image` or subclass or ``(X, Y, ..., Z, C)`` `ndarray` The ES features image. It has the same type and shape as the input ``pixels``. The output number of channels is ``C = 2``. Raises ------ ValueError Image has to be 2D in order to extract ES features. References ---------- .. [1] T. Cootes, C. Taylor, "On representing edge structure for model matching", Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition (CVPR), 2001. """ # check number of dimensions if len(pixels.shape) != 3: raise ValueError( "ES features only work on 2D images. Expects " "image data to be 3D, channels + shape." ) n_img_chnls = pixels.shape[0] # feature channels per image channel feat_channels = 2 # compute gradients grad = gradient(pixels) # compute magnitude grad_abs = np.abs(grad[:n_img_chnls] + 1j * grad[n_img_chnls:]) # compute es image grad_abs = grad_abs + np.median(grad_abs) es_pixels = np.empty( (pixels.shape[0] * feat_channels, pixels.shape[1], pixels.shape[2]), dtype=pixels.dtype, ) es_pixels[:n_img_chnls] = grad[:n_img_chnls] / grad_abs es_pixels[n_img_chnls:] = grad[n_img_chnls:] / grad_abs # print information if verbose: info_str = "ES Features:\n" info_str = "{} - Input image is {}W x {}H with {} channels.\n".format( info_str, pixels.shape[2], pixels.shape[1], n_img_chnls ) info_str = "{}Output image size {}W x {}H with {} channels.".format( info_str, es_pixels.shape[2], es_pixels.shape[1], n_img_chnls ) print(info_str) return es_pixels
[docs]@ndfeature def daisy( pixels, step=1, radius=15, rings=2, histograms=2, orientations=8, normalization="l1", sigmas=None, ring_radii=None, verbose=False, ): r""" Extracts Daisy features from the input image. The output image has ``N * C`` number of channels, where ``N`` is the number of channels of the original image and ``C`` is the feature channels determined by the input options. Specifically, ``C = (rings * histograms + 1) * orientations``. Parameters ---------- pixels : :map:`Image` or subclass or ``(C, X, Y, ..., Z)`` `ndarray` Either the image object itself or an array with the pixels. The first dimension is interpreted as channels. This means an N-dimensional image is represented by an N+1 dimensional array. step : `int`, optional The sampling step that defines the density of the output image. radius : `int`, optional The radius (in pixels) of the outermost ring. rings : `int`, optional The number of rings to be used. histograms : `int`, optional The number of histograms sampled per ring. orientations : `int`, optional The number of orientations (bins) per histogram. normalization : [ 'l1', 'l2', 'daisy', None ], optional It defines how to normalize the descriptors If 'l1' then L1-normalization is applied at each descriptor. If 'l2' then L2-normalization is applied at each descriptor. If 'daisy' then L2-normalization is applied at individual histograms. If None then no normalization is employed. sigmas : `list` of `float` or ``None``, optional Standard deviation of spatial Gaussian smoothing for the centre histogram and for each ring of histograms. The `list` of sigmas should be sorted from the centre and out. I.e. the first sigma value defines the spatial smoothing of the centre histogram and the last sigma value defines the spatial smoothing of the outermost ring. Specifying sigmas overrides the `rings` parameter by setting ``rings = len(sigmas) - 1``. ring_radii : `list` of `float` or ``None``, optional Radius (in pixels) for each ring. Specifying `ring_radii` overrides the `rings` and `radius` parameters by setting ``rings = len(ring_radii)`` and ``radius = ring_radii[-1]``. If both sigmas and ring_radii are given, they must satisfy :: len(ring_radii) == len(sigmas) + 1 since no radius is needed for the centre histogram. verbose : `bool` Flag to print Daisy related information. Returns ------- daisy : :map:`Image` or subclass or ``(X, Y, ..., Z, C)`` `ndarray` The ES features image. It has the same type and shape as the input ``pixels``. The output number of channels is ``C = (rings * histograms + 1) * orientations``. Raises ------ ValueError len(sigmas)-1 != len(ring_radii) ValueError Invalid normalization method. References ---------- .. [1] E. Tola, V. Lepetit and P. Fua, "Daisy: An efficient dense descriptor applied to wide-baseline stereo", IEEE Transactions on Pattern Analysis and Machine Intelligence, vol. 32, num. 5, p. 815-830, 2010. """ from menpo.external.skimage._daisy import _daisy # Parse options 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 is None: normalization = "off" if normalization not in ["l1", "l2", "daisy", "off"]: raise ValueError("Invalid normalization method.") # Compute daisy features daisy_descriptor = _daisy( pixels, step=step, radius=radius, rings=rings, histograms=histograms, orientations=orientations, normalization=normalization, sigmas=sigmas, ring_radii=ring_radii, ) # print information if verbose: info_str = "Daisy Features:\n" info_str = "{} - Input image is {}W x {}H with {} channels.\n".format( info_str, pixels.shape[2], pixels.shape[1], pixels.shape[0] ) info_str = "{} - Sampling step is {}.\n".format(info_str, step) info_str = ( "{} - Radius of {} pixels, {} rings and {} histograms " "with {} orientations.\n".format( info_str, radius, rings, histograms, orientations ) ) if not normalization == "off": info_str = "{} - Using {} normalization.\n".format(info_str, normalization) else: info_str = "{} - No normalization emplyed.\n".format(info_str) info_str = "{}Output image size {}W x {}H x {}.".format( info_str, daisy_descriptor.shape[2], daisy_descriptor.shape[1], daisy_descriptor.shape[0], ) print(info_str) return daisy_descriptor
[docs]@imgfeature def normalize(img, scale_func=None, mode="all", error_on_divide_by_zero=True): r""" Normalize the pixel values via mean centering and an optional scaling. By default the scaling will be ``1.0``. The ``mode`` parameter selects whether the normalisation is computed across all pixels in the image or per-channel. Parameters ---------- img : :map:`Image` or subclass or ``(C, X, Y, ..., Z)`` `ndarray` Either the image object itself or an array with the pixels. The first dimension is interpreted as channels. This means an N-dimensional image is represented by an N+1 dimensional array. scale_func : `callable`, optional Compute the scaling factor. Expects a single parameter and an optional `axis` keyword argument and will be passed the entire pixel array. Should return a 1D numpy array of one or more values. mode : ``{all, per_channel}``, optional If ``all``, the normalization is over all channels. If ``per_channel``, each channel individually is mean centred and normalized in variance. error_on_divide_by_zero : `bool`, optional If ``True``, will raise a ``ValueError`` on dividing by zero. If ``False``, will merely raise a warning and only those values with non-zero denominators will be normalized. Returns ------- pixels : :map:`Image` or subclass or ``(X, Y, ..., Z, C)`` `ndarray` A normalized copy of the image that was passed in. Raises ------ ValueError If any of the denominators are 0 and ``error_on_divide_by_zero`` is ``True``. """ if scale_func is None: def scale_func(_, axis=None): return np.array([1.0]) pixels = img.as_vector(keep_channels=True) if mode == "all": centered_pixels = pixels - np.mean(pixels) scale_factor = scale_func(centered_pixels) elif mode == "per_channel": centered_pixels = pixels - np.mean(pixels, axis=1, keepdims=True) scale_factor = scale_func(centered_pixels, axis=1).reshape([-1, 1]) else: raise ValueError( "Supported modes are {{'all', 'per_channel'}} - '{}' " "is not known".format(mode) ) zero_denom = (scale_factor == 0).ravel() any_non_zero = np.any(zero_denom) if error_on_divide_by_zero and any_non_zero: raise ValueError("Computed scale factor cannot be 0.0") elif any_non_zero: warnings.warn( "One or more the scale factors are 0.0 and thus these" "entries will be skipped during normalization." ) non_zero_denom = ~zero_denom centered_pixels[non_zero_denom] = ( centered_pixels[non_zero_denom] / scale_factor[non_zero_denom] ) return img.from_vector(centered_pixels) else: return img.from_vector(centered_pixels / scale_factor)
[docs]@ndfeature def normalize_norm(pixels, mode="all", error_on_divide_by_zero=True): r""" Normalize the pixels to be mean centred and have unit norm. The ``mode`` parameter selects whether the normalisation is computed across all pixels in the image or per-channel. Parameters ---------- pixels : :map:`Image` or subclass or ``(C, X, Y, ..., Z)`` `ndarray` Either the image object itself or an array with the pixels. The first dimension is interpreted as channels. This means an N-dimensional image is represented by an N+1 dimensional array. mode : ``{all, per_channel}``, optional If ``all``, the normalization is over all channels. If ``per_channel``, each channel individually is mean centred and normalized in variance. error_on_divide_by_zero : `bool`, optional If ``True``, will raise a ``ValueError`` on dividing by zero. If ``False``, will merely raise a warning and only those values with non-zero denominators will be normalized. Returns ------- pixels : :map:`Image` or subclass or ``(X, Y, ..., Z, C)`` `ndarray` A normalized copy of the image that was passed in. Raises ------ ValueError If any of the denominators are 0 and ``error_on_divide_by_zero`` is ``True``. """ def unit_norm(x, axis=None): return np.linalg.norm(x, axis=axis) return normalize( pixels, scale_func=unit_norm, mode=mode, error_on_divide_by_zero=error_on_divide_by_zero, )
[docs]@ndfeature def normalize_std(pixels, mode="all", error_on_divide_by_zero=True): r""" Normalize the pixels to be mean centred and have unit standard deviation. The ``mode`` parameter selects whether the normalisation is computed across all pixels in the image or per-channel. Parameters ---------- pixels : :map:`Image` or subclass or ``(C, X, Y, ..., Z)`` `ndarray` Either the image object itself or an array with the pixels. The first dimension is interpreted as channels. This means an N-dimensional image is represented by an N+1 dimensional array. mode : ``{all, per_channel}``, optional If ``all``, the normalization is over all channels. If ``per_channel``, each channel individually is mean centred and normalized in variance. error_on_divide_by_zero : `bool`, optional If ``True``, will raise a ``ValueError`` on dividing by zero. If ``False``, will merely raise a warning and only those values with non-zero denominators will be normalized. Returns ------- pixels : :map:`Image` or subclass or ``(X, Y, ..., Z, C)`` `ndarray` A normalized copy of the image that was passed in. Raises ------ ValueError If any of the denominators are 0 and ``error_on_divide_by_zero`` is ``True``. """ def unit_std(x, axis=None): return np.std(x, axis=axis) return normalize( pixels, scale_func=unit_std, mode=mode, error_on_divide_by_zero=error_on_divide_by_zero, )
[docs]@ndfeature def normalize_var(pixels, mode="all", error_on_divide_by_zero=True): r""" Normalize the pixels to be mean centred and normalize according to the variance. The ``mode`` parameter selects whether the normalisation is computed across all pixels in the image or per-channel. Parameters ---------- pixels : :map:`Image` or subclass or ``(C, X, Y, ..., Z)`` `ndarray` Either the image object itself or an array with the pixels. The first dimension is interpreted as channels. This means an N-dimensional image is represented by an N+1 dimensional array. mode : ``{all, per_channel}``, optional If ``all``, the normalization is over all channels. If ``per_channel``, each channel individually is mean centred and normalized in variance. error_on_divide_by_zero : `bool`, optional If ``True``, will raise a ``ValueError`` on dividing by zero. If ``False``, will merely raise a warning and only those values with non-zero denominators will be normalized. Returns ------- pixels : :map:`Image` or subclass or ``(X, Y, ..., Z, C)`` `ndarray` A normalized copy of the image that was passed in. Raises ------ ValueError If any of the denominators are 0 and ``error_on_divide_by_zero`` is ``True``. """ def unit_var(x, axis=None): return np.var(x, axis=axis) return normalize( pixels, scale_func=unit_var, mode=mode, error_on_divide_by_zero=error_on_divide_by_zero, )
[docs]@ndfeature def no_op(pixels): r""" A no operation feature - does nothing but return a copy of the pixels passed in. Parameters ---------- pixels : :map:`Image` or subclass or ``(C, X, Y, ..., Z)`` `ndarray` Either the image object itself or an array with the pixels. The first dimension is interpreted as channels. This means an N-dimensional image is represented by an N+1 dimensional array. Returns ------- pixels : :map:`Image` or subclass or ``(X, Y, ..., Z, C)`` `ndarray` A copy of the image that was passed in. """ return pixels.copy()