bioimageio.core.cli

bioimageio CLI

Note: Some docstrings use a hair space ' ' to place the added '(default: ...)' on a new line.

  1"""bioimageio CLI
  2
  3Note: Some docstrings use a hair space ' '
  4      to place the added '(default: ...)' on a new line.
  5"""
  6
  7import json
  8import shutil
  9import subprocess
 10import sys
 11from abc import ABC
 12from argparse import RawTextHelpFormatter
 13from difflib import SequenceMatcher
 14from functools import cached_property
 15from io import StringIO
 16from pathlib import Path
 17from pprint import pformat, pprint
 18from typing import (
 19    Any,
 20    Dict,
 21    Iterable,
 22    List,
 23    Literal,
 24    Mapping,
 25    Optional,
 26    Sequence,
 27    Set,
 28    Tuple,
 29    Type,
 30    Union,
 31)
 32
 33import rich.markdown
 34from loguru import logger
 35from pydantic import AliasChoices, BaseModel, Field, model_validator
 36from pydantic_settings import (
 37    BaseSettings,
 38    CliPositionalArg,
 39    CliSettingsSource,
 40    CliSubCommand,
 41    JsonConfigSettingsSource,
 42    PydanticBaseSettingsSource,
 43    SettingsConfigDict,
 44    YamlConfigSettingsSource,
 45)
 46from tqdm import tqdm
 47from typing_extensions import assert_never
 48
 49import bioimageio.spec
 50from bioimageio.core import __version__
 51from bioimageio.spec import (
 52    AnyModelDescr,
 53    InvalidDescr,
 54    ResourceDescr,
 55    load_description,
 56    save_bioimageio_yaml_only,
 57    settings,
 58    update_format,
 59    update_hashes,
 60)
 61from bioimageio.spec._internal.io import is_yaml_value
 62from bioimageio.spec._internal.io_utils import open_bioimageio_yaml
 63from bioimageio.spec._internal.types import FormatVersionPlaceholder, NotEmpty
 64from bioimageio.spec.dataset import DatasetDescr
 65from bioimageio.spec.model import ModelDescr, v0_4, v0_5
 66from bioimageio.spec.notebook import NotebookDescr
 67from bioimageio.spec.utils import ensure_description_is_model, get_reader, write_yaml
 68
 69from .commands import WeightFormatArgAll, WeightFormatArgAny, package, test
 70from .common import MemberId, SampleId, SupportedWeightsFormat
 71from .digest_spec import get_member_ids, load_sample_for_model
 72from .io import load_dataset_stat, save_dataset_stat, save_sample
 73from .prediction import create_prediction_pipeline
 74from .proc_setup import (
 75    DatasetMeasure,
 76    Measure,
 77    MeasureValue,
 78    StatsCalculator,
 79    get_required_dataset_measures,
 80)
 81from .sample import Sample
 82from .stat_measures import Stat
 83from .utils import compare
 84from .weight_converters._add_weights import add_weights
 85
 86WEIGHT_FORMAT_ALIASES = AliasChoices(
 87    "weight-format",
 88    "weights-format",
 89    "weight_format",
 90    "weights_format",
 91)
 92
 93
 94class CmdBase(BaseModel, use_attribute_docstrings=True, cli_implicit_flags=True):
 95    pass
 96
 97
 98class ArgMixin(BaseModel, use_attribute_docstrings=True, cli_implicit_flags=True):
 99    pass
100
101
102class WithSummaryLogging(ArgMixin):
103    summary: List[Union[Literal["display"], Path]] = Field(
104        default_factory=lambda: ["display"],
105        examples=[
106            Path("summary.md"),
107            Path("bioimageio_summaries/"),
108            ["display", Path("summary.md")],
109        ],
110    )
111    """Display the validation summary or save it as JSON, Markdown or HTML.
112    The format is chosen based on the suffix: `.json`, `.md`, `.html`.
113    If a folder is given (path w/o suffix) the summary is saved in all formats.
114    Choose/add `"display"` to render the validation summary to the terminal.
115    """
116
117    def log(self, descr: Union[ResourceDescr, InvalidDescr]):
118        _ = descr.validation_summary.log(self.summary)
119
120
121class WithSource(ArgMixin):
122    source: CliPositionalArg[str]
123    """Url/path to a (folder with a) bioimageio.yaml/rdf.yaml file
124    or a bioimage.io resource identifier, e.g. 'affable-shark'"""
125
126    @cached_property
127    def descr(self):
128        return load_description(self.source)
129
130    @property
131    def descr_id(self) -> str:
132        """a more user-friendly description id
133        (replacing legacy ids with their nicknames)
134        """
135        if isinstance(self.descr, InvalidDescr):
136            return str(getattr(self.descr, "id", getattr(self.descr, "name")))
137
138        nickname = None
139        if (
140            isinstance(self.descr.config, v0_5.Config)
141            and (bio_config := self.descr.config.bioimageio)
142            and bio_config.model_extra is not None
143        ):
144            nickname = bio_config.model_extra.get("nickname")
145
146        return str(nickname or self.descr.id or self.descr.name)
147
148
149class ValidateFormatCmd(CmdBase, WithSource, WithSummaryLogging):
150    """Validate the meta data format of a bioimageio resource."""
151
152    perform_io_checks: bool = Field(
153        settings.perform_io_checks, alias="perform-io-checks"
154    )
155    """Wether or not to perform validations that requires downloading remote files.
156    Note: Default value is set by `BIOIMAGEIO_PERFORM_IO_CHECKS` environment variable.
157    """
158
159    @cached_property
160    def descr(self):
161        return load_description(self.source, perform_io_checks=self.perform_io_checks)
162
163    def run(self):
164        self.log(self.descr)
165        sys.exit(
166            0
167            if self.descr.validation_summary.status in ("valid-format", "passed")
168            else 1
169        )
170
171
172class TestCmd(CmdBase, WithSource, WithSummaryLogging):
173    """Test a bioimageio resource (beyond meta data formatting)."""
174
175    weight_format: WeightFormatArgAll = Field(
176        "all",
177        alias="weight-format",
178        validation_alias=WEIGHT_FORMAT_ALIASES,
179    )
180    """The weight format to limit testing to.
181
182    (only relevant for model resources)"""
183
184    devices: Optional[List[str]] = None
185    """Device(s) to use for testing"""
186
187    runtime_env: Union[Literal["currently-active", "as-described"], Path] = Field(
188        "currently-active", alias="runtime-env"
189    )
190    """The python environment to run the tests in
191        - `"currently-active"`: use active Python interpreter
192        - `"as-described"`: generate a conda environment YAML file based on the model
193            weights description.
194        - A path to a conda environment YAML.
195          Note: The `bioimageio.core` dependency will be added automatically if not present.
196    """
197
198    determinism: Literal["seed_only", "full"] = "seed_only"
199    """Modes to improve reproducibility of test outputs."""
200
201    stop_early: bool = Field(
202        False, alias="stop-early", validation_alias=AliasChoices("stop-early", "x")
203    )
204    """Do not run further subtests after a failed one."""
205
206    format_version: Union[FormatVersionPlaceholder, str] = Field(
207        "discover", alias="format-version"
208    )
209    """The format version to use for testing.
210        - 'latest': Use the latest implemented format version for the given resource type (may trigger auto updating)
211        - 'discover': Use the format version as described in the resource description
212        - '0.4', '0.5', ...: Use the specified format version (may trigger auto updating)
213    """
214
215    def run(self):
216        sys.exit(
217            test(
218                self.descr,
219                weight_format=self.weight_format,
220                devices=self.devices,
221                summary=self.summary,
222                runtime_env=self.runtime_env,
223                determinism=self.determinism,
224                format_version=self.format_version,
225            )
226        )
227
228
229class PackageCmd(CmdBase, WithSource, WithSummaryLogging):
230    """Save a resource's metadata with its associated files."""
231
232    path: CliPositionalArg[Path]
233    """The path to write the (zipped) package to.
234    If it does not have a `.zip` suffix
235    this command will save the package as an unzipped folder instead."""
236
237    weight_format: WeightFormatArgAll = Field(
238        "all",
239        alias="weight-format",
240        validation_alias=WEIGHT_FORMAT_ALIASES,
241    )
242    """The weight format to include in the package (for model descriptions only)."""
243
244    def run(self):
245        if isinstance(self.descr, InvalidDescr):
246            self.log(self.descr)
247            raise ValueError(f"Invalid {self.descr.type} description.")
248
249        sys.exit(
250            package(
251                self.descr,
252                self.path,
253                weight_format=self.weight_format,
254            )
255        )
256
257
258def _get_stat(
259    model_descr: AnyModelDescr,
260    dataset: Iterable[Sample],
261    dataset_length: int,
262    stats_path: Path,
263) -> Mapping[DatasetMeasure, MeasureValue]:
264    req_dataset_meas, _ = get_required_dataset_measures(model_descr)
265    if not req_dataset_meas:
266        return {}
267
268    req_dataset_meas, _ = get_required_dataset_measures(model_descr)
269
270    if stats_path.exists():
271        logger.info("loading precomputed dataset measures from {}", stats_path)
272        stat = load_dataset_stat(stats_path)
273        for m in req_dataset_meas:
274            if m not in stat:
275                raise ValueError(f"Missing {m} in {stats_path}")
276
277        return stat
278
279    stats_calc = StatsCalculator(req_dataset_meas)
280
281    for sample in tqdm(
282        dataset, total=dataset_length, desc="precomputing dataset stats", unit="sample"
283    ):
284        stats_calc.update(sample)
285
286    stat = stats_calc.finalize()
287    save_dataset_stat(stat, stats_path)
288
289    return stat
290
291
292class UpdateCmdBase(CmdBase, WithSource, ABC):
293    output: Union[Literal["display", "stdout"], Path] = "display"
294    """Output updated bioimageio.yaml to the terminal or write to a file.
295    Notes:
296    - `"display"`: Render to the terminal with syntax highlighting.
297    - `"stdout"`: Write to sys.stdout without syntax highligthing.
298      (More convenient for copying the updated bioimageio.yaml from the terminal.)
299    """
300
301    diff: Union[bool, Path] = Field(True, alias="diff")
302    """Output a diff of original and updated bioimageio.yaml.
303    If a given path has an `.html` extension, a standalone HTML file is written,
304    otherwise the diff is saved in unified diff format (pure text).
305    """
306
307    exclude_unset: bool = Field(True, alias="exclude-unset")
308    """Exclude fields that have not explicitly be set."""
309
310    exclude_defaults: bool = Field(False, alias="exclude-defaults")
311    """Exclude fields that have the default value (even if set explicitly)."""
312
313    @cached_property
314    def updated(self) -> Union[ResourceDescr, InvalidDescr]:
315        raise NotImplementedError
316
317    def run(self):
318        original_yaml = open_bioimageio_yaml(self.source).unparsed_content
319        assert isinstance(original_yaml, str)
320        stream = StringIO()
321
322        save_bioimageio_yaml_only(
323            self.updated,
324            stream,
325            exclude_unset=self.exclude_unset,
326            exclude_defaults=self.exclude_defaults,
327        )
328        updated_yaml = stream.getvalue()
329
330        diff = compare(
331            original_yaml.split("\n"),
332            updated_yaml.split("\n"),
333            diff_format=(
334                "html"
335                if isinstance(self.diff, Path) and self.diff.suffix == ".html"
336                else "unified"
337            ),
338        )
339
340        if isinstance(self.diff, Path):
341            _ = self.diff.write_text(diff, encoding="utf-8")
342        elif self.diff:
343            console = rich.console.Console()
344            diff_md = f"## Diff\n\n````````diff\n{diff}\n````````"
345            console.print(rich.markdown.Markdown(diff_md))
346
347        if isinstance(self.output, Path):
348            _ = self.output.write_text(updated_yaml, encoding="utf-8")
349            logger.info(f"written updated description to {self.output}")
350        elif self.output == "display":
351            updated_md = f"## Updated bioimageio.yaml\n\n```yaml\n{updated_yaml}\n```"
352            rich.console.Console().print(rich.markdown.Markdown(updated_md))
353        elif self.output == "stdout":
354            print(updated_yaml)
355        else:
356            assert_never(self.output)
357
358        if isinstance(self.updated, InvalidDescr):
359            logger.warning("Update resulted in invalid description")
360            _ = self.updated.validation_summary.display()
361
362
363class UpdateFormatCmd(UpdateCmdBase):
364    """Update the metadata format to the latest format version."""
365
366    exclude_defaults: bool = Field(True, alias="exclude-defaults")
367    """Exclude fields that have the default value (even if set explicitly).
368
369    Note:
370        The update process sets most unset fields explicitly with their default value.
371    """
372
373    perform_io_checks: bool = Field(
374        settings.perform_io_checks, alias="perform-io-checks"
375    )
376    """Wether or not to attempt validation that may require file download.
377    If `True` file hash values are added if not present."""
378
379    @cached_property
380    def updated(self):
381        return update_format(
382            self.source,
383            exclude_defaults=self.exclude_defaults,
384            perform_io_checks=self.perform_io_checks,
385        )
386
387
388class UpdateHashesCmd(UpdateCmdBase):
389    """Create a bioimageio.yaml description with updated file hashes."""
390
391    @cached_property
392    def updated(self):
393        return update_hashes(self.source)
394
395
396class PredictCmd(CmdBase, WithSource):
397    """Run inference on your data with a bioimage.io model."""
398
399    inputs: NotEmpty[List[Union[str, NotEmpty[List[str]]]]] = Field(
400        default_factory=lambda: ["{input_id}/001.tif"]
401    )
402    """Model input sample paths (for each input tensor)
403
404    The input paths are expected to have shape...
405     - (n_samples,) or (n_samples,1) for models expecting a single input tensor
406     - (n_samples,) containing the substring '{input_id}', or
407     - (n_samples, n_model_inputs) to provide each input tensor path explicitly.
408
409    All substrings that are replaced by metadata from the model description:
410    - '{model_id}'
411    - '{input_id}'
412
413    Example inputs to process sample 'a' and 'b'
414    for a model expecting a 'raw' and a 'mask' input tensor:
415    --inputs="[[\\"a_raw.tif\\",\\"a_mask.tif\\"],[\\"b_raw.tif\\",\\"b_mask.tif\\"]]"
416    (Note that JSON double quotes need to be escaped.)
417
418    Alternatively a `bioimageio-cli.yaml` (or `bioimageio-cli.json`) file
419    may provide the arguments, e.g.:
420    ```yaml
421    inputs:
422    - [a_raw.tif, a_mask.tif]
423    - [b_raw.tif, b_mask.tif]
424    ```
425
426    `.npy` and any file extension supported by imageio are supported.
427     Aavailable formats are listed at
428    https://imageio.readthedocs.io/en/stable/formats/index.html#all-formats.
429    Some formats have additional dependencies.
430
431
432    """
433
434    outputs: Union[str, NotEmpty[Tuple[str, ...]]] = (
435        "outputs_{model_id}/{output_id}/{sample_id}.tif"
436    )
437    """Model output path pattern (per output tensor)
438
439    All substrings that are replaced:
440    - '{model_id}' (from model description)
441    - '{output_id}' (from model description)
442    - '{sample_id}' (extracted from input paths)
443
444
445    """
446
447    overwrite: bool = False
448    """allow overwriting existing output files"""
449
450    blockwise: bool = False
451    """process inputs blockwise"""
452
453    stats: Path = Path("dataset_statistics.json")
454    """path to dataset statistics
455    (will be written if it does not exist,
456    but the model requires statistical dataset measures)
457     """
458
459    preview: bool = False
460    """preview which files would be processed
461    and what outputs would be generated."""
462
463    weight_format: WeightFormatArgAny = Field(
464        "any",
465        alias="weight-format",
466        validation_alias=WEIGHT_FORMAT_ALIASES,
467    )
468    """The weight format to use."""
469
470    example: bool = False
471    """generate and run an example
472
473    1. downloads example model inputs
474    2. creates a `{model_id}_example` folder
475    3. writes input arguments to `{model_id}_example/bioimageio-cli.yaml`
476    4. executes a preview dry-run
477    5. executes prediction with example input
478
479
480    """
481
482    def _example(self):
483        model_descr = ensure_description_is_model(self.descr)
484        input_ids = get_member_ids(model_descr.inputs)
485        example_inputs = (
486            model_descr.sample_inputs
487            if isinstance(model_descr, v0_4.ModelDescr)
488            else [
489                t
490                for ipt in model_descr.inputs
491                if (t := ipt.sample_tensor or ipt.test_tensor)
492            ]
493        )
494        if not example_inputs:
495            raise ValueError(f"{self.descr_id} does not specify any example inputs.")
496
497        inputs001: List[str] = []
498        example_path = Path(f"{self.descr_id}_example")
499        example_path.mkdir(exist_ok=True)
500
501        for t, src in zip(input_ids, example_inputs):
502            reader = get_reader(src)
503            dst = Path(f"{example_path}/{t}/001{reader.suffix}")
504            dst.parent.mkdir(parents=True, exist_ok=True)
505            inputs001.append(dst.as_posix())
506            with dst.open("wb") as f:
507                shutil.copyfileobj(reader, f)
508
509        inputs = [inputs001]
510        output_pattern = f"{example_path}/outputs/{{output_id}}/{{sample_id}}.tif"
511
512        bioimageio_cli_path = example_path / YAML_FILE
513        stats_file = "dataset_statistics.json"
514        stats = (example_path / stats_file).as_posix()
515        cli_example_args = dict(
516            inputs=inputs,
517            outputs=output_pattern,
518            stats=stats_file,
519            blockwise=self.blockwise,
520        )
521        assert is_yaml_value(cli_example_args), cli_example_args
522        write_yaml(
523            cli_example_args,
524            bioimageio_cli_path,
525        )
526
527        yaml_file_content = None
528
529        # escaped double quotes
530        inputs_json = json.dumps(inputs)
531        inputs_escaped = inputs_json.replace('"', r"\"")
532        source_escaped = self.source.replace('"', r"\"")
533
534        def get_example_command(preview: bool, escape: bool = False):
535            q: str = '"' if escape else ""
536
537            return [
538                "bioimageio",
539                "predict",
540                # --no-preview not supported for py=3.8
541                *(["--preview"] if preview else []),
542                "--overwrite",
543                *(["--blockwise"] if self.blockwise else []),
544                f"--stats={q}{stats}{q}",
545                f"--inputs={q}{inputs_escaped if escape else inputs_json}{q}",
546                f"--outputs={q}{output_pattern}{q}",
547                f"{q}{source_escaped if escape else self.source}{q}",
548            ]
549
550        if Path(YAML_FILE).exists():
551            logger.info(
552                "temporarily removing '{}' to execute example prediction", YAML_FILE
553            )
554            yaml_file_content = Path(YAML_FILE).read_bytes()
555            Path(YAML_FILE).unlink()
556
557        try:
558            _ = subprocess.run(get_example_command(True), check=True)
559            _ = subprocess.run(get_example_command(False), check=True)
560        finally:
561            if yaml_file_content is not None:
562                _ = Path(YAML_FILE).write_bytes(yaml_file_content)
563                logger.debug("restored '{}'", YAML_FILE)
564
565        print(
566            "🎉 Sucessfully ran example prediction!\n"
567            + "To predict the example input using the CLI example config file"
568            + f" {example_path / YAML_FILE}, execute `bioimageio predict` from {example_path}:\n"
569            + f"$ cd {str(example_path)}\n"
570            + f'$ bioimageio predict "{source_escaped}"\n\n'
571            + "Alternatively run the following command"
572            + " in the current workind directory, not the example folder:\n$ "
573            + " ".join(get_example_command(False, escape=True))
574            + f"\n(note that a local '{JSON_FILE}' or '{YAML_FILE}' may interfere with this)"
575        )
576
577    def run(self):
578        if self.example:
579            return self._example()
580
581        model_descr = ensure_description_is_model(self.descr)
582
583        input_ids = get_member_ids(model_descr.inputs)
584        output_ids = get_member_ids(model_descr.outputs)
585
586        minimum_input_ids = tuple(
587            str(ipt.id) if isinstance(ipt, v0_5.InputTensorDescr) else str(ipt.name)
588            for ipt in model_descr.inputs
589            if not isinstance(ipt, v0_5.InputTensorDescr) or not ipt.optional
590        )
591        maximum_input_ids = tuple(
592            str(ipt.id) if isinstance(ipt, v0_5.InputTensorDescr) else str(ipt.name)
593            for ipt in model_descr.inputs
594        )
595
596        def expand_inputs(i: int, ipt: Union[str, Sequence[str]]) -> Tuple[str, ...]:
597            if isinstance(ipt, str):
598                ipts = tuple(
599                    ipt.format(model_id=self.descr_id, input_id=t) for t in input_ids
600                )
601            else:
602                ipts = tuple(
603                    p.format(model_id=self.descr_id, input_id=t)
604                    for t, p in zip(input_ids, ipt)
605                )
606
607            if len(set(ipts)) < len(ipts):
608                if len(minimum_input_ids) == len(maximum_input_ids):
609                    n = len(minimum_input_ids)
610                else:
611                    n = f"{len(minimum_input_ids)}-{len(maximum_input_ids)}"
612
613                raise ValueError(
614                    f"[input sample #{i}] Include '{{input_id}}' in path pattern or explicitly specify {n} distinct input paths (got {ipt})"
615                )
616
617            if len(ipts) < len(minimum_input_ids):
618                raise ValueError(
619                    f"[input sample #{i}] Expected at least {len(minimum_input_ids)} inputs {minimum_input_ids}, got {ipts}"
620                )
621
622            if len(ipts) > len(maximum_input_ids):
623                raise ValueError(
624                    f"Expected at most {len(maximum_input_ids)} inputs {maximum_input_ids}, got {ipts}"
625                )
626
627            return ipts
628
629        inputs = [expand_inputs(i, ipt) for i, ipt in enumerate(self.inputs, start=1)]
630
631        sample_paths_in = [
632            {t: Path(p) for t, p in zip(input_ids, ipts)} for ipts in inputs
633        ]
634
635        sample_ids = _get_sample_ids(sample_paths_in)
636
637        def expand_outputs():
638            if isinstance(self.outputs, str):
639                outputs = [
640                    tuple(
641                        Path(
642                            self.outputs.format(
643                                model_id=self.descr_id, output_id=t, sample_id=s
644                            )
645                        )
646                        for t in output_ids
647                    )
648                    for s in sample_ids
649                ]
650            else:
651                outputs = [
652                    tuple(
653                        Path(p.format(model_id=self.descr_id, output_id=t, sample_id=s))
654                        for t, p in zip(output_ids, self.outputs)
655                    )
656                    for s in sample_ids
657                ]
658
659            for i, out in enumerate(outputs, start=1):
660                if len(set(out)) < len(out):
661                    raise ValueError(
662                        f"[output sample #{i}] Include '{{output_id}}' in path pattern or explicitly specify {len(output_ids)} distinct output paths (got {out})"
663                    )
664
665                if len(out) != len(output_ids):
666                    raise ValueError(
667                        f"[output sample #{i}] Expected {len(output_ids)} outputs {output_ids}, got {out}"
668                    )
669
670            return outputs
671
672        outputs = expand_outputs()
673
674        sample_paths_out = [
675            {MemberId(t): Path(p) for t, p in zip(output_ids, out)} for out in outputs
676        ]
677
678        if not self.overwrite:
679            for sample_paths in sample_paths_out:
680                for p in sample_paths.values():
681                    if p.exists():
682                        raise FileExistsError(
683                            f"{p} already exists. use --overwrite to (re-)write outputs anyway."
684                        )
685        if self.preview:
686            print("🛈 bioimageio prediction preview structure:")
687            pprint(
688                {
689                    "{sample_id}": dict(
690                        inputs={"{input_id}": "<input path>"},
691                        outputs={"{output_id}": "<output path>"},
692                    )
693                }
694            )
695            print("🔎 bioimageio prediction preview output:")
696            pprint(
697                {
698                    s: dict(
699                        inputs={t: p.as_posix() for t, p in sp_in.items()},
700                        outputs={t: p.as_posix() for t, p in sp_out.items()},
701                    )
702                    for s, sp_in, sp_out in zip(
703                        sample_ids, sample_paths_in, sample_paths_out
704                    )
705                }
706            )
707            return
708
709        def input_dataset(stat: Stat):
710            for s, sp_in in zip(sample_ids, sample_paths_in):
711                yield load_sample_for_model(
712                    model=model_descr,
713                    paths=sp_in,
714                    stat=stat,
715                    sample_id=s,
716                )
717
718        stat: Dict[Measure, MeasureValue] = dict(
719            _get_stat(
720                model_descr, input_dataset({}), len(sample_ids), self.stats
721            ).items()
722        )
723
724        pp = create_prediction_pipeline(
725            model_descr,
726            weight_format=None if self.weight_format == "any" else self.weight_format,
727        )
728        predict_method = (
729            pp.predict_sample_with_blocking
730            if self.blockwise
731            else pp.predict_sample_without_blocking
732        )
733
734        for sample_in, sp_out in tqdm(
735            zip(input_dataset(dict(stat)), sample_paths_out),
736            total=len(inputs),
737            desc=f"predict with {self.descr_id}",
738            unit="sample",
739        ):
740            sample_out = predict_method(sample_in)
741            save_sample(sp_out, sample_out)
742
743
744class AddWeightsCmd(CmdBase, WithSource, WithSummaryLogging):
745    output: CliPositionalArg[Path]
746    """The path to write the updated model package to."""
747
748    source_format: Optional[SupportedWeightsFormat] = Field(None, alias="source-format")
749    """Exclusively use these weights to convert to other formats."""
750
751    target_format: Optional[SupportedWeightsFormat] = Field(None, alias="target-format")
752    """Exclusively add this weight format."""
753
754    verbose: bool = False
755    """Log more (error) output."""
756
757    tracing: bool = True
758    """Allow tracing when converting pytorch_state_dict to torchscript
759    (still uses scripting if possible)."""
760
761    def run(self):
762        model_descr = ensure_description_is_model(self.descr)
763        if isinstance(model_descr, v0_4.ModelDescr):
764            raise TypeError(
765                f"model format {model_descr.format_version} not supported."
766                + " Please update the model first."
767            )
768        updated_model_descr = add_weights(
769            model_descr,
770            output_path=self.output,
771            source_format=self.source_format,
772            target_format=self.target_format,
773            verbose=self.verbose,
774            allow_tracing=self.tracing,
775        )
776        self.log(updated_model_descr)
777
778
779JSON_FILE = "bioimageio-cli.json"
780YAML_FILE = "bioimageio-cli.yaml"
781
782
783class Bioimageio(
784    BaseSettings,
785    cli_implicit_flags=True,
786    cli_parse_args=True,
787    cli_prog_name="bioimageio",
788    cli_use_class_docs_for_groups=True,
789    use_attribute_docstrings=True,
790):
791    """bioimageio - CLI for bioimage.io resources 🦒"""
792
793    model_config = SettingsConfigDict(
794        json_file=JSON_FILE,
795        yaml_file=YAML_FILE,
796    )
797
798    validate_format: CliSubCommand[ValidateFormatCmd] = Field(alias="validate-format")
799    "Check a resource's metadata format"
800
801    test: CliSubCommand[TestCmd]
802    "Test a bioimageio resource (beyond meta data formatting)"
803
804    package: CliSubCommand[PackageCmd]
805    "Package a resource"
806
807    predict: CliSubCommand[PredictCmd]
808    "Predict with a model resource"
809
810    update_format: CliSubCommand[UpdateFormatCmd] = Field(alias="update-format")
811    """Update the metadata format"""
812
813    update_hashes: CliSubCommand[UpdateHashesCmd] = Field(alias="update-hashes")
814    """Create a bioimageio.yaml description with updated file hashes."""
815
816    add_weights: CliSubCommand[AddWeightsCmd] = Field(alias="add-weights")
817    """Add additional weights to the model descriptions converted from available
818    formats to improve deployability."""
819
820    @classmethod
821    def settings_customise_sources(
822        cls,
823        settings_cls: Type[BaseSettings],
824        init_settings: PydanticBaseSettingsSource,
825        env_settings: PydanticBaseSettingsSource,
826        dotenv_settings: PydanticBaseSettingsSource,
827        file_secret_settings: PydanticBaseSettingsSource,
828    ) -> Tuple[PydanticBaseSettingsSource, ...]:
829        cli: CliSettingsSource[BaseSettings] = CliSettingsSource(
830            settings_cls,
831            cli_parse_args=True,
832            formatter_class=RawTextHelpFormatter,
833        )
834        sys_args = pformat(sys.argv)
835        logger.info("starting CLI with arguments:\n{}", sys_args)
836        return (
837            cli,
838            init_settings,
839            YamlConfigSettingsSource(settings_cls),
840            JsonConfigSettingsSource(settings_cls),
841        )
842
843    @model_validator(mode="before")
844    @classmethod
845    def _log(cls, data: Any):
846        logger.info(
847            "loaded CLI input:\n{}",
848            pformat({k: v for k, v in data.items() if v is not None}),
849        )
850        return data
851
852    def run(self):
853        logger.info(
854            "executing CLI command:\n{}",
855            pformat({k: v for k, v in self.model_dump().items() if v is not None}),
856        )
857        cmd = (
858            self.add_weights
859            or self.package
860            or self.predict
861            or self.test
862            or self.update_format
863            or self.update_hashes
864            or self.validate_format
865        )
866        assert cmd is not None
867        cmd.run()
868
869
870assert isinstance(Bioimageio.__doc__, str)
871Bioimageio.__doc__ += f"""
872
873library versions:
874  bioimageio.core {__version__}
875  bioimageio.spec {bioimageio.spec.__version__}
876
877spec format versions:
878        model RDF {ModelDescr.implemented_format_version}
879      dataset RDF {DatasetDescr.implemented_format_version}
880     notebook RDF {NotebookDescr.implemented_format_version}
881
882"""
883
884
885def _get_sample_ids(
886    input_paths: Sequence[Mapping[MemberId, Path]],
887) -> Sequence[SampleId]:
888    """Get sample ids for given input paths, based on the common path per sample.
889
890    Falls back to sample01, samle02, etc..."""
891
892    matcher = SequenceMatcher()
893
894    def get_common_seq(seqs: Sequence[Sequence[str]]) -> Sequence[str]:
895        """extract a common sequence from multiple sequences
896        (order sensitive; strips whitespace and slashes)
897        """
898        common = seqs[0]
899
900        for seq in seqs[1:]:
901            if not seq:
902                continue
903            matcher.set_seqs(common, seq)
904            i, _, size = matcher.find_longest_match()
905            common = common[i : i + size]
906
907        if isinstance(common, str):
908            common = common.strip().strip("/")
909        else:
910            common = [cs for c in common if (cs := c.strip().strip("/"))]
911
912        if not common:
913            raise ValueError(f"failed to find common sequence for {seqs}")
914
915        return common
916
917    def get_shorter_diff(seqs: Sequence[Sequence[str]]) -> List[Sequence[str]]:
918        """get a shorter sequence whose entries are still unique
919        (order sensitive, not minimal sequence)
920        """
921        min_seq_len = min(len(s) for s in seqs)
922        # cut from the start
923        for start in range(min_seq_len - 1, -1, -1):
924            shortened = [s[start:] for s in seqs]
925            if len(set(shortened)) == len(seqs):
926                min_seq_len -= start
927                break
928        else:
929            seen: Set[Sequence[str]] = set()
930            dupes = [s for s in seqs if s in seen or seen.add(s)]
931            raise ValueError(f"Found duplicate entries {dupes}")
932
933        # cut from the end
934        for end in range(min_seq_len - 1, 1, -1):
935            shortened = [s[:end] for s in shortened]
936            if len(set(shortened)) == len(seqs):
937                break
938
939        return shortened
940
941    full_tensor_ids = [
942        sorted(
943            p.resolve().with_suffix("").as_posix() for p in input_sample_paths.values()
944        )
945        for input_sample_paths in input_paths
946    ]
947    try:
948        long_sample_ids = [get_common_seq(t) for t in full_tensor_ids]
949        sample_ids = get_shorter_diff(long_sample_ids)
950    except ValueError as e:
951        raise ValueError(f"failed to extract sample ids: {e}")
952
953    return sample_ids
WEIGHT_FORMAT_ALIASES = AliasChoices(choices=['weight-format', 'weights-format', 'weight_format', 'weights_format'])
class CmdBase(pydantic.main.BaseModel):
95class CmdBase(BaseModel, use_attribute_docstrings=True, cli_implicit_flags=True):
96    pass

!!! abstract "Usage Documentation" Models

A base class for creating Pydantic models.

Attributes:
  • __class_vars__: The names of the class variables defined on the model.
  • __private_attributes__: Metadata about the private attributes of the model.
  • __signature__: The synthesized __init__ [Signature][inspect.Signature] of the model.
  • __pydantic_complete__: Whether model building is completed, or if there are still undefined fields.
  • __pydantic_core_schema__: The core schema of the model.
  • __pydantic_custom_init__: Whether the model has a custom __init__ function.
  • __pydantic_decorators__: Metadata containing the decorators defined on the model. This replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
  • __pydantic_generic_metadata__: Metadata for generic models; contains data used for a similar purpose to __args__, __origin__, __parameters__ in typing-module generics. May eventually be replaced by these.
  • __pydantic_parent_namespace__: Parent namespace of the model, used for automatic rebuilding of models.
  • __pydantic_post_init__: The name of the post-init method for the model, if defined.
  • __pydantic_root_model__: Whether the model is a [RootModel][pydantic.root_model.RootModel].
  • __pydantic_serializer__: The pydantic-core SchemaSerializer used to dump instances of the model.
  • __pydantic_validator__: The pydantic-core SchemaValidator used to validate instances of the model.
  • __pydantic_fields__: A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
  • __pydantic_computed_fields__: A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
  • __pydantic_extra__: A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra] is set to 'allow'.
  • __pydantic_fields_set__: The names of fields explicitly set during instantiation.
  • __pydantic_private__: Values of private attributes set on the model instance.
model_config: ClassVar[pydantic.config.ConfigDict] = {'use_attribute_docstrings': True, 'cli_implicit_flags': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class ArgMixin(pydantic.main.BaseModel):
 99class ArgMixin(BaseModel, use_attribute_docstrings=True, cli_implicit_flags=True):
100    pass

!!! abstract "Usage Documentation" Models

A base class for creating Pydantic models.

Attributes:
  • __class_vars__: The names of the class variables defined on the model.
  • __private_attributes__: Metadata about the private attributes of the model.
  • __signature__: The synthesized __init__ [Signature][inspect.Signature] of the model.
  • __pydantic_complete__: Whether model building is completed, or if there are still undefined fields.
  • __pydantic_core_schema__: The core schema of the model.
  • __pydantic_custom_init__: Whether the model has a custom __init__ function.
  • __pydantic_decorators__: Metadata containing the decorators defined on the model. This replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
  • __pydantic_generic_metadata__: Metadata for generic models; contains data used for a similar purpose to __args__, __origin__, __parameters__ in typing-module generics. May eventually be replaced by these.
  • __pydantic_parent_namespace__: Parent namespace of the model, used for automatic rebuilding of models.
  • __pydantic_post_init__: The name of the post-init method for the model, if defined.
  • __pydantic_root_model__: Whether the model is a [RootModel][pydantic.root_model.RootModel].
  • __pydantic_serializer__: The pydantic-core SchemaSerializer used to dump instances of the model.
  • __pydantic_validator__: The pydantic-core SchemaValidator used to validate instances of the model.
  • __pydantic_fields__: A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
  • __pydantic_computed_fields__: A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
  • __pydantic_extra__: A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra] is set to 'allow'.
  • __pydantic_fields_set__: The names of fields explicitly set during instantiation.
  • __pydantic_private__: Values of private attributes set on the model instance.
