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


10b121a734b2452f8509bfede85edf0b.jpg

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

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

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

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

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


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

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

Данный алгоритм довольно просто реализуется на языке Си, но для ПЛИС его реализация несколько отличается. Функциональная классическая схема фильтра показана на рисунке ниже. Она состоит из 19-ти базовых элементов.
7d8f62f4de18422aa6e2b2dd122164bc.png
Каждый базовый элемент (узел) представляет собой компаратор и мультиплексор.
d1053d99ba7a4c6ba80f420931dfefc4.png
На языке 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 выглядит так:
b5e00257bf0d421dae36cd9c3db98c05.png
Красный прямоугольник — входные данные, жёлтый — выход фильтра. Выходной сигнал задержан на 1 такт т.к. Фильтр имеет синхронный регистровый выход.

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

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


Вот так оно выглядет в действии. Я себе это представляю не как окно скользит по картинке, а как картинка проходит сквозь статичное окно), но сути это не меняет.
ea03991b78b9484eab7d435432410a04.png
В ПЛИС окно делается не сложно, но требует 2 элемента FIFO размером в одну строку, в нашем случае 320 элементов. Выглядит это так:
f55d7681a12748c984c680794e9a7139.png
Нижний 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)
    );

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


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

Матрица коэффициентов фильтра:
44cbc4f590b64ab19afca9e81774dcd5.png
Градиент вычисляется методом свёртки значений пикселей с коэффициентами матрицы фильтра по формуле:
5313309254a14369bc5e710144ebf37c.png

Как и в случае с медианным фильтром, нам нужно использовать формирование окна 3×3 пикселя для работы с этим фильтром:
79ea284e47d649628ff8c9be7529dab7.png
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)
    );


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

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

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


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

Как и два ранее рассмотренных фильтра, оператор Гаусса тоже является оконной функцией 3×3.
abaf5b583831409d8ade63626f999462.png
Схематично его реализация выглядит так:
43eb08060548455988c500f8317155f7.png
Код на языке 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

Комментарии (0)

© Habrahabr.ru