Skip to content

Commit c1c4971

Browse files
committed
Documentation and Refactor
1 parent c9cd59e commit c1c4971

40 files changed

Lines changed: 987 additions & 1159 deletions

examples/example.ipynb

Lines changed: 56 additions & 88 deletions
Large diffs are not rendered by default.

mapmanagercore/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
from .annotations import Annotations as MapAnnotations
2-
from .loader.imageio import MultiImageLoader
3-
from .loader.mmap import MMapLoader
2+
from .lazy_geo_pd_images.loader import MultiImageLoader

mapmanagercore/analysis_params.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ def __init__(self, loadJson : str = None):
99
# self.__version__ = 0.1
1010
# self.__version__ = 0.1 # switched to dict of dicts
1111
self.__version__ = 0.2 # 20240508 added anchorPointSearchDistance
12+
self.__version__ = 0.3 # segmentTracingMaxDistance
1213

1314
if loadJson is not None:
1415
self._dict = json.loads(loadJson)
@@ -67,6 +68,13 @@ def _getDefauls(self):
6768
'currentValue': 4,
6869
'description': 'Radius of segment tracing.'
6970
},
71+
72+
# The distance
73+
'segmentTracingMaxDistance': {
74+
'defaultValue': 30,
75+
'currentValue': 30,
76+
'description': 'Max distance to trace a brightest path with relatively low performance cost.'
77+
},
7078

7179
# anchor point search distance
7280
# 'anchorPointSearchDistance': {

mapmanagercore/annotations/base.py

Lines changed: 68 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,50 @@
11
from copy import copy
2-
from typing import Any, Tuple
2+
from io import BytesIO
3+
from typing import Any, Tuple, Union
34
import zipfile
45
import numpy as np
56
import pandas as pd
67

78
from mapmanagercore.benchmark import timer
8-
from mapmanagercore.config import COLORS, scaleColors, symbols
9+
from mapmanagercore.config import Colors, scaleColors, symbols
10+
from mapmanagercore.lazy_geo_pd_images.loader.zarr import ZarrLoader
911
from ..lazy_geo_pandas import LazyGeoFrame
1012
from ..schemas import Segment, Spine
11-
from ..lazy_geo_pd_image import LazyImagesGeoPandas
12-
from ..image_slices import ImageSlice
13-
from ..loader.base import ImageLoader, Loader
13+
from ..lazy_geo_pd_images import LazyImagesGeoPandas, ImageLoader
14+
from ..lazy_geo_pd_images.image_slices import ImageSlice
1415
import zarr
1516
import warnings
1617
from plotly.express.colors import sample_colorscale
18+
import geopandas as gp
1719

1820
from mapmanagercore.analysis_params import AnalysisParams
19-
from mapmanagercore.logger import logger
2021

2122

2223
class AnnotationsBase(LazyImagesGeoPandas):
2324
_images: ImageLoader
2425

25-
def __init__(self, loader: Loader):
26-
super().__init__(loader.images())
26+
def __init__(self,
27+
loader: ImageLoader,
28+
lineSegments: Union[str, pd.DataFrame] = pd.DataFrame(),
29+
points: Union[str, pd.DataFrame] = pd.DataFrame(),
30+
analysisParams: AnalysisParams = AnalysisParams()):
2731

28-
self._segments = LazyGeoFrame(
29-
Segment, data=loader.segments(), store=self)
30-
self._points = LazyGeoFrame(Spine, data=loader.points(), store=self)
32+
super().__init__(loader)
33+
34+
if not isinstance(lineSegments, gp.GeoDataFrame):
35+
if not isinstance(lineSegments, pd.DataFrame):
36+
lineSegments = pd.read_csv(lineSegments, index_col=False)
37+
38+
if not isinstance(points, gp.GeoDataFrame):
39+
if not isinstance(points, pd.DataFrame):
40+
points = pd.read_csv(points, index_col=False)
3141

