Masks#

One key feature of stimupy is stimulus "mask"s. Just like the "img", the stimulus "mask" is a numpy.ndarray which can be found in the stimulus-dict. Each entry of the stimulus "mask" corresponds to a pixel in "img" (i.e., it has the same shape as "img").

import numpy as np
import matplotlib.pyplot as plt

from stimupy.utils import plot_stim, plot_stimuli
from stimupy.components import shapes

Let’s create a simple example to understand masks:

rectangle = shapes.rectangle(visual_size=(6,8), ppd=10,
                        rectangle_size=(4,2), rectangle_position=(1,2),
                        intensity_rectangle=.7)

disc = shapes.disc(visual_size=(6,8), ppd=10,
                        radius=2,
                        intensity_disc=1, intensity_background=.5)

plot_stimuli({"rectangle": rectangle, "disc": disc})
plt.show()
../_images/060f4dc8269a7fce9430dff4d6a95bf16c06db10fdc8001d65310b4e74dd28b0.png

Basic masks#

Importantly, the "mask" contains only integer-values (compared to the floating point pixel-intensities in "img"). Each integer-value in the mask, corresponds to a geometric region of interest, e.g. the shape. For basic shapes like these there are only two such regions: the background (mask value: 0), and the shape itself (mask value: 1). These can be used to subset or mask the regions: all pixels with value 1 belong to the shape.

# Display the masks for our shapes
plt.subplot(1,2,1)
plot_stim(rectangle, mask="rectangle_mask")
plt.title("Rectangle mask")
plt.subplot(1,2,2)
plot_stim(disc, mask="ring_mask")
plt.title("Disc mask")
plt.tight_layout()
plt.show()
../_images/0fbd17c85f0c86e21ec79282d75496cafc8134bf356b44c4bdedc4ce264048cc.png ../_images/c7c48795df35fb03b3ef69f7108defb642c96c76e5d802e1960fb1bec750fad3.png ../_images/ed27a64ee768a3a564707350ff11d016edea5f7756cf9cfd4c809c6709f71450.png

Multi-region masks#

Masks can also contain multiple regions, each with different integer values. Let’s create a bullseye with multiple rings to demonstrate this:

# Define resolution parameters
visual_size = (10,12)
ppd = 10

# Create center (target) disc:
disc = shapes.disc(visual_size=visual_size, ppd=ppd,
                   radius=2,
                   intensity_disc=.5, intensity_background=.5)

# Create first ring, white:
ring_1 = shapes.ring(visual_size=visual_size, ppd=ppd,
                     radii=(2, 3),
                     intensity_ring=1, intensity_background=.5)

# Create second ring, black:
ring_2 = shapes.ring(visual_size=visual_size, ppd=ppd,
                     radii=(3, 4),
                     intensity_ring=0, intensity_background=.5)

We can combine multiple masks into one that indexes different regions:

# Accumulate mask, starting with disc mask
mask = disc["ring_mask"]

# Add first ring mask
mask = np.where(ring_1["ring_mask"], 2, mask)

# Add second ring mask
mask = np.where(ring_2["ring_mask"], 3, mask)

print("Unique mask values:", np.unique(mask))
Unique mask values: [0 1 2 3]

This gives a mask with 4 unique values which each index pixels belonging to different areas:

  • 1 for the central disc

  • 2 for the first ring around that

  • 3 for the outer ring

  • 0 for the background, i.e., everywhere else

plt.imshow(mask)
plt.colorbar()
plt.title("Multi-region mask")
plt.show()
../_images/907f8373f772ffa0861b3ce6ea5455efa073bf8ac6ff12684a765ecb26e85849.png

Using masks to manipulate stimuli#

One advantage of having these kinds of "mask"s that index regions (rather than just binary masks) is that we can use the "mask" to selectively alter one region in an existing stimulus without having to recreate the whole image:

# Create image using the mask
img = np.where(mask==1, 0.5, 0.5)  # Central disc
img = np.where(mask==2, 1, img)    # First ring
img = np.where(mask==3, 0, img)    # Second ring

bullseye = {
    "img": img,
    "mask": mask,
    "visual_size": visual_size,
    "ppd": ppd
}

plt.subplot(1,2,1)
plot_stim(bullseye)
plt.title("Original")

# Change intensity of middle ring to .75; leave rest of image as is:
bullseye["img"] = np.where(bullseye["mask"]==2, .75, bullseye["img"])

plt.subplot(1,2,2)
plot_stim(bullseye)
plt.title("Modified middle ring")
plt.tight_layout()
plt.show()
../_images/dbd2a7e0a3d3a9c51576b3ecc57c7f1b845761514461755f82700ad3a7bce846.png ../_images/9d97239d42571cc811e72f0f19c2247d215f782b61d83100a48516834b7fceec.png ../_images/b9869bb1efaaa6e0df394b7a5c781aa6d6f25fd8c4c6cf538103f5203266b624.png

Visualizing masks#

We can easily visualize masks overlaid as color coding on top of the stimulus:

plt.subplot(1,2,1)
plot_stim(bullseye)
plt.title("Stimulus")
plt.subplot(1,2,2)
plot_stim(bullseye, mask="mask")
plt.title("With mask overlay")
plt.tight_layout()
plt.show()
../_images/b29e3d6a635bc61b19bbbecd954763362e7b7b64d8be5738837e097dc81d5133.png ../_images/8a243b6898717012cff8787c653227cde58442f444030b0f0c10faa8c715ab18.png ../_images/ce97e034c0fd6363be648df14f2a876bcbacaae86e3f4c5b6ef5b3245f51c895.png

Logical operations on masks#

Masks can be combined using logical operations. For example, we can create masks for overlapping regions:

# Create overlapping shapes
rectangle = shapes.rectangle(visual_size=(6,8), ppd=10,
                        rectangle_size=(4,2), rectangle_position=(1,2),
                        intensity_rectangle=.7)

disc = shapes.disc(visual_size=(6,8), ppd=10,
                        radius=2,
                        intensity_disc=1, intensity_background=.5)

# Logical operations on masks
overlap_mask = (rectangle["rectangle_mask"] == 1) & (disc["ring_mask"] == 1)
union_mask = (rectangle["rectangle_mask"] == 1) | (disc["ring_mask"] == 1)
difference_mask = (rectangle["rectangle_mask"] == 1) & (~(disc["ring_mask"] == 1))

# Visualize the different logical operations
fig, axes = plt.subplots(2, 3, figsize=(12, 8))

axes[0,0].imshow(rectangle["rectangle_mask"], cmap="gray")
axes[0,0].set_title("Rectangle mask")
axes[0,1].imshow(disc["ring_mask"], cmap="gray")
axes[0,1].set_title("Disc mask")
axes[0,2].imshow(overlap_mask, cmap="gray")
axes[0,2].set_title("Overlap (AND)")

axes[1,0].imshow(union_mask, cmap="gray")
axes[1,0].set_title("Union (OR)")
axes[1,1].imshow(difference_mask, cmap="gray")
axes[1,1].set_title("Difference (rectangle - disc)")
axes[1,2].axis('off')

plt.tight_layout()
plt.show()
../_images/7222470d2f844aa2dff599c2fb0c899c8d05769ad06fc231b2ff8050053290d8.png

Masks are fundamental to how stimupy works and enable precise control over different regions of stimuli, making it easy to create complex visual patterns and manipulate them after creation.