From 60a3e66eb7efcdf8a2e12008cb745054a82f15ce Mon Sep 17 00:00:00 2001
From: Christopher Rhodes <christopher.rhodes@embl.de>
Date: Sun, 22 Oct 2023 09:16:51 +0200
Subject: [PATCH] Expose image pixel scale to Czi accessor

---
 conf/testing.py           |  1 +
 model_server/accessors.py | 21 +++++++++++++++++++--
 tests/test_accessors.py   |  6 +++++-
 3 files changed, 25 insertions(+), 3 deletions(-)

diff --git a/conf/testing.py b/conf/testing.py
index f2df5040..3c52b95c 100644
--- a/conf/testing.py
+++ b/conf/testing.py
@@ -10,6 +10,7 @@ czifile = {
     'h': 1274,
     'c': 5,
     'z': 1,
+    'um_per_pixel': 1/3.9881,
 }
 
 filename = 'rgb.png'
diff --git a/model_server/accessors.py b/model_server/accessors.py
index b6e5da1f..caec8ddf 100644
--- a/model_server/accessors.py
+++ b/model_server/accessors.py
@@ -37,13 +37,19 @@ class GenericImageDataAccessor(ABC):
         if self._data.dtype == 'bool':
             return True
         elif self._data.dtype == 'uint8':
-            return np.all(np.unique(self._data) == [0, 255])
+            unique = np.unique(self._data)
+            if unique.shape[0] == 2 and np.all(unique == [0, 255]):
+                return True
         return False
 
     def get_one_channel_data (self, channel: int):
         c = int(channel)
         return InMemoryDataAccessor(self.data[:, :, c:(c+1), :])
 
+    @property
+    def pixel_scale_in_micrometers(self):
+        return None
+
     @property
     def dtype(self):
         return self.data.dtype
@@ -163,12 +169,23 @@ class CziImageFileAccessor(GenericImageFileAccessor):
             [cf.axes.index(ch) for ch in idx],
             [0, 1, 2, 3]
         )
-
         self._data = self.conform_data(yxcz.reshape(yxcz.shape[0:4]))
 
     def __del__(self):
         self.czifile.close()
 
+    @property
+    def pixel_scale_in_micrometers(self):
+        meta_sc_str = self.czifile.metadata(raw=False)['ImageDocument']['Metadata']['ImageScaling']['ImagePixelSize']
+        meta_mags = self.czifile.metadata(raw=False)['ImageDocument']['Metadata']['ImageScaling']['ScalingComponent']
+        sc_xy = list(map(
+            float,
+            meta_sc_str.split(',')
+        ))
+        assert sc_xy[0] == sc_xy[1]
+        mags = [float(m['Magnification']) for m in meta_mags]
+        return sc_xy[0] / np.product(mags)
+
 
 def write_accessor_data_to_file(fpath: Path, accessor: GenericImageDataAccessor, mkdir=True) -> bool:
     if mkdir:
diff --git a/tests/test_accessors.py b/tests/test_accessors.py
index f7a4341d..1a465dfb 100644
--- a/tests/test_accessors.py
+++ b/tests/test_accessors.py
@@ -110,4 +110,8 @@ class TestCziImageFileAccess(unittest.TestCase):
 
     def test_read_zstack_mono_mask(self):
         acc = generate_file_accessor(monozstackmask['path'])
-        self.assertTrue(acc.is_mask())
\ No newline at end of file
+        self.assertTrue(acc.is_mask())
+
+    def test_read_in_pixel_scale_from_czi(self):
+        cf = CziImageFileAccessor(czifile['path'])
+        self.assertAlmostEqual(cf.pixel_scale_in_micrometers, czifile['um_per_pixel'], places=3)
\ No newline at end of file
-- 
GitLab