# Source code for glimpse.backends.scipy_backend

"""Implementation of filter operations using Scipy's ndimage.correlate()."""

# Copyright (c) 2011-2013 Mick Thomure
#
# Please see the file LICENSE.txt in this distribution for usage terms.

import scipy
from scipy.ndimage import maximum_filter
import numpy as np

from glimpse.util import docstring
from glimpse.backends.misc import IBackend

def Correlate(data, kernel, output = None):
"""Apply a multi-band filter to a set of 2D arrays.

This is done by applying Scipy's :func:scipy.ndimage.correlate to each 2D
input array and then summing across bands.

:param data: Input data.
:type data: ndarray of float
:param kernel: Kernel array.
:type kernel: ndarray of float
:param output: Array in which to store result. If None, a new array will be
created.
:type output: ndarray of float
:return: Output of correlation.
:rtype: ndarray of float

"""
assert data.ndim >= 2
assert data.shape[:-2] == kernel.shape[:-2]
if output == None:
output = np.zeros(data.shape[-2:], data.dtype)
else:
assert output.shape == data.shape[-2:]
output[:] = 0
temp = np.empty_like(output)
data_ = data.reshape((-1,) + data.shape[-2:])
kernel_ = kernel.reshape((-1,) + kernel.shape[-2:])
# TODO consider changing the following to:
#   scipy.signal.convolve(data_, kernel_, mode='valid')
for dband, kband in zip(data_, kernel_):
scipy.ndimage.correlate(dband, kband, output = temp)
output += temp
return output

def PruneArray(data, kernel_shape, scaling):
"""Crop correlation results.

Scipy's :func:scipy.ndimage.correlate applies kernel to border units (in the
last two dimensions), and performs no subsampling. This function returns a
cropped view of the result array, in which border units have been removed, and
subsampling has been performed.

:param data: Result from one or more :func:scipy.ndimage.correlate calls
:type data: ndarray of float
:param kernel_shape: Shape of the kernel passed to
:func:scipy.ndimage.correlate. If None, then cropping is disabled and
only subsampling is performed.
:type kernel_shape: tuple of int
:param scaling: Subsampling factor.
:type scaling: positive int
:return: Cropped, sampled view (not copy) of the input data.
:rtype: ndarray of float

"""
if kernel_shape == None:
# Keep all bands
band_indices = [slice(None)] * (data.ndim - 2)
# Perform subsampling of maps
map_indices = [slice(None, None, scaling)] * 2
else:
# Keep all bands
band_indices = [slice(None)] * (data.ndim - 2)
# Crop borders of maps by half the kernel width, and perform subsampling.
assert len(kernel_shape) >= 2
h, w = kernel_shape[-2:]
hh = int(h / 2)
hw = int(w / 2)
map_indices = [slice(hh, -hh, scaling), slice(hw, -hw, scaling)]
return data[band_indices + map_indices]

[docs]class ScipyBackend(object):
""":class:IBackend implementation using calls to :mod:scipy functions."""

@docstring.copy(IBackend.ContrastEnhance)
def ContrastEnhance(self, data, kwidth, bias, scaling, out = None):
assert data.ndim == 2
kshape = (kwidth, kwidth)
# x - mu / std
k = np.ones(kshape)
k /= k.size
mu = Correlate(data, k)
# XXX does it matter whether we add the bias before or after apply sqrt()?
sigma = np.sqrt(Correlate((data - mu)**2, k) + bias)
result = (data - mu) / sigma
return PruneArray(result, kshape, scaling)

@docstring.copy(IBackend.DotProduct)
def DotProduct(self, data, kernels, scaling = None, out = None, **ignore):
assert scaling != None
assert data.ndim == 3
assert kernels.ndim == 4
output_bands = np.empty((kernels.shape[0],) + data.shape[-2:], data.dtype)
for k, o in zip(kernels, output_bands):
Correlate(data, k, o)
output_bands = PruneArray(output_bands, kernels.shape, scaling)
return output_bands

