角度(方位)の平均を出す
角度(方位)の平均を出す方法についてです。
普通、平均は
= 値の合計値/値の総数
で計算できますが、値の範囲が0〜360内で収まるべき角度や方位の場合、単純に合計を総数で割っても平均がでません。
0度と90度の平均は45度
(0+90)/2 = 45
ですが、
350度と10度の平均は0度(または360度)
(350+10)/2 = 0(または360)
です。
(350+10)/2 = 180ではありません。
それをどうやって計算するの?というテーマです。
角度の平均についてぐぐると色々ヒットしますが、3行以上は読めない上「んでプログラム上でどうするの」という分かりやすいサイトがなかったので、自作しました。
利用される想定としては、一定期間に連続してデータの入力がある状況で、ある期間でのデータを滑らかにできます。
1秒間に何度もデータイベントが発生するよな、ジョイスティックの向きや、携帯端末の方角測位などに向いています。イベントの情報をそのまま採用すると、手の微妙な震えで向きが小刻みにブレるので滑らかにしたい などです。
ただジョイスティックの情報の多くはx,y方向の2次元ベクトル情報で取れることが多いので、その場合は単位ベクトル合算の方が向いていると思います。
「いいからサンプルよこせ」的な方はこちら。eclipseのサンプルです。
以下、解説。
解説
DirectionAverager.java | 角度を平均化するクラス |
Sample.java | 角度を平均化をテスト実行するエントリクラス |
平均化するアルゴリズムは、簡単に言うと以下のとおりです。
- 平均を出したい角度をArrayList等配列にキャッシュする
- キャッシュされた角度配列を正規化する
- 正規化された角度配列の中身を、通常の「 = 値の合計値/値の総数」で平均値を出す
です。
仮に、1秒間の間に2回角度データが取得でき、それが0度と90度だったとします。その1秒間の平均の角度は45度になります。
{ 0, 90 }
というデータならば、この配列内のデータを単純に足して総数で割れば平均化できます。
しかし、
{ 350, 10 }
という角度の平均は、0であって欲しいわけです。
この場合、角度の平均化の難点は、0度(360度)です。データがここをまたぐと、平均が正常に出せません。円グラフの形で想像してもらえればわかりやすいですが、350と10の平均は180ではなく0(360)であるべきなのです。
350度と10度の平均のイメージ。北北西(350度)と北北東(10度)との平均の方向として真北(0度)が取得したい。
(350+10)/2 = 180 で真南になる180度になっては困る。
そこでデータを以下のように正規化し、平均計算ができるようにします。
- キャッシュされている値を、「前の値との差分」に変える
- その差分を、キャッシュされている最初の値に加算していく
です。
数値で説明したほうがわかりやすいと思うので、 { 350, 10, 45 } というデータで説明します。
{ 350, 10, 45 } を正規化し、
{ 350, 370, 405 } にします。
こうすることで、通常の平均計算で角度の平均が取得できます。 10度というのは370度で、45度というのは405度、というわけです。
以下、データの正規化の手順です。
- 一つ前の値からの差分をだす
- { 0, 20(350→10の差分), 35(10→45の差分) }
- この{ 0, 20, 35 }を、キャッシュ最初の350へ順に加算していく
- { 350(350+0),370(350+20),405(370+35) }
- 得られた{ 350,370,405 }の平均を出す。
という手法です。
基本原理は以上です。以下はサンプルコードの解説です。
ソースコードの解説
DirectionAverager.java
import java.util.ArrayList; /** * 方向を平均化するクラス * * @author ootanAW * */ public class DirectionAverager { // 定数 /** 方向バッファサイズ */ private static final int BUFFER_MAX = 15; /** 方向イベントの角度をそのまま保持する配列 */ private ArrayList<Float> nextRotateArray; /** 保持している角度の変動量を保持する配列(作業用) */ private ArrayList<Float> nextRotateArray2; /** 方向+変動量の値を保存する配列(0~360を超える。2度は362度になったりする) */ private ArrayList<Float> nextRotateArray3; /** 最新の回転角度 */ private float nextRotate; /** * コンストラクタ */ public DirectionAverager(){ nextRotateArray = new ArrayList<Float>(BUFFER_MAX); nextRotateArray2 = new ArrayList<Float>(BUFFER_MAX); nextRotateArray3 = new ArrayList<Float>(BUFFER_MAX); nextRotate = 0; } /** * 角度設定 * @param rotate */ public void setRotate(float rotate){ nextRotate = rotate; synchronized(nextRotateArray){ nextRotateArray.add(rotate); if(BUFFER_MAX < nextRotateArray.size()){ nextRotateArray.remove(0); } // 方向バッファの内容から角度の変動量を作成する angleNormalize(nextRotateArray2); // 方向バッファ+変動量から正規化された角度を作る(0~359を超える。2度は362度になったりする) float preNum = nextRotateArray.get(0); nextRotateArray3.clear(); for(float f : nextRotateArray2){ preNum += f; nextRotateArray3.add(preNum); } } } /** * 角度取得 * @return */ public float getRotate(){ float next = 0; synchronized(nextRotateArray3){ if(nextRotateArray3.size() <= 1){ next = nextRotate; }else{ next = 0; for(float f : nextRotateArray3){ next += f; } next /= nextRotateArray3.size(); } } return next; } /** * 角度配列を、正規化する * nextRotateArrayの中身を正規化する * * 例: * 10,20,40,80 を * 0,10,30,40 にする * * 2, 1, 0,359,358 を * 0,-1,-1, -1, -1 にする * * 358,1,5, 0,359 を * 0, 3,2,-5, -1 にする * * @param result */ private void angleNormalize(ArrayList<Float> result){ synchronized(nextRotateArray){ result.clear(); result.ensureCapacity(nextRotateArray.size()); float preNum = 0; result.add(preNum); for(int i=0; i<nextRotateArray.size()-1; i++){ float num1 = nextRotateArray.get(i); float num2 = nextRotateArray.get(i+1); float distance = num2 - num1; float distanceABS = Math.abs(distance); float distance2 = distance; if((360/2) < distanceABS){ if(distance < 0){ distance2 += 360; } if(0 < distance){ distance2 -= 360; } } float num = preNum + distance2; result.add(num); } } } /** * 与えたれた値を0~360の間に丸め込む * @return */ public float rounding(float angle){ float result = angle; if(result < 0){ result = (result%360)+360; // -10は350にする } if(360 < result){ result = (result%360); // 370は10にする } return result; } /** * バッファクリア */ public void clear(){ synchronized(nextRotateArray){ nextRotateArray.clear(); nextRotateArray2.clear(); nextRotateArray3.clear(); nextRotate = 0; } } /** * デバッグ機能:方向配列トレース * @return */ public void debug_trace(){ synchronized (nextRotateArray) { System.out.print("生角度:"); for(float f : nextRotateArray){ System.out.print(String.format(",%4d",(int)f)); } System.out.println(""); System.out.print("正規化:"); for(float f : nextRotateArray3){ System.out.print(String.format(",%4d",(int)f)); } System.out.println(""); } } }
平均化を行うクラスです。setRotate()で角度を内部配列にキャッシュしてゆき、getRotate()でキャッシュされている角度の平均値を取得します。
nextRotateArray nextRotateArray2 nextRotateArray3
という配列があり、nextRotateArray に角度の生データを格納してゆき、nextRotateArray2 に上記説明の「前の角度との差分」を格納します。そして最終的に nextRotateArray3 に正規化された角度データが格納されます。あとは、nextRotateArray3 内のデータの平均を計算すればよいだけです。
Sample.java
/** * 方向平均化サンプル * @author ootanAW * */ public class Sample { /** 平均化オブジェクト */ private static DirectionAverager dAverager; /** * 開始 * @param args */ public static void main(String[] args) { dAverager = new DirectionAverager(); // テスト1 System.out.println("テスト1"); float data[] = {0.0f, 90.0f}; // 平均をとりたい角度。0度と90度の平均は45度になります proc(dAverager, data); // 平均化処理 dAverager.clear(); // 角度キャッシュクリア // テスト2(0から180まで45度ずつ System.out.println("テスト2"); float data2[] = {0.0f, 45.0f, 90.0f, 135.0f, 180.0f}; // 0,45,90,135,180度の平均は平均90度になります proc(dAverager, data2); dAverager.clear(); // テスト3(0度をまたぐデータ System.out.println("テスト3"); float data3[] = {350.0f, 10.0f}; // 平均0度 proc(dAverager, data3); dAverager.clear(); // テスト4(テスト3に45度を加えたデータ System.out.println("テスト4"); float data4[] = {350.0f, 10.0f, 45}; // 平均15度 proc(dAverager, data4); dAverager.clear(); } /** * 平均化処理 * @param dAverager * @param data */ private static void proc(DirectionAverager dAverager, float data[]){ for(float angel : data){ dAverager.setRotate(angel); // 【1】 } float rotate = dAverager.getRotate(); // 【2】 System.out.println("平均角度:"+ rotate +"("+ dAverager.rounding(rotate)+")度"); dAverager.debug_trace(); System.out.println(""); } }
これは、角度平均化 DirectionAverager クラスを使って、テスト1〜4の、4パターンの平均化結果を出力しているだけのコードです。
テスト1〜4で用意されているdata1〜data4の値を、それぞれproc()の中でDirectionAveragerに設定してゆき【1】、そして平均化した値を取り出しています【2】。
このサンプルを実行すると以下のように出力されます。
テスト1 平均角度:45.0(45.0)度 生角度:, 0, 90 正規化:, 0, 90 テスト2 平均角度:90.0(90.0)度 生角度:, 0, 45, 90, 135, 180 正規化:, 0, 45, 90, 135, 180 テスト3 平均角度:360.0(360.0)度 生角度:, 350, 10 正規化:, 350, 370 テスト4 平均角度:375.0(15.0)度 生角度:, 350, 10, 45 正規化:, 350, 370, 405
注目はテスト3と4です。
テスト3では、{ 350, 10 } という角度が与えられており、このままこの2値の平均を出したら180になっていまいますが、処理結果は理想の360度が取得できています。
生角度:, 350, 10 正規化:, 350, 370
注目はここで、生の角度は { 350, 10 } ですが、正規化された後のデータは { 350, 370 } になっています。
テスト4も同様、正規化によって { 350, 10, 45 } が { 350, 370, 405 } になっています。
余談
もし、使用するシステム側で-10度を350度、370度を10度と認識していないのなら、得られたこの値を0〜360に丸め込みます。
DirectionAverager#rounding()がその処理で、引数の値を0〜360に丸め込んで返します。