Introduction to animation with 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(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 (634, 666) to (640, 672) 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:25,  4.24it/s]
[swscaler @ 0x650fa00] Warning: data is not aligned! This can lead to a speed loss

  4%|▎         | 4/111 [00:00<00:08, 12.59it/s]
  6%|▋         | 7/111 [00:00<00:05, 18.27it/s]
 10%|▉         | 11/111 [00:00<00:04, 23.89it/s]
 14%|█▎        | 15/111 [00:00<00:03, 26.92it/s]
 17%|█▋        | 19/111 [00:00<00:03, 29.22it/s]
 21%|██        | 23/111 [00:00<00:02, 30.76it/s]
 24%|██▍       | 27/111 [00:01<00:02, 31.56it/s]
 28%|██▊       | 31/111 [00:01<00:02, 31.88it/s]
 32%|███▏      | 35/111 [00:01<00:02, 32.25it/s]
 35%|███▌      | 39/111 [00:01<00:02, 32.43it/s]
 39%|███▊      | 43/111 [00:01<00:02, 32.06it/s]
 42%|████▏     | 47/111 [00:01<00:02, 28.43it/s]
 45%|████▌     | 50/111 [00:01<00:02, 27.05it/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)
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)