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.
-
Разработанное решение представляет собой надежный и эффективный инструмент для визуализации ЭКГ, который легко адаптируется к различным медицинским приложениям и может быть использован в исследованиях в области кардиологии.
ссылка на оригинал статьи https://habr.com/ru/articles/867830/
Добавить комментарий