Plotting

Scallops provides a comprehensive library of plotting functions focusing on the needs of optical pooled screens. All visualization tools reside within the module visualize, which in turn contains different sub-modules, following the structure:

  • visualize: The Module for visualization of OPS data.

    • composite: Composite image visualization sub-module.

    • crosstalk: Cross-talk analysis sub-module.

    • distribution: Data distribution analysis Sub-module.

    • heatmap: Heatmap generation sub-module.

    • histogram: Histogram generation sub-module.

    • imshow: Image display sub-module.

    • napari: Napari interactive viewer sub-module.

    • registration: Sub-module for registration visualization.

    • segmentation: Sub-module for segmentation visualization.

There are also some utilities in the utils sub-module.

Below a showcase of some of the plotting functions available to you.

Load Experiment

Let’s first load an experiment to use in some of the plotting functions. To do that we’ll use SCALLOPS’ read_experiment function.

[1]:
from scallops.datasets import feldman_2019_small
from scallops.io import read_experiment  # Import the reading function

experiment_c_path = feldman_2019_small()

experiment = read_experiment(
    experiment_c_path / "input",
    "{prefix}/{mag}X_c{t}-{experiment}-{t}_{well}_Tile-{tile}.{datatype}.tif",  # Pattern that is followed by the images
    group_by=("well", "tile"),
)
experiment.images.keys()
[1]:
dict_keys(['A1-102', 'A1-103'])

This will load two tiles of well A1 of the experiment C (for more information on experiment C and SCALLOPS IO functions, refer to the data structures notebook and SCALLOPS’ API).

Composites

The first sub-module is the composite sub-module, which Provides functions for generating composite images from multi-dimensional imaging data. A composite image is a single visual created by combining multiple individual images or elements through layering, blending, and manipulation techniques. Is useful to visualize the channels as one image, or plot montages of them.

Image By Channel Montage

One of the first tasks when dealing with image data is to visualize a few tiles and check each channel separately. With SCALLOPS you can do this using the montage_plot function:

[2]:
from scallops.visualize.composite import montage_plot

montage_plot(experiment.images["A1-102"].isel(t=0))
../_images/notebooks_plotting_3_0.png

You can also directly plot the entire experiment with the same function, where each entry would be a row. In our example, experiment there are two tiles:

[3]:
montage_plot(experiment);
../_images/notebooks_plotting_5_0.png

Now you probably notice that the title of the columns (channels) is not representative of the what they capture. Sometimes the metadata does not contain such information. For those cases you can customize the column and row titles:

[4]:
montage_plot(
    experiment,
    col_labels=["c0", "c1", "c2", "c3", "c4"],
    row_labels=["r1", "r2"],
);
../_images/notebooks_plotting_7_0.png

Sometimes you would like a closeup or crop. Let’s look at a 300 x 300 pixels crop at center of the tile

[5]:
montage_plot(
    experiment,
    col_labels=["c0", "c1", "c2", "c3", "c4"],
    row_labels=["r1", "r2"],
    crop=300,
)
../_images/notebooks_plotting_9_0.png

Or you can crop any dimension (equal or less than the original image) quadrilateral, for example a rectangle of shape 20, 40, 200, 300:

[6]:
montage_plot(experiment.images["A1-102"].isel(t=0), crop=(20, 40, 200, 300))
../_images/notebooks_plotting_11_0.png

One of the things that probably already crossed your mind is the contrast of each channel. Yes, with montage_plot you can specify the channel threshold by providing a dictionary mapping the channel and its range:

[7]:
montage_plot(
    experiment.images["A1-102"].isel(t=0),
    thresholds={"Channel:0:0": [288, 3000], "Channel:0:3": [100, 2000]},
)
../_images/notebooks_plotting_13_0.png

Here we modified channel 0 (to the range 288-3000) and channel 3 (to the range 100-2000), all other channels remain with the same contrast.

Composite of Multiple Channels

Now a true composite will blend the channels into one multichannel image. With SCALLOPS this can be achieved with the imcomposite function:

[8]:
from scallops.visualize.composite import imcomposite

imcomposite(experiment.images["A1-102"].isel(t=0, z=0), vmax=8000);
../_images/notebooks_plotting_15_0.png

Is specially useful when checking channel alignment or markers co-localization. Similar to the montage_plot, you can also plot the entire experiment object, but in this case you have to use a different function experiment_composite:

