achouffe's picture
Temporal smoke verifier demo: visual sequence picker, looping animations, verdict-colored tubes, live CPU run
b78f06f verified
Raw
History Blame Contribute Delete
2.44 kB
"""Shared rendering for the demo: overlays and the looping sequence animation.
Used by app.py (live runs) and scripts/precompute.py (shipped payloads).
Color semantics everywhere: orange = the tube is judged smoke, gray = the
tube is rejected by the classifier.
"""
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
SMOKE = "#ea580c"
REJECTED = "#94a3b8"
INK = "#1e293b"
ANIM_WIDTH = 960
ANIM_FRAME_MS = 400
def font(size: int):
try:
return ImageFont.truetype("DejaVuSans-Bold.ttf", size)
except OSError:
return ImageFont.load_default()
def tube_is_smoke(tube: dict, threshold: float) -> bool:
prob = tube["probability"]
return prob is not None and prob >= threshold
def tube_color(tube: dict, threshold: float) -> str:
return SMOKE if tube_is_smoke(tube, threshold) else REJECTED
def overlay_frame(payload: dict, paths: list[Path], idx: int, width: int | None = None) -> Image.Image:
"""Frame idx with each kept tube's fixed crop window, colored by verdict."""
img = Image.open(paths[idx]).convert("RGB")
if width and img.width > width:
img = img.resize((width, round(img.height * width / img.width)), Image.LANCZOS)
draw = ImageDraw.Draw(img)
w, h = img.size
fnt = font(16)
threshold = payload["details"]["decision"]["threshold"]
for tube in payload["details"]["tubes"]["kept"]:
if not tube["stabilized_window"]:
continue
cx, cy, bw, bh = tube["stabilized_window"]
x0, y0 = (cx - bw / 2) * w, (cy - bh / 2) * h
x1, y1 = (cx + bw / 2) * w, (cy + bh / 2) * h
color = tube_color(tube, threshold)
draw.rectangle([x0, y0, x1, y1], outline=color, width=3)
# tube_id is 0-based internally; display 1-based for readability
draw.text((x0 + 3, max(0, y0 - 20)), f"tube {tube['tube_id'] + 1}", fill=color, font=fnt)
draw.text((10, 8), f"frame {idx + 1}", fill="white", font=font(22))
return img
def save_animation(payload: dict, paths: list[Path], out_path: Path) -> Path:
"""Looping animated WebP of the whole sequence with overlays burned in."""
frames = [overlay_frame(payload, paths, i, width=ANIM_WIDTH) for i in range(len(paths))]
frames[0].save(
out_path,
format="WEBP",
save_all=True,
append_images=frames[1:],
duration=ANIM_FRAME_MS,
loop=0,
quality=78,
)
return out_path