napari / napari-animation

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

Unexpected ValueError in animation when changing gamma parameter. #149

Closed kolibril13 closed 9 months ago

kolibril13 commented 1 year ago

I ran into an error when changing the gamma value during an animation: "ValueError: gamma must be > 0" But the value of gamma only changes from 1 to 0.4, so it should never have a negative value. Minimal example to reproduce:

import napari
from skimage import data
from napari_animation import Animation

cat = data.cat()
viewer = napari.view_image(cat, rgb=True)

animation = Animation(viewer)
viewer.camera.zoom = 1

viewer.layers[0].gamma = 1
animation.capture_keyframe(steps=0)
viewer.layers[0].gamma = 0.4
animation.capture_keyframe(steps=30)

animation.capture_keyframe(steps=60)
animation.animate("mov.mp4") # <- commenting this one in would result in message "ValueError: gamma must be > 0"

Whole message:

ValueError                                Traceback (most recent call last)
Cell In[11], line 17
     14 animation.capture_keyframe(steps=30)
     16 animation.capture_keyframe(steps=60)
---> 17 animation.animate("mov.mp4") # <- commenting this one in would result in message "ValueError: gamma must be > 0"

File [~/opt/anaconda3/envs/napari-env2/lib/python3.9/site-packages/napari_animation/animation.py:214](https://file+.vscode-resource.vscode-cdn.net/Users/jan-hendrik/projects/okapi/napari_animation/~/opt/anaconda3/envs/napari-env2/lib/python3.9/site-packages/napari_animation/animation.py:214), in Animation.animate(self, filename, fps, quality, format, canvas_only, scale_factor)
    212 sleep(0.05)
    213 with tqdm(total=n_frames) as pbar:
--> 214     for frame_index, image in enumerate(frame_generator):
    215         if save_as_folder is True:
    216             frame_filename = (
    217                 folder_path [/](https://file+.vscode-resource.vscode-cdn.net/) f"{file_path.stem}_{frame_index:06d}.png"
    218             )

File [~/opt/anaconda3/envs/napari-env2/lib/python3.9/site-packages/napari_animation/frame_sequence.py:121](https://file+.vscode-resource.vscode-cdn.net/Users/jan-hendrik/projects/okapi/napari_animation/~/opt/anaconda3/envs/napari-env2/lib/python3.9/site-packages/napari_animation/frame_sequence.py:121), in FrameSequence.iter_frames(self, viewer, canvas_only, scale_factor)
    119 """Iterate over interpolated viewer states, and yield rendered frames."""
    120 for i, state in enumerate(self):
--> 121     frame = state.render(viewer, canvas_only=canvas_only)
    122     if scale_factor not in (None, 1):
    123         from scipy import ndimage as ndi

File [~/opt/anaconda3/envs/napari-env2/lib/python3.9/site-packages/napari_animation/viewer_state.py:79](https://file+.vscode-resource.vscode-cdn.net/Users/jan-hendrik/projects/okapi/napari_animation/~/opt/anaconda3/envs/napari-env2/lib/python3.9/site-packages/napari_animation/viewer_state.py:79), in ViewerState.render(self, viewer, canvas_only)
     61 def render(
...
--> 456     raise ValueError("gamma must be > 0")
    457 self._gamma = float(value)
    458 # shortcut so we don't have to rebuild the color transform

ValueError: gamma must be > 0
alisterburt commented 1 year ago

huh - interesting! Thanks for the report - maybe @katherine-hutchings would like to take a look at this

kolibril13 commented 1 year ago

One more note on this: the error only comes when gamma is decreasing. An increasing gamma does not throw any error:

from napari_animation import Animation
from napari_animation.easing import Easing

img = viewer.layers[0]

animation = Animation(viewer)
viewer.camera.zoom = 0.5
img.gamma = 0.2
animation.capture_keyframe(steps=30)

viewer.camera.zoom = 1.4
animation.capture_keyframe(steps=60, ease=Easing.QUADRATIC)

img.gamma = 1
animation.capture_keyframe(steps=30, ease=Easing.QUADRATIC)

# img.gamma = 0.9 # <-- only this would cause an error because gamma is decreaing
animation.capture_keyframe(steps=30, ease=Easing.QUADRATIC)

https://user-images.githubusercontent.com/44469195/222401642-b6a90f80-b0da-409a-b96e-9e4d9a1b5b98.mp4

psobolewskiPhD commented 9 months ago

I can reproduce this, will poke around. Would be nice to get this fixed for sure.

psobolewskiPhD commented 9 months ago

The traceback above is truncated, not sure why. Here's the full:

``` ValueError Traceback (most recent call last) Cell In[10], line 1 ----> 1 anim.animate("test.mov") File ~/Dev/napari-animation/napari_animation/animation.py:224, in Animation.animate(self, filename, fps, quality, format, canvas_only, scale_factor) 222 sleep(0.05) 223 with tqdm(total=n_frames) as pbar: --> 224 for frame_index, image in enumerate(frame_generator): 225 if save_as_folder is True: 226 frame_filename = ( 227 folder_path / f"{file_path.stem}_{frame_index:06d}.png" 228 ) File ~/Dev/napari-animation/napari_animation/frame_sequence.py:152, in FrameSequence.iter_frames(self, viewer, canvas_only, scale_factor) 150 """Iterate over interpolated viewer states, and yield rendered frames.""" 151 for i, state in enumerate(self): --> 152 frame = state.render(viewer, canvas_only=canvas_only) 153 if scale_factor not in (None, 1): 154 from scipy import ndimage as ndi File ~/Dev/napari-animation/napari_animation/viewer_state.py:85, in ViewerState.render(self, viewer, canvas_only) 67 def render( 68 self, viewer: napari.viewer.Viewer, canvas_only: bool = True 69 ) -> np.ndarray: 70 """Render this ViewerState to an image. 71 72 Parameters (...) 83 An RGBA image of shape (h, w, 4). 84 """ ---> 85 self.apply(viewer) 86 return viewer.screenshot(canvas_only=canvas_only, flash=False) File ~/Dev/napari-animation/napari_animation/viewer_state.py:63, in ViewerState.apply(self, viewer) 61 if layer_attribute_changed(value, original_value): 62 try: ---> 63 setattr(layer, attribute_name, value) 64 except AttributeError: 65 pass File ~/Dev/miniforge3/envs/napari-418/lib/python3.10/site-packages/napari/layers/intensity_mixin.py:152, in IntensityVisualizationMixin.gamma(self, value) 150 self._gamma = value 151 self._update_thumbnail() --> 152 self.events.gamma() File ~/Dev/miniforge3/envs/napari-418/lib/python3.10/site-packages/napari/utils/events/event.py:768, in EventEmitter.__call__(self, *args, **kwargs) 765 self._block_counter.update([cb]) 766 continue --> 768 self._invoke_callback(cb, event if pass_event else None) 769 if event.blocked: 770 break File ~/Dev/miniforge3/envs/napari-418/lib/python3.10/site-packages/napari/utils/events/event.py:806, in EventEmitter._invoke_callback(self, cb, event) 804 self.disconnect(cb) 805 return --> 806 _handle_exception( 807 self.ignore_callback_errors, 808 self.print_callback_errors, 809 self, 810 cb_event=(cb, event), 811 ) File ~/Dev/miniforge3/envs/napari-418/lib/python3.10/site-packages/napari/utils/events/event.py:795, in EventEmitter._invoke_callback(self, cb, event) 793 cb(event) 794 else: --> 795 cb() 796 except Exception as e: # noqa: BLE001 797 # dead Qt object with living python pointer. not importing Qt 798 # here... but this error is consistent across backends 799 if ( 800 isinstance(e, RuntimeError) 801 and 'C++' in str(e) 802 and str(e).endswith(('has been deleted', 'already deleted.')) 803 ): File ~/Dev/miniforge3/envs/napari-418/lib/python3.10/site-packages/napari/_vispy/layers/image.py:207, in VispyImageLayer._on_gamma_change(self) 205 def _on_gamma_change(self): 206 if len(self.node.shared_program.frag._set_items) > 0: --> 207 self.node.gamma = self.layer.gamma File ~/Dev/miniforge3/envs/napari-418/lib/python3.10/site-packages/vispy/util/frozen.py:17, in Frozen.__setattr__(self, key, value) 13 if self.__isfrozen and not hasattr(self, key): 14 raise AttributeError('%r is not an attribute of class %s. Call ' 15 '"unfreeze()" to allow addition of new ' 16 'attributes' % (key, self)) ---> 17 object.__setattr__(self, key, value) File ~/Dev/miniforge3/envs/napari-418/lib/python3.10/site-packages/vispy/visuals/image.py:451, in ImageVisual.gamma(self, value) 449 """Set gamma used when rendering the image.""" 450 if value <= 0: --> 451 raise ValueError("gamma must be > 0") 452 self._gamma = float(value) 453 # shortcut so we don't have to rebuild the color transform ValueError: gamma must be > 0 ```

Interestingly, checking the viewer_state of key_frames returns correct values, so I assume the interpolation is doing something wonky for decreasing values of gamma.

psobolewskiPhD commented 9 months ago

Hmm, I think I'm on to something: Here's a trimmed viewer.layers[-1]._get_state():

{'name': 'astronaut',
 'metadata': {},
 'scale': [1.0, 1.0],
 'translate': [0.0, 0.0],
 'rotate': [[1.0, 0.0], [0.0, 1.0]],
 'shear': [0.0],
 'affine': array([[1., 0., 0.],
        [0., 1., 0.],
        [0., 0., 1.]]),
 'opacity': 1.0,
 'blending': 'translucent_no_depth',
 'visible': True,
 'experimental_clipping_planes': [],
 'rgb': True,
 'multiscale': False,
 'colormap': 'gray',
 'contrast_limits': [0, 255],
 'interpolation2d': 'nearest',
 'interpolation3d': 'linear',
 'rendering': 'mip',
 'depiction': 'volume',
 'plane': {'normal': (1.0, 0.0, 0.0),
  'position': (0.0, 0.0, 0.0),
  'thickness': 1.0},
 'iso_threshold': 127.5,
 'attenuation': 0.05,
 'gamma': 1,

gamma is 1 which will return type int and will trigger wrong interpolation here: https://github.com/napari/napari-animation/blob/969fefb5f442d7f6e1ba9ca7ae287a67f55c5c6d/napari_animation/interpolation/base_interpolation.py#L70-L88

psobolewskiPhD commented 9 months ago

So I think the issue lies in napari codebase, but as a temporary fix you can use viewer.layers[-1].gamma = 1.0 (or whatever layer) to force the initial gamma to be a float for proper interpolation.