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 Likes

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

2 Likes

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

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

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

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

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

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

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

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

4 Likes

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

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

3 Likes

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

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>

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

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

9 Likes

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

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

감사합니다.

1 Like