napari-mlarray 0.0.2__py3-none-any.whl → 0.0.3__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.
napari_mlarray/_reader.py CHANGED
@@ -9,68 +9,26 @@ from mlarray import MLArray
9
9
  from pathlib import Path
10
10
  import numpy as np
11
11
 
12
+ # Ensure napari-bbox registers its custom layer type.
13
+ import napari_bbox # noqa: F401
14
+
12
15
 
13
16
  def napari_get_reader(path):
14
- """A basic implementation of a Reader contribution.
15
-
16
- Parameters
17
- ----------
18
- path : str or list of str
19
- Path to file, or list of paths.
20
-
21
- Returns
22
- -------
23
- function or None
24
- If the path is a recognized format, return a function that accepts the
25
- same path or list of paths, and returns a list of layer data tuples.
26
- """
17
+ """A basic implementation of a Reader contribution."""
27
18
  if isinstance(path, list):
28
- # reader plugins may be handed single path, or a list of paths.
29
- # if it is a list, it is assumed to be an image stack...
30
- # so we are only going to look at the first file.
31
19
  path = path[0]
32
20
 
33
- # the get_reader function should make as many checks as possible
34
- # (without loading the full file) to determine if it can read
35
- # the path. Here, we check the dtype of the array by loading
36
- # it with memmap, so that we don't actually load the full array into memory.
37
- # We pretend that this reader can only read integer arrays.
38
21
  try:
39
22
  if not str(path).endswith(".mla"):
40
23
  return None
41
- # napari_get_reader should never raise an exception, because napari
42
- # raises its own specific errors depending on what plugins are
43
- # available for the given path, so we catch
44
- # the OSError that np.load might raise if the file is malformed
45
24
  except OSError:
46
25
  return None
47
26
 
48
- # otherwise we return the *function* that can read ``path``.
49
27
  return reader_function
50
28
 
51
29
 
52
30
  def reader_function(path):
53
- """Take a path or list of paths and return a list of LayerData tuples.
54
-
55
- Readers are expected to return data as a list of tuples, where each tuple
56
- is (data, [add_kwargs, [layer_type]]), "add_kwargs" and "layer_type" are
57
- both optional.
58
-
59
- Parameters
60
- ----------
61
- path : str or list of str
62
- Path to file, or list of paths.
63
-
64
- Returns
65
- -------
66
- layer_data : list of tuples
67
- A list of LayerData tuples where each tuple in the list contains
68
- (data, metadata, layer_type), where data is a numpy array, metadata is
69
- a dict of keyword arguments for the corresponding viewer.add_* method
70
- in napari, and layer_type is a lower-case string naming the type of
71
- layer. Both "meta", and "layer_type" are optional. napari will
72
- default to layer_type=="image" if not provided
73
- """
31
+ """Take a path or list of paths and return a list of LayerData tuples."""
74
32
  paths = [path] if isinstance(path, str) else path
75
33
  layer_data = []
76
34
  for path in paths:
@@ -82,30 +40,58 @@ def reader_function(path):
82
40
  layer_type = "labels" if mlarray.meta.is_seg.is_seg == True else "image"
83
41
  layer_data.append((data, metadata, layer_type))
84
42
  if mlarray.meta.bbox.bboxes is not None:
85
- data = bboxes_minmax_to_napari_rectangles_2d(mlarray.meta.bbox.bboxes)
86
- edge_color = _napari_bbox_edge_colors(
87
- data,
88
- labels=getattr(mlarray.meta.bbox, "labels", None),
89
- )
90
- text = _napari_bbox_score_text(
91
- scores=getattr(mlarray.meta.bbox, "scores", None),
92
- labels=getattr(mlarray.meta.bbox, "labels", None),
93
- count=len(data),
94
- edge_color=edge_color,
95
- rectangles=data,
96
- )
97
- metadata = {
98
- "name": f"{name} (BBoxes)",
99
- "shape_type": "rectangle",
100
- "affine": mlarray.affine,
101
- "metadata": mlarray.meta.to_mapping(),
102
- "face_color": "transparent",
103
- "edge_color": edge_color,
104
- }
105
- if text is not None:
106
- metadata["text"] = text
107
- layer_type = "shapes"
108
- layer_data.append((data, metadata, layer_type))
43
+ bboxes = np.asarray(mlarray.meta.bbox.bboxes)
44
+
45
+ # MLArray bboxes are always (N, D, 2)
46
+ if bboxes.ndim != 3 or bboxes.shape[2] != 2:
47
+ raise ValueError(f"Unsupported bbox shape: {bboxes.shape}")
48
+
49
+ dims = bboxes.shape[1]
50
+
51
+ # 2D -> keep shapes rectangles (original behavior)
52
+ if dims == 2:
53
+ data = bboxes_minmax_to_napari_rectangles_2d(bboxes)
54
+ edge_color = _napari_bbox_edge_colors(
55
+ data,
56
+ labels=getattr(mlarray.meta.bbox, "labels", None),
57
+ )
58
+ text = _napari_bbox_score_text(
59
+ scores=getattr(mlarray.meta.bbox, "scores", None),
60
+ labels=getattr(mlarray.meta.bbox, "labels", None),
61
+ count=len(data),
62
+ edge_color=edge_color,
63
+ rectangles=data,
64
+ )
65
+ metadata = {
66
+ "name": f"{name} (BBoxes)",
67
+ "shape_type": "rectangle",
68
+ "affine": mlarray.affine,
69
+ "metadata": mlarray.meta.to_mapping(),
70
+ "face_color": "transparent",
71
+ "edge_color": edge_color,
72
+ }
73
+ if text is not None:
74
+ metadata["text"] = text
75
+ layer_type = "shapes"
76
+ layer_data.append((data, metadata, layer_type))
77
+
78
+ # 3D+ -> napari-bbox layer
79
+ elif dims >= 3:
80
+ data = bboxes_minmax_to_napari_bboxes_nd(bboxes)
81
+ edge_color = _napari_bbox_edge_colors_count(
82
+ count=len(data),
83
+ labels=getattr(mlarray.meta.bbox, "labels", None),
84
+ )
85
+ metadata = {
86
+ "name": f"{name} (BBoxes)",
87
+ "affine": mlarray.affine,
88
+ "metadata": mlarray.meta.to_mapping(),
89
+ "face_color": "transparent",
90
+ "edge_color": edge_color,
91
+ # "edge_width": 2,
92
+ }
93
+ layer_type = "boundingboxlayer"
94
+ layer_data.append((data, metadata, layer_type))
109
95
  return layer_data
110
96
 
111
97
 
@@ -115,34 +101,14 @@ def bboxes_minmax_to_napari_rectangles_2d(
115
101
  dtype=np.float32,
116
102
  validate: bool = True,
117
103
  ) -> np.ndarray:
118
- """
119
- Convert 2D axis-aligned bounding boxes from min/max format to napari Shapes rectangles.
120
-
121
- Accepted input formats (both mean the same thing):
122
- 1) (N, 2, 2): [[min_dim0, max_dim0], [min_dim1, max_dim1]]
123
- Example (dim order is whatever you use, e.g. (y, x)):
124
- [[[ymin, ymax], [xmin, xmax]], ...]
125
-
126
- 2) (N, 4): [min_dim0, min_dim1, max_dim0, max_dim1]
127
- Example:
128
- [[ymin, xmin, ymax, xmax], ...]
129
-
130
- Output format (napari Shapes rectangle vertices):
131
- (N, 4, 2) with vertices in non-twisting cyclic order:
132
- (min0, min1) -> (min0, max1) -> (max0, max1) -> (max0, min1)
133
-
134
- Raises:
135
- ValueError if bboxes are not 2D (i.e., D != 2) or shapes are invalid.
136
- """
104
+ """Convert 2D axis-aligned bounding boxes from min/max format to napari Shapes rectangles."""
137
105
  arr = np.asarray(bboxes)
138
106
 
139
- # Normalize input to shape (N, 2, 2)
140
107
  if arr.ndim == 2 and arr.shape[1] == 4:
141
- # (N, 4) -> (N, 2, 2)
142
108
  arr = np.stack(
143
109
  [
144
- arr[:, [0, 2]], # dim0: [min0, max0]
145
- arr[:, [1, 3]], # dim1: [min1, max1]
110
+ arr[:, [0, 2]],
111
+ arr[:, [1, 3]],
146
112
  ],
147
113
  axis=1,
148
114
  )
@@ -153,13 +119,20 @@ def bboxes_minmax_to_napari_rectangles_2d(
153
119
  f"Expected bboxes of shape (N, 2, 2) or (N, 4). Got {arr.shape}."
154
120
  )
155
121
 
156
- N, D, two = arr.shape
122
+ # MLArray uses (N, D, 2) -> convert to (N, 2, 2)
123
+ if arr.shape == (arr.shape[0], 2, 2):
124
+ arr2 = arr
125
+ else:
126
+ arr2 = np.transpose(arr, (0, 2, 1))
127
+
128
+ N, D, two = arr2.shape
157
129
  if D != 2 or two != 2:
158
- # Defensive; should never hit because of checks above.
159
130
  raise ValueError(f"Only 2D bboxes are supported. Got (N, {D}, {two}).")
160
131
 
