Spaces:
Running
Running
One page scroll bar and American English
Browse files- app.py +38 -21
- fcmodel.py +1 -1
- fibercheck.py +8 -8
- onescroll.html +62 -0
app.py
CHANGED
|
@@ -39,6 +39,7 @@ CT_KEEP = int(8e9) # CT chunks kept between runs; the oldest go
|
|
| 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)]
|
| 44 |
CLAUDE_PATCHES, CLAUDE_CHECK_N = 40, 10
|
|
@@ -341,8 +342,9 @@ def guide_html():
|
|
| 341 |
if not GUIDE.is_file():
|
| 342 |
return "<p>The welcome page is missing from this copy.</p>"
|
| 343 |
page = GUIDE.read_text()
|
| 344 |
-
# its pictures point at Gradio's file route (a <base> cannot do it); in-page anchors scroll
|
| 345 |
-
# ?tab= links switch the Space's tabs, other links open a
|
|
|
|
| 346 |
page = page.replace("'assets/", "'/gradio_api/file=assets/").replace('"assets/', '"/gradio_api/file=assets/')
|
| 347 |
fix = ('<script>document.addEventListener("click",function(e){var a=e.target.closest&&e.target.closest("a");'
|
| 348 |
'if(!a)return;var h=a.getAttribute("href")||"";if(h.charAt(0)==="#"){e.preventDefault();'
|
|
@@ -438,7 +440,7 @@ class Game:
|
|
| 438 |
return FC.controls(on, rng)[kind]
|
| 439 |
|
| 440 |
def judge_pair(self, a, b, ref):
|
| 441 |
-
"""P(the free judge picks a), both scaled by the on render's
|
| 442 |
if self.judge is None:
|
| 443 |
self.judge = FC.LocalJudge(str(WEIGHTS))
|
| 444 |
p, _ = self.judge.prefer([FC.to_input(a, ref)], [FC.to_input(b, ref)])
|
|
@@ -513,7 +515,7 @@ texture: it never reads or finds text.
|
|
| 513 |
<h3 id="read">2. Read the result</h3>
|
| 514 |
|
| 515 |
- **The map** is the segment as traced, flattened on its own mesh grid. Yellow and green: clear fibers. Purple: weak.
|
| 516 |
-
|
| 517 |
- **Fibers show on** is the share of patches with evidence 0.75 or more. **Median evidence** is the typical patch.
|
| 518 |
- **Self-check**: on 40 of your patches, the judge must prefer the real render over a noised, a blurred and a
|
| 519 |
scrambled copy at least 80% of the time. If it fails, do not trust that run.
|
|
@@ -643,9 +645,9 @@ CSS = """
|
|
| 643 |
.fc-untested { font-size:13.5px; margin-top:8px; } .fc-untested summary { cursor:pointer; }
|
| 644 |
.fc-legend { font-size:12.5px; color: var(--body-text-color-subdued); }
|
| 645 |
.fc-legend i { display:inline-block; width:11px; height:11px; border-radius:3px; margin:0 4px -1px 10px; }
|
| 646 |
-
|
| 647 |
-
.fc-
|
| 648 |
-
|
| 649 |
.fc-howto img { border-radius:10px; border:1px solid var(--border-color-primary); max-width:min(100%, 820px);
|
| 650 |
display:block; margin:10px 0 4px; }
|
| 651 |
.fc-howto figure { margin:10px 0 18px; } .fc-howto figcaption { font-size:13px; color: var(--body-text-color-subdued); }
|
|
@@ -666,12 +668,17 @@ CSS = """
|
|
| 666 |
.fc-credit { text-align:center; font-size:13px; color: var(--body-text-color-subdued); padding:18px 0 6px; }
|
| 667 |
.fc-credit a { color: var(--fc-accent) !important; }
|
| 668 |
#fc-dl .empty { min-height: 64px !important; height: 64px !important; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 669 |
@media (max-width: 640px) {
|
| 670 |
.fc-head h1 { font-size:24px; }
|
| 671 |
.fc-head span { display:none; }
|
| 672 |
#fc-tabs .tab-container button { padding:6px 6px !important; font-size:12.5px !important; }
|
| 673 |
-
.fc-guide iframe { height:calc(100vh - 120px); min-height:520px; }
|
| 674 |
.fc-strip { grid-template-columns:repeat(2,minmax(0,1fr)); }
|
|
|
|
| 675 |
}
|
| 676 |
"""
|
| 677 |
|
|
@@ -688,6 +695,7 @@ model or by Claude with your own key. Model output, not a reading.">
|
|
| 688 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 689 |
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,650&display=swap" rel="stylesheet">
|
| 690 |
"""
|
|
|
|
| 691 |
|
| 692 |
TAB_JS = """
|
| 693 |
() => {
|
|
@@ -699,7 +707,13 @@ TAB_JS = """
|
|
| 699 |
const btn = btns.find((b) => b.getAttribute('role') === 'tab') || btns[btns.length - 1];
|
| 700 |
if (btn) btn.click();
|
| 701 |
};
|
| 702 |
-
window.addEventListener('message', (e) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 703 |
const q = new URLSearchParams(window.location.search).get('tab');
|
| 704 |
if (q) setTimeout(() => pick(q), 300);
|
| 705 |
}
|
|
@@ -854,13 +868,13 @@ def build_demo():
|
|
| 854 |
summary = gr.HTML("<p class='fc-meta'>Your map and summary appear here. 60 patches take "
|
| 855 |
"about two minutes; try <b>Fiber or not?</b> while you wait.</p>")
|
| 856 |
tiles = gr.Gallery(label="Clearest and weakest patches (the renders the judge saw)",
|
| 857 |
-
columns=6,
|
| 858 |
dl = gr.File(label="Download: fiber.json, fiber.tif, fiber_map.png, report.html, tiles/",
|
| 859 |
-
|
| 860 |
with gr.Accordion("Full report (report.html)", open=False):
|
| 861 |
report = gr.HTML()
|
| 862 |
with gr.Accordion("Record (fiber.json)", open=False):
|
| 863 |
-
record = gr.JSON()
|
| 864 |
with gr.Accordion("About the judges", open=False):
|
| 865 |
gr.Markdown(ABOUT_JUDGE.format(sha=WEIGHTS_SHA256[:16] + "…", cp=CLAUDE_PATCHES))
|
| 866 |
with gr.Tab("Fiber or not?", id="game") as game_tab:
|
|
@@ -886,7 +900,7 @@ def build_demo():
|
|
| 886 |
cgo = gr.Button("Compare", variant="primary")
|
| 887 |
cside = gr.HTML()
|
| 888 |
with gr.Accordion("Record (JSON)", open=False):
|
| 889 |
-
cout = gr.JSON(label="Side by side")
|
| 890 |
with gr.Tab("How-to guides", id="howto"):
|
| 891 |
gr.Markdown(with_figures(HOW_TO), elem_classes="fc-howto")
|
| 892 |
with gr.Tab("Agents and API", id="agents"):
|
|
@@ -911,19 +925,22 @@ def build_demo():
|
|
| 911 |
segment.input(on_segment, [sample, segment], volume, **hidden)
|
| 912 |
judge.change(on_judge, judge, claude_box, **hidden)
|
| 913 |
outs = [pic, summary, tiles, dl, report, record]
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
|
|
|
|
|
|
|
|
|
|
| 919 |
|
| 920 |
def first_pair(s):
|
| 921 |
if (s or {}).get("i") is not None or not game.ok:
|
| 922 |
return gr.update(), gr.update(), gr.update(), gr.update(), s
|
| 923 |
return new_pair(s)
|
| 924 |
-
game_tab.select(guard(first_pair), st, [img_a, img_b, reveal, score_md, st], **
|
| 925 |
-
pick_a.click(lambda s: answer("A", s), st, [reveal, score_md, st], **hidden)
|
| 926 |
-
pick_b.click(lambda s: answer("B", s), st, [reveal, score_md, st], **hidden)
|
| 927 |
def on_compare(a, b):
|
| 928 |
res = compare_results(a, b)
|
| 929 |
return compare_html(res), res
|
|
|
|
| 39 |
GAME = HERE / "assets" / "game.npz"
|
| 40 |
GUIDE = HERE / "guide" / "index.html"
|
| 41 |
CONNECT_HTML = HERE / "connect.html"
|
| 42 |
+
ONESCROLL = HERE / "onescroll.html" # one page scroll bar: frames fit their content (kit spaceconnect render)
|
| 43 |
PATCHES = [("Quick: 60 patches, about 2 minutes", 60), ("Thorough: 150 patches, about 5 minutes", 150),
|
| 44 |
("Whole segment: up to 400 patches, about 15 minutes", 400)]
|
| 45 |
CLAUDE_PATCHES, CLAUDE_CHECK_N = 40, 10
|
|
|
|
| 342 |
if not GUIDE.is_file():
|
| 343 |
return "<p>The welcome page is missing from this copy.</p>"
|
| 344 |
page = GUIDE.read_text()
|
| 345 |
+
# its pictures point at Gradio's file route (a <base> cannot do it); in-page anchors scroll the page to their
|
| 346 |
+
# section (the frame fits its content, onescroll.html), ?tab= links switch the Space's tabs, other links open a
|
| 347 |
+
# new tab (as Lasagna Fields does)
|
| 348 |
page = page.replace("'assets/", "'/gradio_api/file=assets/").replace('"assets/', '"/gradio_api/file=assets/')
|
| 349 |
fix = ('<script>document.addEventListener("click",function(e){var a=e.target.closest&&e.target.closest("a");'
|
| 350 |
'if(!a)return;var h=a.getAttribute("href")||"";if(h.charAt(0)==="#"){e.preventDefault();'
|
|
|
|
| 440 |
return FC.controls(on, rng)[kind]
|
| 441 |
|
| 442 |
def judge_pair(self, a, b, ref):
|
| 443 |
+
"""P(the free judge picks a), both scaled by the on render's gray scale as in the checker."""
|
| 444 |
if self.judge is None:
|
| 445 |
self.judge = FC.LocalJudge(str(WEIGHTS))
|
| 446 |
p, _ = self.judge.prefer([FC.to_input(a, ref)], [FC.to_input(b, ref)])
|
|
|
|
| 515 |
<h3 id="read">2. Read the result</h3>
|
| 516 |
|
| 517 |
- **The map** is the segment as traced, flattened on its own mesh grid. Yellow and green: clear fibers. Purple: weak.
|
| 518 |
+
Gray: no sign of fibers. Slate: traced but not scored (holes nearby, masked CT, or not sampled). Black: no surface.
|
| 519 |
- **Fibers show on** is the share of patches with evidence 0.75 or more. **Median evidence** is the typical patch.
|
| 520 |
- **Self-check**: on 40 of your patches, the judge must prefer the real render over a noised, a blurred and a
|
| 521 |
scrambled copy at least 80% of the time. If it fails, do not trust that run.
|
|
|
|
| 645 |
.fc-untested { font-size:13.5px; margin-top:8px; } .fc-untested summary { cursor:pointer; }
|
| 646 |
.fc-legend { font-size:12.5px; color: var(--body-text-color-subdued); }
|
| 647 |
.fc-legend i { display:inline-block; width:11px; height:11px; border-radius:3px; margin:0 4px -1px 10px; }
|
| 648 |
+
/* no fixed heights on the frames: onescroll.html fits each to its content, so only the page scrolls */
|
| 649 |
+
.fc-report { width:100%; border:0; border-radius:12px; }
|
| 650 |
+
.fc-guide iframe { width:100%; border:0; border-radius:12px; background:#f6f1e7; }
|
| 651 |
.fc-howto img { border-radius:10px; border:1px solid var(--border-color-primary); max-width:min(100%, 820px);
|
| 652 |
display:block; margin:10px 0 4px; }
|
| 653 |
.fc-howto figure { margin:10px 0 18px; } .fc-howto figcaption { font-size:13px; color: var(--body-text-color-subdued); }
|
|
|
|
| 668 |
.fc-credit { text-align:center; font-size:13px; color: var(--body-text-color-subdued); padding:18px 0 6px; }
|
| 669 |
.fc-credit a { color: var(--fc-accent) !important; }
|
| 670 |
#fc-dl .empty { min-height: 64px !important; height: 64px !important; }
|
| 671 |
+
/* the twelve patches grow the gallery instead of scrolling inside it (Gradio's grid-wrap always has a scroll bar) */
|
| 672 |
+
#fc-tiles .grid-wrap { height:auto !important; min-height:0 !important; max-height:none !important;
|
| 673 |
+
overflow-y:visible !important; }
|
| 674 |
+
#fc-tiles .grid-container { grid-template-columns:repeat(6,minmax(0,1fr)) !important; grid-template-rows:none !important;
|
| 675 |
+
grid-auto-rows:auto !important; }
|
| 676 |
@media (max-width: 640px) {
|
| 677 |
.fc-head h1 { font-size:24px; }
|
| 678 |
.fc-head span { display:none; }
|
| 679 |
#fc-tabs .tab-container button { padding:6px 6px !important; font-size:12.5px !important; }
|
|
|
|
| 680 |
.fc-strip { grid-template-columns:repeat(2,minmax(0,1fr)); }
|
| 681 |
+
#fc-tiles .grid-container { grid-template-columns:repeat(3,minmax(0,1fr)) !important; }
|
| 682 |
}
|
| 683 |
"""
|
| 684 |
|
|
|
|
| 695 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 696 |
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,650&display=swap" rel="stylesheet">
|
| 697 |
"""
|
| 698 |
+
HEAD += ONESCROLL.read_text() if ONESCROLL.is_file() else ""
|
| 699 |
|
| 700 |
TAB_JS = """
|
| 701 |
() => {
|
|
|
|
| 707 |
const btn = btns.find((b) => b.getAttribute('role') === 'tab') || btns[btns.length - 1];
|
| 708 |
if (btn) btn.click();
|
| 709 |
};
|
| 710 |
+
window.addEventListener('message', (e) => {
|
| 711 |
+
if (!(e.data && e.data.fcTab)) return;
|
| 712 |
+
pick(e.data.fcTab);
|
| 713 |
+
// the welcome page grows the page (one scroll bar), so a link from it lands at the top of the new tab
|
| 714 |
+
const bar = document.getElementById('fc-tabs');
|
| 715 |
+
if (bar && bar.getBoundingClientRect().top < 0) bar.scrollIntoView({block: 'start'});
|
| 716 |
+
});
|
| 717 |
const q = new URLSearchParams(window.location.search).get('tab');
|
| 718 |
if (q) setTimeout(() => pick(q), 300);
|
| 719 |
}
|
|
|
|
| 868 |
summary = gr.HTML("<p class='fc-meta'>Your map and summary appear here. 60 patches take "
|
| 869 |
"about two minutes; try <b>Fiber or not?</b> while you wait.</p>")
|
| 870 |
tiles = gr.Gallery(label="Clearest and weakest patches (the renders the judge saw)",
|
| 871 |
+
columns=6, object_fit="contain", allow_preview=True, elem_id="fc-tiles")
|
| 872 |
dl = gr.File(label="Download: fiber.json, fiber.tif, fiber_map.png, report.html, tiles/",
|
| 873 |
+
elem_id="fc-dl")
|
| 874 |
with gr.Accordion("Full report (report.html)", open=False):
|
| 875 |
report = gr.HTML()
|
| 876 |
with gr.Accordion("Record (fiber.json)", open=False):
|
| 877 |
+
record = gr.JSON(max_height=None) # grows with the record: the page scrolls
|
| 878 |
with gr.Accordion("About the judges", open=False):
|
| 879 |
gr.Markdown(ABOUT_JUDGE.format(sha=WEIGHTS_SHA256[:16] + "…", cp=CLAUDE_PATCHES))
|
| 880 |
with gr.Tab("Fiber or not?", id="game") as game_tab:
|
|
|
|
| 900 |
cgo = gr.Button("Compare", variant="primary")
|
| 901 |
cside = gr.HTML()
|
| 902 |
with gr.Accordion("Record (JSON)", open=False):
|
| 903 |
+
cout = gr.JSON(label="Side by side", max_height=None)
|
| 904 |
with gr.Tab("How-to guides", id="howto"):
|
| 905 |
gr.Markdown(with_figures(HOW_TO), elem_classes="fc-howto")
|
| 906 |
with gr.Tab("Agents and API", id="agents"):
|
|
|
|
| 925 |
segment.input(on_segment, [sample, segment], volume, **hidden)
|
| 926 |
judge.change(on_judge, judge, claude_box, **hidden)
|
| 927 |
outs = [pic, summary, tiles, dl, report, record]
|
| 928 |
+
# progress on the visible outputs only: in the folded report and record it would overflow their boxes
|
| 929 |
+
run = dict(concurrency_id="check", concurrency_limit=2, show_progress_on=[pic, summary, tiles, dl], **hidden)
|
| 930 |
+
go.click(guard(on_check), [sample, segment, volume, patches, judge, key, max_usd], outs, **run)
|
| 931 |
+
go_up.click(guard(on_upload), [upload, up_vol, patches, judge, key, max_usd], outs, **run)
|
| 932 |
+
# the progress shows on the two pictures: in the one-line score and the reveal it would not fit, and they
|
| 933 |
+
# would scroll inside the page while the judge works
|
| 934 |
+
pair_progress = dict(show_progress_on=[img_a, img_b], **hidden)
|
| 935 |
+
nxt.click(guard(new_pair), st, [img_a, img_b, reveal, score_md, st], **pair_progress)
|
| 936 |
|
| 937 |
def first_pair(s):
|
| 938 |
if (s or {}).get("i") is not None or not game.ok:
|
| 939 |
return gr.update(), gr.update(), gr.update(), gr.update(), s
|
| 940 |
return new_pair(s)
|
| 941 |
+
game_tab.select(guard(first_pair), st, [img_a, img_b, reveal, score_md, st], **pair_progress)
|
| 942 |
+
pick_a.click(lambda s: answer("A", s), st, [reveal, score_md, st], show_progress="hidden", **hidden)
|
| 943 |
+
pick_b.click(lambda s: answer("B", s), st, [reveal, score_md, st], show_progress="hidden", **hidden)
|
| 944 |
def on_compare(a, b):
|
| 945 |
res = compare_results(a, b)
|
| 946 |
return compare_html(res), res
|
fcmodel.py
CHANGED
|
@@ -148,7 +148,7 @@ def train(patches, epochs, lr, batch=32, seed=SEED, say=print, negatives=("phase
|
|
| 148 |
|
| 149 |
|
| 150 |
def scores(m, paths):
|
| 151 |
-
"""Fiber score of each image: the mean over the
|
| 152 |
import torch
|
| 153 |
out = {}
|
| 154 |
with torch.no_grad():
|
|
|
|
| 148 |
|
| 149 |
|
| 150 |
def scores(m, paths):
|
| 151 |
+
"""Fiber score of each image: the mean over the center crop and its four quarter turns."""
|
| 152 |
import torch
|
| 153 |
out = {}
|
| 154 |
with torch.no_grad():
|
fibercheck.py
CHANGED
|
@@ -46,7 +46,7 @@ def read_tifxyz(folder):
|
|
| 46 |
|
| 47 |
|
| 48 |
def normals(xyz, valid):
|
| 49 |
-
"""Unit normal per vertex from central differences; ok where it and its four
|
| 50 |
du = np.zeros_like(xyz)
|
| 51 |
dv = np.zeros_like(xyz)
|
| 52 |
du[:, 1:-1] = xyz[:, 2:] - xyz[:, :-2]
|
|
@@ -200,7 +200,7 @@ def render_patches(folder, ct, level, P, K, stride=None, max_patches=None, maske
|
|
| 200 |
|
| 201 |
|
| 202 |
def to_input(img, ref):
|
| 203 |
-
"""The frozen PNG
|
| 204 |
lo, hi = np.percentile(ref, [1, 99])
|
| 205 |
a = np.clip((img - lo) / max(hi - lo, 1e-6), 0, 1)
|
| 206 |
a = np.round(a * 255) / 255
|
|
@@ -327,7 +327,7 @@ def fiber_direction(img):
|
|
| 327 |
|
| 328 |
|
| 329 |
def jump_hints(patches, grid_step):
|
| 330 |
-
"""Patches whose evidence is under 0.5 while every scored
|
| 331 |
or a stretch off the sheet. Untested idea."""
|
| 332 |
at = {(p["r"], p["c"]): p["evidence"] for p in patches}
|
| 333 |
out = []
|
|
@@ -421,7 +421,7 @@ def write_maps(rec, out, valid=None):
|
|
| 421 |
for p in rec["patches"]:
|
| 422 |
m[p["r"]:p["r"] + P, p["c"]:p["c"] + P] = p["evidence"]
|
| 423 |
tifffile.imwrite(out / "fiber.tif", m, compression="zlib")
|
| 424 |
-
rgb =
|
| 425 |
if valid is not None:
|
| 426 |
rgb[np.isnan(m) & valid] = (52, 54, 64) # traced but not scored: the segment's shape
|
| 427 |
im = Image.fromarray(rgb)
|
|
@@ -429,8 +429,8 @@ def write_maps(rec, out, valid=None):
|
|
| 429 |
im.resize((W * s, H * s), Image.NEAREST).save(out / "fiber_map.png")
|
| 430 |
|
| 431 |
|
| 432 |
-
def
|
| 433 |
-
"""Evidence 0.5 to 1 on a
|
| 434 |
stops = np.array([[68, 1, 84], [59, 82, 139], [33, 145, 140], [94, 201, 98], [253, 231, 37]], float)
|
| 435 |
v = np.clip((np.nan_to_num(m, nan=0) - 0.5) / 0.5, 0, 1)
|
| 436 |
pos = v * (len(stops) - 1)
|
|
@@ -443,7 +443,7 @@ def colour(m):
|
|
| 443 |
|
| 444 |
|
| 445 |
def _img_png(a, save=None):
|
| 446 |
-
"""PNG bytes of a render (1 and 99 percentile
|
| 447 |
from PIL import Image
|
| 448 |
b = np.clip((a - np.percentile(a, 1)) / max(np.percentile(a, 99) - np.percentile(a, 1), 1e-6), 0, 1)
|
| 449 |
im = Image.fromarray((b * 255).astype(np.uint8)).resize((a.shape[1] * 2, a.shape[0] * 2), Image.BILINEAR)
|
|
@@ -503,7 +503,7 @@ figure img{{width:100%;border-radius:6px}} figcaption{{font-size:13px;color:var(
|
|
| 503 |
<div class="card"><div class="big {'ok' if ck['passed'] else 'bad'}">{'passed' if ck['passed'] else 'failed'}</div>self-check: noise {ck['against']['noise']:.0%}, blur {ck['against']['blur']:.0%}, scramble {ck['against']['scramble']:.0%}</div>
|
| 504 |
</div>
|
| 505 |
<h2>Fiber map</h2><div class="map"><img src="data:image/png;base64,{mp}" alt="fiber evidence over the segment">
|
| 506 |
-
<div class="sub">Yellow: clear fiber. Purple: weak.
|
| 507 |
<h2>Clearest and weakest patches</h2><div class="grid">{''.join(tiles)}</div>
|
| 508 |
<h2>Method</h2><p>The Progress Prize asks entrants to "show visually that papyrus fibers are visible on your output surface"
|
| 509 |
(scrollprize.org/prizes). For each patch, the CT is rendered along the surface and the judge compares that render with
|
|
|
|
| 46 |
|
| 47 |
|
| 48 |
def normals(xyz, valid):
|
| 49 |
+
"""Unit normal per vertex from central differences; ok where it and its four neighbors are valid."""
|
| 50 |
du = np.zeros_like(xyz)
|
| 51 |
dv = np.zeros_like(xyz)
|
| 52 |
du[:, 1:-1] = xyz[:, 2:] - xyz[:, :-2]
|
|
|
|
| 200 |
|
| 201 |
|
| 202 |
def to_input(img, ref):
|
| 203 |
+
"""The frozen PNG gray scale (1 and 99 percentiles of the on render), then z-scored, as the model was trained."""
|
| 204 |
lo, hi = np.percentile(ref, [1, 99])
|
| 205 |
a = np.clip((img - lo) / max(hi - lo, 1e-6), 0, 1)
|
| 206 |
a = np.round(a * 255) / 255
|
|
|
|
| 327 |
|
| 328 |
|
| 329 |
def jump_hints(patches, grid_step):
|
| 330 |
+
"""Patches whose evidence is under 0.5 while every scored neighbor is over 0.8: a drop that may mark a sheet jump
|
| 331 |
or a stretch off the sheet. Untested idea."""
|
| 332 |
at = {(p["r"], p["c"]): p["evidence"] for p in patches}
|
| 333 |
out = []
|
|
|
|
| 421 |
for p in rec["patches"]:
|
| 422 |
m[p["r"]:p["r"] + P, p["c"]:p["c"] + P] = p["evidence"]
|
| 423 |
tifffile.imwrite(out / "fiber.tif", m, compression="zlib")
|
| 424 |
+
rgb = color(m)
|
| 425 |
if valid is not None:
|
| 426 |
rgb[np.isnan(m) & valid] = (52, 54, 64) # traced but not scored: the segment's shape
|
| 427 |
im = Image.fromarray(rgb)
|
|
|
|
| 429 |
im.resize((W * s, H * s), Image.NEAREST).save(out / "fiber_map.png")
|
| 430 |
|
| 431 |
|
| 432 |
+
def color(m):
|
| 433 |
+
"""Evidence 0.5 to 1 on a color-blind-safe scale (gray below 0.5 means no sign of fiber), holes transparent-dark."""
|
| 434 |
stops = np.array([[68, 1, 84], [59, 82, 139], [33, 145, 140], [94, 201, 98], [253, 231, 37]], float)
|
| 435 |
v = np.clip((np.nan_to_num(m, nan=0) - 0.5) / 0.5, 0, 1)
|
| 436 |
pos = v * (len(stops) - 1)
|
|
|
|
| 443 |
|
| 444 |
|
| 445 |
def _img_png(a, save=None):
|
| 446 |
+
"""PNG bytes of a render (1 and 99 percentile gray scale, drawn 2 pixels per voxel); also written to `save`."""
|
| 447 |
from PIL import Image
|
| 448 |
b = np.clip((a - np.percentile(a, 1)) / max(np.percentile(a, 99) - np.percentile(a, 1), 1e-6), 0, 1)
|
| 449 |
im = Image.fromarray((b * 255).astype(np.uint8)).resize((a.shape[1] * 2, a.shape[0] * 2), Image.BILINEAR)
|
|
|
|
| 503 |
<div class="card"><div class="big {'ok' if ck['passed'] else 'bad'}">{'passed' if ck['passed'] else 'failed'}</div>self-check: noise {ck['against']['noise']:.0%}, blur {ck['against']['blur']:.0%}, scramble {ck['against']['scramble']:.0%}</div>
|
| 504 |
</div>
|
| 505 |
<h2>Fiber map</h2><div class="map"><img src="data:image/png;base64,{mp}" alt="fiber evidence over the segment">
|
| 506 |
+
<div class="sub">Yellow: clear fiber. Purple: weak. Gray: no sign of fiber. Slate: traced but not scored. Black: no surface.</div></div>
|
| 507 |
<h2>Clearest and weakest patches</h2><div class="grid">{''.join(tiles)}</div>
|
| 508 |
<h2>Method</h2><p>The Progress Prize asks entrants to "show visually that papyrus fibers are visible on your output surface"
|
| 509 |
(scrollprize.org/prizes). For each patch, the CT is rendered along the surface and the judge compares that render with
|
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 -->
|