diff --git a/model_server/base/accessors.py b/model_server/base/accessors.py index fec80c819ad808a1f8ca2afa09b6b0fd65a8b4fe..4b91eb5cda12dc79c3b1bd300c5630d1370d2c91 100644 --- a/model_server/base/accessors.py +++ b/model_server/base/accessors.py @@ -3,12 +3,12 @@ import os from pathlib import Path import numpy as np -from skimage.io import imread +from skimage.io import imread, imsave import czifile import tifffile -from extensions.chaeo.accessors import InvalidDataForPatchStackError +from base.process import make_rgb from model_server.base.process import is_mask class GenericImageDataAccessor(ABC): @@ -184,33 +184,49 @@ class CziImageFileAccessor(GenericImageFileAccessor): return sc -def write_accessor_data_to_file(fpath: Path, accessor: GenericImageDataAccessor, mkdir=True) -> bool: +def write_accessor_data_to_file(fpath: Path, acc: GenericImageDataAccessor, mkdir=True) -> bool: """ Export an image accessor to file. :param fpath: complete path including filename and extension - :param accessor: image accessor to be written + :param acc: image accessor to be written :param mkdir: create any needed subdirectories in fpath if True :return: True """ + if 'P' in acc.shape_dict.keys(): + raise FileWriteError(f'Can only write single-position accessor to file') + ext = fpath.suffix.upper() if mkdir: fpath.parent.mkdir(parents=True, exist_ok=True) - try: + + if ext == '.PNG': + if acc.dtype != 'uint8': + raise FileWriteError(f'Invalid data type {acc.dtype}') + if acc.chroma == 1: + data = acc.data[:, :, 0, 0] + elif acc.chroma == 2: # add a blank blue channel + data = make_rgb(acc.data) + else: # preserve RGB order + data = acc.data[:, :, :, 0] + imsave(fpath, data, check_contrast=False) + return True + + elif ext in ['.TIF', '.TIFF']: zcyx= np.moveaxis( - accessor.data, # yxcz + acc.data, # yxcz [3, 2, 0, 1], [0, 1, 2, 3] ) - if accessor.is_mask(): - if accessor.dtype == 'bool': + if acc.is_mask(): + if acc.dtype == 'bool': data = (zcyx * 255).astype('uint8') else: data = zcyx.astype('uint8') tifffile.imwrite(fpath, data, imagej=True) else: tifffile.imwrite(fpath, zcyx, imagej=True) - except: - raise FileWriteError(f'Unable to write data to file') + else: + raise FileWriteError(f'Unable to write data to file of extension {ext}') return True @@ -256,7 +272,7 @@ class PatchStack(InMemoryDataAccessor): self._data = nda def iat(self, i): - return self.data[i, :, :, :, :] + return InMemoryDataAccessor(self.data[i, :, :, :, :]) def iat_yxcz(self, i): return self.iat(i) @@ -307,6 +323,7 @@ def make_patch_stack_from_file(fpath): # interpret z-dimension as patch positio return PatchStack(pyxcz) + class Error(Exception): pass @@ -328,5 +345,8 @@ class InvalidAxisKey(Error): class InvalidDataShape(Error): pass +class InvalidDataForPatchStackError(Error): + pass + diff --git a/model_server/base/roiset.py b/model_server/base/roiset.py index 819f8552660f1051f2bf3e713faf74d21e58d508..a66f97414c61b163403abfb75d96c119f616400b 100644 --- a/model_server/base/roiset.py +++ b/model_server/base/roiset.py @@ -17,7 +17,6 @@ from model_server.base.accessors import GenericImageDataAccessor, InMemoryDataAc from model_server.base.models import InstanceSegmentationModel from model_server.base.process import pad, rescale, resample_to_8bit, make_rgb from base.annotators import draw_box_on_patch, draw_contours_on_patch, draw_boxes_on_3d_image -from model_server.extensions.chaeo.accessors import write_patch_to_file from base.accessors import PatchStack from base.process import mask_largest_object @@ -294,7 +293,7 @@ class RoiSet(object): for i, roi in enumerate(self): oc = np.unique( mask_largest_object( - obmap_patches.iat(i) + obmap_patches.iat(i).data ) )[1] self._df.loc[roi.Index, 'classify_by_' + name] = oc @@ -309,7 +308,7 @@ class RoiSet(object): patch = patches_acc.iat_yxcz(i) ext = 'png' fname = f'{prefix}-la{roi.label:04d}-zi{roi.zi:04d}.{ext}' - write_patch_to_file(where, fname, patch) + write_accessor_data_to_file(where / fname, patch) exported.append(fname) return exported @@ -324,9 +323,10 @@ class RoiSet(object): fname = f'{prefix}-la{roi.label:04d}-zi{roi.zi:04d}.{ext}' if patch.dtype is np.dtype('uint16'): - write_patch_to_file(where, fname, resample_to_8bit(patch.data)) + resampled = InMemoryDataAccessor(resample_to_8bit(patch.data)) + write_accessor_data_to_file(where / fname, resampled) else: - write_patch_to_file(where, fname, patch) + write_accessor_data_to_file(where / fname, patch) exported.append({ 'df_index': roi.Index, diff --git a/model_server/extensions/chaeo/accessors.py b/model_server/extensions/chaeo/accessors.py deleted file mode 100644 index 7c9f74e57caf5413e515e89eff49622967cd4c2c..0000000000000000000000000000000000000000 --- a/model_server/extensions/chaeo/accessors.py +++ /dev/null @@ -1,41 +0,0 @@ -import numpy as np -from skimage.io import imsave -from tifffile import imwrite - -from base.process import make_rgb - - -# TODO: move this as method in base.accessors -def write_patch_to_file(where, fname, yxcz): - ext = fname.split('.')[-1].upper() - where.mkdir(parents=True, exist_ok=True) - - if ext == 'PNG': - assert yxcz.dtype == 'uint8', f'Invalid data type {yxcz.dtype}' - assert yxcz.shape[2] <= 3, f'Cannot export images with more than 3 channels as PNGs' - assert yxcz.shape[3] == 1, f'Cannot export z-stacks as PNGs' - if yxcz.shape[2] == 1: - outdata = yxcz[:, :, 0, 0] - elif yxcz.shape[2] == 2: # add a blank blue channel - outdata = make_rgb(yxcz) - else: # preserve RGB order - outdata = yxcz[:, :, :, 0] - imsave(where / fname, outdata, check_contrast=False) - return True - - elif ext in ['TIF', 'TIFF']: - zcyx = np.moveaxis(yxcz, [3, 2, 0, 1], [0, 1, 2, 3]) - imwrite(where / fname, zcyx, imagej=True) - return True - - else: - raise Exception(f'Unsupported file extension: {ext}') - - -class Error(Exception): - pass - -class InvalidDataForPatchStackError(Error): - pass - - diff --git a/model_server/extensions/chaeo/batch_jobs/coloring_book.py b/model_server/extensions/chaeo/batch_jobs/coloring_book.py index a3a9c5268a12cac204897d9ecd5dba4d8a648ad6..e65dd1d27345852d2687715de5d91cfd90f770b9 100644 --- a/model_server/extensions/chaeo/batch_jobs/coloring_book.py +++ b/model_server/extensions/chaeo/batch_jobs/coloring_book.py @@ -7,7 +7,7 @@ from skimage.measure import label, regionprops_table import tifffile -from model_server.extensions.chaeo.accessors import MonoPatchStack +from model_server.base.accessors import PatchStack from model_server.extensions.ilastik.models import IlastikPixelClassifierModel from model_server.base.accessors import write_accessor_data_to_file, InMemoryDataAccessor @@ -19,7 +19,7 @@ if __name__ == '__main__': min_area = 400 tf = tifffile.imread(root / '20231008-162336-z04-TL.tif') - instack = MonoPatchStack(np.moveaxis(tf, 0, -1)) + instack = PatchStack(np.moveaxis(tf, 0, -1)) px_mod = IlastikPixelClassifierModel(params={'project_file': px_ilp}) pxmaps = [] diff --git a/model_server/extensions/chaeo/examples/transfer_labels_to_ilastik_object_classifier.py b/model_server/extensions/chaeo/examples/transfer_labels_to_ilastik_object_classifier.py index 5d9625b55b03cc25a8c9476d6ac27af166de38dd..04e0c20e31d2da78ab11bec16908333e56a7c74f 100644 --- a/model_server/extensions/chaeo/examples/transfer_labels_to_ilastik_object_classifier.py +++ b/model_server/extensions/chaeo/examples/transfer_labels_to_ilastik_object_classifier.py @@ -3,7 +3,7 @@ import numpy as np import pandas as pd import skimage -from model_server.extensions.chaeo.accessors import MonoPatchStackFromFile +from model_server.base.accessors import make_patch_stack_from_file from model_server.extensions.chaeo.models import generate_ilastik_object_classifier from extensions.ilastik.models import PatchStackObjectClassifier from model_server.base.accessors import GenericImageDataAccessor, write_accessor_data_to_file @@ -69,9 +69,9 @@ if __name__ == '__main__': classifier_file = generate_ilastik_object_classifier( template_ilp, root / 'new_auto_obj.ilp', - MonoPatchStackFromFile(root / 'zstack_train_raw.tif'), - MonoPatchStackFromFile(root / 'zstack_train_mask.tif'), - MonoPatchStackFromFile(root / 'zstack_train_label.tif'), + make_patch_stack_from_file(root / 'zstack_train_raw.tif'), + make_patch_stack_from_file(root / 'zstack_train_mask.tif'), + make_patch_stack_from_file(root / 'zstack_train_label.tif'), label_names, allow_multiple_objects=False ) @@ -81,17 +81,17 @@ if __name__ == '__main__': infer_and_compare( classifier, 'train', - MonoPatchStackFromFile(root / 'zstack_train_raw.tif'), - MonoPatchStackFromFile(root / 'zstack_train_mask.tif'), - MonoPatchStackFromFile(root / 'zstack_train_label.tif') + make_patch_stack_from_file(root / 'zstack_train_raw.tif'), + make_patch_stack_from_file(root / 'zstack_train_mask.tif'), + make_patch_stack_from_file(root / 'zstack_train_label.tif') ) # run test set infer_and_compare( classifier, 'test', - MonoPatchStackFromFile(root / 'zstack_test_raw.tif'), - MonoPatchStackFromFile(root / 'zstack_test_mask.tif'), - MonoPatchStackFromFile(root / 'zstack_test_label.tif'), + make_patch_stack_from_file(root / 'zstack_test_raw.tif'), + make_patch_stack_from_file(root / 'zstack_test_mask.tif'), + make_patch_stack_from_file(root / 'zstack_test_label.tif'), ) diff --git a/model_server/extensions/chaeo/models.py b/model_server/extensions/chaeo/models.py index 58235d81ed8d56845d220cd6203b7ed22ec6624c..865537e67ba79738a82e5b7e63472210026657f2 100644 --- a/model_server/extensions/chaeo/models.py +++ b/model_server/extensions/chaeo/models.py @@ -5,7 +5,7 @@ import h5py import numpy as np import skimage -from model_server.extensions.chaeo.accessors import PatchStack +from model_server.base.accessors import PatchStack def generate_ilastik_object_classifier( diff --git a/model_server/extensions/ilastik/tests/test_ilastik.py b/model_server/extensions/ilastik/tests/test_ilastik.py index 5f765ba709eb30364eda1e2c9fa60db57f5c0f69..89fe2b98d1f8ea25702e8a221c6178e8fe3a645b 100644 --- a/model_server/extensions/ilastik/tests/test_ilastik.py +++ b/model_server/extensions/ilastik/tests/test_ilastik.py @@ -295,6 +295,6 @@ class TestIlastikObjectClassification(unittest.TestCase): res_patches, _ = self.object_classifier.infer(raw_patches, patch_masks) self.assertEqual(res_patches.count, self.roiset.count) for pi in range(0, res_patches.count): # assert that there is only one nonzero label per patch - unique = np.unique(res_patches.iat(pi)) + unique = np.unique(res_patches.iat(pi).data) self.assertEqual(len(unique), 2) self.assertEqual(unique[0], 0)