API Reference

medotter

Public package status: the Python distributions and desktop installers are not released yet. These pages document an evolving interface, not currently usable install artifacts. View release status.

This page documents every public name exported by medotter (imported as mo): 16 public API names (medotter.__all__ also lists __version__, documented separately below), plus the lazily-exposed mo.eval attribute (deliberately excluded from __all__ — see below). For narrative walkthroughs with worked examples, see the Guides section; this page is the terse, per-name reference.

medotter.__version__ is "0.1.0".

Names are grouped by area (catalog, data, inference, training, evaluation, benchmark):

Catalog

Torch-free: reads a JSON file bundled with the package. No GPU, network, or ML dependency required.

DatasetInfo

@dataclass(frozen=True)
class DatasetInfo:
    name: str
    modality: Optional[str] = None
    anatomy: Optional[str] = None
    task: Optional[str] = None
    n_classes: Optional[int] = None
    class_names: Optional[List[str]] = None
    class_labels: Optional[Dict[int, str]] = None
    background_id: Optional[int] = None
    ignore_ids: Optional[List[int]] = None
    input_channels: Optional[int] = None
    has_video: bool = False
    hf_repo: Optional[str] = None
    source: Optional[str] = None
    card: Optional[str] = None

Stability: stable

A frozen dataclass returned by describe() and list_datasets().

FieldTypeMeaning
namestrExact registry key, e.g. "ACDC". Case-sensitive.
modalitystr | NoneFree-text imaging modality, e.g. "CT", "CT, MRI".
anatomystr | NoneFree-text anatomical region(s), e.g. "abdomen, liver".
taskstr | NoneFree-text task description, e.g. "organ, cardiac".
n_classesint | NoneNumber of label classes, including background if named.
class_nameslist[str] | NoneClass names in label-index order; index 0 is conventionally background.
class_labelsdict[int, str] | NoneExplicit {label_id: name} map — the authoritative source LabelSchema.from_catalog prefers.
background_idint | NoneThe label id treated as background, when known.
ignore_idslist[int] | NoneLabel ids that should not be scored (e.g. “unannotated” regions).
input_channelsint | NoneExpected image channel count.
has_videoboolWhether the dataset is also registered as a video-style loader.
hf_repostr | NoneHugging Face dataset repo backing this entry, if any.
sourcestr | NoneHuman-readable source/provenance note.
cardstr | NonePath to the dataset’s knowledge card, if one exists.
import medotter as mo

info = mo.describe("ACDC")
info.modality, info.n_classes   # ('MRI', 4)

describe

def describe(name: str) -> DatasetInfo

Stability: stable

ParamTypeDefaultMeaning
namestrExact, case-sensitive registry key (e.g. "ACDC", not "acdc").

Returns: the dataset’s DatasetInfo.

Raises: KeyError if name is not in the catalog. The message includes up to 10 “did you mean” candidates (existing keys containing name as a substring) when any exist.

import medotter as mo

mo.describe("ACDC")
# DatasetInfo(name='ACDC', modality='MRI', anatomy='heart, cardiac', ...)

list_datasets

def list_datasets(modality=None, anatomy=None, task=None, has_video=None) -> List[DatasetInfo]

Stability: stable

ParamTypeDefaultMeaning
modalitystr | NoneNoneCase-insensitive substring match against the free-text modality field.
anatomystr | NoneNoneCase-insensitive substring match against the free-text anatomy field.
taskstr | NoneNoneCase-insensitive substring match against the free-text task field.
has_videobool | NoneNoneExact boolean match on has_video.

Returns: list[DatasetInfo], sorted by name. With no arguments, every catalog entry.

Raises: nothing — an unmatched filter simply yields an empty list.

import medotter as mo

mo.list_datasets(modality="CT, MRI")   # substring match: HANSeg, MSD
mo.list_datasets(anatomy="liver")      # substring match across the catalog

Data

