| """Display utilities for heatmaps and colormaps.""" |
| import numpy as np |
| import cv2 |
|
|
| from config.constants import COLORMAPS, COLORMAP_N_SAMPLES |
|
|
|
|
| def cv_colormap_to_plotly_colorscale(colormap_name, n_samples=None): |
| """Build a Plotly colorscale from OpenCV colormap so UI matches download/PDF exactly.""" |
| n = n_samples or COLORMAP_N_SAMPLES |
| cv2_cmap = COLORMAPS.get(colormap_name, cv2.COLORMAP_JET) |
| gradient = np.linspace(0, 255, n, dtype=np.uint8).reshape(1, -1) |
| rgb = cv2.applyColorMap(gradient, cv2_cmap) |
| rgb = cv2.cvtColor(rgb, cv2.COLOR_BGR2RGB) |
| scale = [] |
| for i in range(n): |
| r, g, b = rgb[0, i] |
| scale.append([i / (n - 1), f"rgb({r},{g},{b})"]) |
| return scale |
|
|
|
|
| def build_range_colorscale(colormap_name, clip_min, clip_max, n_range_samples=32): |
| """ |
| Build a Plotly colorscale for Range mode: normal gradient in [clip_min, clip_max], |
| the "zero" color everywhere else (0 → clip_min and clip_max → 1). |
| """ |
| cv2_cmap = COLORMAPS.get(colormap_name, cv2.COLORMAP_JET) |
|
|
| zero_px = np.array([[0]], dtype=np.uint8) |
| zero_rgb = cv2.applyColorMap(zero_px, cv2_cmap) |
| zero_rgb = cv2.cvtColor(zero_rgb, cv2.COLOR_BGR2RGB) |
| zr, zg, zb = zero_rgb[0, 0] |
| zero_color = f"rgb({zr},{zg},{zb})" |
|
|
| eps = 0.0005 |
| scale = [] |
|
|
| scale.append([0.0, zero_color]) |
| if clip_min > eps: |
| scale.append([clip_min - eps, zero_color]) |
|
|
| positions = np.linspace(clip_min, clip_max, n_range_samples) |
| pixel_vals = np.clip((positions * 255).astype(np.uint8), 0, 255).reshape(1, -1) |
| rgb = cv2.applyColorMap(pixel_vals, cv2_cmap) |
| rgb = cv2.cvtColor(rgb, cv2.COLOR_BGR2RGB) |
| for i, pos in enumerate(positions): |
| r, g, b = rgb[0, i] |
| scale.append([float(pos), f"rgb({r},{g},{b})"]) |
|
|
| if clip_max < 1.0 - eps: |
| scale.append([clip_max + eps, zero_color]) |
| scale.append([1.0, zero_color]) |
|
|
| return scale |
|
|
|
|
| def apply_display_scale(heatmap, mode, min_percentile=0, max_percentile=100, |
| clip_min=0, clip_max=1, clip_bounds=False): |
| """ |
| Apply display scaling. Display only—does not change underlying values. |
| - Default: full 0–1 range as-is. |
| - Range: keep original values inside [clip_min, clip_max]. |
| clip_bounds=False → zero out outside. clip_bounds=True → clamp to bounds. |
| - Rescale: map [clip_min, clip_max] → [0, 1]. |
| clip_bounds=False → zero out outside. clip_bounds=True → clamp to bounds first. |
| """ |
| if mode == "Default" or mode == "Auto" or mode == "Full": |
| return np.clip(heatmap, 0, 1).astype(np.float32) |
| if mode == "Percentile": |
| pmin = float(np.percentile(heatmap, min_percentile)) |
| pmax = float(np.percentile(heatmap, max_percentile)) |
| if pmax > pmin: |
| out = (heatmap.astype(np.float32) - pmin) / (pmax - pmin) |
| return np.clip(out, 0, 1).astype(np.float32) |
| return np.clip(heatmap, 0, 1).astype(np.float32) |
| if mode == "Range" or mode == "Filter" or mode == "Threshold": |
| |
| vmin, vmax = float(clip_min), float(clip_max) |
| if vmax > vmin: |
| h = heatmap.astype(np.float32) |
| if clip_bounds: |
| return np.clip(h, vmin, vmax).astype(np.float32) |
| mask = (h >= vmin) & (h <= vmax) |
| out = np.zeros_like(h) |
| out[mask] = (h[mask] - vmin) / (vmax - vmin) |
| return np.clip(out, 0, 1).astype(np.float32) |
| return np.clip(heatmap, 0, 1).astype(np.float32) |
| if mode == "Rescale": |
| vmin, vmax = float(clip_min), float(clip_max) |
| if vmax > vmin: |
| h = heatmap.astype(np.float32) |
| if clip_bounds: |
| clamped = np.clip(h, vmin, vmax) |
| return ((clamped - vmin) / (vmax - vmin)).astype(np.float32) |
| mask = (h >= vmin) & (h <= vmax) |
| out = np.zeros_like(h) |
| out[mask] = (h[mask] - vmin) / (vmax - vmin) |
| return np.clip(out, 0, 1).astype(np.float32) |
| return np.clip(heatmap, 0, 1).astype(np.float32) |
| return np.clip(heatmap, 0, 1).astype(np.float32) |
|
|