Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix label direct mode for installation without numba #6571

Merged
merged 8 commits into from Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/reusable_pip_test.yml
Expand Up @@ -27,6 +27,10 @@ jobs:
env:
PIP_CONSTRAINT: napari-from-github/resources/constraints/constraints_py3.9.txt

- name: uninstall numba
run: |
pip uninstall -y numba

- name: Test
uses: aganders3/headless-gui@v1
with:
Expand Down
15 changes: 10 additions & 5 deletions napari/_qt/_tests/test_qt_viewer.py
Expand Up @@ -749,7 +749,9 @@ def test_label_colors_matching_widget_auto(

@skip_local_popups
@skip_on_win_ci
@pytest.mark.parametrize("use_selection", [True, False])
@pytest.mark.parametrize(
"use_selection", [True, False], ids=["selected", "all"]
)
@pytest.mark.parametrize("dtype", [np.uint64, np.uint16, np.uint8, np.int16])
def test_label_colors_matching_widget_direct(
qtbot, qt_viewer_with_controls, use_selection, dtype
Expand Down Expand Up @@ -786,12 +788,14 @@ def test_label_colors_matching_widget_direct(
color_box_color, middle_pixel = _update_data(
layer, label, qtbot, qt_viewer_with_controls, dtype
)
assert np.allclose(color_box_color, middle_pixel, atol=1), label
assert np.allclose(
npt.assert_almost_equal(
color_box_color, middle_pixel, err_msg=f"{label=}"
)
npt.assert_almost_equal(
color_box_color,
layer.color.get(label, layer.color[None]) * 255,
atol=1,
), label
err_msg=f"{label=}",
)


def test_axes_labels(make_napari_viewer):
Expand Down Expand Up @@ -1006,6 +1010,7 @@ def test_all_supported_dtypes(qt_viewer):


def test_more_than_uint16_colors(qt_viewer):
pytest.importorskip("numba")
# this test is slow (10s locally)
data = np.zeros((10, 10), dtype=np.uint32)
colors = {
Expand Down
1 change: 1 addition & 0 deletions napari/layers/labels/_tests/test_labels.py
Expand Up @@ -1468,6 +1468,7 @@ def test_is_default_color():

def test_large_labels_direct_color():
"""Make sure direct color works with large label ranges"""
pytest.importorskip('numba')
data = np.array([[0, 1], [2**16, 2**20]], dtype=np.uint32)
colors = {1: 'white', 2**16: 'green', 2**20: 'magenta'}
layer = Labels(data)
Expand Down
23 changes: 23 additions & 0 deletions napari/utils/colormaps/_tests/test_colormap.py
Expand Up @@ -11,6 +11,7 @@
from napari.utils.colormaps.colormap import (
MAPPING_OF_UNKNOWN_VALUE,
DirectLabelColormap,
_labels_raw_to_texture_direct_numpy,
)
from napari.utils.colormaps.colormap_utils import label_colormap

Expand Down Expand Up @@ -430,3 +431,25 @@ def test_direct_colormap_negative_values():
# Map multiple values
mapped = cmap.map(np.array([-1, -2], dtype=np.int8))
npt.assert_array_equal(mapped, np.array([[1, 0, 0, 1], [0, 1, 0, 1]]))


def test_direct_colormap_negative_values_numpy():
color_dict = {
-1: np.array([1, 0, 0, 1]),
-2: np.array([0, 1, 0, 1]),
None: np.array([0, 0, 0, 1]),
}
cmap = DirectLabelColormap(color_dict=color_dict)

res = _labels_raw_to_texture_direct_numpy(
np.array([-1, -2, 5], dtype=np.int8), cmap
)
npt.assert_array_equal(res, [1, 2, 0])

cmap.selection = -2
cmap.use_selection = True

res = _labels_raw_to_texture_direct_numpy(
np.array([-1, -2, 5], dtype=np.int8), cmap
)
npt.assert_array_equal(res, [0, 1, 0])
16 changes: 13 additions & 3 deletions napari/utils/colormaps/colormap.py
Expand Up @@ -534,7 +534,12 @@ def _get_typed_dict_mapping(self, data_dtype: np.dtype) -> 'typed.Dict':
@cached_property
def _array_map(self):
"""Create an array to map labels to texture values of smaller dtype."""
max_value = max(x for x in self.color_dict if x is not None)

max_value = max(
(abs(x) for x in self.color_dict if x is not None), default=0
)
if any(x < 0 for x in self.color_dict if x is not None):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We really need to deprecate None and use defaultdict I think... (next PR) Anyway, like below this isn't tested — would you like it to be?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is tested, but not with code coverage (in pip job). I will add additional tests

Copy link
Collaborator Author

@Czaki Czaki Jan 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not want to use defaultdict globally. As defaultdict is adding elements when they are missed. So size is increasing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The option may be to add an additional property to DirectLAbelColormap to store default color outside color dict

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point @Czaki, I like this option.

max_value *= 2
if max_value > 2**16:
raise RuntimeError( # pragma: no cover
"Cannot use numpy implementation for large values of labels "
Expand Down Expand Up @@ -827,10 +832,15 @@ def _labels_raw_to_texture_direct_numpy(

See `_cast_labels_data_to_texture_dtype_direct` for more details.
"""
if direct_colormap.use_selection:
return (data == direct_colormap.selection).astype(np.uint8)
mapper = direct_colormap._array_map

if data.dtype.itemsize > 2:
if any(x < 0 for x in direct_colormap.color_dict if x is not None):
Czaki marked this conversation as resolved.
Show resolved Hide resolved
half_shape = mapper.shape[0] // 2 - 1
data = np.clip(data, -half_shape, half_shape)
else:
data = np.clip(data, 0, mapper.shape[0] - 1)

return mapper[data]


Expand Down