Tutorial – Level 2 – Using Python with OpenViBE

  • NB: last partial update for OpenViBE 1.2.0 (24.may.2016, minor tweak on 22.jan.2021)

Note from the author : the Python plugin for OpenViBE has been developed for the researchers more experienced with Python scripting than C++ programming. This plugin shoud be further tested to be considered as stable, especially by experienced Python users. Please send us your feedback and suggestions of improvement: bug reports, usability feedback, documentation requests, etc. are all warmly welcome.

Introduction

OpenViBE is shipped with a box capable of executing Python code. The Python Scripting box is available under the category “Scripting“. This box can have as many inputs, outputs or settings as needed. By implementing 3 scripts for initialization, process and uninitialization functions, user is able to process or produce any kind of data from/to OpenViBE.

The design of this box is strongly related to the OpenViBE design (box structure, stream structure, etc.). Interested reader should look at the developer documentation for more details about the OpenViBe architecture. It may come useful if you want to understand the architecture of the Python box.

Note: One Python Interpreter is created for all the boxes in the same scenario.

This box relies on Python, however we do not provide Python bundled with OpenViBE. To compile or use the Python Scripting box you will have to download and install Python manually before. Note that Python is now a major package for many Linux distributions and thus will be directly included in such systems. Please note that if you uninstall Python incorrectly on Windows (or you move it somewhere), the box may find Python but then may crash as Python won’t have a complete, accessible installation.

From OpenViBE 3.0.0, we migrated to Python 3 (Python 2 is no longer officially supported since January 1, 2020).
To convert your previous scripts, python 3 has a script called 2to3.

Here are the versions of python OpenViBE has been tested with:

Windows:

Please note that the ‘bitness’ of Python must match the bitness of your OpenViBE. If you downloaded 32bit OpenViBE, you need 32bit Python. All OpenViBE releases up to 2.1.0 were 32bit only.

For OpenVibe up to 2.2.0:

From OpenViBE 3.0.0:

From OpenViBE 3.6.0:

Python Scripting box not listed in Designer ?

If the Python scripting box does not appear in the Scripting folder of the Designer boxes list, it’s because the Python environment was not found.
To make sure OpenViBE finds it, do the following:

  • Open Control Panel -> System -> Advanced System Settings -> Environment Variables
  • Modify the Path variable: add the path to your python install to it (e.g. C:\Program Files\Python37).

Linux :

For OpenViBE up to 2.2.0:

  • Python 2.7 (from package manager) on Ubuntu and Fedora

From OpenViBE 3.0.0:

  • Python 3.7 (from package manager) on Ubuntu and Fedora

Note that on Fedora 17 and Ubuntu 12.04, the Python development libraries are not included with the operating system. The OpenViBE dependency installer (linux-install_dependencies) installs the package python-dev for you.

You can install any packages for Python and import them in your scripts. We strongly recommend you to install the numpy package, for signal processing and matrix manipulation.

On Windows, if you ever uninstall or move Python manually (i.e. just by removing, renaming or moving the Python folder) some incorrect registry keys will be still available, and OpenViBE will find a ghost Python. This may cause the Designer to fail loading. If this case happens to you, please uninstall Python properly. You may also delete the python project DLL in the bin/ folder of OpenViBE (openvibe-plugins-python.dll) to solve the problem.

Compiling the ‘contrib/plugins/processing/python’ project

To compile the box in the python project, you must have a valid installation of Python on your computer. CMake tries to find the python installation path by calling the script cmake-modules/FindthirdPartyPython.cmake.

When building the software, the process should find Python and all its libraries, as shown in the following build log example:

 (click here to show content)
 (click here to hide content)

Configuring and building contrib\plugins\python …

