| --- |
| 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**. |
|
|
|  |
|
|
| *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. |
|
|
|  |
|
|
| *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 |
| <https://github.com/ScrollPrize/villa> (`scripts/fiber_5class/`). |
|
|
| ## Links |
|
|
| - **Code:** <https://github.com/ScrollPrize/villa> β 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:** <https://wandb.ai/vesuvius-challenge/paris4-full-features/runs/36pykwky> |
| - **Data:** <https://scrollprize.org/data_browser> |
| - **Vesuvius Challenge:** <https://scrollprize.org> |
|
|
| ## 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. |
|
|