С чем хорошо справляется медианный фильтр
Перейти к содержимому

С чем хорошо справляется медианный фильтр

  • автор:

Фильтрация шума сигнала

Фильтрация шума очень важная вещь, при работе с различными датчиками. Сигнал, получаемый от них всегда приходит с шумами, и важно уметь их грамотно отфильтровать. Качественная фильтрация шума способна уменьшить погрешность и увеличить качество измерения датчика. Этим мы сегодня и займемся.

Примерами будут три графика — синусоида, квадратный сигнал (или дискретный, или цифровой) и треугольный сигнал (или пилообразный).

Код для вывода графиков

import matplotlib.pyplot as plt from math import sin # Generals variables length = 30 resolution = 20 # Creating arrays with graphic sinus_g = [sin(i / resolution) for i in range(length * resolution)] square_g = [(1 if p > 0 else -1) for p in sinus_g] triangle_g = [] t = -1 for _ in range(length * resolution): t = t+0.035 if t < 1 else -1 triangle_g.append(t) # Output of graphs graphics = [sinus_g, square_g, triangle_g] fig, axs = plt.subplots(3, 1) for i in range(len(graphics)): axs[i].plot(graphics[i]) axs[i].set_ylim([-2, 2]), axs[i].set_xlim([0, length*resolution]), axs[i].set_yticklabels([]), axs[i].set_xticklabels([]) plt.show()

синусоида, квадратный сигнал и треугольный сигнал

Бороться мы будем с двумя видами шума: постоянный шум (аддитивный белый гауссовский шум или АБГШ) с относительно стабильной амплитудой и случайные импульсы, вызванные внешними факторами. Амплитудой шума - стандартное отклонение зашумленного сигнала от не зашумленного.

Симулировать это мы будем данным образом.

Функция для добавление шума

def noised(func, k=0.3, prob=0.03): o = [] for p in func: r = (np.random.random()*2-1) * k # Standard noise and random emissions if np.random.random() < prob: c = p + r*7 else: c = p + r o.append(c) return o

Среднее арифметическое

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

Код фильтрация графика средним арифметическим

def arith_mean(f, buffer_size=10): # Creating buffer if not hasattr(arith_mean, "buffer"): arith_mean.buffer = [f] * buffer_size # Move buffer to actually values ( [0, 1, 2, 3] -> [1, 2, 3, 4] ) arith_mean.buffer = arith_mean.buffer[1:] arith_mean.buffer.append(f) # Calculation arithmetic mean mean = 0 for e in arith_mean.buffer: mean += e mean /= len(arith_mean.buffer) return mean

buffer_size = 7buffer_size = 25

Как мы видим, при увеличении буфера квадратный и треугольный сигналы сильно исказились, а синусоида сместилась. Проявляется запаздывание среднего значения. Так что, при использовании данного фильтра стоит аккуратнее подбирать размер буфера.

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

Медианный фильтр предназначен справляться со случайными импульсами. Если среднее арифметическое получая на вход (10, 12, 55), выдаст 25.67, то медиан выдаст 12. На первый взгляд не так просто понять как он устроен, но со своей задачей он справляется отлично. На просторах интернета я нашел лаконичное исполнение. Но оно подойдет только в случаях когда длительность импульса не более одного шага, иначе придется использовать другое исполнение медианы высшего порядка.

middle = (max(a,b) == max(b, c)) ? max(a, c) : max(b, min(a, c)); // c++
middle = max(a, c) if (max(a, b) == max(b, c)) else max(b, min(a, c)) # python

Код медианного фильтра

def median(f): # Creating buffer if not hasattr(median, "buffer"): median.buffer = [f] * 3 # Move buffer to actually values ( [0, 1, 2] -> [1, 2, 3] ) median.buffer = median.buffer[1:] median.buffer.append(f) # Calculation median a = median.buffer[0] b = median.buffer[1] c = median.buffer[2] middle = max(a, c) if (max(a, b) == max(b, c)) else max(b, min(a, c)) return middle

Результат действия медианного фильтра

Медианный фильтр справился почти со всеми импульсами. К тому же этот алгоритм совершенно прост в вычислении. И используя его в комбинации с каким-либо другим другим фильтром можно получить максимальный результат.

Экспоненциальное бегущее среднее и адаптивный коэффициент

Этот фильтр по своей сути схож с первым, а главное он более простой по вычислениям. Работает он так: к предыдущему фильтрованному значению прибавляется новое, и каждое из них помножено на собственный коэффициент, сумма которых равна 1. Коэффициент k подбирается от 0 до 1 и означает важность нового значения по сравнению с предыдущем, то есть чем больше k, тем больше важность нового нефильтрованного значения и фильтрованный график ближе к изначальному.

normalised = new * k + normalised * (1-k) # Эта формула
normalised += (new - normalised) * k # А лучше эта

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

k = s_k if (abs(new - normalised) < d) else max_k

Код фильтра экспоненциального бегущего среднего с адаптивным коэффициент

