Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,7 @@ venv/
**/_version.py

src/napari_stitcher/_playground
doc_old
doc_old
_playground

uv.lock
11 changes: 8 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ classifiers = [
requires-python = ">=3.11"
dependencies = [
"dask",
"joblib",
"magicgui",
"multiscale_spatial_image",
"multiview-stitcher[czi, itk-elastix] >=0.1.49",
"multiview-stitcher[czi,itk-elastix]>=0.1.59",
"napari",
"numpy >=1.18",
"qtpy",
Expand All @@ -48,19 +49,23 @@ dynamic = ["version"]
[project.optional-dependencies]
testing_no_gui = [
"tox",
"multiview-stitcher[czi] >=0.1.47",
"pytest",
"pytest-cov",
"pytest-qt",
"napari-ome-zarr",
]
testing = [
"tox",
"multiview-stitcher[czi] >=0.1.47",
"pytest",
"pytest-cov",
"pytest-qt",
"napari",
"pyqt5",
"napari-ome-zarr",
]
playground = [
"napari[all]",
"napari-ome-zarr",
]

[project.entry-points."napari.manifest"]
Expand Down
45 changes: 40 additions & 5 deletions src/napari_stitcher/_mosaic_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,33 @@
import napari


def _get_layer_combined_transform(layer):
ndim = len(layer.translate)

affine = np.array(layer.affine.affine_matrix)
affine = affine[-(ndim + 1):, -(ndim + 1):]

translate = np.eye(ndim + 1)
translate[:-1, -1] = layer.translate

return affine @ translate


def _get_layer_translation(layer):
return _get_layer_combined_transform(layer)[:-1, -1][-2:].copy()


def _clear_layer_affine_translation(layer):
affine = np.array(layer.affine.affine_matrix)
translation = affine[-3:-1, -1]

if np.allclose(translation, 0):
return

affine[-3:-1, -1] = 0
layer.affine = affine


class MosaicQWidget(QWidget):
# your QWidget.__init__ can optionally request the napari viewer instance
# in one of two ways:
Expand Down Expand Up @@ -151,19 +178,27 @@ def arrange_tiles(self):
)

if self.input_order.value == 'forward':
l0_translate = self.viewer.layers[0].translate[-2:]
l0_translate = _get_layer_translation(self.viewer.layers[0])
else:
l0_translate = self.viewer.layers[-1].translate[-2:]
l0_translate = _get_layer_translation(self.viewer.layers[-1])

for l in self.viewer.layers:
view = _utils.get_str_unique_to_view_from_layer_name(l.name)
itile = view_order.index(view)
tile_index = tile_indices[itile]
# print(f'Layer {l.name} -> tile index: {tile_index}')
l.translate[-2:] = [
l0_translate[0] + tile_index[1] * tile_w - tile_index[1] * tile_w * self.overlap.value,
l0_translate[1] + tile_index[0] * tile_h - tile_index[0] * tile_h * self.overlap.value
tile_stride_w = tile_w * (1 - self.overlap.value)
tile_stride_h = tile_h * (1 - self.overlap.value)
grid_position = [
l0_translate[0] + tile_index[1] * tile_stride_w,
l0_translate[1] + tile_index[0] * tile_stride_h,
]

_clear_layer_affine_translation(l)

translate = np.array(l.translate)
translate[-2:] = grid_position
l.translate = translate
l.refresh()


Expand Down
133 changes: 133 additions & 0 deletions src/napari_stitcher/_sample_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

Replace code below according to your needs.
"""

from __future__ import annotations

import numpy as np
Expand All @@ -19,6 +20,108 @@
from multiview_stitcher.io import METADATA_TRANSFORM_KEY
from multiview_stitcher.sample_data import generate_tiled_dataset
from multiview_stitcher.msi_utils import get_msim_from_sim
from multiview_stitcher import msi_utils, spatial_image_utils

_DIGIT_SEGMENTS = {
0: (
"top",
"upper_left",
"upper_right",
"lower_left",
"lower_right",
"bottom",
),
1: ("upper_right", "lower_right"),
2: ("top", "upper_right", "middle", "lower_left", "bottom"),
3: ("top", "upper_right", "middle", "lower_right", "bottom"),
4: ("upper_left", "upper_right", "middle", "lower_right"),
5: ("top", "upper_left", "middle", "lower_right", "bottom"),
6: ("top", "upper_left", "middle", "lower_left", "lower_right", "bottom"),
7: ("top", "upper_right", "lower_right"),
8: (
"top",
"upper_left",
"upper_right",
"middle",
"lower_left",
"lower_right",
"bottom",
),
9: ("top", "upper_left", "upper_right", "middle", "lower_right", "bottom"),
}


def _draw_segment(image, segment, value):
"""Draw one segment of a seven-segment digit into ``image``."""
size = image.shape[0]
margin = size // 6
thickness = max(size // 12, 3)
y_mid = size // 2
y_bottom = size - margin - thickness
x_right = size - margin - thickness

segments = {
"top": (
slice(margin, margin + thickness),
slice(margin, size - margin),
),
"middle": (
slice(y_mid - thickness // 2, y_mid + (thickness + 1) // 2),
slice(margin, size - margin),
),
"bottom": (
slice(y_bottom, y_bottom + thickness),
slice(margin, size - margin),
),
"upper_left": (
slice(margin, y_mid),
slice(margin, margin + thickness),
),
"upper_right": (
slice(margin, y_mid),
slice(x_right, x_right + thickness),
),
"lower_left": (
slice(y_mid, y_bottom + thickness),
slice(margin, margin + thickness),
),
"lower_right": (
slice(y_mid, y_bottom + thickness),
slice(x_right, x_right + thickness),
),
}

image[segments[segment]] = value


def _digit_image(digit, size, seed):
"""Create a noisy square image with a large seven-segment digit."""
rng = np.random.default_rng(seed)
image = rng.integers(5, 70, size=(size, size), dtype=np.uint8)

for segment in _DIGIT_SEGMENTS[digit % 10]:
_draw_segment(image, segment, value=240)

return image


def _multiscale_digit_msim(origin, level_images, tile_size=500):
"""Build one prepositioned multiscale tile from per-level digit images."""
sims = []
for image in level_images:
pixel_size = tile_size / image.shape[0]
sim = spatial_image_utils.get_sim_from_array(
image,
dims=["y", "x"],
scale={"y": pixel_size, "x": pixel_size},
translation=origin,
c_coords=["digits"],
t_coords=[0],
transform_key=METADATA_TRANSFORM_KEY,
)
sims.append(sim)

return msi_utils.get_msim_from_sims(sims)


def make_sample_data():
Expand Down Expand Up @@ -64,3 +167,33 @@ def timelapse_with_stage_shifts_with_overlap_3d():
msims, transform_key=METADATA_TRANSFORM_KEY)

return layer_tuples


def multiscale_digits():
"""Four 500x500 tiles whose pyramid levels contain scale-index digits."""
tile_size = 500
level_sizes = [500, 250, 125]
level_images = [
_digit_image(digit=iscale, size=size, seed=iscale)
for iscale, size in enumerate(level_sizes)
]

msims = []
for tile_y, tile_x in np.ndindex(2, 2):
origin = {"y": tile_y * tile_size, "x": tile_x * tile_size}
msims.append(_multiscale_digit_msim(origin, level_images, tile_size))

layer_tuples = viewer_utils.create_image_layer_tuples_from_msims(
msims,
positional_cmaps=False,
name_prefix="multiscale_digits_tile",
transform_key=METADATA_TRANSFORM_KEY,
contrast_limits=[0, 255],
)

# Keep the tiles visually identical; only their grid positions differ.
for _, add_image_kwargs, _ in layer_tuples:
add_image_kwargs["colormap"] = "gray"
add_image_kwargs["gamma"] = 1.0

return layer_tuples
Loading
Loading