Animation in napari#
Creating a short animation is a great way to introduce your data during a presentation and provide insight into the image analysis. It is especially valuable while working with 3D data that can otherwise be presented only in a form of 2D screenshots.
To grasp the process of creating an animation, it is essential to understand the concept of keyframes (https://en.wikipedia.org/wiki/Key_frame ). Keyframes can be likened to anchor points that steer an animation. Thus, by defining the keyframes, you determine what will be portrayed. The transitions between keyframes are smooth and are automatically filled in for you. Keyframes in a napari animation can capture such events as change of a frame, zoom, camera angle (3D projections) or even brightness of an image. The speed of the animation is controlled by the number of in-between frames (‘Steps’) inserted between the key frames.
Napari provides a simple way to create animations in both manual (interactive) and programmatic (scripting) way through the Animation Plugin.
In this tutorial we will create a simple animation of FIB-SEM data.
Setup#
# this cell is required to run these notebooks on Binder. Make sure that you also have a desktop tab open.
import os
if 'BINDER_SERVICE_HOST' in os.environ:
os.environ['DISPLAY'] = ':1.0'
# Instal animation plugin
# This step has to be performed only once in a given environment.
# You can remove this cell or use '#' to comment the code out after a successful installation.
!pip install napari-animation
# No problem if you execute it again though, Python will just tell you 'Requirement already satisfied'.
import os
import zarr
import dask.array as da
import napari
from napari_animation import Animation
from napari_animation.easing import Easing
Reading in data#
Get data from OpenOrganelle.
group = zarr.open(store=zarr.N5FSStore('s3://janelia-cosem-datasets/jrc_mus-kidney/jrc_mus-kidney.n5', anon=True)) # access the root of the n5 container
# Get access to all resolution levels of the data.
img_data = [
da.from_zarr(group[f'em/fibsem-uint8/s{i}'])
for i in range(0, 4)
]
# Get label data for all resolution levels
label_data = [
da.from_zarr(group[f'labels/empanada-mito_seg/s{i}'])
for i in range(0, 4)
]
Get an example cube of the data and load it into memory.
cropped_img = img_data[3][50:150:2,400:500,1000:1100].compute()
print(cropped_img.shape)
# Note that the segmentations are downsampled by 2, so we need to use a different scale for labels
cropped_label = label_data[2][50:150:2,400:500,1000:1100].compute()
print(cropped_label.shape)
(50, 100, 100)
(50, 100, 100)
Visualize in napari#
viewer = napari.Viewer()
viewer.add_image(cropped_img)
Use animation plugin interactively#
From the upper menu choose Plugins - wizard (napari-animation)
With the bottom slides choose first slice #0
Click ‘Capture’ to set it as a first keyframe
Choose number of Steps to 30
Move slider to slice #40
Capture the keyframe
Move slider to slice #49
Choose number of Steps to 60
Capture the keyframe
Save your first animation
Use script to record an animation#
# define pathway to save your animation
save_path = 'my_animation_from_script.mp4'
Note
If you don’t specify a full pathway but only a name of the file it will be saved in your current working directory (cwd). If you don’t know where it is use os.getcwd()
animation = Animation(viewer)
viewer.reset_view()
viewer.dims.current_step = (0, 0, 0)
animation.capture_keyframe()
viewer.dims.current_step = (40, 0, 0)
animation.capture_keyframe(steps=40)
viewer.dims.current_step = (49, 0, 0)
animation.capture_keyframe(steps=70)
animation.animate(save_path, canvas_only=True, fps=24)
Rendering frames...
0%| | 0/111 [00:00<?, ?it/s]
IMAGEIO FFMPEG_WRITER WARNING: input image is not divisible by macro_block_size=16, resizing from (966, 922) to (976, 928) to ensure video compatibility with most codecs and players. To prevent resizing, make your input image divisible by the macro_block_size or set the macro_block_size to 1 (risking incompatibility).
1%| | 1/111 [00:00<00:11, 9.94it/s]
[swscaler @ 0x68e0bc0] Warning: data is not aligned! This can lead to a speed loss
5%|▍ | 5/111 [00:00<00:04, 26.17it/s]
9%|▉ | 10/111 [00:00<00:02, 34.39it/s]
14%|█▎ | 15/111 [00:00<00:02, 38.09it/s]
18%|█▊ | 20/111 [00:00<00:02, 40.23it/s]
23%|██▎ | 25/111 [00:00<00:02, 41.44it/s]
27%|██▋ | 30/111 [00:00<00:01, 41.57it/s]
32%|███▏ | 35/111 [00:00<00:01, 42.11it/s]
36%|███▌ | 40/111 [00:01<00:01, 42.32it/s]
41%|████ | 45/111 [00:01<00:01, 40.59it/s]
45%|████▌ | 50/111 [00:01<00:02, 29.47it/s]
49%|████▊ | 54/111 [00:01<00:02, 28.00it/s]
52%|█████▏ | 58/111 [00:01<00:01, 27.75it/s]
55%|█████▍ | 61/111 [00:01<00:01, 26.36it/s]
58%|█████▊ | 64/111 [00:01<00:01, 25.95it/s]
60%|██████ | 67/111 [00:02<00:01, 24.26it/s]
63%|██████▎ | 70/111 [00:02<00:01, 23.37it/s]
66%|██████▌ | 73/111 [00:02<00:01, 22.20it/s]
68%|██████▊ | 76/111 [00:02<00:01, 20.82it/s]
71%|███████ | 79/111 [00:02<00:01, 21.45it/s]
74%|███████▍ | 82/111 [00:02<00:01, 22.39it/s]
77%|███████▋ | 85/111 [00:03<00:01, 19.86it/s]
79%|███████▉ | 88/111 [00:03<00:01, 20.56it/s]
82%|████████▏ | 91/111 [00:03<00:00, 21.74it/s]
85%|████████▍ | 94/111 [00:03<00:00, 22.97it/s]
88%|████████▊ | 98/111 [00:03<00:00, 25.23it/s]
91%|█████████ | 101/111 [00:03<00:00, 25.21it/s]
94%|█████████▎| 104/111 [00:03<00:00, 26.27it/s]
97%|█████████▋| 108/111 [00:03<00:00, 27.92it/s]
100%|██████████| 111/111 [00:03<00:00, 27.96it/s]
Script from the community#
Check out this demo from the community that was collaboratively developed on the by Callum Tromans-Coia, Lorenzo Gaifas, and Alister Burt. For more details see https://forum.image.sc/t/creating-an-animation-for-visualisation-of-3d-labels-emerging-from-a-2d-plane/77517/6.
viewer = napari.Viewer(ndisplay=3)
image_layer = viewer.add_image(cropped_img, name="image", depiction="plane", blending='additive')
labels_layer = viewer.add_labels(cropped_label, name="labels")
viewer.camera.angles = (-18.23797054423494, 41.97404742075617, 141.96173085742896)
/opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/napari/plugins/_plugin_manager.py:560: UserWarning: Plugin 'napari_skimage_regionprops2' has already registered a function widget 'duplicate current frame' which has now been overwritten
warn(message=warn_message)
animation = Animation(viewer)
labels_layer.visible = False
image_layer.plane.position = (0, 0, 0)
animation.capture_keyframe(steps=30)
image_layer.plane.position = (49, 0, 0)
animation.capture_keyframe(steps=30)
image_layer.plane.position = (0, 0, 0)
animation.capture_keyframe(steps=30)
# define a function to replace label data when viewer position changes
def replace_labels_data():
z_cutoff = int(image_layer.plane.position[0])
new_labels_data = cropped_label.copy()
new_labels_data[z_cutoff:] = 0
labels_layer.data = new_labels_data
image_layer.plane.events.position.connect(replace_labels_data)
labels_layer.visible = True
labels_layer.experimental_clipping_planes = [{
"position": (0, 0, 0),
"normal": (-1, 0, 0), # point up in z (i.e: show stuff above plane)
}]
image_layer.plane.position = (49, 0, 0)
# access first plane, since it's a list
labels_layer.experimental_clipping_planes[0].position = (49, 0, 0)
animation.capture_keyframe(steps=30)
image_layer.plane.position = (0, 0, 0)
animation.capture_keyframe(steps=30)
animation.animate("animation_labels.mp4", canvas_only=True)
image_layer.plane.position = (0, 0, 0)
Rendering frames...
0%| | 0/121 [00:00<?, ?it/s]
IMAGEIO FFMPEG_WRITER WARNING: input image is not divisible by macro_block_size=16, resizing from (959, 565) to (960, 576) to ensure video compatibility with most codecs and players. To prevent resizing, make your input image divisible by the macro_block_size or set the macro_block_size to 1 (risking incompatibility).
[swscaler @ 0x5872bc0] Warning: data is not aligned! This can lead to a speed loss
2%|▏ | 2/121 [00:00<00:07, 16.74it/s]
4%|▍ | 5/121 [00:00<00:05, 19.95it/s]
7%|▋ | 8/121 [00:00<00:05, 21.14it/s]
9%|▉ | 11/121 [00:00<00:05, 21.72it/s]
12%|█▏ | 14/121 [00:00<00:04, 21.90it/s]
14%|█▍ | 17/121 [00:01<00:07, 14.39it/s]
17%|█▋ | 20/121 [00:01<00:06, 16.35it/s]
19%|█▉ | 23/121 [00:01<00:05, 17.96it/s]
21%|██▏ | 26/121 [00:01<00:04, 19.24it/s]
24%|██▍ | 29/121 [00:01<00:04, 20.17it/s]
26%|██▋ | 32/121 [00:01<00:04, 20.85it/s]
29%|██▉ | 35/121 [00:01<00:04, 21.38it/s]
31%|███▏ | 38/121 [00:01<00:03, 21.64it/s]
34%|███▍ | 41/121 [00:02<00:03, 21.91it/s]
36%|███▋ | 44/121 [00:02<00:03, 21.63it/s]
39%|███▉ | 47/121 [00:02<00:03, 21.77it/s]
41%|████▏ | 50/121 [00:02<00:03, 21.84it/s]
44%|████▍ | 53/121 [00:02<00:03, 21.98it/s]
46%|████▋ | 56/121 [00:02<00:02, 22.18it/s]
49%|████▉ | 59/121 [00:02<00:02, 22.28it/s]
51%|█████ | 62/121 [00:03<00:04, 12.12it/s]
53%|█████▎ | 64/121 [00:04<00:07, 7.40it/s]
55%|█████▍ | 66/121 [00:04<00:09, 5.59it/s]
56%|█████▌ | 68/121 [00:05<00:11, 4.69it/s]
57%|█████▋ | 69/121 [00:05<00:11, 4.38it/s]
58%|█████▊ | 70/121 [00:05<00:12, 4.11it/s]
59%|█████▊ | 71/121 [00:06<00:12, 3.88it/s]
60%|█████▉ | 72/121 [00:06<00:13, 3.71it/s]
60%|██████ | 73/121 [00:06<00:13, 3.58it/s]
61%|██████ | 74/121 [00:07<00:13, 3.46it/s]
62%|██████▏ | 75/121 [00:07<00:13, 3.39it/s]
63%|██████▎ | 76/121 [00:07<00:13, 3.34it/s]
64%|██████▎ | 77/121 [00:08<00:13, 3.29it/s]
64%|██████▍ | 78/121 [00:08<00:13, 3.27it/s]
65%|██████▌ | 79/121 [00:08<00:12, 3.25it/s]
66%|██████▌ | 80/121 [00:09<00:12, 3.24it/s]
67%|██████▋ | 81/121 [00:09<00:12, 3.23it/s]
68%|██████▊ | 82/121 [00:09<00:12, 3.22it/s]
69%|██████▊ | 83/121 [00:09<00:11, 3.22it/s]
69%|██████▉ | 84/121 [00:10<00:11, 3.21it/s]
70%|███████ | 85/121 [00:10<00:11, 3.22it/s]
71%|███████ | 86/121 [00:10<00:10, 3.22it/s]
72%|███████▏ | 87/121 [00:11<00:10, 3.21it/s]
73%|███████▎ | 88/121 [00:11<00:10, 3.22it/s]
74%|███████▎ | 89/121 [00:11<00:09, 3.22it/s]
74%|███████▍ | 90/121 [00:12<00:09, 3.21it/s]
75%|███████▌ | 91/121 [00:12<00:09, 3.17it/s]
76%|███████▌ | 92/121 [00:13<00:11, 2.53it/s]
77%|███████▋ | 93/121 [00:13<00:12, 2.25it/s]
78%|███████▊ | 94/121 [00:14<00:12, 2.09it/s]
79%|███████▊ | 95/121 [00:14<00:13, 1.99it/s]
79%|███████▉ | 96/121 [00:15<00:13, 1.92it/s]
80%|████████ | 97/121 [00:15<00:12, 1.88it/s]
81%|████████ | 98/121 [00:16<00:12, 1.85it/s]
82%|████████▏ | 99/121 [00:16<00:12, 1.83it/s]
83%|████████▎ | 100/121 [00:17<00:11, 1.82it/s]
83%|████████▎ | 101/121 [00:18<00:11, 1.81it/s]
84%|████████▍ | 102/121 [00:18<00:10, 1.80it/s]
85%|████████▌ | 103/121 [00:19<00:10, 1.80it/s]
86%|████████▌ | 104/121 [00:19<00:09, 1.79it/s]
87%|████████▋ | 105/121 [00:20<00:08, 1.79it/s]
88%|████████▊ | 106/121 [00:20<00:08, 1.79it/s]
88%|████████▊ | 107/121 [00:21<00:07, 1.78it/s]
89%|████████▉ | 108/121 [00:22<00:07, 1.78it/s]
90%|█████████ | 109/121 [00:22<00:06, 1.78it/s]
91%|█████████ | 110/121 [00:23<00:06, 1.78it/s]
92%|█████████▏| 111/121 [00:23<00:05, 1.78it/s]
93%|█████████▎| 112/121 [00:24<00:05, 1.79it/s]
93%|█████████▎| 113/121 [00:24<00:04, 1.79it/s]
94%|█████████▍| 114/121 [00:25<00:03, 1.79it/s]
95%|█████████▌| 115/121 [00:25<00:03, 1.78it/s]
96%|█████████▌| 116/121 [00:26<00:02, 1.78it/s]
97%|█████████▋| 117/121 [00:27<00:02, 1.78it/s]
97%|█████████▋| 117/121 [00:27<00:00, 4.24it/s]
---------------------------------------------------------------------------
KeyboardInterrupt Traceback (most recent call last)
Cell In[10], line 37
34 image_layer.plane.position = (0, 0, 0)
35 animation.capture_keyframe(steps=30)
---> 37 animation.animate("animation_labels.mp4", canvas_only=True)
38 image_layer.plane.position = (0, 0, 0)
File /opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/napari_animation/animation.py:237, in Animation.animate(self, filename, fps, quality, file_format, canvas_only, scale_factor)
235 sleep(0.05)
236 with tqdm(total=n_frames) as pbar:
--> 237 for frame_index, image in enumerate(frame_generator):
238 if save_as_folder is True:
239 frame_filename = (
240 folder_path / f"{file_path.stem}_{frame_index:06d}.png"
241 )
File /opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/napari_animation/frame_sequence.py:153, in FrameSequence.iter_frames(self, viewer, canvas_only, scale_factor)
151 """Iterate over interpolated viewer states, and yield rendered frames."""
152 for _i, state in enumerate(self):
--> 153 frame = state.render(viewer, canvas_only=canvas_only)
154 if scale_factor not in (None, 1):
155 from scipy import ndimage as ndi
File /opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/napari_animation/viewer_state.py:85, in ViewerState.render(self, viewer, canvas_only)
69 """Render this ViewerState to an image.
70
71 Parameters
(...)
82 An RGBA image of shape (h, w, 4).
83 """
84 self.apply(viewer)
---> 85 return viewer.screenshot(canvas_only=canvas_only, flash=False)
File /opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/napari/viewer.py:182, in Viewer.screenshot(self, path, size, scale, canvas_only, flash)
146 def screenshot(
147 self,
148 path: Optional[str] = None,
(...)
153 flash: bool = True,
154 ):
155 """Take currently displayed screen and convert to an image array.
156
157 Parameters
(...)
180 upper-left corner of the rendered region.
181 """
--> 182 return self.window.screenshot(
183 path=path,
184 size=size,
185 scale=scale,
186 flash=flash,
187 canvas_only=canvas_only,
188 )
File /opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/napari/_qt/qt_main_window.py:1783, in Window.screenshot(self, path, size, scale, flash, canvas_only)
1752 def screenshot(
1753 self, path=None, size=None, scale=None, flash=True, canvas_only=False
1754 ):
1755 """Take currently displayed viewer and convert to an image array.
1756
1757 Parameters
(...)
1780 upper-left corner of the rendered region.
1781 """
-> 1783 img = QImg2array(self._screenshot(size, scale, flash, canvas_only))
1784 if path is not None:
1785 imsave(path, img)
File /opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/napari/_qt/qt_main_window.py:1687, in Window._screenshot(self, size, scale, flash, canvas_only, fit_to_data_extent)
1685 self._qt_viewer.viewer.reset_view(margin=0)
1686 try:
-> 1687 img = canvas.screenshot()
1688 if flash:
1689 add_flash_animation(self._qt_viewer._welcome_widget)
File /opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/napari/_vispy/canvas.py:676, in VispyCanvas.screenshot(self)
674 def screenshot(self) -> QImage:
675 """Return a QImage based on what is shown in the viewer."""
--> 676 return self.native.grabFramebuffer()
KeyboardInterrupt: