들어가며
지난 시간에 배운 STFT(Short-Time Fourier Transform)는 신호를 짧게 잘라 분석하는 강력한 도구이지만, 분석 내내 '고정된 크기의 윈도우'를 사용해야 한다는 근본적인 한계가 있습니다. 저주파 신호는 천천히 진동하므로 긴 윈도우가 필요하고, 고주파 신호는 빠르게 진동하므로 짧은 윈도우가 필요한데, STFT는 이 둘을 동시에 만족시킬 수 없었습니다. 시간 분해능을 높이면 주파수 분해능이 떨어지고, 주파수 분해능을 높이면 시간 분해능이 떨어지는 '불확정성의 딜레마'에 빠지게 됩니다.
이를 극복하기 위해, 주파수 대역에 따라 윈도우의 크기를 유연하게 고무줄처럼 늘렸다 줄였다 하며 다중 해상도(Multi-Resolution)로 신호를 쪼개보는 마법 같은 수학적 도구가 등장합니다. 오늘은 신호 처리의 또 다른 혁명, 웨이블릿 변환(Wavelet Transform)의 원리를 이해하고, Python 실습을 통해 한계에 부딪힌 STFT를 넘어서는 방법을 알아보겠습니다.
1. 웨이블릿(Wavelet)이란 무엇인가?
푸리에 변환의 핵심은 복잡한 신호를 무한히 진동하는 '사인파와 코사인파'의 합으로 분해하는 것이었습니다. 하지만 무한히 진동하는 파동은 국소적인 시간의 변화(갑자기 튀어 오르는 노이즈, 임펄스 등)를 잡아내는 데 취약합니다.
반면 웨이블릿(Wavelet)은 이름 그대로 '작은(let) 파동(Wave)'이라는 뜻으로, 유한한 시간 구간에서 진동하다가 빠르게 소멸하는 특수한 함수입니다. 웨이블릿은 국소적(local)으로 집중된 기저 함수 즉, 에너지가 한곳에 집중되어 있고 빠르게 감쇠하여 0으로 수렴하는 짧은 파동을 기본 형태(Mother Wavelet)로 사용합니다.
웨이블릿 변환은 이 기본 파동을 조작하여 신호를 분석합니다.
- 스케일링(Scaling, 축소/확대): 파동(기저 함수)을 양옆으로 늘리거나 압축하여, 저주파 또는 고주파 정보를 선택적으로 포착합니다.
- 이동(Translation, 시간 이동): 파동(기저 함수)을 시간 축을 따라 좌우로 스캐닝하며 신호와 얼마나 겹치는지(내적)를 계산합니다.
이 두 연산의 조합 덕분에, 웨이블릿은 STFT가 갖는 시간-주파수 해상도의 고정된 트레이드오프를 근본적으로 해결합니다.
여기서 $\psi(t)$는 모(母) 웨이블릿(mother wavelet), $a$는 스케일(scale) 파라미터, $b$는 이동(translation) 파라미터입니다. 스케일 $a$가 크면 파형이 넓게 퍼져 저주파 성분을, 스케일 $a$가 작으면 파형이 좁게 압축되어 고주파 성분을 분석합니다.
💡 스케일(Scale) vs. 주파수: 스케일은 주파수와 반비례 관계입니다. 대스케일(large scale) = 저주파, 소스케일(small scale) = 고주파로 이해하면 됩니다. 각 모 웨이블릿마다 스케일과 Hz 단위 주파수 간의 변환 공식이 정의되어 있습니다.
2. 주요 모 웨이블릿(Mother Wavelet) 종류
모 웨이블릿의 선택은 분석 대상 신호의 특성에 따라 달라집니다. 대표적인 웨이블릿은 다음과 같습니다.
| 웨이블릿 | 특징 | 주요 활용 |
|---|---|---|
| Haar | 가장 단순한 계단 함수 형태 | 불연속 신호, 에지 검출 |
| Daubechies (db) | 컴팩트 지지(compact support), 대칭성 없음 | 신호 압축, 잡음 제거 |
| Morlet | 가우시안 변조된 복소 정현파 | 오디오, 뇌파(EEG) 분석 |
| Mexican Hat (ricker) | 가우시안 2차 미분 형태 | 지진파, 이미지 분석 |
| Symlet (sym) | Daubechies의 대칭성 개선 버전 | 생체 신호 처리 |
| Coiflet (coif) | 높은 소실 모멘트(vanishing moment) | 수치 해석 |
3. 연속 웨이블릿 변환(CWT)
연속 웨이블릿 변환(Continuous Wavelet Transform, CWT)은 스케일 $a$와 이동 $b$를 연속적으로 변화시키며 신호와 웨이블릿의 내적(inner product)을 계산합니다.
$W(a, b)$는 **웨이블릿 계수(wavelet coefficient)**로, 시각 $b$에서 스케일 $a$에 해당하는 주파수 성분이 신호에 얼마나 존재하는지를 나타냅니다. 이 계수의 제곱 $|W(a, b)|^2$을 시각화한 것이 스칼로그램(Scalogram)으로, 스펙트로그램에 대응하는 CWT의 시간-주파수 표현입니다.
Python을 이용하여 처프 신호와 순간 충격 신호가 섞인 신호의 스칼로그램을 직접 그려보겠습니다. 이번 편에서는 Python의 웨이블릿 라이브러리인 PyWavelets를 사용합니다. Anaconda 환경 또는 pip 환경에서 아래와 같이 설치합니다.
# Anaconda 환경
conda install -c conda-forge pywavelets
# pip 환경
pip install PyWavelets
설치 후 아래와 같이 임포트가 정상적으로 동작하는지 확인합니다.
import pywt
print(pywt.__version__) # 1.x.x 출력 확인
print(pywt.wavelist()[:10]) # 사용 가능한 웨이블릿 목록 일부 확인
아래 코드는 스칼로그램(Scalogram)에서 충격(impulse) 신호가 처프 신호에 비해 상대적으로 작아 신호가 묻힐 수 있어서 로그 스케일 컬러맵을 적용하여 넓은 동적 범위를 표현하였습니다.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
from matplotlib.colors import LogNorm
import pywt
# 한국어 폰트 설정
plt.rcParams['font.family'] = 'Malgun Gothic'
plt.rcParams['axes.unicode_minus'] = False
# ── 1. 테스트 신호 생성 (주파수가 시간에 따라 변하는 처프 신호 + 순간 충격) ──
fs = 1000 # 샘플링 주파수 (Hz)
T = 2.0 # 신호 길이 (초)
t = np.linspace(0, T, int(fs * T), endpoint=False)
# 저주파(10 Hz) → 고주파(100 Hz)로 선형 처프
f0, f1 = 10, 100
chirp = np.sin(2 * np.pi * (f0 + (f1 - f0) / (2 * T) * t) * t)
# 순간 충격(Dirac-like impulse): t = 1.0초 지점
impulse = np.zeros_like(t)
impulse[int(fs * 1.0)] = 5.0
signal = chirp + impulse
# ── 2. CWT 수행 (Morlet 웨이블릿) ──
wavelet = 'morl'
scales = np.arange(1, 128)
coeffs, freqs = pywt.cwt(signal, scales, wavelet, 1.0 / fs)
power = np.abs(coeffs) ** 2
# ── 3. 스칼로그램 시각화 (로그 스케일 컬러맵) ──
fig, axes = plt.subplots(2, 1, figsize=(12, 8), sharex=True)
# (a) 원본 신호
axes[0].plot(t, signal, color='steelblue', lw=0.8)
axes[0].axvline(1.0, color='tomato', lw=1.2, ls='--', label='충격 위치 (t = 1.0 s)')
axes[0].set_title('원본 신호 (처프 + 충격)')
axes[0].set_ylabel('진폭')
axes[0].set_xlim(0, T)
axes[0].legend(loc='upper left', fontsize=9)
axes[0].grid(True, alpha=0.3)
# (b) 스칼로그램 — LogNorm으로 넓은 동적 범위 표현
vmax = power.max()
vmin = vmax * 1e-4 # 동적 범위 10,000 : 1
img = axes[1].pcolormesh(
t, freqs, power,
norm=LogNorm(vmin=vmin, vmax=vmax),
cmap='inferno', shading='auto'
)
axes[1].axvline(1.0, color='cyan', lw=1.2, ls='--', label='충격 위치 (t = 1.0 s)')
axes[1].set_title('CWT 스칼로그램 (Morlet) — 로그 스케일')
axes[1].set_ylabel('주파수 (Hz)')
axes[1].set_xlabel('시간 (초)')
axes[1].set_ylim([5, 300]) # 관심 주파수 대역(5 ~ 300 Hz) 확대
axes[1].legend(loc='upper left', fontsize=9)
cbar = fig.colorbar(img, ax=axes[1])
cbar.set_label('파워 |W|² (로그 스케일)')
plt.tight_layout()
plt.savefig('cwt_scalogram.png', dpi=150, bbox_inches='tight')
plt.show()
print("✅ CWT 스칼로그램 저장 완료")