Import is torch-free; load_dataset pulls torch and the dataloader package via a function-local import the first time it is called.

Sample

@dataclass(frozen=True)
class Sample:
    image: np.ndarray
    mask: Optional[np.ndarray]
    spacing: Optional[Tuple[float, ...]]
    meta: Dict[str, Any] = field(default_factory=dict)

Stability: stable

One browse/train sample, uniform across all 130 catalog datasets.

FieldTypeMeaning
imagenp.ndarray(H, W) or (H, W, 3). Native dtype — never re-normalized; normalization is the model’s job.
masknp.ndarray | NoneInteger label map, same spatial shape as image, in the dataset’s native label space (label 0 is not universally background). None if the loader returns no mask.
spacingtuple[float, ...] | NonePixel/voxel spacing if the loader surfaces it, else None — documented, never fabricated.
metadictAt least {"dataset": name}; commonly also modality, case_id, slice_idx, class_names, image_path, mask_path when the loader surfaces them.

See Loading data for the full contract and how meta keys drive scoring.

sample = mo.load_dataset("ACDC", split="test")[0]
sample.image.shape, sample.mask.shape

load_dataset

def load_dataset(name: str, *, split="train", type="image", source=None, **kwargs)

Stability: stable

ParamTypeDefaultMeaning
namestrRegistry short name (see list_datasets()); case-sensitive.
splitstr"train""train" | "val" | "test"; normalized per-dataset.
typestr"image"Only "image" is supported.
sourcestr | NoneNoneMaps to the loader’s source_type: "local" | "hf" | "download". None lets the loader choose (HF namespace MedOtter/<name>).
**kwargsPer-dataset options (modality=, dataset=, task=, …) forwarded to the loader constructor.

Returns: an indexable and iterable view of Sample objects.

Raises: NotImplementedError if type != "image"; ImportError if the data stack (torch, datasets, the repo’s dataloader package) is not installed; KeyError if name is not a registered dataset.

import medotter as mo

ds = mo.load_dataset("ACDC", split="test")
len(ds), ds[0].image.shape

Inference

Import is torch-free; load_model pulls torch (and, for SAM2, the model backbone) only when called.

load_model

def load_model(source, **kwargs)

Stability: stable

ParamTypeDefaultMeaning
sourcestr | PathEither a registered name (from list_models()) or a checkpoint path ending in .pth/.pt/.ckpt (or any existing file path).
**kwargsFor checkpoints: arch=, num_classes=, input_channel=, input_size=, device=, threshold=, normalize=, input_scale=. For SAM2 names: device=, prompt= ("box" or "point").

Returns: a Predictor — an object with .predict(image, spacing=None) -> np.ndarray (an integer label map matching image’s spatial shape).

Raises: ValueError if source is neither a registered name nor a plausible checkpoint path, or if an explicit arch= is not a known architecture (see list_architectures()).

import medotter as mo

model = mo.load_model("sam2_hiera_small")                       # a registered baseline
model = mo.load_model("runs/best.pth", arch="CMUNeXt", num_classes=4)  # a checkpoint

list_models

def list_models() -> list[str]

Stability: stable

Params: none.

Returns: sorted registered model names loadable by name alone via load_model(name) — the SAM2 baselines. This is not the full set load_model can build; checkpoint models also need arch= (see list_architectures()).

Raises: nothing; torch-free.

mo.list_models()
# ['sam2_hiera_base_plus', 'sam2_hiera_large', 'sam2_hiera_small', 'sam2_hiera_tiny']

list_architectures

def list_architectures() -> list[str]

Stability: stable

Params: none.

Returns: sorted architecture names valid for load_model(checkpoint, arch=...) — the trainable archs registered in models_train/model_id.json plus the RWKV allowlist. Unlike list_models(), these are not loadable by name alone: an arch describes how to build a network, so it needs a checkpoint’s weights.

Raises: nothing; torch-free (JSON read only).

mo.list_architectures()   # includes 'CMUNeXt', 'U_Net', 'RWKV_UNet', ...

