画像の画質評価指標 SSIM/PSNR/Butteraugli 実践ガイド 2025

公開: 2025年9月19日 · 読了目安: 4 · 著者: Unified Image Tools 編集部

画像の画質評価指標 SSIM/PSNR/Butteraugli 実践ガイド 2025

画像圧縮リサイズ による「劣化」を客観的に比較したいとき、SSIM/PSNR/Butteraugli などの定量指標が強力な武器となります。しかし、これらの指標は適切な理解と使い方を知らないと、かえって誤った判断を招くことがあります。本稿では指標の特徴・読み解き方と、実務に落とし込む具体的な手順を体系的に解説します。

画質指標の必要性と限界

なぜ主観評価だけでは不十分なのか

人間の視覚による画質評価は個人差が大きく、疲労や環境光によって結果が変動します。また、大量の画像を処理する現代の Web 開発では、すべてを目視確認することは現実的ではありません。

しかし、機械的な指標にも以下のような限界があります:

  • コンテキスト無視: 画像の用途(アイコン vs 写真)を考慮しない
  • 知覚とのズレ: 数値的に優秀でも視覚的に劣る場合がある
  • 局所的評価: 画像全体の印象より、ピクセル単位の差異を重視

Note: 指標は万能ではありません。複数を組み合わせ、実画面の確認と併用することで信頼性が上がります。

主要指標の詳細解説

PSNR(Peak Signal-to-Noise Ratio)

基本概念: 原画像と比較画像の画素値差を信号対雑音比で表現

import cv2
import numpy as np

def calculate_psnr(original, compressed):
    mse = np.mean((original - compressed) ** 2)
    if mse == 0:
        return float('inf')
    
    max_pixel = 255.0
    psnr = 20 * np.log10(max_pixel / np.sqrt(mse))
    return psnr

# 使用例
original = cv2.imread('original.jpg')
compressed = cv2.imread('compressed.jpg')
psnr_value = calculate_psnr(original, compressed)
print(f"PSNR: {psnr_value:.2f} dB")

解釈の指針:

  • 30dB以下: 明らかな劣化、実用性に疑問
  • 30-35dB: 許容範囲だが注意深い確認が必要
  • 35-40dB: 良好、多くの用途で問題なし
  • 40dB以上: 優秀、原画像との差は最小限

適用場面: 技術文書の図表、ロゴ、線画など、エッジが重要な画像

注意点: 平均的な差異しか捉えられず、局所的な劣化(文字のボケなど)を見落とす可能性

SSIM(Structural Similarity Index)

基本概念: 輝度・コントラスト・構造の3要素から類似度を算出

from skimage.metrics import structural_similarity as ssim
import cv2

def calculate_ssim(original, compressed):
    # グレースケール変換
    gray_original = cv2.cvtColor(original, cv2.COLOR_BGR2GRAY)
    gray_compressed = cv2.cvtColor(compressed, cv2.COLOR_BGR2GRAY)
    
    # SSIM 計算
    ssim_value, diff = ssim(gray_original, gray_compressed, full=True)
    
    # 差分マップを可視化
    diff = (diff * 255).astype(np.uint8)
    
    return ssim_value, diff

# 使用例
ssim_value, diff_map = calculate_ssim(original, compressed)
print(f"SSIM: {ssim_value:.4f}")

# 差分の大きい領域を特定
cv2.imwrite('ssim_diff.jpg', diff_map)

解釈の指針:

  • 0.95以上: ほぼ区別不可能
  • 0.90-0.95: 高品質、実用上問題なし
  • 0.80-0.90: 許容範囲、用途によって判断
  • 0.80未満: 劣化が目立つ、要注意

適用場面: 写真、自然画像、テクスチャが豊富な画像

強み: 人間の視覚特性に近い評価、局所的な構造変化を検出

Butteraugli(Google開発)

基本概念: 人間視覚モデルに基づく知覚的距離測定

# Butteraugli のインストール(Ubuntu/Debian)
sudo apt-get install libjpeg-dev libpng-dev
git clone https://github.com/google/butteraugli.git
cd butteraugli
make

# 使用例
./butteraugli original.jpg compressed.jpg

特徴:

  • 色差に敏感(特にブルーチャンネル)
  • 空間周波数特性を考慮
  • エッジ周辺の劣化を重点評価

解釈の指針:

  • 1.0未満: 優秀、差はほぼ知覚されない
  • 1.0-1.5: 良好、注意深く見れば差が分かる程度
  • 1.5-3.0: 許容範囲、明らかな差があるが実用可能
  • 3.0以上: 劣化が顕著、品質改善が必要

公正比較のための前処理標準化

1. 色空間の統一

def normalize_colorspace(image_path, target_colorspace='sRGB'):
    """画像を指定色空間に統一"""
    import cv2
    
    img = cv2.imread(image_path)
    
    # 一般的には sRGB への変換
    if target_colorspace == 'sRGB':
        # すでに sRGB の場合はそのまま
        return img
    
    # 必要に応じて色空間変換
    # 詳細は色管理記事を参照
    return img