[9]:
from scallops.visualize.composite import experiment_composite

experiment_composite(experiment, vmax=7000);
../_images/notebooks_plotting_17_0.png

Both composite functions allow you to visualize labels as well.

[ ]:

[10]:
from scallops.registration.crosscorrelation import align_image  # Align channels
from scallops.segmentation.stardist import segment_nuclei_stardist
from scallops.segmentation.watershed import segment_cells_watershed

iss_image = align_image(
    experiment.images["A1-102"].isel(z=0),
    align_within_time_channels=[
        1,
        2,
        3,
        4,
    ],  # Ensure we align all channels within a cycle
    align_between_time_channel=0,  # Ensure we align all cycles
    filter_percentiles=[0, 90],  # reduces potential noise during aligning (is optional)
)
nuclei = segment_nuclei_stardist(image=iss_image)
cells, _ = segment_cells_watershed(
    iss_image, nuclei, threshold=650
)  # segment the cells
imcomposite(
    iss_image.isel(t=0),
    labels=nuclei,
    labels_contour=cells,
    labels_alpha=0.7,
    figsize=(10, 10),
    dim="c",
);
bioimageio_utils.py (2): pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
tf2onnx_lib.py (8): In the future `np.object` will be defined as the corresponding NumPy scalar.
../_images/notebooks_plotting_20_1.png

Crosstalk

When dealing with ISS data, often we want to determine the extent of cross-talk, and how it compares when then cross-talk have been corrected. This usually happens when we have estimated the barcode spots, and have call the bases. For this example lets use a Laplacian of Gaussian transform using SCALLOPS’ transform_log, followed by a standard deviation filter using SCALLOPS’s std and a max filter. To find the spots/peaks, we’ll use find_peaks. Once the filters have been applied and the spots have been identified, we can call peaks_to_bases to estimate the bases array, and then assign the corresponding bases using xarray’s assign coordinates:

[11]:
from scallops.io import read_barcodes
from scallops.reads import (
    apply_channel_crosstalk_matrix,
    channel_crosstalk_matrix,
    decode_max,
    peaks_to_bases,
    read_statistics,
)
from scallops.spots import (
    find_peaks,
    max_filter,
    peak_thresholds_from_bases,
    peak_thresholds_from_reads,
    std,
    transform_log,
)

barcodes_path = experiment_c_path / "barcodes.csv"


df_barcode = read_barcodes(
    barcodes_path, iss_image.t.values - 1
)  # Read the barcodes using the actual t indexes (missing cycle 6)

loged = transform_log(iss_image.isel(c=[1, 2, 3, 4]))
std_arr = std(loged)
maxed = max_filter(loged)
peaks = find_peaks(std_arr)
bases_array = peaks_to_bases(maxed=maxed, peaks=peaks, labels=cells)


threshold_peaks_crosstalk_correction_df = peak_thresholds_from_bases(
    bases_array=bases_array
)
threshold_peaks_crosstalk_correction = threshold_peaks_crosstalk_correction_df.iloc[0][
    "threshold"
]

w = channel_crosstalk_matrix(
    bases_array.where(
        bases_array.peak > threshold_peaks_crosstalk_correction, drop=True
    )
)
corrected_bases_array = apply_channel_crosstalk_matrix(bases_array, w)


df_reads = decode_max(corrected_bases_array, barcodes=df_barcode)
peak_thresholds_lower_df = peak_thresholds_from_reads(df_reads.query("barcode_match"))
threshold_peaks = peak_thresholds_lower_df.iloc[0]["threshold"]
df_reads = df_reads.query(f"peak>{threshold_peaks}")

Now we can pass the bases array to the pairwise_channel_scatter_plot to visualize the cross-talk after correction:

[12]:
from scallops.visualize.crosstalk import pairwise_channel_scatter_plot

pairwise_channel_scatter_plot(corrected_bases_array);
../_images/notebooks_plotting_24_0.png

Distribution

Once you have some features data extracted, you probably want to explore some of the distributional properties of said features. Let’s create some test data for demonstration:

[13]:
import numpy as np
import pandas as pd

treatments = ["test-1", "test-2"]
features_df = pd.DataFrame(data=dict(cell_mean_1=np.random.random(100)))
features_df["treatment"] = np.random.choice(treatments, size=len(features_df))

You want to compare their distributions with a ridge_plot, let’s say for mean intensity in channel 1 in cells:

[14]:
from scallops.visualize.distribution import ridge_plot

