Coverage for src/evutils/vis/plot3d.py: 33%
79 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"""Matplotlib 3D visualization utilities for event data.
3This module provides functions to plot event streams, 3D histograms,
4and time surfaces using matplotlib's 3D plotting capabilities.
6``cv2`` and ``matplotlib`` are imported lazily, inside the functions that
7use them, so ``import evutils.vis`` (and importing this module) stays cheap
8and does not drag in OpenCV / the full matplotlib stack.
9"""
10from __future__ import annotations
12from typing import TYPE_CHECKING, Optional, Union
14import numpy as np
16if TYPE_CHECKING:
17 from matplotlib.colors import Colormap
18 from matplotlib.figure import Figure
19 from mpl_toolkits.mplot3d import Axes3D
21def plot_3d(events: np.ndarray,
22 width: int =1280,
23 height: int = 720,
24 colormap: Union[str, Colormap] ='Spectral',
25 time_scale: float = 1000.0,
26 time_unit: str = 'ms',
27 fig: Optional[Figure] = None,
28 ax: Optional[Axes3D] = None) -> tuple[Optional[Figure], Optional[Axes3D]]:
29 """Plot a 3D scatter plot of events.
31 Parameters
32 ----------
33 events : np.ndarray
34 Array of events with fields 't', 'x', 'y', and 'p'.
35 width : int, optional
36 Width of the event frame, by default 1280.
37 height : int, optional
38 Height of the event frame, by default 720.
39 colormap : Union[str, Colormap], optional
40 Colormap to use for the event polarities, by default 'Spectral'.
41 fig : Optional[plt.Figure], optional
42 Matplotlib figure to plot on, by default None (a new figure will be created).
43 ax : Optional[Axes3D], optional
44 Matplotlib 3D axis to plot on, by default None (a new axis will be created).
46 Returns
47 -------
48 tuple
49 A tuple containing the figure and axis objects.
51 Raises
52 ------
53 ValueError
54 If the colormap is not found or invalid.
56 """
57 import matplotlib.pyplot as plt
58 from mpl_toolkits.mplot3d import Axes3D # noqa: F401 (registers the '3d' projection)
60 ts = events['t'] / time_scale
62 # Create color map: red for polarity=0, blue for polarity=1
63 # Resolve colormap
64 if isinstance(colormap, str):
65 colormap = plt.get_cmap(colormap)
66 if colormap is None:
67 raise ValueError(f"Colormap not found or invalid.")
69 colors = colormap(events['p'].astype(np.float32)) # Normalize polarity to [0, 1] for colormap
71 # Create figure/axis if not provided
72 if ax is None:
73 fig = plt.figure(figsize=(10, 7)) if fig is None else fig
74 ax = fig.add_subplot(111, projection='3d')
76 if len(events) == 0:
77 return fig, ax
79 # Plot as points
80 ax.scatter(events['x'], events['y'], ts, c=colors, s=1, alpha=0.2)
82 # Labels
83 ax.set_xlabel('x')
84 ax.set_ylabel('y')
85 ax.set_zlabel(f'time ({time_unit})')
86 ax.set_title('Event Stream 3D Plot')
87 ax.set_xlim([0, width])
88 ax.set_ylim([height, 0])
89 ax.set_zlim([ts.min(), ts.max()])
90 ax.set_box_aspect([width, height, max(width, height)]) # Aspect ratio
92 return fig, ax
94def plot_3d_histogram(histogram: np.ndarray,
95 down_sample: int = 4,
96 fig: Optional[Figure] = None,
97 ax: Optional[Axes3D] = None) -> tuple[Optional[Figure], Optional[Axes3D]]:
98 """Plot a 3D histogram of events.
100 Parameters
101 ----------
102 histogram : np.ndarray
103 3D histogram array with shape (depth, height, width).
104 down_sample : int, optional
105 Downsampling factor for the histogram, by default 4.
106 fig : Optional[plt.Figure], optional
107 Matplotlib figure to plot on, by default None (a new figure will be created).
108 ax : Optional[Axes3D], optional
109 Matplotlib 3D axis to plot on, by default None (a new axis will be created).
111 Returns
112 -------
113 tuple
114 A tuple containing the figure and axis objects.
116 """
117 import cv2
118 import matplotlib.pyplot as plt
119 from mpl_toolkits.mplot3d import Axes3D # noqa: F401 (registers the '3d' projection)
121 # Create figure/axis if not provided
122 if ax is None:
123 fig = plt.figure(figsize=(10, 7)) if fig is None else fig
124 ax = fig.add_subplot(111, projection='3d')
125 # Create a meshgrid for the histogram
127 assert histogram.ndim == 4, "Histogram must be a 4D array."
128 n_bins, height, width, ch = histogram.shape
130 height_downsampled = height // down_sample
131 width_downsampled = width // down_sample
132 x, y = np.meshgrid(np.linspace(0, width - 1, width_downsampled),
133 np.linspace(0, height - 1, height_downsampled))
136 for bin in range(n_bins):
137 print(f"Plotting bin {bin+1}/{n_bins}")
138 image = histogram[bin]
140 # Downsample the image for better visualization
141 image = cv2.resize(image, (width_downsampled, height_downsampled), interpolation=cv2.INTER_LINEAR)
143 # Conver to RGBA
144 image = cv2.cvtColor(image, cv2.COLOR_BGR2RGBA)
146 # Set the A channel to 0 where the pixel is black
147 image[:, :, 3] = np.where(np.all(image[:, :, :3] == 0, axis=-1), 0, 255)
149 z = np.full(x.shape, bin)
151 # Plot the surface
152 ax.plot_surface(x, y, z, facecolors=image/255, rstride=1, cstride=1, shade=False)
154 # Create a meshgrid for the histogram
156 ax.set_xlabel('x')
157 ax.set_ylabel('y')
158 ax.set_zlabel('bins')
159 ax.set_title('Voxel Histogram')
160 ax.set_xlim([0, width])
161 ax.set_ylim([height, 0])
162 ax.set_zlim([0, n_bins])
163 ax.set_box_aspect([width, height, max(width, height)]) # Aspect ratio
165 return fig, ax
167def plot_3d_timesurface(events: np.ndarray,
168 width: int =1280,
169 height: int = 720,
170 tau: int = 10_000,
171 fig: Optional[Figure] = None,
172 ax: Optional[Axes3D] = None) -> tuple[Optional[Figure], Optional[Axes3D]]:
173 """Plot a 3D time surface of events.
175 Parameters
176 ----------
177 events : np.ndarray
178 Array of events with fields 't', 'x', 'y', and 'p'.
179 width : int, optional
180 Width of the event frame, by default 1280.
181 height : int, optional
182 Height of the event frame, by default 720.
183 tau : int, optional
184 Time constant for the time surface exponential decay in microseconds, by default 10_000.
185 fig : Optional[plt.Figure], optional
186 Matplotlib figure to plot on, by default None (a new figure will be created).
187 ax : Optional[Axes3D], optional
188 Matplotlib 3D axis to plot on, by default None (a new axis will be created).
190 Returns
191 -------
192 tuple
193 A tuple containing the figure and axis objects.
195 """
196 import matplotlib.pyplot as plt
197 from mpl_toolkits.mplot3d import Axes3D # noqa: F401 (registers the '3d' projection)
199 ts = events['t'] # Convert timestamps to seconds
201 # Create figure/axis if not provided
202 if ax is None:
203 fig = plt.figure(figsize=(10, 7)) if fig is None else fig
204 ax = fig.add_subplot(111, projection='3d')
206 if len(events) == 0:
207 return fig, ax
209 ts_ref = ts[-1] # Reference timestamp for normalization
211 p_sign = (events['p'].astype(np.float32) * 2 - 1)
213 colors = np.exp(-(ts_ref - ts) / tau) * p_sign # Normalize polarity
215 ts = ts / 1_000 # Convert timestamps to milliseconds
216 ax.scatter(events['x'], events['y'], ts, c=colors, s=1, alpha=0.2)
218 # Labels
219 ax.set_xlabel('x')
220 ax.set_ylabel('y')
221 ax.set_zlabel('time (ms)')
222 ax.set_title('Event Stream 3D Plot')
223 ax.set_xlim([0, width])
224 ax.set_ylim([height, 0])
225 ax.set_zlim([ts.min(), ts.max()])
226 ax.set_box_aspect([width, height, max(width, height)]) # Aspect ratio
228 return fig, ax