CrocoDocs
CrocoDocs

Introduction

What is CrocoDocs?Season Breakdown

Getting Started

Programming in FTCJavaBlocksAndroid Studio

Control Systems

IntroductionJoystick MappingPID ControlMotion ProfilingKalman FilterLow-Pass Filter

Autonomous

IntroductionTime vs Encoder-Based MovementOdometryMotion PlanningPure PursuitSensor Fusion

Codebase Etiquette and Good Practices

IntroductionNaming ConventionsCode OrganizationComments and DocumentationTeam Collaboration

Libraries

LibrariesNextFTCPedro PathingFTC DashboardMercurialPanelsSloth

Sensors and Vision

Vision OverviewVision Basics

Low-Pass Filter

Simple and effective sensor smoothing technique

What is a Low-Pass Filter?

A low-pass filter is a simple smoothing algorithm that reduces high-frequency noise while allowing slow changes to pass through. It's called "low-pass" because it lets low-frequency signals (slow changes) pass through while blocking high-frequency signals (rapid noise).

Think of it like dampening: imagine watching a bouncing ball through frosted glass - you can still see the general motion, but the rapid vibrations are smoothed out.

Why Use a Low-Pass Filter in FTC?

Common Problems:

  • Encoder readings jump around due to vibrations
  • Distance sensors give noisy measurements
  • Color sensor values fluctuate
  • IMU has small random variations

Low-Pass Filter Benefits:

  • Dead simple to implement (1-2 lines of code)
  • Very low computational cost
  • No tuning required for basic use
  • Effective for most FTC applications
  • Predictable behavior

How It Works

The low-pass filter calculates a weighted average between the previous filtered value and the new measurement:

Filtered Value = (α × New Measurement) + ((1 - α) × Previous Filtered Value)

Where α (alpha) is the smoothing factor between 0 and 1:

  • α = 1.0: No filtering (instant response)
  • α = 0.5: Equal weight to old and new
  • α = 0.1: Heavy smoothing (slow response)

Visual Example

Raw sensor data (very noisy):

Time:     0    1    2    3    4    5    6    7    8
Sensor: [100, 95, 105, 98, 102, 97, 103, 99, 101]

With α = 0.3 (moderate filtering):

Filter: [100, 98, 100, 99, 100, 99, 100, 99, 100]
         ↑    ↑    ↑    ↑    ↑    ↑    ↑    ↑    ↑
      Smooth, stable, tracks general trend

With α = 0.8 (light filtering):

Filter: [100, 96, 102, 99, 101, 98, 101, 99, 100]
         ↑    ↑    ↑    ↑    ↑    ↑    ↑    ↑    ↑
      Follows closer, less smooth

Basic Implementation

Simple Low-Pass Filter Class

public class LowPassFilter {
    private double alpha;
    private double filteredValue;
    private boolean initialized;
    
    /**
     * @param alpha Smoothing factor (0-1)
     *              Higher = faster response, less smoothing
     *              Lower = slower response, more smoothing
     */
    public LowPassFilter(double alpha) {
        this.alpha = alpha;
        this.initialized = false;
    }
    
    public double update(double measurement) {
        if (!initialized) {
            filteredValue = measurement;
            initialized = true;
            return filteredValue;
        }
        
        filteredValue = alpha * measurement + (1 - alpha) * filteredValue;
        return filteredValue;
    }
    
    public double getValue() {
        return filteredValue;
    }
    
    public void reset() {
        initialized = false;
    }
}
class LowPassFilter(private val alpha: Double) {
    private var filteredValue: Double = 0.0
    private var initialized: Boolean = false
    
    /**
     * @param alpha Smoothing factor (0-1)
     *              Higher = faster response, less smoothing
     *              Lower = slower response, more smoothing
     */
    
    fun update(measurement: Double): Double {
        if (!initialized) {
            filteredValue = measurement
            initialized = true
            return filteredValue
        }
        
        filteredValue = alpha * measurement + (1 - alpha) * filteredValue
        return filteredValue
    }
    
    fun getValue(): Double = filteredValue
    
    fun reset() {
        initialized = false
    }
}

Ultra-Simple One-Line Version

// In your class
private double filteredValue = 0;

// In your loop
public void loop() {
    double rawValue = sensor.getValue();
    
    // One-line low-pass filter!
    filteredValue = 0.3 * rawValue + 0.7 * filteredValue;
    
    telemetry.addData("Filtered", filteredValue);
}
// In your class
private var filteredValue = 0.0

// In your loop
override fun loop() {
    val rawValue = sensor.value
    
    // One-line low-pass filter!
    filteredValue = 0.3 * rawValue + 0.7 * filteredValue
    
    telemetry.addData("Filtered", filteredValue)
}

