Coverage for src/evutils/io/_evt.py: 90%
355 statements
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-15 09:33 +0000
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-15 09:33 +0000
1"""EVT (Prophesee EVT2 / EVT2.1 / EVT3) decoder backed by the native C parser.
3Reads the Prophesee ASCII header from a :class:`ByteSource`, then decodes the
4binary EVT payload into ``EventArray`` chunks using the compiled parser in
5``csrc`` (via :mod:`evutils.io._native_evt`).
7Input strategy:
9* If the source is *mappable* (mmap / in-memory), the whole payload is exposed
10 as a single zero-copy ``uint16`` view and the parser walks it in windows --
11 no per-chunk copy, no vector-group carry across chunk boundaries.
12* Otherwise the remaining stream is slurped into memory once and treated the
13 same way. (Truly incremental streaming for live devices is future work; the
14 parser ABI already supports it via ``result.current``.)
16The Prophesee formats are wired to native parsers, dispatched by the header
17``format`` field: EVT3 (16-bit words), EVT2 / EVT4 (32-bit words) and EVT2.1
18(64-bit words). EVT4 is not a standard Prophesee RAW variant -- it reuses EVT2's
19CD/TIME_HIGH layout with distinct type codes plus vectorised CD; evutils defines
20its own ``% evt 4.0`` header token for a self-consistent round-trip. All formats
21have encoders.
22"""
23from __future__ import annotations
25import io
26import re
27from datetime import datetime
28from typing import Any, TYPE_CHECKING, Dict
30if TYPE_CHECKING:
31 from ..types import TriggerArray
33from .._jit import lazy_njit
34import numpy as np
36from ..types import EventArray
37from .common import EventDecoder, EventEncoder
38from ._native_core import (
39 EventSoABuffers,
40 TriggerSoABuffers,
41 decode_all_soa,
42 events_view,
43 triggers_view,
44 parse_step,
45)
46from ._native_evt import (
47 Evt2Input,
48 Evt2Parser,
49 Evt3Input,
50 Evt3Parser,
51 Evt21Input,
52 Evt21Parser,
53 Evt4Input,
54 Evt4Parser,
55)
56from ._source import ByteSource
58_EMPTY_EVENTS = EventArray.empty()
60# Prophesee sensor generation -> (width, height). Older EVT2 RAWs omit an
61# explicit `format`/`geometry` field, so the geometry has to be inferred from
62# the sensor identity -- the same thing the Metavision SDK does.
63_GEN_RESOLUTION = {
64 "1": (304, 240), "1.0": (304, 240), # Gen1 ATIS
65 "2": (640, 480), "2.0": (640, 480), # Gen2 VGA
66 "3": (640, 480), "3.0": (640, 480), # Gen3 VGA
67 "3.1": (640, 480), # Gen3.1 VGA
68 "4": (1280, 720), "4.0": (1280, 720), # Gen4 HD
69 "4.1": (1280, 720), # Gen4.1 HD
70 "4.2": (1280, 720), # Gen4.2 (IMX636) HD
71}
73# Sensor model name -> (width, height).
74_SENSOR_RESOLUTION = {
75 "imx636": (1280, 720), # Gen4.2 HD
76}
78# system_ID -> (width, height), for headers that carry nothing else. Values
79# observed on Prophesee EVKs: 21-29 are Gen3/Gen3.1 VGA, 40-49 are Gen4.x HD.
80_SYSTEM_ID_RESOLUTION = {
81 21: (640, 480), 22: (640, 480), 23: (640, 480),
82 28: (640, 480), 29: (640, 480),
83 40: (1280, 720), 41: (1280, 720), 42: (1280, 720),
84 48: (1280, 720), 49: (1280, 720),
85}
88def _resolution_from_generation(gen: Any) -> tuple[int, int] | None:
89 """Map a generation string (``"4.2"``, ``"gen31"``, ...) to a resolution."""
90 if gen is None:
91 return None
92 g = str(gen).strip().lower()
93 if g in _GEN_RESOLUTION:
94 return _GEN_RESOLUTION[g]
95 m = re.fullmatch(r"gen(\d)(\d?)", g) # "gen31" -> "3.1", "gen4" -> "4"
96 if m:
97 key = m.group(1) + ("." + m.group(2) if m.group(2) else "")
98 return _GEN_RESOLUTION.get(key)
99 return None
102# Per-format native backend: parser class, zero-copy input wrapper, and the
103# numpy word dtype the binary payload is viewed as.
104_BACKENDS = {
105 "evt3": (Evt3Parser, Evt3Input, np.uint16),
106 "evt2": (Evt2Parser, Evt2Input, np.uint32),
107 "evt21": (Evt21Parser, Evt21Input, np.uint64),
108 "evt4": (Evt4Parser, Evt4Input, np.uint32),
109}
111# Canonical format name -> (`% evt` token, `% format` token). Drives both header
112# emission (encoder writes `% evt 3.0` + `% format EVT3`) and header parsing
113# (decoder maps either token back to the canonical name). Single source of truth
114# for the three Prophesee EVT variants.
115_EVT_FORMATS: dict[str, tuple[str, str]] = {
116 "evt3": ("3.0", "EVT3"),
117 "evt21": ("2.1", "EVT21"),
118 "evt2": ("2.0", "EVT2"),
119 # EVT4 is not a standard Prophesee RAW header variant; there is no public
120 # `% evt 4.0` recording. These tokens are evutils' own convention so the
121 # encoder/decoder round-trip is self-consistent.
122 "evt4": ("4.0", "EVT4"),
123}
125# `% evt <token>` value -> canonical name, e.g. "3.0" -> "evt3".
126_EVT_TOKEN_TO_NAME = {token: name for name, (token, _) in _EVT_FORMATS.items()}
129class EventDecoder_EVT(EventDecoder):
130 """Decode Prophesee EVT2, EVT2.1, and EVT3 streams into ``EventArray`` chunks.
132 Parameters
133 ----------
134 source
135 Byte source to read from.
136 chunk_size
137 Maximum number of events produced per :meth:`read_chunk` call (the
138 native output-buffer capacity). Does not bound the file size.
140 References
141 ----------
142 [1] Prophesee RAW file format
143 https://docs.prophesee.ai/stable/data/file_formats/raw.html
145 """
147 _TAIL_PAD = 8 # >= parser look-ahead padding (EVT3_INPUT_PADDING)
148 SUPPORTS_EXT_TRIGGERS = True
150 def __init__(self, source: ByteSource, chunk_size: int = 1_000_000, read_external_triggers: bool = False):
151 super().__init__(source, chunk_size, read_external_triggers=read_external_triggers)
153 self._format: str | None = None
154 self._header: Dict[str, Any] = {
155 "date": datetime.now(),
156 "evt": None,
157 "format": None,
158 "generation": None,
159 "serial_number": "00000000",
160 "system_id": 49,
161 "camera_integrator_name": "Prophesee",
162 "integrator_name": "Prophesee",
163 "sensor_name": None,
164 "sensor_generation": None,
165 "geometry": None,
166 "plugin_name": None,
167 "plugin_integrator_name": None,
168 }
170 # Filled in init()
171 self._buf: Any = None # keeps the underlying storage alive
172 self._payload_off: int = 0 # byte offset where the binary payload starts
173 self._words: Any = None # uint16 view of the whole payload
174 self._offset: int = 0 # current word offset into _words
175 self._parser: Any = None
178 # ------------------------------------------------------------------ #
179 # Header
180 # ------------------------------------------------------------------ #
181 def _parse_header(self, buf: Any) -> int:
182 """Scan the leading ``%``-prefixed ASCII header of ``buf`` (a bytes-like).
184 Returns the byte offset of the first non-header byte (start of payload).
185 """
186 mv = memoryview(buf)
187 n = len(mv)
188 off = 0
189 # A header line always starts with "% " (0x25 0x20). The `% end` marker
190 # is optional -- the payload begins at the first line that does *not*
191 # start with "% ", so that two-byte prefix is the real terminator.
192 while off + 1 < n and mv[off] == 0x25 and mv[off + 1] == 0x20:
193 window = bytes(mv[off:off + 8192])
194 rel = window.find(b"\n")
195 if rel < 0:
196 break
197 line = window[:rel]
198 # Exact end-of-header marker. Must not match other keys such as
199 # "% endianness ...".
200 if line.strip() == b"% end":
201 off += rel + 1
202 break
203 self._consume_header_line(line)
204 off += rel + 1
205 return off
207 def _consume_header_line(self, line: bytes) -> None:
208 try:
209 split = line.decode("utf-8").strip().split(" ")
210 except UnicodeDecodeError:
211 return
212 if len(split) < 2:
213 return
214 key = split[1].lower()
215 raw = " ".join(split[2:])
217 value: str | int | datetime = raw
218 try:
219 if key == "date":
220 value = datetime.strptime(raw, "%Y-%m-%d %H:%M:%S")
221 elif key in ("height", "width", "system_id"):
222 value = int(raw)
223 except ValueError:
224 return
225 self._header[key] = value
227 def _finalize_header(self) -> None:
228 """Resolve format / width / height from the parsed header fields."""
229 fmt = self._header.get("format")
230 if isinstance(fmt, str):
231 for s in fmt.split(";"):
232 if s.startswith("height"):
233 self._height = int(s.split("=")[1])
234 elif s.startswith("width"):
235 self._width = int(s.split("=")[1])
236 else:
237 s = s.lower().replace(".", "")
238 if s in _EVT_FORMATS:
239 self._format = s
241 geom = self._header.get("geometry")
242 if isinstance(geom, str):
243 parts = geom.split("x")
244 if len(parts) == 2:
245 self._width = int(parts[0])
246 self._height = int(parts[1])
248 evt = self._header.get("evt")
249 if evt in _EVT_TOKEN_TO_NAME:
250 self._format = _EVT_TOKEN_TO_NAME[evt]
252 if self._format is None:
253 self._format = "evt3" # sensible default for Prophesee RAW
255 # Geometry not stated explicitly (older EVT2 RAWs): infer it from the
256 # sensor identity, the way the Metavision SDK does.
257 if self._width is None or self._height is None:
258 res = self._infer_resolution()
259 if res is not None:
260 self._width, self._height = res
262 if self._width is None or not (0 < self._width <= 2048):
263 self._width = 2048
264 if self._height is None or not (0 < self._height <= 2048):
265 self._height = 2048
267 def _infer_resolution(self) -> tuple[int, int] | None:
268 """Guess (width, height) from the sensor identity fields in the header.
270 Tries, in order of reliability: an explicit sensor model, the
271 ``sensor_generation`` / ``generation`` fields, the generation token
272 embedded in ``plugin_name`` (e.g. ``hal_plugin_gen31_fx3``), and
273 finally the ``system_ID``. Returns ``None`` if nothing matches.
274 """
275 h = self._header
277 name = h.get("sensor_name")
278 if isinstance(name, str):
279 res = _SENSOR_RESOLUTION.get(name.strip().lower())
280 if res:
281 return res
283 for key in ("sensor_generation", "generation"):
284 res = _resolution_from_generation(h.get(key))
285 if res:
286 return res
288 plugin = h.get("plugin_name")
289 if isinstance(plugin, str):
290 low = plugin.lower()
291 for model, res in _SENSOR_RESOLUTION.items():
292 if model in low:
293 return res
294 m = re.search(r"gen(\d)(\d?)", low) # hal_plugin_gen31_fx3 -> "3.1"
295 if m:
296 key = m.group(1) + ("." + m.group(2) if m.group(2) else "")
297 res = _GEN_RESOLUTION.get(key)
298 if res:
299 return res
301 return _SYSTEM_ID_RESOLUTION.get(h.get("system_id"))
303 # ------------------------------------------------------------------ #
304 # Lifecycle
305 # ------------------------------------------------------------------ #
306 def init(self) -> None:
307 """Initialize the EVT reader.
309 Returns
310 -------
311 None
313 """
314 if self._is_initialized:
315 return
317 if self._source.mappable():
318 self._buf = self._source.buffer() # zero-copy, whole file
319 else:
320 self._buf = memoryview(self._source.read(-1)) # slurp the stream
322 self._payload_off = self._parse_header(self._buf)
323 self._finalize_header()
325 if self._format not in _BACKENDS:
326 raise NotImplementedError(
327 f"native decoder does not support format {self._format!r}"
328 )
329 parser_cls, input_cls, word_dtype = _BACKENDS[self._format]
330 self._input_cls = input_cls
331 self._word_dtype = word_dtype
333 # View the binary payload as words of the format's native width. The
334 # payload can be unaligned relative to the word size (the header length
335 # is arbitrary); numpy tolerates this and x86 handles the unaligned
336 # loads in the C parser.
337 itemsize = np.dtype(word_dtype).itemsize
338 n_words = (len(self._buf) - self._payload_off) // itemsize
339 if n_words > 0:
340 self._words = np.frombuffer(
341 self._buf, dtype=word_dtype, count=n_words, offset=self._payload_off
342 )
343 else:
344 self._words = np.empty(0, dtype=word_dtype)
346 self._offset = 0
347 self._parser = parser_cls()
348 cap = int(self._chunk_size)
349 self._events = EventSoABuffers(cap)
350 self._triggers = TriggerSoABuffers(max(cap // 16, 1))
351 self._is_initialized = True
353 @property
354 def _tail_pad(self) -> int:
355 return self._TAIL_PAD if self._format == "evt3" else 0
357 def parse_step(self, events: EventSoABuffers, triggers: TriggerSoABuffers) -> int:
358 """Run the parser once, appending decoded events into ``events``.
360 Advances the internal word offset and sets EOF when the input is drained.
362 Parameters
363 ----------
364 events : EventSoABuffers
365 Buffer to append events to.
366 triggers : TriggerSoABuffers
367 Buffer to append triggers to.
369 Returns
370 -------
371 int
372 Number of events appended.
374 """
375 if not self._is_initialized:
376 self.init()
377 if self._words is None or self._offset >= len(self._words):
378 self._eof = True
379 return 0
380 appended, self._offset = parse_step(
381 self._words, self._offset, self._input_cls, self._parser,
382 events, triggers, tail_pad=self._tail_pad, word_dtype=self._word_dtype,
383 )
384 if self._offset >= len(self._words):
385 self._eof = True
386 return appended
388 def read_chunk(self, delta_t_hint: int | None = None,
389 n_events_hint: int | None = None) -> 'EventArray | tuple[EventArray, TriggerArray]':
390 if not self._is_initialized:
391 self.init()
393 # Nothing left: signal EOF with an empty array (never a stale buffer).
394 if self._words is None or self._offset >= len(self._words):
395 self._eof = True
396 if self.read_external_triggers:
397 from ..types import TriggerArray
398 return _EMPTY_EVENTS, TriggerArray.empty()
399 return _EMPTY_EVENTS
401 ev, tr = self._events, self._triggers
402 ev.reset()
403 tr.reset()
405 # Parse until we produce something or genuinely exhaust the input. A
406 # window can consume words yet emit no events (pure timing packets), so
407 # we must not treat an empty result as EOF unless the input is drained.
408 appended = 0
409 while appended == 0 and self._offset < len(self._words):
410 appended = self.parse_step(ev, tr)
412 n = ev.size
413 ev_view = events_view(ev) if n > 0 else _EMPTY_EVENTS
414 if self.read_external_triggers:
415 from ..types import TriggerArray
416 tr_view = triggers_view(tr) if tr.size > 0 else TriggerArray.empty()
417 return ev_view, tr_view
418 return ev_view
420 # Initial output-capacity estimate (events per input word) for the
421 # single-buffer read_all() path. EVT2 is an exact upper bound (<=1 event per
422 # 32-bit word); EVT3/EVT2.1 vary with vector density, so decode_all_soa grows
423 # the buffer if the estimate is too small.
424 _READ_ALL_EST = {"evt3": 1.0, "evt2": 1.0, "evt21": 1.5, "evt4": 1.0}
426 def read_all(self) -> 'EventArray | tuple[EventArray, TriggerArray]':
427 """Decode the whole remaining payload into one buffer (no per-chunk copy).
429 See :func:`evutils.io._native_core.decode_all_soa`. Note this materialises
430 every event at once; for very large recordings that do not fit in memory,
431 iterate with :meth:`read_chunk` (via ``EventReader``) instead.
432 """
433 if not self._is_initialized:
434 self.init()
436 # decode_all_soa is an events-only fast path; with external triggers
437 # requested, fall back to the chunked base implementation (which
438 # carries the trigger stream through).
439 if self.read_external_triggers:
440 return super().read_all()
442 if self._words is None or self._offset >= len(self._words):
443 self._eof = True
444 return _EMPTY_EVENTS
446 assert self._format is not None # set by init()
447 out, self._offset = decode_all_soa(
448 self._words, self._offset, self._input_cls, self._parser,
449 est_events_per_word=self._READ_ALL_EST.get(self._format, 1.0),
450 tail_pad=self._tail_pad, word_dtype=self._word_dtype,
451 )
452 self._eof = True
453 return out
455 def reset(self) -> None:
456 """Reset the EVT reader to the beginning.
458 Returns
459 -------
460 None
462 """
463 self._offset = 0
464 self._eof = False
465 if self._parser is not None:
466 self._parser.reset()
468 def tell(self) -> int:
469 """Get the current byte offset.
471 Returns
472 -------
473 int
474 Current byte offset.
476 """
477 if not self._is_initialized:
478 return 0
479 word_size = np.dtype(self._word_dtype).itemsize
480 return self._payload_off + self._offset * word_size
482 def close(self) -> None:
483 """Close the EVT reader.
485 Returns
486 -------
487 None
489 """
490 # Drop numpy views into the (possibly mmap-backed) storage so the source
491 # can be closed without BufferError.
492 self._words = None
493 self._buf = None
496# --------------------------------------------------------------------------- #
497# Encoders (EVT3 / EVT2 / EVT2.1 writers)
498#
499# The writers are numba (there is no native encoder yet). They live here with
500# the format module now that _raw.py is gone.
501# --------------------------------------------------------------------------- #
502EVT3_EVT_ADDR_Y = 0x0000
503EVT3_EVT_ADDR_X = 0x2000
504EVT3_VECT_BASE_X = 0x3000
505EVT3_VECT_12 = 0x4000
506EVT3_VECT_8 = 0x5000
507EVT3_EVT_TIME_LOW = 0x6000
508EVT3_CONTINUED_4 = 0x7000
509EVT3_EVT_TIME_HIGH = 0x8000
510EVT3_EXT_TRIGGER = 0xA000
511EVT3_OTHERS = 0xE000
512EVT3_CONTINUED_12 = 0xF000
515@lazy_njit
516def get_raw_evt3_buffer(events: np.ndarray, last_lower12_ts: int, last_upper12_ts: int, last_y: int, master: bool = True) -> tuple[np.ndarray, int, int, int]:
517 """Encode events as EVT3.
519 Parameters
520 ----------
521 events : np.ndarray
522 Array of events to encode.
523 last_lower12_ts : int
524 Last lower 12-bit timestamp.
525 last_upper12_ts : int
526 Last upper 12-bit timestamp.
527 last_y : int
528 Last y coordinate.
529 master : bool, optional
530 Whether this is the master camera, by default True.
532 Returns
533 -------
534 tuple
535 A tuple containing the raw buffer, last lower 12-bit timestamp, last upper 12-bit timestamp, and last y coordinate.
537 """
538 # Pre-allocate large buffer
539 buffer = np.zeros(len(events) * 8, dtype=np.uint8)
541 # Prepare the master/slave bit
542 if master:
543 master_slave = 0x000
544 else:
545 master_slave = 0x800
547 # Current position of the buffer
548 i = 0
550 for ev in events:
551 upper12_ts = (int(ev['t']) & 0x0FFF000) >> 12
552 lower12_ts = int(ev['t']) & 0x00000FFF
554 # EVT_TIME_HIGH - Updates the higher 12-bit portion of the 24-bit time base
555 if upper12_ts != last_upper12_ts:
556 last_upper12_ts = upper12_ts
557 value = EVT3_EVT_TIME_HIGH | (upper12_ts & 0xFFF)
559 buffer[i] = value & 0xFF
560 buffer[i + 1] = (value >> 8) & 0xFF
561 i += 2
563 # EVT_TIME_LOW - Updates the lower 12-bit portion of the 24-bit time base
564 if lower12_ts != last_lower12_ts:
565 last_lower12_ts = lower12_ts
566 value = EVT3_EVT_TIME_LOW | (lower12_ts & 0xFFF)
568 buffer[i] = value & 0xFF
569 buffer[i + 1] = (value >> 8) & 0xFF
570 i += 2
572 # EVT_ADDR_Y - Y coordinate, and system type (master/slave camera)
573 if last_y != ev['y']:
574 last_y = ev['y']
575 value = (EVT3_EVT_ADDR_Y | master_slave | (int(ev['y']) & 0x7FF))
577 buffer[i] = value & 0xFF
578 buffer[i + 1] = (value >> 8) & 0xFF
579 i += 2
581 # EVT_ADDR_X - Single valid event, X coordinate and polarity
582 value = EVT3_EVT_ADDR_X | (int(ev['x']) & 0x7FF) | ((int(ev['p']) & 0x01) << 11)
584 buffer[i] = value & 0xFF
585 buffer[i + 1] = (value >> 8) & 0xFF
586 i += 2
588 return buffer[:i], last_lower12_ts, last_upper12_ts, last_y
591@lazy_njit
592def get_raw_evt2_buffer(events: np.ndarray, last_ts_high: int) -> tuple[np.ndarray, int]:
593 """Encode events as EVT2 (32-bit words).
595 Timestamp is split into a 28-bit high part (EVT_TIME_HIGH word) and a 6-bit
596 low part carried in each CD word. A TIME_HIGH word is emitted only when the
597 high part changes. Layout: type[28:31], ts_low[22:27], x[11:21], y[0:10].
599 Parameters
600 ----------
601 events : np.ndarray
602 Array of events to encode.
603 last_ts_high : int
604 Last high timestamp part.
606 Returns
607 -------
608 tuple
609 A tuple containing the raw buffer and last high timestamp part.
611 """
612 n = len(events)
613 buffer = np.empty(2 * n, dtype=np.uint32) # <= 1 TIME_HIGH + 1 CD per event
614 i = 0
615 for k in range(n):
616 ts = np.int64(events[k]['t'])
617 x = np.int64(events[k]['x']) & 0x7FF
618 y = np.int64(events[k]['y']) & 0x7FF
619 p = np.int64(events[k]['p']) & 0x1
621 ts_high = (ts >> 6) & 0x0FFFFFFF
622 ts_low = ts & 0x3F
624 if ts_high != last_ts_high:
625 buffer[i] = np.uint32((8 << 28) | ts_high) # EVT2_EVT_TIME_HIGH
626 i += 1
627 last_ts_high = int(ts_high)
629 buffer[i] = np.uint32((p << 28) | (ts_low << 22) | (x << 11) | y)
630 i += 1
632 return buffer[:i], last_ts_high
635@lazy_njit
636def get_raw_evt21_buffer(events: np.ndarray, last_ts_high: int) -> tuple[np.ndarray, int]:
637 """Encode events as EVT2.1 (64-bit words, legacy endianness).
639 Same descriptor layout as EVT2 in the low 32 bits (type[28:31],
640 ts_low[22:27], x_base[11:21], y[0:10]); the high 32 bits are a validity
641 bitmask for x_base..x_base+31. This writer emits one event per word (mask
642 with a single bit set at x_base = x) -- valid EVT2.1, not yet vectorised.
644 Parameters
645 ----------
646 events : np.ndarray
647 Array of events to encode.
648 last_ts_high : int
649 Last high timestamp part.
651 Returns
652 -------
653 tuple
654 A tuple containing the raw buffer and last high timestamp part.
656 """
657 n = len(events)
658 buffer = np.empty(2 * n, dtype=np.uint64) # <= 1 TIME_HIGH + 1 CD per event
659 i = 0
660 for k in range(n):
661 ts = np.int64(events[k]['t'])
662 x = np.int64(events[k]['x']) & 0x7FF
663 y = np.int64(events[k]['y']) & 0x7FF
664 p = np.int64(events[k]['p']) & 0x1
666 ts_high = (ts >> 6) & 0x0FFFFFFF
667 ts_low = ts & 0x3F
669 if ts_high != last_ts_high:
670 buffer[i] = np.uint64((8 << 28) | ts_high) # EVT21_EVT_TIME_HIGH
671 i += 1
672 last_ts_high = int(ts_high)
674 desc = (p << 28) | (ts_low << 22) | (x << 11) | y
675 # High 32 bits: validity mask with bit 0 set (single event at x_base=x).
676 buffer[i] = (np.uint64(1) << np.uint64(32)) | np.uint64(desc)
677 i += 1
679 return buffer[:i], last_ts_high
682@lazy_njit
683def get_raw_evt4_buffer(events: np.ndarray, last_ts_high: int) -> tuple[np.ndarray, int]:
684 """Encode events as EVT4 (32-bit words).
686 Same CD / TIME_HIGH bit layout as EVT2 (type[28:31], ts_low[22:27],
687 x[11:21], y[0:10]) but with EVT4's type codes: CD_OFF=0xA / CD_ON=0xB and
688 TIME_HIGH=0xE. One CD word per event (EVT4's vectorised CD_VEC form is a
689 decode-side optimisation and is not emitted here).
691 Parameters
692 ----------
693 events : np.ndarray
694 Array of events to encode.
695 last_ts_high : int
696 Last high timestamp part.
698 Returns
699 -------
700 tuple
701 A tuple containing the raw buffer and last high timestamp part.
703 """
704 n = len(events)
705 buffer = np.empty(2 * n, dtype=np.uint32) # <= 1 TIME_HIGH + 1 CD per event
706 i = 0
707 for k in range(n):
708 ts = np.int64(events[k]['t'])
709 x = np.int64(events[k]['x']) & 0x7FF
710 y = np.int64(events[k]['y']) & 0x7FF
711 p = np.int64(events[k]['p']) & 0x1
713 ts_high = (ts >> 6) & 0x0FFFFFFF
714 ts_low = ts & 0x3F
716 if ts_high != last_ts_high:
717 buffer[i] = np.uint32((0xE << 28) | ts_high) # EVT4_EVT_TIME_HIGH
718 i += 1
719 last_ts_high = int(ts_high)
721 # type = CD_OFF (0xA) for p=0, CD_ON (0xB) for p=1.
722 buffer[i] = np.uint32(((0xA | p) << 28) | (ts_low << 22) | (x << 11) | y)
723 i += 1
725 return buffer[:i], last_ts_high
728class EventEncoder_EVT(EventEncoder):
729 """Encoder for Prophesee RAW/EVT files.
731 Parameters
732 ----------
733 writable
734 Destination stream to write to.
735 width, height : int
736 Frame geometry written into the header.
737 dt : datetime, optional
738 Recording timestamp (defaults to now).
739 serial : str
740 Camera serial number written into the header.
741 format : {"evt3", "evt21", "evt2", "evt4"}
742 Output format. All are supported; EVT2.1 and EVT4 are written one event
743 per word (valid but not vectorised).
745 References
746 ----------
747 [1] Prophesee RAW file format
748 https://docs.prophesee.ai/stable/data/file_formats/raw.html
750 """
752 def __init__(self, writable: io.BufferedWriter, width: int = 1280, height: int = 720,
753 dt: datetime | None = None, serial: str = "00000000", format: str = "evt3"):
754 super().__init__(writable, width, height, dt)
756 format = format.lower().replace(".", "")
757 if format not in _EVT_FORMATS:
758 raise ValueError(f"Unsupported format {format}. Supported formats are {list(_EVT_FORMATS)}")
759 self._format = format
761 self._system_id = 49
763 self._last_upper12_ts = -1
764 self._last_lower12_ts = -1
765 self._last_y = -1
766 self._last_ts_high = -1 # EVT2 / EVT2.1 time-high state
768 self._serial_number = serial
770 self._formatted_datetime = self._dt.strftime("%Y-%m-%d %H:%M:%S")
772 def init(self) -> None:
773 """Initialize the EVT writer.
775 Returns
776 -------
777 None
779 """
780 if self._is_initialized:
781 return
783 # EVT2.1 packs its 64-bit words as two swapped 32-bit halves ("legacy").
784 endianness = "% endianness legacy\n" if self._format == "evt21" else ""
785 self._fd.write(
786f"""% camera_integrator_name Prophesee
787% date {self._formatted_datetime}
788{endianness}% evt {_EVT_FORMATS[self._format][0]}
789% format {_EVT_FORMATS[self._format][1]};height={self._height};width={self._width}
790% generation 4.2
791% geometry {self._width}x{self._height}
792% integrator_name Prophesee
793% plugin_integrator_name Prophesee
794% plugin_name hal_plugin_prophesee
795% sensor_generation 4.2
796% serial_number {self._serial_number}
797% system_ID {self._system_id}
798% end
799""".encode('utf-8'))
800 self._is_initialized = True
802 def write(self, events: 'np.ndarray | EventArray', triggers: 'np.ndarray | TriggerArray | None' = None) -> int:
803 """Write events to the EVT file.
805 Parameters
806 ----------
807 events : np.ndarray or EventArray
808 Array of events to write.
810 Returns
811 -------
812 int
813 Number of written events.
815 """
816 assert self._fd is not None
818 if not self._is_initialized:
819 self.init()
821 # Accept EventArray transparently (SoA -> AoS for the numba encoder).
822 if isinstance(events, EventArray):
823 events = events.to_aos()
825 if self._format == "evt3":
826 buffer, self._last_lower12_ts, self._last_upper12_ts, self._last_y = get_raw_evt3_buffer(
827 events,
828 self._last_lower12_ts,
829 self._last_upper12_ts,
830 self._last_y)
831 elif self._format == "evt2":
832 buffer, self._last_ts_high = get_raw_evt2_buffer(events, self._last_ts_high)
833 elif self._format == "evt21":
834 buffer, self._last_ts_high = get_raw_evt21_buffer(events, self._last_ts_high)
835 elif self._format == "evt4":
836 buffer, self._last_ts_high = get_raw_evt4_buffer(events, self._last_ts_high)
837 else:
838 raise NotImplementedError(
839 f"format {self._format!r} not implemented"
840 )
842 self._n_written_events += len(events)
844 buffer.tofile(self._fd)
846 return len(events)