biosignal-device-interface 0.2.1a1__py3-none-any.whl → 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- biosignal_device_interface/constants/devices/__init__.py +3 -3
- biosignal_device_interface/constants/devices/core/base_device_constants.py +61 -61
- biosignal_device_interface/constants/devices/otb/otb_muovi_constants.py +129 -129
- biosignal_device_interface/constants/devices/otb/otb_quattrocento_constants.py +313 -313
- biosignal_device_interface/constants/devices/otb/otb_quattrocento_light_constants.py +59 -59
- biosignal_device_interface/constants/devices/otb/otb_syncstation_constants.py +233 -233
- biosignal_device_interface/constants/plots/color_palette.py +59 -59
- biosignal_device_interface/devices/__init__.py +17 -17
- biosignal_device_interface/devices/core/base_device.py +424 -412
- biosignal_device_interface/devices/otb/__init__.py +29 -29
- biosignal_device_interface/devices/otb/otb_muovi.py +290 -290
- biosignal_device_interface/devices/otb/otb_quattrocento.py +332 -332
- biosignal_device_interface/devices/otb/otb_quattrocento_light.py +210 -210
- biosignal_device_interface/devices/otb/otb_syncstation.py +407 -407
- biosignal_device_interface/gui/device_template_widgets/all_devices_widget.py +51 -51
- biosignal_device_interface/gui/device_template_widgets/core/base_device_widget.py +130 -130
- biosignal_device_interface/gui/device_template_widgets/core/base_multiple_devices_widget.py +108 -108
- biosignal_device_interface/gui/device_template_widgets/otb/otb_devices_widget.py +44 -44
- biosignal_device_interface/gui/device_template_widgets/otb/otb_muovi_plus_widget.py +158 -158
- biosignal_device_interface/gui/device_template_widgets/otb/otb_muovi_widget.py +158 -158
- biosignal_device_interface/gui/device_template_widgets/otb/otb_quattrocento_light_widget.py +174 -174
- biosignal_device_interface/gui/device_template_widgets/otb/otb_quattrocento_widget.py +260 -260
- biosignal_device_interface/gui/device_template_widgets/otb/otb_syncstation_widget.py +262 -262
- biosignal_device_interface/gui/plot_widgets/biosignal_plot_widget.py +500 -501
- biosignal_device_interface/gui/ui/devices_template_widget.ui +38 -38
- biosignal_device_interface/gui/ui/otb_muovi_plus_template_widget.ui +171 -171
- biosignal_device_interface/gui/ui/otb_muovi_template_widget.ui +171 -171
- biosignal_device_interface/gui/ui/otb_quattrocento_light_template_widget.ui +266 -266
- biosignal_device_interface/gui/ui/otb_quattrocento_template_widget.ui +415 -415
- biosignal_device_interface/gui/ui/otb_syncstation_template_widget.ui +732 -732
- biosignal_device_interface/gui/ui_compiled/devices_template_widget.py +56 -56
- biosignal_device_interface/gui/ui_compiled/otb_muovi_plus_template_widget.py +153 -153
- biosignal_device_interface/gui/ui_compiled/otb_muovi_template_widget.py +153 -153
- biosignal_device_interface/gui/ui_compiled/otb_quattrocento_light_template_widget.py +217 -217
- biosignal_device_interface/gui/ui_compiled/otb_quattrocento_template_widget.py +318 -318
- biosignal_device_interface/gui/ui_compiled/otb_syncstation_template_widget.py +495 -495
- {biosignal_device_interface-0.2.1a1.dist-info → biosignal_device_interface-0.2.2.dist-info}/METADATA +3 -2
- biosignal_device_interface-0.2.2.dist-info/RECORD +46 -0
- {biosignal_device_interface-0.2.1a1.dist-info → biosignal_device_interface-0.2.2.dist-info}/WHEEL +1 -1
- {biosignal_device_interface-0.2.1a1.dist-info → biosignal_device_interface-0.2.2.dist-info/licenses}/LICENSE +675 -675
- biosignal_device_interface-0.2.1a1.dist-info/RECORD +0 -46
|
@@ -1,501 +1,500 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
from functools import partial
|
|
3
|
-
from typing import TYPE_CHECKING
|
|
4
|
-
|
|
5
|
-
from PySide6.QtGui import QResizeEvent, QWheelEvent
|
|
6
|
-
from vispy import app, gloo
|
|
7
|
-
from PySide6.QtWidgets import (
|
|
8
|
-
QVBoxLayout,
|
|
9
|
-
QWidget,
|
|
10
|
-
QScrollArea,
|
|
11
|
-
QCheckBox,
|
|
12
|
-
QGridLayout,
|
|
13
|
-
QSizePolicy,
|
|
14
|
-
)
|
|
15
|
-
from PySide6.QtCore import Qt, Signal, QPoint
|
|
16
|
-
import matplotlib.colors as mcolors
|
|
17
|
-
import numpy as np
|
|
18
|
-
from scipy.signal import resample
|
|
19
|
-
|
|
20
|
-
from biosignal_device_interface.constants.plots.color_palette import (
|
|
21
|
-
COLOR_PALETTE_RGB_DARK,
|
|
22
|
-
COLOR_PALETTE_RGB_LIGHT,
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class BiosignalPlotWidget(QWidget):
|
|
27
|
-
bad_channels_updated: Signal = Signal(np.ndarray)
|
|
28
|
-
|
|
29
|
-
def __init__(self, parent=None):
|
|
30
|
-
super().__init__(parent)
|
|
31
|
-
|
|
32
|
-
self.main_window = parent
|
|
33
|
-
self.number_of_lines: int | None = None
|
|
34
|
-
self.number_of_vertices: int | None = None
|
|
35
|
-
self.internal_sampling_frequency_threshold = 256
|
|
36
|
-
self.external_sampling_frequency: int | None = None
|
|
37
|
-
self.internal_sampling_frequency: int | None = None
|
|
38
|
-
self.sampling_factor: int | None = None
|
|
39
|
-
self.downsample_buffer: np.ndarray | None = None
|
|
40
|
-
|
|
41
|
-
self.line_checkboxes: list[QCheckBox] = []
|
|
42
|
-
self.lines_enabled: np.ndarray | None = None
|
|
43
|
-
|
|
44
|
-
self.canvas_layout: QVBoxLayout | None = None
|
|
45
|
-
|
|
46
|
-
self.color_dict = mcolors.CSS4_COLORS
|
|
47
|
-
|
|
48
|
-
self._configure_widget()
|
|
49
|
-
|
|
50
|
-
self.is_configured: bool = False
|
|
51
|
-
|
|
52
|
-
def _configure_widget(self):
|
|
53
|
-
# Create scroll_area
|
|
54
|
-
self.scroll_area = QScrollArea(self)
|
|
55
|
-
self.scroll_area.setHorizontalScrollBarPolicy(
|
|
56
|
-
Qt.ScrollBarAlwaysOff
|
|
57
|
-
) # Disable horizontal scrollbar
|
|
58
|
-
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
|
59
|
-
self.scroll_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
60
|
-
self.scroll_area.setLayoutDirection(Qt.RightToLeft)
|
|
61
|
-
|
|
62
|
-
# Create a layout for the VispyFastPlotWidget
|
|
63
|
-
self.setLayout(QVBoxLayout())
|
|
64
|
-
# Add the scroll_area to the layout
|
|
65
|
-
self.layout().addWidget(self.scroll_area)
|
|
66
|
-
|
|
67
|
-
# Create a layout for the Scroll Area
|
|
68
|
-
self.scroll_area_layout = QVBoxLayout()
|
|
69
|
-
# Add the layout to the scroll_area
|
|
70
|
-
self.scroll_area.setLayout(self.scroll_area_layout)
|
|
71
|
-
|
|
72
|
-
# Make the plot_widget a child of the scroll_area
|
|
73
|
-
self.container_widget = QWidget()
|
|
74
|
-
self.container_widget.setSizePolicy(
|
|
75
|
-
QSizePolicy.Expanding, QSizePolicy.Expanding
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
self.scroll_area.setWidget(self.container_widget)
|
|
79
|
-
|
|
80
|
-
self.container_widget_layout = QGridLayout()
|
|
81
|
-
self.container_widget.setLayout(self.container_widget_layout)
|
|
82
|
-
self.container_widget.setLayoutDirection(Qt.LeftToRight)
|
|
83
|
-
|
|
84
|
-
self.container_widget_layout.setColumnStretch(0, 0)
|
|
85
|
-
self.container_widget_layout.setColumnStretch(1, 1)
|
|
86
|
-
|
|
87
|
-
self.canvas = VispyFastPlotCanvas(
|
|
88
|
-
parent=self.main_window, scroll_area=self.scroll_area
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
self.max_texture_size = gloo.gl.glGetParameter(gloo.gl.GL_MAX_TEXTURE_SIZE)
|
|
92
|
-
self.plot_widget = self.canvas.native
|
|
93
|
-
self.container_widget_layout.addWidget(self.plot_widget, 0, 1, 0, 1)
|
|
94
|
-
|
|
95
|
-
def configure(
|
|
96
|
-
self,
|
|
97
|
-
lines: int,
|
|
98
|
-
sampling_frequency: int = 2000,
|
|
99
|
-
plot_sampling_frequency: int | None = None,
|
|
100
|
-
display_time: int = 10,
|
|
101
|
-
line_height: int = 50,
|
|
102
|
-
background_color: np.ndarray = np.array([18.0, 18.0, 18.0, 1]),
|
|
103
|
-
):
|
|
104
|
-
self.number_of_lines = lines
|
|
105
|
-
self.external_sampling_frequency = sampling_frequency
|
|
106
|
-
if plot_sampling_frequency is not None:
|
|
107
|
-
self.internal_sampling_frequency_threshold = plot_sampling_frequency
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
self.external_sampling_frequency
|
|
111
|
-
> self.internal_sampling_frequency_threshold
|
|
112
|
-
):
|
|
113
|
-
self.sampling_factor = int(
|
|
114
|
-
self.external_sampling_frequency
|
|
115
|
-
/ self.internal_sampling_frequency_threshold
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
self.internal_sampling_frequency = (
|
|
119
|
-
self.external_sampling_frequency // self.sampling_factor
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
else:
|
|
123
|
-
self.internal_sampling_frequency = self.external_sampling_frequency
|
|
124
|
-
|
|
125
|
-
self.number_of_vertices = int(display_time * self.internal_sampling_frequency)
|
|
126
|
-
|
|
127
|
-
background_color = self._check_background_color_for_format(background_color)
|
|
128
|
-
self.setStyleSheet(
|
|
129
|
-
f"background-color: rgba({background_color[0]}, {background_color[1]}, {background_color[2]}, {background_color[3]});"
|
|
130
|
-
)
|
|
131
|
-
self.container_widget.setStyleSheet(
|
|
132
|
-
f"background-color: rgba({background_color[0]}, {background_color[1]}, {background_color[2]}, {background_color[3]});"
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
self.space_for_each_line = min(
|
|
136
|
-
int(self.max_texture_size // self.number_of_lines // 1.5), line_height
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
self.canvas.configure(
|
|
140
|
-
self.number_of_lines, self.number_of_vertices, background_color
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
# Clear the layout
|
|
144
|
-
for i in reversed(range(self.container_widget_layout.rowCount())):
|
|
145
|
-
# Remove the widget from the layout
|
|
146
|
-
if self.container_widget_layout.itemAt(i) is not None:
|
|
147
|
-
self.container_widget_layout.itemAt(i).widget().setParent(None)
|
|
148
|
-
|
|
149
|
-
lines_space = self.space_for_each_line * self.number_of_lines
|
|
150
|
-
self.container_widget.setFixedHeight((lines_space))
|
|
151
|
-
|
|
152
|
-
self.line_checkboxes = []
|
|
153
|
-
self.number_of_lines_enabled = []
|
|
154
|
-
self.lines_enabled = np.ones((self.number_of_lines,)).astype(bool)
|
|
155
|
-
|
|
156
|
-
for i in range(self.number_of_lines):
|
|
157
|
-
checkbox = QCheckBox(f"
|
|
158
|
-
checkbox.setChecked(True)
|
|
159
|
-
checkbox.stateChanged.connect(partial(self._toggle_line, i))
|
|
160
|
-
checkbox.setStyleSheet("padding-left: 10px;")
|
|
161
|
-
checkbox.setFixedHeight(self.space_for_each_line)
|
|
162
|
-
self.line_checkboxes.append(checkbox)
|
|
163
|
-
self.container_widget_layout.addWidget(checkbox, i, 0)
|
|
164
|
-
|
|
165
|
-
self.container_widget_layout.removeWidget(self.plot_widget)
|
|
166
|
-
self.container_widget_layout.addWidget(
|
|
167
|
-
self.plot_widget, 0, 1, self.number_of_lines, 1
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
# Set the standardized height for each row
|
|
171
|
-
for i in range(self.container_widget_layout.rowCount()):
|
|
172
|
-
if i < self.number_of_lines:
|
|
173
|
-
self.container_widget_layout.setRowMinimumHeight(
|
|
174
|
-
i, self.space_for_each_line
|
|
175
|
-
) # Set the minimum height of each row to 100 pixels
|
|
176
|
-
else:
|
|
177
|
-
self.container_widget_layout.setRowMinimumHeight(i, 0)
|
|
178
|
-
|
|
179
|
-
self.is_configured = True
|
|
180
|
-
|
|
181
|
-
def update_plot(self, input_data: np.ndarray) -> None:
|
|
182
|
-
# Downsample input_data with external sampling frequency to match internal sampling frequency
|
|
183
|
-
if self.external_sampling_frequency != self.internal_sampling_frequency:
|
|
184
|
-
if self.downsample_buffer is not None:
|
|
185
|
-
input_data = np.hstack((self.downsample_buffer, input_data))
|
|
186
|
-
self.downsample_buffer = None
|
|
187
|
-
|
|
188
|
-
input_samples = input_data.shape[1]
|
|
189
|
-
left_over_samples = input_samples % self.sampling_factor
|
|
190
|
-
if left_over_samples > 0:
|
|
191
|
-
self.downsample_buffer = input_data[:, -left_over_samples:]
|
|
192
|
-
input_data = input_data[:, :-left_over_samples]
|
|
193
|
-
output_samples = input_samples // self.sampling_factor
|
|
194
|
-
input_data = resample(
|
|
195
|
-
input_data,
|
|
196
|
-
output_samples,
|
|
197
|
-
axis=1,
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
self.canvas.on_update(input_data)
|
|
201
|
-
|
|
202
|
-
def reset_data(self) -> None:
|
|
203
|
-
self.canvas.on_reset()
|
|
204
|
-
|
|
205
|
-
def _toggle_line(self, line_number: int, state: int) -> None:
|
|
206
|
-
is_checked = state == 2
|
|
207
|
-
self.lines_enabled[line_number] = is_checked
|
|
208
|
-
self.canvas.on_update_color(line_number, not is_checked)
|
|
209
|
-
self.bad_channels_updated.emit(self.lines_enabled)
|
|
210
|
-
|
|
211
|
-
def set_lines_enabled(self, indices: list) -> None:
|
|
212
|
-
self.lines_enabled = np.ones((self.number_of_lines,)).astype(bool)
|
|
213
|
-
self.lines_enabled[indices] = False
|
|
214
|
-
|
|
215
|
-
def resizeEvent(self, event: QResizeEvent) -> None:
|
|
216
|
-
self.container_widget.setFixedWidth(self.scroll_area.width())
|
|
217
|
-
|
|
218
|
-
return super().resizeEvent(event)
|
|
219
|
-
|
|
220
|
-
def _check_background_color_for_format(
|
|
221
|
-
self, input_color: str | list | np.ndarray
|
|
222
|
-
) -> np.ndarray:
|
|
223
|
-
if isinstance(input_color, str):
|
|
224
|
-
if input_color.startswith("#"):
|
|
225
|
-
input_color = np.array(
|
|
226
|
-
[int(input_color[i : i + 2], 16) / 255 for i in (1, 3, 5)] + [1]
|
|
227
|
-
)
|
|
228
|
-
elif input_color.startswith("rgb"):
|
|
229
|
-
input_color = np.array(
|
|
230
|
-
[int(i) for i in input_color[4:-1].split(",")] + [1]
|
|
231
|
-
)
|
|
232
|
-
elif input_color.startswith("rgba"):
|
|
233
|
-
input_color = np.array([int(i) for i in input_color[5:-1].split(",")])
|
|
234
|
-
# Check if color is given as "red" or "blue" etc.
|
|
235
|
-
elif input_color in self.color_dict:
|
|
236
|
-
input_color = np.array(
|
|
237
|
-
[
|
|
238
|
-
int(self.color_dict[input_color][i : i + 2], 16) / 255
|
|
239
|
-
for i in (1, 3, 5)
|
|
240
|
-
]
|
|
241
|
-
+ [1]
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
elif isinstance(input_color, list):
|
|
245
|
-
input_color = np.array(input_color)
|
|
246
|
-
|
|
247
|
-
if np.max(input_color) > 1:
|
|
248
|
-
input_color = input_color / 255
|
|
249
|
-
|
|
250
|
-
if input_color.shape[0] == 3:
|
|
251
|
-
input_color = np.append(input_color, 1)
|
|
252
|
-
|
|
253
|
-
return input_color
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
class VispyFastPlotCanvas(app.Canvas):
|
|
257
|
-
def __init__(self, scroll_area: QScrollArea, parent=None):
|
|
258
|
-
super().__init__(keys=None, parent=parent)
|
|
259
|
-
|
|
260
|
-
self.main_widget = parent
|
|
261
|
-
self.plot_scroll_area = scroll_area
|
|
262
|
-
self.line_data = None
|
|
263
|
-
self.line_colors = None
|
|
264
|
-
self._init_shaders()
|
|
265
|
-
self.program = gloo.Program(self.vert_shader, self.frag_shader)
|
|
266
|
-
|
|
267
|
-
self.background_color: np.ndarray = None
|
|
268
|
-
|
|
269
|
-
def configure(
|
|
270
|
-
self,
|
|
271
|
-
lines: int,
|
|
272
|
-
vertices: int,
|
|
273
|
-
background_color: np.ndarray = np.array([18.0, 18.0, 18.0]),
|
|
274
|
-
line_color: np.ndarray | None = None,
|
|
275
|
-
):
|
|
276
|
-
"""_summary_
|
|
277
|
-
|
|
278
|
-
Args:
|
|
279
|
-
lines (int): _description_
|
|
280
|
-
vertices (int): _description_
|
|
281
|
-
background_color (str | np.ndarray, optional): _description_. Defaults to "black".
|
|
282
|
-
line_color (np.ndarray | None, optional): Numpy array of rgb value(s). RGB values range between 0 and 255. Defaults to None.
|
|
283
|
-
"""
|
|
284
|
-
|
|
285
|
-
self.lines = lines
|
|
286
|
-
self.vertices = vertices
|
|
287
|
-
self.background_color = background_color
|
|
288
|
-
|
|
289
|
-
# Generate template signal
|
|
290
|
-
self.line_data = np.zeros((self.lines, self.vertices)).astype(np.float32)
|
|
291
|
-
|
|
292
|
-
colors = []
|
|
293
|
-
if line_color is None:
|
|
294
|
-
for line in range(self.lines):
|
|
295
|
-
if self.is_light_color(self.background_color):
|
|
296
|
-
colors.append(
|
|
297
|
-
COLOR_PALETTE_RGB_LIGHT[line % len(COLOR_PALETTE_RGB_LIGHT)]
|
|
298
|
-
/ 255.0
|
|
299
|
-
)
|
|
300
|
-
else:
|
|
301
|
-
colors.append(
|
|
302
|
-
COLOR_PALETTE_RGB_DARK[line % len(COLOR_PALETTE_RGB_DARK)]
|
|
303
|
-
/ 255.0
|
|
304
|
-
)
|
|
305
|
-
else:
|
|
306
|
-
for line in range(self.lines):
|
|
307
|
-
colors.append(line_color[line % len(line_color)] / 255.0)
|
|
308
|
-
|
|
309
|
-
self.line_colors = np.repeat(colors, self.vertices, axis=0)
|
|
310
|
-
|
|
311
|
-
self.index = np.c_[
|
|
312
|
-
np.repeat(np.repeat(np.arange(1), self.lines), self.vertices),
|
|
313
|
-
np.repeat(np.tile(np.arange(self.lines), 1), self.vertices),
|
|
314
|
-
np.tile(np.arange(self.vertices), self.lines),
|
|
315
|
-
].astype(np.float32)
|
|
316
|
-
|
|
317
|
-
# Setup Program
|
|
318
|
-
self.program["a_position"] = self.line_data.reshape(-1, 1)
|
|
319
|
-
self.program["a_color"] = self.line_colors
|
|
320
|
-
self.program["a_index"] = self.index
|
|
321
|
-
self.program["u_scale"] = (1.0, 1.0)
|
|
322
|
-
self.program["u_size"] = (self.lines, 1)
|
|
323
|
-
self.program["u_n"] = self.vertices
|
|
324
|
-
|
|
325
|
-
gloo.set_viewport(0, 0, *self.physical_size)
|
|
326
|
-
gloo.set_state(
|
|
327
|
-
clear_color=self.background_color,
|
|
328
|
-
blend=True,
|
|
329
|
-
blend_func=("src_alpha", "one_minus_src_alpha"),
|
|
330
|
-
)
|
|
331
|
-
|
|
332
|
-
def on_update(self, input_data: np.ndarray) -> None:
|
|
333
|
-
input_lines = input_data.shape[0]
|
|
334
|
-
input_vertices = input_data.shape[1]
|
|
335
|
-
|
|
336
|
-
assert (
|
|
337
|
-
input_lines == self.lines
|
|
338
|
-
), "Input data lines do not match the configured lines."
|
|
339
|
-
|
|
340
|
-
# Flip the order of the input lines backwards
|
|
341
|
-
input_data = np.flip(input_data, axis=0)
|
|
342
|
-
|
|
343
|
-
# Check if the input data has more vertices than the configured vertices
|
|
344
|
-
if input_vertices > self.vertices:
|
|
345
|
-
self.line_data = input_data[:, -self.vertices :]
|
|
346
|
-
else:
|
|
347
|
-
self.line_data[:, :-input_vertices] = self.line_data[:, input_vertices:]
|
|
348
|
-
self.line_data[:, -input_vertices:] = input_data
|
|
349
|
-
|
|
350
|
-
plot_data = self.line_data.ravel().astype(np.float32)
|
|
351
|
-
self.program["a_position"].set_data(plot_data)
|
|
352
|
-
self.update()
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
fliped_line_number
|
|
362
|
-
* self.vertices
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
fliped_line_number
|
|
370
|
-
* self.vertices
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
fliped_line_number
|
|
381
|
-
* self.vertices
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
self.
|
|
391
|
-
self.
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
self.
|
|
396
|
-
self.
|
|
397
|
-
self.
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
luminance
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
varying
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
float
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
vec2
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
varying
|
|
488
|
-
varying
|
|
489
|
-
varying
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
"""
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from functools import partial
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from PySide6.QtGui import QResizeEvent, QWheelEvent
|
|
6
|
+
from vispy import app, gloo
|
|
7
|
+
from PySide6.QtWidgets import (
|
|
8
|
+
QVBoxLayout,
|
|
9
|
+
QWidget,
|
|
10
|
+
QScrollArea,
|
|
11
|
+
QCheckBox,
|
|
12
|
+
QGridLayout,
|
|
13
|
+
QSizePolicy,
|
|
14
|
+
)
|
|
15
|
+
from PySide6.QtCore import Qt, Signal, QPoint
|
|
16
|
+
import matplotlib.colors as mcolors
|
|
17
|
+
import numpy as np
|
|
18
|
+
from scipy.signal import resample
|
|
19
|
+
|
|
20
|
+
from biosignal_device_interface.constants.plots.color_palette import (
|
|
21
|
+
COLOR_PALETTE_RGB_DARK,
|
|
22
|
+
COLOR_PALETTE_RGB_LIGHT,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BiosignalPlotWidget(QWidget):
|
|
27
|
+
bad_channels_updated: Signal = Signal(np.ndarray)
|
|
28
|
+
|
|
29
|
+
def __init__(self, parent=None):
|
|
30
|
+
super().__init__(parent)
|
|
31
|
+
|
|
32
|
+
self.main_window = parent
|
|
33
|
+
self.number_of_lines: int | None = None
|
|
34
|
+
self.number_of_vertices: int | None = None
|
|
35
|
+
self.internal_sampling_frequency_threshold = 256
|
|
36
|
+
self.external_sampling_frequency: int | None = None
|
|
37
|
+
self.internal_sampling_frequency: int | None = None
|
|
38
|
+
self.sampling_factor: int | None = None
|
|
39
|
+
self.downsample_buffer: np.ndarray | None = None
|
|
40
|
+
|
|
41
|
+
self.line_checkboxes: list[QCheckBox] = []
|
|
42
|
+
self.lines_enabled: np.ndarray | None = None
|
|
43
|
+
|
|
44
|
+
self.canvas_layout: QVBoxLayout | None = None
|
|
45
|
+
|
|
46
|
+
self.color_dict = mcolors.CSS4_COLORS
|
|
47
|
+
|
|
48
|
+
self._configure_widget()
|
|
49
|
+
|
|
50
|
+
self.is_configured: bool = False
|
|
51
|
+
|
|
52
|
+
def _configure_widget(self):
|
|
53
|
+
# Create scroll_area
|
|
54
|
+
self.scroll_area = QScrollArea(self)
|
|
55
|
+
self.scroll_area.setHorizontalScrollBarPolicy(
|
|
56
|
+
Qt.ScrollBarAlwaysOff
|
|
57
|
+
) # Disable horizontal scrollbar
|
|
58
|
+
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
|
59
|
+
self.scroll_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
60
|
+
self.scroll_area.setLayoutDirection(Qt.RightToLeft)
|
|
61
|
+
|
|
62
|
+
# Create a layout for the VispyFastPlotWidget
|
|
63
|
+
self.setLayout(QVBoxLayout())
|
|
64
|
+
# Add the scroll_area to the layout
|
|
65
|
+
self.layout().addWidget(self.scroll_area)
|
|
66
|
+
|
|
67
|
+
# Create a layout for the Scroll Area
|
|
68
|
+
self.scroll_area_layout = QVBoxLayout()
|
|
69
|
+
# Add the layout to the scroll_area
|
|
70
|
+
self.scroll_area.setLayout(self.scroll_area_layout)
|
|
71
|
+
|
|
72
|
+
# Make the plot_widget a child of the scroll_area
|
|
73
|
+
self.container_widget = QWidget()
|
|
74
|
+
self.container_widget.setSizePolicy(
|
|
75
|
+
QSizePolicy.Expanding, QSizePolicy.Expanding
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
self.scroll_area.setWidget(self.container_widget)
|
|
79
|
+
|
|
80
|
+
self.container_widget_layout = QGridLayout()
|
|
81
|
+
self.container_widget.setLayout(self.container_widget_layout)
|
|
82
|
+
self.container_widget.setLayoutDirection(Qt.LeftToRight)
|
|
83
|
+
|
|
84
|
+
self.container_widget_layout.setColumnStretch(0, 0)
|
|
85
|
+
self.container_widget_layout.setColumnStretch(1, 1)
|
|
86
|
+
|
|
87
|
+
self.canvas = VispyFastPlotCanvas(
|
|
88
|
+
parent=self.main_window, scroll_area=self.scroll_area
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
self.max_texture_size = gloo.gl.glGetParameter(gloo.gl.GL_MAX_TEXTURE_SIZE)
|
|
92
|
+
self.plot_widget = self.canvas.native
|
|
93
|
+
self.container_widget_layout.addWidget(self.plot_widget, 0, 1, 0, 1)
|
|
94
|
+
|
|
95
|
+
def configure(
|
|
96
|
+
self,
|
|
97
|
+
lines: int,
|
|
98
|
+
sampling_frequency: int = 2000,
|
|
99
|
+
plot_sampling_frequency: int | None = None,
|
|
100
|
+
display_time: int = 10,
|
|
101
|
+
line_height: int = 50,
|
|
102
|
+
background_color: np.ndarray = np.array([18.0, 18.0, 18.0, 1]),
|
|
103
|
+
):
|
|
104
|
+
self.number_of_lines = lines
|
|
105
|
+
self.external_sampling_frequency = sampling_frequency
|
|
106
|
+
if plot_sampling_frequency is not None:
|
|
107
|
+
self.internal_sampling_frequency_threshold = plot_sampling_frequency
|
|
108
|
+
|
|
109
|
+
if (
|
|
110
|
+
self.external_sampling_frequency
|
|
111
|
+
> self.internal_sampling_frequency_threshold
|
|
112
|
+
):
|
|
113
|
+
self.sampling_factor = int(
|
|
114
|
+
self.external_sampling_frequency
|
|
115
|
+
/ self.internal_sampling_frequency_threshold
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
self.internal_sampling_frequency = (
|
|
119
|
+
self.external_sampling_frequency // self.sampling_factor
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
else:
|
|
123
|
+
self.internal_sampling_frequency = self.external_sampling_frequency
|
|
124
|
+
|
|
125
|
+
self.number_of_vertices = int(display_time * self.internal_sampling_frequency)
|
|
126
|
+
|
|
127
|
+
background_color = self._check_background_color_for_format(background_color)
|
|
128
|
+
self.setStyleSheet(
|
|
129
|
+
f"background-color: rgba({background_color[0]}, {background_color[1]}, {background_color[2]}, {background_color[3]});"
|
|
130
|
+
)
|
|
131
|
+
self.container_widget.setStyleSheet(
|
|
132
|
+
f"background-color: rgba({background_color[0]}, {background_color[1]}, {background_color[2]}, {background_color[3]});"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
self.space_for_each_line = min(
|
|
136
|
+
int(self.max_texture_size // self.number_of_lines // 1.5), line_height
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
self.canvas.configure(
|
|
140
|
+
self.number_of_lines, self.number_of_vertices, background_color
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Clear the layout
|
|
144
|
+
for i in reversed(range(self.container_widget_layout.rowCount())):
|
|
145
|
+
# Remove the widget from the layout
|
|
146
|
+
if self.container_widget_layout.itemAt(i) is not None:
|
|
147
|
+
self.container_widget_layout.itemAt(i).widget().setParent(None)
|
|
148
|
+
|
|
149
|
+
lines_space = self.space_for_each_line * self.number_of_lines
|
|
150
|
+
self.container_widget.setFixedHeight((lines_space))
|
|
151
|
+
|
|
152
|
+
self.line_checkboxes = []
|
|
153
|
+
self.number_of_lines_enabled = []
|
|
154
|
+
self.lines_enabled = np.ones((self.number_of_lines,)).astype(bool)
|
|
155
|
+
|
|
156
|
+
for i in range(self.number_of_lines):
|
|
157
|
+
checkbox = QCheckBox(f"Ch {i + 1}")
|
|
158
|
+
checkbox.setChecked(True)
|
|
159
|
+
checkbox.stateChanged.connect(partial(self._toggle_line, i))
|
|
160
|
+
checkbox.setStyleSheet("padding-left: 10px;")
|
|
161
|
+
checkbox.setFixedHeight(self.space_for_each_line)
|
|
162
|
+
self.line_checkboxes.append(checkbox)
|
|
163
|
+
self.container_widget_layout.addWidget(checkbox, i, 0)
|
|
164
|
+
|
|
165
|
+
self.container_widget_layout.removeWidget(self.plot_widget)
|
|
166
|
+
self.container_widget_layout.addWidget(
|
|
167
|
+
self.plot_widget, 0, 1, self.number_of_lines, 1
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Set the standardized height for each row
|
|
171
|
+
for i in range(self.container_widget_layout.rowCount()):
|
|
172
|
+
if i < self.number_of_lines:
|
|
173
|
+
self.container_widget_layout.setRowMinimumHeight(
|
|
174
|
+
i, self.space_for_each_line
|
|
175
|
+
) # Set the minimum height of each row to 100 pixels
|
|
176
|
+
else:
|
|
177
|
+
self.container_widget_layout.setRowMinimumHeight(i, 0)
|
|
178
|
+
|
|
179
|
+
self.is_configured = True
|
|
180
|
+
|
|
181
|
+
def update_plot(self, input_data: np.ndarray) -> None:
|
|
182
|
+
# Downsample input_data with external sampling frequency to match internal sampling frequency
|
|
183
|
+
if self.external_sampling_frequency != self.internal_sampling_frequency:
|
|
184
|
+
if self.downsample_buffer is not None:
|
|
185
|
+
input_data = np.hstack((self.downsample_buffer, input_data))
|
|
186
|
+
self.downsample_buffer = None
|
|
187
|
+
|
|
188
|
+
input_samples = input_data.shape[1]
|
|
189
|
+
left_over_samples = input_samples % self.sampling_factor
|
|
190
|
+
if left_over_samples > 0:
|
|
191
|
+
self.downsample_buffer = input_data[:, -left_over_samples:]
|
|
192
|
+
input_data = input_data[:, :-left_over_samples]
|
|
193
|
+
output_samples = input_samples // self.sampling_factor
|
|
194
|
+
input_data = resample(
|
|
195
|
+
input_data,
|
|
196
|
+
output_samples,
|
|
197
|
+
axis=1,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
self.canvas.on_update(input_data)
|
|
201
|
+
|
|
202
|
+
def reset_data(self) -> None:
|
|
203
|
+
self.canvas.on_reset()
|
|
204
|
+
|
|
205
|
+
def _toggle_line(self, line_number: int, state: int) -> None:
|
|
206
|
+
is_checked = state == 2
|
|
207
|
+
self.lines_enabled[line_number] = is_checked
|
|
208
|
+
self.canvas.on_update_color(line_number, not is_checked)
|
|
209
|
+
self.bad_channels_updated.emit(self.lines_enabled)
|
|
210
|
+
|
|
211
|
+
def set_lines_enabled(self, indices: list) -> None:
|
|
212
|
+
self.lines_enabled = np.ones((self.number_of_lines,)).astype(bool)
|
|
213
|
+
self.lines_enabled[indices] = False
|
|
214
|
+
|
|
215
|
+
def resizeEvent(self, event: QResizeEvent) -> None:
|
|
216
|
+
self.container_widget.setFixedWidth(self.scroll_area.width())
|
|
217
|
+
|
|
218
|
+
return super().resizeEvent(event)
|
|
219
|
+
|
|
220
|
+
def _check_background_color_for_format(
|
|
221
|
+
self, input_color: str | list | np.ndarray
|
|
222
|
+
) -> np.ndarray:
|
|
223
|
+
if isinstance(input_color, str):
|
|
224
|
+
if input_color.startswith("#"):
|
|
225
|
+
input_color = np.array(
|
|
226
|
+
[int(input_color[i : i + 2], 16) / 255 for i in (1, 3, 5)] + [1]
|
|
227
|
+
)
|
|
228
|
+
elif input_color.startswith("rgb"):
|
|
229
|
+
input_color = np.array(
|
|
230
|
+
[int(i) for i in input_color[4:-1].split(",")] + [1]
|
|
231
|
+
)
|
|
232
|
+
elif input_color.startswith("rgba"):
|
|
233
|
+
input_color = np.array([int(i) for i in input_color[5:-1].split(",")])
|
|
234
|
+
# Check if color is given as "red" or "blue" etc.
|
|
235
|
+
elif input_color in self.color_dict:
|
|
236
|
+
input_color = np.array(
|
|
237
|
+
[
|
|
238
|
+
int(self.color_dict[input_color][i : i + 2], 16) / 255
|
|
239
|
+
for i in (1, 3, 5)
|
|
240
|
+
]
|
|
241
|
+
+ [1]
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
elif isinstance(input_color, list):
|
|
245
|
+
input_color = np.array(input_color)
|
|
246
|
+
|
|
247
|
+
if np.max(input_color) > 1:
|
|
248
|
+
input_color = input_color / 255
|
|
249
|
+
|
|
250
|
+
if input_color.shape[0] == 3:
|
|
251
|
+
input_color = np.append(input_color, 1)
|
|
252
|
+
|
|
253
|
+
return input_color
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class VispyFastPlotCanvas(app.Canvas):
|
|
257
|
+
def __init__(self, scroll_area: QScrollArea, parent=None):
|
|
258
|
+
super().__init__(keys=None, parent=parent)
|
|
259
|
+
|
|
260
|
+
self.main_widget = parent
|
|
261
|
+
self.plot_scroll_area = scroll_area
|
|
262
|
+
self.line_data = None
|
|
263
|
+
self.line_colors = None
|
|
264
|
+
self._init_shaders()
|
|
265
|
+
self.program = gloo.Program(self.vert_shader, self.frag_shader)
|
|
266
|
+
|
|
267
|
+
self.background_color: np.ndarray = None
|
|
268
|
+
|
|
269
|
+
def configure(
|
|
270
|
+
self,
|
|
271
|
+
lines: int,
|
|
272
|
+
vertices: int,
|
|
273
|
+
background_color: np.ndarray = np.array([18.0, 18.0, 18.0]),
|
|
274
|
+
line_color: np.ndarray | None = None,
|
|
275
|
+
):
|
|
276
|
+
"""_summary_
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
lines (int): _description_
|
|
280
|
+
vertices (int): _description_
|
|
281
|
+
background_color (str | np.ndarray, optional): _description_. Defaults to "black".
|
|
282
|
+
line_color (np.ndarray | None, optional): Numpy array of rgb value(s). RGB values range between 0 and 255. Defaults to None.
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
self.lines = lines
|
|
286
|
+
self.vertices = vertices
|
|
287
|
+
self.background_color = background_color
|
|
288
|
+
|
|
289
|
+
# Generate template signal
|
|
290
|
+
self.line_data = np.zeros((self.lines, self.vertices)).astype(np.float32)
|
|
291
|
+
|
|
292
|
+
colors = []
|
|
293
|
+
if line_color is None:
|
|
294
|
+
for line in range(self.lines):
|
|
295
|
+
if self.is_light_color(self.background_color):
|
|
296
|
+
colors.append(
|
|
297
|
+
COLOR_PALETTE_RGB_LIGHT[line % len(COLOR_PALETTE_RGB_LIGHT)]
|
|
298
|
+
/ 255.0
|
|
299
|
+
)
|
|
300
|
+
else:
|
|
301
|
+
colors.append(
|
|
302
|
+
COLOR_PALETTE_RGB_DARK[line % len(COLOR_PALETTE_RGB_DARK)]
|
|
303
|
+
/ 255.0
|
|
304
|
+
)
|
|
305
|
+
else:
|
|
306
|
+
for line in range(self.lines):
|
|
307
|
+
colors.append(line_color[line % len(line_color)] / 255.0)
|
|
308
|
+
|
|
309
|
+
self.line_colors = np.repeat(colors, self.vertices, axis=0)
|
|
310
|
+
|
|
311
|
+
self.index = np.c_[
|
|
312
|
+
np.repeat(np.repeat(np.arange(1), self.lines), self.vertices),
|
|
313
|
+
np.repeat(np.tile(np.arange(self.lines), 1), self.vertices),
|
|
314
|
+
np.tile(np.arange(self.vertices), self.lines),
|
|
315
|
+
].astype(np.float32)
|
|
316
|
+
|
|
317
|
+
# Setup Program
|
|
318
|
+
self.program["a_position"] = self.line_data.reshape(-1, 1)
|
|
319
|
+
self.program["a_color"] = self.line_colors
|
|
320
|
+
self.program["a_index"] = self.index
|
|
321
|
+
self.program["u_scale"] = (1.0, 1.0)
|
|
322
|
+
self.program["u_size"] = (self.lines, 1)
|
|
323
|
+
self.program["u_n"] = self.vertices
|
|
324
|
+
|
|
325
|
+
gloo.set_viewport(0, 0, *self.physical_size)
|
|
326
|
+
gloo.set_state(
|
|
327
|
+
clear_color=self.background_color,
|
|
328
|
+
blend=True,
|
|
329
|
+
blend_func=("src_alpha", "one_minus_src_alpha"),
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
def on_update(self, input_data: np.ndarray) -> None:
|
|
333
|
+
input_lines = input_data.shape[0]
|
|
334
|
+
input_vertices = input_data.shape[1]
|
|
335
|
+
|
|
336
|
+
assert (
|
|
337
|
+
input_lines == self.lines
|
|
338
|
+
), "Input data lines do not match the configured lines."
|
|
339
|
+
|
|
340
|
+
# Flip the order of the input lines backwards
|
|
341
|
+
input_data = np.flip(input_data, axis=0)
|
|
342
|
+
|
|
343
|
+
# Check if the input data has more vertices than the configured vertices
|
|
344
|
+
if input_vertices > self.vertices:
|
|
345
|
+
self.line_data = input_data[:, -self.vertices :]
|
|
346
|
+
else:
|
|
347
|
+
self.line_data[:, :-input_vertices] = self.line_data[:, input_vertices:]
|
|
348
|
+
self.line_data[:, -input_vertices:] = input_data
|
|
349
|
+
|
|
350
|
+
plot_data = self.line_data.ravel().astype(np.float32)
|
|
351
|
+
self.program["a_position"].set_data(plot_data)
|
|
352
|
+
self.update()
|
|
353
|
+
|
|
354
|
+
def on_update_color(self, line_number: int, disable: bool = False) -> None:
|
|
355
|
+
# Update alpha value of the line color
|
|
356
|
+
disable_color = self.background_color
|
|
357
|
+
fliped_line_number = self.lines - line_number - 1
|
|
358
|
+
if disable:
|
|
359
|
+
self.line_colors[
|
|
360
|
+
fliped_line_number
|
|
361
|
+
* self.vertices : (fliped_line_number + 1)
|
|
362
|
+
* self.vertices
|
|
363
|
+
] = disable_color[:3]
|
|
364
|
+
else:
|
|
365
|
+
|
|
366
|
+
if self.is_light_color(self.background_color):
|
|
367
|
+
self.line_colors[
|
|
368
|
+
fliped_line_number
|
|
369
|
+
* self.vertices : (fliped_line_number + 1)
|
|
370
|
+
* self.vertices
|
|
371
|
+
] = (
|
|
372
|
+
COLOR_PALETTE_RGB_LIGHT[
|
|
373
|
+
fliped_line_number % len(COLOR_PALETTE_RGB_LIGHT)
|
|
374
|
+
]
|
|
375
|
+
/ 255.0
|
|
376
|
+
)
|
|
377
|
+
else:
|
|
378
|
+
self.line_colors[
|
|
379
|
+
fliped_line_number
|
|
380
|
+
* self.vertices : (fliped_line_number + 1)
|
|
381
|
+
* self.vertices
|
|
382
|
+
] = (
|
|
383
|
+
COLOR_PALETTE_RGB_DARK[
|
|
384
|
+
fliped_line_number % len(COLOR_PALETTE_RGB_DARK)
|
|
385
|
+
]
|
|
386
|
+
/ 255.0
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
self.program["a_color"].set_data(self.line_colors)
|
|
390
|
+
self.update()
|
|
391
|
+
self.context.flush()
|
|
392
|
+
|
|
393
|
+
def on_reset(self):
|
|
394
|
+
self.line_data = np.zeros((self.lines, self.vertices)).astype(np.float32)
|
|
395
|
+
self.program["a_position"].set_data(self.line_data.ravel().astype(np.float32))
|
|
396
|
+
self.update()
|
|
397
|
+
self.context.flush()
|
|
398
|
+
|
|
399
|
+
def on_resize(self, event):
|
|
400
|
+
gloo.set_viewport(0, 0, *event.physical_size)
|
|
401
|
+
|
|
402
|
+
def on_draw(self, event):
|
|
403
|
+
if self.line_data is None:
|
|
404
|
+
return
|
|
405
|
+
|
|
406
|
+
gloo.clear()
|
|
407
|
+
self.program.draw("line_strip")
|
|
408
|
+
|
|
409
|
+
def on_mouse_wheel(self, event):
|
|
410
|
+
# Get the delta from the mouse event
|
|
411
|
+
dx, dy = event.delta
|
|
412
|
+
|
|
413
|
+
scale_factor = 15
|
|
414
|
+
dx *= scale_factor
|
|
415
|
+
dy *= scale_factor
|
|
416
|
+
# Create a QWheelEvent
|
|
417
|
+
pos = QPoint(event.pos[0], event.pos[1])
|
|
418
|
+
global_pos = QPoint(event.pos[0], event.pos[1])
|
|
419
|
+
pixel_delta = QPoint(dx, dy)
|
|
420
|
+
angle_delta = QPoint(dx * 8, dy * 8) # Convert to eighths of a degree
|
|
421
|
+
buttons = Qt.NoButton
|
|
422
|
+
modifiers = Qt.NoModifier
|
|
423
|
+
phase = Qt.ScrollUpdate
|
|
424
|
+
inverted = False
|
|
425
|
+
|
|
426
|
+
wheel_event = QWheelEvent(
|
|
427
|
+
pos,
|
|
428
|
+
global_pos,
|
|
429
|
+
pixel_delta,
|
|
430
|
+
angle_delta,
|
|
431
|
+
buttons,
|
|
432
|
+
modifiers,
|
|
433
|
+
phase,
|
|
434
|
+
inverted,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
self.plot_scroll_area.wheelEvent(wheel_event)
|
|
438
|
+
|
|
439
|
+
def is_light_color(self, rgb):
|
|
440
|
+
# Normalize to 1
|
|
441
|
+
luminance = 0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]
|
|
442
|
+
return luminance > 0.5
|
|
443
|
+
|
|
444
|
+
def _init_shaders(self):
|
|
445
|
+
self.vert_shader = """
|
|
446
|
+
#version 120
|
|
447
|
+
// y coordinate of the position.
|
|
448
|
+
attribute float a_position;
|
|
449
|
+
// row, col, and time index.
|
|
450
|
+
attribute vec3 a_index;
|
|
451
|
+
varying vec3 v_index;
|
|
452
|
+
// 2D scaling factor (zooming).
|
|
453
|
+
uniform vec2 u_scale;
|
|
454
|
+
// Size of the table.
|
|
455
|
+
uniform vec2 u_size;
|
|
456
|
+
// Number of samples per signal.
|
|
457
|
+
uniform float u_n;
|
|
458
|
+
// Color.
|
|
459
|
+
attribute vec3 a_color;
|
|
460
|
+
varying vec4 v_color;
|
|
461
|
+
// Varying variables used for clipping in the fragment shader.
|
|
462
|
+
varying vec2 v_position;
|
|
463
|
+
varying vec4 v_ab;
|
|
464
|
+
void main() {
|
|
465
|
+
float nrows = u_size.x;
|
|
466
|
+
float ncols = u_size.y;
|
|
467
|
+
// Compute the x coordinate from the time index.
|
|
468
|
+
float x = -1 + 2*a_index.z / (u_n-1);
|
|
469
|
+
vec2 position = vec2(x - (1 - 1 / u_scale.x), a_position);
|
|
470
|
+
// Find the affine transformation for the subplots.
|
|
471
|
+
vec2 a = vec2(1./ncols, 1./nrows)*.9;
|
|
472
|
+
vec2 b = vec2(-1 + 2*(a_index.x+.5) / ncols,
|
|
473
|
+
-1 + 2*(a_index.y+.5) / nrows);
|
|
474
|
+
// Apply the static subplot transformation + scaling.
|
|
475
|
+
gl_Position = vec4(a*u_scale*position+b, 0.0, 1.0);
|
|
476
|
+
v_color = vec4(a_color, 1.);
|
|
477
|
+
v_index = a_index;
|
|
478
|
+
// For clipping test in the fragment shader.
|
|
479
|
+
v_position = gl_Position.xy;
|
|
480
|
+
v_ab = vec4(a, b);
|
|
481
|
+
}
|
|
482
|
+
"""
|
|
483
|
+
|
|
484
|
+
self.frag_shader = """
|
|
485
|
+
#version 120
|
|
486
|
+
varying vec4 v_color;
|
|
487
|
+
varying vec3 v_index;
|
|
488
|
+
varying vec2 v_position;
|
|
489
|
+
varying vec4 v_ab;
|
|
490
|
+
void main() {
|
|
491
|
+
gl_FragColor = v_color;
|
|
492
|
+
// Discard the fragments between the signals (emulate glMultiDrawArrays).
|
|
493
|
+
if ((fract(v_index.x) > 0.) || (fract(v_index.y) > 0.))
|
|
494
|
+
discard;
|
|
495
|
+
// Clipping test.
|
|
496
|
+
vec2 test = abs((v_position.xy-v_ab.zw)/v_ab.xy);
|
|
497
|
+
if ((test.x > 1) || (test.y > 1))
|
|
498
|
+
discard;
|
|
499
|
+
}
|
|
500
|
+
"""
|