SharpMeow commited on
Commit
eadab8d
·
verified ·
1 Parent(s): b24a488

Open one wheel step farther out

Browse files
Files changed (10) hide show
  1. LICENSE +25 -202
  2. README.md +7 -55
  3. app.js +0 -0
  4. data/gpu.json +12 -353
  5. index.html +27 -175
  6. og.jpg +2 -2
  7. style.css +0 -117
  8. tools/build.py +23 -120
  9. tools/fetch.py +11 -30
  10. tools/gpu_layer.py +5 -5
LICENSE CHANGED
@@ -1,202 +1,25 @@
1
-
2
- Apache License
3
- Version 2.0, January 2004
4
- http://www.apache.org/licenses/
5
-
6
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
-
8
- 1. Definitions.
9
-
10
- "License" shall mean the terms and conditions for use, reproduction,
11
- and distribution as defined by Sections 1 through 9 of this document.
12
-
13
- "Licensor" shall mean the copyright owner or entity authorized by
14
- the copyright owner that is granting the License.
15
-
16
- "Legal Entity" shall mean the union of the acting entity and all
17
- other entities that control, are controlled by, or are under common
18
- control with that entity. For the purposes of this definition,
19
- "control" means (i) the power, direct or indirect, to cause the
20
- direction or management of such entity, whether by contract or
21
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
- outstanding shares, or (iii) beneficial ownership of such entity.
23
-
24
- "You" (or "Your") shall mean an individual or Legal Entity
25
- exercising permissions granted by this License.
26
-
27
- "Source" form shall mean the preferred form for making modifications,
28
- including but not limited to software source code, documentation
29
- source, and configuration files.
30
-
31
- "Object" form shall mean any form resulting from mechanical
32
- transformation or translation of a Source form, including but
33
- not limited to compiled object code, generated documentation,
34
- and conversions to other media types.
35
-
36
- "Work" shall mean the work of authorship, whether in Source or
37
- Object form, made available under the License, as indicated by a
38
- copyright notice that is included in or attached to the work
39
- (an example is provided in the Appendix below).
40
-
41
- "Derivative Works" shall mean any work, whether in Source or Object
42
- form, that is based on (or derived from) the Work and for which the
43
- editorial revisions, annotations, elaborations, or other modifications
44
- represent, as a whole, an original work of authorship. For the purposes
45
- of this License, Derivative Works shall not include works that remain
46
- separable from, or merely link (or bind by name) to the interfaces of,
47
- the Work and Derivative Works thereof.
48
-
49
- "Contribution" shall mean any work of authorship, including
50
- the original version of the Work and any modifications or additions
51
- to that Work or Derivative Works thereof, that is intentionally
52
- submitted to Licensor for inclusion in the Work by the copyright owner
53
- or by an individual or Legal Entity authorized to submit on behalf of
54
- the copyright owner. For the purposes of this definition, "submitted"
55
- means any form of electronic, verbal, or written communication sent
56
- to the Licensor or its representatives, including but not limited to
57
- communication on electronic mailing lists, source code control systems,
58
- and issue tracking systems that are managed by, or on behalf of, the
59
- Licensor for the purpose of discussing and improving the Work, but
60
- excluding communication that is conspicuously marked or otherwise
61
- designated in writing by the copyright owner as "Not a Contribution."
62
-
63
- "Contributor" shall mean Licensor and any individual or Legal Entity
64
- on behalf of whom a Contribution has been received by Licensor and
65
- subsequently incorporated within the Work.
66
-
67
- 2. Grant of Copyright License. Subject to the terms and conditions of
68
- this License, each Contributor hereby grants to You a perpetual,
69
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
- copyright license to reproduce, prepare Derivative Works of,
71
- publicly display, publicly perform, sublicense, and distribute the
72
- Work and such Derivative Works in Source or Object form.
73
-
74
- 3. Grant of Patent License. Subject to the terms and conditions of
75
- this License, each Contributor hereby grants to You a perpetual,
76
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
- (except as stated in this section) patent license to make, have made,
78
- use, offer to sell, sell, import, and otherwise transfer the Work,
79
- where such license applies only to those patent claims licensable
80
- by such Contributor that are necessarily infringed by their
81
- Contribution(s) alone or by combination of their Contribution(s)
82
- with the Work to which such Contribution(s) was submitted. If You
83
- institute patent litigation against any entity (including a
84
- cross-claim or counterclaim in a lawsuit) alleging that the Work
85
- or a Contribution incorporated within the Work constitutes direct
86
- or contributory patent infringement, then any patent licenses
87
- granted to You under this License for that Work shall terminate
88
- as of the date such litigation is filed.
89
-
90
- 4. Redistribution. You may reproduce and distribute copies of the
91
- Work or Derivative Works thereof in any medium, with or without
92
- modifications, and in Source or Object form, provided that You
93
- meet the following conditions:
94
-
95
- (a) You must give any other recipients of the Work or
96
- Derivative Works a copy of this License; and
97
-
98
- (b) You must cause any modified files to carry prominent notices
99
- stating that You changed the files; and
100
-
101
- (c) You must retain, in the Source form of any Derivative Works
102
- that You distribute, all copyright, patent, trademark, and
103
- attribution notices from the Source form of the Work,
104
- excluding those notices that do not pertain to any part of
105
- the Derivative Works; and
106
-
107
- (d) If the Work includes a "NOTICE" text file as part of its
108
- distribution, then any Derivative Works that You distribute must
109
- include a readable copy of the attribution notices contained
110
- within such NOTICE file, excluding those notices that do not
111
- pertain to any part of the Derivative Works, in at least one
112
- of the following places: within a NOTICE text file distributed
113
- as part of the Derivative Works; within the Source form or
114
- documentation, if provided along with the Derivative Works; or,
115
- within a display generated by the Derivative Works, if and
116
- wherever such third-party notices normally appear. The contents
117
- of the NOTICE file are for informational purposes only and
118
- do not modify the License. You may add Your own attribution
119
- notices within Derivative Works that You distribute, alongside
120
- or as an addendum to the NOTICE text from the Work, provided
121
- that such additional attribution notices cannot be construed
122
- as modifying the License.
123
-
124
- You may add Your own copyright statement to Your modifications and
125
- may provide additional or different license terms and conditions
126
- for use, reproduction, or distribution of Your modifications, or
127
- for any such Derivative Works as a whole, provided Your use,
128
- reproduction, and distribution of the Work otherwise complies with
129
- the conditions stated in this License.
130
-
131
- 5. Submission of Contributions. Unless You explicitly state otherwise,
132
- any Contribution intentionally submitted for inclusion in the Work
133
- by You to the Licensor shall be under the terms and conditions of
134
- this License, without any additional terms or conditions.
135
- Notwithstanding the above, nothing herein shall supersede or modify
136
- the terms of any separate license agreement you may have executed
137
- with Licensor regarding such Contributions.
138
-
139
- 6. Trademarks. This License does not grant permission to use the trade
140
- names, trademarks, service marks, or product names of the Licensor,
141
- except as required for reasonable and customary use in describing the
142
- origin of the Work and reproducing the content of the NOTICE file.
143
-
144
- 7. Disclaimer of Warranty. Unless required by applicable law or
145
- agreed to in writing, Licensor provides the Work (and each
146
- Contributor provides its Contributions) on an "AS IS" BASIS,
147
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
- implied, including, without limitation, any warranties or conditions
149
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
- PARTICULAR PURPOSE. You are solely responsible for determining the
151
- appropriateness of using or redistributing the Work and assume any
152
- risks associated with Your exercise of permissions under this License.
153
-
154
- 8. Limitation of Liability. In no event and under no legal theory,
155
- whether in tort (including negligence), contract, or otherwise,
156
- unless required by applicable law (such as deliberate and grossly
157
- negligent acts) or agreed to in writing, shall any Contributor be
158
- liable to You for damages, including any direct, indirect, special,
159
- incidental, or consequential damages of any character arising as a
160
- result of this License or out of the use or inability to use the
161
- Work (including but not limited to damages for loss of goodwill,
162
- work stoppage, computer failure or malfunction, or any and all
163
- other commercial damages or losses), even if such Contributor
164
- has been advised of the possibility of such damages.
165
-
166
- 9. Accepting Warranty or Additional Liability. While redistributing
167
- the Work or Derivative Works thereof, You may choose to offer,
168
- and charge a fee for, acceptance of support, warranty, indemnity,
169
- or other liability obligations and/or rights consistent with this
170
- License. However, in accepting such obligations, You may act only
171
- on Your own behalf and on Your sole responsibility, not on behalf
172
- of any other Contributor, and only if You agree to indemnify,
173
- defend, and hold each Contributor harmless for any liability
174
- incurred by, or claims asserted against, such Contributor by reason
175
- of your accepting any such warranty or additional liability.
176
-
177
- END OF TERMS AND CONDITIONS
178
-
179
- APPENDIX: How to apply the Apache License to your work.
180
-
181
- To apply the Apache License to your work, attach the following
182
- boilerplate notice, with the fields enclosed by brackets "[]"
183
- replaced with your own identifying information. (Don't include
184
- the brackets!) The text should be enclosed in the appropriate
185
- comment syntax for the file format. We also recommend that a
186
- file or class name and description of purpose be included on the
187
- same "printed page" as the copyright notice for easier
188
- identification within third-party archives.
189
-
190
- Copyright [yyyy] [name of copyright owner]
191
-
192
- Licensed under the Apache License, Version 2.0 (the "License");
193
- you may not use this file except in compliance with the License.
194
- You may obtain a copy of the License at
195
-
196
- http://www.apache.org/licenses/LICENSE-2.0
197
-
198
- Unless required by applicable law or agreed to in writing, software
199
- distributed under the License is distributed on an "AS IS" BASIS,
200
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
- See the License for the specific language governing permissions and
202
- limitations under the License.
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SharpMeow
4
+
5
+ This licence covers the code in this repository: index.html, style.css, app.js and tools/.
6
+ The files in data/ and og.jpg are derived from Vesuvius Challenge open data and are licensed
7
+ CC BY-NC 4.0 (https://creativecommons.org/licenses/by-nc/4.0/); see README.md.
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -6,7 +6,7 @@ colorTo: gray
6
  sdk: static
7
  app_file: index.html
8
  pinned: false
9
- license: apache-2.0
10
  short_description: Unroll a carbonized Herculaneum scroll in your browser
11
  thumbnail: https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/og.jpg
12
  tags:
@@ -21,73 +21,25 @@ models:
21
 
22
  # Unroll
23
 
24
- PHerc. 139 is a papyrus roll carbonized when Vesuvius buried Herculaneum in 79 CE. It cannot be opened, so it was scanned with X-rays at the ESRF synchrotron. This page rebuilds 37 of its windings in 3D from the public Vesuvius Challenge meshes and lets you unroll them in the browser. Ink layers show where models detect ink; letters visible here are candidates for expert reading.
25
 
26
  - **Shapes**: the 9.362 µm tifxyz meshes of the 37 traced windings (`w023` to `w059`), decimated to 579,450 vertices.
27
  - **X-ray layer**: the CT intensity on each surface, from the public 9.362 µm surface volumes.
28
- - **Published ink (2.4 µm), amber**: the Vesuvius Challenge's public ink detection on the 2.4 µm scan. In 3D it is drawn through a local-contrast filter so letter-sized strokes stand out. The close-up view shows the published preview without filtering, downscaled to 2,400 px on its long side and re-saved as JPEG.
29
- - **ink_9um (9 µm), cyan**: inked by running the public [`scrollprize/ink_9um`](https://huggingface.co/scrollprize/ink_9um) model (seed 42, step 75000) on the 9.4 µm scan (9.362 µm voxels). The ink_9um model was trained by the Vesuvius Challenge and run by Chase Hendrick, on Hugging Face ZeroGPU and Modal, over all 37 windings (`w023` to `w059`). The first batch was 37,141 patches in 57 s of measured GPU time. The page reads the covered windings from the manifest, so its text updates as windings are added. The 9.4 µm scan (9.362 µm voxels) is about four times coarser than the 2.4 µm one, so this layer is noisier. `w047` and `w050` ran with patch overlap 0.5; others ran with 0.25 or 0.2, and their 128 px patch grid shows as faint square tiles. Per-run records (checkpoint sha256, villa commit, requested and measured GPU seconds) are in `data/gpu/receipts/`; `data/gpu.json` is the summary the page reads.
30
 
31
  Unrolling lays the windings end to end, outermost first. Each traced winding runs a little past one full turn, so roughly a tenth of the 2.8 m strip repeats surface from its neighbour.
32
 
33
- ## Release
34
-
35
- Unroll is a released tool: an interactive 3D viewer that virtually unrolls Herculaneum scroll scans with ink layers, an Ink Bench for comparing ink models, and an agent API.
36
-
37
- - Status: released, v1.0.0.
38
- - Version: 1.1.0, which adds Install app (`version.json`, [CHANGELOG.md](CHANGELOG.md)). The page shows it in the About panel and `window.unroll.version()` returns it.
39
- - Licence: Apache 2.0 for all code (`LICENSE`, `NOTICE`). The scan data in `data/` and `og.jpg` are CC BY-NC 4.0. three.js keeps its own licence (see `NOTICE`).
40
- - Install app: open [the app page](https://sharpmeow-unroll.static.hf.space/) and press **Install app** (View panel or Settings), or use the browser's install icon. It works offline after the first visit; see [Install app](#install-app).
41
- - Listed in the public repository: [ChaseHendrick/Scrolls](https://github.com/ChaseHendrick/Scrolls#released-tools).
42
-
43
- ## Install app
44
-
45
- Unroll is a progressive web app. Install it and it opens in its own window and works with no network connection.
46
-
47
- 1. Open [sharpmeow-unroll.static.hf.space](https://sharpmeow-unroll.static.hf.space/) (the Space page embeds the app in a frame, where browsers do not offer installation; the **Install app** button there opens this page for you).
48
- 2. Press **Install app** in the View panel or in Settings. In Chrome and Edge you can also use the install icon in the address bar. In Safari on iPhone and iPad: Share, then Add to Home Screen. In Safari on macOS: File, then Add to Dock.
49
- 3. The page, three.js and the default scroll are kept on your device. Other scrolls and close-ups are kept after you view them once.
50
-
51
- Agents can call `window.unrollView.action('install')` and read `window.unrollView.pwa.state()`. The service worker (`sw.js`) is registered as `sw.js?v=<version>-<sha>` from `version.json`, so each stamped build uses fresh caches (`unroll-shell-<tag>`, `unroll-data-<tag>`, `unroll-runtime-<tag>`) and deletes the old ones.
52
-
53
- ## Scrolls, windings and layers
54
-
55
- The page is data driven. `scrolls/index.json` lists the scrolls and each one has a manifest (`scrolls/PHerc0139/manifest.json`) naming its windings, meshes, ink layers, sources, resolutions, credits and data terms. With more than one scroll a picker appears. `node tools/validate_scroll.mjs` checks the registry and every manifest with the same validator the page uses. See [docs/ADDING_A_SCROLL.md](docs/ADDING_A_SCROLL.md) to add one; `tools/precompute_template.py` is a starting point for building a package.
56
-
57
- The atlases ship as WebP (`data/atlas_*.webp`), with the JPGs kept as a fallback.
58
-
59
- ## Open your own
60
-
61
- Press **+** to unroll your own scroll: a zip, a folder, or a manifest with OBJ or PLY meshes (with UVs) and PNG, JPG or TIFF layers, or the URL of one (the server must allow CORS). Everything is read in your browser and kept in its storage; nothing is uploaded. The sheet has a downloadable synthetic example package. Details and limits are in [docs/ADDING_A_SCROLL.md](docs/ADDING_A_SCROLL.md#open-your-own).
62
-
63
- ## Destroy mode
64
-
65
- Just for fun, not a real unrolling. Press **Destroy** (T) to tear (X), crush (C), burn (B) or soak (W) the scroll at any unroll amount; the damage follows the sheet as it rolls and unrolls. Smoke, ash and burnt flakes rise from each ember where it is on the sheet right now. Debris lands on a soft floor that sits just below the scroll as currently drawn, rolled or unrolled, and fades soon after landing.
66
-
67
- **Crumble** (K, or `unrollView.crumble()` in the console) breaks the whole scroll up from its edges inward over about four seconds; the pieces are pulled in toward the middle and fall into a heap of dust on the floor. **Reset** restores the scroll and clears all damage, debris and the heap. Particle and fragment counts stay capped (about 1,800 particles, 600 fragments and 2,600 heap grains on desktop, less on phones).
68
-
69
- Why the tools behave as they do, with sources: [docs/REALISM.md](docs/REALISM.md).
70
-
71
- For testing, `unrollView.damageStats()` reports the counts, the floor height, `stillAirMax` (longest any debris hung still above the floor, should stay 0) and `stillMax`.
72
-
73
  ## Rebuilding the data
74
 
75
  From the Space's root, with Python 3, `numpy`, `scipy`, `tifffile`, `Pillow` and `curl`:
76
 
77
  1. `python tools/fetch.py` lists the public bucket and downloads the meshes and ink previews into `raw/` (about 110 MB).
78
- 2. `python tools/build.py` writes `data/scroll.bin`, `data/scroll.json`, the atlases and `data/hi/`. It reads the X-ray layer straight from the bucket. `UNROLL_STEP` (default 3) sets the mesh coarseness; `python tools/build.py --coarsen 2 data/<scroll>` writes a lighter `scroll_lite.*` copy for phones without new downloads.
79
- 3. `python tools/gpu_layer.py` rebuilds the ink_9um (9 µm) layer. It needs the ink_9um output TIFFs in `gpu/wNNN/` (about 25 MB each, not shipped). Regenerate them with the model card's inference command on the surface-volume URLs in `data/gpu/receipts/`.
80
 
81
  ## Licence and credits
82
 
83
  Data: Giorgio Angelotti, Stephen Parsons, Sean Johnson, Elian Rafael Dal Prà, Johannes Rudolph, Paul Tafforeau, Alessandro Mirone, Paul Henderson, Hendrik Schilling, Forrest McDonald, David Josey, Youssef Nader, C. Seth Parker, W. Brent Seales. *Vesuvius Challenge - CT Scans of Herculaneum Papyri.* Vesuvius Challenge. [Data browser for PHerc. 139](https://scrollprize.org/data_browser/PHerc0139). PHerc. 139 is held by the Biblioteca Nazionale di Napoli.
84
 
85
- `data/` and `og.jpg` are derived from that data and are licensed [CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/). The code (`index.html`, `style.css`, `app.js`, `tear.js`, `polish.js`, `lib/`, `scrolls/*.json` and `tools/`) is Apache 2.0, see `LICENSE` and `NOTICE`. The page uses [three.js](https://threejs.org) 0.160 from jsDelivr, which keeps its own MIT licence.
86
-
87
- ## Ink Bench
88
-
89
- Score your own ink prediction for any winding against the published ink, in the browser (nothing is uploaded): automatic UV alignment, side by side, swipe and difference views, Pearson r, IoU, Dice and a precision and recall curve (agreement with another model's output, not ground truth), letter boxes marked "candidate for expert review", and a one-click reproducible bundle. `tools/inkbench.py` gives identical results in Python and on the command line. See [docs/BENCH.md](docs/BENCH.md) and the open bundle format [docs/BENCH_FORMAT.md](docs/BENCH_FORMAT.md). The Bench code and format are Apache-2.0 (`LICENSE`, `NOTICE`, by Chase Hendrick), like the rest of the code; the data stays CC BY-NC 4.0.
90
-
91
- ### Researcher takeaway
92
-
93
- On the public ink_9um run over all 37 windings (`docs/examples/bench/`), against the published 2.4 µm ink: median AP 0.319 for ink_9um, 0.117 for raw X-ray intensity and 0.089 for uniform noise, and ink_9um beats X-ray intensity on AP on every winding. On Pearson r the medians are 0.422, 0.266 and 0.000, but X-ray intensity outranks ink_9um on `w039`, `w053`, `w054` and `w057`. So: rank ink models by AP (threshold-free and robust to brightness), always report the X-ray floor next to your model, and look at windings where the floor wins before trusting a correlation. These are agreement scores with another model's output, not ground truth.
 
6
  sdk: static
7
  app_file: index.html
8
  pinned: false
9
+ license: cc-by-nc-4.0
10
  short_description: Unroll a carbonized Herculaneum scroll in your browser
11
  thumbnail: https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/og.jpg
12
  tags:
 
21
 
22
  # Unroll
23
 
24
+ PHerc. 139 is a papyrus roll carbonized when Vesuvius buried Herculaneum in 79 CE. It cannot be opened, so it was scanned with X-rays at the ESRF synchrotron. This page rebuilds 37 of its windings in 3D from the public Vesuvius Challenge meshes and lets you unroll them in the browser, with the ink that neural networks found on them.
25
 
26
  - **Shapes**: the 9.362 µm tifxyz meshes of the 37 traced windings (`w023` to `w059`), decimated to 579,450 vertices.
27
  - **X-ray layer**: the CT intensity on each surface, from the public 9.362 µm surface volumes.
28
+ - **Ink layer (amber)**: the Vesuvius Challenge's public ink detection on the 2.4 µm scan. In 3D it is drawn through a local-contrast filter so letter-sized strokes stand out. The close-up view shows the published preview without filtering, downscaled to 2,400 px on its long side and re-saved as JPEG.
29
+ - **ZeroGPU layer (cyan)**: windings `w046` to `w051`, inked by running the public [`scrollprize/ink_9um`](https://huggingface.co/scrollprize/ink_9um) model (seed 42, step 75000) on the 9.362 µm scan. It used all 8 of a free account's daily ZeroGPU runs: 37,141 patches in 57 s of measured GPU time (ZeroGPU charged about 66 of the 300 daily GPU-seconds). The 9.362 µm scan is about four times coarser than the 2.4 µm one, so this layer is noisier. `w047` and `w050` ran with patch overlap 0.5; the other four ran with 0.25 or 0.2, and their 128 px patch grid shows as faint square tiles. Per-run records (checkpoint sha256, villa commit, requested and measured GPU seconds) are in `data/gpu/receipts/`; `data/gpu.json` is the summary the page reads.
30
 
31
  Unrolling lays the windings end to end, outermost first. Each traced winding runs a little past one full turn, so roughly a tenth of the 2.8 m strip repeats surface from its neighbour.
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  ## Rebuilding the data
34
 
35
  From the Space's root, with Python 3, `numpy`, `scipy`, `tifffile`, `Pillow` and `curl`:
36
 
37
  1. `python tools/fetch.py` lists the public bucket and downloads the meshes and ink previews into `raw/` (about 110 MB).
38
+ 2. `python tools/build.py` writes `data/scroll.bin`, `data/scroll.json`, the atlases and `data/hi/`. It reads the X-ray layer straight from the bucket.
39
+ 3. `python tools/gpu_layer.py` rebuilds the ZeroGPU layer. It needs the ink_9um output TIFFs in `gpu/wNNN/` (about 25 MB each, not shipped). Regenerate them with the model card's inference command on the surface-volume URLs in `data/gpu/receipts/`.
40
 
41
  ## Licence and credits
42
 
43
  Data: Giorgio Angelotti, Stephen Parsons, Sean Johnson, Elian Rafael Dal Prà, Johannes Rudolph, Paul Tafforeau, Alessandro Mirone, Paul Henderson, Hendrik Schilling, Forrest McDonald, David Josey, Youssef Nader, C. Seth Parker, W. Brent Seales. *Vesuvius Challenge - CT Scans of Herculaneum Papyri.* Vesuvius Challenge. [Data browser for PHerc. 139](https://scrollprize.org/data_browser/PHerc0139). PHerc. 139 is held by the Biblioteca Nazionale di Napoli.
44
 
45
+ `data/` and `og.jpg` are derived from that data and are licensed [CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/). The code (`index.html`, `style.css`, `app.js` and `tools/`) is MIT, see `LICENSE`. The page uses [three.js](https://threejs.org) 0.160 from jsDelivr.
 
 
 
 
 
 
 
 
app.js CHANGED
The diff for this file is too large to render. See raw diff
 
data/gpu.json CHANGED
@@ -1,258 +1,5 @@
1
  {
2
  "windings": {
3
- "w023": {
4
- "tex": "data/gpu/w023.jpg",
5
- "receipt": "data/gpu/receipts/w023.json",
6
- "hi": "hi/w023_gpu.jpg",
7
- "patches": 9227,
8
- "gpuSeconds": 8.4,
9
- "calls": 2,
10
- "overlap": 0.5,
11
- "started": "2026-10-16T03:28:07Z",
12
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 9,227 patches of 128\u00d7128 px, overlap 0.5 \u00b7 8.4 s of GPU in 2 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
13
- },
14
- "w024": {
15
- "tex": "data/gpu/w024.jpg",
16
- "receipt": "data/gpu/receipts/w024.json",
17
- "hi": "hi/w024_gpu.jpg",
18
- "patches": 9758,
19
- "gpuSeconds": 8.5,
20
- "calls": 2,
21
- "overlap": 0.5,
22
- "started": "2026-10-16T03:28:08Z",
23
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 9,758 patches of 128\u00d7128 px, overlap 0.5 \u00b7 8.5 s of GPU in 2 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
24
- },
25
- "w025": {
26
- "tex": "data/gpu/w025.jpg",
27
- "receipt": "data/gpu/receipts/w025.json",
28
- "hi": "hi/w025_gpu.jpg",
29
- "patches": 10316,
30
- "gpuSeconds": 10.1,
31
- "calls": 2,
32
- "overlap": 0.5,
33
- "started": "2026-10-16T03:28:21Z",
34
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 10,316 patches of 128\u00d7128 px, overlap 0.5 \u00b7 10.1 s of GPU in 2 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
35
- },
36
- "w026": {
37
- "tex": "data/gpu/w026.jpg",
38
- "receipt": "data/gpu/receipts/w026.json",
39
- "hi": "hi/w026_gpu.jpg",
40
- "patches": 11741,
41
- "gpuSeconds": 10.5,
42
- "calls": 2,
43
- "overlap": 0.5,
44
- "started": "2026-10-16T03:28:28Z",
45
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 11,741 patches of 128\u00d7128 px, overlap 0.5 \u00b7 10.5 s of GPU in 2 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
46
- },
47
- "w027": {
48
- "tex": "data/gpu/w027.jpg",
49
- "receipt": "data/gpu/receipts/w027.json",
50
- "hi": "hi/w027_gpu.jpg",
51
- "patches": 11328,
52
- "gpuSeconds": 10.2,
53
- "calls": 2,
54
- "overlap": 0.5,
55
- "started": "2026-10-16T03:28:12Z",
56
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 11,328 patches of 128\u00d7128 px, overlap 0.5 \u00b7 10.2 s of GPU in 2 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
57
- },
58
- "w028": {
59
- "tex": "data/gpu/w028.jpg",
60
- "receipt": "data/gpu/receipts/w028.json",
61
- "hi": "hi/w028_gpu.jpg",
62
- "patches": 11358,
63
- "gpuSeconds": 9.1,
64
- "calls": 2,
65
- "overlap": 0.5,
66
- "started": "2026-10-16T03:28:09Z",
67
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 11,358 patches of 128\u00d7128 px, overlap 0.5 \u00b7 9.1 s of GPU in 2 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
68
- },
69
- "w029": {
70
- "tex": "data/gpu/w029.jpg",
71
- "receipt": "data/gpu/receipts/w029.json",
72
- "hi": "hi/w029_gpu.jpg",
73
- "patches": 11356,
74
- "gpuSeconds": 10.2,
75
- "calls": 2,
76
- "overlap": 0.5,
77
- "started": "2026-10-16T03:28:12Z",
78
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 11,356 patches of 128\u00d7128 px, overlap 0.5 \u00b7 10.2 s of GPU in 2 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
79
- },
80
- "w030": {
81
- "tex": "data/gpu/w030.jpg",
82
- "receipt": "data/gpu/receipts/w030.json",
83
- "hi": "hi/w030_gpu.jpg",
84
- "patches": 11412,
85
- "gpuSeconds": 10.4,
86
- "calls": 2,
87
- "overlap": 0.5,
88
- "started": "2026-10-16T03:28:26Z",
89
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 11,412 patches of 128\u00d7128 px, overlap 0.5 \u00b7 10.4 s of GPU in 2 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
90
- },
91
- "w031": {
92
- "tex": "data/gpu/w031.jpg",
93
- "receipt": "data/gpu/receipts/w031.json",
94
- "hi": "hi/w031_gpu.jpg",
95
- "patches": 11436,
96
- "gpuSeconds": 12.5,
97
- "calls": 2,
98
- "overlap": 0.5,
99
- "started": "2026-10-16T03:29:25Z",
100
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 11,436 patches of 128\u00d7128 px, overlap 0.5 \u00b7 12.5 s of GPU in 2 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
101
- },
102
- "w032": {
103
- "tex": "data/gpu/w032.jpg",
104
- "receipt": "data/gpu/receipts/w032.json",
105
- "hi": "hi/w032_gpu.jpg",
106
- "patches": 11640,
107
- "gpuSeconds": 10.3,
108
- "calls": 2,
109
- "overlap": 0.5,
110
- "started": "2026-10-16T03:29:31Z",
111
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 11,640 patches of 128\u00d7128 px, overlap 0.5 \u00b7 10.3 s of GPU in 2 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
112
- },
113
- "w033": {
114
- "tex": "data/gpu/w033.jpg",
115
- "receipt": "data/gpu/receipts/w033.json",
116
- "hi": "hi/w033_gpu.jpg",
117
- "patches": 11803,
118
- "gpuSeconds": 10.1,
119
- "calls": 2,
120
- "overlap": 0.5,
121
- "started": "2026-10-16T03:30:17Z",
122
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 11,803 patches of 128\u00d7128 px, overlap 0.5 \u00b7 10.1 s of GPU in 2 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
123
- },
124
- "w034": {
125
- "tex": "data/gpu/w034.jpg",
126
- "receipt": "data/gpu/receipts/w034.json",
127
- "hi": "hi/w034_gpu.jpg",
128
- "patches": 11785,
129
- "gpuSeconds": 9.3,
130
- "calls": 2,
131
- "overlap": 0.5,
132
- "started": "2026-10-16T03:29:44Z",
133
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 11,785 patches of 128\u00d7128 px, overlap 0.5 \u00b7 9.3 s of GPU in 2 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
134
- },
135
- "w035": {
136
- "tex": "data/gpu/w035.jpg",
137
- "receipt": "data/gpu/receipts/w035.json",
138
- "hi": "hi/w035_gpu.jpg",
139
- "patches": 6456,
140
- "gpuSeconds": 5.6,
141
- "calls": 1,
142
- "overlap": 0.5,
143
- "started": "2026-10-16T03:29:27Z",
144
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 6,456 patches of 128\u00d7128 px, overlap 0.5 \u00b7 5.6 s of GPU in 1 call on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
145
- },
146
- "w036": {
147
- "tex": "data/gpu/w036.jpg",
148
- "receipt": "data/gpu/receipts/w036.json",
149
- "hi": "hi/w036_gpu.jpg",
150
- "patches": 8923,
151
- "gpuSeconds": 8.5,
152
- "calls": 2,
153
- "overlap": 0.5,
154
- "started": "2026-10-16T03:30:09Z",
155
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 8,923 patches of 128\u00d7128 px, overlap 0.5 \u00b7 8.5 s of GPU in 2 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
156
- },
157
- "w037": {
158
- "tex": "data/gpu/w037.jpg",
159
- "receipt": "data/gpu/receipts/w037.json",
160
- "hi": "hi/w037_gpu.jpg",
161
- "patches": 9501,
162
- "gpuSeconds": 9.5,
163
- "calls": 2,
164
- "overlap": 0.5,
165
- "started": "2026-10-16T03:30:14Z",
166
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 9,501 patches of 128\u00d7128 px, overlap 0.5 \u00b7 9.5 s of GPU in 2 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
167
- },
168
- "w038": {
169
- "tex": "data/gpu/w038.jpg",
170
- "receipt": "data/gpu/receipts/w038.json",
171
- "hi": "hi/w038_gpu.jpg",
172
- "patches": 9970,
173
- "gpuSeconds": 9.2,
174
- "calls": 2,
175
- "overlap": 0.5,
176
- "started": "2026-10-16T03:30:16Z",
177
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 9,970 patches of 128\u00d7128 px, overlap 0.5 \u00b7 9.2 s of GPU in 2 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
178
- },
179
- "w039": {
180
- "tex": "data/gpu/w039.jpg",
181
- "receipt": "data/gpu/receipts/w039.json",
182
- "hi": "hi/w039_gpu.jpg",
183
- "patches": 10514,
184
- "gpuSeconds": 8.7,
185
- "calls": 2,
186
- "overlap": 0.5,
187
- "started": "2026-10-16T03:30:33Z",
188
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 10,514 patches of 128\u00d7128 px, overlap 0.5 \u00b7 8.7 s of GPU in 2 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
189
- },
190
- "w040": {
191
- "tex": "data/gpu/w040.jpg",
192
- "receipt": "data/gpu/receipts/w040.json",
193
- "hi": "hi/w040_gpu.jpg",
194
- "patches": 11076,
195
- "gpuSeconds": 9.0,
196
- "calls": 2,
197
- "overlap": 0.5,
198
- "started": "2026-10-16T03:31:00Z",
199
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 11,076 patches of 128\u00d7128 px, overlap 0.5 \u00b7 9.0 s of GPU in 2 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
200
- },
201
- "w041": {
202
- "tex": "data/gpu/w041.jpg",
203
- "receipt": "data/gpu/receipts/w041.json",
204
- "hi": "hi/w041_gpu.jpg",
205
- "patches": 10920,
206
- "gpuSeconds": 9.4,
207
- "calls": 2,
208
- "overlap": 0.5,
209
- "started": "2026-10-16T03:31:02Z",
210
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 10,920 patches of 128\u00d7128 px, overlap 0.5 \u00b7 9.4 s of GPU in 2 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
211
- },
212
- "w042": {
213
- "tex": "data/gpu/w042.jpg",
214
- "receipt": "data/gpu/receipts/w042.json",
215
- "hi": "hi/w042_gpu.jpg",
216
- "patches": 10440,
217
- "gpuSeconds": 8.4,
218
- "calls": 2,
219
- "overlap": 0.5,
220
- "started": "2026-10-16T03:31:11Z",
221
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 10,440 patches of 128\u00d7128 px, overlap 0.5 \u00b7 8.4 s of GPU in 2 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
222
- },
223
- "w043": {
224
- "tex": "data/gpu/w043.jpg",
225
- "receipt": "data/gpu/receipts/w043.json",
226
- "hi": "hi/w043_gpu.jpg",
227
- "patches": 11006,
228
- "gpuSeconds": 9.9,
229
- "calls": 2,
230
- "overlap": 0.5,
231
- "started": "2026-10-16T03:31:44Z",
232
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 11,006 patches of 128\u00d7128 px, overlap 0.5 \u00b7 9.9 s of GPU in 2 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
233
- },
234
- "w044": {
235
- "tex": "data/gpu/w044.jpg",
236
- "receipt": "data/gpu/receipts/w044.json",
237
- "hi": "hi/w044_gpu.jpg",
238
- "patches": 11022,
239
- "gpuSeconds": 11.4,
240
- "calls": 2,
241
- "overlap": 0.5,
242
- "started": "2026-10-16T03:31:54Z",
243
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 11,022 patches of 128\u00d7128 px, overlap 0.5 \u00b7 11.4 s of GPU in 2 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
244
- },
245
- "w045": {
246
- "tex": "data/gpu/w045.jpg",
247
- "receipt": "data/gpu/receipts/w045.json",
248
- "hi": "hi/w045_gpu.jpg",
249
- "patches": 10950,
250
- "gpuSeconds": 9.7,
251
- "calls": 2,
252
- "overlap": 0.5,
253
- "started": "2026-10-16T03:31:58Z",
254
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 10,950 patches of 128\u00d7128 px, overlap 0.5 \u00b7 9.7 s of GPU in 2 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
255
- },
256
  "w046": {
257
  "tex": "data/gpu/w046.jpg",
258
  "receipt": "data/gpu/receipts/w046.json",
@@ -262,7 +9,7 @@
262
  "calls": 1,
263
  "overlap": 0.25,
264
  "started": "2026-10-16T00:22:15Z",
265
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 4,891 patches of 128\u00d7128 px, overlap 0.25 (patch seams visible) \u00b7 7.5 s of GPU in 1 call \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
266
  },
267
  "w047": {
268
  "tex": "data/gpu/w047.jpg",
@@ -273,7 +20,7 @@
273
  "calls": 3,
274
  "overlap": 0.5,
275
  "started": "2026-10-15T20:09:32Z",
276
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 10,950 patches of 128\u00d7128 px, overlap 0.5 \u00b7 18.4 s of GPU in 3 calls \u00b7 2026-10-15 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
277
  },
278
  "w048": {
279
  "tex": "data/gpu/w048.jpg",
@@ -284,7 +31,7 @@
284
  "calls": 1,
285
  "overlap": 0.25,
286
  "started": "2026-10-16T00:23:44Z",
287
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 4,885 patches of 128\u00d7128 px, overlap 0.25 (patch seams visible) \u00b7 7.6 s of GPU in 1 call \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
288
  },
289
  "w049": {
290
  "tex": "data/gpu/w049.jpg",
@@ -295,7 +42,7 @@
295
  "calls": 1,
296
  "overlap": 0.25,
297
  "started": "2026-10-16T00:25:23Z",
298
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 5,306 patches of 128\u00d7128 px, overlap 0.25 (patch seams visible) \u00b7 8.2 s of GPU in 1 call \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
299
  },
300
  "w050": {
301
  "tex": "data/gpu/w050.jpg",
@@ -306,7 +53,7 @@
306
  "calls": 1,
307
  "overlap": 0.5,
308
  "started": "2026-10-16T00:26:27Z",
309
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 4,629 patches of 128\u00d7128 px, overlap 0.5 \u00b7 6.4 s of GPU in 1 call \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
310
  },
311
  "w051": {
312
  "tex": "data/gpu/w051.jpg",
@@ -317,103 +64,15 @@
317
  "calls": 1,
318
  "overlap": 0.2,
319
  "started": "2026-10-16T00:28:35Z",
320
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 6,480 patches of 128\u00d7128 px, overlap 0.2 (patch seams visible) \u00b7 9.1 s of GPU in 1 call \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
321
- },
322
- "w052": {
323
- "tex": "data/gpu/w052.jpg",
324
- "receipt": "data/gpu/receipts/w052.json",
325
- "hi": "hi/w052_gpu.jpg",
326
- "patches": 16763,
327
- "gpuSeconds": 13.6,
328
- "calls": 3,
329
- "overlap": 0.5,
330
- "started": "2026-10-16T03:32:06Z",
331
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 16,763 patches of 128\u00d7128 px, overlap 0.5 \u00b7 13.6 s of GPU in 3 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
332
- },
333
- "w053": {
334
- "tex": "data/gpu/w053.jpg",
335
- "receipt": "data/gpu/receipts/w053.json",
336
- "hi": "hi/w053_gpu.jpg",
337
- "patches": 16590,
338
- "gpuSeconds": 20.9,
339
- "calls": 3,
340
- "overlap": 0.5,
341
- "started": "2026-10-16T03:32:19Z",
342
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 16,590 patches of 128\u00d7128 px, overlap 0.5 \u00b7 20.9 s of GPU in 3 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
343
- },
344
- "w054": {
345
- "tex": "data/gpu/w054.jpg",
346
- "receipt": "data/gpu/receipts/w054.json",
347
- "hi": "hi/w054_gpu.jpg",
348
- "patches": 18048,
349
- "gpuSeconds": 14.3,
350
- "calls": 3,
351
- "overlap": 0.5,
352
- "started": "2026-10-16T03:32:53Z",
353
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 18,048 patches of 128\u00d7128 px, overlap 0.5 \u00b7 14.3 s of GPU in 3 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
354
- },
355
- "w055": {
356
- "tex": "data/gpu/w055.jpg",
357
- "receipt": "data/gpu/receipts/w055.json",
358
- "hi": "hi/w055_gpu.jpg",
359
- "patches": 17156,
360
- "gpuSeconds": 13.1,
361
- "calls": 3,
362
- "overlap": 0.5,
363
- "started": "2026-10-16T03:32:47Z",
364
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 17,156 patches of 128\u00d7128 px, overlap 0.5 \u00b7 13.1 s of GPU in 3 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
365
- },
366
- "w056": {
367
- "tex": "data/gpu/w056.jpg",
368
- "receipt": "data/gpu/receipts/w056.json",
369
- "hi": "hi/w056_gpu.jpg",
370
- "patches": 15243,
371
- "gpuSeconds": 11.9,
372
- "calls": 2,
373
- "overlap": 0.5,
374
- "started": "2026-10-16T03:32:46Z",
375
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 15,243 patches of 128\u00d7128 px, overlap 0.5 \u00b7 11.9 s of GPU in 2 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
376
- },
377
- "w057": {
378
- "tex": "data/gpu/w057.jpg",
379
- "receipt": "data/gpu/receipts/w057.json",
380
- "hi": "hi/w057_gpu.jpg",
381
- "patches": 16223,
382
- "gpuSeconds": 14.8,
383
- "calls": 3,
384
- "overlap": 0.5,
385
- "started": "2026-10-16T03:33:53Z",
386
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 16,223 patches of 128\u00d7128 px, overlap 0.5 \u00b7 14.8 s of GPU in 3 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
387
- },
388
- "w058": {
389
- "tex": "data/gpu/w058.jpg",
390
- "receipt": "data/gpu/receipts/w058.json",
391
- "hi": "hi/w058_gpu.jpg",
392
- "patches": 15924,
393
- "gpuSeconds": 15.0,
394
- "calls": 3,
395
- "overlap": 0.5,
396
- "started": "2026-10-16T03:34:22Z",
397
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 15,924 patches of 128\u00d7128 px, overlap 0.5 \u00b7 15.0 s of GPU in 3 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
398
- },
399
- "w059": {
400
- "tex": "data/gpu/w059.jpg",
401
- "receipt": "data/gpu/receipts/w059.json",
402
- "hi": "hi/w059_gpu.jpg",
403
- "patches": 15294,
404
- "gpuSeconds": 13.1,
405
- "calls": 3,
406
- "overlap": 0.5,
407
- "started": "2026-10-16T03:34:29Z",
408
- "meta": "ink_9um (9 \u00b5m): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 \u00b5m scan (9.362 \u00b5m voxels) \u00b7 15,294 patches of 128\u00d7128 px, overlap 0.5 \u00b7 13.1 s of GPU in 3 calls on Modal \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
409
  }
410
  },
411
  "totals": {
412
- "windings": 37,
413
- "patches": 412320,
414
- "gpuSeconds": 392.8,
415
- "calls": 76,
416
  "device": "NVIDIA RTX PRO 6000 Blackwell Server Edition MIG 2g.48gb"
417
  },
418
- "about": "<b>ink_9um (9 \u00b5m)</b> is the cyan band. Chase Hendrick ran the public <a href=\"https://huggingface.co/scrollprize/ink_9um\" target=\"_blank\" rel=\"noopener\">scrollprize/ink_9um</a> model, which the Vesuvius Challenge trained, on Hugging Face ZeroGPU over windings w046 to w051. More windings are added as daily free GPU runs allow, so this layer is a work in progress and will grow. The first batch was 37,141 patches in 57 s of measured GPU time on half an NVIDIA RTX PRO 6000 Blackwell. It reads the 9.362 \u00b5m scan, about four times coarser than the 2.4 \u00b5m scan behind the amber layer, so its ink map is noisier. w047 and w050 ran with patch overlap 0.5; w046, w048, w049 and w051 ran with less overlap, so their 128 px patch grid shows as faint square tiles. That grid is a stitching artifact, not ink. Click a cyan winding to compare the two maps."
419
- }
 
1
  {
2
  "windings": {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  "w046": {
4
  "tex": "data/gpu/w046.jpg",
5
  "receipt": "data/gpu/receipts/w046.json",
 
9
  "calls": 1,
10
  "overlap": 0.25,
11
  "started": "2026-10-16T00:22:15Z",
12
+ "meta": "ZeroGPU run: scrollprize/ink_9um (seed 42, step 75000) on the 9.362 \u00b5m scan \u00b7 4,891 patches of 128\u00d7128 px, overlap 0.25 (patch seams visible) \u00b7 7.5 s of GPU in 1 call \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
13
  },
14
  "w047": {
15
  "tex": "data/gpu/w047.jpg",
 
20
  "calls": 3,
21
  "overlap": 0.5,
22
  "started": "2026-10-15T20:09:32Z",
23
+ "meta": "ZeroGPU run: scrollprize/ink_9um (seed 42, step 75000) on the 9.362 \u00b5m scan \u00b7 10,950 patches of 128\u00d7128 px, overlap 0.5 \u00b7 18.4 s of GPU in 3 calls \u00b7 2026-10-15 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
24
  },
25
  "w048": {
26
  "tex": "data/gpu/w048.jpg",
 
31
  "calls": 1,
32
  "overlap": 0.25,
33
  "started": "2026-10-16T00:23:44Z",
34
+ "meta": "ZeroGPU run: scrollprize/ink_9um (seed 42, step 75000) on the 9.362 \u00b5m scan \u00b7 4,885 patches of 128\u00d7128 px, overlap 0.25 (patch seams visible) \u00b7 7.6 s of GPU in 1 call \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
35
  },
36
  "w049": {
37
  "tex": "data/gpu/w049.jpg",
 
42
  "calls": 1,
43
  "overlap": 0.25,
44
  "started": "2026-10-16T00:25:23Z",
45
+ "meta": "ZeroGPU run: scrollprize/ink_9um (seed 42, step 75000) on the 9.362 \u00b5m scan \u00b7 5,306 patches of 128\u00d7128 px, overlap 0.25 (patch seams visible) \u00b7 8.2 s of GPU in 1 call \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
46
  },
47
  "w050": {
48
  "tex": "data/gpu/w050.jpg",
 
53
  "calls": 1,
54
  "overlap": 0.5,
55
  "started": "2026-10-16T00:26:27Z",
56
+ "meta": "ZeroGPU run: scrollprize/ink_9um (seed 42, step 75000) on the 9.362 \u00b5m scan \u00b7 4,629 patches of 128\u00d7128 px, overlap 0.5 \u00b7 6.4 s of GPU in 1 call \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
57
  },
58
  "w051": {
59
  "tex": "data/gpu/w051.jpg",
 
64
  "calls": 1,
65
  "overlap": 0.2,
66
  "started": "2026-10-16T00:28:35Z",
67
+ "meta": "ZeroGPU run: scrollprize/ink_9um (seed 42, step 75000) on the 9.362 \u00b5m scan \u00b7 6,480 patches of 128\u00d7128 px, overlap 0.2 (patch seams visible) \u00b7 9.1 s of GPU in 1 call \u00b7 2026-10-16 \u00b7 drag to pan, scroll or pinch to zoom, double-click to fit"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  }
69
  },
70
  "totals": {
71
+ "windings": 6,
72
+ "patches": 37141,
73
+ "gpuSeconds": 57.2,
74
+ "calls": 8,
75
  "device": "NVIDIA RTX PRO 6000 Blackwell Server Edition MIG 2g.48gb"
76
  },
77
+ "about": "<b>The cyan band</b> is ours. We used all 8 of a free Hugging Face account's daily ZeroGPU runs (the limit is 8; most of the daily GPU-time quota was left over) to run the public <a href=\"https://huggingface.co/scrollprize/ink_9um\" target=\"_blank\" rel=\"noopener\">scrollprize/ink_9um</a> model over windings w046 to w051: 37,141 patches in 57 s of measured GPU time on half an NVIDIA RTX PRO 6000 Blackwell. It reads the 9.362 \u00b5m scan, about four times coarser than the 2.4 \u00b5m scan behind the amber layer, so its ink map is noisier. w047 and w050 ran with patch overlap 0.5; w046, w048, w049 and w051 ran with less overlap, so their 128 px patch grid shows as faint square tiles. That grid is a stitching artifact, not ink. Click a cyan winding to compare the two maps."
78
+ }
index.html CHANGED
@@ -4,78 +4,26 @@
4
  <meta charset="utf-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
  <title>Unroll · PHerc. 139</title>
7
- <meta name="description" content="Unroll a carbonized Herculaneum scroll in your browser: 37 windings traced in X-ray CT. Ink layers show where models detect ink; letters visible here are candidates for expert reading.">
8
  <meta property="og:title" content="Unroll · a Herculaneum scroll, in your browser">
9
- <meta property="og:description" content="37 windings of PHerc. 139, traced in X-ray CT. Ink layers show where models detect ink; letters visible here are candidates for expert reading.">
10
  <meta property="og:image" content="https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/og.jpg">
11
- <meta property="og:image:width" content="1200">
12
- <meta property="og:image:height" content="630">
13
- <meta property="og:image:alt" content="Carbonized papyrus sheets of PHerc. 139 laid flat in 3D, with amber ink-layer glow">
14
- <meta property="og:type" content="website">
15
- <meta property="og:site_name" content="Unroll">
16
- <meta property="og:url" content="https://huggingface.co/spaces/SharpMeow/unroll">
17
- <link rel="canonical" href="https://huggingface.co/spaces/SharpMeow/unroll">
18
- <meta name="twitter:title" content="Unroll · a Herculaneum scroll, in your browser">
19
- <meta name="twitter:description" content="37 windings of PHerc. 139, traced in X-ray CT. Ink layers show where models detect ink; letters visible here are candidates for expert reading.">
20
- <meta name="twitter:image" content="https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/og.jpg">
21
- <meta name="twitter:image:alt" content="Carbonized papyrus sheets of PHerc. 139 laid flat in 3D, with amber ink-layer glow">
22
- <meta name="theme-color" content="#060505">
23
- <meta name="color-scheme" content="dark">
24
  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23060505'/%3E%3Cpath d='M16 6a10 10 0 1 0 0.01 0M16 10a6 6 0 1 1-0.01 0M16 14a2 2 0 1 0 0.01 0' fill='none' stroke='%23ffb347' stroke-width='1.6'/%3E%3C/svg%3E">
25
- <link rel="manifest" href="manifest.webmanifest">
26
- <link rel="apple-touch-icon" href="icons/apple-touch-icon.png">
27
- <meta name="apple-mobile-web-app-capable" content="yes">
28
- <meta name="mobile-web-app-capable" content="yes">
29
- <meta name="apple-mobile-web-app-title" content="Unroll">
30
- <meta name="apple-mobile-web-app-status-bar-style" content="black">
31
  <meta name="twitter:card" content="summary_large_image">
32
  <link rel="preconnect" href="https://fonts.googleapis.com">
33
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
34
  <link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
35
- <link rel="stylesheet" href="style.css?v=20261016vol3">
36
- <link rel="stylesheet" href="bench.css?v=20261016vol3">
37
- <link rel="stylesheet" href="ui.css?v=20261016vol3">
38
- <link rel="alternate" type="application/json+oembed" href="https://sharpmeow-unroll.static.hf.space/oembed.json" title="Unroll">
39
- <script>
40
- // embed mode is decided before the first paint so the compact layout never flashes
41
- (function () { var q = location.search; if (/[?&]embed=(1|true|yes)\b/.test(q)) { document.documentElement.classList.add('embed'); if (/[?&]controls=(0|false|no)\b/.test(q)) document.documentElement.classList.add('embed-nocontrols'); } })();
42
- </script>
43
- <!-- start the big downloads and the 3D library in parallel with the page instead of one after another -->
44
- <link rel="modulepreload" href="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js" crossorigin>
45
- <script>
46
- // the default scroll's big files, fetched while three.js loads; skipped when the URL opens another scroll (#s=...)
47
- if (!/(^|[#&])s=/.test(location.hash)) ['scrolls/index.json', 'scrolls/PHerc0139/manifest.json', 'data/scroll.json', 'data/scroll.i16p.gz', 'data/atlas_ink.webp', 'data/atlas_ct.webp'].forEach(function (h) {
48
- var l = document.createElement('link'); l.rel = 'preload'; l.as = 'fetch'; l.crossOrigin = 'anonymous'; l.href = h; document.head.appendChild(l);
49
- });
50
- </script>
51
  <script type="importmap">
52
  { "imports": {
53
  "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
54
- "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/",
55
- "./lib/ctsource.js": "./lib/ctsource.js?v=20261016vol3",
56
- "./lib/scrolls.js": "./lib/scrolls.js?v=20261016vol3",
57
- "./lib/manifest.js": "./lib/manifest.js?v=20261016vol3",
58
- "./lib/openown.js": "./lib/openown.js?v=20261016vol3",
59
- "./lib/formats.js": "./lib/formats.js?v=20261016vol3",
60
- "./lib/example.js": "./lib/example.js?v=20261016vol3",
61
- "./lib/bench-core.js": "./lib/bench-core.js?v=20261016vol3",
62
- "./lib/api.js": "./lib/api.js?v=20261016vol3",
63
- "./lib/apispec.js": "./lib/apispec.js?v=20261016vol3",
64
- "./lib/embed.js": "./lib/embed.js?v=20261016vol3",
65
- "./lib/version.js": "./lib/version.js?v=20261016vol3",
66
- "./tear.js": "./tear.js?v=20261016vol3nan",
67
- "./damage.js": "./damage.js?v=20261016vol3"
68
  } }
69
  </script>
70
  </head>
71
  <body>
72
- <canvas id="gl" tabindex="0" role="application" aria-roledescription="3D view" aria-label="3D view of the scroll. Arrow keys orbit, Shift and arrows pan, plus and minus zoom, Enter opens the close-up of the current winding." aria-describedby="viewDesc" data-testid="view-3d" data-api="camera.orbit()"></canvas>
73
- <button id="embedGate" class="embed-gate" hidden data-testid="embed-activate" data-api="embed.activate()" aria-label="Activate the 3D viewer: after this, dragging, pinching and the mouse wheel control the scroll"><span class="mono">Click to interact</span></button>
74
- <a id="embedFull" class="embed-full mono" href="./" target="_blank" rel="noopener" data-testid="embed-open-full" data-api="deepLink()">Open full viewer ↗</a>
75
- <p id="viewDesc" class="sr-only" data-testid="view-description">Loading the scroll.</p>
76
- <div id="liveState" class="sr-only" role="status" aria-live="polite" aria-atomic="true" data-testid="live-state"></div>
77
  <div class="scrim top" aria-hidden="true"></div><div class="scrim bot" aria-hidden="true"></div>
78
- <div class="vignette" aria-hidden="true"></div>
79
  <div class="grain" aria-hidden="true"></div>
80
 
81
  <div id="loader">
@@ -84,66 +32,24 @@ if (!/(^|[#&])s=/.test(location.hash)) ['scrolls/index.json', 'scrolls/PHerc0139
84
  <div class="loader-title">Reconstructing the scroll</div>
85
  <div class="bar"><i id="loadbar"></i></div>
86
  <div class="mono dim" id="loadtxt">0 MB</div>
87
- <div class="load-err" id="loadErr" hidden>
88
- <p>The scroll could not be loaded. <span id="loadErrWhy"></span></p>
89
- <button class="chip" onclick="location.reload()" data-testid="load-retry" data-api="reload()">Try again</button>
90
- </div>
91
  </div>
92
  </div>
93
- <script>
94
- // empty and error states: if the scroll data or 3D graphics fail, say so instead of loading forever
95
- (function () {
96
- function fail(e) {
97
- var box = document.getElementById('loadErr');
98
- if (!box || !document.getElementById('loader')) return;
99
- var msg = String((e && (e.reason || e.error || e.message)) || '');
100
- var why = window.__unrollWhy ? window.__unrollWhy : /webgl|context/i.test(msg) ? 'Your browser could not start 3D graphics (WebGL). Try another browser or turn on hardware acceleration.'
101
- : 'A data file did not arrive. Check your connection and try again.';
102
- document.getElementById('loadErrWhy').textContent = why;
103
- box.hidden = false;
104
- document.getElementById('loader').classList.add('failed');
105
- }
106
- addEventListener('unhandledrejection', fail);
107
- addEventListener('error', function (e) { if (e.filename && /app\.js|tear\.js|lib\/|three/.test(e.filename)) fail(e); });
108
- })();
109
- </script>
110
 
111
  <header class="hud tl">
112
  <div class="mono dim kicker">PHERC. 0139 <span class="sep">/</span> HERCULANEUM <span class="sep">/</span> BURIED 79 CE</div>
113
  <h1>Unroll a scroll nobody has opened in <em>1,947 years.</em></h1>
114
- <p class="lede">37 of its windings have been traced through X-ray CT by the Vesuvius Challenge. Ink layers show where models detect ink; letters visible here are candidates for expert reading. Drag to look around. Scrub to unroll.</p>
115
- <div class="scroll-pick" id="scrollPick" hidden><label class="mono dim" for="scrollSel">SCROLL</label> <select id="scrollSel" aria-label="Choose a scroll" data-testid="scroll-picker" data-api="openScroll()"></select> <a class="prize-badge" id="prizeBadge" data-testid="prize-badge" data-api="links()" hidden href="https://scrollprize.org/prizes" target="_blank" rel="noopener" title="An open Vesuvius Challenge prize can be won on this scroll. See the prizes page">🏆 prize eligible</a> <button class="chip prize-only" id="prizeOnly" data-testid="prize-only" data-api="prizeOnly()" hidden aria-pressed="false" title="Show only scrolls marked prize eligible on scrollprize.org">🏆 prize eligible only</button></div>
116
  </header>
117
 
118
  <nav class="hud tr" aria-label="Layers">
119
- <div class="seg" role="tablist" aria-label="Layer">
120
- <button class="chip" data-layer="xray" role="tab" aria-selected="false" data-testid="layer-xray" data-api="setLayer('xray')" title="Raw X-ray CT brightness on each winding. No ink detection.">X‑ray</button>
121
- <button class="chip on" data-layer="ink" role="tab" aria-selected="true" data-testid="layer-ink" data-api="setLayer('ink')" title="Vesuvius Challenge's published ink detection on the fine 2.4 µm scan, covering all 37 windings.">Published ink (2.4 <span class="mu">µ</span>m)</button>
122
- <button class="chip gpu" data-layer="gpu" role="tab" aria-selected="false" id="gpuChip" data-testid="layer-overlay" data-api="setLayer('overlay')" hidden title="Lower resolution comparison. Chase Hendrick ran the public scrollprize/ink_9um model, which the Vesuvius Challenge trained, on the coarser 9.4 µm scan (about 4x coarser than 2.4 µm), for all 37 windings. It looks blurrier and shows patch seams. It is here to show what an open model can recover from cheaper, more widely available scans, as a reproducible baseline you can check against the published result."><i class="dot"></i>ink_9um (9 <span class="mu">µ</span>m)</button>
123
- </div>
124
- <p class="layer-hint" id="layerHint" aria-live="polite">Published ink (2.4 µm): Vesuvius Challenge detection, 2.4 µm scan, all windings.</p>
125
- <details class="layer-why" id="layerWhy"><summary data-testid="layer-why" data-api="panel('why')">Why two ink layers?</summary><p>Published ink is the Vesuvius Challenge result on the fine 2.4 µm scan. ink_9um (9 µm) comes from the public scrollprize/ink_9um model, which the Vesuvius Challenge trained; Chase Hendrick ran it on a scan about 4x coarser, so it is blurrier. It shows what an open model can recover from cheaper scans, as a baseline you can check.</p></details>
126
- <div class="seg tear-seg">
127
- <button class="chip" id="btnTear" data-testid="destroy-mode" data-api="destroy.mode()" aria-pressed="false" title="Destroy: tear, crush, burn or soak the scroll, rolled up or unrolled. Damage stays as you roll and unroll. Just for fun, not a real unrolling. (T)">Destroy</button>
128
- <button class="chip tool" id="toolTear" data-tool="tear" data-testid="destroy-tool-tear" data-api="destroy.tool('tear')" hidden aria-pressed="false" title="Drag across the papyrus to rip it (X)">Tear</button>
129
- <button class="chip tool" id="btnCrush" data-tool="crush" data-testid="destroy-tool-crush" data-api="destroy.tool('crush')" hidden aria-pressed="false" title="Press to crumble it; hold to press harder and go deeper into the roll (C)">Crush</button>
130
- <button class="chip tool" id="toolBurn" data-tool="burn" data-testid="destroy-tool-burn" data-api="destroy.tool('burn')" hidden aria-pressed="false" title="Click to ignite: embers creep slowly along the fibres (B)">Burn</button>
131
- <button class="chip tool" id="toolWater" data-tool="water" data-testid="destroy-tool-water" data-api="destroy.tool('water')" hidden aria-pressed="false" title="Press or hold to drip and pour water: it darkens, softens, weakens and slowly dissolves the papyrus (W)">Water</button>
132
- <button class="chip" id="btnReset" data-testid="destroy-reset" data-api="destroy.reset()" hidden title="Undo all damage and clear the debris">Reset</button>
133
- <button class="chip" id="btnCrumble" data-testid="destroy-crumble" data-api="destroy.crumble()" hidden title="Crumble the whole scroll: it breaks up from the edges and falls into a heap of dust. Reset restores it. (K)">Crumble</button>
134
- <button class="chip" id="btnSandbox" data-testid="destroy-sandbox" data-api="destroy.sandbox()" hidden aria-pressed="false" title="Lay the strip flat with full fracture physics: grab to rip, Crush to crumble">Sandbox</button>
135
- <button class="chip" id="btnShake" data-testid="destroy-shake" data-api="destroy.shake()" hidden title="Knock the table: loosens random bits">Shake</button>
136
- <button class="chip" id="btnSound" data-testid="destroy-sound" data-api="sound()" hidden aria-pressed="false" title="Soft crackle when it breaks">Sound off</button>
137
  </div>
138
  <div class="tools">
139
- <button class="icon" id="btnOpen" data-testid="open-own" data-api="panel('open')" title="Open your own scroll: files stay in your browser, nothing is uploaded" aria-label="Open your own scroll">+</button>
140
- <button class="icon" id="btnBench" data-testid="bench" data-api="bench.open()" title="Ink Bench: score your own ink prediction against the published ink. Runs in your browser, nothing is uploaded" aria-label="Ink Bench"><svg viewBox="0 0 24 24" width="15" height="15"><path d="M4 19V9M10 19V5M16 19v-7M21 19H3" fill="none" stroke="currentColor" stroke-width="1.6"/></svg></button>
141
- <button class="icon" id="btnInfo" data-testid="about" data-api="panel('about')" title="About this page" aria-label="About">i</button>
142
- <button class="icon" id="btnEmbed" data-testid="embed" data-api="panel('embed')" title="Embed this viewer on your site" aria-label="Embed this viewer">&lt;/&gt;</button>
143
- <button class="icon" id="btnFull" data-testid="fullscreen" data-api="fullscreen()" title="Fullscreen" aria-label="Fullscreen" aria-pressed="false">
144
- <svg viewBox="0 0 24 24" width="15" height="15"><path d="M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>
145
- </button>
146
- <button class="icon" id="btnShot" data-testid="screenshot" data-api="screenshot()" title="Save a picture" aria-label="Save a picture">
147
  <svg viewBox="0 0 24 24" width="15" height="15"><path d="M4 8h3l2-3h6l2 3h3v11H4z" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="12" cy="13" r="3.5" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>
148
  </button>
149
  </div>
@@ -153,17 +59,16 @@ if (!/(^|[#&])s=/.test(location.hash)) ['scrolls/index.json', 'scrolls/PHerc0139
153
  <div><span class="dim">WINDING</span> <b id="rWind">w059</b></div>
154
  <div><span class="dim">RADIUS</span> <b id="rRad">13.3 mm</b></div>
155
  <div><span class="dim">STRIP</span> <b id="rLen">0.00 m</b></div>
156
- <div id="rGpuRow" hidden><span class="dim">INK_9UM</span> <b id="rGpu">·</b></div>
157
- <button class="chip read" id="btnRead" data-testid="read-winding" data-api="focus()">Read w059</button>
158
  </aside>
159
 
160
  <footer class="hud bottom">
161
- <button id="play" aria-pressed="false" data-testid="play" data-api="play()" class="play" aria-label="Unroll">
162
  <svg viewBox="0 0 24 24" width="16" height="16" id="playIcon"><path d="M7 5l12 7-12 7z" fill="currentColor"/></svg>
163
  </button>
164
- <button id="btnRollUp" class="chip roll-up" data-testid="roll-up" data-api="rollUp()" aria-pressed="false" title="Roll it back up (U)">Roll up</button>
165
- <div class="scrub" id="scrub" data-testid="scrubber">
166
- <input type="range" id="scrubIn" class="scrub-in" min="0" max="1000" step="1" value="0" aria-label="Unroll position" data-testid="unroll-slider" data-api="setUnroll()">
167
  <div class="ticks" id="ticks"></div>
168
  <div class="track"><i id="fill"></i></div>
169
  <div class="knob" id="knob"></div>
@@ -175,88 +80,35 @@ if (!/(^|[#&])s=/.test(location.hash)) ['scrolls/index.json', 'scrolls/PHerc0139
175
 
176
  <div id="tip" class="tip mono" hidden></div>
177
 
178
- <section id="focus" class="sheet" role="dialog" aria-modal="false" aria-labelledby="fTitle" hidden data-testid="closeup-panel">
179
  <div class="sheet-head">
180
  <div>
181
  <div class="mono dim" id="fKicker">WINDING</div>
182
  <h2 id="fTitle">w046</h2>
183
  </div>
184
  <div class="sheet-tools">
185
- <div class="seg small" id="fSeg" role="tablist" aria-label="Close-up view"></div>
186
- <button class="icon" id="fClose" data-testid="closeup-close" data-api="focus(null)" aria-label="Close">✕</button>
187
  </div>
188
  </div>
189
- <div class="viewer" id="viewer" tabindex="0" role="img" aria-label="Close-up image. Plus and minus zoom, arrows pan, 0 fits. Drag or pinch also work." data-testid="closeup-image" data-api="closeup()"><img id="fImg" alt="Ink detection of this winding, flattened" draggable="false"><img id="fImg2" class="cmp" alt="The comparison layer for the same winding" draggable="false" hidden>
190
- <div class="swipe-line" id="fSwipeLine" hidden></div>
191
- <div class="scalebar mono" id="fScale" aria-hidden="true"><i></i><span>10 mm</span></div>
192
- <div class="img-err mono dim" id="fErr" hidden>This image could not be loaded.</div>
193
- <label class="swipe mono" id="fSwipeWrap" hidden data-testid="compare-slider-label" data-api="compare(true, pos)"><span id="fSwipeA">Published ink (2.4 <span class="mu">µ</span>m)</span> <input type="range" id="fSwipe" data-testid="compare-slider" data-api="compare(true, pos)" min="0" max="100" value="50" aria-label="Swipe between Published ink (2.4 µm) and ink_9um (9 µm)"> <span id="fSwipeB">ink_9um (9 <span class="mu">µ</span>m)</span></label>
194
- </div>
195
  <div class="sheet-foot mono dim" id="fMeta"></div>
196
  </section>
197
 
198
- <section id="openSheet" class="sheet about open-own" role="dialog" aria-modal="false" aria-labelledby="oTitle" hidden data-testid="open-panel">
199
- <div class="sheet-head">
200
- <div><div class="mono dim">OPEN YOUR OWN</div><h2 id="oTitle">Unroll your own scroll</h2></div>
201
- <button class="icon" id="oClose" data-testid="open-close" data-api="panel('open', false)" aria-label="Close">✕</button>
202
- </div>
203
- <div class="about-body">
204
- <p class="o-private"><b>Nothing is uploaded.</b> Your files are read by this page inside your browser and kept on this device (in browser storage) until you remove them. No server sees them.</p>
205
- <div class="o-drop" id="oDrop" tabindex="0" role="button" data-testid="open-drop" data-api="open.fromFiles()" aria-label="Drop files here or press Enter to pick files">
206
- <div class="o-drop-text">Drop a <b>zip</b>, or a <b>manifest.json</b> with its meshes and images, or a folder</div>
207
- <div class="o-drop-btns">
208
- <label class="chip" data-testid="open-pick-files-label" data-api="open.fromFiles()">Pick files<input type="file" id="oPick" aria-label="Pick files" data-testid="open-pick-files" data-api="open.fromFiles()" multiple accept=".zip,.json,.obj,.ply,.png,.jpg,.jpeg,.tif,.tiff" hidden></label>
209
- <label class="chip" data-testid="open-pick-folder-label" data-api="open.fromFiles()">Pick a folder<input type="file" id="oPickDir" aria-label="Pick a folder" data-testid="open-pick-folder" data-api="open.fromFiles()" webkitdirectory multiple hidden></label>
210
- </div>
211
- </div>
212
- <form class="o-url" id="oURLForm"><input type="url" id="oURL" placeholder="https://example.org/my-scroll/manifest.json or .zip" aria-label="URL of a manifest or zip" data-testid="open-url" data-api="open.fromURL()"><button class="chip" type="submit" data-testid="open-url-submit" data-api="open.fromURL()">Open URL</button></form>
213
- <p class="mono dim small">A URL is fetched by your browser straight from that server, which must allow cross-origin requests (CORS).</p>
214
- <div class="o-status mono" id="oStatus" role="status" aria-live="polite" hidden></div>
215
- <div id="oSaved" hidden><div class="mono dim">IN THIS BROWSER</div><ul class="o-list" id="oSavedList"></ul></div>
216
- <p>Meshes: OBJ or PLY with texture coordinates (UVs), one per winding. Layers: PNG, JPG or TIFF images laid out on those UVs: an X-ray layer, an ink layer and an optional comparison layer. The manifest says which file is which, the voxel size, and who made the data.</p>
217
- <p><button class="chip" id="oExample" data-testid="open-example" data-api="open.example()">Download example package</button> <a href="https://huggingface.co/spaces/SharpMeow/unroll/blob/main/docs/ADDING_A_SCROLL.md#open-your-own" target="_blank" rel="noopener">How to make one</a></p>
218
- <p class="mono dim small" id="oLimits"></p>
219
- </div>
220
- </section>
221
-
222
- <section id="about" class="sheet about" role="dialog" aria-modal="false" aria-labelledby="aTitle" hidden data-testid="about-panel">
223
  <div class="sheet-head">
224
- <div><div class="mono dim">ABOUT</div><h2 id="aTitle">What you&rsquo;re looking at</h2></div>
225
- <button class="icon" id="aClose" data-testid="about-close" data-api="panel('about', false)" aria-label="Close">✕</button>
226
  </div>
227
  <div class="about-body">
228
  <p>PHerc. 139 is one of the papyrus rolls carbonized when Vesuvius buried Herculaneum in 79 CE. It is too fragile to open, so it was scanned with X‑rays at the ESRF synchrotron in Grenoble. It is held by the Biblioteca Nazionale di Napoli.</p>
229
- <p>Each sheet in the 3D view is one winding of the roll, traced through the CT volume by the <a href="https://scrollprize.org" target="_blank" rel="noopener">Vesuvius Challenge</a>. The shapes are the real ones, from the public 9.362 µm meshes. The <b>X‑ray</b> layer is the CT intensity on each surface. The <b>Published ink (2.4 µm)</b> layer is the Vesuvius Challenge's public ink detection on the 2.4 µm scan. In 3D it is drawn through a local‑contrast filter so letter‑sized strokes stand out; the close‑up view shows the published preview unfiltered, downscaled to 2,400 px.</p>
230
  <p id="aboutGpu" hidden></p>
231
  <p>Unrolling lays the windings end to end, outermost first, the way a reader would open the roll. Each traced winding runs a little past one full turn, so roughly a tenth of the 2.8 m strip repeats surface from its neighbour.</p>
232
- <p class="mono dim small">Sources: open data bucket <code>PHerc0139/segments/*/mesh/*9.362um.tifxyz</code>, <code>surface-volumes/9.362um…zarr</code> and <code>ink-detection/downsampled</code>. Data: Giorgio Angelotti, Stephen Parsons, Sean Johnson, Elian Rafael Dal Prà, Johannes Rudolph, Paul Tafforeau, Alessandro Mirone, Paul Henderson, Hendrik Schilling, Forrest McDonald, David Josey, Youssef Nader, C. Seth Parker, W. Brent Seales. <i>Vesuvius Challenge - CT Scans of Herculaneum Papyri.</i> Vesuvius Challenge. Licensed <a href="https://creativecommons.org/licenses/by-nc/4.0/" target="_blank" rel="noopener">CC BY‑NC 4.0</a>. The page's code (HTML, CSS, JS and <code>tools/</code>) is Apache 2.0; see the Space's README.</p>
233
- <p class="mono dim small made-by">Made by Chase Hendrick.</p>
234
- </div>
235
- <div class="sheet-foot mono dim small about-foot"><span>For agents: <a href="llms.txt" target="_blank" rel="noopener">llms.txt</a> · <a href="AGENTS.md" target="_blank" rel="noopener">AGENTS.md</a> · <a href="api.json" target="_blank" rel="noopener">api.json</a></span> <span id="aVersion" class="version" data-testid="version" title="Version and build (CHANGELOG.md)"></span></div>
236
- </section>
237
-
238
- <section id="embedSheet" class="sheet about" role="dialog" aria-modal="false" aria-labelledby="eTitle" hidden data-testid="embed-panel">
239
- <div class="sheet-head">
240
- <div><div class="mono dim">EMBED</div><h2 id="eTitle">Put this viewer on your site</h2></div>
241
- <button class="icon" id="eClose" data-testid="embed-close" data-api="panel('embed', false)" aria-label="Close">✕</button>
242
- </div>
243
- <div class="about-body">
244
- <p>Paste this into your page. Visitors can unroll, scrub, switch layers and use every tool inside the frame; a click hands the mouse wheel to the viewer, so your page still scrolls normally.</p>
245
- <label class="mono small" data-testid="embed-option-state-label" data-api="embed.snippet({ state })"><input type="checkbox" id="embedState" checked data-testid="embed-option-state" data-api="embed.snippet({ state })"> Start at the current view</label>
246
- <label class="mono small" data-testid="embed-option-autoplay-label" data-api="embed.snippet({ autoplay })"><input type="checkbox" id="embedAutoplay" data-testid="embed-option-autoplay" data-api="embed.snippet({ autoplay })"> Autoplay the unroll</label>
247
- <label class="mono small" data-testid="embed-option-nocontrols-label" data-api="embed.snippet({ controls })"><input type="checkbox" id="embedNoControls" data-testid="embed-option-nocontrols" data-api="embed.snippet({ controls })"> Hide the controls</label>
248
- <textarea id="embedCode" class="embed-code mono" rows="5" readonly aria-label="Embed code" data-testid="embed-code" data-api="embed.snippet()"></textarea>
249
- <p><button class="chip" id="embedCopy" data-testid="embed-copy" data-api="embed.snippet()">Copy</button> <a id="embedPreviewURL" href="./" target="_blank" rel="noopener" data-testid="embed-preview" data-api="embed.snippet()">Preview the embed</a></p>
250
- <p class="mono dim small">oEmbed: <a href="oembed.json" target="_blank" rel="noopener">/oembed.json</a>. Host pages can drive the viewer with postMessage; see <a href="docs/EMBED.md" target="_blank" rel="noopener">EMBED.md</a>.</p>
251
  </div>
252
  </section>
253
 
254
- <script type="module" src="app.js?v=20261016vol3nan"></script>
255
- <script type="module" src="polish.js?v=20261016vol3"></script>
256
- <script type="module" src="lib/bench.js?v=20261016vol3"></script>
257
- <script type="module" src="ui.js?v=20261016vol3"></script>
258
- <script type="module" src="lib/ctvolume.js?v=20261016vol3"></script>
259
- <script type="module" src="lib/ctwl.js?v=20261016vol3"></script>
260
- <script type="module" src="lib/dragres.js?v=20261016vol3"></script>
261
  </body>
262
  </html>
 
4
  <meta charset="utf-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
  <title>Unroll · PHerc. 139</title>
7
+ <meta name="description" content="Unroll a carbonized Herculaneum scroll in your browser: 37 windings traced in X-ray CT, with the ink found by neural networks.">
8
  <meta property="og:title" content="Unroll · a Herculaneum scroll, in your browser">
9
+ <meta property="og:description" content="37 windings of PHerc. 139, traced in X-ray CT. Scrub to unroll it and watch the ink appear.">
10
  <meta property="og:image" content="https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/og.jpg">
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23060505'/%3E%3Cpath d='M16 6a10 10 0 1 0 0.01 0M16 10a6 6 0 1 1-0.01 0M16 14a2 2 0 1 0 0.01 0' fill='none' stroke='%23ffb347' stroke-width='1.6'/%3E%3C/svg%3E">
 
 
 
 
 
 
12
  <meta name="twitter:card" content="summary_large_image">
13
  <link rel="preconnect" href="https://fonts.googleapis.com">
14
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
15
  <link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
16
+ <link rel="stylesheet" href="style.css">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  <script type="importmap">
18
  { "imports": {
19
  "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
20
+ "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  } }
22
  </script>
23
  </head>
24
  <body>
25
+ <canvas id="gl"></canvas>
 
 
 
 
26
  <div class="scrim top" aria-hidden="true"></div><div class="scrim bot" aria-hidden="true"></div>
 
27
  <div class="grain" aria-hidden="true"></div>
28
 
29
  <div id="loader">
 
32
  <div class="loader-title">Reconstructing the scroll</div>
33
  <div class="bar"><i id="loadbar"></i></div>
34
  <div class="mono dim" id="loadtxt">0 MB</div>
 
 
 
 
35
  </div>
36
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
  <header class="hud tl">
39
  <div class="mono dim kicker">PHERC. 0139 <span class="sep">/</span> HERCULANEUM <span class="sep">/</span> BURIED 79 CE</div>
40
  <h1>Unroll a scroll nobody has opened in <em>1,947 years.</em></h1>
41
+ <p class="lede">37 of its windings have been traced through X‑ray CT by the Vesuvius Challenge, and neural networks found the ink on them. Drag to look around. Scrub to unroll.</p>
 
42
  </header>
43
 
44
  <nav class="hud tr" aria-label="Layers">
45
+ <div class="seg" role="tablist">
46
+ <button class="chip" data-layer="xray" role="tab">X‑ray</button>
47
+ <button class="chip on" data-layer="ink" role="tab">Ink</button>
48
+ <button class="chip gpu" data-layer="gpu" role="tab" id="gpuChip" hidden><i class="dot"></i>ZeroGPU</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  </div>
50
  <div class="tools">
51
+ <button class="icon" id="btnInfo" title="About this page" aria-label="About">i</button>
52
+ <button class="icon" id="btnShot" title="Save a picture" aria-label="Save a picture">
 
 
 
 
 
 
53
  <svg viewBox="0 0 24 24" width="15" height="15"><path d="M4 8h3l2-3h6l2 3h3v11H4z" fill="none" stroke="currentColor" stroke-width="1.5"/><circle cx="12" cy="13" r="3.5" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>
54
  </button>
55
  </div>
 
59
  <div><span class="dim">WINDING</span> <b id="rWind">w059</b></div>
60
  <div><span class="dim">RADIUS</span> <b id="rRad">13.3 mm</b></div>
61
  <div><span class="dim">STRIP</span> <b id="rLen">0.00 m</b></div>
62
+ <div id="rGpuRow" hidden><span class="dim">ZEROGPU</span> <b id="rGpu">·</b></div>
63
+ <button class="chip read" id="btnRead">Read w059</button>
64
  </aside>
65
 
66
  <footer class="hud bottom">
67
+ <button id="play" class="play" aria-label="Unroll">
68
  <svg viewBox="0 0 24 24" width="16" height="16" id="playIcon"><path d="M7 5l12 7-12 7z" fill="currentColor"/></svg>
69
  </button>
70
+ <div class="scrub" id="scrub">
71
+ <input type="range" id="scrubIn" class="scrub-in" min="0" max="1000" step="1" value="0" aria-label="Unroll position">
 
72
  <div class="ticks" id="ticks"></div>
73
  <div class="track"><i id="fill"></i></div>
74
  <div class="knob" id="knob"></div>
 
80
 
81
  <div id="tip" class="tip mono" hidden></div>
82
 
83
+ <section id="focus" class="sheet" hidden>
84
  <div class="sheet-head">
85
  <div>
86
  <div class="mono dim" id="fKicker">WINDING</div>
87
  <h2 id="fTitle">w046</h2>
88
  </div>
89
  <div class="sheet-tools">
90
+ <div class="seg small" id="fSeg"></div>
91
+ <button class="icon" id="fClose" aria-label="Close">✕</button>
92
  </div>
93
  </div>
94
+ <div class="viewer" id="viewer"><img id="fImg" alt="Ink detection of this winding, flattened" draggable="false"></div>
 
 
 
 
 
95
  <div class="sheet-foot mono dim" id="fMeta"></div>
96
  </section>
97
 
98
+ <section id="about" class="sheet about" hidden>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  <div class="sheet-head">
100
+ <div><div class="mono dim">ABOUT</div><h2>What you are looking at</h2></div>
101
+ <button class="icon" id="aClose" aria-label="Close">✕</button>
102
  </div>
103
  <div class="about-body">
104
  <p>PHerc. 139 is one of the papyrus rolls carbonized when Vesuvius buried Herculaneum in 79 CE. It is too fragile to open, so it was scanned with X‑rays at the ESRF synchrotron in Grenoble. It is held by the Biblioteca Nazionale di Napoli.</p>
105
+ <p>Each sheet in the 3D view is one winding of the roll, traced through the CT volume by the <a href="https://scrollprize.org" target="_blank" rel="noopener">Vesuvius Challenge</a>. The shapes are the real ones, from the public 9.362 µm meshes. The <b>X‑ray</b> layer is the CT intensity on each surface. The <b>Ink</b> layer is the Vesuvius Challenge's public ink detection on the 2.4 µm scan. In 3D it is drawn through a local‑contrast filter so letter‑sized strokes stand out; the close‑up view shows the published preview unfiltered, downscaled to 2,400 px.</p>
106
  <p id="aboutGpu" hidden></p>
107
  <p>Unrolling lays the windings end to end, outermost first, the way a reader would open the roll. Each traced winding runs a little past one full turn, so roughly a tenth of the 2.8 m strip repeats surface from its neighbour.</p>
108
+ <p class="mono dim small">Sources: open data bucket <code>PHerc0139/segments/*/mesh/*9.362um.tifxyz</code>, <code>surface-volumes/9.362um…zarr</code> and <code>ink-detection/downsampled</code>. Data: Giorgio Angelotti, Stephen Parsons, Sean Johnson, Elian Rafael Dal Prà, Johannes Rudolph, Paul Tafforeau, Alessandro Mirone, Paul Henderson, Hendrik Schilling, Forrest McDonald, David Josey, Youssef Nader, C. Seth Parker, W. Brent Seales. <i>Vesuvius Challenge - CT Scans of Herculaneum Papyri.</i> Vesuvius Challenge. Licensed <a href="https://creativecommons.org/licenses/by-nc/4.0/" target="_blank" rel="noopener">CC BY‑NC 4.0</a>. The page's code (HTML, CSS, JS and <code>tools/</code>) is MIT; see the Space's README.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  </div>
110
  </section>
111
 
112
+ <script type="module" src="app.js"></script>
 
 
 
 
 
 
113
  </body>
114
  </html>
og.jpg CHANGED

Git LFS Details

  • SHA256: f00ac2e22673ca1401e7f12478386e7b446926a8d3b989f61f85eb34ba9d7f71
  • Pointer size: 130 Bytes
  • Size of remote file: 96 kB

Git LFS Details

  • SHA256: 076ecfb22ea2788854e95360b354e81349a7343afded1513e591cbfb783480a5
  • Pointer size: 131 Bytes
  • Size of remote file: 117 kB
style.css CHANGED
@@ -171,120 +171,3 @@ button:focus-visible, a:focus-visible { outline: 2px solid var(--fg); outline-of
171
  }
172
 
173
  .credit { text-shadow: 0 0 8px #000, 0 0 2px #000; }
174
-
175
- .tr { flex-wrap: wrap; justify-content: flex-end; }
176
- .layer-hint { order: 3; flex-basis: 100%; margin: 6px 2px 0; font-size: 10.5px; line-height: 1.35; color: var(--dim); max-width: 300px; text-align: right; }
177
- .layer-why { order: 4; flex-basis: 100%; max-width: 300px; margin-left: auto; font-size: 10.5px; line-height: 1.35; color: var(--dim); text-align: right; }
178
- .layer-why summary { cursor: pointer; }
179
- .layer-why p { margin: 4px 0 0; }
180
- @media (max-width: 700px) { .layer-hint, .layer-why { display: none; } }
181
- .tear-seg { order: 5; margin-left: auto; margin-top: 6px; }
182
- body.tearing .bottom, body.tearing #readout { opacity: .35; pointer-events: none; }
183
- .load-err { margin-top: 14px; max-width: 340px; line-height: 1.5; color: var(--fg); }
184
- .load-err p { margin: 0 0 10px; }
185
- #loader.failed .bar { opacity: .3; }
186
- .viewer img.cmp { pointer-events: none; }
187
- .swipe-line { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--gpu); box-shadow: 0 0 6px var(--gpu); pointer-events: none; }
188
- .swipe { position: absolute; left: 50%; bottom: 14px; transform: translateX(-50%); display: flex; gap: 10px; align-items: center; font-size: 11px; padding: 6px 12px; background: rgba(0,0,0,.6); border: 1px solid var(--line); color: var(--fg); }
189
- .swipe input { width: 180px; }
190
- .scalebar { position: absolute; left: 16px; bottom: 16px; font-size: 10.5px; color: var(--fg); pointer-events: none; text-shadow: 0 0 4px #000; }
191
- .scalebar i { display: block; height: 4px; border: 1px solid var(--fg); border-top: 0; margin-bottom: 4px; }
192
- .img-err { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
193
- .layer-why summary { display: block; text-align: right; }
194
- [hidden] { display: none !important; }
195
-
196
- /* polish: vignette over the 3D view, under the HUD */
197
- .vignette { position: fixed; inset: 0; z-index: 1; pointer-events: none; background: radial-gradient(ellipse 85% 80% at 55% 52%, rgba(0,0,0,0) 55%, rgba(0,0,0,.55) 100%); }
198
- body.shot .vignette { display: none; }
199
-
200
- /* polish: keyboard help */
201
- .help { inset: auto auto var(--gut) var(--gut); width: min(440px, calc(100vw - 2 * var(--gut))); max-height: calc(100vh - 2 * var(--gut)); }
202
- .keys { display: grid; grid-template-columns: auto 1fr; gap: 9px 16px; margin: 0; padding: 16px 20px 6px; overflow: auto; font-size: 11.5px; color: #c9c1b5; }
203
- .keys dt { white-space: nowrap; color: var(--dim); }
204
- .keys dd { margin: 0; }
205
- kbd { display: inline-block; min-width: 20px; padding: 2px 6px; border: 1px solid var(--line-strong); border-bottom-width: 2px; font: inherit; color: var(--fg); text-align: center; }
206
- .help-foot { padding: 4px 20px 16px; margin: 0; }
207
- @media (pointer: coarse) { #btnHelp { display: none; } }
208
-
209
- /* polish: phones, keep the layer switch, the read button and the scrubber from overlapping */
210
- @media (max-width: 720px) {
211
- .tr { left: var(--gut); justify-content: space-between; row-gap: 8px; }
212
- .tr > .seg[role=tablist] { flex: 1 1 100%; }
213
- .tr > .seg[role=tablist] .chip { flex: 1 1 0; min-width: 0; padding-left: 6px; padding-right: 6px; font-size: 10px; letter-spacing: .06em; line-height: 1.25; }
214
- .tear-seg { margin-top: 0; order: 2; }
215
- .tools { order: 1; }
216
- .bl { bottom: calc(var(--gut) + 64px + 44px + 8px + 44px + 14px); }
217
- .tear-seg { flex-wrap: wrap; justify-content: flex-end; }
218
- }
219
- /* polish: a flex-wrap row sizes itself to all its items side by side, which pushed the layer switch over the title */
220
- @media (min-width: 721px) { .tr { max-width: 600px; } }
221
- /* scroll picker and "Open your own" */
222
- .scroll-pick { display: flex; align-items: center; gap: 8px; margin-top: 12px; pointer-events: auto; }
223
- .scroll-pick select { background: rgba(12,10,9,0.8); color: #ece6db; border: 1px solid rgba(255,255,255,0.18); border-radius: 999px; padding: 5px 10px; font: inherit; font-size: 12px; max-width: 240px; }
224
- .local-tag { color: #7fd8ff; }
225
- .open-own .o-private { border-left: 2px solid #7fd8ff; padding-left: 10px; }
226
- .o-drop { border: 1.5px dashed rgba(255,255,255,0.28); border-radius: 12px; padding: 22px 16px; text-align: center; cursor: pointer; margin: 10px 0; transition: background .2s, border-color .2s; }
227
- .o-drop.over, .o-drop:focus-visible { background: rgba(255,179,71,0.08); border-color: #ffb347; outline: none; }
228
- .o-drop-btns { display: flex; gap: 8px; justify-content: center; margin-top: 12px; flex-wrap: wrap; }
229
- .o-drop-btns label { cursor: pointer; }
230
- .o-url { display: flex; gap: 8px; margin-top: 8px; }
231
- .o-url input { flex: 1; min-width: 0; background: rgba(0,0,0,0.4); color: inherit; border: 1px solid rgba(255,255,255,0.2); border-radius: 8px; padding: 7px 10px; font: inherit; font-size: 13px; }
232
- .o-status { padding: 10px 12px; border-radius: 8px; font-size: 12px; margin: 8px 0; background: rgba(255,255,255,0.05); }
233
- .o-status.err { background: rgba(255,80,60,0.12); color: #ffb4a8; }
234
- .o-status.ok { background: rgba(80,200,255,0.1); color: #a8e6ff; }
235
- .o-status ul { margin: 6px 0 0; padding-left: 18px; }
236
- .o-list { list-style: none; padding: 0; margin: 6px 0 12px; }
237
- .o-list li { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px solid rgba(255,255,255,0.08); }
238
- .o-list li > span:last-child { display: flex; gap: 6px; }
239
- .mu { text-transform: none; }
240
- @media (min-width: 721px) { .hud.tr .seg[role=tablist] .chip { letter-spacing: .07em; padding: 8px 11px; } }
241
-
242
- /* Destroy mode: tool cursors (a fist for Crush, squeezed while pressed; a flame for Burn; a droplet for Water) */
243
- body.destroy.tool-tear #gl { cursor: crosshair; }
244
- body.destroy.tool-crush #gl { cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cg fill='%23f3e6cf' stroke='%231a120c' stroke-width='1.6' stroke-linejoin='round' stroke-linecap='round'%3E%3Cpath d='M6 13c0-2 1.4-3.2 3-3.2s2.8 1.1 2.8 2.6c0-1.9 1.3-3 2.9-3s2.8 1.1 2.8 2.8c.1-1.6 1.4-2.6 2.9-2.6 1.6 0 2.8 1.2 2.8 2.9v.9c.4-.7 1.2-1.1 2-1.1 1.4 0 2.3 1 2.3 2.5v5.2c0 4.7-3.7 8.1-8.6 8.1h-3.4C10 28.1 6 24.6 6 20.4z'/%3E%3Cpath d='M11.8 12.4v3.6M17.5 12.2v3.6M23.2 12.5v3.4M6.4 17.4c2.3-1.6 5.4-1.6 7.4.4' fill='none'/%3E%3C/g%3E%3C/svg%3E") 16 11, pointer; }
245
- body.destroy.tool-crush.pressing #gl, body.destroy.tool-crush #gl:active { cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='-1 -4 34 38'%3E%3Cg fill='%23f3e6cf' stroke='%231a120c' stroke-width='1.6' stroke-linejoin='round' stroke-linecap='round'%3E%3Cpath d='M6 13c0-2 1.4-3.2 3-3.2s2.8 1.1 2.8 2.6c0-1.9 1.3-3 2.9-3s2.8 1.1 2.8 2.8c.1-1.6 1.4-2.6 2.9-2.6 1.6 0 2.8 1.2 2.8 2.9v.9c.4-.7 1.2-1.1 2-1.1 1.4 0 2.3 1 2.3 2.5v5.2c0 4.7-3.7 8.1-8.6 8.1h-3.4C10 28.1 6 24.6 6 20.4z'/%3E%3Cpath d='M11.8 12.4v3.6M17.5 12.2v3.6M23.2 12.5v3.4M6.4 17.4c2.3-1.6 5.4-1.6 7.4.4' fill='none'/%3E%3C/g%3E%3C/svg%3E") 16 12, pointer; }
246
- body.destroy.tool-burn #gl { cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cpath d='M16 3c1 5 7.5 8 7.5 15.5a7.5 7.5 0 0 1-15 0c0-4.2 2-6.5 3.2-8.6.4 3 1.8 4.3 3 4.4-1-4.2 0-8.3 1.3-11.3z' fill='%23ff8a2a' stroke='%231a120c' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M16 15c.6 3 3.6 4.4 3.6 7.6a3.6 3.6 0 0 1-7.2 0c0-2.2 2-4.4 3.6-7.6z' fill='%23ffd56a'/%3E%3C/svg%3E") 16 4, pointer; }
247
- body.destroy.tool-water #gl { cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cpath d='M16 3C12.4 9.6 8 13.8 8 19.4a8 8 0 0 0 16 0C24 13.8 19.6 9.6 16 3z' fill='%2378b8e8' stroke='%230c1620' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M12.4 19.6a3.8 3.8 0 0 0 3 3.6' fill='none' stroke='%23e8f6ff' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E") 16 27, pointer; }
248
- .touch-pop { position: fixed; z-index: 8; width: 44px; height: 44px; margin: -22px 0 0 -22px; pointer-events: none; background: center / contain no-repeat; animation: pop .65s ease-out forwards; }
249
- .touch-pop.crush { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='-1 -4 34 38'%3E%3Cg fill='%23f3e6cf' stroke='%231a120c' stroke-width='1.6' stroke-linejoin='round' stroke-linecap='round'%3E%3Cpath d='M6 13c0-2 1.4-3.2 3-3.2s2.8 1.1 2.8 2.6c0-1.9 1.3-3 2.9-3s2.8 1.1 2.8 2.8c.1-1.6 1.4-2.6 2.9-2.6 1.6 0 2.8 1.2 2.8 2.9v.9c.4-.7 1.2-1.1 2-1.1 1.4 0 2.3 1 2.3 2.5v5.2c0 4.7-3.7 8.1-8.6 8.1h-3.4C10 28.1 6 24.6 6 20.4z'/%3E%3Cpath d='M11.8 12.4v3.6M17.5 12.2v3.6M23.2 12.5v3.4M6.4 17.4c2.3-1.6 5.4-1.6 7.4.4' fill='none'/%3E%3C/g%3E%3C/svg%3E"); }
250
- .touch-pop.burn { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cpath d='M16 3c1 5 7.5 8 7.5 15.5a7.5 7.5 0 0 1-15 0c0-4.2 2-6.5 3.2-8.6.4 3 1.8 4.3 3 4.4-1-4.2 0-8.3 1.3-11.3z' fill='%23ff8a2a' stroke='%231a120c' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M16 15c.6 3 3.6 4.4 3.6 7.6a3.6 3.6 0 0 1-7.2 0c0-2.2 2-4.4 3.6-7.6z' fill='%23ffd56a'/%3E%3C/svg%3E"); }
251
- .touch-pop.water { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cpath d='M16 3C12.4 9.6 8 13.8 8 19.4a8 8 0 0 0 16 0C24 13.8 19.6 9.6 16 3z' fill='%2378b8e8' stroke='%230c1620' stroke-width='1.5' stroke-linejoin='round'/%3E%3Cpath d='M12.4 19.6a3.8 3.8 0 0 0 3 3.6' fill='none' stroke='%23e8f6ff' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E"); }
252
- .touch-pop.tear { display: none; }
253
- @keyframes pop { 0% { transform: scale(.6); opacity: 0 } 25% { transform: scale(1.05); opacity: 1 } 100% { transform: scale(.9); opacity: 0 } }
254
- .chip.tool.on { background: var(--fg); color: var(--bg); }
255
- .chip:disabled { opacity: .35; cursor: default; }
256
- .roll-up { flex: none; border: 1px solid var(--line-strong); background: var(--panel); color: var(--fg); min-height: 34px; }
257
- .roll-up.on { background: var(--ink); color: var(--bg); border-color: var(--ink); }
258
- @media (max-width: 720px) { .roll-up { padding: 8px; font-size: 10px; } }
259
- @media (max-width: 720px) { body.destroy #readout { display: none; } }
260
-
261
- .prize-badge { font-size: 11px; color: #f2c66d; border: 1px solid rgba(242,198,109,0.45); border-radius: 999px; padding: 3px 9px; text-decoration: none; white-space: nowrap; }
262
- .prize-badge:hover { background: rgba(242,198,109,0.12); }
263
- .scroll-pick .prize-only { font-size: 11px; padding: 3px 9px; }
264
- .scroll-pick .prize-only.on { border-color: rgba(242,198,109,0.7); color: #f2c66d; }
265
- .scroll-pick { flex-wrap: wrap; }
266
- .sr-only { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
267
- #gl:focus-visible, .viewer:focus-visible { outline: 2px solid var(--ink); outline-offset: -2px; }
268
- .about-foot { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; opacity: .75; }
269
- .about-foot a { color: inherit; }
270
- .about-foot .version { letter-spacing: .02em; }
271
-
272
- /* ---------------------------------------------------------------- embed mode (?embed=1) */
273
- #btnFull, .embed-full, .embed-gate { display: none; }
274
- .embed #btnFull { display: inline-flex; }
275
- .embed .embed-full { display: block; position: fixed; left: 14px; top: 56px; z-index: 30; font-size: 11px; color: var(--dim, #9a8f80); text-decoration: none; padding: 4px 8px; border-radius: 999px; background: rgba(6, 5, 5, .6); }
276
- .embed .embed-full:hover, .embed .embed-full:focus-visible { color: #fff; }
277
- .embed .embed-gate:not([hidden]) { display: flex; position: fixed; inset: 0; z-index: 5; align-items: center; justify-content: center; border: 0; background: transparent; color: inherit; cursor: pointer; touch-action: pan-x pan-y; }
278
- .embed .embed-gate span { padding: 8px 14px; border-radius: 999px; background: rgba(6, 5, 5, .72); border: 1px solid rgba(255, 255, 255, .18); font-size: 12px; letter-spacing: .06em; }
279
- .embed .embed-gate:hover span, .embed .embed-gate:focus-visible span { border-color: rgba(255, 255, 255, .5); }
280
- .embed .hud.tl .kicker, .embed .hud.tl .lede, .embed .vignette { display: none; }
281
- .embed .hud.tl h1 { font-size: clamp(16px, 4.2vw, 24px); line-height: 1.1; max-width: 60vw; }
282
- .embed .layer-hint, .embed .layer-why { display: none; }
283
- .embed .readout { display: none; }
284
- .embed-nocontrols .hud, .embed-nocontrols .scrub, .embed-nocontrols .dock { display: none !important; }
285
- @media (max-height: 420px) { .embed .hud.tl h1 { display: none; } }
286
- .embed-code { width: 100%; box-sizing: border-box; background: rgba(255, 255, 255, .05); color: inherit; border: 1px solid rgba(255, 255, 255, .15); border-radius: 8px; padding: 8px; font-size: 11px; resize: vertical; }
287
- #embedSheet label { display: block; margin: 4px 0; }
288
-
289
- .toast { position: fixed; left: 50%; bottom: 84px; transform: translateX(-50%); z-index: 60; max-width: min(560px, 90vw); padding: 8px 14px; border-radius: 10px; background: rgba(6, 5, 5, .88); border: 1px solid rgba(255, 255, 255, .18); font-size: 12px; line-height: 1.4; }
290
- .toast[hidden] { display: none; }
 
171
  }
172
 
173
  .credit { text-shadow: 0 0 8px #000, 0 0 2px #000; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tools/build.py CHANGED
@@ -8,12 +8,6 @@ Outputs:
8
  scroll.json manifest: per-winding grid sizes, byte offsets, physical sizes, atlas rects, sources
9
  atlas_ink.jpg, atlas_ct.jpg every winding packed at one physical scale (grayscale)
10
  hi/wNNN.jpg per-winding Vesuvius Challenge ink preview for the close-up view
11
-
12
- Coarseness: UNROLL_STEP sets the grid step of a full build (default 3). A lighter copy of a built mesh, for phones
13
- and low-power devices, needs no raw downloads:
14
- python tools/build.py --coarsen 2 data/PHerc0172 writes scroll_lite.bin, scroll_lite.json, scroll_lite.i16p.gz
15
- It keeps every 2nd sampled row and column (plus the last ones), so the UV mapping (rowIdx, colIdx over the same
16
- gridH, gridW), the atlases, the ink layers and the flat layout are unchanged.
17
  """
18
  import glob
19
  import http.client
@@ -34,15 +28,11 @@ from PIL import Image
34
  ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
35
  RAW = os.environ.get("UNROLL_RAW", f"{ROOT}/raw")
36
  OUT = os.environ.get("UNROLL_OUT", f"{ROOT}/data")
37
- B = "https://vesuvius-challenge-open-data.s3.amazonaws.com"
38
- SCROLL = os.environ.get("UNROLL_SCROLL", "PHerc0139")
39
- VOX_MM = float(os.environ.get("UNROLL_VOX_MM", "0.009362")) # scan voxel of the meshes (9.362 um for PHerc0139)
40
  CELL_MM = 20 * VOX_MM # tifxyz scale 0.05: one grid cell is 20 voxels
41
- CT_RES = os.environ.get("UNROLL_CT_RES", "9.362um") # surface volume for the X-ray layer (any scan: it is a UV texture)
42
- NO_INK = os.environ.get("UNROLL_NO_INK") == "1"
43
- RENUMBER = os.environ.get("UNROLL_RENUMBER") == "1" # name the windings w001.. by radius, innermost first
44
- STEP = int(os.environ.get("UNROLL_STEP", "3")) # grid decimation for the browser mesh (larger is coarser)
45
- PPMM = float(os.environ.get("UNROLL_PPMM", "7.5")) # atlas pixels per mm (lowered for long scrolls)
46
  ATLAS = 4096
47
  PAD = 6
48
  HI_MAX = 2400
@@ -53,26 +43,13 @@ Image.MAX_IMAGE_PIXELS = None
53
 
54
  def fetch_ct(w, src, shape_hw):
55
  """Level-3 X-ray texture (mean of the middle layers) of a winding's 9.362 um surface volume."""
56
- cache = f"{RAW}/{src.get('w', w)}/ct_l3.npy"
57
  if os.path.exists(cache):
58
  return np.load(cache)
59
  seg = src["seg"]
60
- sv = [s for s in src["sv"] if s.startswith(CT_RES) and "-L1" not in s and s.endswith(".zarr")][0]
61
- root = f"{B}/{SCROLL}/segments/{seg}/surface-volumes/{sv}"
62
- # the level nearest the grid's size (level 3 for 9 um scans; finer scans need a coarser level)
63
- lvl = 3
64
- for lv in range(3, 7):
65
- try:
66
- mt = json.loads(urllib.request.urlopen(f"{root}/{lv}/.zarray").read())
67
- except urllib.error.HTTPError:
68
- break
69
- lvl = lv
70
- if mt["shape"][1] <= shape_hw[0] * 8:
71
- break
72
- base = f"{root}/{lvl}"
73
  meta = json.loads(urllib.request.urlopen(f"{base}/.zarray").read())
74
- comp = meta.get("compressor")
75
- assert comp is None or comp.get("id") == "blosc", f"compressed zarr not handled: {comp}"
76
  D, H, W = meta["shape"]
77
  cz, ch, cw = meta["chunks"]
78
  sep = meta.get("dimension_separator") or "."
@@ -88,9 +65,6 @@ def fetch_ct(w, src, shape_hw):
88
  body = r.stdout[:-3]
89
  if code in (b"403", b"404"):
90
  return ij, None
91
- if code == b"200" and comp:
92
- import imagecodecs
93
- body = imagecodecs.blosc_decode(body)
94
  if code == b"200" and len(body) == cz * ch * cw:
95
  raw = body
96
  break
@@ -98,8 +72,7 @@ def fetch_ct(w, src, shape_hw):
98
  else:
99
  raise OSError(f"could not fetch {url}")
100
  a = np.frombuffer(raw, np.uint8).reshape(cz, ch, cw)
101
- lo = CT_LAYERS[0] if cz >= 28 else max(0, cz // 2 - 3)
102
- return ij, a[lo:lo + (CT_LAYERS[1] - CT_LAYERS[0])].astype(np.float32).mean(0)
103
 
104
  with ThreadPoolExecutor(4) as ex:
105
  for (i, j), tile in ex.map(one, jobs):
@@ -154,7 +127,7 @@ def shelf_pack(sizes, width):
154
 
155
  def main():
156
  os.makedirs(f"{OUT}/hi", exist_ok=True)
157
- dirs = sorted(glob.glob(f"{RAW}/[ws]*[0-9]"))
158
  names = [os.path.basename(d) for d in dirs]
159
  grids, srcs, metas = [], [], []
160
  for d in dirs:
@@ -163,33 +136,7 @@ def main():
163
  grids.append((x, y, z, v))
164
  srcs.append(json.load(open(f"{d}/source.json")))
165
  metas.append(json.load(open(f"{d}/meta.json")))
166
- if RENUMBER:
167
- # order by median distance from a first axis fit through all windings: the innermost becomes w001
168
- allg = [np.concatenate([g[i][g[3]] for g in grids]) for i in range(3)]
169
- ax0, ay0 = np.polyfit(allg[2], allg[0], 1), np.polyfit(allg[2], allg[1], 1)
170
- rad = [float(np.median(np.hypot(g[0][g[3]] - np.polyval(ax0, g[2][g[3]]), g[1][g[3]] - np.polyval(ay0, g[2][g[3]]))))
171
- for g in grids]
172
- order = np.argsort(rad)
173
- grids, srcs, metas, dirs = ([L[i] for i in order] for L in (grids, srcs, metas, dirs))
174
- names = [f"w{k + 1:03d}" for k in range(len(dirs))]
175
- print("renumbered by radius:", [(n, srcs[k]["seg"], round(rad[order[k]])) for k, n in enumerate(names)])
176
  ax, ay = axis_fit(grids)
177
- rad2 = [float(np.median(np.hypot(g[0][g[3]] - np.polyval(ax, g[2][g[3]]), g[1][g[3]] - np.polyval(ay, g[2][g[3]])))) * VOX_MM
178
- for g in grids]
179
- if RENUMBER:
180
- # second pass on the refined axis, so the ids follow the radius exactly
181
- order = np.argsort(rad2)
182
- grids, srcs, metas, dirs = ([L[i] for i in order] for L in (grids, srcs, metas, dirs))
183
- rad2 = [rad2[i] for i in order]
184
- ax, ay = axis_fit(grids)
185
- print("winding order check (id, median radius mm):", [(n, round(r, 1)) for n, r in zip(names, rad2)])
186
- bad = sum(1 for a, b in zip(rad2, rad2[1:]) if b < a)
187
- print("radius decreases between consecutive ids:", bad)
188
- global PPMM
189
- if "UNROLL_PPMM" not in os.environ:
190
- cells = sum(g[3].shape[0] * g[3].shape[1] for g in grids) * CELL_MM * CELL_MM
191
- PPMM = min(PPMM, float(np.sqrt(0.62 * ATLAS * ATLAS / cells)))
192
- print("PPMM", round(PPMM, 3))
193
  allz = np.concatenate([g[2][g[3]] for g in grids])
194
  zc = float((allz.min() + allz.max()) / 2)
195
  cx0, cy0 = float(np.polyval(ax, zc)), float(np.polyval(ay, zc))
@@ -201,21 +148,11 @@ def main():
201
  H, W = v.shape
202
  tw, th = int(round(W * CELL_MM * PPMM)), int(round(H * CELL_MM * PPMM))
203
  sizes[w] = (tw, th)
204
- if not NO_INK:
205
- ink = Image.open(f"{d}/ink2um_ds8.jpg").convert("L")
206
- ink_tiles[w] = local_contrast(ink.resize((tw, th), Image.LANCZOS), 2.2 * PPMM)
207
- hi = ink.copy()
208
- hi.thumbnail((HI_MAX, HI_MAX), Image.LANCZOS)
209
- hi.save(f"{OUT}/hi/{w}.jpg", quality=84, optimize=True, progressive=True)
210
- if SCROLL != "PHerc0139" and os.path.exists(f"{d}/ink1um_ds8.jpg"):
211
- # second published ink layer (1.129 um scan): a per-winding overlay texture plus a close-up
212
- os.makedirs(f"{OUT}/ink1", exist_ok=True)
213
- i1 = Image.open(f"{d}/ink1um_ds8.jpg").convert("L")
214
- h1 = i1.copy()
215
- h1.thumbnail((HI_MAX, HI_MAX), Image.LANCZOS)
216
- h1.save(f"{OUT}/hi/{w}_1um.jpg", quality=84, optimize=True, progressive=True)
217
- i1.thumbnail((1200, 1200), Image.LANCZOS)
218
- i1.save(f"{OUT}/ink1/{w}.jpg", quality=80, optimize=True, progressive=True)
219
  ct = fetch_ct(w, src, (H, W))
220
  valid = ct > 0
221
  lo, hi_ = np.percentile(ct[valid], [1.5, 99.5]) if valid.any() else (0, 1)
@@ -227,15 +164,12 @@ def main():
227
  if used > ATLAS:
228
  sys.exit(f"atlas overflow: {used} px > {ATLAS}; lower PPMM")
229
  atlas_h = 1 << int(np.ceil(np.log2(used)))
230
- atlases = {"ct": Image.new("L", (ATLAS, atlas_h), 0)}
231
- if not NO_INK:
232
- atlases["ink"] = Image.new("L", (ATLAS, atlas_h), 0)
233
  for w in names:
234
- for k in atlases:
235
- atlases[k].paste((ink_tiles if k == "ink" else ct_tiles)[w], pos[w])
236
  for k, im in atlases.items():
237
  im.save(f"{OUT}/atlas_{k}.jpg", quality=88, optimize=True, progressive=True)
238
- im.save(f"{OUT}/atlas_{k}.webp", quality=82, method=6)
239
  print("atlas", ATLAS, atlas_h, "used rows", used)
240
 
241
  # meshes
@@ -253,9 +187,7 @@ def main():
253
  px = np.where(vs, xs - cx0, 0)
254
  py = np.where(vs, ys - cy0, 0)
255
  pz = np.where(vs, zs - zc, 0)
256
- st = np.stack([px, py, pz], -1).round()
257
- assert np.abs(st).max() < 32767, "coordinates overflow int16: use a coarser mesh"
258
- arr = st.astype(np.int16)
259
  arr[~vs] = -32768
260
  b = arr.tobytes()
261
  blobs.append(b)
@@ -270,16 +202,17 @@ def main():
270
  "gridH": int(H), "gridW": int(W),
271
  "offset": offset, "bytes": len(b),
272
  "widthMM": round(W * CELL_MM, 2), "heightMM": round(H * CELL_MM, 2),
273
- "areaCM2": round((meta["area_vx2"] * VOX_MM * VOX_MM if "area_vx2" in meta else v.sum() * CELL_MM * CELL_MM) / 100, 2),
274
  "radiusMM": round(float(np.median(r)), 2),
275
  "atlas": [x0 / ATLAS, y0 / atlas_h, (x0 + tw) / ATLAS, (y0 + th) / atlas_h],
276
- **({} if NO_INK else {"hi": f"hi/{w}.jpg", "ink_source": src.get("ink_ds") or [f for f, _ in src["ink"] if "2.399um" in f][0]}),
 
277
  "mesh_source": src["mesh"],
278
  })
279
  offset += len(b)
280
  open(f"{OUT}/scroll.bin", "wb").write(b"".join(blobs))
281
  manifest = {
282
- "scroll": SCROLL,
283
  "voxelMM": VOX_MM, "cellMM": CELL_MM,
284
  "axis": {"centre": [cx0, cy0, zc], "dxdz": ax[0], "dydz": ay[0]},
285
  "atlas": {"w": ATLAS, "h": atlas_h, "ppmm": PPMM},
@@ -287,41 +220,11 @@ def main():
287
  "totalWidthMM": round(sum(wi["widthMM"] for wi in wins), 1),
288
  "totalAreaCM2": round(sum(wi["areaCM2"] for wi in wins), 1),
289
  "vertices": int(offset // 6),
290
- "step": STEP,
291
  }
292
  json.dump(manifest, open(f"{OUT}/scroll.json", "w"), separators=(",", ":"))
293
  print("vertices", manifest["vertices"], "bin MB", offset / 1e6, "strip m", manifest["totalWidthMM"] / 1000,
294
  "area cm2", manifest["totalAreaCM2"])
295
 
296
 
297
- def coarsen(folder, k, name="scroll_lite"):
298
- """Keep every k-th sampled row and column of each winding (always the first and last) of a built mesh."""
299
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
300
- from pack_bin import pack
301
- man = json.load(open(f"{folder}/scroll.json"))
302
- raw = np.fromfile(f"{folder}/scroll.bin", "<i2")
303
- blobs, offset, wins = [], 0, []
304
- for w in man["windings"]:
305
- R, C = w["rows"], w["cols"]
306
- a = raw[w["offset"] // 2:(w["offset"] + w["bytes"]) // 2].reshape(R, C, 3)
307
- ri = sorted(set(range(0, R, k)) | {R - 1})
308
- ci = sorted(set(range(0, C, k)) | {C - 1})
309
- b = np.ascontiguousarray(a[np.ix_(ri, ci)]).astype("<i2").tobytes()
310
- blobs.append(b)
311
- wins.append({**w, "rows": len(ri), "cols": len(ci), "rowIdx": [w["rowIdx"][i] for i in ri],
312
- "colIdx": [w["colIdx"][i] for i in ci], "offset": offset, "bytes": len(b)})
313
- offset += len(b)
314
- data = b"".join(blobs)
315
- out = {**man, "windings": wins, "vertices": int(offset // 6), "step": man.get("step", 3) * k}
316
- open(f"{folder}/{name}.bin", "wb").write(data)
317
- open(f"{folder}/{name}.i16p.gz", "wb").write(pack(data))
318
- json.dump(out, open(f"{folder}/{name}.json", "w"), separators=(",", ":"))
319
- print("vertices", man["vertices"], "->", out["vertices"], "bin bytes", len(raw) * 2, "->", len(data),
320
- "packed", os.path.getsize(f"{folder}/{name}.i16p.gz"))
321
-
322
-
323
  if __name__ == "__main__":
324
- if len(sys.argv) == 4 and sys.argv[1] == "--coarsen":
325
- coarsen(sys.argv[3].rstrip("/"), int(sys.argv[2]))
326
- else:
327
- main()
 
8
  scroll.json manifest: per-winding grid sizes, byte offsets, physical sizes, atlas rects, sources
9
  atlas_ink.jpg, atlas_ct.jpg every winding packed at one physical scale (grayscale)
10
  hi/wNNN.jpg per-winding Vesuvius Challenge ink preview for the close-up view
 
 
 
 
 
 
11
  """
12
  import glob
13
  import http.client
 
28
  ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
29
  RAW = os.environ.get("UNROLL_RAW", f"{ROOT}/raw")
30
  OUT = os.environ.get("UNROLL_OUT", f"{ROOT}/data")
31
+ B = "https://vesuvius-challenge-open-data.s3.us-east-1.amazonaws.com"
32
+ VOX_MM = 0.009362 # 9.362 um scan voxel
 
33
  CELL_MM = 20 * VOX_MM # tifxyz scale 0.05: one grid cell is 20 voxels
34
+ STEP = 3 # grid decimation for the browser mesh
35
+ PPMM = 7.5 # atlas pixels per mm
 
 
 
36
  ATLAS = 4096
37
  PAD = 6
38
  HI_MAX = 2400
 
43
 
44
  def fetch_ct(w, src, shape_hw):
45
  """Level-3 X-ray texture (mean of the middle layers) of a winding's 9.362 um surface volume."""
46
+ cache = f"{RAW}/{w}/ct_l3.npy"
47
  if os.path.exists(cache):
48
  return np.load(cache)
49
  seg = src["seg"]
50
+ sv = [s for s in src["sv"] if s.startswith("9.362um")][0]
51
+ base = f"{B}/PHerc0139/segments/{seg}/surface-volumes/{sv}/3"
 
 
 
 
 
 
 
 
 
 
 
52
  meta = json.loads(urllib.request.urlopen(f"{base}/.zarray").read())
 
 
53
  D, H, W = meta["shape"]
54
  cz, ch, cw = meta["chunks"]
55
  sep = meta.get("dimension_separator") or "."
 
65
  body = r.stdout[:-3]
66
  if code in (b"403", b"404"):
67
  return ij, None
 
 
 
68
  if code == b"200" and len(body) == cz * ch * cw:
69
  raw = body
70
  break
 
72
  else:
73
  raise OSError(f"could not fetch {url}")
74
  a = np.frombuffer(raw, np.uint8).reshape(cz, ch, cw)
75
+ return ij, a[CT_LAYERS[0]:CT_LAYERS[1]].astype(np.float32).mean(0)
 
76
 
77
  with ThreadPoolExecutor(4) as ex:
78
  for (i, j), tile in ex.map(one, jobs):
 
127
 
128
  def main():
129
  os.makedirs(f"{OUT}/hi", exist_ok=True)
130
+ dirs = sorted(glob.glob(f"{RAW}/w*"))
131
  names = [os.path.basename(d) for d in dirs]
132
  grids, srcs, metas = [], [], []
133
  for d in dirs:
 
136
  grids.append((x, y, z, v))
137
  srcs.append(json.load(open(f"{d}/source.json")))
138
  metas.append(json.load(open(f"{d}/meta.json")))
 
 
 
 
 
 
 
 
 
 
139
  ax, ay = axis_fit(grids)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  allz = np.concatenate([g[2][g[3]] for g in grids])
141
  zc = float((allz.min() + allz.max()) / 2)
142
  cx0, cy0 = float(np.polyval(ax, zc)), float(np.polyval(ay, zc))
 
148
  H, W = v.shape
149
  tw, th = int(round(W * CELL_MM * PPMM)), int(round(H * CELL_MM * PPMM))
150
  sizes[w] = (tw, th)
151
+ ink = Image.open(f"{d}/ink2um_ds8.jpg").convert("L")
152
+ ink_tiles[w] = local_contrast(ink.resize((tw, th), Image.LANCZOS), 2.2 * PPMM)
153
+ hi = ink.copy()
154
+ hi.thumbnail((HI_MAX, HI_MAX), Image.LANCZOS)
155
+ hi.save(f"{OUT}/hi/{w}.jpg", quality=84, optimize=True, progressive=True)
 
 
 
 
 
 
 
 
 
 
156
  ct = fetch_ct(w, src, (H, W))
157
  valid = ct > 0
158
  lo, hi_ = np.percentile(ct[valid], [1.5, 99.5]) if valid.any() else (0, 1)
 
164
  if used > ATLAS:
165
  sys.exit(f"atlas overflow: {used} px > {ATLAS}; lower PPMM")
166
  atlas_h = 1 << int(np.ceil(np.log2(used)))
167
+ atlases = {"ink": Image.new("L", (ATLAS, atlas_h), 0), "ct": Image.new("L", (ATLAS, atlas_h), 0)}
 
 
168
  for w in names:
169
+ atlases["ink"].paste(ink_tiles[w], pos[w])
170
+ atlases["ct"].paste(ct_tiles[w], pos[w])
171
  for k, im in atlases.items():
172
  im.save(f"{OUT}/atlas_{k}.jpg", quality=88, optimize=True, progressive=True)
 
173
  print("atlas", ATLAS, atlas_h, "used rows", used)
174
 
175
  # meshes
 
187
  px = np.where(vs, xs - cx0, 0)
188
  py = np.where(vs, ys - cy0, 0)
189
  pz = np.where(vs, zs - zc, 0)
190
+ arr = np.stack([px, py, pz], -1).round().astype(np.int16)
 
 
191
  arr[~vs] = -32768
192
  b = arr.tobytes()
193
  blobs.append(b)
 
202
  "gridH": int(H), "gridW": int(W),
203
  "offset": offset, "bytes": len(b),
204
  "widthMM": round(W * CELL_MM, 2), "heightMM": round(H * CELL_MM, 2),
205
+ "areaCM2": round(meta["area_vx2"] * VOX_MM * VOX_MM / 100, 2),
206
  "radiusMM": round(float(np.median(r)), 2),
207
  "atlas": [x0 / ATLAS, y0 / atlas_h, (x0 + tw) / ATLAS, (y0 + th) / atlas_h],
208
+ "hi": f"hi/{w}.jpg",
209
+ "ink_source": [f for f, _ in src["ink"] if "2.399um" in f][0],
210
  "mesh_source": src["mesh"],
211
  })
212
  offset += len(b)
213
  open(f"{OUT}/scroll.bin", "wb").write(b"".join(blobs))
214
  manifest = {
215
+ "scroll": "PHerc0139",
216
  "voxelMM": VOX_MM, "cellMM": CELL_MM,
217
  "axis": {"centre": [cx0, cy0, zc], "dxdz": ax[0], "dydz": ay[0]},
218
  "atlas": {"w": ATLAS, "h": atlas_h, "ppmm": PPMM},
 
220
  "totalWidthMM": round(sum(wi["widthMM"] for wi in wins), 1),
221
  "totalAreaCM2": round(sum(wi["areaCM2"] for wi in wins), 1),
222
  "vertices": int(offset // 6),
 
223
  }
224
  json.dump(manifest, open(f"{OUT}/scroll.json", "w"), separators=(",", ":"))
225
  print("vertices", manifest["vertices"], "bin MB", offset / 1e6, "strip m", manifest["totalWidthMM"] / 1000,
226
  "area cm2", manifest["totalAreaCM2"])
227
 
228
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  if __name__ == "__main__":
230
+ main()
 
 
 
tools/fetch.py CHANGED
@@ -14,14 +14,10 @@ import urllib.parse
14
  import urllib.request
15
  from concurrent.futures import ThreadPoolExecutor
16
 
17
- B = "https://vesuvius-challenge-open-data.s3.amazonaws.com"
18
  ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
19
  RAW = os.environ.get("UNROLL_RAW", f"{ROOT}/raw")
20
- SCROLL = os.environ.get("UNROLL_SCROLL", "PHerc0139")
21
- MESH_RES = os.environ.get("UNROLL_MESH_RES", "9.362um") # which tifxyz to take (the coarse scan keeps int16 coordinates in range)
22
- MESH_VOL = os.environ.get("UNROLL_MESH_VOL", "") # optional volume id, when a segment has two meshes at one resolution
23
- INK_RES = os.environ.get("UNROLL_INK_RES", "") # which ink preview to call ink2um_ds8.jpg ("" = the non-1.129 um one, newest model)
24
- NO_INK = os.environ.get("UNROLL_NO_INK") == "1" # X-ray only: fetch no ink at all
25
 
26
 
27
  def get(url, tries=5):
@@ -77,36 +73,21 @@ def main():
77
  inv = list(ex.map(segment, segs))
78
  json.dump(inv, open(f"{RAW}/inv_{SCROLL}.json", "w"), indent=1)
79
  jobs = []
80
- named = [re.search(r"-(w\d{3})_", s["seg"]) for s in inv]
81
- nums = [m.group(1) for m in named if m]
82
- use_w = len(nums) >= 0.8 * len(inv) and len(set(nums)) == len(nums)
83
- for s, m in zip(inv, named):
84
- if use_w and not m:
85
  continue
86
- meshes = [x for x in s.get("mesh", []) if x.endswith(f"-{MESH_RES}.tifxyz") and MESH_VOL in x]
87
- if not meshes:
88
- continue
89
- inks = sorted(s.get("ink_ds", []))
90
- main_ink = [f for f in inks if (INK_RES in f if INK_RES else "1.129um" not in f)]
91
- if not NO_INK and not main_ink:
92
- print("skip", s["seg"], "(no ink preview)")
93
- continue
94
- # wNNN when the segment names carry unique winding numbers; otherwise the segment timestamp (build renumbers by radius)
95
- w = m.group(1) if use_w else "s" + s["seg"].split("-")[0]
96
- mesh = meshes[0]
97
  d = f"{RAW}/{w}"
98
  os.makedirs(d, exist_ok=True)
99
  base = f"{B}/{SCROLL}/segments/{s['seg']}"
100
  for f in ("x.tif", "y.tif", "z.tif", "meta.json"):
101
  jobs.append((f"{base}/mesh/{mesh}/{f}", f"{d}/{f}"))
102
- if not NO_INK:
103
- jobs.append((f"{base}/ink-detection/downsampled/{main_ink[-1]}", f"{d}/ink2um_ds8.jpg"))
104
- if True:
105
- for f in inks:
106
- if "1.129um" in f:
107
- jobs.append((f"{base}/ink-detection/downsampled/{f}", f"{d}/ink1um_ds8.jpg"))
108
- json.dump({"seg": s["seg"], "w": w, "mesh": mesh, "sv": s.get("sv"), "ink": s.get("ink"),
109
- "ink_ds": None if NO_INK else main_ink[-1]},
110
  open(f"{d}/source.json", "w"), indent=1)
111
 
112
  def fetch(job):
 
14
  import urllib.request
15
  from concurrent.futures import ThreadPoolExecutor
16
 
17
+ B = "https://vesuvius-challenge-open-data.s3.us-east-1.amazonaws.com"
18
  ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
19
  RAW = os.environ.get("UNROLL_RAW", f"{ROOT}/raw")
20
+ SCROLL = "PHerc0139"
 
 
 
 
21
 
22
 
23
  def get(url, tries=5):
 
73
  inv = list(ex.map(segment, segs))
74
  json.dump(inv, open(f"{RAW}/inv_{SCROLL}.json", "w"), indent=1)
75
  jobs = []
76
+ for s in inv:
77
+ m = re.search(r"-(w\d{3})_", s["seg"])
78
+ meshes = [x for x in s.get("mesh", []) if "9.362um" in x]
79
+ if not m or not meshes:
 
80
  continue
81
+ w, mesh = m.group(1), meshes[0]
 
 
 
 
 
 
 
 
 
 
82
  d = f"{RAW}/{w}"
83
  os.makedirs(d, exist_ok=True)
84
  base = f"{B}/{SCROLL}/segments/{s['seg']}"
85
  for f in ("x.tif", "y.tif", "z.tif", "meta.json"):
86
  jobs.append((f"{base}/mesh/{mesh}/{f}", f"{d}/{f}"))
87
+ for f in s.get("ink_ds", []):
88
+ tag = "ink1um" if "1.129um" in f else "ink2um"
89
+ jobs.append((f"{base}/ink-detection/downsampled/{f}", f"{d}/{tag}_ds8.jpg"))
90
+ json.dump({"seg": s["seg"], "w": w, "mesh": mesh, "sv": s.get("sv"), "ink": s.get("ink")},
 
 
 
 
91
  open(f"{d}/source.json", "w"), indent=1)
92
 
93
  def fetch(job):
tools/gpu_layer.py CHANGED
@@ -78,7 +78,7 @@ def main():
78
  "calls": g["calls"],
79
  "overlap": args["overlap"],
80
  "started": receipt["calls"][0]["started"],
81
- "meta": (f"ink_9um (9 µm): scrollprize/ink_9um (seed 42, step 75000) on the 9.4 µm scan (9.362 µm voxels) · "
82
  f"{p:,} patches of 128×128 px, overlap {args['overlap']}"
83
  f"{' (patch seams visible)' if args['overlap'] < 0.5 else ''} · {g['measured']:.1f} s of GPU in "
84
  f"{g['calls']} call{'s' if g['calls'] > 1 else ''} · {receipt['calls'][0]['started'][:10]} · "
@@ -92,12 +92,12 @@ def main():
92
  "windings": wins,
93
  "totals": {"windings": len(wins), "patches": tot_patches, "gpuSeconds": round(tot_measured, 1),
94
  "calls": tot_calls, "device": device},
95
- "about": (f"<b>ink_9um (9 µm)</b> is the cyan band. Chase Hendrick ran the public "
 
96
  f"<a href=\"https://huggingface.co/scrollprize/ink_9um\" target=\"_blank\" rel=\"noopener\">"
97
- f"scrollprize/ink_9um</a> model, which the Vesuvius Challenge trained, over windings {ids[0]} to {ids[-1]} "
98
- f"and processed the output for this viewer: {tot_patches:,} patches in "
99
  f"{tot_measured:.0f} s of measured GPU time on half an NVIDIA RTX PRO 6000 Blackwell. It reads the "
100
- f"9.4 µm scan (9.362 µm voxels), about four times coarser than the 2.4 µm scan behind the amber layer, so its ink map "
101
  f"is noisier. {say(full)} ran with patch overlap 0.5; {say(low)} ran with less overlap, so "
102
  f"their 128 px patch grid shows as faint square tiles. That grid is a stitching artifact, not ink. "
103
  f"Click a cyan winding to compare the two maps."),
 
78
  "calls": g["calls"],
79
  "overlap": args["overlap"],
80
  "started": receipt["calls"][0]["started"],
81
+ "meta": (f"ZeroGPU run: scrollprize/ink_9um (seed 42, step 75000) on the 9.362 µm scan · "
82
  f"{p:,} patches of 128×128 px, overlap {args['overlap']}"
83
  f"{' (patch seams visible)' if args['overlap'] < 0.5 else ''} · {g['measured']:.1f} s of GPU in "
84
  f"{g['calls']} call{'s' if g['calls'] > 1 else ''} · {receipt['calls'][0]['started'][:10]} · "
 
92
  "windings": wins,
93
  "totals": {"windings": len(wins), "patches": tot_patches, "gpuSeconds": round(tot_measured, 1),
94
  "calls": tot_calls, "device": device},
95
+ "about": (f"<b>The cyan band</b> is ours. We used all {tot_calls} of a free Hugging Face account's daily "
96
+ f"ZeroGPU runs (the limit is 8; most of the daily GPU-time quota was left over) to run the public "
97
  f"<a href=\"https://huggingface.co/scrollprize/ink_9um\" target=\"_blank\" rel=\"noopener\">"
98
+ f"scrollprize/ink_9um</a> model over windings {ids[0]} to {ids[-1]}: {tot_patches:,} patches in "
 
99
  f"{tot_measured:.0f} s of measured GPU time on half an NVIDIA RTX PRO 6000 Blackwell. It reads the "
100
+ f"9.362 µm scan, about four times coarser than the 2.4 µm scan behind the amber layer, so its ink map "
101
  f"is noisier. {say(full)} ran with patch overlap 0.5; {say(low)} ran with less overlap, so "
102
  f"their 128 px patch grid shows as faint square tiles. That grid is a stitching artifact, not ink. "
103
  f"Click a cyan winding to compare the two maps."),