처프 신호의 궤적이 시간이 지나면서(주파수가 높아지면서) 점점 두꺼워지는 것을 확인할 수 있습니다. 이는 오류가 아니라 웨이블릿의 Constant-Q 특성을 그대로 반영한 결과입니다. CWT는 중심 주파수에 대한 대역폭의 비율($\Delta f / f_c$)이 항상 일정하므로, 고주파 영역에서는 절대적인 Hz 단위의 주파수 분해능이 낮아지는 대신 시간 분해능이 높아집니다. $t = 1.0$초 지점의 충격은 로그 스케일 덕분에 모든 주파수 대역을 수직으로 관통하는 선으로 선명하게 확인됩니다.
위 실험 결과에서 언급한 등비 대역폭(Constant-Q)을 부연하면, CWT(Morlet)는 이 특성을 가집니다. 즉, 중심 주파수에 대한 대역폭의 비율이 항상 일정합니다.
$$\frac{\Delta f}{f_c} = \text{일정}$$
따라서 주파수가 높아질수록 절대적인 Hz 단위의 대역폭이 함께 넓어집니다.
| 구분 | 저주파 (10 Hz 부근) | 고주파 (100 Hz 부근) |
|---|---|---|
| 시간 분해능 | 낮음 (윈도우 넓음) | 높음 (윈도우 좁음) |
| 주파수 분해능 | 높음 (대역폭 좁음) | 낮음 (대역폭 넓음) |
| 스칼로그램 선 굵기 | 얇음 | 두꺼워짐 ✅ |
이것이 바로 STFT와의 핵심 차이입니다. STFT는 모든 주파수에서 대역폭이 동일한 균일 해상도인 반면, CWT는 주파수에 비례하여 대역폭이 넓어지는 다중 해상도입니다.
4. DWT: CWT의 다중 해상도를 실용으로 구현
CWT는 스케일과 이동을 연속적으로 변화시키므로 정밀한 분석이 가능하지만, 계산량이 매우 많고 결과에 중복 정보가 포함됩니다. 이산 웨이블릿 변환(Discrete Wavelet Transform, DWT)은 스케일과 이동을 이진(dyadic) 기반으로 이산화하여 이 문제를 해결합니다.
이 이산화가 곧 다음 그림이 보여주는 옥타브 기반 가변 타일 구조입니다. 주파수가 한 옥타브(2배) 높아질 때마다 시간 타일이 정확히 절반으로 쪼개집니다. 이것이 DWT가 구현하는 다중 해상도의 본질이며, STFT의 균일 타일 구조와 근본적으로 다른 점입니다.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
import matplotlib.patches as mpatches
# ── 한글 폰트 설정 ──
plt.rcParams['font.family'] = 'Malgun Gothic'
plt.rcParams['axes.unicode_minus'] = False
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# ── (a) STFT: 균일 타일 ──
ax = axes[0]
for f in np.linspace(0, 250, 6):
ax.axhline(f, color='steelblue', lw=0.8)
for ti in np.linspace(0, 2.0, 5):
ax.axvline(ti, color='steelblue', lw=0.8)
ax.set_title('STFT: 균일 타일 (고정 해상도)')
ax.set_xlabel('시간 (초)')
ax.set_ylabel('주파수 (Hz)')
ax.set_xlim(0, 2.0)
ax.set_ylim(0, 250)
ax.grid(False)
# ── (b) DWT: 옥타브 기반 가변 타일 ──
# 주파수가 2배 높아질 때마다 시간 타일 수도 2배로 쪼개짐
ax = axes[1]
freq_edges_cwt = [0, 10, 20, 40, 80, 160, 250]
for i in range(len(freq_edges_cwt) - 1):
f_lo = freq_edges_cwt[i]
f_hi = freq_edges_cwt[i + 1]
n_tiles = 2 ** i
tile_w = 2.0 / n_tiles
for j in range(n_tiles):
rect = mpatches.FancyBboxPatch(
(j * tile_w, f_lo), tile_w, f_hi - f_lo,
boxstyle="square,pad=0",
edgecolor='tomato', facecolor='lightyellow', lw=0.9
)
ax.add_patch(rect)
ax.set_title('DWT: 옥타브 기반 가변 타일 (다중 해상도)')
ax.set_xlabel('시간 (초)')
ax.set_ylabel('주파수 (Hz)')
ax.set_xlim(0, 2.0)
ax.set_ylim(0, 250)
ax.grid(False)
plt.suptitle('STFT vs DWT: 시간-주파수 해상도 타일 비교', fontsize=13)
plt.tight_layout()
plt.savefig('stft_vs_dwt_tiling.png', dpi=150, bbox_inches='tight')
plt.show()
print("✅ 해상도 타일 비교 그림 저장 완료")

