Фильтрация изображения на FPGA



    Данная статья является продолжением моей предыдущей статьи о детектировании движения на ПЛИС. В ней я хочу рассмотреть реализацию трёх алгоритмов фильтрации изображения, один из которых является наиболее важным при разработке детектора движения.

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

    В данном проекте я столкнулся с шумами, которые вносит камера и алгоритмы бинаризации изображения (Background subtraction и Frame difference). Эти шумы проявляют себя в виде отдельных точек или их скопления как локально, так и по всему кадру. В зависимости от применяемого алгоритма обнаружения, они могут быть проигнорированы или приняты за объект.

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

    Медианный фильтр


    Медианный фильтр, на мой взгляд, является наиболее значимым для подавления помех, с которыми мне пришлось столкнуться при разработке этого проекта. Он, как раз, подходит для устранения всякого рода мелких рассредоточенных вкраплений, однако он не является идеальным инструментом т.к. пропускает чуть более крупные области сосредоточенных помех.

    Медианный фильтр представляет собой скользящие окно, в нашем случае, размерностью 3x3 пикселя. На вход он принимает 9 значений (пикселей), а на выход выдаёт одно. Работает медианный фильтр так: сортирует входные данные (пиксели) в порядке возрастания и выдаёт серединный результат (медиану).

    Данный алгоритм довольно просто реализуется на языке Си, но для ПЛИС его реализация несколько отличается. Функциональная классическая схема фильтра показана на рисунке ниже. Она состоит из 19-ти базовых элементов.

    Каждый базовый элемент (узел) представляет собой компаратор и мультиплексор.

    На языке Verilog это выглядит так:

    Median filter node
    module median_node
    #(
        parameter DATA_WIDTH = 8,
        parameter LOW_MUX = 1, // disable low output
        parameter HI_MUX = 1 // disable high output
    )(
        input wire [DATA_WIDTH-1:0] data_a,
        input wire [DATA_WIDTH-1:0] data_b,
    
        output reg [DATA_WIDTH-1:0] data_hi,
        output reg [DATA_WIDTH-1:0] data_lo
    );
    
    wire sel0;
        alt_compare cmp(
            .dataa(data_a),
            .datab(data_b),
            .agb(sel0),
            .alb()
        );
    
    
        always @(*)
        begin : mux_lo_hi
            case (sel0)
                1'b0 :
                begin
                    if(LOW_MUX == 1)
                        data_lo = data_a;
                    if(HI_MUX == 1)
                        data_hi = data_b;
                end
                1'b1 :
                begin
                    if(LOW_MUX == 1)
                        data_lo = data_b;
                    if(HI_MUX == 1)
                        data_hi = data_a;
                end
                default :
                begin
                    data_lo = {DATA_WIDTH{1'b0}};
                    data_hi = {DATA_WIDTH{1'b0}};
                end
            endcase
        end
    
    endmodule



    В качестве компаратора используется мегафункция alt_compare. Далее все узлы соединяются согласно схеме. Симуляция работы фильтра в ModelSim выглядит так:


    Красный прямоугольник — входные данные, жёлтый — выход фильтра. Выходной сигнал задержан на 1 такт т.к. Фильтр имеет синхронный регистровый выход.

    Итак, с медианным фильтром всё понятно, осталось разобраться с окном 3x3.

    Скользящее окно 3x3


    Вот так оно выглядет в действии. Я себе это представляю не как окно скользит по картинке, а как картинка проходит сквозь статичное окно ), но сути это не меняет.


    В ПЛИС окно делается не сложно, но требует 2 элемента FIFO размером в одну строку, в нашем случае 320 элементов. Выглядит это так:


    Нижний Line buffer — это строка 1, верхний Line buffer — строка 2, а данные строки 3 берутся прямо из потока когда оба FIFO заполнены по 320, в нашем случае, элементов.
    На языке Verilog это сделано так:

    Line buffer
    wire [7:0] line3_data = data_in;
        
    wire line2_empty;
    wire line2_wr_rq = (data_in_en && !line2_data_ready);
    wire line2_data_valid = !line2_empty;
    wire line2_data_ready;
    wire [7:0] line2_data;
    wire [7:0] median_out_t, sobel_out_t, gaus_out_t;
    reg [7:0] filter_out_r = 0;
    
    // row 3 FIFO
        alt_fifo_512x8 LINE2_FIFO (
            .aclr(),
            .clock(clk),
            .data(line3_data),
            .rdreq(line1_wr_rq),
            .wrreq(line2_wr_rq),
            .almost_full(line2_data_ready),
            .empty(line2_empty),
            .full(),
            .q(line2_data),
            .usedw()
        );
              
    wire line1_wr_rq = (line2_data_valid && !line1_data_ready);
    wire line1_data_ready;
    wire [7:0] line1_data;
    
        // row 2 FIFO
        alt_fifo_512x8 LINE1_FIFO (
            .aclr(),
            .clock(clk),
            .data(line2_data),
            .rdreq(line1_rd_rq),
            .wrreq(line1_wr_rq),
            .almost_full(line1_data_ready),
            .empty(),
            .full(),
            .q(line1_data),
            .usedw()
        );
        
        // median filter top
        median_top
        #(
            .DATA_WIDTH(8)
        ) median_top (
            .clk(clk),
            .a0(a0),
            .b0(b0),
            .c0(c0),
            .a1(a1),
            .b1(b1),
            .c1(c1),
            .a2(a2),
            .b2(b2),
            .c2(c2),
            .median(median_out_t)
        );


    Детектор Собеля


    Детектор собеля представляет собой оператор, вычисляющий приблизительный градиент яркости. Как и медианный фильтр, детектор Собеля — это оконная, в нашем случае 3x3, функция с 9-ю входами и одним выходом. В классическом исполнении выходом данной функции является квадратный корень из суммы квадратов градиентов по осям X и Y. Результат работы детектора выглядит как белые контуры контрастных объектов на черном фоне.

    Матрица коэффициентов фильтра:


    Градиент вычисляется методом свёртки значений пикселей с коэффициентами матрицы фильтра по формуле:


    Как и в случае с медианным фильтром, нам нужно использовать формирование окна 3x3 пикселя для работы с этим фильтром:


    Shift register 1, 2 и 3 на функциональной схеме это запайплайненные входные данные из FIFO, на Verilog выглядит так:

    reg [7:0] a0,b0,c0,a1,b1,c1,a2,b2,c2;
    
    always @(posedge clk or negedge nRst)
        if (!nRst) begin
            a0 <= 8'd0; b0 <= 8'd0; c0 <= 8'd0;
            a1 <= 8'd0; b1 <= 8'd0; c1 <= 8'd0;
            a2 <= 8'd0; b2 <= 8'd0; c2 <= 8'd0;
        end else begin
            a0 <= line1_data;
            b0 <= line2_data;
            c0 <= line3_data;
            //pipeline step 1
            a1 <= a0;
            b1 <= b0;
            c1 <= c0;
            //pipeline step 2
            a2 <= a1;
            b2 <= b1;
            c2 <= c1;
        end

    Код самого детектора очень прост:

    Edge detector
    
    module sobel_detector (clk,z0,z1,z2,z3,z4,z5,z6,z7,z8,edge_out);
       input clk;
       input [7:0] z0,z1,z2,z3,z4,z5,z6,z7,z8;
       output [7:0] edge_out;
     
       reg signed [10:0] Gx;  
       reg signed [10:0] Gy;  
       reg signed [10:0] abs_Gx;
       reg signed [10:0] abs_Gy;
       reg [10:0]          sum;
       
    always @ (posedge clk) begin
        //original
        //Gx<=((z2-z0)+((z5-z3)<<1)+(z8-z6)); //masking in x direction
        //Gy<=((z0-z6)+((z1-z7)<<1)+(z2-z8)); //masking in y direction
        // modified
        Gx <= (z4-z3);
        Gy <= (z4-z1);
        
        abs_Gx <= (Gx[10]?~Gx+1'b1:Gx);//if negative, then invert and add  to make pos.
        abs_Gy <= (Gy[10]?~Gy+1'b1:Gy);//if negative, then invert and add  to make pos.
        sum <= abs_Gx+abs_Gy;
    end
    //Apply Threshold
    assign edge_out = (sum > 20) ? 8'hff : 8'h00;
    endmodule
    
    sobel_detector sobel (
            .clk(clk),
            .z0(a0),
            .z1(a1),
            .z2(a2),
            .z3(b0),
            .z4(b1),
            .z5(b2),
            .z6(c0),
            .z7(c1),
            .z8(c2),
            .edge_out(sobel_out_t)
        );
    


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

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

    Классическая матрица:


    Модифицированная матрица:


    Фильтр Гаусса


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

    Как и два ранее рассмотренных фильтра, оператор Гаусса тоже является оконной функцией 3x3.


    Схематично его реализация выглядит так:


    Код на языке Verilog:

    Gaussian filter
    
    module gaus_filter
    #(
        parameter DATA_IN_WIDTH = 8
    )(
        input wire    [DATA_IN_WIDTH-1:0]    d00_in,
        input wire    [DATA_IN_WIDTH-1:0]    d01_in,
        input wire    [DATA_IN_WIDTH-1:0]    d02_in,
        input wire    [DATA_IN_WIDTH-1:0]    d10_in,
        input wire    [DATA_IN_WIDTH-1:0]    d11_in,
        input wire    [DATA_IN_WIDTH-1:0]    d12_in,
        input wire    [DATA_IN_WIDTH-1:0]    d20_in,
        input wire    [DATA_IN_WIDTH-1:0]    d21_in,
        input wire    [DATA_IN_WIDTH-1:0]    d22_in,
        output wire   [DATA_IN_WIDTH-1:0]    ftr_out,
    );
    
        wire [10:0] s1 = d00_in+(d01_in<<1)+d02_in+(d10_in<<1);
        wire [10:0] s2 = (d11_in<<2)+(d12_in<<1)+d20_in+(d21_in<<1);
        wire [11:0] s3 = s1+s2+d22_in;
        assign ftr_out = s3>>4;
    endmodule
    gaus_filter
        #(
            .DATA_IN_WIDTH(8)
        ) gaus_filter_inst(
          .d00_in    (a0),
          .d01_in    (a1),
          .d02_in    (a2),
          .d10_in    (b0),
          .d11_in    (b1),
          .d12_in    (b2),
          .d20_in    (c0),
          .d21_in    (c1),
          .d22_in    (c2),
          .ftr_out   (gaus_out_t),
        );
    


    Входные значения фильтров


    На вход медианного фильтра подаётся абсолютная разница кадров grayscale представления для его последующей фильтрации, в то время как на вход детектора Собеля и фильтра Гаусса подаётся само grayscale представление, а не его разница.

    Выводы


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

    Для дальнейшего развития этого проекта нам потребуется только медианный фильтр.

    Демонстрация результата



    На правой половине экрана попеременно отображаются разные режимы работы, обозначенные цветовыми маркерами (квадрат в углу изображения)

    Черный — grayscale представление без фильтрации
    Красный — разница кадров без фильтрации
    Зелёный — разница кадров отфильтрованная медианным фильтром
    Синий — детектор Собеля
    Белый — фильтр Гаусса

    Первый проход на видео — это работа алгоритма Frame difference. Фильтрация медианным фильтром не очень заметна на видео. Второй проход — Background subtraction. Здесь заметна разница между разницей без фильтрации и с фильтрацией — исчезли некоторые отдельные белые точки.

    После фильтра Гаусса изображение сменяется на grayscale и становится заметна разница: с Гауссом изображение менее резкое чем просто grayscale.

    Материалы


    Median filter FPGA implementation
    Sobel filter implementation
    Gaussian filter on FPGA
    Поделиться публикацией
    Похожие публикации
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 12
    • 0
      Возникает вопрос, что происходит с первой и последней строками в данной реализации скользящего окна? Ну и заодно с певым и последним пикселями в строке.
      • 0
        На практике происходет пропуск первой строки и в фифо остаётся последняя после конца кадра. Она может быть выведена в первую строку следующего кадра или в следующую после последней строки предыдущего кадра (смещение изображения на одну строку вниз). Я принял решение не обрабатывать её совсем, чем городить логику по её спасению ). Я просто ресет на фифо подаю по завершении кадра, а в первую строку черный цвет идёт. Меня пока эта реализация устраевает.
        • 0
          FPGA известна возможностью делать многие вычисления параллельными. Можно ли увеличить количество блоков, что бы обрабатывать все строки одновременно? Будут ли при этом какие-то последствия?
          • +1
            Прежде чем что-то увеличивать надо продумать архитектуру системы и требования к ней. Если Вы хотите обрабатывать все строки одновременно и это критично для Вас, то, скорее всего, от использования SDRAM придётся отказаться и заменить её на что-то более быстрое или выбрать ПЛИС с большим объёмом блочной памяти, чтобы весь кадр хранить в ней, а не во внешней памяти. Такие кристаллы наверняка есть, но и цена на них в разы больше. Отсюда и последствия — удорожание изделия. А может, достаточно будет пересмотреть архитектуру и продумать её по-новому.
          • 0
            Вопрос не по теме, но всё же задам. В своей предыдущей статье «Детектирование движения в видеопотоке на FPGA» Вы сказали, что планируете выложить проект на github после доработки. Данный проект просто захватил меня, будет ли он доступен?
            • 0
              Да, будет. Потерпите немного, пожалуйста. Мне надо доработать кое-какие блоки. В сыром виде не люблю работу сдавать. Но это будет не гитхаб, скорее всего, а просто архив с проектом т.к. проект лежит в домашнем SVN, а поддерживать 2 репозитория мне, признаться, лениво.
              • 0
                О, Спасибо огромное! Очень буду ждать, как, думаю, и многие! Именно Ваш проект заставил меня снова стряхнуть пыль со своей платки DE2-115!
                • 0
                  Опубликовал в теме проекта.
            • 0
              Спасибо огромное! Это, по-настоящему, классный проект и для обучения, и для прикручивания собственных идей!
              • 0
                Прикручивайте наздоровье. Идеи — это очень здорово!
              • 0
                Очень здорово, спасибо за интересный проект. Скажите, как продвигаются работы? Все согласно намеченному плану?
                • 0
                  Взял передышку. Надо подумать над blob detection и свёрточным кластером )

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое