diff --git a/src/aliby/io/image.py b/src/aliby/io/image.py index 5af04ca679d92fe9d40ca33f029b9553f42a9dc2..42005645b6935e07ff264f8e4b699258d2c75d37 100644 --- a/src/aliby/io/image.py +++ b/src/aliby/io/image.py @@ -29,15 +29,11 @@ from tifffile import TiffFile from agora.io.metadata import dir_to_meta, dispatch_metadata_parser -def get_examples_dir(): - """Get examples directory that stores dummy image for tiler.""" - return files("aliby").parent.parent / "examples" / "tiler" - - def instantiate_image( source: t.Union[str, int, t.Dict[str, str], Path], **kwargs ): - """Wrapper to instantiate the appropriate image + """ + Instantiate the image. Parameters ---------- @@ -46,10 +42,9 @@ def instantiate_image( Examples -------- - image_path = "path/to/image"] + image_path = "path/to/image" with instantiate_image(image_path) as img: print(imz.data, img.metadata) - """ return dispatch_image(source)(source, **kwargs) @@ -63,14 +58,12 @@ def dispatch_image(source: t.Union[str, int, t.Dict[str, str], Path]): elif isinstance(source, dict) or ( isinstance(source, (str, Path)) and Path(source).is_dir() ): + # zarr files are considered directories if Path(source).suffix == ".zarr": instantiator = ImageZarr else: instantiator = ImageDir - elif isinstance(source, Path) and source.is_file(): - # my addition - instantiator = ImageLocalOME - elif isinstance(source, str) and Path(source).is_file(): + elif isinstance(source, (str, Path)) and Path(source).is_file(): instantiator = ImageLocalOME else: raise Exception(f"Invalid data source at {source}") @@ -78,9 +71,7 @@ def dispatch_image(source: t.Union[str, int, t.Dict[str, str], Path]): class BaseLocalImage(ABC): - """ - Base Image class to set path and provide context management method. - """ + """Set path and provide method for context management.""" # default image order _default_dimorder = "tczyx" @@ -112,30 +103,35 @@ class BaseLocalImage(ABC): ) return self._rechunked_img + @property + def data(self): + """Get data.""" + return self.get_data_lazy() + + @property + def metadata(self): + """Get metadata.""" + return self._meta + + def set_meta(self): + """Load metadata using parser dispatch.""" + self._meta = dispatch_metadata_parser(self.path) + @abstractmethod def get_data_lazy(self) -> da.Array: + """Define in child class.""" pass @abstractproperty def name(self): + """Define in child class.""" pass @abstractproperty def dimorder(self): + """Define in child class.""" pass - @property - def data(self): - return self.get_data_lazy() - - @property - def metadata(self): - return self._meta - - def set_meta(self): - """Load metadata using parser dispatch""" - self._meta = dispatch_metadata_parser(self.path) - class ImageLocalOME(BaseLocalImage): """ @@ -146,11 +142,13 @@ class ImageLocalOME(BaseLocalImage): """ def __init__(self, path: str, dimorder=None, **kwargs): + """Initialise using file name.""" super().__init__(path) self._id = str(path) self.set_meta(str(path)) def set_meta(self, path): + """Get metadata from the associated tiff file.""" meta = dict() try: with TiffFile(path) as f: @@ -194,7 +192,7 @@ class ImageLocalOME(BaseLocalImage): @property def dimorder(self): - """Order of dimensions in image""" + """Return order of dimensions in the image.""" if not hasattr(self, "_dimorder"): self._dimorder = self._meta["Image"]["Pixels"]["@DimensionOrder"] return self._dimorder @@ -205,16 +203,16 @@ class ImageLocalOME(BaseLocalImage): return self._dimorder def get_data_lazy(self) -> da.Array: - """Return 5D dask array. For lazy-loading multidimensional tiff files""" - + """Return 5D dask array via lazy-loading of tiff files.""" if not hasattr(self, "formatted_img"): - if not hasattr(self, "ids"): # Standard dimension order + if not hasattr(self, "ids"): + # standard order of image dimensions img = (imread(str(self.path))[0],) - else: # Custom dimension order, we rearrange the axes for compatibility + else: + # bespoke order, so rearrange axes for compatibility img = imread(str(self.path))[0] for i, d in enumerate(self._dimorder): self._meta["size_" + d.lower()] = img.shape[i] - target_order = ( *self.ids, *[ @@ -233,12 +231,13 @@ class ImageLocalOME(BaseLocalImage): img = da.moveaxis( reshaped, range(len(reshaped.shape)), target_order ) - return self.rechunk_data(img) class ImageDir(BaseLocalImage): """ + Standard image class for tiff files. + Image class for the case in which all images are split in one or multiple folders with time-points and channels as independent files. It inherits from BaseLocalImage so we only override methods that are critical. @@ -247,28 +246,23 @@ class ImageDir(BaseLocalImage): - One folder per position. - Images are flat. - Channel, Time, z-stack and the others are determined by filenames. - - Provides Dimorder as it is set in the filenames, or expects order during instatiation + - Provides Dimorder as it is set in the filenames, or expects order """ def __init__(self, path: t.Union[str, Path], **kwargs): + """Initialise using file name.""" super().__init__(path) self.image_id = str(self.path.stem) - self._meta = dir_to_meta(self.path) def get_data_lazy(self) -> da.Array: - """Return 5D dask array. For lazy-loading local multidimensional tiff files""" - + """Return 5D dask array.""" img = imread(str(self.path / "*.tiff")) - # If extra channels, pick the first stack of the last dimensions - while len(img.shape) > 3: img = img[..., 0] - if self._meta: self._meta["size_x"], self._meta["size_y"] = img.shape[-2:] - # Reshape using metadata # img = da.reshape(img, (*self._meta, *img.shape[1:])) img = da.reshape(img, self._meta.values()) @@ -289,6 +283,7 @@ class ImageDir(BaseLocalImage): @property def name(self): + """Return name of image directory.""" return self.path.stem @property @@ -302,24 +297,27 @@ class ImageDir(BaseLocalImage): class ImageZarr(BaseLocalImage): """ Read zarr compressed files. - These are outputed by the script + + These files are generated by the script skeletons/scripts/howto_omero/convert_clone_zarr_to_tiff.py """ def __init__(self, path: t.Union[str, Path], **kwargs): + """Initialise using file name.""" super().__init__(path) self.set_meta() try: self._img = zarr.open(self.path) self.add_size_to_meta() except Exception as e: - print(f"Could not add size info to metadata: {e}") + print(f"ImageZarr: Could not add size info to metadata: {e}.") def get_data_lazy(self) -> da.Array: """Return 5D dask array for lazy-loading local multidimensional zarr files.""" return self._img def add_size_to_meta(self): + """Add shape of image array to metadata.""" self._meta.update( { f"size_{dim}": shape @@ -329,16 +327,13 @@ class ImageZarr(BaseLocalImage): @property def name(self): + """Return name of zarr directory.""" return self.path.stem @property def dimorder(self): - # FIXME hardcoded order based on zarr compression/cloning script + """Impose a hard-coded order of dimensions based on the zarr compression script.""" return "TCZYX" - # Assumes only dimensions start with "size" - # return [ - # k.split("_")[-1] for k in self._meta.keys() if k.startswith("size") - # ] class ImageDummy(BaseLocalImage): diff --git a/src/aliby/io/omero.py b/src/aliby/io/omero.py index 0ea37359360598a7335db55081ab9500e91c3efc..8cc4fd939dcf1aacd0ac2e8cb84a48ae87a4f292 100644 --- a/src/aliby/io/omero.py +++ b/src/aliby/io/omero.py @@ -253,7 +253,8 @@ class Dataset(BridgeOmero): cls, filepath: t.Union[str, Path], ): - """Instatiate Dataset from a hdf5 file. + """ + Instantiate data set from a h5 file. Parameters ---------- diff --git a/src/aliby/tile/tiler.py b/src/aliby/tile/tiler.py index b0fdbefcce328faefa1425fcaa2886bf38c5d278..b35ca5c10e6611d9eb928c2fc44f2b4b7391e639 100644 --- a/src/aliby/tile/tiler.py +++ b/src/aliby/tile/tiler.py @@ -320,7 +320,7 @@ class Tiler(StepABC): image.data, metadata, parameters, - tile_locs=tile_locs, + tile_locations=tile_locs, ) if hasattr(tile_locs, "drifts"): tiler.no_processed = len(tile_locs.drifts) diff --git a/src/aliby/utils/imageViewer.py b/src/aliby/utils/imageViewer.py index fd529108dd9091b7ec19041ccc8cf2d265af2fa9..52a15ee763f776aba369276e34748b1becff76c0 100644 --- a/src/aliby/utils/imageViewer.py +++ b/src/aliby/utils/imageViewer.py @@ -17,17 +17,22 @@ riv.plot_labelled_trap(tile_id, trange, [0], ncols=ncols) import re import typing as t +from abc import ABC import h5py -from abc import ABC import matplotlib.pyplot as plt import numpy as np import seaborn as sns -from PIL import Image from skimage.morphology import dilation from agora.io.cells import Cells from agora.io.metadata import dispatch_metadata_parser +from aliby.io.image import ImageDir, ImageZarr, dispatch_image + +try: + from aliby.io.omero import UnsafeImage as OImage +except ModuleNotFoundError: + print("Viewing available only for local files.") from aliby.tile.tiler import Tiler, TilerParameters from aliby.utils.plot import stretch_clip @@ -40,9 +45,7 @@ default_colours = { def custom_imshow(a, norm=None, cmap=None, *args, **kwargs): - """ - Wrapper on plt.imshow function. - """ + """Wrap plt.imshow.""" if cmap is None: cmap = "Greys_r" return plt.imshow( @@ -57,16 +60,13 @@ def custom_imshow(a, norm=None, cmap=None, *args, **kwargs): class BaseImageViewer(ABC): def __init__(self, fpath): - self._fpath = fpath - attrs = dispatch_metadata_parser(fpath.parent) + self.attrs = dispatch_metadata_parser(fpath.parent) self._logfiles_meta = {} - - self.image_id = attrs.get("image_id") + self.image_id = self.attrs.get("image_id") if self.image_id is None: with h5py.File(fpath, "r") as f: self.image_id = f.attrs.get("image_id") - assert self.image_id is not None, "No valid image_id found in metadata" @property @@ -75,42 +75,39 @@ class BaseImageViewer(ABC): @property def ntraps(self): + """Find the number of traps available.""" return self.cells.ntraps @property def max_labels(self): - # Print max cell label in whole experiment + """Find maximum cell label in whole experiment.""" return [max(x) for x in self.cells.labels] def labels_at_time(self, tp: int): - # Print cell label at a given time-point + """Find cell label at a given time point.""" return self.cells.labels_at_time(tp) class LocalImageViewer(BaseImageViewer): """ - Tool to generate figures from local files, either zarr or files organised - in directories. + Generate figures from local files. + + File are either zarr or organised in directories. TODO move common functionality from RemoteImageViewer to BaseImageViewer """ - def __init__(self, results_path: str, data_path: str): - super().__init__(results_path) - - from aliby.io.image import ImageDir, ImageZarr - + def __init__(self, h5file_path: str, data_path: str): + super().__init__(h5file_path) self._image_class = ( - ImageZarr if data_path.endswith(".zar") else ImageDir + ImageZarr if str(data_path).endswith(".zarr") else ImageDir ) - - with dispatch_image(data_path)(data_path) as image: - self.tiler = Tiler( - image.data, - self._meta if hasattr(self, "_meta") else self._logfiles_meta, - TilerParameters.default(), - ) - - self.cells = Cells.from_source(results_path) + image = ImageZarr(data_path) + self.tiler = Tiler( + image.data, + self._meta if hasattr(self, "_meta") else self._logfiles_meta, + TilerParameters.default(), + ) + self.cells = Cells.from_source(h5file_path) class RemoteImageViewer(BaseImageViewer): @@ -126,16 +123,12 @@ class RemoteImageViewer(BaseImageViewer): server_info: t.Dict[str, str], ): super().__init__(results_path) - - from aliby.io.omero import UnsafeImage as OImage - self._server_info = server_info or { - k: attrs["parameters"]["general"][k] for k in self._credentials + k: self.attrs["parameters"]["general"][k] + for k in self._credentials } - self._image_instance = OImage(self.image_id, **self._server_info) self.tiler = Tiler.from_h5(self._image_instance, results_path) - self.cells = Cells.from_source(results_path) def random_valid_trap_tp( @@ -194,7 +187,6 @@ class RemoteImageViewer(BaseImageViewer): z: int = None, server_info=None, ): - if tps and not isinstance(tps, t.Collection): tps = range(tps) diff --git a/src/aliby/utils/plot.py b/src/aliby/utils/plot.py index 3f0a66c0849839fac357851ade037e78e8d17634..8a34c3b7ade6c8abe68e1491d2b0885d815def4a 100644 --- a/src/aliby/utils/plot.py +++ b/src/aliby/utils/plot.py @@ -52,7 +52,7 @@ def plot_in_square(data: t.Iterable): def stretch_clip(image, clip=True): """ - Performs contrast stretching on an input image. + Perform contrast stretching on an input image. This function takes an array-like input image and enhances its contrast by adjusting the dynamic range of pixel values. It first scales the pixel values between 0 and 255,