From 4d16baa59f4e86e8d43e964764de9ef957d1147e Mon Sep 17 00:00:00 2001 From: Christopher Rhodes <christopher.rhodes@embl.de> Date: Thu, 26 Oct 2023 15:40:02 +0200 Subject: [PATCH] Split out patch/patch mask creation for file export; implemented and test accessor abstraction for patch arrays --- extensions/chaeo/accessors.py | 51 ++++++++++++++++ extensions/chaeo/products.py | 76 ++++++++++++++++++++---- extensions/chaeo/tests/test_accessors.py | 27 +++++++++ 3 files changed, 141 insertions(+), 13 deletions(-) create mode 100644 extensions/chaeo/accessors.py create mode 100644 extensions/chaeo/tests/test_accessors.py diff --git a/extensions/chaeo/accessors.py b/extensions/chaeo/accessors.py new file mode 100644 index 00000000..32fb7020 --- /dev/null +++ b/extensions/chaeo/accessors.py @@ -0,0 +1,51 @@ +import numpy as np +from model_server.accessors import InMemoryDataAccessor + +class MonoPatchStack(InMemoryDataAccessor): + + def __init__(self, data): + """ + A sequence of n monochrome images of the same size + :param data: either np.ndarray of dimensions YXn, or a list of np.ndarrays of size YX + """ + + if isinstance(data, np.ndarray): # interpret as YXZ + assert len(data.shape) == 3 + self._data = np.expand_dims(data, 2) + elif isinstance(data, list): # list of YX patches + self._data = np.expand_dims( + np.moveaxis( + np.array(data), + [1, 2, 0], + [0, 1, 2]), + 2 + ) + else: + raise InvalidDataForPatchStackError(f'Cannot create accessor from {type(data)}') + + def make_tczyx(self): + assert self.chroma == 1 + tyx = np.moveaxis( + self.data[:, :, 0, :], # YX(C)Z + [2, 0, 1], + [0, 1, 2] + ) + return np.expand_dims(tyx, (1, 2)) + + @property + def count(self): + return self.nz + + def iat(self, i): + return self.data[:, :, 0, i] + + def get_list(self): + n = self.nz + return [self.data[:, :, 0, zi] for zi in range(0, n)] + + +class Error(Exception): + pass + +class InvalidDataForPatchStackError(Error): + pass \ No newline at end of file diff --git a/extensions/chaeo/products.py b/extensions/chaeo/products.py index e665d878..a9310e37 100644 --- a/extensions/chaeo/products.py +++ b/extensions/chaeo/products.py @@ -9,6 +9,7 @@ from skimage.io import imsave from skimage.measure import find_contours, shannon_entropy from tifffile import imwrite +from extensions.chaeo.accessors import MonoPatchStack from extensions.chaeo.annotators import draw_box_on_patch, draw_contours_on_patch from model_server.accessors import GenericImageDataAccessor, InMemoryDataAccessor from model_server.process import pad, rescale, resample_to_8bit @@ -55,16 +56,13 @@ def _write_patch_to_file(where, fname, data): else: raise Exception(f'Unsupported file extension: {ext}') -def export_patch_masks_from_zstack( - where: Path, +def get_patch_masks_from_zmask_meta( stack: GenericImageDataAccessor, zmask_meta: list, pad_to: int = 256, - prefix='mask', -): - exported = [] +) -> MonoPatchStack: + patches = [] for mi in zmask_meta: - obj = mi['info'] sl = mi['slice'] rbb = mi['relative_bounding_box'] @@ -82,27 +80,48 @@ def export_patch_masks_from_zstack( if pad_to: patch = pad(patch, pad_to) + patches.append(patch) + return MonoPatchStack(patches) + +def export_patch_masks_from_zstack( + where: Path, + stack: GenericImageDataAccessor, + zmask_meta: list, + pad_to: int = 256, + prefix='mask', + **kwargs +): + patches_acc = get_patch_masks_from_zmask_meta( + stack, + zmask_meta, + pad_to=pad_to, + **kwargs + ) + assert len(zmask_meta) == patches_acc.count + + exported = [] + for i in range(0, len(zmask_meta)): + mi = zmask_meta[i] + obj = mi['info'] + patch = patches_acc.iat(i) ext = 'png' fname = f'{prefix}-la{obj.label:04d}-zi{obj.zi:04d}.{ext}' _write_patch_to_file(where, fname, patch) exported.append(fname) return exported -def export_patches_from_zstack( - where: Path, +def get_patches_from_zmask_meta( stack: GenericImageDataAccessor, zmask_meta: list, rescale_clip: float = 0.0, pad_to: int = 256, make_3d: bool = False, - prefix='patch', focus_metric: str = None, **kwargs -): - - exported = [] +) -> MonoPatchStack: + patches = [] for mi in zmask_meta: - obj = mi['info'] + sl = mi['slice'] rbb = mi['relative_bounding_box'] idx = mi['df_index'] @@ -177,6 +196,37 @@ def export_patches_from_zstack( if pad_to: patch = pad(patch, pad_to) + patches.append(patch) + return MonoPatchStack(patches) + +def export_patches_from_zstack( + where: Path, + stack: GenericImageDataAccessor, + zmask_meta: list, + rescale_clip: float = 0.0, + pad_to: int = 256, + make_3d: bool = False, + prefix='patch', + focus_metric: str = None, + **kwargs +): + patches_acc = get_patches_from_zmask_meta( + stack, + zmask_meta, + rescale_clip=rescale_clip, + pad_to=pad_to, + make_3d=make_3d, + focus_metric=focus_metric, + **kwargs + ) + assert len(zmask_meta) == patches_acc.count + + exported = [] + for i in range(0, len(zmask_meta)): + mi = zmask_meta[i] + patch = patches_acc.iat(i) + obj = mi['info'] + idx = mi['df_index'] ext = 'tif' if make_3d else 'png' fname = f'{prefix}-la{obj.label:04d}-zi{obj.zi:04d}.{ext}' diff --git a/extensions/chaeo/tests/test_accessors.py b/extensions/chaeo/tests/test_accessors.py new file mode 100644 index 00000000..465f15e0 --- /dev/null +++ b/extensions/chaeo/tests/test_accessors.py @@ -0,0 +1,27 @@ +import unittest + +import numpy as np + +from extensions.chaeo.accessors import MonoPatchStack + + + +class TestCziImageFileAccess(unittest.TestCase): + def setUp(self) -> None: + pass + + def test_make_patch_stack_from_list(self): + w = 256 + h = 512 + n = 4 + acc = MonoPatchStack(np.random.rand(h, w, n)) + assert acc.count == n + assert acc.hw == (h, w) + + def test_make_patch_stack_from_3d_array(self): + w = 256 + h = 512 + n = 4 + acc = MonoPatchStack([np.random.rand(h, w) for _ in range(0, 4)]) + assert acc.count == n + assert acc.hw == (h, w) \ No newline at end of file -- GitLab