Вступление
В этой статье я хочу поделиться опытом разработки iOS-приложения для роботизированного микроскопа с AI-распознаванием клеток крови — как оно устроено, какие задачи пришлось решать, на какие грабли пришлось наткнуться и как iPhone можно использовать в качестве лабораторного инструмента.
Это не очередной todo-лист с авторизацией или приложение для наложения масок на селфи — в центре внимания: видеопоток с окуляра микроскопа, нейронки, работа с железом, Bluetooth-управление перемещением стекол, и всё это — прямо на iPhone.
При этом я постарался не уходить в чрезмерные технические детали, чтобы статья оставалась доступной для большей части аудитории.
Немного про продукт
Даже с современными гематологическими анализаторами до 15% образцов всё равно требуют ручного пересмотра под микроскопом — особенно если в крови найдены аномалии. Автоматизированные системы микроскопирования есть, но стоят как крыло от боинга, поэтому большинство лабораторий продолжают смотреть мазки вручную. Мы делаем это иначе: наш набор превращает обычный лабораторный микроскоп в цифровой сканер с автоматической подачей и съёмкой — просто, дёшево и эффективно. Мы его разрабатываем вместе с @ansaril3 и командой.

Комплект подключается к стандартному лабораторному микроскопу, превращая его в цифровой сканер. Его хардварная часть состоит из:
iPhone — управление системой, анализ клеток
Линза-адаптер — подключение смартфона к окуляру микроскопа
Роботизированный предметный столик — позволяет перемещать препарат, управлять фокусировкой и переходить между образцами
Программно система состоит из:
мобильного приложения на iPhone
контроллера на столике
веб-портала/облака.

Мобильное приложение решает следующие задачи:
обрабатывает поток изображения с камеры, детектирует, классифицирует, подсчитывает клетки
отправляет их на веб-сервер вместе с другими артефактами анализа
управляет перемещением предметного столика, выполняя сканирование мазка по заложенному алгоритму
и также отвечает за конфигурацию/запуск анализа, настройку параметров съемки, просмотр краткого отчета анализа и другие сопутствующие задачи
Веб-портал предназначен для просмотра результатов, подтверждения анализа врачом и экспорта отчётов. Ниже видео, где показано как это работает вместе:


Немного контекста про гематологическую диагностику
Основной анализ, который мы делаем — это микроскопия мазка крови. Он является частью общего анализа крови (ОАК), одного из самых распространённых и базовых медицинских тестов. Многие его делали и видели у себя подобные таблицы:

При выполнении ОАК, образцы крови прогоняются через геманализатор. Если аппарат показывает отклонение от нормы, то образец микроскопируют.
Это выглядит следующим образом:
лаборант наносит каплю крови на стекло,
окрашивает по Романовскому (или аналогам)1, фиксирует
глазами изучает препарат под микроскопом.

Именно здесь можно:
увидеть аномальные формы клеток (незрелые нейтрофилы, атипичные лимфоциты, бластные клетки),
оценить зрелость, размер, гранулярность, включения и другие параметры,
иногда — поставить предварительный диагноз до получения данных из ПЦР2 или ИФА3
Но ручной анализ это боль:
очень субъективен,
зависит от опыта лаборанта,
человек подвержен усталости и ошибкам,
плохо масштабируется.
Автоматические системы микроскопирования хороши, но стоят дорого (от 5 млн руб и выше), поэтому более 90% лабораторий довольствуются ручным методом до сих пор!
Мы поставили задачу сделать недорогой комплект автоматизации микроскопа (в пределах нескольких сотен тысяч рублей), который можно массово ставить в лаборатории. И тут на сцену выходит iPhone.
1 - образец крови на стекле обрабатывается специальным красителем, разработанным Дмитрием Леонидовичем Романовском (1861-1921). Этот краситель позволяет сделать различные компоненты клеток крови более видимыми под микроскопом, так как они окрашиваются в разные цвета.
2 - ПЦР (полимеразная цепная реакция) делает возможным обнаружение даже очень малого количества генетического материала, например, вирусов или бактерий, что важно для диагностики инфекционных заболеваний. Помним с ковидных времен
3 - ИФА (иммуноферментный анализ) применяется, когда важно определить наличие специфических белков.

