mozarrt 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mozarrt/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .mobie_collection import MoBIECollectionEntry, collection_dataframe
2
+
3
+ __all__ = ["MoBIECollectionEntry", "collection_dataframe"]
mozarrt/cli.py ADDED
@@ -0,0 +1,18 @@
1
+ from cyclopts import App
2
+
3
+ from mozarrt.collect import create_plate_project, folder, plate
4
+ from mozarrt.folder import project
5
+
6
+ app = App()
7
+ plate_app = App(
8
+ name="plate",
9
+ help="Actions that take an HCS plate OME-Zarr as input",
10
+ )
11
+ folder_app = App(
12
+ name="folder",
13
+ help="Actions that take a folder with OME-Zarr datasets as input",
14
+ )
15
+
16
+ folder_app.command(project)
17
+ app.command(plate_app)
18
+ app.command(folder_app)
mozarrt/collect.py ADDED
@@ -0,0 +1,220 @@
1
+ from collections import defaultdict
2
+ from cyclopts.types import ExistingDirectory
3
+ from loguru import logger
4
+ from natsort import natsorted
5
+ from ngio import OmeZarrContainer, open_ome_zarr_container, open_ome_zarr_plate
6
+ from ngio.hcs import OmeZarrPlate
7
+ from mobiedantic import Project, Dataset
8
+ from pathlib import Path
9
+
10
+ from mozarrt.mobie_collection import MoBIECollectionEntry, collection_dataframe
11
+
12
+ def _create_intensities_entry(
13
+ uri: str,
14
+ name: str,
15
+ channel: int,
16
+ grid: str,
17
+ display: str,
18
+ # grid_position: tuple[int, int],
19
+ ) -> MoBIECollectionEntry:
20
+ return MoBIECollectionEntry(
21
+ uri=uri,
22
+ name=name,
23
+ type="intensities",
24
+ format="OmeZarr",
25
+ channel=channel,
26
+ contrast_limits=(0, 255),
27
+ view="all",
28
+ display=display,
29
+ grid=grid,
30
+ # grid_position=grid_position,
31
+ )
32
+
33
+ def _create_labels_entry(
34
+ uri: str,
35
+ name: str,
36
+ channel: int,
37
+ grid: str,
38
+ display: str,
39
+ # grid_position: tuple[int, int],
40
+ ) -> MoBIECollectionEntry:
41
+ return MoBIECollectionEntry(
42
+ uri=uri,
43
+ name=name,
44
+ type="labels",
45
+ format="OmeZarr",
46
+ channel=channel,
47
+ view="all",
48
+ grid=grid,
49
+ display=display,
50
+ # grid_position=grid_position,
51
+ )
52
+
53
+ def _create_spots_entry(
54
+ uri: str,
55
+ name: str,
56
+ grid: str,
57
+ display: str,
58
+ # grid_position: tuple[int, int],
59
+ ) -> MoBIECollectionEntry:
60
+ return MoBIECollectionEntry(
61
+ uri=uri,
62
+ name=name,
63
+ type="spots",
64
+ view="all",
65
+ grid=grid,
66
+ display=display,
67
+ # grid_position=grid_position,
68
+ )
69
+
70
+ def plate(
71
+ plate_zarr_path: ExistingDirectory,
72
+ output_directory: ExistingDirectory,
73
+ /,
74
+ ):
75
+ # Open plate zarr container
76
+ plate = open_ome_zarr_plate(plate_zarr_path)
77
+
78
+ mobie_collection_entries: list[MoBIECollectionEntry] = []
79
+
80
+ sub_path = "0"
81
+
82
+ for well_path, well in plate.get_wells().items():
83
+ logger.info(f"Processing well: {well_path}")
84
+ assert sub_path in well.paths()
85
+ # For each well, generate MoBIECollectionEntry
86
+ well_image = well.get_image(sub_path).get_image()
87
+ logger.info(well_image.channels_meta)
88
+ for ch_idx, channel in enumerate(well_image.channels_meta.channels):
89
+ logger.info(f" Channel: {channel.label}")
90
+ entry = _create_intensities_entry(
91
+ uri=f"{plate_zarr_path}/{well_path}/{sub_path}",
92
+ name=f"{well_path}_c{ch_idx}",
93
+ channel=ch_idx,
94
+ display=channel.label,
95
+ grid=f"{ch_idx}_grid",
96
+ # grid_position=(0, 0), # TODO compute position from well_path
97
+ )
98
+ mobie_collection_entries.append(entry)
99
+
100
+ df = collection_dataframe(mobie_collection_entries)
101
+ output_path = Path(output_directory) / "mobie_collection.csv"
102
+ df.to_csv(output_path, index=False)
103
+
104
+ def folder(
105
+ input_directory: ExistingDirectory,
106
+ output_directory: ExistingDirectory,
107
+ /,
108
+ *,
109
+ intensity_paths: list[str] | None = None,
110
+ label_paths: list[str] | None = None,
111
+ ):
112
+ mobie_collection_entries: list[MoBIECollectionEntry] = []
113
+ # Loop over all (.zarr) directories in input_directory
114
+ for zarr_dir in Path(input_directory).rglob("*.zarr"):
115
+ # Open zarr container
116
+ container = open_ome_zarr_container(zarr_dir)
117
+ logger.info(f"Processing {zarr_dir}")
118
+ image = container.get_image()
119
+ logger.info(image)
120
+ logger.info(image.path)
121
+ logger.info(image.channels_meta)
122
+ for channel in image.channel_labels:
123
+ logger.info(channel)
124
+ logger.info(image.get_channel_idx(channel))
125
+ for channel in image.channels_meta.channels:
126
+ logger.info(channel)
127
+ logger.info(channel.label)
128
+ logger.info(channel.channel_visualisation.start)
129
+ # Append an intensities entry for each channel
130
+ # for channel_index in range(image.get_channel_count()):
131
+ # entry = _create_intensities_entry(
132
+ # uri=str(zarr_dir.resolve()), # TODO path to image
133
+ # name=f"{zarr_dir.stem}_c{channel_index}",
134
+ # channel=channel_index,
135
+ # grid=f"channel_{channel_index}_grid",
136
+ # grid_position=(0, 0), # TODO compute position from filename/folder content
137
+ # )
138
+ # mobie_collection_entries.append(entry)
139
+
140
+ # df = collection_dataframe(mobie_collection_entries)
141
+ # output_path = output_directory / "mobie_collection.csv"
142
+ # df.to_csv(output_path, index=False)
143
+
144
+ def create_plate_project(
145
+ plate_zarr_path: ExistingDirectory,
146
+ output_directory: ExistingDirectory,
147
+ /,
148
+ ) -> Project:
149
+ plate: OmeZarrPlate = open_ome_zarr_plate(plate_zarr_path)
150
+ logger.info(f"Creating MoBIE Project for plate at {plate_zarr_path}")
151
+ # Initialize Project
152
+ project: Project = Project(output_directory)
153
+ project.initialize_model(
154
+ description="Test project",
155
+ )
156
+
157
+ # Create Dataset for plate
158
+ plate_dataset: Dataset = project.new_dataset(
159
+ # name=plate.meta.plate.name,
160
+ name=plate_zarr_path.name,
161
+ )
162
+ plate_dataset.initialize_with_paths(
163
+ path_dict={},
164
+ is2d=True,
165
+ )
166
+
167
+ plate_dataset.save()
168
+
169
+ # From the first well, get subpaths and channel names
170
+ # Then, for each subpath and each channel, create sources and merged grid views
171
+ first_well = plate.get_well(row=plate.rows[0], column=plate.columns[0])
172
+ path_names = first_well.paths()
173
+ logger.info(f"Path names in first well: {path_names}")
174
+ first_well_image = first_well.get_image(path_names[0]).get_image()
175
+ channel_names = [
176
+ channel.label for channel in first_well_image.channels_meta.channels
177
+ ]
178
+ logger.info(f"Channel names: {channel_names}")
179
+
180
+ # format: sources[sub_path][channel_name] = pathdict (name: Path)
181
+ sources = defaultdict(lambda: defaultdict(dict))
182
+ sources_per_well = defaultdict(list)
183
+
184
+ for well_path in plate.wells_paths():
185
+ logger.info(f"Processing well: {well_path}")
186
+ # add sources for each subpath and channel
187
+ for sub_path in path_names:
188
+ for channel in channel_names:
189
+ logger.info(f" Channel: {channel}")
190
+ source_path = plate_zarr_path / well_path / sub_path
191
+ source_name = f"{well_path.replace("/", "")}_{channel}"
192
+ sources[sub_path][channel][source_name] = source_path
193
+ sources_per_well[well_path].append(source_name)
194
+
195
+ for sub_path, channel_dict in sources.items():
196
+ for channel_index, channel_name in enumerate(channel_names):
197
+ logger.info(f"Adding source for subpath {sub_path}, channel {channel_name}...")
198
+ plate_dataset.add_sources(
199
+ path_dict=channel_dict[channel_name],
200
+ channel_index=channel_index,
201
+ data_format="ome.zarr",
202
+ )
203
+ plate_dataset.add_merged_grid(
204
+ name=f"merged_grid_{sub_path}_{channel_name}",
205
+ sources=list(channel_dict[channel_name]),
206
+ )
207
+
208
+ # Add region view containing all wells
209
+ plate_dataset.add_region_view(
210
+ name="all_wells",
211
+ map_of_sources=sources_per_well,
212
+ )
213
+
214
+
215
+ plate_dataset.save()
216
+ project.save()
217
+ # add sources for each well and channel
218
+ # add merged grid view for each channel, containing all well sources for that channel
219
+ # add sources for labels
220
+
mozarrt/folder.py ADDED
@@ -0,0 +1,86 @@
1
+ from collections import defaultdict
2
+ from cyclopts.types import ExistingDirectory
3
+ from loguru import logger
4
+ from mobiedantic import Project, Dataset
5
+ from natsort import natsorted
6
+ from ngio import open_ome_zarr_container
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from ngio import OmeZarrContainer, Image
12
+
13
+ def project(
14
+ input_directory: ExistingDirectory,
15
+ output_directory: ExistingDirectory,
16
+ /,
17
+ *,
18
+ description: str | None = None,
19
+ ):
20
+ project: Project = Project(output_directory)
21
+ project.initialize_model(
22
+ description=description or "Folder Project",
23
+ )
24
+ dataset: Dataset = project.new_dataset(
25
+ name=input_directory.name,
26
+ )
27
+
28
+ # Create sources for each zarr (and each channel within it)
29
+ # then add merged grid views for each channel
30
+
31
+ zarr_dirs = natsorted(Path(input_directory).glob("*.zarr"))
32
+ first_zarr: OmeZarrContainer = open_ome_zarr_container(zarr_dirs[0])
33
+ first_zarr_image: Image = first_zarr.get_image()
34
+
35
+ # Determine if data is 2D based on number of spatial axes (excluding channel dimension)
36
+ axes_names = first_zarr_image.axes
37
+ # Count spatial axes (z, y, x) - typically 2D has [y, x] and 3D has [z, y, x]
38
+ is2d = "z" not in axes_names
39
+ logger.info(f"Axes: {axes_names}, is2D: {is2d}")
40
+
41
+ dataset.initialize_with_paths(
42
+ path_dict={},
43
+ is2d=is2d,
44
+ )
45
+
46
+ channel_names = [
47
+ channel.label for channel in first_zarr_image.channels_meta.channels
48
+ ]
49
+ logger.info(f"Channel names: {channel_names}")
50
+
51
+
52
+ # format: sources[channel_name] = pathdict (name: Path)
53
+ sources = defaultdict(dict)
54
+ position_map = defaultdict(list)
55
+
56
+ for zarr_dir in zarr_dirs:
57
+ logger.info(f"Processing {zarr_dir}")
58
+ position: OmeZarrContainer = open_ome_zarr_container(zarr_dir)
59
+ logger.info(position.meta)
60
+ for channel_name in channel_names:
61
+ logger.info(f" Channel: {channel_name}")
62
+ source_path = zarr_dir
63
+ source_name = f"{zarr_dir.stem}_{channel_name}"
64
+ sources[channel_name][source_name] = source_path
65
+ position_map[zarr_dir.stem].append(source_name)
66
+
67
+ for channel_index, channel_name in enumerate(channel_names):
68
+ logger.info(f"Adding source for channel {channel_name}...")
69
+ dataset.add_sources(
70
+ path_dict=sources[channel_name],
71
+ channel_index=channel_index,
72
+ data_format="ome.zarr",
73
+ )
74
+ dataset.add_merged_grid(
75
+ name=f"merged_grid_{channel_name}",
76
+ sources=list(sources[channel_name]),
77
+ )
78
+
79
+ # Add region view containing all positions
80
+ dataset.add_region_view(
81
+ name="all_positions",
82
+ map_of_sources=position_map,
83
+ )
84
+
85
+ dataset.save()
86
+ project.save()
@@ -0,0 +1,30 @@
1
+ from dataclasses import dataclass
2
+ from typing import Literal
3
+
4
+ @dataclass
5
+ class MoBIECollectionEntry:
6
+ uri: str
7
+ name: str | None = None
8
+ type: Literal["intensities", "labels", "spots"] | None = None
9
+ channel: int | None = None
10
+ color: str | None = None
11
+ blend: Literal["sum", "alpha"] | None = None
12
+ affine: tuple[float] | None = None
13
+ tps: str | None = None
14
+ view: str | None = None
15
+ exclusive: bool | None = None
16
+ group: str | None = None
17
+ contrast_limits: tuple[int, int] | None = None
18
+ grid: str | None = None # name of the grid
19
+ grid_position: tuple[int, int] | None = None
20
+ display: str | None = None
21
+ format: Literal["OmeZarr"] | None = None
22
+ spot_radius: float | None = None
23
+ bounding_box: str | None = None
24
+
25
+
26
+ def collection_dataframe(entries: list[MoBIECollectionEntry]):
27
+ """Convert a list of MoBIECollectionEntry to a pandas DataFrame."""
28
+ import pandas as pd
29
+ df = pd.DataFrame(entries)
30
+ return df
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: mozarrt
3
+ Version: 0.1.0
4
+ Summary: MoBIE OME-Zarr Tools
5
+ Author-email: Jan Eglinger <jan.eglinger@fmi.ch>
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: cyclopts<5,>=4.5.0
8
+ Requires-Dist: loguru<0.8,>=0.7.3
9
+ Requires-Dist: mobiedantic<0.5,>=0.4.1
10
+ Requires-Dist: natsort<9,>=8.4.0
11
+ Requires-Dist: ngio<0.6,>=0.5.3
12
+ Requires-Dist: pandas<3,>=2.3.3
@@ -0,0 +1,9 @@
1
+ mozarrt/__init__.py,sha256=zumS9pWC3YakVXRQI4CSH-if4W_qq2s7yDJv-1emQE0,133
2
+ mozarrt/cli.py,sha256=LgjzmQI4htOIK5kyqWTKlPm20cxO9S5dfXjLY918A6o,422
3
+ mozarrt/collect.py,sha256=gKt7LdVoL85VqqUsbjTavNcfd-ziYnoRMJ-Ws4T0jY4,7590
4
+ mozarrt/folder.py,sha256=SEWirMFxD4RlxjZLx6h2kcsujYnRG6S4Y8zwNlJAJXw,2826
5
+ mozarrt/mobie_collection.py,sha256=gHmwSiKsma0r5Ru37MeRGbqljWD7YVzyyBkCmOMzGfA,977
6
+ mozarrt-0.1.0.dist-info/METADATA,sha256=HlvEHlgNdZcpYJCo8BrdnMAGUAnNfQ34SpVUbcovlpc,358
7
+ mozarrt-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
8
+ mozarrt-0.1.0.dist-info/entry_points.txt,sha256=VC78V3AHovyTKycOoSZG9qbTal6t_I6I3cQAJH71qEE,44
9
+ mozarrt-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mozarrt = mozarrt.cli:app