Shady.Utilities Sub-module

This module contains various optional (but useful) utility functions.

Note that everything exported from this module is also available in the top-level Shady.* namespace.

Shady.Utilities.AutoFinish(world, shell=False, prefer_ipython=True, plot='auto')

A useful call at the end of a demo script, to navigate the various ways in which the script, and any Shady.World that was created in it, might be running (single- or dual-threaded by direct interpretation of the script; dual-threaded with the World running in the main thread and the script interpreted in a subsidiary thread via python -m Shady).

Parameters:
  • world – A World instance, ready to run.

  • shell (bool) – Whether or not to spawn a threaded interactive shell while the World is running.

  • prefer_ipython (bool) – If a threaded shell is used, this indicates whether to try. to import the third-party package IPython to improve the interactive experience. If this is False, or if IPython is not installed, then any interactive prompt spawned by shell=True will be a plain-vanilla Python prompt.

  • plot – This argument can be:

    • a callable function with zero arguments: in this case it is called when world finishes running; then it is assumed that one or more matplotlib figures have been generated by the call and that they should be kept alive and responsive.

    • a boolean specifying whether matplotlib figures already exist that need to be kept alive and responsive when world finishes running.

    • 'auto' (default): If world.debugTiming is True, then call PlotTimings( world ) when world finishes running. If not, infer automatically whether there are matplotlib figures.

Returns:

None

Shady.Utilities.CheckPattern(canvasSize=(1920, 1080), amplitude=1.0, checkSize=1, meanZero=True)

Return a 2-dimensional numpy.array containing a checkerboard pattern.

Parameters:
  • canvasSize – may be an int (resulting in a square stimulus), or a sequence of two numbers (width, height). May also be a Shady.World instance, Shady.Stimulus instance, a PIL image, or an image represented as a 2- or 3-dimensional numpy.array - in these cases the dimensions of the output match the dimensions of the instance.

  • amplitude (float) – 0.0 means blank, 1.0 means full contrast.

  • checkSize (int) – size, in pixels, of the individual light and dark squares making up the checkerboard pattern.

  • meanZero (bool) – if True, then the mean of the output array is 0 and its range is [-1,1] when amplitude=1. If False, then the mean of the output array is 0.5 and the range is [0,1] when amplitude=1.

Returns:

A two-dimensional numpy.array containing the image pixel values.

Examples:

world.Stimulus( CheckPattern( world ) * 0.5 + 0.5 )
world.Stimulus( CheckPattern( world, meanZero=False ) )  # same as above
class Shady.Utilities.CommandLine(argv=None, dashes=2, caseSensitive=True, doc='')
c = CommandLine()

Create the object. Uses argv=sys.argv[1:] by default, as well as doc=__doc__.

c.Option(...)

Define an option. By default, the option name and resolved value will be stored as key and value in the dict c.opts. If you specify another container it will be stored there instead (and if you say container=None, it will not be stored anywhere). In any case the resolved value will also be returned from Option()

c.Help()

Define and process the --help option: if it was supplied, print documentation and then either sys.exit() or raise CommandLineHelp().

c.Finalize()

Issue an error if there are unrecognized options.

c.Delegate()

An alternative to Finalize(). Returns argv with all already-recognized options removed, so you can pass it to the next CommandLine parser in the cascade (in cases where you have more than one).

Error

alias of CommandLineError

OptionValueError

alias of CommandLineValueError

UsagePrinted

alias of CommandLineHelp

Shady.Utilities.ComplexPolygonBase(nsides, appendNaN=True, joined=False)

Return a 1-by-n numpy.array of complex numbers that describe the vertices of a polygon.

Parameters:
  • nsides (int) – number of sides (or vertices) of the polygon.

  • appendNaN (bool) – whether to append a NaN (interpreted as a break between polygons in the Stimulus.points property).

  • joined (bool) – whether to repeat the first vertex explicitly at the end. Only necessary if you want to draw unfilled (i.e. wireframe) closed polygons.

Returns:

a 1-by-n numpy.array of complex numbers, where n is nsides, plus 1 if appendNaN is True, plus 1 if joined is True.

Example:

