【Java】2つのベクトルがなす角度を求める

f:id:shogonir:20171202134506p:plain

 

目次

  1. この記事の目的
  2. 三点がなす角度の求め方
  3. x,y-平面上の座標を表すクラス
  4. ベクトルの差、長さ、内積外積
  5. 三点がなす外角を求める
  6. まとめ

 

1. この記事の目的

この記事では、xy-平面上の点の情報から角度を求める方法について紹介します。
特に、三点を入力とし、この三点がなす角の外角を求める方法を紹介します。

また、入力された三点をこの順になぞったときに時計回りに曲がっているかを判定できるものにしたいと思います。
具体的には、時計回りに90度曲がったときに-π/2、反時計回りに90度曲がったときにπ/2を返すようにします。
こうすることで、求めた角度の正負で時計回りかどうかを判定できます。

サンプルコードを上げました。
github.com

 

2. 三点がなす角度の求め方

まずは数学的にどのようなアプローチで三点がなす角度を求めるかを紹介します。
数学のことはいいからコードを見せろ!という方は次の段落「3. x,y-平面上の座標を表すクラス ポイントクラス」まで読み飛ばしてください。
一般的にはベクトルの内積を使用するようです。

ベクトルa, bの内積をa.b、大きさを|a|, |b|、なす角をtとすると、内積は次の通り。
a.b = |a| * |b| * cos(t)

この方程式をtについて解くと、
t = acos( a.b / (|a| * |b|))

これで三点がなす角度がわかったので、180度からこれを引けば外角が求まります。
しかしこのままでは時計回りかどうかは判定できません。
入力の最初と最後を入れ替えても同じ角度が帰ってきてしまいます。

 

f:id:shogonir:20171202132014p:plain

 

そこで役に立つのが外積です。
外積は、時計回りのときにマイナス、反時計回りのときにプラスになります。
これでプログラミングに移れそうです。

 

3. x,y-平面上の座標を表すクラス ポイントクラス

では早速プログラミングに入っていきます。
まずは二次元平面上の座標を表すクラスです。

今回はPoint2と名付けることにします。
double型のメンバx, yを持つだけです。

 

package model;

public class Point2 {

    private double x;
    private double y;

    public Point2(double x, double y) {
        this.x = x;
        this.y = y;
    }
}

 

Point2はシンプルなクラスなので、ベクトルとしても扱うことができます。
例えば点p1, p2があったときに、
v(p1 -> p2) = p2 - p1
と計算することができます。

では次の段落でPoint2にベクトル演算用のメソッドを実装していきます。

 

4. ベクトルの差、長さ、内積外積

ベクトルの差を返す関数を実装します。
使いやすさ重視で、引き算した結果を新しいインスタンスで返します。

 

public Point2 subtract(Point2 point) {
    return new Point2(this.x - point.x, this.y - point.y);
}

 

次にベクトルの長さを返すメソッドを実装します。

 

public double magnitude() {
    return Math.sqrt(x * x + y * y);
}

 

次にベクトルの内積を返すメソッドを実装します。 二次元平面のベクトルa, bの内積はax * bx + ay * byで求められます。

 

public double innerProduct(Point2 point) {
    return this.x * point.x + this.y * point.y;
}

 

最後にベクトルの外積を返すメソッドを実装します。 二次元平面のベクトルa, bの外積はa.x * b.y - a.y * b.xで求められます。

 

public double outerProduct(Point2 point) {
    return this.x * point.y - this.y * point.x;
}

 

5. 三点がなす外角を求める

ここまでで二次元平面上の座標やベクトルの定義と、それらの計算につかうメソッドをいくつか実装しました。 それでは早速、三点がなす外角を求めましょう。

手順としては以下の通りです。
t = acos( a.b / (|a| * |b|)) で内角を求める。
外積の正負で処理を分け、内角から外角を求める。

 

package util;

import model.Point2;

public class MathUtil {

    public static double calculateExternalAngle(Point2 p1, Point2 p2, Point2 p3) {
        Point2 v1 = p2.subtract(p1);
        Point2 v2 = p2.subtract(p3);
        double angleRadian = Math.acos(v1.innerProduct(v2) / (v1.magnitude() * v2.magnitude()));
        double angleDegree = angleRadian * 180 / Math.PI;
        if (v1.outerProduct(v2) > 0) {
            return angleDegree - 180;
        } else {
            return 180 - angleDegree;
        }
    }
}

 

動作を確認するためテストコードを作成しました。
(動作を見るだけで、アサートなどはおこなっていません)

 

package util;

import model.Point2;
import org.junit.Test;

public class MathUtilTest {

    @Test
    public void calculateExternalAngle() throws Exception {
        Point2 one = new Point2(1.0, 0.0);
        Point2 zero = new Point2(0.0, 0.0);
        for (int angleDegree = 0; angleDegree <= 360; angleDegree += 10) {
            double angleRadian = angleDegree / 180f * Math.PI;
            Point2 point = new Point2(Math.cos(angleRadian), Math.sin(angleRadian));
            double externalAngleDegree = MathUtil.calculateExternalAngle(one, zero, point);
            System.out.println(String.format("%4d -> %8.3f", angleDegree, externalAngleDegree));
        }
    }
}

 

calculateExternalAngleの第一引数をPoint2(1, 0)、第二引数をPoint2(0, 0)に固定して、第三引数を単位円上の移動する点にして入力と出力が正しいかたしかめています。

実行結果は下記のとおりです。
意図どおりの返り値が帰ってきました。

 

   0 ->  180.000
  10 -> -170.000
  20 -> -160.000
  30 -> -150.000
  40 -> -140.000
  50 -> -130.000
  60 -> -120.000
  70 -> -110.000
  80 -> -100.000
  90 ->  -90.000
 100 ->  -80.000
 110 ->  -70.000
 120 ->  -60.000
 130 ->  -50.000
 140 ->  -40.000
 150 ->  -30.000
 160 ->  -20.000
 170 ->  -10.000
 180 ->    0.000
 190 ->   10.000
 200 ->   20.000
 210 ->   30.000
 220 ->   40.000
 230 ->   50.000
 240 ->   60.000
 250 ->   70.000
 260 ->   80.000
 270 ->   90.000
 280 ->  100.000
 290 ->  110.000
 300 ->  120.000
 310 ->  130.000
 320 ->  140.000
 330 ->  150.000
 340 ->  160.000
 350 ->  170.000
 360 ->  180.000

 

6. まとめ

三点がなす外角を求める方法を紹介しました。
ポリゴンの三角形分割で役に立つので、その紹介も行いたいと思います。