From 6f8220329941bd0be106c9906e88fb9a795b7b77 Mon Sep 17 00:00:00 2001 From: lynnschmittwilken Date: Tue, 14 Mar 2023 17:29:38 -0400 Subject: [PATCH] added circles (lines and shapes) --- stimupy/components/lines.py | 97 +++++++++++++++++++++++++++++------- stimupy/components/shapes.py | 63 ++++++++++++++++++++++- 2 files changed, 140 insertions(+), 20 deletions(-) diff --git a/stimupy/components/lines.py b/stimupy/components/lines.py index 4b534b39..81e2e7f4 100644 --- a/stimupy/components/lines.py +++ b/stimupy/components/lines.py @@ -4,12 +4,13 @@ import numpy as np from PIL import Image, ImageDraw -from stimupy.components.shapes import ring +from stimupy.components.shapes import ellipse as ellipse_shape from stimupy.utils import resolution __all__ = [ "line", "dipole", + "ellipse", "circle", ] @@ -104,9 +105,6 @@ def line( ), ) - if any(num < 0 for num in coords[0]) or any(num < 0 for num in coords[1]): - raise ValueError("Line does not fully fit into image") - # Create line image ImageDraw.Draw(img).line(coords, width=int(line_width * ppd[0])) @@ -230,7 +228,7 @@ def dipole( return stim1 -def circle( +def ellipse( visual_size=None, ppd=None, shape=None, @@ -239,7 +237,7 @@ def circle( intensity_line=1, intensity_background=0, ): - """Draw a circle given the input parameters + """Draw an ellipse given the input parameters Parameters ---------- @@ -249,8 +247,8 @@ def circle( pixels per degree [vertical, horizontal] shape : Sequence[Number, Number], Number, or None (default) shape [height, width] of image, in pixels - radius : Number - radius of circle in degrees visual angle + radius : Sequence[Number, Number], Number or None (default) + ellipse radius [ry, rx] in degrees visual angle line_width : Number width of the line, in degrees visual angle; if line_width=0 (default), line will be one pixel wide @@ -267,25 +265,87 @@ def circle( and additional keys containing stimulus parameters """ if radius is None: - raise ValueError("circle() missing argument 'radius' which is not 'None'") + raise ValueError("ellipse() missing argument 'radius' which is not 'None'") # Resolve resolution shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd) if line_width * ppd[0] == 0: line_width = 1 / ppd[0] - stim = ring( - radii=(radius, radius + line_width), - intensity_ring=intensity_line, + stim = ellipse_shape( + radius=np.array(radius), + intensity_ellipse=intensity_line, visual_size=visual_size, ppd=ppd, shape=shape, intensity_background=intensity_background, origin="mean", ) - stim["ring_mask"] = np.where(stim["ring_mask"] == 2, 1, 0) - stim["line_mask"] = stim["ring_mask"] - del stim["ring_mask"] + + stim2 = ellipse_shape( + radius=np.array(radius) - line_width, + visual_size=visual_size, + ppd=ppd, + shape=shape, + origin="mean", + ) + + stim["img"] = np.where(stim2["shape_mask"]==1, intensity_background, stim["img"]) + stim["line_mask"] = np.where(stim2["shape_mask"]==1, 0, stim["shape_mask"]) + del stim["shape_mask"] + return stim + + +def circle( + visual_size=None, + ppd=None, + shape=None, + radius=None, + line_width=0, + intensity_line=1, + intensity_background=0, +): + """Draw a circle given the input parameters + + Parameters + ---------- + 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 + radius : Number + radius of circle in degrees visual angle + line_width : Number + width of the line, in degrees visual angle; + if line_width=0 (default), line will be one pixel wide + intensity_line : Number + intensity value of the line (default: 1) + intensity_background : Number + intensity value of the background (default: 0) + + Returns + ---------- + dict[str, Any] + dict with the stimulus (key: "img"), + mask with integer index for each line (key: "line_mask"), + and additional keys containing stimulus parameters + """ + if radius is None: + raise ValueError("circle() missing argument 'radius' which is not 'None'") + if not isinstance(radius, (int, float)): + raise ValueError("radius should be a single number") + + stim = ellipse( + visual_size=visual_size, + ppd=ppd, + shape=shape, + radius=radius, + line_width=line_width, + intensity_line=intensity_line, + intensity_background=intensity_background, + ) return stim @@ -298,13 +358,12 @@ def circle( "line_length": 2, "line_width": 0.01, "rotation": 30, - # "line_position": (5, 1), - # "origin": "center" } stims = { - "line": line(**p1), + "line": line(**p1, origin="center", line_position=(-1, -1)), "dipole": dipole(**p1, line_gap=1), "circle": circle(visual_size=10, ppd=10, radius=3), + "ellipse": ellipse(visual_size=10, ppd=10, radius=(3, 4)), } - plot_stimuli(stims, mask=False) + plot_stimuli(stims, mask=True) diff --git a/stimupy/components/shapes.py b/stimupy/components/shapes.py index 71f1c50c..852ee57d 100644 --- a/stimupy/components/shapes.py +++ b/stimupy/components/shapes.py @@ -2,7 +2,7 @@ from stimupy.components import image_base from stimupy.components.angulars import wedge -from stimupy.components.circulars import annulus, disc, ring +from stimupy.components.circulars import annulus, ring, disc from stimupy.utils import resolution __all__ = [ @@ -11,6 +11,7 @@ "cross", "parallelogram", "ellipse", + "circle", "wedge", "annulus", "disc", @@ -535,6 +536,65 @@ def ellipse( } +def circle( + visual_size=None, + ppd=None, + shape=None, + radius=None, + intensity_circle=1.0, + intensity_background=0.0, + origin="mean", + restrict_size=True, +): + """Draw an ellipse + + Parameters + ---------- + visual_size : Sequence[Number, Number], Number, or None (default) + visual size [height, width] of image, in degrees visual angle + 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 + radius : Number or None (default) + circle radius in degrees visual angle + intensity_circle : float, optional + intensity value for circle, by default 1.0 + intensity_background : float, optional + intensity value of background, 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) + restrict_size : Bool + if False, allow circle to reach beyond image size (default: True) + + Returns + ---------- + dict[str, Any] + dict with the stimulus (key: "img"), + mask with integer index for the shape (key: "shape_mask"), + and additional keys containing stimulus parameters + """ + if radius is None: + raise ValueError("circle() missing argument 'radius' which is not 'None'") + if not isinstance(radius, (int, float)): + raise ValueError("radius should be a single number") + + stim = ellipse( + visual_size=visual_size, + ppd=ppd, + shape=shape, + radius=radius, + intensity_ellipse=intensity_circle, + intensity_background=intensity_background, + rotation=0, + origin=origin, + restrict_size=restrict_size, + ) + return stim + + if __name__ == "__main__": from stimupy.utils.plotting import plot_stimuli @@ -551,6 +611,7 @@ def ellipse( "parallelogram": parallelogram(**p, parallelogram_size=(5.2, 3.1, 0.9)), "parallelogram2": parallelogram(shape=(100, 100), ppd=10, parallelogram_size=(10, 9, -1)), "ellipse": ellipse(**p, radius=(4, 3)), + "circle": circle(visual_size=(10, 8), ppd=50, radius=3), } plot_stimuli(stims, mask=False)