Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using python to calculate radial angle, in clockwise/counterclockwise directions, given pixel coordinates (and then vice-versa)

For a context I won't get into, I need two functions that are essentially reciprocals of each other.

angle_to() should return the number of degrees a clockhand would have to turn to travel from 0° to the line connecting p1 to p2 (ie. p1 is the center of rotation), and where both p1 and p2 are pixel coordinates.

point_pos() should return the pixel coordinates of where a clockhand of length amplitude would be had it turned angle.

For both, the positive x-axis = 0° = 3 o'clock, and the argument rotation should shift that axis before the calculation begins in either the clockwise or counter-clockwise direction; then said calculation should move in the same direction with this adjusted reference.

My progress on each is included below; the failure is:

When clockwise=False, it returns the correct answer for the clockwise condition; when clockwise=True, angle_between() returns the right answer with a rounding error, and point_pos() gives me the wrong answer entirely.

I've also attached a visual explanation I mocked up in Illustrator as an apology to the internet for being unable to solve this and in case what I'm seeking isn't clear.

Edit: cleaned up a line that unnecessarily complicated as per one answer below.

from math import sin, cos, radians, pi, atan2, degrees

def angle_to(p1, p2, rotation=0, clockwise=False):
    if abs(rotation) > 360:
        rotation %= 360
    p2 = list(p2)
    p2[0] = p2[0] - p1[0]
    p2[1] = p2[1] - p1[1]

    angle = degrees(atan2(p2[1], p2[0]))
    if clockwise:
        angle -= rotation
        return angle if angle > 0 else angle + 360
    else:
        angle = (360 - angle if angle > 0 else -1 * angle) - rotation
        return angle if angle > 0 else angle + 360

def point_pos(origin, amplitude, angle, rotation=0, clockwise=False):
    if abs(rotation) > 360:
        rotation %= 360
    if clockwise:
        rotation *= -1
    if clockwise:
        angle -= rotation
        angle = angle if angle > 0 else angle + 360
    else:
        angle = (360 - angle if angle > 0 else -1 * angle) - rotation
        angle = angle if angle > 0 else angle + 360

    theta_rad = radians(angle)
    return int(origin[0] + amplitude * cos(theta_rad)), int(origin[1] + amplitude * sin(theta_rad))

angle_to() point_pos()

Edit #2: Upon request, here's some failed output:

angle_to() is flipping clockwise and counterclockwise (when I've tried to fix it, I end up getting wrong answers altogether), and in the clockwise direction, rotating and calculating in different directions

>>> print angle_to((100,100), (25,25))  # should be 225  
135.0
>>> print angle_to((100,100), (25,25), 45)  # should be 180
90.0
>>> print angle_to((100,100), (25,25), clockwise=True) # should be 135
225.0
>>> print angle_to((100,100), (25,25), 45, clockwise=True)  # should be 90
180.0

point_pos() is just wrong in the counterclockwise direction

# dunno what these should be (i'm bad at trig) but when I visually place the
# given p1 and the output p2 on screen it's obvious that they're wrong
>>> print point_pos((100,100), 75, 225)               
(46, 153)
>>> print point_pos((100,100), 75, 225, 45)
(100, 175)

# these are basically correct, rounding-errors notwithstanding
>>> print point_pos((100,100), 75, 225, clockwise=True)
(46, 46)
>>> print point_pos((100,100), 75, 225, 45, clockwise=True)
(99, 25)
like image 350
Jonline Avatar asked May 16 '16 17:05

Jonline


3 Answers

You can simplify your code quite a bit by using a couple of simple rules. Simple code is less likely to have bugs.

First, converting between clockwise and counter-clockwise just means inverting the sign: angle = -angle.

Second, to restrict an angle to the range [0, 360) you simply use angle % 360. This works no matter if the angle started out negative or positive, integer or floating point.

def angle_to(p1, p2, rotation=0, clockwise=False):
    angle = degrees(atan2(p2[1] - p1[1], p2[0] - p1[0])) - rotation
    if not clockwise:
        angle = -angle
    return angle % 360
like image 187
Mark Ransom Avatar answered Oct 23 '22 23:10

Mark Ransom


This: angle = (360 - angle if angle > 0 else -1 * angle) - rotation I don't know what you were trying to achieve there, but that indeed does not do what you want. Just having -angle reflects the angle; changes the angle direction, from anti-clockwise to clockwise, noting that you're in the counter-clockwise branch of the condition. Then you add 360, and that messes everything up. The else branch just multiplies the angle by -1 - reversing it again. The clockwise branch is where you needed to reverse the angle (and add 360 to ensure the angle is positive). Here is a simple version of your function fixed without the extra rotation parameter:

def angle_to(p1, p2, clockwise=False):
    p2 = list(p2)
    p2[0] = p2[0] - p1[0]
    p2[1] = (p2[1] - p1[1])
    angle = degrees(atan2(p2[1], p2[0]))
    angle = 360 + angle if angle < 0 else angle
    return angle if not clockwise else -angle+360

Your other function suffers from exactly the same problem in these lines:

if clockwise:
    angle -= rotation
    angle = angle if angle > 0 else angle + 360
else:
    angle = (360 - angle if angle > 0 else -1 * angle) - rotation
    angle = angle if angle > 0 else angle + 360

Should be:

angle -= rotation
if clockwise:
    angle = -angle+360 if angle > 0 else -angle
else:
    angle = angle if angle > 0 else angle + 360
like image 38
Aiman Al-Eryani Avatar answered Oct 24 '22 01:10

Aiman Al-Eryani


Re: "angle_to() should return the number of degrees a clockhand would have to turn to travel from p1 to p2"

In your code, you subtract the coordinates of point p1 from p2 before you calculate the angle using atan2. Essentially, you're considering p1 to be the center of your clock, so it doesn't make any sense to talk about "travelling from p1 to p2 by a rotation". You'll need to specify three points: the center around which you do the rotations, point 1, and point 2. If the coordinates are xc, yc, x1, y1, x2, y2, then you'd need to do something like this:

angle1 = atan2(y1-yc, x1-xc)
angle2 = atan2(y2-yc, x2-xc)
relative_angle = angle1 - angle2
# now convert to degrees and handle +/-360 issues.

Update with your new specification: "return the number of degrees a clockhand would have to turn to travel from 0° to the line connecting p1 to p2":

angle = degrees(atan2(p2[1], p2[0]))

This will return the clockwise angle (in pixel coordinates) in the range -pi to +pi (-180 to +180 deg). In your example, angle_to((100,100), (25,25)) ("want 225, but get 135"), the atan2 will result in -135 deg, which means +135 deg counterclockwise. That is the answer that you would want (modulo 360 degrees), since you have not specified whether the clock hand should be turning cw or ccw (you only specify whether the starting position is cw or ccw relative to the 3 o'clock position). However, depending on the value of clockwise, which defaults to False, you do something complicated.

If you want to ensure that the clock hand turns cw, then you should add 360 deg to the result angle if it is negative, not revert the angle.

(Note: I deleted the old answer; the first two comments refer to the old answer.)

like image 41
Han-Kwang Nienhuys Avatar answered Oct 24 '22 00:10

Han-Kwang Nienhuys