Skip to content

Commit 45e2435

Browse files
committed
Decoupled channels & time points, functions to invalidate computed columns, enforce bound restrictions on getShapePixels by injecting nan where the shape is out of bound, & bug fixes.
1 parent 0887f70 commit 45e2435

17 files changed

Lines changed: 662 additions & 231 deletions

File tree

mapmanagercore/analysis_params.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,23 @@
33

44
from mapmanagercore.logger import logger
55

6+
67
class AnalysisParams():
7-
def __init__(self, loadJson : str = None):
8-
8+
def __init__(self, loadJson: str = None):
9+
910
# self.__version__ = 0.1
1011
# self.__version__ = 0.1 # switched to dict of dicts
1112
self.__version__ = 0.2 # 20240508 added anchorPointSearchDistance
1213
self.__version__ = 0.3 # segmentTracingMaxDistance
14+
self.__version__ = 0.4 # maxChannels
1315

1416
if loadJson is not None:
1517
self._dict = json.loads(loadJson)
1618
if self._dict['__version__'] <= self.__version__:
1719
self._getDefauls()
1820
else:
1921
self._getDefauls()
20-
22+
2123
def getDict(self):
2224
return self._dict
2325

@@ -29,7 +31,7 @@ def _getDefauls(self):
2931
"""
3032
self._dict = {
3133
'__version__': self.__version__,
32-
34+
3335
# new spine
3436
'brightestPathDistance': {
3537
'defaultValue': 10,
@@ -61,21 +63,27 @@ def _getDefauls(self):
6163
'currentValue': 4,
6264
'description': 'Width of spine ROI.'
6365
},
64-
66+
6567
# segment
6668
'segmentRadius': {
6769
'defaultValue': 4,
6870
'currentValue': 4,
6971
'description': 'Radius of segment tracing.'
7072
},
71-
72-
# The distance
73+
74+
# The distance
7375
'segmentTracingMaxDistance': {
7476
'defaultValue': 30,
7577
'currentValue': 30,
7678
'description': 'Max distance to trace a brightest path with relatively low performance cost.'
7779
},
7880

81+
'maxChannels': {
82+
'defaultValue': 2,
83+
'currentValue': 2,
84+
'description': 'Max number of channels.'
85+
},
86+
7987
# anchor point search distance
8088
# 'anchorPointSearchDistance': {
8189
# 'defaultValue': 10,
@@ -90,15 +98,15 @@ def __getitem__(self, key) -> Optional[object]:
9098
"""
9199
return self.getValue(key)
92100

93-
def getValue(self, key : str) -> Optional[object]:
101+
def getValue(self, key: str) -> Optional[object]:
94102
"""Get the value for a key, return None of KeyError.
95103
"""
96104
try:
97105
return self._dict[key]['currentValue']
98106
except (KeyError):
99107
logger.error(f'did not find key "{key}", possible keys are {self._dict.keys()}')
100108

101-
def setValue(self, key : str, value : object):
109+
def setValue(self, key: str, value: object):
102110
try:
103111
self._dict[key]['currentValue'] = value
104112
except (KeyError):
@@ -109,9 +117,9 @@ def save(self):
109117
"""
110118
pass
111119

112-
def load(self, path : str):
120+
def load(self, path: str):
113121
"""Load JSON from zarr file into our _dict.
114-
122+
115123
Parameters
116124
----------
117125
path : str

mapmanagercore/annotations/base.py

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import Any, Tuple, Union
55
import weakref
66
import zipfile
7+
from mapmanagercore.lazy_geo_pd_images.loader.base import Position
78
import numpy as np
89
import pandas as pd
910

@@ -24,6 +25,7 @@
2425
from mapmanagercore.analysis_params import AnalysisParams
2526
from mapmanagercore.logger import logger
2627

28+
2729
class AnnotationsBase(LazyImagesGeoPandas):
2830
_images: ImageLoader
2931

@@ -32,7 +34,8 @@ def __init__(self,
3234
lineSegments: Union[str, pd.DataFrame] = pd.DataFrame(),
3335
points: Union[str, pd.DataFrame] = pd.DataFrame(),
3436
analysisParams: AnalysisParams = AnalysisParams(),
35-
path: str = None):
37+
path: str = None,
38+
version: int = None):
3639

3740
super().__init__(loader)
3841

