Обновить

Комментарии 4

Вот что подсказывает Глубоко Больной Пациент. Ничего личного.

----------:

Отличная статья, которая честно описывает инженерный компромисс. Однако как инженер-алгоритмист я вижу в предложенном решении несколько уязвимостей, которые не освещены (или сглажены) в тексте, и которые могут выстрелить в продакшене. А также есть как минимум 3 класса альтернативных решений, которые стоило рассмотреть.

Вот конструктивная критика и альтернативные варианты.

  1. Критика предложенного подхода (Phase Correlation)

Проблема №1: Опасное упрощение вращения Вы пишете: «важнее было быстро и устойчиво определять смещение… ограничение приемлемо». Это самое слабое место. В реальности предметное стекло никогда не двигается идеально поступательно без вращения (даже на моторизованном столике есть рыскание (yaw)). Игнорирование вращения означает, что при большой площади склейки вы получите эффект «веера» — края изображения разъедутся на десятки пикселей. Да, у вас есть глобальная оптимизация (Bundle Adjustment), но она корректирует центры кадров, а поворот каждого фрагмента относительно соседа останется нескомпенсированным. Это приведет к двоению контуров на стыках при детальном рассмотрении.

Проблема №2: «Центральные области» — это убийство данных Выбрать только центральную часть — значит отбросить ~30-40% полезной площади каждого кадра. Для большого мозаики это критично увеличивает время сканирования (приходится делать больше оверлапов). Кроме того, если у вас нет весового смешивания (feathering), а просто жесткая обрезка, то в местах стыков останутся геометрические разрывы, если движение было неидеальным. Простое усреднение (averaging) даёт размытие, но жесткая обрезка даёт артефакты в виде полос, если кадр чуть повернут.

Проблема №3: FPS против реальной пропускной способности 200 FPS для фазовой корреляции — это цифра для FFT размера 256x256. Но если у вас камера Full HD (1920x1080), вы вынуждены делать даунсэмплинг или считать по патчам. В статье не указан размер патча. Если считать по всему кадру — 200 FPS на CPU невозможно (только на GPU с cuFFT). Если считать по маленькому патчу — метод теряет устойчивость при резких перепадах фокуса.

Проблема №4: Цветокоррекция «вслепую» Выравнивание цвета по соседним областям без учета эталонного кадра (или без глобальной гистограммной привязки) порождает эффект «зебры» — градиент яркости, ползущий от левого края к правому, если съемка шла долго и лампа микроскопа нагрелась. Без модели изменения освещения во времени (трендовая коррекция) результат будет пестрым.

  1. Другие варианты решения (Альтернативы)

Если бы я проектировал эту систему заново, я бы рассмотрел следующие подходы, разделив их по сценариям.

А. Гибридный подход (Feature + Phase Correlation)

Вместо того, чтобы выбирать одно, объедините их в конвейере:

  1. Грубая оценка сдвига через Phase Correlation по даунсемпленному (x4) изображению.

  2. Уточнение (Refinement) через поиск совпадений одной ключевой точки (например, центр масс бликов) методом NCC (Normalized Cross-Correlation) с подпиксельной интерполяцией (parabolic peak fitting).

  3. Детекция выбросов: Если пик фазовой корреляции низкий (размытие), мы не отбрасываем кадр, а запускаем SIFT/ORB только на этом конкретном переходе как “спасательный круг”. Это даст скорость ~100 FPS, но спасет от сбоев.

Б. Логарифмически-полярное преобразование (Log-Polar Transform)

Если уж мы работаем в частотной области (БПФ), почему мы не используем Log-Polar FFT? Этот метод позволяет за один проход найти и сдвиг, и вращение, и масштаб. Вы переводите изображение в логарифмически-полярные координаты, где поворот и скейлинг становятся простым линейным сдвигом. Плюс: Вы решаете проблему вращения на скорости, лишь в 2-3 раза медленнее, чем обычная фазовая корреляция. Это снимает главное ограничение, указанное в статье.

В. Сшивка на основе глубокого обучения (LoFTR / DKM)

Для микроскопии с бедными текстурами классический CV (даже SIFT) часто проигрывает. Современные матчеры на трансформерах (LoFTR, SuperGlue, DKMv3) специально обучены на парах изображений с низкой текстурой (аэрофотосъемка пустынь, медицинские снимки). Сценарий: Вы берете каждый 5-й кадр, находите на нем 1000 точек через LoFTR (на GPU это 15 FPS), получаете точную аффинную матрицу, а промежуточные кадры привязываете через оптический поток (Farneback) к этим опорным. Это дает абсолютную устойчивость к накоплению ошибки (drift) и учитывает нелинейные дисторсии объектива.

Г. Инкрементальная оптимизация с факторизацией (iSAM / Sliding Window)

В статье упомянут глобальный BA, но для потока это требует хранения всех кадров в памяти, что при размере мозаики в 10k x 10k пикселей приводит к Out-of-Memory. Альтернатива: Использовать EKF (Extended Kalman Filter) на основе показаний Phase Correlation. Это не требует пересчета всех кадров, дает оценку ковариации (мы знаем, где ошибка накапливается), и позволяет строить превью в реальном времени, а глобальный BA запускать только в конце для финального рендера. Это также позволяет “забывать” старые кадры, экономя RAM.

Д. Подход с приоритетом резкости (Sharpness-Weighted Blending)

Вместо жесткого выбора центральных пикселей — используйте взвешивание по карте градиентов (Laplacian variance). Для каждого пикселя каждого кадра вычисляется локальная резкость. В финальное изображение записывается пиксель с максимальной резкостью среди перекрывающихся кадров. Это магическим образом убирает швы (потому что границы кадров обычно нерезкие) и автоматически вырезает смазанные участки, не теряя полезные данные по краям, если они вдруг оказались резче центра (бывает при кривизне образца).

  1. Что стоило добавить в финал (Hardware-in-the-loop)

В статье не учтено, что микроскопы с моторизованным столиком часто отдают координаты XY (энкодеры). Самое лучшее решение — не полагаться только на CV, а сделать Sensor Fusion:

  1. Берем координаты с энкодеров столика (грубо, но без дрейфа).

  2. Берем смещение с Phase Correlation (точно, но с дрейфом).

  3. Сливаем через Калмана. Это дает субликсельную точность, устойчивость к размытию (если кадр смазан — доверяем энкодеру) и позволяет детектировать проскальзывание столика. Именно так делают в промышленных системах (например, Leica или Zeiss), а не в лоб через чистый CV.

Итог: Вы выбрали самый быстрый метод, пожертвовав вращением и памятью. Если заказчик когда-нибудь захочет сканировать мазок крови при 1000x увеличении (где поле зрения крошечное, а стыков сотни), накопленная ошибка по углу разрушит мозаику. Мой выбор — Log-Polar FFT + Sharpness-Weighted Blending + глобальный BA с аффинными матрицами, а при наличии энкодеров — EKF-слияние.

--------------------------------------------

Привлечение совместной энтропии/информации

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

Общая идея комбинации

Метод Сильная сторона Слабая сторона Фазовая корреляция (ФК) Скорость (сотни FPS), надёжна на контрастных текстурах Не работает при нелинейном изменении яркости, не учитывает вращение, чувствительна к размытию Взаимная информация (ВИ) Устойчива к изменениям освещения и цветовым сдвигам, может оценивать вращение/масштаб Очень медленная (десятки мс на один расчёт), требует оптимизации

Гибридный принцип:

· ФК используется как грубый детектор — даёт начальное приближение сдвига за 1–2 мс. · ВИ используется как точный корректор — уточняет положение, но только там, где ФК дала сомнительный результат, либо на этапе глобальной оптимизации.

  1. Общий пайплайн (ядро для обоих режимов)

На входе — видеопоток, каждый кадр с временной меткой.

  1. Предобработка кадра: · Преобразование в градации серого (для ФК) и сохранение цветного оригинала (для финального рендера). · Даунсэмплинг в 2–4 раза для ускорения (особенно для ВИ). · Вычисление карты резкости (Laplacian variance) — чтобы оценить качество кадра.

  2. Оценка грубого сдвига (ФК): · Применяем БПФ к двум соседним кадрам (или к текущему и предыдущему). · Получаем пик корреляции и его координаты → грубое смещение (dx, dy) и коэффициент надёжности conf = peak / (mean + std).

  3. Решение о необходимости уточнения (ВИ): · Если conf высок (> 0.6) — считаем, что ФК справилась, и сразу передаём сдвиг в трекер. · Если conf низкий (размытие, плохая текстура, смена яркости) — запускаем локальный поиск максимума ВИ в небольшом окне вокруг (dx, dy) ± 5 пикселей.

  4. Уточнение с помощью ВИ (быстрый вариант): · Строим совместную гистограмму интенсивностей для текущего предполагаемого сдвига (и нескольких соседних). · Используем нормализованную взаимную информацию (NMI) как метрику. · Для поиска максимума применяем параболическую интерполяцию по 3×3 сетке вокруг начального сдвига — это даёт подпиксельную точность без итеративного спуска.

  5. Сохранение результата: · Запоминаем уточнённый сдвиг (и, возможно, угол, если мы его оценивали) и метку качества кадра. · Если сдвиг слишком мал (кадр стоит на месте) — пропускаем его (дубликат).

Это ядро работает и в реальном времени, и в офлайне. Отличие — в том, как часто и как глубоко мы используем ВИ, а также в постобработке.

  1. Режим реального времени (online)

Цель: показывать пользователю превью мозаики с минимальной задержкой, откликаясь на движение столика.

Стратегия:

· 99% кадров обрабатываем только ФК — это даёт скорость >100 FPS на CPU (при размере 512×512 в сером). · ВИ вызываем только при двух условиях: · conf упал ниже порога (например, 0.4) — кадр размыт или освещение резко изменилось. · Каждые 5–10 кадров принудительно — чтобы сбросить накопленный дрейф (делаем «реперную» проверку). · Для ВИ используем сильно уменьшенные изображения (128×128) и ограничиваем поиск окном ±10 пикселей — это укладывается в 5–10 мс на современном CPU (с использованием SSE/AVX или OpenMP). · Трекер движения: · Запускаем простой фильтр Калмана на основе последовательности сдвигов. Он сглаживает шум ФК и предсказывает сдвиг для следующего кадра, что уменьшает поисковое окно для ФК (ускоряет ещё сильнее). · Если ВИ дала поправку, она обновляет состояние Калмана. · Рендеринг: · Каждый новый кадр размещается на полотне по текущей оценке (с использованием весов по резкости центральной области, как в статье). · Поскольку вращение не учитывается, мы компенсируем его предварительной калибровкой траектории — предполагаем, что угол дрейфа мал, и игнорируем его на этапе превью.

Итог по времени: средняя обработка одного кадра — 2–3 мс (ФК) + редкие 10 мс (ВИ). Превью обновляется плавно, пользователь видит процесс сканирования.

  1. Режим постобработки (offline)

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

Стратегия — двухпроходная:

Проход 1. Построение графа связей с уточнением всех пар

· Для каждого кадра мы имеем грубую траекторию от online-режима (или можем пересчитать заново, если online не сохранил все данные). · Теперь мы не ограничены по времени. Поэтому: · Для каждой пары соседних (и, возможно, через одного) кадров вычисляем точную трансформацию (сдвиг + поворот + масштаб) с помощью максимизации взаимной информации. · Используем многомасштабную пирамиду (от 64×64 до полного разрешения) и оптимизацию Бройдена–Флетчера–Голдфарба–Шанно (BFGS) по 4 параметрам (dx, dy, угол, масштаб). Это занимает 0.5–2 секунды на пару, но мы делаем это только для ключевых пар (например, каждые 10 кадров), а для остальных — интерполируем. · Для ускорения можно использовать GPU-ускорение гистограмм (CUDA) — тогда расчёт ВИ для одной пары занимает <50 мс.

Проход 2. Глобальная оптимизация (Bundle Adjustment)

· Имеем набор относительных трансформаций между кадрами (граф с рёбрами). · Строим факторный граф: · Вершины — положения кадров в глобальной системе координат (свободные параметры: x, y, угол). · Рёбра — измерения (сдвиг, угол) с ковариационной матрицей (вес, обратно пропорциональный неопределённости, которую мы оцениваем по кривизне пика ВИ). · Запускаем нелинейный метод наименьших квадратов (например, Ceres Solver или g2o) для минимизации суммарной ошибки reprojection. · Это одновременно сглаживает дрейф, распределяет ошибку по всем кадрам и вычисляет оптимальные параметры вращения. · После оптимизации все кадры размещаются на итоговом полотне.

Проход 3. Финальный рендеринг (бесшовное смешивание)

· Для каждого пикселя итогового изображения определяем, какие кадры его покрывают. · Используем мультибандовое смешивание (как в панорамах) или взвешивание по резкости (на основе лапласиана), но с учётом того, что теперь у нас есть точные геометрические трансформации. · Для выравнивания цвета применяем глобальную гистограммную привязку — выбираем опорный кадр с медианной яркостью и корректируем все остальные так, чтобы их гистограммы совпадали (это решает проблему дрейфа освещения, которую ВИ только детектирует, но не исправляет).