ridge_plot(
    features_df, scale="standard", feature="cell_mean_1", row="treatment", height=1
);
../_images/notebooks_plotting_28_0.png

This plot allows you to visualize the percentage of reads that are exact matches given the read quality and the percentage of reads above said quality.

Volcano Plots

Once you have done your statistical testing you often end up with a summary statistics table that must contain (at least) a p-value and en effect size, like so:

[15]:
from scallops.datasets import example_feature_summary_stats

summary_stats = pd.read_parquet(example_feature_summary_stats())
summary_stats.head()
[15]:
Gene feature Effect_size p-value FDR -log2FDR
0 AMBRA1 nuclei_median_1_IF -0.001392 8.505015e-01 8.595686e-01 0.151325
1 AMD1 nuclei_median_1_IF 0.001324 4.759486e-01 5.108687e-01 0.671643
2 ARID1A nuclei_median_1_IF -0.001909 6.049140e-01 6.325015e-01 0.458073
3 BARD1 nuclei_median_1_IF 0.045101 1.764859e-10 3.701519e-09 19.414523
4 BRCA1 nuclei_median_1_IF 0.021894 3.693842e-07 2.156258e-06 13.047136

Now you can visualize it with volcano_plot:

[16]:
from scallops.visualize.distribution import volcano_plot

volcano_plot(
    summary_stats.groupby("Gene").first().reset_index(),
    effect_size_col="Effect_size",
    ycol="-log2FDR",
    fdr_col="FDR",
    vbar_std=2,  # Adjust the standard deviation multiplier for vertical bars
    hbar_value=-np.log2(0.05),  # Adjust the horizontal line value for significance
    legend=("Down-regulated", "Up-regulated"),
    highlight="all",
    highlight_col="Gene",
    star=["ESR1", "BARD1", "BRCA1"],
);
../_images/notebooks_plotting_32_0.png

Comparative Effect Scatter

Sometimes you have multiple treatments in your data, and you which to compare the effect sizes of the hits. SCALLOPS have the comparative_effect_scatter to do such task. For this example let’s create and synthetically modify the summary statistics above:

[17]:
summary_stats2 = summary_stats.copy()
std_effect_size = summary_stats2.Effect_size.std()
std_log2FDR = summary_stats2["-log2FDR"].std()
summary_stats2["Effect_size"] += np.random.uniform(
    -std_effect_size, std_effect_size, summary_stats2.shape[0]
)
summary_stats2["-log2FDR"] += np.random.uniform(
    -std_log2FDR, std_log2FDR, summary_stats2.shape[0]
)
summary_stats2.head()
[17]:
Gene feature Effect_size p-value FDR -log2FDR
0 AMBRA1 nuclei_median_1_IF -0.013502 8.505015e-01 8.595686e-01 -1.922170
1 AMD1 nuclei_median_1_IF 0.000196 4.759486e-01 5.108687e-01 -0.171886
2 ARID1A nuclei_median_1_IF -0.012429 6.049140e-01 6.325015e-01 0.646851
3 BARD1 nuclei_median_1_IF 0.048346 1.764859e-10 3.701519e-09 18.869191
4 BRCA1 nuclei_median_1_IF 0.021984 3.693842e-07 2.156258e-06 17.488955

Now we can use both summary statistics dataframes to plot the comparative effect:

[18]:
from scallops.visualize.distribution import comparative_effect_scatter

comparative_effect_scatter(
    df_x=summary_stats,
    x_label="Treatment1",
    df_y=summary_stats2,
    y_label="Treatment2",
    effect_size_column="Effect_size",
    colors=["green", "red", "blue"],
    grouping="Gene",
);
../_images/notebooks_plotting_36_0.png

Comparative Cumulative distribution

You might also want to see how the reference distribution (often the NTCs distribution) compares with some other distributions. SCALLOPS have a cumulative distribution function plotting function, cdf_plot. To use this function you need to pass a reference group and a group of targets (both must exist in your data frame). The data must contain the original data from the features extracted, and the corresponding grouping (oftentimes genes):

[19]:
from scallops.visualize.distribution import cdf_plot

# Synthetic data for brevity
data = {
    "gene_symbol": np.random.choice(["Gene 1", "Gene 2", "Gene 3", "Ref"], size=1000),
    "cells_mean_1": np.random.normal(loc=0, scale=1, size=1000),
}
df = pd.DataFrame(data)

