evlab 0.1.0a1__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,27 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.10", "3.13"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ - name: Install
21
+ run: pip install -e .[dev]
22
+ - name: Lint
23
+ run: |
24
+ ruff check src tests
25
+ ruff format --check src tests
26
+ - name: Test
27
+ run: pytest -v
@@ -0,0 +1,36 @@
1
+ name: Release to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.12"
16
+ - name: Build distributions
17
+ run: |
18
+ pip install build
19
+ python -m build
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 # trusted publishing (OIDC)
31
+ steps:
32
+ - uses: actions/download-artifact@v4
33
+ with:
34
+ name: dist
35
+ path: dist/
36
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ .venv/
4
+ dist/
5
+ build/
6
+ .pytest_cache/
7
+ .ruff_cache/
evlab-0.1.0a1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jacky Li
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.
evlab-0.1.0a1/PKG-INFO ADDED
@@ -0,0 +1,115 @@
1
+ Metadata-Version: 2.4
2
+ Name: evlab
3
+ Version: 0.1.0a1
4
+ Summary: Inspect, clean, convert, and benchmark event-camera data from the command line
5
+ Project-URL: Homepage, https://github.com/JPL11/evlab
6
+ Project-URL: Issues, https://github.com/JPL11/evlab/issues
7
+ Author-email: Jacky Li <jackydli95@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: dvs,event-based-vision,event-camera,neuromorphic
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: click>=8.1
17
+ Requires-Dist: numpy>=1.24
18
+ Provides-Extra: aedat
19
+ Requires-Dist: aedat>=2.0; extra == 'aedat'
20
+ Provides-Extra: dev
21
+ Requires-Dist: matplotlib>=3.7; extra == 'dev'
22
+ Requires-Dist: pillow>=10.0; extra == 'dev'
23
+ Requires-Dist: pytest>=7.0; extra == 'dev'
24
+ Requires-Dist: rosbags>=0.10; extra == 'dev'
25
+ Requires-Dist: ruff>=0.4; extra == 'dev'
26
+ Provides-Extra: ros
27
+ Requires-Dist: rosbags>=0.10; extra == 'ros'
28
+ Provides-Extra: viz
29
+ Requires-Dist: matplotlib>=3.7; extra == 'viz'
30
+ Requires-Dist: pillow>=10.0; extra == 'viz'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # evlab
34
+
35
+ **Inspect, clean, convert, and benchmark event-camera data from the command line.**
36
+
37
+ Event-based vision has a preprocessing problem: every sensor and dataset ships a
38
+ different container (AEDAT, ROS bags, CSV, NPZ, proprietary `.dat`), every paper
39
+ wants a different representation (voxel grids, time surfaces, frames), and every
40
+ pipeline reimplements the same denoising filters. `evlab` is the small, boring
41
+ tool that handles that mess so you can get to the actual work.
42
+
43
+ ```bash
44
+ pip install evlab[viz]
45
+
46
+ evlab info recording.npz # what is this file?
47
+ evlab convert events.aedat4 events.npz # normalize the container
48
+ evlab convert drive.dat events.npz # Prophesee .dat works too
49
+ evlab convert flight.bag events.npz # ...and ROS bags (dvs_msgs)
50
+ evlab denoise events.npz clean.npz --filter baf --window 5000
51
+ evlab voxel clean.npz voxels.npy --bins 10
52
+ evlab visualize clean.npz preview.gif --mode gif
53
+ evlab benchmark events.npz clean.npz # what did the filter do?
54
+ ```
55
+
56
+ How good is a denoising filter, really? Generate a labeled stream and score it:
57
+
58
+ ```bash
59
+ evlab synth labeled.npz --signal-rate 20000 --noise-rate 8000
60
+ evlab denoise-bench labeled.npz --filter baf --window 3000
61
+ # precision : 99.2%
62
+ # recall : 37.8%
63
+ # f1 : 0.548
64
+ # noise removed : 99.2%
65
+ ```
66
+
67
+ ## Status
68
+
69
+ Early alpha. Works and is tested: the canonical representation; loading
70
+ NPZ/CSV/TXT, AEDAT4 (`[aedat]` extra), Prophesee legacy `.dat` (with
71
+ timestamp-wrap handling), and ROS1/ROS2 bags with `EventArray` topics
72
+ (`[ros]` extra); BAF/refractory denoising; voxel grids, time surfaces,
73
+ accumulate frames; synthetic labeled streams and precision/recall filter
74
+ scoring; and the eight CLI commands above. Planned next: Prophesee EVT3
75
+ `.raw`, dataset-aware loaders (via [Tonic]), streaming via [Faery], and
76
+ more filters (STCF, IE/YNoise) under `denoise-bench`.
77
+
78
+ [Tonic]: https://github.com/neuromorphs/tonic
79
+ [Faery]: https://github.com/aestream/faery
80
+
81
+ ## Python API
82
+
83
+ ```python
84
+ import evlab
85
+
86
+ data = evlab.load("recording.npz") # EventData: (x, y, t, p) + sensor size
87
+ print(data.event_rate)
88
+
89
+ from evlab.filters import background_activity_filter
90
+ clean = background_activity_filter(data, time_window_us=5000)
91
+
92
+ from evlab.representations import voxel_grid
93
+ grid = voxel_grid(clean, bins=10) # (10, H, W) float32, ready for torch
94
+ ```
95
+
96
+ ## Design notes
97
+
98
+ - One canonical in-memory format: a t-sorted structured numpy array
99
+ (`x: u2, y: u2, t: i8 (µs), p: i1`) plus sensor geometry. Every loader
100
+ normalizes into it; every filter/representation consumes it.
101
+ - Zero heavy dependencies in the core (`numpy` + `click`). Visualization,
102
+ AEDAT support, and dataset loaders are opt-in extras.
103
+ - Filters return copies; nothing mutates your data behind your back.
104
+
105
+ ## Development
106
+
107
+ ```bash
108
+ pip install -e .[dev]
109
+ pytest
110
+ ruff check src tests
111
+ ```
112
+
113
+ ## License
114
+
115
+ MIT
@@ -0,0 +1,83 @@
1
+ # evlab
2
+
3
+ **Inspect, clean, convert, and benchmark event-camera data from the command line.**
4
+
5
+ Event-based vision has a preprocessing problem: every sensor and dataset ships a
6
+ different container (AEDAT, ROS bags, CSV, NPZ, proprietary `.dat`), every paper
7
+ wants a different representation (voxel grids, time surfaces, frames), and every
8
+ pipeline reimplements the same denoising filters. `evlab` is the small, boring
9
+ tool that handles that mess so you can get to the actual work.
10
+
11
+ ```bash
12
+ pip install evlab[viz]
13
+
14
+ evlab info recording.npz # what is this file?
15
+ evlab convert events.aedat4 events.npz # normalize the container
16
+ evlab convert drive.dat events.npz # Prophesee .dat works too
17
+ evlab convert flight.bag events.npz # ...and ROS bags (dvs_msgs)
18
+ evlab denoise events.npz clean.npz --filter baf --window 5000
19
+ evlab voxel clean.npz voxels.npy --bins 10
20
+ evlab visualize clean.npz preview.gif --mode gif
21
+ evlab benchmark events.npz clean.npz # what did the filter do?
22
+ ```
23
+
24
+ How good is a denoising filter, really? Generate a labeled stream and score it:
25
+
26
+ ```bash
27
+ evlab synth labeled.npz --signal-rate 20000 --noise-rate 8000
28
+ evlab denoise-bench labeled.npz --filter baf --window 3000
29
+ # precision : 99.2%
30
+ # recall : 37.8%
31
+ # f1 : 0.548
32
+ # noise removed : 99.2%
33
+ ```
34
+
35
+ ## Status
36
+
37
+ Early alpha. Works and is tested: the canonical representation; loading
38
+ NPZ/CSV/TXT, AEDAT4 (`[aedat]` extra), Prophesee legacy `.dat` (with
39
+ timestamp-wrap handling), and ROS1/ROS2 bags with `EventArray` topics
40
+ (`[ros]` extra); BAF/refractory denoising; voxel grids, time surfaces,
41
+ accumulate frames; synthetic labeled streams and precision/recall filter
42
+ scoring; and the eight CLI commands above. Planned next: Prophesee EVT3
43
+ `.raw`, dataset-aware loaders (via [Tonic]), streaming via [Faery], and
44
+ more filters (STCF, IE/YNoise) under `denoise-bench`.
45
+
46
+ [Tonic]: https://github.com/neuromorphs/tonic
47
+ [Faery]: https://github.com/aestream/faery
48
+
49
+ ## Python API
50
+
51
+ ```python
52
+ import evlab
53
+
54
+ data = evlab.load("recording.npz") # EventData: (x, y, t, p) + sensor size
55
+ print(data.event_rate)
56
+
57
+ from evlab.filters import background_activity_filter
58
+ clean = background_activity_filter(data, time_window_us=5000)
59
+
60
+ from evlab.representations import voxel_grid
61
+ grid = voxel_grid(clean, bins=10) # (10, H, W) float32, ready for torch
62
+ ```
63
+
64
+ ## Design notes
65
+
66
+ - One canonical in-memory format: a t-sorted structured numpy array
67
+ (`x: u2, y: u2, t: i8 (µs), p: i1`) plus sensor geometry. Every loader
68
+ normalizes into it; every filter/representation consumes it.
69
+ - Zero heavy dependencies in the core (`numpy` + `click`). Visualization,
70
+ AEDAT support, and dataset loaders are opt-in extras.
71
+ - Filters return copies; nothing mutates your data behind your back.
72
+
73
+ ## Development
74
+
75
+ ```bash
76
+ pip install -e .[dev]
77
+ pytest
78
+ ruff check src tests
79
+ ```
80
+
81
+ ## License
82
+
83
+ MIT
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "evlab"
7
+ version = "0.1.0a1"
8
+ description = "Inspect, clean, convert, and benchmark event-camera data from the command line"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ authors = [{ name = "Jacky Li", email = "jackydli95@gmail.com" }]
12
+ requires-python = ">=3.10"
13
+ dependencies = [
14
+ "numpy>=1.24",
15
+ "click>=8.1",
16
+ ]
17
+ keywords = ["event-camera", "neuromorphic", "dvs", "event-based-vision"]
18
+ classifiers = [
19
+ "Development Status :: 3 - Alpha",
20
+ "Intended Audience :: Science/Research",
21
+ "Programming Language :: Python :: 3",
22
+ "Topic :: Scientific/Engineering :: Image Processing",
23
+ ]
24
+
25
+ [project.optional-dependencies]
26
+ viz = ["matplotlib>=3.7", "pillow>=10.0"]
27
+ aedat = ["aedat>=2.0"]
28
+ ros = ["rosbags>=0.10"]
29
+ dev = ["pytest>=7.0", "ruff>=0.4", "matplotlib>=3.7", "pillow>=10.0", "rosbags>=0.10"]
30
+
31
+ [project.scripts]
32
+ evlab = "evlab.cli:main"
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/JPL11/evlab"
36
+ Issues = "https://github.com/JPL11/evlab/issues"
37
+
38
+ [tool.ruff]
39
+ line-length = 100
40
+
41
+ [tool.ruff.lint]
42
+ select = ["E", "F", "I", "W", "UP"]
43
+
44
+ [tool.pytest.ini_options]
45
+ testpaths = ["tests"]
@@ -0,0 +1,6 @@
1
+ """evlab: inspect, clean, convert, and benchmark event-camera data."""
2
+
3
+ from .formats import EventData, from_arrays, load, save
4
+
5
+ __version__ = "0.1.0a1"
6
+ __all__ = ["EventData", "from_arrays", "load", "save", "__version__"]
@@ -0,0 +1,204 @@
1
+ """evlab command line interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ import click
8
+ import numpy as np
9
+
10
+ from . import formats, metrics
11
+ from .filters import FILTERS
12
+ from .representations import voxel_grid
13
+ from .synth import GENERATORS
14
+
15
+
16
+ @click.group()
17
+ @click.version_option(package_name="evlab")
18
+ def main():
19
+ """Inspect, clean, convert, and benchmark event-camera data."""
20
+
21
+
22
+ @main.command()
23
+ @click.argument("path", type=click.Path(exists=True, dir_okay=False))
24
+ @click.option("--json", "as_json", is_flag=True, help="Emit machine-readable JSON.")
25
+ def info(path, as_json):
26
+ """Summarize an event file (count, resolution, duration, rate)."""
27
+ data = formats.load(path)
28
+ stats = metrics.summary(data)
29
+ if as_json:
30
+ click.echo(json.dumps(stats, indent=2))
31
+ return
32
+ click.echo(f"{path}")
33
+ click.echo(f" events : {stats['num_events']:,}")
34
+ click.echo(f" resolution : {stats['width']} x {stats['height']}")
35
+ click.echo(f" duration : {stats['duration_s']:.3f} s")
36
+ click.echo(f" event rate : {stats['event_rate_hz']:,.0f} ev/s")
37
+ if stats["num_events"]:
38
+ click.echo(f" polarity ON : {stats['polarity_balance']:.1%}")
39
+ click.echo(
40
+ f" active px : {stats['active_pixels']:,}"
41
+ f" ({stats['active_pixel_fraction']:.1%} of sensor)"
42
+ )
43
+
44
+
45
+ @main.command()
46
+ @click.argument("src", type=click.Path(exists=True, dir_okay=False))
47
+ @click.argument("dst", type=click.Path(dir_okay=False))
48
+ def convert(src, dst):
49
+ """Convert between event file formats (npz, csv, txt, aedat4-in)."""
50
+ data = formats.load(src)
51
+ formats.save(data, dst)
52
+ click.echo(f"wrote {len(data.events):,} events -> {dst}")
53
+
54
+
55
+ @main.command()
56
+ @click.argument("src", type=click.Path(exists=True, dir_okay=False))
57
+ @click.argument("dst", type=click.Path(dir_okay=False))
58
+ @click.option(
59
+ "--filter",
60
+ "filter_name",
61
+ type=click.Choice(sorted(FILTERS)),
62
+ default="baf",
63
+ show_default=True,
64
+ help="Denoising filter to apply.",
65
+ )
66
+ @click.option(
67
+ "--window",
68
+ type=int,
69
+ default=5000,
70
+ show_default=True,
71
+ help="Time window in microseconds (baf) / refractory period (refractory).",
72
+ )
73
+ def denoise(src, dst, filter_name, window):
74
+ """Denoise an event stream and write the result."""
75
+ data = formats.load(src)
76
+ if filter_name == "baf":
77
+ out = FILTERS[filter_name](data, time_window_us=window)
78
+ else:
79
+ out = FILTERS[filter_name](data, refractory_us=window)
80
+ formats.save(out, dst)
81
+ kept = metrics.retention(data, out)
82
+ click.echo(f"kept {len(out.events):,}/{len(data.events):,} events ({kept:.1%}) -> {dst}")
83
+
84
+
85
+ @main.command()
86
+ @click.argument("src", type=click.Path(exists=True, dir_okay=False))
87
+ @click.argument("dst", type=click.Path(dir_okay=False))
88
+ @click.option("--bins", type=int, default=10, show_default=True, help="Temporal bins.")
89
+ def voxel(src, dst, bins):
90
+ """Build a (bins, H, W) voxel grid and save it as .npy."""
91
+ data = formats.load(src)
92
+ grid = voxel_grid(data, bins=bins)
93
+ np.save(dst, grid)
94
+ click.echo(f"voxel grid {grid.shape} (sum={grid.sum():+.1f}) -> {dst}")
95
+
96
+
97
+ @main.command()
98
+ @click.argument("src", type=click.Path(exists=True, dir_okay=False))
99
+ @click.argument("dst", type=click.Path(dir_okay=False))
100
+ @click.option(
101
+ "--mode",
102
+ type=click.Choice(["accumulate", "time-surface", "gif"]),
103
+ default="accumulate",
104
+ show_default=True,
105
+ )
106
+ @click.option(
107
+ "--window", type=float, default=33.0, show_default=True, help="Frame window in ms (gif mode)."
108
+ )
109
+ @click.option(
110
+ "--tau", type=float, default=30.0, show_default=True, help="Time-surface decay in ms."
111
+ )
112
+ def visualize(src, dst, mode, window, tau):
113
+ """Render events to a PNG (accumulate/time-surface) or GIF."""
114
+ from . import viz
115
+
116
+ data = formats.load(src)
117
+ if mode == "gif":
118
+ n = viz.render_gif(data, dst, window_ms=window)
119
+ click.echo(f"wrote {n} frames -> {dst}")
120
+ else:
121
+ viz.render_frame(data, mode, dst, tau_us=tau * 1000)
122
+ click.echo(f"wrote {mode} image -> {dst}")
123
+
124
+
125
+ @main.command()
126
+ @click.argument("dst", type=click.Path(dir_okay=False))
127
+ @click.option(
128
+ "--pattern", type=click.Choice(sorted(GENERATORS)), default="moving-bar", show_default=True
129
+ )
130
+ @click.option("--resolution", default="240x240", show_default=True, help="WIDTHxHEIGHT.")
131
+ @click.option("--duration", type=float, default=1.0, show_default=True, help="Seconds.")
132
+ @click.option("--signal-rate", type=float, default=20000, show_default=True, help="Signal ev/s.")
133
+ @click.option("--noise-rate", type=float, default=5000, show_default=True, help="Noise ev/s.")
134
+ @click.option("--seed", type=int, default=0, show_default=True)
135
+ def synth(dst, pattern, resolution, duration, signal_rate, noise_rate, seed):
136
+ """Generate a synthetic stream with ground-truth signal/noise labels."""
137
+ width, _, height = resolution.partition("x")
138
+ data = GENERATORS[pattern](
139
+ width=int(width),
140
+ height=int(height),
141
+ duration_us=int(duration * 1e6),
142
+ signal_rate_hz=signal_rate,
143
+ noise_rate_hz=noise_rate,
144
+ seed=seed,
145
+ )
146
+ formats.save(data, dst)
147
+ n_sig = int(data.meta["signal"].sum())
148
+ click.echo(f"wrote {len(data.events):,} events ({n_sig:,} signal) -> {dst}")
149
+
150
+
151
+ @main.command("denoise-bench")
152
+ @click.argument("src", type=click.Path(exists=True, dir_okay=False))
153
+ @click.option(
154
+ "--filter", "filter_name", type=click.Choice(sorted(FILTERS)), default="baf", show_default=True
155
+ )
156
+ @click.option(
157
+ "--window",
158
+ type=int,
159
+ default=5000,
160
+ show_default=True,
161
+ help="Time window (baf) / refractory period, microseconds.",
162
+ )
163
+ @click.option("--json", "as_json", is_flag=True, help="Emit machine-readable JSON.")
164
+ def denoise_bench(src, filter_name, window, as_json):
165
+ """Score a denoising filter against a labeled stream (see `evlab synth`)."""
166
+ from .filters import MASKS
167
+
168
+ data = formats.load(src)
169
+ if filter_name == "baf":
170
+ mask = MASKS[filter_name](data, time_window_us=window)
171
+ else:
172
+ mask = MASKS[filter_name](data, refractory_us=window)
173
+ score = metrics.denoise_score(data, mask)
174
+ if as_json:
175
+ click.echo(json.dumps(score, indent=2))
176
+ return
177
+ click.echo(f"{filter_name} (window={window} us) on {src}")
178
+ click.echo(f" precision : {score['precision']:.1%}")
179
+ click.echo(f" recall : {score['recall']:.1%}")
180
+ click.echo(f" f1 : {score['f1']:.3f}")
181
+ click.echo(f" noise removed : {score['noise_removed']:.1%}")
182
+
183
+
184
+ @main.command()
185
+ @click.argument("reference", type=click.Path(exists=True, dir_okay=False))
186
+ @click.argument("other", type=click.Path(exists=True, dir_okay=False))
187
+ @click.option("--json", "as_json", is_flag=True, help="Emit machine-readable JSON.")
188
+ def benchmark(reference, other, as_json):
189
+ """Compare two event streams (e.g. raw vs. denoised)."""
190
+ ref = formats.load(reference)
191
+ oth = formats.load(other)
192
+ result = metrics.compare(ref, oth)
193
+ if as_json:
194
+ click.echo(json.dumps(result, indent=2))
195
+ return
196
+ click.echo(f"retention : {result['retention']:.1%}")
197
+ click.echo(f"event rate (ref) : {result['event_rate_ref_hz']:,.0f} ev/s")
198
+ click.echo(f"event rate (new) : {result['event_rate_other_hz']:,.0f} ev/s")
199
+ click.echo(f"structure (ref) : {result['structure_ref']:.3f}")
200
+ click.echo(f"structure (new) : {result['structure_other']:.3f}")
201
+
202
+
203
+ if __name__ == "__main__":
204
+ main()
@@ -0,0 +1,109 @@
1
+ """Event stream denoising filters.
2
+
3
+ Each filter has a mask function (returns the boolean keep-mask, used by
4
+ `evlab denoise-bench` to score against ground truth) and a public wrapper
5
+ that applies it. Returned EventData shares no mutable state with the input;
6
+ per-event metadata (like the ``signal`` labels) is subset alongside events.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import numpy as np
12
+
13
+ from .formats import EventData
14
+
15
+ # Sentinel for "never fired": far in the past, but small enough that
16
+ # `t - sentinel` cannot overflow int64 for microsecond timestamps.
17
+ _NEVER = np.int64(np.iinfo(np.int64).min // 4)
18
+
19
+
20
+ def _apply_mask(data: EventData, keep: np.ndarray) -> EventData:
21
+ meta = {}
22
+ for key, value in data.meta.items():
23
+ arr = np.asarray(value) if not np.isscalar(value) else None
24
+ if arr is not None and arr.shape[:1] == (len(data.events),):
25
+ meta[key] = arr[keep].copy()
26
+ else:
27
+ meta[key] = value
28
+ return EventData(data.events[keep].copy(), data.width, data.height, meta)
29
+
30
+
31
+ def background_activity_mask(
32
+ data: EventData, time_window_us: int = 5000, neighborhood: int = 1
33
+ ) -> np.ndarray:
34
+ """Keep-mask for the nearest-neighbor / background-activity filter.
35
+
36
+ An event survives if at least one other event occurred within
37
+ ``time_window_us`` in its ``(2*neighborhood+1)^2 - 1`` spatial
38
+ neighborhood. This is the classic background-activity filter used by
39
+ DVS pipelines.
40
+ """
41
+ ev = data.events
42
+ keep = np.zeros(len(ev), dtype=bool)
43
+ if len(ev) == 0:
44
+ return keep
45
+
46
+ # last_seen[y, x] = timestamp of the most recent event at that pixel
47
+ last_seen = np.full(
48
+ (data.height + 2 * neighborhood, data.width + 2 * neighborhood), _NEVER, dtype=np.int64
49
+ )
50
+
51
+ xs = ev["x"].astype(np.int64) + neighborhood
52
+ ys = ev["y"].astype(np.int64) + neighborhood
53
+ ts = ev["t"]
54
+
55
+ n = neighborhood
56
+ for i in range(len(ev)):
57
+ x, y, t = xs[i], ys[i], ts[i]
58
+ window = last_seen[y - n : y + n + 1, x - n : x + n + 1]
59
+ # Exclude the center pixel: a pixel refiring alone is still noise.
60
+ center = window[n, n]
61
+ window[n, n] = _NEVER
62
+ keep[i] = bool((t - window <= time_window_us).any())
63
+ window[n, n] = center
64
+ last_seen[y, x] = t
65
+ return keep
66
+
67
+
68
+ def background_activity_filter(
69
+ data: EventData, time_window_us: int = 5000, neighborhood: int = 1
70
+ ) -> EventData:
71
+ """Apply the background-activity filter (see `background_activity_mask`)."""
72
+ return _apply_mask(data, background_activity_mask(data, time_window_us, neighborhood))
73
+
74
+
75
+ def refractory_mask(data: EventData, refractory_us: int = 1000) -> np.ndarray:
76
+ """Keep-mask dropping events within ``refractory_us`` of the previous
77
+ event at the same pixel (hot-pixel / oscillation suppression)."""
78
+ ev = data.events
79
+ keep = np.zeros(len(ev), dtype=bool)
80
+ if len(ev) == 0:
81
+ return keep
82
+
83
+ last_seen = np.full((data.height, data.width), _NEVER, dtype=np.int64)
84
+ xs = ev["x"].astype(np.intp)
85
+ ys = ev["y"].astype(np.intp)
86
+ ts = ev["t"]
87
+
88
+ for i in range(len(ev)):
89
+ x, y, t = xs[i], ys[i], ts[i]
90
+ keep[i] = (t - last_seen[y, x]) > refractory_us
91
+ if keep[i]:
92
+ last_seen[y, x] = t
93
+ return keep
94
+
95
+
96
+ def refractory_filter(data: EventData, refractory_us: int = 1000) -> EventData:
97
+ """Apply the refractory filter (see `refractory_mask`)."""
98
+ return _apply_mask(data, refractory_mask(data, refractory_us))
99
+
100
+
101
+ FILTERS = {
102
+ "baf": background_activity_filter,
103
+ "refractory": refractory_filter,
104
+ }
105
+
106
+ MASKS = {
107
+ "baf": background_activity_mask,
108
+ "refractory": refractory_mask,
109
+ }