def easy_mean(f, s_k=0.2, max_k=0.9, d=1.5): # Creating static variable if not hasattr(easy_mean, "fit"): easy_mean.fit = f # Adaptive ratio k = s_k if (abs(f - easy_mean.fit) < d) else max_k # Calculation easy mean easy_mean.fit += (f - easy_mean.fit) * k return easy_mean.fit

Работает достаточно хорошо, но из-за адаптивного коэффициента появляются некие артефакты в моментах с импульсами, так как они достаточно велики, чтобы определиться как составляющие квадратного сигнала. С этим можно бороться, увеличивая параметр d (требуемое расстояние для определения квадратного сигнала), но есть решение элегантнее. И тут нам как раз нам потребуется медианный фильтр. Если перед подачей данных на бегущее среднее, прогнать их через медиану, то можно получить качественный и быстродейственный алгоритм для фильтрации сигнала.

с использованием медианного фильтра

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

Применение связки фильтров

def normalise(func): o = [] for p in func: res = median(p) res = easy_mean(res) o.append(res) return o

Фильтр Калмана

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

Код фильтра Калмана

def kalman(f, q=0.25, r=0.7): if not hasattr(kalman, "Accumulated_Error"): kalman.Accumulated_Error = 1 kalman.kalman_adc_old = 0 if abs(f-kalman.kalman_adc_old)/50 > 0.25: Old_Input = f*0.382 + kalman.kalman_adc_old*0.618 else: Old_Input = kalman.kalman_adc_old Old_Error_All = (kalman.Accumulated_Error**2 + q**2)**0.5 H = Old_Error_All**2/(Old_Error_All**2 + r**2) kalman_adc = Old_Input + H * (f - Old_Input) kalman.Accumulated_Error = ((1 - H)*Old_Error_All**2)**0.5 kalman.kalman_adc_old = kalman_adc return kalman_adc

Со своей задачей фильтр справляется, но он не всегда подойдет из-за множества вычислений с плавающей точкой.

Какой фильтр выбрать?

Это зависит от обстоятельств в которых фильтр будет использоваться. Естественно стоит попробовать каждый, но практически для каждого случая подойдёт вариант №2 из списка ниже. Напомню, что наиболее выгодная связка - медианного фильтра с другим. Пройдемся вкратце по перечисленным фильтрам из данной статьи:

  1. Медианный фильтр - самый быстродейственный алгоритм, применятся для отсеивания случайных импульсов, наиболее эффективен в связках с другими алгоритмами.
  2. Экспоненциальное бегущее среднее с адаптивным коэффициент - универсальный, и простой фильтр, подойдет в большинстве ситуаций
  3. Среднее арифметическое - эффективный, но не всегда столь быстродейственный алгоритм
  4. Фильтр Калмана - универсальный способ фильтрации любого сигнала, но громоздкийпо вычислениям

Кстати, если вы работаете со средой arduino, то там присутствует удобный инструмент визуализации графиков из значений переданных в порт, статья об этом. Это может упростить работу с сигналами.

В данной статье я не стал затрагивать большую часть терминологии ради доступности. И естественно, фильтры предоставленные в этой статье - это лишь малая часть от всех существующих, и если вы не нашли интересующую информацию по данной теме или предоставленные фильтры не дали достаточный результат, то я рекомендую поподробнее разобраться в теме цифровой фильтрации.

И на последок исходники кодов, и еще несколько примеров.

Код для визуализация графиков

import matplotlib.pyplot as plt from math import sin from _f import * # Generals variables length = 30 resolution = 20 # Creating arrays with graphic sinus_g = [sin(i / resolution) for i in range(length * resolution)] square_g = [(1 if p > 0 else -1) for p in sinus_g] triangle_g = [] t = -1 for _ in range(length * resolution): t = t+0.035 if t < 1 else -1 triangle_g.append(t) # Output of graphs graphics = [sinus_g, square_g, triangle_g] fig, axs = plt.subplots(3, 1) for i in range(len(graphics)): noised_f = noised(graphics[i]) axs[i].plot(noised_f, color='blue') axs[i].plot(graphics[i], color='black') axs[i].plot(normalise(noised_f), linewidth=3, color='red') axs[i].set_ylim([-2, 2]), axs[i].set_xlim([0, length*resolution]), axs[i].set_yticklabels([]), axs[i].set_xticklabels([]) plt.show() 

Код с фильтрами

