From de215a27d731d539345f1a56ab533c96ff0d5b06 Mon Sep 17 00:00:00 2001 From: rakadu1 Date: Thu, 9 Apr 2026 14:20:17 +0530 Subject: [PATCH] feat(radar): implement sharp-focus Hann windowing and calibrated Jet RA maps Standardizes the metrology diagnostic suite for high-fidelity focus and consistent dB scaling. Core Improvements: - Signal Processing: Switched Range and Doppler FFTs to Hann windowing, reducing peak width by ~50% for sharper target focus (Iteration 27). - Visualization Style: Implemented a fixed-scale MATLAB-style Jet colormap (-5 to 45 dB). Disabled per-frame auto-scaling to preserve temporal intensity consistency and physical R^-4 decay. - Calibration: Applied a -68.0 dB System Gain Offset in post-processing (calculated via statistical analysis of raw power levels in Iter 28) to align noise floors (~7 dB) and target peaks (~41 dB). - Pipeline: Standardized these focus and calibration settings across both the testbench (test_shenron.py) and packaging pipeline (data_to_mcap.py). - Docs: Verified and restored gemini.md at the repository root as the master project context. This milestone (Iteration 29) finalizes the visual interpretability of the physics-based Shenron radar engine. --- .../sim_radar_utils/radar_processor.py | 4 +- scripts/data_to_mcap.py | 38 ++++++++++++------- scripts/test_shenron.py | 20 ++++++---- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/scripts/ISOLATE/sim_radar_utils/radar_processor.py b/scripts/ISOLATE/sim_radar_utils/radar_processor.py index e2c65b6..47a0682 100644 --- a/scripts/ISOLATE/sim_radar_utils/radar_processor.py +++ b/scripts/ISOLATE/sim_radar_utils/radar_processor.py @@ -22,10 +22,10 @@ cfarCfg = config['CFAR'] class RadarProcessor: def __init__(self): # radar data will be shaped as (# of chirp, # of sample, # of antenna) - self.rangeWin = np.tile(signal.windows.blackmanharris(radarCfg['N']), (radarCfg['Np'], radarCfg['NrChn'], 1)) + self.rangeWin = np.tile(signal.windows.hann(radarCfg['N']), (radarCfg['Np'], radarCfg['NrChn'], 1)) self.rangeWin = np.transpose(self.rangeWin, (2, 0, 1)) - self.velWin = np.tile(signal.windows.blackmanharris(radarCfg['Np']), (radarCfg['N'], radarCfg['NrChn'], 1)) + self.velWin = np.tile(signal.windows.hann(radarCfg['Np']), (radarCfg['N'], radarCfg['NrChn'], 1)) self.velWin = np.transpose(self.velWin, (0, 2, 1)) rangeRes = fftCfg['c0'] / (2*(radarCfg['fStop'] - radarCfg['fStrt'])) diff --git a/scripts/data_to_mcap.py b/scripts/data_to_mcap.py index 5ffe9e1..88aaf5d 100644 --- a/scripts/data_to_mcap.py +++ b/scripts/data_to_mcap.py @@ -69,14 +69,19 @@ FOXGLOVE_PCL_SCHEMA = { } } -def render_heatmap(data, cmap='viridis'): +def render_heatmap(data, cmap='viridis', vmin=None, vmax=None): """Convert 2D array to colormapped B64 PNG with guide-compliant normalization.""" - # Step 6: Normalization [0, 1] relative to current frame - d_min, d_max = np.min(data), np.max(data) - if d_max > d_min: - norm = (data - d_min) / (d_max - d_min) + # Step 6: Normalization [0, 1] + # If vmin/vmax are provided, use fixed scaling to preserve physical intensity. + # Otherwise, fall back to relative normalization (relative to current frame). + if vmin is not None and vmax is not None: + norm = np.clip((data - vmin) / (vmax - vmin), 0, 1) else: - norm = np.zeros_like(data) + d_min, d_max = np.min(data), np.max(data) + if d_max > d_min: + norm = (data - d_min) / (d_max - d_min) + else: + norm = np.zeros_like(data) # Step 7: Apply Radar-style Colormap (Blue-style) # Using matplotlib.cm API for consistency with this script's imports @@ -97,17 +102,21 @@ def postprocess_ra(ra_heatmap, range_axis, smooth_sigma=1.0): # 1. Clutter removal (subtract per-range-bin mean to suppress static ground) clutter = np.mean(ra_heatmap, axis=1, keepdims=True) ra = ra_heatmap - (0.8 * clutter) # Subtract context-aware mean - ra = np.clip(ra, 1e-9, None) - # 2. Physics-based dynamic range compression (Linear -> Log) - ra_log = 10 * np.log10(ra) + # Conversion to dB scale with System Gain Calibration (calculated from Iter 28) + SYSTEM_GAIN_OFFSET = 68.0 + ra_db = 10 * np.log10(ra) - SYSTEM_GAIN_OFFSET + + # 3. Fixed dynamic range clipping (-5 to 45 dB) + # This ensures consistent contrast and preserves physical R^-4 decay + ra_db = np.clip(ra_db, -5, 45) - # 3. Optional Gaussian smoothing + # 4. Optional Gaussian smoothing to reduce speckle if smooth_sigma > 0: from scipy.ndimage import gaussian_filter - ra_log = gaussian_filter(ra_log, sigma=smooth_sigma) + ra_db = gaussian_filter(ra_db, sigma=smooth_sigma) - return ra_log + return ra_db def scan_convert_ra(ra_heatmap, range_axis, angle_axis, img_size=512): """ @@ -353,9 +362,10 @@ def convert_folder(folder_path): ra_p = os.path.join(met_dir, "ra", f"{frame_name}.npy") if os.path.exists(ra_p) and range_ax is not None: ra_data = np.load(ra_p) - ra_processed = postprocess_ra(ra_data, range_ax, smooth_sigma=1.0) + # Use smooth_sigma=0.0 for sharp focus (Iter 27 baseline) + ra_processed = postprocess_ra(ra_data, range_ax, smooth_sigma=0.0) bev_data = scan_convert_ra(ra_processed, range_ax, angle_ax, img_size=512) - b64 = render_heatmap(bev_data, cmap='magma') + b64 = render_heatmap(bev_data, cmap='jet', vmin=-5, vmax=45) msg = {"timestamp": {"sec": ts_sec, "nsec": ts_nsec}, "frame_id": "ego_vehicle", "format": "png", "data": b64} writer.add_message(met_ra_id, log_time=ts_ns, data=json.dumps(msg).encode(), publish_time=ts_ns) diff --git a/scripts/test_shenron.py b/scripts/test_shenron.py index b37d696..a7490ef 100644 --- a/scripts/test_shenron.py +++ b/scripts/test_shenron.py @@ -124,15 +124,21 @@ def postprocess_ra(ra_heatmap, range_axis, smooth_sigma=1.0): ra = np.clip(ra, 1e-9, None) # 2. Physics-based dynamic range compression (Linear -> Log) - # We do this AFTER clutter removal but BEFORE normalization - ra_log = 10 * np.log10(ra) + # Conversion to dB scale with System Gain Calibration (calculated from Iter 28) + # This offset maps raw physical power to the diagnostic visual range. + SYSTEM_GAIN_OFFSET = 68.0 + ra_db = 10 * np.log10(ra) - SYSTEM_GAIN_OFFSET + + # 3. Fixed dynamic range clipping (-5 to 45 dB) + # This ensures consistent contrast and preserves physical R^-4 decay + ra_db = np.clip(ra_db, -5, 45) - # 3. Optional Gaussian smoothing to reduce speckle + # 4. Optional Gaussian smoothing to reduce speckle if smooth_sigma > 0: from scipy.ndimage import gaussian_filter - ra_log = gaussian_filter(ra_log, sigma=smooth_sigma) + ra_db = gaussian_filter(ra_db, sigma=smooth_sigma) - return ra_log + return ra_db def scan_convert_ra(ra_heatmap, range_axis, angle_axis, img_size=512): @@ -446,10 +452,10 @@ def run_testbench(iter_name): if axes is not None: # Apply full post-processing chain (log, R² compensation, clutter, normalize, smooth) - ra_processed = postprocess_ra(ra_data, axes['range_axis'], smooth_sigma=1.0) + ra_processed = postprocess_ra(ra_data, axes['range_axis'], smooth_sigma=0.0) # Disabled smoothing as per focus fix # Polar Sector BEV plot — geometrically accurate bev_data = scan_convert_ra(ra_processed, axes['range_axis'], axes['angle_axis'], img_size=512) - b64 = render_heatmap(bev_data, cmap='viridis') + b64 = render_heatmap(bev_data, cmap='jet', vmin=-5, vmax=45) else: # Fallback: rectangular log plot (no axis info available) b64 = render_heatmap(np.log10(np.flipud(ra_data) + 1e-9), cmap='magma')