Итоговый результат: мозаика без швов, с точностью <0.1 пикселя, скомпенсированными поворотами и единой цветопередачей. На всё это уйдёт несколько минут для 1000 кадров — что приемлемо для офлайн-исследований.

  1. Как именно связать ВИ с ФК в офлайн-режиме?

Предлагаю метод гибридной метрики:

· Начальное приближение для ВИ-оптимизации берём из ФК (это сокращает область поиска и гарантирует сходимость). · В качестве финальной метрики используем комбинированный штраф: (1 - α) NCC + α NMI, где α регулируется в зависимости от разницы гистограмм (если гистограммы сильно отличаются, α → 1). Это даёт робастность на всём диапазоне кадров.

  1. Рекомендации по реализации для инженеров

· Для online: используйте библиотеку OpenCV (phaseCorrelate) и собственную быструю реализацию NMI через предварительно вычисленные таблицы. Ограничьте разрешение до 256×256 для ФК и до 128×128 для ВИ. · Для offline: используйте библиотеку SimpleITK или ITK — там уже есть готовые регистраторы на основе взаимной информации с поддержкой многопараметрических трансформаций. Либо реализуйте свой оптимизатор на Eigen + CUDA. · Хранение траектории: сохраняйте все сдвиги (даже online-оценки) в файл, чтобы в офлайне можно было пересчитать только сомнительные участки, а не все пары заново.

Итог: два режима — один код

В коде достаточно сделать флаг is_online.

· Если true — используем ФК + редкий ВИ, рендерим на лету. · Если false — запускаем полный пайплайн оптимизации, используя сохранённые кадры и их online-треки как стартовую точку.

Такая архитектура даёт заказчику гибкость: он может быстро просканировать область, посмотреть черновик, а затем запустить «финишную обработку» на ночь и получить готовый высококачественный файл в GeoTIFF/DICOM. И никаких компромиссов — каждый метод работает там, где он сильнее всего.

Вы там случайно не сговаривались?

Вот комментарий Глубоко Больного Пациента на предыдущую статью

https://habr.com/ru/articles/1067168/#comment_30302964

А, это его совместный разбор обоих. Ничего личного.

-----------------------------

Отличная статья! Видно, что команда прошла через классический “ад энтузиаста” — от блестящего прототипа до продакшен-ада. Давайте разберём её под микроскопом (простите за каламбур), жёстко покритикуем инженерные решения и проведём параллели с предыдущей темой сшивки изображений.

  1. Критика статьи и технических решений MotionCode

Проблема №1: Самоуспокоенность по поводу “Replay Attack” (Атака повторным воспроизведением)

В статье говорится: «Скриншот или запись старой последовательности быстро теряют практический смысл». Это опасное заблуждение. Если я запишу видео панели длительностью 5 секунд на другой телефон и просто покажу это видео камере целевого устройства, алгоритм CV увидит точно такую же динамику, временные интервалы и яркость. Если вы не добавили рандомный временной челлендж (например, смена состояния должна произойти строго после того, как камера моргнула, или с привязкой к текущей миллисекунде сервера), система уязвима. Ваш CV анализирует только геометрию и яркость — он не видит, что это запись с другого экрана.

Проблема №2: Отказ от нейросети — это победа, но вы потеряли контекст

Вы заменили детектор на поиск ROI и яркость. Однако классический CV слеп к частичным перекрытиям. Если палец пользователя перекроет половину ячейки, ваша яркостная метрика сломается. Более того, при съёмке под углом геометрические искажения делают центр ячейки совсем не там, где вы его ждёте. В статье не упомянута аффинная коррекция перспективы перед сравнением областей — а без неё при наклоне телефона > 30° точность резко падает.

Проблема №3: “Магия” калибровки и отсутствие нормализации

Вы пишете: «Параметры, работавшие на одном телефоне, ломались на другом». Это классическая ошибка — настройка абсолютных порогов яркости. Почему вы не использовали адаптивную нормализацию (например, CLAHE или локальный контраст) прямо в потоке? Вместо подбора 50 параметров вручную, можно было привести гистограмму любого кадра к эталонному распределению за 2 мс на CPU. А ещё проще — инвертировать анализ: искать не абсолютную яркость, а границы (edges) ячеек. Границы устойчивы к экспозиции, в отличие от уровня серого.

Проблема №4: Временная фильтрация “в лоб”

Вы накапливаете состояния кадров, чтобы подтвердить их. Это увеличивает задержку (латентность). Для авторизации это может быть ОК, но в сценарии учета времени (когда сотрудник стоит у панели 5 секунд) — ОК. Но вы упустили детекцию смены состояния (переход 0->1). Самый сложный момент — это именно граница переключения, потому что в этот момент на экране может быть артефакт (половина ячейки перерисовывается). Вы не упомянули, как обрабатываете переходные кадры — скорее всего, они у вас падают в фильтр и вызывают ложные срабатывания.

  1. Жёсткая связь с предыдущей задачей (Сшивка микроскопа)

Читая MotionCode, я узнал ту же самую архитектурную драму, что была в микроскопии. Вот 4 точки пересечения:

Аспект Сшивка микроскопа (прошлая статья) MotionCode (текущая) Общий вывод Выбор подхода Отказались от глубоких нейросетей (детекторы точек) в пользу фазовой корреляции. Отказались от Object Detection в пользу классического CV (яркость + ROI). Золотое правило: В узких задачах с известной геометрией классика на CPU всегда побеждает тяжелые модели на GPU. Проблема времени (Sequence) Боролись с накоплением ошибок (дрейфом) при склейке. Использовали глобальную оптимизацию. Борются с ошибочными кадрами через накопление (фильтр большинством). Обе системы поняли, что один кадр — ничто. Только поток (время) дает истину. Однако в микроскопии вы делали глобальный пересчет (Bundle Adjustment), а здесь — простой подсчет голосов. Это слабее. Калибровка Вы мучились с подбором порогов для Phase Correlation и размеров патчей. Вы мучились с порогами яркости и временными окнами. Это проклятие классического CV. В микроскопии я предлагал гибрид с Mutual Information. Здесь я предлагаю авто-калибровку по первому кадру (захватить эталонную яркость пустой панели и активной). Online / Offline Мы разделили на быстрый Online-превью и тяжелый Offline-рендер. MotionCode — это чистый Online. Offline не предусмотрен. Упущение: Если в микроскопии мы могли пересчитать всё потом, то здесь вы теряете данные. Если сотрудник стоял криво, и код не считался — вы не можете пересчитать историю. Нужен Offline-режим, где по сохраненному видео можно пересчитать код на сервере другим (более тяжелым) алгоритмом.

  1. Что я предлагаю (как связать это с прошлым обсуждением и усилить)

Помните наш гибрид Phase Correlation + Mutual Information? Здесь точно такая же история, но с другими компонентами. Применим ту же философию к MotionCode.

А. Гибрид “Яркость + Края” (аналог ФК + ВИ)

· Грубо (Яркость): Ваш текущий метод анализа центральных областей — быстрый, но ненадежный при свете. · Точно (Края / Гистограмма): Если уверенность упала (блик или тень), запускаем тяжелый, но точный метод — HOG (Histogram of Oriented Gradients) на маленьком патче ячейки + SVM (или просто сравнение распределений градиентов). Это устойчиво к экспозиции. Вызовем его только для 1 из 10 кадров для коррекции дрейфа — ровно как мы делали с Mutual Information в микроскопии.

Б. Введение “Offline-Арбитра” (аналог глобальной оптимизации)

Ваше приложение должно сохранять сырые кадры (или сжатые дескрипторы) в буфер на 5 секунд. Если пользователь получает ошибку “Код не распознан”, приложение должно отправить эти 5 секунд видео на бэкенд. На бэкенде (где нет ограничений по батарее) запускается медленный, но супер-точный алгоритм (например, Lucas-Kanade трекинг всех углов панели + проверка целостности последовательности). Это спасет сценарий, когда телефон нагрелся и просел FPS.

В. Безопасность через “Шум” (как у нас было с вращением)

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

  1. Итоговая ретроспектива

В обеих историях прослеживается один и тот же путь героя:

  1. Наивный старт: «Возьмем нейросеть/FFT, она всё решит».

  2. Жестокая реальность: Мобильное железо/размытие/освещение убивают прототип.

  3. Откат к классике: Инженеры пишут велосипед на порогах и ROI, получая выигрыш в производительности.

  4. Ад калибровки: Бесконечные правки параметров под разные устройства.

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

Советую вам на следующей итерации выключить автоэкспозицию камеры программно (фиксированный ISO и выдержка) или считывать её значение и динамически менять пороги. Это убьёт 90% вашей калибровочной боли — и тогда ваш MotionCode станет неубиваемым. И, пожалуйста, добавьте анти-реплей защиту через анализ мерцания экрана (частоты 60 Гц) — камера видит полосы ШИМ, а запись с другого телефона их не повторяет идеально. Это как раз ваш “Mutual Information” для безопасности.

В лаборатории идеальных кадров не бывает, поэтому устойчивость важнее

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации