Skip to content

Commit 1ae52b6

Browse files
authored
Fix dynspread not to count already-spread pixels and respect array bnds (#1001)
* Fix dynspread not to count already-spread pixels and respect array bounds * Defined dynspread as fraction of non-empty pixels with neighbors * Updated tests to match new behavior
1 parent 28d9b8e commit 1ae52b6

File tree

5 files changed

+90
-63
lines changed

5 files changed

+90
-63
lines changed

datashader/tests/test_transfer_functions.py

+44-27
Original file line numberDiff line numberDiff line change
@@ -912,96 +912,113 @@ def test_rgb_density():
912912
assert tf._rgb_density(data) == 1.0
913913
data = np.zeros((4, 4), dtype='uint32')
914914
assert tf._rgb_density(data) == np.inf
915-
data[2, 2] = b
915+
data[3, 3] = b
916916
assert tf._rgb_density(data) == 0
917-
data[2, 1] = data[1, 2] = data[1, 1] = b
918-
assert np.allclose(tf._rgb_density(data), 3./8.)
917+
data[2, 0] = data[0, 2] = data[1, 1] = b
918+
assert np.allclose(tf._rgb_density(data), 0.75)
919+
assert np.allclose(tf._rgb_density(data, 3), 1)
919920

920921
def test_int_array_density():
921922
data = np.ones((4, 4), dtype='uint32')
922923
assert tf._array_density(data, float_type=False) == 1.0
923924
data = np.zeros((4, 4), dtype='uint32')
924925
assert tf._array_density(data, float_type=False) == np.inf
925-
data[2, 2] = 1
926+
data[3, 3] = 1
926927
assert tf._array_density(data, float_type=False) == 0
927-
data[2, 1] = data[1, 2] = data[1, 1] = 1
928-
assert np.allclose(tf._array_density(data, float_type=False), 3./8.)
928+
data[2, 0] = data[0, 2] = data[1, 1] = 1
929+
assert np.allclose(tf._array_density(data, float_type=False), 0.75)
930+
assert np.allclose(tf._array_density(data, float_type=False, px=3), 1)
929931

932+
930933
def test_float_array_density():
931934
data = np.ones((4, 4), dtype='float32')
932935
assert tf._array_density(data, float_type=True) == 1.0
933936
data = np.full((4, 4), np.nan, dtype='float32')
934937
assert tf._array_density(data, float_type=True) == np.inf
935-
data[2, 2] = 1
938+
data[3, 3] = 1
936939
assert tf._array_density(data, float_type=True) == 0
937-
data[2, 1] = data[1, 2] = data[1, 1] = 1
938-
assert np.allclose(tf._array_density(data, float_type=True), 3./8.)
939-
940+
data[2, 0] = data[0, 2] = data[1, 1] = 1
941+
assert np.allclose(tf._array_density(data, float_type=True), 0.75)
942+
assert np.allclose(tf._array_density(data, float_type=True, px=3), 1)
943+
940944

941945
def test_rgb_dynspread():
942946
b = 0xffff0000
947+
coords = [np.arange(5), np.arange(5)]
943948
data = np.array([[b, b, 0, 0, 0],
944949
[b, b, 0, 0, 0],
945950
[0, 0, 0, 0, 0],
946951
[0, 0, 0, b, 0],
947952
[0, 0, 0, 0, 0]], dtype='uint32')
948-
coords = [np.arange(5), np.arange(5)]
949953
img = tf.Image(data, coords=coords, dims=dims)
950-
assert tf.dynspread(img).equals(tf.spread(img, 1))
951-
assert tf.dynspread(img, threshold=0.9).equals(tf.spread(img, 2))
952-
assert tf.dynspread(img, threshold=0).equals(img)
954+
assert tf.dynspread(img).equals(img)
955+
data = np.array([[b, 0, 0, 0, 0],
956+
[0, 0, 0, 0, 0],
957+
[b, 0, 0, 0, b],
958+
[0, 0, 0, 0, 0],
959+
[0, 0, 0, 0, 0]], dtype='uint32')
960+
img = tf.Image(data, coords=coords, dims=dims)
961+
assert tf.dynspread(img, threshold=0.4).equals(tf.spread(img, 0))
962+
assert tf.dynspread(img, threshold=0.7).equals(tf.spread(img, 1))
963+
assert tf.dynspread(img, threshold=1.0).equals(tf.spread(img, 3))
953964
assert tf.dynspread(img, max_px=0).equals(img)
954965

955966
pytest.raises(ValueError, lambda: tf.dynspread(img, threshold=1.1))
956967
pytest.raises(ValueError, lambda: tf.dynspread(img, max_px=-1))
957968

958969
def test_array_dynspread():
970+
coords = [np.arange(5), np.arange(5)]
959971
data = np.array([[1, 1, 0, 0, 0],
960972
[1, 1, 0, 0, 0],
961973
[0, 0, 0, 0, 0],
962974
[0, 0, 0, 1, 0],
963975
[0, 0, 0, 0, 0]], dtype='uint32')
964-
coords = [np.arange(5), np.arange(5)]
965976
arr = xr.DataArray(data, coords=coords, dims=dims)
966-
assert tf.dynspread(arr).equals(tf.spread(arr, 1))
967-
assert tf.dynspread(arr, threshold=0.9).equals(tf.spread(arr, 2))
968-
assert tf.dynspread(arr, threshold=0).equals(arr)
977+
assert tf.dynspread(arr).equals(arr)
978+
data = np.array([[1, 0, 0, 0, 0],
979+
[0, 0, 0, 0, 0],
980+
[1, 0, 0, 0, 1],
981+
[0, 0, 0, 0, 0],
982+
[0, 0, 0, 0, 0]], dtype='uint32')
983+
arr = xr.DataArray(data, coords=coords, dims=dims)
984+
assert tf.dynspread(arr, threshold=0.4).equals(tf.spread(arr, 0))
985+
assert tf.dynspread(arr, threshold=0.7).equals(tf.spread(arr, 1))
986+
assert tf.dynspread(arr, threshold=1.0).equals(tf.spread(arr, 3))
969987
assert tf.dynspread(arr, max_px=0).equals(arr)
970988

971989
pytest.raises(ValueError, lambda: tf.dynspread(arr, threshold=1.1))
972990
pytest.raises(ValueError, lambda: tf.dynspread(arr, max_px=-1))
973991

974992

975993
def test_categorical_dynspread():
976-
a_data = np.array([[0, 1, 0, 0, 0],
994+
a_data = np.array([[1, 0, 0, 0, 0],
977995
[0, 0, 0, 0, 0],
978996
[0, 0, 0, 0, 0],
979997
[0, 0, 0, 0, 0],
980998
[0, 0, 0, 0, 0]], dtype='int32')
981999

9821000
b_data = np.array([[0, 0, 0, 0, 0],
983-
[0, 1, 0, 0, 0],
9841001
[0, 0, 0, 0, 0],
1002+
[1, 0, 0, 0, 0],
9851003
[0, 0, 0, 0, 0],
9861004
[0, 0, 0, 0, 0]], dtype='int32')
9871005

988-
c_data = np.array([[1, 0, 0, 0, 0],
989-
[1, 0, 0, 0, 0],
1006+
c_data = np.array([[0, 0, 0, 0, 0],
1007+
[0, 0, 0, 0, 0],
1008+
[0, 0, 0, 0, 1],
9901009
[0, 0, 0, 0, 0],
991-
[0, 0, 0, 1, 0],
9921010
[0, 0, 0, 0, 0]], dtype='int32')
9931011

9941012
data = np.dstack([a_data, b_data, c_data])
9951013
coords = [np.arange(5), np.arange(5)]
9961014
arr = xr.DataArray(data, coords=coords + [['a', 'b', 'c']],
9971015
dims=dims + ['cat'])
998-
assert tf.dynspread(arr).equals(tf.spread(arr, 1))
999-
assert tf.dynspread(arr, threshold=0.9).equals(tf.spread(arr, 2))
1000-
assert tf.dynspread(arr, threshold=0).equals(arr)
1016+
assert tf.dynspread(arr, threshold=0.4).equals(tf.spread(arr, 0))
1017+
assert tf.dynspread(arr, threshold=0.7).equals(tf.spread(arr, 1))
1018+
assert tf.dynspread(arr, threshold=1.0).equals(tf.spread(arr, 3))
10011019
assert tf.dynspread(arr, max_px=0).equals(arr)
10021020

10031021

1004-
10051022
def check_eq_hist_cdf_slope(eq):
10061023
# Check that the slope of the cdf is ~1
10071024
# Adapted from scikit-image's test for the same function

datashader/transfer_functions/__init__.py

+40-30
Original file line numberDiff line numberDiff line change
@@ -744,60 +744,70 @@ def dynspread(img, threshold=0.5, max_px=3, shape='circle', how=None, name=None)
744744
raise ValueError("max_px must be >= 0")
745745
# Simple linear search. Not super efficient, but max_px is usually small.
746746
float_type = img.dtype in [np.float32, np.float64]
747-
for px in range(max_px + 1):
748-
out = spread(img, px, shape=shape, how=how, name=name)
747+
px_=0
748+
for px in range(1, max_px + 1):
749+
px_=px
749750
if is_image:
750-
density = _rgb_density(out.data)
751+
density = _rgb_density(img.data, px*2)
751752
elif len(img.shape) == 2:
752-
density = _array_density(out.data, float_type)
753+
density = _array_density(img.data, float_type, px*2)
753754
else:
754-
masked = np.logical_not(np.isnan(out)) if float_type else (out != 0)
755+
masked = np.logical_not(np.isnan(img)) if float_type else (img != 0)
755756
flat_mask = np.sum(masked, axis=2, dtype='uint32')
756-
density = _array_density(flat_mask.data, False)
757-
if density >= threshold:
757+
density = _array_density(flat_mask.data, False, px*2)
758+
if density > threshold:
759+
px_=px_-1
758760
break
759-
760-
return out
761+
762+
if px_>=1:
763+
return spread(img, px_, shape=shape, how=how, name=name)
764+
else:
765+
return img
761766

762767

763768
@nb.jit(nopython=True, nogil=True, cache=True)
764-
def _array_density(arr, float_type):
769+
def _array_density(arr, float_type, px=1):
765770
"""Compute a density heuristic of an array.
766771
767772
The density is a number in [0, 1], and indicates the normalized mean number
768-
of non-empty adjacent pixels for each non-empty pixel.
773+
of non-empty pixels that have neighbors in the given px radius.
769774
"""
770775
M, N = arr.shape
771-
cnt = total = 0
772-
for y in range(1, M - 1):
773-
for x in range(1, N - 1):
776+
cnt = has_neighbors = 0
777+
for y in range(0, M):
778+
for x in range(0, N):
774779
el = arr[y, x]
775780
if (float_type and not np.isnan(el)) or (not float_type and el!=0):
776781
cnt += 1
777-
for i in range(y - 1, y + 2):
778-
for j in range(x - 1, x + 2):
779-
if float_type and not np.isnan(arr[i, j]):
780-
total += 1
781-
elif not float_type and arr[i, j] != 0:
782-
total += 1
783-
return (total - cnt)/(cnt * 8) if cnt else np.inf
782+
neighbors = 0
783+
for i in range(max(0, y - px), min(y + px + 1, M)):
784+
for j in range(max(0, x - px), min(x + px + 1, N)):
785+
if ((float_type and not np.isnan(arr[i, j])) or
786+
(not float_type and arr[i, j] != 0)):
787+
neighbors += 1
788+
if neighbors>1: # (excludes self)
789+
has_neighbors += 1
790+
return has_neighbors/cnt if cnt else np.inf
784791

785792

786793
@nb.jit(nopython=True, nogil=True, cache=True)
787-
def _rgb_density(arr):
794+
def _rgb_density(arr, px=1):
788795
"""Compute a density heuristic of an image.
789796
790797
The density is a number in [0, 1], and indicates the normalized mean number
791-
of non-empty adjacent pixels for each non-empty pixel.
798+
of non-empty pixels that have neighbors in the given px radius.
792799
"""
793800
M, N = arr.shape
794-
cnt = total = 0
795-
for y in range(1, M - 1):
796-
for x in range(1, N - 1):
801+
cnt = has_neighbors = 0
802+
for y in range(0, M):
803+
for x in range(0, N):
797804
if (arr[y, x] >> 24) & 255:
798805
cnt += 1
799-
for i in range(y - 1, y + 2):
800-
for j in range(x - 1, x + 2):
806+
neighbors = 0
807+
for i in range(max(0, y - px), min(y + px + 1, M)):
808+
for j in range(max(0, x - px), min(x + px + 1, N)):
801809
if (arr[i, j] >> 24) & 255:
802-
total += 1
803-
return (total - cnt)/(cnt * 8) if cnt else np.inf
810+
neighbors += 1
811+
if neighbors>1: # (excludes self)
812+
has_neighbors += 1
813+
return has_neighbors/cnt if cnt else np.inf

examples/getting_started/3_Interactivity.ipynb

+2-2
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@
181181
"outputs": [],
182182
"source": [
183183
"datashaded = hd.datashade(points, aggregator=ds.count_cat('cat')).redim.range(x=(-5,5),y=(-5,5))\n",
184-
"hd.dynspread(datashaded, threshold=0.50, how='over').opts(height=500,width=500)"
184+
"hd.dynspread(datashaded, threshold=0.8, how='over', max_px=5).opts(height=500,width=500)"
185185
]
186186
},
187187
{
@@ -214,7 +214,7 @@
214214
"gaussspread = hd.dynspread(datashaded, threshold=0.50, how='over').opts(plot=dict(height=400,width=400))\n",
215215
"\n",
216216
"color_key = [(name,color) for name,color in zip([\"d1\",\"d2\",\"d3\",\"d4\",\"d5\"], Sets1to3)]\n",
217-
"color_points = hv.NdOverlay({n: hv.Points([0,0], label=str(n)).opts(style=dict(color=c)) for n,c in color_key})\n",
217+
"color_points = hv.NdOverlay({n: hv.Points([0,0], label=str(n)).opts(color=c,size=0) for n,c in color_key})\n",
218218
"\n",
219219
"color_points * gaussspread"
220220
]

examples/user_guide/4_Trajectories.ipynb

+3-3
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@
150150
"metadata": {},
151151
"outputs": [],
152152
"source": [
153-
"from holoviews.operation.datashader import datashade\n",
153+
"from holoviews.operation.datashader import datashade, spread\n",
154154
"import holoviews as hv\n",
155155
"hv.extension('bokeh')"
156156
]
@@ -168,8 +168,8 @@
168168
"metadata": {},
169169
"outputs": [],
170170
"source": [
171-
"opts = hv.opts.RGB(width=500, height=500)\n",
172-
"datashade(hv.Path(df, kdims=['x','y']), normalization='linear', aggregator=ds.any()).opts(opts)"
171+
"opts = hv.opts.RGB(width=900, height=500, aspect='equal')\n",
172+
"spread(datashade(hv.Path(df, kdims=['x','y']), normalization='linear', aggregator=ds.any())).opts(opts)"
173173
]
174174
},
175175
{

examples/user_guide/7_Networks.ipynb

+1-1
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@
414414
"\n",
415415
"circle = hv.Graph(edges, label='Bokeh edges').opts(node_size=5)\n",
416416
"hnodes = circle.nodes.opts(size=5)\n",
417-
"dscirc = (hd.dynspread(hd.datashade(circle))*hnodes).relabel(\"Datashader edges\")\n",
417+
"dscirc = (hd.spread(hd.datashade(circle))*hnodes).relabel(\"Datashader edges\")\n",
418418
"\n",
419419
"circle + dscirc"
420420
]

0 commit comments

Comments
 (0)