サンプリング定理(ナイキスト・シャノン)の証明とエイリアシングの可視化

アナログ信号をデジタルに変換するとき、「どのくらいの頻度でサンプリングすれば元の信号を完全に復元できるか?」という根本的な問いに答えるのがサンプリング定理(Nyquist-Shannon sampling theorem)です。この定理は、デジタル音声、画像処理、通信システムなど、あらゆるデジタル信号処理の理論的基盤です。

本記事の内容

  • サンプリング定理の定式化
  • フーリエ変換を用いた証明
  • ナイキスト周波数とエイリアシング
  • sinc関数による完全復元(D/A変換)
  • Pythonでの可視化

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

サンプリング定理の定式化

定理の主張

帯域制限信号 $x(t)$ の最高周波数が $f_{\max}$ であるとき(すなわち $|f| > f_{\max}$ で $X(f) = 0$)、サンプリング周波数 $f_s$ が以下を満たせば、離散サンプル $x(nT_s)$($T_s = 1/f_s$)から $x(t)$ を完全に復元できます:

$$ \boxed{f_s > 2 f_{\max}} $$

ここで $f_N = f_s / 2$ をナイキスト周波数(Nyquist frequency)と呼びます。

この定理は次のように読み替えられます:

  • 信号に含まれる最高周波数の2倍より高いレートでサンプリングすれば、情報の損失はない
  • 逆に、$f_s \leq 2f_{\max}$ のときは元の信号を正しく復元できない(エイリアシングが発生)

フーリエ変換を用いた証明

ステップ1:サンプリングの数学的表現

連続信号 $x(t)$ をサンプリング周期 $T_s$ でサンプリングすることは、デルタ関数の櫛(comb function)を掛けることに等しいです:

$$ x_s(t) = x(t) \cdot \sum_{n=-\infty}^{\infty} \delta(t – nT_s) $$

$$ = \sum_{n=-\infty}^{\infty} x(nT_s) \, \delta(t – nT_s) $$

ステップ2:サンプリング信号のスペクトル

デルタ関数の櫛のフーリエ変換は、周波数領域でも周期 $f_s = 1/T_s$ のデルタ櫛になります:

$$ \sum_{n=-\infty}^{\infty} \delta(t – nT_s) \quad \longleftrightarrow \quad f_s \sum_{k=-\infty}^{\infty} \delta(f – kf_s) $$

時間領域の乗算は周波数領域の畳み込みに対応するので:

$$ X_s(f) = X(f) * f_s \sum_{k=-\infty}^{\infty} \delta(f – kf_s) $$

$$ = f_s \sum_{k=-\infty}^{\infty} X(f – kf_s) $$

これは、元のスペクトル $X(f)$ が $f_s$ の間隔で周期的に繰り返されることを意味します。

ステップ3:復元の条件

元のスペクトルを正しく取り出すには、隣り合うスペクトルのコピーが重ならない必要があります。$X(f)$ の帯域が $[-f_{\max}, f_{\max}]$ であるとき、重なりが起きない条件は:

$$ f_s – f_{\max} > f_{\max} $$

$$ \boxed{f_s > 2f_{\max}} $$

この条件が満たされていれば、理想低域通過フィルタ(カットオフ周波数 $f_s/2$、ゲイン $T_s$)を適用するだけで $X(f)$ を復元できます。

エイリアシング

$f_s \leq 2f_{\max}$ のとき、サンプリング後のスペクトルのコピーが重なり合います。この重なりをエイリアシング(aliasing)と呼びます。

エイリアシングが起きると、高周波成分が低周波成分と区別不可能になります。具体的には、周波数 $f_0 > f_s/2$ の信号は、サンプリング後に $|f_0 – kf_s|$($f_s/2$ 以下になるような $k$)の周波数に「折り返され」て見えます。

たとえば、$f_s = 8\,\text{kHz}$ のとき、$5\,\text{kHz}$ の正弦波は $8 – 5 = 3\,\text{kHz}$ の正弦波として観測されます。

sinc関数による完全復元

理想的なD/A変換

サンプリング定理の条件が満たされるとき、元の連続信号は以下のsinc補間で完全に復元できます:

$$ \boxed{x(t) = \sum_{n=-\infty}^{\infty} x(nT_s) \, \text{sinc}\left(\frac{t – nT_s}{T_s}\right)} $$

ここで sinc 関数は:

$$ \text{sinc}(u) = \frac{\sin(\pi u)}{\pi u} $$

導出

理想低域通過フィルタの伝達関数は:

$$ H(f) = \begin{cases} T_s & (|f| \leq f_s/2) \\ 0 & (|f| > f_s/2) \end{cases} $$

この逆フーリエ変換がインパルス応答 $h(t)$ です:

$$ h(t) = \int_{-f_s/2}^{f_s/2} T_s \, e^{j2\pi ft} \, df = T_s \cdot f_s \cdot \text{sinc}(f_s t) = \text{sinc}\left(\frac{t}{T_s}\right) $$

復元信号は:

$$ x(t) = x_s(t) * h(t) = \sum_{n=-\infty}^{\infty} x(nT_s) \, \text{sinc}\left(\frac{t – nT_s}{T_s}\right) $$

sinc補間の性質

sinc 関数は $u = 0$ で $\text{sinc}(0) = 1$、$u$ が0でない整数のとき $\text{sinc}(u) = 0$ です。したがって:

$$ x(mT_s) = \sum_{n=-\infty}^{\infty} x(nT_s) \, \text{sinc}(m – n) = x(mT_s) $$

サンプル点上では完全にサンプル値を再現し、サンプル点間は滑らかに補間されます。

Pythonでの可視化

import numpy as np
import matplotlib.pyplot as plt

# --- パラメータ ---
f_signal = 5       # 信号周波数 [Hz]
duration = 1.0     # 信号長 [s]
t_fine = np.linspace(0, duration, 10000)  # 連続時間の近似
x_original = np.sin(2 * np.pi * f_signal * t_fine)

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# (1) 十分なサンプリング vs 不十分なサンプリング
for fs, color, label in [(40, 'blue', '$f_s=40$ Hz (OK)'),
                          (8, 'red', '$f_s=8$ Hz (aliasing)')]:
    Ts = 1.0 / fs
    n_samples = int(duration * fs)
    t_samples = np.arange(n_samples) * Ts
    x_samples = np.sin(2 * np.pi * f_signal * t_samples)
    axes[0, 0].stem(t_samples, x_samples, linefmt=f'{color[0]}-',
                     markerfmt=f'{color[0]}o', basefmt='gray', label=label)

axes[0, 0].plot(t_fine, x_original, 'k-', lw=1, alpha=0.5, label='Original')
axes[0, 0].set_xlabel('Time [s]')
axes[0, 0].set_ylabel('$x(t)$')
axes[0, 0].set_title(f'Sampling of {f_signal} Hz sine wave')
axes[0, 0].legend(fontsize=9)
axes[0, 0].grid(True, alpha=0.3)

# (2) エイリアシングの可視化
fs_alias = 8
Ts_alias = 1.0 / fs_alias
n_alias = int(duration * fs_alias)
t_alias = np.arange(n_alias) * Ts_alias
x_alias = np.sin(2 * np.pi * f_signal * t_alias)
f_aliased = fs_alias - f_signal  # 折り返し周波数
x_aliased_continuous = np.sin(2 * np.pi * f_aliased * t_fine)

axes[0, 1].plot(t_fine, x_original, 'b-', lw=1.5, alpha=0.5, label=f'Original {f_signal} Hz')
axes[0, 1].plot(t_fine, x_aliased_continuous, 'r--', lw=1.5,
                label=f'Aliased {f_aliased} Hz')
axes[0, 1].stem(t_alias, x_alias, linefmt='k-', markerfmt='ko', basefmt='gray',
                label=f'Samples ($f_s$={fs_alias} Hz)')
axes[0, 1].set_xlabel('Time [s]')
axes[0, 1].set_ylabel('$x(t)$')
axes[0, 1].set_title('Aliasing demonstration')
axes[0, 1].legend(fontsize=9)
axes[0, 1].grid(True, alpha=0.3)

# (3) スペクトルの繰り返し(周波数領域)
fs_good = 40
freqs = np.linspace(-60, 60, 2000)
# 元の信号スペクトル(デルタ関数の近似)
sigma = 0.3
X_orig = np.exp(-0.5 * ((freqs - f_signal) / sigma)**2) + \
         np.exp(-0.5 * ((freqs + f_signal) / sigma)**2)

for k in [-2, -1, 0, 1, 2]:
    X_copy = np.exp(-0.5 * ((freqs - f_signal - k*fs_good) / sigma)**2) + \
             np.exp(-0.5 * ((freqs + f_signal - k*fs_good) / sigma)**2)
    style = 'b-' if k == 0 else 'b--'
    alpha = 1.0 if k == 0 else 0.3
    axes[1, 0].plot(freqs, X_copy, style, lw=1.5, alpha=alpha,
                    label=f'$k={k}$' if abs(k) <= 1 else None)

axes[1, 0].axvline(fs_good/2, color='r', ls=':', lw=1.5, label='$f_N$')
axes[1, 0].axvline(-fs_good/2, color='r', ls=':', lw=1.5)
axes[1, 0].set_xlabel('Frequency [Hz]')
axes[1, 0].set_ylabel('$|X_s(f)|$')
axes[1, 0].set_title(f'Spectrum copies ($f_s$={fs_good} Hz, no overlap)')
axes[1, 0].legend(fontsize=9)
axes[1, 0].grid(True, alpha=0.3)
axes[1, 0].set_xlim(-60, 60)

# (4) sinc 補間による復元
fs_sinc = 20
Ts_sinc = 1.0 / fs_sinc
n_sinc = int(duration * fs_sinc)
t_sinc_samples = np.arange(n_sinc) * Ts_sinc
x_sinc_samples = np.sin(2 * np.pi * f_signal * t_sinc_samples)

# sinc補間
x_reconstructed = np.zeros_like(t_fine)
for n_idx in range(n_sinc):
    x_reconstructed += x_sinc_samples[n_idx] * np.sinc((t_fine - n_idx * Ts_sinc) / Ts_sinc)

axes[1, 1].plot(t_fine, x_original, 'b-', lw=2, alpha=0.5, label='Original')
axes[1, 1].plot(t_fine, x_reconstructed, 'r--', lw=1.5, label='Sinc interpolation')
axes[1, 1].stem(t_sinc_samples, x_sinc_samples, linefmt='k-', markerfmt='ko',
                basefmt='gray', label=f'Samples ($f_s$={fs_sinc} Hz)')
axes[1, 1].set_xlabel('Time [s]')
axes[1, 1].set_ylabel('$x(t)$')
axes[1, 1].set_title('Signal reconstruction via sinc interpolation')
axes[1, 1].legend(fontsize=9)
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

可視化から、以下のことが確認できます:

  • $f_s > 2f_{\max}$ のとき、サンプル点は元の波形を正しく捉える
  • $f_s \leq 2f_{\max}$ のとき、高い周波数の信号がより低い周波数に折り返されて見える(エイリアシング)
  • sinc補間により、十分なサンプリングレートで取得したサンプルから元の連続信号が高精度で復元される

まとめ

本記事では、ナイキスト・シャノンのサンプリング定理について解説しました。

  • サンプリング定理: $f_s > 2f_{\max}$ で完全復元が可能
  • ナイキスト周波数: $f_N = f_s / 2$、これを超える周波数は正しく取得できない
  • エイリアシング: $f_s \leq 2f_{\max}$ で高周波成分が低周波に折り返される
  • sinc補間: $x(t) = \sum_n x(nT_s) \, \text{sinc}((t – nT_s)/T_s)$ で完全復元
  • 実用上はアンチエイリアシングフィルタで事前に帯域制限する

次のステップとして、以下の記事も参考にしてください。