Привет, Хабр! Представьте, что вы пытаетесь обработать фотографию высокого разрешения на вашем смартфоне — добавить размытие, убрать шум или улучшить качество изображения. Кажется, задача проста, но за кулисами работает алгоритм, требующий немало вычислительных ресурсов. Речь идет о фильтре Гаусса – одной из самых популярных операций в области компьютерной обработки изображений.
Для каждого пикселя нужно выполнить множество операций сложения и умножения, особенно если размер фильтра велик. Это становится серьёзным препятствием, когда есть требования к работе в режиме реального времени, например, при обработке видео, адаптации для беспилотных летательных аппаратов и пр. Но что, если сказать, что такие вычисления можно ускорить в десятки раз с незначительной потерей качества?
В мае 2024 года группа исследователей Smart Engines опубликовала статью [1] в журнале MDPI Applied Sciences, в которой проводилось сравнение нескольких методов, предложенных для ускоренного приближенного вычисления гауссовской фильтрации. Сегодня мы расскажем о том, как она устроена математически и познакомим читателей с алгоритмами, которые помогают значительно быстрее посчитать ее приближенные версии.
Как работает фильтр Гаусса?
Основная задача гауссовской фильтрации — сглаживать изображение, уменьшая шум и устраняя ненужные детали, которые могут мешать анализу. Примеры до/после применения фильтра представлены на Рис. 1.
С математической точки зрения обработка изображения фильтром Гаусса есть свертка изображения с гауссовским ядром. На практике рассматриваются ядра конечного размера. Каждый пиксель заменяется взвешенным средним его окружения внутри окна размера , приложенного в рассматриваемом пикселе. Чем ближе соседние пиксели к текущему, тем больший "вклад” они дают в ответ. Рис. 2 иллюстрирует описанный процесс.
Формально гауссовское ядро, далее обозначаемое , определяется уравнением
где – стандартное отклонение. Процедура гауссовой фильтрации имеет вид:
Тут умножений и сложений на пиксель, а обозначает значение -го пикселя исходного изображения, – значение-го пикселя результирующего изображения, а . Согласно данному выражению, зависимость числа арифметических операций, необходимых для вычисления ответа, от размера окна квадратичная.
Гауссовское ядро обладает свойством сепарабельности, что помогает на порядок сократить вычислительную сложность. Это иллюстрируется следующими выкладками:
Так, свертка с двумерным гауссовым ядром может быть разложена на пару сверток с одномерными гауссовыми ядрами: вертикальную и горизонтальную. Отметим, что такое разложение само по себе не является приближением и по-прежнему дает точный результат. Присмотревшись, видим, что вычислительные затраты сократились до умножений и сложений на пиксель.
В дальнейшем ограничимся рассмотрением одномерных фильтров, подразумевая, что для аппроксимации двумерного фильтра Гаусса требуется их применение в обоих направлениях: горизонтальном и вертикальном.
Подходы к приближенному вычислению
Существует два основных типа аппроксимаций: фильтры с конечной импульсной характеристикой (КИХ) и фильтры с бесконечной импульсной характеристикой (БИХ).
КИХ-фильтры обеспечивают более близкое приближение к гауссовскому ядру. Это делает их хорошо подходящими для приложений, требующих точной фильтрации, таких как улучшение изображений и извлечение признаков. БИХ-фильтры с их рекурсивной природой больше подходят для ситуаций, где на первый план выходит эффективность вычислений. Они находят применение в обработке видео в реальном времени, обработке аудиосигналов или сценариях, где вычислительная мощность ограничена.
Для аппроксимации гауссовского фильтра КИХ-фильтром в качестве импульсной характеристики h используется приближение усеченной гауссианы. Сама аппроксимация имеет следующий вид:
В случае аппроксимации БИХ-фильтром используются рекурсивные выражения типа разностных уравнений, подобных следующему:
Коэффициенты , , и , , необходимо настраивать.
С БИХ-фильтрами мы разберемся в следующий раз, а сейчас речь пойдет о нескольких аппроксимациях первого типа. Здесь мы постарались объяснить схему работы “на пальцах”. Заинтересовавшиеся могут найти строгое математическое изложение в [1].
Stack blur и Bell blur
Алгоритм Stack blur был предложен Марио Клингеманном [2]. Используется метод "скользящего окна". Значения внутри окна выбраны так, чтобы крайние пиксели имели единичный вес, а затем веса увеличивались на 1 по мере продвижения к центральному пикселю – его вес будет наибольшим. Во время исполнения алгоритма окно перемещается вдоль строки или столбца (при горизонтальном или вертикальном проходах соответственно). При каждом наложении производится умножение элементов окна на соответствующие элементы изображения и последующее суммирование – пока это просто определение КИХ-фильтра. Весь смысл заключается в способе быстрого вычисления.
Обратимся к Рис. 3, который иллюстрирует то, как работает Stack blur в горизонтальном направлении с окном длины 5. Допустим, что мы посчитали ответ для пикселя со значением c и хотим посчитать ответ для пикселя со значением . Для этого необходимо всего лишь добавить к сумму “входящих” элементов (отмеченных “+1”) и вычесть сумму “уходящих” элементов (отмеченных “-1”), т. е. затратить 1 сложение и 1 вычитание. На соседних итерациях алгоритма “входящие” элементы отличаются на пару крайних пикселей – чтобы пересчитать сумму “входящих” элементов, нужно выполнить 1 сложение и 1 вычитание. Аналогично для уходящих. Итого 6 аддитивных операций.
При таком алгоритме одномерное гауссовское ядро gσ аппроксимируется кусочно-линейной функцией
что на графике выглядит как уголок, близкий к гауссиане (Рис. 4).
Метод Stack blur можно усложнить, если заменить суммы входящих и уходящих пикселей на взвешенные суммы. Эта идея была описана в [3] и названа Quadratic Stack blur, или Bell blur, т. к. в качестве весов выбраны те же, что используются в алгоритме Stack blur, а полученное кусочно-постоянное приближение гауссианы визуально имеет форму колокола (Рис. 4). Принцип работы аналогичен методу Stack blur и схематично представлен на Рис.5.
Итого для получения ответа нужно к ответу прибавить Stack blur по “входящим” пикселям и вычесть Stack blur по “уходящим” пикселям – всего 14 аддитивных операций. Математическое выражение для получающейся кусочно-постоянной функции-аппроксиматора гауссианы здесь мы не приводим.
Running sums
Еще одну кусочно-постоянную аппроксимацию гауссианы и способ быстрого вычисления свертки с ней предложили Эльбохер и Верман [4]. В отличие от метода Bell blur, в котором значения весов являются целыми числами, метод Running sums оперирует вещественными весами. Авторы определяют функцию из k ступенек с высотами , , на отрезке (Рис. 6). Высота и длина ступеней определяются путем минимизации среднеквадратичной ошибки.
Для быстрого вычисления используется концепция интегральных изображений. Для каждой строки или столбца изображения (при горизонтальном или вертикальном проходах соответственно) предварительно заполняется массив I, хранящий кумулятивные суммы
так что в -том элементе массива содержится сумма значений всех пикселей в строке/столбце от нулевого до -го. Для того чтобы понять, как это упрощает вычисления, обратимся к Рис. 7. Он иллюстрирует идею вычисления для случая .
Запишем выражение для результата фильтрации в пикселе со значением и переупорядочим в нем слагаемые:
Левая часть данного тождества соответствует суммированию по вертикальным цветным прямоугольникам, правая – по горизонтальным штрихованным. Обозначив и выразив второй множитель внутри каждого слагаемого через элементы массива , можно переписать результат следующим образом:
То есть для получения результата необходимо затратить 4 вычитания, 4 сложения (1 идет из заполнения массива I) и 4 умножения. В случае произвольного метод требует аддитивных и мультипликативных операций на пиксель. Заметим также, что метод требует использование дополнительной памяти для хранения кумулятивной суммы по строке или столбцу – , где и – высота и ширина изображения.
Что по точности и сложности?
Для каждого метода мы подсчитали ошибку аппроксимации и сложность в смысле количества необходимых операций на пиксель. Точность мы получали путем минимизации среднеквадратичной ошибки (MSE), задаваемой выражением
для точек, равномерно взятых на интервале с = 10. Результаты представлены в Таблице 1.
Таблица 1. Сравнение аппроксимаций гауссовского фильтра для изображения размера по количеству аддитивных (# +) и мультипликативных (# ·) операций на пиксель, ошибке и дополнительной памяти. – размер ядра.
Метод |
# + |
# · |
Ошибка |
Доп. память |
Фильтр Гаусса |
2(K − 1) |
2K |
зависит от K |
O(1) |
Stack blur |
12 |
0 |
9.35e−6 |
O(1) |
Bell blur |
28 |
0 |
4.40e−6 |
O(1) |
Running sums, k = 3 |
12 |
6 |
1.43e−5 |
O(max{h, w}) |
Running sums, k = 4 |
16 |
8 |
7.91e−6 |
O(max{h, w}) |
Тестирование на устройствах
Мы запрограммировали методы на языке C++ и опробовали их на процессорах Intel Core i9-9900KF (с архитектурой x86_64) и ARM Cortex-A73 (с архитектурой ARMv8). Тестировали 2 типа входных данных: 8-битные беззнаковые целые (uint8) и 32-битные с плавающей точкой (real32). Фильтр Гаусса и метод Running sums используют вещественные коэффициенты – для целочисленных реализаций мы делали квантование весов. Также аппроксимации были распараллелены по данным при помощи SIMD расширений. На x86_64 применялись интринсики семейства SSE, на ARMv8 – интринсики семейства NEON. Использовались 128-битные регистры, способные хранить 4 значения с плавающей точкой одинарной точности или 16 беззнаковых целых значений. Каждый метод запускался на 1000 случайно инициализированных изображениях размером 1024 x 1024, время работы усреднялось по серии этих запусков. Численные результаты приведены в Таблице 2.
Таблица 2. Среднее время работы рассмотренных аппроксимаций ().
Метод |
Время работы, мс (x86_64) |
Время работы, мс (ARMv8) |
||
без SIMD |
с SIMD |
без SIMD |
с SIMD |
|
Фильтр Гаусса (real32) |
81.3 |
– |
281.5 |
– |
Фильтр Гаусса (uint8) |
12.0 |
– |
128.0 |
– |
Stack blur (real32) |
6.0 |
3.5 |
20.4 |
17.7 |
Stack blur (uint8) |
3.8 |
1.6 |
15.5 |
14.7 |
Bell blur (real32) |
6.7 |
4.3 |
31.6 |
26.8 |
Bell blur (uint8) |
5.7 |
2.6 |
22.5 |
22.0 |
Running sums, k = 3 (real32) |
8.3 |
5.6 |
40.7 |
23.2 |
Running sums, k = 3 (uint8) |
7.0 |
2.7 |
32.9 |
14.1 |
Running sums, k = 4 (real32) |
9.7 |
5.9 |
46.8 |
25.0 |
Running sums, k = 4 (uint8) |
8.3 |
3.0 |
39.4 |
15.7 |
Метод Stack Blur в реализации над целыми числами оказался самым быстрым на x86_64. На ARMv8 его обогнал метод Running sums в 3-ступенчатом варианте. В сравнении с настоящей гауссовской фильтрацией эти методы оказались быстрее в 50 и 20 раз соответственно.
Визуальный эффект от использования приближенных методов на картинке можно посмотреть на Рис. 5. В целом, все рассмотренные аппроксимации демонстрируют хороший эффект, внешне практически неотличимый от оригинала.
Заключение
Сегодня мы рассмотрели три способа приближенного вычисления гауссовского фильтра. Метод Stack blur показал себя как самый быстрый на платформе x86_64, благодаря минимальным вычислительным затратам. В то же время метод Running sums в 3-ступенчатом варианте продемонстрировал высокую производительность на ARMv8, что делает его отличным выбором для мобильных устройств и встраиваемых систем. Метод Bell blur обеспечивает более точное приближение гауссовского ядра при незначительном увеличении сложности расчетов.
Такие методы могут найти широкое применение на практике, в таких задачах реального времени, как компьютерное зрение, обработка видео и изображений, например, тут, а также в области искусственного интеллекта. Выбор подходящего метода зависит от того, что важнее в той или иной ситуации — точность, время работы или ограничения по памяти.
Источники:
Rybakova, E. O.; Limonova, E. E.; Nikolaev, D. P. “Fast Gaussian Filter Approximations Comparison on SIMD Computing Platforms,” Appl. Sci. 2024, 14, 4664.
https://observablehq.com/@jobleonard/mario-klingemans-stackblur
Elboher, E.; Werman, M. “Efficient and accurate Gaussian image filtering using running sums,” Proc. ISDA, 2012, стр. 897–902.
Комментарии (21)
adeshere
28.01.2025 16:33Глянул на годы выхода публикаций. Для меня это просто шок-контент ;-) Я был уверен, что все это придумано и описано лет на 50 раньше. Мы у себя в программе используем подобные трюки где-то с 1989г, когда у нас встал вопрос о скорости вычислений. Нам тогда и в голову не приходило, что в этом может быть какая-то новизна.. Единственное существенное отличие - у нас в сигналах присутствует много Nan-значений, которые приходится обрабатывать особо. Из-за них алгоритмы получаются совсем не такими красивыми, как у классиков :-( Ну и еще у нас временные ряды, то есть задачка во всех случаях исключительно одномерная.
Sdima1357
28.01.2025 16:33Нет тут особой новизны. У меня тоже лет 20 назад были примерно такие же результаты как в статье( для feature detection DOG и похожих ) но на заметно более слабых компьютерах. Точные цифры не помню, надо посмотреть реализацию и замеры для точных цифр, но было быстрее чем open cv раз в пять Тут важно правильно заботиться о попадании в кеш
adeshere
28.01.2025 16:33Тут важно правильно заботиться о попадании в кеш
Я тогда такого слова вообще не знал ;-)
Хотя, в начале 1990-х уже как бы появился 486 (с кэшем L1), но мы жили и работали в горной экспедиции, куда новая техника подъезжала с опозданием лет эдак на 10. Да и в институте (куда я вернулся после распада СССР) в 90-е было не сильно лучше. Приходилось выкручиваться на старом железе за счет алгоритмов. Так как как раз в это время наблюдения массово переводились на минутный опрос, и мегабайтные ряды у нас стали нормой.
adenis78
28.01.2025 16:33пока проверял, когда появились контроллеры кэша для 386го и как кэш был устроен, истекло время редактирования.
Вот про микросхему 82385, контроллер совмещнного кэша программ-данных для intel 80386 https://theretroweb.com/chips/3095
К ней требовались еще микросхемы быстрой статической памяти для хранения кэшируемых данных.
Первые экземпляры = 1987 год.
c0mmandor
28.01.2025 16:33в таблице производительность в наносекундах на элемент или в миллисекундах на все изображение? явно не сказано, а кажется сомнительным что фильтр гаусса с сигмой 10 для FP32 изображения 1024х1024 можно посчитать на x86 процессоре за 81.3 нс
Sdima1357
28.01.2025 16:33На точку конечно. 4 мегабайта данных только прочитать из памяти нужно порядка миллисекунды или сотен микросекунд (10-89 гигабайт в секунду)
Jijiki
28.01.2025 16:331024x1024 уже в памяти - когда я жму на применить фильтр к картинке например в Гимп на ползунки X/Y фильтр применяется на всю картинку почти мгновенно, тут скорее всего время без загрузки картинки 1024х1024 и в примере чб(поидее это 1024х1024х2хsizeof(...) байт ) и еще процессор i9 в тесте написано по крайней мере (мне просто стало интересно я запустил Гимп создал чб 1024х1024(32FP) накалякал чтото там чтоб был чб и применил фильтр, плюс в Гимп может модный фильтр, тут пишут что способ Stack Blur разновидност Гаусса быстрее)
c0mmandor
28.01.2025 16:33я представляю сколько работает фильтр гаусса в разных условиях, коммент больше направлен на улучшение описания. А то в таблице единица измерения указана не полностью, т.к. видимо это не просто время, это время на один элемент(пиксел) изображения.
единица измерения "почти мгновенно" улыбнулаJijiki
28.01.2025 16:33допустим 1024 на 1024 на 2 канала частота 4 ГГц доступ к памяти 3МГЦ, допустим выборка окна 4 на 4 где 4 на 4 в лучшем случае на 2 канала в 1 регистр тогда за такт выполнится 1 к операций поидее ну допустим при 4 ГГЦ это 4 тысячи, 65536 / 4000 (просто предположил)
c0mmandor
28.01.2025 16:33тут как чего не допускай, но за 81 наносекунду не может быть обработано 1048576 элементов (это 1024*1024), т.к. это около 77 фемто секунд на элемент получится (если арифметика меня не подвела или я ее) 81e-9 / (1024*1024). это ж процессор на терра герцах работать должен, какой бы он i9 ни был, но это пока рановато
Jijiki
28.01.2025 16:33у нас предположим 1 момент все изображение 1024х1024 - а следующий момент у нас маска выборка по изображению 256 на 256 элементов по 4 на 4 чтото такое. я тоже могу ошибаться просто почемуто подумал что так легче обсчитать если учесть что влезает в регистр маска(отталкивался от словосочетания частота дискретизации. )
c0mmandor
28.01.2025 16:33да нет, тут как ни крути, оценка в 81 наносекунду для всего изображения не физична. просто в таблице стоит добавить что это nsec / pixel тогда получается 84 миллисекунды на все изображение, вот в это поверю.
SmartEngines Автор
28.01.2025 16:33Вы правы, наносекунды получатся, если оценивать время на пиксель изображения. В Таблице 2 приведено время обработки всего изображения, поэтому правильная единица измерения - миллисекунды. Спасибо, исправили в статье.
Andy_U
28.01.2025 16:33А со скоростью вычисления свертки с помощью преобразования Фурье вы свои свои алгоритмы не сравнивали?
SmartEngines Автор
28.01.2025 16:33Да, это сравнение мы тоже делали. Оно есть в статье, а здесь мы напишем про него в следующей части нашего материала. Спойлер: оказалось не быстрее.
lgorSL
28.01.2025 16:33Позвольте уточнить, а Вы гамма-коррекцию для изображений делали? Для float32 всё прекрасно, но в случае восьми бит на пиксель либо гамма коррекции нет и фильтр размывает физически некорректно, либо восемь бит с гамма-коррекцией теряют цвета и нужно повышать точность (например, до шестнадцати бит)
Главный подвох в том, что количество фотонов от пикселя яркостью в 127 и 255 отличается совсем не в два раза, а примерно в четыре. Если быть точным, в (255/127)^2.1 раз
Sdima1357
1. В такой статье обойтись без упоминания https://en.m.wikipedia.org/wiki/Central_limit_theorem ? Повторная свертка прямоугольников (бегущее среднее) очень быстро сходится к гауссиану.
Когда ошибка 10^-6, картинки показывать не нужно, они одинаковые
Refridgerator
Делал такой трюк для полосового фильтра с линейной ФЧХ - 4 скользящих средних с пропорциями 5,6,7,8 дают подавление боковых лепестков в 65 дБ. 6, 7, 8, 9, 10 - 80 дБ.