API Reference

The Python API lives in the top-level eventcv package. Every operation exists both as a method on a core type (EventStream, EventFrame, EventReader, EventPointSet, Camera) and as an OpenCV-style free function (listed under Functional API below); the free functions are generated from the methods, so the two forms stay in sync.

Loading & saving

eventcv.load(path: str, *, sensor_size: tuple[int, int] | None = None, time_unit: str | None = None, order: str = 'txyp', topic: str | None = None, max_events: int | None = None, offset: float | None = None, offset_s: float | None = None, offset_ms: float | None = None, offset_us: float | None = None, offset_ns: float | None = None, keys: dict[str, str] | None = None) → EventStream[source]

Load events from any supported file, detected by its extension.

Supported today: .npz (N-ImageNet), .txt/.csv (e.g. EV-IMO t x y p), .bag (ROS dvs_msgs/EventArray), .hdf5/.h5, .aedat (AEDAT 2.0, jAER/DAVIS), .aedat4 (AEDAT 4.0, iniVation DV), .dat (Prophesee CD events), and Prophesee EVT2 and EVT3 .raw recordings.

A .raw file’s encoding comes from its header, and its sensor size from % geometry when present — some recorders omit it, and the size is then derived from the events themselves, which is a tight bound on what fired rather than the physical sensor. Pass sensor_size to override.

An .aedat file’s geometry comes from the chip named in its header (the DAVIS family; DVS128 and older chips use a different address layout and are refused rather than misread); an .aedat4 file’s comes from the stream description DV writes into its header. In both, the APS frames and IMU samples are not events and so are not returned here — read them with read_frames() and read_imu().

sensor_size and time_unit are auto-detected when omitted and only act as overrides: rosbags carry both in the message; HDF5/text infer the time unit from the timestamps (a fractional text value means seconds) and the resolution from the coordinate range. Passing sensor_size for HDF5 also skips that scan. time_unit is seconds/milliseconds/microseconds/nanoseconds (or auto); order (txyp/xytp) applies to headerless text. topic selects the rosbag topic (default /davis/left/events). offset is an absolute timestamp in the file’s own time base — the same base as stream.numpy()[:, 2]: events before it are skipped, and max_events then caps how many are kept after it — together they read a window, handy for previewing a slice of a very large file. For a recording whose timestamps are epoch-based, pass the epoch time (e.g. offset=1_587_540_271_650); <= 0 reads from the start. The bare offset is milliseconds; offset_s, offset_ms, offset_us, and offset_ns say which unit you mean, and passing two of them raises.

The x/y/t/p columns are found automatically, whatever they’re named or nested: an HDF5 file’s datasets are searched recursively and matched by synonym (x, x_coordinates, u, …; t/timestamp/timestamps; polarity/pol; etc.), and a single compound/structured dataset with those fields is read too — so the common ROS dvs_msgs layout (events/{x_coordinates, y_coordinates, timestamps, polarities}) just works. A .csv/.txt file with a header row is mapped by its column names (comma or whitespace separated). When detection can’t identify the columns it raises, listing what the file contains. Pass keys to name them explicitly: keys={"x": …, "y": …, "t": …, "p": …} — for HDF5 each value is a dataset path (or dataset/field to pick a compound field); for text a header name or 0-based column index. keys overrides auto-detection (and order).

eventcv.from_numpy(events, *, sensor_size: tuple[int, int] | None = None, time_unit: str | None = None, order: str = 'xytp') → EventStream[source]

Build an EventStream from an in-memory (N, 4) NumPy array.

The constructor mirror of EventStream.numpy(): order defaults to xytp (the column layout numpy() emits, with timestamps in microseconds), so ecv.from_numpy(stream.numpy(), time_unit="us") round-trips a stream. Pass order="txyp" for arrays in the common t x y p dataset layout. Any integer or float dtype is accepted; polarity is positive when its value is greater than zero (both 0/1 and -1/1 conventions work).

sensor_size and time_unit are auto-detected when omitted, exactly like load(): the sensor is the smallest grid holding every event, and the time unit is inferred from the timestamp span (fractional values mean seconds; the inference assumes a recording of at least ~1 s, so pass time_unit explicitly for short arrays). Events outside an explicit sensor_size are dropped.

Example:

events = np.array([[0, 0, 100, 1], [1, 2, 250, 0]])   # x y t p
stream = ecv.from_numpy(events, time_unit="us")
stream.count().numpy()
eventcv.open(path: str, *, dt_ms: float | None = None, dt_s: float | None = None, dt_us: float | None = None, dt_ns: float | None = None, max_events: int | None = None, offset: float | None = None, offset_s: float | None = None, offset_ms: float | None = None, offset_us: float | None = None, offset_ns: float | None = None, repr: str | None = None, sensor_size: tuple[int, int] | None = None, time_unit: str | None = None, order: str = 'txyp', topic: str | None = None, hot_pixel_filter: bool = False, hot_pixel_std: float = 3.0, keys: dict[str, str] | None = None, downsample: int | None = None) → EventReader[source]

Open a file for lazy slicing without loading it whole.

Where load() is OpenCV’s imread (read the entire stream eagerly), open is its VideoCapture: it returns an EventReader that points at the original file and fetches a slice on demand. HDF5 binary-searches the on-disk timestamps; Prophesee EVT3 .raw uses a sparse byte/time index. In both cases a slice of a multi-gigabyte recording is decoded on demand and the file is never fully materialised. Other formats are loaded once and sliced in memory.

Pass dt_ms to treat the recording as a sequence of fixed-duration frames: the reader reports n_slices and reader.slice(n) returns the n-th frame (reader[n] works too). Frame n is measured from the recording start, so you never deal with absolute timestamps (which may be epoch-based). max_events is the event-count twin: open(path, max_events=10_000) makes each slice exactly 10 000 consecutive events (the last one may be shorter), which keeps the event rate per frame constant instead of the duration. The two are mutually exclusive — pass one or the other, not both. Without either, slice by explicit time/count window instead.

Any timescale works. Every time argument comes in four units — dt_s, dt_ms, dt_us, dt_ns — and they mean the same thing, so dt_us=500 and dt_ms=0.5 open the same reader. Passing two units for the same quantity raises rather than picking one. The same applies to offset_* here, to t0_*/t1_* on EventReader.slice(), and to step_*/span_* on EventReader.windows(). Timestamps are stored in microseconds, so a _ns value is rounded to the nearest microsecond, and a duration below half a microsecond raises rather than being silently rounded up:

ecv.open("rec.h5", dt_us=500)                  # 500 us frames
ecv.open("rec.h5", dt_s=0.03).slice(t0_us=1e6, t1_us=2e6)

offset is an absolute timestamp in the file’s own time base — exactly what slice(t0_ms=…) takes — that moves the framing origin: slice(0), windows(), and n_slices all begin at that time, and events before it fall outside every indexed frame. It is clamped up to t_min, so an offset before the recording is a no-op, and one past the end yields zero frames. For an epoch-based recording pass the epoch time (e.g. offset=1_587_540_271_650). It composes with either dt_ms or max_events. The bare offset is milliseconds; offset_s/offset_ms/offset_us/offset_ns are explicit.

sensor_size and time_unit are auto-detected when omitted (see load()); order/topic match load(). For a multi-GB HDF5, pass sensor_size to skip the one-time coordinate scan resolution inference needs.

The x/y/t/p columns are found automatically whatever their names or nesting (HDF5 synonym/recursive/compound detection; text header rows), exactly as in load(). Pass keys={"x": …, "y": …, "t": …, "p": …} to name them explicitly when a file’s layout can’t be guessed (HDF5 dataset paths or dataset/field; text header names or 0-based indices).

Pass hot_pixel_filter=True to strip stuck pixels consistently across the whole recording. open scans the file once up front, flags every pixel whose event count exceeds mean + hot_pixel_std·std (over the active pixels), and drops those pixels from every slice it returns — the filter runs before any per-slice op (efast / repr / …). This is deliberately global: calling EventStream.hot_pixel_filter() per slice re-thresholds each window, so genuinely hot pixels survive at long dt_ms. The pre-scan is kept lightweight — it reads only the event coordinates (skipping the timestamp/polarity columns) and, for a large recording, samples windows spread evenly across it rather than every event, so the mask is a robust estimate rather than an exact tally. hot_pixel_std (default 3.0) tunes how aggressive it is, matching EventStream.hot_pixel_filter().

Pass repr (a representation name — "count", "voxel", "tsurf", "flow" for optical flow, …) to make the reader a PyTorch-style map dataset: len(reader) == n_slices, reader[i] returns the dense [C, H, W] array for frame i, and reader.batch(indices) stacks a [B, C, H, W] batch — so a DataLoader can collate the reader directly. Use reader.with_repr(name, **opts) to set per-representation options (e.g. bins=5, or window=5 for "flow"). Without repr, reader[i] stays a raw EventStream; to still batch those through a DataLoader pass collate_fn=eventcv.collate (each batch is a list[EventStream], since sparse streams can’t stack into a tensor).

Phase 5 algorithms apply per slice: reader.efast() / reader.harris_corners(thr) return a new reader whose every slice is the corner sub-stream, composing with slice/windows/with_repr. To render an algorithm as a video, map it over windows() and hand the frames to export_png() (then assemble with ffmpeg).

When repr is set, slice/slice_count/windows also apply it: each returns the rendered EventFrame (the rich object — .numpy(), .view(), .save()), while reader[i]/batch stay dense NumPy arrays for the DataLoader path. So open(path, repr="mcts").slice(0) equals open(path).slice(0).mcts(). Without repr, slice(i) returns the raw EventStream, so name the representation on the stream: data.slice(1000).view("flow") (or .slice(1000).optical_flow().view()).

Time spans follow the slice window. An unset window_ms / tau_ms / max_window_ms — on repr=, on with_repr, or on a representation built from a slice (reader.slice(0).mcts()) — defaults to the duration of the window the events came from, so the representation covers exactly the events it was handed: open(path, dt_ms=50, repr="mcts") encodes the full 50 ms instead of the 30 ms default (which would silently discard the oldest 20 ms of every slice). slice(t0, t1) and windows(span_ms=…) follow their own durations the same way. Set a span explicitly to override; in max_events mode there is no fixed duration, so the 30 ms defaults stand.

Example:

r = eventcv.open("rec.hdf5", dt_ms=30)   # resolution + time unit auto-detected
r.n_slices                               # how many 30 ms frames
r.slice(50).mcts().view()                # the 50th 30 ms frame
for frame in r.windows():                # walk every frame (step defaults to dt_ms)
    voxel = frame.voxel()

# As a training dataset:
ds = eventcv.open("rec.hdf5", dt_ms=30, repr="count")
loader = torch.utils.data.DataLoader(ds, batch_size=32, shuffle=True)

# Corner-detection video (one PNG per frame):
corners = eventcv.open("rec.hdf5", dt_ms=30).efast()
eventcv.export_png((w.count() for w in corners.windows()), "corners/", colormap="turbo")
# Optical-flow video:
eventcv.export_png((w.optical_flow() for w in r.windows()), "flow/")

downsample=k (polarity only) bins k``×``k sensor pixels into one cell while counting; with repr="polarity" the RAW decoder feeds the histogram directly, without building the window’s events.

eventcv.save(obj, path: str, *, topic: str | None = None, format: str | None = None, compression: int | bool | None = None) → None[source]

Save an EventStream, EventFrame, EventReader, or FEAST model to path.

The mirror of load(): the format is chosen by the file extension. Streams go to .npz/.txt/.h5/.bag (npz, HDF5, and rosbag round-trip exactly; txt stores t x y p and recovers the sensor size/unit on load via inference or options). Frames (computed representations) go to .npz or .h5, preserving shape, dtype, kind, and channel_names. A trained FEAST model is written to .npz (its learned features, thresholds, and parameters) and reloaded with load_feast(). topic names the rosbag connection. Equivalent to obj.save(path) for streams/frames.

E2VID export. A .zip target (or format="e2vid" on a .txt) writes the layout E2VID reads — a width height header, then t x y p per event with t in float seconds — so a recording goes straight into event-to-video reconstruction without a conversion script:

ecv.save(ecv.open("rec.h5"), "events.zip")
# python run_reconstruction.py --input_file events.zip --fixed_duration -T 33

Passing an EventReader converts it window by window, so a recording far larger than memory re-exports without being loaded, and any deferred ops on the reader (crop, mask, hot_pixel_filter, …) apply to what is written. eventcv does not read the E2VID layout back — keep an .npz/.h5 if you need the recording itself.

HDF5 compression. Events are stored as four columns — x/y (uint16), t (int64) and p (uint8) — which is 13 raw bytes an event, so a large recording is enormous stored plainly. .h5 is therefore chunked, byte-shuffled and gzipped by default, which typically cuts it by more than half at some cost in write speed. Pass compression=False for the fastest possible write, or 1–9 to pick a level:

ecv.save(events, "rec.h5")                     # gzip 1, the default
ecv.save(events, "rec.h5", compression=False)  # fastest, largest
eventcv.load_frame(path: str) → EventFrame[source]

Load an EventFrame written by save() (.npz or .h5).

Restores the representation’s shape, dtype, kind, and channel_names.

eventcv.load_feast(path: str) → FEAST[source]

Load a FEAST model written by save() (.npz).

Restores the learned features, selection thresholds, and every constructor parameter, so the reloaded model reproduces FEAST.transform() exactly and can resume training with FEAST.fit().

eventcv.export_png(frames, out_dir: str, *, colormap: str = 'viridis', normalize: bool = True, prefix: str = 'frame_', start: int = 0, digits: int = 5)[source]

Write one or many EventFrame s to numbered .png files — the “frame sequence → video frames” export.

frames is a single EventFrame or any iterable of them (e.g. a generator over a reader’s windows), so a whole recording renders lazily without materialising every frame at once:

r = eventcv.open("rec.hdf5", dt_ms=30)
eventcv.export_png((w.count() for w in r.windows()), "out/", colormap="turbo")

Each frame is colormapped through the same path as frame.save("x.png") (colormap: viridis/turbo/grayscale/redblue; normalize auto-contrasts). Files are named {prefix}{index:0{digits}d}.png counting from start. Returns the list of written paths (assemble a video with, e.g., ffmpeg -i out/frame_%05d.png out.mp4).

eventcv.collate(batch)[source]

collate_fn for torch.utils.data.DataLoader over an EventReader.

A reader opened with a representation (open(repr=…)) yields dense [C, H, W] arrays that torch’s default collate stacks into a [B, C, H, W] tensor with no help — so you only need this for a reader opened without repr, whose reader[i] is a raw EventStream. Those are variable-length and sparse, so they can’t stack into a tensor; this returns the batch as a plain list of streams instead (dense/array batches still defer to torch’s default collate). Pass it explicitly:

loader = torch.utils.data.DataLoader(reader, batch_size=32, collate_fn=eventcv.collate)
for batch in loader:        # batch is a list[EventStream]
    batch[0].view()

Devices

Where representations and the simulator run. The CPU is the default and the reference; see Representations for what the GPU changes numerically and when it is worth using.

eventcv.set_device(name)

Sets the device every representation uses when a call does not name one.

The session starts on “cpu” — the CPU implementations are the reference, and nothing changes numerically unless this is called. EVENTCV_DEVICE sets the same thing for a whole process without touching any code.

eventcv.get_device()

The device representations run on when a call does not name one.

eventcv.gpu_available()

Whether device=”gpu” can actually run here — the build carries the kernels and an adapter was found. The first call opens the adapter, which takes a few milliseconds.

Viewing

eventcv.play(source=None, **kwargs) → None[source]

Open the interactive player, optionally with an EventReader, EventStream, or path already loaded.

The offline twin of EventCamera.show(): with no repr it shows the raw event stream, polarity dots fading by age, so watching a recording takes no more setup than watching a camera:

ecv.play()                              # choose a recording in the GUI
ecv.play("rec.h5", dt_ms=5)
ecv.play("rec.h5", repr="count", dt_ms=20)
ecv.play(events, speed=0.25)

At speed=1 recording time matches wall time. dt_ms is the accumulation width while refresh_hz (default 60) controls how often that window advances, so overlapping windows are cheap. fps remains accepted for compatibility but is deprecated. The call blocks on the main thread until the window is closed.

eventcv.save_video(source, path: str, *, fps: float = 30.0, repr: str | None = None, dt_ms: float | None = None, decay_ms: float | None = None, colormap: str = 'viridis', clim: float | None = None, max_frames: int | None = None) → int[source]

Render source as an animation, returning the number of frames written.

source is an EventReader or an EventStream; the container comes from path’s extension (.gif, .apng, .mp4 — the last needs ffmpeg on PATH).

With no repr this writes the raw stream: every event lights its pixel in its polarity colour and fades over decay_ms, which is the picture a camera’s own viewer shows and needs no choice of representation to be meaningful:

ecv.save_video(ecv.open("rec.h5"), "raw.mp4", dt_ms=5)
ecv.save_video(events, "raw.gif", dt_ms=5, decay_ms=20)

dt_ms is how much of the recording each rendered frame covers — the shutter, not the playback rate, which is fps. Together they set the speed: dt_ms=5, fps=30 plays six times slower than real time. Passing it here rather than at open() is deliberate; it is a property of the view, and a reader is often opened for something else entirely.

Pass repr="count" (or any representation name) to render that instead, with colormap and clim as in EventReader.save_video(). A reader opened with repr= uses it unless repr="raw" overrides.

See :doc:video for the raw view and the interactive player’s controls.

Simulation and reconstruction

eventcv.simulate(source, *, pos_thres: float = 0.2, neg_thres: float = 0.2, sigma_thres: float = 0.03, refractory_us: int = 100, cutoff_hz: float = 200.0, leak_rate_hz: float = 1.0, shot_noise_rate_hz: float = 10.0, seed: int = 0, upsample: str | None = None, max_events_per_pixel: float = 1.0, max_upsample: int | None = None, fps: float | None = None, scale: tuple[int, int] | None = None, max_frames: int | None = None, out: str | None = None, compression: int | bool | None = None, progress: bool = False, device: str | None = None, interpolate: str | None = None, interpolate_factor: int = 2)[source]

Simulate a DVS camera watching source, returning the events it would have produced.

source is a video file path (which carries its own frame rate) or an array of frames — [N, H, W] greyscale or [N, H, W, 3] RGB, integer 0..255 or float 0..1 — which needs an explicit fps. Writing .mp4 and reading video both go through ffmpeg, so a video source needs it on PATH.

The pixel model follows v2e (Hu et al., CVPRW 2021). Frames are linearised out of sRGB and mapped lin-log before differencing, so contrast is measured in light rather than in display-encoded values:

events = ecv.simulate("clip.mp4")                  # realistic defaults
events = ecv.simulate(frames, fps=1000)            # from an array
events = ecv.simulate("clip.mp4", sigma_thres=0.0, leak_rate_hz=0,
                      shot_noise_rate_hz=0)        # an ideal, noiseless sensor

Writing straight to a file. A realistic sensor emits a lot — a second of 1080p is on the order of a hundred million events, which is many gigabytes held in RAM before you can save any of it. out= writes each frame interval as it is produced and returns a SimulationResult (.path, .frames, .events; len() is the event count) instead of an EventStream, so memory stays flat at one interval:

result = ecv.simulate("clip.mp4", out="events.h5", progress=True)
reader = ecv.open(result.path, dt_ms=20)

.h5 streams; other formats are buffered and written once at the end. compression picks the HDF5 filter (gzip 1 by default, False for none), and progress=True prints frames and event counts to stderr as it runs.

Parameters mirror the sensor: pos_thres/neg_thres are the log-contrast thresholds, sigma_thres their per-pixel spread, cutoff_hz the photoreceptor bandwidth for a white pixel, leak_rate_hz spontaneous ON events, shot_noise_rate_hz the noise floor in dark pixels, and refractory_us the dead time after each event. Set the noise terms to 0 for a clean stream; the defaults are what a real camera does.

upsample subdivides each frame interval before simulating, which is what keeps timestamps accurate when a lot happens between two frames: "adaptive" (the default) subdivides until no pixel would emit more than max_events_per_pixel per sub-interval, "off" disables it, and an integer string is a fixed factor. Each sub-step is a full pass over every pixel and the busiest pixel sets the factor, so one hard edge can cost the maximum on an otherwise quiet frame — max_upsample (default 64) caps that, trading timestamp accuracy for time.

scale decodes video at a different resolution, which is both much cheaper than resizing afterwards and the most effective way to cut the event count — halving each side quarters the pixels and roughly quarters the output. max_frames stops early, and seed makes a run reproducible from its configuration (including across machines: the simulation runs on every core, but its noise is seeded so that the result does not depend on how many there are).

device="gpu" runs the pixel model as a compute shader instead, which is worth it on a large sensor: this is the heaviest loop in the library, and the one place the GPU wins clearly. The two backends are not interchangeable to the last event, and the difference is worth knowing before you rely on it:

  • With shot_noise_rate_hz=0 and leak_rate_hz=0 they produce identical events.

  • With sigma_thres on they produce the same events with the same polarities, and a couple of timestamps in ~25 000 land a microsecond apart — the crossing fraction is double precision on the CPU and shaders have no double precision.

  • With noise on they draw different random numbers by construction (a per-pixel kernel cannot replay a sequential generator), so the streams agree in rate, not event for event. A GPU run is still bit-reproducible from its seed, run to run and across devices.

So compare a GPU run against a stored CPU one only with the noise terms off.

interpolate inserts learned frames before simulating, which is the other half of the timestamp-accuracy problem. upsample subdivides the interval, but the levels it subdivides are a straight-line blend of the two frames — right when a pixel’s intensity ramps over the gap, wrong when an edge crosses it and the pixel steps. Point interpolate at a RIFE ONNX export (or anything shaped like one: a frame pair in, one image out, ideally with a timestep input) and interpolate_factor says how many intervals each source pair becomes:

events = ecv.simulate("clip.mp4", interpolate="rife_v4.onnx", interpolate_factor=4)

The interpolated frames are fed to the simulator as ordinary source frames at proportional timestamps, exactly as v2e uses Super-SloMo — the pixel model is untouched, and upsample still refines whatever is left. Frames are interpolated in luma, since that is all the simulator consumes. No weights ship with eventcv; export the model yourself.

eventcv.reconstruct(reader, model, path: str, *, fps: float = 30.0, colormap: str = 'grayscale', max_frames: int | None = None) → int[source]

Reconstruct an intensity video from events, returning the number of frames written.

reader must carry the representation the model expects, and model is an Model wrapping any ONNX graph that maps that representation to a single-channel image — E2VID and its relatives:

reader = ecv.open("recording.h5", dt_ms=33, repr="voxel", bins=5)
ecv.reconstruct(reader, ecv.Model("e2vid.onnx"), "out.mp4")

EventCV supplies the runner and the tensors, not the weights: there is no official E2VID ONNX export, so export one yourself with torch.onnx.export. A recurrent model must have its hidden state exposed as explicit inputs and outputs; a stateless export (--no-recurrent) works as-is.

The output format comes from path’s extension (.gif, .apng, .mp4), and the reconstruction is rendered at its natural scale rather than auto-contrasted, so brightness stays consistent across the sequence.

class eventcv.StatefulModel(model, state_map: dict[str, str])[source]

Bases: object

Drives a recurrent ONNX model, carrying its hidden state between calls.

E2VID and its relatives take (data, state_0, …) and return (image, new_state_0, …); what makes them recurrent is feeding those states back, which a plain Model call deliberately does not do. state_map says which output feeds which input:

model = ecv.StatefulModel(
    ecv.Model("e2vid_recurrent.onnx"),
    state_map={"new_state": "state"},
)
for i in range(len(reader)):
    image = model(reader[i])
model.reset()      # before an unrelated recording

State lives here rather than inside Model so that Model stays a pure function and resetting is something you ask for rather than something that happens invisibly. On the first call — and after reset() — each state input is seeded with zeros of its declared shape, with any free dimension taken as 1.

There is no official recurrent E2VID export; produce one with torch.onnx.export, exposing the ConvLSTM states as explicit inputs and outputs. A stateless export needs none of this and works with Model directly.

reset() → None[source]

Forgets the hidden state, so the next call starts a fresh sequence.

See :doc:simulation for the pixel model and :doc:reconstruction for the ONNX export requirements.

Models

class eventcv.Model(path)

Bases: object

Runs a trained ONNX network over a representation.

Deliberately a runner, not a model zoo: it takes any .onnx file and knows nothing about the architecture inside it. EventCV’s job is to produce the tensor — a voxel grid or time surface is already a dense f32 array in the layout ONNX wants — and to get out of the way.

Requires a build with the onnx feature; eventcv –version reports whether this one has it.

inputs

a list of {“name”, “shape”, “dtype”}, with -1 for any dimension the graph leaves free (usually the batch axis).

Type:

What the graph expects

outputs

What the graph produces, in the same form as inputs.

run_named(inputs)

Runs the model with every input bound by name, returning {name: array}.

The form __call__ cannot express. A recurrent graph takes its hidden state as extra inputs and returns the updated state as extra outputs; feeding those back is what StatefulModel does on top of this.

Requires a build with the onnx feature (the published wheels have it). See :doc:models for scope and usage.

E2VID export

