Custom sliders to highlight pixel subsets

Hi
first of all thanks a lot for your work on napari, my daily data exploration workflow improved a lot since i integrated napari into it.
Now I would like to customise napari by adding extra sliders to use to highlight different subsets of pixels in the image (the coords of the pixels to highlight are precalculated). Is it possible to create custom sliders like is done with layers?
Thanks a lot!
Simone

2 Likes

Hi Simone, so glad you’re enjoying napari! We don’t have a super easy way to add custom sliders right now - you have to instantiate the Qt objects and add them yourself, but it’s not too hard either. We might try and add more convenience functionality around this stuff in the future and APIs may change a bit here, but for now the following example should work:

# import napari and make an empty viewer object
import napari
import numpy as np
viewer = napari.Viewer()

# Define a callback that will take the value of the slider and the viewer
# and do something, in this case we just set the status
def my_custom_callback(viewer, value):
    viewer.status = str(value)

# Make a horizontal slider from 0 - 100
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QSlider
my_slider = QSlider(Qt.Horizontal)
my_slider.setMinimum(0)
my_slider.setMaximum(100)
my_slider.setSingleStep(1)

# Connect your slider to your callback function
my_slider.valueChanged[int].connect(
    lambda value=my_slider: my_custom_callback(viewer, value)
)

# Add your slider as a new dockwidget
# note that you could have created a much more complex QtWidget here
# with multiple widgets inside of it etc.
viewer.window.add_dock_widget(my_slider, name='my slider', area='left')

# add data / do things you normally do inside napari
viewer.add_image(np.random.random((20, 100, 200)))

Your custom dock widget now appears in the bottom left corner of the screen, but you can pick it up and drag it around to other places, undock it etc.

See this gif:

Finally I’m not quite sure about what you visually want to happen when you say

highlight different subsets of pixels in the image

Can you maybe explain a little more what you have in mind and then maybe we can figure out how to make it work!!

Hope this gets you going though!!

1 Like

Another option is to create an extra “virtual” dimension in your points data. napari will “broadcast” leading dimensions over existing ones. So, supposing you have sets of points in a list points_list to highlight, you could do:


viewer = napari.view_image(my_image)
points_nd = np.concatenate(
    [np.concatenate((np.full((len(pts), 1), i), pts), axis=1)
     for i, pts in enumerate(points_list)],
    axis=0,
)
pts_layer = viewer.add_points(points_nd)

This will give you an additional dimensionality slider to slide over your different point sets.

1 Like

Hi Nick and Juan!
I will give it a try but before I will better describe what we want to do because you may have a better solution for what we want to achieve.

We are using napari to visualise single RNA molecule counting over large areas. We first visualise RNA molecules identified in a large stitched imaged region (and napari is incredibly fast!). If we identify a field of view in which the counting failed or has some issues we then load the failed fov image and manually go through a set of thresholds at which we already precalculated RNA molecules coordinates. For each thresholds we would like to load points corresponding to the counted molecules. Even though this QC step is quite tedious is an important step in our analysis. Now the selection of a specific fov is done by clicking on the visualised large area and the callback start a new napari instance that load the corresponding image. The next step will be to visualise the molecules identified using different thrs. A possible solution will be to have a point layer for each thr but i thought that using a slider and loading the corresponding points would have been less clunky especially when i have a lot of thresholds to go through. Please let me know if you have a better idea on how to implement this.

Thanks a lot for the help!

@simocode the solution I would use right now is the one I mentioned above. There is actually an example of it in the napari announcement blog post: see the second code snippet under “parameter sweeps”:

image = np.load('heatmap.npy')

viewer = napari.view_image(image, name='original', colormap='green',
                           blending='additive')
result = np.array([hmaxima(image, f) for f in np.linspace(0.2, 100.0, num=100)])
points = np.transpose(np.nonzero(result))
viewer.add_points(points, name='maxima coordinates', size=3, opacity=0.5)
result_image = viewer.add_image(result, name='result', colormap='magenta',
                                blending='additive')

The heatmap.npy image can be found linked in this scikit-image issue if you want to try it out yourself!

In the future, we will want to make these things more convenient. Follow this issue for progress!

1 Like

Awesome use case btw! :tada: Definitely the sort of thing we ourselves have found napari useful for: quick human interaction within a bigger automated pipeline.

1 Like

Great! Thanks a lot!
I will try to implement it in the next few days and keep you posted with the result.

1 Like

Hi
I tested @jni suggested approach and it works great, really fast. As additional thing i noticed that the label for the points in the slider accept only int and I cannot visualise the corresponding value of the threshold (ex. 0.000035). It will be a plus to be able to visualise it because it will speed up the selection of the best value. As alternative I could simply visualise a reference table with the int value of the slider and the corresponding thr somewhere in napari GUI. Do you have any suggestion of how to best tackle this? Thanks a lot!

1 Like

We’re working on better physical coordinate systems here https://github.com/napari/napari/issues/763 which will ultimately include the ability to display locations in microns (i.e. floats with units). That machinery should then be pretty easy to leverage to show your threshold values too even though they aren’t “physical coordinates” in the proper sense.

Maybe in the mean time @talley has some ideas on a quick fix

Are you using the broadcasting strategy with the dimensions sliders that @jni mentioned? If so, the quick solution is probably a bit hacky (since those sliders were generally built around indexing and not parameter sweeps). But it’s probably doable… can you give me a small bit of example code that you’re using, and which slider/label you need to accept floats?

Hi
Here is the link is a the code snippet i have been playing with: gist code. Here is the link to the data: zarr_data.

A quick tmp fix will be already great while waiting for the updated mentioned by @sofroniewn.

I understand that this may be a specific user application with little interest for other users. I will be happy to implement any solution you can suggest.

Thanks!

1 Like

Hi all,

I haven’t been involved in #napari development so far, so sorry in advance if this is not much of help. But I thought I’ll mention that we added similar improvements to support floating point sliders (where Java’s Swing framework only supports integer-based JSliders) to #scijava a while ago. The Java code lives in scijava-ui-swing here:


I also found a Python implementation (where the underlying problem of QtWidgets’ QSlider supporting only int values seems to be simliar) DoubleSlider.py here:

1 Like

thanks for the example code, very helpful. Is the main goal here to see the threshold that you are using to select the points? or is it to be able to enter arbitrary thresholds with float precision and view the updated image?

The former is relatively easy to workaround. Using your first approach (“using int as label in the slider”), add the following code right after you create the points layer.

thresh_axis = 0
label = viewer.window.qt_viewer.dims.slider_widgets[thresh_axis].curslice_label
label.setMinimumWidth(40)  

def fix_label(event):
    if event.axis == thresh_axis:
        label.setText(f"{thrs[event.value]:.3f}")

pts_layer.dims.events.axis.connect(fix_label)

This will fix the label to show you the actual threshold value for that particular slice.
It won’t however, accept arbitrary values.

It’s definitely not an ideal solution (as mentioned, those sliders are meant more for dimension indexing than parameter sweeps). So ultimately, I would probably try to implement a new slider like @sofroniewn showed… but hooking that up to the view (to, for instance, show only those points above some threshold) is a bit tricky at the moment. hopefully in the near-ish future we’ll add some infrastructure to make controlling parameters like that easier in the GUI!

1 Like