—   Found OpenViBE…
— …
—   Found OpenViBE plugins global defines…
—   Found Python…
—      [ OK ] Third party lib C:/Program Files (x86)/Python27/libs/bz2.lib
—      [ OK ] Third party lib C:/Program Files (x86)/Python27/libs/pyexpat.lib
—      [ OK ] Third party lib C:/Program Files (x86)/Python27/libs/python27.lib
—      [ OK ] Third party lib C:/Program Files (x86)/Python27/libs/select.lib
—      [ OK ] Third party lib C:/Program Files (x86)/Python27/libs/unicodedata.lib
—      [ OK ] Third party lib C:/Program Files (x86)/Python27/libs/winsound.lib
—      [ OK ] Third party lib C:/Program Files (x86)/Python27/libs/_bsddb.lib
—      [ OK ] Third party lib C:/Program Files (x86)/Python27/libs/_ctypes.lib
—      [ OK ] Third party lib C:/Program Files (x86)/Python27/libs/_ctypes_test.lib
—      [ OK ] Third party lib C:/Program Files (x86)/Python27/libs/_elementtree.lib
—      [ OK ] Third party lib C:/Program Files (x86)/Python27/libs/_hashlib.lib
—      [ OK ] Third party lib C:/Program Files (x86)/Python27/libs/_msi.lib
—      [ OK ] Third party lib C:/Program Files (x86)/Python27/libs/_multiprocessing.lib
—      [ OK ] Third party lib C:/Program Files (x86)/Python27/libs/_socket.lib
—      [ OK ] Third party lib C:/Program Files (x86)/Python27/libs/_sqlite3.lib
—      [ OK ] Third party lib C:/Program Files (x86)/Python27/libs/_ssl.lib
—      [ OK ] Third party lib C:/Program Files (x86)/Python27/libs/_testcapi.lib
—      [ OK ] Third party lib C:/Program Files (x86)/Python27/libs/_tkinter.lib
—   Configuring done
—   Generating done

If the build process does not find a valid Python installation, the box won’t be built with the project. An empty dynamic library will be produced, which will result in the following message in the console when executing the OpenViBE Designer :

[WARNING] File [../bin/openvibe-plugins-python-dynamic.dll] is not a plugin module (error: XXXXXXX)

Where the error message is related to a missing module (this message may vary from on distribution to another). This message has no negative consequences for using OpenViBE except that the Python scripting box will not be usable.

The Python Scripting box

The Python Scripting box is available in the Designer in the category Scripting.

Here is the different settings and how to set it correctly :

  • Clock frequency (in Hertz) : defines the frequency on which the box will be called by OpenViBE, from 1 to 128 Hz. Default value is 64Hz, you can lower it if you experience bad performance when executing the box. Note that the python box will also call the process() function every time there is an input chunk, even if the chunk is empty (this can happen with stimulation streams). If you need to know if the process() call is clock triggered, you can test on the python script side if there is an input pending or not.
  • Script : The Python script file.

You can add as many inputs and outputs as you need. So far, 3 stream types are available : Streamed Matrix, Signal and Stimulations. You can also add new settings in the box, they will be available in the Python box object as well.

OpenViBE constructs an OVBox class object in Python with different attributes (inputs, outputs and settings fro example). This class has different predefined methods:

  • initialize(self) : called once, when starting the scenario
  • process(self) : called on each box clock tick
  • uninitialize(self) : called once, when stopping the scenario

These 3 functions do nothing by default. The user script has to construct a new box class that inherits from OVBox, and rewrite these 3 functions so they actually do something. The Python box instance manipulated by OpenViBE during the execution is named ‘box’. This variable should be created by the user in his script (e.g. box = MyOVBox())

NB : other functions are defined, used internally by the Python Scripting box. Interested user may refer to the base Python file openvibe.py in share/openvibe/plugins/python for further details.

You can use more than one Python Scripting box in the same scenario, they will share the same Python interpreter but not the same box instance.

The OpenViBE-Python framework

The Python Scripting box relies on a set of Python classes to work. All these classes are defined in share/openvibe/plugins/python/openvibe.py.

This complete framework allows the user to manipulate streamed matrix, signal or stimulations streams, to process input and produce output.

Box

The OpenViBE box in Python is defined in the class OVBox. Here is the information you need to know about this class (for more information, please look at the implementation in openvibe.py) :

class OVBox(object) : the default box class
> .input   : list - the list of input buffers in which chunks will be added by openViBE during the execution.
> .output  : list -  the list of output buffers in which chunks will be added by the user script during execution.
> .setting : dict - The dictionary of settings (dict-key is the setting name, dict-value is the setting value)
> .var     : dict - A personal dictionary available for the user. Default is empty.