Using the Filter

Example: Smooth Distance Sensor

@TeleOp(name = "Low-Pass Example")
public class LowPassExample extends LinearOpMode {
    private DistanceSensor distanceSensor;
    private LowPassFilter filter;
    
    @Override
    public void runOpMode() {
        distanceSensor = hardwareMap.get(DistanceSensor.class, "distance");
        
        // α = 0.2 means 20% new data, 80% old data
        filter = new LowPassFilter(0.2);
        
        waitForStart();
        
        while (opModeIsActive()) {
            // Get noisy distance reading
            double rawDistance = distanceSensor.getDistance(DistanceUnit.INCH);
            
            // Filter it
            double smoothDistance = filter.update(rawDistance);
            
            telemetry.addData("Raw Distance", "%.1f in", rawDistance);
            telemetry.addData("Smooth Distance", "%.1f in", smoothDistance);
            telemetry.update();
        }
    }
}
@TeleOp(name = "Low-Pass Example")
class LowPassExample : LinearOpMode() {
    private lateinit var distanceSensor: DistanceSensor
    private lateinit var filter: LowPassFilter
    
    override fun runOpMode() {
        distanceSensor = hardwareMap.get(DistanceSensor::class.java, "distance")
        
        // α = 0.2 means 20% new data, 80% old data
        filter = LowPassFilter(0.2)
        
        waitForStart()
        
        while (opModeIsActive()) {
            // Get noisy distance reading
            val rawDistance = distanceSensor.getDistance(DistanceUnit.INCH)
            
            // Filter it
            val smoothDistance = filter.update(rawDistance)
            
            telemetry.addData("Raw Distance", "%.1f in", rawDistance)
            telemetry.addData("Smooth Distance", "%.1f in", smoothDistance)
            telemetry.update()
        }
    }
}

Choosing the Alpha Value

The alpha (α) value controls the trade-off between responsiveness and smoothness:

Alpha (α)ResponseSmoothnessUse Case
0.9 - 1.0Very fastMinimalLight noise, need quick response
0.5 - 0.8FastModerateModerate noise, balanced needs
0.2 - 0.4MediumGoodNoisy sensors, stable readings wanted
0.05 - 0.15SlowVery smoothVery noisy, don't need fast response

How to Choose

// For clean sensors (encoders, IMU)
filter = new LowPassFilter(0.7);  // Light smoothing

// For moderate noise (distance sensors)
filter = new LowPassFilter(0.3);  // Medium smoothing

// For very noisy sensors (cheap ultrasonic)
filter = new LowPassFilter(0.1);  // Heavy smoothing

// For display-only values (telemetry)
filter = new LowPassFilter(0.15); // Extra smooth for readability
// For clean sensors (encoders, IMU)
val filter = LowPassFilter(0.7)  // Light smoothing

// For moderate noise (distance sensors)
val filter = LowPassFilter(0.3)  // Medium smoothing

// For very noisy sensors (cheap ultrasonic)
val filter = LowPassFilter(0.1)  // Heavy smoothing

// For display-only values (telemetry)
val filter = LowPassFilter(0.15) // Extra smooth for readability

Real-World Examples

Example 1: Smooth Encoder Velocity

public class SmoothVelocity {
    private DcMotorEx motor;
    private LowPassFilter velocityFilter;
    
    public SmoothVelocity(HardwareMap hardwareMap) {
        motor = hardwareMap.get(DcMotorEx.class, "motor");
        velocityFilter = new LowPassFilter(0.4);
    }
    
    public double getSmoothedVelocity() {
        double rawVelocity = motor.getVelocity();
        return velocityFilter.update(rawVelocity);
    }
}
class SmoothVelocity(hardwareMap: HardwareMap) {
    private val motor: DcMotorEx = hardwareMap.get(DcMotorEx::class.java, "motor")
    private val velocityFilter = LowPassFilter(0.4)
    
    fun getSmoothedVelocity(): Double {
        val rawVelocity = motor.velocity
        return velocityFilter.update(rawVelocity)
    }
}

Example 2: Smooth Color Sensor for Line Following

@Autonomous(name = "Line Follow with Filter")
public class LineFollower extends LinearOpMode {
    private ColorSensor colorSensor;
    private LowPassFilter redFilter, blueFilter;
    private DcMotor leftMotor, rightMotor;
    
