Overview
The IFilter interface is the base class for all filter plugins. Filters process raw tracking data to reduce jitter, apply smoothing, implement deadzones, and perform other transformations before the data reaches the output protocol.
Interface Definition
struct IFilter : module_status_mixin
{
IFilter ();
~IFilter () override ;
// Core methods (must implement)
virtual void filter ( const double * input , double * output ) = 0 ;
// Optional methods
virtual void center () {}
// From module_status_mixin
virtual module_status initialize () = 0 ;
// Deleted methods (non-copyable)
IFilter ( const IFilter & ) = delete ;
IFilter & operator = ( const IFilter & ) = delete ;
};
Methods
initialize()
virtual module_status initialize () = 0 ;
Initializes the filter and validates configuration.
Returns status_ok() on success, or error(message) on failure.
Description:
Called once when the filter is created
Validate settings and parameters
Initialize internal state, buffers, or data structures
Usually just returns status_ok()
Example:
module_status MyFilter :: initialize ()
{
// Validate configuration
if ( settings . smoothing_factor < 0 || settings . smoothing_factor > 1 )
return error ( "Smoothing factor must be between 0 and 1" );
// Initialize state
reset_state ();
return status_ok ();
}
filter()
virtual void filter ( const double * input , double * output ) = 0 ;
Processes tracking data and writes filtered result.
Input pose array: [TX, TY, TZ, Yaw, Pitch, Roll] in cm/degrees
Output pose array to fill with filtered data in same format
This method is called at 250Hz. Keep it fast and deterministic.
Description:
Called at 250Hz from tracking pipeline thread
Read from input array, write to output array
Apply smoothing, deadzone, or other transformations
Manage your own timing (dt) if needed
Don’t block or perform heavy computation
Data format:
input[TX]: X translation in centimeters
input[TY]: Y translation in centimeters
input[TZ]: Z translation in centimeters
input[Yaw]: Yaw rotation in degrees (-180 to 180)
input[Pitch]: Pitch rotation in degrees (-90 to 90)
input[Roll]: Roll rotation in degrees (-180 to 180)
Example implementations:
filter-accela/ftnoir_filter_accela.cpp
Example: Simple Low-Pass Filter
Example: Deadzone Filter
void accela :: filter ( const double* input , double * output )
{
static constexpr double full_turn = 360.0 ;
static constexpr double half_turn = 180.0 ;
// First run initialization
if (first_run) [[ unlikely ]]
{
first_run = false ;
for ( int i = 0 ; i < 6 ; i ++ )
{
output [i] = input [i];
last_output [i] = input [i];
}
t . start ();
return ;
}
const double rot_thres = s . rot_smoothing ;
const double pos_thres = s . pos_smoothing ;
const double dt = t . elapsed_seconds ();
t . start ();
const double rot_dz = s . rot_deadzone ;
const double pos_dz = s . pos_deadzone ;
// Process rotation (Yaw, Pitch, Roll)
for ( unsigned i = 3 ; i < 6 ; i ++ )
{
double d = input [i] - last_output [i];
// Handle wrap-around at 180 degrees
if ( fabs (d) > half_turn)
d -= copysign (full_turn, d);
// Apply deadzone
if ( fabs (d) > rot_dz)
d -= copysign (rot_dz, d);
else
d = 0 ;
deltas [i] = d / rot_thres;
}
// Apply rotation spline curve
do_deltas ( & deltas [Yaw], & output [Yaw], [ this ]( double x ) {
return spline_rot . get_value_no_save (x);
});
// Process position (TX, TY, TZ)
for ( unsigned i = 0 ; i < 3 ; i ++ )
{
double d = input [i] - last_output [i];
// Apply deadzone
if ( fabs (d) > pos_dz)
d -= copysign (pos_dz, d);
else
d = 0 ;
deltas [i] = d / pos_thres;
}
// Apply position spline curve
do_deltas ( & deltas [TX], & output [TX], [ this ]( double x ) {
return spline_pos . get_value_no_save (x);
});
// Integrate deltas with dt and handle wrap-around
for ( unsigned k = 0 ; k < 6 ; k ++ )
{
output [k] *= dt;
output [k] += last_output [k];
// Handle rotation wrap-around
if (k >= Yaw && fabs ( output [k]) > half_turn)
output [k] -= copysign (full_turn, output [k]);
last_output [k] = output [k];
}
}
center()
Called when user requests to center/reset tracking.
Description:
Called from UI thread when center hotkey pressed
Reset internal filter state if needed
Clear history buffers
Default implementation does nothing
Example:
void MyFilter :: center ()
{
// Reset filter state
first_run = true ;
// Clear history
for ( int i = 0 ; i < 6 ; i ++ )
{
last_output [i] = 0 ;
history [i]. clear ();
}
}
Dialog Interface
struct IFilterDialog : public BaseDialog
{
virtual void register_filter ( IFilter * filter ) = 0 ;
virtual void unregister_filter () = 0 ;
};
register_filter()
virtual void register_filter ( IFilter * filter ) = 0 ;
Receives a pointer to the active filter instance.
Pointer to the running filter instance
Description:
Called from UI thread when filter starts
Store pointer for runtime interaction (optional)
Usually implemented as empty function
Example:
void MyFilterDialog :: register_filter ( IFilter * filter )
{
this -> filter = filter;
}
unregister_filter()
virtual void unregister_filter () = 0 ;
Called when filter is about to be destroyed.
Example:
void MyFilterDialog :: unregister_filter ()
{
filter = nullptr ;
}
Complete Example
#pragma once
#include "api/plugin-api.hpp"
#include "compat/timer.hpp"
#include <deque>
class MyFilter : public IFilter
{
public:
MyFilter ();
module_status initialize () override ;
void filter ( const double* input , double* output ) override ;
void center () override ;
private:
struct Settings {
double smoothing = 0.5 ;
double deadzone = 0.5 ;
} settings;
double last_output [ 6 ] = {};
std ::deque < double > history [ 6 ];
Timer timer;
bool first_run = true ;
};
class MyFilterDialog : public IFilterDialog
{
Q_OBJECT
public:
MyFilterDialog ();
void register_filter ( IFilter * f ) override { filter = f; }
void unregister_filter () override { filter = nullptr ; }
private:
IFilter * filter = nullptr ;
};
class MyFilterMetadata : public Metadata
{
Q_OBJECT
QString name () override { return tr ( "My Filter" ); }
QIcon icon () override { return QIcon ( ":/images/filter.png" ); }
};
Common Filter Patterns
Low-Pass Filter (Smoothing)
Reduces high-frequency jitter using exponential moving average. void filter ( const double* input , double* output )
{
const double alpha = 0.3 ; // Smoothing factor
for ( int i = 0 ; i < 6 ; i ++ )
{
// EWMA: output = alpha * input + (1-alpha) * prev
output [i] = alpha * input [i] +
( 1 - alpha) * prev_output [i];
prev_output [i] = output [i];
}
}
Ignores small movements to reduce micro-jitter. void filter ( const double* input , double* output )
{
const double pos_dz = 0.5 ; // cm
const double rot_dz = 1.0 ; // degrees
for ( int i = 0 ; i < 6 ; i ++ )
{
double delta = input [i] - last_input [i];
double threshold = (i < 3 ) ? pos_dz : rot_dz;
if ( fabs (delta) > threshold)
{
output [i] = input [i];
last_input [i] = input [i];
}
else
{
output [i] = last_input [i];
}
}
}
Acceleration-Based Filter
Applies more smoothing when moving slowly, less when moving fast. void filter ( const double* input , double* output )
{
const double dt = timer . elapsed_seconds ();
timer . start ();
for ( int i = 0 ; i < 6 ; i ++ )
{
// Calculate velocity
double velocity = ( input [i] - last_input [i]) / dt;
// Map velocity to smoothing amount
double speed = fabs (velocity);
double smoothing = 1.0 / ( 1.0 + speed * gain);
// Apply adaptive smoothing
output [i] = smoothing * last_output [i] +
( 1 - smoothing) * input [i];
last_input [i] = input [i];
last_output [i] = output [i];
}
}
Optimal estimator combining measurements with predictions. void filter ( const double* input , double* output )
{
const double dt = timer . elapsed_seconds ();
timer . start ();
// Update Kalman filter for each axis
for ( int i = 0 ; i < 6 ; i ++ )
{
// Prediction step
kalman [i]. predict (dt);
// Update step with measurement
kalman [i]. update ( input [i]);
// Get filtered estimate
output [i] = kalman [i]. get_state ();
}
}
Handling Rotation Wrap-Around
Rotation angles wrap around at ±180°. Handle this carefully in filters.
Problem: Rotation values jump from 179° to -179° when crossing the boundary.
Solution: Detect and handle wrap-around when computing deltas.
void filter ( const double* input , double* output )
{
constexpr double half_turn = 180.0 ;
constexpr double full_turn = 360.0 ;
for ( int i = 3 ; i < 6 ; i ++ ) // Yaw, Pitch, Roll
{
double delta = input [i] - last_input [i];
// Handle wrap-around
if ( fabs (delta) > half_turn)
delta -= copysign (full_turn, delta);
// Apply filtering to delta
double filtered_delta = apply_filter (delta);
// Integrate back
output [i] = last_output [i] + filtered_delta;
// Normalize output to [-180, 180]
while ( output [i] > half_turn)
output [i] -= full_turn;
while ( output [i] < - half_turn)
output [i] += full_turn;
last_input [i] = input [i];
last_output [i] = output [i];
}
}
Time Management
Filters often need accurate time measurement for velocity-based algorithms:
#include "compat/timer.hpp"
class MyFilter : public IFilter
{
Timer timer;
void filter ( const double* input , double* output ) override
{
// Get time since last call
const double dt = timer . elapsed_seconds ();
timer . start ();
// Use dt for velocity calculations
double velocity = ( input [ 0 ] - last_input [ 0 ]) / dt;
// ...
}
};
Avoid Dynamic Allocations
// Bad: allocates every call
void filter ( const double* input , double* output ) {
std ::vector < double > temp ( 6 ); // Allocation!
// ...
}
// Good: member variable
class MyFilter : IFilter {
double temp [ 6 ]; // No allocation
void filter ( const double* input , double* output ) {
// Use temp...
}
};
// Prefer built-in operations
double abs_value = fabs (value); // Fast
double sqrt_value = sqrt (value); // Fast
// Avoid expensive operations
double pow_value = pow (value, 2 ); // Use value * value instead
double exp_value = exp (value); // Expensive
// Better branch prediction with likely/unlikely
if (first_run) [[ unlikely ]]
{
// Initialization code
}
// Or restructure to avoid branches
double factor = (delta > threshold) ? 1.0 : 0.0 ;
output = input * factor;
See Also