1. Введение

В современной медицине точное отображение электрокардиограммы (ЭКГ) играет ключевую роль в диагностике и мониторинге сердечно-сосудистых заболеваний. Разработка специализированного графика для визуализации ЭКГ в реальном времени на мобильных устройствах требует не только глубокого понимания медицинских стандартов, но и тщательного выбора технологий для реализации. В статье мы рассмотрим процесс создания такого графика с использованием технологии Canvas, обсудим возникшие проблемы и найденные решения.

2. Выбор технологии: почему Canvas?

При разработке графика ЭКГ выбор Canvas в качестве основной технологии был обусловлен несколькими ключевыми факторами:

  1. Высокая точность отображения: Canvas позволяет контролировать отрисовку на уровне пикселей, что критически важно для соответствия стандартной миллиметровой сетке ЭКГ.

  2. Эффективность в отрисовке линейных элементов: ЭКГ-сигнал представляет собой набор соединенных линий, и Canvas предоставляет оптимизированные методы для их отрисовки.

  3. Полный контроль над процессом отрисовки: Есть возможность настроить отображение различных элементов графика (оси, сетка, сигнал) в соответствии с требуемыми медицинскими стандартами.

  4. Гибкость в реализации интерактивности: Canvas облегчает реализацию динамического обновления графика, масштабирования и перемещения при помощи жестов.

  5. Оптимизация производительности: Во время длительных сеансов записи накапливается большой объем информации, но благодаря собственной реализации графика мы можем точно контролировать расход памяти и рисовать только видимую его часть (см. gif ниже).

Использование Canvas обеспечивает необходимый баланс между точностью отображения, производительностью и гибкостью разработки, что делает его оптимальным выбором для создания специализированного графика ЭКГ.

Архивная запись первых попыток нарисовать график. Здесь для отрисовки использовался метод DrawPatch.
Архивная запись первых попыток нарисовать график. Здесь для отрисовки использовался метод DrawPatch.

3. Первоначальная реализация графика ЭКГ

Базовый код для отрисовки графика
// scaledPointSize - зависит от DPI

for (int i = position, valuesCount, i < _ecg.Count 
	 && valuesCount * scaledPointSize < Width; valuesCount++, i++)
{
	ECGSample sample = _ecg[i];
	
	float sampleX = (float)(valuesCount * scaledPointSize);
	
	float sampleY = graphCenter - (sample.Value * scaledPointSize);
	
	chartLines.AddRange(new [] { prevSampleX, prevSampleY, sampleX, sampleY });
	
	prevSampleX = sampleX;
	prevSampleY = sampleY;
}

canvas.DrawLines(chartLines.ToArray(), Style.GraphPaint);

Код создает массив из набора линий, из которых состоит график. Затем одним вызовом canvas.DrawLines отправляет его на отрисовку.

Код отображения сетки
for (int y = 0; y < Height + microCellSize; y += microCellSize)
{    
    microCellsLines.AddRange(new float[] { 0, y, Width, y });
}


for (int x = 0; x < Width + microCellSize; x += microCellSize)
{
    microCellsLines.AddRange(new float[] { x, 0, x, Height });
}


for (int y = 0; y < Height + cellSize; y += cellSize)
{    
    cellsLines.AddRange(new float[] { 0, y, Width, y });
}


for (int x = 0; x < Width + cellSize; x += cellSize)
{
    cellsLines.AddRange(new float[] { x, 0, x, Height });
}

canvas.DrawLines(microCellsLines.ToArray(), Style.MicroCellPaint);
canvas.DrawLines(cellsLines.ToArray(), Style.CellPaint);

Код создает два набора линий для большой сетки (одна клетка — 5 миллиметров) и для маленькой (одна клетка — 1 миллиметр). Затем происходит отрисовка при помощи DrawLines.

P.S. В данном примере показан сам механизм отрисовки без учета DPI.

Screenshot_2024_10_03_13_11_40_00_465b898a334a324c899aef6963b81999
На скриноште представлена отрисовка сетки и графика при помощи метода DrawLines.

4. Отрисовка и синхронизация дополнительного графика

