How to properly do an animation loop in Napari?

Hi,

I would like to ask for an advice regarding how to properly do an animation loop in Napari.
I have a 2d image with a high number of rows (n), and k number of columns (n is much bigger than k).
Let’s say I divide the number of rows (n) in 100 equally size parts (n1, n2,…,n100), therefore the dimensions of each part is (n/100) x k. The animation that I would like to accomplish consists of the following steps:

  1. Show at the top of the viewer the last part of the image (let’s call it n100, where n100 is of dimension (n/100) x k). The current state of the viewer would be:

    n100


  2. Wait 200 milliseconds.

  3. Now, let’s show the next chunk along side the previous one. The new current state of the viewer would be:


    n99

    n100


  4. Wait 200 milliseconds.

  5. Now, let’s show the next chunk along side all the previous ones. The new current state of the viewer would be:

    n98

    n99

    n100


  6. And so on and so forth until we filled the “whole” viewer:

    n1

    n2

    .
    .
    .

    n98

    n99

    n100

  7. The animation would continue “in a loop” by plotting the last chunk (n100) and the top, but without deleting the previous plotted chunks:

    n100

    n1

    n2

    .
    .
    .

    n98

    n99

This animation would create the “illusion” that the image is “moving”.

Is it possible to do this kind of animation loop in Napari?

All advices and suggestions are truly welcome.

Thank you very much!

Hi @jmontoyam,
I’m not entirely clear on the desired visual effect. Are you trying to achieve a “pan” effect where the image moves across the canvas at a constant zoom level? or more of “wipe” effect, where the full image is slowly revealed (but once a particular segment of the image is visible, it doesn’t move with respect to the canvas)?

I’ve taken a stab at this based on my understanding of your question @jmontoyam:

import time
import numpy as np
import napari
from napari.qt import thread_worker


image = np.random.random((2048, 2048))

@thread_worker
def update_image(interval=0.02):
    for i in range(1, 102):
        yield image[-min(i * 20, 2048):, :]
        time.sleep(interval)

with napari.gui_qt():
    v = napari.Viewer()
    # add "n100"
    image_layer = v.add_image(image[-20:, :])
    def update_image_layer(data):
        image_layer.data = data
        image_layer.refresh()
        v.reset_view()

    worker = update_image()
    worker.yielded.connect(update_image_layer)
    worker.start()

Result:

Screen Recording 2020-09-08 at 10.46.29 am

1 Like

Thank you very much @talley and @jni for your kind reply!
The desired effect is similar to the code example uploaded by @jni, but instead of growing the image from the middle of the viewer, the image would start growing from a fixed position (the top of the viewer).
By playing a little bit with @jni example code I was able to get such effect:

But as you can see in the above gif, after a few iterations the animation does not look smooth anymore.
I think this laggy behavior is because the worker thread is sending to the viewer a “big” matrix (2048x2048) at each iteration. I modified @jni code as follows:

import time
import numpy as np
import napari
from napari.qt import thread_worker
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QPushButton


image = np.random.random((2048, 2048))
output = np.zeros((2048, 2048))


@thread_worker
def update_image(interval=0.1):

    for i in range(1, 129):

        num_rows = i * 16
        output[:num_rows] = image[-min(num_rows, 2048):]
        yield output

        time.sleep(interval)


def update_image_layer(data):

    image_layer.data = data
    image_layer.refresh()
    v.reset_view()


with napari.gui_qt():

    v = napari.Viewer(show=False)
    v.window._qt_window.setWindowState(Qt.WindowMaximized)
    v.show()

    image_layer = v.add_image(output)

    worker = update_image()
    worker.yielded.connect(update_image_layer)

    button = QPushButton("Start animation")
    button.clicked.connect(worker.start)
    v.window.add_dock_widget(widget=button, area='left')

I am pretty sure I did something wrong (I am a beginner user :blush:).

Could you please suggest me a possible solution to obtain the same effect but in a smoother way?

Is it possible to send to the viewer only the new chuck (in the above example, only 16 rows at a time, instead of sending all 2048 rows each time)?..at each iteration, the viewer already knows all the previous chunks, we already sent them in the previous iterations, therefore we do not need to send all of them again, am I right?..The general pseudo-code of the animation is as follows: we send a chuck, wait n milliseconds, we send a new chunk and move the previous ones down, wait n milliseconds, …, and so on and so forth).

Thank you very much for all your help and your patience with this beginner user :slight_smile: !

ah, I see.
Well, I’ll also just point out an alternative strategy that has some pros/cons… it looks like you’re basically looking for the image to “slide in” from the top, so it’s worth knowing about a couple “camera” or “view” related attributes here that would be much less computationally expensive than actually resetting the data each time (and remove your lag). For instance, here’s a slight modification of your script that just updates layer.translate, rather than layer.data:

import time
import numpy as np
import napari
from napari.qt import thread_worker
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QPushButton


image = np.random.random((2048, 2048))
zeros = np.zeros((2048, 2048))

@thread_worker
def pan(layer, interval=0.05):
    layer.translate = (-2148, 0)
    layer.data = image
    for i in range(-2140, 8, 16):
        layer.translate = (i, 0)
        yield
        time.sleep(interval)