cdf_plot(
    df,
    groupby_column="gene_symbol",
    feature="cells_mean_1",
    reference_group="Ref",
    targets=None,
);
../_images/notebooks_plotting_38_0.png

Heatmaps

Another important visualizatiob is heatmap of different features and caracteristics of the data. SCALLOPS sub-module heatmap contains a series of functions to do just that.

Plate Heat Maps

Often we want to be able to visualize the distribution of the features in the wells of a plate. So lets built some synthetic data on squared wells:

[20]:
wells = ["A1", "A2", "B1", "B2"]
well_tile_df = None
for well in wells:
    df = pd.DataFrame(data=dict(value=np.random.normal(0, 3, size=(7, 7)).flatten()))
    df["well"] = well
    df["tile"] = np.arange(len(df))
    well_tile_df = pd.concat((well_tile_df, df)) if well_tile_df is not None else df
well_tile_df = well_tile_df.set_index(["well", "tile"])
well_tile_df.head()
[20]:
value
well tile
A1 0 1.240926
1 1.053209
2 1.308024
3 -0.328345
4 3.973495

now we can use the plate_heatmap to plot the column value spatially based on the tiles:

[21]:
from scallops.visualize.heatmap import plate_heatmap

plate_heatmap(df=well_tile_df, column="value");
../_images/notebooks_plotting_42_0.png

Heatmaps in well shape

Sometimes you capture the entirety of the well, and you would like to show the shape of the well. Using plate_heatmap you can do this by passing the actual row structure of your well:

[22]:
wells = ["A1", "A2", "A3", "B1", "B2", "B3"]
well_tile_df = None
for well in wells:
    df = pd.DataFrame(data=dict(value=np.random.normal(0, 3, size=(25, 25)).flatten()))
    df["well"] = well
    df["tile"] = np.arange(len(df))
    well_tile_df = pd.concat((well_tile_df, df)) if well_tile_df is not None else df
well_tile_df = well_tile_df.set_index(["well", "tile"])
row_structure = [9, 13, 17, 19, 19, 21, 21, *[23] * 9, 21, 21, 19, 19, 17, 13, 9]
plate_heatmap(
    well_tile_df,
    column="value",
    tile_shape=(25, 25),
    cmap="inferno",
    colorbar_location="right",
    full_well=row_structure,
    overall_ann={"Median": np.nanmedian},
);
../_images/notebooks_plotting_44_0.png

However, sometimes you do not know the row structure and you do not even have tile information (or you want to have a finer grained definition of the aggregation). For these cases SCALLOPS have the plot_well_aggregated_heatmaps function. To apply this function your dataframe must contain the x and y coordinates:

[23]:
from scallops.visualize.heatmap import plot_well_aggregated_heatmaps

# Create a fake 6 well plate

df = pd.DataFrame(
    data=dict(
        well=np.concatenate([np.repeat(well, 2000) for well in range(1, 7)]),
        value=np.random.uniform(0, 1, 6 * 2000),
        y=np.random.uniform(15, 9990, 6 * 2000),
        x=np.random.uniform(15, 9990, 6 * 2000),
    )
)
plot_well_aggregated_heatmaps(
    df=df,
    agg_function=np.mean,
    feature="value",
    grid_size=20,
    plate_shape=(2, 3),
    tile_size_x=500,
    tile_size_y=500,
    xlabel={"Median value:": np.nanmedian},
    cbar_label="Mean Value",
    x_col_name="x",
    y_col_name="y",
);
../_images/notebooks_plotting_46_0.png

In real data, the grid size should match the grid size of the capture program.

Heatmap representing basecall mismatches

In ISS an important interrogation is the basecall mismatches. This allows you to determine the biases in the basecall, and make experimental adjustments accordingly. SCALLOPS provide the base_call_mismatches_heatmap function to visualize his bias in a heatmap format:

[24]:
from scallops.visualize.heatmap import base_call_mismatches_heatmap

data = {
    "whitelist_base": ["A", "A", "A", "C", "C"],
    "read_position": [1, 2, 1, 3, 2],
    "called_base": ["T", "A", "G", "C", "A"],
    "count": [5, 8, 2, 3, 7],
}
base_call_mismatches_df = pd.DataFrame(data)

# Generate the base call mismatches heatmap
base_call_mismatches_heatmap(base_call_mismatches_df);
../_images/notebooks_plotting_49_0.png

It returns a Seaborn ClusterGrid instance representing the base call mismatches heatmap.

Heatmap of the mismatch correction

When using scallops pooled-sbs reads command with the option --mismatches it gives you (among other things) the original and corrected barcodes in the reads folder. You can explore the rates or counts of errors and mismatches by using the plot_barcode_errors function:

[25]:
import numpy as np
import pandas as pd

from scallops.visualize.heatmap import plot_barcode_errors

# Create synthetic data
np.random.seed(42)
nucleotides = ["A", "C", "G", "T"]

# Generate 100 barcodes of length 6
original = [
    "".join(np.random.choice(nucleotides) for _ in range(6)) for _ in range(100)
]

# Create uncorrected versions with some errors
uncorrected = []
mismatches = []

for barcode in original:
    # Introduce random errors
    result = list(barcode)
    errors = 0

    for i in range(len(result)):
        if np.random.random() < 0.1:  # 10% error rate
            options = [n for n in nucleotides if n != result[i]]
            result[i] = np.random.choice(options)
            errors += 1

    uncorrected.append("".join(result))
    mismatches.append(errors)

# Create DataFrame with the minimal columns needed
df = pd.DataFrame(
    {
        "barcode": original,
        "closest_match": original,
        "barcode_uncorrected": uncorrected,
        "mismatches": mismatches,
    }
)

# Analyze and plot
error_counts, fig = plot_barcode_errors(df, title="Example Error Analysis")
Processing errors: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 49/49 [00:00<00:00, 207806.77it/s]
Total errors detected: 61

Errors by position:
Position 1: 10 (16.4%)
Position 2: 10 (16.4%)
Position 3: 10 (16.4%)
Position 4: 10 (16.4%)
Position 5: 9 (14.8%)
Position 6: 12 (19.7%)

Most common error types:
T→G: 10 (16.4%)
T→A: 9 (14.8%)
A→C: 6 (9.8%)
G→C: 6 (9.8%)
C→A: 5 (8.2%)
../_images/notebooks_plotting_51_2.png

In-situ identity matrix

Once you have called all reads, you might have some false positives. One quick stop to see the quality of your basecall is to visualize identity matrix depicting cellular read distribution categorized by read identity. The matrix provides insights into the relationship between different read identity categories, particularly focusing on the total reads and top barcode reads. To do this you can pass the reads dataframe (a dataframe with at least barcode, barcode_count and barcode_match). For traditional OPS (based in spot counts rather than intensity like perturbview), you can use the in_situ_identity_matrix_plot

[26]:
from scallops.visualize.heatmap import in_situ_identity_matrix_plot

data = {
    "barcode_match": np.random.choice([True, False], size=50),
    "barcode": [f"BC{i}" for i in range(1, 51)],
    "barcode_count": np.random.randint(1, 7, size=50),
}
data["barcode_count"] = [np.random.randint(count, 7) for count in data["barcode_count"]]
cells_df = pd.DataFrame(data)
in_situ_identity_matrix_plot(cells_df, xlabel="Total Reads", ylabel="Top Barcode Reads");
../_images/notebooks_plotting_53_0.png

Histograms

SCALLOPS also have a few histogram plotting functions to make your exploration easier.

Percentage of reads with barcodes

You would like to more accurately quantify the percentage of cells (or any other label), and visually compare across a grouping value (e.g. well). For that SCALLOPS have the in_situ_barcode_hist_plot function

[27]:
from scallops.visualize.histogram import in_situ_barcode_hist_plot

data = {
    "well": np.random.choice(["A", "B", "C", "D"], size=100),
    "label": np.random.choice(range(1, 10), size=100),
    "barcode_match": np.random.choice([True, False], size=100),
}
reads_df = pd.DataFrame(data)
in_situ_barcode_hist_plot(reads_df, counts=[0, 1, 2, 3], hue="well", normalize=True);
../_images/notebooks_plotting_55_0.png

Channel histogram

You might want to visualize how the intensity distribution compares across channels. You can achieve this by using SCALLOPS’ channel_hist_plot] function:

[28]:
from scallops.visualize.histogram import channel_hist_plot

# Plot histogram
channel_hist_plot(
    iss_image,
    binrange=(0, 3000),
    bins=100,
    height=8,
    aspect=2,  # Defaults to None, the data extremes,
);
../_images/notebooks_plotting_57_0.png

This way you can compare the distributional properties of the different channels with respect to one another.