161
- mins = arr[:, :, 0]
162
- maxs = arr[:, :, 1]
132
+ mins = arr2[:, 0, :]
133
+ maxs = arr2[:, 1, :]
134
+ # Ensure proper min/max ordering even if input is flipped
135
+ mins, maxs = np.minimum(mins, maxs), np.maximum(mins, maxs)
163
136
 
164
137
  if validate and np.any(maxs < mins):
165
138
  bad = np.argwhere(maxs < mins)
@@ -171,7 +144,6 @@ def bboxes_minmax_to_napari_rectangles_2d(
171
144
  min0, min1 = mins[:, 0], mins[:, 1]
172
145
  max0, max1 = maxs[:, 0], maxs[:, 1]
173
146
 
174
- # Cyclic order (no twisting):
175
147
  rects = np.stack(
176
148
  [
177
149
  np.stack([min0, min1], axis=1),
@@ -185,6 +157,40 @@ def bboxes_minmax_to_napari_rectangles_2d(
185
157
  return rects
186
158
 
187
159
 
160
+ def bboxes_minmax_to_napari_bboxes_nd(
161
+ bboxes,
162
+ *,
163
+ dtype=np.float32,
164
+ validate: bool = True,
165
+ ):
166
+ """
167
+ Convert N-D axis-aligned bboxes from min/max to napari-bbox format.
168
+ Input (MLArray): (N, D, 2) where [:, :, 0] are mins and [:, :, 1] are maxs.
169
+ Returns:
170
+ - list of (2, D) arrays, one per bbox.
171
+ """
172
+ arr = np.asarray(bboxes)
173
+
174
+ if arr.ndim != 3 or arr.shape[2] != 2:
175
+ raise ValueError(
176
+ f"Expected bboxes of shape (N, D, 2). Got {arr.shape}."
177
+ )
178
+
179
+ mins = arr[:, :, 0]
180
+ maxs = arr[:, :, 1]
181
+ # Ensure proper min/max ordering even if input is flipped
182
+ mins, maxs = np.minimum(mins, maxs), np.maximum(mins, maxs)
183
+ if validate and np.any(maxs < mins):
184
+ bad = np.argwhere(maxs < mins)
185
+ raise ValueError(
186
+ "Found bbox with max < min at indices (bbox_index, dim): "
187
+ f"{bad[:10].tolist()}" + (" ..." if len(bad) > 10 else "")
188
+ )
189
+
190
+ arr2 = np.stack([mins, maxs], axis=1).astype(dtype, copy=False)
191
+ return [arr2[i] for i in range(arr2.shape[0])]
192
+
193
+
188
194
  def _napari_bbox_edge_colors(rectangles, labels):
189
195
  """Return RGBA edge colors for each bbox."""
190
196
  count = len(rectangles)
@@ -203,6 +209,23 @@ def _napari_bbox_edge_colors(rectangles, labels):
203
209
  return colors
204
210
 
205
211
 
212
+ def _napari_bbox_edge_colors_count(count, labels=None):
213
+ """Return RGBA edge colors for each bbox (count-based)."""
214
+ if count == 0:
215
+ return np.empty((0, 4), dtype=np.float32)
216
+
217
+ if labels is not None and len(labels) == count:
218
+ unique_labels = list(dict.fromkeys(labels))
219
+ label_to_color = {
220
+ label: _palette_rgba(idx) for idx, label in enumerate(unique_labels)
221
+ }
222
+ colors = np.array([label_to_color[label] for label in labels], dtype=np.float32)
223
+ else:
224
+ colors = np.array([_palette_rgba(idx) for idx in range(count)], dtype=np.float32)
225
+
226
+ return colors
227
+
228
+
206
229
  def _napari_bbox_score_text(scores, labels, count, edge_color, rectangles):
207
230
  """Return napari Shapes text metadata if scores are provided."""
208
231
  have_scores = scores is not None and len(scores) == count
@@ -210,7 +233,6 @@ def _napari_bbox_score_text(scores, labels, count, edge_color, rectangles):
210
233
  if not have_scores and not have_labels:
211
234
  return None
212
235
 
213
- # Place text at the top-left corner of each rectangle.
214
236
  top_left = rectangles[:, 0, :]
215
237
  top_left = np.maximum(top_left - np.array([4.0, 0.0], dtype=top_left.dtype), 0)
216
238
 
@@ -221,7 +243,6 @@ def _napari_bbox_score_text(scores, labels, count, edge_color, rectangles):
221
243
  parts.append(f"Label: {labels[idx]}")
222
244
  if have_scores:
223
245
  parts.append(f"Score: {scores[idx]:.3f}")
224
- # Add a trailing empty line to create spacing below the score.
225
246
  parts.append("\n")
226
247
  strings.append("\n".join(parts))
227
248
 
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.0.2'
32
- __version_tuple__ = version_tuple = (0, 0, 2)
31
+ __version__ = version = '0.0.3'
32
+ __version_tuple__ = version_tuple = (0, 0, 3)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: napari-mlarray
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: A reader/writer Napari plugin for MLArray images.
5
5
  Author: Karol-G
6
6
  Author-email: karol.gotkowski@dkfz.de
@@ -45,6 +45,7 @@ Description-Content-Type: text/markdown
45
45
  License-File: LICENSE
46
46
  Requires-Dist: mlarray
47
47
  Requires-Dist: numpy
48
+ Requires-Dist: napari-bbox-fix
48
49
  Provides-Extra: all
49
50
  Requires-Dist: napari[all]; extra == "all"
50
51
  Dynamic: license-file
@@ -0,0 +1,12 @@
1
+ napari_mlarray/__init__.py,sha256=c2b0_m5sORovlHyyozJS1l00lXjFM7ULIkrHqojq5N4,249
2
+ napari_mlarray/_reader.py,sha256=VwJ0GZqsH7prQZrfwlMiYAsDFffsj7TpuzgTaX9sA7A,9746
3
+ napari_mlarray/_version.py,sha256=pBZsQt6tlL02W-ri--X_4JCubpAK7jjCSnOmUp_isjc,704
4
+ napari_mlarray/_widget.py,sha256=K6MYwgFiQg7-dORp-kC_VivrfppiUwMmevbPMhwMT9c,4810
5
+ napari_mlarray/_writer.py,sha256=EG-013mGR14L5V7tygzoZg2EPyIwnZAP8zG8flg58hU,1209
6
+ napari_mlarray/napari.yaml,sha256=HILzIxDmFpRPgCmM7uVzzZ86Ayp2ecWiSXHTCC1qjx4,796
7
+ napari_mlarray-0.0.3.dist-info/licenses/LICENSE,sha256=LKlNG6Bx5z0YnzAp9GjCxCR0aypSPO_JcUHmuVGtwds,1162
8
+ napari_mlarray-0.0.3.dist-info/METADATA,sha256=cnSU5VuWk2nOqERetsv8KKFwrV9BDmUOCgy8D_BUhh4,5593
9
+ napari_mlarray-0.0.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
10
+ napari_mlarray-0.0.3.dist-info/entry_points.txt,sha256=ibu_ymiLzJPNpL0x0Fdendi5fQiq_baxx224E5pAW0c,62
11
+ napari_mlarray-0.0.3.dist-info/top_level.txt,sha256=ZfJPiLTSmZ9eakEU1J6znzWi6dS_OTGAXQSI08HOgQg,15
12
+ napari_mlarray-0.0.3.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- napari_mlarray/__init__.py,sha256=c2b0_m5sORovlHyyozJS1l00lXjFM7ULIkrHqojq5N4,249
2
- napari_mlarray/_reader.py,sha256=IvFdBZnA3AaTt_wqh9B_q_7tXVV2N3nr722thUCAncY,9373
3
- napari_mlarray/_version.py,sha256=huLsL1iGeXWQKZ8bjwDdIWC7JOkj3wnzBh-HFMZl1PY,704
4
- napari_mlarray/_widget.py,sha256=K6MYwgFiQg7-dORp-kC_VivrfppiUwMmevbPMhwMT9c,4810
5
- napari_mlarray/_writer.py,sha256=EG-013mGR14L5V7tygzoZg2EPyIwnZAP8zG8flg58hU,1209
6
- napari_mlarray/napari.yaml,sha256=HILzIxDmFpRPgCmM7uVzzZ86Ayp2ecWiSXHTCC1qjx4,796
7
- napari_mlarray-0.0.2.dist-info/licenses/LICENSE,sha256=LKlNG6Bx5z0YnzAp9GjCxCR0aypSPO_JcUHmuVGtwds,1162
8
- napari_mlarray-0.0.2.dist-info/METADATA,sha256=LYZJf7Fan_qTRKnxY_KOxAJnBkfbpt8pTYn-_xa58Wk,5562
9
- napari_mlarray-0.0.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
10
- napari_mlarray-0.0.2.dist-info/entry_points.txt,sha256=ibu_ymiLzJPNpL0x0Fdendi5fQiq_baxx224E5pAW0c,62
11
- napari_mlarray-0.0.2.dist-info/top_level.txt,sha256=ZfJPiLTSmZ9eakEU1J6znzWi6dS_OTGAXQSI08HOgQg,15
12
- napari_mlarray-0.0.2.dist-info/RECORD,,