3242
# abb analysisparams
33-
self._analysisParams: AnalysisParams = loader.analysisParams()
43+
self._analysisParams: AnalysisParams = analysisParams
44+
45+
self._segments = LazyGeoFrame(
46+
Segment, data=lineSegments, store=self)
47+
self._points = LazyGeoFrame(Spine, data=points, store=self)
3448

3549
@property
3650
def segments(self) -> LazyGeoFrame:
@@ -45,16 +59,25 @@ def analysisParams(self) -> AnalysisParams:
4559
return self._analysisParams
4660

4761
def filterPoints(self, filter: Any):
62+
"""
63+
Filters the points.
64+
"""
4865
c = copy(self)
4966
c._points = c._points[filter]
5067
return c
5168

5269
def filterSegments(self, filter: Any):
70+
"""
71+
Filters the segments.
72+
"""
5373
c = copy(self)
5474
c._segments = c._segments[filter]
5575
return c
5676

5777
def getTimePoint(self, time: int):
78+
"""
79+
Returns the annotations for a single time point.
80+
"""
5881
from .single_time_point import SingleTimePointAnnotations
5982
return SingleTimePointAnnotations(self, time)
6083

@@ -83,10 +106,26 @@ def getPixels(self, time: int, channel: int, zRange: Tuple[int, int] = None, z:
83106

84107
return super().getPixels(time, channel, zRange)
85108

109+
# Serialization
110+
111+
@classmethod
112+
def load(cls, path: str, lazy=False):
113+
loader = ZarrLoader(path, lazy=lazy)
114+
points = pd.read_pickle(BytesIO(loader.group["points"][:].tobytes()))
115+
points = gp.GeoDataFrame(points, geometry="point")
116+
lineSegments = pd.read_pickle(
117+
BytesIO(loader.group["lineSegments"][:].tobytes()))
118+
lineSegments = gp.GeoDataFrame(lineSegments, geometry="segment")
119+
120+
# abb analysisparams
121+
_analysisParams_json = loader.group.attrs['analysisParams'] # json str
122+
analysisParams = AnalysisParams(loadJson=_analysisParams_json)
123+
124+
return cls(loader, lineSegments, points, analysisParams)
125+
86126
def save(self, path: str, compression=zipfile.ZIP_STORED):
87127
if not path.endswith(".mmap"):
88-
raise ValueError(
89-
"Invalid file format. Please provide a path ending with '.mmap'.")
128+
path += ".mmap"
90129

91130
with warnings.catch_warnings():
92131
warnings.simplefilter("ignore")
@@ -104,6 +143,8 @@ def save(self, path: str, compression=zipfile.ZIP_STORED):
104143
# abb analysisparams
105144
group.attrs['analysisParams'] = self._analysisParams.getJson()
106145

146+
# Context manager
147+
107148
def __enter__(self):
108149
self._images = self._images.__enter__()
109150
return self
@@ -115,12 +156,17 @@ def close(self):
115156
self._images.close()
116157
return
117158

159+
# Utility functions
160+
118161
@timer
119162
def getColors(self, colorOn: str = None, function=False) -> pd.Series:
163+
"""
164+
Returns the colors of the points.
165+
"""
120166
if colorOn is None:
121167
if function:
122-
return lambda _: COLORS["spine"]
123-
return pd.Series([COLORS["spine"]] * len(self.points), index=self.points.index)
168+
return lambda _: Colors.spine
169+
return pd.Series([Colors.spine] * len(self.points), index=self.points.index)
124170

125171
categorical = False
126172
if colorOn not in self.points.columnsAttributes:
@@ -130,12 +176,12 @@ def getColors(self, colorOn: str = None, function=False) -> pd.Series:
130176
if "colors" in attr:
131177
colors = attr["colors"]
132178
elif "categorical" in attr and attr["categorical"]:
133-
colors = COLORS["categorical"]
179+
colors = Colors.categorical
134180
categorical = True
135181
elif "divergent" in attr and attr["divergent"]:
136-
colors = COLORS["divergent"]
182+
colors = Colors.divergent
137183
else:
138-
colors = COLORS["scalar"]
184+
colors = Colors.scalar
139185

140186
if colorOn in self.points.index.names:
141187
values = pd.Series(self.points.index.get_level_values(
@@ -176,6 +222,9 @@ def extractColor(x):
176222

177223
@timer
178224
def getSymbols(self, shapeOn: str = None, function=False) -> pd.Series:
225+
"""
226+
Returns the symbols of the points.
227+
"""
179228
if shapeOn is None:
180229
if function:
181230
return lambda _: "circle"

mapmanagercore/annotations/mutation.py

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
from typing import Tuple, Union
2-
from mapmanagercore.lazy_geo_pandas.schema import MISSING_VALUE
32
from ..schemas import Spine, Segment
43
from ..config import SegmentId, SpineId
54
from .base import AnnotationsBase
65

7-
from mapmanagercore.logger import logger
8-
96
Key = Union[SpineId, Tuple[SpineId, int]]
107
Keys = Union[Key, list[Key]]
118

129

1310
class AnnotationsBaseMut(AnnotationsBase):
14-
def deleteSpine(self, spineId: Keys, skipLog=False) -> None:
11+
def deleteSpine(self, spineId: Keys, skipLog=False):
12+
"""
13+
Delete the spine with the given ID.
14+
"""
1515
self._drop("Spine", spineId, skipLog=skipLog)
1616

17-
def deleteSegment(self, segmentId: Keys, skipLog=False) -> None:
17+
def deleteSegment(self, segmentId: Keys, skipLog=False):
18+
"""
19+
Delete the segment with the given ID.
20+
"""
1821
try:
1922
if not self.points[["segmentID"]].reset_index().set_index(["segmentID", "t"]).loc[segmentId].empty:
2023
raise ValueError(
@@ -26,47 +29,28 @@ def deleteSegment(self, segmentId: Keys, skipLog=False) -> None:
2629

2730
def updateSpine(self, spineId: Keys, value: Spine, replaceLog=False, skipLog=False):
2831
"""
29-
Set the spine with the given ID to the specified value.
30-
31-
Parameters:
32-
spineId (str): The ID of the spine.
33-
value (Union[dict, gp.Series, pd.Series]): The value to set for the spine.
32+
Set the spine with the given ID to the specified value.
3433
"""
35-
36-
# if value.t != MISSING_VALUE:
37-
# raise ValueError(
38-
# f"Invalid type for column 't' must be set on the spine key")
39-
40-
# if value.spineID != MISSING_VALUE:
41-
# raise ValueError(
42-
# f"Invalid type for column 'spineID' must be set on the spine key")
43-
4434
return self._update("Spine", spineId, value, replaceLog, skipLog)
4535

4636
def updateSegment(self, segmentId: Keys, value: Segment, replaceLog=False, skipLog=False):
4737
"""
4838
Set the segment with the given ID to the specified value.
49-
50-
Parameters:
51-
segmentId (str): The ID of the spine.
52-
value (Union[dict, gp.Series, pd.Series]): The value to set for the spine.
5339
"""
54-
# if value.t != MISSING_VALUE:
55-
# raise ValueError(
56-
# f"Invalid type for column 't' must be set on the segment key")
57-
58-
# if value.segmentID != MISSING_VALUE:
59-
# raise ValueError(
60-
# f"Invalid type for column 'segmentID' must be set on the segment key")
61-
6240
return self._update("Segment", segmentId, value, replaceLog, skipLog)
6341

6442
def newUnassignedSpineId(self) -> SpineId:
43+
"""
44+
Returns a new unassigned spine ID.
45+
"""
6546
if len(self.points) == 0:
6647
return 0
6748
return self.points.index.get_level_values(0).max() + 1
6849

6950
def newUnassignedSegmentId(self) -> SegmentId:
51+
"""
52+
Returns a new unassigned segment ID.
53+
"""
7054
if len(self.segments) == 0:
7155
return 0
7256
return self.segments.index.get_level_values(0).max() + 1

mapmanagercore/annotations/pyodide.py

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
1-
from io import BytesIO, StringIO
21
import json
32
from typing import Tuple
4-
53
import numpy as np
6-
74
from ..benchmark import timeAll
85
from ..config import SpineId
96
from .single_time_point.layers import AnnotationsOptions
10-
from ..image_slices import ImageSlice
7+
from ..lazy_geo_pd_images.image_slices import ImageSlice
118
from ..layers.utils import inRange
12-
from ..loader.mmap import MMapLoader
139
from ..utils import filterMask
1410
from . import Annotations
15-
from pyodide.http import pyfetch
1611
from pyodide.ffi import to_js
1712
from .single_time_point import SingleTimePointAnnotations
1813

@@ -28,9 +23,11 @@ def getAnnotations_js(self, options: AnnotationsOptions):
2823
return [layer.encodeBin() for layer in layers]
2924

3025
def metadata_json(self):
31-
return json.dumps(self.metadata())
26+
"""Returns the metadata as a JSON string."""
27+
return self.metadata().to_json()
3228

3329
def getSpinePosition(self, spineID: SpineId):
30+
"""Returns the position of a spine in the current time point."""
3431
if (spineID, self._t) not in self._annotations.points.index:
3532
return None
3633
return to_js(list(self._annotations.points[(spineID, self._t), "point"].coords)[0])
@@ -80,10 +77,6 @@ class PyodideAnnotations(Annotations):
8077
""" PyodideAnnotations contains pyodide specific helper methods to allow JS to use Annotations.
8178
"""
8279

83-
async def load(path: str):
84-
loader = MMapLoader(path)
85-
return PyodideAnnotations(loader)
86-
8780
def timePoint_js(self, time: int):
8881
return PyodideSingleTimePoint(self, time)
8982

@@ -102,7 +95,9 @@ def slices_js(self, time: int, channel: int, zRange: Tuple[int, int]) -> ImageSl
10295
return self.getPixels(time, channel, (zRange[0], zRange[1]))
10396

10497
def table(self):
105-
columns = [key for key, value in self.points.columnsAttributes.items() if value["plot"]]
98+
"""Returns the points as a pandas DataFrame."""
99+
columns = [
100+
key for key, value in self.points.columnsAttributes.items() if value["plot"]]
106101
columns.remove("t")
107102
df = self.points[columns].reset_index()
108103
for i, type in enumerate(df.dtypes):
@@ -111,6 +106,7 @@ def table(self):
111106
return df
112107

113108
def getColumn(self, column: str):
109+
"""Returns the values of a column in the points DataFrame."""
114110
if column in self.points.index.names:
115111
result = self.points.index.get_level_values(column).to_list()
116112
else:
@@ -121,21 +117,13 @@ def getColumn(self, column: str):
121117
return result
122118

123119
def getColors(self, colorOn: str = None):
120+
"""Returns the colors of the points in the DataFrame."""
124121
return super().getColors(colorOn).to_list()
125-
122+
126123
def getSymbols(self, shapeOn: str = None):
124+
"""Returns the symbols of the points in the DataFrame."""
127125
return super().getSymbols(shapeOn).to_list()
128126

129127
def columnsAttributes_json(self):
128+
"""Returns the columnsAttributes as a JSON string."""
130129
return json.dumps(self.points.columnsAttributes, skipkeys=True)
131-
132-
133-
async def loadGeoCsv(path):
134-
response = await pyfetch(path)
135-
csv_text = await response.text()
136-
return StringIO(csv_text)
137-
138-
139-
async def fetchBytes(url: str):
140-
response = await pyfetch(url)
141-
return BytesIO(await response.memoryview())

0 commit comments

Comments
 (0)