@@ -49,35 +52,43 @@ def __init__(self,
4952

5053
self._segments = LazyGeoFrame(
5154
Segment, data=lineSegments, store=weakref.ref(self))
52-
self._points = LazyGeoFrame(Spine, data=points, store=weakref.ref(self))
55+
self._points = LazyGeoFrame(
56+
Spine, data=points, store=weakref.ref(self))
5357

5458
self.loader = loader
5559
self.path = path
60+
61+
# To invalidate columns that were miss-computed in previous version
62+
# we can conditionally check the version number
63+
# if version === 0:
64+
# then we can invalidate the invalid columns by name
65+
# self._segments.invalidateColumns([... columns ...])
66+
5667

5768
# abb
5869
def __str__(self):
5970
"""Print info about the map.
60-
71+
6172
See: _SingleTimePointAnnotationsBase()
6273
"""
6374
numTimepoints = len(self._images.timePoints())
6475
numPnts = len(self.points._rootDf)
6576
numSegments = len(self.segments._rootDf)
6677

6778
return f't:{numTimepoints}, points:{numPnts} segments:{numSegments} loader:{self.loader}'
68-
79+
6980
@property
7081
def segments(self) -> LazyGeoFrame:
7182
return self._segments
7283

7384
@property
7485
def points(self) -> LazyGeoFrame:
7586
return self._points
76-
87+
7788
@property
7889
def analysisParams(self) -> AnalysisParams:
7990
return self._analysisParams
80-
91+
8192
def filterPoints(self, filter: Any):
8293
"""
8394
Filters the points.
@@ -100,7 +111,7 @@ def getTimePoint(self, time: int):
100111
"""
101112
from .single_time_point import SingleTimePointAnnotations
102113
return SingleTimePointAnnotations(self, time)
103-
114+
104115
def getPixels(self, time: int, channel: int, zRange: Tuple[int, int] = None, z: int = None, zSpread: int = 0) -> ImageSlice:
105116
"""
106117
Loads the image data for a slice.
@@ -152,7 +163,7 @@ def checkFile(cls, path: str, lazy=True, verbose=False) -> bool:
152163
store = zarr.DirectoryStore(path)
153164
else:
154165
store = zarr.ZipStore(path, mode="r")
155-
166+
156167
group = zarr.group(store=store)
157168

158169
if verbose:
@@ -191,7 +202,8 @@ def checkFile(cls, path: str, lazy=True, verbose=False) -> bool:
191202

192203
# (2) points
193204
try:
194-
_points = group["points"] # zarr.core.Array '/points' (255865,) uint8
205+
# zarr.core.Array '/points' (255865,) uint8
206+
_points = group["points"]
195207
except (KeyError) as e:
196208
logger.error('did not find group "points"')
197209
logger.error(f' {e}')
@@ -216,7 +228,8 @@ def checkFile(cls, path: str, lazy=True, verbose=False) -> bool:
216228
_errors += 1
217229
finally:
218230
try:
219-
_lineSegments = pd.read_pickle(BytesIO(_lineSegments[:].tobytes()))
231+
_lineSegments = pd.read_pickle(
232+
BytesIO(_lineSegments[:].tobytes()))
220233
if verbose:
221234
logger.info(f'lineSegments: {len(_lineSegments)}')
222235
# print(_lineSegments.head())
@@ -244,24 +257,33 @@ def checkFile(cls, path: str, lazy=True, verbose=False) -> bool:
244257
logger.info(f'encountered {_errors} errors while inspecting {path}')
245258

246259
return _errors == 0
247-
260+
261+
def merge(self, loader: ImageLoader):
262+
self.loader.merge(loader)
263+
248264
@classmethod
249265
def load(cls, path: Union[str, None], lazy=False):
250266
loader = ZarrLoader(path, lazy=lazy)
251-
points = pd.read_pickle(BytesIO(loader.group["points"][:].tobytes()))
252-
points = gp.GeoDataFrame(points, geometry="point")
253-
lineSegments = pd.read_pickle(
254-
BytesIO(loader.group["lineSegments"][:].tobytes()))
255-
lineSegments = gp.GeoDataFrame(lineSegments, geometry="segment")
256267

257-
# abb analysisparams
258-
_analysisParams_json = loader.group.attrs['analysisParams'] # json str
259-
analysisParams = AnalysisParams(loadJson=_analysisParams_json)
268+
if "points" in loader.group:
269+
points = pd.read_pickle(
270+
BytesIO(loader.group["points"][:].tobytes()))
271+
points = gp.GeoDataFrame(points, geometry="point")
272+
else:
273+
points = gp.GeoDataFrame()
260274

275+
if "lineSegments" in loader.group:
276+
lineSegments = pd.read_pickle(
277+
BytesIO(loader.group["lineSegments"][:].tobytes()))
278+
lineSegments = gp.GeoDataFrame(lineSegments, geometry="segment")
279+
else:
280+
lineSegments = gp.GeoDataFrame()
281+
282+
analysisParams = loader.analysisParams()
261283

262284
return cls(loader, lineSegments, points, analysisParams, path)
263285

264-
def save(self, path: str=None, compression=zipfile.ZIP_STORED):
286+
def save(self, path: str = None, compression=zipfile.ZIP_STORED):
265287
if path is None:
266288
path = self.path
267289

@@ -279,7 +301,8 @@ def save(self, path: str=None, compression=zipfile.ZIP_STORED):
279301

280302
with fs as store:
281303
group = zarr.group(store=store)
282-
self._images.saveTo(group)
304+
images = group.create_group("images");
305+
self._images.saveTo(images)
283306
group.create_dataset(
284307
"points", data=self.points.toBytes(), dtype=np.uint8)
285308
group.create_dataset(

mapmanagercore/annotations/pyodide.py

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import json
2-
from typing import Tuple
2+
from typing import List, Tuple, Union
3+
from mapmanagercore.lazy_geo_pd_images.loader.base import Position
4+
from mapmanagercore.lazy_geo_pd_images.loader.imageio import MultiImageLoader
5+
from mapmanagercore.lazy_geo_pd_images.loader.zarr import ZarrLoader
36
import numpy as np
47
from ..benchmark import timeAll
58
from ..config import SpineId
@@ -10,6 +13,10 @@
1013
from . import Annotations
1114
from pyodide.ffi import to_js
1215
from .single_time_point import SingleTimePointAnnotations
16+
from enum import Enum
17+
18+
19+
type ChangeOrError = Union[bool, str]
1320

1421

1522
class PyodideSingleTimePoint(SingleTimePointAnnotations):
@@ -30,7 +37,7 @@ def getSpinePosition(self, spineID: SpineId):
3037
"""Returns the position of a spine in the current time point."""
3138
if spineID not in self.points.index:
3239
return None
33-
40+
3441
points = self.points[spineID, ["point", "z"]]
3542
return to_js([*list(*points["point"].coords), int(points["z"])])
3643

@@ -79,8 +86,22 @@ class PyodideAnnotations(Annotations):
7986
""" PyodideAnnotations contains pyodide specific helper methods to allow JS to use Annotations.
8087
"""
8188

89+
def mergeFile(self, path: str, timePoint: int = 0, channel: int = 0, name: str = None, position: Position = Position.OVER):
90+
if name is None:
91+
name = path
92+
if path.endswith(".mmap"):
93+
loader = ZarrLoader(path)
94+
if path.endswith(".tif"):
95+
loader = MultiImageLoader()
96+
loader.read(path, time=timePoint, channel=channel, name=name)
97+
self.loader.merge(loader)
98+
8299
def timePoint_js(self, time: int):
83100
return PyodideSingleTimePoint(self, time)
101+
102+
def metadata_json(self, time: int):
103+
"""Returns the metadata as a JSON string."""
104+
return self.loader.metadata(time).to_json()
84105

85106
def slices_js(self, time: int, channel: int, zRange: Tuple[int, int]) -> ImageSlice:
86107
"""
@@ -129,3 +150,66 @@ def getSymbols(self, shapeOn: str = None):
129150
def columnsAttributes_json(self):
130151
"""Returns the columnsAttributes as a JSON string."""
131152
return json.dumps(self.points.columnsAttributes, skipkeys=True)
153+
154+
def dataTree(self):
155+
return json.dumps(self.loader.dataTree(), skipkeys=True)
156+
157+
def createTimePoint(self) -> ChangeOrError:
158+
try:
159+
return self.loader.createTimePoint()
160+
except Exception as e:
161+
return str(e)
162+
163+
def appendChannelToTimePoint(self, srcTimePoint: int, srcChannel: int, destTimePoint: int) -> ChangeOrError:
164+
try:
165+
return self.loader.appendChannelToTimePoint(srcTimePoint, srcChannel, destTimePoint)
166+
except Exception as e:
167+
return str(e)
168+
169+
def moveChannel(self, srcTimePoint: int, srcChannel: int, destTimePoint: int, destChannel: int) -> ChangeOrError:
170+
try:
171+
return self.loader.moveChannel(srcTimePoint, srcChannel, destTimePoint, destChannel)
172+
except Exception as e:
173+
return str(e)
174+
175+
def moveTimePoint(self, srcTimePoint: int, destTimePoint: int, position: Position = Position.OVER) -> ChangeOrError:
176+
try:
177+
return self.loader.moveTimePoint(srcTimePoint, destTimePoint, int(position))
178+
except Exception as e:
179+
return str(e)
180+
181+
def deleteTimePoint(self, timePoint: int) -> ChangeOrError:
182+
try:
183+
return self.loader.deleteTimePoint(timePoint)
184+
except Exception as e:
185+
return str(e)
186+
187+
def deleteChannel(self, timePoint: int, channel: int) -> ChangeOrError:
188+
try:
189+
return self.loader.deleteChannel(timePoint, channel)
190+
except Exception as e:
191+
return str(e)
192+
193+
def updateChannel(self, timePoint: int, channel: int, updates: dict) -> ChangeOrError:
194+
try:
195+
return self.loader.updateChannel(timePoint, channel, updates.to_py())
196+
except Exception as e:
197+
return str(e)
198+
199+
def updateTimePoint(self, timePoint: int, updates: dict) -> ChangeOrError:
200+
try:
201+
return self.loader.updateTimePoint(timePoint, updates.to_py())
202+
except Exception as e:
203+
return str(e)
204+
205+
def maxChannels(self) -> int:
206+
return self.loader.maxChannels()
207+
208+
def timePoints_js(self):
209+
return to_js(list(self.loader.timePoints()))
210+
211+
def setMaxChannels(self, maxChannels: int):
212+
try:
213+
return self.loader.setMaxChannels(maxChannels)
214+
except Exception as e:
215+
return str(e)

0 commit comments

Comments
 (0)