model_config: ClassVar[pydantic.config.ConfigDict] = {'use_attribute_docstrings': True, 'cli_implicit_flags': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class WithSummaryLogging(ArgMixin):
103class WithSummaryLogging(ArgMixin):
104    summary: List[Union[Literal["display"], Path]] = Field(
105        default_factory=lambda: ["display"],
106        examples=[
107            Path("summary.md"),
108            Path("bioimageio_summaries/"),
109            ["display", Path("summary.md")],
110        ],
111    )
112    """Display the validation summary or save it as JSON, Markdown or HTML.
113    The format is chosen based on the suffix: `.json`, `.md`, `.html`.
114    If a folder is given (path w/o suffix) the summary is saved in all formats.
115    Choose/add `"display"` to render the validation summary to the terminal.
116    """
117
118    def log(self, descr: Union[ResourceDescr, InvalidDescr]):
119        _ = descr.validation_summary.log(self.summary)

!!! abstract "Usage Documentation" Models

A base class for creating Pydantic models.

Attributes:
  • __class_vars__: The names of the class variables defined on the model.
  • __private_attributes__: Metadata about the private attributes of the model.
  • __signature__: The synthesized __init__ [Signature][inspect.Signature] of the model.
  • __pydantic_complete__: Whether model building is completed, or if there are still undefined fields.
  • __pydantic_core_schema__: The core schema of the model.
  • __pydantic_custom_init__: Whether the model has a custom __init__ function.
  • __pydantic_decorators__: Metadata containing the decorators defined on the model. This replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
  • __pydantic_generic_metadata__: Metadata for generic models; contains data used for a similar purpose to __args__, __origin__, __parameters__ in typing-module generics. May eventually be replaced by these.
  • __pydantic_parent_namespace__: Parent namespace of the model, used for automatic rebuilding of models.
  • __pydantic_post_init__: The name of the post-init method for the model, if defined.
  • __pydantic_root_model__: Whether the model is a [RootModel][pydantic.root_model.RootModel].
  • __pydantic_serializer__: The pydantic-core SchemaSerializer used to dump instances of the model.
  • __pydantic_validator__: The pydantic-core SchemaValidator used to validate instances of the model.
  • __pydantic_fields__: A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
  • __pydantic_computed_fields__: A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
  • __pydantic_extra__: A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra] is set to 'allow'.
  • __pydantic_fields_set__: The names of fields explicitly set during instantiation.
  • __pydantic_private__: Values of private attributes set on the model instance.
summary: List[Union[Literal['display'], pathlib.Path]]

Display the validation summary or save it as JSON, Markdown or HTML. The format is chosen based on the suffix: .json, .md, .html. If a folder is given (path w/o suffix) the summary is saved in all formats. Choose/add "display" to render the validation summary to the terminal.

def log( self, descr: Union[Annotated[Union[Annotated[Union[Annotated[bioimageio.spec.application.v0_2.ApplicationDescr, FieldInfo(annotation=NoneType, required=True, title='application 0.2')], Annotated[bioimageio.spec.ApplicationDescr, FieldInfo(annotation=NoneType, required=True, title='application 0.3')]], Discriminator(discriminator='format_version', custom_error_type=None, custom_error_message=None, custom_error_context=None), FieldInfo(annotation=NoneType, required=True, title='application')], Annotated[Union[Annotated[bioimageio.spec.dataset.v0_2.DatasetDescr, FieldInfo(annotation=NoneType, required=True, title='dataset 0.2')], Annotated[bioimageio.spec.DatasetDescr, FieldInfo(annotation=NoneType, required=True, title='dataset 0.3')]], Discriminator(discriminator='format_version', custom_error_type=None, custom_error_message=None, custom_error_context=None), FieldInfo(annotation=NoneType, required=True, title='dataset')], Annotated[Union[Annotated[bioimageio.spec.model.v0_4.ModelDescr, FieldInfo(annotation=NoneType, required=True, title='model 0.4')], Annotated[bioimageio.spec.ModelDescr, FieldInfo(annotation=NoneType, required=True, title='model 0.5')]], Discriminator(discriminator='format_version', custom_error_type=None, custom_error_message=None, custom_error_context=None), FieldInfo(annotation=NoneType, required=True, title='model')], Annotated[Union[Annotated[bioimageio.spec.NotebookDescr, FieldInfo(annotation=NoneType, required=True, title='notebook 0.2')], Annotated[bioimageio.spec.NotebookDescr, FieldInfo(annotation=NoneType, required=True, title='notebook 0.3')]], Discriminator(discriminator='format_version', custom_error_type=None, custom_error_message=None, custom_error_context=None), FieldInfo(annotation=NoneType, required=True, title='notebook')]], Discriminator(discriminator='type', custom_error_type=None, custom_error_message=None, custom_error_context=None)], Annotated[Union[Annotated[bioimageio.spec.generic.v0_2.GenericDescr, FieldInfo(annotation=NoneType, required=True, title='generic 0.2')], Annotated[bioimageio.spec.GenericDescr, FieldInfo(annotation=NoneType, required=True, title='generic 0.3')]], Discriminator(discriminator='format_version', custom_error_type=None, custom_error_message=None, custom_error_context=None), FieldInfo(annotation=NoneType, required=True, title='generic')], bioimageio.spec.InvalidDescr]):
118    def log(self, descr: Union[ResourceDescr, InvalidDescr]):
119        _ = descr.validation_summary.log(self.summary)
model_config: ClassVar[pydantic.config.ConfigDict] = {'use_attribute_docstrings': True, 'cli_implicit_flags': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class WithSource(ArgMixin):
122class WithSource(ArgMixin):
123    source: CliPositionalArg[str]
124    """Url/path to a (folder with a) bioimageio.yaml/rdf.yaml file
125    or a bioimage.io resource identifier, e.g. 'affable-shark'"""
126
127    @cached_property
128    def descr(self):
129        return load_description(self.source)
130
131    @property
132    def descr_id(self) -> str:
133        """a more user-friendly description id
134        (replacing legacy ids with their nicknames)
135        """
136        if isinstance(self.descr, InvalidDescr):
137            return str(getattr(self.descr, "id", getattr(self.descr, "name")))
138
139        nickname = None
140        if (
141            isinstance(self.descr.config, v0_5.Config)
142            and (bio_config := self.descr.config.bioimageio)
143            and bio_config.model_extra is not None
144        ):
145            nickname = bio_config.model_extra.get("nickname")
146
147        return str(nickname or self.descr.id or self.descr.name)

!!! abstract "Usage Documentation" Models

A base class for creating Pydantic models.

Attributes:
  • __class_vars__: The names of the class variables defined on the model.
  • __private_attributes__: Metadata about the private attributes of the model.
  • __signature__: The synthesized __init__ [Signature][inspect.Signature] of the model.
  • __pydantic_complete__: Whether model building is completed, or if there are still undefined fields.
  • __pydantic_core_schema__: The core schema of the model.
  • __pydantic_custom_init__: Whether the model has a custom __init__ function.
  • __pydantic_decorators__: Metadata containing the decorators defined on the model. This replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
  • __pydantic_generic_metadata__: Metadata for generic models; contains data used for a similar purpose to __args__, __origin__, __parameters__ in typing-module generics. May eventually be replaced by these.
  • __pydantic_parent_namespace__: Parent namespace of the model, used for automatic rebuilding of models.
  • __pydantic_post_init__: The name of the post-init method for the model, if defined.
  • __pydantic_root_model__: Whether the model is a [RootModel][pydantic.root_model.RootModel].
  • __pydantic_serializer__: The pydantic-core SchemaSerializer used to dump instances of the model.
  • __pydantic_validator__: The pydantic-core SchemaValidator used to validate instances of the model.
  • __pydantic_fields__: A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
  • __pydantic_computed_fields__: A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
  • __pydantic_extra__: A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra] is set to 'allow'.
  • __pydantic_fields_set__: The names of fields explicitly set during instantiation.
  • __pydantic_private__: Values of private attributes set on the model instance.
source: Annotated[str, <class 'pydantic_settings.sources.types._CliPositionalArg'>]

Url/path to a (folder with a) bioimageio.yaml/rdf.yaml file or a bioimage.io resource identifier, e.g. 'affable-shark'

descr
127    @cached_property
128    def descr(self):
129        return load_description(self.source)
descr_id: str
131    @property
132    def descr_id(self) -> str:
133        """a more user-friendly description id
134        (replacing legacy ids with their nicknames)
135        """
136        if isinstance(self.descr, InvalidDescr):
137            return str(getattr(self.descr, "id", getattr(self.descr, "name")))
138
139        nickname = None
140        if (
141            isinstance(self.descr.config, v0_5.Config)
142            and (bio_config := self.descr.config.bioimageio)
143            and bio_config.model_extra is not None
144        ):
145            nickname = bio_config.model_extra.get("nickname")
146
147        return str(nickname or self.descr.id or self.descr.name)

a more user-friendly description id (replacing legacy ids with their nicknames)

