Jak obliczyć kąt między linią a osią poziomą?

248

W języku programowania (Python, C # itp.) Muszę ustalić, jak obliczyć kąt między linią a osią poziomą?

Myślę, że obraz najlepiej opisuje to, czego chcę:

żadne słowa nie mogą tego opisać

Biorąc pod uwagę (P1 x , P1 y ) i (P2 x , P2 y ), jaki jest najlepszy sposób obliczenia tego kąta? Początek znajduje się w lewym górnym rogu i używana jest tylko dodatnia ćwiartka.

orlp
źródło
Zobacz także: To samo pytanie dla JavaScript
Martin Thoma,

Odpowiedzi:

388

Najpierw znajdź różnicę między punktem początkowym i końcowym (tutaj jest to bardziej ukierunkowany odcinek linii, a nie „linia”, ponieważ linie rozciągają się nieskończenie i nie zaczynają w określonym punkcie).

deltaY = P2_y - P1_y
deltaX = P2_x - P1_x

Następnie obliczyć kąt (który biegnie od dodatniej osi X P1do dodatniej osi Y w P1).

angleInDegrees = arctan(deltaY / deltaX) * 180 / PI

Ale arctanmoże nie być idealny, ponieważ podzielenie różnic w ten sposób usunie rozróżnienie potrzebne do rozróżnienia, w której kwadrancie znajduje się kąt (patrz poniżej). Zamiast tego zastosuj następujące informacje, jeśli Twój język zawiera atan2funkcję:

angleInDegrees = atan2(deltaY, deltaX) * 180 / PI

EDIT (22 lutego 2017): Na ogół jednak, nazywając atan2(deltaY,deltaX)tak aby uzyskać odpowiedni kąt dla cosi sinmoże być nieeleganckie. W takich przypadkach często można zamiast tego wykonać następujące czynności:

  1. Traktuj (deltaX, deltaY)jak wektor.
  2. Znormalizuj ten wektor do wektora jednostkowego. Aby to zrobić, podziel deltaXi deltaYprzez długość wektora ( sqrt(deltaX*deltaX+deltaY*deltaY)), chyba że długość wynosi 0.
  3. Następnie deltaXbędzie cosinus kąta między wektorem a osią poziomą (w kierunku od dodatniego X do dodatniej osi Y w P1).
  4. I deltaYbędzie teraz sinus tego kąta.
  5. Jeśli wektor ma długość 0, nie będzie miał kąta między nim a osią poziomą (więc nie będzie miał sensownego sinusa i cosinusa).

EDYCJA (28 lutego 2017 r.): Nawet bez normalizacji (deltaX, deltaY):

  • Znak deltaXwskazuje, czy cosinus opisany w kroku 3 jest dodatni czy ujemny.
  • Znak deltaYwskazuje, czy sinus opisany w kroku 4 jest dodatni czy ujemny.
  • Znaki deltaXi deltaYpowiedzą, w której kwadrancie znajduje się kąt, w stosunku do dodatniej osi X w P1:
    • +deltaX, +deltaY: Od 0 do 90 stopni.
    • -deltaX, +deltaY: Od 90 do 180 stopni.
    • -deltaX, -deltaY: 180 do 270 stopni (-180 do -90 stopni).
    • +deltaX, -deltaY: 270 do 360 stopni (od -90 do 0 stopni).

Implementacja w Pythonie przy użyciu radianów (dostarczona 19 lipca 2015 r. Przez Erica Leschinskiego, który zredagował moją odpowiedź):

from math import *
def angle_trunc(a):
    while a < 0.0:
        a += pi * 2
    return a

def getAngleBetweenPoints(x_orig, y_orig, x_landmark, y_landmark):
    deltaY = y_landmark - y_orig
    deltaX = x_landmark - x_orig
    return angle_trunc(atan2(deltaY, deltaX))

angle = getAngleBetweenPoints(5, 2, 1,4)
assert angle >= 0, "angle must be >= 0"
angle = getAngleBetweenPoints(1, 1, 2, 1)
assert angle == 0, "expecting angle to be 0"
angle = getAngleBetweenPoints(2, 1, 1, 1)
assert abs(pi - angle) <= 0.01, "expecting angle to be pi, it is: " + str(angle)
angle = getAngleBetweenPoints(2, 1, 2, 3)
assert abs(angle - pi/2) <= 0.01, "expecting angle to be pi/2, it is: " + str(angle)
angle = getAngleBetweenPoints(2, 1, 2, 0)
assert abs(angle - (pi+pi/2)) <= 0.01, "expecting angle to be pi+pi/2, it is: " + str(angle)
angle = getAngleBetweenPoints(1, 1, 2, 2)
assert abs(angle - (pi/4)) <= 0.01, "expecting angle to be pi/4, it is: " + str(angle)
angle = getAngleBetweenPoints(-1, -1, -2, -2)
assert abs(angle - (pi+pi/4)) <= 0.01, "expecting angle to be pi+pi/4, it is: " + str(angle)
angle = getAngleBetweenPoints(-1, -1, -1, 2)
assert abs(angle - (pi/2)) <= 0.01, "expecting angle to be pi/2, it is: " + str(angle)

Wszystkie testy przeszły pomyślnie. Zobacz https://en.wikipedia.org/wiki/Unit_circle

Peter O.
źródło
35
Jeśli to znalazłeś i używasz JAVASCRiPT, bardzo ważne jest, aby pamiętać, że Math.sin i Math.cos przyjmują radiany, więc nie trzeba przeliczać wyniku na stopnie! Dlatego zignoruj ​​bit * 180 / PI. Dowiedziałem się tego 4 godziny. :)
sidonaldson,
Czego należy użyć do obliczenia kąta wzdłuż osi pionowej?
ZeMoon
3
@akashg: 90 - angleInDegrees ?
jbaums
Dlaczego musimy zrobić kąt 90 stopni, czy jest na to jakiś powód? Wyjaśnij to samo.
Praveen Matanam,
2
@sidonaldson To coś więcej niż tylko Javascript, to C, C #, C ++, Java itd. W rzeczywistości ośmielę się powiedzieć, że większość języków ma bibliotekę matematyczną działającą głównie z radianami. Nie widziałem jeszcze języka, który domyślnie obsługuje tylko stopnie.
Pharap,
50

