Download engine.py from SharpMeow/ink-lens: direct link, hf CLI and curl.
- Browser
- Download file 24.9 kB
-
https://huggingface.co/spaces/SharpMeow/ink-lens/resolve/main/engine.py
- Command line
-
hf download hf://spaces/SharpMeow/ink-lens/engine.py
-
curl -L -o engine.py https://huggingface.co/spaces/SharpMeow/ink-lens/resolve/main/engine.py
24.9 kB
| """Ink Lens engine: an openly licensed 9 um ink model on one region, three depth windows, both depth orders. | |
| The model is built by villa's own flat ink inference (vesuvius.ink_detection.inference.infer.configure_model, at the | |
| commit pinned in VILLA_COMMIT) from a checkpoint downloaded by URL and refused unless its SHA-256 matches. The patch | |
| schedule (128 px patches, stride 64, the last flush with the end), the layer choice (center crop, reversed for the | |
| control), villa's per-patch robust normalization and the Hann blend are villa's; two things differ from villa's | |
| command line, both to fit one ZeroGPU call: | |
| 1. the robust normalization runs batched on the device (robust_torch), checked against villa's CPU function | |
| (robust_np is a line-for-line copy of villa's normalize_robust for a patch with no mask; tests/test_inklens.py | |
| compares them, and compares robust_np with villa's own function when villa is importable); | |
| 2. the maps stay float (0 to 1) instead of villa's uint8 encoding, so the control-set threshold is not quantized. | |
| run_reference() is the same algorithm in numpy with any predict function: the tests run it with a stand-in model, | |
| and the torch path is checked against it. | |
| python3 engine.py estimate 1024 1024 --windows 3 patches and the GPU seconds one call asks for | |
| python3 engine.py fetch d9v2 download and check a checkpoint into the cache | |
| """ | |
| from __future__ import annotations | |
| import hashlib | |
| import math | |
| import os | |
| import platform | |
| import shutil | |
| import subprocess | |
| import sys | |
| import tarfile | |
| import tempfile | |
| import time | |
| import urllib.request | |
| from pathlib import Path | |
| VERSION = "0.1.0" | |
| VILLA_REPO = "https://github.com/ScrollPrize/villa.git" | |
| VILLA_TARBALL = "https://codeload.github.com/ScrollPrize/villa/tar.gz/{commit}" | |
| VILLA_COMMIT = "8aa507c1215f1fa8758f13d0538f7af9710e02d7" # ScrollPrize/villa main, 18 Oct 2026 | |
| CACHE = Path(os.environ.get("INKLENS_CACHE") or Path(tempfile.gettempdir()) / "ink-lens") | |
| USER_AGENT = f"ink-lens/{VERSION}" | |
| # Openly licensed 9 um readers that drop into villa's flat inference. Public held-out scores (PHerc0841, crops with a | |
| # 64 px inner edge, raw / reversed AUC) are in docs/results.json; d9v2 is the default. | |
| READERS = { | |
| "d9v2": { | |
| "title": "d9v2 (TAUIL-Abd-Elilah, MIT)", | |
| "url": "https://github.com/TAUIL-Abd-Elilah/pherc0826-first-letters-search/releases/download/v1.0/" | |
| "d9v2_ft-012000.pth", | |
| "file": "d9v2_ft-012000.pth", | |
| "sha256": "50d2ad0ef7690a6a422640804d38f01409da8bc96cfc1ab72f45324a4a18f966", | |
| "license": "MIT", | |
| "source": "https://github.com/TAUIL-Abd-Elilah/pherc0826-first-letters-search", | |
| "held_out": "PHerc0841 w00, ag896, ag405 (its reader/test.txt)", | |
| }, | |
| "ink_9um": { | |
| "title": "ink_9um seed 42 step 75,000 (Vesuvius Challenge, MIT)", | |
| "url": "https://huggingface.co/scrollprize/ink_9um/resolve/main/hybrid_3d2d-seed42/step-075000.pth", | |
| "file": "ink_9um-seed42-step-075000.pth", | |
| "sha256": "e635558ae6a1a807a7e5ec1e83adfd45bc3c0ac53883ea43f1d4e085d62a9cab", | |
| "license": "MIT", | |
| "source": "https://huggingface.co/scrollprize/ink_9um", | |
| "held_out": "PHerc0841 (its label dataset has no PHerc0841 segment)", | |
| }, | |
| } | |
| READER_CHOICES = ("d9v2", "ink_9um", "both") | |
| PATCH, STRIDE, DEPTH = 128, 64, 17 | |
| STACK = 21 # layers kept around the middle: the three windows below fit in it | |
| WINDOW_OFFSETS = {1: (2,), 3: (0, 2, 4)} # window starts inside the 21-layer stack (2 is villa's own center) | |
| BATCH = 64 | |
| MIN_SIDE, MAX_SIDE = PATCH, 2048 | |
| MIN_LAYERS, MAX_LAYERS = DEPTH, 65 | |
| # One call's duration: a fixed start plus a per-patch allowance, capped. The per-patch figure is an estimate (3 to | |
| # 6 ms per patch with batched fp16 and normalization on the GPU) with a margin; every receipt records the measured | |
| # seconds so the first calls calibrate it. | |
| GPU_START_S, GPU_PER_PATCH_S, GPU_MAX_S = 4.0, 0.012, 40 | |
| class EngineError(ValueError): | |
| """A request the engine refuses; the message says why.""" | |
| # ---------------------------------------------------------------- sizing | |
| def positions(length, patch=PATCH, stride=STRIDE): | |
| """Patch starts along one axis, the last flush with the end (villa's _sliding_positions_1d).""" | |
| if length <= patch: | |
| return [0] | |
| out = list(range(0, length - patch + 1, max(1, stride))) | |
| if out[-1] != length - patch: | |
| out.append(length - patch) | |
| return out | |
| def readers_of(choice): | |
| if choice not in READER_CHOICES: | |
| raise EngineError(f"reader {choice!r}: choose one of {', '.join(READER_CHOICES)}") | |
| return ("d9v2", "ink_9um") if choice == "both" else (choice,) | |
| def patches(height, width, windows=3, reader="d9v2"): | |
| """Patches one call runs: per pass the patch grid, times windows, times two depth orders, times readers.""" | |
| if windows not in WINDOW_OFFSETS: | |
| raise EngineError(f"windows {windows}: use 1 or 3") | |
| return len(positions(height)) * len(positions(width)) * windows * 2 * len(readers_of(reader)) | |
| def gpu_seconds(n_patches): | |
| """The duration a call asks ZeroGPU for; refuses a call that would need more than GPU_MAX_S.""" | |
| need = math.ceil(GPU_START_S + n_patches * GPU_PER_PATCH_S) | |
| if need > GPU_MAX_S: | |
| raise EngineError(f"{n_patches} patches need about {need} GPU seconds, more than the {GPU_MAX_S} s one call " | |
| "may ask for; use a smaller region, one depth window, or local mode") | |
| return need | |
| def estimate(height, width, windows=3, reader="d9v2"): | |
| check_side(height, width) | |
| n = patches(height, width, windows, reader) | |
| out = {"height": int(height), "width": int(width), "windows": int(windows), "reader": reader, "patches": n} | |
| try: | |
| out["gpu_seconds_requested"] = gpu_seconds(n) | |
| except EngineError as exc: | |
| out["gpu_seconds_requested"] = None | |
| out["refused"] = str(exc) | |
| return out | |
| def check_side(height, width): | |
| for name, side in (("height", height), ("width", width)): | |
| if not MIN_SIDE <= int(side) <= MAX_SIDE: | |
| raise EngineError(f"{name} {side} px: a region is {MIN_SIDE} to {MAX_SIDE} px a side at 9.6 um") | |
| def check_volume(volume, windows=3): | |
| """Refuse anything but a uint8 (layers, rows, columns) region of the right size and depth.""" | |
| import numpy as np | |
| a = np.asarray(volume) | |
| if a.ndim != 3: | |
| raise EngineError(f"a surface volume has 3 axes (layers, rows, columns), this one has {a.ndim}") | |
| if a.dtype != np.uint8: | |
| raise EngineError(f"type {a.dtype}: the engine takes uint8 (volio.to_uint8 converts uint16)") | |
| L, H, W = a.shape | |
| need = STACK if windows == 3 else DEPTH | |
| if not need <= L <= MAX_LAYERS: | |
| raise EngineError(f"{L} layers: {windows} depth window{'s' if windows > 1 else ''} need{'' if windows > 1 else 's'} " | |
| f"{need} to {MAX_LAYERS} layers, layers first") | |
| check_side(H, W) | |
| if not a.any(): | |
| raise EngineError("the region is all zero (off the segment, or a hole in it)") | |
| return L, H, W | |
| def stack(volume, windows=3): | |
| """The middle STACK layers (or all, if fewer), as villa's center crop chooses them (upper center when uneven).""" | |
| L = volume.shape[0] | |
| n = min(L, STACK) | |
| start = L // 2 - n // 2 | |
| return volume[start:start + n] | |
| def window_indices(n_layers, offset, direction): | |
| """Layer indices of one 17-layer window inside the stack; reversed for the control.""" | |
| if n_layers < STACK: # one window, centered (villa's own choice) | |
| start = n_layers // 2 - DEPTH // 2 | |
| else: | |
| start = offset | |
| idx = list(range(start, start + DEPTH)) | |
| return idx[::-1] if direction == "reverse" else idx | |
| def passes(windows): | |
| """(offset, direction) for every pass of one reader: each window in both orders.""" | |
| return [(o, d) for o in WINDOW_OFFSETS[windows] for d in ("forward", "reverse")] | |
| # ---------------------------------------------------------------- villa's normalization, numpy and torch | |
| def robust_np(patch): | |
| """villa's normalize_robust (vesuvius/image_proc/intensity/normalization.py at VILLA_COMMIT) with no mask: | |
| clip to the 1st and 99th percentiles, subtract the median, divide by 1.4826 x MAD, with villa's fallbacks.""" | |
| import numpy as np | |
| arr = np.asarray(patch).astype(np.float32, copy=True) | |
| valid = arr.reshape(-1) | |
| if valid.size == 0: | |
| return arr | |
| std_preclip = float(np.std(valid)) | |
| lower = float(np.percentile(valid, 1.0)) | |
| upper = float(np.percentile(valid, 99.0)) | |
| np.clip(arr, lower, upper, out=arr) | |
| valid = arr.reshape(-1) | |
| median = float(np.median(valid)) | |
| mad = float(np.median(np.abs(valid - median))) | |
| scaled = 1.4826 * mad | |
| if not np.isfinite(scaled) or scaled < 1e-6: | |
| span = upper - lower | |
| cands = [] | |
| if np.isfinite(std_preclip): | |
| cands.append(abs(std_preclip)) | |
| if np.isfinite(span): | |
| cands.append(abs(span) / 2.0) | |
| scaled = next((c for c in cands if c >= 1e-6), None) | |
| if scaled is None or not np.isfinite(scaled): | |
| scaled = 1.0 | |
| arr -= median | |
| arr /= scaled | |
| np.nan_to_num(arr, copy=False, nan=0.0, posinf=0.0, neginf=0.0) | |
| return arr | |
| def _lerp_sorted(torch, s, q): | |
| """numpy's linear percentile of each row of an already sorted (B, N) tensor.""" | |
| n = s.shape[1] | |
| pos = q * (n - 1) | |
| lo = int(math.floor(pos)) | |
| hi = min(lo + 1, n - 1) | |
| frac = pos - lo | |
| return s[:, lo] + (s[:, hi] - s[:, lo]) * frac | |
| def robust_torch(x): | |
| """robust_np on every row of a (B, N) float32 tensor at once, on its device.""" | |
| import torch | |
| x = x.float() | |
| s, _ = torch.sort(x, dim=1) | |
| std_pre = x.std(dim=1, unbiased=False) | |
| lower, upper = _lerp_sorted(torch, s, 0.01), _lerp_sorted(torch, s, 0.99) | |
| x = torch.maximum(torch.minimum(x, upper[:, None]), lower[:, None]) | |
| s = torch.maximum(torch.minimum(s, upper[:, None]), lower[:, None]) | |
| median = _lerp_sorted(torch, s, 0.5) | |
| dev, _ = torch.sort((x - median[:, None]).abs(), dim=1) | |
| scaled = 1.4826 * _lerp_sorted(torch, dev, 0.5) | |
| half_span = (upper - lower).abs() / 2.0 | |
| ok = torch.isfinite(scaled) & (scaled >= 1e-6) | |
| fb = torch.where(torch.isfinite(std_pre) & (std_pre.abs() >= 1e-6), std_pre.abs(), | |
| torch.where(torch.isfinite(half_span) & (half_span >= 1e-6), half_span, | |
| torch.ones_like(half_span))) | |
| scaled = torch.where(ok, scaled, fb) | |
| out = (x - median[:, None]) / scaled[:, None] | |
| return torch.nan_to_num(out, nan=0.0, posinf=0.0, neginf=0.0) | |
| def hann2d(): | |
| """villa's floored Hann blend map (compute_importance_map_2d, mode hann), in numpy.""" | |
| import numpy as np | |
| w1 = np.hanning(PATCH).astype(np.float32) # numpy's hanning is the symmetric (periodic=False) window | |
| w = np.outer(w1, w1) | |
| w /= max(float(w.max()), float(np.finfo(np.float32).eps)) | |
| return np.maximum(w, 0.001).astype(np.float32) | |
| # ---------------------------------------------------------------- the numpy reference | |
| def run_reference(volume, predict, windows=3): | |
| """The whole algorithm in numpy: predict(batch (B, 1, 17, 128, 128) float32) -> (B, 128, 128) probabilities. | |
| Returns {"forward": F, "reverse": R} (float32, 0 to 1), each the mean over the windows.""" | |
| import numpy as np | |
| vol = stack(np.asarray(volume), windows) | |
| L, H, W = vol.shape | |
| ys, xs = positions(H), positions(W) | |
| wmap = hann2d() | |
| maps = {"forward": [], "reverse": []} | |
| for off, direction in passes(windows): | |
| idx = window_indices(L, off, direction) | |
| psum = np.zeros((H, W), np.float32) | |
| wsum = np.zeros((H, W), np.float32) | |
| for y0 in ys: | |
| for x0 in xs: | |
| raw = vol[idx, y0:y0 + PATCH, x0:x0 + PATCH] | |
| if not raw.any(): | |
| continue | |
| p = np.asarray(predict(robust_np(raw)[None, None]), np.float32)[0] | |
| psum[y0:y0 + PATCH, x0:x0 + PATCH] += p * wmap | |
| wsum[y0:y0 + PATCH, x0:x0 + PATCH] += wmap | |
| prob = np.divide(psum, wsum, out=np.zeros_like(psum), where=wsum > 1e-6) | |
| maps[direction].append(np.clip(prob, 0, 1)) | |
| return {d: np.mean(m, axis=0).astype(np.float32) for d, m in maps.items()} | |
| # ---------------------------------------------------------------- downloads | |
| def sha256_file(path, block=1 << 22): | |
| h = hashlib.sha256() | |
| with open(path, "rb") as fh: | |
| for chunk in iter(lambda: fh.read(block), b""): | |
| h.update(chunk) | |
| return h.hexdigest() | |
| def _download(url, dest): | |
| dest.parent.mkdir(parents=True, exist_ok=True) | |
| part = dest.with_name(dest.name + ".part") | |
| req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}) | |
| with urllib.request.urlopen(req, timeout=600) as resp, open(part, "wb") as out: | |
| shutil.copyfileobj(resp, out, 1 << 20) | |
| os.replace(part, dest) | |
| def ensure_checkpoint(name, cache=CACHE): | |
| """A reader's checkpoint, downloaded once and refused unless its SHA-256 matches READERS. INKLENS_<NAME> (for | |
| example INKLENS_D9V2) may name a local copy; its SHA-256 is still checked.""" | |
| spec = READERS[name] | |
| env = os.environ.get("INKLENS_" + name.upper()) | |
| path = Path(env).expanduser() if env else Path(cache) / "checkpoints" / spec["file"] | |
| if not path.is_file(): | |
| if env: | |
| raise EngineError(f"INKLENS_{name.upper()}={env} does not exist") | |
| _download(spec["url"], path) | |
| digest = sha256_file(path) | |
| if digest != spec["sha256"]: | |
| raise EngineError(f"{path} has SHA-256 {digest}, expected {spec['sha256']} for {name}; refusing it") | |
| return path | |
| def _git_commit(path): | |
| try: | |
| return subprocess.run(["git", "-C", str(path), "rev-parse", "HEAD"], capture_output=True, text=True, | |
| check=True).stdout.strip() | |
| except (OSError, subprocess.CalledProcessError): | |
| return "unknown" | |
| def _villa_tarball(tmp, commit): | |
| root = tmp.resolve() | |
| kept = 0 | |
| req = urllib.request.Request(VILLA_TARBALL.format(commit=commit), headers={"User-Agent": USER_AGENT}) | |
| with urllib.request.urlopen(req, timeout=600) as resp, tarfile.open(fileobj=resp, mode="r|gz") as tf: | |
| for m in tf: | |
| _, _, rel = m.name.partition("/") | |
| if not rel.startswith("vesuvius/src/"): | |
| continue | |
| target = (root / rel).resolve() | |
| if root not in target.parents or m.issym() or m.islnk(): | |
| continue | |
| if m.isdir(): | |
| target.mkdir(parents=True, exist_ok=True) | |
| elif m.isfile(): | |
| target.parent.mkdir(parents=True, exist_ok=True) | |
| with tf.extractfile(m) as src, open(target, "wb") as out: | |
| shutil.copyfileobj(src, out) | |
| kept += 1 | |
| if not kept: | |
| raise EngineError(f"the villa tarball for {commit} held no vesuvius/src files") | |
| def ensure_villa(cache=CACHE): | |
| """villa's vesuvius package on sys.path: VILLA_SRC (a local vesuvius/src) when set, else the pinned commit, | |
| downloaded once (sparse git fetch, else GitHub's tarball).""" | |
| if os.environ.get("VILLA_SRC"): | |
| src = Path(os.environ["VILLA_SRC"]).expanduser().resolve() | |
| info = {"source": "VILLA_SRC", "commit": _git_commit(src)} | |
| else: | |
| dest = Path(cache) / f"villa-{VILLA_COMMIT[:12]}" | |
| if not (dest / "COMMIT").is_file(): | |
| tmp = dest.with_name(dest.name + ".part") | |
| shutil.rmtree(tmp, ignore_errors=True) | |
| tmp.mkdir(parents=True) | |
| try: | |
| git = ["git", "-C", str(tmp)] | |
| for cmd in (["init", "-q"], ["remote", "add", "origin", VILLA_REPO], | |
| ["sparse-checkout", "set", "vesuvius/src"], | |
| ["fetch", "-q", "--depth", "1", "--filter=blob:none", "origin", VILLA_COMMIT], | |
| ["checkout", "-q", "FETCH_HEAD"]): | |
| subprocess.run(git + cmd, capture_output=True, text=True, check=True, timeout=600) | |
| except (OSError, subprocess.SubprocessError): | |
| shutil.rmtree(tmp, ignore_errors=True) | |
| tmp.mkdir(parents=True) | |
| _villa_tarball(tmp, VILLA_COMMIT) | |
| (tmp / "COMMIT").write_text(VILLA_COMMIT + "\n") | |
| shutil.rmtree(dest, ignore_errors=True) | |
| os.replace(tmp, dest) | |
| src = dest / "vesuvius" / "src" | |
| info = {"source": "pinned download", "commit": (dest / "COMMIT").read_text().strip()} | |
| if not (src / "vesuvius" / "ink_detection" / "inference" / "infer.py").is_file(): | |
| raise EngineError(f"{src} holds no villa vesuvius package") | |
| if str(src) not in sys.path: | |
| sys.path.insert(0, str(src)) | |
| info["pinned"] = VILLA_COMMIT | |
| return info | |
| def load_villa_model(name, cache=CACHE): | |
| """(model, preprocessing, amp dtype, info) for one reader, built by villa's configure_model. The checkpoint is | |
| opened with torch.load(weights_only=True): villa's loader allows any pickle, so it is replaced for this call.""" | |
| import argparse | |
| import torch | |
| villa = ensure_villa(cache) | |
| path = ensure_checkpoint(name, cache) | |
| from vesuvius.ink_detection.inference import infer | |
| original = infer.load_checkpoint | |
| infer.load_checkpoint = lambda p: torch.load(str(p), map_location="cpu", weights_only=True) | |
| try: | |
| cm = infer.configure_model(argparse.Namespace(checkpoint=path, amp_dtype="auto")) | |
| finally: | |
| infer.load_checkpoint = original | |
| got = (int(cm.patch_size), int(cm.input_depth)) | |
| if got != (PATCH, DEPTH): | |
| raise EngineError(f"{name} has patch and depth {got}; Ink Lens is built for {(PATCH, DEPTH)}") | |
| if cm.preprocessing not in ("tifxyz_robust", "divide_255"): | |
| raise EngineError(f"{name} asks for preprocessing {cm.preprocessing!r}, which Ink Lens does not run") | |
| info = {"name": name, "sha256": READERS[name]["sha256"], "url": READERS[name]["url"], | |
| "license": READERS[name]["license"], "preprocessing": cm.preprocessing, | |
| "amp_on_cuda": str(cm.amp_dtype).replace("torch.", ""), "villa": villa, "loader": "weights_only=True"} | |
| return cm.model.eval(), cm.preprocessing, cm.amp_dtype, info | |
| # ---------------------------------------------------------------- the torch engine | |
| def pick_device(prefer=None): | |
| import torch | |
| if prefer: | |
| return torch.device(prefer) | |
| if torch.cuda.is_available(): | |
| return torch.device("cuda") | |
| if getattr(torch.backends, "mps", None) is not None and torch.backends.mps.is_available(): | |
| return torch.device("mps") | |
| return torch.device("cpu") | |
| class Engine: | |
| """Readers loaded once; run() does one region on one device. | |
| models: {name: (module, preprocessing, amp_dtype, info)} to use instead of downloading (the tests pass a | |
| stand-in); otherwise every name in `readers` is loaded through villa.""" | |
| def __init__(self, readers=("d9v2",), cache=CACHE, models=None, place_on_cuda=True): | |
| t0 = time.time() | |
| import torch | |
| self.torch = torch | |
| self.models = dict(models) if models else {n: load_villa_model(n, cache) for n in readers} | |
| if place_on_cuda and torch.cuda.is_available(): | |
| for m in self.models.values(): | |
| m[0].to("cuda") | |
| self.startup_s = round(time.time() - t0, 1) | |
| def describe(self): | |
| return {"engine": f"ink-lens {VERSION}", "readers": {n: m[3] for n, m in self.models.items()}, | |
| "patch": PATCH, "stride": STRIDE, "depth": DEPTH, "stack": STACK, "blend": "hann (villa, floored)", | |
| "batch": BATCH, "torch": self.torch.__version__} | |
| def run(self, volume, reader="d9v2", windows=3, device=None, batch=BATCH): | |
| """{"forward": F, "reverse": R, "per_reader": {...}, ...} for one uint8 region, float32 maps in 0 to 1.""" | |
| import numpy as np | |
| torch = self.torch | |
| names = readers_of(reader) | |
| missing = [n for n in names if n not in self.models] | |
| if missing: | |
| raise EngineError(f"reader {', '.join(missing)} is not loaded here") | |
| check_volume(volume, windows) | |
| dev = pick_device(device) | |
| t0 = time.perf_counter() | |
| vol = stack(np.asarray(volume), windows) | |
| L, H, W = vol.shape | |
| stack_start = int(np.asarray(volume).shape[0] // 2 - L // 2) | |
| vt = torch.from_numpy(np.ascontiguousarray(vol)).to(dev) | |
| coords = [(y, x) for y in positions(H) for x in positions(W)] | |
| wmap = torch.from_numpy(hann2d()).to(dev) | |
| per_reader, n_patches = {}, 0 | |
| for name in names: | |
| model, prep, amp, _ = self.models[name] | |
| model = model.to(dev) | |
| maps = {"forward": [], "reverse": []} | |
| for off, direction in passes(windows): | |
| idx = torch.tensor(window_indices(L, off, direction), device=dev) | |
| sub = vt.index_select(0, idx) | |
| psum = torch.zeros((H, W), dtype=torch.float32, device=dev) | |
| wsum = torch.zeros((H, W), dtype=torch.float32, device=dev) | |
| for b0 in range(0, len(coords), batch): | |
| chunk = coords[b0:b0 + batch] | |
| raw = torch.stack([sub[:, y:y + PATCH, x:x + PATCH] for y, x in chunk]) # (B, 17, 128, 128) | |
| keep = raw.reshape(len(chunk), -1).any(dim=1) # villa skips all-zero patches, before normalizing | |
| if not bool(keep.any()): | |
| continue | |
| if not bool(keep.all()): | |
| chunk = [c for c, k in zip(chunk, keep.tolist()) if k] | |
| raw = raw[keep] | |
| flat = raw.reshape(len(chunk), -1).float() | |
| norm = robust_torch(flat) if prep == "tifxyz_robust" else flat / 255.0 | |
| images = norm.reshape(len(chunk), 1, DEPTH, PATCH, PATCH) | |
| ctx = (torch.autocast("cuda", dtype=amp) if dev.type == "cuda" and amp is not None | |
| else _null()) | |
| with torch.inference_mode(), ctx: | |
| logits = model(images) | |
| prob = logits.float().sigmoid() | |
| if prob.shape[-2:] != (PATCH, PATCH): | |
| prob = torch.nn.functional.interpolate(prob, size=(PATCH, PATCH), mode="bilinear", | |
| align_corners=False) | |
| prob = prob[:, 0] | |
| for (y, x), p in zip(chunk, prob): | |
| psum[y:y + PATCH, x:x + PATCH] += p * wmap | |
| wsum[y:y + PATCH, x:x + PATCH] += wmap | |
| n_patches += len(chunk) | |
| out = torch.where(wsum > 1e-6, psum / wsum.clamp_min(1e-6), torch.zeros_like(psum)).clamp(0, 1) | |
| maps[direction].append(out) | |
| per_reader[name] = {d: torch.stack(m).mean(0).cpu().numpy().astype(np.float32) for d, m in maps.items()} | |
| if dev.type == "cuda": | |
| torch.cuda.synchronize(dev) | |
| F = np.mean([per_reader[n]["forward"] for n in names], axis=0).astype(np.float32) | |
| R = np.mean([per_reader[n]["reverse"] for n in names], axis=0).astype(np.float32) | |
| name = torch.cuda.get_device_name(dev) if dev.type == "cuda" else (platform.processor() or platform.machine()) | |
| return {"forward": F, "reverse": R, "per_reader": per_reader, "readers": list(names), "windows": windows, | |
| "window_layers": {f"{o}:{d}": window_indices(L, o, d) for o, d in passes(windows)}, | |
| "stack_layers": int(L), "stack_start": stack_start, "patches": n_patches, "device": str(dev), "device_name": name, | |
| "seconds": round(time.perf_counter() - t0, 3)} | |
| class _null: | |
| def __enter__(self): | |
| return None | |
| def __exit__(self, *exc): | |
| return False | |
| def main(argv=None): | |
| import argparse | |
| import json | |
| ap = argparse.ArgumentParser(description="Ink Lens engine helpers") | |
| sub = ap.add_subparsers(dest="cmd", required=True) | |
| e = sub.add_parser("estimate") | |
| e.add_argument("height", type=int) | |
| e.add_argument("width", type=int) | |
| e.add_argument("--windows", type=int, default=3, choices=(1, 3)) | |
| e.add_argument("--reader", default="d9v2", choices=READER_CHOICES) | |
| f = sub.add_parser("fetch") | |
| f.add_argument("reader", choices=sorted(READERS)) | |
| a = ap.parse_args(argv) | |
| try: | |
| if a.cmd == "estimate": | |
| print(json.dumps(estimate(a.height, a.width, a.windows, a.reader), indent=1)) | |
| else: | |
| print(json.dumps({"reader": a.reader, "path": str(ensure_checkpoint(a.reader))})) | |
| except EngineError as exc: | |
| print(f"refused: {exc}", file=sys.stderr) | |
| return 2 | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |