Spaces:
Running on Zero
Running on Zero
One page scroll bar and American English
Browse files- README.md +1 -1
- app.py +18 -11
- guide/index.html +3 -3
- lasfield.py +8 -8
- media/example-result.png +2 -2
- media/howto-make.gif +2 -2
- onescroll.html +62 -0
README.md
CHANGED
|
@@ -74,7 +74,7 @@ The page is also an API and an MCP server (`https://sharpmeow-lasagna-fields.hf.
|
|
| 74 |
`list_scans`, `list_models`, `example_points`, `plan_field` and `make_field`. See the page's **Agents and API** tab.
|
| 75 |
A field uses the caller's ZeroGPU time.
|
| 76 |
|
| 77 |
-
## Credit and
|
| 78 |
|
| 79 |
Made by Chase Hendrick. Code: Apache 2.0 (see `LICENSE` and `NOTICE`). Model and code it runs: villa's lasagna authors,
|
| 80 |
MIT. Scans: Vesuvius Challenge open data, CC BY-NC 4.0; the fields, made from them, are CC BY-NC 4.0 too. Source and
|
|
|
|
| 74 |
`list_scans`, `list_models`, `example_points`, `plan_field` and `make_field`. See the page's **Agents and API** tab.
|
| 75 |
A field uses the caller's ZeroGPU time.
|
| 76 |
|
| 77 |
+
## Credit and licenses
|
| 78 |
|
| 79 |
Made by Chase Hendrick. Code: Apache 2.0 (see `LICENSE` and `NOTICE`). Model and code it runs: villa's lasagna authors,
|
| 80 |
MIT. Scans: Vesuvius Challenge open data, CC BY-NC 4.0; the fields, made from them, are CC BY-NC 4.0 too. Source and
|
app.py
CHANGED
|
@@ -316,7 +316,7 @@ def make_field(volume: str, z: float, y: float, x: float, size: float = 256, mod
|
|
| 316 |
size: the box side in voxels of the level the model reads, 256 or 512.
|
| 317 |
model: a model id from list_models; 'lasagna' is villa's own predict3d.
|
| 318 |
|
| 319 |
-
Returns a picture (middle slices along z, y and x: CT, cos, grad_mag, normals as
|
| 320 |
box, the model and its pinned commit and checkpoint, timings, and the agreement with the open data's own field
|
| 321 |
with a shifted control), and a zip with villa's output folder, field.npz and receipt.json. Uses the caller's
|
| 322 |
ZeroGPU time. Model output: geometry, not a reading.
|
|
@@ -324,7 +324,7 @@ def make_field(volume: str, z: float, y: float, x: float, size: float = 256, mod
|
|
| 324 |
p = L.plan(volume, z, y, x, int(size), model=model)
|
| 325 |
with one_run(request):
|
| 326 |
r = run_field(p, progress)
|
| 327 |
-
c = p["
|
| 328 |
pic = L.compose(r["pictures"], f"{p['sample']} · {p['size_zyx'][0]}-voxel box at z {c[0]}, y {c[1]}, x {c[2]}")
|
| 329 |
return pic, r["receipt"], r["zip"]
|
| 330 |
|
|
@@ -361,7 +361,7 @@ says which way the sheet faces at each point. villa's lasagna surface fit and th
|
|
| 361 |
|
| 362 |

|
| 363 |
|
| 364 |
-
*PHerc0139 (not on any prize list), one box about 2.5 mm a side: the CT, cos and the normal as
|
| 365 |
through z. Model output: geometry, not a reading of any text.*
|
| 366 |
|
| 367 |
<h3 id="first">1. Make your first field (one minute)</h3>
|
|
@@ -378,7 +378,7 @@ through z. Model output: geometry, not a reading of any text.*
|
|
| 378 |

|
| 379 |
|
| 380 |
- **Rows** are the middle slice of your box along z, y and x. **Columns** are the CT, cos, grad_mag and the
|
| 381 |
-
normal as
|
| 382 |
- **cos** should run along the papyrus in the CT, one bright and one dark band per layer. Where layers touch or
|
| 383 |
the CT is unclear, the bands bend or merge: that is where surface tracing is hard too.
|
| 384 |
- **The check** compares your field with the published one for the same box, and with a **control**: the same
|
|
@@ -404,7 +404,7 @@ through z. Model output: geometry, not a reading of any text.*
|
|
| 404 |
|
| 405 |
Boxes are whole 256-voxel blocks of the scan, because villa writes the field in 64-voxel chunks of its quarter
|
| 406 |
grid and puts its tiles on one lattice over the whole scan: that is why a box's field is the whole-scan field
|
| 407 |
-
there. The box is the block nearest your point, so your point is inside it but not always at its
|
| 408 |
|
| 409 |
<h3 id="download">4. Use the download</h3>
|
| 410 |
|
|
@@ -444,7 +444,7 @@ python scripts/bootstrap_venv.py --venv .venv
|
|
| 444 |
```
|
| 445 |
|
| 446 |
Measured on an NVIDIA L4 (25 Oct 2026): about 2 million voxels a second end to end, so about ten minutes per
|
| 447 |
-
cubic
|
| 448 |
|
| 449 |
<h3 id="wrong">When something goes wrong</h3>
|
| 450 |
|
|
@@ -492,8 +492,9 @@ CSS = """
|
|
| 492 |
background:repeating-linear-gradient(180deg,#c2462b 0 4px,#e07a3f 4px 7px,#f3e2bf 7px 10px,#4f7a3a 10px 11px,#f3e2bf 11px 12px); }
|
| 493 |
.lf-head h1 { white-space:nowrap; font-family: Fraunces, Georgia, serif; font-weight:650; font-size:30px; margin:0; letter-spacing:-.01em; }
|
| 494 |
.lf-head span { color: var(--body-text-color-subdued); font-size:15px; }
|
| 495 |
-
.
|
| 496 |
-
|
|
|
|
| 497 |
.lf-note p { font-size:13px; color: var(--body-text-color-subdued); margin:0; }
|
| 498 |
.lf-est p { font-size:13.5px; color: var(--body-text-color-subdued); }
|
| 499 |
.lf-point { flex-wrap: nowrap !important; }
|
|
@@ -502,6 +503,9 @@ CSS = """
|
|
| 502 |
.lf-howto img { border-radius:10px; border:1px solid var(--border-color-primary); max-width:min(100%, 760px); display:block; margin:8px 0; }
|
| 503 |
#lf-dl .empty { min-height: 64px !important; height: 64px !important; }
|
| 504 |
#lf-dl .empty .icon { height: 28px; }
|
|
|
|
|
|
|
|
|
|
| 505 |
.lf-howto { max-width: 1000px; margin: 0 auto !important; }
|
| 506 |
.lf-howto h3 { scroll-margin-top: 16px; margin-top: 1.6em !important; }
|
| 507 |
.lf-howto em { color: var(--body-text-color-subdued); font-size: 13.5px; }
|
|
@@ -510,7 +514,6 @@ CSS = """
|
|
| 510 |
@media (max-width: 640px) {
|
| 511 |
.lf-head h1 { font-size:24px; }
|
| 512 |
.lf-head span { display:none; }
|
| 513 |
-
.lf-guide iframe { height:calc(100vh - 120px); min-height:520px; }
|
| 514 |
#lf-tabs .tab-container button { padding:6px 6px !important; font-size:12.5px !important; }
|
| 515 |
}
|
| 516 |
"""
|
|
@@ -524,6 +527,9 @@ ten whole scans to download, and any box made on demand. Model output, not a rea
|
|
| 524 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 525 |
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,650&display=swap" rel="stylesheet">
|
| 526 |
"""
|
|
|
|
|
|
|
|
|
|
| 527 |
|
| 528 |
|
| 529 |
def theme():
|
|
@@ -548,7 +554,8 @@ def guide_html():
|
|
| 548 |
import html
|
| 549 |
page = GUIDE.read_text()
|
| 550 |
# its pictures (assets/..., built in its script) point at Gradio's file route; a <base> cannot do it, because a
|
| 551 |
-
# base of /gradio_api/file= resolves assets/x to /gradio_api/assets/x. Anchors scroll
|
|
|
|
| 552 |
page = page.replace("'assets/", "'/gradio_api/file=assets/").replace('"assets/', '"/gradio_api/file=assets/')
|
| 553 |
fix = ('<style>.wrap{max-width:1480px!important}</style>'
|
| 554 |
'<script>document.addEventListener("click",function(e){var a=e.target.closest&&e.target.closest("a");'
|
|
@@ -644,7 +651,7 @@ def build_demo():
|
|
| 644 |
dl = gr.File(label="Download: villa output folder, field.npz, receipt.json", height=88,
|
| 645 |
elem_id="lf-dl")
|
| 646 |
with gr.Accordion("Receipt", open=False):
|
| 647 |
-
receipt = gr.JSON()
|
| 648 |
with gr.Accordion("About the model", open=False):
|
| 649 |
gr.Markdown(ABOUT_MODEL.format(commit=L.VILLA["commit"][:12],
|
| 650 |
sha=L.WEIGHTS["sha256"][:16] + "…"))
|
|
|
|
| 316 |
size: the box side in voxels of the level the model reads, 256 or 512.
|
| 317 |
model: a model id from list_models; 'lasagna' is villa's own predict3d.
|
| 318 |
|
| 319 |
+
Returns a picture (middle slices along z, y and x: CT, cos, grad_mag, normals as color), the receipt (the
|
| 320 |
box, the model and its pinned commit and checkpoint, timings, and the agreement with the open data's own field
|
| 321 |
with a shifted control), and a zip with villa's output folder, field.npz and receipt.json. Uses the caller's
|
| 322 |
ZeroGPU time. Model output: geometry, not a reading.
|
|
|
|
| 324 |
p = L.plan(volume, z, y, x, int(size), model=model)
|
| 325 |
with one_run(request):
|
| 326 |
r = run_field(p, progress)
|
| 327 |
+
c = p["center_level0_zyx"]
|
| 328 |
pic = L.compose(r["pictures"], f"{p['sample']} · {p['size_zyx'][0]}-voxel box at z {c[0]}, y {c[1]}, x {c[2]}")
|
| 329 |
return pic, r["receipt"], r["zip"]
|
| 330 |
|
|
|
|
| 361 |
|
| 362 |

|
| 363 |
|
| 364 |
+
*PHerc0139 (not on any prize list), one box about 2.5 mm a side: the CT, cos and the normal as color, stepping
|
| 365 |
through z. Model output: geometry, not a reading of any text.*
|
| 366 |
|
| 367 |
<h3 id="first">1. Make your first field (one minute)</h3>
|
|
|
|
| 378 |

|
| 379 |
|
| 380 |
- **Rows** are the middle slice of your box along z, y and x. **Columns** are the CT, cos, grad_mag and the
|
| 381 |
+
normal as color (red for x, green for y, blue for z).
|
| 382 |
- **cos** should run along the papyrus in the CT, one bright and one dark band per layer. Where layers touch or
|
| 383 |
the CT is unclear, the bands bend or merge: that is where surface tracing is hard too.
|
| 384 |
- **The check** compares your field with the published one for the same box, and with a **control**: the same
|
|
|
|
| 404 |
|
| 405 |
Boxes are whole 256-voxel blocks of the scan, because villa writes the field in 64-voxel chunks of its quarter
|
| 406 |
grid and puts its tiles on one lattice over the whole scan: that is why a box's field is the whole-scan field
|
| 407 |
+
there. The box is the block nearest your point, so your point is inside it but not always at its center.
|
| 408 |
|
| 409 |
<h3 id="download">4. Use the download</h3>
|
| 410 |
|
|
|
|
| 444 |
```
|
| 445 |
|
| 446 |
Measured on an NVIDIA L4 (25 Oct 2026): about 2 million voxels a second end to end, so about ten minutes per
|
| 447 |
+
cubic centimeter at 9.4 um. It fits in 5.6 GB of GPU memory at its default tile, 2.3 GB at `--tile-size 128`.
|
| 448 |
|
| 449 |
<h3 id="wrong">When something goes wrong</h3>
|
| 450 |
|
|
|
|
| 492 |
background:repeating-linear-gradient(180deg,#c2462b 0 4px,#e07a3f 4px 7px,#f3e2bf 7px 10px,#4f7a3a 10px 11px,#f3e2bf 11px 12px); }
|
| 493 |
.lf-head h1 { white-space:nowrap; font-family: Fraunces, Georgia, serif; font-weight:650; font-size:30px; margin:0; letter-spacing:-.01em; }
|
| 494 |
.lf-head span { color: var(--body-text-color-subdued); font-size:15px; }
|
| 495 |
+
/* no fixed height: onescroll.html fits the guide's frame to the guide, so the page scrolls and the frame does not;
|
| 496 |
+
min-height only holds the place until it has */
|
| 497 |
+
.lf-guide iframe { width:100%; min-height:640px; border:0; border-radius:12px; background:#f7f2ea; }
|
| 498 |
.lf-note p { font-size:13px; color: var(--body-text-color-subdued); margin:0; }
|
| 499 |
.lf-est p { font-size:13.5px; color: var(--body-text-color-subdued); }
|
| 500 |
.lf-point { flex-wrap: nowrap !important; }
|
|
|
|
| 503 |
.lf-howto img { border-radius:10px; border:1px solid var(--border-color-primary); max-width:min(100%, 760px); display:block; margin:8px 0; }
|
| 504 |
#lf-dl .empty { min-height: 64px !important; height: 64px !important; }
|
| 505 |
#lf-dl .empty .icon { height: 28px; }
|
| 506 |
+
/* one page scroll bar: the download's file row and the receipt grow with what they hold, so the page scrolls */
|
| 507 |
+
#lf-dl .file-preview-holder { max-height:none !important; overflow:visible !important; }
|
| 508 |
+
#lf-receipt { max-height:none !important; overflow:visible !important; }
|
| 509 |
.lf-howto { max-width: 1000px; margin: 0 auto !important; }
|
| 510 |
.lf-howto h3 { scroll-margin-top: 16px; margin-top: 1.6em !important; }
|
| 511 |
.lf-howto em { color: var(--body-text-color-subdued); font-size: 13.5px; }
|
|
|
|
| 514 |
@media (max-width: 640px) {
|
| 515 |
.lf-head h1 { font-size:24px; }
|
| 516 |
.lf-head span { display:none; }
|
|
|
|
| 517 |
#lf-tabs .tab-container button { padding:6px 6px !important; font-size:12.5px !important; }
|
| 518 |
}
|
| 519 |
"""
|
|
|
|
| 527 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 528 |
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,650&display=swap" rel="stylesheet">
|
| 529 |
"""
|
| 530 |
+
# one page scroll bar: fits the guide's frame to its content (written by python -m kit spaceconnect render)
|
| 531 |
+
ONESCROLL = HERE / "onescroll.html"
|
| 532 |
+
HEAD += ONESCROLL.read_text() if ONESCROLL.is_file() else ""
|
| 533 |
|
| 534 |
|
| 535 |
def theme():
|
|
|
|
| 554 |
import html
|
| 555 |
page = GUIDE.read_text()
|
| 556 |
# its pictures (assets/..., built in its script) point at Gradio's file route; a <base> cannot do it, because a
|
| 557 |
+
# base of /gradio_api/file= resolves assets/x to /gradio_api/assets/x. Anchors scroll to their section (the frame
|
| 558 |
+
# is as tall as the guide, so the page scrolls, not the frame)
|
| 559 |
page = page.replace("'assets/", "'/gradio_api/file=assets/").replace('"assets/', '"/gradio_api/file=assets/')
|
| 560 |
fix = ('<style>.wrap{max-width:1480px!important}</style>'
|
| 561 |
'<script>document.addEventListener("click",function(e){var a=e.target.closest&&e.target.closest("a");'
|
|
|
|
| 651 |
dl = gr.File(label="Download: villa output folder, field.npz, receipt.json", height=88,
|
| 652 |
elem_id="lf-dl")
|
| 653 |
with gr.Accordion("Receipt", open=False):
|
| 654 |
+
receipt = gr.JSON(max_height=None, elem_id="lf-receipt")
|
| 655 |
with gr.Accordion("About the model", open=False):
|
| 656 |
gr.Markdown(ABOUT_MODEL.format(commit=L.VILLA["commit"][:12],
|
| 657 |
sha=L.WEIGHTS["sha256"][:16] + "…"))
|
guide/index.html
CHANGED
|
@@ -79,12 +79,12 @@ dl.facts{display:grid;grid-template-columns:auto 1fr;gap:6px 14px;margin:0;font-
|
|
| 79 |
dl.facts dt{color:var(--ink3)}dl.facts dd{margin:0;word-break:break-word}
|
| 80 |
.links{display:flex;gap:10px;flex-wrap:wrap;margin-top:14px}
|
| 81 |
.codebox{position:relative;margin-top:10px}
|
| 82 |
-
pre{
|
| 83 |
pre .c{color:#9a8f7e}
|
| 84 |
.codebox pre{padding-top:34px}
|
| 85 |
.copy{position:absolute;top:8px;right:8px;border:1px solid #ffffff22;background:#ffffff14;color:#efe6d6;border-radius:7px;padding:3px 9px;font-size:12px;cursor:pointer}
|
| 86 |
.copy:hover{background:#ffffff26}
|
| 87 |
-
.tabs{display:flex;gap:4px;border-bottom:1px solid var(--line);margin-top:18px}
|
| 88 |
.tabs button{border:0;background:none;padding:8px 12px;color:var(--ink2);cursor:pointer;font:inherit;font-size:14px;border-bottom:2px solid transparent;margin-bottom:-1px}
|
| 89 |
.tabs button[aria-selected="true"]{color:var(--ink);border-bottom-color:var(--accent);font-weight:600}
|
| 90 |
.detail>*,.region>*,.panel,.codebox,.cards3>*{min-width:0}
|
|
@@ -430,7 +430,7 @@ let drag=false;$('map').onpointerdown=e=>{drag=true;mapAt(e)};addEventListener('
|
|
| 430 |
|
| 431 |
/* copy */
|
| 432 |
document.addEventListener('click',e=>{const b=e.target.closest('.copy');if(!b)return;
|
| 433 |
-
const t=$(b.dataset.copy).textContent;const done=()=>{$('toast').classList.add('on');setTimeout(()=>$('toast').classList.remove('on'),1200)};
|
| 434 |
if(navigator.clipboard&&navigator.clipboard.writeText)navigator.clipboard.writeText(t).then(done,()=>fallback(t,done));else fallback(t,done)});
|
| 435 |
function fallback(t,done){const a=document.createElement('textarea');a.value=t;document.body.appendChild(a);a.select();try{document.execCommand('copy');done()}catch(e){}a.remove()}
|
| 436 |
|
|
|
|
| 79 |
dl.facts dt{color:var(--ink3)}dl.facts dd{margin:0;word-break:break-word}
|
| 80 |
.links{display:flex;gap:10px;flex-wrap:wrap;margin-top:14px}
|
| 81 |
.codebox{position:relative;margin-top:10px}
|
| 82 |
+
pre{background:var(--code);color:var(--codeink);border-radius:10px;padding:14px 16px;overflow-x:auto;overflow-y:hidden;margin:0;line-height:1.55}
|
| 83 |
pre .c{color:#9a8f7e}
|
| 84 |
.codebox pre{padding-top:34px}
|
| 85 |
.copy{position:absolute;top:8px;right:8px;border:1px solid #ffffff22;background:#ffffff14;color:#efe6d6;border-radius:7px;padding:3px 9px;font-size:12px;cursor:pointer}
|
| 86 |
.copy:hover{background:#ffffff26}
|
| 87 |
+
.tabs{display:flex;flex-wrap:wrap;gap:4px;border-bottom:1px solid var(--line);margin-top:18px}
|
| 88 |
.tabs button{border:0;background:none;padding:8px 12px;color:var(--ink2);cursor:pointer;font:inherit;font-size:14px;border-bottom:2px solid transparent;margin-bottom:-1px}
|
| 89 |
.tabs button[aria-selected="true"]{color:var(--ink);border-bottom-color:var(--accent);font-weight:600}
|
| 90 |
.detail>*,.region>*,.panel,.codebox,.cards3>*{min-width:0}
|
|
|
|
| 430 |
|
| 431 |
/* copy */
|
| 432 |
document.addEventListener('click',e=>{const b=e.target.closest('.copy');if(!b)return;
|
| 433 |
+
const t=$(b.dataset.copy).textContent;const done=()=>{b.textContent='Copied';$('toast').classList.add('on');setTimeout(()=>{b.textContent='Copy';$('toast').classList.remove('on')},1200)};
|
| 434 |
if(navigator.clipboard&&navigator.clipboard.writeText)navigator.clipboard.writeText(t).then(done,()=>fallback(t,done));else fallback(t,done)});
|
| 435 |
function fallback(t,done){const a=document.createElement('textarea');a.value=t;document.body.appendChild(a);a.select();try{document.execCommand('copy');done()}catch(e){}a.remove()}
|
| 436 |
|
lasfield.py
CHANGED
|
@@ -252,19 +252,19 @@ def plan(vid, z, y, x, size=DEFAULT_SIZE, level=None, model="lasagna"):
|
|
| 252 |
meta = zarray(v, level)
|
| 253 |
shape = [int(s) for s in meta["shape"]]
|
| 254 |
f = 2 ** level
|
| 255 |
-
|
| 256 |
-
for c, n in zip(
|
| 257 |
if not 0 <= c < n:
|
| 258 |
-
raise FieldError(f"the point {
|
| 259 |
origin = [max(0, min((c // f - size // 2 + ALIGN // 2) // ALIGN * ALIGN, (n - 1) // ALIGN * ALIGN))
|
| 260 |
-
for c, n in zip(
|
| 261 |
box_size = [min(size, n - o) for o, n in zip(origin, shape)]
|
| 262 |
read = [[max(0, o - MARGIN), min(n, o + s + MARGIN)] for o, s, n in zip(origin, box_size, shape)]
|
| 263 |
ch = meta["chunks"]
|
| 264 |
nchunks = math.prod((b - 1) // c - a // c + 1 for (a, b), c in zip(read, ch))
|
| 265 |
out = {"volume": v["volume"], "sample": v["sample"], "long_id": v["long_id"], "voxel_um": v["voxel_um"],
|
| 266 |
"url": v["url"], "level": level, "level_shape_zyx": shape, "base_shape_zyx": base["shape"],
|
| 267 |
-
"
|
| 268 |
"origin_level0_zyx": [o * f for o in origin], "size_level0_zyx": [s * f for s in box_size],
|
| 269 |
"read_zyx": read, "chunks": nchunks, "chunk_bytes": math.prod(ch) * np.dtype(meta["dtype"]).itemsize,
|
| 270 |
"model": model}
|
|
@@ -669,7 +669,7 @@ def _resize(img, shape):
|
|
| 669 |
|
| 670 |
|
| 671 |
def picture(ct, arrays, boxes, axis=0):
|
| 672 |
-
"""One middle slice through the box along `axis` (0 z, 1 y, 2 x): CT, cos, grad_mag and the normal as
|
| 673 |
(|x| red, |y| green, |z| blue), side by side at the CT's grid. Returns an RGB uint8 array."""
|
| 674 |
def mid(a):
|
| 675 |
return np.take(a, a.shape[axis] // 2, axis=axis)
|
|
@@ -691,7 +691,7 @@ def picture(ct, arrays, boxes, axis=0):
|
|
| 691 |
|
| 692 |
|
| 693 |
PANELS = ("CT", "cos", "grad_mag", "normal")
|
| 694 |
-
LEGEND = "normal as
|
| 695 |
|
| 696 |
|
| 697 |
def compose(pictures, title="", note=LEGEND):
|
|
@@ -799,7 +799,7 @@ def finish(p, prep, ran, work, out_root):
|
|
| 799 |
"claim": "Model output: a lasagna field is geometry fitted by a model, not a reading of any text."}
|
| 800 |
out_root = Path(out_root)
|
| 801 |
out_root.mkdir(parents=True, exist_ok=True)
|
| 802 |
-
stem = f"{p['sample']}-{p['volume']}-z{p['
|
| 803 |
zpath = package(Path(ran["manifest"]).parent, p, arrays, receipt, out_root / f"{stem}.zip")
|
| 804 |
return {"receipt": receipt, "zip": str(zpath), "pictures": pictures, "arrays": arrays}
|
| 805 |
|
|
|
|
| 252 |
meta = zarray(v, level)
|
| 253 |
shape = [int(s) for s in meta["shape"]]
|
| 254 |
f = 2 ** level
|
| 255 |
+
center = [int(round(float(c))) for c in (z, y, x)]
|
| 256 |
+
for c, n in zip(center, base["shape"]):
|
| 257 |
if not 0 <= c < n:
|
| 258 |
+
raise FieldError(f"the point {center} is outside the scan (level-0 shape {base['shape']})")
|
| 259 |
origin = [max(0, min((c // f - size // 2 + ALIGN // 2) // ALIGN * ALIGN, (n - 1) // ALIGN * ALIGN))
|
| 260 |
+
for c, n in zip(center, shape)]
|
| 261 |
box_size = [min(size, n - o) for o, n in zip(origin, shape)]
|
| 262 |
read = [[max(0, o - MARGIN), min(n, o + s + MARGIN)] for o, s, n in zip(origin, box_size, shape)]
|
| 263 |
ch = meta["chunks"]
|
| 264 |
nchunks = math.prod((b - 1) // c - a // c + 1 for (a, b), c in zip(read, ch))
|
| 265 |
out = {"volume": v["volume"], "sample": v["sample"], "long_id": v["long_id"], "voxel_um": v["voxel_um"],
|
| 266 |
"url": v["url"], "level": level, "level_shape_zyx": shape, "base_shape_zyx": base["shape"],
|
| 267 |
+
"center_level0_zyx": center, "origin_zyx": origin, "size_zyx": box_size,
|
| 268 |
"origin_level0_zyx": [o * f for o in origin], "size_level0_zyx": [s * f for s in box_size],
|
| 269 |
"read_zyx": read, "chunks": nchunks, "chunk_bytes": math.prod(ch) * np.dtype(meta["dtype"]).itemsize,
|
| 270 |
"model": model}
|
|
|
|
| 669 |
|
| 670 |
|
| 671 |
def picture(ct, arrays, boxes, axis=0):
|
| 672 |
+
"""One middle slice through the box along `axis` (0 z, 1 y, 2 x): CT, cos, grad_mag and the normal as color
|
| 673 |
(|x| red, |y| green, |z| blue), side by side at the CT's grid. Returns an RGB uint8 array."""
|
| 674 |
def mid(a):
|
| 675 |
return np.take(a, a.shape[axis] // 2, axis=axis)
|
|
|
|
| 691 |
|
| 692 |
|
| 693 |
PANELS = ("CT", "cos", "grad_mag", "normal")
|
| 694 |
+
LEGEND = "normal as color: x red, y green, z blue · villa lasagna, model output"
|
| 695 |
|
| 696 |
|
| 697 |
def compose(pictures, title="", note=LEGEND):
|
|
|
|
| 799 |
"claim": "Model output: a lasagna field is geometry fitted by a model, not a reading of any text."}
|
| 800 |
out_root = Path(out_root)
|
| 801 |
out_root.mkdir(parents=True, exist_ok=True)
|
| 802 |
+
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]}"
|
| 803 |
zpath = package(Path(ran["manifest"]).parent, p, arrays, receipt, out_root / f"{stem}.zip")
|
| 804 |
return {"receipt": receipt, "zip": str(zpath), "pictures": pictures, "arrays": arrays}
|
| 805 |
|
media/example-result.png
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|
media/howto-make.gif
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|
onescroll.html
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!-- space-onescroll:start -->
|
| 2 |
+
<!-- one page scroll bar: written by python -m kit spaceconnect render; do not edit -->
|
| 3 |
+
<style>iframe[srcdoc]{display:block;max-height:none!important}</style>
|
| 4 |
+
<script>
|
| 5 |
+
(function () {
|
| 6 |
+
if (window.__oneScroll) return;
|
| 7 |
+
window.__oneScroll = true;
|
| 8 |
+
var CSS = 'html,body{height:auto!important;min-height:0!important}html{overflow:hidden!important}';
|
| 9 |
+
function size(f) {
|
| 10 |
+
var d, st = f.__os;
|
| 11 |
+
try { d = f.contentDocument; } catch (e) { return; }
|
| 12 |
+
if (!d || !d.body || st.off) return;
|
| 13 |
+
var de = d.documentElement, r = Math.ceil(de.getBoundingClientRect().height), over = de.scrollHeight;
|
| 14 |
+
var ch = f.clientHeight, t = r;
|
| 15 |
+
if (over > ch + 1) t = Math.max(r, over);
|
| 16 |
+
else if (r >= st.r) t = Math.max(r, ch);
|
| 17 |
+
st.r = r;
|
| 18 |
+
if (Math.abs(t - ch) < 2) return;
|
| 19 |
+
var now = Date.now();
|
| 20 |
+
st.n = now - st.t0 < 4000 ? st.n + 1 : 1;
|
| 21 |
+
if (st.n === 1) st.t0 = now;
|
| 22 |
+
if (st.n > 40) { st.off = true; f.removeAttribute('scrolling'); var g = d.getElementById('space-onescroll'); if (g) g.remove(); return; }
|
| 23 |
+
f.style.setProperty('height', t + 'px', 'important');
|
| 24 |
+
}
|
| 25 |
+
function attach(f) {
|
| 26 |
+
var d, st = f.__os;
|
| 27 |
+
try { d = f.contentDocument; } catch (e) { return; }
|
| 28 |
+
if (!d || !d.documentElement || st.doc === d) return;
|
| 29 |
+
st.doc = d; st.r = Infinity; st.n = 0; st.off = false;
|
| 30 |
+
if (!d.getElementById('space-onescroll')) {
|
| 31 |
+
var s = d.createElement('style'); s.id = 'space-onescroll'; s.textContent = CSS;
|
| 32 |
+
(d.head || d.documentElement).appendChild(s);
|
| 33 |
+
}
|
| 34 |
+
f.setAttribute('scrolling', 'no');
|
| 35 |
+
f.style.setProperty('min-height', '0', 'important');
|
| 36 |
+
f.style.setProperty('max-height', 'none', 'important');
|
| 37 |
+
var w = d.defaultView, go = function () { size(f); };
|
| 38 |
+
if (w && w.ResizeObserver) { var ro = new w.ResizeObserver(go); ro.observe(d.documentElement); if (d.body) ro.observe(d.body); }
|
| 39 |
+
d.addEventListener('load', go, true);
|
| 40 |
+
go();
|
| 41 |
+
}
|
| 42 |
+
function bind(f) {
|
| 43 |
+
if (f.__os || f.getAttribute('data-scroll') === 'inner') return;
|
| 44 |
+
try { if (!f.contentDocument) return; } catch (e) { return; }
|
| 45 |
+
f.__os = {t0: 0, n: 0, r: 0};
|
| 46 |
+
f.addEventListener('load', function () { attach(f); });
|
| 47 |
+
if (f.contentDocument.readyState !== 'loading') attach(f);
|
| 48 |
+
else f.contentDocument.addEventListener('DOMContentLoaded', function () { attach(f); });
|
| 49 |
+
}
|
| 50 |
+
function scan() { var fs = document.getElementsByTagName('iframe'); for (var i = 0; i < fs.length; i++) bind(fs[i]); }
|
| 51 |
+
function start() {
|
| 52 |
+
scan();
|
| 53 |
+
new MutationObserver(scan).observe(document.documentElement, {childList: true, subtree: true});
|
| 54 |
+
window.addEventListener('resize', function () {
|
| 55 |
+
var fs = document.getElementsByTagName('iframe');
|
| 56 |
+
for (var i = 0; i < fs.length; i++) if (fs[i].__os) size(fs[i]);
|
| 57 |
+
});
|
| 58 |
+
}
|
| 59 |
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); else start();
|
| 60 |
+
})();
|
| 61 |
+
</script>
|
| 62 |
+
<!-- space-onescroll:end -->
|