ImaGen

The ImaGen package provides comprehensive support for creating resolution-independent one- and two-dimensional pattern distributions. ImaGen consists of a large library of primarily two-dimensional spatial patterns, including mathematical functions, geometric primitives, and images read from files, along with many ways to combine or select from any other patterns. These patterns can be used in any Python program that needs configurable patterns or streams of patterns. Basically, as long as the code can accept a Python callable and will call it each time it needs a new pattern, users can then specify any pattern possible in ImaGen's simple declarative pattern language, and the downstream code need not worry about any of the details about how the pattern is specified or generated. This approach gives users full flexibility about which patterns they wish to use, while relieving the downstream code from having to implement anything about patterns. The detailed examples below should help make this clear.

Usage

To create a pattern, just import imagen, then instantiate one of ImaGen's PatternGenerator classes. Each of these classes support various parameters, which are each described in the Reference Manual or via help(pattern-object-or-class). Any parameter values specified on instantiation become the defaults for that object:

In [1]:
import imagen as ig
line=ig.Line(xdensity=5, ydensity=5, smoothing=0)

Then whenever the line object is called, you'll get a new NumPy array:

In [2]:
line()
Out[2]:
array([[ 0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.],
       [ 1.,  1.,  1.,  1.,  1.],
       [-0., -0., -0., -0., -0.],
       [-0., -0., -0., -0., -0.]])

Here the parameters xdensity and ydensity specified that a continuous 1.0×1.0 region in (x,y) space should be sampled on a 5×5 grid. The line object can now be called repeatedly, with any parameter values specified to override those declared above:

In [3]:
import numpy as np
np.set_printoptions(1)
line(smoothing=0.1,orientation=0.8,thickness=0.4)
Out[3]:
array([[ 0. ,  0.1,  0.7,  1. ,  1. ],
       [ 0.1,  0.7,  1. ,  1. ,  1. ],
       [ 0.7,  1. ,  1. ,  1. ,  0.7],
       [ 1. ,  1. ,  1. ,  0.7,  0.1],
       [ 1. ,  1. ,  0.7,  0.1,  0. ]])

ImaGen depends only on NumPy, Param, and HoloViews, none of which have any other required dependencies, and it is thus easy to incorporate ImaGen into your own code to generate or use patterns freely. An optional interface to matplotlib via HoloViews is also available, which provides a convenient way to plot the pattern objects:

In [4]:
import holoviews
%load_ext holoviews.ipython

line.set_param(xdensity=72,ydensity=72,orientation=np.pi/4, thickness=0.1, smoothing=0.02)
line[:]
The holoviews.ipython extension is already loaded. To reload it, use:
  %reload_ext holoviews.ipython
Out[4]:

We will use this plotting interface to show off the remaining patterns, but please remember that the main purpose of ImaGen is to generate arrays for use in other programs, not simply to draw pretty patterns for plotting!

Dynamic parameter values

As you can see above, PatternGenerator objects return different patterns depending on their parameter values. An important feature of these parameter values is that any of them can be set to "dynamic" values, which will then result in a different pattern each time (see the Param package and its numbergen module for details). With dynamic parameters, PatternGenerators provide streams of patterns, not just individual patterns. For example, let's define a SineGrating object with a random orientation, collect four of them at different times (using the .anim() method), and lay them out next to each other (using the NdLayout class from HoloViews):

In [5]:
import numbergen as ng
from holoviews import NdLayout
import param
param.Dynamic.time_dependent=True
NdLayout(ig.SineGrating(orientation=np.pi*ng.UniformRandom()).anim(3))
Out[5]:

As you can see, each time the sine grating was rendered, the pattern differed, because the parameter value for orientation was chosen randomly. Of course, you can set any combination of patterns to dynamic values, to get arbitrarily complex variation over time:

In [6]:
%%opts Image (cmap='gray')
sine_disk = ig.SineGrating(orientation=np.pi*ng.UniformRandom(),
                           scale=0.25*ng.ExponentialDecay(time_constant=3),
                           frequency=4+7*ng.UniformRandom(),
                           x=0.3*ng.NormalRandom(seed=1),
                           y=0.2*ng.UniformRandom(seed=2)-0.1,
                           mask_shape=ig.Disk(size=0.5,smoothing=0.01))
NdLayout(sine_disk.anim(3))
Out[6]:

Composite patterns

As you can see above, PatternGenerator objects can also be used as a mask for another PatternGenerator, which is one simple way to combine them.

PatternGenerators can also be combined directly with each other to create Composite PatternGenerators, which can make any possible 2D pattern. For instance, we can easily sum 10 oriented Gaussian patterns, each with random positions and orientations, giving a different overall pattern at each time:

In [7]:
gs = ig.Composite(operator=np.add,
                  generators=[ig.Gaussian(size=0.15,
                                          x=ng.UniformRandom(seed=i+1)-0.5,
                                          y=ng.UniformRandom(seed=i+2)-0.5,
                                          orientation=np.pi*ng.UniformRandom(seed=i+3))
                                for i in range(10)])
                 
NdLayout(gs.anim(4)).cols(5)
Out[7]:

Once it has been defined, a Composite pattern works just like any other pattern, so that it can be placed, rotated, combined with others, etc., allowing you to build up arbitrarily complex objects out of simple primitives. Here we created a Composite pattern explicitly, but it's usually easier to create them by simply using any of the usual Python operators (+, -, *, /, **, %, & (min), and | (max)) as in the examples below.

For instance, here's an example using np.maximum (via the | operator on PatternGenerators), rotating the composite pattern together as a unit. We also leave it as a HoloViews animation rather than laying it out over space:

In [8]:
%%opts Image.Pattern (cmap='Blues_r')
l1 = ig.Line(orientation=-np.pi/4)
l2 = ig.Line(orientation=+np.pi/4)
cross = l1 | l2
cross.orientation=ng.ScaledTime()*(np.pi/-20)
l1.anim(20) + l2.anim(20) + cross.anim(20)
Out[8]:

The .anim() method collects results at different times conveniently. What it's doing repeatedly is getting a copy of each pattern, then running param.Dynamic.time_fn.advance(1.0) to advance the nominal time, then getting another copy of each pattern until 20 different times have been sampled. The values are "time dependent" (because we set them to be so above), so that any randomness changes only when the time changes, and the randomness is computed as a function of time. That way, regardless of the order you generate the patterns, or even if you go back and forward in time, you will always get the same results at a given nominal time. In your own code, you can turn off time dependence (param.Dynamic.time_dependent=False), in which case new parameter values will be generated for every call to the PatternGenerator. Or, if you are working in a domain that has a clear temporal component, such as simulation, you can set param.Dynamic.time_fn to a function based on your own nominal time, advancing it as appropriate. You can even set that function to real time, in which case you'll get completely unpredictable randomness, which may be appropriate in some circumstances. Whenever there is some notion of time that governs the patterns you want to see, setting time_dependent=True is a good idea, so that you have precise control over the randomness to ensure reproducible results.

We used one operator above to make the cross image, but we can combine operators in any combination, here to build a cartoon face and add the result to a sweeping Line pattern masked with a Disk, creating an animated GIF of the results with HoloViews:

In [9]:
%opts Image (cmap='gray')
import param

param.Dynamic.time_fn.advance(1)

print("The current nominal time value is %s" % param.Dynamic.time_fn())
The current nominal time value is 1
In [10]:
%%output holomap='gif'
lefteye    = ig.Disk(aspect_ratio=0.7, x=0.04, y=0.10, size=0.08,smoothing=0.005)
leftpupil  = ig.Disk(aspect_ratio=1.0, x=0.03, y=0.08, size=0.04,smoothing=0.005)
righteye   = ig.Disk(aspect_ratio=0.7, x=0.04, y=-0.1, size=0.08,smoothing=0.005)
rightpupil = ig.Disk(aspect_ratio=1.0, x=0.03, y=-0.08,size=0.04,smoothing=0.005)
nose   = ig.Gaussian(aspect_ratio=0.8, x=-0.1, y=0.00, size=0.04)
mouth  = ig.Gaussian(aspect_ratio=0.8, x=-0.2, y=0.00, size=0.06)
head   = ig.Disk(    aspect_ratio=1.5, x=-0.02,y=0.00, size=0.40, scale=0.70,smoothing=0.005)
face=head + lefteye - 1.6*leftpupil + righteye - 1.6*rightpupil - 0.5*nose - 0.8*mouth
face.set_param(x=0.2, y=0.1, offset=0.5, size=0.75)
face.orientation=ng.ScaledTime()*np.pi/20

line  = ig.Line(y=0.6-ng.ScaledTime()*0.03)
disk = ig.Disk(smoothing=0.01, size=0.4, x=-0.2, y=-0.2)

(face + line*disk).anim(39)
Out[10]:

Image patterns

ImaGen can load and manipulate photographic images just like other patterns, apart from them not being resolution independent. For full functionality this requires the optional PIL or Pillow library, but support for Numpy arrays as images is provided with no further dependencies. For instance, if you have a database of images (here consisting of only one image for simplicity), you can repeatedly select an image at random from the database using Selector, rotate it randomly if desired, and select a random patch of the image at each time:

In [11]:
from imagen.image import FileImage
inputs=[FileImage(filename=f, size=6.0,
                  x=ng.UniformRandom(lbound=-2,ubound=2),
                  y=ng.UniformRandom(lbound=-2,ubound=2),
                  orientation=ng.NormalRandom(sigma=0.1*np.pi))
        for f in ["images/ellen_arthur.pgm"]]