Evaluation

Predictor, ScoreReport, and LabelSchema are torch-free. score() uses only numpy and the torch-free metrics/ package; evaluate() additionally imports load_dataset (torch on call).

Predictor

@runtime_checkable
class Predictor(Protocol):
    def predict(self, image: np.ndarray, spacing=None) -> np.ndarray: ...

Stability: stable

A runtime_checkable typing.Protocolisinstance(model, mo.Predictor) works, but nothing needs to subclass it; any object with a matching .predict method satisfies it, and evaluate() also accepts a bare callable model(image) with no .predict at all.

predict returns an integer label map matching image’s spatial shape, in the dataset’s native label space (label 0 is not universally background). This is the same prompt-free, label-map contract medotter_bench uses, at 2D-sample granularity — see medotter_bench.inference.Predictor for the volume-level (3D) counterpart used by Benchmark.evaluate.

class MyPredictor:
    def predict(self, image, spacing=None):
        return my_model(image)

isinstance(MyPredictor(), mo.Predictor)   # True — duck typing via Protocol

ScoreReport

@dataclass(frozen=True)
class ScoreReport:
    per_class: Dict[str, Dict[str, Optional[float]]]
    macro: Dict[str, Optional[float]]
    n_cases: int
    metrics: Tuple[str, ...]
    support: Dict[str, int] = field(default_factory=dict)
    per_case: Optional[Dict[str, Dict[str, Dict[str, float]]]] = None

Stability: stable

Returned by score() and evaluate() (and, in the experimental 3D path, by Benchmark.evaluate()).

FieldTypeMeaning
per_classdict[str, dict[str, float | None]]{class_name: {metric: mean_value}} — each foreground class’s mean per requested metric.
macrodict[str, float | None]{metric: mean_value} — the mean of per_class values, averaged only over classes present in at least one ground-truth case.
n_casesintNumber of samples/cases scored.
metricstuple[str, ...]The metric names that were computed, e.g. ("dice", "iou").
supportdict[str, int]{class_name: count} — how many cases had that class present in the ground truth.
per_casedict[str, dict[str, dict[str, float]]] | None{case_key: {class_name: {metric: value}}}, populated only when per_case=True was passed to score/evaluate.

ScoreReport also defines a custom __repr__ that prints a readable per-class + macro summary (used by print(report)).

report = mo.evaluate(model, "ACDC")
report.macro["dice"], report.support
print(report)   # human-readable per-class + macro summary

LabelSchema

@dataclass(frozen=True)
class LabelSchema:
    classes: Dict[int, str]
    background_id: Optional[int] = 0
    ignore_ids: FrozenSet[int] = field(default_factory=frozenset)

Stability: stable

Describes the integer label space used by a dataset, replacing the convention that label 0 always means background. classes names every label (including background/ignore ones, when named); background_id=None means the dataset has no background class.

Key surface, beyond the constructor:

MemberMeaning
.scored_classes{id: name} — named classes eligible for per-class scoring (excludes background_id and ignore_ids).
.legal_prediction_idsIds a model is allowed to emit: scored_classes plus background_id.
.legal_ground_truth_idslegal_prediction_ids plus ignore_ids — ids allowed to appear in ground truth.
LabelSchema.from_explicit(labels)Classmethod: build from a list of names (index 0 = background) or a {id: name} dict (infers background/ignore from names).
LabelSchema.from_catalog(info)Classmethod: build from a DatasetInfo’s class_labels/class_names. Returns None if the catalog has no class info.
LabelSchema.infer_from_masks(masks)Classmethod: infer a schema from observed ground-truth label ids (assumes 0 is background).
.validate_prediction(pred, *, valid_mask=None, allow_unknown_ids=False)Validate a prediction’s dtype/finiteness/label ids against the schema; raises ValueError on violation.

Raises (constructor): ValueError for blank/duplicate class names, or if background_id is also listed in ignore_ids.

