WPF 캔버스에 라이다 점 데이터 표현

안녕하세요.

제가 최근에 관심있는 주제가 있어서 이렇게 글을 올립니다.

라이다 데이터를 이용해서 WPF Canvas에 표현을 하려고 하는데요.

약 7Hz로 회전하는 라이다가 있습니다.

약 150 ms 당 3800개의 포인트를 리스트를 만들어서 Canvas에 뿌려줍니다.

고속의 데이터를 실시간으로 보여주려고 하는데요.

아래는 제가 표현하려고하는 방식입니다.

그런데 Canvas에 아래 코드로 실행하면 프로그램이 멈출정도로 느려지는 현상이 있습니다.

그러다가 프로그램이 죽는데, 3~4천개면 엄청 대량의 데이터도 아니고, WPF가 느린건지 아니면

구현 방법이 잘못된건지 모르겠습니다. 물론 제 구현방법이 잘못된것이라고 생각되지만, WPF가 참

이럴때마다 좀실망스럽네요. ㅠㅠ

<UserControl.Resources>
		<utils:OffsetConverter x:Key="OffsetConverter"/>
	</UserControl.Resources>
			<Canvas Background="#88000000"
					ClipToBounds="True">
			<ItemsControl ItemsSource="{Binding Points, UpdateSourceTrigger=PropertyChanged}">
				<ItemsControl.ItemsPanel>
					<ItemsPanelTemplate>
						<Canvas/>
					</ItemsPanelTemplate>
				</ItemsControl.ItemsPanel>
				<ItemsControl.ItemTemplate>
					<DataTemplate>
						<Ellipse Width="3" Height="3" Fill="WhiteSmoke">
							<Ellipse.RenderTransform>
								<!--<TranslateTransform X="{Binding X, Converter={StaticResource OffsetConverter}, ConverterParameter=X}" Y="{Binding Y, Converter={StaticResource OffsetConverter}, ConverterParameter=Y}"/>-->
								<TranslateTransform X="{Binding X}" Y="{Binding Y}"/>
							</Ellipse.RenderTransform>
						</Ellipse>
					</DataTemplate>
				</ItemsControl.ItemTemplate>
				<ItemsControl.ItemContainerStyle>
					<Style>
						<!--<Setter Property="Canvas.Left" Value="{Binding X, Converter={StaticResource OffsetConverter}, ConverterParameter=X}"/>
						<Setter Property="Canvas.Top" Value="{Binding Y, Converter={StaticResource OffsetConverter}, ConverterParameter=Y/> -->
						<Setter Property="Canvas.Left" Value="{Binding X}"/>
						<Setter Property="Canvas.Top" Value="{Binding Y}"/>
					</Style>
				</ItemsControl.ItemContainerStyle>
			</ItemsControl>
		</Canvas>

이렇게 표현하고,

ViewModel은 이렇게 사용합니다.

public class VisualViewModel : BaseViewModel
    {
        #region - Ctors -
        public VisualViewModel(IEventAggregator eventAggregator
                                , LidarService lidarService) 
            : base(eventAggregator)
        {
            _lidarService = lidarService;
            locker = new object();
        }
        #endregion
        #region - Implementation of Interface -
        #endregion
        #region - Overrides -
        protected override Task OnActivateAsync(CancellationToken cancellationToken)
        {
            _lidarService.SendPoints += _lidarService_SendPoints;
            return base.OnActivateAsync(cancellationToken);
        }

        protected override Task OnDeactivateAsync(bool close, CancellationToken cancellationToken)
        {
            return base.OnDeactivateAsync(close, cancellationToken);
        }
        #endregion
        #region - Binding Methods -
        #endregion
        #region - Processes -
        private Task _lidarService_SendPoints(List<Measure> measures)
        {
            return Task.Run(() =>
            {
                Application.Current.Dispatcher.Invoke(() =>
                {
                    Points = measures;
                    NotifyOfPropertyChange(() => Points);
                });

            });

            //return Task.Run(() => 
            //{
            //    lock (locker)
            //    {

            //        Debug.WriteLine($"=====Start=====");
            //        foreach (var item in measures)
            //        {
            //            Debug.WriteLine($"θ:{item.angle}, L:{item.distance}, X:{item.X}, Y:{item.Y}");
            //        }
            //        Debug.WriteLine($"=====End=====");
            //    }
            //});
            
        }
        #endregion
        #region - IHanldes -
        #endregion
        #region - Properties -
        public List<Measure> Points { get; set; } 
        #endregion
        #region - Attributes -
        private LidarService _lidarService;
        private object locker;
        #endregion
    }

