wsireg
wsireg copied to clipboard
Unifying `add_modality` API for code reuse
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- ndarraymeta- 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).