A .zip target — or format="e2vid" on a .txt — writes the layout E2VID reads: a width height header line, then one whitespace-separated t x y p row per event with t in float seconds and p as 0/1, ascending in time. That removes the conversion script between an EventCV recording and event-to-video reconstruction.

ecv.save(stream, "events.zip")                       # from a stream
ecv.save(ecv.open("rec.h5"), "events.zip")           # or straight from a reader
# python run_reconstruction.py --input_file events.zip --fixed_duration -T 33

Passing an EventReader converts it window by window, so a recording far larger than memory re-exports without being loaded, and any deferred ops on the reader (crop, mask, hot_pixel_filter, …) apply to what gets written. The export is one-way — EventCV does not read the E2VID layout back, so keep an .npz/.h5 if you need the recording itself.

Region-of-interest masking

A mask is a plain (H, W) boolean NumPy array — True where events are kept — so it composes with NumPy’s own operators (|, &, ~) and can be built, drawn, or loaded. Pass one to mask(), to mask() (deferred onto every slice), or to ecv.stream(mask=…), where events outside it are dropped as they are decoded — so they never reach a record= file or the windows a loop reads.

import eventcv as ecv

aperture = ecv.circle_mask((640, 480), cx=320, cy=240, r=230)   # a circular sensor aperture
aperture &= ~ecv.rect_mask((640, 480), 0, 0, 64, 64)            # minus a hot corner
ecv.save_mask(aperture, "aperture.png")                         # reuse it next session

ecv.open("rec.h5", dt_ms=30).mask(aperture)                     # every slice, lazily
ecv.stream(dt_ms=50, mask=aperture, record="session.h5")        # live, before the recorder

sensor_size is (width, height) — the same order as everywhere else in eventcv — while the array you get back is (H, W), like any other NumPy image. Coordinates are continuous rather than pixel indices: a pixel is kept when its centre falls inside the shape, and geometry off the sensor is clamped. An 8-bit map works anywhere a boolean one does (any non-zero value keeps the pixel), so a mask binarised elsewhere is passed straight in. A mask that isn’t the size of the sensor raises, rather than silently dropping every event.

To draw one instead, draw_mask() (or draw_mask()) opens the viewer on a still frame, and draw_mask() draws over the live camera and applies the result. Drag to keep an area, shift+drag to drop one, e/r/f to switch between ellipse, rectangle, and freehand, a/c to select all or clear, z to undo; whatever stays bright is what the mask keeps. Enter accepts, Esc returns None.

eventcv.circle_mask(sensor_size: tuple[int, int], cx: float, cy: float, r: float)[source]

Build a circular ROI mask of radius r centred on (cx, cy).

Returns an (H, W) boolean NumPy array — True where events are kept — ready for EventStream.mask(), EventReader.mask(), or ecv.stream(mask=…). Note the two orders: sensor_size is (width, height) like everywhere else in eventcv, while the array that comes back is (H, W) like every other NumPy image.

Masks are plain arrays, so they compose with NumPy’s own operators — | unions, & intersects, ~ inverts:

aperture = ecv.circle_mask((640, 480), cx=320, cy=240, r=230)
aperture &= ~ecv.rect_mask((640, 480), 0, 0, 64, 64)   # minus a corner
ecv.load("rec.npz").mask(aperture)

Coordinates are continuous, not pixel indices: a pixel is kept when its centre falls inside the shape, and geometry reaching off the sensor is clamped. See also ellipse_mask(), rect_mask(), polygon_mask(), load_mask(), and — to draw one by hand — EventStream.draw_mask() / EventCamera.draw_mask().

eventcv.ellipse_mask(sensor_size: tuple[int, int], cx: float, cy: float, rx: float, ry: float)[source]

Build an elliptical ROI mask centred on (cx, cy) with semi-axes rx, ry.

The axis-independent form of circle_mask(); see it for the conventions.

eventcv.rect_mask(sensor_size: tuple[int, int], x0: float, y0: float, width: float, height: float)[source]

Build a rectangular ROI mask keeping the width``×``height box at (x0, y0).

Unlike EventStream.crop(), which shrinks the sensor grid, a mask keeps the coordinates (and the sensor size) unchanged and only drops events. See circle_mask() for the conventions.

eventcv.polygon_mask(sensor_size: tuple[int, int], points)[source]

Build an ROI mask keeping the interior of the closed polygon through points.

points is a sequence of (x, y) vertices; the last joins back to the first. Filled by the even-odd rule, so a self-intersecting outline leaves holes. See circle_mask() for the conventions.

eventcv.save_mask(mask, path: str) → None[source]

Write an ROI mask to an 8-bit greyscale .png — white keeps, black drops.

Lets an ROI drawn or computed once be reused across sessions (and checked in any image viewer). Read it back with load_mask().

eventcv.load_mask(path: str)[source]

Read an ROI mask from a .png as an (H, W) boolean array.

A pixel is kept where the image is non-black and not fully transparent, so a mask binarised in any other tool loads as-is — greyscale, palette, or colour, with or without alpha:

ecv.stream(dt_ms=50, mask=ecv.load_mask("aperture.png"))

The counterpart of save_mask().

Motion estimation

class eventcv.Tracker(*, connectivity=8, min_area=4, max_distance=20.0, max_missed=3)

Bases: object

Follows connected components across frames, keeping their identities.

Connected-component labels are assigned in scan order and renumber whenever anything moves, so they mean nothing across time. A tracker adds the memory.

reset()

Forgets every track. Ids continue from where they left off, so an old track and a new one can never be confused in a log.

tracks

The live tracks without advancing the tracker.

update(frame)

Segments frame, matches its blobs to the existing tracks, and returns the live tracks as a list of dicts: id, centroid, velocity, area, age, missed.

See :doc:motion for contrast maximisation and the tracker’s association rule.

Networking

class eventcv.UdpSender(target, *, timestamps=False, host_endian=True)

Bases: object

Sends events over UDP, wire-compatible with aestream and SPIF.

send(stream)

Sends every event, split across datagrams. Returns how many were sent.

class eventcv.UdpReceiver(address, sensor_size, *, timestamps=False, host_endian=True)

Bases: object

Receives events over UDP.

recv(dt_ms=30.0)

Collects whatever arrives within dt_ms. An empty stream means silence, not failure.

Core types

class eventcv.EventStream

Bases: object

atsurf(*, tau_ms=None, tau_s=None, tau_us=None, tau_ns=None, device=None)

Averaged time surface — the per-pixel mean of exp(-age/tau_ms) over all events (two polarity channels, float32). Brighter where activity recurs; see tsurf.

background_activity_filter(dt, *, neighbourhood='block')

Background-activity (nearest-neighbour) noise filter: keeps an event only if a neighbouring pixel fired within dt (raw timestamp units, e.g. microseconds).

neighbourhood=”block” (the default) counts all eight surrounding pixels; “cross” counts only the four edge-adjacent ones, which is the stricter test and retains measurably more signal at matched noise rejection on uncorrelated shot noise — it is what tonic and jAER use. The default is “block” because that is what this filter has always done.

concat(others)

Concatenates this stream with others (argument order; sensor = element-wise max).

contrast_maximise(*, model='translation', objective='variance', camera=None, blur_sigma=None, initial=None, initial_step=50.0, max_iterations=200, tolerance=0.001, time_reference='midpoint')

Recovers the camera motion that makes the warped events sharpest.

Returns a dict with params (px/s for translation, rad/s for rotation), score, score_at_rest and improvement — check improvement before trusting params, since at or below 1.0 the optimiser found no motion and stopped wherever it happened to be.

count(*, normalize=False, device=None)

Event-count image — one channel of total events per pixel (both polarities). Raw uint64 counts by default; uint8 rescaled to the busiest pixel when normalize=True.

countmask(*, pct=99.0, white_frame=False, device=None)

Count-mask image (GEPT, Sec. 3.2) — red holds the per-pixel positive-event count, blue the negative one, and green is a binary activity mask at full scale wherever any event landed. Both count planes are clipped and normalized by a single scale: the pct-th percentile of the non-zero counts of the two planes pooled together. Timestamps are not used. white_frame inverts to a white background — the black-background default is the form downstream descriptor models expect.

crop(x0, y0, w, h)

Keeps events inside the w`×`h window at (x0, y0), shifted to a new origin.

decimate(k)

Keeps every k-th event by index.

draw_mask(representation=None, *, colormap='viridis', normalize=None)

Draws a region of interest over this stream and returns it as an (H, W) boolean mask — the interactive way to build the argument mask() takes. Shows the same view view() would (pass a representation name to choose another), then: drag to keep an area, shift+drag to drop one, e/r/f to switch between ellipse, rectangle, and freehand, a/c to select all or clear, z to undo. Whatever stays bright is what the mask keeps.

Enter accepts and returns the mask; closing the window or pressing Esc returns None.

efast()

eFAST event corner detector (Mueggler et al., BMVC 2017). Keeps the events sitting on a moving corner, tested on two Bresenham rings over the per-polarity surface of active events.

event_drop(p=0.1, *, seed=0)

Drops each event independently with probability p.

event_rate(bin_ms=None, *, bin_s=None, bin_us=None, bin_ns=None)

Activity over time, binned into fixed intervals.

Returns a dict of numpy arrays — t (each bin’s left edge, µs), count, positive, negative, and rate (events/second) — so it plots directly: plt.plot(*r[“t”] / 1e6, r[“rate”]). The spatial counterpart is pixel_counts() on a reader, which totals events per pixel over a whole file.

filter_polarity(polarity)

Keeps only events of the given polarity (nonzero / True = ON, 0 / False = OFF).

flip_x()

Mirrors horizontally (x → width-1-x).

flip_y()

Mirrors vertically (y → height-1-y).

harris_corners(threshold=0.0)

Harris corner score on the Surface of Active Events: keeps events whose normalised Harris response det/trace² - k of the SAE-ramp structure tensor exceeds threshold. The default threshold=0 keeps corners (rank-2, R>0) and rejects straight edges (rank-1, R<0). The score is bounded to [-0.04, 0.21], so raise threshold within that range to be stricter; above 0.21 nothing is kept.

hot_pixel_filter(n_std=3.0)

Hot-pixel removal: drops pixels whose event count exceeds mean + n_std·std over the active pixels (default n_std=3.0).

invert_polarity()

Flips every event’s polarity.

iwe(params, *, model='translation', camera=None)

Builds the image of warped events for an explicit motion — the picture the objective scores.

mask(mask)

Keeps events inside the region of interest: a (H, W) boolean mask (or 8-bit map, where non-zero keeps the pixel) covering this stream’s sensor. Build one with eventcv.circle_mask / ellipse_mask / rect_mask / polygon_mask, draw one with draw_mask(), or load one with eventcv.load_mask.

mcts(*, max_window_ms=None, max_window_s=None, max_window_us=None, max_window_ns=None, windows_ms=None, windows_s=None, windows_us=None, windows_ns=None)

Multi-channel time surface (SuperEvent) — per polarity, one linear-decay channel per time window, float32. The windows are 5 log-spaced spans from 1 ms up to max_window_ms (default: the slice window, else 30 ms), or exactly the spans in windows_ms=[…] (2N channels, used in the order given); pass one form, not both.

normalize_time()

Shifts timestamps so the earliest event starts at zero.

optical_flow(*, window=3)

Dense Lucas-Kanade optical flow on the time surface.

Returns a two-channel (flow_x, flow_y) frame in pixels/ms; window is the half-width of the least-squares neighbourhood.

pixel_dropout(p=0.1, *, seed=0)

Silences a random p fraction of pixels for the whole stream (dead-pixel simulation).

play(*, fps=None, dt_ms=30.0, decay_ms=None, speed=1.0, refresh_hz=60.0, loop_=False)

Opens the playback GUI over this in-memory stream.

The in-memory counterpart of EventReader.play — same raw polarity-and-decay view, no file and no reader needed. At speed=1, recording time matches wall time; loop_ restarts at the end. Explicit fps= preserves the old fixed-frame-rate timing and is deprecated.

random_crop(width, height, *, seed=0)

Takes a random width × height crop; a window at least as large as the sensor is a no-op.

random_flip_x(p=0.5, *, seed=0)

Mirrors left-right with probability p.

random_flip_y(p=0.5, *, seed=0)

Mirrors top-bottom with probability p.

random_polarity_flip(p=0.5, *, seed=0)

Inverts every polarity with probability p (all events, not a random subset).

raw_image(*, decay_ms=None)

The raw polarity image of this stream as an (H, W, 3) uint8 array — what view(“raw”) displays, handed back instead of shown.

Events are coloured by polarity and dimmed by age, measured back from the newest event, so the picture reads as “what the sensor just saw” rather than as a flat accumulation.

redblue(*, pct=99.0, white_frame=True, device=None)

