Winkel zwischen zwei n-dimensionalen Vektoren in Python


82

Ich muss den Winkel zwischen zwei n-dimensionalen Vektoren in Python bestimmen. Die Eingabe kann beispielsweise zwei Listen wie die folgenden sein: [1,2,3,4]und [6,7,8,9].


1
Dies ist die beste Antwort für @ MK83, da es sich genau um den mathematischen Ausdruck theta = atan2 (u ^ v, uv) handelt. Selbst der Fall, in dem u = [0 0] oder v = [0 0] behandelt wird, weil dies nur die Zeit ist, in der atan2 das NaN in den anderen Antworten erzeugt. NaN wird durch die / norm (u) oder / norm (v) erzeugt.
PilouPili

Antworten:


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

Hinweis : Dies schlägt fehl, wenn die Vektoren entweder die gleiche oder die entgegengesetzte Richtung haben. Die korrekte Implementierung finden Sie hier: https://stackoverflow.com/a/13849249/71522


2
Wenn Sie nur cos, sin, tan of angle und nicht den Winkel selbst benötigen, können Sie die math.acos überspringen, um Cosinus zu erhalten, und cross product verwenden, um Sinus zu erhalten.
mbeckish

9
Angesichts der Tatsache, dass dies math.sqrt(x)äquivalent zu x**0.5und math.pow(x,y)äquivalent zu ist x**y, bin ich überrascht, dass diese die Redundanz-Axt überlebt haben, die während des Python 2.x-> 3.0-Übergangs angewendet wurde. In der Praxis mache ich diese Art von numerischen Dingen normalerweise als Teil eines größeren rechenintensiven Prozesses, und die Unterstützung des Interpreters für '**' geht direkt zum Bytecode BINARY_POWER, im Gegensatz zur Suche nach 'math', dem Zugriff Aufgrund seines Attributs 'sqrt' und des schmerzhaft langsamen Bytecodes CALL_FUNCTION kann die Geschwindigkeit ohne Kosten für Codierung oder Lesbarkeit messbar verbessert werden.
PaulMcG

5
Wie in der Antwort mit numpy: Dies kann fehlschlagen, wenn der Rundungsfehler ins Spiel kommt! Dies kann für parallele und antiparallele Vektoren passieren!
BandGap

2
Hinweis: Dies schlägt fehl, wenn die Vektoren identisch sind (z angle((1., 1., 1.), (1., 1., 1.)). B. ). Siehe meine Antwort für eine etwas korrektere Version.
David Wolever

2
Wenn Sie über die obige Implementierung sprechen, schlägt dies aufgrund von Rundungsfehlern fehl, nicht weil die Vektoren parallel sind.
Tempo

150

Hinweis : alle anderen Antworten hier wird scheitern , wenn die beiden Vektoren entweder in die gleiche Richtung (ex, (1, 0, 0), (1, 0, 0)) oder entgegengesetzte Richtungen (ex, (-1, 0, 0), (1, 0, 0)).

Hier ist eine Funktion, die diese Fälle korrekt behandelt:

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

Wäre es nicht besser, np.isnananstelle des aus der Mathematikbibliothek zu verwenden? Theoretisch sollten sie identisch sein, aber in der Praxis bin ich mir nicht ganz sicher. So oder so würde ich mir vorstellen, dass es sicherer wäre.
Hooked

2
Mein numpy (Version == 1.12.1) kann arccosdirekt und sicher verwendet werden. : 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
Der Sonderfall, bei dem mindestens ein Eingabevektor der Nullvektor ist, wird weggelassen, was für die Division in problematisch ist unit_vector. Eine Möglichkeit besteht darin, in dieser Funktion nur den Eingabevektor zurückzugeben, wenn dies der Fall ist.
Kafman

1
angle_between ((0, 0, 0), (0, 1, 0)) ergibt nan als Ergebnis und nicht 90
FabioSpaghetti

2
Der Winkel von @kafman 0-Vektoren ist undefiniert (in Mathematik). Die Tatsache, dass ein Fehler auftritt, ist also gut.
Benutzer

45

Mit numpy (sehr zu empfehlen) würden Sie Folgendes tun:

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
Die letzte Zeile kann zu einem Fehler führen, wie ich aufgrund von Rundungsfehlern herausgefunden habe. Wenn Sie also (u, u) / norm (u) ** 2 punktieren, ergibt dies 1.0000000002 und der Arccos schlägt fehl (funktioniert auch für antiparallele Vektoren)
BandGap

Ich habe mit u = [1,1,1] getestet. u = [1,1,1,1] funktioniert gut, aber jede hinzugefügte Dimension gibt etwas größere oder kleinere Werte als 1 zurück ...
BandGap

3
Hinweis: Dies schlägt fehl (Ausbeute nan), wenn die Richtung der beiden Vektoren entweder identisch oder entgegengesetzt ist. Siehe meine Antwort für eine korrektere Version.
David Wolever

2
Wenn Sie den Kommentar von neo hinzufügen, sollte die letzte Zeile darin bestehen angle = arccos(clip(c, -1, 1)), Rundungsprobleme zu vermeiden. Dies löst das Problem von @DavidWolever.
Tim Tisdall

4
Für die Leute, die das obige Code-Snippet verwenden: clipsollte zur Liste der Numpy-Importe hinzugefügt werden.
Liam Deacon

26

Die andere Möglichkeit ist nur zu verwenden numpyund es gibt Ihnen den Innenwinkel

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)

und hier ist die Ausgabe:

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
Dies ist die beste Antwort, da es sich genau um den mathematischen Ausdruck theta = atan2 (u ^ v, uv) handelt. Und das scheitert nie!
PilouPili

1
Dies ist für 2-D. Das OP bat um nD
normanius

3

Wenn Sie mit 3D-Vektoren arbeiten, können Sie dies mit dem Toolbelt vg präzise tun . Es ist eine leichte Schicht auf Numpy.

import numpy as np
import vg

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

vg.angle(vec1, vec2)

Sie können auch einen Betrachtungswinkel angeben, um den Winkel über die Projektion zu berechnen:

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

Oder berechnen Sie den vorzeichenbehafteten Winkel per Projektion:

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

Ich habe die Bibliothek bei meinem letzten Start erstellt, wo sie durch Verwendungen wie diese motiviert war: einfache Ideen, die in NumPy ausführlich oder undurchsichtig sind.


3

Die Lösung von David Wolever ist gut, aber

Wenn Sie signierte Winkel haben möchten, müssen Sie feststellen, ob ein bestimmtes Paar Rechts- oder Linkshänder ist ( weitere Informationen finden Sie im Wiki ).

Meine Lösung dafür ist:

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

Aus diesem Grund ist es nicht perfekt, NotImplementedErroraber für meinen Fall funktioniert es gut. Dieses Verhalten könnte behoben werden (weil die Handlichkeit für jedes gegebene Paar bestimmt wird), aber es braucht mehr Code, als ich schreiben möchte und muss.


1

Aufbauend auf der großartigen Antwort von sgt pepper und der Unterstützung für ausgerichtete Vektoren sowie einer Beschleunigung von über 2x mit 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 Ergebnisse ohne Numba

  • 359 µs ± 2,86 µs pro Schleife (Mittelwert ± Standardabweichung von 7 Läufen, jeweils 1000 Schleifen)

Und mit

  • 151 µs ± 820 ns pro Schleife (Mittelwert ± Standardabweichung von 7 Läufen, jeweils 10000 Schleifen)

1

Einfache Möglichkeit, den Winkel zwischen zwei Vektoren zu finden (funktioniert für n-dimensionale Vektoren),

Python-Code:

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

0

Verwenden Sie numpy und kümmern Sie sich um die Rundungsfehler von 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)

Beachten Sie, dass diese Funktion eine Ausnahme auslöst, wenn einer der Vektoren eine Größe von Null hat (durch 0 teilen).


0

Für die wenigen, die (aufgrund von SEO-Komplikationen) hier versucht haben, den Winkel zwischen zwei Linien in Python zu berechnen , wie in (x0, y0), (x1, y1)geometrischen Linien, gibt es die folgende minimale Lösung (verwendet das shapelyModul, kann aber leicht geändert werden, um dies nicht zu tun):

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)

Und die Verwendung wäre

>>> 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
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.