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 |
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/
- 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.
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.
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 --helppreprocessing/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).
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 nnUNetResEncUNetMPlansginTrio: 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 ... --ttatraining/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.
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.
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.
- 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.pyis 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.pycan build SegFormer and UPerNet, which needtransformers==4.48.3. The submitted Task 3 model isdinov3_vit_dpt, which does not, and the shipped image has notransformersinstalled.- 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.pybuilds the descriptor bank withtraining/gaussian_hessian_torch.pyinstead of the off-repo build the submission used;docker/Dockerfilecopies 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 undertraining/andinference/is the submitted code, taken from the frozen snapshot the final image was built from.