-
Notifications
You must be signed in to change notification settings - Fork 57
separate array logic and graphic logic in ImageWidget
#868
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
base: main
Are you sure you want to change the base?
Conversation
|
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: index: dict that maps dim name to current index, ex: image_dims: Also, for 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 |
|
Make a few ready-made subclasses of In IW also has a new kwarg 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. |
|
new kwarg on |
|
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. |
|
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 |
|
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. |
|
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}") |
|
Thoughts on how sliders should map onto dims: if we have this array, let's say it's then, do we assume the following smaller array is Another way to think about this: We want to allow arbitrary numbers of dims, so if we have the following array with ndim 6:
Do we assume a smaller array with ndim 4 is: or:
Sliders will be made for 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 |
|
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 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
) |
|
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. |
|
|
|
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
|
ok so both are actually easy to do and this is something that # 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: left -> right ordering for n in n_slider_dims:
print(f"{n} slider_dims, left -> right indices are: {indices[:n]}")out: |
|
my all time favorite commit in fastplotlib 😄 : 7770ee0 |
|
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.mp4nd2-2025-11-06_02.49.11.mp4nd3-2025-11-06_02.50.17.mp4@apasarkar @FlynnOConnell @clewis7 gonna need a review from all of you :D |
|
Should make Need to implement ImageWidget properties and setters for window func stuff and finalizer func. Each of these should call |
|
OK can get and set So all this works: iw.data[2] = new_array_of_any_dims
# can also use the subplot name
iw.data["pmd"] = new_pmd_array
# likewise for other image processor properties
iw.window_funcs[1] = (None, np.mean) # mean window function only on dim at index 1
iw.window_sizes[1] = (None, 23)
# similar for dimension indices
iw.indices[1] = 7
# if `slider_dim_names` is set, can also use that:
iw.slider_dim_names = ("t", "z")
iw.indices["t"] = 5otherwise you'd have to do this (you can also do this if necessary) # set all new data arrays
iw.data = [new_a, new_b]
# set only the 2nd one as new
iw.data = [old_a, new_b]
# this is equivalent to
iw.data[1] = new_b
# same for all other window funcs
# likewise for setting dim indices
# if the current indices are already (10, 0)
iw.indices = (10, 3) # keep first dim same, change second dim
# the above is identical to this:
iw.indices[1] = 3 |
|
There is a new requirement that the data input must implement If so, might be helpful to add something like if hasattr(data, "astype"):
else:
("Input data must implement astype")
...to the |
|
Histogram stuff I tested which all works:
Also tested large ranges such as between 0 - 2^16, 0 - 2^32, 0 - 2^64, Can also have a mode where the same histgram lut tool just shows the colorbar with the scalebar and the linear region selector can change the vmin, vmax, probably in a future PR. |
|
I encountered what may be a bug while making the mcorr widget. |
|
We should also try to support arrays with dynamic shapes, i.e. where the shape of the array is a function of one of the indices of a dimension 😆 . This may sound weird but it has a very clear usecase, multi-session data! @apasarkar @FlynnOConnell For example consider a multi session array with the following shape:
Each session may have a different number of timepoints or z planes! I think we can actually implement this very easily in a future @property
def shape(self) -> tuple[int, ...]:
match self._last_indices # last indices tuple that was used on a `get()` call
case 0: # session 0
return ... # shape of session 0
...The issue with this is that we've written Maybe when |

Basically a re-write of
ImageWidget.NDImageProcessorThis 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").
NDImageProcessorhas 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
ImageGraphicor anImageVolumeGraphicshould be used.rgb: whether it is rgb or not
n_slider_dims: auto-computed on demand based on
n_display_dimsandrgb.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
NDImageProcessoralso handles computing the histogram.NDImageProcessorcan 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.ImageWidgetImageWidget 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.indicessettable property) change, it just callsNDImageProcessor.get(new_indices)on each processor and the result is displayed, that's it 😄 .When data arrays,
n_display_dims, orrgbchanges, dimensions are pushed/popped onImageWidget.indices. For example, if a dimension is no longer present on any of the current arrays in theImageWidgetthen it pops a dimension fromindicesand a slider will disappear. And vice-versa.HistogramLUTToolwas also re-written. It now just manages the graphics side of things. A precomputed histogram must be given to it.