napari: Images including NaN pixels cannot be displayed in napari

🐛 Bug

To Reproduce

Steps to reproduce the behavior:

In iPython, you can reproduce this behaviour like so:

%gui qt
import napari
import numpy as np

viewer = napari.Viewer()
data = np.random.random((512, 512))  # make some float type data
float_image = napari.layers.Image(data)
viewer.add_image(data, name='float_data')
data[0] = np.nan  # change a single pixel's value to NaN
viewer.add_image(data, name='data_with_nan')  # oh no!

Traceback:

CLICK HERE TO OPEN FULL TRACEBACK

c:\users\genevieve\anaconda3\envs\napari-dev\lib\site-packages\vispy\color\color_array.py:61: RuntimeWarning: invalid value encountered in less
  if color.min() < 0 or color.max() > 1:
c:\users\genevieve\anaconda3\envs\napari-dev\lib\site-packages\vispy\color\color_array.py:61: RuntimeWarning: invalid value encountered in greater
  if color.min() < 0 or color.max() > 1:
c:\users\genevieve\anaconda3\envs\napari-dev\lib\site-packages\vispy\color\color_array.py:61: RuntimeWarning: invalid value encountered in less
  if color.min() < 0 or color.max() > 1:
c:\users\genevieve\anaconda3\envs\napari-dev\lib\site-packages\vispy\color\color_array.py:61: RuntimeWarning: invalid value encountered in greater
  if color.min() < 0 or color.max() > 1:
c:\users\genevieve\anaconda3\envs\napari-dev\lib\site-packages\vispy\color\color_array.py:61: RuntimeWarning: invalid value encountered in less
  if color.min() < 0 or color.max() > 1:
c:\users\genevieve\anaconda3\envs\napari-dev\lib\site-packages\vispy\color\color_array.py:61: RuntimeWarning: invalid value encountered in greater
  if color.min() < 0 or color.max() > 1:
c:\users\genevieve\anaconda3\envs\napari-dev\lib\site-packages\vispy\color\color_array.py:61: RuntimeWarning: invalid value encountered in less
  if color.min() < 0 or color.max() > 1:
c:\users\genevieve\anaconda3\envs\napari-dev\lib\site-packages\vispy\color\color_array.py:61: RuntimeWarning: invalid value encountered in greater
  if color.min() < 0 or color.max() > 1:
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-9-b1aa5fd5b5cd> in <module>
----> 1 viewer.add_image(data, name='data_with_nan')

c:\users\genevieve\documents\github\napari\napari\components\add_layers_mixin.py in add_image(self, data, channel_axis, rgb, colormap, contrast_limits, gamma, interpolation, rendering, iso_threshold, attenuation, name, metadata, scale, translate, opacity, blending, visible, multiscale)
    228                     )
    229
--> 230             return self.add_layer(layers.Image(data, **kwargs))
    231         else:
    232             # Determine if data is a multiscale

c:\users\genevieve\documents\github\napari\napari\components\add_layers_mixin.py in add_layer(self, layer)
     60         layer.dims.events.order.connect(self._on_layers_change)
     61         layer.dims.events.range.connect(self._on_layers_change)
---> 62         self.layers.append(layer)
     63         self._update_layers(layers=[layer])
     64

c:\users\genevieve\documents\github\napari\napari\utils\list\_model.py in append(self, obj)
     58     def append(self, obj):
     59         TypedList.append(self, obj)
---> 60         self.events.added(item=obj, index=len(self) - 1)
     61
     62     def pop(self, key):

c:\users\genevieve\documents\github\napari\napari\utils\event.py in __call__(self, *args, **kwargs)
    506                     continue
    507
--> 508                 self._invoke_callback(cb, event)
    509                 if event.blocked:
    510                     break

c:\users\genevieve\documents\github\napari\napari\utils\event.py in _invoke_callback(self, cb, event)
    527                 self.print_callback_errors,
    528                 self,
--> 529                 cb_event=(cb, event),
    530             )
    531

c:\users\genevieve\documents\github\napari\napari\utils\event.py in _invoke_callback(self, cb, event)
    521     def _invoke_callback(self, cb, event):
    522         try:
--> 523             cb(event)
    524         except Exception:
    525             _handle_exception(

c:\users\genevieve\documents\github\napari\napari\_qt\qt_controls.py in _add(self, event)
     65         """
     66         layer = event.item
---> 67         controls = create_qt_controls(layer)
     68         self.addWidget(controls)
     69         self.widgets[layer] = controls

c:\users\genevieve\documents\github\napari\napari\_qt\layers\utils.py in create_qt_controls(layer)
     32             Qt controls widget
     33     """
---> 34     controls = layer_to_controls[type(layer)](layer)
     35
     36     return controls

c:\users\genevieve\documents\github\napari\napari\_qt\layers\qt_image_layer.py in __init__(self, layer)
     43
     44     def __init__(self, layer):
---> 45         super().__init__(layer)
     46
     47         self.layer.events.interpolation.connect(self._on_interpolation_change)

c:\users\genevieve\documents\github\napari\napari\_qt\layers\qt_image_base_layer.py in __init__(self, layer)
     59             self.layer.contrast_limits,
     60             self.layer.contrast_limits_range,
---> 61             parent=self,
     62         )
     63         self.contrastLimitsSlider.mousePressEvent = self._clim_mousepress

c:\users\genevieve\documents\github\napari\napari\_qt\qt_range_slider.py in __init__(self, initial_values, data_range, step_size, collapsible, collapsed, parent)
     76
     77         self.setRange((0, 100) if data_range is None else data_range)
---> 78         self.setValues((20, 80) if initial_values is None else initial_values)
     79         if step_size is None:
     80             # pick an appropriate slider step size based on the data range

c:\users\genevieve\documents\github\napari\napari\_qt\qt_range_slider.py in setValues(self, values)
    114
    115     def setValues(self, values):
--> 116         self.setSliderValues([self._data_to_slider_value(v) for v in values])
    117
    118     def sliderValues(self):

c:\users\genevieve\documents\github\napari\napari\_qt\qt_range_slider.py in setSliderValues(self, values)
    137         self.value_min, self.value_max = values
    138         self.valuesChanged.emit(self.values())
--> 139         self.updateDisplayPositions()
    140
    141     def setStep(self, step):

c:\users\genevieve\documents\github\napari\napari\_qt\qt_range_slider.py in updateDisplayPositions(self)
    255     def updateDisplayPositions(self):
    256         size = self.rangeSliderSize()
--> 257         range_min = int(size * self.value_min)
    258         range_max = int(size * self.value_max)
    259         self.display_min = range_min + self.handle_radius

ValueError: cannot convert float NaN to integer

c:\users\genevieve\anaconda3\envs\napari-dev\lib\site-packages\vispy\visuals\image.py:407: RuntimeWarning: invalid value encountered in greater
  if clim[1] - clim[0] > 0:

Expected behavior

I expect to be able to see all the not-NaN pixels. In my ideal case NaN pixels would be totally transparent, so you’d never see them no matter what kind of lookup table you use. If that isn’t possible, then I’d be less happy but still ok with rendering NaNs as zeros.

Environment

  • Please copy and paste the information at napari info option in help menubar here:
napari: 0.3.1+29.g1b9a8fa
Platform: Windows-10-10.0.17134-SP0
Python: 3.7.7 (default, May 6 2020, 11:45:54) [MSC v.1916 64 bit (AMD64)]
Qt: 5.14.2
PySide2: 5.14.2.1
NumPy: 1.18.4
SciPy: 1.4.1
Dask: 2.16.0
VisPy: 0.6.4

GL version: 4.4.0 - Build 20.19.15.5058
MAX_TEXTURE_SIZE: 16384

Plugins:
- napari-plugin-engine: 0.1.5
- svg: 0.1.2
  • Any other relevant information:

Additional context

napari_nan_pixel_screenshot1

napari_nan_pixel_screenshot2

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 3
  • Comments: 18 (16 by maintainers)

Most upvoted comments

bumping this. I agree, for scientific work, we definitely need to handle this more gracefully. I think @GenevieveBuckley’s expectation is spot on:

I expect to be able to see all the not-NaN pixels. NaN pixels would be totally transparent,

I think that this can be closed now that #3701 is merged. Are there any outstanding issues?

I don’t think so, https://github.com/napari/napari/pull/3701 should have fixed things. I’ll close this, we can always reopen if we discover problems later on.

(TIL there is no walrus emoji. Very sad.)

So, things like layer_utils.calc_data_range should be using np.nanmin/np.nanmax, etc…

Note that in skimage we decided against nanmin/nanmaxing everything because there is a significant performance hit, 4-8x depending on your image:

In [1]: import numpy as np

In [2]: image = np.random.random((2000, 2000))

In [3]: %timeit np.min(image)
2.08 ms ± 58.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [4]: %timeit np.nanmin(image)
8.42 ms ± 331 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [5]: image2 = np.random.random((500, 500, 500))

In [6]: %timeit np.min(image2)
67.4 ms ± 339 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [7]: %timeit np.nanmin(image2)
261 ms ± 8.66 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

So to compute nanmin/nanmax for a very reasonably sized image you are already at 16ms. The workaround is:

minval = np.min(array)
if np.isnan(minval):
    minval = np.nanmin(array)

Gets a little more compact in Py3.8+, which we can use after Christmas 😃:

if np.isnan(minval := np.min(array)):
    minval = np.nanmin(array)

After some quick tests, it appears to be a napari thing. I’ll dive in a bit more!

I’m in favour of waiting and then bumping our vispy requirement, that seem like the simplest thing to do. (Although if anyone wants to make a quick fix in image.py, I’m not opposed to that either)

Just checking, are there any known quick fixes to be able to handle this? Or is the only one for now to alter the data to convert nans?

@scottstanie Lorenzo’s comment https://github.com/napari/napari/issues/1310#issuecomment-937567875 has a few tips you can look at.

A good general way to test this would be to add some data with NaN to _tests.utils.layer_test_data … which would immediately hit a big surface of the code…

I’ve done this here, for the convenience of this discussion: https://github.com/napari/napari/pull/3446

Some good places to start digging into this are:

=========================== short test summary info ============================
FAILED napari/_qt/_tests/test_qt_viewer.py::test_add_layer[Image-data4-2] - A...
FAILED napari/_qt/_tests/test_qt_viewer.py::test_add_layer[Labels-data7-2] - ...
FAILED napari/_tests/test_adding_removing.py::test_add_remove_layer_external_callbacks[Labels-data7-2]
FAILED napari/_tests/test_view_layers.py::test_view[Image-data4-2] - Assertio...
FAILED napari/_tests/test_view_layers.py::test_view[Labels-data7-2] - TypeErr...

i think ideally, we’d like to avoid casting/copying data to a safe form if at all possible (that is, a NaN should probably stay a NaN in layer.data itself). Which kind of means that every place in the code that looks at the data would need to be “ready” for nans. So, things like layer_utils.calc_data_range should be using np.nanmin/np.nanmax, etc… A good general way to test this would be to add some data with NaN to _tests.utils.layer_test_data … which would immediately hit a big surface of the code…