Что умеет iPhone
Когда мы говорим «AI на смартфоне», чаще всего представляют какие-то фильтры в камере, автодополнение текста или чат-боты. Но современные iPhone — это мини-компьютеры с выделенными нейромодулями, способные выполнять серьезную работу: в нашем случае анализ клеток крови в реальном времени, рассмотрим 3 ключевые компонента, которые делают это возможным:
Графический процессор (GPU). Используется для операций с изображениями: преобработка, фильтрация, коррекция. Например: оценка блюра, цветокоррекция, удаление артефактов и другие специфичные операции с графикой / анализом картинок.
Нейронный движок (NPU / Neural Engine) Apple встраивает Neural Engine в свои устройства начиная с A11 (iPhone 8/X), а с A12 (iPhone XR и новее) уже можно выполнять 5+ трлн операций в секунду на NPU (TOPS). На момент написании статьи последнии A17 Pro и A18/A18 Pro выполняют 35 TOPS. Это используется для inference моделей детекции и классификации клеток, оценки фона препарата и т.п, освобождая CPU/GPU.
Процессор (CPU) Отвечает за общую логику, управление, обработку конфигураций, сериализацию/десериализацию, работу с API и файловой системой - все, что не вошло в предыдущие два пункта
Говорить будем на примере iPhone XR (A12 Bionic, 2018), как некий baseline, хоть и старый. Даже на нем у нас получалось:
обрабатывать видеопоток 50fps с камеры микроскопа,
одновременно выполнять CoreML-инференс (~15ms per frame),
параллельно сохранять данные на диск и синхронизировать с облаком,
удерживать температуру в допустимых пределах (если аккуратно настроить throttling и приоритизацию задач)
Тем не менее, устройство могло заметно нагреваться и начинать тормозить. Например, при анализе мазков на малярии, где требуется обрабатывать 100+ клеток на одном кадре, уже на втором-третьем мазке начинался thermal throttling — снижалась частота CPU, появлялись лаги и подтормаживания интерфейса. К тому же плотное прилегание адаптера к задней панели устройства мешает отводу тепла.

На скриншоте ниже показан другой анализ, не малярия — но тут важно, сколько срабатывают детекций на один кадр.

В целом, в iOS можно отслеживать термальное состояние системы через ProcessInfo.processInfo.thermalState. У нас в бою до Critical дело не доходило, но Serious происходит регулярно при очень интенсивной нагрузке. Для perfomance замеров, мы использовали Xcode Profiler, где можно измерять как загрузку CPU, GPU, памяти, так и Thermal State:

А вот Таблица значений thermalState
с расшифровкой из документации:
Состояние (Thermal State) | Рекомендации (Recommendations) | Действия системы (System Actions) |
Nominal (Нормальное) | Корректирующие действия не требуются. | — |
Fair (Умеренное) | Слегка повышена температура.Приложения могут заранее начинать энергосберегающие меры. | Анализ фото ставится на паузу |
Serious (Серьёзное) | Производительность системы снижается. Приложения должны сократить использование CPU, GPU и операций ввода-вывода. | ARKit и FaceTime снижают частоту кадров (FPS)Восстановление из резервной копии iCloud приостанавливается |
Critical (Критическое) | Приложения должны сократить использование CPU, GPU и операций ввода-вывода, а также прекратить использование периферийных устройств (например, камеры) | ARKit и FaceTime значительно снижают частоту кадров (FPS) |
Полный тепловой и энергетический разбор тянет на отдельную статью — как в начале говорил, глубоко уходить не хочу. По открытым источникам можно грубо предположить, что serious соответствует 80-90°C у чипа и ~40°C у поверхности.
iPhone работает с любыми Bluetooth Low Energy устройствами. Для других отдельный флоу, где устройство должно иметь сертификат MFI (made for iPhone), работать по протоколу iAP2 (Apple Accessory Protocol) и т.д. Короче говоря — это не наш кейс.
Тут полезно напомнить базовые роли и структуру протокола:
Peripheral (периферийное устройство) — это устройство, к которому подключаются. Обычно именно периферия рассылает данные или ждёт подключения (пример: часы, термометр, пульсометр).
Central (центральное устройство) — это устройство, которое подключается к периферийному. Он инициирует соединение, отправляет команды и получает данные.
GATT (Generic Attribute Profile) — это структура, по которой BLE-устройства обмениваются данными. GATT описывает, какие “поля” доступны, что можно прочитать, записать или подписаться на уведомление.
Services и Characteristics — данные внутри BLE-соединения структурированы в виде сервисов — логических групп и характеристик — конкретных параметров. Например, у фитнес-браслета может быть сервис Heart Rate, в котором есть характеристика Heart Rate Measurement (текущий пульс).
В нашем случае, iPhone управляет столиком через встроенный BLE модуль, который распознается как Peripheral с кастомным GATT-сервисом и выполняет 2 задачи:
передавать контроллеру команды перемещения по (осям XY) и фокусировки (ось Z)
получал данные от контроллера (статус, позиция)
К слову о тепловой нагрузке, BLE-соединение не должно вносить заметного вклада. Если верить данным Silicon Labs в их доке по энергопотреблению BLE, передача команд или получение статуса с частотой 20 Гц (интервал 50 мс):
даёт прирост <1 мВт. У iPhone XR типичная нагрузка в режиме ожидания ~50–100 мВт. Добавка <1 мВт — почти незаметна, особенно по сравнению с нейронками, GPU и экраном
радиоканал включается всего на ~2% времени, остальное время спит
В блоке “Работа с моторизованным столиком”, где чуть подробнее погрузимся в детали работы приложения с BLE модулем и контроллером.
Теперь немного про камеру. Используем основную (широкоугольную заднюю) камеру: получаем видео H.264 с разрешением 1280x720 и битрейтом около 40 Мбит/с.
Чем выше битрейт, тем больше данных на единицу времени → выше качество изображения. 40 Мбит/с — достаточно высокий для разрешения 1280×720 (HD). Более чем хватает для изображения для анализа клеток.
H.264 — это международный стандарт видеокодирования, также известный как AVC — Advanced Video Coding или MPEG-4 Part 10. Он убирает избыточные данные (межкадровое и внутрикадровое сжатие), снижая битрейт и как следствие размер файла. (У нас, кстати, была задача записывать видео всего анализа для отладки и валидации)
Вот и получаем это не просто мобильный UI-клиент, а вполне себе edge-девайс, то есть устройство, которое самостоятельно обрабатывает данные на месте, без постоянной связи с сервером.
Мобильное приложение
Теперь, когда мы разобрались с хардварной частью, посмотрим, как всё это работает на уровне приложения. Начнем сначала с постановки задачи:
На вход приложению с камеры подается поток кадров — движение поля зрения микроскопа по мазку.
На выходе приложение должно:
детектировать лейкоциты (и другие клетки в зависимости от анализа)
отображать BBox'ами детектируемых объектов
делать их подсчет
отсылать в фоне данные на бекенд (изображения клеток, скана, отдельных кадров)

Как видно на схеме выше, всё крутится вокруг кадра с камеры — на нём держится и детекция, и навигация по стеклу, и какие артефакты из него нужно отправлять в облако. Поэтому в центре всего — потоковая обработка кадров, опишем ее основные этапы и важные моменты.
1) Преобработать кадры. Сюда входит коррекция дисторсии, удаление артефактов, определение уровня размытия (блюр), световая и цветовая коррекция
Например, в каждой лаборатории или микроскопе специфичное освещение, из-за чего нейронка может сбоить. Здесь нужно сделать нормировку белого цвета — навести поле зрения на пустую область и запустить баланс белого цвета на камере.

Еще у нас был такой баг: клетки на портал приходили без калибровки цвета. Из-за того, что детекция в паралель запускалась раньше применения настроек камеры.

2) Задетектить, классифицировать и подсчитать без дублей клетки.
Например, на фото ниже красным отмечены некоторые дубли в одном из старых анализов:

3) Проконтролировать микроскоп, чтобы он правильно двигался по стеклу, переходил с одного на другого и, самое важное, точно фокусировался на стекле, определял, когда он выходит за границы стекла или попадает на пустые пространства
4) Загружать на облако пачку клеток (снапшоты, метаданные) и не блокировать этим прогон следующего анализа
5) Повторить n раз, так как анализы делаются пачкой
6) и сделать это так, чтобы телефон не взорвался от перегрева
Приложение развивалось как обычно в стартапах: был быстро набросан proof-of-concept, затем доведен до MVP (minimum viable product), чтобы можно было пилотировать в лабах и питчить инвесторам. . В итоге архитектура приложения получилась гибридной: часть экранов реализована на UIKit-овских MVP экранах (model-view-presenter), а новые фичи и интерфейсы пишутся на Swift c MVVM (Model-View-ViewModel).
Используем сервисный слой для изоляции бизнес-логики: CameraService, BluetoothController, AnalysisService. Все зависимости инжектируются через конструкторы или через DI-контейнеры. В плане реактивности и асинхронных цепочек с подписками на события у нас был “эволюционный путь”: сначала завезли RxSwift, потом начали переходить на Combine, а с выходом async/await часть цепочек ушла на них. Получился такой “франкенштейн”, но затем мы изолировали эти куски в отдельные компоненты — чтобы в будущем можно было просто заменить их компонент с новым стеком. Всё приложение прошито подробными логами, а для сложных случаев (особенно связанных с обработкой кадров) используем NSLogger: туда можно логировать не только текст, но и изображения — это не раз спасало при отладке пайплайна обработки клеток перед их отправкой на сервер.

Про тестирование можно было бы написать отдельную статью: от мокирования отдельных частей анализа и быстрой настройки нужных состояний через ProcessInfo (у меня, кстати, есть небольшая тех. заметка на эту тему), до симуляции отдельных шагов анализа и покрытия всего этого интеграционными и юнит-тестами.
Но вернемся к обработке кадров и рассмотрим чуть более подробную, чем выше архитектурную схему:

Analysis Controller — центр принятия решений: получает кадры, запускает обработку в Frame Pipeline.
Camera Service — получает сырой поток кадров с камеры, преобразует их и отправляет дальше
Microscope Controller управляет контроллером микроскопа
Frame Pipeline — цепочка из нескольких стадий:
Preprocessing — коррекция, фильтрация
Detection — поиск объектов/клеток
Counting — подсчет уникальных объектов
Postprocessing — финальная фильтрация и подготовка к визуализации
UI — отвечает за отображение результатов пользователю в реальном времени (bounding boxes, статистика, алерты).
Uploader — синхронизирует артефакты анализа (снапшоты, клетки, конфиг) с бэкэндом.
В плане менеджера зависимостей: использовался CocoaPods (перешёл в режим поддержки и не развивается активно с 2024 года), но затем мы завезли SPM (Swift Package Manager). Часть сервисов (CV, Bluetooth, утилиты) были вынесены в SPM модули, также были попытки вынесения ObjC/C++ кода в отдельные xcframework-ы, но было некогда разгребать, поэтому оставили этот код в основном проекте. ObjC нужен для обертки над C++, чтобы его можно было вызывать из Swift-а. Получались такие ObjC++ классы: интерфейс у них чисто ObjC-шный и с ним может работать Swift, а в реализации перемешан код ObjC и C++. Это было еще до поддержки вызова C++ прямо из Swift. Оговорюсь, что я далеко не гуру C++ и Computer Vision алгоритмов, но в мои задачи входило базовые погружение и портирование алгоритмов, эвристик с Python, на которым у нас был основной R&D. Ниже опишу некоторые из них.
Задачи
Удаление дисторсии
У одного из адаптеров был артефакт оптической дисторсии на изображении. В результате клетка, которая должна быть круглой, кажется вытянутой или кривой, особенно по краям кадра. Мы использовали калибровку на шахматной сетке и OpenCV cv::undistort() для восстановления геометрии кадра:
Калибруем камеру — снимаем шахматную доску/сетку с известной геометрией:
OpenCV вычисляет:
матрицу камеры K (параметры проекции)
коэффициенты дисторсии D = [k1, k2, p1, p2, k3, …]
Применяем cv::undistort() или cv::initUndistortRectifyMap() + remap():
вычисляется, куда реально «должна была попасть» каждая точка
изображение “разгибается” назад

В последствии, адаптер поменяли — этот шаг убрали
Определение позиции на стекле:
Чтобы точно считать клетки, нужно максимально точно знать их координаты. Вот тут на видео видно, что происходит с кривым определением сдвига.
Изначально, мы пытались посчитать относительный сдвиг между двумя кадрами и суммировать абсолютный сдвиг. Перепробовали несколько вариантов:
классический метод регистрации изображений через фазовую корреляцию, основанную на быстром преобразовании Фурье. Реализовали на OpenCV и даже использовали Apple Accelerate.
Методы на основе локальных ключевых точек с дескрипторами: SURF, SIFT, ORB и другие.
Optical Flow
встроенный в Apple Vision: VNTranslationalImageRegistrationRequest
С одной стороны, у нас были некоторые допущения:
отсутствовало изменение масштаба, поворотов.
оптические: чистый, несмазанный мазок, без пустых пространств
Но несмотря на это, все равно были проблемы: из-за изменения освещения, фокуса, накопления ошибки, резких сдвигов, шумов/артефактов на изображении.
Получилась вот такая вот таблица их сравнения:
Метод | Преимущества | Недостатки | Особенности использования | Скорость | Комментарий |
FFT + кросс-корреляция (OpenCV, Accelerate) | Очень быстрый, глобальный сдвиг, прост в реализации | Накапливается ошибка, не устойчив к резким сдвигам. | Требует одинакового размера изображений, подходит для “чистого” сдвига | Очень высокая | Использовался как основной |
SIFT | Высокая точность, масштаб/поворот-инвариантность | Медленный, раньше был не free | Отлично для разнообразных сцен, с текстурой и сложными преобразованиями | Медленный | Экспериментальный вариант |
SURF | Быстрее SIFT, тоже устойчив к масштабу/повороту | Проприетарный, не всегда доступен | Подходит для real-time чуть лучше, но тоже “тяжёлый” | Средний | Экспериментальный вариант, тем более что под патентом |
ORB | Быстрый, бесплатный, инвариантен к повороту | Чувствителен к освещению, неустойчив к масштабу | Неплохо показывать себя в склейке изображений | Высокая | Пока не перенесли склейку в облаке, были версии с ним |
Optical Flow (Lucas-Kanade) | Отслеживает движение точек между кадрами, хорош для видео | Не работает на глобальных трансформациях, зависит от освещения | Лучше всего в видео или сериях с малым движением | Средняя | Были эксперименты в оцифровке (склейке) изображений |
Optical Flow (Farneback) | Плотная карта движения, применим к целому изображению | Медленный, чувствителен к шуму | Хорош для анализа локальных движений в кадре | Медленный | Были эксперименты в оцифровке (склейке) изображений |
Apple Vision (VNTranslationalImageRegistrationRequest) | Очень удобный API, быстрый, железо-оптимизирован | В нашем случае точность была слабая. | Отлично подходит для простых кейсов на iOS/macOS | Очень высокая | Попробовали и закопали |
Для каждой варианта мы пытались найти оптимальную по точности/производительности конфигурацию сравнения с эталонным сдвигом: меняли разрешения изображения, параметры алгоритмы, разные настройки камеры и оптики микроскопа. Ниже пару графиков из такого рода экспериментов

А вот как выглядела отладка поиска ключевых точек, по которым мы в дальнейшем хотели строить сдвиг.

В итоге, когда у нас в системе появился роботизированный столик, мы начали использовать координаты его контроллера, которые мы уже уточняли с помощью CV-эвристик.
Подсчет клеток
По сути, задача подсчета клеток — это частный случай object tracking & deduplication: “увидеть, что за клетка, не посчитать дважды, не пересчитать лишнее, и не пропустить нужное — всё это за доли секунды, в онлайне через камеру и на железе телефона. Как мы это решали:
Обнаружение объектов. Используем нейронки для детекции объектов на кадре (Bounding Box, BB). Каждый BB имеет свой “confidence score” (доверие сети) и класс клетки.