詳細な色空間管理については 色管理と ICC 運用ガイド を参照してください。

2. 解像度とアスペクト比の調整

def standardize_resolution(original, compressed, target_size=None):
    """比較用に解像度を統一"""
    if target_size is None:
        # 小さい方に合わせる
        h1, w1 = original.shape[:2]
        h2, w2 = compressed.shape[:2]
        target_size = (min(w1, w2), min(h1, h2))
    
    original_resized = cv2.resize(original, target_size, cv2.INTER_LANCZOS4)
    compressed_resized = cv2.resize(compressed, target_size, cv2.INTER_LANCZOS4)
    
    return original_resized, compressed_resized

3. ビット深度の正規化

8bit/16bit の混在や、異なるガンマカーブを持つ画像の比較では、事前の正規化が必須です。

実践的なワークフロー設計

バッチ評価スクリプト

import os
import csv
from pathlib import Path

def batch_quality_assessment(original_dir, compressed_dir, output_csv):
    """ディレクトリ内の画像を一括評価"""
    results = []
    
    for original_path in Path(original_dir).glob('*.jpg'):
        compressed_path = Path(compressed_dir) / original_path.name
        
        if not compressed_path.exists():
            continue
            
        try:
            # 画像読み込み
            original = cv2.imread(str(original_path))
            compressed = cv2.imread(str(compressed_path))
            
            # 解像度統一
            original, compressed = standardize_resolution(original, compressed)
            
            # 各指標を計算
            psnr_val = calculate_psnr(original, compressed)
            ssim_val, _ = calculate_ssim(original, compressed)
            
            # ファイルサイズ情報
            original_size = original_path.stat().st_size
            compressed_size = compressed_path.stat().st_size
            compression_ratio = compressed_size / original_size
            
            results.append({
                'filename': original_path.name,
                'psnr': psnr_val,
                'ssim': ssim_val,
                'compression_ratio': compression_ratio,
                'original_size_kb': original_size // 1024,
                'compressed_size_kb': compressed_size // 1024
            })
            
        except Exception as e:
            print(f"Error processing {original_path.name}: {e}")
    
    # CSV出力
    with open(output_csv, 'w', newline='') as csvfile:
        fieldnames = ['filename', 'psnr', 'ssim', 'compression_ratio', 
                     'original_size_kb', 'compressed_size_kb']
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(results)
    
    return results

CI/CD パイプラインでの自動品質チェック

# GitHub Actions での品質回帰検知
name: Image Quality Regression Test

on:
  pull_request:
    paths:
      - 'assets/images/**'

jobs:
  quality-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Set up Python
        uses: actions/setup-python@v3
        with:
          python-version: '3.9'
      
      - name: Install dependencies
        run: |
          pip install opencv-python scikit-image numpy
      
      - name: Quality assessment
        run: |
          python scripts/quality_check.py \
            --baseline assets/baseline \
            --current assets/images \
            --threshold-ssim 0.90 \
            --threshold-psnr 30.0
      
      - name: Upload quality report
        uses: actions/upload-artifact@v3
        with:
          name: quality-report
          path: quality_report.csv

画像種別に応じた評価戦略

写真・自然画像

  • 主指標: SSIM(構造保持が重要)
  • 補助指標: Butteraugli(知覚的品質)
  • 重要領域: 人物の顔、重要な被写体
  • 閾値: SSIM > 0.90, Butteraugli < 1.5

ロゴ・アイコン

  • 主指標: PSNR(エッジの鮮明さが重要)
  • 補助指標: SSIM(構造変化の検出)
  • 重要領域: 文字、細い線、境界部分
  • 閾値: PSNR > 35dB, SSIM > 0.95

グラフ・チャート

  • 主指標: PSNR(数値の読み取り精度)
  • 補助指標: 局所的な SSIM(軸ラベル部分)
  • 重要領域: 軸、文字、データポイント
  • 閾値: PSNR > 40dB(可読性確保)

よくある落とし穴と対策

1. 異なる圧縮形式の不公正比較

# 悪い例: 形式が異なる画像の直接比較
jpeg_img = cv2.imread('image.jpg')      # 8bit
png_img = cv2.imread('image.png')       # 8bit だが可逆圧縮

# 良い例: 基準形式での再保存後に比較
def fair_format_comparison(img1_path, img2_path):
    # 一旦 PNG で統一保存
    img1 = cv2.imread(img1_path)
    img2 = cv2.imread(img2_path)
    
    cv2.imwrite('temp1.png', img1)
    cv2.imwrite('temp2.png', img2)
    
    # 再読み込みして比較
    norm1 = cv2.imread('temp1.png')
    norm2 = cv2.imread('temp2.png')
    
    return calculate_ssim(norm1, norm2)

2. サンプル画像の偏り

多様なコンテンツタイプ(人物、風景、グラフィック、テキスト)での評価が必要です。

3. 局所的劣化の見落とし

