Визуальная электроника

Представим себе, что, глядя на какую-нибудь сцену одним глазом, мы подносим к нему стеклянную пластину. Если эта пластина не была идеально прозрачной, то наблюдаемое изображение изменится. В зависимости от стекла, из которого сделана пластина, изменение может быть самым разнообразным. К примеру, если это стекло было цветным, то изображение приобретет соответствующий оттенок, а от мутного стекла - станет размытым.

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

В этой статье мы рассмотрим три типа фильтров: медианный фильтр, детектор Собеля и фильтр Гаусса. Все три фильтра мы будем испытывать вот на этом рисунке:

pic1

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

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

pic2

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

pic3

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

Verilog Code:
  1. wire data_in_en;
  2. wire [7:0] line3_data;
  3.  
  4. assign data_in_en=data_ok;
  5. assign line3_data=pic_rgb;
  6.  
  7.  
  8. wire line2_wr_rq = (data_in_en && !line2_data_ready);
  9. wire line2_rd_rq = (line2_data_valid && !line1_data_ready);
  10. wire line2_data_ready;
  11. wire line2_empty;
  12. wire line2_data_valid = !line2_empty;
  13. wire [7:0] line2_data;
  14.  
  15.  
  16. // row 2 FIFO
  17. fifo_bufer LINE2_FIFO (
  18. .aclr(),
  19. .clock(pclk),
  20. .data(line3_data),
  21. .rdreq(line2_rd_rq),
  22. .wrreq(line2_wr_rq),
  23. .almost_full(line2_data_ready),
  24. .empty(line2_empty),
  25. .full(),
  26. .q(line2_data),
  27. .usedw()
  28. );
  29.  
  30.  
  31. wire line1_wr_rq = (line2_data_valid && !line1_data_ready);
  32. wire line1_rd_rq = (line1_data_valid);
  33. wire line1_data_ready;
  34. wire line1_empty;
  35. wire line1_data_valid = !line1_empty;
  36. wire [7:0] line1_data;
  37.  
  38. // row 1 FIFO
  39. fifo_bufer LINE1_FIFO (
  40. .aclr(),
  41. .clock(pclk),
  42. .data(line2_data),
  43. .rdreq(line1_rd_rq),
  44. .wrreq(line1_wr_rq),
  45. .almost_full(line1_data_ready),
  46. .empty(line1_empty),
  47. .full(),
  48. .q(line1_data),
  49. .usedw()
  50. );

А так выглядит собственно само окно фильтра:

Verilog Code:
  1. reg [7:0] a0,b0,c0,a1,b1,c1,a2,b2,c2;
  2.  
  3. always @(posedge pclk or negedge reset)
  4. if (!reset) begin
  5. a0 <= 8'd0; b0 <= 8'd0; c0 <= 8'd0;
  6. a1 <= 8'd0; b1 <= 8'd0; c1 <= 8'd0;
  7. a2 <= 8'd0; b2 <= 8'd0; c2 <= 8'd0;
  8. end else begin
  9. a0 <= line1_data;
  10. b0 <= line2_data;
  11. c0 <= line3_data;
  12. //pipeline step 1
  13. a1 <= a0;
  14. b1 <= b0;
  15. c1 <= c0;
  16. //pipeline step 2
  17. a2 <= a1;
  18. b2 <= b1;
  19. c2 <= c1;
  20. end

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

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

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

pic4

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

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

pic6

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

