Skip to content

Commit

Permalink
Remove catch_warnings in slicing (#5603)
Browse files Browse the repository at this point in the history
# Description
This removes use of `catch_warnings` in slicing because [using that
context manager has the side of effect of forgetting any previously
raised warnings](python/cpython#73858). Using
those in slicing means that any warnings raised in slicing are shown
every time, whereas [the default behavior for Python is to only show the
first time per
location](https://docs.python.org/3/library/warnings.html#the-warnings-filter).

The removal of `catch_warnings` in `Image._update_thumbnail` is
justified because [napari requires scipy
>=1.4.1](https://github.com/andy-sweet/napari/blob/196fb164ba12402dd10869fa8e4fd62ed8b6de00/setup.cfg#L71)
and because the warning that was catching was [removed in scipy
1.4.0](scipy/scipy#10395) (thanks @jni!).

The one in the `Layer.thumbnail` setter is a little harder to justify,
but here's my reasoning.
- I protected one common case of warnings, which is non-finite values.
- Even if we get a warning, it will be shown once by default.
- Since we depend on scikit-image now, I considered just using its
`img_to_ubyte` instead of our `convert_to_uint8`, but I think it warns
in more cases.

Obviously, we have other usage of `catch_warnings` that could cause
similar problems and this PR doesn't touch those. In general, we should
be cautious about using `catch_warnings` in code paths that are hit
frequently.

## Type of change
- [x] Bug-fix (non-breaking change which fixes an issue)

# References
Closes #5588.

# How has this been tested?
- [x] all existing tests pass with my change

I manually tested with `examples/add_multiscale_image`.
  • Loading branch information
andy-sweet authored and Czaki committed Jun 21, 2023
1 parent 597fac0 commit 6d18eed
Show file tree
Hide file tree
Showing 3 changed files with 17 additions and 22 deletions.
4 changes: 1 addition & 3 deletions napari/layers/base/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -868,9 +868,7 @@ def thumbnail(self, thumbnail):
if 0 in thumbnail.shape:
thumbnail = np.zeros(self._thumbnail_shape, dtype=np.uint8)
if thumbnail.dtype != np.uint8:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
thumbnail = convert_to_uint8(thumbnail)
thumbnail = convert_to_uint8(thumbnail)

padding_needed = np.subtract(self._thumbnail_shape, thumbnail.shape)
pad_amounts = [(p // 2, (p + 1) // 2) for p in padding_needed]
Expand Down
18 changes: 6 additions & 12 deletions napari/layers/image/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -900,12 +900,9 @@ def _update_thumbnail(self):
)
zoom_factor = tuple(new_shape / image.shape[:2])
if self.rgb:
# warning filter can be removed with scipy 1.4
with warnings.catch_warnings():
warnings.simplefilter("ignore")
downsampled = ndi.zoom(
image, zoom_factor + (1,), prefilter=False, order=0
)
downsampled = ndi.zoom(
image, zoom_factor + (1,), prefilter=False, order=0
)
if image.shape[2] == 4: # image is RGBA
colormapped = np.copy(downsampled)
colormapped[..., 3] = downsampled[..., 3] * self.opacity
Expand All @@ -922,12 +919,9 @@ def _update_thumbnail(self):
alpha = np.full(downsampled.shape[:2] + (1,), self.opacity)
colormapped = np.concatenate([downsampled, alpha], axis=2)
else:
# warning filter can be removed with scipy 1.4
with warnings.catch_warnings():
warnings.simplefilter("ignore")
downsampled = ndi.zoom(
image, zoom_factor, prefilter=False, order=0
)
downsampled = ndi.zoom(
image, zoom_factor, prefilter=False, order=0
)
low, high = self.contrast_limits
downsampled = np.clip(downsampled, low, high)
color_range = high - low
Expand Down
17 changes: 10 additions & 7 deletions napari/layers/utils/layer_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,18 +279,20 @@ def segment_normal(a, b, p=(0, 0, 1)):

def convert_to_uint8(data: np.ndarray) -> np.ndarray:
"""
Convert array content to uint8.
Convert array content to uint8, always returning a copy.
If all negative values are changed on 0.
Based on skimage.util.dtype._convert but limited to an output type uint8,
so should be equivalent to skimage.util.dtype.img_as_ubyte.
If values are integer and bellow 256 it is simple casting otherwise maximum value for this data type is picked
and values are scaled by 255/maximum type value.
If all negative, values are clipped to 0.
Binary images ar converted to [0,255] images.
If values are integers and below 256, this simply casts.
Otherwise the maximum value for the input data type is determined and
output values are proportionally scaled by this value.
float images are multiply by 255 and then casted to uint8.
Binary images are converted so that False -> 0, True -> 255.
Based on skimage.util.dtype.convert but limited to output type uint8
Float images are multiplied by 255 and then cast to uint8.
"""
out_dtype = np.dtype(np.uint8)
out_max = np.iinfo(out_dtype).max
Expand All @@ -303,6 +305,7 @@ def convert_to_uint8(data: np.ndarray) -> np.ndarray:
image_out = np.multiply(data, out_max, dtype=data.dtype)
np.rint(image_out, out=image_out)
np.clip(image_out, 0, out_max, out=image_out)
image_out = np.nan_to_num(image_out, copy=False)
return image_out.astype(out_dtype)

if in_kind in "ui":
Expand Down

0 comments on commit 6d18eed

Please sign in to comment.