from Shady import World, ComplexPolygonBase, Real2DToComplex
w = World(1000)
s = w.Stimulus( drawMode=Shady.DRAWMODE.POLYGON, anchor=-1, color=[1,0,0] )
shape = 50 * ComplexPolygonBase( 12 )
locations = 150 * ComplexPolygonBase( 3, appendNaN=False ).T
s.points = shape + locations
Shady.Utilities.ComplexToReal2D(x)

Given a single complex number x, return [ x.real, x.imag ]. Or, by extension, given a single scalar real number x, return [ float(x), 0.0 ].

Alternatively, if x is a sequence (i.e. a list, tuple or numpy array) of complex numbers, convert to an n-by-2 array of real-valued coordinates.

Inverse of Real2DToComplex()

Shady.Utilities.Cross(world=None, size=13, thickness=3, **props)

Create an image of a simple cross (horizontal and vertical strokes) and optionally render it as a Stimulus object.

Parameters:
  • world (World) – Optional. If supplied, the image will be rendered as a Stimulus with z=-0.9 and linearMagnification=False. If omitted, the function will simply return the array of pixel values.

  • size (int) – Desired dimensions of the final image, in pixels. For symmetry, if thickness is odd, size should also be odd; and if thickness is even, size should also be even. Otherwise size will get reduced by 1.

  • thickness (int) – Thickness of the strokes of the cross, in pixels.

  • **props – Optional properties to be applied to the Stimulus object (ignored if world is not supplied).

Returns:

If a World instance world is supplied, returns a Stimulus instance. Otherwise just return the array of pixel values.

Shady.Utilities.DegreesToPixels(extentInDegrees, screenInfo, eccentricityInDegrees=0)

Compute the extent of a stimulus in pixels, given the number of degrees of visual arc it subtends and its eccentricity in degrees away from the line of sight.

Parameters:
  • extentInDegrees – an int, float or sequence/array of numbers, denoting the number of degrees subtended at the eye by the stimulus.

  • screenInfo – either the input (a filename or a dict) or the output (a floating-point number) of VDP().

  • eccentricityInDegrees – an int, float or sequence/array of numbers, denoting the angle in degrees between the line of sight and the line from the eye to the center of the stimulus

Returns:

a float (for scalar inputs) or a numpy.array (for array inputs) denoting stimulus extent(s) in pixels.

Shady.Utilities.EllipticalTukeyWindow(size, plateauProportion=0.0)

This function constructs a two-dimensional numpy.array of the specified size containing a discretely sampled one- or two-dimensional spatial windowing function. The function has an optional plateau in the center, outside of which it falls off to 0 according to a raised cosine profile. For two-dimensional windows, all contours of the array are elliptical and have the same aspect ratio as the specified size.

This function replicates the formula that is used for windowing on the GPU, as per the fragment shader program at the heart of Shady.

Parameters:
  • size – May be an integer (dictating a square output), a sequence of two integers [width, height], a World or Stimulus instance, or an existing 2- or 3-D numpy.array to use as a template (i.e. one whose first two dimensions are [height, width] ).

  • plateauProportion – This is either a single floating-point number, or a sequence of two numbers corresponding to the horizontal and vertical dimensions. The numbers specify the linear proportion of the cross section of the window function that is at maximum. They can be in the range 0.0 (no plateau => radial Hann window) to 1.0 (all plateau, no skirt => sharp-edged circle or ellipse). Alternatively, negative values can be used to disable windowing (so, for example, [0.0, -1.0] specifies Hann windowing in the horizontal dimension only, and no windowing in the vertical).

Returns:

A two-dimensional numpy.array containing image pixel multipliers, in the range [0.0, 1.0].

Shady.Utilities.EstimateFrameRate(world, nIdentical=10, nMaxFrames=100, nWarmUpFrames=10, threshold=1, wait=True)

DOC-TODO

Shady.Utilities.FindGamma(world, finish=None, xBlue=True, text=False, **kwargs)

Interactively use a LinearizationTestPattern to estimate the correct gamma.

Blank patches are interspersed in checkerboard fashion with textured patches. Textured patches contain horizontal stripes, vertical stripes, and checkerboard patterns, all of single-pixel granularity. Textured patches vary in contrast. When the screen (or stimulus) is perfectly linearized, textured patches of all contrasts should be indistinguishable from blank patches when viewed from a sufficient distance (or with sufficiently bad uncorrected vision).