random_selection=ig.Selector(generators=inputs)
NdLayout(random_selection.anim(5)).cols(6)
Out[11]:

Applying functions to generated patterns

Once the pattern has been generated, but before it is returned, you can apply any function to the data that you like, via the output_fns parameter. A variety of useful TransferFns are supplied for use as output_fns, such as thresholding functions, normalizing functions (L0, L1, L2, L-infinity, etc.), and convolutions. Any number of these or your own functions (anything that can operate on a 2D Numpy array) can be applied, in order:

In [12]:
import imagen.transferfn as tf
from imagen.transferfn.sheet_tf import Convolve

(FileImage()[:] + \
FileImage(output_fns=[tf.BinaryThreshold()])[:] + \
FileImage(output_fns=[Convolve()])[:] + \
FileImage(output_fns=[Convolve(),tf.BinaryThreshold(threshold=0.45)])[:] + \
FileImage(output_fns=[Convolve(kernel_pattern=ig.DifferenceOfGaussians(size=0.12)),
                      tf.BinaryThreshold()])[:]).cols(5)
Out[12]:

Multi-channel patterns

The above examples all show "single-channel" PatternGenerator objects, which are very general and usable for a huge variety of applications, as they are simply Numpy arrays.

PatternGenerator objects can have any number of channels, with each channel generating a Numpy array of the same size. Multi-channel patterns are used less often, but are particularly useful for generating color images. Color images loaded by the FileImage pattern will have four channels, one for the monochrome image (as above), and the other three for the red, green, and blue channels (accessed using object.channels()). RGB images can also be constructed by colorizing a monochrome pattern, or out of combinations of any of the other patterns, using the ComposeChannels object:

In [13]:
from imagen.image import ScaleChannels
ig.ComposeChannels(generators=[ig.Spiral(smoothing=0.02),ig.Spiral(),ig.Spiral(scale=0)])[:] + \
ig.ComposeChannels(generators=[ig.Line(orientation=np.pi/2),ig.Ring(),ig.SquareGrating()])[:]
Out[13]:

Pattern types provided

Below are shown examples of each of the pattern types currently provided, using their default parameter values. Very many different parameter values can be chosen, to produce a much wider range of patterns, and of course new patterns can be created as Composite patterns as shown above.

In [14]:
%opts Layout [sublabel_format="" horizontal_spacing=.1 vertical_spacing=.1]
%opts Image (cmap='gray') [xaxis=None yaxis=None show_frame=True] {+axiswise}
from imagen import *
from imagen.random import *
from imagen.image import *
np.sum([x()[:] for _, x in sorted(locals().items()) if isinstance(x,type)
        and issubclass(x,PatternGenerator) and not x.abstract]).cols(5)
Out[14]:

Extending ImaGen

New Composite patterns can be created easily without writing new classes, as shown above. If you want to create a new non-composite type, you can simply define a new class inheriting from PatternGenerator, then override self.function() to draw the pattern, and declare any new parameter(s) used by self.function(). The new pattern can then be rotated, scaled, translated, etc. automatically, with no further coding, it will support dynamic pattern streams automatically, and it can be combined with any existing or new pattern to make new Composite patterns. If you don't want the automatic scaling, rotating, etc. (e.g. for a whole-field pattern like a new type of random distribution), you can override self.__call__ instead of self.function(), which allows you to do anything that returns a Numpy array of the requested size. See the many classes in imagen/__init__.py and imagen/random.py for examples of each approach.

Installation

Package License PyVersions

ImaGen requires NumPy, Param, and HoloViews, none of which have any required external dependencies.

Official releases of ImaGen are available on PyPI , and can be installed using pip. If you don’t have pip already, we recommend installing a scientific Python distribution like Anaconda first. Then installation of ImaGen and required dependencies is simply:

pip install imagen

Once you’ve installed ImaGen, an easy way to get started is to launch IPython Notebook:

ipython notebook

Now you can download the tutorial notebooks, unzip them somewhere IPython Notebook can find them, and then open the index.ipynb tutorial in the Notebook. Then try out any of the patterns you like, using help(``*obj-or-class*)`` to find out its parameters and their options, or repeatedly press <Shift+TAB> in IPython after opening an object constructor. Just add [:] after your pattern object to plot it using matplotlib and HoloViews. Note that IPython Notebook and matplotlib are not in any way required for ImaGen, but when used with HoloViews they do provide a very handy way to visualize and explore the patterns interactively even if you will eventually use them separately from IPython and matplotlib.

Support

Questions and comments are welcome at https://github.com/ioam/imagen/issues.

Indices and tables

ImaGen

Table Of Contents

This Page