メジアンフィルタとは
メジアンフィルタ(Median Filter)は、窓内のデータをソートし、その中央値(メジアン)を出力値とする非線形フィルタです。
EMAフィルタや移動平均フィルタなどの線形フィルタとは根本的に異なり、インパルスノイズ(スパイク状のノイズ)を極めて効果的に除去できることが最大の特徴です。
主な用途
- インパルスノイズ除去: センサーの一時的な誤検知、通信エラーによるビット反転
- 画像処理: 塩胡椒ノイズ(Salt-and-Pepper Noise)の除去
- 医療信号処理: ECG(心電図)のアーチファクト除去
- 金融データ: 株価データの外れ値除去(スポット誤値)
数学的定義
窓幅 \(2m+1\)(\(m\) は片側のデータ点数)のメジアンフィルタは、時刻 \(n\) における出力 \(y[n]\) を以下のように定義します:
\[ y[n] = \text{median}\bigl(x[n-m],\, x[n-m+1],\, \dots,\, x[n],\, \dots,\, x[n+m]\bigr) \tag{1} \]ここで \(\text{median}(\cdot)\) は集合の中央値を表します。\(2m+1\) 個の値を昇順または降順に並べたとき、中央(\(m+1\) 番目)の値が出力となります。
例(窓幅 \(=5\), \(m=2\)):
入力列の一部が \([\ldots, 3, 4, \mathbf{100}, 4, 3, \ldots]\) のとき(太字はスパイク):
\[ \text{sort}(3, 4, 100, 4, 3) = (3, 3, \mathbf{4}, 4, 100) \Rightarrow y[n] = 4 \tag{2} \]スパイク値 \(100\) は完全に除去され、本来の信号値 \(4\) が出力されます。
線形フィルタとの本質的な違い
重ね合わせ原理が成立しない
線形フィルタでは入力の加重和として出力が計算されます。しかしメジアン演算はソートを伴う非線形演算であるため、重ね合わせ原理(superposition principle)が成立しません:
\[ \text{median}(ax_1 + bx_2) \neq a\cdot\text{median}(x_1) + b\cdot\text{median}(x_2) \tag{3} \]この非線形性こそが、インパルスノイズ除去における線形フィルタとの決定的な差を生みます。
インパルスノイズへの耐性
線形フィルタはインパルスノイズを周囲の多くのサンプルに「拡散」させてしまいます。一方、メジアンフィルタはインパルスの影響を窓内に閉じ込め、ソート操作によって完全に排除します。
| フィルタ | ガウシアンノイズ除去 | インパルスノイズ除去 | 信号のエッジ保存 |
|---|---|---|---|
| 移動平均 | 良好 | 不十分(拡散) | 低い |
| EMA | 良好(重み付き) | 不十分(拡散) | 低い |
| サビツキー・ゴーレイ | 良好 | 不十分 | 高い |
| メジアン | 普通 | 非常に優れる | 高い(保存しやすい) |
Python実装
手動実装
import numpy as np
def median_filter_manual(x: np.ndarray, window_size: int) -> np.ndarray:
"""メジアンフィルタの手動実装(エッジは折り返しパディング)"""
assert window_size % 2 == 1, "window_size は奇数を指定してください"
m = window_size // 2
n = len(x)
y = np.empty(n)
# reflect パディングでエッジを処理
x_padded = np.pad(x, m, mode="reflect")
for i in range(n):
window = x_padded[i : i + window_size]
y[i] = np.median(window)
return y
scipy.signal.medfilt を使った実装
実用上は scipy.signal.medfilt を使うのが最も簡単です。
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
# --- 信号生成 ---
np.random.seed(42)
t = np.linspace(0, 1, 500)
# 正弦波 + ガウシアンノイズ
x_clean = np.sin(2 * np.pi * 5 * t)
noise_gaussian = 0.2 * np.rng.standard_normal(len(t))
x_noisy = x_clean + noise_gaussian
# インパルスノイズを追加(ランダムな位置に大きなスパイク)
rng = np.random.default_rng(0)
impulse_indices = rng.choice(len(t), size=30, replace=False)
x_impulse = x_noisy.copy()
x_impulse[impulse_indices] += rng.choice([-4.0, 4.0], size=30)
# --- フィルタ適用 ---
window_size = 11
# メジアンフィルタ
y_median = signal.medfilt(x_impulse, kernel_size=window_size)
# 比較用:移動平均フィルタ
y_ma = np.convolve(x_impulse, np.ones(window_size) / window_size, mode="same")
# --- プロット ---
fig, axes = plt.subplots(3, 1, figsize=(12, 9), sharex=True)
axes[0].plot(t, x_impulse, color="gray", alpha=0.7, label="インパルスノイズ入り信号")
axes[0].plot(t, x_clean, color="blue", linestyle="--", label="原信号", linewidth=2)
axes[0].set_ylabel("振幅")
axes[0].legend()
axes[0].set_title("入力信号(インパルスノイズ混入)")
axes[1].plot(t, y_median, color="red", label=f"メジアンフィルタ (窓={window_size})")
axes[1].plot(t, x_clean, color="blue", linestyle="--", label="原信号", linewidth=2)
axes[1].set_ylabel("振幅")
axes[1].legend()
axes[1].set_title("メジアンフィルタ出力")
axes[2].plot(t, y_ma, color="green", label=f"移動平均フィルタ (窓={window_size})")
axes[2].plot(t, x_clean, color="blue", linestyle="--", label="原信号", linewidth=2)
axes[2].set_ylabel("振幅")
axes[2].set_xlabel("時間 [s]")
axes[2].legend()
axes[2].set_title("移動平均フィルタ出力(比較)")
plt.tight_layout()
plt.show()
窓幅の選択
窓幅 \(W = 2m+1\) はメジアンフィルタの特性を決定する唯一のパラメータです。
import numpy as np
from scipy import signal
import matplotlib.pyplot as plt
np.random.seed(0)
t = np.linspace(0, 1, 300)
x_clean = np.sin(2 * np.pi * 3 * t)
x_noisy = x_clean.copy()
# 10%の点にインパルスノイズ
idx = np.random.choice(len(t), size=30, replace=False)
x_noisy[idx] += np.random.choice([-3, 3], size=30)
window_sizes = [3, 7, 15, 31]
fig, axes = plt.subplots(len(window_sizes), 1, figsize=(12, 10), sharex=True)
for ax, ws in zip(axes, window_sizes):
y = signal.medfilt(x_noisy, kernel_size=ws)
mse = np.mean((y - x_clean) ** 2)
ax.plot(t, x_noisy, color="gray", alpha=0.5, label="入力(ノイズ混入)")
ax.plot(t, y, color="red", label=f"窓幅={ws}, MSE={mse:.4f}")
ax.plot(t, x_clean, color="blue", linestyle="--", label="原信号", linewidth=1.5)
ax.legend(loc="upper right")
ax.set_ylabel("振幅")
axes[-1].set_xlabel("時間 [s]")
plt.suptitle("窓幅によるメジアンフィルタの違い")
plt.tight_layout()
plt.show()
選択の指針:
- 小さな窓幅(3〜5): 細かいスパイクを除去、信号の詳細を保持。ノイズが少ない場合向き。
- 大きな窓幅(11〜31): より強いノイズ除去。ただし信号のエッジや鋭いピークが鈍化する。
- ノイズが連続している場合: 窓幅はノイズの連続長より大きくする必要があります。
ガウシアンノイズへの対応:EMAとの組み合わせ
メジアンフィルタはインパルスノイズに強い一方、ガウシアンノイズの除去はEMAや移動平均に劣ります。実用的なパイプラインでは、両フィルタを組み合わせます:
from scipy import signal
import numpy as np
def hybrid_filter(x: np.ndarray, median_window: int = 5, ema_alpha: float = 0.2) -> np.ndarray:
"""
メジアン → EMA の2段フィルタ
Step1: メジアンフィルタでインパルスノイズを除去
Step2: EMAでガウシアンノイズをさらに平滑化
"""
# Step 1: インパルスノイズ除去
y_median = signal.medfilt(x, kernel_size=median_window)
# Step 2: EMAで残留ガウシアンノイズを平滑化
y_ema = np.zeros_like(y_median)
y_ema[0] = y_median[0]
for i in range(1, len(y_median)):
y_ema[i] = ema_alpha * y_median[i] + (1 - ema_alpha) * y_ema[i - 1]
return y_ema
この2段構成は、センサーデータ処理や通信システムのデノイジングで広く採用されています。
計算量と実装上の注意
計算量
単純な実装では、各出力サンプルにつき \(O(W \log W)\)(ソートのコスト)かかるため、全体は \(O(N W \log W)\) です。
しかし、scipy.signal.medfilt は内部でヒストグラムベースの高速アルゴリズムを使用しており、実用上は十分高速です。大規模データには scipy.ndimage.median_filter の方が効率的な場合があります。
from scipy import ndimage
# scipy.ndimage.median_filter(1D信号にも使用可能)
y = ndimage.median_filter(x, size=11)
エッジ処理
scipy.signal.medfilt はデフォルトでゼロパディングを使用します。エッジ処理を制御したい場合は scipy.ndimage.median_filter の mode パラメータを使用します:
# reflect: 折り返しパディング(通常推奨)
y_reflect = ndimage.median_filter(x, size=11, mode="reflect")
# nearest: 端点の値でパディング
y_nearest = ndimage.median_filter(x, size=11, mode="nearest")
まとめ
| 特性 | メジアンフィルタ |
|---|---|
| フィルタ種別 | 非線形 |
| インパルスノイズ除去 | 非常に優秀 |
| ガウシアンノイズ除去 | 普通(線形フィルタに劣る) |
| エッジ保存性 | 高い |
| パラメータ数 | 1(窓幅 \(W\)) |
| 計算量 | \(O(NW \log W)\) |
| 因果性(リアルタイム対応) | 半因果(窓を前後にとる場合は非因果) |
メジアンフィルタはインパルスノイズが支配的な環境で最も力を発揮します。ガウシアンノイズが主体であればEMAフィルタやバターワースフィルタ、信号のピーク形状を保存したい場合はサビツキー・ゴーレイフィルタ、環境が時変な場合は適応フィルタを検討してください。
関連記事
- 指数移動平均(EMA)フィルタの周波数特性 - 線形フィルタの代表例。メジアンフィルタとの特性比較に役立ちます。
- 移動平均フィルタの種類と比較 - SMA・WMA・EMAの比較。メジアンフィルタとの違いを理解する出発点。
- サビツキー・ゴーレイフィルタの理論とPython実装 - 多項式フィッティングによる平滑化フィルタ。信号形状の保存性で共通点がある。
- 適応フィルタ(LMS/RLS)の理論とPython実装 - 時変環境向けの適応型フィルタ。メジアンフィルタの固定窓幅の制限を克服します。
- バターワースフィルタの設計とPython実装 - 急峻な遮断特性が必要な場合の線形フィルタ選択肢。
- 時系列データの異常検知:統計的手法からカルマンフィルタまで - メジアンフィルタを前処理として組み合わせることで異常検知精度が向上します。
- ノッチフィルタの設計とPython実装 - 特定周波数の定常ノイズ除去。インパルスノイズとは性質の異なるノイズへの対応。
- 信号処理におけるフィルタリング手法の基礎 - フィルタ全体の体系的な位置づけを確認できます。
参考
- Tukey, J. W. (1977). Exploratory Data Analysis. Addison-Wesley.
- Pitas, I., & Venetsanopoulos, A. N. (1990). Nonlinear Digital Filters: Principles and Applications. Springer.
- scipy.signal.medfilt documentation
- scipy.ndimage.median_filter documentation
関連ツール
- DevToolBox - 開発者向け無料ツール集 - JSON整形、正規表現テスターなど85種類以上の開発者向けツール
- CalcBox - 暮らしの計算ツール - 統計計算、複利計算など61種類以上の計算ツール