SkillAgentSearch skills...

MedianFilter

Heap-free sliding window median filter for embedded C/C++ — O(n/2) insert, zero allocations, ideal for ADC noise removal on bare-metal and RTOS targets

Install / Use

/learn @accabog/MedianFilter

README

MedianFilter

CI License: MIT Language: C/C++ Platform: Bare-metal / RTOS / Desktop No dynamic allocation PlatformIO

Heap-free, O(n) sliding-window median filter for embedded systems. Drop two files into your project and go.

Removes impulse noise (salt-and-pepper spikes) from analog signals in real time — ideal for ADC readings, sensor fusion, and any DSP pipeline where you need a robust filter with zero malloc.

Features

  • Zero heap allocation — stack or static buffers only, safe for bare-metal and RTOS
  • O(n/2) average insert — bidirectional search from the median pointer
  • O(1) median tracking — the median pointer adjusts by at most one step per insert, never recomputed
  • Stable duplicate handling — address-based tiebreaker eliminates ambiguity for equal-valued samples
  • C API — caller-allocated buffers, two functions, done
  • C++ header-only template — type-safe, compile-time static_assert on window size and type
  • No dependencies — only <stdint.h> (C) or <type_traits> / <cstdint> (C++)
  • 24 bytes per sample — constant, predictable RAM footprint (64-bit); 12 bytes on 32-bit ARM

Quick Start

C

#include "MedianFilter.h"

#define WINDOW_SIZE 7

static sMedianFilter_t filter;
static sMedianNode_t   buffer[WINDOW_SIZE];

void setup(void)
{
    filter.numNodes     = WINDOW_SIZE;
    filter.medianBuffer = buffer;
    MEDIANFILTER_Init(&filter);
}

int process(int sample)
{
    return MEDIANFILTER_Insert(&filter, sample);
}

C++

#include "MedianFilter.hpp"

MedianFilter<int, 7> filter;

int process(int sample)
{
    return filter.Insert(sample);
}

The C++ template accepts any arithmetic type (int, float, double, int16_t, ...) and enforces an odd window size >= 3 at compile time.

Examples

The examples/ folder contains runnable demos and platform-specific reference code:

| Example | Description | |---------|-------------| | Basic Usage | Minimal C API — init, insert, print | | ADC Noise Filter | Spike removal from a simulated 12-bit ADC signal | | Multichannel Sensors | Independent filters on temperature, humidity, and pressure | | Window Size Comparison | Noise removal vs. signal lag tradeoff | | C++ Template | float, int16_t, double type flexibility | | STM32 HAL | ADC interrupt callback (reference) | | Arduino | analogRead() filter | | ESP-IDF | FreeRTOS ADC task (reference) |

Build and run all desktop demos:

make examples

How It Works

Each sample lives in a node with two link chains:

Age order:     buffer[0] → [1] → ... → [N-1] → [0]   (implicit, round-robin)

Value chain:   smallest <══> ... <══> largest          (doubly-linked, sorted)

Median ptr:         ──────────> M                      (always the middle node)

Age order is implicit — nodes are recycled in buffer order via pointer arithmetic, so no explicit age pointer is stored. This saves one pointer per node (25% RAM reduction).

On every insert:

  1. Evict the oldest node (age head, advanced by ptr + 1 with wraparound) and unlink it from the value chain
  2. Search from the median pointer — up or down depending on the new sample's value
  3. Insert the recycled node at the correct sorted position
  4. Adjust the median pointer by one step if needed

The bidirectional search means average traversal is ~n/4 rather than n/2. The median pointer never needs to be recomputed from scratch — it tracks incrementally.

Performance

Consistently among the fastest across all window sizes — and the clear winner at larger windows where it matters most.

Instructions per insert (lower is better), measured with Valgrind/Callgrind (gcc -O2, averaged over 5 random seeds). Results are deterministic and hardware-independent — reproducible on any machine:

| Window | Ours (C) | Ours (C++) | Insertion-sort ring | vpetrigo | std::nth_element | Naive sort | |:------:|:------------:|:--------------:|:-------------------:|:------------------------------------------------------:|:----------------:|:----------:| | 3 | 124 | 131 | 112 | 159 | 179 | 194 | | 7 | 128 | 137 | 149 | 190 | 310 | 300 | | 11 | 134 | 143 | 173 | 218 | 390 | 444 | | 31 | 162 | 172 | 288 | 359 | 751 | 1446 | | 51 | 189 | 200 | 395 | 499 | 1078 | 2504 | | 101 | 259 | 270 | 669 | 847 | 1880 | 5452 |

"Ours (C)" uses the opt-in MEDIANFILTER_INLINE_API inline Insert. At small windows (3), insertion-sort ring wins thanks to cache locality on tiny arrays. From window 7+, our linked-list approach takes the lead — and at window 101, it uses 2.6× fewer instructions than insertion-sort and 3.3× fewer than vpetrigo.

RAM = window_size * 24 + 40 bytes (64-bit). On 32-bit ARM, nodes are 12 bytes each. See benchmarks/ for full results, throughput benchmarks, and methodology.

API Reference

C

| Function | Signature | Returns | |:---------|:----------|:--------| | Init | int MEDIANFILTER_Init(sMedianFilter_t *filter) | 0 on success, -1 on invalid config | | Insert | int MEDIANFILTER_Insert(sMedianFilter_t *restrict filter, int sample) | Current median value |

The caller must allocate sMedianFilter_t and a sMedianNode_t[N] buffer, set numNodes = N and medianBuffer = buffer, then call Init. Window size must be odd and > 1.

Inline variant: For tight-loop callers (e.g. processing an ADC buffer), define MEDIANFILTER_INLINE_API before including the header to get a static inline Insert that eliminates function-call overhead.

C++

| Method | Signature | Returns | |:-------|:----------|:--------| | Constructor | MedianFilter<type, N>() | — | | Insert | type Insert(type value) | Current median value |

Template parameters: type must be arithmetic, N must be odd and >= 3. Enforced via static_assert.

Integration

Just copy the files — no build system required.

For C: add MedianFilter.h and MedianFilter.c to your project. For C++: add MedianFilter.hpp (header-only, nothing to compile).

Both APIs are compatible in the same translation unit — the C header includes extern "C" guards.

License

MIT — free for commercial and personal use.

View on GitHub
GitHub Stars68
CategoryDevelopment
Updated11d ago
Forks21

Languages

C

Security Score

100/100

Audited on Mar 30, 2026

No findings