import numpy as np np.random.seed(58008) def normalise(func): o = [] for p in func: res = median(p) res = easy_mean(res) o.append(res) return o def noised(func, k=0.3, fitob=0.03): o = [] for p in func: r = (np.random.random()*2-1) * k # Standard noise and random emissions if np.random.random() < fitob: c = p + r*7 else: c = p + r o.append(c) return o def arith_mean(f, buffer_size=10): # Creating buffer if not hasattr(arith_mean, "buffer"): arith_mean.buffer = [f] * buffer_size # Move buffer to actually values ( [0, 1, 2, 3] ->[1, 2, 3, 4] ) arith_mean.buffer = arith_mean.buffer[1:] arith_mean.buffer.append(f) # Calculation arithmetic mean mean = 0 for e in arith_mean.buffer: mean += e mean /= len(arith_mean.buffer) return mean def easy_mean(f, s_k=0.2, max_k=0.9, d=1.5): # Creating static variable if not hasattr(easy_mean, "fit"): easy_mean.fit = f # Adaptive ratio k = s_k if (abs(f - easy_mean.fit) < d) else max_k # Calculation easy mean easy_mean.fit += (f - easy_mean.fit) * k return easy_mean.fit def median(f): # Creating buffer if not hasattr(median, "buffer"): median.buffer = [f] * 3 # Move buffer to actually values ( [0, 1, 2] ->[1, 2, 3] ) median.buffer = median.buffer[1:] median.buffer.append(f) # Calculation median a = median.buffer[0] b = median.buffer[1] c = median.buffer[2] middle = max(a, c) if (max(a, b) == max(b, c)) else max(b, min(a, c)) return middle def kalman(f, q=0.25, r=0.7): if not hasattr(kalman, "Accumulated_Error"): kalman.Accumulated_Error = 1 kalman.kalman_adc_old = 0 if abs(f-kalman.kalman_adc_old)/50 > 0.25: Old_Input = f*0.382 + kalman.kalman_adc_old*0.618 else: Old_Input = kalman.kalman_adc_old Old_Error_All = (kalman.Accumulated_Error**2 + q**2)**(1/2) H = Old_Error_All**2/(Old_Error_All**2 + r**2) kalman_adc = Old_Input + H * (f - Old_Input) kalman.Accumulated_Error = ((1 - H)*Old_Error_All**2)**(1/2) kalman.kalman_adc_old = kalman_adc return kalman_adc 

фильтрация сильного шума с помощью kalman(p, r=3, q=0.4), arith_mean(res, buffer_size=5)просто линейный шум, нормализованный средним арифметическим с буфером в 20

Ещё интересный эксперимент: я построчно загрузил зашумленную картинку своего кота и пропустил её через фильтр Калмана.

  • Python
  • Программирование
  • Алгоритмы
  • Визуализация данных
  • Разработка под Arduino

Фильтрация изображения на 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_hi = >; 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 

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

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 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.

Материалы

Фильтрация сигналов

Шум можно условно разделить на два типа: постоянный шум датчика с одинаковым отклонением (скриншот 1), и случайный шум, который возникает при различных случайных (чаще всего внешних) обстоятельствах (скриншот 2). Небольшой шум наблюдается у любого аналогового датчика, который опрашивается средствами АЦП Ардуино. Причем сам АЦП практически не шумит, если обеспечить качественное питание платы и отсутствие электромагнитных наводок – сигнал с того же потенциометра будет идеально ровный. Но как только питание становится некачественным, например от дешёвого блока питания, картина меняется. Или, например, без нагрузки блок питания даёт хорошее питание и шума нет, но как только появляется нагрузка – вылезают шумы, связанные с устройством блока питания и отсутствием нормальных выходных фильтров. Или другой вариант – где то рядом с проводом к аналоговому датчику появляется мощный источник электромагнитного излучения (провод с большим переменным током), который наводит в проводах дополнительную ЭДС и мы опять же видим шум. Да, от этих причин можно избавиться аппаратно, добавив фильтры по питанию и экранировав все аналоговые провода, но это не всегда получается и поэтому в этом уроке мы поговорим о программной фильтрации значений.

Измерение значений

Давайте посмотрим, как измеряется сигнал в реальном устройстве. Естественно это происходит не каждую итерацию loop, а например по какому то таймеру. Представим что синий график отражает реальный процесс, а красный – измеренное значение с некоторым периодом. Я думаю очевидно, что между периодами измерения значения измеренное значение не меняется и остаётся постоянным, что видно из графика. Давайте увеличим период и посмотрим, как будут измеряться значения. Из этого можно сделать вывод, что чем быстрее меняется сигнал с датчика, тем чаще его нужно опрашивать. Но вообще всё зависит от целей, который должна выполнять программа и проект в целом. На этом в принципе и строится обработка сигналов.

Фильтры

Цифровые (программные) фильтры позволяют отфильтровать различные шумы. В следующих примерах будут показаны некоторые популярные фильтры. Все примеры оформлены как фильтрующая функция, которой в качестве параметра передаётся новое значение, и функция возвращает фильтрованную величину. Некоторым функциям нужны дополнительные настройки, которые вынесены как переменные. Важно: практически каждый фильтр можно настроить лучше, чем показано на примерах с графиками. На примерах фильтр специально настроен не идеально, чтобы можно было оценить особенность работы алгоритма каждого из фильтров.

Среднее арифметическое

Однократная выборка

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

