画像の画質評価指標 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
まとめ
画質評価指標は、適切に理解し使い分けることで、圧縮とリサイズの最適化において強力な武器となります。重要なポイントは:
- 指標の特性理解: PSNR(エッジ重視)、SSIM(構造重視)、Butteraugli(知覚重視)の使い分け
- 前処理の標準化: 公正な比較のための色空間・解像度統一
- 用途に応じた閾値: 画像の種類とコンテキストに適した基準設定
- 複合的評価: 単一指標に頼らず、複数の視点からの総合判断
- 継続的監視: CI/CD での自動品質チェックと回帰防止
これらの実践により、「数値的に良いが見た目が悪い」「主観的には良いが定量的に問題」といった判断ミスを防ぎ、安定した高品質の画像配信を実現できます。画像圧縮戦略 と組み合わせることで、より効果的な最適化が可能になります。