1. Введение
В современной медицине точное отображение электрокардиограммы (ЭКГ) играет ключевую роль в диагностике и мониторинге сердечно-сосудистых заболеваний. Разработка специализированного графика для визуализации ЭКГ в реальном времени на мобильных устройствах требует не только глубокого понимания медицинских стандартов, но и тщательного выбора технологий для реализации. В статье мы рассмотрим процесс создания такого графика с использованием технологии Canvas, обсудим возникшие проблемы и найденные решения.
2. Выбор технологии: почему Canvas?
При разработке графика ЭКГ выбор Canvas в качестве основной технологии был обусловлен несколькими ключевыми факторами:
-
Высокая точность отображения: Canvas позволяет контролировать отрисовку на уровне пикселей, что критически важно для соответствия стандартной миллиметровой сетке ЭКГ.
-
Эффективность в отрисовке линейных элементов: ЭКГ-сигнал представляет собой набор соединенных линий, и Canvas предоставляет оптимизированные методы для их отрисовки.
-
Полный контроль над процессом отрисовки: Есть возможность настроить отображение различных элементов графика (оси, сетка, сигнал) в соответствии с требуемыми медицинскими стандартами.
-
Гибкость в реализации интерактивности: Canvas облегчает реализацию динамического обновления графика, масштабирования и перемещения при помощи жестов.
-
Оптимизация производительности: Во время длительных сеансов записи накапливается большой объем информации, но благодаря собственной реализации графика мы можем точно контролировать расход памяти и рисовать только видимую его часть (см. gif ниже).
Использование Canvas обеспечивает необходимый баланс между точностью отображения, производительностью и гибкостью разработки, что делает его оптимальным выбором для создания специализированного графика ЭКГ.
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.
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.Activity != -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, что привело к значительному повышению производительности.
Причины перехода:
-
Ограничения DrawPatch:
-
Метод DrawPatch, несмотря на свою универсальность, оказался недостаточно оптимизированным для специфики отрисовки графика ЭКГ.
-
При большом количестве точек данных производительность начинала существенно падать.
-
-
Преимущества 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. Тестирование и валидация
Одним из критических аспектов разработки специализированного графика ЭКГ является обеспечение точности отображения данных. Для проверки соответствия отображаемой миллиметровой сетки реальным физическим размерам был разработан следующий подход:
Первоначальный метод проверки и его ограничения:
-
Использование физической линейки:
-
Изначально была предпринята попытка измерения с помощью обычной физической линейки.
-
Выявленные проблемы:
-
Сенсорный экран реагировал на касания линейки, что мешало точным измерениям.
-
Сложность в обеспечении точного позиционирования линейки на экране.
-
-
Использование виртуальной линейки:
-
Применение виртуальной линейки:
-
Было найдено приложение On-screen Ruler, которое отображает виртуальную линейку непосредственно на экране устройства.
-
Преимущества метода:
-
Линейка основывается на DPI устройства, что обеспечивает точность измерений.
-
Гибкие настройки позволяют корректировать отображение делений линейки.
-
Удобство позиционирования виртуальной линейки на экране устройства
-
-
Процесс валидации графика:
-
Калибровка виртуальной линейки:
-
Сравнение виртуальной линейки с физической для подтверждения точности отображения.
-
При необходимости, настройка параметров виртуальной линейки для обеспечения соответствия физическим размерам.
-
-
Проверка отображения миллиметровки:
-
Использование откалиброванной виртуальной линейки для измерения расстояний между линиями сетки на графике ЭКГ.
-
Проверка соответствия одного деления сетки одному миллиметру.
-
-
Валидация масштаба графика:
-
Измерение амплитуды и временных интервалов ЭКГ-сигнала с помощью виртуальной линейки.
-
Сравнение полученных значений с ожидаемыми стандартными параметрами ЭКГ.
-
8. Заключение
В ходе разработки специализированного графика для отображения ЭКГ с использованием Canvas удалось достичь следующих ключевых результатов:
-
Точность отображения:
-
Реализовано точное отображение графика ЭКГ в соответствии с медицинскими нормами.
-
Достигнуто корректное отображение миллиметровой сетки, что критически важно для интерпретации ЭКГ.
-
-
Высокая производительность:
-
График отрисовывается плавно даже на устройствах с ограниченными ресурсами.
-
Оптимизация с использованием метода DrawLines вместо DrawPatch позволила увеличить производительность в два с половиной раза.
-
-
Гибкость и расширяемость:
-
Успешно реализована синхронизация основного графика ЭКГ с дополнительным графиком, имеющим меньшую частоту дискретизации.
-
Разработанная архитектура позволяет легко добавлять новые функции и графики.
-
-
Точность измерений:
-
Разработан и применен эффективный метод проверки точности отображения с использованием виртуальной линейки (On-screen Ruler).
-
Обеспечено соответствие отображаемых данных реальным физическим размерам.
-
-
Оптимизация для различных устройств:
-
График корректно отображается на устройствах с разными разрешениями экрана и DPI.
-
Разработанное решение представляет собой надежный и эффективный инструмент для визуализации ЭКГ, который легко адаптируется к различным медицинским приложениям и может быть использован в исследованиях в области кардиологии.
Автор: CrabCoder