napari / napari-animation

A napari plugin for making animations
https://napari.github.io/napari-animation/
Other
76 stars 27 forks source link

Time taken to capture frame increases with frame index #115

Closed ptbrown1729 closed 2 years ago

ptbrown1729 commented 2 years ago

I have 3D time series images, and I am trying to export a movie showing time-series data of a three orthogonal max-projections. When I script napari-animation I find initially the each frame is acquired quickly, but as the number of frames increases the time to acquire a frame animation.capture_keyframe(steps=1) slows down dramatically. In my case, I am trying to export 10,000 time points for images with size ~1632 x 880, and a similar script to my example below took 3 days to run. I am comparing this with manually creating frames and exporting with mmpeg, which takes perhaps 10 minutes.

My question/issue is: am I running into a limitation of napari or napari-animation, or am I using it wrong?

Here's my system info:

napari: 0.4.14
Platform: Windows-10-10.0.19042-SP0
Python: 3.9.7 (default, Sep 16 2021, 16:59:28) [MSC v.1916 64 bit (AMD64)]
Qt: 5.12.9
PyQt5: 5.12.3
NumPy: 1.22.1
SciPy: 1.7.3
Dask: 2022.01.0
VisPy: 0.9.4

OpenGL:
- GL version: 4.6.0 NVIDIA 511.23
- MAX_TEXTURE_SIZE: 32768

Screens:
- screen 1: resolution 1920x1080, scale 1.0

Plugins:
- animation: 0.0.3.dev79+g54aa0e5
- console: 0.0.4
- micromanager: 0.0.1rc6.dev14+gb75c513
- scikit-image: 0.4.14
- svg: 0.1.5

Here is a minimal example. For the first 50 frames or so each frame takes ~0.1s to capture. By frame 600 this has increased to ~0.4s.

import napari
import numpy as np
from napari_animation import Animation
import time
from pathlib import Path

viewer = napari.Viewer()

nt = 10000
nx = 287
ny = 201
nz = 69

dxy = 0.234
dz = 0.4703

n_real_maxz = np.random.rand(nt, ny, nx)
n_real_maxy = np.random.rand(nt, nz, nx)
n_real_maxx = np.random.rand(nt, nz, ny)

# plot max projections along 3-axis
max_y_offset = np.array([(ny + 10) * dxy, 0])
max_x_offset = np.array([0, (nx + 10) * dxy])

viewer.add_image(n_real_maxz, contrast_limits=(np.percentile(n_real_maxz, 1), np.percentile(n_real_maxz, 99.999)),
                 scale=(dxy, dxy), colormap="gray_r", name="n real max-z")
viewer.add_image(n_real_maxy,
                 contrast_limits=(np.percentile(n_real_maxy, 1), np.percentile(n_real_maxy, 99.999)),
                 scale=(dz, dxy), translate=max_y_offset, colormap="gray_r", name="n real max-y")
viewer.add_image(n_real_maxx.transpose([0, 2, 1]),
                 contrast_limits=(np.percentile(n_real_maxx, 1), np.percentile(n_real_maxx, 99.999)),
                 scale=(dxy, dz), translate=max_x_offset,  colormap="gray_r", name="n real max-x")

# set scale bar
viewer.scale_bar.visible = True
viewer.scale_bar.unit = "um"
viewer.scale_bar.position = "bottom_left"
viewer.scale_bar.font_size = 30.0

# position camera
viewer.camera.center = (0.0, 61.65467799459777, 90.19643145945702)
viewer.camera.zoom = 6.859355692041186
viewer.camera.angles = (0.0, 0.0, 90.0)
viewer.camera.perspective = 0.0

# export video
print("collecting frames...")

# animate programmatically
animation = Animation(viewer)
# plot layer, then capture
viewer.dims.set_current_step(0, 0)
animation.capture_keyframe()
tstart = time.perf_counter()
for ii in range(1, nt):
    tstart_frame = time.perf_counter()
    # viewer.dims.current_step
    viewer.dims.set_current_step(0, ii)
    animation.capture_keyframe(steps=1)
    print("capturing frame %d/%d. This frame took %0.2fs, total time elapsed %0.2fs" %
          (ii + 1, nt, time.perf_counter() - tstart_frame, time.perf_counter() - tstart))

fname_out = Path(r"C:\Users\q2ilab\Desktop\animation_test.mp4")
animation.animate(fname_out, canvas_only=True)
alisterburt commented 2 years ago

Hi @ptbrown1729! Thank you for such a detailed report 🙂 I think both things are true, you've come across a performance limitation and you're not using the package as intended.

I'll look into the performance side of things next week. For now, could you try to set just two keyframes, one at the start one at the end of your time series, with linear interpolation between the keyframes and an appropriate number of steps?

Interpolation between keyframes was the intended mode of use and will likely solve your performance issues 🙂

ptbrown1729 commented 2 years ago

@alisterburt thanks for the help. Ok, seems like user error. If I replace the last portion of my example with

# animate programmatically
print("collecting frames...")

# animate programmatically
animation = Animation(viewer)
# plot layer, then capture
viewer.dims.set_current_step(0, 0)
animation.capture_keyframe()
viewer.dims.set_current_step(0, nt - 1)
animation.capture_keyframe(steps=nt)

fname_out = Path(r"C:\Users\q2ilab\Desktop\animation_test.mp4")
animation.animate(fname_out, canvas_only=True)

then the animation is produced quickly and correctly

Looking at the examples I see that this is basically the same code as in animate2D.py. I guess it did not click with me that that was time series data before. Oh well, as usual it pays to look through the examples...

alisterburt commented 2 years ago

Really glad to hear this is working better for you - we need to improve the documentation that's for sure

I'll close this now, thanks again for the report!