Skip to content

Commit

Permalink
Add new widget for 2ch proj (#117)
Browse files Browse the repository at this point in the history
This PR addresses issue #116 and adds a new widget (Projection - 2
channel projection with reference channel) to perform projection of
2-channel images where one channel is the reference for the surface of
interest. This will typically be a "clean" surface marker eg Decad for
the apical surface and the second channel will be projected based on
that.

The new widget works as expected however I will need a bit of help in
addressing some pre-commit checks.

Closes #116

---------

Co-authored-by: Pablo Vicente Munuera <pablovm1990@gmail.com>
  • Loading branch information
giuliapaci and Pablo1990 committed Jul 31, 2023
1 parent 5a43a81 commit 5eebef3
Show file tree
Hide file tree
Showing 10 changed files with 225 additions and 45 deletions.
3 changes: 2 additions & 1 deletion src/epitools/_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
import PartSegCore.napari_plugins.loader

if TYPE_CHECKING:
from typing import Any, Callable
from collections.abc import Callable
from typing import Any

import numpy.typing as npt

Expand Down
42 changes: 28 additions & 14 deletions src/epitools/analysis/projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""

import itertools
from typing import Union

import numpy as np
import numpy.typing as npt
Expand Down Expand Up @@ -99,13 +100,13 @@ def _calculate_projected_image(
def calculate_projection(
input_image: npt.NDArray[np.float64],
smoothing_radius: float,
surface_smoothness_1: int,
surface_smoothness_2: int,
surface_smoothness: list[int],
cut_off_distance: int,
) -> npt.NDArray[np.float64]:
input_image_2: Union[npt.NDArray[np.float64], None] = None,
) -> tuple[npt.NDArray[np.float64], Union[npt.NDArray[np.float64], None]]:
"""Z projection using image interpolation.
Perfrom an iterative projection of 3D points along the z axis.
Perform an iterative projection of 3D points along the z axis.
An initial projection is performed to obtain the 'first estimated surface'.
Points furtherthan ``cut_off_distance`` from the first estimated surface will
Expand All @@ -121,16 +122,20 @@ def calculate_projection(
smoothing_radius:
Kernel radius for gaussian blur to apply before estimating the surface.
surface_smoothness_1:
Surface smoothness for 1st griddata estimation. Larger values will produce
greater smoothing.
surface_smoothness_2:
Surface smoothness for 2nd iteration of smoothing. Again, larger values
will produce greater smoothing.
surface_smoothness:
Surface smoothness for 1st and 2nd griddata estimation.
Larger values will produce greater smoothing.
cut_off_distance:
Cutoff distance in z-planes from the first estimated surface.
input_image_2:
if a second image is passed as argument the function will project a 2
channel image based on a reference channel (assumed to be the first image)
Numpy ndarray representation of 4D or 3D image stack. ``input_image`` is
assumed to have dimensions that correspond to TZYX or ZYX if it is 4D or 3D,
respectively.
Returns:
np.NDArray
Timeseries of the image stack projected onto a single plane in z. The
Expand All @@ -147,6 +152,9 @@ def calculate_projection(

# We will always have a single slice in the Z dimension
t_interp = np.zeros((t_size, SINGLE_SLICE, y_size, x_size))
t_interp_2: Union[npt.NDArray, None] = np.zeros(
(t_size, SINGLE_SLICE, y_size, x_size)
)

for t in range(t_size):
smoothed_t = smoothed_imstack[t]
Expand All @@ -162,7 +170,7 @@ def calculate_projection(
mask = confidencemap > confthres
max_indices_confthres = max_indices * mask
z_interp = _interpolate(
max_indices_confthres, x_size, y_size, surface_smoothness_1
max_indices_confthres, x_size, y_size, surface_smoothness[0]
)

# given the height locations of the surface (z_interp) compute the difference
Expand All @@ -180,8 +188,14 @@ def calculate_projection(
# (max_indices_cut) this is to make sure that the highest intensity points will
# be selected from the correct surface (The coarse grained estimate could
# potentially approximate the origin of the point to another plane)
z_interp = _interpolate(max_indices_cut, x_size, y_size, surface_smoothness_2)
z_interp = _interpolate(max_indices_cut, x_size, y_size, surface_smoothness[1])

t_interp[t] = _calculate_projected_image(input_image[t], z_interp)

return t_interp
if input_image_2 is None or t_interp_2 is None:
t_interp_2 = None
else:
# second channel projected based on the first one (reference channel)
t_interp_2[t] = _calculate_projected_image(input_image_2[t], z_interp)

return t_interp, t_interp_2
99 changes: 78 additions & 21 deletions src/epitools/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
__all__ = [
"create_projection_widget",
"create_segmentation_widget",
"create_projection_2ch_widget",
"create_cell_statistics_widget",
]

Expand All @@ -40,42 +41,98 @@ def create_projection_widget() -> magicgui.widgets.Container:
lambda: run_projection(
image=projection_widget.input_image.value,
smoothing_radius=projection_widget.smoothing_radius.value,
surface_smoothness_1=projection_widget.surface_smoothness_1.value,
surface_smoothness_2=projection_widget.surface_smoothness_2.value,
surface_smoothness=[
projection_widget.surface_smoothness_1.value,
projection_widget.surface_smoothness_2.value,
],
cutoff_distance=projection_widget.cutoff_distance.value,
),
)

return projection_widget


def create_projection_2ch_widget() -> magicgui.widgets.Container:
"""Create a widget to project a 2 channel, 4d timeseries (TZYX)
along the z dimension based on a reference channel"""

projection_2ch_widget = epitools.widgets.create_projection_2ch_widget()

# Project the timeseries when pressing the 'Run' button
projection_2ch_widget.run.changed.connect(
lambda: run_projection(
image=projection_2ch_widget.refchannel.value,
smoothing_radius=projection_2ch_widget.smoothing_radius.value,
surface_smoothness=[
projection_2ch_widget.surface_smoothness_1.value,
projection_2ch_widget.surface_smoothness_2.value,
],
cutoff_distance=projection_2ch_widget.cutoff_distance.value,
second_image=projection_2ch_widget.channel.value,
),
)

return projection_2ch_widget


def run_projection(
image: napari.layers.Image,
smoothing_radius,
surface_smoothness_1,
surface_smoothness_2,
surface_smoothness,
cutoff_distance,
second_image: napari.layers.Image | None = None,
) -> None:
"""Project a 4d timeseries along the z dimension"""

projected_data = epitools.analysis.calculate_projection(
image.data,
smoothing_radius,
surface_smoothness_1,
surface_smoothness_2,
cutoff_distance,
)
"If second_image is not empty, project 2 channels based on reference channel"
if second_image is not None:
projected_data_1, projected_data_2 = epitools.analysis.calculate_projection(
image.data,
smoothing_radius,
surface_smoothness,
cutoff_distance,
second_image.data,
)

viewer = napari.current_viewer()
viewer.add_image(
data=projected_data,
name="Projection",
scale=image.scale,
translate=image.translate,
rotate=image.rotate,
plane=image.plane,
metadata=image.metadata,
)
viewer = napari.current_viewer()
viewer.add_image(
data=projected_data_1,
name="Projection_ch1",
scale=image.scale,
translate=image.translate,
rotate=image.rotate,
plane=image.plane,
metadata=image.metadata,
)

viewer.add_image(
data=projected_data_2,
name="Projection_ch2",
scale=image.scale,
translate=image.translate,
rotate=image.rotate,
plane=image.plane,
metadata=image.metadata,
)

else:
projected_data, _ = epitools.analysis.calculate_projection(
image.data,
smoothing_radius,
surface_smoothness,
cutoff_distance,
)

viewer = napari.current_viewer()
viewer.add_image(
data=projected_data,
name="Projection",
scale=image.scale,
translate=image.translate,
rotate=image.rotate,
plane=image.plane,
metadata=image.metadata,
)


def create_segmentation_widget() -> magicgui.widgets.Container:
Expand Down
5 changes: 5 additions & 0 deletions src/epitools/napari.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ contributions:
- id: epitools.projection_widget
python_name: epitools.main:create_projection_widget
title: Epitools Projection Widget
- id: epitools.projection_2ch_widget
python_name: epitools.main:create_projection_2ch_widget
title: Epitools Projection Widget 2 Channels
- id: epitools.segmentation_widget
python_name: epitools.main:create_segmentation_widget
title: Epitools Segmentation Widget
Expand All @@ -42,6 +45,8 @@ contributions:
- command: epitools.projection_widget
# autogenerate: true <- for use with assistant
display_name: Projection (selective plane)
- command: epitools.projection_2ch_widget
display_name: Projection (2 channel with reference channel)
- command: epitools.segmentation_widget
display_name: Segmentation (local minima seeded watershed)
- command: epitools.cell_statistics_widget
Expand Down
2 changes: 2 additions & 0 deletions src/epitools/widgets/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from epitools.widgets import dialogue
from epitools.widgets.cell_statistics import create_cell_statistics_widget
from epitools.widgets.projection import create_projection_widget
from epitools.widgets.projection_2ch import create_projection_2ch_widget
from epitools.widgets.segmentation import create_segmentation_widget

__all__ = [
"dialogue",
"create_cell_statistics_widget",
"create_projection_widget",
"create_projection_2ch_widget",
"create_segmentation_widget",
]
103 changes: 103 additions & 0 deletions src/epitools/widgets/projection_2ch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import magicgui.widgets
import napari

__all__ = [
"create_projection_2ch_widget",
]


def create_projection_2ch_widget() -> magicgui.widgets.Container:
"""Create a widget for projecting a 2 channel, 3d timeseries to a
2d timeseries based on a reference channel"""

refchannel_tooltip = (
"Select a 'Reference' channel to project along the z-dimension."
)
refchannel = magicgui.widgets.create_widget(
annotation=napari.layers.Image,
name="refchannel",
label="reference channel",
options={"tooltip": refchannel_tooltip},
)

channel_tooltip = "Select a second channel to project along the \
z-dimension based on the 'Reference'."
channel = magicgui.widgets.create_widget(
annotation=napari.layers.Image,
name="channel",
label="second channel",
options={"tooltip": channel_tooltip},
)

smoothing_radius_tooltip = (
"Kernel radius for gaussian blur to apply before estimating the surface."
)
smoothing_radius = magicgui.widgets.create_widget(
value=1,
name="smoothing_radius",
label="smoothing radius",
widget_type="FloatSpinBox",
options={
"tooltip": smoothing_radius_tooltip,
},
)

surface_smoothness_1_tooltip = (
"Surface smoothness for 1st griddata estimation, larger means smoother."
)
surface_smoothness_1 = magicgui.widgets.create_widget(
value=5,
name="surface_smoothness_1",
label="surface smoothness 1",
widget_type="SpinBox",
options={
"tooltip": surface_smoothness_1_tooltip,
},
)

surface_smoothness_2_tooltip = (
"Surface smoothness for 2nd griddata estimation, larger means smoother."
)
surface_smoothness_2 = magicgui.widgets.create_widget(
value=5,
name="surface_smoothness_2",
label="surface smoothness 2",
widget_type="SpinBox",
options={
"tooltip": surface_smoothness_2_tooltip,
},
)

cutoff_distance_tooltip = (
"Cutoff distance in z-planes from the 1st estimated surface."
)
cutoff_distance = magicgui.widgets.create_widget(
value=3,
name="cutoff_distance",
label="z cutoff distance",
widget_type="SpinBox",
options={
"tooltip": cutoff_distance_tooltip,
},
)

run_button_tooltip = "Perform the projection"
run_button = magicgui.widgets.create_widget(
name="run",
label="Run",
widget_type="PushButton",
options={"tooltip": run_button_tooltip},
)

return magicgui.widgets.Container(
widgets=[
refchannel,
channel,
smoothing_radius,
surface_smoothness_1,
surface_smoothness_2,
cutoff_distance,
run_button,
],
scrollable=False,
)
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import Callable
from collections.abc import Callable

import pytest

Expand Down
2 changes: 1 addition & 1 deletion tests/test_cell_statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Callable
from collections.abc import Callable


def test_add_cell_statistics_widget(
Expand Down
Loading

0 comments on commit 5eebef3

Please sign in to comment.