はじめに
デジタルカメラで撮影した写真をJPEGで保存するとき、スマートフォンでMP3を再生するとき、あるいはH.264でエンコードされた動画を視聴するとき——これらすべての背後に**離散コサイン変換(DCT: Discrete Cosine Transform)**が動いています。
DCTは1974年にNasir AhmedらによってIEEE Transactions on Computersに発表された変換であり、信号をコサイン基底関数の重みつき和として表現します。DFT(離散フーリエ変換)と密接に関係しながらも、実数演算のみで完結し、エネルギー集中性に優れるという特性から、データ圧縮の世界で事実上の標準となっています。
本記事では、DCTの数学的定義からエネルギー集中性の原理、FFTを利用した高速計算法、そしてJPEG圧縮・MP3音声符号化への応用まで、Pythonコードを交えて体系的に解説します。
DCT-IIの定義
DCTにはDCT-I〜DCT-IVまでの変種がありますが、データ圧縮で「DCT」と呼ぶ場合は通常DCT-IIを指します。
長さ \(N\) の実数列 \(x[n]\) (\(n = 0, 1, \ldots, N-1\) )に対して、DCT-IIは次のように定義されます。
\[X[k] = \sum_{n=0}^{N-1} x[n] \cos\left(\frac{\pi k (2n+1)}{2N}\right), \quad k = 0, 1, \ldots, N-1 \tag{1}\]コサイン引数 \(\frac{\pi k (2n+1)}{2N}\) の形状が重要です。半整数シフト \((2n+1)/2\) により、各基底関数は \(n = -0.5\) と \(n = N - 0.5\) で偶対称となり、これがエネルギー集中性の源泉になります(後述)。
DFTとの比較
DFTは複素指数関数 \(e^{-j2\pi kn/N}\) を基底とします。
\[X_{\text{DFT}}[k] = \sum_{n=0}^{N-1} x[n]\, e^{-j2\pi kn/N} \tag{2}\]DCTとDFTの主な違いを以下に整理します。
| 特性 | DCT-II | DFT |
|---|---|---|
| 基底関数 | コサイン(実数) | 複素指数 |
| 出力 | 実数 | 複素数 |
| 対称性 | 偶対称拡張 | 周期拡張 |
| エネルギー集中性 | 高い | 中程度 |
| ギブス現象 | 抑制 | 発生しやすい |
| 主な応用 | JPEG, MP3, H.264 | スペクトル解析, 通信 |
正規化形式
実用上は直交正規化形式が用いられます。
\[X[k] = w(k) \sum_{n=0}^{N-1} x[n] \cos\left(\frac{\pi k (2n+1)}{2N}\right) \tag{3}\] \[w(k) = \begin{cases} \sqrt{1/N} & (k = 0) \\ \sqrt{2/N} & (k \geq 1) \end{cases} \tag{4}\]この正規化のもとでパーセバルの等式が成立します。
\[\sum_{n=0}^{N-1} x[n]^2 = \sum_{k=0}^{N-1} X[k]^2 \tag{5}\]時間領域の総エネルギーと変換領域の総エネルギーが等しくなり、可逆変換であることが保証されます。
逆DCT(DCT-III)
DCT-IIの逆変換はDCT-IIIです。正規化形式では、DCT-IIIはDCT-IIの転置行列と一致します。
\[x[n] = \sum_{k=0}^{N-1} w(k)\, X[k] \cos\left(\frac{\pi k (2n+1)}{2N}\right) \tag{6}\]変換行列を \(\mathbf{C}\) とすると \(\mathbf{C}\mathbf{C}^T = \mathbf{I}\) が成り立ち、これが逆変換の一意性を保証します。
エネルギー集中性
DCTが圧縮に強い最大の理由は**エネルギー集中性(Energy Compaction)**です。自然信号(画像・音声など)のDCT係数は、低次数(低周波)の少数の係数に大半のエネルギーが集中します。
ギブス現象との比較
DFTは信号の両端を暗黙的に周期接続します。信号の先端と末端に不連続がある場合(矩形波など)、スペクトルに高周波成分が現れるギブス現象が発生し、エネルギーが広い周波数帯域に分散します。
DCT-IIは、信号 \(x[n]\) を境界で偶対称に拡張(\(\tilde{x}[-n-1] = x[n]\) )してからDFTを適用するのと等価です(次節参照)。この偶拡張により、境界での不連続がなくなり、ギブス現象が抑制されます。その結果、エネルギーが低周波に集中します。
エネルギー集中度の定量的比較
import numpy as np
import matplotlib.pyplot as plt
from scipy.fft import dct, idct
# 典型的な自然信号(徐々に変化する成分 + 小さなノイズ)
np.random.seed(42)
N = 64
n = np.arange(N)
x = (np.cos(2 * np.pi * 2 * n / N)
+ 0.5 * np.cos(2 * np.pi * 5 * n / N)
+ 0.1 * np.random.randn(N))
# DCT-II(正規化あり)
X_dct = dct(x, type=2, norm='ortho')
# DFT(片側スペクトル)
X_dft = np.fft.rfft(x)
energy_dft = np.abs(X_dft) ** 2
energy_dct = X_dct ** 2
# 上位k個の係数で占めるエネルギー累積分布
energy_dct_sorted = np.sort(energy_dct)[::-1]
energy_dft_sorted = np.sort(energy_dft)[::-1]
cumsum_dct = np.cumsum(energy_dct_sorted) / np.sum(energy_dct_sorted)
cumsum_dft = np.cumsum(energy_dft_sorted) / np.sum(energy_dft_sorted)
# プロット
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
axes[0].bar(range(N), energy_dct / energy_dct.sum(),
alpha=0.7, label='DCT', color='steelblue')
axes[0].bar(range(len(energy_dft)), energy_dft / energy_dft.sum(),
alpha=0.5, label='DFT', color='tomato')
axes[0].set_xlabel('Coefficient index')
axes[0].set_ylabel('Normalized energy')
axes[0].set_title('Energy Distribution: DCT vs DFT')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
axes[1].plot(cumsum_dct, label='DCT', color='steelblue')
axes[1].plot(cumsum_dft, label='DFT', color='tomato')
axes[1].axhline(0.95, color='gray', linestyle='--', label='95% threshold')
axes[1].set_xlabel('Number of coefficients (sorted by energy)')
axes[1].set_ylabel('Cumulative energy fraction')
axes[1].set_title('Cumulative Energy Compaction')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# 95%エネルギーに必要な係数数
n95_dct = np.searchsorted(cumsum_dct, 0.95) + 1
n95_dft = np.searchsorted(cumsum_dft, 0.95) + 1
print(f"95%エネルギーに必要な係数数 — DCT: {n95_dct}, DFT: {n95_dft}")
実行すると、DCTでは少数の係数で95%のエネルギーを表現できるのに対し、DFTでは多くの係数が必要であることが定量的に確認できます。
DCTとDFTの関係:偶拡張トリック
DCT-IIとDFTの数学的関係を導きます。
長さ \(N\) の信号 \(x[n]\) に対して、長さ \(2N\) の偶拡張信号 \(\tilde{x}[n]\) を構成します。
\[\tilde{x}[n] = \begin{cases} x[n] & 0 \leq n \leq N-1 \\ x[2N-1-n] & N \leq n \leq 2N-1 \end{cases} \tag{7}\]この \(\tilde{x}[n]\) の長さ \(2N\) のDFTを \(\tilde{X}[k]\) とすると、次の関係が成立します。
\[X_{\text{DCT-II}}[k] = \text{Re}\left(e^{-j\pi k/(2N)}\, \tilde{X}[k]\right), \quad k = 0, 1, \ldots, N-1 \tag{8}\]証明の概略: \(\tilde{X}[k] = \sum_{n=0}^{2N-1} \tilde{x}[n] e^{-j\pi kn/N}\) に偶拡張の定義を代入し、\(x[n]\) と \(x[2N-1-n]\) の項をまとめると、\(e^{j\pi k/(2N)}\) の因子が現れ、実部をとることでDCT-IIの定義式と一致します。
この式は、DCT-IIの計算はFFTを用いて \(O(N \log N)\) で実行できることを意味します。位相補正因子 \(e^{-j\pi k/(2N)}\) の乗算は \(O(N)\) で済みます。
高速DCT計算アルゴリズム
FFTを利用した高速DCT
式 \((8)\) を直接実装した高速DCTアルゴリズムは次のとおりです。
import numpy as np
from scipy.fft import dct
def fast_dct2(x):
"""FFTを利用したDCT-II実装(O(N log N))"""
N = len(x)
# ステップ1: 偶拡張(長さ2Nの信号を構成)
x_ext = np.concatenate([x, x[::-1]])
# ステップ2: FFT(長さ2N)
X_fft = np.fft.fft(x_ext)
# ステップ3: 位相補正と実部の取得
k = np.arange(N)
phase = np.exp(-1j * np.pi * k / (2 * N))
X_dct = np.real(phase * X_fft[:N])
return X_dct
# 検証:SciPyのdctと比較
x = np.random.randn(256)
X_fast = fast_dct2(x)
X_scipy = dct(x, type=2, norm=None)
print(f"最大誤差: {np.max(np.abs(X_fast - X_scipy)):.2e}")
# 出力例: 最大誤差: 2.13e-12
計算量の比較
さらなる高速化として、偶拡張後のFFTの対称性を利用した最適化があります。元の信号を偶数インデックス \(y[n] = x[2n]\) と奇数インデックス \(z[n] = x[2n+1]\) に分割し、長さ \(N/2\) の2本のFFTとして実行できます(Chen et al., 1977)。
| 手法 | 演算量 |
|---|---|
| 素朴なDCT(定義通り) | \(O(N^2)\) |
| 偶拡張 + FFT | \(O(N \log N)\) |
| Chen (1977) 最適化版 | \(\approx \frac{3}{2} N \log_2 N\) 乗算 |
実用的には scipy.fft.dct を使用すれば、内部でこれらの最適化が適用されます。
2次元DCT:画像処理への応用
2D DCT-IIの定義と分離可能性
\(M \times N\) の画像ブロック \(x[m, n]\) に対する2次元DCT-IIは次の式で定義されます。
\[X[u, v] = w(u)\, w(v) \sum_{m=0}^{M-1} \sum_{n=0}^{N-1} x[m,n] \cos\left(\frac{\pi u (2m+1)}{2M}\right) \cos\left(\frac{\pi v (2n+1)}{2N}\right) \tag{9}\]2D DCTは分離可能であり、まず各行に1D DCTを適用し、次に各列に1D DCTを適用することで計算できます。計算量は \(O(MN \log(MN))\) です。
8×8 DCT基底関数の視覚化
import numpy as np
import matplotlib.pyplot as plt
from scipy.fft import dct, idct
def dct2d(block):
"""2次元DCT-II(行→列の順に適用)"""
return dct(dct(block, axis=0, norm='ortho'), axis=1, norm='ortho')
def idct2d(block):
"""2次元逆DCT"""
return idct(idct(block, axis=0, norm='ortho'), axis=1, norm='ortho')
# 8x8 DCT基底関数の可視化
fig, axes = plt.subplots(8, 8, figsize=(10, 10))
for u in range(8):
for v in range(8):
basis = np.zeros((8, 8))
basis[u, v] = 1.0
basis_img = idct2d(basis)
axes[u, v].imshow(basis_img, cmap='RdBu', vmin=-0.5, vmax=0.5)
axes[u, v].axis('off')
plt.suptitle('8×8 DCT Basis Functions (row=u, col=v)', fontsize=14)
plt.tight_layout()
plt.show()
左上(\(u=0, v=0\) )の基底は直流成分(全体の平均輝度)を表し、右下ほど高周波の格子模様になります。自然画像ではほとんどのエネルギーが左上の係数に集中するため、右下の係数は小さく、量子化でゼロに丸めることができます。
JPEG圧縮でのDCT
JPEGの圧縮手順
JPEG(Joint Photographic Experts Group)は1992年に標準化された静止画像圧縮フォーマットです。
圧縮手順の概略:
- 色空間変換: RGBからYCbCrへ変換(輝度・色差分離)
- クロマサブサンプリング: 人間の目が色差の細部に鈍感であることを利用
- 8×8ブロック分割: 画像を8×8ピクセルのブロックに分割
- レベルシフト: 各ピクセル値から128を引いて \([-128, 127]\) の範囲にシフト
- 2D DCT: 各ブロックに8×8 DCT-IIを適用
- 量子化: DCT係数を量子化テーブルで割り、整数に丸める(非可逆圧縮の本質)
- エントロピー符号化: ジグザグスキャン後にハフマン符号化
量子化ステップは次式で表されます。
\[\hat{X}[u, v] = \text{round}\left(\frac{X[u, v]}{Q[u, v]}\right) \tag{10}\]量子化テーブル \(Q[u, v]\) は低周波係数を細かく、高周波係数を粗く量子化します。これにより、視覚的に重要な低周波成分を保持しながら、高周波の細部を積極的に切り捨てます。
JPEGライク圧縮のPython実装
import numpy as np
import matplotlib.pyplot as plt
from scipy.fft import dct, idct
# JPEG標準輝度量子化テーブル(品質50相当)
QUANT_TABLE = np.array([
[16, 11, 10, 16, 24, 40, 51, 61],
[12, 12, 14, 19, 26, 58, 60, 55],
[14, 13, 16, 24, 40, 57, 69, 56],
[14, 17, 22, 29, 51, 87, 80, 62],
[18, 22, 37, 56, 68, 109, 103, 77],
[24, 35, 55, 64, 81, 104, 113, 92],
[49, 64, 78, 87, 103, 121, 120, 101],
[72, 92, 95, 98, 112, 100, 103, 99],
], dtype=float)
def dct2d(block):
return dct(dct(block, axis=0, norm='ortho'), axis=1, norm='ortho')
def idct2d(block):
return idct(idct(block, axis=0, norm='ortho'), axis=1, norm='ortho')
def jpeg_compress_block(block, quality=50):
"""8x8ブロックをJPEGライク圧縮"""
if quality < 50:
scale = 5000 / quality
else:
scale = 200 - 2 * quality
q_table = np.clip(np.floor((QUANT_TABLE * scale + 50) / 100), 1, 255)
shifted = block.astype(float) - 128
coeffs = dct2d(shifted)
quantized = np.round(coeffs / q_table)
return quantized, q_table
def jpeg_decompress_block(quantized, q_table):
"""8x8ブロックを復元"""
dequantized = quantized * q_table
restored = idct2d(dequantized) + 128
return np.clip(restored, 0, 255).astype(np.uint8)
# テスト画像(輝度グラデーション)の生成
np.random.seed(0)
img_size = 64
img = np.zeros((img_size, img_size), dtype=np.uint8)
for i in range(img_size):
img[i, :] = np.clip(128 + 80 * np.sin(2 * np.pi * i / img_size), 0, 255)
img = (img.astype(int) + np.random.randint(0, 20, img.shape)).clip(0, 255).astype(np.uint8)
# 品質ごとの圧縮・復元
fig, axes = plt.subplots(1, 4, figsize=(14, 4))
qualities = [90, 50, 20, 5]
for ax, quality in zip(axes, qualities):
restored_img = np.zeros_like(img, dtype=float)
total_nonzero = 0
total_coeffs = 0
for row in range(0, img_size, 8):
for col in range(0, img_size, 8):
block = img[row:row+8, col:col+8].astype(float)
quantized, q_table = jpeg_compress_block(block, quality)
restored_block = jpeg_decompress_block(quantized, q_table)
restored_img[row:row+8, col:col+8] = restored_block
total_nonzero += np.count_nonzero(quantized)
total_coeffs += quantized.size
psnr_val = 10 * np.log10(255**2 / np.mean((img.astype(float) - restored_img)**2))
ratio = total_nonzero / total_coeffs * 100
ax.imshow(restored_img, cmap='gray', vmin=0, vmax=255)
ax.set_title(f'Quality={quality}\nPSNR={psnr_val:.1f}dB\n非ゼロ={ratio:.1f}%')
ax.axis('off')
plt.suptitle('JPEG-like Compression at Different Quality Levels', fontsize=12)
plt.tight_layout()
plt.show()
品質を下げるほどPSNRが低下し、非ゼロ係数の割合が減少(=圧縮率向上)します。JPEG品質90では非ゼロ係数が多く高品質を維持し、品質5では大半がゼロとなり高い圧縮率を達成します。
なぜDCTが画像圧縮に適しているか
自然画像の統計的性質がDCTに適している理由は次の2点です。
- 1/f特性: 自然画像のパワースペクトルは低周波ほど大きく(\(P(f) \propto 1/f^2\) )、この性質がDCT係数の低次集中と対応します。
- マルコフ性とKLT近似: 隣接ピクセル間の相関が強く、1次マルコフモデルで近似できる場合、DCT変換行列はKLT(Karhunen-Loève変換)行列に漸近します。KLTは任意の相関行列を完全に非相関化する理想的な変換であり、DCTはその優れた近似です。
SciPyによるPython実装:DCTの圧縮効果デモ
import numpy as np
import matplotlib.pyplot as plt
from scipy.fft import dct, idct
def dct_compress(x, keep_ratio):
"""DCT係数の上位 keep_ratio 割合を保持して再構成"""
X = dct(x, type=2, norm='ortho')
N = len(X)
k = max(1, int(N * keep_ratio))
# エネルギーの大きい係数のみ保持
idx = np.argsort(np.abs(X))[::-1]
X_sparse = np.zeros_like(X)
X_sparse[idx[:k]] = X[idx[:k]]
x_rec = idct(X_sparse, type=2, norm='ortho')
mse = np.mean((x - x_rec) ** 2)
snr = 10 * np.log10(np.mean(x**2) / (mse + 1e-12))
return x_rec, snr
# テスト信号
N = 256
n = np.arange(N)
x = (np.cos(2 * np.pi * 10 * n / N)
+ 0.5 * np.cos(2 * np.pi * 30 * n / N)
+ 0.3 * np.random.randn(N))
ratios = [0.5, 0.2, 0.1, 0.05]
fig, axes = plt.subplots(len(ratios) + 1, 1, figsize=(12, 12), sharex=True)
axes[0].plot(n, x, color='steelblue', label='Original')
axes[0].set_title('Original Signal')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
for ax, ratio in zip(axes[1:], ratios):
x_rec, snr = dct_compress(x, ratio)
ax.plot(n, x, color='steelblue', alpha=0.4, label='Original')
ax.plot(n, x_rec, color='tomato',
label=f'Reconstructed ({ratio*100:.0f}% coeffs, SNR={snr:.1f}dB)')
ax.set_title(f'{ratio*100:.0f}% coefficients — SNR: {snr:.1f} dB')
ax.legend()
ax.grid(True, alpha=0.3)
axes[-1].set_xlabel('Sample index')
plt.tight_layout()
plt.show()
係数の10%だけを保持しても波形を忠実に再現できることが確認できます。これがDCTベース圧縮の威力です。
音声圧縮におけるMDCT
MP3やAACなどの音声符号化では、DCT-IIの変形である**MDCT(Modified Discrete Cosine Transform、変形離散コサイン変換)**が使われます。
MDCTの定義
MDCTは長さ \(2N\) の入力から \(N\) 個の係数を生成します。
\[X[k] = \sum_{n=0}^{2N-1} x[n] \cos\left(\frac{\pi}{N}\left(n + \frac{1}{2} + \frac{N}{2}\right)\left(k + \frac{1}{2}\right)\right), \quad k = 0, \ldots, N-1 \tag{11}\]MDCTの重要な性質:
- 50%オーバーラップ: 連続するフレームが半分ずつ重なる
- TDAC(Time-Domain Aliasing Cancellation): フレーム間のエイリアシングが相殺され、完全再構成が可能
- 臨界サンプリング: \(2N\) サンプルから \(N\) 係数を生成(50%削減)
MP3では、サブバンドフィルタリングと組み合わせてMDCTを使用し、さらに心理音響モデル(人間の聴覚の感度が周波数により異なることを利用)で不要な係数を積極的に量子化します。
ケプストラム分析で扱う対数スペクトルのDCT(メル周波数ケプストラム係数: MFCC)も音声認識の特徴量として広く使われています。
まとめ:DCT vs DFT vs ウェーブレット
| 特性 | DCT(DCT-II) | DFT | ウェーブレット変換 |
|---|---|---|---|
| 出力 | 実数 | 複素数 | 実数(マルチスケール) |
| 計算量 | \(O(N \log N)\) | \(O(N \log N)\) | \(O(N)\) |
| エネルギー集中性 | 高(自然信号に最適) | 中 | 高(時間局在も考慮) |
| 境界処理 | 偶対称拡張(ギブスなし) | 周期拡張(ギブスあり) | 反射・ゼロパディング等 |
| 時間周波数局在 | なし(大域的) | なし(大域的) | あり(マルチスケール) |
| 主な応用 | JPEG, MP3, H.264 | スペクトル解析, 通信 | JPEG2000, 医用画像 |
| 逆変換 | DCT-III(実数) | IDFT(複素数) | 逆ウェーブレット変換 |
本記事では、離散コサイン変換(DCT-II)の数学的定義から実用的な応用まで体系的に解説しました。
- DCT-IIの定義: コサイン基底の重みつき和。正規化形式でパーセバルの等式が成立する
- エネルギー集中性: 偶対称拡張によりギブス現象を回避し、低周波への高い集中性を実現
- DFTとの関係: 偶拡張 + FFT + 位相補正で \(O(N \log N)\) の高速計算が可能
- 2D DCT: 分離可能性により行・列に順次1D DCTを適用。8×8ブロックDCTがJPEGの中核
- JPEG量子化: 視覚的な重要性に応じて高周波係数を粗く量子化し、高圧縮率を達成
- MDCT: オーバーラップ処理とTDACにより、MP3/AACで完全再構成を実現
関連記事
- 高速フーリエ変換(FFT)の仕組みとPython実装 — DCTと密接に関連するFFTのアルゴリズムを詳しく解説しています。
- 窓関数とパワースペクトル密度(PSD)の理論とPython実装 — 窓関数とスペクトル漏れをDFTとの対比で解説しています。
- ケプストラム分析の理論とPython実装 — DCTと同様にスペクトルの対数を扱う変換で、MFCCなど音声処理に応用されます。
- サンプリング定理とエイリアシングの理論とPython実装 — DCT係数をサンプリングする際の基礎となるナイキスト定理を解説しています。
参考文献
- Ahmed, N., Natarajan, T., & Rao, K. R. (1974). “Discrete cosine transform”. IEEE Transactions on Computers, C-23(1), 90-93.
- Wallace, G. K. (1991). “The JPEG still picture compression standard”. Communications of the ACM, 34(4), 30-44.
- Chen, W. H., Smith, C. H., & Fralick, S. C. (1977). “A fast computational algorithm for the discrete cosine transform”. IEEE Transactions on Communications, 25(9), 1004-1009.
- Oppenheim, A. V., & Schafer, R. W. (2009). Discrete-Time Signal Processing (3rd ed.). Prentice Hall.
- SciPy DCT documentation