- 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.
To convert your previous scripts, python 3 has a script called 2to3.
Here are the versions of python OpenViBE has been tested with:
Windows:
For OpenVibe up to 2.2.0:
- Python 2.7.3 32bits for 32bit OpenViBE on Windows 7
- Python 2.7.3 64bits for 64bit OpenViBE (an option since 2.2.0) on Windows 7
From OpenViBE 3.0.0:
- Python 3.7.8 64bits for 64bit OpenViBE on Windows 10
- Python 3.7.8 32bits for 32bit OpenViBE on Windows 10
From OpenViBE 3.6.0:
- Python 3.10.11 64bits for 64bit OpenViBE on Windows 10
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.
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:
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.
# 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.
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.
# 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).
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.