dv-toolkit 0.2.0__tar.gz
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.
- dv_toolkit-0.2.0/LICENSE +21 -0
- dv_toolkit-0.2.0/PKG-INFO +18 -0
- dv_toolkit-0.2.0/README.md +268 -0
- dv_toolkit-0.2.0/pyproject.toml +3 -0
- dv_toolkit-0.2.0/python/src/dv_toolkit/__init__.py +2 -0
- dv_toolkit-0.2.0/python/src/dv_toolkit/plot/__init__.py +4 -0
- dv_toolkit-0.2.0/python/src/dv_toolkit/plot/player.py +80 -0
- dv_toolkit-0.2.0/python/src/dv_toolkit/plot/tools/__init__.py +5 -0
- dv_toolkit-0.2.0/python/src/dv_toolkit/plot/tools/func.py +56 -0
- dv_toolkit-0.2.0/python/src/dv_toolkit/plot/tools/go_player.py +247 -0
- dv_toolkit-0.2.0/python/src/dv_toolkit/plot/tools/mp_player.py +281 -0
- dv_toolkit-0.2.0/python/src/dv_toolkit.egg-info/PKG-INFO +18 -0
- dv_toolkit-0.2.0/python/src/dv_toolkit.egg-info/SOURCES.txt +17 -0
- dv_toolkit-0.2.0/python/src/dv_toolkit.egg-info/dependency_links.txt +1 -0
- dv_toolkit-0.2.0/python/src/dv_toolkit.egg-info/not-zip-safe +1 -0
- dv_toolkit-0.2.0/python/src/dv_toolkit.egg-info/requires.txt +4 -0
- dv_toolkit-0.2.0/python/src/dv_toolkit.egg-info/top_level.txt +2 -0
- dv_toolkit-0.2.0/setup.cfg +4 -0
- dv_toolkit-0.2.0/setup.py +207 -0
dv_toolkit-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Kuga Maxx
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dv_toolkit
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: a generic and simple toolkit for processing event-based data
|
|
5
|
+
Author: Kuga Maxx
|
|
6
|
+
Author-email: KugaMaxx@outlook.com
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: numpy>=1.24
|
|
10
|
+
Requires-Dist: dv_processing>=2.0
|
|
11
|
+
Requires-Dist: plotly>=5.17
|
|
12
|
+
Requires-Dist: matplotlib>=3.7
|
|
13
|
+
Dynamic: author
|
|
14
|
+
Dynamic: author-email
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
Dynamic: requires-dist
|
|
17
|
+
Dynamic: requires-python
|
|
18
|
+
Dynamic: summary
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# DV Toolkit
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
DV Toolkit is an extension of [dv-processing](https://dv-processing.inivation.com/),
|
|
7
|
+
providing a series of wrapped modules to help users to do processing and
|
|
8
|
+
analysis on event camera data. Any questions, please contact me with
|
|
9
|
+
[KugaMaxx@outlook.com](mailto:KugaMaxx@outlook.com).
|
|
10
|
+
|
|
11
|
+
<div align=center><img src="https://github.com/KugaMaxx/yam-toolkit/blob/main/assets/images/demonstrate.gif" alt="demonstrate" width="100%"></div>
|
|
12
|
+
|
|
13
|
+
## Background
|
|
14
|
+
|
|
15
|
+
### Why create this extension?
|
|
16
|
+
|
|
17
|
+
The dv-processing library provides convenience for developers to handle event-based data. However, there is still room for improvement in **data reading, slicing, and visualization** during actual usage. Therefore, this repository has made the following improvements:
|
|
18
|
+
|
|
19
|
+
+ Enables fast interoperation between C++ and Python.
|
|
20
|
+
+ Adds data alignment and slicing for events, frames, imus, and triggers.
|
|
21
|
+
+ Adds offline data reading and processing, eliminating the need for online operations.
|
|
22
|
+
+ Implements a more convenient visual interactive interface in Python.
|
|
23
|
+
+ Provides a unified usage method for C++ and Python interfaces.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
### Preliminaries
|
|
28
|
+
|
|
29
|
+
Since our library is an extension of dv-processing, you need to install dv-processing first. The official installation tutorial is available [here](https://dv-processing.inivation.com/rel_1.7/installation.html), or you can follow the steps below to install it on Ubuntu 20.04:
|
|
30
|
+
|
|
31
|
+
+ Install necessary dependencies that dv-processing required:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
sudo apt-get install libboost-all-dev libeigen3-dev libopencv-dev
|
|
35
|
+
sudo apt-get install pybind11-dev python3-dev python3-numpy
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
+ Add repository and install dv-processing:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
sudo add-apt-repository ppa:inivation-ppa/inivation
|
|
42
|
+
sudo apt-get update
|
|
43
|
+
sudo apt-get install libcaer-dev libfmt-dev liblz4-dev libzstd-dev libssl-dev libusb-1.0-0-dev
|
|
44
|
+
sudo apt-get install dv-processing
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Git submodule usage
|
|
48
|
+
|
|
49
|
+
Assuming you are working on a project in Git, you can use this repository as a
|
|
50
|
+
submodule. Here is an example of placing this repository as a dependency in the
|
|
51
|
+
`/external` folder.
|
|
52
|
+
|
|
53
|
+
+ Add the repository as a submodule in your project:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
git submodule add git@github.com:KugaMaxx/yam-toolkit.git external/dv-toolkit
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
+ Use in your cmake project:
|
|
60
|
+
|
|
61
|
+
```CMake
|
|
62
|
+
# Find dv-processing supports
|
|
63
|
+
find_package(dv-processing REQUIRED)
|
|
64
|
+
|
|
65
|
+
# Install toolkit supports.
|
|
66
|
+
add_subdirectory(external/dv-toolkit)
|
|
67
|
+
|
|
68
|
+
# link your targets against the library
|
|
69
|
+
target_link_libraries(your_target
|
|
70
|
+
dv::processing
|
|
71
|
+
dv::toolkit
|
|
72
|
+
...)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Python package usage
|
|
76
|
+
|
|
77
|
+
This repository also allows for Python integration, which can be used by binding
|
|
78
|
+
it as a Python package before usage. Here is an example in a conda environment
|
|
79
|
+
named `toolkit`. Please refer to the [README](https://github.com/KugaMaxx/yam-toolkit/blob/main/python/README.md) to get more information.
|
|
80
|
+
|
|
81
|
+
+ Create conda environment and install:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# recommend python ≥ 3.8
|
|
85
|
+
conda create -n toolkit
|
|
86
|
+
|
|
87
|
+
# activate environment
|
|
88
|
+
conda activate toolkit
|
|
89
|
+
|
|
90
|
+
# include pybind11 as submodule
|
|
91
|
+
git submodule update --init
|
|
92
|
+
|
|
93
|
+
# install as package
|
|
94
|
+
pip install .
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
+ Import the library in your script:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
# must have
|
|
101
|
+
import dv_processing as dv
|
|
102
|
+
|
|
103
|
+
# introduce extension
|
|
104
|
+
import dv_toolkit as kit
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Getting started
|
|
108
|
+
|
|
109
|
+
### Standard Type
|
|
110
|
+
|
|
111
|
+
The dv-toolkit library encapsulates `event`, `frame`, `imu`, and `trigger` into
|
|
112
|
+
addressable storage types, which supports add, erase and slice operations.
|
|
113
|
+
|
|
114
|
+
+ `dv::toolkit::MonoCameraData` stores all basic storage types of event-based data.
|
|
115
|
+
|
|
116
|
+
```C++
|
|
117
|
+
#include <dv-toolkit/core/core.hpp>
|
|
118
|
+
|
|
119
|
+
int main() {
|
|
120
|
+
namespace kit = dv::toolkit;
|
|
121
|
+
|
|
122
|
+
// Initialize MonoCameraData
|
|
123
|
+
kit::MonoCameraData data;
|
|
124
|
+
|
|
125
|
+
// Access immutable variables through functions, support types are:
|
|
126
|
+
// events, frames, imus and triggers
|
|
127
|
+
std::cout << data.events() << std::endl;
|
|
128
|
+
// "Storage is empty!"
|
|
129
|
+
|
|
130
|
+
// Emplace back event elements, the function arguments are:
|
|
131
|
+
// timestamp, x, y, polarity
|
|
132
|
+
kit::EventStorage store;
|
|
133
|
+
store.emplace_back(dv::now(), 0, 0, false);
|
|
134
|
+
store.emplace_back(dv::now(), 1, 1, true);
|
|
135
|
+
store.emplace_back(dv::now(), 2, 2, false);
|
|
136
|
+
store.emplace_back(dv::now(), 3, 3, true);
|
|
137
|
+
|
|
138
|
+
// Assignment value to MonoCameraData
|
|
139
|
+
data["events"] = store;
|
|
140
|
+
|
|
141
|
+
// Access mutable variables through std::get
|
|
142
|
+
std::cout << std::get<kit::EVTS>(data["events"]) << std::endl;
|
|
143
|
+
// "Storage containing 4 elements within ..."
|
|
144
|
+
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### I/O Operations
|
|
150
|
+
|
|
151
|
+
The dv-toolkit library provide convenient method to read and write standard
|
|
152
|
+
aedat4 files offline. It facilitates the repeated operation and processing on
|
|
153
|
+
data, avoiding the issue of reading and writing files over and over again.
|
|
154
|
+
|
|
155
|
+
+ `dv::toolkit::MonoCameraReader` reads offline data from standard aedat4
|
|
156
|
+
files.
|
|
157
|
+
|
|
158
|
+
```C++
|
|
159
|
+
#include <dv-toolkit/io/reader.hpp>
|
|
160
|
+
|
|
161
|
+
int main() {
|
|
162
|
+
namespace kit = dv::toolkit;
|
|
163
|
+
|
|
164
|
+
// Initialize reader
|
|
165
|
+
kit::io::MonoCameraReader reader("/path/to/aedat4");
|
|
166
|
+
|
|
167
|
+
// Get offline MonoCameraData
|
|
168
|
+
kit::MonoCameraData data = reader.loadData();
|
|
169
|
+
|
|
170
|
+
// Check all basic types
|
|
171
|
+
std::cout << data.events() << std::endl;
|
|
172
|
+
std::cout << data.frames() << std::endl;
|
|
173
|
+
std::cout << data.imus() << std::endl;
|
|
174
|
+
std::cout << data.triggers() << std::endl;
|
|
175
|
+
|
|
176
|
+
// Get camera resolution
|
|
177
|
+
// Can also use reader.getEventResolution()
|
|
178
|
+
const auto resolution = reader.getResolution("events");
|
|
179
|
+
if (resolution.has_value()) {
|
|
180
|
+
std::cout << *resolution << std::endl;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
+ `dv::toolkit::MonoCameraWrite` write data back to standard aedat4 files.
|
|
188
|
+
|
|
189
|
+
```C++
|
|
190
|
+
#include <dv-toolkit/core/core.hpp>
|
|
191
|
+
#include <dv-toolkit/io/reader.hpp>
|
|
192
|
+
#include <dv-toolkit/io/writer.hpp>
|
|
193
|
+
#include <dv-toolkit/simulation/generator.hpp>
|
|
194
|
+
|
|
195
|
+
int main() {
|
|
196
|
+
namespace kit = dv::toolkit;
|
|
197
|
+
|
|
198
|
+
// Enable literal time expression from the chrono library
|
|
199
|
+
using namespace std::chrono_literals;
|
|
200
|
+
|
|
201
|
+
// Initialize MonoCameraData
|
|
202
|
+
kit::MonoCameraData data;
|
|
203
|
+
|
|
204
|
+
// Initialize resolution
|
|
205
|
+
const auto resolution = cv::Size(346, 260);
|
|
206
|
+
|
|
207
|
+
// Create sample events
|
|
208
|
+
data["events"] = kit::simulation::generateSampleEvents(resolution);
|
|
209
|
+
|
|
210
|
+
// Initialize MonoCameraWriter
|
|
211
|
+
kit::io::MonoCameraWriter writer("/path/to/file.aedat4", resolution);
|
|
212
|
+
|
|
213
|
+
// Write MonoCameraData
|
|
214
|
+
writer.writeData(data);
|
|
215
|
+
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Unified stream slicing
|
|
221
|
+
|
|
222
|
+
Thanks to the redefined standard data structure, it is possible to achieve
|
|
223
|
+
unified slicing of various types of data by using dv-toolkit library.
|
|
224
|
+
|
|
225
|
+
+ `dv::toolkit::MonoCameraSlicer` allows to slice by time or by number. Before registering each slicer, please ensure that the reference type of the slicer
|
|
226
|
+
is specified.
|
|
227
|
+
|
|
228
|
+
```C++
|
|
229
|
+
#include <dv-toolkit/core/slicer.hpp>
|
|
230
|
+
#include <dv-toolkit/io/reader.hpp>
|
|
231
|
+
|
|
232
|
+
int main() {
|
|
233
|
+
namespace kit = dv::toolkit;
|
|
234
|
+
|
|
235
|
+
// Initialize reader
|
|
236
|
+
kit::io::MonoCameraReader reader("/path/to/aedat4");
|
|
237
|
+
|
|
238
|
+
// Get offline MonoCameraData
|
|
239
|
+
kit::MonoCameraData data = reader.loadData();
|
|
240
|
+
|
|
241
|
+
// Initialize slicer, it will have no jobs at this time
|
|
242
|
+
kit::MonoCameraSlicer slicer;
|
|
243
|
+
|
|
244
|
+
// Use this namespace to enable literal time expression from the chrono library
|
|
245
|
+
using namespace std::chrono_literals;
|
|
246
|
+
|
|
247
|
+
// Register this method to be called every 33 millisecond of events
|
|
248
|
+
slicer.doEveryTimeInterval("events", 33ms, [](const kit::MonoCameraData &mono) {
|
|
249
|
+
std::cout << mono.events() << std::endl;
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Register this method to be called every 2 elements of frames
|
|
253
|
+
slicer.doEveryNumberOfElements("frames", 2, [](const kit::MonoCameraData &mono) {
|
|
254
|
+
std::cout << mono.events() << std::endl;
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Now push the store into the slicer, the data contents within the store
|
|
258
|
+
// can be arbitrary, the slicer implementation takes care of correct slicing
|
|
259
|
+
// algorithm and calls the previously registered callbacks accordingly.
|
|
260
|
+
slicer.accept(data);
|
|
261
|
+
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Acknowledgement
|
|
267
|
+
|
|
268
|
+
Special thanks to [Jinze Chen](mailto:chjz@mail.ustc.edu.cn).
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from typing import Tuple
|
|
2
|
+
from datetime import timedelta
|
|
3
|
+
|
|
4
|
+
from ..lib import _lib_toolkit as kit
|
|
5
|
+
|
|
6
|
+
from .tools import mp_player
|
|
7
|
+
from .tools import go_player
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OfflineMonoCameraPlayer(object):
|
|
11
|
+
def __init__(self,
|
|
12
|
+
resolution: Tuple[int, int],
|
|
13
|
+
mode: str = "hybrid",
|
|
14
|
+
core: str = "matplotlib"):
|
|
15
|
+
if (len(resolution) == 2):
|
|
16
|
+
self.resolution = resolution
|
|
17
|
+
else:
|
|
18
|
+
raise ValueError("Resolution must be a tuple of length 2.")
|
|
19
|
+
|
|
20
|
+
if core == "matplotlib":
|
|
21
|
+
self.core = mp_player
|
|
22
|
+
elif core == "plotly":
|
|
23
|
+
self.core = go_player
|
|
24
|
+
else:
|
|
25
|
+
raise ValueError("Invalid player core. use 'matplotlib' or 'plotly'")
|
|
26
|
+
|
|
27
|
+
if mode in ["hybrid", "2d", "3d"]:
|
|
28
|
+
self.mode = mode
|
|
29
|
+
else:
|
|
30
|
+
raise ValueError("Invalid view mode. use '2d', '3d' or 'hybrid'")
|
|
31
|
+
|
|
32
|
+
def viewPerTimeInterval(self, data,
|
|
33
|
+
reference: str = "events",
|
|
34
|
+
interval: timedelta = timedelta(milliseconds=33)):
|
|
35
|
+
# slice data
|
|
36
|
+
slicer, packets = kit.MonoCameraSlicer(), list()
|
|
37
|
+
slicer.doEveryTimeInterval(
|
|
38
|
+
reference, interval, lambda data: packets.append(data)
|
|
39
|
+
)
|
|
40
|
+
slicer.accept(data)
|
|
41
|
+
|
|
42
|
+
# run
|
|
43
|
+
self._run(
|
|
44
|
+
self.core.Figure(),
|
|
45
|
+
self.core.Preset(self.resolution, packets)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def viewPerNumberInterval(self, data,
|
|
49
|
+
reference: str = "events",
|
|
50
|
+
interval: int = 15000):
|
|
51
|
+
# slice data
|
|
52
|
+
slicer, packets = kit.MonoCameraSlicer(), list()
|
|
53
|
+
slicer.doEveryNumberOfElements(
|
|
54
|
+
reference, interval, lambda data: packets.append(data)
|
|
55
|
+
)
|
|
56
|
+
slicer.accept(data)
|
|
57
|
+
|
|
58
|
+
# run
|
|
59
|
+
self._run(
|
|
60
|
+
self.core.Figure(),
|
|
61
|
+
self.core.Preset(self.resolution, packets)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def _run(self, figure, preset):
|
|
65
|
+
figure.set_ticks(preset.get_ticks())
|
|
66
|
+
if self.mode == "2d":
|
|
67
|
+
figure.set_subplot(rows=1, cols=1, specs=[[{"type": "2d"}]])
|
|
68
|
+
figure.append_trace(row=1, col=1, plot_func=preset.plot_2d_frame, set_func=preset.set_2d_plot)
|
|
69
|
+
figure.append_trace(row=1, col=1, plot_func=preset.plot_2d_event, set_func=preset.set_2d_plot)
|
|
70
|
+
elif self.mode == "3d":
|
|
71
|
+
figure.set_subplot(rows=1, cols=1, specs=[[{"type": "3d"}]])
|
|
72
|
+
figure.append_trace(row=1, col=1, plot_func=preset.plot_3d_frame, set_func=preset.set_3d_plot)
|
|
73
|
+
figure.append_trace(row=1, col=1, plot_func=preset.plot_3d_event, set_func=preset.set_3d_plot)
|
|
74
|
+
else:
|
|
75
|
+
figure.set_subplot(rows=1, cols=2, specs=[[{"type": "2d"}, {"type": "3d"}]])
|
|
76
|
+
figure.append_trace(row=1, col=1, plot_func=preset.plot_2d_frame, set_func=preset.set_2d_plot)
|
|
77
|
+
figure.append_trace(row=1, col=1, plot_func=preset.plot_2d_event, set_func=preset.set_2d_plot)
|
|
78
|
+
figure.append_trace(row=1, col=2, plot_func=preset.plot_3d_frame, set_func=preset.set_3d_plot)
|
|
79
|
+
figure.append_trace(row=1, col=2, plot_func=preset.plot_3d_event, set_func=preset.set_3d_plot)
|
|
80
|
+
figure.show()
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _unravel_index(index, shape):
|
|
6
|
+
row, col = np.unravel_index(index, shape)
|
|
7
|
+
return row, col
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _ravel_multi_index(row, col, shape):
|
|
11
|
+
ind = np.ravel_multi_index(np.array([row, col]), shape)
|
|
12
|
+
return ind
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _reformat_layout(list_of_dict, mapping: dict):
|
|
16
|
+
for _list in list_of_dict:
|
|
17
|
+
for _dict in _list:
|
|
18
|
+
_dict['type'] = mapping[_dict['type']]
|
|
19
|
+
|
|
20
|
+
return list_of_dict
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _visualize_events(events, size, mode="accumulate"):
|
|
24
|
+
counts = np.zeros(size)
|
|
25
|
+
_bins = [size[0], size[1]]
|
|
26
|
+
_range = [[0, size[0]], [0, size[1]]]
|
|
27
|
+
|
|
28
|
+
# w/ polar
|
|
29
|
+
if mode == 'polar':
|
|
30
|
+
counts = np.histogram2d(events.ys(), events.xs(),
|
|
31
|
+
weights=(-1) ** (1 + events.polarities()),
|
|
32
|
+
bins=_bins, range=_range)[0]
|
|
33
|
+
return counts
|
|
34
|
+
|
|
35
|
+
# w/o polar
|
|
36
|
+
elif mode == 'monopolar':
|
|
37
|
+
counts = np.histogram2d(events.ys(), events.xs(),
|
|
38
|
+
weights=(+1) ** (1 + events.polarities()),
|
|
39
|
+
bins=_bins, range=_range)[0]
|
|
40
|
+
return counts
|
|
41
|
+
|
|
42
|
+
# count before polar assignment
|
|
43
|
+
elif mode == 'accumulate':
|
|
44
|
+
counts = np.histogram2d(events.ys(), events.xs(),
|
|
45
|
+
weights=(+1) ** (1 + events.polarities()),
|
|
46
|
+
bins=_bins, range=_range)[0]
|
|
47
|
+
weight = np.zeros(size)
|
|
48
|
+
weight[events.ys(), events.xs()] = (-1) ** (1 + events.polarities())
|
|
49
|
+
return counts * weight
|
|
50
|
+
|
|
51
|
+
return counts
|
|
52
|
+
|
|
53
|
+
def _convert_to_rgba(image):
|
|
54
|
+
if len(image.shape) == 2:
|
|
55
|
+
image = np.stack((image,) * 3, axis=-1)
|
|
56
|
+
return np.concatenate((image, 255 * np.ones((*image.shape[:2], 1))), axis=2)
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import dv_processing as dv
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
import plotly.graph_objects as go
|
|
5
|
+
|
|
6
|
+
from .func import _reformat_layout, _visualize_events
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Animator(object):
|
|
10
|
+
def __init__(self, canvas, update, ticks, fps=25):
|
|
11
|
+
self._canvas = canvas
|
|
12
|
+
self._frames = [update(i) for i in range(len(ticks))]
|
|
13
|
+
self._ticks = ticks
|
|
14
|
+
self._interval = int(1E3 / fps)
|
|
15
|
+
self._init_widgets()
|
|
16
|
+
|
|
17
|
+
def _init_widgets(self):
|
|
18
|
+
self._buttons = [{
|
|
19
|
+
"type": "buttons",
|
|
20
|
+
"buttons": [
|
|
21
|
+
{
|
|
22
|
+
"method": "animate", "label": "▶",
|
|
23
|
+
"args": [None, {"frame": {"duration": self._interval},
|
|
24
|
+
"mode": "immediate", "fromcurrent": True}]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"method": "animate", "label": "◼",
|
|
28
|
+
"args": [[None], {"frame": {"duration": self._interval},
|
|
29
|
+
"mode": "immediate", "fromcurrent": True}]
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"x": 0.1, "y": 0.0, "direction": "left", "pad": {"r": 10, "t": 70},
|
|
33
|
+
}]
|
|
34
|
+
|
|
35
|
+
self._sliders = [{
|
|
36
|
+
"steps": [
|
|
37
|
+
{
|
|
38
|
+
"method": "animate", "label": self._ticks[i],
|
|
39
|
+
"args": [[self._ticks[i]], {"frame": {"duration": self._interval},
|
|
40
|
+
"mode": "immediate", "fromcurrent": True}]
|
|
41
|
+
} for i, val in enumerate(self._frames)
|
|
42
|
+
],
|
|
43
|
+
"x": 0.1, "y": 0.0, "len": 0.9, "pad": {"b": 10, "t": 50},
|
|
44
|
+
}]
|
|
45
|
+
self._canvas.layout.update(
|
|
46
|
+
sliders=self._sliders,
|
|
47
|
+
updatemenus=self._buttons,
|
|
48
|
+
template="plotly_white",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def run(self):
|
|
52
|
+
self._canvas.update(frames=self._frames)
|
|
53
|
+
self._canvas.show()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Figure(object):
|
|
57
|
+
def __init__(self):
|
|
58
|
+
self._figure = go.Figure()
|
|
59
|
+
self._traces = {
|
|
60
|
+
"inds": list(),
|
|
61
|
+
"rows": list(),
|
|
62
|
+
"cols": list(),
|
|
63
|
+
"plot_func": list(),
|
|
64
|
+
"set_func": list(),
|
|
65
|
+
"kwargs": list(),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
self._index = 0
|
|
69
|
+
self._nrows = 0
|
|
70
|
+
self._ncols = 0
|
|
71
|
+
self._shape = (0, 0)
|
|
72
|
+
self._layouts = None
|
|
73
|
+
self._ticks = range(1)
|
|
74
|
+
|
|
75
|
+
def set_ticks(self, ticks):
|
|
76
|
+
self._ticks = ticks
|
|
77
|
+
|
|
78
|
+
def set_subplot(self, rows, cols, specs):
|
|
79
|
+
self._nrows = rows
|
|
80
|
+
self._ncols = cols
|
|
81
|
+
self._shape = (rows, cols)
|
|
82
|
+
self._layouts = _reformat_layout(specs, {'2d': 'xy', '3d': 'scene'})
|
|
83
|
+
|
|
84
|
+
def append_trace(self, row, col, plot_func, set_func, **kwargs):
|
|
85
|
+
self._index = self._index + 1
|
|
86
|
+
self._traces["rows"].append(row)
|
|
87
|
+
self._traces["cols"].append(col)
|
|
88
|
+
self._traces["plot_func"].append(plot_func)
|
|
89
|
+
self._traces["set_func"].append(set_func)
|
|
90
|
+
self._traces["kwargs"].append(kwargs)
|
|
91
|
+
self._traces["inds"].append(self._index - 1)
|
|
92
|
+
|
|
93
|
+
def show(self):
|
|
94
|
+
self._traces["data"] = list()
|
|
95
|
+
for plot_func, kwargs in zip(self._traces["plot_func"], self._traces["kwargs"]):
|
|
96
|
+
self._traces["data"].append(plot_func(**kwargs))
|
|
97
|
+
|
|
98
|
+
self._figure.set_subplots(rows=self._nrows, cols=self._ncols, specs=self._layouts)
|
|
99
|
+
self._figure.add_traces(data=self._traces["data"],
|
|
100
|
+
rows=self._traces["rows"],
|
|
101
|
+
cols=self._traces["cols"])
|
|
102
|
+
|
|
103
|
+
for set_func, kwargs in zip(self._traces["set_func"], self._traces["kwargs"]):
|
|
104
|
+
self._figure.layout.update(set_func(**kwargs))
|
|
105
|
+
|
|
106
|
+
Animator(canvas=self._figure, update=self._update, ticks=self._ticks).run()
|
|
107
|
+
|
|
108
|
+
def _update(self, k):
|
|
109
|
+
data = list()
|
|
110
|
+
for obj, plot_func, kwargs in zip(self._traces["data"], self._traces["plot_func"], self._traces["kwargs"]):
|
|
111
|
+
data.append(plot_func(obj, k, **kwargs))
|
|
112
|
+
self._traces["data"] = data
|
|
113
|
+
|
|
114
|
+
return go.Frame(data=self._traces["data"],
|
|
115
|
+
traces=self._traces["inds"], name=self._ticks[k])
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class Preset(object):
|
|
119
|
+
def __init__(self, size, packets):
|
|
120
|
+
"""
|
|
121
|
+
Create a Figure() class with default settings
|
|
122
|
+
"""
|
|
123
|
+
self._size = size
|
|
124
|
+
self._packets = packets
|
|
125
|
+
self._ticks = range(len(packets))
|
|
126
|
+
|
|
127
|
+
self._fr_cmap = 'gray'
|
|
128
|
+
self._ev_cmap = [[0.0, "rgba(223,73,63,1)"],
|
|
129
|
+
[0.5, "rgba(0,0,0,0)"],
|
|
130
|
+
[1.0, "rgba(46,102,153,1)"]]
|
|
131
|
+
|
|
132
|
+
# ---------------- 2d settings ----------------
|
|
133
|
+
def set_2d_plot(self, **kwargs):
|
|
134
|
+
return {
|
|
135
|
+
"xaxis": dict(range=[0, self._size[0]]),
|
|
136
|
+
"yaxis": dict(range=[0, self._size[1]],
|
|
137
|
+
scaleanchor="x",
|
|
138
|
+
scaleratio=1.),
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
def plot_2d_event(self, obj=None, i=None, **kwargs):
|
|
142
|
+
if i is None:
|
|
143
|
+
obj = go.Heatmap(
|
|
144
|
+
z=[], zmin=-1, zmax=1,
|
|
145
|
+
colorscale=self._ev_cmap,
|
|
146
|
+
showscale=False,
|
|
147
|
+
showlegend=False,
|
|
148
|
+
)
|
|
149
|
+
return obj
|
|
150
|
+
|
|
151
|
+
events = self._packets[i].events()
|
|
152
|
+
if not events.isEmpty():
|
|
153
|
+
image = _visualize_events(events, self._size[::-1])
|
|
154
|
+
obj = go.Heatmap(
|
|
155
|
+
z=np.flip(image, axis=0),
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
obj = go.Heatmap(
|
|
159
|
+
z=np.zeros(self._size[::-1]),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return obj
|
|
163
|
+
|
|
164
|
+
def plot_2d_frame(self, obj=None, i=None, **kwargs):
|
|
165
|
+
if i is None:
|
|
166
|
+
obj = go.Heatmap(
|
|
167
|
+
z=[], zmin=0, zmax=255,
|
|
168
|
+
colorscale=self._fr_cmap,
|
|
169
|
+
showscale=False,
|
|
170
|
+
showlegend=False,
|
|
171
|
+
)
|
|
172
|
+
return obj
|
|
173
|
+
|
|
174
|
+
frames = self._packets[i].frames()
|
|
175
|
+
if not frames.isEmpty():
|
|
176
|
+
image = frames.front().image
|
|
177
|
+
obj = go.Heatmap(
|
|
178
|
+
z=np.flip(image, axis=0)
|
|
179
|
+
)
|
|
180
|
+
self._tmp_frame = frames.front().image
|
|
181
|
+
|
|
182
|
+
return obj
|
|
183
|
+
|
|
184
|
+
# ---------------- 3d settings ----------------
|
|
185
|
+
def set_3d_plot(self, **kwargs):
|
|
186
|
+
return {
|
|
187
|
+
"scene": dict(yaxis=dict(range=[0, self._size[0]]),
|
|
188
|
+
zaxis=dict(range=[self._size[1], 0]),
|
|
189
|
+
aspectratio=dict(x=1., y=self._size[0]/500, z=self._size[1]/500)),
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
def plot_3d_event(self, obj=None, i=None, **kwargs):
|
|
193
|
+
if i is None:
|
|
194
|
+
obj = go.Scatter3d(
|
|
195
|
+
x=[], y=[], z=[],
|
|
196
|
+
mode='markers',
|
|
197
|
+
marker=dict(
|
|
198
|
+
size=2,
|
|
199
|
+
cmin=0,
|
|
200
|
+
cmax=1,
|
|
201
|
+
colorscale=self._ev_cmap,
|
|
202
|
+
),
|
|
203
|
+
showlegend=False,
|
|
204
|
+
)
|
|
205
|
+
return obj
|
|
206
|
+
|
|
207
|
+
events = self._packets[i].events()
|
|
208
|
+
if not events.isEmpty():
|
|
209
|
+
obj = go.Scatter3d(
|
|
210
|
+
x=[f"{datetime.fromtimestamp(ts * 1E-6).strftime('%H:%M:%S.%f')}"
|
|
211
|
+
for ts in events.timestamps()],
|
|
212
|
+
y=events.xs(), z=events.ys(),
|
|
213
|
+
marker=dict(color=events.polarities()),
|
|
214
|
+
)
|
|
215
|
+
else:
|
|
216
|
+
obj = go.Scatter3d(
|
|
217
|
+
x=[], y=[], z=[]
|
|
218
|
+
)
|
|
219
|
+
return obj
|
|
220
|
+
|
|
221
|
+
def plot_3d_frame(self, obj=None, i=None, **kwargs):
|
|
222
|
+
if i is None:
|
|
223
|
+
obj = go.Surface(
|
|
224
|
+
x=[], y=[], z=[],
|
|
225
|
+
surfacecolor=np.full((*self._size, 1), np.nan),
|
|
226
|
+
colorscale=self._fr_cmap,
|
|
227
|
+
cmin=0,
|
|
228
|
+
cmax=255,
|
|
229
|
+
showscale=False,
|
|
230
|
+
showlegend=False,
|
|
231
|
+
)
|
|
232
|
+
return obj
|
|
233
|
+
|
|
234
|
+
frames = self._packets[i].frames()
|
|
235
|
+
if not frames.isEmpty():
|
|
236
|
+
for frame in frames:
|
|
237
|
+
xx, yy = np.mgrid[0:self._size[0], 0:self._size[1]]
|
|
238
|
+
obj = go.Surface(
|
|
239
|
+
x=np.full(self._size, datetime.fromtimestamp(frame.timestamp * 1E-6).strftime('%H:%M:%S.%f')),
|
|
240
|
+
y=xx, z=yy,
|
|
241
|
+
surfacecolor=np.fliplr(np.rot90(frame.image, -1))
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return obj
|
|
245
|
+
|
|
246
|
+
def get_ticks(self):
|
|
247
|
+
return self._ticks
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
import numpy as np
|
|
3
|
+
import dv_processing as dv
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
import matplotlib.dates as mdates
|
|
8
|
+
import matplotlib.colors as mcolors
|
|
9
|
+
from matplotlib.animation import FuncAnimation
|
|
10
|
+
from matplotlib.widgets import Button, Slider, TextBox
|
|
11
|
+
from mpl_toolkits.axes_grid1 import make_axes_locatable
|
|
12
|
+
|
|
13
|
+
from .func import _reformat_layout, _ravel_multi_index, _visualize_events, _convert_to_rgba
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Animator(FuncAnimation):
|
|
17
|
+
"""
|
|
18
|
+
Animated interactive plot using matplotlib
|
|
19
|
+
https://stackoverflow.com/questions/46325447/animated-interactive-plot-using-matplotlib
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, canvas, update, ticks, fps=25) -> None:
|
|
23
|
+
self._canvas = canvas
|
|
24
|
+
self._frames = update
|
|
25
|
+
self._ticks = ticks
|
|
26
|
+
self._len = len(ticks) - 1
|
|
27
|
+
self._pos = -1
|
|
28
|
+
self._min = 0
|
|
29
|
+
self._max = self._len if self._len > 0 else 1
|
|
30
|
+
self._run = True
|
|
31
|
+
self._forward = True
|
|
32
|
+
self._interval = int(1E3 / fps)
|
|
33
|
+
self._init_widgets()
|
|
34
|
+
super().__init__(self._canvas, self._frames, frames=self._play(), interval=self._interval, save_count=self._len)
|
|
35
|
+
|
|
36
|
+
def _init_widgets(self):
|
|
37
|
+
# add another axes
|
|
38
|
+
playerax = self._canvas.add_axes([0.13, 0.05, 0.74, 0.04])
|
|
39
|
+
divider = make_axes_locatable(playerax)
|
|
40
|
+
|
|
41
|
+
# create button on axes
|
|
42
|
+
bax = divider.append_axes("right", size="100%", pad="15%")
|
|
43
|
+
sax = divider.append_axes("right", size="100%", pad="15%")
|
|
44
|
+
fax = divider.append_axes("right", size="100%", pad="15%")
|
|
45
|
+
ofax = divider.append_axes("right", size="100%", pad="15%")
|
|
46
|
+
sliderax = divider.append_axes("right", size="700%", pad="30%")
|
|
47
|
+
textax = divider.append_axes("right", size="200%", pad="30%")
|
|
48
|
+
self.button_oneback = Button(playerax, label='$\u29CF$')
|
|
49
|
+
self.button_backward = Button(bax, label='$\u25C0$')
|
|
50
|
+
self.button_pauseplay = Button(sax, label='$\u25A0$')
|
|
51
|
+
self.button_forward = Button(fax, label='$\u25B6$')
|
|
52
|
+
self.button_onenext = Button(ofax, label='$\u29D0$')
|
|
53
|
+
self.slider = Slider(sliderax, '', self._min, self._max, valstep=1)
|
|
54
|
+
self.text = TextBox(textax, '', color='1.', textalignment='center')
|
|
55
|
+
|
|
56
|
+
# create button functions
|
|
57
|
+
self.button_oneback.on_clicked(self._play_back_frame)
|
|
58
|
+
self.button_backward.on_clicked(self._play_backwards)
|
|
59
|
+
self.button_pauseplay.on_clicked(self._set_stop)
|
|
60
|
+
self.button_forward.on_clicked(self._play_forwards)
|
|
61
|
+
self.button_onenext.on_clicked(self._play_next_frame)
|
|
62
|
+
self.slider.on_changed(self._set_position)
|
|
63
|
+
self.slider.valtext.set_visible(False)
|
|
64
|
+
|
|
65
|
+
def run(self, path=None, fps=30):
|
|
66
|
+
if path is None:
|
|
67
|
+
plt.show()
|
|
68
|
+
else:
|
|
69
|
+
self.save(path, writer='imagemagick', fps=fps)
|
|
70
|
+
|
|
71
|
+
# ---------------- call back function ----------------
|
|
72
|
+
def _play(self):
|
|
73
|
+
while self._run:
|
|
74
|
+
self._one_step()
|
|
75
|
+
yield self._pos
|
|
76
|
+
|
|
77
|
+
def _one_step(self):
|
|
78
|
+
if self._len == 0:
|
|
79
|
+
self._pos = 0
|
|
80
|
+
self.event_source.stop()
|
|
81
|
+
elif 0 < self._pos < self._len:
|
|
82
|
+
self._pos += self._forward - (not self._forward)
|
|
83
|
+
elif self._pos <= 0:
|
|
84
|
+
self._pos += self._forward
|
|
85
|
+
elif self._pos >= self._len and not self._forward:
|
|
86
|
+
self._pos -= not self._forward
|
|
87
|
+
self.slider.set_val(self._pos)
|
|
88
|
+
self.text.set_val(f"{self._ticks[self._pos]}")
|
|
89
|
+
self._frames(self._pos)
|
|
90
|
+
self._canvas.canvas.draw_idle()
|
|
91
|
+
|
|
92
|
+
def _play_back_frame(self, event=None):
|
|
93
|
+
self._run, self._forward = False, False
|
|
94
|
+
self.event_source.stop()
|
|
95
|
+
self._one_step()
|
|
96
|
+
|
|
97
|
+
def _play_backwards(self, event=None):
|
|
98
|
+
self._run, self._forward = True, False
|
|
99
|
+
self.event_source.start()
|
|
100
|
+
|
|
101
|
+
def _set_stop(self, event=None):
|
|
102
|
+
if not self._run:
|
|
103
|
+
self._run = True
|
|
104
|
+
self.event_source.start()
|
|
105
|
+
elif self._run:
|
|
106
|
+
self._run = False
|
|
107
|
+
self.event_source.stop()
|
|
108
|
+
|
|
109
|
+
def _play_forwards(self, event=None):
|
|
110
|
+
self._run, self._forward = True, True
|
|
111
|
+
self.event_source.start()
|
|
112
|
+
|
|
113
|
+
def _play_next_frame(self, event=None):
|
|
114
|
+
self._run, self._forward = False, True
|
|
115
|
+
self.event_source.stop()
|
|
116
|
+
self._one_step()
|
|
117
|
+
|
|
118
|
+
def _set_position(self, i):
|
|
119
|
+
self._pos = int(self.slider.val)
|
|
120
|
+
self.text.set_val(f"{self._ticks[self._pos]}")
|
|
121
|
+
self._frames(self._pos)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class Figure(object):
|
|
125
|
+
def __init__(self):
|
|
126
|
+
self._figure = plt.figure(figsize=(16, 9))
|
|
127
|
+
self._traces = {
|
|
128
|
+
"inds": list(),
|
|
129
|
+
"rows": list(),
|
|
130
|
+
"cols": list(),
|
|
131
|
+
"plot_func": list(),
|
|
132
|
+
"set_func": list(),
|
|
133
|
+
"kwargs": list(),
|
|
134
|
+
"AxesSubplots": list(),
|
|
135
|
+
"AxesImages": list(),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
self._index = 0
|
|
139
|
+
self._nrows = 0
|
|
140
|
+
self._ncols = 0
|
|
141
|
+
self._shape = (0, 0)
|
|
142
|
+
self._layouts = None
|
|
143
|
+
self._ticks = range(1)
|
|
144
|
+
|
|
145
|
+
def set_ticks(self, ticks):
|
|
146
|
+
self._ticks = ticks
|
|
147
|
+
|
|
148
|
+
def set_subplot(self, rows, cols, specs):
|
|
149
|
+
self._nrows = rows
|
|
150
|
+
self._ncols = cols
|
|
151
|
+
self._shape = (rows, cols)
|
|
152
|
+
self._layouts = _reformat_layout(specs, {'2d': None, '3d': '3d'})
|
|
153
|
+
|
|
154
|
+
def append_trace(self, row, col, plot_func, set_func, **kwargs):
|
|
155
|
+
self._index = self._index + 1
|
|
156
|
+
self._traces["rows"].append(row)
|
|
157
|
+
self._traces["cols"].append(col)
|
|
158
|
+
self._traces["plot_func"].append(plot_func)
|
|
159
|
+
self._traces["set_func"].append(set_func)
|
|
160
|
+
self._traces["kwargs"].append(kwargs)
|
|
161
|
+
self._traces["inds"].append(_ravel_multi_index(row-1, col-1, self._shape))
|
|
162
|
+
|
|
163
|
+
def show(self):
|
|
164
|
+
ax_list = list()
|
|
165
|
+
for ind, _layout in enumerate(list(itertools.chain(*self._layouts))):
|
|
166
|
+
ax_list.append(self._figure.add_subplot(*self._shape, ind+1, projection=_layout["type"]))
|
|
167
|
+
|
|
168
|
+
for ind, plot_func, set_func, kwargs \
|
|
169
|
+
in zip(self._traces["inds"], self._traces["plot_func"], self._traces["set_func"], self._traces["kwargs"]):
|
|
170
|
+
AxesSubplot = set_func(ax_list[ind], **kwargs)
|
|
171
|
+
self._traces["AxesSubplots"].append(AxesSubplot)
|
|
172
|
+
self._traces["AxesImages"].append(plot_func(AxesSubplot, **kwargs))
|
|
173
|
+
|
|
174
|
+
Animator(canvas=self._figure, update=self._update, ticks=self._ticks).run()
|
|
175
|
+
|
|
176
|
+
def _update(self, k):
|
|
177
|
+
for i in range(self._index):
|
|
178
|
+
plot_func = self._traces["plot_func"][i]
|
|
179
|
+
AxesSubplot, AxesImage = self._traces["AxesSubplots"][i], self._traces["AxesImages"][i]
|
|
180
|
+
self._traces["AxesImages"][i] = plot_func(AxesSubplot, AxesImage, k, **self._traces["kwargs"][i])
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class Preset(object):
|
|
184
|
+
def __init__(self, size, packets) -> None:
|
|
185
|
+
"""
|
|
186
|
+
Create a Figure() class with default settings
|
|
187
|
+
"""
|
|
188
|
+
self._size = size
|
|
189
|
+
self._packets = packets
|
|
190
|
+
self._ticks = range(len(packets))
|
|
191
|
+
|
|
192
|
+
self._fr_cmap = plt.get_cmap('gray')
|
|
193
|
+
self._ev_cmap = mcolors.LinearSegmentedColormap.from_list("evCmap", [(0.871, 0.286, 0.247, 1.),
|
|
194
|
+
(0.000, 0.000, 0.000, 0.),
|
|
195
|
+
(0.180, 0.400, 0.600, 1.)])
|
|
196
|
+
|
|
197
|
+
# ---------------- 2d settings ----------------
|
|
198
|
+
def set_2d_plot(self, ax, **kwargs):
|
|
199
|
+
ax.set_xlim(0, self._size[0])
|
|
200
|
+
ax.set_ylim(0, self._size[1])
|
|
201
|
+
ax.set_xticks([])
|
|
202
|
+
ax.set_yticks([])
|
|
203
|
+
return ax
|
|
204
|
+
|
|
205
|
+
def plot_2d_event(self, ax, obj=None, i=None, **kwargs):
|
|
206
|
+
if i is None:
|
|
207
|
+
obj = ax.imshow(
|
|
208
|
+
np.zeros(self._size[::-1]),
|
|
209
|
+
vmin=-1, vmax=1,
|
|
210
|
+
cmap=self._ev_cmap
|
|
211
|
+
)
|
|
212
|
+
return obj
|
|
213
|
+
|
|
214
|
+
events = self._packets[i].events()
|
|
215
|
+
if not events.isEmpty():
|
|
216
|
+
image = _visualize_events(events, self._size[::-1])
|
|
217
|
+
obj.set_data(np.flip(image, axis=0))
|
|
218
|
+
else:
|
|
219
|
+
obj.set_data(np.zeros(self._size[::-1]))
|
|
220
|
+
|
|
221
|
+
return obj
|
|
222
|
+
|
|
223
|
+
def plot_2d_frame(self, ax, obj=None, i=None, **kwargs):
|
|
224
|
+
if i is None:
|
|
225
|
+
obj = ax.imshow(
|
|
226
|
+
np.zeros(self._size[::-1]),
|
|
227
|
+
vmin=0, vmax=255,
|
|
228
|
+
cmap=self._fr_cmap
|
|
229
|
+
)
|
|
230
|
+
return obj
|
|
231
|
+
|
|
232
|
+
frames = self._packets[i].frames()
|
|
233
|
+
if not frames.isEmpty():
|
|
234
|
+
image = frames.front().image
|
|
235
|
+
obj.set_data(np.flip(image, axis=0))
|
|
236
|
+
|
|
237
|
+
return obj
|
|
238
|
+
|
|
239
|
+
# ---------------- 3d settings ----------------
|
|
240
|
+
def set_3d_plot(self, ax, **kwargs):
|
|
241
|
+
ax.set_ylim3d(0, self._size[0])
|
|
242
|
+
ax.set_zlim3d(self._size[1], 0)
|
|
243
|
+
ax.set_box_aspect((500, self._size[0], self._size[1]))
|
|
244
|
+
ax.view_init(elev=45, azim=30, roll=0)
|
|
245
|
+
return ax
|
|
246
|
+
|
|
247
|
+
def plot_3d_event(self, ax, obj=None, i=None, **kwargs):
|
|
248
|
+
if i is None:
|
|
249
|
+
obj = ax.scatter([], [], [], c=[], s=1.0, vmin=0, vmax=1, cmap=self._ev_cmap)
|
|
250
|
+
return obj
|
|
251
|
+
|
|
252
|
+
events = self._packets[i].events()
|
|
253
|
+
if not events.isEmpty():
|
|
254
|
+
obj._offsets3d = (events.timestamps(), events.xs(), events.ys())
|
|
255
|
+
obj.set_array(events.polarities())
|
|
256
|
+
ax.set_xlim3d(events.getLowestTime(), events.getHighestTime())
|
|
257
|
+
ax.set_xticks(np.linspace(events.getLowestTime(), events.getHighestTime(), 5))
|
|
258
|
+
ax.set_xticklabels([f"{datetime.fromtimestamp(ts * 1E-6).strftime('%H:%M:%S')}s"
|
|
259
|
+
for ts in ax.get_xticks().tolist()])
|
|
260
|
+
else:
|
|
261
|
+
obj._offsets3d = ([], [], [])
|
|
262
|
+
|
|
263
|
+
return obj
|
|
264
|
+
|
|
265
|
+
def plot_3d_frame(self, ax, obj=None, i=None, **kwargs):
|
|
266
|
+
xx, yy = np.mgrid[0:self._size[0], 0:self._size[1]]
|
|
267
|
+
if i is None:
|
|
268
|
+
obj = ax.plot_surface(np.nan, xx, yy, facecolors=np.full((*self._size, 1), np.nan))
|
|
269
|
+
return obj
|
|
270
|
+
|
|
271
|
+
frames = self._packets[i].frames()
|
|
272
|
+
if not frames.isEmpty():
|
|
273
|
+
obj.remove()
|
|
274
|
+
for frame in frames:
|
|
275
|
+
image = np.fliplr(np.rot90(_convert_to_rgba(frame.image) / 255.0, -1))
|
|
276
|
+
obj = ax.plot_surface(frame.timestamp, xx, yy, facecolors=image)
|
|
277
|
+
|
|
278
|
+
return obj
|
|
279
|
+
|
|
280
|
+
def get_ticks(self):
|
|
281
|
+
return self._ticks
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dv_toolkit
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: a generic and simple toolkit for processing event-based data
|
|
5
|
+
Author: Kuga Maxx
|
|
6
|
+
Author-email: KugaMaxx@outlook.com
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: numpy>=1.24
|
|
10
|
+
Requires-Dist: dv_processing>=2.0
|
|
11
|
+
Requires-Dist: plotly>=5.17
|
|
12
|
+
Requires-Dist: matplotlib>=3.7
|
|
13
|
+
Dynamic: author
|
|
14
|
+
Dynamic: author-email
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
Dynamic: requires-dist
|
|
17
|
+
Dynamic: requires-python
|
|
18
|
+
Dynamic: summary
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
python/src/dv_toolkit/__init__.py
|
|
6
|
+
python/src/dv_toolkit.egg-info/PKG-INFO
|
|
7
|
+
python/src/dv_toolkit.egg-info/SOURCES.txt
|
|
8
|
+
python/src/dv_toolkit.egg-info/dependency_links.txt
|
|
9
|
+
python/src/dv_toolkit.egg-info/not-zip-safe
|
|
10
|
+
python/src/dv_toolkit.egg-info/requires.txt
|
|
11
|
+
python/src/dv_toolkit.egg-info/top_level.txt
|
|
12
|
+
python/src/dv_toolkit/plot/__init__.py
|
|
13
|
+
python/src/dv_toolkit/plot/player.py
|
|
14
|
+
python/src/dv_toolkit/plot/tools/__init__.py
|
|
15
|
+
python/src/dv_toolkit/plot/tools/func.py
|
|
16
|
+
python/src/dv_toolkit/plot/tools/go_player.py
|
|
17
|
+
python/src/dv_toolkit/plot/tools/mp_player.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import platform
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from setuptools import Extension, setup, find_packages
|
|
9
|
+
from setuptools.command.build_ext import build_ext
|
|
10
|
+
|
|
11
|
+
global extra_cmake_args
|
|
12
|
+
extra_cmake_args = [
|
|
13
|
+
"-DTOOLKIT_ENABLE_SAMPLES=OFF",
|
|
14
|
+
"-DTOOLKIT_ENABLE_PYTHON=ON"
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
# Convert distutils Windows platform specifiers to CMake -A arguments
|
|
18
|
+
PLAT_TO_CMAKE = {
|
|
19
|
+
"win32": "Win32",
|
|
20
|
+
"win-amd64": "x64",
|
|
21
|
+
"win-arm32": "ARM",
|
|
22
|
+
"win-arm64": "ARM64",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# A CMakeExtension needs a sourcedir instead of a file list.
|
|
27
|
+
# The name must be the _single_ output extension from the CMake build.
|
|
28
|
+
# If you need multiple extensions, see scikit-build.
|
|
29
|
+
class CMakeExtension(Extension):
|
|
30
|
+
|
|
31
|
+
def __init__(self, name: str, sourcedir: str = "") -> None:
|
|
32
|
+
super().__init__(name, sources=[])
|
|
33
|
+
self.sourcedir = os.fspath(Path(sourcedir).resolve())
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CMakeBuild(build_ext):
|
|
37
|
+
|
|
38
|
+
def build_extension(self, ext: CMakeExtension) -> None:
|
|
39
|
+
# Must be in this form due to bug in .resolve() only fixed in Python 3.10+
|
|
40
|
+
ext_fullpath = Path.cwd() / self.get_ext_fullpath(ext.name) # type: ignore[no-untyped-call]
|
|
41
|
+
extdir = ext_fullpath.parent.resolve()
|
|
42
|
+
|
|
43
|
+
# Using this requires trailing slash for auto-detection & inclusion of
|
|
44
|
+
# auxiliary "native" libs
|
|
45
|
+
|
|
46
|
+
debug = int(os.environ.get("DEBUG", 0)) if self.debug is None else self.debug
|
|
47
|
+
cfg = "Debug" if debug else "Release"
|
|
48
|
+
|
|
49
|
+
# CMake lets you override the generator - we need to check this.
|
|
50
|
+
# Can be set with Conda-Build, for example.
|
|
51
|
+
cmake_generator = os.environ.get("CMAKE_GENERATOR", "")
|
|
52
|
+
|
|
53
|
+
# Set Python_EXECUTABLE instead if you use PYBIND11_FINDPYTHON
|
|
54
|
+
# EXAMPLE_VERSION_INFO shows you how to pass a value into the C++ code
|
|
55
|
+
# from Python.
|
|
56
|
+
cmake_args = [
|
|
57
|
+
f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={extdir}{os.sep}dv_toolkit/lib",
|
|
58
|
+
f"-DPYTHON_EXECUTABLE={sys.executable}",
|
|
59
|
+
f"-DCMAKE_BUILD_TYPE={cfg}", # not used on MSVC, but no harm
|
|
60
|
+
]
|
|
61
|
+
build_args = []
|
|
62
|
+
# Adding CMake arguments set as environment variable
|
|
63
|
+
# (needed e.g. to build for ARM OSx on conda-forge)
|
|
64
|
+
if "CMAKE_ARGS" in os.environ:
|
|
65
|
+
cmake_args += [item for item in os.environ["CMAKE_ARGS"].split(" ") if item]
|
|
66
|
+
|
|
67
|
+
# In this example, we pass in the version to C++. You might not need to.
|
|
68
|
+
# cmake_args += [f"-DEXAMPLE_VERSION_INFO={self.distribution.get_version()}"] # type: ignore[attr-defined]
|
|
69
|
+
|
|
70
|
+
# Pass Python3 executable path to CMake after external args (eg. VCPKG toolchain files)
|
|
71
|
+
cmake_args += [f"-DPython3_EXECUTABLE={sys.executable}"]
|
|
72
|
+
|
|
73
|
+
# Add custom CMake arguments needed for build features
|
|
74
|
+
global extra_cmake_args
|
|
75
|
+
cmake_args += extra_cmake_args
|
|
76
|
+
|
|
77
|
+
if self.compiler.compiler_type != "msvc":
|
|
78
|
+
# Using Ninja-build since it a) is available as a wheel and b)
|
|
79
|
+
# multithreads automatically. MSVC would require all variables be
|
|
80
|
+
# exported for Ninja to pick it up, which is a little tricky to do.
|
|
81
|
+
# Users can override the generator with CMAKE_GENERATOR in CMake
|
|
82
|
+
# 3.15+.
|
|
83
|
+
if not cmake_generator or cmake_generator == "Ninja":
|
|
84
|
+
try:
|
|
85
|
+
import ninja
|
|
86
|
+
|
|
87
|
+
ninja_executable_path = Path(ninja.BIN_DIR) / "ninja"
|
|
88
|
+
cmake_args += [
|
|
89
|
+
"-GNinja",
|
|
90
|
+
f"-DCMAKE_MAKE_PROGRAM:FILEPATH={ninja_executable_path}",
|
|
91
|
+
]
|
|
92
|
+
except ImportError:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
else:
|
|
96
|
+
# Single config generators are handled "normally"
|
|
97
|
+
single_config = any(x in cmake_generator for x in {"NMake", "Ninja"})
|
|
98
|
+
|
|
99
|
+
# CMake allows an arch-in-generator style for backward compatibility
|
|
100
|
+
contains_arch = any(x in cmake_generator for x in {"ARM", "Win64"})
|
|
101
|
+
|
|
102
|
+
# Specify the arch if using MSVC generator, but only if it doesn't
|
|
103
|
+
# contain a backward-compatibility arch spec already in the
|
|
104
|
+
# generator name.
|
|
105
|
+
if not single_config and not contains_arch:
|
|
106
|
+
cmake_args += ["-A", PLAT_TO_CMAKE[self.plat_name]]
|
|
107
|
+
|
|
108
|
+
# Multi-config generators have a different way to specify configs
|
|
109
|
+
if not single_config:
|
|
110
|
+
cmake_args += [f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{cfg.upper()}={extdir}"]
|
|
111
|
+
build_args += ["--config", cfg]
|
|
112
|
+
|
|
113
|
+
if sys.platform.startswith("darwin"):
|
|
114
|
+
# Cross-compile support for macOS - respect ARCHFLAGS if set
|
|
115
|
+
archs = re.findall(r"-arch (\S+)", os.environ.get("ARCHFLAGS", ""))
|
|
116
|
+
if archs:
|
|
117
|
+
cmake_args += ["-DCMAKE_OSX_ARCHITECTURES={}".format(";".join(archs))]
|
|
118
|
+
|
|
119
|
+
# Set CMAKE_BUILD_PARALLEL_LEVEL to control the parallel build level
|
|
120
|
+
# across all generators.
|
|
121
|
+
if "CMAKE_BUILD_PARALLEL_LEVEL" not in os.environ:
|
|
122
|
+
# self.parallel is a Python 3 only way to set parallel jobs by hand
|
|
123
|
+
# using -j in the build_ext call, not supported by pip or PyPA-build.
|
|
124
|
+
if hasattr(self, "parallel") and self.parallel:
|
|
125
|
+
# CMake 3.12+ only.
|
|
126
|
+
build_args += [f"-j{self.parallel}"]
|
|
127
|
+
|
|
128
|
+
build_temp = Path(self.build_temp) / ext.name
|
|
129
|
+
if not build_temp.exists():
|
|
130
|
+
build_temp.mkdir(parents=True)
|
|
131
|
+
|
|
132
|
+
ret = subprocess.run(["cmake", ext.sourcedir, *cmake_args], cwd=build_temp, check=True)
|
|
133
|
+
print("Called cmake with: " + str(ret.args))
|
|
134
|
+
ret = subprocess.run(["cmake", "--build", ".", *build_args], cwd=build_temp, check=True)
|
|
135
|
+
print("Called cmake with: " + str(ret.args))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def parse_gplusplus_major_version(gpp_path):
|
|
139
|
+
try:
|
|
140
|
+
p = subprocess.Popen([gpp_path, "--version"], stdout=subprocess.PIPE)
|
|
141
|
+
out, _ = p.communicate()
|
|
142
|
+
if p.returncode == 0:
|
|
143
|
+
output_text = out.decode('UTF-8')
|
|
144
|
+
prog = re.compile(r"^(\S+)\s\(.*\)\s(\d+)\.\d+\.\d+.*")
|
|
145
|
+
result = prog.match(output_text)
|
|
146
|
+
if result is not None and result[1][0:3] == "g++":
|
|
147
|
+
return int(result[2])
|
|
148
|
+
except FileNotFoundError:
|
|
149
|
+
pass
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def verify_and_configure_compiler():
|
|
154
|
+
# This verify and check only relevant on linux
|
|
155
|
+
if platform.system() != 'Linux':
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
version = parse_gplusplus_major_version("g++")
|
|
159
|
+
if version is not None and version >= 10:
|
|
160
|
+
# default compiler seems fine, continue
|
|
161
|
+
return
|
|
162
|
+
try:
|
|
163
|
+
version = parse_gplusplus_major_version(os.environ["CXX"])
|
|
164
|
+
if version is not None and version >= 10:
|
|
165
|
+
# A preset compiler found, it seems suitable
|
|
166
|
+
return
|
|
167
|
+
except KeyError:
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
version = parse_gplusplus_major_version("g++-10")
|
|
171
|
+
if version is not None and version >= 10:
|
|
172
|
+
global extra_cmake_args
|
|
173
|
+
print("Detected installation of g++10, it will be used for compilation")
|
|
174
|
+
extra_cmake_args += ["-DCMAKE_C_COMPILER=gcc-10", "-DCMAKE_CXX_COMPILER=g++-10"]
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
# None of the above worked. Print a warning.
|
|
178
|
+
print("Compatible GCC compiler was not found.", file=sys.stderr)
|
|
179
|
+
print("Compiler compatibility verification failed, compilation will use system compiler.", file=sys.stderr)
|
|
180
|
+
print("This might fail!", file=sys.stderr)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# Initial setup
|
|
184
|
+
verify_and_configure_compiler()
|
|
185
|
+
|
|
186
|
+
# The information here can also be placed in setup.cfg - better separation of
|
|
187
|
+
# logic and declaration, and simpler if you include description/version in a file.
|
|
188
|
+
setup(
|
|
189
|
+
name="dv_toolkit",
|
|
190
|
+
version="0.2.0",
|
|
191
|
+
author="Kuga Maxx",
|
|
192
|
+
author_email="KugaMaxx@outlook.com",
|
|
193
|
+
description="a generic and simple toolkit for processing event-based data",
|
|
194
|
+
long_description="",
|
|
195
|
+
ext_modules=[CMakeExtension("_lib_toolkit")],
|
|
196
|
+
cmdclass={"build_ext": CMakeBuild},
|
|
197
|
+
zip_safe=False,
|
|
198
|
+
python_requires=">=3.8",
|
|
199
|
+
packages=find_packages(where='python/src'),
|
|
200
|
+
package_dir={'':'python/src'},
|
|
201
|
+
install_requires=[
|
|
202
|
+
'numpy>=1.24',
|
|
203
|
+
'dv_processing>=2.0',
|
|
204
|
+
'plotly>=5.17',
|
|
205
|
+
'matplotlib>=3.7'
|
|
206
|
+
]
|
|
207
|
+
)
|