Methods 
> getClock()       : return the box clock frequency as defined by the user in the box settings.
> getCurrentTime() : returns the current OpenViBE time in seconds.
                     This value is automatically updated by the OpenViBE box every time is is called.
> initialize()     : dummy method 
> process()        : dummy method
> uninitialize()   : dummy method

You must define a new Box class that inherits from OVBox, and implement in it your version of the 3 functions initialize, process and uninitialize.

Chunk

Every block of data received or send by this box (headers, buffers and end), inherits from the basic class OVChunk:

class OVChunk(object) : a generic chunk, a dummy container for any kind of data.
 > .startTime : float - Start time of this data block
 > .endTime   : float - End time of this data block

Methods
> OVChunk(startTime, endTime) : constructor

Streamed Matrix

We specialize the OVChunk class to handle Streamed Matrix stream : we need a header, with the matrix structure information, a buffer containing the matrix values, and an end to notify end-of-stream.

class OVStreamedMatrixHeader(OVChunk) : the header chunk for the streamed matrix stream
 > .dimensionSizes : list - Sizes of the dimensions in the matrix
 > .dimensionLabels : list - Names of the dimensions in the matrix

Methods
> OVStreamedMatrixHeader(startTime, endTime, dimensionSizes, dimensionLabels) : constructor
> int getDimensionCount()     : returns the number of dimensions of the matrix
> int getBufferElementCount() : returns the number of elements in the matrix (i.e. product of every dimension sizes)
class OVStreamedMatrixBuffer(OVChunk, list) : the buffer chunk for the streamed matrix stream

Methods
> OVStreamedMatrixBuffer(startTime, endTime, bufferElements) : constructor.
class OVStreamedMatrixEnd(OVChunk) : the end chunk for the streamed matrix stream
> this class has no specific behavior, as the end buffer is only a simple notification

Signal

The signal stream is a specialization of the streamed matrix stream. It adds the sampling rate information.

class OVSignalHeader(OVStreamedMatrixHeader) : the header chyunk for the signal stream
> .samplingRate : int - Signal sampling frequency

Methods
OVSignalHeader(startTime, endTime, dimensionSizes, dimensionLabels, samplingRate) : constructor
class OVSignalBuffer(OVStreamedMatrixBuffer) : the buffer chunk of a signal stream
> this class has no specific behavior. Signal buffers are regular Streamed Matrix buffers.
class OVSignalEnd(OVChunk) : the end chunk of a signal stream
> this class has no specific behavior, as the end buffer is only a simple notification

Stimulation

Stimulation stream needs again a header and an end to notify start and end-of-stream. In this case the buffers are Stimulation Sets which contain Stimulation objects.

class OVStimulation(object) : a stimulation object
> .identifier : int   - Stimulation identifier, as in OpenViBE
> .date       : float - Stimulation date in seconds
> .duration   : float - Stimulation duration in seconds

Methods
OVStimulation(identifier, date, duration) : constructor
class OVStimulationHeader(OVChunk) : the header chunk for the stimulation stream
> this class has no specific behavior, as the stimulation header has no particular information.
class OVStimulationSet(OVChunk, list) : the buffer chunk of the stimulation stream

Methods
> OVStimulationSet(startTime, endTime) : constructor (empty set)
> append(stimulation) : append the stimulation in the list.
                        If the item is not an OVStimulation, this method will raise an exception.
class OVStimulationEnd(OVChunk) : the end chunk of a stimulation stream
> this class has no specific behavior, as the end buffer is only a simple notification

Tutorial 1 : Signal average

In this tutorial we will simply filter a signal stream to output one channel : the average of every channel. Please make sure you have installed the numpy package for Python as we will use numpy functions to simplify the scripts.

This box will have one input (signal) and one output (signal).

Open the folder share/openvibe/scenarios/box-tutorials/python/scripts. Open file python-signal-average.py.

