NHPatterson / wsireg

multimodal whole slide image registration in a graph structure

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Unifying `add_modality` API for code reuse

manzt opened this issue · comments

Motivation

The API for wsireg feels familiar because adding a modality reminds me of adding images to a napari.Viewer instance.

viewer.add_image(arr, channel_axis=0, names=[...], colors=[...])

reg_graph.add_modality("name", arr, channel_names=[...], channel_colors=[...])

To that end, there might be an opportunity to reuse I/O reader plugins from the napari ecosystem (e.g. napari-lazy-openslide, aicsimageio) in wsireg. This could reduce the amount of maintenance required for readers for wsireg.

EDIT: I removed the example to narrow the scope of this issue.

Follow up from conversation this morning

TL;DR - It would be nice to let someone explicitly use a reader plugin (if installed in their environment) to read a specific format if not directly supported in wsireg. This decreases the scope of what wsireg needs to handle out-of-the-box.

Plugin interface

The main point is that a napari plugin that implements the reader hook has the following signature (roughly):

reader_function(path: str) -> Optional[Callable[[str], List[LayerData]]]

It either returns None or a function that returns a LayerData tuple. LayerData tuples contain:

  • data - ndarray
  • meta - dict of layer metadata (channel_axis, names, visibilities, colors)
  • type - "image" in our case

The problem

Plugins aren't discoverable through a convenient API in wsireg (or outside of napari), so adopting an existing plugin is verbose and "meta" must be converted to fields wsireg understands.

# functions can have different names and be nested in a package
from napari_foo_reader.nested import some_function_name as read_foo
from napari_bar_reader import reader_function as read_bar

# ... 

data, meta, type_ = read_foo("./data.foo")[0]
reg_graph.add_modality(
  data[0], # if pyramid
  channel_names=meta["names"],
  channel_colors=meta["colors"],
  # ...
)

data, meta, type_ = read_bar("./data.bar")[0]
# ...

Reusing installed I/O plugins in wsireg

Ideally, there is a convenience method in wsireg that when called inspects the environment and returns a reader function if available. E.g. something like:

from wsireg import WsiReg2D, plugin_reader

reg_graph.add_modality(plugin_reader("./data.foo"))
reg_graph.add_modality(plugin_reader("./data.foo", plugin_name="napari-foo-reader"))
reg_graph.add_modality("./data.foo")

The napari_plugin_engine is a small pure python package for managing plugins in napari, that could be used in this context:

from napari_plugin_engine import PluginManager

# create a plugin manager for wsireg that looks for the napari plugin entrypoint
pm = PluginManager(project_name='wsireg', discover_entry_point='napari.plugin')

def plugin_reader(path: str, plugin_name: str = None):
    get_readers = pm.hook.napari_get_reader
    if plugin_name:
        try:
            reader = get_readers(path=path, _plugin=plugin_name)
        except KeyError:
            raise ValueError(
                f"No plugin named {plugin_name}. Valid names are {set(pm.plugins)}"
            )
        if not callable(reader):
          raise ValueError(f"Plugin named {plugin_name} cannot read {path}")
    else:
        # returns a list of functions that claim to be able to read path
        readers = get_readers(path=path)

        # here you have to decide whether to just go with the first one,
        # or "try/except" and return the first one that doesn't raise an
        # exception.  let's just use the first one.
        if readers:
            reader = readers[0]
        else:
            raise ValueError(f"No plugin available to read path: {path}")

    # actually call the reader function (plugin may raise an error?)
    return reader(path)

Thanks to @tlambert03 for the code snippet above. A convenience method like this might end up napari_plugin_engine itself, but the library tries to be as napari-agnostic as possible so we will see. However, it should be simple enough to think about here and implement if we want it (without requiring napari as a dependency).