Skip to content

Commit

Permalink
Update color texture build to reduce collisions, and fix collision ha…
Browse files Browse the repository at this point in the history
…ndling (#6182)

# Description

During work on #6179 I found that the mechanism for avoiding collision
does not work correctly (see tests). I also found that the shader code
do not have a stop condition.

In this PR, I also add a dynamic calculation of the hash table size. I
also decide to use prime numbers as the size of the table to reduce
collision probability.

---------

Co-authored-by: Juan Nunez-Iglesias <jni@fastmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Lorenzo Gaifas <brisvag@gmail.com>
  • Loading branch information
4 people committed Oct 2, 2023
1 parent cad51af commit 7ed4ea8
Show file tree
Hide file tree
Showing 6 changed files with 409 additions and 36 deletions.
2 changes: 1 addition & 1 deletion examples/dev/direct-colormap-aliasing-test.py
Expand Up @@ -56,7 +56,7 @@
texel_pos_img = np.zeros((1, nb_steps, 4))
texel_pos_img[..., -1] = 1 # alpha
for k in range(nb_steps):
grid_position = hash2d_get(k + 1, keys, values)[0]
grid_position = hash2d_get(k + 1, keys)[0]
# divide by shape and set to RG values like in shader (tex coords)
texel_pos_img[:, k, :2] = (np.array(grid_position) + 0.5) / tex_shape

Expand Down
121 changes: 121 additions & 0 deletions napari/_vispy/_tests/test_vispy_labels.py
@@ -0,0 +1,121 @@
from itertools import product
from unittest.mock import patch

import numpy as np
import pytest

from napari._vispy.layers.labels import (
MAX_LOAD_FACTOR,
PRIME_NUM_TABLE,
build_textures_from_dict,
hash2d_get,
idx_to_2d,
)


@pytest.fixture(scope='module', autouse=True)
def mock_max_texture_size():
"""When running tests in this file, pretend max texture size is 2^16."""
with patch('napari._vispy.layers.labels.MAX_TEXTURE_SIZE', 2**16):
yield


def test_idx_to_2d():
assert idx_to_2d(0, (100, 100)) == (0, 0)
assert idx_to_2d(1, (100, 100)) == (0, 1)
assert idx_to_2d(101, (100, 100)) == (1, 1)
assert idx_to_2d(521, (100, 100)) == (5, 21)
assert idx_to_2d(100 * 100 + 521, (100, 100)) == (5, 21)


def test_build_textures_from_dict():
keys, values, collision = build_textures_from_dict(
{1: (1, 1, 1, 1), 2: (2, 2, 2, 2)}
)
assert not collision
assert keys.shape == (37, 37)
assert values.shape == (37, 37, 4)
assert keys[0, 1] == 1
assert keys[0, 2] == 2
assert np.array_equiv(values[0, 1], (1, 1, 1, 1))
assert np.array_equiv(values[0, 2], (2, 2, 2, 2))


def test_build_textures_from_dict_too_many_labels(monkeypatch):
with pytest.raises(MemoryError):
build_textures_from_dict(
{i: (i, i, i, i) for i in range(1001)}, shape=(10, 10)
)
monkeypatch.setattr(
"napari._vispy.layers.labels.PRIME_NUM_TABLE", [[61], [127]]
)
with pytest.raises(MemoryError):
build_textures_from_dict(
{i: (i, i, i, i) for i in range((251**2) // 2)},
)


def test_size_of_texture_square():
count = int(127 * 127 * MAX_LOAD_FACTOR) - 1
keys, values, *_ = build_textures_from_dict(
{i: (i, i, i, i) for i in range(count)}
)
assert keys.shape == (127, 127)
assert values.shape == (127, 127, 4)


def test_size_of_texture_rectangle():
count = int(128 * 128 * MAX_LOAD_FACTOR) + 5
keys, values, *_ = build_textures_from_dict(
{i: (i, i, i, i) for i in range(count)}
)
assert keys.shape == (251, 127)
assert values.shape == (251, 127, 4)


def test_build_textures_from_dict_collision():
keys, values, collision = build_textures_from_dict(
{1: (1, 1, 1, 1), 26: (2, 2, 2, 2), 27: (3, 3, 3, 3)}, shape=(5, 5)
)
assert collision
assert keys.shape == (5, 5)
assert keys[0, 1] == 1
assert keys[0, 2] == 26
assert keys[0, 3] == 27
assert np.array_equiv(values[0, 1], (1, 1, 1, 1))
assert np.array_equiv(values[0, 2], (2, 2, 2, 2))
assert np.array_equiv(values[0, 3], (3, 3, 3, 3))

assert hash2d_get(1, keys) == (0, 1)
assert hash2d_get(26, keys) == (0, 2)
assert hash2d_get(27, keys) == (0, 3)


def test_collide_keys():
base_keys = [x * y for x, y in product(PRIME_NUM_TABLE[0], repeat=2)]
colors = {0: (0, 0, 0, 0), 1: (1, 1, 1, 1)}
colors.update({i + 10: (1, 0, 0, 1) for i in base_keys})
colors.update({2 * i + 10: (0, 1, 0, 1) for i in base_keys})
keys, values, collision = build_textures_from_dict(colors)
assert not collision
assert keys.shape == (37, 61)
assert values.shape == (37, 61, 4)


def test_collide_keys2():
base_keys = [x * y for x, y in product(PRIME_NUM_TABLE[0], repeat=2)] + [
x * y for x, y in product(PRIME_NUM_TABLE[0], PRIME_NUM_TABLE[1])
]
colors = {0: (0, 0, 0, 0), 1: (1, 1, 1, 1)}
colors.update({i + 10: (1, 0, 0, 1) for i in base_keys})
colors.update({2 * i + 10: (0, 1, 0, 1) for i in base_keys})

# enforce collision for collision table of size 31
colors.update({31 * i + 10: (0, 0, 1, 1) for i in base_keys})
# enforce collision for collision table of size 29
colors.update({29 * i + 10: (0, 0, 1, 1) for i in base_keys})

keys, values, collision = build_textures_from_dict(colors)
assert collision
assert keys.shape == (37, 37)
assert values.shape == (37, 37, 4)
2 changes: 1 addition & 1 deletion napari/_vispy/layers/image.py
Expand Up @@ -167,7 +167,7 @@ def _on_depiction_change(self):
if isinstance(self.node, VolumeNode):
self.node.raycasting_mode = str(self.layer.depiction)

def _on_colormap_change(self):
def _on_colormap_change(self, event=None):
self.node.cmap = VispyColormap(*self.layer.colormap)

def _update_mip_minip_cutoff(self):
Expand Down

0 comments on commit 7ed4ea8

Please sign in to comment.