@docstring.copy(IBackend.NormDotProduct)
def NormDotProduct(self, data, kernels, bias = None, scaling = None,
out = None, **ignore):
assert bias != None
assert scaling != None
assert data.ndim == 3
assert kernels.ndim == 4
assert np.allclose(np.array(map(np.linalg.norm, kernels)), 1), \
"Expected kernels to have unit norm"
def Op(k, o):
# dot product of kernel with local input patches
d = Correlate(data, k, o)
# norm of local input patches
n = Correlate(data**2, np.ones(k.shape))
# use conditional bias
n[ n < bias ] = bias
# normalized dot product
o /= n
output_bands = np.empty((kernels.shape[0],) + data.shape[-2:], data.dtype)
for k, o in zip(kernels, output_bands):
Op(k, o)
output_bands = PruneArray(output_bands, kernels.shape, scaling)
if out != None:
out[:] = output_bands.flat
return out
return output_bands

@docstring.copy(IBackend.Rbf)
def Rbf(self, data, kernels, beta = None, scaling = None, out = None,
**ignore):
assert beta != None
assert scaling != None
assert data.ndim == 3
assert kernels.ndim == 4
def Op(k, o):
# (a,a) is squared input patch length
input_norm = Correlate(data**2, np.ones(k.shape))
# (b,b) is squared kernel length
kernel_norm = np.dot(k.flat, k.flat)
# (a,b) is convolution
conv = Correlate(data, k)
# squared distance between input patches and kernel is
#  ||a-b||^2 = (a,a) + (b,b) - 2(a,b)
# where a is data and b is kernel.
square_dist = input_norm + kernel_norm - 2 * conv
o[:] = np.exp(-1 * beta * square_dist)
output_bands = np.empty((kernels.shape[0],) + data.shape[-2:], data.dtype)
for k, o in zip(kernels, output_bands):
Op(k, o)
output_bands = PruneArray(output_bands, kernels.shape, scaling)
if out != None:
out[:] = output_bands.flat
return out
return output_bands

@docstring.copy(IBackend.NormRbf)
def NormRbf(self, data, kernels, bias = None, beta = None, scaling = None,
out = None, **ignore):
assert bias != None
assert beta != None
assert scaling != None
nd = self.NormDotProduct(data, kernels, bias, scaling)
y = np.exp(-2 * beta * (1 - nd))
if out != None:
out[:] = y.flat
return out
return y

@docstring.copy(IBackend.LocalMax)
def LocalMax(self, data, kwidth, scaling, out = None):
assert len(data.shape) == 3, \
"Unsupported shape for input data: %s" % (data.shape,)
kshape = (kwidth, kwidth)
output = np.empty_like(data)
for d, o in zip(data, output):
maximum_filter(d, kshape, output = o)
output_bands = PruneArray(output, kshape, scaling)
if out != None:
out[:] = output_bands.flat
return out
return output_bands

@docstring.copy(IBackend.GlobalMax)
def GlobalMax(self, data, out = None):
assert len(data.shape) == 3, \
"Unsupported shape for input data: %s" % (data.shape,)
return data.reshape(data.shape[0], -1).max(1, out)

@docstring.copy(IBackend.OutputMapShapeForInput)
def OutputMapShapeForInput(self, kheight, kwidth, scaling, iheight, iwidth):
oheight = iheight / scaling - kheight + 1
owidth = iwdith / scaling - kheight + 1

@docstring.copy(IBackend.InputMapShapeForOutput)
def InputMapShapeForOutput(self, kheight, kwidth, scaling, oheight, owidth):
iheight = oheight * scaling + kheight - 1
iwidth = owidth * scaling + kwidth - 1
return iheight, iwidth

@docstring.copy(IBackend.PrepareArray)
def PrepareArray(self, array):
return array