You are here: Start » Programming Reference » Macrofilters
Macrofilters
Read before: Introduction to Data Flow Programming.
For information about creating macrofilter in the user interface, please refer to Creating Macrofilters.
This article discusses more advanced details on macrofilter construction and operation.
Macrofilter Structures
There are four possible structures of macrofilters which play four different roles in the programs:
- Steps
- Variant Steps
- Tasks
- Workers
Steps
Step is the most basic macrofilter structure. Its main purpose is to make programs clean and organized.
A macrofilter of this structure simply separates a sequence of several filters that can be used as one block in many places of the program. It works exactly as if its filters were expanded in the place of each macrofilter's instance. Consequently:
- The state of the contained filters and registers is preserved between consecutive invocations.
- If a loop generator is inserted to a Step macrofilter, then this step becomes a loop generator as the whole.
- If a loop accumulator is inserted to a Step macrofilter, then this step becomes a loop accumulator as the whole.
Variant Steps
Variant macrofilters are similar to Steps, but they can have multiple alternative execution paths. Each of the paths is called a variant. At each invocation exactly one of the variants is executed – the one that is chosen depends on the value of the forking input or register (depicted with the icon), which is compared against the labels of the variants.
The type of the forking port and the labels can be: Bool, Integer, String or any enumeration type. Furthermore, any conditional or optional types can be used, and then a "Nil" variant will be available. This can be very useful for forking the program execution on the basis on whether some analysis was successful (there exists a proper value) or not (Nil).
All variants share a single external interface – inputs, outputs and also registers are the same. In consecutive iterations different variants might be executed, but they can exchange information internally through the local registers of this macrofilter. From outside a variant step looks like any other filter.
Here are some example applications of variant macrofilters:
- When some part of the program being created can have multiple alternative implementations, which we want to be chosen by the end-user. For example, there might be two different methods of detecting an object, having different trade-offs. The user might be able to choose one of the methods through a combo-box in the HMI or by changing a value in a configuration file.
- When there is an object detection step which can produce several classes of detected objects, and the next step of object recognition should depend on the detected class.
- When an inspection algorithm can have multiple results (most typically: OK and NOK) and we want to execute different communication or visualization filters for each of the cases.
- Finite State Machines.
Variant Step macrofilters correspond to the switch statement in C++.
Example 1
A Variant Step can be used to create a subprogram encapsulating image acquisition that will have two options controlled with a String-typed input:
- Variant "Files": Loading images from files of some directory.
- Variant "Camera": Grabbing images from a camera.
In the Project Explorer, the variants will be accessible after expanding the macrofilter item:
Example 2
Another application of Variant Step macrofilters is in creating optional data processing steps. For example, there may be a configurable image smoothing filter in the program and the user may be able to switch it on or off through a CheckBox in the HMI. For that we create a macrofilter like this:
NOTE: Using a variant step here is also important due to performance reasons. Even if we set inStdDev to 0, the SmoothImage_Gauss filter will still need to copy data from input to output. Also filters such as ChooseByPredicate or MergeDefault perform a full copy. On the other hand, when we use macrofilters, internal connections from macrofilter inputs to macrofilter outputs do not copy data (but they link them). If this is about heavy types of data such as images, the performance benefit can be significant.
Tasks
A Task macrofilter is much more than a separated sequence of filters. It is a logical program unit realizing a complete computing process. It can be used as a subprogram, and is usually required in more advanced situations, for example:
- When we need an explicit nested loop in the program which cannot be easily achieved with array connections.
- When we need to perform some computations before the main program loop starts.
Execution Process
The most important difference between Tasks and Steps is that a Task can perform many iterations of its filter sequence before it finishes its own single invocation. This filter sequence gets executed repeatedly until one of the contained loop generating filters signals an end-of-sequence. If there are no loop generators at all, the task will execute exactly one iteration. Then, when the program execution exits the task (returning to the parent macrofilter) the state of the task's filters and registers is destroyed (which includes closing connections with related I/O devices).
What is also very important, Tasks define the notion of an iteration of the program. During program execution there is always a single task that is the most nested and currently being executed. We call it the current task. When the execution process comes to the end of this task, we say that an iteration of a program has finished. When the user clicks the Iterate (F6) button, execution continues to the end of the current task. Also the Update Data Previews Once an Iteration refers to the same notion.
Task macrofilters can be considered an equivalent of C/C++ functions with a single while loop.
Example: Initial Computations before the Main Loop
A quite common case is when some filters have to be executed before the main loop of the program starts. Typical examples of such initial computations are: setting some camera parameters, establishing some I/O communication or loading some model definitions for external files. You can enclose all these initial operations in a macrofilter and place it in Initialize section. Programs of this kind should have the following standard structure consisting of two parts:
When the program is started, all the filters and macrofilters in the Initialize section will be executed once and then the loop in the Acquire and Process section will be executed until the program ends.
Worker Tasks
The main purpose of Worker Tasks is to allow users to process data parallelly. They also make several other new things possible:
- Parallel receiving and processing of data coming from different, not synchronized, devices
- Parallel control of output devices, that is not dependent on the cycle of the program (e.g. light up a diode every 500ms)
- Dividing our program, splitting parts of the program that need to be processed in real time from the ones that can wait for processing
- Flawless processing of HMI events
- Having additional programs in a project for unit testing of our algorithms.
- Having additional programs in a project for precomputing data that will be later used in the main program.
Every program will contain at least one Worker Task, which will act as "Main". To run programs with more than one Worker Task it is necessary to own also a Parallel Add-on license.
Creating a Worker Task
Due to its special use you cannot create a Worker Task by pressing Ctrl+Space shortcut, or directly in the program editor. The only option to do so is to make one in the Project Explorer, as shown in the Creating Macrofilters article.
Queues
Queues play an important role in communication and synchronization between different Worker Tasks in the program. They may pass most types of data, as well as Arrays between the threads. Operations on the queues are atomic meaning that once the processor starts processing them it cannot be interrupted or affected in any way. So in case that several threads would like to perform an operation, some of them would have to wait.
Please note, that different arrays are not synchronized with each other. To process complex data or pass data that needs to be synchronized (like image and its information) you should use user types. We also highly advise not to use queues as an replacement of global parameters.
Creating Queues
To create a new queue in the Project Explorer click the Create New Queue... icon. A new window will appear, allowing you to select the name and parameters of the new queue such as:
- Items Type that specifies the type of data passed by the queue
- Maximum size of the queue
- Module to which it belongs
- Access - public or private
Queue operations
In order to perform queue operations and fully manage queues in Worker Tasks, you can use several filters available in our software, namely:
- Queue_Pop - takes value from the queue without copying it. Waits infinitely if the queue is empty. This operation only reads data and does not copy it, so it is performed instantly.
- Queue_Pop_Timeout - takes value from the queue without copying it. Waits for the time specified in Timeout if the queue is empty. This operation only reads data and does not copy it, so it is performed instantly.
- Queue_Peek - returns specified element of the queue without removing it. Waits infinitely if the queue is empty.
- Queue_Peek_Timeout - returns specified element of the queue without removing it. Waits for the time specified in Timeout if the queue is empty.
- Queue_Push - adds element to the queue. This operation copies data, so it may take more time compared to the others.
- Queue_Size - returns the size of the queue.
- Queue_Flush - clears the queue. Does not block data flow.
Macrofilters Ports
Inputs
Macrofilter inputs are set before execution of the macrofilter is started. In the case of Task macrofilters the inputs do not change values in consecutive iterations.
Outputs
Macrofilter outputs can be set from connections with filters or with global parameters as well as from constant values defined directly in the outputs.
In the case of Tasks (with many iterations) the value of a connected output is equal to the value that was computed the latest. One special case to be considered is when there is a loop generator that exits in the very first iteration. As there is no "the latest" value in this case, the output's default value is used instead. This default value is part of the output's definition. Furthermore, if there is a conditional connection at an output, Nil values cause the previously computed value to be preserved.
Registers
Registers make it possible to pass information between consecutive iterations of a Task. They can also be defined locally in Steps and in Variant Steps, but their values will still be set exactly in the same way as if they belonged to the parent Task:
- Register values are initialized to their defaults when the Task starts.
- Register values are changed at the end of each iteration.
In case the of variant macrofilters, register values for the next iteration are defined separately in each variant. Very often some registers should just preserve their values in most of the variants. This can be done by creating long connections between the prev and next ports, but usually a more convenient way is to disable the next port in some variants.
Please note, that in many cases it is possible to use accumulating filters (e.g. AccumulateElements, AddIntegers_OfLoop and other OfLoop) instead of registers. Each of these filters has an internal state that stores information between consecutive invocations and does not require explicit connections for that. Thus, as a method this is simpler and more readable, it should be preferred. Registers, however, are more general.
Yet another alternative to macrofilter registers is the prev operator in the formula blocks.
Registers correspond to variables in C/C++. In FabImage Studio, however, registers follow the single assignment rule, which is typical for functional and data-flow programming languages. It says that a variable can be assigned at most once in each iteration. Programs created this way are much easier to understand and analyze.
Example: Computing Greatest Common Denominator
With Task macrofilters and registers it is possible to create very complex algorithms. For example, here is a comparison of how the Greatest Common Denominator function can be implemented in FabImage Studio and in C/C++:
int gcd(int inA, int inB) { int a = inA; int b = inB; while (a != 0) { int tmp = a; a = b % a; b = tmp; } return b; } |
Sequence of Filter Execution
It can be assumed that filters are always executed from top to bottom. Internally the order of filter execution can be changed and even some filters may operate in parallel to take advantage of multi-core processors, but this happens only if the top-down execution produces exactly the same results. Specifically, all I/O functions will never be parallelized, so if one I/O operation is above another, it will always be executed first.
Previous: Generic Filters | Next: Formulas |