Skip to content

Commit

Permalink
Merge branch 'refactor' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
JorisVincent committed Apr 7, 2023
2 parents 4f8a889 + 2fb5f42 commit 5992727
Show file tree
Hide file tree
Showing 25 changed files with 349 additions and 235 deletions.
19 changes: 16 additions & 3 deletions docs/reference/demos/stimuli/bullseyes.md
Expand Up @@ -210,6 +210,7 @@ w_int2 = iw.FloatSlider(value=0, min=0, max=1, description="int-ring2")
w_int_back = iw.FloatSlider(value=0.5, min=0, max=1, description="intensity background")
w_ori = iw.Dropdown(value="center", options=['mean', 'corner', 'center'], description="origin")
w_rot = iw.FloatSlider(value=0, min=0, max=360, description="rotation [deg]")
w_clip = iw.ToggleButton(value=False, disabled=False, description="clip")
w_mask = iw.Dropdown(value=None, options=[None, 'target_mask', 'frame_mask'], description="add mask")
Expand All @@ -220,7 +221,7 @@ b_im_size = iw.HBox([w_height, w_width, w_ppd])
b_geometry = iw.HBox([w_freq, w_phase])
b_intensities = iw.HBox([w_int1, w_int2, w_int_back])
b_target = iw.HBox([w_tint])
b_add = iw.HBox([w_ori, w_clip, w_mask])
b_add = iw.HBox([w_ori, w_rot, w_clip, w_mask])
ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_target, b_add])
# Function for showing stim
Expand All @@ -237,6 +238,7 @@ def show_rectangular(
clip=False,
add_mask=False,
intensity_target=None,
rotation=0.0,
):
stim = rectangular(
visual_size=(height, width),
Expand All @@ -248,6 +250,7 @@ def show_rectangular(
origin=origin,
clip=clip,
intensity_target=intensity_target,
rotation=rotation,
)
plot_stim(stim, mask=add_mask)
Expand All @@ -267,6 +270,7 @@ out = iw.interactive_output(
"clip": w_clip,
"add_mask": w_mask,
"intensity_target": w_tint,
"rotation": w_rot,
},
)
Expand Down Expand Up @@ -295,6 +299,7 @@ w_int3 = iw.FloatSlider(value=0.8, min=0, max=1, description="int-ring3")
w_int_back = iw.FloatSlider(value=0., min=0, max=1, description="intensity background")
w_ori = iw.Dropdown(value="center", options=['mean', 'corner', 'center'], description="origin")
w_rot = iw.FloatSlider(value=0, min=0, max=360, description="rotation [deg]")
w_mask = iw.Dropdown(value=None, options=[None, 'target_mask', 'frame_mask'], description="add mask")
w_tint = iw.FloatSlider(value=0.5, min=0, max=1, description="target int")
Expand All @@ -304,7 +309,7 @@ b_im_size = iw.HBox([w_height, w_width, w_ppd])
b_geometry = iw.HBox([w_radius1, w_radius2, w_radius3])
b_intensities = iw.HBox([w_int1, w_int2, w_int3, w_int_back])
b_target = iw.HBox([w_tint])
b_add = iw.HBox([w_ori, w_mask])
b_add = iw.HBox([w_ori, w_rot, w_mask])
ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_target, b_add])
# Function for showing stim
Expand All @@ -322,6 +327,7 @@ def show_rectangular_generalized(
origin=None,
add_mask=False,
intensity_target=None,
rotation=0.0,
):
stim = rectangular_generalized(
visual_size=(height, width),
Expand All @@ -331,6 +337,7 @@ def show_rectangular_generalized(
intensity_background=intensity_background,
origin=origin,
intensity_target=intensity_target,
rotation=rotation,
)
plot_stim(stim, mask=add_mask)
Expand All @@ -351,6 +358,7 @@ out = iw.interactive_output(
"origin": w_ori,
"add_mask": w_mask,
"intensity_target": w_tint,
"rotation": w_rot,
},
)
Expand All @@ -376,6 +384,8 @@ w_int1 = iw.FloatSlider(value=1, min=0, max=1, description="int-ring1")
w_int2 = iw.FloatSlider(value=0, min=0, max=1, description="int-ring2")
w_int_back = iw.FloatSlider(value=0.5, min=0, max=1, description="intensity background")
w_rot = iw.FloatSlider(value=0, min=0, max=360, description="rotation [deg]")
w_mask = iw.Dropdown(value=None, options=[None, 'target_mask', 'frame_mask'], description="add mask")
w_tint = iw.FloatSlider(value=0.5, min=0, max=1, description="target int")
Expand All @@ -385,7 +395,7 @@ b_im_size = iw.HBox([w_height, w_width, w_ppd])
b_geometry = iw.HBox([w_freq, w_phase])
b_intensities = iw.HBox([w_int1, w_int2, w_int_back])
b_target = iw.HBox([w_tint])
b_add = iw.HBox([w_mask])
b_add = iw.HBox([w_rot, w_mask])
ui = iw.VBox([b_im_size, b_geometry, b_intensities, b_target, b_add])
# Function for showing stim
Expand All @@ -400,6 +410,7 @@ def show_rectangular_two_sided(
intensity_background=None,
add_mask=False,
intensity_target=None,
rotation=0.0,
):
stim = rectangular_two_sided(
visual_size=(height, width),
Expand All @@ -409,6 +420,7 @@ def show_rectangular_two_sided(
intensity_frames=(int1, int2),
intensity_background=intensity_background,
intensity_target=intensity_target,
rotation=rotation,
)
plot_stim(stim, mask=add_mask)
Expand All @@ -426,6 +438,7 @@ out = iw.interactive_output(
"intensity_background": w_int_back,
"add_mask": w_mask,
"intensity_target": w_tint,
"rotation": w_rot,
},
)
Expand Down
112 changes: 75 additions & 37 deletions stimupy/components/__init__.py
Expand Up @@ -10,7 +10,8 @@
"plot_overview",
"image_base",
"draw_regions",
"mask_elements",
"mask_regions",
"combine_masks",
"overview",
"angulars",
"radials",
Expand All @@ -35,7 +36,7 @@ def image_base(visual_size=None, shape=None, ppd=None, rotation=0.0, origin="mea
shape : Sequence[Number, Number], Number, or None (default)
shape [height, width] of image, in pixels
rotation : float, optional
rotation (in degrees) counterclockwise from 3 o'clock, by default 0.0
rotation (in degrees) from 3 o'clock, counterclockwise, by default 0.0
origin : "corner", "mean" or "center"
if "corner": set origin to upper left corner
if "mean": set origin to hypothetical image center (default)
Expand All @@ -49,7 +50,7 @@ def image_base(visual_size=None, shape=None, ppd=None, rotation=0.0, origin="mea
"x", "y" : single axes
"horizontal", "vertical" : numpy.ndarray of shape, with distance from origin,
in deg. visual angle, at each pixel
"oblique" : numpy.ndarray of shape, with oblique distance from origin,
"oblique", "oblique_y" : numpy.ndarray of shape, with oblique distances from origin,
in deg. visual angle, at each pixel
"radial" : numpyn.ndarray of shape, with radius from origin,
in deg. visual angle, at each pixel
Expand All @@ -62,26 +63,23 @@ def image_base(visual_size=None, shape=None, ppd=None, rotation=0.0, origin="mea
# Resolve resolution
shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd)

# Set origin
if origin == "corner":
x = np.linspace(0, visual_size.width, shape.width)
y = np.linspace(0, visual_size.height, shape.height)
elif origin == "mean":
vrange = (visual_size.height / 2, visual_size.width / 2)
x = np.linspace(-vrange[1], vrange[1], shape.width)
y = np.linspace(-vrange[0], vrange[0], shape.height)
elif origin == "center":
vrange = (visual_size.height / 2, visual_size.width / 2)
x = np.linspace(-vrange[1], vrange[1], shape.width, endpoint=False)
y = np.linspace(-vrange[0], vrange[0], shape.height, endpoint=False)
else:
raise ValueError("origin can only be be corner, mean or center")
# Get single axes
x, y = resolution.visual_size_to_axes(visual_size=visual_size, shape=shape, origin=origin)

# Linear distance image bases
xx, yy = np.meshgrid(x, y)

# Rotate to get obliques
alpha = [np.cos(np.deg2rad(-rotation)), np.sin(np.deg2rad(-rotation))]
beta = [np.cos(np.deg2rad(rotation)), np.sin(np.deg2rad(rotation))]
oblique_x = alpha[0] * xx + alpha[1] * yy
oblique_y = beta[1] * xx + beta[0] * yy
if origin == "corner":
oblique_x = oblique_x - oblique_x.min()
oblique_y = oblique_y - oblique_y.min()

# Rectilinear distance (frames)
rectilinear = np.maximum(np.abs(xx), np.abs(yy))
rectilinear = np.maximum(np.abs(oblique_x), np.abs(oblique_y))

# Radial distance
radial = np.sqrt(xx**2 + yy**2)
Expand All @@ -91,13 +89,6 @@ def image_base(visual_size=None, shape=None, ppd=None, rotation=0.0, origin="mea
angular -= np.deg2rad(rotation + 90)
angular %= 2 * np.pi

# Oblique
alpha = [np.cos(np.deg2rad(rotation)), np.sin(np.deg2rad(rotation))]
oblique = alpha[0] * xx + alpha[1] * yy

if origin == "corner":
oblique = oblique - oblique.min()

return {
"visual_size": visual_size,
"ppd": ppd,
Expand All @@ -107,14 +98,15 @@ def image_base(visual_size=None, shape=None, ppd=None, rotation=0.0, origin="mea
"y": y,
"horizontal": xx,
"vertical": yy,
"oblique": oblique,
"oblique": oblique_x,
"oblique_y": oblique_y,
"rectilinear": rectilinear,
"radial": radial,
"angular": angular,
}


def mask_elements(
def mask_regions(
distance_metric,
edges,
shape=None,
Expand All @@ -123,26 +115,30 @@ def mask_elements(
rotation=0.0,
origin=None,
):
"""Generate mask with integer indices for consecutive elements
"""Generate mask for regions in image
Regions are defined by `edges` along a `distance_metric`.
Regions will be masked consecutively, from `origin` outwards,
such that each `edge` is the upper-limit of a region.
Parameters
----------
distance_metric : any of keys in stimupy.components.image_base()
which dimension to mask over
which distance metric to mask over
edges : Sequence[Number]
upper-limit of each consecutive elements
upper-limit of each consecutive region
visual_size : Sequence[Number, Number], Number, or None (default)
visual size [height, width] of image, in degrees
ppd : Sequence[Number, Number], Number, or None (default)
pixels per degree [vertical, horizontal]
shape : Sequence[Number, Number], Number, or None (default)
shape [height, width] of image, in pixels
rotation : float, optional
angle of rotation (in degrees) of segments,
counterclockwise away from 3 o'clock, by default 0.0
origin : Sequence[Number, Number], Number, or None (default)
placement of origin [height,width from topleft] to calculate distances from.
If None, set to center of visual_size
rotation (in degrees) from 3 o'clock, counterclockwise, by default 0.0
origin : "corner", "mean" or "center"
if "corner": set origin to upper left corner
if "mean": set origin to hypothetical image center (default)
if "center": set origin to real center (closest existing value to mean)
Returns
-------
Expand Down Expand Up @@ -180,8 +176,49 @@ def mask_elements(
}


def combine_masks(*masks):
"""Combines several masks into a singular mask
Increments mask-indices, such that the resulting mask contains consecutive integer
indices.
Masks are combined in order.
Parameters
----------
mask_1, mask_2, ... : numpy.ndarray
Masks to be combined
Returns
-------
numpy.ndarray
Combined mask, where integer indices are in order of the input masks.
Raises
------
ValueError
if masks do not all have the same shape (in pixels)
ValueError
if multiple masks index the same pixel
"""
# Initialize
combined_mask = np.zeros_like(masks[0])
for mask in masks:
# Check that masks have the same shape
if not mask.shape == combined_mask.shape:
raise ValueError("Not all masks have the same shape")

# Check that masks don't overlap
if (combined_mask & mask).any():
raise ValueError("Masks overlap")

# Combine: increase `mask`-idc by adding the current highest idx in combined_mask
combined_mask = np.where(mask, mask + combined_mask.max(), combined_mask)

return combined_mask


def draw_regions(mask, intensities, intensity_background=0.5):
"""Draw image with intensities for components in mask
"""Draw regions defined by mask, with given intensities
Parameters
----------
Expand Down Expand Up @@ -232,7 +269,8 @@ def overview(skip=False):
"plot_overview",
"draw_regions",
"image_base",
"mask_elements",
"mask_regions",
"combine_masks",
]:
continue

Expand Down
15 changes: 6 additions & 9 deletions stimupy/components/angulars.py
@@ -1,6 +1,6 @@
import numpy as np

from stimupy.components import draw_regions, mask_elements
from stimupy.components import draw_regions, mask_regions
from stimupy.components.radials import ring

__all__ = [
Expand Down Expand Up @@ -42,7 +42,7 @@ def mask_angle(
dict with boolean mask (key: bool_mask) for pixels falling in given angle,
and additional params
"""
stim = mask_elements(
stim = mask_regions(
edges=np.deg2rad(angles),
distance_metric="angular",
rotation=rotation,
Expand Down Expand Up @@ -83,8 +83,7 @@ def wedge(
radius : float
radius of disc, in degrees visual angle
rotation : float, optional
angle of rotation (in degrees) of segment,
counterclockwise from 3 o'clock, by default 0.0
rotation (in degrees) from 3 o'clock, counterclockwise, by default 0.0
inner_radius : float, optional
inner radius (in degrees visual angle), to turn disc into a ring, by default 0
intensity_wedge : float, optional
Expand Down Expand Up @@ -155,8 +154,7 @@ def mask_segments(
edges : Sequence[Number]
upper-limit of each consecutive segment, in angular degrees 0-360
rotation : float, optional
angle of rotation (in degrees) of segments,
counterclockwise away from 3 o'clock, by default 0.0
rotation (in degrees) from 3 o'clock, counterclockwise, by default 0.0
visual_size : Sequence[Number, Number], Number, or None (default)
visual size [height, width] of image, in degrees
ppd : Sequence[Number, Number], Number, or None (default)
Expand All @@ -175,7 +173,7 @@ def mask_segments(
mask with integer index for each segment (key: "wedge_mask"),
and additional keys containing stimulus parameters
"""
stim = mask_elements(
stim = mask_regions(
distance_metric="angular",
edges=np.deg2rad(edges),
rotation=rotation,
Expand Down Expand Up @@ -212,8 +210,7 @@ def segments(
angles : Sequence[Number] or None (default)
upper-limit of each segment, in angular degrees 0-360
rotation : float, optional
angle of rotation (in degrees) of segments,
counterclockwise away from 3 o'clock, by default 0.0angles
rotation (in degrees) from 3 o'clock, counterclockwise, by default 0.0
intensity_background : Number
intensity value for background; default is 0.5.
intensity_segments : Sequence[Number, ...]
Expand Down

0 comments on commit 5992727

Please sign in to comment.