See below for comments.

 python-signal-average.py(click here to show content)
 python-signal-average.py(click here to hide content)

 # we use numpy to compute the mean of an array of values
 import numpy

 # let's define a new box class that inherits from OVBox
 class MyOVBox(OVBox):
    def __init__(self):
    OVBox.__init__(self)
    # we add a new member to save the signal header information we will receive
    self.signalHeader = None

    # The process method will be called by openvibe on every clock tick
    def process(self):
       # we iterate over all the input chunks in the input buffer
       for chunkIndex in range( len(self.input[0]) ):
          # if it's a header we save it and send the output header (same as input, except it has only one channel named 'Mean'
          if(type(self.input[0][chunkIndex]) == OVSignalHeader):
             self.signalHeader = self.input[0].pop()
             outputHeader = OVSignalHeader(
             self.signalHeader.startTime,
             self.signalHeader.endTime,
             [1, self.signalHeader.dimensionSizes[1]],
             ['Mean']+self.signalHeader.dimensionSizes[1]*[''],
             self.signalHeader.samplingRate)
             self.output[0].append(outputHeader)

          # if it's a buffer we pop it and put it in a numpy array at the right dimensions
          # We compute the mean and add the buffer in the box output buffer
          elif(type(self.input[0][chunkIndex]) == OVSignalBuffer):
             chunk = self.input[0].pop()
             numpyBuffer = numpy.array(chunk).reshape(tuple(self.signalHeader.dimensionSizes))
             numpyBuffer = numpyBuffer.mean(axis=0)
             chunk = OVSignalBuffer(chunk.startTime, chunk.endTime, numpyBuffer.tolist())
             self.output[0].append(chunk)
          # if it's a end-of-stream we just forward that information to the output
          elif(type(self.input[0][chunkIndex]) == OVSignalEnd):
             self.output[0].append(self.input[0].pop())

 # Finally, we notify openvibe that the box instance 'box' is now an instance of MyOVBox.
 # Don't forget that step !!
 box = MyOVBox()

As you can see, this class defines only the process function. We don’t need to add a specific behavior when initializing or uninitializing the box.

Tutorial 2 : Sinus Oscillator

In this tutorial we use Python to produce a sinusoidal signal. This script will use Numpy.

The Python box must be configured with 1 output (signal) and 3 new settings (integers).

User can set the number of channel, the sampling frequency and the sample count per epoch.

Open file python-sinus-oscillator.py.

 python-sinus-oscillator.py(click here to show content)
 python-sinus-oscillator.py(click here to hide content)

import numpy

class MyOVBox(OVBox):
   def __init__(self):
      OVBox.__init__(self)
      self.channelCount = 0
      self.samplingFrequency = 0
      self.epochSampleCount = 0
      self.startTime = 0.
      self.endTime = 0.
      self.dimensionSizes = list()
      self.dimensionLabels = list()
      self.timeBuffer = list()
      self.signalBuffer = None
      self.signalHeader = None

   # this time we also re-define the initialize method to directly prepare the header and the first data chunk
   def initialize(self):
      # settings are retrieved in the dictionary
      self.channelCount = int(self.setting['Channel count'])
      self.samplingFrequency = int(self.setting['Sampling frequency'])
      self.epochSampleCount = int(self.setting['Generated epoch sample count'])

      #creation of the signal header
      for i in range(self.channelCount):
         self.dimensionLabels.append( 'Sinus'+str(i) )
      self.dimensionLabels += self.epochSampleCount*['']
      self.dimensionSizes = [self.channelCount, self.epochSampleCount]
      self.signalHeader = OVSignalHeader(0., 0., self.dimensionSizes, self.dimensionLabels, self.samplingFrequency)
      self.output[0].append(self.signalHeader)

      #creation of the first signal chunk
      self.endTime = 1.*self.epochSampleCount/self.samplingFrequency
      self.signalBuffer = numpy.zeros((self.channelCount, self.epochSampleCount))
      self.updateTimeBuffer()
      self.updateSignalBuffer()

   def updateStartTime(self):
      self.startTime += 1.*self.epochSampleCount/self.samplingFrequency

   def updateEndTime(self):
      self.endTime = float(self.startTime + 1.*self.epochSampleCount/self.samplingFrequency)

   def updateTimeBuffer(self):
      self.timeBuffer = numpy.arange(self.startTime, self.endTime, 1./self.samplingFrequency)

   def updateSignalBuffer(self):
      for rowIndex, row in enumerate(self.signalBuffer):
         self.signalBuffer[rowIndex,:] = 100.*numpy.sin( 2.*numpy.pi*(rowIndex+1.)*self.timeBuffer )

   def sendSignalBufferToOpenvibe(self):
      start = self.timeBuffer[0]
      end = self.timeBuffer[-1] + 1./self.samplingFrequency
      bufferElements = self.signalBuffer.reshape(self.channelCount*self.epochSampleCount).tolist()
      self.output[0].append( OVSignalBuffer(start, end, bufferElements) )

   # the process is straightforward
   def process(self):
      start = self.timeBuffer[0]
      end = self.timeBuffer[-1]
      if self.getCurrentTime() >= end:
         self.sendSignalBufferToOpenvibe()
         self.updateStartTime()
         self.updateEndTime()
         self.updateTimeBuffer()
         self.updateSignalBuffer()

   # this time we also re-define the uninitialize method to output the end chunk.
   def uninitialize(self):
      end = self.timeBuffer[-1]
      self.output[0].append(OVSignalEnd(end, end))            

