Spaces:
Running
Running
Fiber Checker: page, checker, free judge weights and pictures
Browse files- .gitattributes +3 -0
- app.py +115 -11
- assets/hero.webp +3 -0
- assets/howto-check.gif +3 -0
- assets/howto-compare.gif +0 -0
- assets/howto-game.gif +3 -0
- assets/howto-result.webp +0 -0
- assets/howto-upload.webp +0 -0
- assets/map-summary.json +1 -0
- assets/map.png +0 -0
- assets/media.json +61 -0
- assets/pair-0-off.webp +0 -0
- assets/pair-0-on.webp +0 -0
- assets/pair-0-scramble.webp +0 -0
- assets/pair-0-shuffle.webp +0 -0
- assets/pair-1-off.webp +0 -0
- assets/pair-1-on.webp +0 -0
- assets/pair-1-scramble.webp +0 -0
- assets/pair-1-shuffle.webp +0 -0
- assets/pair-2-off.webp +0 -0
- assets/pair-2-on.webp +0 -0
- assets/pair-2-scramble.webp +0 -0
- assets/pair-2-shuffle.webp +0 -0
- assets/tile-0.webp +0 -0
- assets/tile-1.webp +0 -0
- assets/tile-2.webp +0 -0
- assets/tile-3.webp +0 -0
- assets/tile-4.webp +0 -0
- assets/tile-5.webp +0 -0
- assets/ui.webp +0 -0
- guide/index.html +204 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
assets/hero.webp filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
assets/howto-check.gif filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
assets/howto-game.gif filter=lfs diff=lfs merge=lfs -text
|
app.py
CHANGED
|
@@ -37,6 +37,7 @@ OUT = Path(tempfile.gettempdir()) / "fiber-checker-out"
|
|
| 37 |
CT_CACHE = C.CACHE / "ct"
|
| 38 |
CT_KEEP = int(8e9) # CT chunks kept between runs; the oldest go first
|
| 39 |
GAME = HERE / "assets" / "game.npz"
|
|
|
|
| 40 |
CONNECT_HTML = HERE / "connect.html"
|
| 41 |
PATCHES = [("Quick: 60 patches, about 2 minutes", 60), ("Thorough: 150 patches, about 5 minutes", 150),
|
| 42 |
("Whole segment: up to 400 patches, about 15 minutes", 400)]
|
|
@@ -243,6 +244,34 @@ def compare_results(a_json, b_json):
|
|
| 243 |
|
| 244 |
# ---------------------------------------------------------------- what the page shows
|
| 245 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
def pct(v):
|
| 247 |
return "n/a" if v is None or v != v else f"{v:.0%}"
|
| 248 |
|
|
@@ -277,7 +306,7 @@ def summary_html(rec):
|
|
| 277 |
· {setting} · judge: {'free local model' if rec['judge'] == 'local' else 'Claude, your key'}{cost}</p>
|
| 278 |
<details class="fc-untested"><summary>Untested hints ({jumps} possible jumps)</summary>
|
| 279 |
<p>These have not passed a test of their own yet, so read them as ideas, not results.
|
| 280 |
-
<b>Jump hints</b>: {jumps} patches with low evidence where every
|
| 281 |
stretch off the sheet. <b>Fiber direction</b>: {pct(s['direction_share_horizontal'])} of patches have strands
|
| 282 |
running along the mesh's columns (horizontal in the map), the rest along its rows. <b>Best depth</b>: the median
|
| 283 |
patch looks most fibrous {s['depth_offset_median']:+g} voxels from the traced surface.</p></details>
|
|
@@ -289,6 +318,40 @@ def tiles_of(out):
|
|
| 289 |
return [(str(f), f"{f.stem.split('-')[-1]} {f.stem.split('-')[1]}") for f in files]
|
| 290 |
|
| 291 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
def report_frame(out):
|
| 293 |
page = (Path(out) / "report.html").read_text()
|
| 294 |
return f'<iframe class="fc-report" title="Report" srcdoc="{html.escape(page, quote=True)}"></iframe>'
|
|
@@ -442,6 +505,10 @@ texture: it never reads or finds text.
|
|
| 442 |
judges them on the CPU.
|
| 443 |
3. The map, the summary and the download appear on the right. Try the **Fiber or not?** tab while you wait.
|
| 444 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 445 |
<h3 id="read">2. Read the result</h3>
|
| 446 |
|
| 447 |
- **The map** is the segment as traced, flattened on its own mesh grid. Yellow and green: clear fibers. Purple: weak.
|
|
@@ -452,18 +519,25 @@ texture: it never reads or finds text.
|
|
| 452 |
- **Unsure**: the judge's answer changed when the patch was turned by a quarter turn.
|
| 453 |
- **Clearest and weakest patches** are the renders themselves, so you can see what the judge saw.
|
| 454 |
|
|
|
|
|
|
|
| 455 |
<h3 id="own">3. Check your own trace</h3>
|
| 456 |
|
| 457 |
Zip your tifxyz folder (x.tif, y.tif and z.tif; VC3D and villa write these) and upload it on **Check a segment**,
|
| 458 |
under **Your own mesh**, with the 14-digit id of the scan its coordinates are in (level-0 voxels). The Space reads the
|
| 459 |
CT of that scan from the open data. Meshes on 2.2 to 2.4 um scans are checked in the tested setting.
|
| 460 |
|
|
|
|
|
|
|
| 461 |
<h3 id="ab">4. Compare two traces</h3>
|
| 462 |
|
| 463 |
Check both (two segments, or your trace before and after an edit), download each zip, and give the two
|
| 464 |
`fiber.json` files to **Compare**. When both are meshes of the same grid, it compares them patch by patch: the share
|
| 465 |
of patches where each is clearly better, and the mean difference.
|
| 466 |
|
|
|
|
|
|
|
|
|
|
| 467 |
<h3 id="prize">5. For prize entries</h3>
|
| 468 |
|
| 469 |
`report.html` in the download is a one-page summary in the prize's words: the map, the self-check, the clearest and
|
|
@@ -507,7 +581,7 @@ the segment in tools that read tifxyz channels. `--judge claude` uses `ANTHROPIC
|
|
| 507 |
- **Outside the tested setting** (other voxel sizes, your own meshes at other spacings): it still runs, and says so.
|
| 508 |
- **Untested hints**: jump hints, fiber direction and best depth are ideas until a test of their own passes.
|
| 509 |
- **Which sheet**: each patch is rendered at the brightest depth within 5 voxels of the trace (the tested rule), and
|
| 510 |
-
papyrus sheets lie close together, so a trace a few voxels off, or on the
|
| 511 |
A clear map says fibers show on and near your surface; it does not prove the trace stayed on one sheet.
|
| 512 |
|
| 513 |
<h3 id="wrong">When something goes wrong</h3>
|
|
@@ -569,6 +643,19 @@ CSS = """
|
|
| 569 |
.fc-legend { font-size:12.5px; color: var(--body-text-color-subdued); }
|
| 570 |
.fc-legend i { display:inline-block; width:11px; height:11px; border-radius:3px; margin:0 4px -1px 10px; }
|
| 571 |
.fc-report { width:100%; height:80vh; border:0; border-radius:12px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 572 |
.fc-howto { max-width: 1000px; margin: 0 auto !important; }
|
| 573 |
.fc-howto h3 { scroll-margin-top: 16px; margin-top: 1.6em !important; }
|
| 574 |
.fc-toc { font-size: 14px; line-height: 1.9; padding: 10px 14px; border-radius: 10px;
|
|
@@ -582,6 +669,8 @@ CSS = """
|
|
| 582 |
.fc-head h1 { font-size:24px; }
|
| 583 |
.fc-head span { display:none; }
|
| 584 |
#fc-tabs .tab-container button { padding:6px 6px !important; font-size:12.5px !important; }
|
|
|
|
|
|
|
| 585 |
}
|
| 586 |
"""
|
| 587 |
|
|
@@ -602,10 +691,14 @@ model or by Claude with your own key. Model output, not a reading.">
|
|
| 602 |
TAB_JS = """
|
| 603 |
() => {
|
| 604 |
const pick = (name) => {
|
| 605 |
-
|
| 606 |
-
const
|
| 607 |
-
|
|
|
|
|
|
|
|
|
|
| 608 |
};
|
|
|
|
| 609 |
const q = new URLSearchParams(window.location.search).get('tab');
|
| 610 |
if (q) setTimeout(() => pick(q), 300);
|
| 611 |
}
|
|
@@ -721,6 +814,8 @@ def build_demo():
|
|
| 721 |
gr.HTML('<div class="fc-head"><div class="weave"></div><h1>Fiber Checker</h1><span>Are papyrus fibers '
|
| 722 |
'visible on a traced scroll surface? A free fiber map of any segment, patch by patch.</span></div>')
|
| 723 |
with gr.Tabs(elem_id="fc-tabs"):
|
|
|
|
|
|
|
| 724 |
with gr.Tab("Check a segment", id="check"):
|
| 725 |
gr.Markdown(INTRO)
|
| 726 |
with gr.Row(equal_height=False):
|
|
@@ -738,7 +833,7 @@ def build_demo():
|
|
| 738 |
go = gr.Button("Check fibers", variant="primary", size="lg")
|
| 739 |
with gr.Tab("Your own mesh"):
|
| 740 |
upload = gr.File(label="Zip of your tifxyz folder (x.tif, y.tif, z.tif)",
|
| 741 |
-
file_types=[".zip"], type="filepath"
|
| 742 |
up_vol = gr.Textbox(label="14-digit scan id its coordinates are in (level-0 voxels)",
|
| 743 |
placeholder="20250526151718")
|
| 744 |
go_up = gr.Button("Check my mesh", variant="primary", size="lg")
|
|
@@ -785,12 +880,14 @@ def build_demo():
|
|
| 785 |
gr.Markdown("Give two `fiber.json` files (from the downloads, or the command line tool): two "
|
| 786 |
"segments, two tracers, or one trace before and after an edit.")
|
| 787 |
with gr.Row():
|
| 788 |
-
ca = gr.File(label="A: fiber.json", file_types=[".json"], type="filepath",
|
| 789 |
-
cb = gr.File(label="B: fiber.json", file_types=[".json"], type="filepath",
|
| 790 |
cgo = gr.Button("Compare", variant="primary")
|
| 791 |
-
|
|
|
|
|
|
|
| 792 |
with gr.Tab("How-to guides", id="howto"):
|
| 793 |
-
gr.Markdown(HOW_TO, elem_classes="fc-howto")
|
| 794 |
with gr.Tab("Agents and API", id="agents"):
|
| 795 |
gr.Markdown(AGENTS.format(host=host))
|
| 796 |
with gr.Accordion("Connect your tool: the scans and meshes by URL", open=True):
|
|
@@ -826,7 +923,10 @@ def build_demo():
|
|
| 826 |
game_tab.select(guard(first_pair), st, [img_a, img_b, reveal, score_md, st], **hidden)
|
| 827 |
pick_a.click(lambda s: answer("A", s), st, [reveal, score_md, st], **hidden)
|
| 828 |
pick_b.click(lambda s: answer("B", s), st, [reveal, score_md, st], **hidden)
|
| 829 |
-
|
|
|
|
|
|
|
|
|
|
| 830 |
|
| 831 |
def api_check(sample_, segment_, volume_, patches_, request: gr.Request, progress=gr.Progress()):
|
| 832 |
return check_segment(sample_, segment_, volume_, int(patches_ or 60), request=request, progress=progress)
|
|
@@ -861,6 +961,10 @@ def fix_mcp_multiple_outputs():
|
|
| 861 |
|
| 862 |
|
| 863 |
def launch_kwargs():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 864 |
return dict(theme=theme(), css=CSS, head=HEAD, footer_links=["api", "gradio"], mcp_server=True,
|
| 865 |
max_file_size="200mb", allowed_paths=[str(OUT)])
|
| 866 |
|
|
|
|
| 37 |
CT_CACHE = C.CACHE / "ct"
|
| 38 |
CT_KEEP = int(8e9) # CT chunks kept between runs; the oldest go first
|
| 39 |
GAME = HERE / "assets" / "game.npz"
|
| 40 |
+
GUIDE = HERE / "guide" / "index.html"
|
| 41 |
CONNECT_HTML = HERE / "connect.html"
|
| 42 |
PATCHES = [("Quick: 60 patches, about 2 minutes", 60), ("Thorough: 150 patches, about 5 minutes", 150),
|
| 43 |
("Whole segment: up to 400 patches, about 15 minutes", 400)]
|
|
|
|
| 244 |
|
| 245 |
# ---------------------------------------------------------------- what the page shows
|
| 246 |
|
| 247 |
+
def compare_html(res):
|
| 248 |
+
"""compare_results' answer as two columns, with the better side of each row marked."""
|
| 249 |
+
def name(v):
|
| 250 |
+
if isinstance(v, dict):
|
| 251 |
+
v = " · ".join(str(v[k]) for k in ("sample", "name", "volume") if v.get(k))
|
| 252 |
+
v = str(v or "?").rstrip("/").rsplit("/", 1)[-1] # a mesh path: its folder name, not where it was cached
|
| 253 |
+
return html.escape(v.rsplit("_mesh_", 1)[-1])
|
| 254 |
+
rows = [("Patches checked", "patches", lambda v: f"{v}", None),
|
| 255 |
+
("Fibers show on", "share_fibers_visible", pct, 1),
|
| 256 |
+
("Median fiber evidence", "median_evidence", lambda v: f"{v:.2f}", 1),
|
| 257 |
+
("Unsure", "share_unsure", pct, -1)]
|
| 258 |
+
body = []
|
| 259 |
+
for label, k, fmt, better in rows:
|
| 260 |
+
a, b = res["summary"][k]
|
| 261 |
+
cells = [fmt(v) if v is not None else "n/a" for v in (a, b)]
|
| 262 |
+
if better and a is not None and b is not None and abs(a - b) > 0.02:
|
| 263 |
+
win = 0 if (a - b) * better > 0 else 1
|
| 264 |
+
cells[win] = f"<b>{cells[win]}</b> ◀"
|
| 265 |
+
body.append(f"<tr><td>{label}</td><td>{cells[0]}</td><td>{cells[1]}</td></tr>")
|
| 266 |
+
pr = res.get("paired_same_grid")
|
| 267 |
+
paired = (f"<p class='fc-meta'>Same mesh grid, {pr['patches']} patches in both: A clearly better on "
|
| 268 |
+
f"{pct(pr['a_better'])}, B on {pct(pr['b_better'])}, mean difference {pr['mean_diff']:+.3f} (A minus B)."
|
| 269 |
+
"</p>" if pr else "<p class='fc-meta'>Different mesh grids, so the summaries are compared, not patch "
|
| 270 |
+
"by patch.</p>")
|
| 271 |
+
return (f"<table class='fc-cmp'><thead><tr><th></th><th>A<span>{name(res.get('a'))}</span></th>"
|
| 272 |
+
f"<th>B<span>{name(res.get('b'))}</span></th></tr></thead><tbody>{''.join(body)}</tbody></table>{paired}")
|
| 273 |
+
|
| 274 |
+
|
| 275 |
def pct(v):
|
| 276 |
return "n/a" if v is None or v != v else f"{v:.0%}"
|
| 277 |
|
|
|
|
| 306 |
· {setting} · judge: {'free local model' if rec['judge'] == 'local' else 'Claude, your key'}{cost}</p>
|
| 307 |
<details class="fc-untested"><summary>Untested hints ({jumps} possible jumps)</summary>
|
| 308 |
<p>These have not passed a test of their own yet, so read them as ideas, not results.
|
| 309 |
+
<b>Jump hints</b>: {jumps} patches with low evidence where every neighbor is clear, which can mark a sheet jump or a
|
| 310 |
stretch off the sheet. <b>Fiber direction</b>: {pct(s['direction_share_horizontal'])} of patches have strands
|
| 311 |
running along the mesh's columns (horizontal in the map), the rest along its rows. <b>Best depth</b>: the median
|
| 312 |
patch looks most fibrous {s['depth_offset_median']:+g} voxels from the traced surface.</p></details>
|
|
|
|
| 318 |
return [(str(f), f"{f.stem.split('-')[-1]} {f.stem.split('-')[1]}") for f in files]
|
| 319 |
|
| 320 |
|
| 321 |
+
def with_figures(text):
|
| 322 |
+
"""Fill the guides' [[fig NAME | caption]] slots with the pictures in assets/ (tools/build_media.py); a slot whose
|
| 323 |
+
picture this copy lacks is dropped, so the guides read the same without them."""
|
| 324 |
+
out, rest = [], text
|
| 325 |
+
while "[[fig " in rest:
|
| 326 |
+
head, _, tail = rest.partition("[[fig ")
|
| 327 |
+
spec, _, rest = tail.partition("]]")
|
| 328 |
+
name, _, caption = (x.strip() for x in spec.partition("|"))
|
| 329 |
+
out.append(head)
|
| 330 |
+
if (HERE / "assets" / name).is_file():
|
| 331 |
+
alt = html.escape(" ".join(caption.split()), quote=True)
|
| 332 |
+
out.append(f'<figure><img src="/gradio_api/file=assets/{name}" alt="{alt}" loading="lazy">'
|
| 333 |
+
f'<figcaption>{html.escape(" ".join(caption.split()))} Recorded on public scrolls on no prize '
|
| 334 |
+
'list.</figcaption></figure>')
|
| 335 |
+
return "".join(out) + rest
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
def guide_html():
|
| 339 |
+
"""The welcome page, its pictures read from this Space's assets/ folder (tools/build_media.py)."""
|
| 340 |
+
if not GUIDE.is_file():
|
| 341 |
+
return "<p>The welcome page is missing from this copy.</p>"
|
| 342 |
+
page = GUIDE.read_text()
|
| 343 |
+
# its pictures point at Gradio's file route (a <base> cannot do it); in-page anchors scroll inside the frame,
|
| 344 |
+
# ?tab= links switch the Space's tabs, other links open a new tab (as Lasagna Fields does)
|
| 345 |
+
page = page.replace("'assets/", "'/gradio_api/file=assets/").replace('"assets/', '"/gradio_api/file=assets/')
|
| 346 |
+
fix = ('<script>document.addEventListener("click",function(e){var a=e.target.closest&&e.target.closest("a");'
|
| 347 |
+
'if(!a)return;var h=a.getAttribute("href")||"";if(h.charAt(0)==="#"){e.preventDefault();'
|
| 348 |
+
'var t=document.getElementById(h.slice(1));if(t)t.scrollIntoView({behavior:"smooth"});}'
|
| 349 |
+
'else if(h.indexOf("?tab=")===0){e.preventDefault();window.parent.postMessage({fcTab:h.slice(5)},"*");}'
|
| 350 |
+
'else if(/^https?:/.test(h)){a.target="_blank";a.rel="noopener";}},true);</script>')
|
| 351 |
+
page = page.replace("<head>", "<head>" + fix, 1)
|
| 352 |
+
return f'<iframe title="Fiber Checker" srcdoc="{html.escape(page, quote=True)}"></iframe>'
|
| 353 |
+
|
| 354 |
+
|
| 355 |
def report_frame(out):
|
| 356 |
page = (Path(out) / "report.html").read_text()
|
| 357 |
return f'<iframe class="fc-report" title="Report" srcdoc="{html.escape(page, quote=True)}"></iframe>'
|
|
|
|
| 505 |
judges them on the CPU.
|
| 506 |
3. The map, the summary and the download appear on the right. Try the **Fiber or not?** tab while you wait.
|
| 507 |
|
| 508 |
+
[[fig howto-check.gif | A check from start to finish, sped up: pick an example, press Check fibers, read the map.]]
|
| 509 |
+
|
| 510 |
+
[[fig howto-game.gif | Fiber or not?: say which render shows fibers, then see what the free judge said.]]
|
| 511 |
+
|
| 512 |
<h3 id="read">2. Read the result</h3>
|
| 513 |
|
| 514 |
- **The map** is the segment as traced, flattened on its own mesh grid. Yellow and green: clear fibers. Purple: weak.
|
|
|
|
| 519 |
- **Unsure**: the judge's answer changed when the patch was turned by a quarter turn.
|
| 520 |
- **Clearest and weakest patches** are the renders themselves, so you can see what the judge saw.
|
| 521 |
|
| 522 |
+
[[fig howto-result.webp | A finished check: the map, the summary with its self-check, and the download.]]
|
| 523 |
+
|
| 524 |
<h3 id="own">3. Check your own trace</h3>
|
| 525 |
|
| 526 |
Zip your tifxyz folder (x.tif, y.tif and z.tif; VC3D and villa write these) and upload it on **Check a segment**,
|
| 527 |
under **Your own mesh**, with the 14-digit id of the scan its coordinates are in (level-0 voxels). The Space reads the
|
| 528 |
CT of that scan from the open data. Meshes on 2.2 to 2.4 um scans are checked in the tested setting.
|
| 529 |
|
| 530 |
+
[[fig howto-upload.webp | Your own mesh: the zip goes in the box, the scan id below it.]]
|
| 531 |
+
|
| 532 |
<h3 id="ab">4. Compare two traces</h3>
|
| 533 |
|
| 534 |
Check both (two segments, or your trace before and after an edit), download each zip, and give the two
|
| 535 |
`fiber.json` files to **Compare**. When both are meshes of the same grid, it compares them patch by patch: the share
|
| 536 |
of patches where each is clearly better, and the mean difference.
|
| 537 |
|
| 538 |
+
[[fig howto-compare.gif | Two fiber.json files in, a side-by-side table out. These two are checks of the same segment on
|
| 539 |
+
two scans, whose meshes have different grids, so the summaries are compared.]]
|
| 540 |
+
|
| 541 |
<h3 id="prize">5. For prize entries</h3>
|
| 542 |
|
| 543 |
`report.html` in the download is a one-page summary in the prize's words: the map, the self-check, the clearest and
|
|
|
|
| 581 |
- **Outside the tested setting** (other voxel sizes, your own meshes at other spacings): it still runs, and says so.
|
| 582 |
- **Untested hints**: jump hints, fiber direction and best depth are ideas until a test of their own passes.
|
| 583 |
- **Which sheet**: each patch is rendered at the brightest depth within 5 voxels of the trace (the tested rule), and
|
| 584 |
+
papyrus sheets lie close together, so a trace a few voxels off, or on the neighboring sheet, can still show fibers.
|
| 585 |
A clear map says fibers show on and near your surface; it does not prove the trace stayed on one sheet.
|
| 586 |
|
| 587 |
<h3 id="wrong">When something goes wrong</h3>
|
|
|
|
| 643 |
.fc-legend { font-size:12.5px; color: var(--body-text-color-subdued); }
|
| 644 |
.fc-legend i { display:inline-block; width:11px; height:11px; border-radius:3px; margin:0 4px -1px 10px; }
|
| 645 |
.fc-report { width:100%; height:80vh; border:0; border-radius:12px; }
|
| 646 |
+
.fc-guide iframe { width:100%; height:calc(100vh - 150px); min-height:640px; border:0; border-radius:12px;
|
| 647 |
+
background:#f6f1e7; }
|
| 648 |
+
.fc-howto img { border-radius:10px; border:1px solid var(--border-color-primary); max-width:min(100%, 820px);
|
| 649 |
+
display:block; margin:10px 0 4px; }
|
| 650 |
+
.fc-howto figure { margin:10px 0 18px; } .fc-howto figcaption { font-size:13px; color: var(--body-text-color-subdued); }
|
| 651 |
+
.fc-strip { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:10px; max-width:820px; margin:10px 0; }
|
| 652 |
+
.fc-strip img { width:100%; margin:0; aspect-ratio:1; object-fit:cover; }
|
| 653 |
+
.fc-strip span { display:block; font-size:12.5px; color: var(--body-text-color-subdued); margin-top:4px; }
|
| 654 |
+
.fc-drop .wrap { padding:14px 10px !important; gap:4px !important; line-height:1.35 !important; }
|
| 655 |
+
.fc-cmp { width:100%; max-width:820px; border-collapse:collapse; font-size:15px; margin-top:6px; }
|
| 656 |
+
.fc-cmp th, .fc-cmp td { text-align:left; padding:9px 12px; border-bottom:1px solid var(--border-color-primary); }
|
| 657 |
+
.fc-cmp th span { display:block; font-weight:400; font-size:12.5px; color: var(--body-text-color-subdued); }
|
| 658 |
+
.fc-cmp td:not(:first-child) { font-variant-numeric: tabular-nums; }
|
| 659 |
.fc-howto { max-width: 1000px; margin: 0 auto !important; }
|
| 660 |
.fc-howto h3 { scroll-margin-top: 16px; margin-top: 1.6em !important; }
|
| 661 |
.fc-toc { font-size: 14px; line-height: 1.9; padding: 10px 14px; border-radius: 10px;
|
|
|
|
| 669 |
.fc-head h1 { font-size:24px; }
|
| 670 |
.fc-head span { display:none; }
|
| 671 |
#fc-tabs .tab-container button { padding:6px 6px !important; font-size:12.5px !important; }
|
| 672 |
+
.fc-guide iframe { height:calc(100vh - 120px); min-height:520px; }
|
| 673 |
+
.fc-strip { grid-template-columns:repeat(2,minmax(0,1fr)); }
|
| 674 |
}
|
| 675 |
"""
|
| 676 |
|
|
|
|
| 691 |
TAB_JS = """
|
| 692 |
() => {
|
| 693 |
const pick = (name) => {
|
| 694 |
+
// by label: on a phone the later tabs sit in Gradio's overflow menu, as buttons without role="tab"
|
| 695 |
+
const labels = {home: 'Welcome', check: 'Check a segment', game: 'Fiber or not?', compare: 'Compare',
|
| 696 |
+
howto: 'How-to guides', agents: 'Agents and API'};
|
| 697 |
+
const btns = [...document.querySelectorAll('#fc-tabs button')].filter((b) => b.textContent.trim() === labels[name]);
|
| 698 |
+
const btn = btns.find((b) => b.getAttribute('role') === 'tab') || btns[btns.length - 1];
|
| 699 |
+
if (btn) btn.click();
|
| 700 |
};
|
| 701 |
+
window.addEventListener('message', (e) => { if (e.data && e.data.fcTab) pick(e.data.fcTab); });
|
| 702 |
const q = new URLSearchParams(window.location.search).get('tab');
|
| 703 |
if (q) setTimeout(() => pick(q), 300);
|
| 704 |
}
|
|
|
|
| 814 |
gr.HTML('<div class="fc-head"><div class="weave"></div><h1>Fiber Checker</h1><span>Are papyrus fibers '
|
| 815 |
'visible on a traced scroll surface? A free fiber map of any segment, patch by patch.</span></div>')
|
| 816 |
with gr.Tabs(elem_id="fc-tabs"):
|
| 817 |
+
with gr.Tab("Welcome", id="home"):
|
| 818 |
+
gr.HTML(guide_html(), elem_classes="fc-guide")
|
| 819 |
with gr.Tab("Check a segment", id="check"):
|
| 820 |
gr.Markdown(INTRO)
|
| 821 |
with gr.Row(equal_height=False):
|
|
|
|
| 833 |
go = gr.Button("Check fibers", variant="primary", size="lg")
|
| 834 |
with gr.Tab("Your own mesh"):
|
| 835 |
upload = gr.File(label="Zip of your tifxyz folder (x.tif, y.tif, z.tif)",
|
| 836 |
+
file_types=[".zip"], type="filepath")
|
| 837 |
up_vol = gr.Textbox(label="14-digit scan id its coordinates are in (level-0 voxels)",
|
| 838 |
placeholder="20250526151718")
|
| 839 |
go_up = gr.Button("Check my mesh", variant="primary", size="lg")
|
|
|
|
| 880 |
gr.Markdown("Give two `fiber.json` files (from the downloads, or the command line tool): two "
|
| 881 |
"segments, two tracers, or one trace before and after an edit.")
|
| 882 |
with gr.Row():
|
| 883 |
+
ca = gr.File(label="A: fiber.json", file_types=[".json"], type="filepath", elem_classes="fc-drop")
|
| 884 |
+
cb = gr.File(label="B: fiber.json", file_types=[".json"], type="filepath", elem_classes="fc-drop")
|
| 885 |
cgo = gr.Button("Compare", variant="primary")
|
| 886 |
+
cside = gr.HTML()
|
| 887 |
+
with gr.Accordion("Record (JSON)", open=False):
|
| 888 |
+
cout = gr.JSON(label="Side by side")
|
| 889 |
with gr.Tab("How-to guides", id="howto"):
|
| 890 |
+
gr.Markdown(with_figures(HOW_TO), elem_classes="fc-howto")
|
| 891 |
with gr.Tab("Agents and API", id="agents"):
|
| 892 |
gr.Markdown(AGENTS.format(host=host))
|
| 893 |
with gr.Accordion("Connect your tool: the scans and meshes by URL", open=True):
|
|
|
|
| 923 |
game_tab.select(guard(first_pair), st, [img_a, img_b, reveal, score_md, st], **hidden)
|
| 924 |
pick_a.click(lambda s: answer("A", s), st, [reveal, score_md, st], **hidden)
|
| 925 |
pick_b.click(lambda s: answer("B", s), st, [reveal, score_md, st], **hidden)
|
| 926 |
+
def on_compare(a, b):
|
| 927 |
+
res = compare_results(a, b)
|
| 928 |
+
return compare_html(res), res
|
| 929 |
+
cgo.click(guard(on_compare), [ca, cb], [cside, cout], **hidden)
|
| 930 |
|
| 931 |
def api_check(sample_, segment_, volume_, patches_, request: gr.Request, progress=gr.Progress()):
|
| 932 |
return check_segment(sample_, segment_, volume_, int(patches_ or 60), request=request, progress=progress)
|
|
|
|
| 961 |
|
| 962 |
|
| 963 |
def launch_kwargs():
|
| 964 |
+
import gradio as gr
|
| 965 |
+
if (HERE / "assets").is_dir():
|
| 966 |
+
gr.set_static_paths(paths=[str(HERE / "assets")])
|
| 967 |
+
os.chdir(HERE) # the welcome page's assets/... resolve against the Space's folder
|
| 968 |
return dict(theme=theme(), css=CSS, head=HEAD, footer_links=["api", "gradio"], mcp_server=True,
|
| 969 |
max_file_size="200mb", allowed_paths=[str(OUT)])
|
| 970 |
|
assets/hero.webp
ADDED
|
Git LFS Details
|
assets/howto-check.gif
ADDED
|
Git LFS Details
|
assets/howto-compare.gif
ADDED
|
assets/howto-game.gif
ADDED
|
Git LFS Details
|
assets/howto-result.webp
ADDED
|
assets/howto-upload.webp
ADDED
|
assets/map-summary.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"patches": 60, "median_evidence": 0.988, "share_fibers_visible": 1.0, "share_unsure": 0.0, "self_check": {"against": {"noise": 1.0, "blur": 1.0, "scramble": 1.0}, "passed": true}, "depth_offset_median": 0.0, "direction_share_horizontal": 0.517}
|
assets/map.png
ADDED
|
assets/media.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"note": "Renders of the Vesuvius Challenge open data (CC BY-NC 4.0) along open-data meshes of public scrolls on no prize list, made by fibercheck's renderer. Not model output except map.png.",
|
| 3 |
+
"pictures": {
|
| 4 |
+
"hero.webp": [
|
| 5 |
+
"PHerc0500P2 \u00b7 scan 20250526151718 \u00b7 segment 20250611171318 \u00b7 patch r252 c252 (judge 0.99)",
|
| 6 |
+
"PHerc0814 \u00b7 scan 20260309142202 \u00b7 segment 20250925161630 \u00b7 patch r168 c672 (judge 1.00)",
|
| 7 |
+
"PHerc0343P \u00b7 scan 20260304131111 \u00b7 segment 20250902170447 \u00b7 patch r196 c252 (judge 0.99)",
|
| 8 |
+
"PHerc0139 \u00b7 scan 20260102150214 \u00b7 segment 20250108000005 \u00b7 patch r868 c392 (judge 0.96)",
|
| 9 |
+
"PHerc0814 \u00b7 scan 20260309142202 \u00b7 segment 20250925161630 \u00b7 patch r336 c420 (judge 1.00)",
|
| 10 |
+
"PHerc0500P2 \u00b7 scan 20250526151718 \u00b7 segment 20250611171318 \u00b7 patch r28 c196 (judge 0.99)",
|
| 11 |
+
"PHerc0343P \u00b7 scan 20260304131111 \u00b7 segment 20250902170447 \u00b7 patch r448 c336 (judge 0.95)",
|
| 12 |
+
"PHerc0009B \u00b7 scan 20250820154339 \u00b7 segment 20250919064353 \u00b7 patch r868 c532 (judge 1.00)",
|
| 13 |
+
"PHerc0500P2 \u00b7 scan 20250526151718 \u00b7 segment 20250611171318 \u00b7 patch r364 c308 (judge 0.99)",
|
| 14 |
+
"PHerc0814 \u00b7 scan 20260309142202 \u00b7 segment 20250925161630 \u00b7 patch r616 c1008 (judge 1.00)",
|
| 15 |
+
"PHerc0343P \u00b7 scan 20260304131111 \u00b7 segment 20250902170447 \u00b7 patch r28 c112 (judge 0.99)",
|
| 16 |
+
"PHerc0139 \u00b7 scan 20260102150214 \u00b7 segment 20250108000005 \u00b7 patch r980 c896 (judge 0.99)",
|
| 17 |
+
"PHerc0500P2 \u00b7 scan 20250526151718 \u00b7 segment 20250611171318 \u00b7 patch r364 c140 (judge 1.00)",
|
| 18 |
+
"PHerc0814 \u00b7 scan 20260309142202 \u00b7 segment 20250925161630 \u00b7 patch r336 c2772 (judge 1.00)",
|
| 19 |
+
"PHerc0343P \u00b7 scan 20260304131111 \u00b7 segment 20250902170447 \u00b7 patch r532 c420 (judge 0.97)",
|
| 20 |
+
"PHerc0009B \u00b7 scan 20250820154339 \u00b7 segment 20250919064353 \u00b7 patch r784 c924 (judge 1.00)",
|
| 21 |
+
"PHerc0500P2 \u00b7 scan 20250526151718 \u00b7 segment 20250611171318 \u00b7 patch r420 c308 (judge 0.96)",
|
| 22 |
+
"PHerc0814 \u00b7 scan 20260309142202 \u00b7 segment 20250925161630 \u00b7 patch r308 c3136 (judge 0.89)",
|
| 23 |
+
"PHerc0343P \u00b7 scan 20260304131111 \u00b7 segment 20250902170447 \u00b7 patch r84 c196 (judge 1.00)",
|
| 24 |
+
"PHerc0139 \u00b7 scan 20260102150214 \u00b7 segment 20250108000005 \u00b7 patch r700 c504 (judge 0.99)",
|
| 25 |
+
"PHerc0500P2 \u00b7 scan 20250526151718 \u00b7 segment 20250611171318 \u00b7 patch r84 c252 (judge 0.99)",
|
| 26 |
+
"PHerc0814 \u00b7 scan 20260309142202 \u00b7 segment 20250925161630 \u00b7 patch r700 c2268 (judge 0.98)",
|
| 27 |
+
"PHerc0343P \u00b7 scan 20260304131111 \u00b7 segment 20250902170447 \u00b7 patch r336 c140 (judge 0.98)",
|
| 28 |
+
"PHerc0009B \u00b7 scan 20250820154339 \u00b7 segment 20250919064353 \u00b7 patch r1092 c840 (judge 1.00)"
|
| 29 |
+
],
|
| 30 |
+
"pair-0-*.webp": "PHerc0500P2 \u00b7 scan 20250526151718 \u00b7 segment 20250611171318 \u00b7 patch r252 c252",
|
| 31 |
+
"pair-1-*.webp": "PHerc0343P \u00b7 scan 20260304131111 \u00b7 segment 20250902170447 \u00b7 patch r532 c420",
|
| 32 |
+
"pair-2-*.webp": "PHerc0139 \u00b7 scan 20260102150214 \u00b7 segment 20250108000005 \u00b7 patch r868 c392",
|
| 33 |
+
"map.png": {
|
| 34 |
+
"model_output": true,
|
| 35 |
+
"segment": "/tmp/fiber-checker/meshes/vesuvius-challenge-open-data.s3.us-east-1.amazonaws.com_PHerc0139_segments_20250108000005-w030_2025010818_mesh_20250108000005-on-20260102150214-2.399um.tifxyz",
|
| 36 |
+
"ct": "https://vesuvius-challenge-open-data.s3.us-east-1.amazonaws.com/PHerc0139/volumes/20260102150214-2.399um-0.2m-78keV-masked.zarr/",
|
| 37 |
+
"summary": {
|
| 38 |
+
"patches": 60,
|
| 39 |
+
"median_evidence": 0.988,
|
| 40 |
+
"share_fibers_visible": 1.0,
|
| 41 |
+
"share_unsure": 0.0,
|
| 42 |
+
"self_check": {
|
| 43 |
+
"against": {
|
| 44 |
+
"noise": 1.0,
|
| 45 |
+
"blur": 1.0,
|
| 46 |
+
"scramble": 1.0
|
| 47 |
+
},
|
| 48 |
+
"passed": true
|
| 49 |
+
},
|
| 50 |
+
"depth_offset_median": 0.0,
|
| 51 |
+
"direction_share_horizontal": 0.517
|
| 52 |
+
}
|
| 53 |
+
},
|
| 54 |
+
"ui.webp": "interface screenshot of the Check tab with a finished check (PHerc0139 w030)",
|
| 55 |
+
"howto-check.gif": "interface recording of the check guide at 1100 px (public scrolls on no prize list)",
|
| 56 |
+
"howto-game.gif": "interface recording of the game guide at 1100 px (public scrolls on no prize list)",
|
| 57 |
+
"howto-compare.gif": "interface recording of the compare guide at 1100 px (public scrolls on no prize list)",
|
| 58 |
+
"howto-result.webp": "interface screenshot (result, PHerc0139 w030)",
|
| 59 |
+
"howto-upload.webp": "interface screenshot (upload, PHerc0139 w030)"
|
| 60 |
+
}
|
| 61 |
+
}
|
assets/pair-0-off.webp
ADDED
|
assets/pair-0-on.webp
ADDED
|
assets/pair-0-scramble.webp
ADDED
|
assets/pair-0-shuffle.webp
ADDED
|
assets/pair-1-off.webp
ADDED
|
assets/pair-1-on.webp
ADDED
|
assets/pair-1-scramble.webp
ADDED
|
assets/pair-1-shuffle.webp
ADDED
|
assets/pair-2-off.webp
ADDED
|
assets/pair-2-on.webp
ADDED
|
assets/pair-2-scramble.webp
ADDED
|
assets/pair-2-shuffle.webp
ADDED
|
assets/tile-0.webp
ADDED
|
assets/tile-1.webp
ADDED
|
assets/tile-2.webp
ADDED
|
assets/tile-3.webp
ADDED
|
assets/tile-4.webp
ADDED
|
assets/tile-5.webp
ADDED
|
assets/ui.webp
ADDED
|
guide/index.html
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>Fiber Checker</title>
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,650&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
| 9 |
+
<style>
|
| 10 |
+
:root{--bg:#f6f1e7;--card:#fffdf8;--ink:#2a2118;--ink2:#5e5144;--ink3:#857767;--line:#e3d7c3;--accent:#9a5b1f;--accent2:#6b3b16;--shadow:0 1px 2px rgba(60,40,20,.06),0 6px 20px rgba(60,40,20,.06)}
|
| 11 |
+
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){--bg:#14120f;--card:#1f1c18;--ink:#f1e9dc;--ink2:#c9bba8;--ink3:#9c8f7e;--line:#332e28;--accent:#d89a52;--accent2:#e8b273;--shadow:0 1px 2px rgba(0,0,0,.3)}}
|
| 12 |
+
:root[data-theme="dark"]{--bg:#14120f;--card:#1f1c18;--ink:#f1e9dc;--ink2:#c9bba8;--ink3:#9c8f7e;--line:#332e28;--accent:#d89a52;--accent2:#e8b273;--shadow:0 1px 2px rgba(0,0,0,.3)}
|
| 13 |
+
*{box-sizing:border-box}
|
| 14 |
+
html{scroll-behavior:smooth}
|
| 15 |
+
body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.6 Inter,system-ui,sans-serif}
|
| 16 |
+
a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}
|
| 17 |
+
h1,h2,h3{font-family:Fraunces,Georgia,serif;font-weight:650;letter-spacing:-.01em;line-height:1.15}
|
| 18 |
+
h2{font-size:clamp(28px,3.4vw,38px);margin:0 0 10px}
|
| 19 |
+
h3{font-size:20px;margin:0 0 6px}
|
| 20 |
+
.wrap{max-width:1180px;margin:0 auto;padding:0 24px}
|
| 21 |
+
nav{position:sticky;top:0;z-index:5;background:color-mix(in srgb,var(--bg) 88%,transparent);backdrop-filter:blur(8px);border-bottom:1px solid var(--line)}
|
| 22 |
+
nav .wrap{display:flex;align-items:center;gap:18px;height:52px}
|
| 23 |
+
.brand{font-family:Fraunces,serif;font-weight:650;font-size:18px;color:var(--ink);white-space:nowrap}
|
| 24 |
+
.navlinks{display:flex;gap:18px;overflow-x:auto;scrollbar-width:none;flex:1}
|
| 25 |
+
.navlinks a{color:var(--ink2);font-size:14px;white-space:nowrap}
|
| 26 |
+
header.hero{position:relative;overflow:hidden;border-bottom:1px solid var(--line);min-height:560px}
|
| 27 |
+
.hero .photos{position:absolute;top:0;right:0;bottom:0;width:62%;background:url('assets/hero.webp') center/cover no-repeat;filter:sepia(.35) saturate(1.1)}
|
| 28 |
+
.hero:after{content:"";position:absolute;inset:0;pointer-events:none;background:linear-gradient(90deg,var(--bg) 0,var(--bg) 40%,color-mix(in srgb,var(--bg) 82%,transparent) 54%,color-mix(in srgb,var(--bg) 25%,transparent) 76%,transparent 100%)}
|
| 29 |
+
.hero .wrap{position:relative;z-index:2;padding-top:72px;padding-bottom:64px}
|
| 30 |
+
.eyebrow{text-transform:uppercase;letter-spacing:.14em;font-size:12px;font-weight:600;color:var(--accent)}
|
| 31 |
+
.hero h1{font-size:clamp(36px,5.6vw,64px);max-width:700px;margin:12px 0 18px}
|
| 32 |
+
.lede{font-size:19px;max-width:600px;margin:0}
|
| 33 |
+
.by{margin-top:18px;color:var(--ink2);font-size:15px}.by b{color:var(--ink)}
|
| 34 |
+
.stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin-top:36px;max-width:720px}
|
| 35 |
+
.stat{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:14px 16px;box-shadow:var(--shadow)}
|
| 36 |
+
.stat b{display:block;font-family:Fraunces,serif;font-size:28px;font-weight:650;line-height:1.1}
|
| 37 |
+
.stat span{font-size:13px;color:var(--ink3)}
|
| 38 |
+
.cta{display:flex;gap:12px;flex-wrap:wrap;margin-top:28px}
|
| 39 |
+
.btn{display:inline-flex;align-items:center;gap:8px;border-radius:10px;padding:10px 16px;font-weight:600;font-size:15px;border:1px solid var(--line);background:var(--card);color:var(--ink)}
|
| 40 |
+
.btn.primary{background:var(--accent);border-color:var(--accent);color:#fff}
|
| 41 |
+
.btn:hover{text-decoration:none;filter:brightness(1.05)}
|
| 42 |
+
section{padding:72px 0;border-bottom:1px solid var(--line)}
|
| 43 |
+
.sub{color:var(--ink2);max-width:760px;margin:0 0 28px;font-size:17px}
|
| 44 |
+
.pairs{display:grid;gap:22px}
|
| 45 |
+
.pair{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;background:var(--card);border:1px solid var(--line);border-radius:16px;padding:14px;box-shadow:var(--shadow)}
|
| 46 |
+
.pair figure{margin:0}
|
| 47 |
+
.pair img{width:100%;aspect-ratio:1;object-fit:cover;border-radius:10px;display:block}
|
| 48 |
+
figcaption{font-size:13px;color:var(--ink3);margin-top:6px;line-height:1.4}
|
| 49 |
+
figcaption b{color:var(--ink);font-weight:600}
|
| 50 |
+
.tag{display:inline-block;font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;padding:2px 8px;border-radius:99px;margin-bottom:4px}
|
| 51 |
+
.yes{background:#e3f1e7;color:#1f6b45}.no{background:#f5e3df;color:#9a2e20}
|
| 52 |
+
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]) .yes{background:#1d3427;color:#7fd8a6}:root:not([data-theme="light"]) .no{background:#3a221d;color:#ff9e8f}}
|
| 53 |
+
.pairsrc{grid-column:1/-1;font-size:12.5px;color:var(--ink3);margin:0}
|
| 54 |
+
.mapgrid{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(0,1fr);gap:24px;align-items:start}
|
| 55 |
+
.mapbox{background:#18181c;border-radius:16px;padding:14px;text-align:center;box-shadow:var(--shadow)}
|
| 56 |
+
.mapbox img{max-width:100%;max-height:560px;image-rendering:pixelated}
|
| 57 |
+
.legend{font-size:13px;color:var(--ink3);margin-top:10px}
|
| 58 |
+
.legend i{display:inline-block;width:11px;height:11px;border-radius:3px;margin:0 4px -1px 10px}
|
| 59 |
+
.tiles{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-top:14px}
|
| 60 |
+
.tiles img{width:100%;border-radius:10px;display:block}
|
| 61 |
+
.cards{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px}
|
| 62 |
+
.card{background:var(--card);border:1px solid var(--line);border-radius:16px;padding:18px;box-shadow:var(--shadow)}
|
| 63 |
+
.card p{margin:0;color:var(--ink2);font-size:15px}
|
| 64 |
+
.card .k{font-size:26px;margin-bottom:6px}
|
| 65 |
+
.shot{width:100%;border-radius:14px;border:1px solid var(--line);box-shadow:var(--shadow);display:block}
|
| 66 |
+
table{border-collapse:collapse;width:100%;font-size:15px;background:var(--card);border-radius:14px;overflow:hidden;box-shadow:var(--shadow)}
|
| 67 |
+
th,td{text-align:left;padding:10px 14px;border-bottom:1px solid var(--line)}
|
| 68 |
+
th{font-weight:600;color:var(--ink2);font-size:13px;text-transform:uppercase;letter-spacing:.05em}
|
| 69 |
+
td.n{font-variant-numeric:tabular-nums;font-weight:600}
|
| 70 |
+
.note{font-size:14px;color:var(--ink3);margin-top:14px;max-width:820px}
|
| 71 |
+
.limits li{margin-bottom:8px}
|
| 72 |
+
footer{padding:36px 0 48px;color:var(--ink3);font-size:14px;text-align:center}
|
| 73 |
+
@media (max-width:860px){
|
| 74 |
+
.hero .photos{width:100%;opacity:.35}
|
| 75 |
+
.hero:after{background:color-mix(in srgb,var(--bg) 70%,transparent)}
|
| 76 |
+
.stats{grid-template-columns:repeat(2,minmax(0,1fr))}
|
| 77 |
+
.pair{grid-template-columns:repeat(2,minmax(0,1fr))}
|
| 78 |
+
.mapgrid{grid-template-columns:1fr}
|
| 79 |
+
.cards{grid-template-columns:repeat(2,minmax(0,1fr))}
|
| 80 |
+
}
|
| 81 |
+
@media (max-width:520px){
|
| 82 |
+
.wrap{padding:0 16px}
|
| 83 |
+
header.hero{min-height:0}
|
| 84 |
+
.hero .wrap{padding-top:40px;padding-bottom:40px}
|
| 85 |
+
.lede{font-size:17px}
|
| 86 |
+
.cards{grid-template-columns:1fr}
|
| 87 |
+
section{padding:48px 0}
|
| 88 |
+
th,td{padding:8px 10px;font-size:14px}
|
| 89 |
+
}
|
| 90 |
+
</style>
|
| 91 |
+
</head>
|
| 92 |
+
<body>
|
| 93 |
+
<nav><div class="wrap">
|
| 94 |
+
<span class="brand">Fiber Checker</span>
|
| 95 |
+
<div class="navlinks">
|
| 96 |
+
<a href="#see">What it sees</a><a href="#map">A fiber map</a><a href="#who">Who it is for</a>
|
| 97 |
+
<a href="#tested">How it was tested</a><a href="#limits">Limits</a><a href="?tab=check">Check a segment</a>
|
| 98 |
+
</div>
|
| 99 |
+
</div></nav>
|
| 100 |
+
|
| 101 |
+
<header class="hero" id="top">
|
| 102 |
+
<div class="photos" role="img" aria-label="A mosaic of CT renders along traced surfaces of public scrolls, each showing papyrus fibers"></div>
|
| 103 |
+
<div class="wrap">
|
| 104 |
+
<div class="eyebrow">Vesuvius Challenge open data · free, no key</div>
|
| 105 |
+
<h1>Are papyrus fibers visible on your traced surface?</h1>
|
| 106 |
+
<p class="lede">Pick any traced segment of the open data, or upload your own mesh. Fiber Checker renders the CT along the surface patch by patch and tells you where long, thin papyrus strands show, with a check of itself on every run.</p>
|
| 107 |
+
<p class="by">Made and shared by <b>Chase Hendrick</b>, October 2026. Scans and meshes: Vesuvius Challenge open data.</p>
|
| 108 |
+
<div class="stats">
|
| 109 |
+
<div class="stat"><b>1.00</b><span>sheet vs the gap beside it, on two scrolls it never saw</span></div>
|
| 110 |
+
<div class="stat"><b>92 to 98%</b><span>sheet vs a tile shuffle (pixel measure 53 to 57%)</span></div>
|
| 111 |
+
<div class="stat"><b>0 USD</b><span>the free judge runs on this page's CPU</span></div>
|
| 112 |
+
<div class="stat"><b>~80 s</b><span>for a 60-patch check of a segment</span></div>
|
| 113 |
+
</div>
|
| 114 |
+
<div class="cta">
|
| 115 |
+
<a class="btn primary" href="?tab=check">Check a segment</a>
|
| 116 |
+
<a class="btn primary" href="?tab=game">Play Fiber or not?</a>
|
| 117 |
+
<a class="btn" href="#see">What the judge sees</a>
|
| 118 |
+
<a class="btn" href="?tab=howto">How-to guides</a>
|
| 119 |
+
<a class="btn" href="?tab=agents">For agents and code</a>
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
</header>
|
| 123 |
+
|
| 124 |
+
<section id="see"><div class="wrap">
|
| 125 |
+
<h2>What the judge sees</h2>
|
| 126 |
+
<p class="sub">Each patch is the CT rendered on the traced surface. The judge has to prefer it over copies that keep its brightness and texture but lose the strands. On a surface that sits on papyrus, it does, by a wide margin.</p>
|
| 127 |
+
<div class="pairs" id="pairs"></div>
|
| 128 |
+
</div></section>
|
| 129 |
+
|
| 130 |
+
<section id="map"><div class="wrap">
|
| 131 |
+
<h2>A fiber map</h2>
|
| 132 |
+
<p class="sub">The result of a check: every patch colored by its fiber evidence, on the segment's own mesh grid, so it lines up with the trace. Below the map, the clearest patches the judge saw.</p>
|
| 133 |
+
<div class="mapgrid">
|
| 134 |
+
<div>
|
| 135 |
+
<div class="mapbox"><img src="assets/map.png" alt="A fiber map of PHerc0139 segment w030"></div>
|
| 136 |
+
<div class="legend"><i style="background:#fde725"></i>clear fibers<i style="background:#21918c"></i>some<i style="background:#440154"></i>weak<i style="background:#6e6e6e"></i>no sign<i style="background:#343640"></i>not scored</div>
|
| 137 |
+
</div>
|
| 138 |
+
<div>
|
| 139 |
+
<h3 id="mapnums">PHerc0139, segment w030</h3>
|
| 140 |
+
<p class="sub" style="margin-bottom:8px" id="mapline">A public scroll on no prize list, the 2.4 um scan of 2 January 2026 at level 2. This winding was one the free judge trained on, so it shows the output, not evidence of how well it works.</p>
|
| 141 |
+
<div class="tiles">
|
| 142 |
+
<img src="assets/tile-0.webp" alt="clearest patch 1"><img src="assets/tile-1.webp" alt="clearest patch 2"><img src="assets/tile-2.webp" alt="clearest patch 3">
|
| 143 |
+
<img src="assets/tile-3.webp" alt="clearest patch 4"><img src="assets/tile-4.webp" alt="clearest patch 5"><img src="assets/tile-5.webp" alt="clearest patch 6">
|
| 144 |
+
</div>
|
| 145 |
+
<p class="note">Model output about texture, never a reading of text.</p>
|
| 146 |
+
</div>
|
| 147 |
+
</div>
|
| 148 |
+
</div></section>
|
| 149 |
+
|
| 150 |
+
<section id="page"><div class="wrap">
|
| 151 |
+
<h2>The page</h2>
|
| 152 |
+
<p class="sub">Choose a scroll, a segment and a scan, press Check fibers, and the map, the numbers, the clearest and weakest patches and a download appear beside it. The download holds <code>fiber.json</code>, <code>fiber.tif</code>, the map and a one-page report.</p>
|
| 153 |
+
<img class="shot" src="assets/ui.webp" alt="The Check a segment tab with a finished check">
|
| 154 |
+
</div></section>
|
| 155 |
+
|
| 156 |
+
<section id="who"><div class="wrap">
|
| 157 |
+
<h2>Who it is for</h2>
|
| 158 |
+
<p class="sub">One check, read four ways.</p>
|
| 159 |
+
<div class="cards">
|
| 160 |
+
<div class="card"><div class="k">🌱</div><h3>Beginners</h3><p>See what papyrus fibers look like in CT, play Fiber or not? against the model, and check an example in two minutes.</p></div>
|
| 161 |
+
<div class="card"><div class="k">🏆</div><h3>Prize entrants</h3><p>The Progress Prize asks you to show fibers on your output surface. Attach the one-page report, and say which judge you used.</p></div>
|
| 162 |
+
<div class="card"><div class="k">🔬</div><h3>Researchers</h3><p>Preregistered, tested once on held-out scrolls against built controls, with the failed first round on record. Compare two tracers patch by patch.</p></div>
|
| 163 |
+
<div class="card"><div class="k">🧭</div><h3>Tracers and tool builders</h3><p>Upload a tifxyz, read <code>fiber.tif</code> on your own mesh grid, or call the API or MCP server from a script or an agent.</p></div>
|
| 164 |
+
</div>
|
| 165 |
+
</div></section>
|
| 166 |
+
|
| 167 |
+
<section id="tested"><div class="wrap">
|
| 168 |
+
<h2>How it was tested</h2>
|
| 169 |
+
<p class="sub">The free judge is a small network trained on Claude's answers about renders of four public scrolls. Its test was written down before any test image existed, then run once on two scrolls it never saw.</p>
|
| 170 |
+
<table>
|
| 171 |
+
<thead><tr><th>Pair the judge had to get right</th><th>PHerc0500P2</th><th>PHerc0343P</th><th>Pixel measure</th></tr></thead>
|
| 172 |
+
<tbody>
|
| 173 |
+
<tr><td>Sheet vs the gap beside it</td><td class="n">1.00</td><td class="n">1.00</td><td class="n">0.79, 0.76</td></tr>
|
| 174 |
+
<tr><td>Sheet vs a 14-pixel tile shuffle, a size never trained on</td><td class="n">0.98</td><td class="n">0.92</td><td class="n">0.53, 0.57</td></tr>
|
| 175 |
+
<tr><td>Sheet vs 3 voxels off</td><td class="n">0.87</td><td class="n">0.68</td><td class="n"></td></tr>
|
| 176 |
+
</tbody>
|
| 177 |
+
</table>
|
| 178 |
+
<p class="note">120 and 62 patches; the pixel measure is structure-tensor coherence. A first round failed (it preferred tile-shuffled renders, because it rewarded the edges a shuffle adds) and stays on record; the second added shuffles of other sizes to training. All of it is model output.</p>
|
| 179 |
+
</div></section>
|
| 180 |
+
|
| 181 |
+
<section id="limits"><div class="wrap">
|
| 182 |
+
<h2>Limits</h2>
|
| 183 |
+
<ul class="limits">
|
| 184 |
+
<li><b>It does not tell which sheet.</b> Each patch is rendered at the brightest depth within 5 voxels of the trace, and sheets lie close together, so a trace on the neighboring sheet can still show fibers.</li>
|
| 185 |
+
<li><b>Tested setting:</b> open-data meshes on 2.2 to 2.4 um scans at level 2. Other scans still run, and the page says so.</li>
|
| 186 |
+
<li><b>Untested hints:</b> jump hints, fiber direction and best depth are ideas until a test of their own passes.</li>
|
| 187 |
+
<li><b>Built controls, not real faulty traces,</b> were what it was tested against.</li>
|
| 188 |
+
</ul>
|
| 189 |
+
</div></section>
|
| 190 |
+
|
| 191 |
+
<footer><div class="wrap">Fiber Checker · code and weights Apache 2.0 · scans and meshes: <a href="https://scrollprize.org/data">Vesuvius Challenge open data</a>, CC BY-NC 4.0, read live and never copied · model output, not a reading</div></footer>
|
| 192 |
+
|
| 193 |
+
<script>
|
| 194 |
+
const PAIRS=[0,1,2].map(n=>({n}));
|
| 195 |
+
const KIND=[["on","yes","On the traced surface","the CT as it is: strands run across the patch"],
|
| 196 |
+
["scramble","no","Scrambled","same brightness and texture, strands gone"],
|
| 197 |
+
["shuffle","no","Tile shuffled","cut into 14-pixel tiles, turned and shuffled"],
|
| 198 |
+
["off","no","Off the surface","the CT 5 voxels from the trace"]];
|
| 199 |
+
const SRC=["PHerc0139","PHerc0500P2","PHerc0343P"];
|
| 200 |
+
document.getElementById('pairs').innerHTML=PAIRS.map(p=>'<div class="pair">'+KIND.map(k=>'<figure><img loading="lazy" src="assets/pair-'+p.n+'-'+k[0]+'.webp" alt="'+k[2]+'"><figcaption><span class="tag '+k[1]+'">'+(k[1]==='yes'?'fibers':'control')+'</span><br><b>'+k[2]+'</b>: '+k[3]+'</figcaption></figure>').join('')+'<p class="pairsrc" data-n="'+p.n+'"></p></div>').join('');
|
| 201 |
+
fetch('assets/media.json').then(r=>r.json()).then(m=>{document.querySelectorAll('.pairsrc').forEach(el=>{const s=m.pictures['pair-'+el.dataset.n+'-*.webp'];if(s)el.textContent='From '+s+'. Public scroll on no prize list.'})}).catch(()=>{});
|
| 202 |
+
</script>
|
| 203 |
+
</body>
|
| 204 |
+
</html>
|