def regional_quality_analysis(original, compressed, region_size=64):
    """画像を小領域に分割して局所的品質を評価"""
    h, w = original.shape[:2]
    regional_scores = []
    
    for y in range(0, h - region_size, region_size):
        for x in range(0, w - region_size, region_size):
            # 小領域を切り出し
            region_orig = original[y:y+region_size, x:x+region_size]
            region_comp = compressed[y:y+region_size, x:x+region_size]
            
            # 局所 SSIM を計算
            local_ssim = ssim(
                cv2.cvtColor(region_orig, cv2.COLOR_BGR2GRAY),
                cv2.cvtColor(region_comp, cv2.COLOR_BGR2GRAY)
            )
            
            regional_scores.append({
                'x': x, 'y': y,
                'ssim': local_ssim
            })
    
    return regional_scores

高度な評価手法

1. 知覚的重要度重み付け

def perceptual_weighted_assessment(original, compressed, saliency_map):
    """視覚的重要度に基づく重み付き評価"""
    
    # 基本 SSIM 計算
    base_ssim = ssim(original, compressed)
    
    # 重要領域での SSIM
    important_regions = saliency_map > 0.7
    
    if np.any(important_regions):
        important_ssim = ssim(
            original[important_regions],
            compressed[important_regions]
        )
        
        # 重み付き平均
        weighted_ssim = 0.7 * important_ssim + 0.3 * base_ssim
        return weighted_ssim
    
    return base_ssim

2. 時系列での品質追跡

class QualityTracker:
    def __init__(self):
        self.history = []
    
    def track_compression_chain(self, original, steps):
        """多段階圧縮での品質劣化を追跡"""
        current = original.copy()
        
        for i, (method, params) in enumerate(steps):
            # 圧縮実行
            compressed = apply_compression(current, method, params)
            
            # 品質測定
            quality_metrics = {
                'step': i,
                'method': method,
                'psnr': calculate_psnr(original, compressed),
                'ssim': calculate_ssim(original, compressed)[0],
                'size': get_compressed_size(compressed, method)
            }
            
            self.history.append(quality_metrics)
            current = compressed
        
        return self.history

結果の可視化と解釈

効率曲線(Rate-Distortion Curve)

import matplotlib.pyplot as plt

def plot_rate_distortion(quality_data):
    """品質 vs ファイルサイズの効率曲線を描画"""
    
    compression_ratios = [d['compression_ratio'] for d in quality_data]
    ssim_scores = [d['ssim'] for d in quality_data]
    
    plt.figure(figsize=(10, 6))
    plt.scatter(compression_ratios, ssim_scores, alpha=0.7)
    plt.xlabel('圧縮率(ファイルサイズ比)')
    plt.ylabel('SSIM スコア')
    plt.title('圧縮効率曲線')
    
    # 効率的な設定を強調
    efficient_points = find_pareto_frontier(compression_ratios, ssim_scores)
    plt.plot(*efficient_points, 'r-', linewidth=2, label='効率フロンティア')
    
    plt.legend()
    plt.grid(True, alpha=0.3)
    plt.show()

ヒートマップによる劣化可視化

比較スライダーツール と組み合わせて、劣化の分布を直感的に把握できます。

関連技術との統合

レスポンシブ画像生成との連携

レスポンシブ画像設計 で異なるサイズバリアントを生成する際、各サイズでの品質指標を監視し、適切な品質レベルを維持します。

自動最適化パイプライン

def adaptive_quality_optimization(image_path, target_ssim=0.92):
    """目標品質を達成する最適な圧縮設定を自動探索"""
    
    original = cv2.imread(image_path)
    
    # 品質設定の候補
    quality_candidates = range(60, 95, 5)
    
    best_setting = None
    best_size = float('inf')
    
    for quality in quality_candidates:
        # 圧縮テスト
        compressed = compress_image(original, quality=quality)
        
        # 品質評価
        current_ssim = calculate_ssim(original, compressed)[0]
        
        if current_ssim >= target_ssim:
            current_size = get_compressed_size(compressed)
            if current_size < best_size:
                best_size = current_size
                best_setting = quality
    
    return best_setting

まとめ

画質評価指標は、適切に理解し使い分けることで、圧縮リサイズの最適化において強力な武器となります。重要なポイントは:

  1. 指標の特性理解: PSNR(エッジ重視)、SSIM(構造重視)、Butteraugli(知覚重視)の使い分け
  2. 前処理の標準化: 公正な比較のための色空間・解像度統一
  3. 用途に応じた閾値: 画像の種類とコンテキストに適した基準設定
  4. 複合的評価: 単一指標に頼らず、複数の視点からの総合判断
  5. 継続的監視: CI/CD での自動品質チェックと回帰防止

これらの実践により、「数値的に良いが見た目が悪い」「主観的には良いが定量的に問題」といった判断ミスを防ぎ、安定した高品質の画像配信を実現できます。画像圧縮戦略 と組み合わせることで、より効果的な最適化が可能になります。

関連記事