box = MyOVBox()

Tutorial 3 : Clock Stimulator

In this example we produce a stimulation on every clock tick.

The box uses one output (stimulation) and one setting (the stimulation identifier to send).

Open file python-clock-stimulator.py.

 python-clock-stimulator.py(click here to show content)
 python-clock-stimulator.py(click here to hide content)

# We construct a box instance that inherits from the basic OVBox class
class MyOVBox(OVBox):
    # the constructor creates the box and initializes object variables
   def __init__(self):
      OVBox.__init__(self)
      self.stimLabel = None
      self.stimCode = None

   # the initialize method reads settings and outputs the first header
   def initialize(self):
                # the stim label is taken from the box setting
      self.stimLabel = self.setting['Stimulation']
      # we get the corresponding code using the OpenViBE_stimulation dictionnary
      self.stimCode = OpenViBE_stimulation[self.stimLabel]
      # we append to the box output a stimulation header. This is just a header, dates are 0.
      self.output[0].append(OVStimulationHeader(0., 0.))

   def process(self):
                # During each process call we produce a stimulation
                # A stimulation set is a chunk which starts at current time and end time is the time step between two calls
      stimSet = OVStimulationSet(self.getCurrentTime(), self.getCurrentTime()+1./self.getClock())
                # the date of the stimulation is simply the current openvibe time when calling the box process
      stimSet.append(OVStimulation(self.stimCode, self.getCurrentTime(), 0.))
      self.output[0].append(stimSet)

   def uninitialize(self):
                # we send a stream end.
      end = self.getCurrentTime()
      self.output[0].append(OVStimulationEnd(end, end))            

box = MyOVBox()

Tutorial 4 : Stimulation Listener

In this example we simply display all the stimulations sent to the box. The intent is to show how it is possible to get input stimulations in python.

The box has only one input (stimulation).

 python-stimulation-listener.py(click here to show content)
 python-stimulation-listener.py(click here to hide content)

class MyOVBox(OVBox):
   def __init__(self):
      OVBox.__init__(self)

   def initialize(self):
      # nop
      return

   def process(self):
      for chunkIndex in range( len(self.input[0]) ):
         chunk = self.input[0].pop()
         if(type(chunk) == OVStimulationSet):
            # We move through all the stimulation received in the StimulationSet and
            # we print their date and identifier
            for stimIdx in range(len(chunk)):
               stim=chunk.pop();
               print 'At time ', stim.date, ' received stim ', stim.identifier
         else:
            print 'Received chunk of type ', type(chunk), " looking for StimulationSet"

      return

   def uninitialize(self):
      # nop
      return

box = MyOVBox()

Execution

The first three scripts are used in the example scenario python-sinus-oscillator in share/openvibe/scenarios/box-tutorials/python/scenarios.

The “sinus oscillator” generates a 4 channel sinusoid signal, which is filtered by the “mean” script. The clock stimulator outputs the stimulation Label_00 every second.

This entry was posted in Tutorials and tagged , . Bookmark the permalink.