КИХ-фильтры (FIR): свёртка с ядром

Разбираем FIR — самый понятный и устойчивый класс фильтров: просто свёртка с подобранным ядром.

FIR-фильтр (КИХ, с конечной импульсной характеристикой) — фильтр, выход которого зависит только от конечного числа входных отсчётов: y[n] = sum(h[k]*x[n-k]). Это прямая свёртка сигнала с ядром h.

FIR — самый дружелюбный тип фильтра. Его работа — это в точности свёртка, которую мы уже освоили. У него нет обратной связи, поэтому он всегда устойчив и может иметь идеально линейную фазу (не искажает форму). Цена — для крутых фильтров нужно длинное ядро. Научимся проектировать FIR двумя способами.

FIR — это свёртка

Весь FIR-фильтр задаётся ядром h (его коэффициенты называют отводами, taps). Применение — свёртка. Самый простой FIR — скользящее среднее: ядро из одинаковых весов.

def fir(x, h):
    M = len(h); pad = M // 2
    out = []
    for i in range(len(x)):
        s = 0.0
        for j in range(M):
            idx = i - pad + j
            if 0 <= idx < len(x):
                s += x[idx] * h[j]
        out.append(round(s, 2))
    return out

def moving_average(k):
    return [1 / k] * k

sig = [2, 8, 4, 6, 10, 2, 4, 8]
print("MA-3 ядро:", [round(v, 3) for v in moving_average(3)])
print("сглажено: ", fir(sig, moving_average(3)))

Вывод:

MA-3 ядро: [0.333, 0.333, 0.333]
сглажено:  [3.33, 4.67, 6.0, 6.67, 6.0, 5.33, 4.67, 4.0]

Скользящее среднее — это FIR-ФНЧ. Но у него грубая АЧХ. Для качественного фильтра ядро проектируют умнее — методом окна.

Проектирование методом окна

Идеальный ФНЧ имеет ядро в виде функции sinc (бесконечной). Мы обрезаем её до конечной длины и умножаем на оконную функцию (чтобы убрать «звон»). Получается практичный FIR-ФНЧ.

import math

def fir_lowpass(numtaps, fc):
    # fc — частота среза в долях fs (0..0.5)
    M = numtaps - 1
    h = []
    for n in range(numtaps):
        k = n - M / 2
        ideal = 2 * fc if k == 0 else math.sin(2 * math.pi * fc * k) / (math.pi * k)
        window = 0.54 - 0.46 * math.cos(2 * math.pi * n / M)   # окно Хэмминга
        h.append(ideal * window)
    s = sum(h)
    return [round(c / s, 4) for c in h]    # нормируем усиление на 0 Гц к 1

kernel = fir_lowpass(7, 0.1)
print("Ядро ФНЧ (7 отводов):", kernel)
print("Сумма ~ 1 (усиление DC):", round(sum(kernel), 3))

Вывод:

Ядро ФНЧ (7 отводов): [0.0135, 0.0785, 0.2409, 0.3344, 0.2409, 0.0785, 0.0135]
Сумма ~ 1 (усиление DC): 1.0

Ядро симметрично (это и даёт линейную фазу) и колоколообразно: центральный отвод самый большой, края малы. Сумма коэффициентов равна единице — значит, постоянный сигнal проходит без изменения уровня.

FIR реально чистит шум

Проверим фильтр в деле: добавим шум к синусоиде и пропустим через наш FIR-ФНЧ.

import math, random
random.seed(42)

def fir(x, h):
    M = len(h); pad = M // 2; out = []
    for i in range(len(x)):
        s = 0.0
        for j in range(M):
            idx = i - pad + j
            if 0 <= idx < len(x):
                s += x[idx] * h[j]
        out.append(s)
    return out

h = [0.0135, 0.0785, 0.2409, 0.3344, 0.2409, 0.0785, 0.0135]
clean = [math.sin(2 * math.pi * n / 32) for n in range(32)]
noisy = [s + random.uniform(-0.4, 0.4) for s in clean]
filtered = fir(noisy, h)

def rms(a, b):
    return round(math.sqrt(sum((x - y) ** 2 for x, y in zip(a, b)) / len(a)), 3)

print("RMS шума до фильтра: ", rms(noisy, clean))
print("RMS шума после:      ", rms(filtered, clean))

Вывод:

RMS шума до фильтра:  0.244
RMS шума после:       0.122

Фильтр вдвое снизил отклонение от чистого сигнала: высокочастотный шум подавлен, синусоида осталась. FIR работает.

Как работает под капотом

Ключевое достоинство FIR — линейная фаза при симметричном ядре. Линейная фаза означает, что все частоты задерживаются на одинаковое время, поэтому форма сигнала не искажается — только сдвигается. Это критично для аудио и связи, где важна сохранность формы (например, при передаче данных). У IIR-фильтров (следующий урок) фаза нелинейна. Обратная сторона: чтобы получить крутой срез, FIR требует много отводов (десятки-сотни), а значит, много умножений на отсчёт. Длинные FIR на практике считают через БПФ (быстрая свёртка): перейти в спектр, перемножить, вернуться — это O(N log N) вместо O(N*M).

Частые ошибки

  • Забыть нормировать ядро. Если сумма коэффициентов не равна 1, фильтр изменит общий уровень сигнала.
  • Обрезать sinc без окна. Резкое обрезание даёт «звон» в АЧХ (эффект Гиббса); окно сглаживает.
  • Брать слишком короткое ядро для крутого среза. Чем круче нужен переход, тем больше отводов — это закон.

Итог

  • FIR-фильтр — это свёртка сигнала с конечным ядром h (отводами).
  • Простейший FIR — скользящее среднее; качественный проектируют методом окна.
  • Симметричное ядро даёт линейную фазу — форма сигнала не искажается.
  • FIR всегда устойчив, но для крутого среза требует много отводов.
Проверьте себя
1. Как работает FIR-фильтр?
AЧерез обратную связь от выхода
BКак свёртка сигнала с конечным ядром h
CЧерез преобразование Фурье обязательно
DСлучайным выбором отсчётов
2. Что даёт симметричное ядро FIR-фильтра?
AНестабильность
BЛинейную фазу — форма сигнала не искажается
CБесконечную импульсную характеристику
DУсиление шума
3. Зачем при проектировании FIR методом окна умножают sinc на оконную функцию?
AЧтобы ускорить фильтр
BЧтобы убрать «звон» (эффект Гиббса) от обрезания идеального ядра
CЧтобы сделать ядро бесконечным
DЭто необязательно