Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MVAA 2026 submitted system

Source for the system we submitted to the MVAA 2026 challenge (Mitral Valve Anatomy and Analysis), across all three tasks. One Docker image runs the three tasks offline with fixed weights, thresholds and postprocessing; nothing is tuned at test time.

Hidden-test scores of this system:

task DSC HD100 ASD
1 CT leaflet segmentation 0.8494 4.96 mm 0.404 mm
2 TEE leaflet segmentation 0.8583 9.17 mm 0.497 mm
3 endoscopic video segmentation 0.8386 170.5 px 35.8 px

Layout

configs/nnunet/         plans.json + dataset.json for Dataset001 (CT) and Dataset002 (TEE)
docker/                 the Dockerfile of the submitted image, and its runtime config
docs/                   how the Task 1 descriptor bank is computed, and its verification tables
inference/              container runners and postprocessing for all three tasks
preprocessing/          challenge data -> nnU-Net datasets; scale selection notes
training/               trainers, the Gaussian/Hessian filter, Task 3 training code
tests/

What is not here

  • No model weights. The submitted image bakes them in under assets/, 30.9 GB measured on the build tree: 8.19 GB of nnU-Net checkpoints (4.10 GB Task 1, 4.09 GB Task 2) and 22.7 GB for the Task 3 pack of 18 leave-one-video-out members. (The Dockerfile's own comment calls the asset layer 22 GB; 30.9 GB is the measured one and where the comment's figure came from is not recorded.) The weights are derived from the challenge data and are not redistributable.
  • No data. The MVAA 2026 data licence forbids redistribution. Point the preprocessing scripts at your own copy.

configs/nnunet/ does contain the nnU-Net plans.json and dataset.json for both CT and TEE datasets, because those are the training recipe rather than data, and without them the patch size, grid and architecture would have to be guessed.

Environment

requirements.txt carries the pins, each read from docker/Dockerfile or from pip freeze inside a container built from it. The load-bearing ones: Python 3.11 (the image runs 3.11.14), torch 2.8.0+cu128, nnunetv2 2.8.1 with dynamic_network_architectures 0.4.4, acvl_utils 0.2.6, batchgenerators 0.25.3 and batchgeneratorsv2 0.3.4, and for Task 3 segmentation-models-pytorch 0.5.0 with timm 1.0.28 (the release that defines vit_large_patch16_dinov3).

The nnU-Net version is not cosmetic: the checkpoints were trained on 2.8.1, whose build_network_architecture signature and architecture classes differ from the 2.6.x that the challenge base image shipped.

requirements.txt lists every pin in one file, but two of them need more than a plain pip install -r: torch has to come from the CUDA 12.8 wheel index, and nnunetv2 has to go in last and with --no-deps, or it replaces the four packages above with its own choices. The commands that built the shipped image are docker/Dockerfile lines 25 and 56 to 71, in that order, and they are the ones to copy.

Nothing here has to be compiled. Training and inference want a GPU; inference/predict_hessian_t1.py selects cuda unconditionally. The descriptor filter in training/gaussian_hessian_torch.py runs on CPU too, which is what the tests use.

Task 1, CT

nnU-Net ResEnc-L on a 0.3 mm isotropic grid, 192^3 patches, 47 input channels: one globally normalised CT channel, a 40-channel four-scale Gaussian/Hessian bank at sigma = 0.45 / 0.9 / 1.8 / 3.6 mm (1.5 / 3 / 6 / 12 voxels), and six sheetness channels (windowed CT, max plate response, winning scale, and the three eigenvalues at that scale).

The bank is computed on the fly, downstream of the spatial transform, and this is not an implementation detail. Channels 1-9 are directional: a gradient rotates as a vector and a Hessian as a tensor, and a mirror flips the sign of every odd derivative. Precomputing them to disk and letting nnU-Net's augmenter resample them as plain scalar fields would silently decorrelate them from the image they are stacked with. In training that means train_step and validation_step build it on the augmented batch before handing it to nnU-Net; at inference it has to sit downstream of the predictor's test-time mirroring as well, which is why the network is wrapped in _BankNet and the bank is built inside its forward() rather than the input being widened.

The filter itself is training/gaussian_hessian_torch.py, a recursive (Young and van Vliet) Gaussian and its derivatives, written in torch. It is exact by default and slow by default; docs/gaussian_hessian_port.md explains the conventions it follows, what the faster method="matmul" path costs, and the measurements behind both.

Training adds shape-aware CarveMix (training/nnunet_mix_trainers.py), applied to the raw HU channel before the descriptor is computed, since mixing after the bank would splice incompatible channel statistics into a representation that is nonlinear in intensity. The pasted region follows the donor leaflet's own distance-transform level set rather than an axis-aligned box, because a box cuts a ~1 mm sheet in half and manufactures a straight artificial boundary.

Deployed: trainer nnUNetTrainerHSCombined_250ep_CarveMix, plans nnUNetPlansIso03SheetResEncL192, 250 epochs, 5 folds averaged, mirroring TTA over all three axes, threshold 0.45, then the largest 26-connected component.

export MVAA_T1_SRC=/path/to/reference_data/t1_ct nnUNet_raw=/path/to/nnUNet_raw
python preprocessing/to_nnunet_raw.py
nnUNetv2_plan_and_preprocess -d 1
python training/build_task1_sheetness_plan.py \
    --architecture-plan ... --ct-plan ... \
    --plans-name nnUNetPlansIso03SheetResEncL192 --output ...