Для борьбы с фоном и ложными срабатываниями применяем быструю фильтрацию:
цветовая: например, по интенсивности или цветовому диапазону. Например، здесь слева красным подсвечен эритроцит — но нейронка приняла его за лейкоцит
Однако дальше в дело вступили цветовые фильтры и он был отсеян.

геометрическая: отбрасываем объекты, размер которых выходит за рамки типичных клеток.
также отбрасываем клетки, которые частично выходят за пределы кадра — нам такие не интересны

Подсчет уникальных объектов. Некоторые BB могут посчитаться более одного раза для одной и той же клетки, нужно уметь ловить такие срабатывания и учитывать лишь один раз. В свое время мы вдохновлялись гайдом от MTurk, где описаны 2 варианта:
Вариант 1: Сравниваем расстояния между центрами BBs — если новый BB находится слишком близко к уже прочитанному, это скорее “та же” клетка.
Вариант 2: Считаем IoU (intersection over union, Jaccard Index) — метрику пересечения прямоугольников. Если новый BB сильно пересекается с существующим, учитываем его только один раз.

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

Здесь снова критично важным было определение позиции, а далее бесшовная склейка.
Надо сказать, что изначально у нас не было моторизированного столика и мы полагались на ручную навигацию. Представьте, что вы клеите мозаику из сотен осколков. Промахнетесь с координатами — и мозаика поплыла.
Вот как выглядели первые эксперименты: прыгающие поля зрения, швы, разница в освещении, пустые пространства.

Или вот пользователь делает скан мазка, резко двигаясь по мазку - некоторые области получаются смазанными (Motion Blur). Мы пробовали дропать такие кадры, если они не прошли допустимый порог блюра или для них не посчитался сдвиг.

Постепенно продвинулись к такому варианту:

Итераций было много: склейка на девайсе, через разные методы, на разных разрешениях кадров и конфигах камеры. Пришли к тому, что скан собирается в облаке, а мобилка отсылает кадры с калибровкой баланса белого и экспозиции.
Ниже пример, как мы замеряли скорость отдельных кусков обработки кадров в зависимости от конфига: настройка камеры, выбранные алгоритмы их параметры.

Работа с моторизованным столиком
Теперь — подробности про связку iPhone и моторизованного столика: как общаемся по BLE, какие команды шлем и как настраивали автофокус. Мобилка связывается через Bluetooth с контроллером на столике и двигается по XYZ координатам. Точнее, двигается сам столик, но для с точки зрения изображения с объектива, которое видит мобилка: движение происходит визуально по стеклу.
Столик у нас тоже самодельный — не потому что «хотим всё своё», а потому что рыночные решения стоят от $10k, и это не шутка. Мы наняли конструкторское бюро и собрали свою версию за ~$800. Получилось сильно дешевле, потому что один из инженеров вовремя заметил: конструкция моторизованного микроскопного столика подозрительно напоминает 3D-принтер. Та же кинематика по XYZ, те же шаговики, те же рельсы. В итоге используем массовые и дешёвые компоненты, но под наши задачи. Конструктивно столик состоит из трёх частей: сама платформа XY, блок фокусировки (ось Z, мотор крепится к ручке тонкой фокусировки) и управляющий блок — контроллер, который принимает команды по Bluetooth и отдает их на шаговики. Всё это работает в связке с мобилкой.

Для ручного перемещения столика используем виртуальный джойстик (отображаем пользователю на экране кнопки перемещения) — он используется в сценариях калибровок и настройки системы. Во время же анализа всегда автоматическое управление. Вот как работал джойстик в первых версиях — потом мы уже докрутили и звук и задержку.
Протокол взаимодействия
В качестве блютуз интерфейса используется плата HC-08. BLE модуль работает по умолчанию в режиме текстового терминала, то есть запросы/ответы просто проходят туда-сюда. Для конфигурации и системных задач (смена имени, скорости обмена) используются at-команды.
Сам контроллер работает на прошивке GRBL, через g-команды. Основные сценарии здесь:
инициализация подключения (телефон должен понять, что столик подключен)
сканирование стекла (перемещение столика по всем осям)
остановка/возобновление сканирования
обработка исключительных ситуаций: дошли до концевика, прерываем перемещение, переполняется буфер команд. По ошибкам есть отдельная дока

