Spaces:
Running
Running
Segment Lens 1.1.0 from ChaseHendrick/Scrolls c147d69
Browse files- CONNECT.md +2 -1
- README.md +18 -2
- app.js +2 -2
- app.py +429 -0
- connect.html +2 -1
- connect.json +2 -1
- index.html +2 -1
- requirements.txt +6 -0
CONNECT.md
CHANGED
|
@@ -72,7 +72,7 @@ xyz = np.stack([get(c) for c in "xyz"], -1) # xyz[i, j] = (x, y, z) in scan vox
|
|
| 72 |
|
| 73 |
## This page's code and data
|
| 74 |
|
| 75 |
-
`https://sharpmeow-segment-lens.
|
| 76 |
|
| 77 |
License: Page code Apache 2.0; data as above. python -m kit atlas show SCROLL and python -m kit tifxyz check MESH_URL reproduce the numbers from a clone of ChaseHendrick/Scrolls.
|
| 78 |
|
|
@@ -80,5 +80,6 @@ License: Page code Apache 2.0; data as above. python -m kit atlas show SCROLL an
|
|
| 80 |
|
| 81 |
```text
|
| 82 |
The whole atlas as one CSV: https://huggingface.co/datasets/SharpMeow/vesuvius-segment-atlas/resolve/main/atlas.csv
|
|
|
|
| 83 |
Page source: https://github.com/ChaseHendrick/Scrolls/tree/main/spaces/segment-lens
|
| 84 |
```
|
|
|
|
| 72 |
|
| 73 |
## This page's code and data
|
| 74 |
|
| 75 |
+
`https://sharpmeow-segment-lens.hf.space/`
|
| 76 |
|
| 77 |
License: Page code Apache 2.0; data as above. python -m kit atlas show SCROLL and python -m kit tifxyz check MESH_URL reproduce the numbers from a clone of ChaseHendrick/Scrolls.
|
| 78 |
|
|
|
|
| 80 |
|
| 81 |
```text
|
| 82 |
The whole atlas as one CSV: https://huggingface.co/datasets/SharpMeow/vesuvius-segment-atlas/resolve/main/atlas.csv
|
| 83 |
+
API and MCP for agents (list_scrolls, list_segments, get_segment, segment_picture): https://sharpmeow-segment-lens.hf.space/agents/
|
| 84 |
Page source: https://github.com/ChaseHendrick/Scrolls/tree/main/spaces/segment-lens
|
| 85 |
```
|
README.md
CHANGED
|
@@ -3,8 +3,10 @@ title: Segment Lens
|
|
| 3 |
emoji: 馃Л
|
| 4 |
colorFrom: gray
|
| 5 |
colorTo: blue
|
| 6 |
-
sdk:
|
| 7 |
-
|
|
|
|
|
|
|
| 8 |
pinned: false
|
| 9 |
license: apache-2.0
|
| 10 |
short_description: Every published Herculaneum segment, measured, in 3D
|
|
@@ -37,5 +39,19 @@ What it shows is geometry only: no CT, no ink, and nothing here judges whether a
|
|
| 37 |
papyrus. The previews keep every k-th grid node (at most 256 per side), so a small flag can look larger than it is;
|
| 38 |
the numbers come from the full grid.
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
Data: Vesuvius Challenge open data, CC BY-NC 4.0, via the Segment Atlas (same license). Code: Apache 2.0,
|
| 41 |
by Chase Hendrick. three.js is MIT.
|
|
|
|
| 3 |
emoji: 馃Л
|
| 4 |
colorFrom: gray
|
| 5 |
colorTo: blue
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 6.28.0
|
| 8 |
+
python_version: "3.12"
|
| 9 |
+
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
license: apache-2.0
|
| 12 |
short_description: Every published Herculaneum segment, measured, in 3D
|
|
|
|
| 39 |
papyrus. The previews keep every k-th grid node (at most 256 per side), so a small flag can look larger than it is;
|
| 40 |
the numbers come from the full grid.
|
| 41 |
|
| 42 |
+
## For agents and scripts
|
| 43 |
+
|
| 44 |
+
The same atlas is an API and an MCP server at [/agents/](https://sharpmeow-segment-lens.hf.space/agents/):
|
| 45 |
+
`list_scrolls`, `list_segments`, `get_segment` and `segment_picture` (a picture of one mesh with its flags painted
|
| 46 |
+
on). MCP: add `https://sharpmeow-segment-lens.hf.space/agents/gradio_api/mcp/`. Python:
|
| 47 |
+
|
| 48 |
+
```python
|
| 49 |
+
from gradio_client import Client
|
| 50 |
+
c = Client("https://sharpmeow-segment-lens.hf.space/agents/")
|
| 51 |
+
top = c.predict("PHerc0139", "area_cm2", 5, True, api_name="/list_segments")
|
| 52 |
+
seg = c.predict(top["segments"][0]["id"], api_name="/get_segment")
|
| 53 |
+
picture, info = c.predict(seg["id"], "3d", 30, 20, api_name="/segment_picture")
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
Data: Vesuvius Challenge open data, CC BY-NC 4.0, via the Segment Atlas (same license). Code: Apache 2.0,
|
| 57 |
by Chase Hendrick. three.js is MIT.
|
app.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
import * as THREE from 'three';
|
| 5 |
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
| 6 |
|
| 7 |
-
const VERSION = '1.
|
| 8 |
// ?data=URL/ reads another copy of the atlas (a local build, for testing); the default is the published dataset
|
| 9 |
const DATA = new URLSearchParams(location.search).get('data') || 'https://huggingface.co/datasets/SharpMeow/vesuvius-segment-atlas/resolve/main/';
|
| 10 |
const S3 = 'https://vesuvius-challenge-open-data.s3.us-east-1.amazonaws.com/';
|
|
@@ -27,7 +27,7 @@ renderer.setPixelRatio(Math.min(2, window.devicePixelRatio || 1));
|
|
| 27 |
const scene = new THREE.Scene();
|
| 28 |
scene.background = new THREE.Color(0x05080b);
|
| 29 |
const camera = new THREE.PerspectiveCamera(40, 1, 0.01, 100);
|
| 30 |
-
camera.position.set(0, 0,
|
| 31 |
const controls = new OrbitControls(camera, canvas);
|
| 32 |
controls.enableDamping = true;
|
| 33 |
scene.add(new THREE.HemisphereLight(0xe8f4ff, 0x1a2228, 0.9));
|
|
|
|
| 4 |
import * as THREE from 'three';
|
| 5 |
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
| 6 |
|
| 7 |
+
const VERSION = '1.1.0';
|
| 8 |
// ?data=URL/ reads another copy of the atlas (a local build, for testing); the default is the published dataset
|
| 9 |
const DATA = new URLSearchParams(location.search).get('data') || 'https://huggingface.co/datasets/SharpMeow/vesuvius-segment-atlas/resolve/main/';
|
| 10 |
const S3 = 'https://vesuvius-challenge-open-data.s3.us-east-1.amazonaws.com/';
|
|
|
|
| 27 |
const scene = new THREE.Scene();
|
| 28 |
scene.background = new THREE.Color(0x05080b);
|
| 29 |
const camera = new THREE.PerspectiveCamera(40, 1, 0.01, 100);
|
| 30 |
+
camera.position.set(0, 0, 4.0); // opens a little zoomed out, so the whole segment and some margin show
|
| 31 |
const controls = new OrbitControls(camera, canvas);
|
| 32 |
controls.enableDamping = true;
|
| 33 |
scene.add(new THREE.HemisphereLight(0xe8f4ff, 0x1a2228, 0.9));
|
app.py
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Segment Lens on Hugging Face: the 3D page (index.html, app.js) at /, and the same atlas for AI agents and scripts
|
| 2 |
+
at /agents, as API endpoints (gradio_client) and MCP tools (/agents/gradio_api/mcp/).
|
| 3 |
+
|
| 4 |
+
Everything comes from the Segment Atlas dataset (SharpMeow/vesuvius-segment-atlas): atlas.jsonl, one row per
|
| 5 |
+
published tifxyz mesh, and lens/*.slg.gz, the decimated previews (format: "Lens files" in docs/tools/atlas.md of
|
| 6 |
+
ChaseHendrick/Scrolls). Nothing is written and no scan data is read. Previews exist only for scrolls on no prize
|
| 7 |
+
list, as on the page.
|
| 8 |
+
|
| 9 |
+
python3 app.py # serves on port 7860 (SEGMENT_LENS_DATA=URL/ reads another copy of the atlas)
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import functools
|
| 13 |
+
import gzip
|
| 14 |
+
import io
|
| 15 |
+
import json
|
| 16 |
+
import math
|
| 17 |
+
import os
|
| 18 |
+
import struct
|
| 19 |
+
import threading
|
| 20 |
+
import time
|
| 21 |
+
import urllib.request
|
| 22 |
+
from collections import OrderedDict
|
| 23 |
+
from pathlib import Path
|
| 24 |
+
|
| 25 |
+
VERSION = "1.1.0"
|
| 26 |
+
HERE = Path(__file__).resolve().parent
|
| 27 |
+
DATA = os.environ.get("SEGMENT_LENS_DATA",
|
| 28 |
+
"https://huggingface.co/datasets/SharpMeow/vesuvius-segment-atlas/resolve/main/")
|
| 29 |
+
S3 = "https://vesuvius-challenge-open-data.s3.us-east-1.amazonaws.com/"
|
| 30 |
+
MAGIC = b"SLG1"
|
| 31 |
+
ATLAS_TTL_S = 3600
|
| 32 |
+
LENS_CACHE = 32
|
| 33 |
+
SORTS = ("area_cm2", "filled_share", "within_10pct", "holes_mm2", "folds_mm2", "stretched_mm2", "grid_h", "segment")
|
| 34 |
+
# the page's colors (app.js COLORS), as 0-255 RGB: plain, hole, fold, stretched
|
| 35 |
+
COLORS = {"base": (204, 214, 219), "hole": (89, 166, 255), "fold": (255, 92, 92), "str": (255, 166, 51)}
|
| 36 |
+
WEB_FILES = ("index.html", "app.js", "style.css", "connect.html", "connect.json", "CONNECT.md")
|
| 37 |
+
SUMMARY_KEYS = ("id", "scroll", "segment", "mesh", "kind", "volume", "voxel_um", "area_cm2", "filled_share",
|
| 38 |
+
"within_10pct", "holes_mm2", "folds_mm2", "stretched_mm2", "clean", "prize_lists")
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class LensError(ValueError):
|
| 42 |
+
"""A refusal an agent can act on: unknown id, no preview, bad argument."""
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# ---------------------------------------------------------------- data
|
| 46 |
+
|
| 47 |
+
def _get(url, timeout=60):
|
| 48 |
+
req = urllib.request.Request(url, headers={"User-Agent": f"segment-lens/{VERSION}"})
|
| 49 |
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
| 50 |
+
return r.read()
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
_atlas = {"rows": None, "at": 0.0}
|
| 54 |
+
_atlas_lock = threading.Lock()
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def atlas_rows(get=_get):
|
| 58 |
+
"""The atlas rows, fetched at most once an hour."""
|
| 59 |
+
with _atlas_lock:
|
| 60 |
+
if _atlas["rows"] is None or time.time() - _atlas["at"] > ATLAS_TTL_S:
|
| 61 |
+
text = get(DATA + "atlas.jsonl").decode("utf-8")
|
| 62 |
+
_atlas["rows"] = [json.loads(line) for line in text.splitlines() if line.strip()]
|
| 63 |
+
_atlas["at"] = time.time()
|
| 64 |
+
return _atlas["rows"]
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def find_row(segment_id, rows=None):
|
| 68 |
+
rows = atlas_rows() if rows is None else rows
|
| 69 |
+
segment_id = (segment_id or "").strip()
|
| 70 |
+
for r in rows:
|
| 71 |
+
if r["id"] == segment_id:
|
| 72 |
+
return r
|
| 73 |
+
raise LensError(f"no mesh with id {segment_id!r}; list_segments gives the ids")
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def parse_lens(blob):
|
| 77 |
+
"""(h, w, k, xyz (h, w, 3) float64 with NaN for missing nodes, flags (h, w) uint8) from lens bytes: the reader
|
| 78 |
+
of kit.atlas.read_lens and the page's parseLens."""
|
| 79 |
+
import numpy as np
|
| 80 |
+
raw = gzip.decompress(blob)
|
| 81 |
+
if raw[:4] != MAGIC:
|
| 82 |
+
raise LensError("not a Segment Lens grid (bad magic)")
|
| 83 |
+
h, w, k = struct.unpack("<HHH", raw[4:10])
|
| 84 |
+
v = struct.unpack("<6f", raw[10:34])
|
| 85 |
+
lo, hi = np.array(v[:3]), np.array(v[3:])
|
| 86 |
+
n = h * w
|
| 87 |
+
if len(raw) != 34 + 7 * n:
|
| 88 |
+
raise LensError("Segment Lens grid has the wrong length")
|
| 89 |
+
q = np.frombuffer(raw, "<u2", 3 * n, 34).reshape(3, h, w)
|
| 90 |
+
flags = np.frombuffer(raw, np.uint8, n, 34 + 6 * n).reshape(h, w)
|
| 91 |
+
span = np.where(hi - lo > 0, hi - lo, 1.0)
|
| 92 |
+
xyz = np.stack([lo[i] + q[i] / 65534.0 * span[i] for i in range(3)], -1)
|
| 93 |
+
xyz[q[0] == 65535] = np.nan
|
| 94 |
+
return h, w, k, xyz, flags
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
_lens = OrderedDict()
|
| 98 |
+
_lens_lock = threading.Lock()
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def load_lens(row, get=_get):
|
| 102 |
+
if not row.get("lens"):
|
| 103 |
+
why = ("its scroll is on a prize list, so it has numbers but no preview (public media rule)"
|
| 104 |
+
if row.get("prize_lists") else "it was not measured or has no filled nodes")
|
| 105 |
+
raise LensError(f"{row['id']} has no preview: {why}")
|
| 106 |
+
with _lens_lock:
|
| 107 |
+
if row["lens"] in _lens:
|
| 108 |
+
_lens.move_to_end(row["lens"])
|
| 109 |
+
return _lens[row["lens"]]
|
| 110 |
+
g = parse_lens(get(DATA + "lens/" + row["lens"]))
|
| 111 |
+
with _lens_lock:
|
| 112 |
+
_lens[row["lens"]] = g
|
| 113 |
+
while len(_lens) > LENS_CACHE:
|
| 114 |
+
_lens.popitem(last=False)
|
| 115 |
+
return g
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def flag_counts(flags):
|
| 119 |
+
f = flags.ravel()
|
| 120 |
+
return {"nodes": int(f.size), "hole": int(((f & 1) > 0).sum()), "fold": int(((f & 2) > 0).sum()),
|
| 121 |
+
"stretched": int(((f & 4) > 0).sum())}
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def node_colors(flags):
|
| 125 |
+
"""One RGB per preview node, with the page's precedence: fold over stretched over hole over plain."""
|
| 126 |
+
import numpy as np
|
| 127 |
+
c = np.empty(flags.shape + (3,), np.uint8)
|
| 128 |
+
c[:] = COLORS["base"]
|
| 129 |
+
c[(flags & 1) > 0] = COLORS["hole"]
|
| 130 |
+
c[(flags & 4) > 0] = COLORS["str"]
|
| 131 |
+
c[(flags & 2) > 0] = COLORS["fold"]
|
| 132 |
+
return c
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
# ---------------------------------------------------------------- pictures
|
| 136 |
+
|
| 137 |
+
def render_grid(flags, xyz, size=512):
|
| 138 |
+
"""The preview grid flat (rows down, columns across), each node colored by its flag; missing nodes black."""
|
| 139 |
+
import numpy as np
|
| 140 |
+
from PIL import Image
|
| 141 |
+
c = node_colors(flags)
|
| 142 |
+
c[np.isnan(xyz[..., 0])] = (12, 16, 20)
|
| 143 |
+
h, w = flags.shape
|
| 144 |
+
s = max(1, size // max(h, w))
|
| 145 |
+
return Image.fromarray(np.repeat(np.repeat(c, s, 0), s, 1))
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def render_3d(flags, xyz, azimuth=30.0, elevation=20.0, size=512):
|
| 149 |
+
"""An orthographic picture of the surface from (azimuth, elevation) degrees, the scan's z up: each grid cell
|
| 150 |
+
filled as two triangles' worth of points, shaded by its facing, colored by flag; nearest wins."""
|
| 151 |
+
import numpy as np
|
| 152 |
+
from PIL import Image
|
| 153 |
+
ok = ~np.isnan(xyz[..., 0])
|
| 154 |
+
if ok.sum() < 4:
|
| 155 |
+
raise LensError("the preview has too few nodes to draw")
|
| 156 |
+
# x, y across, z up (the page rotates the same way)
|
| 157 |
+
p = xyz[..., [0, 1, 2]].copy()
|
| 158 |
+
center = np.nanmean(p[ok], 0)
|
| 159 |
+
p -= center
|
| 160 |
+
scale = np.nanmax(np.abs(p[ok])) or 1.0
|
| 161 |
+
p /= scale
|
| 162 |
+
a, e = math.radians(azimuth), math.radians(elevation)
|
| 163 |
+
right = np.array([math.cos(a), math.sin(a), 0.0])
|
| 164 |
+
view = np.array([-math.sin(a) * math.cos(e), math.cos(a) * math.cos(e), math.sin(e)])
|
| 165 |
+
up = np.cross(right, view)
|
| 166 |
+
# normals from the grid's neighbors, for shading
|
| 167 |
+
dy = np.gradient(np.nan_to_num(p), axis=0)
|
| 168 |
+
dx = np.gradient(np.nan_to_num(p), axis=1)
|
| 169 |
+
nrm = np.cross(dx, dy)
|
| 170 |
+
nrm /= np.linalg.norm(nrm, axis=-1, keepdims=True) + 1e-9
|
| 171 |
+
light = np.abs(nrm @ view) * 0.75 + 0.25
|
| 172 |
+
# densify: 4 x 4 samples per cell, bilinear between the cell's corners
|
| 173 |
+
h, w = flags.shape
|
| 174 |
+
t = np.linspace(0, 1, 4, endpoint=False)
|
| 175 |
+
rr, cc = np.meshgrid(t, t, indexing="ij")
|
| 176 |
+
P00, P01, P10, P11 = p[:-1, :-1], p[:-1, 1:], p[1:, :-1], p[1:, 1:]
|
| 177 |
+
cell_ok = ok[:-1, :-1] & ok[:-1, 1:] & ok[1:, :-1] & ok[1:, 1:]
|
| 178 |
+
pts = (P00[..., None, None, :] * ((1 - rr) * (1 - cc))[..., None] + P01[..., None, None, :] * ((1 - rr) * cc)[..., None]
|
| 179 |
+
+ P10[..., None, None, :] * (rr * (1 - cc))[..., None] + P11[..., None, None, :] * (rr * cc)[..., None])
|
| 180 |
+
pts = pts[cell_ok].reshape(-1, 3)
|
| 181 |
+
col = (node_colors(flags)[:-1, :-1][cell_ok].astype(np.float32) * light[:-1, :-1][cell_ok][:, None])
|
| 182 |
+
col = np.repeat(col, 16, 0)
|
| 183 |
+
u = pts @ right
|
| 184 |
+
v = pts @ up
|
| 185 |
+
d = pts @ view
|
| 186 |
+
half = size / 2 * 0.92
|
| 187 |
+
px = np.clip((u * half + size / 2).astype(int), 0, size - 1)
|
| 188 |
+
py = np.clip((size / 2 - v * half).astype(int), 0, size - 1)
|
| 189 |
+
order = np.argsort(d) # far first, near last, so near overwrite
|
| 190 |
+
img = np.full((size, size, 3), (5, 8, 11), np.uint8)
|
| 191 |
+
img[py[order], px[order]] = np.clip(col[order], 0, 255).astype(np.uint8)
|
| 192 |
+
return Image.fromarray(img)
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
# ---------------------------------------------------------------- tools
|
| 196 |
+
|
| 197 |
+
def page_url(segment_id=""):
|
| 198 |
+
host = os.environ.get("SPACE_HOST")
|
| 199 |
+
base = f"https://{host}/" if host else "/"
|
| 200 |
+
return base + (f"#id={segment_id}" if segment_id else "")
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def kit_command(row):
|
| 204 |
+
return f"python -m kit tifxyz check {row['url'].replace(S3, 's3://vesuvius-challenge-open-data/')} --png check.png"
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def summary(row):
|
| 208 |
+
out = {k: row.get(k) for k in SUMMARY_KEYS}
|
| 209 |
+
out["has_preview"] = bool(row.get("lens"))
|
| 210 |
+
return out
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def list_scrolls():
|
| 214 |
+
"""Every scroll in the Segment Atlas, with how many top-level meshes it has, how many have a 3D preview, and
|
| 215 |
+
their total area.
|
| 216 |
+
|
| 217 |
+
Returns:
|
| 218 |
+
JSON: scrolls (scroll, meshes, previews, area_cm2, prize_lists), total_meshes, version, open_in_browser.
|
| 219 |
+
"""
|
| 220 |
+
rows = [r for r in atlas_rows() if r.get("kind") == "mesh"]
|
| 221 |
+
by = {}
|
| 222 |
+
for r in rows:
|
| 223 |
+
s = by.setdefault(r["scroll"], {"scroll": r["scroll"], "meshes": 0, "previews": 0, "area_cm2": 0.0,
|
| 224 |
+
"prize_lists": sorted(set(r.get("prize_lists") or []))})
|
| 225 |
+
s["meshes"] += 1
|
| 226 |
+
s["previews"] += bool(r.get("lens"))
|
| 227 |
+
s["area_cm2"] = round(s["area_cm2"] + (r.get("area_cm2") or 0.0), 3)
|
| 228 |
+
return {"scrolls": sorted(by.values(), key=lambda s: s["scroll"]), "total_meshes": len(rows),
|
| 229 |
+
"version": VERSION, "open_in_browser": page_url()}
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def list_segments(scroll: str = "", sort: str = "area_cm2", top: int = 50, previews_only: bool = False):
|
| 233 |
+
"""The measured meshes of one scroll (or all scrolls), largest first by default: each mesh's id, area, filled
|
| 234 |
+
share, isometry (share of edges within 10 percent) and the area of its holes, folds and stretched cells.
|
| 235 |
+
|
| 236 |
+
Args:
|
| 237 |
+
scroll: a scroll name as list_scrolls gives it, such as PHerc0139; empty for every scroll.
|
| 238 |
+
sort: one of area_cm2, filled_share, within_10pct, holes_mm2, folds_mm2, stretched_mm2, grid_h, segment.
|
| 239 |
+
Numbers sort largest first, segment names A to Z.
|
| 240 |
+
top: how many rows to return, 1 to 500.
|
| 241 |
+
previews_only: only meshes with a 3D preview (scrolls on no prize list).
|
| 242 |
+
|
| 243 |
+
Returns:
|
| 244 |
+
JSON: segments (one summary per mesh, with has_preview), count (before the top cut), and the call's settings.
|
| 245 |
+
"""
|
| 246 |
+
if sort not in SORTS:
|
| 247 |
+
raise LensError(f"sort must be one of {', '.join(SORTS)}")
|
| 248 |
+
top = int(top)
|
| 249 |
+
if not 1 <= top <= 500:
|
| 250 |
+
raise LensError("top must be 1 to 500")
|
| 251 |
+
scroll = (scroll or "").strip()
|
| 252 |
+
rows = [r for r in atlas_rows() if r.get("kind") == "mesh" and (not scroll or r["scroll"] == scroll)]
|
| 253 |
+
if scroll and not rows:
|
| 254 |
+
raise LensError(f"no meshes for scroll {scroll!r}; list_scrolls gives the names")
|
| 255 |
+
if previews_only:
|
| 256 |
+
rows = [r for r in rows if r.get("lens")]
|
| 257 |
+
if sort == "segment":
|
| 258 |
+
rows = sorted(rows, key=lambda r: r["segment"])
|
| 259 |
+
else:
|
| 260 |
+
rows = sorted(rows, key=lambda r: -(r.get(sort) if r.get(sort) is not None else -math.inf))
|
| 261 |
+
return {"segments": [summary(r) for r in rows[:top]], "count": len(rows), "scroll": scroll or None,
|
| 262 |
+
"sort": sort, "top": top}
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def get_segment(segment_id: str):
|
| 266 |
+
"""Everything the Segment Atlas knows about one mesh: its open-data folder, grid size, area, isometry,
|
| 267 |
+
holes, folds and stretched cells (nodes and mm2), z range, the kit command that reproduces the numbers, and,
|
| 268 |
+
when it has a preview, the preview's flag counts.
|
| 269 |
+
|
| 270 |
+
Args:
|
| 271 |
+
segment_id: the mesh's id, as list_segments gives it (SCROLL/SEGMENT/MESH).
|
| 272 |
+
|
| 273 |
+
Returns:
|
| 274 |
+
JSON: the atlas row, plus kit_command, preview (grid size and flag counts, or why there is none) and
|
| 275 |
+
open_in_browser.
|
| 276 |
+
"""
|
| 277 |
+
row = find_row(segment_id)
|
| 278 |
+
out = dict(row)
|
| 279 |
+
out["kit_command"] = kit_command(row) if row.get("url") else None
|
| 280 |
+
try:
|
| 281 |
+
h, w, k, xyz, flags = load_lens(row)
|
| 282 |
+
out["preview"] = {"grid_w": w, "grid_h": h, "every_kth_node": k, "flags": flag_counts(flags)}
|
| 283 |
+
except LensError as exc:
|
| 284 |
+
out["preview"] = {"none": str(exc)}
|
| 285 |
+
out["open_in_browser"] = page_url(row["id"])
|
| 286 |
+
return out
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
def segment_picture(segment_id: str, view: str = "3d", azimuth: float = 30.0, elevation: float = 20.0):
|
| 290 |
+
"""A picture of one mesh's preview with its flags painted on, as on the page: holes blue, folds red,
|
| 291 |
+
stretched cells orange, plain grey. Geometry only: no CT, no ink, and a clean surface is not proof the trace
|
| 292 |
+
stays on one sheet. Only meshes with a preview (scrolls on no prize list) can be drawn.
|
| 293 |
+
|
| 294 |
+
Args:
|
| 295 |
+
segment_id: the mesh's id, as list_segments gives it.
|
| 296 |
+
view: "3d" for the surface seen from azimuth and elevation (the scan's z up), or "grid" for the flat grid.
|
| 297 |
+
azimuth: degrees around the scan's z axis, for view 3d.
|
| 298 |
+
elevation: degrees above the horizontal, -90 to 90, for view 3d.
|
| 299 |
+
|
| 300 |
+
Returns:
|
| 301 |
+
A PNG picture, and JSON: id, view, grid size, every_kth_node, flag counts, open_in_browser.
|
| 302 |
+
"""
|
| 303 |
+
if view not in ("3d", "grid"):
|
| 304 |
+
raise LensError('view must be "3d" or "grid"')
|
| 305 |
+
if not -90 <= float(elevation) <= 90:
|
| 306 |
+
raise LensError("elevation must be -90 to 90")
|
| 307 |
+
row = find_row(segment_id)
|
| 308 |
+
h, w, k, xyz, flags = load_lens(row)
|
| 309 |
+
img = render_grid(flags, xyz) if view == "grid" else render_3d(flags, xyz, float(azimuth), float(elevation))
|
| 310 |
+
info = {"id": row["id"], "view": view, "grid_w": w, "grid_h": h, "every_kth_node": k,
|
| 311 |
+
"flags": flag_counts(flags), "open_in_browser": page_url(row["id"])}
|
| 312 |
+
if view == "3d":
|
| 313 |
+
info.update(azimuth=float(azimuth), elevation=float(elevation))
|
| 314 |
+
return img, info
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
# ---------------------------------------------------------------- app
|
| 318 |
+
|
| 319 |
+
AGENTS_TEXT = """\
|
| 320 |
+
# Segment Lens for agents and scripts
|
| 321 |
+
|
| 322 |
+
The same Segment Atlas the [3D page](/) shows, as API endpoints and MCP tools. Every answer that names a mesh
|
| 323 |
+
includes `open_in_browser`, a link that opens it on the page for a person.
|
| 324 |
+
|
| 325 |
+
- `list_scrolls`: every scroll, its mesh and preview counts and total area.
|
| 326 |
+
- `list_segments`: one scroll's meshes (or all), sorted, with area, filled share, isometry and flagged areas.
|
| 327 |
+
- `get_segment`: one mesh's full atlas row, the `kit tifxyz check` command and the preview's flag counts.
|
| 328 |
+
- `segment_picture`: a picture of one mesh with holes, folds and stretched cells painted on (3D or flat grid).
|
| 329 |
+
|
| 330 |
+
**MCP**: add the server `{url}/agents/gradio_api/mcp/`.
|
| 331 |
+
|
| 332 |
+
**Python**:
|
| 333 |
+
|
| 334 |
+
```python
|
| 335 |
+
from gradio_client import Client
|
| 336 |
+
c = Client("{url}/agents/")
|
| 337 |
+
top = c.predict("PHerc0139", "area_cm2", 5, True, api_name="/list_segments")
|
| 338 |
+
seg = c.predict(top["segments"][0]["id"], api_name="/get_segment")
|
| 339 |
+
```
|
| 340 |
+
|
| 341 |
+
Geometry only: no CT, no ink, and nothing here judges whether a segment stays on one sheet. Data: Vesuvius
|
| 342 |
+
Challenge open data via the Segment Atlas, CC BY-NC 4.0. Segment Lens {version}.
|
| 343 |
+
"""
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
def fix_mcp_multiple_outputs():
|
| 347 |
+
"""Gradio 6.28's MCP server joins a queued tool's several outputs into one text (the same patch as Scroll Lens),
|
| 348 |
+
so segment_picture's image would reach an agent as text. Hand them over one by one."""
|
| 349 |
+
try:
|
| 350 |
+
from gradio.mcp import GradioMCPServer
|
| 351 |
+
from anyio.to_thread import run_sync
|
| 352 |
+
except ImportError:
|
| 353 |
+
return
|
| 354 |
+
|
| 355 |
+
async def without_progress(self, job):
|
| 356 |
+
result = await run_sync(job.result)
|
| 357 |
+
return list(result) if isinstance(result, tuple) else [result]
|
| 358 |
+
|
| 359 |
+
GradioMCPServer._execute_tool_without_progress = without_progress
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
def build_demo():
|
| 363 |
+
import gradio as gr
|
| 364 |
+
|
| 365 |
+
def guard(fn):
|
| 366 |
+
# functools.wraps keeps the name, docstring and signature, which the API and the MCP tool list come from
|
| 367 |
+
@functools.wraps(fn)
|
| 368 |
+
def run(*a, **kw):
|
| 369 |
+
try:
|
| 370 |
+
return fn(*a, **kw)
|
| 371 |
+
except LensError as exc:
|
| 372 |
+
raise gr.Error(str(exc))
|
| 373 |
+
return run
|
| 374 |
+
|
| 375 |
+
url = page_url().rstrip("/") if page_url() != "/" else "https://sharpmeow-segment-lens.hf.space"
|
| 376 |
+
with gr.Blocks(title="Segment Lens for agents") as demo:
|
| 377 |
+
gr.Markdown(AGENTS_TEXT.format(url=url, version=VERSION))
|
| 378 |
+
with gr.Row():
|
| 379 |
+
sc = gr.Textbox(value="", label="scroll")
|
| 380 |
+
so = gr.Dropdown(list(SORTS), value="area_cm2", label="sort")
|
| 381 |
+
tp = gr.Number(value=50, precision=0, label="top")
|
| 382 |
+
pv = gr.Checkbox(value=False, label="previews_only")
|
| 383 |
+
sid = gr.Textbox(value="", label="segment_id")
|
| 384 |
+
with gr.Row():
|
| 385 |
+
vw = gr.Dropdown(["3d", "grid"], value="3d", label="view")
|
| 386 |
+
az = gr.Number(value=30.0, label="azimuth")
|
| 387 |
+
el = gr.Number(value=20.0, label="elevation")
|
| 388 |
+
with gr.Row():
|
| 389 |
+
b1, b2, b3, b4 = (gr.Button(n) for n in ("list_scrolls", "list_segments", "get_segment", "segment_picture"))
|
| 390 |
+
pic = gr.Image(type="pil", label="picture", format="png")
|
| 391 |
+
out = gr.JSON(label="answer")
|
| 392 |
+
b1.click(guard(list_scrolls), None, out, api_name="list_scrolls")
|
| 393 |
+
b2.click(guard(list_segments), [sc, so, tp, pv], out, api_name="list_segments")
|
| 394 |
+
b3.click(guard(get_segment), [sid], out, api_name="get_segment")
|
| 395 |
+
b4.click(guard(segment_picture), [sid, vw, az, el], [pic, out], api_name="segment_picture")
|
| 396 |
+
return demo
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
def build_app():
|
| 400 |
+
import gradio as gr
|
| 401 |
+
from fastapi import FastAPI
|
| 402 |
+
from fastapi.responses import FileResponse, RedirectResponse
|
| 403 |
+
|
| 404 |
+
fix_mcp_multiple_outputs()
|
| 405 |
+
app = FastAPI()
|
| 406 |
+
app = gr.mount_gradio_app(app, build_demo(), path="/agents", mcp_server=True, footer_links=["api"])
|
| 407 |
+
|
| 408 |
+
@app.get("/")
|
| 409 |
+
def index():
|
| 410 |
+
return FileResponse(HERE / "index.html")
|
| 411 |
+
|
| 412 |
+
def serve(name):
|
| 413 |
+
def f():
|
| 414 |
+
return FileResponse(HERE / name)
|
| 415 |
+
return f
|
| 416 |
+
|
| 417 |
+
for name in WEB_FILES:
|
| 418 |
+
app.add_api_route("/" + name, serve(name), methods=["GET"])
|
| 419 |
+
|
| 420 |
+
@app.get("/agents")
|
| 421 |
+
def agents():
|
| 422 |
+
return RedirectResponse("/agents/")
|
| 423 |
+
|
| 424 |
+
return app
|
| 425 |
+
|
| 426 |
+
|
| 427 |
+
if __name__ == "__main__":
|
| 428 |
+
import uvicorn
|
| 429 |
+
uvicorn.run(build_app(), host="0.0.0.0", port=int(os.environ.get("PORT", "7860")))
|
connect.html
CHANGED
|
@@ -57,9 +57,10 @@ xyz = np.stack([get(c) for c in "xyz"], -1) # xyz[i, j] = (x, y, z) i
|
|
| 57 |
</div>
|
| 58 |
<div class="scc-src" id="connect-page"><h3>This page's code and data</h3>
|
| 59 |
<p class="scc-meta">Page code Apache 2.0; data as above 路 python -m kit atlas show SCROLL and python -m kit tifxyz check MESH_URL reproduce the numbers from a clone of ChaseHendrick/Scrolls.</p>
|
| 60 |
-
<div class="scc-url"><button type="button" class="scc-copy" data-testid="connect-copy" onclick="navigator.clipboard&&navigator.clipboard.writeText(this.parentNode.querySelector('code').innerText).then(()=>{this.textContent='Copied';setTimeout(()=>this.textContent='Copy',1200)})">Copy</button><pre><code>https://sharpmeow-segment-lens.
|
| 61 |
<details><summary>Links and embed<span class="scc-b ok">tested 2026-10-24</span></summary>
|
| 62 |
<div class="scc-code"><button type="button" class="scc-copy" data-testid="connect-copy" onclick="navigator.clipboard&&navigator.clipboard.writeText(this.parentNode.querySelector('code').innerText).then(()=>{this.textContent='Copied';setTimeout(()=>this.textContent='Copy',1200)})">Copy</button><pre><code>The whole atlas as one CSV: https://huggingface.co/datasets/SharpMeow/vesuvius-segment-atlas/resolve/main/atlas.csv
|
|
|
|
| 63 |
Page source: https://github.com/ChaseHendrick/Scrolls/tree/main/spaces/segment-lens</code></pre></div></details>
|
| 64 |
</div>
|
| 65 |
<footer>Machine-readable: <a href="https://huggingface.co/spaces/SharpMeow/segment-lens/blob/main/connect.json" target="_blank" rel="noopener">connect.json</a> and <a href="https://huggingface.co/spaces/SharpMeow/segment-lens/blob/main/CONNECT.md" target="_blank" rel="noopener">CONNECT.md</a>. Checked 2026-10-24. "Tested" means the snippet ran against this public data on that date. Scan data keeps its own license (the Vesuvius Challenge open data is CC BY-NC 4.0).</footer></section>
|
|
|
|
| 57 |
</div>
|
| 58 |
<div class="scc-src" id="connect-page"><h3>This page's code and data</h3>
|
| 59 |
<p class="scc-meta">Page code Apache 2.0; data as above 路 python -m kit atlas show SCROLL and python -m kit tifxyz check MESH_URL reproduce the numbers from a clone of ChaseHendrick/Scrolls.</p>
|
| 60 |
+
<div class="scc-url"><button type="button" class="scc-copy" data-testid="connect-copy" onclick="navigator.clipboard&&navigator.clipboard.writeText(this.parentNode.querySelector('code').innerText).then(()=>{this.textContent='Copied';setTimeout(()=>this.textContent='Copy',1200)})">Copy</button><pre><code>https://sharpmeow-segment-lens.hf.space/</code></pre></div>
|
| 61 |
<details><summary>Links and embed<span class="scc-b ok">tested 2026-10-24</span></summary>
|
| 62 |
<div class="scc-code"><button type="button" class="scc-copy" data-testid="connect-copy" onclick="navigator.clipboard&&navigator.clipboard.writeText(this.parentNode.querySelector('code').innerText).then(()=>{this.textContent='Copied';setTimeout(()=>this.textContent='Copy',1200)})">Copy</button><pre><code>The whole atlas as one CSV: https://huggingface.co/datasets/SharpMeow/vesuvius-segment-atlas/resolve/main/atlas.csv
|
| 63 |
+
API and MCP for agents (list_scrolls, list_segments, get_segment, segment_picture): https://sharpmeow-segment-lens.hf.space/agents/
|
| 64 |
Page source: https://github.com/ChaseHendrick/Scrolls/tree/main/spaces/segment-lens</code></pre></div></details>
|
| 65 |
</div>
|
| 66 |
<footer>Machine-readable: <a href="https://huggingface.co/spaces/SharpMeow/segment-lens/blob/main/connect.json" target="_blank" rel="noopener">connect.json</a> and <a href="https://huggingface.co/spaces/SharpMeow/segment-lens/blob/main/CONNECT.md" target="_blank" rel="noopener">CONNECT.md</a>. Checked 2026-10-24. "Tested" means the snippet ran against this public data on that date. Scan data keeps its own license (the Vesuvius Challenge open data is CC BY-NC 4.0).</footer></section>
|
connect.json
CHANGED
|
@@ -15,8 +15,9 @@
|
|
| 15 |
"license": "CC BY-NC 4.0 (Vesuvius Challenge open data)",
|
| 16 |
"note": "Every row of the atlas names its mesh folder in url; the table's mesh folder link opens it. Read live from the open-data bucket, never copied here."},
|
| 17 |
{"id": "page", "title": "This page's code and data", "kind": "page-api",
|
| 18 |
-
"url": "https://sharpmeow-segment-lens.
|
| 19 |
"links": [{"label": "The whole atlas as one CSV", "url": "https://huggingface.co/datasets/SharpMeow/vesuvius-segment-atlas/resolve/main/atlas.csv"},
|
|
|
|
| 20 |
{"label": "Page source", "url": "https://github.com/ChaseHendrick/Scrolls/tree/main/spaces/segment-lens"}],
|
| 21 |
"license": "Page code Apache 2.0; data as above",
|
| 22 |
"note": "python -m kit atlas show SCROLL and python -m kit tifxyz check MESH_URL reproduce the numbers from a clone of ChaseHendrick/Scrolls."}
|
|
|
|
| 15 |
"license": "CC BY-NC 4.0 (Vesuvius Challenge open data)",
|
| 16 |
"note": "Every row of the atlas names its mesh folder in url; the table's mesh folder link opens it. Read live from the open-data bucket, never copied here."},
|
| 17 |
{"id": "page", "title": "This page's code and data", "kind": "page-api",
|
| 18 |
+
"url": "https://sharpmeow-segment-lens.hf.space/",
|
| 19 |
"links": [{"label": "The whole atlas as one CSV", "url": "https://huggingface.co/datasets/SharpMeow/vesuvius-segment-atlas/resolve/main/atlas.csv"},
|
| 20 |
+
{"label": "API and MCP for agents (list_scrolls, list_segments, get_segment, segment_picture)", "url": "https://sharpmeow-segment-lens.hf.space/agents/"},
|
| 21 |
{"label": "Page source", "url": "https://github.com/ChaseHendrick/Scrolls/tree/main/spaces/segment-lens"}],
|
| 22 |
"license": "Page code Apache 2.0; data as above",
|
| 23 |
"note": "python -m kit atlas show SCROLL and python -m kit tifxyz check MESH_URL reproduce the numbers from a clone of ChaseHendrick/Scrolls."}
|
index.html
CHANGED
|
@@ -153,9 +153,10 @@ xyz = np.stack([get(c) for c in "xyz"], -1) # xyz[i, j] = (x, y, z) i
|
|
| 153 |
</div>
|
| 154 |
<div class="scc-src" id="connect-page"><h3>This page's code and data</h3>
|
| 155 |
<p class="scc-meta">Page code Apache 2.0; data as above 路 python -m kit atlas show SCROLL and python -m kit tifxyz check MESH_URL reproduce the numbers from a clone of ChaseHendrick/Scrolls.</p>
|
| 156 |
-
<div class="scc-url"><button type="button" class="scc-copy" data-testid="connect-copy" onclick="navigator.clipboard&&navigator.clipboard.writeText(this.parentNode.querySelector('code').innerText).then(()=>{this.textContent='Copied';setTimeout(()=>this.textContent='Copy',1200)})">Copy</button><pre><code>https://sharpmeow-segment-lens.
|
| 157 |
<details><summary>Links and embed<span class="scc-b ok">tested 2026-10-24</span></summary>
|
| 158 |
<div class="scc-code"><button type="button" class="scc-copy" data-testid="connect-copy" onclick="navigator.clipboard&&navigator.clipboard.writeText(this.parentNode.querySelector('code').innerText).then(()=>{this.textContent='Copied';setTimeout(()=>this.textContent='Copy',1200)})">Copy</button><pre><code>The whole atlas as one CSV: https://huggingface.co/datasets/SharpMeow/vesuvius-segment-atlas/resolve/main/atlas.csv
|
|
|
|
| 159 |
Page source: https://github.com/ChaseHendrick/Scrolls/tree/main/spaces/segment-lens</code></pre></div></details>
|
| 160 |
</div>
|
| 161 |
<footer>Machine-readable: <a href="https://huggingface.co/spaces/SharpMeow/segment-lens/blob/main/connect.json" target="_blank" rel="noopener">connect.json</a> and <a href="https://huggingface.co/spaces/SharpMeow/segment-lens/blob/main/CONNECT.md" target="_blank" rel="noopener">CONNECT.md</a>. Checked 2026-10-24. "Tested" means the snippet ran against this public data on that date. Scan data keeps its own license (the Vesuvius Challenge open data is CC BY-NC 4.0).</footer></section>
|
|
|
|
| 153 |
</div>
|
| 154 |
<div class="scc-src" id="connect-page"><h3>This page's code and data</h3>
|
| 155 |
<p class="scc-meta">Page code Apache 2.0; data as above 路 python -m kit atlas show SCROLL and python -m kit tifxyz check MESH_URL reproduce the numbers from a clone of ChaseHendrick/Scrolls.</p>
|
| 156 |
+
<div class="scc-url"><button type="button" class="scc-copy" data-testid="connect-copy" onclick="navigator.clipboard&&navigator.clipboard.writeText(this.parentNode.querySelector('code').innerText).then(()=>{this.textContent='Copied';setTimeout(()=>this.textContent='Copy',1200)})">Copy</button><pre><code>https://sharpmeow-segment-lens.hf.space/</code></pre></div>
|
| 157 |
<details><summary>Links and embed<span class="scc-b ok">tested 2026-10-24</span></summary>
|
| 158 |
<div class="scc-code"><button type="button" class="scc-copy" data-testid="connect-copy" onclick="navigator.clipboard&&navigator.clipboard.writeText(this.parentNode.querySelector('code').innerText).then(()=>{this.textContent='Copied';setTimeout(()=>this.textContent='Copy',1200)})">Copy</button><pre><code>The whole atlas as one CSV: https://huggingface.co/datasets/SharpMeow/vesuvius-segment-atlas/resolve/main/atlas.csv
|
| 159 |
+
API and MCP for agents (list_scrolls, list_segments, get_segment, segment_picture): https://sharpmeow-segment-lens.hf.space/agents/
|
| 160 |
Page source: https://github.com/ChaseHendrick/Scrolls/tree/main/spaces/segment-lens</code></pre></div></details>
|
| 161 |
</div>
|
| 162 |
<footer>Machine-readable: <a href="https://huggingface.co/spaces/SharpMeow/segment-lens/blob/main/connect.json" target="_blank" rel="noopener">connect.json</a> and <a href="https://huggingface.co/spaces/SharpMeow/segment-lens/blob/main/CONNECT.md" target="_blank" rel="noopener">CONNECT.md</a>. Checked 2026-10-24. "Tested" means the snippet ran against this public data on that date. Scan data keeps its own license (the Vesuvius Challenge open data is CC BY-NC 4.0).</footer></section>
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Segment Lens on a free CPU Space (Gradio 6.28.0 and Python 3.12 come from the README card). numpy reads the
|
| 2 |
+
# preview grids; mcp is gradio's MCP server extra (mount_gradio_app(mcp_server=True)) at the version gradio 6.28.0
|
| 3 |
+
# resolves with, and pydantic in the range that extra asks for. Pillow and uvicorn come with gradio.
|
| 4 |
+
numpy==2.5.2
|
| 5 |
+
mcp==1.30.0
|
| 6 |
+
pydantic>=2.11.10,<=2.12.5
|