МИНИСТЕРСТВО ОБРАЗОВАНИЯ РЕСПУБЛИКИ БЕЛАРУСЬ БЕЛОРУССКИЙ ГОСУДАСТРВЕННЫЙ УНИВЕРСИТЕТ Факультет прикладной математики и информатики Кафедра математического моделирования и управления ЧЕКАН РОСТИСЛАВ ВЛАДИМИРОВИЧ Дипломная работа студента 5 курса 6 группы Руководитель “Допустить к защите” Дубров Борис Михайлович Зав. Кафедры ММУ Кандидат физико-математических ______________________ «__»_____________2010 г. наук, доцент Рецензент Минск 2010 АННОТАЦИЯ В данной дипломной работе изучаются различные реализации фильтра Гаусса и производится их сравнительный анализ с параллелизацией оптимального метода. АНАТАЦЫЯ У дадзенай дыпломнай працы вывучаюцца розныя рэалізацыі фільтра Гаўса і робіцца іх параўнальны аналіз з параллелизацией аптымальнага метада. ANNOTATION Different implementations of the Gaussian filter are researched and comparative analysis was made in this thesis including parallelization of the optimal method. РЕФЕРАТ Отчёт по дипломной работе, 56 страниц, 7 источников, 2 приложения. ФИЛЬТРА ГАУССА, РАЗМЫТИЕ ПО ГАУССУ, РЕКУРСИВНЫЙ АЛГОРИТМ, ПАРАЛЛЕЛИЗАЦИЯ, OPENCL Объект исследования – реализации фильтра Гаусса. Цель работы – исследовать существующие реализации алгоритмов фильтрации по Гауссу, выделить оптимальные, улучшить с помощью параллелизации. Метод исследования – аналитический метод, практическая реализация. Результатом работы является программа, которая позволяет посмотреть различные реализации фильтра Гаусса. Содержание ВВЕДЕНИЕ ....................................................................................................................................................................... 5 1. ФИЛЬТРЫ .................................................................................................................................................................... 7 1.1 ФИЛЬТР ГАУССА .................................................................................................................................................... 7 1.2 БАЗОВЫЙ СЛУЧАЙ ФИЛЬТРА ГАУССА .................................................................................................................. 9 1.3 ПРИМЕНЕНИЕ ФИЛЬТРА ГАУССА С ПОМОЩЬЮ ПРЕОБРАЗОВАНИЯ ФУРЬЕ.................................................... 13 1.3.1 Дискретное преобразование Фурье ......................................................................................................... 13 1.3.2 Быстрое преобразование Фурье ............................................................................................................... 15 1.4 РЕКУРСИВНЫЙ ФИЛЬТР ГАУССА ........................................................................................................................ 18 2. МЕТОДЫ ПАРАЛЛЕЛИЗАЦИИ. OPENCL ........................................................................................................ 22 2.1 ВЫБОР ПЛАТФОРМЫ ............................................................................................................................................ 22 2.2 OPENCL ................................................................................................................................................................ 24 2.3 ПРИМЕНЕНИЕ ТЕХНОЛОГИИ OPENCL К РЕКУРСИВНОМУ ФИЛЬТРУ ............................................................... 26 3. СРАВНИТЕЛЬНЫЙ АНАЛИЗ, УСЛОВИЯ ТЕСТИРОВАНИЯ, РЕЗУЛЬТАТЫ ........................................ 27 4. ВЫВОДЫ .................................................................................................................................................................... 36 ЗАКЛЮЧЕНИЕ ............................................................................................................................................................. 38 СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ ................................................................................................ 39 ПРИЛОЖЕНИЕ А. РУКОВОДСТВО ПОЛЬЗОВАТЕЛЯ ..................................................................................... 40 ПРИЛОЖЕНИЕ Б. ЛИСТИНГ ПРОГРАММЫ ...................................................................................................... 42 Введение В настоящее время объемы различных видов мультимедиа информации неуклонно растут. Производство фильмов, музыки и музыкальных клипов на профессиональных студиях не сокращается, а количество любительских записей постоянно увеличивается. С одной стороны это является следствием большего оборота денежных средств в индустрии развлечений, а с другой возросшей доступность необходимых технических средств. В связи с широким распространением сетей Интернет упрощается доступ и обмен мультимедиа информацией. Следствием этого является проблема поиска, обработки и анализа необходимой информации. В частности методы распознавания образов и понимания сцены в настоящее время из-за отсутствия эффективных универсальных алгоритмов применяются в узких предметных областях. Для успешной обработки изображений в графическом дизайне и моделировании необходимо иметь качественно быстрые фильтры начальной обработки изображений. Размытие изображений играет большую роль в современных областях компьютерной графики. Размытие изображений часто бывает направлено на имитацию близорукости (в тех случаях, когда близорукость становится желательной или даже необходимой). Так, размытие отдельных частей изображения часто используют из соображений цензуры. В ряде случаев размытие является неотъемлемой частью различных техник коррекции изображения, направленных на устранение специфических дефектов (излишняя детализация, дефекты сканирования, царапины, пыль). Известно, что фотомодели и их фотографы используют специальные процедуры размытия фотографических изображений для достижения эффекта устранения морщин. Размытые изображения также лучше поддаются сжатию (так, при сохранении в формате JPEG графический файл имеет меньший размер, а также менее выраженные артефакты компрессии). Различные техники размытия изображения доступны во всех современных графических редакторах. Одним из наиболее важных алгоритмов размытия изображений является т. н. размытие по Гауссу. Разработка и анализ алгоритмов для решения проблемы фильтрации цифровых сигналов будет являться предметом данной работы оптимальных по скорости на больших объёмах данных. 1. Фильтры Цифровой фильтр — любой фильтр, обрабатывающий цифровой сигнал с целью выделения и/или подавления определённых частот этого сигнала. В отличие от цифрового, аналоговый фильтр имеет дело с аналоговым сигналом, его свойства не дискретны, соответственно передаточная функция зависит от внутренних свойств составляющих его элементов. Особую роль среди цифровых фильтров играют фильтры нижних частот (ФНЧ). ФНЧ – фильтр, эффективно пропускающий частотный спектр сигнала ниже некоторой частоты (частоты среза), и уменьшающий (или подавляющий) частоты сигнала выше этой частоты. Степень подавления каждой частоты зависит от вида фильтра. Широко применяется как аппаратная (на основе специализированных микросхем или FPGA), так и программная (на базе процессоров общего назначения или сигнальных процессоров) реализация фильтров нижних частот. Мы же будем рассматривать различные варианты программной реализации одного из ФНЧ - фильтра Гаусса. 1.1 Фильтр Гаусса В электронике и обработке сигналов, фильтром Гаусса называют фильтр, чья импульсная характеристика является функцией Гаусса. Импульсная характеристика - выходной сигнал динамической системы как реакция на входной сигнал, то есть наши данные (импульс). Гауссов фильтр спроектирован таким образом, чтобы свести к минимуму отклонения от входных данных переходной функции, реакцию системы на входное ступенчатое воздействие при нулевых начальных условиях во время нарастания и спада. Такое поведение связано с тем, что фильтр Гаусса имеет минимальную групповую задержку, меру прохождения данных через ядро фильтра. Математически, фильтр Гаусса представляет собой свёртку входного сигнала и функции Гаусса. Это преобразование также известно как преобразование Вейерштрасса. Фильтр Гаусса обычно используется в цифровом виде для обработки двумерных сигналов с целью снижения уровня шума. Визуально данных эффект представляет собой лёгкое размытие, как при наблюдении через мутное стекло. Стоит отметить весьма ограниченную скорость фильтра Гаусса при реализации с помощью явного метода, особенно заметную на больших объёмах данных. 1.2 Базовый случай фильтра Гаусса Импульсная характеристика одномерного фильтра Гаусса может быть представлена в виде: g ( x) a e a x 2 . А со среднеквадратичным отклонением: 1 e 2 g ( x) x2 2 2 . Для двумерного случая мы представляем фильтр как произведение двух одномерных случаев, поэтому получим: g ( x, y ) 1 2 2 e x2 y2 2 2 . где x - расстояние от центра по горизонтальной оси, y - расстояние от центра по вертикальной оси, σ- среднеквадратичное отклонение распределения гаусса. Рассмотрим более общий случай. Результирующая формула будет иметь следующий вид для n-мерного случая: L( x1 ,..., xn , t ) ... f ( x c 1 u1 ,..., xn un )g N (u1 ,..., un , t )du1...dun u1 un Однако для реализации данное определение может быть нецелесообразным в виду непрерывности. Поэтому в дальнейшем можем сделать некоторые упрощения. Так как гауссовское ядро обладает свойствами отделимости g N ( x1 ,..., xn , t ) G( x1 , t )...G( xN , t ) , то n-мерная операция свёртки может быть разбита на множество одномерных применений гауссова ядра для каждого измерения: L( x1 ,..., xn , t ) ... f ( x u ,..., x c u1 u n 1 1 n un )G (u1 , t )du1...G(un , t )dun , где 1 G ( x, t ) 2 t e x2 2t . и где t является корнем дисперсии и равно σ2. Свойство отделимости на практике имеет большое значение, так как позволяет упростить вычисления и привести их к одномерному случаю. Далее будем рассматривать именно его. Для реализации одномерного шага сглаживания наиболее простым способом является операция свёртки дискретного сигнала и гауссового ядра: L ( x, t ) f ( x n)G(n, t ) , n где G ( n, t ) 1 2 t e n2 2t . что в свою очередь может быть ограничено для сигнала с конечной импульсной характеристикой: L ( x, t ) M f ( x n)G (n, t ) , n M для M выбрано достаточно большим, что: 2 G(u, t )du 2 u m G(v,1)dv . vM t Общий выбор выбора M заключается в создании зависимости её от дисперсии, к примеру: M C 1 C t 1, где С зачастую выбирается где-то между 3 и 6. Использование заданного гауссова ядра может привести к проблемам точности в случаях, когда важны точность и надёжность. При незначительной роли погрешности при вычислениях (10 - 6 до 10 - 8 ) , ошибки вносимые ограничением ядра незначительны. Однако если точность важна, то есть более лучшие альтернативы гауссову ядру как оконной функции, к примеру, оконные функции Хэмминга, Блэкмана, Кайзера (см. [5]) будут меньше изменять спектр, чем это сделает ядро гаусса. А так как функция Гаусса быстро убывает на концах, то рассматривание значений на целесообразным. больше 10 - 8 не является 1.3 Применение фильтра Гаусса с помощью преобразования Фурье 1.3.1 Дискретное преобразование Фурье Итак, вспомним, что же такое преобразование Фурье – это интегральное преобразование, которое ставит функцию вещественной переменой другую функцию вещественной переменной и может быть записано в виде: 1 F ( w) 2 f ( x)e ixw dx . Эта новая функция описывает коэффициенты («амплитуды») при разложении исходной функции на элементарные составляющие — гармонические колебания с разными частотами. Не будем перечислять все свойства преобразования, а отметим только важные для нас. 1. Формула обращения позволяет получить искомую функцию f ( w) 2. 1 2 ixw F ( w ) e dw . Теорема о свёртке. Свёртка функций — операция, показывающая «схожесть» одной функции с отражённой и сдвинутой копией другой. Пусть f ,g : интегрируемые — относительно называется функция: две функции вещественной меры Лебега. Тогда переменной, их свёрткой ( f g )(t ) f ( )g (t )d . f , g L1 () , тогда Тогда теорема о свёртке гласит: если 2 F ( f ) F ( g ) . F ( f g) Так как мы работаем с изображениями, то представим перечисленные выше высказывания в дискретном виде. Прямое преобразование примет вид: N 1 X k xn e 2ikn N n 0 , k 0,..., N 1 . Обратное преобразование: 1 xn N N 1 X k 0 k e 2ikn N , n 0,..., N 1 . Теорема о свёртке: f ( x) g ( x) F ( x) G ( x) . Таким образом, мы можем выполнить частотную фильтрацию изображения в частотной области. Это означает, что при частотной фильтрации выполняются прямое и обратное пространственно-частотное преобразование, в нашем случае двумерное дискретное преобразование Фурье (ДПФ) преобразует изображение, заданное в пространственной координатной системе (x, y) , в двумерное дискретное преобразование изображения, заданное в частотной координатной системе (u, v). В соответствии с теоремой о свертке, свертка двух функций в пространственной области может быть получена ОДПФ произведения их ДПФ. Таким образом, алгоритм фильтрации по Гауссу в частотной области будет выглядеть следующим образом: выполнить двумерное ДПФ входного изображения f(x,y) (подвергаемого фильтрации) размером (N *M), получить F(u,v); вычислить передаточную характеристику фильтра Гаусса в частотной области r 2 (u ,v ) H (u, v) e 2 2 , размер матрицы (N*M); выполнить децентрирование характеристики H(u,v); выполнить поточечное умножение S (u, v) F (u , v) H (u , v), u [0, N 1], v [0, M 1] , выполнить ОДФП На практике ДПФ крайне не эффективно, так как имеет сложность O(N2), поэтому обычно применяют быстрое преобразование Фурье (БПФ). 1.3.2 Быстрое преобразование Фурье Алгоритм быстрого преобразования Фурье (БПФ) - это оптимизированный по скорости способ вычисления ДПФ. Основная идея заключается в двух пунктах. 1. Необходимо разделить сумму ДПФ из N слагаемых на две суммы по N/2 слагаемых, и вычислить их по отдельности. Для вычисления каждой из подсумм, надо их тоже разделить на две и т.д. 2. Необходимо повторно использовать уже вычисленные слагаемые. Применяют либо "прореживание по времени" (когда в первую сумму попадают слагаемые с четными номерами, а во вторую - с нечетными), либо "прореживание по частоте" (когда в первую сумму попадают первые N/2 слагаемых, а во вторую - остальные). Оба варианта равноценны. В силу специфики алгоритма приходится применять только N, являющиеся степенями 2. Рассмотрим случай прореживания по времени. Введём определение поворачивающегося множителя: . Определим еще две последовательности: {x[even]} и {x[odd]} через последовательность {x} следующим образом: X[even]n = X2n, X[odd]n = X2n+1, n = 0, 1,..., N/2-1. Пусть к этим последовательностям применены ДПФ и получены результаты в виде двух новых последовательностей {X[even]} и {X[odd]} по N/2 элементов в каждой. Утверждается, что элементы последовательности {X} можно выразить через элементы последовательностей {X[even]} и {X[odd]} по формуле: . Согласно второй части формулы вышеописанной, получим: . ДПФ можно вычислить также по формуле: . Также по этой теореме видно, что отпадает необходимость хранить вычисленные X[even]k и X[odd]k после использования при вычислении очередной пары и одно вычисление можно использовать для вычисления двух элементов последовательности {X}. На этом шаге будет выполнено N/2 умножений комплексных чисел. Если мы применим ту же схему для вычисления последовательностей {X[even]} и {X[odd]}, то каждая из них потребует N/4 умножений, итого еще N/2. Продолжая далее в том же духе log2N раз, дойдем до сумм, состоящих всего из одного слагаемого, так что общее количество умножений окажется равно (N/2)log2N, что явно лучше, чем N2 умножений по формуле оригинального ДПФ. Если N четно, то это разделение можно продолжать рекурсивно до тех пор, пока не дойдем до двух точечного преобразования Фурье, которое вычисляется по следующим формулам: X 0 x0 x1 . X 1 x0 x1 1.4 Рекурсивный фильтр Гаусса Существует также ещё один метод фильтрации, основанный на аппроксимации преобразования Фурье гауссового ядра, который широко изложен в [1]. Покажем основную идею данного метода. Для этого представим экспоненту в виде ряда Тэйлора, тогда ядро запишется в виде: t2 g (t ) 1 1 e 2 (t ) , a0 a 2 t 2 a 4 t 4 a6 t 6 2 где a0 = 2.490895, a 2 = 1.466003, a 4 = -0.024393, a 0 = 0.178257. Далее мы будем аппроксимировать не само гауссово ядро, а его преобразование Фурье, которое хорошо известно: G ( w) e 2 w2 2 1 t F e 2 2 2 . Тогда, подставляя вместо σ2 - q, мы получим выражение вида: Gq ( w) A0 . a0 a2 (qw) 2 a4 (qw) 4 a6 (qw) 6 И при s = jw: Gq ( s ) A0 . a0 ( a 2 q ) s ( a 4 q 4 ) s 4 ( a6 q 6 ) s 6 2 2 Тогда выражение (выше), может быть разложено на множители Gq (s ) = Gl (s) * Gr (s) : GL ( s ) A1 (1.1668 qs )(3.20373 2.21567 qs q 2 s 2 ) . И GR ( s ) A1 (1.1668 qs)(3.20373 2.21567qs q 2 s 2 ) . После этого для представления G (s) в H(z), моно было бы воспользоваться стандартным билинейным преобразованием: s 1 z 1 . 1 z 1 Но это бы привело к тому что в передаточной функции появились бы нули, которых нам лучше избежать. Поэтому мы применим технику, описанную в [6] и представим s 1 z 1 для Gl (s) и s z 1 для Gr (s) . Принимая T = 1, T получим: H L ( z ) GL ( s) s 1 z 1 A0 , (1.1668 q(1 z ))(3.20373 2.21567q(1 z 1 ) q 2 (1 z 1 ) 2 ) 1 и H R ( z ) GR ( s) s z 1 A0 . (1.1668 q( z 1))(3.20373 2.21567q( z 1) q 2 ( z 1) 2 ) Оба выражения могут быть переписаны как стандартные полиномы степени z и z 1 : H L ( z) A2 , b0 b1 z b2 z 2 b3 z 3 H R ( z) A2 , где b0 b1 z b2 z 2 b3 z 3 1 1 b0 1.5725 2.44413 q 1.4281 q 2 b1 2.44413 q 2.85619 q 2 1.26661 q 3 b2 1.4281 q 2 1.26661 q 3 . b3 0.422205 q 3 Тогда реализация [1] советует следующую фильтрующую стратегию. Входные данные сначала фильтруются в прямом направлении согласно выражению для H L . Тогда результат этой фильтрации, назовём его w[n], фильтруется в обратном направлении согласно выражению для H R , и разностные выражения принимают следующий вид: Прямое: w [n] B in [n] обратное: b1 w [n 1] b2 w [n 2] b3 w [n 3] , b0 out [n] B w [n] b1out [n 1] b2 out [n 2] b3 out [n 3] . b0 Оба выражения используют нормализационную константу, которая имеет вид: B 1 b1 b2 b3 . b0 Стандартные рекомендации по выбору константы q представляют собой: 0.98711 0 0.96330 q 3.97156 4.14554 1 0.26891 0 , 0 2.5 ,0.5 0 2.5 . Итого сам фильтр сначала применяется по вертикали, а затем по горизонтали. Средняя погрешность не превышает 10 3 , что составляет не более 0.68% согласно [1]. 2. Методы параллелизации. OpenCL Идея распараллеливания вычислений основана на том, что большинство задач может быть разделено на набор меньших задач, которые могут быть решены одновременно. Обычно параллельные вычисления требуют координации действий. Параллельные вычисления существуют в нескольких формах: параллелизм на уровне битов, параллелизм на уровне инструкций, параллелизм данных, параллелизм задач. Параллельные вычисления использовались много лет в основном в высокопроизводительных вычислениях, но в последнее время к ним возрос интерес вследствие существования физических ограничений на рост тактовой частоты процессоров. Параллельные вычисления стали доминирующей парадигмой в архитектуре компьютеров, в основном в форме многоядерных процессоров. Писать программы для параллельных систем сложнее, чем для последовательных, так как конкуренция за ресурсы представляет новый класс потенциальных ошибок в программном обеспечении, среди которых состояние гонки является самой распространённой. Если при вычислении не применяются циклические (повторяющиеся) действия, то N вычислительных модулей никогда не выполнят работу в N раз быстрее, чем один единственный вычислительный модуль. А так как при реализации фильтра Гаусса применяется в основном циклическая обработка данных, то разумно будет применить один из методов параллелизации. 2.1 Выбор платформы Наиболее распространенными видами параллелизации на данный момент является многопоточность стандартными средствами центрального процессора и многопоточность средствами GPU. К первому типу относится наиболее универсальная платформа под названием OpenMP. OpenMP реализует параллельные вычисления с помощью многопоточности, в которой «главный» (master) поток создает набор подчиненных (slave) потоков и задача распределяется между ними. Предполагается, что потоки выполняются параллельно на машине с несколькими процессорами (количество процессоров не обязательно должно быть больше или равно количеству потоков). Количество создаваемых потоков может регулироваться как самой программой при помощи вызова библиотечных процедур, так и извне, при помощи переменных окружения. Однако процессоры общего назначения менее приспособлены к интенсивным арифметическим вычислениям, в отличие от GPU, которые проектируются как раз для этих целей. Поэтому посмотрим в их сторону, что приводит нас к следующему понятию. GPGPU (англ. General-purpose graphics processing units — «GPU общего назначения») — техника использования графического процессора видеокарты для общих вычислений, которые обычно проводит центральный процессор. На данный момент существуют следующие реализации: AMD FireStream — технология GPGPU, позволяющая программистам реализовывать алгоритмы, выполнимые на графических процессорах ускорителей ATI. CUDA — технология GPGPU, позволяющая программистам реализовывать на языке программирования Си алгоритмы, выполнимые на графических процессорах ускорителей GeForce восьмого поколения и старше (GeForce 8 Series, GeForce 9 Series, GeForce 200 Series), Nvidia Quadro и Nvidia Tesla компании Nvidia. Технология CUDA разработана компанией Nvidia. Direct3D 11 — вычислительный шейдер (англ. Compute Shader). OpenCL является языком программирования задач, связанных с параллельными вычислениями на различных графических и центральных процессорах. Как видно, каждый производитель пытается продвинуть на рынок именно свою технологию, и только OpenCL является открытым стандартом, который поддерживается на большинстве современных ускорителях. 2.2 OpenCL OpenCL (от англ. Open Computing Language — открытый язык вычислений) — платформа для написания компьютерных программ, связанных с параллельными вычислениями на различных графических центральных процессорах (CPU). В фреймворк OpenCL (GPU) и входят язык программирования, который базируется на стандарте C99, и интерфейс программирования приложений (англ. API). OpenCL обеспечивает параллелизм на уровне инструкций и на уровне данных и является реализацией техники GPGPU. OpenCL является полностью открытым стандартом, его использование не облагается лицензионными отчислениями. Цель OpenCL состоит в том, чтобы дополнить OpenGL и OpenAL, которые являются открытыми отраслевыми стандартами для трёхмерной компьютерной графики и звука, пользуясь возможностями GPU. OpenCL разрабатывается и поддерживается некоммерческим консорциумом Khronos Group, в который входят много крупных компаний, включая Apple, AMD, Intel, nVidia, Sun Microsystems, Sony Computer Entertainment и другие. Машина, на которой проводятся вычисления может содержать процессоры x86, x86-64, Itanium, SpursEngine (Cell), NVidia GPU, AMD GPU, VIA (S3 Graphics) GPU. Для каждого из этих типов процессов существует свой SDK (кроме разве что VIA), свой язык программирования и программная модель. OpenCL задумывался как технология для создания приложений, которые могли бы исполняться в гетерогенной среде. Более того, он разработан так, чтобы обеспечивать комфортную работу с такими устройствами, которые сейчас находятся только в планах и даже с теми, которые еще никто не придумал. Для координации работы всех этих устройств гетерогенной системе всегда есть одно «главное» устройство, который взаимодействует со всеми остальным посредствами OpenCL API. Такое устройство называется «хост», он определяется вне OpenCL. Поэтому OpenCL исходит из наиболее общих предпосылок, дающих представление об устройстве с поддержкой OpenCL: так как это устройство предполагается использовать для вычислений – в нем есть некий «процессор» в общем смысле этого слова, назовём его «клиент». Нечто, что может исполнять команды. Так как OpenCL создан для параллельных вычислений, то такой процессор может, иметь средства параллелизма внутри себя (например, несколько ядер одного CPU, несколько SPE процессоров в Cell). Также элементарным способом наращивания производительности параллельных вычислений является установка нескольких таких процессоров на устройстве (к примеру, многопроцессорные материнские платы PC и т.д.). И естественно в гетерогенной системе может быть несколько таких OpenCL-устройств (вообще говоря, с различной архитектурой). Кроме вычислительных ресурсов устройство имеет какой-то объем памяти. Причем никаких требований к этой памяти не предъявляется, она может быть как на устройстве, так и вообще быть размечена на ОЗУ хоста (как например, это сделано у встроенных видеокарт). Такое широкое понятие об устройстве позволяет не накладывать какихлибо ограничений на программы, разработанные для OpenCL. Эта технология позволит разрабатывать как приложения, сильно оптимизированные под конкретную OpenCL, архитектуру так и те, специфического которые будут устройства, поддерживающего демонстрировать стабильную производительность на всех типах устройств (при условии эквивалентной производительности этих устройств). OpenCL предоставляет программисту низкоуровневый API, через который он взаимодействует с ресурсами устройства. OpenCL API может либо напрямую поддерживаться устройством, либо работать через промежуточный API (как в случае NVidia: OpenCL работает поверх CUDA Driver API, поддерживаемый устройствами), это зависит от конкретной реализации не описывается стандартом. 2.3 Применение технологии OpenCL к рекурсивному фильтру Так как само приложение написано с помощью универсального фреймворка Qt, то, для использования OpenCL, была взята экспериментальная разработка QtOpenCL (апрель 2010), которая берёт на себя все вопросы, связанные с менеджментом ядер OpenCL, что даёт огромное удобство при реализации. Устройство хоста в нашем случае будет вычислять только необходимые константы, настраивать окружение OpenCL и инициировать выполнение программного кода в среде GPU (или CPU). Итак, наш алгоритм состоит из четырёх этапов, каждая пара которых Начало между собой идентична. Сперва, мы для каждой строки изображения применяем рекурсивный фильтр Гаусса, это делает Проходим по строкам для всех трёх цветовых компонент. Не Транспонируем трудно заметить, что будет присутствовать два цикла, поэтому мы можем выделить часть кода клиента, который Проходим по столбцам будет выполняться на одном из нескольких ядер GPU. Затем транспонируем Транспонируем наше изображение и применяем код клиента, Конец выделенного раньше (но в данном случае это будет код для столбцов, а не строк). И затем обратно транспонируем изображение. Код клиента будет содержать все необходимые вычисления для расчёта текущей точки. Так как параллельно будет запущено несколько ядер (N штук), то общие вычисления будут в N раз быстрее. Это позволяет добиться огромного прироста производительности. 3. Сравнительный анализ, условия тестирования, результаты На практике я использовал release-сборку программы. Время измерялось только в период выполнения операций фильтрования. Операции загрузки изображений, преобразований их во внутренний формат, расчёт первоначальных констант – это время не учитывалось. Одна очень важная особенность техники GPGPU состоит в том, что программа хоста, позволяет компилировать код клиента в момент выполнения программы и оптимизировать его в зависимости от системы и оборудования. Это очень важно, так например, не имея подходящего GPU, клиент может выполняться на CPU, но также распараллелено. В моём случае, при тестировании не удалось найти подходящий компьютер с новейшими GPU, но можно утверждать, что код, выполненный на CPU, будет значительно быстрее работать на GPU. Итак, условия тестирования включают в себя: Процессор Intel Core 2 T7200 CPU @ 2.00 Ghz Оперативная память 2.00 Gb RAM Операционаня система Microsoft Windows XP Service Pack 3 Видеокарта ATI Mobility Radeon X1600 Исходное 32ух-битное изображение использовалось следующих размеров (в пикселях): 256х256 512х512 1024х1024 2048х2048 4096х4096 Для разрешения 256х256 имеем следующие тесты (все результаты в миллисекундах): Явный метод: 78, 63, 79 С помощью FFT: 3859, 3965, 3842 Рекурсивный: 47, 47, 32 OpenCL: 15, 15, 15 OpenCL Рекурсивный Фурье Явный 256x256 0 500 1000 1500 2000 2500 3000 3500 256x256 15 OpenCL Рекурсивный 42 Фурье 3888,666667 Явный 73,33333333 Для изображения 512х512: Явный метод: 219, 218, 219 С помощью FFT: 40015, 41134, 40187 Рекурсивный: 172, 172, 157 OpenCL: 47, 32, 31 4000 OpenCL Рекурсивный Фурье Явный 512x512 0 5000 10000 15000 20000 25000 30000 35000 40000 45000 512x512 36,66666667 OpenCL Рекурсивный 167 Фурье 40445,33333 Явный 218,6666667 Для изображения 1024х1024: Явный метод: 750, 765, 766 С помощью FFT: 592422, 587886, 591135 Рекурсивный: 454, 469, 454 OpenCL: 141, 140, 141 OpenCL Рекурсивный Фурье Явный 1024x1024 0 100000 200000 300000 400000 500000 600000 1024x1024 140,6666667 OpenCL Рекурсивный 459 Фурье 590481 Явный 760,3333333 Как видим, фильтрация с помощью преобразования Фурье увеличивает время выполнения программы более чем в 10 раз, при увеличении изображения вдвое. Поэтому в дальнейшем я решил исключить данный тип фильтрации как крайне неэффективный в данной реализации. Для изображения 2048х2048: Явный метод: 2969, 2984, 2985 Рекурсивный: 1782, 1781, 1781 OpenCL: 703, 703, 703 2048x2048 OpenCL 2048x2048 Рекурсивный Явный 0 2048x2048 500 1000 1500 2000 2500 3000 Явный Рекурсивный OpenCL 2979,333333 1781,333333 703 Для изображения 4096х4096: Явный метод: 11922, 11937, 11954 Рекурсивный: 6174, 6159, 6160 OpenCL: 3109, 3094, 3172 4096x4096 OpenCL 4096x4096 Рекурсивный Явный 0 4096x4096 2000 4000 6000 8000 10000 12000 Явный Рекурсивный OpenCL 11937,66667 6164,333333 3125 Так же покажем совместную диаграмму для всех типов изображения: 4096x4096 2048x2048 OpenCL Рекурсивный Фурье 1024x1024 Явный 512x512 256x256 0 100000 200000 300000 400000 500000 2048x2048 600000 256x256 512x512 1024x1024 4096x4096 OpenCL 15 36,66666667 140,6666667 703 3125 Рекурсивный 42 167 459 1781,333333 6164,333333 Фурье 3888,666667 40445,33333 590481 0 0 Явный 73,33333333 218,6666667 760,3333333 2979,333333 11937,66667 И для наглядности, без преобразования Фурье: 4096x4096 2048x2048 OpenCL Рекурсивный Явный 1024x1024 512x512 256x256 0 2000 4000 6000 8000 10000 12000 256x256 512x512 1024x1024 2048x2048 OpenCL 15 36,66666667 140,6666667 703 3125 Рекурсивный 42 167 459 1781,333333 6164,333333 73,33333333 218,6666667 760,3333333 2979,333333 11937,66667 Явный 4096x4096 Само исходное изображение и результат работы фильтра: 4. Выводы Какие же можно сделать выводы? Первое, что хочется отметить – это явное отставание с помощью фильтрации с применением преобразования Фурье. Вопреки ожиданиям, этот метод показал себя крайне неэффективно и заочно выбыл из испытаний. Как такое могло случиться? Возможно, это связано с огромным количеством циклических операций сложения в реализации метода. Так же может повлиять постоянная адресация в различные сегменты памяти ОЗУ, где влияет уже латентность(memory latency) самого физического устройства. Возможно, из-за этой задержки сказывается такое поведение. Так же в этой реализации присутствуют множество операций с плавающей точкой. Хотя современные процессоры научились хорошо работать в этом режиме – но в совокупности факторов это могло повлиять. Я так же проводил тесты, задавая указание компилятору, внутренне представлять числа с плавающей точкой в виде фиксированной. Но данный метод не сильно оправдал себя. По результатам исследования я получил не более 6% прироста производительности (~6400 миллисекунд против ~6000). В качестве вывода для этого метода можно предположить следующее: без соответствующей хорошей реализации самого преобразования Фурье мы будем худшую производительность за счёт огромного количества проходов в циклах, где осуществляются операции с плавающей точкой и неструктурные запросы в оперативную память. Явный метод показал себя, как и следовало ожидать, не в первых местах. Однако в данном исследовании не на последнем месте. Здесь сказывается большое количество операций - M*N*D*D, где М – изображение в ширину, N – изображение в высоту, D – размер окна. Как видим, здесь каких либо оптимизаций сделать не удастся, разве что за счёт уменьшения размера окна. Но вопрос, зачем, если это уменьшает эффективность самого фильтра. Намного лучше предыдущих показал себя рекурсивный фильтр Гаусса. Здесь мы видим скорость и эффективность недоступную вышеописанным претендентам. Хотя он и имеет сложность M*N*4, за счёт прохождения по строкам изображения, по столбцам и дважды транспонирования, но показал сея как вполне эффективный фильтр, который уже можно применять в прикладных программах. Что можно улучшить в данном фильтре – так это избавиться от транспонирования, что даст небольшой, но всё же прирост производительности. И наконец, наиболее эффективный алгоритм нашего исследования – рекурсивный фильтр Гаусса с использованием параллелизации на GPU и CPU средствами OpenCL. Данный метод показал себя лучше всех даже на тестовом двуядерном CPU. В некоторых тестах его производительность была более чем в три раза выше ближайшего конкурента. А это очень много. Как можно улучшить данный метод? Со стороны алгоритма, как и в рекурсивном случае, избавиться от транспонирования, а со стороны аппаратного обеспечения – повысить количество рабочих ядер. OpenCL позволяет задействовать все имеющиеся в наличии ресурсы компьютера, как GPU, так и CPU, поэтому, как говорится, чем больше – тем лучше. И данный метод уже может применяться на больших объёмах данных с использованием мощных кластеров. ЗАКЛЮЧЕНИЕ 1. В работе реализованы основные алгоритмы вычисления гауссова фильтра, применяемого для сглаживания растровых изображений: явный метод, метод быстрого преобразования Фурье, рекурсивный метод. Проведен сравнительный анализ этих методов. 2. Рекурсивный метод является наиболее пригодным для эффективного сглаживания растровых изображений, не смотря на то, что он вычисляет результирующее изображение с некоторой погрешностью (0.69%). Такая погрешность допустима в подавляющем большинстве практических задач современной компьютерной графики. 3. Реализована параллелизация рекурсивного алгоритма с использованием библиотеки OpenCL. повышено в 2.5-3 раза. В результате быстродействие этого метода СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ 1. Young I., van Vliet L. Recursive implementation of the Gaussian filter. Signal Processing, vol. 44. – Elsevier, 1995. – C.139-151. 2. Witkin A. Scale-space filtering. – Karlsruhe, Germany, 1983. – C. 10191021. 3. Shapiro L., Stockman G. Computer Vision. — Prentence Hall, 2001. — C. 137-150. 4. Nixon M., Aguado A. Feature Extraction and Image Processing. — Academic Press, 2008. — C. 88. 5. Сергиенко А. Б. Цифровая обработка сигналов. — Спб: Питер, 2006. — С. 751. 6. Papoulis A. Signal Analysis. – McGraw-Hill, 1977. – C. 23. 7. Рудин У. Основы математического анализа. – М., 1976. ПРИЛОЖЕНИЕ А. Руководство пользователя Для корректной работы приложения необходимо установить OpenCL на компьютер. После запуска программы необходимо открыть файл необходимо открыть какой-либо файл с высотой и шириной степени двойки: После в меню выбрать какой либо фильтр и нажать применить: Так же можно посмотреть один из этапов преобразования Фурье выбрав в меню соответствующий пункт: ПРИЛОЖЕНИЕ Б. Листинг программы #include <complex> #include <algorithm> #include <qmath.h> #include <QtDebug> #include <QTime> #include <QImageReader> #include "gauss.h" #include "fourier.h" #include "..\..\qtopencl\qclcontext.h" #define idxc(x,y, stride) ((y)*(stride)*3+(x)*3 + c) #define idx(x,y, stride) ((y)*(stride)+(x)) Gauss::Gauss():width(-1),height(-1),bpp(-1) { static double predefinedMatrix[] = { 0.00000067,0.00002292,0.00019117,0.00038771,0.00019117,0.00002292,0.00000067, 0.00002292,0.00078633,0.00655965,0.01330373,0.00655965,0.00078633,0.00002292, 0.00019117,0.00655965,0.05472157,0.11098164,0.05472157,0.00655965,0.00019117, 0.00038771,0.01330373,0.11098164,0.22508352,0.11098164,0.01330373,0.00038771, 0.00019117,0.00655965,0.05472157,0.11098164,0.05472157,0.00655965,0.00019117, 0.00002292,0.00078633,0.00655965,0.01330373,0.00655965,0.00078633,0.00002292, 0.00000067,0.00002292,0.00019117,0.00038771,0.00019117,0.00002292,0.00000067 }; //we regenerate it by ourselves genereateNativeGaussian(7, 0.84089642); //generateFourierMatrix(5); qDebug()<<"------------------------"; } QVector<double> Gauss::genFou(int dim){ QVector<double> matrix(dim*dim); double sigma = native.sigma; for (int i = 0; i < dim; i++){ for (int j = 0; j < dim; j++){ double p = (-i * i - j * j)/(2.0 * sigma * sigma); double e = exp(p); matrix[ j*dim + i ] = e; //qDebug()<<QString("%1 k=%2 p=%3 e=%4").arg(matrix[idx-1], 12, 'f',8).arg(k, 12, 'f',8).arg(p, 12, 'f',8).arg(e, 12, 'f',8); } } return matrix; } QVector<double> Gauss::genereateNativeGaussian( int dimension, double sigma, bool f ) { qDebug()<<"generating gaussian matrix"; if (dimension == -1) dimension = native.dim; if (sigma == -1) sigma = native.sigma; QVector<double> matrix = QVector<double>(dimension*dimension); int half_dim = dimension/2, idx = 0; double sum = 0; int odd = dimension % 2; for (int i = -half_dim; i <= half_dim - 1 + odd; i++){ for (int j = -half_dim; j <= half_dim - 1 + odd; j++){ double k = 1.0 / sqrt(2.0 * M_PI) / sigma; double p = (-i * i - j * j)/(2.0 * sigma * sigma); double e = exp(p); sum += (matrix[ idx++ ] = (!f?k:1) * e); } } //normalize for (uint i = 0; !f && i < (uint)dimension * dimension; i++) matrix[i] /= sum; native.dim = dimension; native.matrix = matrix; native.sigma = sigma; if (!"show matrix"){ showNative(native.matrix, native.dim); QVector<double> im(dimension*dimension); QVector<double> dRE(dimension*dimension); QVector<double> dIM(dimension*dimension); DFT2D<1>(dimension, dimension,0,native.matrix, im, dRE, dIM); showNative(dRE, native.dim); DFT2D<1>(dimension, dimension,1, dRE, dIM, native.matrix, im); showNative(native.matrix, native.dim); showNative(im, native.dim); } return native.matrix; } bool Gauss::openImage(QString filePath){ loaded = false; QImageReader reader(filePath); orig.image = reader.read(); if (!orig.image.isNull()){ loaded = true; orig.image = orig.image.convertToFormat(QImage::Format_RGB32); width = orig.image.width(); height = orig.image.height(); bpp = orig.image.depth(); initFourier(); } else qDebug()<<reader.errorString(); return loaded; } QPixmap Gauss::originalPixmap(){ return QPixmap::fromImage(orig.image); } QPixmap Gauss::nativeFilter(){ return QPixmap::fromImage(native.image); } int Gauss::applyNativeFilter(){ QRgb *bits = (QRgb*)orig.image.bits(); int qrgbCount = orig.image.byteCount() / sizeof(QRgb); //TODO free QRgb *gauss = new QRgb[qrgbCount]; QTime t; t.start(); for (int x = 0; x < width; x++){ for (int y = 0; y < height; y++){ int pixelIndex = y * width + x; double resultRed=0; double resultGreen=0; double resultBlue=0; for (int x0 = x - native.dim / 2, xg=0; x0 < x + native.dim / 2; x0++, xg++){ if (x0 < 0 || x0 >= width) continue; for (int y0 = y - native.dim / 2, yg=0; y0 < y + native.dim / 2; y0++, yg++){ if (y0 < 0 || y0 >= height) continue; double gaussian = native.matrix[ yg * native.dim + xg ]; QRgb in = bits[y0 * width + x0]; resultRed += (double)qRed(in) * gaussian; resultGreen += (double)qGreen(in) * gaussian; resultBlue += (double)qBlue(in) * gaussian; } } gauss[pixelIndex] = qRgb(resultRed, resultGreen, resultBlue); } } int elapsed = t.elapsed(); native.image = QImage((uchar*)gauss, width, height, QImage::Format_RGB32); return elapsed; } int Gauss::applyFourier(int type, bool inv, bool shift) { QTime t; t.start(); if (type==0) DFT2D<3>(fourier.width2, fourier.height2, inv, fourier.fRe, fourier.fIm, fourier.FRe, fourier.FIm); else FFT2D<3>(fourier.width2, fourier.height2, inv, fourier.fRe, fourier.fIm, fourier.FRe, fourier.FIm); int elapsed = t.elapsed(); fix(fourier.width2, fourier.height2, fourier.FRe, fourier.gauss, shift, false); std::swap(fourier.fRe,fourier.FRe); std::swap(fourier.fIm,fourier.FIm); fourier.image = QImage((uchar*)fourier.gauss, fourier.width2, fourier.height2, QImage::Format_RGB32); return elapsed; } int Gauss::applyFourierFilter() { QTime t; t.start(); QVector<double> mIm; QVector<double> mRE; QVector<double> mIM; if ("fft gaussian kernel"){ genereateNativeGaussian(fourier.width2, native.sigma, true); int d = native.dim * native.dim; mIm = QVector<double>(d); mRE = QVector<double>(d); mIM = QVector<double>(d); DFT2D<1>(native.dim, native.dim, 0, native.matrix, mIm, mRE, mIM); } FFT2D<3>(fourier.width2, fourier.height2, 0, fourier.fRe, fourier.fIm, fourier.FRe, fourier.FIm); std::swap(fourier.fRe,fourier.FRe); std::swap(fourier.fIm,fourier.FIm); if (!"do small kernel"){ QVector<double> matrix = native.matrix; int dim = native.dim; int cent = dim / 2; for (int i = 0; i < dim/2 ; i++){ for (int j = 0; j < dim/2 ; j++){ for(int c = 0; c < 3; c++){ if ("fft g kernel"){ if ("do real"){ fourier.fRe[j*dim*3 + i*3 + c] *= mRE[(j )*dim + i]; fourier.fRe[j*dim*3 + (dim - i - 1)*3 + c] *= mRE[(j )*dim + (dim - i - 1)]; fourier.fRe[(dim - j - 1)*dim*3 + i*3 + c] *= mRE[(dim - j - 1)*dim + i]; fourier.fRe[(dim - j - 1)*dim*3 + (dim - i - 1)*3 + c] *= mRE[(dim - j -1 )*dim + (dim - i - 1)]; } + + - + - if ("do imag"){ fourier.fIm[j*dim*3 + i*3 + c] *= mIM[(j )*dim + i ]; fourier.fIm[j*dim*3 + (dim - i - 1)*3 c] *= mIM[(j )*dim + (dim - i - 1)]; fourier.fIm[(dim - j - 1)*dim*3 + i*3 c] *= mIM[(dim - j - 1)*dim + i]; fourier.fIm[(dim - j - 1)*dim*3 + (dim i - 1)*3 + c] *= mIM[(dim - j - 1)*dim + (dim - i - 1)]; } } else { if (!"do real"){ fourier.fRe[j*dim*3 + i*3 + c] *= matrix[(j + cent)*dim + i + cent]; fourier.fRe[j*dim*3 + (dim - i)*3 + c] *= matrix[(j + cent)*dim + (dim - i - cent)]; fourier.fRe[(dim - j - 1)*dim*3 + i*3 c] *= matrix[(dim - j - cent)*dim + i + cent]; fourier.fRe[(dim - j - 1)*dim*3 + (dim i)*3 + c] *= matrix[(dim - j - cent)*dim + (dim - i - cent)]; } if (!"do imag"){ fourier.fIm[j*dim*3 + i*3 + c] *= matrix[(j + cent)*dim + i + cent]; fourier.fIm[j*dim*3 + (dim - i)*3 + c] *= matrix[(j + cent)*dim + (dim - i - cent)]; fourier.fIm[(dim - j - 1)*dim*3 + i*3 + c] *= matrix[(dim - j - cent)*dim + i + cent]; fourier.fIm[(dim - j - 1)*dim*3 + (dim - i)*3 + c] *= matrix[(dim - j - cent)*dim + (dim - i - cent)]; } } } } } } else{ //genereateNativeGaussian(fourier.width2, native.sigma); int half = fourier.width2/2; QVector<double> matrix = genFou(fourier.width2); for (int i = 0; i < fourier.height2 ; i++){ for (int j = 0; j < fourier.width2 ; j++){ for(int c = 0; c <3; c++){ fourier.fRe[ idxc(j,i,fourier.width2)] *= matrix[idx(j,i,fourier.width2)]; //fourier.fIm[ idxc(j,i,fourier.width2)] *= matrix[idx(j,i,fourier.width2)]; } } } } FFT2D<3>(fourier.width2, fourier.height2, 1, fourier.fRe, fourier.fIm, fourier.FRe, fourier.FIm); int elapsed = t.elapsed(); fix(fourier.width2, fourier.height2, fourier.FRe ,fourier.gauss, false, false); std::swap(fourier.fRe,fourier.FRe); std::swap(fourier.fIm,fourier.FIm); fourier.image = QImage((uchar*)fourier.gauss,fourier.width2,fourier.height2,QImage::Format_RGB32 ); return elapsed; } complex_t* Gauss::generateFourierMatrix(int n ) { Q_UNUSED(n); return 0; } QPixmap Gauss::fourierFilter() { return QPixmap::fromImage(fourier.image); } void Gauss::initFourier() { int width2 = fourier.width2 = (int)pow(2,ceil(log(double(width))/log(2.0))); int height2 = fourier.height2 = (int)pow(2,ceil(log(double(height))/log(2.0))); const QRgb* bits = (const QRgb*)orig.image.bits(); QRgb* gauss = fourier.gauss = new QRgb[width2*height2 ]; size_t length = width2*height2*3; QVector<double> &fRe = fourier.fRe = QVector<double>(length); if (width2<=2500){ fourier.fIm = QVector<double>(length); fourier.FRe = QVector<double>(length); fourier.FIm = QVector<double>(length); } size_t tt = width2*height2; std::fill(gauss, gauss + tt, 0); //set signal to the image for(int x = 0; x < width2; x++) for(int y = 0; y < height2; y++) { if (x>=width) continue; if (y>=height) continue; QRgb in = bits[y*width + x]; fRe[3*y*width2 + 3*x + 0] = qRed(in); fRe[3*y*width2 + 3*x + 1] = qGreen(in); fRe[3*y*width2 + 3*x + 2] = qBlue(in); } } int Gauss::applyRecursive() { Q_ASSERT(native.sigma >= 0.5); if (0 <= native.sigma && native.sigma <= 2.5) recursive.q = 3.97156 - 4.14554 * sqrt(1.0 - 0.26891*native.sigma ); else if (native.sigma > 2.5) recursive.q = -0.9633 + 0.98711 * native.sigma ; double q = recursive.q; double b0 = recursive.b0 = 1.57825 + (2.44413 * q) + (1.4281 * q * q); double b1 = recursive.b1 = (2.44413 * q) + (2.85619 * q * q) + (1.26661 * q * q * q); double b2 = recursive.b2 = -((1.4281 * q * q) + (1.26661 * q * q * q)); double b3 = recursive.b3 = 0.422205 * q * q * q; double B = recursive.B = 1.0 - ((b1 + b2 + b3)/b0); QVector<double> in = fourier.fRe; QTime t; t.start(); recursiveStep(in.data(), B, b1, b2, b3, b0);//fixme transpose(in.data()); recursiveStep(in.data(), B, b1, b2, b3, b0); transpose(in.data()); int elapsed = t.elapsed(); fix(fourier.width2, fourier.height2, in ,fourier.gauss, false, false); recursive.image = QImage((uchar*)fourier.gauss,width,height,QImage::Format_RGB32); return elapsed; } QPixmap Gauss::recursiveFilter() { return QPixmap::fromImage(recursive.image); } void Gauss::transpose( double * in ) { Q_ASSERT_X(width == height,"Recursive filter","Image is not square!"); for (int y = 0; y < height; y++){ for (int x = y; x < width; x++){ std::swap(in[x*width*3 + y*3 + 0], in[y*width*3 + x*3 + 0]); std::swap(in[x*width*3 + y*3 + 1], in[y*width*3 + x*3 + 1]); std::swap(in[x*width*3 + y*3 + 2], in[y*width*3 + x*3 + 2]); } } std::swap(width,height); } double * Gauss::recursiveStep( double * ind, double B, double b1, double b2, double b3, double b0 ) { for (int c = 0; c < 3; c++){ for (int y = 0; y < height; y++){ double *in = ind + y * width * 3; double V = in[0 + c]; in[0*3 + c] = B * V + (b1 * V + b2 * V + b3*V) / b0; in[1*3 + c] = B * in[1*3 + c] + (b1 * in[0*3 + c] + b2 * V + b3 * V) / b0 ; in[2*3 + c] = B * in[2*3 + c] + (b1 * in[1*3 + c] + b2 * in[0*3 + c] + b3 * V) / b0 ; for (int i = 3; i < width ; ++i) in [i*3 + c] = B * in[i*3 + c] + (b1 * in[i*3 - 1*3 c] + b2 * in[i*3 - 2*3 + c] + b3 * in[i*3 - 3*3 + c]) / b0 ; + int r = width - 1; V = in[r*3 + c]; in [r*3 - 0*3 + c] = B * V + (b1 * V + b2 * V + b3 * V ) / b0; in [r*3 - 1*3 + c] = B * in[r*3 - 1*3 + c] + (b1 * in[r*3 + c] + b2 * V + b3 * V ) / b0; in [r*3 - 2*3 + c] = B * in[r*3 - 2*3 + c] + (b1 * in[r*3 1*3 + c] + b2 * in[r*3 + c] + b3 * V ) / b0; for (int i = r - 3; i >= 0; --i) in[i*3 + c] = B * in[i*3 + c] + (b1 * in[i*3 + 1*3 + c] + b2 * in[i*3 + 2*3 + c] + b3 * in[i*3 + 3*3 + c]) / b0; } } return 0; } int Gauss::applyRecursiveCL() { int width2 = fourier.width2, height2 = fourier.height2; QCLContext context; if (!context.create()){ qDebug()<<QCLContext::errorName(context.lastError()); return 0; } qDebug()<< context.defaultDevice().vendor()<< context.defaultDevice().version(); QCLProgram program = context.buildProgramFromSourceFile(":/kernel"); if (program.isNull()){ qDebug()<<program.log()<<QCLContext::errorName(context.lastError()); return 0; } QCLKernel recursiveKernel = program.createKernel("recursive_kernel"); if (recursiveKernel.isNull()){ qDebug()<<QCLContext::errorName(context.lastError()); return 0; } QCLKernel transposeKernel = program.createKernel("transpose"); if (transposeKernel.isNull()){ qDebug()<<QCLContext::errorName(context.lastError()); return 0; } //transposeKernel.setGlobalWorkSize(width2, height2); //transposeKernel.setLocalWorkSize(8, 8); recursiveKernel.setGlobalWorkSize(height2, 3); Q_ASSERT(native.sigma >= 0.5); if (0 <= native.sigma && native.sigma <= 2.5) recursive.q = 3.97156 - 4.14554 * sqrt(1.0 - 0.26891*native.sigma ); else if (native.sigma > 2.5) recursive.q = -0.9633 + 0.98711 * native.sigma ; double q = recursive.q; float b0 = recursive.b0 = 1.57825 + (2.44413 * q) + (1.4281 * q * q); float b1 = recursive.b1 = (2.44413 * q) + (2.85619 * q * q) + (1.26661 * q * q * q); float b2 = recursive.b2 = -((1.4281 * q * q) + (1.26661 * q * q * q)); float b3 = recursive.b3 = 0.422205 * q * q * q; float B = recursive.B = 1.0 - ((b1 + b2 + b3)/b0); QCLVector<float> vec = context.createVector<float>(width2*height2*3); for (int i = 0; i < width2 * height2 * 3; i++) vec[i] = fourier.fRe[i]; QTime t; t.start(); vec.map(); recursiveKernel(vec, B, b1, b2, b3, b0, width2, height2); transposeKernel(vec, width2, height2); recursiveKernel(vec, B, b1, b2, b3, b0, width2, height2); QCLEvent clevent = transposeKernel(vec); clevent.waitForFinished(); vec.unmap(); int elapsed = t.elapsed(); QVector<float> in = QVector<float>(width2*height2*3); vec.read(in.data(),width2*height2*3); fix(fourier.width2, fourier.height2, in ,fourier.gauss, false, false); recursive.image = QImage((uchar*)fourier.gauss,width2,height2,QImage::Format_RGB32); return elapsed; } void Gauss::showNative(QVector<double> mx, int dim) { QString s = "--------------------\n"; for (int i = 0; i < dim; i++){ for (int j = 0; j < dim; j++) s.append(QString("%1 ").arg(mx[dim * i + j], 12, 'f',8)); s += "\n"; } qDebug()<<s; } #ifndef __FFT_H__ #define __FFT_H__ #include <qmath.h> #include <QRgb> #ifdef DEBUG #include <QtDebug> #endif #include "utils.h" int prepare(int n, int m); template<typename int D, typename C1, typename C2, typename C3, typename C4> void FFT2D(int n, int m, bool inverse, C1& gRe, C2& gIm, C3& GRe, C4& GIm) { int l2n = 0, p = 1; //l2n will become log_2(n) while(p < n) {p *= 2; l2n++;} int l2m = 0; p = 1; //l2m will become log_2(m) while(p < m) {p *= 2; l2m++;} m= 1<<l2m; n= 1<<l2n; //Make sure m and n will be powers of 2, otherwise you'll get in an infinite loop //Erase all history of this array for(int x = 0; x <m; x++){ //for each column for(int y = 0; y < m; y++){ //for each for(int c = 0; c < D; c++) //for { GRe[D * m * x + D * y + c] GIm[D * m * x + D * y + c] } } } row each color component = gRe[D * m * x + D * y + c]; = gIm[D * m * x + D * y + c]; //Bit reversal of each row int j; for(int y = 0; y < m; y++){ //for each row for(int c = 0; c < D; c++) //for each color component { j = 0; for(int i = 0; i < n - 1; i++) { GRe[D * m * i + D * y + c] = gRe[D * m * j + D * y + c]; GIm[D * m * i + D * y + c] = gIm[D * m * j + D * y + c]; int k = n / 2; while (k <= j) {j -= k; k/= 2;} j += k; } } } //Bit reversal of each column double tx = 0, ty = 0; for(int x = 0; x < n; x++){ //for each column for(int c = 0; c < D; c++) //for each color component { j = 0; for(int i = 0; i < m - 1; i++) { if(i < j) { tx = GRe[D * m * x + D * i + c]; ty = GIm[D * m * x + D * i + c]; GRe[D * m * x + D * i + c] = GRe[D * m * x + D * j + c]; GIm[D * m * x + D * i + c] = GIm[D * m * x + D * j + c]; GRe[D * m * x + D * j + c] = tx; GIm[D * m * x + D * j + c] = ty; } int k = m / 2; while (k <= j) {j -= k; k/= 2;} j += k; } } } //Calculate the FFT of the columns for(int x = 0; x < n; x++){ //for each column + + + for(int c = 0; c < D; c++) //for each color component { //This is the 1D FFT: double ca = -1.0; double sa = 0.0; int l1 = 1, l2 = 1; for(int l=0;l<l2n;l++) { l1 = l2; l2 *= 2; double u1 = 1.0; double u2 = 0.0; for(int j = 0; j < l1; j++) { for(int i = j; i < n; i += l2) { int i1 = i + l1; double t1 = u1 * GRe[D * m * x + D * i1 u2 * GIm[D * m * x + D * i1 + c]; double t2 = u1 * GIm[D * m * x + D * i1 u2 * GRe[D * m * x + D * i1 + c]; GRe[D * m * x + D * i1 + c] = GRe[D * m D * i + c] - t1; GIm[D * m * x + D * i1 + c] = GIm[D * m D * i + c] - t2; GRe[D * m * x + D * i + c] += t1; GIm[D * m * x + D * i + c] += t2; } double z = u1 * ca - u2 * sa; u2 = u1 * sa + u2 * ca; u1 = z; } sa = sqrt((1.0 - ca) / 2.0); if(!inverse) sa = -sa; ca = sqrt((1.0 + ca) / 2.0); } } } //Calculate the FFT of the rows for(int y = 0; y < m; y++) {//for each row for(int c = 0; c < D; c++) //for each color component { //This is the 1D FFT: double ca = -1.0; double sa = 0.0; int l1= 1, l2 = 1; for(int l = 0; l < l2m; l++) { l1 = l2; l2 *= 2; double u1 = 1.0; double u2 = 0.0; for(int j = 0; j < l1; j++) { for(int i = j; i < n; i += l2) { int i1 = i + l1; + c] + c] * x * x double t1 = u1 * GRe[D * m * i1 + D * y + c] - u2 * GIm[D * m * i1 + D * y + c]; double t2 = u1 * GIm[D * m * i1 + D * y + c] + u2 * GRe[D * m * i1 + D * y + c]; GRe[D * m * i1 + D * y + c] = GRe[D * m * i + D * y + c] - t1; GIm[D * m * i1 + D * y + c] = GIm[D * m * i + D * y + c] - t2; GRe[D * m * i + D * y + c] += t1; GIm[D * m * i + D * y + c] += t2; } double z = u1 * ca - u2 * sa; u2 = u1 * sa + u2 * ca; u1 = z; } sa = sqrt((1.0 - ca) / 2.0); if(!inverse) sa = -sa; ca = sqrt((1.0 + ca) / 2.0); } } } int d; if(inverse) d = n; else d = m; for(int x = 0; x < n; x++) for(int y = 0; y < m; y++) for(int c = 0; c < D; c++) //for every value of the buffers { GRe[D * m * x + D * y + c] /= d; GIm[D * m * x + D * y + c] /= d; } } /********************************************/ double get_cos(double a); double get_sin(double a); template<typename int D, typename C1, typename C2, typename C3, typename C4> void DFT2D(int n, int m, bool inverse, C1& gRe, C2& gIm, C3& GRe, C4& GIm){ std::vector<double> Gr2(m * n * D); std::vector<double> Gi2(m * n * D); //temporary buffers //calculate the fourier transform of the columns for(int x = 0; x < n; x++){ for(int c = 0; c < D; c++) { //This is the 1D DFT: for(int w = 0; w < m; w++) { Gr2[m * D * x + D * w + c] =Gi2[m * D * x + D * w + c] = 0; for(int y = 0; y < m; y++) { double a= 2 * M_PI * w * y / double(m); if(!inverse)a = -a; double ca = get_cos(a); double sa = get_sin(a); Gr2[m * D * x + D * w + c] += gRe[m * D * x + D * y + c] * ca - gIm[m * D * x + D * y + c] * sa; Gi2[m * D * x + D * w + c] += gRe[m * D * x + D * y + c] * sa + gIm[m * D * x + D * y + c] * ca; } } } } //calculate the fourier transform of the rows for(int y = 0; y < m; y++){ for(int c = 0; c < D; c++) { //This is the 1D DFT: for(int w = 0; w < n; w++) { GRe[m * D * w + D * y + c] = GIm[m * D * w + D * y + c] = 0; for(int x = 0; x < n; x++) { double a = 2 * M_PI * w if(!inverse)a = -a; double ca = get_cos(a); double sa = get_sin(a); GRe[m * D * w + D * y + y + c] * ca - Gi2[m * D * x + D * y + c] * sa; GIm[m * D * w + D * y + y + c] * sa + Gi2[m * D * x + D * y + c] * ca; } if(inverse) { GRe[m * D * w + D * y + GIm[m * D * w + D * y + } else{ GRe[m * D * w + D * y + GIm[m * D * w + D * y + } } } } } * x / double(n); c] += Gr2[m * D * x + D * c] += Gr2[m * D * x + D * c] /= n; c] /= n; c] /= m; c] /= m; struct ColorRGB{ ColorRGB():r(0),g(0),b(0){}; int r,g,b; }; template<typename T> void fix(int n, int m, T g, QRgb * G, bool shift, bool neg128){ Q_UNUSED(neg128); for(int x = 0; x < n; x++) for(int y = 0; y < m; y++) { int x2 = x, y2 = y; if(shift) {x2 = (x + n / 2) % n; y2 = (y + m / 2) % m;} //Shift corners to center ColorRGB c; //calculate c values out of the floating point buffer c.r = int(g[3 * m * x2 + 3 * y2 + 0]); c.g = int(g[3 * m * x2 + 3 * y2 + 1]); c.b = int(g[3 * m * x2 + 3 * y2 + 2]); //negative colors give confusing effects so set them to 0 if(c.r < 0) c.r = 0; if(c.g < 0) c.g = 0; if(c.b < 0) c.b = 0; //set c components higher than 255 to 255 if(c.r > 255) c.r = 255; if(c.g > 255) c.g = 255; if(c.b > 255) c.b = 255; //plot the pixel G[(x)*m+y] = qRgb(c.r,c.g,c.b); } } #endif