Coverage for src/evutils/transforms/functional/_spatial.py: 93%
28 statements
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-18 05:24 +0000
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-18 05:24 +0000
1"""Spatial functional transforms (flips, jitter)."""
2import math
4import numpy as np
5from evutils.jit import lazy_njit
7from ._common import apply_kernel
9@lazy_njit
10def _flip_lr_jit(t, x, y, p, width: int):
11 """Mirror x about the sensor width: ``x' = width - 1 - x``."""
12 # Promote through int64 so the uint16 subtraction can't wrap around.
13 new_x = (np.int64(width) - 1 - x.astype(np.int64)).astype(np.uint16)
14 return t, new_x, y, p
16def flip_lr(events, sensor_size):
17 """Flip events horizontally: ``x' = width - 1 - x``.
19 Parameters
20 ----------
21 events : np.ndarray or EventArray
22 Events to flip.
23 sensor_size : tuple
24 ``(W, H, P)`` sensor size; only the width ``W`` is used.
26 Returns
27 -------
28 np.ndarray or EventArray
29 Horizontally flipped events, in their original container type.
30 """
31 return apply_kernel(events, _flip_lr_jit, int(sensor_size[0]))
33@lazy_njit
34def _spatial_jitter_jit(t, x, y, p, width: int, height: int,
35 var_x: float, var_y: float, sigma_xy: float,
36 clip_outliers: bool):
37 """Add correlated Gaussian noise to ``x``/``y``.
39 Numba has no ``multivariate_normal``, so we sample two independent standard
40 normals and correlate them with the Cholesky factor of
41 ``[[var_x, sigma_xy], [sigma_xy, var_y]]``.
42 """
43 n = len(x)
44 l11 = math.sqrt(var_x)
45 l21 = sigma_xy / l11 if l11 > 0.0 else 0.0
46 under = var_y - l21 * l21
47 l22 = math.sqrt(under) if under > 0.0 else 0.0
49 z1 = np.random.normal(0.0, 1.0, n)
50 z2 = np.random.normal(0.0, 1.0, n)
51 new_x = x.astype(np.float64) + l11 * z1
52 new_y = y.astype(np.float64) + (l21 * z1 + l22 * z2)
54 if clip_outliers:
55 keep = (new_x >= 0) & (new_x < width) & (new_y >= 0) & (new_y < height)
56 t, p = t[keep], p[keep]
57 new_x, new_y = new_x[keep], new_y[keep]
59 return t, new_x.astype(np.uint16), new_y.astype(np.uint16), p
61def spatial_jitter(events, sensor_size, var_x=1.0, var_y=1.0, sigma_xy=0.0,
62 clip_outliers=False):
63 """Jitter event coordinates with a 2D Gaussian.
65 Parameters
66 ----------
67 events : np.ndarray or EventArray
68 Events to jitter.
69 sensor_size : tuple
70 ``(W, H, P)`` sensor size, used for clipping.
71 var_x, var_y : float, optional
72 Variances of the jitter in x and y. Default ``1.0``.
73 sigma_xy : float, optional
74 Off-diagonal covariance (diagonal skew). Default ``0.0``.
75 clip_outliers : bool, optional
76 Drop events jittered outside the sensor instead of casting them back
77 (which would wrap the unsigned coordinate). Default ``False``.
79 Returns
80 -------
81 np.ndarray or EventArray
82 Jittered events, in their original container type.
83 """
84 return apply_kernel(events, _spatial_jitter_jit, int(sensor_size[0]),
85 int(sensor_size[1]), float(var_x), float(var_y),
86 float(sigma_xy), bool(clip_outliers))