| """ |
| explainability/spectral_heatmap.py |
| ------------------------------------ |
| Spectral Heatmap Visualization for FFT/DCT Branch. |
| STATUS: COMPLETE |
| |
| Converts the FFT log-magnitude spectrum from the spectral branch |
| into a colorized heatmap suitable for display in the web UI. |
| |
| Also generates a side-by-side comparison of: |
| - Log-magnitude spectrum |
| - High-frequency anomaly regions highlighted |
| |
| Output: |
| - heatmap_b64 : base64-encoded JPEG of the spectral heatmap |
| - annotated_b64 : base64-encoded JPEG with anomaly rings annotated |
| """ |
|
|
| import numpy as np |
| import cv2 |
| from utils.image_utils import encode_image_to_base64 |
|
|
|
|
| def render_spectral_heatmap( |
| spectrum_map: np.ndarray, |
| img: np.ndarray, |
| ) -> dict: |
| """ |
| Render the FFT spectrum as a colored heatmap and annotate anomaly peaks. |
| |
| Args: |
| spectrum_map : (H, W) float32 in [0, 1] β from spectral_branch |
| img : (H, W, 3) float32 in [0, 1] β original image |
| |
| Returns: |
| dict with: |
| "spectrum_b64" : base64 JPEG of raw spectrum heatmap |
| "annotated_b64" : base64 JPEG with anomaly rings drawn |
| """ |
| H, W = spectrum_map.shape |
|
|
| |
| spec_u8 = (spectrum_map * 255).astype(np.uint8) |
| spec_colored = cv2.applyColorMap(spec_u8, cv2.COLORMAP_INFERNO) |
| spec_colored_rgb = cv2.cvtColor(spec_colored, cv2.COLOR_BGR2RGB) |
| spectrum_b64 = encode_image_to_base64(spec_colored_rgb) |
|
|
| |
| annotated = spec_colored_rgb.copy() |
| cy, cx = H // 2, W // 2 |
|
|
| |
| half_diag = int(min(H, W) * 0.5) |
| radii = [ |
| ("LF", int(half_diag * 0.10), (0, 200, 0)), |
| ("MF", int(half_diag * 0.25), (255, 200, 0)), |
| ("HF", int(half_diag * 0.45), (255, 50, 50)), |
| ] |
|
|
| for label, r, color in radii: |
| cv2.circle(annotated, (cx, cy), r, color, 1) |
| cv2.putText( |
| annotated, label, |
| (cx + r + 3, cy), |
| cv2.FONT_HERSHEY_SIMPLEX, 0.4, color, 1, cv2.LINE_AA |
| ) |
|
|
| |
| cv2.circle(annotated, (cx, cy), 3, (255, 255, 255), -1) |
|
|
| |
| y_idx, x_idx = np.ogrid[:H, :W] |
| dist = np.sqrt((y_idx - cy) ** 2 + (x_idx - cx) ** 2) |
| hf_mask = (dist > half_diag * 0.15) & (dist < half_diag * 0.45) |
| hf_spectrum = spectrum_map * hf_mask |
|
|
| |
| flat_idx = np.argsort(hf_spectrum.ravel())[::-1][:5] |
| peak_coords = np.unravel_index(flat_idx, hf_spectrum.shape) |
|
|
| for py, px in zip(peak_coords[0], peak_coords[1]): |
| if hf_spectrum[py, px] > 0.5: |
| cv2.drawMarker( |
| annotated, (px, py), (0, 255, 255), |
| cv2.MARKER_CROSS, 8, 1, cv2.LINE_AA |
| ) |
|
|
| annotated_b64 = encode_image_to_base64(annotated) |
|
|
| return { |
| "spectrum_b64": spectrum_b64, |
| "annotated_b64": annotated_b64, |
| } |
|
|
|
|
| def render_noise_map(noise_map: np.ndarray) -> str: |
| """ |
| Render the diffusion branch's residual noise map as a colorized heatmap. |
| |
| Args: |
| noise_map : (H, W) float32 in [0, 1] |
| |
| Returns: |
| base64-encoded JPEG string |
| """ |
| noise_u8 = (np.clip(noise_map, 0, 1) * 255).astype(np.uint8) |
| colored = cv2.applyColorMap(noise_u8, cv2.COLORMAP_VIRIDIS) |
| colored_rgb = cv2.cvtColor(colored, cv2.COLOR_BGR2RGB) |
| return encode_image_to_base64(colored_rgb) |
|
|
|
|
| def render_edge_map(edge_map: np.ndarray) -> str: |
| """ |
| Render the edge branch's edge magnitude map as a colorized heatmap. |
| |
| Args: |
| edge_map : (H, W) float32 in [0, 1] |
| |
| Returns: |
| base64-encoded JPEG string |
| """ |
| edge_u8 = (np.clip(edge_map, 0, 1) * 255).astype(np.uint8) |
| colored = cv2.applyColorMap(edge_u8, cv2.COLORMAP_BONE) |
| colored_rgb = cv2.cvtColor(colored, cv2.COLOR_BGR2RGB) |
| return encode_image_to_base64(colored_rgb) |
|
|