none
マルチタッチを用いての複数線の同時描画とパフォーマンスについて RRS feed

  • 質問

  • お世話になります。

    マルチタッチで同時に複数の線を描画可能とする簡易お絵かきアプリを下記サイトを参考にして作成しております。

    https://code.msdn.microsoft.com/CVBXAML-WPF-4-TouchDown-b1018a60/
    https://code.msdn.microsoft.com/windowsdesktop/CVBXAML-WPF-Windows-WPF-0738a600

    XAMLコード:

    <Window x:Name="window" x:Class="multiTouchApp.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:multiTouchApp"
            mc:Ignorable="d"
            Title="MainWindow" Height="700" Width="1050">
        <Grid>
            <StackPanel x:Name="stackPanel" HorizontalAlignment="Center" VerticalAlignment="Center">
                <Border BorderBrush="Black" BorderThickness="1" HorizontalAlignment="Left"  VerticalAlignment="Top" Width="{Binding Width, ElementName=image1}" Height="{Binding Height, ElementName=image1}" >
                    <Image x:Name="image1" 
            	    Stretch="Fill" TouchDown="image1_TouchDown" TouchMove="image1_TouchMove" TouchUp="image1_TouchUp" Width="1000" Height="600"  />
                </Border>
            </StackPanel>
           
        </Grid>
    </Window>

    C#コード:

    namespace multiTouchApp
    {
        /// <summary>
        /// MainWindow.xaml の相互作用ロジック
        /// </summary>
        public partial class MainWindow : Window
        {
    
            // タッチ情報
            public class TouchData
            {
                public Point lastTouchPoint;
                public Brush brush;
                public double press;
            }
    
            RenderTargetBitmap bitmap;
    
            public MainWindow()
            {
                InitializeComponent();
    
                bitmap = new RenderTargetBitmap((int)image1.Width, (int)image1.Height, 96, 96, PixelFormats.Default);
    
                image1.Source = bitmap;
            }
    
            private DrawingVisual drawVisual = new DrawingVisual();
            private DrawingContext drawContext;
            private Random rand = new Random();
    
    
            private Dictionary<TouchDevice, TouchData> map = new Dictionary<TouchDevice, TouchData>();
    
            private void image1_TouchDown(object sender, TouchEventArgs e)
            {
                var pt = e.GetTouchPoint(image1).Position;
                TouchData td = new TouchData();
    
                td.lastTouchPoint = pt;
                // 擬似的に筆圧を設定
                td.press = 12;
                // 線の色をタッチデバイス毎にランダムで決定する
                td.brush = new SolidColorBrush(Color.FromRgb((byte)rand.Next(256),
                        (byte)rand.Next(256), (byte)rand.Next(256)));
    
                // キーコレクションに現在のタッチ情報を登録
                map[e.TouchDevice] = td;
                image1.CaptureTouch(e.TouchDevice);
    
                // 描画処理開始
                drawContext = drawVisual.RenderOpen();
                drawContext.Close();
                bitmap.Render(drawVisual);
    
            }
    
            private void image1_TouchMove(object sender, TouchEventArgs e)
            {
    
                if (e.TouchDevice.Captured == image1)
                {
                    var pt = e.GetTouchPoint(image1).Position;
                    var tm = map[e.TouchDevice];
    
                    drawContext = drawVisual.RenderOpen();
                    Pen penDraw = new Pen(tm.brush, tm.press);
                    penDraw.StartLineCap = PenLineCap.Round;
                    penDraw.EndLineCap = PenLineCap.Round;
    
                    // 直線を描画 
                    drawContext.DrawLine(penDraw, tm.lastTouchPoint, pt);
    
                    drawContext.Close();
                    bitmap.Render(drawVisual);
    
                    tm.lastTouchPoint = pt;
    
                }
            }
    
            private void image1_TouchUp(object sender, TouchEventArgs e)
            {
                if (e.TouchDevice.Captured == image1)
                {
                    // タッチを離したデバイスの情報を解放
                    image1.ReleaseTouchCapture(e.TouchDevice);
                    map.Remove(e.TouchDevice);
    
                }
            }
        }
    }

    しかし、上記のコードでは以下のような問題が発生しており、解決できず困っております。

    1)同時に複数ポイントをタッチして線を引こうとした場合、描画処理が著しく遅くなる
    2)描画時に200MB~500MBと大量のメモリを使用しており、描画完了後も200MB程度が開放されずに使用中となっている

    これらの解決方法、または他にパフォーマンスの良い描画方法があればご教授願えないでしょうか。
    現状、描く線はラスタ形式ですが、ベクタ形式となっても問題ありません。

    (補足情報)
    ■実行環境
    ノートPC(10タッチポイントでのWindowsタッチのフルサポート)
    OS: Win8.1 64bit
    CPU: i7-4500U 1.80GHz
    GPU: Intel HD Graphics 4400
    MEM: 8GB
    ※MSペイントにてマルチタッチによる線描画が可能であることを確認しております。

    以上よろしくお願いいたします。

    2016年2月4日 9:56

