Spaces:
Running
Running
File size: 4,598 Bytes
7a3a384 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | """Shared step-visual renderers — the single source of truth for how a pipeline stage
is drawn, used by both the Gradio app and the static HTML export.
Lifted from `scripts/audit_pipeline.py` (b64/overlay/draw_outlines/class_panel/
lidar_panel) so the interactive and shareable views render identically, plus an
`iou_overlay` for scoring a recipe mask against a hand-drawn gold mask.
"""
from __future__ import annotations
import base64
import io
import numpy as np
from PIL import Image, ImageDraw
# ADE20K names for the EoMT cascade primary (outdoor-relevant subset).
ADE_NAMES = {0: "wall", 1: "building", 2: "sky", 4: "tree", 6: "road", 9: "grass",
11: "sidewalk", 13: "earth", 17: "plant", 21: "water", 20: "car", 25: "?",
29: "field", 46: "sand", 52: "path", 94: "land"}
# Empirical meanings for the incumbent mask2former's generic LABEL_0..7.
M2F_NAMES = {0: "background", 1: "open (grass+dirt)", 2: "street-edge band", 3: "pavement",
4: "canopy", 6: '"water" (fires on flat turf)', 7: "roofs (as cropland)"}
PALETTE = [(80, 200, 60), (0, 110, 40), (220, 60, 60), (150, 110, 70), (235, 220, 120),
(170, 120, 40), (60, 130, 235), (120, 120, 130), (200, 60, 200), (230, 130, 30),
(90, 200, 200), (200, 120, 200), (255, 180, 40)]
def b64(img: Image.Image, max_w: int = 900, quality: int = 82) -> str:
"""A data-URI JPEG, downscaled to `max_w` — embeds directly in shareable HTML."""
if img.width > max_w:
img = img.resize((max_w, round(img.height * max_w / img.width)), Image.LANCZOS)
buf = io.BytesIO()
img.convert("RGB").save(buf, "JPEG", quality=quality)
return "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode()
def overlay(base: Image.Image, mask: np.ndarray, color, alpha: float = 0.55) -> Image.Image:
arr = np.asarray(base.convert("RGB")).astype(np.float32)
arr[mask] = arr[mask] * (1 - alpha) + np.array(color, np.float32) * alpha
return Image.fromarray(arr.astype(np.uint8))
def draw_outlines(base: Image.Image, outlines, color, width: int = 4) -> Image.Image:
"""`outlines` is a list of (xs, ys) coord-list pairs (polygon_pixel_outlines)."""
img = base.convert("RGB").copy()
d = ImageDraw.Draw(img)
for xs, ys in outlines:
pts = [(float(x), float(y)) for x, y in zip(xs, ys, strict=False)]
if len(pts) > 1:
d.line(pts + [pts[0]], fill=color, width=width)
return img
def class_panel(base: Image.Image, pred: np.ndarray, names: dict,
min_frac: float = 0.003) -> tuple[Image.Image, list]:
"""Per-class colored overlay + legend chips (name, %, color) for present classes."""
arr = np.asarray(base.convert("RGB")).astype(np.float32) * 0.45
legend = []
codes = [c for c in np.unique(pred) if (pred == c).sum() / pred.size >= min_frac]
for i, c in enumerate(sorted(codes, key=lambda c: -(pred == c).sum())):
col = PALETTE[i % len(PALETTE)]
arr[pred == c] += np.array(col, np.float32) * 0.55
legend.append({"name": names.get(int(c), f"class {c}"),
"color": "#{:02x}{:02x}{:02x}".format(*col),
"pct": round(float((pred == c).sum() / pred.size * 100), 1)})
return Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8)), legend
def lidar_panel(base: Image.Image, viz: dict) -> Image.Image:
"""Ground points colored: green = counted lawn, red = removed (hardscape/roof)."""
img = base.convert("RGB").copy()
gpx, gpy = viz.get("ground_px"), viz.get("ground_py")
if gpx is None:
return img
d = ImageDraw.Draw(img)
is_lawn = viz.get("lawn_mask")
for k in range(len(gpx)):
x, y = float(gpx[k]), float(gpy[k])
col = (60, 220, 60) if (is_lawn is not None and is_lawn[k]) else (235, 60, 60)
d.ellipse([x - 2, y - 2, x + 2, y + 2], fill=col)
return img
def iou_overlay(base: Image.Image, recipe_mask: np.ndarray, gold_mask: np.ndarray,
alpha: float = 0.5) -> Image.Image:
"""True-positive (green) / false-positive (red, recipe-only) / false-negative
(blue, gold-only) overlay — a visual read on where a recipe misses the truth."""
arr = np.asarray(base.convert("RGB")).astype(np.float32)
tp = recipe_mask & gold_mask
fp = recipe_mask & ~gold_mask
fn = ~recipe_mask & gold_mask
for mask, color in ((tp, (60, 220, 60)), (fp, (235, 60, 60)), (fn, (70, 130, 235))):
arr[mask] = arr[mask] * (1 - alpha) + np.array(color, np.float32) * alpha
return Image.fromarray(arr.astype(np.uint8))
|