diff --git a/extensions/chaeo/products.py b/extensions/chaeo/products.py index a0f50de3ca097ef1b02a20dd241593bfc8480e83..fa47a57d82cd4e94c699cd86d0ae9659a30c654b 100644 --- a/extensions/chaeo/products.py +++ b/extensions/chaeo/products.py @@ -1,47 +1,61 @@ -from PIL import Image, ImageDraw, ImageFont +from pathlib import Path -def draw_boxes_on_2d_image(img, boxes, **kwargs): - pilimg = Image.fromarray(np.copy(img)) # drawing modifies array in-place - draw = ImageDraw.Draw(pilimg) - font_size = kwargs.get('font_size', 18) - linewidth = kwargs.get('linewidth', 4) +import numpy as np +from PIL import Image, ImageDraw - draw.font = ImageFont.truetype(font="arial.ttf", size=font_size) +from skimage.io import imsave +from tifffile import imwrite - for box in boxes: - y0 = box['info'].y0 - y1 = box['info'].y1 - x0 = box['info'].x0 - x1 = box['info'].x1 - xm = round((x0 + x1) / 2) +from model_server.accessors import GenericImageDataAccessor +from model_server.process import pad, rescale, resample_to_8bit - la = box['info'].label - zi = box['info'].zi +def _write_patch_to_file(where, fname, data): + ext = fname.split('.')[-1].upper() + where.mkdir(parents=True, exist_ok=True) - draw.rectangle([(x0, y0), (x1, y1)], outline='white', width=linewidth) + if ext == 'PNG': + assert data.dtype == 'uint8', f'Invalid data type {data.dtype}' + assert data.shape[2] == 1, f'Cannot export multichannel images as PNGs; RGB not supported' + assert data.shape[3] == 1, f'Cannot export z-stacks as PNGs' + imsave(where / fname, data[:, :, 0, 0], check_contrast=False) + return True - if kwargs.get('add_label') is True: - draw.text((xm, y0), f'Z{zi:04d}-L{la:04d}', fill='white', anchor='mb') + elif ext in ['TIF', 'TIFF']: + zcyx = np.moveaxis(data, [3, 2, 0, 1], [0, 1, 2, 3]) + imwrite(where / fname, zcyx, imagej=True) + return True - return pilimg + else: + raise Exception(f'Unsupported file extension: {ext}') -def generate_patches( - desc, stack, boxes, rescale_clip=0.0, - pad_to=256, - proj=lambda x: x.max(axis=0), + +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', **kwargs ): - patch_dir = root / 'output' / 'patches' / desc - patch_dir.mkdir(parents=True, exist_ok=True) + assert stack.chroma == 1, 'Expecting monochromatic image data' + assert stack.nz > 1, 'Expecting z-stack' + + exported = [] + for mi in zmask_meta: + obj = mi['info'] + sl = mi['slice'] + rbb = mi['relative_bounding_box'] - for box in boxes: - obj = box['info'] - sl = box['slice'] - rbb = box['relative_bounding_box'] + if make_3d: + patch = stack.data[sl] + else: + patch = np.max(stack.data[sl], axis=3, keepdims=True) - patch = proj(stack[sl]) - patch_fname = f'{prefix}-la{obj.label:04d}-zi{obj.zi:04d}' + assert len(patch.shape) == 4 + assert patch.shape[2] == stack.chroma + # assert patch.shape[3] == 1 # should not get to zstacks by this point if rescale_clip is not None: patch = rescale(patch, rescale_clip) @@ -52,46 +66,17 @@ def generate_patches( x1 = rbb['x1'] y1 = rbb['y1'] - pilimg = Image.fromarray(patch) # drawing modifies array in-place - draw = ImageDraw.Draw(pilimg) - draw.rectangle([(x0, y0), (x1, y1)], outline='white', width=kwargs.get('linewidth', 1)) - patch = np.array(pilimg) + for zi in range(0, patch.shape[3]): + pilimg = Image.fromarray(patch[:, :, 0, zi]) # drawing modifies array in-place + draw = ImageDraw.Draw(pilimg) + draw.rectangle([(x0, y0), (x1, y1)], outline='white', width=kwargs.get('linewidth', 1)) + patch[:, :, 0, zi] = np.array(pilimg) if pad_to: patch = pad(patch, pad_to) - imsave( - patch_dir / (patch_fname + '.png'), - resample(patch), - check_contrast=False, - ) - print(f'Successfully wrote {len(boxes)} patches to:\n{patch_dir}') - - -def generate_3d_patches( # in extensions.chaeo.products - desc, stack, boxes, rescale_clip=0.0, - pad_to=256, - prefix='patch', - proj=lambda x: x, -): - patch_dir = root / 'output' / '3d_patches' / desc - patch_dir.mkdir(parents=True, exist_ok=True) - - for box in boxes: - obj = box['info'] - sl = box['slice'] - patch = proj(stack[sl]) - patch_fname = f'{prefix}-la{obj.label:04d}-zi{obj.zi:04d}' - - if rescale_clip is not None: - patch = rescale(patch, rescale_clip) - - if pad_to: - patch = pad_3d(patch, pad_to) - - imwrite( - patch_dir / (patch_fname + '.tif'), - patch, - imagej=True - ) - print(f'Successfully wrote {len(boxes)} patches to:\n{patch_dir}') \ No newline at end of file + ext = 'tif' if make_3d else 'png' + fname = f'{prefix}-la{obj.label:04d}-zi{obj.zi:04d}.{ext}' + success = _write_patch_to_file(where, fname, resample_to_8bit(patch)) + exported.append(fname) + return success, exported \ No newline at end of file diff --git a/extensions/chaeo/test_zstack.py b/extensions/chaeo/tests/test_zstack.py similarity index 70% rename from extensions/chaeo/test_zstack.py rename to extensions/chaeo/tests/test_zstack.py index ebe1172203187b345d9e4b16b35a5f932fd2d3ab..fcbbb3e1fa6dc4502e9326f39d8ff13bc6a2a1b8 100644 --- a/extensions/chaeo/test_zstack.py +++ b/extensions/chaeo/tests/test_zstack.py @@ -5,6 +5,7 @@ import numpy as np from conf.testing import output_path from extensions.chaeo.conf.testing import multichannel_zstack, pixel_classifier, pipeline_params +from extensions.chaeo.products import export_patches_from_zstack from extensions.chaeo.zmask import build_zmask_from_object_mask from model_server.accessors import generate_file_accessor, InMemoryDataAccessor, write_accessor_data_to_file from extensions.ilastik.models import IlastikObjectClassifierModel, IlastikPixelClassifierModel @@ -51,6 +52,8 @@ class TestZStackDerivedDataProducts(unittest.TestCase): ar = meta[1]['info'].area self.assertGreaterEqual(sh[0] * sh[1], ar) + return zmask, meta + def test_zmask_makes_correct_contours(self): return self.test_zmask_makes_correct_boxes(mask_type='contours') @@ -58,4 +61,32 @@ class TestZStackDerivedDataProducts(unittest.TestCase): return self.test_zmask_makes_correct_boxes(filters={'area': (1e3, 1e4)}) def test_zmask_makes_correct_expanded_boxes(self): - return self.test_zmask_makes_correct_boxes(expand_box_by=(64, 2)) \ No newline at end of file + return self.test_zmask_makes_correct_boxes(expand_box_by=(64, 2)) + + def test_make_2d_patches_from_zmask(self): + zmask, meta = self.test_zmask_makes_correct_boxes( + filters={'area': (1e3, 1e4)}, + expand_box_by=(64, 2) + ) + success, files = export_patches_from_zstack( + output_path / '2d_patches', + self.stack.get_one_channel_data(channel=1), + meta, + draw_bounding_box=True, + ) + self.assertTrue(success) + self.assertGreaterEqual(len(files), 1) + + def test_make_3d_patches_from_zmask(self): + zmask, meta = self.test_zmask_makes_correct_boxes( + filters={'area': (1e3, 1e4)}, + expand_box_by=(64, 2), + ) + success, files = export_patches_from_zstack( + output_path / '3d_patches', + self.stack.get_one_channel_data(channel=1), + meta, + make_3d=True) + self.assertTrue(success) + self.assertGreaterEqual(len(files), 1) + diff --git a/extensions/chaeo/zmask.py b/extensions/chaeo/zmask.py index 03188688db74cff9dc168745ac0dbcb2ca61ea42..19c1db5780027be8e734f9403d326158c436e816 100644 --- a/extensions/chaeo/zmask.py +++ b/extensions/chaeo/zmask.py @@ -5,7 +5,6 @@ from skimage.measure import find_contours, label, regionprops_table from model_server.accessors import GenericImageDataAccessor -# build a single boolean 3d mask (objects v. bboxes) and return bounding boxes def build_zmask_from_object_mask( obmask: GenericImageDataAccessor, zstack: GenericImageDataAccessor, @@ -97,7 +96,7 @@ def build_zmask_from_object_mask( 'x1': ob.x1 - x0, } - sl = np.s_[y0: y1, x0: x1, 0, z0: z1 + 1] + sl = np.s_[y0: y1, x0: x1, :, z0: z1 + 1] # compute contours obmask = (lamap == ob.label)