Spaces:
Running on Zero
Running on Zero
File size: 42,667 Bytes
fb722f1 f0be2e8 fb722f1 f0be2e8 fb722f1 f0be2e8 fb722f1 f0be2e8 fb722f1 aa04a26 fb722f1 aa04a26 fb722f1 aa04a26 fb722f1 aa04a26 fb722f1 0f3d4ae fb722f1 0f3d4ae fb722f1 f0be2e8 fb722f1 f0be2e8 fb722f1 f0be2e8 fb722f1 f0be2e8 fb722f1 aa04a26 fb722f1 fc41ae9 aa04a26 fb722f1 fc41ae9 fb722f1 f0be2e8 fb722f1 f0be2e8 fb722f1 f0be2e8 fc41ae9 fb722f1 f0be2e8 fb722f1 f0be2e8 fb722f1 f0be2e8 fc41ae9 fb722f1 aa04a26 fb722f1 | 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 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 | """Lasagna Fields: make villa's lasagna field (cos, grad_mag, nx, ny) for one box of a public scan.
Everything that is not the web page: the scan list (the open data's metadata.json), reading a box of CT from the
open-data bucket, running a model on it, reading the field back, the agreement check against a published field,
pictures and the download. app.py is the page and the API.
The one model today is villa's own lasagna: its `predict3d` (lasagna/preprocess_cos_omezarr.py at a pinned commit)
with the `scrollprize/lasagna` checkpoint, called in this process exactly as its command line would be, so the field
is villa's field, not a copy of it. MODELS is the slot for other models (a faster student, a better net): each entry
names its runner, and a model is offered only when its entry says it is released.
python3 lasfield.py selftest the plumbing on synthetic CT (no network, no model)
python3 lasfield.py run VOLUME Z Y X [SIZE] one field on this computer (CUDA if present, else CPU)
"""
import concurrent.futures
import gzip
import hashlib
import io
import json
import math
import os
import re
import shutil
import subprocess
import sys
import tarfile
import tempfile
import threading
import time
import urllib.error
import urllib.request
import zipfile
from pathlib import Path
import numpy as np
BUCKET = "https://vesuvius-challenge-open-data.s3.us-east-1.amazonaws.com"
METADATA = BUCKET + "/metadata.json"
USER_AGENT = "lasagna-fields-space (huggingface.co/spaces/SharpMeow/lasagna-fields)"
# Scans this Space never reads: PHercParis4 is kept out of every public tool of ours (Chase's rule).
REFUSED_SAMPLES = {"phercparis4"}
CACHE = Path(os.environ.get("LASFIELD_CACHE") or Path(tempfile.gettempdir()) / "lasagna-fields")
# villa's lasagna at the commit the ten whole-scan fields of this Space were made with (docs/tools/lasagna-fields.md);
# lasagna/ imports vesuvius from vesuvius/src (train_unet_3d builds the net with vesuvius' NetworkFromConfig).
VILLA = dict(repo="https://github.com/ScrollPrize/villa.git",
tarball="https://codeload.github.com/ScrollPrize/villa/tar.gz/{commit}",
commit="db3dbea26185a9e09ab997bfa21a2a00477f0d56", keep=("lasagna/", "vesuvius/src/"),
check="lasagna/preprocess_cos_omezarr.py")
WEIGHTS = dict(repo="scrollprize/lasagna", revision="de45f01147adea9f4ad0f621b1ccd885a1f03760", file="model_current.pt",
sha256="4cc0c40a846b296769b9f7982470559078662a1a0b0e1f74ff41ed9341c0d600")
TILE = 192 # predict3d's tile at this checkpoint (its patch size), stride TILE - overlap 64
MARGIN = TILE + 16 + 4 # CT read around the box: a tile, predict3d's border and the scaledown rounding
# predict3d widens a crop to whole output chunks (64 voxels of the quarter grid: 256 input voxels), so boxes are whole
# 256-voxel blocks of the input level: anything smaller costs the same, and the field matches villa's whole-scan run.
ALIGN = 256
SIZES = (256, 512)
DEFAULT_SIZE = 256
TARGET_UM = 9.4 # the model's training resolution: 1.2 m scans at level 0, 0.2 m (2.4 um) scans at level 2
CHANNELS = ("cos", "grad_mag", "nx", "ny")
class FieldError(Exception):
"""A refusal or failure shown to the visitor as is."""
def log(message):
print(f"[lasfield {time.strftime('%H:%M:%S')}] {message}", flush=True)
# ---------------------------------------------------------------- http
def http_get(url, timeout=60, tries=4):
"""Bytes at `url`, or None for a missing object (an unwritten chunk is a 403 or 404 on the bucket)."""
err = None
for i in range(tries):
try:
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(req, timeout=timeout) as r:
return r.read()
except urllib.error.HTTPError as exc:
if exc.code in (403, 404):
return None
err = exc
except (urllib.error.URLError, OSError) as exc:
err = exc
time.sleep(1.5 * (i + 1))
raise FieldError(f"could not read {url}: {err}")
def get_json(url):
raw = http_get(url)
if raw is None:
raise FieldError(f"{url} does not exist")
if raw[:2] == b"\x1f\x8b":
raw = gzip.decompress(raw)
return json.loads(raw)
# ---------------------------------------------------------------- the scans
_CATALOG = {"at": 0.0, "volumes": None}
_LOCK = threading.Lock()
def level_for(um):
"""The CT level whose voxels are nearest the model's 9.4 um (0 for 8.6 to 9.4 um scans, 2 for 2.4 um)."""
return max(0, int(round(math.log2(TARGET_UM / float(um))))) if um else 0
def catalog(refresh_s=6 * 3600):
"""Every volume of the open data's metadata.json this Space may read, by volume id."""
with _LOCK:
if _CATALOG["volumes"] is not None and time.time() - _CATALOG["at"] < refresh_s:
return _CATALOG["volumes"]
meta = get_json(METADATA)
vols = {}
for sid, s in sorted((meta.get("samples") or {}).items()):
if sid.lower() in REFUSED_SAMPLES:
continue
for vid, v in (s.get("volumes") or {}).items():
pr = v.get("properties") or {}
um, shape = pr.get("pixel_size_um"), pr.get("shape")
if not um or not v.get("long_id"):
continue
vols[vid] = {"volume": vid, "sample": sid, "long_id": v["long_id"], "voxel_um": float(um),
"shape_zyx": shape, "level": level_for(um),
"superseded_by": pr.get("superseded_by"),
"url": f"{BUCKET}/{sid}/volumes/{v['long_id']}/"}
_CATALOG.update(at=time.time(), volumes=vols)
return vols
def volume(vid):
"""The catalog row for a volume id (the 14-digit id or its long name), or a refusal."""
vid = str(vid or "").strip().rstrip("/").split("/")[-1]
vols = catalog()
if vid in vols:
return vols[vid]
for v in vols.values():
if v["long_id"] == vid or v["long_id"].startswith(vid + "-"):
return v
if any(vid.lower().startswith(s) for s in REFUSED_SAMPLES):
raise FieldError("this Space does not read PHercParis4")
raise FieldError(f"{vid!r} is not a volume in the Vesuvius Challenge open data (metadata.json); "
"give its 14-digit volume id, such as 20260102150214")
def zarray(v, level):
meta = get_json(f"{v['url']}{level}/.zarray")
if len(meta.get("shape", ())) != 3:
raise FieldError(f"{v['long_id']} level {level} is not a 3D array")
return meta
_PUBLISHED = {}
# the ten whole-scan fields this Space hosts, made with the same villa commit and checkpoint as Make a field: volume id
# to (sample, run folder) in the public bucket; the check uses them where the open data has no field
OWN_BUCKET = "https://huggingface.co/buckets/SharpMeow/lasagna-fields/resolve"
OWN_FIELDS = {
"20250521115057": ("PHerc0175A", "20250521115057-lasagna-de45f011"),
"20250521125822": ("PHerc0175B", "20250521125822-lasagna-de45f011"),
"20250521133212": ("PHerc0306B", "20250521133212-lasagna-de45f011"),
"20250521140913": ("PHerc0483A", "20250521140913-lasagna-de45f011"),
"20251124083638": ("PHerc0483B", "20251124083638-lasagna-de45f011"),
"20250521151210": ("PHerc0490A", "20250521151210-lasagna-de45f011"),
"20250521151215": ("PHerc0490B", "20250521151215-lasagna-de45f011"),
"20250821151531": ("PHerc0841", "20250821151531-lasagna-de45f011"),
"20250728152254": ("PHerc0846A", "20250728152254-lasagna-de45f011"),
"20250804142305": ("PHerc0846B", "20250804142305-lasagna-de45f011"),
}
def published_field(v):
"""The open data's own lasagna run for this volume at its default level, if there is one: its manifest's URL
and groups. The run folder names start with the volume id; an -L2 suffix says it read level 2."""
key = v["volume"]
if key in _PUBLISHED:
return _PUBLISHED[key]
found = _published_run(v)
if found is None:
# a remasked volume replaces an older one in the same voxel frame (metadata.json's superseded_by); the field
# published for the older one is the one to check against, with the mask edge the only difference
for old in catalog().values():
if (old.get("superseded_by") or {}).get("volume_id") == key and old["level"] == v["level"]:
found = _published_run(old)
if found:
found = dict(found, note=f"published for {old['volume']}, which this volume replaces in the "
"same voxel frame")
break
if found is None and key in OWN_FIELDS and v["level"] == 0:
found = _own_run(*OWN_FIELDS[key])
_PUBLISHED[key] = found
return found
def _own_run(sample, run):
"""One of this Space's ten whole-scan fields, described like an open-data run."""
url = f"{OWN_BUCKET}/{sample}/representations/predictions/lasagna/{run}/"
try:
manifest = get_json(f"{url}{sample}.lasagna.json")
except FieldError as exc:
log(f"own field lookup for {sample} failed: {exc}")
return None
return {"run": run, "url": url, "manifest": f"{url}{sample}.lasagna.json", "groups": manifest.get("groups"),
"base_shape_zyx": manifest.get("base_shape_zyx"), "source": "lasagna-fields",
"note": "this Space's own whole-scan field, made with the same villa commit and checkpoint"}
def _published_run(v):
found = None
try:
prefix = f"{v['sample']}/representations/predictions/lasagna/"
raw = http_get(f"{BUCKET}/?list-type=2&delimiter=/&prefix={prefix}") or b""
runs = re.findall(rb"<Prefix>([^<]+)</Prefix>", raw)
for run in sorted(r.decode() for r in runs if r.decode() != prefix):
name = run[len(prefix):].rstrip("/")
tail = name.rsplit("-", 1)[-1]
lvl = int(tail[1:]) if re.fullmatch(r"L\d", tail) else 0
if not name.startswith(v["volume"]) or lvl != v["level"]:
continue
listing = http_get(f"{BUCKET}/?list-type=2&delimiter=/&prefix={run}") or b""
js = [k.decode() for k in re.findall(rb"<Key>([^<]+\.lasagna\.json)</Key>", listing)]
if js:
manifest = get_json(f"{BUCKET}/{js[0]}")
found = {"run": name, "url": f"{BUCKET}/{run}", "manifest": f"{BUCKET}/{js[0]}",
"groups": manifest.get("groups"), "base_shape_zyx": manifest.get("base_shape_zyx"),
"source": "open-data"}
except FieldError as exc:
log(f"published field lookup for {v['volume']} failed: {exc}")
return found
# ---------------------------------------------------------------- a box
def plan(vid, z, y, x, size=DEFAULT_SIZE, level=None, model="lasagna"):
"""Where the field will be made: the `size`-voxel box of whole ALIGN blocks at the input level that holds
level-0 voxel (z, y, x) (as near its middle as the grid allows), and the CT read around it. Refuses a point
outside the scan or a size not offered."""
v = volume(vid)
spec = MODELS.get(model)
if not spec or not spec["offered"]:
raise FieldError(f"model {model!r} is not offered; choose from {', '.join(k for k, m in MODELS.items() if m['offered'])}")
size = int(size)
if size not in SIZES:
raise FieldError(f"size {size} is not offered; choose from {', '.join(map(str, SIZES))}")
level = v["level"] if level in (None, "", "auto") else int(level)
if not 0 <= level <= 5:
raise FieldError("level must be 0 to 5")
base = zarray(v, 0)
meta = zarray(v, level)
shape = [int(s) for s in meta["shape"]]
f = 2 ** level
center = [int(round(float(c))) for c in (z, y, x)]
for c, n in zip(center, base["shape"]):
if not 0 <= c < n:
raise FieldError(f"the point {center} is outside the scan (level-0 shape {base['shape']})")
origin = [max(0, min((c // f - size // 2 + ALIGN // 2) // ALIGN * ALIGN, (n - 1) // ALIGN * ALIGN))
for c, n in zip(center, shape)]
box_size = [min(size, n - o) for o, n in zip(origin, shape)]
read = [[max(0, o - MARGIN), min(n, o + s + MARGIN)] for o, s, n in zip(origin, box_size, shape)]
ch = meta["chunks"]
nchunks = math.prod((b - 1) // c - a // c + 1 for (a, b), c in zip(read, ch))
out = {"volume": v["volume"], "sample": v["sample"], "long_id": v["long_id"], "voxel_um": v["voxel_um"],
"url": v["url"], "level": level, "level_shape_zyx": shape, "base_shape_zyx": base["shape"],
"center_level0_zyx": center, "origin_zyx": origin, "size_zyx": box_size,
"origin_level0_zyx": [o * f for o in origin], "size_level0_zyx": [s * f for s in box_size],
"read_zyx": read, "chunks": nchunks, "chunk_bytes": math.prod(ch) * np.dtype(meta["dtype"]).itemsize,
"model": model}
out["tiles"] = tiles(out)
out["gpu_s"] = gpu_seconds(out, model)
return out
def tiles(p_or_size, tile=TILE, overlap=64, border=16, sd_max=4):
"""Tiles predict3d runs for a box. predict3d puts tiles on one lattice over the whole scan (so a box's field is
the whole-scan field there) and runs every lattice tile that touches the box plus its border; with villa loaded
this asks villa's own _canonical_local_tile_positions, else it counts the same lattice here."""
if isinstance(p_or_size, dict):
f = 2 ** p_or_size["level"]
starts = p_or_size["origin_zyx"]
sizes = p_or_size["size_zyx"]
shape = p_or_size["level_shape_zyx"]
else:
sizes, starts, shape = list(p_or_size), [10 * 1024] * 3, [100 * 1024] * 3
stride = tile - overlap
try:
from tiled_predict3d import _canonical_local_tile_positions as positions
except ImportError:
positions = None
n = 1
for s0, sz, vol in zip(starts, sizes, shape):
padded = -(-(sz + 2 * border) // sd_max) * sd_max
if positions is not None:
n *= len(positions(volume_size=vol, crop_start=s0, crop_padded_size=padded, tile_size=tile,
stride=stride, border=border, scaledown_multiple=sd_max))
else:
first, last = max(0, (s0 - tile) // stride), (s0 + padded) // stride
n *= sum(1 for k in range(first, last + 1) if k * stride - s0 < padded and k * stride - s0 + tile > 0)
return n
def gpu_seconds(p_or_size, model="lasagna"):
"""The GPU time to ask ZeroGPU for: the model's start-up plus its per-tile time, with a margin of 1.5."""
spec = MODELS[model]
return int(math.ceil(1.5 * (spec["startup_s"] + spec["tile_s"] * tiles(p_or_size))))
# ---------------------------------------------------------------- the CT
def stage(p, root):
"""The CT around the box on local disk, laid out like the open data: an OME-Zarr group whose level 0 is only its
metadata (predict3d reads the scan's base shape from it) and whose input level holds the chunks it reads."""
root = Path(root)
shutil.rmtree(root, ignore_errors=True)
meta0 = get_json(f"{p['url']}0/.zarray")
meta = get_json(f"{p['url']}{p['level']}/.zarray")
zattrs = get_json(f"{p['url']}.zattrs")
sep = meta.get("dimension_separator", ".")
(root / "0").mkdir(parents=True)
(root / "0" / ".zarray").write_text(json.dumps(meta0))
lvl = root / str(p["level"])
lvl.mkdir(parents=True, exist_ok=True)
(lvl / ".zarray").write_text(json.dumps(meta))
(root / ".zgroup").write_text(json.dumps({"zarr_format": 2}))
(root / ".zattrs").write_text(json.dumps(zattrs))
ch = meta["chunks"]
keys = [(i, j, k) for i in range(p["read_zyx"][0][0] // ch[0], (p["read_zyx"][0][1] - 1) // ch[0] + 1)
for j in range(p["read_zyx"][1][0] // ch[1], (p["read_zyx"][1][1] - 1) // ch[1] + 1)
for k in range(p["read_zyx"][2][0] // ch[2], (p["read_zyx"][2][1] - 1) // ch[2] + 1)]
def one(key):
name = sep.join(map(str, key))
raw = http_get(f"{p['url']}{p['level']}/{name}")
if raw is None:
return 0
dest = lvl.joinpath(*name.split("/"))
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_bytes(raw)
return len(raw)
t = time.time()
with concurrent.futures.ThreadPoolExecutor(32) as ex:
sizes = list(ex.map(one, keys))
return {"chunks": len(keys), "written": sum(1 for s in sizes if s), "bytes": sum(sizes),
"seconds": round(time.time() - t, 2), "input": str(lvl)}
def read_box(arr_path, box):
"""One zarr array's values in a box [[z0, z1], [y0, y1], [x0, x1]] (zarr 3 reads villa's v2 output)."""
import zarr
a = zarr.open_array(str(arr_path), mode="r")
return np.asarray(a[box[0][0]:box[0][1], box[1][0]:box[1][1], box[2][0]:box[2][1]])
def ct_box(p, root):
lvl = Path(root) / str(p["level"])
import zarr
a = zarr.open_array(str(lvl), mode="r")
o, s = p["origin_zyx"], p["size_zyx"]
return np.asarray(a[o[0]:o[0] + s[0], o[1]:o[1] + s[1], o[2]:o[2] + s[2]])
# ---------------------------------------------------------------- code and weights
def sha256_file(path, block=1 << 22):
h = hashlib.sha256()
with open(path, "rb") as fh:
for part in iter(lambda: fh.read(block), b""):
h.update(part)
return h.hexdigest()
def _fetch_villa(dest):
"""villa's lasagna/ and vesuvius/src/ at the pinned commit: a sparse, blobless git fetch, else GitHub's tarball."""
commit = VILLA["commit"]
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", *[k.rstrip("/") for k in VILLA["keep"]]],
["fetch", "-q", "--depth", "1", "--filter=blob:none", "origin", commit],
["checkout", "-q", "FETCH_HEAD"]):
subprocess.run(git + cmd, capture_output=True, text=True, check=True, timeout=900)
head = subprocess.run(git + ["rev-parse", "HEAD"], capture_output=True, text=True, check=True).stdout.strip()
if head != commit:
raise RuntimeError(f"git fetched {head}, not {commit}")
shutil.rmtree(tmp / ".git", ignore_errors=True)
except (OSError, RuntimeError, subprocess.SubprocessError) as exc:
log(f"git fetch of villa failed ({getattr(exc, 'stderr', None) or exc}); trying the tarball")
shutil.rmtree(tmp, ignore_errors=True)
tmp.mkdir(parents=True)
root = tmp.resolve()
req = urllib.request.Request(VILLA["tarball"].format(commit=commit), headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(req, timeout=900) as resp, tarfile.open(fileobj=resp, mode="r|gz") as tf:
for m in tf:
_, _, rel = m.name.partition("/")
if not rel.startswith(VILLA["keep"]) or m.issym() or m.islnk():
continue
target = (root / rel).resolve()
if root not in target.parents:
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)
if not (tmp / VILLA["check"]).is_file():
raise RuntimeError(f"villa at {commit} has no {VILLA['check']}")
(tmp / "COMMIT").write_text(commit + "\n")
shutil.rmtree(dest, ignore_errors=True)
os.replace(tmp, dest)
def ensure_villa(cache=CACHE):
"""villa's lasagna on sys.path (as top-level modules and as the `lasagna` package, the way its own install lays
them out) with vesuvius/src beside it. LASFIELD_VILLA names a local villa checkout instead."""
if os.environ.get("LASFIELD_VILLA"):
root = Path(os.environ["LASFIELD_VILLA"]).expanduser().resolve()
source = "local checkout (LASFIELD_VILLA)"
else:
root = Path(cache) / f"villa-{VILLA['commit'][:12]}"
if not (root / "COMMIT").is_file():
log(f"fetching villa {VILLA['commit'][:12]}")
_fetch_villa(root)
source = "pinned download"
shim = Path(cache) / "villa-shim"
shim.mkdir(parents=True, exist_ok=True)
link = shim / "lasagna"
if not link.exists():
link.symlink_to(root / "lasagna", target_is_directory=True)
for path in (root / "vesuvius" / "src", shim, root / "lasagna"):
if str(path) not in sys.path:
sys.path.insert(0, str(path))
os.environ["PYTHONPATH"] = os.pathsep.join([str(root / "lasagna"), str(shim), str(root / "vesuvius" / "src"),
os.environ.get("PYTHONPATH", "")]).rstrip(os.pathsep)
return {"source": source, "commit": VILLA["commit"], "path": str(root)}
def ensure_weights(cache=CACHE):
"""The checkpoint from the Hub at its pinned revision, refused unless its SHA-256 matches. LASFIELD_WEIGHTS names
a local copy of model_current.pt instead (its SHA-256 is still checked)."""
if os.environ.get("LASFIELD_WEIGHTS"):
path = Path(os.environ["LASFIELD_WEIGHTS"]).expanduser()
else:
from huggingface_hub import hf_hub_download
path = Path(hf_hub_download(WEIGHTS["repo"], WEIGHTS["file"], revision=WEIGHTS["revision"],
cache_dir=str(Path(cache) / "hub")))
digest = sha256_file(path)
if digest != WEIGHTS["sha256"]:
raise RuntimeError(f"{WEIGHTS['repo']}/{WEIGHTS['file']} has SHA-256 {digest}, not {WEIGHTS['sha256']}")
return path
# ---------------------------------------------------------------- the models
def predict3d_argv(p, ct_input, out_json, checkpoint, device):
"""villa predict3d's command line for the box, as its CLI takes it: --crop is x y z w h d in the scan's base
(level 0) voxels, which predict3d divides by the input level's factor (its log: base_crop, input_crop).
Synchronous flush and accumulation (no worker processes): the stored field is the same."""
o, s = p["origin_level0_zyx"], p["size_level0_zyx"]
return ["predict3d", "--input", str(ct_input), "--output", str(out_json), "--unet-checkpoint", str(checkpoint),
"--tile-size", str(TILE), "--crop", str(o[2]), str(o[1]), str(o[0]), str(s[2]), str(s[1]), str(s[0]),
"--device", device, "--no-download", "--flush-workers", "0", "--accumulator-workers", "0"]
POOL_CPUS = 8 # pyramid processes predict3d may start (its default is every CPU of the host)
@__import__("contextlib").contextmanager
def _pool_friendly():
"""Let predict3d's OME-Zarr pyramid step start its process pool when it runs inside a daemon process (ZeroGPU
runs a GPU call in one, and Python refuses children there), and cap that pool at POOL_CPUS. The pool's processes
only downsample chunks on the CPU. Restored on exit."""
import multiprocessing as mp
proc = mp.current_process()
was_daemon = proc._config.get("daemon")
real = mp.cpu_count
mp.cpu_count = lambda: max(1, min(POOL_CPUS, real()))
if was_daemon:
proc._config["daemon"] = False
try:
yield
finally:
mp.cpu_count = real
if was_daemon:
proc._config["daemon"] = was_daemon
def run_villa_lasagna(p, ct_input, out_dir, device, checkpoint):
"""villa's predict3d in this process. Returns its exit code, wall seconds and the tail of its log."""
import contextlib
import preprocess_cos_omezarr as pco
out_dir = Path(out_dir)
shutil.rmtree(out_dir, ignore_errors=True)
out_dir.mkdir(parents=True)
out_json = out_dir / f"{p['sample']}-{p['volume']}.lasagna.json"
argv = predict3d_argv(p, ct_input, out_json, checkpoint, device)
buf = io.StringIO()
t = time.time()
class Tee(io.TextIOBase):
def write(self, s):
buf.write(s)
sys.__stdout__.write(s)
return len(s)
try:
with contextlib.redirect_stdout(Tee()), contextlib.redirect_stderr(Tee()), _pool_friendly():
rc = pco.cli_main(argv)
except SystemExit as exc:
rc = exc.code if isinstance(exc.code, int) else 1
return {"exit": int(rc or 0), "wall_s": round(time.time() - t, 2), "argv": argv,
"log_tail": buf.getvalue()[-6000:], "manifest": str(out_json)}
# The model slot. `offered` False keeps a model off the page and the API until it passes its own held-out test;
# Lasagnetta (the lasagna student) and any better net come in here with their own runner, checkpoint and costs.
MODELS = {
"lasagna": {"title": "villa lasagna (exact)", "offered": True, "run": run_villa_lasagna,
"about": "villa's own predict3d with the scrollprize/lasagna checkpoint (MIT): the same field villa "
"makes, at the same grids (cos at half, the rest at a quarter of the input level)",
# 25 Oct 2026, L4: 310 ms a 192^3 tile end to end; the GPU here is faster, so these are ceilings
"startup_s": 25, "tile_s": 0.35},
}
# ---------------------------------------------------------------- the field
def field_boxes(p, manifest):
"""Each channel's array path and the box covering the plan's box in that channel's grid."""
man = json.loads(Path(manifest).read_text())
root = Path(manifest).parent
out = {}
for name in CHANNELS:
g = man["groups"][name]
sd = 2 ** int(g["scaledown"])
box = [[o // sd, -(-(o + s) // sd)] for o, s in zip(p["origin_level0_zyx"], p["size_level0_zyx"])]
out[name] = {"path": root / g["zarr"], "scaledown": int(g["scaledown"]), "box": box}
return out, man
def read_field(p, manifest):
boxes, man = field_boxes(p, manifest)
arrays = {n: read_box(b["path"], b["box"]) for n, b in boxes.items()}
return arrays, boxes, man
def decode_normal(nx_u8, ny_u8):
"""The unit normal from stored nx, ny (villa's encode_normal_nxny_u8 flips n so that nz >= 0)."""
nx = (nx_u8.astype(np.float32) - 128.0) / 127.0
ny = (ny_u8.astype(np.float32) - 128.0) / 127.0
nz = np.sqrt(np.clip(1.0 - nx * nx - ny * ny, 0.0, 1.0))
n = np.stack([nx, ny, nz])
return n / (np.linalg.norm(n, axis=0, keepdims=True) + 1e-8)
def _corr(a, b, mask):
a, b = a[mask].astype(np.float64), b[mask].astype(np.float64)
if a.size < 100 or a.std() == 0 or b.std() == 0:
return None
return round(float(np.corrcoef(a, b)[0, 1]), 4)
def compare(mine, theirs):
"""Agreement of two fields on the same grids: Pearson r of cos and grad_mag where either is non-zero, and the
median angle between normals (sign ignored) where both are, over all those voxels and over the strong half (our
grad_mag at or above its median there). Where the layer signal is weak the normal is barely defined and tiny
numerical differences turn it, so the strong half is the fair test."""
m_cos = (mine["cos"] > 0) | (theirs["cos"] > 0)
m_oth = (mine["grad_mag"] > 0) | (theirs["grad_mag"] > 0)
both = (mine["nx"] > 0) & (theirs["nx"] > 0)
out = {"cos_r": _corr(mine["cos"], theirs["cos"], m_cos),
"grad_mag_r": _corr(mine["grad_mag"], theirs["grad_mag"], m_oth), "normal_median_deg": None,
"voxels": int(m_cos.sum())}
if both.sum() >= 100:
a, b = decode_normal(mine["nx"], mine["ny"]), decode_normal(theirs["nx"], theirs["ny"])
dot = np.abs((a * b).sum(axis=0))
out["normal_median_deg"] = round(float(np.degrees(np.arccos(np.clip(np.median(dot[both]), 0, 1)))), 2)
g = mine["grad_mag"]
strong = both & (g >= np.median(g[both]))
if strong.sum() >= 100:
out["normal_median_deg_strong"] = round(
float(np.degrees(np.arccos(np.clip(np.median(dot[strong]), 0, 1)))), 2)
return out
def read_published(pub, boxes, shift=(0, 0, 0)):
"""The published field's values in the same boxes, optionally shifted (in each channel's voxels)."""
import zarr
out = {}
for name, b in boxes.items():
g = (pub["groups"] or {}).get(name)
if not g or int(g["scaledown"]) != b["scaledown"]:
return None
a = zarr.open_array(pub["url"] + g["zarr"], mode="r", zarr_format=2)
box = [[lo + d, hi + d] for (lo, hi), d in zip(b["box"], shift)]
if any(lo < 0 or hi > n for (lo, hi), n in zip(box, a.shape)):
return None
out[name] = np.asarray(a[box[0][0]:box[0][1], box[1][0]:box[1][1], box[2][0]:box[2][1]])
return out
CONTROL_SHIFT = 48 # input-level voxels: the control reads the published field this far away in y and x
OPEN_DATA_NORMALS = ("cos and grad_mag should match the open data's field exactly. Its normals can differ in "
"parts of a box: villa changed how lasagna estimates normals after the open data's fields "
"were made (April 2026, by their run names), and Make a field runs the later code, the same "
"as this Space's ten whole-scan fields, which it matches exactly")
def agreement(p, arrays, boxes):
"""How close this field is to the open data's own lasagna field for the scan, where one exists, with a control
that must score lower: the published field read CONTROL_SHIFT input voxels away along y and x (a few papyrus
layers), the same physical offset on every channel's grid."""
pub = published_field(volume(p["volume"]))
if not pub:
return {"published": None, "note": "the open data has no lasagna field for this volume at this level"}
shift0 = CONTROL_SHIFT * 2 ** p["level"]
try:
same = read_published(pub, boxes)
shifted = {}
for name, b in boxes.items():
d = shift0 // 2 ** b["scaledown"]
r = read_published(pub, {name: b}, (0, d, d))
if r is None:
shifted = None
break
shifted[name] = r[name]
except Exception as exc: # noqa: BLE001 (reported, never fatal)
return {"published": pub["run"], "error": f"{type(exc).__name__}: {exc}"[:300]}
if same is None:
return {"published": pub["run"], "note": "the published field uses other grids; not compared"}
out = {"published": pub["run"], "published_url": pub["url"], "published_source": pub.get("source"),
"same_box": compare(arrays, same)}
if pub.get("note"):
out["published_note"] = pub["note"]
if pub.get("source") == "open-data":
out["normals_note"] = OPEN_DATA_NORMALS
if shifted is not None:
out["control_shifted"] = compare(arrays, shifted)
out["control_note"] = (f"the published field {CONTROL_SHIFT} input voxels away in y and x: "
"it must agree less than the same box")
return out
# ---------------------------------------------------------------- pictures and the download
def _u8(img, lo=None, hi=None):
img = img.astype(np.float32)
lo = np.percentile(img, 1) if lo is None else lo
hi = np.percentile(img, 99) if hi is None else hi
return np.clip((img - lo) / max(hi - lo, 1e-6) * 255, 0, 255).astype(np.uint8)
def _up(img, n):
return np.repeat(np.repeat(img, n, axis=0), n, axis=1) if n > 1 else img
def _resize(img, shape):
"""An image (H, W) or (H, W, 3) uint8 resized to `shape` (H, W) with bilinear smoothing, for display only."""
from PIL import Image
return np.asarray(Image.fromarray(img).resize((int(shape[1]), int(shape[0])), Image.BILINEAR))
def picture(ct, arrays, boxes, axis=0):
"""One middle slice through the box along `axis` (0 z, 1 y, 2 x): CT, cos, grad_mag and the normal as color
(|x| red, |y| green, |z| blue), side by side at the CT's grid. Returns an RGB uint8 array."""
def mid(a):
return np.take(a, a.shape[axis] // 2, axis=axis)
ct_img = _u8(mid(ct))
shape = ct_img.shape
panels = [np.stack([ct_img] * 3, -1)]
for name in ("cos", "grad_mag"):
img = _u8(mid(arrays[name]), 0, 255 if name == "cos" else None)
panels.append(np.stack([_resize(img, shape)] * 3, -1))
n = decode_normal(mid(arrays["nx"]), mid(arrays["ny"]))
rgb = (np.abs(np.moveaxis(n, 0, -1)) * 255).astype(np.uint8)
rgb[mid(arrays["nx"]) == 0] = 0
panels.append(_resize(rgb, shape))
gap = np.full((shape[0], 6, 3), 255, np.uint8)
row = []
for i, t in enumerate(panels):
row += [t] + ([gap] if i < len(panels) - 1 else [])
return np.concatenate(row, axis=1)
PANELS = ("CT", "cos", "grad_mag", "normal")
LEGEND = "normal as color: x red, y green, z blue · villa lasagna, model output"
def compose(pictures, title="", note=LEGEND):
"""The three rows (z, y, x middle slices) under one header naming the panels, each row named on its left, on a
white card. Text is sized for the card to be shown about 700 px wide. Returns an RGB uint8 array."""
from PIL import Image, ImageDraw, ImageFont
rows = [_fit(r, pictures[0].shape[:2]) if r.shape != pictures[0].shape else r for r in pictures]
h, w = rows[0].shape[:2]
k = max(1.0, w / 640)
try:
font = ImageFont.load_default(size=round(14 * k))
bold = ImageFont.load_default(size=round(16 * k))
except TypeError: # Pillow before 10.1
font = bold = ImageFont.load_default()
pw = (w - 18) // 4
left, gap = round(66 * k), round(8 * k)
top = round(26 * k) + (round(30 * k) if title else 0)
foot = round(26 * k) if note else 0
canvas = Image.new("RGB", (left + w + gap, top + 3 * h + 3 * gap + foot), (255, 255, 255))
d = ImageDraw.Draw(canvas)
if title:
d.text((left, round(6 * k)), title, fill=(35, 32, 28), font=bold)
for i, name in enumerate(PANELS):
d.text((left + i * (pw + 6) + 2, top - round(21 * k)), name, fill=(93, 85, 75), font=font)
for i, (r, name) in enumerate(zip(rows, ("z slice", "y slice", "x slice"))):
y0 = top + i * (h + gap)
canvas.paste(Image.fromarray(r), (left, y0))
d.text((round(6 * k), y0 + h // 2 - round(8 * k)), name, fill=(93, 85, 75), font=font)
if note:
d.text((left, top + 3 * h + 2 * gap + round(4 * k)), note, fill=(93, 85, 75), font=font)
return np.asarray(canvas)
def png(rgb):
from PIL import Image
buf = io.BytesIO()
Image.fromarray(rgb).save(buf, format="PNG", optimize=True)
return buf.getvalue()
def package(out_dir, p, arrays, receipt, dest):
"""The download: villa's own output folder (its .lasagna.json and four OME-Zarr channels, holding only this
box, in the scan's base coordinates, so lasagna's fit opens it as is), field.npz with the box's arrays, and the
receipt."""
dest = Path(dest)
with zipfile.ZipFile(dest, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as zf:
for f in sorted(Path(out_dir).rglob("*")):
if f.is_file():
zf.write(f, f"villa/{f.relative_to(out_dir)}")
buf = io.BytesIO()
np.savez_compressed(buf, **arrays)
zf.writestr("field.npz", buf.getvalue())
zf.writestr("receipt.json", json.dumps(receipt, indent=1))
zf.writestr("README.txt", README_TXT)
return dest
README_TXT = """Lasagna field for one box, made by the Lasagna Fields Space (huggingface.co/spaces/SharpMeow/lasagna-fields).
villa/ villa lasagna predict3d's own output: SCAN.lasagna.json and SCAN_{cos,grad_mag,nx,ny}.ome.zarr. Only the
chunks of this box are written; the arrays have the whole scan's shape, in its base (level 0) frame, so
villa's lasagna fit (lasagna/fit.py) opens the manifest as is.
field.npz the box as numpy arrays: cos and grad_mag, nx and ny (uint8, as villa stores them). Grids: see receipt.json
(boxes: each channel's voxel box and scaledown from level 0).
receipt.json what ran: scan, box, model, villa commit, checkpoint SHA-256, timings, and the agreement check.
Encodings (villa): cos = clip(cos * 255); grad_mag = clip(grad_mag * 1000 * grad_mag_factor); nx, ny = the unit
normal's x and y times 127 plus 128, with the normal flipped so its z is not negative.
Scan data: Vesuvius Challenge open data, CC BY-NC 4.0. Model and code: ScrollPrize/villa lasagna, MIT.
This is model output: geometry inputs, not a reading of any text.
"""
# ---------------------------------------------------------------- one run, end to end (no GPU decorator here)
def prepare(p, work):
"""Everything before the GPU: code, weights and the CT on disk."""
t = time.time()
code = ensure_villa()
ck = ensure_weights()
st = stage(p, Path(work) / "ct")
return {"code": code, "checkpoint": str(ck), "stage": st, "prepare_s": round(time.time() - t, 2)}
def finish(p, prep, ran, work, out_root):
"""Everything after the GPU: read the field, compare, draw, pack."""
if ran["exit"] != 0:
raise FieldError(f"villa predict3d exited {ran['exit']}: {ran['log_tail'][-800:]}")
arrays, boxes, man = read_field(p, ran["manifest"])
if not any(int(a.max()) for a in arrays.values()):
raise FieldError("the field is empty: the box is probably outside the scroll (masked CT is all zero there)")
ct = ct_box(p, Path(work) / "ct")
pictures = [picture(ct, arrays, boxes, axis) for axis in (0, 1, 2)]
agree = agreement(p, arrays, boxes)
receipt = {"made_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "plan": p,
"model": {"id": p["model"], "title": MODELS[p["model"]]["title"],
"villa_commit": VILLA["commit"], "checkpoint": f"{WEIGHTS['repo']}@{WEIGHTS['revision']}",
"checkpoint_file": WEIGHTS["file"], "checkpoint_sha256": WEIGHTS["sha256"]},
"predict3d_argv": [a if not a.startswith("/") else Path(a).name for a in ran["argv"]],
"manifest": {k: man[k] for k in man if k != "groups"},
"boxes": {n: {"scaledown": b["scaledown"], "box_zyx": b["box"]} for n, b in boxes.items()},
"timings": {"stage_s": prep["stage"]["seconds"], "stage_bytes": prep["stage"]["bytes"],
"model_s": ran["wall_s"]},
"agreement": agree,
"claim": "Model output: a lasagna field is geometry fitted by a model, not a reading of any text."}
out_root = Path(out_root)
out_root.mkdir(parents=True, exist_ok=True)
stem = f"{p['sample']}-{p['volume']}-z{p['center_level0_zyx'][0]}-y{p['center_level0_zyx'][1]}-x{p['center_level0_zyx'][2]}-s{p['size_zyx'][0]}"
zpath = package(Path(ran["manifest"]).parent, p, arrays, receipt, out_root / f"{stem}.zip")
return {"receipt": receipt, "zip": str(zpath), "pictures": pictures, "arrays": arrays}
def make(p, device=None, work=None, out_root=None, runner=None):
"""One field on this computer: stage, run, finish. `runner(p, ct_input, out_dir, device, checkpoint)` replaces
the model call (the Space passes its ZeroGPU-decorated one)."""
import torch
device = device or ("cuda" if torch.cuda.is_available() else "cpu")
work = Path(work or tempfile.mkdtemp(prefix="lasfield-"))
prep = prepare(p, work)
run = runner or MODELS[p["model"]]["run"]
ran = run(p, prep["stage"]["input"], work / "out", device, prep["checkpoint"])
return finish(p, prep, ran, work, out_root or work / "dl")
# ---------------------------------------------------------------- self-test (no network, no model)
def selftest():
"""The parts that need neither network nor model: grids, the tile count, the GPU ask, comparison and the
control, decoding and pictures. Each check can fail."""
ok = []
assert level_for(9.362) == 0 and level_for(8.64) == 0 and level_for(2.399) == 2 and level_for(1.129) == 3
ok.append("levels")
assert tiles([256] * 3) < tiles([512] * 3)
assert gpu_seconds([256] * 3) < gpu_seconds([512] * 3)
ok.append("tiles")
rng = np.random.default_rng(0)
z = np.arange(64)[:, None, None] * np.ones((1, 64, 64))
cos = (np.cos(z / 3.0) * 127 + 128).astype(np.uint8)
f = {"cos": cos, "grad_mag": (cos // 2 + 1).astype(np.uint8), "nx": np.full_like(cos, 128),
"ny": np.full_like(cos, 128)}
g = {k: np.clip(v.astype(int) + rng.integers(-3, 4, v.shape), 1, 255).astype(np.uint8) for k, v in f.items()}
c_same = compare(f, g)
c_shift = compare(f, {k: np.roll(v, 5, axis=0) for k, v in g.items()})
assert c_same["cos_r"] > 0.99 and c_shift["cos_r"] < 0.5, (c_same, c_shift)
assert c_same["normal_median_deg"] < 3, c_same
ok.append("compare and control")
n = decode_normal(np.array([255], np.uint8), np.array([128], np.uint8))
assert abs(n[0, 0] - 1) < 0.01 and abs(n[2, 0]) < 0.1
ok.append("normals")
boxes = {"cos": {"scaledown": 1}, "grad_mag": {"scaledown": 2}, "nx": {"scaledown": 2}, "ny": {"scaledown": 2}}
ct = rng.integers(0, 255, (128, 128, 128)).astype(np.uint8)
arr = {"cos": cos, "grad_mag": f["grad_mag"][::2, ::2, ::2], "nx": f["nx"][::2, ::2, ::2],
"ny": f["ny"][::2, ::2, ::2]}
pic = picture(ct, arr, boxes, 0)
assert pic.shape == (128, 4 * 128 + 18, 3), pic.shape
card = compose([pic, pic, pic], "test")
assert card.shape[0] > 3 * 128 and card.shape[1] > pic.shape[1], card.shape
ok.append("pictures")
argv = predict3d_argv({"origin_level0_zyx": [1, 2, 3], "size_level0_zyx": [4, 5, 6]}, "in", "out.json", "ck.pt",
"cuda")
assert argv[argv.index("--crop") + 1:argv.index("--crop") + 7] == ["3", "2", "1", "6", "5", "4"]
ok.append("predict3d crop order")
return {"passed": True, "checks": ok}
def main(argv=None):
argv = list(sys.argv[1:] if argv is None else argv)
if not argv or argv[0] == "selftest":
print(json.dumps(selftest(), indent=1))
return 0
if argv[0] == "run":
vid, z, y, x = argv[1:5]
size = int(argv[5]) if len(argv) > 5 else DEFAULT_SIZE
p = plan(vid, z, y, x, size)
log(json.dumps(p))
r = make(p, out_root=os.environ.get("LASFIELD_OUT") or ".")
print(json.dumps(r["receipt"], indent=1))
print(r["zip"])
return 0
print(__doc__)
return 2
if __name__ == "__main__":
sys.exit(main())
|