The interactive component works best with touch-screens, but you can use the mouse. Up-down movement changes the overall gamma. With the option xBlue=True, left-right movement will also adjust the “color temperature” by varying the blue gamma relative to the others (without this, even when the overall gamma is neither too high nor too low, the textured patches can look yellower than the blank patches on some screens).

Supply an optional callback as finish. When you press the return key, the adjustment procedure will end and finish( gamma ) will be called with the final empirical gamma values. If you press the escape key instead, the adjustment will end by calling finish( None ) instead. Press any letter key to report the current gamma setting on the console.

class Shady.Utilities.FrameIntervalGauge(world, corner=(-1, -1), thickness=49, variable='width', color=(0, 0, 0), useTexture=True, rulerMaxMsec=50)

Display an animated gauge that visually records the frame-to-frame interval in milliseconds. Every millisecond is marked in blue; every ten milliseconds in red. To avoid overhead, no text is rendered.

Shady.Utilities.Hann(x, rise=0.5, start=0.0)

A Hann window (raised cosine) function of x. Wraps around the more general Tukey() function but ensures no plateau, and equal lengths of rise and fall.

Shady.Utilities.Histogram(img, plot=True, DACmax=255, title=None, xlabel='Red, Green or Blue DAC Value', ylabel='Number of Pixels')

Takes an array of pixel values (for example, a Stimulus.Capture() output) and computes histograms of the luminance values in each channel. Optionally, plots the histograms.

Requires the third-party package numpy to compute histograms, and matplotlib if you want to plot them.

Shady.Utilities.LinearizationTestPattern(canvasSize=(1920, 1080), patchSize=(50, 50), amplitudes=(0.95, 0.75, 0.5, 0.25), plateauProportion=0.85, meanZero=True)

Construct a 2-dimensional numpy.array containing a special linearization pattern. Blank patches are interspersed in checkerboard fashion with textured patches. Textured patches contain horizontal stripes, vertical stripes, and checkerboard patterns, all of single-pixel granularity. Textured patches vary in contrast. When the screen (or stimulus) is perfectly linearized, textured patches of all contrasts should be indistinguishable from blank patches when viewed from a sufficient distance (or with sufficiently bad uncorrected vision).

Parameters:
  • canvasSize – may be an int (resulting in a square stimulus), or a sequence of two numbers (width, height). May also be a Shady.World instance, Shady.Stimulus instance, a PIL image, or an image represented as a 2- or 3-dimensional numpy.array - in these cases the dimensions of the output match the dimensions of the instance.

  • patchSize (sequence of 2 ints) – dimensions, in pixels, of the individual patches that make up the pattern in checkerboard fashion.

  • amplitudes (tuple of floats) – 0.0 means blank, 1.0 means full contrast.

  • plateauProportion (float) – governs the one-dimensional raised-cosine fading at the edges of striped patches. With plateauProportion=1, bright and dark edge artifacts tend to be visible.

  • meanZero (bool) – if True, then the mean of the output array is 0 and its range is [-1,1] when amplitude=1. If False, then the mean of the output array is 0.5 and the range is [0,1] when amplitude=1.

Returns:

A two-dimensional numpy.array containing the image pixel values.

Examples:

world.Stimulus( LinearizationTestPattern( world ) * 0.5 + 0.5 )
world.Stimulus( LinearizationTestPattern( world, meanZero=False ) ) # same as above
Shady.Utilities.Loupe(target, update_period=1.0, **kwargs)

Given a target instance of class Stimulus, create another Stimulus instance that presents a magnified, contrast-enhanced, slowed-down (or rather, temporally sub-sampled) view of the pixels rendered by the target.

The returned instance has the following additional attributes:

  • target: a weakref.ref to the target instance

  • update_period: a floating-point number of seconds

  • update_now: a boolean which, if set to True, forces an update on the next frame (and which is then automatically set back to False).

NB: raw (post-linearization) pixel values are captured from the target and then rendered on the loupe which is itself, necessarily, unlinearized. At extreme values, color-contrast-enhancement may therefore lead to an apparent change in mean luminance even though there is no such change in the target.

Shady.Utilities.PixelRuler(base, steps=((10, (0, 0, 1)), (100, (1, 0, 0)), (1000, (0, 1, 0))), alpha=None, topDown=False, world=None, oscillateGamma=False)

If base is an image size specification, or a World instance, create a 90%-contrast gray CheckPattern() of the appropriate size, to use as a base. Alternatively, base may be a ready-made image.

Draw grid lines over the base: the first pixel (pixel 0) is not colored. The 10th, 20th, 30th,… pixels (i.e. pixels 9, 19, 29, …) are blue. Similarly every 100th pixel is red, and every 1000th pixel is green.

The topDown argument changes the definition of “first pixel”: the default is topDown=False, which means the first pixel is the bottom row and we work upwards, just like Shady’s normal coordinate system. But if you specify topDown=True, the first pixel is on the top row and we work downwards (the way one would index rows of a matrix).

If base is a World instance, or if world is supplied as a separate argument, render the pattern at a depth of z=0.9 and return the corresponding Stimulus instance. Otherwise just return the texture as a numpy array.

When rendering as a stimulus, the oscillateGamma argument may be useful. Non-zero values cause the stimulus gamma property to oscillate between 1.0 and 3.0 as a function of time. The pixel values set by PixelRuler, as well as those of the underlying default CheckPattern, are all either 1.0 or 0.0—therefore, changes in gamma should not be visible. The oscillation becomes visible if there is any spatial interpolation of pixel values, so it is a useful tool for highlighting any unintended geometric anomalies that violate the pixel-for-pixel assumption (sub-pixel shifts, non-90-degree rotations, scaling).

Shady.Utilities.PixelsToDegrees(extentInPixels, screenInfo, eccentricityInPixels=0)

Compute the number of degrees of visual angle subtended by a stimulus of given its extent in pixels, and its distance from the point of fixation in pixels.

Parameters:
  • extentInPixels – an int, float or sequence/array of numbers, denoting the size of the stimulus in pixels.

  • screenInfo – either the input (a filename or a dict) or the output (a floating-point number) of VDP().

  • eccentricityInPixels – an int, float or sequence/array of numbers, denoting the distance between fixation and the center of the stimulus, in pixels.

Returns:

a float (for scalar inputs) or a numpy.array (for array inputs) denoting angle(s) subtended, in degrees of visual arc.

Shady.Utilities.PlotTimings(arg, savefig=None, traces=(), axes=None, finish=True, **kwargs)

Plot a graph of the recent timing diagnostics from a World instance or log-file name.

The information is richer if the World instance, and even more so if individual Stimulus instances, had debugTiming set to True.

Requires the third-party packages numpy and matplotlib.

Shady.Utilities.Real2DToComplex(x, add_dims_left=0, add_dims_right='auto')

Given an n-by-2 array of real-valued coordinates, return the coordinates as an array of complex numbers (default shape n-by-1, but depends on add_dims_left and add_dims_right).

Alternatively, given one coordinate expressed as a 2-element numeric sequence (i.e. a list, tuple or 1-D numpy array of length 2), return a single complex number.

Inverse of ComplexToReal2D()

Shady.Utilities.RelativeLocation(loc, obj, anchor=None, normalized=False)

Translate a two-dimensional location loc

From:

World coordinates (i.e. pixels relative to the World’s current anchor position)

To:

coordinates relative to the current xy position of a Stimulus instance in the same World, or (optionally) relative to a different anchor position of the Stimulus instance.

Parameters:
  • loc – input coordinates, in pixels (single scalar or sequence of two scalars)

  • objShady.Stimulus or Shady.World instance

  • anchor – By default, the return value is expressed as an offset in pixels relative to the anchor position of obj (so, if obj is a Stimulus, the function just subtracts obj.xy; or if obj is the World, the output value will be equal to the input value).However, if you specify an explicit anchor (2 numbers each in the range [-1, +1]) then the return value will be re-expressed as a pixel offset relative to some other part of obj. For example, with anchor=[-1,-1] you would get pixel coordinates relative to the bottom left corner of obj, or with anchor=[0,+1] you get pixel coordinates relative to the middle of the top edge of obj. The anchor argument is ignored if you specify normalized=True.

  • normalized (bool) – If False, return 2-D coordinates in pixels. If True, ignore anchor and return 2-D coordinates in the normalized coordinate system of obj. This effectively makes this function the inverse of obj.Place().

Returns:

[x, y] coordinates, in pixels or normalized units depending on the normalized argument.

See also

  • Stimulus.Place()

  • World.Place()

Shady.Utilities.RunShadyScript(*argv, **kwargs)

If a script name is specified, run it. If not, run an interactive prompt. In either case, interpret the commands in a new subsidiary thread—the main thread will be reserved. Construction and running of one Shady.World, if performed in the script or from the prompt, will be diverted to the main thread. This allows Shady applications to run in multi-threaded mode even on non-Windows operating systems, on which the graphical back-end insists on being in the main thread.

With a script, the console option allows the script to run in demo mode, in which sections of the code can be run from an interactive prompt.

This routine is used under the hood when you start Python with -m Shady. Here are some pairs of commands that produce equivalent results starting from outside and from inside Python:

python -m Shady demo showcase
--> RunShadyScript( 'showcase', console='IPython' )

python -m Shady run showcase
--> RunShadyScript( 'showcase', console=None )

python -m Shady run showcase --console=Python
--> RunShadyScript( 'showcase', console='Python' )
Parameters:
  • *argv – positional arguments, just as you would use from a system command-line. If the first element is sys.argv then sys.argv[1:] will be used plus any subsequent *argv and **kwargs arguments.

  • **kwargs – optional keyword arguments, just as you would specify --key=value options from the command-line.

If you specify neither argv nor kwargs, then sys.argv[1:] will be used. Options are as follows:

--script (or positional argument 0)

The name of a Python script to run. If your script argument is not a valid (absolute or relative) path to an existing file, Shady will make a second attempt to find the named script inside the examples subdirectory of the Shady package itself (adding a py extension if necessary). If no script is specified, an interactive prompt will be opened instead.

--console

Values can be 'None', 'Python' or 'IPython', specifying a preference for the type of interactive prompt that should be interleaved with script execution. The default is 'IPython' although Shady will fall back to 'Python' if the third-party IPython package is not available. 'None' will only be respected if a script has been specified, and it causes the script to be run without any interaction from the console.

Shady.Utilities.TearingTest(world, period_sec=4.0, proportional_width=0.15)

Show a high-contrast bar oscillating from side to side, allowing visual inspection of possible “tearing” artifacts.

Shady.Utilities.Tukey(x, rise=0.5, plateau=0.0, fall='rise', start=0.0)

This is a one-dimensional windowing function consisting of a raised- cosine rise from zero, an optional plateau, and a raised-cosine fall back to zero. It is a generalization of the Hann function. It requires the numpy package to be installed.

Parameters:
  • x – The input argument to the function, in units of space or time (let’s assume it’s time from here on).

  • rise – In the same units as x, the amount of time the function takes to rise from 0.0 to 1.0.

  • plateau – In the same units as x, the amount of time for which the function remains at 1.0 between the rising and falling phases. The default is 0, which means this function returns a simple Hann or raised-cosine function by default.

  • fall – In the same units as x, the amount of time the function takes to fall from 1.0 back to 0.0. Alternatively, the string 'rise' may be used (and it is the default) which makes the duration of the fall match the duration of the rise.

  • start – In the same units as x, the initial amount of time for which the function remains at 0.0 before starting to rise. Defaults to 0.

Returns:

The numeric output of the window function (as a float if the input x was a scalar numeric; as a numpy.array if x was a sequence or array of numbers).

Shady.Utilities.VDP(*pargs, **kwargs)

Return viewing distance measured in pixels, based on a set of configuration parameters. This allows easy conversion between degrees and pixels.

If a single scalar numeric argument is provided, return it unchanged.

If a string is provided, treat it the name of a Python file which defines the necessary settings as variables, and execute it.

If a Shady.World object is provided, infer widthInPixels and heightInPixels from its size. Otherwise, if a dict is provided, take the necessary settings from that.

Use **kwargs to augment / override settings.

The necessary settings are:

  • viewingDistanceInMm, and

  • EITHER: widthInPixels AND widthInMm

    OR: heightInPixels AND heightInMm

Flexibility is allowed with these variable names: they are case-invariant and underscore-invariant, the 'In' is optional, and the physical units can be mm, cm or m. So for example:

viewing_distance_cm = 75

would give the same result as:

ViewingDistanceInMm = 750

Example:

v = VDP( world, heightInMm=169, viewingDistanceInCm=75 )
pixelsPerDegree = DegreesToPixels( 1.0, v )
class Shady.Utilities.VideoRecording(filename, fps=60.0, codec='mp4v', container='.avi')

This class allows frames (in the form of numpy arrays) to be written to a video file. It wraps the VideoWriter class from OpenCV.

It requires third-party packages numpy and cv2 (the latter being part of opencv-python).

Write frames with WriteFrame(). Remember to Close() when finished.

Example (capture video of one particular Stimulus):

world = Shady.World( canvas=True, gamma=-1, noise=-0.1 )
stim = world.Stimulus( sigfunc=1, siga=0.4, pp=0, cx=lambda t:t*100, atmosphere=world )

world.fakeFrameRate = 60.0     # ensures accurate slower-than-real-time animation
                               # (because .Capture() operations may be slow)
movie = Shady.VideoRecording( 'example_movie', fps=world )
world.OnClose( movie.Close )

@stim.AnimationCallback
def StimFrame( self, t ):
        movie.WriteFrame( self )   # we can pass a frame directly as a `numpy` array,
                                   # or we can pass a `Stimulus` or `World` instance
                                   # in which case its `.Capture()` method will be
                                   # called automatically
world.Run()
Parameters:
  • filename (str) – stem, or name, or path, to the video file to be saved. If filename includes a file extension, the container format is inferred from that instead of from the container argument.

  • fps (int, float, World) – frames per second. Can also use a World instance if that instance’s fakeFrameRate property has been set.

  • codec (str) – four characters that specify the FOURCC code of the codec to use. The default ‘mp4v’ is a reasonably safe cross-platform/out-of-the-box option, especially with the .avi container format. But, as with all things OpenCV, and indeed all things video-codec-related, your mileage may vary considerably. Note that ‘mp4v’ is a lossy codec. For lossless compression (and hence perfectly accurate reconstruction of your stimuli) you may need to install a third-party codec—this seems to be quite a technical task so we can’t go into detail here.

  • container (str) – file extension dictating the default video container format (used only if there is no file extension in filename).

WriteFrame(frame)

The frame argument can be a numpy array, height x width x channels, of uint8 pixel values. The number of channels should be either 3 (RGB) or 4 (RGBA). The alpha channel, if any, will be ignored.

Alternatively, frame can be a Stimulus or World instance, in which case its Capture() method is called automatically.

Note that, once the first frame is written, you should ensure that all subsequent frames have the same dimensions as the first.

Shady.Utilities.WorldConstructorCommandLine(argv=None, doc=None, **defaults)

This tool is used in our example scripts, and may be useful to you in any applications you write that are similarly launched from the command line. It creates a general-purpose CommandLine object, but then pre-configures various options to match the main arguments that can be passed to the World object constructor.

Example:

'Welcome to foo.py'
import Shady

cmdline = Shady.WorldConstructorCommandLine( canvas=True )
# Add your own custom command-line option definitions here:
hello = cmdline.Option( 'hello', default=False, type=bool, container=None )
cmdline.Help().Finalize()

world = Shady.World( **cmdline.opts )
if hello:
        import Shady.Text
        greeting = world.Stimulus( text='Hello World!' )
world.Run()

In the above example, most of the World constructor arguments now have counterparts that can be passed on the command-line when you run foo.py. For example, if you say python foo.py --screen=2 then the World will be opened on your second screen. If you say python foo.py --help then (thanks to the Help() call above) the command-line arguments will be printed and then Python will exit without creating the World. The command-line arguments all have the same default values as their counterparts in the World constructor prototype, with one exception: canvas has had its default value explicitly changed to True.

In the particular cases of width and/or height, they can be passed as named arguments (say, python foo.py --width=1920 --height=1080) or as positional arguments (python foo.py 1920 1080)

If the doc argument is left as None, this function will impolitely ransack the calling namespace for a __doc__ variable, and use that if it finds it. Pass doc='' to suppress this.