Skip to content

Signal Processing Basics

Mathematical foundations for sensor data analysis in robotics.

What is a Signal?

A signal is a sequence of measurements over time:

Signal: [0.02, 0.05, 0.03, 0.08, 0.02, 0.06, ...]
         t=0   t=1   t=2   t=3   t=4   t=5

In our robot: - Accelerometer signal: acceleration in g (gravity units) - Gyroscope signal: rotation rate in °/s - Ultrasonic signal: distance in cm

Sample Rate

Sample rate (Fs) = how many measurements per second

Fs = 500 Hz means 500 samples per second
Each sample is 1/500 = 0.002 seconds = 2 milliseconds apart
Nyquist Theorem

You can only detect frequencies up to Fs/2 (the Nyquist frequency).

At 500 Hz sample rate: max detectable frequency = 250 Hz

Time Domain vs Frequency Domain

Time Domain

Shows when things happen:

Time Domain Signal:
amplitude
 1.0│    ∧      ∧      ∧
    │   / \    / \    / \
 0.0│──/───\──/───\──/───\──→ time
    │        \/     \/
-1.0│

Frequency Domain

Shows what frequencies are present:

Frequency Domain (FFT):
magnitude
 1.0│     ▌
    │     █
 0.5│     █
    │  ▌  █
 0.0│──█──█──█──█──█──→ frequency (Hz)
       5  10 15 20 25
Analogy: Music
  • Time domain = sheet music (notes over time)
  • Frequency domain = list of instruments playing (what sounds)

Key Mathematical Concepts

1. Mean (Average)

The central value of a signal:

\[\mu = \frac{1}{N} \sum_{i=1}^{N} x_i = \frac{x_1 + x_2 + ... + x_N}{N}\]
def mean(data):
    return sum(data) / len(data)

Use: Remove DC offset from signal

2. Variance

How spread out the values are from the mean:

\[\sigma^2 = \frac{1}{N} \sum_{i=1}^{N} (x_i - \mu)^2\]
def variance(data):
    m = mean(data)
    return sum((x - m)**2 for x in data) / len(data)

Use: Measure vibration intensity (more vibration = higher variance)

3. Standard Deviation

Square root of variance (same units as signal):

\[\sigma = \sqrt{\sigma^2}\]
def std_dev(data):
    return math.sqrt(variance(data))

4. Magnitude

For 3D vectors (accelerometer, gyroscope):

\[|a| = \sqrt{a_x^2 + a_y^2 + a_z^2}\]
def magnitude(ax, ay, az):
    return math.sqrt(ax**2 + ay**2 + az**2)

Use: Combine 3 axes into single value for bump detection

Filtering

Filters remove unwanted parts of a signal.

High-Pass Filter

Removes low frequencies (DC offset, slow drift), keeps fast changes:

Input:    ───────∿∿∿∿∿∿∿──────
                  ↓ HP filter
Output:   ───────↑↓↑↓↑↓↑───────

Implementation:

# High-pass filter (removes DC, keeps AC)
hp_alpha = 0.95  # Closer to 1 = slower cutoff
hp_prev_in = 0
hp_prev_out = 0

def high_pass(new_sample):
    global hp_prev_in, hp_prev_out
    hp_out = hp_alpha * (hp_prev_out + new_sample - hp_prev_in)
    hp_prev_in = new_sample
    hp_prev_out = hp_out
    return hp_out

Use: Isolate wheel bumps from accelerometer (bumps are fast changes)

Low-Pass Filter

Removes high frequencies (noise), keeps slow changes:

Input:    ∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿∿
                  ↓ LP filter
Output:   ─────────────────────

Implementation (Exponential Moving Average):

# Low-pass filter (smooths signal)
lp_alpha = 0.1  # Smaller = more smoothing
lp_prev = 0

def low_pass(new_sample):
    global lp_prev
    lp_prev = lp_alpha * new_sample + (1 - lp_alpha) * lp_prev
    return lp_prev

Use: Smooth noisy sensor readings

Fourier Transform (FFT)

Converts signal from time domain to frequency domain.

The Idea

Any signal can be built from sine waves at different frequencies:

Original signal = sin(10Hz) + 0.5*sin(25Hz) + 0.3*sin(50Hz)

FFT reveals:
  10 Hz: magnitude 1.0
  25 Hz: magnitude 0.5
  50 Hz: magnitude 0.3

Implementation

import numpy as np

# Sample data
signal = [...]  # Your sensor data
Fs = 500        # Sample rate

# Compute FFT
fft_result = np.fft.fft(signal)
frequencies = np.fft.fftfreq(len(signal), 1/Fs)