schema = mo.LabelSchema(
    classes={0: "tumor", 1: "background"},
    background_id=1,
)
report = mo.score(preds, ds, schema=schema)

score

def score(predictions: Dict[str, np.ndarray], dataset, *,
         metrics=("dice", "iou"), labels=None, schema=None,
         per_case=False, present_only=False) -> ScoreReport

Stability: stable

ParamTypeDefaultMeaning
predictionsdict[str, np.ndarray]{key: pred_label_array}. key is f"{case_id}#{slice_idx}" when a sample’s meta has both, else case_id, else the sample’s index as a string.
datasetA mo.load_dataset(...) view, or any iterable of Sample.
metricstuple[str, ...]("dice", "iou")Any of "dice", "iou", "hd95".
labelslist[str] | dict[int, str] | NoneNoneBackwards-compatible explicit label set; mutually exclusive with schema.
schemaLabelSchema | NoneNoneExplicit, strict label contract; mutually exclusive with labels.
per_caseboolFalseWhen True, ScoreReport.per_case also carries each case’s individual metric values.
present_onlyboolFalseWhen True, a class’s per-case mean is averaged only over cases where that class is present in the ground truth.

Returns: a ScoreReport.

Raises: KeyError if a dataset case key has no entry in predictions; ValueError for a prediction/sample count mismatch, a duplicate case key, an unsupported metric name, both labels and schema passed, or a prediction containing a label id outside the resolved schema.

import medotter as mo

ds = mo.load_dataset("ACDC", split="test")
preds = {"patient001_frame01": my_prediction_array}
report = mo.score(preds, ds, metrics=("dice", "iou"), per_case=True)

evaluate

def evaluate(model, name: str, *, split="test", type="image",
             metrics=("dice", "iou"), labels=None, schema=None,
             per_case=False, present_only=False, **load_kwargs) -> ScoreReport

Stability: stable

ParamTypeDefaultMeaning
modelPredictor | callableAn object with .predict(image, spacing=None), or a bare callable model(image).
namestrDataset registry key, forwarded to load_dataset.
splitstr"test"Forwarded to load_dataset.
typestr"image"Forwarded to load_dataset.
metricstuple[str, ...]("dice", "iou")Same as score().
labelslist | dict | NoneNoneSame as score(); mutually exclusive with schema.
schemaLabelSchema | NoneNoneSame as score(); mutually exclusive with labels.
per_caseboolFalseSame as score().
present_onlyboolFalseSame as score().
**load_kwargsForwarded to load_dataset (e.g. source=, per-dataset options).

Returns: a ScoreReport.

Raises: same ValueError conditions as score() (schema/label resolution, metric names); ImportError if the data stack load_dataset needs is not installed.

import medotter as mo

model = mo.load_model("sam2_hiera_small")
report = mo.evaluate(model, "ACDC", split="test", metrics=("dice", "iou"))
print(report)

Training

Import is torch-free; train pulls torch and the training stack only when called.

train

def train(config, **opts) -> dict

Stability: stable

ParamTypeDefaultMeaning
configstr | Path | dict | core.config.ConfigA YAML path, a plain dict (wrapped in Config), or an existing Config (passed through unchanged).
**optsForwarded to training.runner.run_training: output_dir=, save= (default True), epochs=, max_batches=, resume= (a checkpoint path — restores optimizer/scheduler state and continues from its saved epoch), hf_weights=, device=, allow_unsafe= (default False; True permits the weights_only=False pickle fallback for resume/hf_weights — an arbitrary-code-execution risk, use only for a trusted source).

Returns: the trainer’s summary dict, with an added "output_dir" key.

Raises: TypeError if config is not a path, dict, or Config; ImportError on the call (not on import) if torch/the training stack is missing; unmodified errors from the training loop otherwise propagate.

import medotter as mo

