Pixel perfect borderless animations in Napari

I’ve been trying to switch more of my image analysis workflow to Napari since my large imaging datasets are a bit much for ImageJ/FIJI. One thing that was missing from my Napari pipeline was a way to easily generate compressed .mp4s of my movies for my slide decks. I came across the Napari animation plugin, but I couldn’t find a way to export a pixel-perfect rendering of my movies without any black borders.

A lot of my movies ended up pretty low resolution with black borders that require careful window resizing to minimize:

After some try and error, I hacked up the following function that should work in most cases (high-DPI displays and scaled axes supported). Running this function should rescale the window to match your movie for pixel-perfect export.

from qtpy.QtCore import QSize, Qt
from qtpy.QtWidgets import QDockWidget

def resize_canvas_to_image(viewer):
    """
    Resize the Napari window so the VisPy canvas exactly matches the layer size.
    """
    h, w = viewer.layers[0].data.shape[-2:]

    qt_window = viewer.window._qt_window
    canvas = viewer.window._qt_viewer.canvas.native

    # switch to frameless window to expand past display
    qt_window.setWindowFlag(Qt.WindowType.FramelessWindowHint)
    qt_window.setMinimumSize(500,500)
    qt_window.show()

    # high-DPI displays
    dpr = canvas.devicePixelRatioF()

    window_size = qt_window.size()
    canvas_size = canvas.size()

    # Physical size in world coordinates
    scale_y, scale_x = viewer.layers[0].scale[-2:]

    # Amount of window occupied by non-canvas widgets
    extra_w = window_size.width()  - canvas_size.width()
    extra_h = window_size.height() - canvas_size.height()

    # Resize the entire window
    qt_window.resize(
        QSize(
            int(round(w / dpr)) + extra_w,
            int(round(h / dpr)) + extra_h,
        )
    )

    # Resize canvas
    viewer.camera.zoom = 1/dpr/scale_x
    viewer.camera.center = (0.0, (h * scale_y)/dpr, (w * scale_x)/dpr)

    return window_size, canvas_size

I wrote some helper functions that hide and show all the widgets so that Napari can go full screen and back:

    def hide_widgets(viewer):
        win = viewer.window
        win._qt_viewer.dockLayerControls.hide()
        win._qt_viewer.dockLayerList.hide()
        win._qt_viewer.dockConsole.hide()
        win._qt_viewer.dims.hide()
        win._qt_window.menuBar().hide()
        win._qt_window.statusBar().hide()

        for dock in win._qt_window.findChildren(QDockWidget):
            if "animation" in dock.windowTitle() or "Timestamper" in dock.windowTitle():
                dock.hide()

    def show_widgets(viewer):
        win = viewer.window
        win._qt_viewer.dockLayerControls.show()
        win._qt_viewer.dockLayerList.show()
        win._qt_viewer.dims.show()
        win._qt_window.menuBar().show()
        win._qt_window.statusBar().show()

        for dock in win._qt_window.findChildren(QDockWidget):
            if "animation" in dock.windowTitle():
                dock.show()

So in complete script, you can run it as so:

import napari
import dask.array as da
import zarr
from napari_animation import Animation

# load a .zarr movie
z = zarr.open("timelapse.zarr", mode="r")
darr = da.from_zarr(z["25"]["0"])

# open the movie in napari
v = napari.Viewer()
v.add_image(darr[:, 0, :, :], name="TRITC", scale = [300.0, 0.22898, 0.22898], units = ('s', 'um', 'um'))
v.add_image(darr[:, 1, :, :], name="BF" , scale = [300.0, 0.22898, 0.22898], blending = "additive", units = ('s', 'um', 'um'))
v.layers["TRITC"].colormap = "bop orange"

hide_widgets(viewer)
resize_canvas_to_image(viewer)

# animate
animation = Animation(v)
v.dims.set_point(-3, 0) # set to first frame
animation.capture_keyframe()
v.dims.set_point(-3, 719*300) # set to last frame
animation.capture_keyframe(steps = 240) # change steps to change length
animation.animate('pos29.mp4', canvas_only = True, fps = 20) # save

# reset to framed window
viewer.window._qt_window.setWindowFlags(Qt.WindowType.Window)
viewer.window._qt_window.show()
show_widgets(viewer)

This should create a full-screen pixel-perfect movie in your current folder:

Pretty nice, right?




Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • Inside Lines Equipment Porteur Rackbag Review
  • A deep dive into the Stellar public network (Part 1)
  • UCSF Graduate Student Taxes FAQ
  • Copper Creek and Lower Paradise Valley Trip Report
  • Backing up a TrueNAS App Pool to another pool