Spaces:
Running on Zero
Running on Zero
Download figures.py from SharpMeow/crackle-check: direct link, hf CLI and curl.
- Browser
- Download file 14.1 kB
-
https://huggingface.co/spaces/SharpMeow/crackle-check/resolve/main/figures.py
- Command line
-
hf download hf://spaces/SharpMeow/crackle-check/figures.py
-
curl -L -o figures.py https://huggingface.co/spaces/SharpMeow/crackle-check/resolve/main/figures.py
14.1 kB
| """Crackle Check pictures: the maps beside their control, where the scores fall, and the answer scale (numpy, Pillow). | |
| Everything here draws what the gate already measured; nothing changes the answer. Map tiles are WebP data URIs and | |
| the charts are inline SVG that take the page's text color, so they read in light and dark themes alike. The colors | |
| follow the overlay: gray for CT and for map values under the control's threshold T, copper to amber to ivory above T, | |
| so amber always means "passes the control's 99th percentile". Everything drawn is model output, not a reading. | |
| """ | |
| from __future__ import annotations | |
| import base64 | |
| import html | |
| import io | |
| import numpy as np | |
| import gate | |
| # Charcoal to copper to amber to ivory, for map values from T up to the region's top value. | |
| RAMP = ((0.0, (0x5a, 0x34, 0x1f)), (0.35, (0xb7, 0x7a, 0x54)), (0.7, (0xf5, 0x9e, 0x1a)), (1.0, (0xee, 0xe7, 0xd9))) | |
| AXIS_MAX = 8.0 # the answer scale runs 0 to 8 times the control; larger values sit at the end with a "+" | |
| BINS = 40 | |
| def ramp(t): | |
| """RGB float (0 to 1) for t in 0 to 1 along RAMP; t is an array.""" | |
| t = np.clip(np.asarray(t, np.float32), 0, 1) | |
| xs = [p for p, _ in RAMP] | |
| return np.stack([np.interp(t, xs, [c[i] / 255.0 for _, c in RAMP]) for i in range(3)], -1) | |
| def ct_gray(ct): | |
| """A CT layer stretched to its 1st to 99th percentile, float 0 to 1.""" | |
| ct = np.asarray(ct, np.float32) | |
| lo, hi = np.percentile(ct, [1, 99]) | |
| return np.clip((ct - lo) / max(hi - lo, 1e-6), 0, 1) | |
| def heat(m, threshold, top, papyrus_mask): | |
| """One map as RGB uint8: under T a dim gray that rises with the value, at or above T the copper ramp. | |
| The same T and top are used for a map and its control, so the two tiles are on one scale.""" | |
| m = np.asarray(m, np.float32) | |
| below = np.clip(m / max(threshold, 1e-6), 0, 1) * 0.42 + 0.08 | |
| rgb = np.stack([below] * 3, -1) | |
| over = m > threshold | |
| if over.any(): | |
| rgb[over] = ramp((m[over] - threshold) / max(top - threshold, 1e-6)) | |
| rgb[~papyrus_mask] = 0.03 | |
| return (rgb * 255).astype(np.uint8) | |
| def data_uri(rgb, max_side=480, quality=82): | |
| """An RGB array as a WebP data URI, at most max_side px a side.""" | |
| from PIL import Image | |
| im = Image.fromarray(np.asarray(rgb, np.uint8)) | |
| if max(im.size) > max_side: | |
| k = max_side / max(im.size) | |
| im = im.resize((max(1, round(im.size[0] * k)), max(1, round(im.size[1] * k))), Image.LANCZOS) | |
| b = io.BytesIO() | |
| im.save(b, "WEBP", quality=quality, method=6) | |
| return "data:image/webp;base64," + base64.b64encode(b.getvalue()).decode("ascii") | |
| def _f(x): | |
| return f"{x:.1f}".rstrip("0").rstrip(".") | |
| def gauge_svg(excess=None, width=440): | |
| """The answer scale as SVG: No under 1.5 times the control, Maybe to 3, Yes from 3, with a marker at excess.""" | |
| h, top, bar = 70, 22, 14 | |
| x = lambda v: 8 + (width - 16) * min(max(v, 0.0), AXIS_MAX) / AXIS_MAX # noqa: E731 | |
| zones = ((0, gate.WEAK_EXCESS, "var(--cc-no, #4a5563)", "No signal found"), | |
| (gate.WEAK_EXCESS, gate.INK_EXCESS, "var(--cc-maybe, #8a5a00)", "Maybe"), | |
| (gate.INK_EXCESS, AXIS_MAX, "var(--cc-yes, #1f7a4d)", "Yes")) | |
| parts = [f'<svg class="cc-gauge" viewBox="0 0 {width} {h}" width="100%" role="img" ' | |
| f'aria-label="Answer scale: No under 1.5, Maybe 1.5 to 3, Yes from 3 times the control' | |
| + (f'; this region {_f(excess)} times' if excess is not None else '') + '" ' | |
| 'style="width:100%;max-width:560px;height:auto;font:12px ui-sans-serif,system-ui,sans-serif">'] | |
| for a, b, color, label in zones: | |
| parts.append(f'<rect x="{x(a):.1f}" y="{top}" width="{x(b) - x(a):.1f}" height="{bar}" fill="{color}" ' | |
| f'opacity=".8"/>') | |
| parts.append(f'<text x="{(x(a) + x(b)) / 2:.1f}" y="{top - 7}" text-anchor="middle" fill="currentColor" ' | |
| f'opacity=".85">{label}</text>') | |
| for v in (0, 1, gate.WEAK_EXCESS, gate.INK_EXCESS, 5, AXIS_MAX): | |
| if excess is not None and abs(x(v) - x(excess)) < 34: | |
| continue # the marker's own label goes there | |
| parts.append(f'<text x="{x(v):.1f}" y="{top + bar + 14}" text-anchor="middle" fill="currentColor" ' | |
| f'opacity=".6">{_f(v)}{"+" if v == AXIS_MAX else ""}</text>') | |
| parts.append(f'<line x1="{x(1):.1f}" x2="{x(1):.1f}" y1="{top - 2}" y2="{top + bar + 2}" stroke="currentColor" ' | |
| 'stroke-dasharray="2 2" opacity=".6"/>') | |
| if excess is not None: | |
| cx = x(excess) | |
| parts.append(f'<path d="M{cx - 7:.1f},{top - 1} L{cx + 7:.1f},{top - 1} L{cx:.1f},{top + 9} Z" ' | |
| 'fill="currentColor" stroke="var(--background-fill-primary, #fff)" stroke-width="1.5"/>') | |
| parts.append(f'<rect x="{cx - 1.5:.1f}" y="{top}" width="3" height="{bar}" fill="currentColor"/>') | |
| anchor = "end" if cx > width - 40 else "start" if cx < 40 else "middle" | |
| parts.append(f'<text x="{cx:.1f}" y="{top + bar + 15}" text-anchor="{anchor}" fill="currentColor" ' | |
| f'font-weight="700">{_f(excess)} x</text>') | |
| parts.append("</svg>") | |
| return "".join(parts) | |
| def histogram_svg(F, R, mask, threshold, width=440, height=170): | |
| """Where the scores fall on the papyrus: the control's (reversed) scores in gray, yours in amber, T dashed. | |
| Bar heights are the share of papyrus pixels per bin on a square-root axis, so the 1 % tail stays visible.""" | |
| f, r = np.asarray(F, np.float32)[mask], np.asarray(R, np.float32)[mask] | |
| hi = float(max(f.max(), r.max(), threshold * 1.05, 1e-3)) | |
| lo = float(min(f.min(), r.min(), threshold * 0.95)) if f.size else 0.0 | |
| edges = np.linspace(lo, hi, BINS + 1) | |
| hf = np.histogram(f, edges)[0] / max(f.size, 1) | |
| hr = np.histogram(r, edges)[0] / max(r.size, 1) | |
| left, right, top, bottom = 8, 8, 26, 30 | |
| pw, ph = width - left - right, height - top - bottom | |
| peak = float(np.sqrt(max(hf.max(), hr.max(), 1e-9))) | |
| bw = pw / BINS | |
| X = lambda v: left + pw * (v - lo) / max(hi - lo, 1e-9) # noqa: E731 | |
| Y = lambda share: top + ph * (1 - np.sqrt(share) / peak) # noqa: E731 | |
| def bars(h, color, opacity, inset): | |
| out = [] | |
| for i, s in enumerate(h): | |
| if s <= 0: | |
| continue | |
| y = Y(s) | |
| out.append(f'<rect x="{left + i * bw + inset:.1f}" y="{y:.1f}" width="{max(bw - 2 * inset, 0.5):.1f}" ' | |
| f'height="{top + ph - y:.1f}" fill="{color}" opacity="{opacity}"/>') | |
| return "".join(out) | |
| tx = X(threshold) | |
| above_f = float((f > threshold).mean()) if f.size else 0.0 | |
| above_r = float((r > threshold).mean()) if r.size else 0.0 | |
| parts = [f'<svg class="cc-hist" viewBox="0 0 {width} {height}" width="100%" role="img" ' | |
| f'aria-label="Score histogram: {above_f:.1%} of your order and {above_r:.1%} of the control pass T" ' | |
| 'style="width:100%;max-width:560px;height:auto;font:12px ui-sans-serif,system-ui,sans-serif">', | |
| f'<rect x="{tx:.1f}" y="{top}" width="{left + pw - tx:.1f}" height="{ph}" fill="#f59e1a" opacity=".08"/>', | |
| bars(hr, "currentColor", ".35", 0.6), | |
| bars(hf, "#f59e1a", ".85", 2.2), | |
| f'<line x1="{left}" x2="{left + pw}" y1="{top + ph}" y2="{top + ph}" stroke="currentColor" opacity=".4"/>', | |
| f'<line x1="{tx:.1f}" x2="{tx:.1f}" y1="{top - 6}" y2="{top + ph}" stroke="currentColor" ' | |
| 'stroke-dasharray="4 3"/>', | |
| f'<text x="{tx - 6:.1f}" y="{top - 10}" text-anchor="end" fill="currentColor">T {threshold:.3f}</text>', | |
| (f'<text x="{tx + 6:.1f}" y="{top - 10}" fill="currentColor">' if tx < 0.6 * width else | |
| f'<text x="{left + pw:.1f}" y="{top + 12}" text-anchor="end" fill="currentColor">') | |
| + f'<tspan font-weight="700" fill="#e08a0c">{above_f:.1%}</tspan> yours, {above_r:.1%} control</text>', | |
| f'<text x="{left}" y="{height - 8}" fill="currentColor" opacity=".6">{lo:.2f} (less ink-like)</text>', | |
| f'<text x="{left + pw}" y="{height - 8}" text-anchor="end" fill="currentColor" opacity=".6">' | |
| f'{hi:.2f} (more ink-like)</text>', | |
| "</svg>"] | |
| return "".join(parts) | |
| def compare_html(volume, F, R, decision): | |
| """The two maps on one scale beside each other and the score histogram, as one HTML block (the verdict card | |
| above it carries the answer scale). | |
| Empty when the gate set no threshold (too small or unreadable): there is nothing to compare then.""" | |
| T = decision.get("threshold") | |
| if T is None: | |
| return "" | |
| ct = np.asarray(volume)[np.asarray(volume).shape[0] // 2] | |
| mask = gate.papyrus(ct) | |
| F, R = np.asarray(F, np.float32), np.asarray(R, np.float32) | |
| top = float(np.percentile(np.concatenate([F[mask], R[mask]]), 99.8)) if mask.any() else 1.0 | |
| tiles = [] | |
| for title, m, share in (("Your layer order", F, decision.get("forward_above")), | |
| ("Control: layers reversed", R, decision.get("control_above"))): | |
| tiles.append(f'<figure><img src="{data_uri(heat(m, T, max(top, T * 1.01), mask))}" alt="{html.escape(title)}: ' | |
| f'model score map, pixels above T in amber"><figcaption><b>{html.escape(title)}</b><br>' | |
| f'{(share or 0):.1%} of papyrus above T</figcaption></figure>') | |
| return (f'<div class="cc-figs"><div class="cc-h">The map and its control, on one scale</div>' | |
| f'<div class="cc-pair">{"".join(tiles)}</div>' | |
| '<div class="cc-cap">Amber: model score above T, the level only 1 % of the control passes. Gray: below T. ' | |
| 'Black: not papyrus.</div>' | |
| f'<div class="cc-h">Where the scores fall</div>{histogram_svg(F, R, mask, T)}' | |
| '<div class="cc-cap">Share of papyrus pixels at each score (square-root scale): the control in gray, your ' | |
| 'order in amber. The answer is your share above T divided by the control\'s 1 %. Model output, not a ' | |
| 'reading.</div></div>') | |
| def composite(volume, F, R, kept, decision, side=480, gap=12, background=(23, 26, 28)): | |
| """CT, the overlay and the two maps in one RGB strip (for the README and the downloads), no text baked in.""" | |
| from PIL import Image | |
| ct = np.asarray(volume)[np.asarray(volume).shape[0] // 2] | |
| g = ct_gray(ct) | |
| mask = gate.papyrus(ct) | |
| T = decision.get("threshold") or 1.0 | |
| top = float(np.percentile(np.concatenate([F[mask], R[mask]]), 99.8)) if mask.any() else 1.0 | |
| a = (np.asarray(kept, np.float32) / 255.0)[..., None] * 0.85 | |
| over = np.stack([g] * 3, -1) * (1 - a) + np.array([1.0, 0.62, 0.1]) * a | |
| panels = [np.stack([g] * 3, -1), over, heat(F, T, max(top, T * 1.01), mask) / 255.0, | |
| heat(R, T, max(top, T * 1.01), mask) / 255.0] | |
| ims = [Image.fromarray((np.clip(p, 0, 1) * 255).astype(np.uint8)).resize((side, side), Image.LANCZOS) | |
| for p in panels] | |
| out = Image.new("RGB", (len(ims) * side + (len(ims) + 1) * gap, side + 2 * gap), background) | |
| for i, im in enumerate(ims): | |
| out.paste(im, (gap + i * (side + gap), gap)) | |
| return np.asarray(out) | |
| def stack_svg(width=480, height=280): | |
| """A drawing (not data) of what the models read: a stack of layers through the papyrus surface, read in your | |
| order and, as the control, reversed.""" | |
| sheets, dy, w, h, skew, x0, y0 = 9, 19, 200, 30, 46, 112, 34 | |
| mid = sheets // 2 | |
| parts = [f'<svg class="cc-stack" viewBox="0 0 {width} {height}" width="100%" role="img" ' | |
| 'aria-label="Drawing: a stack of nine flat layers through a papyrus sheet; the middle one is the surface ' | |
| 'with ink; arrows show your order top to bottom and the control bottom to top" ' | |
| 'style="width:100%;max-width:520px;height:auto;font:12px ui-sans-serif,system-ui,sans-serif">', | |
| '<defs><marker id="cc-arr" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="7" markerHeight="7" ' | |
| 'orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="currentColor"/></marker></defs>'] | |
| for i in range(sheets): | |
| y = y0 + i * dy | |
| pts = f"{x0 + skew},{y} {x0 + skew + w},{y} {x0 + w},{y + h} {x0},{y + h}" | |
| if i == mid: | |
| parts.append(f'<polygon points="{pts}" fill="#b77a54" fill-opacity=".55" stroke="#b77a54"/>') | |
| for cx, cy, rx in ((0.30, 0.45, 20), (0.52, 0.55, 14), (0.70, 0.40, 18)): | |
| parts.append(f'<ellipse cx="{x0 + skew * (1 - cy) + w * cx:.1f}" cy="{y + h * cy:.1f}" rx="{rx}" ' | |
| 'ry="5" fill="#f59e1a"/>') | |
| else: | |
| near = 1 - abs(i - mid) / (mid + 1) | |
| parts.append(f'<polygon points="{pts}" fill="currentColor" fill-opacity="{0.05 + 0.08 * near:.2f}" ' | |
| 'stroke="currentColor" stroke-opacity=".35"/>') | |
| yb = y0 + (sheets - 1) * dy + h | |
| parts += [f'<text x="{x0 + skew + w + 8}" y="{y0 + mid * dy + 12}" fill="currentColor">surface, with ink</text>', | |
| f'<text x="{x0 + skew + w + 8}" y="{y0 + 12}" fill="currentColor" opacity=".7">layer 00</text>', | |
| f'<text x="{x0 + skew + w + 8}" y="{y0 + (sheets - 1) * dy + 12}" fill="currentColor" ' | |
| 'opacity=".7">layer 64</text>', | |
| f'<line x1="40" y1="{y0 + 4}" x2="40" y2="{yb - 4}" stroke="currentColor" stroke-width="2" ' | |
| 'marker-end="url(#cc-arr)"/>', | |
| f'<text x="40" y="{y0 - 12}" text-anchor="middle" fill="currentColor" font-weight="700">Your order</text>', | |
| f'<line x1="86" y1="{yb - 4}" x2="86" y2="{y0 + 4}" stroke="currentColor" stroke-width="2" ' | |
| 'stroke-dasharray="5 4" opacity=".7" marker-end="url(#cc-arr)"/>', | |
| f'<text x="86" y="{yb + 18}" text-anchor="middle" fill="currentColor" opacity=".8">Control</text>', | |
| f'<text x="{width / 2:.0f}" y="{height - 4}" text-anchor="middle" fill="currentColor" opacity=".7">' | |
| 'Drawing, not data. The models read 17 layers at once.</text>', | |
| "</svg>"] | |
| return "".join(parts) | |