napari-animation icon indicating copy to clipboard operation
napari-animation copied to clipboard

Procedural Animations

Open kolibril13 opened this issue 1 year ago • 2 comments

Procedural animations are animation that are generated automatically via mathematical transformations that occur over time. E.g. manim can produce procedural animations with the t.add_updater(foo) function. I just wrote a manim script that is connected to the napari viewer, and it produces the below animation. Note that the camera is panning in a sinusoidal path, while other parameters (zoom, opacity, contrast limits) are changing simultaneously. I think that won't be possible by only using key frames. It would open up more possible animations if napari-animation would support these procedural animations as well.

https://user-images.githubusercontent.com/44469195/223225956-2a461fbf-68fc-4f14-92d7-c78daedd4bfe.mp4


Click here to see the script

import napari
from skimage import data
import numpy as np
from manim import * 

viewer = napari.view_image(data.cells3d(), channel_axis=1, ndisplay=3)
viewer.camera.angles = (0, 0, 90)

class Example(Scene):
    def construct(self):
        self.camera.background_color = BLUE_A
        img = viewer.screenshot(canvas_only=True, flash=False)
        t = ImageMobject(img)
        t.time = 0
        self.add(t)

        tr_zoom = ValueTracker(1)
        contrast_tracker = ValueTracker(65535) # dark limit

        def foo(mob,dt):
            mob.time += dt

        SECOND = 1

        def bar(mob):
            viewer.camera.angles = (0, 30*np.sin((mob.time*PI/11*2)), 50)
            viewer.camera.zoom = tr_zoom.get_value()

            viewer.layers[0].contrast_limits = (0, contrast_tracker.get_value())
            img = viewer.screenshot(canvas_only=True, flash=False)
            new_mob = ImageMobject(img)
            mob.become(new_mob)

        t.add_updater(foo)
        t.add_updater(bar)

        viewer.layers[1].opacity = 1
        self.play(tr_zoom.animate.set_value(2), rate_func=smooth, run_time=SECOND)
        self.play(contrast_tracker.animate.set_value(19500), rate_func=smooth, run_time=SECOND) #bright limit
        self.wait(SECOND)
        viewer.layers[1].opacity = 0
        self.play(tr_zoom.animate.set_value(4), rate_func=smooth, run_time=SECOND)
        self.wait(SECOND)
        self.wait(SECOND)
        self.play(tr_zoom.animate.set_value(2), rate_func=smooth, run_time=SECOND)
        self.wait(SECOND)
        viewer.layers[1].opacity = 1
        self.wait(SECOND)
        self.play(contrast_tracker.animate.set_value( 65535 ), rate_func=smooth, run_time=SECOND) #dark limit

        self.play(tr_zoom.animate.set_value(1), rate_func=smooth, run_time=SECOND)
        t.remove_updater(foo)
        self.wait(SECOND)

%manim -v WARNING -qh --disable_caching --progress_bar None Example

kolibril13 avatar Mar 06 '23 20:03 kolibril13

Hey @kolibril13 - this is cool, it feels like an alternative method for generating a FrameSequence - I'm not exactly sure of the API in manim but you could...

  • write functions for each parameter in terms of t
  • loop
    1. capture viewer state
    2. increment t
  • render the frame sequence

If we do end up integrating this Animation might become KeyFrameAnimation and this could be a ProceduralAnimation - the render method might need to be moved onto the framesequence in that case

sound reasonable?

alisterburt avatar Mar 07 '23 11:03 alisterburt

write functions for each parameter in terms of t

I'm not a fan of case discrimination (see in example below in zoom_over_time), but yes, that would work.

I'm not familiar with the FrameSequence module, but I think key_frame = viewer.screenshot() is most likely not the object that should be phrased to it. Therefore, this below example is only a sketch, and won't actually run. @alisterburt : If you're interested, feel free to test if you can make my prototype code run :)

def angle_over_time(t):
    return (0, 30*np.sin((t*np.pi)), 50)


def zoom_over_time(t):
    zoom = 0
    start_time = 1
    if t < start_time:
        zoom = 2
    # zoom starts for t threshold 
    elif start_time < t: 
        zoom = t -1
    return zoom
    

my_key_frames = []
for t in np.linspace(0,4, 300):
    viewer.camera.angles = angle_over_time(t)
    viewer.camera.zoom = zoom_over_time(t)
    key_frame = viewer.screenshot() # <- this should probably be something else.
    my_key_frames.append(key_frame)

fs = FrameSequence(key_frames = my_key_frames)
animation = Animation(frame_sequence=fs, viewer=viewer, fps=30) # <- this should probably be something else as well.
# and here should happend the rendering

kolibril13 avatar Mar 08 '23 15:03 kolibril13