Skip to main content

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.
return
module_status
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
const double*
Input pose array: [TX, TY, TZ, Yaw, Pitch, Roll] in cm/degrees
output
double*
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:
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()

virtual void 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.
filter
IFilter*
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

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];
        }
    }
}
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;
        
        // ...
    }
};

Performance Tips

// 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