어떻게 접근하는 것이 좋을지 조언 부탁드립니다.

2개의 좋아요

Canvas의 OnRender 메소드를 오버라이드 해서 인자로 오는 DrawingContext를 통해 직접 그리시는 것을 추천합니다.

2개의 좋아요

데이터 3~4 천 개 처리는 문제가 안되는데…

Ellipse 3800개를 렌더링하는 것은 얘기가 달라지죠.
그것도 150ms 주기이니 초 단위로 환산하면 25,000 개입니다.

매 초마다 circle 연산이 들어가는 콘트롤 25,000 개를 새롭게 렌더링하는 것은 어떤 도구라도 무리가 아닐까요?

UI 도구는 사용자에게 보여지는 것만 처리하고, 로직은 뷰모델이나 별도의 서비스에서 처리하는 방향으로 변경하는 게 맞을 것 같습니다.

라이다 서비스 = (x,y)[] => 비트맵 이미지 생성기 = 이미지 => UI

라이다 서비스부터 이미지 생성기까지 하나의 단위 서비스로 만들어 놔야, 윈폼 이나 asp.net 등 비트맵을 다룰 수 있는 다른 UI 프레임워크에도 사용할 수가 있게 되겠죠.

그리고, 라이다 서비스의 속성 SendPoints의 형식으로 Action<List> 을 설정하신 것 같은데, 대리자도 쓸 수 있지만, 이 경우, event 로 한정하시는 게 일반적인 패턴입니다.

대리자를 event 로 한정하는 것은 대리자의 호출 권한을 대리자 소유주로 한정합니다.
즉, 소유주를 발행자-구독자 패턴의 발행자로 만드는 것이죠.

4개의 좋아요

가볍게 테스트 프로그램을 만든신것 같은데
저도 저런 처리 할때 concurrencythreadpool 같은 버퍼를 두고 처리했던것 같네요
그리고 이런 control을 쓰셔야 할것예요

여기서 최대한 비슷한 chart 를 사용해보시는것이 어떠신지요??

3개의 좋아요

간단히 성능을 테스트해보았습니다.

public class DrawningCanvas : Canvas
{
    private static readonly Pen _pen;

    static DrawningCanvas()
    {
        _pen = new Pen(Brushes.Red, 1);
        _pen.Freeze();
    }

    public DrawningCanvas()
    {
        _ = Task.Run(async () =>
        {
            var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(150));
            while (await timer.WaitForNextTickAsync())
            {
                await Dispatcher.InvokeAsync(InvalidateVisual);
            }
        });

       
    }
    protected override void OnRender(DrawingContext dc)
    {
        var rand = Random.Shared;
        var count = 3800;

        for (var i = 0; i < count; i++)
        {
            dc.DrawEllipse(Brushes.Red, _pen, new Point(rand.Next((int)ActualWidth), rand.Next((int)ActualHeight)), 3, 3);
        }
    }
}
<Window
    x:Class="WpfApp38.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:local="clr-namespace:WpfApp38"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <local:DrawningCanvas x:Name="canvas" />
</Window>

image

제 컴퓨터에서는 무리 없이 동작하는군요.

참고로 사용하는 Brush나 Pen의 Freeze() 유무에 따라 성능 차이가 많이 납니다.

9개의 좋아요

실제적인 해결책을 주셔서 감사합니다.

해당 로직을 기반으로 Control을 만들어서 적용해서 해결했습니다.

감사합니다.

1개의 좋아요