Verilog Code:
  1. module median_node
  2. #(
  3. parameter DATA_WIDTH = 8,
  4. parameter LOW_MUX = 1, // disable low output
  5. parameter HI_MUX = 1 // disable high output
  6. )(
  7. input wire [DATA_WIDTH-1:0] data_a,
  8. input wire [DATA_WIDTH-1:0] data_b,
  9.  
  10. output reg [DATA_WIDTH-1:0] data_hi,
  11. output reg [DATA_WIDTH-1:0] data_lo
  12. );
  13.  
  14. wire sel0;
  15. alt_compare cmp(
  16. .dataa(data_a),
  17. .datab(data_b),
  18. .agb(sel0),
  19. .alb()
  20. );
  21.  
  22.  
  23. always @(*)
  24. begin : mux_lo_hi
  25. case (sel0)
  26. 1'b0 :
  27. begin
  28. if(LOW_MUX == 1)
  29. data_lo = data_a;
  30. if(HI_MUX == 1)
  31. data_hi = data_b;
  32. end
  33. 1'b1 :
  34. begin
  35. if(LOW_MUX == 1)
  36. data_lo = data_b;
  37. if(HI_MUX == 1)
  38. data_hi = data_a;
  39. end
  40. default :
  41. begin
  42. data_lo = {DATA_WIDTH{1'b0}};
  43. data_hi = {DATA_WIDTH{1'b0}};
  44. end
  45. endcase
  46. end
  47.  
  48. endmodule

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

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

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

pic7

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

pic8

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

Verilog Code:
  1. reg [7:0] a0,b0,c0,a1,b1,c1,a2,b2,c2;
  2.  
  3. always @(posedge clk or negedge nRst)
  4. if (!nRst) begin
  5. a0 <= 8'd0; b0 <= 8'd0; c0 <= 8'd0;
  6. a1 <= 8'd0; b1 <= 8'd0; c1 <= 8'd0;
  7. a2 <= 8'd0; b2 <= 8'd0; c2 <= 8'd0;
  8. end else begin
  9. a0 <= line1_data;
  10. b0 <= line2_data;
  11. c0 <= line3_data;
  12. //pipeline step 1
  13. a1 <= a0;
  14. b1 <= b0;
  15. c1 <= c0;
  16. //pipeline step 2
  17. a2 <= a1;
  18. b2 <= b1;
  19. c2 <= c1;
  20. end

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

Verilog Code:
  1. module sobel_detector (clk,z0,z1,z2,z3,z4,z5,z6,z7,z8,edge_out);
  2. input clk;
  3. input [7:0] z0,z1,z2,z3,z4,z5,z6,z7,z8;
  4. output [7:0] edge_out;
  5.  
  6. reg signed [10:0] Gx;
  7. reg signed [10:0] Gy;
  8. reg signed [10:0] abs_Gx;
  9. reg signed [10:0] abs_Gy;
  10. reg [10:0] sum;
  11.  
  12. always @ (posedge clk) begin
  13. //original
  14. //Gx<=((z2-z0)+((z5-z3)<<1)+(z8-z6)); //masking in x direction
  15. //Gy<=((z0-z6)+((z1-z7)<<1)+(z2-z8)); //masking in y direction
  16. // modified
  17. Gx <= (z4-z3);
  18. Gy <= (z4-z1);
  19.  
  20. abs_Gx <= (Gx[10]?~Gx+1'b1:Gx);//if negative, then invert and add to make pos.
  21. abs_Gy <= (Gy[10]?~Gy+1'b1:Gy);//if negative, then invert and add to make pos.
  22. sum <= abs_Gx+abs_Gy;
  23. end
  24. //Apply Threshold
  25. assign edge_out = (sum > 20) ? 8'hff : 8'h00;
  26.  
  27. endmodule

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

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

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

pic9

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

pic10

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

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

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

pic11

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

pic12

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

Verilog Code:
  1. module gaus_filter
  2. #(
  3. parameter DATA_IN_WIDTH = 8
  4. )(
  5. input wire [DATA_IN_WIDTH-1:0] d00_in,
  6. input wire [DATA_IN_WIDTH-1:0] d01_in,
  7. input wire [DATA_IN_WIDTH-1:0] d02_in,
  8. input wire [DATA_IN_WIDTH-1:0] d10_in,
  9. input wire [DATA_IN_WIDTH-1:0] d11_in,
  10. input wire [DATA_IN_WIDTH-1:0] d12_in,
  11. input wire [DATA_IN_WIDTH-1:0] d20_in,
  12. input wire [DATA_IN_WIDTH-1:0] d21_in,
  13. input wire [DATA_IN_WIDTH-1:0] d22_in,
  14. output wire [DATA_IN_WIDTH-1:0] ftr_out
  15. );
  16.  
  17. wire [10:0] s1 = d00_in+(d01_in<<1)+d02_in+(d10_in<<1);
  18. wire [10:0] s2 = (d11_in<<2)+(d12_in<<1)+d20_in+(d21_in<<1);
  19. wire [11:0] s3 = s1+s2+d22_in;
  20. assign ftr_out = s3>>4;
  21.  
  22. endmodule

Все фильтры подключаются по следующей схеме:

Verilog Code:
  1. //-------------------------------------------------------------------
  2. median_top
  3. #(
  4. .DATA_WIDTH(8)
  5. ) median_top (
  6. .clk(pclk),
  7. .a0(a0),
  8. .b0(b0),
  9. .c0(c0),
  10. .a1(a1),
  11. .b1(b1),
  12. .c1(c1),
  13. .a2(a2),
  14. .b2(b2),
  15. .c2(c2),
  16. .median(median_out_t)
  17. );
  18.  
  19. //-------------------------------------------------------------------
  20. sobel_detector sobel (
  21. .clk(pclk),
  22. .z0(a0),
  23. .z1(a1),
  24. .z2(a2),
  25. .z3(b0),
  26. .z4(b1),
  27. .z5(b2),
  28. .z6(c0),
  29. .z7(c1),
  30. .z8(c2),
  31. .edge_out(sobel_out_t)
  32. );
  33.  
  34. //-------------------------------------------------------------------
  35. gaus_filter
  36. #(
  37. .DATA_IN_WIDTH(8)
  38. ) gaus_filter_inst(
  39. .d00_in (a0),
  40. .d01_in (a1),
  41. .d02_in (a2),
  42. .d10_in (b0),
  43. .d11_in (b1),
  44. .d12_in (b2),
  45. .d20_in (c0),
  46. .d21_in (c1),
  47. .d22_in (c2),
  48. .ftr_out (gaus_out_t),
  49. );

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

Видео работы фильтров:

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

  • Красный — исходное изображение, канал яркости.
  • Зелёный — медианный фильтр.
  • Синий — детектор Собеля.
  • Белый — фильтр Гаусса.

Автор: Юрий

Источник

 

PS

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

Правильный вариант формирования окна:

Verilog Code:
  1. wire [7:0] line3_data;
  2.  
  3. // row 3 FIFO
  4. fifo_bufer line3_fifo (
  5. .clock(pclk),
  6. .data(pic_rgb),
  7. .rdreq(data_in_en),
  8. .wrreq(data_in_en),
  9. .almost_full(),
  10. .empty(),
  11. .q(line3_data)
  12. );
  13.  
  14. wire line2_wr_rq = (data_in_en);
  15. wire line2_rd_rq = (line2_data_valid && !line1_data_ready);
  16. wire line2_data_ready;
  17. wire line2_empty;
  18. wire line2_data_valid = !line2_empty;
  19. wire [7:0] line2_data;
  20.  
  21.  
  22. // row 2 FIFO
  23. fifo_bufer line2_fifo (
  24. .clock(pclk),
  25. .data(pic_rgb),
  26. .rdreq(line2_rd_rq),
  27. .wrreq(line2_wr_rq),
  28. .almost_full(line2_data_ready),
  29. .empty(line2_empty),
  30. .q(line2_data)
  31. );
  32.  
  33.  
  34. wire line1_wr_rq = (line2_data_valid && !line1_data_ready);
  35. wire line1_data_ready;
  36. wire line1_empty;
  37. wire line1_data_valid = !line1_empty;
  38. wire [7:0] line1_data;
  39.  
  40. // row 1 FIFO
  41. fifo_bufer line1_fifo (
  42. .clock(pclk),
  43. .data(line2_data),
  44. .rdreq(line1_rd_rq ),
  45. .wrreq(line1_wr_rq),
  46. .almost_full(line1_data_ready),
  47. .empty(line1_empty),
  48. .q(line1_data)
  49. );
  50. //-------------------------------------------------------------------
  51. reg [1:0] read_state = 0;
  52.  
  53. // The first line FIFO read request manager
  54. always @(posedge pclk or negedge reset)
  55. if (!reset) read_state <= 2'd0;
  56. else begin
  57. case (read_state)
  58. 2'h0: if (line1_data_ready && line2_data_ready) read_state <= 2'h1;
  59. 2'h1: if (line1_empty) read_state <= 2'h0;
  60. endcase
  61. end
  62.  
  63. reg line1_rd_rq = 0;
  64. always @(*) begin
  65. case(read_state)
  66. 2'h0: line1_rd_rq = 1'b0;
  67. 2'h1: line1_rd_rq = data_in_en;
  68. endcase
  69. end

Результат симуляции, присланный нашим читателем:

pic13

Видео с исправленным окном:

Проект фильтрация изображения для платы VE-EP4CE10E: image_processing

Добавить комментарий