離散コサイン変換(DCT)の理論とPython実装:JPEG・音声圧縮の基礎

離散コサイン変換(DCT)の数学的定義・エネルギー集中性・高速計算法から、JPEG画像圧縮・MP3音声圧縮への応用まで、Pythonで体系的に解説します。

はじめに

デジタルカメラで撮影した写真を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-IIDFT
基底関数コサイン(実数)複素指数
出力実数複素数
対称性偶対称拡張周期拡張
エネルギー集中性高い中程度
ギブス現象抑制発生しやすい
主な応用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年に標準化された静止画像圧縮フォーマットです。

圧縮手順の概略:

  1. 色空間変換: RGBからYCbCrへ変換(輝度・色差分離)
  2. クロマサブサンプリング: 人間の目が色差の細部に鈍感であることを利用
  3. 8×8ブロック分割: 画像を8×8ピクセルのブロックに分割
  4. レベルシフト: 各ピクセル値から128を引いて \([-128, 127]\) の範囲にシフト
  5. 2D DCT: 各ブロックに8×8 DCT-IIを適用
  6. 量子化: DCT係数を量子化テーブルで割り、整数に丸める(非可逆圧縮の本質
  7. エントロピー符号化: ジグザグスキャン後にハフマン符号化

量子化ステップは次式で表されます。

\[\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. 1/f特性: 自然画像のパワースペクトルは低周波ほど大きく(\(P(f) \propto 1/f^2\) )、この性質がDCT係数の低次集中と対応します。
  2. マルコフ性と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の重要な性質:

  1. 50%オーバーラップ: 連続するフレームが半分ずつ重なる
  2. TDAC(Time-Domain Aliasing Cancellation): フレーム間のエイリアシングが相殺され、完全再構成が可能
  3. 臨界サンプリング: \(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で完全再構成を実現

関連記事

参考文献

  • 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