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.
- 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_asserton 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
#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);
}#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.
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
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:
- Evict the oldest node (age head, advanced by
ptr + 1with wraparound) and unlink it from the value chain - Search from the median pointer — up or down depending on the new sample's value
- Insert the recycled node at the correct sorted position
- 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.
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.
| 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.
| 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.
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.
MIT — free for commercial and personal use.