Preprocessing
The preprocessing package provides utilities for preparing and transforming HD-sEMG signals before analysis. Currently, it focuses on differential signal computation, which is a common preprocessing step in EMG analysis.
Differential Analysis
The package provides functionality to compute differential signals from HD-sEMG channels, with optional filtering. This is particularly useful for reducing common-mode noise and enhancing local muscle activity detection.
API Reference
def to_differential(
mats: list[np.ndarray],
sr: float,
f: dict[str, float]
) -> tuple[list[np.ndarray], list[np.ndarray]]:
"""Compute differential channels with optional filtering."""
Parameters
-
mats (
list[np.ndarray]):- List of 2D arrays containing EMG signals
- Each array shape: (j, T), where:
- j: number of channels
- T: number of time samples
- Type: float
-
sr (
float):- Sampling rate in Hz
- Used for the bandpass filter if applied
-
f (
dict[str, float]):- Filter parameters dictionary with keys:
- 'n': filter order (int)
- 'low': lower cutoff frequency in Hz
- 'up': upper cutoff frequency in Hz
- Filter parameters dictionary with keys:
Returns
-
dmat (
list[np.ndarray]):- List of filtered differential signals
- Each array shape: (j-1, T)
- Bandpass filtered using specified parameters
-
dmat_no_filter (
list[np.ndarray]):- List of unfiltered differential signals
- Each array shape: (j-1, T)
- Raw differences between adjacent channels
Example Usage
import numpy as np
from hdsemg_shared.preprocessing.differential import to_differential
# Create sample EMG data (2 matrices, 8 channels each, 1000 samples)
emg1 = np.random.randn(8, 1000)
emg2 = np.random.randn(8, 1000)
mats = [emg1, emg2]
# Define filter parameters
filter_params = {
'n': 4, # 4th order Butterworth
'low': 20, # 20 Hz highpass
'up': 450 # 450 Hz lowpass
}
# Compute differential signals
diff_filtered, diff_raw = to_differential(
mats=mats,
sr=2000, # 2kHz sampling rate
f=filter_params
)
# Results:
# - diff_filtered[0].shape == (7, 1000) # Filtered differentials from emg1
# - diff_filtered[1].shape == (7, 1000) # Filtered differentials from emg2
# - diff_raw[0].shape == (7, 1000) # Raw differentials from emg1
# - diff_raw[1].shape == (7, 1000) # Raw differentials from emg2
Implementation Details
The differential computation: 1. Takes adjacent pairs of channels (j and j+1) 2. Computes their difference (channel_j+1 - channel_j) 3. Optionally applies a bandpass filter to the differential signals 4. Returns both filtered and unfiltered versions
This results in j-1 differential channels for j input channels.
Best Practices
- Signal Preparation:
- Ensure input signals are properly scaled
- Remove DC offset if present
-
Check for artifacts or bad channels before computing differentials
-
Filter Parameter Selection:
- Use standard EMG frequency bands (20-450 Hz typical)
- Match filter order to your signal quality needs
-
Consider edge effects in filtered signals
-
Channel Ordering:
- Ensure channels are ordered correctly spatially
- Verify channel alignment matches your electrode layout
- Document channel arrangement for reproducibility
Common Use Cases
- Reducing common-mode noise
- Enhancing local muscle activity detection
- Preprocessing for motor unit decomposition
- Improving spatial resolution of EMG recordings
Grid Mapping
hdsemg_shared.preprocessing.grid_map turns a flat channel matrix into HDsEMG
grid columns. It only selects and orders — no filtering, no differentiation.
The emg_map format
Follows openhdemg's custom_sorting_order: a list of lists (or a 2-D array)
where the outer index is the grid column and the inner index is the
row, holding base-0 channel numbers, with np.nan at positions that carry
no electrode or were excluded.
emg_map = [[0, 1, 2, 3 ], # col0
[4, 5, 6, 7 ], # col1
[8, 9, 10, np.nan]] # col2, one gap
API Reference
map_to_columns(emg_channels, emg_map)
emg_channels— rows are channels, columns are samples. May hold more channels than the map refers to (force and path references, for example).EMGFile.datais samples-by-channels, so pass its transpose.- returns a list of
nColsmatrices, eachnRows x nSamples, in map order. ANaNmap entry becomes an all-NaNrow.
Raises ValueError on a ragged map, an entry that is not a valid channel
number, or a channel used more than once.
emg_map_from_indices(emg_indices, rows, cols, order='column')
Builds a map from a Grid's flat emg_indices plus rows/cols.
order='column' (default) means the channels run down the first column
before moving to the next — this matches MATLAB's mat2grid. order='row'
transposes that.
Grids with an unwired position — HD08MM1305 has 64 electrodes in 5x13 = 65
positions — cannot be described this way and raise. Pass an explicit map with
NaN at the empty position instead; openhdemg's sort_rawemg and
hdsemg-select's electrode display grid both produce one.
Example Usage
import numpy as np
from hdsemg_shared.fileio.file_io import EMGFile
from hdsemg_shared.preprocessing.grid_map import emg_map_from_indices, map_to_columns
from hdsemg_shared.preprocessing.differential import to_differential
emg_file = EMGFile.load("recording.otb+")
grid = emg_file.grids[0]
emg_map = emg_map_from_indices(grid.emg_indices, grid.rows, grid.cols)
columns = map_to_columns(emg_file.data.T, emg_map)
dmat, dmat_no_filter = to_differential(columns, emg_file.sampling_frequency,
{"n": 2, "low": 20.0, "up": 450.0})
hdsemg_shared.global_parameters.global_amplitude takes the emg_map directly
and does the mapping and the MP/SD/DD differentiation itself.
Source Code
The implementations can be found in
src/hdsemg_shared/preprocessing/—differential.pyandgrid_map.py. The code includes type hints and detailed docstrings for easy integration with development tools.
API Documentation
to_differential(mats, sr, f)
Exact Python replica of MATLAB toDifferential.
Parameters
mats : list of ndarray
k matrices, each shape (j, T) where j = number of monopolar
EMG channels and T = samples.
sr : float
Sampling rate [Hz].
f : dict
Filter options – must contain keys
'n' : even Butterworth order (MATLAB style)
'low' : lower cut-off frequency [Hz]
'up' : upper cut-off frequency [Hz]
Returns
dmat : list of ndarray Filtered single-differential signals, each (j-1, T). dmatNoF : list of ndarray Unfiltered single-differential signals, each (j-1, T).
Source code in hdsemg_shared/preprocessing/differential.py
def to_differential(
mats: List[np.ndarray],
sr: Union[int, float],
f: Dict[str, Union[int, float]],
) -> Tuple[List[np.ndarray], List[np.ndarray]]:
"""
Exact Python replica of MATLAB `toDifferential`.
Parameters
----------
mats : list of ndarray
`k` matrices, each shape (j, T) where j = number of monopolar
EMG channels and T = samples.
sr : float
Sampling rate [Hz].
f : dict
Filter options – must contain keys
'n' : even Butterworth order (MATLAB style)
'low' : lower cut-off frequency [Hz]
'up' : upper cut-off frequency [Hz]
Returns
-------
dmat : list of ndarray
Filtered single-differential signals, each (j-1, T).
dmatNoF : list of ndarray
Unfiltered single-differential signals, each (j-1, T).
"""
# --------------- 1. sanity checks ----------
if not mats:
raise ValueError("`mats` must contain at least one matrix.")
first_rows = mats[0].shape[0]
if first_rows < 2:
raise ValueError("Each matrix must have at least two rows (channels).")
order = int(f["n"])
fcl = float(f["low"])
fch = float(f["up"])
if order < 2 or order % 2:
raise ValueError("Filter order 'n' must be an even integer ≥ 2.")
dmat: List[np.ndarray] = [None] * len(mats)
dmatNoF: List[np.ndarray] = [None] * len(mats)
# --------------- 2. loop -----------------------
for k, mat in enumerate(mats):
mat = np.asarray(mat, dtype=np.float64)
if mat.ndim != 2:
raise ValueError("Each element of `mats` must be 2-D (channels × samples).")
rows, T = mat.shape
if rows != first_rows:
raise ValueError("All matrices must have the same number of rows as mats[0].")
diff_raw = np.empty((rows - 1, T), dtype=np.float64)
diff_filt = np.empty_like(diff_raw)
for j in range(rows - 1):
delta = mat[j + 1, :] - mat[j, :]
diff_raw[j, :] = delta
diff_filt[j, :] = bandpass_filter(delta, order, fcl, fch, sr)
dmat[k] = diff_filt
dmatNoF[k] = diff_raw
return dmat, dmatNoF
GRID_MAP: turning a channel matrix into HDsEMG grid columns
An emg_map says which channel of a recording sits at which electrode position. The format follows openhdemg's custom_sorting_order: a list of lists (or a 2-D array) where the OUTER index is the grid COLUMN and the INNER index is the ROW, holding base-0 channel numbers, with NaN at positions that carry no electrode or were excluded.
emg_map = [[0, 1, 2, 3 ], # col0
[4, 5, 6, 7 ], # col1
[8, 9, 10, np.nan]] # col2, one gap
Nothing here filters or differentiates - it only selects and orders.
ADDITION of the Python implementation, no MATLAB equivalent (MATLAB's mat2grid takes gridSize from the caller's input structure).
(c) H Penasso. Written for hdsemg-shared by Claude Opus 5, 2026-08-19, from the MATLAB conventions of Global-HDsEMG-Analysis.
emg_map_from_indices(emg_indices, rows, cols, order='column')
EMG_MAP_FROM_INDICES builds an emg_map from a flat list of channel numbers
emg_map = emg_map_from_indices(grid.emg_indices, grid.rows, grid.cols)
INPUT emg_indices ... sequence of base-0 channel numbers, in acquisition order, exactly rows*cols of them [] rows ... number of electrode rows of the grid [] cols ... number of electrode columns of the grid [] order ... char, 'column' (default) if the channels run down the first column before moving to the next, 'row' if they run along the first row []
OUTPUT emg_map ... 2-D array, cols-by-rows, base-0 channel numbers, ready for map_to_columns []
RAISES ValueError ... if rows or cols are not positive, if order is unknown, or if len(emg_indices) != rows*cols
INFO ... grids with an unwired position, e.g. HD08MM1305 with 64 electrodes in 5x13 = 65 positions, cannot be described this way. Pass an explicit map with NaN at the empty position instead - openhdemg's sort_rawemg and hdsemg-select's electrode display grid both produce one
Source code in hdsemg_shared/preprocessing/grid_map.py
def emg_map_from_indices(emg_indices, rows, cols, order = 'column'):
"""
EMG_MAP_FROM_INDICES builds an emg_map from a flat list of channel numbers
emg_map = emg_map_from_indices(grid.emg_indices, grid.rows, grid.cols)
INPUT
emg_indices ... sequence of base-0 channel numbers, in acquisition
order, exactly rows*cols of them []
rows ... number of electrode rows of the grid []
cols ... number of electrode columns of the grid []
order ... char, 'column' (default) if the channels run down the
first column before moving to the next, 'row' if
they run along the first row []
OUTPUT
emg_map ... 2-D array, cols-by-rows, base-0 channel numbers, ready
for map_to_columns []
RAISES
ValueError ... if rows or cols are not positive, if order is unknown,
or if len(emg_indices) != rows*cols
*INFO* ... grids with an unwired position, e.g. HD08MM1305 with 64
electrodes in 5x13 = 65 positions, cannot be described this
way. Pass an explicit map with NaN at the empty position
instead - openhdemg's sort_rawemg and hdsemg-select's
electrode display grid both produce one
"""
if rows < 1 or cols < 1:
raise ValueError(f"rows and cols must be positive, got {rows} and {cols}.")
if order not in ('column', 'row'):
raise ValueError(f"order must be 'column' or 'row', got {order!r}.")
idx = np.asarray(emg_indices, dtype=np.float64).ravel()
if idx.size != rows * cols:
raise ValueError(
f"emg_indices holds {idx.size} channels but rows*cols is {rows * cols}. "
"A grid with an unwired position needs an explicit map with NaN there."
)
if order == 'column':
return idx.reshape(cols, rows)
# Acquisition ran along the rows, so reshape row-wise and transpose
return idx.reshape(rows, cols).T
map_to_columns(emg_channels, emg_map)
MAP_TO_COLUMNS splits a channel matrix into one matrix per grid column
columns = map_to_columns(emg.T, emg_map)
INPUT emg_channels ... matrix, each row contains one channel's signal over time (rows = channels, columns = samples). May hold more channels than the map refers to, e.g. force or path references [xV] emg_map ... list of lists or 2-D array, outer index = grid column, inner index = row, entries are base-0 channel numbers into emg_channels, NaN where no electrode sits []
OUTPUT columns ... list of nCols matrices, each nRows-by-nSamples, in map order. A NaN map entry becomes an all-NaN row [xV]
RAISES ValueError ... if the map is ragged or not 2-D, if an entry is not a valid row index of emg_channels, or if a channel is used more than once
INFO ... EMGFile.data is samples-by-channels, so pass its transpose INFO ... excluding a channel means putting NaN in its map position; global_amplitude then drops it, matching the MATLAB 'INFO NaN channels are ignored'
Source code in hdsemg_shared/preprocessing/grid_map.py
def map_to_columns(emg_channels, emg_map):
"""
MAP_TO_COLUMNS splits a channel matrix into one matrix per grid column
columns = map_to_columns(emg.T, emg_map)
INPUT
emg_channels ... matrix, each row contains one channel's signal over
time (rows = channels, columns = samples). May hold
more channels than the map refers to, e.g. force or
path references [xV]
emg_map ... list of lists or 2-D array, outer index = grid
column, inner index = row, entries are base-0
channel numbers into emg_channels, NaN where no
electrode sits []
OUTPUT
columns ... list of nCols matrices, each nRows-by-nSamples, in
map order. A NaN map entry becomes an all-NaN row
[xV]
RAISES
ValueError ... if the map is ragged or not 2-D, if an entry is not a
valid row index of emg_channels, or if a channel is
used more than once
*INFO* ... EMGFile.data is samples-by-channels, so pass its transpose
*INFO* ... excluding a channel means putting NaN in its map position;
global_amplitude then drops it, matching the MATLAB
'*INFO* NaN channels are ignored'
"""
emg = np.asarray(emg_channels, dtype=np.float64)
if emg.ndim != 2:
raise ValueError(f"emg_channels must be 2-D (channels x samples), got {emg.ndim}-D.")
grid = _as_map(emg_map)
_check_indices(grid, emg.shape[0])
# Build one matrix per column, NaN rows where the map has a gap
columns = []
for col in grid:
block = np.full((col.size, emg.shape[1]), np.nan, dtype=np.float64)
wired = ~np.isnan(col)
block[wired] = emg[col[wired].astype(int)]
columns.append(block)
return columns