Spaces:
Sleeping
Sleeping
Download visualize.py from CircleStar/segspace_app: direct link, hf CLI and curl.
- Browser
- Download file 7.93 kB
-
https://huggingface.co/spaces/CircleStar/segspace_app/resolve/main/visualize.py
- Command line
-
hf download hf://spaces/CircleStar/segspace_app/visualize.py
-
curl -L -o visualize.py https://huggingface.co/spaces/CircleStar/segspace_app/resolve/main/visualize.py
7.93 kB
| import io | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| from PIL import Image, ImageDraw | |
| from config import CLASS_COLORS, CLASS_NAMES, BAND_NAMES, BAND_DESCRIPTIONS, IGNORE_INDEX, NUM_CLASSES | |
| # โโ Low-level helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def percentile_stretch(x: np.ndarray, low: float = 2.0, high: float = 98.0) -> np.ndarray: | |
| x = x.astype(np.float32) | |
| lo = np.percentile(x, low) | |
| hi = np.percentile(x, high) | |
| if hi <= lo: | |
| hi = lo + 1e-6 | |
| return np.clip((x - lo) / (hi - lo), 0, 1) | |
| def _fig_to_numpy(fig) -> np.ndarray: | |
| buf = io.BytesIO() | |
| fig.savefig(buf, format="png", bbox_inches="tight", dpi=110) | |
| plt.close(fig) | |
| buf.seek(0) | |
| return np.array(Image.open(buf).convert("RGB")) | |
| def _blank_rgb(h: int = 300, w: int = 400) -> np.ndarray: | |
| return np.full((h, w, 3), 220, dtype=np.uint8) | |
| # โโ Composite rendering (full image or patch) โโโโโโโโโโโโโโโโโ | |
| def render_composite(img7: np.ndarray, r: int, g: int, b: int) -> np.ndarray: | |
| """img7: (7, H, W) -> (H, W, 3) uint8.""" | |
| return (np.stack([ | |
| percentile_stretch(img7[r]), | |
| percentile_stretch(img7[g]), | |
| percentile_stretch(img7[b]), | |
| ], axis=-1) * 255).astype(np.uint8) | |
| def render_single_band(img7: np.ndarray, band_idx: int) -> np.ndarray: | |
| """Single band as grayscale RGB.""" | |
| gray = (percentile_stretch(img7[band_idx]) * 255).astype(np.uint8) | |
| return np.stack([gray, gray, gray], axis=-1) | |
| def multispectral_to_rgb(img7: np.ndarray) -> np.ndarray: | |
| """Natural colour composite: H_4/H_3/H_2 -> R/G/B.""" | |
| return render_composite(img7, r=3, g=2, b=1) | |
| # โโ Label markers on full-scene image โโโโโโโโโโโโโโโโโโโโโโโโ | |
| def add_labels_overlay( | |
| base_rgb: np.ndarray, | |
| train_mask: np.ndarray, | |
| val_mask: np.ndarray, | |
| radius: int = 5, | |
| ) -> np.ndarray: | |
| """ | |
| Draw class-coloured markers on base_rgb. | |
| Training labels -> filled squares; validation labels -> circles with white ring. | |
| """ | |
| img = Image.fromarray(base_rgb) | |
| draw = ImageDraw.Draw(img) | |
| H, W = base_rgb.shape[:2] | |
| for cls_idx in range(NUM_CLASSES): | |
| color = tuple(int(c) for c in CLASS_COLORS[cls_idx]) | |
| ys, xs = np.where(train_mask == cls_idx) | |
| for y, x in zip(ys.tolist(), xs.tolist()): | |
| box = [x - radius, y - radius, x + radius, y + radius] | |
| box = [max(0, box[0]), max(0, box[1]), min(W-1, box[2]), min(H-1, box[3])] | |
| draw.rectangle(box, fill=color, outline=(255, 255, 255)) | |
| ys, xs = np.where(val_mask == cls_idx) | |
| for y, x in zip(ys.tolist(), xs.tolist()): | |
| outer = [x-radius-2, y-radius-2, x+radius+2, y+radius+2] | |
| inner = [x-radius, y-radius, x+radius, y+radius ] | |
| outer = [max(0, v) for v in outer] | |
| draw.ellipse(outer, fill=(255, 255, 255)) | |
| draw.ellipse(inner, fill=color) | |
| return np.array(img) | |
| # โโ Mask colourisation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def mask_to_color(mask: np.ndarray) -> np.ndarray: | |
| """Class indices -> RGB. IGNORE_INDEX pixels rendered as light gray.""" | |
| out = np.full((*mask.shape, 3), 200, dtype=np.uint8) | |
| labeled = (mask != IGNORE_INDEX) & (mask >= 0) | |
| if labeled.any(): | |
| out[labeled] = CLASS_COLORS[mask[labeled].astype(np.int64)] | |
| return out | |
| def overlay_mask(rgb: np.ndarray, mask: np.ndarray, alpha: float = 0.45) -> np.ndarray: | |
| color_mask = mask_to_color(mask) | |
| out = ((1 - alpha) * rgb.astype(np.float32) + alpha * color_mask.astype(np.float32)).clip(0, 255) | |
| return out.astype(np.uint8) | |
| def correctness_map(pred: np.ndarray, gt: np.ndarray) -> np.ndarray: | |
| """Green = correct, red = wrong, gray = unlabeled.""" | |
| out = np.full((*pred.shape, 3), 180, dtype=np.uint8) | |
| labeled = gt != IGNORE_INDEX | |
| out[labeled & (pred == gt)] = [0, 220, 0] | |
| out[labeled & (pred != gt)] = [220, 0, 0] | |
| return out | |
| def correctness_overlay(rgb: np.ndarray, pred: np.ndarray, gt: np.ndarray, alpha: float = 0.38) -> np.ndarray: | |
| cm = correctness_map(pred, gt) | |
| out = ((1 - alpha) * rgb.astype(np.float32) + alpha * cm.astype(np.float32)).clip(0, 255) | |
| return out.astype(np.uint8) | |
| # โโ Full-scene prediction rendering โโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def render_full_prediction_overlay( | |
| full_image: np.ndarray, | |
| full_pred: np.ndarray, | |
| val_mask: np.ndarray, | |
| alpha: float = 0.40, | |
| dot_radius: int = 6, | |
| ) -> np.ndarray: | |
| """ | |
| Blend predicted class colours over natural-colour composite, then draw | |
| correctness markers at every validation label location. | |
| """ | |
| rgb = render_composite(full_image, r=3, g=2, b=1) | |
| base = overlay_mask(rgb, full_pred, alpha=alpha) | |
| img = Image.fromarray(base) | |
| draw = ImageDraw.Draw(img) | |
| H, W = base.shape[:2] | |
| for cls_idx in range(NUM_CLASSES): | |
| ys, xs = np.where(val_mask == cls_idx) | |
| for y, x in zip(ys.tolist(), xs.tolist()): | |
| correct = (full_pred[y, x] == cls_idx) | |
| ring = (0, 200, 0) if correct else (220, 0, 0) | |
| r = dot_radius | |
| outer = [max(0, x-r-2), max(0, y-r-2), min(W-1, x+r+2), min(H-1, y+r+2)] | |
| inner = [max(0, x-r), max(0, y-r), min(W-1, x+r), min(H-1, y+r) ] | |
| draw.ellipse(outer, fill=(255, 255, 255)) | |
| draw.ellipse(inner, fill=ring) | |
| return np.array(img) | |
| # โโ Matplotlib charts โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def render_spectral_signatures_chart(signatures: dict) -> np.ndarray: | |
| """Line chart of per-class mean ยฑ 1-sigma across the 7 bands.""" | |
| fig, ax = plt.subplots(figsize=(8, 3.8)) | |
| x = np.arange(len(BAND_NAMES)) | |
| for cls_idx, sig in signatures.items(): | |
| mean = sig["mean"] | |
| std = sig["std"] | |
| n = sig["n"] | |
| color = CLASS_COLORS[cls_idx] / 255.0 | |
| label = f"{CLASS_NAMES[cls_idx]} (n={n})" | |
| ax.plot(x, mean, "o-", color=color, label=label, linewidth=2, markersize=5) | |
| ax.fill_between(x, mean - std, mean + std, alpha=0.18, color=color) | |
| ax.set_xticks(x) | |
| ax.set_xticklabels([d.replace(" (", "\n(") for d in BAND_DESCRIPTIONS], fontsize=8) | |
| ax.set_ylabel("Rรฉflectance normalisรฉe") | |
| ax.set_title("Signatures spectrales par classe d'occupation du sol") | |
| ax.legend(loc="upper left", fontsize=8) | |
| ax.grid(True, alpha=0.3) | |
| fig.tight_layout() | |
| return _fig_to_numpy(fig) | |
| def render_index_map( | |
| index_arr: np.ndarray, | |
| name: str, | |
| train_mask: np.ndarray, | |
| val_mask: np.ndarray, | |
| ) -> np.ndarray: | |
| """NDVI or NDWI heatmap with class-coloured label markers.""" | |
| cmap = "RdYlGn" if name == "NDVI" else "RdYlBu" | |
| fig, ax = plt.subplots(figsize=(10, 4.5)) | |
| im = ax.imshow(index_arr, cmap=cmap, vmin=-1, vmax=1, aspect="auto") | |
| plt.colorbar(im, ax=ax, fraction=0.018, pad=0.02) | |
| for cls_idx in range(NUM_CLASSES): | |
| color = CLASS_COLORS[cls_idx] / 255.0 | |
| name_ = CLASS_NAMES[cls_idx] | |
| ys, xs = np.where(train_mask == cls_idx) | |
| ax.scatter(xs, ys, c=[color], s=18, marker="s", label=f"{name_} (train)", zorder=5) | |
| ys, xs = np.where(val_mask == cls_idx) | |
| ax.scatter(xs, ys, c=[color], s=18, marker="o", | |
| edgecolors="white", linewidths=0.6, zorder=6) | |
| ax.set_title(f"{name} โ carrรฉs=รฉtiquettes d'entraรฎnement, cercles=รฉtiquettes de validation") | |
| ax.legend(loc="upper right", fontsize=7, markerscale=1.4) | |
| fig.tight_layout() | |
| return _fig_to_numpy(fig) | |