# training and inference need nnunet_hessian_trainer.py and gaussian_hessian_torch.py
# side by side, wherever they are copied
CUDA_VISIBLE_DEVICES=0 nnUNetv2_train 1 3d_fullres <fold> \
    -tr nnUNetTrainerHSCombined_250ep_CarveMix -p nnUNetPlansIso03SheetResEncL192
python inference/predict_hessian_t1.py --help

preprocessing/SIGMAS.md records how the four scales were chosen, and preprocessing/resample_iso.py why the grid is 0.3 mm (at 0.35 mm the round-trip already fragments 3 of 27 ground-truth sheets; at 0.4 mm, 10 of 27).

Task 2, TEE

nnU-Net ResEnc-M, trainer nnUNetTrainer_250epochs_Mirror01 (training/nnunet_trainer_mirror01.py), which restricts mirroring to the two axes that preserve leaflet identity. Five folds, probabilities averaged then argmax, followed by the distance-aware component filter and the geodesically restricted opening in inference/task2_postprocess.py.

Read the docstring on the Mirror01 trainer before reusing it: it is an ablation, not a corrected anatomical invariant. A geometry audit found genuine x-orientation reversals in 14 of 105 acquisitions, so array-x leaflet order is not an invariant to begin with.

export MVAA_T2_SRC=/path/to/reference_data/t2_tee nnUNet_raw=/path/to/nnUNet_raw
python preprocessing/to_nnunet_raw_tee.py
python preprocessing/plans_tee.py --name ... --spacing ... --norm ...
nnUNetv2_train 2 3d_fullres <fold> -tr nnUNetTrainer_250epochs_Mirror01 -p nnUNetResEncUNetMPlans

Task 3, endoscopic video

ginTrio: three DINOv3 ViT-L/16 + DPT profiles (gin, vitl, vitlhead), each with its six leave-one-video-out checkpoints, 18 in total, equally weighted (1/18 each, and the three profiles equally weighted at 1/3). Training uses Mean Teacher on the unlabelled frame pool, GIN augmentation and synthetic occlusion frames; the exported pack mixes student and teacher states per checkpoint, as recorded in the runtime export.

At inference: hflip-averaged probabilities, per-checkpoint logit bias (fold_logit_bias, alpha 1.0), ensemble mean, then a single global threshold of 0.5 chosen on a 65536-bin dense sweep. No component filtering and no spatial clustering (min_component_pixels = 0). Checkpoints are streamed one at a time rather than held on the GPU together, because 18 fp32 ViT-L members are 21.9 GB of weights; streaming was verified to produce byte-identical output.

python training/task3/train_cv.py --help
python training/task3/export_runtime.py --help
python training/task3/make_t3_submission.py --ckpt ... --data-dir ... --out ... \
    --calibration-json ... --tta

training/task3/README.md is the working description of that pipeline, kept as it stood in the research repository; its own release note lists the few things it names that did not travel into this release.

The Docker image

docker/Dockerfile is the recipe of the image that was submitted, kept as the record of what actually ran, together with its runtime config in docker/nnunet_config.json (Task 1 threshold 0.45 and largest-component on; the Task 2 block).

It cannot be built from this repository, by design. Three things it copies are not here: the off-repo igu tree that its first stage builds, the 30.9 GB of weights under assets/, and repo/training/nnunet_pseudo_trainer.py, a trainer left over from an earlier image that no task uses at inference. Treat the file as documentation of the submitted environment; the pins in requirements.txt are the part you can act on.

Tests

python -m pytest tests/

The suite pins the conventions of the Task 1 descriptor bank: the axis-to-x/y/z mapping, the derivative scaling, the channel order, and the behaviour of every guarded branch on volumes of side 1 to 4. Tests that need data or a reference build skip cleanly without them; see docs/gaussian_hessian_port.md for how to run those.

Known gaps

  • The Task 1 descriptor bank is exact by default and slow by default: 5.1 s per 192^3 four-scale bank, against 91 ms for the implementation that trained the shipped checkpoints, which comes out at about 21x on a whole Task 1 case once the network and the resampling are counted. method="matmul" is 180x faster than the exact path and costs 56 differing mask voxels in 154 469 506 over the 27 labelled cases. Nobody has tried to make the exact path fast.
  • training/nnunet_mix_trainers.py is copied from the research repository, so it also defines the BCP semi-supervised trainer, which the submitted system does not use. It was left intact rather than trimmed, so that the file that trained the shipped Task 1 checkpoints stays the file that trained them.
  • training/task3/model_factory.py can build SegFormer and UPerNet, which need transformers==4.48.3. The submitted Task 3 model is dinov3_vit_dpt, which does not, and the shipped image has no transformers installed.
  • The Task 3 tree is copied from the research repository and reads several paths from the environment (training/task3/presence_appearance_probe.py). The defaults are relative placeholders, not working paths; set the variables rather than editing the file.
  • Deviations from the code that ran, and nothing else: training/nnunet_hessian_trainer.py builds the descriptor bank with training/gaussian_hessian_torch.py instead of the off-repo build the submission used; docker/Dockerfile copies that file into the image alongside it; the three preprocessing entry points read their input and output paths from the environment instead of hardcoding ours; the cluster paths above became placeholders; and comment punctuation was normalised across the copied files, which is a text change and touches no statement. Everything else under training/ and inference/ is the submitted code, taken from the frozen snapshot the final image was built from.

About

MVAA 2026 challenge submission: source, environment and reproduction notes for the CT, TEE and endoscopic video mitral valve segmentation systems

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages