napari-mlarray 0.0.1__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
@@ -6,72 +6,287 @@ implement multiple readers or even other plugin contributions. see:
6
6
  https://napari.org/stable/plugins/building_a_plugin/guides.html#readers
7
7
  """
8
8
  from mlarray import MLArray
9
+ from pathlib import Path
10
+ import numpy as np
11
+
12
+ # Ensure napari-bbox registers its custom layer type.
13
+ import napari_bbox # noqa: F401
9
14
 
10
15
 
11
16
  def napari_get_reader(path):
12
- """A basic implementation of a Reader contribution.
13
-
14
- Parameters
15
- ----------
16
- path : str or list of str
17
- Path to file, or list of paths.
18
-
19
- Returns
20
- -------
21
- function or None
22
- If the path is a recognized format, return a function that accepts the
23
- same path or list of paths, and returns a list of layer data tuples.
24
- """
17
+ """A basic implementation of a Reader contribution."""
25
18
  if isinstance(path, list):
26
- # reader plugins may be handed single path, or a list of paths.
27
- # if it is a list, it is assumed to be an image stack...
28
- # so we are only going to look at the first file.
29
19
  path = path[0]
30
20
 
31
- # the get_reader function should make as many checks as possible
32
- # (without loading the full file) to determine if it can read
33
- # the path. Here, we check the dtype of the array by loading
34
- # it with memmap, so that we don't actually load the full array into memory.
35
- # We pretend that this reader can only read integer arrays.
36
21
  try:
37
22
  if not str(path).endswith(".mla"):
38
23
  return None
39
- # napari_get_reader should never raise an exception, because napari
40
- # raises its own specific errors depending on what plugins are
41
- # available for the given path, so we catch
42
- # the OSError that np.load might raise if the file is malformed
43
24
  except OSError:
44
25
  return None
45
26
 
46
- # otherwise we return the *function* that can read ``path``.
47
27
  return reader_function
48
28
 
49
29
 
50
30
  def reader_function(path):
51
- """Take a path or list of paths and return a list of LayerData tuples.
52
-
53
- Readers are expected to return data as a list of tuples, where each tuple
54
- is (data, [add_kwargs, [layer_type]]), "add_kwargs" and "layer_type" are
55
- both optional.
56
-
57
- Parameters
58
- ----------
59
- path : str or list of str
60
- Path to file, or list of paths.
61
-
62
- Returns
63
- -------
64
- layer_data : list of tuples
65
- A list of LayerData tuples where each tuple in the list contains
66
- (data, metadata, layer_type), where data is a numpy array, metadata is
67
- a dict of keyword arguments for the corresponding viewer.add_* method
68
- in napari, and layer_type is a lower-case string naming the type of
69
- layer. Both "meta", and "layer_type" are optional. napari will
70
- default to layer_type=="image" if not provided
71
- """
72
- # handle both a string and a list of strings
31
+ """Take a path or list of paths and return a list of LayerData tuples."""
73
32
  paths = [path] if isinstance(path, str) else path
74
- # load all files into array
75
- mlarrays = [MLArray.open(_path) for _path in paths]
76
- layer_data = [(mlarray, {"affine": mlarray.affine, "metadata": mlarray.meta.to_dict()}, "labels" if mlarray.meta.is_seg.is_seg == True else "image") for mlarray in mlarrays]
33
+ layer_data = []
34
+ for path in paths:
35
+ name = Path(path).stem
36
+ mlarray = MLArray.open(path)
37
+ if mlarray.meta._has_array.has_array == True:
38
+ data = mlarray
39
+ metadata = {"name": f"{name}", "affine": mlarray.affine, "metadata": mlarray.meta.to_mapping()}
40
+ layer_type = "labels" if mlarray.meta.is_seg.is_seg == True else "image"
41
+ layer_data.append((data, metadata, layer_type))
42
+ if mlarray.meta.bbox.bboxes is not None:
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))
77
95
  return layer_data
96
+
97
+
98
+ def bboxes_minmax_to_napari_rectangles_2d(
99
+ bboxes,
100
+ *,
101
+ dtype=np.float32,
102
+ validate: bool = True,
103
+ ) -> np.ndarray:
104
+ """Convert 2D axis-aligned bounding boxes from min/max format to napari Shapes rectangles."""
105
+ arr = np.asarray(bboxes)
106
+
107
+ if arr.ndim == 2 and arr.shape[1] == 4:
108
+ arr = np.stack(
109
+ [
110
+ arr[:, [0, 2]],
111
+ arr[:, [1, 3]],
112
+ ],
113
+ axis=1,
114
+ )
115
+ elif arr.ndim == 3 and arr.shape[1:] == (2, 2):
116
+ pass
117
+ else:
118
+ raise ValueError(
119
+ f"Expected bboxes of shape (N, 2, 2) or (N, 4). Got {arr.shape}."
120
+ )
121
+
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
129
+ if D != 2 or two != 2:
130
+ raise ValueError(f"Only 2D bboxes are supported. Got (N, {D}, {two}).")
131
+
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)
136
+
137
+ if validate and np.any(maxs < mins):
138
+ bad = np.argwhere(maxs < mins)
139
+ raise ValueError(
140
+ "Found bbox with max < min at indices (bbox_index, dim): "
141
+ f"{bad[:10].tolist()}" + (" ..." if len(bad) > 10 else "")
142
+ )
143
+
144
+ min0, min1 = mins[:, 0], mins[:, 1]
145
+ max0, max1 = maxs[:, 0], maxs[:, 1]
146
+
147
+ rects = np.stack(
148
+ [
149
+ np.stack([min0, min1], axis=1),
150
+ np.stack([min0, max1], axis=1),
151
+ np.stack([max0, max1], axis=1),
152
+ np.stack([max0, min1], axis=1),
153
+ ],
154
+ axis=1,
155
+ ).astype(dtype, copy=False)
156
+
157
+ return rects
158
+
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
+
194
+ def _napari_bbox_edge_colors(rectangles, labels):
195
+ """Return RGBA edge colors for each bbox."""
196
+ count = len(rectangles)
197
+ if count == 0:
198
+ return np.empty((0, 4), dtype=np.float32)
199
+
200
+ if labels is not None and len(labels) == count:
201
+ unique_labels = list(dict.fromkeys(labels))
202
+ label_to_color = {
203
+ label: _palette_rgba(idx) for idx, label in enumerate(unique_labels)
204
+ }
205
+ colors = np.array([label_to_color[label] for label in labels], dtype=np.float32)
206
+ else:
207
+ colors = np.array([_palette_rgba(idx) for idx in range(count)], dtype=np.float32)
208
+
209
+ return colors
210
+
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
+
229
+ def _napari_bbox_score_text(scores, labels, count, edge_color, rectangles):
230
+ """Return napari Shapes text metadata if scores are provided."""
231
+ have_scores = scores is not None and len(scores) == count
232
+ have_labels = labels is not None and len(labels) == count
233
+ if not have_scores and not have_labels:
234
+ return None
235
+
236
+ top_left = rectangles[:, 0, :]
237
+ top_left = np.maximum(top_left - np.array([4.0, 0.0], dtype=top_left.dtype), 0)
238
+
239
+ strings = []
240
+ for idx in range(count):
241
+ parts = []
242
+ if have_labels:
243
+ parts.append(f"Label: {labels[idx]}")
244
+ if have_scores:
245
+ parts.append(f"Score: {scores[idx]:.3f}")
246
+ parts.append("\n")
247
+ strings.append("\n".join(parts))
248
+
249
+ return {
250
+ "string": strings,
251
+ "color": edge_color,
252
+ "size": 12,
253
+ "anchor": "upper_left",
254
+ "position": top_left,
255
+ }
256
+
257
+
258
+ def _palette_rgba(index):
259
+ """Simple, distinct-ish palette; returns RGBA in 0..1."""
260
+ palette = [
261
+ (0.90, 0.10, 0.12, 1.0),
262
+ (0.00, 0.48, 1.00, 1.0),
263
+ (0.20, 0.80, 0.20, 1.0),
264
+ (0.98, 0.60, 0.00, 1.0),
265
+ (0.60, 0.20, 0.80, 1.0),
266
+ (0.10, 0.75, 0.80, 1.0),
267
+ (0.80, 0.80, 0.00, 1.0),
268
+ (0.95, 0.40, 0.60, 1.0),
269
+ (0.90, 0.30, 0.00, 1.0),
270
+ (0.00, 0.70, 0.40, 1.0),
271
+ (0.40, 0.80, 1.00, 1.0),
272
+ (1.00, 0.20, 0.70, 1.0),
273
+ (0.50, 0.90, 0.20, 1.0),
274
+ (0.20, 0.90, 0.70, 1.0),
275
+ (0.70, 0.50, 1.00, 1.0),
276
+ (1.00, 0.50, 0.20, 1.0),
277
+ (0.20, 0.60, 1.00, 1.0),
278
+ (1.00, 0.70, 0.20, 1.0),
279
+ (0.60, 1.00, 0.20, 1.0),
280
+ (0.20, 1.00, 0.40, 1.0),
281
+ (0.20, 1.00, 0.90, 1.0),
282
+ (0.20, 0.90, 1.00, 1.0),
283
+ (0.40, 0.60, 1.00, 1.0),
284
+ (0.80, 0.20, 1.00, 1.0),
285
+ (1.00, 0.20, 0.30, 1.0),
286
+ (1.00, 0.30, 0.50, 1.0),
287
+ (1.00, 0.60, 0.60, 1.0),
288
+ (1.00, 0.90, 0.30, 1.0),
289
+ (0.60, 1.00, 0.60, 1.0),
290
+ (0.60, 0.90, 1.00, 1.0),
291
+ ]
292
+ return palette[index % len(palette)]
@@ -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.1'
32
- __version_tuple__ = version_tuple = (0, 0, 1)
31
+ __version__ = version = '0.0.3'
32
+ __version_tuple__ = version_tuple = (0, 0, 3)
33
33
 
34
34
  __commit_id__ = commit_id = None
napari_mlarray/_writer.py CHANGED
@@ -34,7 +34,7 @@ def write_single_image(path: str, data: Any, meta: dict) -> list[str]:
34
34
  -------
35
35
  [path] : A list containing the string path to the saved file.
36
36
  """
