Skip to content

Commit

Permalink
closes #89; general component Mondrian, updated related code
Browse files Browse the repository at this point in the history
  • Loading branch information
LynnSchmittwilken committed Jan 9, 2023
1 parent 9b4c507 commit 17058e9
Show file tree
Hide file tree
Showing 3 changed files with 244 additions and 88 deletions.
143 changes: 143 additions & 0 deletions stimuli/components/mondrians.py
@@ -0,0 +1,143 @@
import numpy as np

from stimuli.components.shapes import parallelogram
from stimuli.utils import resolution, degrees_to_pixels

__all__ = [
"mondrians",
]


def mondrians(
visual_size=None,
ppd=None,
shape=None,
mondrian_positions=None,
mondrian_sizes=None,
mondrian_intensities=None,
intensity_background=0.5,
):

# Resolve resolution
shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd)
if len(np.unique(ppd)) > 1:
raise ValueError("ppd should be equal in x and y direction")

img = np.ones(shape) * intensity_background
mask = np.zeros(shape)

n_mondrians = len(mondrian_positions)

if isinstance(mondrian_intensities, (float, int)):
mondrian_intensities = (mondrian_intensities,) * n_mondrians

if isinstance(mondrian_sizes, (float, int)):
mondrian_sizes = ((mondrian_sizes, mondrian_sizes), ) * n_mondrians

if any(len(lst) != n_mondrians for lst in [mondrian_positions, mondrian_sizes, mondrian_intensities]):
raise Exception("There need to be as many mondrian_positions as there are "
"mondrian_sizes and mondrian_intensities.")

mondrian_positions_px = []
mondrian_shapes = []

for m in range(n_mondrians):
try:
if len(mondrian_positions[m]) != 2:
raise ValueError("Mondrian position tuples should be (ypos, xpos)")
except Exception:
raise ValueError("Mondrian position tuples should be (ypos, xpos)")

ypos, xpos = degrees_to_pixels(mondrian_positions[m], ppd[0])
individual_shapes = degrees_to_pixels(mondrian_sizes[m], ppd[0])

try:
if len(individual_shapes) == 2:
depth = 0
individual_shapes = individual_shapes + [depth,]
elif len(individual_shapes) == 3:
depth = mondrian_sizes[m][2]
else:
raise ValueError("Mondrian size tuples should be (height, width) for "
"rectangles or (height, width, depth) for parallelograms")
except Exception:
raise ValueError("Mondrian size tuples should be (height, width) for"
"rectangles or (height, width, depth) for parallelograms")

if depth < 0:
xpos += int(depth*ppd[0])
mondrian_positions_px.append(tuple([ypos, xpos]))
mondrian_shapes.append(tuple(individual_shapes))

# Create parallelogram
patch = parallelogram(
visual_size=(mondrian_sizes[m][0], mondrian_sizes[m][1]+np.abs(depth)),
ppd=ppd,
parallelogram_depth=depth,
intensity_background=intensity_background,
intensity_parallelogram=mondrian_intensities[m],
)

# Place it into Mondrian mosaic
yshape, xshape = patch["img"].shape
if ypos < 0 or xpos < 0:
raise ValueError("There are no negative position coordinates")
if (ypos+yshape > shape[0]) or (xpos+xshape > shape[1]):
raise ValueError("Not all Mondrians fit into the stimulus")
mask_large = np.zeros(shape)
mask_large[ypos:ypos+yshape, xpos:xpos+xshape] = patch["mask"]

img[mask_large == 1] = mondrian_intensities[m]
mask[mask_large == 1] = m+1

stim = {
"img": img,
"mondrian_mask": mask.astype(int),
"ppd": ppd,
"visual_size": visual_size,
"shape": shape,
"mondrian_positions": tuple(mondrian_positions),
"mondrian_positions_px": tuple(mondrian_positions_px),
"mondrian_sizes": tuple(mondrian_sizes),
"mondrian_shapes": tuple(mondrian_shapes),
"mondrian_intensities": tuple(mondrian_intensities),
"intensity_background": intensity_background,
}
return stim


if __name__ == "__main__":
from stimuli.utils.plotting import plot_stimuli

p1 = {
"mondrian_positions": ((0,0), (0,4), (1,3), (4,4), (5,1)),
"mondrian_sizes": 3,
"mondrian_intensities": np.random.rand(5),
}

p2 = {
"mondrian_positions": ((0,0), (8,4), (1,6), (4,4), (5,1)),
"mondrian_sizes": ((3,4,1), (2,2,0), (5,4,-1), (3,4,1), (5,2,0)),
"mondrian_intensities": np.random.rand(5),
}

p3 = {
"mondrian_positions": ((0,0), (0, 2)),
"mondrian_sizes": ((2,2,0), (2,2,0)),
"mondrian_intensities": (0.2, 0.8),
}

p4 = {
"mondrian_positions": ((0,0), (0, 2)),
"mondrian_sizes": ((2,2,1), (2,2,1)),
"mondrian_intensities": (0.2, 0.8),
}

stims = {
"mondrians1": mondrians(visual_size=8, ppd=10, **p1),
"mondrians2": mondrians(visual_size=10, ppd=10, **p2),
"mondrians3": mondrians(visual_size=(2, 6), ppd=10, **p3),
"mondrians4": mondrians(visual_size=(2, 6), ppd=10, **p4),
}

plot_stimuli(stims)
182 changes: 98 additions & 84 deletions stimuli/illusions/mondrians.py
@@ -1,42 +1,42 @@
import numpy as np

from stimuli.components.shapes import parallelogram
from stimuli.utils import degrees_to_pixels
from stimuli.components.mondrians import mondrians
from stimuli.utils import resolution, degrees_to_pixels

__all__ = [
"corrugated_mondrians",
]


def corrugated_mondrians(
visual_size=None,
ppd=None,
width=None,
heights=None,
depths=None,
shape=None,
mondrian_depths=None,
mondrian_intensities=None,
target_indices=None,
intensities=None,
intensity_background=0.5,
):
"""
Corrugated mondrians
Parameters
----------
ppd : int
pixels per degree (visual angle)
width : float
width of rectangles in degree visual angle
heights : float or tuple of floats
height of rectangles; if single float, all rectangles have the same height
depths : float or tuple of floats
depth of rectangles; as many depths as there are rows
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
mondrian_depths : float or tuple of floats
depth of parallelograms (ie mondrians) per row
mondrian_intensities : nested tuples
intensities of mondrians; as many tuples as there are rows and as many
numbers in each tuple as there are columns
target_indices : nested tuples
indices of targets; as many tuples as there are targets each with (x, y) indices
intensities : nested tuples
intensities of indiidual rectangles; as many tuples as there are rows and as many numbers in each
tuple as there are columns
indices of targets; as many tuples as there are targets with (y, x) indices
intensity_background : float
value for background
intensity value for background
Returns
-------
Expand All @@ -48,88 +48,89 @@ def corrugated_mondrians(
Science, 262(5142), 2042–2044. https://doi.org/10.1126/science.8266102
"""

if isinstance(heights, (float, int)):
heights = [heights] * len(depths)

if any(len(lst) != len(heights) for lst in [depths, intensities]):
raise Exception("heights, depths, and intensities need the same length.")

widths_px = degrees_to_pixels(width, ppd)
heights_px = degrees_to_pixels(heights, ppd)
depths_px = degrees_to_pixels(depths, ppd)

nrows = len(depths)
ncols = len(intensities[0])
height = int(np.array(heights_px).sum())
width_ = int(widths_px * ncols + np.abs(np.array(depths_px)).sum())
img = np.ones([height, width_]) * intensity_background
mask = np.zeros([height, width_])
mval = 1

# Resolve resolution
shape, visual_size, ppd = resolution.resolve(shape=shape, visual_size=visual_size, ppd=ppd)
if len(np.unique(ppd)) > 1:
raise ValueError("ppd should be equal in x and y direction")
nrows = len(mondrian_intensities)
ncols = len(mondrian_intensities[0])

if isinstance(mondrian_depths, (float, int)):
mondrian_depths = (mondrian_depths,) * nrows

if len(mondrian_depths) != nrows:
raise ValueError("Unclear number of Mondrians in y-direction, check elements "
"in mondrian_intensities and mondrian_depths")

height, width = visual_size
mdepths_px = degrees_to_pixels(mondrian_depths, ppd[0])
max_depth = np.abs(np.array(mdepths_px)).max()
sum_depth = np.array(mdepths_px).sum()
red_depth = np.maximum(max_depth, sum_depth)
mheight_px, mwidth_px = int(shape[0] / nrows), int((shape[1]-red_depth) / ncols)

# Initial y coordinates
yst = 0
yen = int(heights_px[0])

# Calculate initial x coordinates
xstarts = np.cumsum(np.hstack([0, depths_px]))
temp = np.hstack([depths_px, 0])
xstarts = np.cumsum(np.hstack([0, mdepths_px]))
temp = np.hstack([mdepths_px, 0])
temp[temp > 0] = 0.0
xstarts += temp
xstarts += np.abs(xstarts.min())


sizes = []
poses = []
ints = []
tlist = []
target_counter = 1

for r in range(nrows):
xst = xstarts[r]
xen = xst + int(widths_px + np.abs(depths_px[r]))
if mondrian_depths[r] < 0:
xst -= int(mondrian_depths[r]*ppd[0])

for c in range(ncols):
stim = parallelogram(
visual_size=(heights[r], width + abs(depths[r])),
ppd=ppd,
parallelogram_depth=depths[r],
intensity_background=0.0,
intensity_parallelogram=intensities[r][c] - intensity_background,
)

img[yst:yen, xst:xen] += stim["img"]

if (r, c) in target_indices:
mask[yst:yen, xst:xen] += stim["mask"] * mval
mval += 1

xst += widths_px
xen += widths_px
yst += heights_px[r]
yen += heights_px[r]

# Find and delete all irrelevant columns:
idx = np.argwhere(np.all(img == intensity_background, axis=0))
img = np.delete(img, idx, axis=1)
mask = np.delete(mask, idx, axis=1)

if len(np.unique(img[mask != 0])) > 1:
msize = (mheight_px/ppd[0], mwidth_px/ppd[1], mondrian_depths[r])
mpos = (yst/ppd[0], xst/ppd[1])
mint = mondrian_intensities[r][c]

sizes.append(msize)
poses.append(mpos)
ints.append(mint)

if (target_indices is not None) and (r, c) in target_indices:
tlist.append(target_counter)

xst += mwidth_px
target_counter += 1
yst += mheight_px

stim = mondrians(
visual_size=visual_size,
ppd=ppd,
shape=shape,
mondrian_positions=poses,
mondrian_sizes=sizes,
mondrian_intensities=ints,
intensity_background=intensity_background,
)
target_mask = np.zeros(shape)
for t in range(len(tlist)):
target_mask[stim["mondrian_mask"] == tlist[t]] = t+1
stim["mask"] = target_mask.astype(int)
stim["target_indices"] = target_indices

if len(np.unique(stim["img"][target_mask != 0])) > 1:
raise Exception("targets are not equiluminant.")

stim = {
"img": img,
"mask": mask.astype(int),
"ppd": ppd,
"visual_size": np.array(img.shape) / ppd,
"shape": img.shape,
"width": width,
"heights": heights,
"depths": depths,
"intensity_background": intensity_background,
"intensities": intensities,
"target_indices": target_indices,
}
return stim


if __name__ == "__main__":
from stimuli.utils import plot_stim

params = {
"ppd": 10,
"ppd": 20,
"width": 2.0,
"heights": 2.0,
"depths": (0.0, 1.0, 0.0, -1.0),
Expand All @@ -141,6 +142,19 @@ def corrugated_mondrians(
(0.0, 0.4, 0.0, 0.4),
),
}

p2 = {
"visual_size": 10,
"ppd": 20,
"mondrian_depths": (1, 0, -1, 0),
"mondrian_intensities": (
(0.4, 0.75, 0.4, 0.75),
(0.75, 0.4, 0.75, 1.0),
(0.4, 0.75, 0.4, 0.75),
(0.0, 0.4, 0.0, 0.4),
),
"target_indices": ((1, 1), (3, 1)),
}

stim = corrugated_mondrians(**params)
plot_stim(stim, stim_name="Corrugated mondrians", mask=True, save=None)
stim = corrugated_mondrians(**p2)
plot_stim(stim, stim_name="Corrugated mondrians", mask=False, save=None)
7 changes: 3 additions & 4 deletions stimuli/papers/RHS2007.py
Expand Up @@ -1412,12 +1412,11 @@ def corrugated_mondrian(ppd=PPD, pad=True):
(v3, v2, v3, v2, v3),
)
params = {
"visual_size": (5*2, 5*2+1),
"ppd": ppd,
"width": 2.0,
"heights": 2.0,
"depths": (0.0, -1.0, 0.0, 1.0, 0.0),
"mondrian_depths": (0.0, -1.0, 0.0, 1.0, 0.0),
"mondrian_intensities": values,
"target_indices": ((1, 2), (3, 2)),
"intensities": values,
"intensity_background": 0.5,
}

Expand Down

0 comments on commit 17058e9

Please sign in to comment.