Cursor jitter on mouse movement only

I’m trying to make an experiment where a cursor moves when the mouse moves. In addition, there’s some jitter added to the movement, but only when the mouse is moved. I can’t get this to work. Here’s what I’ve tried, and a few bumps along the way:

Simple version: just mouse movement

This works, but changing the sensitivity (/ 2)of the mouse causes the cursor to stop moving beyond 25% from the center-point of the screen. It’s as if it’s captured in a box.

import numpy as np
from psychopy import visual, event

win = visual.Window(fullscr=True)
cursor = visual.Circle(win, radius=0.02)
mouse = event.Mouse(visible=False)

# Loop until mouse press
while not mouse.getPressed()[0]:
    # Do something if mouse moved
    move = mouse.getRel()
    if np.any(move):
        cursor.pos += move / 2  # change sensitivity
        #cursor.pos += np.random.randn(2)*0.01  # random jitter
        #mouse.setPos(cursor.pos)  # move outside "box"
        #mouse.lastPos = cursor.pos  # do not count setPos as movement
        
    cursor.draw()
    win.flip()

Moving the cursor beyond the “box”

Try outcommenting the line

mouse.setPos(cursor.pos)

to make the system mouse “follow” the cursor object, so that it should be able to move to the physical screen edge. Now I see a weird behavior where the mouse is “dragged” towards the center, so mouse.getRel() captures this position setting as a movement. To fix that, outcomment

#mouse.lastPos = cursor.pos

It works! Yay!

Movement jitter on physical mouse movement

Outcomment the last line to add jitter to the cursor position, and thereby also to the mouse position:

cursor.pos += np.random.randn(2)*0.01

The problem is that it jitters even though the mouse is still. And sure enough, mouse.getRel() returns non-zero movement as does replacing it with mouse.getPos() - cursor.pos. I’ve also tried replacing np.any(move) with mouse.mouseMoved() but no dice.

Is there a way to get the desired behavior?

I can get it to work, but its odd

i used pixels not visual ang units postualting that visual angle will be tiny but non zero.

I also put some print statements in which shoe mouse.Moved not working (BTW im on 1.82 here) …

    import numpy as np
    from psychopy import visual, event

    win = visual.Window(fullscr=True, units='pix')
    cursor = visual.Circle(win, radius=20)
    mouse = event.Mouse(visible=True)

    # Loop until mouse press
    while not mouse.getPressed()[0]:
        # Do something if mouse moved
        move = mouse.getRel()
        print move, mouse.mouseMoved(), np.any(move)
        if np.any(move):
            cursor.pos += move / 2  # change sensitivity
            cursor.pos += np.random.randn(2)* 2  # random jitter
            mouse.setPos(cursor.pos)  # move outside "box"
            mouse.lastPos = cursor.pos  # do not count setPos as movement
        cursor.draw()
        win.flip()

Thanks for looking into this. It still jitters continuously here, also when I don’t move the mouse. I.e. mouse.getRel() does not return (0, 0) but instead (for the first few frames, using your code):

[-960 -539] False True
[-0.61675708 -0.22463241] False True
[-0.02648788 -0.24274459] False True
[-0.12286803 -0.77669327] False True
[-0.77039584 -0.41315915] False True

These movements are smaller than the jitter by a factor around 3-5. So it seems like there’s some jitter-induced “residual noise”! Is the cursor still on your computer when you don’t move the mouse?

the cursor went still, i’ll check when i get on my work machine

have you tried a different mouse, moved it onto a new surface?

i couldn’t get it to work when i starting messing again.
There clearly is something odd going on cos this code does what you want (i think) by doing it manually !

import numpy as np
from psychopy import visual, event

win = visual.Window(fullscr=True, units='pix')
cursor = visual.Circle(win, radius=20)
mouse = event.Mouse(visible=True)

 # Loop until mouse press
mold=mouse.getPos()
while not mouse.getPressed()[0]:
    mnew = mouse.getPos()
    if (mold != mnew).all():
        cursor.pos = mnew / 2  # change sensitivity
        cursor.pos += np.random.randn(2)* 6  # random jitter
        mold=mouse.getPos()
    cursor.draw()
    win.flip()

Hi, I hope this can help, it somehow works (the if statement can be, of course, improved), I was not able to get the sensitivity (/ 2) working.
Cheers
Emanuele

import numpy as np
from psychopy import visual, event

win = visual.Window(fullscr=False)
cursor = visual.Circle(win, radius=0.02)
mouse = event.Mouse(visible=False)

old_pos = np.array([0,0])
jitter = np.array([0,0])
mouse.setPos([0,0])

while not mouse.getPressed()[0]:
    # Do something if mouse moved
    move = mouse.getPos()
    if abs(move[0]-old_pos[0]) > abs(jitter[0]):
        old_pos = move
        jitter = np.random.randn(2)*0.01
        cursor.setPos(move + jitter)
        mouse.setPos(move + jitter)  # move outside "box"
    cursor.draw()
    win.flip()

I’ve narrowed down the bug somewhat. The problem is that mouse.setPos(new_coordinate) does not result in mouse.getPos() to return exactly new_coordinate but rather something quite close. Therefore, mouse.getRel() detects this as a movement. The problem is in GL, so it’s out of reach for PsychoPy.

Solution #1

The solution is to only call mouse.setPos when the system cursor reaches the screen edge instead of doing it every frame. In my case, the code is

if any(abs(mouse.getPos()*1.1) >= 1):  # if system mouse is outside screen border
    mouse.setPos(cursor.pos)  # to allow for continued movement even though a regular cursor would have exited the monitor edges
    core.wait(0.008)  # this solves a weird mouse problem, where it "jumps" randomly on mouse.setPos.
    mouse.lastPos = cursor.pos  # Needed to not count the above move as an acceleration.            

The if-statement is a bit more convoluted if you work in different window units. For instance, I do it in degrees visual angle, so it’s:

if any(abs(mouse.getPos()*1.1) >= pix2deg(np.array(MON_SIZE)/2, my_monitor)):  # if system mouse is outside screen border

… where MON_SIZE=[1440, 2560] on my system (monitor size in pixels) and my_monitor is an psychopy.monitors.Monitor object. pix2deg is psychopy.misc.monitortools.pix2deg.

Solution #2

A different is to specify a minimum movement distance to seperate physical mouse movements from system-noise-movement. In my original example, outcomment all code and then substitute

if np.any(move):

with

if np.any(np.abs(move) >= 0.005):  

… or whatever criterion you find appropriate. For units=‘pix’, 1 is a good threshold. FWIW it seems like the signal-to-noise ratio seems higher for units='pix' than for units='norm', i.e. the threshold does better in distinguishing physical mouse movement from noise. I haven’t tested it much.

Other event.Mouse bugs

Thanks a bunch @rob.stone and @porcu.emanuele for your code examples. This made the debugging a lot easier. In debugging this, I discovered other bugs in the event.Mouse class. I will submit fixes to GitHub. Until then, here they are for completeness:

  • Mouse.setPos(x) where x is not the current mouse position does not result in mouse.getPos() in returning x but something rather close. This is a deterministic behavior (it’s repeatable) but with no obvious mechanism. For example, the error seems to be somewhat proportional to the amount of mouse displacement, but not exactly. There also seems to be a minimum movement below which there is no inaccuracy.
  • mouse.mouseMoved() will only run if mouse.lastPos is set and that is only set if mouse.getPos() has been called. This should not be necessary.
  • mouse.mouseMoved(reset=False) still resets the position so that a subsequent mouse.getRel() will return [0,0] even on movement.
  • Perhaps not a bug, but surprising behavior to me: mouse.setPos(x) counts as movement, i.e. detected by mouse.getRel() and mouse.mouseMoved(). If this is intended, there should be an argument like mouse.setPos(x, as_movement=True)
2 Likes