    @Override
    public void runOpMode() {
        colorSensor = hardwareMap.get(ColorSensor.class, "color");
        leftMotor = hardwareMap.get(DcMotor.class, "left");
        rightMotor = hardwareMap.get(DcMotor.class, "right");
        
        // Color sensors can be noisy
        redFilter = new LowPassFilter(0.3);
        blueFilter = new LowPassFilter(0.3);
        
        waitForStart();
        
        while (opModeIsActive()) {
            // Get raw color values
            int rawRed = colorSensor.red();
            int rawBlue = colorSensor.blue();
            
            // Smooth them
            double smoothRed = redFilter.update(rawRed);
            double smoothBlue = blueFilter.update(rawBlue);
            
            // Use smoothed values for more stable line following
            double error = smoothRed - smoothBlue;
            double correction = error * 0.01;
            
            leftMotor.setPower(0.5 + correction);
            rightMotor.setPower(0.5 - correction);
            
            telemetry.addData("Raw Red", rawRed);
            telemetry.addData("Smooth Red", "%.0f", smoothRed);
            telemetry.update();
        }
    }
}
@Autonomous(name = "Line Follow with Filter")
class LineFollower : LinearOpMode() {
    private lateinit var colorSensor: ColorSensor
    private lateinit var redFilter: LowPassFilter
    private lateinit var blueFilter: LowPassFilter
    private lateinit var leftMotor: DcMotor
    private lateinit var rightMotor: DcMotor
    
    override fun runOpMode() {
        colorSensor = hardwareMap.get(ColorSensor::class.java, "color")
        leftMotor = hardwareMap.get(DcMotor::class.java, "left")
        rightMotor = hardwareMap.get(DcMotor::class.java, "right")
        
        // Color sensors can be noisy
        redFilter = LowPassFilter(0.3)
        blueFilter = LowPassFilter(0.3)
        
        waitForStart()
        
        while (opModeIsActive()) {
            // Get raw color values
            val rawRed = colorSensor.red()
            val rawBlue = colorSensor.blue()
            
            // Smooth them
            val smoothRed = redFilter.update(rawRed.toDouble())
            val smoothBlue = blueFilter.update(rawBlue.toDouble())
            
            // Use smoothed values for more stable line following
            val error = smoothRed - smoothBlue
            val correction = error * 0.01
            
            leftMotor.power = 0.5 + correction
            rightMotor.power = 0.5 - correction
            
            telemetry.addData("Raw Red", rawRed)
            telemetry.addData("Smooth Red", "%.0f", smoothRed)
            telemetry.update()
        }
    }
}

Example 3: Multiple Filters for Drivetrain

public class FilteredDrivetrain {
    private DcMotorEx frontLeft, frontRight, backLeft, backRight;
    private LowPassFilter flFilter, frFilter, blFilter, brFilter;
    
    public FilteredDrivetrain(HardwareMap hardwareMap) {
        frontLeft = hardwareMap.get(DcMotorEx.class, "frontLeft");
        frontRight = hardwareMap.get(DcMotorEx.class, "frontRight");
        backLeft = hardwareMap.get(DcMotorEx.class, "backLeft");
        backRight = hardwareMap.get(DcMotorEx.class, "backRight");
        
        // Create filter for each motor velocity
        double alpha = 0.5;
        flFilter = new LowPassFilter(alpha);
        frFilter = new LowPassFilter(alpha);
        blFilter = new LowPassFilter(alpha);
        brFilter = new LowPassFilter(alpha);
    }
    
    public double[] getSmoothedVelocities() {
        return new double[] {
            flFilter.update(frontLeft.getVelocity()),
            frFilter.update(frontRight.getVelocity()),
            blFilter.update(backLeft.getVelocity()),
            brFilter.update(backRight.getVelocity())
        };
    }
}
class FilteredDrivetrain(hardwareMap: HardwareMap) {
    private val frontLeft: DcMotorEx = hardwareMap.get(DcMotorEx::class.java, "frontLeft")
    private val frontRight: DcMotorEx = hardwareMap.get(DcMotorEx::class.java, "frontRight")
    private val backLeft: DcMotorEx = hardwareMap.get(DcMotorEx::class.java, "backLeft")
    private val backRight: DcMotorEx = hardwareMap.get(DcMotorEx::class.java, "backRight")
    
    // Create filter for each motor velocity
    private val alpha = 0.5
    private val flFilter = LowPassFilter(alpha)
    private val frFilter = LowPassFilter(alpha)
    private val blFilter = LowPassFilter(alpha)
    private val brFilter = LowPassFilter(alpha)
    
    fun getSmoothedVelocities(): DoubleArray {
        return doubleArrayOf(
            flFilter.update(frontLeft.velocity),
            frFilter.update(frontRight.velocity),
            blFilter.update(backLeft.velocity),
            brFilter.update(backRight.velocity)
        )
    }
}

Advanced: Time-Based Alpha

