From 799f2ea538b390a3725ec38ca44e2a1cebac8594 Mon Sep 17 00:00:00 2001
From: Christopher Rhodes <christopher.rhodes@embl.de>
Date: Thu, 1 Aug 2024 16:46:06 +0200
Subject: [PATCH] Implemented and tested creation of RoiSet from polygon
 coordinates

---
 model_server/base/roiset.py | 50 ++++++++++++++++++++++++++-----------
 tests/base/test_roiset.py   | 22 ++++++++++------
 2 files changed, 49 insertions(+), 23 deletions(-)

diff --git a/model_server/base/roiset.py b/model_server/base/roiset.py
index dcd8cfa7..bdbd2c8c 100644
--- a/model_server/base/roiset.py
+++ b/model_server/base/roiset.py
@@ -10,6 +10,7 @@ from pydantic import BaseModel
 from scipy.stats import moment
 from skimage.filters import sobel
 
+from skimage import draw
 from skimage.measure import approximate_polygon, find_contours, label, points_in_poly, regionprops, regionprops_table, shannon_entropy
 from skimage.morphology import binary_dilation, disk
 
@@ -181,10 +182,6 @@ def make_df_from_object_ids(acc_raw, acc_obj_ids, expand_box_by) -> pd.DataFrame
     )
     return df
 
-# TODO: implement
-def make_df_from_polygons(acc_raw, polygons:np.ndarray) -> pd.DataFrame:
-    pass
-
 
 def df_insert_slices(df: pd.DataFrame, sd: dict, expand_box_by) -> pd.DataFrame:
     h = sd['Y']
@@ -345,20 +342,43 @@ class RoiSet(object):
         Create a RoiSet from a binary segmentation mask (either 2D or 3D)
         :param acc_raw: accessor to a generally a multichannel z-stack
         :param acc_seg: accessor of a binary segmentation mask (mono) of either two or three dimensions
-        :param allow_3d: return a 3D map if True; return a 2D map of the mask's maximum intensity project if False
+        :param allow_3d: use a 3D map if True; use a 2D map of the mask's maximum intensity project if False
         :param connect_3d: objects can span multiple z-positions if True; objects are unique to a single z if False
         :param params: optional arguments that influence the definition and representation of ROIs
-        :return: object identities map
         """
-        return RoiSet.from_object_ids(acc_raw, get_label_ids(acc_seg, allow_3d=allow_3d, connect_3d=connect_3d), params)
+        return RoiSet.from_object_ids(
+            acc_raw,
+            get_label_ids(
+                acc_seg,
+                allow_3d=allow_3d,
+                connect_3d=connect_3d
+            ),
+            params
+        )
 
     @staticmethod
-    #TODO: implement
-    def from_polygons(acc_raw, polygons: np.ndarray):
-        binary_poly = np.zeros(acc_raw.mono(0).shape, dtype=bool)
-        for p in poly:
-            pidcs = draw.polygon(p[:, 1], p[:, 0])
-            binary_poly[pidcs] = True
+    def from_polygons_2d(
+            acc_raw,
+            polygons: List[np.ndarray],
+            params: RoiSetMetaParams = RoiSetMetaParams()
+    ):
+        """
+        Create a RoiSet where objects are defined from a list of polygon coordinates
+        :param acc_raw: accessor to a generally a multichannel z-stack
+        :param polygons: list of (variable x 2) np.ndarrays describing (x, y) polymer coordinates
+        :param params: optional arguments that influence the definition and representation of ROIs
+        """
+        mask = np.zeros(acc_raw.get_mono(0, mip=True).shape, dtype=bool)
+        for p in polygons:
+            sl = draw.polygon(p[:, 1], p[:, 0])
+            mask[sl] = True
+        return RoiSet.from_binary_mask(
+            acc_raw,
+            InMemoryDataAccessor(mask),
+            allow_3d=False,
+            connect_3d=False,
+            params=params,
+        )
 
 
     # TODO: get overlapping segments
@@ -394,7 +414,7 @@ class RoiSet(object):
         write_accessor_data_to_file(fp, annotated)
         return (prefix + '.tif')
 
-    def get_zmask(self, mask_type='boxes'):
+    def get_zmask(self, mask_type='boxes') -> np.ndarray:
         """
         Return a mask of same dimensionality as raw data
 
@@ -780,7 +800,7 @@ class RoiSet(object):
         return record
 
     def get_polygons(self, poly_threshold=0, dilation_radius=1) -> pd.DataFrame:
-        """
+        self.coordinates_ = """
         Fit polygons to all object boundaries in the RoiSet
         :param poly_threshold: threshold distance for polygon fit; a smaller number follows sharp features more closely
         :param dilation_radius: radius of binary dilation to apply before fitting polygon
diff --git a/tests/base/test_roiset.py b/tests/base/test_roiset.py
index 1626aca0..3f31948d 100644
--- a/tests/base/test_roiset.py
+++ b/tests/base/test_roiset.py
@@ -659,7 +659,7 @@ class TestRoiSetSerialization(unittest.TestCase):
 class TestRoiSetPolygons(BaseTestRoiSetMonoProducts, unittest.TestCase):
 
     def test_compute_polygons(self):
-        roiset = RoiSet.from_binary_mask(
+        roiset_ref = RoiSet.from_binary_mask(
             self.stack_ch_pa,
             self.seg_mask,
             params=RoiSetMetaParams(
@@ -668,19 +668,25 @@ class TestRoiSetPolygons(BaseTestRoiSetMonoProducts, unittest.TestCase):
             )
         )
 
-        poly = roiset.get_polygons()
-        binary_poly = np.zeros(self.seg_mask.hw, dtype=bool)
-        for p in poly:
-            pidcs = draw.polygon(p[:, 1], p[:, 0])
-            binary_poly[pidcs] = True
+        poly = roiset_ref.get_polygons()
+        roiset_test = RoiSet.from_polygons_2d(self.stack_ch_pa, poly)
+        binary_poly = (roiset_test.acc_obj_ids.get_mono(0, mip=True).data > 0)
+        self.assertEqual(self.seg_mask.shape, binary_poly.shape)
+
 
+        # most mask pixels are within in fitted polygon
         test_mask = np.logical_and(
             np.logical_not(binary_poly),
-            (self.seg_mask.data[:, :, 0, 0] == 255)
+            (self.seg_mask.data == 255)
         )
-        # most mask pixels are within in fitted polygon
         self.assertLess(test_mask.sum() / test_mask.size, 0.001)
 
+        # output results
+        od = output_path / 'polygons'
+        write_accessor_data_to_file(od / 'from_polygons.tif', InMemoryDataAccessor(binary_poly))
+        write_accessor_data_to_file(od / 'ref_mask.tif', self.seg_mask)
+        write_accessor_data_to_file(od / 'diff.tif', InMemoryDataAccessor(test_mask))
+
     def test_serialize_coco(self):
         roiset = RoiSet.from_binary_mask(
             self.stack_ch_pa,
-- 
GitLab