# Get magnitude (only positive frequencies)
n = len(signal) // 2
magnitudes = np.abs(fft_result[:n]) / n
freqs = frequencies[:n]

# Find peak frequency
peak_idx = np.argmax(magnitudes[1:]) + 1  # Skip DC (index 0)
peak_freq = freqs[peak_idx]

Interpreting Results

Peak Frequency Likely Source
0-5 Hz Slow swaying, drift
10-50 Hz Wheel bumps
50-100 Hz Motor vibration
100+ Hz Electrical noise

Threshold Detection

Count events by detecting when signal crosses a threshold.

Algorithm

threshold = 0.015
was_below = True
count = 0

for sample in filtered_signal:
    is_above = sample > threshold

    if is_above and was_below:
        # Rising edge detected
        count += 1

    was_below = not is_above
Signal:    ──────/\────/\────/\──────
Threshold: ════════════════════════  (0.015)
Detections:      ↑      ↑     ↑
                 1      2     3

Use: Count wheel bumps for distance estimation

Sliding Window Analysis

Analyze signal in overlapping windows for real-time processing.

Concept

Signal:    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Window 1:  [1, 2, 3, 4, 5]  → variance = 2.5
Window 2:     [2, 3, 4, 5, 6]  → variance = 2.5
Window 3:        [3, 4, 5, 6, 7]  → variance = 2.5
...

Implementation

from collections import deque

window = deque(maxlen=50)  # 50 samples = 100ms at 500Hz

def add_sample(sample):
    window.append(sample)
    if len(window) >= 25:  # Need at least half window
        return variance(list(window))
    return 0

Use: Real-time vibration intensity measurement

Practical Example: Bump Detection

Complete pipeline for detecting wheel bumps:

import math

# Configuration
HP_ALPHA = 0.95
THRESHOLD = 0.015

# State
hp_prev_in = 0
hp_prev_out = 0
was_above = False
bump_count = 0

def process_sample(ax, ay, az):
    global hp_prev_in, hp_prev_out, was_above, bump_count

    # Step 1: Calculate magnitude
    mag = math.sqrt(ax**2 + ay**2 + az**2) - 1.0  # Remove gravity

    # Step 2: High-pass filter
    hp_out = HP_ALPHA * (hp_prev_out + mag - hp_prev_in)
    hp_prev_in = mag
    hp_prev_out = hp_out

    # Step 3: Threshold crossing detection
    is_above = hp_out > THRESHOLD
    if is_above and not was_above:
        bump_count += 1
    was_above = is_above

    return bump_count

Practical Example: Speed from FFT

def estimate_speed_from_fft(accel_data, sample_rate=500):
    """
    Estimate speed from wheel bump frequency.

    Args:
        accel_data: List of acceleration magnitude samples
        sample_rate: Samples per second

    Returns:
        Estimated speed in cm/s (requires calibration)
    """
    import numpy as np

    # Remove DC offset
    data = np.array(accel_data) - np.mean(accel_data)

    # Apply Hann window to reduce spectral leakage
    window = np.hanning(len(data))
    windowed = data * window

    # Compute FFT
    fft = np.fft.fft(windowed)
    freqs = np.fft.fftfreq(len(data), 1/sample_rate)

    # Find peak in bump frequency range (10-100 Hz)
    magnitudes = np.abs(fft[:len(fft)//2])
    freq_range = (freqs[:len(freqs)//2] > 10) & (freqs[:len(freqs)//2] < 100)

    if not any(freq_range):
        return 0

    peak_idx = np.argmax(magnitudes * freq_range)
    bump_freq = freqs[peak_idx]

    # Convert to speed (needs calibration for your robot)
    # Example: 10 bumps per wheel rotation, 20cm circumference
    BUMPS_PER_ROTATION = 10
    WHEEL_CIRCUMFERENCE = 20  # cm

    rotation_rate = bump_freq / BUMPS_PER_ROTATION  # rotations/sec
    speed = rotation_rate * WHEEL_CIRCUMFERENCE      # cm/sec

    return speed

Summary: Which Method When?

Task Method Real-time? Accuracy
Bump counting HP filter + threshold Yes Good
Find bump frequency FFT No (batch) Excellent
Vibration intensity Sliding variance Yes Good
Speed estimation Bumps or FFT Depends Needs calibration
Motion classification ML (forest/knn) Yes Excellent

Next Steps

  • [[Reference/07-data-analysis/overview|Data Analysis Overview]] - Practical workflow
  • [[Reference/07-data-analysis/analyzers-reference|Analyzers Reference]] - CLI tool reference
  • [[Reference/extras/hardware/picocar-pinout|PicoCar Pinout]] - Hardware configuration