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    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
UI Setup#
 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.

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

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

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

Gallery generated by Sphinx-Gallery