From 02d429282e27da2adf9f73a327c7b5e02f071aea Mon Sep 17 00:00:00 2001 From: CaoWangrenbo Date: Mon, 8 Jun 2026 11:47:19 +0800 Subject: [PATCH] feat: add --show-events overlay with raw log intensity Visualize raw temporal brightness change (threshold=0, log domain) as green(+)/red(-) gradient overlay proportional to |change|. Supports video output and live display modes. Enables EventProcessor threshold=0 for raw mode without clipping. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- src/event_utils.py | 19 ++++++++++++++++--- visualize/visualize_dataset.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/event_utils.py b/src/event_utils.py index f37c69c..ee3c8cd 100644 --- a/src/event_utils.py +++ b/src/event_utils.py @@ -71,9 +71,14 @@ class EventProcessor: frame: np.ndarray, shape (H, W) or (H, W, C), uint8 or float. Returns: - events_binary: np.ndarray (H, W), values in {-1, 0, +1} - events_strength: np.ndarray (H, W), values in [-1, 1] - event_count: int, number of non-zero events + When threshold > 0: + events_binary: np.ndarray (H, W), values in {-1, 0, +1} + events_strength: np.ndarray (H, W), values in [-1, 1] + event_count: int, number of non-zero events + When threshold == 0 (raw output, no thresholding): + change_raw: np.ndarray (H, W), raw log/linear brightness change (float32) + change_raw: same as above + event_count: int, number of pixels with non-zero change """ brightness = self._to_grayscale(frame) @@ -81,10 +86,18 @@ class EventProcessor: if self.prev_brightness is None: self.prev_brightness = brightness h, w = brightness.shape + if self.threshold == 0: + return np.zeros((h, w), dtype=np.float32), np.zeros((h, w), dtype=np.float32), 0 return np.zeros((h, w), dtype=np.int8), np.zeros((h, w), dtype=np.float32), 0 change = self._compute_change(brightness) + # threshold == 0: raw mode, skip thresholding + if self.threshold == 0: + self.prev_brightness = brightness + change_f32 = change.astype(np.float32) + return change_f32, change_f32, int(np.count_nonzero(change)) + if self.auto_threshold: self._update_auto_threshold(change) diff --git a/visualize/visualize_dataset.py b/visualize/visualize_dataset.py index d92f2e9..13df47b 100644 --- a/visualize/visualize_dataset.py +++ b/visualize/visualize_dataset.py @@ -34,6 +34,7 @@ from src.velocity_prediction.utils import ( R_ODOM_TO_BODY, ) from src.velocity_prediction.config import DATASET_ROOT, VELOCITY_MEAN, VELOCITY_STD +from src.event_utils import EventProcessor # ──────────────────────────── Data loading ──────────────────────────── @@ -140,6 +141,8 @@ def draw_pose_overlay( euler: np.ndarray, frame_idx: int, ts: float, + events: np.ndarray | None = None, + show_events: bool = False, ): """ Draw body-frame pose and velocity information onto the image. @@ -285,6 +288,21 @@ def draw_pose_overlay( cv2.LINE_AA, ) + # ── Event overlay (gradient temporal intensity) ── + if show_events and events is not None: + limit = max(np.abs(events).max(), 1e-6) + norm = np.clip(events / limit, -1.0, 1.0) + pos = norm > 0 + neg = norm < 0 + intensity = np.abs(norm) # [0, 1] magnitude + overlay = np.zeros_like(display, dtype=np.uint8) + # bg = np.ones_like(display, dtype=np.uint8) * 255 + # Color intensity proportional to |norm|: dark → bright + overlay[pos, 1] = (255 * intensity[pos]).astype(np.uint8) # green channel + overlay[neg, 2] = (255 * intensity[neg]).astype(np.uint8) # red channel + # display = cv2.addWeighted(bg, 0.5, overlay, 1.0, 0) + display = cv2.addWeighted(display, 0.5, overlay, 1.0, 0) + return display @@ -297,6 +315,7 @@ def create_video( fps: float = 30.0, max_frames: int | None = None, show: bool = False, + show_events: bool = False, ): """ Read scene data, overlay pose info, and write to video file (or show). @@ -316,6 +335,9 @@ def create_video( # Reset attitude offset for this scene reset_attitude_offset() + # Event processor (threshold=0 → raw temporal intensity) + event_processor = EventProcessor(threshold=0.3, use_log=True) if show_events else None + # Get dimensions from first frame h, w = frames[0]["img"].shape @@ -334,6 +356,12 @@ def create_video( for i, frame_data in enumerate(frames): q_raw = frame_data["pose"][3:7] # [qx, qy, qz, qw] world→odom + # Compute events if enabled + if event_processor is not None: + events_binary, _, _ = event_processor(frame_data["img"]) + else: + events_binary = None + # Body up vector (pitch & roll only, no yaw) — matches DiffPhysDrone body_up = body_up_vector_np(q_raw) # (3,) unit vector @@ -354,6 +382,8 @@ def create_video( euler=euler_deg, frame_idx=i, ts=frame_data["ts"], + events=events_binary, + show_events=show_events, ) if show: @@ -404,6 +434,9 @@ def main(): parser.add_argument( "--show", action="store_true", help="Display on screen instead of saving video" ) + parser.add_argument( + "--show-events", action="store_true", help="Overlay event frames (green=+1, red=-1)" + ) args = parser.parse_args() # Collect scenes to process @@ -436,6 +469,7 @@ def main(): fps=args.fps, max_frames=args.max_frames, show=args.show, + show_events=args.show_events, )