eventify-dvs 0.1.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.
- eventify_dvs-0.1.0/.github/workflows/ci.yml +25 -0
- eventify_dvs-0.1.0/.github/workflows/publish.yml +38 -0
- eventify_dvs-0.1.0/.gitignore +66 -0
- eventify_dvs-0.1.0/LICENSE +21 -0
- eventify_dvs-0.1.0/PKG-INFO +150 -0
- eventify_dvs-0.1.0/README.md +137 -0
- eventify_dvs-0.1.0/pyproject.toml +28 -0
- eventify_dvs-0.1.0/scripts/render_dvs_gesture.py +210 -0
- eventify_dvs-0.1.0/src/eventify/__init__.py +15 -0
- eventify_dvs-0.1.0/src/eventify/_fast.py +39 -0
- eventify_dvs-0.1.0/src/eventify/cli.py +279 -0
- eventify_dvs-0.1.0/src/eventify/dvs.py +232 -0
- eventify_dvs-0.1.0/tests/__init__.py +0 -0
- eventify_dvs-0.1.0/tests/test_cli.py +154 -0
- eventify_dvs-0.1.0/tests/test_fast_path.py +100 -0
- eventify_dvs-0.1.0/tests/test_frame_to_event_tuples.py +228 -0
- eventify_dvs-0.1.0/tests/test_interpolate_frames.py +99 -0
- eventify_dvs-0.1.0/tests/test_video_to_event_stream.py +124 -0
- eventify_dvs-0.1.0/tests/test_write_hdf5.py +85 -0
- eventify_dvs-0.1.0/uv.lock +455 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- uses: astral-sh/setup-uv@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python-version }}
|
|
22
|
+
|
|
23
|
+
- run: uv sync --group dev
|
|
24
|
+
|
|
25
|
+
- run: uv run pytest tests/ -v
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*.*.*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
|
|
14
|
+
- uses: astral-sh/setup-uv@v5
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.12"
|
|
17
|
+
|
|
18
|
+
- run: uv build
|
|
19
|
+
|
|
20
|
+
- uses: actions/upload-artifact@v4
|
|
21
|
+
with:
|
|
22
|
+
name: dist
|
|
23
|
+
path: dist/
|
|
24
|
+
|
|
25
|
+
publish:
|
|
26
|
+
needs: build
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
environment: pypi
|
|
29
|
+
permissions:
|
|
30
|
+
id-token: write
|
|
31
|
+
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/download-artifact@v4
|
|
34
|
+
with:
|
|
35
|
+
name: dist
|
|
36
|
+
path: dist/
|
|
37
|
+
|
|
38
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
*.egg
|
|
8
|
+
*.egg-info/
|
|
9
|
+
.eggs/
|
|
10
|
+
build/
|
|
11
|
+
dist/
|
|
12
|
+
wheels/
|
|
13
|
+
pip-wheel-metadata/
|
|
14
|
+
share/python-wheels/
|
|
15
|
+
MANIFEST
|
|
16
|
+
|
|
17
|
+
# Virtual environments
|
|
18
|
+
.venv/
|
|
19
|
+
venv/
|
|
20
|
+
env/
|
|
21
|
+
ENV/
|
|
22
|
+
|
|
23
|
+
# uv
|
|
24
|
+
.python-version
|
|
25
|
+
|
|
26
|
+
# Testing / coverage
|
|
27
|
+
.pytest_cache/
|
|
28
|
+
.coverage
|
|
29
|
+
.coverage.*
|
|
30
|
+
htmlcov/
|
|
31
|
+
.tox/
|
|
32
|
+
.nox/
|
|
33
|
+
coverage.xml
|
|
34
|
+
*.cover
|
|
35
|
+
.hypothesis/
|
|
36
|
+
|
|
37
|
+
# Type checkers
|
|
38
|
+
.mypy_cache/
|
|
39
|
+
.pyre/
|
|
40
|
+
.pytype/
|
|
41
|
+
.ruff_cache/
|
|
42
|
+
|
|
43
|
+
# IDE
|
|
44
|
+
.idea/
|
|
45
|
+
.vscode/
|
|
46
|
+
*.swp
|
|
47
|
+
*.swo
|
|
48
|
+
.DS_Store
|
|
49
|
+
|
|
50
|
+
# Claude
|
|
51
|
+
.claude/
|
|
52
|
+
|
|
53
|
+
# Project outputs / scratch
|
|
54
|
+
*.mp4
|
|
55
|
+
*.avi
|
|
56
|
+
*.mov
|
|
57
|
+
*.h5
|
|
58
|
+
*.hdf5
|
|
59
|
+
*.aedat
|
|
60
|
+
*.npz
|
|
61
|
+
*.png
|
|
62
|
+
scratch/
|
|
63
|
+
tmp/
|
|
64
|
+
|
|
65
|
+
# Datasets (local caches, not committed)
|
|
66
|
+
dvs_gesture_data/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Arpan Pandey
|
|
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,150 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: eventify-dvs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Convert video files or webcam feeds into simulated event-camera (DVS) data.
|
|
5
|
+
Author: Arpan Pandey
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: h5py>=3.8
|
|
10
|
+
Requires-Dist: numpy>=1.24
|
|
11
|
+
Requires-Dist: opencv-python>=4.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# eventify-dvs
|
|
15
|
+
|
|
16
|
+
Convert video files or webcam feeds into simulated event-camera (DVS) data using log-intensity differencing. A clean-room, dependency-light reimplementation of the core idea behind v2e/ESIM — no CUDA, no PyTorch, no pretrained models. Just NumPy, OpenCV, and h5py.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install eventify-dvs
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The CLI entry point is `eventify`. With `uv`:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uv add eventify-dvs
|
|
28
|
+
uv run eventify --help
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## CLI
|
|
32
|
+
|
|
33
|
+
Three subcommands under the `eventify` entry point.
|
|
34
|
+
|
|
35
|
+
### `eventify webcam` — live event preview
|
|
36
|
+
|
|
37
|
+
Opens the default camera and shows the event stream in an OpenCV window. Press `q` to quit. On macOS, grant camera permission to your terminal app first (System Settings → Privacy & Security → Camera).
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Defaults — 1280x720 @ 60 FPS
|
|
41
|
+
eventify webcam
|
|
42
|
+
|
|
43
|
+
# Snappy, high-sensitivity preview
|
|
44
|
+
eventify webcam --threshold 0.03 --accum-ms 40
|
|
45
|
+
|
|
46
|
+
# Different camera
|
|
47
|
+
eventify webcam --device 1
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
| Flag | Default | Purpose |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| `--device` | `0` | Webcam device index |
|
|
53
|
+
| `--threshold` | `0.05` | Log-intensity event threshold |
|
|
54
|
+
| `--width` | `1280` | Requested capture width |
|
|
55
|
+
| `--height` | `720` | Requested capture height |
|
|
56
|
+
| `--fps` | `60` | Requested capture FPS |
|
|
57
|
+
| `--accum-ms` | `80` | Event accumulator half-life in ms |
|
|
58
|
+
| `--max-events` | `8` | Saturation ceiling for accumulated events per pixel |
|
|
59
|
+
|
|
60
|
+
### `eventify convert` — video file → event-visualized video
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
eventify convert input.mp4 events.mp4
|
|
64
|
+
eventify convert input.mp4 events.mp4 --threshold 0.03
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
| Flag | Default | Purpose |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| `input` | — | Path to source video |
|
|
70
|
+
| `output` | — | Path to write the rendered MP4 |
|
|
71
|
+
| `--threshold` | `0.05` | Log-intensity event threshold |
|
|
72
|
+
|
|
73
|
+
### `eventify export` — video → DVS-Gesture HDF5
|
|
74
|
+
|
|
75
|
+
Emits binary-polarity DVS events to an HDF5 file compatible with the DVS128 Gesture dataset layout (Tonic / SpikingJelly loaders).
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
eventify export input.mp4 events.h5
|
|
79
|
+
eventify export input.mp4 events.h5 --sensor-size 128,128 --interp 4
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
| Flag | Default | Purpose |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `input` | — | Path to source video |
|
|
85
|
+
| `output` | — | Path to write the HDF5 events file |
|
|
86
|
+
| `--threshold` | `0.05` | Log-intensity event threshold |
|
|
87
|
+
| `--sensor-size` | source resolution | Override as `W,H` |
|
|
88
|
+
| `--interp` | `0` | Interpolated sub-frames between real frames |
|
|
89
|
+
|
|
90
|
+
## Library
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
import numpy as np
|
|
94
|
+
from eventify import (
|
|
95
|
+
frame_to_event_tuples,
|
|
96
|
+
video_to_event_stream,
|
|
97
|
+
interpolate_frames,
|
|
98
|
+
write_hdf5,
|
|
99
|
+
EVENT_DTYPE,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Per-frame-pair event tuples
|
|
103
|
+
events = frame_to_event_tuples(prev, curr, prev_t_us=0, curr_t_us=1000)
|
|
104
|
+
# events["x"], events["y"], events["t"], events["p"] — p ∈ {0, 1}
|
|
105
|
+
|
|
106
|
+
# Full stream from a video file
|
|
107
|
+
chunks = list(video_to_event_stream("video.mp4", sensor_size=(128, 128), interp=4))
|
|
108
|
+
all_events = np.concatenate(chunks)
|
|
109
|
+
write_hdf5("out.h5", all_events, sensor_shape=(128, 128))
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## API reference
|
|
113
|
+
|
|
114
|
+
- **`frame_to_event_tuples(prev, curr, prev_t_us, curr_t_us, c_thresh=0.05, eps=1.0, sensor_size=None)`** —
|
|
115
|
+
returns a NumPy structured array with dtype `EVENT_DTYPE` and fields
|
|
116
|
+
`(x: i2, y: i2, t: i8, p: i1)`. Polarity is binary (0 = OFF, 1 = ON).
|
|
117
|
+
A pixel whose log-delta spans `K` thresholds emits `K` events, uniformly
|
|
118
|
+
staggered across the interval.
|
|
119
|
+
|
|
120
|
+
- **`video_to_event_stream(source, c_thresh=0.05, sensor_size=None, interp=0, capture_settings=None)`** —
|
|
121
|
+
generator yielding one structured event array per (sub-)frame-pair.
|
|
122
|
+
Timestamps are monotonic microseconds.
|
|
123
|
+
|
|
124
|
+
- **`interpolate_frames(prev, curr, n_intermediate)`** —
|
|
125
|
+
linearly interpolates `n_intermediate` frames between two endpoints,
|
|
126
|
+
returning a list of `n_intermediate + 2` frames.
|
|
127
|
+
|
|
128
|
+
- **`write_hdf5(path, events, sensor_shape)`** — writes events in the
|
|
129
|
+
DVS-Gesture reprocessed layout:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
/events
|
|
133
|
+
.attrs["sensor_shape"] (height, width)
|
|
134
|
+
/xs i2
|
|
135
|
+
/ys i2
|
|
136
|
+
/ts i8 microseconds
|
|
137
|
+
/ps i1 ∈ {0, 1}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
- **`EVENT_DTYPE`** — NumPy structured dtype `[("x", "<i2"), ("y", "<i2"), ("t", "<i8"), ("p", "<i1")]`.
|
|
141
|
+
|
|
142
|
+
## Tests
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
uv run pytest
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# eventify-dvs
|
|
2
|
+
|
|
3
|
+
Convert video files or webcam feeds into simulated event-camera (DVS) data using log-intensity differencing. A clean-room, dependency-light reimplementation of the core idea behind v2e/ESIM — no CUDA, no PyTorch, no pretrained models. Just NumPy, OpenCV, and h5py.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install eventify-dvs
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The CLI entry point is `eventify`. With `uv`:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uv add eventify-dvs
|
|
15
|
+
uv run eventify --help
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## CLI
|
|
19
|
+
|
|
20
|
+
Three subcommands under the `eventify` entry point.
|
|
21
|
+
|
|
22
|
+
### `eventify webcam` — live event preview
|
|
23
|
+
|
|
24
|
+
Opens the default camera and shows the event stream in an OpenCV window. Press `q` to quit. On macOS, grant camera permission to your terminal app first (System Settings → Privacy & Security → Camera).
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Defaults — 1280x720 @ 60 FPS
|
|
28
|
+
eventify webcam
|
|
29
|
+
|
|
30
|
+
# Snappy, high-sensitivity preview
|
|
31
|
+
eventify webcam --threshold 0.03 --accum-ms 40
|
|
32
|
+
|
|
33
|
+
# Different camera
|
|
34
|
+
eventify webcam --device 1
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
| Flag | Default | Purpose |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `--device` | `0` | Webcam device index |
|
|
40
|
+
| `--threshold` | `0.05` | Log-intensity event threshold |
|
|
41
|
+
| `--width` | `1280` | Requested capture width |
|
|
42
|
+
| `--height` | `720` | Requested capture height |
|
|
43
|
+
| `--fps` | `60` | Requested capture FPS |
|
|
44
|
+
| `--accum-ms` | `80` | Event accumulator half-life in ms |
|
|
45
|
+
| `--max-events` | `8` | Saturation ceiling for accumulated events per pixel |
|
|
46
|
+
|
|
47
|
+
### `eventify convert` — video file → event-visualized video
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
eventify convert input.mp4 events.mp4
|
|
51
|
+
eventify convert input.mp4 events.mp4 --threshold 0.03
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
| Flag | Default | Purpose |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `input` | — | Path to source video |
|
|
57
|
+
| `output` | — | Path to write the rendered MP4 |
|
|
58
|
+
| `--threshold` | `0.05` | Log-intensity event threshold |
|
|
59
|
+
|
|
60
|
+
### `eventify export` — video → DVS-Gesture HDF5
|
|
61
|
+
|
|
62
|
+
Emits binary-polarity DVS events to an HDF5 file compatible with the DVS128 Gesture dataset layout (Tonic / SpikingJelly loaders).
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
eventify export input.mp4 events.h5
|
|
66
|
+
eventify export input.mp4 events.h5 --sensor-size 128,128 --interp 4
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
| Flag | Default | Purpose |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `input` | — | Path to source video |
|
|
72
|
+
| `output` | — | Path to write the HDF5 events file |
|
|
73
|
+
| `--threshold` | `0.05` | Log-intensity event threshold |
|
|
74
|
+
| `--sensor-size` | source resolution | Override as `W,H` |
|
|
75
|
+
| `--interp` | `0` | Interpolated sub-frames between real frames |
|
|
76
|
+
|
|
77
|
+
## Library
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
import numpy as np
|
|
81
|
+
from eventify import (
|
|
82
|
+
frame_to_event_tuples,
|
|
83
|
+
video_to_event_stream,
|
|
84
|
+
interpolate_frames,
|
|
85
|
+
write_hdf5,
|
|
86
|
+
EVENT_DTYPE,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Per-frame-pair event tuples
|
|
90
|
+
events = frame_to_event_tuples(prev, curr, prev_t_us=0, curr_t_us=1000)
|
|
91
|
+
# events["x"], events["y"], events["t"], events["p"] — p ∈ {0, 1}
|
|
92
|
+
|
|
93
|
+
# Full stream from a video file
|
|
94
|
+
chunks = list(video_to_event_stream("video.mp4", sensor_size=(128, 128), interp=4))
|
|
95
|
+
all_events = np.concatenate(chunks)
|
|
96
|
+
write_hdf5("out.h5", all_events, sensor_shape=(128, 128))
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## API reference
|
|
100
|
+
|
|
101
|
+
- **`frame_to_event_tuples(prev, curr, prev_t_us, curr_t_us, c_thresh=0.05, eps=1.0, sensor_size=None)`** —
|
|
102
|
+
returns a NumPy structured array with dtype `EVENT_DTYPE` and fields
|
|
103
|
+
`(x: i2, y: i2, t: i8, p: i1)`. Polarity is binary (0 = OFF, 1 = ON).
|
|
104
|
+
A pixel whose log-delta spans `K` thresholds emits `K` events, uniformly
|
|
105
|
+
staggered across the interval.
|
|
106
|
+
|
|
107
|
+
- **`video_to_event_stream(source, c_thresh=0.05, sensor_size=None, interp=0, capture_settings=None)`** —
|
|
108
|
+
generator yielding one structured event array per (sub-)frame-pair.
|
|
109
|
+
Timestamps are monotonic microseconds.
|
|
110
|
+
|
|
111
|
+
- **`interpolate_frames(prev, curr, n_intermediate)`** —
|
|
112
|
+
linearly interpolates `n_intermediate` frames between two endpoints,
|
|
113
|
+
returning a list of `n_intermediate + 2` frames.
|
|
114
|
+
|
|
115
|
+
- **`write_hdf5(path, events, sensor_shape)`** — writes events in the
|
|
116
|
+
DVS-Gesture reprocessed layout:
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
/events
|
|
120
|
+
.attrs["sensor_shape"] (height, width)
|
|
121
|
+
/xs i2
|
|
122
|
+
/ys i2
|
|
123
|
+
/ts i8 microseconds
|
|
124
|
+
/ps i1 ∈ {0, 1}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
- **`EVENT_DTYPE`** — NumPy structured dtype `[("x", "<i2"), ("y", "<i2"), ("t", "<i8"), ("p", "<i1")]`.
|
|
128
|
+
|
|
129
|
+
## Tests
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
uv run pytest
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "eventify-dvs"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Convert video files or webcam feeds into simulated event-camera (DVS) data."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
authors = [{ name = "Arpan Pandey" }]
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"h5py>=3.8",
|
|
11
|
+
"numpy>=1.24",
|
|
12
|
+
"opencv-python>=4.8",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
eventify = "eventify.cli:main"
|
|
17
|
+
|
|
18
|
+
[dependency-groups]
|
|
19
|
+
dev = [
|
|
20
|
+
"pytest>=7.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["hatchling"]
|
|
25
|
+
build-backend = "hatchling.build"
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.wheel]
|
|
28
|
+
packages = ["src/eventify"]
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Render a real DVS128 Gesture .aedat recording through eventify's palette.
|
|
2
|
+
|
|
3
|
+
Parses raw (x, y, t_µs, polarity) events from an AEDAT 3.1 file (the
|
|
4
|
+
format the DVS128 Gesture dataset ships in), bins them into short time
|
|
5
|
+
windows, and renders each window through an exponentially-fading
|
|
6
|
+
accumulator so that motion trails persist on screen — the look of the
|
|
7
|
+
reference DVS visualization.
|
|
8
|
+
|
|
9
|
+
Uses SpikingJelly's ``load_aedat_v3`` framing logic (MIT-licensed,
|
|
10
|
+
credited inline) but with a vectorized event-decode loop for a ~100×
|
|
11
|
+
speedup on 60 MB files.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
uv run python scripts/render_dvs_gesture.py \\
|
|
15
|
+
dvs_gesture_data/DvsGesture/user01_fluorescent.aedat \\
|
|
16
|
+
user01_events.mp4 \\
|
|
17
|
+
[--start-us 80000000 --duration-s 10 --bin-ms 20 --fade-ms 100 --upscale 4]
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import struct
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
import cv2
|
|
27
|
+
import numpy as np
|
|
28
|
+
|
|
29
|
+
# --- AEDAT 3.1 parser -------------------------------------------------------
|
|
30
|
+
#
|
|
31
|
+
# Header framing adapted from SpikingJelly's load_aedat_v3
|
|
32
|
+
# (https://github.com/fangwei123456/spikingjelly, MIT). The inner event
|
|
33
|
+
# decode loop was rewritten with NumPy so we don't iterate in Python over
|
|
34
|
+
# millions of events.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_aedat_v3(path: Path) -> dict:
|
|
38
|
+
"""Parse an AEDAT 3.1 file into a dict of NumPy arrays.
|
|
39
|
+
|
|
40
|
+
Returns ``{"t": µs int64, "x": int16, "y": int16, "p": uint8}``.
|
|
41
|
+
"""
|
|
42
|
+
ts_list: list = []
|
|
43
|
+
xs_list: list = []
|
|
44
|
+
ys_list: list = []
|
|
45
|
+
ps_list: list = []
|
|
46
|
+
|
|
47
|
+
with open(path, "rb") as f:
|
|
48
|
+
# Skip ASCII header — lines start with '#', ends with #!END-HEADER.
|
|
49
|
+
line = f.readline()
|
|
50
|
+
while line.startswith(b"#"):
|
|
51
|
+
if line == b"#!END-HEADER\r\n":
|
|
52
|
+
break
|
|
53
|
+
line = f.readline()
|
|
54
|
+
|
|
55
|
+
while True:
|
|
56
|
+
header = f.read(28)
|
|
57
|
+
if not header or len(header) < 28:
|
|
58
|
+
break
|
|
59
|
+
|
|
60
|
+
e_type = struct.unpack("H", header[0:2])[0]
|
|
61
|
+
e_size = struct.unpack("I", header[4:8])[0]
|
|
62
|
+
e_tsoverflow = struct.unpack("I", header[12:16])[0]
|
|
63
|
+
e_capacity = struct.unpack("I", header[16:20])[0]
|
|
64
|
+
|
|
65
|
+
data_length = e_capacity * e_size
|
|
66
|
+
data = f.read(data_length)
|
|
67
|
+
|
|
68
|
+
if e_type != 1:
|
|
69
|
+
# Only polarity events (type 1) are relevant here.
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
# AEDAT 3.1 polarity event: 8 bytes = 4-byte aer_data + 4-byte timestamp.
|
|
73
|
+
# Vectorized decode: interpret the whole packet as a uint32 pair per event.
|
|
74
|
+
packet = np.frombuffer(data, dtype=np.uint32).reshape(-1, 2)
|
|
75
|
+
aer = packet[:, 0]
|
|
76
|
+
ts_low = packet[:, 1].astype(np.int64)
|
|
77
|
+
|
|
78
|
+
xs = ((aer >> 17) & 0x00007FFF).astype(np.int16)
|
|
79
|
+
ys = ((aer >> 2) & 0x00007FFF).astype(np.int16)
|
|
80
|
+
ps = ((aer >> 1) & 0x00000001).astype(np.uint8)
|
|
81
|
+
ts = ts_low | (np.int64(e_tsoverflow) << 31)
|
|
82
|
+
|
|
83
|
+
ts_list.append(ts)
|
|
84
|
+
xs_list.append(xs)
|
|
85
|
+
ys_list.append(ys)
|
|
86
|
+
ps_list.append(ps)
|
|
87
|
+
|
|
88
|
+
if not ts_list:
|
|
89
|
+
return {"t": np.zeros(0, np.int64), "x": np.zeros(0, np.int16),
|
|
90
|
+
"y": np.zeros(0, np.int16), "p": np.zeros(0, np.uint8)}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
"t": np.concatenate(ts_list),
|
|
94
|
+
"x": np.concatenate(xs_list),
|
|
95
|
+
"y": np.concatenate(ys_list),
|
|
96
|
+
"p": np.concatenate(ps_list),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# --- Renderer ---------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# DVS palette (BGR). Matches eventify.core.
|
|
104
|
+
_ON = np.array([180, 70, 0], dtype=np.float32)
|
|
105
|
+
_OFF = np.array([0, 170, 220], dtype=np.float32)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def render_accum(accum: np.ndarray, max_events: float) -> np.ndarray:
|
|
109
|
+
"""Signed accumulator → BGR uint8 image (black bg, blue ON, amber OFF)."""
|
|
110
|
+
intensity = np.clip(np.abs(accum) / max_events, 0.0, 1.0)[..., None]
|
|
111
|
+
positive = accum >= 0
|
|
112
|
+
target = np.empty(accum.shape + (3,), dtype=np.float32)
|
|
113
|
+
target[..., 0] = np.where(positive, _ON[0], _OFF[0])
|
|
114
|
+
target[..., 1] = np.where(positive, _ON[1], _OFF[1])
|
|
115
|
+
target[..., 2] = np.where(positive, _ON[2], _OFF[2])
|
|
116
|
+
img = intensity * target
|
|
117
|
+
return np.clip(img, 0, 255).astype(np.uint8)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def main() -> int:
|
|
121
|
+
p = argparse.ArgumentParser(description=__doc__)
|
|
122
|
+
p.add_argument("aedat", help="Path to .aedat file")
|
|
123
|
+
p.add_argument("out", help="Output MP4 path")
|
|
124
|
+
p.add_argument("--start-us", type=int, default=None, help="Start timestamp in µs (default: first event)")
|
|
125
|
+
p.add_argument("--duration-s", type=float, default=10.0, help="Duration to render (default: 10 s)")
|
|
126
|
+
p.add_argument("--bin-ms", type=float, default=20.0, help="Time window per output frame in ms (default: 20 → 50 fps)")
|
|
127
|
+
p.add_argument("--fade-ms", type=float, default=100.0, help="Accumulator half-life in ms (default: 100)")
|
|
128
|
+
p.add_argument("--upscale", type=int, default=4, help="Upscale factor for the 128×128 sensor grid (default: 4)")
|
|
129
|
+
p.add_argument("--max-events", type=float, default=6.0, help="Saturation ceiling per pixel (default: 6)")
|
|
130
|
+
args = p.parse_args()
|
|
131
|
+
|
|
132
|
+
aedat_path = Path(args.aedat)
|
|
133
|
+
print(f"parsing {aedat_path} ({aedat_path.stat().st_size / 1e6:.1f} MB) ...")
|
|
134
|
+
ev = load_aedat_v3(aedat_path)
|
|
135
|
+
n = len(ev["t"])
|
|
136
|
+
if n == 0:
|
|
137
|
+
raise SystemExit("no events found in file")
|
|
138
|
+
|
|
139
|
+
t0_file = int(ev["t"].min())
|
|
140
|
+
t1_file = int(ev["t"].max())
|
|
141
|
+
span_s = (t1_file - t0_file) / 1e6
|
|
142
|
+
print(f" {n:,} events over {span_s:.1f} s (t0={t0_file}, t1={t1_file})")
|
|
143
|
+
|
|
144
|
+
start_us = args.start_us if args.start_us is not None else t0_file
|
|
145
|
+
end_us = start_us + int(args.duration_s * 1e6)
|
|
146
|
+
|
|
147
|
+
mask = (ev["t"] >= start_us) & (ev["t"] < end_us)
|
|
148
|
+
t = ev["t"][mask]
|
|
149
|
+
x = ev["x"][mask]
|
|
150
|
+
y = ev["y"][mask]
|
|
151
|
+
pol = ev["p"][mask]
|
|
152
|
+
if len(t) == 0:
|
|
153
|
+
raise SystemExit(f"no events in [{start_us}, {end_us})")
|
|
154
|
+
print(f" {len(t):,} events in selected {args.duration_s:.1f} s window")
|
|
155
|
+
|
|
156
|
+
# Sensor is 128×128 for DVS128, but be defensive against coords outside that.
|
|
157
|
+
H = int(max(128, y.max() + 1))
|
|
158
|
+
W = int(max(128, x.max() + 1))
|
|
159
|
+
print(f" sensor grid: {H}×{W}")
|
|
160
|
+
|
|
161
|
+
bin_us = int(args.bin_ms * 1000)
|
|
162
|
+
n_bins = (end_us - start_us + bin_us - 1) // bin_us
|
|
163
|
+
fps = 1000.0 / args.bin_ms
|
|
164
|
+
up = args.upscale
|
|
165
|
+
h_out, w_out = H * up, W * up
|
|
166
|
+
|
|
167
|
+
writer = cv2.VideoWriter(args.out, cv2.VideoWriter_fourcc(*"mp4v"), fps, (w_out, h_out), True)
|
|
168
|
+
if not writer.isOpened():
|
|
169
|
+
raise SystemExit(f"could not open writer: {args.out}")
|
|
170
|
+
|
|
171
|
+
accum = np.zeros((H, W), dtype=np.float32)
|
|
172
|
+
# Precompute per-bin event slice boundaries (events are already time-sorted).
|
|
173
|
+
bin_ids = ((t - start_us) // bin_us).astype(np.int64)
|
|
174
|
+
slice_starts = np.searchsorted(bin_ids, np.arange(n_bins))
|
|
175
|
+
slice_ends = np.searchsorted(bin_ids, np.arange(1, n_bins + 1))
|
|
176
|
+
|
|
177
|
+
half_life_bins = args.fade_ms / args.bin_ms
|
|
178
|
+
decay = 0.5 ** (1.0 / max(half_life_bins, 1e-6))
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
for b in range(n_bins):
|
|
182
|
+
accum *= decay
|
|
183
|
+
s, e = slice_starts[b], slice_ends[b]
|
|
184
|
+
if e > s:
|
|
185
|
+
bx = x[s:e]
|
|
186
|
+
by = y[s:e]
|
|
187
|
+
bp = pol[s:e]
|
|
188
|
+
# Signed splat via bincount, faster than np.add.at.
|
|
189
|
+
signed = np.where(bp == 1, 1.0, -1.0).astype(np.float32)
|
|
190
|
+
flat_idx = by.astype(np.int64) * W + bx.astype(np.int64)
|
|
191
|
+
delta = np.bincount(flat_idx, weights=signed, minlength=H * W)
|
|
192
|
+
accum += delta.reshape(H, W).astype(np.float32)
|
|
193
|
+
|
|
194
|
+
frame = render_accum(accum, max_events=args.max_events)
|
|
195
|
+
big = cv2.resize(frame, (w_out, h_out), interpolation=cv2.INTER_LINEAR)
|
|
196
|
+
# Time overlay
|
|
197
|
+
t_sec = (start_us + b * bin_us - t0_file) / 1e6
|
|
198
|
+
cv2.putText(big, f"t={t_sec:5.2f}s bin {b + 1}/{n_bins}",
|
|
199
|
+
(10, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1, cv2.LINE_AA)
|
|
200
|
+
writer.write(big)
|
|
201
|
+
finally:
|
|
202
|
+
writer.release()
|
|
203
|
+
|
|
204
|
+
print(f"wrote {n_bins} frames to {args.out} at {fps:.1f} fps "
|
|
205
|
+
f"(bin={args.bin_ms}ms, fade={args.fade_ms}ms)")
|
|
206
|
+
return 0
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
if __name__ == "__main__":
|
|
210
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from eventify.dvs import (
|
|
2
|
+
EVENT_DTYPE,
|
|
3
|
+
frame_to_event_tuples,
|
|
4
|
+
interpolate_frames,
|
|
5
|
+
video_to_event_stream,
|
|
6
|
+
write_hdf5,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"EVENT_DTYPE",
|
|
11
|
+
"frame_to_event_tuples",
|
|
12
|
+
"interpolate_frames",
|
|
13
|
+
"video_to_event_stream",
|
|
14
|
+
"write_hdf5",
|
|
15
|
+
]
|