Одной из интересных задач стало отображение дополнительного графика, который должен двигаться синхронно с основным графиком ЭКГ, несмотря на отличающуюся частоту дискретизации. Решение заключалось в привязке значений второго графика к точкам графика ЭКГ:

Код
// scaledPointSize - зависит от DPI

for (int i = position, valuesCount, i < _ecg.Count && valuesCount * scaledPointSize < Width; valuesCount++, i++) 
{ 

ECGSample sample = _ecg[i];

float sampleX = (float)(valuesCount * scaledPointSize);

float sampleY = graphCenter - (sample.Value * scaledPointSize);

chartLines.AddRange(new [] { prevSampleX, prevSampleY, sampleX, sampleY });

prevSampleX = sampleX;
prevSampleY = sampleY;

if (sample.ExtraChartValue != -1)  
{  
    float extraChartSampleY = CalculateExtraChartY(sample.ExtraChartValue);  
  
    extraChartLines.AddRange(new[] { 
    prevExtraChartSampleX, 
    prevExtraChartSampleY, 
    sampleX, // берем X координату от основого графика, тем самым достагаем синхронизации
    extraChartSampleY });  
  
    prevExtraChartSampleY = extraChartSampleY;    
    prevExtraChartSampleX = sampleX;
}
                                                                                    
}

canvas.DrawLines(chartLines.ToArray(), Style.GraphPaint);
canvas.DrawLines(extraChartLines.ToArray(), Style.ExtraChartPaint);

Результат
На скриншоте представлен дополнительный график. Его движение синхронизировано с графиком ЭКГ
На скриншоте представлен дополнительный график. Его движение синхронизировано с графиком ЭКГ

5. Оптимизация производительности

В процессе разработки графика ЭКГ одной из ключевых задач стала оптимизация производительности отрисовки. Изначальная реализация с использованием метода DrawPatch показала недостаточную эффективность при работе с большими объемами данных ЭКГ. В ходе исследования и экспериментов был осуществлен переход к методу DrawLines, что привело к значительному повышению производительности.

Причины перехода:

  1. Ограничения DrawPatch:

    • Метод DrawPatch, несмотря на свою универсальность, оказался недостаточно оптимизированным для специфики отрисовки графика ЭКГ.

    • При большом количестве точек данных производительность начинала существенно падать.

  2. Преимущества DrawLines:

    • В результате поиска альтернативных решений было обнаружено, что метод DrawLines использует аппаратную акселерацию.

    • Аппаратная акселерация позволяет задействовать возможности графического процессора для ускорения отрисовки линий.

Результаты оптимизации:

  • После перехода на метод DrawLines производительность отрисовки графика ЭКГ выросла в два с половиной раза. (см. результаты бенчмарка ниже)

  • Рост производительности позволил обрабатывать и отображать большие объемы данных ЭКГ с высокой частотой обновления, что критически важно для точного отображения динамики сердечного ритма в реальном времени.

Бенчмарк
// Бенчмарк, отрисока 200 точек графика 1000 раз

Stopwatch watchDrawLines = new Stopwatch();  
Stopwatch watchDrawPath = new Stopwatch();

for (int r = 0; r < 1000; r++)
{
	for (int i = position, valuesCount, i < _ecg.Count 
		 && valuesCount * scaledPointSize < Width; valuesCount++, i++)
	{
		ECGSample sample = _ecg[i];
		
		float sampleX = (float)(valuesCount * scaledPointSize);
		
		float sampleY = graphCenter - (sample.Value * scaledPointSize);
		
		watchDrawLines.Start();  
		chartLines.AddRange(new [] { prevSampleX, prevSampleY, sampleX, sampleY });
		watchDrawLines.Stop();
		
		watchDrawPath.Start();  
		chartPath.LineTo(sampleX, sampleY);  
		watchDrawPath.Stop();
		
		prevSampleX = sampleX;
		prevSampleY = sampleY;
	}
	
	watchDrawLines.Start();  
	canvas.DrawLines(chartLines.ToArray(), Style.GraphPaint);  
	watchDrawLines.Stop();
	
	watchDrawPath.Start();  
	canvas.DrawPath(chartPath, Style.GraphPaint);  
	watchDrawPath.Stop();
}

