Part 2: Recording Interface#

The recording interface is the second component of a visual interface in MyoGestic. It manages per-VI recording settings and ground truth data collection.

Shared Task Selector#

Tasks are no longer managed per-VI. A shared task selector in the RecordProtocol manages task selection for all active VIs. The per-VI task combo box is hidden during initialize_ui_logic(), and the _current_task attribute is set by the RecordProtocol before each recording.

VIs in the same category share a task map:

  • Hand VIs (VHI, KHI) use HAND_TASK_MAP (10 gestures).

  • Cursor VIs (VCI) use CURSOR_TASK_MAP (5 directions).

To register your VI in a task category, add an entry to VI_TASK_CATEGORY in myogestic/gui/protocols/record.py:

VI_TASK_CATEGORY["MYI"] = ("Hand", HAND_TASK_MAP)

Ground Truth Data#

Each recording interface must define:

  • ground_truth__task_map – Maps task names to integer labels. This should match the task map for your VI’s category.

  • ground_truth__nr_of_recording_values – Number of DOF values recorded per sample (independent of the number of tasks). For the VHI this is 9 (thumb + 4 fingers + 3 wrist + 1 grasp).

The get_ground_truth_data() method returns a dict with the collected ground truth after recording. During multi-VI recordings, the RecordProtocol collects data from each active VI’s get_ground_truth_data() and saves them in a combined pickle.

Step 1: Create the Recording UI#

Your recording UI must include a QProgressBar named groundTruthProgressBar. Other common widgets:

  • recordRecordingGroupBox – Per-VI settings group box.

  • recordUseKinematicsCheckBox – Toggle kinematics recording.

Note

Copy an existing UI file from myogestic/gui/widgets/visual_interfaces/virtual_hand_interface/ui/ and adapt it.

Step 2: Understand the Base Class#

RecordingInterfaceTemplate(*args, **kwargs)

Base class for the recording interface of a visual interface.

Key constructor parameters:

You must implement:

You should implement:

Step 3: Implement a Recording Interface#

Below is a minimal example, followed by references to the VHI implementation.

Step 3.1: Minimal Recording Interface#

This skeleton shows the required structure. In practice, you would add kinematics buffering, progress tracking, and UI wiring.

import time
import numpy as np
from PySide6.QtCore import SignalInstance
from PySide6.QtGui import QCloseEvent

from myogestic.gui.widgets.templates.visual_interface import RecordingInterfaceTemplate

KINEMATICS_SAMPLING_FREQUENCY = 60


class MyInterface_RecordingInterface(RecordingInterfaceTemplate):
    """Minimal recording interface example."""

    ground_truth__task_map: dict[str, int] = {
        "rest": 0,
        "action_a": 1,
        "action_b": 2,
    }
    ground_truth__nr_of_recording_values: int = 5  # Number of DOF per sample

    def __init__(
        self,
        main_window,
        name: str = "MyInterface",
        incoming_message_signal: SignalInstance = None,
    ) -> None:
        # In practice, pass your generated UI:
        # from .ui import Ui_RecordingMyInterface
        # super().__init__(main_window, name, ui=Ui_RecordingMyInterface(), ...)
        pass  # Replace with real init

    def initialize_ui_logic(self) -> None:
        """Wire up UI and hide per-VI controls managed by RecordProtocol."""
        # Add per-VI GroupBox to the record layout
        # self._main_window.ui.recordVerticalLayout.addWidget(...)
        #
        # Hide per-VI task selector (shared selector handles this):
        # self.record_task_combo_box.hide()
        pass

    def start_recording_preparation(self) -> bool:
        """Validate state and prepare for recording."""
        return True

    def update_ground_truth_buffer(self, data: np.ndarray) -> None:
        """Append kinematics data and update progress bar."""
        pass

    def check_recording_completion(self) -> None:
        """Check if recording is complete and notify protocol."""
        pass

    def get_ground_truth_data(self) -> dict:
        """Return ground truth collected during this recording."""
        return {
            "ground_truth": np.array([]),
            "ground_truth_timings": np.array([]),
            "ground_truth_sampling_frequency": KINEMATICS_SAMPLING_FREQUENCY,
            "task": getattr(self, "_current_task", ""),
            "use_as_classification": True,
        }

    def enable(self) -> None:
        """Enable the per-VI UI elements."""
        pass

    def disable(self) -> None:
        """Disable the per-VI UI elements."""
        pass

    def close_event(self, _: QCloseEvent) -> None:
        """Clean up buffers on close."""
        pass

Step 3.2: VHI Reference – Initialization#

The VHI recording interface records 9 DOF of hand kinematics at 60 Hz and maps 10 gestures.

Imports and Constructor#
 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    """Recording interface for the Virtual Hand Interface.
19
20    Handles per-VI settings (task selector, kinematics checkbox) while the
21    shared RecordProtocol controls manage the Record button, Duration spinner,
22    and unified review dialog.
23    """
24
25    ground_truth__task_map: dict[str, int] = {
26        "rest": 0,
27        "index": 1,
28        "thumb": 2,
29        "middle": 3,
30        "ring": 4,
31        "pinky": 5,
32        "power grasp": 6,
33        "pinch": 7,
34        "tripod pinch": 8,
35        "pointing": 9,
36    }
37    ground_truth__nr_of_recording_values: int = 9
38
39    def __init__(
40        self,
41        main_window,
42        name: str = "VirtualHandInterface",
43        incoming_message_signal: SignalInstance = None,
44    ) -> None:
45        super().__init__(
46            main_window,
47            name,
48            ui=Ui_RecordingVirtualHandInterface(),
49            incoming_message_signal=incoming_message_signal,
50            ground_truth__nr_of_recording_values=self.ground_truth__nr_of_recording_values,
51            ground_truth__task_map=self.ground_truth__task_map,
52        )
53
54        RECORDING_DIR_PATH.mkdir(parents=True, exist_ok=True)
55
56        self._current_task: str = ""
57        self._kinematics__buffer = []
58
59        self._recording_protocol = self._main_window.protocols[0]
60
61        self._has_finished_kinematics: bool = False
62        self._start_time: float = 0
63
64        self.initialize_ui_logic()