回答

  • TouchMoveイベントが大量に発生するのにたいして、毎回bitmap.Render(drawVisual)を行っているために負荷がかかっています。
    描画する回数を減らしてやれば負荷も減ります。

    <Grid>
        <StackPanel x:Name="stackPanel" HorizontalAlignment="Center" VerticalAlignment="Center">
            <Border BorderBrush="Black" BorderThickness="1" HorizontalAlignment="Left" 
                    VerticalAlignment="Top" Width="{Binding Width, ElementName=image1}" 
                    Height="{Binding Height, ElementName=image1}" >
                <Grid>
                    <Image x:Name="image1" Stretch="Fill" TouchDown="image1_TouchDown" TouchMove="image1_TouchMove"
                        TouchUp="image1_TouchUp" Width="1000" Height="600"  />
                    <!-- 一時描画用のキャンパスをかぶせる -->
                    <Canvas IsHitTestVisible="False" x:Name="tempCanvas" 
                            HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
                    <Polyline/>
                    </Canvas>
                </Grid>
            </Border>
        </StackPanel>
    </Grid>
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Shapes;
    
    namespace WpfApplication1
    {
        public partial class MainWindow : Window
        {
            // タッチ情報
            public class TouchData
            {
                public Point lastTouchPoint;
                public Brush brush;
                public double press;
    
                public System.Windows.Shapes.Polyline Line { get; private set; }
    
                /// <summary>一時描画用のPolylineを用意</summary>
                /// <param name="cvs">一時描画先のCanvas</param>
                public void CreatePolyline(Canvas cvs)
                {
                    if (Line != null) { throw new InvalidOperationException(); }
                    Line = new Polyline();
                    Line.Stroke = brush;
                    Line.StrokeThickness = press;
    
                    cvs.Children.Add(Line);
                }
                /// <summary>
                /// 一時的な描画に使ったPolylineを除去
                /// </summary>
                public void RemovePolyline()
                {
                    if (Line != null)
                    {
                        ((Canvas)VisualTreeHelper.GetParent(Line)).Children.Remove(Line);
                        Line = null;
                    }
                }
                /// <summary>Polylineの軌跡でdrawingContextに線を描きなおす</summary>
                public void DrawLineTo(DrawingContext dc)
                {
                    if (Line != null && Line.Points.Count > 0)
                    {
                        Pen penDraw = new Pen(brush, press);
                        penDraw.StartLineCap = PenLineCap.Round;
                        penDraw.EndLineCap = PenLineCap.Round;
                        Point p0;
                        p0 = Line.Points.First();
                        foreach (Point p in Line.Points.Skip(1))
                        {
                            dc.DrawLine(penDraw, p0, p);
                            p0 = p;
                        }
                    }
                }
            }
    
            public MainWindow()
            {
                InitializeComponent();
    
                bitmap = new RenderTargetBitmap((int)image1.Width, (int)image1.Height, 96, 96, PixelFormats.Default);
                image1.Source = bitmap;
            }
            private RenderTargetBitmap bitmap;
            private DrawingVisual drawVisual = new DrawingVisual();
            private DrawingContext drawContext;
            private Random rand = new Random();
            private Dictionary<TouchDevice, TouchData> map = new Dictionary<TouchDevice, TouchData>();
    
            private void image1_TouchDown(object sender, TouchEventArgs e)
            {
                var pt = e.GetTouchPoint(image1).Position;
                TouchData td = new TouchData();
                td.lastTouchPoint = pt;
                td.press = 12;
                td.brush = new SolidColorBrush(Color.FromRgb((byte)rand.Next(256), (byte)rand.Next(256), (byte)rand.Next(256)));
                map[e.TouchDevice] = td;
                image1.CaptureTouch(e.TouchDevice);
    
                //いきなりDrawingContextに描画をすると重いので、まずはPolylineで描くようにする
                td.CreatePolyline(this.tempCanvas);
                td.Line.Points.Add(pt);
            }
    
    
            private void image1_TouchMove(object sender, TouchEventArgs e)
            {
                if (e.TouchDevice.Captured == image1)
                {
                    var pt = e.GetTouchPoint(image1).Position;
                    //Polylineの描画点を追加して自動で描かせる
                    map[e.TouchDevice].Line.Points.Add(pt);
                }
            }
    
            private void image1_TouchUp(object sender, TouchEventArgs e)
            {
                if (e.TouchDevice.Captured == image1)
                {
                    // タッチを離したデバイスの情報を解放
                    image1.ReleaseTouchCapture(e.TouchDevice);
    
                    var td = map[e.TouchDevice];
                    map.Remove(e.TouchDevice);
    
                    //軌跡からビットマップに描画しなおす
                    drawContext = drawVisual.RenderOpen();
                    td.DrawLineTo(drawContext);
                    drawContext.Close();
                    bitmap.Render(drawVisual);
    
                    td.RemovePolyline();//一時描画の線を除去
                }
            }
        }
    }

    なお、このサンプルでは適当なPolylineなので描画途中にヒゲが生えることがありますw

    上記のサンプルではタッチを離した時にそれまでの軌跡でbitmapに描画を行うため書き換え回数は少なくなります。
    ただし、別の色の軌跡が交差した場合は、途中に関係なく後に書き終えた色で上書きになります。

    もう少し負荷がかかってもきれいにしたい場合は、10回のMoveに対して10回分をまとめて描画とかすればいいです。
    あと、移動していなくてもイベントが発生し続けるので、座標が移動していなければ描画しないとか。

    #インフルエンザ出社禁止中orz


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 回答としてマーク der_y 2016年2月5日 6:52
    2016年2月5日 5:46