Debug.WriteLine($"Draw lines time: {watchDrawLines.Elapsed.TotalMilliseconds}");
Debug.WriteLine($"Draw path time: {watchDrawPath.Elapsed.TotalMilliseconds}");

// Output:
// Draw lines time: 334.0327
// Draw path time: 846.5474

Код создает два набора линий для большой сетки (одна клетка — 5 миллиметров) и для маленькой (одна клетка — 1 миллиметр). Затем происходит отрисовка при помощи DrawLines.

6. Реализация интерактивности

Для реализации интерактивности мы не использовали стандартные решения или сторонние библиотеки. Поиск и интеграция готового решения, удовлетворяющего всем нашим требованиям, могли занять больше времени, чем разработка собственного. Кроме того, с помощью собственного решения мы добились большей гибкости, что было крайне важно с учетом специфики проекта. В результате был использован стандартный event Touch и добавлена поддержка как простых жестов одним касанием, так и мультитач-жестов.

Обработка события OnTouch
private void OnTouch(object? sender, TouchEventArgs e)
{
	if (e.Event == null)
		return;
	
	if (e.Event.PointerCount == 2)
	{
		StopSingleTouchEventHandling();
		HandleTwoTouches(e.Event);
	}
	else if (e.Event.PointerCount == 1)
	{
		StopTwoTouchesEventHandling();
		HandleSingleTouch(e.Event);
	}
}

Обработка единичных касаний

private void HandleSingleTouch(MotionEvent touchEvent)
{
    if (PinchGestureStarted)
        return;

    if (touchEvent.Action == MotionEventActions.Down && !MoveGestureStarted)
    {
        _touchXPosition = touchEvent.GetX();
        _touchYPosition = touchEvent.GetY();
    }
    else if (MoveGestureStarted && touchEvent.Action == MotionEventActions.Move)
    {
        float deltaX = (_touchXPosition.Value - touchEvent.GetX());
        float deltaY = (_touchYPosition.Value - touchEvent.GetY());

	// Далее на основе полученных дельт вычисляется сдвиг графика под двум осям
    }
}

Обработка жестов для зума

private void HandleTwoTouches(MotionEvent touchEvent)
{
    PointF GetPinchGestureDelta(PointF start1, PointF start2, PointF end1, PointF end2)
    {
        float initialDistanceX = Math.Abs(start2.X - start1.X);
        float initialDistanceY = Math.Abs(start2.Y - start1.Y);

        float finalDistanceX = Math.Abs(end2.X - end1.X);
        float finalDistanceY = Math.Abs(end2.Y - end1.Y);

        float deltaX = finalDistanceX / initialDistanceX;
        float deltaY = finalDistanceY / initialDistanceY;

        return new PointF(deltaX, deltaY);
    }

    int point1Id = touchEvent.GetPointerId(0);
    int point2Id = touchEvent.GetPointerId(1);

    PointF point1 = new PointF(touchEvent.GetX(point1Id), touchEvent.GetY(point1Id));
    
    PointF point2 = new PointF(touchEvent.GetX(point2Id), touchEvent.GetY(point2Id));

    if ((touchEvent.Action == MotionEventActions.Pointer1Down || touchEvent.Action == MotionEventActions.Pointer2Down) && !PinchGestureStarted)
    {
        _startPoint1 = point1;
        _startPoint2 = point2;
        _startSampleHorizontalScale = SampleHorizontalScale;
        _startSampleVerticalScale = SampleVerticalScale;
    }
    else if (touchEvent.Action == MotionEventActions.Move && PinchGestureStarted)
    {
        PointF ratios = GetPinchGestureDelta(_startPoint1.Value, _startPoint2.Value, point1, point2);

        SampleVerticalScale = _startSampleVerticalScale.Value * ratios.Y;
        SampleHorizontalScale = _startSampleHorizontalScale.Value * ratios.X;
    }
    else if (touchEvent.Action == MotionEventActions.Pointer1Up || touchEvent.Action == MotionEventActions.Pointer2Up && PinchGestureStarted)
    {
        StopTwoTouchesEventHandling();
    }
}

Код по двум точкам определяет вертикальную и горизонтальную дельту, отталкиваясь от начальных точек соприкосновения. Далее по этим дельтам происходит масштабирование графика.

7. Тестирование и валидация

Одним из критических аспектов разработки специализированного графика ЭКГ является обеспечение точности отображения данных. Для проверки соответствия отображаемой миллиметровой сетки реальным физическим размерам был разработан следующий подход:

Первоначальный метод проверки и его ограничения:

  1. Использование физической линейки:

    • Изначально была предпринята попытка измерения с помощью обычной физической линейки.

    • Выявленные проблемы:

      • Сенсорный экран реагировал на касания линейки, что мешало точным измерениям.

      • Сложность в обеспечении точного позиционирования линейки на экране.

Использование виртуальной линейки:

  1. Применение виртуальной линейки:

    • Было найдено приложение On-screen Ruler, которое отображает виртуальную линейку непосредственно на экране устройства.

    • Преимущества метода:

      • Линейка основывается на DPI устройства, что обеспечивает точность измерений.

      • Гибкие настройки позволяют корректировать отображение делений линейки.

      • Удобство позиционирования виртуальной линейки на экране устройства

На скриншоте представлен неоткалиброванный график.
На скриншоте представлен неоткалиброванный график.

Процесс валидации графика:

  1. Калибровка виртуальной линейки:

    • Сравнение виртуальной линейки с физической для подтверждения точности отображения.

    • При необходимости, настройка параметров виртуальной линейки для обеспечения соответствия физическим размерам.

  2. Проверка отображения миллиметровки:

    • Использование откалиброванной виртуальной линейки для измерения расстояний между линиями сетки на графике ЭКГ.

    • Проверка соответствия одного деления сетки одному миллиметру.

  3. Валидация масштаба графика:

    • Измерение амплитуды и временных интервалов ЭКГ-сигнала с помощью виртуальной линейки.

    • Сравнение полученных значений с ожидаемыми стандартными параметрами ЭКГ.

8. Заключение

В ходе разработки специализированного графика для отображения ЭКГ с использованием Canvas удалось достичь следующих ключевых результатов:

  1. Точность отображения:

    • Реализовано точное отображение графика ЭКГ в соответствии с медицинскими нормами.

    • Достигнуто корректное отображение миллиметровой сетки, что критически важно для интерпретации ЭКГ.

  2. Высокая производительность:

    • График отрисовывается плавно даже на устройствах с ограниченными ресурсами.

    • Оптимизация с использованием метода DrawLines вместо DrawPatch позволила увеличить производительность в два с половиной раза.

  3. Гибкость и расширяемость:

    • Успешно реализована синхронизация основного графика ЭКГ с дополнительным графиком, имеющим меньшую частоту дискретизации.

    • Разработанная архитектура позволяет легко добавлять новые функции и графики.

  4. Точность измерений:

    • Разработан и применен эффективный метод проверки точности отображения с использованием виртуальной линейки (On-screen Ruler).

    • Обеспечено соответствие отображаемых данных реальным физическим размерам.

  5. Оптимизация для различных устройств:

    • График корректно отображается на устройствах с разными разрешениями экрана и DPI.

Разработанное решение представляет собой надежный и эффективный инструмент для визуализации ЭКГ, который легко адаптируется к различным медицинским приложениям и может быть использован в исследованиях в области кардиологии.

Комментарии (2)


  1. BitWood
    19.12.2024 04:41

    Неплохо получилось. Надо попробовать Ваш код с OpenSilver совместить.


  1. KEugene
    19.12.2024 04:41

    Было бы интересно узнать, почему вообще стал этот вопрос. Сам недавно с больницы и видел этих аппаратов несколько моделей. Но ни один из них даже близко не попадал под категорию "мобильное устройство". Даже тот, что был в машине Emergency. Кроме того, 100% из них рисовали диаграмму в реальном времени. То есть, изначально не было массива X, Y. Перерисовывать линию с добавлением каждой новой пары координат, думаю, будет не эффективно.