From 3143d68dadede9b26256cfbb009b746721be1489 Mon Sep 17 00:00:00 2001 From: Matthew Cutone Date: Mon, 14 Oct 2019 11:13:10 -0400 Subject: [PATCH 1/8] NF: Pie class for creating pac-man shapes. --- psychopy/visual/pie.py | 116 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 psychopy/visual/pie.py diff --git a/psychopy/visual/pie.py b/psychopy/visual/pie.py new file mode 100644 index 0000000000..1aae25f660 --- /dev/null +++ b/psychopy/visual/pie.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Creates a pie shape.""" + +# Part of the PsychoPy library +# Copyright (C) 2002-2018 Jonathan Peirce (C) 2019 Open Science Tools Ltd. +# Distributed under the terms of the GNU General Public License (GPL). + +from __future__ import absolute_import, print_function +from psychopy.visual.shape import BaseShapeStim +from psychopy.tools.attributetools import attributeSetter, setAttribute +import numpy as np + + +class Pie(BaseShapeStim): + """Creates a pie shape which is a circle with a wedge cut-out. This + shape is frequently used for creating Kanizsa figures. However, it + can be adapted for other uses. + + Attributes + ---------- + start, end : float or int + Start and end angles of the filled region of the shape in + degrees. Shapes are filled counter clockwise between the + specified angles. + radius : float or int + Radius of the shape. + + """ + def __init__(self, win, radius=.5, edges=64, start=0.0, end=90.0, **kwargs): + self.radius = radius + self.edges = edges + self.__dict__['start'] = start + self.__dict__['end'] = end + + self._initParams = dir() + self._initParams.remove('self') + # kwargs isn't a parameter, but a list of params + self._initParams.remove('kwargs') + self._initParams.extend(kwargs) + + self.vertices = self._calcVertices() + kwargs['closeShape'] = True + kwargs['vertices'] = self.vertices + + super(Pie, self).__init__(win, **kwargs) + + def _calcVertices(self): + """Calculate the required vertices for the figure. + """ + startRadians = np.radians(self.start) + endRadians = np.radians(self.end) + + # get number of steps for vertices + steps = np.linspace(startRadians, endRadians, num=self.edges) + + # offset by 1 since the first vertex needs to be at centre + verts = np.zeros((self.edges + 1, 2), float) + verts[1:, 0] = np.sin(steps) + verts[1:, 1] = np.cos(steps) + + verts *= self.radius + + return verts + + @attributeSetter + def start(self, value): + """int or float. + Height of the Rectangle (in its respective units, if specified). + + :ref:`Operations ` supported. + """ + self.__dict__['start'] = value + self._calcVertices() + self.setVertices(self.vertices, log=False) + + def setStart(self, start, operation='', log=None): + """Usually you can use 'stim.attribute = value' syntax instead, + but use this method if you need to suppress the log message + """ + setAttribute(self, 'start', start, log, operation) + + @attributeSetter + def end(self, value): + """int or float. + Height of the Rectangle (in its respective units, if specified). + + :ref:`Operations ` supported. + """ + self.__dict__['end'] = value + self._calcVertices() + self.setVertices(self.vertices, log=False) + + def setEnd(self, end, operation='', log=None): + """Usually you can use 'stim.attribute = value' syntax instead, + but use this method if you need to suppress the log message + """ + setAttribute(self, 'end', end, log, operation) + + @attributeSetter + def radius(self, value): + """int or float. + Height of the Rectangle (in its respective units, if specified). + + :ref:`Operations ` supported. + """ + self.__dict__['radius'] = value + self._calcVertices() + self.setVertices(self.vertices, log=False) + + def setRadius(self, end, operation='', log=None): + """Usually you can use 'stim.attribute = value' syntax instead, + but use this method if you need to suppress the log message + """ + setAttribute(self, 'radius', end, log, operation) From 1c0853c98b5eb13fdab1f4914a27bb4e8f0c126a Mon Sep 17 00:00:00 2001 From: Matthew Cutone Date: Mon, 14 Oct 2019 21:18:18 -0400 Subject: [PATCH 2/8] RF: Explicit arguments for class, removed kwargs. --- psychopy/visual/pie.py | 76 ++++++++++++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 18 deletions(-) diff --git a/psychopy/visual/pie.py b/psychopy/visual/pie.py index 1aae25f660..98fc197605 100644 --- a/psychopy/visual/pie.py +++ b/psychopy/visual/pie.py @@ -22,29 +22,69 @@ class Pie(BaseShapeStim): ---------- start, end : float or int Start and end angles of the filled region of the shape in - degrees. Shapes are filled counter clockwise between the - specified angles. + degrees. Shapes are filled counter clockwise between the specified + angles. radius : float or int Radius of the shape. """ - def __init__(self, win, radius=.5, edges=64, start=0.0, end=90.0, **kwargs): - self.radius = radius - self.edges = edges + def __init__(self, + win, + radius=.5, + start=0.0, + end=90.0, + edges=32, + units='', + lineWidth=1.5, + lineColor=(1.0, 1.0, 1.0), + lineColorSpace='rgb', + fillColor=None, + fillColorSpace='rgb', + pos=(0, 0), + ori=0.0, + opacity=1.0, + contrast=1.0, + depth=0, + interpolate=True, + lineRGB=None, + fillRGB=None, + name=None, + autoLog=None, + autoDraw=False, + color=None, + colorSpace=None): + + self.__dict__['radius'] = radius + self.__dict__['edges'] = edges self.__dict__['start'] = start self.__dict__['end'] = end - self._initParams = dir() - self._initParams.remove('self') - # kwargs isn't a parameter, but a list of params - self._initParams.remove('kwargs') - self._initParams.extend(kwargs) - self.vertices = self._calcVertices() - kwargs['closeShape'] = True - kwargs['vertices'] = self.vertices - super(Pie, self).__init__(win, **kwargs) + super(Pie, self).__init__( + win, + units=units, + lineWidth=lineWidth, + lineColor=lineColor, + lineColorSpace=lineColorSpace, + fillColor=fillColor, + fillColorSpace=fillColorSpace, + vertices=self.vertices, + closeShape=True, + pos=pos, + size=1, + ori=ori, + opacity=opacity, + contrast=contrast, + depth=depth, + interpolate=interpolate, + lineRGB=lineRGB, + fillRGB=fillRGB, + name=name, + autoLog=autoLog, + autoDraw=autoDraw, + color=color, + colorSpace=colorSpace) def _calcVertices(self): """Calculate the required vertices for the figure. @@ -53,10 +93,11 @@ def _calcVertices(self): endRadians = np.radians(self.end) # get number of steps for vertices - steps = np.linspace(startRadians, endRadians, num=self.edges) + edges = self.__dict__['edges'] + steps = np.linspace(startRadians, endRadians, num=edges) # offset by 1 since the first vertex needs to be at centre - verts = np.zeros((self.edges + 1, 2), float) + verts = np.zeros((edges + 1, 2), float) verts[1:, 0] = np.sin(steps) verts[1:, 1] = np.cos(steps) @@ -106,8 +147,7 @@ def radius(self, value): :ref:`Operations ` supported. """ self.__dict__['radius'] = value - self._calcVertices() - self.setVertices(self.vertices, log=False) + self.size = value def setRadius(self, end, operation='', log=None): """Usually you can use 'stim.attribute = value' syntax instead, From a1f741be866c1a8292a6b5645a67cfc247b127e1 Mon Sep 17 00:00:00 2001 From: Matthew Cutone Date: Mon, 14 Oct 2019 22:17:21 -0400 Subject: [PATCH 3/8] DEMO: Added Kanizsa demo. --- psychopy/demos/coder/stimuli/kanizsa.py | 41 +++++++++++++++++++++ psychopy/visual/__init__.py | 1 + psychopy/visual/pie.py | 47 +++++++++++++++++-------- 3 files changed, 75 insertions(+), 14 deletions(-) create mode 100644 psychopy/demos/coder/stimuli/kanizsa.py diff --git a/psychopy/demos/coder/stimuli/kanizsa.py b/psychopy/demos/coder/stimuli/kanizsa.py new file mode 100644 index 0000000000..981620869e --- /dev/null +++ b/psychopy/demos/coder/stimuli/kanizsa.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Demo for the class psychopy.visual.Pie(). + +Use the `Pie` class to create a Kanizsa figure which produces illusory +contours. + +""" +from psychopy import core +import psychopy.visual as visual +from psychopy.visual.pie import Pie +from psychopy import event + +# open a window to render the shape +win = visual.Window((600, 600), allowGUI=False, monitor='testMonitor') + +# create the stimulus object +pieStim = Pie( + win, radius=50, start=0., end=270., fillColor=(-1., -1., -1.), units='pix') + +message = visual.TextStim( + win, text='Any key to quit', pos=(0, -0.8), units='norm') + +# positions of the corners of the shapes +pos = [(-100, 100), (100, 100), (-100, -100), (100, -100)] + +# orientations of the shapes +ori = [180., 270., 90., 0.] + +while not event.getKeys(): + + for i in range(4): + pieStim.pos = pos[i] + pieStim.ori = ori[i] + pieStim.draw() + + message.draw() + win.flip() + +win.close() +core.quit() diff --git a/psychopy/visual/__init__.py b/psychopy/visual/__init__.py index 22db2aada1..dcf827420c 100644 --- a/psychopy/visual/__init__.py +++ b/psychopy/visual/__init__.py @@ -75,6 +75,7 @@ from psychopy.visual.line import Line from psychopy.visual.polygon import Polygon from psychopy.visual.rect import Rect +from psychopy.visual.pie import Pie # stimuli derived from Polygon from psychopy.visual.circle import Circle diff --git a/psychopy/visual/pie.py b/psychopy/visual/pie.py index 98fc197605..bc98ee7990 100644 --- a/psychopy/visual/pie.py +++ b/psychopy/visual/pie.py @@ -14,9 +14,11 @@ class Pie(BaseShapeStim): - """Creates a pie shape which is a circle with a wedge cut-out. This - shape is frequently used for creating Kanizsa figures. However, it - can be adapted for other uses. + """Creates a pie shape which is a circle with a wedge cut-out. + + This shape is sometimes refered to as a Pac-Man shape which is frequently + used for creating Kanizsa figures. However, the shape can be adapted for + other uses. Attributes ---------- @@ -25,7 +27,8 @@ class Pie(BaseShapeStim): degrees. Shapes are filled counter clockwise between the specified angles. radius : float or int - Radius of the shape. + Radius of the shape. Avoid using `size` for adjusting figure dimensions + if radius != 0.5 which will result in undefined behavior. """ def __init__(self, @@ -36,11 +39,12 @@ def __init__(self, edges=32, units='', lineWidth=1.5, - lineColor=(1.0, 1.0, 1.0), + lineColor=None, lineColorSpace='rgb', fillColor=None, fillColorSpace='rgb', pos=(0, 0), + size=1, ori=0.0, opacity=1.0, contrast=1.0, @@ -54,6 +58,23 @@ def __init__(self, color=None, colorSpace=None): + """ + Parameters + ---------- + win : `~psychopy.visual.Window` + Window this shape is associated with. + radius : float or int + Radius of the shape. Avoid using `size` for adjusting figure + dimensions if radius != 0.5 which will result in undefined behavior. + start, end : float or int + Start and end angles of the filled region of the shape in + degrees. Shapes are filled counter clockwise between the specified + angles. + edges : int + Number of edges to use when drawing the figure. A greater number of + edges will result in smoother curves, but will require more time + to compute. + """ self.__dict__['radius'] = radius self.__dict__['edges'] = edges self.__dict__['start'] = start @@ -72,7 +93,7 @@ def __init__(self, vertices=self.vertices, closeShape=True, pos=pos, - size=1, + size=size, ori=ori, opacity=opacity, contrast=contrast, @@ -97,9 +118,9 @@ def _calcVertices(self): steps = np.linspace(startRadians, endRadians, num=edges) # offset by 1 since the first vertex needs to be at centre - verts = np.zeros((edges + 1, 2), float) - verts[1:, 0] = np.sin(steps) - verts[1:, 1] = np.cos(steps) + verts = np.zeros((edges + 2, 2), float) + verts[1:-1, 0] = np.sin(steps) + verts[1:-1, 1] = np.cos(steps) verts *= self.radius @@ -107,8 +128,7 @@ def _calcVertices(self): @attributeSetter def start(self, value): - """int or float. - Height of the Rectangle (in its respective units, if specified). + """Start angle of the slice/wedge in degrees (`float` or `int`). :ref:`Operations ` supported. """ @@ -124,8 +144,7 @@ def setStart(self, start, operation='', log=None): @attributeSetter def end(self, value): - """int or float. - Height of the Rectangle (in its respective units, if specified). + """End angle of the slice/wedge in degrees (`float` or `int`). :ref:`Operations ` supported. """ @@ -142,7 +161,7 @@ def setEnd(self, end, operation='', log=None): @attributeSetter def radius(self, value): """int or float. - Height of the Rectangle (in its respective units, if specified). + Radius of the shape in `units` (`float` or `int`). :ref:`Operations ` supported. """ From 2f66240d65c38ec298175d80eb0d42391f0a02d1 Mon Sep 17 00:00:00 2001 From: Matthew Cutone Date: Mon, 14 Oct 2019 22:17:53 -0400 Subject: [PATCH 4/8] FF: Use lazy loaded module path. --- psychopy/demos/coder/stimuli/kanizsa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psychopy/demos/coder/stimuli/kanizsa.py b/psychopy/demos/coder/stimuli/kanizsa.py index 981620869e..02a816b58f 100644 --- a/psychopy/demos/coder/stimuli/kanizsa.py +++ b/psychopy/demos/coder/stimuli/kanizsa.py @@ -8,7 +8,7 @@ """ from psychopy import core import psychopy.visual as visual -from psychopy.visual.pie import Pie +from psychopy.visual import Pie from psychopy import event # open a window to render the shape From 77390fb6b7fd881d1ce8264655755938740cccea Mon Sep 17 00:00:00 2001 From: Matthew Cutone Date: Mon, 14 Oct 2019 22:19:57 -0400 Subject: [PATCH 5/8] DOCS: Add documentation stub for Pie. --- docs/source/api/visual/pie.rst | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 docs/source/api/visual/pie.rst diff --git a/docs/source/api/visual/pie.rst b/docs/source/api/visual/pie.rst new file mode 100644 index 0000000000..4c41ec5bde --- /dev/null +++ b/docs/source/api/visual/pie.rst @@ -0,0 +1,8 @@ + + +:class:`Pie` +------------------------------------ +.. autoclass:: psychopy.visual.Pie + :members: + :undoc-members: + :inherited-members: From 8472f8c85529647ea33ca9a68c658ede03aff9c8 Mon Sep 17 00:00:00 2001 From: Matthew Cutone Date: Mon, 14 Oct 2019 22:33:10 -0400 Subject: [PATCH 6/8] DOCS: Formatting fix. --- psychopy/visual/pie.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/psychopy/visual/pie.py b/psychopy/visual/pie.py index bc98ee7990..8091349d57 100644 --- a/psychopy/visual/pie.py +++ b/psychopy/visual/pie.py @@ -160,8 +160,7 @@ def setEnd(self, end, operation='', log=None): @attributeSetter def radius(self, value): - """int or float. - Radius of the shape in `units` (`float` or `int`). + """Radius of the shape in `units` (`float` or `int`). :ref:`Operations ` supported. """ From 1c6ea0e13d980cef64aa562ba960adec933f89eb Mon Sep 17 00:00:00 2001 From: Matthew Cutone Date: Mon, 14 Oct 2019 22:34:23 -0400 Subject: [PATCH 7/8] DOCS: Typos. --- psychopy/visual/pie.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psychopy/visual/pie.py b/psychopy/visual/pie.py index 8091349d57..908632ceed 100644 --- a/psychopy/visual/pie.py +++ b/psychopy/visual/pie.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -"""Creates a pie shape.""" +"""Create a pie shape.""" # Part of the PsychoPy library # Copyright (C) 2002-2018 Jonathan Peirce (C) 2019 Open Science Tools Ltd. @@ -16,7 +16,7 @@ class Pie(BaseShapeStim): """Creates a pie shape which is a circle with a wedge cut-out. - This shape is sometimes refered to as a Pac-Man shape which is frequently + This shape is sometimes referred to as a Pac-Man shape which is often used for creating Kanizsa figures. However, the shape can be adapted for other uses. From 035e57e47062844eb6433cd65ec323ec1f3b40ab Mon Sep 17 00:00:00 2001 From: Matthew Cutone Date: Mon, 14 Oct 2019 22:35:59 -0400 Subject: [PATCH 8/8] FF: Make sure attributes update vertices. --- psychopy/visual/pie.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/psychopy/visual/pie.py b/psychopy/visual/pie.py index 908632ceed..0970118468 100644 --- a/psychopy/visual/pie.py +++ b/psychopy/visual/pie.py @@ -133,7 +133,7 @@ def start(self, value): :ref:`Operations ` supported. """ self.__dict__['start'] = value - self._calcVertices() + self.vertices = self._calcVertices() self.setVertices(self.vertices, log=False) def setStart(self, start, operation='', log=None): @@ -149,7 +149,7 @@ def end(self, value): :ref:`Operations ` supported. """ self.__dict__['end'] = value - self._calcVertices() + self.vertices = self._calcVertices() self.setVertices(self.vertices, log=False) def setEnd(self, end, operation='', log=None): @@ -165,7 +165,8 @@ def radius(self, value): :ref:`Operations ` supported. """ self.__dict__['radius'] = value - self.size = value + self.vertices = self._calcVertices() + self.setVertices(self.vertices, log=False) def setRadius(self, end, operation='', log=None): """Usually you can use 'stim.attribute = value' syntax instead,