Skip to content

Commit 9c628a0

Browse files
Merge pull request #171 from haesleinhuepf/blend
Add blend and animate_blend
2 parents 6766b87 + abd5c05 commit 9c628a0

File tree

7 files changed

+327
-5
lines changed

7 files changed

+327
-5
lines changed

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ stackview.imshow(labels, plot=axs[2], alpha=0.4, title='image + labels')
131131

132132
### Static animations
133133

134-
The `animate` and `animate_curtain` functions can be used to store animations of image stacks / images blended over each other as gif to disk.
134+
The `animate`, `animate_curtain` and `animate_blend` functions can be used to store animations of image stacks / images blended over each other as gif to disk.
135135

136136
```python
137137
stackview.animate(blobs_images, frame_delay_ms=50)
@@ -198,6 +198,17 @@ stackview.curtain(slice_image, labels)
198198

199199
![](https://raw.githubusercontent.com/haesleinhuepf/stackview/main/docs/images/demo_curtain3.gif)
200200

201+
### Blend
202+
203+
You can blend images interactively like this:
204+
205+
```
206+
stackview.blend(image, label_image)
207+
```
208+
209+
![](https://raw.githubusercontent.com/haesleinhuepf/stackview/main/docs/images/blend.gif)
210+
211+
201212
### Side-by-side view
202213

203214
A side-by-side view for colocalization visualization is also available.

docs/blend.ipynb

Lines changed: 125 additions & 0 deletions
Large diffs are not rendered by default.

docs/images/blend.gif

3.14 MB
Loading

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setuptools.setup(
77
name="stackview",
8-
version="0.17.0",
8+
version="0.18.0",
99
author="Robert Haase",
1010
author_email="[email protected]",
1111
description="Interactive image stack viewing in jupyter notebooks",

stackview/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "0.17.0"
1+
__version__ = "0.18.0"
22

33
from ._static_view import jupyter_displayable_output, insight
44
from ._utilities import merge_rgb
@@ -16,7 +16,7 @@
1616
from ._switch import switch
1717
from ._colormaps import create_colormap
1818
from ._imshow import imshow
19-
from ._animate import animate, animate_curtain
19+
from ._animate import animate, animate_curtain, animate_blend
2020
from ._display_range import display_range
2121
from ._scatterplot import scatterplot
2222
from ._grid import grid
@@ -25,3 +25,4 @@
2525
from ._wordcloudplot import wordcloudplot
2626
from ._add_bounding_boxes import add_bounding_boxes
2727
from ._histogram import histogram
28+
from ._blend import blend

stackview/_animate.py

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
def animate(timelapse, filename:str=None, overwrite_file:bool=True, frame_delay_ms:int=150, num_loops:int=1000, colormap=None, display_min=None, display_max=None, zoom_factor:float=1.0):
32
"""
43
Create an animated GIF from a list of 2D images and return it as Markdown object, that can be shown in Jupyter notebooks.
@@ -148,3 +147,82 @@ def animate_curtain(timelapse, timelapse_curtain,
148147
images.append(image_slice)
149148

150149
return animate(np.asarray(images), filename=filename, overwrite_file=overwrite_file, frame_delay_ms=frame_delay_ms, num_loops=num_loops, zoom_factor=zoom_factor)
150+
151+
def animate_blend(
152+
image1,
153+
image2,
154+
num_steps: int = 20,
155+
zoom_factor: float = 1.0,
156+
colormap1: str = None,
157+
display_min1: float = None,
158+
display_max1: float = None,
159+
colormap2: str = None,
160+
display_min2: float = None,
161+
display_max2: float = None,
162+
filename: str = None,
163+
overwrite_file: bool = True,
164+
frame_delay_ms: int = 150,
165+
num_loops: int = 1000
166+
):
167+
"""Create an animated GIF showing a smooth blend transition between two images.
168+
169+
Parameters
170+
----------
171+
image1 : image
172+
First image to blend
173+
image2 : image
174+
Second image to blend
175+
num_steps : int, optional
176+
Number of steps in the animation
177+
zoom_factor : float, optional
178+
Allows showing the image larger (> 1) or smaller (<1)
179+
colormap1 : str, optional
180+
Matplotlib colormap name or "pure_green", "pure_magenta", ... for first image
181+
display_min1 : float, optional
182+
Lower bound of properly shown intensities for first image
183+
display_max1 : float, optional
184+
Upper bound of properly shown intensities for first image
185+
colormap2 : str, optional
186+
Matplotlib colormap name or "pure_green", "pure_magenta", ... for second image
187+
display_min2 : float, optional
188+
Lower bound of properly shown intensities for second image
189+
display_max2 : float, optional
190+
Upper bound of properly shown intensities for second image
191+
filename : str, optional
192+
Name of the file where the animation will be saved
193+
overwrite_file : bool, optional
194+
Overwrite the file if it already exists. Default: True
195+
frame_delay_ms : int, optional
196+
Delay between frames in milliseconds
197+
num_loops : int, optional
198+
Number of loops in the animation
199+
200+
Returns
201+
-------
202+
An HTML object that can be displayed in a Jupyter notebook.
203+
"""
204+
import numpy as np
205+
from ._image_widget import _img_to_rgb
206+
207+
if 'cupy.ndarray' in str(type(image1)):
208+
image1 = image1.get()
209+
210+
if 'cupy.ndarray' in str(type(image2)):
211+
image2 = image2.get()
212+
213+
# Convert images to RGB
214+
image1_rgb = _img_to_rgb(image1, colormap=colormap1, display_min=display_min1, display_max=display_max1)
215+
image2_rgb = _img_to_rgb(image2, colormap=colormap2, display_min=display_min2, display_max=display_max2)
216+
217+
# Create frames with different blend factors
218+
frames = []
219+
for i in range(num_steps):
220+
blend_factor = i / (num_steps - 1)
221+
blended = (1 - blend_factor) * image1_rgb + blend_factor * image2_rgb
222+
frames.append(blended.astype(np.uint8))
223+
224+
# Add reverse frames to create a smooth loop
225+
frames.extend(frames[::-1])
226+
227+
return animate(np.asarray(frames), filename=filename, overwrite_file=overwrite_file,
228+
frame_delay_ms=frame_delay_ms, num_loops=num_loops, zoom_factor=zoom_factor)

stackview/_blend.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
def blend(
2+
image1,
3+
image2,
4+
slice_number: int = None,
5+
axis: int = 0,
6+
continuous_update: bool = True,
7+
blend_factor: float = 0.5,
8+
zoom_factor: float = 1.0,
9+
zoom_spline_order: int = 0,
10+
colormap1: str = None,
11+
display_min1: float = None,
12+
display_max1: float = None,
13+
colormap2: str = None,
14+
display_min2: float = None,
15+
display_max2: float = None
16+
):
17+
"""Show two images blended together with a slider to control the blend factor.
18+
19+
Parameters
20+
----------
21+
image1 : image
22+
First image to blend
23+
image2 : image
24+
Second image to blend
25+
slice_number : int, optional
26+
Slice-position in case we are looking at an image stack
27+
axis : int, optional
28+
This parameter is obsolete. If you want to show any other axis than the first, you need to transpose the image before, e.g. using np.swapaxes().
29+
continuous_update : bool, optional
30+
Update the image while dragging the mouse, default: True
31+
blend_factor: float, optional
32+
Controls the blend between images (0 = only image1, 1 = only image2)
33+
zoom_factor: float, optional
34+
Allows showing the image larger (> 1) or smaller (<1)
35+
zoom_spline_order: int, optional
36+
Spline order used for interpolation (default=0, nearest-neighbor)
37+
colormap1: str, optional
38+
Matplotlib colormap name or "pure_green", "pure_magenta", ... for first image
39+
display_min1: float, optional
40+
Lower bound of properly shown intensities for first image
41+
display_max1: float, optional
42+
Upper bound of properly shown intensities for first image
43+
colormap2: str, optional
44+
Matplotlib colormap name or "pure_green", "pure_magenta", ... for second image
45+
display_min2: float, optional
46+
Lower bound of properly shown intensities for second image
47+
display_max2: float, optional
48+
Upper bound of properly shown intensities for second image
49+
50+
Returns
51+
-------
52+
An ipywidget with an image display and a slider.
53+
"""
54+
import ipywidgets
55+
from ._image_widget import ImageWidget
56+
from ._slice_viewer import _SliceViewer
57+
import numpy as np
58+
from ._utilities import _no_resize
59+
from ._uint_field import intSlider
60+
61+
if 'cupy.ndarray' in str(type(image1)):
62+
image1 = image1.get()
63+
64+
if 'cupy.ndarray' in str(type(image2)):
65+
image2 = image2.get()
66+
67+
# setup user interface for changing the blend factor
68+
blend_slider = intSlider(
69+
value=blend_factor,
70+
min=0,
71+
max=100,
72+
continuous_update=continuous_update,
73+
description="Blend"
74+
)
75+
76+
viewer = None
77+
from ._image_widget import _img_to_rgb
78+
79+
def transform_image():
80+
image_slice1 = _img_to_rgb(viewer.get_view_slice(), colormap=colormap1, display_min=display_min1, display_max=display_max1).copy()
81+
image_slice2 = _img_to_rgb(viewer.get_view_slice(image2), colormap=colormap2, display_min=display_min2, display_max=display_max2)
82+
blend_value = blend_slider.value / 100
83+
blended_image = (1 - blend_value) * image_slice1 + blend_value * image_slice2
84+
return blended_image
85+
86+
viewer = _SliceViewer(image1, continuous_update=continuous_update, zoom_factor=zoom_factor,
87+
zoom_spline_order=zoom_spline_order, colormap=colormap1, display_min=display_min1,
88+
display_max=display_max1)
89+
90+
view = viewer.view
91+
sliders = viewer.slice_slider
92+
93+
# event handler when the user changed something:
94+
def configuration_updated(event=None):
95+
view.data = transform_image()
96+
97+
configuration_updated(None)
98+
99+
# connect user interface with event
100+
blend_slider.observe(configuration_updated)
101+
102+
# connect user interface with event
103+
viewer.observe(configuration_updated)
104+
result = _no_resize(ipywidgets.VBox([_no_resize(view), sliders, blend_slider]))
105+
result.update = configuration_updated
106+
result.viewer = viewer
107+
return result

0 commit comments

Comments
 (0)