前言

上位机开发过程中,信号数据是最常遇到的,在采集到信号数据后,如果能更好的展示成了难题。刚好最近接手了一个脑电信号数据的采集的项目,需要实时采集脑电信号并以波形展示出来。经过一番调研,网上有不少开源的图形控件用于波形的展示,比如oxyplotscottplotlivecharts等,在尝试后发现oxyplot采用MVVM设计,非常符合WPF的开发,Scottplot比较符合Winform的开发,而且接口也比较奇怪,livecharts绘图特别漂亮,但有性能问题,比如绘制大数据点时会特别卡。经过详细调研后决定使用Scottplot来实现,理由下面会说。

环境

运行环境:.Net 6
开发环境:VS2022 17.1

为什么选择Scottplot

我们知道波形图由坐标轴来绘制,需要横坐标和纵坐标,像脑电数据,跟采集频率有关,即横坐标是时间递增的,而Scottplot专门为这种横坐标递增的设计了一种波形:Signal Plot(信号图),信号图具有均匀分布的 Y 点。信号图非常快,可以交互显示数百万个数据点。有许多不同类型的可绘制对象,每种都有不同的用途。在实际使用中,只需要将数据以double的数组传进去就行,再设置频率,即可自动生成相应的波形,这对于需要高频采集数据并展示在坐标轴上是非常方便的。

使用

通过NuGet安装
在这里插入图片描述
Scottplot的使用可能与WPF的MVVM模式不太适应,官方也不推荐使用MVVM的方式来实现,就直接在后端加载就行。最终效果如图(8KHz的实时采集波形):
在这里插入图片描述
因为上手非常简单,而且官方教程里也有非常多的案例,具体的使用可以参考官方教程及Demo
我这里主要说一下遇到的问题吧:

图例

官方提供的图例非常简单,而且不支持点击某个图例对该波形的显示与隐藏,上图中,我是自己实现的,并不包括在坐标轴内。需要注意的是在初始化的时候,初始化一个ScottPlot.Plottable.SignalPlot代表一条折线。这也是上面说的,SDK接口使用比较奇怪的地方。

ScottPlot.Plottable.SignalPlot signalPlot=this.wpfPlot.Plot.AddSignal(data,freq,color);
//自定义图例控制折线的显示与隐藏
signalPlot.IsVisible=true;
this.wpfPlot.Render();

对,每次对图表更新后记得调用Render(),另外还有一个Refresh()也是可以刷新图表的。

十字线

鼠标移到坐标轴后以十字线显示当前点的坐标,这里需要在MosuMove事件中进行相应的数据处理

ScottPlot.Plottable.Crosshair Crosshair=this.wpfPlot.Plot.AddCrosshair(0, 0);
private void wpfPlot_MouseMove(object sender, MouseEventArgs e)
        {
            int pixelX = (int)e.MouseDevice.GetPosition(wpfPlot).X;
            int pixelY = (int)e.MouseDevice.GetPosition(wpfPlot).Y;

            (double coordinateX, double coordinateY) = wpfPlot.GetMouseCoordinates();

            //XPixelLabel.Content = $"{pixelX:0.000}";
            //YPixelLabel.Content = $"{pixelY:0.000}";

            txtX.Text = $"{wpfPlot.Plot.GetCoordinateX(pixelX):0.00}";
            txtY.Text = $"{wpfPlot.Plot.GetCoordinateY(pixelY):0.00}";

            Crosshair.X = coordinateX;
            Crosshair.Y = coordinateY;

            wpfPlot.Refresh();
        }

这里需要注意的是如果你的横坐标或者纵坐标的值并不是数组中的值 ,而是经过计算过的值时,则需要进行相应的计算后才能显示正确的值。

自动计算坐标轴

在使用过程中为了展示一段指定时间范围内的波形,需要保证整个波形都在坐标轴内,此时需要将这一段时间范围内的最大Y值和最小Y值计算出来,然后划分成若干段显示在Y轴上。如果数据不需要经过计算,可以直接使用下面的方式实现。

this.wpfPlot.Plot.AxisAutoY();

如果经过计算,则需要对坐标轴进行格式化TickLabelFormat

this.wpfPlot.Plot.XAxis.TickLabelFormat(customTickFormatter);

private string customTickFormatter(double position)
        {
            double res;
            if (_currentIndex > _showCount)
                res = (double)(_currentIndex - _showCount + position) /(double)_currentFreq;
            else
                res = position/ (double)_currentFreq;
            return res.ToString("F2");
        }
泛型的问题

这个控件目前只能传入double[]的数组,实时采集过程中是不知道数组的长度的,如果对于未知数组长度最好是使用List,官方说明

List<double> originalData = new List<double>() {5, 2, 7, 4, 9, 5};
double[] myData = originalData.ToArray();
signalPlot.AddSignal(myData );

但此时如果你往originalData 里增加数据,会发现坐标轴并不会变化,因为赋值的过程是将myData数组赋值给signalPlot的Ys属性,Ys属性是一个T[] Ys,这就很尴尬了,一旦将数组赋值后,就无法对其增加长度或者重新赋值了,换句话说:不支持长度可变的数据。

如何解决大数据量的实时采集

虽然官方说可以展示百万数据点,但实际测试发现,超过100万点的时候就会变得非常卡了,这个跟计算机的性能有关,毕竟100万点实际就是一个100万长度的数组中的数据渲染在坐标轴上。而且实时采集过程是需要让信号波在坐标轴上动态展示,比如向屏幕左侧偏移,这里需要解决的是数据实时加载到数组里,同时将波形往左偏移指定数据长度。有两种方案实现:
1、先定义一个非常长的数组,坐标轴上只显示10000个数据点,然后采集过程中,数据实时填充到坐标轴中,通过控制X轴的最小值和最大值来平移坐标轴

signalPlot.MinRenderIndex=min;
signalPlot.MaxRenderIndex=max;

这个方案有一个问题就是,在一开始采集的时候,并不知道具体的数组长度,而且并不支持泛型List,所以为了下标不越界,需要定义一个特别大的数组。
2、先定义一个固定长度的数组,比如10000,坐标轴上也只显示10000个数据点,采集过程中将新的数据点填充到数组里,超过10000之后将数组整体前移一位,同时将新的数据点赋值给数组最后一位

            Array.Copy(liveData, 1, liveData, 0, liveData.Length - 1);
            liveData[liveData.Length - 1] = nextValue;

这个方案可以非常好的控制对计算机资源的占用,而且能做到不卡顿,非常稳定的采集数据,我选用的是这个方案。

结论

以上就是目前使用该图形控件遇到的若干问题,希望能帮助各位读者,而且这个控件在国内使用的较少,很难找到比较全的资料。在实际使用中,测试该控件还是非常稳健的,大量数据点都能较好的显示,而且性能方面也较好,以上就是我个人对Scottplot的使用心得,如果有不正确的还望指出。
另外,官方目前已经有V5预览版本了,据说该版本有较大的升级。

Logo

鸿蒙生态一站式服务平台。

更多推荐