GRBL обладает собственным набором команд, который начинается с символа $, например:
$H - хоминг или калибровка и поиск аппаратного нуля по концевика. Обычно выполняется при первом старте и далее по необходимости в случае большой накопленной ошибки в процессе движения.
$J=<команда> - режим Jogging, то есть имитация управления джойстиком. Сама команда должна описывать относительное перемещение по всем осям. Пример такой команды: $J=G21G91Y1F10
G21 - режим расстояния в миллиметрах
G91 - относительное смещение
Y1 - перемещение по оси Y на 1 миллиметр
F10 - скорость перемещения.
? запрос состояния grbl. Возвращает строку с основными параметрами машины. Пример выполнения: <Alarm|MPos:0.000,0.000,0.000|FS:0,0|Pn:XYZ|WCO:-5.300,0.000,-2.062>
Нам нужны первые два параметра:
состояние. Может быть "Idle", "Running", "Alarm"
MPos - текущая позиция столика
В детали GRBL и протоколов управления столиком сильно уходить не буду — это тянет на отдельную статью. Если коротко: GRBL — это опенсорсная прошивка из мира ЧПУ, отлично подходит для управления трех осевыми системами (XY+Z) через простые G-коды. BLE-модуль взяли максимально простой — HC-08, чтобы не возиться с MFi и iAP. Нам было важно, чтобы iPhone мог надежно отдавать команды и получать статус с минимальной задержкой и стоимость комплекта сильно не поменялась.
Задачи
Автофокус
Выше я упоминал про фокусирование. Этот процесс выполняется с периодичностью во время сканирования мазка, так как препарат нанесен неравномерно, особенно это заметно на больших увеличениях. Приходится мониторить уровень размытия и своевременно обновлять фокус. Выглядит это так.
На графике ниже показана зависимость уровня фокусировки от времени. Начинаем с размытого изображения, постепенно устанавливая столик в положение оптимального фокуса

Сканирование
Я уже упоминал про оцифровку скана со стороны мобилки. Тут полезно упомянуть, что оцифровка может происходить на разных увеличениях: от 5x до 40x. На маленьком зуме — проще ориентироваться и искать границы пятна, на большом — видно детали клеток.

В нашем случае мы работаем с 2-мя уровнями:
Поиск границ на увеличении 4x. Алгоритм проходит по всему стеклу, определяет область пятна и выдает карту границ на следующий этап. На выходе получается что-то вроде тепловой карты. Например, из изображения на низком увеличении слева мы получим матрицу, по которым уже построим шаги, чтобы двигаться на высоком увеличении:
Гистологическое изображение ткани на увеличении 4x
Сканирование пятна на увеличении 20x (или другом). Алгоритм сканирует и сохраняет изображения для последующего построения в единую карту. Сканирование идет построчно, в рамках границ пятна. Фото для склейки берется когда:
изображение сфокусировано
контроллер в состоянии idle, т.е не двигается

Чтоб пользователю не менять объектив каждый раз, мы делаем поиск границ и сканирование сразу по всем стеклам в батче, параллельно загружая предыдущий батч на облако. На нем уже происходит стичинг или сшивка изображения, но это тема для отдельной статьи.
Заключение
Этот проект показал: даже смартфон из 2018 года может тянуть задачи, которые раньше решались десктопами, серверами и дорогими автоматическими микроскопами. Конечно, за кадром осталось много всего: от сбора датасета до тонкой настройки экспозиции. Если интересно — могу отдельно разобрать это. Задавайте вопросы, делитесь своим опытом, и, возможно, вместе соберём продолжение или разберём отдельные аспекты глубже. Спасибо, что дочитали!