Step 3.3: VHI Reference – UI Setup#

Note how per-VI controls are hidden to defer to the shared selector:

initialize_ui_logic – Hide per-VI task combo, wire up widgets#
 66    def initialize_ui_logic(self) -> None:
 67        """Initializes the logic for the UI elements."""
 68        ui: Ui_RecordingVirtualHandInterface = self.ui
 69
 70        self._main_window.ui.recordVerticalLayout.addWidget(ui.recordRecordingGroupBox)
 71        self._main_window.ui.recordVerticalLayout.addWidget(
 72            ui.recordReviewRecordingStackedWidget
 73        )
 74
 75        self.record_group_box = ui.recordRecordingGroupBox
 76        self.record_task_combo_box = ui.recordTaskComboBox
 77        self.record_duration_spin_box = ui.recordDurationSpinBox
 78        self.record_toggle_push_button = ui.recordRecordPushButton
 79
 80        self.review_recording_stacked_widget = ui.recordReviewRecordingStackedWidget
 81        self.review_recording_task_label = ui.reviewRecordingTaskLabel
 82        self.review_recording_label_line_edit = ui.reviewRecordingLabelLineEdit
 83
 84        self.review_recording_accept_push_button = ui.reviewRecordingAcceptPushButton
 85        self.review_recording_reject_push_button = ui.reviewRecordingRejectPushButton
 86
 87        self.use_kinematics_check_box = ui.recordUseKinematicsCheckBox
 88
 89        # Set GroupBox title to VI name
 90        self.record_group_box.setTitle(self.name)
 91
 92        # Hide per-VI controls now managed by shared RecordProtocol
 93        self.record_toggle_push_button.hide()
 94        self.record_duration_spin_box.hide()
 95        ui.label_7.hide()  # Duration label
 96        self.review_recording_stacked_widget.hide()
 97
 98        # Shorten checkbox text (GroupBox title already identifies the VI)
 99        self.use_kinematics_check_box.setText("Use Kinematics")
100
101        # Hide per-VI task selector (now managed by shared RecordProtocol)
102        self.record_task_combo_box.hide()
103        ui.label.hide()  # Task label
104
105        # Add tooltips for remaining visible controls
106        self.use_kinematics_check_box.setToolTip(
107            "Record hand position data from the Virtual Hand Interface"
108        )
109
110        self.record_ground_truth_progress_bar.setValue(0)

Step 3.4: VHI Reference – Recording and Ground Truth#

update_ground_truth_buffer – Append kinematics samples#
129    def update_ground_truth_buffer(self, data: np.ndarray) -> None:
130        """Updates the buffer with the incoming kinematics data."""
131        if not self.use_kinematics_check_box.isChecked():
132            return
133
134        self._kinematics__buffer.append((time.time(), data))
135        current_samples = len(self._kinematics__buffer)
136        self._set_progress_bar(
137            self.record_ground_truth_progress_bar,
138            current_samples,
139            self.kinematics_recording_time,
140        )
141
142        if current_samples >= self.kinematics_recording_time:
143            self._main_window.logger.print(
144                f"Kinematics recording finished at: {round(time.time() - self._start_time)} seconds"
145            )
146            self._has_finished_kinematics = True
147            self.incoming_message_signal.disconnect(self.update_ground_truth_buffer)
148            self.check_recording_completion()
check_recording_completion – Notify RecordProtocol when done#
150    def check_recording_completion(self) -> None:
151        """Checks if this VI's recording is complete and notifies the protocol."""
152        if (
153            self._recording_protocol.is_biosignal_recording_complete
154            and self._has_finished_kinematics
155        ):
156            self._recording_protocol.vi_recording_completed(self.name)
get_ground_truth_data – Return kinematics or empty (classification)#
158    def get_ground_truth_data(self) -> dict:
159        """Return kinematics data if checkbox checked, else empty."""
160        if self.use_kinematics_check_box.isChecked() and self._kinematics__buffer:
161            return {
162                "ground_truth": np.vstack(
163                    [data for _, data in self._kinematics__buffer]
164                ).T,
165                "ground_truth_timings": np.array(
166                    [t for t, _ in self._kinematics__buffer]
167                ),
168                "ground_truth_sampling_frequency": KINEMATICS_SAMPLING_FREQUENCY,
169                "task": self._current_task,
170                "use_as_classification": False,
171            }
172        return {
173            "ground_truth": np.array([]),
174            "ground_truth_timings": np.array([]),
175            "ground_truth_sampling_frequency": KINEMATICS_SAMPLING_FREQUENCY,
176            "task": self._current_task,
177            "use_as_classification": True,
178        }

Gallery generated by Sphinx-Gallery