const int NUM_READ = 30; // количество усреднений для средних арифм. фильтров // обычное среднее арифметическое для float float midArifm() < float sum = 0; // локальная переменная sum for (int i = 0; i < NUM_READ; i++) // согласно количеству усреднений sum += значение; // суммируем значения с любого датчика в переменную sum return (sum / NUM_READ); >// обычное среднее арифметическое для int int midArifm() < long sum = 0; // локальная переменная sum for (int i = 0; i < NUM_READ; i++) // согласно количеству усреднений sum += значение; // суммируем значения с любого датчика в переменную sum return ((float)sum / NUM_READ); >

Особенности использования

  • Отлично усредняет шум любого характера и величины
  • Для целочисленных значений количество измерений есть смысл брать из степеней двойки (2, 4, 8, 16, 32…) тогда компилятор оптимизирует деление в сдвиг, который выполняется в сотню раз быстрее. Это если вы совсем гонитесь за оптимизацией выполнения кода
  • “Сила” фильтра настраивается размером выборки (NUM_READS)
  • Делает несколько измерений за один раз, что может приводить к большим затратам времени!
  • Рекомендуется использовать там, где время одного измерения ничтожно мало, или измерения в принципе делаются редко

Растянутая выборка

Отличается от предыдущего тем, что суммирует несколько измерений, и только после этого выдаёт результат. Между расчётами выдаёт предыдущий результат:

const int NUM_READ = 10; // количество усреднений для средних арифм. фильтров // растянутое среднее арифметическое float midArifm2(float newVal) < static byte counter = 0; // счётчик static float prevResult = 0; // хранит предыдущее готовое значение static float sum = 0; // сумма sum += newVal; // суммируем новое значение counter++; // счётчик++ if (counter == NUM_READ) < // достигли кол-ва измерений prevResult = sum / NUM_READ; // считаем среднее sum = 0; // обнуляем сумму counter = 0; // сброс счётчика >return prevResult; >

Примечание: это просто пример, функция статически хранит предыдущие данные “в себе”. Реализация в реальном коде может отличаться.

Особенности использования

  • Отлично усредняет шум любого характера и величины
  • “Сила” фильтра настраивается размером выборки (NUM_READS)
  • Для целочисленных значений количество измерений есть смысл брать из степеней двойки (2, 4, 8, 16, 32…) тогда компилятор оптимизирует деление в сдвиг, который выполняется в сотню раз быстрее. Это если вы совсем гонитесь за оптимизацией выполнения кода
  • Делает только одно измерение за раз, не блокирует код на длительный период
  • Рекомендуется использовать там, где сам сигнал изменяется медленно, потому что за счёт растянутой по времени выборки сигнал может успеть измениться

Бегущее среднее арифметическое

Данный алгоритм работает по принципу буфера, в котором хранятся несколько последних измерений для усреднения. При каждом вызове фильтра буфер сдвигается, в него добавляется новое значение и убирается самое старое, далее буфер усредняется по среднему арифметическому. Есть два варианта исполнения: понятный и оптимальный:

const int NUM_READ = 10; // количество усреднений для средних арифм. фильтров // бегущее среднее арифметическое float runMiddleArifm(float newVal) < // принимает новое значение static byte idx = 0; // индекс static float valArray[NUM_READ]; // массив valArray[idx] = newVal; // пишем каждый раз в новую ячейку if (++idx >= NUM_READ) idx = 0; // перезаписывая самое старое значение float average = 0; // обнуляем среднее for (int i = 0; i < NUM_READ; i++) < average += valArray[i]; // суммируем >return (float)average / NUM_READ; // возвращаем > // оптимальное бегущее среднее арифметическое float runMiddleArifmOptim(float newVal) < static int t = 0; static float vals[NUM_READ]; static float average = 0; if (++t >= NUM_READ) t = 0; // перемотка t average -= vals[t]; // вычитаем старое average += newVal; // прибавляем новое vals[t] = newVal; // запоминаем в массив return ((float)average / NUM_READ); >

Примечание: это просто пример, функция статически хранит предыдущие данные “в себе”. Реализация в реальном коде может отличаться.

Особенности использования

  • Усредняет последние N измерений, за счёт чего значение запаздывает. Нуждается в тонкой настройке частоты опроса и размера выборки
  • Для целочисленных значений количество измерений есть смысл брать из степеней двойки (2, 4, 8, 16, 32…) тогда компилятор оптимизирует деление в сдвиг, который выполняется в сотню раз быстрее. Это если вы совсем гонитесь за оптимизацией выполнения кода
  • “Сила” фильтра настраивается размером выборки (NUM_READS)
  • Делает только одно измерение за раз, не блокирует код на длительный период
  • Данный фильтр показываю чисто для ознакомления, в реальных проектах лучше использовать бегущее среднее. О нём ниже

Экспоненциальное бегущее среднее

Бегущее среднее (Running Average) – самый простой и эффективный фильтр значений, по эффекту аналогичен предыдущему, но гораздо оптимальнее в плане реализации фильтрованное += (новое - фильтрованное) * коэффициент :

float k = 0.1; // коэффициент фильтрации, 0.0-1.0 // бегущее среднее float expRunningAverage(float newVal)

Примечание: это просто пример, функция статически хранит предыдущие данные “в себе”. Реализация в реальном коде может отличаться.

Особенности использования

  • Самый лёгкий, быстрый и простой для вычисления алгоритм! В то же время очень эффективный
  • “Сила” фильтра настраивается коэффициентом (0.0 – 1.0). Чем он меньше, тем плавнее фильтр
  • Делает только одно измерение за раз, не блокирует код на длительный период
  • Чем чаще измерения, тем лучше работает
  • При маленьких значениях коэффициента работает очень плавно, что также можно использовать в своих целях

Вот так бегущее среднее справляется с равномерно растущим сигналом + случайные выбросы. Синий график – реальное значение, красный – фильтрованное с коэффициентом 0.1, зелёное – коэффициент 0.5.
Пример с шумящим синусом

Пример с шумным квадратным сигналом, на котором видно запаздывание фильтра:

Адаптивный коэффициент

Чтобы бегущее среднее корректно работало с резко изменяющимися сигналами, коэффициент можно сделать адаптивным, чтобы он подстраивался под резкие изменения значения, например так: если фильтрованное значение “далеко” от реального – коэффициент резко увеличивается, позволяя быстро сократить “разрыв” между величинами. Если значение “близко” – коэффициент ставится маленьким, чтобы хорошо фильтровать шум:

// бегущее среднее с адаптивным коэффициентом float expRunningAverageAdaptive(float newVal) < static float filVal = 0; float k; // резкость фильтра зависит от модуля разности значений if (abs(newVal - filVal) >1.5) k = 0.9; else k = 0.03; filVal += (newVal - filVal) * k; return filVal; >

Таким образом даже простейший фильтр можно “программировать” и делать более умным. В этом и заключается прелесть программирования!

Простой пример

Покажу отдельный простой пример реальной работы фильтра бегущее среднее, как самого часто используемого. Остальные фильтры – по аналогии. Фильтровать будем сигнал с аналогового пина А0:

void setup() < Serial.begin(9600); Serial.println("raw , filter"); >float filtVal = 0; void loop()

Код выводит в порт реальное и фильтрованное значение. Можно подключить к А0 потенциометр и покрутить его, наблюдая за графиком.

Целочисленная реализация

Для экономии ресурсов МК (поддержка дробных чисел требует пару килобайт Flash) можно отказаться от float и сделать фильтр целочисленным, для этого все переменные будут целого типа, а вместо умножения на дробный коэффициент будем делить на целое число, например fil += (val - fil) / 10 – получим фильтр с коэффициентом 0.1. Если вы читали урок по оптимизации кода, то знаете, что умножение на float быстрее деления на целое число! Не беда, всегда можно заменить деление сдвигом. Например фильтр с коэффициентом 1/16 можно записать так: fil += (val - fil) >> 4 . Оптимизировать получится коэффициенты, кратные “степени двойки”, то есть 1/2, 1/4, 1/8 и так далее.

Но есть один неприятный момент: в реализации с float у нас в распоряжении огромная точность вычислений и максимальная плавность фильтра, фильтрованное значение со временем полностью совпадёт с текущим. В целочисленной же реализации точность будет ограничена коэффициентом: если разность в скобках (фильтрованное – текущее) будет меньше делителя – переменная фильтра не изменится, так как к ней будет прибавляться 0! Простой пример: float и целочисленный фильтры “фильтруют” величину от значения 1000 к значению 100 с коэффициентом 0.1 (используем число 10 для наглядности):

fu += (val - fu) / 10; ff += (val - ff) * 0.1;

Где синий график – целочисленный фильтр, красный – float. Из графика видно, что фильтры работают +- одинаково, но вот целочисленный резко остановился на значении 109, как и ожидалось: в уравнении фильтра получается (100 – 109) / 10, что даёт 0 и переменная фильтра больше не меняется. Таким образом точность фильтра определяется делителем коэффициента. Поэтому при использовании целочисленной реализации нужно подумать о двух вещах:

  • Либо порядок фильтруемых величин у нас такой, что точность +- делитель не критична
  • Либо искусственно повышаем разрядность фильтра следующим образом: переменная фильтра хранит увеличенное значение, и текущее значение мы для вычислений тоже домножаем, а при получении результата – делим, например:
