Skip to content

tools

Rearrange EMG channels on an electrode grid and replace bad channels.

arrange_data_spatially

arrange_data_spatially(data: ndarray, ch_map: ndarray) -> np.ndarray

Arrange (samples, chs) data on the electrode grid given by the channel map.

Grid positions with a negative or out-of-range channel index are filled with zeros.

Parameters:

Name Type Description Default
data ndarray

Input data with shape (samples, chs).

required
ch_map ndarray

Channel map with shape (rows, cols) holding the 0-based channel index of each electrode, and -1 for empty positions.

required

Returns:

Type Description
ndarray

np.ndarray: Spatially arranged data with shape (rows, cols, samples).

Source code in emg_tools/tools.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def arrange_data_spatially(
    data: np.ndarray,
    ch_map: np.ndarray
    ) -> np.ndarray:

    """Arrange (samples, chs) data on the electrode grid given by the channel map.

    Grid positions with a negative or out-of-range channel index are filled with zeros.

    Args:
        data (np.ndarray): Input data with shape (samples, chs).
        ch_map (np.ndarray): Channel map with shape (rows, cols) holding the 0-based
            channel index of each electrode, and -1 for empty positions.

    Returns:
        np.ndarray: Spatially arranged data with shape (rows, cols, samples).
    """

    rows, cols = ch_map.shape
    samples, chs = data.shape
    data_spatial = np.zeros((rows, cols, samples))

    for row, col in np.ndindex(ch_map.shape):
        ch = ch_map[row, col]
        if (ch < 0) or (ch >= chs):
            continue
        data_spatial[row, col] = data[:, ch]

    return data_spatial

flatten_data_spatially

flatten_data_spatially(data: ndarray, ch_map: ndarray) -> np.ndarray

Flatten (rows, cols, samples) grid data back to (samples, chs).

Inverse of arrange_data_spatially. Channels missing from the map are filled with zeros.

Parameters:

Name Type Description Default
data ndarray

Input data with shape (rows, cols, samples).

required
ch_map ndarray

Channel map with shape (rows, cols) holding the 0-based channel index of each electrode, and -1 for empty positions.

required

Returns:

Type Description
ndarray

np.ndarray: Flattened data with shape (samples, chs), where chs is ch_map.max() + 1.

Source code in emg_tools/tools.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def flatten_data_spatially(
    data: np.ndarray,
    ch_map: np.ndarray
    ) -> np.ndarray:

    """Flatten (rows, cols, samples) grid data back to (samples, chs).

    Inverse of `arrange_data_spatially`. Channels missing from the map are filled with zeros.

    Args:
        data (np.ndarray): Input data with shape (rows, cols, samples).
        ch_map (np.ndarray): Channel map with shape (rows, cols) holding the 0-based
            channel index of each electrode, and -1 for empty positions.

    Returns:
        np.ndarray: Flattened data with shape (samples, chs), where chs is
            `ch_map.max() + 1`.
    """

    samples = data.shape[-1]
    chs = np.amax(ch_map) + 1

    data_flat = np.zeros((samples, chs))

    for ch in range(chs):
        idx = np.where(ch_map == ch)
        if len(idx[0]):
            data_flat[:, ch] = data[idx[0], idx[1]]

    return data_flat

replace_bad_ch

replace_bad_ch(data: ndarray, bad_ch: Union[list, ndarray], ch_map: ndarray) -> np.ndarray

Replace bad channels with the mean of their good neighbouring channels.

Neighbours are the up to 8 surrounding electrodes on the grid. Bad channels are replaced in the given order, so a replaced channel can serve as a neighbour for the next ones. Bad channels without good neighbours are set to zero.

Parameters:

Name Type Description Default
data ndarray

Input data with shape (samples, chs) or (rows, cols, samples).

required
bad_ch Union[list, ndarray]

0-based indices of the bad channels.

required
ch_map ndarray

Channel map with shape (rows, cols) holding the 0-based channel index of each electrode, and -1 for empty positions.

required

Returns:

Type Description
ndarray

np.ndarray: Copy of the data, same shape as the input, with the bad channels replaced.

Source code in emg_tools/tools.py
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
def replace_bad_ch(
    data: np.ndarray,
    bad_ch: Union[list, np.ndarray],
    ch_map: np.ndarray,
    ) -> np.ndarray:

    """Replace bad channels with the mean of their good neighbouring channels.

    Neighbours are the up to 8 surrounding electrodes on the grid. Bad channels are
    replaced in the given order, so a replaced channel can serve as a neighbour for the
    next ones. Bad channels without good neighbours are set to zero.

    Args:
        data (np.ndarray): Input data with shape (samples, chs) or (rows, cols, samples).
        bad_ch (Union[list, np.ndarray]): 0-based indices of the bad channels.
        ch_map (np.ndarray): Channel map with shape (rows, cols) holding the 0-based
            channel index of each electrode, and -1 for empty positions.

    Returns:
        np.ndarray: Copy of the data, same shape as the input, with the bad channels replaced.
    """

    # Initialise variables
    rows, cols = ch_map.shape
    data_out = copy(data)
    if isinstance(bad_ch, list):
        bad_ch_out = copy(bad_ch)
    else:
        bad_ch_out = copy(bad_ch).tolist()

    # Substitute bad channels until there are no more bad channels left
    while len(bad_ch_out) > 0:

        # Set updated bad channels to -1
        ch_map_out = copy(ch_map)
        for ch in bad_ch_out:
            ch_map_out[ ch_map == ch ] = -1

        # Get first bad channel
        curr_bad_ch = bad_ch_out.pop(0)

        # Find coordinates of the current bad channel
        [x, y] = np.where(ch_map == curr_bad_ch)
        x = x[0]
        y = y[0]

        # # Define mask
        neigh_mask = np.array([
            [x, y + 1],
            [x, y - 1],
            [x + 1, y],
            [x - 1, y],
            [x + 1, y + 1],
            [x + 1, y - 1],
            [x - 1, y + 1],
            [x - 1, y - 1]
            ])

        # Remove neighbours out of bounds
        in_range = np.logical_and(
            np.logical_and( neigh_mask[:,0] > -1, neigh_mask[:,0] < rows),
            np.logical_and( neigh_mask[:,1] > -1, neigh_mask[:,1] < cols)
        )
        neigh_mask = neigh_mask[in_range]
        in_range = in_range[in_range]

        # Check for good neighbours
        for i in range(neigh_mask.shape[0]):
            if (ch_map_out[ neigh_mask[i,0], neigh_mask[i,1] ] < 0):
                in_range[i] = False

        # Get the actual neighbouring channels
        neigh_mask = neigh_mask[in_range, :]
        neigh_chs = ch_map[neigh_mask[:,0], neigh_mask[:,1]]

        # Substitue channels (set to zero if there are no good neighbours)
        if len(data_out.shape) == 2: # (samples, chs) format
            if len(neigh_chs) == 0:
                data_out[:, curr_bad_ch] = 0
            else:
                data_out[:, curr_bad_ch] = np.mean( data_out[:, neigh_chs], axis=-1)
        elif len(data_out.shape) == 3: # (ch_rows, ch_cols, samples) format
            if len(neigh_chs) == 0:
                data_out[x,y] = 0
            else:
                data_out[x,y] = np.mean( data_out[neigh_mask[:,0], neigh_mask[:,1]], axis=0)

    return data_out