Moving object does not stop at the assigned pixel location

OS (e.g. Win10): Win10
PsychoPy version (e.g. 1.84.x): 2022.1.4
**Standard Standalone? yes
What are you trying to achieve?:
The goal of the study is to have participants watch an object move across the screen (it should stop at a specified location). The trial will then end and participants will be asked to mark the location that they think the object stopped. I have an object (it is an image stimulus not a polygon) that moves across the screen. I have a set starting location in pixels. In the image stimulus’ position section I have an equation that pulls different variables (velocity and the starting point in pixels) from an excel sheet to set the x-axis position based on ‘t’ or time (see below).

[((t * Poly_Velocity) + Img_StartPosX), 0] 

This equation updates the object’s location every frame.

I also have a code-component in place to stop the image at a specific x-axis location. I have 6 unique stop points (endpoints). In the same code component I had psychopy take the position in pixels of the image when the status of the image is “stopped” and put it into my data file. This allowed me to compare any differences in where the image stopped according to PsychoPy and where I originally told the image to stop. I am trying to be as precise as possible, so ideally there shouldn’t be a difference between the endpoint I set for the image and the x-axis coordinate that PsychoPy gives me in my data. When I look at the data I see that there is somewhere between 0.3 and approximately 2 pixels difference between my set endpoint and where PsychoPy says the image stopped.

In each frame

if Move == "Left" and Endpoint==1:
    if Picture.pos[0] <= -349:
        Picture.status = STOPPED
elif Move == "Right" and Endpoint==1:
    if Picture.pos[0] >= 349:
        Picture.status = STOPPED
elif Move == "Left" and Endpoint==2:
    if Picture.pos[0] <= -351:
        Picture.status = STOPPED
elif Move == "Right" and Endpoint==2:
    if Picture.pos[0] >= 351:
        Picture.status = STOPPED
elif Move == "Left" and Endpoint==3:
    if Picture.pos[0] <= -353:
        Picture.status = STOPPED
elif Move == "Right" and Endpoint==3:
    if Picture.pos[0] >= 353:
        Picture.status = STOPPED
if Picture.status==STOPPED:
    continueRoutine=False and Picture.pos

End routine

thisExp.addData("ImageEPs", Picture.pos)

The code and everything works correctly (aka the object stops, the trial ends and the data is recorded), I just don’t know if the data is accurate or what the true endpoint of the object is.

Here are my questions:
Is this because the program needs time to stop the object and then record the object’s location? Meaning the object kept moving as the program took time to register the information and then enact my code? Does this mean that the output PsychoPy gives me in my data file is the exact location that the object stopped?

Or did the program stop the object exactly where I set the end point, but because the program needs time to work its magic the location that PsychoPy indicates is based on the slightly delayed command for pulling the endpoint? (also possibly caused by my use of an equation to create the movement) Does this mean that the location that PsychoPy gives me in the data file is incorrect?

Lastly, is there a way to be more precise? For instance is there anything I can do that makes sure that the image truly does stop at the exact pixel value I want it to?

If there are solutions that involve different building or coding methods than what I have used I am open to anything. Thank you!

Best,
Genna

We probably need to see a table of such values. But is there any guarantee that your formula for calculating the position as a function of time would in fact ever coincide precisely with the pre-specified positions?

e.g. it doesn’t get rounded to ensure that the output is an integer. Also t varies discretely in steps between refreshes, with increments depending on the refresh rate of your monitor (e.g. by 0.01666 s on a 60 Hz monitor). That alone makes it unlikely that successively calculated values will align neatly with an arbitrary integer pixel location. Some tolerance will be necessary. We don’t know the relevant parameters (screen refresh rate and Poly_Velocity) but you should really tabulate a range of outputs across successive values of t (even just in a spreadsheet) and see how they correspond to your pre-specified values. Depending on the parameters, given the small range of target values (only 4 pixels), it’s quite possible that some will be skipped entirely (e.g. successive values could increment by more than two pixels), regardless of the non-integer issue.

They key thing here is to realise that PsychoPy’s event loop (i.e. when it updates stimuli and checks for responses) is rate-limited by the refresh frequency of your monitor (say, 60 Hz). There is no point in updating stimulus characteristics if they won’t ever be displayed, so the updates occur discretely rather than continuously, just once per screen refresh. So the position values you generate are not a continuous mathematical function, guaranteed to intercept every pixel value. Rather, they are a discrete sampling from that function, and will quite possibly skip entire pixel values, depending on the parameters.

Thank you for your response. I’ve added a copy of the data file I get from this experiment. The “EpPix” column is where I have my set x-axis endpoint and “ImageEPs” is the column where psychopy outputs the location of the image when its status is “stopped”.

Endpoint StopCode_Img EpPix Poly_Velocity Poly_Duration Img_StartPosX Loop.thisRepN Loop.thisTrialN Loop.thisN Loop.thisIndex Picture.started ImageEPs session psychopyVersion frameRate
1 Picture.pos[0] <= [-349] -349 -196.3515 4.990666667 640 0 0 0 0 46.6961419 [-350.68154215 -100. ] 1 2022.1.4 59.96703013
2 Picture.pos[0] >= [351] 351 196.3515 5 -640 0 1 1 1 54.7298984 [ 351.10722043 -100. ] 1 2022.1.4 59.96703013
3 Picture.pos[0] <= [-353] -353 -196.3515 5.009333333 640 0 2 2 2 62.7640266 [-353.40349231 -100. ] 1 2022.1.4 59.96703013
1 Picture.pos[0] >= [349] 349 196.3515 4.990666667 -640 0 3 3 3 70.8140654 [ 350.06677346 -100. ] 1 2022.1.4 59.96703013
2 Picture.pos[0] >= [351] 351 196.3515 5 -640 0 4 4 4 78.8476136 [ 353.48780565 -100. ] 1 2022.1.4 59.96703013
3 Picture.pos[0] >= [353] 353 196.3515 5.009333333 -640 0 5 5 5 86.8977876 [ 353.49677892 -100. ] 1 2022.1.4 59.96703013
1 Picture.pos[0] <= [-349] -349 -196.3515 4.990666667 640 0 6 6 6 94.9472667 [-350.32362086 -100. ] 1 2022.1.4 59.96703013
2 Picture.pos[0] <= [-351] -351 -196.3515 5 640 0 7 7 7 102.9815194 [-353.4274472 -100. ] 1 2022.1.4 59.96703013
3 Picture.pos[0] >= [353] 353 196.3515 5.009333333 -640 0 8 8 8 111.031539 [ 353.71378659 -100. ] 1 2022.1.4 59.96703013
1 Picture.pos[0] >= [349] 349 196.3515 4.990666667 -640 0 9 9 9 119.0818293 [ 350.04568531 -100. ] 1 2022.1.4 59.96703013
2 Picture.pos[0] >= [351] 351 196.3515 5 -640 0 10 10 10 127.1147482 [ 354.26472927 -100. ] 1 2022.1.4 59.96703013
3 Picture.pos[0] <= [-353] -353 -196.3515 5.009333333 640 0 11 11 11 135.165302 [-353.82056254 -100. ] 1 2022.1.4 59.96703013

I don’t think that my formula coincides exactly with the endpoint I want. There is probably some rounding error since my calculations dealt with a lot of decimals. I’ll have to find some way to fix that. I chose the velocity (196.3515px/sec) because it aligns with the previous literature. And the same goes for why I chose endpoints that only differ by 2-4 pixels.
Am I right to assume that the information you provided generally means that I won’t be able to tell psychopy to stop my image at an exact pixel location and the program actually do that?
With that, is there a way for me to get the exact pixel value at which my image stopped moving? This way I can at least have record of the true endpoint, whether I set it or psychopy tells me after the fact. The last paragraph of your response leads me to believe that I won’t be able to, but I just wanted to double check.

I think this is a case where the script is doing exactly what you tell it to, but you’re perhaps just not sure what that is. With example values you’ve provided (Poly_Velocity = -196.3515 and Img_StartPosX = 640), we can tabulate the x position of your stimulus, counting from the zeroth (i.e. first) frame onwards:

frame	 t	x	     round(x)  diff
0	0.0000	640.0000	640	
1	0.0167	636.7275	637	    -3
2	0.0333	633.4550	633	    -4
3	0.0500	630.1824	630	    -3
4	0.0667	626.9099	627	    -3
…
86	1.4333	358.5629	359	    -3
87	1.4500	355.2903	355	    -4
88	1.4667	352.0178	352	    -3
89	1.4833	348.7453	349	    -3
90	1.5000	345.4728	345	    -4
91	1.5167	342.2002	342	    -3

This is assuming a monitor with a refresh rate of 60 Hz, and hence 0.0167 seconds between successive stimulus updates.

Notice what happens when x gets into the range you are interested in (pixel values of 349, 351, 353).

  • The stimulus is actually jumping by 3.275 pixels between each screen refresh, so concerning yourself with pixel target values that are separated by only 2 pixels is not practical, as that is less than the smallest jump the stimulus makes on its successive appearances.
  • Pixels in the real world are discrete physical objects and can be counted (i.e. they should be addressed by integer values). Now I don’t know what PsychoPy does when you provide a non-integer value to a stimulus location. A reasonable guess is that it effectively gets rounded so that the stimulus appears at the closest discrete pixel to the continuous value you provided. e.g. the x value of 348.7453 above (on frame 89) would appear at pixel 349, but the stimulus will never appear at either pixel 351 or 353, because none of your generated values round to one of those integer values.

Now I really don’t know what your task is about and what the goal is, but perhaps you need to take a step back and think about the level of precision involved and exactly what you are trying to achieve. You are currently doing the right thing in your comparisons by using <= or >= instead of ==, because you can’t reliably compare floating point values for equality (that only works for integers, which computers can store exactly).

Effectively your task (at 60 Hz at least) is stepping the target by either 3 or 4 pixels per cycle (to produce an average step value of 3.2725 pixels per frame). Any calculations you do have to reflect that level of granularity in the animation. If you want more precision, run a faster monitor (e.g. gaming LCDs these days routinely run at 144 Hz) and/or one with much higher pixel resolution (say 4K). The combination of high refresh rate and high pixel density implies the need for a pretty good graphics card to handle the load. Otherwise if that level of precision is not actually required, just continue as you are but incorporate the actual level of granularity in your code’s logic.

You can certainly do that - but you just need to be aware of the level of precision in your animation and the actual pixel values involved. At the simplest level, you can just round() the output of your pixel calculation to have no decimal places, forcing it to be an integer:

round((t * Poly_Velocity) + Img_StartPosX))

and as above, perhaps tabulate the output of your function in a spreadsheet beforehand so you know which actual pixel locations it will land on, so that you don’t test against a pixel location that it actually skips over.

Thank you so much for your help! I didn’t have a great grasp on what happens behind the scenes in Psychopy so this makes it much clearer.