White-background red/blue counts, independently percentile-normalized per polarity. The stronger normalized polarity wins (positive on ties); timestamps are unused. white_frame is accepted for compatibility but the background is always white.

refractory_filter(dt, *, refresh='kept')

Refractory-period filter: suppresses a pixel’s events for dt after it fires.

refresh=”kept” (the default) measures the dead time from the pixel’s last emitted event, the hardware definition — a suppressed event does not restart it. refresh=”all” restarts it on every arriving event, so the pixel must fall silent for dt before it emits again; that is what tonic and evlib do, and it suppresses more under a noise burst. The two are indistinguishable on clean data.

repr

The default representation name carried from open(repr=…) / with_repr (what view()/flatten() render), or None for a raw stream.

resize(width, height)

Event-domain resize to a width`×`height grid (rebinned, not interpolated).

rotate90(k)

Rotates by k * 90° clockwise (quarter turns swap the sensor dims).

save(path, *, topic=None, format=None, compression=None)

Saves the stream to path, format chosen by extension (.npz/.txt/.h5/.bag, or .zip for E2VID) — the counterpart of eventcv.load. npz/HDF5/rosbag round-trip exactly; topic names the rosbag connection; format overrides the extension; compression picks the HDF5 filter (default gzip 1, False for none).

save_video(path, *, fps=30.0, dt_ms=30.0, decay_ms=None, repr=None, colormap='viridis', clim=None, max_frames=None)

Renders this stream as an animation, returning the number of frames written.

The in-memory counterpart of EventReader.save_video, and raw by default for the same reason: looking at a recording should not require first deciding how to represent it. dt_ms is how much of the stream each frame covers; fps is how fast they play back.

scale(sx, sy)

Scales the sensor by (sx, sy).

sort_by_time()

Returns a copy reordered by ascending timestamp (stable).

spatial_jitter(sigma=1.0, *, seed=0)

Jitters each event’s position by a gaussian offset of standard deviation sigma pixels.

time_jitter(sigma_ms=None, *, sigma_s=None, sigma_us=None, sigma_ns=None, seed=0)

Jitters each event’s timestamp by a gaussian offset, re-sorting afterwards.

time_reversal(p=0.5, *, seed=0)

Plays the stream backwards with probability p, inverting polarity to match.

time_scale(factor)

Scales every timestamp by factor (rounded).

time_shift(dt=None, *, dt_s=None, dt_ms=None, dt_us=None, dt_ns=None)

Shifts every timestamp by dt. The positional form is microseconds; dt_s/dt_ms/dt_us/ dt_ns take any unit, and unlike a window duration this one may be negative.

time_window(t0=None, t1=None, *, t0_s=None, t0_ms=None, t0_us=None, t0_ns=None, t1_s=None, t1_ms=None, t1_us=None, t1_ns=None)

Keeps events whose timestamp lies in the half-open window [t0, t1). Positional bounds are microseconds (the unit timestamps are stored in); t0_s/t0_ms/t0_us/t0_ns and the t1 equivalents take any unit.

translate(dx, dy)

Translates by (dx, dy); events shifted off the sensor are dropped.

transpose()

Reflects across the main diagonal ((x, y) → (y, x)); swaps the sensor dims.

undistort(camera)

Rectifies events with a Camera’s intrinsics + distortion (lens undistortion).

view(representation=None, *, colormap='viridis', normalize=None, decay_ms=None)

Opens the interactive viewer on this stream. Pass a representation name to choose what to show — stream.view(“flow”), stream.view(“count”), stream.view(“voxel”), … — or omit it to use the stream’s stored representation (from open(repr=…)), falling back to the polarity image. (Equivalent to stream.<repr>().view().)

stream.view(“raw”) shows the events themselves — polarity dots faded by age, the camera viewer’s own picture — rather than any representation of them.

warp_affine(matrix)

Applies a 2×3 affine matrix [[a,b,c],[d,e,f]] (rounded, no interpolation).

warp_perspective(matrix)

Applies a 3×3 perspective (homography) matrix.

class eventcv.EventFrame

Bases: object

connected_components(*, connectivity=8)

Connected-component labelling (Phase 5): treats any non-zero pixel as foreground and labels each 4- or 8-connected blob 1..=k, background 0. Returns a single-channel u64 frame.

draw_mask(*, colormap='viridis', normalize=True)

Draws a region of interest over this frame and returns it as an (H, W) boolean mask. See EventStream.draw_mask for the controls; Esc (or closing the window) returns None.

resize(width, height, *, pooling='average')

Resize spatial dimensions using average or sum pooling on shrinking axes.

save(path, *, colormap='viridis', normalize=True)

Saves the frame to path. .npz/.h5 store the raw array (shape, dtype, kind, channel_names) for eventcv.load_frame; .png writes a colormapped 2-D view (colormap = viridis/turbo/grayscale/redblue; normalize auto-contrasts).

view(*, colormap='viridis', normalize=True)

Opens the interactive GPU viewer. Image reprs are shown colour-mapped (colormap: viridis/turbo/grayscale/redblue; normalize auto-contrasts); volumetric reprs become an orbitable 3-D point cloud (drag to rotate, Esc to close).

class eventcv.EventReader

Bases: object

Lazy, seekable handle over a file’s events — the VideoCapture to load’s imread. Slices are fetched on demand (HDF5 by binary-searching its timestamps on disk), so multi-GB files never need to be fully resident. Opening with dt_ms fixes a frame duration so slice(n) returns the n-th frame (like seeking a video).

background_activity_filter(dt, *, neighbourhood='block')

Background-activity (nearest-neighbour) noise filter, per slice. neighbourhood is “block” (default, all eight neighbours) or “cross” (the four edge-adjacent ones).

batch(indices)

Renders slice indices into one dense [B, C, H, W] array — the explicit-batch path for training. Requires a representation (open(repr=…) / with_repr). indices is any int sequence (list / range / …); each is a dt_ms frame index.

crop(x0, y0, w, h)

Crops each slice to the w`×`h window at (x0, y0) (see [EventStream::crop]).

decimate(k)

Keeps every k-th event of each slice.

dt(unit='ms')

The fixed slice duration set at open, in unit (“s”, “ms”, “us”, “ns”), or None if the reader is in max_events mode.

dt_ms

The fixed slice duration set at open, or None if it was not given.

duration(unit='ms')

How long the recording runs, in unit (“s”, “ms”, “us”, “ns”).

efast()

Returns a new reader whose every slice is passed through [EventStream::efast], so the reader yields corner sub-streams (chain .count() / with_repr to visualise them).

event_drop(p=0.1, *, seed=0)

Drops each event in a slice independently with probability p.

filter_polarity(polarity)

Keeps only events of the given polarity per slice (nonzero / True = ON).

flip_x()

Mirrors each slice horizontally.

flip_y()

Mirrors each slice vertically.

harris_corners(threshold=0.0)

Returns a new reader whose every slice is passed through [EventStream::harris_corners].

hot_pixel_filter(n_std=3.0)

Hot-pixel removal applied to each slice (per-slice statistics; distinct from open(hot_pixel_filter=True), which computes one mask over the whole recording).

invert_polarity()

Flips every event’s polarity, per slice.

mask(mask)

Keeps each slice’s events inside the region of interest — a (H, W) boolean mask (or 8-bit map, where non-zero keeps the pixel) covering this recording’s sensor. Applied lazily to every slice, so open(…).mask(roi) reads a whole recording through the ROI.

max_events

The fixed events-per-slice set at open(max_events=…), or None if it was not given.

n_slices

Number of fixed slices spanning the recording (requires open(dt_ms=…) or open(max_events=…)).

normalize_time()

Shifts each slice’s timestamps so its earliest event starts at zero.

offset

The absolute framing origin (ms, the file’s time base) set at open(offset=…), or None if it was not given.

pixel_dropout(p=0.1, *, seed=0)

Silences a random p fraction of pixels in each slice.

play(*, fps=None, dt_ms=None, decay_ms=None, repr=None, colormap='viridis', clim=None, speed=1.0, refresh_hz=60.0, loop_=False, max_frames=None)

Opens the recording in the interactive player — the offline twin of camera.show().

With no representation this is the raw view: polarity dots fading over decay_ms, the same [RawSurface](eventcv_core::viz::RawSurface) the live viewer draws, so watching a file needs no more setup than watching a camera. Pass repr=”count” (or any representation name) to play that instead. speed multiplies the playback rate — 0.25 for a quarter-speed look at a fast scene — and dt_ms sets the window without reopening the reader.

Blocks on the main thread until the window is closed. At speed=1, recording time matches wall time. Explicit fps= preserves the old fixed-frame-rate timing and is deprecated.

random_crop(width, height, *, seed=0)

Takes a random width × height crop of each slice.

random_flip_x(p=0.5, *, seed=0)

Mirrors each slice left-right with probability p.

random_flip_y(p=0.5, *, seed=0)

Mirrors each slice top-bottom with probability p.

random_polarity_flip(p=0.5, *, seed=0)

Inverts every polarity in a slice with probability p.

read_camera_info()

The camera intrinsics the recording carries, or None when it carries none. Only ROS bags record them today; AEDAT does not.

read_frames(*, t0_us=0, t1_us=Ellipsis)

The intensity (APS) frames recorded alongside the events, as [(t_us, EventFrame), …].

A DAVIS records a greyscale video beside its event stream; this is that video. Sources without one (.npz, Prophesee, HDF5, …) return an empty list rather than an error. The window defaults to the whole recording, which on a multi-gigabyte file is a great many frames — pass t0_us/t1_us for anything larger than a demo.

read_imu(*, t0_us=0, t1_us=Ellipsis)

The IMU samples recorded alongside the events, as a dict of numpy arrays: t (µs), angular_velocity (rad/s) and linear_acceleration (m/s², gravity included), both [N, 3]. Empty for sources without an IMU.

refractory_filter(dt, *, refresh='kept')

Refractory-period filter, per slice. refresh is “kept” (default, the dead time runs from the last emitted event) or “all” (every arriving event restarts it).

repr

The per-slice representation name set at open/with_repr, or None (raw streams).

resize(width, height)

Event-domain resize of each slice to a width`×`height grid.

rotate90(k)

Rotates each slice by k * 90° clockwise.

save(path, *, format=None)

Converts the whole recording to path without loading it — slices are read in order and appended as they go, with any deferred ops (crop, mask, hot_pixel_filter, …) applied.

Targets E2VID’s interchange (.zip, or format=”e2vid” for a .txt), which is what a multi-gigabyte recording usually needs converting to. Saving to a round-tripping format works from a slice: eventcv.save(reader.slice(0), “frame.npz”).

save_video(path, *, fps=30.0, colormap='viridis', clim=None, max_frames=None, repr=None, dt_ms=None, decay_ms=None)

Renders every slice and writes them as one animation. Format comes from path’s extension: .gif, .apng/.png, or .mp4 (which pipes to a system ffmpeg).

Needs a representation — open(repr=…) or with_repr(…) — since a video is a sequence of rendered frames.

clim fixes the value that maps to the top of the colormap, held across every frame. Left as None, it is calibrated from the first few slices and then held, because auto-contrast computed per frame makes the brightness pump visibly over a sequence. Pass clim=0 to get the per-frame behaviour back, or an explicit value to match two videos to the same scale. The raw stream. With no representation — neither open(repr=…) nor repr= here — the events are drawn directly: each one lights its pixel in its polarity colour and fades over decay_ms, which is the view a camera’s own viewer shows and the one that needs no choice of representation to look at a recording. repr=”raw” asks for it explicitly even when the reader carries a representation. dt_ms sets the window here rather than at open, so the playback rate is a property of the video and not of the reader.

scale(sx, sy)

Scales each slice’s sensor by (sx, sy).

slice(index=None, *, t0_ms=None, t0_s=None, t0_us=None, t0_ns=None, t1_ms=None, t1_s=None, t1_us=None, t1_ns=None)

One slice. With a positional index n (requires open(dt_ms=…) or open(max_events=…)), returns the n-th fixed frame — the dt_ms-long window [t_min + n·dt, t_min + (n+1)·dt), or the max_events-sized event chunk [n·N, (n+1)·N); negative n counts from the end. Otherwise returns the half-open time window [t0_ms, t1_ms), with omitted bounds extending to the recording’s start / end. Bounds take any unit — t0_s / t0_ms / t0_us / t0_ns, and the same for t1. When the reader carries a representation (open(repr=…) / with_repr) the slice is rendered to that EventFrame; otherwise it is a raw EventStream.

slice_count(i0, i1)

Events whose index lies in [i0, i1) (clamped to the file). Rendered to the reader’s EventFrame when a representation is set (open(repr=…) / with_repr), else a raw EventStream.

sort_by_time()

