ngio 0.2.2__py3-none-any.whl → 0.2.4__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.
ngio/common/_pyramid.py CHANGED
@@ -190,12 +190,16 @@ def init_empty_pyramid(
190
190
  "The shape and chunks must have the same number of dimensions."
191
191
  )
192
192
 
193
+ if chunks is not None:
194
+ chunks = [min(c, s) for c, s in zip(chunks, ref_shape, strict=True)]
195
+
193
196
  if len(ref_shape) != len(scaling_factors):
194
197
  raise NgioValueError(
195
198
  "The shape and scaling factor must have the same number of dimensions."
196
199
  )
197
200
 
198
- root_group, _ = open_group_wrapper(store, mode=mode)
201
+ root_group = open_group_wrapper(store, mode=mode)
202
+
199
203
  for path in paths:
200
204
  if any(s < 1 for s in ref_shape):
201
205
  raise NgioValueError(
ngio/hcs/plate.py CHANGED
@@ -1,5 +1,7 @@
1
1
  """A module for handling the Plate Collection in an OME-Zarr file."""
2
2
 
3
+ from typing import Literal, overload
4
+
3
5
  from ngio.images import OmeZarrContainer
4
6
  from ngio.ome_zarr_meta import (
5
7
  ImageInWellPath,
@@ -12,7 +14,29 @@ from ngio.ome_zarr_meta import (
12
14
  get_well_meta_handler,
13
15
  path_in_well_validation,
14
16
  )
15
- from ngio.utils import AccessModeLiteral, StoreOrGroup, ZarrGroupHandler
17
+ from ngio.tables import (
18
+ FeatureTable,
19
+ GenericRoiTable,
20
+ MaskingRoiTable,
21
+ RoiTable,
22
+ Table,
23
+ TablesContainer,
24
+ TypedTable,
25
+ )
26
+ from ngio.utils import (
27
+ AccessModeLiteral,
28
+ NgioValidationError,
29
+ NgioValueError,
30
+ StoreOrGroup,
31
+ ZarrGroupHandler,
32
+ )
33
+
34
+
35
+ def _default_table_container(handler: ZarrGroupHandler) -> TablesContainer | None:
36
+ """Return a default table container."""
37
+ success, table_handler = handler.safe_derive_handler("tables")
38
+ if success and isinstance(table_handler, ZarrGroupHandler):
39
+ return TablesContainer(table_handler)
16
40
 
17
41
 
18
42
  # Mock lock class that does nothing
@@ -165,14 +189,20 @@ class OmeZarrWell:
165
189
  class OmeZarrPlate:
166
190
  """A class to handle the Plate Collection in an OME-Zarr file."""
167
191
 
168
- def __init__(self, group_handler: ZarrGroupHandler) -> None:
192
+ def __init__(
193
+ self,
194
+ group_handler: ZarrGroupHandler,
195
+ table_container: TablesContainer | None = None,
196
+ ) -> None:
169
197
  """Initialize the LabelGroupHandler.
170
198
 
171
199
  Args:
172
200
  group_handler: The Zarr group handler that contains the Plate.
201
+ table_container: The tables container that contains plate level tables.
173
202
  """
174
203
  self._group_handler = group_handler
175
204
  self._meta_handler = find_plate_meta_handler(group_handler)
205
+ self._tables_container = table_container
176
206
 
177
207
  def __repr__(self) -> str:
178
208
  """Return a string representation of the plate."""
@@ -638,6 +668,107 @@ class OmeZarrPlate:
638
668
  parallel_safe=parallel_safe,
639
669
  )
640
670
 
671
+ @property
672
+ def tables_container(self) -> TablesContainer:
673
+ """Return the tables container."""
674
+ if self._tables_container is None:
675
+ self._tables_container = _default_table_container(self._group_handler)
676
+ if self._tables_container is None:
677
+ raise NgioValidationError("No tables found in the image.")
678
+ return self._tables_container
679
+
680
+ @property
681
+ def list_tables(self) -> list[str]:
682
+ """List all tables in the image."""
683
+ return self.tables_container.list()
684
+
685
+ def list_roi_tables(self) -> list[str]:
686
+ """List all ROI tables in the image."""
687
+ return self.tables_container.list_roi_tables()
688
+
689
+ @overload
690
+ def get_table(self, name: str) -> Table: ...
691
+
692
+ @overload
693
+ def get_table(self, name: str, check_type: None) -> Table: ...
694
+
695
+ @overload
696
+ def get_table(self, name: str, check_type: Literal["roi_table"]) -> RoiTable: ...
697
+
698
+ @overload
699
+ def get_table(
700
+ self, name: str, check_type: Literal["masking_roi_table"]
701
+ ) -> MaskingRoiTable: ...
702
+
703
+ @overload
704
+ def get_table(
705
+ self, name: str, check_type: Literal["feature_table"]
706
+ ) -> FeatureTable: ...
707
+
708
+ @overload
709
+ def get_table(
710
+ self, name: str, check_type: Literal["generic_roi_table"]
711
+ ) -> GenericRoiTable: ...
712
+
713
+ def get_table(self, name: str, check_type: TypedTable | None = None) -> Table:
714
+ """Get a table from the image.
715
+
716
+ Args:
717
+ name (str): The name of the table.
718
+ check_type (TypedTable | None): The type of the table. If None, the
719
+ type is not checked. If a type is provided, the table must be of that
720
+ type.
721
+ """
722
+ if check_type is None:
723
+ table = self.tables_container.get(name, strict=False)
724
+ return table
725
+
726
+ table = self.tables_container.get(name, strict=True)
727
+ match check_type:
728
+ case "roi_table":
729
+ if not isinstance(table, RoiTable):
730
+ raise NgioValueError(
731
+ f"Table '{name}' is not a ROI table. Found type: {table.type()}"
732
+ )
733
+ return table
734
+ case "masking_roi_table":
735
+ if not isinstance(table, MaskingRoiTable):
736
+ raise NgioValueError(
737
+ f"Table '{name}' is not a masking ROI table. "
738
+ f"Found type: {table.type()}"
739
+ )
740
+ return table
741
+
742
+ case "generic_roi_table":
743
+ if not isinstance(table, GenericRoiTable):
744
+ raise NgioValueError(
745
+ f"Table '{name}' is not a generic ROI table. "
746
+ f"Found type: {table.type()}"
747
+ )
748
+ return table
749
+
750
+ case "feature_table":
751
+ if not isinstance(table, FeatureTable):
752
+ raise NgioValueError(
753
+ f"Table '{name}' is not a feature table. "
754
+ f"Found type: {table.type()}"
755
+ )
756
+ return table
757
+ case _:
758
+ raise NgioValueError(f"Unknown check_type: {check_type}")
759
+
760
+ def add_table(
761
+ self,
762
+ name: str,
763
+ table: Table,
764
+ backend: str | None = None,
765
+ overwrite: bool = False,
766
+ ) -> None:
767
+ """Add a table to the image."""
768
+ self.tables_container.add(
769
+ name=name, table=table, backend=backend, overwrite=overwrite
770
+ )
771
+
641
772
 
642
773
  def open_ome_zarr_plate(
643
774
  store: StoreOrGroup,
@@ -162,6 +162,7 @@ class AbstractImage(Generic[_image_handler]):
162
162
 
163
163
  def has_axis(self, axis: str) -> bool:
164
164
  """Return True if the image has the given axis."""
165
+ self.axes_mapper.get_index("x")
165
166
  return self.dimensions.has_axis(axis)
166
167
 
167
168
  def get_array(
ngio/images/image.py CHANGED
@@ -154,6 +154,8 @@ class ImagesContainer:
154
154
  self,
155
155
  labels: Collection[str] | int | None = None,
156
156
  wavelength_id: Collection[str] | None = None,
157
+ start: Collection[float] | None = None,
158
+ end: Collection[float] | None = None,
157
159
  percentiles: tuple[float, float] | None = None,
158
160
  colors: Collection[str] | None = None,
159
161
  active: Collection[bool] | None = None,
@@ -166,6 +168,10 @@ class ImagesContainer:
166
168
  If an integer is provided, the channels will be named "channel_i".
167
169
  wavelength_id(Collection[str] | None): The wavelength ID of the channel.
168
170
  If None, the wavelength ID will be the same as the channel name.
171
+ start(Collection[float] | None): The start value for each channel.
172
+ If None, the start value will be computed from the image.
173
+ end(Collection[float] | None): The end value for each channel.
174
+ If None, the end value will be computed from the image.
169
175
  percentiles(tuple[float, float] | None): The start and end percentiles
170
176
  for each channel. If None, the percentiles will not be computed.
171
177
  colors(Collection[str, NgioColors] | None): The list of colors for the
@@ -177,12 +183,40 @@ class ImagesContainer:
177
183
  low_res_dataset = self.meta.get_lowest_resolution_dataset()
178
184
  ref_image = self.get(path=low_res_dataset.path)
179
185
 
186
+ if start is not None and end is None:
187
+ raise NgioValidationError(
188
+ "If start is provided, end must be provided as well."
189
+ )
190
+ if end is not None and start is None:
191
+ raise NgioValidationError(
192
+ "If end is provided, start must be provided as well."
193
+ )
194
+
195
+ if start is not None and percentiles is not None:
196
+ raise NgioValidationError(
197
+ "If start and end are provided, percentiles must be None."
198
+ )
199
+
180
200
  if percentiles is not None:
181
201
  start, end = compute_image_percentile(
182
202
  ref_image,
183
203
  start_percentile=percentiles[0],
184
204
  end_percentile=percentiles[1],
185
205
  )
206
+ elif start is not None and end is not None:
207
+ if len(start) != len(end):
208
+ raise NgioValidationError(
209
+ "The start and end lists must have the same length."
210
+ )
211
+ if len(start) != self.num_channels:
212
+ raise NgioValidationError(
213
+ "The start and end lists must have the same length as "
214
+ "the number of channels."
215
+ )
216
+
217
+ start = list(start)
218
+ end = list(end)
219
+
186
220
  else:
187
221
  start, end = None, None
188
222
 
@@ -462,11 +496,17 @@ def derive_image_container(
462
496
  active = [
463
497
  c.channel_visualisation.active for c in ref_image._channels_meta.channels
464
498
  ]
499
+ start = [
500
+ c.channel_visualisation.start for c in ref_image._channels_meta.channels
501
+ ]
502
+ end = [c.channel_visualisation.end for c in ref_image._channels_meta.channels]
465
503
  else:
466
504
  _labels = None
467
505
  wavelength_id = None
468
506
  colors = None
469
507
  active = None
508
+ start = None
509
+ end = None
470
510
 
471
511
  if labels is not None:
472
512
  if len(labels) != image_container.num_channels:
@@ -481,5 +521,7 @@ def derive_image_container(
481
521
  percentiles=None,
482
522
  colors=colors,
483
523
  active=active,
524
+ start=start,
525
+ end=end,
484
526
  )
485
527
  return image_container
ngio/images/label.py CHANGED
@@ -133,6 +133,12 @@ class LabelsContainer:
133
133
  closest pixel size level will be returned.
134
134
 
135
135
  """
136
+ if name not in self.list():
137
+ raise NgioValueError(
138
+ f"Label '{name}' not found in the Labels group. "
139
+ f"Available labels: {self.list()}"
140
+ )
141
+
136
142
  group_handler = self._group_handler.derive_handler(name)
137
143
  label_meta_handler = find_label_meta_handler(group_handler)
138
144
  path = label_meta_handler.meta.get_dataset(
@@ -143,7 +149,7 @@ class LabelsContainer:
143
149
  def derive(
144
150
  self,
145
151
  name: str,
146
- ref_image: Image,
152
+ ref_image: Image | Label,
147
153
  shape: Collection[int] | None = None,
148
154
  pixel_size: PixelSize | None = None,
149
155
  axes_names: Collection[str] | None = None,
@@ -157,7 +163,8 @@ class LabelsContainer:
157
163
 
158
164
  Args:
159
165
  store (StoreOrGroup): The Zarr store or group to create the image in.
160
- ref_image (Image): The reference image.
166
+ ref_image (Image | Label): A reference image that will be used to create
167
+ the new image.
161
168
  name (str): The name of the new image.
162
169
  shape (Collection[int] | None): The shape of the new image.
163
170
  pixel_size (PixelSize | None): The pixel size of the new image.
@@ -174,13 +181,13 @@ class LabelsContainer:
174
181
  existing_labels = self.list()
175
182
  if name in existing_labels and not overwrite:
176
183
  raise NgioValueError(
177
- f"Table '{name}' already exists in the group. "
184
+ f"Label '{name}' already exists in the group. "
178
185
  "Use overwrite=True to replace it."
179
186
  )
180
187
 
181
188
  label_group = self._group_handler.get_group(name, create_mode=True)
182
189
 
183
- _derive_label(
190
+ derive_label(
184
191
  store=label_group,
185
192
  ref_image=ref_image,
186
193
  name=name,
@@ -199,9 +206,9 @@ class LabelsContainer:
199
206
  return self.get(name)
200
207
 
201
208
 
202
- def _derive_label(
209
+ def derive_label(
203
210
  store: StoreOrGroup,
204
- ref_image: Image,
211
+ ref_image: Image | Label,
205
212
  name: str,
206
213
  shape: Collection[int] | None = None,
207
214
  pixel_size: PixelSize | None = None,
@@ -214,7 +221,8 @@ def _derive_label(
214
221
 
215
222
  Args:
216
223
  store (StoreOrGroup): The Zarr store or group to create the image in.
217
- ref_image (Image): The reference image.
224
+ ref_image (Image | Label): A reference image that will be used to
225
+ create the new image.
218
226
  name (str): The name of the new image.
219
227
  shape (Collection[int] | None): The shape of the new image.
220
228
  pixel_size (PixelSize | None): The pixel size of the new image.
@@ -353,6 +353,9 @@ class OmeZarrContainer:
353
353
  """List all ROI tables in the image."""
354
354
  return self.tables_container.list_roi_tables()
355
355
 
356
+ @overload
357
+ def get_table(self, name: str) -> Table: ...
358
+
356
359
  @overload
357
360
  def get_table(self, name: str, check_type: None) -> Table: ...
358
361
 
@@ -375,8 +378,19 @@ class OmeZarrContainer:
375
378
  ) -> GenericRoiTable: ...
376
379
 
377
380
  def get_table(self, name: str, check_type: TypedTable | None = None) -> Table:
378
- """Get a table from the image."""
379
- table = self.tables_container.get(name)
381
+ """Get a table from the image.
382
+
383
+ Args:
384
+ name (str): The name of the table.
385
+ check_type (TypedTable | None): The type of the table. If None, the
386
+ type is not checked. If a type is provided, the table must be of that
387
+ type.
388
+ """
389
+ if check_type is None:
390
+ table = self.tables_container.get(name, strict=False)
391
+ return table
392
+
393
+ table = self.tables_container.get(name, strict=True)
380
394
  match check_type:
381
395
  case "roi_table":
382
396
  if not isinstance(table, RoiTable):
@@ -407,8 +421,6 @@ class OmeZarrContainer:
407
421
  f"Found type: {table.type()}"
408
422
  )
409
423
  return table
410
- case None:
411
- return table
412
424
  case _:
413
425
  raise NgioValueError(f"Unknown check_type: {check_type}")
414
426
 
@@ -502,7 +514,7 @@ class OmeZarrContainer:
502
514
  def derive_label(
503
515
  self,
504
516
  name: str,
505
- ref_image: Image | None = None,
517
+ ref_image: Image | Label | None = None,
506
518
  shape: Collection[int] | None = None,
507
519
  pixel_size: PixelSize | None = None,
508
520
  axes_names: Collection[str] | None = None,
@@ -515,9 +527,9 @@ class OmeZarrContainer:
515
527
  And add the label to the /labels group.
516
528
 
517
529
  Args:
518
- store (StoreOrGroup): The Zarr store or group to create the image in.
519
- ref_image (Image): The reference image.
520
530
  name (str): The name of the new image.
531
+ ref_image (Image | Label | None): A reference image that will be used
532
+ to create the new image.
521
533
  shape (Collection[int] | None): The shape of the new image.
522
534
  pixel_size (PixelSize | None): The pixel size of the new image.
523
535
  axes_names (Collection[str] | None): The axes names of the new image.
@@ -605,7 +617,6 @@ def create_empty_ome_zarr(
605
617
  dtype: str = "uint16",
606
618
  channel_labels: list[str] | None = None,
607
619
  channel_wavelengths: list[str] | None = None,
608
- percentiles: tuple[float, float] | None = None,
609
620
  channel_colors: Collection[str] | None = None,
610
621
  channel_active: Collection[bool] | None = None,
611
622
  overwrite: bool = False,
@@ -640,8 +651,6 @@ def create_empty_ome_zarr(
640
651
  Defaults to None.
641
652
  channel_wavelengths (list[str] | None, optional): The wavelengths of the
642
653
  channels. Defaults to None.
643
- percentiles (tuple[float, float] | None, optional): The percentiles of the
644
- channels. Defaults to None.
645
654
  channel_colors (Collection[str] | None, optional): The colors of the channels.
646
655
  Defaults to None.
647
656
  channel_active (Collection[bool] | None, optional): Whether the channels are
@@ -674,7 +683,7 @@ def create_empty_ome_zarr(
674
683
  ome_zarr.set_channel_meta(
675
684
  labels=channel_labels,
676
685
  wavelength_id=channel_wavelengths,
677
- percentiles=percentiles,
686
+ percentiles=None,
678
687
  colors=channel_colors,
679
688
  active=channel_active,
680
689
  )
@@ -2,9 +2,8 @@ from collections.abc import Iterable
2
2
  from typing import Protocol
3
3
 
4
4
  import pandas as pd
5
- import pandas.api.types as ptypes
6
5
 
7
- from ngio.utils import NgioTableValidationError, NgioValueError
6
+ from ngio.utils import NgioTableValidationError
8
7
 
9
8
 
10
9
  class TableValidator(Protocol):
@@ -56,87 +55,6 @@ def validate_table(
56
55
  # Common table validators
57
56
  #
58
57
  ####################################################################################################
59
- def validate_index_key(
60
- dataframe: pd.DataFrame, index_key: str | None, overwrite: bool = False
61
- ) -> pd.DataFrame:
62
- """Correctly set the index of the DataFrame.
63
-
64
- This function checks if the index_key is present in the DataFrame.
65
- If not it tries to set sensible defaults.
66
-
67
- In order:
68
- - If index_key is None, nothing can be done.
69
- - If index_key is already the index of the DataFrame, nothing is done.
70
- - If index_key is in the columns, we set the index to that column.
71
- - If current index is None, we set the index to the index_key.
72
- - If current index is not None and overwrite is True,
73
- we set the index to the index_key.
74
-
75
- """
76
- if index_key is None:
77
- # Nothing to do
78
- return dataframe
79
-
80
- if dataframe.index.name == index_key:
81
- # Index is already set to index_key correctly
82
- return dataframe
83
-
84
- if index_key in dataframe.columns:
85
- dataframe = dataframe.set_index(index_key)
86
- return dataframe
87
-
88
- if dataframe.index.name is None:
89
- dataframe.index.name = index_key
90
- return dataframe
91
-
92
- elif overwrite:
93
- dataframe.index.name = index_key
94
- return dataframe
95
- else:
96
- raise NgioTableValidationError(
97
- f"Index key {index_key} not found in DataFrame. "
98
- f"Current index is {dataframe.index.name}. If you want to overwrite the "
99
- "index set overwrite=True."
100
- )
101
-
102
-
103
- def validate_index_dtype(dataframe: pd.DataFrame, index_type: str) -> pd.DataFrame:
104
- """Check if the index of the DataFrame has the correct dtype."""
105
- match index_type:
106
- case "str":
107
- if ptypes.is_integer_dtype(dataframe.index):
108
- # Convert the int index to string is generally safe
109
- dataframe = dataframe.set_index(dataframe.index.astype(str))
110
-
111
- if not ptypes.is_string_dtype(dataframe.index):
112
- raise NgioTableValidationError(
113
- f"Table index must be of string type, got {dataframe.index.dtype}"
114
- )
115
-
116
- case "int":
117
- if ptypes.is_string_dtype(dataframe.index):
118
- # Try to convert the string index to int
119
- try:
120
- dataframe = dataframe.set_index(dataframe.index.astype(int))
121
- except ValueError as e:
122
- if "invalid literal for int() with base 10" in str(e):
123
- raise NgioTableValidationError(
124
- "Table index must be of integer type, got str."
125
- f" We tried implicit conversion and failed: {e}"
126
- ) from None
127
- else:
128
- raise e from e
129
-
130
- if not ptypes.is_integer_dtype(dataframe.index):
131
- raise NgioTableValidationError(
132
- f"Table index must be of integer type, got {dataframe.index.dtype}"
133
- )
134
- case _:
135
- raise NgioValueError(f"index_type {index_type} not recognized")
136
-
137
- return dataframe
138
-
139
-
140
58
  def validate_columns(
141
59
  table_df: pd.DataFrame,
142
60
  required_columns: list[str],
@@ -1,8 +1,34 @@
1
1
  """Ngio Tables backend implementations."""
2
2
 
3
+ from ngio.tables.backends._abstract_backend import AbstractTableBackend, BackendMeta
3
4
  from ngio.tables.backends._table_backends import (
4
5
  ImplementedTableBackends,
5
6
  TableBackendProtocol,
6
7
  )
8
+ from ngio.tables.backends._utils import (
9
+ convert_anndata_to_pandas,
10
+ convert_anndata_to_polars,
11
+ convert_pandas_to_anndata,
12
+ convert_pandas_to_polars,
13
+ convert_polars_to_anndata,
14
+ convert_polars_to_pandas,
15
+ normalize_anndata,
16
+ normalize_pandas_df,
17
+ normalize_polars_lf,
18
+ )
7
19
 
8
- __all__ = ["ImplementedTableBackends", "TableBackendProtocol"]
20
+ __all__ = [
21
+ "AbstractTableBackend",
22
+ "BackendMeta",
23
+ "ImplementedTableBackends",
24
+ "TableBackendProtocol",
25
+ "convert_anndata_to_pandas",
26
+ "convert_anndata_to_polars",
27
+ "convert_pandas_to_anndata",
28
+ "convert_pandas_to_polars",
29
+ "convert_polars_to_anndata",
30
+ "convert_polars_to_pandas",
31
+ "normalize_anndata",
32
+ "normalize_pandas_df",
33
+ "normalize_polars_lf",
34
+ ]