使用Python计算径向角度,以顺时针/逆时针方向,给定像素坐标(然后反过来)

9
为了一个我不想深入讨论的背景,我需要两个本质上是相互倒数的函数。
`angle_to()` 函数应返回钟表指针从0°开始旋转到连接 `p1` 和 `p2` 的直线所需的度数(即 `p1` 是旋转中心),其中 `p1` 和 `p2` 均为像素坐标。
`point_pos()` 函数应返回长度为 `amplitude` 的钟表指针旋转 `angle` 后的像素坐标。
对于上述两个函数,正向 x 轴 = 0° = 3 点钟,并且参数 `rotation` 应在计算之前将该轴移动,在顺时针或逆时针方向上移动;然后该计算应沿着此已调整的参考系同向移动。
下面是我的进展; 失败原因如下:
当 `clockwise=False` 时,它返回顺时针条件的正确答案;当`clockwise=True` 时,`angle_between()` 返回正确答案但有四舍五入误差,而 `point_pos()` 完全给了我错误的答案。
我还附加了一个我在Illustrator中设计的视觉解释,以表达我无法解决这个问题并向互联网道歉,同时也希望说明我正在寻找的内容。
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()

编辑#2: 根据要求,这里提供一些失败的输出:

angle_to() 在顺时针和逆时针方向上翻转(当我试图修复它时,最终得到错误答案),在顺时针方向上,旋转和计算方向不同。

>>> 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() 在逆时针方向上是错误的

# 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)

请问您能否解释一下您的代码存在什么问题? - Jacques Gaudin
你能举个例子,说明一个输入和期望的结果吗(例如90度旋转)? - Han-Kwang Nienhuys
@Han-KwangNienhuys 已添加到问题底部。 - Jonline
angle_to() 应该报告一个时钟指针从 p1 到 p2 需要旋转多少度(不是弧度),可以是顺时针或逆时针方向。我意识到关于 y 轴,每个函数都有处理的线条(基本上将值在 0 和 -180° 之间转换为 180 + abs(angle))。但是,在这个混乱中,我仍然存在一些小的逻辑问题,阻止了期望的输出。如果有帮助,我在描述中也进行了澄清。 - Jonline
@Han-KwangNienhuys,图形学中y轴向下绝不是标准的。例如OpenGL使用直立的坐标系。 - Aiman Al-Eryani
显示剩余2条评论
3个回答

7

你可以通过遵循一些简单规则来简化代码。简单的代码更少容易出现错误。

首先,将顺时针和逆时针转换只需要改变符号: angle = -angle

其次,将角度限制在范围[0, 360)内,只需使用angle % 360即可。无论角度最初是负数还是正数、整数还是浮点数,此方法都适用。

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

但这并不意味着简单地颠倒符号;顺时针旋转45°实际上是从同一位置开始逆时针旋转315°。它最小为(360-angle)%360 - Jonline
1
这就是为什么我告诉你要两个都做。(360 - angle) % 360-angle % 360 是相同的。 - Mark Ransom
...并且Jono越过终点线。谢谢。 - Jonline

1

关于“angle_to()应该返回一个指针从p1到p2旋转所需的角度数”的问题:

在您的代码中,您在使用atan2计算角度之前从点p1中减去了点p2的坐标。本质上,您认为p1是您时钟的中心,因此谈论“通过旋转从p1到p2行进”没有任何意义。您需要指定三个点:您进行旋转的中心点、点1和点2。如果坐标是xc、yc、x1、y1、x2、y2,则需要执行以下操作:

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

使用您的新规格更新:“返回时钟指针从0度旋转到连接p1和p2的线路所需旋转的度数”:

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

这将返回范围在-pi到+pi(-180到+180度)之间的顺时针角度(以像素坐标表示)。在您的示例中,angle_to((100,100), (25,25))("想要225,但得到135"),atan2将导致-135度,这意味着+135度逆时针旋转。这是您想要的答案(模360度),因为您没有指定时钟指针应该顺时针还是逆时针旋转(您只指定起始位置相对于3点钟位置是顺时针还是逆时针)。但是,根据默认为False的clockwise的值,您会执行一些复杂的操作。
如果您想确保时钟指针顺时针旋转,则应在结果角度为负时将360度添加到结果角度,而不是恢复角度。
(注意:我删除了旧答案;前两个评论是关于旧答案的。)

确认;清理仍然无法工作的代码。 - Jonline
1
不应该有关系,因为你只是将两个坐标乘以一个常数因子(他/她本质上是在执行缩放操作)。 - Aiman Al-Eryani
叹气。你说得完全正确,但那是我描述中的错误,我现在会更改它;它应该是“如果p1是时钟的中心,并且当前指向0度,则度数指针将转到达到p2”。 - Jonline

1
这是原文: angle = (360 - angle if angle > 0 else -1 * angle) - rotation 我不知道你想要达到什么目的,但这确实不能实现你想要的效果。只有 -angle 反转了角度;改变了角度方向,从逆时针变为顺时针,注意你在条件的逆时针分支中。然后你加上 360,这搞砸了一切。else 分支只是将角度乘以 -1——再次反转它。顺时针分支是你需要反转角度的地方(并添加 360 以确保角度是正的)。
这里是一个简单版本的函数,没有额外的旋转参数进行修复:
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

你的另一个函数在这些行中存在完全相同的问题:
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

应该是:

应该是:

angle -= rotation
if clockwise:
    angle = -angle+360 if angle > 0 else -angle
else:
    angle = angle if angle > 0 else angle + 360

好的,这几乎解决了所有问题,除了参数“rotation”,在原地实际上是至关重要的(我正在为认知心理学的实验开发,有时需要满足奇怪的要求)。你观察到的一些不清楚的行——毫无疑问它们确实如此,我已经超出了我的能力范围——旨在调整为在计算之前将0°轴旋转“rotation”度。假设解决方案对两者都适用,您能否评论应该如何合并? - Jonline
尽管...如果确实返回了正确的角度,我想无论是哪个方向angle -= rotation都可以。但是,由于我总是希望返回一个正角度,所以我需要额外的return angle if angle > 0 else 360 + angle,这样正确吗? - Jonline
没错。或者,你可以将 return 语句后面的内容用括号括起来,并像 @MarkRansom 建议的那样使用 360 作为操作符 % 的参数。这样可以确保你始终获得范围在 [0, 360[ 内的正角度。 - Aiman Al-Eryani

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接