MicroridgeVectorAI / README.md
leobk's picture
Replace v2 with v3: membrane-first label priority
b64c182 verified
|
Raw History Blame Contribute Delete
7.31 kB
---
license: cc-by-nc-sa-4.0
library_name: nnunetv2
pipeline_tag: image-segmentation
tags:
- biology
- microscopy
- cell-segmentation
- microridge
- nnunet
- zebrafish
---
# MicroridgeVectorAI β€” v3
A 2D nnU-Net that segments **actin microridges**, **cell regions** and **cell
membranes** in projected single-channel microscopy of epithelial tissue.
Companion application: <https://github.com/LBK888/MicroridgeVectorAI> (CellVector,
AGPL-3.0). The model runs standalone with nnU-Net v2 alone β€” CellVector is not
required.
| | |
|---|---|
| Model id | `a619b15c-e00f-489b-887c-6386e3836c11` |
| Task | 2D semantic segmentation, 4 classes |
| Architecture | nnU-Net v2 PlainConvUNet, 8 stages, patch 512x512 |
| Trainer / folds | `nnUNetTrainer_100epochs`, fold 0 |
| Input | single-channel 2D image, any size |
| Snapshot hash | `dbe134f83c52b8ecae6cb62182310205496497ec297406ed8a2c912b60ba8cc9` |
| Label policy | `membrane-first-v2`, membrane 3 px, microridge 5 px |
Labels: `0` background, `1` cell_region, `2` cell_membrane, `3` microridge.
## Scores
Frozen test β€” 36 tiles from **3 fields the model never saw**. Splits are grouped
by source field, so no tile of a training field appears in the test set.
| Metric | Value | v2 |
|---|---|---|
| cell_region Dice | 0.943 | 0.944 |
| cell_membrane Dice | **0.707** | 0.471 |
| cell_membrane boundary F1 (1 px tolerance) | **0.874** | 0.659 |
| microridge Dice | 0.852 | 0.877 |
| microridge precision / recall | 0.899 / 0.822 | 0.911 / 0.855 |
| microridge skeleton length error | 0.105 | 0.100 |
nnU-Net's own fold-0 validation (89 tiles): cell_region 0.964, cell_membrane
0.696, microridge 0.886.
v2 was trained on labels in which the microridge class had erased 69.6% of the
membrane: classes are mutually exclusive and microridges were stroked last, so a
ridge running beside a cell edge overwrote it. v3 reverses that contest. The
membrane keeps all of its pixels and the microridge class yields 7.2% of its
own, which it can afford at a quarter of the frame. Nothing else changed β€” same
data, same architecture, same 100 epochs.
**Reading the membrane number.** Dice on a 3-pixel line covering under 3% of the
frame collapses when a prediction is offset by a pixel even where it follows the
right path, so it understates a thin structure. The boundary F1 of 0.874, which
allows one pixel of tolerance, is the more informative figure; the gap between
0.707 and 0.874 is the residual sub-pixel offset, not missing membrane.
## Limitations
- **Cell instances are approximate.** Cells are recovered as connected
components separated by the predicted membrane. On a frozen-test tile holding
10 cells this returns 9, against 1 for v2, whose membrane was too broken to
separate anything. Expect near-misses where the membrane is faint, not exact
instance segmentation.
- **The ground truth was not human-reviewed.** Labels were imported from
published raster masks and corrected only for import artifacts, not by an
expert. Treat this model as a proposal generator to be corrected, which is how
the companion application uses it.
- **Trained on 13 fields.** Train and validation loss diverge (-0.782 vs
-0.636), which is what a small number of independent acquisitions looks like.
More fields will help more than more epochs.
- **One fold, not an ensemble.** Only fold 0 was trained.
- Validated on zebrafish periderm-style epithelial microridge imagery. Behaviour
on other tissue, magnification or modality is unknown.
## Training data
Wide-field frames cut into 477 tiles of at most 512x512 from 19 fields, keeping
only regions whose raster truth is trustworthy. Uneven illumination leaves part
of such a frame too dark for the upstream segmentation to resolve anything, and
that failure is silent β€” the skeleton mask is empty while the cell mask still
looks complete. Blocks were kept only where skeleton density cleared both an
absolute floor and a share of the frame's own 90th percentile, **and** at least
95% of the block was attributed to a cell. 68.3% of the field pixels survived.
Labels were rasterized from vector geometry with a 3 px membrane and a 5 px
microridge stroke.
## Files
```text
registry.json provenance record, metrics, checksums
nnUNet_results/Dataset503_MicroridgeMembraneFirst/
└─ nnUNetTrainer_100epochs__nnUNetPlans__2d/
β”œβ”€ dataset.json channel names and label map
β”œβ”€ plans.json preprocessing and architecture
└─ fold_0/checkpoint_final.pth weights
```
Those three files under the trainer folder are the complete inference set. The
directory names encode the configuration β€” nnU-Net parses
`Dataset<ID>_<name>/<trainer>__<plans>__<configuration>` β€” so do not rename them.
The checkpoint is shipped unmodified so the `checkpoint_sha256` in
`registry.json` verifies. About half of it is optimizer state; stripping to
`network_weights`, `init_args`, `trainer_name` and
`inference_allowed_mirroring_axes` halves the size but invalidates that
checksum.
## Usage
```bash
pip install nnunetv2 huggingface_hub
hf download leobk/MicroridgeVectorAI --local-dir microridge-model
```
```python
import torch, numpy as np, tifffile
from nnunetv2.inference.predict_from_raw_data import nnUNetPredictor
MODEL = ("microridge-model/nnUNet_results/Dataset503_MicroridgeMembraneFirst"
"/nnUNetTrainer_100epochs__nnUNetPlans__2d")
predictor = nnUNetPredictor(device=torch.device("cuda"))
predictor.initialize_from_trained_model_folder(
MODEL, use_folds=(0,), checkpoint_name="checkpoint_final.pth"
)
image = tifffile.imread("frame.tif").astype("float32")
segmentation = predictor.predict_single_npy_array(
image[None, None], {"spacing": (999.0, 1.0, 1.0)}, None, None, False
)
```
No nnU-Net environment variables are needed for this path. Roughly 13 s for a
512x512 tile on an RTX 4080 SUPER.
### Reimplementing the pipeline
The network takes `(1, 1, H, W)` and returns 4 logit channels, and exports to
TorchScript. If you drive it yourself, reproduce all of:
- **Normalization** β€” z-score using *each image's own* mean and standard
deviation (`use_mask_for_norm=False`). No dataset statistics;
`foreground_intensity_properties_per_channel` in `plans.json` is for CT
normalization and unused here.
- **Sliding window** β€” 512x512 patches, step 0.5, Gaussian-weighted overlap.
- **Test-time augmentation** β€” mirroring over axes `(0, 1)`.
- **Output** β€” argmax over the 4 channels.
Skipping the normalization or the Gaussian window degrades results noticeably
and without any error.
## Licensing note
These weights are released under **CC BY-NC-SA 4.0**: attribution required,
**non-commercial use only**, derivatives under the same terms. Note that this
differs from the companion application's code licence (AGPL-3.0) β€” the code and
the weights are covered separately.
The weights were trained on third-party imagery; if that source data carries its
own terms, they may constrain redistribution of this model independently of this
label.