CV Normalization in ESPectre: A Gain-Invariant Turbulence Metric for ESP32

CV normalization in ESPectre divides the standard deviation of sub-carrier amplitudes by their mean to create a gain-invariant turbulence metric, essential for motion detection on ESP32 devices without hardware gain lock.

When working with Wi-Fi sensing on the ESP32 radio chip, amplitude measurements fluctuate unpredictably due to unlocked automatic gain control (AGC). The ESPectre repository provides CV (Coefficient-of-Variation) normalization as a robust solution that stabilizes turbulence calculations across varying power levels. This technique ensures reliable motion detection even when the radio chip cannot maintain a constant gain factor.

What is CV Normalization?

CV normalization is a statistical preprocessing technique that makes turbulence measurements independent of signal amplitude scaling. Instead of using raw standard deviation—which scales linearly with gain changes—the method computes the ratio of standard deviation to mean amplitude.

The mathematical implementation follows this formula:

CV = σ(amplitudes) / μ(amplitudes)

Where σ represents the standard deviation and μ represents the mean of the sub-carrier amplitude values. Because both numerator and denominator scale by the same gain factor k, the ratio remains constant (σ(kA)/μ(kA) = σ(A)/μ(A)), providing a stable turbulence value regardless of power fluctuations.

Implementation in ESPectre Source Code

Core Calculation in utils.py

The primary implementation resides in micro-espectre/src/utils.py within the calculate_spatial_turbulence function. This utility accepts a use_cv_normalization boolean flag that determines whether to return the coefficient of variation or raw standard deviation.


# micro-espectre/src/utils.py – calculate_spatial_turbulence

def calculate_spatial_turbulence(magnitudes, band, use_cv_normalization=True):
    ...
    std = calculate_std(band_mags)
    if use_cv_normalization:
        mean = sum(band_mags) / len(band_mags)
        return std / mean if mean > 0 else 0.0
    else:
        return std

The function extracts the specified sub-carrier band, calculates the standard deviation, and conditionally divides by the mean when CV normalization is enabled.

Flag Management in segmentation.py

The Segmentation class in micro-espectre/src/segmentation.py owns the use_cv_normalization flag and propagates it throughout the processing pipeline. By default, this value is set to False because most ESP chips (ESP8266, ESP32-S2) maintain hardware gain lock.


# micro-espectre/src/segmentation.py – constructor

self.use_cv_normalization = False   # default for gain-locked chips

# Later, based on NPZ metadata:

self.use_cv_normalization = True    # ESP32 with no gain lock

The system automatically sets this flag to True when the dataset's NPZ files contain "gain_locked": false, as validated in the test suite.

Integration with Calibrators and Detectors

The NBVI calibrator (micro-espectre/src/nbvi_calibrator.py) and MVS detector (micro-espectre/src/mvs_detector.py) both expose methods to control CV mode programmatically. The calibrator provides explicit setter methods:


# micro-espectre/src/nbvi_calibrator.py

def set_cv_normalization(self, enable: bool):
    self.use_cv_normalization = enable

However, the ML-based detector (micro-espectre/src/ml_detector.py) explicitly disables CV normalization at line 184 because the underlying model was trained exclusively on raw standard deviation values, forcing use_cv_normalization = False during inference regardless of hardware configuration.

When to Enable CV Normalization

Enable CV normalization when processing data from ESP32 devices that lack hardware gain lock. The following conditions trigger automatic enabling:

  • ESP32 default behavior: The chip cannot lock gain, causing raw amplitude variations to dominate turbulence calculations
  • NPZ metadata detection: When "gain_locked": false appears in dataset metadata
  • Cross-environment stability: Required when operating across different power levels where gain fluctuations would cause false motion detections

Disable CV normalization when:

  • Using ESP8266 or ESP32-S2 chips with hardware gain lock
  • Running the ML-based detector (ml_detector.py), which expects raw standard deviation inputs

Code Examples

Enabling CV Normalization for ESP32 Recordings

from micro_espectre.src.segmentation import Segmentation

# Initialize segmentation

seg = Segmentation()

# Force CV mode for ESP32 (no hardware gain lock)

seg.use_cv_normalization = True

Computing Turbulence Manually

from micro_espectre.src.utils import calculate_spatial_turbulence

# Sample amplitude values from sub-carriers

magnitudes = [/* ... amplitude values ... */]
band = list(range(20, 40))  # Select sub-carrier subset

# Calculate CV-normalized turbulence

turb = calculate_spatial_turbulence(
    magnitudes, 
    band, 
    use_cv_normalization=True
)

print("CV-normalized turbulence:", turb)

Summary

  • CV normalization divides standard deviation by mean amplitude to create a gain-invariant turbulence metric essential for ESP32 devices
  • The core implementation lives in micro-espectre/src/utils.py within the calculate_spatial_turbulence function
  • The Segmentation class manages the use_cv_normalization flag, defaulting to False for gain-locked chips but enabling it automatically for ESP32 based on NPZ metadata
  • The ML detector explicitly disables CV normalization because it was trained on raw standard deviation values
  • Use CV normalization whenever hardware gain is unlocked to prevent false motion detections and maintain stable measurements across varying power levels

Frequently Asked Questions

Why is CV normalization necessary for ESP32 devices?

ESP32 chips lack hardware gain lock, meaning their automatic gain control (AGC) fluctuates continuously. Raw amplitude standard deviation scales with these gain changes, making it impossible to distinguish between actual motion-induced turbulence and simple power level variations. CV normalization eliminates this dependency by using a ratio that remains constant regardless of gain scaling, providing a stable metric for motion detection.

How does ESPectre automatically detect when to use CV normalization?

ESPectre checks the metadata within NPZ dataset files for the "gain_locked" field. When this value is false, as determined during the calibration step in nbvi_calibrator.py and validated in test_validation_real_data.py, the system automatically sets use_cv_normalization = True in the Segmentation class. This ensures the correct turbulence metric is applied without manual configuration.

Can I use CV normalization with the ML-based detector?

No. The ML detector (ml_detector.py) explicitly forces use_cv_normalization = False during inference because the underlying machine learning model was trained exclusively on raw standard deviation values. Enabling CV normalization would provide inputs outside the model's training distribution, leading to inaccurate predictions. Use CV normalization only with the NBVI or MVS detection methods.

What is the mathematical formula for CV normalization in ESPectre?

ESPectre calculates CV normalization as σ / μ, where σ is the standard deviation of the selected sub-carrier amplitudes and μ is their arithmetic mean. This ratio remains invariant under linear scaling because both numerator and denominator are multiplied by the same gain factor, effectively canceling out gain-induced variations while preserving motion-induced turbulence signatures.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →