python -m kit labelsnap snaps one tifxyz surface label onto the recto face of the nearest CT band and writes the snapped tifxyz, a per-vertex abstain mask and confidence, an OME-Zarr surface label (1 surface, 2 ignore) and a VC3D review queue of abstaining regions.
| Field | Value |
|---|---|
| Id | labelsnap |
| Status | Current |
| Kind | kit command, kit module |
| Area | Surfaces and tracing |
| Run | python -m kit labelsnap snap, python -m kit labelsnap check, from kit import labelsnap |
| Code | kit/labelsnap.py, kit/cli.py (build_parser), kit/villafmt.py (vc3d_points) |
| Tests | tests/test_labelsnap.py, and tests/test_snap_face.py for the snap underneath |
| Needs | Python 3.10 or newer (run here on 3.13) and numpy; kit's snap, snaptruth, vc3d_region, vc3d_audit, vc3d_io and villafmt. Optional: tifffile (mesh TIFFs; without it kit's own reader), numcodecs (blosc or zstd chunks). No scipy, no zarr package |
| Runs on | Any CPU. The synthetic quick start (720 vertices) took about 3 s with --self-check on a shared 4-core cloud container (measured 2026-10-29). On real meshes the snap reads a profile of 97 samples per vertex; about 1 s per 1,000 vertices on a local zarr is expected from kit snap's face mode (Interpretation; not measured on a whole segment) |
| Network | None for a local --volume. An https:// or s3:// volume is read anonymously over HTTPS, chunk by chunk, with kit's chunk cache in memory; nothing of it is written to disk |
| Writes | OUT/snapped.tifxyz/ (x, y, z.tif, meta.json), OUT/abstain.tif, OUT/reason.tif, OUT/confidence.tif, OUT/surface_label.zarr/ (unless --no-label), OUT/review.json, OUT/review.tsv, OUT/labelsnap.json. It refuses an OUT that is not empty. check writes nothing |
| Go-ahead | Not needed for a local run or reads of the public open-data bucket |
| Released | Public main from the pull request that adds this page (October 2026); the same code shipped earlier inside AutoPipeline 0.1.10 (branch submission/progress-2026-10-fix10, 29 October 2026). The Snapped Labels dataset (snapped-labels) was made with it and with finesnap |
| Guide | snapped-labels (the released dataset and its viewer), snap (the snap underneath), finesnap (the same snap on a finer scan) |
| Checked | 2026-11-05 against commit cc5f793 |
villa's 2026 open problems ask for surface labels "better localized on the papyrus' recto" and for zarr image and label pairs that work better than voxelized meshes (Sourced fact, 2026 open problems, read 21 October 2026; rodriguescarson's villa #1640, 29 August 2026, reports that surface labels sit a median 2.3 to 2.6 voxels off the density maximum, and pmh47 replied that the segmentation ideally is the recto surface, Community report). A published surface label (a tifxyz segment mesh) often sits inside the sheet or beside it rather than on its recto face: on fresh PHerc0139 and PHerc0814 test sites only about a quarter of published label vertices were within 1 voxel of the recto face of an independent finer scan (Checks and controls).
kit labelsnap snap moves each vertex along its normal onto the recto face of the bright CT band nearest it, found at sub-voxel precision on a trilinear profile: kit snap's band face snap (kit.snap.snap_face) in its per-vertex form (FACE_SNAP_PER_VERTEX). The recto face is the face toward the scroll axis, so the axis is required for the recto or verso target (--umbilicus, a straight axis, or --recto-sign). A vertex abstains when its band is ambiguous or runs off the profile, its contrast is too low, the band is more than max_move (4 voxels) away, or its normal is nearly tangent to the axis direction. --merged adds the merged-band rule: it also abstains where the band is more than 1.8 times the mesh's 25th-percentile band thickness, meant for two sheets merged by the blur. On PHerc0139 and PHerc0814 that rule mostly held back genuinely thick single sheets and cost accuracy, so it is off by default. An abstaining vertex may still move by an offset filled in from measured neighbors, or stays; the abstain mask says that nothing at that vertex vouches for its position.
The products are made for training and review: the abstain mask and reason grids per vertex; a confidence in [0, 1] per vertex (an untested ranking: contrast over the floor, separation from the next band and thickness under the cap, multiplied); an OME-Zarr surface label in the CT's voxel frame, 1 where every corner of a grid cell measured its face, 2 (ignore) on cells with an abstaining corner and in a halo of max_move voxels along the normal around them, 0 elsewhere (2 is the largest value, as nnU-Net expects of its ignore label; missing chunks read 0); and a VC3D review queue, a point collection with one point per region of connected abstaining vertices, largest first, at the label's position, for a person to check in VC3D. kit labelsnap check validates a folder for agents.
It does not label the other windings in a crop (the surface label marks only this label's surface), it does not fix a label that follows the wrong wrap (it moves along the normal only), and where two sheets touch with no visible gap the face it finds can belong to the neighbor. A snapped label is geometry, not a reading; nothing here is about ink.
Synthetic demo, made by scripts/media/labelsnap_demo.py, which runs the tool on the gap ladder of the quick start (no scan data). A loop of a real snap, on PHerc0139 w033 at z 7600 over its own CT (Model output), is snap-pherc0139-w033-z7600.gif on the Snapped Labels dataset.
- To move a published or traced surface label onto the recto face of its sheet before making training pairs, with the places it could not vouch for marked ignore.
- To get a review queue of the places where the label sits in merged, faint or ambiguous papyrus, for a person in VC3D.
- When the scroll has only the scan the label was traced on. Where a finer scan of the same scroll exists with a catalog matrix, finesnap placed more vertices within 1 voxel on both scrolls it was tested on.
Do not use it:
- When a finer registered scan of the same scroll exists: use finesnap.
- To snap onto a surface prediction or RectoRefine's midline: use snap (
--source pred,--source ct --mode midline). - To repair sheet switches, or a label on the wrong wrap: a snap moves along the normal only; check the wrap first in VC3D.
- Where two sheets touch without a visible gap: the face found there can belong to the neighbor (
--mergedmakes such vertices abstain instead). - Without the released data: snapped-labels already holds every published label of PHerc0139, PHerc0814, PHerc0841 and PHerc0172 snapped.
On a synthetic volume (the gap ladder of tests/test_snap_face.py: three blurred 6-voxel sheets, the recto-side neighbor at gaps of 4, 2, 1, 0.5 and 0 voxels; the label 2.9 voxels inside the middle sheet), with a straight axis below the volume:
python -m kit labelsnap snap LABEL --volume ZARR --out OUT --axis-point 20 12 -400 --axis-dir 1 0 0 --self-check
python -m kit labelsnap check OUTLABEL: a tifxyz folder;ZARR: the volume;OUT: a new folder.
On the full ladder (720 vertices) it printed (2026-10-29):
labelsnap: 720 of 720 vertices measured their recto face and moved onto it, 0 abstained (0.0); 0 review regions; wrote OUT
label: 783 surface and 0 ignore voxels in 1 chunks
self-check: passed; recovered, and both controls caught
labelsnap check: ok
On the gap-4 and gap-2 steps the snapped z had a median of 27.80 against the recto face at 27.5, every vertex within 1 voxel. On the steps with gaps of 1 voxel or less the two sheets read as one band and the snap took that band's recto face, the neighbor's, near z 21: the limit named in What it does. With --merged those 432 vertices abstain as too thick instead (one review region). It worked when both commands exit 0 and check prints ok.
- Self-check (
--self-check, kit snap's check with the face defaults: plant a smooth 2-voxel displacement, snap, require a median within 1 voxel and 80 % of vertices within it; a volume shifted 3 voxels toward the planted side must not also recover; a volume with no sheet must not move the mesh). It can fail, and does on merged bands with--merged, where abstaining vertices do not return a planted shift. - Output check (
labelsnap check): every product present, grids the mesh's shape, abstain and reason agree, confidence in [0, 1] and 0 where abstaining, label values only 0, 1 and 2, review points equal to the record's regions. A tampered mask fails it (testtest_check_catches_a_tampered_mask_and_a_missing_file). - Against an independent scan, PHerc0814 (27 October 2026, Model output): a test fixed in writing before it ran scored the default snap on 60 fresh sites of the 12 automatic segments traced on the 9.362 um scan, against the recto face of the 1.129 um scan registered per site; 42 sites were scored (18 fell below the registration floor), 310 vertices. Within 1 voxel of that face: 24.2 % published, 71.0 % snapped; within 2 voxels 82 % snapped; median distance 2.32 voxels before and 0.45 after. Mean gain toward the face +1.78 voxels (95 % interval +1.40 to +2.21). Controls: the sham (the snap's move reversed) moved labels away (-2.38), a random sign gave no gain, and the 2.399 um scan's own face, a positive control, gained +2.40. The merged-band rule cost 0.52 voxels (0.32 to 0.75), so the default dropped it.
- Against an independent scan, PHerc0139 (25 October 2026, Model output): the same kind of test on 59 scored sites (444 vertices) against PHerc0139's 1.129 um scan, run with the merged-band rule then the default, gained +1.71 voxels (1.38 to 2.07); without the rule, today's default, +2.37 (2.01 to 2.76). The sham moved labels away and a random sign gave no gain. A later test on 60 other fresh PHerc0139 sites put 79.4 % of labelsnap's vertices within 1 voxel of the 1.129 um face against 23.7 % published, and finesnap's 91.2 % (finesnap). Disclosure: 11 of the 59 sites lie on three segments the Snapped Labels release later held back for evaluation; these tests ran before that hold and their per-site rows were not kept.
- Against seven community snaps (29 October 2026, Model output, a test fixed before it ran): on 32 fresh PHerc0814 sites (1.129 um truth) and 60 fresh PHerc0841 sites (2.403 um truth), 71 scored, mean distance along the published normal to the finer scan's recto face, per site, then pooled. labelsnap (default, label scan only) was +2.153 voxels closer than the published label (95 % interval +1.821 to +2.507) and +1.892 closer than an exact-parity port of VC3D's
vc_objrefinealpha-composition refinement (Hendrik Schilling, villa382e48d35ae8) with thresholds tuned on other PHerc0139 sites (+1.480 to +2.349). It was also closer thanvc_objrefineas shipped, recto-offset r2 by TAUIL-Abd-Elilah (commit16bc31f) with and without its gate, Wittmoon's surface_placement_audit (villa PR #2147), RectoRefine's midline and a snap onto the m7 surface prediction, on each scroll and pooled; every control held. Scored against a finer scan's face found with labelsnap's own band rule; with a face rule sharing neither, the lead held at about half the size (+1.04, +1.23 voxels). Within 1 voxel: labelsnap 55 % (PHerc0814) and 66 % (PHerc0841), the published label 19 % on both, the best community snap 22 % and 21 %. - Development comparison with A.Q. Snyder's scroll-label-snap (3 November 2026, Model output, spent development sites, not a held-out test): on 108 PHerc0139 sites (windings w023 to w029, w031 to w034, w036 to w038; 601 covered vertices), a placement counted as a hit within 1 voxel of the recto face that the 1.129 um and ROI2 scans agree on: published label 0.298, the m7 surface prediction snap 0.301, scroll-label-snap (commit
347ea5b4, unmodified) 0.431 with its defaults, 0.579 withconvention_offset 0and 0.674 withtemplate_weight 0as well, kit labelsnap 0.854. scroll-label-snap is built for whole meshes and was untuned on these small crops, and labelsnap's face finder resembles the truth's face definition, so this is not a general ranking. The vertex table, the truth columns and every placement are public:docs/data/pherc0139-snap-sites/(CC BY-NC 4.0).
Scored against a finer scan's face found with labelsnap's own band rule; with a face rule sharing neither, the lead held at about half the size (+1.04, +1.23 voxels).
Against scroll-label-snap on real drift (3 November 2026, Model output, a preregistered test scored once). A.Q. Snyder's scroll-label-snap (MIT, pinned at commit 347ea5b4) moves surface labels and VC3D tifxyz meshes back onto the papyrus surface from the CT (Sourced fact, its README at that commit, read 3 November 2026). On 43 fresh PHerc0139 sites in one band (z 6194 to 7890), each placement was scored per vertex, along the published normal, against the recto face where the 2.403 um region scan and the 2.399 um scan find it within 1 voxel of each other; 173 of 387 vertices, on 28 sites, had such a face. Both tools read only the 9.362 um label scan. Share of vertices within 1 voxel of that face, with 95 % intervals: labelsnap 0.798 (0.732 to 0.859); scroll-label-snap at its best setting here (--target-offset 0, template_weight 0) 0.434 (0.310 to 0.560), with its author's settings 0.393 (0.268 to 0.518), with its defaults 0.179; the published labels 0.173. The rule fixed before the run put labelsnap ahead of all four scroll-label-snap settings (Bonferroni intervals). Controls held: the label scan's own face scored 0.954, and moving the reference 1.5 voxels either way dropped it to 0.069 and 0.104. labelsnap abstained on 19 % of the scored vertices, which were scored where its grid puts them. The chart calls labelsnap snap_labels, and ROI2 is the 2.403 um region scan.
Caveats: one scroll and one band, with 128 of the 173 scored vertices on w038. The reference face comes from two scans of about 2.4 um at a similar energy, so they share a modality and may share a face bias; a hit shows agreement with them, not that the placement is on the papyrus. scroll-label-snap is built for whole segments; here it ran untuned on 13 x 13 vertex crops, which gives its offset estimate and template fewer samples, and its default keeps the mesh's own offset by design. labelsnap's defaults were set on other PHerc0139 sites. The third panel is self-consistency under a planted 3 voxel shift on 108 earlier development sites, not accuracy. A check after the run (4 November 2026, Model output): labelsnap and the reference both take the recto face of the nearest bright band, so where two sheets touch with no gap visible even at 2.4 um, both can land on the inner neighbor's face together; at 12 of the 173 scored vertices another published segment crosses within 3.5 voxels inward of that face or inside the band, and leaving out the thickest bands and those vertices, or scoring them against the outer sheet's face instead, still put labelsnap ahead of all four scroll-label-snap settings. Numbers: the fifth unit of label_snap_checks in docs/results.json.
Real data render: a chart of scores, with no CT pixels, mesh or map; Vesuvius Challenge open data, CC BY-NC 4.0. Made by scripts/experiments/lsnap-realdrift/chart.py; a dark copy is beside it.
A wider window: --half 16 --fallback-half 12 --min-contrast 0 (30 October 2026, Model output; every rule fixed before the read, sites drawn once from geometry, scored once). The face level sits half way between the profile's 10th and 90th percentiles, so when a vertex sits deep in a thick band a +-12 voxel window holds too little gap, the level comes out too high and the face lands about a voxel inside the band. This setting (kit.snap.FACE_SNAP_WINDOW16) reads the face on a +-16 voxel profile, drops back to +-12 of the same profile where +-16 abstains, and has no mesh-wide contrast floor. It was compared on the PHercMANBp fragment, which is not prize-eligible: label scan 20251216152116 (2.399 um, read at 9.596 um), truth the recto face in the second scan 20260427100434 (1.129 um, read at 9.03 um), linked by its published transform.json and registered per site. Of 300 sites, one published label vertex each from the 11 published windings, the second scan showed a face at 220. At those 220, counting an abstention as a miss, the share within 1 voxel of that face:
| Method | Within 1 voxel |
|---|---|
--half 16 --fallback-half 12 --min-contrast 0 |
92.7 % (204 of 220) |
RectoRefine recto edge (ScrollFiesta bce8a65), the strongest community tool tested |
69.1 % |
| Published label | 24.5 % |
recto-offset r2 (TAUIL-Abd-Elilah, 16bc31f), ungated and --min-snr 2 |
23.2 % and 23.6 % raw; 42 % and 44 % at its documented 2 voxel inward offset |
vc_objrefine (villa 382e48d): shipped thresholds, shipped at full resolution, thresholds tuned on other spent sites |
5.9 %, 2.7 % and 20.5 % |
| grow3d snap, RectoRefine midline, surface_placement_audit (villa PR #2147) nearest peak | 14 %, 10 %, 5 % raw; 48 %, 51 %, 55 % scored at their own band target |
The lead over the strongest community tool tested has a one-sided 97.5 % lower bound of +0.17, clustered by 150 voxel cell. recto-offset failed its own push-back check on these sites, so it was not counted; the setting still leads it by at least +0.41. An earlier test of the same kind on a band of PHerc0332 gave the same order: on 326 sites with a face, 78.2 % against 60.4 % for RectoRefine's recto edge, lead lower bound +0.12. That test also set a 90 % exactness bar, and 85.9 % of answered vertices lay within 1 voxel (upper bound 89.6 %); its two 3.240 um truth scans agree with each other within 1 voxel only 86.9 % of the time, so the bar was out of reach of the instrument there.
Caveats: one fragment and one truth scan at about the label voxel size, so this is a comparison, not proof that labels end up within 1 voxel. The fragment was chosen as the test site after another candidate's truth scan failed its check. The truth face is found with the same half-level face rule, which favors a method built on it; tools aimed at the band middle are also scored at their own target. The scores come from the test harness reading the same rule on its own profiles, not from a run of labelsnap snap on whole meshes; on synthetic profiles through the whole snap path, the snap's per-vertex offsets equal that rule (test test_window16_equals_the_rule_read_profile_by_profile, which also catches the difference when the fallback is off or the window is +-12). With the default contrast floor left on, vertices under it abstain as well; the tests ran without it. The same setting is villa's FACE_SNAP_WINDOW16 in its snap_labels draft (villa PR #2368). Numbers: the seventh and eighth units of label_snap_checks in docs/results.json.
Limits, stated plainly: three scrolls, sites drawn from automatic and hand segments of their scans; PHerc0814 is held out by site, not by scroll; the tests show labels moved closer to the recto face, not exactness at every vertex; a label displaced toward the verso was not shown to come back (the planted toward-verso control was weak); the midline and nearest-peak snaps aim at the sheet center by design, so part of their loss is the target, not error; finesnap beat labelsnap on PHerc0814 by +0.93 voxels where a finer scan exists. On PHerc0841 the truth scan is the one the Snapped Labels release's finesnap read, so the released PHerc0841 meshes were not scored. Scored against a finer scan's face found with labelsnap's own band rule; with a face rule sharing neither, the lead held at about half the size (+1.04, +1.23 voxels). Numbers: label_snap_checks in docs/results.json.
Claim label: None for the products (geometry and masks, no claim); Model output for the measured placement against independent scans on PHerc0139, PHerc0814 and PHerc0841, and for the comparison with scroll-label-snap; Untested idea for the confidence ranking and for other scrolls.
usage: python -m kit labelsnap snap [-h] --volume VOLUME [--level LEVEL]
--out OUT [--umbilicus UMBILICUS]
[--umbilicus-scale UMBILICUS_SCALE]
[--axis-point X Y Z] [--axis-dir X Y Z]
[--recto-sign {1.0,-1.0}]
[--target {recto,mid,verso}] [--merged]
[--no-merged] [--face-sigma FACE_SIGMA]
[--max-move MAX_MOVE] [--half HALF]
[--fallback-half FALLBACK_HALF]
[--min-contrast MIN_CONTRAST] [--no-label]
[--label-level LABEL_LEVEL]
[--ignore-halo IGNORE_HALO]
[--label-step LABEL_STEP]
[--review-min REVIEW_MIN]
[--review-max REVIEW_MAX] [--self-check]
[--json]
surface
| Option | Default | Meaning |
|---|---|---|
surface |
required | tifxyz folder (x.tif, y.tif, z.tif, meta.json) in the volume's level-0 voxels |
--volume VOLUME |
required | the CT the label lives in: OME-Zarr folder or http(s) URL |
--level LEVEL |
0 |
pyramid level the snap reads (coordinates stay level 0) |
--out OUT |
required | output folder (created; existing products are refused) |
--umbilicus UMBILICUS |
none | umbilicus JSON naming the scroll axis (kit snap's format) |
--umbilicus-scale UMBILICUS_SCALE |
1.0 |
multiply the umbilicus points by this |
--axis-point X Y Z |
none | a point on the scroll axis, level-0 voxels; with --axis-dir, a straight axis instead of an umbilicus |
--axis-dir X Y Z |
none | the straight axis's direction |
--recto-sign RECTO_SIGN |
none | +1: the recto face lies at negative offsets along the mesh normal; -1: at positive ones |
--target TARGET |
recto |
recto (the band face toward the axis), mid (the band midline, needs no axis) or verso |
--merged |
off | add the merged-band rule (FACE_SNAP_PER_VERTEX_MERGED; the default until 27 Oct 2026) |
--no-merged |
off | the default since 27 Oct 2026 (kept for old commands) |
--face-sigma FACE_SIGMA |
1.0 |
smoothing of the band finder along the profile, voxels |
--max-move MAX_MOVE |
none | snap_face's max_move (default 4 voxels) |
--half HALF |
none | profile half-length along the normal (default 12 voxels) |
--fallback-half FALLBACK_HALF |
none | measure a vertex that abstains on +-half again on this shorter window (default off); must be above 0 and below the half-length |
--min-contrast MIN_CONTRAST |
none | least profile contrast, CT units; 0 turns the floor off (default: a quarter of the mesh's 1st to 99th percentile window) |
--no-label |
off | skip the OME-Zarr surface label |
--label-level LABEL_LEVEL |
0 |
the image level the label array matches |
--ignore-halo IGNORE_HALO |
none | ignore band along the normal, level-0 voxels (default max_move) |
--label-step LABEL_STEP |
0.5 |
sample spacing on the surface (and along the normal in the ignore halo) when the label is drawn, voxels of the label level |
--review-min REVIEW_MIN |
4 |
an abstaining region needs this many vertices to get a review point |
--review-max REVIEW_MAX |
500 |
at most this many review points, largest regions first |
--self-check |
off | also run kit snap's self-check (plant 2 voxels, recover; shifted and empty controls) |
--json |
off | print the record (labelsnap.json) as JSON instead of the summary lines |
usage: python -m kit labelsnap check [-h] [--json] out
| Option | Default | Meaning |
|---|---|---|
out |
required | an output folder written by labelsnap snap |
--json |
off | print {out, ok, problems} as JSON |
| Function | Returns | Raises | Notes |
|---|---|---|---|
label_snap(xs, ys, zs, valid, sampler, axis=None, recto_sign=None, target='recto', merged=False, face_sigma=1.0, extra_opts=None, batch=...) |
dict: xs, ys, zs (snapped grids), valid, abstain (uint8 0, 1, 255), reason (uint8, kit.snap.FACE_REASONS), confidence (float32), apply, D3, normals, ok, report, record, detail, opts |
kit.snap.SnapError on a bad target |
The snap of one mesh on a kit.snap.Sampler; what the command and the study's primary arm both call |
face_opts(target='recto', merged=False, extra=None) |
dict of snap_face options |
none | FACE_SNAP_PER_VERTEX (or FACE_SNAP_PER_VERTEX_MERGED with merged=True) with the target and extras |
confidence(detail, measured_mask) |
float32 grid in [0, 1] | none | Contrast, separation and thickness factors multiplied; 0 where the vertex did not measure its face |
reason_counts(reason, valid) |
dict reason name to count | none | Over valid vertices |
components(mask) |
int32 grid, 0 outside, 1..n | none | 4-connected components, numpy and the standard library only |
review_regions(xs, ys, zs, abstain, reason, min_size=4, max_points=500) |
list of dicts: size, row, col, xyz, reason, bbox_rc |
none | Largest first |
review_collection(regions, name='labelsnap review', meta=None) |
VC3D point collection dict | none | One collection; region details under kit |
write_surface_label(path, xs, ys, zs, valid, abstain, normals, shape_zyx, scale=1.0, halo=4.0, step=0.5, chunk=128, source=None, level=0) |
summary dict (chunks_written, voxels, cells_surface, cells_ignore, samples_outside_volume) |
LabelSnapError when path exists |
Writes by z slabs, only touched chunks |
read_label_chunks(path) |
(shape, {(cz, cy, cx): block}) |
OSError, zlib.error |
For checks and tests |
write_outputs(out, src, res, volume_desc, label_shape=None, ...) |
the record dict | LabelSnapError |
Every product of one label_snap result |
check_outputs(out) |
list of problems (empty when fine) | none | What labelsnap check runs |
main(argv=None) |
exit code | none | The command line without python -m kit |
build_parser(p) |
None | none | Adds the snap and check subcommands to an argparse parser (kit/cli.py uses it) |
level_scale(vol, level) |
float | LabelSnapError when the level's z, y and x factors differ |
Level voxels per level-0 voxel (0.5 at level 1), the factor the label array's coordinates are multiplied by |
run_snap(args) |
the record dict | LabelSnapError, kit.snap.SnapError |
snap on parsed arguments |
cmd_labelsnap(args) |
exit code | none | Dispatch for python -m kit labelsnap |
LabelSnapError (a ValueError) is raised for refused inputs; the command turns it, and kit.snap.SnapError, into exit code 2.
surface: a tifxyz folder:x.tif,y.tif,z.tif(float32 grids of one shape, level-0 voxel coordinates of the volume, -1 or any negative value where there is no vertex) andmeta.json(copied into the snapped mesh with a new bbox and asnaprecord).--volume: an OME-Zarr (zarr v2 or unsharded v3) folder or URL of the CT the label lives in;--levelpicks the pyramid level the snap reads, coordinates stay level 0. The snap's distances (max_move4, the profile's 12 each way) are in voxels of the level it reads, so a mesh on a 2.4 um scan is best snapped at the level whose voxels are near 9 to 10 um (level 2), where the defaults were set (about 9.4 to 9.6 um voxels).- The axis:
--umbilicus FILE(kit snap's umbilicus format,--umbilicus-scaleto bring it to level 0), or--axis-pointwith--axis-dir, or--recto-sign 1|-1for a mesh whose normals all point the same way relative to the axis.
labelsnap.json:
| Field | Type | Meaning |
|---|---|---|
opts |
object | the snap_face options used (target, max_thick_ratio, ...) |
report |
object | snap_face's report: frac_snapped, median_abs_off, abstained counts, ref_band_thick, thick_cap |
vertices |
object | valid, measured, abstained, frac_abstained, moved, confidence_median_measured |
reasons |
object | vertices per reason (kit.snap.FACE_REASONS) |
review |
object | regions, vertices_in_regions, file |
label |
object | shape_zyx, level, chunks_written, voxels (surface, ignore), cells_surface, cells_ignore, samples_outside_volume |
self_check |
object | with --self-check: kit snap's check record (passed, recovery, control_shifted, control_empty, why) |
seconds |
object | snap, total |
abstain.tif (uint8: 0 measured, 1 abstained, 255 invalid), reason.tif (uint8: 0 measured, 1 missing data, 2 side not named, 3 low contrast, 4 no band, 5 ambiguous or off the profile, 6 too far, 7 too thick, 8 rejected by its neighbors, 9 not a valid vertex), confidence.tif (float32). review.tsv columns: rank, size, row, col, x, y, z, reason. surface_label.zarr/: a zarr v2 group, array 0 of the label level's full shape, chunks 128, zlib, / keys; .zattrs holds OME-NGFF 0.4 multiscales and image-label (values 0 background, 1 surface, 2 ignore). At --label-level N a level-0 point p sits at p times the level's factor (p / 2N for a halving pyramid, as kit snap's Sampler reads it) and the OME scale is 2N on each axis, with no translation.
| Exit code | Meaning |
|---|---|
0 |
snap: every product written and its own check passed; check: no problem found |
1 |
check found a problem (listed); snap wrote its products but they fail the same check (listed on stderr) |
2 |
refused input (kit labelsnap: ... on stderr), or an argument error |
| What you see | Why | What to do |
|---|---|---|
usage: ... then error: ... on stderr, exit code 2 |
A missing or wrong argument | Check the command against Commands and options |
exists: the label is never written over an old one |
surface_label.zarr already exists in the output |
Write each run to a new folder |
give the scroll axis or --recto-sign, not both |
Both an axis and a sign were given | Give one |
the recto face is named by its side toward the scroll axis: give --umbilicus, --axis-point with --axis-dir, or --recto-sign |
A recto or verso target with no way to name the side | Add the umbilicus, or use --target mid |
is not empty: write each run to a new folder |
--out holds files |
Pick a new folder |
kit labelsnap: cannot open ... |
The volume path or URL cannot be read | Check the path, the URL and the network |
--label-level N: the volume has M levels |
The label level is not in the pyramid | Pick a level below M |
--label-level N: the level's factors [...] (z, y, x) are not one isotropic scale |
The level is downsampled differently along z, y and x; the label writer takes one factor | Use level 0, or a level halved on every axis |
the outputs fail their own check, exit code 1 |
snap ran check on what it wrote and found a problem (for example no surface voxel inside the label array) |
Read the listed problems; do not use the folder |
Most vertices abstain with side not named |
Normals nearly along the axis direction (a mesh lying flat in z) or a wrong axis | Check the umbilicus frame and scale |
Most vertices abstain with too far |
The mesh is more than 4 voxels off the sheet, or the level is too fine | Read a coarser --level, or snap with kit snap first |
self-check: FAILED |
Merged or faint bands, or a mesh far from the sheet | Look at the review queue; do not use the snapped label as if it passed |
- Normals by kit snap's rule (central differences on the grid,
cross(d/dcol, d/drow)). - Per vertex, a trilinear profile of the CT along the normal from -12 to 12 voxels in steps of 0.25 (
FACE_SNAP_DEFAULTShalf,step;--halfchanges the length). With--fallback-half F, a vertex that abstains on that profile (missing data, low contrast, no band, ambiguous or off the profile, too far; never side not named) is measured again on the samples within +-F of the same profile, its level set from those samples alone; the reason kept is the last window's. kit.snaptruth.sheet_faceson that profile (smoothing--face-sigma1.0, level halfway between the 10th and 90th percentiles) gives the band nearest the vertex and its two faces; the recto face is the one toward the axis.- Abstain rules (above); with
--merged, the merged-band cap is 1.8 times the 25th percentile of the mesh's measured band thicknesses, applied when at least 10 bands were measured. - Per-vertex form: no jump rejection, holes filled from measured neighbors by normalized convolution (
min_support1), offsets clipped tomax_moveplus the profile half-length. - The label: each grid cell sampled bilinearly every
--label-stepvoxels; an ignore cell also sampled along its normal over the halo; samples rounded to voxels; surface wins over ignore where both touch a voxel; written by z slabs of one chunk, at most 2,000,000 samples in memory at once. - The review queue: 4-connected components of abstaining vertices on the grid, each at the vertex nearest its middle.
Constants: FACE_SNAP_PER_VERTEX_MERGED = {"jump": 1e9, "min_support": 1, "max_thick_ratio": 1.8}, FACE_SNAP_WINDOW16 = {"jump": 1e9, "min_support": 1, "half": 16.0, "fallback_half": 12.0, "min_contrast": 0.0} (in kit/snap.py), LABEL_CHUNK = 128, MAX_BATCH_POINTS = 2_000_000, defaults in DEFAULTS.
Local CPU only; it rents nothing. Measured: the synthetic quick start (above). A whole real segment's label at level 0 of a 9 um scan writes a few thousand 128-cubed chunks (Interpretation from the surface area; not measured); use --no-label or --label-level 1 for a quick look. A batch of whole segments that would run over 10 minutes on a laptop goes to Hugging Face (the where-jobs-run rule, hard rule 11).
- Command:
python -m kit labelsnap snap SURFACE --volume ZARR_OR_URL --out NEW_DIR --umbilicus UMB.json --json, thenpython -m kit labelsnap check NEW_DIR --json. - Parse: from
snap --json:vertices.frac_abstained,reasons,review.regions,label.voxels; fromcheck --json:okandproblems. - Rerun: safe into a new folder; it refuses a non-empty one and never overwrites a label.
- Success: exit 0 from both,
checkok: true.snapruns the same check on what it wrote and exits 1 when it fails. - Not a pass: a run with exit 0 says the products were written, not that the label improved; a failed
self_check, or a highfrac_abstained, means most of the label is not vouched for; a label on the wrong wrap stays on the wrong wrap. - Go-ahead: Nothing for local runs on public data.
- Never: call a snapped label exact or correct beyond what the tests measured (closer to the recto face of an independent scan on three scrolls); describe an ink map made on it as a reading.
python3 -m unittest tests.test_labelsnap (19 tests, under 10 s here): the gap ladder (snaps from gaps of 2 and 4 voxels onto the recto face, abstains too thick under 1 with merged=True), the default without the merged-band rule, the detail grids, the confidence factors, the axis refusal, connected components, review regions and the VC3D document, the surface label (values, halo, surface winning, level scale, refusal to overwrite, read back through kit's volume reader), and the command line end to end with check passing on a good folder and failing on a tampered one, --label-level 1 on a 2-level pyramid (surface voxels at level-1 z 13 to 14, OME scale 2), the anisotropic-level refusal, snap exiting 1 when its own check fails, and --half 16 --fallback-half 12 --min-contrast 0 reaching the snap (with a fallback outside the window refused, exit 2). tests/test_snap_face.py (WindowFallbackTest) covers the window and fallback themselves. Not covered: real CT, a whole segment, URLs. The tests against independent scans in Checks and controls ran outside this repository's test suite.
- 2026-11-05:
--half,--fallback-halfand--min-contrast, withkit.snap.FACE_SNAP_WINDOW16andsnap_face'sfallback_half; the PHercMANBp and PHerc0332 comparisons of the +-16 window in Checks and controls. Defaults unchanged (on top ofcc5f793). - 2026-11-03: the 108-site PHerc0139 development table, with scroll-label-snap's placements beside labelsnap's, published in
docs/data/pherc0139-snap-sites/; no code change. - 2026-10-29: on public
mainwith kit snap and kit finesnap (on top ofafcc05a); the code is the AutoPipeline 0.1.10 copy (eea2df5). - 2026-10-27: the default drops the merged-band rule after the PHerc0814 test (the rule cost 0.52 voxels);
--mergedrestores the old behavior (eea2df5). - 2026-10-21: first version:
kit labelsnap snapandcheck, withkit.snap.snap_facereturning per-vertex detail grids;--label-level Nhalves coordinates per level andsnaprunscheckon its own output (eea2df5).
- snap: the snaps underneath (the band face snap, the self-check, the sham).
- finesnap: the same products from a finer registered scan of the same scroll; closer where such a scan exists.
- snapped-labels: the released dataset of snapped labels and its viewer.
docs/results.json: thelabelsnaprows behind Checks and controls.docs/data/pherc0139-snap-sites/: 108 PHerc0139 development sites with two-scan recto face offsets, labelsnap's and scroll-label-snap's placements.

