"""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''] for a, b, color, label in zones: parts.append(f'') parts.append(f'{label}') 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'{_f(v)}{"+" if v == AXIS_MAX else ""}') parts.append(f'') if excess is not None: cx = x(excess) parts.append(f'') parts.append(f'') anchor = "end" if cx > width - 40 else "start" if cx < 40 else "middle" parts.append(f'{_f(excess)} x') parts.append("") 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'') 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'', f'', bars(hr, "currentColor", ".35", 0.6), bars(hf, "#f59e1a", ".85", 2.2), f'', f'', f'T {threshold:.3f}', (f'' if tx < 0.6 * width else f'') + f'{above_f:.1%} yours, {above_r:.1%} control', f'{lo:.2f} (less ink-like)', f'' f'{hi:.2f} (more ink-like)', ""] 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'
{html.escape(title)}: '
                     f'model score map, pixels above T in amber
{html.escape(title)}
' f'{(share or 0):.1%} of papyrus above T
') return (f'
The map and its control, on one scale
' f'
{"".join(tiles)}
' '
Amber: model score above T, the level only 1 % of the control passes. Gray: below T. ' 'Black: not papyrus.
' f'
Where the scores fall
{histogram_svg(F, R, mask, T)}' '
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.
') 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'', ''] 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'') for cx, cy, rx in ((0.30, 0.45, 20), (0.52, 0.55, 14), (0.70, 0.40, 18)): parts.append(f'') else: near = 1 - abs(i - mid) / (mid + 1) parts.append(f'') yb = y0 + (sheets - 1) * dy + h parts += [f'surface, with ink', f'layer 00', f'layer 64', f'', f'Your order', f'', f'Control', f'' 'Drawing, not data. The models read 17 layers at once.', ""] return "".join(parts)