Sorry, something went wrong.
|
If we add the following properties we can make things way more arbitrary: dim_names: dict that maps numerical dim index to a dimension name, ex: {0: "t", 1: "z"}. Only need to name scrollable dims. index: dict that maps dim name to current index, ex: {"t": 123, "z": 5} image_dims: tuple[int, int] | tuple[int, int, int] numerical dim indices that define the image displayed in the ImageGraphic or ImageVolumeGraphic. Also, for window_funcs, if a window func is provided for only one dim then it should apply the window only along that dim. If window_funcs are defined for multiple dimensions, then apply the window for all provided dims. Can also have a way to define the order, ex. do "t" first and "z" etc. Histogram calculation can also be done within this widget anytime the window funcs or frame_apply funcs are changed. We can then just allow force-setting the HistogramLUTGraph. |
Sorry, something went wrong.
|
Make a few ready-made subclasses of ImageWidgetArray, for example IWA_Dask, IWA_Lazy etc. In ImageWidget.__init__(), parse each data array in the list use the appropriate IWA type based on the data array type. IW also has a new kwarg iwa_types which can explicitly define the IWA type which corresponds to the data array. Make the window functions more flexible. Allow options kwargs that include the full data array, i.e. IWA.data, and the current index. Useful for things like computing a window function and then using that on the original data or current frame. Example, use a rolling filter on the current window, use this to subtract from the original frame. |
Sorry, something went wrong.
|
new kwarg on ImageWidget, "adaptive_histogram", List[bool] | bool which creates an event handler for current_index so that any time the slider is moved or the index is changed it sets the histogram based on the current frame/volume. |
Sorry, something went wrong.
|
If the data doesn't change when moving through dims, example: if there's a simple 2D image amongst multiple txy videos, then would be good to handle this efficiently so that Texture doesn't isn't uploaded when it doesn't change. |
Sorry, something went wrong.
|
ok so another issue is that you can't use a window function when you have a mix of movies and images (no t dim) 😞 . So this separation also needs to fix this issue. Also need to make it possible for an ImageWidget subplot to be blank, set that data index as None and just ignore when scrolling. |
Sorry, something went wrong.
|
argument to specify a transpose tuple of size 2 or 3 that is applied on the final image/volume after slicing, window functions, etc. could be useful. |
Sorry, something went wrong.
|
ok I think I got a good way to apply window funcs, clean, understandable. # array with many slider_dims
a = np.random.rand(25, 20, 8, 4, 100, 100)
print("shape of a:\t\t", a.shape)
# final slice is: (i - w, i, i + w)
# specify window size for each dim
windows = (3, None, 2, None)
# order in which window funcs are applied
order = (0, 2)
# window function for each dim
funcs = (np.mean, None, np.std, None)
# current slider indices
indices = (15, 5, 4, 2)
indexer = list()
for i, w in zip(indices, windows):
if w is not None:
s = slice(i - w, i + w, 1) # start, stop, step
else:
s = slice(i, i + 1, 1)
indexer.append(s)
print("indexer:\t\t", indexer)
a_s = a[tuple(indexer)]
# also provide the user the option to use this window-sliced array in one function where they handle everything
print("indexer applied to a:\t", a_s.shape)
for dim in order:
f = funcs[dim]
a = f(a, axis=dim, keepdims=True)
print(f"shape of a after window func: {f} on dim: {dim} is:\t {a.shape}")
|
Sorry, something went wrong.
|
Thoughts on how sliders should map onto dims: if we have this array, let's say it's [t, z, m, n]: then, do we assume the following smaller array is [t, m, n] or [z, m, n]? Another way to think about this: We want to allow arbitrary numbers of dims, so if we have the following array with ndim 6: [m, n] are the image dims (it could also be RGB or volumetric, that stuff is easy to deal with, ignore it for now) Do we assume a smaller array with ndim 4 is: or: [c, d, m, n] Sliders will be made for a, b, c, d so this is important to decide. One way to thing of it is: right-most are image dims, so slider dims accumulate as soon as image dims finish? Another way to think of it is, slider dims accumulate from left to right until they reach the image dims. EDIT: We have chosen to do [c, d, m, n] |
Sorry, something went wrong.
|
I think accumulating the slider dims from left to right makes sense. However, depending on the use-case, I would want to be able to choose between [a b m n] and [c d m n]. For microscopy, [t z m n] is the most popular but I've also seen libraries use [z t m n] (e.g. suite3d). These two dim orders behave completely differently and take a lot of fine-tuning in dask/numpy depending on which you use. What about defaulting to "left to right" mapping with an optional semantic argument, e.g.: ImageWidget(
data=array,
image_dims=(-2, -1), # or just always the last two dims
slider_dims=(0, 1, 2, 3), # explicit ordering
dim_names={0: "t", 1: "z", 2: "c"} # for imgui slider labels
)
|
Sorry, something went wrong.
|
Another important thing: other than slider dims, nothing else has to be the same between iw arrays. For example, we can allow each individual array in the ImageWidget to have their own window function or frame apply function. Not sure what's the best API for this though. Maybe the constructor can take a window funca arg, which if it's a list it defines the window func per data array. This would of course be symmetric with the properties. |
Sorry, something went wrong.
|
display_dims should be a mutable property. When changed from 2 or 3, delete the existing graphic and replace it. |
Sorry, something went wrong.
|
ok I got the basics down! I now realize the histogram is not gonna be trivial 😄 We can't sub-sample the original array and then pass it through the window funcs because that won't correspond to windows in the original data. I think we need to do something like this:
I think we can use some of the logic in utils.subsample_array and do something like;
|
Sorry, something went wrong.
|
Thoughts on how sliders should map onto dims: if we have this array, let's say it's [t, z, m, n]: [1_000, 30, 100, 100] then, do we assume the following smaller array is [t, m, n] or [z, m, n]? [n, 100, 100] Another way to think about this: We want to allow arbitrary numbers of dims, so if we have the following array with ndim 6: [a, b, c, d, m, n] [m, n] are the image dims (it could also be RGB or volumetric, that stuff is easy to deal with, ignore it for now) Do we assume a smaller array with ndim 4 is: [a, b, m, n] or: [c, d, m, n] Sliders will be made for a, b, c, d so this is important to decide. One way to thing of it is: right-most are image dims, so slider dims accumulate as soon as image dims finish? This would mean we interpret the shorter array above as [c, d, m, n] Another way to think of it is, slider dims accumulate from left to right until they reach the image dims. This would mean we interpret the above array as [a, b, m, n] EDIT: We have chosen to do [c, d, m, n] ok so both are actually easy to do and this is something that ImageWidget can handle before calling get() on each of the individual NDImageViews: # consider an example index from all 4 sliders
indices = (100, 15, 5, 3)
# if we have 4 example array which have the following number of slider dims
n_slider_dims = [4, 3, 2, 1]
right -> left ordering # get the indices we use for each of the 4 arrays with different numbers of dims
for n in n_slider_dims:
print(f"{n} slider_dims, right -> left indices are: {indices[-n:]}")
out: 4 slider_dims, right -> left indices are: (100, 15, 5, 3)
3 slider_dims, right -> left indices are: (15, 5, 3)
2 slider_dims, right -> left indices are: (5, 3)
1 slider_dims, right -> left indices are: (3,)
left -> right ordering for n in n_slider_dims:
print(f"{n} slider_dims, left -> right indices are: {indices[:n]}")
out: 4 slider_dims, left -> right indices are: (100, 15, 5, 3)
3 slider_dims, left -> right indices are: (100, 15, 5)
2 slider_dims, left -> right indices are: (100, 15)
1 slider_dims, left -> right indices are: (100,)
|
Sorry, something went wrong.
|
my all time favorite commit in fastplotlib 😄 : 7770ee0 |
Sorry, something went wrong.
Sorry, something went wrong.
|
ok the basics work! 🥳 Needs tests, more manual testing, some cleanup and tweaks and will be ready to go! I anticipate there will be a few growing pains, lots of parsing since this has to deal with a diverse range of complex datasets and compute possibilities so it may take a while to iron out all the bugs. nd2-2025-11-06_02.40.05.mp4 nd2-2025-11-06_02.49.11.mp4 nd3-2025-11-06_02.50.17.mp4@apasarkar @FlynnOConnell @clewis7 gonna need a review from all of you :D |
Sorry, something went wrong.
|
Allow the spatial func to be a compute shader or a python function. |
Sorry, something went wrong.
|
@kushalkolar I'm confused at where in the code we expect custom lazy arrays to be called on a set of indices. This line is causing me problems: fastplotlib/fastplotlib/graphics/features/_image.py Lines 44 to 47 in 219ea3c .. where data[:] is causing my arrays getitem to load fully into RAM. These are 4D TZXY, so when texture = pygfx.Texture(self.value[data_slice], dim=2) is called, my array's 4D structure errors: File "C:\Users\RBO\repos\mbo_utilities\.venv\Lib\site-packages\pygfx\resources\_texture.py", line 145, in __init__
the_size = size_from_array(view, dim)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\RBO\repos\mbo_utilities\.venv\Lib\site-packages\pygfx\resources\_texture.py", line 591, in size_from_array
raise ValueError(
ValueError: Can't map shape (71, 14, 448, 448) on 2D tex. Maybe also specify size?
In the ImageGraphic.data docstring , it mentions data must implement memoryview(), but the call to memoryview doesn't happen until the Texture() definition here: fastplotlib/fastplotlib/graphics/features/_image.py Lines 72 to 76 in 219ea3c The dim=2 here is throwing me off, so we expect this to always be 2D at this point? EDIT ValueError: Can't map shape (71, 14, 448, 448) on 2D tex. Maybe also specify size? This happened because my array.ndim was returning the wrong value. Everything is working flawlessly so far! |
Sorry, something went wrong.
|
@kushalkolar YESYESYESYES THANK YOU iw-array-test.mp4More to test on Sunday! |
Sorry, something went wrong.
|
if issubclass(processors, NDImageProcessor):
processors = [processors] * len(data)
Gives TypeError: issubclass() arg 1 must be a class when I pass a list of processors. Could do: if isinstance(processors, type) and issubclass(processors, NDImageProcessor):
processors = [processors] * len(data)
Works with a single processor: isinstance(processors[0], type)
Out[1]: True
|
Sorry, something went wrong.
|
More ideas:
|
Sorry, something went wrong.
|
right-click histogram to change the colormap? |
Sorry, something went wrong.
|
I just learned python has builtin sigfig formatting! https://docs.cems.umn.edu/intro/Prt_01_Lssn_04_Using_Strings_and_Print%28%29.html#general-format-notation |
Sorry, something went wrong.
|
@kushalkolar some thoughts re: the multi-session use case. I think I understand what you're suggesting above re: arrays with dynamical shapes. Conceptually this would allow us to define a (num_sessions, <other_dimensions>) array. As you scroll through the "num_sessions" axis, you display data from a different session, and this movie can have a different spatial FOV (and number of frames). I wonder whether it would be more intuitive to allow the "data" parameter in ImageWidget to be multi-dimensional. For e.g.:
group_1 = [session_1_vid1, session_1_vid2, ..._session_1_vidn1]
group_2 = [session_2_vid2, session_2_vid2, ..., session_2_vidn2]
group_M = [...]
data_arr = [group_1, group_2, ..., group_M]
iw = fpl.ImageWidget(data = [data_arr])
iw.show()
Here, imagewidget would recognize the nested list structure and add an extra scrollable dimension to allow the user to move between groups of arrays. This probably also relates to the multi-dimensional-everything vis. |
Sorry, something went wrong.
|
Re: the parameters for the constructor, I wonder whether we can get rid of n_display_dims by adding a "volume" boolean. This boolean is by default False (in the same way that rgb is). By inspecting the values for volume and rgb, the imagewidget constructor can track what the number of display dimensions should be automatically. (2, 3, or 4 in the case where rgb and volume are both true). This might be easier for the user (since they are specifying parameters directly related to what they expect to see in the data). |
Sorry, something went wrong.
|
ok this is ready to merge into the ndwidget branch, but I will do that tomorrow to make sure it goes well 🥳 |
Sorry, something went wrong.
Basically a re-write of ImageWidget.
NDImageProcessor
This is a class that manages one n-dimensional array-like object. An array-like object must have at least 2 dimensions, and can have an unlimited number of more dimensions (but practically I doubt there's use cases for beyond ~5 dimensions, but it will work anyways). It can also process window functions and a finalizer function (previously called "frame_apply").
NDImageProcessor has the following important settable properties:
data: the n-dimensional array it manages
n_display_dims: one of 2 | 3, the number of "spatial dimensions". This is used to determine if an ImageGraphic or an ImageVolumeGraphic should be used.
rgb: whether it is rgb or not
n_slider_dims: auto-computed on demand based on n_display_dims and rgb.
NDImageProcessor.get(indices: tuple[int, ...]) is where all the work is done. It retrieves a 2D or 3D image and applies independent window functions on each dimension where a window_func is defined. Functions and window sizes can be unique across all dims!
Once the window functions are applied, a finalizer_func() is applied on the remaining 2 or 3 spatial dims if necessary.
The NDImageProcessor also handles computing the histogram.
NDImageProcessor can be subclassed to create ones that are specific for different types of arrays, such as dask arrays, and also allow for histogram logic to be unique to each.
ImageWidget
ImageWidget no longer does any compute or processing! It just hands each data array to an NDImageProcessor, and therefore keeps a list of all the processors that correspond to each array: ImageWidget._image_processors.
ImageWidget just handles graphics and sliders. When the slider indices (managed by ImageWidget.indices settable property) change, it just calls NDImageProcessor.get(new_indices) on each processor and the result is displayed, that's it 😄 .
When data arrays, n_display_dims, or rgb changes, dimensions are pushed/popped on ImageWidget.indices. For example, if a dimension is no longer present on any of the current arrays in the ImageWidget then it pops a dimension from indices and a slider will disappear. And vice-versa.
HistogramLUTTool was also re-written. It now just manages the graphics side of things. A precomputed histogram must be given to it.