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.
@@ -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
+ ]