config = {
    "experiment": {"name": "busi_unet", "seed": 42},
    "model": {"name": "U_Net", "input_channel": 3, "num_classes": 1},
    "dataset": {"BUSI": {"split": "train"}},
    "training": {"epochs": 1, "batch_size": 8,
                "optimizer": {"type": "AdamW", "lr": 1e-4}},
}
summary = mo.train(config, max_batches=2, output_dir="./runs/smoke")
summary["output_dir"]

Benchmark

list_benchmarks is stable and torch-free (directory listing only). load_benchmark’s metadata access is torch-free too, but Benchmark.evaluate is experimental: it needs a prepared data_root most bundles keep out of the development source tree, and is only partially proven on GPU. Expect that one surface to evolve — see the full 3D volume benchmarks guide for the caveats.

list_benchmarks

def list_benchmarks() -> list[str]

Stability: stable

Params: none.

Returns: sorted names of the benchmark bundles under the repo’s benchmarks/ directory (those with a task.yaml).

Raises: nothing; torch-free (reads directory names only).

mo.list_benchmarks()
# ['abdominal_multiorgan_ct', 'glioma_wt_tc_et', 'histopath_nucleus_seg', ...]

load_benchmark

def load_benchmark(name_or_dir) -> Benchmark

Stability: stable for discovery and metadata — load_benchmark itself and the returned Benchmark’s .task_id/.modality/.anatomy/.regions/ .metrics/.mode are torch-free and always available; only Benchmark.evaluate(...) (the 3D volume eval) is experimental — see below.

ParamTypeDefaultMeaning
name_or_dirstr | PathA bundle name under benchmarks/, or a path to a bundle directory containing task.yaml.

Returns: a Benchmark — exposes .task_id, .modality, .anatomy, .regions, .metrics, .mode (torch-free, always available); .train / .test (need the bundle’s manifest_test.json); and .evaluate(predictor, *, data_root, split="test", templates=None) -> ScoreReport for volume-level (3D) scoring — see the 3D volume benchmarks guide for the full accessor list and Benchmark.evaluate’s parameters. evaluate’s predictor is a medotter_bench.inference.Predictor (predict(image[C,i,j,k]) -> label volume) — not this page’s 2D medotter.Predictor.

Raises: ValueError for an unknown bundle name/directory. Benchmark.train / .test / .evaluate additionally raise FileNotFoundError when manifest_test.json is absent, and Benchmark.evaluate raises NotImplementedError for any bundle whose .mode is not "3d_volumetric".

import medotter as mo
from medotter_bench.inference import ZeroPredictor

bm = mo.load_benchmark("glioma_wt_tc_et")
bm.modality, bm.regions   # torch-free metadata

report = bm.evaluate(ZeroPredictor(), data_root="/path/to/DATA_ROOT")  # experimental

Lazy attribute: mo.eval

mo.eval is not in medotter.__all__ — it is reachable via attribute access (a module-level __getattr__) and shown by __dir__, but deliberately kept out of __all__ so that from medotter import * and __all__-walking tools (Sphinx, etc.) don’t eagerly resolve it and pull in the scoring stack.

Accessing mo.eval imports medotter.eval, which re-exports medotter_bench’s public names byte-identically (no wrapping, no copies: mo.eval.Bundle is medotter_bench.Bundle is True). That import pulls in scipy (via medotter_benchmetrics.hd95) on first access. Note that mo.score / mo.evaluate also pull in scipy independently — via their own metrics.region_eval import chain, on any call regardless of metrics=, not through mo.eval (see Evaluation). A script that only uses the catalog / data / inference / training surfaces — never scoring and never touching mo.eval — never pays for scipy.

Stability: stable (the lazy-import mechanism); the surface it exposes is medotter_bench, documented on the medotter_bench reference page.

import medotter as mo

mo.eval.CANONICAL_REGIONS   # {'WT': (1, 2, 3), 'TC': (1, 3), 'ET': (3,)}
# same effect, explicit re-export form:
from medotter import eval as mo_eval