Reorders each slice by ascending timestamp (stable).

spatial_jitter(sigma=1.0, *, seed=0)

Jitters event positions in each slice by a gaussian of standard deviation sigma pixels.

time_jitter(sigma_ms=None, *, sigma_s=None, sigma_us=None, sigma_ns=None, seed=0)

Jitters event timestamps in each slice, re-sorting afterwards.

time_reversal(p=0.5, *, seed=0)

Plays each slice backwards with probability p, inverting polarity to match.

time_scale(factor)

Scales each slice’s timestamps by factor.

time_shift(dt=None, *, dt_s=None, dt_ms=None, dt_us=None, dt_ns=None)

Shifts each slice’s timestamps by dt — positionally in microseconds, or in any unit via dt_s/dt_ms/dt_us/dt_ns.

time_span(unit='ms')

The recording’s (first, last) timestamp in unit — “s”, “ms” (the default), “us”, or “ns”. The unit-flexible form of time_span_ms.

time_window(t0=None, t1=None, *, t0_s=None, t0_ms=None, t0_us=None, t0_ns=None, t1_s=None, t1_ms=None, t1_us=None, t1_ns=None)

Keeps each slice’s events whose timestamp lies in [t0, t1) — positionally in microseconds, or in any unit via t0_s/t0_ms/t0_us/t0_ns and the t1 equivalents.

translate(dx, dy)

Translates each slice by (dx, dy).

transpose()

Reflects each slice across the main diagonal.

undistort(camera)

Undistorts each slice with a Camera’s intrinsics + distortion.

warp_affine(matrix)

Applies a 2×3 affine matrix to each slice.

warp_perspective(matrix)

Applies a 3×3 perspective matrix to each slice.

windows(*, step_ms=None, step_s=None, step_us=None, step_ns=None, span_ms=None, span_s=None, span_us=None, span_ns=None)

Lazy iterator of consecutive windows: each is [start, start + span_ms) and start advances by step_ms. step_ms defaults to the dt_ms set at open (so windows() walks every slice(n)), and span_ms defaults to step_ms (non-overlapping). For a max_events reader, windows() without arguments walks the fixed-count slices instead. Streams a multi-GB file window-by-window without loading it. Each item is a rendered EventFrame when the reader carries a representation (open(repr=…) / with_repr), else a raw EventStream.

with_repr(repr, *, bins=None, window_ms=None, window_s=None, window_us=None, window_ns=None, tau_ms=None, tau_s=None, tau_us=None, tau_ns=None, max_window_ms=None, max_window_s=None, max_window_us=None, max_window_ns=None, windows_ms=None, windows_s=None, windows_us=None, windows_ns=None, window=None, normalize=None, pct=None, white_frame=None, downsample=None)

Returns a new reader over the same file that renders each slice with repr (unset params take their method defaults; unset time spans follow the reader’s dt). The dataset-mode counterpart of open(repr=…), but with per-representation options: e.g. reader.with_repr(“voxel”, bins=5).

class eventcv.EventPointSet

Bases: object

class eventcv.Camera(fx, fy, cx, cy, *, k1=0.0, k2=0.0, p1=0.0, p2=0.0, k3=0.0)

Bases: object

Pinhole intrinsics + Brown–Conrady distortion, e.g. an EV-IMO calib.txt (fx fy cx cy k1 k2 p1 p2). Pass to stream.undistort(camera).

undistort_point(u, v)

Maps a distorted pixel (u, v) to its undistorted location.

class eventcv.EventCamera

Bases: object

A live USB event camera — the streaming twin of [PyEventReader]. Open one with eventcv.stream(…).

backlog

Buffers waiting in the driver’s ring. The pump thread drains it continuously, so this stays near zero unless the camera is producing faster than events can be decoded and written.

bias_state

What the adaptive-bias controller is doing, or None when the camera was opened without stream(adaptive_bias=…).

A dict of the last measured event_rate (events/second), the target_rate band being aimed at, whether it is still calibrating, the five bias values the controller drives, its authority, and n_slow_steps — how many times the slow loop has had to shift the operating point.

For the first second or so calibrating is true: the controller is measuring the scene at the camera’s stock biases and has changed nothing. target_rate afterwards is the band it settled on, which is the number to look at if the results surprise you.

authority says how the fast loop is coping, and is the field to watch:

  • “tracking” — fine: the rate is in your band, or the loop is on its way there.

  • “starved” / “flooded” — the refractory bias is at one end of its travel and the rate is still out of band, so the slow loop is shifting the operating point to help.

  • “hunting” — the loop keeps reversing without ever landing in the band, which means no bias setting reaches it. Almost always a sign that target_rate does not match what the scene can produce; measure the scene unbiased and re-bracket it.

Note event_rate was measured over the period before the biases now reported were applied — the two come from consecutive periods, not the same one.

close()

Stops the pump, closes the device, and finishes a stream(record=…) file if one is open. Idempotent; further use raises.

Dropping the camera does the same thing, so a throwaway eventcv.stream(…) still releases the device and closes its recording — but only at whatever point Python collects it. Call this (or use with eventcv.stream(…) as camera:) to control when that happens.

draw_mask(representation=None, *, decay_ms=None, colormap='viridis', normalize=True)

Draws a region of interest over the live view and returns it as an (H, W) boolean mask, applying it to this camera as well — so the next read(), loop, or record= window is already filtered. Everything show() takes applies here too (the view you draw over).

Drag to keep an area, shift+drag to drop one, e/r/f to switch between ellipse, rectangle, and freehand, a/c to select all or clear, z to undo. Whatever stays bright is what the mask keeps. Enter accepts; closing the window or Esc returns None and leaves the camera’s current mask alone.

mask

The region-of-interest mask events are filtered through — an (H, W) boolean array, or None when the whole sensor is in use.

Assign one to change it mid-session (camera.mask = eventcv.circle_mask(…), or None to clear it); stream(mask=…) sets it at open. Masked events are dropped as they are decoded, so they never reach a record= file, the windows a loop reads, or show() — and they cost nothing downstream. Where stream(roi=…) is a rectangle, blocked on-chip wherever the sensor can do it, this is any shape and always enforced on the host — so it can be changed while the camera runs.

n_frames

How many APS frames this camera has decoded, including any dropped from the backlog.

n_imu

How many IMU samples this camera has decoded, including any dropped from the backlog.

n_recorded

Events written so far by stream(record=…) (0 when the camera isn’t recording).

n_skimmed_events

Times the driver’s ring overflowed and dropped events before a window — the loss n_skipped can’t prevent, because it happens upstream of eventcv. Non-zero means the camera outran the decoder or the recording; lower the event rate, or record uncompressed. Events discarded on purpose by latest=True, counted exactly.

n_skipped counts whole windows the consumer never saw; this counts the events inside the ones dropped before a window was ever built. Together they are the whole of what this library throws away deliberately — n_overflows is the part the driver threw away, which only it can see.

n_skipped

Windows a stream(latest=True) loop skipped to stay live — the count of frames the loop was too slow to process. Always 0 without latest=True (that mode never skips). A steadily rising value means the per-window work is slower than the camera; recordings stay complete either way.

read(*, timeout_ms=None)

Returns the next window for a while loop — an EventFrame when the camera was opened with a representation (stream(repr=…)), else a raw EventStream. The window spans exactly the dt_ms / max_events chosen at stream(…) (so stream(dt_ms=50, repr=”mcts”).read() hands back one mcts frame per ~50 ms of events).

Blocks until that window is ready. Pass timeout_ms to cap the wait: if no window completes within that many milliseconds read returns None, so the loop can do other work or exit instead of blocking forever on an idle scene. (timeout_ms is a wait cap, not the window length.) Ctrl+C breaks out. Raises if the camera is closed.

By default windows are returned in order, so a loop slower than the camera falls further and further behind live. stream(latest=True) returns the newest completed window instead, counting what it skipped in n_skipped — record= still archives every window either way.

read_frames()

The APS frames the sensor produced alongside the windows read so far, as [(t_us, EventFrame), …], and clears them. Needs stream(frames=True) and a DAVIS; anything else returns an empty list.

Frames arrive with the window that covers them, so collect after each read(). Only the most recent few dozen are kept: an uncollected backlog is dropped rather than grown, the same trade stream(latest=True) makes for events.

read_imu()

The IMU samples alongside the windows read so far, as a dict of numpy arrays — t (µs), angular_velocity (rad/s) and linear_acceleration (m/s²), both [N, 3] — and clears them. Same shape eventcv.read_imu returns for a file. Needs stream(imu=True).

record(path, *, seconds=None, compression=None)

Records the live stream to path (format by extension, like eventcv.save). Blocks until seconds elapses (if given) or Ctrl+C, then returns the number of events saved.

HDF5 targets (.h5 / .hdf5) are written continuously, window-by-window straight to disk via an EventSink, and flushed roughly once a second — so a long session never piles up in memory and a crash keeps everything captured so far. compression is an optional gzip level (0..=9) for that path; omit it for the fastest, uncompressed writes. Other formats (npz/txt/bag can’t be appended incrementally) buffer the whole recording in memory and write once at the end, ignoring compression.

Only events are saved — a DAVIS346’s APS frames and IMU samples are dropped.

roi

…}` — or None when the whole sensor is in use.

applied is “hardware” when the sensor is blocking those pixels on-chip (they cost no USB bandwidth, no decoding, and nothing downstream) or “host” when eventcv is filtering them as they are decoded, because this sensor has no region masks. Both drop the same events; only the saving differs. Unlike mask, this is fixed when the camera is opened — the sensor writes its region masks during the opening handshake.

Type:

The stream(roi=…) rectangle, as `{“rect”

Type:

(x0, y0, width, height), “applied”

show(representation=None, *, decay_ms=None, colormap='viridis', normalize=True, dt_ms=30.0, refresh_hz=60.0)

Opens the interactive live viewer. With no representation, shows the raw event stream (polarity dots with exponential decay — the default view). Pass a representation name (“count”, “tencode”, “voxel”, …) to render that live instead, “raw” to force the raw view, or “aps” to watch the sensor’s own greyscale video (DAVIS only — every other camera shows nothing, having no APS array). The event view shares the recording player’s accumulation, refresh, statistics, and processor controls; its bounded freshness queue keeps capture and record= running while display is paused. Blocks until the window is closed. decay_ms tunes the raw fade; colormap / normalize apply to representations and APS.

class eventcv.EventSink(path, *, format=None, compression=None)

Bases: object

Streams EventStream windows into an event file — the event-level twin of FrameSink. Where FrameSink appends computed representations, this appends the raw events, so a live camera (or any window source) can be recorded to disk continuously without holding the whole session in memory. The sensor size and time base are taken from the first appended stream, and the file reads straight back with eventcv.open / eventcv.load.

Every event format eventcv.save writes can be appended this way, chosen by the path’s extension exactly as save chooses it: .h5, .npz, .txt/.csv, .bag, .aedat, .aedat4, .dat, .raw and E2VID’s .zip. What differs is when the file becomes readable — HDF5, text, AEDAT and the Prophesee formats append straight to disk, while npz and rosbag have a header or an index to write at the end and are only complete once finish() runs.

Behind a Mutex only to satisfy pyo3’s Sync requirement on a #[pyclass]: a sink is a single writer by construction, and the zip encoder E2VID’s export uses is Send but not Sync.

append(stream)

Appends one EventStream window to the end of the file. Empty windows are a no-op.

finish()

Flushes and closes the file; further appends raise. Idempotent.

flush()

Forces buffered events out to disk without closing, so a crash mid-recording keeps everything appended so far. Call periodically during a long capture. For npz and rosbag the bytes go out but the file stays incomplete until finish().

class eventcv.FrameSink(path)

Bases: object

Streams EventFrame`s into one extendable `[N, C, H, W] HDF5 dataset — for writing a representation window-by-window over a whole recording. The dtype and [C, H, W] shape are fixed by the first appended frame.

append(frame)

Appends one frame as the next slice; every frame must match the first’s dtype + shape.

finish()

Flushes and closes the file; further appends raise. Idempotent.

FEAST feature learning

FEAST is an unsupervised, online event-feature extractor (Afshar et al., Event-based Feature Extraction Using Adaptive Selection Thresholds, Sensors 2020 / arXiv:1907.07853). Unlike the stateless stream/frame ops, it is a stateful, trainable model (scikit-learn-style fit/transform): fit adapts a set of feature prototypes and their selection thresholds event-by-event, and transform then maps each event to its nearest learned feature.

import eventcv as ecv

stream = ecv.load("recording.npz")
feast = ecv.FEAST(n_features=100, patch=11, tau_ms=30.0, per_polarity=True, seed=0)
feast.fit(stream, epochs=2)             # online, unsupervised; returns the miss rate

ids  = feast.transform(stream)          # (N,) nearest-feature id per event (-1 at borders)
hist = feast.histogram(stream)          # pooled feature-event counts (classifier input)
imgs = feast.feature_images()           # (n_features_total, patch, patch) learned patches

ecv.save(feast, "model.npz")            # persist the trained model
feast = ecv.load_feast("model.npz")     # ...and reload it
class eventcv.FEAST(n_features=100, patch=11, tau_ms=None, eta=0.001, delta_i=0.001, delta_e=0.003, per_polarity=True, seed=0, *, tau_s=None, tau_us=None, tau_ns=None)

Bases: object

FEAST feature extractor (Afshar et al., 2020) — an unsupervised, online, sklearn-style model. fit adapts feature weights and selection thresholds event-by-event (repeatable across recordings/epochs); transform then maps each event to its nearest feature id.

feature_images()

The learned features as an (n_features_total, patch, patch) float32 array — reproduces the paper’s feature grids for visualisation.

fit(stream, epochs=1)

Trains on stream for epochs online passes; returns the final-epoch miss rate (the fraction of in-bounds events matching no feature), which settles low as the network converges. Call repeatedly to train across multiple recordings.

get_params()

The constructor parameters as a dict (sklearn-style); consumed by eventcv.save.

histogram(stream)

Pooled feature-event counts over stream (the paper’s classifier input). Shape (n_features_total,), dtype uint32.

missed_rate

Miss rate recorded by the most recent fit (0.0 before any training).

thresholds

The current selection thresholds, shape (n_features_total,), dtype float32.

transform(stream)

Feature id for every event (nearest feature by cosine distance; -1 for border events). Shape (len(stream),), dtype int32; ids are population * n_features + feature.

Live camera streaming

eventcv.stream(...) opens a USB event camera (Prophesee EVK4/EVK3-HD, iniVation DVXplorer/DAVIS346, CenturyArks) as a live EventCamera — the streaming twin of open(). It yields the same EventStream windows the file readers do, so every representation, transform, feature detector, and viewer composes on a live feed:

import eventcv as ecv

with ecv.stream(dt_ms=50, repr="mcts", record="session.h5") as cam:
    while running:
        infer(cam.read().numpy())   # a representation per window; raw events archived as you go

record() is the one-shot form for scripts that only want a file — it opens the camera, captures for seconds, and closes it before returning, so the recording is complete and the device free by the next line:

ecv.record("session.h5", seconds=10)
reader = ecv.open("session.h5", dt_ms=50)

Windowing mirrors open (dt_ms or max_events), repr= and its options render each window, record= archives the raw events, latest=True keeps a slow loop on live data, max_event_rate / roi cap what the sensor emits in hardware, and mask= restricts it to an arbitrarily shaped region of interest on the host. See the streaming guide for the full picture — recording, staying live under load, source caps, and troubleshooting. These functions are built into wheels that include camera support; on Linux the camera needs udev rules for non-root USB access.

Adaptive biasing

Fixed biases make the same scene produce wildly different event rates as the light changes. adaptive_bias=True measures the rate and retunes the sensor’s bias currents as it runs, after Nair et al., Enhancing Visual Place Recognition via Fast and Slow Adaptive Biasing in Event Cameras (IROS 2024) — a fast loop mapping the rate onto the refractory period several times a second, and a slow loop shifting the photoreceptor and threshold biases when the fast one runs out of travel. It starts from the camera’s stock biases, so enabling it never jumps the picture, and bias_state reports what it is doing.

with ecv.stream(dt_ms=50, adaptive_bias={"target_rate": (3e4, 1.2e5)}) as cam:
    while running:
        infer(cam.read().numpy())
        print(cam.bias_state)   # event_rate, the five bias values, authority, n_slow_steps

It measures your scene first. For about the first second the controller changes nothing and just watches, then centres its band on the rate the camera was actually producing — so adaptive_bias=True means “hold the event rate wherever this scene started”, with no numbers to pick. Asking for a rate a scene cannot supply at any bias setting is the one way to get bad results (it drives the sensor into amplifying its own noise), and measuring first avoids it. Pass target_rate only for a specific absolute rate; that skips the measurement. bias_state reports the band chosen, whether it is still calibrating, and an authority of "hunting" if the band turns out to be unreachable anyway.

Supported on the iniVation DAVIS346 and the Prophesee EVK4, each with its own defaults. Measured on a DAVIS346 over a static indoor scene, the median rate held within 1.2x across runs where the unbiased camera wandered 5.7x; on an EVK4, 1.8x against 4.2x. Other cameras raise rather than silently doing nothing.

eventcv.stream(serial: str | None = None, *, dt_ms: float | None = None, dt_s: float | None = None, dt_us: float | None = None, dt_ns: float | None = None, max_events: int | None = None, repr: str | None = None, bins: int | None = None, window_ms: float | None = None, window_s: float | None = None, window_us: float | None = None, window_ns: float | None = None, tau_ms: float | None = None, tau_s: float | None = None, tau_us: float | None = None, tau_ns: float | None = None, max_window_ms: float | None = None, max_window_s: float | None = None, max_window_us: float | None = None, max_window_ns: float | None = None, window: int | None = None, normalize: bool | None = None, pct: float | None = None, white_frame: bool | None = None, record: str | None = None, compression: int | None = None, latest: bool = False, max_event_rate: float | None = None, roi: tuple[int, int, int, int] | None = None, mask=None, adaptive_bias: bool | dict | None = None, decay_ms: float | None = None, decay_s: float | None = None, decay_us: float | None = None, decay_ns: float | None = None, frames: bool = False, imu: bool = False) → EventCamera[source]

Open a live USB event camera — the streaming twin of open().

Where open() turns a file into sliceable EventReader windows, stream turns a camera into a live EventCamera that yields the very same EventStream windows — so every representation, transform, feature detector, and viewer in eventcv composes on a live feed exactly as it does on a recording.

serial selects a specific device (from list_cameras()); None opens the first found. Windowing mirrors open(): pass dt_ms for fixed-duration windows or max_events for a fixed event count (mutually exclusive; default dt_ms=30). Only non-empty windows are yielded, so a loop never spins on idle time.

Pass repr (a representation name — "count", "voxel", "tencode", …) to make iteration (and EventCamera.read()) yield rendered EventFrame s instead of raw streams, mirroring open(repr=…). The per-representation options are the same ones EventReader.with_repr() takes — bins, window_ms, tau_ms, max_window_ms, window (for "flow"), normalize, pct, and white_frame. decay_ms sets the fade time constant of the raw EventCamera.show() view.

Close the camera when you are done with it. The device and any record= file are released when the camera is closed — by EventCamera.close(), by leaving a with block, or, if you do neither, whenever Python happens to collect the object. Prefer the first two, and prefer record() over stream(...).record(...) when a script only wants a file:

with ecv.stream(dt_ms=50) as cam:    # closed on the way out
    ...
ecv.record("session.h5", seconds=10)  # opened, recorded, and closed in one call

Time spans follow the capture window. An unset window_ms / tau_ms / max_window_ms defaults to dt_ms, so a live representation covers exactly the events it was handed: stream(dt_ms=50, repr="tencode") encodes the full 50 ms instead of the 30 ms default (which would silently discard the oldest 20 ms of every window). Set one explicitly to override — stream(dt_ms=50, repr="tencode", window_ms=20) keeps only the newest 20 ms. In max_events mode there is no fixed duration, so the 30 ms defaults stand.

Pass record (a path in any event format save() writes — .h5, .npz, .txt, .csv, .bag, .aedat, .aedat4, .dat, .raw) to archive the session while you work: every window the loop reads has its raw events appended to that file first, so a repr= loop processes representations live and still keeps the full-resolution recording for later. Writing happens in Rust as each window is decoded — no per-window Python round trip — and is flushed about once a second, so a crash keeps everything up to a second ago.

Formats differ in when the file becomes readable, which matters for a capture that might be interrupted: HDF5, text, AEDAT and the Prophesee formats append straight to disk and are a valid recording after every window, while npz and rosbag have a header or an index to write at the end and are only complete once the camera is closed. compression is an optional gzip level (0..=9) for HDF5; omit it for the fastest writes. The file is closed by EventCamera.close() or the with block, and EventCamera.n_recorded counts what has been written. Iteration and the event GUI both keep this background recorder running; EventCamera.record() is the blocking alternative when no interactive view or loop is needed.

Capping the source. Every event costs time to decode, window, and render, so the cheapest event is one the camera never sends. max_event_rate (events per second) enables the sensor’s on-chip event-rate controller, and roi=(x0, y0, width, height) masks every pixel outside that rectangle, so neither costs the host anything. A saturating scene on a 1280×720 sensor can emit far more than one core can decode, and capping the source is the only fix that keeps the events you do get contiguous rather than punched full of dropout holes:

ecv.stream(dt_ms=50, max_event_rate=40_000_000)     # 40 Mev/s ceiling, enforced on-chip
ecv.stream(dt_ms=50, roi=(320, 180, 640, 360))      # centre quarter only

Both are on-chip on the sensors built around Prophesee’s pipeline — the EVK4, the EVK3 HD, and the CenturyArks VGA. The iniVation cameras (DVXplorer, DAVIS346) have neither, so there roi= falls back to a host-side mask (the same events are dropped, but only after crossing the cable and being decoded) and warns that it did; max_event_rate still raises, since capping the rate on the host would save none of the work it exists to avoid. EventCamera.roi reports the rectangle and whether it was "hardware" or "host".

Region of interest. mask takes an arbitrarily shaped ROI — an (H, W) boolean array (or 8-bit map, where non-zero keeps the pixel) covering the sensor. Events outside it are dropped as they are decoded, so they never reach a record= file, the windows your loop reads, or EventCamera.show(), and they cost nothing downstream. Where roi= is a rectangle fixed at open, mask is any shape and can be changed while the camera runs — so it suits sensors whose useful data is a circle:

aperture = ecv.circle_mask((640, 480), cx=320, cy=240, r=230)
with ecv.stream(dt_ms=50, mask=aperture, record="session.h5") as cam:
    for events in cam:              # masked already, as is the recording
        track(events)

Set EventCamera.mask to change it mid-session (None clears it), or draw one over the live view with EventCamera.draw_mask(). Build masks with circle_mask(), ellipse_mask(), rect_mask(), polygon_mask(), or load one with load_mask().

Adaptive biasing. A camera left on fixed biases produces wildly different event rates as the light changes — the same scene can starve at dusk and saturate in sun, and anything downstream that was tuned on one no longer works on the other. adaptive_bias=True closes the loop: eventcv measures the event rate and retunes the sensor’s bias currents as it runs, so the stream stays comparable across conditions. Two loops do it, after Nair et al., Enhancing Visual Place Recognition via Fast and Slow Adaptive Biasing in Event Cameras (IROS 2024) — a fast one that maps the rate onto the refractory period several times a second, and a slow one that shifts the photoreceptor and threshold biases whenever the fast one runs out of room:

ecv.stream(adaptive_bias=True)                              # the paper's tuning
ecv.stream(adaptive_bias={"target_rate": (2e5, 1e6)})       # aim for a quieter stream

It starts from whatever biases the camera is already running (its stock configuration) and adjusts from there, so turning it on never jumps the picture. Pass a dict to override any of period_ms, target_rate (a (low, high) band in events/second), throttle_range, max_slew, patience, step, or limits; EventCamera.bias_state reports what the controller is doing, including an authority of "hunting" when the band you asked for is not reachable at all. Supported on the iniVation DAVIS346 and the Prophesee EVK4; other cameras raise rather than silently doing nothing.

Defaults are per sensor, so adaptive_bias=True means something sensible on either — the EVK4 has 10x the pixels and plain byte registers rather than the DAVIS346’s 2041-step current ladder, so its rates, step sizes and bounds all differ. Anything you pass overrides just that field and leaves the rest to the camera. limits takes either one (low, high) for every bias or a dict naming them (photoreceptor, follower, on_threshold, off_threshold); the per-bias form matters on an IMX636, whose ON and OFF thresholds must stay on opposite sides of its diff reference.

It measures your scene first. For about the first second the controller changes nothing and just watches, then centres its target band on the rate the camera was actually producing. So adaptive_bias=True means “hold the event rate wherever this scene started” — the reference condition is whatever it saw at startup, and the loops then work to keep later conditions looking like it. That is what you want for consistency across lighting, and it avoids the trap of asking for a rate the scene cannot supply at any bias setting, which would drive the sensor into the regime where it is amplifying its own noise.

Pass target_rate only when you need a specific absolute rate; doing so skips the measurement. bias_state reports the band that was chosen, and calibrating while it is still measuring:

ecv.stream(adaptive_bias=True)                              # hold this scene's rate
ecv.stream(adaptive_bias={"target_rate": (3e4, 1.2e5)})     # hold this exact band
ecv.stream(adaptive_bias={"calibrate": 20})                 # measure for longer first

This complements max_event_rate rather than replacing it: the rate limiter is a hard on-chip ceiling that discards events once you exceed it, while adaptive biasing changes what the pixels produce in the first place, so the events you keep stay evenly spread rather than clipped.

Decoding, and any record= writing, run on a background thread that owns the camera, so the driver’s ring is drained continuously no matter what your loop is doing — the loop only collects windows that are already decoded. On a Prophesee EVK4 at dt_ms=50 this leaves ~80% of each window’s wall-clock budget free for your own work: 40 ms of per-frame processing still holds 19.9 of an ideal 20 fps with the ring empty. The event GUI uses this same pump; EventCamera.record(), EventCamera.draw_mask(), APS viewing, and EventCamera.close() take the camera back directly.

Windows are delivered in order, so a loop whose per-window work is slower than the camera still falls behind — the thread buffers a few windows, then applies backpressure, and under sustained overload the driver’s ring is what finally overflows (watch EventCamera.n_overflows). Pass latest=True to trade completeness for freshness instead: reads hand back the newest decoded window and drop what it overtook. Buffers queued behind the freshest one are skimmed — decoded far enough to keep the sensor’s stream state exact, then discarded without being built into windows — which is both cheaper than building results nobody reads and, more importantly, keeps the driver’s ring drained. That matters because a full ring backs pressure up into the sensor, where events are lost with nothing able to count them. Two counters say what went: EventCamera.n_skipped counts whole windows passed over and EventCamera.n_skimmed_events counts events discarded before a window was built. Both are exact; EventCamera.n_overflows remains the part the driver dropped, which only it sees.

Skimming is off while record= is open, because the file is promised every window and a skimmed buffer’s events are gone before the recorder could archive them. With a recording attached the loop still gets the newest window and the file still gets all of them, at the old cost — asking for a complete recording is asking for complete decoding to go with it.

The returned camera is a context manager and an iterator:

# Live raw event view (the default) — polarity dots with exponential decay:
eventcv.stream().show()                      # 30 ms window, up to 60 refreshes/s

# Live representation view:
eventcv.stream(dt_ms=30).show("count", refresh_hz=60)

# Iterate windows and run any eventcv op on the live feed:
with eventcv.stream(dt_ms=30) as cam:
    for events in cam:              # each `events` is a 30 ms EventStream
        corners = events.efast()
        if done:
            break

# A `while` loop that returns one representation per time window — `read()` blocks for
# the next window (here one MCTS frame per ~50 ms of events):
cam = eventcv.stream(dt_ms=50, repr="mcts")
while running:
    frame = cam.read()              # EventFrame (mcts); use max_events=N for count windows
    infer(frame.numpy())

# Process representations live *and* archive the raw events in the same loop:
with eventcv.stream(dt_ms=50, repr="mcts", record="session.h5") as cam:
    while running:
        infer(cam.read().numpy())   # raw events for this window are already on disk

# `latest=True` keeps slow work on live data — the file still gets every window:
with eventcv.stream(dt_ms=30, repr="count", record="session.h5", latest=True) as cam:
    while running:
        slow_inference(cam.read().numpy())
    print(cam.n_skipped, "windows skipped,", cam.n_recorded, "events recorded")

# Record continuously, straight to disk (window-by-window, never buffering the whole
# session), stopping after 10 s or on Ctrl+C. `eventcv.record` is the one-shot form of
# this and closes the camera for you:
eventcv.record("session.h5", seconds=10)

# Or drive the recorder yourself with an EventSink, mixing capture and processing:
with eventcv.stream(dt_ms=50) as cam, eventcv.EventSink("session.h5") as sink:
    for events in cam:
        sink.append(events)         # to disk
        track(events)               # ...and process the same window live

APS frames and IMU. A DAVIS produces a greyscale video and an IMU stream alongside its events. Pass frames=True / imu=True to collect them, then take them with EventCamera.read_frames() and EventCamera.read_imu() — the same shapes read_frames() and read_imu() return for a recording, so code written against a file works unchanged on a live camera:

with ecv.stream(dt_ms=50, frames=True, imu=True) as cam:
    for events in cam:
        for t_us, frame in cam.read_frames():   # EventFrame, uint16 greyscale
            show(frame.numpy())
        gyro = cam.read_imu()["angular_velocity"]

ecv.stream(frames=True).show("aps")             # watch the APS video live

Both are off by default: copying a frame out of every decode buffer is not free, and other sensors have neither stream. Collect them as you read — only the most recent few dozen frames and a few thousand samples are held, and the oldest are dropped rather than buffered forever.

Requires a build with camera support and, on Linux, udev rules for USB access.

eventcv.record(path: str, *, seconds: float | None = None, serial: str | None = None, dt_ms: float | None = None, dt_s: float | None = None, dt_us: float | None = None, dt_ns: float | None = None, max_events: int | None = None, compression: int | None = None, max_event_rate: float | None = None, roi: tuple[int, int, int, int] | None = None, mask=None, adaptive_bias: bool | dict | None = None) → int[source]

Record a camera to path in one call, and return the number of events saved.

The one-shot form of stream(...).record(...), and the one to reach for when a script only wants a file: the camera is opened, captured from for seconds (or until Ctrl+C), and closed before this returns — so the recording is complete and the device is free by the time the next line runs:

ecv.record("session.h5", seconds=10)
reader = ecv.open("session.h5", dt_ms=50)      # safe: nothing is still writing

The format follows the extension, as in save(), and every one of them is written continuously, window-by-window — a recording is never held in memory, however long it runs. compression is an optional gzip level (0..=9) for HDF5. npz and bag are the two that only become readable once the recording closes, since both have something to write at the end.

Everything stream() takes about what the sensor sends applies — serial, dt_ms / max_events, roi, mask, max_event_rate, adaptive_bias. The representation and viewer options do not, since nothing is displayed. Open a camera with stream() instead when you want to process windows as they arrive.

eventcv.list_cameras() → list[dict][source]

List every connected, supported USB event camera.

Returns one dict per device with keys kind (machine name, e.g. "prophesee_evk4"), name (model), serial, bus, address, and speed. A device’s serial can be passed to stream() to select it when several are attached; an empty list means no supported camera was found.

Asking never fails. An empty list is the answer on a machine with no camera, in a container without USB passthrough, or on a CI runner with no USB subsystem at all — none of which are errors. The only exception raised is when the build has no camera support compiled in.

On Linux, accessing the device needs udev rules. If a camera is plugged in but not listed, this emits a RuntimeWarning saying so — install the rules (see the neuromorphic-drivers README) and re-plug. It is a warning rather than an error because the list is genuinely empty either way; the warning only tells you why.

Functional (OpenCV-style) API

Each function below forwards to the identically named method on the object passed as its first argument — e.g. eventcv.voxel(stream, bins=5) is stream.voxel(bins=5). They are generated by introspecting the compiled types, so this list always matches the methods above. Applying a stream op to an EventReader — e.g. eventcv.hot_pixel_filter(reader) — returns a new lazy reader that applies the op to every slice it yields (identical to reader.hot_pixel_filter()), so filtering/geometry compose with slice/windows/with_repr without loading the file.

class eventcv.SimulationResult

Bases: object

What simulate(…, out=…) returns in place of an EventStream: the file it wrote and what went into it. len(result) is the event count.

events

Events written.

frames

Source frames processed.

path

The file that was written.

eventcv.circle_mask(sensor_size: tuple[int, int], cx: float, cy: float, r: float)[source]

Build a circular ROI mask of radius r centred on (cx, cy).

Returns an (H, W) boolean NumPy array — True where events are kept — ready for EventStream.mask(), EventReader.mask(), or ecv.stream(mask=…). Note the two orders: sensor_size is (width, height) like everywhere else in eventcv, while the array that comes back is (H, W) like every other NumPy image.

Masks are plain arrays, so they compose with NumPy’s own operators — | unions, & intersects, ~ inverts:

aperture = ecv.circle_mask((640, 480), cx=320, cy=240, r=230)
aperture &= ~ecv.rect_mask((640, 480), 0, 0, 64, 64)   # minus a corner
ecv.load("rec.npz").mask(aperture)

Coordinates are continuous, not pixel indices: a pixel is kept when its centre falls inside the shape, and geometry reaching off the sensor is clamped. See also ellipse_mask(), rect_mask(), polygon_mask(), load_mask(), and — to draw one by hand — EventStream.draw_mask() / EventCamera.draw_mask().

eventcv.bag_topics(path: str) → list[tuple[str, str]][source]

List every topic in a ROS bag with its message type.

Bags disagree about names — a DAVIS recorded through dvs_ros_driver publishes /dvs/events where load() defaults to /davis/left/events — so this is usually the first thing to call on an unfamiliar file:

>>> ecv.bag_topics("recording.bag")
[('/dvs/camera_info', 'sensor_msgs/CameraInfo'),
 ('/dvs/events', 'dvs_msgs/EventArray'),
 ('/dvs/image_raw', 'sensor_msgs/Image'),
 ('/dvs/imu', 'sensor_msgs/Imu')]
eventcv.ellipse_mask(sensor_size: tuple[int, int], cx: float, cy: float, rx: float, ry: float)[source]

Build an elliptical ROI mask centred on (cx, cy) with semi-axes rx, ry.

The axis-independent form of circle_mask(); see it for the conventions.

eventcv.load_mask(path: str)[source]

Read an ROI mask from a .png as an (H, W) boolean array.

A pixel is kept where the image is non-black and not fully transparent, so a mask binarised in any other tool loads as-is — greyscale, palette, or colour, with or without alpha:

ecv.stream(dt_ms=50, mask=ecv.load_mask("aperture.png"))

The counterpart of save_mask().

eventcv.polygon_mask(sensor_size: tuple[int, int], points)[source]

Build an ROI mask keeping the interior of the closed polygon through points.

points is a sequence of (x, y) vertices; the last joins back to the first. Filled by the even-odd rule, so a self-intersecting outline leaves holes. See circle_mask() for the conventions.

eventcv.read_camera_info(source, *, topic: str | None = None)[source]

Read camera intrinsics from a recording, or None if it carries none.

Returns a Camera ready to pass to contrast_maximise(model="rotation", camera=…), which needs intrinsics to map pixels onto rays. source is a path or an open EventReader; only ROS bags (sensor_msgs/CameraInfo) record intrinsics today. Distortion coefficients are not applied — build a Camera yourself from the bag’s D if you need undistortion.

eventcv.read_frames(source, *, topic: str | None = None, t0_us: int = 0, t1_us: int | None = None) → list[tuple[int, EventFrame]][source]

Read DAVIS APS frames as [(t_us, EventFrame), …].

The greyscale images a DAVIS captures alongside its events — the reference the simulator is calibrated against, since the frames and the events see the same scene through the same lens.

source is a path or an open EventReader; ROS bags and AEDAT 2.0 recordings carry frames, and any other format returns an empty list. Frames come back at the sensor’s own precision, so frame.numpy() is uint8 from a bag (ROS mono8) and uint16 from AEDAT (a 10-bit ADC).

topic selects the bag topic and defaults to its only sensor_msgs/Image topic when there is exactly one; it does not apply to other formats. The time window matters: these recordings run to tens of gigabytes, and whole chunks outside it are skipped without being decoded. From a bag only mono8/rgb8/bgr8 are supported; any other encoding raises rather than being misread.

eventcv.read_imu(source, *, topic: str | None = None, t0_us: int = 0, t1_us: int | None = None) → dict[source]

Read IMU samples as a dict of arrays.

Keys are t (µs), angular_velocity and linear_acceleration, the latter two [N, 3] in rad/s and m/s². On a DAVIS this is the only independent measurement of how the camera moved, which makes it the ground truth for contrast_maximise():

imu = ecv.read_imu("recording.bag", t0_us=t0, t1_us=t1)
truth = imu["angular_velocity"].mean(axis=0)

source is a path or an open EventReader; ROS bags and AEDAT 2.0 recordings carry an IMU, and any other format returns empty arrays.

Orientation is not returned: on a DAVIS it is dead-reckoned from this same gyro, so it is not an independent quantity. Temperature is not returned either.

eventcv.record(path: str, *, seconds: float | None = None, serial: str | None = None, dt_ms: float | None = None, dt_s: float | None = None, dt_us: float | None = None, dt_ns: float | None = None, max_events: int | None = None, compression: int | None = None, max_event_rate: float | None = None, roi: tuple[int, int, int, int] | None = None, mask=None, adaptive_bias: bool | dict | None = None) → int[source]

Record a camera to path in one call, and return the number of events saved.

The one-shot form of stream(...).record(...), and the one to reach for when a script only wants a file: the camera is opened, captured from for seconds (or until Ctrl+C), and closed before this returns — so the recording is complete and the device is free by the time the next line runs:

ecv.record("session.h5", seconds=10)
reader = ecv.open("session.h5", dt_ms=50)      # safe: nothing is still writing

The format follows the extension, as in save(), and every one of them is written continuously, window-by-window — a recording is never held in memory, however long it runs. compression is an optional gzip level (0..=9) for HDF5. npz and bag are the two that only become readable once the recording closes, since both have something to write at the end.

Everything stream() takes about what the sensor sends applies — serial, dt_ms / max_events, roi, mask, max_event_rate, adaptive_bias. The representation and viewer options do not, since nothing is displayed. Open a camera with stream() instead when you want to process windows as they arrive.

eventcv.rect_mask(sensor_size: tuple[int, int], x0: float, y0: float, width: float, height: float)[source]

Build a rectangular ROI mask keeping the width``×``height box at (x0, y0).

Unlike EventStream.crop(), which shrinks the sensor grid, a mask keeps the coordinates (and the sensor size) unchanged and only drops events. See circle_mask() for the conventions.

eventcv.save_mask(mask, path: str) → None[source]

Write an ROI mask to an 8-bit greyscale .png — white keeps, black drops.

Lets an ROI drawn or computed once be reused across sessions (and checked in any image viewer). Read it back with load_mask().

eventcv.atsurf(obj, *args, **kwargs)

Averaged time surface — the per-pixel mean of exp(-age/tau_ms) over all events

Free-function form of obj.atsurf(*args, **kwargs).

eventcv.background_activity_filter(obj, *args, **kwargs)

Background-activity (nearest-neighbour) noise filter: keeps an event only if a

Free-function form of obj.background_activity_filter(*args, **kwargs).

eventcv.batch(obj, *args, **kwargs)

Renders slice indices into one dense [B, C, H, W] array — the explicit-batch path

Free-function form of obj.batch(*args, **kwargs).

eventcv.concat(obj, *args, **kwargs)

Concatenates this stream with others (argument order; sensor = element-wise max).

Free-function form of obj.concat(*args, **kwargs).

eventcv.connected_components(obj, *args, **kwargs)

Connected-component labelling (Phase 5): treats any non-zero pixel as foreground and labels

Free-function form of obj.connected_components(*args, **kwargs).

eventcv.contrast_maximise(obj, *args, **kwargs)

Recovers the camera motion that makes the warped events sharpest.

Free-function form of obj.contrast_maximise(*args, **kwargs).

eventcv.count(obj, *args, **kwargs)

Event-count image — one channel of total events per pixel (both polarities). Raw

Free-function form of obj.count(*args, **kwargs).

eventcv.countmask(obj, *args, **kwargs)

Count-mask image (GEPT, Sec. 3.2) — red holds the per-pixel positive-event count, blue the

Free-function form of obj.countmask(*args, **kwargs).

eventcv.crop(obj, *args, **kwargs)

Keeps events inside the w`×`h window at (x0, y0), shifted to a new origin.

Free-function form of obj.crop(*args, **kwargs).

eventcv.decimate(obj, *args, **kwargs)

Keeps every k-th event by index.

Free-function form of obj.decimate(*args, **kwargs).

eventcv.draw_mask(obj, *args, **kwargs)

Draws a region of interest over this stream and returns it as an (H, W) boolean mask —

Free-function form of obj.draw_mask(*args, **kwargs).

eventcv.dt(obj, *args, **kwargs)

The fixed slice duration set at open, in unit (“s”, “ms”, “us”, “ns”), or

Free-function form of obj.dt(*args, **kwargs).

eventcv.duration(obj, *args, **kwargs)

How long the recording runs, in unit (“s”, “ms”, “us”, “ns”).

Free-function form of obj.duration(*args, **kwargs).

eventcv.efast(obj, *args, **kwargs)

eFAST event corner detector (Mueggler et al., BMVC 2017). Keeps the events sitting on a

Free-function form of obj.efast(*args, **kwargs).

eventcv.event_drop(obj, *args, **kwargs)

Drops each event independently with probability p.

Free-function form of obj.event_drop(*args, **kwargs).

eventcv.event_rate(obj, *args, **kwargs)

Activity over time, binned into fixed intervals.

Free-function form of obj.event_rate(*args, **kwargs).

eventcv.filter_polarity(obj, *args, **kwargs)

Keeps only events of the given polarity (nonzero / True = ON, 0 / False = OFF).

Free-function form of obj.filter_polarity(*args, **kwargs).

eventcv.flatten(obj, *args, **kwargs)

Calls obj.flatten(...).

Free-function form of obj.flatten(*args, **kwargs).

eventcv.flip_x(obj, *args, **kwargs)

Mirrors horizontally (x → width-1-x).

Free-function form of obj.flip_x(*args, **kwargs).

eventcv.flip_y(obj, *args, **kwargs)

Mirrors vertically (y → height-1-y).

Free-function form of obj.flip_y(*args, **kwargs).

eventcv.harris_corners(obj, *args, **kwargs)

Harris corner score on the Surface of Active Events: keeps events whose normalised Harris

Free-function form of obj.harris_corners(*args, **kwargs).

eventcv.hot_pixel_filter(obj, *args, **kwargs)

Hot-pixel removal: drops pixels whose event count exceeds mean + n_std·std over the

Free-function form of obj.hot_pixel_filter(*args, **kwargs).

eventcv.invert_polarity(obj, *args, **kwargs)

Flips every event’s polarity.

Free-function form of obj.invert_polarity(*args, **kwargs).

eventcv.iwe(obj, *args, **kwargs)

Builds the image of warped events for an explicit motion — the picture the objective scores.

Free-function form of obj.iwe(*args, **kwargs).

eventcv.mask(obj, *args, **kwargs)

Keeps events inside the region of interest: a (H, W) boolean mask (or 8-bit map, where

Free-function form of obj.mask(*args, **kwargs).

eventcv.mcts(obj, *args, **kwargs)

Multi-channel time surface (SuperEvent) — per polarity, one linear-decay channel per time

Free-function form of obj.mcts(*args, **kwargs).

eventcv.normalize_time(obj, *args, **kwargs)

Shifts timestamps so the earliest event starts at zero.

Free-function form of obj.normalize_time(*args, **kwargs).

eventcv.numpy(obj, *args, **kwargs)

Calls obj.numpy(...).

Free-function form of obj.numpy(*args, **kwargs).

eventcv.optical_flow(obj, *args, **kwargs)

Dense Lucas-Kanade optical flow on the time surface.

Free-function form of obj.optical_flow(*args, **kwargs).

eventcv.pixel_dropout(obj, *args, **kwargs)

Silences a random p fraction of pixels for the whole stream (dead-pixel simulation).

Free-function form of obj.pixel_dropout(*args, **kwargs).

eventcv.pset(obj, *args, **kwargs)

Calls obj.pset(...).

Free-function form of obj.pset(*args, **kwargs).

eventcv.random_crop(obj, *args, **kwargs)

Takes a random width × height crop; a window at least as large as the sensor is a no-op.

Free-function form of obj.random_crop(*args, **kwargs).

eventcv.random_flip_x(obj, *args, **kwargs)

Mirrors left-right with probability p.

Free-function form of obj.random_flip_x(*args, **kwargs).

eventcv.random_flip_y(obj, *args, **kwargs)

Mirrors top-bottom with probability p.

Free-function form of obj.random_flip_y(*args, **kwargs).

eventcv.random_polarity_flip(obj, *args, **kwargs)

Inverts every polarity with probability p (all events, not a random subset).

Free-function form of obj.random_polarity_flip(*args, **kwargs).

eventcv.raw_image(obj, *args, **kwargs)

The raw polarity image of this stream as an (H, W, 3) uint8 array — what

Free-function form of obj.raw_image(*args, **kwargs).

eventcv.redblue(obj, *args, **kwargs)

White-background red/blue counts, independently percentile-normalized per polarity.

Free-function form of obj.redblue(*args, **kwargs).

eventcv.refractory_filter(obj, *args, **kwargs)

Refractory-period filter: suppresses a pixel’s events for dt after it fires.

Free-function form of obj.refractory_filter(*args, **kwargs).

eventcv.resize(obj, *args, **kwargs)

Event-domain resize to a width`×`height grid (rebinned, not interpolated).

Free-function form of obj.resize(*args, **kwargs).

eventcv.rotate90(obj, *args, **kwargs)

Rotates by k * 90° clockwise (quarter turns swap the sensor dims).

Free-function form of obj.rotate90(*args, **kwargs).

eventcv.scale(obj, *args, **kwargs)

Scales the sensor by (sx, sy).

Free-function form of obj.scale(*args, **kwargs).

eventcv.slice(obj, *args, **kwargs)

One slice. With a positional index n (requires open(dt_ms=…) or

Free-function form of obj.slice(*args, **kwargs).

eventcv.slice_count(obj, *args, **kwargs)

Events whose index lies in [i0, i1) (clamped to the file). Rendered to the reader’s

Free-function form of obj.slice_count(*args, **kwargs).

eventcv.sort_by_time(obj, *args, **kwargs)

Returns a copy reordered by ascending timestamp (stable).

Free-function form of obj.sort_by_time(*args, **kwargs).

eventcv.spatial_jitter(obj, *args, **kwargs)

Jitters each event’s position by a gaussian offset of standard deviation sigma pixels.

Free-function form of obj.spatial_jitter(*args, **kwargs).

eventcv.tencode(obj, *args, **kwargs)

Calls obj.tencode(...).

Free-function form of obj.tencode(*args, **kwargs).

eventcv.time_jitter(obj, *args, **kwargs)

Jitters each event’s timestamp by a gaussian offset, re-sorting afterwards.

Free-function form of obj.time_jitter(*args, **kwargs).

eventcv.time_reversal(obj, *args, **kwargs)

Plays the stream backwards with probability p, inverting polarity to match.

Free-function form of obj.time_reversal(*args, **kwargs).

eventcv.time_scale(obj, *args, **kwargs)

Scales every timestamp by factor (rounded).

Free-function form of obj.time_scale(*args, **kwargs).

eventcv.time_shift(obj, *args, **kwargs)

Shifts every timestamp by dt. The positional form is microseconds; dt_s/dt_ms/dt_us/

Free-function form of obj.time_shift(*args, **kwargs).

eventcv.time_span(obj, *args, **kwargs)

The recording’s (first, last) timestamp in unit — “s”, “ms” (the default), “us”,

Free-function form of obj.time_span(*args, **kwargs).

eventcv.time_window(obj, *args, **kwargs)

Keeps events whose timestamp lies in the half-open window [t0, t1). Positional bounds are

Free-function form of obj.time_window(*args, **kwargs).

eventcv.translate(obj, *args, **kwargs)

Translates by (dx, dy); events shifted off the sensor are dropped.

Free-function form of obj.translate(*args, **kwargs).

eventcv.transpose(obj, *args, **kwargs)

Reflects across the main diagonal ((x, y) → (y, x)); swaps the sensor dims.

Free-function form of obj.transpose(*args, **kwargs).

eventcv.tsurf(obj, *args, **kwargs)

Calls obj.tsurf(...).

Free-function form of obj.tsurf(*args, **kwargs).

eventcv.undistort(obj, *args, **kwargs)

Rectifies events with a Camera’s intrinsics + distortion (lens undistortion).

Free-function form of obj.undistort(*args, **kwargs).

eventcv.undistort_point(obj, *args, **kwargs)

Maps a distorted pixel (u, v) to its undistorted location.

Free-function form of obj.undistort_point(*args, **kwargs).

eventcv.view(obj, *args, **kwargs)

Opens the interactive viewer on this stream. Pass a representation name to choose what

Free-function form of obj.view(*args, **kwargs).

eventcv.voxel(obj, *args, **kwargs)

Calls obj.voxel(...).

Free-function form of obj.voxel(*args, **kwargs).

eventcv.warp_affine(obj, *args, **kwargs)

Applies a 2×3 affine matrix [[a,b,c],[d,e,f]] (rounded, no interpolation).

Free-function form of obj.warp_affine(*args, **kwargs).

eventcv.warp_perspective(obj, *args, **kwargs)

Applies a 3×3 perspective (homography) matrix.

Free-function form of obj.warp_perspective(*args, **kwargs).

eventcv.windows(obj, *args, **kwargs)

Lazy iterator of consecutive windows: each is [start, start + span_ms) and

Free-function form of obj.windows(*args, **kwargs).

eventcv.with_repr(obj, *args, **kwargs)

Returns a new reader over the same file that renders each slice with repr (unset

Free-function form of obj.with_repr(*args, **kwargs).