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 <vibe@mistral.ai>
This commit is contained in:
2026-06-08 11:47:19 +08:00
parent 1369edaad7
commit 02d429282e
2 changed files with 50 additions and 3 deletions

View File

@@ -71,9 +71,14 @@ class EventProcessor:
frame: np.ndarray, shape (H, W) or (H, W, C), uint8 or float. frame: np.ndarray, shape (H, W) or (H, W, C), uint8 or float.
Returns: Returns:
events_binary: np.ndarray (H, W), values in {-1, 0, +1} When threshold > 0:
events_strength: np.ndarray (H, W), values in [-1, 1] events_binary: np.ndarray (H, W), values in {-1, 0, +1}
event_count: int, number of non-zero events 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) brightness = self._to_grayscale(frame)
@@ -81,10 +86,18 @@ class EventProcessor:
if self.prev_brightness is None: if self.prev_brightness is None:
self.prev_brightness = brightness self.prev_brightness = brightness
h, w = brightness.shape 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 return np.zeros((h, w), dtype=np.int8), np.zeros((h, w), dtype=np.float32), 0
change = self._compute_change(brightness) 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: if self.auto_threshold:
self._update_auto_threshold(change) self._update_auto_threshold(change)

View File

@@ -34,6 +34,7 @@ from src.velocity_prediction.utils import (
R_ODOM_TO_BODY, R_ODOM_TO_BODY,
) )
from src.velocity_prediction.config import DATASET_ROOT, VELOCITY_MEAN, VELOCITY_STD from src.velocity_prediction.config import DATASET_ROOT, VELOCITY_MEAN, VELOCITY_STD
from src.event_utils import EventProcessor
# ──────────────────────────── Data loading ──────────────────────────── # ──────────────────────────── Data loading ────────────────────────────
@@ -140,6 +141,8 @@ def draw_pose_overlay(
euler: np.ndarray, euler: np.ndarray,
frame_idx: int, frame_idx: int,
ts: float, ts: float,
events: np.ndarray | None = None,
show_events: bool = False,
): ):
""" """
Draw body-frame pose and velocity information onto the image. Draw body-frame pose and velocity information onto the image.
@@ -285,6 +288,21 @@ def draw_pose_overlay(
cv2.LINE_AA, 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 return display
@@ -297,6 +315,7 @@ def create_video(
fps: float = 30.0, fps: float = 30.0,
max_frames: int | None = None, max_frames: int | None = None,
show: bool = False, show: bool = False,
show_events: bool = False,
): ):
""" """
Read scene data, overlay pose info, and write to video file (or show). 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 for this scene
reset_attitude_offset() 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 # Get dimensions from first frame
h, w = frames[0]["img"].shape h, w = frames[0]["img"].shape
@@ -334,6 +356,12 @@ def create_video(
for i, frame_data in enumerate(frames): for i, frame_data in enumerate(frames):
q_raw = frame_data["pose"][3:7] # [qx, qy, qz, qw] world→odom 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 vector (pitch & roll only, no yaw) — matches DiffPhysDrone
body_up = body_up_vector_np(q_raw) # (3,) unit vector body_up = body_up_vector_np(q_raw) # (3,) unit vector
@@ -354,6 +382,8 @@ def create_video(
euler=euler_deg, euler=euler_deg,
frame_idx=i, frame_idx=i,
ts=frame_data["ts"], ts=frame_data["ts"],
events=events_binary,
show_events=show_events,
) )
if show: if show:
@@ -404,6 +434,9 @@ def main():
parser.add_argument( parser.add_argument(
"--show", action="store_true", help="Display on screen instead of saving video" "--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() args = parser.parse_args()
# Collect scenes to process # Collect scenes to process
@@ -436,6 +469,7 @@ def main():
fps=args.fps, fps=args.fps,
max_frames=args.max_frames, max_frames=args.max_frames,
show=args.show, show=args.show,
show_events=args.show_events,
) )