medotter
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 —
DatasetInfo,describe,list_datasets - Data —
Sample,load_dataset - Inference —
load_model,list_models,list_architectures - Evaluation —
Predictor,ScoreReport,LabelSchema,score,evaluate - Training —
train - Benchmark —
list_benchmarks,load_benchmark - Lazy attribute —
eval
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().
| Field | Type | Meaning |
|---|---|---|
name | str | Exact registry key, e.g. "ACDC". Case-sensitive. |
modality | str | None | Free-text imaging modality, e.g. "CT", "CT, MRI". |
anatomy | str | None | Free-text anatomical region(s), e.g. "abdomen, liver". |
task | str | None | Free-text task description, e.g. "organ, cardiac". |
n_classes | int | None | Number of label classes, including background if named. |
class_names | list[str] | None | Class names in label-index order; index 0 is conventionally background. |
class_labels | dict[int, str] | None | Explicit {label_id: name} map — the authoritative source LabelSchema.from_catalog prefers. |
background_id | int | None | The label id treated as background, when known. |
ignore_ids | list[int] | None | Label ids that should not be scored (e.g. “unannotated” regions). |
input_channels | int | None | Expected image channel count. |
has_video | bool | Whether the dataset is also registered as a video-style loader. |
hf_repo | str | None | Hugging Face dataset repo backing this entry, if any. |
source | str | None | Human-readable source/provenance note. |
card | str | None | Path 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
| Param | Type | Default | Meaning |
|---|---|---|---|
name | str | — | Exact, 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
| Param | Type | Default | Meaning |
|---|---|---|---|
modality | str | None | None | Case-insensitive substring match against the free-text modality field. |
anatomy | str | None | None | Case-insensitive substring match against the free-text anatomy field. |
task | str | None | None | Case-insensitive substring match against the free-text task field. |
has_video | bool | None | None | Exact 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.
| Field | Type | Meaning |
|---|---|---|
image | np.ndarray | (H, W) or (H, W, 3). Native dtype — never re-normalized; normalization is the model’s job. |
mask | np.ndarray | None | Integer 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. |
spacing | tuple[float, ...] | None | Pixel/voxel spacing if the loader surfaces it, else None — documented, never fabricated. |
meta | dict | At 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
| Param | Type | Default | Meaning |
|---|---|---|---|
name | str | — | Registry short name (see list_datasets()); case-sensitive. |
split | str | "train" | "train" | "val" | "test"; normalized per-dataset. |
type | str | "image" | Only "image" is supported. |
source | str | None | None | Maps to the loader’s source_type: "local" | "hf" | "download". None lets the loader choose (HF namespace MedOtter/<name>). |
**kwargs | — | — | Per-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
| Param | Type | Default | Meaning |
|---|---|---|---|
source | str | Path | — | Either a registered name (from list_models()) or a checkpoint path ending in .pth/.pt/.ckpt (or any existing file path). |
**kwargs | — | — | For 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.Protocol — isinstance(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()).
| Field | Type | Meaning |
|---|---|---|
per_class | dict[str, dict[str, float | None]] | {class_name: {metric: mean_value}} — each foreground class’s mean per requested metric. |
macro | dict[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_cases | int | Number of samples/cases scored. |
metrics | tuple[str, ...] | The metric names that were computed, e.g. ("dice", "iou"). |
support | dict[str, int] | {class_name: count} — how many cases had that class present in the ground truth. |
per_case | dict[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:
| Member | Meaning |
|---|---|
.scored_classes | {id: name} — named classes eligible for per-class scoring (excludes background_id and ignore_ids). |
.legal_prediction_ids | Ids a model is allowed to emit: scored_classes plus background_id. |
.legal_ground_truth_ids | legal_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
| Param | Type | Default | Meaning |
|---|---|---|---|
predictions | dict[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. |
dataset | — | — | A mo.load_dataset(...) view, or any iterable of Sample. |
metrics | tuple[str, ...] | ("dice", "iou") | Any of "dice", "iou", "hd95". |
labels | list[str] | dict[int, str] | None | None | Backwards-compatible explicit label set; mutually exclusive with schema. |
schema | LabelSchema | None | None | Explicit, strict label contract; mutually exclusive with labels. |
per_case | bool | False | When True, ScoreReport.per_case also carries each case’s individual metric values. |
present_only | bool | False | When 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
| Param | Type | Default | Meaning |
|---|---|---|---|
model | Predictor | callable | — | An object with .predict(image, spacing=None), or a bare callable model(image). |
name | str | — | Dataset registry key, forwarded to load_dataset. |
split | str | "test" | Forwarded to load_dataset. |
type | str | "image" | Forwarded to load_dataset. |
metrics | tuple[str, ...] | ("dice", "iou") | Same as score(). |
labels | list | dict | None | None | Same as score(); mutually exclusive with schema. |
schema | LabelSchema | None | None | Same as score(); mutually exclusive with labels. |
per_case | bool | False | Same as score(). |
present_only | bool | False | Same as score(). |
**load_kwargs | — | — | Forwarded 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
| Param | Type | Default | Meaning |
|---|---|---|---|
config | str | Path | dict | core.config.Config | — | A YAML path, a plain dict (wrapped in Config), or an existing Config (passed through unchanged). |
**opts | — | — | Forwarded 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.
| Param | Type | Default | Meaning |
|---|---|---|---|
name_or_dir | str | Path | — | A 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_bench → metrics.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