Skip to content

Commit 406c961

Browse files
committed
Fix handling of None and NaN filling values in MaskedImageMixin
1 parent 46d6568 commit 406c961

File tree

3 files changed

+142
-1
lines changed

3 files changed

+142
-1
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog #
22

3+
## Version 2.8.2 ##
4+
5+
🛠️ Bug fixes:
6+
7+
* Fixed `RuntimeWarning` when changing masked image data type from float to integer:
8+
* `MaskedImageItem.update_mask()` now handles NaN and None `filling_value` gracefully
9+
* When converting to integer dtypes, NaN/None values are replaced with 0 instead of triggering numpy cast warnings
10+
* When converting to float dtypes, NaN is preserved as the fill value
11+
* Added comprehensive tests to validate dtype conversion scenarios
12+
313
## Version 2.8.1 ##
414

515
🛠️ Bug fixes:

plotpy/items/image/masked.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,17 @@ def update_mask(self) -> None:
154154
# in future versions of NumPy (at the time of writing, this raises a
155155
# DeprecationWarning "NumPy will stop allowing conversion of out-of-bound
156156
# Python integers to integer arrays.")
157-
val = np.array(self.param.filling_value).astype(self.data.dtype)
157+
filling_value = self.param.filling_value
158+
if filling_value is None or (
159+
isinstance(filling_value, float) and np.isnan(filling_value)
160+
):
161+
# Use a safe default for integer types when filling_value is None or NaN
162+
if np.issubdtype(self.data.dtype, np.integer):
163+
val = np.array(0, dtype=self.data.dtype)
164+
else:
165+
val = np.array(np.nan, dtype=self.data.dtype)
166+
else:
167+
val = np.array(filling_value).astype(self.data.dtype)
158168

159169
self.data.set_fill_value(val)
160170

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Licensed under the terms of the BSD 3-Clause
4+
# (see plotpy/LICENSE for details)
5+
6+
"""
7+
Test for MaskedImageItem dtype changes
8+
9+
This test verifies that changing the data type of a masked image from float to
10+
integer types (like float64 to uint8) doesn't raise RuntimeWarnings when the
11+
filling_value is NaN or None.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import numpy as np
17+
import pytest
18+
19+
from plotpy.builder import make
20+
21+
22+
def test_masked_image_dtype_change_from_float_to_uint8():
23+
"""Test changing masked image dtype from float64 to uint8 with NaN filling_value"""
24+
# Create a float64 masked array with NaN as fill value (the default)
25+
data = np.random.rand(10, 10).astype(np.float64)
26+
mask = np.zeros_like(data, dtype=bool)
27+
mask[3:5, 3:5] = True
28+
masked_data = np.ma.masked_array(data, mask=mask)
29+
30+
# Create masked image item
31+
item = make.maskedimage(masked_data, colormap="viridis", show_mask=True)
32+
33+
# Verify initial state
34+
assert item.data.dtype == np.float64
35+
36+
# Set filling_value to NaN (typical for float images)
37+
item.param.filling_value = np.nan
38+
39+
# Change data dtype to uint8
40+
new_data = (masked_data * 255).astype(np.uint8)
41+
item.set_data(new_data)
42+
43+
# This should not raise a RuntimeWarning
44+
with pytest.warns(None) as warning_list:
45+
item.update_mask()
46+
47+
# Check that no RuntimeWarning was raised
48+
runtime_warnings = [
49+
w for w in warning_list if issubclass(w.category, RuntimeWarning)
50+
]
51+
assert len(runtime_warnings) == 0, "RuntimeWarning should not be raised"
52+
53+
# Verify the data type changed
54+
assert item.data.dtype == np.uint8
55+
56+
57+
def test_masked_image_dtype_change_with_none_filling_value():
58+
"""Test changing masked image dtype with None filling_value"""
59+
# Create a float64 masked array
60+
data = np.random.rand(10, 10).astype(np.float64)
61+
mask = np.zeros_like(data, dtype=bool)
62+
mask[3:5, 3:5] = True
63+
masked_data = np.ma.masked_array(data, mask=mask)
64+
65+
# Create masked image item
66+
item = make.maskedimage(masked_data, colormap="viridis", show_mask=True)
67+
68+
# Set filling_value to None
69+
item.param.filling_value = None
70+
71+
# Change data dtype to uint8
72+
new_data = (masked_data * 255).astype(np.uint8)
73+
item.set_data(new_data)
74+
75+
# This should not raise a RuntimeWarning
76+
with pytest.warns(None) as warning_list:
77+
item.update_mask()
78+
79+
# Check that no RuntimeWarning was raised
80+
runtime_warnings = [
81+
w for w in warning_list if issubclass(w.category, RuntimeWarning)
82+
]
83+
assert len(runtime_warnings) == 0, "RuntimeWarning should not be raised"
84+
85+
86+
def test_masked_image_filling_value_defaults():
87+
"""Test that filling_value defaults are appropriate for different dtypes"""
88+
test_dtypes = [np.uint8, np.uint16, np.int16, np.float32, np.float64]
89+
90+
for dtype in test_dtypes:
91+
data = np.random.rand(5, 5)
92+
if np.issubdtype(dtype, np.integer):
93+
data = (data * 100).astype(dtype)
94+
else:
95+
data = data.astype(dtype)
96+
97+
mask = np.zeros_like(data, dtype=bool)
98+
mask[2:3, 2:3] = True
99+
masked_data = np.ma.masked_array(data, mask=mask)
100+
101+
item = make.maskedimage(masked_data, colormap="viridis", show_mask=True)
102+
103+
# Set filling_value to NaN (typical initial state for float images)
104+
item.param.filling_value = np.nan
105+
106+
# This should handle the conversion gracefully
107+
with pytest.warns(None) as warning_list:
108+
item.update_mask()
109+
110+
# Check that no RuntimeWarning was raised
111+
runtime_warnings = [
112+
w for w in warning_list if issubclass(w.category, RuntimeWarning)
113+
]
114+
assert len(runtime_warnings) == 0, f"RuntimeWarning for dtype {dtype}"
115+
116+
117+
if __name__ == "__main__":
118+
test_masked_image_dtype_change_from_float_to_uint8()
119+
test_masked_image_dtype_change_with_none_filling_value()
120+
test_masked_image_filling_value_defaults()
121+
print("All tests passed!")

0 commit comments

Comments
 (0)