Przepraszam, ale jestem pewien, że odpowiedź Petera jest zła. Zauważ, że oś y idzie w dół strony (często w grafice). W związku z tym obliczenia delta muszą zostać odwrócone, w przeciwnym razie otrzymasz błędną odpowiedź.

Rozważać:

System.out.println (Math.toDegrees(Math.atan2(1,1)));
System.out.println (Math.toDegrees(Math.atan2(-1,1)));
System.out.println (Math.toDegrees(Math.atan2(1,-1)));
System.out.println (Math.toDegrees(Math.atan2(-1,-1)));

daje

45.0
-45.0
135.0
-135.0

Więc jeśli w powyższym przykładzie P1 to (1,1), a P2 to (2,2) [ponieważ Y zwiększa się w dół strony], powyższy kod da 45,0 stopni dla pokazanego przykładu, co jest błędne. Zmień kolejność obliczania delta i działa poprawnie.

użytkownik1641082
źródło
3
Odwróciłem to, jak zasugerowałeś, a mój obrót był do tyłu.
Devil's Advocate
1
W moim kodzie naprawiam to za pomocą: double arc = Math.atan2(mouse.y - obj.getPy(), mouse.x - obj.getPx()); degrees = Math.toDegrees(arc); if (degrees < 0) degrees += 360; else if (degrees > 360) degrees -= 360;
Marcus Becker
To zależy od ćwiartki koła, w której się znajdujesz: jeśli jesteś w pierwszym kwadracie (do 90 stopni), użyj dodatnich wartości dla deltaX i deltaY (Math.abs), w drugim (90-180) użyj neguje abstrakcyjną wartość deltaX, w trzecim (180-270) neguje zarówno deltaX, jak i delta, aw czwartym (270-360) neguje tylko deltaY - patrz moja odpowiedź poniżej
mamashare
1

Znalazłem rozwiązanie w Pythonie, które działa dobrze!

from math import atan2,degrees

def GetAngleOfLineBetweenTwoPoints(p1, p2):
    return degrees(atan2(p2 - p1, 1))

print GetAngleOfLineBetweenTwoPoints(1,3)
dctremblay
źródło
1

Biorąc pod uwagę dokładne pytanie, umieszczając nas w „specjalnym” układzie współrzędnych, w którym oś dodatnia oznacza przesuwanie się W DÓŁ (jak ekran lub widok interfejsu), musisz dostosować tę funkcję w ten sposób, a ujemne współrzędne Y:

Przykład w Swift 2.0

func angle_between_two_points(pa:CGPoint,pb:CGPoint)->Double{
    let deltaY:Double = (Double(-pb.y) - Double(-pa.y))
    let deltaX:Double = (Double(pb.x) - Double(pa.x))
    var a = atan2(deltaY,deltaX)
    while a < 0.0 {
        a = a + M_PI*2
    }
    return a
}

Ta funkcja daje prawidłową odpowiedź na pytanie. Odpowiedź jest w radianach, więc użycie, aby zobaczyć kąty w stopniach, jest następujące:

let p1 = CGPoint(x: 1.5, y: 2) //estimated coords of p1 in question
let p2 = CGPoint(x: 2, y : 3) //estimated coords of p2 in question

print(angle_between_two_points(p1, pb: p2) / (M_PI/180))
//returns 296.56
Filip
źródło
0

Na podstawie referencji „Peter O” .. Oto wersja Java

private static final float angleBetweenPoints(PointF a, PointF b) {
float deltaY = b.y - a.y;
float deltaX = b.x - a.x;
return (float) (Math.atan2(deltaY, deltaX)); }
Venkateswara Rao
źródło
0

funkcja matlab:

function [lineAngle] = getLineAngle(x1, y1, x2, y2) 
    deltaY = y2 - y1;
    deltaX = x2 - x1;

    lineAngle = rad2deg(atan2(deltaY, deltaX));

    if deltaY < 0
        lineAngle = lineAngle + 360;
    end
end
Benas
źródło
0

Wzór na kąt od 0 do 2pi.

Jest x = x2-x1 i y = y2-y1. Formuła działa

dowolna wartość xiy. Dla x = y = 0 wynik jest niezdefiniowany.

f (x, y) = pi () - pi () / 2 * (1 + znak (x)) * (1-znak (y ^ 2))

     -pi()/4*(2+sign(x))*sign(y)

     -sign(x*y)*atan((abs(x)-abs(y))/(abs(x)+abs(y)))
theodore panagos
źródło
0
deltaY = Math.Abs(P2.y - P1.y);
deltaX = Math.Abs(P2.x - P1.x);

angleInDegrees = Math.atan2(deltaY, deltaX) * 180 / PI

if(p2.y > p1.y) // Second point is lower than first, angle goes down (180-360)
{
  if(p2.x < p1.x)//Second point is to the left of first (180-270)
    angleInDegrees += 180;
  else //(270-360)
    angleInDegrees += 270;
}
else if (p2.x < p1.x) //Second point is top left of first (90-180)
  angleInDegrees += 90;
mamashare
źródło
Twój kod nie ma sensu: inaczej (270-360) .. co?
WDUK,
0
import math
from collections import namedtuple


Point = namedtuple("Point", ["x", "y"])


def get_angle(p1: Point, p2: Point) -> float:
    """Get the angle of this line with the horizontal axis."""
    dx = p2.x - p1.x
    dy = p2.y - p1.y
    theta = math.atan2(dy, dx)
    angle = math.degrees(theta)  # angle is in (-180, 180]
    if angle < 0:
        angle = 360 + angle
    return angle

Testowanie

Do testowania pozwalam hipotezie generować przypadki testowe.

wprowadź opis zdjęcia tutaj

import hypothesis.strategies as s
from hypothesis import given


@given(s.floats(min_value=0.0, max_value=360.0))
def test_angle(angle: float):
    epsilon = 0.0001
    x = math.cos(math.radians(angle))
    y = math.sin(math.radians(angle))
    p1 = Point(0, 0)
    p2 = Point(x, y)
    assert abs(get_angle(p1, p2) - angle) < epsilon
Martin Thoma
źródło