Imshow

This sub-module provides functions for displaying images and overlays.

imshow_plane

The simplest of the imshow module functions is imshow_plane, which provides a way to visualize a 2D image array from an image stack:

[29]:
import matplotlib.pyplot as plt

from scallops.visualize.imshow import imshow_plane

# Plot the image using imshow_plane
fig, ax = plt.subplots()
imshow_plane(iss_image.isel(c=0, t=0), ax=ax, title="Example image", cmap="viridis");
../_images/notebooks_plotting_59_0.png

Note that imshow is a lot faster than the composite ones, but can only plot 2D images.

plot_plate

When you want to explore the entirety of the plate’s row images and you have the plate loaded in an experiment you can use the plot_plate function:

[30]:
import matplotlib.pyplot as plt
import numpy as np
import xarray as xr

from scallops.experiment.elements import Experiment
from scallops.visualize.imshow import plot_plate


def create_circular_well(shape, radius, center):
    """Create a circular well in an image."""
    y, x = np.ogrid[: shape[0], : shape[1]]
    distance = (x - center[1]) ** 2 + (y - center[0]) ** 2
    mask = distance <= radius**2
    arr = np.random.rand(*shape)
    return np.where(mask, arr, 0)


def generate_experiment(num_wells=6, well_shape=(65, 65), well_radius=30):
    """Generate a synthetic experiment with circular wells."""
    images = {}
    for i in range(num_wells):
        center = (well_shape[0] // 2, well_shape[1] // 2)
        well = create_circular_well(well_shape, well_radius, center)
        # Use a format like "A01-01" for well identifiers
        row = chr(65 + i // 3)  # A, B
        col = (i % 3) + 1  # 1, 2, 3
        # Add dummy dimensions for compatibility
        well = xr.DataArray(well, dims=["y", "x"]).expand_dims(c=[0], t=[0], z=[0])
        images[f"{row}{col}-01"] = well

    return Experiment(images=images)


# Generate the experiment
exp = generate_experiment()

# Plot the 6-well plate
fig = plot_plate(exp, plate_shape=(2, 3), well_shape=(1, 1), vmax=1, vmin=0)
../_images/notebooks_plotting_61_0.png

This way you wan visualize an entire experiment at once.

tiles_over_stitch

Since SCALLOPS deals with stitched tiles, you sometimes want to overlay the stitching to the well. For this purpose you can use tiles_over_stitch function:

[31]:
from collections import defaultdict
from itertools import product

from scallops.visualize.imshow import tiles_over_stitch


def create_stitched_coords(image: xr.DataArray, grid_shape=(3, 3), overlap=0.2):
    """Create a DataFrame with stitched coordinates for a grid of tiles with overlap."""
    img_height, img_width = image.sizes["y"], image.sizes["x"]
    tile_height, tile_width = (
        int(img_height / grid_shape[0] * (1 + overlap)),
        int(img_width / grid_shape[1] * (1 + overlap)),
    )
    step_y, step_x = int(tile_height * (1 - overlap)), int(tile_width * (1 - overlap))
    data = defaultdict(list)
    for row, col in product(range(grid_shape[0]), range(grid_shape[1])):
        tile_id = f"Tile-{row * grid_shape[1] + col + 1}"
        x = col * step_x
        y = row * step_y
        data["Tile"].append(tile_id)
        data["X"].append(x)
        data["Y"].append(y)
        data["XSize"].append(tile_width)
        data["YSize"].append(tile_height)

    return pd.DataFrame(data)


well = exp.images["A1-01"]
stitched_coords = create_stitched_coords(well)
fig, ax = tiles_over_stitch(well, stitched_coords)
../_images/notebooks_plotting_63_0.png

Now you can see where the overlap is (in this case 20%), where the tiles lay, etc.

plot_percentile_montage

Often you want to see some cells on a specific phenotype value percentile in a montage of gene-specific images. For this SCALLOPS provides the function plot_percentile_montage, which creates a montage of images for specified genes, displaying representative cells at given percentiles of a specified feature:

[32]:
from skimage.measure import regionprops_table

from scallops.visualize.imshow import plot_percentile_montage

image = iss_image.isel(t=0).squeeze()
label = xr.DataArray(nuclei, dims=["y", "x"])
props = pd.DataFrame(
    regionprops_table(
        label.data,
        image.isel(c=0).data,
        properties=["label", "centroid", "intensity_mean"],
    )
)
props["Gene"] = "GeneA"
props = props.rename(
    columns={
        "centroid-1": "nuclei_centroid-1",
        "centroid-0": "nuclei_centroid-0",
        "intensity_mean": "intensity_mean_0",
    }
)
genes_to_plot = ["GeneA"]

fig, selected = plot_percentile_montage(
    props,
    genes_to_plot,
    image,
    group_by=["Gene"],
    gene_name_col="Gene",
    feature="intensity_mean_0",
    guide_lines=True,
    context=True,
)
../_images/notebooks_plotting_65_0.png

Napari

For interactivity, SCALLOPS have implemented the napari viewer with some very useful features

View Image In Napari

First, lets create the viewer using imnapari. Since this is interactive, it will open a second window for you. You call the viewer like so:

[33]:
from scallops.visualize.napari import imnapari

viewer = imnapari(
    experiment.images["A1-102"],
    labels={"stardist-nuclei": nuclei},
)

The viewer should open in a second window, looking something like this:

Image

View Experiment In Napari

Now, if you have an experiment, you might not want to open multiple windows. SCALLOPS Experiment can be explored using the experiment_napari function:

[34]:
from scallops.visualize.napari import experiment_napari

viewer = experiment_napari(experiment)

It should open a window that look like this:

Experiment

were you can toggle between tiles (or any groups) you have in your experiment.

Explore base calls

One of the cornerstones of OPS is a good base calling. Verifying the calls is not easy, and therefore SCALLOPS added a function to make the visual inspection a bit easier. It takes the viewer (for a single image) and adds bases to it, so you can explore it. The function is called add_bases:

[35]:
from scallops.visualize.napari import add_bases, imnapari

viewer = imnapari(
    iss_image,
)
points = add_bases(viewer, df_reads)

A zoomed area should look like this:

Base

Where each base call would be overlayed to the ISS data, and the cycles can be changed accordingly to do the basecall by eye.

Visualize registration

After alignment, you might want to make sure the registration was successful. To that effect, SCALLOPS provides a set of functions to help you visualize the registration.

diagnose_registration

This function overlays 2-d images from an alignment of images. Given colormap restriction, the diagnose_registration can handle a maximum of six images, showing each image in a different color, helping visualize miss-alignments:

[36]:
from scallops.visualize.registration import diagnose_registration

f, axs = plt.subplots(ncols=2, figsize=(10, 6))
# Plot the registration diagnosis
selectors = [
    {"t": 1, "c": "Channel:0:0"},  # DAPI in first cycle
    {"t": 1, "c": "Channel:0:1"},  # G in first cycle
    {"t": 1, "c": "Channel:0:2"},  # T in first cycle
    {"t": 1, "c": "Channel:0:3"},  # A in first cycle
    {"t": 1, "c": "Channel:0:4"},  # C in first cycle
    {"t": 2, "c": "Channel:0:0"},  # DAPI in second cycle
]
axes = diagnose_registration(
    experiment.images["A1-102"].squeeze(),
    *selectors,
    title="Before alignment",
    ax=axs[0],
)
axes = diagnose_registration(
    iss_image,
    *selectors,
    title="After alignment",
    ax=axs[1],
)
../_images/notebooks_plotting_75_0.png

plot_registration

Another way to check stacks is to plot the side by side, and to plot more registrations at once. For this, you can use plot_registration:

[37]:
from scallops.visualize.registration import plot_registration

stacks = [
    experiment.images["A1-102"].squeeze(),
    iss_image,
]

# Plot the registration results
axes = plot_registration(
    stacks,
    dpi=150,
    titles=["Before 102", "After 102"],
    plot_cols=2,
    figsize=(20, 20),
    layout="constrained",
)
../_images/notebooks_plotting_77_0.png

Segmentation

Helping the visualization of the segmentation results, SCALLOPS provides the function plot_segmentation:

[38]:
from scallops.visualize.segmentation import plot_segmentation

# Plot segmentation results
plot_segmentation(
    data=[nuclei, cells],
    dpi=150,
    titles=["Nuclei", "Cells"],
    fontsize=12,
)
dtype.py (529): Downcasting int32 to uint16 without scaling because max value 2968 fits in uint16
../_images/notebooks_plotting_79_1.png

This is an easy and quick check of the segmentation. Note that the composite’s and napari’s functions also have label plotting functionalities that expand that of this module.

[ ]: