psychopy.org | Reference | Downloads | Github

Wrong mouse location with Psychopy 3.06 with MacOS Retina display


#1

I’m running the following program on macOS 10.14.3 Mojave

from psychopy import core,visual,event

def checkKeyboard():
    for key in event.getKeys():
        if key in ['q']:
            sys.exit()
        else:
            event.clearEvents()

win = visual.Window([1024,768],monitor="monitor",rgb = [0,0,0],winType='pyglet',waitBlanking=True,units='pix', allowGUI=False,fullscr=False)
myMouse = event.Mouse(visible=True,newPos=[0,0],win=win)

while True:
    print(myMouse.getPos())
    win.flip()
    checkKeyboard()

When I use Psychopy2 1.85.4, the program works as intended with the mouse appearing in the middle of the screen though myMouse.getPos() does not return (0,0) but (-171,364).

When I run the same program with Psychopy3 1.06, the mouse appears at (1024,768), with at least, myMouse.getPos() returning the correct (x,y) coordinates).

This seems to be the cause of some major problems I’m encountering with a more complicated program that used to run smoothly in Psychopy2 1.85.4 but now behave strangely under Psychopy3 1.06.

Does anybody know the source of the problem and if it will be fixed in future version of psychopy?


#2

#3

Thanks for the information. I edited my post following your guidelines.


#4

My guess is that this is an issue with a retina display (is your mac a retina display?)

In essence, we couldn’t decide the best way to handle reporting of resolution given that Apple took the policy of having these double-resolution monitors but “pretending” that their screen was regular resolution.

When I run the following, adapted version of your script I get sensible values reported for my retina display and the message appears on the mouse icon as intended. Does this explain the issue you’ve got?

cheers


#5

Thanks for the answer! My Mac has indeed a Retina display so the problem has to come from there. I could not find the code you mentioned in your answer to see if it solves the problem: Did I miss it? Thanks again!


#6

Apologies:

from psychopy import core,visual,event

win = visual.Window([1024,768],monitor="monitor",rgb = [0,0,0],winType='pyglet',waitBlanking=True,units='pix', allowGUI=False,fullscr=False)
myMouse = event.Mouse(visible=True,newPos=[0,0],win=win)

msg =  visual.TextStim(win, text='')
msg.setAutoDraw(True)

if win.useRetina:
    multiplier = 0.5

while True:
    pos = myMouse.getPos()
    msg.text = str(pos)
    msg.pos = pos * multiplier
    win.flip()
    if 'escape' in event.getKeys():
        core.quit()
    
    

#7

Strangely, the code still does not work on my computer. The mouse and the message still appear in the upper left corner of the window with the message saying [1024, 768].

The coordinates which appear in the message seem correct. For instance, when I move the mouse manually to the center of the screen, it correctly indicates that the mouse is at (0,0) but the program ignores any instruction I give it regarding the location of the mouse, either when creating the mouse or by invoking the setPos() method of the mouse object.

Actually, I noticed that, no matter the instruction I give to the program regarding the location of the mouse, the mouse always appears in the upper right corner of the window. For instance, with the following program

from psychopy import core,visual,event

FULLSCREEN = False
posMouseX = 800
poMouseY = 600

win = visual.Window([1024,768],monitor="monitor",rgb = [0,0,0],winType='pyglet',waitBlanking=True,units='pix', allowGUI=False,fullscr=FULLSCREEN)
myMouse = event.Mouse(visible=True,newPos=[posMouseX,posMouseY],win=win)

msg =  visual.TextStim(win, text='')
msg.setAutoDraw(True)

while True:
    pos = myMouse.getPos()
    msg.text = str(pos)
    msg.pos = pos * multiplier
    win.flip()
    if 'escape' in event.getKeys():
        core.quit()
    ```
The mouse will appear at [1024,768] (adding a line myMouse.setPos([800,600]) does not change anything).

If FULLSCREEN is set to True, the mouse will appear at [1440,900], which is half of what is supposed to be my actual screen resolution (2880x1800) but I guess this is related to the issue you mentioned with the Retina display. 

In a nutshell, psychopy seems to ignore any instruction I give it regarding the location of the mouse.

#8

So, one thing that’s an FYI but doesn’t fix your problem: The way Python coordinates work, [0,0] is in the dead center of the window. So if you have a window that’s 1024x768, then [800,600] is actually way out of bounds to the top and right. The top-right corner of the window would have the coordinates [512,384].

However, even if I set the coordinates to something in-bounds, it’s still putting the mouse in the top-right corner of the window no matter what, which is very weird. Even if I try to set it to [0,0] there, which should not be affected by retina wonkiness, it ends up in the top right. However, if I set it to [-512, -384], it puts the mouse at 0,0! Similarly, if I fullscreen it and set the coordinates to [-720, -450], it ends up at 0,0.

Some kind of very wonky math is happening with retina displays and window location/dimensions in relation to mouse coordinates. This is not news, I’ve been trying to wrestle a related set of issues to the ground for months (it gets very complicated if you have two screens and one of them is not a retina display). The mouse coordinates that are being reported by getPos are correct to the grid centered on 0,0. Those used by newPos seem instead to set 0,0 as the top-right corner of the window. That at least gives you reliable behavior so you can do some kind of workaround, but it’s definitely weird.

EDIT: @jon Found the offending line of code in event.py (610). I’ll have a go at a bug-fix shortly.

from psychopy import core,visual,event

FULLSCREEN = False
posMouseX = -512
posMouseY = -384

win = visual.Window([1024,768],monitor="monitor",rgb = [0,0,0],winType='pyglet',waitBlanking=True,units='pix', allowGUI=False,fullscr=FULLSCREEN)
myMouse = event.Mouse(visible=True,newPos=[posMouseX,posMouseY],win=win)

msg =  visual.TextStim(win, text='')
msg.setAutoDraw(True)

if win.useRetina:
    multiplier = 0.5
else:
    multiplier=1

while True:
    pos = myMouse.getPos()
    msg.text = str(pos)
    msg.pos = pos * multiplier
    win.flip()
    if 'escape' in event.getKeys():
        core.quit()

#9

I still wonder if it’s worth going a different route with retina displays and providing everything in native pixel coords instead of this confusion with a (sometimes failed) attempt to use the silly virtual coords


#10

I’m not opposed, but I’m not sure it’s entirely on PsychoPy’s end. This seems to be something about how Pyglet reports window size and the locations of certain things. Like, in this case, it’s not that PsychoPy is trying to work in one set of units and Pyglet in another, it seems to be that Pyglet is reporting size in raw pixels but expecting mouse coordinates in points. If we could find an easy solution to make it just commit one way or the other, I’d be all for it.


#11

Experimenting a bit after reading your comments, I found out that the following program puts the mouse correctly at the center of the screen

from psychopy import core,visual,event

FULLSCREEN = True
win = visual.Window([800,600],monitor="monitor",rgb = [0,0,0],winType='pyglet',waitBlanking=True,units='pix', allowGUI=False,fullscr=FULLSCREEN)
myMouse = event.Mouse(visible=True,newPos=[0,0],win=win)
print(win.size)
if win.useRetina:
    #myMouse.setPos([0-800*0.5,0-600*0.5])
    myMouse.setPos([0-win.size[0]*0.25,0-win.size[1]*0.25])

msg =  visual.TextStim(win, text='')
msg.setAutoDraw(True)

while True:
    pos = myMouse.getPos()
    msg.text = str(pos)
    win.flip()
    if 'escape' in event.getKeys():
        core.quit()

This solves this mouse location problem but I’m encountering other problems which seems to be linked to the mouse location but that cannot be fixed as easily. Notably, the program below is based on a program for which I started noticing this problem. It used to work well on Psychopy2 1.85.4.

from psychopy import visual,event,core

cl = core.Clock()

#RESPONSE PANEL PARAMETERS
serif = ['Arial']
scaling = 0.95 #To increase/decrease all the elements in the response panel
responsePanelTextSizeButton = (40/1.3)*scaling #Control the size of the text inside the button of the response panel

#Control the line in the lickert scale
lineScale = 1.2                                                      #Control the size of the line. Allows the button to scale immediately with it
posLineX = 0*scaling                                            #X position of the center of the line
posLineY = -0*scaling                                           #Y position of the center of the line
lineWidth = 590*scaling*lineScale                      #Width of the line
lineHeight = 2.5*scaling                                        #Heigth of the line 

#General appearance for the appearance of the buttons and the accompanying lables
buttonTextSize = 17.0*scaling                                                           #13 Determines the size of the font
borderColor = "Black"                                                                    #Determines the color of the border of the buttons
inactiveColor = "White"                                                                   #Determines the color of the buttons when inactive
activeColor = "Black"                                                                      #Determines the color of the buttons when active
betweenButtons = 56*scaling*lineScale                                  #Distance between buttons
buttonRadius = 22*scaling*lineScale                                          #radius of the buttons
buttonOutWidth = 2                                                                           #Thickness of the border line of the buttons
buttonX = -280*scaling*lineScale                                                #X position of the left most button
textY = 1.2*(-60/1.3)*scaling                                                                                        #Y position of the first level of text

#Control the location of the numbers inside the buttons. Watch out! Number 10 requires special adjustment
buttonNumberPosAdjX = 2*(-12/1.3)*scaling
buttonNumberPosAdjY = (-25/1.3)*scaling

#MOUSE PARAMETERS
xMouse = 0                                   #xlocation of the mouse when the response panel is shown. 
yMouse = -200                                   #ylocation of the mouse when the response panel is shown. 
clickDuration = 0.15                      #Control the duration of the feedback given when participants click on the response panel (sec)

def waitForMouseClick(myMouse):
    waiting = True
    #Waiting for mouse to be pressed
    while (waiting):
        buffer = checkKeyboard()
        if buffer == 'escape':
            sys.exit()
        buttons = myMouse.getPressed()
        if (buttons[0] == 1):
            waiting = False
    waiting = True
    #Waiting for mouse to be released
    while (waiting):
        buttons = myMouse.getPressed()
        if sum(buttons) == 0:
            waiting = False
    return
    
def checkKeyboard():
    for key in event.getKeys():
        if key in ['q']:
            sys.exit()
        else:
            event.clearEvents()
            
def wait(duration):
    t = cl.getTime()
    while(cl.getTime()-t)<duration:
        pass
    return

class responsePanel:
    def __init__ (self):
        self.respLine = visual.GratingStim(win,
                                                units="pix",
                                                tex="None",
                                                mask="None",
                                                texRes=256, 
                                                pos=(posLineX,posLineY), 
                                                size=[lineWidth,lineHeight], 
                                                sf=[0,0], 
                                                ori = 0, 
                                                name='picFrame', 
                                                rgb=(-1,-1,-1))
        self.b1 = visual.Circle(win,
                        units='pix',
                        radius = buttonRadius,
                        edges=32,
                        lineWidth = buttonOutWidth,
                        fillColor = inactiveColor,
                        lineColor = borderColor,
                        pos=(buttonX,posLineY))
        self.b2 = visual.Circle(win,
                        units='pix',
                        radius = buttonRadius,
                        edges=32,
                        lineWidth = buttonOutWidth,
                        fillColor = inactiveColor,
                        lineColor = borderColor,
                        pos=((buttonX+betweenButtons*1),posLineY))
        self.b3 = visual.Circle(win,
                        units='pix',
                        radius = buttonRadius,
                        edges=32,
                        lineWidth = buttonOutWidth,
                        fillColor = inactiveColor,
                        lineColor = borderColor,
                        pos=((buttonX+betweenButtons*2),posLineY))
        self.b4 = visual.Circle(win,
                        units='pix',
                        radius = buttonRadius,
                        edges=32,
                        lineWidth = buttonOutWidth,
                        fillColor = inactiveColor,
                        lineColor = borderColor,
                        pos=((buttonX+betweenButtons*3),posLineY))
        self.b5 = visual.Circle(win,
                        units='pix',
                        radius = buttonRadius,
                        edges=32,
                        lineWidth = buttonOutWidth,
                        fillColor = inactiveColor,
                        lineColor = borderColor,
                        pos=((buttonX+betweenButtons*4),posLineY))
        self.b6 = visual.Circle(win,
                        units='pix',
                        radius = buttonRadius,
                        edges=32,
                        lineWidth = buttonOutWidth,
                        fillColor = inactiveColor,
                        lineColor = borderColor,
                        pos=((buttonX+betweenButtons*5),posLineY))
        self.b7 = visual.Circle(win,
                        units='pix',
                        radius = buttonRadius,
                        edges=32,
                        lineWidth = buttonOutWidth,
                        fillColor = inactiveColor,
                        lineColor = borderColor,
                        pos=((buttonX+betweenButtons*6),posLineY))
        self.b8 = visual.Circle(win,
                        units='pix',
                        radius = buttonRadius,
                        edges=32,
                        lineWidth = buttonOutWidth,
                        fillColor = inactiveColor,
                        lineColor = borderColor,
                        pos=((buttonX+betweenButtons*7),posLineY))
        self.b9 = visual.Circle(win,
                        units='pix',
                        radius = buttonRadius,
                        edges=32,
                        lineWidth = buttonOutWidth,
                        fillColor = inactiveColor,
                        lineColor = borderColor,
                        pos=((buttonX+betweenButtons*8),posLineY))
        self.b10 = visual.Circle(win,
                        units='pix',
                        radius = buttonRadius,
                        edges=32,
                        lineWidth = buttonOutWidth,
                        fillColor = inactiveColor,
                        lineColor = borderColor,
                        pos=((buttonX+betweenButtons*9),posLineY))
        self.b11 = visual.Circle(win,
                        units='pix',
                        radius = buttonRadius,
                        edges=32,
                        lineWidth = buttonOutWidth,
                        fillColor = inactiveColor,
                        lineColor = borderColor,
                        pos=((buttonX+betweenButtons*10),posLineY))
        self.b1Number = visual.TextStim(win, 
                            units='pix',height = responsePanelTextSizeButton,
                            pos=((buttonX+buttonNumberPosAdjX),(posLineY+buttonNumberPosAdjY)), text=" 0",
                            font=serif, 
                            wrapWidth=700,
                            alignHoriz = 'left',alignVert='bottom',
                            color='Black')
        self.b2Number = visual.TextStim(win, 
                            units='pix',height = responsePanelTextSizeButton,
                            pos=(((buttonX+betweenButtons*1)+buttonNumberPosAdjX),(posLineY+buttonNumberPosAdjY)), text="10",
                            font=serif, 
                            wrapWidth=700,
                            alignHoriz = 'left',alignVert='bottom',
                            color='Black')
        self.b3Number = visual.TextStim(win, 
                            units='pix',height = responsePanelTextSizeButton,
                            pos=(((buttonX+betweenButtons*2)+buttonNumberPosAdjX),(posLineY+buttonNumberPosAdjY)), text="20",
                            font=serif, 
                            wrapWidth=700,
                            alignHoriz = 'left',alignVert='bottom',
                            color='Black')
        self.b4Number = visual.TextStim(win, 
                            units='pix',height = responsePanelTextSizeButton,
                            pos=(((buttonX+betweenButtons*3)+buttonNumberPosAdjX),(posLineY+buttonNumberPosAdjY)), text="30",
                            font=serif, 
                            wrapWidth=700,
                            alignHoriz = 'left',alignVert='bottom',
                            color='Black')
        self.b5Number = visual.TextStim(win, 
                            units='pix',height = responsePanelTextSizeButton,
                            pos=(((buttonX+betweenButtons*4)+buttonNumberPosAdjX),(posLineY+buttonNumberPosAdjY)), text="40",
                            font=serif, 
                            wrapWidth=700,
                            alignHoriz = 'left',alignVert='bottom',
                            color='Black')
        self.b6Number = visual.TextStim(win, 
                            units='pix',height = responsePanelTextSizeButton,
                            pos=(((buttonX+betweenButtons*5)+buttonNumberPosAdjX),(posLineY+buttonNumberPosAdjY)), text="50",
                            font=serif, 
                            wrapWidth=700,
                            alignHoriz = 'left',alignVert='bottom',
                            color='Black')
        self.b7Number = visual.TextStim(win, 
                            units='pix',height = responsePanelTextSizeButton,
                            pos=(((buttonX+betweenButtons*6)+buttonNumberPosAdjX),(posLineY+buttonNumberPosAdjY)), text="60",
                            font=serif, 
                            wrapWidth=700,
                            alignHoriz = 'left',alignVert='bottom',
                            color='Black')
        self.b8Number = visual.TextStim(win, 
                            units='pix',height = responsePanelTextSizeButton,
                            pos=(((buttonX+betweenButtons*7)+buttonNumberPosAdjX),(posLineY+buttonNumberPosAdjY)), text="70",
                            font=serif, 
                            wrapWidth=700,
                            alignHoriz = 'left',alignVert='bottom',
                            color='Black')
        self.b9Number = visual.TextStim(win, 
                            units='pix',height = responsePanelTextSizeButton,
                            pos=(((buttonX+betweenButtons*8)+buttonNumberPosAdjX),(posLineY+buttonNumberPosAdjY)), text="80",
                            font=serif, 
                            wrapWidth=700,
                            alignHoriz = 'left',alignVert='bottom',
                            color='Black')
        self.b10Number = visual.TextStim(win, 
                            units='pix',height = responsePanelTextSizeButton,
                            pos=(((buttonX+betweenButtons*9)+buttonNumberPosAdjX),(posLineY+buttonNumberPosAdjY)), text="90",
                            font=serif, 
                            wrapWidth=700,
                            alignHoriz = 'left',alignVert='bottom',
                            color='Black')
        self.b11Number = visual.TextStim(win, 
                            units='pix',height = responsePanelTextSizeButton,
                            pos=(((buttonX+betweenButtons*10)+buttonNumberPosAdjX-((12/1.3)*scaling)),(posLineY+buttonNumberPosAdjY)), text="100",
                            font=serif, 
                            wrapWidth=700,
                            alignHoriz = 'left',alignVert='bottom',
                            color='Black')
                            
    def draw(self):
        self.respLine.draw()
        self.b1.draw()
        self.b2.draw()
        self.b3.draw()
        self.b4.draw()
        self.b5.draw()
        self.b6.draw()
        self.b7.draw()
        self.b8.draw()
        self.b9.draw()
        self.b10.draw()
        self.b11.draw()
        self.b1Number.draw()
        self.b2Number.draw()
        self.b3Number.draw()
        self.b4Number.draw()
        self.b5Number.draw()
        self.b6Number.draw()
        self.b7Number.draw()
        self.b8Number.draw()
        self.b9Number.draw()
        self.b10Number.draw()
        self.b11Number.draw()
        
    def showPanel(self,mouse): #[retentionInterval,firstRecovery,[inst1a,inst1b,inst2]]
        self.draw()
        mouse.setPos(newPos=[0,0])
        mouse.setVisible(1)
        win.flip()
        resp = self.recordResponse(mouse)
        mouse.setVisible(0)
        return resp
            
    def recordResponse(self,mouse):
        event.clearEvents()
        while True:
            buffer = waitForMouseClick(mouse)
            if self.b1.contains(mouse):
                self.activateButton(self.b1)
                return 0
            elif self.b2.contains(mouse):
                self.activateButton(self.b2)
                return 10
            elif self.b3.contains(mouse):
                self.activateButton(self.b3)
                return 20
            elif self.b4.contains(mouse):
                self.activateButton(self.b4)
                return 30
            elif self.b5.contains(mouse):
                self.activateButton(self.b5)
                return 40
            elif self.b6.contains(mouse):
                self.activateButton(self.b6)
                return 50
            elif self.b7.contains(mouse):
                self.activateButton(self.b7)
                return 60
            elif self.b8.contains(mouse):
                self.activateButton(self.b8)
                return 70
            elif self.b9.contains(mouse):
                self.activateButton(self.b9)
                return 80
            elif self.b10.contains(mouse):
                self.activateButton(self.b10)
                return 90
            elif self.b11.contains(mouse):
                self.activateButton(self.b11)
                return 100
              
    def activateButton(self,button):
        button.setFillColor(activeColor)
        self.draw()
        win.flip()
        wait(clickDuration)
        button.setFillColor(inactiveColor)
        win.flip()
        
FULLSCREEN = False
win = visual.Window([1024,768],monitor="monitor",rgb=[0,0,0],winType='pyglet',waitBlanking=True,units='pix', allowGUI=False,fullscr=False)
myMouse = event.Mouse(visible=True,newPos=[0,0],win=win)

testPanel = responsePanel()
while True:
    print (testPanel.showPanel(myMouse))

I apologise as the code is a bit long but basically, it creates a a 11-point Lickert scale going from 0 to 10: the participant click on the number to provide his answer.

In Psychopy3, and I think in versions of Psychopy2 released after 1.85.4, the program works strangely: (a) If you click on button 50 (whose location must be 0,0 at the center of the screen), button 50 is activated but (b) if you click on button 40, button 30 is activated; if you click on button 30, button 10 is activated; nothing happens if you click on buttons 20, 10 and 0. Likewise, © if you click on button 60, button 70 is activated; if you click on button 70, button 90 is activated; nothing happens if you click on buttons 80, 90 and 100.

So there seems some kind of non-linear distortion between the actual position of the mouse on the x-axis and the position recorded by psychopy.

The part of the program dealing with the location of the mouse is the following method

def recordResponse(self,mouse):
        event.clearEvents()
        while True:
            buffer = waitForMouseClick(mouse)
            if self.b1.contains(mouse):
                self.activateButton(self.b1)
                return 0
            elif self.b2.contains(mouse):
                self.activateButton(self.b2)
                return 10
            elif self.b3.contains(mouse):
                self.activateButton(self.b3)
                return 20
            elif self.b4.contains(mouse):
                self.activateButton(self.b4)
                return 30
            elif self.b5.contains(mouse):
                self.activateButton(self.b5)
                return 40
            elif self.b6.contains(mouse):
                self.activateButton(self.b6)
                return 50
            elif self.b7.contains(mouse):
                self.activateButton(self.b7)
                return 60
            elif self.b8.contains(mouse):
                self.activateButton(self.b8)
                return 70
            elif self.b9.contains(mouse):
                self.activateButton(self.b9)
                return 80
            elif self.b10.contains(mouse):
                self.activateButton(self.b10)
                return 90
            elif self.b11.contains(mouse):
                self.activateButton(self.b11)
                return 100

It waits for a mouse click and check if that click has taken place in one of the b1 to b11 visual.Circle objects, using the contains(mouse) method build in in the visual.Circle class. I believe the problem is in that contains() method and the way it handles the mouse position with a retina display.