napari-tmidas 0.1.1__py3-none-any.whl → 0.1.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_tmidas/__init__.py CHANGED
@@ -3,14 +3,10 @@ try:
3
3
  except ImportError:
4
4
  __version__ = "unknown"
5
5
 
6
+
7
+ from ._label_inspection import label_inspector_widget
6
8
  from ._reader import napari_get_reader
7
9
  from ._sample_data import make_sample_data
8
- from ._widget import (
9
- ExampleQWidget,
10
- ImageThreshold,
11
- threshold_autogenerate_widget,
12
- threshold_magic_widget,
13
- )
14
10
  from ._writer import write_multiple, write_single_image
15
11
 
16
12
  __all__ = (
@@ -18,8 +14,6 @@ __all__ = (
18
14
  "write_single_image",
19
15
  "write_multiple",
20
16
  "make_sample_data",
21
- "ExampleQWidget",
22
- "ImageThreshold",
23
- "threshold_autogenerate_widget",
24
- "threshold_magic_widget",
17
+ "file_selector",
18
+ "label_inspector_widget",
25
19
  )
@@ -0,0 +1,518 @@
1
+ import contextlib
2
+ import os
3
+ import sys
4
+ from typing import Any, Dict, List
5
+
6
+ import napari
7
+ import numpy as np
8
+ import tifffile
9
+ from magicgui import magicgui
10
+ from qtpy.QtCore import Qt
11
+ from qtpy.QtWidgets import (
12
+ QComboBox,
13
+ QDoubleSpinBox,
14
+ QFormLayout,
15
+ QHeaderView,
16
+ QLabel,
17
+ QLineEdit,
18
+ QPushButton,
19
+ QSpinBox,
20
+ QTableWidget,
21
+ QTableWidgetItem,
22
+ QVBoxLayout,
23
+ QWidget,
24
+ )
25
+
26
+ # Import registry and processing functions
27
+ from napari_tmidas._registry import BatchProcessingRegistry
28
+
29
+ sys.path.append("src/napari_tmidas")
30
+ from napari_tmidas.processing_functions import (
31
+ discover_and_load_processing_functions,
32
+ )
33
+
34
+
35
+ class ProcessedFilesTableWidget(QTableWidget):
36
+ """
37
+ Custom table widget with lazy loading and processing capabilities
38
+ """
39
+
40
+ def __init__(self, viewer: napari.Viewer):
41
+ super().__init__()
42
+ self.viewer = viewer
43
+
44
+ # Configure table
45
+ self.setColumnCount(2)
46
+ self.setHorizontalHeaderLabels(["Original Files", "Processed Files"])
47
+ self.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
48
+
49
+ # Track file mappings
50
+ self.file_pairs = {}
51
+
52
+ # Currently loaded images
53
+ self.current_original_image = None
54
+ self.current_processed_image = None
55
+
56
+ def add_initial_files(self, file_list: List[str]):
57
+ """
58
+ Add initial files to the table
59
+ """
60
+ # Clear existing rows
61
+ self.setRowCount(0)
62
+ self.file_pairs.clear()
63
+
64
+ # Add files
65
+ for filepath in file_list:
66
+ row = self.rowCount()
67
+ self.insertRow(row)
68
+
69
+ # Original file item
70
+ original_item = QTableWidgetItem(os.path.basename(filepath))
71
+ original_item.setData(Qt.UserRole, filepath)
72
+ self.setItem(row, 0, original_item)
73
+
74
+ # Initially empty processed file column
75
+ processed_item = QTableWidgetItem("")
76
+ self.setItem(row, 1, processed_item)
77
+
78
+ # Store file pair
79
+ self.file_pairs[filepath] = {
80
+ "original": filepath,
81
+ "processed": None,
82
+ "row": row,
83
+ }
84
+
85
+ def update_processed_files(self, processing_info: dict):
86
+ """
87
+ Update table with processed files
88
+
89
+ processing_info: {
90
+ 'original_file': original filepath,
91
+ 'processed_file': processed filepath
92
+ }
93
+ """
94
+ for item in processing_info:
95
+ original_file = item["original_file"]
96
+ processed_file = item["processed_file"]
97
+
98
+ # Find the corresponding row
99
+ if original_file in self.file_pairs:
100
+ row = self.file_pairs[original_file]["row"]
101
+
102
+ # Update processed file column
103
+ processed_item = QTableWidgetItem(
104
+ os.path.basename(processed_file)
105
+ )
106
+ processed_item.setData(Qt.UserRole, processed_file)
107
+ self.setItem(row, 1, processed_item)
108
+
109
+ # Update file pairs
110
+ self.file_pairs[original_file]["processed"] = processed_file
111
+
112
+ def mousePressEvent(self, event):
113
+ """
114
+ Load image when clicked
115
+ """
116
+ if event.button() == Qt.LeftButton:
117
+ item = self.itemAt(event.pos())
118
+ if item:
119
+ filepath = item.data(Qt.UserRole)
120
+ if filepath:
121
+ # Determine which column was clicked
122
+ column = self.columnAt(event.pos().x())
123
+ if column == 0:
124
+ # Original image clicked
125
+ self._load_original_image(filepath)
126
+ elif column == 1 and filepath:
127
+ # Processed image clicked
128
+ self._load_processed_image(filepath)
129
+
130
+ super().mousePressEvent(event)
131
+
132
+ def _load_original_image(self, filepath: str):
133
+ """
134
+ Load original image into viewer
135
+ """
136
+ # Remove existing original layer if it exists
137
+ if self.current_original_image is not None:
138
+ with contextlib.suppress(KeyError):
139
+ self.viewer.layers.remove(self.current_original_image)
140
+
141
+ # Load new image
142
+ try:
143
+ image = tifffile.imread(filepath)
144
+ self.current_original_image = self.viewer.add_image(
145
+ image, name=f"Original: {os.path.basename(filepath)}"
146
+ )
147
+ except (ValueError, TypeError, OSError, tifffile.TiffFileError) as e:
148
+ print(f"Error loading original image {filepath}: {e}")
149
+ self.viewer.status = f"Error processing {filepath}: {e}"
150
+
151
+ def _load_processed_image(self, filepath: str):
152
+ """
153
+ Load processed image into viewer, distinguishing labels by filename pattern
154
+ """
155
+ # Remove existing processed layer if it exists
156
+ if self.current_processed_image is not None:
157
+ with contextlib.suppress(KeyError):
158
+ self.viewer.layers.remove(self.current_processed_image)
159
+
160
+ # Load new image
161
+ try:
162
+ image = tifffile.imread(filepath)
163
+ filename = os.path.basename(filepath)
164
+
165
+ # Check if filename contains label indicators
166
+ is_label = "labels" in filename or "semantic" in filename
167
+
168
+ # Add the layer using the appropriate method
169
+ if is_label:
170
+ # Ensure it's an appropriate dtype for labels
171
+ if not np.issubdtype(image.dtype, np.integer):
172
+ image = image.astype(np.uint32)
173
+
174
+ self.current_processed_image = self.viewer.add_labels(
175
+ image, name=f"Labels: {filename}"
176
+ )
177
+ else:
178
+ self.current_processed_image = self.viewer.add_image(
179
+ image, name=f"Processed: {filename}"
180
+ )
181
+
182
+ except (ValueError, TypeError) as e:
183
+ print(f"Error loading processed image {filepath}: {e}")
184
+ self.viewer.status = f"Error processing {filepath}: {e}"
185
+
186
+ def _load_image(self, filepath: str):
187
+ """
188
+ Legacy method kept for compatibility
189
+ """
190
+ self._load_original_image(filepath)
191
+
192
+
193
+ class ParameterWidget(QWidget):
194
+ """
195
+ Widget to display and edit processing function parameters
196
+ """
197
+
198
+ def __init__(self, parameters: Dict[str, Dict[str, Any]]):
199
+ super().__init__()
200
+
201
+ self.parameters = parameters
202
+ self.param_widgets = {}
203
+
204
+ layout = QFormLayout()
205
+ self.setLayout(layout)
206
+
207
+ # Create widgets for each parameter
208
+ for param_name, param_info in parameters.items():
209
+ param_type = param_info.get("type")
210
+ default_value = param_info.get("default")
211
+ min_value = param_info.get("min")
212
+ max_value = param_info.get("max")
213
+ description = param_info.get("description", "")
214
+
215
+ # Create appropriate widget based on parameter type
216
+ if param_type is int:
217
+ widget = QSpinBox()
218
+ if min_value is not None:
219
+ widget.setMinimum(min_value)
220
+ if max_value is not None:
221
+ widget.setMaximum(max_value)
222
+ if default_value is not None:
223
+ widget.setValue(default_value)
224
+ elif param_type is float:
225
+ widget = QDoubleSpinBox()
226
+ if min_value is not None:
227
+ widget.setMinimum(min_value)
228
+ if max_value is not None:
229
+ widget.setMaximum(max_value)
230
+ widget.setDecimals(3)
231
+ if default_value is not None:
232
+ widget.setValue(default_value)
233
+ else:
234
+ # Default to text input for other types
235
+ widget = QLineEdit(
236
+ str(default_value) if default_value is not None else ""
237
+ )
238
+
239
+ # Add widget to layout with label
240
+ layout.addRow(f"{param_name} ({description}):", widget)
241
+ self.param_widgets[param_name] = widget
242
+
243
+ def get_parameter_values(self) -> Dict[str, Any]:
244
+ """
245
+ Get current parameter values from widgets
246
+ """
247
+ values = {}
248
+ for param_name, widget in self.param_widgets.items():
249
+ param_type = self.parameters[param_name]["type"]
250
+
251
+ if isinstance(widget, (QSpinBox, QDoubleSpinBox)):
252
+ values[param_name] = widget.value()
253
+ else:
254
+ # For text inputs, try to convert to the appropriate type
255
+ try:
256
+ values[param_name] = param_type(widget.text())
257
+ except (ValueError, TypeError):
258
+ # Fall back to string if conversion fails
259
+ values[param_name] = widget.text()
260
+
261
+ return values
262
+
263
+
264
+ @magicgui(
265
+ call_button="Find and Index Image Files",
266
+ input_folder={"label": "Select Folder"},
267
+ input_suffix={"label": "File Suffix (Example: _labels.tif)", "value": ""},
268
+ )
269
+ def file_selector(
270
+ viewer: napari.Viewer, input_folder: str, input_suffix: str = "_labels.tif"
271
+ ) -> List[str]:
272
+ """
273
+ Find files in a specified input folder with a given suffix and prepare for batch processing.
274
+ """
275
+ # Validate input_folder
276
+ if not os.path.isdir(input_folder):
277
+ viewer.status = f"Invalid input folder: {input_folder}"
278
+ return []
279
+
280
+ # Find matching files
281
+ matching_files = [
282
+ os.path.join(input_folder, f)
283
+ for f in os.listdir(input_folder)
284
+ if f.endswith(input_suffix)
285
+ ]
286
+
287
+ # Create a results widget with batch processing option
288
+ results_widget = FileResultsWidget(
289
+ viewer,
290
+ matching_files,
291
+ input_folder=input_folder,
292
+ input_suffix=input_suffix,
293
+ )
294
+
295
+ # Add the results widget to the Napari viewer
296
+ viewer.window.add_dock_widget(
297
+ results_widget, name="Matching Files", area="right"
298
+ )
299
+
300
+ # Update viewer status
301
+ viewer.status = f"Found {len(matching_files)} files"
302
+
303
+ return matching_files
304
+
305
+
306
+ class FileResultsWidget(QWidget):
307
+ """
308
+ Custom widget to display matching files and enable batch processing
309
+ """
310
+
311
+ def __init__(
312
+ self,
313
+ viewer: napari.Viewer,
314
+ file_list: List[str],
315
+ input_folder: str,
316
+ input_suffix: str,
317
+ ):
318
+ super().__init__()
319
+
320
+ # Store viewer and file list
321
+ self.viewer = viewer
322
+ self.file_list = file_list
323
+ self.input_folder = input_folder
324
+ self.input_suffix = input_suffix
325
+
326
+ # Load all processing functions
327
+ print("Calling discover_and_load_processing_functions")
328
+ discover_and_load_processing_functions()
329
+ # print what is found by discover_and_load_processing_functions
330
+ print("Available processing functions:")
331
+ for func_name in BatchProcessingRegistry.list_functions():
332
+ print(func_name)
333
+
334
+ # Create main layout
335
+ layout = QVBoxLayout()
336
+ self.setLayout(layout)
337
+
338
+ # Create table of files
339
+ self.table = ProcessedFilesTableWidget(viewer)
340
+ self.table.add_initial_files(file_list)
341
+
342
+ # Add table to layout
343
+ layout.addWidget(self.table)
344
+
345
+ # Create processing function selector
346
+ processing_layout = QVBoxLayout()
347
+ processing_label = QLabel("Select Processing Function:")
348
+ processing_layout.addWidget(processing_label)
349
+
350
+ self.processing_selector = QComboBox()
351
+ self.processing_selector.addItems(
352
+ BatchProcessingRegistry.list_functions()
353
+ )
354
+ processing_layout.addWidget(self.processing_selector)
355
+
356
+ # Add description label
357
+ self.function_description = QLabel("")
358
+ processing_layout.addWidget(self.function_description)
359
+
360
+ # Create parameters section (will be populated when function is selected)
361
+ self.parameters_widget = QWidget()
362
+ processing_layout.addWidget(self.parameters_widget)
363
+
364
+ # Connect function selector to update parameters
365
+ self.processing_selector.currentTextChanged.connect(
366
+ self.update_function_info
367
+ )
368
+
369
+ # Optional output folder selector
370
+ output_layout = QVBoxLayout()
371
+ output_label = QLabel("Output Folder (optional):")
372
+ output_layout.addWidget(output_label)
373
+
374
+ self.output_folder = QLineEdit()
375
+ self.output_folder.setPlaceholderText(
376
+ "Leave blank to use source folder"
377
+ )
378
+ output_layout.addWidget(self.output_folder)
379
+
380
+ layout.addLayout(processing_layout)
381
+ layout.addLayout(output_layout)
382
+
383
+ # Add batch processing button
384
+ self.batch_button = QPushButton("Start Batch Processing")
385
+ self.batch_button.clicked.connect(self.start_batch_processing)
386
+ layout.addWidget(self.batch_button)
387
+
388
+ # Initialize parameters for the first function
389
+ if self.processing_selector.count() > 0:
390
+ self.update_function_info(self.processing_selector.currentText())
391
+
392
+ def update_function_info(self, function_name: str):
393
+ """
394
+ Update the function description and parameters when a new function is selected
395
+ """
396
+ function_info = BatchProcessingRegistry.get_function_info(
397
+ function_name
398
+ )
399
+ if not function_info:
400
+ return
401
+
402
+ # Update description
403
+ description = function_info.get("description", "")
404
+ self.function_description.setText(description)
405
+
406
+ # Update parameters
407
+ parameters = function_info.get("parameters", {})
408
+
409
+ # Remove old parameters widget if it exists
410
+ if hasattr(self, "param_widget_instance"):
411
+ self.parameters_widget.layout().removeWidget(
412
+ self.param_widget_instance
413
+ )
414
+ self.param_widget_instance.deleteLater()
415
+
416
+ # Create new layout if needed
417
+ if self.parameters_widget.layout() is None:
418
+ self.parameters_widget.setLayout(QVBoxLayout())
419
+
420
+ # Create and add new parameters widget
421
+ if parameters:
422
+ self.param_widget_instance = ParameterWidget(parameters)
423
+ self.parameters_widget.layout().addWidget(
424
+ self.param_widget_instance
425
+ )
426
+ else:
427
+ # Create empty widget if no parameters
428
+ self.param_widget_instance = QLabel(
429
+ "No parameters for this function"
430
+ )
431
+ self.parameters_widget.layout().addWidget(
432
+ self.param_widget_instance
433
+ )
434
+
435
+ def start_batch_processing(self):
436
+ """
437
+ Initiate batch processing of selected files
438
+ """
439
+ # Get selected processing function
440
+ selected_function_name = self.processing_selector.currentText()
441
+ function_info = BatchProcessingRegistry.get_function_info(
442
+ selected_function_name
443
+ )
444
+
445
+ if not function_info:
446
+ self.viewer.status = "No processing function selected"
447
+ return
448
+
449
+ processing_func = function_info["func"]
450
+ output_suffix = function_info["suffix"]
451
+
452
+ # Get parameter values if available
453
+ param_values = {}
454
+ if hasattr(self, "param_widget_instance") and hasattr(
455
+ self.param_widget_instance, "get_parameter_values"
456
+ ):
457
+ param_values = self.param_widget_instance.get_parameter_values()
458
+
459
+ # Determine output folder
460
+ output_folder = self.output_folder.text().strip()
461
+ if not output_folder:
462
+ output_folder = os.path.dirname(self.file_list[0])
463
+ else:
464
+ # make output folder a subfolder of the input folder
465
+ output_folder = os.path.join(self.input_folder, output_folder)
466
+
467
+ # Ensure output folder exists
468
+ os.makedirs(output_folder, exist_ok=True)
469
+
470
+ # Track processed files
471
+ processed_files_info = []
472
+
473
+ # Process each file
474
+ for filepath in self.file_list:
475
+ try:
476
+ # Load the image
477
+ image = tifffile.imread(filepath)
478
+
479
+ # Apply processing with parameters
480
+ processed_image = processing_func(image, **param_values)
481
+
482
+ # Generate new filename
483
+ filename = os.path.basename(filepath)
484
+ name, ext = os.path.splitext(filename)
485
+ new_filename = (
486
+ name.replace(self.input_suffix, "") + output_suffix + ext
487
+ )
488
+ new_filepath = os.path.join(output_folder, new_filename)
489
+
490
+ # Save processed image
491
+ tifffile.imwrite(new_filepath, processed_image)
492
+
493
+ # Track processed file
494
+ processed_files_info.append(
495
+ {"original_file": filepath, "processed_file": new_filepath}
496
+ )
497
+
498
+ except (
499
+ ValueError,
500
+ TypeError,
501
+ OSError,
502
+ tifffile.TiffFileError,
503
+ ) as e:
504
+ self.viewer.status = f"Error processing {filepath}: {e}"
505
+ print(f"Error processing {filepath}: {e}")
506
+
507
+ # Update table with processed files
508
+ self.table.update_processed_files(processed_files_info)
509
+
510
+ # Update viewer status
511
+ self.viewer.status = f"Processed {len(processed_files_info)} files with {selected_function_name}"
512
+
513
+
514
+ def napari_experimental_provide_dock_widget():
515
+ """
516
+ Provide the file selector widget to Napari
517
+ """
518
+ return file_selector
@@ -0,0 +1,160 @@
1
+ import os
2
+ import sys
3
+
4
+ from magicgui import magicgui
5
+ from napari.layers import Labels
6
+ from napari.viewer import Viewer
7
+ from skimage.io import imread, imsave
8
+
9
+ sys.path.append("src/napari_tmidas")
10
+
11
+
12
+ class LabelInspector:
13
+ def __init__(self, viewer: Viewer):
14
+ self.viewer = viewer
15
+ self.image_label_pairs = []
16
+ self.current_index = 0
17
+
18
+ def load_image_label_pairs(
19
+ self, folder_path: str, image_suffix: str, label_suffix: str
20
+ ):
21
+ """
22
+ Load image-label pairs from a folder.
23
+ """
24
+ files = os.listdir(folder_path)
25
+ image_files = [file for file in files if file.endswith(image_suffix)]
26
+ label_files = [file for file in files if file.endswith(label_suffix)]
27
+
28
+ # Modified matching logic
29
+ self.image_label_pairs = []
30
+ for img in image_files:
31
+ img_base = img[: -len(image_suffix)] # Remove image suffix
32
+ for lbl in label_files:
33
+ if lbl.startswith(
34
+ img_base
35
+ ): # Check if label starts with image base
36
+ self.image_label_pairs.append(
37
+ (
38
+ os.path.join(folder_path, img),
39
+ os.path.join(folder_path, lbl),
40
+ )
41
+ )
42
+ break # Match found, move to next image
43
+
44
+ if not self.image_label_pairs:
45
+ self.viewer.status = "No matching image-label pairs found."
46
+ return
47
+
48
+ self.viewer.status = (
49
+ f"Found {len(self.image_label_pairs)} image-label pairs."
50
+ )
51
+ self.current_index = 0
52
+ self._load_current_pair()
53
+
54
+ def _load_current_pair(self):
55
+ """
56
+ Load the current image-label pair into the Napari viewer.
57
+ """
58
+ if not self.image_label_pairs:
59
+ self.viewer.status = "No pairs to inspect."
60
+ return
61
+
62
+ image_path, label_path = self.image_label_pairs[self.current_index]
63
+ image = imread(image_path)
64
+ label_image = imread(label_path)
65
+
66
+ # Clear existing layers
67
+ self.viewer.layers.clear()
68
+
69
+ # Add the new layers
70
+ self.viewer.add_image(
71
+ image, name=f"Image ({os.path.basename(image_path)})"
72
+ )
73
+ self.viewer.add_labels(
74
+ label_image, name=f"Labels ({os.path.basename(label_path)})"
75
+ )
76
+
77
+ def save_current_labels(self):
78
+ """
79
+ Save the current labels back to the original file.
80
+ """
81
+ if not self.image_label_pairs:
82
+ self.viewer.status = "No pairs to save."
83
+ return
84
+
85
+ _, label_path = self.image_label_pairs[self.current_index]
86
+
87
+ # Find the labels layer in the viewer
88
+ labels_layer = next(
89
+ (
90
+ layer
91
+ for layer in self.viewer.layers
92
+ if isinstance(layer, Labels)
93
+ ),
94
+ None,
95
+ )
96
+
97
+ if labels_layer is None:
98
+ self.viewer.status = "No labels found."
99
+ return
100
+
101
+ # Save the labels layer data to the original file path
102
+ imsave(label_path, labels_layer.data.astype("uint16"))
103
+ self.viewer.status = f"Saved labels to {label_path}."
104
+
105
+ def next_pair(self):
106
+ """
107
+ Save changes and proceed to the next image-label pair.
108
+ """
109
+ if not self.image_label_pairs:
110
+ self.viewer.status = "No pairs to inspect."
111
+ return
112
+
113
+ # Save current labels before proceeding
114
+ self.save_current_labels()
115
+
116
+ # Check if we're already at the last pair
117
+ if self.current_index >= len(self.image_label_pairs) - 1:
118
+ self.viewer.status = "No more pairs to inspect."
119
+ # should also clear the viewer
120
+ self.viewer.layers.clear()
121
+ return
122
+
123
+ # Move to the next pair
124
+ self.current_index += 1
125
+
126
+ # Load the next pair
127
+ self._load_current_pair()
128
+
129
+
130
+ @magicgui(
131
+ call_button="Start Label Inspection",
132
+ folder_path={"label": "Folder Path"},
133
+ image_suffix={"label": "Image Suffix (e.g., .tif)"},
134
+ label_suffix={"label": "Label Suffix (e.g., _labels.tif)"},
135
+ )
136
+ def label_inspector(
137
+ folder_path: str,
138
+ image_suffix: str,
139
+ label_suffix: str,
140
+ viewer: Viewer,
141
+ ):
142
+ """
143
+ MagicGUI widget for starting label inspection.
144
+ """
145
+ inspector = LabelInspector(viewer)
146
+ inspector.load_image_label_pairs(folder_path, image_suffix, label_suffix)
147
+
148
+ # Add buttons for saving and continuing to the next pair
149
+ @magicgui(call_button="Save Changes and Continue")
150
+ def save_and_continue():
151
+ inspector.next_pair()
152
+
153
+ viewer.window.add_dock_widget(save_and_continue)
154
+
155
+
156
+ def label_inspector_widget():
157
+ """
158
+ Provide the label inspector widget to Napari
159
+ """
160
+ return label_inspector
@@ -0,0 +1,68 @@
1
+ # napari_tmidas/_registry.py
2
+ """
3
+ Registry for batch processing functions.
4
+ """
5
+ from typing import Any, Dict, List, Optional
6
+
7
+
8
+ class BatchProcessingRegistry:
9
+ """
10
+ A registry to manage and track available processing functions with parameter support
11
+ """
12
+
13
+ _processing_functions = {}
14
+
15
+ @classmethod
16
+ def register(
17
+ cls,
18
+ name: str,
19
+ suffix: str = "_processed",
20
+ description: str = "",
21
+ parameters: Optional[Dict[str, Dict[str, Any]]] = None,
22
+ ):
23
+ """
24
+ Decorator to register processing functions
25
+
26
+ Args:
27
+ name: Name of the processing function
28
+ suffix: Suffix to append to processed files
29
+ description: Description of what the function does
30
+ parameters: Dictionary of parameters with their metadata
31
+ {
32
+ "param_name": {
33
+ "type": type,
34
+ "default": default_value,
35
+ "min": min_value, # optional, for numeric types
36
+ "max": max_value, # optional, for numeric types
37
+ "description": "Parameter description"
38
+ },
39
+ ...
40
+ }
41
+ """
42
+ if parameters is None:
43
+ parameters = {}
44
+
45
+ def decorator(func):
46
+ cls._processing_functions[name] = {
47
+ "func": func,
48
+ "suffix": suffix,
49
+ "description": description,
50
+ "parameters": parameters,
51
+ }
52
+ return func
53
+
54
+ return decorator
55
+
56
+ @classmethod
57
+ def get_function_info(cls, name: str) -> Optional[dict]:
58
+ """
59
+ Retrieve a registered processing function and its metadata
60
+ """
61
+ return cls._processing_functions.get(name)
62
+
63
+ @classmethod
64
+ def list_functions(cls) -> List[str]:
65
+ """
66
+ List all registered processing function names
67
+ """
68
+ return list(cls._processing_functions.keys())
napari_tmidas/_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.1.1'
21
- __version_tuple__ = version_tuple = (0, 1, 1)
20
+ __version__ = version = '0.1.3'
21
+ __version_tuple__ = version_tuple = (0, 1, 3)
napari_tmidas/napari.yaml CHANGED
@@ -18,18 +18,12 @@ contributions:
18
18
  - id: napari-tmidas.make_sample_data
19
19
  python_name: napari_tmidas._sample_data:make_sample_data
20
20
  title: Load sample data from T-MIDAS
21
- - id: napari-tmidas.make_container_widget
22
- python_name: napari_tmidas:ImageThreshold
23
- title: Make threshold Container widget
24
- - id: napari-tmidas.make_magic_widget
25
- python_name: napari_tmidas:threshold_magic_widget
26
- title: Make threshold magic widget
27
- - id: napari-tmidas.make_function_widget
28
- python_name: napari_tmidas:threshold_autogenerate_widget
29
- title: Make threshold function widget
30
- - id: napari-tmidas.make_qwidget
31
- python_name: napari_tmidas:ExampleQWidget
32
- title: Make example QWidget
21
+ - id: napari-tmidas._label_inspection # hyphen!
22
+ python_name: napari_tmidas._label_inspection:label_inspector_widget # underscore!
23
+ title: Label inspector
24
+ - id: napari-tmidas.file_selector
25
+ python_name: napari_tmidas._file_selector:napari_experimental_provide_dock_widget
26
+ title: File selector
33
27
  readers:
34
28
  - command: napari-tmidas.get_reader
35
29
  accepts_directories: false
@@ -46,12 +40,7 @@ contributions:
46
40
  display_name: T-MIDAS
47
41
  key: unique_id.1
48
42
  widgets:
49
- - command: napari-tmidas.make_container_widget
50
- display_name: Container Threshold
51
- - command: napari-tmidas.make_magic_widget
52
- display_name: Magic Threshold
53
- - command: napari-tmidas.make_function_widget
54
- autogenerate: true
55
- display_name: Autogenerate Threshold
56
- - command: napari-tmidas.make_qwidget
57
- display_name: Example QWidget
43
+ - command: napari-tmidas.file_selector
44
+ display_name: File selector
45
+ - command: napari-tmidas._label_inspection
46
+ display_name: Label inspector
@@ -0,0 +1,61 @@
1
+ # processing_functions/__init__.py
2
+ """
3
+ Package for processing functions that can be registered with the batch processing system.
4
+ """
5
+ import importlib
6
+ import os
7
+ import pkgutil
8
+ from typing import Dict, List
9
+
10
+ # Keep the registry global
11
+ from napari_tmidas._registry import BatchProcessingRegistry
12
+
13
+
14
+ def discover_and_load_processing_functions() -> List[str]:
15
+ """
16
+ Discover and load all processing functions from the processing_functions package.
17
+
18
+ Returns:
19
+ List of registered function names
20
+ """
21
+ # Get the current package
22
+ package = __name__
23
+
24
+ # Find all modules in the package
25
+ for _, module_name, is_pkg in pkgutil.iter_modules(
26
+ [os.path.dirname(__file__)]
27
+ ):
28
+ if not is_pkg: # Only load non-package modules
29
+ try:
30
+ # Import the module
31
+ importlib.import_module(f"{package}.{module_name}")
32
+ print(f"Loaded processing function module: {module_name}")
33
+ except ImportError as e:
34
+ # Log the error but continue with other modules
35
+ print(f"Failed to import {module_name}: {e}")
36
+
37
+ # Return the list of registered functions
38
+ return BatchProcessingRegistry.list_functions()
39
+
40
+
41
+ def get_processing_function_info() -> Dict[str, Dict]:
42
+ """
43
+ Get information about all registered processing functions.
44
+
45
+ Returns:
46
+ Dictionary of function information
47
+ """
48
+ return {
49
+ name: {
50
+ "description": BatchProcessingRegistry.get_function_info(name).get(
51
+ "description", ""
52
+ ),
53
+ "suffix": BatchProcessingRegistry.get_function_info(name).get(
54
+ "suffix", ""
55
+ ),
56
+ "parameters": BatchProcessingRegistry.get_function_info(name).get(
57
+ "parameters", {}
58
+ ),
59
+ }
60
+ for name in BatchProcessingRegistry.list_functions()
61
+ }
@@ -0,0 +1,60 @@
1
+ # processing_functions/basic.py
2
+ """
3
+ Basic image processing functions that don't require additional dependencies.
4
+ """
5
+ import numpy as np
6
+
7
+ from napari_tmidas._registry import BatchProcessingRegistry
8
+
9
+
10
+ @BatchProcessingRegistry.register(
11
+ name="Min-Max Normalization",
12
+ suffix="_normalized",
13
+ description="Normalize image values to range [0, 1] using min-max scaling",
14
+ )
15
+ def normalize_image(image: np.ndarray) -> np.ndarray:
16
+ """
17
+ Simple min-max normalization
18
+ """
19
+ if image.min() == image.max():
20
+ return np.zeros_like(image, dtype=float)
21
+ return (image - image.min()) / (image.max() - image.min())
22
+
23
+
24
+ @BatchProcessingRegistry.register(
25
+ name="Contrast Stretch",
26
+ suffix="_contrast",
27
+ description="Stretch the contrast by clipping percentiles and rescaling",
28
+ parameters={
29
+ "low_percentile": {
30
+ "type": float,
31
+ "default": 2.0,
32
+ "min": 0.0,
33
+ "max": 49.0,
34
+ "description": "Low percentile to clip",
35
+ },
36
+ "high_percentile": {
37
+ "type": float,
38
+ "default": 98.0,
39
+ "min": 51.0,
40
+ "max": 100.0,
41
+ "description": "High percentile to clip",
42
+ },
43
+ },
44
+ )
45
+ def contrast_stretch(
46
+ image: np.ndarray,
47
+ low_percentile: float = 2.0,
48
+ high_percentile: float = 98.0,
49
+ ) -> np.ndarray:
50
+ """
51
+ Stretch contrast by clipping percentiles
52
+ """
53
+ p_low = np.percentile(image, low_percentile)
54
+ p_high = np.percentile(image, high_percentile)
55
+
56
+ # Clip and normalize
57
+ image_clipped = np.clip(image, p_low, p_high)
58
+ if p_high == p_low:
59
+ return np.zeros_like(image, dtype=float)
60
+ return (image_clipped - p_low) / (p_high - p_low)
@@ -0,0 +1,57 @@
1
+ # processing_functions/scipy_filters.py
2
+ """
3
+ Processing functions that depend on SciPy.
4
+ """
5
+ import numpy as np
6
+
7
+ try:
8
+ from scipy import ndimage
9
+
10
+ SCIPY_AVAILABLE = True
11
+ except ImportError:
12
+ SCIPY_AVAILABLE = False
13
+ print("SciPy not available, some processing functions will be disabled")
14
+
15
+ from napari_tmidas._registry import BatchProcessingRegistry
16
+
17
+ if SCIPY_AVAILABLE:
18
+
19
+ @BatchProcessingRegistry.register(
20
+ name="Gaussian Blur",
21
+ suffix="_blurred",
22
+ description="Apply Gaussian blur to the image",
23
+ parameters={
24
+ "sigma": {
25
+ "type": float,
26
+ "default": 1.0,
27
+ "min": 0.1,
28
+ "max": 10.0,
29
+ "description": "Standard deviation for Gaussian kernel",
30
+ }
31
+ },
32
+ )
33
+ def gaussian_blur(image: np.ndarray, sigma: float = 1.0) -> np.ndarray:
34
+ """
35
+ Apply Gaussian blur to the image
36
+ """
37
+ return ndimage.gaussian_filter(image, sigma=sigma)
38
+
39
+ @BatchProcessingRegistry.register(
40
+ name="Median Filter",
41
+ suffix="_median",
42
+ description="Apply median filter for noise reduction",
43
+ parameters={
44
+ "size": {
45
+ "type": int,
46
+ "default": 3,
47
+ "min": 3,
48
+ "max": 15,
49
+ "description": "Size of the median filter window",
50
+ }
51
+ },
52
+ )
53
+ def median_filter(image: np.ndarray, size: int = 3) -> np.ndarray:
54
+ """
55
+ Apply median filter for noise reduction
56
+ """
57
+ return ndimage.median_filter(image, size=size)
@@ -0,0 +1,113 @@
1
+ # processing_functions/skimage_filters.py
2
+ """
3
+ Processing functions that depend on scikit-image.
4
+ """
5
+ import numpy as np
6
+
7
+ try:
8
+ import skimage.exposure
9
+ import skimage.filters
10
+
11
+ SKIMAGE_AVAILABLE = True
12
+ except ImportError:
13
+ SKIMAGE_AVAILABLE = False
14
+ print(
15
+ "scikit-image not available, some processing functions will be disabled"
16
+ )
17
+
18
+ from napari_tmidas._registry import BatchProcessingRegistry
19
+
20
+ if SKIMAGE_AVAILABLE:
21
+
22
+ @BatchProcessingRegistry.register(
23
+ name="Adaptive Histogram Equalization",
24
+ suffix="_clahe",
25
+ description="Enhance contrast using Contrast Limited Adaptive Histogram Equalization",
26
+ parameters={
27
+ "kernel_size": {
28
+ "type": int,
29
+ "default": 8,
30
+ "min": 4,
31
+ "max": 64,
32
+ "description": "Size of local region for histogram equalization",
33
+ },
34
+ "clip_limit": {
35
+ "type": float,
36
+ "default": 0.01,
37
+ "min": 0.001,
38
+ "max": 0.1,
39
+ "description": "Clipping limit for contrast enhancement",
40
+ },
41
+ },
42
+ )
43
+ def adaptive_hist_eq(
44
+ image: np.ndarray, kernel_size: int = 8, clip_limit: float = 0.01
45
+ ) -> np.ndarray:
46
+ """
47
+ Apply Contrast Limited Adaptive Histogram Equalization
48
+ """
49
+ # CLAHE expects image in [0, 1] range
50
+ img_norm = skimage.exposure.rescale_intensity(image, out_range=(0, 1))
51
+ return skimage.exposure.equalize_adapthist(
52
+ img_norm, kernel_size=kernel_size, clip_limit=clip_limit
53
+ )
54
+
55
+ @BatchProcessingRegistry.register(
56
+ name="Edge Detection",
57
+ suffix="_edges",
58
+ description="Detect edges using Sobel filter",
59
+ )
60
+ def edge_detection(image: np.ndarray) -> np.ndarray:
61
+ """
62
+ Detect edges using Sobel filter
63
+ """
64
+ return skimage.filters.sobel(image)
65
+
66
+ # simple otsu thresholding
67
+ @BatchProcessingRegistry.register(
68
+ name="Otsu Thresholding (semantic)",
69
+ suffix="_otsu_semantic",
70
+ description="Threshold image using Otsu's method",
71
+ )
72
+ def otsu_thresholding(image: np.ndarray) -> np.ndarray:
73
+ """
74
+ Threshold image using Otsu's method
75
+ """
76
+ thresh = skimage.filters.threshold_otsu(image)
77
+ return (image > thresh).astype(np.uint32)
78
+
79
+ # instance segmentation
80
+ @BatchProcessingRegistry.register(
81
+ name="Otsu Thresholding (instance)",
82
+ suffix="_otsu_labels",
83
+ description="Threshold image using Otsu's method",
84
+ )
85
+ def otsu_thresholding_instance(image: np.ndarray) -> np.ndarray:
86
+ """
87
+ Threshold image using Otsu's method
88
+ """
89
+ thresh = skimage.filters.threshold_otsu(image)
90
+ return skimage.measure.label(image > thresh).astype(np.uint32)
91
+
92
+ # simple thresholding
93
+ @BatchProcessingRegistry.register(
94
+ name="Manual Thresholding (8-bit)",
95
+ suffix="_thresh",
96
+ description="Threshold image using a fixed threshold",
97
+ parameters={
98
+ "threshold": {
99
+ "type": int,
100
+ "default": 128,
101
+ "min": 0,
102
+ "max": 255,
103
+ "description": "Threshold value",
104
+ },
105
+ },
106
+ )
107
+ def simple_thresholding(
108
+ image: np.ndarray, threshold: int = 128
109
+ ) -> np.ndarray:
110
+ """
111
+ Threshold image using a fixed threshold
112
+ """
113
+ return image > threshold
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: napari-tmidas
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Tissue Microscopy Image Data Analysis Suite
5
5
  Author: Marco Meer
6
6
  Author-email: marco.meer@pm.me
@@ -57,6 +57,7 @@ Requires-Dist: numpy
57
57
  Requires-Dist: magicgui
58
58
  Requires-Dist: qtpy
59
59
  Requires-Dist: scikit-image
60
+ Requires-Dist: pyqt5
60
61
  Provides-Extra: testing
61
62
  Requires-Dist: tox; extra == "testing"
62
63
  Requires-Dist: pytest; extra == "testing"
@@ -71,34 +72,54 @@ Requires-Dist: pyqt5; extra == "testing"
71
72
  [![PyPI](https://img.shields.io/pypi/v/napari-tmidas.svg?color=green)](https://pypi.org/project/napari-tmidas)
72
73
  [![Python Version](https://img.shields.io/pypi/pyversions/napari-tmidas.svg?color=green)](https://python.org)
73
74
  [![tests](https://github.com/macromeer/napari-tmidas/workflows/tests/badge.svg)](https://github.com/macromeer/napari-tmidas/actions)
74
- [![codecov](https://codecov.io/gh/macromeer/napari-tmidas/branch/main/graph/badge.svg)](https://codecov.io/gh/macromeer/napari-tmidas)
75
75
  [![napari hub](https://img.shields.io/endpoint?url=https://api.napari-hub.org/shields/napari-tmidas)](https://napari-hub.org/plugins/napari-tmidas)
76
+ <!-- [![codecov](https://codecov.io/gh/macromeer/napari-tmidas/branch/main/graph/badge.svg)](https://codecov.io/gh/macromeer/napari-tmidas) -->
76
77
 
77
- Tissue Microscopy Image Data Analysis Suite
78
+ The Tissue Microscopy Image Data Analysis Suite (short: T-MIDAS), is a collection of pipelines for batch image preprocessing, segmentation, regions-of-interest (ROI) analysis and other useful features. This is a work in progress (WIP) and an evolutionary step away from the [terminal / command-line version of T-MIDAS](https://github.com/MercaderLabAnatomy/T-MIDAS).
78
79
 
79
- ----------------------------------
80
+ ## Installation
80
81
 
81
- This [napari] plugin was generated with [copier] using the [napari-plugin-template].
82
+ First install Napari in a virtual environment following the latest [Napari installation instructions](https://github.com/Napari/napari?tab=readme-ov-file#installation).
82
83
 
83
- <!--
84
- Don't miss the full getting started guide to set up your new package:
85
- https://github.com/napari/napari-plugin-template#getting-started
86
84
 
87
- and review the napari docs for plugin developers:
88
- https://napari.org/stable/plugins/index.html
89
- -->
85
+ After you have activated the environment, you can install `napari-tmidas` via [pip]:
90
86
 
91
- ## Installation
87
+ pip install napari-tmidas
92
88
 
93
- You can install `napari-tmidas` via [pip]:
89
+ To install the latest development version:
94
90
 
95
- pip install napari-tmidas
91
+ pip install git+https://github.com/macromeer/napari-tmidas.git
96
92
 
93
+ ## Usage
97
94
 
95
+ You can find the installed plugin here:
96
+
97
+ ![image](https://github.com/user-attachments/assets/504db09a-d66e-49eb-90cd-3237024d9d7a)
98
98
 
99
- To install latest development version :
99
+ ### File inspector
100
100
 
101
- pip install git+https://github.com/macromeer/napari-tmidas.git
101
+ 1. After opening `Plugins > T-MIDAS > File selector`, enter the path to the folder containing the images to be processed (currently supports TIF, later also ZARR). You can also filter for filename suffix.
102
+
103
+ ![image](https://github.com/user-attachments/assets/41ecb689-9abe-4371-83b5-9c5eb37069f9)
104
+
105
+ 2. As a result, a table appears with the found images.
106
+
107
+ ![image](https://github.com/user-attachments/assets/8360942a-be8f-49ec-bc25-385ee43bd601)
108
+
109
+ 3. Next, select a processing function, set parameters if applicable and `Start Batch Processing`.
110
+
111
+ ![image](https://github.com/user-attachments/assets/05929660-6672-4f76-89da-4f17749ccfad)
112
+
113
+ 4. You can click on the images in the table to show them in the viewer. For example first click on one of the `Original Files`, and then the corresponding `Processed File` to see an overlay.
114
+
115
+ ![image](https://github.com/user-attachments/assets/cfe84828-c1cc-4196-9a53-5dfb82d5bfce)
116
+
117
+ Note that whenever you click on an `Original File` or `Processed File` in the table, it will replace the one that is currently shown in the viewer. So naturally, you'd first select the original image, and then the processed image to correctly see the image pair that you want to inspect.
118
+
119
+ ### Label inspector
120
+ If you have already segmented a folder full of images and now you want to maybe inspect and edit each label image, you can use the `Plugins > T-MIDAS > Label inspector`, which automatically saves your changes to the existing label image once you click the `Save Changes and Continue` button (bottom right).
121
+
122
+ ![image](https://github.com/user-attachments/assets/0bf8c6ae-4212-449d-8183-e91b23ba740e)
102
123
 
103
124
 
104
125
  ## Contributing
@@ -128,6 +149,18 @@ If you encounter any problems, please [file an issue] along with a detailed desc
128
149
 
129
150
  [file an issue]: https://github.com/macromeer/napari-tmidas/issues
130
151
 
152
+ ----------------------------------
153
+
154
+ This [napari] plugin was generated with [copier] using the [napari-plugin-template].
155
+
156
+ <!--
157
+ Don't miss the full getting started guide to set up your new package:
158
+ https://github.com/napari/napari-plugin-template#getting-started
159
+
160
+ and review the napari docs for plugin developers:
161
+ https://napari.org/stable/plugins/index.html
162
+ -->
163
+
131
164
  [napari]: https://github.com/napari/napari
132
165
  [tox]: https://tox.readthedocs.io/en/latest/
133
166
  [pip]: https://pypi.org/project/pip/
@@ -0,0 +1,25 @@
1
+ napari_tmidas/__init__.py,sha256=Z9mznblUlUsRyH3d4k8SxUo4iXLMwJXURbq41QzhPpo,459
2
+ napari_tmidas/_file_selector.py,sha256=YzjS-XIqLD8826n50KXAc0GfQeXOhvDs41QP3-bTtCU,17471
3
+ napari_tmidas/_label_inspection.py,sha256=0icowMfyNRnaxgTIOi8KHxLXKfpfqtShIl8iVR4wJjc,4819
4
+ napari_tmidas/_reader.py,sha256=A9_hdDxtVkVGmbOsbqgnARCSvpEh7GGPo7ylzmbnu8o,2485
5
+ napari_tmidas/_registry.py,sha256=Oz9HFJh41MKRLeKxRuc7x7yzc-OrmoTdRFnfngFU_XE,2007
6
+ napari_tmidas/_sample_data.py,sha256=khuv1jemz_fCjqNwEKMFf83Ju0EN4S89IKydsUMmUxw,645
7
+ napari_tmidas/_version.py,sha256=NIzzV8ZM0W-CSLuEs1weG4zPrn_-8yr1AwwI1iuS6yo,511
8
+ napari_tmidas/_widget.py,sha256=u9uf9WILAwZg_InhFyjWInY4ej1TV1a59dR8Fe3vNF8,4794
9
+ napari_tmidas/_writer.py,sha256=wbVfHFjjHdybSg37VR4lVmL-kdCkDZsUPDJ66AVLaFQ,1941
10
+ napari_tmidas/napari.yaml,sha256=Xmui3_7pxNxOkIFRWsZWuka56d6PXLQ2rl4XvMDl2aw,1839
11
+ napari_tmidas/_tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ napari_tmidas/_tests/test_reader.py,sha256=gN_2StATLZYUL56X27ImJTVru_qSoFiY4vtgajcx3H0,975
13
+ napari_tmidas/_tests/test_sample_data.py,sha256=D1HU_C3hWpO3mlSW_7Z94xaYHDtxz0XUrMjQoYop9Ag,104
14
+ napari_tmidas/_tests/test_widget.py,sha256=I_d-Cra_CTcS0QdMItg_HMphvhj0XCx81JnFyCHk9lg,2204
15
+ napari_tmidas/_tests/test_writer.py,sha256=4_MlZM9a5So74J16_4tIOJc6pwTOw9R0-oAE_YioIx4,122
16
+ napari_tmidas/processing_functions/__init__.py,sha256=osXY9jSgDsrwFaS6ShPHP0wGRxMuX1mHRN9EDa9l41g,1891
17
+ napari_tmidas/processing_functions/basic.py,sha256=g7tQ25UIxA26n6GBYcHlkSjUbv_lD-7x_Sd-ZvWbzUY,1711
18
+ napari_tmidas/processing_functions/scipy_filters.py,sha256=kKpDAlQQ0ZNbkt77QUWi-Bwolk6MMDvtG_bZJV3MjOo,1612
19
+ napari_tmidas/processing_functions/skimage_filters.py,sha256=RpBywSImAQc_L_0pysA3yAJlHHfZuqu6c3vokDv5p1I,3517
20
+ napari_tmidas-0.1.3.dist-info/LICENSE,sha256=tSjiOqj57exmEIfP2YVPCEeQf0cH49S6HheQR8IiY3g,1485
21
+ napari_tmidas-0.1.3.dist-info/METADATA,sha256=bXMq78n-Y-vWxw1W3zC9PHkG2-4KvIqxoUDXSNV72Yk,8222
22
+ napari_tmidas-0.1.3.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
23
+ napari_tmidas-0.1.3.dist-info/entry_points.txt,sha256=fbVjzbJTm4aDMIBtel1Lyqvq-CwXY7wmCOo_zJ-jtRY,60
24
+ napari_tmidas-0.1.3.dist-info/top_level.txt,sha256=63ybdxCZ4SeT13f_Ou4TsivGV_2Gtm_pJOXToAt30_E,14
25
+ napari_tmidas-0.1.3.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.2)
2
+ Generator: setuptools (76.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,18 +0,0 @@
1
- napari_tmidas/__init__.py,sha256=cVpVT4zJ5I9N6YFige0iySIMW3JzE35KKnEy-PFpFx0,592
2
- napari_tmidas/_reader.py,sha256=A9_hdDxtVkVGmbOsbqgnARCSvpEh7GGPo7ylzmbnu8o,2485
3
- napari_tmidas/_sample_data.py,sha256=khuv1jemz_fCjqNwEKMFf83Ju0EN4S89IKydsUMmUxw,645
4
- napari_tmidas/_version.py,sha256=Mmxse1R0ki5tjz9qzU8AQyqUsLt8nTyCAbYQp8R87PU,511
5
- napari_tmidas/_widget.py,sha256=u9uf9WILAwZg_InhFyjWInY4ej1TV1a59dR8Fe3vNF8,4794
6
- napari_tmidas/_writer.py,sha256=wbVfHFjjHdybSg37VR4lVmL-kdCkDZsUPDJ66AVLaFQ,1941
7
- napari_tmidas/napari.yaml,sha256=XRz2siVLEciSqUHv1tlLjX9BB0Sc1jBRB3KyAN2ISgA,2276
8
- napari_tmidas/_tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- napari_tmidas/_tests/test_reader.py,sha256=gN_2StATLZYUL56X27ImJTVru_qSoFiY4vtgajcx3H0,975
10
- napari_tmidas/_tests/test_sample_data.py,sha256=D1HU_C3hWpO3mlSW_7Z94xaYHDtxz0XUrMjQoYop9Ag,104
11
- napari_tmidas/_tests/test_widget.py,sha256=I_d-Cra_CTcS0QdMItg_HMphvhj0XCx81JnFyCHk9lg,2204
12
- napari_tmidas/_tests/test_writer.py,sha256=4_MlZM9a5So74J16_4tIOJc6pwTOw9R0-oAE_YioIx4,122
13
- napari_tmidas-0.1.1.dist-info/LICENSE,sha256=tSjiOqj57exmEIfP2YVPCEeQf0cH49S6HheQR8IiY3g,1485
14
- napari_tmidas-0.1.1.dist-info/METADATA,sha256=TuiFctic_XaF8TxYPQYuLJOlVTKiF_Qxl0Lh6SiYBb4,5850
15
- napari_tmidas-0.1.1.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
16
- napari_tmidas-0.1.1.dist-info/entry_points.txt,sha256=fbVjzbJTm4aDMIBtel1Lyqvq-CwXY7wmCOo_zJ-jtRY,60
17
- napari_tmidas-0.1.1.dist-info/top_level.txt,sha256=63ybdxCZ4SeT13f_Ou4TsivGV_2Gtm_pJOXToAt30_E,14
18
- napari_tmidas-0.1.1.dist-info/RECORD,,