# Copyright (c) 2011-2013 Mick Thomure
# All rights reserved.
#
# Please see the file LICENSE.txt in this distribution for usage terms.
import Image
import numpy as np
from pprint import pformat
from glimpse.backends import ACTIVATION_DTYPE
from glimpse.util.garray import fromimage, toimage
from glimpse.util.dataflow import Node
from glimpse.util.dataflow import node_builder as layer_builder
__all__ = [
'LayerSpec',
'Layer',
'FromImage',
'ToImage',
'layer_builder',
]
class LayerSpec(Node):
"""Describes a single layer in a model."""
def __init__(self, id_, name, depends = None):
super(LayerSpec, self).__init__(id_, depends)
self.name = name #: (str) Name of model layer.
def __repr__(self):
return "%s(%s)" % (type(self).__name__, ", ".join("%s=%s" % (k,
pformat(getattr(self, k))) for k in ('id_', 'name', 'depends')))
[docs]class Layer(object):
"""Enumerator for model layers."""
@classmethod
[docs] def FromId(cls, id_):
"""Lookup a LayerSpec object by ID.
:param id_: Model-unique layer id_ifier.
:rtype: :class:`LayerSpec`
Example:
>>> lyr = Layer.FromId(Layer.IMAGE.id_)
>>> assert(lyr == Layer.IMAGE)
"""
if isinstance(id_, LayerSpec):
return id_
for layer in cls.AllLayers():
if layer.id_ == id_:
return layer
raise ValueError("Unknown layer id: %r" % id_)
@classmethod
[docs] def FromName(cls, name):
"""Lookup a LayerSpec object by name.
This method is not case sensitive.
:param str name: Layer name.
:rtype: :class:`LayerSpec`
Example:
>>> lyr = Layer.FromName(Layer.IMAGE.name)
>>> assert(lyr == Layer.IMAGE)
"""
if isinstance(name, LayerSpec):
return name
# Here we perform a linear search, rather than use a dictionary, since we
# want a case-insensitive comparison. This should be fine, since this method
# is not called often.
name_ = name.lower()
for layer in cls.AllLayers():
if layer.name.lower() == name_:
return layer
raise ValueError("Unknown layer name: %s" % name)
@classmethod
[docs] def AllLayers(cls):
"""Return the unordered set of all layers.
:rtype: list of :class:`LayerSpec`
Example:
>>> assert(Layer.IMAGE in Layer.AllLayers())
"""
names = [ k for k in dir(cls) if not k.startswith('_') ]
values = [ getattr(cls, k) for k in names ]
return [ v for v in values if isinstance(v, LayerSpec) ]
@classmethod
[docs] def IsSublayer(cls, sub_layer, super_layer):
"""Determine if one layer appears later in the network than another.
:param sub_layer: Lower layer.
:type sub_layer: :class:`LayerSpec`
:param super_layer: Higher layer.
:type super_layer: :class:`LayerSpec`
:rtype: bool
Examples:
>>> assert(Layer.IsSublayer(Layer.SOURCE, Layer.IMAGE))
>>> assert(not Layer.IsSublayer(Layer.IMAGE, Layer.SOURCE))
"""
for lyr in (super_layer.depends or ()):
if lyr == sub_layer or cls.IsSublayer(sub_layer, lyr):
return True
return False
@classmethod
[docs] def TopLayer(cls):
"""Determine the top layer in this network.
The top-most layer is defined as the layer on which no other layer depends.
If multiple layers meet this criteria, then the first such layer (as
returned by :meth:`AllLayers`) is returned.
:rtype: :class:`LayerSpec`
Example:
>>> assert(Layer.TopLayer() == Layer.IMAGE)
"""
all_layers = cls.AllLayers()
for layer in all_layers:
if any(layer in (l.depends or ()) for l in all_layers):
continue
return layer
raise Exception("Internal error: Failed to find top layer in network")
[docs]def FromImage(input_, backend):
"""Create the initial image layer from some input.
:param input_: Input data. If array, values should lie in the range [0, 1].
:type input_: PIL.Image or 2D ndarray of float
:returns: Image layer data with values in the range [0, 1].
:rtype: 2D ndarray of float
"""
if Image.isImageType(input_):
input_ = fromimage(input_.convert('L'))
input_ = input_.astype(np.float)
# Map from [0,255) to [0,1)
input_ /= 255
# Note: we could have scaled pixels to [-1,1). This would result in a
# doubling of the dynamic range of the S1 response, since each S1 activation
# is of the form:
# y = XW
# where X and W are the input and weight vector, respectively. The rescaled
# version of X (call it X') is given by:
# X' = 2X - 1
# so the activation is given by
# y' = X'W = (2X - 1)W = 2XW - \sum w_i = 2XW
# since W is a mean-zero Gabor filter. (This ignores retinal processing, and
# nonlinearities caused by normalization). The scaling of S1 response seems
# unlikely to cause a significant change in the network output.
elif isinstance(input_, np.ndarray):
if input_.ndim != 2:
raise ValueError("Image array must be 2D")
else:
raise ValueError("Unknown input value of type: %s" % type(input_))
output = backend.PrepareArray(input_)
if np.isnan(output).any():
raise BackendError("Found illegal values in image layer")
return output
[docs]def ToImage(data):
"""Create an image from a 2D array of model activity.
:param data: Single scale of one layer of model activity, with elements in the
range [0,1].
:type data: 2D ndarray of floats
:rtype: Image
:returns: Greyscale image of layer activity.
"""
if not (isinstance(data, np.ndarray) and data.ndim == 2 and \
data.dtype == ACTIVATION_DTYPE):
raise ValueError("Invalid image layer data")
data = data.copy()
data *= 255
img = toimage(data.astype(np.uint8))
return img