Writing components
A successful tool is one that was used to do something undreamt of by its author.
—Stephen C. Johnson
It is unlikely that you will be able to do everything you want to do with Pyctools “out of the box”. Sooner or later you will find that there isn’t a component for an image processing operation you need to solve your problem. The solution is to extend Pyctools by writing a new component.
Preliminaries
Integrating your new component(s) into a Pyctools installation will be easier if you set up your build environment as described below. The Pyctools source files include example files to help with this.
Namespaces
Python namespace packages allow the Python import
statement to import modules in the same namespace hierarchy from different locations.
For example, if you have installed both pyctools and pyctools-pal on your computer then you should have Pyctools components in two different directories:
jim@Brains:~$ ls -l /usr/lib64/python2.7/site-packages/pyctools.core-0.1.2-py2.7-linux-x86_64.egg/pyctools/components/
total 44
-rw-r--r-- 1 root root 2098 Nov 18 08:58 arithmetic.py
-rw-r--r-- 1 root root 2121 Nov 18 08:58 arithmetic.pyc
drwxr-xr-x 2 root root 4096 Nov 18 08:58 colourspace
-rw-r--r-- 1 root root 902 Nov 18 08:58 __init__.py
-rw-r--r-- 1 root root 283 Nov 18 08:58 __init__.pyc
drwxr-xr-x 2 root root 4096 Nov 18 08:58 interp
drwxr-xr-x 2 root root 4096 Nov 18 08:58 io
drwxr-xr-x 2 root root 4096 Nov 18 08:58 modulate
drwxr-xr-x 2 root root 4096 Nov 18 08:58 plumbing
drwxr-xr-x 2 root root 4096 Nov 18 08:58 qt
drwxr-xr-x 2 root root 4096 Nov 18 08:58 zone
jim@Brains:~$ ls -l /usr/lib/python2.7/site-packages/pyctools.pal-0.1.0-py2.7.egg/pyctools/components/
total 12
-rw-r--r-- 1 root root 902 Nov 12 14:48 __init__.py
-rw-r--r-- 1 root root 267 Nov 12 14:48 __init__.pyc
drwxr-xr-x 2 root root 4096 Nov 12 14:48 pal
jim@Brains:~$
When a Python program imports from pyctools.components
both these directories are searched for modules or subpackages.
The command import pyctools.components.io.videofilereader
will use site-packages/pyctools.core
whilst import pyctools.components.pal.decoder
will use site-packages/pyctools.pal
.
The program user needn’t know that the two import
statements are using files from different installation packages.
When you write your components you should follow a similar naming structure and make them part of the pyctools.components
hierarchy.
This will ensure that they are included in the component list shown in the pyctools-editor
visual editor.
Choosing a name
This is well known to be one of the most important parts of software writing.
If your new component fits the existing Pyctools component hierarchy then it makes a lot of sense to add it to an existing package.
For example, if you’re writing a component to read a common file type you should probably add it to pyctools.components.io
.
Alternatively you may prefer to group all your components under one package.
Perhaps you work for a company that wants to make its ownership explicit.
In this case you might want to add your new file reader to pyctools.components.bigcorp.io
.
(Substitute your company name for bigcorp
, but keep it lower case. All Python packages and modules should have lower case names.)
There is just one golden rule – don’t use a (complete) module name that’s already in use.
For example, don’t use pyctools.components.io.videofilereader
.
Consider pyctools.components.bigcorp.io.videofilereader
or pyctools.components.io.foofilereader
instead.
Build environment
The easiest way to get started is to copy the src/examples/simple
directory, edit the setup.py
file and try building and installing.
If this works you should have a new Flip
component available in the pyctools-editor
program.
The src/examples/simple/test_flip.py
script demonstrates the effect of the Flip
component.
Having successfully set up your build environment you are ready to start writing your new component.
“Transformer” components
The most common Pyctools components have one input and one output.
They do nothing until a frame is received, then they “transform” that input frame into an output frame and send it to their output.
Pyctools provides a Transformer
base class to make it easier to write transformer components.
Consider the Flip
example component.
This listing shows all the active Python code:
1__all__ = ['Flip']
2
3import PIL.Image
4
5from pyctools.core.base import Transformer
6from pyctools.core.config import ConfigEnum
7
8class Flip(Transformer):
9 def initialise(self):
10 self.config['direction'] = ConfigEnum(choices=('vertical', 'horizontal'))
11
12 def transform(self, in_frame, out_frame):
13 self.update_config()
14 direction = self.config['direction']
15 if direction == 'vertical':
16 flip = PIL.Image.FLIP_TOP_BOTTOM
17 else:
18 flip = PIL.Image.FLIP_LEFT_RIGHT
19 in_data = in_frame.as_PIL()
20 out_frame.data = in_data.transpose(flip)
21 audit = out_frame.metadata.get('audit')
22 audit += 'data = Flip(data)\n'
23 audit += ' direction: %s\n' % direction
24 out_frame.metadata.set('audit', audit)
25 return True
Line 1 is important.
The module’s __all__
value is used by pyctools-editor
to determine what components a module provides.
The initialise
method (lines 9-10) is called by the component’s constructor.
It is here that you add any configuration values that your component uses.
The main part of the component is the transform
method (lines 12-25).
This is called each time there is some work to do, i.e. an input frame has arrived and an output frame is available from the ObjectPool
.
A component’s configuration can be changed while it is running.
This is done via a threadsafe queue.
The update_config
method (line 13) gets any new configuration values from the queue so each time the component does any work it is using the most up-to-date config.
The out_frame
result is already initialised with a copy of the in_frame
’s metadata and a link to its image data.
The in_frame.as_PIL()
call (line 19) gets the input image data as a PIL.Image
object, converting it if necessary.
(The frame’s as_numpy
method could be used to get numpy data instead.)
Line 20 sets the output frame data to a new PIL image. Note that you must never modify the input frame’s data. Because of the parallel nature of Pyctools that same input frame may also be used by another component.
Finally lines 21-24 add some text to the output frame’s “audit trail” metadata and line 25 returns True
to indicate that processing was successful.
“Passthrough” components
Components that don’t appear to need an output (e.g. a video display or file writer) are usually implemented as “passthrough” components – the input data is passed straight through to the output.
This conveniently allows a stream of frames to be simultaneously saved in a file and displayed in a window by pipelining a VideoFileWriter
with a QtDisplay
component.
The passthrough component’s transform
method saves or displays the input frame, but need not do anything else.
The base class takes care of creating the output frame correctly.
Source components
Components such as file readers have an output but no inputs.
They use the Component
base class directly.
In most cases they use an output frame pool and generate a new frame each time a frame object is available from the pool.
See the ZonePlateGenerator
source code for an example.
Comments or questions? Please email jim@jim-easterbrook.me.uk.