napari-stitcher 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- napari_stitcher/__init__.py +16 -0
- napari_stitcher/_mosaic_widget.py +192 -0
- napari_stitcher/_reader.py +98 -0
- napari_stitcher/_sample_data.py +66 -0
- napari_stitcher/_stitcher_widget.py +594 -0
- napari_stitcher/_tests/__init__.py +0 -0
- napari_stitcher/_tests/test_mosaic_widget.py +54 -0
- napari_stitcher/_tests/test_reader.py +36 -0
- napari_stitcher/_tests/test_stitcher_widget.py +277 -0
- napari_stitcher/_tests/test_viewer_utils.py +69 -0
- napari_stitcher/_tests/test_writer.py +95 -0
- napari_stitcher/_utils.py +102 -0
- napari_stitcher/_version.py +16 -0
- napari_stitcher/_writer.py +65 -0
- napari_stitcher/napari.yaml +55 -0
- napari_stitcher/viewer_utils.py +399 -0
- napari_stitcher-0.1.0.dist-info/LICENSE +28 -0
- napari_stitcher-0.1.0.dist-info/METADATA +102 -0
- napari_stitcher-0.1.0.dist-info/RECORD +22 -0
- napari_stitcher-0.1.0.dist-info/WHEEL +5 -0
- napari_stitcher-0.1.0.dist-info/entry_points.txt +2 -0
- napari_stitcher-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
__version__ = "0.0.1"
|
|
2
|
+
|
|
3
|
+
from ._reader import napari_get_reader
|
|
4
|
+
from ._sample_data import make_sample_data
|
|
5
|
+
from ._stitcher_widget import StitcherQWidget
|
|
6
|
+
from ._mosaic_widget import MosaicQWidget
|
|
7
|
+
from ._writer import write_multiple, write_single_image
|
|
8
|
+
|
|
9
|
+
__all__ = (
|
|
10
|
+
"napari_get_reader",
|
|
11
|
+
"write_single_image",
|
|
12
|
+
"write_multiple",
|
|
13
|
+
"make_sample_data",
|
|
14
|
+
"StitcherQWidget",
|
|
15
|
+
"MosaicQWidget",
|
|
16
|
+
)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module is an example of a barebones QWidget plugin for napari
|
|
3
|
+
|
|
4
|
+
It implements the Widget specification.
|
|
5
|
+
see: https://napari.org/stable/plugins/guides.html?#widgets
|
|
6
|
+
|
|
7
|
+
Replace code below according to your needs.
|
|
8
|
+
"""
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
import os, tempfile, sys
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from napari.utils import notifications
|
|
15
|
+
|
|
16
|
+
from magicgui import widgets
|
|
17
|
+
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
|
18
|
+
|
|
19
|
+
from multiview_stitcher import (
|
|
20
|
+
registration,
|
|
21
|
+
fusion,
|
|
22
|
+
spatial_image_utils,
|
|
23
|
+
msi_utils,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from napari_stitcher import _reader, viewer_utils, _utils
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
import napari
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MosaicQWidget(QWidget):
|
|
33
|
+
# your QWidget.__init__ can optionally request the napari viewer instance
|
|
34
|
+
# in one of two ways:
|
|
35
|
+
# 1. use a parameter called `napari_viewer`, as done here
|
|
36
|
+
# 2. use a type annotation of 'napari.viewer.Viewer' for any parameter
|
|
37
|
+
def __init__(self, napari_viewer):
|
|
38
|
+
super().__init__()
|
|
39
|
+
self.viewer = napari_viewer
|
|
40
|
+
self.viewer.title = "Napari Stitcher"
|
|
41
|
+
|
|
42
|
+
self.setLayout(QVBoxLayout())
|
|
43
|
+
|
|
44
|
+
# mosaic arrangement widgets
|
|
45
|
+
self.overlap = widgets.FloatSlider(
|
|
46
|
+
value=0.1, min=0, max=0.9999, label='Overlap:')
|
|
47
|
+
self.n_col = widgets.SpinBox(
|
|
48
|
+
value=1, min=1, max=100, label='Number of columns:')
|
|
49
|
+
self.n_row = widgets.SpinBox(
|
|
50
|
+
value=1, min=1, max=100, label='Number of rows:')
|
|
51
|
+
self.mosaic_arr = widgets.ComboBox(
|
|
52
|
+
choices=['rows first','columns first','snake by rows','snake by columns'],
|
|
53
|
+
value='snake by columns',
|
|
54
|
+
label='Mosaic arrangement:',
|
|
55
|
+
tooltip='Select the type of grid arrangement. The tile order is taken from the list of layers.')
|
|
56
|
+
self.input_order = widgets.ComboBox(
|
|
57
|
+
choices=['forward','backward'],
|
|
58
|
+
value='forward',
|
|
59
|
+
label='Image order:',
|
|
60
|
+
tooltip='Whether to arrange the tiles in forward or backward order.')
|
|
61
|
+
self.button_arrange_tiles = widgets.Button(text='Arrange tiles')
|
|
62
|
+
|
|
63
|
+
# organize widgets
|
|
64
|
+
self.mosaic_widgets = [
|
|
65
|
+
self.overlap,
|
|
66
|
+
self.n_col,
|
|
67
|
+
self.n_row,
|
|
68
|
+
self.mosaic_arr,
|
|
69
|
+
self.input_order,
|
|
70
|
+
self.button_arrange_tiles,
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
self.container = widgets.VBox(widgets=\
|
|
74
|
+
self.mosaic_widgets
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
self.container.native.setMinimumWidth = 50
|
|
78
|
+
|
|
79
|
+
self.layout().addWidget(self.container.native)
|
|
80
|
+
|
|
81
|
+
# connect callbacks
|
|
82
|
+
self.button_arrange_tiles.clicked.connect(self.arrange_tiles)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def reset(self):
|
|
86
|
+
|
|
87
|
+
self.msims = {}
|
|
88
|
+
self.input_layers = []
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def link_channel_layers(self, layers):
|
|
92
|
+
|
|
93
|
+
# link channel layers
|
|
94
|
+
from napari.experimental import link_layers
|
|
95
|
+
|
|
96
|
+
sims = {l.name: msi_utils.get_sim_from_msim(self.msims[l.name])
|
|
97
|
+
for l in layers}
|
|
98
|
+
|
|
99
|
+
channels = [_utils.get_str_unique_to_ch_from_sim_coords(sim.coords) for sim in sims.values()]
|
|
100
|
+
for ch in channels:
|
|
101
|
+
ch_layers = list(_utils.filter_layers(layers, sims, ch=ch))
|
|
102
|
+
|
|
103
|
+
if len(ch_layers):
|
|
104
|
+
link_layers(ch_layers, ('contrast_limits', 'visible'))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def arrange_tiles(self):
|
|
108
|
+
"""
|
|
109
|
+
Arrange tiles in the viewer according to the selected mosaic arrangement.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
layer_channels = [
|
|
113
|
+
l.name if not '::' in l.name else _utils.get_str_unique_to_ch_from_layer_name(l.name)
|
|
114
|
+
for l in self.viewer.layers]
|
|
115
|
+
|
|
116
|
+
layer_views = [
|
|
117
|
+
_utils.get_str_unique_to_view_from_layer_name(l.name)
|
|
118
|
+
for l in self.viewer.layers]
|
|
119
|
+
|
|
120
|
+
view_order = []
|
|
121
|
+
for lview in layer_views:
|
|
122
|
+
if lview not in view_order:
|
|
123
|
+
view_order.append(lview)
|
|
124
|
+
|
|
125
|
+
if self.input_order.value == 'backward':
|
|
126
|
+
view_order = view_order[::-1]
|
|
127
|
+
|
|
128
|
+
n_channels = len(np.unique(layer_channels))
|
|
129
|
+
n_tiles = len(np.unique(layer_views))
|
|
130
|
+
|
|
131
|
+
if n_tiles != self.n_col.value * self.n_row.value:
|
|
132
|
+
notifications.notification_manager.receive_info(
|
|
133
|
+
'Warning: Total number of tiles does not match the selected mosaic arrangement.'
|
|
134
|
+
)
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
if n_tiles == 0:
|
|
138
|
+
notifications.notification_manager.receive_info(
|
|
139
|
+
'No layers loaded.'
|
|
140
|
+
)
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
tile_w = self.viewer.layers[0].extent.world[1,-1] - self.viewer.layers[0].extent.world[0,-1] # tile width
|
|
144
|
+
tile_h = self.viewer.layers[0].extent.world[1,-2] - self.viewer.layers[0].extent.world[0,-2] # tile height
|
|
145
|
+
|
|
146
|
+
tile_indices = _utils.get_tile_indices(
|
|
147
|
+
mosaic_arr=self.mosaic_arr.value,
|
|
148
|
+
n_col=self.n_row.value,
|
|
149
|
+
n_row=self.n_col.value,
|
|
150
|
+
n_tiles=n_tiles
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if self.input_order.value == 'forward':
|
|
154
|
+
l0_translate = self.viewer.layers[0].translate[-2:]
|
|
155
|
+
else:
|
|
156
|
+
l0_translate = self.viewer.layers[-1].translate[-2:]
|
|
157
|
+
|
|
158
|
+
for l in self.viewer.layers:
|
|
159
|
+
view = _utils.get_str_unique_to_view_from_layer_name(l.name)
|
|
160
|
+
itile = view_order.index(view)
|
|
161
|
+
tile_index = tile_indices[itile]
|
|
162
|
+
# print(f'Layer {l.name} -> tile index: {tile_index}')
|
|
163
|
+
l.translate[-2:] = [
|
|
164
|
+
l0_translate[0] + tile_index[1] * tile_w - tile_index[1] * tile_w * self.overlap.value,
|
|
165
|
+
l0_translate[1] + tile_index[0] * tile_h - tile_index[0] * tile_h * self.overlap.value
|
|
166
|
+
]
|
|
167
|
+
l.refresh()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def __del__(self):
|
|
171
|
+
|
|
172
|
+
print('Deleting napari-stitcher mosaic widget')
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == "__main__":
|
|
176
|
+
|
|
177
|
+
import napari
|
|
178
|
+
|
|
179
|
+
viewer = napari.Viewer()
|
|
180
|
+
|
|
181
|
+
wdg = MosaicQWidget(viewer)
|
|
182
|
+
viewer.window.add_dock_widget(wdg)
|
|
183
|
+
|
|
184
|
+
for irow in range(2):
|
|
185
|
+
for icol in range(2):
|
|
186
|
+
for ch in range(2):
|
|
187
|
+
viewer.add_image(np.random.randint(0, 100, [100] * 3), name=f'layer_{irow}_{icol} :: ch{ch}')
|
|
188
|
+
|
|
189
|
+
wdg.n_col.value = 2
|
|
190
|
+
wdg.n_row.value = 2
|
|
191
|
+
|
|
192
|
+
wdg.button_arrange_tiles.clicked()
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module is an example of a barebones numpy reader plugin for napari.
|
|
3
|
+
|
|
4
|
+
It implements the Reader specification, but your plugin may choose to
|
|
5
|
+
implement multiple readers or even other plugin contributions. see:
|
|
6
|
+
https://napari.org/stable/plugins/guides.html?#readers
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from multiview_stitcher import msi_utils
|
|
10
|
+
from multiview_stitcher.io import read_mosaic_image_into_list_of_spatial_xarrays,\
|
|
11
|
+
METADATA_TRANSFORM_KEY
|
|
12
|
+
|
|
13
|
+
from napari_stitcher import viewer_utils
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def napari_get_reader(path):
|
|
17
|
+
"""A basic implementation of a Reader contribution.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
path : str or list of str
|
|
22
|
+
Path to file, or list of paths.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
function or None
|
|
27
|
+
If the path is a recognized format, return a function that accepts the
|
|
28
|
+
same path or list of paths, and returns a list of layer data tuples.
|
|
29
|
+
"""
|
|
30
|
+
if isinstance(path, list):
|
|
31
|
+
# reader plugins may be handed single path, or a list of paths.
|
|
32
|
+
# if it is a list, it is assumed to be an image stack...
|
|
33
|
+
# so we are only going to look at the first file.
|
|
34
|
+
path = path[0]
|
|
35
|
+
|
|
36
|
+
# if we know we cannot read the file, we immediately return None.
|
|
37
|
+
# otherwise we return the *function* that can read ``path``.
|
|
38
|
+
if path.endswith(".czi"):
|
|
39
|
+
return read_mosaic
|
|
40
|
+
else:
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def read_mosaic(path, scene_index=None):
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
Read in tiles as layers.
|
|
48
|
+
|
|
49
|
+
Take a path or list of paths and return a list of LayerData tuples.
|
|
50
|
+
|
|
51
|
+
Readers are expected to return data as a list of tuples, where each tuple
|
|
52
|
+
is (data, [add_kwargs, [layer_type]]), "add_kwargs" and "layer_type" are
|
|
53
|
+
both optional.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
path : str or list of str
|
|
58
|
+
Path to file, or list of paths.
|
|
59
|
+
|
|
60
|
+
Returns
|
|
61
|
+
-------
|
|
62
|
+
layer_data : list of tuples
|
|
63
|
+
A list of LayerData tuples where each tuple in the list contains
|
|
64
|
+
(data, metadata, layer_type), where data is a numpy array, metadata is
|
|
65
|
+
a dict of keyword arguments for the corresponding viewer.add_* method
|
|
66
|
+
in napari, and layer_type is a lower-case string naming the type of
|
|
67
|
+
layer. Both "meta", and "layer_type" are optional. napari will
|
|
68
|
+
default to layer_type=="image" if not provided
|
|
69
|
+
"""
|
|
70
|
+
# handle both a string and a list of strings
|
|
71
|
+
paths = [path] if isinstance(path, str) else path
|
|
72
|
+
|
|
73
|
+
sims = read_mosaic_image_into_list_of_spatial_xarrays(paths[0], scene_index=scene_index)
|
|
74
|
+
|
|
75
|
+
msims = [msi_utils.get_msim_from_sim(sim, scale_factors=[])
|
|
76
|
+
for sim in sims]
|
|
77
|
+
|
|
78
|
+
out_layers = viewer_utils.create_image_layer_tuples_from_msims(
|
|
79
|
+
msims,
|
|
80
|
+
transform_key=METADATA_TRANSFORM_KEY,
|
|
81
|
+
data_as_array=False)
|
|
82
|
+
|
|
83
|
+
return out_layers
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
|
|
88
|
+
from multiview_stitcher.sample_data import get_mosaic_sample_data_path
|
|
89
|
+
|
|
90
|
+
filename = get_mosaic_sample_data_path()
|
|
91
|
+
|
|
92
|
+
sims = read_mosaic_image_into_list_of_spatial_xarrays(filename)
|
|
93
|
+
|
|
94
|
+
from multiview_stitcher import msi_utils
|
|
95
|
+
|
|
96
|
+
msim = msi_utils.get_msim_from_sim(sims[0], scale_factors=[])
|
|
97
|
+
|
|
98
|
+
msim_sel = msi_utils.multiscale_sel_coords(msim, {'c':'EGFP'})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module is an example of a barebones sample data provider for napari.
|
|
3
|
+
|
|
4
|
+
It implements the "sample data" specification.
|
|
5
|
+
see: https://napari.org/stable/plugins/guides.html?#sample-data
|
|
6
|
+
|
|
7
|
+
Replace code below according to your needs.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from napari_stitcher import viewer_utils
|
|
16
|
+
from napari_stitcher._reader import read_mosaic
|
|
17
|
+
|
|
18
|
+
from multiview_stitcher.sample_data import get_mosaic_sample_data_path
|
|
19
|
+
from multiview_stitcher.io import METADATA_TRANSFORM_KEY
|
|
20
|
+
from multiview_stitcher.sample_data import generate_tiled_dataset
|
|
21
|
+
from multiview_stitcher.msi_utils import get_msim_from_sim
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def make_sample_data():
|
|
25
|
+
"""Generates an image"""
|
|
26
|
+
# Return list of tuples
|
|
27
|
+
# [(data1, add_image_kwargs1), (data2, add_image_kwargs2)]
|
|
28
|
+
# Check the documentation for more information about the
|
|
29
|
+
# add_image_kwargs
|
|
30
|
+
# https://napari.org/stable/api/napari.Viewer.html#napari.Viewer.add_image
|
|
31
|
+
|
|
32
|
+
sample_path = get_mosaic_sample_data_path()
|
|
33
|
+
|
|
34
|
+
return read_mosaic([sample_path])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def drifting_timelapse_with_stage_shifts_no_overlap_2d():
|
|
38
|
+
|
|
39
|
+
sims = generate_tiled_dataset(
|
|
40
|
+
ndim=2, N_t=20, N_c=1,
|
|
41
|
+
tile_size=30, tiles_x=3, tiles_y=3, tiles_z=1,
|
|
42
|
+
drift_scale=2., shift_scale=2.,
|
|
43
|
+
overlap=0, zoom=8, dtype=np.uint8)
|
|
44
|
+
|
|
45
|
+
msims = [get_msim_from_sim(sim) for sim in sims]
|
|
46
|
+
|
|
47
|
+
layer_tuples = viewer_utils.create_image_layer_tuples_from_msims(
|
|
48
|
+
msims, transform_key=METADATA_TRANSFORM_KEY)
|
|
49
|
+
|
|
50
|
+
return layer_tuples
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def timelapse_with_stage_shifts_with_overlap_3d():
|
|
54
|
+
|
|
55
|
+
sims = generate_tiled_dataset(
|
|
56
|
+
ndim=3, N_t=20, N_c=1,
|
|
57
|
+
tile_size=30, tiles_x=3, tiles_y=3, tiles_z=1,
|
|
58
|
+
drift_scale=0., shift_scale=2.,
|
|
59
|
+
overlap=3, zoom=8, dtype=np.uint8)
|
|
60
|
+
|
|
61
|
+
msims = [get_msim_from_sim(sim) for sim in sims]
|
|
62
|
+
|
|
63
|
+
layer_tuples = viewer_utils.create_image_layer_tuples_from_msims(
|
|
64
|
+
msims, transform_key=METADATA_TRANSFORM_KEY)
|
|
65
|
+
|
|
66
|
+
return layer_tuples
|