datanavigator 1.0.0__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.
@@ -0,0 +1,133 @@
1
+ r"""
2
+ Interactive data visualization for signals, videos, and complex data objects.
3
+
4
+ Browsers
5
+
6
+ - :py:class:`GenericBrowser`: Generic class to browse data. Meant to be extended.
7
+ - :py:class:`SignalBrowser`: Browse an array of pysampled.Data elements, or 2D arrays.
8
+ - :py:class:`PlotBrowser`: Scroll through an array of complex data where a plotting function is defined for each element.
9
+ - :py:class:`VideoBrowser`: Scroll through the frames of a video.
10
+ - :py:class:`VideoPlotBrowser`: Browse through video and 1D signals synced to the video side by side.
11
+ - :py:class:`ComponentBrowser`: Browse signals (e.g., from periodic motion) as scatterplots of components (e.g., from UMAP, PCA).
12
+
13
+
14
+ Point tracking
15
+
16
+ - :py:class:`Video`: Extended VideoReader class with additional functionalities (helper for VideoPointAnnotator).
17
+ - :py:class:`VideoAnnotation`: Manage one point annotation layer in a video.
18
+ - :py:class:`VideoAnnotations`: Manager for multiple video annotation layers.
19
+ - :py:class:`VideoPointAnnotator`: Annotate points in a video.
20
+
21
+ Optical flow
22
+
23
+ - :py:func:`lucas_kanade`: Track points in a video using the Lucas-Kanade algorithm.
24
+ - :py:func:`lucas_kanade_rstc`: Track points in a video using Lucas-Kanade with reverse sigmoid tracking correction.
25
+ - :py:func:`test_lucas_kanade_rstc`: Test function for Lucas-Kanade with reverse sigmoid tracking correction.
26
+
27
+ Assets
28
+
29
+ - :py:class:`Button`: Custom button widget with a 'name' state.
30
+ - :py:class:`StateButton`: Button widget that stores a number/coordinate state.
31
+ - :py:class:`ToggleButton`: Button widget with a toggle state.
32
+ - :py:class:`Selector`: Select points in a plot using the lasso selection widget.
33
+ - :py:class:`StateVariable`: Manage state variables with multiple states.
34
+ - :py:class:`EventData`: Manage the data from one event type in one trial.
35
+ - :py:class:`Event`: Manage selection of a sequence of events.
36
+
37
+ Assetcontainers
38
+
39
+ - :py:class:`AssetContainer`: Container for managing assets such as buttons, memory slots, etc.
40
+ - :py:class:`Buttons`: Manager for buttons in a matplotlib figure or GUI.
41
+ - :py:class:`Selectors`: Manager for selector objects for picking points on line2D objects.
42
+ - :py:class:`MemorySlots`: Manager for memory slots to store and navigate positions.
43
+ - :py:class:`StateVariables`: Manager for state variables.
44
+ - :py:class:`Events`: Manager for event objects.
45
+ """
46
+ import os
47
+ import sys
48
+ import shutil
49
+
50
+ from .__version__ import __version__
51
+
52
+ from ._config import (
53
+ get_cache_folder,
54
+ get_clip_folder,
55
+ set_cache_folder,
56
+ set_clip_folder,
57
+ )
58
+ from .assets import (
59
+ AssetContainer,
60
+ Button,
61
+ Buttons,
62
+ MemorySlots,
63
+ Selector,
64
+ Selectors,
65
+ StateButton,
66
+ StateVariable,
67
+ StateVariables,
68
+ ToggleButton,
69
+ )
70
+ from .events import portion, Event, EventData, Events
71
+
72
+ from .core import GenericBrowser
73
+ from .plots import PlotBrowser
74
+ from .signals import SignalBrowser
75
+ from .videos import VideoBrowser, VideoPlotBrowser
76
+ from .components import ComponentBrowser
77
+
78
+ from .opticalflow import lucas_kanade, lucas_kanade_rstc
79
+ from .pointtracking import VideoAnnotation, VideoAnnotations, VideoPointAnnotator
80
+
81
+ from .utils import (
82
+ TextView,
83
+ Video,
84
+ get_palette,
85
+ is_path_exists_or_creatable,
86
+ is_video,
87
+ ticks_from_times,
88
+ )
89
+
90
+ from .examples import (
91
+ get_example_video,
92
+ EventPickerDemo,
93
+ ButtonDemo,
94
+ SelectorDemo,
95
+ )
96
+
97
+
98
+ def _check_ffmpeg():
99
+ def check_command(command):
100
+ """Check if a command is available in the system's PATH."""
101
+ return shutil.which(command) is not None
102
+
103
+ def print_install_instructions():
104
+ """Print installation instructions for ffmpeg and ffprobe."""
105
+ if sys.platform.startswith("win"):
106
+ print("\nFFmpeg is not installed or not in PATH.")
107
+ print("Download it from: https://ffmpeg.org/download.html")
108
+ print("After installation, add FFmpeg's 'bin' folder to the system PATH.")
109
+ else:
110
+ print("\nFFmpeg is not installed or not in PATH.")
111
+ print("On Debian/Ubuntu, install it with: sudo apt install ffmpeg")
112
+ print("On macOS, install it with: brew install ffmpeg")
113
+ print("On Fedora, install it with: sudo dnf install ffmpeg")
114
+ print("On Arch Linux, install it with: sudo pacman -S ffmpeg")
115
+
116
+ # Check if ffmpeg and ffprobe are available
117
+ ffmpeg_found = check_command("ffmpeg")
118
+
119
+ if not ffmpeg_found:
120
+ print("Cound not find ffmpeg.")
121
+ print_install_instructions()
122
+
123
+
124
+ def _check_clip_folder():
125
+ if not os.path.exists(get_clip_folder()):
126
+ folder = os.getcwd()
127
+ print(f"Using the current working directory-{folder}-for storing video clips.")
128
+ set_clip_folder(folder)
129
+ print("To change, use datanavigator.set_clip_folder(<folder_name>)")
130
+
131
+
132
+ _check_ffmpeg()
133
+ _check_clip_folder()
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
@@ -0,0 +1,53 @@
1
+ """
2
+ This module provides functions to set and get the paths for the clip and cache folders.
3
+
4
+ The clip folder is used to store video clips, for example, when using VideoBrowser.
5
+ The cache folder is used by the :py:mod:`datanavigator.examples` to write json file containing marked events.
6
+ """
7
+
8
+ import os
9
+ import logging
10
+
11
+ # Configure logging
12
+ logging.basicConfig(level=logging.INFO)
13
+
14
+ # Use environment variables for default paths
15
+ CLIP_FOLDER: str = os.getenv("CLIP_FOLDER", "C:\\data\\_clipcollection")
16
+ CACHE_FOLDER: str = os.getenv("CACHE_FOLDER", "C:\\data\\_cache")
17
+
18
+
19
+ def set_clip_folder(folder: str) -> None:
20
+ """Set the path for storing video clips."""
21
+ if not os.path.exists(folder):
22
+ raise ValueError(f"The provided folder path does not exist: {folder}")
23
+
24
+ global CLIP_FOLDER
25
+ CLIP_FOLDER = folder
26
+ logging.info(f"Clip folder set to: {CLIP_FOLDER}")
27
+
28
+ global CACHE_FOLDER
29
+ if not os.path.exists(CACHE_FOLDER):
30
+ logging.info(
31
+ "Setting the cache folder to be the same as the clip folder. To change, use set_cache_folder(<folder_name>)."
32
+ )
33
+ CACHE_FOLDER = folder
34
+
35
+
36
+ def get_clip_folder() -> str:
37
+ """Get the current path of the clip folder."""
38
+ return CLIP_FOLDER
39
+
40
+
41
+ def set_cache_folder(folder: str) -> None:
42
+ """Set the path for the cache folder."""
43
+ if not os.path.exists(folder):
44
+ raise ValueError(f"The provided folder path does not exist: {folder}")
45
+
46
+ global CACHE_FOLDER
47
+ CACHE_FOLDER = folder
48
+ logging.info(f"Cache folder set to: {CACHE_FOLDER}")
49
+
50
+
51
+ def get_cache_folder() -> str:
52
+ """Get the current path of the cache folder."""
53
+ return CACHE_FOLDER
@@ -0,0 +1,362 @@
1
+ """
2
+ This module provides classes and functions for managing various assets such as buttons, selectors, and state variables in a graphical user interface.
3
+
4
+ Classes:
5
+ Button - Add a 'name' state to a matplotlib widget button.
6
+ StateButton - Store a number/coordinate in a button.
7
+ ToggleButton - Add a toggle button to a matplotlib figure.
8
+ Selector - Select points in a plot using the lasso selection widget.
9
+ StateVariable - Manage state variables with multiple states.
10
+
11
+ AssetContainer - Container for managing assets such as buttons, memory slots, etc.
12
+
13
+ Buttons - Manager for buttons in a matplotlib figure or GUI.
14
+ Selectors - Manager for selector objects for picking points on line2D objects.
15
+ MemorySlots - Manager for memory slots to store and navigate positions.
16
+ StateVariables - Manager for state variables.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import numpy as np
22
+ from matplotlib import lines as mlines
23
+ from matplotlib import pyplot as plt
24
+ from matplotlib.path import Path as mPath
25
+ from matplotlib.widgets import Button as ButtonWidget
26
+ from matplotlib.widgets import LassoSelector as LassoSelectorWidget
27
+ from typing import Any, Callable, List, Optional, Union
28
+
29
+ from .utils import TextView
30
+
31
+
32
+ class Button(ButtonWidget):
33
+ """Add a 'name' state to a matplotlib widget button."""
34
+
35
+ def __init__(self, ax, name: str, **kwargs) -> None:
36
+ super().__init__(ax, name, **kwargs)
37
+ self.name = name
38
+
39
+
40
+ class StateButton(Button):
41
+ """Store a number/coordinate in a button."""
42
+
43
+ def __init__(self, ax, name: str, start_state: Any, **kwargs) -> None:
44
+ super().__init__(ax, name, **kwargs)
45
+ self.state = start_state # stores something in the state
46
+
47
+
48
+ class ToggleButton(StateButton):
49
+ """
50
+ Add a toggle button to a matplotlib figure.
51
+
52
+ For example usage, see PlotBrowser.
53
+ """
54
+
55
+ def __init__(self, ax, name: str, start_state: bool = True, **kwargs) -> None:
56
+ super().__init__(ax, name, start_state, **kwargs)
57
+ self.on_clicked(self.toggle)
58
+ self.set_text()
59
+
60
+ def set_text(self) -> None:
61
+ """Set the text of the toggle button."""
62
+ self.label._text = f"{self.name}={self.state}"
63
+
64
+ def toggle(self, event=None) -> None:
65
+ """Toggle the state of the button."""
66
+ self.state = not self.state
67
+ self.set_text()
68
+
69
+ def set_state(self, state: bool) -> None:
70
+ """Set the state of the button."""
71
+ assert isinstance(state, bool)
72
+ self.state = state
73
+ self.set_text()
74
+
75
+
76
+ class Selector:
77
+ """
78
+ Select points in a plot using the lasso selection widget.
79
+
80
+ Indices of selected points are stored in self.sel.
81
+
82
+ Example:
83
+ f, ax = plt.subplots(1, 1)
84
+ ph, = ax.plot(np.random.rand(20))
85
+ plt.show(block=False)
86
+ ls = gui.Lasso(ph)
87
+ ls.start()
88
+ -- play around with selecting points --
89
+ ls.stop() -> disconnects the events
90
+ """
91
+
92
+ def __init__(self, plot_handle: mlines.Line2D) -> None:
93
+ """Initialize the selector with a plot handle."""
94
+ assert isinstance(plot_handle, mlines.Line2D)
95
+ self.plot_handle = plot_handle
96
+ self.xdata, self.ydata = plot_handle.get_data()
97
+ self.ax = plot_handle.axes
98
+ (self.overlay_handle,) = self.ax.plot([], [], ".")
99
+ self.sel = np.zeros(self.xdata.shape, dtype=bool)
100
+ self.is_active = False
101
+
102
+ def get_data(self) -> np.ndarray:
103
+ """Get the data points of the plot."""
104
+ return np.vstack((self.xdata, self.ydata)).T
105
+
106
+ def onselect(self, verts: List[tuple]) -> None:
107
+ """Select if not previously selected; Unselect if previously selected."""
108
+ selected_ind = mPath(verts).contains_points(self.get_data())
109
+ self.sel = np.logical_xor(selected_ind, self.sel)
110
+ sel_x = list(self.xdata[self.sel])
111
+ sel_y = list(self.ydata[self.sel])
112
+ self.overlay_handle.set_data(sel_x, sel_y)
113
+ plt.draw()
114
+
115
+ def start(self, event=None) -> None:
116
+ """Start the lasso selection."""
117
+ self.lasso = LassoSelectorWidget(self.plot_handle.axes, self.onselect)
118
+ self.is_active = True
119
+
120
+ def stop(self, event=None) -> None:
121
+ """Stop the lasso selection."""
122
+ self.lasso.disconnect_events()
123
+ self.is_active = False
124
+
125
+ def toggle(self, event=None) -> None:
126
+ """Toggle the lasso selection."""
127
+ if self.is_active:
128
+ self.stop(event)
129
+ else:
130
+ self.start(event)
131
+
132
+
133
+ class AssetContainer:
134
+ """
135
+ Container for assets such as a button, memoryslot, etc.
136
+
137
+ Args:
138
+ parent (Any): matplotlib figure, or something that has a 'figure' attribute that is a figure.
139
+ """
140
+
141
+ def __init__(self, parent: Any) -> None:
142
+ self._list: List[Any] = [] # list of assets
143
+ self.parent = parent
144
+
145
+ def __len__(self) -> int:
146
+ return len(self._list)
147
+
148
+ @property
149
+ def names(self) -> List[str]:
150
+ return [x.name for x in self._list]
151
+
152
+ def __getitem__(self, key: Union[int, str]) -> Any:
153
+ """Return an asset by the name key or by position in the list."""
154
+ if not self._has_names():
155
+ assert isinstance(key, int)
156
+
157
+ if isinstance(key, int) and key not in self.names:
158
+ return self._list[key]
159
+
160
+ return {x.name: x for x in self._list}[key]
161
+
162
+ def _has_names(self) -> bool:
163
+ try:
164
+ self.names
165
+ return True
166
+ except AttributeError:
167
+ return False
168
+
169
+ def add(self, asset: Any) -> Any:
170
+ """Add an asset to the container."""
171
+ if hasattr(asset, "name"):
172
+ assert asset.name not in self.names
173
+ self._list.append(asset)
174
+ return asset
175
+
176
+ def __contains__(self, item: str) -> bool:
177
+ """Check if an asset with the given name exists in the container."""
178
+ return item in self.names
179
+
180
+
181
+ class Buttons(AssetContainer):
182
+ """Manager for buttons in a matplotlib figure or GUI (see GenericBrowser for example)."""
183
+
184
+ def add(
185
+ self,
186
+ text: str = "Button",
187
+ action_func: Optional[Union[Callable, List[Callable]]] = None,
188
+ pos: Optional[tuple] = None,
189
+ w: float = 0.25,
190
+ h: float = 0.05,
191
+ buf: float = 0.01,
192
+ type_: str = "Push",
193
+ **kwargs,
194
+ ) -> Button:
195
+ """
196
+ Add a button to the parent figure / object.
197
+
198
+ If pos is provided, then w, h, and buf will be ignored.
199
+ """
200
+ assert type_ in ("Push", "Toggle")
201
+ nbtn = len(self)
202
+ if pos is None: # start adding at the top left corner
203
+ parent_fig = self.parent.figure
204
+ mul_factor = 6.4 / parent_fig.get_size_inches()[0]
205
+
206
+ btn_w = w * mul_factor
207
+ btn_h = h * mul_factor
208
+ btn_buf = buf
209
+ pos = (
210
+ btn_buf,
211
+ (1 - btn_buf) - ((btn_buf + btn_h) * (nbtn + 1)),
212
+ btn_w,
213
+ btn_h,
214
+ )
215
+
216
+ if type_ == "Toggle":
217
+ b = ToggleButton(plt.axes(pos), text, **kwargs)
218
+ else:
219
+ b = Button(plt.axes(pos), text, **kwargs)
220
+
221
+ if action_func is not None: # more than one can be attached
222
+ if isinstance(action_func, (list, tuple)):
223
+ for af in action_func:
224
+ b.on_clicked(af)
225
+ else:
226
+ b.on_clicked(action_func)
227
+
228
+ return super().add(b)
229
+
230
+
231
+ class Selectors(AssetContainer):
232
+ """Manager for selector objects - for picking points on line2D objects."""
233
+
234
+ def add(self, plot_handle: mlines.Line2D) -> Selector:
235
+ """Add a selector to the container."""
236
+ return super().add(Selector(plot_handle))
237
+
238
+
239
+ class MemorySlots(AssetContainer):
240
+ """Manager for memory slots to store and navigate positions."""
241
+
242
+ def __init__(self, parent: Any) -> None:
243
+ super().__init__(parent)
244
+ self._list = self.initialize()
245
+ self._memtext = None
246
+
247
+ @staticmethod
248
+ def initialize() -> dict:
249
+ """Initialize memory slots."""
250
+ return {str(k): None for k in range(1, 10)}
251
+
252
+ def disable(self) -> None:
253
+ """Disable memory slots."""
254
+ self._list = {}
255
+
256
+ def enable(self) -> None:
257
+ """Enable memory slots."""
258
+ self._list = self.initialize()
259
+
260
+ def show(self, pos: str = "bottom left") -> None:
261
+ """Show memory slot text."""
262
+ self._memtext = TextView(self._list, fax=self.parent.figure, pos=pos)
263
+
264
+ def update(self, key: str) -> None:
265
+ """
266
+ Handle memory slot updates.
267
+
268
+ Initiate when None, go to the slot if it exists, free slot if pressed when it exists.
269
+ key is the event.key triggered by a callback.
270
+ """
271
+ if self._list[key] is None:
272
+ self._list[key] = self.parent._current_idx
273
+ self.update_display()
274
+ elif self._list[key] == self.parent._current_idx:
275
+ self._list[key] = None
276
+ self.update_display()
277
+ else:
278
+ self.parent._current_idx = self._list[key]
279
+ self.parent.update()
280
+
281
+ def update_display(self) -> None:
282
+ """Refresh memory slot text if it is not hidden."""
283
+ if self._memtext is not None:
284
+ self._memtext.update(self._list)
285
+
286
+ def hide(self) -> None:
287
+ """Hide the memory slot text."""
288
+ if self._memtext is not None:
289
+ self._memtext._text.remove()
290
+ self._memtext = None
291
+
292
+ def is_enabled(self) -> bool:
293
+ """Check if memory slots are enabled."""
294
+ return bool(self._list)
295
+
296
+
297
+ class StateVariable:
298
+ """Manage state variables with multiple states."""
299
+
300
+ def __init__(self, name: str, states: list) -> None:
301
+ self.name = name
302
+ self.states = list(states)
303
+ self._current_state_idx = 0
304
+
305
+ @property
306
+ def current_state(self) -> Any:
307
+ """Get the current state."""
308
+ return self.states[self._current_state_idx]
309
+
310
+ def n_states(self) -> int:
311
+ """Get the number of states."""
312
+ return len(self.states)
313
+
314
+ def cycle(self) -> None:
315
+ """Cycle to the next state."""
316
+ self._current_state_idx = (self._current_state_idx + 1) % self.n_states()
317
+
318
+ def cycle_back(self) -> None:
319
+ """Cycle to the previous state."""
320
+ self._current_state_idx = (self._current_state_idx - 1) % self.n_states()
321
+
322
+ def set_state(self, state: Union[int, str]) -> None:
323
+ """Set the state."""
324
+ if isinstance(state, int):
325
+ assert 0 <= state < self.n_states()
326
+ self._current_state_idx = state
327
+ if isinstance(state, str):
328
+ assert state in self.states
329
+ self._current_state_idx = self.states.index(state)
330
+
331
+
332
+ class StateVariables(AssetContainer):
333
+ """Manager for state variables."""
334
+
335
+ def __init__(self, parent: Any) -> None:
336
+ super().__init__(parent)
337
+ self._text = None
338
+
339
+ def asdict(self) -> dict:
340
+ """Return state variables as a dictionary."""
341
+ return {x.name: x.states for x in self._list}
342
+
343
+ def add(self, name: str, states: list) -> StateVariable:
344
+ """Add a state variable to the container."""
345
+ assert name not in self.names
346
+ return super().add(StateVariable(name, states))
347
+
348
+ def _get_display_text(self) -> List[str]:
349
+ """Get the display text for state variables."""
350
+ return ["State variables:"] + [
351
+ f"{x.name} - {x.current_state}" for x in self._list
352
+ ]
353
+
354
+ def show(self, pos: str = "bottom right") -> None:
355
+ """Show state variables text."""
356
+ self._text = TextView(self._get_display_text(), fax=self.parent.figure, pos=pos)
357
+
358
+ def update_display(self, draw: bool = True) -> None:
359
+ """Update the display of state variables."""
360
+ self._text.update(self._get_display_text())
361
+ if draw:
362
+ plt.draw()