Skip to content

Commit

Permalink
Merge pull request #4 from cvjena/develop
Browse files Browse the repository at this point in the history
Develop - Internal simplifications
  • Loading branch information
Timozen authored Nov 8, 2023
2 parents f4bd021 + 1d9866a commit 9aa41e8
Show file tree
Hide file tree
Showing 10 changed files with 376 additions and 300 deletions.
22 changes: 12 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ A small demo is hosted [here](https://semg.inf-cv.uni-jena.de/), together with t

- **Easy to use**: The package provides a straightforward interface, making it accessible for users of all levels of expertise.
- **Visualize muscle activity**: The EMG Intensity plot allows you to visualize the intensity of muscle activity over the face, providing insights into patterns and variations.
- **Designed explicitly for facial** muscles**: The tool focuses on facial muscles, enabling you to study and understand muscle activity in the face, which can be particularly useful in fields like facial expression analysis, neuroscience, and rehabilitation.
- **Designed explicitly for facial muscles**: The tool focuses on facial muscles, enabling you to study and understand muscle activity in the face, which can be particularly useful in fields like facial expression analysis, neuroscience, and rehabilitation.
- **FACS**: Visualize the Facial Action Coding System at the correct anatomical locations for a more intuitive understanding of the data.
- **Potential for extension**: While the current focus is on facial muscles, this tool could potentially be extended to analyze other muscle groups.
- **Beyond muscles**: The tool can also be used to plot additional facial information, such as oxygen saturation, but this is not officially supported yet.


## Installation

The package is available on [PyPI](https://pypi.org/project/electromyogram/) and can be installed with `pip`:
Expand All @@ -31,15 +33,15 @@ cd electromyogram
pip install -e .
```

## Usage
## Quickstart

This tool is intended to simplify the creation of only the spatial intensity map for surface EMG data.
All the required preprocessing of the data is not part of this package and is likely project-specific.
We assume that the data is given in a dictionary (or pandas table) and the keys are the sensor locations.

Then, the correct physical interpolation between the sensors is done, and the result is a 2D array of the interpolated values on the canonical face model.
You can then apply different color maps to the interpolation to create the final plot.
Detailed examples with test data can be found in `examples/`.
We provide detailed examples in `examples/`.


```python
Expand All @@ -62,21 +64,21 @@ For the colorization, the users can use any color map from [matplotlib](https://
We currently support the two following schematics for acquiring the EMG data.
If you want to have your own, please open an issue or create a pull request, and we will be happy to add it.

| [Fridlund and Cappacio, 1986](https://pubmed.ncbi.nlm.nih.gov/3809364/) | [Kuramoto et al., 2019](https://onlinelibrary.wiley.com/doi/10.1002/npr2.12059) |
|---|---|
| ![Locations ](files/locations_fridlund.jpg) | ![Locations ](files/locations_kuramoto.jpg) |
| [Fridlund and Cappacio, 1986](https://pubmed.ncbi.nlm.nih.gov/3809364/) | [Kuramoto et al., 2019](https://onlinelibrary.wiley.com/doi/10.1002/npr2.12059) | [Ekman and Friesen - FACS](https://psycnet.apa.org/record/1971-07999-001)|
|---|---|---|
| ![Locations ](files/locations_fridlund.jpg) | ![Locations ](files/locations_kuramoto.jpg) | ![Locations ](files/locations_facs.jpg) |

If you want to define your own scheme, just create a new class that inherits from `emg.Schematic` and implement the `get_sensor_locations` function.
Then use it in the `interpolate` function, and you are good to go.
If you want to define your custom scheme, create a new class inherited from `emg.Schematic` and implement the `locations` member. If you support the mirroring of the face, implement the `pairs_L` and `pairs_R` members.
Then, use it in the `interpolate` function, and you are good to go.

## Todos

- [ ] Handle if not all values are given for a better schematic
- [X] Handle if not all values are given for a better schematic
- [X] Add result images
- [X] Add a function to draw triangulation onto the 2D canvas
- [X] Add a function to draw sensor locations onto the 2D canvas
- [X] Add the option to remove the area outside the canonical face model
- [ ] Make a better interface for the channel names
- [X] Make a better interface for the channel names (mapping to defined terms)
- [ ] Add function to create the according colorbar for matplotlib in the correct size

## License
Expand Down
16 changes: 8 additions & 8 deletions examples/emotions.ipynb

Large diffs are not rendered by default.

49 changes: 29 additions & 20 deletions examples/locations.ipynb

Large diffs are not rendered by default.

147 changes: 147 additions & 0 deletions examples/schemes.ipynb

Large diffs are not rendered by default.

Binary file added files/locations_facs.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion src/electromyogram/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"plot_locations",
"Kuramoto",
"Fridlund",
"FACS",
"Scheme",
"colorize",
"get_colormap",
Expand All @@ -17,4 +18,4 @@
postprocess,
)

from electromyogram.schemes import Fridlund, Kuramoto, Scheme
from electromyogram.schemes import FACS, Fridlund, Kuramoto, Scheme
90 changes: 0 additions & 90 deletions src/electromyogram/locations_fridlund.json

This file was deleted.

78 changes: 0 additions & 78 deletions src/electromyogram/locations_kuramoto.json

This file was deleted.

33 changes: 18 additions & 15 deletions src/electromyogram/plot.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
__all__ = ["interpolate", "plot_locations", "colorize", "get_colormap", "postprocess"]


from dataclasses import dataclass
import math
from typing import Optional, Type, Union
from dataclasses import dataclass
from pathlib import Path
from typing import Optional, Type, Union

import cv2
import h5py
import numpy as np
from matplotlib import pyplot as plt
from scipy import interpolate as interp

from .consts import parula_colormap
from .schemes import Scheme
from .utils import rel_to_abs


@dataclass
class FaceModel:
points: np.ndarray
Expand Down Expand Up @@ -93,7 +93,7 @@ def plot_locations(
radius_i = int((radius * 0.8) * scale_factor)

for emg_name, emg_loc in scheme.locations.items():
name = scheme.shortcuts.get(emg_name, emg_name)
name = scheme.mapping.get(emg_name, emg_name)
x, y = rel_to_abs(emg_loc[0], emg_loc[1], size=shape)

canvas = cv2.circle(canvas, (x, y), radius=radius_o, color=( 0, 0, 0), thickness=-1, lineType=lineType)
Expand All @@ -120,18 +120,19 @@ def interpolate(
vmax: Optional[float] = None,
mirror: bool = False,
mirror_plane_width: int = 2,
missing_value: float = 0.0,
) -> Union[np.ndarray, tuple[np.ndarray, np.ndarray, np.ndarray]]:
if not mirror:
return __interpolate(scheme, emg_values, shape, vmin, vmax)
return __interpolate(scheme, emg_values, shape, vmin, vmax, missing_value)

emg_values_mirrored_l, emg_values_mirrored_r = scheme.mirror(emg_values)

if vmax is None:
vmax = max(emg_values.values())

interpolation_n = __interpolate(scheme, emg_values, shape, vmin, vmax)
interpolation_l = __interpolate(scheme, emg_values_mirrored_l, shape, vmin, vmax)
interpolation_r = __interpolate(scheme, emg_values_mirrored_r, shape, vmin, vmax)
interpolation_n = __interpolate(scheme, emg_values, shape, vmin, vmax, missing_value)
interpolation_l = __interpolate(scheme, emg_values_mirrored_l, shape, vmin, vmax, missing_value)
interpolation_r = __interpolate(scheme, emg_values_mirrored_r, shape, vmin, vmax, missing_value)

# draw a vertical line in the mirrored images to indicate the mirror plane
middle_slice = slice(shape[1] // 2 - mirror_plane_width, shape[1] // 2 + mirror_plane_width)
Expand All @@ -147,6 +148,7 @@ def __interpolate(
shape: tuple[int, int] = (512, 512),
vmin: float = 0.0,
vmax: Optional[float] = None,
missing_value: float = 0.0,
) -> np.ndarray:
"""Interpolate the EMG values to a 2D canvas.
Expand All @@ -169,23 +171,24 @@ def __interpolate(
The maximum value of the EMG values. Defaults to None and will be set to the maximum value of the EMG values.
"""

scheme.valid(emg_values) # this raises a ValueError if the values are not valid

emg_values = scheme.validify(emg_values, missing_value=missing_value)
canvas = np.zeros(shape, dtype=np.float32)
outer_dict = {f"O{i}" : (x,y) for i, (x,y) in enumerate(face_model.get_outer(shape=shape))}

keys_sorted_semg = sorted(scheme.locations.keys())
keys_sorted_hull = sorted(scheme.outer_dict.keys())
keys_sorted_hull = sorted(outer_dict.keys())

# # get the values for each location
xy = np.array([scheme.locations[k] for k in keys_sorted_semg] + [scheme.outer_dict[k] for k in keys_sorted_hull])
v = np.array([emg_values[k] for k in keys_sorted_semg] + [0] * len(keys_sorted_hull))
# get the values for each location
v = np.array([emg_values[k][0] for k in keys_sorted_semg] + [0] * len(keys_sorted_hull))
xy = np.array([emg_values[k][1] for k in keys_sorted_semg] + [outer_dict[k] for k in keys_sorted_hull])

vmin = vmin or v.min()
vmax = vmax or v.max()
lmax = v.max()

# prepare the data for RBF interpolation
p = xy.reshape(-1, 2)
v = v.reshape(-1, 1)
v = v.reshape(-1, 1)
x_grid = np.mgrid[-100 : 100 : canvas.shape[0] * 1j, -100 : 100 : canvas.shape[1] * 1j].reshape(2, -1).T
Z = interp.RBFInterpolator(p, v, kernel="thin_plate_spline", smoothing=0.0)(x_grid)
# reshape the data to the correct shape, and transpose it such it is rotated 90 degrees counter-clockwise
Expand Down
Loading

0 comments on commit 9aa41e8

Please sign in to comment.