Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
240 commits
Select commit Hold shift + click to select a range
6a95aba
Update feature extraction for heatmap visualization
cbmorrell Jul 24, 2024
1bf5dc3
Change features to fe in OnlineStreamer
cbmorrell Jul 24, 2024
3bd2657
Offline regression example
cbmorrell Aug 8, 2024
94091d9
Merge pull request #66 from LibEMG/main
eeddy Aug 14, 2024
353e286
Example for new streamer
eeddy Aug 14, 2024
df08317
Revert "Example for new streamer"
eeddy Aug 14, 2024
34de77a
Rename ColumnFetch to ColumnFetcher
cbmorrell Aug 16, 2024
2da2612
Remove duplicate method
cbmorrell Aug 16, 2024
bc0972f
Add feature queue for time series models
cbmorrell Aug 16, 2024
f4d9e4f
Move constructor docstrings outside __init__
cbmorrell Aug 16, 2024
af1c726
Regression explanation
cbmorrell Aug 16, 2024
ae71d0f
Add explicit conditional check instead of relying on casting
cbmorrell Aug 19, 2024
8e71705
Only pop if queue is at max length
cbmorrell Aug 19, 2024
cfc9a84
Add feature queue parameter to child classes
cbmorrell Aug 19, 2024
6d83b96
Add online channel mask
cbmorrell Aug 19, 2024
f27c6e3
Merge branch 'online-model-feature-queue' into feature-extractor-rework
cbmorrell Aug 19, 2024
19f2049
Revert "Revert "Rework FeatureExtractor""
cbmorrell Aug 19, 2024
81318c1
Add skip until buffer fills up
cbmorrell Aug 19, 2024
d30f608
Merge pull request #68 from LibEMG/rename-column-fetch
eeddy Aug 20, 2024
457e5ec
Merge pull request #59 from LibEMG/offline-regression-example
eeddy Aug 20, 2024
e8701ad
Add TODO
cbmorrell Aug 20, 2024
5a6bd18
Add wildcard to regex helper
cbmorrell Aug 21, 2024
88821ef
Add check for None in RegexFilter
cbmorrell Aug 21, 2024
c5c8748
Add wildcard to regex helper
cbmorrell Aug 21, 2024
2e63dfd
Add check for None in RegexFilter
cbmorrell Aug 21, 2024
87a4003
Add MEC24 workshop to landing page
cbmorrell Aug 22, 2024
e8ddc12
Add post-processing visual to regression example
cbmorrell Aug 22, 2024
34cecb3
Add skip until buffer fills up
cbmorrell Aug 19, 2024
784372c
Regression visualization
cbmorrell Aug 23, 2024
ee3503e
Handle coordinates without steady state frames
cbmorrell Aug 23, 2024
30ef842
Animation documentation
cbmorrell Aug 23, 2024
ac127c3
Add heatmap visualization
cbmorrell Aug 23, 2024
a751cbb
Add string parameters for common metadata operations
cbmorrell Aug 26, 2024
124a1dd
Merge pull request #71 from LibEMG/common-metadata-operations
ECEEvanCampbell Aug 26, 2024
60249b0
ninapro db2 dataglove support added
A-R-Hariri Aug 27, 2024
5d92b81
Merge branch 'feature-extractor-rework' into online-model-feature-queue
cbmorrell Aug 27, 2024
29e677c
Revert "Revert "Revert "Rework FeatureExtractor"""
cbmorrell Aug 27, 2024
8aaf19d
Add online standardization method
cbmorrell Aug 27, 2024
80969d8
Use proper FeatureExtractor interface
cbmorrell Aug 27, 2024
3466dad
Fix heatmap feature extraction
cbmorrell Sep 4, 2024
5f20583
Add clarification to install_standardization docstring
cbmorrell Sep 4, 2024
2c10637
Fix online regressor visualize lag
cbmorrell Sep 4, 2024
7c86fa3
Add docstring to OnlineEMGRegressor.visualize
cbmorrell Sep 4, 2024
5bfe048
Skeleton for Controller class
cbmorrell Sep 4, 2024
6f5e72d
Check for correct MacOS string
cbmorrell Sep 5, 2024
318fc0c
ninapro db2
A-R-Hariri Sep 6, 2024
24c1951
Remove unnecessary pop of oldest window
cbmorrell Sep 6, 2024
9aa3f4e
Create SocketController
cbmorrell Sep 6, 2024
f18ec91
Add comment idea
cbmorrell Sep 6, 2024
48beed0
Add abstract timestamp parsing function
cbmorrell Sep 6, 2024
7ce9bc1
RegressorController
cbmorrell Sep 6, 2024
8254942
Merge pull request #79 from LibEMG/oymotion-streamer-incorrect-os-check
ECEEvanCampbell Sep 9, 2024
95e1111
Merge pull request #73 from LibEMG/online-model-feature-queue
ECEEvanCampbell Sep 9, 2024
25460b3
updates sifi streamer to accept bridge version. forked sifi bridge in…
ECEEvanCampbell Sep 9, 2024
deb3a98
Remove model from RegressorController
cbmorrell Sep 9, 2024
8a20f1b
db2 dataglove support limited to NinaproDB2 class
A-R-Hariri Sep 9, 2024
fb41e9a
ClassifierController implementation
cbmorrell Sep 9, 2024
edb395d
Fix online regressor visualize lag
cbmorrell Sep 4, 2024
19769b5
Add option to parse timestamp
cbmorrell Sep 9, 2024
030d6b2
Remove OnlineEMGRegressor.parse_output and references
cbmorrell Sep 9, 2024
6e436a9
documentation for NinaproDB2
A-R-Hariri Sep 9, 2024
c943fff
Merge pull request #80 from AmirRezaHariri/ninapro_regression
cbmorrell Sep 9, 2024
68942af
Merge pull request #81 from LibEMG/sifi-breakout
cbmorrell Sep 9, 2024
7d0a7d6
Remove duplicate method
cbmorrell Sep 13, 2024
5073d27
Throw error when streaming stops during SGT
cbmorrell Sep 13, 2024
e38e66c
Update .gitignore
cbmorrell Sep 13, 2024
2b49cf7
Convert methods to private
cbmorrell Sep 13, 2024
2142233
Regression Fitts task
cbmorrell Sep 13, 2024
e2357e5
Added sifi-bridge-py to dependencies. Renamed executable to sifibridge
Sep 24, 2024
6965053
cleanup init and sifibridge configurations
Sep 24, 2024
190818c
Change raw writes to sbp methods
Sep 24, 2024
c547681
Updated some redundant parameters and docstrings
Sep 24, 2024
29aafb2
Remove executable from repo
Sep 24, 2024
45c8034
Fixed sifibridge process launch, added sifibridge to gitignore
Sep 24, 2024
a7a839f
Fix gitignore
Sep 24, 2024
ef60d25
Fixed cleanup
Sep 24, 2024
48f907b
make streamer function more in-line with sifi-breakout
Sep 27, 2024
62b0064
docstring, change 'device' to 'name'
Sep 27, 2024
9ae16eb
Remove metric calculation functions
cbmorrell Oct 4, 2024
38031de
Implement classifier controller for visualization
cbmorrell Oct 4, 2024
633f173
Add check for data not being received
cbmorrell Oct 4, 2024
063b965
Add EMG hero legacy code
cbmorrell Oct 4, 2024
e71897c
Replace deque with multiprocessing Queue
cbmorrell Oct 5, 2024
e3b27e5
Fix online ClassifierController parsing
cbmorrell Oct 5, 2024
f846b0b
Fix OnlineEMGClassifier.visualize
cbmorrell Oct 5, 2024
e5e1c26
Automatically start Controller in Environment
cbmorrell Oct 5, 2024
2db5c59
Classifier working in FittsLawTest
cbmorrell Oct 5, 2024
7fb7dd6
Rename FittsLawTest to IsoFitts
cbmorrell Oct 5, 2024
bc16d76
Move common functionality to run method
cbmorrell Oct 5, 2024
f40619c
Update OnlineEMGClassifier visualize docstring
cbmorrell Oct 6, 2024
a697bd1
Hide pygame welcome message
cbmorrell Oct 6, 2024
92756ea
Move functionality to Environment class and Fitts parameters
cbmorrell Oct 6, 2024
5812e7a
Rework prediction map
cbmorrell Oct 6, 2024
b022309
Convert EMGHero to Environment
cbmorrell Oct 6, 2024
f7a126f
Rework _get_action for queue
cbmorrell Oct 6, 2024
d687ec9
Remove _parse_timestamp from Controller class
cbmorrell Oct 6, 2024
a6435c8
Fix prediction map for IsoFitts
cbmorrell Oct 6, 2024
a729a9b
Implemented KeyboardController
cbmorrell Oct 6, 2024
5a7721c
Properly receive None in EMGHero
cbmorrell Oct 6, 2024
4e1fb29
Split environments.py into multiple files
cbmorrell Oct 7, 2024
459c1a2
Add environments to __init__
cbmorrell Oct 7, 2024
c2bf8f5
Fix environment imports
cbmorrell Oct 7, 2024
f8ae722
Hide pygame welcome message
cbmorrell Oct 7, 2024
31d5a46
IsoFitts direction fix
cbmorrell Oct 7, 2024
87ff27e
Convert IsoFitts methods to hidden methods
cbmorrell Oct 7, 2024
277cb14
Environments documentation
cbmorrell Oct 7, 2024
d056439
Rework SocketController
cbmorrell Oct 7, 2024
fad6227
Add wait in OnlineEMGRegressor.visualize
cbmorrell Oct 7, 2024
9822c14
Merge pull request #83 from LibEMG/duplicate-method-in-ninaprodb2
ECEEvanCampbell Oct 8, 2024
d28d0f6
Merge pull request #84 from LibEMG/handle-sgt-streaming-interruption
ECEEvanCampbell Oct 8, 2024
391ab53
Merge pull request #74 from LibEMG/online-regressor-visualization-lag
ECEEvanCampbell Oct 8, 2024
b6121a9
Merge branch 'latest' into game-environments
ECEEvanCampbell Oct 8, 2024
a8367ee
Merge pull request #89 from LibEMG/game-environments
ECEEvanCampbell Oct 8, 2024
bf252a2
fixed windows compilation target
Oct 8, 2024
b483dcf
Override terminate method
cbmorrell Oct 8, 2024
157dc45
Allow non-callable dtypes
cbmorrell Oct 8, 2024
ff61e87
Convert Manager to SharedMemory
cbmorrell Oct 8, 2024
265e482
Add proportional control flag to IsoFitts
cbmorrell Oct 8, 2024
75a71ea
Automatically create parent directories when saving coordinates
cbmorrell Oct 9, 2024
278c202
Update Fitts example with regression
cbmorrell Oct 9, 2024
2ffb798
added sifi bridge to setup
ECEEvanCampbell Oct 10, 2024
938d797
Launch the subprocess in run method for pickling issues
gabrielpgagne Oct 10, 2024
f2c0d5a
Merge branch 'latest' into feature/sifibridge-1.1
ECEEvanCampbell Oct 10, 2024
8dce28b
Merge pull request #93 from gabrielpgagne/feature/sifibridge-1.1
ECEEvanCampbell Oct 10, 2024
d51fe42
Merge pull request #91 from LibEMG/socket-controller-shared-memory
ECEEvanCampbell Oct 10, 2024
ac9ce2f
Update streamers.py
ECEEvanCampbell Oct 10, 2024
4a73b76
removed version from sifi bridge import
ECEEvanCampbell Oct 16, 2024
2fb22a1
added more checks for creating variables
ECEEvanCampbell Oct 16, 2024
b4c42e6
SocketController Lock Fix
cbmorrell Oct 16, 2024
9410fff
Add pygame to setup.py
cbmorrell Oct 16, 2024
22cdcf1
Merge pull request #94 from LibEMG/socket-controller-shared-memory-lo…
ECEEvanCampbell Oct 17, 2024
d1ff3ee
Merge pull request #92 from LibEMG/regression-documentation
ECEEvanCampbell Oct 17, 2024
e58b1cd
Simplify SocketController
cbmorrell Oct 17, 2024
0d95a50
Make socket non-blocking
cbmorrell Oct 17, 2024
f9a8644
Replace classifier_smm_writes with model_smm_writes
cbmorrell Oct 17, 2024
67dc160
Pass model timestamp instead of current time
cbmorrell Oct 17, 2024
d6b0486
Fixed saving of IMU data
ulysseTM Oct 18, 2024
c48b3f8
Remove Delsys pasted docstring
cbmorrell Oct 18, 2024
81727d0
Merge pull request #97 from ulysseTM/patch-1
ECEEvanCampbell Oct 20, 2024
4b04b99
Add numpy import to Delsys API streamer
cbmorrell Oct 23, 2024
8e6ad11
Add check for num_channels in delsys_api_streamer
cbmorrell Oct 23, 2024
1e02c49
Add support for .json when logging environment
cbmorrell Oct 24, 2024
25492e4
Add target and distance radii parameters
cbmorrell Oct 24, 2024
96f7e07
Add option to redo final rep
cbmorrell Oct 29, 2024
960d6f9
Show "Rep X of Y" during data collection
cbmorrell Oct 29, 2024
6e32c11
Automatically calculate rep time for videos
cbmorrell Oct 30, 2024
cf1e181
Find labels files and move to corresponding data directory
cbmorrell Oct 30, 2024
1bbff78
Throw ConnectionError during start callback
cbmorrell Oct 30, 2024
08b0428
GUI docstring update
cbmorrell Oct 30, 2024
a27ca1f
Merge pull request #98 from LibEMG/sgt-gui-improvements
eeddy Oct 31, 2024
f5bf631
Merge pull request #96 from LibEMG/simplify-controllers
eeddy Oct 31, 2024
dfb2c69
Add game time parameter
cbmorrell Nov 12, 2024
3113b50
Removed unused dependencies in sifibridge streamer. Set EDA to 0Hz by…
Nov 12, 2024
97be849
Merge pull request #100 from gabrielpgagne/bugfix/sifi-bridge-py-1.2
ECEEvanCampbell Nov 13, 2024
5a89b2d
Add regression info to docstring
cbmorrell Nov 13, 2024
dc296cf
Add timestamps to KeyboardController
cbmorrell Nov 13, 2024
510fd1f
Draw cursor for RotationalIsoFitts
cbmorrell Nov 13, 2024
fb3f835
WIP: converting to using polygons
cbmorrell Nov 20, 2024
56dfe95
Rename isofitts module to fitts
cbmorrell Nov 20, 2024
5617bfd
Rename IsoFitts class to ISOFitts
cbmorrell Nov 20, 2024
a245816
Create PolarFitts
cbmorrell Nov 20, 2024
959caf7
Fitts base class
cbmorrell Nov 20, 2024
d89af62
Remove duplicated code from ISOFitts
cbmorrell Nov 20, 2024
a087460
Inherit from Fitts instead of ISOFitts for PolarFitts
cbmorrell Nov 20, 2024
adb2695
Remove dead code
cbmorrell Nov 20, 2024
e81d6e6
Create Fitts targets in uniform circle
cbmorrell Nov 21, 2024
39dc79a
Handle different target sizes
cbmorrell Nov 21, 2024
fe9ae77
Custom directions for PolarFitts prediction_map
cbmorrell Nov 21, 2024
5bf193e
Set minimum target size based on cursor
cbmorrell Nov 21, 2024
c028774
Reimplement feature parameters online
cbmorrell Nov 21, 2024
1ca93c4
Only add to smm_items list if tag has not already been provided
cbmorrell Nov 21, 2024
bfeacaf
Fix check for cursor being on new goal target
cbmorrell Nov 21, 2024
1a2f147
Keep radius on screen
cbmorrell Nov 21, 2024
b168f98
Fix incorrect polar to cartesian action map
cbmorrell Nov 21, 2024
ad08df5
Restrict PolarFitts to a single side
cbmorrell Nov 21, 2024
1799048
Removed unused dependencies in sifibridge streamer. Set EDA to 0Hz by…
Nov 12, 2024
125ed3a
Stop targets from generating in the center of the screen
cbmorrell Nov 24, 2024
1110a3f
Set theta pointed at bottom of screen instead of right
cbmorrell Nov 24, 2024
02cb852
Docstring for PolarFitts
cbmorrell Nov 24, 2024
2cd4b73
Initialize theta to pi/2
cbmorrell Nov 25, 2024
9a8fc8f
Remap theta direction to correspond to prediction direction
cbmorrell Nov 25, 2024
a7bfbc4
Fix visualize_heatmap throwing error for feature lists of length 1
cbmorrell Nov 25, 2024
143f0a5
WIP: Functional conversion from Cartesian to Polar space
cbmorrell Nov 26, 2024
2b16984
Remove dead code
cbmorrell Nov 26, 2024
ea1a5dd
Pass in rect when drawing to polar space
cbmorrell Nov 26, 2024
84694d5
Add fill parameter for drawing polar circle
cbmorrell Nov 26, 2024
b387b2b
Fix logical error when checking if new target is on cursor
cbmorrell Nov 26, 2024
9557de0
Combine PolarFitts into parent Fitts class
cbmorrell Nov 26, 2024
65e7699
Fix check for cursor remaining on screen
cbmorrell Nov 26, 2024
9ce1a0e
Update Fitts and ISOFitts docstrings
cbmorrell Nov 26, 2024
0a73901
Add option to draw radius in polar Fitts
cbmorrell Nov 26, 2024
1c5f514
Extract origin to variable
cbmorrell Nov 26, 2024
154496c
Improve check for cursor staying in bounds
cbmorrell Nov 27, 2024
e4ed8b3
Add check in ISOFitts for target distance radius and screen size
cbmorrell Nov 27, 2024
ee00681
Improve error handling for small screen size error
cbmorrell Nov 27, 2024
5fe02b8
Fix fstring formatting in error message
cbmorrell Nov 27, 2024
47f1837
Use full screen size when possible
cbmorrell Nov 27, 2024
3275158
Switch polar Fitts to vertical layout instead of horizontal
cbmorrell Nov 27, 2024
569ec35
Change default num_targets in ISOFitts
cbmorrell Nov 27, 2024
b617a4a
Add parameters for target and cursor color
cbmorrell Nov 27, 2024
f3d9e46
Extract Fitts parameters to config dataclass
cbmorrell Nov 27, 2024
d25b360
Merge pull request #103 from LibEMG/visualize-heatmap-fix
eeddy Nov 28, 2024
9356b44
Update colors
cbmorrell Nov 28, 2024
8da18f8
Make OnlineDataHandler processes daemons
cbmorrell Nov 28, 2024
eb9187e
Dataset Updates (#85)
eeddy Nov 29, 2024
defc993
Fixed setup issue
eeddy Nov 29, 2024
bf52580
Remove incorrect docstring
cbmorrell Dec 3, 2024
d672145
Reset dwell timer in _get_new_goal_target
cbmorrell Dec 3, 2024
57cdf2f
Add HyserMVC Dataset (#105)
cbmorrell Dec 5, 2024
e1e5d1e
Wait to start timers until data have been received
cbmorrell Dec 5, 2024
ea17e42
Add cursor_radius parameter to FittsConfig
cbmorrell Dec 5, 2024
46d7d86
Updated the normalization in the feature extractor
eeddy Dec 7, 2024
7b4a727
Modify FittsConfig default parameters
cbmorrell Dec 13, 2024
71845f9
Update _delsys_streamer.py
ECEEvanCampbell Jan 15, 2025
afe7b5e
Added option for dictionary list
eeddy Jan 25, 2025
f40e3ef
Added option for dictionary list
eeddy Jan 25, 2025
2f161f0
Added a fix feature errors to feature extractor
eeddy Jan 28, 2025
7e7808a
Changed feature_extractor to default to fix errors is false
eeddy Jan 28, 2025
a45013d
Changed dataset names
eeddy Jan 29, 2025
aeea667
Updated feature dictionary
eeddy Jan 30, 2025
33a5177
Updates
eeddy Jan 30, 2025
265d0ae
Merge pull request #104 from LibEMG/isofitts-improvements
ECEEvanCampbell Feb 11, 2025
fb716b7
Remove old calls to Controller.start
cbmorrell Mar 27, 2025
49617f7
Merge pull request #113 from LibEMG/online-visualize-bug-fix
ECEEvanCampbell Mar 27, 2025
5a85608
Fixed code example for feature performance (#118)
cbmorrell Jun 3, 2025
17e56e6
added pca visualization to online data handler (#110)
eeddy Jun 3, 2025
54ed325
Regressor Visualize On One Axis (#112)
cbmorrell Jun 3, 2025
b7f3f01
Streamer Cleanup (#115)
cbmorrell Jun 3, 2025
701180c
Updated README
eeddy Jun 3, 2025
0b22023
Merge branch 'main' into latest
eeddy Jun 3, 2025
1e3981e
Updated yaml
eeddy Jun 3, 2025
8dea103
Added option to configure BLE Power and Memory Mode (#121)
eeddy Jun 3, 2025
46d8af5
Updated version
eeddy Jun 3, 2025
37c3b1b
added connection to specific myo armband via MAC address (#122)
Tob1n8tor Jun 24, 2025
5402d98
EMG Streaming Pipeline for OTB MuoviPlus (#124)
Tob1n8tor Jul 24, 2025
8674552
Merge branch 'main' into latest
eeddy Jul 25, 2025
a36a69c
Fixed CRC error
eeddy Aug 21, 2025
de37630
Emager v3 (#127)
Michiboi29 Nov 12, 2025
76381c3
Adding discrete control functionality to libemg (beta) (#129)
eeddy Jan 22, 2026
c63d68a
Merge branch 'main' into latest
eeddy Jan 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,4 @@ MLPR.py
docs/Makefile
sifibridge-*
*.pyc
*.model
135 changes: 135 additions & 0 deletions docs/source/documentation/prediction/discrete_classification_doc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Discrete Classifiers

Unlike continuous classifiers that output a prediction for every window of EMG data, discrete classifiers are designed for recognizing transient, isolated gestures. These classifiers operate on variable-length templates (sequences of windows) and are well-suited for detecting distinct movements like finger snaps, taps, or quick hand gestures.

Discrete classifiers expect input data in a different format than continuous classifiers:
- **Continuous classifiers**: Operate on individual windows of shape `(n_windows, n_features)`.
- **Discrete classifiers**: Operate on templates (sequences of windows) where each template has shape `(n_frames, n_features)` and can vary in length.

To prepare data for discrete classifiers, use the `discrete=True` parameter when calling `parse_windows()` on your `OfflineDataHandler`:

```Python
from libemg.data_handler import OfflineDataHandler

odh = OfflineDataHandler()
odh.get_data('./data/', regex_filters)
windows, metadata = odh.parse_windows(window_size=50, window_increment=10, discrete=True)
# windows is now a list of templates, one per file/rep
```

For feature extraction with discrete data, use the `discrete=True` parameter:

```Python
from libemg.feature_extractor import FeatureExtractor

fe = FeatureExtractor()
features = fe.extract_features(['MAV', 'ZC', 'SSC', 'WL'], windows, discrete=True, array=True)
# features is a list of arrays, one per template
```

## Majority Vote LDA (MVLDA)

A classifier that applies Linear Discriminant Analysis (LDA) to each frame within a template and uses majority voting to determine the final prediction. This approach is simple yet effective for discrete gesture recognition.

```Python
from libemg._discrete_models import MVLDA

model = MVLDA()
model.fit(train_features, train_labels)
predictions = model.predict(test_features)
probabilities = model.predict_proba(test_features)
```

## Dynamic Time Warping Classifier (DTWClassifier)

A template-matching classifier that uses Dynamic Time Warping (DTW) distance to compare test samples against stored training templates. DTW is particularly useful when gestures may vary in speed or duration, as it can align sequences with different temporal characteristics.

```Python
from libemg._discrete_models import DTWClassifier

model = DTWClassifier(n_neighbors=3)
model.fit(train_features, train_labels)
predictions = model.predict(test_features)
probabilities = model.predict_proba(test_features)
```

The `n_neighbors` parameter controls how many nearest templates are used for voting (k-nearest neighbors with DTW distance).

## Pretrained Myo Cross-User Model (MyoCrossUserPretrained)

A pretrained deep learning model for cross-user discrete gesture recognition using the Myo armband. This model uses a convolutional-recurrent architecture and recognizes 6 gestures: Nothing, Close, Flexion, Extension, Open, and Pinch.

```Python
from libemg._discrete_models import MyoCrossUserPretrained

model = MyoCrossUserPretrained()
# Model is automatically downloaded on first use

# The model provides recommended parameters for OnlineDiscreteClassifier
print(model.args)
# {'window_size': 10, 'window_increment': 5, 'null_label': 0, ...}

predictions = model.predict(test_data)
probabilities = model.predict_proba(test_data)
```

This model expects raw windowed EMG data (not extracted features) with shape `(batch_size, seq_len, n_channels, n_samples)`.

## Online Discrete Classification

For real-time discrete gesture recognition, use the `OnlineDiscreteClassifier`:

```Python
from libemg.emg_predictor import OnlineDiscreteClassifier
from libemg._discrete_models import MyoCrossUserPretrained

# Load pretrained model
model = MyoCrossUserPretrained()

# Create online classifier
classifier = OnlineDiscreteClassifier(
odh=online_data_handler,
model=model,
window_size=model.args['window_size'],
window_increment=model.args['window_increment'],
null_label=model.args['null_label'],
feature_list=model.args['feature_list'], # None for raw data
template_size=model.args['template_size'],
min_template_size=model.args['min_template_size'],
gesture_mapping=model.args['gesture_mapping'],
buffer_size=model.args['buffer_size'],
rejection_threshold=0.5,
debug=True
)

# Start recognition loop
classifier.run()
```

## Creating Custom Discrete Classifiers

Any custom discrete classifier should implement the following methods to work with LibEMG:

- `fit(x, y)`: Train the model where `x` is a list of templates and `y` is the corresponding labels.
- `predict(x)`: Return predicted class labels for a list of templates.
- `predict_proba(x)`: Return predicted class probabilities for a list of templates.

```Python
class CustomDiscreteClassifier:
def __init__(self):
self.classes_ = None

def fit(self, x, y):
# x: list of templates (each template is an array of frames)
# y: labels for each template
self.classes_ = np.unique(y)
# ... training logic

def predict(self, x):
# Return array of predictions
pass

def predict_proba(self, x):
# Return array of shape (n_samples, n_classes)
pass
```
3 changes: 3 additions & 0 deletions docs/source/documentation/prediction/prediction.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ EMG Prediction
.. include:: classification_doc.md
:parser: myst_parser.sphinx_

.. include:: discrete_classification_doc.md
:parser: myst_parser.sphinx_

.. include:: regression_doc.md
:parser: myst_parser.sphinx_

Expand Down
2 changes: 1 addition & 1 deletion docs/source/documentation/prediction/predictors.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

After recording, processing, and extracting features from a window of EMG data, it is passed to a machine learning algorithm for prediction. These control systems have evolved in the prosthetics community for continuously predicting muscular contractions for enabling prosthesis control. Therefore, they are primarily limited to recognizing static contractions (e.g., hand open/close and wrist flexion/extension) as they have no temporal awareness. Currently, this is the form of recognition supported by LibEMG and is an initial step to explore EMG as an interaction opportunity for general-purpose use. This section highlights the machine-learning strategies that are part of `LibEMG`'s pipeline.

There are two types of models supported in `LibEMG`: classifiers and regressors. Classifiers output a discrete motion class for each window, whereas regressors output a continuous prediction along a degree of freedom. For both classifiers and regressors, `LibEMG` supports statistical models as well as deep learning models. Additionally, a number of post-processing methods (i.e., techniques to improve performance after prediction) are supported for all models.
There are three types of models supported in `LibEMG`: classifiers, regressors, and discrete classifiers. Classifiers output a motion class for each window of EMG data, whereas regressors output a continuous prediction along a degree of freedom. Discrete classifiers are designed for recognizing transient, isolated gestures and operate on variable-length templates rather than individual windows. For classifiers and regressors, `LibEMG` supports statistical models as well as deep learning models. Additionally, a number of post-processing methods (i.e., techniques to improve performance after prediction) are supported for all models.

## Statistical Models

Expand Down
1 change: 1 addition & 0 deletions libemg/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
from libemg import gui
from libemg import shared_memory_manager
from libemg import environments
from libemg import _discrete_models
126 changes: 126 additions & 0 deletions libemg/_discrete_models/DTW.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from tslearn.metrics import dtw_path
import numpy as np


class DTWClassifier:
"""Dynamic Time Warping k-Nearest Neighbors classifier.

A classifier that uses Dynamic Time Warping (DTW) distance for template
matching with k-nearest neighbors. Suitable for discrete gesture recognition
where temporal alignment between samples varies.

Parameters
----------
n_neighbors: int, default=1
Number of neighbors to use for k-nearest neighbors voting.

Attributes
----------
templates: list of ndarray
The training templates stored after fitting.
labels: ndarray
The labels corresponding to each template.
classes_: ndarray
The unique class labels known to the classifier.
"""

def __init__(self, n_neighbors=1):
"""Initialize the DTW classifier.

Parameters
----------
n_neighbors: int, default=1
Number of neighbors to use for k-nearest neighbors voting.
"""
self.n_neighbors = n_neighbors
self.templates = None
self.labels = None
self.classes_ = None

def fit(self, features, labels):
"""Fit the DTW classifier by storing training templates.

Parameters
----------
features: list of ndarray
A list of training samples (templates) where each sample is
a 2D array of shape (n_frames, n_features).
labels: array-like
The target labels for each template.
"""
self.templates = features
self.labels = np.array(labels)
self.classes_ = np.unique(labels)

def predict(self, samples):
"""Predict class labels for samples.

Parameters
----------
samples: list of ndarray
A list of samples to classify where each sample is a 2D array
of shape (n_frames, n_features).

Returns
-------
ndarray
Predicted class labels for each sample.
"""
# We can reuse predict_proba logic to get the class with highest probability
probas = self.predict_proba(samples)
return self.classes_[np.argmax(probas, axis=1)]

def predict_proba(self, samples, gamma=None, eps=1e-12):
"""Predict class probabilities using DTW distance-weighted voting.

Computes DTW distances to all templates, selects k-nearest neighbors,
and computes class probabilities using exponentially weighted voting.

Parameters
----------
samples: list of ndarray
A list of samples to classify where each sample is a 2D array
of shape (n_frames, n_features).
gamma: float, default=None
The kernel bandwidth for distance weighting. If None, automatically
computed based on median neighbor distance.
eps: float, default=1e-12
Small constant to prevent division by zero.

Returns
-------
ndarray
Predicted class probabilities of shape (n_samples, n_classes).
"""
if self.templates is None:
raise ValueError("Call fit() before predict_proba().")

X = np.asarray(samples, dtype=object)
out = np.zeros((len(X), len(self.classes_)), dtype=float)

for i, s in enumerate(X):
# DTW distances to templates
dists = np.array([dtw_path(t, s)[1] for t in self.templates], dtype=float)

# kNN
nn_idx = np.argsort(dists)[:self.n_neighbors]
nn_dists = dists[nn_idx]
nn_labels = self.labels[nn_idx]

# choose gamma if not provided (scale to typical distance)
g = gamma
if g is None:
scale = np.median(nn_dists) if len(nn_dists) else 1.0
g = 1.0 / max(scale, eps)

weights = np.exp(-g * nn_dists) # closer -> bigger weight

# accumulate per class
for cls_j, cls in enumerate(self.classes_):
out[i, cls_j] = weights[nn_labels == cls].sum()

# normalize to probabilities
z = out[i].sum()
out[i] = out[i] / max(z, eps)

return out
95 changes: 95 additions & 0 deletions libemg/_discrete_models/MVLDA.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
import numpy as np
from scipy import stats


class MVLDA:
"""Majority Vote Linear Discriminant Analysis classifier.

A classifier that uses Linear Discriminant Analysis (LDA) on individual frames
and aggregates predictions using majority voting. This is designed for discrete
gesture recognition where each sample contains multiple frames.

Attributes
----------
model: LinearDiscriminantAnalysis
The underlying LDA model.
classes_: ndarray
The class labels known to the classifier.
"""

def __init__(self):
"""Initialize the MVLDA classifier."""
self.model = None
self.classes_ = None

def fit(self, x, y):
"""Fit the MVLDA classifier on training data.

Parameters
----------
x: list of ndarray
A list of samples where each sample is a 2D array of shape
(n_frames, n_features). Each sample can have a different number of frames.
y: array-like
The target labels for each sample.
"""
self.model = LinearDiscriminantAnalysis()
# Create a flat array of labels corresponding to every frame in x
labels = np.hstack([[v] * x[i].shape[0] for i, v in enumerate(y)])
self.model.fit(np.vstack(x), labels)
# Store classes for consistent probability mapping
self.classes_ = self.model.classes_

def predict(self, y):
"""Predict class labels using majority voting.

Performs frame-level LDA predictions and returns the majority vote
for each sample.

Parameters
----------
y: list of ndarray
A list of samples where each sample is a 2D array of shape
(n_frames, n_features).

Returns
-------
ndarray
Predicted class labels for each sample.
"""
preds = []
for s in y:
frame_predictions = self.model.predict(s)
# Majority vote on the labels
majority_vote = stats.mode(frame_predictions, keepdims=False)[0]
preds.append(majority_vote)
return np.array(preds)

def predict_proba(self, y):
"""Predict class probabilities using soft voting.

Calculates probabilities by averaging the frame-level probabilities
for each sample (soft voting).

Parameters
----------
y: list of ndarray
A list of samples where each sample is a 2D array of shape
(n_frames, n_features).

Returns
-------
ndarray
Predicted class probabilities of shape (n_samples, n_classes).
"""
probas = []
for s in y:
# Get probabilities for each frame: shape (n_frames, n_classes)
frame_probas = self.model.predict_proba(s)

# Average probabilities across all frames in this sample
sample_proba = np.mean(frame_probas, axis=0)
probas.append(sample_proba)

return np.array(probas)
Loading
Loading