For more consistent behavior regardless of loop frequency:

public class TimeBasedLowPassFilter {
    private double timeConstant;  // In seconds (e.g., 0.1 = 100ms)
    private double filteredValue;
    private long lastTime;
    private boolean initialized;
    
    public TimeBasedLowPassFilter(double timeConstant) {
        this.timeConstant = timeConstant;
        this.initialized = false;
    }
    
    public double update(double measurement) {
        long currentTime = System.nanoTime();
        
        if (!initialized) {
            filteredValue = measurement;
            lastTime = currentTime;
            initialized = true;
            return filteredValue;
        }
        
        double dt = (currentTime - lastTime) / 1e9;  // Convert to seconds
        double alpha = dt / (timeConstant + dt);
        
        filteredValue = alpha * measurement + (1 - alpha) * filteredValue;
        lastTime = currentTime;
        
        return filteredValue;
    }
}
class TimeBasedLowPassFilter(private val timeConstant: Double) {
    // timeConstant in seconds (e.g., 0.1 = 100ms)
    private var filteredValue: Double = 0.0
    private var lastTime: Long = 0
    private var initialized: Boolean = false
    
    fun update(measurement: Double): Double {
        val currentTime = System.nanoTime()
        
        if (!initialized) {
            filteredValue = measurement
            lastTime = currentTime
            initialized = true
            return filteredValue
        }
        
        val dt = (currentTime - lastTime) / 1e9  // Convert to seconds
        val alpha = dt / (timeConstant + dt)
        
        filteredValue = alpha * measurement + (1 - alpha) * filteredValue
        lastTime = currentTime
        
        return filteredValue
    }
}

When to Use vs. Not Use

Use Low-Pass Filter when:

  • Sensor has random noise
  • You need simple, fast smoothing
  • Computational resources are limited
  • Delayed response is acceptable
  • Displaying values to drivers

Don't use Low-Pass Filter when:

  • Need instant response (use raw data)
  • Sensor has systematic drift (use calibration)
  • Need predictive capabilities (use Kalman filter)
  • Combining multiple sensors (use sensor fusion)

Comparison: Low-Pass vs. Kalman vs. Moving Average

FeatureLow-PassKalmanMoving Average
ComplexityVery simpleComplexSimple
CPU UsageMinimalModerateLow
Memory1 valueMultiple valuesWindow of values
LagModerateLowHigh
AdaptabilityFixedAdaptiveFixed
Best ForGeneral smoothingOptimal filteringDisplay values

Tips for Success

  1. Start with α = 0.3: Good default for most cases
  2. Test in Motion: Filter behaves differently when robot moves
  3. Log Data: Compare raw vs. filtered to see the effect
  4. Don't Over-Filter: Too much smoothing = too much lag
  5. One Filter Per Sensor: Don't share filters between sensors
  6. Reset When Needed: Call reset() when starting new movements

Common Mistakes

❌ Using same filter instance for different sensors

// WRONG - filters will mix data
filter.update(sensor1.getValue());
filter.update(sensor2.getValue());

✅ Create separate filter for each sensor

// CORRECT
filter1.update(sensor1.getValue());
filter2.update(sensor2.getValue());

❌ Alpha value outside 0-1 range

// WRONG
filter = new LowPassFilter(1.5);  // Invalid!

✅ Keep alpha between 0 and 1

// CORRECT
filter = new LowPassFilter(0.3);  // Valid

Resources

  • CTRL ALT FTC: Signal processing guide
  • Wikipedia: Low-pass filter
  • Game Manual 0: Software filtering

Next Steps

  1. Implement the basic filter class
  2. Test with a noisy sensor
  3. Experiment with different alpha values
  4. Compare with raw readings using telemetry
  5. Apply to your robot's sensors
  6. Consider Kalman filter for more advanced needs

Kalman Filter

Reduce sensor noise and improve accuracy with Kalman filtering

Introduction

Learn to create reliable autonomous routines using path planning, localization, and sensor-driven decision-making

On this page

What is a Low-Pass Filter?Why Use a Low-Pass Filter in FTC?How It WorksVisual ExampleBasic ImplementationSimple Low-Pass Filter ClassUltra-Simple One-Line VersionUsing the FilterExample: Smooth Distance SensorChoosing the Alpha ValueHow to ChooseReal-World ExamplesExample 1: Smooth Encoder VelocityExample 2: Smooth Color Sensor for Line FollowingExample 3: Multiple Filters for DrivetrainAdvanced: Time-Based AlphaWhen to Use vs. Not UseComparison: Low-Pass vs. Kalman vs. Moving AverageTips for SuccessCommon MistakesResourcesNext Steps