This release is a pre-release and may not be stable for production use.
pyplatypus
Computer vision for medical imaging — the engine behind the platypus R package.
0.3.0a9 — an alpha. This replaces the 2022 TensorFlow package with a PyTorch one. The API will still move and the R surface does not exist yet, so pin the exact version if you build on it.
Everything on PyPI so far is a pre-release, so
pip install pyplatypusresolves to this one.pip install pyplatypus==0.1.0rc2gets the old TensorFlow package.
What works today
Semantic segmentation in 2D, end to end:
- One spec, two ways in. Build it from arguments or load it from YAML — both produce the same object, so nothing downstream can tell which you used.
- Four architectures: U-Net, U-Net++, Res-U-Net, LinkNet, each composable with separable convolutions, spatial dropout, learned or interpolated upsampling, deep supervision and configurable block width.
- Nine losses (IoU, Dice, CCE, CCE-Dice, Focal, Tversky, Focal-Tversky, Combo, Lovász) and three metrics, all reducing over every axis except batch and channel — so they already work on volumes.
- Tiling that goes both ways: cut a large image into a grid instead of shrinking it, and get a full-size mask back.
- DICOM, read properly: stored values converted to real units through the modality LUT, a fixed window rather than one taken from the image, and MONOCHROME1 inverted. Opening the file is the easy part; those three are what make two scans comparable.
- Splitting by patient, not by slice:
split_dataset()turns one directory into train/validation/test CSVs, keeping every group whole. Slices of one patient on both sides of a split is the most common way to publish a Dice that means nothing, and agroup_bythat matches nothing is an error rather than a silent fallback. - Scores per case, not one number:
evaluate_cases()reports each image or patient separately andsummarise_cases()gives the distribution and names the worst one. On the Data Science Bowl a model at 0.855 over the split turned out to score 0.006 on three images; the mean had no way of saying so. - Volumes, not only slices: NIfTI in, 3D U-Net out, patches through the same tiling
that cuts 2D images. Every volume is reoriented to canonical (RAS) first - a NIfTI's
affine, not its array order, says where the anatomy is, and two datasets read naively can
be mirror images of each other. Masks may be label maps (
labels: [0, 1]) as well as pictures, because that is how every volume format stores them. - Resampling to a common voxel size:
target_spacing: [1, 1, 1]fixes the millimetres per voxel and then centre-crops or pads to the model'sinput_shape, instead of squeezing every scan into the same box. Clinical scans cover whatever length the question needed - 40 slices of 1 mm is 40 mm of patient, 40 slices of 2.5 mm is 100 mm - so resizing alone makes the same organ a different size in each and the network cannot tell. - One channel per file, in a stated order:
channels_fromtakes one pattern per channel, which is how BraTS ships four MRI sequences per patient and how satellite sets keep their bands. Sorting the names gives flair, t1, t1ce, t2 - reproducible, and anatomically meaningless; a model trained with FLAIR in channel one and used where channel one is T1 answers plausibly and reports nothing. A pattern matching two files, or none, or leaving a file unused, is an error. - The answer on the grid it came from:
predict(space="source")maps each prediction back onto the scan it was computed from, undoing the resampling and the crop. A mask on the model's grid cannot be laid over the patient's scan by anything, which is the whole point of producing one. Returns a list rather than an array, because scans differ in size and resizing them to match is how a mask ends up describing the wrong anatomy. - Weights by name, pinned to a commit:
weights: dsbowl-unetis published and fetches from a registry entry that carries the exact commit they were published at, so a name means one set of numbers forever.hf://owner/repo/file.safetensors@committakes anything else on the Hub, a path takes a local file, and a sidecar records what the weights were trained for - loading refuses a model they do not belong to, because weights with the wrong colormap and the right shape load cleanly and predict nonsense. - Many models from one file, with a comparison table at the end - which is how you find out that on some problems the architecture is not what matters. See below.
Augmentation works in 3D, with a caveat the package handles rather than hides: albumentations
supports volumes unevenly - 97 of its transforms take one and the rest raise from inside the
library, GaussNoise as KeyError: 'images'. Every transform in a 3D specification is tried
against a small probe volume while the pipeline is built, so an unsupported one is named before
training starts, and available_transforms(rank=3) lists what is usable.
Object detection, ensembling and pretrained backbones remain out of scope.
Resampling is opt-in rather than automatic: it changes the voxel grid the model sees, which
is a decision to take deliberately. Without target_spacing the old behaviour stands and
volumes are resized to input_shape.
Try it
uv venv --python 3.11 .venv
uv pip install --python .venv/bin/python -e ".[dev]"
.venv/bin/python -m pytest
# The linter is pinned in pyproject.toml and installed on its own, so it agrees with CI
# and does not drag torch along to read text files.
uv run --only-group lint ruff check pyplatypus tests
from pyplatypus import Engine, from_yaml
engine = Engine(from_yaml("examples/data_science_bowl.yaml"))
engine.fit(verbose=True)
for row in engine.evaluate():
print(row)
masks = engine.predict(engine.best_model("dice"), split="test")
examples/data_science_bowl.yaml trains a U-Net and a LinkNet on the 2018 Data Science
Bowl and prints a comparison. On a GTX 1070 that is about 11 seconds per epoch at
160×160.
Many models, one table
The point of a specification holding several models is that the comparison costs one command. Run
examples/compare_dsbowl_architectures.py and all four architectures train on the same split with
the same augmentation:
| model | parameters | s/epoch | epochs | Dice mean | sd | median | min |
|---|---|---|---|---|---|---|---|
| U-Net | 1,942,594 | 15.5 | 60 | 0.9217 | 0.054 | 0.9353 | 0.7227 |
| U-Net++ | 2,263,730 | 18.8 | 44 | 0.9212 | 0.055 | 0.9323 | 0.7459 |
| LinkNet | 1,746,754 | 14.0 | 60 | 0.9203 | 0.053 | 0.9293 | 0.7205 |
| Res-U-Net | 2,029,682 | 14.2 | 55 | 0.9187 | 0.058 | 0.9293 | 0.6706 |
Which is worth reading for what it says rather than for the winner. The spread across the four is 0.0030, and the spread across images within any one of them is 0.054 — the architectures are eighteen times closer to each other than the images are. Re-running the same U-Net with a different seed moves the number by 0.0012, which is 40% of the whole spread.
On this problem the architecture is not where the result comes from, and a table like this is how you find that out in an hour instead of a fortnight. That is also why only one set of weights is published: four names that mean the same thing would be four promises nobody needed.
Requirements
Python ≥ 3.10, and torch ≥ 2.7.
To fetch published weights by name, install the hub extra:
pip install "pyplatypus[hub]"
It is not in the base install because most runs never fetch weights and an air-gapped one cannot. Local weights files and everything else work without it.
If your GPU is a GTX 10-series (Pascal) or older, install the pascal extra:
pip install "pyplatypus[pascal]"
torch 2.8 and later ship CUDA 13 builds, and CUDA 13 dropped the Maxwell, Pascal and Volta generations outright - no driver update brings them back. The last torch built against CUDA 12 is 2.7.x, which the extra pins. On anything from Turing (RTX 20-series) onwards, ignore this.
Licence
MIT.
Metadata
Release files for pyplatypus 0.3.0a9
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| pyplatypus-0.3.0a9.tar.gz | 136.1 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyplatypus-0.3.0a9-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 223.0 kB
Release files / pyplatypus-0.3.0a9.tar.gz
| Download URL | pyplatypus-0.3.0a9.tar.gz |
|---|---|
| Size | 136.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9f3b0e7217df9fcf6803225fb057634f4e7a697d2ea8549fca67684ab5325e42
|
|
BLAKE2b-256 checksum How to use checksums |
7e86f1f449b5bd97d30de7b7f2b044cbddacb4580050d91cbdd97241de4104c3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logRelease files / pyplatypus-0.3.0a9-py3-none-any.whl
| Download URL | pyplatypus-0.3.0a9-py3-none-any.whl |
|---|---|
| Size | 86.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
290a7d348dd2878bb083780a25f834dbf7e30e3e83d91100e03c1018c14690c3
|
|
BLAKE2b-256 checksum How to use checksums |
e0ab14cf6849aa2ed452403e9e6aa7668973b7f23f8f45acf1d853146d384913
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logSecurity for pyplatypus 0.3.0a9
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for pyplatypus-0.3.0a9.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.