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

RecordingInterfaceTemplate(main_window[, ...])

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:

  1. Class Initialization and UI Setup

  2. Starting & Stopping Recordings

  3. Managing Recording Sessions

  4. 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.

Imports#
 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
Class Initialization#
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()
UI Setup#
 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.

Start Recording Preparation - This method prepares the recording session.#
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
Start Recording - This method starts the recording session.#
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.

Update Ground Truth Buffer - This method updates the ground-truth buffer with the current 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()
Check Recording Completion - This method checks if the recording session is complete.#
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()
Finish Recording - This method finalizes the recording session.#
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())
Accept Recording - This method accepts the recorded data and saves it.#
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        )
Reject Recording - This method rejects the recorded data and discards it.#
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.

Reset UI - This method resets the recording interface’s UI elements.#
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()
Close Event - This method handles the closing event of the recording interface.#
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.")
Enable - This method enables the recording interface.#
254    def enable(self):
255        """Enable the UI elements."""
256        self.ui.recordRecordingGroupBox.setEnabled(True)
257        self.ui.recordReviewRecordingStackedWidget.setEnabled(True)
Disable - This method disables the recording interface.#
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

Gallery generated by Sphinx-Gallery