model_config: ClassVar[pydantic.config.ConfigDict] = {'use_attribute_docstrings': True, 'cli_implicit_flags': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class ValidateFormatCmd(CmdBase, WithSource, WithSummaryLogging):
150class ValidateFormatCmd(CmdBase, WithSource, WithSummaryLogging):
151    """Validate the meta data format of a bioimageio resource."""
152
153    perform_io_checks: bool = Field(
154        settings.perform_io_checks, alias="perform-io-checks"
155    )
156    """Wether or not to perform validations that requires downloading remote files.
157    Note: Default value is set by `BIOIMAGEIO_PERFORM_IO_CHECKS` environment variable.
158    """
159
160    @cached_property
161    def descr(self):
162        return load_description(self.source, perform_io_checks=self.perform_io_checks)
163
164    def run(self):
165        self.log(self.descr)
166        sys.exit(
167            0
168            if self.descr.validation_summary.status in ("valid-format", "passed")
169            else 1
170        )

Validate the meta data format of a bioimageio resource.

perform_io_checks: bool

Wether or not to perform validations that requires downloading remote files. Note: Default value is set by BIOIMAGEIO_PERFORM_IO_CHECKS environment variable.

descr
160    @cached_property
161    def descr(self):
162        return load_description(self.source, perform_io_checks=self.perform_io_checks)
def run(self):
164    def run(self):
165        self.log(self.descr)
166        sys.exit(
167            0
168            if self.descr.validation_summary.status in ("valid-format", "passed")
169            else 1
170        )
model_config: ClassVar[pydantic.config.ConfigDict] = {'use_attribute_docstrings': True, 'cli_implicit_flags': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class TestCmd(CmdBase, WithSource, WithSummaryLogging):
173class TestCmd(CmdBase, WithSource, WithSummaryLogging):
174    """Test a bioimageio resource (beyond meta data formatting)."""
175
176    weight_format: WeightFormatArgAll = Field(
177        "all",
178        alias="weight-format",
179        validation_alias=WEIGHT_FORMAT_ALIASES,
180    )
181    """The weight format to limit testing to.
182
183    (only relevant for model resources)"""
184
185    devices: Optional[List[str]] = None
186    """Device(s) to use for testing"""
187
188    runtime_env: Union[Literal["currently-active", "as-described"], Path] = Field(
189        "currently-active", alias="runtime-env"
190    )
191    """The python environment to run the tests in
192        - `"currently-active"`: use active Python interpreter
193        - `"as-described"`: generate a conda environment YAML file based on the model
194            weights description.
195        - A path to a conda environment YAML.
196          Note: The `bioimageio.core` dependency will be added automatically if not present.
197    """
198
199    determinism: Literal["seed_only", "full"] = "seed_only"
200    """Modes to improve reproducibility of test outputs."""
201
202    stop_early: bool = Field(
203        False, alias="stop-early", validation_alias=AliasChoices("stop-early", "x")
204    )
205    """Do not run further subtests after a failed one."""
206
207    format_version: Union[FormatVersionPlaceholder, str] = Field(
208        "discover", alias="format-version"
209    )
210    """The format version to use for testing.
211        - 'latest': Use the latest implemented format version for the given resource type (may trigger auto updating)
212        - 'discover': Use the format version as described in the resource description
213        - '0.4', '0.5', ...: Use the specified format version (may trigger auto updating)
214    """
215
216    def run(self):
217        sys.exit(
218            test(
219                self.descr,
220                weight_format=self.weight_format,
221                devices=self.devices,
222                summary=self.summary,
223                runtime_env=self.runtime_env,
224                determinism=self.determinism,
225                format_version=self.format_version,
226            )
227        )

Test a bioimageio resource (beyond meta data formatting).

weight_format: Literal['keras_hdf5', 'onnx', 'pytorch_state_dict', 'tensorflow_saved_model_bundle', 'torchscript', 'all']

The weight format to limit testing to.

(only relevant for model resources)

devices: Optional[List[str]]

Device(s) to use for testing

runtime_env: Union[Literal['currently-active', 'as-described'], pathlib.Path]

The python environment to run the tests in

  • "currently-active": use active Python interpreter
  • "as-described": generate a conda environment YAML file based on the model weights description.
  • A path to a conda environment YAML. Note: The bioimageio.core dependency will be added automatically if not present.
determinism: Literal['seed_only', 'full']

Modes to improve reproducibility of test outputs.

stop_early: bool

Do not run further subtests after a failed one.

format_version: Union[Literal['latest', 'discover'], str]

The format version to use for testing.

  • 'latest': Use the latest implemented format version for the given resource type (may trigger auto updating)
  • 'discover': Use the format version as described in the resource description
  • '0.4', '0.5', ...: Use the specified format version (may trigger auto updating)
def run(self):
216    def run(self):
217        sys.exit(
218            test(
219                self.descr,
220                weight_format=self.weight_format,
221                devices=self.devices,
222                summary=self.summary,
223                runtime_env=self.runtime_env,
224                determinism=self.determinism,
225                format_version=self.format_version,
226            )
227        )
model_config: ClassVar[pydantic.config.ConfigDict] = {'use_attribute_docstrings': True, 'cli_implicit_flags': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class PackageCmd(CmdBase, WithSource, WithSummaryLogging):
230class PackageCmd(CmdBase, WithSource, WithSummaryLogging):
231    """Save a resource's metadata with its associated files."""
232
233    path: CliPositionalArg[Path]
234    """The path to write the (zipped) package to.
235    If it does not have a `.zip` suffix
236    this command will save the package as an unzipped folder instead."""
237
238    weight_format: WeightFormatArgAll = Field(
239        "all",
240        alias="weight-format",
241        validation_alias=WEIGHT_FORMAT_ALIASES,
242    )
243    """The weight format to include in the package (for model descriptions only)."""
244
245    def run(self):
246        if isinstance(self.descr, InvalidDescr):
247            self.log(self.descr)
248            raise ValueError(f"Invalid {self.descr.type} description.")
249
250        sys.exit(
251            package(
252                self.descr,
253                self.path,
254                weight_format=self.weight_format,
255            )
256        )

Save a resource's metadata with its associated files.

path: Annotated[pathlib.Path, <class 'pydantic_settings.sources.types._CliPositionalArg'>]

The path to write the (zipped) package to. If it does not have a .zip suffix this command will save the package as an unzipped folder instead.

weight_format: Literal['keras_hdf5', 'onnx', 'pytorch_state_dict', 'tensorflow_saved_model_bundle', 'torchscript', 'all']

The weight format to include in the package (for model descriptions only).

def run(self):
245    def run(self):
246        if isinstance(self.descr, InvalidDescr):
247            self.log(self.descr)
248            raise ValueError(f"Invalid {self.descr.type} description.")
249
250        sys.exit(
251            package(
252                self.descr,
253                self.path,
254                weight_format=self.weight_format,
255            )
256        )
model_config: ClassVar[pydantic.config.ConfigDict] = {'use_attribute_docstrings': True, 'cli_implicit_flags': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class UpdateCmdBase(CmdBase, WithSource, abc.ABC):
293class UpdateCmdBase(CmdBase, WithSource, ABC):
294    output: Union[Literal["display", "stdout"], Path] = "display"
295    """Output updated bioimageio.yaml to the terminal or write to a file.
296    Notes:
297    - `"display"`: Render to the terminal with syntax highlighting.
298    - `"stdout"`: Write to sys.stdout without syntax highligthing.
299      (More convenient for copying the updated bioimageio.yaml from the terminal.)
300    """
301
302    diff: Union[bool, Path] = Field(True, alias="diff")
303    """Output a diff of original and updated bioimageio.yaml.
304    If a given path has an `.html` extension, a standalone HTML file is written,
305    otherwise the diff is saved in unified diff format (pure text).
306    """
307
308    exclude_unset: bool = Field(True, alias="exclude-unset")
309    """Exclude fields that have not explicitly be set."""
310
311    exclude_defaults: bool = Field(False, alias="exclude-defaults")
312    """Exclude fields that have the default value (even if set explicitly)."""
313
314    @cached_property
315    def updated(self) -> Union[ResourceDescr, InvalidDescr]:
316        raise NotImplementedError
317
318    def run(self):
319        original_yaml = open_bioimageio_yaml(self.source).unparsed_content
320        assert isinstance(original_yaml, str)
321        stream = StringIO()
322
323        save_bioimageio_yaml_only(
324            self.updated,
325            stream,
326            exclude_unset=self.exclude_unset,
327            exclude_defaults=self.exclude_defaults,
328        )
329        updated_yaml = stream.getvalue()
330
331        diff = compare(
332            original_yaml.split("\n"),
333            updated_yaml.split("\n"),
334            diff_format=(
335                "html"
336                if isinstance(self.diff, Path) and self.diff.suffix == ".html"
337                else "unified"
338            ),
339        )
340
341        if isinstance(self.diff, Path):
342            _ = self.diff.write_text(diff, encoding="utf-8")
343        elif self.diff:
344            console = rich.console.Console()
345            diff_md = f"## Diff\n\n````````diff\n{diff}\n````````"
346            console.print(rich.markdown.Markdown(diff_md))
347
348        if isinstance(self.output, Path):
349            _ = self.output.write_text(updated_yaml, encoding="utf-8")
350            logger.info(f"written updated description to {self.output}")
351        elif self.output == "display":
352            updated_md = f"## Updated bioimageio.yaml\n\n```yaml\n{updated_yaml}\n```"
353            rich.console.Console().print(rich.markdown.Markdown(updated_md))
354        elif self.output == "stdout":
355            print(updated_yaml)
356        else:
357            assert_never(self.output)
358
359        if isinstance(self.updated, InvalidDescr):
360            logger.warning("Update resulted in invalid description")
361            _ = self.updated.validation_summary.display()

!!! abstract "Usage Documentation" Models

A base class for creating Pydantic models.

Attributes:
  • __class_vars__: The names of the class variables defined on the model.
  • __private_attributes__: Metadata about the private attributes of the model.
  • __signature__: The synthesized __init__ [Signature][inspect.Signature] of the model.
  • __pydantic_complete__: Whether model building is completed, or if there are still undefined fields.
  • __pydantic_core_schema__: The core schema of the model.
  • __pydantic_custom_init__: Whether the model has a custom __init__ function.
  • __pydantic_decorators__: Metadata containing the decorators defined on the model. This replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
  • __pydantic_generic_metadata__: Metadata for generic models; contains data used for a similar purpose to __args__, __origin__, __parameters__ in typing-module generics. May eventually be replaced by these.
  • __pydantic_parent_namespace__: Parent namespace of the model, used for automatic rebuilding of models.
  • __pydantic_post_init__: The name of the post-init method for the model, if defined.
  • __pydantic_root_model__: Whether the model is a [RootModel][pydantic.root_model.RootModel].
  • __pydantic_serializer__: The pydantic-core SchemaSerializer used to dump instances of the model.
  • __pydantic_validator__: The pydantic-core SchemaValidator used to validate instances of the model.
  • __pydantic_fields__: A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
  • __pydantic_computed_fields__: A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
  • __pydantic_extra__: A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra] is set to 'allow'.
  • __pydantic_fields_set__: The names of fields explicitly set during instantiation.
  • __pydantic_private__: Values of private attributes set on the model instance.
output: Union[Literal['display', 'stdout'], pathlib.Path]

Output updated bioimageio.yaml to the terminal or write to a file. Notes:

  • "display": Render to the terminal with syntax highlighting.
  • "stdout": Write to sys.stdout without syntax highligthing. (More convenient for copying the updated bioimageio.yaml from the terminal.)
diff: Union[bool, pathlib.Path]

Output a diff of original and updated bioimageio.yaml. If a given path has an .html extension, a standalone HTML file is written, otherwise the diff is saved in unified diff format (pure text).

exclude_unset: bool

Exclude fields that have not explicitly be set.

exclude_defaults: bool

Exclude fields that have the default value (even if set explicitly).

updated: Union[Annotated[Union[Annotated[Union[Annotated[bioimageio.spec.application.v0_2.ApplicationDescr, FieldInfo(annotation=NoneType, required=True, title='application 0.2')], Annotated[bioimageio.spec.ApplicationDescr, FieldInfo(annotation=NoneType, required=True, title='application 0.3')]], Discriminator(discriminator='format_version', custom_error_type=None, custom_error_message=None, custom_error_context=None), FieldInfo(annotation=NoneType, required=True, title='application')], Annotated[Union[Annotated[bioimageio.spec.dataset.v0_2.DatasetDescr, FieldInfo(annotation=NoneType, required=True, title='dataset 0.2')], Annotated[bioimageio.spec.DatasetDescr, FieldInfo(annotation=NoneType, required=True, title='dataset 0.3')]], Discriminator(discriminator='format_version', custom_error_type=None, custom_error_message=None, custom_error_context=None), FieldInfo(annotation=NoneType, required=True, title='dataset')], Annotated[Union[Annotated[bioimageio.spec.model.v0_4.ModelDescr, FieldInfo(annotation=NoneType, required=True, title='model 0.4')], Annotated[bioimageio.spec.ModelDescr, FieldInfo(annotation=NoneType, required=True, title='model 0.5')]], Discriminator(discriminator='format_version', custom_error_type=None, custom_error_message=None, custom_error_context=None), FieldInfo(annotation=NoneType, required=True, title='model')], Annotated[Union[Annotated[bioimageio.spec.NotebookDescr, FieldInfo(annotation=NoneType, required=True, title='notebook 0.2')], Annotated[bioimageio.spec.NotebookDescr, FieldInfo(annotation=NoneType, required=True, title='notebook 0.3')]], Discriminator(discriminator='format_version', custom_error_type=None, custom_error_message=None, custom_error_context=None), FieldInfo(annotation=NoneType, required=True, title='notebook')]], Discriminator(discriminator='type', custom_error_type=None, custom_error_message=None, custom_error_context=None)], Annotated[Union[Annotated[bioimageio.spec.generic.v0_2.GenericDescr, FieldInfo(annotation=NoneType, required=True, title='generic 0.2')], Annotated[bioimageio.spec.GenericDescr, FieldInfo(annotation=NoneType, required=True, title='generic 0.3')]], Discriminator(discriminator='format_version', custom_error_type=None, custom_error_message=None, custom_error_context=None), FieldInfo(annotation=NoneType, required=True, title='generic')], bioimageio.spec.InvalidDescr]
314    @cached_property
315    def updated(self) -> Union[ResourceDescr, InvalidDescr]:
316        raise NotImplementedError
def run(self):
318    def run(self):
319        original_yaml = open_bioimageio_yaml(self.source).unparsed_content
320        assert isinstance(original_yaml, str)
321        stream = StringIO()
322
323        save_bioimageio_yaml_only(
324            self.updated,
325            stream,
326            exclude_unset=self.exclude_unset,
327            exclude_defaults=self.exclude_defaults,
328        )
329        updated_yaml = stream.getvalue()
330
331        diff = compare(
332            original_yaml.split("\n"),
333            updated_yaml.split("\n"),
334            diff_format=(
335                "html"
336                if isinstance(self.diff, Path) and self.diff.suffix == ".html"
337                else "unified"
338            ),
339        )
340
341        if isinstance(self.diff, Path):
342            _ = self.diff.write_text(diff, encoding="utf-8")
343        elif self.diff:
344            console = rich.console.Console()
345            diff_md = f"## Diff\n\n````````diff\n{diff}\n````````"
346            console.print(rich.markdown.Markdown(diff_md))
347
348        if isinstance(self.output, Path):
349            _ = self.output.write_text(updated_yaml, encoding="utf-8")
350            logger.info(f"written updated description to {self.output}")
351        elif self.output == "display":
352            updated_md = f"## Updated bioimageio.yaml\n\n```yaml\n{updated_yaml}\n```"
353            rich.console.Console().print(rich.markdown.Markdown(updated_md))
354        elif self.output == "stdout":
355            print(updated_yaml)
356        else:
357            assert_never(self.output)
358
359        if isinstance(self.updated, InvalidDescr):
360            logger.warning("Update resulted in invalid description")
361            _ = self.updated.validation_summary.display()
model_config: ClassVar[pydantic.config.ConfigDict] = {'use_attribute_docstrings': True, 'cli_implicit_flags': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Inherited Members
WithSource
source
descr
descr_id
class UpdateFormatCmd(UpdateCmdBase):
364class UpdateFormatCmd(UpdateCmdBase):
365    """Update the metadata format to the latest format version."""
366
367    exclude_defaults: bool = Field(True, alias="exclude-defaults")
368    """Exclude fields that have the default value (even if set explicitly).
369
370    Note:
371        The update process sets most unset fields explicitly with their default value.
372    """
373
374    perform_io_checks: bool = Field(
375        settings.perform_io_checks, alias="perform-io-checks"
376    )
377    """Wether or not to attempt validation that may require file download.
378    If `True` file hash values are added if not present."""
379
380    @cached_property
381    def updated(self):
382        return update_format(
383            self.source,
384            exclude_defaults=self.exclude_defaults,
385            perform_io_checks=self.perform_io_checks,
386        )

Update the metadata format to the latest format version.

exclude_defaults: bool

Exclude fields that have the default value (even if set explicitly).

Note:

The update process sets most unset fields explicitly with their default value.

perform_io_checks: bool

Wether or not to attempt validation that may require file download. If True file hash values are added if not present.

updated
380    @cached_property
381    def updated(self):
382        return update_format(
383            self.source,
384            exclude_defaults=self.exclude_defaults,
385            perform_io_checks=self.perform_io_checks,
386        )
model_config: ClassVar[pydantic.config.ConfigDict] = {'use_attribute_docstrings': True, 'cli_implicit_flags': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class UpdateHashesCmd(UpdateCmdBase):
389class UpdateHashesCmd(UpdateCmdBase):
390    """Create a bioimageio.yaml description with updated file hashes."""
391
392    @cached_property
393    def updated(self):
394        return update_hashes(self.source)

Create a bioimageio.yaml description with updated file hashes.

updated
392    @cached_property
393    def updated(self):
394        return update_hashes(self.source)
model_config: ClassVar[pydantic.config.ConfigDict] = {'use_attribute_docstrings': True, 'cli_implicit_flags': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class PredictCmd(CmdBase, WithSource):
397class PredictCmd(CmdBase, WithSource):
398    """Run inference on your data with a bioimage.io model."""
399
400    inputs: NotEmpty[List[Union[str, NotEmpty[List[str]]]]] = Field(
401        default_factory=lambda: ["{input_id}/001.tif"]
402    )
403    """Model input sample paths (for each input tensor)
404
405    The input paths are expected to have shape...
406     - (n_samples,) or (n_samples,1) for models expecting a single input tensor
407     - (n_samples,) containing the substring '{input_id}', or
408     - (n_samples, n_model_inputs) to provide each input tensor path explicitly.
409
410    All substrings that are replaced by metadata from the model description:
411    - '{model_id}'
412    - '{input_id}'
413
414    Example inputs to process sample 'a' and 'b'
415    for a model expecting a 'raw' and a 'mask' input tensor:
416    --inputs="[[\\"a_raw.tif\\",\\"a_mask.tif\\"],[\\"b_raw.tif\\",\\"b_mask.tif\\"]]"
417    (Note that JSON double quotes need to be escaped.)
418
419    Alternatively a `bioimageio-cli.yaml` (or `bioimageio-cli.json`) file
420    may provide the arguments, e.g.:
421    ```yaml
422    inputs:
423    - [a_raw.tif, a_mask.tif]
424    - [b_raw.tif, b_mask.tif]
425    ```
426
427    `.npy` and any file extension supported by imageio are supported.
428     Aavailable formats are listed at
429    https://imageio.readthedocs.io/en/stable/formats/index.html#all-formats.
430    Some formats have additional dependencies.
431
432
433    """
434
435    outputs: Union[str, NotEmpty[Tuple[str, ...]]] = (
436        "outputs_{model_id}/{output_id}/{sample_id}.tif"
437    )
438    """Model output path pattern (per output tensor)
439
440    All substrings that are replaced:
441    - '{model_id}' (from model description)
442    - '{output_id}' (from model description)
443    - '{sample_id}' (extracted from input paths)
444
445
446    """
447
448    overwrite: bool = False
449    """allow overwriting existing output files"""
450
451    blockwise: bool = False
452    """process inputs blockwise"""
453
454    stats: Path = Path("dataset_statistics.json")
455    """path to dataset statistics
456    (will be written if it does not exist,
457    but the model requires statistical dataset measures)
458     """
459
460    preview: bool = False
461    """preview which files would be processed
462    and what outputs would be generated."""
463
464    weight_format: WeightFormatArgAny = Field(
465        "any",
466        alias="weight-format",
467        validation_alias=WEIGHT_FORMAT_ALIASES,
468    )
469    """The weight format to use."""
470
471    example: bool = False
472    """generate and run an example
473
474    1. downloads example model inputs
475    2. creates a `{model_id}_example` folder
476    3. writes input arguments to `{model_id}_example/bioimageio-cli.yaml`
477    4. executes a preview dry-run
478    5. executes prediction with example input
479
480
481    """
482
483    def _example(self):
484        model_descr = ensure_description_is_model(self.descr)
485        input_ids = get_member_ids(model_descr.inputs)
486        example_inputs = (
487            model_descr.sample_inputs
488            if isinstance(model_descr, v0_4.ModelDescr)
489            else [
490                t
491                for ipt in model_descr.inputs
492                if (t := ipt.sample_tensor or ipt.test_tensor)
493            ]
494        )
495        if not example_inputs:
496            raise ValueError(f"{self.descr_id} does not specify any example inputs.")
497
498        inputs001: List[str] = []
499        example_path = Path(f"{self.descr_id}_example")
500        example_path.mkdir(exist_ok=True)
501
502        for t, src in zip(input_ids, example_inputs):
503            reader = get_reader(src)
504            dst = Path(f"{example_path}/{t}/001{reader.suffix}")
505            dst.parent.mkdir(parents=True, exist_ok=True)
506            inputs001.append(dst.as_posix())
507            with dst.open("wb") as f:
508                shutil.copyfileobj(reader, f)
509
510        inputs = [inputs001]
511        output_pattern = f"{example_path}/outputs/{{output_id}}/{{sample_id}}.tif"
512
513        bioimageio_cli_path = example_path / YAML_FILE
514        stats_file = "dataset_statistics.json"
515        stats = (example_path / stats_file).as_posix()
516        cli_example_args = dict(
517            inputs=inputs,
518            outputs=output_pattern,
519            stats=stats_file,
520            blockwise=self.blockwise,
521        )
522        assert is_yaml_value(cli_example_args), cli_example_args
523        write_yaml(
524            cli_example_args,
525            bioimageio_cli_path,
526        )
527
528        yaml_file_content = None
529
530        # escaped double quotes
531        inputs_json = json.dumps(inputs)
532        inputs_escaped = inputs_json.replace('"', r"\"")
533        source_escaped = self.source.replace('"', r"\"")
534
535        def get_example_command(preview: bool, escape: bool = False):
536            q: str = '"' if escape else ""
537
538            return [
539                "bioimageio",
540                "predict",
541                # --no-preview not supported for py=3.8
542                *(["--preview"] if preview else []),
543                "--overwrite",
544                *(["--blockwise"] if self.blockwise else []),
545                f"--stats={q}{stats}{q}",
546                f"--inputs={q}{inputs_escaped if escape else inputs_json}{q}",
547                f"--outputs={q}{output_pattern}{q}",
548                f"{q}{source_escaped if escape else self.source}{q}",
549            ]
550
551        if Path(YAML_FILE).exists():
552            logger.info(
553                "temporarily removing '{}' to execute example prediction", YAML_FILE
554            )
555            yaml_file_content = Path(YAML_FILE).read_bytes()
556            Path(YAML_FILE).unlink()
557
558        try:
559            _ = subprocess.run(get_example_command(True), check=True)
560            _ = subprocess.run(get_example_command(False), check=True)
561        finally:
562            if yaml_file_content is not None:
563                _ = Path(YAML_FILE).write_bytes(yaml_file_content)
564                logger.debug("restored '{}'", YAML_FILE)
565
566        print(
567            "🎉 Sucessfully ran example prediction!\n"
568            + "To predict the example input using the CLI example config file"
569            + f" {example_path / YAML_FILE}, execute `bioimageio predict` from {example_path}:\n"
570            + f"$ cd {str(example_path)}\n"
571            + f'$ bioimageio predict "{source_escaped}"\n\n'
572            + "Alternatively run the following command"
573            + " in the current workind directory, not the example folder:\n$ "
574            + " ".join(get_example_command(False, escape=True))
575            + f"\n(note that a local '{JSON_FILE}' or '{YAML_FILE}' may interfere with this)"
576        )
577
578    def run(self):
579        if self.example:
580            return self._example()
581
582        model_descr = ensure_description_is_model(self.descr)
583
584        input_ids = get_member_ids(model_descr.inputs)
585        output_ids = get_member_ids(model_descr.outputs)
586
587        minimum_input_ids = tuple(
588            str(ipt.id) if isinstance(ipt, v0_5.InputTensorDescr) else str(ipt.name)
589            for ipt in model_descr.inputs
590            if not isinstance(ipt, v0_5.InputTensorDescr) or not ipt.optional
591        )
592        maximum_input_ids = tuple(
593            str(ipt.id) if isinstance(ipt, v0_5.InputTensorDescr) else str(ipt.name)
594            for ipt in model_descr.inputs
595        )
596
597        def expand_inputs(i: int, ipt: Union[str, Sequence[str]]) -> Tuple[str, ...]:
598            if isinstance(ipt, str):
599                ipts = tuple(
600                    ipt.format(model_id=self.descr_id, input_id=t) for t in input_ids
601                )
602            else:
603                ipts = tuple(
604                    p.format(model_id=self.descr_id, input_id=t)
605                    for t, p in zip(input_ids, ipt)
606                )
607
608            if len(set(ipts)) < len(ipts):
609                if len(minimum_input_ids) == len(maximum_input_ids):
610                    n = len(minimum_input_ids)
611                else:
612                    n = f"{len(minimum_input_ids)}-{len(maximum_input_ids)}"
613
614                raise ValueError(
615                    f"[input sample #{i}] Include '{{input_id}}' in path pattern or explicitly specify {n} distinct input paths (got {ipt})"
616                )
617
618            if len(ipts) < len(minimum_input_ids):
619                raise ValueError(
620                    f"[input sample #{i}] Expected at least {len(minimum_input_ids)} inputs {minimum_input_ids}, got {ipts}"
621                )
622
623            if len(ipts) > len(maximum_input_ids):
624                raise ValueError(
625                    f"Expected at most {len(maximum_input_ids)} inputs {maximum_input_ids}, got {ipts}"
626                )
627
628            return ipts
629
630        inputs = [expand_inputs(i, ipt) for i, ipt in enumerate(self.inputs, start=1)]
631
632        sample_paths_in = [
633            {t: Path(p) for t, p in zip(input_ids, ipts)} for ipts in inputs
634        ]
635
636        sample_ids = _get_sample_ids(sample_paths_in)
637
638        def expand_outputs():
639            if isinstance(self.outputs, str):
640                outputs = [
641                    tuple(
642                        Path(
643                            self.outputs.format(
644                                model_id=self.descr_id, output_id=t, sample_id=s
645                            )
646                        )
647                        for t in output_ids
648                    )
649                    for s in sample_ids
650                ]
651            else:
652                outputs = [
653                    tuple(
654                        Path(p.format(model_id=self.descr_id, output_id=t, sample_id=s))
655                        for t, p in zip(output_ids, self.outputs)
656                    )
657                    for s in sample_ids
658                ]
659
660            for i, out in enumerate(outputs, start=1):
661                if len(set(out)) < len(out):
662                    raise ValueError(
663                        f"[output sample #{i}] Include '{{output_id}}' in path pattern or explicitly specify {len(output_ids)} distinct output paths (got {out})"
664                    )
665
666                if len(out) != len(output_ids):
667                    raise ValueError(
668                        f"[output sample #{i}] Expected {len(output_ids)} outputs {output_ids}, got {out}"
669                    )
670
671            return outputs
672
673        outputs = expand_outputs()
674
675        sample_paths_out = [
676            {MemberId(t): Path(p) for t, p in zip(output_ids, out)} for out in outputs
677        ]
678
679        if not self.overwrite:
680            for sample_paths in sample_paths_out:
681                for p in sample_paths.values():
682                    if p.exists():
683                        raise FileExistsError(
684                            f"{p} already exists. use --overwrite to (re-)write outputs anyway."
685                        )
686        if self.preview:
687            print("🛈 bioimageio prediction preview structure:")
688            pprint(
689                {
690                    "{sample_id}": dict(
691                        inputs={"{input_id}": "<input path>"},
692                        outputs={"{output_id}": "<output path>"},
693                    )
694                }
695            )
696            print("🔎 bioimageio prediction preview output:")
697            pprint(
698                {
699                    s: dict(
700                        inputs={t: p.as_posix() for t, p in sp_in.items()},
701                        outputs={t: p.as_posix() for t, p in sp_out.items()},
702                    )
703                    for s, sp_in, sp_out in zip(
704                        sample_ids, sample_paths_in, sample_paths_out
705                    )
706                }
707            )
708            return
709
710        def input_dataset(stat: Stat):
711            for s, sp_in in zip(sample_ids, sample_paths_in):
712                yield load_sample_for_model(
713                    model=model_descr,
714                    paths=sp_in,
715                    stat=stat,
716                    sample_id=s,
717                )
718
719        stat: Dict[Measure, MeasureValue] = dict(
720            _get_stat(
721                model_descr, input_dataset({}), len(sample_ids), self.stats
722            ).items()
723        )
724
725        pp = create_prediction_pipeline(
726            model_descr,
727            weight_format=None if self.weight_format == "any" else self.weight_format,
728        )
729        predict_method = (
730            pp.predict_sample_with_blocking
731            if self.blockwise
732            else pp.predict_sample_without_blocking
733        )
734
735        for sample_in, sp_out in tqdm(
736            zip(input_dataset(dict(stat)), sample_paths_out),
737            total=len(inputs),
738            desc=f"predict with {self.descr_id}",
739            unit="sample",
740        ):
741            sample_out = predict_method(sample_in)
742            save_sample(sp_out, sample_out)

Run inference on your data with a bioimage.io model.

inputs: Annotated[List[Union[str, Annotated[List[str], MinLen(min_length=1)]]], MinLen(min_length=1)]

Model input sample paths (for each input tensor)

The input paths are expected to have shape...

  • (n_samples,) or (n_samples,1) for models expecting a single input tensor
  • (n_samples,) containing the substring '{input_id}', or
  • (n_samples, n_model_inputs) to provide each input tensor path explicitly.

All substrings that are replaced by metadata from the model description:

  • '{model_id}'
  • '{input_id}'

Example inputs to process sample 'a' and 'b' for a model expecting a 'raw' and a 'mask' input tensor: --inputs="[[\"a_raw.tif\",\"a_mask.tif\"],[\"b_raw.tif\",\"b_mask.tif\"]]" (Note that JSON double quotes need to be escaped.)

Alternatively a bioimageio-cli.yaml (or bioimageio-cli.json) file may provide the arguments, e.g.:

inputs:
- [a_raw.tif, a_mask.tif]
- [b_raw.tif, b_mask.tif]

.npy and any file extension supported by imageio are supported. Aavailable formats are listed at https://imageio.readthedocs.io/en/stable/formats/index.html#all-formats. Some formats have additional dependencies.

outputs: Union[str, Annotated[Tuple[str, ...], MinLen(min_length=1)]]

Model output path pattern (per output tensor)

All substrings that are replaced:

  • '{model_id}' (from model description)
  • '{output_id}' (from model description)
  • '{sample_id}' (extracted from input paths)
overwrite: bool

allow overwriting existing output files

blockwise: bool

process inputs blockwise

stats: pathlib.Path

path to dataset statistics (will be written if it does not exist, but the model requires statistical dataset measures)

preview: bool

preview which files would be processed and what outputs would be generated.

weight_format: Literal['keras_hdf5', 'onnx', 'pytorch_state_dict', 'tensorflow_saved_model_bundle', 'torchscript', 'any']

The weight format to use.

example: bool

generate and run an example

  1. downloads example model inputs
  2. creates a {model_id}_example folder
  3. writes input arguments to {model_id}_example/bioimageio-cli.yaml
  4. executes a preview dry-run
  5. executes prediction with example input
def run(self):
578    def run(self):
579        if self.example:
580            return self._example()
581
582        model_descr = ensure_description_is_model(self.descr)
583
584        input_ids = get_member_ids(model_descr.inputs)
585        output_ids = get_member_ids(model_descr.outputs)
586
587        minimum_input_ids = tuple(
588            str(ipt.id) if isinstance(ipt, v0_5.InputTensorDescr) else str(ipt.name)
589            for ipt in model_descr.inputs
590            if not isinstance(ipt, v0_5.InputTensorDescr) or not ipt.optional
591        )
592        maximum_input_ids = tuple(
593            str(ipt.id) if isinstance(ipt, v0_5.InputTensorDescr) else str(ipt.name)
594            for ipt in model_descr.inputs
595        )
596
597        def expand_inputs(i: int, ipt: Union[str, Sequence[str]]) -> Tuple[str, ...]:
598            if isinstance(ipt, str):
599                ipts = tuple(
600                    ipt.format(model_id=self.descr_id, input_id=t) for t in input_ids
601                )
602            else:
603                ipts = tuple(
604                    p.format(model_id=self.descr_id, input_id=t)
605                    for t, p in zip(input_ids, ipt)
606                )
607
608            if len(set(ipts)) < len(ipts):
609                if len(minimum_input_ids) == len(maximum_input_ids):
610                    n = len(minimum_input_ids)
611                else:
612                    n = f"{len(minimum_input_ids)}-{len(maximum_input_ids)}"
613
614                raise ValueError(
615                    f"[input sample #{i}] Include '{{input_id}}' in path pattern or explicitly specify {n} distinct input paths (got {ipt})"
616                )
617
618            if len(ipts) < len(minimum_input_ids):
619                raise ValueError(
620                    f"[input sample #{i}] Expected at least {len(minimum_input_ids)} inputs {minimum_input_ids}, got {ipts}"
621                )
622
623            if len(ipts) > len(maximum_input_ids):
624                raise ValueError(
625                    f"Expected at most {len(maximum_input_ids)} inputs {maximum_input_ids}, got {ipts}"
626                )
627
628            return ipts
629
630        inputs = [expand_inputs(i, ipt) for i, ipt in enumerate(self.inputs, start=1)]
631
632        sample_paths_in = [
633            {t: Path(p) for t, p in zip(input_ids, ipts)} for ipts in inputs
634        ]
635
636        sample_ids = _get_sample_ids(sample_paths_in)
637
638        def expand_outputs():
639            if isinstance(self.outputs, str):
640                outputs = [
641                    tuple(
642                        Path(
643                            self.outputs.format(
644                                model_id=self.descr_id, output_id=t, sample_id=s
645                            )
646                        )
647                        for t in output_ids
648                    )
649                    for s in sample_ids
650                ]
651            else:
652                outputs = [
653                    tuple(
654                        Path(p.format(model_id=self.descr_id, output_id=t, sample_id=s))
655                        for t, p in zip(output_ids, self.outputs)
656                    )
657                    for s in sample_ids
658                ]
659
660            for i, out in enumerate(outputs, start=1):
661                if len(set(out)) < len(out):
662                    raise ValueError(
663                        f"[output sample #{i}] Include '{{output_id}}' in path pattern or explicitly specify {len(output_ids)} distinct output paths (got {out})"
664                    )
665
666                if len(out) != len(output_ids):
667                    raise ValueError(
668                        f"[output sample #{i}] Expected {len(output_ids)} outputs {output_ids}, got {out}"
669                    )
670
671            return outputs
672
673        outputs = expand_outputs()
674
675        sample_paths_out = [
676            {MemberId(t): Path(p) for t, p in zip(output_ids, out)} for out in outputs
677        ]
678
679        if not self.overwrite:
680            for sample_paths in sample_paths_out:
681                for p in sample_paths.values():
682                    if p.exists():
683                        raise FileExistsError(
684                            f"{p} already exists. use --overwrite to (re-)write outputs anyway."
685                        )
686        if self.preview:
687            print("🛈 bioimageio prediction preview structure:")
688            pprint(
689                {
690                    "{sample_id}": dict(
691                        inputs={"{input_id}": "<input path>"},
692                        outputs={"{output_id}": "<output path>"},
693                    )
694                }
695            )
696            print("🔎 bioimageio prediction preview output:")
697            pprint(
698                {
699                    s: dict(
700                        inputs={t: p.as_posix() for t, p in sp_in.items()},
701                        outputs={t: p.as_posix() for t, p in sp_out.items()},
702                    )
703                    for s, sp_in, sp_out in zip(
704                        sample_ids, sample_paths_in, sample_paths_out
705                    )
706                }
707            )
708            return
709
710        def input_dataset(stat: Stat):
711            for s, sp_in in zip(sample_ids, sample_paths_in):
712                yield load_sample_for_model(
713                    model=model_descr,
714                    paths=sp_in,
715                    stat=stat,
716                    sample_id=s,
717                )
718
719        stat: Dict[Measure, MeasureValue] = dict(
720            _get_stat(
721                model_descr, input_dataset({}), len(sample_ids), self.stats
722            ).items()
723        )
724
725        pp = create_prediction_pipeline(
726            model_descr,
727            weight_format=None if self.weight_format == "any" else self.weight_format,
728        )
729        predict_method = (
730            pp.predict_sample_with_blocking
731            if self.blockwise
732            else pp.predict_sample_without_blocking
733        )
734
735        for sample_in, sp_out in tqdm(
736            zip(input_dataset(dict(stat)), sample_paths_out),
737            total=len(inputs),
738            desc=f"predict with {self.descr_id}",
739            unit="sample",
740        ):
741            sample_out = predict_method(sample_in)
742            save_sample(sp_out, sample_out)
model_config: ClassVar[pydantic.config.ConfigDict] = {'use_attribute_docstrings': True, 'cli_implicit_flags': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Inherited Members
WithSource
source
descr
descr_id
class AddWeightsCmd(CmdBase, WithSource, WithSummaryLogging):
745class AddWeightsCmd(CmdBase, WithSource, WithSummaryLogging):
746    output: CliPositionalArg[Path]
747    """The path to write the updated model package to."""
748
749    source_format: Optional[SupportedWeightsFormat] = Field(None, alias="source-format")
750    """Exclusively use these weights to convert to other formats."""
751
752    target_format: Optional[SupportedWeightsFormat] = Field(None, alias="target-format")
753    """Exclusively add this weight format."""
754
755    verbose: bool = False
756    """Log more (error) output."""
757
758    tracing: bool = True
759    """Allow tracing when converting pytorch_state_dict to torchscript
760    (still uses scripting if possible)."""
761
762    def run(self):
763        model_descr = ensure_description_is_model(self.descr)
764        if isinstance(model_descr, v0_4.ModelDescr):
765            raise TypeError(
766                f"model format {model_descr.format_version} not supported."
767                + " Please update the model first."
768            )
769        updated_model_descr = add_weights(
770            model_descr,
771            output_path=self.output,
772            source_format=self.source_format,
773            target_format=self.target_format,
774            verbose=self.verbose,
775            allow_tracing=self.tracing,
776        )
777        self.log(updated_model_descr)

!!! abstract "Usage Documentation" Models

A base class for creating Pydantic models.

Attributes:
  • __class_vars__: The names of the class variables defined on the model.
  • __private_attributes__: Metadata about the private attributes of the model.
  • __signature__: The synthesized __init__ [Signature][inspect.Signature] of the model.
  • __pydantic_complete__: Whether model building is completed, or if there are still undefined fields.
  • __pydantic_core_schema__: The core schema of the model.
  • __pydantic_custom_init__: Whether the model has a custom __init__ function.
  • __pydantic_decorators__: Metadata containing the decorators defined on the model. This replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.
  • __pydantic_generic_metadata__: Metadata for generic models; contains data used for a similar purpose to __args__, __origin__, __parameters__ in typing-module generics. May eventually be replaced by these.
  • __pydantic_parent_namespace__: Parent namespace of the model, used for automatic rebuilding of models.
  • __pydantic_post_init__: The name of the post-init method for the model, if defined.
  • __pydantic_root_model__: Whether the model is a [RootModel][pydantic.root_model.RootModel].
  • __pydantic_serializer__: The pydantic-core SchemaSerializer used to dump instances of the model.
  • __pydantic_validator__: The pydantic-core SchemaValidator used to validate instances of the model.
  • __pydantic_fields__: A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects.
  • __pydantic_computed_fields__: A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.
  • __pydantic_extra__: A dictionary containing extra values, if [extra][pydantic.config.ConfigDict.extra] is set to 'allow'.
  • __pydantic_fields_set__: The names of fields explicitly set during instantiation.
  • __pydantic_private__: Values of private attributes set on the model instance.
output: Annotated[pathlib.Path, <class 'pydantic_settings.sources.types._CliPositionalArg'>]

The path to write the updated model package to.

source_format: Optional[Literal['keras_hdf5', 'onnx', 'pytorch_state_dict', 'tensorflow_saved_model_bundle', 'torchscript']]

Exclusively use these weights to convert to other formats.

target_format: Optional[Literal['keras_hdf5', 'onnx', 'pytorch_state_dict', 'tensorflow_saved_model_bundle', 'torchscript']]

Exclusively add this weight format.

verbose: bool

Log more (error) output.

tracing: bool

Allow tracing when converting pytorch_state_dict to torchscript (still uses scripting if possible).

def run(self):
762    def run(self):
763        model_descr = ensure_description_is_model(self.descr)
764        if isinstance(model_descr, v0_4.ModelDescr):
765            raise TypeError(
766                f"model format {model_descr.format_version} not supported."
767                + " Please update the model first."
768            )
769        updated_model_descr = add_weights(
770            model_descr,
771            output_path=self.output,
772            source_format=self.source_format,
773            target_format=self.target_format,
774            verbose=self.verbose,
775            allow_tracing=self.tracing,
776        )
777        self.log(updated_model_descr)
model_config: ClassVar[pydantic.config.ConfigDict] = {'use_attribute_docstrings': True, 'cli_implicit_flags': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

JSON_FILE = 'bioimageio-cli.json'
YAML_FILE = 'bioimageio-cli.yaml'
class Bioimageio(pydantic_settings.main.BaseSettings):
784class Bioimageio(
785    BaseSettings,
786    cli_implicit_flags=True,
787    cli_parse_args=True,
788    cli_prog_name="bioimageio",
789    cli_use_class_docs_for_groups=True,
790    use_attribute_docstrings=True,
791):
792    """bioimageio - CLI for bioimage.io resources 🦒"""
793
794    model_config = SettingsConfigDict(
795        json_file=JSON_FILE,
796        yaml_file=YAML_FILE,
797    )
798
799    validate_format: CliSubCommand[ValidateFormatCmd] = Field(alias="validate-format")
800    "Check a resource's metadata format"
801
802    test: CliSubCommand[TestCmd]
803    "Test a bioimageio resource (beyond meta data formatting)"
804
805    package: CliSubCommand[PackageCmd]
806    "Package a resource"
807
808    predict: CliSubCommand[PredictCmd]
809    "Predict with a model resource"
810
811    update_format: CliSubCommand[UpdateFormatCmd] = Field(alias="update-format")
812    """Update the metadata format"""
813
814    update_hashes: CliSubCommand[UpdateHashesCmd] = Field(alias="update-hashes")
815    """Create a bioimageio.yaml description with updated file hashes."""
816
817    add_weights: CliSubCommand[AddWeightsCmd] = Field(alias="add-weights")
818    """Add additional weights to the model descriptions converted from available
819    formats to improve deployability."""
820
821    @classmethod
822    def settings_customise_sources(
823        cls,
824        settings_cls: Type[BaseSettings],
825        init_settings: PydanticBaseSettingsSource,
826        env_settings: PydanticBaseSettingsSource,
827        dotenv_settings: PydanticBaseSettingsSource,
828        file_secret_settings: PydanticBaseSettingsSource,
829    ) -> Tuple[PydanticBaseSettingsSource, ...]:
830        cli: CliSettingsSource[BaseSettings] = CliSettingsSource(
831            settings_cls,
832            cli_parse_args=True,
833            formatter_class=RawTextHelpFormatter,
834        )
835        sys_args = pformat(sys.argv)
836        logger.info("starting CLI with arguments:\n{}", sys_args)
837        return (
838            cli,
839            init_settings,
840            YamlConfigSettingsSource(settings_cls),
841            JsonConfigSettingsSource(settings_cls),
842        )
843
844    @model_validator(mode="before")
845    @classmethod
846    def _log(cls, data: Any):
847        logger.info(
848            "loaded CLI input:\n{}",
849            pformat({k: v for k, v in data.items() if v is not None}),
850        )
851        return data
852
853    def run(self):
854        logger.info(
855            "executing CLI command:\n{}",
856            pformat({k: v for k, v in self.model_dump().items() if v is not None}),
857        )
858        cmd = (
859            self.add_weights
860            or self.package
861            or self.predict
862            or self.test
863            or self.update_format
864            or self.update_hashes
865            or self.validate_format
866        )
867        assert cmd is not None
868        cmd.run()

bioimageio - CLI for bioimage.io resources 🦒

library versions: bioimageio.core 0.9.4 bioimageio.spec 0.5.5.6

spec format versions: model RDF 0.5.5 dataset RDF 0.3.0 notebook RDF 0.3.0

model_config = {'extra': 'forbid', 'arbitrary_types_allowed': True, 'validate_default': True, 'case_sensitive': False, 'env_prefix': '', 'nested_model_default_partial_update': False, 'env_file': None, 'env_file_encoding': None, 'env_ignore_empty': False, 'env_nested_delimiter': None, 'env_nested_max_split': None, 'env_parse_none_str': None, 'env_parse_enums': None, 'cli_prog_name': 'bioimageio', 'cli_parse_args': True, 'cli_parse_none_str': None, 'cli_hide_none_type': False, 'cli_avoid_json': False, 'cli_enforce_required': False, 'cli_use_class_docs_for_groups': True, 'cli_exit_on_error': True, 'cli_prefix': '', 'cli_flag_prefix_char': '-', 'cli_implicit_flags': True, 'cli_ignore_unknown_args': False, 'cli_kebab_case': False, 'cli_shortcuts': None, 'json_file': 'bioimageio-cli.json', 'json_file_encoding': None, 'yaml_file': 'bioimageio-cli.yaml', 'yaml_file_encoding': None, 'yaml_config_section': None, 'toml_file': None, 'secrets_dir': None, 'protected_namespaces': ('model_validate', 'model_dump', 'settings_customise_sources'), 'enable_decoding': True, 'use_attribute_docstrings': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

validate_format: Annotated[Optional[ValidateFormatCmd], <class 'pydantic_settings.sources.types._CliSubCommand'>]

Check a resource's metadata format

test: Annotated[Optional[TestCmd], <class 'pydantic_settings.sources.types._CliSubCommand'>]

Test a bioimageio resource (beyond meta data formatting)

package: Annotated[Optional[PackageCmd], <class 'pydantic_settings.sources.types._CliSubCommand'>]

Package a resource

predict: Annotated[Optional[PredictCmd], <class 'pydantic_settings.sources.types._CliSubCommand'>]

Predict with a model resource

update_format: Annotated[Optional[UpdateFormatCmd], <class 'pydantic_settings.sources.types._CliSubCommand'>]

Update the metadata format

update_hashes: Annotated[Optional[UpdateHashesCmd], <class 'pydantic_settings.sources.types._CliSubCommand'>]

Create a bioimageio.yaml description with updated file hashes.

add_weights: Annotated[Optional[AddWeightsCmd], <class 'pydantic_settings.sources.types._CliSubCommand'>]

Add additional weights to the model descriptions converted from available formats to improve deployability.

@classmethod
def settings_customise_sources( cls, settings_cls: Type[pydantic_settings.main.BaseSettings], init_settings: pydantic_settings.sources.base.PydanticBaseSettingsSource, env_settings: pydantic_settings.sources.base.PydanticBaseSettingsSource, dotenv_settings: pydantic_settings.sources.base.PydanticBaseSettingsSource, file_secret_settings: pydantic_settings.sources.base.PydanticBaseSettingsSource) -> Tuple[pydantic_settings.sources.base.PydanticBaseSettingsSource, ...]:
821    @classmethod
822    def settings_customise_sources(
823        cls,
824        settings_cls: Type[BaseSettings],
825        init_settings: PydanticBaseSettingsSource,
826        env_settings: PydanticBaseSettingsSource,
827        dotenv_settings: PydanticBaseSettingsSource,
828        file_secret_settings: PydanticBaseSettingsSource,
829    ) -> Tuple[PydanticBaseSettingsSource, ...]:
830        cli: CliSettingsSource[BaseSettings] = CliSettingsSource(
831            settings_cls,
832            cli_parse_args=True,
833            formatter_class=RawTextHelpFormatter,
834        )
835        sys_args = pformat(sys.argv)
836        logger.info("starting CLI with arguments:\n{}", sys_args)
837        return (
838            cli,
839            init_settings,
840            YamlConfigSettingsSource(settings_cls),
841            JsonConfigSettingsSource(settings_cls),
842        )

Define the sources and their order for loading the settings values.

Arguments:
  • settings_cls: The Settings class.
  • init_settings: The InitSettingsSource instance.
  • env_settings: The EnvSettingsSource instance.
  • dotenv_settings: The DotEnvSettingsSource instance.
  • file_secret_settings: The SecretsSettingsSource instance.
Returns:

A tuple containing the sources and their order for loading the settings values.

def run(self):
853    def run(self):
854        logger.info(
855            "executing CLI command:\n{}",
856            pformat({k: v for k, v in self.model_dump().items() if v is not None}),
857        )
858        cmd = (
859            self.add_weights
860            or self.package
861            or self.predict
862            or self.test
863            or self.update_format
864            or self.update_hashes
865            or self.validate_format
866        )
867        assert cmd is not None
868        cmd.run()