From 03d43bf20e32ee73c369206b3ad87244eb680cc9 Mon Sep 17 00:00:00 2001
From: Christopher Rhodes <christopher.rhodes@embl.de>
Date: Fri, 29 Sep 2023 14:55:15 +0200
Subject: [PATCH] Batch export of patches and annotated 2d images is now
 working

---
 extensions/chaeo/annotators.py                | 17 ++--
 extensions/chaeo/products.py                  |  1 -
 extensions/chaeo/workflows.py                 | 91 +++++++++++++++++++
 .../ilastik/examples/batch_run_patches.py     | 34 +++++++
 4 files changed, 136 insertions(+), 7 deletions(-)
 create mode 100644 extensions/chaeo/workflows.py
 create mode 100644 extensions/ilastik/examples/batch_run_patches.py

diff --git a/extensions/chaeo/annotators.py b/extensions/chaeo/annotators.py
index b0cc1453..eb763feb 100644
--- a/extensions/chaeo/annotators.py
+++ b/extensions/chaeo/annotators.py
@@ -3,8 +3,8 @@ from PIL import Image, ImageDraw, ImageFont
 
 from model_server.process import rescale
 
-def draw_boxes_on_2d_image(img, boxes, **kwargs):
-    pilimg = Image.fromarray(np.copy(img))  # drawing modifies array in-place
+def draw_boxes_on_2d_image(yx_img, boxes, **kwargs):
+    pilimg = Image.fromarray(np.copy(yx_img))  # drawing modifies array in-place
     draw = ImageDraw.Draw(pilimg)
     font_size = kwargs.get('font_size', 18)
     linewidth = kwargs.get('linewidth', 4)
@@ -29,15 +29,20 @@ def draw_boxes_on_2d_image(img, boxes, **kwargs):
     return pilimg
 
 
-def draw_boxes_on_3d_image(img, boxes, draw_full_depth=False, **kwargs):
-    annotated = np.zeros(img.shape, dtype=img.dtype)
+def draw_boxes_on_3d_image(yxcz_img, boxes, draw_full_depth=False, **kwargs):
+    assert len(yxcz_img.shape) == 4
+    nz = yxcz_img.shape[3]
+    assert yxcz_img.shape[2] == 1
+    assert nz > 1
 
-    for zi in range(0, img.shape[0]):
+    annotated = np.zeros(yxcz_img.shape, dtype=yxcz_img.dtype)
+
+    for zi in range(0, nz):
         if draw_full_depth:
             zi_boxes = boxes
         else:
             zi_boxes = [bb for bb in boxes if bb['info'].zi == zi]
-        annotated[zi, :, :] = draw_boxes_on_2d_image(img[zi, :, :], zi_boxes, **kwargs)
+        annotated[:, :, 0, zi] = draw_boxes_on_2d_image(yxcz_img[:, :, 0, zi], zi_boxes, **kwargs)
 
     if clip := kwargs.get('rescale_clip'):
         assert clip >= 0.0 and clip <= 1.0
diff --git a/extensions/chaeo/products.py b/extensions/chaeo/products.py
index d2a372b0..dd9334a7 100644
--- a/extensions/chaeo/products.py
+++ b/extensions/chaeo/products.py
@@ -1,7 +1,6 @@
 from pathlib import Path
 
 import numpy as np
-from PIL import Image, ImageDraw
 
 from skimage.io import imsave
 from tifffile import imwrite
diff --git a/extensions/chaeo/workflows.py b/extensions/chaeo/workflows.py
new file mode 100644
index 00000000..9e836713
--- /dev/null
+++ b/extensions/chaeo/workflows.py
@@ -0,0 +1,91 @@
+from pathlib import Path
+from typing import Dict
+
+from pydantic import BaseModel
+
+
+from extensions.ilastik.models import IlastikPixelClassifierModel
+from extensions.chaeo.annotators import draw_boxes_on_3d_image
+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 model_server.workflows import Timer
+
+def export_patches_from_multichannel_zstack(  # TODO: PyDantic model for arguments
+        input_zstack_path: Path,
+        px_model: IlastikPixelClassifierModel,
+        pxmap_threshold: float,
+        pixel_class: int,
+        zmask_channel: int,
+        patches_channel: int,
+        where_output: Path,
+        mask_type: str = 'boxes',
+        zmask_filters: Dict = None,
+        zmask_expand_box_by: int = None,
+) -> Dict:
+
+    ti = Timer()
+    stack = generate_file_accessor(input_zstack_path)
+    fstem = input_zstack_path.stem
+    ti.click('file_input')
+    assert stack.nz > 1, 'Expecting z-stack'
+
+    # MIP and classify pixels
+    mip = InMemoryDataAccessor(
+        stack.get_one_channel_data(channel=0).data.max(axis=-1, keepdims=True)
+    )
+    pxmap, _ = px_model.infer(mip)
+    ti.click('infer_pixel_probability')
+
+    write_accessor_data_to_file(
+        where_output / 'pixel_probabilities' / (fstem + '.tif'),
+        pxmap
+    )
+    ti.click('export_pixel_probability')
+
+    obmask = InMemoryDataAccessor(
+        pxmap.data > pxmap_threshold
+    )
+    ti.click('threshold_pixel_mask')
+
+    # make zmask
+    zmask, zmask_meta = build_zmask_from_object_mask(
+        obmask.get_one_channel_data(pixel_class),
+        stack.get_one_channel_data(zmask_channel),
+        mask_type=mask_type,
+        filters=zmask_filters,
+        expand_box_by=zmask_expand_box_by,
+    )
+    zmask_acc = InMemoryDataAccessor(zmask)
+    ti.click('generate_zmasks')
+
+    # export patches
+    files = export_patches_from_zstack(
+        where_output / '2d_patches',
+        stack.get_one_channel_data(patches_channel),
+        zmask_meta,
+        prefix=fstem,
+        draw_bounding_box=True,
+    )
+    ti.click('export_patches')
+
+    # export annotated zstack
+    annotated = InMemoryDataAccessor(
+        draw_boxes_on_3d_image(
+            stack.get_one_channel_data(patches_channel).data,
+            zmask_meta
+        )
+    )
+    write_accessor_data_to_file(
+        where_output / 'annotated_zstacks' / (fstem + '.tif'),
+        annotated
+    )
+    ti.click('export_annotated_zstack')
+
+    return {
+        'pixel_model_id': px_model.model_id,
+        'input_filepath': str(input_zstack_path),
+        'number_of_objects': len(zmask_meta),
+        'success': True,
+        'timer_results': ti.events,
+    }
\ No newline at end of file
diff --git a/extensions/ilastik/examples/batch_run_patches.py b/extensions/ilastik/examples/batch_run_patches.py
new file mode 100644
index 00000000..c985d818
--- /dev/null
+++ b/extensions/ilastik/examples/batch_run_patches.py
@@ -0,0 +1,34 @@
+from pathlib import Path
+
+from extensions.chaeo.workflows import export_patches_from_multichannel_zstack
+from extensions.ilastik.models import IlastikPixelClassifierModel
+
+if __name__ == '__main__':
+    where_czi = Path(
+        'z:/rhodes/projects/proj0004-marine-photoactivation/data/exp0038/AutoMic/20230906-163415/Selection'
+    )
+    where_output = Path(
+        'c:/Users/rhodes/projects/proj0011-plankton-seg/exp0009/batch_output'
+    )
+    px_ilp = Path.home() / 'model-server' / 'ilastik' / 'AF405-bodies_boundaries.ilp'
+    px_model = IlastikPixelClassifierModel(
+        params={'project_file': px_ilp}
+    )
+    for ff in where_czi.iterdir():
+        print(ff)
+        if not ff.suffix.upper() == '.CZI':
+            continue
+
+        export_patches_from_multichannel_zstack(
+            input_zstack_path=where_czi/ff,
+            px_model=px_model,
+            pxmap_threshold=0.6,
+            pixel_class=0,
+            zmask_channel=0,
+            patches_channel=4,
+            where_output=where_output,
+            mask_type='boxes',
+            zmask_filters={'area': (1e2, 1e5)},
+            zmask_expand_box_by=(64, 3),
+        )
+
-- 
GitLab