すべての返信

  • 直線を描画する前に以下のコードを入れてみてはどうでしょう

    penDraw.Freeze();

    2016年2月5日 1:24
  • 確認したわけではなくソースを眺めただけでの印象ですが、

    > Pen penDraw = new Pen(tm.brush, tm.press);

    TouchMoveイベント毎にPenインスタンスを生成するのは重いように思います。単なるコンストラクターですが内部的にはBrushとThicknessに対してそれぞれPropertyChangedイベントが発生するようです。

    2016年2月5日 1:34
  • ご指摘ありがとうございます。

    TouchDataクラスのメンバにPenを追加し、TouchDownイベントでインスタンスを生成するように修正いたしました。

    残念ながら問題点の改善には至りませんでしたが、こういった箇所の指摘は大変助かります。

    >BrushとThicknessに対してそれぞれPropertyChangedイベントが発生するようです。

    そうなのですか。単にインスタンス生成の負荷だけではないのですね。勉強になります。

    2016年2月5日 3:22
  • ご回答ありがとうございます。

    試しに以下のようにFreeze()を行ってみましたが、問題とする動作に変化はないようでした。。。

    // 直線を描画 
     tm.penDraw.Freeze();
    drawContext.DrawLine(tm.penDraw, tm.lastTouchPoint, pt);

    また、このFreeze()メソッドはどのような場面で使用するものなのかを理解できておりませんので、一度調べてみようと思います。

    2016年2月5日 3:29
  • TouchMoveイベントが大量に発生するのにたいして、毎回bitmap.Render(drawVisual)を行っているために負荷がかかっています。
    描画する回数を減らしてやれば負荷も減ります。

    <Grid>
        <StackPanel x:Name="stackPanel" HorizontalAlignment="Center" VerticalAlignment="Center">
            <Border BorderBrush="Black" BorderThickness="1" HorizontalAlignment="Left" 
                    VerticalAlignment="Top" Width="{Binding Width, ElementName=image1}" 
                    Height="{Binding Height, ElementName=image1}" >
                <Grid>
                    <Image x:Name="image1" Stretch="Fill" TouchDown="image1_TouchDown" TouchMove="image1_TouchMove"
                        TouchUp="image1_TouchUp" Width="1000" Height="600"  />
                    <!-- 一時描画用のキャンパスをかぶせる -->
                    <Canvas IsHitTestVisible="False" x:Name="tempCanvas" 
                            HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
                    <Polyline/>
                    </Canvas>
                </Grid>
            </Border>
        </StackPanel>
    </Grid>
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Shapes;
    
    namespace WpfApplication1
    {
        public partial class MainWindow : Window
        {
            // タッチ情報
            public class TouchData
            {
                public Point lastTouchPoint;
                public Brush brush;
                public double press;
    
                public System.Windows.Shapes.Polyline Line { get; private set; }
    
                /// <summary>一時描画用のPolylineを用意</summary>
                /// <param name="cvs">一時描画先のCanvas</param>
                public void CreatePolyline(Canvas cvs)
                {
                    if (Line != null) { throw new InvalidOperationException(); }
                    Line = new Polyline();
                    Line.Stroke = brush;
                    Line.StrokeThickness = press;
    
                    cvs.Children.Add(Line);
                }
                /// <summary>
                /// 一時的な描画に使ったPolylineを除去
                /// </summary>
                public void RemovePolyline()
                {
                    if (Line != null)
                    {
                        ((Canvas)VisualTreeHelper.GetParent(Line)).Children.Remove(Line);
                        Line = null;
                    }
                }
                /// <summary>Polylineの軌跡でdrawingContextに線を描きなおす</summary>
                public void DrawLineTo(DrawingContext dc)
                {
                    if (Line != null && Line.Points.Count > 0)
                    {
                        Pen penDraw = new Pen(brush, press);
                        penDraw.StartLineCap = PenLineCap.Round;
                        penDraw.EndLineCap = PenLineCap.Round;
                        Point p0;
                        p0 = Line.Points.First();
                        foreach (Point p in Line.Points.Skip(1))
                        {
                            dc.DrawLine(penDraw, p0, p);
                            p0 = p;
                        }
                    }
                }
            }
    
            public MainWindow()
            {
                InitializeComponent();
    
                bitmap = new RenderTargetBitmap((int)image1.Width, (int)image1.Height, 96, 96, PixelFormats.Default);
                image1.Source = bitmap;
            }
            private RenderTargetBitmap bitmap;
            private DrawingVisual drawVisual = new DrawingVisual();
            private DrawingContext drawContext;
            private Random rand = new Random();
            private Dictionary<TouchDevice, TouchData> map = new Dictionary<TouchDevice, TouchData>();
    
            private void image1_TouchDown(object sender, TouchEventArgs e)
            {
                var pt = e.GetTouchPoint(image1).Position;
                TouchData td = new TouchData();
                td.lastTouchPoint = pt;
                td.press = 12;
                td.brush = new SolidColorBrush(Color.FromRgb((byte)rand.Next(256), (byte)rand.Next(256), (byte)rand.Next(256)));
                map[e.TouchDevice] = td;
                image1.CaptureTouch(e.TouchDevice);
    
                //いきなりDrawingContextに描画をすると重いので、まずはPolylineで描くようにする
                td.CreatePolyline(this.tempCanvas);
                td.Line.Points.Add(pt);
            }
    
    
            private void image1_TouchMove(object sender, TouchEventArgs e)
            {
                if (e.TouchDevice.Captured == image1)
                {
                    var pt = e.GetTouchPoint(image1).Position;
                    //Polylineの描画点を追加して自動で描かせる
                    map[e.TouchDevice].Line.Points.Add(pt);
                }
            }
    
            private void image1_TouchUp(object sender, TouchEventArgs e)
            {
                if (e.TouchDevice.Captured == image1)
                {
                    // タッチを離したデバイスの情報を解放
                    image1.ReleaseTouchCapture(e.TouchDevice);
    
                    var td = map[e.TouchDevice];
                    map.Remove(e.TouchDevice);
    
                    //軌跡からビットマップに描画しなおす
                    drawContext = drawVisual.RenderOpen();
                    td.DrawLineTo(drawContext);
                    drawContext.Close();
                    bitmap.Render(drawVisual);
    
                    td.RemovePolyline();//一時描画の線を除去
                }
            }
        }
    }

    なお、このサンプルでは適当なPolylineなので描画途中にヒゲが生えることがありますw

    上記のサンプルではタッチを離した時にそれまでの軌跡でbitmapに描画を行うため書き換え回数は少なくなります。
    ただし、別の色の軌跡が交差した場合は、途中に関係なく後に書き終えた色で上書きになります。

    もう少し負荷がかかってもきれいにしたい場合は、10回のMoveに対して10回分をまとめて描画とかすればいいです。
    あと、移動していなくてもイベントが発生し続けるので、座標が移動していなければ描画しないとか。

    #インフルエンザ出社禁止中orz


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 回答としてマーク der_y 2016年2月5日 6:52
    2016年2月5日 5:46
  • ご回答頂きありがとうございます

    掲載頂いたコードを実行したところ、理想的な速度で描画処理ができておりました
    また、CPU使用率、メモリ使用率ともに実用範囲に収まっており、問題は解決しているようです
    TouchMoveでの描画の負荷を抑える方法としてこんな手法が取れたのですね。。。

    まだ頂いたコードの内容を全て理解できておらず心苦しい限りですが、
    大変有効な手法をご教授いただきまして大変感謝しております。ありがとうございました!

    2016年2月5日 6:52