accusleepy 0.9.3__tar.gz → 0.10.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {accusleepy-0.9.3 → accusleepy-0.10.1}/PKG-INFO +5 -8
- {accusleepy-0.9.3 → accusleepy-0.10.1}/README.md +2 -3
- accusleepy-0.10.1/accusleepy/__init__.py +1 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/__main__.py +2 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/bouts.py +2 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/brain_state_set.py +2 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/classification.py +2 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/config.json +2 -1
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/constants.py +4 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/fileio.py +12 -3
- accusleepy-0.10.1/accusleepy/gui/__init__.py +1 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/images/primary_window.png +0 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/main.py +5 -7
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/manual_scoring.py +105 -42
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/mplwidget.py +2 -1
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/primary_window.py +85 -85
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/primary_window.ui +95 -107
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/recording_manager.py +24 -14
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/settings_widget.py +12 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/text/main_guide.md +15 -12
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/text/manual_scoring_guide.md +1 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/models.py +2 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/multitaper.py +3 -60
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/signal_processing.py +2 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/temperature_scaling.py +2 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/validation.py +2 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/pyproject.toml +4 -5
- accusleepy-0.9.3/accusleepy/__init__.py +0 -0
- accusleepy-0.9.3/accusleepy/gui/__init__.py +0 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/dialogs.py +0 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/icons/brightness_down.png +0 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/icons/brightness_up.png +0 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/icons/double_down_arrow.png +0 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/icons/double_up_arrow.png +0 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/icons/down_arrow.png +0 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/icons/home.png +0 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/icons/question.png +0 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/icons/save.png +0 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/icons/up_arrow.png +0 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/icons/zoom_in.png +0 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/icons/zoom_out.png +0 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/images/viewer_window.png +0 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/images/viewer_window_annotated.png +0 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/resources.qrc +0 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/resources_rc.py +0 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/text/dev_guide.md +0 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/viewer_window.py +0 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/viewer_window.ui +0 -0
- {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/services.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: accusleepy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.1
|
|
4
4
|
Summary: Python implementation of AccuSleep
|
|
5
5
|
License: GPL-3.0-only
|
|
6
6
|
Author: Zeke Barger
|
|
@@ -11,14 +11,12 @@ Classifier: Programming Language :: Python :: 3
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
-
Requires-Dist: fastparquet (>=2024.11.0,<2025.0.0)
|
|
15
|
-
Requires-Dist: joblib (>=1.4.2,<2.0.0)
|
|
16
14
|
Requires-Dist: matplotlib (>=3.10.1,<4.0.0)
|
|
17
15
|
Requires-Dist: numpy (>=2.2.4,<3.0.0)
|
|
18
16
|
Requires-Dist: pandas (>=2.2.3,<3.0.0)
|
|
19
17
|
Requires-Dist: pillow (>=11.1.0,<12.0.0)
|
|
20
|
-
Requires-Dist:
|
|
21
|
-
Requires-Dist: pyside6 (>=6.
|
|
18
|
+
Requires-Dist: pyarrow (>=23.0.0,<24.0.0)
|
|
19
|
+
Requires-Dist: pyside6 (>=6.10.1,<7.0.0)
|
|
22
20
|
Requires-Dist: scipy (>=1.15.2,<2.0.0)
|
|
23
21
|
Requires-Dist: torch (>=2.8.0,<3.0.0)
|
|
24
22
|
Requires-Dist: torchvision (>=0.23.0,<1.0.0)
|
|
@@ -79,9 +77,8 @@ please consult the [developer guide](accusleepy/gui/text/dev_guide.md).
|
|
|
79
77
|
|
|
80
78
|
## Changelog
|
|
81
79
|
|
|
82
|
-
- 0.
|
|
83
|
-
- 0.
|
|
84
|
-
- 0.7.1-0.7.3: Bugfixes, code cleanup
|
|
80
|
+
- 0.10.0-0.10.1: Improved zoom behavior, updated dependencies
|
|
81
|
+
- 0.7.1-0.9.3: Bugfixes, code cleanup, additional config settings
|
|
85
82
|
- 0.7.0: More settings can be configured in the UI
|
|
86
83
|
- 0.6.0: Confidence scores can now be displayed and saved. Retraining your models is recommended
|
|
87
84
|
since the new calibration feature will make the confidence scores more accurate.
|
|
@@ -52,9 +52,8 @@ please consult the [developer guide](accusleepy/gui/text/dev_guide.md).
|
|
|
52
52
|
|
|
53
53
|
## Changelog
|
|
54
54
|
|
|
55
|
-
- 0.
|
|
56
|
-
- 0.
|
|
57
|
-
- 0.7.1-0.7.3: Bugfixes, code cleanup
|
|
55
|
+
- 0.10.0-0.10.1: Improved zoom behavior, updated dependencies
|
|
56
|
+
- 0.7.1-0.9.3: Bugfixes, code cleanup, additional config settings
|
|
58
57
|
- 0.7.0: More settings can be configured in the UI
|
|
59
58
|
- 0.6.0: Confidence scores can now be displayed and saved. Retraining your models is recommended
|
|
60
59
|
since the new calibration feature will make the confidence scores more accurate.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""AccuSleePy: Python implementation of AccuSleep for automated sleep scoring."""
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Application-wide constants and default settings."""
|
|
2
|
+
|
|
1
3
|
import numpy as np
|
|
2
4
|
|
|
3
5
|
# probably don't change these unless you really need to
|
|
@@ -72,6 +74,7 @@ EMG_FILTER_KEY = "emg_filter"
|
|
|
72
74
|
HYPERPARAMETERS_KEY = "hyperparameters"
|
|
73
75
|
EPOCHS_TO_SHOW_KEY = "epochs_to_show"
|
|
74
76
|
AUTOSCROLL_KEY = "autoscroll_state"
|
|
77
|
+
DELETE_TRAINING_IMAGES_KEY = "delete_training_images"
|
|
75
78
|
|
|
76
79
|
# default values
|
|
77
80
|
# default UI settings
|
|
@@ -87,6 +90,7 @@ DEFAULT_BATCH_SIZE = 64
|
|
|
87
90
|
DEFAULT_LEARNING_RATE = 1e-3
|
|
88
91
|
DEFAULT_MOMENTUM = 0.9
|
|
89
92
|
DEFAULT_TRAINING_EPOCHS = 6
|
|
93
|
+
DEFAULT_DELETE_TRAINING_IMAGES_STATE = True
|
|
90
94
|
# default manual scoring settings
|
|
91
95
|
DEFAULT_EPOCHS_TO_SHOW = 5
|
|
92
96
|
DEFAULT_AUTOSCROLL_STATE = False
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""File I/O for recordings, labels, calibration data, and configuration."""
|
|
2
|
+
|
|
1
3
|
import json
|
|
2
4
|
import os
|
|
3
5
|
from dataclasses import dataclass
|
|
@@ -5,7 +7,6 @@ from importlib.metadata import version, PackageNotFoundError
|
|
|
5
7
|
|
|
6
8
|
import numpy as np
|
|
7
9
|
import pandas as pd
|
|
8
|
-
from PySide6.QtWidgets import QListWidgetItem
|
|
9
10
|
|
|
10
11
|
from accusleepy.brain_state_set import BRAIN_STATES_KEY, BrainState, BrainStateSet
|
|
11
12
|
import accusleepy.constants as c
|
|
@@ -43,6 +44,7 @@ class AccuSleePyConfig:
|
|
|
43
44
|
hyperparameters: Hyperparameters
|
|
44
45
|
epochs_to_show: int
|
|
45
46
|
autoscroll_state: bool
|
|
47
|
+
delete_training_images: bool
|
|
46
48
|
|
|
47
49
|
|
|
48
50
|
@dataclass
|
|
@@ -54,7 +56,6 @@ class Recording:
|
|
|
54
56
|
label_file: str = "" # path to label file
|
|
55
57
|
calibration_file: str = "" # path to calibration file
|
|
56
58
|
sampling_rate: int | float = 0.0 # sampling rate, in Hz
|
|
57
|
-
widget: QListWidgetItem = None # list item widget shown in the GUI
|
|
58
59
|
|
|
59
60
|
|
|
60
61
|
def load_calibration_file(filename: str) -> tuple[np.ndarray, np.ndarray]:
|
|
@@ -139,7 +140,8 @@ def load_config() -> AccuSleePyConfig:
|
|
|
139
140
|
EMG filter parameters,
|
|
140
141
|
model training hyperparameters,
|
|
141
142
|
default epochs to show for manual scoring,
|
|
142
|
-
default autoscroll state for manual scoring
|
|
143
|
+
default autoscroll state for manual scoring,
|
|
144
|
+
setting to delete training images automatically
|
|
143
145
|
"""
|
|
144
146
|
with open(
|
|
145
147
|
os.path.join(os.path.dirname(os.path.abspath(__file__)), c.CONFIG_FILE), "r"
|
|
@@ -183,6 +185,9 @@ def load_config() -> AccuSleePyConfig:
|
|
|
183
185
|
),
|
|
184
186
|
epochs_to_show=data.get(c.EPOCHS_TO_SHOW_KEY, c.DEFAULT_EPOCHS_TO_SHOW),
|
|
185
187
|
autoscroll_state=data.get(c.AUTOSCROLL_KEY, c.DEFAULT_AUTOSCROLL_STATE),
|
|
188
|
+
delete_training_images=data.get(
|
|
189
|
+
c.DELETE_TRAINING_IMAGES_KEY, c.DEFAULT_DELETE_TRAINING_IMAGES_STATE
|
|
190
|
+
),
|
|
186
191
|
)
|
|
187
192
|
|
|
188
193
|
|
|
@@ -196,6 +201,7 @@ def save_config(
|
|
|
196
201
|
hyperparameters: Hyperparameters,
|
|
197
202
|
epochs_to_show: int,
|
|
198
203
|
autoscroll_state: bool,
|
|
204
|
+
delete_training_images: bool,
|
|
199
205
|
) -> None:
|
|
200
206
|
"""Save configuration of brain state options to json file
|
|
201
207
|
|
|
@@ -210,6 +216,8 @@ def save_config(
|
|
|
210
216
|
:param hyperparameters: model training hyperparameters
|
|
211
217
|
:param epochs_to_show: default epochs to show for manual scoring,
|
|
212
218
|
:param autoscroll_state: default autoscroll state for manual scoring
|
|
219
|
+
:param delete_training_images: whether to automatically delete images
|
|
220
|
+
created for model training once training is complete
|
|
213
221
|
"""
|
|
214
222
|
output_dict = brain_state_set.to_output_dict()
|
|
215
223
|
output_dict.update({c.DEFAULT_EPOCH_LENGTH_KEY: default_epoch_length})
|
|
@@ -220,6 +228,7 @@ def save_config(
|
|
|
220
228
|
output_dict.update({c.HYPERPARAMETERS_KEY: hyperparameters.__dict__})
|
|
221
229
|
output_dict.update({c.EPOCHS_TO_SHOW_KEY: epochs_to_show})
|
|
222
230
|
output_dict.update({c.AUTOSCROLL_KEY: autoscroll_state})
|
|
231
|
+
output_dict.update({c.DELETE_TRAINING_IMAGES_KEY: delete_training_images})
|
|
223
232
|
with open(
|
|
224
233
|
os.path.join(os.path.dirname(os.path.abspath(__file__)), c.CONFIG_FILE), "w"
|
|
225
234
|
) as f:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Graphical user interface components for AccuSleePy."""
|
|
Binary file
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"""AccuSleePy main window.
|
|
2
|
+
|
|
3
|
+
Icon source: Arkinasi, https://www.flaticon.com/authors/arkinasi
|
|
4
|
+
"""
|
|
3
5
|
|
|
4
6
|
import logging
|
|
5
7
|
import os
|
|
@@ -76,7 +78,6 @@ class TrainingSettings:
|
|
|
76
78
|
"""Settings for training a new model"""
|
|
77
79
|
|
|
78
80
|
epochs_per_img: int = 9
|
|
79
|
-
delete_images: bool = True
|
|
80
81
|
model_type: str = DEFAULT_MODEL_TYPE
|
|
81
82
|
calibrate: bool = True
|
|
82
83
|
|
|
@@ -176,9 +177,6 @@ class AccuSleepWindow(QMainWindow):
|
|
|
176
177
|
self.ui.image_number_input.valueChanged.connect(
|
|
177
178
|
lambda v: setattr(self.training, "epochs_per_img", v)
|
|
178
179
|
)
|
|
179
|
-
self.ui.delete_image_box.stateChanged.connect(
|
|
180
|
-
lambda v: setattr(self.training, "delete_images", bool(v))
|
|
181
|
-
)
|
|
182
180
|
self.ui.calibrate_checkbox.stateChanged.connect(
|
|
183
181
|
self.update_training_calibration
|
|
184
182
|
)
|
|
@@ -304,7 +302,7 @@ class AccuSleepWindow(QMainWindow):
|
|
|
304
302
|
emg_filter=self.config.emg_filter,
|
|
305
303
|
hyperparameters=self.config.hyperparameters,
|
|
306
304
|
model_filename=model_filename,
|
|
307
|
-
delete_images=self.
|
|
305
|
+
delete_images=self.config.delete_training_images,
|
|
308
306
|
)
|
|
309
307
|
|
|
310
308
|
# Display results
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
# Icon sources:
|
|
3
|
-
# Arkinasi, https://www.flaticon.com/authors/arkinasi
|
|
4
|
-
# kendis lasman, https://www.flaticon.com/packs/ui-79
|
|
1
|
+
"""AccuSleePy manual scoring GUI.
|
|
5
2
|
|
|
3
|
+
Icon sources:
|
|
4
|
+
Arkinasi, https://www.flaticon.com/authors/arkinasi
|
|
5
|
+
kendis lasman, https://www.flaticon.com/packs/ui-79
|
|
6
|
+
"""
|
|
6
7
|
|
|
7
8
|
import copy
|
|
8
9
|
import os
|
|
10
|
+
import time
|
|
9
11
|
from dataclasses import dataclass
|
|
10
12
|
from functools import partial
|
|
11
13
|
from types import SimpleNamespace
|
|
@@ -82,9 +84,10 @@ UNDO_LIMIT = 1000
|
|
|
82
84
|
# brightness scaling factors for the spectrogram
|
|
83
85
|
BRIGHTER_SCALE_FACTOR = 0.96
|
|
84
86
|
DIMMER_SCALE_FACTOR = 1.07
|
|
85
|
-
# zoom factor for upper plots
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
# zoom factor for upper plots - larger values = bigger changes
|
|
88
|
+
ZOOM_FACTOR = 0.1
|
|
89
|
+
# rate limit for zoom events triggered by scrolling
|
|
90
|
+
MAX_SCROLL_EVENTS_PER_SEC = 24
|
|
88
91
|
|
|
89
92
|
|
|
90
93
|
@dataclass
|
|
@@ -352,8 +355,10 @@ class ManualScoringWindow(QDialog):
|
|
|
352
355
|
)
|
|
353
356
|
keypress_redo.activated.connect(self.redo)
|
|
354
357
|
|
|
355
|
-
# user input:
|
|
358
|
+
# user input: mouse events
|
|
356
359
|
self.ui.upperfigure.canvas.mpl_connect("button_press_event", self.click_to_jump)
|
|
360
|
+
self.ui.upperfigure.canvas.mpl_connect("scroll_event", self.scroll_zoom)
|
|
361
|
+
self.now_zooming = False # impose timeout on zoom events
|
|
357
362
|
|
|
358
363
|
# user input: buttons
|
|
359
364
|
self.ui.savebutton.clicked.connect(self.save)
|
|
@@ -483,7 +488,7 @@ class ManualScoringWindow(QDialog):
|
|
|
483
488
|
|
|
484
489
|
def closeEvent(self, event: QCloseEvent) -> None:
|
|
485
490
|
"""Check if there are unsaved changes before closing"""
|
|
486
|
-
if not
|
|
491
|
+
if not np.array_equal(self.labels, self.last_saved_labels):
|
|
487
492
|
result = QMessageBox.question(
|
|
488
493
|
self,
|
|
489
494
|
"Unsaved changes",
|
|
@@ -668,6 +673,8 @@ class ManualScoringWindow(QDialog):
|
|
|
668
673
|
self.label_display_options,
|
|
669
674
|
)
|
|
670
675
|
self.update_figures()
|
|
676
|
+
# upper plot x limits might need to change
|
|
677
|
+
self.zoom_x(direction=None)
|
|
671
678
|
|
|
672
679
|
def update_signal_offset(self, signal: str, direction: str) -> None:
|
|
673
680
|
"""Shift EEG or EMG up or down
|
|
@@ -713,39 +720,19 @@ class ManualScoringWindow(QDialog):
|
|
|
713
720
|
(self.upper_left_epoch, self.upper_right_epoch + 1)
|
|
714
721
|
)
|
|
715
722
|
|
|
716
|
-
def zoom_x(self, direction: str) -> None:
|
|
723
|
+
def zoom_x(self, direction: str | None) -> None:
|
|
717
724
|
"""Change upper figure x-axis zoom level
|
|
718
725
|
|
|
719
|
-
:param direction: in, out, or
|
|
726
|
+
:param direction: in, out, reset, or None
|
|
720
727
|
"""
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
self.upper_left_epoch
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
self.upper_right_epoch = min(
|
|
731
|
-
[
|
|
732
|
-
self.upper_right_epoch,
|
|
733
|
-
round(self.epoch + ZOOM_IN_FACTOR * epochs_shown),
|
|
734
|
-
]
|
|
735
|
-
)
|
|
736
|
-
|
|
737
|
-
elif direction == ZOOM_OUT:
|
|
738
|
-
self.upper_left_epoch = max(
|
|
739
|
-
[0, round(self.epoch - ZOOM_OUT_FACTOR * epochs_shown)]
|
|
740
|
-
)
|
|
741
|
-
|
|
742
|
-
self.upper_right_epoch = min(
|
|
743
|
-
[self.n_epochs - 1, round(self.epoch + ZOOM_OUT_FACTOR * epochs_shown)]
|
|
744
|
-
)
|
|
745
|
-
|
|
746
|
-
else: # reset
|
|
747
|
-
self.upper_left_epoch = 0
|
|
748
|
-
self.upper_right_epoch = self.n_epochs - 1
|
|
728
|
+
self.upper_left_epoch, self.upper_right_epoch = find_new_x_limits(
|
|
729
|
+
direction=direction,
|
|
730
|
+
left_epoch=self.upper_left_epoch,
|
|
731
|
+
right_epoch=self.upper_right_epoch,
|
|
732
|
+
min_n_shown=self.epochs_to_show,
|
|
733
|
+
total_epochs=self.n_epochs,
|
|
734
|
+
selected_epoch=self.epoch,
|
|
735
|
+
)
|
|
749
736
|
self.adjust_upper_figure_x_limits()
|
|
750
737
|
self.ui.upperfigure.canvas.draw()
|
|
751
738
|
|
|
@@ -806,8 +793,11 @@ class ManualScoringWindow(QDialog):
|
|
|
806
793
|
# update upper plot if needed
|
|
807
794
|
upper_epochs_shown = self.upper_right_epoch - self.upper_left_epoch + 1
|
|
808
795
|
if (
|
|
809
|
-
|
|
810
|
-
|
|
796
|
+
(
|
|
797
|
+
self.epoch
|
|
798
|
+
> self.upper_left_epoch + (1 - SCROLL_BOUNDARY) * upper_epochs_shown
|
|
799
|
+
or self.epoch + (self.epochs_to_show - 1) / 2 > self.upper_right_epoch
|
|
800
|
+
)
|
|
811
801
|
and self.upper_right_epoch < (self.n_epochs - 1)
|
|
812
802
|
and direction == DIRECTION_RIGHT
|
|
813
803
|
):
|
|
@@ -815,7 +805,11 @@ class ManualScoringWindow(QDialog):
|
|
|
815
805
|
self.upper_right_epoch += 1
|
|
816
806
|
self.adjust_upper_figure_x_limits()
|
|
817
807
|
elif (
|
|
818
|
-
|
|
808
|
+
(
|
|
809
|
+
self.epoch
|
|
810
|
+
< self.upper_left_epoch + SCROLL_BOUNDARY * upper_epochs_shown
|
|
811
|
+
or self.epoch - (self.epochs_to_show - 1) / 2 < self.upper_left_epoch
|
|
812
|
+
)
|
|
819
813
|
and self.upper_left_epoch > 0
|
|
820
814
|
and direction == DIRECTION_LEFT
|
|
821
815
|
):
|
|
@@ -984,6 +978,23 @@ class ManualScoringWindow(QDialog):
|
|
|
984
978
|
|
|
985
979
|
self.update_figures()
|
|
986
980
|
|
|
981
|
+
def scroll_zoom(self, event) -> None:
|
|
982
|
+
"""Zoom on mouse scroll events"""
|
|
983
|
+
if self.now_zooming:
|
|
984
|
+
return
|
|
985
|
+
|
|
986
|
+
self.now_zooming = True
|
|
987
|
+
start_time = time.time()
|
|
988
|
+
if event.button == "up":
|
|
989
|
+
self.zoom_x(direction=ZOOM_IN)
|
|
990
|
+
else:
|
|
991
|
+
self.zoom_x(direction=ZOOM_OUT)
|
|
992
|
+
end_time = time.time()
|
|
993
|
+
time_elapsed = end_time - start_time
|
|
994
|
+
if time_elapsed < 1 / MAX_SCROLL_EVENTS_PER_SEC:
|
|
995
|
+
time.sleep(1 / MAX_SCROLL_EVENTS_PER_SEC - time_elapsed)
|
|
996
|
+
self.now_zooming = False
|
|
997
|
+
|
|
987
998
|
|
|
988
999
|
def convert_labels(labels: np.array, style: str) -> np.array:
|
|
989
1000
|
"""Convert labels between "display" and "digit" formats
|
|
@@ -1095,3 +1106,55 @@ def transform_eeg_emg(eeg: np.array, emg: np.array) -> (np.array, np.array):
|
|
|
1095
1106
|
eeg = eeg / np.percentile(eeg, 95) / 2.2
|
|
1096
1107
|
emg = emg / np.percentile(emg, 95) / 2.2
|
|
1097
1108
|
return eeg, emg
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
def find_new_x_limits(
|
|
1112
|
+
direction: str | None,
|
|
1113
|
+
left_epoch: int,
|
|
1114
|
+
right_epoch: int,
|
|
1115
|
+
total_epochs: int,
|
|
1116
|
+
min_n_shown: int,
|
|
1117
|
+
selected_epoch: int,
|
|
1118
|
+
) -> (int, int):
|
|
1119
|
+
"""Calculate new plot x limits to allow zooming
|
|
1120
|
+
|
|
1121
|
+
:param direction: in, out, reset, or None
|
|
1122
|
+
:param left_epoch: index of current leftmost epoch
|
|
1123
|
+
:param right_epoch: index of current rightmost epoch
|
|
1124
|
+
:param total_epochs: total number of epochs in the recording
|
|
1125
|
+
:param min_n_shown: minimum number of epochs to display
|
|
1126
|
+
:param selected_epoch: currently selected epoch
|
|
1127
|
+
"""
|
|
1128
|
+
# number of epochs currently displayed in the upper plots
|
|
1129
|
+
current_n_shown = right_epoch - left_epoch + 1
|
|
1130
|
+
if direction == ZOOM_IN:
|
|
1131
|
+
# can't display fewer than the number of epochs in the lower plot
|
|
1132
|
+
new_n_shown = max([min_n_shown, round(current_n_shown * (1 - ZOOM_FACTOR))])
|
|
1133
|
+
elif direction == ZOOM_OUT:
|
|
1134
|
+
# can't display more than the total number of epochs
|
|
1135
|
+
new_n_shown = min([total_epochs, round(current_n_shown / (1 - ZOOM_FACTOR))])
|
|
1136
|
+
elif direction == ZOOM_RESET:
|
|
1137
|
+
left_epoch = 0
|
|
1138
|
+
right_epoch = total_epochs - 1
|
|
1139
|
+
return left_epoch, right_epoch
|
|
1140
|
+
else: # just recalculating if min_n_shown has changed
|
|
1141
|
+
new_n_shown = int(
|
|
1142
|
+
np.clip(current_n_shown, a_min=min_n_shown, a_max=total_epochs)
|
|
1143
|
+
)
|
|
1144
|
+
|
|
1145
|
+
# count epochs to show on either side of the selected epoch
|
|
1146
|
+
epochs_on_left_side = int(np.ceil((new_n_shown - 1) / 2))
|
|
1147
|
+
epochs_on_right_side = new_n_shown - epochs_on_left_side - 1
|
|
1148
|
+
if selected_epoch - epochs_on_left_side < 0:
|
|
1149
|
+
# can't go further left than 0
|
|
1150
|
+
left_epoch = 0
|
|
1151
|
+
right_epoch = new_n_shown - 1
|
|
1152
|
+
elif selected_epoch + epochs_on_right_side >= total_epochs:
|
|
1153
|
+
# can't go further right than the total number of epochs
|
|
1154
|
+
left_epoch = total_epochs - new_n_shown
|
|
1155
|
+
right_epoch = total_epochs - 1
|
|
1156
|
+
else:
|
|
1157
|
+
left_epoch = selected_epoch - epochs_on_left_side
|
|
1158
|
+
right_epoch = selected_epoch + epochs_on_right_side
|
|
1159
|
+
|
|
1160
|
+
return left_epoch, right_epoch
|