with napari.gui_qt():
    v = napari.Viewer(show=False)
    v.window._qt_window.setWindowState(Qt.WindowMaximized)
    v.show()

    image_layer = v.add_image(zeros)
    worker = pan(image_layer)

    button = QPushButton("Start animation")
    button.clicked.connect(worker.start)
    v.window.add_dock_widget(widget=button, area='left')

it’s definitely not exactly the same… and the “downside” here is that you don’t get that black strip at the top, between the edge of the canvas and the top of the image… but that visual effect could probably also be achieved somehow.

Another “view-related” attribute to be aware of (that wouldn’t require you to modify the actual data) is the camera “pan” method:

# set the x, y coordinate of the camera position
viewer.window.qt_viewer.view.camera.pan((0,1000))

lastly… if you like looking through code examples, I’d point you to @guiwitz’s https://github.com/guiwitz/naparimovie repository… that has all kinds of view-wrangling and animation tricks.

2 Likes

I’m plus one to an approach that just moves the camera if you don’t need to change the underlying data. Once we have https://github.com/napari/napari/pull/854 merged this will be much easier too!!

Thank you very much @talley!, now it works fine!..Wow, napari is really amazing!, there are a lot, but a lot of things that I need and want to learn about napari :blush:, I didn’t know about the camera magic!..Please excuse me @talley, but I do not understand very well the way you managed to accomplish the desired effect, I don’t understand the way you implemented the pan method :blush:

  • What is the relationship between a camera and a layer?
  • Is there one camera associated to each layer?
  • How did you know you need to translate the camera (layer) to the position (-2148,0)?
  • Does translating a layer mean translating the camera?
  • If we do not translate the camera, what is its default position?, is it the center of the layer?
  • Where is the origin of the coordinate system of the layer?..how can I query the width and height of such coordinate system?

Please excuse me for asking you so many questions, the camera concept is new to me, but I am eager to learn it :slight_smile:

Once again thank you very much for all your help!

yes, there are lots of hidden/undocumented tricks in napari. part of the reason for that is that napari leans heavily on other amazing packages in the python ecosystem, and when it comes to the main canvas in napari, a lot of the magic is provided by vispy.

our main canvas (the big black area) is an instance of a vispy.SceneCanvas, which is an implementation of a “scene graph”, which is a way of organizing a visual scene as a tree/hierarchy of nodes/objects. In this case, the nodes are individual layers (each of which may in turn be composed of multiple nodes). So, for instance, an image layer in napari is implemented as a vispy Image when viewed in 2D and a Volume when viewed in 3D. A points layer is implemented with vispy Markers, a surface layer is visualized as a Mesh, and so on… Each of these visual nodes is added to the SceneCanvas. The vispy camera object is responsible for determining which part of a scene is displayed in a viewbox and for handling user input to change the view (which, behind the scenes is really just implemented with a series of transforms that map the scene visuals to the viewbox according to the “perspective” of the viewer). When you click and drag the mouse around on the napari canvas, it’s the camera object that you’re really modifying.

What is the relationship between a camera and a layer?

hopefully made a bit clearer above. The “scene” (canvas) can be composed of multiple “nodes” (layers), and the camera determines our perspective onto the scene.

Is there one camera associated to each layer?

nope, there is one camera associated with a vispy ViewBox, and in the case of napari, there is only a single viewbox in our canvas.

How did you know you need to translate the camera (layer) to the position (-2148,0)?

because your image was 2048 x 2048 … I shifted it 2048 + 100, and that last 100 was just to get it truly out of the scene to begin with (because by default we have that little additional padding between the edge of the image an the edge of the canvas)

Does translating a layer mean translating the camera?

No, layers can be moved around independently “within the scene”. And the camera determines our perspective on the whole scene. If you had multiple layers in the viewer, and you used viewer.window.qt_viewer.view.camera.pan() … then the whole scene would move together. On the other hand, if you use layer.translate = (x, y) on just one of the layers, then you are just shifting the position of that one layer with respect to the scene.

If we do not translate the camera, what is its default position?, is it the center of the layer?

yeah, by default, the camera is positioned with a zoom level that fits the largest object in the scene, and is centered on the object.

Where is the origin of the coordinate system of the layer?..how can I query the width and height of such coordinate system?

That’s actually a complicated question, there are multiple coordinate systems going on, so I don’t want to give you an oversimplified answer to that question. However, to query the current state of the camera, use viewer.window.qt_viewer.view.camera.get_state(). You can pan/zoom/rotate(3d) the image and use that again and you’ll see how each adjustment changes the state of the camera (and how 2D and 3D cameras differ). For example:

In [23]: viewer.add_image(np.random.rand(800, 800))
Out[23]: <Image layer 'Image' at 0x13d6df6a0>

In [24]: viewer.reset_view()

In [25]: viewer.window.qt_viewer.view.camera.get_state()
Out[25]: {'rect': <Rect (-40, -40) (880, 880)>}
3 Likes

Thank you very much @talley for the friendly and detailed explanation! :wink:, now everything is much more clear!.. I am enjoying a lot my learning process in Napari land! :slight_smile:

We enjoy your questions. Keep ‘em coming :slight_smile: