午後わてんのブログ

ベランダ菜園とWindows用アプリ作成(WPFとC#)

ベジェ曲線の長さ測定できた、C#とWPF

4日前 

gogowaten.hatenablog.com

の続きで今度は曲線Pathの長さを測る
GetFlattenedPathGeometryとGetPointAtFractionLengthを使って測る
今回のアプリのダウンロード先
半径50の円周で確認
イメージ 1
円周は2*パイ*半径なので
2*3.14*50=314になればいい
 
イメージ 2
結果は314.2、小数点2桁目で四捨五入しているからこれであっている
 
 
 
 
ベジェ曲線の長さ測定
イメージ 8
 
 
イメージ 4
水色の線の長さは
 
イメージ 3
966.3と測定された
だいたいあっているはず
 
 
 
 
公差と分割数
 
GetFlattenedPathGeometryに渡すTolerance(公差)が102の場合
イメージ 5
得られる近似直線は雑になる(細い青線)
なのでこれを細かく測定しても
 
イメージ 6
いい結果は得られない
 
 
公差0で丁寧な近似直線でも分割数が少ないと
イメージ 7
これもかなり不正確
測定値は赤の直線の長さの合計
 
より正確に測るには
GetFlattenedPathGeometryに渡す引数Tolerance(公差)は0を指定して
分割数を増やしてGetPointAtFractionLengthで細かく測定する
 
 
分割数をより増やしてみたら
イメージ 9
972まで増やしたら966.7になった
200のときでも966.3だったので、あんまり変わらない
もっと長く複雑な曲線なら増やしたほうが良さそうだけど
この程度の曲線なら200で十分かな
デザイン画面

f:id:gogowaten:20191213102424p:plain

C#コード
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
//曲線のPathをGetFlattenedPathGeometryで近似直線Pathに変換して
//それをGetPointAtFractionLengthで分割した頂点同士の距離を合計する

namespace _20180614_曲線の長さを測る
{
    public partial class MainWindow : Window
    {
        PathGeometry FlattenedPathGeometry;//曲線を近似直線に変換したPathGeometry
        List<Path> StepEllipseList = new List<Path>();//分割したところの○印
        List<Path> MyListLine = new List<Path>();//○印を結ぶ直線Path

        public MainWindow()
        {
            InitializeComponent();

            FlattenedPathGeometry = MyPath.Data.GetFlattenedPathGeometry();
            FlattendeLine.Data = FlattenedPathGeometry;

            SliderStep.ValueChanged += SliderStep_ValueChanged;
            SliderTolerance.ValueChanged += SliderTolerance_ValueChanged;
            SliderStep.MouseWheel += Slider_MouseWheel;
            SliderTolerance.MouseWheel += Slider_MouseWheel;

        }
        //マウスホイール回したとき
        private void Slider_MouseWheel(object sender, MouseWheelEventArgs e)
        {
            Slider slider = (Slider)sender;
            if (e.Delta > 0) { slider.Value += slider.SmallChange; }
            else { slider.Value -= slider.SmallChange; }
        }
        //公差変更時
        private void SliderTolerance_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            ChangeFlattendePath();
            Measure();
        }
        //分割数変更時
        private void SliderStep_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            Measure();
        }
        //長さ測定
        private void Measure()
        {
            ClearStepEllipse();
            var pg = (PathGeometry)MyPath.Data;
            Point p1 = pg.Figures[0].StartPoint;
            Point p2;
            double pathLength = 0;//長さ測定用
            double step = SliderStep.Value;
            for (int i = 0; i < step + 1; i++)
            {
                //○印の作成と追加
                //第1引数は0から1を指定、0がPathの始点になり1は終点、0.5なら中間位置を取得できる
                FlattenedPathGeometry.GetPointAtFractionLength(i / step, out Point p, out Point pt);
                var ellipseGeo = new EllipseGeometry(p, 4, 4);
                var path = new Path();
                path.Stroke = Brushes.Blue;
                path.Data = ellipseGeo;
                MyGrid.Children.Add(path);
                StepEllipseList.Add(path);

                //長さ測定
                p2 = p;
                pathLength += Distance(p1, p2);
                //測定対象の2頂点を結ぶ直線Pathの作成と追加
                var lineG = new LineGeometry(p1, p2);
                p1 = p;
                path = new Path();
                path.Stroke = Brushes.Red;
                path.Data = lineG;
                MyGrid.Children.Add(path);
                MyListLine.Add(path);
            }
            //表示更新
            TextBlockMeasure.Text =
                "測定値 = " + Math.Round(pathLength, 1, MidpointRounding.AwayFromZero).ToString();
        }
        //2点間の距離、ユークリッド距離
        private double Distance(Point p1, Point p2)
        {
            return Math.Sqrt(Math.Pow(p2.X - p1.X, 2) + Math.Pow(p2.Y - p1.Y, 2));
        }
        //表示をクリア
        private void ClearStepEllipse()
        {
            foreach (var item in StepEllipseList)
            {
                MyGrid.Children.Remove(item);
            }
            StepEllipseList.Clear();

            foreach (var item in MyListLine)
            {
                MyGrid.Children.Remove(item);
            }
            MyListLine.Clear();
        }
        //近似直線の更新
        private void ChangeFlattendePath()
        {
            //第1引数Toleranceは0以上を指定、0が一番曲線に近く正確だけど計算量が増える
            FlattenedPathGeometry =
                MyPath.Data.GetFlattenedPathGeometry(SliderTolerance.Value, ToleranceType.Absolute);
            FlattendeLine.Data = FlattenedPathGeometry;
        }

    }
}
 
 
もっと正確に測りたいときはGetFlattenedPathGeometryで得た近似直線のPathGeometryから直接測ればいいけど、めんどくさそう

f:id:gogowaten:20191213102649p:plain

ベジェ曲線をGetFlattenedPathGeometryで
公差=0を指定で得られた近似直線のPathGeometryの頂点座標数は4096!
これを計算すればかなり正確になるんだろうけど多すぎるw
だったら公差の値を大きくすればいいけどそれなら分割数で指定しても同じかなと
それに今回はPolyLineSegmentで得られたけど、元のPathによってはLineSegmentとかになることもあったり、同じ座標が何回も出てきたりすることもあったので、近似直線のPathGeometryから直接測定するのはめんどくさそうだったので、やっぱりGetPointAtFractionLengthで分割がラクでいいかなあ
 

f:id:gogowaten:20191213102700p:plain

円をGetFlattenedPathGeometryで公差0
Segment数が9でこの中にPolyLineSegmentとLineSegmentが交互に混じっていたはず
 
 
 
パスマークアップ構文で円を描く
半径50の円の場合は
PathGeometry Figures="M100,100 A50,50 0 1 1 100,200 A50,50 0 1 1 100,100"
 
パスマークアップ構文に円はないので、円弧を2つ組み合わせて書く
イメージ 11
"M100,100 A50,50 0 1 1 100,200"
Mは始点、Aからが円弧で
50,50が半径のx,y
0 回転角度
1 よくわからんけど1か0を指定する
1 よくわからん
100,200 終点のx,y
これで右半分描けたので
左半分を足して
"M100,100 A50,50 0 1 1 100,200 A50,50 0 1 1 100,100"
左半分は終点が100,100になるだけで右半分と同じでいいみたい
これで円になる
イメージ 12
なった
 
円を描くだけなら円専用のEllipseGeometryが便利だけど、これだとGetFlattenedPathGeometryメソッドがないのでPathGeometryで書く必要があった
なかった
23時09分追記
PathGeometryクラスのCreateFromGeometryメソッドを使えば
var ellipsePath = new Path();
ellipsePath.Data = new EllipseGeometry(new Point(100, 100), 50, 50);
PathGeometry pg = PathGeometry.CreateFromGeometry(ellipsePath.Data);
または
var pg = PathGeometry.CreateFromGeometry(new EllipseGeometry(new Point(100, 100), 50, 50));
とか
これでEllipseGeometryをPathGeometryに変換できたからGetFlattenedPathGeometry使える、また余計なことしてたわw
 
 
 
参照したところ
 
 
コード
 
 
 
 
関連記事
2018/6/14は4日前
曲線Pathを近似の直線PathにするGetFlattenedPathGeometry使ってみた ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15549065.html
2018/6/15は3日前
Pathを等分したところに印と角度を表示してみた、GetPointAtFractionLength ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15550562.html
2018/6/16はおととい
GetPointAtFractionLengthで分割した座標からのPathの長さ測定の確認 ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15551929.html