<jupyter_start><jupyter_text>Spot detection with napari OverviewIn this activity, we will perform spot detection on some in situ sequencing data ([<NAME> et al., Cell, 2019](https://www.cell.com/cell/fulltext/S0092-8674(19)31067-0s)). In doing so, we will combine methods from [scipy](https://www.scipy.org/), [scikit-image](https://scikit-image.org/), and [cellpose](https://github.com/MouseLand/cellpose). The goal is to familiarize you with performing analysis that integrates the scientific python ecosystem and napari. Data sourceThe data were downloaded from the [OpticalPooledScreens github repository](https://github.com/feldman4/OpticalPooledScreens). Next stepsFollowing this activity, we will use the workflow generated in this activity to create a napari spot detection plugin.   Load the dataIn the cells below load the data using the scikit-image `imread()` function. For more information about the `imread()` function, please see the [scikit-image docs](https://scikit-image.org/docs/dev/api/skimage.io.htmlskimage.io.imread). We are loading two images:- `nuclei`: an image of cell nuclei- `spots`: an image of in situ sequencing spots<jupyter_code>from skimage import io

nuclei = io.imread('./data/nuclei_cropped.tif')
spots = io.imread('./data/spots_cropped.tif')<jupyter_output><empty_output><jupyter_text>View the dataWe will use napari to view our data. To do so, we first must create the viewer. Once the Viewer is created, we can add images to the viewer via the Viewer's `add_image()` method.<jupyter_code>import napari

# create the napari viewer
viewer = napari.Viewer();

# add the nuclei image to the viewer
viewer.add_image(nuclei);<jupyter_output><empty_output><jupyter_text>In the cell below, add the spots image to the viewer as was done above for the nuclei image. After loading the data, inspect it in the viewer and adjust the layer settings to your liking (e.g., contrast limits, colormap). You can pan/zoom around the image by click/dragging to pan and scrolling with your mousewheel or trackpad to zoom.<jupyter_code># add the spots image to the viewer<jupyter_output><empty_output><jupyter_text>Create an image filterYou may have noticed the the spots image contains background and autofluorescence from the cells. To improve spot detection, we will apply a high pass filter to improve the contrast of the spots.<jupyter_code>import numpy as np
from scipy import ndimage as ndi

def gaussian_high_pass(image: np.ndarray, sigma: float = 2):
    """Apply a gaussian high pass filter to an image.

    Parameters
    ----------
    image : np.ndarray
        The image to be filtered.
    sigma : float
        The sigma (width) of the gaussian filter to be applied.
        The default value is 2.
    
    Returns
    -------
    high_passed_im : np.ndarray
        The image with the high pass filter applied
    """
    low_pass = ndi.gaussian_filter(image, sigma)
    high_passed_im = image - low_pass
    
    return high_passed_im<jupyter_output><empty_output><jupyter_text>In the cell below, apply the gaussian high pass filter to the `spots` image and add the image to the viewer.<jupyter_code># Use the gaussian_high_pass function to filter the spots image


# add the filtered image to the viewer
# hint: set the opacity < 1 in order to see the layers underneath<jupyter_output><empty_output><jupyter_text>Detect spotsNext, we will create a function to detect the spots in the spot image. This function should take the raw image, apply the gaussian high pass filter from above and then use one of the blob detection algorithms from sci-kit image to perform the blob detection. The `detect_spots()` function should return a numpy array containing the coordinates of each spot and a numpy array containing the diameter of each spot.Some hints:- See the [blob detection tutorial from scikit-image](https://scikit-image.org/docs/dev/auto_examples/features_detection/plot_blob.html). - We recommend the [blob_log detector](https://scikit-image.org/docs/dev/api/skimage.feature.htmlskimage.feature.blob_log), but feel free to experiment!- See the "Note" from the blob_log docs: "The radius of each blob is approximately $\sqrt{2}\sigma$ for a 2-D image"<jupyter_code>import numpy as np
from skimage.feature import blob_log

def detect_spots(
    image: np.ndarray,
    high_pass_sigma: float = 2,
    spot_threshold: float = 0.01,
    blob_sigma: float = 2
):
    """Apply a gaussian high pass filter to an image.

    Parameters
    ----------
    image : np.ndarray
        The image in which to detect the spots.
    high_pass_sigma : float
        The sigma (width) of the gaussian filter to be applied.
        The default value is 2.
    spot_threshold : float
        The threshold to be passed to the blob detector.
        The default value is 0.01.
    blob_sigma: float
        The expected sigma (width) of the spots. This parameter
        is passed to the "max_sigma" parameter of the blob
        detector.
    
    Returns
    -------
    points_coords : np.ndarray
        An NxD array with the coordinate for each detected spot.
        N is the number of spots and D is the number of dimensions.
    sizes : np.ndarray
        An array of size N, where N is the number of detected spots
        with the diameter of each spot.
    
    """
    # filter the image with the gaussian_high_pass filter
    filtered_spots = gaussian_high_pass(image, high_pass_sigma)

    # detect the spots on the filtered image
    blobs_log = blob_log(
        filtered_spots,
        max_sigma=blob_sigma,
        num_sigma=1,
        threshold=spot_threshold
    )
    
    # convert the output of the blob detector to the 
    # desired points_coords and sizes arrays
    # (see the docstring for details)
    points_coords = blobs_log[:, 0:2]
    sizes = 3 * blobs_log[:, 2]

    return points_coords, sizes<jupyter_output><empty_output><jupyter_text>In the cell below, apply `detect_spots()` to our `spots` image. To visualize the results, add the spots to the viewer as a [Points layer](https://napari.org/tutorials/fundamentals/points.html). If you would like to see an example of using a points layer, see [this example](https://github.com/napari/napari/blob/master/examples/add_points.py). To test out your function, vary the detection parameters and see how they affect the results. Note that each time you run the cell, the new results are added as an addition Points layer, allowing you to compare results from different parameters.<jupyter_code># detect the spots


# add the detected spots to the viewer as a Points layer<jupyter_output><empty_output><jupyter_text>Segment nucleiTo segment the nuclei, we will use the [cellpose napari plugin](https://github.com/MouseLand/cellpose-napari). Through this and the following section, we will see how one can integrate plugins developed both others into their analysis workflow. Please perform the segmentation using the instructions below. For more information on cellpose, please see the [paper](https://www.nature.com/articles/s41592-020-01018-x) and [repository](https://github.com/MouseLand/cellpose).1. Start the cellpose plugin. From the menu bar, click Plugins->cellpose-napari: cellpose. You should see the plugin added to the right side of the viewer.2. Select the "nuclei" image layer. 3. Set the model type to "nuclei"4. We need to give cellpose an estimate of the size of the nuclei so it can properly scale the data. We can do so using a napari Shapes layer. With the Shapes layer, we will outline some nuclei and then cellpose will use those annotations to estimate the size of the nuclei.    1. Click the "add Shapes" layer button in the viewer. This will create and select a new layer called "Shapes".        2. Set the mode to "Ellipse" by clicking the button in the layer controls.    3. In the canvas, click and drag to add an ellipse that around a "representative" nucleus. For the purpose of this demo, this is enough, but for other data you may need to give more examples to make a better estimate of the cell diameter. If you need to pan/zoom while adding an ellipse, holding the spacebar will allow you to pan/zoom using your mouse (pan via click/drag, zoom by scrolling).    4. If you would like to edit or move an ellipse, you can switch to "Select shapes" mode in the viewer. Shapes can now be moved by clicking on them and then dragging. They can be resized by selecting them and then dragging the control points.        5. Once you are happy with your annotations, you can click the "compute diameter from shape layer" button and you will see the "diameter" value populated. For this demo, the value is typically around 10 pixels.    5. For this demo, we recommend de-selecting "average 4 nets"(potentially less accurate, but faster segmentation) and otherwise using the default settings. If you would like to learn more about the cellpose settings, please see the [cellpose plugin documentation](https://cellpose-napari.readthedocs.io/en/latest/settings.html).6. Now you are ready to run the segmentation! Click the "run segmentation" button. Segmentation for this demo typically takes ~1 minute.7. When the segmentation is completed, you will see some new layers added to the layer list. Of particular interest is "nuclei_cp_masks_000", which contains our segmentation mask added as a Labels layer.  Extension activity: assign detected spots to cellsWe have provided an optional exercise that demonstrates some more advanced napari features. If you do not have time during the practical session for this exercise, please feel free to perform it at a later time and ask the instructors any questions you may have via the class forum.In this exercise, we will assign the detected spots to the nearest nucleus since as a proxy for assigning detected spots to cells. Step 1: get the locations of the nucleiAs we saw at the end of the cellpose section above, the segmentations are stored in the Labels layer called `nuclei_cp_masks_000`. In the labels image, each each nucleus is given a unique integer label. We can access the label image from viewer as shown below.The Viewer object has a property calls `layers`, which is the `LayersList`. As the name suggests, the `Viewer.layers` contains each layer that is being displayed in the viewer and the layers can be accessed by their name. From the Labels layer object (called `nuclei_labels_layer` below), we can then access label image from the `data` property (stored as `nuclei_labels` below).<jupyter_code># get the nuclei masks from the output of cellpose
nuclei_labels_layer = viewer.layers['nuclei_cp_masks_000']
nuclei_labels = nuclei_labels_layer.data<jupyter_output><empty_output><jupyter_text>Now that we have the label image for the segmented nuclei, we can determine the centroid for each nucleus. First, use regionprops_table to find the centroids of all detected nuclei. Then, create an N x 2 numpy array containing the coordinate of the centroid of each nucleus. You can visualize the coordinates in a Points layer to verify they are correct.For more details on the regionprops_table see the scikit-image [docs](https://scikit-image.org/docs/dev/api/skimage.measure.htmlskimage.measure.regionprops_table) and [examples](https://scikit-image.org/docs/stable/auto_examples/segmentation/plot_regionprops.html).<jupyter_code>from skimage.measure import regionprops_table

# create a regionprops table from the nuclei_labels
# containing the label values and the centroids
# of the nuclei
rp_table = regionprops_table(
    nuclei_labels,
    properties=('label', 'centroid')
)

# create a numpy array containing the coordinates
# of the centroids of the nuclei. Each row should
# correspond to a nucleus and the columns should be
# for the coordinate in the first and second axis
nuclei_centroids = np.column_stack(
    (rp_table['centroid-0'], rp_table['centroid-1'])
)<jupyter_output><empty_output><jupyter_text>In the cell below, add the nuclei centroids to the viewer as a Points layer.  Step 2: Find the nearest nucleus for each spotIn this step, we will use a KDTree to quickly determine which nucleus (`nuclei_centroids`) each spot (`spot_coords`) is closest to. For more information on the scipy cKDTree, please see [the docs](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.cKDTree.html) and [query examples](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.cKDTree.query.html).<jupyter_code>from scipy.spatial import cKDTree

# create the KDTree containing the nuclei locations
kdt = cKDTree(nuclei_centroids)

# query kdtree to get the nearest nucleus for each detected spot
_, nearest_nucleus_ind = kdt.query(spot_coords, k=1)

# convert the index to label value in the segmentation image
nucleus_label_values = rp_table['label']
nearest_nucleus_label = nucleus_label_values[nearest_nucleus_ind]<jupyter_output><empty_output><jupyter_text>Step 3: visualize the resultsIn this step, we will visualize how the detected spots were assigned to nuclei. To do so, we will only view the nuclei with spots and color the points to match the color of their respective nuclei.To do so, we will make use of the properties feature of the napari Points layer. `Points.properties` is a table where each row corresponds with a point (index matched to the coordinates) and each column is a feature of that point. The color of points can be mapped to their properties and we will use this to give all points assigned to the same nucleus the same color. To see an implementation of coloring points with properties, please see [this example](https://github.com/napari/napari/blob/master/examples/add_points_with_properties.py).The properties are stored as a dictionary where each key is the name of the column and each value is a numpy array containing the values for that column. For example, if we had 3 points with properties table as shown in the table below,| snr | confidence || --- | --- || 1 | 0.1 || 3 | 0.5 || 10 | 0.9 |we would create the properties as follows:```pythonproperties = {    'snr': np.array([1, 3, 10]),    'confidence': np.array([0.1, 0.5, 0.9])}```In this case, we will create a properties table with a `nucleus_label` column, which is the label value for the nucleus to which that spot has been assigned. That is the key is `nucleus_label` and the value is `nearest_nucleus_label`, which was defined above.<jupyter_code># create a properties table that maps each detected spot
# to a nucleus label.
spot_properties = {'nucleus_label': nearest_nucleus_label}<jupyter_output><empty_output><jupyter_text>Below, we define the face color parameters that we will use to create the new points layer. We will color the points using a color cycle. A color cycle is a list of colors that is applied in order and will repeat once all colors have been used.We define the face color options using a dictionary where the key is the setting name and the value is the setting value. See below for an explanation of the settings.- `colors`: these are the colors that will be applied to the faces of the points. Here, we are providing the property name (i.e., key in the properties dictionary) that we will map the colors to. This can also be a single color that is applied to all points or an array of colors (one for each point).- `color_mode`: select mode for how the face color is set. Here, we choose `cycle`, so that the face color is set by color cycle.- `categorical_colormap`: we provide the color cycle that will be used to set the face color.<jupyter_code># define the color cycle as a list of RGBA colors
color_cycle = [
    [0.12156863, 0.46666667, 0.70588235, 1.],
    [1.        , 0.49803922, 0.05490196, 1.],
    [0.17254902, 0.62745098, 0.17254902, 1.],
    [0.83921569, 0.15294118, 0.15686275, 1.],
    [0.58039216, 0.40392157, 0.74117647, 1.],
    [0.54901961, 0.3372549 , 0.29411765, 1.],
    [0.89019608, 0.46666667, 0.76078431, 1.],
    [0.49803922, 0.49803922, 0.49803922, 1.],
    [0.7372549 , 0.74117647, 0.13333333, 1.],
    [0.09019608, 0.74509804, 0.81176471, 1.]
]

# define the face color options
face_color = {
    'colors': 'nucleus_label',
    'color_mode': 'cycle',
    'categorical_colormap': color_cycle
}<jupyter_output><empty_output><jupyter_text>Finally, we can add the points to the viewer. As before, we use the `add_points()` method. However, this time, we have additional keyword arguments to pass the properties and face color settings we defined in the cells above.<jupyter_code>pts = viewer.add_points(
    spot_coords,
    properties=spot_properties,
    face_color=face_color,
    symbol='ring',
    size=4,
    name='assigned_spots'
)<jupyter_output><empty_output><jupyter_text>Another thing to note is that `viewer.add_points()` returned the variable we assigned to `pts`. From the line below, we see that `pts` is the Points layer object that was created. This can be useful when further operations need to be performed on a newly created layer. In fact, all of the `viewer.add_*()` methods (e.g., `add_image()`) return the new layer object!<jupyter_code>print(pts is viewer.layers['assigned_spots'])<jupyter_output><empty_output><jupyter_text>In the cell below, we inspect the colormap that is used to set the face colors of the points layer. We see that is it is a dictionary where the keys are the nucleus labels and the values are the corresponding colors. In the following cell, we will use this same mapping to make the labels image of the segmented nuclei use the same colormap.<jupyter_code># get the mapping of nucleus label to color
# from our points layer
spots_cmap = pts._face.categorical_colormap.colormap
spots_cmap<jupyter_output><empty_output><jupyter_text>In the final step, we set the colors of the nuclei from our segmentation (`nuclei_cp_masks_000`) to match the spot colors. To do so, we use the colormapping we got from the points layer in the cell above (`spots_cmap`) and apply it to our nuclei labels layer via the `Labels.color` property. Setting the `color` property with a dictionary mapping label values to colors (as in `spots_cmap`) will apply that color map to the displayed labels.<jupyter_code># apply the same color mapping to our labels layer
nuclei_labels_layer = viewer.layers['nuclei_cp_masks_000']
nuclei_labels_layer.color = spots_cmap<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.07038941150732882, 'fraction_numerical': 0.020947276307153796, 'mean_word_length': 4.315406976744186, 'pattern_counts': {'":': 0, '<': 67, '<?xml version=': 0, '>': 67, 'https://': 19, 'lorem ipsum': 0, 'www.': 3, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 1, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '16', 'dir': 'jupyter-structured-clean-dedup', 'id': '270583', 'license': 'BSD-3-Clause', 'n_tokens_mistral': 5496, 'n_tokens_neox': 5005, 'n_words': 2395, 'path': 'spot_detection_tutorial_beginner.ipynb', 'repo_name': 'colinpalmer/napari-spot-detection-tutorial', 'size': '18295'}