--- license: mit pipeline_tag: image-segmentation tags: - vesuvius - herculaneum - fibers - ink-detection - computed-tomography - 3d-segmentation - self-distillation - self-supervised - volumetric-imaging --- # PHerc. Paris 4 — 4-class fiber/ink segmentation, self-distilled (step 29000) Segments **background / vertical fiber / horizontal-angular fiber / ink** — 4 classes, in 3D, directly in micro-CT of **PHerc. Paris 4** — trained with **no fixed ground truth** via self-distillation from two frozen teacher UNets. This is villa's [`scripts/fiber_5class`](https://github.com/ScrollPrize/villa/tree/main/scripts/fiber_5class) pipeline (PR [#985](https://github.com/ScrollPrize/villa/pull/985)), and this checkpoint is confirmed (via direct inspection of its embedded training config, which matches its W&B run exactly) to be the real, finished model that pipeline produced. **This model's own training pipeline does not use DINO at all** — it is pure two-teacher self-distillation (see below). DINO only appears earlier/elsewhere in this broader fiber-modeling effort, in a separate checkpoint that may feed this run's fiber teacher input — see **Related models**. ![debug figure](images/p4_4class_36pykwky_debug_figure_step29899.png) *Training-time debug visualization from this run at step 29899 (image slice, teacher probability maps, watershed instances, student prediction). Logged to W&B, not an independent evaluation.* ## Model details | | | |---|---| | Architecture | `vesuvius` `NetworkFromConfig` 3D UNet (`shared_encoder`/`shared_decoder`/`task_heads`) — verified directly from the checkpoint: 544 encoder tensors, 60 decoder tensors, single head | | Output | **4 channels**, softmax, head named `task_heads.labels` — verified shape `(4, 32, 1, 1, 1)` | | Classes | `0` background · `1` vertical fiber · `2` horizontal/angular fiber · `3` ink | | Input | 1-channel CT, 256³ patches | | This checkpoint | step **29000** of a 30000-step schedule · W&B run [`p4_4class_ddp8_20260526`](https://wandb.ai/vesuvius-challenge/paris4-full-features/runs/36pykwky) (`36pykwky`, project `paris4-full-features`, state **finished**) | | Weights | `model` (raw) and `ema` (**EMA — recommended for inference**, decay 0.9995) | | Optimisation | SGD + Nesterov (lr 0.005, momentum 0.99, weight_decay 3e-5), cosine LR, 2000-step warmup, bf16, CE + multiclass soft Dice (0.1 label smoothing each, Dice over foreground classes only), batch size 2 × 8 GPUs (`ddp8`) | | Trained on | PHerc. Paris 4, 2.4 µm scan (`s3://vesuvius-challenge-open-data/PHercParis4/volumes/20260411134726-2.400um-0.2m-78keV-masked.zarr/`) | We verified this checkpoint directly (`torch.load(..., weights_only=False)`): its embedded `config.wandb_run_name` is `p4_4class_ddp8_20260526`, its `config.out_dir` is `/ephemeral/fiber_5class_ckpts/p4_4class_ddp8_20260526` — both matching the W&B run's own recorded config exactly, and `step=29000` matches the file's provenance exactly. `save_every=1000` with `num_iterations=30000` means step 29000 is mathematically the **last** checkpoint this run could have saved (the loop exits at step 30000 before another save triggers) — this is the final checkpoint of a finished run, not an arbitrary snapshot. This run was reached after two earlier attempts under the same name failed (`5ga68dxv`) or crashed (`q8jmzbiv`), and after an even earlier, broader experiment line tagged `5class`/`fiber-ink-papyrus` (`out_channels=5`, adding a "papyrus" class) was simplified down to the 4-class scheme published here. ## Training procedure: two-teacher self-distillation (verified against source) Read directly from `label_generator.py` (`FiveClassLabelGenerator`) and `train.py` in PR #985 — this is a from-source description, not an inference from config field names: 1. `fiber_prob = sigmoid(fiber_teacher(image))`, `ink_prob = sigmoid(ink_teacher(image))` — two independent frozen teacher UNets, FG channel only. 2. `fiber_mask = fiber_prob > fiber_thr` (0.5). 3. GPU watershed-from-minima (`cuws`) on the distance transform of `fiber_mask` (`ws_image_mode="distance"`, `ws_h_merge=14000`) → per-instance fiber segmentation. 4. **Per-instance PCA** on each instance's ZYX voxel coordinates: `|principal_axis · ẑ| > pca_cos_threshold` (0.819 = cos 35°) → class **1** (vertical), else class **2** (horizontal/angular). Instances below `ws_min_voxels` (400) default to class 2 rather than being dropped. 5. **Ink overrides fiber:** `label[ink_prob > ink_thr] = 3` (`ink_thr=0.1`) — applied after the fiber/orientation assignment, so ink always wins where the ink teacher is confident. 6. **Dark-voxel guard (final step):** `label[raw < dark_voxel_thr] = 0` (`dark_voxel_thr=90`) — forces very dark/air voxels to background regardless of any earlier assignment. Loss = cross-entropy (label smoothing 0.1) + multiclass soft Dice (smoothing 0.1, foreground classes only). A fresh pseudo-label is generated from the two frozen teachers **every step** — there is no fixed/static label set at any point in training. The two teacher checkpoints for this run were configured as `/ephemeral/fiber_5class_inputs/fiber_teacher.pth` and `ink_teacher.pth` — generic on-disk names that don't self-identify their origin. Per the identical `scripts/fiber_5class/train.py` module docstring, the fiber teacher is documented as "ihoo3tpl ckpt", i.e. very likely [`scrollprize/fiber_dinoguided_2class_step010000`](https://huggingface.co/scrollprize/fiber_dinoguided_2class_step010000) (not proven byte-identical — it was copied/renamed on the training box). The ink teacher is a separate checkpoint we never had; it no longer exists on the original training instance and was not found anywhere else we checked, so we are treating it **as unrecoverable** and are not able to publish it. ## Metrics Final logged values at step 29999 (run marked **finished**; checkpoint published here is step 29000, the last one actually saved): | metric | value | |---|---| | `loss` (ce + dice) | 1.0055 | | `loss_ce` | 0.4513 | | `loss_dice` | 0.5542 | | `metrics/dice_0_bg` | 0.961 | | `metrics/dice_1_vert_fiber` | 0.673 | | `metrics/dice_2_horiz_fiber` | 0.705 | | `metrics/dice_3_ink` | 0.791 | | `metrics/dice_fg_mean` | 0.723 | | `pseudo/frac_bg` / `frac_vert` / `frac_horiz` / `frac_ink` | 0.845 / 0.026 / 0.080 / 0.049 | | `pseudo/n_instances_mean` | 27.5 | | `pseudo/n_vert_mean` | 9 | **Important:** the per-class Dice above is **student-vs-its-own-pseudo-label self-consistency**, recomputed each `val_every` steps by re-forwarding the student on a clean (non-augmented) training crop and comparing to that crop's pseudo-label — confirmed directly from `train.py`'s logging code. It is **not** accuracy against independent, human-verified ground truth (none exists for this pipeline). Treat these numbers as a training-health signal, not a benchmark score. ![debug mask](images/p4_4class_36pykwky_debug_mask_step29899.png) *Categorical mask visualization (pseudo-label vs. student prediction) from the same step, with the fixed class palette used throughout this pipeline.* ## Relationship to other fiber-effort models — please read before conflating pipelines This is the only one of the four related repos published so far whose own training loop is DINO-free. The others are separate, earlier, or upstream components of the same broader effort: - **[`scrollprize/fiber_dinoguided_2class_step010000`](https://huggingface.co/scrollprize/fiber_dinoguided_2class_step010000)** — 2-class (background/fiber) DINO-embedding-guided self-training checkpoint, very likely (not proven byte-identical) the `fiber_teacher` input consumed by this run. Trains completely differently (Otsu + DINO-similarity dynamic pseudo-labels, no watershed, no PCA, no ink). - **[`scrollprize/dinovol_v2_ps8_supcon3class_step362500`](https://huggingface.co/scrollprize/dinovol_v2_ps8_supcon3class_step362500)** and **[`scrollprize/fiber_selftrain_teacher_epoch30`](https://huggingface.co/scrollprize/fiber_selftrain_teacher_epoch30)** — further upstream still (inputs to producing this run's likely fiber-teacher checkpoint, not direct inputs to this run itself). - **[`scrollprize/fiber_hz_vt`](https://huggingface.co/scrollprize/fiber_hz_vt)** — an independent, supervised, real-annotation-trained 2-class horizontal/vertical model (villa PR [#825](https://github.com/ScrollPrize/villa/pull/825)). Different pipeline, different training data (WebKnossos skeleton traces vs. self-distillation), not directly comparable. ## Files | File | Size | Role | |---|---|---| | `p4_4class_ddp8_20260526_step029000.pth` | ~2.1 GB | `model` (raw) + `ema.model_state` (recommended for inference) + `optimizer` + embedded `config`. | | `images/p4_4class_36pykwky_debug_figure_step29899.png` | — | Training-time debug figure (illustrative only), from this run's W&B logs. | | `images/p4_4class_36pykwky_debug_mask_step29899.png` | — | Training-time categorical mask visualization (illustrative only), from this run's W&B logs. | ## Usage ```python import torch from huggingface_hub import hf_hub_download path = hf_hub_download( "scrollprize/fiber_ink_4class_selfdistill", "p4_4class_ddp8_20260526_step029000.pth", ) ckpt = torch.load(path, map_location="cpu", weights_only=False) state = ckpt["ema"]["model_state"] # recommended over ckpt["model"] # Build with vesuvius' NetworkFromConfig (target "labels", out_channels=4, # in_channels=1, patch_size 256^3) then load_state_dict(state). ``` The `vesuvius` package and the full training pipeline are in (`scripts/fiber_5class/`). ## Links - **Code:** — PR [#985](https://github.com/ScrollPrize/villa/pull/985) (`scripts/fiber_5class`, this model's exact training code) · PR [#825](https://github.com/ScrollPrize/villa/pull/825) (related cross-frame affine infra, separate pipeline) - **W&B run:** - **Data:** - **Vesuvius Challenge:** ## Caveats - Metrics are self-consistency (student vs. its own pseudo-label), not held-out validation against independent ground truth — there is none in this pipeline. - The ink teacher checkpoint used to train this model is not published here and is believed unrecoverable (no longer present on the original training instance; not found elsewhere in our search). - The fiber teacher checkpoint used to train this model is very likely [`scrollprize/fiber_dinoguided_2class_step010000`](https://huggingface.co/scrollprize/fiber_dinoguided_2class_step010000) based on matching documentation in the training source, but this was not proven byte-identical (it was renamed to a generic filename on the training box before this run consumed it). - Trained on a single scroll (PHerc. Paris 4); generalization to other scrolls is untested by us. ## License MIT.