Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 63fe7b7

Browse filesBrowse files
authored
Merge pull request #150 from kushalkolar/better-buffer-handling
better buffer handling
2 parents 9484594 + 216216a commit 63fe7b7
Copy full SHA for 63fe7b7

File tree

Expand file treeCollapse file tree

4 files changed

+80
-26
lines changed
Filter options
Expand file treeCollapse file tree

4 files changed

+80
-26
lines changed

‎fastplotlib/graphics/features/_base.py

Copy file name to clipboardExpand all lines: fastplotlib/graphics/features/_base.py
+5-5Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import *
55

66
import numpy as np
7-
from pygfx import Buffer
7+
from pygfx import Buffer, Texture
88

99

1010
supported_dtypes = [
@@ -226,7 +226,7 @@ def _update_range(self, key):
226226

227227
@property
228228
@abstractmethod
229-
def _buffer(self) -> Buffer:
229+
def buffer(self) -> Union[Buffer, Texture]:
230230
pass
231231

232232
@property
@@ -238,21 +238,21 @@ def _update_range_indices(self, key):
238238
key = cleanup_slice(key, self._upper_bound)
239239

240240
if isinstance(key, int):
241-
self._buffer.update_range(key, size=1)
241+
self.buffer.update_range(key, size=1)
242242
return
243243

244244
# else if it's a slice obj
245245
if isinstance(key, slice):
246246
if key.step == 1: # we cleaned up the slice obj so step of None becomes 1
247247
# update range according to size using the offset
248-
self._buffer.update_range(offset=key.start, size=key.stop - key.start)
248+
self.buffer.update_range(offset=key.start, size=key.stop - key.start)
249249

250250
else:
251251
step = key.step
252252
# convert slice to indices
253253
ixs = range(key.start, key.stop, step)
254254
for ix in ixs:
255-
self._buffer.update_range(ix, size=1)
255+
self.buffer.update_range(ix, size=1)
256256
else:
257257
raise TypeError("must pass int or slice to update range")
258258

‎fastplotlib/graphics/features/_colors.py

Copy file name to clipboardExpand all lines: fastplotlib/graphics/features/_colors.py
+4-4Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77

88
class ColorFeature(GraphicFeatureIndexable):
99
@property
10-
def _buffer(self):
10+
def buffer(self):
1111
return self._parent.world_object.geometry.colors
1212

1313
def __getitem__(self, item):
14-
return self._buffer.data[item]
14+
return self.buffer.data[item]
1515

1616
def __init__(self, parent, colors, n_colors: int, alpha: float = 1.0, collection_index: int = None):
1717
"""
@@ -113,7 +113,7 @@ def __setitem__(self, key, value):
113113
raise ValueError("fancy indexing for colors must be 2-dimension, i.e. [n_datapoints, RGBA]")
114114

115115
# set the user passed data directly
116-
self._buffer.data[key] = value
116+
self.buffer.data[key] = value
117117

118118
# update range
119119
# first slice obj is going to be the indexing so use key[0]
@@ -162,7 +162,7 @@ def __setitem__(self, key, value):
162162
else:
163163
raise ValueError("numpy array passed to color must be of shape (4,) or (n_colors_modify, 4)")
164164

165-
self._buffer.data[key] = new_colors
165+
self.buffer.data[key] = new_colors
166166

167167
self._update_range(key)
168168
self._feature_changed(key, new_colors)

‎fastplotlib/graphics/features/_data.py

Copy file name to clipboardExpand all lines: fastplotlib/graphics/features/_data.py
+20-11Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import *
22

33
import numpy as np
4-
from pygfx import Buffer, Texture
4+
from pygfx import Buffer, Texture, TextureView
55

66
from ._base import GraphicFeatureIndexable, cleanup_slice, FeatureEvent, to_gpu_supported_dtype
77

@@ -16,11 +16,11 @@ def __init__(self, parent, data: Any, collection_index: int = None):
1616
super(PointsDataFeature, self).__init__(parent, data, collection_index=collection_index)
1717

1818
@property
19-
def _buffer(self) -> Buffer:
19+
def buffer(self) -> Buffer:
2020
return self._parent.world_object.geometry.positions
2121

2222
def __getitem__(self, item):
23-
return self._buffer.data[item]
23+
return self.buffer.data[item]
2424

2525
def _fix_data(self, data, parent):
2626
graphic_type = parent.__class__.__name__
@@ -54,7 +54,7 @@ def __setitem__(self, key, value):
5454
# otherwise assume that they have the right shape
5555
# numpy will throw errors if it can't broadcast
5656

57-
self._buffer.data[key] = value
57+
self.buffer.data[key] = value
5858
self._update_range(key)
5959
# avoid creating dicts constantly if there are no events to handle
6060
if len(self._event_handlers) > 0:
@@ -97,29 +97,33 @@ def __init__(self, parent, data: Any):
9797
"``[x_dim, y_dim]`` or ``[x_dim, y_dim, rgb]``"
9898
)
9999

100-
data = to_gpu_supported_dtype(data)
101100
super(ImageDataFeature, self).__init__(parent, data)
102101

103102
@property
104-
def _buffer(self) -> Texture:
103+
def buffer(self) -> Texture:
104+
"""Texture buffer for the image data"""
105105
return self._parent.world_object.geometry.grid.texture
106106

107+
def update_gpu(self):
108+
"""Update the GPU with the buffer"""
109+
self._update_range(None)
110+
107111
def __getitem__(self, item):
108-
return self._buffer.data[item]
112+
return self.buffer.data[item]
109113

110114
def __setitem__(self, key, value):
111115
# make sure float32
112116
value = to_gpu_supported_dtype(value)
113117

114-
self._buffer.data[key] = value
118+
self.buffer.data[key] = value
115119
self._update_range(key)
116120

117121
# avoid creating dicts constantly if there are no events to handle
118122
if len(self._event_handlers) > 0:
119123
self._feature_changed(key, value)
120124

121125
def _update_range(self, key):
122-
self._buffer.update_range((0, 0, 0), size=self._buffer.size)
126+
self.buffer.update_range((0, 0, 0), size=self.buffer.size)
123127

124128
def _feature_changed(self, key, new_data):
125129
if key is not None:
@@ -144,9 +148,14 @@ def _feature_changed(self, key, new_data):
144148

145149
class HeatmapDataFeature(ImageDataFeature):
146150
@property
147-
def _buffer(self) -> List[Texture]:
151+
def buffer(self) -> List[Texture]:
152+
"""list of Texture buffer for the image data"""
148153
return [img.geometry.grid.texture for img in self._parent.world_object.children]
149154

155+
def update_gpu(self):
156+
"""Update the GPU with the buffer"""
157+
self._update_range(None)
158+
150159
def __getitem__(self, item):
151160
return self._data[item]
152161

@@ -162,7 +171,7 @@ def __setitem__(self, key, value):
162171
self._feature_changed(key, value)
163172

164173
def _update_range(self, key):
165-
for buffer in self._buffer:
174+
for buffer in self.buffer:
166175
buffer.update_range((0, 0, 0), size=buffer.size)
167176

168177
def _feature_changed(self, key, new_data):

‎fastplotlib/graphics/image.py

Copy file name to clipboardExpand all lines: fastplotlib/graphics/image.py
+51-6Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
from math import ceil
33
from itertools import product
44

5+
import numpy as np
56
import pygfx
67
from pygfx.utils import unpack_bitfield
78

89
from ._base import Graphic, Interaction, PreviouslyModifiedData
910
from .features import ImageCmapFeature, ImageDataFeature, HeatmapDataFeature, HeatmapCmapFeature
11+
from .features._base import to_gpu_supported_dtype
1012
from ..utils import quick_min_max
1113

1214

@@ -23,6 +25,7 @@ def __init__(
2325
vmax: int = None,
2426
cmap: str = 'plasma',
2527
filter: str = "nearest",
28+
isolated_buffer: bool = True,
2629
*args,
2730
**kwargs
2831
):
@@ -43,6 +46,10 @@ def __init__(
4346
colormap to use to display the image data, ignored if data is RGB
4447
filter: str, optional, default "nearest"
4548
interpolation filter, one of "nearest" or "linear"
49+
isolated_buffer: bool, default True
50+
If True, initialize a buffer with the same shape as the input data and then
51+
set the data, useful if the data arrays are ready-only such as memmaps.
52+
If False, the input array is itself used as the buffer.
4653
args:
4754
additional arguments passed to Graphic
4855
kwargs:
@@ -65,20 +72,29 @@ def __init__(
6572

6673
super().__init__(*args, **kwargs)
6774

68-
self.data = ImageDataFeature(self, data)
75+
data = to_gpu_supported_dtype(data)
76+
77+
# TODO: we need to organize and do this better
78+
if isolated_buffer:
79+
# initialize a buffer with the same shape as the input data
80+
# we do not directly use the input data array as the buffer
81+
# because if the input array is a read-only type, such as
82+
# numpy memmaps, we would not be able to change the image data
83+
buffer_init = np.zeros(shape=data.shape, dtype=data.dtype)
84+
else:
85+
buffer_init = data
6986

7087
if (vmin is None) or (vmax is None):
7188
vmin, vmax = quick_min_max(data)
7289

73-
texture_view = pygfx.Texture(self.data(), dim=2).get_view(filter=filter)
90+
texture_view = pygfx.Texture(buffer_init, dim=2).get_view(filter=filter)
7491

7592
geometry = pygfx.Geometry(grid=texture_view)
7693

7794
# if data is RGB
78-
if self.data().ndim == 3:
95+
if data.ndim == 3:
7996
self.cmap = None
8097
material = pygfx.ImageBasicMaterial(clim=(vmin, vmax))
81-
8298
# if data is just 2D without color information, use colormap LUT
8399
else:
84100
self.cmap = ImageCmapFeature(self, cmap)
@@ -89,6 +105,13 @@ def __init__(
89105
material
90106
)
91107

108+
self.data = ImageDataFeature(self, data)
109+
# TODO: we need to organize and do this better
110+
if isolated_buffer:
111+
# if the buffer was initialized with zeros
112+
# set it with the actual data
113+
self.data = data
114+
92115
@property
93116
def vmin(self) -> float:
94117
"""Minimum contrast limit."""
@@ -176,6 +199,7 @@ def __init__(
176199
cmap: str = 'plasma',
177200
filter: str = "nearest",
178201
chunk_size: int = 8192,
202+
isolated_buffer: bool = True,
179203
*args,
180204
**kwargs
181205
):
@@ -198,6 +222,10 @@ def __init__(
198222
interpolation filter, one of "nearest" or "linear"
199223
chunk_size: int, default 8192, max 8192
200224
chunk size for each tile used to make up the heatmap texture
225+
isolated_buffer: bool, default True
226+
If True, initialize a buffer with the same shape as the input data and then
227+
set the data, useful if the data arrays are ready-only such as memmaps.
228+
If False, the input array is itself used as the buffer.
201229
args:
202230
additional arguments passed to Graphic
203231
kwargs:
@@ -223,7 +251,17 @@ def __init__(
223251
if chunk_size > 8192:
224252
raise ValueError("Maximum chunk size is 8192")
225253

226-
self.data = HeatmapDataFeature(self, data)
254+
data = to_gpu_supported_dtype(data)
255+
256+
# TODO: we need to organize and do this better
257+
if isolated_buffer:
258+
# initialize a buffer with the same shape as the input data
259+
# we do not directly use the input data array as the buffer
260+
# because if the input array is a read-only type, such as
261+
# numpy memmaps, we would not be able to change the image data
262+
buffer_init = np.zeros(shape=data.shape, dtype=data.dtype)
263+
else:
264+
buffer_init = data
227265

228266
row_chunks = range(ceil(data.shape[0] / chunk_size))
229267
col_chunks = range(ceil(data.shape[1] / chunk_size))
@@ -249,7 +287,7 @@ def __init__(
249287
# x and y positions of the Tile in world space coordinates
250288
y_pos, x_pos = row_start, col_start
251289

252-
tex_view = pygfx.Texture(data[row_start:row_stop, col_start:col_stop], dim=2).get_view(filter=filter)
290+
tex_view = pygfx.Texture(buffer_init[row_start:row_stop, col_start:col_stop], dim=2).get_view(filter=filter)
253291
geometry = pygfx.Geometry(grid=tex_view)
254292
# material = pygfx.ImageBasicMaterial(clim=(0, 1), map=self.cmap())
255293

@@ -264,6 +302,13 @@ def __init__(
264302

265303
self.world_object.add(img)
266304

305+
self.data = HeatmapDataFeature(self, buffer_init)
306+
# TODO: we need to organize and do this better
307+
if isolated_buffer:
308+
# if the buffer was initialized with zeros
309+
# set it with the actual data
310+
self.data = data
311+
267312
@property
268313
def vmin(self) -> float:
269314
"""Minimum contrast limit."""

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.