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 def __init__(
36 self,
37 main_window,
38 name: str = "VirtualHandInterface",
39 incoming_message_signal: SignalInstance = None,
40 ) -> None:
41 super().__init__(
42 main_window,
43 name,
44 ui=Ui_RecordingVirtualHandInterface(),
45 incoming_message_signal=incoming_message_signal,
46 ground_truth__nr_of_recording_values=9,
47 ground_truth__task_map={
48 "rest": 0,
49 "index": 1,
50 "thumb": 2,
51 "middle": 3,
52 "ring": 4,
53 "pinky": 5,
54 "fist": 6,
55 "pinch": 7,
56 "3fpinch": 8,
57 "pointing": 9,
58 }
59 )
60
61 RECORDING_DIR_PATH.mkdir(parents=True, exist_ok=True)
62
63 self._current_task: str = ""
64 self._kinematics__buffer = []
65
66 self._recording_protocol = self._main_window.protocols[0]
67
68 self._has_finished_kinematics: bool = False
69 self._start_time: float = 0
70
71 self.initialize_ui_logic()
73 def initialize_ui_logic(self) -> None:
74 """Initializes the logic for the UI elements."""
75 ui: Ui_RecordingVirtualHandInterface = self.ui
76
77 self._main_window.ui.recordVerticalLayout.addWidget(ui.recordRecordingGroupBox)
78 self._main_window.ui.recordVerticalLayout.addWidget(
79 ui.recordReviewRecordingStackedWidget
80 )
81
82 self.record_group_box = ui.recordRecordingGroupBox
83 self.record_task_combo_box = ui.recordTaskComboBox
84 self.record_duration_spin_box = ui.recordDurationSpinBox
85 self.record_toggle_push_button = ui.recordRecordPushButton
86
87 self.review_recording_stacked_widget = ui.recordReviewRecordingStackedWidget
88 self.review_recording_task_label = ui.reviewRecordingTaskLabel
89 self.review_recording_label_line_edit = ui.reviewRecordingLabelLineEdit
90
91 self.review_recording_accept_push_button = ui.reviewRecordingAcceptPushButton
92 self.review_recording_reject_push_button = ui.reviewRecordingRejectPushButton
93
94 self.use_kinematics_check_box = ui.recordUseKinematicsCheckBox
95
96 self.record_toggle_push_button.toggled.connect(self.start_recording)
97 self.review_recording_accept_push_button.clicked.connect(self.accept_recording)
98 self.review_recording_reject_push_button.clicked.connect(self.reject_recording)
99
100 self.record_ground_truth_progress_bar.setValue(0)
101 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.
129 def start_recording_preparation(self) -> bool:
130 """Prepares the recording process by checking if the device is streaming."""
131 if (
132 not self._main_window.device__widget._get_current_widget()._device._is_streaming
133 ):
134 self._main_window.logger.print(
135 "Biosignal device not streaming!", level=LoggerLevel.ERROR
136 )
137 return False
138
139 self.kinematics_recording_time = int(
140 self.record_duration_spin_box.value() * KINEMATICS_SAMPLING_FREQUENCY
141 )
142 self._kinematics__buffer = []
143 return True
103 def start_recording(self, checked: bool) -> None:
104 """Starts the recording process."""
105 if checked:
106 if not self.start_recording_preparation():
107 self.record_toggle_push_button.setChecked(False)
108 return
109
110 if not self._recording_protocol.start_recording_preparation(
111 self.record_duration_spin_box.value()
112 ):
113 self.record_toggle_push_button.setChecked(False)
114 return
115
116 self._start_time = time.time()
117
118 self.record_toggle_push_button.setText("Recording...")
119 self.record_group_box.setEnabled(False)
120 self._current_task = self.record_task_combo_box.currentText()
121
122 if self.use_kinematics_check_box.isChecked():
123 self.incoming_message_signal.connect(self.update_ground_truth_buffer)
124
125 self._has_finished_kinematics = (
126 not self.use_kinematics_check_box.isChecked()
127 )
Step 3.3: Managing Recording Sessions#
This step manages the recording session, which includes updating the UI and recording data.
145 def update_ground_truth_buffer(self, data: np.ndarray) -> None:
146 """Updates the buffer with the incoming kinematics data."""
147 if not self.use_kinematics_check_box.isChecked():
148 return
149
150 self._kinematics__buffer.append((time.time(), data))
151 current_samples = len(self._kinematics__buffer)
152 self._set_progress_bar(
153 self.record_ground_truth_progress_bar,
154 current_samples,
155 self.kinematics_recording_time,
156 )
157
158 if current_samples >= self.kinematics_recording_time:
159 self._main_window.logger.print(
160 f"Kinematics recording finished at: {round(time.time() - self._start_time)} seconds"
161 )
162 self._has_finished_kinematics = True
163 self.incoming_message_signal.disconnect(self.update_ground_truth_buffer)
164 self.check_recording_completion()
166 def check_recording_completion(self) -> None:
167 """Checks if the recording process is complete and finishes it if so."""
168 if (
169 self._recording_protocol.is_biosignal_recording_complete
170 and self._has_finished_kinematics
171 ):
172 self.finish_recording()
174 def finish_recording(self) -> None:
175 """Finishes the recording process and switches to the review recording interface."""
176 self.review_recording_stacked_widget.setCurrentIndex(1)
177 self.record_toggle_push_button.setText("Finished Recording")
178 self.review_recording_task_label.setText(self._current_task.capitalize())
180 def accept_recording(self) -> None:
181 """
182 Accepts the current recording and saves the data to a pickle file.
183
184 The saved data is a dictionary containing:
185
186 - emg: A 2D NumPy array of EMG signals with time samples as rows and channels as columns.
187 - kinematics: A 2D NumPy array of kinematics data (empty if not used).
188 - timings_emg: A 1D NumPy array of timestamps for EMG samples.
189 - timings_kinematics: A 1D NumPy array of timestamps for kinematics samples (empty if not used).
190 - label: The user-provided label for the recording.
191 - task: The task being recorded.
192 - device: The name of the device used for recording.
193 - bad_channels: A list of channels marked as "bad."
194 - _sampling_frequency: The EMG sampling frequency.
195 - kinematics_sampling_frequency: The kinematics sampling frequency.
196 - recording_time: The recording duration in seconds.
197 - use_kinematics: Boolean indicating whether kinematics data was recorded.
198 """
199 label = self.review_recording_label_line_edit.text() or "default"
200 (
201 biosignal_data,
202 biosignal_timings,
203 ) = self._recording_protocol.retrieve_recorded_data()
204
205 self.save_recording(
206 biosignal=biosignal_data,
207 biosignal_timings=biosignal_timings,
208 ground_truth=(
209 np.vstack([data for _, data in self._kinematics__buffer]).T
210 if self.use_kinematics_check_box.isChecked()
211 else np.array([])
212 ),
213 ground_truth_timings=(
214 np.array([time_stamp for time_stamp, _ in self._kinematics__buffer])
215 if self.use_kinematics_check_box.isChecked()
216 else np.array([])
217 ),
218 recording_label=label,
219 task=self._current_task,
220 ground_truth_sampling_frequency=KINEMATICS_SAMPLING_FREQUENCY,
221 use_as_classification=not self.use_kinematics_check_box.isChecked(),
222 record_duration=self.record_duration_spin_box.value(),
223 )
224
225 self.reset_ui()
226 self._main_window.logger.print(
227 f"Recording of task {self._current_task.lower()} with label {label} accepted!"
228 )
230 def reject_recording(self) -> None:
231 """Rejects the current recording and resets the recording interface."""
232 self.reset_ui()
233 self._main_window.logger.print("Recording rejected.")
Step 3.4: Resetting the Interface#
This step resets the recording interface to its initial state.
235 def reset_ui(self) -> None:
236 """Resets the recording interface UI elements."""
237 self.review_recording_stacked_widget.setCurrentIndex(0)
238 self.record_toggle_push_button.setText("Start Recording")
239 self.record_toggle_push_button.setChecked(False)
240 self.record_group_box.setEnabled(True)
241
242 self._recording_protocol._reset_recording_ui()
243
244 self.record_ground_truth_progress_bar.setValue(0)
245 self._kinematics__buffer.clear()
247 def close_event(self, _: QCloseEvent) -> None:
248 """Closes the recording interface."""
249 self.record_toggle_push_button.setChecked(False)
250 self.reset_ui()
251 self._recording_protocol.close_event(_)
252 self._main_window.logger.print("Recording interface closed.")
254 def enable(self):
255 """Enable the UI elements."""
256 self.ui.recordRecordingGroupBox.setEnabled(True)
257 self.ui.recordReviewRecordingStackedWidget.setEnabled(True)
259 def disable(self):
260 """Disable the UI elements."""
261 self.ui.recordRecordingGroupBox.setEnabled(False)
262 self.ui.recordReviewRecordingStackedWidget.setEnabled(False)
Total running time of the script: (0 minutes 0.000 seconds)
Estimated memory usage: 528 MB