37
- mlarray = MLArray(data, meta=Meta.from_dict(meta["metadata"]))
37
+ mlarray = MLArray(data, meta=Meta.from_mapping(meta["metadata"]))
38
38
  mlarray.save(path)
39
39
 
40
40
  # return path to any file(s) that were successfully written
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: napari-mlarray
3
- Version: 0.0.1
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
@@ -44,6 +44,8 @@ Requires-Python: >=3.10
44
44
  Description-Content-Type: text/markdown
45
45
  License-File: LICENSE
46
46
  Requires-Dist: mlarray
47
+ Requires-Dist: numpy
48
+ Requires-Dist: napari-bbox-fix
47
49
  Provides-Extra: all
48
50
  Requires-Dist: napari[all]; extra == "all"
49
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=TT6-L8t7wlYRA5laoVH-frf-ogBxXlEQWE7y70IUHHs,3045
3
- napari_mlarray/_version.py,sha256=qf6R-J7-UyuABBo8c0HgaquJ8bejVbf07HodXgwAwgQ,704
4
- napari_mlarray/_widget.py,sha256=K6MYwgFiQg7-dORp-kC_VivrfppiUwMmevbPMhwMT9c,4810
5
- napari_mlarray/_writer.py,sha256=tAFCX1-LnmDqeDFpO2H22Xrju58-3D-d0ZrEvdAXMbE,1206
6
- napari_mlarray/napari.yaml,sha256=HILzIxDmFpRPgCmM7uVzzZ86Ayp2ecWiSXHTCC1qjx4,796
7
- napari_mlarray-0.0.1.dist-info/licenses/LICENSE,sha256=LKlNG6Bx5z0YnzAp9GjCxCR0aypSPO_JcUHmuVGtwds,1162
8
- napari_mlarray-0.0.1.dist-info/METADATA,sha256=tHD_AJuOHEEWg3OU6FepCJSMQDbVPHtCGoVa1y6ZBHg,5541
9
- napari_mlarray-0.0.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
10
- napari_mlarray-0.0.1.dist-info/entry_points.txt,sha256=ibu_ymiLzJPNpL0x0Fdendi5fQiq_baxx224E5pAW0c,62
11
- napari_mlarray-0.0.1.dist-info/top_level.txt,sha256=ZfJPiLTSmZ9eakEU1J6znzWi6dS_OTGAXQSI08HOgQg,15
12
- napari_mlarray-0.0.1.dist-info/RECORD,,