Note
Go to the end to download the full example code.
Part 2: Recording Interface#
The recording interface is the second component of a visual interface in MyoGestic.
It is responsible for managing the ground-truth data collection. Since the ground-truth data depends on the visual interface, the recording interface must define the data collection process.
Step 1: Create the setup UI#
You will need an UI that will allow the user to start the recording, visualize the progress, and save the data.
Note
To start copy the UI files in myogestic > gui > widgets > visual_interfaces > ui
and adapt them with the functionality you need.
You need to modify them with QT-Designer and convert them using UIC to a python file.
Step 2: Understand what is needed in the recording interface#
Note
A recording interface is a class that inherits from
|
Base class for the recording interface of a visual interface. |
Please read the documentation of the class and make a mental note of what you have to provide (e.g. signals, methods, attributes) and what you have to implement (e.g. start_recording, stop_recording, accept_recording).
Step 3: Implement a recording interface (Example Virtual Hand Interface)#
This example focuses on implementing and adding the recording interface for the Virtual Hand Interface using the VirtualHandInterface_RecordingInterface class from recording_interface.py. We explain how it is constructed and registered into MyoGestic via CONFIG_REGISTRY in config.py.
Steps:
Class Initialization and UI Setup
Starting & Stopping Recordings
Managing Recording Sessions
Resetting the Interface
Step 3.1: Class Initialization and UI Setup#
This step initializes the recording interface’s buffers and task-related attributes. It also configures the UI elements required for this interface.
1import time
2
3import numpy as np
4from PySide6.QtCore import SignalInstance
5from PySide6.QtGui import QCloseEvent
6
7from myogestic.gui.widgets.logger import LoggerLevel
8from myogestic.gui.widgets.templates.visual_interface import RecordingInterfaceTemplate
9from myogestic.gui.widgets.visual_interfaces.virtual_hand_interface.ui import (
10 Ui_RecordingVirtualHandInterface,
11)
12from myogestic.utils.constants import RECORDING_DIR_PATH
13
14KINEMATICS_SAMPLING_FREQUENCY = 60
15
16
17class VirtualHandInterface_RecordingInterface(RecordingInterfaceTemplate):
18 """
19 Class for the recording interface of the Virtual Hand Interface.
20
21 This class is responsible for handling the recording of EMG and kinematics data.
22
23 Parameters
24 ----------
25 main_window : MainWindow
26 The main window of the application.
27 name : str
28 The name of the interface, by default "VirtualHandInterface".
29
30 .. important:: This name is used to identify the interface in the main window. It should be unique.
31 incoming_message_signal : SignalInstance
32 The signal instance used to receive incoming messages from the device.
33 """
34
35 ground_truth__task_map: dict[str, int] = {
36 "rest": 0,
37 "index": 1,
38 "thumb": 2,
39 "middle": 3,
40 "ring": 4,
41 "pinky": 5,
42 "power grasp": 6,
43 "pinch": 7,
44 "tripod pinch": 8,
45 "pointing": 9,
46 }
47 ground_truth__nr_of_recording_values: int = 9
48
49 def __init__(
50 self,
51 main_window,
52 name: str = "VirtualHandInterface",
53 incoming_message_signal: SignalInstance = None,
54 ) -> None:
55 super().__init__(
56 main_window,
57 name,
58 ui=Ui_RecordingVirtualHandInterface(),
59 incoming_message_signal=incoming_message_signal,
60 ground_truth__nr_of_recording_values=self.ground_truth__nr_of_recording_values,
61 ground_truth__task_map=self.ground_truth__task_map
62 )
63
64 RECORDING_DIR_PATH.mkdir(parents=True, exist_ok=True)
65
66 self._current_task: str = ""
67 self._kinematics__buffer = []
68
69 self._recording_protocol = self._main_window.protocols[0]
70
71 self._has_finished_kinematics: bool = False
76 def initialize_ui_logic(self) -> None:
77 """Initializes the logic for the UI elements."""
78 ui: Ui_RecordingVirtualHandInterface = self.ui
79
80 self._main_window.ui.recordVerticalLayout.addWidget(ui.recordRecordingGroupBox)
81 self._main_window.ui.recordVerticalLayout.addWidget(
82 ui.recordReviewRecordingStackedWidget
83 )
84
85 self.record_group_box = ui.recordRecordingGroupBox
86 self.record_task_combo_box = ui.recordTaskComboBox
87 self.record_duration_spin_box = ui.recordDurationSpinBox
88 self.record_toggle_push_button = ui.recordRecordPushButton
89
90 self.review_recording_stacked_widget = ui.recordReviewRecordingStackedWidget
91 self.review_recording_task_label = ui.reviewRecordingTaskLabel
92 self.review_recording_label_line_edit = ui.reviewRecordingLabelLineEdit
93
94 self.review_recording_accept_push_button = ui.reviewRecordingAcceptPushButton
95 self.review_recording_reject_push_button = ui.reviewRecordingRejectPushButton
96
97 self.use_kinematics_check_box = ui.recordUseKinematicsCheckBox
98
99 self.record_toggle_push_button.toggled.connect(self.start_recording)
100 self.review_recording_accept_push_button.clicked.connect(self.accept_recording)
101 self.review_recording_reject_push_button.clicked.connect(self.reject_recording)
102
103 self.record_ground_truth_progress_bar.setValue(0)
104 self.review_recording_stacked_widget.setCurrentIndex(0)
Step 3.2: Starting & Stopping Recordings#
This step manages the logic for starting or stopping recording sessions, which includes preparing recording parameters and updating the UI.
132 def start_recording_preparation(self) -> bool:
133 """Prepares the recording process by checking if the device is streaming."""
134 if (
135 not self._main_window.device__widget._get_current_widget()._device._is_streaming
136 ):
137 self._main_window.logger.print(
138 "Biosignal device not streaming!", level=LoggerLevel.ERROR
139 )
140 return False
141
142 self.kinematics_recording_time = int(
143 self.record_duration_spin_box.value() * KINEMATICS_SAMPLING_FREQUENCY
144 )
145 self._kinematics__buffer = []
146 return True
106 def start_recording(self, checked: bool) -> None:
107 """Starts the recording process."""
108 if checked:
109 if not self.start_recording_preparation():
110 self.record_toggle_push_button.setChecked(False)
111 return
112
113 if not self._recording_protocol.start_recording_preparation(
114 self.record_duration_spin_box.value()
115 ):
116 self.record_toggle_push_button.setChecked(False)
117 return
118
119 self._start_time = time.time()
120
121 self.record_toggle_push_button.setText("Recording...")
122 self.record_group_box.setEnabled(False)
123 self._current_task = self.record_task_combo_box.currentText()
124
125 if self.use_kinematics_check_box.isChecked():
126 self.incoming_message_signal.connect(self.update_ground_truth_buffer)
127
128 self._has_finished_kinematics = (
129 not self.use_kinematics_check_box.isChecked()
130 )
Step 3.3: Managing Recording Sessions#
This step manages the recording session, which includes updating the UI and recording data.
148 def update_ground_truth_buffer(self, data: np.ndarray) -> None:
149 """Updates the buffer with the incoming kinematics data."""
150 if not self.use_kinematics_check_box.isChecked():
151 return
152
153 self._kinematics__buffer.append((time.time(), data))
154 current_samples = len(self._kinematics__buffer)
155 self._set_progress_bar(
156 self.record_ground_truth_progress_bar,
157 current_samples,
158 self.kinematics_recording_time,
159 )
160
161 if current_samples >= self.kinematics_recording_time:
162 self._main_window.logger.print(
163 f"Kinematics recording finished at: {round(time.time() - self._start_time)} seconds"
164 )
165 self._has_finished_kinematics = True
166 self.incoming_message_signal.disconnect(self.update_ground_truth_buffer)
167 self.check_recording_completion()
169 def check_recording_completion(self) -> None:
170 """Checks if the recording process is complete and finishes it if so."""
171 if (
172 self._recording_protocol.is_biosignal_recording_complete
173 and self._has_finished_kinematics
174 ):
175 self.finish_recording()
177 def finish_recording(self) -> None:
178 """Finishes the recording process and switches to the review recording interface."""
179 self.review_recording_stacked_widget.setCurrentIndex(1)
180 self.record_toggle_push_button.setText("Finished Recording")
181 self.review_recording_task_label.setText(self._current_task.capitalize())
183 def accept_recording(self) -> None:
184 """
185 Accepts the current recording and saves the data to a pickle file.
186
187 The saved data is a dictionary containing:
188
189 - emg: A 2D NumPy array of EMG signals with time samples as rows and channels as columns.
190 - kinematics: A 2D NumPy array of kinematics data (empty if not used).
191 - timings_emg: A 1D NumPy array of timestamps for EMG samples.
192 - timings_kinematics: A 1D NumPy array of timestamps for kinematics samples (empty if not used).
193 - label: The user-provided label for the recording.
194 - task: The task being recorded.
195 - device: The name of the device used for recording.
196 - bad_channels: A list of channels marked as "bad."
197 - _sampling_frequency: The EMG sampling frequency.
198 - kinematics_sampling_frequency: The kinematics sampling frequency.
199 - recording_time: The recording duration in seconds.
200 - use_kinematics: Boolean indicating whether kinematics data was recorded.
201 """
202 label = self.review_recording_label_line_edit.text() or "default"
203 (
204 biosignal_data,
205 biosignal_timings,
206 ) = self._recording_protocol.retrieve_recorded_data()
207
208 self.save_recording(
209 biosignal=biosignal_data,
210 biosignal_timings=biosignal_timings,
211 ground_truth=(
212 np.vstack([data for _, data in self._kinematics__buffer]).T
213 if self.use_kinematics_check_box.isChecked()
214 else np.array([])
215 ),
216 ground_truth_timings=(
217 np.array([time_stamp for time_stamp, _ in self._kinematics__buffer])
218 if self.use_kinematics_check_box.isChecked()
219 else np.array([])
220 ),
221 recording_label=label,
222 task=self._current_task,
223 ground_truth_sampling_frequency=KINEMATICS_SAMPLING_FREQUENCY,
224 use_as_classification=not self.use_kinematics_check_box.isChecked(),
225 record_duration=self.record_duration_spin_box.value(),
226 )
227
228 self.reset_ui()
229 self._main_window.logger.print(
230 f"Recording of task {self._current_task.lower()} with label {label} accepted!"
231 )
233 def reject_recording(self) -> None:
234 """Rejects the current recording and resets the recording interface."""
235 self.reset_ui()
236 self._main_window.logger.print("Recording rejected.")
Step 3.4: Resetting the Interface#
This step resets the recording interface to its initial state.
238 def reset_ui(self) -> None:
239 """Resets the recording interface UI elements."""
240 self.review_recording_stacked_widget.setCurrentIndex(0)
241 self.record_toggle_push_button.setText("Start Recording")
242 self.record_toggle_push_button.setChecked(False)
243 self.record_group_box.setEnabled(True)
244
245 self._recording_protocol._reset_recording_ui()
246
247 self.record_ground_truth_progress_bar.setValue(0)
248 self._kinematics__buffer.clear()
250 def close_event(self, _: QCloseEvent) -> None:
251 """Closes the recording interface."""
252 self.record_toggle_push_button.setChecked(False)
253 self.reset_ui()
254 self._recording_protocol.close_event(_)
255 self._main_window.logger.print("Recording interface closed.")
257 def enable(self):
258 """Enable the UI elements."""
259 self.ui.recordRecordingGroupBox.setEnabled(True)
260 self.ui.recordReviewRecordingStackedWidget.setEnabled(True)
262 def disable(self):
263 """Disable the UI elements."""
264 self.ui.recordRecordingGroupBox.setEnabled(False)
265 self.ui.recordReviewRecordingStackedWidget.setEnabled(False)
Total running time of the script: (0 minutes 0.000 seconds)
Estimated memory usage: 623 MB