import numpy as np
import warnings
from .pyr_utils import max_pyr_height
from .filters import named_filter
from .steer import steer
[docs]
class Pyramid:
"""Base class for multiscale pyramids
You should not instantiate this base class, it is instead inherited by the other classes found
in this module.
Parameters
----------
image : `array_like`
1d or 2d image upon which to construct to the pyramid.
edge_type : {'circular', 'reflect1', 'reflect2', 'repeat', 'zero', 'extend', 'dont-compute'}
Specifies how to handle edges. Options are:
* `'circular'` - circular convolution
* `'reflect1'` - reflect about the edge pixels
* `'reflect2'` - reflect, doubling the edge pixels
* `'repeat'` - repeat the edge pixels
* `'zero'` - assume values of zero outside image boundary
* `'extend'` - reflect and invert
* `'dont-compute'` - zero output when filter overhangs imput boundaries.
Attributes
----------
image : `array_like`
The input image used to construct the pyramid.
image_size : `tuple`
The size of the input image.
pyr_type : `str` or `None`
Human-readable string specifying the type of pyramid. For base class, is None.
edge_type : `str`
Specifies how edges were handled.
pyr_coeffs : `dict`
Dictionary containing the coefficients of the pyramid. Keys are `(level, band)` tuples and
values are 1d or 2d numpy arrays (same number of dimensions as the input image)
pyr_size : `dict`
Dictionary containing the sizes of the pyramid coefficients. Keys are `(level, band)`
tuples and values are tuples.
is_complex : `bool`
Whether the coefficients are complex- or real-valued. Only `SteerablePyramidFreq` can have
a value of True, all others must be False.
"""
def __init__(self, image, edge_type):
self.image = np.asarray(image).astype(float)
if self.image.ndim == 1:
self.image = self.image.reshape(-1, 1)
assert self.image.ndim == 2, "Error: Input signal must be 1D or 2D."
self.image_size = self.image.shape
if not hasattr(self, 'pyr_type'):
self.pyr_type = None
self.edge_type = edge_type
self.pyr_coeffs = {}
self.pyr_size = {}
self.is_complex = False
def _set_num_scales(self, filter_name, height, extra_height=0):
"""Figure out the number of scales (height) of the pyramid
The user should not call this directly. This is called during construction of a pyramid,
and is based on the size of the filters (thus, should be called after instantiating the
filters) and the input image, as well as the `extra_height` parameter (which corresponds to
the residuals, which the Gaussian pyramid contains and others do not).
This sets `self.num_scales` directly instead of returning something, so be careful.
Parameters
----------
filter_name : `str`
Name of the filter in the `filters` dict that determines the height of the pyramid
height : `'auto'` or `int`
During construction, user can specify the number of scales (height) of the pyramid.
The pyramid will have this number of scales unless that's greater than the maximum
possible height.
extra_height : `int`, optional
The automatically calculated maximum number of scales is based on the size of the input
image and filter size. The Gaussian pyramid also contains the final residuals and so we
need to add one more to this number.
Returns
-------
None
"""
# the Gaussian and Laplacian pyramids can go one higher than the value returned here, so we
# use the extra_height argument to allow for that
max_ht = max_pyr_height(self.image.shape, self.filters[filter_name].shape) + extra_height
if height == 'auto':
self.num_scales = max_ht
elif height > max_ht:
raise ValueError("Cannot build pyramid higher than %d levels." % (max_ht))
else:
self.num_scales = int(height)
def _recon_levels_check(self, levels):
"""Check whether levels arg is valid for reconstruction and return valid version
When reconstructing the input image (i.e., when calling `recon_pyr()`), the user specifies
which levels to include. This makes sure those levels are valid and gets them in the form
we expect for the rest of the reconstruction. If the user passes `'all'`, this constructs
the appropriate list (based on the values of `self.pyr_coeffs`).
Parameters
----------
levels : `list`, `int`, or {`'all'`, `'residual_highpass'`, or `'residual_lowpass'`}
If `list` should contain some subset of integers from `0` to `self.num_scales-1`
(inclusive) and `'residual_highpass'` and `'residual_lowpass'` (if appropriate for the
pyramid). If `'all'`, returned value will contain all valid levels. Otherwise, must be
one of the valid levels.
Returns
-------
levels : `list`
List containing the valid levels for reconstruction.
"""
if isinstance(levels, str) and levels == 'all':
levels = ['residual_highpass'] + list(range(self.num_scales)) + ['residual_lowpass']
else:
if not hasattr(levels, '__iter__') or isinstance(levels, str):
# then it's a single int or string
levels = [levels]
levs_nums = np.asarray([int(i) for i in levels if isinstance(i, int) or i.isdigit()])
assert (levs_nums >= 0).all(), "Level numbers must be non-negative."
assert (levs_nums < self.num_scales).all(), "Level numbers must be in the range [0, %d]" % (self.num_scales-1)
levs_tmp = list(np.sort(levs_nums)) # we want smallest first
if 'residual_highpass' in levels:
levs_tmp = ['residual_highpass'] + levs_tmp
if 'residual_lowpass' in levels:
levs_tmp = levs_tmp + ['residual_lowpass']
levels = levs_tmp
# not all pyramids have residual highpass / lowpass, but it's easier to construct the list
# including them, then remove them if necessary.
if 'residual_lowpass' not in self.pyr_coeffs.keys() and 'residual_lowpass' in levels:
levels.pop(-1)
if 'residual_highpass' not in self.pyr_coeffs.keys() and 'residual_highpass' in levels:
levels.pop(0)
return levels
def _recon_bands_check(self, bands):
"""Check whether bands arg is valid for reconstruction and return valid version
When reconstructing the input image (i.e., when calling `recon_pyr()`), the user specifies
which orientations to include. This makes sure those orientations are valid and gets them
in the form we expect for the rest of the reconstruction. If the user passes `'all'`, this
constructs the appropriate list (based on the values of `self.pyr_coeffs`).
Parameters
----------
bands : `list`, `int`, or `'all'`.
If list, should contain some subset of integers from `0` to `self.num_orientations-1`.
If `'all'`, returned value will contain all valid orientations. Otherwise, must be one
of the valid orientations.
Returns
-------
bands: `list`
List containing the valid orientations for reconstruction.
"""
if isinstance(bands, str) and bands == "all":
bands = np.arange(self.num_orientations)
else:
bands = np.array(bands, ndmin=1)
assert (bands >= 0).all(), "Error: band numbers must be larger than 0."
assert (bands < self.num_orientations).all(), "Error: band numbers must be in the range [0, %d]" % (self.num_orientations - 1)
return bands
def _recon_keys(self, levels, bands, max_orientations=None):
"""Make a list of all the relevant keys from `pyr_coeffs` to use in pyramid reconstruction
When reconstructing the input image (i.e., when calling `recon_pyr()`), the user specifies
some subset of the pyramid coefficients to include in the reconstruction. This function
takes in those specifications, checks that they're valid, and returns a list of tuples
that are keys into the `pyr_coeffs` dictionary.
Parameters
----------
levels : `list`, `int`, or {`'all'`, `'residual_highpass'`, `'residual_lowpass'`}
If `list` should contain some subset of integers from `0` to `self.num_scales-1`
(inclusive) and `'residual_highpass'` and `'residual_lowpass'` (if appropriate for the
pyramid). If `'all'`, returned value will contain all valid levels. Otherwise, must be
one of the valid levels.
bands : `list`, `int`, or `'all'`.
If list, should contain some subset of integers from `0` to `self.num_orientations-1`.
If `'all'`, returned value will contain all valid orientations. Otherwise, must be one
of the valid orientations.
max_orientations: `None` or `int`.
The maximum number of orientations we allow in the reconstruction. when we determine
which ints are allowed for bands, we ignore all those greater than max_orientations.
Returns
-------
recon_keys : `list`
List of `tuples`, all of which are keys in `pyr_coeffs`. These are the coefficients to
include in the reconstruction of the image.
"""
levels = self._recon_levels_check(levels)
bands = self._recon_bands_check(bands)
if max_orientations is not None:
for i in bands:
if i >= max_orientations:
warnings.warn(("You wanted band %d in the reconstruction but max_orientation"
" is %d, so we're ignoring that band" % (i, max_orientations)))
bands = [i for i in bands if i < max_orientations]
recon_keys = []
for level in levels:
# residual highpass and lowpass
if isinstance(level, str):
recon_keys.append(level)
# else we have to get each of the (specified) bands at
# that level
else:
recon_keys.extend([(level, band) for band in bands])
return recon_keys
[docs]
class SteerablePyramidBase(Pyramid):
"""base class for steerable pyramid
should not be called directly, we just use it so we can make both SteerablePyramidFreq and
SteerablePyramidSpace inherit the steer_coeffs function
"""
def __init__(self, image, edge_type):
super().__init__(image=image, edge_type=edge_type)
[docs]
def steer_coeffs(self, angles, even_phase=True):
"""Steer pyramid coefficients to the specified angles
This allows you to have filters that have the Gaussian derivative order specified in
construction, but arbitrary angles or number of orientations.
Parameters
----------
angles : `list`
list of angles (in radians) to steer the pyramid coefficients to
even_phase : `bool`
specifies whether the harmonics are cosine or sine phase aligned about those positions.
Returns
-------
resteered_coeffs : `dict`
dictionary of re-steered pyramid coefficients. will have the same number of scales as
the original pyramid (though it will not contain the residual highpass or lowpass).
like `self.pyr_coeffs`, keys are 2-tuples of ints indexing the scale and orientation,
but now we're indexing `angles` instead of `self.num_orientations`.
resteering_weights : `dict`
dictionary of weights used to re-steer the pyramid coefficients. will have the same
keys as `resteered_coeffs`.
"""
resteered_coeffs = {}
resteering_weights = {}
for i in range(self.num_scales):
basis = np.vstack([self.pyr_coeffs[(i, j)].flatten() for j in
range(self.num_orientations)]).T
for j, a in enumerate(angles):
res, steervect = steer(basis, a, return_weights=True, even_phase=even_phase)
resteered_coeffs[(i, j)] = res.reshape(self.pyr_coeffs[(i, 0)].shape)
resteering_weights[(i, j)] = steervect
return resteered_coeffs, resteering_weights