fu += (val * 16 - fu) / 10; Serial.print(fu / 16);
fu += ((val > 4);

Таким образом переменная фильтра хранит наше число с 16 кратной точностью, что позволяет в 16 раз увеличить плавность фильтра на критическом участке.

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

fu += (uint32_t)(((uint32_t)val > 5; Serial.print(fu >> 4);

Ещё целочисленные варианты

На сайте easyelectronics есть отличная статья по целочисленным реализациям, давайте коротко их рассмотрим.

Вариант 1

Фильтр не имеет настроек, состоит из сложения и двух сдвигов, выполняется моментально. По сути это среднее арифметическое, или же рассмотренный выше фильтр с коэффициентом 1/2.

filt = (filt >> 1) + (signal >> 1);

Вариант 2
filt = (A * filt + B * signal) >> k;

Коэффициенты у этого фильтра выбираются следующим образом:

  • k = 1, 2, 3…
  • A + B = 2^k
  • Чем больше А, тем плавнее фильтр (отношение A/B)
  • Чем больше k, тем более плавным можно сделать фильтр. Но больше 5 уже нет смысла, т.к. A=31, B=1 уже очень плавно, а при увеличении может переполниться int и придётся использовать 32-х битные типы данных.
  • Результат умножения не должен выходить за int, иначе придётся преобразовывать к long
  • Более подробно о выборе коэффициентов читайте в статье, ссылка выше
  • Особенность: если “ошибка” сигнала (signal – filter) меньше 2^k – фильтрованное значение меняться не будет. Для повышения “разрешения” фильтра можно домножить (или сдвинуть) переменную фильтра, то есть фильтровать в бОльших величинах

Например k = 4, значит A+B = 16. Хотим плавный фильтр, принимаем A=14, B=16: filt = (14 * filt + 2 * signal) >> 4;

Вариант 3

Третий алгоритм вытекает из второго: коэффициент B принимаем равным 1 и экономим одно умножение: filt = (A * filt + signal) >> k;

Тогда коэффициенты выбираются так:

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

Медианный фильтр тоже находит среднее значение, но не усредняя, а выбирая его из представленных. Алгоритм для медианы 3-го порядка (выбор из трёх значений) выглядит так:

int middle; if ((a // тут middle - ваша медиана

Мой постоянный читатель Андрей Степанов предложил сокращённую версию этого алгоритма, которая занимает одну строку кода и выполняется чуть быстрее за счёт меньшего количества сравнений:

middle = (a < b) ? ((b < c) ? b : ((c < a) ? a : c)) : ((a < c) ? a : ((c < b) ? b : c));

Можно ещё визуально сократить за счёт использования функций min() и max() :

middle = (max(a,b) == max(b, c)) ? max(a, c) : max(b, min(a, c));

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

// медиана на 3 значения со своим буфером int median(int newVal) < static int buf[3]; static byte count = 0; buf[count] = newVal; if (++count >= 3) count = 0; return (max(buf[0], buf[1]) == max(buf[1], buf[2])) ? max(buf[0], buf[2]) : max(buf[1], min(buf[0], buf[2])); >

Большое преимущество медианного фильтра заключается в том, что он ничего не вычислят, а просто сравнивает числа. Это делает его быстрее фильтров других типов!

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

Медиана для N значений лёгкая

// облегчённый вариант медианы для N значений // предложен Виталием Емельяновым, доработан AlexGyver // фильтр лёгкий с точки зрения кода и памяти, но выполняется долго // возвращает медиану по последним NUM_READ вызовам #define NUM_READ 10 int findMedianN(int newVal) < static int buffer[NUM_READ]; // статический буфер static byte count = 0; // счётчик buffer[count] = newVal; if (++count >= NUM_READ) count = 0; // перемотка буфера int buf[NUM_READ]; // локальный буфер для медианы for (byte i = 0; i < NUM_READ; i++) buf[i] = buffer[i]; for (int i = 0; i buf[m + 1]) < int buff = buf[m]; buf[m] = buf[m + 1]; buf[m + 1] = buff; >> > int ans = 0; if (NUM_READ % 2 == 0) < // кол-во элементов в массиве четное (NUM_READ - последний индекс массива) ans = buf[(int) (NUM_READ / 2)]; // берем центральное >else < ans = (buf[(int) (NUM_READ / 2)] + buf[((int) (NUM_READ / 2)) + 1]) / 2; // берем среднее от двух центральных >return ans; >

Медиана для N значений лёгкая оптимальная

// облегчённый вариант медианы для N значений // предложен Виталием Емельяновым, доработан AlexGyver // возвращает медиану по последним NUM_READ вызовам // НАВЕРНОЕ ЛУЧШИЙ ВАРИАНТ! #define NUM_READ 10 // порядок медианы // медиана на N значений со своим буфером, ускоренный вариант float findMedianN_optim(float newVal) < static float buffer[NUM_READ]; // статический буфер static byte count = 0; buffer[count] = newVal; if ((count < NUM_READ - 1) and (buffer[count] >buffer[count + 1])) < for (int i = count; i < NUM_READ - 1; i++) < if (buffer[i] >buffer[i + 1]) < float buff = buffer[i]; buffer[i] = buffer[i + 1]; buffer[i + 1] = buff; >> > else < if ((count >0) and (buffer[count - 1] > buffer[count])) < for (int i = count; i >0; i--) < if (buffer[i] < buffer[i - 1]) < float buff = buffer[i]; buffer[i] = buffer[i - 1]; buffer[i - 1] = buff; >> > > if (++count >= NUM_READ) count = 0; return buffer[(int)NUM_READ / 2]; >

Особенности использования

  • Медиана отлично фильтрует резкие изменения значения
  • Делает только одно измерение за раз, не блокирует код на длительный период
  • Алгоритм “больше трёх” весьма громоздкий
  • Запаздывает на половину размерности фильтра

Простой “Калман”

Данный алгоритм я нашёл на просторах Интернета, источник потерял. В фильтре настраивается разброс измерения (ожидаемый шум измерения), разброс оценки (подстраивается сам в процессе работы фильтра, можно поставить таким же как разброс измерения), скорость изменения значений (0.001-1, варьировать самому).

float _err_measure = 0.8; // примерный шум измерений float _q = 0.1; // скорость изменения значений 0.001-1, варьировать самому float simpleKalman(float newVal) < float _kalman_gain, _current_estimate; static float _err_estimate = _err_measure; static float _last_estimate; _kalman_gain = (float)_err_estimate / (_err_estimate + _err_measure); _current_estimate = _last_estimate + (float)_kalman_gain * (newVal - _last_estimate); _err_estimate = (1.0 - _kalman_gain) * _err_estimate + fabs(_last_estimate - _current_estimate) * _q; _last_estimate = _current_estimate; return _current_estimate; >

Особенности использования

  • Хорошо фильтрует и постоянный шум, и резкие выбросы
  • Делает только одно измерение за раз, не блокирует код на длительный период
  • Слегка запаздывает, как бегущее среднее
  • Подстраивается в процессе работы
  • Чем чаще измерения, тем лучше работает
  • Алгоритм весьма тяжёлый, вычисление длится ~90 мкс при системной частоте 16 МГц

Альфа-Бета фильтр

AB фильтр тоже является одним из видов фильтра Калмана, подробнее можно почитать можно на Википедии.

// период дискретизации (измерений), process variation, noise variation float dt = 0.02; float sigma_process = 3.0; float sigma_noise = 0.7; float ABfilter(float newVal) < static float xk_1, vk_1, a, b; static float xk, vk, rk; static float xm; float lambda = (float)sigma_process * dt * dt / sigma_noise; float r = (4 + lambda - (float)sqrt(8 * lambda + lambda * lambda)) / 4; a = (float)1 - r * r; b = (float)2 * (2 - a) - 4 * (float)sqrt(1 - a); xm = newVal; xk = xk_1 + ((float) vk_1 * dt ); vk = vk_1; rk = xm - xk; xk += (float)a * rk; vk += (float)( b * rk ) / dt; xk_1 = xk; vk_1 = vk; return xk_1; >

Особенности использования

  • Хороший фильтр, если правильно настроить
  • Но очень тяжёлый!

Метод наименьших квадратов

Следующий фильтр позволяет наблюдать за шумным процессом и предсказывать его поведение, называется он метод наименьших квадратов, теорию читаем тут. Чисто графическое объяснение здесь такое: у нас есть набор данных в виде несколько точек. Мы видим, что общее направление идёт на увеличение, но шум не позволяет сделать точный вывод или прогноз. Предположим, что существует линия, сумма квадратов расстояний от каждой точки до которой – минимальна. Такая линия наиболее точно будет показывать реальное изменение среди шумного значения. В какой то статье я нашел алгоритм, который позволяет найти параметры этой линии, опять же портировал на с++ и готов вам показать. Этот алгоритм выдает параметры прямой линии, которая равноудалена от всех точек.

float a, b, delta; // переменные, которые получат значения после вызова minQuad void minQuad(int *x_array, int *y_array, int arrSize) < int32_t sumX = 0, sumY = 0, sumX2 = 0, sumXY = 0; arrSize /= sizeof(int); for (int i = 0; i < arrSize; i++) < // для всех элементов массива sumX += x_array[i]; sumY += (long)y_array[i]; sumX2 += x_array[i] * x_array[i]; sumXY += (long)y_array[i] * x_array[i]; >a = (long)arrSize * sumXY; // расчёт коэффициента наклона прямой a = a - (long)sumX * sumY; a = (float)a / (arrSize * sumX2 - sumX * sumX); b = (float)(sumY - (float)a * sumX) / arrSize; delta = a * (x_array[arrSize-1] - x_array[0]); // расчёт изменения >

Особенности использования

  • В моей реализации принимает два массива и рассчитывает параметры линии, равноудалённой от всех точек

Какой фильтр выбрать?

Выбор фильтра зависит от типа сигнала и ограничений по времени фильтрации. Среднее арифметическое – хорошо подходит, если фильтрации производятся редко и время одного измерения значения мало. Для большинства ситуаций подходит бегущее среднее, он довольно быстрый и даёт хороший результат на правильной настройке. Медианный фильтр 3-го порядка тоже очень быстрый, но он может только отсеить выбросы, сам сигнал он не сгладит. Медиана большего порядка является довольно более громоздким и долгим алгоритмом, но работает уже лучше. Очень хорошо работает медиана 3 порядка + бегущее среднее, получается сглаженный сигнал с отфильтрованными выбросами (сначала фильтровать медианой, потом бегущим). AB фильтр и фильтр Калмана – отличные фильтры, справляются с шумным сигналом не хуже связки медиана + бегущее среднее, но нуждаются в тонкой настройке, также они довольно громоздкие с точки зрения кода. Линейная аппроксимация – инструмент специального назначения, позволяющий буквально предсказывать поведение значения за период – ведь мы получаем уравнение прямой. Если нужно максимальное быстродействие – работаем только с целыми числами и используем целочисленные фильтры.

Библиотека GyverFilters

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

  • GFilterRA – компактная альтернатива фильтра экспоненциальное бегущее среднее (Running Average)
  • GMedian3 – быстрый медианный фильтр 3-го порядка (отсекает выбросы)
  • GMedian – медианный фильтр N-го порядка. Порядок настраивается в GyverFilters.h – MEDIAN_FILTER_SIZE
  • GABfilter – альфа-бета фильтр (разновидность Калмана для одномерного случая)
  • GKalman – упрощённый Калман для одномерного случая (на мой взгляд лучший из фильтров)
  • GLinear – линейная аппроксимация методом наименьших квадратов для двух массивов

Видео

Полезные страницы

  • Набор GyverKIT – большой стартовый набор Arduino моей разработки, продаётся в России
  • Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
  • Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
  • Полная документация по языку Ардуино, все встроенные функции и макросы, все доступные типы данных
  • Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
  • Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
  • Поддержать автора за работу над уроками
  • Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])

Сравнение эффективности нелинейных методов фильтрации медицинских изображений

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

Ключевые слова:
Цитировать как:

Каныгина А.А. Сравнение эффективности нелинейных методов фильтрации медицинских изображений. Кардио-ИТ 2017; 4(1): e0101. [Kanygina AA. Comparison of the effectiveness of nonlinear methods for filtering medical images. Cardio-IT 2017; 4(1): e0101.]

10.15275/cardioit.2017.0101

PDF icon

cardio-it-2017-0101.pdf

Введение

Очистка от шума является одной из основных задач цифровой обработки изображений [1]. Изображение в математическом представлении — это двумерный сигнал, несущий в себе некое количество информации. Любой практический сигнал содержит не только полезную информацию, но и следы посторонних воздействий. Для устранения ненужной составляющей используют фильтры, а совокупность методов называется фильтрацией.

Возникновение шума обусловлено многочисленными факторами: тепловыми эффектами, сбоем детектора или сенсора, взаимодействиями между электронными компонентами системы формирования изображения, ошибками дискретизации, ошибками передачи и т.п. [2]. Для более достоверных результатов нужны качественные материалы и изображения, из-за этого в настоящее время проблеме очистки медицинских изображений от шумов и выбору оптимального метода фильтрации уделяется большое внимание [3].

На практике зачастую не всегда удаётся выявить на изображении полезную информацию. Часть этой информации не регистрируется человеческим глазом из-за слабого контраста, фоновой неоднородности, высокой зернистости, дефектов аппаратуры и, следовательно, не поддаётся анализу. В свою очередь, эти проблемы, а также проблемы, связанные с расшифровкой экспериментального контраста и надёжной идентификацией объектов исследования, можно решить цифровой обработкой экспериментальных изображений и представлением их в виде, более удобном для визуального анализа и регистрации дополнительных особенностей изображений, выявления объекта исследования и локализации его в объёме изображения. Способы устранения слабого контраста и фоновой неоднородности изображений дефектов, включая вейвлет-анализ, были рассмотрены многими авторами [4–8].

Следует отметить, что успехи современных исследований в области обработки одномерных сигналов (временных рядов) биомедицинской природы во многом связаны именно с применением сложных нелинейных методов [9–12], позволяющих снизить эффект, вносимый шумами, путём выделения информационной составляющей для её последующей обработки. Вопрос о применении сложных нелинейных методов для обработки двумерных сигналов всё ещё развит недостаточно [13, 14].

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

Материал и методы

В качестве исходных изображений для анализа были выбраны томографические изображения, полученные до прихода на блок обработки в компьютерном томографе, и изображение сердца, полученное на аппарате для ультразвуковых исследований (УЗИ).

Оценка возможность применения данных нелинейных фильтров (медианный фильтр [15], адаптивный медианный фильтр [16], курвлет-преобразование [17]) для очистки медицинских изображений от шумов была основана на методе визуальной оценки, методе сравнения похожести изображений (пиковое отношение сигнал/шум) [18], среднем времени работы каждого выбранного фильтра для изображений разных размеров.

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

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

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

Результаты

Результаты расчета пикового отношения сигнал/шум по изображениям с заранее известным видом и количеством шума представлены в таблице 1.

Таблица 1. Пиковое отношение сигнал/шум у изображений с аддитивным (независимо добавленным) гауссовским шумом с различными параметрами

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *