使用SkiaSharp打造专业级12导联心电图查看器:性能与美观兼具的可视化实践

article/2025/7/18 19:16:41

在这里插入图片描述

前言

欢迎关注dotnet研习社,今天我们研究的Google Skia图形库的.NET绑定SkiaSharp图形库

在医疗软件开发领域,心电图(ECG)数据的可视化是一个既有挑战性又极其重要的任务。作为开发者,我们需要创建既专业又直观的界面来展示复杂的生物医学数据。本文将分享我使用SkiaSharp在.NET平台上构建12导联心电图查看器的完整过程,希望能为医疗软件开发者提供一些有价值的参考。

项目背景

心电图是临床诊断中最重要的工具之一,标准的12导联心电图能够从不同角度观察心脏的电活动。传统的心电图显示软件往往依赖于专有的图形库或复杂的绘图框架,这不仅增加了开发成本,也限制了定制化的可能性。

SkiaSharp作为Google Skia图形库的.NET绑定,为我们提供了一个强大而灵活的2D图形渲染解决方案。它不仅性能优异,还支持硬件加速,非常适合用于构建高质量的医学数据可视化应用。

技术选型与架构设计

技术栈

  • .NET 8.0: 最新的.NET框架,提供优异的性能和丰富的API
  • Windows Forms: 成熟稳定的桌面应用框架
  • SkiaSharp: 高性能2D图形渲染引擎
  • SkiaSharp.Views.WindowsForms: Windows Forms集成包

在这里插入图片描述

  • Nuget地址
  • GitHub地址
  • API文档地址

架构设计

项目采用分层架构,清晰地分离了数据模型、渲染逻辑和用户界面:
在这里插入图片描述

ECG12LeadViewer/
├── ECGData.cs              # 数据模型层
├── ECGRenderer.cs          # 渲染引擎层
├── ECGViewerControl.cs     # 控件封装层
├── MainForm.cs            # 用户界面层
└── Program.cs             # 程序入口

核心技术实现

1.新建项目引用SkiaSharp相关的库

<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>WinExe</OutputType><TargetFramework>net8.0-windows10.0.19041.0</TargetFramework><UseWindowsForms>true</UseWindowsForms><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup><ItemGroup><PackageReference Include="SkiaSharp" Version="3.119.0" /><PackageReference Include="SkiaSharp.Views.Desktop.Common" Version="3.119.0" /><PackageReference Include="SkiaSharp.Views.WindowsForms" Version="3.119.0" /></ItemGroup></Project> 

2. 数据模型设计

首先,我们需要定义一个强类型的数据模型来表示12导联心电图数据:

using System;namespace ECG12LeadViewer
{/// <summary>/// 12导联心电图数据/// </summary>public class ECGData{public float[] LeadI { get; set; } = Array.Empty<float>();public float[] LeadII { get; set; } = Array.Empty<float>();public float[] LeadIII { get; set; } = Array.Empty<float>();public float[] aVR { get; set; } = Array.Empty<float>();public float[] aVL { get; set; } = Array.Empty<float>();public float[] aVF { get; set; } = Array.Empty<float>();public float[] V1 { get; set; } = Array.Empty<float>();public float[] V2 { get; set; } = Array.Empty<float>();public float[] V3 { get; set; } = Array.Empty<float>();public float[] V4 { get; set; } = Array.Empty<float>();public float[] V5 { get; set; } = Array.Empty<float>();public float[] V6 { get; set; } = Array.Empty<float>();public int SampleRate { get; set; } = 360; // Hzpublic float Duration { get; set; } = 10.0f; // seconds/// <summary>/// 获取所有导联的数据/// </summary>public Dictionary<string, float[]> GetAllLeads(){return new Dictionary<string, float[]>{{ "I", LeadI },{ "II", LeadII },{ "III", LeadIII },{ "aVR", aVR },{ "aVL", aVL },{ "aVF", aVF },{ "V1", V1 },{ "V2", V2 },{ "V3", V3 },{ "V4", V4 },{ "V5", V5 },{ "V6", V6 }};}}
}

这个设计考虑了医学标准,支持360Hz的采样率,这是心电图设备的常见采样频率。以及通过字典数据获取所有导联的数据。

3. 模拟心电图数据生成

为了测试和演示,我们实现了一个逼真的心电图数据生成器:

using System;namespace ECG12LeadViewer
{/// <summary>/// 12导联心电图数据/// </summary>public class ECGData{//...数据模型设计/// <summary>/// 生成模拟的12导联心电图数据/// </summary>public static ECGData GenerateSimulatedData(){var ecg = new ECGData();int sampleCount = (int)(ecg.SampleRate * ecg.Duration);// 心率约75 BPMdouble heartRate = 75.0;double rPeakInterval = 60.0 / heartRate; // seconds between R peaksint samplesPerBeat = (int)(ecg.SampleRate * rPeakInterval);var random = new Random(42); // 固定种子以获得一致的结果// 为每个导联生成数据ecg.LeadI = GenerateLeadData(sampleCount, samplesPerBeat, 1.0f, random);ecg.LeadII = GenerateLeadData(sampleCount, samplesPerBeat, 1.2f, random);ecg.LeadIII = GenerateLeadData(sampleCount, samplesPerBeat, 0.8f, random);ecg.aVR = GenerateLeadData(sampleCount, samplesPerBeat, -0.5f, random);ecg.aVL = GenerateLeadData(sampleCount, samplesPerBeat, 0.6f, random);ecg.aVF = GenerateLeadData(sampleCount, samplesPerBeat, 1.0f, random);ecg.V1 = GenerateLeadData(sampleCount, samplesPerBeat, 0.4f, random);ecg.V2 = GenerateLeadData(sampleCount, samplesPerBeat, 1.5f, random);ecg.V3 = GenerateLeadData(sampleCount, samplesPerBeat, 1.8f, random);ecg.V4 = GenerateLeadData(sampleCount, samplesPerBeat, 1.6f, random);ecg.V5 = GenerateLeadData(sampleCount, samplesPerBeat, 1.2f, random);ecg.V6 = GenerateLeadData(sampleCount, samplesPerBeat, 0.8f, random);return ecg;}/// <summary>/// 为单个导联生成模拟数据/// </summary>private static float[] GenerateLeadData(int sampleCount, int samplesPerBeat, float amplitude, Random random){var data = new float[sampleCount];for (int i = 0; i < sampleCount; i++){double time = (double)i / 360.0; // 时间(秒)int beatPosition = i % samplesPerBeat;double beatPhase = (double)beatPosition / samplesPerBeat;// 基线噪声float noise = (float)(random.NextDouble() - 0.5) * 0.05f;// 生成ECG波形float ecgValue = GenerateECGWaveform(beatPhase) * amplitude + noise;data[i] = ecgValue;}return data;}/// <summary>/// 生成单个心跳的ECG波形/// </summary>private static float GenerateECGWaveform(double phase){// P波 (0.0 - 0.2)if (phase < 0.2){double pPhase = phase / 0.2;return (float)(0.2 * Math.Sin(Math.PI * pPhase));}// PR间期 (0.2 - 0.35)else if (phase < 0.35){return 0.0f;}// QRS复合波 (0.35 - 0.45)else if (phase < 0.45){double qrsPhase = (phase - 0.35) / 0.1;if (qrsPhase < 0.3) // Q波return (float)(-0.3 * Math.Sin(Math.PI * qrsPhase / 0.3));else if (qrsPhase < 0.7) // R波return (float)(2.0 * Math.Sin(Math.PI * (qrsPhase - 0.3) / 0.4));else // S波return (float)(-0.5 * Math.Sin(Math.PI * (qrsPhase - 0.7) / 0.3));}// ST段 (0.45 - 0.6)else if (phase < 0.6){return 0.0f;}// T波 (0.6 - 0.9)else if (phase < 0.9){double tPhase = (phase - 0.6) / 0.3;return (float)(0.4 * Math.Sin(Math.PI * tPhase));}// 基线 (0.9 - 1.0)else{return 0.0f;}}}
} 

这个算法基于真实的心电图生理学原理,生成包含P波、QRS复合波和T波的完整心跳周期。

4. SkiaSharp渲染引擎

渲染引擎是整个项目的核心,负责将数据转换为可视化的心电图。以下是关键的渲染逻辑:

布局类设计
/// <summary>
/// ECG布局参数
/// </summary>
public class ECGLayout
{public int Margin { get; set; }public int LeadWidth { get; set; }public int LeadHeight { get; set; }public int Rows { get; set; }public int Cols { get; set; }public int GridSpacing { get; set; }
}
ECG渲染器设计
public class ECGRenderer
{private readonly SKPaint _gridPaint;private readonly SKPaint _waveformPaint;private readonly SKPaint _textPaint;private readonly SKFont _skFont;/// <summary>/// 渲染12导联心电图/// </summary>public void Render(SKSurface surface, ECGData ecgData, int width, int height){var canvas = surface.Canvas;canvas.Clear(SKColors.White);	// 计算布局var layout = CalculateLayout(width, height);// 绘制背景canvas.DrawRect(0, 0, width, height, _backgroundPaint);	// 绘制网格DrawGrid(canvas, layout);	// 绘制导联DrawLeads(canvas, ecgData, layout);	// 绘制标签DrawLabels(canvas, layout);}
}
计算布局设计
/// <summary>
/// 计算布局参数
/// </summary>
private ECGLayout CalculateLayout(int width, int height)
{const int margin = 40;const int rows = 4;const int cols = 3;int availableWidth = width - 2 * margin;int availableHeight = height - 2 * margin;int leadWidth = availableWidth / cols;int leadHeight = availableHeight / rows;return new ECGLayout{Margin = margin,LeadWidth = leadWidth,LeadHeight = leadHeight,Rows = rows,Cols = cols,GridSpacing = 20 // 网格间距,代表200ms或0.2mV};
}
医学标准网格绘制

心电图的网格有特定的医学标准:细网格代表0.04秒和0.1mV,粗网格代表0.2秒和0.5mV。我们用SkiaSharp精确实现了这些标准:

/// <summary>
/// 绘制网格
/// </summary>
private void DrawGrid(SKCanvas canvas, ECGLayout layout)
{// 绘制细网格线var fineGridPaint = new SKPaint{Color = SKColor.Parse("#FFE0E0"),StrokeWidth = 0.5f,Style = SKPaintStyle.Stroke};// 绘制粗网格线var coarseGridPaint = new SKPaint{Color = SKColor.Parse("#FFCCCC"),StrokeWidth = 1.0f,Style = SKPaintStyle.Stroke};for (int row = 0; row < layout.Rows; row++){for (int col = 0; col < layout.Cols; col++){float x = layout.Margin + col * layout.LeadWidth;float y = layout.Margin + row * layout.LeadHeight;float w = layout.LeadWidth;float h = layout.LeadHeight;// 绘制导联边框canvas.DrawRect(x, y, w, h, _gridPaint);// 绘制细网格for (float gx = x; gx <= x + w; gx += layout.GridSpacing / 5){canvas.DrawLine(gx, y, gx, y + h, fineGridPaint);}for (float gy = y; gy <= y + h; gy += layout.GridSpacing / 5){canvas.DrawLine(x, gy, x + w, gy, fineGridPaint);}// 绘制粗网格for (float gx = x; gx <= x + w; gx += layout.GridSpacing){canvas.DrawLine(gx, y, gx, y + h, coarseGridPaint);}for (float gy = y; gy <= y + h; gy += layout.GridSpacing){canvas.DrawLine(x, gy, x + w, gy, coarseGridPaint);}}}fineGridPaint.Dispose();coarseGridPaint.Dispose();
}
绘制导联标签
/// <summary>
/// 绘制导联标签
/// </summary>
private void DrawLabels(SKCanvas canvas, ECGLayout layout)
{var leadNames = new[] { "I", "aVR", "V1", "V4", "II", "aVL", "V2", "V5", "III", "aVF", "V3", "V6" };for (int i = 0; i < leadNames.Length; i++){int row = i / layout.Cols;int col = i % layout.Cols;float x = layout.Margin + col * layout.LeadWidth + 10;float y = layout.Margin + row * layout.LeadHeight + 20;canvas.DrawText(leadNames[i], x, y,_skFont,_textPaint);}
}
波形路径优化

对于大量的心电图数据点,我们使用SKPath来优化绘制性能:

/// <summary>
/// 绘制单个导联
/// </summary>
private void DrawSingleLead(SKCanvas canvas, float[] data, float x, float y, float width, float height, int sampleRate)
{if (data.Length == 0) return;var path = new SKPath();// 计算显示的数据范围(显示2.5秒的数据)float displayDuration = 2.5f; // secondsint displaySamples = (int)(sampleRate * displayDuration);int startIndex = Math.Max(0, data.Length - displaySamples);int endIndex = data.Length;// 计算缩放参数float timeScale = width / displayDuration;float amplitudeScale = height / 4.0f; // 假设±2mV的范围// 基线位置float baseline = y + height / 2;bool firstPoint = true;for (int i = startIndex; i < endIndex; i++){float time = (float)(i - startIndex) / sampleRate;float px = x + time * timeScale;float py = baseline - data[i] * amplitudeScale;if (firstPoint){path.MoveTo(px, py);firstPoint = false;}else{path.LineTo(px, py);}}canvas.DrawPath(path, _waveformPaint);path.Dispose();
}

5. 实时动画系统

为了模拟真实的心电监护仪效果,我们实现了一个基于滑动窗口的实时动画系统:

using SkiaSharp;
using SkiaSharp.Views.Desktop;
using System.ComponentModel;namespace ECG12LeadViewer
{/// <summary>/// ECG查看器控件/// </summary>public class ECGViewerControl : SKControl{private ECGData? _ecgData;private ECGRenderer? _renderer;private System.Windows.Forms.Timer? _animationTimer;private int _currentSample = 0;[Browsable(false)]public ECGData? ECGData{get => _ecgData;set{_ecgData = value;_currentSample = 0;Invalidate();}}[Browsable(true)][Description("是否启用实时动画效果")]public bool EnableAnimation { get; set; } = true;[Browsable(true)][Description("动画更新间隔(毫秒)")]public int AnimationInterval { get; set; } = 50;public ECGViewerControl(){_renderer = new ECGRenderer();InitializeAnimation();}private void InitializeAnimation(){_animationTimer = new System.Windows.Forms.Timer();_animationTimer.Interval = AnimationInterval;_animationTimer.Tick += OnAnimationTick;}private void OnAnimationTick(object? sender, EventArgs e){if (_ecgData != null && EnableAnimation){_currentSample += 5; // 每次前进5个样本点if (_currentSample >= _ecgData.LeadI.Length){_currentSample = 0; // 循环播放}Invalidate();}}protected override void OnPaintSurface(SKPaintSurfaceEventArgs e){base.OnPaintSurface(e);if (_renderer == null) return;var surface = e.Surface;var canvas = surface.Canvas;var info = e.Info;canvas.Clear(SKColors.White);if (_ecgData != null){// 如果启用动画,创建当前时刻的数据快照var displayData = EnableAnimation ? CreateAnimatedData(_ecgData, _currentSample) : _ecgData;_renderer.Render(surface, displayData, info.Width, info.Height);}else{// 绘制占位符DrawPlaceholder(canvas, info.Width, info.Height);}}/// <summary>/// 创建动画数据(模拟实时显示)/// </summary>private ECGData CreateAnimatedData(ECGData originalData, int currentSample){// 显示2.5秒的数据窗口int windowSize = (int)(originalData.SampleRate * 2.5f);int startIndex = Math.Max(0, currentSample - windowSize);int endIndex = Math.Min(originalData.LeadI.Length, currentSample);var animatedData = new ECGData{SampleRate = originalData.SampleRate,Duration = (float)(endIndex - startIndex) / originalData.SampleRate};// 复制窗口内的数据animatedData.LeadI = CopyDataWindow(originalData.LeadI, startIndex, endIndex);animatedData.LeadII = CopyDataWindow(originalData.LeadII, startIndex, endIndex);animatedData.LeadIII = CopyDataWindow(originalData.LeadIII, startIndex, endIndex);animatedData.aVR = CopyDataWindow(originalData.aVR, startIndex, endIndex);animatedData.aVL = CopyDataWindow(originalData.aVL, startIndex, endIndex);animatedData.aVF = CopyDataWindow(originalData.aVF, startIndex, endIndex);animatedData.V1 = CopyDataWindow(originalData.V1, startIndex, endIndex);animatedData.V2 = CopyDataWindow(originalData.V2, startIndex, endIndex);animatedData.V3 = CopyDataWindow(originalData.V3, startIndex, endIndex);animatedData.V4 = CopyDataWindow(originalData.V4, startIndex, endIndex);animatedData.V5 = CopyDataWindow(originalData.V5, startIndex, endIndex);animatedData.V6 = CopyDataWindow(originalData.V6, startIndex, endIndex);return animatedData;}private float[] CopyDataWindow(float[] sourceData, int startIndex, int endIndex){if (startIndex >= endIndex || startIndex >= sourceData.Length)return Array.Empty<float>();int length = Math.Min(endIndex - startIndex, sourceData.Length - startIndex);var result = new float[length];Array.Copy(sourceData, startIndex, result, 0, length);return result;}/// <summary>/// 绘制占位符/// </summary>private void DrawPlaceholder(SKCanvas canvas, int width, int height){SKFont skFont = new SKFont{Size = 24,};SKTextAlign textAlign = SKTextAlign.Center;using var paint = new SKPaint{Color = SKColors.Gray,IsAntialias = true,};string text = "加载ECG数据...";canvas.DrawText(text, width / 2, height / 2,textAlign, skFont, paint);}/// <summary>/// 开始播放动画/// </summary>public void StartAnimation(){if (EnableAnimation && _animationTimer != null){_animationTimer.Start();}}/// <summary>/// 停止播放动画/// </summary>public void StopAnimation(){if (_animationTimer != null){_animationTimer.Stop();}}/// <summary>/// 重置到开始位置/// </summary>public void Reset(){_currentSample = 0;Invalidate();}protected override void Dispose(bool disposing){if (disposing){_animationTimer?.Stop();_animationTimer?.Dispose();_renderer?.Dispose();}base.Dispose(disposing);}}
} 

6. 主页面设计

1. 标准医学布局

12导联按照医学标准排列:

|   I   |  aVR  |  V1   |  V4   |
|  II   |  aVL  |  V2   |  V5   |
|  III  |  aVF  |  V3   |  V6   |
2. 直观的控制界面
  • 播放控制: 播放、停止、重置按钮
  • 动画开关: 可以禁用动画查看静态图像
  • 速度调节: 实时调整播放速度
  • 数据加载: 支持外部数据文件加载
3. 响应式布局

界面能够适应不同的窗口大小,保持最佳的显示效果。

4. 控件布局设计和事件实现
using System;
using System.Drawing;
using System.Windows.Forms;namespace ECG12LeadViewer
{/// <summary>/// 主窗体/// </summary>public partial class MainForm : Form{private ECGViewerControl? _ecgViewer;private Button? _loadDataButton;private Button? _playButton;private Button? _stopButton;private Button? _resetButton;private CheckBox? _animationCheckBox;private TrackBar? _speedTrackBar;private Label? _speedLabel;private Panel? _controlPanel;public MainForm(){InitializeComponent();LoadSimulatedData();}private void InitializeComponent(){SuspendLayout();// 设置窗体属性Text = "12导联心电图查看器 - SkiaSharp";Size = new Size(1200, 800);StartPosition = FormStartPosition.CenterScreen;MinimumSize = new Size(800, 600);// 创建控制面板_controlPanel = new Panel{Dock = DockStyle.Top,Height = 80,BackColor = SystemColors.Control};// 创建按钮_loadDataButton = new Button{Text = "加载数据",Location = new Point(10, 10),Size = new Size(80, 30)};_loadDataButton.Click += OnLoadDataClick;_playButton = new Button{Text = "播放",Location = new Point(100, 10),Size = new Size(60, 30)};_playButton.Click += OnPlayClick;_stopButton = new Button{Text = "停止",Location = new Point(170, 10),Size = new Size(60, 30)};_stopButton.Click += OnStopClick;_resetButton = new Button{Text = "重置",Location = new Point(240, 10),Size = new Size(60, 30)};_resetButton.Click += OnResetClick;// 创建动画复选框_animationCheckBox = new CheckBox{Text = "启用动画",Location = new Point(320, 15),Size = new Size(80, 20),Checked = true};_animationCheckBox.CheckedChanged += OnAnimationCheckChanged;// 创建速度控制_speedLabel = new Label{Text = "播放速度:",Location = new Point(420, 15),Size = new Size(70, 20)};_speedTrackBar = new TrackBar{Location = new Point(500, 10),Size = new Size(150, 30),Minimum = 1,Maximum = 10,Value = 5,TickStyle = TickStyle.BottomRight,TickFrequency = 1};_speedTrackBar.ValueChanged += OnSpeedChanged;// 添加控件到控制面板_controlPanel.Controls.AddRange(new Control[] { _loadDataButton, _playButton, _stopButton, _resetButton, _animationCheckBox, _speedLabel, _speedTrackBar });// 创建ECG查看器_ecgViewer = new ECGViewerControl{Dock = DockStyle.Fill,EnableAnimation = true,AnimationInterval = 100};// 添加控件到窗体Controls.Add(_ecgViewer);Controls.Add(_controlPanel);ResumeLayout(false);}private void LoadSimulatedData(){try{var ecgData = ECGData.GenerateSimulatedData();if (_ecgViewer != null){_ecgViewer.ECGData = ecgData;}}catch (Exception ex){MessageBox.Show($"加载模拟数据时出错: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);}}private void OnLoadDataClick(object? sender, EventArgs e){using var openFileDialog = new OpenFileDialog{Filter = "所有文件 (*.*)|*.*",Title = "选择ECG数据文件"};if (openFileDialog.ShowDialog() == DialogResult.OK){try{// 这里可以添加实际的文件加载逻辑// 目前使用模拟数据LoadSimulatedData();MessageBox.Show("数据加载成功!\n(当前显示模拟数据)", "信息", MessageBoxButtons.OK, MessageBoxIcon.Information);}catch (Exception ex){MessageBox.Show($"加载文件时出错: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);}}}private void OnPlayClick(object? sender, EventArgs e){_ecgViewer?.StartAnimation();UpdateButtonStates(true);}private void OnStopClick(object? sender, EventArgs e){_ecgViewer?.StopAnimation();UpdateButtonStates(false);}private void OnResetClick(object? sender, EventArgs e){_ecgViewer?.Reset();UpdateButtonStates(false);}private void OnAnimationCheckChanged(object? sender, EventArgs e){if (_ecgViewer != null && _animationCheckBox != null){_ecgViewer.EnableAnimation = _animationCheckBox.Checked;if (!_animationCheckBox.Checked){_ecgViewer.StopAnimation();UpdateButtonStates(false);}}}private void OnSpeedChanged(object? sender, EventArgs e){if (_ecgViewer != null && _speedTrackBar != null){// 将滑块值转换为时间间隔(值越大,间隔越小,速度越快)int interval = 200 - (_speedTrackBar.Value * 15);_ecgViewer.AnimationInterval = Math.Max(20, interval);}}private void UpdateButtonStates(bool isPlaying){if (_playButton != null) _playButton.Enabled = !isPlaying;if (_stopButton != null) _stopButton.Enabled = isPlaying;}protected override void OnFormClosing(FormClosingEventArgs e){_ecgViewer?.StopAnimation();base.OnFormClosing(e);}}
} 

性能优化策略

1. 内存管理

在图形密集型应用中,内存管理至关重要:

public void Dispose()
{_gridPaint?.Dispose();_waveformPaint?.Dispose();_textPaint?.Dispose();_backgroundPaint?.Dispose();
}

2. 按需渲染

只在数据更新时触发重绘,避免不必要的CPU消耗:

public ECGData? ECGData
{get => _ecgData;set{_ecgData = value;_currentSample = 0;Invalidate(); // 只在数据变化时重绘}
}

3. 硬件加速

SkiaSharp的一个重要优势是支持GPU硬件加速,特别是在绘制大量数据点时能显著提升性能。

扩展性考虑

1. 文件格式支持

项目架构支持轻松添加各种医学数据格式:

  • EDF (European Data Format)
  • MIT-BIH数据库格式
  • WFDB (Waveform Database)
  • HL7 FHIR标准

2. 信号处理集成

可以集成各种信号处理算法:

public interface IECGProcessor
{float[] ApplyFilter(float[] rawData, FilterType type);QRSComplex[] DetectQRS(float[] data);DiagnosticResult AnalyzeRhythm(ECGData data);
}

3. 云端集成

支持与云端医疗系统集成,实现远程监护和数据同步。

部署与测试

运行命令

dotnet restore
dotnet build
dotnet run

在这里插入图片描述
生成的动画
在这里插入图片描述

总结

通过这个项目,我们可以深刻体会到SkiaSharp在医学软件开发中的巨大潜力。它不仅提供了出色的性能和灵活性,还让我们能够创建真正专业级的医学数据可视化应用。SkiaSharp的强大功能: 从基础绘图到复杂动画,一站式解决方案。希望这个项目和分享的经验能够帮助更多开发者在医疗软件领域探索和创新。


http://www.hkcw.cn/article/WyJBVGMpdm.shtml

相关文章

24位高精度数据采集卡NET8860音频振动信号采集监测满足自动化测试应用现场的多样化需求

NET8860 高分辨率数据采集卡技术解析 阿尔泰科技的NET8860是一款高性能数据采集卡&#xff0c;具备8路同步模拟输入通道和24bit分辨率&#xff0c;适用于高精度信号采集场景。其输入量程覆盖10V、5V、2V、1V&#xff0c;采样速率高达256KS/s&#xff0c;能够满足多种工业与科研…

2025年05月30日Github流行趋势

项目名称&#xff1a;agenticSeek 项目地址url&#xff1a;https://github.com/Fosowl/agenticSeek项目语言&#xff1a;Python历史star数&#xff1a;13040今日star数&#xff1a;1864项目维护者&#xff1a;Fosowl, steveh8758, klimentij, ganeshnikhil, apps/copilot-pull-…

PCB设计实践(三十一)PCB设计中机械孔的合理设计与应用指南

一、机械孔的基本概念与分类 机械孔是PCB设计中用于实现机械固定、结构支撑、散热及电气连接的关键结构元件&#xff0c;其分类基于功能特性、制造工艺和应用场景的差异&#xff0c;主要分为以下几类&#xff1a; 1. 金属化机械孔 通过电镀工艺在孔内壁形成导电层&#xff0c;…

TC/BC/OC P2P/E2E有啥区别?-PTP协议基础概念介绍

前言 时间同步网络中的每个节点&#xff0c;都被称为时钟&#xff0c;PTP协议定义了三种基本时钟节点。本文将介绍这三种类型的时钟&#xff0c;以及gPTP在同步机制上与其他机制的区别 本系列文章将由浅入深的带你了解gPTP&#xff0c;欢迎关注 时钟类型 在PTP中我们将各节…

五.MySQL表的约束

1.not null空属性 和 default缺省值 两个值&#xff1a;null&#xff08;默认的&#xff09;和not null(不为空) 元素可以分为两类 1.not null 不能为空的&#xff0c;这种没有默认default 要手动设定&#xff0c;我们必须插入数据而且不能为NULL。但我们插入数据有两种方式 1.…

4.Haproxy搭建Web群集

一.案例分析 1.案例概述 Haproxy是目前比较流行的一种群集调度工具&#xff0c;同类群集调度工具有很多&#xff0c;包括LVS、Nginx&#xff0c;LVS性能最好&#xff0c;但是搭建相对复杂&#xff1b;Nginx的upstream模块支持群集功能&#xff0c;但是对群集节点健康检查功能…

NewsNow:免费好用的实时新闻聚合平台,让信息获取更优雅(深度解析、部署攻略)

名人说&#xff1a;博观而约取&#xff0c;厚积而薄发。——苏轼《稼说送张琥》 创作者&#xff1a;Code_流苏(CSDN)&#xff08;一个喜欢古诗词和编程的Coder&#x1f60a;&#xff09; 目录 一、NewsNow项目概览1. 项目核心亮点2. 技术架构特点 二、核心功能深度解析1. 智能新…

论文阅读笔记——FLOW MATCHING FOR GENERATIVE MODELING

Flow Matching 论文 扩散模型&#xff1a;根据中心极限定理&#xff0c;对原始图像不断加高斯噪声&#xff0c;最终将原始信号破坏为近似的标准正态分布。这其中每一步都构造为条件高斯分布&#xff0c;形成离散的马尔科夫链。再通过逐步去噪得到原始图像。 Flow matching 采取…

【leetcode】02.07. 链表相交

链表相交 题目代码1. 计算两个链表的长度2. 双指针 题目 02.07. 链表相交 给你两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点&#xff0c;返回 null 。 图示两个链表在节点 c1 开始相交&#xff1a; 代码 …

文字转图片的字符画生成工具

软件介绍 今天要介绍的这款软件可以将文字转换成图片的排列形式&#xff0c;非常适合需要将文字图形化的场景&#xff0c;建议有需要的朋友收藏。 软件名称与用途 这款软件名为《字符画大师》&#xff0c;是一款在网吧等场所非常流行的聊天辅助工具&#xff0c;其主要功能就…

Bitlocker密钥提取之SYSTEM劫持

该漏洞编号CVE-2024-20666&#xff0c;本文实现复现过程&#xff0c;Windows系统版本如下 简介 从Windows10&#xff08;th1&#xff09;开始&#xff0c;微软在winload模块中&#xff0c;增加了systemdatadevice字段值的获取&#xff0c;该字段值存储在BCD引导配置文件中。当…

明场检测与暗场检测的原理

知识星球里的学员问&#xff1a;明场检测与暗场检测原理上有什么区别&#xff1f; 如上图&#xff0c; 明场检测&#xff08;Bright-field Inspection&#xff09; 工作原理&#xff1a; 光线从近乎垂直照射到样品表面。 如果表面平整、无缺陷&#xff0c;光线会直接反射回镜…

STL解析——vector的使用及模拟实现

目录 1.使用篇 1.1默认成员函数 1.2其他常用接口 2.模拟实现 2.1源码逻辑参考 2.2基本函数实现 2.3增 2.4删 2.5迭代器失效 2.6拷贝构造级其他接口 2.7赋值运算符重载(现代写法) 2.8深层次拷贝优化 3.整体代码 在C中vector算正式STL容器&#xff0c;功能可以类比于…

day2实训

实训任务1 FTPASS wireshark打开 实训任务2 数据包中的线索 解码的图片 实训任务3 被嗅探的流量 过滤http&#xff0c;追踪post的http流 实训任务6 小明的保险箱 winhex打开

Window10+ 安装 go环境

一、 下载 golang 源码&#xff1a; 去官网下载&#xff1a; https://go.dev/dl/ &#xff0c;当前时间&#xff08;2025-05&#xff09;最新版本如下: 二、 首先在指定的磁盘下创建几个文件夹 比如在 E盘创建 software 文件夹 E:\SoftWare,然后在创建如下几个文件夹 E:\S…

8.5 Q1|广州医科大学CHARLS发文 甘油三酯葡萄糖指数累积变化与 0-3期心血管-肾脏-代谢综合征人群中风发生率的相关性

1.第一段-文章基本信息 文章题目&#xff1a;Association between cumulative changes of the triglyceride glucose index and incidence of stroke in a population with cardiovascular-kidney-metabolic syndrome stage 0-3: a nationwide prospective cohort study 中文标…

重读《人件》Peopleware -(13)Ⅱ 办公环境 Ⅵ 电话

当你开始收集有关工作时间质量的数据时&#xff0c;你的注意力自然会集中在主要的干扰源之一——打进来的电话。一天内接15个电话并不罕见。虽然这看似平常&#xff0c;但由于重新沉浸所需的时间&#xff0c;它可能会耗尽你几乎一整天的时间。当一天结束时&#xff0c;你会纳闷…

ARXML解析与可视化工具

随着汽车电子行业的快速发展,AUTOSAR标准在车辆软件架构中发挥着越来越重要的作用。然而,传统的ARXML文件处理工具往往存在高昂的许可费用、封闭的数据格式和复杂的使用门槛等问题。本文介绍一种基于TXT格式输出的ARXML解析方案,为开发团队提供了一个高效的替代解决方案。 …

C#中数据绑定的简单例子

数据绑定允许将控件的属性和数据链接起来——控件属性值发生改变&#xff0c;会导致数据跟着自动改变。 数据绑定还可以是双向的——控件属性值发生改变&#xff0c;会导致数据跟着自动改变&#xff1b;数据发生改变&#xff0c;也会导致控件属性值跟着自动改变。 1、数据绑定…

训练和测试的规范写法

单通道图片的规范写法 import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader import matplotlib.pyplot as plt import numpy as np# 设置中文字体支持 plt.rcParams[&quo…