Углы между двумя n-мерными векторами в Python


82

Мне нужно определить угол (ы) между двумя n-мерными векторами в Python. Например, входными данными могут быть два списка, например: [1,2,3,4]и [6,7,8,9].


1
Это лучший ответ - @ MK83, поскольку это в точности математическое выражение theta = atan2 (u ^ v, uv). покрывается даже случай, когда u = [0 0] или v = [0 0], потому что это только время, когда atan2 выдаст NaN в других ответах. NaN будет произведено с помощью / norm (u) или / norm (v)
PilouPili 01

Ответы:


66
import math

def dotproduct(v1, v2):
  return sum((a*b) for a, b in zip(v1, v2))

def length(v):
  return math.sqrt(dotproduct(v, v))

def angle(v1, v2):
  return math.acos(dotproduct(v1, v2) / (length(v1) * length(v2)))

Примечание : это не сработает, если векторы имеют одинаковое или противоположное направление. Правильная реализация здесь: https://stackoverflow.com/a/13849249/71522


2
Кроме того, если вам нужны только cos, sin, tan угла, а не сам угол, вы можете пропустить math.acos, чтобы получить косинус, и использовать перекрестное произведение, чтобы получить синус.
mbeckish

10
Учитывая, что math.sqrt(x)это эквивалентно x**0.5и math.pow(x,y)эквивалентно x**y, я удивлен, что они пережили топор избыточности, использованный во время перехода Python 2.x-> 3.0. На практике я обычно выполняю такие числовые операции как часть более крупного вычислительно-интенсивного процесса, и поддержка интерпретатором «**», идущего непосредственно к байт-коду BINARY_POWER, вместо поиска «математики», доступа его атрибуту sqrt, а затем мучительно медленному байт-коду CALL_FUNCTION, можно ощутимо улучшить скорость без затрат на кодирование или читаемость.
PaulMcG

5
Как и в ответе с numpy: это может не сработать, если возникнет ошибка округления! Это может происходить как с параллельными, так и с антипараллельными векторами!
BandGap

2
Примечание: это не удастся, если векторы идентичны (например, angle((1., 1., 1.), (1., 1., 1.))). См. Мой ответ для чуть более правильной версии.
Дэвид Волевер,

2
Если вы говорите о реализации выше, то она не работает из-за ошибок округления, а не из-за параллельности векторов.
Pace

153

Примечание : все другие ответы здесь не получится, если два вектора имеют либо в одном направлении (например, (1, 0, 0), (1, 0, 0)) или в противоположных направлениях (например, (-1, 0, 0), (1, 0, 0)).

Вот функция, которая будет правильно обрабатывать эти случаи:

import numpy as np

def unit_vector(vector):
    """ Returns the unit vector of the vector.  """
    return vector / np.linalg.norm(vector)

def angle_between(v1, v2):
    """ Returns the angle in radians between vectors 'v1' and 'v2'::

            >>> angle_between((1, 0, 0), (0, 1, 0))
            1.5707963267948966
            >>> angle_between((1, 0, 0), (1, 0, 0))
            0.0
            >>> angle_between((1, 0, 0), (-1, 0, 0))
            3.141592653589793
    """
    v1_u = unit_vector(v1)
    v2_u = unit_vector(v2)
    return np.arccos(np.clip(np.dot(v1_u, v2_u), -1.0, 1.0))

Не лучше ли использовать np.isnanвместо математической библиотеки? Теоретически они должны быть идентичными, но на практике я не совсем уверен. В любом случае, я полагаю, это будет безопаснее.
Зацепил

2
Мой numpy (версия == 1.12.1) можно использовать arccosнапрямую и безопасно. : In [140]: np.arccos (np.dot (np.array ([1,0,0]), np.array ([- 1,0,0]))) Out [140]: 3.1415926535897931 In [ 141]: np.arccos (np.dot (np.array ([1,0,0]), np.array ([1,0,0]))) Out [141]: 0.0
ene

2
Особый случай, когда хотя бы один входной вектор является нулевым вектором, опускается, что проблематично для деления на unit_vector. Одна из возможностей - просто вернуть входной вектор в этой функции, когда это так.
кафман

1
angle_between ((0, 0, 0), (0, 1, 0)) в качестве результата даст nan, а не 90
FabioSpaghetti 04

2
Угол @kafman 0-векторов не определен (по математике). Так что то, что это вызывает ошибку, - это хорошо.
пользователь

45

Используя numpy (настоятельно рекомендуется), вы должны:

from numpy import (array, dot, arccos, clip)
from numpy.linalg import norm

u = array([1.,2,3,4])
v = ...
c = dot(u,v)/norm(u)/norm(v) # -> cosine of the angle
angle = arccos(clip(c, -1, 1)) # if you really want the angle

3
Последняя строка может привести к ошибке, как я выяснил, из-за ошибок округления. Таким образом, если вы поставите точку (u, u) / norm (u) ** 2, это приведет к 1.0000000002, и arccos затем не удастся (также
``

Я тестировал с u = [1,1,1]. u = [1,1,1,1] работает нормально, но каждое добавленное измерение возвращает немного большие или меньшие значения, чем 1 ...
BandGap

3
Примечание: это не сработает (даст результат nan), если направление двух векторов одинаковое или противоположное. См. Мой ответ для более правильной версии.
Дэвид Уолевер,

2
добавляя к этому комментарий neo, последняя строка должна быть angle = arccos(clip(c, -1, 1))во избежание проблем с округлением. Это решает проблему @DavidWolever.
Тим Тисдалл

4
Для людей, использующих приведенный выше фрагмент кода: clipследует добавить в список импорта numpy.
Лиам Дикон

27

Другой вариант - использовать просто, numpyи он дает вам внутренний угол

import numpy as np

p0 = [3.5, 6.7]
p1 = [7.9, 8.4]
p2 = [10.8, 4.8]

''' 
compute angle (in degrees) for p0p1p2 corner
Inputs:
    p0,p1,p2 - points in the form of [x,y]
'''

v0 = np.array(p0) - np.array(p1)
v1 = np.array(p2) - np.array(p1)

angle = np.math.atan2(np.linalg.det([v0,v1]),np.dot(v0,v1))
print np.degrees(angle)

и вот результат:

In [2]: p0, p1, p2 = [3.5, 6.7], [7.9, 8.4], [10.8, 4.8]

In [3]: v0 = np.array(p0) - np.array(p1)

In [4]: v1 = np.array(p2) - np.array(p1)

In [5]: v0
Out[5]: array([-4.4, -1.7])

In [6]: v1
Out[6]: array([ 2.9, -3.6])

In [7]: angle = np.math.atan2(np.linalg.det([v0,v1]),np.dot(v0,v1))

In [8]: angle
Out[8]: 1.8802197318858924

In [9]: np.degrees(angle)
Out[9]: 107.72865519428085

6
Это лучший ответ, так как это в точности математическое выражение theta = atan2 (u ^ v, uv). И это никогда не подводит!
PilouPili 01

1
Это для 2-D. OP просил nD
Normanius

3

Если вы работаете с 3D-векторами, вы можете сделать это кратко, используя toolbelt vg . Это легкий слой поверх numpy.

import numpy as np
import vg

vec1 = np.array([1, 2, 3])
vec2 = np.array([7, 8, 9])

vg.angle(vec1, vec2)

Вы также можете указать угол обзора для вычисления угла через проекцию:

vg.angle(vec1, vec2, look=vg.basis.z)

Или вычислите знаковый угол через проекцию:

vg.signed_angle(vec1, vec2, look=vg.basis.z)

Я создал библиотеку при моем последнем запуске, где она была мотивирована таким использованием: простые идеи, которые многословны или непрозрачны в NumPy.


3

Решение Дэвида Волевера хорошее, но

Если вы хотите иметь подписанные углы, вы должны определить, является ли данная пара правой или левой (см. Вики для получения дополнительной информации).

Мое решение для этого:

def unit_vector(vector):
    """ Returns the unit vector of the vector"""
    return vector / np.linalg.norm(vector)

def angle(vector1, vector2):
    """ Returns the angle in radians between given vectors"""
    v1_u = unit_vector(vector1)
    v2_u = unit_vector(vector2)
    minor = np.linalg.det(
        np.stack((v1_u[-2:], v2_u[-2:]))
    )
    if minor == 0:
        raise NotImplementedError('Too odd vectors =(')
    return np.sign(minor) * np.arccos(np.clip(np.dot(v1_u, v2_u), -1.0, 1.0))

Из-за этого он не идеален, NotImplementedErrorно в моем случае работает хорошо. Такое поведение можно исправить (поскольку для каждой пары определяется удобство работы), но для этого требуется больше кода, который я хочу и должен написать.


2

Простой способ найти угол между двумя векторами (работает для n-мерного вектора),

Код Python:

import numpy as np

vector1 = [1,0,0]
vector2 = [0,1,0]

unit_vector1 = vector1 / np.linalg.norm(vector1)
unit_vector2 = vector2 / np.linalg.norm(vector2)

dot_product = np.dot(unit_vector1, unit_vector2)

angle = np.arccos(dot_product) #angle in radian

1

Основываясь на отличном ответе sgt pepper и добавляя поддержку выровненных векторов, а также добавляя ускорение более чем в 2 раза с помощью Numba

@njit(cache=True, nogil=True)
def angle(vector1, vector2):
    """ Returns the angle in radians between given vectors"""
    v1_u = unit_vector(vector1)
    v2_u = unit_vector(vector2)
    minor = np.linalg.det(
        np.stack((v1_u[-2:], v2_u[-2:]))
    )
    if minor == 0:
        sign = 1
    else:
        sign = -np.sign(minor)
    dot_p = np.dot(v1_u, v2_u)
    dot_p = min(max(dot_p, -1.0), 1.0)
    return sign * np.arccos(dot_p)

@njit(cache=True, nogil=True)
def unit_vector(vector):
    """ Returns the unit vector of the vector.  """
    return vector / np.linalg.norm(vector)

def test_angle():
    def npf(x):
        return np.array(x, dtype=float)
    assert np.isclose(angle(npf((1, 1)), npf((1,  0))),  pi / 4)
    assert np.isclose(angle(npf((1, 0)), npf((1,  1))), -pi / 4)
    assert np.isclose(angle(npf((0, 1)), npf((1,  0))),  pi / 2)
    assert np.isclose(angle(npf((1, 0)), npf((0,  1))), -pi / 2)
    assert np.isclose(angle(npf((1, 0)), npf((1,  0))),  0)
    assert np.isclose(angle(npf((1, 0)), npf((-1, 0))),  pi)

%%timeit результаты без Numba

  • 359 мкс ± 2,86 мкс на цикл (среднее ± стандартное отклонение из 7 прогонов, по 1000 циклов в каждом)

И с

  • 151 мкс ± 820 нс на цикл (среднее ± стандартное отклонение из 7 прогонов, по 10000 циклов в каждом)

0

Использование numpy и устранение ошибок округления BandGap:

from numpy.linalg import norm
from numpy import dot
import math

def angle_between(a,b):
  arccosInput = dot(a,b)/norm(a)/norm(b)
  arccosInput = 1.0 if arccosInput > 1.0 else arccosInput
  arccosInput = -1.0 if arccosInput < -1.0 else arccosInput
  return math.acos(arccosInput)

Обратите внимание, что эта функция вызовет исключение, если один из векторов имеет нулевую величину (делится на 0).


0

Для тех немногих, кто, возможно, (из-за осложнений с SEO) закончил здесь попытки вычислить угол между двумя линиями в python, как и в случае с (x0, y0), (x1, y1)геометрическими линиями, существует следующее минимальное решение (использует shapelyмодуль, но его можно легко изменить, чтобы не использовать):

from shapely.geometry import LineString
import numpy as np

ninety_degrees_rad = 90.0 * np.pi / 180.0

def angle_between(line1, line2):
    coords_1 = line1.coords
    coords_2 = line2.coords

    line1_vertical = (coords_1[1][0] - coords_1[0][0]) == 0.0
    line2_vertical = (coords_2[1][0] - coords_2[0][0]) == 0.0

    # Vertical lines have undefined slope, but we know their angle in rads is = 90° * π/180
    if line1_vertical and line2_vertical:
        # Perpendicular vertical lines
        return 0.0
    if line1_vertical or line2_vertical:
        # 90° - angle of non-vertical line
        non_vertical_line = line2 if line1_vertical else line1
        return abs((90.0 * np.pi / 180.0) - np.arctan(slope(non_vertical_line)))

    m1 = slope(line1)
    m2 = slope(line2)

    return np.arctan((m1 - m2)/(1 + m1*m2))

def slope(line):
    # Assignments made purely for readability. One could opt to just one-line return them
    x0 = line.coords[0][0]
    y0 = line.coords[0][1]
    x1 = line.coords[1][0]
    y1 = line.coords[1][1]
    return (y1 - y0) / (x1 - x0)

И польза была бы

>>> line1 = LineString([(0, 0), (0, 1)]) # vertical
>>> line2 = LineString([(0, 0), (1, 0)]) # horizontal
>>> angle_between(line1, line2)
1.5707963267948966
>>> np.degrees(angle_between(line1, line2))
90.0
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.