좌측 STFT 타일은 모든 주파수 대역에서 크기가 동일합니다. 우측 DWT 타일은 저주파일수록 타일이 넓고 낮으며(주파수 분해능 우수), 고주파일수록 타일이 좁고 높습니다(시간 분해능 우수). 이것이 웨이블릿이 STFT의 한계를 극복하는 핵심 구조입니다.
이 타일 구조는 DWT의 필터 뱅크(Filter Bank)로 효율적으로 구현됩니다. 각 분해 단계(레벨)에서 신호는 두 서브밴드로 분리됩니다.
- 근사 계수(Approximation Coefficients, cA): 저역통과 필터(LPF)를 통과한 저주파 성분
- 상세 계수(Detail Coefficients, cD): 고역통과 필터(HPF)를 통과한 고주파 성분
각 단계마다 다운샘플링(2:1)을 적용하여 계산 효율성을 확보하며, 이 과정을 반복하면 위 타일 그림의 계층 구조가 그대로 구현됩니다.
원본 신호 (N 샘플)
├─ cD1: 상세 계수 레벨 1 (고주파, N/2 샘플)
└─ cA1: 근사 계수 레벨 1 (저주파, N/2 샘플)
├─ cD2: 상세 계수 레벨 2 (중주파, N/4 샘플)
└─ cA2: 근사 계수 레벨 2 (저주파, N/4 샘플)
├─ cD3: 상세 계수 레벨 3 (N/8 샘플)
└─ cA3: 근사 계수 레벨 3 (N/8 샘플)
이 구조에서 총 계수의 수가 원본 신호 샘플 수와 동일합니다. 정보 손실 없이 완전 재구성(Perfect Reconstruction)이 가능한 이유입니다. 아래 코드는 DWT를 이용하여 다중 레벨로 신호를 분해하는 예제입니다.
import numpy as np
import matplotlib.pyplot as plt
import pywt
np.random.seed(42)
# 한국어 폰트 설정
plt.rcParams['font.family'] = 'Malgun Gothic'
plt.rcParams['axes.unicode_minus'] = False
# ── 1. 테스트 신호 생성 ──
fs = 1000
t = np.linspace(0, 1.0, fs, endpoint=False)
# 서로 다른 주파수 성분을 구간별로 혼합
signal = np.zeros(fs)
signal += np.sin(2 * np.pi * 5 * t) # 5 Hz (전 구간)
signal += np.sin(2 * np.pi * 50 * t) * (t < 0.5) # 50 Hz (전반부)
signal += np.sin(2 * np.pi * 120 * t) * (t >= 0.5) # 120 Hz (후반부)
signal += np.random.normal(0, 0.1, fs) # 가우시안 잡음
# ── 2. DWT 다중 레벨 분해 (Daubechies 4, 4레벨) ──
wavelet = 'db4'
level = 4
coeffs = pywt.wavedec(signal, wavelet, level=level)
# coeffs = [cA4, cD4, cD3, cD2, cD1]
labels = [f'cA{level}'] + [f'cD{l}' for l in range(level, 0, -1)]
# ── 3. 분해 결과 시각화 ──
fig, axes = plt.subplots(level + 2, 1, figsize=(12, 14))
axes[0].plot(t, signal, color='steelblue', lw=0.7)
axes[0].set_title('원본 신호')
axes[0].set_ylabel('진폭')
axes[0].set_xlim(0, 1.0)
axes[0].grid(True, alpha=0.3)
colors = ['darkorange', 'tomato', 'mediumseagreen', 'mediumpurple', 'slateblue']
for i, (coeff, label) in enumerate(zip(coeffs, labels)):
t_coeff = np.linspace(0, 1.0, len(coeff))
axes[i + 1].plot(t_coeff, coeff, color=colors[i % len(colors)], lw=0.8)
axes[i + 1].set_title(f'{label} (샘플 수: {len(coeff)})')
axes[i + 1].set_ylabel('계수')
axes[i + 1].set_xlim(0, 1.0)
axes[i + 1].grid(True, alpha=0.3)
axes[-1].set_xlabel('시간 (초)')
plt.suptitle(f'DWT 다중 레벨 분해 ({wavelet}, {level}레벨)', fontsize=13, y=1.01)
plt.tight_layout()
plt.savefig('dwt_decomposition.png', dpi=150, bbox_inches='tight')
plt.show()
# ── 4. 완전 재구성 (Inverse DWT) ──
reconstructed = pywt.waverec(coeffs, wavelet)
# 길이 맞춤 (수치 오차로 1샘플 차이 발생 가능)
reconstructed = reconstructed[:len(signal)]
max_error = np.max(np.abs(signal - reconstructed))
print(f"✅ 재구성 최대 오차: {max_error:.2e} (완전 재구성 검증)")

각 레벨의 상세 계수(cD)는 해당 주파수 대역의 신호 성분을 분리하여 보여줍니다. cD1은 가장 고주파(120 Hz 성분이 후반부에만), cA4는 5 Hz의 저주파 기저 신호를 담고 있음을 관찰할 수 있습니다. 재구성 최대 오차는 수치 정밀도 수준(약 1e-15)이므로, 완전 재구성이 이루어졌음을 확인합니다.
5. DWT 응용: 웨이블릿 기반 잡음 제거 (Wavelet Denoising)
- DWT의 대표적인 실용 응용은 잡음 제거(Denoising)입니다. 잡음은 주로 고주파 상세 계수(cD)에 집중되는 반면, 신호의 주요 정보는 근사 계수(cA)와 일부 상세 계수에 보존됩니다. 임계값 처리(Thresholding)를 이용하여 작은 계수(잡음)를 0으로 만들어 잡음을 제거합니다.
- 하드 임계값(Hard Thresholding): $|c| < \lambda$인 계수를 0으로 설정
- 소프트 임계값(Soft Thresholding): $|c| < \lambda$인 계수를 0으로, 나머지는 $\text{sign}(c)(|c| - \lambda)$로 축소
아래 코드는 Wavelet Denoising 예제이며, 임계값 계산에 대해서 부연하면, coeffs[-1]은 가장 고주파 상세 계수(cD1)로, 잡음 성분이 가장 집중된 대역입니다. MAD를 0.6745로 나누면 가우시안 잡음의 표준편차 $\sigma$를 강건하게 추정할 수 있으며, 이로부터 Universal Threshold $\lambda = \sigma \sqrt{2 \ln N}$을 계산합니다.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
import pywt
np.random.seed(42)
# ── 한글 폰트 설정 ──
plt.rcParams['font.family'] = 'Malgun Gothic'
plt.rcParams['axes.unicode_minus'] = False
# ── 1. 잡음 있는 신호 생성 ──
fs = 1000
t = np.linspace(0, 1.0, fs, endpoint=False)
clean = np.sin(2 * np.pi * 5 * t) + 0.5 * np.sin(2 * np.pi * 30 * t)
noise = np.random.normal(0, 0.4, fs)
noisy = clean + noise
# ── 2. DWT 분해 ──
wavelet = 'sym6'
level = 5
coeffs = pywt.wavedec(noisy, wavelet, level=level)
# ── 3. MAD 기반 잡음 분산 추정 및 Universal Threshold 계산 ──
# coeffs[-1]: 최고 주파수 상세 계수 (cD1) → 잡음 성분이 가장 집중된 대역
sigma = np.median(np.abs(coeffs[-1])) / 0.6745
threshold = sigma * np.sqrt(2 * np.log(len(noisy)))
# ── 4. 소프트 임계값 처리 (근사 계수 cA는 그대로 유지) ──
coeffs_denoised = [coeffs[0]]
for c in coeffs[1:]:
coeffs_denoised.append(pywt.threshold(c, threshold, mode='soft'))
# ── 5. 역변환으로 복원 ──
denoised = pywt.waverec(coeffs_denoised, wavelet)[:len(noisy)]
# ── 6. SNR 계산 및 결과 비교 ──
snr_in = 10 * np.log10(np.mean(clean ** 2) / np.mean(noise ** 2))
snr_out = 10 * np.log10(np.mean(clean ** 2) / np.mean((clean - denoised) ** 2))
fig, axes = plt.subplots(3, 1, figsize=(12, 8), sharex=True)
axes[0].plot(t, clean, color='steelblue', lw=1.0, label='원본 신호')
axes[1].plot(t, noisy, color='tomato', lw=0.5, label=f'잡음 신호 (SNR = {snr_in:.1f} dB)')
axes[2].plot(t, denoised, color='seagreen', lw=1.0, label=f'복원 신호 (SNR = {snr_out:.1f} dB)')
for ax in axes:
ax.legend(loc='upper right')
ax.set_xlim(0, 1.0)
ax.grid(True, alpha=0.3)
axes[-1].set_xlabel('시간 (초)')
plt.suptitle(f'웨이블릿 잡음 제거 ({wavelet}, 소프트 임계값)', fontsize=13)
plt.tight_layout()
plt.savefig('wavelet_denoising.png', dpi=150, bbox_inches='tight')
plt.show()
print(f"✅ 입력 SNR: {snr_in:.2f} dB")
print(f"✅ 출력 SNR: {snr_out:.2f} dB (개선량: {snr_out - snr_in:.2f} dB)")

Universal Threshold 기반의 소프트 임계값 처리 결과, SNR이 유의미하게 개선됩니다. 소프트 임계값은 하드 임계값에 비해 복원 신호에서 깁스 현상(Gibbs phenomenon)이 줄어들어 더 부드러운 결과를 만들어냅니다.
✅ 입력 SNR: 6.10 dB
✅ 출력 SNR: 7.62 dB (개선량: 1.52 dB)
6. 2D DWT: 이미지 처리에 대한 응용
웨이블릿 변환은 1차원 신호에만 국한되지 않습니다. 2D DWT를 이미지에 적용하면 수평(H), 수직(V), 대각선(D) 방향의 세부 정보를 분리할 수 있습니다. 이는 JPEG 2000 이미지 압축 표준의 핵심 기술이기도 합니다.
import numpy as np
import matplotlib.pyplot as plt
import pywt
from skimage import data
# ── 한글 폰트 설정 ──
plt.rcParams['font.family'] = 'Malgun Gothic'
plt.rcParams['axes.unicode_minus'] = False
# ── 1. 테스트 이미지 로드 ──
image = data.camera().astype(float) # 512×512 그레이스케일
# ── 2. 2D DWT 1레벨 분해 ──
coeffs2d = pywt.dwt2(image, 'haar')
cA, (cH, cV, cD) = coeffs2d
# ── 3. 서브밴드 시각화 ──
def normalize(arr):
a = np.abs(arr)
return (a - a.min()) / (a.max() - a.min() + 1e-10)
fig, axes = plt.subplots(2, 2, figsize=(10, 10))
axes[0, 0].imshow(normalize(cA), cmap='gray')
axes[0, 0].set_title('LL (근사, 저주파)')
axes[0, 1].imshow(normalize(cH), cmap='gray')
axes[0, 1].set_title('LH (수평 에지)')
axes[1, 0].imshow(normalize(cV), cmap='gray')
axes[1, 0].set_title('HL (수직 에지)')
axes[1, 1].imshow(normalize(cD), cmap='gray')
axes[1, 1].set_title('HH (대각선 에지)')
for ax in axes.flat:
ax.axis('off')
plt.suptitle('2D DWT 1레벨 분해 (Haar 웨이블릿)', fontsize=13)
plt.tight_layout()
plt.savefig('dwt2d_decomposition.png', dpi=150, bbox_inches='tight')
plt.show()
print("✅ 2D DWT 분해 완료")

LL 서브밴드는 원본 이미지의 축소판처럼 보이는 저주파 정보를, LH/HL/HH 서브밴드는 각 방향의 에지(경계선) 정보를 선명하게 보여줍니다. JPEG 2000이 이 구조를 반복적으로 적용하여 탁월한 압축 품질을 달성하고 있습니다.
[Insight]
최근 딥러닝이 발전하면서 "DSP 지식 없이 1D Conv나 RNN에 신호를 밀어 넣으면 알아서 특징을 추출하는 것 아니냐?"라는 질문을 종종 받습니다. 하지만 현업의 딥러닝 전문가들은 여전히 전처리 단계에서 웨이블릿 변환을 강력하게 활용합니다.
의료 및 센서 데이터 분야에서는 특히 ECG(심전도), EEG(뇌파)처럼 미세한 스파이크(Spike)의 '발생 시점'이 생사를 가르는 경우, 시간 해상도를 잃어버리는 FFT 기반 전처리보다, 이상 징후를 날카롭게 집어내는 웨이블릿 스칼로그램을 CNN의 입력 이미지로 사용하는 것이 모델 성능을 비약적으로 끌어올립니다.
우리가 아는 JPEG2000 이미지 압축 표준 역시 DWT를 기반으로 하는 것을 앞에서 살펴보았습니다. 딥러닝 모델에 들어가기 전, DWT를 이용해 의미 없는 고주파 노이즈 대역을 미리 걷어내면 모델의 수렴 속도와 일반화 성능(Robustness)이 크게 향상됩니다.
결국 도메인 지식(DSP)을 바탕으로 데이터를 모델이 가장 잘 이해할 수 있는 형태(다중 해상도 특성)로 가공해 주는 엔지니어가, 단순히 파이토치(PyTorch) 코드만 돌리는 엔지니어보다 많은 선택지를 가지게 됩니다.
다음 글 예고
웨이블릿 변환을 통해 우리는 신호를 다중 해상도로 분해하고, 각 주파수 대역의 성분을 독립적으로 다루는 방법을 익혔습니다. 이 과정에서 "저역통과 필터와 고역통과 필터를 반복 적용하여 신호를 서브밴드로 분해한다"는 핵심 구조를 살펴보았습니다.
그렇다면 필터(Filter)란 정확히 무엇이며, 신호에 필터를 적용한다는 것은 수학적으로 어떤 연산을 의미할까요? 이 모든 필터의 동작 원리를 하나의 수학 언어로 기술하는 연산이 바로 컨볼루션(Convolution)입니다.
다음 시간에는 [14. 필터의 기초와 컨볼루션(Convolution): 시스템 응답의 원리] 편을 통해, 디지털 필터 설계의 출발점인 컨볼루션 연산의 직관적 의미와 임펄스 응답(Impulse Response)의 개념을 Python으로 직접 체험해 보겠습니다.
📌 이전 연재 글 보기
- [10] FFT 심화: 진폭/위상 스펙트럼 및 PSD(Power Spectral Density) 해석
- [11] 윈도우 함수(Windowing): Hamming, Hann 등 윈도우별 분해능 비교
- [12] STFT와 스펙트로그램: 시간에 따라 변하는 주파수 분석 (오디오 분석)
