Кути між двома 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

Відповіді:


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, загар кута, а не сам кут, тоді ви можете пропустити 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
Якщо ви говорите про реалізацію вище, вона не вдається через помилки округлення, а не тому, що вектори паралельні.
Темп

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
My numpy (версія == 1.12.1) може використовувати arccosбезпосередньо та безпечно. : У [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. Одна з можливостей - просто повернути вхідний вектор у цій функції, коли це так.
kafman

1
angle_between ((0, 0, 0), (0, 1, 0)) дасть nn як результат, а не 90
FabioSpaghetti

2
Кут 0-векторів @kafman невизначений (в математиці). Тож той факт, що він викликає помилку - це добре.
користувач

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) / норму (u) ** 2, це призводить до 1.0000000002, і arccos тоді виходить з ладу (також "працює" для антипаралельних векторів)
BandGap

Я тестував з u = [1,1,1]. u = [1,1,1,1] працює нормально, але кожен доданий розмір повертає трохи більші або менші значення, ніж 1 ...
BandGap

3
Примітка: це не вдасться (вихід nan), коли напрямок двох векторів буде однаковим або протилежним. Дивіться мою відповідь для більш правильної версії.
Девід Волевер,

2
додаючи до цього коментар нео, останній рядок повинен бути, 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

1
Це для 2-D. ОП просила nD
normanius

3

Якщо ви працюєте з 3D-векторами, ви можете зробити це стисло, використовуючи інструментальну стрічку 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

Рішення Девіда Волевера - це добре, але

Якщо ви хочете мати підписані кути, вам слід визначити, чи є дана пара правшею чи лівшею (див. Wiki для отримання додаткової інформації).

Моє рішення для цього:

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 додавши підтримку вирівняних векторів, а також додавши прискорення понад 2x за допомогою 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.