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 manages per-VI recording settings and ground truth data collection.
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#
|
Base class for the recording interface of a visual interface. |
Key constructor parameters:
incoming_message_signal– Signal carrying kinematics data from the setup interface’s UDP receiver.ground_truth__nr_of_recording_values– Number of DOF values per sample.ground_truth__task_map– Task name to label mapping.
You must implement:
initialize_ui_logic()– Wire up widgets, hide per-VI controls.close_event()– Clean up buffers.
You should implement:
start_recording_preparation()– Validate state, clear buffers, calculate expected samples.update_ground_truth_buffer()– Append incoming kinematics data and update the progress bar.check_recording_completion()– Notify the protocol when done.get_ground_truth_data()– Return collected ground truth.
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.
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:
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#
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()
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)
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 }